From 8740cbe931fc59bf05e5d68f53a2b5750dd42234 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 20 Nov 2025 16:42:16 -0500 Subject: [PATCH 1/4] Handle `future` and `experimental` config keys during upgrade --- .../src/codemods/config/migrate-js-config.ts | 25 +++++++++++++++++++ .../src/compat/config/resolve-config.ts | 1 + .../tailwindcss/src/compat/config/types.ts | 9 +++++++ 3 files changed, 35 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts index 24afe56b85b5..e70848cac0fb 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts @@ -411,6 +411,8 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { 'presets', 'prefix', // Prefix is handled in the dedicated prefix migrator 'corePlugins', + 'future', + 'experimental', ] if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) { @@ -425,6 +427,29 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return false } + // If there are unknown "future" flags we should bail + if (unresolvedConfig.future && unresolvedConfig.future !== 'all') { + let knownFutureFlags = [ + 'hoverOnlyWhenSupported', + 'respectDefaultRingColorOpacity', + 'disableColorOpacityUtilitiesByDefault', + 'relativeContentPathsByDefault', + ] + + if (Object.keys(unresolvedConfig.future).some((key) => !knownFutureFlags.includes(key))) { + return false + } + } + + // If there are unknown "experimental" flags we should bail + if (unresolvedConfig.experimental && unresolvedConfig.experimental !== 'all') { + let knownFutureFlags = ['generalizedModifiers'] + + if (Object.keys(unresolvedConfig.experimental).some((key) => !knownFutureFlags.includes(key))) { + return false + } + } + // Only migrate the config file if all top-level theme keys are allowed to be // migrated if (theme && typeof theme === 'object') { diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 3fdc1f4548ae..858f4bbbe14a 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -33,6 +33,7 @@ interface ResolutionContext { let minimal: ResolvedConfig = { blocklist: [], future: {}, + experimental: {}, prefix: '', important: false, darkMode: null, diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index f70afa2cbe61..76851a397994 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -105,3 +105,12 @@ export interface UserConfig { export interface ResolvedConfig { future: Record } + +// `experimental` key support +export interface UserConfig { + experimental?: 'all' | Record +} + +export interface ResolvedConfig { + experimental: Record +} From a073cacb344168b702e13f36488ad96cf799b29f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 20 Nov 2025 16:49:05 -0500 Subject: [PATCH 2/4] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 206e0bc0a612..7dcb564a2163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243)) - Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243)) - Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970)) +- Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344)) ### Added From 86e1a14d124904e484ee78cb58e6f6edb490ad16 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 20 Nov 2025 16:49:39 -0500 Subject: [PATCH 3/4] Add tests --- integrations/upgrade/js-config.test.ts | 240 +++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 039f3dc8dd57..22da265c5e68 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -1841,3 +1841,243 @@ describe('border compatibility', () => { }, ) }) + +test( + `future and experimental keys are supported`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + hoverOnlyWhenSupported: true, + respectDefaultRingColorOpacity: true, + disableColorOpacityUtilitiesByDefault: true, + relativeContentPathsByDefault: true, + }, + experimental: { + generalizedModifiers: true, + }, + theme: { + colors: { + red: { + 400: '#f87171', + 500: 'red', + }, + }, + }, + plugins: [], + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant dark (&:where(.dark, .dark *)); + + @theme { + --color-*: initial; + --color-red-400: #f87171; + --color-red-500: red; + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('') + }, +) + +test( + `unknown future keys dont migrate the config`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + something: true, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(` + "--- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + something: true, + }, + } satisfies Config" + `) + }, +) + +test( + `unknown experimental keys dont migrate the config`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + experimental: { + something: true, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(` + "--- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + experimental: { + something: true, + }, + } satisfies Config" + `) + }, +) From 9c53d5edcf5b30ab903ee006a454b2e9c20fb4ce Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 20 Nov 2025 17:06:46 -0500 Subject: [PATCH 4/4] Fix variable name --- .../src/codemods/config/migrate-js-config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts index e70848cac0fb..1b2b066ab661 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts @@ -429,23 +429,23 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { // If there are unknown "future" flags we should bail if (unresolvedConfig.future && unresolvedConfig.future !== 'all') { - let knownFutureFlags = [ + let knownFlags = [ 'hoverOnlyWhenSupported', 'respectDefaultRingColorOpacity', 'disableColorOpacityUtilitiesByDefault', 'relativeContentPathsByDefault', ] - if (Object.keys(unresolvedConfig.future).some((key) => !knownFutureFlags.includes(key))) { + if (Object.keys(unresolvedConfig.future).some((key) => !knownFlags.includes(key))) { return false } } // If there are unknown "experimental" flags we should bail if (unresolvedConfig.experimental && unresolvedConfig.experimental !== 'all') { - let knownFutureFlags = ['generalizedModifiers'] + let knownFlags = ['generalizedModifiers'] - if (Object.keys(unresolvedConfig.experimental).some((key) => !knownFutureFlags.includes(key))) { + if (Object.keys(unresolvedConfig.experimental).some((key) => !knownFlags.includes(key))) { return false } }