Skip to content
Closed
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
22 changes: 15 additions & 7 deletions src/jsc/bindings/BunPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -659,15 +659,23 @@ extern "C" JSC_DEFINE_HOST_FUNCTION(JSMock__jsModuleMock, (JSC::JSGlobalObject *
JSObject::getOwnPropertyNames(object, globalObject, names, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(scope, {});

JSC::MarkedArgumentBuffer values;
values.ensureCapacity(names.size());
if (values.hasOverflowed()) [[unlikely]] {
JSC::throwOutOfMemoryError(globalObject, scope);
return {};
}
for (auto& name : names) {
// consistent with regular esm handling code
auto topExceptionScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
JSValue value = object->get(globalObject, name);
if (scope.exception()) [[unlikely]] {
(void)scope.tryClearException();
value = jsUndefined();
}
moduleNamespaceObject->overrideExportValue(globalObject, name, value);
RETURN_IF_EXCEPTION(scope, {});
Comment thread
robobun marked this conversation as resolved.
values.append(value);
}
if (values.hasOverflowed()) [[unlikely]] {
JSC::throwOutOfMemoryError(globalObject, scope);
return {};
}
for (size_t i = 0; i < names.size(); ++i) {
moduleNamespaceObject->overrideExportValue(globalObject, names[i], values.at(i));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
RETURN_IF_EXCEPTION(scope, {});
}
Comment thread
robobun marked this conversation as resolved.

Expand Down
6 changes: 1 addition & 5 deletions src/jsc/modules/ObjectModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,8 @@ generateObjectModuleSourceCode(JSC::JSGlobalObject* globalObject,
for (auto& entry : properties.releaseData()->propertyNameVector()) {
exportNames.append(entry);

auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
JSValue value = object->get(globalObject, entry);
if (scope.exception()) [[unlikely]] {
(void)scope.tryClearException();
value = jsUndefined();
}
RETURN_IF_EXCEPTION(throwScope, void());
exportValues.append(value);
}
};
Expand Down
42 changes: 42 additions & 0 deletions test/js/bun/plugin/plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,3 +722,45 @@ it.concurrent("a no-op onResolve that returns args.path unchanged is transparent
expect(stdout.trim() || stderr).toBe("entry ran:dep");
expect(exitCode).toBe(0);
});

it.concurrent("loader: 'object' propagates exceptions from export property getters", async () => {
const script = `
const sentinel = new Error("GETTER-THROW");
const exportsObj = { before: 1 };
Object.defineProperty(exportsObj, "boom", { enumerable: true, get() { throw sentinel; } });
exportsObj.after = 2;

Bun.plugin({
name: "throwing-getter",
setup(build) {
build.module("throwing-getter:esm", () => ({ exports: exportsObj, loader: "object" }));
build.module("throwing-getter:cjs", () => ({ exports: exportsObj, loader: "object" }));
},
});

async function attempt(label, fn) {
try {
const ns = await fn();
return label + " resolved boom=" + ns.boom;
} catch (e) {
return label + " rejected " + (e === sentinel) + " " + e.message;
}
}

console.log(await attempt("import", () => import("throwing-getter:esm")));
console.log(await attempt("require", () => require("throwing-getter:cjs")));
`;

await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const lines = stdout.trim() ? stdout.trim().split("\n") : [stderr];
expect({ lines, exitCode }).toEqual({
lines: ["import rejected true GETTER-THROW", "require rejected true GETTER-THROW"],
exitCode: 0,
});
});
2 changes: 2 additions & 0 deletions test/js/bun/test/mock/mock-module-getter-throw-fixture.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions test/js/bun/test/mock/mock-module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { expect, mock, spyOn, test } from "bun:test";
import { default as defaultValue, fn, iCallFn, rexported, rexportedAs, variable } from "./mock-module-fixture";
import * as getterThrowFixture from "./mock-module-getter-throw-fixture";
import * as spyFixture from "./spymodule-fixture";

test("mock.module async", async () => {
Expand Down Expand Up @@ -156,6 +157,23 @@ test("mocking a package", async () => {
expect(require("ha-ha-ha").wow()).toBe(43);
});

test("mock.module propagates a throwing getter on the factory's return value", () => {
expect({ a: getterThrowFixture.a, b: getterThrowFixture.b }).toEqual({ a: "original-a", b: "original-b" });

const sentinel = new Error("GETTER-THROW");
const mockExports: { a: string; b: string } = { a: "mock-a" } as any;
Object.defineProperty(mockExports, "b", {
enumerable: true,
get: () => {
throw sentinel;
},
});

expect(() => mock.module("./mock-module-getter-throw-fixture", () => mockExports)).toThrow(sentinel);
// All values are read before any override is written, so a throwing getter leaves the namespace untouched.
expect({ a: getterThrowFixture.a, b: getterThrowFixture.b }).toEqual({ a: "original-a", b: "original-b" });
});

test("mocking a builtin", async () => {
mock.module("fs/promises", () => {
return {
Expand Down
Loading