Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "88b2f7a2159c913f7dd0d73c0e88d66138cd67ba";
export const WEBKIT_VERSION = "5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
51 changes: 51 additions & 0 deletions test/regression/issue/30493.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// https://github.com/oven-sh/bun/issues/30493
//
// `require()` of an ESM module whose graph contains a diamond dependency
// through a barrel deadlocked (release) / aborted on `ASSERTION FAILED:
// m_status == Status::Fetching` at ModuleRegistryEntry.cpp:254 (debug).
// Regressed by the require(esm) sync-replay path; same root cause as
// #30281 — `moduleRegistryModuleSettled` fired twice for the same entry
// when `hostLoadImportedModule`'s synchronous-replay branch had already
// driven `fetchComplete` inline while a stale normal-queue reaction was
// still pending. Fix: oven-sh/WebKit#225.
//
// This is the dependency-free reduction of #30281; it subsumes the
// react + MUI repro from #30283.

import { expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";

test("require() of ESM with diamond dependency through barrel does not deadlock", async () => {
using dir = tempDir("issue-30493", {
// shared.js: imports a synthetic builtin so its fetch goes through
// the normal microtask queue *before* the require(esm) entry point
// switches to synchronous draining — that ordering is what leaves a
// stale ModuleRegistryModuleSettled reaction queued. `path.posix.sep`
// so the snapshot is platform-stable.
"shared.js": `import path from 'path';\nexport const SHARED = path.posix.sep;\n`,
"barrel.js": `import { SHARED } from './shared.js';\nexport { SHARED };\nexport const BARREL = 'barrel';\n`,
"a.js": `import { SHARED } from './barrel.js';\nexport default function a() { return SHARED; }\n`,
"b.js": `import { BARREL } from './barrel.js';\nexport default function b() { return BARREL; }\n`,
"app.js": `import a from './a.js';\nimport b from './b.js';\nimport { SHARED } from './shared.js';\nexport default { a: a(), b: b(), shared: SHARED };\n`,
"entry.js": `const mod = require('./app.js');\nconsole.log(JSON.stringify(mod.default));\n`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "entry.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
// Before the fix: release builds deadlock indefinitely; debug builds
// abort. Bound the subprocess so the assertions below show a clear
// failure instead of the test itself timing out.
timeout: 10_000,
Comment thread
claude[bot] marked this conversation as resolved.
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`"{"a":"/","b":"barrel","shared":"/"}"`);
// null ⇒ exited on its own; non-null ⇒ killed by the spawn timeout (deadlocked).
expect(proc.signalCode).toBeNull();
expect(exitCode).toBe(0);
}, 30_000);
Loading