From d6e5858e564f8d641cae5c3e7ff70f13ba1ff73e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 19:03:36 +0100 Subject: [PATCH 01/24] return `rebuild` function from `compile` --- .../src/commands/build/index.ts | 34 ++++++++----------- packages/@tailwindcss-postcss/src/index.ts | 4 +-- packages/@tailwindcss-vite/src/index.ts | 2 +- packages/tailwindcss/src/index.ts | 15 ++++++-- packages/tailwindcss/src/test-utils/run.ts | 4 +-- packages/tailwindcss/tests/ui.spec.ts | 2 +- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index f3627ff91..ebfa0fde4 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -101,10 +101,11 @@ export async function handle(args: Result>) { // Compile the input let result = compile(input, candidates) + let compiledCss = result.css // Optimize the output if (args['--minify'] || args['--optimize']) { - result = optimizeCss(result, { + compiledCss = optimizeCss(compiledCss, { file: args['--input'] ?? 'input.css', minify: args['--minify'] ?? false, }) @@ -112,9 +113,9 @@ export async function handle(args: Result>) { // Write the output if (args['--output']) { - await outputFile(args['--output'], result) + await outputFile(args['--output'], compiledCss) } else { - println(result) + println(compiledCss) } let end = process.hrtime.bigint() @@ -168,19 +169,7 @@ export async function handle(args: Result>) { // Re-scan the directory to get the new `candidates`. candidates = scanDir({ base }).candidates - } - // Scan changed files only for incremental rebuilds. - else if (rebuildStrategy === 'incremental') { - let uniqueCandidates = new Set(candidates) - for (let candidate of scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)) { - uniqueCandidates.add(candidate) - } - candidates = Array.from(uniqueCandidates) - } - - // Resolve the input - if (rebuildStrategy === 'full') { // Collect the new `input` and `cssImportPaths`. ;[input, cssImportPaths] = await handleImports( args['--input'] @@ -190,14 +179,19 @@ export async function handle(args: Result>) { `, args['--input'] ?? base, ) + + result = compile(input, candidates) + compiledCss = result.css } - // Compile the input - let result = compile(input, candidates) + // Scan changed files only for incremental rebuilds. + else if (rebuildStrategy === 'incremental') { + compiledCss = result.rebuild(scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)) + } // Optimize the output if (args['--minify'] || args['--optimize']) { - result = optimizeCss(result, { + compiledCss = optimizeCss(compiledCss, { file: args['--input'] ?? 'input.css', minify: args['--minify'] ?? false, }) @@ -205,9 +199,9 @@ export async function handle(args: Result>) { // Write the output if (args['--output']) { - await outputFile(args['--output'], result) + await outputFile(args['--output'], compiledCss) } else { - println(result) + println(compiledCss) } let end = process.hrtime.bigint() diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index adab5cc56..13bca6283 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -53,7 +53,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // No `@tailwind` means we don't have to look for candidates if (!hasTailwind) { - replaceCss(compile(root.toString(), [])) + replaceCss(compile(root.toString(), []).css) return } @@ -83,7 +83,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { }) } - replaceCss(compile(root.toString(), candidates)) + replaceCss(compile(root.toString(), candidates).css) }, ], } diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index a4f66530c..e02f6e2cc 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -61,7 +61,7 @@ export default function tailwindcss(): Plugin[] { } function generateCss(css: string) { - return compile(css, Array.from(candidates)) + return compile(css, Array.from(candidates)).css } function generateOptimizedCss(css: string) { diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d066a5a1f..217d71d77 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -6,7 +6,13 @@ import * as CSS from './css-parser' import { buildDesignSystem } from './design-system' import { Theme } from './theme' -export function compile(css: string, rawCandidates: string[]) { +export function compile( + css: string, + rawCandidates: string[], +): { + rebuild(candidates: string[]): string + css: string +} { let ast = CSS.parse(css) if (process.env.NODE_ENV !== 'test') { @@ -162,7 +168,12 @@ export function compile(css: string, rawCandidates: string[]) { }) } - return toCss(ast) + return { + rebuild(rawCandidates: string[]) { + return toCss(ast) + }, + css: toCss(ast), + } } export function optimizeCss( diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts index 8df6feab2..5ba052633 100644 --- a/packages/tailwindcss/src/test-utils/run.ts +++ b/packages/tailwindcss/src/test-utils/run.ts @@ -1,9 +1,9 @@ import { compile, optimizeCss } from '..' export function compileCss(css: string, candidates: string[] = []) { - return optimizeCss(compile(css, candidates)).trim() + return optimizeCss(compile(css, candidates).css).trim() } export function run(candidates: string[]) { - return optimizeCss(compile('@tailwind utilities;', candidates)).trim() + return optimizeCss(compile('@tailwind utilities;', candidates).css).trim() } diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index eae93402c..126b194fc 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -243,7 +243,7 @@ async function render(page: Page, content: string) { } `, scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential), - ), + ).css, ), }) From 1e0b35d83659e4306b58718bf5e9c477039beaa7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 19:24:48 +0100 Subject: [PATCH 02/24] move sorting logic to the end --- packages/tailwindcss/src/compile.ts | 7 +++---- packages/tailwindcss/src/variants.ts | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index b91c19aca..5e5027edb 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -11,9 +11,6 @@ export function compileCandidates( designSystem: DesignSystem, { throwOnInvalidCandidate = false } = {}, ) { - // Ensure the candidates are sorted alphabetically - rawCandidates.sort() - let nodeSorting = new Map< AstNode, { properties: number[]; variants: bigint; candidate: string } @@ -153,7 +150,9 @@ export function compileCandidates( // Sort by lowest property index first (aSorting.properties[offset] ?? Infinity) - (zSorting.properties[offset] ?? Infinity) || // Sort by most properties first, then by least properties - zSorting.properties.length - aSorting.properties.length + zSorting.properties.length - aSorting.properties.length || + // Sort alphabetically + (aSorting.candidate < zSorting.candidate ? -1 : 1) ) }) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 3e5138c62..1a1cfa46e 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -94,7 +94,7 @@ export class Variants { if (z === null) return 1 if (a.kind === 'arbitrary' && z.kind === 'arbitrary') { - return a.selector.localeCompare(z.selector) + return a.selector < z.selector ? -1 : 1 } else if (a.kind === 'arbitrary') { return 1 } else if (z.kind === 'arbitrary') { @@ -114,7 +114,7 @@ export class Variants { let compareFn = this.compareFns.get(aOrder) if (compareFn === undefined) return 0 - return compareFn(a, z) + return compareFn(a, z) || (a.root < z.root ? -1 : 1) } keys() { From 43eecb981a1cc2d90eff325950bb382dc305b0ce Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 19:27:20 +0100 Subject: [PATCH 03/24] remove `localeCompare` calls --- packages/tailwindcss/src/intellisense.ts | 2 +- packages/tailwindcss/src/variants.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/intellisense.ts b/packages/tailwindcss/src/intellisense.ts index 8b67641c6..726e3c57b 100644 --- a/packages/tailwindcss/src/intellisense.ts +++ b/packages/tailwindcss/src/intellisense.ts @@ -40,7 +40,7 @@ export function getClassList(design: DesignSystem): ClassEntry[] { } } - list.sort((a, b) => a[0].localeCompare(b[0])) + list.sort((a, b) => (a[0] === b[0] ? 0 : a[0] < b[0] ? -1 : 1)) return list } diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 1a1cfa46e..3cc0919ad 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -469,7 +469,7 @@ export function createVariants(theme: Theme): Variants { let order = // Compare by bucket name - aBucket.localeCompare(zBucket) || + (aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) || // If bucket names are the same, compare by value (direction === 'asc' ? parseInt(aValue) - parseInt(zValue) @@ -489,7 +489,7 @@ export function createVariants(theme: Theme): Variants { // In this scenario, we want to alphabetically sort `calc(100%-1rem)` and // `calc(100%-2rem)` to make it deterministic. if (Number.isNaN(order)) { - return aValue.localeCompare(zValue) + return aValue < zValue ? -1 : 1 } return order From b0353871f7dc4bcfd3e0931c5aa900fc160725a9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 19:27:34 +0100 Subject: [PATCH 04/24] update tests to reflect order change --- packages/tailwindcss/src/variants.test.ts | 96 +++++++++++------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 675fa6931..3df052b5a 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1280,7 +1280,13 @@ test('supports', () => { expect( run(['supports-gap:grid', 'supports-[display:grid]:flex', 'supports-[selector(A_>_B)]:flex']), ).toMatchInlineSnapshot(` - "@supports (display: grid) { + "@supports (gap: var(--tw)) { + .supports-gap\\:grid { + display: grid; + } + } + + @supports (display: grid) { .supports-\\[display\\:grid\\]\\:flex { display: flex; } @@ -1290,12 +1296,6 @@ test('supports', () => { .supports-\\[selector\\(A_\\>_B\\)\\]\\:flex { display: flex; } - } - - @supports (gap: var(--tw)) { - .supports-gap\\:grid { - display: grid; - } }" `) }) @@ -1322,11 +1322,11 @@ test('not', () => { display: flex; } - .group-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:checked) *) { + .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *) { display: flex; } - .group-not-\\[\\:checked\\]\\:flex:is(:where(.group):not(:checked) *) { + .group-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):not(:checked) *) { display: flex; } @@ -1334,11 +1334,11 @@ test('not', () => { display: flex; } - .peer-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):not(:checked) ~ *) { + .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *) { display: flex; } - .peer-not-\\[\\:checked\\]\\:flex:is(:where(.peer):not(:checked) ~ *) { + .peer-not-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):not(:checked) ~ *) { display: flex; }" `) @@ -1362,11 +1362,11 @@ test('has', () => { display: flex; } - .group-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *) { + .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *) { display: flex; } - .group-has-\\[\\:checked\\]\\:flex:is(:where(.group):has(:checked) *) { + .group-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name):has(:checked) *) { display: flex; } @@ -1374,11 +1374,11 @@ test('has', () => { display: flex; } - .peer-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):has(:checked) ~ *) { + .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *) { display: flex; } - .peer-has-\\[\\:checked\\]\\:flex:is(:where(.peer):has(:checked) ~ *) { + .peer-has-\\[\\:checked\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name):has(:checked) ~ *) { display: flex; } @@ -1405,15 +1405,7 @@ test('aria', () => { 'peer-aria-checked/parent-name:flex', ]), ).toMatchInlineSnapshot(` - ".group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *) { - display: flex; - } - - .group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *) { - display: flex; - } - - .group-aria-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-checked="true"] *) { + ".group-aria-\\[modal\\]\\:flex:is(:where(.group)[aria-modal] *) { display: flex; } @@ -1421,7 +1413,11 @@ test('aria', () => { display: flex; } - .peer-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-modal] ~ *) { + .group-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-modal] *) { + display: flex; + } + + .group-aria-checked\\/parent-name\\:flex:is(:where(.group\\/parent-name)[aria-checked="true"] *) { display: flex; } @@ -1429,20 +1425,24 @@ test('aria', () => { display: flex; } - .peer-aria-checked\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-checked="true"] ~ *) { - display: flex; - } - .peer-aria-checked\\:flex:is(:where(.peer)[aria-checked="true"] ~ *) { display: flex; } - .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"] { + .peer-aria-\\[modal\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-modal] ~ *) { + display: flex; + } + + .peer-aria-checked\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[aria-checked="true"] ~ *) { display: flex; } .aria-checked\\:flex[aria-checked="true"] { display: flex; + } + + .aria-\\[invalid\\=spelling\\]\\:flex[aria-invalid="spelling"] { + display: flex; }" `) }) @@ -1460,15 +1460,11 @@ test('data', () => { 'peer-data-[disabled]/parent-name:flex', ]), ).toMatchInlineSnapshot(` - ".group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *) { + ".group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) { display: flex; } - .group-data-\\[disabled\\]\\:flex:is(:where(.group)[data-disabled] *) { - display: flex; - } - - .peer-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-disabled] ~ *) { + .group-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.group\\/parent-name)[data-disabled] *) { display: flex; } @@ -1476,12 +1472,16 @@ test('data', () => { display: flex; } - .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] { + .peer-data-\\[disabled\\]\\/parent-name\\:flex:is(:where(.peer\\/parent-name)[data-disabled] ~ *) { display: flex; } .data-disabled\\:flex[data-disabled] { display: flex; + } + + .data-\\[potato\\=salad\\]\\:flex[data-potato="salad"] { + display: flex; }" `) }) @@ -1567,14 +1567,14 @@ test('container queries', () => { --width-lg: 1024px; } - @container name (width < 1024px) { - .\\@max-lg\\/name\\:flex { + @container (width < 1024px) { + .\\@max-lg\\:flex { display: flex; } } - @container (width < 1024px) { - .\\@max-lg\\:flex { + @container name (width < 1024px) { + .\\@max-lg\\/name\\:flex { display: flex; } } @@ -1615,12 +1615,6 @@ test('container queries', () => { } } - @container name (width >= 1024px) { - .\\@lg\\/name\\:flex { - display: flex; - } - } - @container (width >= 1024px) { .\\@lg\\:flex { display: flex; @@ -1628,7 +1622,7 @@ test('container queries', () => { } @container name (width >= 1024px) { - .\\@min-lg\\/name\\:flex { + .\\@lg\\/name\\:flex { display: flex; } } @@ -1637,6 +1631,12 @@ test('container queries', () => { .\\@min-lg\\:flex { display: flex; } + } + + @container name (width >= 1024px) { + .\\@min-lg\\/name\\:flex { + display: flex; + } }" `) }) From 1b3ad0db65db246c7607b54bcb6ea9571d63f646 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 19:54:10 +0100 Subject: [PATCH 05/24] WIP --- packages/tailwindcss/src/ast.ts | 7 +++++++ packages/tailwindcss/src/index.test.ts | 4 +--- packages/tailwindcss/src/index.ts | 23 ++++++++++++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index d316c5411..dae7cf0e8 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -105,6 +105,13 @@ export function toCss(ast: AstNode[]) { return css } + if (node.selector === '@tailwind utilities') { + for (let child of node.nodes) { + css += stringify(child, depth) + } + return css + } + // Print at-rules without nodes with a `;` instead of an empty block. // // E.g.: diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index cf7025102..1488b4817 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -93,9 +93,7 @@ describe('compiling CSS', () => { .grid { display: grid; - } - - @tailwind utilities;" + }" `) }) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 217d71d77..86077ecbb 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -103,11 +103,17 @@ export function compile( let designSystem = buildDesignSystem(theme) + let tailwindUtilitiesNode: Rule | null = null + // Find `@tailwind utilities` and replace it with the actual generated utility // class CSS. - walk(ast, (node, { replaceWith }) => { + walk(ast, (node) => { if (node.kind === 'rule' && node.selector === '@tailwind utilities') { - replaceWith(compileCandidates(rawCandidates, designSystem).astNodes) + tailwindUtilitiesNode = node + + // Set the `@tailwind utilities` nodes, to the actual generated CSS + node.nodes = compileCandidates(rawCandidates, designSystem).astNodes + // Stop walking after finding `@tailwind utilities` to avoid walking all // of the generated CSS. This means `@tailwind utilities` can only appear // once per file but that's the intended usage at this point in time. @@ -168,8 +174,19 @@ export function compile( }) } + // TODO: Don't do this unless rebuilding? + let allCandidates = new Set(rawCandidates) + return { - rebuild(rawCandidates: string[]) { + rebuild(newRawCandidates: string[]) { + for (let candidate of newRawCandidates) { + allCandidates.add(candidate) + } + + if (tailwindUtilitiesNode) { + let newNodes = compileCandidates(Array.from(allCandidates), designSystem).astNodes + tailwindUtilitiesNode.nodes = newNodes + } return toCss(ast) }, css: toCss(ast), From 6282e2b22e89c65e44ee9ce261a54b40780b34bf Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 20:50:19 +0100 Subject: [PATCH 06/24] ensure we don't crash on deleted files --- oxide/crates/core/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oxide/crates/core/src/lib.rs b/oxide/crates/core/src/lib.rs index 87327f39a..6f04adae0 100644 --- a/oxide/crates/core/src/lib.rs +++ b/oxide/crates/core/src/lib.rs @@ -433,9 +433,9 @@ fn read_all_files_sync(changed_content: Vec) -> Vec> { changed_content .into_iter() - .map(|c| match (c.file, c.content) { - (Some(file), None) => std::fs::read(file).unwrap(), - (None, Some(content)) => content.into_bytes(), + .filter_map(|c| match (c.file, c.content) { + (Some(file), None) => std::fs::read(file).ok(), + (None, Some(content)) => Some(content.into_bytes()), _ => Default::default(), }) .collect() From 27e06c76d2ad55c81a1d8accf7acf597ff2113d3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 20:51:18 +0100 Subject: [PATCH 07/24] cache all the things --- packages/tailwindcss/src/compile.ts | 136 +++++++++++++++++----------- packages/tailwindcss/src/index.ts | 31 ++++++- 2 files changed, 111 insertions(+), 56 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 5e5027edb..df0c73759 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -6,45 +6,34 @@ import { DefaultMap } from './utils/default-map' import { escape } from './utils/escape' import type { Variants } from './variants' -export function compileCandidates( - rawCandidates: string[], +export function createParsedVariants( designSystem: DesignSystem, - { throwOnInvalidCandidate = false } = {}, -) { - let nodeSorting = new Map< - AstNode, - { properties: number[]; variants: bigint; candidate: string } - >() - let astNodes: AstNode[] = [] - +): DefaultMap { // A lazy map implementation that will return the variant if it exists. If it // doesn't exist yet, the raw string variant will be parsed and added to the // map. - let parsedVariants: DefaultMap = new DefaultMap((variant, map) => { + return new DefaultMap((variant, map) => { return parseVariant(variant, designSystem.variants, map) }) +} - let candidates = new Map() - - // Parse candidates and variants - for (let rawCandidate of rawCandidates) { - let candidate = parseCandidate(rawCandidate, designSystem.utilities, parsedVariants) - if (candidate === null) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue // Bail, invalid candidate - } - candidates.set(candidate, rawCandidate) - } - - // Sort the variants - let variants = Array.from(parsedVariants.values()).sort((a, z) => { - return designSystem.variants.compare(a, z) +export function createParsedCandidates( + designSystem: DesignSystem, + parsedVariants: DefaultMap, +) { + return new DefaultMap((candidate) => { + return parseCandidate(candidate, designSystem.utilities, parsedVariants) }) +} + +export function createParsedAstNodes( + designSystem: DesignSystem, + parsedCandidates: DefaultMap, +) { + return new DefaultMap((rawCandidate: string) => { + let candidate = parsedCandidates.get(rawCandidate) + if (candidate === null) return null - // Create the AST - next: for (let [candidate, rawCandidate] of candidates) { let nodes: AstNode[] = [] // Handle arbitrary properties @@ -53,12 +42,7 @@ export function compileCandidates( // Build the node let compiledNodes = compileFn(candidate) - if (compiledNodes === undefined) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } + if (compiledNodes === undefined) return null nodes = compiledNodes } @@ -73,12 +57,7 @@ export function compileCandidates( // Build the node let compiledNodes = compileFn(candidate) - if (compiledNodes === undefined) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } + if (compiledNodes === undefined) return null nodes = compiledNodes } @@ -95,23 +74,76 @@ export function compileCandidates( nodes, } - let variantOrder = 0n for (let variant of candidate.variants) { let result = applyVariant(node, variant, designSystem.variants) // When the variant results in `null`, it means that the variant cannot be // applied to the rule. Discard the candidate and continue to the next // one. - if (result === null) { - if (throwOnInvalidCandidate) { - throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) - } - continue next - } + if (result === null) return null + } - // Track the variant order which is a number with each bit representing a - // variant. This allows us to sort the rules based on the order of - // variants used. + return { + node, + propertySort, + } + }) +} + +export function compileCandidates( + rawCandidates: string[], + designSystem: DesignSystem, + { + parsedVariants = createParsedVariants(designSystem), + parsedCandidates = createParsedCandidates(designSystem, parsedVariants), + parsedAstNodes = createParsedAstNodes(designSystem, parsedCandidates), + invalidRawCandidates = new Set(), + throwOnInvalidCandidate = false, + } = {}, +) { + let nodeSorting = new Map< + AstNode, + { properties: number[]; variants: bigint; candidate: string } + >() + let astNodes: AstNode[] = [] + let candidates = new Map() + + // Parse candidates and variants + for (let rawCandidate of rawCandidates) { + let candidate = parsedCandidates.get(rawCandidate) + if (candidate === null) { + if (throwOnInvalidCandidate) { + throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) + } + invalidRawCandidates.add(rawCandidate) + continue // Bail, invalid candidate + } + candidates.set(candidate, rawCandidate) + } + + // Sort the variants + let variants = Array.from(parsedVariants.values()).sort((a, z) => { + return designSystem.variants.compare(a, z) + }) + + // Create the AST + next: for (let [candidate, rawCandidate] of candidates) { + let astNode = parsedAstNodes.get(rawCandidate) + if (astNode === null) { + if (throwOnInvalidCandidate) { + throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) + } + invalidRawCandidates.add(rawCandidate) + continue next + } + + let { node, propertySort } = astNode + + // Track the variant order which is a number with each bit representing a + // variant. This allows us to sort the rules based on the order of + // variants used. + let variantOrder = 0n + for (let variant of candidate.variants) { variantOrder |= 1n << BigInt(variants.indexOf(variant)) } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 86077ecbb..de124dea3 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -1,7 +1,12 @@ import { Features, transform } from 'lightningcss' import { version } from '../package.json' import { WalkAction, comment, decl, rule, toCss, walk, type AstNode, type Rule } from './ast' -import { compileCandidates } from './compile' +import { + compileCandidates, + createParsedAstNodes, + createParsedCandidates, + createParsedVariants, +} from './compile' import * as CSS from './css-parser' import { buildDesignSystem } from './design-system' import { Theme } from './theme' @@ -102,6 +107,9 @@ export function compile( } let designSystem = buildDesignSystem(theme) + let parsedVariants = createParsedVariants(designSystem) + let parsedCandidates = createParsedCandidates(designSystem, parsedVariants) + let parsedAstNodes = createParsedAstNodes(designSystem, parsedCandidates) let tailwindUtilitiesNode: Rule | null = null @@ -112,7 +120,11 @@ export function compile( tailwindUtilitiesNode = node // Set the `@tailwind utilities` nodes, to the actual generated CSS - node.nodes = compileCandidates(rawCandidates, designSystem).astNodes + node.nodes = compileCandidates(rawCandidates, designSystem, { + parsedVariants, + parsedCandidates, + parsedAstNodes, + }).astNodes // Stop walking after finding `@tailwind utilities` to avoid walking all // of the generated CSS. This means `@tailwind utilities` can only appear @@ -134,6 +146,9 @@ export function compile( { // Parse the candidates to an AST that we can replace the `@apply` rule with. let candidateAst = compileCandidates(candidates, designSystem, { + parsedVariants, + parsedCandidates, + parsedAstNodes, throwOnInvalidCandidate: true, }).astNodes @@ -176,15 +191,23 @@ export function compile( // TODO: Don't do this unless rebuilding? let allCandidates = new Set(rawCandidates) + let invalidRawCandidates = new Set() return { rebuild(newRawCandidates: string[]) { for (let candidate of newRawCandidates) { - allCandidates.add(candidate) + if (!invalidRawCandidates.has(candidate)) { + allCandidates.add(candidate) + } } if (tailwindUtilitiesNode) { - let newNodes = compileCandidates(Array.from(allCandidates), designSystem).astNodes + let newNodes = compileCandidates(Array.from(allCandidates), designSystem, { + parsedVariants, + parsedCandidates, + parsedAstNodes, + invalidRawCandidates, + }).astNodes tailwindUtilitiesNode.nodes = newNodes } return toCss(ast) From 62ee42c30c6169dd5850a3794ccb5e0ae6112871 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 22:12:13 +0100 Subject: [PATCH 08/24] ensure input file is an import dependency --- packages/@tailwindcss-cli/src/commands/build/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index ebfa0fde4..1ea813667 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -237,7 +237,9 @@ function handleImports( // Relevant specification: // - CSS Import Resolve: https://csstools.github.io/css-import-resolve/ - if (!input.includes('@import')) return [input, []] + if (!input.includes('@import')) { + return [input, [file]] + } return postcss() .use(atImport()) @@ -247,6 +249,8 @@ function handleImports( // Use `result.messages` to get the imported files. This also includes the // current file itself. - result.messages.filter((msg) => msg.type === 'postcss-import').map((msg) => msg.file), + [file].concat( + result.messages.filter((msg) => msg.type === 'dependency').map((msg) => msg.file), + ), ]) } From c65598e12c3cc2747e0cbff74205d58a8a8182da Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 22:14:08 +0100 Subject: [PATCH 09/24] cache compiledCss --- packages/tailwindcss/src/index.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index de124dea3..c50a17334 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -193,26 +193,43 @@ export function compile( let allCandidates = new Set(rawCandidates) let invalidRawCandidates = new Set() + let compiledCss = toCss(ast) + return { rebuild(newRawCandidates: string[]) { + // Detect all new candidates + let previousSize = allCandidates.size for (let candidate of newRawCandidates) { if (!invalidRawCandidates.has(candidate)) { allCandidates.add(candidate) } } + // If no new candidates were added, we can return the original CSS + if (previousSize === allCandidates.size) { + return compiledCss + } + if (tailwindUtilitiesNode) { + let previousAstNodeCount = parsedAstNodes.size let newNodes = compileCandidates(Array.from(allCandidates), designSystem, { parsedVariants, parsedCandidates, parsedAstNodes, invalidRawCandidates, }).astNodes + + if (previousAstNodeCount === parsedAstNodes.size) { + return compiledCss + } + tailwindUtilitiesNode.nodes = newNodes + compiledCss = toCss(ast) } - return toCss(ast) + + return compiledCss }, - css: toCss(ast), + css: compiledCss, } } From 48b56017cdb1c079a663eca557204d0180d45117 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 23:24:11 +0100 Subject: [PATCH 10/24] hoist `invalidRawCandidates` This way the cache can start filling up before a rebuild happens --- packages/tailwindcss/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index c50a17334..d99f74eb7 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -107,6 +107,7 @@ export function compile( } let designSystem = buildDesignSystem(theme) + let invalidRawCandidates = new Set() let parsedVariants = createParsedVariants(designSystem) let parsedCandidates = createParsedCandidates(designSystem, parsedVariants) let parsedAstNodes = createParsedAstNodes(designSystem, parsedCandidates) @@ -124,6 +125,7 @@ export function compile( parsedVariants, parsedCandidates, parsedAstNodes, + invalidRawCandidates, }).astNodes // Stop walking after finding `@tailwind utilities` to avoid walking all @@ -149,6 +151,7 @@ export function compile( parsedVariants, parsedCandidates, parsedAstNodes, + invalidRawCandidates, throwOnInvalidCandidate: true, }).astNodes @@ -191,8 +194,6 @@ export function compile( // TODO: Don't do this unless rebuilding? let allCandidates = new Set(rawCandidates) - let invalidRawCandidates = new Set() - let compiledCss = toCss(ast) return { From 02b1794684cbecc501f4a1cad857a9d36ca4dfc4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 23:25:32 +0100 Subject: [PATCH 11/24] drop todo This only takes ~0.4ms in the tailwindcss.com repo --- packages/tailwindcss/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d99f74eb7..67294d724 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -192,7 +192,6 @@ export function compile( }) } - // TODO: Don't do this unless rebuilding? let allCandidates = new Set(rawCandidates) let compiledCss = toCss(ast) From 50da0a1be20d49670bc45038819e9a3befc32615 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 23:27:14 +0100 Subject: [PATCH 12/24] allow any iterable string as input This way we can pass it an array or a set without first converting it. --- packages/tailwindcss/src/compile.ts | 2 +- packages/tailwindcss/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index df0c73759..2cc2d97f8 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -91,7 +91,7 @@ export function createParsedAstNodes( } export function compileCandidates( - rawCandidates: string[], + rawCandidates: Iterable, designSystem: DesignSystem, { parsedVariants = createParsedVariants(designSystem), diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 67294d724..78579a5dc 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -212,7 +212,7 @@ export function compile( if (tailwindUtilitiesNode) { let previousAstNodeCount = parsedAstNodes.size - let newNodes = compileCandidates(Array.from(allCandidates), designSystem, { + let newNodes = compileCandidates(allCandidates, designSystem, { parsedVariants, parsedCandidates, parsedAstNodes, From a19bc73e70b246a3c29b74a49716875a5251141d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 23:40:36 +0100 Subject: [PATCH 13/24] build `allValidCandidates` from all rawCandidates that are not invalid --- packages/tailwindcss/src/index.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 78579a5dc..40226ca73 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -192,33 +192,42 @@ export function compile( }) } - let allCandidates = new Set(rawCandidates) + let allValidCandidates = new Set() + for (let rawCandidate of rawCandidates) { + if (!invalidRawCandidates.has(rawCandidate)) { + allValidCandidates.add(rawCandidate) + } + } let compiledCss = toCss(ast) return { rebuild(newRawCandidates: string[]) { - // Detect all new candidates - let previousSize = allCandidates.size + // Add all new candidates unless we know that they are invalid. + let previousSize = allValidCandidates.size for (let candidate of newRawCandidates) { if (!invalidRawCandidates.has(candidate)) { - allCandidates.add(candidate) + allValidCandidates.add(candidate) } } - // If no new candidates were added, we can return the original CSS - if (previousSize === allCandidates.size) { + // 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 (previousSize === allValidCandidates.size) { return compiledCss } if (tailwindUtilitiesNode) { let previousAstNodeCount = parsedAstNodes.size - let newNodes = compileCandidates(allCandidates, designSystem, { + let newNodes = compileCandidates(allValidCandidates, designSystem, { parsedVariants, parsedCandidates, parsedAstNodes, invalidRawCandidates, }).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 === parsedAstNodes.size) { return compiledCss } From d04a17c1026a28fcd5fa5a433e6b766d027d66ca Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Mar 2024 23:59:19 +0100 Subject: [PATCH 14/24] drop unnecessary comment --- packages/tailwindcss/src/compile.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 2cc2d97f8..0f6ecec78 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -9,9 +9,6 @@ import type { Variants } from './variants' export function createParsedVariants( designSystem: DesignSystem, ): DefaultMap { - // A lazy map implementation that will return the variant if it exists. If it - // doesn't exist yet, the raw string variant will be parsed and added to the - // map. return new DefaultMap((variant, map) => { return parseVariant(variant, designSystem.variants, map) }) From 115417fed445255908655158d8d9c6c9bea94346 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 00:25:06 +0100 Subject: [PATCH 15/24] move cached parsers to designSystem --- packages/tailwindcss/src/candidate.bench.ts | 9 +- packages/tailwindcss/src/candidate.test.ts | 13 +- packages/tailwindcss/src/candidate.ts | 42 ++--- packages/tailwindcss/src/compile.ts | 161 ++++++++------------ packages/tailwindcss/src/design-system.ts | 44 +++++- packages/tailwindcss/src/index.ts | 37 +---- packages/tailwindcss/src/intellisense.ts | 7 +- 7 files changed, 137 insertions(+), 176 deletions(-) diff --git a/packages/tailwindcss/src/candidate.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index f10e95d09..f46dec7ec 100644 --- a/packages/tailwindcss/src/candidate.bench.ts +++ b/packages/tailwindcss/src/candidate.bench.ts @@ -1,9 +1,8 @@ import { scanDir } from '@tailwindcss/oxide' import { bench } from 'vitest' -import { parseCandidate, parseVariant } from './candidate' +import { parseCandidate } from './candidate' import { buildDesignSystem } from './design-system' import { Theme } from './theme' -import { DefaultMap } from './utils/default-map' // FOLDER=path/to/folder vitest bench const root = process.env.FOLDER || process.cwd() @@ -15,10 +14,6 @@ const designSystem = buildDesignSystem(new Theme()) bench('parseCandidate', () => { for (let candidate of result.candidates) { - parseCandidate( - candidate, - designSystem.utilities, - new DefaultMap((variant, map) => parseVariant(variant, designSystem.variants, map)), - ) + parseCandidate(candidate, designSystem) } }) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 672e8cbb4..17d1371b6 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -1,7 +1,7 @@ import { expect, it } from 'vitest' -import { parseCandidate, parseVariant } from './candidate' +import { buildDesignSystem } from './design-system' +import { Theme } from './theme' import { Utilities } from './utilities' -import { DefaultMap } from './utils/default-map' import { Variants } from './variants' function run( @@ -11,11 +11,12 @@ function run( utilities ??= new Utilities() variants ??= new Variants() - let parsedVariants = new DefaultMap((variant, map) => { - return parseVariant(variant, variants!, map) - }) + let designSystem = buildDesignSystem(new Theme()) - return parseCandidate(candidate, utilities, parsedVariants) + designSystem.utilities = utilities + designSystem.variants = variants + + return designSystem.parseCandidate(candidate) } it('should skip unknown utilities', () => { diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index e3f6b92f2..03420e4d8 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -1,3 +1,4 @@ +import type { DesignSystem } from './design-system' import { decodeArbitraryValue } from './utils/decode-arbitrary-value' import { segment } from './utils/segment' @@ -206,14 +207,7 @@ export type Candidate = important: boolean } -export function parseCandidate( - input: string, - utilities: { - has: (value: string) => boolean - kind: (root: string) => Omit - }, - parsedVariants: { get: (value: string) => Variant | null }, -): Candidate | null { +export function parseCandidate(input: string, designSystem: DesignSystem): Candidate | null { // hover:focus:underline // ^^^^^ ^^^^^^ -> Variants // ^^^^^^^^^ -> Base @@ -228,7 +222,7 @@ export function parseCandidate( let parsedCandidateVariants: Variant[] = [] for (let variant of rawVariants) { - let parsedVariant = parsedVariants.get(variant) + let parsedVariant = designSystem.parseVariant(variant) if (parsedVariant === null) return null // Variants are applied left-to-right meaning that any representing pseudo- @@ -320,7 +314,7 @@ export function parseCandidate( base = base.slice(1) } - let [root, value] = findRoot(base, utilities) + let [root, value] = findRoot(base, designSystem.utilities) let modifierSegment: string | null = null @@ -335,13 +329,13 @@ export function parseCandidate( modifierSegment = rootModifierSegment // Try to find the root and value, without the modifier present - ;[root, value] = findRoot(rootWithoutModifier, utilities) + ;[root, value] = findRoot(rootWithoutModifier, designSystem.utilities) } // If there's no root, the candidate isn't a valid class and can be discarded. if (root === null) return null - let kind = utilities.kind(root) + let kind = designSystem.utilities.kind(root) if (kind === 'static') { if (value !== null) return null @@ -475,15 +469,7 @@ function parseModifier(modifier: string): CandidateModifier { } } -export function parseVariant( - variant: string, - variants: { - has: (value: string) => boolean - kind: (root: string) => Omit - compounds: (root: string) => boolean - }, - parsedVariants: { get: (value: string) => Variant | null }, -): Variant | null { +export function parseVariant(variant: string, designSystem: DesignSystem): Variant | null { // Arbitrary variants if (variant[0] === '[' && variant[variant.length - 1] === ']') { /** @@ -535,20 +521,20 @@ export function parseVariant( // - `group-hover/foo/bar` if (additionalModifier) return null - let [root, value] = findRoot(variantWithoutModifier, variants) + let [root, value] = findRoot(variantWithoutModifier, designSystem.variants) // Variant is invalid, therefore the candidate is invalid and we can skip // continue parsing it. if (root === null) return null - switch (variants.kind(root)) { + switch (designSystem.variants.kind(root)) { case 'static': { if (value !== null) return null return { kind: 'static', root, - compounds: variants.compounds(root), + compounds: designSystem.variants.compounds(root), } } @@ -564,7 +550,7 @@ export function parseVariant( kind: 'arbitrary', value: decodeArbitraryValue(value.slice(1, -1)), }, - compounds: variants.compounds(root), + compounds: designSystem.variants.compounds(root), } } @@ -573,14 +559,14 @@ export function parseVariant( root, modifier: modifier === null ? null : parseModifier(modifier), value: { kind: 'named', value }, - compounds: variants.compounds(root), + compounds: designSystem.variants.compounds(root), } } case 'compound': { if (value === null) return null - let subVariant = parsedVariants.get(value) + let subVariant = designSystem.parseVariant(value) if (subVariant === null) return null if (subVariant.compounds === false) return null @@ -589,7 +575,7 @@ export function parseVariant( root, modifier: modifier === null ? null : { kind: 'named', value: modifier }, variant: subVariant, - compounds: variants.compounds(root), + compounds: designSystem.variants.compounds(root), } } } diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 0f6ecec78..2d972566d 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -1,102 +1,14 @@ import { rule, type AstNode, type Rule } from './ast' -import { parseCandidate, parseVariant, type Candidate, type Variant } from './candidate' +import { type Candidate, type Variant } from './candidate' import { type DesignSystem } from './design-system' import GLOBAL_PROPERTY_ORDER from './property-order' -import { DefaultMap } from './utils/default-map' import { escape } from './utils/escape' import type { Variants } from './variants' -export function createParsedVariants( - designSystem: DesignSystem, -): DefaultMap { - return new DefaultMap((variant, map) => { - return parseVariant(variant, designSystem.variants, map) - }) -} - -export function createParsedCandidates( - designSystem: DesignSystem, - parsedVariants: DefaultMap, -) { - return new DefaultMap((candidate) => { - return parseCandidate(candidate, designSystem.utilities, parsedVariants) - }) -} - -export function createParsedAstNodes( - designSystem: DesignSystem, - parsedCandidates: DefaultMap, -) { - return new DefaultMap((rawCandidate: string) => { - let candidate = parsedCandidates.get(rawCandidate) - if (candidate === null) return null - - let nodes: AstNode[] = [] - - // Handle arbitrary properties - if (candidate.kind === 'arbitrary') { - let compileFn = designSystem.utilities.getArbitrary() - - // Build the node - let compiledNodes = compileFn(candidate) - if (compiledNodes === undefined) return null - - nodes = compiledNodes - } - - // Handle named utilities - else if (candidate.kind === 'static' || candidate.kind === 'functional') { - // Safety: At this point it is safe to use TypeScript's non-null assertion - // operator because if the `candidate.root` didn't exist, `parseCandidate` - // would have returned `null` and we would have returned early resulting - // in not hitting this code path. - let { compileFn } = designSystem.utilities.get(candidate.root)! - - // Build the node - let compiledNodes = compileFn(candidate) - if (compiledNodes === undefined) return null - - nodes = compiledNodes - } - - let propertySort = getPropertySort(nodes) - - if (candidate.important) { - applyImportant(nodes) - } - - let node: Rule = { - kind: 'rule', - selector: `.${escape(rawCandidate)}`, - nodes, - } - - for (let variant of candidate.variants) { - let result = applyVariant(node, variant, designSystem.variants) - - // When the variant results in `null`, it means that the variant cannot be - // applied to the rule. Discard the candidate and continue to the next - // one. - if (result === null) return null - } - - return { - node, - propertySort, - } - }) -} - export function compileCandidates( rawCandidates: Iterable, designSystem: DesignSystem, - { - parsedVariants = createParsedVariants(designSystem), - parsedCandidates = createParsedCandidates(designSystem, parsedVariants), - parsedAstNodes = createParsedAstNodes(designSystem, parsedCandidates), - invalidRawCandidates = new Set(), - throwOnInvalidCandidate = false, - } = {}, + { throwOnInvalidCandidate = false } = {}, ) { let nodeSorting = new Map< AstNode, @@ -107,30 +19,30 @@ export function compileCandidates( // Parse candidates and variants for (let rawCandidate of rawCandidates) { - let candidate = parsedCandidates.get(rawCandidate) + let candidate = designSystem.parseCandidate(rawCandidate) if (candidate === null) { if (throwOnInvalidCandidate) { throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) } - invalidRawCandidates.add(rawCandidate) + designSystem.invalidRawCandidates.add(rawCandidate) continue // Bail, invalid candidate } candidates.set(candidate, rawCandidate) } // Sort the variants - let variants = Array.from(parsedVariants.values()).sort((a, z) => { + let variants = Array.from(designSystem.parsedVariants.values()).sort((a, z) => { return designSystem.variants.compare(a, z) }) // Create the AST next: for (let [candidate, rawCandidate] of candidates) { - let astNode = parsedAstNodes.get(rawCandidate) + let astNode = designSystem.parseAstNode(rawCandidate) if (astNode === null) { if (throwOnInvalidCandidate) { throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) } - invalidRawCandidates.add(rawCandidate) + designSystem.invalidRawCandidates.add(rawCandidate) continue next } @@ -191,6 +103,65 @@ export function compileCandidates( } } +export function parseAstNodes(rawCandidate: string, designSystem: DesignSystem) { + let candidate = designSystem.parseCandidate(rawCandidate) + if (candidate === null) return null + + let nodes: AstNode[] = [] + + // Handle arbitrary properties + if (candidate.kind === 'arbitrary') { + let compileFn = designSystem.utilities.getArbitrary() + + // Build the node + let compiledNodes = compileFn(candidate) + if (compiledNodes === undefined) return null + + nodes = compiledNodes + } + + // Handle named utilities + else if (candidate.kind === 'static' || candidate.kind === 'functional') { + // Safety: At this point it is safe to use TypeScript's non-null assertion + // operator because if the `candidate.root` didn't exist, `parseCandidate` + // would have returned `null` and we would have returned early resulting + // in not hitting this code path. + let { compileFn } = designSystem.utilities.get(candidate.root)! + + // Build the node + let compiledNodes = compileFn(candidate) + if (compiledNodes === undefined) return null + + nodes = compiledNodes + } + + let propertySort = getPropertySort(nodes) + + if (candidate.important) { + applyImportant(nodes) + } + + let node: Rule = { + kind: 'rule', + selector: `.${escape(rawCandidate)}`, + nodes, + } + + for (let variant of candidate.variants) { + let result = applyVariant(node, variant, designSystem.variants) + + // When the variant results in `null`, it means that the variant cannot be + // applied to the rule. Discard the candidate and continue to the next + // one. + if (result === null) return null + } + + return { + node, + propertySort, + } +} + export function applyVariant(node: Rule, variant: Variant, variants: Variants): null | void { if (variant.kind === 'arbitrary') { node.nodes = [rule(variant.selector, node.nodes)] diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 190c5e7e2..67595322f 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -1,9 +1,11 @@ import { toCss } from './ast' -import { compileCandidates } from './compile' +import { parseCandidate, parseVariant } from './candidate' +import { compileCandidates, parseAstNodes as parseAstNode } from './compile' import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense' import { getClassOrder } from './sort' import type { Theme } from './theme' import { Utilities, createUtilities } from './utilities' +import { DefaultMap } from './utils/default-map' import { Variants, createVariants } from './variants' export type DesignSystem = { @@ -15,13 +17,35 @@ export type DesignSystem = { getClassOrder(classes: string[]): [string, bigint | null][] getClassList(): ClassEntry[] getVariants(): VariantEntry[] + + parseCandidate(candidate: string): ReturnType + parseVariant(variant: string): ReturnType + parseAstNode(candidate: string): ReturnType + + parsedCandidates: DefaultMap> + parsedVariants: DefaultMap> + parsedAstNodes: DefaultMap> + + invalidRawCandidates: Set } export function buildDesignSystem(theme: Theme): DesignSystem { - return { + let utilities = createUtilities(theme) + let variants = createVariants(theme) + let invalidRawCandidates = new Set() + + let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem)) + let parsedCandidates = new DefaultMap((candidate) => parseCandidate(candidate, designSystem)) + let parsedAstNodes = new DefaultMap((candidate) => parseAstNode(candidate, designSystem)) + + let designSystem: DesignSystem = { theme, - utilities: createUtilities(theme), - variants: createVariants(theme), + utilities, + variants, + parsedCandidates, + parsedVariants, + parsedAstNodes, + invalidRawCandidates, candidatesToCss(classes: string[]) { let result: (string | null)[] = [] @@ -47,5 +71,17 @@ export function buildDesignSystem(theme: Theme): DesignSystem { getVariants() { return getVariants(this) }, + + parseCandidate(candidate: string) { + return parsedCandidates.get(candidate) + }, + parseVariant(variant: string) { + return parsedVariants.get(variant) + }, + parseAstNode(candidate: string) { + return parsedAstNodes.get(candidate) + }, } + + return designSystem } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 40226ca73..f09612307 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -1,12 +1,7 @@ import { Features, transform } from 'lightningcss' import { version } from '../package.json' import { WalkAction, comment, decl, rule, toCss, walk, type AstNode, type Rule } from './ast' -import { - compileCandidates, - createParsedAstNodes, - createParsedCandidates, - createParsedVariants, -} from './compile' +import { compileCandidates } from './compile' import * as CSS from './css-parser' import { buildDesignSystem } from './design-system' import { Theme } from './theme' @@ -107,10 +102,6 @@ export function compile( } let designSystem = buildDesignSystem(theme) - let invalidRawCandidates = new Set() - let parsedVariants = createParsedVariants(designSystem) - let parsedCandidates = createParsedCandidates(designSystem, parsedVariants) - let parsedAstNodes = createParsedAstNodes(designSystem, parsedCandidates) let tailwindUtilitiesNode: Rule | null = null @@ -121,12 +112,7 @@ export function compile( tailwindUtilitiesNode = node // Set the `@tailwind utilities` nodes, to the actual generated CSS - node.nodes = compileCandidates(rawCandidates, designSystem, { - parsedVariants, - parsedCandidates, - parsedAstNodes, - invalidRawCandidates, - }).astNodes + node.nodes = compileCandidates(rawCandidates, designSystem).astNodes // Stop walking after finding `@tailwind utilities` to avoid walking all // of the generated CSS. This means `@tailwind utilities` can only appear @@ -148,10 +134,6 @@ export function compile( { // Parse the candidates to an AST that we can replace the `@apply` rule with. let candidateAst = compileCandidates(candidates, designSystem, { - parsedVariants, - parsedCandidates, - parsedAstNodes, - invalidRawCandidates, throwOnInvalidCandidate: true, }).astNodes @@ -194,7 +176,7 @@ export function compile( let allValidCandidates = new Set() for (let rawCandidate of rawCandidates) { - if (!invalidRawCandidates.has(rawCandidate)) { + if (!designSystem.invalidRawCandidates.has(rawCandidate)) { allValidCandidates.add(rawCandidate) } } @@ -205,7 +187,7 @@ export function compile( // Add all new candidates unless we know that they are invalid. let previousSize = allValidCandidates.size for (let candidate of newRawCandidates) { - if (!invalidRawCandidates.has(candidate)) { + if (!designSystem.invalidRawCandidates.has(candidate)) { allValidCandidates.add(candidate) } } @@ -217,18 +199,13 @@ export function compile( } if (tailwindUtilitiesNode) { - let previousAstNodeCount = parsedAstNodes.size - let newNodes = compileCandidates(allValidCandidates, designSystem, { - parsedVariants, - parsedCandidates, - parsedAstNodes, - invalidRawCandidates, - }).astNodes + let previousAstNodeCount = designSystem.parsedAstNodes.size + let newNodes = compileCandidates(allValidCandidates, designSystem).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 === parsedAstNodes.size) { + if (previousAstNodeCount === designSystem.parsedAstNodes.size) { return compiledCss } diff --git a/packages/tailwindcss/src/intellisense.ts b/packages/tailwindcss/src/intellisense.ts index 726e3c57b..4b1d814fb 100644 --- a/packages/tailwindcss/src/intellisense.ts +++ b/packages/tailwindcss/src/intellisense.ts @@ -1,8 +1,6 @@ import { decl, rule } from './ast' -import { parseVariant, type Variant } from './candidate' import { applyVariant } from './compile' import type { DesignSystem } from './design-system' -import { DefaultMap } from './utils/default-map' interface ClassMetadata { modifiers: string[] @@ -60,9 +58,6 @@ export interface VariantEntry { export function getVariants(design: DesignSystem) { let list: VariantEntry[] = [] - let parsedVariants = new DefaultMap((variant, map) => - parseVariant(variant, design.variants, map), - ) for (let [root, variant] of design.variants.entries()) { if (variant.kind === 'arbitrary') continue @@ -74,7 +69,7 @@ export function getVariants(design: DesignSystem) { if (value) name += `-${value}` if (modifier) name += `/${modifier}` - let variant = parsedVariants.get(name) + let variant = design.parseVariant(name) if (!variant) return [] From a0f23d3306374a6c267cbda8d95edbe4b4eaa571 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 00:36:21 +0100 Subject: [PATCH 16/24] do not clear the Rust cache The candidates from the content files are still going to be correct as long as the file didn't change. --- .../@tailwindcss-cli/src/commands/build/index.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 1ea813667..be8a03fcb 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -1,12 +1,5 @@ import watcher from '@parcel/watcher' -import { - IO, - Parsing, - clearCache, - scanDir, - scanFiles, - type ChangedContent, -} from '@tailwindcss/oxide' +import { IO, Parsing, scanDir, scanFiles, type ChangedContent } from '@tailwindcss/oxide' import { existsSync } from 'node:fs' import fs from 'node:fs/promises' import path from 'node:path' @@ -164,9 +157,6 @@ export async function handle(args: Result>) { // Scan the entire `base` directory for full rebuilds. if (rebuildStrategy === 'full') { - // Clear the cache because we need to re-scan the entire directory. - clearCache() - // Re-scan the directory to get the new `candidates`. candidates = scanDir({ base }).candidates From ce38fcde3e61923b6c65c8a1928ade448f786be0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 00:50:31 +0100 Subject: [PATCH 17/24] only optimize using Lightning CSS if the compiled css actually changed --- .../src/commands/build/index.ts | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index be8a03fcb..ca3859b6e 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -92,24 +92,41 @@ export async function handle(args: Result>) { args['--input'] ?? base, ) + let previous = { + css: '', + optimizedCss: '', + } + + async function write(css: string, args: Result>) { + let output = css + + // Optimize the output + if (args['--minify'] || args['--optimize']) { + if (css !== previous.css) { + let optimizedCss = optimizeCss(css, { + file: args['--input'] ?? 'input.css', + minify: args['--minify'] ?? false, + }) + previous.css = css + previous.optimizedCss = optimizedCss + output = optimizedCss + } else { + output = previous.optimizedCss + } + } + + // Write the output + if (args['--output']) { + await outputFile(args['--output'], output) + } else { + println(output) + } + } + // Compile the input let result = compile(input, candidates) - let compiledCss = result.css - // Optimize the output - if (args['--minify'] || args['--optimize']) { - compiledCss = optimizeCss(compiledCss, { - file: args['--input'] ?? 'input.css', - minify: args['--minify'] ?? false, - }) - } - - // Write the output - if (args['--output']) { - await outputFile(args['--output'], compiledCss) - } else { - println(compiledCss) - } + await write(result.css, args) let end = process.hrtime.bigint() eprintln(header()) @@ -155,6 +172,9 @@ export async function handle(args: Result>) { // Re-compile the input let start = process.hrtime.bigint() + // Track the compiled CSS + let compiledCss = '' + // Scan the entire `base` directory for full rebuilds. if (rebuildStrategy === 'full') { // Re-scan the directory to get the new `candidates`. @@ -179,20 +199,7 @@ export async function handle(args: Result>) { compiledCss = result.rebuild(scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)) } - // Optimize the output - if (args['--minify'] || args['--optimize']) { - compiledCss = optimizeCss(compiledCss, { - file: args['--input'] ?? 'input.css', - minify: args['--minify'] ?? false, - }) - } - - // Write the output - if (args['--output']) { - await outputFile(args['--output'], compiledCss) - } else { - println(compiledCss) - } + await write(compiledCss, args) let end = process.hrtime.bigint() eprintln(`Done in ${formatDuration(end - start)}`) From e05e5ece43a81f9e49f68ae2a059260c402a17f7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 11:42:08 +0100 Subject: [PATCH 18/24] expose methods instead of raw `DefaultMap`s --- packages/tailwindcss/src/compile.ts | 2 +- packages/tailwindcss/src/design-system.ts | 14 ++++++++------ packages/tailwindcss/src/index.ts | 7 +++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 2d972566d..19629af42 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -31,7 +31,7 @@ export function compileCandidates( } // Sort the variants - let variants = Array.from(designSystem.parsedVariants.values()).sort((a, z) => { + let variants = designSystem.getUsedVariants().sort((a, z) => { return designSystem.variants.compare(a, z) }) diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 67595322f..55d29e51c 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -22,9 +22,8 @@ export type DesignSystem = { parseVariant(variant: string): ReturnType parseAstNode(candidate: string): ReturnType - parsedCandidates: DefaultMap> - parsedVariants: DefaultMap> - parsedAstNodes: DefaultMap> + getUsedVariants(): ReturnType[] + getAstNodeSize(): number invalidRawCandidates: Set } @@ -42,9 +41,6 @@ export function buildDesignSystem(theme: Theme): DesignSystem { theme, utilities, variants, - parsedCandidates, - parsedVariants, - parsedAstNodes, invalidRawCandidates, candidatesToCss(classes: string[]) { @@ -81,6 +77,12 @@ export function buildDesignSystem(theme: Theme): DesignSystem { parseAstNode(candidate: string) { return parsedAstNodes.get(candidate) }, + getUsedVariants() { + return Array.from(parsedVariants.values()) + }, + getAstNodeSize() { + return parsedAstNodes.size + }, } return designSystem diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index f09612307..193d97453 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -174,6 +174,9 @@ export function compile( }) } + // Track all valid candidates, these are the incoming `rawCandidate` that + // resulted in a generated AST Node. All the other `rawCandidates` are invalid + // and should be ignored. let allValidCandidates = new Set() for (let rawCandidate of rawCandidates) { if (!designSystem.invalidRawCandidates.has(rawCandidate)) { @@ -199,13 +202,13 @@ export function compile( } if (tailwindUtilitiesNode) { - let previousAstNodeCount = designSystem.parsedAstNodes.size + let previousAstNodeCount = designSystem.getAstNodeSize() let newNodes = compileCandidates(allValidCandidates, designSystem).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 === designSystem.parsedAstNodes.size) { + if (previousAstNodeCount === designSystem.getAstNodeSize()) { return compiledCss } From d7ec348412ce416203d0212439447aef374e9fc5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 12:07:12 +0100 Subject: [PATCH 19/24] rename `parseAstNode` to `compileAstNodes` --- packages/tailwindcss/src/compile.ts | 4 ++-- packages/tailwindcss/src/design-system.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 19629af42..deca45e85 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -37,7 +37,7 @@ export function compileCandidates( // Create the AST next: for (let [candidate, rawCandidate] of candidates) { - let astNode = designSystem.parseAstNode(rawCandidate) + let astNode = designSystem.compileAstNodes(rawCandidate) if (astNode === null) { if (throwOnInvalidCandidate) { throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) @@ -103,7 +103,7 @@ export function compileCandidates( } } -export function parseAstNodes(rawCandidate: string, designSystem: DesignSystem) { +export function compileAstNodes(rawCandidate: string, designSystem: DesignSystem) { let candidate = designSystem.parseCandidate(rawCandidate) if (candidate === null) return null diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 55d29e51c..dd844a4f5 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -1,6 +1,6 @@ import { toCss } from './ast' import { parseCandidate, parseVariant } from './candidate' -import { compileCandidates, parseAstNodes as parseAstNode } from './compile' +import { compileAstNodes, compileCandidates } from './compile' import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense' import { getClassOrder } from './sort' import type { Theme } from './theme' @@ -20,7 +20,7 @@ export type DesignSystem = { parseCandidate(candidate: string): ReturnType parseVariant(variant: string): ReturnType - parseAstNode(candidate: string): ReturnType + compileAstNodes(candidate: string): ReturnType getUsedVariants(): ReturnType[] getAstNodeSize(): number @@ -35,7 +35,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem { let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem)) let parsedCandidates = new DefaultMap((candidate) => parseCandidate(candidate, designSystem)) - let parsedAstNodes = new DefaultMap((candidate) => parseAstNode(candidate, designSystem)) + let compiledAstNodes = new DefaultMap((candidate) => compileAstNodes(candidate, designSystem)) let designSystem: DesignSystem = { theme, @@ -74,14 +74,14 @@ export function buildDesignSystem(theme: Theme): DesignSystem { parseVariant(variant: string) { return parsedVariants.get(variant) }, - parseAstNode(candidate: string) { - return parsedAstNodes.get(candidate) + compileAstNodes(candidate: string) { + return compiledAstNodes.get(candidate) }, getUsedVariants() { return Array.from(parsedVariants.values()) }, getAstNodeSize() { - return parsedAstNodes.size + return compiledAstNodes.size }, } From 145df6ca53c3459c960469abd8898f4e4119e100 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 12:13:23 +0100 Subject: [PATCH 20/24] encapsulate `invalidRawCandidates` --- packages/tailwindcss/src/compile.ts | 4 ++-- packages/tailwindcss/src/design-system.ts | 11 +++++++++-- packages/tailwindcss/src/index.ts | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index deca45e85..d1dc7d0ee 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -24,7 +24,7 @@ export function compileCandidates( if (throwOnInvalidCandidate) { throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) } - designSystem.invalidRawCandidates.add(rawCandidate) + designSystem.markInvalidCandidate(rawCandidate) continue // Bail, invalid candidate } candidates.set(candidate, rawCandidate) @@ -42,7 +42,7 @@ export function compileCandidates( if (throwOnInvalidCandidate) { throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`) } - designSystem.invalidRawCandidates.add(rawCandidate) + designSystem.markInvalidCandidate(rawCandidate) continue next } diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index dd844a4f5..fa6390f0a 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -25,7 +25,8 @@ export type DesignSystem = { getUsedVariants(): ReturnType[] getAstNodeSize(): number - invalidRawCandidates: Set + markInvalidCandidate(candidate: string): void + isInvalidCandidate(candidate: string): boolean } export function buildDesignSystem(theme: Theme): DesignSystem { @@ -41,7 +42,6 @@ export function buildDesignSystem(theme: Theme): DesignSystem { theme, utilities, variants, - invalidRawCandidates, candidatesToCss(classes: string[]) { let result: (string | null)[] = [] @@ -83,6 +83,13 @@ export function buildDesignSystem(theme: Theme): DesignSystem { getAstNodeSize() { return compiledAstNodes.size }, + + markInvalidCandidate(candidate: string) { + invalidRawCandidates.add(candidate) + }, + isInvalidCandidate(candidate: string) { + return invalidRawCandidates.has(candidate) + }, } return designSystem diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 193d97453..d5205bd52 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -179,7 +179,7 @@ export function compile( // and should be ignored. let allValidCandidates = new Set() for (let rawCandidate of rawCandidates) { - if (!designSystem.invalidRawCandidates.has(rawCandidate)) { + if (!designSystem.isInvalidCandidate(rawCandidate)) { allValidCandidates.add(rawCandidate) } } @@ -190,7 +190,7 @@ export function compile( // Add all new candidates unless we know that they are invalid. let previousSize = allValidCandidates.size for (let candidate of newRawCandidates) { - if (!designSystem.invalidRawCandidates.has(candidate)) { + if (!designSystem.isInvalidCandidate(candidate)) { allValidCandidates.add(candidate) } } From a45ae05fcaf93ee9a888c890bcfb3745c2d876ee Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 15:56:39 +0100 Subject: [PATCH 21/24] improve checking if new candidates were added or not --- packages/@tailwindcss-cli/src/commands/build/index.ts | 4 +++- packages/tailwindcss/src/design-system.ts | 2 +- packages/tailwindcss/src/index.ts | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index ca3859b6e..7d007f308 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -196,7 +196,9 @@ export async function handle(args: Result>) { // Scan changed files only for incremental rebuilds. else if (rebuildStrategy === 'incremental') { - compiledCss = result.rebuild(scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)) + let newCandidates = scanFiles(changedFiles, IO.Sequential | Parsing.Sequential) + + compiledCss = result.rebuild(newCandidates) } await write(compiledCss, args) diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index fa6390f0a..22277eda3 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -32,8 +32,8 @@ export type DesignSystem = { export function buildDesignSystem(theme: Theme): DesignSystem { let utilities = createUtilities(theme) let variants = createVariants(theme) - let invalidRawCandidates = new Set() + let invalidRawCandidates = new Set() let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem)) let parsedCandidates = new DefaultMap((candidate) => parseCandidate(candidate, designSystem)) let compiledAstNodes = new DefaultMap((candidate) => compileAstNodes(candidate, designSystem)) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d5205bd52..cb5559a1b 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -187,17 +187,20 @@ export function compile( return { rebuild(newRawCandidates: string[]) { + let didChange = false + // Add all new candidates unless we know that they are invalid. - let previousSize = allValidCandidates.size + let prevSize = allValidCandidates.size for (let candidate of newRawCandidates) { if (!designSystem.isInvalidCandidate(candidate)) { allValidCandidates.add(candidate) + didChange ||= allValidCandidates.size !== prevSize } } // 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 (previousSize === allValidCandidates.size) { + if (!didChange) { return compiledCss } From ac3f03a07e42c97eb2ef8ff571b36bbec090e830 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 16:55:25 +0100 Subject: [PATCH 22/24] rename `tailwindcss-v4` to `@tailwindcss/postcss` --- 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 13bca6283..e2d3d8eca 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -16,7 +16,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let optimize = opts.optimize ?? process.env.NODE_ENV === 'production' return { - postcssPlugin: 'tailwindcss-v4', + postcssPlugin: '@tailwindcss/postcss', plugins: [ // We need to run `postcss-import` first to handle `@import` rules. postcssImport(), @@ -64,7 +64,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { for (let file of files) { result.messages.push({ type: 'dependency', - plugin: 'tailwindcss-v4', + plugin: '@tailwindcss/postcss', file, parent: result.opts.from, }) @@ -76,7 +76,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { for (let { base, glob } of globs) { result.messages.push({ type: 'dir-dependency', - plugin: 'tailwindcss-v4', + plugin: '@tailwindcss/postcss', dir: base, glob, parent: result.opts.from, From 316190fe5adba04cd81381ad5b35dddf221d2d2d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 16:56:05 +0100 Subject: [PATCH 23/24] start incremental rebuild work on PostCss --- packages/@tailwindcss-postcss/src/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index e2d3d8eca..6555bbfd0 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -51,9 +51,11 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { root.append(postcss.parse(output, result.opts)) } + let compileResult = compile(root.toString(), []) + // No `@tailwind` means we don't have to look for candidates if (!hasTailwind) { - replaceCss(compile(root.toString(), []).css) + replaceCss(compileResult.css) return } @@ -83,7 +85,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { }) } - replaceCss(compile(root.toString(), candidates).css) + console.time('Compile') + replaceCss(compileResult.rebuild(candidates)) + console.timeEnd('Compile') }, ], } From a99bcfba60715f9b26540f956fe2c90b2725fed9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 8 Mar 2024 18:29:51 +0100 Subject: [PATCH 24/24] use incremental builds in PostCSS --- packages/@tailwindcss-postcss/src/index.ts | 121 +++++++++++++++++---- 1 file changed, 100 insertions(+), 21 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 6555bbfd0..0e200a6f1 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -1,8 +1,30 @@ import { scanDir } from '@tailwindcss/oxide' +import fs from 'fs' import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss' import postcssImport from 'postcss-import' import { compile, optimizeCss } from 'tailwindcss' +/** + * A Map that can generate default values for keys that don't exist. + * Generated default values are added to the map to avoid recomputation. + */ +class DefaultMap extends Map { + constructor(private factory: (key: T, self: DefaultMap) => V) { + super() + } + + get(key: T): V { + let value = super.get(key) + + if (value === undefined) { + value = this.factory(key, this) + this.set(key, value) + } + + return value + } +} + type PluginOptions = { // The base directory to scan for class candidates. base?: string @@ -15,6 +37,15 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let base = opts.base ?? process.cwd() let optimize = opts.optimize ?? process.env.NODE_ENV === 'production' + let cache = new DefaultMap(() => { + return { + mtimes: new Map(), + rebuild: null as null | ReturnType['rebuild'], + previousCss: '', + previousOptimizedCss: '', + } + }) + return { postcssPlugin: '@tailwindcss/postcss', plugins: [ @@ -22,6 +53,38 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { postcssImport(), (root, result) => { + let from = result.opts.from ?? '' + let context = cache.get(from) + + let rebuildStrategy: 'full' | 'incremental' = 'incremental' + + // Bookkeeping — track file modification times to CSS files + { + let changedTime = fs.statSync(from, { throwIfNoEntry: false })?.mtimeMs ?? null + if (changedTime !== null) { + let prevTime = context.mtimes.get(from) + if (prevTime !== changedTime) { + rebuildStrategy = 'full' + context.mtimes.set(from, changedTime) + } + } else { + rebuildStrategy = 'full' + } + for (let message of result.messages) { + if (message.type === 'dependency') { + let file = message.file as string + let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null + if (changedTime !== null) { + let prevTime = context.mtimes.get(file) + if (prevTime !== changedTime) { + rebuildStrategy = 'full' + context.mtimes.set(file, changedTime) + } + } + } + } + } + let hasApply = false let hasTailwind = false @@ -40,24 +103,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // Do nothing if neither `@tailwind` nor `@apply` is used if (!hasTailwind && !hasApply) return - function replaceCss(css: string) { - root.removeAll() - let output = css - if (optimize) { - output = optimizeCss(output, { - minify: typeof optimize === 'object' ? optimize.minify : false, - }) - } - root.append(postcss.parse(output, result.opts)) - } - - let compileResult = compile(root.toString(), []) - - // No `@tailwind` means we don't have to look for candidates - if (!hasTailwind) { - replaceCss(compileResult.css) - return - } + let css = '' // Look for candidates used to generate the CSS let { candidates, files, globs } = scanDir({ base, globs: true }) @@ -85,9 +131,42 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { }) } - console.time('Compile') - replaceCss(compileResult.rebuild(candidates)) - console.timeEnd('Compile') + if (rebuildStrategy === 'full') { + if (hasTailwind) { + let compileResult = compile(root.toString(), candidates) + css = compileResult.css + context.rebuild = compileResult.rebuild.bind(compileResult) + } else { + css = compile(root.toString(), []).css + } + } else if (rebuildStrategy === 'incremental') { + css = context.rebuild!(candidates) + } + + function replaceCss(css: string) { + root.removeAll() + root.append(postcss.parse(css, result.opts)) + } + + // Replace CSS + if (css === context.previousCss) { + if (optimize) { + replaceCss(context.previousOptimizedCss) + } else { + replaceCss(css) + } + } else { + if (optimize) { + let optimizedCss = optimizeCss(css, { + minify: typeof optimize === 'object' ? optimize.minify : false, + }) + replaceCss(optimizedCss) + context.previousOptimizedCss = optimizedCss + } else { + replaceCss(css) + } + context.previousCss = css + } }, ], }