From d73a1874d4a9f32e7f8fd5aa732218b8d19fb71e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 15:09:07 +0100 Subject: [PATCH 1/9] do not emit empty rules The first occurrence is for `& {}`, which is an edge case already The second occurrence is for normal rules that are empty --- packages/tailwindcss/src/ast.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 6052724f33b9..54eefe395368 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -261,7 +261,9 @@ export function optimizeAst(ast: AstNode[]) { for (let child of node.nodes) { let nodes: AstNode[] = [] transform(child, nodes, depth + 1) - parent.push(...nodes) + if (nodes.length > 0) { + parent.push(...nodes) + } } } @@ -271,7 +273,9 @@ export function optimizeAst(ast: AstNode[]) { for (let child of node.nodes) { transform(child, copy.nodes, depth + 1) } - parent.push(copy) + if (copy.nodes.length > 0) { + parent.push(copy) + } } } From 3d8db39d55a700d2def727db3fca905fd14dcdc6 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 15:10:37 +0100 Subject: [PATCH 2/9] do not emit empty at-rules There are some exceptions to this rule. Some at-rules can be body-less: ```css @charset "UTF-8"; @layer foo, bar, baz; @custom-media --modern (color), (hover); ``` --- packages/tailwindcss/src/ast.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 54eefe395368..86f064bb1c9b 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -301,7 +301,14 @@ export function optimizeAst(ast: AstNode[]) { for (let child of node.nodes) { transform(child, copy.nodes, depth + 1) } - parent.push(copy) + if ( + copy.nodes.length > 0 || + copy.name === '@layer' || + copy.name === '@charset' || + copy.name === '@custom-media' + ) { + parent.push(copy) + } } // AtRoot From e63595473fae71359f713e8f4092d5aa82cbea78 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 15:11:33 +0100 Subject: [PATCH 3/9] update tests to remove empty rules and at-rules --- packages/tailwindcss/src/compat/plugin-api.test.ts | 2 -- packages/tailwindcss/src/compat/screens-config.test.ts | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 4502118f458f..9eb65ccb954a 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -3167,8 +3167,6 @@ describe('addUtilities()', () => { color: red; } } - } - :root, :host { }" `, ) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 09ab97fc9f8a..e8f96ee55fd3 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -655,9 +655,5 @@ test('JS config `screens` can overwrite default CSS `--breakpoint-*`', async () // currently. expect( compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-2xl:flex']), - ).toMatchInlineSnapshot(` - ":root, :host { - } - " - `) + ).toMatchInlineSnapshot(`""`) }) From 356d0c52f2af510af0926c04d8752ef5fc0137ef Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 15:17:29 +0100 Subject: [PATCH 4/9] add dedicated optimization test --- packages/tailwindcss/src/ast.test.ts | 87 ++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index c3d9b2f159bf..5bfb3d23981d 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -2,6 +2,8 @@ import { expect, it } from 'vitest' import { context, decl, optimizeAst, styleRule, toCss, walk, WalkAction } from './ast' import * as CSS from './css-parser' +const css = String.raw + it('should pretty print an AST', () => { expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}')))) .toMatchInlineSnapshot(` @@ -95,3 +97,88 @@ it('should stop walking when returning `WalkAction.Stop`', () => { } `) }) + +it('should not emit empty rules once optimized', () => { + let ast = CSS.parse(css` + /* Empty rule */ + .foo { + } + + /* Empty rule, with nesting */ + .foo { + .bar { + } + .baz { + } + } + + /* Empty rule, with special case '&' rules */ + .foo { + & { + &:hover { + } + &:focus { + } + } + } + + /* Empty at-rule */ + @media (min-width: 768px) { + } + + /* Empty at-rule with nesting*/ + @media (min-width: 768px) { + .foo { + } + + @media (min-width: 1024px) { + .bar { + } + } + } + + /* Exceptions: */ + @charset 'UTF-8'; + @layer foo, bar, baz; + @custom-media --modern (color), (hover); + `) + + expect(toCss(ast)).toMatchInlineSnapshot(` + ".foo { + } + .foo { + .bar { + } + .baz { + } + } + .foo { + & { + &:hover { + } + &:focus { + } + } + } + @media (min-width: 768px); + @media (min-width: 768px) { + .foo { + } + @media (min-width: 1024px) { + .bar { + } + } + } + @charset 'UTF-8'; + @layer foo, bar, baz; + @custom-media --modern (color), (hover); + " + `) + + expect(toCss(optimizeAst(ast))).toMatchInlineSnapshot(` + "@charset 'UTF-8'; + @layer foo, bar, baz; + @custom-media --modern (color), (hover); + " + `) +}) From 268487d47b7ae472bb3107296a29bf1c5006ebc4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 15:21:58 +0100 Subject: [PATCH 5/9] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 114412e42d3c..45e54de0830b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prevent camelCasing CSS custom properties added by JavaScript plugins ([#16103](https://github.com/tailwindlabs/tailwindcss/pull/16103)) - Do not emit `@keyframes` in `@theme reference` ([#16120](https://github.com/tailwindlabs/tailwindcss/pull/16120)) - Discard invalid declarations when parsing CSS ([#16093](https://github.com/tailwindlabs/tailwindcss/pull/16093)) +- Do not emit empty CSS rules and at-rules ([#16121](https://github.com/tailwindlabs/tailwindcss/pull/16121)) ## [4.0.1] - 2025-01-29 From 2396d9cec870353f6dbad44f933bdc2e3478af93 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 15:25:31 +0100 Subject: [PATCH 6/9] remove empty rules from integration tests --- integrations/cli/index.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index 2e6b9ad03549..0e3aba4c4e88 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -368,12 +368,7 @@ describe.each([ `, ) - await fs.expectFileToContain('project-a/dist/out.css', [ - css` - :root, :host { - } - `, - ]) + await fs.expectFileToContain('project-a/dist/out.css', [css``]) }, ) From f7f38a84b5b57497763912d887e59e0692a194d5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 15:39:53 +0100 Subject: [PATCH 7/9] Update packages/tailwindcss/src/ast.ts Co-authored-by: Jordan Pittman --- packages/tailwindcss/src/ast.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 86f064bb1c9b..7f67fdda5f57 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -305,7 +305,8 @@ export function optimizeAst(ast: AstNode[]) { copy.nodes.length > 0 || copy.name === '@layer' || copy.name === '@charset' || - copy.name === '@custom-media' + copy.name === '@custom-media' || + copy.name === '@namespace' ) { parent.push(copy) } From 92c2d1c359b90c535333781080343867305509c0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 16:01:14 +0100 Subject: [PATCH 8/9] add `@namespace` edge case to tests Co-authored-by: Jordan Pittman --- packages/tailwindcss/src/ast.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index 5bfb3d23981d..4ade0f7bf9eb 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -141,6 +141,7 @@ it('should not emit empty rules once optimized', () => { @charset 'UTF-8'; @layer foo, bar, baz; @custom-media --modern (color), (hover); + @namespace 'http://www.w3.org/1999/xhtml'; `) expect(toCss(ast)).toMatchInlineSnapshot(` @@ -172,6 +173,7 @@ it('should not emit empty rules once optimized', () => { @charset 'UTF-8'; @layer foo, bar, baz; @custom-media --modern (color), (hover); + @namespace 'http://www.w3.org/1999/xhtml'; " `) @@ -179,6 +181,7 @@ it('should not emit empty rules once optimized', () => { "@charset 'UTF-8'; @layer foo, bar, baz; @custom-media --modern (color), (hover); + @namespace 'http://www.w3.org/1999/xhtml'; " `) }) From 8e6fe3b189126ea4f7121a86004e11e4a55193c5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 31 Jan 2025 16:12:07 +0100 Subject: [PATCH 9/9] use double quotes for `@charset` --- packages/tailwindcss/src/ast.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index 4ade0f7bf9eb..174971ea541d 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -138,7 +138,7 @@ it('should not emit empty rules once optimized', () => { } /* Exceptions: */ - @charset 'UTF-8'; + @charset "UTF-8"; @layer foo, bar, baz; @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @@ -170,7 +170,7 @@ it('should not emit empty rules once optimized', () => { } } } - @charset 'UTF-8'; + @charset "UTF-8"; @layer foo, bar, baz; @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @@ -178,7 +178,7 @@ it('should not emit empty rules once optimized', () => { `) expect(toCss(optimizeAst(ast))).toMatchInlineSnapshot(` - "@charset 'UTF-8'; + "@charset "UTF-8"; @layer foo, bar, baz; @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml';