diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index baf980892..c10ed728c 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -57,6 +57,8 @@ export function options() { } satisfies Arg } +let cache: Map = new Map() + export async function handle(args: Result>) { let base = path.resolve(args['--cwd']) @@ -96,7 +98,7 @@ export async function handle(args: Result>) { ) // Compile the input - let result = optimizeCss(compile(input, candidates), { + let result = optimizeCss(compile(input, candidates, { cache }), { file: args['--input'] ?? 'input.css', minify: args['--minify'], }) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index b91c19aca..1d40a69a8 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -6,18 +6,27 @@ import { DefaultMap } from './utils/default-map' import { escape } from './utils/escape' import type { Variants } from './variants' +type SortDetails = { + properties: number[] + variants: bigint + candidate: string +} + +type CompileOptions = { + throwOnInvalidCandidate?: boolean + cache?: Map +} + export function compileCandidates( rawCandidates: string[], designSystem: DesignSystem, - { throwOnInvalidCandidate = false } = {}, + { throwOnInvalidCandidate = false, cache }: CompileOptions = {}, ) { // Ensure the candidates are sorted alphabetically rawCandidates.sort() - let nodeSorting = new Map< - AstNode, - { properties: number[]; variants: bigint; candidate: string } - >() + let nodeSorting = new Map() + let astNodes: AstNode[] = [] // A lazy map implementation that will return the variant if it exists. If it @@ -31,6 +40,14 @@ export function compileCandidates( // Parse candidates and variants for (let rawCandidate of rawCandidates) { + let existing = cache?.get(rawCandidate) + if (existing) { + candidates.set(existing[0], rawCandidate) + astNodes.push(existing[1]) + nodeSorting.set(existing[1], existing[2]) + continue + } + let candidate = parseCandidate(rawCandidate, designSystem.utilities, parsedVariants) if (candidate === null) { if (throwOnInvalidCandidate) { @@ -48,6 +65,9 @@ export function compileCandidates( // Create the AST next: for (let [candidate, rawCandidate] of candidates) { + let existing = cache?.get(rawCandidate) + if (existing) continue + let nodes: AstNode[] = [] // Handle arbitrary properties @@ -124,6 +144,8 @@ export function compileCandidates( candidate: rawCandidate, }) astNodes.push(node) + + cache?.set(rawCandidate, [candidate, node, nodeSorting.get(node)!]) } astNodes.sort((a, z) => { diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index e0e3750ab..2f6abdcb5 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -6,7 +6,11 @@ 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[], + cache: Map = new Map(), +) { let ast = CSS.parse(css) if (process.env.NODE_ENV !== 'test') { @@ -90,7 +94,7 @@ export function compile(css: string, rawCandidates: string[]) { // class CSS. walk(ast, (node, { replaceWith }) => { if (node.kind === 'rule' && node.selector === '@tailwind utilities') { - replaceWith(compileCandidates(rawCandidates, designSystem).astNodes) + replaceWith(compileCandidates(rawCandidates, designSystem, { cache: cache as any }).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. @@ -110,6 +114,7 @@ export function compile(css: string, rawCandidates: string[]) { { // Parse the candidates to an AST that we can replace the `@apply` rule with. let candidateAst = compileCandidates(candidates, designSystem, { + cache: cache as any, throwOnInvalidCandidate: true, }).astNodes