From 3b61277e7afa7e2febddcb3c27a850ea42d6d70d Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 4 Feb 2025 15:43:48 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + .../src/compat/apply-config-to-theme.test.ts | 36 +++++++++++++ .../tailwindcss/src/compat/config.test.ts | 51 +++++++++++++++++++ .../src/compat/config/resolve-config.test.ts | 45 ++++++++++++++++ .../src/compat/plugin-functions.ts | 2 +- 5 files changed, 134 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e087548..71cd3e343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts index 23f5c46cb..619c99182 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -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", + } + `) +}) diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 4b095c172..eb18cd734 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -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); + } + " + `) +}) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.test.ts b/packages/tailwindcss/src/compat/config/resolve-config.test.ts index 0a1daeee6..d96bdc700 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.test.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.test.ts @@ -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'])) +}) diff --git a/packages/tailwindcss/src/compat/plugin-functions.ts b/packages/tailwindcss/src/compat/plugin-functions.ts index 9e3917eaa..339bd11c6 100644 --- a/packages/tailwindcss/src/compat/plugin-functions.ts +++ b/packages/tailwindcss/src/compat/plugin-functions.ts @@ -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 }