Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve theme keys when migrating JS config to CSS #14675

Merged
merged 11 commits into from
Oct 17, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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(
)

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 @@ -230,24 +230,26 @@ test(
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';

@theme {
--color-gray-50: oklch(0.985 0 none);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this PR didn't introduce it, but I wonder if the "legacy" colors (v3) should still use the v3 values (hex) instead of oklch(…) 🤔

--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
"
`)
},
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
63 changes: 28 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,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
}
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
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