Skip to content

dev server: retry a failed import with the import kind of its record - #43937

Open
robobun wants to merge 3 commits into
mainfrom
robobun/9066b020/dev-server-watcher-import-kind
Open

robobun wants to merge 3 commits into
mainfrom
robobun/9066b020/dev-server-watcher-import-kind

Conversation

@robobun

@robobun robobun commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A CSS or HTML reference with a ?query or #fragment names a missing file. When the file is created, the dev server page does not reload. Could not resolve: "./logo.png?v=2" stays on the open page.
  • A CSS @import "./other" beside an other.ts rebuilds the stylesheet on each directory change.
  • The retry (src/runtime/bake/dev_server/mod.rs:461) resolves each failed import as ImportKind::Stmt. The resolver removes a suffix only for a CSS import kind (src/resolver/resolver.rs:1385).

Fix

  • track_resolution_failure takes the ImportKind of the record. Dep.import_kind stores it, and the retry passes it to the resolver.
  • A CSS specifier that starts with ? is not tracked. With the ./ prefix, ./?v=2 resolves to the directory.
  • Verified: 17 new tests in test/bake/dev/{css,html}.test.ts, all fail on 1.4.3-canary. Also bundle, plugins, hot.
  • Self-reviewed: 12 concerns raised, 12 addressed.

Background

  • DirectoryWatchStore lists the imports that did not resolve. On a directory change the dev server resolves each entry (Dep) again, and rebuilds the importer on success.
  • An ImportKind is the origin of an import: Stmt (JS), At (CSS @import), Url (CSS url(), HTML asset).
  • Considered a kind from the importer's loader. One HTML file has Stmt and Url records.

