From 2f48282cbdfba27437bc78415781f32cde51eb3c Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Tue, 25 Feb 2025 14:44:49 -0800 Subject: [PATCH] feat(build): `PluginBuilder` supports method chaining (#17683) --- packages/bun-types/bun.d.ts | 18 ++++++++---- src/bun.js/bindings/BunPlugin.cpp | 7 +++-- src/bun.js/bindings/JSBundlerPlugin.cpp | 4 +-- src/js/builtins/BundlerPlugin.ts | 37 ++++++++++++++++--------- test/bundler/native-plugin.test.ts | 6 +++- test/js/bun/plugin/plugins.test.ts | 6 ++-- 6 files changed, 52 insertions(+), 26 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 2df6f84880..91ead34066 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -5536,12 +5536,14 @@ declare module "bun" { * }, * }); * ``` + * + * @returns `this` for method chaining */ - onStart(callback: OnStartCallback): void; + onStart(callback: OnStartCallback): this; onBeforeParse( constraints: PluginConstraints, callback: { napiModule: unknown; symbol: string; external?: unknown | undefined }, - ): void; + ): this; /** * Register a callback to load imports with a specific import specifier * @param constraints The constraints to apply the plugin to @@ -5556,8 +5558,10 @@ declare module "bun" { * }, * }); * ``` + * + * @returns `this` for method chaining */ - onLoad(constraints: PluginConstraints, callback: OnLoadCallback): void; + onLoad(constraints: PluginConstraints, callback: OnLoadCallback): this; /** * Register a callback to resolve imports matching a filter and/or namespace * @param constraints The constraints to apply the plugin to @@ -5572,8 +5576,10 @@ declare module "bun" { * }, * }); * ``` + * + * @returns `this` for method chaining */ - onResolve(constraints: PluginConstraints, callback: OnResolveCallback): void; + onResolve(constraints: PluginConstraints, callback: OnResolveCallback): this; /** * The config object passed to `Bun.build` as is. Can be mutated. */ @@ -5604,8 +5610,10 @@ declare module "bun" { * const { foo } = require("hello:world"); * console.log(foo); // "bar" * ``` + * + * @returns `this` for method chaining */ - module(specifier: string, callback: () => OnLoadResult | Promise): void; + module(specifier: string, callback: () => OnLoadResult | Promise): this; } interface BunPlugin { diff --git a/src/bun.js/bindings/BunPlugin.cpp b/src/bun.js/bindings/BunPlugin.cpp index 0591cda623..f39bddf3b9 100644 --- a/src/bun.js/bindings/BunPlugin.cpp +++ b/src/bun.js/bindings/BunPlugin.cpp @@ -94,7 +94,7 @@ static JSC::EncodedJSValue jsFunctionAppendOnLoadPluginBody(JSC::JSGlobalObject* plugin.append(vm, filter->regExp(), func.getObject(), namespaceString); callback(ctx, globalObject); - return JSValue::encode(jsUndefined()); + return JSValue::encode(callframe->thisValue()); } static EncodedJSValue jsFunctionAppendVirtualModulePluginBody(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) @@ -150,7 +150,7 @@ static EncodedJSValue jsFunctionAppendVirtualModulePluginBody(JSC::JSGlobalObjec global->requireMap()->remove(globalObject, moduleIdValue); global->esmRegistryMap()->remove(globalObject, moduleIdValue); - return JSValue::encode(jsUndefined()); + return JSValue::encode(callframe->thisValue()); } static JSC::EncodedJSValue jsFunctionAppendOnResolvePluginBody(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, BunPluginTarget target, BunPlugin::Base& plugin, void* ctx, OnAppendPluginCallback callback) @@ -204,7 +204,7 @@ static JSC::EncodedJSValue jsFunctionAppendOnResolvePluginBody(JSC::JSGlobalObje plugin.append(vm, filter->regExp(), jsCast(func), namespaceString); callback(ctx, globalObject); - return JSValue::encode(jsUndefined()); + return JSValue::encode(callframe->thisValue()); } static JSC::EncodedJSValue jsFunctionAppendOnResolvePluginGlobal(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, BunPluginTarget target) @@ -260,6 +260,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionAppendOnResolvePluginBrowser, (JSC::JSGlobalO return jsFunctionAppendOnResolvePluginGlobal(globalObject, callframe, BunPluginTargetBrowser); } +/// `Bun.plugin()` static inline JSC::EncodedJSValue setupBunPlugin(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe, BunPluginTarget target) { auto& vm = JSC::getVM(globalObject); diff --git a/src/bun.js/bindings/JSBundlerPlugin.cpp b/src/bun.js/bindings/JSBundlerPlugin.cpp index d0130a9803..f4b77c4a95 100644 --- a/src/bun.js/bindings/JSBundlerPlugin.cpp +++ b/src/bun.js/bindings/JSBundlerPlugin.cpp @@ -38,6 +38,7 @@ namespace Bun { +extern "C" void CrashHandler__setInsideNativePlugin(const char* plugin_name); extern "C" int OnBeforeParsePlugin__isDone(void* context); extern "C" void OnBeforeParseResult__reset(OnBeforeParseResult* result); #define WRAP_BUNDLER_PLUGIN(argName) jsDoubleNumber(std::bit_cast(reinterpret_cast(argName))) @@ -189,6 +190,7 @@ DEFINE_VISIT_CHILDREN(JSBundlerPlugin); const JSC::ClassInfo JSBundlerPlugin::s_info = { "BundlerPlugin"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBundlerPlugin) }; +/// `BundlerPlugin.prototype.addFilter(filter: RegExp, namespace: string, isOnLoad: 0 | 1): void` JSC_DEFINE_HOST_FUNCTION(jsBundlerPluginFunction_addFilter, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSBundlerPlugin* thisObject = jsCast(callFrame->thisValue()); @@ -273,8 +275,6 @@ bool BundlerPlugin::FilterRegExp::match(JSC::VM& vm, const String& path) return regex.match(path) != -1; } -extern "C" void CrashHandler__setInsideNativePlugin(const char* plugin_name); - int BundlerPlugin::NativePluginList::call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, OnBeforeParseArguments* onBeforeParseArgs, OnBeforeParseResult* onBeforeParseResult) { unsigned index = 0; diff --git a/src/js/builtins/BundlerPlugin.ts b/src/js/builtins/BundlerPlugin.ts index 2648c96ca5..2dfdba6bb1 100644 --- a/src/js/builtins/BundlerPlugin.ts +++ b/src/js/builtins/BundlerPlugin.ts @@ -1,6 +1,9 @@ import type { BuildConfig, BunPlugin, OnLoadCallback, OnResolveCallback, PluginBuilder, PluginConstraints } from "bun"; type AnyFunction = (...args: any[]) => any; +/** + * @see `JSBundlerPlugin.h` + */ interface BundlerPlugin { onLoad: Map; onResolve: Map; @@ -108,10 +111,10 @@ export function runSetupFunction( promises: Array> | undefined, is_last: boolean, isBake: boolean, -): Promise>> | undefined { +): Promise[]> | Promise[] | undefined { this.promises = promises; - var onLoadPlugins = new Map(); - var onResolvePlugins = new Map(); + var onLoadPlugins = new Map(); + var onResolvePlugins = new Map(); var onBeforeParsePlugins = new Map< string, [RegExp, napiModule: unknown, symbol: string, external?: undefined | unknown][] @@ -172,23 +175,27 @@ export function runSetupFunction( } } - function onLoad(filterObject, callback) { + function onLoad(this: PluginBuilder, filterObject: PluginConstraints, callback: OnLoadCallback): PluginBuilder { validate(filterObject, callback, onLoadPlugins, undefined, undefined); + return this; } - function onResolve(filterObject, callback) { + function onResolve(this: PluginBuilder, filterObject: PluginConstraints, callback): PluginBuilder { validate(filterObject, callback, onResolvePlugins, undefined, undefined); + return this; } function onBeforeParse( - filterObject, + this: PluginBuilder, + filterObject: PluginConstraints, { napiModule, external, symbol }: { napiModule: unknown; symbol: string; external?: undefined | unknown }, - ) { + ): PluginBuilder { validate(filterObject, napiModule, onBeforeParsePlugins, symbol, external); + return this; } const self = this; - function onStart(callback) { + function onStart(this: PluginBuilder, callback): PluginBuilder { if (isBake) { throw new TypeError("onStart() is not supported in Bake yet"); } @@ -203,6 +210,7 @@ export function runSetupFunction( self.promises.push(ret); } } + return this; } const processSetupResult = () => { @@ -210,14 +218,14 @@ export function runSetupFunction( anyOnResolve = false, anyOnBeforeParse = false; - for (var [namespace, callbacks] of onLoadPlugins.entries()) { + for (let [namespace, callbacks] of onLoadPlugins.entries()) { for (var [filter] of callbacks) { this.addFilter(filter, namespace, 1); anyOnLoad = true; } } - for (var [namespace, callbacks] of onResolvePlugins.entries()) { + for (let [namespace, callbacks] of onResolvePlugins.entries()) { for (var [filter] of callbacks) { this.addFilter(filter, namespace, 0); anyOnResolve = true; @@ -236,7 +244,7 @@ export function runSetupFunction( if (!onResolveObject) { this.onResolve = onResolvePlugins; } else { - for (var [namespace, callbacks] of onResolvePlugins.entries()) { + for (let [namespace, callbacks] of onResolvePlugins.entries()) { var existing = onResolveObject.$get(namespace) as [RegExp, AnyFunction][]; if (!existing) { @@ -253,7 +261,7 @@ export function runSetupFunction( if (!onLoadObject) { this.onLoad = onLoadPlugins; } else { - for (var [namespace, callbacks] of onLoadPlugins.entries()) { + for (let [namespace, callbacks] of onLoadPlugins.entries()) { var existing = onLoadObject.$get(namespace) as [RegExp, AnyFunction][]; if (!existing) { @@ -284,6 +292,9 @@ export function runSetupFunction( module: () => { throw new TypeError("module() is not supported in Bun.build() yet. Only via Bun.plugin() at runtime"); }, + addPreload: () => { + throw new TypeError("addPreload() is not supported in Bun.build() yet."); + }, // esbuild's options argument is different, we provide some interop initialOptions: { ...config, @@ -297,7 +308,7 @@ export function runSetupFunction( platform: config.target === "bun" ? "node" : config.target, }, esbuild: {}, - } satisfies PluginBuilderExt as PluginBuilder); + } as PluginBuilderExt); if (setupResult && $isPromise(setupResult)) { if ($getPromiseInternalField(setupResult, $promiseFieldFlags) & $promiseStateFulfilled) { diff --git a/test/bundler/native-plugin.test.ts b/test/bundler/native-plugin.test.ts index 3b42e7c315..f545dd1f53 100644 --- a/test/bundler/native-plugin.test.ts +++ b/test/bundler/native-plugin.test.ts @@ -97,7 +97,11 @@ values;`, { name: "xXx123_foo_counter_321xXx", setup(build) { - build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "plugin_impl", external }); + const chainedThis = build.onBeforeParse( + { filter: /\.ts/ }, + { napiModule, symbol: "plugin_impl", external }, + ); + expect(chainedThis).toBe(build); build.onLoad({ filter: /lmao\.json/ }, async ({ defer }) => { await defer(); diff --git a/test/js/bun/plugin/plugins.test.ts b/test/js/bun/plugin/plugins.test.ts index a6f57b7425..efca70591c 100644 --- a/test/js/bun/plugin/plugins.test.ts +++ b/test/js/bun/plugin/plugins.test.ts @@ -16,20 +16,22 @@ declare global { plugin({ name: "url text file loader", setup(builder) { - builder.onResolve({ namespace: "http", filter: /.*/ }, ({ path }) => { + var chainedThis = builder.onResolve({ namespace: "http", filter: /.*/ }, ({ path }) => { return { path, namespace: "url", }; }); + expect(chainedThis).toBe(builder); - builder.onLoad({ filter: /.*/, namespace: "url" }, async ({ path, namespace }) => { + chainedThis = builder.onLoad({ filter: /.*/, namespace: "url" }, async ({ path, namespace }) => { const res = await fetch("http://" + path); return { exports: { default: await res.text() }, loader: "object", }; }); + expect(chainedThis).toBe(builder); }, });