Fix webpack loader cache key for resource queries (#19723)

<!--

👋 Hey, thanks for your interest in contributing to Tailwind!

**Please ask first before starting work on any significant new
features.**

It's never a fun experience to have your pull request declined after
investing a lot of time and effort into a new feature. To avoid this
from happening, we request that contributors create a discussion to
first discuss any significant new features.

For more info, check out the contributing guide:


https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md

-->

## Summary

<!--

Provide a summary of the issue and the changes you're making. How does
your change solve the problem?

-->

`@tailwindcss/webpack` currently uses `this.resourcePath` as the cache
key, which ignores the resource query. When the same CSS file is
imported multiple times with different `resourceQuery` values, all of
those imports share a single `CacheEntry`. That means the utilities
discovered for one entry can leak into the CSS output for another entry.

This PR changes the cache key to use `this.resource` (path + query)
instead, while still using `this.resourcePath` for all filesystem work.

## Test plan

<!--

Explain how you tested your changes. Include the exact commands that you
used to verify the change works and include screenshots/screen
recordings of the update behavior in the browser if applicable.

-->

- `pnpm test:integrations -- webpack/loader.test.ts`  
  - Confirms all existing webpack loader integration tests pass.  
- Confirms the new `@tailwindcss/webpack loader isolates cache by
resource including query` test passes, verifying that two entries
importing the same CSS file with different queries produce isolated
outputs (`dist/a.css` only contains `only-a` / `--color-red-500`, and
`dist/b.css` only contains `only-b` / `--color-blue-500`).

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Qingyu Wang 2026-03-21 00:34:11 +08:00 • committed by GitHub
parent 5cb1efdf41
commit 17d324f896
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 85 additions and 4 deletions

View file

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improve canonicalizations for `tracking-*` utilities ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827))
- Fix crash due to invalid characters in candidate ([#19829](https://github.com/tailwindlabs/tailwindcss/pull/19829))
- Ensure query params in imports are considered unique resources when using `@tailwindcss/webpack` ([#19723](https://github.com/tailwindlabs/tailwindcss/pull/19723))
## [4.2.2] - 2026-03-18

View file

@ -418,3 +418,78 @@ test(
`)
},
)
test(
'@tailwindcss/webpack loader isolates cache by resource including query',
{
fs: {
'package.json': json`
{
"main": "./src/index.js",
"browser": "./src/index.js",
"dependencies": {
"css-loader": "^6",
"webpack": "^5",
"webpack-cli": "^5",
"mini-css-extract-plugin": "^2",
"tailwindcss": "workspace:^",
"@tailwindcss/webpack": "workspace:^"
}
}
`,
'webpack.config.js': js`
let MiniCssExtractPlugin = require('mini-css-extract-plugin')
let path = require('node:path')
module.exports = {
mode: 'development',
entry: {
a: './src/a.js',
b: './src/b.js',
},
output: {
clean: true,
},
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /.css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'@tailwindcss/webpack',
path.resolve(__dirname, 'query-loader.js'),
],
},
],
},
}
`,
'query-loader.js': js`
module.exports = function (source) {
if (this.resourceQuery.includes('a')) {
return '@import "tailwindcss/utilities"; @utility only-a {color: var(--color-red-500);}'
}
if (this.resourceQuery.includes('b')) {
return '@import "tailwindcss/utilities"; @utility only-b {color: var(--color-blue-500);}'
}
return source
}
`,
'src/a.js': js`import './index.css?a'`,
'src/b.js': js`import './index.css?b'`,
'src/index.css': css``,
},
},
async ({ fs, exec }) => {
await exec('pnpm webpack --mode=development')
await fs.expectFileToContain('dist/a.css', ['only-a', '--color-red-500'])
await fs.expectFileToContain('dist/b.css', ['only-b', '--color-blue-500'])
await fs.expectFileNotToContain('dist/a.css', ['only-b', '--color-blue-500'])
await fs.expectFileNotToContain('dist/b.css', ['only-a', '--color-red-500'])
},
)

View file

@ -40,8 +40,12 @@ interface CacheEntry {
const cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })
function getContextFromCache(inputFile: string, opts: LoaderOptions): CacheEntry {
let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
function getCacheKey(resourceId: string, opts: LoaderOptions): string {
return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
}
function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry {
let key = getCacheKey(resourceId, opts)
if (cache.has(key)) return cache.get(key)!
let entry: CacheEntry = {
mtimes: new Map<string, number>(),
@ -61,6 +65,7 @@ export default async function tailwindLoader(
let callback = this.async()
let options = this.getOptions() ?? {}
let inputFile = this.resourcePath
let resourceId = this.resource
let base = options.base ?? process.cwd()
let shouldOptimize = options.optimize ?? process.env.NODE_ENV === 'production'
let isCSSModuleFile = inputFile.endsWith('.module.css')
@ -83,7 +88,7 @@ export default async function tailwindLoader(
}
try {
let context = getContextFromCache(inputFile, options)
let context = getContextFromCache(resourceId, options)
let inputBasePath = path.dirname(path.resolve(inputFile))
// Whether this is the first build or not
@ -273,7 +278,7 @@ export default async function tailwindLoader(
callback(null, result)
} catch (error) {
// Clear the cache entry on error to force a full rebuild next time
let key = `${inputFile}:${options.base ?? ''}:${JSON.stringify(options.optimize)}`
let key = getCacheKey(resourceId, options)
cache.delete(key)
DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)