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 <adam.wathan@gmail.com> Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
edb066e0aa
commit
0971eadd8f
5 changed files with 47 additions and 51 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"
|
||||
`)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<JSConfigMigration> {
|
||||
|
|
@ -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<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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue