We only need to check whether a tsconfig file has changed.
Agreed — but for a bare TaskInput::FullBuild that check cannot be answered: changed_files() is empty by construction, so changed_tsconfig is false even when a tsconfig did change. Concrete case:
- Start dev with
tsconfig.json v1 = { "compilerOptions": { "target": "es5" } }. It's valid JSON, so find_tsconfig loads it into the caches you listed (tsconfigs_raw / tsconfigs_built), but the per-file merge rejects it ("Rolldown only supports ES2015 and above"). The initial build fails → coordinator state = FullBuildFailed.
- The user fixes the tsconfig (v2,
"target": "esnext"). The change event arrives while in FullBuildFailed, and that arm of handle_file_changes pushes a bare TaskInput::FullBuild — the changed file name is dropped here.
- The task runs:
changed_files() is empty → changed_tsconfig is false → no cache clear. The full scan calls find_tsconfig, which answers from tsconfigs_built with v1, the merge fails again → back to FullBuildFailed.
- Every subsequent edit repeats 2–3, because in
FullBuildFailed every change becomes a bare FullBuild. Pressing r goes through trigger_full_build, which also produces a bare FullBuild — same result. The dev server can't recover without a restart, even though the tsconfig on disk has been correct since step 2.
The same erasure happens on two other paths: trigger_full_build clears queued_tasks (dropping a queued Hmr { [tsconfig] }), and merge_with has FullBuild absorb other inputs and discard their file lists.
So the || self.input.requires_full_rebuild() isn't about clearing for unrelated full builds — it's the fallback for exactly the inputs whose file list has already been erased, where "did a tsconfig change" is unanswerable. Since full builds only happen at startup, on r, and in failure recovery, the extra clears are cheap. The alternative would be to keep the changed-file info alive through those three coordinator paths, but that's a much bigger change to the state machine.
Originally posted by @h-a-n-a in #10261 (comment)
Agreed — but for a bare
TaskInput::FullBuildthat check cannot be answered:changed_files()is empty by construction, sochanged_tsconfigisfalseeven when a tsconfig did change. Concrete case:tsconfig.jsonv1 ={ "compilerOptions": { "target": "es5" } }. It's valid JSON, sofind_tsconfigloads it into the caches you listed (tsconfigs_raw/tsconfigs_built), but the per-file merge rejects it ("Rolldown only supports ES2015 and above"). The initial build fails → coordinator state =FullBuildFailed."target": "esnext"). The change event arrives while inFullBuildFailed, and that arm ofhandle_file_changespushes a bareTaskInput::FullBuild— the changed file name is dropped here.changed_files()is empty →changed_tsconfigisfalse→ no cache clear. The full scan callsfind_tsconfig, which answers fromtsconfigs_builtwith v1, the merge fails again → back toFullBuildFailed.FullBuildFailedevery change becomes a bareFullBuild. Pressingrgoes throughtrigger_full_build, which also produces a bareFullBuild— same result. The dev server can't recover without a restart, even though the tsconfig on disk has been correct since step 2.The same erasure happens on two other paths:
trigger_full_buildclearsqueued_tasks(dropping a queuedHmr { [tsconfig] }), andmerge_withhasFullBuildabsorb other inputs and discard their file lists.So the
|| self.input.requires_full_rebuild()isn't about clearing for unrelated full builds — it's the fallback for exactly the inputs whose file list has already been erased, where "did a tsconfig change" is unanswerable. Since full builds only happen at startup, onr, and in failure recovery, the extra clears are cheap. The alternative would be to keep the changed-file info alive through those three coordinator paths, but that's a much bigger change to the state machine.Originally posted by @h-a-n-a in #10261 (comment)