Skip to content

Commit

Permalink
Support @theme reference without @import (#13222)
Browse files Browse the repository at this point in the history
* Support `@theme reference` without `@import`

* Fix test

* Update tests

* Update changelog

---------

Co-authored-by: Adam Wathan <[email protected]>
  • Loading branch information
adamwathan and adamwathan authored Mar 12, 2024
1 parent 65f6f7c commit 4e1f981
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 66 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
131 changes: 131 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.]`,
)
})
})
65 changes: 36 additions & 29 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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([])
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
})
})

Expand Down
28 changes: 12 additions & 16 deletions packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
20 changes: 9 additions & 11 deletions packages/tailwindcss/src/sort.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 9 additions & 9 deletions packages/tailwindcss/src/theme.ts
Original file line number Diff line number Diff line change
@@ -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}\``)
Expand All @@ -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 })
}
}

Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -82,7 +82,7 @@ export class Theme {

if (!themeKey) return null

return this.values.get(themeKey)!
return this.values.get(themeKey)!.value
}

resolveWith(
Expand All @@ -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) {
Expand All @@ -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)
}
}

Expand Down

0 comments on commit 4e1f981

Please sign in to comment.