From 6a1df6acf691966e4f79393c3d34c93e2272529f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 5 May 2025 11:21:55 -0400 Subject: [PATCH] Ignore `@tailwind utilities` inside `@reference` (#17836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You can use `@reference "tailwindcss"` or `@reference "../path/to/your/css/file.css"` to reference your theme for use in `@apply`, `theme(…)`, etc… Unfortunatley, because the imported file still contains `@tailwind utilities` it would trigger a re-scan of the filesystem — even though the use of `@reference` ensures that no CSS can actually be output by the import itself. This PR does two things: - Adds some explicit feature detection tests for what features we pick up in a stylesheet based on the CSS written and how things are imported - Explicitly ignores `@tailwind utilities` inside of `@reference` so it isn't a trigger for file scanning Because of how Vite itself handles dependencies editing files on disk will still trigger a rebuild of any file using `@reference`. This is because Vite rebuilds files when _any_ of its transitive dependencies change. For example, given this Vue file: ```vue ``` And this stylesheet: ```css @import "tailwindcss"; ``` The dependency chain looks like this: `file.vue -> styles.css -> {all the sources in your project}` Vite sees that a file (e.g. `index.html`) has changed, thus `styles.css` needs change, which means `file.vue` needs to be compiled again as well. Now in reality we depend on the _on disk_ version of styles.css not the compiled version but Vite itself doesn't know that (or have a way to indicate this afaik). Coming up with a solution to that problem will have to be a separate PR — but there is a workaround: ### 1. Inline the imports from `@import "tailwindcss";` Replace this in your main stylesheet: ```css @import "tailwindcss"; ``` with this (this is basically what `node_modules/tailwindcss/index.css` is): ```css @layer theme, base, components, utilities; @import 'tailwindcss/theme' layer(theme); @import 'tailwindcss/preflight' layer(base); @import 'tailwindcss/utilities' layer(utilities); /* the rest of your styles imports, styles, etc… */ ``` ### 2. Split your stylesheet into "main" and "theme" parts Your "theme" is comprised of the `@import 'tailwindcss/theme' layer(theme);` import, any custom `@theme` blocks, any `@config` directives, and any `@plugin` directives. Move all of these into their own file. For example, replace this with two files: ```css @layer theme, base, components, utilities; @import 'tailwindcss/theme' layer(theme); @import 'tailwindcss/preflight' layer(base); @import 'tailwindcss/utilities' layer(utilities); @theme { --color-primary: #c0ffee; } @plugin "./my-plugin.js"; /* the rest of your styles imports, styles, etc… */ ``` with a theme file: ```css @import 'tailwindcss/theme' layer(theme); /* all your `@theme` stuff goes in this file */ @theme { --color-primary: #c0ffee; } /* additionally any @config or @plugin does too */ @plugin "./my-plugin.js"; ``` and your main CSS file: ```css @layer theme, base, components, utilities; @import './my-theme.css'; /* I replaced this import */ @import 'tailwindcss/preflight' layer(base); @import 'tailwindcss/utilities' layer(utilities); /* the rest of your styles imports, styles, etc… */ ``` ### 3. Import only your "theme" file in your Vue components / CSS modules / etc… ```vue ``` Fixes #17693 --- CHANGELOG.md | 1 + packages/tailwindcss/src/index.test.ts | 129 ++++++++++++++++++++++++- packages/tailwindcss/src/index.ts | 8 ++ 3 files changed, 137 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16cf63f53..ef34a1a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure negative arbitrary `scale` values generate negative values ([#17831](https://github.com/tailwindlabs/tailwindcss/pull/17831)) - Fix HAML extraction with embedded Ruby ([#17846](https://github.com/tailwindlabs/tailwindcss/pull/17846)) +- Don't scan files for utilities when using `@reference` ([#17836](https://github.com/tailwindlabs/tailwindcss/pull/17836)) ## [4.1.5] - 2025-04-30 diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 88ea541a1..00e73a90b 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import path from 'node:path' import { describe, expect, it, test } from 'vitest' -import { compile, Polyfills } from '.' +import { compile, Features, Polyfills } from '.' import type { PluginAPI } from './compat/plugin-api' import plugin from './plugin' import { compileCss, optimizeCss, run } from './test-utils/run' @@ -5373,3 +5373,130 @@ describe('`@property` polyfill', async () => { `) }) }) + +describe('feature detection', () => { + test('using `@tailwind utilities`', async () => { + let compiler = await compile(css` + @tailwind utilities; + `) + + expect(compiler.features & Features.Utilities).toBeTruthy() + }) + + test('using `@apply`', async () => { + let compiler = await compile(css` + .foo { + @apply underline; + } + `) + + expect(compiler.features & Features.AtApply).toBeTruthy() + }) + + test('using `@import`', async () => { + let compiler = await compile( + css` + @import 'tailwindcss/preflight'; + `, + { loadStylesheet: async (_, base) => ({ base, content: '' }) }, + ) + + expect(compiler.features & Features.AtImport).toBeTruthy() + }) + + test('using `@reference`', async () => { + let compiler = await compile( + css` + @import 'tailwindcss/preflight'; + `, + { loadStylesheet: async (_, base) => ({ base, content: '' }) }, + ) + + // There's little difference between `@reference` and `@import` on a feature + // level as it's just like an import but with side-effect behavior. + // + // It's really just shorthand for `@import "…" reference;` + expect(compiler.features & Features.AtImport).toBeTruthy() + }) + + test('using `theme(…)`', async () => { + let compiler = await compile( + css` + @theme { + --color-red: #f00; + } + + .foo { + color: theme(--color-red); + } + `, + { loadStylesheet: async (_, base) => ({ base, content: '' }) }, + ) + + expect(compiler.features & Features.ThemeFunction).toBeTruthy() + }) + + test('using `@plugin`', async () => { + let compiler = await compile( + css` + @plugin "./some-plugin.js"; + `, + { loadModule: async (_, base) => ({ base, module: () => {} }) }, + ) + + expect(compiler.features & Features.JsPluginCompat).toBeTruthy() + }) + + test('using `@config`', async () => { + let compiler = await compile( + css` + @config "./some-config.js"; + `, + { loadModule: async (_, base) => ({ base, module: {} }) }, + ) + + expect(compiler.features & Features.JsPluginCompat).toBeTruthy() + }) + + test('using `@variant`', async () => { + let compiler = await compile(css` + .foo { + @variant dark { + color: red; + } + } + `) + + expect(compiler.features & Features.Variants).toBeTruthy() + }) + + test('legacy `@variant` syntax does not trigger the variant feature', async () => { + let compiler = await compile(css` + @variant dark (&:is(.dark, .dark *)); + `) + + expect(compiler.features & Features.Variants).toBeFalsy() + }) + + test('`@tailwind utilities` is ignored inside `@reference`', async () => { + let compiler = await compile( + css` + @reference "tailwindcss/utilities"; + `, + { + async loadStylesheet(id, base) { + return { + base, + content: css` + @tailwind utilities; + `, + } + }, + }, + ) + + // We see @tailwind utilities but because of @reference it is ignored + expect(compiler.features & Features.AtImport).toBeTruthy() + expect(compiler.features & Features.Utilities).toBeFalsy() + }) +}) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 8d953782f..e8f34e366 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -162,6 +162,14 @@ async function parseCss( return } + // When inside `@reference` we should treat `@tailwind utilities` as if + // it wasn't there in the first place. This should also let `build()` + // return the cached static AST. + if (context.reference) { + replaceWith([]) + return + } + let params = segment(node.params, ' ') for (let param of params) { if (param.startsWith('source(')) {