Skip to content

bake: make import.meta.hot.on/off accept the documented vite: event names - #37786

Closed
robobun wants to merge 1 commit into
mainfrom
farm/ce0acfe1/hmr-vite-event-alias
Closed

robobun wants to merge 1 commit into
mainfrom
farm/ce0acfe1/hmr-vite-event-alias

Conversation

@robobun

@robobun robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In a dev server page, import.meta.hot.on("vite:beforeUpdate", h) never fires; the same code with "bun:beforeUpdate" fires on every hot update. Docs and types say vite:* is an alias of bun:*.
  • off() has the mirror problem: after on("bun:beforeUpdate", h); off("vite:beforeUpdate", h), h still fires.
  • Cause: on() stripped four characters from the five character vite: prefix, so the handler was stored under bun::beforeUpdate, a key nothing emits to. off() did not translate the name at all.
  • The existing test expected the vite: handler not to log, so it was pinning the bug.

Fix

  • on() and off() both pass the name through one helper that swaps the whole vite: prefix for bun:. Other names are stored unchanged.
  • Property to check: every key in the handler table is a bun:* name, so a handler can be removed under either spelling, including the automatic removal of a replaced module's listeners. The emit side is untouched.
  • Only other visible change: vite:* handlers that were silently dead now run, with the same empty payload as bun:* handlers. Docs and types need no change.
  • Verification: the reworked test and one new test fail on the current release and pass with this change, per the original description. The evidence block below says the test file was not run on the submitting machine and defers to CI.

Background

  • Bake is bun's dev server with hot module replacement. import.meta.hot is the per-module HMR API page code sees, implemented in a runtime shipped to the browser.
  • import.meta.hot.on/off subscribe to events such as bun:beforeUpdate and bun:afterUpdate, emitted around each hot update. vite:* names are documented aliases so code written for Vite works unchanged.
  • Handlers live in one table keyed by event name; emitting looks up only the bun:* key, so a handler under any other key is unreachable.
  • When a module is hot-replaced its dispose callbacks run; on() registers one that calls off(), which is how a replaced module's listeners are dropped.

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

Original description

What

import.meta.hot.on() and import.meta.hot.off() in the dev server runtime now treat the vite:* event names as the aliases of bun:* that the docs and types say they are.

Repro

In a page served by the dev server:

import.meta.hot.on("vite:beforeUpdate", () => console.log("before update"));
import.meta.hot.accept();

Save the file. Nothing is logged, while the same code with "bun:beforeUpdate" logs on every hot update. off() has the mirror image of the problem: after on("bun:beforeUpdate", h); off("vite:beforeUpdate", h), h still fires.

Cause

HMRModule.on() in src/runtime/bake/hmr-module.ts rewrote the name with

event = "bun:" + event.slice(4);

"vite:" is five characters, so "vite:beforeUpdate" became "bun::beforeUpdate" (two colons) and the handler sat under a key that emitEvent("bun:beforeUpdate") never looks at. off() did not rewrite the name at all, so it looked up eventHandlers["vite:..."], which never has anything in it.

The existing test import.meta.hot on/off events registered a vite:beforeUpdate handler and expected it not to log, so it was pinning the broken behavior.

Fix

