Repository navigation
Conversation
…fails to open The resolver keeps one listing for each directory. A file that moved after its directory was listed, and that no build had read, still resolves to its old path. The read fails with ENOENT, and IncrementalGraph::on_file_deleted returned for a file with no graph record before it dropped that listing. The import then stayed a dead import in every later build, until the server restarted. The early return now drops the listing, so the next build that resolves the import reads the directory again. A file with a graph record keeps its order: its importers are enqueued before the listing is dropped.
|
Updated 12:15 AM PT - Oct 25th, 2026
❌ @robobun, your commit 49651bc has 1 failures in
🧪 To try this PR locally: bunx bun-pr 46846That installs a local version of the PR into your bun-46846 --bun |
|
Status: draft. It waits for #38212 (the resolver generation for each How this was reproduced. A dev server ( The test PR: #46846 |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Deletion cache invalidation and regression coverage src/runtime/bake/dev_server/incremental_graph.rs, test/bake/dev/bundle.test.ts |
When a deleted path has no graph index, on_file_deleted derives its directory, busts the resolver cache, and returns. Tests cover resolution after a package module rename and build errors after package removal. |
Suggested reviewers: jarred-sumner, alii
Priority:
Merge Risk: 
Minimal · up to c96c1
-
No actionable merge-blocking risk remains from the reviewed changes.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description clearly explains the problem, proposed fix, limitations, design trade-offs, and verification results. Although it does not use the template headings verbatim, it provides the required information in equivalent sections.
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
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 @coderabbitai help to get the list of available commands.
…tus of the second page
There was a problem hiding this comment.
Still open from earlier reviews (1):
- 🔴
src/runtime/bake/dev_server/incremental_graph.rs:1623—Dev server users whose never-bundled import vanishes briefly and returns now get Could not resolve until they restart t…
If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.
When a package is removed, the failed open of each bundled file is what builds its importers again, and each importer is found through the old listing. A file of that package that no build had read could fail first and drop the listing, and then the importers of the bundled files were skipped. The listing is now dropped only while the directory still exists.
There was a problem hiding this comment.
Beyond the inline findings, I also checked the shape of the new bust at incremental_graph.rs:1622-1624 against the existing one at :1672-1673 in the same function: the dirname(...).unwrap_or(abs_path) fallback and the discarded bust_dir_cache bool match it, and bust_dir_cache (resolver.rs:2480) strips trailing Windows slashes itself, so using bun_paths::dirname rather than dirname_platform is not a problem here. The exists(dirname) check can race a concurrent delete, but the worst case is a skipped bust, which is the base behavior.
Extended reasoning...
The change adds five lines to the no-graph-record early return of IncrementalGraph::on_file_deleted that bust the resolver's directory listing when the directory still exists, plus two dev-server tests. It touches no security-sensitive surface. The two confirmed inline findings (a removed-or-replaced package can answer 200 with stale modules, and the test guarding the exists clause depends on ENOENT arrival order) are the reason a human should weigh in; the remaining style concerns about the new lines were ruled out by comparison with the existing bust in the same function.
Findings marked 🟡 are optional suggestions and need no follow-up push.
Draft, not ready. The branch has a first design that is worse than main in some orders. The fix below waits for #38212 and #36874.
Problem
Failed to load bundled modulein the browser), also after a save.on_file_deleted(src/runtime/bake/dev_server/incremental_graph.rs:1620) returns early for a file that no build bundled, and the resolvers stay at generation 0, so the stale listing stays.Fix
test/bake/dev/bundle.test.tsfail on main, and all pass. Self-reviewed: 12 concerns raised, none addressed yet (Notes).Background
Bun.buildraises its generation.Downsides
Notes
Origin. Found while a fuzz report on #46648 was checked. No user reported this form. It needs no barrel. #46648 makes it reach more pages: a barrel that loads every export reads each re-export in every build, so each page that imports the barrel asks for the moved file. #17576 ("HMR cannot resolve new or renamed files") is the same class for a file that a build has bundled.
Repro without a barrel (
Bun.serveHTML routes/aand/b,development: true):a.tsimports./src/x,b.tsimports./src/y. Load/a, renamesrc/y.tstosrc/y.tsx, then load/b./bsrc/y.ts, noEXPORT_Y/ba.tsb.tssrc/y.tsx, holdsEXPORT_YDesigns tried, in order. All numbers are from release builds.
bust_dir_cache) in the early return ofon_file_deleted(this branch, with a check that the directory exists). It heals the rename at the next build of the importer. It is worse than main in these orders:Could not resolveuntil restart. main finds it at a save of the importer.on_file_deletedskip an importer that no longer resolves. A page then answers 200 with removed modules in 10 of 15 runs. main answers 500.c.cssbesidec.js), it scans the directory on the JS thread, and it still leaks.The prototype (
robobun/761f31d9/dev-server-generation-after-failed-open, 10 source files, +46/-9, of which 6 files and +27 are insrc/runtime/bake/dev_server).handle_parse_task_failuresetsDevServer.reread_directorieswhen a file with no graph record fails to open. A failed resolve (ModuleNotFound) does not set it.start_async_bundletakes the flag, drawsbun_resolver::next_generation()and writes it to the server, client and SSR resolvers. The generation stays raised.Generationisu32,next_generation()is one counter for the process, and theBun.buildthread draws from it.What the prototype does in each case.
c.jstoc.jsx, or toc/index.js(bare specifier,node_modules)minimalFramework)c.jsremoved (bare or alias), two builds ask for itCould not resolvec.jsreturns/page1500/page1500/page1500/page1200 with old modulesThe last two rows are worse than main. For the first of them, main gives the same 200 when the first missing file is one that a build has bundled. The last row is the cached error of a failed re-read, which #36874 changes.
Tests on the prototype (
test/bake/dev/bundle.test.ts, tendevTests). Eight fail on main. Two pass on main and pin what must not change (reports an updated package to a page that imports a file beside its barrel,a bundled file that is gone leaves the removed files of another package to be reported). All ten pass 30 of 30 on a release build, and the file passes (64) on a debug build.Each clause was removed in turn on a scratch build:
finds a module that moved after a build that does not ask for ita bundled file that is gone ...ModuleNotFoundexclusionMeasurements on the prototype (main is a release build of the
src/of 179664e).node_modulesand one relative import. 300 saves before any failed open: 0 directory opens and 0getdents64on main and on the prototype. The save that heals: 7 directory opens, 3 failed directory opens, 14getdents64(15 with 500 entries in the package, 33 with 5,000). 300 saves after it: 987 failed directory opens (3.3 per save), main 0. A resolver above generation 0 opens a missing directory again on every lookup.Bun.buildpays the same from its second build.Bun.resolveSync, warm cache, 250,000 resolves: main 9,927 / 9,927 / 9,929, prototype 9,929 / 9,920 / 9,925.Why it waits.
Bun.buildafter a dev server build that read a directory again can run at an older generation and miss a new file. The prototype draws from one counter when a queue drain starts its first build. That keeps main's rule of one generation for each queue drain, and on one CPU that rule already fails on main: three awaited builds in a row do not see a file written after the first one (taskset -c 0, 1 of 1 run on main and on the prototype). bundler: advance the resolver generation once per build instead of once per queue drain #38212 changes the same lines to one generation for each build. The counter belongs in that PR, and this change then is the six files undersrc/runtime/bake/dev_server.Self-review concerns, none addressed yet.
pathsalias never resolves a file created after the server starts #43391 then heals or not by what happened earlier in the process (not re-run by me).Bun.builddraw keeps the race at thread start and the rule of one generation for each drain.import()of the returned file fails where main finds it. Not re-run by me.Could not resolvefor one build in one order.Not in this PR. The directory event that is ignored while no import is unresolved (
hot_reload_event.rs:97), the failing build that still serves the dead import, and an importer thaton_file_deletedskips because it no longer resolves. Each is the same on main.