Skip to content

Commit 8b0fff6

Browse files
Extract more backwards compatibility logic to compatibility layer (#14365)
I noticed a lot more backwards compatibility concerns had started leaking into core, especially around the `theme` function, so did a bit of work to try and pull that stuff out and into the compatibility layer. Now the core version of `theme` only handles CSS variables (like `--color-red-500`) and has no knowledge of the dot notation or how to upgrade it. Instead, we unconditionally override that function in the compatibility layer with a light version that _does_ know how to do the dot notation upgrade, and override that again with the very heavy/slow version that handles JS config objects only if plugins/JS configs are actually used. I've also renamed `registerPlugins` to `applyCompatibilityHooks` because the name was definitely a bit out of date given how much work it's doing now, and now call it unconditionally from core, leaving that function to do any conditional optimizations itself internally. Next steps I think would be to split up `plugin-api.ts` a bit and maybe make `applyCompatibilityHooks` its own file, and move both of those files into the `compat` folder so everything is truly isolated there. My goal with this stuff is that if/when we ever decide to drop backwards compatibility with these features in the future (maybe v5), that all we have to do is delete the one line of code that calls `applyCompatibilityHooks` in `index.ts`, and delete the `compat` folder and we're done. I could be convinced that this isn't a worthwhile goal if we feel it's making the codebase needlessly complex, so open to that discussion as well. --------- Co-authored-by: Adam Wathan <[email protected]> Co-authored-by: Robin Malfait <[email protected]>
1 parent 8c6c291 commit 8b0fff6

File tree

8 files changed

+232
-323
lines changed

8 files changed

+232
-323
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function applyConfigToTheme(designSystem: DesignSystem, configs: ConfigFi
6363
{
6464
let fontFamily = resolveThemeValue(theme.fontFamily.mono)
6565
if (fontFamily && designSystem.theme.hasDefault('--font-family-mono')) {
66-
designSystem.theme.add('--default-mono-font-family', 'theme(fontFamily.mono)', options)
66+
designSystem.theme.add('--default-mono-font-family', fontFamily, options)
6767
designSystem.theme.add(
6868
'--default-mono-font-feature-settings',
6969
resolveThemeValue(theme.fontFamily.mono, 'fontFeatureSettings') ?? 'normal',

packages/tailwindcss/src/css-functions.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { type ValueAstNode } from './value-parser'
44

55
export const THEME_FUNCTION_INVOCATION = 'theme('
66

7-
type ResolveThemeValue = (path: string) => unknown
7+
type ResolveThemeValue = (path: string) => string | undefined
88

99
export function substituteFunctions(ast: AstNode[], resolveThemeValue: ResolveThemeValue) {
1010
walk(ast, (node) => {
@@ -39,7 +39,7 @@ export function substituteFunctionsInValue(
3939
if (node.kind === 'function' && node.value === 'theme') {
4040
if (node.nodes.length < 1) {
4141
throw new Error(
42-
'Expected `theme()` function call to have a path. For example: `theme(colors.red.500)`.',
42+
'Expected `theme()` function call to have a path. For example: `theme(--color-red-500)`.',
4343
)
4444
}
4545

@@ -55,7 +55,7 @@ export function substituteFunctionsInValue(
5555
// comma (`,`), spaces alone should be merged into the previous word to
5656
// avoid splitting in this case:
5757
//
58-
// theme(colors.red.500 / 75%) theme(colors.red.500 / 75%, foo, bar)
58+
// theme(--color-red-500 / 75%) theme(--color-red-500 / 75%, foo, bar)
5959
//
6060
// We only need to do this for the first node, as the fallback values are
6161
// passed through as-is.
@@ -83,20 +83,7 @@ function cssThemeFn(
8383
path: string,
8484
fallbackValues: ValueAstNode[],
8585
): ValueAstNode[] {
86-
let resolvedValue: string | null = null
87-
let themeValue = resolveThemeValue(path)
88-
89-
if (Array.isArray(themeValue) && themeValue.length === 2) {
90-
// When a tuple is returned, return the first element
91-
resolvedValue = themeValue[0]
92-
} else if (Array.isArray(themeValue)) {
93-
// Arrays get serialized into a comma-separated lists
94-
resolvedValue = themeValue.join(', ')
95-
} else if (typeof themeValue === 'string') {
96-
// Otherwise only allow string values here, objects (and namespace maps)
97-
// are treated as non-resolved values for the CSS `theme()` function.
98-
resolvedValue = themeValue
99-
}
86+
let resolvedValue = resolveThemeValue(path)
10087

10188
if (!resolvedValue && fallbackValues.length > 0) {
10289
return fallbackValues

packages/tailwindcss/src/design-system.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { parseCandidate, parseVariant, type Candidate } from './candidate'
33
import { compileAstNodes, compileCandidates } from './compile'
44
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
55
import { getClassOrder } from './sort'
6-
import type { Theme } from './theme'
7-
import { resolveThemeValue } from './theme-fn'
8-
import { Utilities, createUtilities } from './utilities'
6+
import type { Theme, ThemeKey } from './theme'
7+
import { Utilities, createUtilities, withAlpha } from './utilities'
98
import { DefaultMap } from './utils/default-map'
109
import { Variants, createVariants } from './variants'
1110

@@ -24,7 +23,7 @@ export type DesignSystem = {
2423
compileAstNodes(candidate: Candidate): ReturnType<typeof compileAstNodes>
2524

2625
getUsedVariants(): ReturnType<typeof parseVariant>[]
27-
resolveThemeValue(path: string, defaultValue?: string): string | undefined
26+
resolveThemeValue(path: string): string | undefined
2827
}
2928

3029
export function buildDesignSystem(theme: Theme): DesignSystem {
@@ -82,8 +81,24 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
8281
return Array.from(parsedVariants.values())
8382
},
8483

85-
resolveThemeValue(path: string, defaultValue?: string) {
86-
return resolveThemeValue(theme, path, defaultValue)
84+
resolveThemeValue(path: `${ThemeKey}` | `${ThemeKey}${string}`) {
85+
// Extract an eventual modifier from the path. e.g.:
86+
// - "--color-red-500 / 50%" -> "50%"
87+
let lastSlash = path.lastIndexOf('/')
88+
let modifier: string | null = null
89+
if (lastSlash !== -1) {
90+
modifier = path.slice(lastSlash + 1).trim()
91+
path = path.slice(0, lastSlash).trim() as ThemeKey
92+
}
93+
94+
let themeValue = theme.get([path]) ?? undefined
95+
96+
// Apply the opacity modifier if present
97+
if (modifier && themeValue) {
98+
return withAlpha(themeValue, modifier)
99+
}
100+
101+
return themeValue
87102
},
88103
}
89104

packages/tailwindcss/src/index.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { compileCandidates } from './compile'
66
import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './css-functions'
77
import * as CSS from './css-parser'
88
import { buildDesignSystem, type DesignSystem } from './design-system'
9-
import { registerPlugins, type CssPluginOptions, type Plugin } from './plugin-api'
9+
import { applyCompatibilityHooks, type CssPluginOptions, type Plugin } from './plugin-api'
1010
import { Theme, ThemeOptions } from './theme'
1111
import { segment } from './utils/segment'
1212
export type Config = UserConfig
@@ -306,24 +306,19 @@ async function parseCss(
306306

307307
let designSystem = buildDesignSystem(theme)
308308

309-
let configs = await Promise.all(
310-
configPaths.map(async (configPath) => ({
311-
path: configPath,
312-
config: await loadConfig(configPath),
313-
})),
314-
)
315-
316-
let plugins = await Promise.all(
317-
pluginPaths.map(async ([pluginPath, pluginOptions]) => ({
318-
path: pluginPath,
319-
plugin: await loadPlugin(pluginPath),
320-
options: pluginOptions,
321-
})),
322-
)
323-
324-
if (plugins.length || configs.length) {
325-
registerPlugins(plugins, designSystem, ast, configs, globs)
326-
}
309+
// Apply hooks from backwards compatibility layer. This function takes a lot
310+
// of random arguments because it really just needs access to "the world" to
311+
// do whatever ungodly things it needs to do to make things backwards
312+
// compatible without polluting core.
313+
await applyCompatibilityHooks({
314+
designSystem,
315+
ast,
316+
pluginPaths,
317+
loadPlugin,
318+
configPaths,
319+
loadConfig,
320+
globs,
321+
})
327322

328323
for (let customVariant of customVariants) {
329324
customVariant(designSystem)
@@ -376,11 +371,7 @@ async function parseCss(
376371
substituteAtApply(ast, designSystem)
377372
}
378373

379-
// Replace `theme()` function calls with the actual theme variables. Plugins
380-
// could register new rules that include functions, and JS config files could
381-
// also contain functions or plugins that use functions so we need to evaluate
382-
// functions if either of those are present.
383-
if (plugins.length > 0 || configs.length > 0 || css.includes(THEME_FUNCTION_INVOCATION)) {
374+
if (css.includes(THEME_FUNCTION_INVOCATION)) {
384375
substituteFunctions(ast, designSystem.resolveThemeValue)
385376
}
386377

0 commit comments

Comments
 (0)