Skip to content

Commit 8952645

Browse files
kirkouimetRobinMalfait
authored andcommitted
fix(canonicalize): handle utilities with empty property maps in collapse
When `canonicalizeCandidates` is called with `collapse: true`, the `collapseGroup` function builds an `otherUtilities` array by mapping each candidate's property values. If a utility generates CSS but has no standard declaration properties (e.g. shadow utilities that use `@property` rules and CSS custom properties), the inner loop over `propertyValues.keys()` never executes, leaving `result` as `null`. The non-null assertion `result!` then returns `null` into the array, causing downstream code to crash with "X is not iterable" or "Cannot read properties of null (reading 'has')" when iterating or calling methods on the null entry. Fix: return an empty Set instead of null when a utility has no property keys. This is semantically correct -- a utility with no standard properties cannot be linked to or collapsed with any other utility, which is exactly what an empty Set represents. Reproduction: `canonicalizeCandidates(['shadow-sm', 'border'], { collapse: true })` crashes on vanilla Tailwind CSS with no custom configuration.
1 parent c586bd6 commit 8952645

File tree

2 files changed

+38
-1
lines changed

2 files changed

+38
-1
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,3 +1220,40 @@ test('collapse canonicalization is not affected by previous calls', { timeout },
12201220
'size-4',
12211221
])
12221222
})
1223+
1224+
test('collapse does not crash when utilities with no standard properties are present', { timeout }, async () => {
1225+
let designSystem = await designSystems.get(__dirname).get(css`
1226+
@import 'tailwindcss';
1227+
`)
1228+
1229+
let options: CanonicalizeOptions = {
1230+
collapse: true,
1231+
logicalToPhysical: true,
1232+
rem: 16,
1233+
}
1234+
1235+
// Shadow utilities use CSS custom properties and @property rules but may
1236+
// produce empty property maps in the collapse algorithm. This should not
1237+
// crash with "Cannot read properties of null" or "X is not iterable".
1238+
expect(() =>
1239+
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options),
1240+
).not.toThrow()
1241+
1242+
expect(() =>
1243+
designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options),
1244+
).not.toThrow()
1245+
1246+
expect(() =>
1247+
designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options),
1248+
).not.toThrow()
1249+
1250+
// Verify the candidates are returned (not collapsed, since shadows can't
1251+
// meaningfully collapse with unrelated utilities)
1252+
expect(
1253+
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options),
1254+
).toEqual(expect.arrayContaining(['shadow-sm', 'border']))
1255+
1256+
expect(
1257+
designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options),
1258+
).toEqual(expect.arrayContaining(['shadow-sm', 'shadow-md']))
1259+
})

packages/tailwindcss/src/canonicalize-candidates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
334334
// all intersections with an empty set will remain empty.
335335
if (result!.size === 0) return result!
336336
}
337-
return result!
337+
return result ?? new Set<string>()
338338
})
339339

340340
// Link each candidate that could be linked via another utility

0 commit comments

Comments
 (0)