Skip to content

Commit 4e1f981

Browse files
authored
Support @theme reference without @import (#13222)
* Support `@theme reference` without `@import` * Fix test * Update tests * Update changelog --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent 65f6f7c commit 4e1f981

File tree

6 files changed

+200
-66
lines changed

6 files changed

+200
-66
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
Nothing yet!
10+
### Added
11+
12+
- Support `@theme reference { … }` for defining theme values without emitting variables ([#13222](https://github.com/tailwindlabs/tailwindcss/pull/13222))
1113

1214
## [4.0.0-alpha.8] - 2024-03-11
1315

packages/tailwindcss/src/index.test.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,4 +948,135 @@ describe('Parsing themes values from CSS', () => {
948948
}"
949949
`)
950950
})
951+
952+
test('theme values added as reference are not included in the output as variables', () => {
953+
expect(
954+
compileCss(
955+
css`
956+
@theme {
957+
--color-tomato: #e10c04;
958+
}
959+
@theme reference {
960+
--color-potato: #ac855b;
961+
}
962+
@tailwind utilities;
963+
`,
964+
['bg-tomato', 'bg-potato'],
965+
),
966+
).toMatchInlineSnapshot(`
967+
":root {
968+
--color-tomato: #e10c04;
969+
}
970+
971+
.bg-potato {
972+
background-color: #ac855b;
973+
}
974+
975+
.bg-tomato {
976+
background-color: #e10c04;
977+
}"
978+
`)
979+
})
980+
981+
test('theme values added as reference that override existing theme value suppress the output of the original theme value as a variable', () => {
982+
expect(
983+
compileCss(
984+
css`
985+
@theme {
986+
--color-potato: #ac855b;
987+
}
988+
@theme reference {
989+
--color-potato: #c794aa;
990+
}
991+
@tailwind utilities;
992+
`,
993+
['bg-potato'],
994+
),
995+
).toMatchInlineSnapshot(`
996+
".bg-potato {
997+
background-color: #c794aa;
998+
}"
999+
`)
1000+
})
1001+
1002+
test('overriding a reference theme value with a non-reference theme value includes it in the output as a variable', () => {
1003+
expect(
1004+
compileCss(
1005+
css`
1006+
@theme reference {
1007+
--color-potato: #ac855b;
1008+
}
1009+
@theme {
1010+
--color-potato: #c794aa;
1011+
}
1012+
@tailwind utilities;
1013+
`,
1014+
['bg-potato'],
1015+
),
1016+
).toMatchInlineSnapshot(`
1017+
":root {
1018+
--color-potato: #c794aa;
1019+
}
1020+
1021+
.bg-potato {
1022+
background-color: #c794aa;
1023+
}"
1024+
`)
1025+
})
1026+
1027+
test('wrapping `@theme` with `@media reference` behaves like `@theme reference` to support `@import` statements', () => {
1028+
expect(
1029+
compileCss(
1030+
css`
1031+
@theme {
1032+
--color-tomato: #e10c04;
1033+
}
1034+
@media reference {
1035+
@theme {
1036+
--color-potato: #ac855b;
1037+
}
1038+
@theme {
1039+
--color-avocado: #c0cc6d;
1040+
}
1041+
}
1042+
@tailwind utilities;
1043+
`,
1044+
['bg-tomato', 'bg-potato', 'bg-avocado'],
1045+
),
1046+
).toMatchInlineSnapshot(`
1047+
":root {
1048+
--color-tomato: #e10c04;
1049+
}
1050+
1051+
.bg-avocado {
1052+
background-color: #c0cc6d;
1053+
}
1054+
1055+
.bg-potato {
1056+
background-color: #ac855b;
1057+
}
1058+
1059+
.bg-tomato {
1060+
background-color: #e10c04;
1061+
}"
1062+
`)
1063+
})
1064+
1065+
test('`@media reference` can only contain `@theme` rules', () => {
1066+
expect(() =>
1067+
compileCss(
1068+
css`
1069+
@media reference {
1070+
.not-a-theme-rule {
1071+
color: cursed;
1072+
}
1073+
}
1074+
@tailwind utilities;
1075+
`,
1076+
['bg-tomato', 'bg-potato', 'bg-avocado'],
1077+
),
1078+
).toThrowErrorMatchingInlineSnapshot(
1079+
`[Error: Files imported with \`@import "…" reference\` must only contain \`@theme\` blocks.]`,
1080+
)
1081+
})
9511082
})

