diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 47dedb61e337..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 = "f20ce7744553c910bcf16a33faf976af208de091"; +export const WEBKIT_VERSION = "fb1167ebf2cb9edc1f6771a2c11771b024693ae0"; /** * WebKit (JavaScriptCore) — the JS engine. 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, diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 83d61eb602f3..a6224b80bc3a 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,55 @@ ${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 evaluation = (globalThis.evaluations = (globalThis.evaluations ?? 0) + 1);`, + "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(); + 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); + } + } + `, + }); + 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: 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 80283bc38fec..70fd04dc8fcd 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -1064,3 +1064,94 @@ it("object loader: an error thrown by a getter on the exports object rejects the }); expect(() => require("object-loader-throwing-esmodule")).toThrow(boom); }); + +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({ + 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 = join(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, + }); +}); + +// 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 () => { + 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, 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" }; + 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, evaluation 1\ndeleted: true\nagain: d.mjs, evaluation 2\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..42dc5b227826 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.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 {};`, + "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); +});