From 6d0371afb17778fd12fe072937ae66dfc852818a Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Wed, 4 Sep 2024 11:49:01 -0400 Subject: [PATCH] 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> --- CHANGELOG.md | 1 + packages/tailwindcss/src/functions.test.ts | 122 ++++++++++++++++++++- packages/tailwindcss/src/index.ts | 11 +- 3 files changed, 131 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9035dc387..5c56a754e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/tailwindcss/src/functions.test.ts b/packages/tailwindcss/src/functions.test.ts index 69cbcec42..f33be7fc0 100644 --- a/packages/tailwindcss/src/functions.test.ts +++ b/packages/tailwindcss/src/functions.test.ts @@ -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; + } + }" + `) + }) +}) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index db93f39c6..893e83984 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -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