Skip to content

watcher: stop serving the watchlist's stored fd to --hot reload reads - #37050

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/ec001d6b/hot-watcher-fd-race
Aug 6, 2026
Merged

watcher: stop serving the watchlist's stored fd to --hot reload reads#37050
Jarred-Sumner merged 3 commits into
mainfrom
farm/ec001d6b/hot-watcher-fd-race

Conversation

@robobun

@robobun robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes test/cli/hot/watch-many-dirs.test.ts ("handles 129 directories being updated simultaneously"), which has been failing on main and across PR CI runs on Linux with:

error: EISDIR reading "/tmp/.../hot-many-dirs_.../dir-0072/index.js"
error: EBADF reading "/tmp/.../hot-many-dirs_.../dir-0009/index.js"

followed by the 30s test timeout (the failed reload never prints the expected output). Seen red on main builds 86133/86209/86347 and in many PR runs, e.g. build 89567. The race is timing-sensitive: it reproduced once in 80 local runs of an unmodified release build (with the exact EBADF + EISDIR + timeout signature), while loaded CI machines hit it regularly.

Cause

bun --hot re-transpiled a changed module by reading through a file descriptor snapshotted from the watcher's watchlist (ImportWatcher::snapshot_fd_and_package_json). The snapshot copies the fd number under the watcher mutex, but the read in transpiler.rs:read_file_with_allocator happens after the mutex is released.

Concurrently, the watcher thread's flush_evictions closes stored descriptors under that same mutex. On Linux, a write to a file inside a watched directory produces both a file event and a directory event; the directory-event arm in hot_reloader.rs evicts the watched file's entry (to handle atomic saves that replace the inode). With 129 directories the inotify events span multiple batches, so a reload triggered by batch N snapshots a descriptor that batch N+1's eviction then closes:

  • fd closed before the read: EBADF reading "<path>"
  • fd number recycled by one of the resolver's many openat(O_DIRECTORY) calls: EISDIR reading "<path>"

The mutex ordering added previously (flush_evictions before enqueue, snapshot under the mutex) only closed the same-event window; nothing can serialize a later batch's eviction against a read that happens after the snapshot returns. Reading through the stored fd was also wrong after atomic saves (pre-rename inode, stale contents), which is why the entrypoint already had a workaround skipping it.

Fix

Reloads now always open the file by path, and the watchlist keeps sole ownership of its stored descriptor:

  • snapshot_fd_and_package_json becomes snapshot_package_json; the stored fd is never handed out for reads. This removes the race structurally and makes the entrypoint's open-by-path workaround universal, so it is deleted.
  • Watcher::add_file no longer replaces a valid stored fd on the already-watched branch (it only upgrades fd-less entries inserted by add_file_by_path_slow, e.g. the --hot entrypoint, so the directory-event recovery path keeps working). The old overwrite dropped the replaced descriptor without closing it, leaking one fd on the entrypoint per reload.
  • add_file now returns FdOwnership saying whether it adopted the caller's descriptor; the transpiler handoff sites, AsyncModule, the bundler's plugin watch path, and add_file_by_path_slow close the descriptor when the watcher did not take it. The bundler's watcher_data path intentionally ignores the outcome and keeps today's behavior, since its descriptor can be borrowed from the resolver's entry cache.

