diff --git a/CHANGELOG.md b/CHANGELOG.md index b26cd3a5bfa6..c3dccdede409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Configure chokidar's `awaitWriteFinish` setting to avoid occasional stale builds on Windows ([#5774](https://github.com/tailwindlabs/tailwindcss/pull/5774)) - Fix CLI `--content` option ([#5775](https://github.com/tailwindlabs/tailwindcss/pull/5775)) - Fix before/after utilities overriding custom content values at larger breakpoints ([#5820](https://github.com/tailwindlabs/tailwindcss/pull/5820)) +- Cleanup duplicate properties ([#5830](https://github.com/tailwindlabs/tailwindcss/pull/5830)) ## [3.0.0-alpha.1] - 2021-10-01 diff --git a/src/lib/collapseDuplicateDeclarations.js b/src/lib/collapseDuplicateDeclarations.js new file mode 100644 index 000000000000..9164f822ea78 --- /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 index 98bd63281e0e..8a6d22dae744 100644 --- 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) } } diff --git a/tests/apply.test.css b/tests/apply.test.css index 08bcbf5b8ef0..485fff0bc429 100644 --- 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 index 172f50b07a71..02be6d633911 100644 --- 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 index 6b07157b2fb9..b85b407254c0 100644 --- 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 index 7fb1e7de0b89..f580afce6abd 100644 --- 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); }