mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Start implementing build.onEnd hook
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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,14 @@ 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)
|
||||
{
|
||||
// For now, let's just return undefined to confirm the basic flow works
|
||||
// The actual implementation will need to find the correct plugin instance
|
||||
// and call runOnEndPlugins with the build result
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
namespace Bun {
|
||||
|
||||
Structure* createModuleMockStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
|
||||
@@ -674,4 +674,5 @@ extern "C" JSC::JSGlobalObject* JSBundlerPlugin__globalObject(Bun::JSBundlerPlug
|
||||
return plugin->m_globalObject;
|
||||
}
|
||||
|
||||
|
||||
} // namespace Bun
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -1867,6 +1867,8 @@ pub const BundleV2 = struct {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn toJSError(this: *JSBundleCompletionTask, promise: *jsc.JSPromise, globalThis: *jsc.JSGlobalObject) void {
|
||||
if (this.config.throw_on_error) {
|
||||
promise.reject(globalThis, this.log.toJSAggregateError(globalThis, bun.String.static("Bundle failed")));
|
||||
@@ -1887,6 +1889,11 @@ pub const BundleV2 = struct {
|
||||
return promise.reject(globalThis, err);
|
||||
},
|
||||
);
|
||||
|
||||
// Run onEnd plugins before resolving - error case
|
||||
if (this.plugins) |plugins| {
|
||||
plugins.runOnEndPlugins(root_obj);
|
||||
}
|
||||
|
||||
promise.resolve(globalThis, root_obj);
|
||||
}
|
||||
@@ -1985,6 +1992,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,7 @@ 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;
|
||||
|
||||
onBeforeParse: (filter: RegExp, namespace: string, addon: unknown, symbol: string, external?: unknown) => void;
|
||||
$napiDlopenHandle: number;
|
||||
@@ -222,6 +223,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 +305,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,
|
||||
@@ -443,6 +458,68 @@ export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespa
|
||||
}
|
||||
}
|
||||
|
||||
export function runOnEndPlugins(
|
||||
this: BundlerPlugin,
|
||||
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)) {
|
||||
// For now, we can't easily await promises in the bundler completion
|
||||
// We'll handle this synchronously
|
||||
console.warn("onEnd callback returned a promise, but async onEnd is not fully supported yet");
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but don't fail the build
|
||||
console.error("onEnd callback error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function runOnLoadPlugins(
|
||||
this: BundlerPlugin,
|
||||
internalID,
|
||||
|
||||
@@ -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) {
|
||||
let onEndCalled = false;
|
||||
builder.onEnd(result => {
|
||||
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) {
|
||||
// This runs after the bundle is complete
|
||||
// If onEnd was working, this should verify it was called
|
||||
console.log("Bundle completed - onEnd should have been called by now");
|
||||
},
|
||||
});
|
||||
|
||||
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