Skip to content

Commit 17d324f

Browse files
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>
1 parent 5cb1efd commit 17d324f

3 files changed

Lines changed: 85 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
- Improve canonicalizations for `tracking-*` utilities ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827))
1717
- Fix crash due to invalid characters in candidate ([#19829](https://github.com/tailwindlabs/tailwindcss/pull/19829))
18+
- Ensure query params in imports are considered unique resources when using `@tailwindcss/webpack` ([#19723](https://github.com/tailwindlabs/tailwindcss/pull/19723))
1819

1920
## [4.2.2] - 2026-03-18
2021

integrations/webpack/loader.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,3 +418,78 @@ test(
418418
`)
419419
},
420420
)
421+
422+
test(
423+
'@tailwindcss/webpack loader isolates cache by resource including query',
424+
{
425+
fs: {
426+
'package.json': json`
427+
{
428+
"main": "./src/index.js",
429+
"browser": "./src/index.js",
430+
"dependencies": {
431+
"css-loader": "^6",
432+
"webpack": "^5",
433+
"webpack-cli": "^5",
434+
"mini-css-extract-plugin": "^2",
435+
"tailwindcss": "workspace:^",
436+
"@tailwindcss/webpack": "workspace:^"
437+
}
438+
}
439+
`,
440+
'webpack.config.js': js`
441+
let MiniCssExtractPlugin = require('mini-css-extract-plugin')
442+
let path = require('node:path')
443+
444+
module.exports = {
445+
mode: 'development',
446+
entry: {
447+
a: './src/a.js',
448+
b: './src/b.js',
449+
},
450+
output: {
451+
clean: true,
452+
},
453+
plugins: [new MiniCssExtractPlugin()],
454+
module: {
455+
rules: [
456+
{
457+
test: /.css$/i,
458+
use: [
459+
MiniCssExtractPlugin.loader,
460+
'css-loader',
461+
'@tailwindcss/webpack',
462+
path.resolve(__dirname, 'query-loader.js'),
463+
],
464+
},
465+
],
466+
},
467+
}
468+
`,
469+
'query-loader.js': js`
470+
module.exports = function (source) {
471+
if (this.resourceQuery.includes('a')) {
472+
return '@import "tailwindcss/utilities"; @utility only-a {color: var(--color-red-500);}'
473+
}
474+
475+
if (this.resourceQuery.includes('b')) {
476+
return '@import "tailwindcss/utilities"; @utility only-b {color: var(--color-blue-500);}'
477+
}
478+
479+
return source
480+
}
481+
`,
482+
'src/a.js': js`import './index.css?a'`,
483+
'src/b.js': js`import './index.css?b'`,
484+
'src/index.css': css``,
485+
},
486+
},
487+
async ({ fs, exec }) => {
488+
await exec('pnpm webpack --mode=development')
489+
490+
await fs.expectFileToContain('dist/a.css', ['only-a', '--color-red-500'])
491+
await fs.expectFileToContain('dist/b.css', ['only-b', '--color-blue-500'])
492+
await fs.expectFileNotToContain('dist/a.css', ['only-b', '--color-blue-500'])
493+
await fs.expectFileNotToContain('dist/b.css', ['only-a', '--color-red-500'])
494+
},
495+
)

packages/@tailwindcss-webpack/src/index.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ interface CacheEntry {
4040

4141
const cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })
4242

43-
function getContextFromCache(inputFile: string, opts: LoaderOptions): CacheEntry {
44-
let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
43+
function getCacheKey(resourceId: string, opts: LoaderOptions): string {
44+
return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
45+
}
46+
47+
function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry {
48+
let key = getCacheKey(resourceId, opts)
4549
if (cache.has(key)) return cache.get(key)!
4650
let entry: CacheEntry = {
4751
mtimes: new Map<string, number>(),
@@ -61,6 +65,7 @@ export default async function tailwindLoader(
6165
let callback = this.async()
6266
let options = this.getOptions() ?? {}
6367
let inputFile = this.resourcePath
68+
let resourceId = this.resource
6469
let base = options.base ?? process.cwd()
6570
let shouldOptimize = options.optimize ?? process.env.NODE_ENV === 'production'
6671
let isCSSModuleFile = inputFile.endsWith('.module.css')
@@ -83,7 +88,7 @@ export default async function tailwindLoader(
8388
}
8489

8590
try {
86-
let context = getContextFromCache(inputFile, options)
91+
let context = getContextFromCache(resourceId, options)
8792
let inputBasePath = path.dirname(path.resolve(inputFile))
8893

8994
// Whether this is the first build or not
@@ -273,7 +278,7 @@ export default async function tailwindLoader(
273278
callback(null, result)
274279
} catch (error) {
275280
// Clear the cache entry on error to force a full rebuild next time
276-
let key = `${inputFile}:${options.base ?? ''}:${JSON.stringify(options.optimize)}`
281+
let key = getCacheKey(resourceId, options)
277282
cache.delete(key)
278283

279284
DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)

0 commit comments

Comments
 (0)