mirror of
https://github.com/oven-sh/bun
synced 2026-02-14 12:51:54 +00:00
Fixes #3894 Previously, runtime plugins (using `plugin()` API) only received `path` in their onLoad callbacks, making them much less capable than Bun.build() plugins which receive `path`, `namespace`, and `loader`. This change adds: - **namespace** property - allows plugins to distinguish between different virtual module namespaces - **loader** property - provides default loader based on file extension (js, tsx, jsx, json, text, toml, file) This enables better plugin composition and makes it possible to: - Pass information from onResolve to onLoad via namespace - Handle query strings in imports (workaround for missing pluginData) - Know the default loader for conditional handling Example usage: ```typescript plugin({ setup(builder) { builder.onResolve({ filter: /\.mdx/ }, (args) => { const [path, query] = args.path.split("?"); // Store query data externally since pluginData doesn't work yet queryMap.set(path, parseQuery(query)); return { path, namespace: "mdx-loader" }; }); builder.onLoad({ namespace: "mdx-loader" }, (args) => { // Can now access namespace and retrieve query data const query = queryMap.get(args.path); // ... handle MDX with query parameters }); } }); ``` **Still missing** (future work): - pluginData property (would require tracking through resolve/load pipeline) - suffix property (esbuild compatibility) - defer, side properties (build-specific) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
225 lines
6.1 KiB
TypeScript
225 lines
6.1 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, tempDir } from "harness";
|
|
|
|
// Tests for https://github.com/oven-sh/bun/issues/3894
|
|
// Runtime plugin onLoad should receive namespace, loader, and other properties
|
|
|
|
test("runtime plugin onLoad receives namespace property", async () => {
|
|
using dir = tempDir("runtime-plugin-namespace", {
|
|
"test.ts": /* ts */ `
|
|
import { plugin } from "bun";
|
|
|
|
plugin({
|
|
name: "namespace-test",
|
|
setup(builder) {
|
|
builder.onResolve({ filter: /\.custom$/ }, (args) => {
|
|
return {
|
|
path: args.path,
|
|
namespace: "custom-namespace",
|
|
};
|
|
});
|
|
|
|
builder.onLoad({ filter: /.*/, namespace: "custom-namespace" }, (args) => {
|
|
return {
|
|
exports: {
|
|
namespace: args.namespace,
|
|
path: args.path,
|
|
},
|
|
loader: "object",
|
|
};
|
|
});
|
|
},
|
|
});
|
|
|
|
const result = await import("./test.custom");
|
|
console.log(JSON.stringify(result));
|
|
`,
|
|
"test.custom": "dummy file",
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "test.ts"],
|
|
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-namespace");
|
|
expect(result.path).toBe("./test.custom");
|
|
expect(stderr).not.toContain("undefined");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("runtime plugin onLoad receives loader property based on file extension", async () => {
|
|
using dir = tempDir("runtime-plugin-loader", {
|
|
"test.ts": /* ts */ `
|
|
import { plugin } from "bun";
|
|
|
|
plugin({
|
|
name: "loader-test",
|
|
setup(builder) {
|
|
builder.onResolve({ filter: /\.js$/ }, (args) => {
|
|
return {
|
|
path: args.path,
|
|
namespace: "loader-test",
|
|
};
|
|
});
|
|
|
|
builder.onLoad({ filter: /.*/, namespace: "loader-test" }, (args) => {
|
|
console.log(JSON.stringify({ loader: args.loader }));
|
|
return {
|
|
exports: {},
|
|
loader: "object",
|
|
};
|
|
});
|
|
},
|
|
});
|
|
|
|
await import("./test.js");
|
|
`,
|
|
"test.js": "",
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "test.ts"],
|
|
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.loader).toBe("js");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("runtime plugin with query string uses namespace workaround", async () => {
|
|
using dir = tempDir("plugin-query-namespace", {
|
|
"test.ts": /* ts */ `
|
|
import { plugin } from "bun";
|
|
|
|
const queryMap = new Map();
|
|
|
|
plugin({
|
|
name: "query-namespace-test",
|
|
setup(builder) {
|
|
builder.onResolve({ filter: /\.custom/ }, (args) => {
|
|
const [path, query = ""] = args.path.split("?");
|
|
const parsed = Object.fromEntries(new URLSearchParams(query));
|
|
|
|
// Store query data (workaround for missing pluginData)
|
|
if (Object.keys(parsed).length > 0) {
|
|
queryMap.set(path, parsed);
|
|
}
|
|
|
|
return {
|
|
path,
|
|
namespace: "custom",
|
|
};
|
|
});
|
|
|
|
builder.onLoad({ filter: /.*/, namespace: "custom" }, (args) => {
|
|
const queryData = queryMap.get(args.path) || {};
|
|
|
|
return {
|
|
exports: {
|
|
namespace: args.namespace,
|
|
loader: args.loader,
|
|
queryData,
|
|
},
|
|
loader: "object",
|
|
};
|
|
});
|
|
},
|
|
});
|
|
|
|
const result = await import("./test.custom?type=example&id=123");
|
|
console.log(JSON.stringify(result));
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "test.ts"],
|
|
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.loader).toBe("file");
|
|
expect(result.queryData).toEqual({ type: "example", id: "123" });
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("runtime plugin onLoad receives all properties", async () => {
|
|
using dir = tempDir("runtime-plugin-all-props", {
|
|
"test.ts": /* ts */ `
|
|
import { plugin } from "bun";
|
|
|
|
plugin({
|
|
name: "all-props-test",
|
|
setup(builder) {
|
|
builder.onResolve({ filter: /file\.ts$/ }, (args) => {
|
|
return {
|
|
path: args.path,
|
|
namespace: "test-namespace",
|
|
};
|
|
});
|
|
|
|
builder.onLoad({ filter: /.*/, namespace: "test-namespace" }, (args) => {
|
|
const props = {
|
|
hasPath: "path" in args,
|
|
hasNamespace: "namespace" in args,
|
|
hasLoader: "loader" in args,
|
|
namespace: args.namespace,
|
|
loader: args.loader,
|
|
allKeys: Object.keys(args).sort(),
|
|
};
|
|
console.log(JSON.stringify(props));
|
|
return {
|
|
exports: {},
|
|
loader: "object",
|
|
};
|
|
});
|
|
},
|
|
});
|
|
|
|
await import("./file.ts");
|
|
`,
|
|
"file.ts": "export default 42;",
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "test.ts"],
|
|
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 props = JSON.parse(stdout.trim());
|
|
|
|
expect(props.hasPath).toBe(true);
|
|
expect(props.hasNamespace).toBe(true);
|
|
expect(props.hasLoader).toBe(true);
|
|
expect(props.namespace).toBe("test-namespace");
|
|
expect(props.loader).toBe("tsx");
|
|
expect(props.allKeys).toEqual(["loader", "namespace", "path"]);
|
|
expect(exitCode).toBe(0);
|
|
});
|