Ignore @tailwind utilities inside @reference (#17836)

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
<style>
@reference "./styles.css";
</style>
<template> <!-- ... --> </template>
```

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
<style>
@reference "./my-theme.css";
</style>
<template> <!-- ... --> </template>
```

Fixes #17693
This commit is contained in:
Jordan Pittman 2025-05-05 11:21:55 -04:00 • committed by GitHub
parent d38554d110
commit 6a1df6acf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 137 additions and 1 deletions

View file

@ -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

View file

@ -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()
})
})

View file

@ -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(')) {