mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Support @theme reference without @import (#13222)
* Support `@theme reference` without `@import` * Fix test * Update tests * Update changelog --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This commit is contained in:
parent
65f6f7c1ca
commit
4e1f98141a
@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
Nothing yet!
|
||||
### Added
|
||||
|
||||
- Support `@theme reference { … }` for defining theme values without emitting variables ([#13222](https://github.com/tailwindlabs/tailwindcss/pull/13222))
|
||||
|
||||
## [4.0.0-alpha.8] - 2024-03-11
|
||||
|
||||
|
||||
@ -948,4 +948,135 @@ describe('Parsing themes values from CSS', () => {
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('theme values added as reference are not included in the output as variables', () => {
|
||||
expect(
|
||||
compileCss(
|
||||
css`
|
||||
@theme {
|
||||
--color-tomato: #e10c04;
|
||||
}
|
||||
@theme reference {
|
||||
--color-potato: #ac855b;
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-tomato', 'bg-potato'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--color-tomato: #e10c04;
|
||||
}
|
||||
|
||||
.bg-potato {
|
||||
background-color: #ac855b;
|
||||
}
|
||||
|
||||
.bg-tomato {
|
||||
background-color: #e10c04;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('theme values added as reference that override existing theme value suppress the output of the original theme value as a variable', () => {
|
||||
expect(
|
||||
compileCss(
|
||||
css`
|
||||
@theme {
|
||||
--color-potato: #ac855b;
|
||||
}
|
||||
@theme reference {
|
||||
--color-potato: #c794aa;
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-potato'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
".bg-potato {
|
||||
background-color: #c794aa;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('overriding a reference theme value with a non-reference theme value includes it in the output as a variable', () => {
|
||||
expect(
|
||||
compileCss(
|
||||
css`
|
||||
@theme reference {
|
||||
--color-potato: #ac855b;
|
||||
}
|
||||
@theme {
|
||||
--color-potato: #c794aa;
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-potato'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--color-potato: #c794aa;
|
||||
}
|
||||
|
||||
.bg-potato {
|
||||
background-color: #c794aa;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('wrapping `@theme` with `@media reference` behaves like `@theme reference` to support `@import` statements', () => {
|
||||
expect(
|
||||
compileCss(
|
||||
css`
|
||||
@theme {
|
||||
--color-tomato: #e10c04;
|
||||
}
|
||||
@media reference {
|
||||
@theme {
|
||||
--color-potato: #ac855b;
|
||||
}
|
||||
@theme {
|
||||
--color-avocado: #c0cc6d;
|
||||
}
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-tomato', 'bg-potato', 'bg-avocado'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--color-tomato: #e10c04;
|
||||
}
|
||||
|
||||
.bg-avocado {
|
||||
background-color: #c0cc6d;
|
||||
}
|
||||
|
||||
.bg-potato {
|
||||
background-color: #ac855b;
|
||||
}
|
||||
|
||||
.bg-tomato {
|
||||
background-color: #e10c04;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('`@media reference` can only contain `@theme` rules', () => {
|
||||
expect(() =>
|
||||
compileCss(
|
||||
css`
|
||||
@media reference {
|
||||
.not-a-theme-rule {
|
||||
color: cursed;
|
||||
}
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-tomato', 'bg-potato', 'bg-avocado'],
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Files imported with \`@import "…" reference\` must only contain \`@theme\` blocks.]`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,25 +28,47 @@ export function compile(css: string): {
|
||||
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.selector !== '@theme') return
|
||||
|
||||
// Drop instances of `@media reference`
|
||||
//
|
||||
// We support `@import "tailwindcss/theme" reference` as a way to import an external theme file
|
||||
// as a reference, which becomes `@media reference { … }` when the `@import` is processed.
|
||||
if (node.selector === '@media reference') {
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind !== 'rule') {
|
||||
throw new Error(
|
||||
'Files imported with `@import "…" reference` must only contain `@theme` blocks.',
|
||||
)
|
||||
}
|
||||
if (child.selector === '@theme') {
|
||||
child.selector = '@theme reference'
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
replaceWith(node.nodes)
|
||||
}
|
||||
|
||||
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
|
||||
|
||||
let isReference = node.selector === '@theme reference'
|
||||
|
||||
// Record all custom properties in the `@theme` declaration
|
||||
walk(node.nodes, (node, { replaceWith }) => {
|
||||
walk(node.nodes, (child, { replaceWith }) => {
|
||||
// Collect `@keyframes` rules to re-insert with theme variables later,
|
||||
// since the `@theme` rule itself will be removed.
|
||||
if (node.kind === 'rule' && node.selector.startsWith('@keyframes ')) {
|
||||
keyframesRules.push(node)
|
||||
if (child.kind === 'rule' && child.selector.startsWith('@keyframes ')) {
|
||||
keyframesRules.push(child)
|
||||
replaceWith([])
|
||||
return WalkAction.Skip
|
||||
}
|
||||
|
||||
if (node.kind === 'comment') return
|
||||
if (node.kind === 'declaration' && node.property.startsWith('--')) {
|
||||
theme.add(node.property, node.value)
|
||||
if (child.kind === 'comment') return
|
||||
if (child.kind === 'declaration' && child.property.startsWith('--')) {
|
||||
theme.add(child.property, child.value, isReference)
|
||||
return
|
||||
}
|
||||
|
||||
let snippet = toCss([rule('@theme', [node])])
|
||||
let snippet = toCss([rule(node.selector, [child])])
|
||||
.split('\n')
|
||||
.map((line, idx, all) => `${idx === 0 || idx >= all.length - 2 ? ' ' : '>'} ${line}`)
|
||||
.join('\n')
|
||||
@ -58,7 +80,7 @@ export function compile(css: string): {
|
||||
|
||||
// Keep a reference to the first `@theme` rule to update with the full theme
|
||||
// later, and delete any other `@theme` rules.
|
||||
if (!firstThemeRule) {
|
||||
if (!firstThemeRule && !isReference) {
|
||||
firstThemeRule = node
|
||||
} else {
|
||||
replaceWith([])
|
||||
@ -75,7 +97,8 @@ export function compile(css: string): {
|
||||
let nodes = []
|
||||
|
||||
for (let [key, value] of theme.entries()) {
|
||||
nodes.push(decl(key, value))
|
||||
if (value.isReference) continue
|
||||
nodes.push(decl(key, value.value))
|
||||
}
|
||||
|
||||
if (keyframesRules.length > 0) {
|
||||
@ -158,23 +181,6 @@ export function compile(css: string): {
|
||||
})
|
||||
}
|
||||
|
||||
// Drop instances of `@media reference`
|
||||
//
|
||||
// We allow importing a theme as a reference so users can define the theme for
|
||||
// the current CSS file without duplicating the theme vars in the final CSS.
|
||||
// This is useful for users who use `@apply` in Vue SFCs and in CSS modules.
|
||||
//
|
||||
// The syntax is derived from `@import "tailwindcss/theme" reference` which
|
||||
// turns into `@media reference { … }` in the final CSS.
|
||||
if (css.includes('@media reference')) {
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
if (node.kind === 'rule' && node.selector === '@media reference') {
|
||||
replaceWith([])
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Track all valid candidates, these are the incoming `rawCandidate` that
|
||||
// resulted in a generated AST Node. All the other `rawCandidates` are invalid
|
||||
// and should be ignored.
|
||||
@ -255,14 +261,15 @@ export function __unstable__loadDesignSystem(css: string) {
|
||||
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.selector !== '@theme') return
|
||||
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
|
||||
let isReference = node.selector === '@theme reference'
|
||||
|
||||
// Record all custom properties in the `@theme` declaration
|
||||
walk([node], (node) => {
|
||||
if (node.kind !== 'declaration') return
|
||||
if (!node.property.startsWith('--')) return
|
||||
|
||||
theme.add(node.property, node.value)
|
||||
theme.add(node.property, node.value, isReference)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -3,22 +3,18 @@ import { buildDesignSystem } from './design-system'
|
||||
import { Theme } from './theme'
|
||||
|
||||
function loadDesignSystem() {
|
||||
return buildDesignSystem(
|
||||
new Theme(
|
||||
new Map([
|
||||
['--spacing-0_5', '0.125rem'],
|
||||
['--spacing-1', '0.25rem'],
|
||||
['--spacing-3', '0.75rem'],
|
||||
['--spacing-4', '1rem'],
|
||||
['--width-4', '1rem'],
|
||||
['--colors-red-500', 'red'],
|
||||
['--colors-blue-500', 'blue'],
|
||||
['--breakpoint-sm', '640px'],
|
||||
['--font-size-xs', '0.75rem'],
|
||||
['--font-size-xs--line-height', '1rem'],
|
||||
]),
|
||||
),
|
||||
)
|
||||
let theme = new Theme()
|
||||
theme.add('--spacing-0_5', '0.125rem')
|
||||
theme.add('--spacing-1', '0.25rem')
|
||||
theme.add('--spacing-3', '0.75rem')
|
||||
theme.add('--spacing-4', '1rem')
|
||||
theme.add('--width-4', '1rem')
|
||||
theme.add('--colors-red-500', 'red')
|
||||
theme.add('--colors-blue-500', 'blue')
|
||||
theme.add('--breakpoint-sm', '640px')
|
||||
theme.add('--font-size-xs', '0.75rem')
|
||||
theme.add('--font-size-xs--line-height', '1rem')
|
||||
return buildDesignSystem(theme)
|
||||
}
|
||||
|
||||
test('getClassList', () => {
|
||||
|
||||
@ -4,17 +4,15 @@ import { Theme } from './theme'
|
||||
|
||||
const input = 'a-class px-3 p-1 b-class py-3 bg-red-500 bg-blue-500'.split(' ')
|
||||
const emptyDesign = buildDesignSystem(new Theme())
|
||||
const simpleDesign = buildDesignSystem(
|
||||
new Theme(
|
||||
new Map([
|
||||
['--spacing-1', '0.25rem'],
|
||||
['--spacing-3', '0.75rem'],
|
||||
['--spacing-4', '1rem'],
|
||||
['--color-red-500', 'red'],
|
||||
['--color-blue-500', 'blue'],
|
||||
]),
|
||||
),
|
||||
)
|
||||
const simpleDesign = (() => {
|
||||
let simpleTheme = new Theme()
|
||||
simpleTheme.add('--spacing-1', '0.25rem')
|
||||
simpleTheme.add('--spacing-3', '0.75rem')
|
||||
simpleTheme.add('--spacing-4', '1rem')
|
||||
simpleTheme.add('--color-red-500', 'red')
|
||||
simpleTheme.add('--color-blue-500', 'blue')
|
||||
return buildDesignSystem(simpleTheme)
|
||||
})()
|
||||
|
||||
bench('getClassOrder (empty theme)', () => {
|
||||
emptyDesign.getClassOrder(input)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { escape } from './utils/escape'
|
||||
|
||||
export class Theme {
|
||||
constructor(private values: Map<string, string> = new Map<string, string>()) {}
|
||||
constructor(private values = new Map<string, { value: string; isReference: boolean }>()) {}
|
||||
|
||||
add(key: string, value: string): void {
|
||||
add(key: string, value: string, isReference = false): void {
|
||||
if (key.endsWith('-*')) {
|
||||
if (value !== 'initial') {
|
||||
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
|
||||
@ -18,7 +18,7 @@ export class Theme {
|
||||
if (value === 'initial') {
|
||||
this.values.delete(key)
|
||||
} else {
|
||||
this.values.set(key, value)
|
||||
this.values.set(key, { value, isReference })
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ export class Theme {
|
||||
for (let key of themeKeys) {
|
||||
let value = this.values.get(key)
|
||||
if (value) {
|
||||
return value
|
||||
return value.value
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ export class Theme {
|
||||
|
||||
if (!themeKey) return null
|
||||
|
||||
return this.values.get(themeKey)!
|
||||
return this.values.get(themeKey)!.value
|
||||
}
|
||||
|
||||
resolveWith(
|
||||
@ -98,11 +98,11 @@ export class Theme {
|
||||
for (let name of nestedKeys) {
|
||||
let nestedValue = this.values.get(`${themeKey}${name}`)
|
||||
if (nestedValue) {
|
||||
extra[name] = nestedValue
|
||||
extra[name] = nestedValue.value
|
||||
}
|
||||
}
|
||||
|
||||
return [this.values.get(themeKey)!, extra]
|
||||
return [this.values.get(themeKey)!.value, extra]
|
||||
}
|
||||
|
||||
namespace(namespace: string) {
|
||||
@ -111,9 +111,9 @@ export class Theme {
|
||||
|
||||
for (let [key, value] of this.values) {
|
||||
if (key === namespace) {
|
||||
values.set(null, value)
|
||||
values.set(null, value.value)
|
||||
} else if (key.startsWith(prefix)) {
|
||||
values.set(key.slice(prefix.length), value)
|
||||
values.set(key.slice(prefix.length), value.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user