Skip to content

Conversation

@timneutkens
Copy link
Contributor

@timneutkens timneutkens commented Jan 28, 2026

Summary

This PR adds a new @tailwindcss/webpack package 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:

  • Eliminates PostCSS as a middleman - works directly with CSS strings (no AST conversions)
  • Simpler and more efficient - follows the same pattern as @tailwindcss/vite
  • Better for Turbopack - gives direct control over dependency reporting via webpack's loader API

How it works

The loader mirrors the Vite plugin's approach:

  1. Uses compile() from @tailwindcss/node to parse CSS and resolve @apply directives
  2. Uses Scanner from @tailwindcss/oxide to scan content files for utility candidates
  3. Reports dependencies via this.addDependency() and this.addContextDependency()
  4. Optionally optimizes output with Lightning CSS

Usage

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/i,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          '@tailwindcss/webpack',  // No PostCSS needed!
        ],
      },
    ],
  },
}

Options

  • base - The base directory to scan for class candidates (defaults to process.cwd())
  • optimize - Whether to optimize/minify the output CSS (defaults to true in production)

Files added

  • packages/@tailwindcss-webpack/ - New package
    • src/index.ts - Main loader implementation
    • src/index.cts - CommonJS entry point for webpack compatibility
    • package.json, tsconfig.json, tsup.config.ts, README.md
  • integrations/webpack/loader.test.ts - Integration tests
  • integrations/utils.ts - Added webpack override for transitive dependencies

Test plan

  • Build test - verifies basic compilation
  • Watch test - verifies HMR when adding new Tailwind classes
  • @apply test - verifies @apply directives work correctly
  • Optimization test - verifies minification works

@timneutkens timneutkens marked this pull request as ready for review January 29, 2026 12:10
@timneutkens timneutkens requested a review from a team as a code owner January 29, 2026 12:10
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

Walkthrough

Adds a new @tailwindcss/webpack package: a webpack loader implementing LoaderOptions (base, optimize) and a default tailwindLoader with caching, dependency tracking, and incremental rebuilds. Includes package.json, TypeScript and tsup build configs, a CJS entry, README, comprehensive webpack loader tests, and a CHANGELOG entry. Workspace catalog gains a webpack: ^5 entry. integrations/utils.ts now adds a pnpm override mapping @tailwindcss/webpack>tailwindcss to the resolved local tailwindcss version when processing the tailwindcss package.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add @tailwindcss/webpack loader for Tailwind CSS v4' clearly and concisely summarizes the main change in the pull request—adding a new webpack loader package.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the purpose, implementation details, usage, and test plan for the new webpack loader.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 async fs.stat instead of synchronous fs.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.

inputFile is this.resourcePath, which is already an absolute path. Using path.resolve(base, inputFile) when inputFile is absolute will just return inputFile unchanged (since path.resolve ignores preceding segments when encountering an absolute path). This works correctly but the intent may be clearer without base:

-      let resolvedInputFile = path.resolve(base, inputFile)
+      let resolvedInputFile = path.resolve(inputFile)

Comment on lines +32 to +33
test: /.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'],
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Member

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.

Copy link
Contributor

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.

@RobinMalfait RobinMalfait enabled auto-merge (squash) January 29, 2026 14:12
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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)

Comment on lines +182 to +209
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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@RobinMalfait RobinMalfait merged commit bccf4bb into tailwindlabs:main Jan 29, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants