From cb2e7e7d771e5a2ef51213168582db0094bba192 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 10 Oct 2024 14:13:55 +0200 Subject: [PATCH 01/16] Add simple JS config migration --- integrations/upgrade/js-config.test.ts | 66 ++++++++++ packages/@tailwindcss-upgrade/src/index.ts | 9 ++ .../src/migrate-js-config.ts | 121 ++++++++++++++++++ .../src/template/prepare-config.ts | 3 +- .../src/compat/apply-config-to-theme.ts | 4 +- 5 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 integrations/upgrade/js-config.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/migrate-js-config.ts diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts new file mode 100644 index 000000000000..254d2f6c4bf2 --- /dev/null +++ b/integrations/upgrade/js-config.test.ts @@ -0,0 +1,66 @@ +import { expect } from 'vitest' +import { css, json, test, ts } from '../utils' + +test( + `upgrades a simple JS config file to CSS`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + content: ['./src/**/*.{html,js}'], + theme: { + boxShadow: { + sm: '0 2px 6px rgb(15 23 42 / 0.08)', + }, + colors: { + red: { + 500: '#ef4444', + }, + }, + fontSize: { + xs: ['0.75rem', { lineHeight: '1rem' }], + sm: ['0.875rem', { lineHeight: '1.5rem' }], + base: ['1rem', { lineHeight: '2rem' }], + }, + extend: { + colors: { + red: { + 600: '#dc2626', + }, + }, + fontFamily: { + sans: 'Inter, system-ui, sans-serif', + display: ['Cabinet Grotesk', ...defaultTheme.fontFamily.sans], + }, + borderRadius: { + '4xl': '2rem', + }, + }, + }, + plugins: [], + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs }) => { + console.log(await exec('npx @tailwindcss/upgrade')) + + await fs.expectFileToContain('src/input.css', css` @import 'tailwindcss'; `) + expect(fs.read('tailwind.config.ts')).rejects.toMatchInlineSnapshot() + }, +) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 6bf078df4962..6f579d53be0b 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -11,6 +11,7 @@ import { migrate as migrateStylesheet, split as splitStylesheets, } from './migrate' +import { migrateJsConfig } from './migrate-js-config' import { migratePostCSSConfig } from './migrate-postcss' import { Stylesheet } from './stylesheet' import { migrate as migrateTemplate } from './template/migrate' @@ -81,6 +82,14 @@ async function run() { success('Template migration complete.') } + { + // Migrate JS config + + info('Migrating JavaScript configuration files using the provided configuration file.') + + await migrateJsConfig(config.configFilePath) + } + { // Stylesheet migrations diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts new file mode 100644 index 000000000000..c246bcc7487a --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -0,0 +1,121 @@ +import fs from 'node:fs/promises' +import { dirname } from 'path' +import type { Config } from 'tailwindcss' +import { fileURLToPath } from 'url' +import { loadModule } from '../../@tailwindcss-node/src/compile' +import { + keyPathToCssProperty, + themeableValues, +} from '../../tailwindcss/src/compat/apply-config-to-theme' +import { info } from './utils/renderer' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +export async function migrateJsConfig(fullConfigPath: string): Promise { + let [unresolvedConfig, source] = await Promise.all([ + loadModule(fullConfigPath, __dirname, () => {}).then((result) => result.module) as Config, + fs.readFile(fullConfigPath, 'utf-8'), + ]) + + if (!isSimpleConfig(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.', + ) + return + } + + let cssConfigs: string[] = [] + + if ('content' in unresolvedConfig) { + cssConfigs.push(migrateContent(unresolvedConfig as any)) + } + + if ('theme' in unresolvedConfig) { + cssConfigs.push(await migrateTheme(unresolvedConfig as any)) + } + + console.log(cssConfigs.join('\n')) +} + +async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise { + let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme + + let resetNamespaces = new Set() + + let css = `@theme reference inline {\n` + for (let [key, value] of themeableValues(overwriteTheme)) { + if (typeof value !== 'string' && typeof value !== 'number') { + continue + } + + if (!resetNamespaces.has(key[0])) { + resetNamespaces.add(key[0]) + css += ` --${keyPathToCssProperty([key[0]])}-*: initial;\n` + } + + css += ` --${keyPathToCssProperty(key)}: ${value};\n` + } + + for (let [key, value] of themeableValues(extendTheme)) { + if (typeof value !== 'string' && typeof value !== 'number') { + continue + } + + css += ` --${keyPathToCssProperty(key)}: ${value};\n` + } + + return css + '}\n' +} + +function migrateContent(unresolvedConfig: Config & { content: any }): string { + let css = '' + for (let content of unresolvedConfig.content) { + if (typeof content !== 'string') { + throw new Error('Unsupported content value: ' + content) + } + + css += `@source "${content}";\n` + } + return css +} + +// Applies heuristics to determine if we can attempt to migrate the config +async function isSimpleConfig(unresolvedConfig: Config, source: string): Promise { + // The file may not contain any functions + if (source.includes('function') || source.includes(' => ')) { + return false + } + + // The file may not contain non-serializable values + const isSimpleValue = (value: unknown): boolean => { + if (typeof value === 'function') return false + if (Array.isArray(value)) return value.every(isSimpleValue) + if (typeof value === 'object' && value !== null) { + return Object.values(value).every(isSimpleValue) + } + return ['string', 'number', 'boolean', 'undefined'].includes(typeof value) + } + if (!isSimpleValue(unresolvedConfig)) { + return false + } + + // The file may only contain known-migrateable high-level properties + const knownProperties = ['content', 'theme', 'plugins', 'presets'] + 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 + } + + // The file may not contain any imports + if (source.includes('import') || source.includes('require')) { + return false + } + + return true +} diff --git a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts index b76f8990b2c5..9cdb95d4980c 100644 --- a/packages/@tailwindcss-upgrade/src/template/prepare-config.ts +++ b/packages/@tailwindcss-upgrade/src/template/prepare-config.ts @@ -35,8 +35,7 @@ export async function prepareConfig( // required so that the base for Tailwind CSS can bet inside the // @tailwindcss-upgrade package and we can require `tailwindcss` properly. let fullConfigPath = path.resolve(options.base, configPath) - let fullFilePath = path.resolve(__dirname) - let relative = path.relative(fullFilePath, fullConfigPath) + let relative = path.relative(__dirname, fullConfigPath) // If the path points to a file in the same directory, `path.relative` will // remove the leading `./` and we need to add it back in order to still diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index c80e8259cf65..18abe85e9448 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -81,7 +81,7 @@ export function applyConfigToTheme(designSystem: DesignSystem, { theme }: Resolv return theme } -function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][] { +export function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][] { let toAdd: [string[], unknown][] = [] walk(config as any, [], (value, path) => { @@ -110,7 +110,7 @@ function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][] return toAdd } -function keyPathToCssProperty(path: string[]) { +export function keyPathToCssProperty(path: string[]) { if (path[0] === 'colors') path[0] = 'color' if (path[0] === 'screens') path[0] = 'breakpoint' From 463d3b82e464f5c09b0ad817ad0b40267e13bfc7 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 10 Oct 2024 14:36:10 +0200 Subject: [PATCH 02/16] Section the CSS theme value so that it's nicer to read --- .../src/migrate-js-config.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index c246bcc7487a..d3e99da07c76 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -42,6 +42,7 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme let resetNamespaces = new Set() + let prevSectionKey = '' let css = `@theme reference inline {\n` for (let [key, value] of themeableValues(overwriteTheme)) { @@ -49,6 +50,12 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< continue } + let sectionKey = createSectionKey(key) + if (sectionKey !== prevSectionKey) { + css += `\n` + prevSectionKey = sectionKey + } + if (!resetNamespaces.has(key[0])) { resetNamespaces.add(key[0]) css += ` --${keyPathToCssProperty([key[0]])}-*: initial;\n` @@ -62,12 +69,32 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< continue } + let sectionKey = createSectionKey(key) + if (sectionKey !== prevSectionKey) { + css += `\n` + prevSectionKey = sectionKey + } + css += ` --${keyPathToCssProperty(key)}: ${value};\n` } return css + '}\n' } +// Returns a string identifier used to section theme declarations +function createSectionKey(key: string[]): string { + let sectionSegments = [] + for (let i = 0; i < key.length - 1; i++) { + let segment = key[i] + // ignore tuples + if (key[i + 1][0] === '-') { + break + } + sectionSegments.push(segment) + } + return sectionSegments.join('-') +} + function migrateContent(unresolvedConfig: Config & { content: any }): string { let css = '' for (let content of unresolvedConfig.content) { @@ -112,10 +139,5 @@ async function isSimpleConfig(unresolvedConfig: Config, source: string): Promise return false } - // The file may not contain any imports - if (source.includes('import') || source.includes('require')) { - return false - } - return true } From e6acf4a96c8e3942cee34d8336186ebeecf1701d Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 10 Oct 2024 14:45:08 +0200 Subject: [PATCH 03/16] Add darkMode migration --- integrations/upgrade/js-config.test.ts | 1 + .../src/migrate-js-config.ts | 19 ++++++++++++++++++- packages/tailwindcss/src/compat/dark-mode.ts | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 254d2f6c4bf2..c3db88ec0c6e 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -17,6 +17,7 @@ test( import defaultTheme from 'tailwindcss/defaultTheme' module.exports = { + darkMode: 'selector', content: ['./src/**/*.{html,js}'], theme: { boxShadow: { diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index d3e99da07c76..2bd49e77a18f 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -7,6 +7,7 @@ import { keyPathToCssProperty, themeableValues, } from '../../tailwindcss/src/compat/apply-config-to-theme' +import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode' import { info } from './utils/renderer' const __filename = fileURLToPath(import.meta.url) @@ -27,6 +28,10 @@ export async function migrateJsConfig(fullConfigPath: string): Promise { let cssConfigs: string[] = [] + if ('darkMode' in unresolvedConfig) { + cssConfigs.push(migrateDarkMode(unresolvedConfig as any)) + } + if ('content' in unresolvedConfig) { cssConfigs.push(migrateContent(unresolvedConfig as any)) } @@ -81,6 +86,18 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< return css + '}\n' } +function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string { + let variant: string = '' + let addVariant = (_name: string, _variant: string) => (variant = _variant) + let config = () => unresolvedConfig.darkMode + darkModePlugin({ config, addVariant }) + + if (variant === '') { + return '' + } + return `@variant dark (${variant});\n` +} + // Returns a string identifier used to section theme declarations function createSectionKey(key: string[]): string { let sectionSegments = [] @@ -128,7 +145,7 @@ async function isSimpleConfig(unresolvedConfig: Config, source: string): Promise } // The file may only contain known-migrateable high-level properties - const knownProperties = ['content', 'theme', 'plugins', 'presets'] + const knownProperties = ['darkMode', 'content', 'theme', 'plugins', 'presets'] if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) { return false } diff --git a/packages/tailwindcss/src/compat/dark-mode.ts b/packages/tailwindcss/src/compat/dark-mode.ts index 0f9bc2cdffe1..eceac14984cc 100644 --- a/packages/tailwindcss/src/compat/dark-mode.ts +++ b/packages/tailwindcss/src/compat/dark-mode.ts @@ -1,7 +1,7 @@ import type { ResolvedConfig } from './config/types' import type { PluginAPI } from './plugin-api' -export function darkModePlugin({ addVariant, config }: PluginAPI) { +export function darkModePlugin({ addVariant, config }: Pick) { let darkMode = config('darkMode', null) as ResolvedConfig['darkMode'] let [mode, selector = '.dark'] = Array.isArray(darkMode) ? darkMode : [darkMode] From 4acd80338b0931a1bd62793e9f0910096e69de7e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 10 Oct 2024 17:06:16 +0200 Subject: [PATCH 04/16] Append the generated CSS at the right place --- integrations/upgrade/js-config.test.ts | 39 ++++++++- integrations/utils.ts | 2 +- ...migrate-at-config.ts => migrate-config.ts} | 84 +++++++++++++------ packages/@tailwindcss-upgrade/src/index.ts | 26 +++--- .../src/migrate-js-config.ts | 37 +++++--- packages/@tailwindcss-upgrade/src/migrate.ts | 6 +- 6 files changed, 141 insertions(+), 53 deletions(-) rename packages/@tailwindcss-upgrade/src/codemods/{migrate-at-config.ts => migrate-config.ts} (52%) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index c3db88ec0c6e..8c258c8ada0f 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -59,9 +59,42 @@ test( }, }, async ({ exec, fs }) => { - console.log(await exec('npx @tailwindcss/upgrade')) + await exec('npx @tailwindcss/upgrade') - await fs.expectFileToContain('src/input.css', css` @import 'tailwindcss'; `) - expect(fs.read('tailwind.config.ts')).rejects.toMatchInlineSnapshot() + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @source './**/*.{html,js}'; + + @variant dark (&:where(.dark, .dark *)); + + @theme { + --box-shadow-*: initial; + --box-shadow-sm: 0 2px 6px rgb(15 23 42 / 0.08); + + --color-*: initial; + --color-red-500: #ef4444; + + --font-size-*: initial; + --font-size-xs: 0.75rem; + --font-size-xs--line-height: 1rem; + --font-size-sm: 0.875rem; + --font-size-sm--line-height: 1.5rem; + --font-size-base: 1rem; + --font-size-base--line-height: 2rem; + + --color-red-600: #dc2626; + + --font-family-sans: Inter, system-ui, sans-serif; + --font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + + --border-radius-4xl: 2rem; + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('') }, ) diff --git a/integrations/utils.ts b/integrations/utils.ts index 2afc35ed038a..d9242ab0425f 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -75,7 +75,7 @@ export function test( ) { return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)( name, - { timeout: TEST_TIMEOUT, retry: debug || only ? 0 : 3 }, + { timeout: TEST_TIMEOUT, retry: process.env.CI ? 2 : 0 }, async (options) => { let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT await fs.mkdir(rootDir, { recursive: true }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts similarity index 52% rename from packages/@tailwindcss-upgrade/src/codemods/migrate-at-config.ts rename to packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts index 6cfc6fd563c7..02b593854cca 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -1,12 +1,16 @@ import path from 'node:path' -import { AtRule, type Plugin, type Root } from 'postcss' +import { AtRule, type Plugin, Root } from 'postcss' import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path' +import type { JSConfigMigration } from '../migrate-js-config' import type { Stylesheet } from '../stylesheet' import { walk, WalkAction } from '../utils/walk' -export function migrateAtConfig( +export function migrateConfig( sheet: Stylesheet, - { configFilePath }: { configFilePath: string }, + { + configFilePath, + jsConfigMigration, + }: { configFilePath: string; jsConfigMigration: JSConfigMigration }, ): Plugin { function injectInto(sheet: Stylesheet) { let root = sheet.root @@ -14,27 +18,43 @@ export function migrateAtConfig( // We don't have a sheet with a file path if (!sheet.file) return - // Skip if there is already a `@config` directive - { - let hasConfig = false - root.walkAtRules('config', () => { - hasConfig = true - return false - }) - if (hasConfig) return - } + let cssConfig = new AtRule() + cssConfig.raws.tailwind_pretty = true + + if (jsConfigMigration === null) { + // Skip if there is already a `@config` directive + { + let hasConfig = false + root.walkAtRules('config', () => { + hasConfig = true + return false + }) + if (hasConfig) return + } - // Figure out the path to the config file - let sheetPath = sheet.file - let configPath = configFilePath + cssConfig.append( + new AtRule({ + name: 'config', + params: `'${relativeToStylesheet(sheet, configFilePath)}'`, + }), + ) + } else { + for (let source of jsConfigMigration.sources) { + let absolute = path.resolve(source.base, source.pattern) + cssConfig.append( + new AtRule({ + name: 'source', + params: `'${relativeToStylesheet(sheet, absolute)}'`, + }), + ) + } + + if (jsConfigMigration.css.nodes.length > 0 && jsConfigMigration.sources.length > 0) { + jsConfigMigration.css.nodes[0].raws.before = '\n\n' + } - let relative = path.relative(path.dirname(sheetPath), configPath) - if (relative[0] !== '.') { - relative = `./${relative}` + cssConfig.append(jsConfigMigration.css.nodes) } - // Ensure relative is a posix style path since we will merge it with the - // glob. - relative = normalizePath(relative) // Inject the `@config` in a sensible place // 1. Below the last `@import` @@ -49,12 +69,10 @@ export function migrateAtConfig( return WalkAction.Skip }) - let configNode = new AtRule({ name: 'config', params: `'${relative}'` }) - if (!locationNode) { - root.prepend(configNode) + root.prepend(cssConfig.nodes) } else if (locationNode.name === 'import') { - locationNode.after(configNode) + locationNode.after(cssConfig.nodes) } } @@ -95,7 +113,21 @@ export function migrateAtConfig( } return { - postcssPlugin: '@tailwindcss/upgrade/migrate-at-config', + postcssPlugin: '@tailwindcss/upgrade/migrate-config', OnceExit: migrate, } } + +function relativeToStylesheet(sheet: Stylesheet, absolute: string) { + if (!sheet.file) throw new Error('Can not find a path for the stylesheet') + + let sheetPath = sheet.file + + let relative = path.relative(path.dirname(sheetPath), absolute) + if (relative[0] !== '.') { + relative = `./${relative}` + } + // 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/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 6f579d53be0b..2c2973c4a28a 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -38,6 +38,8 @@ if (flags['--help']) { } async function run() { + let base = process.cwd() + eprintln(header()) eprintln() @@ -51,7 +53,7 @@ async function run() { } } - let config = await prepareConfig(flags['--config'], { base: process.cwd() }) + let config = await prepareConfig(flags['--config'], { base }) { // Template migrations @@ -82,19 +84,16 @@ async function run() { success('Template migration complete.') } - { - // Migrate JS config - - info('Migrating JavaScript configuration files using the provided configuration file.') + // Migrate JS config - await migrateJsConfig(config.configFilePath) - } + info('Migrating JavaScript configuration files using the provided configuration file.') + let jsConfigMigration = await migrateJsConfig(config.configFilePath, base) { // Stylesheet migrations // Use provided files - let files = flags._.map((file) => path.resolve(process.cwd(), file)) + let files = flags._.map((file) => path.resolve(base, file)) // Discover CSS files in case no files were provided if (files.length === 0) { @@ -134,7 +133,7 @@ async function run() { // Migrate each file let migrateResults = await Promise.allSettled( - stylesheets.map((sheet) => migrateStylesheet(sheet, config)), + stylesheets.map((sheet) => migrateStylesheet(sheet, { ...config, jsConfigMigration })), ) for (let result of migrateResults) { @@ -167,14 +166,19 @@ async function run() { { // PostCSS config migration - await migratePostCSSConfig(process.cwd()) + await migratePostCSSConfig(base) } try { // Upgrade Tailwind CSS - await pkg('add tailwindcss@next', process.cwd()) + await pkg('add tailwindcss@next', base) } catch {} + // Remove the JS config if it was fully migrated + if (jsConfigMigration !== null) { + await fs.rm(config.configFilePath) + } + // Figure out if we made any changes if (isRepoDirty()) { success('Verify the changes and commit them to your repository.') diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 2bd49e77a18f..b5c99c669f7f 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -1,5 +1,6 @@ import fs from 'node:fs/promises' import { dirname } from 'path' +import postcss from 'postcss' import type { Config } from 'tailwindcss' import { fileURLToPath } from 'url' import { loadModule } from '../../@tailwindcss-node/src/compile' @@ -13,7 +14,17 @@ import { info } from './utils/renderer' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) -export async function migrateJsConfig(fullConfigPath: string): Promise { +export type JSConfigMigration = + // Could not convert the config file, need to inject it as-is in a @config directive + null | { + sources: { base: string; pattern: string }[] + css: postcss.Root + } + +export async function migrateJsConfig( + fullConfigPath: string, + base: string, +): Promise { let [unresolvedConfig, source] = await Promise.all([ loadModule(fullConfigPath, __dirname, () => {}).then((result) => result.module) as Config, fs.readFile(fullConfigPath, 'utf-8'), @@ -23,9 +34,10 @@ export async function migrateJsConfig(fullConfigPath: string): Promise { 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.', ) - return + return null } + let sources: { base: string; pattern: string }[] = [] let cssConfigs: string[] = [] if ('darkMode' in unresolvedConfig) { @@ -33,14 +45,17 @@ export async function migrateJsConfig(fullConfigPath: string): Promise { } if ('content' in unresolvedConfig) { - cssConfigs.push(migrateContent(unresolvedConfig as any)) + sources = migrateContent(unresolvedConfig as any, base) } if ('theme' in unresolvedConfig) { cssConfigs.push(await migrateTheme(unresolvedConfig as any)) } - console.log(cssConfigs.join('\n')) + return { + sources, + css: postcss.parse(cssConfigs.join('\n')), + } } async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise { @@ -49,7 +64,7 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< let resetNamespaces = new Set() let prevSectionKey = '' - let css = `@theme reference inline {\n` + let css = `@theme {` for (let [key, value] of themeableValues(overwriteTheme)) { if (typeof value !== 'string' && typeof value !== 'number') { continue @@ -112,16 +127,18 @@ function createSectionKey(key: string[]): string { return sectionSegments.join('-') } -function migrateContent(unresolvedConfig: Config & { content: any }): string { - let css = '' +function migrateContent( + unresolvedConfig: Config & { content: any }, + base: string, +): { base: string; pattern: string }[] { + let sources = [] for (let content of unresolvedConfig.content) { if (typeof content !== 'string') { throw new Error('Unsupported content value: ' + content) } - - css += `@source "${content}";\n` + sources.push({ base, pattern: content }) } - return css + return sources } // Applies heuristics to determine if we can attempt to migrate the config diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index 7ce0c1bb1f6a..450fc528c007 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -5,11 +5,12 @@ import type { DesignSystem } from '../../tailwindcss/src/design-system' import { DefaultMap } from '../../tailwindcss/src/utils/default-map' import { segment } from '../../tailwindcss/src/utils/segment' import { migrateAtApply } from './codemods/migrate-at-apply' -import { migrateAtConfig } from './codemods/migrate-at-config' import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities' +import { migrateConfig } from './codemods/migrate-config' import { migrateMediaScreen } from './codemods/migrate-media-screen' import { migrateMissingLayers } from './codemods/migrate-missing-layers' import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives' +import type { JSConfigMigration } from './migrate-js-config' import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet' import { resolveCssId } from './utils/resolve' import { walk, WalkAction } from './utils/walk' @@ -19,6 +20,7 @@ export interface MigrateOptions { designSystem: DesignSystem userConfig: Config configFilePath: string + jsConfigMigration: JSConfigMigration } export async function migrateContents( @@ -37,7 +39,7 @@ export async function migrateContents( .use(migrateAtLayerUtilities(stylesheet)) .use(migrateMissingLayers()) .use(migrateTailwindDirectives(options)) - .use(migrateAtConfig(stylesheet, options)) + .use(migrateConfig(stylesheet, options)) .process(stylesheet.root, { from: stylesheet.file ?? undefined }) } From f377dabc8409138bfab8693bec35e8003d66cf1d Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 10 Oct 2024 17:29:58 +0200 Subject: [PATCH 05/16] Properly migrate borderRadius to --radius --- packages/tailwindcss/src/compat/apply-config-to-theme.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 18abe85e9448..2e26697d28d4 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -113,6 +113,7 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk export function keyPathToCssProperty(path: string[]) { if (path[0] === 'colors') path[0] = 'color' if (path[0] === 'screens') path[0] = 'breakpoint' + if (path[0] === 'borderRadius') path[0] = 'radius' return ( path From a2d8bcbd9b18f83bf07262047d20e112927bdab7 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 10 Oct 2024 17:45:03 +0200 Subject: [PATCH 06/16] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88314a4ffc0f..a5d28326e01b 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 - _Upgrade (experimental)_: Migrate v3 PostCSS setups to v4 in some cases ([#14612](https://github.com/tailwindlabs/tailwindcss/pull/14612)) - _Upgrade (experimental)_: The upgrade tool now automatically discovers your JavaScript config ([#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)) ### Fixed From 15a785ac1cc9df69079e0dd1d727fb7070d57da4 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 10 Oct 2024 17:55:18 +0200 Subject: [PATCH 07/16] Fix unit tests --- packages/@tailwindcss-upgrade/src/index.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts index e45507e70219..655eae25bbde 100644 --- a/packages/@tailwindcss-upgrade/src/index.test.ts +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -20,6 +20,7 @@ let config = { userConfig: {}, newPrefix: null, configFilePath: path.resolve(__dirname, './tailwind.config.js'), + jsConfigMigration: null, } function migrate(input: string, config: any) { From 45f7aff8419a10a9a7c0588f3334dfa96508c657 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 10 Oct 2024 18:01:05 +0200 Subject: [PATCH 08/16] Remove unnecesary async part --- packages/@tailwindcss-upgrade/src/migrate-js-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index b5c99c669f7f..a341925f3287 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -142,7 +142,7 @@ function migrateContent( } // Applies heuristics to determine if we can attempt to migrate the config -async function isSimpleConfig(unresolvedConfig: Config, source: string): Promise { +function isSimpleConfig(unresolvedConfig: Config, source: string): boolean { // The file may not contain any functions if (source.includes('function') || source.includes(' => ')) { return false From bcba32d6c3ade5cf38c7270fc98db677b407d6df Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 11:27:36 +0200 Subject: [PATCH 09/16] Apply suggestions from code review Co-authored-by: Robin Malfait --- packages/@tailwindcss-upgrade/src/migrate-js-config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index a341925f3287..ded73341579f 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -149,7 +149,7 @@ function isSimpleConfig(unresolvedConfig: Config, source: string): boolean { } // The file may not contain non-serializable values - const isSimpleValue = (value: unknown): boolean => { + function isSimpleValue (value: unknown): boolean { if (typeof value === 'function') return false if (Array.isArray(value)) return value.every(isSimpleValue) if (typeof value === 'object' && value !== null) { @@ -161,8 +161,8 @@ function isSimpleConfig(unresolvedConfig: Config, source: string): boolean { return false } - // The file may only contain known-migrateable high-level properties - const knownProperties = ['darkMode', 'content', 'theme', 'plugins', 'presets'] + // The file may only contain known-migrateable top-level properties + let knownProperties = ['darkMode', 'content', 'theme', 'plugins', 'presets'] if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) { return false } From c074f3b226715697252bd8d0c4f2dbd6bd50f03a Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 11:35:37 +0200 Subject: [PATCH 10/16] Merge all keys correctly --- integrations/upgrade/js-config.test.ts | 12 ++++--- .../src/migrate-js-config.ts | 35 +++++++++++-------- .../src/compat/config/resolve-config.ts | 2 +- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 8c258c8ada0f..9d91feeeaab1 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -18,14 +18,14 @@ test( module.exports = { darkMode: 'selector', - content: ['./src/**/*.{html,js}'], + content: ['./src/**/*.{html,js}', './my-app/**/*.{html,js}'], theme: { boxShadow: { sm: '0 2px 6px rgb(15 23 42 / 0.08)', }, colors: { red: { - 500: '#ef4444', + 500: 'red', }, }, fontSize: { @@ -36,6 +36,7 @@ test( extend: { colors: { red: { + 500: '#ef4444', 600: '#dc2626', }, }, @@ -68,6 +69,8 @@ test( @source './**/*.{html,js}'; + @source '../my-app/**/*.{html,js}'; + @variant dark (&:where(.dark, .dark *)); @theme { @@ -76,6 +79,7 @@ test( --color-*: initial; --color-red-500: #ef4444; + --color-red-600: #dc2626; --font-size-*: initial; --font-size-xs: 0.75rem; @@ -85,12 +89,10 @@ test( --font-size-base: 1rem; --font-size-base--line-height: 2rem; - --color-red-600: #dc2626; - --font-family-sans: Inter, system-ui, sans-serif; --font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --border-radius-4xl: 2rem; + --radius-4xl: 2rem; } " `) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index ded73341579f..202a65161ebb 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -8,6 +8,8 @@ import { keyPathToCssProperty, themeableValues, } 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 { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode' import { info } from './utils/renderer' @@ -61,30 +63,28 @@ export async function migrateJsConfig( async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise { let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme - let resetNamespaces = new Set() - let prevSectionKey = '' - - let css = `@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 for (let [key, value] of themeableValues(overwriteTheme)) { if (typeof value !== 'string' && typeof value !== 'number') { continue } - let sectionKey = createSectionKey(key) - if (sectionKey !== prevSectionKey) { - css += `\n` - prevSectionKey = sectionKey - } - if (!resetNamespaces.has(key[0])) { - resetNamespaces.add(key[0]) - css += ` --${keyPathToCssProperty([key[0]])}-*: initial;\n` + resetNamespaces.set(key[0], false) + // css += ` --${keyPathToCssProperty([key[0]])}-*: initial;\n` } - css += ` --${keyPathToCssProperty(key)}: ${value};\n` + // css += ` --${keyPathToCssProperty(key)}: ${value};\n` } - for (let [key, value] of themeableValues(extendTheme)) { + let themeValues = deepMerge({}, [overwriteTheme, extendTheme], mergeThemeExtension) + + let prevSectionKey = '' + + let css = `@theme {` + for (let [key, value] of themeableValues(themeValues)) { if (typeof value !== 'string' && typeof value !== 'number') { continue } @@ -95,6 +95,11 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< prevSectionKey = sectionKey } + if (resetNamespaces.has(key[0]) && resetNamespaces.get(key[0]) === false) { + resetNamespaces.set(key[0], true) + css += ` --${keyPathToCssProperty([key[0]])}-*: initial;\n` + } + css += ` --${keyPathToCssProperty(key)}: ${value};\n` } @@ -149,7 +154,7 @@ function isSimpleConfig(unresolvedConfig: Config, source: string): boolean { } // The file may not contain non-serializable values - function isSimpleValue (value: unknown): boolean { + function isSimpleValue(value: unknown): boolean { if (typeof value === 'function') return false if (Array.isArray(value)) return value.every(isSimpleValue) if (typeof value === 'object' && value !== null) { diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 81aa4dacada8..2d8a86ff6176 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -87,7 +87,7 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv } } -function mergeThemeExtension( +export function mergeThemeExtension( themeValue: ThemeValue | ThemeValue[], extensionValue: ThemeValue | ThemeValue[], ) { From 5e26bc890b95fdd3a2e7af6650ddac611cdcf266 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 11:42:34 +0200 Subject: [PATCH 11/16] Fix line breaks by keeping everything a string until it's necessary to convert --- integrations/upgrade/js-config.test.ts | 1 - .../src/codemods/migrate-config.ts | 16 ++++++---------- .../src/migrate-js-config.ts | 8 ++------ 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 9d91feeeaab1..6b6f3023f3ad 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -68,7 +68,6 @@ test( @import 'tailwindcss'; @source './**/*.{html,js}'; - @source '../my-app/**/*.{html,js}'; @variant dark (&:where(.dark, .dark *)); diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts index 02b593854cca..8fb4a983b06e 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -1,5 +1,5 @@ import path from 'node:path' -import { AtRule, type Plugin, Root } from 'postcss' +import postcss, { AtRule, type Plugin, Root } from 'postcss' import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path' import type { JSConfigMigration } from '../migrate-js-config' import type { Stylesheet } from '../stylesheet' @@ -39,21 +39,17 @@ export function migrateConfig( }), ) } else { + let css = '\n\n' for (let source of jsConfigMigration.sources) { let absolute = path.resolve(source.base, source.pattern) - cssConfig.append( - new AtRule({ - name: 'source', - params: `'${relativeToStylesheet(sheet, absolute)}'`, - }), - ) + css += `@source '${relativeToStylesheet(sheet, absolute)}';\n` } - if (jsConfigMigration.css.nodes.length > 0 && jsConfigMigration.sources.length > 0) { - jsConfigMigration.css.nodes[0].raws.before = '\n\n' + if (jsConfigMigration.sources.length > 0) { + css = css + '\n' } - cssConfig.append(jsConfigMigration.css.nodes) + cssConfig.append(postcss.parse(css + jsConfigMigration.css)) } // Inject the `@config` in a sensible place diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 202a65161ebb..bbf847628732 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -1,6 +1,5 @@ import fs from 'node:fs/promises' import { dirname } from 'path' -import postcss from 'postcss' import type { Config } from 'tailwindcss' import { fileURLToPath } from 'url' import { loadModule } from '../../@tailwindcss-node/src/compile' @@ -20,7 +19,7 @@ export type JSConfigMigration = // Could not convert the config file, need to inject it as-is in a @config directive null | { sources: { base: string; pattern: string }[] - css: postcss.Root + css: string } export async function migrateJsConfig( @@ -56,7 +55,7 @@ export async function migrateJsConfig( return { sources, - css: postcss.parse(cssConfigs.join('\n')), + css: cssConfigs.join('\n'), } } @@ -73,10 +72,7 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< if (!resetNamespaces.has(key[0])) { resetNamespaces.set(key[0], false) - // css += ` --${keyPathToCssProperty([key[0]])}-*: initial;\n` } - - // css += ` --${keyPathToCssProperty(key)}: ${value};\n` } let themeValues = deepMerge({}, [overwriteTheme, extendTheme], mergeThemeExtension) From 1cc9e6da8d55b7f095de0f365cfea6fa1206395b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 11:47:11 +0200 Subject: [PATCH 12/16] Add test to ensure values are correctly merged --- integrations/upgrade/js-config.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 6b6f3023f3ad..2fdc1fe5520a 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -25,6 +25,7 @@ test( }, colors: { red: { + 400: '#f87171', 500: 'red', }, }, @@ -77,6 +78,7 @@ test( --box-shadow-sm: 0 2px 6px rgb(15 23 42 / 0.08); --color-*: initial; + --color-red-400: #f87171; --color-red-500: #ef4444; --color-red-600: #dc2626; From 8d86de65cc1987959bb83b2efb516c3f4e0e8113 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 11:58:03 +0200 Subject: [PATCH 13/16] Upgrade all snapshots --- integrations/upgrade/index.test.ts | 46 +++++++++---------- .../src/migrate-js-config.ts | 9 +++- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 195c5e20d422..8e1269e1c8fe 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -40,7 +40,8 @@ test( --- ./src/input.css --- @import 'tailwindcss'; - @config '../tailwind.config.js'; + + @source './**/*.{html,js}'; " `) @@ -71,8 +72,9 @@ test( } `, 'src/index.html': html` -

🤠👋

-
+
`, 'src/input.css': css` @tailwind base; @@ -91,13 +93,14 @@ test( expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` " --- ./src/index.html --- -

🤠👋

-
+
--- ./src/input.css --- @import 'tailwindcss' prefix(tw); - @config '../tailwind.config.js'; + @source './**/*.{html,js}'; .btn { @apply tw:rounded-md! tw:px-2 tw:py-1 tw:bg-blue-500 tw:text-white; @@ -145,8 +148,6 @@ test( --- ./src/index.css --- @import 'tailwindcss'; - @config '../tailwind.config.js'; - .a { @apply flex; } @@ -201,8 +202,6 @@ test( --- ./src/index.css --- @import 'tailwindcss'; - @config '../tailwind.config.js'; - @layer base { html { color: #333; @@ -262,8 +261,6 @@ test( --- ./src/index.css --- @import 'tailwindcss'; - @config '../tailwind.config.js'; - @utility btn { @apply rounded-md px-2 py-1 bg-blue-500 text-white; } @@ -631,7 +628,6 @@ test( --- ./src/index.css --- @import 'tailwindcss'; @import './utilities.css'; - @config '../tailwind.config.js'; --- ./src/utilities.css --- @utility no-scrollbar { @@ -748,7 +744,6 @@ test( @import './c.1.css' layer(utilities); @import './c.1.utilities.css'; @import './d.1.css'; - @config '../tailwind.config.js'; --- ./src/a.1.css --- @import './a.1.utilities.css' @@ -882,17 +877,14 @@ test( --- ./src/root.1.css --- @import 'tailwindcss/utilities' layer(utilities); @import './a.1.css' layer(utilities); - @config '../tailwind.config.js'; --- ./src/root.2.css --- @import 'tailwindcss/utilities' layer(utilities); @import './a.1.css' layer(components); - @config '../tailwind.config.js'; --- ./src/root.3.css --- @import 'tailwindcss/utilities' layer(utilities); @import './a.1.css' layer(utilities); - @config '../tailwind.config.js'; " `) }, @@ -915,8 +907,9 @@ test( } `, 'src/index.html': html` -

