Skip to content

Fix @tailwindcss/postcss losing watch dependencies after a compile error - #20395

Open
andershagbard wants to merge 1 commit into
tailwindlabs:mainfrom
andershagbard:fix/postcss-watch-mode-dependency-tracking
Open

Fix @tailwindcss/postcss losing watch dependencies after a compile error#20395
andershagbard wants to merge 1 commit into
tailwindlabs:mainfrom
andershagbard:fix/postcss-watch-mode-dependency-tracking

Conversation

@andershagbard

Copy link
Copy Markdown

Summary

With @tailwindcss/postcss + postcss-loader (e.g. webpack), fixing a
CSS error during --watch doesn't resume compilation. Repro:

  1. webpack --watch with @tailwindcss/postcss in postcss.config.js.
  2. Introduce @apply some-unknown-class (or break an @imported
    partial with any error) in a watched CSS file.
  3. Fix the error and save again — webpack never recompiles. Not just
    for that file: any previously-tracked CSS import stops being
    watched until some unrelated, still-watched file happens to change.

Root cause

Any compile error currently makes the plugin throw inside its
Once() hook. That causes the whole postcss().process() promise to
reject instead of resolve. postcss-loader only reads
result.messages — and therefore only calls this.addDependency()
from a resolved result:

// postcss-loader/dist/index.js
try {
  result = await processor.process(root || content, processOptions);
} catch (error) {
  reportError(this, callback, error);
  return; // <- result.messages is never read here
}
for (const message of result.messages) {
  if (message.type === "dependency") this.addDependency(message.file);
  ...
}

So on rejection, none of the dependency messages the plugin pushed
(including from any previously successful build) are ever seen by
webpack, which drops every file in the CSS import graph from its
watcher — not just the file that errored — until some other,
still-watched file happens to change (or the entry file itself is
touched directly).

This isn't new: #17754 fixed exactly this by not throwing
(console.error(error); root.removeAll()). #18373 then reinstated the
throw to fix #18370 (errors weren't failing the build), unintentionally
reintroducing this regression as a side effect, while leaving behind a
comment describing the behavior the code no longer has.

Fix

Gate the behavior on the plugin's existing optimize option (already
defaults to NODE_ENV === 'production'):

  • When optimize is enabled (typically production builds), keep
    throwing — this preserves Angular: Error in PostCSS + tailwind 4 doesn't break the build #18370's fix, so broken CSS still fails
    the build.
  • Otherwise, report the error via result.warn() instead of throwing,
    so result still resolves and every dependency message (including
    ones re-derived from the last successful build, for cases where the
    failure happens before this attempt's own import/candidate scanning
    ran) reaches the consumer. The stylesheet keeps serving the last
    known-good output rather than going blank while the error is fixed.

Test plan

  • Added unit tests in packages/@tailwindcss-postcss/src/index.test.ts
    asserting dependency messages survive a compile error outside of
    optimize mode (and that optimize mode still throws). These fail
    against the current throw-only behavior and pass with this fix.
  • integrations/postcss/index.test.ts's existing rebuild error recovery test needed a small adjustment: recovered builds now also
    log Waiting for file changes... (same as any successful build), so
    a later wait for that exact message needed process.flush() first
    to avoid matching a stale occurrence from the recovered build.
  • Verified full suites pass: @tailwindcss/postcss unit tests (22/22),
    integrations/postcss/ (31/31), integrations/webpack/ (8/8,
    including a webpack+postcss-loader watch test).
  • Manually reproduced the original bug with webpack --watch in a
    real project and confirmed this fixes it.

Any compile error (invalid `@apply`, a broken `@import`/`@reference`, a
plain syntax error in an imported file) causes the plugin to throw,
which makes the whole `postcss().process()` promise reject instead of
resolve. Consumers that read `result.messages` to register file
dependencies -- most notably `postcss-loader` (webpack) -- only ever do
so from a *resolved* result, so on rejection none of the dependency
messages for the CSS import graph are seen, dropping every file (not
just the one that errored) from the watcher until some other,
still-watched file happens to change.

This was previously fixed in tailwindlabs#17754 by not throwing, then
unintentionally reintroduced by tailwindlabs#18373, which reinstated the throw to
fix tailwindlabs#18370 (errors weren't failing production builds).

