feat(build): PluginBuilder supports method chaining (#17683)

This commit is contained in:
Don Isaac
2025-02-25 14:44:49 -08:00
committed by GitHub
parent 1574df835e
commit 2f48282cbd
6 changed files with 52 additions and 26 deletions

View File

@@ -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<OnLoadResult>): void;
module(specifier: string, callback: () => OnLoadResult | Promise<OnLoadResult>): this;
}
interface BunPlugin {

View File

@@ -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<JSObject*>(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);

View File

@@ -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<double>(reinterpret_cast<uintptr_t>(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<JSBundlerPlugin*>(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;

View File

@@ -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<string, [RegExp, OnLoadCallback][]>;
onResolve: Map<string, [RegExp, OnResolveCallback][]>;
@@ -108,10 +111,10 @@ export function runSetupFunction(
promises: Array<Promise<any>> | undefined,
is_last: boolean,
isBake: boolean,
): Promise<Array<Promise<any>>> | undefined {
): Promise<Promise<any>[]> | Promise<any>[] | undefined {
this.promises = promises;
var onLoadPlugins = new Map<string, [RegExp, AnyFunction][]>();
var onResolvePlugins = new Map<string, [RegExp, AnyFunction][]>();
var onLoadPlugins = new Map<string, [filter: RegExp, callback: OnLoadCallback][]>();
var onResolvePlugins = new Map<string, [filter: RegExp, OnResolveCallback][]>();
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) {

View File

@@ -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();

View File

@@ -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);
},
});