From 5c8af2b4051c69b9b2ae569810dc7b1c7fad04e5 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 05:14:54 +0000 Subject: [PATCH 01/14] Test replacing a module while its load or evaluation is in flight --- test/js/bun/plugin/plugins.test.ts | 93 +++++++++++++++++++++++ test/js/bun/test/mock/mock-module.test.ts | 43 +++++++++++ 2 files changed, 136 insertions(+) diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index 80283bc38fec..bc596af0061d 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -1064,3 +1064,96 @@ it("object loader: an error thrown by a getter on the exports object rejects the }); expect(() => require("object-loader-throwing-esmodule")).toThrow(boom); }); + +it("build.module() of a module whose import() is still loading its dependencies", async () => { + using dir = tempDir("plugin-module-import-in-flight", { + "a.ts": `import "./dependency"; export const from = "file";`, + "dependency.ts": `export {};`, + "entry.ts": ` + const dependencyRequested = Promise.withResolvers(); + const dependencyMayLoad = Promise.withResolvers(); + Bun.plugin({ + name: "hold the dependency's load open", + setup(build) { + build.onLoad({ filter: /dependency\\.ts$/ }, async () => { + dependencyRequested.resolve(); + await dependencyMayLoad.promise; + return { contents: "export {}", loader: "ts" }; + }); + }, + }); + + const a = import.meta.dir + "/a.ts"; + const inFlight = import(a); + await dependencyRequested.promise; + Bun.plugin({ + name: "replace a.ts", + setup(build) { + build.module(a, () => ({ exports: { from: "build.module()" }, loader: "object" })); + }, + }); + dependencyMayLoad.resolve(); + + console.log("in flight:", (await inFlight).from); + console.log("next:", (await import(a)).from); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "in flight: file\nnext: build.module()\n", + stderr: "", + exitCode: 0, + }); +}); + +it.each(["import", "require"])( + "a module that build.module() replaced while it was evaluating does not leave its error on the replacement (%s)", + async how => { + using dir = tempDir("plugin-module-replaced-while-evaluating", { + "replaces-itself.mjs": ` + Bun.plugin({ + name: "replace this module", + setup(build) { + build.module(import.meta.path, () => ({ exports: { from: "build.module()" }, loader: "object" })); + }, + }); + console.log("while evaluating:", (${how === "import" ? "await import" : "require"}(import.meta.path)).from); + throw new Error("the replaced module threw"); + `, + "import.mjs": ` + const path = import.meta.dir + "/replaces-itself.mjs"; + console.log("first:", await import(path).then(module => module.from, error => error.message)); + console.log("next:", await import(path).then(module => module.from, error => error.message)); + `, + "require.cjs": ` + const path = __dirname + "/replaces-itself.mjs"; + try { + console.log("first:", require(path).from); + } catch (error) { + console.log("first:", error.message); + } + import(path).then(module => console.log("next:", module.from), error => console.log("next:", error.message)); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), how === "import" ? "import.mjs" : "require.cjs"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "while evaluating: build.module()\nfirst: the replaced module threw\nnext: build.module()\n", + stderr: "", + exitCode: 0, + }); + }, +); diff --git a/test/js/bun/test/mock/mock-module.test.ts b/test/js/bun/test/mock/mock-module.test.ts index 53af9358a204..3a08189e1932 100644 --- a/test/js/bun/test/mock/mock-module.test.ts +++ b/test/js/bun/test/mock/mock-module.test.ts @@ -497,3 +497,46 @@ test.concurrent( expect(exitCode).toBe(0); }, ); + +test("mock.module() of a module whose import() is still loading its dependencies", async () => { + using dir = tempDir("mock-module-import-in-flight", { + "a.ts": `import "./dependency"; export const a = "real-a";`, + "dependency.ts": `export {};`, + "in-flight.test.ts": ` + import { expect, mock, test } from "bun:test"; + + test("the import in flight gets the module it was loading, the next one gets the mock", async () => { + const dependencyRequested = Promise.withResolvers(); + const dependencyMayLoad = Promise.withResolvers(); + Bun.plugin({ + name: "hold the dependency's load open", + setup(build) { + build.onLoad({ filter: /dependency\\.ts$/ }, async () => { + dependencyRequested.resolve(); + await dependencyMayLoad.promise; + return { contents: "export {}", loader: "ts" }; + }); + }, + }); + + const inFlight = import("./a"); + await dependencyRequested.promise; + mock.module("./a", () => ({ a: "mocked-a" })); + dependencyMayLoad.resolve(); + + expect((await inFlight).a).toBe("real-a"); + expect((await import("./a")).a).toBe("mocked-a"); + }); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "./in-flight.test.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain(" 1 pass"); + expect(exitCode).toBe(0); +}); From f8fd616202786ee5277894fbfd8781b091eeaae0 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 06:12:02 +0000 Subject: [PATCH 02/14] Test a module replaced right after its import() and concurrent import()s sharing one onLoad --- src/jsc/bindings/ModuleLoader.cpp | 15 ------- test/js/bun/plugin/plugins.test.ts | 69 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/jsc/bindings/ModuleLoader.cpp b/src/jsc/bindings/ModuleLoader.cpp index aa2ecaefb495..9d13caae6041 100644 --- a/src/jsc/bindings/ModuleLoader.cpp +++ b/src/jsc/bindings/ModuleLoader.cpp @@ -491,21 +491,6 @@ extern "C" void Bun__onFulfillAsyncModule( auto* specifierValue = Bun::toJS(globalObject, *specifier); RETURN_IF_EXCEPTION(scope, ); - // The new C++ module loader does not create a registry entry until *after* - // this fetch promise resolves (provideFetch runs inside the - // ModuleLoadTopSettled microtask). Two concurrent dynamic imports of the - // same key therefore each get their own embedder fetch promise, and the - // loser of that race must still resolve so its loadModule chain can reach - // the (idempotent) provideFetch and reuse the already-loaded record. - // The old #6946/#12910 short-circuit was for the JS loader's *shared* - // entry.fetch promise; under the new loader returning here would strand - // the loser's promise pending forever. - // - // FIXME(module-loader): the loser still re-transpiled the file. The right - // fix is for JSModuleLoader::loadModule to ensureRegistered() *before* - // calling fetch so concurrent importers share the entry's fetchPromise - // instead of each round-tripping through the embedder. - if (res->result.value.isCommonJSModule) { auto created = Bun::createCommonJSModule(globalObject, graph, specifierValue, res->result.value); EXCEPTION_ASSERT(created.has_value() == !scope.exception()); diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index bc596af0061d..58ac6fe050ab 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -1157,3 +1157,72 @@ it.each(["import", "require"])( }); }, ); + +it.each([ + ["that was loaded before", `await import(a);`], + ["whose fetch has not finished", ``], +])("build.module() right after an import() of a module %s", async (_, before) => { + using dir = tempDir("plugin-module-right-after-import", { + "a.ts": `globalThis.evaluations = (globalThis.evaluations ?? 0) + 1; export const from = "file";`, + "entry.ts": ` + const a = import.meta.dir + "/a.ts"; + ${before} + const started = import(a); + Bun.plugin({ + name: "replace a.ts", + setup(build) { + build.module(a, () => ({ exports: { from: "build.module()" }, loader: "object" })); + }, + }); + console.log("started before:", (await started).from); + console.log("next:", (await import(a)).from); + console.log("a.ts evaluated:", globalThis.evaluations); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "started before: file\nnext: build.module()\na.ts evaluated: 1\n", + stderr: "", + exitCode: 0, + }); +}); + +it("concurrent import()s of a module share one onLoad", async () => { + using dir = tempDir("plugin-onload-concurrent-imports", { + "a.ts": `export {};`, + "entry.ts": ` + let loads = 0; + Bun.plugin({ + name: "count loads", + setup(build) { + build.onLoad({ filter: /a\\.ts$/ }, () => { + loads++; + return { contents: "export {}", loader: "ts" }; + }); + }, + }); + const [first, second, third] = await Promise.all([import("./a"), import("./a"), import("./a")]); + console.log({ sameModule: first === second && second === third, loads }); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "{\n sameModule: true,\n loads: 1,\n}\n", + stderr: "", + exitCode: 0, + }); +}); From 395cb3d3b8c235f12e43ebe1e7a5ee1891e89f92 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 10:40:31 +0000 Subject: [PATCH 03/14] Test that a module that failed to build as a static import is fetched again by the next importer --- test/js/bun/resolve/build-error.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/js/bun/resolve/build-error.test.ts b/test/js/bun/resolve/build-error.test.ts index 18e4d00a6b2a..ee8408cf6f0a 100644 --- a/test/js/bun/resolve/build-error.test.ts +++ b/test/js/bun/resolve/build-error.test.ts @@ -316,3 +316,28 @@ test("BuildMessage finalize frees with the same allocator it was created with", Bun.gc(true); } }); + +test("a module that failed to build as a static import is fetched again by the next importer", async () => { + using dir = tempDir("build-error-static-retry", { + "child.mjs": "export const value = ;", + "before.mjs": `export { value } from "./child.mjs";`, + "after.mjs": `export { value } from "./child.mjs"; // another importer`, + "main.mjs": ` + import { writeFileSync } from "node:fs"; + const outcome = path => import(path).then(m => "value " + m.value, e => "failed"); + console.log("broken:", await outcome("./before.mjs")); + writeFileSync(import.meta.dir + "/child.mjs", "export const value = 1;"); + console.log("fixed:", await outcome("./after.mjs")); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.mjs"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr }).toEqual({ stdout: "broken: failed\nfixed: value 1\n", stderr: "" }); + expect(exitCode).toBe(0); +}); From f440486995f87e15713e725730703a95003f2144 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 18:52:41 +0000 Subject: [PATCH 04/14] Bump WebKit to the preview build of oven-sh/WebKit#748 --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 47dedb61e337..eca99242e59e 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "f20ce7744553c910bcf16a33faf976af208de091"; +export const WEBKIT_VERSION = "autobuild-preview-pr-748-eff6b5f5"; /** * WebKit (JavaScriptCore) — the JS engine. From 6bfdd9d5d5b5739b5ca930edb02d312ce39878f3 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 19:44:41 +0000 Subject: [PATCH 05/14] Keep the tests of the crash, add one for a hot reload, and go back to main's WebKit until the new preview is built --- scripts/build/deps/webkit.ts | 2 +- src/jsc/bindings/ModuleLoader.cpp | 15 ++++ test/cli/hot/hot.test.ts | 48 +++++++++- test/js/bun/plugin/plugins.test.ts | 114 ------------------------ test/js/bun/resolve/build-error.test.ts | 25 ------ 5 files changed, 63 insertions(+), 141 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index eca99242e59e..47dedb61e337 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-748-eff6b5f5"; +export const WEBKIT_VERSION = "f20ce7744553c910bcf16a33faf976af208de091"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/jsc/bindings/ModuleLoader.cpp b/src/jsc/bindings/ModuleLoader.cpp index 9d13caae6041..aa2ecaefb495 100644 --- a/src/jsc/bindings/ModuleLoader.cpp +++ b/src/jsc/bindings/ModuleLoader.cpp @@ -491,6 +491,21 @@ extern "C" void Bun__onFulfillAsyncModule( auto* specifierValue = Bun::toJS(globalObject, *specifier); RETURN_IF_EXCEPTION(scope, ); + // The new C++ module loader does not create a registry entry until *after* + // this fetch promise resolves (provideFetch runs inside the + // ModuleLoadTopSettled microtask). Two concurrent dynamic imports of the + // same key therefore each get their own embedder fetch promise, and the + // loser of that race must still resolve so its loadModule chain can reach + // the (idempotent) provideFetch and reuse the already-loaded record. + // The old #6946/#12910 short-circuit was for the JS loader's *shared* + // entry.fetch promise; under the new loader returning here would strand + // the loser's promise pending forever. + // + // FIXME(module-loader): the loser still re-transpiled the file. The right + // fix is for JSModuleLoader::loadModule to ensureRegistered() *before* + // calling fetch so concurrent importers share the entry's fetchPromise + // instead of each round-tripping through the embedder. + if (res->result.value.isCommonJSModule) { auto created = Bun::createCommonJSModule(globalObject, graph, specifierValue, res->result.value); EXCEPTION_ASSERT(created.has_value() == !scope.exception()); diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 83d61eb602f3..b7e0414ce1bc 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -1,7 +1,7 @@ import { spawn } from "bun"; import { beforeEach, expect, it } from "bun:test"; import { copyFileSync, cpSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, isDebug, isWindows, tmpdirSync, waitForFileToExist } from "harness"; +import { bunEnv, bunExe, isDebug, isWindows, tempDir, tmpdirSync, waitForFileToExist } from "harness"; import { join } from "path"; const timeout = isDebug ? Infinity : 10_000; @@ -776,3 +776,49 @@ ${Buffer.alloc(counter * 2, " ").toString()}throw new Error(${counter});`, }, longTimeout, ); + +it( + "should import a module again after a hot reload while its import() was still loading its dependencies", + async () => { + using dir = tempDir("hot-reload-import-in-flight", { + "a.mjs": `import "./dependency.mjs"; export const from = "a.mjs";`, + "dependency.mjs": `export {};`, + "entry.mjs": ` + import { readFileSync, writeFileSync } from "node:fs"; + globalThis.runs = (globalThis.runs ?? 0) + 1; + if (globalThis.runs === 1) { + Bun.plugin({ + name: "hold the dependency's load open until the reload", + setup(build) { + build.onLoad({ filter: /dependency\\.mjs$/ }, () => { + const loaded = { contents: "export {}", loader: "js" }; + if (globalThis.dependencyMayLoad) return loaded; + const { promise, resolve } = Promise.withResolvers(); + globalThis.dependencyMayLoad = () => resolve(loaded); + writeFileSync(import.meta.path, readFileSync(import.meta.path)); + return promise; + }); + }, + }); + globalThis.inFlight = import("./a.mjs"); + } else { + globalThis.dependencyMayLoad(); + console.log("in flight:", (await globalThis.inFlight).from); + console.log("next:", (await import("./a.mjs")).from); + process.exit(0); + } + `, + }); + await using proc = spawn({ + cmd: [bunExe(), "--hot", "entry.mjs"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "inherit", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout).toBe("in flight: a.mjs\nnext: a.mjs\n"); + expect(exitCode).toBe(0); + }, + timeout, +); diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index 58ac6fe050ab..6dabc2da0bb1 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -1112,117 +1112,3 @@ it("build.module() of a module whose import() is still loading its dependencies" exitCode: 0, }); }); - -it.each(["import", "require"])( - "a module that build.module() replaced while it was evaluating does not leave its error on the replacement (%s)", - async how => { - using dir = tempDir("plugin-module-replaced-while-evaluating", { - "replaces-itself.mjs": ` - Bun.plugin({ - name: "replace this module", - setup(build) { - build.module(import.meta.path, () => ({ exports: { from: "build.module()" }, loader: "object" })); - }, - }); - console.log("while evaluating:", (${how === "import" ? "await import" : "require"}(import.meta.path)).from); - throw new Error("the replaced module threw"); - `, - "import.mjs": ` - const path = import.meta.dir + "/replaces-itself.mjs"; - console.log("first:", await import(path).then(module => module.from, error => error.message)); - console.log("next:", await import(path).then(module => module.from, error => error.message)); - `, - "require.cjs": ` - const path = __dirname + "/replaces-itself.mjs"; - try { - console.log("first:", require(path).from); - } catch (error) { - console.log("first:", error.message); - } - import(path).then(module => console.log("next:", module.from), error => console.log("next:", error.message)); - `, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), how === "import" ? "import.mjs" : "require.cjs"], - cwd: String(dir), - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout, stderr, exitCode }).toEqual({ - stdout: "while evaluating: build.module()\nfirst: the replaced module threw\nnext: build.module()\n", - stderr: "", - exitCode: 0, - }); - }, -); - -it.each([ - ["that was loaded before", `await import(a);`], - ["whose fetch has not finished", ``], -])("build.module() right after an import() of a module %s", async (_, before) => { - using dir = tempDir("plugin-module-right-after-import", { - "a.ts": `globalThis.evaluations = (globalThis.evaluations ?? 0) + 1; export const from = "file";`, - "entry.ts": ` - const a = import.meta.dir + "/a.ts"; - ${before} - const started = import(a); - Bun.plugin({ - name: "replace a.ts", - setup(build) { - build.module(a, () => ({ exports: { from: "build.module()" }, loader: "object" })); - }, - }); - console.log("started before:", (await started).from); - console.log("next:", (await import(a)).from); - console.log("a.ts evaluated:", globalThis.evaluations); - `, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "entry.ts"], - cwd: String(dir), - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout, stderr, exitCode }).toEqual({ - stdout: "started before: file\nnext: build.module()\na.ts evaluated: 1\n", - stderr: "", - exitCode: 0, - }); -}); - -it("concurrent import()s of a module share one onLoad", async () => { - using dir = tempDir("plugin-onload-concurrent-imports", { - "a.ts": `export {};`, - "entry.ts": ` - let loads = 0; - Bun.plugin({ - name: "count loads", - setup(build) { - build.onLoad({ filter: /a\\.ts$/ }, () => { - loads++; - return { contents: "export {}", loader: "ts" }; - }); - }, - }); - const [first, second, third] = await Promise.all([import("./a"), import("./a"), import("./a")]); - console.log({ sameModule: first === second && second === third, loads }); - `, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "entry.ts"], - cwd: String(dir), - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout, stderr, exitCode }).toEqual({ - stdout: "{\n sameModule: true,\n loads: 1,\n}\n", - stderr: "", - exitCode: 0, - }); -}); diff --git a/test/js/bun/resolve/build-error.test.ts b/test/js/bun/resolve/build-error.test.ts index ee8408cf6f0a..18e4d00a6b2a 100644 --- a/test/js/bun/resolve/build-error.test.ts +++ b/test/js/bun/resolve/build-error.test.ts @@ -316,28 +316,3 @@ test("BuildMessage finalize frees with the same allocator it was created with", Bun.gc(true); } }); - -test("a module that failed to build as a static import is fetched again by the next importer", async () => { - using dir = tempDir("build-error-static-retry", { - "child.mjs": "export const value = ;", - "before.mjs": `export { value } from "./child.mjs";`, - "after.mjs": `export { value } from "./child.mjs"; // another importer`, - "main.mjs": ` - import { writeFileSync } from "node:fs"; - const outcome = path => import(path).then(m => "value " + m.value, e => "failed"); - console.log("broken:", await outcome("./before.mjs")); - writeFileSync(import.meta.dir + "/child.mjs", "export const value = 1;"); - console.log("fixed:", await outcome("./after.mjs")); - `, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "main.mjs"], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout, stderr }).toEqual({ stdout: "broken: failed\nfixed: value 1\n", stderr: "" }); - expect(exitCode).toBe(0); -}); From 2e3695da74770a1b3eaffc8475850e05d99bdb2d Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 19:46:33 +0000 Subject: [PATCH 06/14] Run the two subprocess tests concurrently and name the module by its resolved path on Windows --- test/js/bun/plugin/plugins.test.ts | 5 +++-- test/js/bun/test/mock/mock-module.test.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index 6dabc2da0bb1..640deb043ce7 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -1065,11 +1065,12 @@ it("object loader: an error thrown by a getter on the exports object rejects the expect(() => require("object-loader-throwing-esmodule")).toThrow(boom); }); -it("build.module() of a module whose import() is still loading its dependencies", async () => { +it.concurrent("build.module() of a module whose import() is still loading its dependencies", async () => { using dir = tempDir("plugin-module-import-in-flight", { "a.ts": `import "./dependency"; export const from = "file";`, "dependency.ts": `export {};`, "entry.ts": ` + import { join } from "node:path"; const dependencyRequested = Promise.withResolvers(); const dependencyMayLoad = Promise.withResolvers(); Bun.plugin({ @@ -1083,7 +1084,7 @@ it("build.module() of a module whose import() is still loading its dependencies" }, }); - const a = import.meta.dir + "/a.ts"; + const a = join(import.meta.dir, "a.ts"); const inFlight = import(a); await dependencyRequested.promise; Bun.plugin({ diff --git a/test/js/bun/test/mock/mock-module.test.ts b/test/js/bun/test/mock/mock-module.test.ts index 3a08189e1932..42dc5b227826 100644 --- a/test/js/bun/test/mock/mock-module.test.ts +++ b/test/js/bun/test/mock/mock-module.test.ts @@ -498,7 +498,7 @@ test.concurrent( }, ); -test("mock.module() of a module whose import() is still loading its dependencies", async () => { +test.concurrent("mock.module() of a module whose import() is still loading its dependencies", async () => { using dir = tempDir("mock-module-import-in-flight", { "a.ts": `import "./dependency"; export const a = "real-a";`, "dependency.ts": `export {};`, From 667f348b6422fe2f50df657c0641fba405f2b347 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 20:37:15 +0000 Subject: [PATCH 07/14] Bump WebKit to the preview build of oven-sh/WebKit#748 --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 47dedb61e337..1a84b4770fde 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "f20ce7744553c910bcf16a33faf976af208de091"; +export const WEBKIT_VERSION = "autobuild-preview-pr-748-e0e02de1"; /** * WebKit (JavaScriptCore) — the JS engine. From 52f3fef3417be913e3f2a2f423f3bf417e28a182 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 21:36:30 +0000 Subject: [PATCH 08/14] Bump WebKit to the preview build of the current oven-sh/WebKit#748 --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 1a84b4770fde..5b9bd759430b 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-748-e0e02de1"; +export const WEBKIT_VERSION = "autobuild-preview-pr-748-c4c46633"; /** * WebKit (JavaScriptCore) — the JS engine. From e57027bbe527c29cfb3a4ac94b29ed9db48f417f Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 21:49:01 +0000 Subject: [PATCH 09/14] Test import() after delete require.cache of a module that onResolve redirected a resolved path to --- test/js/bun/plugin/plugins.test.ts | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index 640deb043ce7..dc63e7311bf4 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -1113,3 +1113,43 @@ it.concurrent("build.module() of a module whose import() is still loading its de exitCode: 0, }); }); + +it.concurrent( + "import() after delete require.cache of a module that onResolve redirected a resolved path to", + async () => { + using dir = tempDir("plugin-onresolve-chain-removed", { + "a.mjs": `export const from = "a.mjs";`, + "b.mjs": `export const from = "b.mjs";`, + "c.mjs": `export const from = "c.mjs";`, + "d.mjs": `export const from = "d.mjs";`, + "entry.ts": ` + import { basename, join } from "node:path"; + const next = { "a.mjs": "b.mjs", "b.mjs": "c.mjs", "c.mjs": "d.mjs" }; + Bun.plugin({ + name: "redirect a path that is already resolved, again and again", + setup(build) { + build.onResolve({ filter: /[abc]\\.mjs$/ }, ({ path }) => ({ path: join(import.meta.dir, next[basename(path)]) })); + }, + }); + + const a = join(import.meta.dir, "a.mjs"); + console.log("first:", (await import(a)).from); + console.log("deleted:", delete require.cache[join(import.meta.dir, "d.mjs")]); + console.log("again:", (await import(a)).from); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "first: d.mjs\ndeleted: true\nagain: d.mjs\n", + stderr: "", + exitCode: 0, + }); + }, +); From 17d21f9706213c41613da8bc9396534a9b7e8153 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 21:57:10 +0000 Subject: [PATCH 10/14] Bump WebKit to the commit oven-sh/WebKit#748 merged as --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 5b9bd759430b..de4a51799a88 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-748-c4c46633"; +export const WEBKIT_VERSION = "0e8e9c238e91b60e944b4a33c1a596bdcb0e29b9"; /** * WebKit (JavaScriptCore) — the JS engine. From e311fe6a7d61adff78bfa1695abf05275add7264 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 22:25:35 +0000 Subject: [PATCH 11/14] The hot reload and redirect tests check that the module imported again was evaluated again --- test/cli/hot/hot.test.ts | 8 ++++---- test/js/bun/plugin/plugins.test.ts | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index b7e0414ce1bc..0f4c173eaec2 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -781,7 +781,7 @@ it( "should import a module again after a hot reload while its import() was still loading its dependencies", async () => { using dir = tempDir("hot-reload-import-in-flight", { - "a.mjs": `import "./dependency.mjs"; export const from = "a.mjs";`, + "a.mjs": `import "./dependency.mjs"; export const evaluation = (globalThis.evaluations = (globalThis.evaluations ?? 0) + 1);`, "dependency.mjs": `export {};`, "entry.mjs": ` import { readFileSync, writeFileSync } from "node:fs"; @@ -803,8 +803,8 @@ it( globalThis.inFlight = import("./a.mjs"); } else { globalThis.dependencyMayLoad(); - console.log("in flight:", (await globalThis.inFlight).from); - console.log("next:", (await import("./a.mjs")).from); + console.log("in flight: evaluation", (await globalThis.inFlight).evaluation); + console.log("next: evaluation", (await import("./a.mjs")).evaluation); process.exit(0); } `, @@ -817,7 +817,7 @@ it( stderr: "inherit", }); const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); - expect(stdout).toBe("in flight: a.mjs\nnext: a.mjs\n"); + expect(stdout).toBe("in flight: evaluation 1\nnext: evaluation 2\n"); expect(exitCode).toBe(0); }, timeout, diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index dc63e7311bf4..70fd04dc8fcd 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -1114,6 +1114,8 @@ it.concurrent("build.module() of a module whose import() is still loading its de }); }); +// The loader resolves a path that import() has resolved twice more, so onResolve is fed its own results: a → b → c → d. +// That leaves d.mjs registered under a key other than the one it was asked for by, which is what this is about. it.concurrent( "import() after delete require.cache of a module that onResolve redirected a resolved path to", async () => { @@ -1121,7 +1123,7 @@ it.concurrent( "a.mjs": `export const from = "a.mjs";`, "b.mjs": `export const from = "b.mjs";`, "c.mjs": `export const from = "c.mjs";`, - "d.mjs": `export const from = "d.mjs";`, + "d.mjs": `export const from = "d.mjs, evaluation " + (globalThis.evaluations = (globalThis.evaluations ?? 0) + 1);`, "entry.ts": ` import { basename, join } from "node:path"; const next = { "a.mjs": "b.mjs", "b.mjs": "c.mjs", "c.mjs": "d.mjs" }; @@ -1147,7 +1149,7 @@ it.concurrent( }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect({ stdout, stderr, exitCode }).toEqual({ - stdout: "first: d.mjs\ndeleted: true\nagain: d.mjs\n", + stdout: "first: d.mjs, evaluation 1\ndeleted: true\nagain: d.mjs, evaluation 2\n", stderr: "", exitCode: 0, }); From 9636305d623ad1789c70f9b41b0684a3ea0b6394 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 22:50:25 +0000 Subject: [PATCH 12/14] The hot reload test exits with the error if an import rejects --- test/cli/hot/hot.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 0f4c173eaec2..a6224b80bc3a 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -803,9 +803,15 @@ it( globalThis.inFlight = import("./a.mjs"); } else { globalThis.dependencyMayLoad(); - console.log("in flight: evaluation", (await globalThis.inFlight).evaluation); - console.log("next: evaluation", (await import("./a.mjs")).evaluation); - process.exit(0); + try { + console.log("in flight: evaluation", (await globalThis.inFlight).evaluation); + console.log("next: evaluation", (await import("./a.mjs")).evaluation); + process.exit(0); + } catch (error) { + // --hot would keep the process alive after an uncaught error. + console.log("rejected:", error); + process.exit(1); + } } `, }); From c787a5f0fb0ac63939b60dd9e74398c7677fb11d Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 22:55:40 +0000 Subject: [PATCH 13/14] sql: say what a statement run inside a transaction resolves to, which fixes the type error in sql.begin() --- src/js/bun/sql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/bun/sql.ts b/src/js/bun/sql.ts index 744b2488c011..64bf08c13cb8 100644 --- a/src/js/bun/sql.ts +++ b/src/js/bun/sql.ts @@ -244,7 +244,7 @@ const SQL = function SQL( if ((values?.length ?? 0) === 0) { flags |= SQLQueryFlags.simple; } - const query = new Query( + const query = new Query, any>( strings, values, flags, From 0c8a9394365cc670b6ba67ec44251b6809648706 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 30 Sep 2026 22:55:40 +0000 Subject: [PATCH 14/14] Bump WebKit to fb1167ebf2cb --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index de4a51799a88..c6e0193955dc 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "0e8e9c238e91b60e944b4a33c1a596bdcb0e29b9"; +export const WEBKIT_VERSION = "fb1167ebf2cb9edc1f6771a2c11771b024693ae0"; /** * WebKit (JavaScriptCore) — the JS engine.