From 60e61950b9bb62d9290f2a9df3330f2e36fc3b8e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 31 Jan 2025 15:20:18 +0100 Subject: [PATCH] Ensure escaped theme variables are handled correctly (#16064) This PR ensures that escaped theme variables are properly handled. We do this by moving the `escape`/`unescape` responsibility back into the main tailwindcss entrypoint that reads and writes from the CSS and making sure that _all internal state of the `Theme` class are unescaped classes. However, due to us accidentally shipping the part where a dot in the theme variable would translate to an underscore in CSS already, this logic is going to stay as-is for now. Here's an example test that visualizes the new changes: ```ts expect( await compileCss( css` @theme { --spacing-*: initial; --spacing-1\.5: 2.5rem; --spacing-foo\/bar: 3rem; } @tailwind utilities; `, ['m-1.5', 'm-foo/bar'], ), ).toMatchInlineSnapshot(` ":root, :host { --spacing-1\.5: 2.5rem; --spacing-foo\\/bar: 3rem; } .m-1\\.5 { margin: var(--spacing-1\.5); } .m-foo\\/bar { margin: var(--spacing-foo\\/bar); }" `) ``` ## Test plan - Added a unit test - Ensure this works end-to-end using the Vite playground: Screenshot 2025-01-30 at 14 51 05 --- CHANGELOG.md | 1 + .../src/compat/apply-config-to-theme.ts | 3 +- packages/tailwindcss/src/index.test.ts | 43 +++++++++++++++++++ packages/tailwindcss/src/index.ts | 5 ++- packages/tailwindcss/src/theme.ts | 23 ++++++---- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 361db062e60b..60a55a08131c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Only generate positive `grid-cols-*` and `grid-rows-*` utilities ([#16020](https://github.com/tailwindlabs/tailwindcss/pull/16020)) +- Ensure escaped theme variables are handled correctly ([#16064](https://github.com/tailwindlabs/tailwindcss/pull/16064)) - Ensure we process Tailwind CSS features when only using `@reference` or `@variant` ([#16057](https://github.com/tailwindlabs/tailwindcss/pull/16057)) - Refactor gradient implementation to work around [prettier/prettier#17058](https://github.com/prettier/prettier/issues/17058) ([#16072](https://github.com/tailwindlabs/tailwindcss/pull/16072)) - Vite: Ensure hot-reloading works with SolidStart setups ([#16052](https://github.com/tailwindlabs/tailwindcss/pull/16052)) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 3f18711c7f16..c57e4308fcb7 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -1,6 +1,5 @@ import type { DesignSystem } from '../design-system' import { ThemeOptions } from '../theme' -import { escape } from '../utils/escape' import type { ResolvedConfig } from './config/types' function resolveThemeValue(value: unknown, subValue: string | null = null): string | null { @@ -55,7 +54,7 @@ export function applyConfigToTheme( if (!name) continue designSystem.theme.add( - `--${escape(name)}`, + `--${name}`, '' + value, ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT, ) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 9f7962916d90..ec4671e97268 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -152,6 +152,49 @@ describe('compiling CSS', () => { `) }) + test('unescapes theme variables and handles dots as underscore', async () => { + expect( + await compileCss( + css` + @theme { + --spacing-*: initial; + --spacing-1\.5: 1.5px; + --spacing-2_5: 2.5px; + --spacing-3\.5: 3.5px; + --spacing-3_5: 3.5px; + --spacing-foo\/bar: 3rem; + } + @tailwind utilities; + `, + ['m-1.5', 'm-2.5', 'm-2_5', 'm-3.5', 'm-foo/bar'], + ), + ).toMatchInlineSnapshot(` + ":root, :host { + --spacing-1\\.5: 1.5px; + --spacing-2_5: 2.5px; + --spacing-3\\.5: 3.5px; + --spacing-3_5: 3.5px; + --spacing-foo\\/bar: 3rem; + } + + .m-1\\.5 { + margin: var(--spacing-1\\.5); + } + + .m-2\\.5, .m-2_5 { + margin: var(--spacing-2_5); + } + + .m-3\\.5 { + margin: var(--spacing-3\\.5); + } + + .m-foo\\/bar { + margin: var(--spacing-foo\\/bar); + }" + `) + }) + test('adds vendor prefixes', async () => { expect( await compileCss( diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 5947e73b810c..d594397ede6e 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -27,6 +27,7 @@ import * as CSS from './css-parser' import { buildDesignSystem, type DesignSystem } from './design-system' import { Theme, ThemeOptions } from './theme' import { createCssUtility } from './utilities' +import { escape, unescape } from './utils/escape' import { segment } from './utils/segment' import { compoundsForSelectors, IS_VALID_VARIANT_NAME } from './variants' export type Config = UserConfig @@ -467,7 +468,7 @@ async function parseCss( if (child.kind === 'comment') return if (child.kind === 'declaration' && child.property.startsWith('--')) { - theme.add(child.property, child.value ?? '', themeOptions) + theme.add(unescape(child.property), child.value ?? '', themeOptions) return } @@ -526,7 +527,7 @@ async function parseCss( for (let [key, value] of theme.entries()) { if (value.options & ThemeOptions.REFERENCE) continue - nodes.push(decl(key, value.value)) + nodes.push(decl(escape(key), value.value)) } let keyframesRules = theme.getKeyframes() diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index a2d3c205fa4a..0b0ca96c8d11 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -41,10 +41,6 @@ export class Theme { ) {} add(key: string, value: string, options = ThemeOptions.NONE): void { - if (key.endsWith('\\*')) { - key = key.slice(0, -2) + '*' - } - if (key.endsWith('-*')) { if (value !== 'initial') { throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``) @@ -149,11 +145,20 @@ export class Theme { #resolveKey(candidateValue: string | null, themeKeys: ThemeKey[]): string | null { for (let namespace of themeKeys) { let themeKey = - candidateValue !== null - ? (escape(`${namespace}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey) - : namespace + candidateValue !== null ? (`${namespace}-${candidateValue}` as ThemeKey) : namespace + + if (!this.values.has(themeKey)) { + // If the exact theme key is not found, we might be trying to resolve a key containing a dot + // that was registered with an underscore instead: + if (candidateValue !== null && candidateValue.includes('.')) { + themeKey = `${namespace}-${candidateValue.replaceAll('.', '_')}` as ThemeKey + + if (!this.values.has(themeKey)) continue + } else { + continue + } + } - if (!this.values.has(themeKey)) continue if (isIgnoredThemeKey(themeKey, namespace)) continue return themeKey @@ -167,7 +172,7 @@ export class Theme { return null } - return `var(${this.#prefixKey(themeKey)})` + return `var(${escape(this.#prefixKey(themeKey))})` } resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {