Skip to content

Commit 6d0371a

Browse files
authored
Evaluate theme functions in plugins (#14326)
This PR fixes a bug where CSS `theme()` functions were not evaluated when present in rules added by plugins, using either `@plugin` or registering a plugin in a JS config file. For example, prior to this PR the `theme()` functions in this plugin would make it into the final CSS without being evaluated: ```js // ./my-plugin.js export default plugin(({ addBase }) => { addBase({ '.my-rule': { background: 'theme(colors.primary)', color: 'theme(colors.secondary)', }, }) }) ``` --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent 44dd6da commit 6d0371a

File tree

3 files changed

+131
-3
lines changed

3 files changed

+131
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Ensure content globs defined in `@config` files are relative to that file ([#14314](https://github.com/tailwindlabs/tailwindcss/pull/14314))
1717
- Ensure CSS `theme()` functions are evaluated in media query ranges with collapsed whitespace ((#14321)[https://github.com/tailwindlabs/tailwindcss/pull/14321])
1818
- Fix support for Nuxt projects in the Vite plugin (requires Nuxt 3.13.1+) ([#14319](https://github.com/tailwindlabs/tailwindcss/pull/14319))
19+
- Evaluate theme functions in plugins and JS config files ([#14326](https://github.com/tailwindlabs/tailwindcss/pull/14326))
1920

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

packages/tailwindcss/src/functions.test.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import fs from 'node:fs/promises'
22
import path from 'node:path'
33
import { describe, expect, test } from 'vitest'
4-
import { compileCss } from './test-utils/run'
4+
import { compile } from '.'
5+
import plugin from './plugin'
6+
import { compileCss, optimizeCss } from './test-utils/run'
57

68
const css = String.raw
79

@@ -618,3 +620,121 @@ describe('theme function', () => {
618620
})
619621
})
620622
})
623+
624+
describe('in plugins', () => {
625+
test('CSS theme functions in plugins are properly evaluated', async () => {
626+
let compiled = await compile(
627+
css`
628+
@layer base, utilities;
629+
@plugin "my-plugin";
630+
@theme reference {
631+
--color-red: red;
632+
--color-orange: orange;
633+
--color-blue: blue;
634+
--color-pink: pink;
635+
}
636+
@layer utilities {
637+
@tailwind utilities;
638+
}
639+
`,
640+
{
641+
async loadPlugin() {
642+
return plugin(({ addBase, addUtilities }) => {
643+
addBase({
644+
'.my-base-rule': {
645+
color: 'theme(colors.red)',
646+
'outline-color': 'theme(colors.orange / 15%)',
647+
'background-color': 'theme(--color-blue)',
648+
'border-color': 'theme(--color-pink / 10%)',
649+
},
650+
})
651+
652+
addUtilities({
653+
'.my-utility': {
654+
color: 'theme(colors.red)',
655+
},
656+
})
657+
})
658+
},
659+
},
660+
)
661+
662+
expect(optimizeCss(compiled.build(['my-utility'])).trim()).toMatchInlineSnapshot(`
663+
"@layer base {
664+
.my-base-rule {
665+
color: red;
666+
background-color: #00f;
667+
border-color: #ffc0cb1a;
668+
outline-color: #ffa50026;
669+
}
670+
}
671+
672+
@layer utilities {
673+
.my-utility {
674+
color: red;
675+
}
676+
}"
677+
`)
678+
})
679+
})
680+
681+
describe('in JS config files', () => {
682+
test('CSS theme functions in config files are properly evaluated', async () => {
683+
let compiled = await compile(
684+
css`
685+
@layer base, utilities;
686+
@config "./my-config.js";
687+
@theme reference {
688+
--color-red: red;
689+
--color-orange: orange;
690+
}
691+
@layer utilities {
692+
@tailwind utilities;
693+
}
694+
`,
695+
{
696+
loadConfig: async () => ({
697+
theme: {
698+
extend: {
699+
colors: {
700+
primary: 'theme(colors.red)',
701+
secondary: 'theme(--color-orange)',
702+
},
703+
},
704+
},
705+
plugins: [
706+
plugin(({ addBase, addUtilities }) => {
707+
addBase({
708+
'.my-base-rule': {
709+
background: 'theme(colors.primary)',
710+
color: 'theme(colors.secondary)',
711+
},
712+
})
713+
714+
addUtilities({
715+
'.my-utility': {
716+
color: 'theme(colors.red)',
717+
},
718+
})
719+
}),
720+
],
721+
}),
722+
},
723+
)
724+
725+
expect(optimizeCss(compiled.build(['my-utility'])).trim()).toMatchInlineSnapshot(`
726+
"@layer base {
727+
.my-base-rule {
728+
color: orange;
729+
background: red;
730+
}
731+
}
732+
733+
@layer utilities {
734+
.my-utility {
735+
color: red;
736+
}
737+
}"
738+
`)
739+
})
740+
})

packages/tailwindcss/src/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,11 @@ async function parseCss(
372372
substituteAtApply(ast, designSystem)
373373
}
374374

375-
// Replace `theme()` function calls with the actual theme variables.
376-
if (css.includes(THEME_FUNCTION_INVOCATION)) {
375+
// Replace `theme()` function calls with the actual theme variables. Plugins
376+
// could register new rules that include functions, and JS config files could
377+
// also contain functions or plugins that use functions so we need to evaluate
378+
// functions if either of those are present.
379+
if (plugins.length > 0 || configs.length > 0 || css.includes(THEME_FUNCTION_INVOCATION)) {
377380
substituteFunctions(ast, pluginApi)
378381
}
379382

@@ -482,6 +485,10 @@ export async function compile(
482485
return compiledCss
483486
}
484487

488+
// Arbitrary values (`text-[theme(--color-red-500)]`) and arbitrary
489+
// properties (`[--my-var:theme(--color-red-500)]`) can contain function
490+
// calls so we need evaluate any functions we find there that weren't in
491+
// the source CSS.
485492
substituteFunctions(newNodes, pluginApi)
486493

487494
previousAstNodeCount = newNodes.length

0 commit comments

Comments
 (0)