From 89526457af9655ce6418b7f01be7acce08b06dd2 Mon Sep 17 00:00:00 2001 From: kirkouimet Date: Wed, 25 Feb 2026 17:21:23 -0700 Subject: [PATCH 1/2] 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. --- .../src/canonicalize-candidates.test.ts | 37 +++++++++++++++++++ .../src/canonicalize-candidates.ts | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index f1cb0c225296..100d900a1a0c 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -1220,3 +1220,40 @@ test('collapse canonicalization is not affected by previous calls', { timeout }, 'size-4', ]) }) + +test('collapse does not crash when utilities with no standard properties are present', { timeout }, async () => { + let designSystem = await designSystems.get(__dirname).get(css` + @import 'tailwindcss'; + `) + + let options: CanonicalizeOptions = { + collapse: true, + logicalToPhysical: true, + rem: 16, + } + + // Shadow utilities use CSS custom properties and @property rules but may + // produce empty property maps in the collapse algorithm. This should not + // crash with "Cannot read properties of null" or "X is not iterable". + expect(() => + designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options), + ).not.toThrow() + + expect(() => + designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options), + ).not.toThrow() + + expect(() => + designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options), + ).not.toThrow() + + // Verify the candidates are returned (not collapsed, since shadows can't + // meaningfully collapse with unrelated utilities) + expect( + designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options), + ).toEqual(expect.arrayContaining(['shadow-sm', 'border'])) + + expect( + designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options), + ).toEqual(expect.arrayContaining(['shadow-sm', 'shadow-md'])) +}) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index b2c54b749ca0..d1b3b361b4cd 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -334,7 +334,7 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st // all intersections with an empty set will remain empty. if (result!.size === 0) return result! } - return result! + return result ?? new Set() }) // Link each candidate that could be linked via another utility From d68df54d6a628d33e65e50fe55836e4de81e1c8d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 10 Mar 2026 16:45:45 +0100 Subject: [PATCH 2/2] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd32163d2bd..c30de00ef425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Guard object lookups against inherited prototype properties ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725)) - Canonicalize `calc(var(--spacing)*…)` expressions into `--spacing(…)` ([#19769](https://github.com/tailwindlabs/tailwindcss/pull/19769)) +- Fix crash in canonicalization step when handling utilities with empty property maps ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727)) ## [4.2.1] - 2026-02-23