Fix theme() in JS plugins returning unresolved object instead of DEFAULT value (#20299)
## Summary
When a CSS theme key defined via `@theme` (or a JS config's `theme`
object) shares a dash-separated prefix with a sibling key — e.g.
`--color-foo` and `--color-foo-bar` — calling `theme('colors.foo')` from
inside a JS plugin (`addUtilities`, `addComponents`, etc.) does not
resolve to the `foo` value. Instead it returns an internal
disambiguation object shaped like `{ DEFAULT: 'red', bar: 'blue',
__CSS_VALUES__: {...} }`, because there's no way to tell from CSS custom
property names alone whether `foo-bar` is a sibling key or a nested
sub-key of `foo`.
This same ambiguity was already fixed for the CSS-embedded `theme()`
function in #19097 (which unwraps to the `DEFAULT` key when present),
and the changelog entry for that PR states it fixes this "in JS configs
**and plugins**" — but the fix only touched `apply-compat-hooks.ts`'s
`resolveThemeValue`, not `createThemeFn`'s `theme` function that's
exposed directly to plugins in `plugin-functions.ts`. This PR closes
that gap by applying the same DEFAULT-unwrapping there.
Without this fix, passing the raw object into `addUtilities` (a very
natural thing to do, since a plugin author expects a string) produces
broken CSS — the reserved `DEFAULT` key gets mangled into a garbage
property name (`-d-e-f-a-u-l-t`) by the kebab-case conversion, and the
internal `__CSS_VALUES__` bookkeeping leaks into the generated
stylesheet.
### Minimal reproduction
```js
// tailwind.config.js (registered via @config, or any @plugin-registered plugin)
const plugin = require('tailwindcss/plugin')
module.exports = {
plugins: [
plugin(function ({ addUtilities, theme }) {
addUtilities({
'.example-foo': { color: theme('colors.foo') },
})
}),
],
}
```
```css
@import "tailwindcss";
@config "./tailwind.config.js";
@theme {
--color-foo: red;
--color-foo-bar: blue;
}
```
**Before:**
```css
.example-foo color {
-d-e-f-a-u-l-t: red;
bar: blue;
}
.example-foo color __CSS_VALUES__ {
-d-e-f-a-u-l-t: 0;
bar: 0;
}
```
**After:**
```css
.example-foo {
color: red;
}
```
## Test plan
- Added a regression test in
`packages/tailwindcss/src/compat/plugin-api.test.ts` ("theme() resolves
the DEFAULT value when a bare CSS theme key shares a prefix with a
sibling key")
- Verified via `pnpm --filter tailwindcss exec vitest run` that this
test fails with the exact broken output shown above when the fix is
reverted, and passes once it's applied
- Ran the full `tailwindcss` package test suite (`pnpm --filter
tailwindcss exec vitest run`) — 4684 tests passing, no regressions
- Verified formatting on the changed files with `npx prettier --check`
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
b53fa096c9
commit
04588b1e8f
3 changed files with 58 additions and 0 deletions
|
|
@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Allow `@tailwindcss/cli` in `--watch` mode to use polling with `--poll` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297))
|
||||
- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298))
|
||||
- Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292))
|
||||
- Prevent `theme('colors.foo')` in JS plugins from returning an internal disambiguation object when a CSS theme key shares a prefix with a sibling key like `--color-foo-bar` ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299))
|
||||
|
||||
## [4.3.2] - 2026-06-26
|
||||
|
||||
|
|
|
|||
|
|
@ -635,6 +635,46 @@ describe('theme', async () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('theme() resolves the DEFAULT value when a bare CSS theme key shares a prefix with a sibling key', async () => {
|
||||
expect(
|
||||
await run(
|
||||
['example-foo', 'example-foo-bar'],
|
||||
css`
|
||||
@tailwind utilities;
|
||||
@theme {
|
||||
--color-foo: red;
|
||||
--color-foo-bar: blue;
|
||||
}
|
||||
@plugin "my-plugin";
|
||||
`,
|
||||
{
|
||||
loadModule: async (_id, base) => {
|
||||
return {
|
||||
path: '',
|
||||
base,
|
||||
module: plugin(function ({ addUtilities, theme }) {
|
||||
addUtilities({
|
||||
'.example-foo': { color: theme('colors.foo') },
|
||||
'.example-foo-bar': { color: theme('colors.foo-bar') },
|
||||
})
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.example-foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.example-foo-bar {
|
||||
color: #00f;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('all necessary theme keys support bare values', async () => {
|
||||
expect(
|
||||
await run(
|
||||
|
|
|
|||
|
|
@ -100,6 +100,23 @@ export function createThemeFn(
|
|||
return [base, extra]
|
||||
}
|
||||
|
||||
// If `--color-foo` and `--color-foo-bar` are both defined, `colors.foo`
|
||||
// produces a synthetic object:
|
||||
//
|
||||
// ```ts
|
||||
// { DEFAULT: 'red', bar: 'blue', __CSS_VALUES__: { DEFAULT: 0, bar: 0 } }
|
||||
// ```
|
||||
//
|
||||
// Prefer `DEFAULT` instead of exposing the object to plugin code.
|
||||
if (
|
||||
cssValue !== null &&
|
||||
typeof cssValue === 'object' &&
|
||||
!Array.isArray(cssValue) &&
|
||||
'DEFAULT' in cssValue
|
||||
) {
|
||||
return cssValue.DEFAULT
|
||||
}
|
||||
|
||||
// Values from CSS take precedence over values from the config
|
||||
return cssValue ?? configValue
|
||||
})()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue