diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b32d350c23..8eff9f0e78ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support linear gradient angles as bare values ([#14707](https://github.com/tailwindlabs/tailwindcss/pull/14707)) - Interpolate gradients in OKLCH by default ([#14708](https://github.com/tailwindlabs/tailwindcss/pull/14708)) - _Upgrade (experimental)_: Migrate `theme(…)` calls to `var(…)` or to the modern `theme(…)` syntax ([#14664](https://github.com/tailwindlabs/tailwindcss/pull/14664), [#14695](https://github.com/tailwindlabs/tailwindcss/pull/14695)) +- _Upgrade (experimental)_: Support migrating JS configurations to CSS that contain functions inside the `theme` object ([#14675](https://github.com/tailwindlabs/tailwindcss/pull/14675)) ### Fixed diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 06904b2a486c..75160aaad41d 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -193,7 +193,7 @@ test( ) test( - 'does not upgrade JS config files with functions in the theme config', + 'upgrades JS config files with functions in the theme config', { fs: { 'package.json': json` @@ -230,24 +230,26 @@ test( " --- src/input.css --- @import 'tailwindcss'; - @config '../tailwind.config.ts'; + + @theme { + --color-gray-50: oklch(0.985 0 none); + --color-gray-100: oklch(0.97 0 none); + --color-gray-200: oklch(0.922 0 none); + --color-gray-300: oklch(0.87 0 none); + --color-gray-400: oklch(0.708 0 none); + --color-gray-500: oklch(0.556 0 none); + --color-gray-600: oklch(0.439 0 none); + --color-gray-700: oklch(0.371 0 none); + --color-gray-800: oklch(0.269 0 none); + --color-gray-900: oklch(0.205 0 none); + --color-gray-950: oklch(0.145 0 none); + } " `) 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 " `) }, diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 2c2973c4a28a..f82717413306 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -87,7 +87,7 @@ async function run() { // Migrate JS config info('Migrating JavaScript configuration files using the provided configuration file.') - let jsConfigMigration = await migrateJsConfig(config.configFilePath, base) + let jsConfigMigration = await migrateJsConfig(config.designSystem, config.configFilePath, base) { // Stylesheet migrations diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 8d6f1baaa07a..6ca267e58fae 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -10,10 +10,10 @@ import { themeableValues, } from '../../tailwindcss/src/compat/apply-config-to-theme' import { keyframesToRules } from '../../tailwindcss/src/compat/apply-keyframes-to-theme' -import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge' -import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config' +import { resolveConfig, type ConfigFile } 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 type { DesignSystem } from '../../tailwindcss/src/design-system' import { findStaticPlugins } from './utils/extract-static-plugins' import { info } from './utils/renderer' @@ -29,6 +29,7 @@ export type JSConfigMigration = } export async function migrateJsConfig( + designSystem: DesignSystem, fullConfigPath: string, base: string, ): Promise { @@ -57,7 +58,7 @@ export async function migrateJsConfig( } if ('theme' in unresolvedConfig) { - let themeConfig = await migrateTheme(unresolvedConfig as any) + let themeConfig = await migrateTheme(designSystem, unresolvedConfig, base) if (themeConfig) cssConfigs.push(themeConfig) } @@ -75,33 +76,27 @@ export async function migrateJsConfig( } } -async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise { - let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme - - let resetNamespaces = new Map() - // 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 - } - - if (!resetNamespaces.has(key[0])) { - resetNamespaces.set(key[0], false) - } +async function migrateTheme( + designSystem: DesignSystem, + unresolvedConfig: Config, + base: string, +): Promise { + // Resolve the config file without applying plugins and presets, as these are + // migrated to CSS separately. + let configToResolve: ConfigFile = { + base, + config: { ...unresolvedConfig, plugins: [], presets: undefined }, } + let { resolvedConfig, replacedThemeKeys } = resolveConfig(designSystem, [configToResolve]) - let themeValues: Record> = deepMerge( - {}, - [overwriteTheme, extendTheme], - mergeThemeExtension, + let resetNamespaces = new Map( + Array.from(replacedThemeKeys.entries()).map(([key]) => [key, false]), ) let prevSectionKey = '' - let css = `@theme {` let containsThemeKeys = false - for (let [key, value] of themeableValues(themeValues)) { + for (let [key, value] of themeableValues(resolvedConfig.theme)) { if (typeof value !== 'string' && typeof value !== 'number') { continue } @@ -125,9 +120,9 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise< css += ` --${keyPathToCssProperty(key)}: ${value};\n` } - if ('keyframes' in themeValues) { + if ('keyframes' in resolvedConfig.theme) { containsThemeKeys = true - css += '\n' + keyframesToCss(themeValues.keyframes) + css += '\n' + keyframesToCss(resolvedConfig.theme.keyframes) } if (!containsThemeKeys) { @@ -179,11 +174,6 @@ function migrateContent( // Applies heuristics to determine if we can attempt to migrate the config function canMigrateConfig(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 @@ -194,8 +184,8 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return ['string', 'number', 'boolean', 'undefined'].includes(typeof value) } - // Plugins are more complex, so we have a special heuristics for them. - let { plugins, ...remainder } = unresolvedConfig + // `theme` and `plugins` are handled separately and allowed to be more complex + let { plugins, theme, ...remainder } = unresolvedConfig if (!isSimpleValue(remainder)) { return false } @@ -224,7 +214,6 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { // 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 && !onlyAllowedThemeValues(theme.extend)) return false let { extend: _extend, ...themeCopy } = theme @@ -234,14 +223,18 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return true } -const DEFAULT_THEME_KEYS = [ +const ALLOWED_THEME_KEYS = [ ...Object.keys(defaultTheme), // Used by @tailwindcss/container-queries 'containers', ] +const BLOCKED_THEME_KEYS = ['supports', 'data', 'aria'] function onlyAllowedThemeValues(theme: ThemeConfig): boolean { for (let key of Object.keys(theme)) { - if (!DEFAULT_THEME_KEYS.includes(key)) { + if (!ALLOWED_THEME_KEYS.includes(key)) { + return false + } + if (BLOCKED_THEME_KEYS.includes(key)) { return false } } diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 6e6c245118c6..9edf038e4dc1 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -24,8 +24,8 @@ export function applyConfigToTheme( { theme }: ResolvedConfig, replacedThemeKeys: Set, ) { - for (let resetThemeKey of replacedThemeKeys) { - let name = keyPathToCssProperty([resetThemeKey]) + for (let replacedThemeKey of replacedThemeKeys) { + let name = keyPathToCssProperty([replacedThemeKey]) if (!name) continue designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT)