227 lines
9.5 KiB
Plaintext
227 lines
9.5 KiB
Plaintext
---
|
|
title: Hot reloading
|
|
description: Hot Module Replacement (HMR) for Bun's development server
|
|
---
|
|
|
|
Hot Module Replacement (HMR) updates modules in a running application without a full page reload, preserving application state.
|
|
|
|
<Note>HMR is enabled by default when using Bun's full-stack development server.</Note>
|
|
|
|
## `import.meta.hot` API Reference
|
|
|
|
Bun implements a client-side HMR API modeled after [Vite's `import.meta.hot` API](https://vite.dev/guide/api-hmr). You can check for it with `if (import.meta.hot)`, which tree-shakes it in production.
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
if (import.meta.hot) {
|
|
// HMR APIs are available.
|
|
}
|
|
```
|
|
|
|
This check is often unnecessary, since Bun dead-code-eliminates calls to all of the HMR APIs in production builds.
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
// This entire function call is removed in production.
|
|
import.meta.hot.dispose(() => {
|
|
console.log("dispose");
|
|
});
|
|
```
|
|
|
|
<Warning>
|
|
For this dead-code elimination to work, Bun forces these APIs to be called without indirection. That means the following do not work:
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
// INVALID: Assigning `hot` to a variable
|
|
const hot = import.meta.hot;
|
|
hot.accept();
|
|
|
|
// INVALID: Assigning `import.meta` to a variable
|
|
const meta = import.meta;
|
|
meta.hot.accept();
|
|
console.log(meta.hot.data);
|
|
|
|
// INVALID: Passing to a function
|
|
doSomething(import.meta.hot.dispose);
|
|
|
|
// OK: The full phrase "import.meta.hot.<API>" must be called directly:
|
|
import.meta.hot.accept();
|
|
|
|
// OK: `data` can be passed to functions:
|
|
doSomething(import.meta.hot.data);
|
|
```
|
|
|
|
</Warning>
|
|
|
|
<Note>
|
|
The HMR API is still a work in progress. Some features are missing. To disable HMR in `Bun.serve`, set the development option to `{ hmr: false }`.
|
|
</Note>
|
|
|
|
## API Methods
|
|
|
|
| Method | Status | Notes |
|
|
| ------------------ | ------ | --------------------------------------------------------------------- |
|
|
| `hot.accept()` | ✅ | Indicate that a hot update can be replaced gracefully. |
|
|
| `hot.data` | ✅ | Persist data between module evaluations. |
|
|
| `hot.dispose()` | ✅ | Add a callback function to run when a module is about to be replaced. |
|
|
| `hot.invalidate()` | ❌ | |
|
|
| `hot.on()` | ✅ | Attach an event listener. |
|
|
| `hot.off()` | ✅ | Remove an event listener from `on`. |
|
|
| `hot.send()` | ❌ | |
|
|
| `hot.prune()` | 🚧 | Callback is currently never called. |
|
|
| `hot.decline()` | ✅ | No-op to match Vite's `import.meta.hot`. |
|
|
|
|
## import.meta.hot.accept()
|
|
|
|
The `accept()` method indicates that a module can be hot-replaced. Called without arguments, it means Bun can replace this module by re-evaluating the file. After a hot update, Bun automatically patches the module's importers.
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
// index.ts
|
|
import { getCount } from "./foo.ts";
|
|
|
|
console.log("count is ", getCount());
|
|
|
|
import.meta.hot.accept();
|
|
|
|
export function getNegativeCount() {
|
|
return -getCount();
|
|
}
|
|
```
|
|
|
|
This call creates a hot-reloading boundary for all of the files that `index.ts` imports. Whenever you save `foo.ts` or any of its dependencies, the update bubbles up to `index.ts`, which re-evaluates. Bun then patches the files that import `index.ts` to import the new version of `getNegativeCount()`. If you update only `index.ts`, Bun re-evaluates only that one file, and the counter in `foo.ts` is reused.
|
|
|
|
Combine this with `import.meta.hot.data` to transfer state from the previous module to the new one.
|
|
|
|
<Info>
|
|
When no modules call `import.meta.hot.accept()` (and there isn't React Fast Refresh or a plugin calling it for you),
|
|
the page reloads when the file updates. A console warning shows which files were invalidated. This warning is safe to
|
|
ignore if it makes more sense to rely on full page reloads.
|
|
</Info>
|
|
|
|
### With callback
|
|
|
|
When passed a callback, `import.meta.hot.accept` works as it does in Vite. Instead of patching the importers of this module, it calls the callback with the new module.
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
export const count = 0;
|
|
|
|
import.meta.hot.accept(newModule => {
|
|
if (newModule) {
|
|
// newModule is undefined when SyntaxError happened
|
|
console.log("updated: count is now ", newModule.count);
|
|
}
|
|
});
|
|
```
|
|
|
|
<Tip>Prefer `import.meta.hot.accept()` without an argument; it usually makes your code clearer.</Tip>
|
|
|
|
### Accepting other modules
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
import { count } from "./foo";
|
|
|
|
import.meta.hot.accept("./foo", newModule => {
|
|
if (!newModule) return;
|
|
|
|
console.log("updated: count is now ", count);
|
|
});
|
|
```
|
|
|
|
Indicates that a dependency's module can be accepted. When the dependency is updated, Bun calls the callback with the new module.
|
|
|
|
### With multiple dependencies
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
import.meta.hot.accept(["./foo", "./bar"], newModules => {
|
|
// newModules holds the updated module at its index and
|
|
// undefined for the other dependencies
|
|
});
|
|
```
|
|
|
|
This variant accepts an array of dependencies. The callback receives an array with the updated module at its index and `undefined` for the other dependencies.
|
|
|
|
## import.meta.hot.data
|
|
|
|
`import.meta.hot.data` carries state from the previous version of a module to the new one across a hot replacement. Writing to `import.meta.hot.data` also marks the module as self-accepting (equivalent to calling `import.meta.hot.accept()`).
|
|
|
|
```tsx title="index.tsx" icon="/icons/typescript.svg"
|
|
import { createRoot } from "react-dom/client";
|
|
import { App } from "./app";
|
|
|
|
const root = (import.meta.hot.data.root ??= createRoot(elem));
|
|
root.render(<App />); // re-use an existing root
|
|
```
|
|
|
|
In production, Bun inlines `data` as `{}`, so you cannot use it as a state holder.
|
|
|
|
<Tip>
|
|
We recommend this pattern for stateful modules because Bun can minify `{}.prop ??= value` into `value` in production.
|
|
</Tip>
|
|
|
|
## import.meta.hot.dispose()
|
|
|
|
Attaches an on-dispose callback. Bun calls it:
|
|
|
|
- Just before the module is replaced with another copy (before the next is loaded)
|
|
- After the module is detached (removing all imports to this module, see `import.meta.hot.prune()`)
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
const sideEffect = setupSideEffect();
|
|
|
|
import.meta.hot.dispose(() => {
|
|
sideEffect.cleanup();
|
|
});
|
|
```
|
|
|
|
<Warning>Bun does not call this callback on route navigation or when the browser tab closes.</Warning>
|
|
|
|
Returning a promise delays module replacement until the module is disposed. Bun calls all dispose callbacks in parallel.
|
|
|
|
## import.meta.hot.prune()
|
|
|
|
Attaches an on-prune callback. Bun calls it when all imports to this module are removed, but the module was previously loaded.
|
|
|
|
Use it to clean up resources that were created when the module was loaded. Unlike `import.meta.hot.dispose()`, it pairs better with `accept` and `data` for managing stateful resources. A full example managing a WebSocket:
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
import { something } from "./something";
|
|
|
|
// Initialize or re-use a WebSocket connection
|
|
export const ws = (import.meta.hot.data.ws ??= new WebSocket(location.origin));
|
|
|
|
// If the module's import is removed, clean up the WebSocket connection.
|
|
import.meta.hot.prune(() => {
|
|
ws.close();
|
|
});
|
|
```
|
|
|
|
<Info>
|
|
If you used `dispose` instead, the WebSocket would close and re-open on every hot update. Both versions of the code
|
|
prevent page reloads when you update imported files.
|
|
</Info>
|
|
|
|
## import.meta.hot.on() and off()
|
|
|
|
Use `on()` and `off()` to listen for events from the HMR runtime. Event names carry a prefix so that plugins do not conflict with each other.
|
|
|
|
```ts title="index.ts" icon="/icons/typescript.svg"
|
|
import.meta.hot.on("bun:beforeUpdate", () => {
|
|
console.log("before a hot update");
|
|
});
|
|
```
|
|
|
|
When a file is replaced, Bun automatically removes all of its event listeners.
|
|
|
|
### Built-in events
|
|
|
|
| Event | Emitted when |
|
|
| ---------------------- | ----------------------------------------------------------------------------------------------- |
|
|
| `bun:beforeUpdate` | before a hot update is applied. |
|
|
| `bun:afterUpdate` | after a hot update is applied. |
|
|
| `bun:beforeFullReload` | before a full page reload happens. |
|
|
| `bun:beforePrune` | before prune callbacks are called. |
|
|
| `bun:invalidate` | when a module is invalidated with `import.meta.hot.invalidate()`. |
|
|
| `bun:error` | when a build or runtime error occurs. |
|
|
| `bun:ws:disconnect` | when the HMR WebSocket connection is lost. This can indicate the development server is offline. |
|
|
| `bun:ws:connect` | when the HMR WebSocket connects or re-connects. |
|
|
|
|
<Note>For compatibility with Vite, these events are also available with the `vite:*` prefix instead of `bun:*`.</Note>
|