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 diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 195c5e20d422..97409d53d4e5 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'; " `) }, @@ -912,11 +904,17 @@ 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` -

🤠👋

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

🤠👋

-
+
--- ./src/root.1.css --- /* Inject missing @config */ diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts new file mode 100644 index 000000000000..5ef1f55b2bb4 --- /dev/null +++ b/integrations/upgrade/js-config.test.ts @@ -0,0 +1,152 @@ +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 = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}', './my-app/**/*.{html,js}'], + theme: { + boxShadow: { + sm: '0 2px 6px rgb(15 23 42 / 0.08)', + }, + colors: { + red: { + 400: '#f87171', + 500: 'red', + }, + }, + fontSize: { + xs: ['0.75rem', { lineHeight: '1rem' }], + sm: ['0.875rem', { lineHeight: '1.5rem' }], + base: ['1rem', { lineHeight: '2rem' }], + }, + extend: { + colors: { + red: { + 500: '#ef4444', + 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 }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @source './**/*.{html,js}'; + @source '../my-app/**/*.{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-400: #f87171; + --color-red-500: #ef4444; + --color-red-600: #dc2626; + + --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; + + --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"; + + --radius-4xl: 2rem; + } + " + `) + + 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 + " + `) + }, +) 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-at-config.ts deleted file mode 100644 index 6cfc6fd563c7..000000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-config.ts +++ /dev/null @@ -1,101 +0,0 @@ -import path from 'node:path' -import { AtRule, type Plugin, type Root } from 'postcss' -import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path' -import type { Stylesheet } from '../stylesheet' -import { walk, WalkAction } from '../utils/walk' - -export function migrateAtConfig( - sheet: Stylesheet, - { configFilePath }: { configFilePath: string }, -): Plugin { - function injectInto(sheet: Stylesheet) { - let root = sheet.root - - // 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 - } - - // Figure out the path to the config file - let sheetPath = sheet.file - let configPath = configFilePath - - let relative = path.relative(path.dirname(sheetPath), configPath) - if (relative[0] !== '.') { - relative = `./${relative}` - } - // 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` - // 2. At the top of the file - let locationNode = null as AtRule | null - - walk(root, (node) => { - if (node.type === 'atrule' && node.name === 'import') { - locationNode = node - } - - return WalkAction.Skip - }) - - let configNode = new AtRule({ name: 'config', params: `'${relative}'` }) - - if (!locationNode) { - root.prepend(configNode) - } else if (locationNode.name === 'import') { - locationNode.after(configNode) - } - } - - function migrate(root: Root) { - // We can only migrate if there is an `@import "tailwindcss"` (or sub-import) - let hasTailwindImport = false - let hasFullTailwindImport = false - root.walkAtRules('import', (node) => { - if (node.params.match(/['"]tailwindcss['"]/)) { - hasTailwindImport = true - hasFullTailwindImport = true - return false - } else if (node.params.match(/['"]tailwindcss\/.*?['"]/)) { - hasTailwindImport = true - } - }) - - 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 (hasFullTailwindImport || sheet.parents.size <= 0) { - injectInto(sheet) - return - } - - // Otherwise, if we are not the root file, we need to inject the `@config` - // into the root file. - if (sheet.parents.size > 0) { - for (let parent of sheet.ancestors()) { - if (parent.parents.size === 0) { - injectInto(parent) - } - } - } - } - - return { - postcssPlugin: '@tailwindcss/upgrade/migrate-at-config', - OnceExit: migrate, - } -} diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts new file mode 100644 index 000000000000..398f612e2514 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -0,0 +1,140 @@ +import path from 'node:path' +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' +import { walk, WalkAction } from '../utils/walk' + +const ALREADY_INJECTED = new WeakMap() + +export function migrateConfig( + sheet: Stylesheet, + { + configFilePath, + jsConfigMigration, + }: { 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 + if (!sheet.file) 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 + } + + cssConfig.append( + new AtRule({ + name: 'config', + params: `'${relativeToStylesheet(sheet, configFilePath)}'`, + }), + ) + } else { + let css = '\n\n' + for (let source of jsConfigMigration.sources) { + let absolute = path.resolve(source.base, source.pattern) + css += `@source '${relativeToStylesheet(sheet, absolute)}';\n` + } + + if (jsConfigMigration.sources.length > 0) { + css = css + '\n' + } + + 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 + let locationNode = null as AtRule | null + + walk(root, (node) => { + if (node.type === 'atrule' && node.name === 'import') { + locationNode = node + } + + return WalkAction.Skip + }) + + if (!locationNode) { + root.prepend(cssConfig.nodes) + } else if (locationNode.name === 'import') { + locationNode.after(cssConfig.nodes) + } + } + + function migrate(root: Root) { + // We can only migrate if there is an `@import "tailwindcss"` (or sub-import) + let hasTailwindImport = false + let hasFullTailwindImport = false + root.walkAtRules('import', (node) => { + if (node.params.match(/['"]tailwindcss['"]/)) { + hasTailwindImport = true + hasFullTailwindImport = true + return false + } else if (node.params.match(/['"]tailwindcss\/.*?['"]/)) { + hasTailwindImport = true + } + }) + + 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 (hasFullTailwindImport || sheet.parents.size <= 0) { + injectInto(sheet) + return + } + + // Otherwise, if we are not the root file, we need to inject the `@config` + // into the root file. + if (sheet.parents.size > 0) { + for (let parent of sheet.ancestors()) { + if (parent.parents.size === 0) { + injectInto(parent) + } + } + } + } + + return { + 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.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) { diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 6bf078df4962..2c2973c4a28a 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' @@ -37,6 +38,8 @@ if (flags['--help']) { } async function run() { + let base = process.cwd() + eprintln(header()) eprintln() @@ -50,7 +53,7 @@ async function run() { } } - let config = await prepareConfig(flags['--config'], { base: process.cwd() }) + let config = await prepareConfig(flags['--config'], { base }) { // Template migrations @@ -81,11 +84,16 @@ async function run() { success('Template migration complete.') } + // Migrate JS config + + 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) { @@ -125,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) { @@ -158,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 new file mode 100644 index 000000000000..23b2fc7d936c --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -0,0 +1,185 @@ +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 { 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' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +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: string + } + +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'), + ]) + + 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 null + } + + let sources: { base: string; pattern: string }[] = [] + let cssConfigs: string[] = [] + + if ('darkMode' in unresolvedConfig) { + cssConfigs.push(migrateDarkMode(unresolvedConfig as any)) + } + + if ('content' in unresolvedConfig) { + sources = migrateContent(unresolvedConfig as any, base) + } + + if ('theme' in unresolvedConfig) { + cssConfigs.push(await migrateTheme(unresolvedConfig as any)) + } + + return { + sources, + css: cssConfigs.join('\n'), + } +} + +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 + for (let [key, value] of themeableValues(overwriteTheme)) { + if (typeof value !== 'string' && typeof value !== 'number') { + continue + } + + if (!resetNamespaces.has(key[0])) { + resetNamespaces.set(key[0], false) + } + } + + 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 + } + + let sectionKey = createSectionKey(key) + if (sectionKey !== prevSectionKey) { + css += `\n` + 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` + } + + 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 = [] + 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 }, + 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) + } + sources.push({ base, pattern: content }) + } + return sources +} + +// Applies heuristics to determine if we can attempt to migrate the config +function isSimpleConfig(unresolvedConfig: Config, source: string): boolean { + // The file may not contain any functions + if (source.includes('function') || source.includes(' => ')) { + return false + } + + // The file may not contain non-serializable values + 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) { + 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 top-level properties + 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 + } + if (unresolvedConfig.plugins && unresolvedConfig.plugins.length > 0) { + return false + } + if (unresolvedConfig.presets && unresolvedConfig.presets.length > 0) { + return false + } + + return true +} 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 }) } 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..2e26697d28d4 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,9 +110,10 @@ 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' + if (path[0] === 'borderRadius') path[0] = 'radius' return ( path 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[], ) { 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]