Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
5ba168a554 Allow onLoad plugins to return null/undefined to fallback to filesystem
Fixes #5303

Previously, when a plugin's onLoad callback returned null or undefined,
Bun would throw a TypeError: "onLoad() expects an object returned".

This change allows plugins to return null/undefined to signal "no match"
and fallback to the default loading mechanism (filesystem or next plugin),
matching the behavior of onResolve.

This is useful for:
- Conditional file handling (only process certain files)
- Plugin chaining (let another plugin handle it)
- Leveraging Bun's built-in loading for some files

The implementation follows the same pattern as onResolve:
- null/undefined -> continue to next plugin/fallback
- Other non-objects (boolean, number, string) -> throw TypeError
- Objects -> validate and process

Note: In Bun.build() API (BundlerPlugin.ts), ALL non-object values
fallback (including primitives like true, 42, "string"), but the C++
layer used by runtime plugins follows stricter validation like onResolve.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 07:50:25 +00:00
2 changed files with 176 additions and 0 deletions

View File

@@ -752,6 +752,11 @@ EncodedJSValue BunPlugin::OnLoad::run(JSC::JSGlobalObject* globalObject, BunStri
}
}
// Check again after promise resolution - null/undefined means "no match, try next plugin"
if (result.isUndefinedOrNull()) {
return JSValue::encode(jsUndefined());
}
if (!result.isObject()) {
JSC::throwTypeError(globalObject, scope, "onLoad() expects an object returned"_s);
return {};

View File

@@ -0,0 +1,171 @@
import { describe } from "bun:test";
import { itBundled } from "../../bundler/expectBundled";
// https://github.com/oven-sh/bun/issues/5303
describe("bundler", () => {
itBundled("plugin/OnLoadReturnsNullFallsBackToFilesystem", {
files: {
"index.ts": /* ts */ `
import { foo } from "./foo.ts";
console.log(foo);
`,
"foo.ts": /* ts */ `
export const foo = "from filesystem";
`,
},
plugins(builder) {
builder.onLoad({ filter: /foo\.ts$/ }, args => {
// Return null to fallback to filesystem
return null as any;
});
},
run: {
stdout: "from filesystem",
},
});
itBundled("plugin/OnLoadReturnsUndefinedFallsBackToFilesystem", {
files: {
"index.ts": /* ts */ `
import { bar } from "./bar.ts";
console.log(bar);
`,
"bar.ts": /* ts */ `
export const bar = "from filesystem";
`,
},
plugins(builder) {
builder.onLoad({ filter: /bar\.ts$/ }, args => {
// Return undefined to fallback to filesystem
return undefined as any;
});
},
run: {
stdout: "from filesystem",
},
});
itBundled("plugin/OnLoadReturnsNullAsyncFallsBackToFilesystem", {
files: {
"index.ts": /* ts */ `
import { baz } from "./baz.ts";
console.log(baz);
`,
"baz.ts": /* ts */ `
export const baz = "from filesystem";
`,
},
plugins(builder) {
builder.onLoad({ filter: /baz\.ts$/ }, async args => {
// Return null asynchronously to fallback to filesystem
return null as any;
});
},
run: {
stdout: "from filesystem",
},
});
itBundled("plugin/OnLoadConditionalFallback", {
files: {
"index.ts": /* ts */ `
import { magic } from "./magic.ts";
import { normal } from "./normal.ts";
console.log(magic, normal);
`,
"magic.ts": /* ts */ `
export const magic = "from filesystem (should be overridden)";
`,
"normal.ts": /* ts */ `
export const normal = "from filesystem";
`,
},
plugins(builder) {
builder.onLoad({ filter: /\.ts$/ }, args => {
// Only handle magic.ts, fallback for everything else
if (args.path.endsWith("magic.ts")) {
return {
contents: `export const magic = "from plugin";`,
loader: "ts",
};
}
// Return null to let other files be loaded from filesystem
return null as any;
});
},
run: {
stdout: "from plugin from filesystem",
},
});
itBundled("plugin/OnLoadReturnsNullForVirtualModule", {
files: {
"index.ts": /* ts */ `
import { value } from "virtual:test";
console.log(value);
`,
},
plugins(builder) {
builder.onResolve({ filter: /^virtual:/ }, args => {
return {
path: args.path,
namespace: "virtual",
};
});
// First plugin returns null, second handles it
builder.onLoad({ filter: /.*/, namespace: "virtual" }, args => {
return null as any;
});
builder.onLoad({ filter: /.*/, namespace: "virtual" }, args => {
return {
contents: `export const value = "from second plugin";`,
loader: "ts",
};
});
},
run: {
stdout: "from second plugin",
},
});
// Test that other primitives also fallback (not error) per BundlerPlugin.ts line 544
itBundled("plugin/OnLoadReturnsBooleanFallsBack", {
files: {
"index.ts": /* ts */ `
import { value } from "./test.ts";
console.log(value);
`,
"test.ts": `export const value = "from filesystem";`,
},
plugins(builder) {
builder.onLoad({ filter: /test\.ts$/ }, args => {
// Non-object primitives also fallback per BundlerPlugin.ts
return true as any;
});
},
run: {
stdout: "from filesystem",
},
});
itBundled("plugin/OnLoadReturnsUndefinedAsyncFallback", {
files: {
"index.ts": /* ts */ `
import { value } from "./test.ts";
console.log(value);
`,
"test.ts": `export const value = "from filesystem";`,
},
plugins(builder) {
builder.onLoad({ filter: /test\.ts$/ }, async args => {
// Async function returning undefined should also fallback
return undefined as any;
});
},
run: {
stdout: "from filesystem",
},
});
});