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:
<img width="1016" alt="Screenshot 2025-01-30 at 14 51 05"
src="https://github.com/user-attachments/assets/463c6fd5-793f-4ecc-86d2-5ad40bbb3e74"
/>
This commit is contained in:
parent
3aa0e494bf
commit
60e61950b9
5 changed files with 62 additions and 13 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue