Skip to content

Commit 7f1d097

Browse files
Do not emit empty rules/at-rules (#16121)
This PR is an optimization where it will not emit empty rules and at-rules. I noticed this while working on #16120 where we emitted: ```css :root, :host { } ``` There are some exceptions for "empty" at-rules, such as: ```css @charset "UTF-8"; @layer foo, bar, baz; @Custom-Media --modern (color), (hover); @namespace "http://www.w3.org/1999/xhtml"; ``` These don't have a body, but they still have a purpose and therefore they will be emitted. However, if you look at this: ```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 { } } } ``` None of these will be emitted. --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
1 parent 35a5e8c commit 7f1d097

File tree

6 files changed

+108
-16
lines changed

6 files changed

+108
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Prevent camelCasing CSS custom properties added by JavaScript plugins ([#16103](https://github.com/tailwindlabs/tailwindcss/pull/16103))
2121
- Do not emit `@keyframes` in `@theme reference` ([#16120](https://github.com/tailwindlabs/tailwindcss/pull/16120))
2222
- Discard invalid declarations when parsing CSS ([#16093](https://github.com/tailwindlabs/tailwindcss/pull/16093))
23+
- Do not emit empty CSS rules and at-rules ([#16121](https://github.com/tailwindlabs/tailwindcss/pull/16121))
2324

2425
## [4.0.1] - 2025-01-29
2526

integrations/cli/index.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,7 @@ describe.each([
368368
`,
369369
)
370370

371-
await fs.expectFileToContain('project-a/dist/out.css', [
372-
css`
373-
:root, :host {
374-
}
375-
`,
376-
])
371+
await fs.expectFileToContain('project-a/dist/out.css', [css``])
377372
},
378373
)
379374

packages/tailwindcss/src/ast.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { expect, it } from 'vitest'
22
import { context, decl, optimizeAst, styleRule, toCss, walk, WalkAction } from './ast'
33
import * as CSS from './css-parser'
44

5+
const css = String.raw
6+
57
it('should pretty print an AST', () => {
68
expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}'))))
79
.toMatchInlineSnapshot(`
@@ -95,3 +97,91 @@ it('should stop walking when returning `WalkAction.Stop`', () => {
9597
}
9698
`)
9799
})
100+
101+
it('should not emit empty rules once optimized', () => {
102+
let ast = CSS.parse(css`
103+
/* Empty rule */
104+
.foo {
105+
}
106+
107+
/* Empty rule, with nesting */
108+
.foo {
109+
.bar {
110+
}
111+
.baz {
112+
}
113+
}
114+
115+
/* Empty rule, with special case '&' rules */
116+
.foo {
117+
& {
118+
&:hover {
119+
}
120+
&:focus {
121+
}
122+
}
123+
}
124+
125+
/* Empty at-rule */
126+
@media (min-width: 768px) {
127+
}
128+
129+
/* Empty at-rule with nesting*/
130+
@media (min-width: 768px) {
131+
.foo {
132+
}
133+
134+
@media (min-width: 1024px) {
135+
.bar {
136+
}
137+
}
138+
}
139+
140+
/* Exceptions: */
141+
@charset "UTF-8";
142+
@layer foo, bar, baz;
143+
@custom-media --modern (color), (hover);
144+
@namespace 'http://www.w3.org/1999/xhtml';
145+
`)
146+
147+
expect(toCss(ast)).toMatchInlineSnapshot(`
148+
".foo {
149+
}
150+
.foo {
151+
.bar {
152+
}
153+
.baz {
154+
}
155+
}
156+
.foo {
157+
& {
158+
&:hover {
159+
}
160+
&:focus {
161+
}
162+
}
163+
}
164+
@media (min-width: 768px);
165+
@media (min-width: 768px) {
166+
.foo {
167+
}
168+
@media (min-width: 1024px) {
169+
.bar {
170+
}
171+
}
172+
}
173+
@charset "UTF-8";
174+
@layer foo, bar, baz;
175+
@custom-media --modern (color), (hover);
176+
@namespace 'http://www.w3.org/1999/xhtml';
177+
"
178+
`)
179+
180+
expect(toCss(optimizeAst(ast))).toMatchInlineSnapshot(`
181+
"@charset "UTF-8";
182+
@layer foo, bar, baz;
183+
@custom-media --modern (color), (hover);
184+
@namespace 'http://www.w3.org/1999/xhtml';
185+
"
186+
`)
187+
})

packages/tailwindcss/src/ast.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,9 @@ export function optimizeAst(ast: AstNode[]) {
261261
for (let child of node.nodes) {
262262
let nodes: AstNode[] = []
263263
transform(child, nodes, depth + 1)
264-
parent.push(...nodes)
264+
if (nodes.length > 0) {
265+
parent.push(...nodes)
266+
}
265267
}
266268
}
267269

@@ -271,7 +273,9 @@ export function optimizeAst(ast: AstNode[]) {
271273
for (let child of node.nodes) {
272274
transform(child, copy.nodes, depth + 1)
273275
}
274-
parent.push(copy)
276+
if (copy.nodes.length > 0) {
277+
parent.push(copy)
278+
}
275279
}
276280
}
277281

@@ -297,7 +301,15 @@ export function optimizeAst(ast: AstNode[]) {
297301
for (let child of node.nodes) {
298302
transform(child, copy.nodes, depth + 1)
299303
}
300-
parent.push(copy)
304+
if (
305+
copy.nodes.length > 0 ||
306+
copy.name === '@layer' ||
307+
copy.name === '@charset' ||
308+
copy.name === '@custom-media' ||
309+
copy.name === '@namespace'
310+
) {
311+
parent.push(copy)
312+
}
301313
}
302314

303315
// AtRoot

packages/tailwindcss/src/compat/plugin-api.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3167,8 +3167,6 @@ describe('addUtilities()', () => {
31673167
color: red;
31683168
}
31693169
}
3170-
}
3171-
:root, :host {
31723170
}"
31733171
`,
31743172
)

packages/tailwindcss/src/compat/screens-config.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -655,9 +655,5 @@ test('JS config `screens` can overwrite default CSS `--breakpoint-*`', async ()
655655
// currently.
656656
expect(
657657
compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-2xl:flex']),
658-
).toMatchInlineSnapshot(`
659-
":root, :host {
660-
}
661-
"
662-
`)
658+
).toMatchInlineSnapshot(`""`)
663659
})

0 commit comments

Comments
 (0)