From f92276307af4344fb1534425a56700dbc5c52e30 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 16:55:26 +0200 Subject: [PATCH 01/13] Add test cases for not-yet-migrateable configs --- CHANGELOG.md | 1 + integrations/upgrade/js-config.test.ts | 193 +++++++++++++++++- packages/@tailwindcss-node/src/compile.ts | 8 +- .../src/template/prepare-config.ts | 1 + .../src/compat/config/resolve-config.ts | 3 + 5 files changed, 200 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1ad6f1f6875..754bfbc422be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603)) - _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635)) - _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644)) +- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639)) ### Fixed diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 87f95ccd0a69..6bf56576ca0c 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -103,7 +103,198 @@ test( ) test( - `does not upgrade a complex JS config file to CSS`, + 'does not upgrade JS config files with functions in the theme config', + { + fs: { + 'package.json': json` + { + "dependencies": { + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + theme: { + extend: { + colors: ({ colors }) => ({ + gray: colors.neutral, + }), + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + @config '../tailwind.config.ts'; + " + `) + + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + --- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + + export default { + theme: { + extend: { + colors: ({ colors }) => ({ + gray: colors.neutral, + }), + }, + }, + } satisfies Config + " + `) + }, +) + +test( + 'does not upgrade JS config files with typography styles in the theme config', + { + fs: { + 'package.json': json` + { + "dependencies": { + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import typographyStyles from './typography' + + export default { + theme: { + typography: typographyStyles, + }, + } satisfies Config + `, + 'typography.ts': ts` + import { type PluginUtils } from 'tailwindcss/types/config' + + export default function typographyStyles({ theme }: PluginUtils) { + return { + DEFAULT: { + css: { + '--tw-prose-body': theme('colors.zinc.600'), + color: 'var(--tw-prose-body)', + }, + }, + } + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + @config '../tailwind.config.ts'; + `, + }, + }, + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + @config '../tailwind.config.ts'; + " + `) + + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + --- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + import typographyStyles from './typography' + + export default { + theme: { + typography: typographyStyles, + }, + } satisfies Config + " + `) + }, +) + +test( + 'does not upgrade JS config files with static plugins', + { + fs: { + 'package.json': json` + { + "dependencies": { + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import typography from '@tailwindcss/typography' + import customPlugin from './custom-plugin' + + export default { + plugins: [typography, customPlugin], + } satisfies Config + `, + 'custom-plugin.js': ts` + export default function ({ addVariant }) { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + @config '../tailwind.config.ts'; + " + `) + + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + --- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + import typography from '@tailwindcss/typography' + import customPlugin from './custom-plugin' + + export default { + plugins: [typography, customPlugin], + } satisfies Config + " + `) + }, +) + +test( + `does not upgrade JS config files with dynamic plugins`, { fs: { 'package.json': json` diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index c31868ce5ec6..25a2d037dfb0 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -100,11 +100,8 @@ async function importModule(path: string): Promise { try { return await import(path) } catch (error) { - try { - jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false }) - return await jiti.import(path) - } catch {} - throw error + jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false }) + return await jiti.import(path) } } @@ -144,6 +141,7 @@ async function resolveCssId(id: string, base: string): Promise { diff --git a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts index 9cdb95d4980c..b5d3ffb77068 100644 --- a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts +++ b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts @@ -65,6 +65,7 @@ export async function prepareConfig( configFilePath: fullConfigPath, } } catch (e: any) { + console.error(e) error('Could not load the configuration file: ' + e.message) process.exit(1) } diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 2d8a86ff6176..fbbf807abf28 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -1,4 +1,5 @@ import type { DesignSystem } from '../../design-system' +import colors from '../colors' import type { PluginWithConfig } from '../plugin-api' import { createThemeFn } from '../plugin-functions' import { deepMerge, isPlainObject } from './deep-merge' @@ -117,6 +118,7 @@ export function mergeThemeExtension( export interface PluginUtils { theme(keypath: string, defaultValue?: any): any + colors: typeof colors } function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFile): void { @@ -176,6 +178,7 @@ function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFi function mergeTheme(ctx: ResolutionContext) { let api: PluginUtils = { theme: createThemeFn(ctx.design, () => ctx.theme, resolveValue), + colors, } function resolveValue(value: ThemeValue | null | undefined): ResolvedThemeValue { From c73242b9f27c52130c831946f917722af6567855 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 16:59:34 +0200 Subject: [PATCH 02/13] Improve comments and API naming --- CHANGELOG.md | 3 +-- integrations/upgrade/js-config.test.ts | 2 +- .../src/codemods/migrate-config.ts | 14 ++++++-------- .../@tailwindcss-upgrade/src/migrate-js-config.ts | 12 ++++++------ .../src/template/prepare-config.ts | 1 - 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754bfbc422be..2f92c575bdd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Upgrade (experimental)_: Migrate v3 PostCSS setups to v4 in some cases ([#14612](https://github.com/tailwindlabs/tailwindcss/pull/14612)) - _Upgrade (experimental)_: Automatically discover JavaScript config files ([#14597](https://github.com/tailwindlabs/tailwindcss/pull/14597)) - _Upgrade (experimental)_: Migrate legacy classes to the v4 alternative ([#14643](https://github.com/tailwindlabs/tailwindcss/pull/14643)) -- _Upgrade (experimental)_: Fully convert simple JS configs to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639)) +- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639)) - _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603)) - _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635)) - _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644)) -- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639)) ### Fixed diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 6bf56576ca0c..e3ba20780c84 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -2,7 +2,7 @@ import { expect } from 'vitest' import { css, json, test, ts } from '../utils' test( - `upgrades a simple JS config file to CSS`, + `upgrade JS config files with flat theme values, darkMode, and content fields`, { fs: { 'package.json': json` diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts index 398f612e2514..1aadec96623d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -63,9 +63,8 @@ export function migrateConfig( cssConfig.append(postcss.parse(css + jsConfigMigration.css)) } - // Inject the `@config` in a sensible place - // 1. Below the last `@import` - // 2. At the top of the file + // Inject the `@config` directive after the last `@import` or at the + // top of the file if no `@import` rules are present let locationNode = null as AtRule | null walk(root, (node) => { @@ -99,10 +98,9 @@ export function migrateConfig( if (!hasTailwindImport) return - // - If a full `@import "tailwindcss"` is present, we can inject the - // `@config` directive directly into this stylesheet. - // - If we are the root file (no parents), then we can inject the `@config` - // directive directly into this file as well. + // If a full `@import "tailwindcss"` is present or this is the root + // stylesheet, we can inject the `@config` directive directly into this + // file. if (hasFullTailwindImport || sheet.parents.size <= 0) { injectInto(sheet) return @@ -134,7 +132,7 @@ function relativeToStylesheet(sheet: Stylesheet, absolute: string) { if (relative[0] !== '.') { relative = `./${relative}` } - // Ensure relative is a posix style path since we will merge it with the + // Ensure relative is a POSIX style path since we will merge it with the // glob. return normalizePath(relative) } diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 23b2fc7d936c..0a0b2a2d9cad 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -31,9 +31,9 @@ export async function migrateJsConfig( fs.readFile(fullConfigPath, 'utf-8'), ]) - if (!isSimpleConfig(unresolvedConfig, source)) { + if (!canMigrateConfig(unresolvedConfig, source)) { info( - 'The configuration file is not a simple object. Please refer to the migration guide for how to migrate it fully to Tailwind CSS v4. For now, we will load the configuration file as-is.', + 'Your configuration file could not be automatically migrated to the new CSS configuration format, so your CSS has been updated to load your existing configuration file.', ) return null } @@ -63,8 +63,8 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme let resetNamespaces = new Map() - // Before we merge the resetting theme values with the `extend` values, we - // capture all namespaces that need to be reset + // Before we merge theme overrides with theme extensions, we capture all + // namespaces that need to be reset. for (let [key, value] of themeableValues(overwriteTheme)) { if (typeof value !== 'string' && typeof value !== 'number') { continue @@ -119,7 +119,7 @@ function createSectionKey(key: string[]): string { let sectionSegments = [] for (let i = 0; i < key.length - 1; i++) { let segment = key[i] - // ignore tuples + // Ignore tuples if (key[i + 1][0] === '-') { break } @@ -143,7 +143,7 @@ function migrateContent( } // Applies heuristics to determine if we can attempt to migrate the config -function isSimpleConfig(unresolvedConfig: Config, source: string): boolean { +function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { // The file may not contain any functions if (source.includes('function') || source.includes(' => ')) { return false diff --git a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts index b5d3ffb77068..9cdb95d4980c 100644 --- a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts +++ b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts @@ -65,7 +65,6 @@ export async function prepareConfig( configFilePath: fullConfigPath, } } catch (e: any) { - console.error(e) error('Could not load the configuration file: ' + e.message) process.exit(1) } From 41617f96da75c69b70c6e10b4401d5ce30220d2f Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 17:51:33 +0200 Subject: [PATCH 03/13] Update test names --- integrations/upgrade/js-config.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index e3ba20780c84..7a803d18acc3 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -234,7 +234,7 @@ test( ) test( - 'does not upgrade JS config files with static plugins', + 'does not upgrade JS config files with plugins', { fs: { 'package.json': json` @@ -294,7 +294,7 @@ test( ) test( - `does not upgrade JS config files with dynamic plugins`, + `does not upgrade JS config files with inline plugins`, { fs: { 'package.json': json` From 0630bb969c2bedda41ca2867d35849cd0c9eac0a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 11 Oct 2024 13:46:52 -0400 Subject: [PATCH 04/13] Tweak test name --- integrations/upgrade/js-config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 7a803d18acc3..85dffe7b822d 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -164,7 +164,7 @@ test( ) test( - 'does not upgrade JS config files with typography styles in the theme config', + 'does not upgrade JS config files with dynamic values in the theme config', { fs: { 'package.json': json` From 635105a7a31b00ce9ebbbe506bb9f7490c862167 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 11 Oct 2024 13:47:00 -0400 Subject: [PATCH 05/13] cleanup a bit --- packages/@tailwindcss-upgrade/src/migrate-js-config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 0a0b2a2d9cad..46e0395e70da 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -158,6 +158,7 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { } return ['string', 'number', 'boolean', 'undefined'].includes(typeof value) } + if (!isSimpleValue(unresolvedConfig)) { return false } @@ -171,12 +172,15 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { 'presets', 'prefix', // Prefix is handled in the dedicated prefix migrator ] + if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) { return false } + if (unresolvedConfig.plugins && unresolvedConfig.plugins.length > 0) { return false } + if (unresolvedConfig.presets && unresolvedConfig.presets.length > 0) { return false } From bdd966b0b829b4e54845d3bc2efaae93be160ec8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 11 Oct 2024 13:47:44 -0400 Subject: [PATCH 06/13] =?UTF-8?q?Don=E2=80=99t=20migrate=20themes=20with?= =?UTF-8?q?=20overly=20nested=20objects=20/=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- integrations/upgrade/js-config.test.ts | 62 +++++++++++++++++++ .../src/migrate-js-config.ts | 37 +++++++++++ 2 files changed, 99 insertions(+) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 85dffe7b822d..120e6e22016e 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -233,6 +233,68 @@ test( }, ) +test( + 'does not upgrade JS config files with deeply nested objects in the theme config', + { + fs: { + 'package.json': json` + { + "dependencies": { + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.js': ts` + export default { + theme: { + colors: { + red: { + 500: { + 50: '#fff5f57f', + }, + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + @config '../tailwind.config.js'; + `, + }, + }, + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade --force') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + @config '../tailwind.config.js'; + " + `) + + expect(await fs.dumpFiles('tailwind.config.js')).toMatchInlineSnapshot(` + " + --- tailwind.config.js --- + export default { + theme: { + colors: { + red: { + 500: { + 50: '#fff5f57f', + }, + }, + }, + }, + } + " + `) + }, +) + test( 'does not upgrade JS config files with plugins', { diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 46e0395e70da..9880b9656df4 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -185,5 +185,42 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return false } + // The file may not contain deeply nested objects in the theme + function isTooNested(value: any, maxDepth: number): boolean { + if (maxDepth === 0) return true + + if (!value) return false + + if (Array.isArray(value)) { + // This is a tuple value so its fine + if (value.length === 2 && typeof value[0] === 'string' && typeof value[1] === 'object') { + return false + } + + return value.some((v) => isTooNested(v, maxDepth - 1)) + } + + if (typeof value === 'object') { + return Object.values(value).some((v) => isTooNested(v, maxDepth - 1)) + } + + return false + } + + let theme = unresolvedConfig.theme + + if (theme && typeof theme === 'object') { + if (theme.extend && isTooNested(theme.extend, 4)) { + return false + } + + let themeCopy = { ...theme } + delete themeCopy.extend + + if (isTooNested(themeCopy, 4)) { + return false + } + } + return true } From c4ba9ee03c384e46e894d2f9f20dee26f4aafc89 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 11 Oct 2024 13:59:47 -0400 Subject: [PATCH 07/13] tweak test --- integrations/upgrade/js-config.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 120e6e22016e..eecb6abafe08 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -244,7 +244,7 @@ test( } } `, - 'tailwind.config.js': ts` + 'tailwind.config.ts': ts` export default { theme: { colors: { @@ -261,7 +261,7 @@ test( @tailwind base; @tailwind components; @tailwind utilities; - @config '../tailwind.config.js'; + @config '../tailwind.config.ts'; `, }, }, @@ -272,13 +272,13 @@ test( " --- src/input.css --- @import 'tailwindcss'; - @config '../tailwind.config.js'; + @config '../tailwind.config.ts'; " `) - expect(await fs.dumpFiles('tailwind.config.js')).toMatchInlineSnapshot(` + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` " - --- tailwind.config.js --- + --- tailwind.config.ts --- export default { theme: { colors: { From fb27131076a333bda23434b31fc593bf36c27e20 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 11 Oct 2024 14:02:01 -0400 Subject: [PATCH 08/13] Add test to verify theme.extend has one extra level of nesting --- integrations/upgrade/js-config.test.ts | 116 +++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index eecb6abafe08..f1a7a6ac98e0 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -295,6 +295,122 @@ test( }, ) +test( + 'does not upgrade JS config files with deeply nested objects in the theme.extend config', + { + fs: { + 'package.json': json` + { + "dependencies": { + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + export default { + theme: { + extend: { + colors: { + red: { + 500: { + 50: '#fff5f57f', + }, + }, + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + @config '../tailwind.config.ts'; + `, + }, + }, + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade --force') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + @config '../tailwind.config.ts'; + " + `) + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + --- tailwind.config.ts --- + export default { + theme: { + extend: { + colors: { + red: { + 500: { + 50: '#fff5f57f', + }, + }, + }, + }, + }, + } + " + `) + }, +) + +test( + 'does upgrade JS config even if theme.extend has 3 levels of nesting', + { + fs: { + 'package.json': json` + { + "dependencies": { + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + export default { + theme: { + extend: { + colors: { + red: { + 500: '#fff5f5', + }, + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + @config '../tailwind.config.ts'; + `, + }, + }, + async ({ exec, fs }) => { + await exec('npx @tailwindcss/upgrade --force') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @theme { + --color-red-500: #fff5f5; + } + @config '../tailwind.config.ts'; + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('') + }, +) + test( 'does not upgrade JS config files with plugins', { From 0f68854b00f6d3d70e6c69ee2e62572656cf1e34 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 11 Oct 2024 15:57:35 -0400 Subject: [PATCH 09/13] =?UTF-8?q?Don=E2=80=99t=20merge=20invalid=20config?= =?UTF-8?q?=20keys=20into=20the=20theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/compat/apply-config-to-theme.test.ts | 24 +++++++++++++++++++ .../src/compat/apply-config-to-theme.ts | 9 +++++++ 2 files changed, 33 insertions(+) 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 41d24d184570..5e37ab9e5b7d 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -74,3 +74,27 @@ test('Config values can be merged into the theme', () => { { '--line-height': '1.5' }, ]) }) + +test('Invalid keys are not merged into the theme', () => { + let theme = new Theme() + let design = buildDesignSystem(theme) + + let resolvedUserConfig = resolveConfig(design, [ + { + config: { + theme: { + colors: { + 'primary color': '#86753099', + }, + }, + }, + base: '/root', + }, + ]) + + applyConfigToTheme(design, resolvedUserConfig) + + let entries = Array.from(theme.entries()) + + expect(entries.length).toEqual(0) +}) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index f09d1770e7be..67f8ccd9c7a6 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -24,7 +24,10 @@ export function applyConfigToTheme(designSystem: DesignSystem, { theme }: Resolv if (typeof value !== 'string' && typeof value !== 'number') { continue } + let name = keyPathToCssProperty(path) + if (!name) continue + designSystem.theme.add( `--${name}`, value as any, @@ -110,6 +113,8 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk return toAdd } +const IS_VALID_KEY = /^[a-zA-Z0-9-_]+$/ + export function keyPathToCssProperty(path: string[]) { if (path[0] === 'colors') path[0] = 'color' if (path[0] === 'screens') path[0] = 'breakpoint' @@ -117,6 +122,10 @@ export function keyPathToCssProperty(path: string[]) { if (path[0] === 'boxShadow') path[0] = 'shadow' if (path[0] === 'animation') path[0] = 'animate' + for (let part of path) { + if (!IS_VALID_KEY.test(part)) return null + } + return ( path // [1] should move into the nested object tuple. To create the CSS variable From afb3d86b51979d36ec29af6a21e127c3df97949b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 11:14:00 +0200 Subject: [PATCH 10/13] Only add a `@theme` block when there are theme keys --- .../@tailwindcss-upgrade/src/migrate-js-config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 9880b9656df4..e4a1531f1f3e 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -50,7 +50,8 @@ export async function migrateJsConfig( } if ('theme' in unresolvedConfig) { - cssConfigs.push(await migrateTheme(unresolvedConfig as any)) + let themeConfig = await migrateTheme(unresolvedConfig as any) + if (themeConfig) cssConfigs.push(themeConfig) } return { @@ -59,7 +60,7 @@ export async function migrateJsConfig( } } -async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise { +async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise { let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme let resetNamespaces = new Map() @@ -80,10 +81,12 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< let prevSectionKey = '' let css = `@theme {` + let containsThemeKeys = false for (let [key, value] of themeableValues(themeValues)) { if (typeof value !== 'string' && typeof value !== 'number') { continue } + containsThemeKeys = true let sectionKey = createSectionKey(key) if (sectionKey !== prevSectionKey) { @@ -99,6 +102,10 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< css += ` --${keyPathToCssProperty(key)}: ${value};\n` } + if (!containsThemeKeys) { + return null + } + return css + '}\n' } From a2ff8cc4a31a1603b57f144bd186b991e2ffec81 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 11:23:45 +0200 Subject: [PATCH 11/13] Clean up tests and document typography behavior that works now --- integrations/upgrade/js-config.test.ts | 210 ++---------------- .../src/migrate-js-config.ts | 47 ++-- 2 files changed, 35 insertions(+), 222 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index f1a7a6ac98e0..69574d06d71c 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -164,7 +164,7 @@ test( ) test( - 'does not upgrade JS config files with dynamic values in the theme config', + 'does not upgrade JS config files with deeply nested objects in the theme config', { fs: { 'package.json': json` @@ -176,86 +176,19 @@ test( `, 'tailwind.config.ts': ts` import { type Config } from 'tailwindcss' - import typographyStyles from './typography' - - export default { - theme: { - typography: typographyStyles, - }, - } satisfies Config - `, - 'typography.ts': ts` - import { type PluginUtils } from 'tailwindcss/types/config' - - export default function typographyStyles({ theme }: PluginUtils) { - return { - DEFAULT: { - css: { - '--tw-prose-body': theme('colors.zinc.600'), - color: 'var(--tw-prose-body)', - }, - }, - } - } - `, - 'src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - @config '../tailwind.config.ts'; - `, - }, - }, - async ({ exec, fs }) => { - await exec('npx @tailwindcss/upgrade') - - expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` - " - --- src/input.css --- - @import 'tailwindcss'; - @config '../tailwind.config.ts'; - " - `) - - expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` - " - --- tailwind.config.ts --- - import { type Config } from 'tailwindcss' - import typographyStyles from './typography' - - export default { - theme: { - typography: typographyStyles, - }, - } satisfies Config - " - `) - }, -) -test( - 'does not upgrade JS config files with deeply nested objects in the theme config', - { - fs: { - 'package.json': json` - { - "dependencies": { - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` export default { theme: { - colors: { - red: { - 500: { - 50: '#fff5f57f', + typography: { + DEFAULT: { + css: { + '--tw-prose-body': 'red', + color: 'var(--tw-prose-body)', }, }, }, }, - } + } satisfies Config `, 'src/input.css': css` @tailwind base; @@ -266,7 +199,7 @@ test( }, }, async ({ exec, fs }) => { - await exec('npx @tailwindcss/upgrade --force') + await exec('npx @tailwindcss/upgrade') expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " @@ -279,135 +212,22 @@ test( expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` " --- tailwind.config.ts --- - export default { - theme: { - colors: { - red: { - 500: { - 50: '#fff5f57f', - }, - }, - }, - }, - } - " - `) - }, -) - -test( - 'does not upgrade JS config files with deeply nested objects in the theme.extend config', - { - fs: { - 'package.json': json` - { - "dependencies": { - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - export default { - theme: { - extend: { - colors: { - red: { - 500: { - 50: '#fff5f57f', - }, - }, - }, - }, - }, - } - `, - 'src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - @config '../tailwind.config.ts'; - `, - }, - }, - async ({ exec, fs }) => { - await exec('npx @tailwindcss/upgrade --force') + import { type Config } from 'tailwindcss' - expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` - " - --- src/input.css --- - @import 'tailwindcss'; - @config '../tailwind.config.ts'; - " - `) - expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` - " - --- tailwind.config.ts --- export default { theme: { - extend: { - colors: { - red: { - 500: { - 50: '#fff5f57f', - }, + typography: { + DEFAULT: { + css: { + '--tw-prose-body': 'red', + color: 'var(--tw-prose-body)', }, }, }, }, - } - " - `) - }, -) - -test( - 'does upgrade JS config even if theme.extend has 3 levels of nesting', - { - fs: { - 'package.json': json` - { - "dependencies": { - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': ts` - export default { - theme: { - extend: { - colors: { - red: { - 500: '#fff5f5', - }, - }, - }, - }, - } - `, - 'src/input.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - @config '../tailwind.config.ts'; - `, - }, - }, - async ({ exec, fs }) => { - await exec('npx @tailwindcss/upgrade --force') - - expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` - " - --- src/input.css --- - @import 'tailwindcss'; - - @theme { - --color-red-500: #fff5f5; - } - @config '../tailwind.config.ts'; + } satisfies Config " `) - - expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('') }, ) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index e4a1531f1f3e..ec9e2f8f65b0 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -193,41 +193,34 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { } // The file may not contain deeply nested objects in the theme - function isTooNested(value: any, maxDepth: number): boolean { - if (maxDepth === 0) return true - - if (!value) return false - - if (Array.isArray(value)) { - // This is a tuple value so its fine - if (value.length === 2 && typeof value[0] === 'string' && typeof value[1] === 'object') { - return false - } - - return value.some((v) => isTooNested(v, maxDepth - 1)) - } + let theme = unresolvedConfig.theme + if (theme && typeof theme === 'object') { + if (theme.extend && isTooNested(theme.extend, 4)) return false + let { extend: _extend, ...themeCopy } = theme + if (isTooNested(themeCopy, 4)) return false + } - if (typeof value === 'object') { - return Object.values(value).some((v) => isTooNested(v, maxDepth - 1)) - } + return true +} - return false - } +// The file may not contain deeply nested objects in the theme +function isTooNested(value: any, maxDepth: number): boolean { + if (maxDepth === 0) return true - let theme = unresolvedConfig.theme + if (!value) return false - if (theme && typeof theme === 'object') { - if (theme.extend && isTooNested(theme.extend, 4)) { + if (Array.isArray(value)) { + // This is a tuple value so its fine + if (value.length === 2 && typeof value[0] === 'string' && typeof value[1] === 'object') { return false } - let themeCopy = { ...theme } - delete themeCopy.extend + return value.some((v) => isTooNested(v, maxDepth - 1)) + } - if (isTooNested(themeCopy, 4)) { - return false - } + if (typeof value === 'object') { + return Object.values(value).some((v) => isTooNested(v, maxDepth - 1)) } - return true + return false } From 7daed84bd8c4913b388cd8676a55dadaaa36f504 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 16:26:27 +0200 Subject: [PATCH 12/13] Only migrate the config file if all top-level theme keys are allowed --- integrations/upgrade/js-config.test.ts | 2 +- .../src/migrate-js-config.ts | 35 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 69574d06d71c..347e08e93063 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -164,7 +164,7 @@ test( ) test( - 'does not upgrade JS config files with deeply nested objects in the theme config', + 'does not upgrade JS config files with theme keys contributed to by plugins in the theme config', { fs: { 'package.json': json` diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index ec9e2f8f65b0..18e6712c8884 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -1,6 +1,7 @@ import fs from 'node:fs/promises' import { dirname } from 'path' import type { Config } from 'tailwindcss' +import defaultTheme from 'tailwindcss/defaultTheme' import { fileURLToPath } from 'url' import { loadModule } from '../../@tailwindcss-node/src/compile' import { @@ -9,6 +10,7 @@ import { } from '../../tailwindcss/src/compat/apply-config-to-theme' import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge' import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config' +import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types' import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode' import { info } from './utils/renderer' @@ -192,35 +194,28 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return false } - // The file may not contain deeply nested objects in the theme + // Only migrate the config file if all top-level theme keys are allowed to be + // migrated let theme = unresolvedConfig.theme if (theme && typeof theme === 'object') { - if (theme.extend && isTooNested(theme.extend, 4)) return false + if (theme.extend && !onlyUsesAllowedTopLevelKeys(theme.extend)) return false let { extend: _extend, ...themeCopy } = theme - if (isTooNested(themeCopy, 4)) return false + if (!onlyUsesAllowedTopLevelKeys(themeCopy)) return false } return true } -// The file may not contain deeply nested objects in the theme -function isTooNested(value: any, maxDepth: number): boolean { - if (maxDepth === 0) return true - - if (!value) return false - - if (Array.isArray(value)) { - // This is a tuple value so its fine - if (value.length === 2 && typeof value[0] === 'string' && typeof value[1] === 'object') { +const DEFAULT_THEME_KEYS = [ + ...Object.keys(defaultTheme), + // Used by @tailwindcss/container-queries + 'containers', +] +function onlyUsesAllowedTopLevelKeys(theme: ThemeConfig): boolean { + for (let key of Object.keys(theme)) { + if (!DEFAULT_THEME_KEYS.includes(key)) { return false } - - return value.some((v) => isTooNested(v, maxDepth - 1)) } - - if (typeof value === 'object') { - return Object.values(value).some((v) => isTooNested(v, maxDepth - 1)) - } - - return false + return true } From 4a2a624c3696d2e7f8004abf7320c22fa2789e3b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 16:28:38 +0200 Subject: [PATCH 13/13] Add second PR to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3d015dd36e7..4157e87c511e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Upgrade (experimental)_: Migrate v3 PostCSS setups to v4 in some cases ([#14612](https://github.com/tailwindlabs/tailwindcss/pull/14612)) - _Upgrade (experimental)_: Automatically discover JavaScript config files ([#14597](https://github.com/tailwindlabs/tailwindcss/pull/14597)) - _Upgrade (experimental)_: Migrate legacy classes to the v4 alternative ([#14643](https://github.com/tailwindlabs/tailwindcss/pull/14643)) -- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639)) +- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650)) - _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603)) - _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635)) - _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644))