Both on() and off() go through one normalizeEventName() helper that replaces the whole vite: prefix with bun:. Because the same helper is used on both sides, a handler can be removed with either name, and the dispose callback that on() registers (which removes a module's listeners when it is replaced) keeps working for handlers added under the vite: name. Names without the prefix are stored unchanged, as before.

This is the behavior documented in docs/bundler/hot-reloading.mdx ("these events are also available with the vite:* prefix") and in the JSDoc for on()/off() in packages/bun-types/devserver.d.ts, and it matches what the original code was clearly trying to do. Neither the docs nor the types need to change.

The only user-visible change besides the alias working is that vite:* handlers which were silently dead before now run, with the same (empty) payload the bun:* handlers get. A handler that throws is handled the same way a throwing bun:* handler already is: the client runtime logs it and falls back to a full reload (hmr-runtime-client.ts, the .catch around replaceModules). Normalization happens only in on()/off(); emitEvent/onEvent are internal and typed to the bun:* names, so every key stored in the handler table is canonical and nothing else needs to know about the alias.

Tests

test/bake/dev/hot.test.ts:

  • import.meta.hot on/off events is reworked so that it asserts the events fire: a bun:beforeUpdate and a vite:beforeUpdate handler both fire for the update that replaces the module, a vite:afterUpdate handler added by the new module fires when that update finishes, a handler that was passed to off() never fires, and the replaced module's listeners (both prefixes) are gone on the next update.
  • import.meta.hot.off accepts bun: and vite: event names interchangeably covers on("bun:...") + off("vite:...") and on("vite:...") + off("bun:..."), with a third handler left registered to prove the event was emitted.

Both tests fail on the current release (USE_SYSTEM_BUN=1 bun test): the first only receives bun:beforeUpdate (initial) and Updated setup, the second receives a instead of still listening. Both pass with this change (bun bd test), as does the rest of hot.test.ts.

…ames

HMRModule.on() mapped "vite:X" to "bun:" + event.slice(4), which keeps the
colon and registers the handler under "bun::X", so handlers added with the
vite: alias never fired. off() did not map the name at all, so it could not
remove a handler by its vite: name either. Both now go through one helper
that strips the full "vite:" prefix.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35a6da82-9cb6-49ad-96a0-b410afa5cbbf

📥 Commits

Reviewing files that changed from the base of the PR and between 626034f and 09f2cc2.

📒 Files selected for processing (2)
  • src/runtime/bake/hmr-module.ts
  • test/bake/dev/hot.test.ts

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

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:05 AM PT - Aug 12th, 2026

❌ @robobun, your commit 09f2cc2 has some failures in Build #92988 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 37786

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

bun-37786 --bun

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on the current release with the two tests in this PR (USE_SYSTEM_BUN=1 bun test test/bake/dev/hot.test.ts -t "on/off"): the vite:beforeUpdate handler never fires and off("vite:beforeUpdate", h) leaves h registered. Both tests pass with this change (bun bd test test/bake/dev/hot.test.ts), along with the rest of the file.

CI (build 92988): 179 of 181 jobs passed. The build is marked failed only because the two macOS 26 aarch64 test jobs expired without ever starting; that queue is backed up for every build right now (about 130 of these jobs queued, the oldest waiting over 5 hours, and a batch of other branches expired at the same minute), so a rebuild would most likely expire the same way and I am not retriggering. The annotated test failures are all retry-passed flakes in unrelated files (napi, sqlite, grpc-js, cron, next-pages, and so on); nothing in test/bake failed anywhere. The change is platform-independent TypeScript in the embedded HMR runtime and its tests passed on every other lane, so this is ready for review as is; the two missing lanes can be retried from Buildkite if wanted.

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

LGTM — clear off-by-one fix (.slice(4) on a 5-char prefix) plus the missing normalization in off(), factored into one shared helper.

What was reviewed

  • normalizeEventName: "vite:".length is 5, so "vite:beforeUpdate" now maps to "bun:beforeUpdate" instead of "bun::beforeUpdate"; non-vite: names pass through unchanged.
  • on() reassigns event before registering the dispose closure, so auto-cleanup on module replace still finds the handler; off() on an already-normalized name is a no-op re-normalize.
  • Traced both new tests through replaceModules (beforeUpdate → dispose → reload → afterUpdate) — the asserted message order matches, and the reworked test now fails on the old behavior instead of pinning it.
Extended reasoning...

Overview

Two-file change. src/runtime/bake/hmr-module.ts extracts a normalizeEventName() helper that rewrites vite:* event names to bun:* and routes both HMRModule.on() and HMRModule.off() through it. This fixes two bugs in one class: on() used .slice(4) on the 5-character "vite:" prefix (producing "bun::beforeUpdate", which nothing emits), and off() did not rewrite the name at all. test/bake/dev/hot.test.ts reworks the existing import.meta.hot on/off events test — which previously asserted the broken behavior — to assert that vite:* handlers actually fire, and adds a second test covering cross-prefix on/off.

Security risks

None. This is client-side dev-server HMR runtime code that maps one string prefix to another in an in-memory event handler table. No parsing of untrusted input, no filesystem/network/auth surface.

Level of scrutiny

Low-to-medium. The runtime change is a ~10-line refactor of an obviously-wrong string slice into a named helper, applied at both sibling sites (on and off) as REVIEW.md asks for. The behavior is documented in docs/bundler/hot-reloading.mdx and packages/bun-types/devserver.d.ts, so this is bringing the implementation in line with the stated contract rather than changing API surface. No CODEOWNERS entry covers src/runtime/bake/.

I traced the test expectations through replaceModules in hmr-module.ts: emitEvent("bun:beforeUpdate") runs before dispose, dispose removes the old module's on()-registered handlers via the closure (which captured the normalized name, so off() finds them), the new module body runs, then emitEvent("bun:afterUpdate") fires. The expected message sequences in both tests line up exactly with that order. The stale dispose callbacks left behind when a user calls off() manually before dispose are harmless — indexOf returns -1.

Other factors

The PR description states both new tests fail under USE_SYSTEM_BUN=1 and pass under bun bd test, and the rest of hot.test.ts still passes. The reworked test replaces one that was certifying the bug (it registered a vite:beforeUpdate handler and asserted it did not log), which is exactly the "read snapshot contents before committing" failure mode REVIEW.md warns about — good that it's fixed. The helper uses "vite:".length rather than a bare 5, which is the repo's preferred magic-number style.

@robobun

robobun commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #39488, which consolidates the open Bake PRs from this period into one branch. It carries this PR's change and its test, reworked where needed (see the commit in #39488 that names this PR). Closing this one in its favor.

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