-
-
Notifications
You must be signed in to change notification settings - Fork 5k
Add @tailwindcss/webpack loader for Tailwind CSS v4 #19610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add @tailwindcss/webpack loader for Tailwind CSS v4 #19610
Conversation
WalkthroughAdds a new 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@integrations/webpack/loader.test.ts`:
- Around line 32-33: Replace the loose regex used for CSS rules (currently
written as "test: /.css$/i") with an escaped-dot pattern so it only matches
".css" files; update each occurrence in the webpack config snippets that use
MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack' (and the three
other matching rule blocks) to use "test: /\.css$/i" instead.
🧹 Nitpick comments (2)
packages/@tailwindcss-webpack/src/index.ts (2)
139-145: Consider using asyncfs.statinstead of synchronousfs.statSync.Using synchronous file system operations in an async webpack loader can block the Node.js event loop, potentially degrading build performance when processing many files concurrently. The same applies to Line 241.
♻️ Suggested refactor using async/await
for (let file of files) { let changedTime: number | null = null try { - changedTime = fs.statSync(file)?.mtimeMs ?? null + changedTime = (await fs.promises.stat(file))?.mtimeMs ?? null } catch { // File might not exist }And at Line 241:
- let stats = fs.statSync(basePath) + let stats = await fs.promises.stat(basePath)
213-213: Potential redundant path resolution.
inputFileisthis.resourcePath, which is already an absolute path. Usingpath.resolve(base, inputFile)wheninputFileis absolute will just returninputFileunchanged (sincepath.resolveignores preceding segments when encountering an absolute path). This works correctly but the intent may be clearer withoutbase:- let resolvedInputFile = path.resolve(base, inputFile) + let resolvedInputFile = path.resolve(inputFile)
| test: /.css$/i, | ||
| use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escape the dot in the CSS test regex.
/.css$/i matches any character + css, so it can also match scss and similar. Prefer \.css$ for exact .css.
🩹 Suggested fix
- test: /.css$/i,
+ test: /\.css$/i,(Apply the same change to all four webpack.config.js snippets in this file.)
Also applies to: 92-93, 177-178, 234-235
🤖 Prompt for AI Agents
In `@integrations/webpack/loader.test.ts` around lines 32 - 33, Replace the loose
regex used for CSS rules (currently written as "test: /.css$/i") with an
escaped-dot pattern so it only matches ".css" files; update each occurrence in
the webpack config snippets that use MiniCssExtractPlugin.loader, 'css-loader',
'@tailwindcss/webpack' (and the three other matching rule blocks) to use "test:
/\.css$/i" instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is true, this is a test where we are only dealing with CSS files so this is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RobinMalfait, understood! You're right that in a controlled test environment where you only have CSS files, the distinction doesn't matter. Thanks for the context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/`@tailwindcss-webpack/src/index.ts:
- Around line 182-209: When recreating the scanner (in the block where
context.scanner is set when context.scanner === null || rebuildStrategy ===
'full'), also clear the existing candidate set to avoid carrying stale entries:
explicitly call context.candidates.clear() (or reassign to a new Set)
immediately before/after creating the new Scanner instance (related symbols:
context.scanner, context.candidates, rebuildStrategy, Scanner, scan()) so
subsequent scan() iterations only add current candidates.
🧹 Nitpick comments (1)
packages/@tailwindcss-webpack/src/index.ts (1)
274-277: Cache key logic is duplicated.The cache key format on line 276 duplicates the logic from
getContextFromCache(line 44). Consider extracting a helper function to avoid inconsistencies if the key format changes.♻️ Suggested refactor
+function getCacheKey(inputFile: string, opts: LoaderOptions): string { + return `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` +} + function getContextFromCache(inputFile: string, opts: LoaderOptions): CacheEntry { - let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` + let key = getCacheKey(inputFile, opts) if (cache.has(key)) return cache.get(key)! // ... } // In error handler: - let key = `${inputFile}:${options.base ?? ''}:${JSON.stringify(options.optimize)}` + let key = getCacheKey(inputFile, options) cache.delete(key)
| if (context.scanner === null || rebuildStrategy === 'full') { | ||
| DEBUG && I.start('Setup scanner') | ||
| let sources = (() => { | ||
| // Disable auto source detection | ||
| if (compiler.root === 'none') { | ||
| return [] | ||
| } | ||
|
|
||
| // No root specified, use the base directory | ||
| if (compiler.root === null) { | ||
| return [{ base, pattern: '**/*', negated: false }] | ||
| } | ||
|
|
||
| // Use the specified root | ||
| return [{ ...compiler.root, negated: false }] | ||
| })().concat(compiler.sources) | ||
|
|
||
| context.scanner = new Scanner({ sources }) | ||
| DEBUG && I.end('Setup scanner') | ||
| } | ||
|
|
||
| // Scan for candidates if utilities are used | ||
| if (compiler.features & Features.Utilities) { | ||
| DEBUG && I.start('Scan for candidates') | ||
| for (let candidate of context.scanner.scan()) { | ||
| context.candidates.add(candidate) | ||
| } | ||
| DEBUG && I.end('Scan for candidates') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Candidates not cleared on full rebuild may cause stale CSS output.
When rebuildStrategy === 'full', the scanner is recreated (line 182) but context.candidates is not cleared. This means candidates from previous builds persist, potentially generating CSS for classes that have been removed from source files.
🐛 Proposed fix
// Setup or update scanner if needed
if (context.scanner === null || rebuildStrategy === 'full') {
DEBUG && I.start('Setup scanner')
+ // Clear candidates on full rebuild to remove stale entries
+ if (rebuildStrategy === 'full') {
+ context.candidates.clear()
+ }
let sources = (() => {🤖 Prompt for AI Agents
In `@packages/`@tailwindcss-webpack/src/index.ts around lines 182 - 209, When
recreating the scanner (in the block where context.scanner is set when
context.scanner === null || rebuildStrategy === 'full'), also clear the existing
candidate set to avoid carrying stale entries: explicitly call
context.candidates.clear() (or reassign to a new Set) immediately before/after
creating the new Scanner instance (related symbols: context.scanner,
context.candidates, rebuildStrategy, Scanner, scan()) so subsequent scan()
iterations only add current candidates.
Summary
This PR adds a new
@tailwindcss/webpackpackage that provides a dedicated webpack loader for Tailwind CSS v4. This loader works with both standard webpack and Turbopack's webpack loader compatibility layer.Why a dedicated loader?
The current webpack integration uses
postcss-loader+@tailwindcss/postcss. While this works, a dedicated loader:@tailwindcss/viteHow it works
The loader mirrors the Vite plugin's approach:
compile()from@tailwindcss/nodeto parse CSS and resolve@applydirectivesScannerfrom@tailwindcss/oxideto scan content files for utility candidatesthis.addDependency()andthis.addContextDependency()Usage
Options
base- The base directory to scan for class candidates (defaults toprocess.cwd())optimize- Whether to optimize/minify the output CSS (defaults totruein production)Files added
packages/@tailwindcss-webpack/- New packagesrc/index.ts- Main loader implementationsrc/index.cts- CommonJS entry point for webpack compatibilitypackage.json,tsconfig.json,tsup.config.ts,README.mdintegrations/webpack/loader.test.ts- Integration testsintegrations/utils.ts- Added webpack override for transitive dependenciesTest plan
@applytest - verifies@applydirectives work correctly