From a9c2360c03a3966f30d9df5162e907e4980afe8a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 15:07:13 +0000 Subject: [PATCH 1/4] fix(plugin): propagate exception from Bun.plugin target string coercion When the `target` option passed to Bun.plugin() is an object whose toString() throws, the pending exception was ignored and execution continued into the setup() call, tripping ExceptionScope::assertNoException in debug builds. Check for the exception after coercing `target` to a string and propagate it. --- src/jsc/bindings/BunPlugin.cpp | 5 ++++- test/js/bun/plugin/plugins.test.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/BunPlugin.cpp b/src/jsc/bindings/BunPlugin.cpp index 5773394aed30..bf2e350e2f4a 100644 --- a/src/jsc/bindings/BunPlugin.cpp +++ b/src/jsc/bindings/BunPlugin.cpp @@ -302,8 +302,11 @@ static inline JSC::EncodedJSValue setupBunPlugin(JSC::JSGlobalObject* globalObje auto targetValue = obj->getIfPropertyExists(globalObject, Identifier::fromString(vm, "target"_s)); RETURN_IF_EXCEPTION(throwScope, {}); if (targetValue) { - if (auto* targetJSString = targetValue.toStringOrNull(globalObject)) { + auto* targetJSString = targetValue.toStringOrNull(globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + if (targetJSString) { String targetString = targetJSString->value(globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); if (!(targetString == "node"_s || targetString == "bun"_s || targetString == "browser"_s)) { JSC::throwTypeError(globalObject, throwScope, "plugin target must be one of 'node', 'bun' or 'browser'"_s); return {}; diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index 1ccd453b2246..539bb57d0b8f 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -366,6 +366,21 @@ describe("errors", () => { }).toThrow("plugin target must be one of 'node', 'bun' or 'browser'"); }); + it("handles a 'target' whose toString throws", () => { + const opts = { + setup: () => {}, + target: { + toString() { + throw new Error("target toString error"); + }, + }, + }; + + expect(() => { + plugin(opts as any); + }).toThrow("target toString error"); + }); + it("invalid loaders throw", () => { const invalidLoaders = ["blah", "blah2", "blah3", "blah4"]; const inputs = ["body { background: red; }", "

hi

", '{"hi": "there"}', "hi"]; From 778670d5fa410d98917294a3a69fde60ce202c00 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 23 May 2026 17:10:22 +0000 Subject: [PATCH 2/4] ci: retrigger From 993e93a749f7f2bab1e8b733126a3e97b80c4012 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 24 May 2026 02:52:37 +0000 Subject: [PATCH 3/4] Use toString() for Bun.plugin target coercion toString() returns the empty string instead of null on exception, so the null check is unnecessary; keep the exception checks. --- src/jsc/bindings/BunPlugin.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/jsc/bindings/BunPlugin.cpp b/src/jsc/bindings/BunPlugin.cpp index bf2e350e2f4a..cc8c2d228554 100644 --- a/src/jsc/bindings/BunPlugin.cpp +++ b/src/jsc/bindings/BunPlugin.cpp @@ -302,15 +302,13 @@ static inline JSC::EncodedJSValue setupBunPlugin(JSC::JSGlobalObject* globalObje auto targetValue = obj->getIfPropertyExists(globalObject, Identifier::fromString(vm, "target"_s)); RETURN_IF_EXCEPTION(throwScope, {}); if (targetValue) { - auto* targetJSString = targetValue.toStringOrNull(globalObject); + auto* targetJSString = targetValue.toString(globalObject); RETURN_IF_EXCEPTION(throwScope, {}); - if (targetJSString) { - String targetString = targetJSString->value(globalObject); - RETURN_IF_EXCEPTION(throwScope, {}); - if (!(targetString == "node"_s || targetString == "bun"_s || targetString == "browser"_s)) { - JSC::throwTypeError(globalObject, throwScope, "plugin target must be one of 'node', 'bun' or 'browser'"_s); - return {}; - } + String targetString = targetJSString->value(globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!(targetString == "node"_s || targetString == "bun"_s || targetString == "browser"_s)) { + JSC::throwTypeError(globalObject, throwScope, "plugin target must be one of 'node', 'bun' or 'browser'"_s); + return {}; } } From 6aa3152965b77702aa46cc888d1d5ac3fda006c0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:02:02 +0000 Subject: [PATCH 4/4] Assert setup is not invoked when target coercion throws --- test/js/bun/plugin/plugins.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index 539bb57d0b8f..f1ad19fc49dd 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -1,6 +1,6 @@ /// import { plugin } from "bun"; -import { describe, expect, it } from "bun:test"; +import { describe, expect, it, jest } from "bun:test"; import { resolve } from "path"; declare global { @@ -367,8 +367,9 @@ describe("errors", () => { }); it("handles a 'target' whose toString throws", () => { + const setup = jest.fn(); const opts = { - setup: () => {}, + setup, target: { toString() { throw new Error("target toString error"); @@ -379,6 +380,7 @@ describe("errors", () => { expect(() => { plugin(opts as any); }).toThrow("target toString error"); + expect(setup).not.toHaveBeenCalled(); }); it("invalid loaders throw", () => {