packages/tailwindcss/src/index.ts

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,47 @@ export function compile(css: string): {
2828

2929
walk(ast, (node, { replaceWith }) => {
3030
if (node.kind !== 'rule') return
31-
if (node.selector !== '@theme') return
31+
32+
// Drop instances of `@media reference`
33+
//
34+
// We support `@import "tailwindcss/theme" reference` as a way to import an external theme file
35+
// as a reference, which becomes `@media reference { … }` when the `@import` is processed.
36+
if (node.selector === '@media reference') {
37+
walk(node.nodes, (child) => {
38+
if (child.kind !== 'rule') {
39+
throw new Error(
40+
'Files imported with `@import "…" reference` must only contain `@theme` blocks.',
41+
)
42+
}
43+
if (child.selector === '@theme') {
44+
child.selector = '@theme reference'
45+
return WalkAction.Skip
46+
}
47+
})
48+
replaceWith(node.nodes)
49+
}
50+
51+
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
52+
53+
let isReference = node.selector === '@theme reference'
3254

3355
// Record all custom properties in the `@theme` declaration
34-
walk(node.nodes, (node, { replaceWith }) => {
56+
walk(node.nodes, (child, { replaceWith }) => {
3557
// Collect `@keyframes` rules to re-insert with theme variables later,
3658
// since the `@theme` rule itself will be removed.
37-
if (node.kind === 'rule' && node.selector.startsWith('@keyframes ')) {
38-
keyframesRules.push(node)
59+
if (child.kind === 'rule' && child.selector.startsWith('@keyframes ')) {
60+
keyframesRules.push(child)
3961
replaceWith([])
4062
return WalkAction.Skip
4163
}
4264

43-
if (node.kind === 'comment') return
44-
if (node.kind === 'declaration' && node.property.startsWith('--')) {
45-
theme.add(node.property, node.value)
65+
if (child.kind === 'comment') return
66+
if (child.kind === 'declaration' && child.property.startsWith('--')) {
67+
theme.add(child.property, child.value, isReference)
4668
return
4769
}
4870

49-
let snippet = toCss([rule('@theme', [node])])
71+
let snippet = toCss([rule(node.selector, [child])])
5072
.split('\n')
5173
.map((line, idx, all) => `${idx === 0 || idx >= all.length - 2 ? ' ' : '>'} ${line}`)
5274
.join('\n')
@@ -58,7 +80,7 @@ export function compile(css: string): {
5880

5981
// Keep a reference to the first `@theme` rule to update with the full theme
6082
// later, and delete any other `@theme` rules.
61-
if (!firstThemeRule) {
83+
if (!firstThemeRule && !isReference) {
6284
firstThemeRule = node
6385
} else {
6486
replaceWith([])
@@ -75,7 +97,8 @@ export function compile(css: string): {
7597
let nodes = []
7698

7799
for (let [key, value] of theme.entries()) {
78-
nodes.push(decl(key, value))
100+
if (value.isReference) continue
101+
nodes.push(decl(key, value.value))
79102
}
80103

81104
if (keyframesRules.length > 0) {
@@ -158,23 +181,6 @@ export function compile(css: string): {
158181
})
159182
}
160183

161-
// Drop instances of `@media reference`
162-
//
163-
// We allow importing a theme as a reference so users can define the theme for
164-
// the current CSS file without duplicating the theme vars in the final CSS.
165-
// This is useful for users who use `@apply` in Vue SFCs and in CSS modules.
166-
//
167-
// The syntax is derived from `@import "tailwindcss/theme" reference` which
168-
// turns into `@media reference { … }` in the final CSS.
169-
if (css.includes('@media reference')) {
170-
walk(ast, (node, { replaceWith }) => {
171-
if (node.kind === 'rule' && node.selector === '@media reference') {
172-
replaceWith([])
173-
return WalkAction.Skip
174-
}
175-
})
176-
}
177-
178184
// Track all valid candidates, these are the incoming `rawCandidate` that
179185
// resulted in a generated AST Node. All the other `rawCandidates` are invalid
180186
// and should be ignored.
@@ -255,14 +261,15 @@ export function __unstable__loadDesignSystem(css: string) {
255261

256262
walk(ast, (node) => {
257263
if (node.kind !== 'rule') return
258-
if (node.selector !== '@theme') return
264+
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
265+
let isReference = node.selector === '@theme reference'
259266

260267
// Record all custom properties in the `@theme` declaration
261268
walk([node], (node) => {
262269
if (node.kind !== 'declaration') return
263270
if (!node.property.startsWith('--')) return
264271

265-
theme.add(node.property, node.value)
272+
theme.add(node.property, node.value, isReference)
266273
})
267274
})
268275

packages/tailwindcss/src/intellisense.test.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,18 @@ import { buildDesignSystem } from './design-system'
33
import { Theme } from './theme'
44

55
function loadDesignSystem() {
6-
return buildDesignSystem(
7-
new Theme(
8-
new Map([
9-
['--spacing-0_5', '0.125rem'],
10-
['--spacing-1', '0.25rem'],
11-
['--spacing-3', '0.75rem'],
12-
['--spacing-4', '1rem'],
13-
['--width-4', '1rem'],
14-
['--colors-red-500', 'red'],
15-
['--colors-blue-500', 'blue'],
16-
['--breakpoint-sm', '640px'],
17-
['--font-size-xs', '0.75rem'],
18-
['--font-size-xs--line-height', '1rem'],
19-
]),
20-
),
21-
)
6+
let theme = new Theme()
7+
theme.add('--spacing-0_5', '0.125rem')
8+
theme.add('--spacing-1', '0.25rem')
9+
theme.add('--spacing-3', '0.75rem')
10+
theme.add('--spacing-4', '1rem')
11+
theme.add('--width-4', '1rem')
12+
theme.add('--colors-red-500', 'red')
13+
theme.add('--colors-blue-500', 'blue')
14+
theme.add('--breakpoint-sm', '640px')
15+
theme.add('--font-size-xs', '0.75rem')
16+
theme.add('--font-size-xs--line-height', '1rem')
17+
return buildDesignSystem(theme)
2218
}
2319

2420
test('getClassList', () => {

packages/tailwindcss/src/sort.bench.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@ import { Theme } from './theme'
44

55
const input = 'a-class px-3 p-1 b-class py-3 bg-red-500 bg-blue-500'.split(' ')
66
const emptyDesign = buildDesignSystem(new Theme())
7-
const simpleDesign = buildDesignSystem(
8-
new Theme(
9-
new Map([
10-
['--spacing-1', '0.25rem'],
11-
['--spacing-3', '0.75rem'],
12-
['--spacing-4', '1rem'],
13-
['--color-red-500', 'red'],
14-
['--color-blue-500', 'blue'],
15-
]),
16-
),
17-
)
7+
const simpleDesign = (() => {
8+
let simpleTheme = new Theme()
9+
simpleTheme.add('--spacing-1', '0.25rem')
10+
simpleTheme.add('--spacing-3', '0.75rem')
11+
simpleTheme.add('--spacing-4', '1rem')
12+
simpleTheme.add('--color-red-500', 'red')
13+
simpleTheme.add('--color-blue-500', 'blue')
14+
return buildDesignSystem(simpleTheme)
15+
})()
1816

1917
bench('getClassOrder (empty theme)', () => {
2018
emptyDesign.getClassOrder(input)

packages/tailwindcss/src/theme.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { escape } from './utils/escape'
22

33
export class Theme {
4-
constructor(private values: Map<string, string> = new Map<string, string>()) {}
4+
constructor(private values = new Map<string, { value: string; isReference: boolean }>()) {}
55

6-
add(key: string, value: string): void {
6+
add(key: string, value: string, isReference = false): void {
77
if (key.endsWith('-*')) {
88
if (value !== 'initial') {
99
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
@@ -18,7 +18,7 @@ export class Theme {
1818
if (value === 'initial') {
1919
this.values.delete(key)
2020
} else {
21-
this.values.set(key, value)
21+
this.values.set(key, { value, isReference })
2222
}
2323
}
2424

@@ -46,7 +46,7 @@ export class Theme {
4646
for (let key of themeKeys) {
4747
let value = this.values.get(key)
4848
if (value) {
49-
return value
49+
return value.value
5050
}
5151
}
5252

@@ -82,7 +82,7 @@ export class Theme {
8282

8383
if (!themeKey) return null
8484

85-
return this.values.get(themeKey)!
85+
return this.values.get(themeKey)!.value
8686
}
8787

8888
resolveWith(
@@ -98,11 +98,11 @@ export class Theme {
9898
for (let name of nestedKeys) {
9999
let nestedValue = this.values.get(`${themeKey}${name}`)
100100
if (nestedValue) {
101-
extra[name] = nestedValue
101+
extra[name] = nestedValue.value
102102
}
103103
}
104104

105-
return [this.values.get(themeKey)!, extra]
105+
return [this.values.get(themeKey)!.value, extra]
106106
}
107107

108108
namespace(namespace: string) {
@@ -111,9 +111,9 @@ export class Theme {
111111

112112
for (let [key, value] of this.values) {
113113
if (key === namespace) {
114-
values.set(null, value)
114+
values.set(null, value.value)
115115
} else if (key.startsWith(prefix)) {
116-
values.set(key.slice(prefix.length), value)
116+
values.set(key.slice(prefix.length), value.value)
117117
}
118118
}
119119

0 commit comments

Comments
 (0)