Skip to content

Commit da37749

Browse files
Tweak internal CSS AST implementation (#1512)
Right now our CSS AST doesn't differentiate at-rules with an empty body and at-rules that have no body at all. This is normally not a problem _however_ for CSS mixins `@apply --foo;` and `@apply --foo {};` mean different things — this requires us to be able to differentiate them. We'll be changing this in Tailwind CSS itself at some point so I'm changing this here to be forward compatible with those changes.
1 parent 5c79a60 commit da37749

File tree

10 files changed

+78
-108
lines changed

10 files changed

+78
-108
lines changed

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

Lines changed: 21 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,26 @@ 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+
node.nodes = null
258+
259+
return WalkAction.Continue
260+
})
261+
262+
cache[cls] = ast
263+
244264
continue
245265
}
246266

Lines changed: 5 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { parseAtRule } from './parse'
21
import type { SourceLocation } from './source'
3-
import type { VisitContext } from '../util/walk'
2+
import { parseAtRule } from './parse'
43

54
const AT_SIGN = 0x40
65

@@ -17,7 +16,7 @@ export type AtRule = {
1716
kind: 'at-rule'
1817
name: string
1918
params: string
20-
nodes: AstNode[]
19+
nodes: AstNode[] | null
2120

2221
src?: SourceLocation
2322
dst?: SourceLocation
@@ -70,7 +69,7 @@ export function styleRule(selector: string, nodes: AstNode[] = []): StyleRule {
7069
}
7170
}
7271

73-
export function atRule(name: string, params: string = '', nodes: AstNode[] = []): AtRule {
72+
export function atRule(name: string, params: string = '', nodes: AstNode[] | null = []): AtRule {
7473
return {
7574
kind: 'at-rule',
7675
name,
@@ -79,12 +78,12 @@ export function atRule(name: string, params: string = '', nodes: AstNode[] = [])
7978
}
8079
}
8180

82-
export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRule {
81+
export function rule(selector: string, nodes: AstNode[] | null = []): StyleRule | AtRule {
8382
if (selector.charCodeAt(0) === AT_SIGN) {
8483
return parseAtRule(selector, nodes)
8584
}
8685

87-
return styleRule(selector, nodes)
86+
return styleRule(selector, nodes ?? [])
8887
}
8988

9089
export function decl(property: string, value: string | undefined, important = false): Declaration {
@@ -117,95 +116,3 @@ export function atRoot(nodes: AstNode[]): AtRoot {
117116
nodes,
118117
}
119118
}
120-
121-
export function cloneAstNode<T extends AstNode>(node: T): T {
122-
switch (node.kind) {
123-
case 'rule':
124-
return {
125-
kind: node.kind,
126-
selector: node.selector,
127-
nodes: node.nodes.map(cloneAstNode),
128-
src: node.src,
129-
dst: node.dst,
130-
} satisfies StyleRule as T
131-
132-
case 'at-rule':
133-
return {
134-
kind: node.kind,
135-
name: node.name,
136-
params: node.params,
137-
nodes: node.nodes.map(cloneAstNode),
138-
src: node.src,
139-
dst: node.dst,
140-
} satisfies AtRule as T
141-
142-
case 'at-root':
143-
return {
144-
kind: node.kind,
145-
nodes: node.nodes.map(cloneAstNode),
146-
src: node.src,
147-
dst: node.dst,
148-
} satisfies AtRoot as T
149-
150-
case 'context':
151-
return {
152-
kind: node.kind,
153-
context: { ...node.context },
154-
nodes: node.nodes.map(cloneAstNode),
155-
src: node.src,
156-
dst: node.dst,
157-
} satisfies Context as T
158-
159-
case 'declaration':
160-
return {
161-
kind: node.kind,
162-
property: node.property,
163-
value: node.value,
164-
important: node.important,
165-
src: node.src,
166-
dst: node.dst,
167-
} satisfies Declaration as T
168-
169-
case 'comment':
170-
return {
171-
kind: node.kind,
172-
value: node.value,
173-
src: node.src,
174-
dst: node.dst,
175-
} satisfies Comment as T
176-
177-
default:
178-
node satisfies never
179-
throw new Error(`Unknown node kind: ${(node as any).kind}`)
180-
}
181-
}
182-
183-
export function cssContext(
184-
ctx: VisitContext<AstNode>,
185-
): VisitContext<AstNode> & { context: Record<string, string | boolean> } {
186-
return {
187-
depth: ctx.depth,
188-
get context() {
189-
let context: Record<string, string | boolean> = {}
190-
for (let child of ctx.path()) {
191-
if (child.kind === 'context') {
192-
Object.assign(context, child.context)
193-
}
194-
}
195-
196-
// Once computed, we never need to compute this again
197-
Object.defineProperty(this, 'context', { value: context })
198-
return context
199-
},
200-
get parent() {
201-
let parent = (this.path().pop() as Extract<AstNode, { nodes: AstNode[] }>) ?? null
202-
203-
// Once computed, we never need to compute this again
204-
Object.defineProperty(this, 'parent', { value: parent })
205-
return parent
206-
},
207-
path() {
208-
return ctx.path().filter((n) => n.kind !== 'context')
209-
},
210-
}
211-
}

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
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { VisitContext } from '../util/walk'
2+
import type { AstNode } from './ast'
3+
4+
export function cssContext(
5+
ctx: VisitContext<AstNode>,
6+
): VisitContext<AstNode> & { context: Record<string, string | boolean> } {
7+
return {
8+
depth: ctx.depth,
9+
get context() {
10+
let context: Record<string, string | boolean> = {}
11+
for (let child of ctx.path()) {
12+
if (child.kind === 'context') {
13+
Object.assign(context, child.context)
14+
}
15+
}
16+
17+
// Once computed, we never need to compute this again
18+
Object.defineProperty(this, 'context', { value: context })
19+
return context
20+
},
21+
get parent() {
22+
let parent = (this.path().pop() as Extract<AstNode, { nodes: AstNode[] }>) ?? null
23+
24+
// Once computed, we never need to compute this again
25+
Object.defineProperty(this, 'parent', { value: parent })
26+
return parent
27+
},
28+
path() {
29+
return ctx.path().filter((n) => n.kind !== 'context')
30+
},
31+
}
32+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './ast'
22
export * from './source'
33
export { parse } from './parse'
4+
export { cloneAstNode } from './clone-ast-node'
45
export { fromPostCSSAst } from './from-postcss-ast'
56
export { toPostCSSAst } from './to-postcss-ast'
67
export { toCss } from './to-css'

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
}

packages/vscode-tailwindcss/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
- Add a source to all emitted diagnostics ([#1491](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1491))
66
- Improve performance in large files ([#1507](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1507))
7-
- Improve utility lookup performance when using v4 ([#1509](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1509))
7+
- Improve utility lookup performance when using v4 ([#1509](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1509), [#1512](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1512))
88
- Fix project initalization when stylesheet is named `tailwindcss.css` ([#1517](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1517))
99

1010
## 0.14.29

0 commit comments

Comments
 (0)