diff --git a/src/bun.js/bindings/BunPlugin.cpp b/src/bun.js/bindings/BunPlugin.cpp index f5abe87b3cc..e6f847749ef 100644 --- a/src/bun.js/bindings/BunPlugin.cpp +++ b/src/bun.js/bindings/BunPlugin.cpp @@ -526,6 +526,12 @@ extern "C" JSC_DEFINE_HOST_FUNCTION(JSMock__jsModuleMock, (JSC::JSGlobalObject * return {}; } + JSC::JSValue callbackValue = callframe->argument(1); + if (!callbackValue.isCell() || !callbackValue.isCallable()) { + scope.throwException(lexicalGlobalObject, JSC::createTypeError(lexicalGlobalObject, "mock(module, fn) requires a function"_s)); + return {}; + } + auto resolveSpecifier = [&]() -> void { JSC::SourceOrigin sourceOrigin = callframe->callerSourceOrigin(vm); if (sourceOrigin.isNull()) @@ -581,12 +587,6 @@ extern "C" JSC_DEFINE_HOST_FUNCTION(JSMock__jsModuleMock, (JSC::JSGlobalObject * resolveSpecifier(); RETURN_IF_EXCEPTION(scope, {}); - JSC::JSValue callbackValue = callframe->argument(1); - if (!callbackValue.isCell() || !callbackValue.isCallable()) { - scope.throwException(lexicalGlobalObject, JSC::createTypeError(lexicalGlobalObject, "mock(module, fn) requires a function"_s)); - return {}; - } - JSC::JSObject* callback = callbackValue.getObject(); JSModuleMock* mock = JSModuleMock::create(vm, globalObject->mockModule.mockModuleStructure.getInitializedOnMainThread(globalObject), callback); diff --git a/test/js/bun/test/mock/mock-module-non-string.test.ts b/test/js/bun/test/mock/mock-module-non-string.test.ts index f7137f1ea64..cbbb37aa03c 100644 --- a/test/js/bun/test/mock/mock-module-non-string.test.ts +++ b/test/js/bun/test/mock/mock-module-non-string.test.ts @@ -16,3 +16,20 @@ test("mock.module still works with valid string argument", async () => { const m = await import("mock-module-non-string-test-fixture"); expect(m.default).toBe(42); }); + +test("mock.module throws TypeError without resolving when callback is missing", () => { + // Running the resolver on an arbitrary user-provided string can enter the + // package-manager auto-install code path and crash. When the caller didn't + // pass a callable second argument, we must fail fast before touching the + // resolver. + const specifiers = [ + "function f3() {}", + "function foo(a, b) { return a + b; }", + "() => 1", + "some bogus package name {with braces}", + ]; + for (const specifier of specifiers) { + // @ts-expect-error missing callback on purpose + expect(() => mock.module(specifier)).toThrow("mock(module, fn) requires a function"); + } +});