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", () => {