Skip to content

Commit ffe1ad1

Browse files
committed
Use internal CSS representation in various places
A few APIs are shared across versions and rely on PostCSS nodes. We’ll pass in PostCSS ASTs to those APIs by translating our AST to PostCSS’s AST. This new setup is also intended to remove AST manipulation wherever possible. We build data structures and can skip over ignored nodes when walking. The eventual goal is to reduce memory usage by using nodes returned by Tailwind CSS’s internal cache.
1 parent b435bee commit ffe1ad1

File tree

7 files changed

+115
-117
lines changed

7 files changed

+115
-117
lines changed

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

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { DesignSystem } from '@tailwindcss/language-service/src/util/v4'
22

3-
import postcss from 'postcss'
43
import { createJiti } from 'jiti'
54
import * as fs from 'node:fs/promises'
65
import * as path from 'node:path'
@@ -10,6 +9,7 @@ import { pathToFileURL } from '../../utils'
109
import type { Jiti } from 'jiti/lib/types'
1110
import { assets } from './assets'
1211
import { plugins } from './plugins'
12+
import { AstNode, cloneAstNode, parse } from '@tailwindcss/language-service/src/css'
1313

1414
const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/
1515
const HAS_V4_THEME = /@theme\s*\{/
@@ -225,35 +225,28 @@ export async function loadDesignSystem(
225225
Object.assign(design, {
226226
dependencies: () => dependencies,
227227

228-
// TODOs:
229-
//
230-
// 1. Remove PostCSS parsing — its roughly 60% of the processing time
231-
// ex: compiling 19k classes take 650ms and 400ms of that is PostCSS
232-
//
233-
// - Replace `candidatesToCss` with a `candidatesToAst` API
234-
// First step would be to convert to a PostCSS AST by transforming the nodes directly
235-
// Then it would be to drop the PostCSS AST representation entirely in all v4 code paths
236-
compile(classes: string[]): postcss.Root[] {
228+
compile(classes: string[]): AstNode[][] {
237229
// 1. Compile any uncached classes
238-
let cache = design.storage[COMPILE_CACHE] as Record<string, postcss.Root>
230+
let cache = design.storage[COMPILE_CACHE] as Record<string, AstNode[]>
239231
let uncached = classes.filter((name) => cache[name] === undefined)
240232

241233
let css = design.candidatesToCss(uncached)
234+
242235
let errors: any[] = []
243236

244237
for (let [idx, cls] of uncached.entries()) {
245238
let str = css[idx]
246239

247240
if (str === null) {
248-
cache[cls] = postcss.root()
241+
cache[cls] = []
249242
continue
250243
}
251244

252245
try {
253-
cache[cls] = postcss.parse(str.trimEnd())
246+
cache[cls] = parse(str.trimEnd())
254247
} catch (err) {
255248
errors.push(err)
256-
cache[cls] = postcss.root()
249+
cache[cls] = []
257250
continue
258251
}
259252
}
@@ -263,10 +256,10 @@ export async function loadDesignSystem(
263256
}
264257

265258
// 2. Pull all the classes from the cache
266-
let roots: postcss.Root[] = []
259+
let roots: AstNode[][] = []
267260

268261
for (let cls of classes) {
269-
roots.push(cache[cls].clone())
262+
roots.push(cache[cls].map(cloneAstNode))
270263
}
271264

272265
return roots

packages/tailwindcss-language-service/src/completionProvider.ts

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import { resolveKnownThemeKeys, resolveKnownThemeNamespaces } from './util/v4/th
4747
import { SEARCH_RANGE } from './util/constants'
4848
import { getLanguageBoundaries } from './util/getLanguageBoundaries'
4949
import { isWithinRange } from './util/isWithinRange'
50+
import { walk, WalkAction } from './util/walk'
51+
import { Declaration, toPostCSSAst } from './css'
5052

5153
let isUtil = (className) =>
5254
Array.isArray(className.__info)
@@ -2296,35 +2298,11 @@ export async function resolveCompletionItem(
22962298
let base = state.designSystem.compile([className])[0]
22972299
let root = state.designSystem.compile([[...variants, className].join(state.separator)])[0]
22982300

2299-
let rules = root.nodes.filter((node) => node.type === 'rule')
2301+
let rules = root.filter((node) => node.kind === 'rule')
23002302
if (rules.length === 0) return item
23012303

23022304
if (!item.detail) {
23032305
if (rules.length === 1) {
2304-
let decls: postcss.Declaration[] = []
2305-
2306-
// Remove any `@property` rules
2307-
base = base.clone()
2308-
base.walkAtRules((rule) => {
2309-
// Ignore declarations inside `@property` rules
2310-
if (rule.name === 'property') {
2311-
rule.remove()
2312-
}
2313-
2314-
// Ignore declarations @supports (-moz-orient: inline)
2315-
// this is a hack used for `@property` fallbacks in Firefox
2316-
if (rule.name === 'supports' && rule.params === '(-moz-orient: inline)') {
2317-
rule.remove()
2318-
}
2319-
2320-
if (
2321-
rule.name === 'supports' &&
2322-
rule.params === '(background-image: linear-gradient(in lab, red, red))'
2323-
) {
2324-
rule.remove()
2325-
}
2326-
})
2327-
23282306
let ignoredValues = new Set([
23292307
'var(--tw-border-style)',
23302308
'var(--tw-outline-style)',
@@ -2334,26 +2312,51 @@ export async function resolveCompletionItem(
23342312
'var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z)',
23352313
])
23362314

2337-
base.walkDecls((node) => {
2338-
if (ignoredValues.has(node.value)) return
2315+
let decls: Declaration[] = []
2316+
2317+
walk(base, (node) => {
2318+
if (node.kind === 'at-rule') {
2319+
// Ignore declarations inside `@property` rules
2320+
if (node.name === '@property') {
2321+
return WalkAction.Skip
2322+
}
23392323

2340-
decls.push(node)
2324+
// Ignore declarations @supports (-moz-orient: inline)
2325+
// this is a hack used for `@property` fallbacks in Firefox
2326+
if (node.name === '@supports' && node.params === '(-moz-orient: inline)') {
2327+
return WalkAction.Skip
2328+
}
2329+
2330+
if (
2331+
node.name === '@supports' &&
2332+
node.params === '(background-image: linear-gradient(in lab, red, red))'
2333+
) {
2334+
return WalkAction.Skip
2335+
}
2336+
}
2337+
2338+
if (node.kind === 'declaration') {
2339+
if (ignoredValues.has(node.value)) return WalkAction.Continue
2340+
decls.push(node)
2341+
}
2342+
2343+
return WalkAction.Continue
23412344
})
23422345

23432346
// TODO: Hardcoding this list is really unfortunate. We should be able
23442347
// to handle this in Tailwind CSS itself.
2345-
function isOtherDecl(node: postcss.Declaration) {
2346-
if (node.prop === '--tw-leading') return false
2347-
if (node.prop === '--tw-duration') return false
2348-
if (node.prop === '--tw-ease') return false
2349-
if (node.prop === '--tw-font-weight') return false
2350-
if (node.prop === '--tw-gradient-via-stops') return false
2351-
if (node.prop === '--tw-gradient-stops') return false
2352-
if (node.prop === '--tw-tracking') return false
2353-
if (node.prop === '--tw-space-x-reverse' && node.value === '0') return false
2354-
if (node.prop === '--tw-space-y-reverse' && node.value === '0') return false
2355-
if (node.prop === '--tw-divide-x-reverse' && node.value === '0') return false
2356-
if (node.prop === '--tw-divide-y-reverse' && node.value === '0') return false
2348+
function isOtherDecl(node: Declaration) {
2349+
if (node.property === '--tw-leading') return false
2350+
if (node.property === '--tw-duration') return false
2351+
if (node.property === '--tw-ease') return false
2352+
if (node.property === '--tw-font-weight') return false
2353+
if (node.property === '--tw-gradient-via-stops') return false
2354+
if (node.property === '--tw-gradient-stops') return false
2355+
if (node.property === '--tw-tracking') return false
2356+
if (node.property === '--tw-space-x-reverse' && node.value === '0') return false
2357+
if (node.property === '--tw-space-y-reverse' && node.value === '0') return false
2358+
if (node.property === '--tw-divide-x-reverse' && node.value === '0') return false
2359+
if (node.property === '--tw-divide-y-reverse' && node.value === '0') return false
23572360

23582361
return true
23592362
}
@@ -2363,7 +2366,10 @@ export async function resolveCompletionItem(
23632366
decls = decls.filter(isOtherDecl)
23642367
}
23652368

2366-
item.detail = await jit.stringifyDecls(state, postcss.rule({ selectors: [], nodes: decls }))
2369+
let root = toPostCSSAst([{ kind: 'rule', selector: '', nodes: decls }])
2370+
let rule = root.nodes[0] as postcss.Rule
2371+
2372+
item.detail = await jit.stringifyDecls(state, rule)
23672373
} else {
23682374
item.detail = `${rules.length} rules`
23692375
}
@@ -2373,8 +2379,9 @@ export async function resolveCompletionItem(
23732379
item.documentation = {
23742380
kind: 'markdown' as typeof MarkupKind.Markdown,
23752381
value: [
2382+
//
23762383
'```css',
2377-
await jit.stringifyRoot(state, postcss.root({ nodes: rules })),
2384+
await jit.stringifyRoot(state, toPostCSSAst(rules)),
23782385
'```',
23792386
].join('\n'),
23802387
}

packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as jit from '../util/jit'
99
import * as postcss from 'postcss'
1010
import type { AtRule, Node, Rule } from 'postcss'
1111
import type { TextDocument } from 'vscode-languageserver-textdocument'
12+
import { walk, WalkAction } from '../util/walk'
1213

1314
function isCustomProperty(property: string): boolean {
1415
return property.startsWith('--')
@@ -238,20 +239,6 @@ interface RuleEntry {
238239

239240
type ClassDetails = Record<string, RuleEntry[]>
240241

241-
export function visit(
242-
nodes: postcss.AnyNode[],
243-
cb: (node: postcss.AnyNode, path: postcss.AnyNode[]) => void,
244-
path: postcss.AnyNode[] = [],
245-
): void {
246-
for (let child of nodes) {
247-
path = [...path, child]
248-
cb(child, path)
249-
if ('nodes' in child && child.nodes && child.nodes.length > 0) {
250-
visit(child.nodes, cb, path)
251-
}
252-
}
253-
}
254-
255242
function recordClassDetails(state: State, classes: DocumentClassName[]): ClassDetails {
256243
const groups: Record<string, RuleEntry[]> = {}
257244

@@ -261,34 +248,38 @@ function recordClassDetails(state: State, classes: DocumentClassName[]): ClassDe
261248
for (let [idx, root] of roots.entries()) {
262249
let { className } = classes[idx]
263250

264-
visit([root], (node, path) => {
265-
if (node.type !== 'rule' && node.type !== 'atrule') return
251+
walk(root, (node, ctx) => {
252+
if (node.kind !== 'rule' && node.kind !== 'at-rule') return WalkAction.Continue
266253

267254
let properties: string[] = []
268255

269256
for (let child of node.nodes ?? []) {
270-
if (child.type !== 'decl') continue
271-
properties.push(child.prop)
257+
if (child.kind !== 'declaration') continue
258+
properties.push(child.property)
272259
}
273260

274-
if (properties.length === 0) return
261+
if (properties.length === 0) return WalkAction.Continue
275262

276263
// We have to slice off the first `context` item because it's the class name and that's always different
264+
let path = [...ctx.path(), node].slice(1)
265+
277266
groups[className] ??= []
278267
groups[className].push({
279268
properties,
280269
context: path
281270
.map((node) => {
282-
if (node.type === 'rule') {
271+
if (node.kind === 'rule') {
283272
return node.selector
284-
} else if (node.type === 'atrule') {
285-
return `@${node.name} ${node.params}`
273+
} else if (node.kind === 'at-rule') {
274+
return `${node.name} ${node.params}`
286275
}
276+
287277
return ''
288278
})
289-
.filter(Boolean)
290-
.slice(1),
279+
.filter(Boolean),
291280
})
281+
282+
return WalkAction.Continue
292283
})
293284
}
294285

packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { resolveKnownThemeKeys } from '../util/v4/theme-keys'
99
import dlv from 'dlv'
1010
import type { TextDocument } from 'vscode-languageserver-textdocument'
1111
import type { DesignSystem } from '../util/v4'
12+
import { walk, WalkAction } from '../util/walk'
1213

1314
type ValidationResult =
1415
| { isValid: true; value: any }
@@ -224,12 +225,17 @@ function resolveThemeValue(design: DesignSystem, path: string) {
224225
//
225226
// Non-CSS representable values are not a concern here because the validation
226227
// only happens for calls in a CSS context.
227-
let [root] = design.compile([candidate])
228+
let root = design.compile([candidate])[0]
228229

229230
let value: string | null = null
230231

231-
root.walkDecls((decl) => {
232-
value = decl.value
232+
walk(root, (node) => {
233+
if (node.kind === 'declaration') {
234+
value = node.value
235+
return WalkAction.Stop
236+
}
237+
238+
return WalkAction.Continue
233239
})
234240

235241
return value

packages/tailwindcss-language-service/src/hoverProvider.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getTextWithoutComments } from './util/doc'
2020
import braces from 'braces'
2121
import { absoluteRange } from './util/absoluteRange'
2222
import { segment } from './util/segment'
23+
import { toPostCSSAst } from './css'
2324

2425
export async function doHover(
2526
state: State,
@@ -101,15 +102,12 @@ async function provideClassNameHover(
101102

102103
if (state.v4) {
103104
let root = state.designSystem.compile([className.className])[0]
104-
105-
if (root.nodes.length === 0) {
106-
return null
107-
}
105+
if (root.length === 0) return null
108106

109107
return {
110108
contents: {
111109
language: 'css',
112-
value: await jit.stringifyRoot(state, root, document.uri),
110+
value: await jit.stringifyRoot(state, toPostCSSAst(root), document.uri),
113111
},
114112
range: className.range,
115113
}

0 commit comments

Comments
 (0)