Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
9ef9af1a41 retry CI 2026-03-04 19:08:26 +00:00
Claude Bot
a5a9e3fcd5 fix(plugin): also pass importer to runtime onLoad callback args
Thread the referrer/importer string through the entire call chain:
C++ call sites → runVirtualModule → Bun__runVirtualModule (Zig) →
runOnLoadPlugins → Bun__runOnLoadPlugins (C++) → OnLoad::run.

The importer is now available as args.importer in onLoad callbacks.
For entry-point modules where JSC sets referrer to "undefined",
the value is normalized to an empty string.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-04 19:06:32 +00:00
Claude Bot
cc0f4bdaea retry CI 2026-03-04 18:47:12 +00:00
Claude Bot
2c80096721 fix(plugin): pass namespace to runtime onLoad callback args
The runtime Bun.plugin() onLoad callback only received `path` in its
args object. The `namespace` parameter was available in the C++ function
signature but was never added to the params object passed to JavaScript.

Closes #27793

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-04 18:30:25 +00:00
7 changed files with 130 additions and 19 deletions

View File

@@ -1114,7 +1114,7 @@ pub export fn Bun__transpileFile(
return promise;
}
export fn Bun__runVirtualModule(globalObject: *JSGlobalObject, specifier_ptr: *const bun.String) JSValue {
export fn Bun__runVirtualModule(globalObject: *JSGlobalObject, specifier_ptr: *const bun.String, referrer_ptr: ?*const bun.String) JSValue {
jsc.markBinding(@src());
if (globalObject.bunVM().plugin_runner == null) return JSValue.zero;
@@ -1132,7 +1132,9 @@ export fn Bun__runVirtualModule(globalObject: *JSGlobalObject, specifier_ptr: *c
else
specifier[@min(namespace.len + 1, specifier.len)..];
return globalObject.runOnLoadPlugins(bun.String.init(namespace), bun.String.init(after_namespace), .bun) catch {
const importer: ?bun.String = if (referrer_ptr) |r| r.* else null;
return globalObject.runOnLoadPlugins(bun.String.init(namespace), bun.String.init(after_namespace), importer, .bun) catch {
return JSValue.zero;
} orelse return .zero;
}

View File

@@ -712,9 +712,10 @@ void JSModuleMock::visitChildrenImpl(JSCell* cell, Visitor& visitor)
DEFINE_VISIT_CHILDREN(JSModuleMock);
EncodedJSValue BunPlugin::OnLoad::run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path)
EncodedJSValue BunPlugin::OnLoad::run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path, BunString* importer)
{
Group* groupPtr = this->group(namespaceString ? namespaceString->toWTFString(BunString::ZeroCopy) : String());
auto nsString = namespaceString ? namespaceString->toWTFString(BunString::ZeroCopy) : String();
Group* groupPtr = this->group(nsString);
if (groupPtr == nullptr) {
return JSValue::encode(jsUndefined());
}
@@ -732,11 +733,25 @@ EncodedJSValue BunPlugin::OnLoad::run(JSC::JSGlobalObject* globalObject, BunStri
auto scope = DECLARE_THROW_SCOPE(vm);
scope.assertNoExceptionExceptTermination();
JSC::JSObject* paramsObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 1);
JSC::JSObject* paramsObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 3);
const auto& builtinNames = WebCore::builtinNames(vm);
paramsObject->putDirect(
vm, builtinNames.pathPublicName(),
jsString(vm, pathString));
paramsObject->putDirect(
vm, Identifier::fromString(vm, "namespace"_s),
jsString(vm, nsString.isEmpty() ? String("file"_s) : nsString));
{
String importerString;
if (importer && importer->tag != BunStringTag::Dead) {
importerString = importer->toWTFString(BunString::ZeroCopy);
if (importerString == "undefined"_s)
importerString = String(emptyString());
}
paramsObject->putDirect(
vm, builtinNames.importerPublicName(),
jsString(vm, importerString));
}
arguments.append(paramsObject);
auto result = AsyncContextFrame::call(globalObject, function, JSC::jsUndefined(), arguments);
@@ -873,9 +888,9 @@ extern "C" JSC::EncodedJSValue Bun__runOnResolvePlugins(Zig::GlobalObject* globa
return globalObject->onResolvePlugins.run(globalObject, namespaceString, path, from);
}
extern "C" JSC::EncodedJSValue Bun__runOnLoadPlugins(Zig::GlobalObject* globalObject, BunString* namespaceString, BunString* path, BunPluginTarget target)
extern "C" JSC::EncodedJSValue Bun__runOnLoadPlugins(Zig::GlobalObject* globalObject, BunString* namespaceString, BunString* path, BunString* importer, BunPluginTarget target)
{
return globalObject->onLoadPlugins.run(globalObject, namespaceString, path);
return globalObject->onLoadPlugins.run(globalObject, namespaceString, path, importer);
}
namespace Bun {
@@ -885,10 +900,10 @@ Structure* createModuleMockStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObj
return Zig::JSModuleMock::createStructure(vm, globalObject, prototype);
}
JSC::JSValue runVirtualModule(Zig::GlobalObject* globalObject, BunString* specifier, bool& wasModuleMock)
JSC::JSValue runVirtualModule(Zig::GlobalObject* globalObject, BunString* specifier, BunString* referrer, bool& wasModuleMock)
{
auto fallback = [&]() -> JSC::JSValue {
return JSValue::decode(Bun__runVirtualModule(globalObject, specifier));
return JSValue::decode(Bun__runVirtualModule(globalObject, specifier, referrer));
};
if (!globalObject->onLoadPlugins.hasVirtualModules()) {

View File

@@ -71,7 +71,7 @@ public:
VirtualModuleMap* _Nullable virtualModules = nullptr;
bool mustDoExpensiveRelativeLookup = false;
JSC::EncodedJSValue run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path);
JSC::EncodedJSValue run(JSC::JSGlobalObject* globalObject, BunString* namespaceString, BunString* path, BunString* importer);
bool hasVirtualModules() const { return virtualModules != nullptr; }
@@ -104,6 +104,6 @@ class GlobalObject;
} // namespace Zig
namespace Bun {
JSC::JSValue runVirtualModule(Zig::GlobalObject*, BunString* specifier, bool& wasModuleMock);
JSC::JSValue runVirtualModule(Zig::GlobalObject*, BunString* specifier, BunString* referrer, bool& wasModuleMock);
JSC::Structure* createModuleMockStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype);
}

View File

@@ -241,12 +241,13 @@ pub const JSGlobalObject = opaque {
node = 1,
browser = 2,
};
extern fn Bun__runOnLoadPlugins(*jsc.JSGlobalObject, ?*const bun.String, *const bun.String, BunPluginTarget) JSValue;
extern fn Bun__runOnLoadPlugins(*jsc.JSGlobalObject, ?*const bun.String, *const bun.String, ?*const bun.String, BunPluginTarget) JSValue;
extern fn Bun__runOnResolvePlugins(*jsc.JSGlobalObject, ?*const bun.String, *const bun.String, *const String, BunPluginTarget) JSValue;
pub fn runOnLoadPlugins(this: *JSGlobalObject, namespace_: bun.String, path: bun.String, target: BunPluginTarget) bun.JSError!?JSValue {
pub fn runOnLoadPlugins(this: *JSGlobalObject, namespace_: bun.String, path: bun.String, importer: ?bun.String, target: BunPluginTarget) bun.JSError!?JSValue {
jsc.markBinding(@src());
const result = try bun.jsc.fromJSHostCall(this, @src(), Bun__runOnLoadPlugins, .{ this, if (namespace_.length() > 0) &namespace_ else null, &path, target });
const importer_ptr: ?*const bun.String = if (importer) |*i| i else null;
const result = try bun.jsc.fromJSHostCall(this, @src(), Bun__runOnLoadPlugins, .{ this, if (namespace_.length() > 0) &namespace_ else null, &path, importer_ptr, target });
if (result.isUndefinedOrNull()) return null;
return result;
}

View File

@@ -667,7 +667,7 @@ JSValue fetchCommonJSModule(
// When "bun test" is enabled, allow users to override builtin modules
// This is important for being able to trivially mock things like the filesystem.
if (isBunTest) {
JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, &specifier, wasModuleMock);
JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, &specifier, referrer, wasModuleMock);
RETURN_IF_EXCEPTION(scope, {});
if (virtualModuleResult) {
JSValue promiseOrCommonJSModule = handleVirtualModuleResult<true>(globalObject, virtualModuleResult, res, &specifier, referrer, wasModuleMock, target);
@@ -717,7 +717,7 @@ JSValue fetchCommonJSModule(
// When "bun test" is NOT enabled, disable users from overriding builtin modules
if (!isBunTest) {
JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, &specifier, wasModuleMock);
JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, &specifier, referrer, wasModuleMock);
RETURN_IF_EXCEPTION(scope, {});
if (virtualModuleResult) {
JSValue promiseOrCommonJSModule = handleVirtualModuleResult<true>(globalObject, virtualModuleResult, res, &specifier, referrer, wasModuleMock, target);
@@ -936,7 +936,7 @@ static JSValue fetchESMSourceCode(
// When "bun test" is enabled, allow users to override builtin modules
// This is important for being able to trivially mock things like the filesystem.
if (isBunTest) {
JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, specifier, wasModuleMock);
JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, specifier, referrer, wasModuleMock);
RETURN_IF_EXCEPTION(scope, {});
if (virtualModuleResult) {
RELEASE_AND_RETURN(scope, handleVirtualModuleResult<allowPromise>(globalObject, virtualModuleResult, res, specifier, referrer, wasModuleMock));
@@ -1002,7 +1002,7 @@ static JSValue fetchESMSourceCode(
// When "bun test" is NOT enabled, disable users from overriding builtin modules
if (!isBunTest) {
JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, specifier, wasModuleMock);
JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, specifier, referrer, wasModuleMock);
RETURN_IF_EXCEPTION(scope, {});
if (virtualModuleResult) {
RELEASE_AND_RETURN(scope, handleVirtualModuleResult<allowPromise>(globalObject, virtualModuleResult, res, specifier, referrer, wasModuleMock));

View File

@@ -364,7 +364,8 @@ extern "C" bool Bun__transpileVirtualModule(
extern "C" JSC::EncodedJSValue Bun__runVirtualModule(
JSC::JSGlobalObject* global,
const BunString* specifier);
const BunString* specifier,
const BunString* referrer);
extern "C" JSC::JSInternalPromise* Bun__transpileFile(
void* bunVM,

View File

@@ -0,0 +1,92 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("onLoad callback receives namespace and importer for file namespace", async () => {
using dir = tempDir("issue-27793", {
"plugin.ts": `
import { plugin } from "bun";
import { readFileSync } from "fs";
plugin({
name: "test-plugin",
setup(build) {
build.onLoad({ filter: /\\.js$/, namespace: "file" }, (args) => {
console.log(JSON.stringify({
path: typeof args.path,
namespace: args.namespace,
importer: typeof args.importer,
}));
const contents = readFileSync(args.path, "utf8");
return { contents, loader: "js" };
});
},
});
`,
"main.js": `
import "./lib.js";
console.log("ok");
`,
"lib.js": `console.log("lib");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./plugin.ts", "./main.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const lines = stdout.trim().split("\n");
// The plugin is called for main.js (entry point) and lib.js (imported by main.js)
const mainOutput = JSON.parse(lines[0]);
expect(mainOutput.path).toBe("string");
expect(mainOutput.namespace).toBe("file");
expect(mainOutput.importer).toBe("string");
expect(exitCode).toBe(0);
});
test("onLoad callback receives namespace and importer for custom namespace", async () => {
using dir = tempDir("issue-27793-custom-ns", {
"plugin.ts": `
import { plugin } from "bun";
plugin({
name: "test-plugin",
setup(build) {
build.onResolve({ filter: /.*/, namespace: "custom" }, (args) => {
return { path: args.path, namespace: "custom" };
});
build.onLoad({ filter: /.*/, namespace: "custom" }, (args) => {
return {
exports: {
default: { namespace: args.namespace, path: args.path, importer: args.importer },
},
loader: "object",
};
});
},
});
`,
"main.js": `
import result from "custom:hello";
console.log(JSON.stringify(result));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./plugin.ts", "./main.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const result = JSON.parse(stdout.trim());
expect(result.namespace).toBe("custom");
expect(result.path).toBe("hello");
expect(typeof result.importer).toBe("string");
expect(exitCode).toBe(0);
});