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:
parent
eefe64593a
commit
a2b89a7012
3 changed files with 58 additions and 9 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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']))
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue