fix(devloop): ask the dev server whether a frontend change compiles - #25466
fix(devloop): ask the dev server whether a frontend change compiles#25466tltv wants to merge 2 commits into
Conversation
Vite compiles a module when something requests it, not on save and not on apply. So whether its complaint lands in the daemon's watch window depended on whether a browser happened to re-fetch during those few hundred milliseconds - and a page already showing the error overlay does not re-fetch at all. The report then sat in app.log from an earlier window, behind Watch.mark(), the new window was silent, and every other signal said the change went fine: apply reported Stable over a module the page could not load, then no_changes on the next apply, because Vite mode marked the file applied regardless. The frontend leg now asks instead of overhearing. FRONTEND_CHECK has the connector fetch each changed frontend file through DevModeHandler.prepareConnection, as the browser would and on the base Vite was launched with, so a context-path app works too. A 500 is a refusal and carries Vite's own message; a 200 means the module compiles; a 404 is not a refusal at all. The answer settles it in both directions, which is the load-bearing part. devServerAsked tells "served every file" apart from "nobody could be asked": a clean answer overrules the log, because an error left in it describes the version the edit just replaced - the daemon's own request for the broken one among them - and only an unaskable dev server falls back to the log. A refusal also leaves the file unmarked, so a repeat apply asks again rather than going quiet. The log line and the quoted errors follow suit: no claim that Vite applied a file it refused, and no stale parse error under Stable. Verified by hand in Vite mode per the README sequence, which now covers the broken-then-fixed case. Vite mode has no IT for the reason the README gives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three gaps in how a Vite-mode frontend failure reached the developer. A type error was missed entirely. Fetching a module answers whether it can be served, and types are stripped without being checked - so a type error, and a stray ">" in JSX that oxc tolerates and tsc does not, come back 200. Only vite-plugin-checker knows, and it logs at INFO where the level says nothing; its errors are now matched separately (CHECKER_ERROR), carried across Watch.mark() like the dev server's since it type-checks within a moment of the save, and fatal when the change-set touched a frontend file. The checker is read as a verdict, not as errors sighted in the window, because it announces a clean project too (CHECKER_CLEAN). Both halves are load-bearing: without the error half a broken .tsx passed as Stable, and without the clean half breaking a file and putting it back failed the apply that repaired it - the report was still in the log and nothing said it had been superseded. The verdict outlives a window on purpose: it is the state of the project, and the checker re-announces only when it changes. The error itself was cut at 160 characters, which spent the budget on an absolute path and dropped the diagnosis. The connector now joins the report's lines with the separator instead of flattening them to spaces, so the daemon can compact a whole report by the same rule it uses on one read line by line (AppLog.report), and a verdict's reason is wrapped rather than truncated (reasonRows, detail).
The frontend verdict now asks the dev server instead of overhearing its logflowchart LR
subgraph Before
direction TB
B1["TransactionEngine.apply()"] -->|reads log, only if a browser re-fetched| B2["AppLog.Watch window"]
B2 -->|carriedLogErrors, filter devServerError| B3["devServerFailure() verdict"]
B3 -.->|silent window: Stable over a broken module| B4["Stable / Failed"]
end
subgraph After
direction TB
A1["TransactionEngine.askDevServer() (new)"]:::changed -->|FRONTEND_CHECK command| A2["DevLoopRedefiner.frontendCheck() (new)"]:::changed
A2 -->|per changed file| A3["ViteHandler.prepareConnection() GET"]
A3 -->|500 refused / 200 compiles| A4["Vite dev server"]
A4 -->|devServerRefusal, overrules log| A5["devServerFailure() verdict"]:::changed
end
Before ~~~ After
classDef changed stroke:#c9a227,stroke-width:3px
The figure shows where Diagram Bot draws the mechanism this pull request touches; it does not review the change. Verify it against the diff.
|
|


Vite compiles a module when something requests it, not on save and not on apply. So whether its complaint lands in the daemon's watch window depended on whether a browser happened to re-fetch during those few hundred milliseconds - and a page already showing the error overlay does not re-fetch at all. The report then sat in app.log from an earlier window, behind Watch.mark(), the new window was silent, and every other signal said the change went fine: apply reported Stable over a module the page could not load, then no_changes on the next apply, because Vite mode marked the file applied regardless.
The frontend leg now asks instead of overhearing. FRONTEND_CHECK has the connector fetch each changed frontend file through DevModeHandler.prepareConnection, as the browser would and on the base Vite was launched with, so a context-path app works too. A 500 is a refusal and carries Vite's own message; a 200 means the module compiles; a 404 is not a refusal at all.
The answer settles it in both directions, which is the load-bearing part. devServerAsked tells "served every file" apart from "nobody could be asked": a clean answer overrules the log, because an error left in it describes the version the edit just replaced - the daemon's own request for the broken one among them - and only an unaskable dev server falls back to the log. A refusal also leaves the file unmarked, so a repeat apply asks again rather than going quiet. The log line and the quoted errors follow suit: no claim that Vite applied a file it refused, and no stale parse error under Stable.
Verified by hand in Vite mode per the README sequence, which now covers the broken-then-fixed case. Vite mode has no IT for the reason the README gives.