Downsides

  • Dep grows from 40 to 48 bytes per pending failure. Release .text does not grow.
  • A failing retry of a suffixed CSS reference makes 2 resolver passes, not 1.
  • A suffix that contains / (url(./a.svg#icons/home)) still does not reload.
Notes

There is no user report for this. It was found by a read of the code during #43854, which names it.

Scope. The references are a CSS url(), @import and composes, and an HTML asset or stylesheet link. A CSS @import "./theme" with no extension has the same fault, because the retry used the JS extensions.

Severity. On 1.4.3-canary, for an HTML route, GET / is 500 before the file is created and 200 after it. The rebuild on request (check_route_failures) recovers it. The open page keeps its error, because no frame is sent. For a framework route, the stylesheet of meta.styles stays Not Found on new requests.

The ./ prefix. The retry runs on the resolver of the server. A bare specifier as written reaches its package and builtin rules: url(bun) and @import "ws" resolved there, and the stylesheet was rebuilt on each change. With the prefix the retry stays on the relative path, which is all that a directory watch can see. insert is the same as on main.

A specifier that starts with ?. The resolver removes a ?query unless it starts the specifier. With the prefix, ./?v=2 loses its query and resolves to the directory, so the stylesheet was rebuilt on each change. track_resolution_failure returns early for a CSS kind with such a specifier. In the bundler url(?v=2) resolves only to a file with that literal name. That case no longer reloads by itself.

One failing retry, in a project with a node_modules directory. Resolver passes / extension probes / package lookups, from temporary counters in a debug build:

specifier main's call this PR
url(./bun.png?v=2) 1 / 9 / 0 2 / 2 / 0
url(bun.png?v=2) 1 / 9 / 0 2 / 2 / 0
url(bun.png) 1 / 9 / 0 1 / 1 / 0
url(bun) not measured 1 / 1 / 0
url(?v=2) not measured not tracked, no retry
JS import "./missing" 1 / 9 / not counted 1 / 9 / not counted

"main's call" is this build with main's arguments at the retry (the ./ prefix and ImportKind::Stmt). It is not a build of main.

Retries for each directory change, one pending Dep (BUN_DEBUG_DevServer=1, the new DirectoryWatchStore retry log line): 2 while the file is missing (main's call: 2), 1 on the change that creates it (main's call: 2, fails), 0 after that (main's call: 2).

Size, release builds of 4227e46 and of this PR on it:

  • size: text 80,829,819, data 110,372, bss 1,823,056 on both.
  • nm -S: DirectoryWatchStore::track_resolution_failure 2763 to 2913 bytes, resolve_import_records 14359 to 14362, handle_parse_task_failure 3420 to 3424, process_file_list 5348 to 5348 (the log line is not in a release build), on_resolve 12160 to 12147.
  • resolve_import_records: 2850 to 2851 instructions. The 5 lines that differ are the argument setup before the call of track_resolution_failure. Nothing differs outside the arm of a failed resolve.
  • on_resolve holds the inlined run_resolver: 2263 instructions on both. The compiler changed its register and stack slot allocation, so 118 lines differ after the stack slots are normalized. It runs only for an import that an onResolve plugin handled.
  • Dep: size_of const assert on both sources, with a failing control in each direction.

Tests. Each new test fails on 1.4.3-canary and passes on the debug build, in repeated runs.

  • The row @import "./other" beside other.ts fails on main inside its no-traffic check. The other rows fail on main at the reload.
  • css retry does not rebuild on an unrelated change has url(?v=2) and url(bun) beside a reference that recovers. It fails at its no-traffic check when the early return for ? is removed, and when a bare CSS specifier is stored as written.
  • html asset and script before create fails when the kind is derived from the loader.
  • The plugin row covers run_resolver: with ImportKind::Stmt passed at that call only, exactly that row fails.
  • The rows for a conditional @import and for composes read the stylesheet that the server sends. The test client drops an @layer block and hashes class names.
  • The tests do not request the page before the no-traffic check. A request of a failing route rebuilds it and sends a frame.

Not checked.

  • Syscall counts: strace is not installed.

Still kind-blind, not changed here.

Open PRs on the same lines. None changes the kind of the retry.


[human-review] gate passed · iteration 2 · 6 files touched

fails on main (without fix)
ASAN without fix: 17 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bake/dev/css.test.ts test/bake/dev/html.test.ts
bun test v1.4.3 (367d939d9)

test/bake/dev/html.test.ts:
Dev server testing directory: /tmp/bun-dev-test-R5pbtb
�[0;30mdev|�[0m Started development server: http://localhost:46869
�[0;30mdev|�[0m �[32mBundled page in 111ms�[0m�[2m:�[0m index.html
�[0;30mdev|�[0m �[32mReloaded in 48ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 36ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Server-side code changed, reloading!
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x3]�[0m �[32mReloaded in 50ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Server-side code changed, reloading!
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x4]�[0m �[32mReloaded in 45ms�[0m�[2m:�[0m script.ts
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting f
... (truncated)

release without fix: 17 FAILED
bun test v1.4.3-canary.1 (c5b562da7)

test/bake/dev/html.test.ts:
Dev server testing directory: /tmp/bun-dev-test-q7QAnP
�[0;30mdev|�[0m Started development server: http://localhost:33669
�[0;30mdev|�[0m �[32mBundled page in 3ms�[0m�[2m:�[0m index.html
�[0;30mdev|�[0m �[32mReloaded in 8ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 50ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Server-side code changed, reloading!
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x3]�[0m �[32mReloaded in 2ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Server-side code changed, reloading!
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x4]�[0m �[32mReloaded in 2ms�[0m�[2m:�[0m script.ts
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
(pass)  DEV:html-1: html file is watched [933.94ms]
�[0;30mdev|�[0m Started development server: http://localhost:41099
�[0;30mdev|�[0m �[32mBundled page in 3ms�[0m�[2m
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bake/dev/css.test.ts test/bake/dev/html.test.ts
bun test v1.4.3 (367d939d9)

test/bake/dev/html.test.ts:
Dev server testing directory: /tmp/bun-dev-test-9cQ7ZU
�[0;30mdev|�[0m Started development server: http://localhost:40759
�[0;30mdev|�[0m �[32mBundled page in 85ms�[0m�[2m:�[0m index.html
�[0;30mdev|�[0m �[32mReloaded in 116ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 76ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Server-side code changed, reloading!
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x3]�[0m �[32mReloaded in 57ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Server-side code changed, reloading!
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[36m[x4]�[0m �[32mReloaded in 35ms�[0m�[2m:�[0m script.ts
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting f
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1260ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/214] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[2/214] gen compressed/codegen/bake.client.js.zst
[3/214] gen compressed/codegen/bake.error.js.zst
[4/214] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, rust=0); 248 extern-C blocks audited
[5/214] gen cpp.rs (cppbind)
[6/214] gen JS modules (bundle-modules)
Preprocess modules (8170ms)
Bundle modules (64ms)
Postprocesss modules (167ms)
Bundle Functions (541ms)
Generate Code (42ms)

[9.00s] Bundled "src/js" for production
  2597 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[7/212] build.rs build_script_build
[8/212] cc obj/codegen/InternalModuleRegistryConstants.S.o
[9/212] rustc bun_platform 
[10/212] rustc bun_core 
[11/212] rustc bun_safety 
[12/212] rustc bun_output 
[13/212] rustc bun_zlib_sys 
[14/212] rustc bun_brotli 
[15/212] rustc bun_cares_sys 
[16/212] rustc bun_boringssl_sys 
[17/212] rustc bun_zstd 
[18/212] r
... (truncated)
diff hotspot
src/bundler/bundle_v2.rs           |   2 +
 src/bundler/lib.rs                 |   2 +-
 src/runtime/bake/DevServer.rs      |   1 +
 src/runtime/bake/dev_server/mod.rs |  62 ++++++++----
 test/bake/dev/css.test.ts          | 190 ++++++++++++++++++++++++++++++++++++-
 test/bake/dev/html.test.ts         |  94 ++++++++++++++++++
 6 files changed, 331 insertions(+), 20 deletions(-)

gate history · 1 passed · 0 rejected · iteration 2

evidence per changed file
file                                reads  edits  tests
src/bundler/bundle_v2.rs                2      1     68
src/bundler/lib.rs                      1      1     69
src/runtime/bake/DevServer.rs           2      1     68
src/runtime/bake/dev_server/mod.rs      6      6     70
test/bake/dev/css.test.ts               7      5     59
test/bake/dev/html.test.ts              3      1     27

@robobun

robobun commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:44 PM PT - Sep 24th, 2026

✅ @robobun, your commit 1792fc10f27455a077afc5a72d005b88250f9ff2 passed in Build #120495! 🎉


🧪   To try this PR locally:

bunx bun-pr 43937

That installs a local version of the PR into your bun-43937 executable, so you can run:

bun-43937 --bun

@robobun

robobun commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on 1.4.3-canary.

  • Run USE_SYSTEM_BUN=1 bun test test/bake/dev/css.test.ts test/bake/dev/html.test.ts -t "before create|does not rebuild". The 17 new tests fail. The 2 tests that were there before pass.
  • Run bun bd test test/bake/dev/css.test.ts test/bake/dev/html.test.ts on this branch. All tests pass.

@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 91b571aa-ab58-451a-8ee0-27a3d136a278

📥 Commits

Reviewing files that changed from the base of the PR and between c5b562d and 2f6c4fb.

📒 Files selected for processing (1)
  • src/runtime/bake/dev_server/mod.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


Walkthrough

Resolution-failure tracking now passes each failed import’s kind to directory watches and stores it with the dependency. Directory-watch retries use the stored kind. CSS and HTML dev-server tests cover unresolved references and file-change behavior.

Changes

Resolution failure retries

Layer / File(s) Summary
Pass import kinds into directory watches
src/bundler/lib.rs, src/bundler/bundle_v2.rs, src/runtime/bake/DevServer.rs, src/runtime/bake/dev_server/mod.rs
The tracking interface and its call sites pass the failed import’s kind. Directory-watch dependencies store that kind. CSS query-only specifiers skip watch registration.
Retry with stored import kinds
src/runtime/bake/dev_server/mod.rs
Directory-watch retries resolve dependencies with their stored import kinds and log the specifier, kind, and resolution result.
Test missing CSS and HTML dependencies
test/bake/dev/css.test.ts, test/bake/dev/html.test.ts
New tests cover unresolved CSS and HTML references, unrelated file changes, and retries after creating referenced files.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 2f6c4

The retry change preserves each import’s resolution kind. No issue identified here requires resolution before merge.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: retrying failed imports with their recorded import kind.
Description check ✅ Passed The description explains the problem, implementation, scope, limitations, and verification results. It does not use the exact template headings, but it provides the required information in equivalent …

Comment @coderabbitai help to get the list of available commands.

@claude claude 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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/runtime/bake/dev_server/mod.rs — pre-existing: a CSS or HTML reference whose ?query or #fragment contains a / still never reloads after the missing file is created. The watched dir at src/runtime/bake/dev_server/mod.rs:1274 is dirname of the joined specifier, so url(./a.svg#icons/home) watches <root>/a.svg#icons, which does not exist. The open at mod.rs:1410 gets ENOENT and insert() returns Ignore, so no watch is registered at all. Fix: for a kind where kind.is_from_css() holds, strip the specifier at its first ? or # (index >= 1, the same rule as resolver.rs:1388) before joining to compute dir, so every suffixed CSS/HTML reference watches the real parent directory. The PR text lists this case as a remaining downside.

    Why this was flagged

    A stylesheet contains background: url(./a.svg#icons/home) or url(./img.png?path=/x) and the file does not exist yet. resolve_import_records at src/bundler/bundle_v2.rs:6797 calls track_resolution_failure with the raw specifier. mod.rs:1269-1274 joins it onto the importer's dirname and takes dirname, giving <root>/a.svg#icons instead of <root>. insert() at mod.rs:1410 opens that path with O_DIRECTORY, gets ENOENT at mod.rs:1420 and returns DirectoryWatchInsertError::Ignore, which track_resolution_failure maps to Ok(()) at mod.rs:1307. No Dep is stored, so the new import_kind retry never runs; creating a.svg in the root does not send a reload and the page keeps Could not resolve. The base branch fails the same way (it also watched the wrong directory and additionally used ImportKind::Stmt). The resolver itself resolves this specifier fine at bundle time by stripping at the first ?/# (resolver.rs:1388), so the only place where the / inside the suffix matters is this dir computation.

    Verification: pre-existing; acknowledged in diff: the PR description's "Downsides" and "Still kind-blind, not changed here" sections state "A suffix that contains / (url(./a.svg#icons/home)) still does not reload ... the watched directory is <root>/a.svg#icons, which does not exist" — the note is accurate. Trigger: a CSS url()/@ import/composes (or HTML asset) reference to a not-yet-existing…

Comment thread src/runtime/bake/dev_server/mod.rs Outdated
Comment thread src/runtime/bake/dev_server/mod.rs Outdated
Comment thread src/runtime/bake/dev_server/mod.rs Outdated
Comment thread src/runtime/bake/DevServer.rs
@robobun
robobun force-pushed the robobun/9066b020/dev-server-watcher-import-kind branch from c5b562d to 2f6c4fb Compare September 25, 2026 00:40
@robobun

robobun commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator Author

Reply to the review finding on a suffix that contains /, such as url(./a.svg#icons/home).

This PR does not change that case. To watch the correct directory, the code that computes it must remove the suffix first. That is the resolver's rule for a CSS kind, written a second time. I prefer to keep that rule in the resolver only, so this needs a change there, and that is a separate PR. The body lists the case under Downsides.

Three of the four inline findings are fixed in 2f6c4fb. The fourth has a reply in its thread.

@claude claude 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.

Code review found no issues

No high-confidence issues detected in this change.

When an import does not resolve, the dev server watches its directory
and resolves the import again on each change. The retry used
ImportKind::Stmt for every record. The resolver removes a ?query or
#fragment, and uses the CSS extension order, only for a CSS import
kind. So the page did not reload when the missing file of a CSS url()
or @import, or of an HTML asset or stylesheet link, with a ?query or
#fragment was created. An @import "./other" beside an other.ts
resolved in the retry and rebuilt the stylesheet on each change.

track_resolution_failure now takes the kind that the bundler resolved
with. DirectoryWatchStore stores it in Dep.import_kind, and the retry
passes it to the resolver. A bare specifier keeps the ./ prefix, so
the retry stays on the relative path. A CSS specifier that starts with
? is stored as written, because ./?v=2 would resolve to the directory.
… is created

css: url() with a query, a fragment, or both, bare and after a plugin
declines it, @import with a suffix, with a condition, and without an
extension beside a file that a JS import finds, and composes with a
query. One test has url(?v=2) and url(bun) beside a reference that
recovers: an unrelated change must send nothing. One test uses a
framework route.

html: img src with a query or a fragment, project-relative, a bare
stylesheet href, and one page with an asset and a script.
With the "./" prefix the resolver removes the query of "?v=2" and
resolves the directory, so the retry rebuilt the stylesheet on each
change. track_resolution_failure now returns early for such a
specifier, and insert is the same as before.

The retry takes the specifier reference inside a block that ends
before the dep is freed. Each unsafe block has the comment for its own
invariant.
@robobun
robobun force-pushed the robobun/9066b020/dev-server-watcher-import-kind branch from 2f6c4fb to 1792fc1 Compare September 25, 2026 01:17

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant