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:
parent
5cb1efdf41
commit
17d324f896
3 changed files with 85 additions and 4 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)}`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue