Don't crash when setting JS theme value to null (#16210)

Closes #16035

In v3 it was possible to unset a specific color namespace by setting
doing something like this:

```js
export default {
  theme: {
    extend: {
      colors: {
        red: null,
      },
    },
  },
}
```

This pattern would crash in v4 right now due to the theme access
function not being able to work on the red property being a `null`. This
PR fixes this crash.

However it leaves the behavior as-is for now so that the red namespace
_defined via CSS will still be accessible_. This is technically
different from v3 but fixing this would be more work as we only allow
unsetting top-level namespaces in the interop layer (via the
non-`extend`-theme-object). I would recommend migrating to the v4 API
for doing these partial namespace resets if you want to get rid of the
defaults in v4:

```css
@theme {
  --color-red-*: initial;
}
```

## Test plan

The crash was mainly captured via the test in `compat/config.test.ts`
but I've added two more tests across the different levels of
abstractions so that it's clear what `null` should be doing.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Philipp Spiess 2025-02-04 15:43:48 +01:00 committed by GitHub
parent 5601fb50a9
commit 3b61277e7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 134 additions and 1 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fix a crash when setting JS theme values to `null` ([#16210](https://github.com/tailwindlabs/tailwindcss/pull/16210))
- Ensure CSS variables in arbitrary values are properly decoded ([#16206](https://github.com/tailwindlabs/tailwindcss/pull/16206))
- Ensure that the `containers` JS theme key is added to the `--container-*` namespace ([#16169](https://github.com/tailwindlabs/tailwindcss/pull/16169))
- Fix missing `@keyframes` definition ([#16237](https://github.com/tailwindlabs/tailwindcss/pull/16237))

View File

@ -223,3 +223,39 @@ test('converts opacity modifiers from decimal to percentage values', () => {
expect(theme.resolve('20', ['--opacity'])).toEqual('20%')
expect(theme.resolve('25', ['--opacity'])).toEqual('25%')
})
test('handles setting theme keys to null', async () => {
let theme = new Theme()
let design = buildDesignSystem(theme)
theme.add('--color-blue-400', 'blue', ThemeOptions.DEFAULT)
theme.add('--color-blue-500', '#3b82f6')
theme.add('--color-red-400', 'red', ThemeOptions.DEFAULT)
theme.add('--color-red-500', '#ef4444')
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
{
config: {
theme: {
extend: {
colors: {
blue: null,
},
},
},
},
base: '/root',
reference: false,
},
])
applyConfigToTheme(design, resolvedConfig, replacedThemeKeys)
expect(theme.namespace('--color')).toMatchInlineSnapshot(`
Map {
"blue-400" => "blue",
"blue-500" => "#3b82f6",
"red-400" => "red",
"red-500" => "#ef4444",
}
`)
})

View File

@ -1628,3 +1628,54 @@ test('old theme values are merged with their renamed counterparts in the CSS the
expect(didCallPluginFn).toHaveBeenCalled()
})
test('handles setting theme keys to null', async () => {
let compiler = await compile(
css`
@theme default {
--color-red-50: oklch(0.971 0.013 17.38);
--color-red-100: oklch(0.936 0.032 17.717);
}
@config "./my-config.js";
@tailwind utilities;
@theme {
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-200: oklch(0.885 0.062 18.334);
}
`,
{
loadModule: async () => {
return {
module: {
theme: {
extend: {
colors: {
red: null,
},
},
},
},
base: '/root',
}
},
},
)
expect(compiler.build(['bg-red-50', 'bg-red-100', 'bg-red-200'])).toMatchInlineSnapshot(`
":root, :host {
--color-red-50: oklch(0.971 0.013 17.38);
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-200: oklch(0.885 0.062 18.334);
}
.bg-red-50 {
background-color: var(--color-red-50);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-red-200 {
background-color: var(--color-red-200);
}
"
`)
})

View File

@ -254,3 +254,48 @@ test('theme keys can read from the CSS theme', () => {
new Set(['colors', 'accentColor', 'placeholderColor', 'caretColor', 'transitionColor']),
)
})
test('handles null as theme values', () => {
let theme = new Theme()
theme.add('--color-red-50', 'red')
theme.add('--color-red-100', 'red')
let design = buildDesignSystem(theme)
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
{
config: {
theme: {
colors: ({ theme }) => ({
// Reads from the --color-* namespace
...theme('color'),
}),
},
},
base: '/root',
reference: false,
},
{
config: {
theme: {
extend: {
colors: {
red: null,
},
},
},
},
base: '/root',
reference: false,
},
])
expect(resolvedConfig).toMatchObject({
theme: {
colors: {
red: null,
},
},
})
expect(replacedThemeKeys).toEqual(new Set(['colors']))
})

View File

@ -224,7 +224,7 @@ function get(obj: any, path: string[]) {
let key = path[i]
// The key does not exist so concatenate it with the next key
if (obj[key] === undefined) {
if (obj?.[key] === undefined) {
if (path[i + 1] === undefined) {
return undefined
}