mirror of
https://github.com/oven-sh/bun
synced 2026-02-14 04:49:06 +00:00
### What does this PR do?
Fixes a crash related to the dev server overwriting the uws user context
pointer when setting abort callback.
Adds support for `return new Response(<jsx />, { ... })` and `return
Response.render(...)` and `return Response.redirect(...)`:
- Created a `SSRResponse` class to handle this (see
`JSBakeResponse.{h,cpp}`)
- `SSRResponse` is designed to "fake" being a React component
- This is done in JSBakeResponse::create inside of
src/bun.js/bindings/JSBakeResponse.cpp
- And `src/js/builtins/BakeSSRResponse.ts` defines a `wrapComponent`
function which wraps
the passed in component (when doing `new Response(<jsx />, ...)`). It
does
this to throw an error (in redirect()/render() case) or return the
component.
- Created a `BakeAdditionsToGlobal` struct which contains some
properties
needed for this
- Added some of the properties we need to fake to BunBuiltinNames.h
(e.g.
`$$typeof`), the rationale behind this is that we couldn't use
`structure->addPropertyTransition` because JSBakeResponse is not a final
JSObject.
- When bake and server-side, bundler rewrites `Response ->
Bun.SSRResponse` (see `src/ast/P.zig` and `src/ast/visitExpr.zig`)
- Created a new WebCore body variant (`Render: struct { path: []const u8
}`)
- Created when `return Response.render(...)`
- When handled, it re-invokes dev server to render the new path
Enables server-side sourcemaps for the dev server:
- New source providers for server-side:
(`DevServerSourceProvider.{h,cpp}`)
- IncrementalGraph and SourceMapStore are updated to support this
There are numerous other stuff:
- allow `app` configuration from Bun.serve(...)
- fix errors stopping dev server
- fix use after free related to in
RequestContext.finishRunningErrorHandler
- Request.cookies
- Make `"use client";` components work
- Fix some bugs using `require(...)` in dev server
- Fix catch-all routes not working in the dev server
- Updates `findSourceMappingURL(...)` to use `std.mem.lastIndexOf(...)`
because
the sourcemap that should be used is the last one anyway
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
968 lines
29 KiB
TypeScript
968 lines
29 KiB
TypeScript
// This is an implementation of a module loader with hot-reloading support.
|
|
// Note that this aims to implement the behavior of `bun build` rather than what
|
|
// the ECMAScript Module spec says. This way, development builds function like
|
|
// the production ones from `bun build`.
|
|
//
|
|
// Some build failures from the bundler surface as runtime errors here, such as
|
|
// `require` on a module with transitive top-level await, or a missing export.
|
|
// This was done to make incremental updates as isolated as possible.
|
|
import {
|
|
__callDispose,
|
|
__legacyDecorateClassTS,
|
|
__legacyDecorateParamTS,
|
|
__legacyMetadataTS,
|
|
__name,
|
|
__using,
|
|
} from "../runtime.bun";
|
|
// This import is different based on client vs server side.
|
|
// On the server, remapping is done automatically.
|
|
import { type SourceMapURL, derefMapping } from "#stack-trace";
|
|
|
|
/** List of loaded modules. Every `Id` gets one HMRModule, mutated across updates. */
|
|
const registry = new Map<Id, HMRModule>();
|
|
const registrySourceMapIds = new Map<string, SourceMapURL>();
|
|
/** Server */
|
|
export const serverManifest = {};
|
|
/** Server */
|
|
export const ssrManifest = {};
|
|
/** Client */
|
|
export let onServerSideReload: (() => Promise<void>) | null = null;
|
|
const eventHandlers: Record<HMREvent | string, HotEventHandler[] | undefined> = {};
|
|
let refreshRuntime: any;
|
|
/** The expression `import(a,b)` is not supported in all browsers, most notably
|
|
* in Mozilla Firefox in 2025. Bun lazily evaluates it, so a SyntaxError gets
|
|
* thrown upon first usage. */
|
|
let lazyDynamicImportWithOptions: null | Function = null;
|
|
|
|
const enum State {
|
|
Pending,
|
|
Stale,
|
|
Loaded,
|
|
Error,
|
|
}
|
|
const enum ESMProps {
|
|
imports,
|
|
exports,
|
|
stars,
|
|
load,
|
|
isAsync,
|
|
}
|
|
|
|
/** Given an Id, return the module namespace object.
|
|
* For use in other functions in the HMR runtime.
|
|
* Registers that module as a root. */
|
|
export async function loadExports<T>(id: Id): Promise<T> {
|
|
const m = await loadModuleAsync(id, false, null);
|
|
return m.esm ? m.exports : m.cjs.exports;
|
|
}
|
|
|
|
interface HotAccept {
|
|
modules: string[];
|
|
cb: HotAcceptFunction;
|
|
single: boolean;
|
|
}
|
|
|
|
interface CJSModule {
|
|
id: Id;
|
|
exports: unknown;
|
|
require: (id: Id) => unknown;
|
|
}
|
|
|
|
/** Implementation details must remain in sync with js_parser.zig and bundle_v2.zig */
|
|
export class HMRModule {
|
|
/** Key in `registry` */
|
|
id: Id;
|
|
/** ES Modules have different semantics for `.exports` and `.cjs` */
|
|
esm: boolean;
|
|
state: State = State.Pending;
|
|
/** The ESM namespace object. `null` if not yet initialized. */
|
|
exports: any = null;
|
|
/** For ESM, this is the converted CJS exports.
|
|
* For CJS, this is the `module` object. */
|
|
cjs: CJSModule | any | null;
|
|
/** When a module fails to load, trying to load it again
|
|
* should throw the same error */
|
|
failure: unknown = null;
|
|
/** Two purposes:
|
|
* 1. HMRModule[] - List of parsed imports. indexOf is used to go from HMRModule -> updater function
|
|
* 2. any[] - List of module namespace objects. Read by the ESM module's load function.
|
|
* Unused for CJS
|
|
*/
|
|
imports: HMRModule[] | any[] | null = null;
|
|
/** Assignned by an ESM module's load function immediately.
|
|
* HTML files do not emit a store to this field */
|
|
updateImport: ((exports: any) => void)[] | null = null;
|
|
/** When calling `import.meta.hot.dispose` */
|
|
onDispose: HotDisposeFunction[] | null = null;
|
|
/** When calling `import.meta.hot.accept` to self-accept */
|
|
selfAccept: HotAcceptFunction | null = null;
|
|
/** When calling `import.meta.hot.accept` on another module */
|
|
depAccepts: Record<Id, HotAccept> | null = null;
|
|
/** All modules that have imported this module */
|
|
importers = new Set<HMRModule>();
|
|
/** import.meta.hot.data rewrites to this */
|
|
data: any = {};
|
|
|
|
constructor(id: Id, isCommonJS: boolean) {
|
|
this.id = id;
|
|
this.esm = !isCommonJS;
|
|
this.cjs = isCommonJS
|
|
? {
|
|
id,
|
|
exports: {},
|
|
require: this.require.bind(this),
|
|
}
|
|
: null;
|
|
}
|
|
|
|
// Module Ids are pre-resolved by the bundler
|
|
requireResolve(id: Id): Id {
|
|
return id;
|
|
}
|
|
|
|
require(id: Id) {
|
|
try {
|
|
const mod = loadModuleSync(id, true, this);
|
|
return mod.esm ? (mod.cjs ??= toCommonJS(mod.exports)) : mod.cjs.exports;
|
|
} catch (e: any) {
|
|
if (e instanceof AsyncImportError) {
|
|
e.message = `Cannot require "${id}" because "${e.asyncId}" uses top-level await, but 'require' is a synchronous operation.`;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/** Lowered from `.e_import` (import(id)) */
|
|
dynamicImport(id: Id, opts?: ImportCallOptions) {
|
|
const found = loadModuleAsync(id, true, this);
|
|
if (found) {
|
|
if ((found as HMRModule).id === id) return Promise.resolve(getEsmExports(found as HMRModule));
|
|
return (found as Promise<HMRModule>).then(getEsmExports);
|
|
}
|
|
return opts
|
|
? (lazyDynamicImportWithOptions ??= new Function("specifier, opts", "import(specifier, opts)"))(id, opts)
|
|
: import(id);
|
|
}
|
|
|
|
reactRefreshAccept() {
|
|
if (isReactRefreshBoundary(this.exports)) {
|
|
this.accept();
|
|
}
|
|
}
|
|
|
|
get importMeta() {
|
|
const importMeta = {
|
|
url: `${location.origin}/${this.id}`,
|
|
main: false,
|
|
require: this.require.bind(this),
|
|
// transpiler rewrites `import.meta.hot.*` to access `HMRModule.*`
|
|
get hot() {
|
|
throw new Error("import.meta.hot cannot be used indirectly.");
|
|
},
|
|
};
|
|
Object.defineProperty(this, "importMeta", { value: importMeta });
|
|
return importMeta;
|
|
}
|
|
|
|
// Bundler rewrites all import.meta.hot.* to access the corresponding methods
|
|
// on HMRModule directly. The following code implements that interface. Data
|
|
// is an opaque property, which is preserved simply by the fact HMRModule is
|
|
// not destructed.
|
|
|
|
accept(
|
|
arg1?: string | readonly string[] | HotAcceptFunction,
|
|
arg2?: HotAcceptFunction | HotArrayAcceptFunction | undefined,
|
|
) {
|
|
if (arg2 == undefined) {
|
|
if (arg1 == undefined) {
|
|
this.selfAccept = implicitAcceptFunction;
|
|
return;
|
|
}
|
|
if (typeof arg1 !== "function") {
|
|
throw new Error("import.meta.hot.accept requires a callback function");
|
|
}
|
|
// Self-accept function
|
|
this.selfAccept = arg1;
|
|
} else {
|
|
throw new Error(
|
|
'"import.meta.hot.accept" must be directly called with string literals for ' +
|
|
"the specifiers. This way, the bundler can pre-process the arguments.",
|
|
);
|
|
}
|
|
}
|
|
|
|
acceptSpecifiers(specifiers: string | readonly string[], cb?: HotAcceptFunction | HotArrayAcceptFunction) {
|
|
this.depAccepts ??= {};
|
|
const isArray = Array.isArray(specifiers);
|
|
const accept: HotAccept = {
|
|
modules: isArray ? specifiers : [specifiers],
|
|
cb: cb as HotAcceptFunction,
|
|
single: !isArray,
|
|
};
|
|
if (isArray) {
|
|
for (const specifier of specifiers) {
|
|
this.depAccepts[specifier] = accept;
|
|
}
|
|
} else {
|
|
this.depAccepts[specifiers as string] = accept;
|
|
}
|
|
}
|
|
|
|
decline() {} // Vite: "This is currently a noop and is there for backward compatibility"
|
|
|
|
dispose(cb: HotDisposeFunction) {
|
|
(this.onDispose ??= []).push(cb);
|
|
}
|
|
|
|
prune(cb: HotDisposeFunction) {
|
|
// Bun currently does not throw away detached modules yet.
|
|
// So never calling the function technically implements this.
|
|
}
|
|
|
|
invalidate() {
|
|
emitEvent("bun:invalidate", null);
|
|
// by throwing an error right now, this will cause a page refresh
|
|
throw new Error("TODO: implement ImportMetaHot.invalidate");
|
|
}
|
|
|
|
on(event: string, cb: HotEventHandler) {
|
|
// Vite compatibility, but favor using Bun's event names.
|
|
if (event.startsWith("vite:")) {
|
|
event = "bun:" + event.slice(4);
|
|
}
|
|
|
|
(eventHandlers[event] ??= []).push(cb);
|
|
this.dispose(() => this.off(event, cb));
|
|
}
|
|
|
|
off(event: string, cb: HotEventHandler) {
|
|
const handlers = eventHandlers[event];
|
|
if (!handlers) return;
|
|
const index = handlers.indexOf(cb);
|
|
if (index !== -1) {
|
|
handlers.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
send(event: string, cb: HotEventHandler) {
|
|
throw new Error("TODO: implement ImportMetaHot.send");
|
|
}
|
|
|
|
declare indirectHot: any;
|
|
|
|
/** Server-only */
|
|
declare builtin: (id: string) => any;
|
|
}
|
|
if (side === "server") {
|
|
HMRModule.prototype.builtin = (id: string) =>
|
|
// @ts-expect-error
|
|
import.meta.bakeBuiltin(import.meta.resolve(id));
|
|
}
|
|
// prettier-ignore
|
|
HMRModule.prototype.indirectHot = new Proxy({}, {
|
|
get(_, prop) {
|
|
if (typeof prop === "symbol") return undefined;
|
|
throw new Error(`import.meta.hot.${prop} cannot be used indirectly.`);
|
|
},
|
|
set() {
|
|
throw new Error(`The import.meta.hot object cannot be mutated.`);
|
|
},
|
|
});
|
|
|
|
// TODO: This function is currently recursive.
|
|
export function loadModuleSync(id: Id, isUserDynamic: boolean, importer: HMRModule | null): HMRModule {
|
|
// First, try and re-use an existing module.
|
|
let mod = registry.get(id);
|
|
if (mod) {
|
|
if (mod.state === State.Error) throw mod.failure;
|
|
if (mod.state === State.Stale) {
|
|
mod.state = State.Pending;
|
|
isUserDynamic = false;
|
|
} else {
|
|
if (importer) {
|
|
mod.importers.add(importer);
|
|
}
|
|
return mod;
|
|
}
|
|
}
|
|
const loadOrEsmModule = unloadedModuleRegistry[id];
|
|
if (!loadOrEsmModule) throwNotFound(id, isUserDynamic);
|
|
|
|
if (typeof loadOrEsmModule === "function") {
|
|
// CommonJS
|
|
if (!mod) {
|
|
mod = new HMRModule(id, true);
|
|
registry.set(id, mod);
|
|
} else if (mod.esm) {
|
|
mod.esm = false;
|
|
mod.cjs = {
|
|
id,
|
|
exports: {},
|
|
require: mod.require.bind(this),
|
|
};
|
|
mod.exports = null;
|
|
}
|
|
if (importer) {
|
|
mod.importers.add(importer);
|
|
}
|
|
try {
|
|
const cjs = mod.cjs;
|
|
loadOrEsmModule(mod, cjs, cjs.exports);
|
|
} catch (e) {
|
|
mod.state = State.Stale;
|
|
mod.cjs.exports = {};
|
|
throw e;
|
|
}
|
|
mod.state = State.Loaded;
|
|
} else {
|
|
// ESM
|
|
if (IS_BUN_DEVELOPMENT) {
|
|
try {
|
|
DEBUG.ASSERT(Array.isArray(loadOrEsmModule[ESMProps.imports]));
|
|
DEBUG.ASSERT(Array.isArray(loadOrEsmModule[ESMProps.exports]));
|
|
DEBUG.ASSERT(Array.isArray(loadOrEsmModule[ESMProps.stars]));
|
|
DEBUG.ASSERT(typeof loadOrEsmModule[ESMProps.load] === "function");
|
|
DEBUG.ASSERT(typeof loadOrEsmModule[ESMProps.isAsync] === "boolean");
|
|
} catch (e) {
|
|
console.warn(id, loadOrEsmModule);
|
|
throw e;
|
|
}
|
|
}
|
|
const { [ESMProps.imports]: deps, [ESMProps.load]: load, [ESMProps.isAsync]: isAsync } = loadOrEsmModule;
|
|
if (isAsync) {
|
|
throw new AsyncImportError(id);
|
|
}
|
|
if (!mod) {
|
|
mod = new HMRModule(id, false);
|
|
registry.set(id, mod);
|
|
} else if (!mod.esm) {
|
|
mod.esm = true;
|
|
mod.cjs = null;
|
|
mod.exports = null;
|
|
}
|
|
if (importer) {
|
|
mod.importers.add(importer);
|
|
}
|
|
|
|
const { list: depsList } = parseEsmDependencies(mod, deps, loadModuleSync);
|
|
const exportsBefore = mod.exports;
|
|
mod.imports = depsList.map(getEsmExports);
|
|
load(mod);
|
|
mod.imports = depsList;
|
|
if (mod.exports === exportsBefore) mod.exports = {};
|
|
mod.cjs = null;
|
|
mod.state = State.Loaded;
|
|
}
|
|
|
|
return mod;
|
|
}
|
|
|
|
// Do not add the `async` keyword to this function, that way the list of
|
|
// `HMRModule`s can be created synchronously, even if evaluation is not.
|
|
// Returns `null` if the module is not found in dynamic mode, so that the caller
|
|
// can use the `import` keyword instead.
|
|
// TODO: This function is currently recursive.
|
|
export function loadModuleAsync<IsUserDynamic extends boolean>(
|
|
id: Id,
|
|
isUserDynamic: IsUserDynamic,
|
|
importer: HMRModule | null,
|
|
): (IsUserDynamic extends true ? null : never) | Promise<HMRModule> | HMRModule {
|
|
// First, try and re-use an existing module.
|
|
let mod = registry.get(id)!;
|
|
if (mod) {
|
|
const { state } = mod;
|
|
if (state === State.Error) throw mod.failure;
|
|
if (state === State.Stale) {
|
|
mod.state = State.Pending;
|
|
isUserDynamic = false as IsUserDynamic;
|
|
} else {
|
|
if (importer) {
|
|
mod.importers.add(importer);
|
|
}
|
|
return mod;
|
|
}
|
|
}
|
|
const loadOrEsmModule = unloadedModuleRegistry[id];
|
|
if (!loadOrEsmModule) {
|
|
if (isUserDynamic) return null!;
|
|
throwNotFound(id, isUserDynamic);
|
|
}
|
|
|
|
if (typeof loadOrEsmModule === "function") {
|
|
// CommonJS
|
|
if (!mod) {
|
|
mod = new HMRModule(id, true);
|
|
registry.set(id, mod);
|
|
} else if (mod.esm) {
|
|
mod.esm = false;
|
|
mod.cjs = {
|
|
id,
|
|
exports: {},
|
|
require: mod.require.bind(this),
|
|
};
|
|
mod.exports = null;
|
|
}
|
|
if (importer) {
|
|
mod.importers.add(importer);
|
|
}
|
|
try {
|
|
const cjs = mod.cjs;
|
|
loadOrEsmModule(mod, cjs, cjs.exports);
|
|
} catch (e) {
|
|
mod.state = State.Stale;
|
|
mod.cjs.exports = {};
|
|
throw e;
|
|
}
|
|
mod.state = State.Loaded;
|
|
return mod;
|
|
} else {
|
|
// ESM
|
|
if (IS_BUN_DEVELOPMENT) {
|
|
try {
|
|
DEBUG.ASSERT(Array.isArray(loadOrEsmModule[0]));
|
|
DEBUG.ASSERT(Array.isArray(loadOrEsmModule[1]));
|
|
DEBUG.ASSERT(Array.isArray(loadOrEsmModule[2]));
|
|
DEBUG.ASSERT(typeof loadOrEsmModule[3] === "function");
|
|
DEBUG.ASSERT(typeof loadOrEsmModule[4] === "boolean");
|
|
} catch (e) {
|
|
console.warn(id, loadOrEsmModule);
|
|
throw e;
|
|
}
|
|
}
|
|
const [deps /* exports */ /* stars */, , , load /* isAsync */] = loadOrEsmModule;
|
|
|
|
if (!mod) {
|
|
mod = new HMRModule(id, false);
|
|
registry.set(id, mod);
|
|
} else if (!mod.esm) {
|
|
mod.esm = true;
|
|
mod.exports = null;
|
|
mod.cjs = null;
|
|
}
|
|
if (importer) {
|
|
mod.importers.add(importer);
|
|
}
|
|
|
|
const { list, isAsync } = parseEsmDependencies(mod, deps, loadModuleAsync<false>);
|
|
DEBUG.ASSERT(
|
|
isAsync //
|
|
? list.some(x => x instanceof Promise)
|
|
: list.every(x => x instanceof HMRModule),
|
|
);
|
|
|
|
// Running finishLoadModuleAsync synchronously when there are no promises is
|
|
// not a performance optimization but a behavioral correctness issue.
|
|
return isAsync
|
|
? Promise.all(list).then(
|
|
list => finishLoadModuleAsync(mod, load, list),
|
|
e => {
|
|
mod.state = State.Error;
|
|
mod.failure = e;
|
|
throw e;
|
|
},
|
|
)
|
|
: finishLoadModuleAsync(
|
|
mod,
|
|
load,
|
|
list as HMRModule[], // no promises as by assert above
|
|
);
|
|
}
|
|
}
|
|
|
|
function finishLoadModuleAsync(mod: HMRModule, load: UnloadedESM[3], modules: HMRModule[]) {
|
|
try {
|
|
const exportsBefore = mod.exports;
|
|
mod.imports = modules.map(getEsmExports);
|
|
const shouldPatchImporters = !mod.selfAccept || mod.selfAccept === implicitAcceptFunction;
|
|
const p = load(mod);
|
|
mod.imports = modules;
|
|
if (p) {
|
|
return p.then(() => {
|
|
mod.state = State.Loaded;
|
|
if (mod.exports === exportsBefore) mod.exports = {};
|
|
mod.cjs = null;
|
|
if (shouldPatchImporters) patchImporters(mod);
|
|
return mod;
|
|
});
|
|
}
|
|
if (mod.exports === exportsBefore) mod.exports = {};
|
|
mod.cjs = null;
|
|
if (shouldPatchImporters) patchImporters(mod);
|
|
mod.state = State.Loaded;
|
|
return mod;
|
|
} catch (e) {
|
|
mod.state = State.Error;
|
|
mod.failure = e;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
type GenericModuleLoader<R> = (id: Id, isUserDynamic: false, importer: HMRModule) => R;
|
|
// TODO: This function is currently recursive.
|
|
function parseEsmDependencies<T extends GenericModuleLoader<any>>(
|
|
parent: HMRModule,
|
|
deps: (string | number)[],
|
|
enqueueModuleLoad: T,
|
|
) {
|
|
let i = 0;
|
|
let list: ReturnType<T>[] = [];
|
|
let isAsync = false;
|
|
const { length } = deps;
|
|
while (i < length) {
|
|
const dep = deps[i] as string;
|
|
DEBUG.ASSERT(typeof dep === "string");
|
|
let expectedExportKeyEnd = i + 2 + (deps[i + 1] as number);
|
|
DEBUG.ASSERT(typeof deps[i + 1] === "number");
|
|
const promiseOrModule = enqueueModuleLoad(dep, false, parent);
|
|
list.push(promiseOrModule);
|
|
|
|
const unloadedModule = unloadedModuleRegistry[dep];
|
|
if (!unloadedModule) {
|
|
throwNotFound(dep, false);
|
|
}
|
|
if (typeof unloadedModule !== "function") {
|
|
const availableExportKeys = unloadedModule[ESMProps.exports];
|
|
i += 2;
|
|
while (i < expectedExportKeyEnd) {
|
|
const key = deps[i] as string;
|
|
DEBUG.ASSERT(typeof key === "string");
|
|
// TODO: there is a bug in the way exports are verified. Additionally a
|
|
// possible performance issue. For the meantime, this is disabled since
|
|
// it was not shipped in the initial 1.2.3 HMR, and real issues will
|
|
// just throw 'undefined is not a function' or so on.
|
|
|
|
// if (!availableExportKeys.includes(key)) {
|
|
// if (!hasExportStar(unloadedModule[ESMProps.stars], key)) {
|
|
// throw new SyntaxError(`Module "${dep}" does not export key "${key}"`);
|
|
// }
|
|
// }
|
|
i++;
|
|
}
|
|
isAsync ||= promiseOrModule instanceof Promise;
|
|
} else {
|
|
DEBUG.ASSERT(!registry.get(dep)?.esm);
|
|
i = expectedExportKeyEnd;
|
|
|
|
if (IS_BUN_DEVELOPMENT) {
|
|
DEBUG.ASSERT((list[list.length - 1] as any) instanceof HMRModule);
|
|
}
|
|
}
|
|
}
|
|
return { list, isAsync };
|
|
}
|
|
|
|
function hasExportStar(starImports: Id[], key: string) {
|
|
if (starImports.length === 0) return false;
|
|
const queue: Id[] = [...starImports];
|
|
const visited = new Set<Id>();
|
|
while (queue.length > 0) {
|
|
const starImport = queue.shift()!;
|
|
if (visited.has(starImport)) continue;
|
|
visited.add(starImport);
|
|
const mod = unloadedModuleRegistry[starImport];
|
|
DEBUG.ASSERT(mod, `Module "${starImport}" not found`);
|
|
if (typeof mod === "function") {
|
|
return true;
|
|
}
|
|
const availableExportKeys = mod[ESMProps.exports];
|
|
if (availableExportKeys.includes(key)) {
|
|
return true; // Found
|
|
}
|
|
const nestedStarImports = mod[ESMProps.stars];
|
|
for (const nestedImport of nestedStarImports) {
|
|
if (!visited.has(nestedImport)) {
|
|
queue.push(nestedImport);
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function getEsmExports(m: HMRModule) {
|
|
return m.esm ? m.exports : (m.exports ??= toESM(m.cjs.exports));
|
|
}
|
|
|
|
type HotAcceptFunction = (esmExports?: any | void) => void;
|
|
type HotArrayAcceptFunction = (esmExports: (any | void)[]) => void;
|
|
type HotDisposeFunction = (data: any) => void | Promise<void>;
|
|
type HotEventHandler = (data: any) => void;
|
|
|
|
// If updating this, make sure the `devserver.d.ts` types are
|
|
// kept in sync.
|
|
type HMREvent =
|
|
| "bun:ready"
|
|
| "bun:beforeUpdate"
|
|
| "bun:afterUpdate"
|
|
| "bun:beforeFullReload"
|
|
| "bun:beforePrune"
|
|
| "bun:invalidate"
|
|
| "bun:error"
|
|
| "bun:ws:disconnect"
|
|
| "bun:ws:connect";
|
|
|
|
/** Called when modules are replaced. */
|
|
export async function replaceModules(modules: Record<Id, UnloadedModule>, sourceMapId?: SourceMapURL) {
|
|
Object.assign(unloadedModuleRegistry, modules);
|
|
|
|
emitEvent("bun:beforeUpdate", null);
|
|
|
|
type ToAccept = {
|
|
cb: HotAccept;
|
|
key: Id;
|
|
};
|
|
const toReload = new Set<HMRModule>();
|
|
const toAccept: ToAccept[] = [];
|
|
let failures: Set<Id> | null = null;
|
|
const toDispose: HMRModule[] = [];
|
|
|
|
// Discover all HMR boundaries
|
|
outer: for (const key of Object.keys(modules)) {
|
|
// Unref old source maps, and track new ones
|
|
if (side === "client") {
|
|
DEBUG.ASSERT(sourceMapId);
|
|
const existingSourceMapId = registrySourceMapIds.get(key);
|
|
if (existingSourceMapId) derefMapping(existingSourceMapId);
|
|
registrySourceMapIds.set(key, sourceMapId);
|
|
}
|
|
|
|
const existing = registry.get(key);
|
|
if (!existing) continue;
|
|
|
|
toReload.add(existing);
|
|
|
|
// Discover all HMR boundaries
|
|
const visited = new Set<HMRModule>();
|
|
const queue: HMRModule[] = [existing];
|
|
visited.add(existing);
|
|
while (true) {
|
|
const mod = queue.shift();
|
|
if (!mod) break;
|
|
|
|
// Stop propagation if the module is self-accepting
|
|
let hadSelfAccept = true;
|
|
if (mod.selfAccept) {
|
|
toReload.add(mod);
|
|
visited.add(mod);
|
|
hadSelfAccept = false;
|
|
if (mod.onDispose) {
|
|
toDispose.push(mod);
|
|
}
|
|
}
|
|
// Modules that mutate data are implied to handle updates via reusing their `data` property
|
|
else if (Object.keys(mod.data).length > 0) {
|
|
mod.selfAccept ??= implicitAcceptFunction;
|
|
toReload.add(mod);
|
|
visited.add(mod);
|
|
hadSelfAccept = false;
|
|
if (mod.onDispose) {
|
|
toDispose.push(mod);
|
|
}
|
|
}
|
|
|
|
// All importers will be visited
|
|
if (hadSelfAccept && mod.importers.size === 0) {
|
|
failures ??= new Set();
|
|
failures.add(key);
|
|
continue outer;
|
|
}
|
|
|
|
for (const importer of mod.importers) {
|
|
const cb = importer.depAccepts?.[key];
|
|
if (cb) {
|
|
toAccept.push({ cb, key });
|
|
} else if (hadSelfAccept) {
|
|
if (visited.has(importer)) continue;
|
|
visited.add(importer);
|
|
queue.push(importer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If roots were hit, print a nice message before reloading.
|
|
if (failures) {
|
|
let message =
|
|
"[Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:\n\n";
|
|
|
|
// For each failed boundary, re-compute the path to the root.
|
|
for (const boundary of failures) {
|
|
const path: Id[] = [];
|
|
let current = registry.get(boundary)!;
|
|
DEBUG.ASSERT(!boundary.endsWith(".html")); // caller should have already reloaded
|
|
DEBUG.ASSERT(current);
|
|
DEBUG.ASSERT(current.selfAccept === null);
|
|
if (current.importers.size === 0) {
|
|
message += `Module "${boundary}" is a root module that does not self-accept.\n`;
|
|
continue;
|
|
}
|
|
outer: while (current.importers.size > 0) {
|
|
path.push(current.id);
|
|
inner: for (const importer of current.importers) {
|
|
if (importer.selfAccept) continue inner;
|
|
if (importer.depAccepts?.[boundary]) continue inner;
|
|
current = importer;
|
|
continue outer;
|
|
}
|
|
DEBUG.ASSERT(false);
|
|
break;
|
|
}
|
|
path.push(current.id);
|
|
DEBUG.ASSERT(path.length > 0);
|
|
message += `Module "${boundary}" is not accepted by ${path[1]}${path.length > 1 ? "," : "."}\n`;
|
|
for (let i = 2, len = path.length; i < len; i++) {
|
|
const isLast = i === len - 1;
|
|
message += `${isLast ? "└" : "├"} imported by "${path[i]}"${isLast ? "." : ","}\n`;
|
|
}
|
|
}
|
|
message = message.trim();
|
|
if (side === "client") {
|
|
sessionStorage?.setItem?.(
|
|
"bun:hmr:message",
|
|
JSON.stringify?.({
|
|
message,
|
|
kind: "warn",
|
|
}),
|
|
);
|
|
fullReload();
|
|
} else {
|
|
console.warn(message);
|
|
}
|
|
}
|
|
|
|
// Dispose all modules
|
|
if (toDispose.length > 0) {
|
|
const disposePromises: Promise<void>[] = [];
|
|
for (const mod of toDispose) {
|
|
mod.state = State.Stale;
|
|
for (const fn of mod.onDispose!) {
|
|
const p = fn(mod.data);
|
|
if (p && p instanceof Promise) {
|
|
disposePromises.push(p);
|
|
}
|
|
}
|
|
mod.onDispose = null;
|
|
}
|
|
if (disposePromises.length > 0) {
|
|
await Promise.all(disposePromises);
|
|
}
|
|
}
|
|
|
|
// Reload all modules
|
|
const promises: Promise<HMRModule>[] = [];
|
|
for (const mod of toReload) {
|
|
mod.state = State.Stale;
|
|
const selfAccept = mod.selfAccept;
|
|
mod.selfAccept = null;
|
|
mod.depAccepts = null;
|
|
|
|
const modOrPromise = loadModuleAsync(mod.id, false, null);
|
|
if (modOrPromise === mod) {
|
|
if (selfAccept) {
|
|
selfAccept(getEsmExports(mod));
|
|
}
|
|
} else {
|
|
DEBUG.ASSERT(modOrPromise instanceof Promise);
|
|
promises.push(
|
|
(modOrPromise as Promise<HMRModule>).then(mod => {
|
|
if (selfAccept) {
|
|
selfAccept(getEsmExports(mod));
|
|
}
|
|
return mod;
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
if (promises.length > 0) {
|
|
await Promise.all(promises);
|
|
}
|
|
for (const mod of toReload) {
|
|
const { selfAccept } = mod;
|
|
if (selfAccept && selfAccept !== implicitAcceptFunction) continue;
|
|
patchImporters(mod);
|
|
}
|
|
|
|
// Call all accept callbacks
|
|
for (const { cb: cbEntry, key } of toAccept) {
|
|
const { cb: cbFn, modules, single } = cbEntry;
|
|
cbFn(single ? getEsmExports(registry.get(key)!) : createAcceptArray(modules, key));
|
|
}
|
|
|
|
if (refreshRuntime) {
|
|
refreshRuntime.performReactRefresh();
|
|
}
|
|
|
|
emitEvent("bun:afterUpdate", null);
|
|
}
|
|
|
|
function patchImporters(mod: HMRModule) {
|
|
const { importers } = mod;
|
|
const exports = getEsmExports(mod);
|
|
for (const importer of importers) {
|
|
if (!importer.esm || !importer.updateImport) continue;
|
|
const index = importer.imports!.indexOf(mod);
|
|
if (index === -1) continue; // require or dynamic import
|
|
importer.updateImport;
|
|
}
|
|
}
|
|
|
|
function createAcceptArray(modules: string[], key: Id) {
|
|
const arr = new Array(modules.length);
|
|
arr.fill(undefined);
|
|
const i = modules.indexOf(key);
|
|
DEBUG.ASSERT(i !== -1);
|
|
arr[i] = getEsmExports(registry.get(key)!);
|
|
return arr;
|
|
}
|
|
|
|
export function emitEvent(event: HMREvent, data: any) {
|
|
const handlers = eventHandlers[event];
|
|
if (!handlers) return;
|
|
for (const handler of handlers) {
|
|
handler(data);
|
|
}
|
|
}
|
|
|
|
export function onEvent(event: HMREvent, cb) {
|
|
(eventHandlers[event] ??= [])!.push(cb);
|
|
}
|
|
|
|
function throwNotFound(id: Id, isUserDynamic: boolean) {
|
|
if (isUserDynamic) {
|
|
throw new Error(
|
|
`Failed to resolve dynamic import '${id}'. With Bun's bundler, all imports must be statically known at build time so that the bundler can trace everything.`,
|
|
);
|
|
}
|
|
if (IS_BUN_DEVELOPMENT) {
|
|
console.warn("Available modules:", Object.keys(unloadedModuleRegistry));
|
|
}
|
|
throw new Error(
|
|
`Failed to load bundled module '${id}'. This is not a dynamic import, and therefore is a bug in Bun's bundler.`,
|
|
);
|
|
}
|
|
|
|
export function fullReload() {
|
|
try {
|
|
emitEvent("bun:beforeFullReload", null);
|
|
} catch {}
|
|
location.reload();
|
|
}
|
|
|
|
class AsyncImportError extends Error {
|
|
asyncId: string;
|
|
constructor(asyncId: string) {
|
|
super(`Cannot load async module "${asyncId}" synchronously because it uses top-level await.`);
|
|
this.asyncId = asyncId;
|
|
Object.defineProperty(this, "name", { value: "Error" });
|
|
}
|
|
}
|
|
|
|
/** See `runtime.js`'s `__toCommonJS`. This omits the cache. */
|
|
function toCommonJS(from: any) {
|
|
var desc,
|
|
entry = Object.defineProperty({}, "__esModule", { value: true });
|
|
if ((from && typeof from === "object") || typeof from === "function")
|
|
Object.getOwnPropertyNames(from).map(
|
|
key =>
|
|
!Object.prototype.hasOwnProperty.call(entry, key) &&
|
|
Object.defineProperty(entry, key, {
|
|
get: () => from[key],
|
|
enumerable: !(desc = Object.getOwnPropertyDescriptor(from, key)) || desc.enumerable,
|
|
}),
|
|
);
|
|
return entry;
|
|
}
|
|
|
|
function toESM(mod: any) {
|
|
const to = Object.defineProperty(Object.create(null), "default", { value: mod, enumerable: true });
|
|
for (let key of Object.getOwnPropertyNames(mod))
|
|
if (!Object.prototype.hasOwnProperty.call(to, key))
|
|
Object.defineProperty(to, key, {
|
|
get: () => mod[key],
|
|
enumerable: true,
|
|
});
|
|
return to;
|
|
}
|
|
|
|
function registerSynthetic(id: Id, esmExports) {
|
|
const module = new HMRModule(id, false);
|
|
module.exports = esmExports;
|
|
registry.set(id, module);
|
|
unloadedModuleRegistry[id] = true as any;
|
|
}
|
|
|
|
export function setRefreshRuntime(runtime: HMRModule) {
|
|
refreshRuntime = getEsmExports(runtime);
|
|
|
|
if (typeof refreshRuntime.injectIntoGlobalHook === "function") {
|
|
refreshRuntime.injectIntoGlobalHook(window);
|
|
} else {
|
|
console.warn(
|
|
"refreshRuntime.injectIntoGlobalHook is not a function. " +
|
|
"Something is wrong with the React Fast Refresh runtime.",
|
|
);
|
|
}
|
|
}
|
|
|
|
// react-refresh/runtime does not provide this function for us
|
|
// https://github.com/facebook/metro/blob/febdba2383113c88296c61e28e4ef6a7f4939fda/packages/metro/src/lib/polyfills/require.js#L748-L774
|
|
function isReactRefreshBoundary(esmExports): boolean {
|
|
const { isLikelyComponentType } = refreshRuntime;
|
|
if (!isLikelyComponentType) return true;
|
|
if (isLikelyComponentType(esmExports)) {
|
|
return true;
|
|
}
|
|
if (esmExports == null || typeof esmExports !== "object") {
|
|
// Exit if we can't iterate over exports.
|
|
return false;
|
|
}
|
|
let hasExports = false;
|
|
let areAllExportsComponents = true;
|
|
for (const key in esmExports) {
|
|
hasExports = true;
|
|
const desc = Object.getOwnPropertyDescriptor(esmExports, key);
|
|
if (desc && desc.get) {
|
|
// Don't invoke getters as they may have side effects.
|
|
return false;
|
|
}
|
|
const exportValue = esmExports[key];
|
|
if (!isLikelyComponentType(exportValue)) {
|
|
areAllExportsComponents = false;
|
|
}
|
|
}
|
|
return hasExports && areAllExportsComponents;
|
|
}
|
|
|
|
function implicitAcceptFunction() {}
|
|
|
|
declare global {
|
|
interface Error {
|
|
asyncId?: string;
|
|
}
|
|
}
|
|
|
|
// bun:bake/server, bun:bake/client, and bun:wrap are
|
|
// provided by this file instead of the bundler
|
|
registerSynthetic("bun:wrap", {
|
|
__name,
|
|
__legacyDecorateClassTS,
|
|
__legacyDecorateParamTS,
|
|
__legacyMetadataTS,
|
|
__using,
|
|
__callDispose,
|
|
});
|
|
|
|
if (side === "server") {
|
|
registerSynthetic("bun:bake/server", {
|
|
serverManifest,
|
|
ssrManifest,
|
|
actionManifest: null,
|
|
});
|
|
}
|
|
|
|
if (side === "client") {
|
|
registerSynthetic("bun:bake/client", {
|
|
onServerSideReload: cb => (onServerSideReload = cb),
|
|
});
|
|
}
|