From c03754e6d5ba11c1c69300ba67c4c6f34c2055e5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 18:30:29 +0200 Subject: [PATCH 01/19] add a general purpose storage area on the design system This will allow us to add data to the design system itself instead of using module-scope Map's where the key is pointing to the DesignSystem. This has the benefit that all data is tied to a DesignSystem. If you don't need a design system anymore, then garbage collection will kick in and remove everything. If we rely on module scope maps, then we have to do the cleanup (or use WeakMap, but that requires more testing to know if that _actually_ works when multiple WeakMap instances reference a DesignSystem). Since it's a general purpose object, we have to ensure that you can't accidentally override data... because of this, the storage is typed such that the keys must be Symbols. --- packages/tailwindcss/src/design-system.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 2dfa24128ae4..d533743c172b 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -59,6 +59,9 @@ export type DesignSystem = { // Used by IntelliSense candidatesToCss(classes: string[]): (string | null)[] + + // General purpose storage + storage: Record } export function buildDesignSystem(theme: Theme): DesignSystem { @@ -214,6 +217,10 @@ export function buildDesignSystem(theme: Theme): DesignSystem { canonicalizeCandidates(candidates: string[], options?: CanonicalizeOptions) { return canonicalizeCandidates(this, candidates, options) }, + + // General purpose storage, each key has to be a unique symbol to avoid + // collisions. + storage: {}, } return designSystem From c7318aaf267cceed9e6da7082929d18a7ae046c0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 12:50:03 +0200 Subject: [PATCH 02/19] start preparing the design system We need to enhance the design system to setup the storage on the design system itself. We can use the `prepareDesignSystemStorage` to "promote" the current `BaseDesignSystem` to the new `DesignSystem` where the storage is properly setup. This way, TypeScript will only allow you to access the necessary storage parts if you are operating on a DesignSystem that has the storage setup already. Now it's up to us to ensure the storage is setup properly in the `prepareDesignSystemStorage` function. --- packages/tailwindcss/src/canonicalize-candidates.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 3977ba2ecd1e..be4b35196008 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -9,7 +9,7 @@ import { type Variant, } from './candidate' import { keyPathToCssProperty } from './compat/apply-config-to-theme' -import type { DesignSystem } from './design-system' +import type { DesignSystem as BaseDesignSystem } from './design-system' import * as SelectorParser from './selector-parser' import { computeUtilityProperties, @@ -74,6 +74,17 @@ interface InternalCanonicalizeOptions { signatureOptions: SignatureOptions } +export interface DesignSystem extends BaseDesignSystem { + storage: { + } +} + +function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignSystem { + let designSystem = baseDesignSystem as DesignSystem + + return designSystem +} + const signatureOptionsCache = new DefaultMap((designSystem: DesignSystem) => { return new DefaultMap((rem: number | null = null) => { return new DefaultMap((features: SignatureFeatures) => { From c8c8123387103ae6bc3952b8d3d532e66937313b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 12:58:24 +0200 Subject: [PATCH 03/19] setup signatureOptionsCache --- .../src/canonicalize-candidates.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index be4b35196008..2390b79d6818 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -76,35 +76,43 @@ interface InternalCanonicalizeOptions { export interface DesignSystem extends BaseDesignSystem { storage: { + [SIGNATURE_OPTIONS_KEY]: DefaultMap< + number | null, // Rem value + DefaultMap + > } } function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignSystem { let designSystem = baseDesignSystem as DesignSystem + designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache(designSystem) + return designSystem } -const signatureOptionsCache = new DefaultMap((designSystem: DesignSystem) => { - return new DefaultMap((rem: number | null = null) => { +const SIGNATURE_OPTIONS_KEY = Symbol() +function createSignatureOptionsCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof SIGNATURE_OPTIONS_KEY] { + return new DefaultMap((rem: number | null) => { return new DefaultMap((features: SignatureFeatures) => { return { designSystem, rem, features } satisfies SignatureOptions }) }) -}) +} export function createSignatureOptions( - designSystem: DesignSystem, + baseDesignSystem: BaseDesignSystem, options?: CanonicalizeOptions, ): SignatureOptions { let features = SignatureFeatures.None if (options?.collapse) features |= SignatureFeatures.ExpandProperties if (options?.logicalToPhysical) features |= SignatureFeatures.LogicalToPhysical - return signatureOptionsCache - .get(designSystem) - .get(options?.rem ?? null) - .get(features) + let designSystem = prepareDesignSystemStorage(baseDesignSystem) + + return designSystem.storage[SIGNATURE_OPTIONS_KEY].get(options?.rem ?? null).get(features) } const internalOptionsCache = new DefaultMap((designSystem: DesignSystem) => { From 249a0e92a77eb10d34f20a1676074fc1d832a2ab Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 13:02:31 +0200 Subject: [PATCH 04/19] setup internalOptionsCache --- .../src/canonicalize-candidates.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 2390b79d6818..d5fdfcb802bb 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -80,6 +80,10 @@ export interface DesignSystem extends BaseDesignSystem { number | null, // Rem value DefaultMap > + [INTERNAL_OPTIONS_KEY]: DefaultMap< + SignatureOptions, + DefaultMap + > } } @@ -87,6 +91,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS let designSystem = baseDesignSystem as DesignSystem designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache(designSystem) + designSystem.storage[INTERNAL_OPTIONS_KEY] ??= createInternalOptionsCache(designSystem) return designSystem } @@ -115,27 +120,28 @@ export function createSignatureOptions( return designSystem.storage[SIGNATURE_OPTIONS_KEY].get(options?.rem ?? null).get(features) } -const internalOptionsCache = new DefaultMap((designSystem: DesignSystem) => { +const INTERNAL_OPTIONS_KEY = Symbol() +function createInternalOptionsCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof INTERNAL_OPTIONS_KEY] { return new DefaultMap((signatureOptions: SignatureOptions) => { return new DefaultMap((features: Features) => { - return { - features, - designSystem, - signatureOptions, - } satisfies InternalCanonicalizeOptions + return { features, designSystem, signatureOptions } satisfies InternalCanonicalizeOptions }) }) -}) +} function createCanonicalizeOptions( - designSystem: DesignSystem, + baseDesignSystem: BaseDesignSystem, signatureOptions: SignatureOptions, options?: CanonicalizeOptions, ) { let features = Features.None if (options?.collapse) features |= Features.CollapseUtilities - return internalOptionsCache.get(designSystem).get(signatureOptions).get(features) + let designSystem = prepareDesignSystemStorage(baseDesignSystem) + + return designSystem.storage[INTERNAL_OPTIONS_KEY].get(signatureOptions).get(features) } export function canonicalizeCandidates( From 0497020b94498de7bed47fa5989d3f66d029430c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 13:05:37 +0200 Subject: [PATCH 05/19] setup canonicalizeCandidateCache --- .../src/canonicalize-candidates.ts | 118 ++++++++++-------- 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index d5fdfcb802bb..45a5fee52cd5 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -84,6 +84,10 @@ export interface DesignSystem extends BaseDesignSystem { SignatureOptions, DefaultMap > + [CANONICALIZE_CANDIDATE_KEY]: DefaultMap< + InternalCanonicalizeOptions, + DefaultMap + > } } @@ -92,6 +96,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache(designSystem) designSystem.storage[INTERNAL_OPTIONS_KEY] ??= createInternalOptionsCache(designSystem) + designSystem.storage[CANONICALIZE_CANDIDATE_KEY] ??= createCanonicalizeCandidateCache() return designSystem } @@ -145,15 +150,17 @@ function createCanonicalizeOptions( } export function canonicalizeCandidates( - designSystem: DesignSystem, + baseDesignSystem: BaseDesignSystem, candidates: string[], options?: CanonicalizeOptions, ): string[] { - let signatureOptions = createSignatureOptions(designSystem, options) - let canonicalizeOptions = createCanonicalizeOptions(designSystem, signatureOptions, options) + let signatureOptions = createSignatureOptions(baseDesignSystem, options) + let canonicalizeOptions = createCanonicalizeOptions(baseDesignSystem, signatureOptions, options) + + let designSystem = prepareDesignSystemStorage(baseDesignSystem) let result = new Set() - let cache = canonicalizeCandidateCache.get(canonicalizeOptions) + let cache = designSystem.storage[CANONICALIZE_CANDIDATE_KEY].get(canonicalizeOptions) for (let candidate of candidates) { result.add(cache.get(candidate)) } @@ -325,66 +332,69 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st } } -const canonicalizeCandidateCache = new DefaultMap((options: InternalCanonicalizeOptions) => { - let ds = options.designSystem - let prefix = ds.theme.prefix ? `${ds.theme.prefix}:` : '' - let variantCache = canonicalizeVariantCache.get(options) - let utilityCache = canonicalizeUtilityCache.get(options) - - return new DefaultMap((rawCandidate: string, self) => { - for (let candidate of ds.parseCandidate(rawCandidate)) { - let variants = candidate.variants - .slice() - .reverse() - .flatMap((variant) => variantCache.get(variant)) - let important = candidate.important - - // Canonicalize the base candidate (utility), and re-attach the variants - // and important flag afterwards. This way we can maximize cache hits for - // the base candidate and each individual variant. - if (important || variants.length > 0) { - let canonicalizedUtility = self.get( - ds.printCandidate({ ...candidate, variants: [], important: false }), - ) +const CANONICALIZE_CANDIDATE_KEY = Symbol() +function createCanonicalizeCandidateCache(): DesignSystem['storage'][typeof CANONICALIZE_CANDIDATE_KEY] { + return new DefaultMap((options: InternalCanonicalizeOptions) => { + let ds = options.designSystem + let prefix = ds.theme.prefix ? `${ds.theme.prefix}:` : '' + let variantCache = canonicalizeVariantCache.get(options) + let utilityCache = canonicalizeUtilityCache.get(options) + + return new DefaultMap((rawCandidate: string, self) => { + for (let candidate of ds.parseCandidate(rawCandidate)) { + let variants = candidate.variants + .slice() + .reverse() + .flatMap((variant) => variantCache.get(variant)) + let important = candidate.important + + // Canonicalize the base candidate (utility), and re-attach the variants + // and important flag afterwards. This way we can maximize cache hits for + // the base candidate and each individual variant. + if (important || variants.length > 0) { + let canonicalizedUtility = self.get( + ds.printCandidate({ ...candidate, variants: [], important: false }), + ) + + // Rebuild the final candidate + let result = canonicalizedUtility + + // Remove the prefix if there are variants, because the variants exist + // between the prefix and the base candidate. + if (ds.theme.prefix !== null && variants.length > 0) { + result = result.slice(prefix.length) + } - // Rebuild the final candidate - let result = canonicalizedUtility + // Re-attach the variants + if (variants.length > 0) { + result = `${variants.map((v) => ds.printVariant(v)).join(':')}:${result}` + } - // Remove the prefix if there are variants, because the variants exist - // between the prefix and the base candidate. - if (ds.theme.prefix !== null && variants.length > 0) { - result = result.slice(prefix.length) - } + // Re-attach the important flag + if (important) { + result += '!' + } - // Re-attach the variants - if (variants.length > 0) { - result = `${variants.map((v) => ds.printVariant(v)).join(':')}:${result}` - } + // Re-attach the prefix if there were variants + if (ds.theme.prefix !== null && variants.length > 0) { + result = `${prefix}${result}` + } - // Re-attach the important flag - if (important) { - result += '!' + return result } - // Re-attach the prefix if there were variants - if (ds.theme.prefix !== null && variants.length > 0) { - result = `${prefix}${result}` + // We are guaranteed to have no variants and no important flag, just the + // base candidate left to canonicalize. + let result = utilityCache.get(rawCandidate) + if (result !== rawCandidate) { + return result } - - return result - } - - // We are guaranteed to have no variants and no important flag, just the - // base candidate left to canonicalize. - let result = utilityCache.get(rawCandidate) - if (result !== rawCandidate) { - return result } - } - return rawCandidate + return rawCandidate + }) }) -}) +} type VariantCanonicalizationFunction = ( variant: Variant, From f83ffeca535d68e7fdfcddd65374767127fa5f51 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 13:06:07 +0200 Subject: [PATCH 06/19] setup canonicalizeVariantCache --- .../src/canonicalize-candidates.ts | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 45a5fee52cd5..2dd5b7a59f9c 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -88,6 +88,10 @@ export interface DesignSystem extends BaseDesignSystem { InternalCanonicalizeOptions, DefaultMap > + [CANONICALIZE_VARIANT_KEY]: DefaultMap< + InternalCanonicalizeOptions, + DefaultMap + > } } @@ -97,6 +101,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache(designSystem) designSystem.storage[INTERNAL_OPTIONS_KEY] ??= createInternalOptionsCache(designSystem) designSystem.storage[CANONICALIZE_CANDIDATE_KEY] ??= createCanonicalizeCandidateCache() + designSystem.storage[CANONICALIZE_VARIANT_KEY] ??= createCanonicalizeVariantCache() return designSystem } @@ -337,7 +342,7 @@ function createCanonicalizeCandidateCache(): DesignSystem['storage'][typeof CANO return new DefaultMap((options: InternalCanonicalizeOptions) => { let ds = options.designSystem let prefix = ds.theme.prefix ? `${ds.theme.prefix}:` : '' - let variantCache = canonicalizeVariantCache.get(options) + let variantCache = ds.storage[CANONICALIZE_VARIANT_KEY].get(options) let utilityCache = canonicalizeUtilityCache.get(options) return new DefaultMap((rawCandidate: string, self) => { @@ -408,25 +413,28 @@ const VARIANT_CANONICALIZATIONS: VariantCanonicalizationFunction[] = [ arbitraryVariants, ] -const canonicalizeVariantCache = new DefaultMap((options: InternalCanonicalizeOptions) => { - return new DefaultMap((variant: Variant): Variant[] => { - let replacement = [variant] - for (let fn of VARIANT_CANONICALIZATIONS) { - for (let current of replacement.splice(0)) { - // A single variant can result in multiple variants, e.g.: - // `[&>[data-selected]]:flex` → `*:data-selected:flex` - let result = fn(cloneVariant(current), options) - if (Array.isArray(result)) { - replacement.push(...result) - continue - } else { - replacement.push(result) +const CANONICALIZE_VARIANT_KEY = Symbol() +function createCanonicalizeVariantCache(): DesignSystem['storage'][typeof CANONICALIZE_VARIANT_KEY] { + return new DefaultMap((options: InternalCanonicalizeOptions) => { + return new DefaultMap((variant: Variant): Variant[] => { + let replacement = [variant] + for (let fn of VARIANT_CANONICALIZATIONS) { + for (let current of replacement.splice(0)) { + // A single variant can result in multiple variants, e.g.: + // `[&>[data-selected]]:flex` → `*:data-selected:flex` + let result = fn(cloneVariant(current), options) + if (Array.isArray(result)) { + replacement.push(...result) + continue + } else { + replacement.push(result) + } } } - } - return replacement + return replacement + }) }) -}) +} type UtilityCanonicalizationFunction = ( candidate: Candidate, From 2e43068424bcf6dc542ba4318cfa35a29885a512 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 13:06:43 +0200 Subject: [PATCH 07/19] setup canonicalizeUtilityCache --- .../src/canonicalize-candidates.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 2dd5b7a59f9c..303f389fab4a 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -92,6 +92,7 @@ export interface DesignSystem extends BaseDesignSystem { InternalCanonicalizeOptions, DefaultMap > + [CANONICALIZE_UTILITY_KEY]: DefaultMap> } } @@ -102,6 +103,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[INTERNAL_OPTIONS_KEY] ??= createInternalOptionsCache(designSystem) designSystem.storage[CANONICALIZE_CANDIDATE_KEY] ??= createCanonicalizeCandidateCache() designSystem.storage[CANONICALIZE_VARIANT_KEY] ??= createCanonicalizeVariantCache() + designSystem.storage[CANONICALIZE_UTILITY_KEY] ??= createCanonicalizeUtilityCache() return designSystem } @@ -343,7 +345,7 @@ function createCanonicalizeCandidateCache(): DesignSystem['storage'][typeof CANO let ds = options.designSystem let prefix = ds.theme.prefix ? `${ds.theme.prefix}:` : '' let variantCache = ds.storage[CANONICALIZE_VARIANT_KEY].get(options) - let utilityCache = canonicalizeUtilityCache.get(options) + let utilityCache = ds.storage[CANONICALIZE_UTILITY_KEY].get(options) return new DefaultMap((rawCandidate: string, self) => { for (let candidate of ds.parseCandidate(rawCandidate)) { @@ -452,25 +454,28 @@ const UTILITY_CANONICALIZATIONS: UtilityCanonicalizationFunction[] = [ optimizeModifier, ] -const canonicalizeUtilityCache = new DefaultMap((options: InternalCanonicalizeOptions) => { - let designSystem = options.designSystem - return new DefaultMap((rawCandidate: string): string => { - for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { - let replacement = cloneCandidate(readonlyCandidate) as Writable +const CANONICALIZE_UTILITY_KEY = Symbol() +function createCanonicalizeUtilityCache(): DesignSystem['storage'][typeof CANONICALIZE_UTILITY_KEY] { + return new DefaultMap((options: InternalCanonicalizeOptions) => { + let designSystem = options.designSystem + return new DefaultMap((rawCandidate: string): string => { + for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { + let replacement = cloneCandidate(readonlyCandidate) as Writable - for (let fn of UTILITY_CANONICALIZATIONS) { - replacement = fn(replacement, options) - } + for (let fn of UTILITY_CANONICALIZATIONS) { + replacement = fn(replacement, options) + } - let canonicalizedCandidate = designSystem.printCandidate(replacement) - if (rawCandidate !== canonicalizedCandidate) { - return canonicalizedCandidate + let canonicalizedCandidate = designSystem.printCandidate(replacement) + if (rawCandidate !== canonicalizedCandidate) { + return canonicalizedCandidate + } } - } - return rawCandidate + return rawCandidate + }) }) -}) +} // ---- From 78ed32431e42470c96a5dd8f3e7fc8fdf63fd124 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 13:07:27 +0200 Subject: [PATCH 08/19] setup converterCache --- .../tailwindcss/src/canonicalize-candidates.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 303f389fab4a..c3eecb896987 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -93,6 +93,7 @@ export interface DesignSystem extends BaseDesignSystem { DefaultMap > [CANONICALIZE_UTILITY_KEY]: DefaultMap> + [CONVERTER_KEY]: (input: string, options?: Convert) => [string, CandidateModifier | null] } } @@ -104,6 +105,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[CANONICALIZE_CANDIDATE_KEY] ??= createCanonicalizeCandidateCache() designSystem.storage[CANONICALIZE_VARIANT_KEY] ??= createCanonicalizeVariantCache() designSystem.storage[CANONICALIZE_UTILITY_KEY] ??= createCanonicalizeUtilityCache() + designSystem.storage[CONVERTER_KEY] ??= createConverterCache(designSystem) return designSystem } @@ -504,7 +506,7 @@ const enum Convert { } function themeToVarUtility(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate { - let convert = converterCache.get(options.designSystem) + let convert = options.designSystem.storage[CONVERTER_KEY] if (candidate.kind === 'arbitrary') { let [newValue, modifier] = convert( @@ -539,7 +541,7 @@ function themeToVarVariant( variant: Variant, options: InternalCanonicalizeOptions, ): Variant | Variant[] { - let convert = converterCache.get(options.designSystem) + let convert = options.designSystem.storage[CONVERTER_KEY] let iterator = walkVariants(variant) for (let [variant] of iterator) { @@ -559,8 +561,11 @@ function themeToVarVariant( return variant } -const converterCache = new DefaultMap((ds: DesignSystem) => { - return createConverter(ds) +const CONVERTER_KEY = Symbol() +function createConverterCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof CONVERTER_KEY] { + return createConverter(designSystem) function createConverter(designSystem: DesignSystem) { function convert(input: string, options = Convert.All): [string, CandidateModifier | null] { @@ -716,7 +721,7 @@ const converterCache = new DefaultMap((ds: DesignSystem) => { return convert } -}) +} function substituteFunctionsInValue( ast: ValueParser.ValueAstNode[], From 519dc87268f7931b00f39ded32c1ce8b6fe441a5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 13:07:48 +0200 Subject: [PATCH 09/19] setup spacingCache --- packages/tailwindcss/src/canonicalize-candidates.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index c3eecb896987..28172f63306b 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -94,6 +94,7 @@ export interface DesignSystem extends BaseDesignSystem { > [CANONICALIZE_UTILITY_KEY]: DefaultMap> [CONVERTER_KEY]: (input: string, options?: Convert) => [string, CandidateModifier | null] + [SPACING_KEY]: DefaultMap | null } } @@ -106,6 +107,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[CANONICALIZE_VARIANT_KEY] ??= createCanonicalizeVariantCache() designSystem.storage[CANONICALIZE_UTILITY_KEY] ??= createCanonicalizeUtilityCache() designSystem.storage[CONVERTER_KEY] ??= createConverterCache(designSystem) + designSystem.storage[SPACING_KEY] ??= createSpacingCache(designSystem) return designSystem } @@ -858,8 +860,11 @@ function printUnprefixedCandidate(designSystem: DesignSystem, candidate: Candida // ---- -const spacing = new DefaultMap | null>((ds) => { - let spacingMultiplier = ds.resolveThemeValue('--spacing') +const SPACING_KEY = Symbol() +function createSpacingCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof SPACING_KEY] { + let spacingMultiplier = designSystem.resolveThemeValue('--spacing') if (spacingMultiplier === undefined) return null let parsed = dimensions.get(spacingMultiplier) @@ -876,7 +881,7 @@ const spacing = new DefaultMap | return myValue / value }) -}) +} function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate { // We are only interested in arbitrary properties and arbitrary values @@ -967,7 +972,7 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO candidate.kind === 'arbitrary' ? candidate.value : (candidate.value?.value ?? null) if (value === null) return - let spacingMultiplier = spacing.get(designSystem)?.get(value) ?? null + let spacingMultiplier = designSystem.storage[SPACING_KEY]?.get(value) ?? null let rootPrefix = '' if (spacingMultiplier !== null && spacingMultiplier < 0) { rootPrefix = '-' From aedd7866400ed7b55fcb4c518852be05e478477c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 13:18:09 +0200 Subject: [PATCH 10/19] move `signatures` logic into `canonicalize-candidates` We only really need it in this file, this will also make it a bit easier to further enhance the design system storage. --- .../src/canonicalize-candidates.ts | 566 +++++++++++++++++- .../tailwindcss/src/expand-declaration.ts | 2 +- packages/tailwindcss/src/signatures.ts | 561 ----------------- 3 files changed, 557 insertions(+), 572 deletions(-) delete mode 100644 packages/tailwindcss/src/signatures.ts diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 28172f63306b..47200944b3a9 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1,7 +1,10 @@ +import { substituteAtApply } from './apply' +import { atRule, cloneAstNode, styleRule, toCss, type AstNode } from './ast' import * as AttributeSelectorParser from './attribute-selector-parser' import { cloneCandidate, cloneVariant, + printArbitraryValue, printModifier, type Candidate, type CandidateModifier, @@ -9,18 +12,12 @@ import { type Variant, } from './candidate' import { keyPathToCssProperty } from './compat/apply-config-to-theme' +import { constantFoldDeclaration } from './constant-fold-declaration' import type { DesignSystem as BaseDesignSystem } from './design-system' +import { CompileAstFlags } from './design-system' +import { expandDeclaration } from './expand-declaration' import * as SelectorParser from './selector-parser' -import { - computeUtilityProperties, - computeUtilitySignature, - computeVariantSignature, - preComputedUtilities, - preComputedVariants, - SignatureFeatures, - staticUtilitiesByPropertyAndValue, - type SignatureOptions, -} from './signatures' +import { ThemeOptions } from './theme' import type { Writable } from './types' import { DefaultMap } from './utils/default-map' import { dimensions } from './utils/dimensions' @@ -1864,6 +1861,555 @@ function optimizeModifier(candidate: Candidate, options: InternalCanonicalizeOpt return candidate } +export enum SignatureFeatures { + None = 0, + ExpandProperties = 1 << 0, + LogicalToPhysical = 1 << 1, +} + +export interface SignatureOptions { + /** + * The root font size in pixels. If provided, `rem` values will be normalized + * to `px` values. + * + * E.g.: `mt-[16px]` with `rem: 16` will become `mt-4` (assuming `--spacing: 0.25rem`). + */ + rem: number | null + + /** + * Features that influence how signatures are computed. + */ + features: SignatureFeatures + + /** + * The design system to use for computing the signature of candidates. + */ + designSystem: DesignSystem +} + +// Given a utility, compute a signature that represents the utility. The +// signature will be a normalised form of the generated CSS for the utility, or +// a unique symbol if the utility is not valid. The class in the selector will +// be replaced with the `.x` selector. +// +// This function should only be passed the base utility so `flex`, `hover:flex` +// and `focus:flex` will all use just `flex`. Variants are handled separately. +// +// E.g.: +// +// | UTILITY | GENERATED SIGNATURE | +// | ---------------- | ----------------------- | +// | `[display:flex]` | `.x { display: flex; }` | +// | `flex` | `.x { display: flex; }` | +// +// These produce the same signature, therefore they represent the same utility. +export const computeUtilitySignature = new DefaultMap((options: SignatureOptions) => { + let designSystem = options.designSystem + + return new DefaultMap((utility) => { + try { + // Ensure the prefix is added to the utility if it is not already present. + utility = + designSystem.theme.prefix && !utility.startsWith(designSystem.theme.prefix) + ? `${designSystem.theme.prefix}:${utility}` + : utility + + // Use `@apply` to normalize the selector to `.x` + let ast: AstNode[] = [styleRule('.x', [atRule('@apply', utility)])] + + temporarilyDisableThemeInline(designSystem, () => { + // There's separate utility caches for respect important vs not + // so we want to compile them both with `@theme inline` disabled + for (let candidate of designSystem.parseCandidate(utility)) { + designSystem.compileAstNodes(candidate, CompileAstFlags.RespectImportant) + } + + substituteAtApply(ast, designSystem) + }) + + // Optimize the AST. This is needed such that any internal intermediate + // nodes are gone. This will also cleanup declaration nodes with undefined + // values or `--tw-sort` declarations. + canonicalizeAst(ast, options) + + // Compute the final signature, by generating the CSS for the utility + let signature = toCss(ast) + return signature + } catch { + // A unique symbol is returned to ensure that 2 signatures resulting in + // `null` are not considered equal. + return Symbol() + } + }) +}) + +// Optimize the CSS AST to make it suitable for signature comparison. We want to +// expand declarations, ignore comments, sort declarations etc... +function canonicalizeAst(ast: AstNode[], options: SignatureOptions) { + let { rem, designSystem } = options + + walk(ast, { + enter(node, ctx) { + // Optimize declarations + if (node.kind === 'declaration') { + if (node.value === undefined || node.property === '--tw-sort') { + return WalkAction.Replace([]) + } + + // Ignore `--tw-{property}` if `{property}` exists with the same value + if (node.property.startsWith('--tw-')) { + if ( + (ctx.parent?.nodes ?? []).some( + (sibling) => + sibling.kind === 'declaration' && + node.value === sibling.value && + node.important === sibling.important && + !sibling.property.startsWith('--tw-'), + ) + ) { + return WalkAction.Replace([]) + } + } + + if (options.features & SignatureFeatures.ExpandProperties) { + let replacement = expandDeclaration(node, options.features) + if (replacement) return WalkAction.Replace(replacement) + } + + // Resolve theme values to their inlined value. + if (node.value.includes('var(')) { + node.value = resolveVariablesInValue(node.value, designSystem) + } + + // Very basic `calc(…)` constant folding to handle the spacing scale + // multiplier: + // + // Input: `--spacing(4)` + // → `calc(var(--spacing, 0.25rem) * 4)` + // → `calc(0.25rem * 4)` ← this is the case we will see + // after inlining the variable + // → `1rem` + node.value = constantFoldDeclaration(node.value, rem) + + // We will normalize the `node.value`, this is the same kind of logic + // we use when printing arbitrary values. It will remove unnecessary + // whitespace. + // + // Essentially normalizing the `node.value` to a canonical form. + node.value = printArbitraryValue(node.value) + } + + // Replace special nodes with its children + else if (node.kind === 'context' || node.kind === 'at-root') { + return WalkAction.Replace(node.nodes) + } + + // Remove comments + else if (node.kind === 'comment') { + return WalkAction.Replace([]) + } + + // Remove at-rules that are not needed for the signature + else if (node.kind === 'at-rule' && node.name === '@property') { + return WalkAction.Replace([]) + } + }, + exit(node) { + if (node.kind === 'rule' || node.kind === 'at-rule') { + node.nodes.sort((a, b) => { + if (a.kind !== 'declaration') return 0 + if (b.kind !== 'declaration') return 0 + return a.property.localeCompare(b.property) + }) + } + }, + }) + + return ast +} + +// Resolve theme values to their inlined value. +// +// E.g.: +// +// `[color:var(--color-red-500)]` → `[color:oklch(63.7%_0.237_25.331)]` +// `[color:oklch(63.7%_0.237_25.331)]` → `[color:oklch(63.7%_0.237_25.331)]` +// +// Due to the `@apply` from above, this will become: +// +// ```css +// .example { +// color: oklch(63.7% 0.237 25.331); +// } +// ``` +// +// Which conveniently will be equivalent to: `text-red-500` when we inline +// the value. +// +// Without inlining: +// ```css +// .example { +// color: var(--color-red-500, oklch(63.7% 0.237 25.331)); +// } +// ``` +// +// Inlined: +// ```css +// .example { +// color: oklch(63.7% 0.237 25.331); +// } +// ``` +// +// Recently we made sure that utilities like `text-red-500` also generate +// the fallback value for usage in `@reference` mode. +// +// The second assumption is that if you use `var(--key, fallback)` that +// happens to match a known variable _and_ its inlined value. Then we can +// replace it with the inlined variable. This allows us to handle custom +// `@theme` and `@theme inline` definitions. +function resolveVariablesInValue(value: string, designSystem: DesignSystem): string { + let changed = false + let valueAst = ValueParser.parse(value) + + let seen = new Set() + walk(valueAst, (valueNode) => { + if (valueNode.kind !== 'function') return + if (valueNode.value !== 'var') return + + // Resolve the underlying value of the variable + if (valueNode.nodes.length !== 1 && valueNode.nodes.length < 3) { + return + } + + let variable = valueNode.nodes[0].value + + // Drop the prefix from the variable name if it is present. The + // internal variable doesn't have the prefix. + if (designSystem.theme.prefix && variable.startsWith(`--${designSystem.theme.prefix}-`)) { + variable = variable.slice(`--${designSystem.theme.prefix}-`.length) + } + let variableValue = designSystem.resolveThemeValue(variable) + // Prevent infinite recursion when the variable value contains the + // variable itself. + if (seen.has(variable)) return + seen.add(variable) + if (variableValue === undefined) return // Couldn't resolve the variable + + // Inject variable fallbacks when no fallback is present yet. + // + // A fallback could consist of multiple values. + // + // E.g.: + // + // ``` + // var(--font-sans, ui-sans-serif, system-ui, sans-serif, …) + // ``` + { + // More than 1 argument means that a fallback is already present + if (valueNode.nodes.length === 1) { + // Inject the fallback value into the variable lookup + changed = true + valueNode.nodes.push(...ValueParser.parse(`,${variableValue}`)) + } + } + + // Replace known variable + inlined fallback value with the value + // itself again + { + // We need at least 3 arguments. The variable, the separator and a fallback value. + if (valueNode.nodes.length >= 3) { + let nodeAsString = ValueParser.toCss(valueNode.nodes) // This could include more than just the variable + let constructedValue = `${valueNode.nodes[0].value},${variableValue}` + if (nodeAsString === constructedValue) { + changed = true + return WalkAction.Replace(ValueParser.parse(variableValue)) + } + } + } + }) + + // Replace the value with the new value + if (changed) return ValueParser.toCss(valueAst) + return value +} + +// Index all static utilities by property and value +export const staticUtilitiesByPropertyAndValue = new DefaultMap((_optiones: SignatureOptions) => { + return new DefaultMap((_property: string) => { + return new DefaultMap((_value: string) => { + return new Set() + }) + }) +}) + +export const computeUtilityProperties = new DefaultMap((options: SignatureOptions) => { + return new DefaultMap((className) => { + let localPropertyValueLookup = new DefaultMap((_property) => new Set()) + let designSystem = options.designSystem + + if ( + options.designSystem.theme.prefix && + !className.startsWith(options.designSystem.theme.prefix) + ) { + className = `${options.designSystem.theme.prefix}:${className}` + } + let parsed = designSystem.parseCandidate(className) + if (parsed.length === 0) return localPropertyValueLookup + + walk( + canonicalizeAst( + designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)), + options, + ), + (node) => { + if (node.kind === 'declaration') { + localPropertyValueLookup.get(node.property).add(node.value!) + staticUtilitiesByPropertyAndValue + .get(options) + .get(node.property) + .get(node.value!) + .add(className) + } + }, + ) + + return localPropertyValueLookup + }) +}) + +// For all static utilities in the system, compute a lookup table that maps the +// utility signature to the utility name. This is used to find the utility name +// for a given utility signature. +// +// For all functional utilities, we can compute static-like utilities by +// essentially pre-computing the values and modifiers. This is a bit slow, but +// also only has to happen once per design system. +export const preComputedUtilities = new DefaultMap((options: SignatureOptions) => { + let { designSystem } = options + let signatures = computeUtilitySignature.get(options) + let lookup = new DefaultMap(() => []) + + // Right now all plugins are implemented using functions so they are a black + // box. Let's use the `getClassList` and consider every known suggestion as a + // static utility for now. + for (let [className, meta] of designSystem.getClassList()) { + let signature = signatures.get(className) + if (typeof signature !== 'string') continue + + // Skip the utility if `-{utility}-0` has the same signature as + // `{utility}-0` (its positive version). This will prefer positive values + // over negative values. + if (className[0] === '-' && className.endsWith('-0')) { + let positiveSignature = signatures.get(className.slice(1)) + if (typeof positiveSignature === 'string' && signature === positiveSignature) { + continue + } + } + + lookup.get(signature).push(className) + computeUtilityProperties.get(options).get(className) + + for (let modifier of meta.modifiers) { + // Modifiers representing numbers can be computed and don't need to be + // pre-computed. Doing the math and at the time of writing this, this + // would save you 250k additionally pre-computed utilities... + if (isValidSpacingMultiplier(modifier)) { + continue + } + + let classNameWithModifier = `${className}/${modifier}` + let signature = signatures.get(classNameWithModifier) + if (typeof signature !== 'string') continue + lookup.get(signature).push(classNameWithModifier) + computeUtilityProperties.get(options).get(classNameWithModifier) + } + } + + return lookup +}) + +// Given a variant, compute a signature that represents the variant. The +// signature will be a normalised form of the generated CSS for the variant, or +// a unique symbol if the variant is not valid. The class in the selector will +// be replaced with `.x`. +// +// E.g.: +// +// | VARIANT | GENERATED SIGNATURE | +// | ---------------- | ----------------------------- | +// | `[&:focus]:flex` | `.x:focus { display: flex; }` | +// | `focus:flex` | `.x:focus { display: flex; }` | +// +// These produce the same signature, therefore they represent the same variant. +export const computeVariantSignature = new DefaultMap((options: SignatureOptions) => { + let { designSystem } = options + return new DefaultMap((variant) => { + try { + // Ensure the prefix is added to the utility if it is not already present. + variant = + designSystem.theme.prefix && !variant.startsWith(designSystem.theme.prefix) + ? `${designSystem.theme.prefix}:${variant}` + : variant + + // Use `@apply` to normalize the selector to `.x` + let ast: AstNode[] = [styleRule('.x', [atRule('@apply', `${variant}:flex`)])] + substituteAtApply(ast, designSystem) + + // Canonicalize selectors to their minimal form + walk(ast, (node) => { + // At-rules + if (node.kind === 'at-rule' && node.params.includes(' ')) { + node.params = node.params.replaceAll(' ', '') + } + + // Style rules + else if (node.kind === 'rule') { + let selectorAst = SelectorParser.parse(node.selector) + let changed = false + walk(selectorAst, (node) => { + if (node.kind === 'separator' && node.value !== ' ') { + node.value = node.value.trim() + changed = true + } + + // Remove unnecessary `:is(…)` selectors + else if (node.kind === 'function' && node.value === ':is') { + // A single selector inside of `:is(…)` can be replaced with the + // selector itself. + // + // E.g.: `:is(.foo)` → `.foo` + if (node.nodes.length === 1) { + changed = true + return WalkAction.Replace(node.nodes) + } + + // A selector with the universal selector `*` followed by a pseudo + // class, can be replaced with the pseudo class itself. + else if ( + node.nodes.length === 2 && + node.nodes[0].kind === 'selector' && + node.nodes[0].value === '*' && + node.nodes[1].kind === 'selector' && + node.nodes[1].value[0] === ':' + ) { + changed = true + return WalkAction.Replace(node.nodes[1]) + } + } + + // Ensure `*` exists before pseudo selectors inside of `:not(…)`, + // `:where(…)`, … + // + // E.g.: + // + // `:not(:first-child)` → `:not(*:first-child)` + // + else if ( + node.kind === 'function' && + node.value[0] === ':' && + node.nodes[0]?.kind === 'selector' && + node.nodes[0]?.value[0] === ':' + ) { + changed = true + node.nodes.unshift({ kind: 'selector', value: '*' }) + } + }) + + if (changed) { + node.selector = SelectorParser.toCss(selectorAst) + } + } + }) + + // Compute the final signature, by generating the CSS for the variant + let signature = toCss(ast) + return signature + } catch { + // A unique symbol is returned to ensure that 2 signatures resulting in + // `null` are not considered equal. + return Symbol() + } + }) +}) + +export const preComputedVariants = new DefaultMap((options: SignatureOptions) => { + let { designSystem } = options + let signatures = computeVariantSignature.get(options) + let lookup = new DefaultMap(() => []) + + // Actual static variants + for (let [root, variant] of designSystem.variants.entries()) { + if (variant.kind === 'static') { + let signature = signatures.get(root) + if (typeof signature !== 'string') continue + lookup.get(signature).push(root) + } + } + + return lookup +}) + +function temporarilyDisableThemeInline(designSystem: DesignSystem, cb: () => T): T { + // Turn off `@theme inline` feature such that `@theme` and `@theme inline` are + // considered the same. The biggest motivation for this is referencing + // variables in another namespace that happen to contain the same value as the + // utility's own namespaces it is reading from. + // + // E.g.: + // + // The `max-w-*` utility doesn't read from the `--breakpoint-*` namespace. + // But it does read from the `--container-*` namespace. It also happens to + // be the case that `--breakpoint-md` and `--container-3xl` are the exact + // same value. + // + // If you then use the `max-w-(--breakpoint-md)` utility, inlining the + // variable would mean: + // - `max-w-(--breakpoint-md)` → `max-width: 48rem;` → `max-w-3xl` + // - `max-w-(--contianer-3xl)` → `max-width: 48rem;` → `max-w-3xl` + // + // Not inlining the variable would mean: + // - `max-w-(--breakpoint-md)` → `max-width: var(--breakpoint-md);` → `max-w-(--breakpoint-md)` + // - `max-w-(--container-3xl)` → `max-width: var(--container-3xl);` → `max-w-3xl` + + // @ts-expect-error We are monkey-patching a method that's considered private + // in TypeScript + let originalGet = designSystem.theme.values.get + + // Track all values with the inline option set, so we can restore them later. + let restorableInlineOptions = new Set<{ options: ThemeOptions }>() + + // @ts-expect-error We are monkey-patching a method that's considered private + // in TypeScript + designSystem.theme.values.get = (key: string) => { + // @ts-expect-error We are monkey-patching a method that's considered private + // in TypeScript + let value = originalGet.call(designSystem.theme.values, key) + if (value === undefined) return value + + // Remove `inline` if it was set + if (value.options & ThemeOptions.INLINE) { + restorableInlineOptions.add(value) + value.options &= ~ThemeOptions.INLINE + } + + return value + } + + try { + // Run the callback with the `@theme inline` feature disabled + return cb() + } finally { + // Restore the `@theme inline` to the original value + // @ts-expect-error We are monkey-patching a method that's private + designSystem.theme.values.get = originalGet + + // Re-add the `inline` option, in case future lookups are done + for (let value of restorableInlineOptions) { + value.options |= ThemeOptions.INLINE + } + } +} + // Generator that generates all combinations of the given set. Using a generator // so we can stop early when we found a suitable combination. // diff --git a/packages/tailwindcss/src/expand-declaration.ts b/packages/tailwindcss/src/expand-declaration.ts index 1dc5df6f2e23..628848ab4aa0 100644 --- a/packages/tailwindcss/src/expand-declaration.ts +++ b/packages/tailwindcss/src/expand-declaration.ts @@ -1,5 +1,5 @@ import { decl, type AstNode } from './ast' -import { SignatureFeatures } from './signatures' +import { SignatureFeatures } from './canonicalize-candidates' import { segment } from './utils/segment' function createPrefixedQuad( diff --git a/packages/tailwindcss/src/signatures.ts b/packages/tailwindcss/src/signatures.ts deleted file mode 100644 index 7ad608f22cb4..000000000000 --- a/packages/tailwindcss/src/signatures.ts +++ /dev/null @@ -1,561 +0,0 @@ -import { substituteAtApply } from './apply' -import { atRule, cloneAstNode, styleRule, toCss, type AstNode } from './ast' -import { printArbitraryValue } from './candidate' -import { constantFoldDeclaration } from './constant-fold-declaration' -import { CompileAstFlags, type DesignSystem } from './design-system' -import { expandDeclaration } from './expand-declaration' -import * as SelectorParser from './selector-parser' -import { ThemeOptions } from './theme' -import { DefaultMap } from './utils/default-map' -import { isValidSpacingMultiplier } from './utils/infer-data-type' -import * as ValueParser from './value-parser' -import { walk, WalkAction } from './walk' - -export enum SignatureFeatures { - None = 0, - ExpandProperties = 1 << 0, - LogicalToPhysical = 1 << 1, -} - -export interface SignatureOptions { - /** - * The root font size in pixels. If provided, `rem` values will be normalized - * to `px` values. - * - * E.g.: `mt-[16px]` with `rem: 16` will become `mt-4` (assuming `--spacing: 0.25rem`). - */ - rem: number | null - - /** - * Features that influence how signatures are computed. - */ - features: SignatureFeatures - - /** - * The design system to use for computing the signature of candidates. - */ - designSystem: DesignSystem -} - -// Given a utility, compute a signature that represents the utility. The -// signature will be a normalised form of the generated CSS for the utility, or -// a unique symbol if the utility is not valid. The class in the selector will -// be replaced with the `.x` selector. -// -// This function should only be passed the base utility so `flex`, `hover:flex` -// and `focus:flex` will all use just `flex`. Variants are handled separately. -// -// E.g.: -// -// | UTILITY | GENERATED SIGNATURE | -// | ---------------- | ----------------------- | -// | `[display:flex]` | `.x { display: flex; }` | -// | `flex` | `.x { display: flex; }` | -// -// These produce the same signature, therefore they represent the same utility. -export const computeUtilitySignature = new DefaultMap((options: SignatureOptions) => { - let designSystem = options.designSystem - - return new DefaultMap((utility) => { - try { - // Ensure the prefix is added to the utility if it is not already present. - utility = - designSystem.theme.prefix && !utility.startsWith(designSystem.theme.prefix) - ? `${designSystem.theme.prefix}:${utility}` - : utility - - // Use `@apply` to normalize the selector to `.x` - let ast: AstNode[] = [styleRule('.x', [atRule('@apply', utility)])] - - temporarilyDisableThemeInline(designSystem, () => { - // There's separate utility caches for respect important vs not - // so we want to compile them both with `@theme inline` disabled - for (let candidate of designSystem.parseCandidate(utility)) { - designSystem.compileAstNodes(candidate, CompileAstFlags.RespectImportant) - } - - substituteAtApply(ast, designSystem) - }) - - // Optimize the AST. This is needed such that any internal intermediate - // nodes are gone. This will also cleanup declaration nodes with undefined - // values or `--tw-sort` declarations. - canonicalizeAst(ast, options) - - // Compute the final signature, by generating the CSS for the utility - let signature = toCss(ast) - return signature - } catch { - // A unique symbol is returned to ensure that 2 signatures resulting in - // `null` are not considered equal. - return Symbol() - } - }) -}) - -// Optimize the CSS AST to make it suitable for signature comparison. We want to -// expand declarations, ignore comments, sort declarations etc... -function canonicalizeAst(ast: AstNode[], options: SignatureOptions) { - let { rem, designSystem } = options - - walk(ast, { - enter(node, ctx) { - // Optimize declarations - if (node.kind === 'declaration') { - if (node.value === undefined || node.property === '--tw-sort') { - return WalkAction.Replace([]) - } - - // Ignore `--tw-{property}` if `{property}` exists with the same value - if (node.property.startsWith('--tw-')) { - if ( - (ctx.parent?.nodes ?? []).some( - (sibling) => - sibling.kind === 'declaration' && - node.value === sibling.value && - node.important === sibling.important && - !sibling.property.startsWith('--tw-'), - ) - ) { - return WalkAction.Replace([]) - } - } - - if (options.features & SignatureFeatures.ExpandProperties) { - let replacement = expandDeclaration(node, options.features) - if (replacement) return WalkAction.Replace(replacement) - } - - // Resolve theme values to their inlined value. - if (node.value.includes('var(')) { - node.value = resolveVariablesInValue(node.value, designSystem) - } - - // Very basic `calc(…)` constant folding to handle the spacing scale - // multiplier: - // - // Input: `--spacing(4)` - // → `calc(var(--spacing, 0.25rem) * 4)` - // → `calc(0.25rem * 4)` ← this is the case we will see - // after inlining the variable - // → `1rem` - node.value = constantFoldDeclaration(node.value, rem) - - // We will normalize the `node.value`, this is the same kind of logic - // we use when printing arbitrary values. It will remove unnecessary - // whitespace. - // - // Essentially normalizing the `node.value` to a canonical form. - node.value = printArbitraryValue(node.value) - } - - // Replace special nodes with its children - else if (node.kind === 'context' || node.kind === 'at-root') { - return WalkAction.Replace(node.nodes) - } - - // Remove comments - else if (node.kind === 'comment') { - return WalkAction.Replace([]) - } - - // Remove at-rules that are not needed for the signature - else if (node.kind === 'at-rule' && node.name === '@property') { - return WalkAction.Replace([]) - } - }, - exit(node) { - if (node.kind === 'rule' || node.kind === 'at-rule') { - node.nodes.sort((a, b) => { - if (a.kind !== 'declaration') return 0 - if (b.kind !== 'declaration') return 0 - return a.property.localeCompare(b.property) - }) - } - }, - }) - - return ast -} - -// Resolve theme values to their inlined value. -// -// E.g.: -// -// `[color:var(--color-red-500)]` → `[color:oklch(63.7%_0.237_25.331)]` -// `[color:oklch(63.7%_0.237_25.331)]` → `[color:oklch(63.7%_0.237_25.331)]` -// -// Due to the `@apply` from above, this will become: -// -// ```css -// .example { -// color: oklch(63.7% 0.237 25.331); -// } -// ``` -// -// Which conveniently will be equivalent to: `text-red-500` when we inline -// the value. -// -// Without inlining: -// ```css -// .example { -// color: var(--color-red-500, oklch(63.7% 0.237 25.331)); -// } -// ``` -// -// Inlined: -// ```css -// .example { -// color: oklch(63.7% 0.237 25.331); -// } -// ``` -// -// Recently we made sure that utilities like `text-red-500` also generate -// the fallback value for usage in `@reference` mode. -// -// The second assumption is that if you use `var(--key, fallback)` that -// happens to match a known variable _and_ its inlined value. Then we can -// replace it with the inlined variable. This allows us to handle custom -// `@theme` and `@theme inline` definitions. -function resolveVariablesInValue(value: string, designSystem: DesignSystem): string { - let changed = false - let valueAst = ValueParser.parse(value) - - let seen = new Set() - walk(valueAst, (valueNode) => { - if (valueNode.kind !== 'function') return - if (valueNode.value !== 'var') return - - // Resolve the underlying value of the variable - if (valueNode.nodes.length !== 1 && valueNode.nodes.length < 3) { - return - } - - let variable = valueNode.nodes[0].value - - // Drop the prefix from the variable name if it is present. The - // internal variable doesn't have the prefix. - if (designSystem.theme.prefix && variable.startsWith(`--${designSystem.theme.prefix}-`)) { - variable = variable.slice(`--${designSystem.theme.prefix}-`.length) - } - let variableValue = designSystem.resolveThemeValue(variable) - // Prevent infinite recursion when the variable value contains the - // variable itself. - if (seen.has(variable)) return - seen.add(variable) - if (variableValue === undefined) return // Couldn't resolve the variable - - // Inject variable fallbacks when no fallback is present yet. - // - // A fallback could consist of multiple values. - // - // E.g.: - // - // ``` - // var(--font-sans, ui-sans-serif, system-ui, sans-serif, …) - // ``` - { - // More than 1 argument means that a fallback is already present - if (valueNode.nodes.length === 1) { - // Inject the fallback value into the variable lookup - changed = true - valueNode.nodes.push(...ValueParser.parse(`,${variableValue}`)) - } - } - - // Replace known variable + inlined fallback value with the value - // itself again - { - // We need at least 3 arguments. The variable, the separator and a fallback value. - if (valueNode.nodes.length >= 3) { - let nodeAsString = ValueParser.toCss(valueNode.nodes) // This could include more than just the variable - let constructedValue = `${valueNode.nodes[0].value},${variableValue}` - if (nodeAsString === constructedValue) { - changed = true - return WalkAction.Replace(ValueParser.parse(variableValue)) - } - } - } - }) - - // Replace the value with the new value - if (changed) return ValueParser.toCss(valueAst) - return value -} - -// Index all static utilities by property and value -export const staticUtilitiesByPropertyAndValue = new DefaultMap((_optiones: SignatureOptions) => { - return new DefaultMap((_property: string) => { - return new DefaultMap((_value: string) => { - return new Set() - }) - }) -}) - -export const computeUtilityProperties = new DefaultMap((options: SignatureOptions) => { - return new DefaultMap((className) => { - let localPropertyValueLookup = new DefaultMap((_property) => new Set()) - let designSystem = options.designSystem - - if ( - options.designSystem.theme.prefix && - !className.startsWith(options.designSystem.theme.prefix) - ) { - className = `${options.designSystem.theme.prefix}:${className}` - } - let parsed = designSystem.parseCandidate(className) - if (parsed.length === 0) return localPropertyValueLookup - - walk( - canonicalizeAst( - designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)), - options, - ), - (node) => { - if (node.kind === 'declaration') { - localPropertyValueLookup.get(node.property).add(node.value!) - staticUtilitiesByPropertyAndValue - .get(options) - .get(node.property) - .get(node.value!) - .add(className) - } - }, - ) - - return localPropertyValueLookup - }) -}) - -// For all static utilities in the system, compute a lookup table that maps the -// utility signature to the utility name. This is used to find the utility name -// for a given utility signature. -// -// For all functional utilities, we can compute static-like utilities by -// essentially pre-computing the values and modifiers. This is a bit slow, but -// also only has to happen once per design system. -export const preComputedUtilities = new DefaultMap((options: SignatureOptions) => { - let { designSystem } = options - let signatures = computeUtilitySignature.get(options) - let lookup = new DefaultMap(() => []) - - // Right now all plugins are implemented using functions so they are a black - // box. Let's use the `getClassList` and consider every known suggestion as a - // static utility for now. - for (let [className, meta] of designSystem.getClassList()) { - let signature = signatures.get(className) - if (typeof signature !== 'string') continue - - // Skip the utility if `-{utility}-0` has the same signature as - // `{utility}-0` (its positive version). This will prefer positive values - // over negative values. - if (className[0] === '-' && className.endsWith('-0')) { - let positiveSignature = signatures.get(className.slice(1)) - if (typeof positiveSignature === 'string' && signature === positiveSignature) { - continue - } - } - - lookup.get(signature).push(className) - computeUtilityProperties.get(options).get(className) - - for (let modifier of meta.modifiers) { - // Modifiers representing numbers can be computed and don't need to be - // pre-computed. Doing the math and at the time of writing this, this - // would save you 250k additionally pre-computed utilities... - if (isValidSpacingMultiplier(modifier)) { - continue - } - - let classNameWithModifier = `${className}/${modifier}` - let signature = signatures.get(classNameWithModifier) - if (typeof signature !== 'string') continue - lookup.get(signature).push(classNameWithModifier) - computeUtilityProperties.get(options).get(classNameWithModifier) - } - } - - return lookup -}) - -// Given a variant, compute a signature that represents the variant. The -// signature will be a normalised form of the generated CSS for the variant, or -// a unique symbol if the variant is not valid. The class in the selector will -// be replaced with `.x`. -// -// E.g.: -// -// | VARIANT | GENERATED SIGNATURE | -// | ---------------- | ----------------------------- | -// | `[&:focus]:flex` | `.x:focus { display: flex; }` | -// | `focus:flex` | `.x:focus { display: flex; }` | -// -// These produce the same signature, therefore they represent the same variant. -export const computeVariantSignature = new DefaultMap((options: SignatureOptions) => { - let { designSystem } = options - return new DefaultMap((variant) => { - try { - // Ensure the prefix is added to the utility if it is not already present. - variant = - designSystem.theme.prefix && !variant.startsWith(designSystem.theme.prefix) - ? `${designSystem.theme.prefix}:${variant}` - : variant - - // Use `@apply` to normalize the selector to `.x` - let ast: AstNode[] = [styleRule('.x', [atRule('@apply', `${variant}:flex`)])] - substituteAtApply(ast, designSystem) - - // Canonicalize selectors to their minimal form - walk(ast, (node) => { - // At-rules - if (node.kind === 'at-rule' && node.params.includes(' ')) { - node.params = node.params.replaceAll(' ', '') - } - - // Style rules - else if (node.kind === 'rule') { - let selectorAst = SelectorParser.parse(node.selector) - let changed = false - walk(selectorAst, (node) => { - if (node.kind === 'separator' && node.value !== ' ') { - node.value = node.value.trim() - changed = true - } - - // Remove unnecessary `:is(…)` selectors - else if (node.kind === 'function' && node.value === ':is') { - // A single selector inside of `:is(…)` can be replaced with the - // selector itself. - // - // E.g.: `:is(.foo)` → `.foo` - if (node.nodes.length === 1) { - changed = true - return WalkAction.Replace(node.nodes) - } - - // A selector with the universal selector `*` followed by a pseudo - // class, can be replaced with the pseudo class itself. - else if ( - node.nodes.length === 2 && - node.nodes[0].kind === 'selector' && - node.nodes[0].value === '*' && - node.nodes[1].kind === 'selector' && - node.nodes[1].value[0] === ':' - ) { - changed = true - return WalkAction.Replace(node.nodes[1]) - } - } - - // Ensure `*` exists before pseudo selectors inside of `:not(…)`, - // `:where(…)`, … - // - // E.g.: - // - // `:not(:first-child)` → `:not(*:first-child)` - // - else if ( - node.kind === 'function' && - node.value[0] === ':' && - node.nodes[0]?.kind === 'selector' && - node.nodes[0]?.value[0] === ':' - ) { - changed = true - node.nodes.unshift({ kind: 'selector', value: '*' }) - } - }) - - if (changed) { - node.selector = SelectorParser.toCss(selectorAst) - } - } - }) - - // Compute the final signature, by generating the CSS for the variant - let signature = toCss(ast) - return signature - } catch { - // A unique symbol is returned to ensure that 2 signatures resulting in - // `null` are not considered equal. - return Symbol() - } - }) -}) - -export const preComputedVariants = new DefaultMap((options: SignatureOptions) => { - let { designSystem } = options - let signatures = computeVariantSignature.get(options) - let lookup = new DefaultMap(() => []) - - // Actual static variants - for (let [root, variant] of designSystem.variants.entries()) { - if (variant.kind === 'static') { - let signature = signatures.get(root) - if (typeof signature !== 'string') continue - lookup.get(signature).push(root) - } - } - - return lookup -}) - -function temporarilyDisableThemeInline(designSystem: DesignSystem, cb: () => T): T { - // Turn off `@theme inline` feature such that `@theme` and `@theme inline` are - // considered the same. The biggest motivation for this is referencing - // variables in another namespace that happen to contain the same value as the - // utility's own namespaces it is reading from. - // - // E.g.: - // - // The `max-w-*` utility doesn't read from the `--breakpoint-*` namespace. - // But it does read from the `--container-*` namespace. It also happens to - // be the case that `--breakpoint-md` and `--container-3xl` are the exact - // same value. - // - // If you then use the `max-w-(--breakpoint-md)` utility, inlining the - // variable would mean: - // - `max-w-(--breakpoint-md)` → `max-width: 48rem;` → `max-w-3xl` - // - `max-w-(--contianer-3xl)` → `max-width: 48rem;` → `max-w-3xl` - // - // Not inlining the variable would mean: - // - `max-w-(--breakpoint-md)` → `max-width: var(--breakpoint-md);` → `max-w-(--breakpoint-md)` - // - `max-w-(--container-3xl)` → `max-width: var(--container-3xl);` → `max-w-3xl` - - // @ts-expect-error We are monkey-patching a method that's considered private - // in TypeScript - let originalGet = designSystem.theme.values.get - - // Track all values with the inline option set, so we can restore them later. - let restorableInlineOptions = new Set<{ options: ThemeOptions }>() - - // @ts-expect-error We are monkey-patching a method that's considered private - // in TypeScript - designSystem.theme.values.get = (key: string) => { - // @ts-expect-error We are monkey-patching a method that's considered private - // in TypeScript - let value = originalGet.call(designSystem.theme.values, key) - if (value === undefined) return value - - // Remove `inline` if it was set - if (value.options & ThemeOptions.INLINE) { - restorableInlineOptions.add(value) - value.options &= ~ThemeOptions.INLINE - } - - return value - } - - try { - // Run the callback with the `@theme inline` feature disabled - return cb() - } finally { - // Restore the `@theme inline` to the original value - // @ts-expect-error We are monkey-patching a method that's private - designSystem.theme.values.get = originalGet - - // Re-add the `inline` option, in case future lookups are done - for (let value of restorableInlineOptions) { - value.options |= ThemeOptions.INLINE - } - } -} From d6cf6c3dce408de82724be9afb05265d616f5fa5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 14:33:50 +0200 Subject: [PATCH 11/19] use `designSystem` as top-level DefaultMap key Instead of nesting the `designSystem` as part of the nested signature options object, we move the design system up. This commit is just here to make the easy change later. We want to store all caches on the design system itself, so by hoisting it up, we can make a nicer change in the near future. --- .../src/canonicalize-candidates.ts | 272 +++++++++--------- 1 file changed, 136 insertions(+), 136 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 47200944b3a9..df2888f44962 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -98,7 +98,7 @@ export interface DesignSystem extends BaseDesignSystem { function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignSystem { let designSystem = baseDesignSystem as DesignSystem - designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache(designSystem) + designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache() designSystem.storage[INTERNAL_OPTIONS_KEY] ??= createInternalOptionsCache(designSystem) designSystem.storage[CANONICALIZE_CANDIDATE_KEY] ??= createCanonicalizeCandidateCache() designSystem.storage[CANONICALIZE_VARIANT_KEY] ??= createCanonicalizeVariantCache() @@ -110,12 +110,10 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS } const SIGNATURE_OPTIONS_KEY = Symbol() -function createSignatureOptionsCache( - designSystem: DesignSystem, -): DesignSystem['storage'][typeof SIGNATURE_OPTIONS_KEY] { +function createSignatureOptionsCache(): DesignSystem['storage'][typeof SIGNATURE_OPTIONS_KEY] { return new DefaultMap((rem: number | null) => { return new DefaultMap((features: SignatureFeatures) => { - return { designSystem, rem, features } satisfies SignatureOptions + return { rem, features } satisfies SignatureOptions }) }) } @@ -180,6 +178,7 @@ export function canonicalizeCandidates( function collapseCandidates(options: InternalCanonicalizeOptions, candidates: string[]): string[] { if (candidates.length <= 1) return candidates + let designSystem = options.designSystem // To keep things simple, we group candidates such that we only collapse // candidates with the same variants and important modifier together. @@ -226,7 +225,9 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st function collapseGroup(candidates: string[]) { let signatureOptions = options.signatureOptions - let computeUtilitiesPropertiesLookup = computeUtilityProperties.get(signatureOptions) + let computeUtilitiesPropertiesLookup = computeUtilityProperties + .get(designSystem) + .get(signatureOptions) let staticUtilities = staticUtilitiesByPropertyAndValue.get(signatureOptions) // For each candidate, compute the used properties and values. E.g.: `mt-1` → `margin-top` → `0.25rem` @@ -309,15 +310,21 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st let potentialReplacements = combo.flatMap((idx) => otherUtilities[idx]).reduce(intersection) - let collapsedSignature = computeUtilitySignature.get(signatureOptions).get( - combo - .map((idx) => candidates[idx]) - .sort((a, z) => a.localeCompare(z)) // Sort to increase cache hits - .join(' '), - ) + let collapsedSignature = computeUtilitySignature + .get(designSystem) + .get(signatureOptions) + .get( + combo + .map((idx) => candidates[idx]) + .sort((a, z) => a.localeCompare(z)) // Sort to increase cache hits + .join(' '), + ) for (let replacement of potentialReplacements) { - let signature = computeUtilitySignature.get(signatureOptions).get(replacement) + let signature = computeUtilitySignature + .get(designSystem) + .get(signatureOptions) + .get(replacement) if (signature !== collapsedSignature) continue // Not a safe replacement // We can replace all items in the combo with the replacement @@ -892,8 +899,8 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO } let designSystem = options.designSystem - let utilities = preComputedUtilities.get(options.signatureOptions) - let signatures = computeUtilitySignature.get(options.signatureOptions) + let utilities = preComputedUtilities.get(designSystem).get(options.signatureOptions) + let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) let targetCandidateString = designSystem.printCandidate(candidate) @@ -1104,8 +1111,8 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO } let designSystem = options.designSystem - let utilities = preComputedUtilities.get(options.signatureOptions) - let signatures = computeUtilitySignature.get(options.signatureOptions) + let utilities = preComputedUtilities.get(designSystem).get(options.signatureOptions) + let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) let targetCandidateString = designSystem.printCandidate(candidate) @@ -1182,7 +1189,7 @@ function deprecatedUtilities( options: InternalCanonicalizeOptions, ): Candidate { let designSystem = options.designSystem - let signatures = computeUtilitySignature.get(options.signatureOptions) + let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) let targetCandidateString = printUnprefixedCandidate(designSystem, candidate) @@ -1209,8 +1216,8 @@ function arbitraryVariants( options: InternalCanonicalizeOptions, ): Variant | Variant[] { let designSystem = options.designSystem - let signatures = computeVariantSignature.get(options.signatureOptions) - let variants = preComputedVariants.get(options.signatureOptions) + let signatures = computeVariantSignature.get(designSystem) + let variants = preComputedVariants.get(designSystem) let iterator = walkVariants(variant) for (let [variant] of iterator) { @@ -1240,7 +1247,7 @@ function dropUnnecessaryDataTypes( options: InternalCanonicalizeOptions, ): Candidate { let designSystem = options.designSystem - let signatures = computeUtilitySignature.get(options.signatureOptions) + let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) if ( candidate.kind === 'functional' && @@ -1272,7 +1279,7 @@ function arbitraryValueToBareValueUtility( } let designSystem = options.designSystem - let signatures = computeUtilitySignature.get(options.signatureOptions) + let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) let expectedSignature = signatures.get(designSystem.printCandidate(candidate)) if (expectedSignature === null) return candidate @@ -1441,7 +1448,7 @@ function modernizeArbitraryValuesVariant( ): Variant | Variant[] { let result = [variant] let designSystem = options.designSystem - let signatures = computeVariantSignature.get(options.signatureOptions) + let signatures = computeVariantSignature.get(designSystem) let iterator = walkVariants(variant) for (let [variant, parent] of iterator) { @@ -1806,7 +1813,7 @@ function optimizeModifier(candidate: Candidate, options: InternalCanonicalizeOpt } let designSystem = options.designSystem - let signatures = computeUtilitySignature.get(options.signatureOptions) + let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) let targetSignature = signatures.get(designSystem.printCandidate(candidate)) let modifier = candidate.modifier @@ -1880,11 +1887,6 @@ export interface SignatureOptions { * Features that influence how signatures are computed. */ features: SignatureFeatures - - /** - * The design system to use for computing the signature of candidates. - */ - designSystem: DesignSystem } // Given a utility, compute a signature that represents the utility. The @@ -1903,50 +1905,50 @@ export interface SignatureOptions { // | `flex` | `.x { display: flex; }` | // // These produce the same signature, therefore they represent the same utility. -export const computeUtilitySignature = new DefaultMap((options: SignatureOptions) => { - let designSystem = options.designSystem - - return new DefaultMap((utility) => { - try { - // Ensure the prefix is added to the utility if it is not already present. - utility = - designSystem.theme.prefix && !utility.startsWith(designSystem.theme.prefix) - ? `${designSystem.theme.prefix}:${utility}` - : utility - - // Use `@apply` to normalize the selector to `.x` - let ast: AstNode[] = [styleRule('.x', [atRule('@apply', utility)])] - - temporarilyDisableThemeInline(designSystem, () => { - // There's separate utility caches for respect important vs not - // so we want to compile them both with `@theme inline` disabled - for (let candidate of designSystem.parseCandidate(utility)) { - designSystem.compileAstNodes(candidate, CompileAstFlags.RespectImportant) - } - - substituteAtApply(ast, designSystem) - }) +export const computeUtilitySignature = new DefaultMap((designSystem: DesignSystem) => { + return new DefaultMap((options: SignatureOptions) => { + return new DefaultMap((utility) => { + try { + // Ensure the prefix is added to the utility if it is not already present. + utility = + designSystem.theme.prefix && !utility.startsWith(designSystem.theme.prefix) + ? `${designSystem.theme.prefix}:${utility}` + : utility + + // Use `@apply` to normalize the selector to `.x` + let ast: AstNode[] = [styleRule('.x', [atRule('@apply', utility)])] + + temporarilyDisableThemeInline(designSystem, () => { + // There's separate utility caches for respect important vs not + // so we want to compile them both with `@theme inline` disabled + for (let candidate of designSystem.parseCandidate(utility)) { + designSystem.compileAstNodes(candidate, CompileAstFlags.RespectImportant) + } - // Optimize the AST. This is needed such that any internal intermediate - // nodes are gone. This will also cleanup declaration nodes with undefined - // values or `--tw-sort` declarations. - canonicalizeAst(ast, options) + substituteAtApply(ast, designSystem) + }) - // Compute the final signature, by generating the CSS for the utility - let signature = toCss(ast) - return signature - } catch { - // A unique symbol is returned to ensure that 2 signatures resulting in - // `null` are not considered equal. - return Symbol() - } + // Optimize the AST. This is needed such that any internal intermediate + // nodes are gone. This will also cleanup declaration nodes with undefined + // values or `--tw-sort` declarations. + canonicalizeAst(designSystem, ast, options) + + // Compute the final signature, by generating the CSS for the utility + let signature = toCss(ast) + return signature + } catch { + // A unique symbol is returned to ensure that 2 signatures resulting in + // `null` are not considered equal. + return Symbol() + } + }) }) }) // Optimize the CSS AST to make it suitable for signature comparison. We want to // expand declarations, ignore comments, sort declarations etc... -function canonicalizeAst(ast: AstNode[], options: SignatureOptions) { - let { rem, designSystem } = options +function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: SignatureOptions) { + let { rem } = options walk(ast, { enter(node, ctx) { @@ -2142,38 +2144,37 @@ export const staticUtilitiesByPropertyAndValue = new DefaultMap((_optiones: Sign }) }) -export const computeUtilityProperties = new DefaultMap((options: SignatureOptions) => { - return new DefaultMap((className) => { - let localPropertyValueLookup = new DefaultMap((_property) => new Set()) - let designSystem = options.designSystem +export const computeUtilityProperties = new DefaultMap((designSystem: DesignSystem) => { + return new DefaultMap((options: SignatureOptions) => { + return new DefaultMap((className) => { + let localPropertyValueLookup = new DefaultMap((_property) => new Set()) - if ( - options.designSystem.theme.prefix && - !className.startsWith(options.designSystem.theme.prefix) - ) { - className = `${options.designSystem.theme.prefix}:${className}` - } - let parsed = designSystem.parseCandidate(className) - if (parsed.length === 0) return localPropertyValueLookup - - walk( - canonicalizeAst( - designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)), - options, - ), - (node) => { - if (node.kind === 'declaration') { - localPropertyValueLookup.get(node.property).add(node.value!) - staticUtilitiesByPropertyAndValue - .get(options) - .get(node.property) - .get(node.value!) - .add(className) - } - }, - ) + if (designSystem.theme.prefix && !className.startsWith(designSystem.theme.prefix)) { + className = `${designSystem.theme.prefix}:${className}` + } + let parsed = designSystem.parseCandidate(className) + if (parsed.length === 0) return localPropertyValueLookup + + walk( + canonicalizeAst( + designSystem, + designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)), + options, + ), + (node) => { + if (node.kind === 'declaration') { + localPropertyValueLookup.get(node.property).add(node.value!) + staticUtilitiesByPropertyAndValue + .get(options) + .get(node.property) + .get(node.value!) + .add(className) + } + }, + ) - return localPropertyValueLookup + return localPropertyValueLookup + }) }) }) @@ -2184,48 +2185,49 @@ export const computeUtilityProperties = new DefaultMap((options: SignatureOption // For all functional utilities, we can compute static-like utilities by // essentially pre-computing the values and modifiers. This is a bit slow, but // also only has to happen once per design system. -export const preComputedUtilities = new DefaultMap((options: SignatureOptions) => { - let { designSystem } = options - let signatures = computeUtilitySignature.get(options) - let lookup = new DefaultMap(() => []) +export const preComputedUtilities = new DefaultMap((designSystem: DesignSystem) => { + return new DefaultMap((options: SignatureOptions) => { + let signatures = computeUtilitySignature.get(designSystem).get(options) + let lookup = new DefaultMap(() => []) + + // Right now all plugins are implemented using functions so they are a black + // box. Let's use the `getClassList` and consider every known suggestion as a + // static utility for now. + for (let [className, meta] of designSystem.getClassList()) { + let signature = signatures.get(className) + if (typeof signature !== 'string') continue - // Right now all plugins are implemented using functions so they are a black - // box. Let's use the `getClassList` and consider every known suggestion as a - // static utility for now. - for (let [className, meta] of designSystem.getClassList()) { - let signature = signatures.get(className) - if (typeof signature !== 'string') continue - - // Skip the utility if `-{utility}-0` has the same signature as - // `{utility}-0` (its positive version). This will prefer positive values - // over negative values. - if (className[0] === '-' && className.endsWith('-0')) { - let positiveSignature = signatures.get(className.slice(1)) - if (typeof positiveSignature === 'string' && signature === positiveSignature) { - continue + // Skip the utility if `-{utility}-0` has the same signature as + // `{utility}-0` (its positive version). This will prefer positive values + // over negative values. + if (className[0] === '-' && className.endsWith('-0')) { + let positiveSignature = signatures.get(className.slice(1)) + if (typeof positiveSignature === 'string' && signature === positiveSignature) { + continue + } } - } - lookup.get(signature).push(className) - computeUtilityProperties.get(options).get(className) + lookup.get(signature).push(className) + computeUtilityProperties.get(designSystem).get(options).get(className) - for (let modifier of meta.modifiers) { - // Modifiers representing numbers can be computed and don't need to be - // pre-computed. Doing the math and at the time of writing this, this - // would save you 250k additionally pre-computed utilities... - if (isValidSpacingMultiplier(modifier)) { - continue - } + for (let modifier of meta.modifiers) { + // Modifiers representing numbers can be computed and don't need to be + // pre-computed. Doing the math and at the time of writing this, this + // would save you 250k additionally pre-computed utilities... + if (isValidSpacingMultiplier(modifier)) { + continue + } - let classNameWithModifier = `${className}/${modifier}` - let signature = signatures.get(classNameWithModifier) - if (typeof signature !== 'string') continue - lookup.get(signature).push(classNameWithModifier) - computeUtilityProperties.get(options).get(classNameWithModifier) + let classNameWithModifier = `${className}/${modifier}` + let signature = signatures.get(classNameWithModifier) + if (typeof signature !== 'string') continue + lookup.get(signature).push(classNameWithModifier) + computeUtilityProperties.get(designSystem).get(options).get(classNameWithModifier) + } } - } - return lookup + return lookup + }) }) // Given a variant, compute a signature that represents the variant. The @@ -2241,8 +2243,7 @@ export const preComputedUtilities = new DefaultMap((options: SignatureOptions) = // | `focus:flex` | `.x:focus { display: flex; }` | // // These produce the same signature, therefore they represent the same variant. -export const computeVariantSignature = new DefaultMap((options: SignatureOptions) => { - let { designSystem } = options +export const computeVariantSignature = new DefaultMap((designSystem: DesignSystem) => { return new DefaultMap((variant) => { try { // Ensure the prefix is added to the utility if it is not already present. @@ -2332,9 +2333,8 @@ export const computeVariantSignature = new DefaultMap((options: SignatureOptions }) }) -export const preComputedVariants = new DefaultMap((options: SignatureOptions) => { - let { designSystem } = options - let signatures = computeVariantSignature.get(options) +export const preComputedVariants = new DefaultMap((designSystem: DesignSystem) => { + let signatures = computeVariantSignature.get(designSystem) let lookup = new DefaultMap(() => []) // Actual static variants From 2336cffc74c68315f921745d40ba0536c7b0672e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 14:41:39 +0200 Subject: [PATCH 12/19] setup utilitySignatureCache --- .../src/codemods/template/migrate.ts | 9 +++- .../src/canonicalize-candidates.ts | 46 ++++++++++--------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index 6060d8020cd1..e859a210f263 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts @@ -1,5 +1,9 @@ import fs from 'node:fs/promises' import path, { extname } from 'node:path' +import { + createSignatureOptions, + prepareDesignSystemStorage, + UTILITY_SIGNATURE_KEY, import { createSignatureOptions } from '../../../../tailwindcss/src/canonicalize-candidates' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' @@ -40,7 +44,8 @@ export const DEFAULT_MIGRATIONS: Migration[] = [ migrateModernizeArbitraryValues, ] -let migrateCached = new DefaultMap((designSystem: DesignSystem) => { +let migrateCached = new DefaultMap((baseDesignSystem: DesignSystem) => { + let designSystem = prepareDesignSystemStorage(baseDesignSystem) let options = createSignatureOptions(designSystem) return new DefaultMap((userConfig: Config | null) => { @@ -57,7 +62,7 @@ let migrateCached = new DefaultMap((designSystem: DesignSystem) => { // Verify that the candidate actually makes sense at all. E.g.: `duration` // is not a valid candidate, but it will parse because `duration-` // exists. - let signature = computeUtilitySignature.get(options).get(rawCandidate) + let signature = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options).get(rawCandidate) if (typeof signature !== 'string') return original return rawCandidate diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index df2888f44962..0d11ae09908b 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -92,6 +92,7 @@ export interface DesignSystem extends BaseDesignSystem { [CANONICALIZE_UTILITY_KEY]: DefaultMap> [CONVERTER_KEY]: (input: string, options?: Convert) => [string, CandidateModifier | null] [SPACING_KEY]: DefaultMap | null + [UTILITY_SIGNATURE_KEY]: DefaultMap> } } @@ -105,6 +106,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[CANONICALIZE_UTILITY_KEY] ??= createCanonicalizeUtilityCache() designSystem.storage[CONVERTER_KEY] ??= createConverterCache(designSystem) designSystem.storage[SPACING_KEY] ??= createSpacingCache(designSystem) + designSystem.storage[UTILITY_SIGNATURE_KEY] ??= createUtilitySignatureCache(designSystem) return designSystem } @@ -310,21 +312,18 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st let potentialReplacements = combo.flatMap((idx) => otherUtilities[idx]).reduce(intersection) - let collapsedSignature = computeUtilitySignature - .get(designSystem) - .get(signatureOptions) - .get( - combo - .map((idx) => candidates[idx]) - .sort((a, z) => a.localeCompare(z)) // Sort to increase cache hits - .join(' '), - ) + let collapsedSignature = designSystem.storage[UTILITY_SIGNATURE_KEY].get( + signatureOptions, + ).get( + combo + .map((idx) => candidates[idx]) + .sort((a, z) => a.localeCompare(z)) // Sort to increase cache hits + .join(' '), + ) for (let replacement of potentialReplacements) { - let signature = computeUtilitySignature - .get(designSystem) - .get(signatureOptions) - .get(replacement) + let signature = + designSystem.storage[UTILITY_SIGNATURE_KEY].get(signatureOptions).get(replacement) if (signature !== collapsedSignature) continue // Not a safe replacement // We can replace all items in the combo with the replacement @@ -900,7 +899,7 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO let designSystem = options.designSystem let utilities = preComputedUtilities.get(designSystem).get(options.signatureOptions) - let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) let targetCandidateString = designSystem.printCandidate(candidate) @@ -1112,7 +1111,7 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO let designSystem = options.designSystem let utilities = preComputedUtilities.get(designSystem).get(options.signatureOptions) - let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) let targetCandidateString = designSystem.printCandidate(candidate) @@ -1189,7 +1188,7 @@ function deprecatedUtilities( options: InternalCanonicalizeOptions, ): Candidate { let designSystem = options.designSystem - let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) let targetCandidateString = printUnprefixedCandidate(designSystem, candidate) @@ -1247,7 +1246,7 @@ function dropUnnecessaryDataTypes( options: InternalCanonicalizeOptions, ): Candidate { let designSystem = options.designSystem - let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) if ( candidate.kind === 'functional' && @@ -1279,7 +1278,7 @@ function arbitraryValueToBareValueUtility( } let designSystem = options.designSystem - let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) let expectedSignature = signatures.get(designSystem.printCandidate(candidate)) if (expectedSignature === null) return candidate @@ -1813,7 +1812,7 @@ function optimizeModifier(candidate: Candidate, options: InternalCanonicalizeOpt } let designSystem = options.designSystem - let signatures = computeUtilitySignature.get(designSystem).get(options.signatureOptions) + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) let targetSignature = signatures.get(designSystem.printCandidate(candidate)) let modifier = candidate.modifier @@ -1905,7 +1904,10 @@ export interface SignatureOptions { // | `flex` | `.x { display: flex; }` | // // These produce the same signature, therefore they represent the same utility. -export const computeUtilitySignature = new DefaultMap((designSystem: DesignSystem) => { +const UTILITY_SIGNATURE_KEY = Symbol() +function createUtilitySignatureCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof UTILITY_SIGNATURE_KEY] { return new DefaultMap((options: SignatureOptions) => { return new DefaultMap((utility) => { try { @@ -1943,7 +1945,7 @@ export const computeUtilitySignature = new DefaultMap((designSystem: DesignSyste } }) }) -}) +} // Optimize the CSS AST to make it suitable for signature comparison. We want to // expand declarations, ignore comments, sort declarations etc... @@ -2187,7 +2189,7 @@ export const computeUtilityProperties = new DefaultMap((designSystem: DesignSyst // also only has to happen once per design system. export const preComputedUtilities = new DefaultMap((designSystem: DesignSystem) => { return new DefaultMap((options: SignatureOptions) => { - let signatures = computeUtilitySignature.get(designSystem).get(options) + let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options) let lookup = new DefaultMap(() => []) // Right now all plugins are implemented using functions so they are a black From 821f7eb9f7e0ccc6112063dbc9ca0f7471bcebdc Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 14:47:14 +0200 Subject: [PATCH 13/19] setup staticUtilitiesCache --- .../src/canonicalize-candidates.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 0d11ae09908b..86697da1d86a 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -93,6 +93,10 @@ export interface DesignSystem extends BaseDesignSystem { [CONVERTER_KEY]: (input: string, options?: Convert) => [string, CandidateModifier | null] [SPACING_KEY]: DefaultMap | null [UTILITY_SIGNATURE_KEY]: DefaultMap> + [STATIC_UTILITIES_KEY]: DefaultMap< + SignatureOptions, + DefaultMap>> + > } } @@ -107,6 +111,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[CONVERTER_KEY] ??= createConverterCache(designSystem) designSystem.storage[SPACING_KEY] ??= createSpacingCache(designSystem) designSystem.storage[UTILITY_SIGNATURE_KEY] ??= createUtilitySignatureCache(designSystem) + designSystem.storage[STATIC_UTILITIES_KEY] ??= createStaticUtilitiesCache() return designSystem } @@ -230,7 +235,7 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st let computeUtilitiesPropertiesLookup = computeUtilityProperties .get(designSystem) .get(signatureOptions) - let staticUtilities = staticUtilitiesByPropertyAndValue.get(signatureOptions) + let staticUtilities = designSystem.storage[STATIC_UTILITIES_KEY].get(signatureOptions) // For each candidate, compute the used properties and values. E.g.: `mt-1` → `margin-top` → `0.25rem` // @@ -2138,13 +2143,16 @@ function resolveVariablesInValue(value: string, designSystem: DesignSystem): str } // Index all static utilities by property and value -export const staticUtilitiesByPropertyAndValue = new DefaultMap((_optiones: SignatureOptions) => { - return new DefaultMap((_property: string) => { - return new DefaultMap((_value: string) => { - return new Set() +const STATIC_UTILITIES_KEY = Symbol() +function createStaticUtilitiesCache(): DesignSystem['storage'][typeof STATIC_UTILITIES_KEY] { + return new DefaultMap((_optiones: SignatureOptions) => { + return new DefaultMap((_property: string) => { + return new DefaultMap((_value: string) => { + return new Set() + }) }) }) -}) +} export const computeUtilityProperties = new DefaultMap((designSystem: DesignSystem) => { return new DefaultMap((options: SignatureOptions) => { @@ -2166,8 +2174,7 @@ export const computeUtilityProperties = new DefaultMap((designSystem: DesignSyst (node) => { if (node.kind === 'declaration') { localPropertyValueLookup.get(node.property).add(node.value!) - staticUtilitiesByPropertyAndValue - .get(options) + designSystem.storage[STATIC_UTILITIES_KEY].get(options) .get(node.property) .get(node.value!) .add(className) From a75c615852858e8a622a39802e92a0b29180b73b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 14:50:42 +0200 Subject: [PATCH 14/19] setup utilityPropertiesCache --- .../src/canonicalize-candidates.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 86697da1d86a..282a9fbbd707 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -97,6 +97,10 @@ export interface DesignSystem extends BaseDesignSystem { SignatureOptions, DefaultMap>> > + [UTILITY_PROPERTIES_KEY]: DefaultMap< + SignatureOptions, + DefaultMap>> + > } } @@ -112,6 +116,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[SPACING_KEY] ??= createSpacingCache(designSystem) designSystem.storage[UTILITY_SIGNATURE_KEY] ??= createUtilitySignatureCache(designSystem) designSystem.storage[STATIC_UTILITIES_KEY] ??= createStaticUtilitiesCache() + designSystem.storage[UTILITY_PROPERTIES_KEY] ??= createUtilityPropertiesCache(designSystem) return designSystem } @@ -232,9 +237,8 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st function collapseGroup(candidates: string[]) { let signatureOptions = options.signatureOptions - let computeUtilitiesPropertiesLookup = computeUtilityProperties - .get(designSystem) - .get(signatureOptions) + let computeUtilitiesPropertiesLookup = + designSystem.storage[UTILITY_PROPERTIES_KEY].get(signatureOptions) let staticUtilities = designSystem.storage[STATIC_UTILITIES_KEY].get(signatureOptions) // For each candidate, compute the used properties and values. E.g.: `mt-1` → `margin-top` → `0.25rem` @@ -2154,7 +2158,10 @@ function createStaticUtilitiesCache(): DesignSystem['storage'][typeof STATIC_UTI }) } -export const computeUtilityProperties = new DefaultMap((designSystem: DesignSystem) => { +const UTILITY_PROPERTIES_KEY = Symbol() +function createUtilityPropertiesCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof UTILITY_PROPERTIES_KEY] { return new DefaultMap((options: SignatureOptions) => { return new DefaultMap((className) => { let localPropertyValueLookup = new DefaultMap((_property) => new Set()) @@ -2185,7 +2192,7 @@ export const computeUtilityProperties = new DefaultMap((designSystem: DesignSyst return localPropertyValueLookup }) }) -}) +} // For all static utilities in the system, compute a lookup table that maps the // utility signature to the utility name. This is used to find the utility name @@ -2217,7 +2224,7 @@ export const preComputedUtilities = new DefaultMap((designSystem: DesignSystem) } lookup.get(signature).push(className) - computeUtilityProperties.get(designSystem).get(options).get(className) + designSystem.storage[UTILITY_PROPERTIES_KEY].get(options).get(className) for (let modifier of meta.modifiers) { // Modifiers representing numbers can be computed and don't need to be @@ -2231,7 +2238,7 @@ export const preComputedUtilities = new DefaultMap((designSystem: DesignSystem) let signature = signatures.get(classNameWithModifier) if (typeof signature !== 'string') continue lookup.get(signature).push(classNameWithModifier) - computeUtilityProperties.get(designSystem).get(options).get(classNameWithModifier) + designSystem.storage[UTILITY_PROPERTIES_KEY].get(options).get(classNameWithModifier) } } From bf02fb379dd1df7455340aee9742b7d483057c85 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 14:52:55 +0200 Subject: [PATCH 15/19] setup preComputedUtilitiesCache --- packages/tailwindcss/src/canonicalize-candidates.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 282a9fbbd707..85cbcb777d91 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -101,6 +101,7 @@ export interface DesignSystem extends BaseDesignSystem { SignatureOptions, DefaultMap>> > + [PRE_COMPUTED_UTILITIES_KEY]: DefaultMap> } } @@ -117,6 +118,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[UTILITY_SIGNATURE_KEY] ??= createUtilitySignatureCache(designSystem) designSystem.storage[STATIC_UTILITIES_KEY] ??= createStaticUtilitiesCache() designSystem.storage[UTILITY_PROPERTIES_KEY] ??= createUtilityPropertiesCache(designSystem) + designSystem.storage[PRE_COMPUTED_UTILITIES_KEY] ??= createPreComputedUtilitiesCache(designSystem) return designSystem } @@ -907,7 +909,7 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO } let designSystem = options.designSystem - let utilities = preComputedUtilities.get(designSystem).get(options.signatureOptions) + let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions) let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) let targetCandidateString = designSystem.printCandidate(candidate) @@ -1119,7 +1121,7 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO } let designSystem = options.designSystem - let utilities = preComputedUtilities.get(designSystem).get(options.signatureOptions) + let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions) let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) let targetCandidateString = designSystem.printCandidate(candidate) @@ -2201,7 +2203,10 @@ function createUtilityPropertiesCache( // For all functional utilities, we can compute static-like utilities by // essentially pre-computing the values and modifiers. This is a bit slow, but // also only has to happen once per design system. -export const preComputedUtilities = new DefaultMap((designSystem: DesignSystem) => { +const PRE_COMPUTED_UTILITIES_KEY = Symbol() +export function createPreComputedUtilitiesCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof PRE_COMPUTED_UTILITIES_KEY] { return new DefaultMap((options: SignatureOptions) => { let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options) let lookup = new DefaultMap(() => []) @@ -2244,7 +2249,7 @@ export const preComputedUtilities = new DefaultMap((designSystem: DesignSystem) return lookup }) -}) +} // Given a variant, compute a signature that represents the variant. The // signature will be a normalised form of the generated CSS for the variant, or From 5ca3f601a45f51668e12442783b8bfc43da4eb31 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 14:54:49 +0200 Subject: [PATCH 16/19] setup variantSignatureCache --- .../tailwindcss/src/canonicalize-candidates.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 85cbcb777d91..036da3b8717d 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -102,6 +102,7 @@ export interface DesignSystem extends BaseDesignSystem { DefaultMap>> > [PRE_COMPUTED_UTILITIES_KEY]: DefaultMap> + [VARIANT_SIGNATURE_KEY]: DefaultMap } } @@ -119,6 +120,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[STATIC_UTILITIES_KEY] ??= createStaticUtilitiesCache() designSystem.storage[UTILITY_PROPERTIES_KEY] ??= createUtilityPropertiesCache(designSystem) designSystem.storage[PRE_COMPUTED_UTILITIES_KEY] ??= createPreComputedUtilitiesCache(designSystem) + designSystem.storage[VARIANT_SIGNATURE_KEY] ??= createVariantSignatureCache(designSystem) return designSystem } @@ -1226,7 +1228,7 @@ function arbitraryVariants( options: InternalCanonicalizeOptions, ): Variant | Variant[] { let designSystem = options.designSystem - let signatures = computeVariantSignature.get(designSystem) + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] let variants = preComputedVariants.get(designSystem) let iterator = walkVariants(variant) @@ -1458,7 +1460,7 @@ function modernizeArbitraryValuesVariant( ): Variant | Variant[] { let result = [variant] let designSystem = options.designSystem - let signatures = computeVariantSignature.get(designSystem) + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] let iterator = walkVariants(variant) for (let [variant, parent] of iterator) { @@ -2264,7 +2266,10 @@ export function createPreComputedUtilitiesCache( // | `focus:flex` | `.x:focus { display: flex; }` | // // These produce the same signature, therefore they represent the same variant. -export const computeVariantSignature = new DefaultMap((designSystem: DesignSystem) => { +const VARIANT_SIGNATURE_KEY = Symbol() +export function createVariantSignatureCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof VARIANT_SIGNATURE_KEY] { return new DefaultMap((variant) => { try { // Ensure the prefix is added to the utility if it is not already present. @@ -2352,10 +2357,10 @@ export const computeVariantSignature = new DefaultMap((designSystem: DesignSyste return Symbol() } }) -}) +} export const preComputedVariants = new DefaultMap((designSystem: DesignSystem) => { - let signatures = computeVariantSignature.get(designSystem) + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] let lookup = new DefaultMap(() => []) // Actual static variants From 4321a4508ca911c56b2e86d4907d3eee580e0001 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 14:56:10 +0200 Subject: [PATCH 17/19] setup preComputedVariantsCache --- .../template/migrate-arbitrary-variants.ts | 18 +++++++++--------- .../tailwindcss/src/canonicalize-candidates.ts | 15 ++++++++++----- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts index c1b9669ac466..58efb0421253 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts @@ -1,23 +1,23 @@ import { cloneCandidate } from '../../../../tailwindcss/src/candidate' -import { createSignatureOptions } from '../../../../tailwindcss/src/canonicalize-candidates' +import { + PRE_COMPUTED_VARIANTS_KEY, + prepareDesignSystemStorage, + VARIANT_SIGNATURE_KEY, +} from '../../../../tailwindcss/src/canonicalize-candidates' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { - computeVariantSignature, - preComputedVariants, -} from '../../../../tailwindcss/src/signatures' import type { Writable } from '../../../../tailwindcss/src/types' import { replaceObject } from '../../../../tailwindcss/src/utils/replace-object' import { walkVariants } from '../../utils/walk-variants' export function migrateArbitraryVariants( - designSystem: DesignSystem, + baseDesignSystem: DesignSystem, _userConfig: Config | null, rawCandidate: string, ): string { - let signatureOptions = createSignatureOptions(designSystem) - let signatures = computeVariantSignature.get(signatureOptions) - let variants = preComputedVariants.get(signatureOptions) + let designSystem = prepareDesignSystemStorage(baseDesignSystem) + let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] + let variants = designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { // We are only interested in the variants diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 036da3b8717d..3cb61f1acbba 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -103,10 +103,11 @@ export interface DesignSystem extends BaseDesignSystem { > [PRE_COMPUTED_UTILITIES_KEY]: DefaultMap> [VARIANT_SIGNATURE_KEY]: DefaultMap + [PRE_COMPUTED_VARIANTS_KEY]: DefaultMap } } -function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignSystem { +export function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignSystem { let designSystem = baseDesignSystem as DesignSystem designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache() @@ -121,6 +122,7 @@ function prepareDesignSystemStorage(baseDesignSystem: BaseDesignSystem): DesignS designSystem.storage[UTILITY_PROPERTIES_KEY] ??= createUtilityPropertiesCache(designSystem) designSystem.storage[PRE_COMPUTED_UTILITIES_KEY] ??= createPreComputedUtilitiesCache(designSystem) designSystem.storage[VARIANT_SIGNATURE_KEY] ??= createVariantSignatureCache(designSystem) + designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] ??= createPreComputedVariantsCache(designSystem) return designSystem } @@ -1229,7 +1231,7 @@ function arbitraryVariants( ): Variant | Variant[] { let designSystem = options.designSystem let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] - let variants = preComputedVariants.get(designSystem) + let variants = designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] let iterator = walkVariants(variant) for (let [variant] of iterator) { @@ -2266,7 +2268,7 @@ export function createPreComputedUtilitiesCache( // | `focus:flex` | `.x:focus { display: flex; }` | // // These produce the same signature, therefore they represent the same variant. -const VARIANT_SIGNATURE_KEY = Symbol() +export const VARIANT_SIGNATURE_KEY = Symbol() export function createVariantSignatureCache( designSystem: DesignSystem, ): DesignSystem['storage'][typeof VARIANT_SIGNATURE_KEY] { @@ -2359,7 +2361,10 @@ export function createVariantSignatureCache( }) } -export const preComputedVariants = new DefaultMap((designSystem: DesignSystem) => { +export const PRE_COMPUTED_VARIANTS_KEY = Symbol() +export function createPreComputedVariantsCache( + designSystem: DesignSystem, +): DesignSystem['storage'][typeof PRE_COMPUTED_VARIANTS_KEY] { let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY] let lookup = new DefaultMap(() => []) @@ -2373,7 +2378,7 @@ export const preComputedVariants = new DefaultMap((designSystem: DesignSystem) = } return lookup -}) +} function temporarilyDisableThemeInline(designSystem: DesignSystem, cb: () => T): T { // Turn off `@theme inline` feature such that `@theme` and `@theme inline` are From 4515ae1db69c3b62a277ffe8ae4774c76f52e5ff Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 15:05:12 +0200 Subject: [PATCH 18/19] make all the tests pass Some codemods used some internal canonicalization stuff, so this commit makes that work. --- packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts | 3 +-- packages/tailwindcss/src/canonicalize-candidates.ts | 2 +- packages/tailwindcss/src/expand-declaration.test.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index e859a210f263..2b39094d45ba 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts @@ -4,10 +4,9 @@ import { createSignatureOptions, prepareDesignSystemStorage, UTILITY_SIGNATURE_KEY, -import { createSignatureOptions } from '../../../../tailwindcss/src/canonicalize-candidates' +} from '../../../../tailwindcss/src/canonicalize-candidates' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' -import { computeUtilitySignature } from '../../../../tailwindcss/src/signatures' import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' import { spliceChangesIntoString, type StringChange } from '../../utils/splice-changes-into-string' import { extractRawCandidates } from './candidates' diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 3cb61f1acbba..4bace211bf8f 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1919,7 +1919,7 @@ export interface SignatureOptions { // | `flex` | `.x { display: flex; }` | // // These produce the same signature, therefore they represent the same utility. -const UTILITY_SIGNATURE_KEY = Symbol() +export const UTILITY_SIGNATURE_KEY = Symbol() function createUtilitySignatureCache( designSystem: DesignSystem, ): DesignSystem['storage'][typeof UTILITY_SIGNATURE_KEY] { diff --git a/packages/tailwindcss/src/expand-declaration.test.ts b/packages/tailwindcss/src/expand-declaration.test.ts index 44456f239025..1f9e71561a6b 100644 --- a/packages/tailwindcss/src/expand-declaration.test.ts +++ b/packages/tailwindcss/src/expand-declaration.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from 'vitest' import { toCss } from './ast' +import { SignatureFeatures } from './canonicalize-candidates' import { parse } from './css-parser' import { expandDeclaration } from './expand-declaration' -import { SignatureFeatures } from './signatures' import { walk, WalkAction } from './walk' const css = String.raw From b4ce0090e44d3c3d3a2f1865d512635de3a023ca Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Oct 2025 15:18:19 +0200 Subject: [PATCH 19/19] remove unnecessary exports --- packages/tailwindcss/src/canonicalize-candidates.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 4bace211bf8f..996859c539cd 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -71,7 +71,7 @@ interface InternalCanonicalizeOptions { signatureOptions: SignatureOptions } -export interface DesignSystem extends BaseDesignSystem { +interface DesignSystem extends BaseDesignSystem { storage: { [SIGNATURE_OPTIONS_KEY]: DefaultMap< number | null, // Rem value @@ -1888,7 +1888,7 @@ export enum SignatureFeatures { LogicalToPhysical = 1 << 1, } -export interface SignatureOptions { +interface SignatureOptions { /** * The root font size in pixels. If provided, `rem` values will be normalized * to `px` values. @@ -2208,7 +2208,7 @@ function createUtilityPropertiesCache( // essentially pre-computing the values and modifiers. This is a bit slow, but // also only has to happen once per design system. const PRE_COMPUTED_UTILITIES_KEY = Symbol() -export function createPreComputedUtilitiesCache( +function createPreComputedUtilitiesCache( designSystem: DesignSystem, ): DesignSystem['storage'][typeof PRE_COMPUTED_UTILITIES_KEY] { return new DefaultMap((options: SignatureOptions) => { @@ -2269,7 +2269,7 @@ export function createPreComputedUtilitiesCache( // // These produce the same signature, therefore they represent the same variant. export const VARIANT_SIGNATURE_KEY = Symbol() -export function createVariantSignatureCache( +function createVariantSignatureCache( designSystem: DesignSystem, ): DesignSystem['storage'][typeof VARIANT_SIGNATURE_KEY] { return new DefaultMap((variant) => { @@ -2362,7 +2362,7 @@ export function createVariantSignatureCache( } export const PRE_COMPUTED_VARIANTS_KEY = Symbol() -export function createPreComputedVariantsCache( +function createPreComputedVariantsCache( designSystem: DesignSystem, ): DesignSystem['storage'][typeof PRE_COMPUTED_VARIANTS_KEY] { let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY]