Skip to content

Commit c8906cf

Browse files
committed
Allow at-rules to have empty bodies
1 parent ebb1a64 commit c8906cf

File tree

7 files changed

+44
-13
lines changed

7 files changed

+44
-13
lines changed

packages/tailwindcss-language-server/src/util/v4/design-system.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Jiti } from 'jiti/lib/types'
1010
import { assets } from './assets'
1111
import { plugins } from './plugins'
1212
import { AstNode, cloneAstNode, parse } from '@tailwindcss/language-service/src/css'
13+
import { walk, WalkAction } from '@tailwindcss/language-service/src/util/walk'
1314

1415
const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/
1516
const HAS_V4_THEME = /@theme\s*\{/
@@ -240,7 +241,27 @@ export async function loadDesignSystem(
240241
let str = css[idx]
241242

242243
if (Array.isArray(str)) {
243-
cache[cls] = str
244+
let ast = str.map(cloneAstNode)
245+
246+
// Rewrite at-rules with zero nodes to act as if they have no body
247+
//
248+
// At a future time we'll only do this conditionally for earlier
249+
// Tailwind CSS v4 versions. We have to clone the AST *first*
250+
// because if the AST was shared with Tailwind CSS internals
251+
// and we mutated it we could break things.
252+
walk(ast, (node) => {
253+
if (node.kind !== 'at-rule') return WalkAction.Continue
254+
if (node.nodes === null) return WalkAction.Continue
255+
if (node.nodes.length !== 0) return WalkAction.Continue
256+
257+
// Treat
258+
node.nodes = null
259+
260+
return WalkAction.Continue
261+
})
262+
263+
cache[cls] = ast
264+
244265
continue
245266
}
246267

packages/tailwindcss-language-service/src/css/ast.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type AtRule = {
1616
kind: 'at-rule'
1717
name: string
1818
params: string
19-
nodes: AstNode[]
19+
nodes: AstNode[] | null
2020

2121
src?: SourceLocation
2222
dst?: SourceLocation
@@ -69,7 +69,7 @@ export function styleRule(selector: string, nodes: AstNode[] = []): StyleRule {
6969
}
7070
}
7171

72-
export function atRule(name: string, params: string = '', nodes: AstNode[] = []): AtRule {
72+
export function atRule(name: string, params: string = '', nodes: AstNode[] | null = []): AtRule {
7373
return {
7474
kind: 'at-rule',
7575
name,
@@ -78,12 +78,12 @@ export function atRule(name: string, params: string = '', nodes: AstNode[] = [])
7878
}
7979
}
8080

81-
export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRule {
81+
export function rule(selector: string, nodes: AstNode[] | null = []): StyleRule | AtRule {
8282
if (selector.charCodeAt(0) === AT_SIGN) {
8383
return parseAtRule(selector, nodes)
8484
}
8585

86-
return styleRule(selector, nodes)
86+
return styleRule(selector, nodes ?? [])
8787
}
8888

8989
export function decl(property: string, value: string | undefined, important = false): Declaration {

packages/tailwindcss-language-service/src/css/clone-ast-node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function cloneAstNode<T extends AstNode>(node: T): T {
1616
kind: node.kind,
1717
name: node.name,
1818
params: node.params,
19-
nodes: node.nodes.map(cloneAstNode),
19+
nodes: node.nodes?.map(cloneAstNode) ?? null,
2020
src: node.src,
2121
dst: node.dst,
2222
} satisfies AtRule as T

packages/tailwindcss-language-service/src/css/from-postcss-ast.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ export function fromPostCSSAst(root: postcss.Root): AstNode[] {
4444

4545
// AtRule
4646
else if (node.type === 'atrule') {
47-
let astNode = atRule(`@${node.name}`, node.params)
47+
let astNode = atRule(`@${node.name}`, node.params, node.nodes ? [] : null)
4848
astNode.src = toSource(node)
49-
node.each((child) => transform(child, astNode.nodes))
49+
node.each((child) => transform(child, astNode.nodes!))
5050
parent.push(astNode)
5151
}
5252

packages/tailwindcss-language-service/src/css/parse.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
277277
}
278278

279279
if (parent) {
280+
parent.nodes ??= []
280281
parent.nodes.push(declaration)
281282
} else {
282283
ast.push(declaration)
@@ -303,6 +304,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
303304

304305
// At-rule is nested inside of a rule, attach it to the parent.
305306
if (parent) {
307+
parent.nodes ??= []
306308
parent.nodes.push(node)
307309
}
308310

@@ -343,6 +345,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
343345
}
344346

345347
if (parent) {
348+
parent.nodes ??= []
346349
parent.nodes.push(declaration)
347350
} else {
348351
ast.push(declaration)
@@ -369,6 +372,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
369372

370373
// Attach the rule to the parent in case it's nested.
371374
if (parent) {
375+
parent.nodes ??= []
372376
parent.nodes.push(node)
373377
}
374378

@@ -421,6 +425,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
421425

422426
// At-rule is nested inside of a rule, attach it to the parent.
423427
if (parent) {
428+
parent.nodes ??= []
424429
parent.nodes.push(node)
425430
}
426431

@@ -460,6 +465,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
460465
node.dst = [source, bufferStart, i]
461466
}
462467

468+
parent.nodes ??= []
463469
parent.nodes.push(node)
464470
}
465471
}
@@ -548,7 +554,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
548554
return ast
549555
}
550556

551-
export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
557+
export function parseAtRule(buffer: string, nodes: AstNode[] | null = []): AtRule {
552558
let name = buffer
553559
let params = ''
554560

packages/tailwindcss-language-service/src/css/to-css.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function toCss(ast: AstNode[], track?: boolean): string {
9191
// ```css
9292
// @layer base, components, utilities;
9393
// ```
94-
if (node.nodes.length === 0) {
94+
if (!node.nodes) {
9595
let css = `${indent}${node.name} ${node.params};\n`
9696

9797
if (track) {

packages/tailwindcss-language-service/src/css/to-postcss-ast.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as postcss from 'postcss'
2-
import { atRule, comment, decl, styleRule, type AstNode } from './ast'
2+
import type { AstNode } from './ast'
33
import type { Source, SourceLocation } from './source'
44
import { DefaultMap } from '../util/default-map'
55
import { createLineTable, LineTable } from '../util/line-table'
@@ -85,11 +85,15 @@ export function toPostCSSAst(ast: AstNode[], source?: postcss.Source): postcss.R
8585

8686
// AtRule
8787
else if (node.kind === 'at-rule') {
88-
let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params })
88+
let astNode = postcss.atRule({
89+
name: node.name.slice(1),
90+
params: node.params,
91+
...(node.nodes ? { nodes: [] } : {}),
92+
})
8993
updateSource(astNode, node.src)
9094
astNode.raws.semicolon = true
9195
parent.append(astNode)
92-
for (let child of node.nodes) {
96+
for (let child of node.nodes ?? []) {
9397
transform(child, astNode)
9498
}
9599
}

0 commit comments

Comments
 (0)