From a5407e363038a3a0382acd99d432c8e7258a53ed Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 3 Dec 2024 13:29:48 +0100 Subject: [PATCH 01/25] allow to pass in the `!important` flag --- packages/tailwindcss/src/ast.ts | 4 ++-- packages/tailwindcss/src/css-parser.ts | 22 +++++++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index e20f6042223a..3d3f0a613238 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, } } 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, + ) } From 29cc49d7c10aa5086da34ddfaf2bfb1b8b02418f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 3 Dec 2024 13:32:39 +0100 Subject: [PATCH 02/25] implement `postCssAstToCssAst` and `cssAstToPostCssAst` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These methods can convert PostCSS ASTs to our internal CSS ASTs and vice versa. This allows us to skip a step where introduce parsing/stringification. Instead of: - `PostCSS AST` -> `.toString()` -> `postcss.parse` -> `CSS AST` - `CSS AST` -> `toCSS(ast)` -> `CSS.parse` -> `PostCSS AST` We will now do this instead: - `PostCSS AST` -> `transform(…)` -> `CSS AST` - `CSS AST` -> `transform(…)` -> `PostCSS AST` --- packages/@tailwindcss-postcss/src/ast.test.ts | 107 ++++++++++++++++++ packages/@tailwindcss-postcss/src/ast.ts | 98 ++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 packages/@tailwindcss-postcss/src/ast.test.ts create mode 100644 packages/@tailwindcss-postcss/src/ast.ts diff --git a/packages/@tailwindcss-postcss/src/ast.test.ts b/packages/@tailwindcss-postcss/src/ast.test.ts new file mode 100644 index 000000000000..632295a511bf --- /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..e27c65805334 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -0,0 +1,98 @@ +import postcss, { + type ChildNode as PostCssChildNode, + type Container as PostCssContainerNode, + type Root as PostCssRoot, +} from 'postcss' +import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast' + +export function cssAstToPostCssAst(ast: AstNode[]): PostCssRoot { + let root = postcss.root() + + function transform(node: AstNode, parent: PostCssContainerNode) { + // Declaration + if (node.kind === 'declaration') { + parent.append( + postcss.decl({ prop: node.property, value: node.value ?? '', important: node.important }), + ) + } + + // Rule + else if (node.kind === 'rule') { + let astNode = postcss.rule({ selector: node.selector }) + 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 }) + parent.append(astNode) + for (let child of node.nodes) { + transform(child, astNode) + } + } + + // Comment + else if (node.kind === 'comment') { + parent.append(postcss.comment({ text: node.value })) + } + + // 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') { + parent.push(comment(node.text)) + } + + // Unknown + else { + node satisfies never + } + } + + let ast: AstNode[] = [] + root.each((node) => transform(node, ast)) + + return ast +} From 94aeeab64f1bee642de795081c5c9284abce44b8 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 3 Dec 2024 16:42:46 +0100 Subject: [PATCH 03/25] optimize AST before printing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces an `optimizeAst(…)` function that creates a fresh AST from an AST but handles all the edge cases we used to have in `toCss(…)`. For example, `@property` is deduped (and fallbacks are generated for older browsers), `Context` nodes are transparent and `AtRoots` move nodes into the actual root. This allows us to simplify the `toCss(…)` code to be a 1-to-1 translation and simply print `declarations`, `rules`, `at-rules` and comments. We don't have to worry about the other special logic. This also means that we don't have to re-introduce the same logic in our PostCSS AST <-> Our AST transformations. --- In addition: I ran some checks on the Catalyst codebase, and noticed that we have the most declarations, then rules, then at-rules, then at-roots, then context nodes and last but not least comments. With this information in mind, the if-branches are using this information to first check for declarations, then rules, ... --- packages/tailwindcss/src/ast.test.ts | 7 +- packages/tailwindcss/src/ast.ts | 215 ++++++++++++++++++--------- packages/tailwindcss/src/index.ts | 6 +- 3 files changed, 153 insertions(+), 75 deletions(-) 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 3d3f0a613238..74a2bc63eb7c 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -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/index.ts b/packages/tailwindcss/src/index.ts index e8cdbd4a97cf..bd06b56d34db 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, @@ -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 compiledCss = features !== Features.None ? toCss(optimizeAst(ast)) : css let previousAstNodeCount = 0 return { @@ -620,7 +621,8 @@ export async function compile( previousAstNodeCount = newNodes.length utilitiesNode.nodes = newNodes - compiledCss = toCss(ast) + + compiledCss = toCss(optimizeAst(ast)) } return compiledCss From 7eefb19aff0214c1128b6cd6c61bb2894d8f7fd4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 3 Dec 2024 18:07:12 +0100 Subject: [PATCH 04/25] =?UTF-8?q?implement=20new=20`compileAst(=E2=80=A6)`?= =?UTF-8?q?=20and=20update=20`compile(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tailwindcss/src/index.ts | 53 ++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index bd06b56d34db..217d49b7fd5c 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -100,7 +100,7 @@ export const enum Features { } async function parseCss( - css: string, + ast: AstNode[], { base = '', loadModule = throwOnLoadModule, @@ -108,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) @@ -557,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 `)) @@ -581,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(optimizeAst(ast)) : css + let compiled = features !== Features.None ? optimizeAst(ast) : input let previousAstNodeCount = 0 return { @@ -603,7 +603,7 @@ 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 + return compiled } if (utilitiesNode) { @@ -615,23 +615,54 @@ export async function compile( // CSS. This currently assumes that we only add new ast nodes and never // remove any. if (previousAstNodeCount === newNodes.length) { - return compiledCss + return compiled } previousAstNodeCount = newNodes.length utilitiesNode.nodes = newNodes - compiledCss = toCss(optimizeAst(ast)) + compiled = optimizeAst(ast) } + return compiled + }, + } +} + +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 + + 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 } From 531670b795c28389b0a014601260b7817a9ce1a8 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 3 Dec 2024 18:08:51 +0100 Subject: [PATCH 05/25] expose `compileAst` from `@tailwindcss/node` --- packages/@tailwindcss-node/src/compile.ts | 65 +++++++++++++++++++++++ packages/@tailwindcss-node/src/index.ts | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index f7e3537515c0..849d141ba5c5 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,6 +18,69 @@ export { Features } export type Resolver = (id: string, base: string) => Promise +export async function compileAst( + ast: AstNode[], + { + base, + onDependency, + shouldRewriteUrls, + + customCssResolver, + customJsResolver, + }: { + base: string + onDependency: (path: string) => void + shouldRewriteUrls?: boolean + + customCssResolver?: Resolver + customJsResolver?: Resolver + }, +) { + let compiler = await _compileAst(ast, { + base, + async loadModule(id, base) { + return loadModule(id, base, onDependency, customJsResolver) + }, + async loadStylesheet(id, base) { + let sheet = await loadStylesheet(id, base, onDependency, customCssResolver) + + if (shouldRewriteUrls) { + sheet.content = await rewriteUrls({ + css: sheet.content, + root: base, + base: sheet.base, + }) + } + + return sheet + }, + }) + + // Verify if the `source(…)` path exists (until the glob pattern starts) + if (compiler.root && compiler.root !== 'none') { + let globSymbols = /[*{]/ + let basePath = [] + for (let segment of compiler.root.pattern.split('/')) { + if (globSymbols.test(segment)) { + break + } + + basePath.push(segment) + } + + let exists = await fsPromises + .stat(path.resolve(base, basePath.join('/'))) + .then((stat) => stat.isDirectory()) + .catch(() => false) + + if (!exists) { + throw new Error(`The \`source(${compiler.root.pattern})\` does not exist`) + } + } + + return compiler +} + export async function compile( css: string, { 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 } From 6cabeea490847b32734204e0b6bfc8ae936efec9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 3 Dec 2024 18:13:08 +0100 Subject: [PATCH 06/25] WIP: PostCSS implementation --- packages/@tailwindcss-postcss/src/index.ts | 45 +++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index a1759f9f4c69..54b7d46442c4 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -1,16 +1,18 @@ 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 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 @@ -69,7 +71,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,6 +130,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } } + let ast: AstNode[] = [] let css = '' if ( @@ -206,24 +209,30 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } env.DEBUG && console.time('[@tailwindcss/postcss] Build CSS') - css = context.compiler.build(candidates) + ast = 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') + if (optimize) { + // 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') + } + context.css = css + + env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') + root.removeAll() + root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts)) + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Update PostCSS AST') + env.DEBUG && + console.timeEnd('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') + } else { + root.removeAll() + root.append(cssAstToPostCssAst(ast)) } - context.css = css - - env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') - root.removeAll() - root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts)) - env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Update PostCSS AST') - env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') }, }, ], From 42401b496f2971b58d2a49617660960a1e732f8c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 3 Dec 2024 12:34:18 -0500 Subject: [PATCH 07/25] Tweak code --- packages/@tailwindcss-postcss/src/ast.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts index e27c65805334..eed5349b31d3 100644 --- a/packages/@tailwindcss-postcss/src/ast.ts +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -11,9 +11,12 @@ export function cssAstToPostCssAst(ast: AstNode[]): PostCssRoot { function transform(node: AstNode, parent: PostCssContainerNode) { // Declaration if (node.kind === 'declaration') { - parent.append( - postcss.decl({ prop: node.property, value: node.value ?? '', important: node.important }), - ) + let astNode = postcss.decl({ + prop: node.property, + value: node.value ?? '', + important: node.important, + }) + parent.append(astNode) } // Rule @@ -36,7 +39,8 @@ export function cssAstToPostCssAst(ast: AstNode[]): PostCssRoot { // Comment else if (node.kind === 'comment') { - parent.append(postcss.comment({ text: node.value })) + let astNode = postcss.comment({ text: node.value }) + parent.append(astNode) } // AtRoot & Context should not happen From 0e954b2a685c04e96b9e46886bdecd774146af0b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 3 Dec 2024 12:34:52 -0500 Subject: [PATCH 08/25] =?UTF-8?q?Convert=20our=20AST=20to=20PostCSS?= =?UTF-8?q?=E2=80=99s=20AST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@tailwindcss-postcss/src/index.ts | 46 ++++++++++++---------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 54b7d46442c4..b246f2023d99 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -14,8 +14,9 @@ interface CacheEntry { mtimes: Map compiler: null | Awaited> scanner: null | Scanner - css: string - optimizedCss: string + ast: AstNode[] + cachedAst: postcss.Root + optimizedAst: postcss.Root fullRebuildPaths: string[] } let cache = new QuickLRU({ maxSize: 50 }) @@ -27,8 +28,11 @@ function getContextFromCache(inputFile: string, opts: PluginOptions): CacheEntry mtimes: new Map(), compiler: null, scanner: null, - css: '', - optimizedCss: '', + + ast: [], + cachedAst: postcss.root(), + optimizedAst: postcss.root(), + fullRebuildPaths: [] as string[], } cache.set(key, entry) @@ -212,27 +216,27 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { ast = context.compiler.build(candidates) env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build CSS') - if (optimize) { - // Replace CSS - if (css !== context.css && optimize) { + if (context.ast !== ast) { + // Convert our AST to a PostCSS AST + context.cachedAst = cssAstToPostCssAst(ast) + + if (optimize) { env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS') - context.optimizedCss = optimizeCss(css, { - minify: typeof optimize === 'object' ? optimize.minify : true, - }) + context.optimizedAst = postcss.parse( + optimizeCss(context.cachedAst.toString(), { + minify: typeof optimize === 'object' ? optimize.minify : true, + }), + result.opts, + ) env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Optimize CSS') } - context.css = css - - env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') - root.removeAll() - root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts)) - env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Update PostCSS AST') - env.DEBUG && - console.timeEnd('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') - } else { - root.removeAll() - root.append(cssAstToPostCssAst(ast)) } + + env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') + root.removeAll() + root.append(optimize ? context.optimizedAst.nodes : context.cachedAst.nodes) + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Update PostCSS AST') + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') }, }, ], From 79cadb886fba443f3494a040398af4141ec7614e Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 3 Dec 2024 12:35:16 -0500 Subject: [PATCH 09/25] Pass through source information --- packages/@tailwindcss-postcss/src/ast.ts | 8 +++++++- packages/@tailwindcss-postcss/src/index.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts index eed5349b31d3..8998dee6a595 100644 --- a/packages/@tailwindcss-postcss/src/ast.ts +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -2,11 +2,13 @@ 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' -export function cssAstToPostCssAst(ast: AstNode[]): PostCssRoot { +export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undefined): PostCssRoot { let root = postcss.root() + root.source = source function transform(node: AstNode, parent: PostCssContainerNode) { // Declaration @@ -16,12 +18,14 @@ export function cssAstToPostCssAst(ast: AstNode[]): PostCssRoot { 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 parent.append(astNode) for (let child of node.nodes) { transform(child, astNode) @@ -31,6 +35,7 @@ export function cssAstToPostCssAst(ast: AstNode[]): PostCssRoot { // AtRule else if (node.kind === 'at-rule') { let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params }) + astNode.source = source parent.append(astNode) for (let child of node.nodes) { transform(child, astNode) @@ -40,6 +45,7 @@ export function cssAstToPostCssAst(ast: AstNode[]): PostCssRoot { // Comment else if (node.kind === 'comment') { let astNode = postcss.comment({ text: node.value }) + astNode.source = source parent.append(astNode) } diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index b246f2023d99..f43361751340 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -218,7 +218,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { if (context.ast !== ast) { // Convert our AST to a PostCSS AST - context.cachedAst = cssAstToPostCssAst(ast) + context.cachedAst = cssAstToPostCssAst(ast, root.source) if (optimize) { env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS') From 2adaf42f28ace6ef9b9d89e38e834773136e5bce Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 10:54:05 +0100 Subject: [PATCH 10/25] print CSS from our own AST --- packages/@tailwindcss-postcss/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index f43361751340..07978d95e770 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -6,7 +6,7 @@ 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 type { AstNode } from '../../tailwindcss/src/ast' +import { toCss, type AstNode } from '../../tailwindcss/src/ast' import { cssAstToPostCssAst, postCssAstToCssAst } from './ast' import fixRelativePathsPlugin from './postcss-fix-relative-paths' @@ -212,9 +212,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } } - env.DEBUG && console.time('[@tailwindcss/postcss] Build CSS') + env.DEBUG && console.time('[@tailwindcss/postcss] Build AST') ast = context.compiler.build(candidates) - env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build CSS') + env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build AST') if (context.ast !== ast) { // Convert our AST to a PostCSS AST @@ -223,7 +223,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { if (optimize) { env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS') context.optimizedAst = postcss.parse( - optimizeCss(context.cachedAst.toString(), { + optimizeCss(toCss(ast), { minify: typeof optimize === 'object' ? optimize.minify : true, }), result.opts, From ef17c45001587c3060684ce41a067e815ef7b52a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 11:06:40 +0100 Subject: [PATCH 11/25] only convert our AST into a PostCSS AST when not optimizing If we are not optimizing, we don't need the `context.cachedAst` because we will use the optimziedAst instead. --- packages/@tailwindcss-postcss/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 07978d95e770..b6322b4154d9 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -217,9 +217,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build AST') if (context.ast !== ast) { - // Convert our AST to a PostCSS AST - context.cachedAst = cssAstToPostCssAst(ast, root.source) - if (optimize) { env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS') context.optimizedAst = postcss.parse( @@ -229,6 +226,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { result.opts, ) env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Optimize CSS') + } else { + // Convert our AST to a PostCSS AST + context.cachedAst = cssAstToPostCssAst(ast, root.source) } } From f006e36b3f862a110a00a3df54fe0c4736874cd6 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 11:08:41 +0100 Subject: [PATCH 12/25] remove unused variable --- packages/@tailwindcss-postcss/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index b6322b4154d9..dd133c4c3883 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -135,7 +135,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } let ast: AstNode[] = [] - let css = '' if ( rebuildStrategy === 'full' && From 709947cbd70ac57cbfc2a292875fce4f2cc8b1ab Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 11:41:09 +0100 Subject: [PATCH 13/25] rename variables to be more explicit So many ASTs, but which ones? --- packages/@tailwindcss-postcss/src/index.ts | 26 ++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index dd133c4c3883..7671d3d5d8f8 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -14,9 +14,9 @@ interface CacheEntry { mtimes: Map compiler: null | Awaited> scanner: null | Scanner - ast: AstNode[] - cachedAst: postcss.Root - optimizedAst: postcss.Root + tailwindCssAst: AstNode[] + cachedPostCssAst: postcss.Root + optimizedPostCssAst: postcss.Root fullRebuildPaths: string[] } let cache = new QuickLRU({ maxSize: 50 }) @@ -29,9 +29,9 @@ function getContextFromCache(inputFile: string, opts: PluginOptions): CacheEntry compiler: null, scanner: null, - ast: [], - cachedAst: postcss.root(), - optimizedAst: postcss.root(), + tailwindCssAst: [], + cachedPostCssAst: postcss.root(), + optimizedPostCssAst: postcss.root(), fullRebuildPaths: [] as string[], } @@ -134,8 +134,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } } - let ast: AstNode[] = [] - if ( rebuildStrategy === 'full' && // We can re-use the compiler if it was created during the @@ -212,14 +210,14 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } env.DEBUG && console.time('[@tailwindcss/postcss] Build AST') - ast = context.compiler.build(candidates) + let tailwindCssAst = context.compiler.build(candidates) env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Build AST') - if (context.ast !== ast) { + if (context.tailwindCssAst !== tailwindCssAst) { if (optimize) { env.DEBUG && console.time('[@tailwindcss/postcss] Optimize CSS') - context.optimizedAst = postcss.parse( - optimizeCss(toCss(ast), { + context.optimizedPostCssAst = postcss.parse( + optimizeCss(toCss(tailwindCssAst), { minify: typeof optimize === 'object' ? optimize.minify : true, }), result.opts, @@ -227,13 +225,13 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Optimize CSS') } else { // Convert our AST to a PostCSS AST - context.cachedAst = cssAstToPostCssAst(ast, root.source) + context.cachedPostCssAst = cssAstToPostCssAst(tailwindCssAst, root.source) } } env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') root.removeAll() - root.append(optimize ? context.optimizedAst.nodes : context.cachedAst.nodes) + root.append(optimize ? context.optimizedPostCssAst.nodes : context.cachedPostCssAst.nodes) env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Update PostCSS AST') env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') }, From a4df4767f671b0dfbaae296bdad58f6e74dcd262 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 11:46:22 +0100 Subject: [PATCH 14/25] =?UTF-8?q?add=20timing=20info=20around=20`cssAstToP?= =?UTF-8?q?ostCssAst(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@tailwindcss-postcss/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 7671d3d5d8f8..d47d1b345a3d 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -225,7 +225,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { 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') } } From 71c1c0daba7a7be69fbd1934131710692b09f28e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 12:08:33 +0100 Subject: [PATCH 15/25] =?UTF-8?q?restructure=20`build(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to bail earlier and delay the `optimizeAst(…)` call and only do it when it's necessary. --- packages/tailwindcss/src/index.ts | 40 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 217d49b7fd5c..57a90e952f0f 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -581,7 +581,7 @@ export async function compileAst( // resulted in a generated AST Node. All the other `rawCandidates` are invalid // and should be ignored. let allValidCandidates = new Set() - let compiled = features !== Features.None ? optimizeAst(ast) : input + let compiled = null as AstNode[] | null let previousAstNodeCount = 0 return { @@ -589,6 +589,15 @@ export async function compileAst( root, features, build(newRawCandidates: string[]) { + if (features === Features.None) { + return input + } + + if (!utilitiesNode) { + if (compiled === null) compiled = optimizeAst(ast) + return compiled + } + let didChange = false // Add all new candidates unless we know that they are invalid. @@ -603,28 +612,27 @@ export async function compileAst( // 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) { + if (compiled === null) 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 compiled - } - - previousAstNodeCount = newNodes.length + // 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) { + if (compiled === null) compiled = optimizeAst(ast) + return compiled + } - utilitiesNode.nodes = newNodes + previousAstNodeCount = newNodes.length - compiled = optimizeAst(ast) - } + utilitiesNode.nodes = newNodes + compiled = optimizeAst(ast) return compiled }, } From a24e4c4931e93326be94b9b0587e9b0c14ab0ebe Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 12:29:38 +0100 Subject: [PATCH 16/25] track the `tailwindCssAst` for subsequent cache hits --- packages/@tailwindcss-postcss/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index d47d1b345a3d..8bee09f9cb5d 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -22,7 +22,7 @@ interface CacheEntry { 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(), @@ -232,6 +232,8 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } } + context.tailwindCssAst = tailwindCssAst + env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') root.removeAll() root.append(optimize ? context.optimizedPostCssAst.nodes : context.cachedPostCssAst.nodes) From 260beed0047c623e89dd97dcdf771ab7b6c5d8e8 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 12:40:48 +0100 Subject: [PATCH 17/25] ensure PostCSS AST nodes have semicolons --- packages/@tailwindcss-postcss/src/ast.test.ts | 4 ++-- packages/@tailwindcss-postcss/src/ast.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/ast.test.ts b/packages/@tailwindcss-postcss/src/ast.test.ts index 632295a511bf..dd7095428e1c 100644 --- a/packages/@tailwindcss-postcss/src/ast.test.ts +++ b/packages/@tailwindcss-postcss/src/ast.test.ts @@ -93,13 +93,13 @@ it('should convert a Tailwind CSS AST into a PostCSS AST', () => { .foo { color: red; &:hover { - color: blue + color: blue; } .bar { color: green !important; background-color: yellow; @media (min-width: 640px) { - color: orange + color: orange; } } }" diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts index 8998dee6a595..1d464bd68fcb 100644 --- a/packages/@tailwindcss-postcss/src/ast.ts +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -26,6 +26,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef 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) @@ -36,6 +37,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef 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) From 87065a8ba59f73aac9dfa0970f09456e408c9bb5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 12:58:51 +0100 Subject: [PATCH 18/25] handle comments correctly 1. If a PostCSS comment is coming in, only map it to our AST's comment node if it starts with `!` (license comments). 2. If a comment from our AST is mapped to a PostCSS AST Comment, then remove the default whitespace and rely on the whitespace encoded in `node.value`. --- packages/@tailwindcss-postcss/src/ast.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts index 1d464bd68fcb..201dd4bf8ac9 100644 --- a/packages/@tailwindcss-postcss/src/ast.ts +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -6,6 +6,8 @@ import postcss, { } 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 @@ -47,6 +49,10 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef // 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) } @@ -94,6 +100,7 @@ export function postCssAstToCssAst(root: PostCssRoot): AstNode[] { // Comment else if (node.type === 'comment') { + if (node.text.charCodeAt(0) !== EXCLAMATION_MARK) return parent.push(comment(node.text)) } From 3de1864b593a7130bb96df4316988e3e8875e91b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 13:38:20 +0100 Subject: [PATCH 19/25] compute current_mtimes in parallel per file/directory --- crates/oxide/src/lib.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) 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 { From 98f66cbc487a47b714b056c4e4f42c8fd2f4816b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 13:38:34 +0100 Subject: [PATCH 20/25] remove instrumentation from `read_dir` --- crates/oxide/src/scanner/allowed_paths.rs | 1 - 1 file changed, 1 deletion(-) 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) From 84bde5e544d467b81c7c790cddc1d5262749c000 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 14:12:47 +0100 Subject: [PATCH 21/25] trick PostCSS into using 2 spaces instead of 4 Bonus points: this also means that PostCSS doesn't need to do a walk to try and detect the indentation. --- packages/@tailwindcss-postcss/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 8bee09f9cb5d..5e602e9c2cb5 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -237,6 +237,11 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') root.removeAll() root.append(optimize ? context.optimizedPostCssAst.nodes : context.cachedPostCssAst.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') }, From 7aeea03b574eef82fac5cc4cff02b4176e0cbbc9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 14:18:21 +0100 Subject: [PATCH 22/25] use `??=` shorthand --- packages/tailwindcss/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 57a90e952f0f..9904608edd41 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -594,7 +594,7 @@ export async function compileAst( } if (!utilitiesNode) { - if (compiled === null) compiled = optimizeAst(ast) + compiled ??= optimizeAst(ast) return compiled } @@ -612,7 +612,7 @@ export async function compileAst( // 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) { - if (compiled === null) compiled = optimizeAst(ast) + compiled ??= optimizeAst(ast) return compiled } @@ -624,7 +624,7 @@ export async function compileAst( // CSS. This currently assumes that we only add new ast nodes and never // remove any. if (previousAstNodeCount === newNodes.length) { - if (compiled === null) compiled = optimizeAst(ast) + compiled ??= optimizeAst(ast) return compiled } From 1a0495703e0673ba7fe49dc026b50c02d1c1c126 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 14:21:21 +0100 Subject: [PATCH 23/25] ensure `compile` and `compileAst` re-use the same logic --- packages/@tailwindcss-node/src/compile.ts | 115 +++++++--------------- 1 file changed, 33 insertions(+), 82 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 849d141ba5c5..b946c79defd2 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -18,93 +18,29 @@ export { Features } export type Resolver = (id: string, base: string) => Promise -export async function compileAst( - ast: AstNode[], - { - base, - onDependency, - shouldRewriteUrls, - - customCssResolver, - customJsResolver, - }: { - base: string - onDependency: (path: string) => void - shouldRewriteUrls?: boolean - - customCssResolver?: Resolver - customJsResolver?: Resolver - }, -) { - let compiler = await _compileAst(ast, { - base, - async loadModule(id, base) { - return loadModule(id, base, onDependency, customJsResolver) - }, - async loadStylesheet(id, base) { - let sheet = await loadStylesheet(id, base, onDependency, customCssResolver) - - if (shouldRewriteUrls) { - sheet.content = await rewriteUrls({ - css: sheet.content, - root: base, - base: sheet.base, - }) - } - - return sheet - }, - }) - - // Verify if the `source(…)` path exists (until the glob pattern starts) - if (compiler.root && compiler.root !== 'none') { - let globSymbols = /[*{]/ - let basePath = [] - for (let segment of compiler.root.pattern.split('/')) { - if (globSymbols.test(segment)) { - break - } +export interface CompileOptions { + base: string + onDependency: (path: string) => void + shouldRewriteUrls?: boolean - basePath.push(segment) - } - - let exists = await fsPromises - .stat(path.resolve(base, basePath.join('/'))) - .then((stat) => stat.isDirectory()) - .catch(() => false) - - if (!exists) { - throw new Error(`The \`source(${compiler.root.pattern})\` does not exist`) - } - } - - return compiler + customCssResolver?: Resolver + customJsResolver?: Resolver } -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, { +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) { @@ -117,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 = /[*{]/ @@ -140,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 } From 5f22986f6f339eddc5560b8ce0892682c4f2150a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 14:36:52 +0100 Subject: [PATCH 24/25] run format Unrelated to this PR, but noticed it when running `prettier` --- packages/tailwindcss/src/compat/plugin-api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 } From c5c7c9f678b7d78e878fa943977378dfae35fb0a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 4 Dec 2024 14:51:25 +0100 Subject: [PATCH 25/25] clone the cached PostCSS tree before appending it to the root --- packages/@tailwindcss-postcss/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 5e602e9c2cb5..d40d34bc3d98 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -236,7 +236,11 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { env.DEBUG && console.time('[@tailwindcss/postcss] Update PostCSS AST') root.removeAll() - root.append(optimize ? context.optimizedPostCssAst.nodes : context.cachedPostCssAst.nodes) + 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.