The fd-per-reload behavior is locked in by a new deterministic test in watch-many-dirs.test.ts that counts /proc/<pid>/fd entries for the entrypoint and an edited dependency across 15 reloads. On an unfixed build the entrypoint gains exactly one fd per reload (+15); with the fix both counts are stable. (The remaining per-reload directory-handle growth comes from the resolver's DirEntry cache and is addressed separately in #36675.)

Verification

  • New test fails on an unfixed build (entryDelta: 15) and passes with the fix.
  • The race fix is structural rather than statistical: after this change the transpiler only ever reads descriptors it opened itself, so the closed-by-eviction read cannot occur. Empirically, "handles 129 directories" failed 1/80 runs on an unfixed release build and passed 40/40 (plus 12/12 debug ASAN) runs on a fixed one.
  • test/cli/hot/hot.test.ts (12), test/cli/hot/watch.test.ts (2), test/cli/watch/ (7), test/bake/dev/hot.test.ts (11, including DEV:hot-9), test/js/bun/util/filesystem_router.test.ts (33), test/cli/test/test-changed.test.ts (20) all pass on a debug ASAN build.
  • bun run rust:check-all: 10/10 target combos clean.

The race predates any recent change (the fd-reuse pattern and evicting close shipped with #30412 and were inherited from the original design); CI frequency rose recently with timing shifts. The open #36675 fixes the adjacent resolver-side leaks and includes a different change to the same add_file branch; this PR supersedes that hunk by never replacing the stored fd at all.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/hot/watch-many-dirs.test.ts

bun --hot re-transpiled a changed module through a file descriptor
snapshotted from the watcher's watchlist. The watcher thread's
flush_evictions closes that descriptor concurrently (a directory event
for an edited file evicts its entry), so the reload read failed with
EBADF, or EISDIR once a resolver openat() recycled the number, and the
reload aborted. A stored descriptor could also point at the pre-rename
inode after an atomic save and return stale contents.

Reloads now always open the file by path. The watchlist keeps sole
ownership of its stored descriptor: add_file never replaces a valid
stored fd (it only upgrades fd-less entries added by path) and reports
whether it adopted the caller's descriptor, so callers close what the
watcher did not take. This also fixes the entrypoint leaking one
descriptor per reload: the already-watched branch used to overwrite the
stored fd without closing the one it replaced.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The watcher now returns descriptor ownership. Hot-reload and transpilation paths reopen files by path and close descriptors unless ownership transfers to the watcher. A Linux regression test checks descriptor counts across repeated reloads.

File descriptor ownership

Layer / File(s) Summary
Watcher ownership contract
src/watcher/Watcher.rs, src/watcher/lib.rs
Watcher::add_file returns FdOwnership, preserves valid existing descriptors, and closes redundant caller-owned descriptors.
Metadata-only watcher snapshots
src/jsc/hot_reloader.rs, src/jsc/RuntimeTranspilerStore.rs, src/runtime/jsc_hooks.rs, src/jsc/AsyncModule.rs
Watcher snapshots return package metadata without cached descriptors. Runtime paths reopen input files by path.
Runtime descriptor cleanup
src/jsc/RuntimeTranspilerStore.rs, src/runtime/jsc_hooks.rs
Close guards remain active unless add_file returns FdOwnership::Watcher, including parse failures and registration failures.
Registration caller updates
src/bundler/bundle_v2.rs
Bundler registration closes valid descriptors when the watcher does not take ownership.
Descriptor leak regression coverage
test/cli/hot/watch-many-dirs.test.ts
A Linux-only hot-reload test checks file-descriptor counts after repeated reloads.

Possibly related PRs

  • oven-sh/bun#36251: Addresses file-descriptor leaks in hot-reload watcher handling.
  • oven-sh/bun#36675: Modifies hot-reload descriptor ownership and Watcher::add_file behavior.

Suggested reviewers: jarred-sumner

🚥 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 describes the main change: preventing --hot reload reads from using file descriptors stored by the watcher.
Description check ✅ Passed The description explains the cause, fix, affected behavior, and verification results, although it uses different headings from the repository template.

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/watcher/Watcher.rs (1)

870-902: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Return Caller when Windows skips registration.

On Windows, append_file_assume_capacity returns Ok(()) without appending a file when the path is outside top_level_dir. Line 902 converts this no-op into FdOwnership::Watcher.

src/runtime/jsc_hooks.rs and src/jsc/RuntimeTranspilerStore.rs then disable their close guards. This leaks the input descriptor for an absolute import outside the project directory.

Check path eligibility before appending, or return an insertion result from append_file_maybe_lock. Return FdOwnership::Caller when the watchlist did not adopt fd. Update FdOwnership::Caller documentation because non-adoption is not limited to an existing valid descriptor.

Proposed fix
         if let Some(index) = self.index_of(hash) {
             // existing ownership handling
         }
 
+        #[cfg(windows)]
+        if bun_paths::resolve_path::is_parent_or_equal(self.top_level_dir(), file_path)
+            == bun_paths::resolve_path::ParentEqual::Unrelated
+        {
+            self.mutex.unlock();
+            return Ok(FdOwnership::Caller);
+        }
+
         let r = self.append_file_maybe_lock::<CLONE_FILE_PATH, false>(

Also applies to: 1064-1073

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/watcher/Watcher.rs` around lines 870 - 902, The append path currently
always maps a no-op Windows registration to FdOwnership::Watcher, leaking
descriptors that the watchlist did not adopt. Update append_file_maybe_lock and
the caller around its map(|()| ...) conversion to report whether fd was actually
inserted, returning FdOwnership::Caller when the path is ineligible or otherwise
not adopted; apply the same behavior to the corresponding flow around the
additional reported location. Revise FdOwnership::Caller documentation to
describe general non-adoption, not only an existing valid descriptor.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/watcher/Watcher.rs`:
- Around line 870-902: The append path currently always maps a no-op Windows
registration to FdOwnership::Watcher, leaking descriptors that the watchlist did
not adopt. Update append_file_maybe_lock and the caller around its map(|()| ...)
conversion to report whether fd was actually inserted, returning
FdOwnership::Caller when the path is ineligible or otherwise not adopted; apply
the same behavior to the corresponding flow around the additional reported
location. Revise FdOwnership::Caller documentation to describe general
non-adoption, not only an existing valid descriptor.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a2d22e03-c682-462a-b0bb-ca8a7df4843f

📥 Commits

Reviewing files that changed from the base of the PR and between 3ee86ad and 075cd61.

📒 Files selected for processing (8)
  • src/bundler/bundle_v2.rs
  • src/jsc/AsyncModule.rs
  • src/jsc/RuntimeTranspilerStore.rs
  • src/jsc/hot_reloader.rs
  • src/runtime/jsc_hooks.rs
  • src/watcher/Watcher.rs
  • src/watcher/lib.rs
  • test/cli/hot/watch-many-dirs.test.ts

…e-add

The pending-imports path carried parse_result.input_fd into the queued
AsyncModule, but the transpile frame's fd guard closes that descriptor
when the watcher does not adopt it, so the deferred re-add could store a
closed (possibly recycled) number in the watchlist. The original parse
already registers the file before the enqueue, so the re-add was
redundant; delete it along with the AsyncModule fields that only served
it.

Also return FdOwnership::Caller when append_file_assume_capacity skips
a path outside the project root on Windows, so callers close the
descriptor the watchlist never stored.
@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed in 123d163: append_file_assume_capacity now returns FdOwnership, so the Windows out-of-tree skip propagates Caller and the close guards stay armed instead of leaking the descriptor. The FdOwnership::Caller doc now covers non-adoption generally.

The same commit also removes the deferred watcher re-add in AsyncModule: the pending-imports path carried parse_result.input_fd into the queue while the transpile frame's fd guard could close that descriptor at return, so the re-add could store a closed (possibly recycled) number. The original parse already registers the file before the enqueue, so the re-add (and the fields that only served it) is gone.

Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/jsc/AsyncModule.rs
Comment thread src/jsc/AsyncModule.rs Outdated
Comment thread src/jsc/RuntimeTranspilerStore.rs Outdated
Comment thread src/jsc/RuntimeTranspilerStore.rs
Comment thread src/jsc/hot_reloader.rs
Comment thread src/jsc/hot_reloader.rs Outdated
Comment thread src/runtime/jsc_hooks.rs Outdated
Comment thread src/watcher/Watcher.rs
Comment thread src/watcher/Watcher.rs
Comment thread src/watcher/Watcher.rs Outdated
Comment thread src/watcher/Watcher.rs Outdated
Comment thread src/watcher/Watcher.rs
Comment thread src/watcher/Watcher.rs
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. bun --hot always leaks memory #11083 - This PR removes a per-reload file-descriptor leak on the --hot entrypoint (plus an unconditional fd drop on already-watched re-transpiles), which is exactly the resource growth that this issue's rewrite-the-entrypoint-in-a-loop repro exercises — note this is a partial fix, since the RSS growth reported there also has non-fd causes.

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #11083

🤖 Generated with Claude Code

Comment thread src/jsc/AsyncModule.rs
Comment thread src/jsc/RuntimeTranspilerStore.rs
Comment thread src/jsc/hot_reloader.rs
Comment thread src/runtime/jsc_hooks.rs
Comment thread src/watcher/Watcher.rs
Comment thread src/watcher/Watcher.rs

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/jsc/AsyncModule.rs:1337-1352resume_loading_module now closes fd_ (= parse_result.input_fd) when add_file returns Caller, but that descriptor's ownership was already settled by maybe_watch_file in transpile_source_code_inner before the AsyncModule was enqueued — so this either closes the watchlist's own stored fd (dropping the kqueue watch and setting up a later double-close in flush_evictions) or double-closes a stale fd that _fd_guard already closed on return Err(AsyncModule). The pre-PR code did let _ = watcher.add_file(...) here without closing; this site should not close fd_ at all (drop the bun_sys::close(fd_), or pass Fd::INVALID to add_file).

    Extended reasoning...

    What the bug is

    The new close-on-FdOwnership::Caller branch in AsyncModule::resume_loading_module closes a file descriptor whose ownership was already transferred (or already closed) during the original parse in transpile_source_code_inner. Depending on which path the original parse took, this is either a close of the watchlist's owned descriptor or a double-close of a stale/possibly-recycled fd number — exactly the EBADF/EISDIR-on-recycled-fd class this PR is meant to eliminate.

    The code path

    1. transpile_source_code_inner (jsc_hooks.rs) parses the file. transpiler.rs:1456-1458 writes the same entry.fd to both parse_result.input_fd and *file_fd_ptr (the caller's input_file_fd local).
    2. At jsc_hooks.rs:2639, maybe_watch_file(..., &mut should_close_input_file_fd, input_file_fd, ...) runs and calls watcher.add_file(input_file_fd, ...). Under this PR, it sets should_close_input_file_fd = false only if the watcher adopted the fd (Ok(FdOwnership::Watcher)).
    3. At jsc_hooks.rs:2922, parse_result.pending_imports.len() > 0 is detected; parse_result (carrying input_fd = Some(X)) is moved into Queue::enqueue, and the function returns Err(Error::AsyncModule) at line 2968.
    4. On that return, _fd_guard (jsc_hooks.rs:2385) fires: if should_close_input_file_fd is still true, it closes input_file_fd (= X).
    5. Later, resume_loading_module runs on the enqueued module. input_fd is Some(X). It calls watcher.add_file(X, ...) — the file is already watched, so add_file returns Ok(FdOwnership::Caller) (on macOS ATOMIC_FILE_WATCHER = false so it always returns Caller on the already-watched branch; on Linux it returns Caller because the stored fd is valid). The new code then calls bun_sys::close(X).

    Why nothing prevents it

    The two disjoint scenarios:

    A. maybe_watch_file adopted X (first transpile of this file under --hot/--watch): should_close = false, so _fd_guard skipped the close. The watchlist now owns X — its stored fd is X. resume_loading_module's add_file(X) sees the file already watched with a valid stored fd → Ok(Caller)close(X). This closes the descriptor the watchlist owns. On macOS/FreeBSD closing the kqueue-registered fd drops the vnode watch for this file; on all platforms flush_evictions will later close(X) again on the (now stale, possibly recycled) number.

    B. maybe_watch_file did not adopt (file was already watched from a prior reload → Caller): should_close stays true, so _fd_guard closed X when Err(AsyncModule) unwound. parse_result.input_fd still holds the closed number X. resume_loading_moduleadd_file(X)Callerclose(X): a double-close of a possibly-recycled fd.

    The guards around the AsyncModule add_file call (is_watcher_enabled, is_absolute, !contains(node_modules)) are the same as maybe_watch_file's guards, so whenever resume_loading_module reaches its add_file, maybe_watch_file already ran on the same fd during the original parse.

    Impact

    Reachable under --hot/--watch when a module has an auto-install pending import (unresolved bare specifier with the package manager enabled). Narrow, but the failure mode is precisely what this PR fixes elsewhere: closing an fd the watchlist owns (silent watch loss on kqueue) and/or a double-close that can hit an unrelated recycled descriptor (EBADF/EISDIR). It's a regression: the pre-PR code did let _ = watcher.add_file(...) with no close at this site.

    Step-by-step proof (scenario A, macOS)

    • bun --hot app.js where app.js contains import 'not-yet-installed-pkg' (auto-install on).
    • Parse opens app.js as fd 12; parse_result.input_fd = Some(12), input_file_fd = 12.
    • maybe_watch_file: not yet watched → append_file_maybe_lock stores fd 12 in the watchlist and registers it with kqueue → returns Ok(Watcher)should_close = false.
    • Pending import found → AsyncModule enqueued with input_fd = Some(12)return Err(AsyncModule)_fd_guard sees should_close == false, skips.
    • Auto-install completes; resume_loading_module runs. fd_ = 12. add_file(12, ...): index_of(hash) finds the entry; ATOMIC_FILE_WATCHER = false on macOS → returns Ok(Caller). New branch calls bun_sys::close(12).
    • fd 12 was the watchlist's stored, kqueue-registered descriptor. The kernel drops the EVFILT_VNODE registration; edits to app.js no longer trigger reloads. When the entry is later evicted, flush_evictions calls close(12) again — if 12 has been recycled by a resolver openat, that's a wrong-fd close.

    Fix

    Ownership of parse_result.input_fd was already settled in transpile_source_code_inner before the AsyncModule was enqueued, so resume_loading_module must not close it. Either revert this hunk to let _ = watcher.add_file::<true>(fd_, ...) (matching pre-PR behavior — the redundant add_file is harmless and updates package_json), or pass Fd::INVALID in place of fd_ since the fd is not this call site's to give.

@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Right, and this was caught and fixed in 123d163 (the review ran against the first commit): resume_loading_module no longer touches parse_result.input_fd at all. The deferred add_file was redundant in both scenarios you describe (ownership of that descriptor is settled by maybe_watch_file before the enqueue, or the frame's fd guard already closed it), so the whole re-add block is deleted rather than reverted to the close-less form, along with the AsyncModule/InitOpts fields that only served it.

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

Beyond the inline nit, I checked a few cross-platform concerns and ruled them out: the removed #[cfg(target_os = "macos")] gate on the auto-watch close in jsc_hooks.rs is safe because non-kqueue platforms pass Fd::INVALID there (the is_valid() guard covers it); the add_file never-replace-valid-fd change is gated on ATOMIC_FILE_WATCHER (Linux-only), so kqueue's watch-descriptor semantics are unaffected; and the new close in bundle_v2.rs's plugin-watch path targets a locally-opened fd, not a borrowed one.

Extended reasoning...

This PR reworks fd ownership between the watcher and the transpile paths — add_file now returns FdOwnership, the watchlist's stored fd is never handed out for reads, and the AsyncModule deferred re-add is removed along with its supporting fields. The one inline finding is a dead-code nit (ParseResult::input_fd lost its last reader). I additionally verified that the platform-gate removal on the auto-watch close and the add_file already-watched branch change do not alter macOS/FreeBSD kqueue semantics, and that the newly-added close in the bundler plugin path targets a descriptor opened in the same block. The change is substantive (cross-thread fd ownership across 6 native files, with acknowledged overlap with open #36675), so it warrants human review; this note just records what was already checked.

Comment thread src/jsc/AsyncModule.rs
@Jarred-Sumner
Jarred-Sumner merged commit b3941fc into main Aug 6, 2026
54 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/ec001d6b/hot-watcher-fd-race branch August 6, 2026 20:31
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun submit a follow-up PR

@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up PR: #37071, removes the write-only ParseResult::input_fd plumbing flagged in review.

Jarred-Sumner pushed a commit that referenced this pull request Aug 6, 2026
Follow-up to #37050, requested in
#37050 (comment).

## What

#37050 removed the deferred watcher re-add in
`AsyncModule::resume_loading_module`, which was the only reader of
`ParseResult::input_fd`. That left the field write-only: assigned at
every construction site and threaded through `parse_maybe`,
`ParseResult::empty_with`, and the four cold loader helpers
(`parse_data_loader`, `parse_text_loader`, `parse_md_loader`,
`parse_wasm_loader`) without ever being read, as review on that PR
pointed out.

This deletes the field, the `parse_maybe` local that fed it, and the
pass-through parameters. The fd the parse opened still reaches the
watcher handoff sites through `ParseOptions::file_fd_ptr`, which was
already written from the same `entry.fd` on the same line. The comment
in `AsyncModule.rs` that named the deleted field is reworded.

No behavior change, dead-code removal only, so there is no fail-before
test to write. While auditing the touched loader paths, the runtime
`.md` module loader turned out to have no coverage at all (the existing
md tests only exercise the `Bun.markdown` API), so this adds tests
locking it in: `import`/`require` of a `.md` file yields the rendered
HTML as the default export, and an empty `.md` file produces a module
with no default export (the `empty_with` path).

## Verification

- `cargo check -p bun_bundler` and a full debug build pass.
- On the debug build: `test/js/bun/md/md-edge-cases.test.ts` (84 pass,
including the new import tests), `test/cli/hot/watch-many-dirs.test.ts`
(2 pass, including the fd-count test added in #37050),
`test/cli/hot/hot.test.ts` (12 pass), `test/js/bun/resolve/toml` +
`yaml` + `jsonc` (21 pass), `test/bundler/bundler_loader.test.ts` (45
pass).
liooil pushed a commit to liooil/poly that referenced this pull request Aug 7, 2026
Follow-up to #37050, requested in
oven-sh/bun#37050 (comment).

## What

#37050 removed the deferred watcher re-add in
`AsyncModule::resume_loading_module`, which was the only reader of
`ParseResult::input_fd`. That left the field write-only: assigned at
every construction site and threaded through `parse_maybe`,
`ParseResult::empty_with`, and the four cold loader helpers
(`parse_data_loader`, `parse_text_loader`, `parse_md_loader`,
`parse_wasm_loader`) without ever being read, as review on that PR
pointed out.

This deletes the field, the `parse_maybe` local that fed it, and the
pass-through parameters. The fd the parse opened still reaches the
watcher handoff sites through `ParseOptions::file_fd_ptr`, which was
already written from the same `entry.fd` on the same line. The comment
in `AsyncModule.rs` that named the deleted field is reworded.

No behavior change, dead-code removal only, so there is no fail-before
test to write. While auditing the touched loader paths, the runtime
`.md` module loader turned out to have no coverage at all (the existing
md tests only exercise the `Bun.markdown` API), so this adds tests
locking it in: `import`/`require` of a `.md` file yields the rendered
HTML as the default export, and an empty `.md` file produces a module
with no default export (the `empty_with` path).

## Verification

- `cargo check -p bun_bundler` and a full debug build pass.
- On the debug build: `test/js/bun/md/md-edge-cases.test.ts` (84 pass,
including the new import tests), `test/cli/hot/watch-many-dirs.test.ts`
(2 pass, including the fd-count test added in #37050),
`test/cli/hot/hot.test.ts` (12 pass), `test/js/bun/resolve/toml` +
`yaml` + `jsonc` (21 pass), `test/bundler/bundler_loader.test.ts` (45
pass).
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.

2 participants