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 <4323180+adamwathan@users.noreply.github.com>
This commit is contained in:
Adam Wathan 2024-09-04 11:49:01 -04:00 • committed by GitHub
parent 44dd6da4fe
commit 6d0371afb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 131 additions and 3 deletions

View file

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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))
## [4.0.0-alpha.21] - 2024-09-02

View file

@ -1,7 +1,9 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
import { compileCss } from './test-utils/run'
import { compile } from '.'
import plugin from './plugin'
import { compileCss, optimizeCss } from './test-utils/run'
const css = String.raw
@ -618,3 +620,121 @@ describe('theme function', () => {
})
})
})
describe('in plugins', () => {
test('CSS theme functions in plugins are properly evaluated', async () => {
let compiled = await compile(
css`
@layer base, utilities;
@plugin "my-plugin";
@theme reference {
--color-red: red;
--color-orange: orange;
--color-blue: blue;
--color-pink: pink;
}
@layer utilities {
@tailwind utilities;
}
`,
{
async loadPlugin() {
return plugin(({ addBase, addUtilities }) => {
addBase({
'.my-base-rule': {
color: 'theme(colors.red)',
'outline-color': 'theme(colors.orange / 15%)',
'background-color': 'theme(--color-blue)',
'border-color': 'theme(--color-pink / 10%)',
},
})
addUtilities({
'.my-utility': {
color: 'theme(colors.red)',
},
})
})
},
},
)
expect(optimizeCss(compiled.build(['my-utility'])).trim()).toMatchInlineSnapshot(`
"@layer base {
.my-base-rule {
color: red;
background-color: #00f;
border-color: #ffc0cb1a;
outline-color: #ffa50026;
}
}
@layer utilities {
.my-utility {
color: red;
}
}"
`)
})
})
describe('in JS config files', () => {
test('CSS theme functions in config files are properly evaluated', async () => {
let compiled = await compile(
css`
@layer base, utilities;
@config "./my-config.js";
@theme reference {
--color-red: red;
--color-orange: orange;
}
@layer utilities {
@tailwind utilities;
}
`,
{
loadConfig: async () => ({
theme: {
extend: {
colors: {
primary: 'theme(colors.red)',
secondary: 'theme(--color-orange)',
},
},
},
plugins: [
plugin(({ addBase, addUtilities }) => {
addBase({
'.my-base-rule': {
background: 'theme(colors.primary)',
color: 'theme(colors.secondary)',
},
})
addUtilities({
'.my-utility': {
color: 'theme(colors.red)',
},
})
}),
],
}),
},
)
expect(optimizeCss(compiled.build(['my-utility'])).trim()).toMatchInlineSnapshot(`
"@layer base {
.my-base-rule {
color: orange;
background: red;
}
}
@layer utilities {
.my-utility {
color: red;
}
}"
`)
})
})

View file

@ -372,8 +372,11 @@ async function parseCss(
substituteAtApply(ast, designSystem)
}
// Replace `theme()` function calls with the actual theme variables.
if (css.includes(THEME_FUNCTION_INVOCATION)) {
// Replace `theme()` function calls with the actual theme variables. Plugins
// could register new rules that include functions, and JS config files could
// also contain functions or plugins that use functions so we need to evaluate
// functions if either of those are present.
if (plugins.length > 0 || configs.length > 0 || css.includes(THEME_FUNCTION_INVOCATION)) {
substituteFunctions(ast, pluginApi)
}
@ -482,6 +485,10 @@ export async function compile(
return compiledCss
}
// Arbitrary values (`text-[theme(--color-red-500)]`) and arbitrary
// properties (`[--my-var:theme(--color-red-500)]`) can contain function
// calls so we need evaluate any functions we find there that weren't in
// the source CSS.
substituteFunctions(newNodes, pluginApi)
previousAstNodeCount = newNodes.length