Allow multiple @utility definitions with same name but different value types (#19777)
## Summary Fixes #16948 When defining multiple CSS `@utility foo-*` with different value types (e.g., one for colors, one for numbers), only the first handler was tried. If it returned `null` (value didn't match), the compile loop stopped, preventing subsequent handlers from being attempted. ```css @utility foo-* { color: --value(--color-*); } @utility foo-* { font-size: --spacing(--value(number)); } ``` Previously, `foo-red-500` worked but `foo-123` did not (or vice versa depending on definition order). The fix distinguishes between CSS `@utility` handlers and JS plugin `matchUtilities` handlers: - **CSS `@utility`** (no typed options): `null` means "try the next handler" - allows multiple definitions with different value types to coexist - **JS `matchUtilities`** (with explicit types): `null` means "the value was invalid for this type, stop" - preserves existing behavior where typed utilities prevent invalid values from falling through ## Test plan - Added test: two `@utility foo-*` definitions with different value types - verifies both `foo-red-500` (color) and `foo-123` (number) produce correct CSS - All 4621 existing tests pass (including the `matchUtilities` type-safety tests) - `pnpm build && pnpm test` passes This contribution was developed with AI assistance (Claude Code). --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
d194d4c3e6
commit
f036f80195
3 changed files with 61 additions and 2 deletions
|
|
@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#19988](https://github.com/tailwindlabs/tailwindcss/pull/19988))
|
||||
- Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991))
|
||||
- Upgrade: don’t migrate inline `style` attributes ([#19918](https://github.com/tailwindlabs/tailwindcss/pull/19918))
|
||||
- Allow multiple `@utility` definitions with the same name but different value types ([#19777](https://github.com/tailwindlabs/tailwindcss/pull/19777))
|
||||
|
||||
## [4.2.4] - 2026-04-21
|
||||
|
||||
|
|
|
|||
|
|
@ -287,7 +287,19 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) {
|
|||
|
||||
let compiledNodes = utility.compileFn(candidate)
|
||||
if (compiledNodes === undefined) continue
|
||||
if (compiledNodes === null) return asts
|
||||
if (compiledNodes === null) {
|
||||
// `null` means that the result is invalid and that this plugin should not
|
||||
// result in any CSS, but that doesn't mean that subsequent plugins are
|
||||
// invalid as well.
|
||||
//
|
||||
// However, for backwards compatibility with `matchUtilities` this means
|
||||
// that we do need to bail entirely: plugins that handle a specific
|
||||
// arbitrary value type prevent falling through to other plugins if the
|
||||
// result is invalid for that plugin
|
||||
if (utility.options?.types?.length) return asts
|
||||
|
||||
continue
|
||||
}
|
||||
asts.push(compiledNodes)
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +311,19 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) {
|
|||
|
||||
let compiledNodes = utility.compileFn(candidate)
|
||||
if (compiledNodes === undefined) continue
|
||||
if (compiledNodes === null) return asts
|
||||
if (compiledNodes === null) {
|
||||
// `null` means that the result is invalid and that this plugin should not
|
||||
// result in any CSS, but that doesn't mean that subsequent plugins are
|
||||
// invalid as well.
|
||||
//
|
||||
// However, for backwards compatibility with `matchUtilities` this means
|
||||
// that we do need to bail entirely: plugins that handle a specific
|
||||
// arbitrary value type prevent falling through to other plugins if the
|
||||
// result is invalid for that plugin
|
||||
if (utility.options?.types?.length) return asts
|
||||
|
||||
continue
|
||||
}
|
||||
asts.push(compiledNodes)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29979,4 +29979,38 @@ describe('custom utilities', () => {
|
|||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('multiple @utility definitions with the same name but different value types', async () => {
|
||||
let input = css`
|
||||
@theme {
|
||||
--color-red-500: #ef4444;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
@utility foo-* {
|
||||
color: --value(--color-*);
|
||||
}
|
||||
|
||||
@utility foo-* {
|
||||
font-size: --spacing(--value(number));
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['foo-red-500', 'foo-123'])).toMatchInlineSnapshot(`
|
||||
":root, :host {
|
||||
--color-red-500: #ef4444;
|
||||
--spacing: .25rem;
|
||||
}
|
||||
|
||||
.foo-123 {
|
||||
font-size: calc(var(--spacing) * 123);
|
||||
}
|
||||
|
||||
.foo-red-500 {
|
||||
color: var(--color-red-500);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue