diff --git a/crates/oxide/src/lib.rs b/crates/oxide/src/lib.rs index 4c7621e7a1c6..f7da2be772ce 100644 --- a/crates/oxide/src/lib.rs +++ b/crates/oxide/src/lib.rs @@ -171,11 +171,18 @@ impl Scanner { fn compute_candidates(&mut self) { let mut changed_content = vec![]; - for path in &self.files { - let current_time = fs::metadata(path) - .and_then(|m| m.modified()) - .unwrap_or(SystemTime::now()); + let current_mtimes = self + .files + .par_iter() + .map(|path| { + fs::metadata(path) + .and_then(|m| m.modified()) + .unwrap_or(SystemTime::now()) + }) + .collect::>(); + for (idx, path) in self.files.iter().enumerate() { + let current_time = current_mtimes[idx]; let previous_time = self.mtimes.insert(path.clone(), current_time); let should_scan_file = match previous_time { @@ -218,14 +225,21 @@ impl Scanner { #[tracing::instrument(skip_all)] fn check_for_new_files(&mut self) { + let current_mtimes = self + .dirs + .par_iter() + .map(|path| { + fs::metadata(path) + .and_then(|m| m.modified()) + .unwrap_or(SystemTime::now()) + }) + .collect::>(); + let mut modified_dirs: Vec = vec![]; // Check all directories to see if they were modified - for path in &self.dirs { - let current_time = fs::metadata(path) - .and_then(|m| m.modified()) - .unwrap_or(SystemTime::now()); - + for (idx, path) in self.dirs.iter().enumerate() { + let current_time = current_mtimes[idx]; let previous_time = self.mtimes.insert(path.clone(), current_time); let should_scan = match previous_time { diff --git a/crates/oxide/src/scanner/allowed_paths.rs b/crates/oxide/src/scanner/allowed_paths.rs index 459d17d27ca3..8a584d06fb2c 100644 --- a/crates/oxide/src/scanner/allowed_paths.rs +++ b/crates/oxide/src/scanner/allowed_paths.rs @@ -40,7 +40,6 @@ pub fn resolve_paths(root: &Path) -> impl Iterator { .filter_map(Result::ok) } -#[tracing::instrument(skip_all)] pub fn read_dir(root: &Path, depth: Option) -> impl Iterator { WalkBuilder::new(root) .hidden(false) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index f7e3537515c0..b946c79defd2 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -7,8 +7,10 @@ import { pathToFileURL } from 'node:url' import { __unstable__loadDesignSystem as ___unstable__loadDesignSystem, compile as _compile, + compileAst as _compileAst, Features, } from 'tailwindcss' +import type { AstNode } from '../../tailwindcss/src/ast' import { getModuleDependencies } from './get-module-dependencies' import { rewriteUrls } from './urls' @@ -16,30 +18,29 @@ export { Features } export type Resolver = (id: string, base: string) => Promise -export async function compile( - css: string, - { - base, - onDependency, - shouldRewriteUrls, - - customCssResolver, - customJsResolver, - }: { - base: string - onDependency: (path: string) => void - shouldRewriteUrls?: boolean - - customCssResolver?: Resolver - customJsResolver?: Resolver - }, -) { - let compiler = await _compile(css, { +export interface CompileOptions { + base: string + onDependency: (path: string) => void + shouldRewriteUrls?: boolean + + customCssResolver?: Resolver + customJsResolver?: Resolver +} + +function createCompileOptions({ + base, + onDependency, + shouldRewriteUrls, + + customCssResolver, + customJsResolver, +}: CompileOptions) { + return { base, - async loadModule(id, base) { + async loadModule(id: string, base: string) { return loadModule(id, base, onDependency, customJsResolver) }, - async loadStylesheet(id, base) { + async loadStylesheet(id: string, base: string) { let sheet = await loadStylesheet(id, base, onDependency, customCssResolver) if (shouldRewriteUrls) { @@ -52,8 +53,13 @@ export async function compile( return sheet }, - }) + } +} +async function ensureSourceDetectionRootExists( + compiler: { root: Awaited>['root'] }, + base: string, +) { // Verify if the `source(…)` path exists (until the glob pattern starts) if (compiler.root && compiler.root !== 'none') { let globSymbols = /[*{]/ @@ -75,7 +81,17 @@ export async function compile( throw new Error(`The \`source(${compiler.root.pattern})\` does not exist`) } } +} + +export async function compileAst(ast: AstNode[], options: CompileOptions) { + let compiler = await _compileAst(ast, createCompileOptions(options)) + await ensureSourceDetectionRootExists(compiler, options.base) + return compiler +} +export async function compile(css: string, options: CompileOptions) { + let compiler = await _compile(css, createCompileOptions(options)) + await ensureSourceDetectionRootExists(compiler, options.base) return compiler } diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts index d11771290317..c4b88e981650 100644 --- a/packages/@tailwindcss-node/src/index.ts +++ b/packages/@tailwindcss-node/src/index.ts @@ -1,7 +1,7 @@ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' import * as env from './env' -export { __unstable__loadDesignSystem, compile, Features } from './compile' +export { __unstable__loadDesignSystem, compile, compileAst, Features } from './compile' export * from './normalize-path' export { env } diff --git a/packages/@tailwindcss-postcss/src/ast.test.ts b/packages/@tailwindcss-postcss/src/ast.test.ts new file mode 100644 index 000000000000..dd7095428e1c --- /dev/null +++ b/packages/@tailwindcss-postcss/src/ast.test.ts @@ -0,0 +1,107 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { toCss } from '../../tailwindcss/src/ast' +import { parse } from '../../tailwindcss/src/css-parser' +import { cssAstToPostCssAst, postCssAstToCssAst } from './ast' + +let css = dedent + +it('should convert a PostCSS AST into a Tailwind CSS AST', () => { + let input = css` + @charset "UTF-8"; + + @layer foo, bar, baz; + + @import 'tailwindcss'; + + .foo { + color: red; + + &:hover { + color: blue; + } + + .bar { + color: green !important; + background-color: yellow; + + @media (min-width: 640px) { + color: orange; + } + } + } + ` + + let ast = postcss.parse(input) + let transformedAst = postCssAstToCssAst(ast) + + expect(toCss(transformedAst)).toMatchInlineSnapshot(` + "@charset "UTF-8"; + @layer foo, bar, baz; + @import 'tailwindcss'; + .foo { + color: red; + &:hover { + color: blue; + } + .bar { + color: green !important; + background-color: yellow; + @media (min-width: 640px) { + color: orange; + } + } + } + " + `) +}) + +it('should convert a Tailwind CSS AST into a PostCSS AST', () => { + let input = css` + @charset "UTF-8"; + + @layer foo, bar, baz; + + @import 'tailwindcss'; + + .foo { + color: red; + + &:hover { + color: blue; + } + + .bar { + color: green !important; + background-color: yellow; + + @media (min-width: 640px) { + color: orange; + } + } + } + ` + + let ast = parse(input) + let transformedAst = cssAstToPostCssAst(ast) + + expect(transformedAst.toString()).toMatchInlineSnapshot(` + "@charset "UTF-8"; + @layer foo, bar, baz; + @import 'tailwindcss'; + .foo { + color: red; + &:hover { + color: blue; + } + .bar { + color: green !important; + background-color: yellow; + @media (min-width: 640px) { + color: orange; + } + } + }" + `) +}) diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts new file mode 100644 index 000000000000..201dd4bf8ac9 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -0,0 +1,117 @@ +import postcss, { + type ChildNode as PostCssChildNode, + type Container as PostCssContainerNode, + type Root as PostCssRoot, + type Source as PostcssSource, +} from 'postcss' +import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast' + +const EXCLAMATION_MARK = 0x21 + +export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undefined): PostCssRoot { + let root = postcss.root() + root.source = source + + function transform(node: AstNode, parent: PostCssContainerNode) { + // Declaration + if (node.kind === 'declaration') { + let astNode = postcss.decl({ + prop: node.property, + value: node.value ?? '', + important: node.important, + }) + astNode.source = source + parent.append(astNode) + } + + // Rule + else if (node.kind === 'rule') { + let astNode = postcss.rule({ selector: node.selector }) + astNode.source = source + astNode.raws.semicolon = true + parent.append(astNode) + for (let child of node.nodes) { + transform(child, astNode) + } + } + + // AtRule + else if (node.kind === 'at-rule') { + let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params }) + astNode.source = source + astNode.raws.semicolon = true + parent.append(astNode) + for (let child of node.nodes) { + transform(child, astNode) + } + } + + // Comment + else if (node.kind === 'comment') { + let astNode = postcss.comment({ text: node.value }) + // Spaces are encoded in our node.value already, no need to add additional + // spaces. + astNode.raws.left = '' + astNode.raws.right = '' + astNode.source = source + parent.append(astNode) + } + + // AtRoot & Context should not happen + else if (node.kind === 'at-root' || node.kind === 'context') { + } + + // Unknown + else { + node satisfies never + } + } + + for (let node of ast) { + transform(node, root) + } + + return root +} + +export function postCssAstToCssAst(root: PostCssRoot): AstNode[] { + function transform( + node: PostCssChildNode, + parent: Extract['nodes'], + ) { + // Declaration + if (node.type === 'decl') { + parent.push(decl(node.prop, node.value, node.important)) + } + + // Rule + else if (node.type === 'rule') { + let astNode = rule(node.selector) + node.each((child) => transform(child, astNode.nodes)) + parent.push(astNode) + } + + // AtRule + else if (node.type === 'atrule') { + let astNode = atRule(`@${node.name}`, node.params) + node.each((child) => transform(child, astNode.nodes)) + parent.push(astNode) + } + + // Comment + else if (node.type === 'comment') { + if (node.text.charCodeAt(0) !== EXCLAMATION_MARK) return + parent.push(comment(node.text)) + } + + // Unknown + else { + node satisfies never + } + } + + let ast: AstNode[] = [] + root.each((node) => transform(node, ast)) + + return ast +} diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index a1759f9f4c69..d40d34bc3d98 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -1,32 +1,38 @@ import QuickLRU from '@alloc/quick-lru' -import { compile, env, Features } from '@tailwindcss/node' +import { compileAst, env, Features } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' import { Features as LightningCssFeatures, transform } from 'lightningcss' import fs from 'node:fs' import path from 'node:path' import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss' +import { toCss, type AstNode } from '../../tailwindcss/src/ast' +import { cssAstToPostCssAst, postCssAstToCssAst } from './ast' import fixRelativePathsPlugin from './postcss-fix-relative-paths' interface CacheEntry { mtimes: Map - compiler: null | Awaited> + compiler: null | Awaited> scanner: null | Scanner - css: string - optimizedCss: string + tailwindCssAst: AstNode[] + cachedPostCssAst: postcss.Root + optimizedPostCssAst: postcss.Root fullRebuildPaths: string[] } let cache = new QuickLRU({ maxSize: 50 }) function getContextFromCache(inputFile: string, opts: PluginOptions): CacheEntry { - let key = `${inputFile}:${opts.base ?? ''}:${opts.optimize ?? ''}` + let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` if (cache.has(key)) return cache.get(key)! let entry = { mtimes: new Map(), compiler: null, scanner: null, - css: '', - optimizedCss: '', + + tailwindCssAst: [], + cachedPostCssAst: postcss.root(), + optimizedPostCssAst: postcss.root(), + fullRebuildPaths: [] as string[], } cache.set(key, entry) @@ -69,7 +75,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { context.fullRebuildPaths = [] - let compiler = await compile(root.toString(), { + let compiler = await compileAst(postCssAstToCssAst(root), { base: inputBasePath, onDependency: (path) => { context.fullRebuildPaths.push(path) @@ -128,8 +134,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } } - let css = '' - if ( rebuildStrategy === 'full' && // We can re-use the compiler if it was created during the @@ -205,23 +209,43 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } } - env.DEBUG && console.time('[@tailwindcss/postcss] Build CSS') - css = context.compiler.build(candidates) - env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build CSS') - - // Replace CSS - if (css !== context.css && optimize) { - env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS') - context.optimizedCss = optimizeCss(css, { - minify: typeof optimize === 'object' ? optimize.minify : true, - }) - env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Optimize CSS') + env.DEBUG && console.time('[@tailwindcss/postcss] Build AST') + let tailwindCssAst = context.compiler.build(candidates) + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build AST') + + if (context.tailwindCssAst !== tailwindCssAst) { + if (optimize) { + env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS') + context.optimizedPostCssAst = postcss.parse( + optimizeCss(toCss(tailwindCssAst), { + minify: typeof optimize === 'object' ? optimize.minify : true, + }), + result.opts, + ) + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Optimize CSS') + } else { + // Convert our AST to a PostCSS AST + env.DEBUG && console.time('[@tailwindcss/postcss] Transform CSS AST into PostCSS AST') + context.cachedPostCssAst = cssAstToPostCssAst(tailwindCssAst, root.source) + env.DEBUG && + console.timeEnd('[@tailwindcss/postcss] Transform CSS AST into PostCSS AST') + } } - context.css = css + + context.tailwindCssAst = tailwindCssAst env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') root.removeAll() - root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts)) + root.append( + optimize + ? context.optimizedPostCssAst.clone().nodes + : context.cachedPostCssAst.clone().nodes, + ) + + // Trick PostCSS into thinking the indent is 2 spaces, so it uses that + // as the default instead of 4. + root.raws.indent = ' ' + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Update PostCSS AST') env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') }, diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index 97f4a72aa253..c3d9b2f159bf 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -1,9 +1,10 @@ import { expect, it } from 'vitest' -import { context, decl, styleRule, toCss, walk, WalkAction } from './ast' +import { context, decl, optimizeAst, styleRule, toCss, walk, WalkAction } from './ast' import * as CSS from './css-parser' it('should pretty print an AST', () => { - expect(toCss(CSS.parse('.foo{color:red;&:hover{color:blue;}}'))).toMatchInlineSnapshot(` + expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}')))) + .toMatchInlineSnapshot(` ".foo { color: red; &:hover { @@ -51,7 +52,7 @@ it('allows the placement of context nodes', () => { expect(blueContext).toEqual({ context: 'a' }) expect(greenContext).toEqual({ context: 'b' }) - expect(toCss(ast)).toMatchInlineSnapshot(` + expect(toCss(optimizeAst(ast))).toMatchInlineSnapshot(` ".foo { color: red; } diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index e20f6042223a..74a2bc63eb7c 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -66,12 +66,12 @@ export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRul return styleRule(selector, nodes) } -export function decl(property: string, value: string | undefined): Declaration { +export function decl(property: string, value: string | undefined, important = false): Declaration { return { kind: 'declaration', property, value, - important: false, + important, } } @@ -208,18 +208,151 @@ export function walkDepth( } } -export function toCss(ast: AstNode[]) { - let atRoots: string = '' +// Optimize the AST for printing where all the special nodes that require custom +// handling are handled such that the printing is a 1-to-1 transformation. +export function optimizeAst(ast: AstNode[]) { + let atRoots: AstNode[] = [] let seenAtProperties = new Set() let propertyFallbacksRoot: Declaration[] = [] let propertyFallbacksUniversal: Declaration[] = [] + function transform( + node: AstNode, + parent: Extract['nodes'], + depth = 0, + ) { + // Declaration + if (node.kind === 'declaration') { + if (node.property === '--tw-sort' || node.value === undefined || node.value === null) { + return + } + parent.push(node) + } + + // Rule + else if (node.kind === 'rule') { + let copy = { ...node, nodes: [] } + for (let child of node.nodes) { + transform(child, copy.nodes, depth + 1) + } + parent.push(copy) + } + + // AtRule `@property` + else if (node.kind === 'at-rule' && node.name === '@property' && depth === 0) { + // Don't output duplicate `@property` rules + if (seenAtProperties.has(node.params)) { + return + } + + // Collect fallbacks for `@property` rules for Firefox support + // We turn these into rules on `:root` or `*` and some pseudo-elements + // based on the value of `inherits`` + let property = node.params + let initialValue = null + let inherits = false + + for (let prop of node.nodes) { + if (prop.kind !== 'declaration') continue + if (prop.property === 'initial-value') { + initialValue = prop.value + } else if (prop.property === 'inherits') { + inherits = prop.value === 'true' + } + } + + if (inherits) { + propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial')) + } else { + propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial')) + } + + seenAtProperties.add(node.params) + + let copy = { ...node, nodes: [] } + for (let child of node.nodes) { + transform(child, copy.nodes, depth + 1) + } + parent.push(copy) + } + + // AtRule + else if (node.kind === 'at-rule') { + let copy = { ...node, nodes: [] } + for (let child of node.nodes) { + transform(child, copy.nodes, depth + 1) + } + parent.push(copy) + } + + // AtRoot + else if (node.kind === 'at-root') { + for (let child of node.nodes) { + let newParent: AstNode[] = [] + transform(child, newParent, 0) + for (let child of newParent) { + atRoots.push(child) + } + } + } + + // Context + else if (node.kind === 'context') { + for (let child of node.nodes) { + transform(child, parent, depth) + } + } + + // Comment + else if (node.kind === 'comment') { + parent.push(node) + } + + // Unknown + else { + node satisfies never + } + } + + let newAst: AstNode[] = [] + for (let node of ast) { + transform(node, newAst, 0) + } + + // Fallbacks + { + let fallbackAst = [] + + if (propertyFallbacksRoot.length > 0) { + fallbackAst.push(rule(':root', propertyFallbacksRoot)) + } + + if (propertyFallbacksUniversal.length > 0) { + fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)) + } + + if (fallbackAst.length > 0) { + newAst.push( + atRule('@supports', '(-moz-orient: inline)', [atRule('@layer', 'base', fallbackAst)]), + ) + } + } + + return newAst.concat(atRoots) +} + +export function toCss(ast: AstNode[]) { function stringify(node: AstNode, depth = 0): string { let css = '' let indent = ' '.repeat(depth) + // Declaration + if (node.kind === 'declaration') { + css += `${indent}${node.property}: ${node.value}${node.important ? ' !important' : ''};\n` + } + // Rule - if (node.kind === 'rule') { + else if (node.kind === 'rule') { css += `${indent}${node.selector} {\n` for (let child of node.nodes) { css += stringify(child, depth + 1) @@ -240,38 +373,6 @@ export function toCss(ast: AstNode[]) { return `${indent}${node.name} ${node.params};\n` } - // - else if (node.name === '@property' && depth === 0) { - // Don't output duplicate `@property` rules - if (seenAtProperties.has(node.params)) { - return '' - } - - // Collect fallbacks for `@property` rules for Firefox support - // We turn these into rules on `:root` or `*` and some pseudo-elements - // based on the value of `inherits`` - let property = node.params - let initialValue = null - let inherits = false - - for (let prop of node.nodes) { - if (prop.kind !== 'declaration') continue - if (prop.property === 'initial-value') { - initialValue = prop.value - } else if (prop.property === 'inherits') { - inherits = prop.value === 'true' - } - } - - if (inherits) { - propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial')) - } else { - propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial')) - } - - seenAtProperties.add(node.params) - } - css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n` for (let child of node.nodes) { css += stringify(child, depth + 1) @@ -284,24 +385,16 @@ export function toCss(ast: AstNode[]) { css += `${indent}/*${node.value}*/\n` } - // Context Node - else if (node.kind === 'context') { - for (let child of node.nodes) { - css += stringify(child, depth) - } + // These should've been handled already by `prepareAstForPrinting` which + // means we can safely ignore them here. We return an empty string + // immediately to signal that something went wrong. + else if (node.kind === 'context' || node.kind === 'at-root') { + return '' } - // AtRoot Node - else if (node.kind === 'at-root') { - for (let child of node.nodes) { - atRoots += stringify(child, 0) - } - return css - } - - // Declaration - else if (node.property !== '--tw-sort' && node.value !== undefined && node.value !== null) { - css += `${indent}${node.property}: ${node.value}${node.important ? ' !important' : ''};\n` + // Unknown + else { + node satisfies never } return css @@ -316,23 +409,5 @@ export function toCss(ast: AstNode[]) { } } - let fallbackAst = [] - - if (propertyFallbacksRoot.length) { - fallbackAst.push(rule(':root', propertyFallbacksRoot)) - } - - if (propertyFallbacksUniversal.length) { - fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)) - } - - let fallback = '' - - if (fallbackAst.length) { - fallback = stringify( - atRule('@supports', '(-moz-orient: inline)', [atRule('@layer', 'base', fallbackAst)]), - ) - } - - return `${css}${fallback}${atRoots}` + return css } diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 99c84483c73f..95f4db9e531a 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -17,10 +17,10 @@ import * as SelectorParser from './selector-parser' export type Config = UserConfig export type PluginFn = (api: PluginAPI) => void -export type PluginWithConfig = { - handler: PluginFn; - config?: UserConfig; - +export type PluginWithConfig = { + handler: PluginFn + config?: UserConfig + /** @internal */ reference?: boolean } diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index 3b6e283e5812..c12c63051cee 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -1,6 +1,7 @@ import { atRule, comment, + decl, rule, type AstNode, type AtRule, @@ -434,15 +435,7 @@ export function parse(input: string) { // Attach the declaration to the parent. if (parent) { - let importantIdx = buffer.indexOf('!important', colonIdx + 1) - parent.nodes.push({ - kind: 'declaration', - property: buffer.slice(0, colonIdx).trim(), - value: buffer - .slice(colonIdx + 1, importantIdx === -1 ? buffer.length : importantIdx) - .trim(), - important: importantIdx !== -1, - } satisfies Declaration) + parent.nodes.push(parseDeclaration(buffer, colonIdx)) } } } @@ -552,10 +545,9 @@ export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule { function parseDeclaration(buffer: string, colonIdx: number = buffer.indexOf(':')): Declaration { let importantIdx = buffer.indexOf('!important', colonIdx + 1) - return { - kind: 'declaration', - property: buffer.slice(0, colonIdx).trim(), - value: buffer.slice(colonIdx + 1, importantIdx === -1 ? buffer.length : importantIdx).trim(), - important: importantIdx !== -1, - } + return decl( + buffer.slice(0, colonIdx).trim(), + buffer.slice(colonIdx + 1, importantIdx === -1 ? buffer.length : importantIdx).trim(), + importantIdx !== -1, + ) } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index e8cdbd4a97cf..9904608edd41 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -6,6 +6,7 @@ import { comment, context as contextNode, decl, + optimizeAst, rule, styleRule, toCss, @@ -99,7 +100,7 @@ export const enum Features { } async function parseCss( - css: string, + ast: AstNode[], { base = '', loadModule = throwOnLoadModule, @@ -107,7 +108,7 @@ async function parseCss( }: CompileOptions = {}, ) { let features = Features.None - let ast = [contextNode({ base }, CSS.parse(css))] as AstNode[] + ast = [contextNode({ base }, ast)] as AstNode[] features |= await substituteAtImports(ast, base, loadStylesheet) @@ -556,16 +557,16 @@ async function parseCss( } } -export async function compile( - css: string, +export async function compileAst( + input: AstNode[], opts: CompileOptions = {}, ): Promise<{ globs: { base: string; pattern: string }[] root: Root features: Features - build(candidates: string[]): string + build(candidates: string[]): AstNode[] }> { - let { designSystem, ast, globs, root, utilitiesNode, features } = await parseCss(css, opts) + let { designSystem, ast, globs, root, utilitiesNode, features } = await parseCss(input, opts) if (process.env.NODE_ENV !== 'test') { ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `)) @@ -580,7 +581,7 @@ export async function compile( // resulted in a generated AST Node. All the other `rawCandidates` are invalid // and should be ignored. let allValidCandidates = new Set() - let compiledCss = features !== Features.None ? toCss(ast) : css + let compiled = null as AstNode[] | null let previousAstNodeCount = 0 return { @@ -588,6 +589,15 @@ export async function compile( root, features, build(newRawCandidates: string[]) { + if (features === Features.None) { + return input + } + + if (!utilitiesNode) { + compiled ??= optimizeAst(ast) + return compiled + } + let didChange = false // Add all new candidates unless we know that they are invalid. @@ -602,34 +612,65 @@ export async function compile( // If no new candidates were added, we can return the original CSS. This // currently assumes that we only add new candidates and never remove any. if (!didChange) { - return compiledCss + compiled ??= optimizeAst(ast) + return compiled } - if (utilitiesNode) { - let newNodes = compileCandidates(allValidCandidates, designSystem, { - onInvalidCandidate, - }).astNodes + let newNodes = compileCandidates(allValidCandidates, designSystem, { + onInvalidCandidate, + }).astNodes - // If no new ast nodes were generated, then we can return the original - // CSS. This currently assumes that we only add new ast nodes and never - // remove any. - if (previousAstNodeCount === newNodes.length) { - return compiledCss - } + // If no new ast nodes were generated, then we can return the original + // CSS. This currently assumes that we only add new ast nodes and never + // remove any. + if (previousAstNodeCount === newNodes.length) { + compiled ??= optimizeAst(ast) + return compiled + } + + previousAstNodeCount = newNodes.length + + utilitiesNode.nodes = newNodes + + compiled = optimizeAst(ast) + return compiled + }, + } +} - previousAstNodeCount = newNodes.length +export async function compile( + css: string, + opts: CompileOptions = {}, +): Promise<{ + globs: { base: string; pattern: string }[] + root: Root + features: Features + build(candidates: string[]): string +}> { + let ast = CSS.parse(css) + let api = await compileAst(ast, opts) + let compiledAst = ast + let compiledCss = css - utilitiesNode.nodes = newNodes - compiledCss = toCss(ast) + return { + ...api, + build(newCandidates) { + let newAst = api.build(newCandidates) + + if (newAst === compiledAst) { + return compiledCss } + compiledCss = toCss(newAst) + compiledAst = newAst + return compiledCss }, } } export async function __unstable__loadDesignSystem(css: string, opts: CompileOptions = {}) { - let result = await parseCss(css, opts) + let result = await parseCss(CSS.parse(css), opts) return result.designSystem }