Fix canonicalize crash with plugin component values (#20052)

## Summary

Fixes #20051.

The collapse canonicalization pass speculatively checks compatible
functional utility roots. When one of those roots comes from a plugin
registered with `matchComponents`/`matchUtilities`, the speculative
candidate can call the plugin callback with an arbitrary value that is
not present in the configured `values` map. Plugins such as the Phoenix
Heroicons helper expect the mapped value shape and can throw while
canonicalize is only probing possible replacements.

This change skips speculative replacement utilities whose property
lookup throws, matching the best-effort behavior already used by utility
signature generation. The original candidates are preserved instead of
crashing canonicalization.

## Test plan

- `source ~/.nvm/nvm.sh && nvm use 22.14.0 && pnpm vitest run
packages/tailwindcss/src/canonicalize-candidates.test.ts -t "does not
crash when plugin matchComponents rejects speculative values during
collapse"`
- `source ~/.nvm/nvm.sh && nvm use 22.14.0 && pnpm vitest run
packages/tailwindcss/src/canonicalize-candidates.test.ts`
- `source ~/.nvm/nvm.sh && nvm use 22.14.0 && pnpm prettier --check
packages/tailwindcss/src/canonicalize-candidates.ts
packages/tailwindcss/src/canonicalize-candidates.test.ts`

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Serge Yudin 2026-05-14 12:59:19 +03:00 • committed by GitHub
parent eefe64593a
commit a2b89a7012
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 58 additions and 9 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Remove deprecation warnings by using `Module#registerHooks` instead of `Module#register` on Node 26+ ([#20028](https://github.com/tailwindlabs/tailwindcss/pull/20028))
- Canonicalization: don't crash when plugin utilities throw for unsupported values ([#20052](https://github.com/tailwindlabs/tailwindcss/pull/20052))
## [4.3.0] - 2026-05-08

View file

@ -5,6 +5,7 @@ import { describe, expect, test } from 'vitest'
import { __unstable__loadDesignSystem } from '.'
import { cartesian } from './cartesian'
import type { CanonicalizeOptions } from './intellisense'
import plugin from './plugin'
import { DefaultMap } from './utils/default-map'
const css = String.raw
@ -1419,4 +1420,51 @@ describe('regressions', () => {
expect(designSystem.canonicalizeCandidates(candidates, options)).toEqual(expected)
},
)
// https://github.com/tailwindlabs/tailwindcss/issues/20051
test(
'does not crash when plugin matchComponents rejects speculative values during collapse',
{ timeout },
async () => {
let designSystem = await __unstable__loadDesignSystem(
css`
@import 'tailwindcss';
@plugin "./plugin.js";
`,
{
async loadStylesheet(_, base) {
return {
base,
path: '',
content: '@tailwind utilities;',
}
},
async loadModule() {
return {
base: '',
path: '',
module: plugin(({ matchComponents }) => {
matchComponents(
{
myicon: () => {
throw new Error('Mimic as-if a custom plugin failed for some reason')
},
},
{ values: { icon: __filename } },
)
}),
}
},
},
)
expect(
designSystem.canonicalizeCandidates(['border-[1.5px]', 'flex'], {
collapse: true,
logicalToPhysical: true,
rem: 16,
}),
).toEqual(expect.arrayContaining(['border-[1.5px]', 'flex']))
},
)
})

View file

@ -2614,13 +2614,9 @@ function createUtilityPropertiesCache(
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) => {
try {
let ast = designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node))
walk(canonicalizeAst(designSystem, ast, options), (node) => {
if (node.kind === 'declaration') {
localPropertyValueLookup.get(node.property).add(node.value!)
designSystem.storage[STATIC_UTILITIES_KEY].get(options)
@ -2628,8 +2624,12 @@ function createUtilityPropertiesCache(
.get(node.value!)
.add(className)
}
},
)
})
} catch {
// Ignore errors, this could happen when we call a plugin with a value
// it didn't expect. But since plugins are functions, we can't know
// ahead of time what it expects.
}
return localPropertyValueLookup
})