Expose candidatesToAst to the language server (#19405)

This will be used to improve performance and potentially enable future
features that require generated CSS source locations.

Note: This is still 100% internal API. You can only access this via
`__unstable__loadDesignSystem` for a reason. We may chance the structure
of the arguments and/or return values as needed.
This commit is contained in:
Jordan Pittman 2025-12-08 10:54:42 -05:00 committed by GitHub
parent 478e959097
commit 820d90797c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 30 deletions

View File

@ -1,5 +1,5 @@
import { Polyfills } from '.'
import { optimizeAst, toCss } from './ast'
import { optimizeAst, toCss, type AstNode } from './ast'
import {
parseCandidate,
parseVariant,
@ -19,11 +19,13 @@ import {
type VariantEntry,
} from './intellisense'
import { getClassOrder } from './sort'
import type { SourceLocation } from './source-maps/source'
import { Theme, ThemeOptions, type ThemeKey } from './theme'
import { Utilities, createUtilities, withAlpha } from './utilities'
import { DefaultMap } from './utils/default-map'
import { extractUsedVariables } from './utils/variables'
import { Variants, createVariants, substituteAtVariant } from './variants'
import { WalkAction, walk } from './walk'
export const enum CompileAstFlags {
None = 0,
@ -59,12 +61,16 @@ export type DesignSystem = {
// Used by IntelliSense
candidatesToCss(classes: string[]): (string | null)[]
candidatesToAst(classes: string[]): AstNode[][]
// General purpose storage
storage: Record<symbol, unknown>
}
export function buildDesignSystem(theme: Theme): DesignSystem {
export function buildDesignSystem(
theme: Theme,
utilitiesSrc?: SourceLocation | undefined,
): DesignSystem {
let utilities = createUtilities(theme)
let variants = createVariants(theme)
@ -109,6 +115,44 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
}
})
function candidatesToAst(classes: string[]): AstNode[][] {
let result: AstNode[][] = []
for (let className of classes) {
let wasValid = true
let { astNodes } = compileCandidates([className], designSystem, {
onInvalidCandidate() {
wasValid = false
},
})
if (utilitiesSrc) {
walk(astNodes, (node) => {
// We do this conditionally to preserve source locations from both
// `@utility` and `@custom-variant`. Even though generated nodes are
// cached this should be fine because `utilitiesNode.src` should not
// change without a full rebuild which destroys the cache.
node.src ??= utilitiesSrc
return WalkAction.Continue
})
}
// Disable all polyfills to not unnecessarily pollute IntelliSense output
astNodes = optimizeAst(astNodes, designSystem, Polyfills.None)
result.push(wasValid ? astNodes : [])
}
return result
}
function candidatesToCss(classes: string[]): (string | null)[] {
return candidatesToAst(classes).map((nodes) => {
return nodes.length > 0 ? toCss(nodes) : null
})
}
let designSystem: DesignSystem = {
theme,
utilities,
@ -117,30 +161,8 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
invalidCandidates: new Set(),
important: false,
candidatesToCss(classes: string[]) {
let result: (string | null)[] = []
for (let className of classes) {
let wasInvalid = false
let { astNodes } = compileCandidates([className], this, {
onInvalidCandidate() {
wasInvalid = true
},
})
// Disable all polyfills to not unnecessarily pollute IntelliSense output
astNodes = optimizeAst(astNodes, designSystem, Polyfills.None)
if (astNodes.length === 0 || wasInvalid) {
result.push(null)
} else {
result.push(toCss(astNodes))
}
}
return result
},
candidatesToCss,
candidatesToAst,
getClassOrder(classes) {
return getClassOrder(this, classes)

View File

@ -595,7 +595,7 @@ async function parseCss(
}
})
let designSystem = buildDesignSystem(theme)
let designSystem = buildDesignSystem(theme, utilitiesNode?.src)
if (important) {
designSystem.important = important
@ -855,7 +855,7 @@ export async function compile(
}
export async function __unstable__loadDesignSystem(css: string, opts: CompileOptions = {}) {
let result = await parseCss(CSS.parse(css), opts)
let result = await parseCss(CSS.parse(css, { from: opts.from }), opts)
return result.designSystem
}

View File

@ -1,5 +1,6 @@
import { expect, test } from 'vitest'
import { __unstable__loadDesignSystem } from '.'
import { decl, rule } from './ast'
import plugin from './plugin'
import { ThemeOptions } from './theme'
@ -165,6 +166,26 @@ test('Can produce CSS per candidate using `candidatesToCss`', async () => {
`)
})
test('Can produce AST per candidate using `candidatesToAst`', async () => {
let design = await loadDesignSystem()
design.invalidCandidates = new Set(['bg-[#fff]'])
expect(
design.candidatesToAst(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]', 'text-xs']),
).toEqual([
[rule('.underline', [decl('text-decoration-line', 'underline')])],
[],
[],
[rule('.bg-\\[\\#000\\]', [decl('background-color', '#000')])],
[
rule('.text-xs', [
decl('font-size', 'var(--text-xs)'),
decl('line-height', 'var(--tw-leading, var(--text-xs--line-height))'),
]),
],
])
})
test('Utilities do not show wrapping selector in intellisense', async () => {
let input = css`
@import 'tailwindcss/utilities';
@ -238,7 +259,7 @@ test('Utilities, when marked as important, show as important in intellisense', a
test('Static utilities from plugins are listed in hovers and completions', async () => {
let input = css`
@import 'tailwindcss/utilities';
@plugin "./plugin.js"l;
@plugin "./plugin.js";
`
let design = await __unstable__loadDesignSystem(input, {
@ -275,7 +296,7 @@ test('Static utilities from plugins are listed in hovers and completions', async
test('Functional utilities from plugins are listed in hovers and completions', async () => {
let input = css`
@import 'tailwindcss/utilities';
@plugin "./plugin.js"l;
@plugin "./plugin.js";
`
let design = await __unstable__loadDesignSystem(input, {