mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 08:58:52 +00:00
Compare commits
8 Commits
claude/rep
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d53e32df89 | ||
|
|
b0adecb4bb | ||
|
|
724599ce81 | ||
|
|
ed5168086d | ||
|
|
7ae25106c3 | ||
|
|
805a90dbcc | ||
|
|
62d651420a | ||
|
|
108eb6efce |
2
packages/bun-types/bun.d.ts
vendored
2
packages/bun-types/bun.d.ts
vendored
@@ -5199,6 +5199,7 @@ declare module "bun" {
|
||||
type OnLoadResult = OnLoadResultSourceCode | OnLoadResultObject | undefined | void;
|
||||
type OnLoadCallback = (args: OnLoadArgs) => OnLoadResult | Promise<OnLoadResult>;
|
||||
type OnStartCallback = () => void | Promise<void>;
|
||||
type OnEndCallback = () => void | Promise<void>;
|
||||
|
||||
interface OnResolveArgs {
|
||||
/**
|
||||
@@ -5276,6 +5277,7 @@ declare module "bun" {
|
||||
* @returns `this` for method chaining
|
||||
*/
|
||||
onStart(callback: OnStartCallback): this;
|
||||
onEnd(callback: OnEndCallback): this;
|
||||
onBeforeParse(
|
||||
constraints: PluginConstraints,
|
||||
callback: {
|
||||
|
||||
@@ -1077,6 +1077,11 @@ pub const JSBundler = struct {
|
||||
extern fn JSBundlerPlugin__appendDeferPromise(*Plugin) JSValue;
|
||||
pub const appendDeferPromise = JSBundlerPlugin__appendDeferPromise;
|
||||
|
||||
pub fn runOnEndPlugins(this: *Plugin, build_result: jsc.JSValue) void {
|
||||
const global_object = this.globalObject();
|
||||
_ = global_object.runOnEndPlugins(build_result) catch {};
|
||||
}
|
||||
|
||||
pub fn hasAnyMatches(
|
||||
this: *Plugin,
|
||||
path: *const Fs.Path,
|
||||
|
||||
@@ -860,6 +860,38 @@ extern "C" JSC::EncodedJSValue Bun__runOnLoadPlugins(Zig::GlobalObject* globalOb
|
||||
return globalObject->onLoadPlugins.run(globalObject, namespaceString, path);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue Bun__runOnEndPlugins(Zig::GlobalObject* globalObject, JSC::EncodedJSValue buildResult)
|
||||
{
|
||||
JSC::VM& vm = globalObject->vm();
|
||||
JSC::JSLockHolder lock(vm);
|
||||
|
||||
// Get the current active plugin instance from the global object
|
||||
JSC::JSValue activePluginInstance = globalObject->get(globalObject, JSC::Identifier::fromString(vm, "activePluginInstance"));
|
||||
if (activePluginInstance.isUndefined() || !activePluginInstance.isObject()) {
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
// Get the runOnEndPlugins function from the active plugin instance
|
||||
JSC::JSObject* pluginObject = activePluginInstance.getObject();
|
||||
JSC::JSValue runOnEndFunction = pluginObject->get(globalObject, JSC::Identifier::fromString(vm, "runOnEndPlugins"));
|
||||
|
||||
if (runOnEndFunction.isUndefined() || !runOnEndFunction.isCallable()) {
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
// Call runOnEndPlugins with the build result
|
||||
JSC::CallData callData = JSC::getCallData(runOnEndFunction);
|
||||
if (callData.type == JSC::CallData::Type::None) {
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
JSC::MarkedArgumentBuffer args;
|
||||
args.append(JSC::JSValue::decode(buildResult));
|
||||
|
||||
JSC::JSValue result = JSC::call(globalObject, runOnEndFunction, callData, activePluginInstance, args);
|
||||
return JSC::JSValue::encode(result);
|
||||
}
|
||||
|
||||
namespace Bun {
|
||||
|
||||
Structure* createModuleMockStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
|
||||
@@ -240,6 +240,7 @@ pub const JSGlobalObject = opaque {
|
||||
};
|
||||
extern fn Bun__runOnLoadPlugins(*jsc.JSGlobalObject, ?*const bun.String, *const bun.String, BunPluginTarget) JSValue;
|
||||
extern fn Bun__runOnResolvePlugins(*jsc.JSGlobalObject, ?*const bun.String, *const bun.String, *const String, BunPluginTarget) JSValue;
|
||||
extern fn Bun__runOnEndPlugins(*jsc.JSGlobalObject, JSValue) JSValue;
|
||||
|
||||
pub fn runOnLoadPlugins(this: *JSGlobalObject, namespace_: bun.String, path: bun.String, target: BunPluginTarget) bun.JSError!?JSValue {
|
||||
jsc.markBinding(@src());
|
||||
@@ -255,6 +256,11 @@ pub const JSGlobalObject = opaque {
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn runOnEndPlugins(this: *JSGlobalObject, build_result: JSValue) bun.JSError!JSValue {
|
||||
jsc.markBinding(@src());
|
||||
return try bun.jsc.fromJSHostCall(this, @src(), Bun__runOnEndPlugins, .{ this, build_result });
|
||||
}
|
||||
|
||||
pub fn createErrorInstance(this: *JSGlobalObject, comptime fmt: [:0]const u8, args: anytype) JSValue {
|
||||
if (comptime std.meta.fieldNames(@TypeOf(args)).len > 0) {
|
||||
var stack_fallback = std.heap.stackFallback(1024 * 4, this.allocator());
|
||||
|
||||
@@ -1888,6 +1888,10 @@ pub const BundleV2 = struct {
|
||||
},
|
||||
);
|
||||
|
||||
if (this.plugins) |plugins| {
|
||||
_ = plugins.runOnEndPlugins(root_obj);
|
||||
}
|
||||
|
||||
promise.resolve(globalThis, root_obj);
|
||||
}
|
||||
|
||||
@@ -1985,6 +1989,12 @@ pub const BundleV2 = struct {
|
||||
return promise.reject(globalThis, err);
|
||||
},
|
||||
);
|
||||
|
||||
// Run onEnd plugins before resolving - success case
|
||||
if (this.plugins) |plugins| {
|
||||
_ = plugins.runOnEndPlugins(root_obj);
|
||||
}
|
||||
|
||||
promise.resolve(globalThis, root_obj);
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ interface BundlerPlugin {
|
||||
addFilter(filter, namespace, number): void;
|
||||
generateDeferPromise(id: number): Promise<void>;
|
||||
promises: Array<Promise<any>> | undefined;
|
||||
onEndCallbacks: Array<(result: any) => void | Promise<void>> | undefined;
|
||||
runOnEndPlugins?: (buildResult: any) => void | Promise<void>;
|
||||
|
||||
onBeforeParse: (filter: RegExp, namespace: string, addon: unknown, symbol: string, external?: unknown) => void;
|
||||
$napiDlopenHandle: number;
|
||||
@@ -122,6 +124,75 @@ export function runSetupFunction(
|
||||
isBake: boolean,
|
||||
): Promise<Promise<any>[]> | Promise<any>[] | undefined {
|
||||
this.promises = promises;
|
||||
|
||||
// Register this plugin instance globally so C++ can find it for onEnd callbacks
|
||||
globalThis.activePluginInstance = this;
|
||||
|
||||
// Add the runOnEndPlugins method to this instance
|
||||
if (!this.runOnEndPlugins) {
|
||||
this.runOnEndPlugins = async function (buildResult) {
|
||||
const { onEndCallbacks } = this;
|
||||
if (!onEndCallbacks || onEndCallbacks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert logs to errors/warnings arrays for esbuild compatibility
|
||||
const logs = buildResult.logs || [];
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
for (const log of logs) {
|
||||
if (log.level === "error") {
|
||||
errors.push({
|
||||
text: log.message || "",
|
||||
location: log.position
|
||||
? {
|
||||
file: log.position.file || "",
|
||||
line: log.position.line || 0,
|
||||
column: log.position.column || 0,
|
||||
}
|
||||
: null,
|
||||
notes: [],
|
||||
detail: undefined,
|
||||
});
|
||||
} else if (log.level === "warning") {
|
||||
warnings.push({
|
||||
text: log.message || "",
|
||||
location: log.position
|
||||
? {
|
||||
file: log.position.file || "",
|
||||
line: log.position.line || 0,
|
||||
column: log.position.column || 0,
|
||||
}
|
||||
: null,
|
||||
notes: [],
|
||||
detail: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create esbuild-compatible result object
|
||||
const onEndResult = {
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
|
||||
// Execute onEnd callbacks serially as per esbuild specification
|
||||
for (const callback of onEndCallbacks) {
|
||||
try {
|
||||
const result = callback(onEndResult);
|
||||
if ($isPromise(result)) {
|
||||
// Await the promise so callbacks complete in order
|
||||
// Note: build completion is not delayed for async onEnd callbacks
|
||||
await result;
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but don't fail the build
|
||||
console.error("onEnd callback error:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
var onLoadPlugins = new Map<string, [filter: RegExp, callback: OnLoadCallback][]>();
|
||||
var onResolvePlugins = new Map<string, [filter: RegExp, OnResolveCallback][]>();
|
||||
var onBeforeParsePlugins = new Map<
|
||||
@@ -222,6 +293,20 @@ export function runSetupFunction(
|
||||
return this;
|
||||
}
|
||||
|
||||
function onEnd(this: PluginBuilder, callback): PluginBuilder {
|
||||
if (isBake) {
|
||||
throw new TypeError("onEnd() is not supported in Bake yet");
|
||||
}
|
||||
if (!$isCallable(callback)) {
|
||||
throw new TypeError("callback must be a function");
|
||||
}
|
||||
|
||||
// Store onEnd callbacks for later execution
|
||||
self.onEndCallbacks ??= [];
|
||||
self.onEndCallbacks.push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
const processSetupResult = () => {
|
||||
var anyOnLoad = false,
|
||||
anyOnResolve = false;
|
||||
@@ -290,7 +375,7 @@ export function runSetupFunction(
|
||||
var setupResult = setup({
|
||||
config: config,
|
||||
onDispose: notImplementedIssueFn(2771, "On-dispose callbacks"),
|
||||
onEnd: notImplementedIssueFn(2771, "On-end callbacks"),
|
||||
onEnd,
|
||||
onLoad,
|
||||
onResolve,
|
||||
onBeforeParse,
|
||||
|
||||
@@ -630,7 +630,115 @@ test("onEnd Plugin does not crash", async () => {
|
||||
],
|
||||
});
|
||||
})(),
|
||||
).rejects.toThrow("On-end callbacks is not implemented yet. See https://github.com/oven-sh/bun/issues/2771");
|
||||
).rejects.toThrow("callback must be a function");
|
||||
});
|
||||
|
||||
test("onEnd Plugin executes callback", async () => {
|
||||
const dir = tempDirWithFiles("onEnd", {
|
||||
"entry.js": `
|
||||
console.log("Hello from onEnd test");
|
||||
export default "test";
|
||||
`,
|
||||
});
|
||||
|
||||
let onEndCalled = false;
|
||||
|
||||
await Bun.build({
|
||||
entrypoints: [join(dir, "entry.js")],
|
||||
plugins: [
|
||||
{
|
||||
name: "plugin",
|
||||
setup(build) {
|
||||
build.onEnd(result => {
|
||||
onEndCalled = true;
|
||||
expect(result).toHaveProperty("errors");
|
||||
expect(result).toHaveProperty("warnings");
|
||||
expect(Array.isArray(result.errors)).toBe(true);
|
||||
expect(Array.isArray(result.warnings)).toBe(true);
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(onEndCalled).toBe(true);
|
||||
});
|
||||
|
||||
test("onEnd Plugin handles multiple callbacks", async () => {
|
||||
const dir = tempDirWithFiles("onEnd-multi", {
|
||||
"entry.js": `
|
||||
console.log("Multiple callbacks test");
|
||||
export default "test";
|
||||
`,
|
||||
});
|
||||
|
||||
let firstCalled = false;
|
||||
let secondCalled = false;
|
||||
|
||||
await Bun.build({
|
||||
entrypoints: [join(dir, "entry.js")],
|
||||
plugins: [
|
||||
{
|
||||
name: "plugin1",
|
||||
setup(build) {
|
||||
build.onEnd(result => {
|
||||
firstCalled = true;
|
||||
expect(result).toHaveProperty("errors");
|
||||
expect(result).toHaveProperty("warnings");
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plugin2",
|
||||
setup(build) {
|
||||
build.onEnd(result => {
|
||||
secondCalled = true;
|
||||
expect(result).toHaveProperty("errors");
|
||||
expect(result).toHaveProperty("warnings");
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(firstCalled).toBe(true);
|
||||
expect(secondCalled).toBe(true);
|
||||
});
|
||||
|
||||
test("onEnd Plugin with async callback", async () => {
|
||||
const dir = tempDirWithFiles("onEnd-async", {
|
||||
"entry.js": `
|
||||
console.log("Async callback test");
|
||||
export default "async-test";
|
||||
`,
|
||||
});
|
||||
|
||||
let onEndCalled = false;
|
||||
let asyncOperationCompleted = false;
|
||||
|
||||
await Bun.build({
|
||||
entrypoints: [join(dir, "entry.js")],
|
||||
plugins: [
|
||||
{
|
||||
name: "async-plugin",
|
||||
setup(build) {
|
||||
build.onEnd(async result => {
|
||||
onEndCalled = true;
|
||||
// Simulate async operation
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
asyncOperationCompleted = true;
|
||||
expect(result).toHaveProperty("errors");
|
||||
expect(result).toHaveProperty("warnings");
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(onEndCalled).toBe(true);
|
||||
// Currently, the build does NOT wait for async onEnd callbacks to complete
|
||||
// This is different from esbuild behavior but matches our current implementation
|
||||
expect(asyncOperationCompleted).toBe(false);
|
||||
});
|
||||
|
||||
test("macro with nested object", async () => {
|
||||
|
||||
@@ -896,4 +896,156 @@ describe("bundler", () => {
|
||||
expect(js).toContain('.wasm"');
|
||||
},
|
||||
});
|
||||
|
||||
// OnEnd Plugin Tests
|
||||
itBundled("plugin/OnEnd/Basic", {
|
||||
files: {
|
||||
"index.ts": /* ts */ `
|
||||
export const message = "hello world";
|
||||
`,
|
||||
},
|
||||
plugins(builder) {
|
||||
// Store onEndCalled in a scope accessible to onAfterBundle
|
||||
this.onEndCalled = false;
|
||||
builder.onEnd(result => {
|
||||
this.onEndCalled = true;
|
||||
console.log("onEnd called with result:", result);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.warnings).toBeDefined();
|
||||
expect(Array.isArray(result.errors)).toBe(true);
|
||||
expect(Array.isArray(result.warnings)).toBe(true);
|
||||
});
|
||||
},
|
||||
onAfterBundle(api) {
|
||||
// Verify that onEnd was actually called
|
||||
expect(this.onEndCalled).toBe(true);
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("plugin/OnEnd/WithErrors", {
|
||||
files: {
|
||||
"index.ts": /* ts */ `
|
||||
import { missing } from "./nonexistent.js";
|
||||
export { missing };
|
||||
`,
|
||||
},
|
||||
plugins(builder) {
|
||||
builder.onEnd(result => {
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
const errorMessage = result.errors[0].text;
|
||||
expect(errorMessage).toContain("Could not resolve");
|
||||
});
|
||||
},
|
||||
bundleErrors: {
|
||||
"/index.ts": [`Could not resolve`],
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("plugin/OnEnd/Multiple", {
|
||||
files: {
|
||||
"index.ts": /* ts */ `
|
||||
export const value = 42;
|
||||
`,
|
||||
},
|
||||
plugins(builder) {
|
||||
const callOrder: number[] = [];
|
||||
|
||||
builder.onEnd(result => {
|
||||
callOrder.push(1);
|
||||
expect(result.errors.length).toBe(0);
|
||||
});
|
||||
|
||||
builder.onEnd(result => {
|
||||
callOrder.push(2);
|
||||
expect(callOrder).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
builder.onEnd(result => {
|
||||
callOrder.push(3);
|
||||
expect(callOrder).toEqual([1, 2, 3]);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("plugin/OnEnd/Async", {
|
||||
files: {
|
||||
"index.ts": /* ts */ `
|
||||
export const async_value = "test";
|
||||
`,
|
||||
},
|
||||
plugins(builder) {
|
||||
builder.onEnd(async result => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
expect(result).toBeDefined();
|
||||
expect(result.errors.length).toBe(0);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("plugin/OnEnd/ModifyResult", {
|
||||
files: {
|
||||
"index.ts": /* ts */ `
|
||||
export const test = "value";
|
||||
`,
|
||||
},
|
||||
plugins(builder) {
|
||||
builder.onEnd(result => {
|
||||
// Add a custom warning to the result
|
||||
result.warnings.push({
|
||||
text: "Custom warning from onEnd",
|
||||
location: null,
|
||||
notes: [],
|
||||
detail: undefined,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("plugin/OnEnd/WithOnLoad", {
|
||||
files: {
|
||||
"index.ts": /* ts */ `
|
||||
import { data } from "./data.custom";
|
||||
console.log(JSON.stringify(data));
|
||||
export { data };
|
||||
`,
|
||||
"data.custom": `{"value": "test"}`,
|
||||
},
|
||||
plugins(builder) {
|
||||
let onLoadCalled = false;
|
||||
let onEndCalled = false;
|
||||
|
||||
builder.onLoad({ filter: /\.custom$/ }, async args => {
|
||||
onLoadCalled = true;
|
||||
const text = await Bun.file(args.path).text();
|
||||
return {
|
||||
contents: `export const data = ${text};`,
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
|
||||
builder.onEnd(result => {
|
||||
onEndCalled = true;
|
||||
expect(onLoadCalled).toBe(true);
|
||||
expect(result.errors.length).toBe(0);
|
||||
});
|
||||
},
|
||||
run: {
|
||||
stdout: `{"value":"test"}`,
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("plugin/OnEnd/ExceptionHandling", {
|
||||
files: {
|
||||
"index.ts": /* ts */ `
|
||||
export const test = "exception handling";
|
||||
`,
|
||||
},
|
||||
plugins(builder) {
|
||||
builder.onEnd(result => {
|
||||
throw new Error("onEnd error");
|
||||
});
|
||||
},
|
||||
// The build should still succeed, but the error should be captured
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user