diff --git a/CHANGELOG.md b/CHANGELOG.md index cf5ebd5fd..306b38426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 4bf446a26..81a53929f 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -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'])) + }, + ) }) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 118f51e60..9a0b0e6af 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -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 })