Skip to content

Commit 0971ead

Browse files
philipp-spiessadamwathanRobinMalfait
authored
Resolve theme keys when migrating JS config to CSS (#14675)
With the changes in #14672, it now becomes trivial to actually resolve the config (while still retaining the reset behavior). This means that we can now convert JS configs that use _functions_, e.g.: ```ts import { type Config } from 'tailwindcss' export default { theme: { extend: { colors: ({ colors }) => ({ gray: colors.neutral, }), }, }, } satisfies Config ``` This becomes: ```css @import 'tailwindcss'; @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; } ``` --------- Co-authored-by: Adam Wathan <[email protected]> Co-authored-by: Robin Malfait <[email protected]>
1 parent edb066e commit 0971ead

File tree

5 files changed

+47
-51
lines changed

5 files changed

+47
-51
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Support linear gradient angles as bare values ([#14707](https://github.com/tailwindlabs/tailwindcss/pull/14707))
1414
- Interpolate gradients in OKLCH by default ([#14708](https://github.com/tailwindlabs/tailwindcss/pull/14708))
1515
- _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))
16+
- _Upgrade (experimental)_: Support migrating JS configurations to CSS that contain functions inside the `theme` object ([#14675](https://github.com/tailwindlabs/tailwindcss/pull/14675))
1617

1718
### Fixed
1819

integrations/upgrade/js-config.test.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ test(
193193
)
194194

195195
test(
196-
'does not upgrade JS config files with functions in the theme config',
196+
'upgrades JS config files with functions in the theme config',
197197
{
198198
fs: {
199199
'package.json': json`
@@ -230,24 +230,26 @@ test(
230230
"
231231
--- src/input.css ---
232232
@import 'tailwindcss';
233-
@config '../tailwind.config.ts';
233+
234+
@theme {
235+
--color-gray-50: oklch(0.985 0 none);
236+
--color-gray-100: oklch(0.97 0 none);
237+
--color-gray-200: oklch(0.922 0 none);
238+
--color-gray-300: oklch(0.87 0 none);
239+
--color-gray-400: oklch(0.708 0 none);
240+
--color-gray-500: oklch(0.556 0 none);
241+
--color-gray-600: oklch(0.439 0 none);
242+
--color-gray-700: oklch(0.371 0 none);
243+
--color-gray-800: oklch(0.269 0 none);
244+
--color-gray-900: oklch(0.205 0 none);
245+
--color-gray-950: oklch(0.145 0 none);
246+
}
234247
"
235248
`)
236249

237250
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
238251
"
239-
--- tailwind.config.ts ---
240-
import { type Config } from 'tailwindcss'
241252
242-
export default {
243-
theme: {
244-
extend: {
245-
colors: ({ colors }) => ({
246-
gray: colors.neutral,
247-
}),
248-
},
249-
},
250-
} satisfies Config
251253
"
252254
`)
253255
},

packages/@tailwindcss-upgrade/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async function run() {
8787
// Migrate JS config
8888

8989
info('Migrating JavaScript configuration files using the provided configuration file.')
90-
let jsConfigMigration = await migrateJsConfig(config.configFilePath, base)
90+
let jsConfigMigration = await migrateJsConfig(config.designSystem, config.configFilePath, base)
9191

9292
{
9393
// Stylesheet migrations

packages/@tailwindcss-upgrade/src/migrate-js-config.ts

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import {
1010
themeableValues,
1111
} from '../../tailwindcss/src/compat/apply-config-to-theme'
1212
import { keyframesToRules } from '../../tailwindcss/src/compat/apply-keyframes-to-theme'
13-
import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge'
14-
import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config'
13+
import { resolveConfig, type ConfigFile } from '../../tailwindcss/src/compat/config/resolve-config'
1514
import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types'
1615
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
16+
import type { DesignSystem } from '../../tailwindcss/src/design-system'
1717
import { findStaticPlugins } from './utils/extract-static-plugins'
1818
import { info } from './utils/renderer'
1919

@@ -29,6 +29,7 @@ export type JSConfigMigration =
2929
}
3030

3131
export async function migrateJsConfig(
32+
designSystem: DesignSystem,
3233
fullConfigPath: string,
3334
base: string,
3435
): Promise<JSConfigMigration> {
@@ -57,7 +58,7 @@ export async function migrateJsConfig(
5758
}
5859

5960
if ('theme' in unresolvedConfig) {
60-
let themeConfig = await migrateTheme(unresolvedConfig as any)
61+
let themeConfig = await migrateTheme(designSystem, unresolvedConfig, base)
6162
if (themeConfig) cssConfigs.push(themeConfig)
6263
}
6364

@@ -75,33 +76,27 @@ export async function migrateJsConfig(
7576
}
7677
}
7778

78-
async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise<string | null> {
79-
let { extend: extendTheme, ...overwriteTheme } = unresolvedConfig.theme
80-
81-
let resetNamespaces = new Map<string, boolean>()
82-
// Before we merge theme overrides with theme extensions, we capture all
83-
// namespaces that need to be reset.
84-
for (let [key, value] of themeableValues(overwriteTheme)) {
85-
if (typeof value !== 'string' && typeof value !== 'number') {
86-
continue
87-
}
88-
89-
if (!resetNamespaces.has(key[0])) {
90-
resetNamespaces.set(key[0], false)
91-
}
79+
async function migrateTheme(
80+
designSystem: DesignSystem,
81+
unresolvedConfig: Config,
82+
base: string,
83+
): Promise<string | null> {
84+
// Resolve the config file without applying plugins and presets, as these are
85+
// migrated to CSS separately.
86+
let configToResolve: ConfigFile = {
87+
base,
88+
config: { ...unresolvedConfig, plugins: [], presets: undefined },
9289
}
90+
let { resolvedConfig, replacedThemeKeys } = resolveConfig(designSystem, [configToResolve])
9391

94-
let themeValues: Record<string, Record<string, unknown>> = deepMerge(
95-
{},
96-
[overwriteTheme, extendTheme],
97-
mergeThemeExtension,
92+
let resetNamespaces = new Map<string, boolean>(
93+
Array.from(replacedThemeKeys.entries()).map(([key]) => [key, false]),
9894
)
9995

10096
let prevSectionKey = ''
101-
10297
let css = `@theme {`
10398
let containsThemeKeys = false
104-
for (let [key, value] of themeableValues(themeValues)) {
99+
for (let [key, value] of themeableValues(resolvedConfig.theme)) {
105100
if (typeof value !== 'string' && typeof value !== 'number') {
106101
continue
107102
}
@@ -125,9 +120,9 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise<
125120
css += ` --${keyPathToCssProperty(key)}: ${value};\n`
126121
}
127122

128-
if ('keyframes' in themeValues) {
123+
if ('keyframes' in resolvedConfig.theme) {
129124
containsThemeKeys = true
130-
css += '\n' + keyframesToCss(themeValues.keyframes)
125+
css += '\n' + keyframesToCss(resolvedConfig.theme.keyframes)
131126
}
132127

133128
if (!containsThemeKeys) {
@@ -179,11 +174,6 @@ function migrateContent(
179174

180175
// Applies heuristics to determine if we can attempt to migrate the config
181176
function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
182-
// The file may not contain any functions
183-
if (source.includes('function') || source.includes(' => ')) {
184-
return false
185-
}
186-
187177
// The file may not contain non-serializable values
188178
function isSimpleValue(value: unknown): boolean {
189179
if (typeof value === 'function') return false
@@ -194,8 +184,8 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
194184
return ['string', 'number', 'boolean', 'undefined'].includes(typeof value)
195185
}
196186

197-
// Plugins are more complex, so we have a special heuristics for them.
198-
let { plugins, ...remainder } = unresolvedConfig
187+
// `theme` and `plugins` are handled separately and allowed to be more complex
188+
let { plugins, theme, ...remainder } = unresolvedConfig
199189
if (!isSimpleValue(remainder)) {
200190
return false
201191
}
@@ -224,7 +214,6 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
224214

225215
// Only migrate the config file if all top-level theme keys are allowed to be
226216
// migrated
227-
let theme = unresolvedConfig.theme
228217
if (theme && typeof theme === 'object') {
229218
if (theme.extend && !onlyAllowedThemeValues(theme.extend)) return false
230219
let { extend: _extend, ...themeCopy } = theme
@@ -234,14 +223,18 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
234223
return true
235224
}
236225

237-
const DEFAULT_THEME_KEYS = [
226+
const ALLOWED_THEME_KEYS = [
238227
...Object.keys(defaultTheme),
239228
// Used by @tailwindcss/container-queries
240229
'containers',
241230
]
231+
const BLOCKED_THEME_KEYS = ['supports', 'data', 'aria']
242232
function onlyAllowedThemeValues(theme: ThemeConfig): boolean {
243233
for (let key of Object.keys(theme)) {
244-
if (!DEFAULT_THEME_KEYS.includes(key)) {
234+
if (!ALLOWED_THEME_KEYS.includes(key)) {
235+
return false
236+
}
237+
if (BLOCKED_THEME_KEYS.includes(key)) {
245238
return false
246239
}
247240
}

packages/tailwindcss/src/compat/apply-config-to-theme.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export function applyConfigToTheme(
2424
{ theme }: ResolvedConfig,
2525
replacedThemeKeys: Set<string>,
2626
) {
27-
for (let resetThemeKey of replacedThemeKeys) {
28-
let name = keyPathToCssProperty([resetThemeKey])
27+
for (let replacedThemeKey of replacedThemeKeys) {
28+
let name = keyPathToCssProperty([replacedThemeKey])
2929
if (!name) continue
3030

3131
designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT)

0 commit comments

Comments
 (0)