Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Support TypeScript for `@plugin` and `@config` files ([#14317](https://github.com/tailwindlabs/tailwindcss/pull/14317))
- Add `default` option to `@theme` to support overriding default theme values from plugins/JS config files ([#14327](https://github.com/tailwindlabs/tailwindcss/pull/14327))

### Fixed

- Ensure content globs defined in `@config` files are relative to that file ([#14314](https://github.com/tailwindlabs/tailwindcss/pull/14314))
- Ensure CSS `theme()` functions are evaluated in media query ranges with collapsed whitespace ((#14321)[https://github.com/tailwindlabs/tailwindcss/pull/14321])
- Fix support for Nuxt projects in the Vite plugin (requires Nuxt 3.13.1+) ([#14319](https://github.com/tailwindlabs/tailwindcss/pull/14319))
- Evaluate theme functions in plugins and JS config files ([#14326](https://github.com/tailwindlabs/tailwindcss/pull/14326))
- Ensure theme values overridden with `reference` values don't generate stale CSS variables ([#14327](https://github.com/tailwindlabs/tailwindcss/pull/14327))

## [4.0.0-alpha.21] - 2024-09-02

Expand Down
3 changes: 2 additions & 1 deletion packages/tailwindcss/src/compat/apply-config-to-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export function applyConfigToTheme(designSystem: DesignSystem, configs: ConfigFi

designSystem.theme.add(`--${name}`, value as any, {
isInline: true,
isReference: false,
isReference: true,
isDefault: true,
})
}

Expand Down
209 changes: 209 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,215 @@ describe('Parsing themes values from CSS', () => {
}"
`)
})

test('`default` theme values can be overridden by regular theme values`', async () => {
expect(
await compileCss(
css`
@theme {
--color-potato: #ac855b;
}
@theme default {
--color-potato: #efb46b;
}

@tailwind utilities;
`,
['bg-potato'],
),
).toMatchInlineSnapshot(`
":root {
--color-potato: #ac855b;
}

.bg-potato {
background-color: var(--color-potato, #ac855b);
}"
`)
})

test('`default` and `inline` can be used together', async () => {
expect(
await compileCss(
css`
@theme default inline {
--color-potato: #efb46b;
}

@tailwind utilities;
`,
['bg-potato'],
),
).toMatchInlineSnapshot(`
":root {
--color-potato: #efb46b;
}

.bg-potato {
background-color: #efb46b;
}"
`)
})

test('`default` and `reference` can be used together', async () => {
expect(
await compileCss(
css`
@theme default reference {
--color-potato: #efb46b;
}

@tailwind utilities;
`,
['bg-potato'],
),
).toMatchInlineSnapshot(`
".bg-potato {
background-color: var(--color-potato, #efb46b);
}"
`)
})

test('`default`, `inline`, and `reference` can be used together', async () => {
expect(
await compileCss(
css`
@theme default reference inline {
--color-potato: #efb46b;
}

@tailwind utilities;
`,
['bg-potato'],
),
).toMatchInlineSnapshot(`
".bg-potato {
background-color: #efb46b;
}"
`)
})

test('`default` can be used in `media(…)`', async () => {
expect(
await compileCss(
css`
@media theme() {
@theme {
--color-potato: #ac855b;
}
}
@media theme(default) {
@theme {
--color-potato: #efb46b;
--color-tomato: tomato;
}
}

@tailwind utilities;
`,
['bg-potato', 'bg-tomato'],
),
).toMatchInlineSnapshot(`
":root {
--color-potato: #ac855b;
--color-tomato: tomato;
}

.bg-potato {
background-color: var(--color-potato, #ac855b);
}

.bg-tomato {
background-color: var(--color-tomato, tomato);
}"
`)
})

test('`default` theme values can be overridden by plugin theme values', async () => {
let { build } = await compile(
css`
@theme default {
--color-red: red;
}
@theme {
--color-orange: orange;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm am I the only one who finds it confusing that we manually specify --color-orange here as a theme variable that is not inline and yet, because of the plugin that we import later, it won't be emitted as CSS variable. I understand the reason that we don't want to emit CSS variables for plugins because of the weird names that some plugins use but if we overwrite something that is part of the user configured theme, I would kind of expect the CSS variable to still work (if someone writes a @theme like this, I think they expect var(--color-orange) to work. 🤔

}
@plugin "my-plugin";
@tailwind utilities;
`,
{
loadPlugin: async () => {
return plugin(({}) => {}, {
theme: {
extend: {
colors: {
red: 'tomato',
orange: '#f28500',
},
},
},
})
},
},
)

expect(optimizeCss(build(['text-red', 'text-orange'])).trim()).toMatchInlineSnapshot(`
":root {
--color-orange: orange;
}

.text-orange {
color: var(--color-orange, orange);
}

.text-red {
color: tomato;
}"
`)
})

test('`default` theme values can be overridden by config theme values', async () => {
let { build } = await compile(
css`
@theme default {
--color-red: red;
}
@theme {
--color-orange: orange;
}
@config "./my-config.js";
@tailwind utilities;
`,
{
loadConfig: async () => {
return {
theme: {
extend: {
colors: {
red: 'tomato',
orange: '#f28500',
},
},
},
}
},
},
)

expect(optimizeCss(build(['text-red', 'text-orange'])).trim()).toMatchInlineSnapshot(`
":root {
--color-orange: orange;
}

.text-orange {
color: var(--color-orange, orange);
}

.text-red {
color: tomato;
}"
`)
})
})

describe('plugins', () => {
Expand Down
67 changes: 35 additions & 32 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,19 @@ function throwOnConfig(): never {
function parseThemeOptions(selector: string) {
let isReference = false
let isInline = false
let isDefault = false

for (let option of segment(selector.slice(6) /* '@theme'.length */, ' ')) {
if (option === 'reference') {
isReference = true
} else if (option === 'inline') {
isInline = true
} else if (option === 'default') {
isDefault = true
}
}

return { isReference, isInline }
return { isReference, isInline, isDefault }
}

async function parseCss(
Expand Down Expand Up @@ -253,8 +256,8 @@ async function parseCss(
'Files imported with `@import "…" theme(…)` must only contain `@theme` blocks.',
)
}
if (child.selector === '@theme') {
child.selector = '@theme ' + themeParams
if (child.selector === '@theme' || child.selector.startsWith('@theme ')) {
child.selector += ' ' + themeParams
return WalkAction.Skip
}
})
Expand All @@ -264,7 +267,7 @@ async function parseCss(

if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return

let { isReference, isInline } = parseThemeOptions(node.selector)
let { isReference, isInline, isDefault } = parseThemeOptions(node.selector)

// Record all custom properties in the `@theme` declaration
walk(node.nodes, (child, { replaceWith }) => {
Expand All @@ -278,7 +281,7 @@ async function parseCss(

if (child.kind === 'comment') return
if (child.kind === 'declaration' && child.property.startsWith('--')) {
theme.add(child.property, child.value ?? '', { isReference, isInline })
theme.add(child.property, child.value ?? '', { isReference, isInline, isDefault })
return
}

Expand All @@ -302,6 +305,33 @@ async function parseCss(
return WalkAction.Skip
})

let designSystem = buildDesignSystem(theme)

let configs = await Promise.all(
configPaths.map(async (configPath) => ({
path: configPath,
config: await loadConfig(configPath),
})),
)

let plugins = await Promise.all(
pluginPaths.map(async ([pluginPath, pluginOptions]) => ({
path: pluginPath,
plugin: await loadPlugin(pluginPath),
options: pluginOptions,
})),
)

let { pluginApi, resolvedConfig } = registerPlugins(plugins, designSystem, ast, configs)

for (let customVariant of customVariants) {
customVariant(designSystem)
}

for (let customUtility of customUtilities) {
customUtility(designSystem)
}

// Output final set of theme variables at the position of the first `@theme`
// rule.
if (firstThemeRule) {
Expand Down Expand Up @@ -340,33 +370,6 @@ async function parseCss(
firstThemeRule.nodes = nodes
}

let designSystem = buildDesignSystem(theme)

let configs = await Promise.all(
configPaths.map(async (configPath) => ({
path: configPath,
config: await loadConfig(configPath),
})),
)

let plugins = await Promise.all(
pluginPaths.map(async ([pluginPath, pluginOptions]) => ({
path: pluginPath,
plugin: await loadPlugin(pluginPath),
options: pluginOptions,
})),
)

let { pluginApi, resolvedConfig } = registerPlugins(plugins, designSystem, ast, configs)

for (let customVariant of customVariants) {
customVariant(designSystem)
}

for (let customUtility of customUtilities) {
customUtility(designSystem)
}

// Replace `@apply` rules with the actual utility classes.
if (css.includes('@apply')) {
substituteAtApply(ast, designSystem)
Expand Down
18 changes: 15 additions & 3 deletions packages/tailwindcss/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import { escape } from './utils/escape'

export class Theme {
constructor(
private values = new Map<string, { value: string; isReference: boolean; isInline: boolean }>(),
private values = new Map<
string,
{ value: string; isReference: boolean; isInline: boolean; isDefault: boolean }
>(),
) {}

add(key: string, value: string, { isReference = false, isInline = false } = {}): void {
add(
key: string,
value: string,
{ isReference = false, isInline = false, isDefault = false } = {},
): void {
if (key.endsWith('-*')) {
if (value !== 'initial') {
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
Expand All @@ -17,10 +24,15 @@ export class Theme {
}
}

if (isDefault) {
let existing = this.values.get(key)
if (existing && !existing.isDefault) return
}

if (value === 'initial') {
this.values.delete(key)
} else {
this.values.set(key, { value, isReference, isInline })
this.values.set(key, { value, isReference, isInline, isDefault })
}
}

Expand Down
Loading