diff --git a/src/bake/bake.zig b/src/bake/bake.zig index c49c67c0d5..d3b56c93c0 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -111,23 +111,9 @@ pub const SplitBundlerOptions = struct { var iter = plugin_array.arrayIterator(global); while (iter.next()) |plugin_config| { - if (!plugin_config.isObject()) { - return global.throwInvalidArguments("Expected plugin to be an object", .{}); - } - - if (try plugin_config.getOptional(global, "name", ZigString.Slice)) |slice| { - defer slice.deinit(); - if (slice.len == 0) { - return global.throwInvalidArguments("Expected plugin to have a non-empty name", .{}); - } - } else { - return global.throwInvalidArguments("Expected plugin to have a name", .{}); - } - - const function = try plugin_config.getFunction(global, "setup") orelse { - return global.throwInvalidArguments("Expected plugin to have a setup() function", .{}); - }; - const plugin_result = try plugin.addPlugin(function, empty_object, .null, false, true); + const jsplugin = try Plugin.JS.fromJS(global, plugin_config); + const plugin_obj = try jsplugin.toObject(global); + const plugin_result = try plugin.addPlugin(plugin_obj.setup, empty_object, .null, false, true); if (plugin_result.asAnyPromise()) |promise| { promise.setHandled(global.vm()); // TODO: remove this call, replace with a promise list that must diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index c981bb8938..bde7c930d0 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -113,22 +113,9 @@ pub const JSBundler = struct { var onstart_promise_array: JSValue = JSValue.undefined; var i: usize = 0; while (iter.next()) |plugin| : (i += 1) { - if (!plugin.isObject()) { - return globalThis.throwInvalidArguments("Expected plugin to be an object", .{}); - } + const jsplugin = try (try Plugin.JS.fromJS(globalThis, plugin)).toObject(globalThis); - if (try plugin.getOptional(globalThis, "name", ZigString.Slice)) |slice| { - defer slice.deinit(); - if (slice.len == 0) { - return globalThis.throwInvalidArguments("Expected plugin to have a non-empty name", .{}); - } - } else { - return globalThis.throwInvalidArguments("Expected plugin to have a name", .{}); - } - - const function = try plugin.getFunction(globalThis, "setup") orelse { - return globalThis.throwInvalidArguments("Expected plugin to have a setup() function", .{}); - }; + const function = jsplugin.setup; var bun_plugins: *Plugin = plugins.* orelse brk: { plugins.* = Plugin.create( @@ -875,6 +862,70 @@ pub const JSBundler = struct { }; pub const Plugin = opaque { + pub const JS = union(enum) { + factory: JSC.JSValue, + object: Object, + + pub const Object = struct { + name: JSC.JSValue, + setup: JSC.JSValue, + + fn fromJS(global: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError!Object { + if (comptime bun.Environment.allow_assert) { + bun.assertWithLocation(value.isObject(), @src()); + } + + // plugin.name is a non-empty string + const name = try value.get(global, "name") orelse + return global.throwInvalidArguments("Expected plugin to have a name", .{}); + if (!name.isString()) { + const ty = name.jsTypeString(global); + return global.throwInvalidArguments("Expected plugin name to be a string, got '{}'", .{ty}); + } + if (name.getLength(global) == 0) + return global.throwInvalidArguments("Expected plugin name to be a non-empty string", .{}); + + // plugin.setup(builder) + const setup = try value.getFunction(global, "setup") orelse { + return global.throwInvalidArguments("Expected plugin to have a setup() function", .{}); + }; + + return Plugin.JS.Object{ + .name = name, + .setup = setup, + }; + } + }; + + pub fn fromJS(global: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError!Plugin.JS { + return if (value.isObject()) + Plugin.JS{ .object = try Object.fromJS(global, value) } + else if (value.isFunction()) + Plugin.JS{ .factory = value } + else err: { + const ty = value.jsTypeString(global); + break :err global.throwInvalidArguments("Expected plugin to be an object or a function, got '{}'", .{ty}); + }; + } + + pub fn toObject(this: *const JS, global: *JSC.JSGlobalObject) bun.JSError!Plugin.JS.Object { + switch (this.*) { + .object => |obj| return obj, + .factory => |factory| { + const result = try factory.call(global, global.toJSValue(), &[_]JSValue{}); + if (!result.isObject()) { + const ty = result.jsTypeString(global); + return global.throwTypeError("Expected plugin factory to return an object, got '{}'", .{ty}); + } + if (result.asPromise()) |_| { + return global.throwTypeError("Plugin factories cannot be async yet. Please move async logic into the setup() function.", .{}); + } + return Plugin.JS.Object.fromJS(global, result); + }, + } + } + }; + extern fn JSBundlerPlugin__create(*JSC.JSGlobalObject, JSC.JSGlobalObject.BunPluginTarget) *Plugin; pub fn create(global: *JSC.JSGlobalObject, target: JSC.JSGlobalObject.BunPluginTarget) *Plugin { JSC.markBinding(@src()); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index ffcb4afd54..505391302a 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3063,6 +3063,10 @@ pub const JSGlobalObject = opaque { } } + pub inline fn throwTypeError(this: *JSGlobalObject, comptime fmt: [:0]const u8, args: anytype) bun.JSError { + return this.throwValue(this.createTypeErrorInstance(fmt, args)); + } + pub fn createTypeErrorInstance(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()); diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index 766a95b78c..cd0d3786c0 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -1,5 +1,5 @@ import type { BunPlugin } from "bun"; -import { describe, beforeAll, afterAll, expect, test } from "bun:test"; +import { describe, beforeAll, afterAll, expect, test, it } from "bun:test"; import { readFileSync, writeFileSync, rmSync } from "fs"; import { bunEnv, bunExe, tempDirWithFiles } from "harness"; import path, { join } from "path";