From 17d324f89680c2675b64959ded7558e84532d74c Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:34:11 +0800 Subject: [PATCH] Fix webpack loader cache key for resource queries (#19723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `@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 - `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 --- CHANGELOG.md | 1 + integrations/webpack/loader.test.ts | 75 ++++++++++++++++++++++ packages/@tailwindcss-webpack/src/index.ts | 13 ++-- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bafb6c744..fccae8e6f 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 - 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 diff --git a/integrations/webpack/loader.test.ts b/integrations/webpack/loader.test.ts index 3a3782c0d..9c3d870af 100644 --- a/integrations/webpack/loader.test.ts +++ b/integrations/webpack/loader.test.ts @@ -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']) + }, +) diff --git a/packages/@tailwindcss-webpack/src/index.ts b/packages/@tailwindcss-webpack/src/index.ts index 2c78a552b..6d02f1f0b 100644 --- a/packages/@tailwindcss-webpack/src/index.ts +++ b/packages/@tailwindcss-webpack/src/index.ts @@ -40,8 +40,12 @@ interface CacheEntry { const cache = new QuickLRU({ 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(), @@ -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)}`)