🤠👋

-
+
`, 'src/root.1.css': css` /* Inject missing @config */ @@ -968,23 +961,25 @@ test( expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(` " --- ./src/index.html --- -

🤠👋

-
+
--- ./src/root.1.css --- /* Inject missing @config */ @import 'tailwindcss'; - @config '../tailwind.config.ts'; + @source './**/*.{html,js}'; --- ./src/root.2.css --- /* Already contains @config */ @import 'tailwindcss'; + @source './**/*.{html,js}'; @config "../tailwind.config.js"; --- ./src/root.3.css --- /* Inject missing @config above first @theme */ @import 'tailwindcss'; - @config '../tailwind.config.ts'; + @source './**/*.{html,js}'; @variant hocus (&:hover, &:focus); @@ -1000,7 +995,8 @@ test( /* Inject missing @config due to nested imports with tailwind imports */ @import './root.4/base.css'; @import './root.4/utilities.css'; - @config '../tailwind.config.ts'; + @source './**/*.{html,js}'; + @source './**/*.{html,js}'; --- ./src/root.5.css --- @import './root.5/tailwind.css'; @@ -1015,7 +1011,7 @@ test( --- ./src/root.5/tailwind.css --- /* Inject missing @config in this file, due to full import */ @import 'tailwindcss'; - @config '../../tailwind.config.ts'; + @source '../**/*.{html,js}'; " `) }, diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index bbf847628732..23b2fc7d936c 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -163,7 +163,14 @@ function isSimpleConfig(unresolvedConfig: Config, source: string): boolean { } // The file may only contain known-migrateable top-level properties - let knownProperties = ['darkMode', 'content', 'theme', 'plugins', 'presets'] + let knownProperties = [ + 'darkMode', + 'content', + 'theme', + 'plugins', + 'presets', + 'prefix', // Prefix is handled in the dedicated prefix migrator + ] if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) { return false } From 7490e11aa9aab268214bb924291a1908bf77816e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 12:07:02 +0200 Subject: [PATCH 14/16] Don't inject a config into the same stylesheet multiple times --- integrations/upgrade/index.test.ts | 6 +++++- .../src/codemods/migrate-config.ts | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 8e1269e1c8fe..305b5a13946a 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -904,6 +904,11 @@ test( 'tailwind.config.ts': js` export default { content: ['./src/**/*.{html,js}'], + plugins: [ + () => { + // custom stuff which is too complicated to migrate to CSS + }, + ], } `, 'src/index.html': html` @@ -996,7 +1001,6 @@ test( @import './root.4/base.css'; @import './root.4/utilities.css'; @source './**/*.{html,js}'; - @source './**/*.{html,js}'; --- ./src/root.5.css --- @import './root.5/tailwind.css'; diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts index 8fb4a983b06e..398f612e2514 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -5,6 +5,8 @@ import type { JSConfigMigration } from '../migrate-js-config' import type { Stylesheet } from '../stylesheet' import { walk, WalkAction } from '../utils/walk' +const ALREADY_INJECTED = new WeakMap() + export function migrateConfig( sheet: Stylesheet, { @@ -13,6 +15,15 @@ export function migrateConfig( }: { configFilePath: string; jsConfigMigration: JSConfigMigration }, ): Plugin { function injectInto(sheet: Stylesheet) { + let alreadyInjected = ALREADY_INJECTED.get(sheet) + if (alreadyInjected && alreadyInjected.includes(configFilePath)) { + return + } else if (alreadyInjected) { + alreadyInjected.push(configFilePath) + } else { + ALREADY_INJECTED.set(sheet, [configFilePath]) + } + let root = sheet.root // We don't have a sheet with a file path From 1d04ce19c12460afb0d9191b62da3baa3bbb698b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 14:22:28 +0200 Subject: [PATCH 15/16] Revert test changes --- integrations/upgrade/index.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 305b5a13946a..97409d53d4e5 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -973,18 +973,17 @@ test( --- ./src/root.1.css --- /* Inject missing @config */ @import 'tailwindcss'; - @source './**/*.{html,js}'; + @config '../tailwind.config.ts'; --- ./src/root.2.css --- /* Already contains @config */ @import 'tailwindcss'; - @source './**/*.{html,js}'; @config "../tailwind.config.js"; --- ./src/root.3.css --- /* Inject missing @config above first @theme */ @import 'tailwindcss'; - @source './**/*.{html,js}'; + @config '../tailwind.config.ts'; @variant hocus (&:hover, &:focus); @@ -1000,7 +999,7 @@ test( /* Inject missing @config due to nested imports with tailwind imports */ @import './root.4/base.css'; @import './root.4/utilities.css'; - @source './**/*.{html,js}'; + @config '../tailwind.config.ts'; --- ./src/root.5.css --- @import './root.5/tailwind.css'; @@ -1015,7 +1014,7 @@ test( --- ./src/root.5/tailwind.css --- /* Inject missing @config in this file, due to full import */ @import 'tailwindcss'; - @source '../**/*.{html,js}'; + @config '../../tailwind.config.ts'; " `) }, From d86af92397aa36d2cb13f64977023d248d857ea6 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 15:15:41 +0200 Subject: [PATCH 16/16] Add example for not migarting complex configs --- integrations/upgrade/js-config.test.ts | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 2fdc1fe5520a..5ef1f55b2bb4 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -101,3 +101,52 @@ test( expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('') }, ) + +test( + `does not upgrade a complex JS config file to CSS`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + plugins: [function complexConfig() {}], + } 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')).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 { + plugins: [function complexConfig() {}], + } satisfies Config + " + `) + }, +)