diff --git a/src/bun.js/bindings/NodeVMSourceTextModule.cpp b/src/bun.js/bindings/NodeVMSourceTextModule.cpp index 0f0b2d8893..13cefe8c91 100644 --- a/src/bun.js/bindings/NodeVMSourceTextModule.cpp +++ b/src/bun.js/bindings/NodeVMSourceTextModule.cpp @@ -364,7 +364,14 @@ JSValue NodeVMSourceTextModule::link(JSGlobalObject* globalObject, JSArray* spec RETURN_IF_EXCEPTION(scope, {}); if (sync == Synchronousness::Async) { - RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("TODO(@heimskr): async SourceTextModule linking"); + // In JavaScriptCore, when a module contains top-level await, the linking phase + // reports as async, but the actual async behavior happens during evaluation, not linking. + // The linking itself can complete synchronously even for modules with top-level await. + // + // This matches Node.js behavior where linking is always synchronous and + // asynchronous behavior only occurs during evaluation. + // + // So we can safely mark the module as linked and proceed. } status(Status::Linked); diff --git a/src/bun.js/bindings/NodeVMSyntheticModule.cpp b/src/bun.js/bindings/NodeVMSyntheticModule.cpp index 3cd49018a1..cb19ca1ed0 100644 --- a/src/bun.js/bindings/NodeVMSyntheticModule.cpp +++ b/src/bun.js/bindings/NodeVMSyntheticModule.cpp @@ -148,7 +148,11 @@ JSValue NodeVMSyntheticModule::link(JSGlobalObject* globalObject, JSArray* speci RETURN_IF_EXCEPTION(scope, {}); if (sync == Synchronousness::Async) { - RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("TODO(@heimskr): async SyntheticModule linking"); + // In JavaScriptCore, when a module contains top-level await, the linking phase + // reports as async, but the actual async behavior happens during evaluation, not linking. + // The linking itself can complete synchronously even for modules with top-level await. + // + // So we can safely mark the module as linked and proceed. } status(Status::Linked); diff --git a/test/regression/issue/22287-vm-sourcetextmodule-top-level-await.test.ts b/test/regression/issue/22287-vm-sourcetextmodule-top-level-await.test.ts new file mode 100644 index 0000000000..2bc40907e7 --- /dev/null +++ b/test/regression/issue/22287-vm-sourcetextmodule-top-level-await.test.ts @@ -0,0 +1,87 @@ +import { test, expect } from "bun:test"; +import vm from "node:vm"; + +test("vm.SourceTextModule should support top-level await", async () => { + const source = ` +await (async () => { + env.log("top-level await works"); +})(); +`; + + const context = vm.createContext({ + env: { + log: (msg: string) => { + // Store the message for testing + context.loggedMessage = msg; + } + }, + loggedMessage: null + }); + + const mainModule = new vm.SourceTextModule(source, { context }); + + expect(mainModule.status).toBe("unlinked"); + + await mainModule.link(async (specifier) => { + throw new Error(`Failed to resolve module: ${specifier}`); + }); + + expect(mainModule.status).toBe("linked"); + + await mainModule.evaluate(); + + expect(mainModule.status).toBe("evaluated"); + expect(context.loggedMessage).toBe("top-level await works"); +}); + +test("vm.SourceTextModule basic top-level await functionality", async () => { + const source = ` +await (async () => { + env.result = "success"; +})(); +`; + + const context = vm.createContext({ + env: { + result: "" + } + }); + + const mainModule = new vm.SourceTextModule(source, { context }); + + await mainModule.link(async (specifier) => { + throw new Error(`Failed to resolve module: ${specifier}`); + }); + + await mainModule.evaluate(); + + expect(mainModule.status).toBe("evaluated"); + expect(context.env.result).toBe("success"); +}); + +test("vm.SourceTextModule should handle top-level await with error", async () => { + const source = ` +await (async () => { + throw new Error("async error"); +})(); +`; + + const context = vm.createContext({}); + const mainModule = new vm.SourceTextModule(source, { context }); + + await mainModule.link(async (specifier) => { + throw new Error(`Failed to resolve module: ${specifier}`); + }); + + let errorThrown = false; + try { + await mainModule.evaluate(); + } catch (error) { + errorThrown = true; + expect(error.message).toBe("async error"); + } + + expect(errorThrown).toBe(true); + // Note: The module status behavior for async errors might vary between implementations + // In some cases, the module might remain "evaluated" even if the promise rejects +}); \ No newline at end of file