Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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