Skip to content
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add first draft of new wide-gamut color palette ([#14693](https://github.com/tailwindlabs/tailwindcss/pull/14693))
- _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)_: Migrate `theme(…)` calls in classes to `var(…)` or to the modern `theme(…)` syntax ([#14664](https://github.com/tailwindlabs/tailwindcss/pull/14664))
- _Upgrade (experimental)_: Support migrating JS configurations to CSS that contain functions inside the `theme` object ([#14675](https://github.com/tailwindlabs/tailwindcss/pull/14675))

### Fixed

Expand Down
28 changes: 15 additions & 13 deletions integrations/upgrade/js-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
)

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`
Expand Down Expand Up @@ -226,28 +226,30 @@
async ({ exec, fs }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`

Check failure on line 229 in integrations/upgrade/js-config.test.ts

View workflow job for this annotation

GitHub Actions / tests (20, namespace-profile-default, false)

upgrade/js-config.test.ts > upgrades JS config files with functions in the theme config

Error: Snapshot `upgrades JS config files with functions in the theme config 1` mismatched - Expected + Received " --- src/input.css --- @import 'tailwindcss'; @theme { - --color-gray-50: #fafafa; + --color-gray-50: oklch(0.985 0 none); - --color-gray-100: #f5f5f5; + --color-gray-100: oklch(0.97 0 none); - --color-gray-200: #e5e5e5; + --color-gray-200: oklch(0.922 0 none); - --color-gray-300: #d4d4d4; + --color-gray-300: oklch(0.87 0 none); - --color-gray-400: #a3a3a3; + --color-gray-400: oklch(0.708 0 none); - --color-gray-500: #737373; + --color-gray-500: oklch(0.556 0 none); - --color-gray-600: #525252; + --color-gray-600: oklch(0.439 0 none); - --color-gray-700: #404040; + --color-gray-700: oklch(0.371 0 none); - --color-gray-800: #262626; + --color-gray-800: oklch(0.269 0 none); - --color-gray-900: #171717; + --color-gray-900: oklch(0.205 0 none); - --color-gray-950: #0a0a0a; + --color-gray-950: oklch(0.145 0 none); } " ❯ upgrade/js-config.test.ts:229:53 ❯ utils.ts:394:14

Check failure on line 229 in integrations/upgrade/js-config.test.ts

View workflow job for this annotation

GitHub Actions / tests (20, namespace-profile-default, false)

upgrade/js-config.test.ts > upgrades JS config files with functions in the theme config

Error: Snapshot `upgrades JS config files with functions in the theme config 2` mismatched - Expected + Received " --- src/input.css --- @import 'tailwindcss'; @theme { - --color-gray-50: #fafafa; + --color-gray-50: oklch(0.985 0 none); - --color-gray-100: #f5f5f5; + --color-gray-100: oklch(0.97 0 none); - --color-gray-200: #e5e5e5; + --color-gray-200: oklch(0.922 0 none); - --color-gray-300: #d4d4d4; + --color-gray-300: oklch(0.87 0 none); - --color-gray-400: #a3a3a3; + --color-gray-400: oklch(0.708 0 none); - --color-gray-500: #737373; + --color-gray-500: oklch(0.556 0 none); - --color-gray-600: #525252; + --color-gray-600: oklch(0.439 0 none); - --color-gray-700: #404040; + --color-gray-700: oklch(0.371 0 none); - --color-gray-800: #262626; + --color-gray-800: oklch(0.269 0 none); - --color-gray-900: #171717; + --color-gray-900: oklch(0.205 0 none); - --color-gray-950: #0a0a0a; + --color-gray-950: oklch(0.145 0 none); } " ❯ upgrade/js-config.test.ts:229:53 ❯ utils.ts:394:14

Check failure on line 229 in integrations/upgrade/js-config.test.ts

View workflow job for this annotation

GitHub Actions / tests (20, namespace-profile-default, false)

upgrade/js-config.test.ts > upgrades JS config files with functions in the theme config

Error: Snapshot `upgrades JS config files with functions in the theme config 3` mismatched - Expected + Received " --- src/input.css --- @import 'tailwindcss'; @theme { - --color-gray-50: #fafafa; + --color-gray-50: oklch(0.985 0 none); - --color-gray-100: #f5f5f5; + --color-gray-100: oklch(0.97 0 none); - --color-gray-200: #e5e5e5; + --color-gray-200: oklch(0.922 0 none); - --color-gray-300: #d4d4d4; + --color-gray-300: oklch(0.87 0 none); - --color-gray-400: #a3a3a3; + --color-gray-400: oklch(0.708 0 none); - --color-gray-500: #737373; + --color-gray-500: oklch(0.556 0 none); - --color-gray-600: #525252; + --color-gray-600: oklch(0.439 0 none); - --color-gray-700: #404040; + --color-gray-700: oklch(0.371 0 none); - --color-gray-800: #262626; + --color-gray-800: oklch(0.269 0 none); - --color-gray-900: #171717; + --color-gray-900: oklch(0.205 0 none); - --color-gray-950: #0a0a0a; + --color-gray-950: oklch(0.145 0 none); } " ❯ upgrade/js-config.test.ts:229:53 ❯ utils.ts:394:14
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';

@theme {
--color-gray-50: #fafafa;
--color-gray-100: #f5f5f5;
--color-gray-200: #e5e5e5;
--color-gray-300: #d4d4d4;
--color-gray-400: #a3a3a3;
--color-gray-500: #737373;
--color-gray-600: #525252;
--color-gray-700: #404040;
--color-gray-800: #262626;
--color-gray-900: #171717;
--color-gray-950: #0a0a0a;
}
"
`)

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
"
`)
},
Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 29 additions & 35 deletions packages/@tailwindcss-upgrade/src/migrate-js-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -29,6 +29,7 @@ export type JSConfigMigration =
}

export async function migrateJsConfig(
designSystem: DesignSystem,
fullConfigPath: string,
base: string,
): Promise<JSConfigMigration> {
Expand Down Expand Up @@ -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)
}

Expand All @@ -75,33 +76,27 @@ export async function migrateJsConfig(
}
}

async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise<string | null> {
let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme

let resetNamespaces = new Map<string, boolean>()
// 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<string | null> {
// 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<string, Record<string, unknown>> = deepMerge(
{},
[overwriteTheme, extendTheme],
mergeThemeExtension,
let resetNamespaces = new Map<string, boolean>(
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
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -194,8 +184,9 @@ 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` can contain functions that we attempt to resolve.
// - `plugins` are more complex, we have a special heuristics for them.
let { plugins, theme, ...remainder } = unresolvedConfig
if (!isSimpleValue(remainder)) {
return false
}
Expand Down Expand Up @@ -224,7 +215,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
Expand All @@ -234,14 +224,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
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/tailwindcss/src/compat/apply-config-to-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export function applyConfigToTheme(
{ theme }: ResolvedConfig,
replacedThemeKeys: Set<string>,
) {
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)
Expand Down
Loading