From 9c340832b2a1e9c7859bc5717af04530fa088301 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 27 Aug 2025 14:34:06 -0400 Subject: [PATCH 1/4] Refactor --- packages/tailwindcss/src/ast.ts | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index e4b5d5a9ab52..b6ccc4731c92 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -351,26 +351,19 @@ export function optimizeAst( // Rule else if (node.kind === 'rule') { - // Rules with `&` as the selector should be flattened - if (node.selector === '&') { - for (let child of node.nodes) { - let nodes: AstNode[] = [] - transform(child, nodes, context, depth + 1) - if (nodes.length > 0) { - parent.push(...nodes) - } - } + let nodes: AstNode[] = [] + + for (let child of node.nodes) { + transform(child, nodes, context, depth + 1) } - // - else { - let copy = { ...node, nodes: [] } - for (let child of node.nodes) { - transform(child, copy.nodes, context, depth + 1) - } - if (copy.nodes.length > 0) { - parent.push(copy) - } + if (nodes.length === 0) return + + // Rules with `&` as the selector should be flattened + if (node.selector === '&') { + parent.push(...nodes) + } else { + parent.push({ ...node, nodes }) } } From 46600372236c95444d55c2b5c0ba4f9cb4e463e4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 27 Aug 2025 14:35:57 -0400 Subject: [PATCH 2/4] Remove duplicate declarations in the same rule Nested rules with an `&` are already flattened by this point so their content is also considered to be part of the same rule --- packages/tailwindcss/src/ast.test.ts | 87 ++++++++++++++++++++++++++++ packages/tailwindcss/src/ast.ts | 25 ++++++++ 2 files changed, 112 insertions(+) diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index 9f879d64b10a..865121c694d4 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -202,6 +202,93 @@ it('should not emit empty rules once optimized', () => { `) }) +it('should not emit exact duplicate declarations in the same rule', () => { + let ast = CSS.parse(css` + .foo { + color: red; + .bar { + color: green; + color: blue; + color: green; + } + color: red; + } + .foo { + color: red; + & { + color: green; + & { + color: red; + color: green; + color: blue; + } + color: red; + } + background: blue; + .bar { + color: green; + color: blue; + color: green; + } + caret-color: orange; + } + `) + + expect(toCss(ast)).toMatchInlineSnapshot(` + ".foo { + color: red; + .bar { + color: green; + color: blue; + color: green; + } + color: red; + } + .foo { + color: red; + & { + color: green; + & { + color: red; + color: green; + color: blue; + } + color: red; + } + background: blue; + .bar { + color: green; + color: blue; + color: green; + } + caret-color: orange; + } + " + `) + + expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` + ".foo { + .bar { + color: blue; + color: green; + } + color: red; + } + .foo { + color: green; + color: blue; + color: red; + background: blue; + .bar { + color: blue; + color: green; + } + caret-color: orange; + } + " + `) +}) + it('should only visit children once when calling `replaceWith` with single element array', () => { let visited = new Set() diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index b6ccc4731c92..a473b9193e7f 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -357,6 +357,31 @@ export function optimizeAst( transform(child, nodes, context, depth + 1) } + // Keep the last decl when there are exact duplicates. Keeping the *first* one might + // not be correct when given nested rules where a rule sits between declarations. + let seen: Record = {} + let toRemove = new Set() + + // Keep track of all nodes that produce a given declaration + for (let child of nodes) { + if (child.kind !== 'declaration') continue + + let key = `${child.property}:${child.value}:${child.important}` + seen[key] ??= [] + seen[key].push(child) + } + + // And remove all but the last of each + for (let key in seen) { + for (let i = 0; i < seen[key].length - 1; ++i) { + toRemove.add(seen[key][i]) + } + } + + if (toRemove.size > 0) { + nodes = nodes.filter((node) => !toRemove.has(node)) + } + if (nodes.length === 0) return // Rules with `&` as the selector should be flattened From 2b5ff745e84ab0acf197abc55f7f8b41bfbb2f2a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 27 Aug 2025 14:46:41 -0400 Subject: [PATCH 3/4] Update tests --- packages/tailwindcss/src/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index a19fcf96d979..cbdb88186dc9 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -527,7 +527,6 @@ describe('@apply', () => { .foo, .bar { --tw-content: "b"; content: var(--tw-content); - content: var(--tw-content); } @property --tw-content { From bff4c7b025a1457a31da65fc194d4a349319ef01 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 27 Aug 2025 15:12:40 -0400 Subject: [PATCH 4/4] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d19e60de1a94..ff8484795fd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Drop warning from browser build ([#18731](https://github.com/tailwindlabs/tailwindcss/issues/18731)) +- Drop exact duplicate declarations when emitting CSS ([#18809](https://github.com/tailwindlabs/tailwindcss/issues/18809)) ### Fixed