Resolve both: throw and fail the build when `optimize` is enabled
(preserves tailwindlabs#18370's fix), otherwise report the error via `result.warn()`
and keep serving the last known-good output, so `result` still resolves
and dependency tracking -- including the scanner's content-glob
dependencies when the failure happens before this attempt's own scan
runs -- survives the error.
@andershagbard
andershagbard requested a review from a team as a code owner August 7, 2026 08:24
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PostCSS plugin now re-registers cached dependencies after compilation failures. Non-optimized builds emit warnings, preserve the last successful stylesheet, and continue processing. Optimized builds still reject with compilation errors. Tests cover dependency tracking, unknown utilities in imported stylesheets, and watcher synchronization during rebuild recovery.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: preserving watch dependencies after a compile error in @tailwindcss/postcss.
Description check ✅ Passed The description clearly explains the watch-mode regression, root cause, conditional fix, and test coverage.
Linked Issues check ✅ Passed The PR preserves build failure behavior for optimized builds, satisfying issue #18370 while fixing dependency recovery for non-optimized watch builds.
Out of Scope Changes check ✅ Passed The implementation and test changes directly support error handling, dependency tracking, rebuild recovery, and the linked issue requirements.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e2010f8-e45a-45b3-ac7c-6eabf852a7b7

📥 Commits

Reviewing files that changed from the base of the PR and between e566a92 and 81f0172.

📒 Files selected for processing (3)
  • integrations/postcss/index.test.ts
  • packages/@tailwindcss-postcss/src/index.test.ts
  • packages/@tailwindcss-postcss/src/index.ts

Comment on lines 402 to +426
console.error(error)

if (error && typeof error === 'object' && 'message' in error) {
throw root.error(`${error.message}`)
let message =
error && typeof error === 'object' && 'message' in error ? `${error.message}` : `${error}`

// In optimized (typically production) builds we want compilation errors to fail
// the build outright so broken CSS never ships.
//
// Otherwise, we avoid throwing here. Throwing causes PostCSS's `process()` promise
// to reject instead of resolve. Tools that rely on `result.messages` to register
// file dependencies (e.g. `postcss-loader`/webpack) only ever read `result.messages`
// from a *resolved* result, so on rejection none of the dependency messages above —
// or from any previously successful build — are seen. That drops every file in the
// CSS import graph from the watcher, not just the one that failed, and nothing
// recompiles again until some other, still-watched file happens to change.
//
// We instead report the error as a warning so `result` still resolves and dependency
// tracking keeps working. We keep serving the last known-good output (if any) rather
// than clearing the stylesheet, so a broken edit doesn't strip all styling while it's
// being fixed.
if (optimize) {
throw root.error(message)
}

throw root.error(`${error}`)
result.warn(message, { plugin: '@tailwindcss/postcss' })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the direct console.error call.

Line 402 prints every error before the plugin reports it through result.warn() or throws it. Non-optimized builds therefore produce duplicate diagnostics. Optimized builds also print an error before PostCSS reports the thrown error. Let the PostCSS warning and error paths own diagnostic output.

Proposed fix
-            console.error(error)
-
             let message =
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error(error)
if (error && typeof error === 'object' && 'message' in error) {
throw root.error(`${error.message}`)
let message =
error && typeof error === 'object' && 'message' in error ? `${error.message}` : `${error}`
// In optimized (typically production) builds we want compilation errors to fail
// the build outright so broken CSS never ships.
//
// Otherwise, we avoid throwing here. Throwing causes PostCSS's `process()` promise
// to reject instead of resolve. Tools that rely on `result.messages` to register
// file dependencies (e.g. `postcss-loader`/webpack) only ever read `result.messages`
// from a *resolved* result, so on rejection none of the dependency messages above —
// or from any previously successful build — are seen. That drops every file in the
// CSS import graph from the watcher, not just the one that failed, and nothing
// recompiles again until some other, still-watched file happens to change.
//
// We instead report the error as a warning so `result` still resolves and dependency
// tracking keeps working. We keep serving the last known-good output (if any) rather
// than clearing the stylesheet, so a broken edit doesn't strip all styling while it's
// being fixed.
if (optimize) {
throw root.error(message)
}
throw root.error(`${error}`)
result.warn(message, { plugin: '@tailwindcss/postcss' })
let message =
error && typeof error === 'object' && 'message' in error ? `${error.message}` : `${error}`
// In optimized (typically production) builds we want compilation errors to fail
// the build outright so broken CSS never ships.
//
// Otherwise, we avoid throwing here. Throwing causes PostCSS's `process()` promise
// to reject instead of resolve. Tools that rely on `result.messages` to register
// file dependencies (e.g. `postcss-loader`/webpack) only ever read `result.messages`
// from a *resolved* result, so on rejection none of the dependency messages above —
// or from any previously successful build — are seen. That drops every file in the
// CSS import graph from the watcher, not just the one that failed, and nothing
// recompiles again until some other, still-watched file happens to change.
//
// We instead report the error as a warning so `result` still resolves and dependency
// tracking keeps working. We keep serving the last known-good output (if any) rather
// than clearing the stylesheet, so a broken edit doesn't strip all styling while it's
// being fixed.
if (optimize) {
throw root.error(message)
}
result.warn(message, { plugin: '`@tailwindcss/postcss`' })

@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

The PR appears safe to merge with no actionable defects identified.

The changed recovery path retains dependency messages, safely falls back to an initialized cached AST, and continues to throw compilation errors when optimization is enabled.

Reviews (1): Last reviewed commit: "Fix @tailwindcss/postcss losing watch de..." | Re-trigger Greptile

daltino

This comment was marked as resolved.

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.

Angular: Error in PostCSS + tailwind 4 doesn't break the build

2 participants