Skip to content

Commit ec8049a

Browse files
committed
fix(canonicalize): prevent collapse cache pollution across calls
1 parent 257dc58 commit ec8049a

3 files changed

Lines changed: 154 additions & 59 deletions

File tree

‎packages/tailwindcss/src/canonicalize-candidates.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,29 @@ describe('theme to var', () => {
11451145
})
11461146
})
11471147

1148+
describe('regressions', () => {
1149+
test('collapse canonicalization is not affected by previous calls', { timeout }, async () => {
1150+
let designSystem = await designSystems.get(__dirname).get(css`
1151+
@import 'tailwindcss'; /* regression */
1152+
`)
1153+
1154+
let options: CanonicalizeOptions = {
1155+
collapse: true,
1156+
logicalToPhysical: true,
1157+
rem: 16,
1158+
}
1159+
1160+
let target = ['underline', 'h-4', 'w-4']
1161+
1162+
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
1163+
1164+
designSystem.canonicalizeCandidates(['mb-4', 'text-sm'], options)
1165+
designSystem.canonicalizeCandidates(['underline', 'mb-4'], options)
1166+
1167+
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
1168+
})
1169+
})
1170+
11481171
describe('options', () => {
11491172
test('normalize `rem` units to `px`', { timeout }, async () => {
11501173
let designSystem = await __unstable__loadDesignSystem(

‎packages/tailwindcss/src/canonicalize-candidates.ts‎

Lines changed: 109 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { Writable } from './types'
2222
import { DefaultMap } from './utils/default-map'
2323
import { dimensions } from './utils/dimensions'
2424
import { isPositiveInteger, isValidSpacingMultiplier } from './utils/infer-data-type'
25+
import './utils/map-get-or-insert'
2526
import { replaceObject } from './utils/replace-object'
2627
import { segment } from './utils/segment'
2728
import { toKeyPath } from './utils/to-key-path'
@@ -93,14 +94,8 @@ interface DesignSystem extends BaseDesignSystem {
9394
[CONVERTER_KEY]: (input: string, options?: Convert) => [string, CandidateModifier | null]
9495
[SPACING_KEY]: DefaultMap<string, number | null> | null
9596
[UTILITY_SIGNATURE_KEY]: DefaultMap<SignatureOptions, DefaultMap<string, string | Symbol>>
96-
[STATIC_UTILITIES_KEY]: DefaultMap<
97-
SignatureOptions,
98-
DefaultMap<string, DefaultMap<string, Set<string>>>
99-
>
100-
[UTILITY_PROPERTIES_KEY]: DefaultMap<
101-
SignatureOptions,
102-
DefaultMap<string, DefaultMap<string, Set<string>>>
103-
>
97+
[STATIC_UTILITIES_KEY]: Map<SignatureOptions, Map<string, Map<string, Set<string>>>>
98+
[UTILITY_PROPERTIES_KEY]: Map<SignatureOptions, Map<string, Map<string, Set<string>>>>
10499
[PRE_COMPUTED_UTILITIES_KEY]: DefaultMap<SignatureOptions, DefaultMap<string, string[]>>
105100
[VARIANT_SIGNATURE_KEY]: DefaultMap<string, string | Symbol>
106101
[PRE_COMPUTED_VARIANTS_KEY]: DefaultMap<string, string[]>
@@ -122,7 +117,7 @@ export function prepareDesignSystemStorage(
122117
designSystem.storage[SPACING_KEY] ??= createSpacingCache(designSystem, options)
123118
designSystem.storage[UTILITY_SIGNATURE_KEY] ??= createUtilitySignatureCache(designSystem)
124119
designSystem.storage[STATIC_UTILITIES_KEY] ??= createStaticUtilitiesCache()
125-
designSystem.storage[UTILITY_PROPERTIES_KEY] ??= createUtilityPropertiesCache(designSystem)
120+
designSystem.storage[UTILITY_PROPERTIES_KEY] ??= createUtilityPropertiesCache()
126121
designSystem.storage[PRE_COMPUTED_UTILITIES_KEY] ??= createPreComputedUtilitiesCache(designSystem)
127122
designSystem.storage[VARIANT_SIGNATURE_KEY] ??= createVariantSignatureCache(designSystem)
128123
designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] ??= createPreComputedVariantsCache(designSystem)
@@ -246,28 +241,26 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
246241

247242
function collapseGroup(candidates: string[]) {
248243
let signatureOptions = options.signatureOptions
249-
let computeUtilitiesPropertiesLookup =
250-
designSystem.storage[UTILITY_PROPERTIES_KEY].get(signatureOptions)
251-
let staticUtilities = designSystem.storage[STATIC_UTILITIES_KEY].get(signatureOptions)
244+
let staticUtilities = getStaticUtilitiesLookup(designSystem, signatureOptions)
252245

253246
// For each candidate, compute the used properties and values. E.g.: `mt-1` → `margin-top` → `0.25rem`
254247
//
255248
// NOTE: Currently assuming we are dealing with static utilities only. This
256249
// will change the moment we have `@utility` for most built-ins.
257250
let candidatePropertiesValues = candidates.map((candidate) =>
258-
computeUtilitiesPropertiesLookup.get(candidate),
251+
getUtilityPropertiesLookup(designSystem, signatureOptions, candidate),
259252
)
260253

261254
// Hard-coded optimization: if any candidate sets `line-height` and another
262255
// candidate sets `font-size`, we pre-compute the `text-*` utilities with
263256
// this line-height to try and collapse to those combined values.
264-
if (candidatePropertiesValues.some((x) => x.has('line-height'))) {
257+
if (candidatePropertiesValues.some((x) => (x.get('line-height')?.size ?? 0) > 0)) {
265258
let fontSizeNames = designSystem.theme.keysInNamespaces(['--text'])
266259
if (fontSizeNames.length > 0) {
267260
let interestingLineHeights = new Set<string | number>()
268261
let seenLineHeights = new Set<string>()
269262
for (let pairs of candidatePropertiesValues) {
270-
for (let lineHeight of pairs.get('line-height')) {
263+
for (let lineHeight of pairs.get('line-height') ?? []) {
271264
if (seenLineHeights.has(lineHeight)) continue
272265
seenLineHeights.add(lineHeight)
273266

@@ -277,13 +270,21 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
277270
interestingLineHeights.add(bareValue)
278271

279272
for (let name of fontSizeNames) {
280-
computeUtilitiesPropertiesLookup.get(`text-${name}/${bareValue}`)
273+
getUtilityPropertiesLookup(
274+
designSystem,
275+
signatureOptions,
276+
`text-${name}/${bareValue}`,
277+
)
281278
}
282279
} else {
283280
interestingLineHeights.add(lineHeight)
284281

285282
for (let name of fontSizeNames) {
286-
computeUtilitiesPropertiesLookup.get(`text-${name}/[${lineHeight}]`)
283+
getUtilityPropertiesLookup(
284+
designSystem,
285+
signatureOptions,
286+
`text-${name}/[${lineHeight}]`,
287+
)
287288
}
288289
}
289290
}
@@ -292,15 +293,23 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
292293

293294
let seenFontSizes = new Set<string>()
294295
for (let pairs of candidatePropertiesValues) {
295-
for (let fontSize of pairs.get('font-size')) {
296+
for (let fontSize of pairs.get('font-size') || []) {
296297
if (seenFontSizes.has(fontSize)) continue
297298
seenFontSizes.add(fontSize)
298299

299300
for (let lineHeight of interestingLineHeights) {
300301
if (isValidSpacingMultiplier(lineHeight)) {
301-
computeUtilitiesPropertiesLookup.get(`text-[${fontSize}]/${lineHeight}`)
302+
getUtilityPropertiesLookup(
303+
designSystem,
304+
signatureOptions,
305+
`text-[${fontSize}]/${lineHeight}`,
306+
)
302307
} else {
303-
computeUtilitiesPropertiesLookup.get(`text-[${fontSize}]/[${lineHeight}]`)
308+
getUtilityPropertiesLookup(
309+
designSystem,
310+
signatureOptions,
311+
`text-[${fontSize}]/[${lineHeight}]`,
312+
)
304313
}
305314
}
306315
}
@@ -317,7 +326,12 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
317326
let result: Set<string> | null = null
318327
for (let property of propertyValues.keys()) {
319328
let otherUtilities = new Set<string>()
320-
for (let group of staticUtilities.get(property).values()) {
329+
let groupsByValue = staticUtilities.get(property)
330+
if (!groupsByValue) {
331+
return new Set<string>()
332+
}
333+
334+
for (let group of groupsByValue.values()) {
321335
for (let candidate of group) {
322336
otherUtilities.add(candidate)
323337
}
@@ -2278,49 +2292,85 @@ function resolveVariablesInValue(value: string, designSystem: DesignSystem): str
22782292
// Index all static utilities by property and value
22792293
const STATIC_UTILITIES_KEY = Symbol()
22802294
function createStaticUtilitiesCache(): DesignSystem['storage'][typeof STATIC_UTILITIES_KEY] {
2281-
return new DefaultMap((_optiones: SignatureOptions) => {
2282-
return new DefaultMap((_property: string) => {
2283-
return new DefaultMap((_value: string) => {
2284-
return new Set<string>()
2285-
})
2286-
})
2287-
})
2295+
return new Map()
22882296
}
22892297

22902298
const UTILITY_PROPERTIES_KEY = Symbol()
2291-
function createUtilityPropertiesCache(
2299+
function createUtilityPropertiesCache(): DesignSystem['storage'][typeof UTILITY_PROPERTIES_KEY] {
2300+
return new Map()
2301+
}
2302+
2303+
function getStaticUtilitiesLookup(
22922304
designSystem: DesignSystem,
2293-
): DesignSystem['storage'][typeof UTILITY_PROPERTIES_KEY] {
2294-
return new DefaultMap((options: SignatureOptions) => {
2295-
return new DefaultMap((className) => {
2296-
let localPropertyValueLookup = new DefaultMap((_property) => new Set<string>())
2305+
options: SignatureOptions,
2306+
): Map<string, Map<string, Set<string>>> {
2307+
let lookup = designSystem.storage[STATIC_UTILITIES_KEY].get(options)
2308+
if (lookup) return lookup
2309+
2310+
return designSystem.storage[STATIC_UTILITIES_KEY].getOrInsert(options, new Map())
2311+
}
2312+
2313+
function getUtilityPropertiesCache(
2314+
designSystem: DesignSystem,
2315+
options: SignatureOptions,
2316+
): Map<string, Map<string, Set<string>>> {
2317+
let cache = designSystem.storage[UTILITY_PROPERTIES_KEY].get(options)
2318+
if (cache) return cache
2319+
2320+
return designSystem.storage[UTILITY_PROPERTIES_KEY].getOrInsert(options, new Map())
2321+
}
2322+
2323+
function getUtilityPropertiesLookup(
2324+
designSystem: DesignSystem,
2325+
options: SignatureOptions,
2326+
className: string,
2327+
): Map<string, Set<string>> {
2328+
let utilityPropertiesCache = getUtilityPropertiesCache(designSystem, options)
2329+
let localPropertyValueLookup = utilityPropertiesCache.get(className)
2330+
if (localPropertyValueLookup) return localPropertyValueLookup
2331+
2332+
localPropertyValueLookup = new Map<string, Set<string>>()
2333+
utilityPropertiesCache.set(className, localPropertyValueLookup)
2334+
2335+
let resolvedClassName = className
2336+
if (designSystem.theme.prefix && !resolvedClassName.startsWith(designSystem.theme.prefix)) {
2337+
resolvedClassName = `${designSystem.theme.prefix}:${resolvedClassName}`
2338+
}
2339+
2340+
let parsed = designSystem.parseCandidate(resolvedClassName)
2341+
if (parsed.length === 0) return localPropertyValueLookup
22972342

2298-
if (designSystem.theme.prefix && !className.startsWith(designSystem.theme.prefix)) {
2299-
className = `${designSystem.theme.prefix}:${className}`
2343+
let staticUtilities = getStaticUtilitiesLookup(designSystem, options)
2344+
2345+
walk(
2346+
canonicalizeAst(
2347+
designSystem,
2348+
designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)),
2349+
options,
2350+
),
2351+
(node) => {
2352+
if (node.kind !== 'declaration') return
2353+
2354+
let values = localPropertyValueLookup.get(node.property)
2355+
if (!values) {
2356+
values = localPropertyValueLookup.getOrInsert(node.property, new Set<string>())
23002357
}
2301-
let parsed = designSystem.parseCandidate(className)
2302-
if (parsed.length === 0) return localPropertyValueLookup
2303-
2304-
walk(
2305-
canonicalizeAst(
2306-
designSystem,
2307-
designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)),
2308-
options,
2309-
),
2310-
(node) => {
2311-
if (node.kind === 'declaration') {
2312-
localPropertyValueLookup.get(node.property).add(node.value!)
2313-
designSystem.storage[STATIC_UTILITIES_KEY].get(options)
2314-
.get(node.property)
2315-
.get(node.value!)
2316-
.add(className)
2317-
}
2318-
},
2319-
)
2358+
values.add(node.value!)
23202359

2321-
return localPropertyValueLookup
2322-
})
2323-
})
2360+
let utilitiesByValue = staticUtilities.get(node.property)
2361+
if (!utilitiesByValue) {
2362+
utilitiesByValue = staticUtilities.getOrInsert(node.property, new Map())
2363+
}
2364+
2365+
let utilities = utilitiesByValue.get(node.value!)
2366+
if (!utilities) {
2367+
utilities = utilitiesByValue.getOrInsert(node.value!, new Set<string>())
2368+
}
2369+
utilities.add(resolvedClassName)
2370+
},
2371+
)
2372+
2373+
return localPropertyValueLookup
23242374
}
23252375

23262376
// For all static utilities in the system, compute a lookup table that maps the
@@ -2356,7 +2406,7 @@ function createPreComputedUtilitiesCache(
23562406
}
23572407

23582408
lookup.get(signature).push(className)
2359-
designSystem.storage[UTILITY_PROPERTIES_KEY].get(options).get(className)
2409+
getUtilityPropertiesLookup(designSystem, options, className)
23602410

23612411
for (let modifier of meta.modifiers) {
23622412
// Modifiers representing numbers can be computed and don't need to be
@@ -2370,7 +2420,7 @@ function createPreComputedUtilitiesCache(
23702420
let signature = signatures.get(classNameWithModifier)
23712421
if (typeof signature !== 'string') continue
23722422
lookup.get(signature).push(classNameWithModifier)
2373-
designSystem.storage[UTILITY_PROPERTIES_KEY].get(options).get(classNameWithModifier)
2423+
getUtilityPropertiesLookup(designSystem, options, classNameWithModifier)
23742424
}
23752425
}
23762426

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export {}
2+
3+
declare global {
4+
interface Map<K, V> {
5+
getOrInsert(key: K, defaultValue: V): V
6+
}
7+
}
8+
9+
if (typeof Map.prototype.getOrInsert !== 'function') {
10+
Object.defineProperty(Map.prototype, 'getOrInsert', {
11+
configurable: true,
12+
enumerable: false,
13+
writable: true,
14+
value: function <K, V>(this: Map<K, V>, key: K, defaultValue: V): V {
15+
if (!this.has(key)) {
16+
this.set(key, defaultValue)
17+
}
18+
19+
return this.get(key)!
20+
},
21+
})
22+
}

0 commit comments

Comments
 (0)