Implement import.meta.hot for bun --hot - #32856
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
WalkthroughAdds import.meta.hot support for bun --hot
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 6:22 PM PT - Aug 20th, 2026
❌ @robobun, your commit 140591c has 1 failures in 🧪 To try this PR locally: bunx bun-pr 32856That installs a local version of the PR into your bun-32856 --bun |
|
Found 3 issues this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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.
Inline comments:
In `@docs/runtime/watch-mode.mdx`:
- Around line 147-155: The `import.meta.hot.data` example in `server.ts` is not
runnable because it references an undefined `tick` and persists a timer handle
without proper cleanup, which can leave a stale interval in `hot.data.interval`
across reloads. Update the snippet so it uses a real callback/function defined
in the example, and make the `import.meta.hot.dispose` handler clear the
interval and remove or reset the stored handle in `import.meta.hot.data`. Keep
the example self-contained and ensure the `setInterval`/`dispose` flow in the
watch-mode docs demonstrates correct lifecycle handling.
In `@src/jsc/bindings/ZigGlobalObject.cpp`:
- Around line 3395-3403: `import.meta.hot.data` entries are never cleared for
modules that are no longer loaded, so `ImportMetaObject::hotProperty` keeps
stale per-URL objects alive across reloads. Update the hot-reload cleanup path
in `GlobalObject::reload()` to also remove or reset the `m_importMetaHotDataMap`
entries for unloaded module URLs, alongside the existing module loader and
`requireMap` clearing. Use the existing
`importMetaHotDataMap()`/`m_importMetaHotDataMap` access points to locate the
cache and ensure unloaded modules no longer retain strong references.
- Around line 3422-3457: The dispose list handling in
ZigGlobalObject::runImportMetaHotDispose callbacks is using an unrooted raw list
after m_importMetaHotDisposeList.clear(), so GC during profiledCall can
invalidate list/entry accesses. Fix this by following the same pattern as
handleRejectedPromises(): take a rooted snapshot of the dispose entries before
clearing the global list, then iterate that rooted snapshot while invoking
callbacks. Ensure any JSValue kept across the native call is rooted or copied in
a GC-safe container.
In `@src/jsc/virtual_machine_exports.rs`:
- Around line 29-31: The hot-reload export in hot_reload_mode is exposing the
raw enum byte, which ties ImportMetaObject.cpp to a numeric contract. Change the
VirtualMachine export to return a boolean hot/not-hot check instead, and update
the C++ consumer to use the new boolean symbol consistently so import.meta.hot
no longer depends on the underlying enum value.
In `@test/cli/hot/hot.test.ts`:
- Around line 845-872: The hot-runner stream drain in the `stderrDone`/`stdout`
reader can throw after `runner.kill()`, making the `--hot` tests flaky. Update
the async reader pattern in `hot.test.ts` so the background `runner.stderr`
drain ignores the abort race (for example by wrapping the reader promise with
the established catch-and-suppress pattern used elsewhere in these hot tests),
and apply the same fix to both `--hot` test blocks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d8e733d6-699c-449d-9b9a-d5657ac49778
📒 Files selected for processing (14)
docs/runtime/watch-mode.mdxsrc/bundler/transpiler.rssrc/js_parser/fold.rssrc/js_parser/parse/parse_entry.rssrc/js_parser/parser.rssrc/jsc/RuntimeTranspilerStore.rssrc/jsc/bindings/ImportMetaObject.cppsrc/jsc/bindings/ImportMetaObject.hsrc/jsc/bindings/ZigGlobalObject.cppsrc/jsc/bindings/ZigGlobalObject.hsrc/jsc/virtual_machine_exports.rssrc/runtime/api/JSTranspiler.rssrc/runtime/jsc_hooks.rstest/cli/hot/hot.test.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/jsc/bindings/ImportMetaObject.cpp (1)
630-633: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep this comment within the 3-line limit.
As per coding guidelines, “Keep code comments to 3 lines max.”
Proposed cleanup
-// `bun --hot` re-evaluates every module on each reload, so the full -// Vite accept() graph / event semantics don't apply; accept()/decline()/ -// on()/off()/prune()/invalidate()/send() are all no-ops. They exist so -// Vite-flavoured code guarded by `if (import.meta.hot)` does not throw. +// `bun --hot` re-evaluates every module, so Vite graph/event +// methods are no-ops. They exist so guarded Vite-flavoured code +// can call them without throwing.🤖 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/jsc/bindings/ImportMetaObject.cpp` around lines 630 - 633, Shorten the explanatory comment in ImportMetaObject’s hot-module block to fit within 3 lines while preserving the key point that bun --hot makes accept/decline/on/off/prune/invalidate/send no-ops for import.meta.hot compatibility. Keep the comment near the existing hot-reload semantics code and trim redundant wording so it stays within the guideline.Source: Coding guidelines
src/jsc/bindings/ZigGlobalObject.cpp (1)
3468-3473: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winPreserve termination exceptions from dispose callbacks.
Line 3467 runs user JS. If that produces a VM termination exception, lines 3468-3473 currently report/clear it like an ordinary throw, which can swallow termination during reload. Check
vm.isTerminationException(...)before reporting or clearing.Proposed fix
JSC::profiledCall(this, ProfilingReason::API, callback, callData, jsUndefined(), args, returnedException); if (auto* ex = returnedException.get()) { + if (vm.isTerminationException(ex)) [[unlikely]] + return; Bun__reportUnhandledError(this, JSValue::encode(JSValue(ex))); } - if (scope.exception()) [[unlikely]] { - scope.clearException(); + if (auto* ex = scope.exception()) [[unlikely]] { + if (vm.isTerminationException(ex)) [[unlikely]] + return; + (void)scope.tryClearException(); }As per coding guidelines, C++ code that can enter JS must handle exception state before continuing, and abort/termination paths must not be swallowed.
🤖 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/jsc/bindings/ZigGlobalObject.cpp` around lines 3468 - 3473, The dispose-callback exception handling in ZigGlobalObject::dispose-style JS entry needs to preserve VM termination exceptions instead of treating them as normal errors. Update the returnedException/scope.exception() handling to check vm.isTerminationException(...) first, and if it is a termination exception, skip Bun__reportUnhandledError and do not clear it so reload/abort can propagate correctly. Keep the ordinary reporting/clearing path only for non-termination exceptions.Source: Coding guidelines
🤖 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/jsc/bindings/ImportMetaObject.cpp`:
- Around line 630-633: Shorten the explanatory comment in ImportMetaObject’s
hot-module block to fit within 3 lines while preserving the key point that bun
--hot makes accept/decline/on/off/prune/invalidate/send no-ops for
import.meta.hot compatibility. Keep the comment near the existing hot-reload
semantics code and trim redundant wording so it stays within the guideline.
In `@src/jsc/bindings/ZigGlobalObject.cpp`:
- Around line 3468-3473: The dispose-callback exception handling in
ZigGlobalObject::dispose-style JS entry needs to preserve VM termination
exceptions instead of treating them as normal errors. Update the
returnedException/scope.exception() handling to check
vm.isTerminationException(...) first, and if it is a termination exception, skip
Bun__reportUnhandledError and do not clear it so reload/abort can propagate
correctly. Keep the ordinary reporting/clearing path only for non-termination
exceptions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 80521ef4-d440-4b0e-baee-37af008db063
📒 Files selected for processing (6)
docs/runtime/watch-mode.mdxsrc/jsc/bindings/ImportMetaObject.cppsrc/jsc/bindings/ZigGlobalObject.cppsrc/jsc/bindings/ZigGlobalObject.hsrc/jsc/virtual_machine_exports.rstest/cli/hot/hot.test.ts
There was a problem hiding this comment.
No further issues from my side — all prior feedback has been addressed — but this introduces a new public API (import.meta.hot shape, no-op accept/prune/etc., fire-and-forget async dispose) and adds GC-tracked state to GlobalObject/ImportMetaObject, so a maintainer should sign off on the API surface and the JSC bindings.
Extended reasoning...
Overview
This PR implements import.meta.hot for bun --hot, adding a runtime HMR object with data/dispose() plus Vite-compat no-op stubs. It spans 14 files: a new hot getter and LazyProperty on ImportMetaObject (C++), two new WriteBarrier fields and a runImportMetaHotDisposeCallbacks() drain loop on Zig::GlobalObject invoked from reload(), a new runtime_hot parser feature flag threaded through fold.rs/parser.rs/transpiler.rs/RuntimeTranspilerStore.rs/jsc_hooks.rs (plus the runtime-transpiler cache hash), a new Bun__VirtualMachine__isHotReloadMode FFI export, ~280 lines of new tests in hot.test.ts, and a docs rewrite of the --hot section in watch-mode.mdx.
Security risks
None identified. The feature is gated to dev-mode bun --hot on the main thread; it stores user-provided callbacks/data in GC-tracked JS objects and invokes them via profiledCall with standard exception handling. No auth, network, filesystem, or privilege boundaries are touched.
Level of scrutiny
High. This is a new user-facing API whose shape (which Vite methods are real vs. no-op, dispose not awaiting Promises, prune deferred to a follow-up, workers excluded) is a product decision a maintainer should ratify. The C++ side adds WriteBarrier<JSMap>/WriteBarrier<JSArray> fields to GlobalObject, a new LazyProperty with visitChildren wiring, a MarkedArgumentBuffer snapshot loop that runs arbitrary JS during reload(), and termination-exception handling — all GC- and exception-scope-sensitive code where review by someone fluent in JSC conventions is warranted. The transpiler change alters when import.meta.hot is folded vs. left live, which affects DCE behavior across bun run/bun build/Bun.Transpiler.
Other factors
I left eight rounds of inline comments on earlier revisions (test flake races, missing Vite no-op stubs, the runtime_hot parser-flag approach to preserve plain-bun run DCE, GC rooting of the dispose snapshot, async-dispose docs caveat, worker gating, etc.); all were addressed and resolved. The bug hunter found nothing on the current revision. The robobun CI comment shows musl build failures on an earlier commit (657774b); the status of the latest commit (1811525) isn't reflected in the thread yet. Test coverage for the new surface is solid (undefined-without---hot, dispose ordering, data persistence, throwing dispose, arg validation, worker exclusion), but the overall scope and the explicitly-deferred follow-ups (async dispose await, prune semantics) are the kind of thing a human should approve.
|
CI on 1811525 is green on everything this diff touches (282/286 jobs passed;
plus two flaky-warning retries in |
|
The retrigger (build 65741) confirms it: 281/282 completed jobs passed and the previous
That is the same agent ( The diff is green on everything it touches. Ready for maintainer review. |
6b99322 to
02fdb7e
Compare
CI status (head
|
02fdb7e to
6ceb47e
Compare
CI status (head
|
Focused upstream integration (no Bun fork, no runtime-source changes): - docs: correct the --watch/--hot semantics to match Bun's official documentation and re-measurement on 1.3.14 — --watch is a hard restart (globalThis fresh, no handler accumulation), --hot is the in-process soft reload preserving globalThis (handlers/timers accumulate). The old Phase 5 notes had these attributes swapped; bin.bun.js's globalThis root guard was always the correct --hot strategy. - bin.bun.js: register import.meta.hot.dispose(() => disposePrevious()) when the runtime provides it (PR build) — runtime-awaited disposal before re-evaluation; no-op on stock Bun/Node. globalThis guard kept as defense-in-depth. - tests/bun/hot.spec.ts: 2 PR-build-gated integration tests (skip cleanly when bun-32856 is absent, verified): awaited async disposal completes before reactivation, no resource duplication across 3 generations; broken generation still disposes the old root and recovers on next edit. - CI: install the PR build best-effort (bunx bun-pr 32856) and run the hot suite; download failures leave the tests skipped, real failures fail. - repros: hot-pr-dispose-order.ts, signal-handler-accumulation.ts; README corrected and extended. bun test tests/bun: 60/60 (216 expect) · Node suite: 163/163 · yakumo esbuild+tsc: exit 0. No Bun-source changes needed; nothing to report on PR #32856.
|
Downstream validation from Cordis — a plugin/composition framework whose dev reload needs exactly the contract this PR adds. Sharing in case it's useful signal; everything below passed on the first artifact we tried (Aug 13 CI build, Our integration: our Bun entrypoint registers Verified cases (fixture:
One observed behavior worth having in the docs (not a bug): editing a non-module file (our YAML config) triggers no reload — expected, since it's outside the module graph. Apps that reload on config changes still need their own file watcher for those. Thanks for the PR — it closes the gap that forced us onto a process-supervisor pattern for Bun. |
Owner-approved. Upstream activity remains a comment only — no Bun code, clone, or defect report (nothing failed).
Under bun --hot, each save re-evaluates every module but nothing disposed the previous generation's timers, event listeners, Workers, or other resources. There was also no way for user code to clean them up: import.meta.hot was undefined at runtime (the transpiler folded it to a literal undefined) and only existed in the bundler's dev server. This adds a runtime import.meta.hot under bun --hot: - import.meta.hot.data: a plain object keyed by module URL and persisted across reloads, for carrying state between generations. - import.meta.hot.dispose(cb): register a callback that runs immediately before the next reload, receiving the module's data object. Exceptions from dispose callbacks are reported but do not block the reload. - import.meta.hot.accept()/decline(): no-ops for Vite compatibility (bun --hot always re-evaluates every module). Without --hot, import.meta.hot is undefined. The transpiler no longer folds import.meta.hot to undefined in runtime mode; it is left as a property access so the runtime getter can answer. The bundler still dead-code-eliminates it as before. The watch-mode docs are updated so the --hot example no longer leaks one interval per save, and the new API is documented.
The stderr reader was a fire-and-forget async task, so the assertion on its accumulated contents could run before the pipe had been read. Capture the reader promise and await it (plus process exit) before asserting.
The previous change stopped folding import.meta.hot to undefined in the runtime transpiler unconditionally, which regressed plain bun run (no --hot): an unguarded import.meta.hot.dispose(fn) used to be dead-code-eliminated but would now throw TypeError at runtime. Instead, thread a new runtime_hot parser feature flag from the VM through ParseOptions into the parser. import.meta.hot is only left as a runtime property access when runtime_hot is true (bun --hot); in every other mode (plain bun run, bun build, Bun.Transpiler) it folds to HotDisabled and calls on it continue to dead-code-eliminate as before. The runtime import.meta.hot object also gains no-op on/off/prune/ invalidate/send so that Vite-style code guarded by if (import.meta.hot) does not throw under bun --hot when it calls those methods.
…s/test polish
- runImportMetaHotDisposeCallbacks now snapshots entries into a
MarkedArgumentBuffer before clearing the global list and invoking
callbacks, mirroring handleRejectedPromises.
- Replace Bun__VirtualMachine__hotReloadMode (u8) with
Bun__VirtualMachine__isHotReloadMode (bool) so the C++ side is not
coupled to the enum encoding.
- Make the import.meta.hot docs example self-contained.
- Suppress post-kill stream aborts in the stderr readers and use the
combined {stdout, stderr, exitCode} assertion form.
If a dispose callback triggers a termination exception, return early instead of reporting and clearing it, matching handleRejectedPromises.
Worker VMs inherit hot_reload from the parent but have no watcher and never reload, so dispose callbacks registered there would never fire and the if (import.meta.hot) guard would be misleadingly truthy. Gate both the runtime getter and the runtime_hot parser flag on the main VM so workers see import.meta.hot as undefined (and unguarded HMR calls still fold away), matching pre-PR behaviour. Also break the stdout drain loop after the final line is received so a doubled watcher event cannot over-fill the expected array.
RuntimeTranspilerStore::run executes on a WorkPool thread concurrent with the JS thread, so every *vm access in it uses the (*vm).field place form (see the SAFETY note at its top). Inline is_main_thread() as (*vm).worker.is_none() so the runtime_hot computation matches that convention. Also shorten the fold.rs comment to three lines.
Address review of the runtime import.meta.hot API: - Await dispose. VirtualMachine::reload() now runs in two phases: the new JSC__JSGlobalObject__runImportMetaHotDispose export drains the dispose queue and, if any callback returned a pending promise, returns a promise that fulfills once they have all settled. reload() stores it and defers; the existing deferred-reload poll re-enters reload() once it settles and proceeds with the registry reset and re-evaluation. Changes arriving meanwhile coalesce into that reload. - Report dispose errors (sync throws and rejected promises) through Bun__logUnhandledException, the non-fatal path used for errors in the reloaded file. Bun__reportUnhandledError exits the process once a script that touched `process` has gone idle. - Make `data` an accessor over the per-URL map so assignment persists, and look data up by URL when running callbacks instead of capturing it at registration. Methods live on a shared ImportMetaHotPrototype and validate `this`; the per-module object only carries its URL. - Decide whether `hot` exists once, when the import.meta prototype is created, via the single VirtualMachine::is_hot_reload_enabled() predicate that both runtime transpile paths also use. The RuntimeTranspilerStore job samples it on the JS thread. - Hash runtime_hot into the transpiler cache only for sources that mention import.meta, so --hot and plain runs stop evicting each other's entries; the parser refuses to cache the rare file where that textual check misses a real import.meta use. - Replace the JSArray dispose list with a WriteBarrierList of (callback, url) InternalFieldTuples, removing the speculative exception clearing around reads that cannot throw. - Docs: data example works without --hot, Bun.serve needs no dispose hook (id: null for a fresh server), async/error semantics; document the --hot subset in devserver.d.ts. - Tests: imported-module path, data reassignment, awaited dispose ordering, errors with a process listener registered, ERR_INVALID_THIS, and cache entries surviving mode switches.
…nning the source The parser sets a flag where it folds import.meta.hot, and the cache entry stores whether the output was produced with or without the hot runtime. Entries for files that never touch import.meta.hot are shared between bun run and bun --hot; the others are rejected and rewritten when the mode changes. Bumps the cache version for the new header byte.
A reload resumed from report_exception_in_hot_reloaded_module_if_needed runs outside the task queue, so the module loader's microtasks were only drained by the next tick, and the idle loop parks for up to a second before that tick. Drain them and pre-arm the waker, as a task-driven reload effectively does. Measured settle-to-evaluate for a dispose promise in a debug build: about 1015 ms before, about 15 ms after.
Docs: the unguarded forms are only full import.meta.hot.<method>() calls and single import.meta.hot.data expressions; dispose callbacks run on the next reload whether or not the module is evaluated again; --preload scripts are evaluated once; a fresh server per reload needs server.stop() in dispose, id: null alone leaves the old one listening. Tests: per-module data and dispose (and their order), a dropped module's dispose still running, a rejecting promise that settles last, resuming a parked reload after a generation that failed to evaluate, the Bun.serve recipe, and both --preload shapes.
hot_reload is a HotReload enum now; ImportMetaHotPrototype uses the shared out-of-line cell helpers like its neighbours; the watch-mode intro example keeps its declare global block.
fe4557d to
140591c
Compare
|
Rebased onto main (
Every other file merged with its added and removed lines unchanged (diffed the before and after patches per file to check). The watch-mode intro example keeps its |
|
Thanks for running this against the artifact. The five cases you list match the contract as documented and tested here (the removed-module and failed-evaluation cases have tests of their own in On the YAML point: files outside the import graph are not watched by |
CI status (rebased head
|
Fixes #7337
Fixes #8963
Problem
Under
bun --hot, each save re-evaluates every module in the same process, but nothing disposed the previous generation's resources. Every top-levelsetInterval,process.on(...)listener,new Worker(...), etc. from prior generations kept running: N saves meant N live copies.There was also no way for user code to clean up:
import.meta.hotwasundefinedat runtime. The transpiler foldedimport.meta.hot(and calls likeimport.meta.hot.dispose(...)) to a literalundefinedregardless of mode, and the actual HMR object only existed in the bundler's dev server. The--hotdocs' own example is a top-levelsetIntervalsaved repeatedly, which leaked one interval per save.Cause
GlobalObject::reload()/VirtualMachine::reload()only clearedBun.Cronjobs and the module registry; no other per-generation state was touched.import.meta.hotAPI was a compile-time rewrite (src/js_parser/fold.rs) gated on the bundler'shot_module_reloadingflag. Outside the Bake dev server it always folded toE::Special::HotDisabled(printed asundefined), so the runtime never had a chance to provide it.Fix
Under
bun --hot(main thread only; Workers are never reloaded, so they keepimport.meta.hot === undefined), every module'simport.metagets ahotobject:data: an accessor backed by aJSMapon the global keyed by module URL, so it survives the module being re-evaluated. Reading it creates{}on first use; assigning to it writes through to the map (the Bake runtime's behaviour; Vite makes it getter-only).dispose(cb): queues(cb, module URL)on the global.VirtualMachine::reload()now runs in two phases: it first calls the newJSC__JSGlobalObject__runImportMetaHotDispose, which drains the whole queue (so a module the next generation no longer imports is disposed too) and calls each callback, in registration order, with its own module's currentdata. If any callback returns a pendingPromise, the export returns a promise that fulfills once all of them have settled;reload()stores it, marks the reload deferred and returns. The existing deferred-reload poll (report_exception_in_hot_reloaded_module_if_needed) re-entersreload(), which sees the settled promise and proceeds with phase two (cron clear, registry reset, re-evaluate); file changes that arrive while waiting coalesce into that reload. This matches what Vite, the Bake runtime anddevserver.d.tsalready promise fordispose.report_exception_in_hot_reloaded_module_if_neededruns outside the task queue, so nothing drained the new generation's module-loader microtasks until the next tick, and an idle process only ticks again when the bounded 1 s park intick_possibly_foreverexpires. The poll now drains microtasks and pre-arms the waker after a reload it resumed (the same thing a task-driven reload gets from the task runner). Settle-to-re-evaluate for a dispose promise in a debug build: about 1015 ms before, about 15 ms after, for every kind of settle source tried (microtask, timer, fs read,server.stop(),setImmediate). Main's pre-existing deferrals behind a pending entry promise take the same path and get the same fix.Bun__logUnhandledException(VirtualMachine::run_error_handler), the same non-fatal path used for errors in the reloaded file itself.Bun__reportUnhandledErrorwas wrong here: once a--hotscript has gone idle after touchingprocess,exit_on_uncaught_exceptionis set and that path exits the process. Remaining callbacks still run and the reload still happens either way.accept,decline,on,off,prune,invalidate,send: no-ops, so code written for Vite or the dev server loads. All methods live on a sharedImportMetaHotPrototypeand validatethis(ERR_INVALID_THIS); the per-module object only carries its URL under a private name.Where the property comes from:
VirtualMachine::is_hot_reload_enabled()is the single predicate. The C++ side calls it once, when theimport.metaprototype is created, to decide whether ahotaccessor exists at all (no per-access check); both runtime transpile paths (jsc_hooks.rsfor entry points and theRuntimeTranspilerStoreworker-pool path that every imported module takes) use it to set the newruntime_hotparser feature.Why the parser needs a flag:
fold.rsused to foldimport.meta.hottoundefinedeverywhere except the Bake dev server, which is what makes unguardedimport.meta.hot.dispose(...)calls disappear from plainbun run,bun buildandBun.Transpileroutput. That folding is kept in every mode;runtime_hotis the one case (the--hotruntime transpiler) where the expression is left alone so the runtime property can answer. Removing the flag would either break DCE in plain runs or make the property unreachable under--hot.Transpiler cache:
runtime_hotis not part of the features hash, because it only changes output for files that useimport.meta.hot(the fold infold.rsis the one place the parser reads it). The parser setshas_import_meta_hotat that fold, and after parsing records anImportMetaHotMode(Unused,PlainorHot) into the cache entry's metadata next toexports_kind.RuntimeTranspilerCache::get()rejects and unlinks an entry whose recorded mode does not match the current run, so the file is re-transpiled and the entry rewritten, the same path afeatures_hashmismatch takes. Files that never touchimport.meta.hotkeep one entry shared bybun --hotand plain runs; files that do get theirs rewritten on every mode switch, in both directions and however the expression is spelled. The same applies within one--hotprocess between the main VM and Workers, which transpile in plain mode: such a file loaded by both sides is re-transpiled by whichever side did not write the entry last. Keeping both outputs would need a second file name plus a two-step lookup (the mode is only known after parsing), which this case does not justify. The metadata grows by one byte, so the cache version goes from 25 to 26 and entries written by earlier builds are rewritten once.Docs (
docs/runtime/watch-mode.mdx) describe the above. They limit the no-guard claim to what the non-hot fold handles (fullimport.meta.hot.<method>(...)calls and singleimport.meta.hot.dataexpressions; anything else needsif (import.meta.hot), same rules as the bundler docs), say that dispose callbacks run on the next reload whether or not the module is evaluated again, that a--preloadscript is evaluated once per process, and thatBun.serve()is reused across reloads by default: a fresh server per reload means callingserver.stop()fromdispose()(the port is released synchronously), whileid: nullon its own only disables the reuse and leaves the old server listening.devserver.d.ts, the onlyImportMeta.hotdeclaration, carries the same notes forhot,dataanddispose.Verification
test/cli/hot/hot.test.ts(import.meta.hotblock):undefinedwithout--hot; unguardeddispose/accept/oncalls and adata.x ??=expression are folded and run fine.data,datapersists, previous generation's interval/listener are released; object identity is stable, methods are on the prototype,this-less calls throwERR_INVALID_THIS.dataassignment persists into the next generation, and a dispose callback registered before the assignment still receives the assigned object.RuntimeTranspilerStorepath) each stamp their owndataand record what their dispose callback was handed: the twodataobjects are distinct, each callback gets its own module's object, and the dependency's callback runs before the entry's.await import()has its dispose run on the 1 to 2 reload and not on 2 to 3.setImmediates, B resolves after one. Gen 2 observesdispose A, dispose B, resolve B, reject A, evaluate, and A's message is printed once per generation. Verified by mutation: fulfilling the aggregate on the first settle makes gen 2 evaluate beforereject A; dropping the settled call from the rejected reaction makes the reload never resume.data.xand registers a dispose, gen 2 registers an async dispose and then throws at top level, gen 3 is plain. Gen 3 seesdata.xand both disposes, and gen 2's error is printed once, so a reload parked on dispose resumes through the poll's already-reported Rejected arm.Bun.serve()recipe: gen 1 listens on port 0 and stores the port indata, later generations bind that port afterdisposestopped the previous server; each generation is a newServerobject and a request after each generation is answered by that generation's handler.--preload, both shapes: a preload not imported by the entry is evaluated once and its dispose runs once across three generations; a preload the entry also imports is evaluated and disposed every generation.process.on("exit")registered: errors printed, later callbacks run, reload happens, process stays alive. (Fails before this revision: exits with code 1.)import.meta.hotisundefinedinside aWorkerspawned under--hot.test/cli/run/transpiler-cache.test.ts: a plain file and one usingimport.meta.urlkeep byte-identical cache entries across plain,--hotand plain runs; a file readingimport.meta.hot(written with a line break afterimport, which a textual check would miss) printsundefined/object/object/undefinedacross plain,--hot,--hotand plain runs, with its entry rewritten on each switch and the other entries untouched. The existing header-layout test in that file has its offsets moved by the new byte.Also exercised: the error paths under
BUN_JSC_validateExceptionChecks=1, arequire()d CommonJS module usingimport.meta.hot, and the existinghot,transpiler-cache,bundler_minify(ImportMetaHotTreeShaking) andimport-metasuites.Related
--watchre-exec to the top ofVirtualMachine::reload(); the dispose phase here therefore only covers--hot(whereimport.meta.hotexists) and the--watchbranch is not duplicated. Main also narrowed neighbouring field visibility topub(crate); the two fields this PR adds follow suit.3d3016e329, 322 commits): main turned the GlobalObject lazy-propertyinitLatercalls into offset tables, so them_importMetaHotStructureinitializer is now an entry inlazyStructureInitsnext to the other twoimport.metastructures;hot_reloadbecame theHotReloadenum, sois_hot_reload_enabled()compares againstHotReload::Hot; andImportMetaHotPrototypeuses the new out-of-line cell helpers (Bun::allocatePlainObjectCell,reifyStaticPropertyTable,putToStringTagWithoutTransition,createClassStructure) that main converted this file's other prototype to. Everything else merged as is (checked by comparing the added and removed lines before and after). The intro example keeps itsdeclare globalblock again, which the first revision had dropped for no reason.import.meta.hotgetter as one part. This PR is independent and narrowly scoped to the runtime--hothook.no test proof · iteration 12 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/hot/hot.test.ts