Fix @tailwindcss/postcss losing watch dependencies after a compile error - #20395
Fix @tailwindcss/postcss losing watch dependencies after a compile error#20395andershagbard wants to merge 1 commit into
Conversation
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.
WalkthroughThe 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)
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.
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
📒 Files selected for processing (3)
integrations/postcss/index.test.tspackages/@tailwindcss-postcss/src/index.test.tspackages/@tailwindcss-postcss/src/index.ts
| 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' }) |
There was a problem hiding this comment.
🎯 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.
| 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`' }) |
Confidence Score: 5/5The 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 |
Summary
With
@tailwindcss/postcss+postcss-loader(e.g. webpack), fixing aCSS error during
--watchdoesn't resume compilation. Repro:webpack --watchwith@tailwindcss/postcssinpostcss.config.js.@apply some-unknown-class(or break an@importedpartial with any error) in a watched CSS file.
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
throwinside itsOnce()hook. That causes the wholepostcss().process()promise toreject instead of resolve.
postcss-loaderonly readsresult.messages— and therefore only callsthis.addDependency()—from a resolved result:
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 thethrow 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
optimizeoption (alreadydefaults to
NODE_ENV === 'production'):optimizeis enabled (typically production builds), keepthrowing — this preserves Angular: Error in PostCSS + tailwind 4 doesn't break the build #18370's fix, so broken CSS still fails
the build.
result.warn()instead of throwing,so
resultstill resolves and every dependency message (includingones 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
packages/@tailwindcss-postcss/src/index.test.tsasserting dependency messages survive a compile error outside of
optimizemode (and thatoptimizemode still throws). These failagainst the current
throw-only behavior and pass with this fix.integrations/postcss/index.test.ts's existingrebuild error recoverytest needed a small adjustment: recovered builds now alsolog
Waiting for file changes...(same as any successful build), soa later wait for that exact message needed
process.flush()firstto avoid matching a stale occurrence from the recovered build.
@tailwindcss/postcssunit tests (22/22),integrations/postcss/(31/31),integrations/webpack/(8/8,including a webpack+postcss-loader watch test).
webpack --watchin areal project and confirmed this fixes it.