Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
[JIT] apply: duplicate transform properties
**What version of Tailwind CSS are you using?**

2.2.15 (also tested with 3.0.0-alpha.1 and 2.2.17)

**Reproduction URL**

https://play.tailwindcss.com/KkuBVniu30?file=css

**Describe your issue**

Using multiple transform-based properties (like translate-x and translate-y) results in duplicated `transform` property, eg

```css
.absolute-center {
  @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
}

```

Generates:

```css
.absolute-center {
    position: absolute;
    top: 50%;
    left: 50%;
    --tw-translate-x: -50%;
    transform: var(--tw-transform);
    --tw-translate-y: -50%;
    transform: var(--tw-transform);
    transform: var(--tw-transform);
}
```


Hints:

Patch:
diff --git a/src/lib/collapseDuplicateDeclarations.js b/src/lib/collapseDuplicateDeclarations.js
new file mode 100644
--- /dev/null
+++ b/src/lib/collapseDuplicateDeclarations.js
@@ -0,0 +1,28 @@
+export default function collapseDuplicateDeclarations() {
+  return (root) => {
+    root.walkRules((node) => {
+      let seen = new Map()
+      let droppable = new Set([])
+
+      node.walkDecls((decl) => {
+        // This could happen if we have nested selectors. In that case the
+        // parent will loop over all its declarations but also the declarations
+        // of nested rules. With this we ensure that we are shallowly checking
+        // declarations.
+        if (decl.parent !== node) {
+          return
+        }
+
+        if (seen.has(decl.prop)) {
+          droppable.add(seen.get(decl.prop))
+        }
+
+        seen.set(decl.prop, decl)
+      })
+
+      for (let decl of droppable) {
+        decl.remove()
+      }
+    })
+  }
+}
diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js
--- a/src/processTailwindFeatures.js
+++ b/src/processTailwindFeatures.js
@@ -5,6 +5,7 @@ import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions'
 import substituteScreenAtRules from './lib/substituteScreenAtRules'
 import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
 import collapseAdjacentRules from './lib/collapseAdjacentRules'
+import collapseDuplicateDeclarations from './lib/collapseDuplicateDeclarations'
 import detectNesting from './lib/detectNesting'
 import { createContext } from './lib/setupContextUtils'
 import { issueFlagNotices } from './featureFlags'
@@ -42,5 +43,6 @@ export default function processTailwindFeatures(setupContext) {
     substituteScreenAtRules(context)(root, result)
     resolveDefaultsAtRules(context)(root, result)
     collapseAdjacentRules(context)(root, result)
+    collapseDuplicateDeclarations(context)(root, result)
   }
 }


Test patch:
diff --git a/tests/apply.test.css b/tests/apply.test.css
--- a/tests/apply.test.css
+++ b/tests/apply.test.css
@@ -10,8 +10,6 @@
 .class-order {
   padding: 2rem;
   padding-left: 0.75rem;
-  padding-right: 0.75rem;
-  padding-top: 1.75rem;
   padding-bottom: 1.75rem;
   padding-top: 1rem;
   padding-right: 0.25rem;
@@ -127,7 +125,6 @@
 /* TODO: This works but the generated CSS is unnecessarily verbose. */
 .complex-utilities {
   --tw-ordinal: ordinal;
-  font-variant-numeric: var(--tw-font-variant-numeric);
   --tw-numeric-spacing: tabular-nums;
   font-variant-numeric: var(--tw-font-variant-numeric);
   --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05);
@@ -155,7 +152,6 @@
   font-weight: 700;
 }
 .use-dependant-only-b {
-  font-weight: 700;
   font-weight: 400;
 }
 .btn {
diff --git a/tests/apply.test.js b/tests/apply.test.js
--- a/tests/apply.test.js
+++ b/tests/apply.test.js
@@ -351,7 +351,6 @@ test('@applying classes from outside a @layer respects the source order', async
   await run(input, config).then((result) => {
     return expect(result.css).toMatchFormattedCss(css`
       .baz {
-        text-decoration: underline;
         text-decoration: none;
       }
 
@@ -402,3 +401,30 @@ test('@applying classes from outside a @layer respects the source order', async
     `)
   })
 })
+
+it('should remove duplicate properties when using apply with similar properties', () => {
+  let config = {
+    content: [{ raw: 'foo' }],
+  }
+
+  let input = css`
+    @tailwind utilities;
+
+    .foo {
+      @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
+    }
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        --tw-translate-x: -50%;
+        --tw-translate-y: -50%;
+        transform: var(--tw-transform);
+      }
+    `)
+  })
+})
diff --git a/tests/custom-plugins.test.js b/tests/custom-plugins.test.js
--- a/tests/custom-plugins.test.js
+++ b/tests/custom-plugins.test.js
@@ -868,7 +868,6 @@ test('when important is a selector it scopes all selectors in a rule, even thoug
       #app .custom-rotate-90,
       #app .custom-rotate-1\/4 {
         transform: rotate(90deg);
-        transform: rotate(90deg);
       }
     `)
   })
@@ -952,7 +951,6 @@ test('all selectors in a rule are prefixed', () => {
       .tw-btn-blue,
       .tw-btn-red {
         padding: 10px;
-        padding: 10px;
       }
     `)
   })
diff --git a/tests/kitchen-sink.test.css b/tests/kitchen-sink.test.css
--- a/tests/kitchen-sink.test.css
+++ b/tests/kitchen-sink.test.css
@@ -199,7 +199,6 @@ div {
 }
 .test-apply-font-variant {
   --tw-ordinal: ordinal;
-  font-variant-numeric: var(--tw-font-variant-numeric);
   --tw-numeric-spacing: tabular-nums;
   font-variant-numeric: var(--tw-font-variant-numeric);
 }