From afd20c6cf0a850301e946c4c532cc3c8272fc9d8 Mon Sep 17 00:00:00 2001 From: koreahghg Date: Thu, 20 Aug 2026 17:46:18 +0900 Subject: [PATCH 1/2] Don't crash when theme.keyframes is explicitly set to null Every sibling module (container.ts, screens-config.ts, etc.) defensively falls back to an empty object when a resolved theme namespace is missing or nullish. `keyframesToRules` used `'keyframes' in resolvedConfig.theme` instead, which is true even when the value itself is `null` (the documented way to clear a theme key), so `Object.entries(null)` threw and crashed the whole build. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + .../compat/apply-keyframes-to-theme.test.ts | 21 +++++++++++++++++++ .../src/compat/apply-keyframes-to-theme.ts | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fec361f5959..5d2efaa03b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417)) - Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419)) - Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420)) +- Don't crash when a JS config explicitly sets `theme.keyframes` (or `theme.extend.keyframes`) to `null` ([#20425](https://github.com/tailwindlabs/tailwindcss/pull/20425)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts index aed761fcec03..23895fbe37cf 100644 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts @@ -55,6 +55,27 @@ test('keyframes can be merged into the theme', () => { `) }) +test('setting `theme.keyframes` to `null` does not throw and clears keyframes', () => { + let theme = new Theme() + let design = buildDesignSystem(theme) + + let { resolvedConfig } = resolveConfig(design, [ + { + config: { + theme: { + keyframes: null, + }, + }, + base: '/root', + reference: false, + src: undefined, + }, + ]) + + expect(() => applyKeyframesToTheme(design, resolvedConfig)).not.toThrow() + expect(toCss(design.theme.getKeyframes())).toEqual('') +}) + test('will append to the default keyframes with new keyframes', () => { let theme = new Theme() let design = buildDesignSystem(theme) diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts index d5855545f1c6..620c7b3fa2ae 100644 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts @@ -14,7 +14,7 @@ export function applyKeyframesToTheme( export function keyframesToRules(resolvedConfig: Pick): AtRule[] { let rules: AtRule[] = [] - if ('keyframes' in resolvedConfig.theme) { + if (resolvedConfig.theme.keyframes) { for (let [name, keyframe] of Object.entries(resolvedConfig.theme.keyframes)) { rules.push(atRule('@keyframes', name, objectToAst(keyframe as any))) } From 9c3e06e8f12c23a54784903edd38fcb2faafd424 Mon Sep 17 00:00:00 2001 From: koreahghg Date: Thu, 20 Aug 2026 18:17:46 +0900 Subject: [PATCH 2/2] Correct changelog wording and add extend.keyframes regression coverage theme.extend.keyframes: null never actually crashed: deepMerge skips null/undefined sources entirely, so the resolved value there is `{}`, not `null`. Drop the inaccurate "(or theme.extend.keyframes)" changelog claim and add a dedicated test for that path so a future change to deepMerge/resolveConfig can't silently reintroduce a crash there too. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 2 +- .../compat/apply-keyframes-to-theme.test.ts | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2efaa03b24..14ff52f444fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417)) - Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419)) - Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420)) -- Don't crash when a JS config explicitly sets `theme.keyframes` (or `theme.extend.keyframes`) to `null` ([#20425](https://github.com/tailwindlabs/tailwindcss/pull/20425)) +- Don't crash when a JS config explicitly sets `theme.keyframes` to `null` ([#20425](https://github.com/tailwindlabs/tailwindcss/pull/20425)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts index 23895fbe37cf..66161cfb8650 100644 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts @@ -76,6 +76,32 @@ test('setting `theme.keyframes` to `null` does not throw and clears keyframes', expect(toCss(design.theme.getKeyframes())).toEqual('') }) +test('setting `theme.extend.keyframes` to `null` does not throw', () => { + // Unlike a top-level `theme.keyframes: null`, this is a no-op: `deepMerge` + // skips `null`/`undefined` sources entirely, so the resolved value here is + // `{}`, not `null`. This test guards against that assumption changing. + let theme = new Theme() + let design = buildDesignSystem(theme) + + let { resolvedConfig } = resolveConfig(design, [ + { + config: { + theme: { + extend: { + keyframes: null, + }, + }, + }, + base: '/root', + reference: false, + src: undefined, + }, + ]) + + expect(() => applyKeyframesToTheme(design, resolvedConfig)).not.toThrow() + expect(toCss(design.theme.getKeyframes())).toEqual('') +}) + test('will append to the default keyframes with new keyframes', () => { let theme = new Theme() let design = buildDesignSystem(theme)