diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 384746b57b17..975e59e9abb5 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -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 } -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) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 6cf77bc81f40..512c2f831d03 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -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 } diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index ca9830664ddf..e37f05b9cb98 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -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, {