Files
bun.sh/src/js/builtins/BundlerPlugin.ts
dave caruso 83ac4f0c33 windows: fix bun plugin (#8485)
* plugins work now

* real

* Update src/js/builtins/BundlerPlugin.ts

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2024-01-25 19:34:51 -08:00

375 lines
12 KiB
TypeScript

import type {
AnyFunction,
BuildConfig,
BunPlugin,
OnLoadCallback,
OnLoadResultObject,
OnLoadResultSourceCode,
OnResolveCallback,
PluginBuilder,
PluginConstraints,
} from "bun";
// This API expects 4 functions:
// It should be generic enough to reuse for Bun.plugin() eventually, too.
interface BundlerPlugin {
onLoad: Map<string, [RegExp, OnLoadCallback][]>;
onResolve: Map<string, [RegExp, OnResolveCallback][]>;
onLoadAsync(
internalID,
sourceCode: string | Uint8Array | ArrayBuffer | DataView | null,
loaderKey: number | null,
): void;
onResolveAsync(internalID, a, b, c): void;
addError(internalID, error, number): void;
addFilter(filter, namespace, number): void;
}
// Extra types
type Setup = BunPlugin["setup"];
type MinifyObj = Exclude<BuildConfig["minify"], boolean>;
interface BuildConfigExt extends BuildConfig {
// we support esbuild-style entryPoints
entryPoints?: string[];
// plugins is guaranteed to not be null
plugins: BunPlugin[];
}
interface PluginBuilderExt extends PluginBuilder {
// these functions aren't implemented yet, so we dont publicly expose them
resolve: AnyFunction;
onStart: AnyFunction;
onEnd: AnyFunction;
onDispose: AnyFunction;
// we partially support initialOptions. it's read-only and a subset of
// all options mapped to their esbuild names
initialOptions: any;
// we set this to an empty object
esbuild: any;
}
export function runSetupFunction(this: BundlerPlugin, setup: Setup, config: BuildConfigExt) {
var onLoadPlugins = new Map<string, [RegExp, AnyFunction][]>();
var onResolvePlugins = new Map<string, [RegExp, AnyFunction][]>();
function validate(filterObject: PluginConstraints, callback, map) {
if (!filterObject || !$isObject(filterObject)) {
throw new TypeError('Expected an object with "filter" RegExp');
}
if (!callback || !$isCallable(callback)) {
throw new TypeError("callback must be a function");
}
var { filter, namespace = "file" } = filterObject;
if (!filter) {
throw new TypeError('Expected an object with "filter" RegExp');
}
if (!$isRegExpObject(filter)) {
throw new TypeError("filter must be a RegExp");
}
if (namespace && !(typeof namespace === "string")) {
throw new TypeError("namespace must be a string");
}
if ((namespace?.length ?? 0) === 0) {
namespace = "file";
}
if (!/^([/$a-zA-Z0-9_\\-]+)$/.test(namespace)) {
throw new TypeError("namespace can only contain $a-zA-Z0-9_\\-");
}
var callbacks = map.$get(namespace);
if (!callbacks) {
map.$set(namespace, [[filter, callback]]);
} else {
$arrayPush(callbacks, [filter, callback]);
}
}
function onLoad(filterObject, callback) {
validate(filterObject, callback, onLoadPlugins);
}
function onResolve(filterObject, callback) {
validate(filterObject, callback, onResolvePlugins);
}
const processSetupResult = () => {
var anyOnLoad = false,
anyOnResolve = false;
for (var [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 (var [filter] of callbacks) {
this.addFilter(filter, namespace, 0);
anyOnResolve = true;
}
}
if (anyOnResolve) {
var onResolveObject = this.onResolve;
if (!onResolveObject) {
this.onResolve = onResolvePlugins;
} else {
for (var [namespace, callbacks] of onResolvePlugins.entries()) {
var existing = onResolveObject.$get(namespace) as [RegExp, AnyFunction][];
if (!existing) {
onResolveObject.$set(namespace, callbacks);
} else {
onResolveObject.$set(namespace, existing.concat(callbacks));
}
}
}
}
if (anyOnLoad) {
var onLoadObject = this.onLoad;
if (!onLoadObject) {
this.onLoad = onLoadPlugins;
} else {
for (var [namespace, callbacks] of onLoadPlugins.entries()) {
var existing = onLoadObject.$get(namespace) as [RegExp, AnyFunction][];
if (!existing) {
onLoadObject.$set(namespace, callbacks);
} else {
onLoadObject.$set(namespace, existing.concat(callbacks));
}
}
}
}
return anyOnLoad || anyOnResolve;
};
var setupResult = setup({
config: config,
onDispose: notImplementedIssueFn(2771, "On-dispose callbacks"),
onEnd: notImplementedIssueFn(2771, "On-end callbacks"),
onLoad,
onResolve,
onStart: notImplementedIssueFn(2771, "On-start callbacks"),
resolve: notImplementedIssueFn(2771, "build.resolve()"),
module: () => {
throw new TypeError("module() is not supported in Bun.build() yet. Only via Bun.plugin() at runtime");
},
// esbuild's options argument is different, we provide some interop
initialOptions: {
...config,
bundle: true,
entryPoints: config.entrypoints ?? config.entryPoints ?? [],
minify: typeof config.minify === "boolean" ? config.minify : false,
minifyIdentifiers: config.minify === true || (config.minify as MinifyObj)?.identifiers,
minifyWhitespace: config.minify === true || (config.minify as MinifyObj)?.whitespace,
minifySyntax: config.minify === true || (config.minify as MinifyObj)?.syntax,
outbase: config.root,
platform: config.target === "bun" ? "node" : config.target,
},
esbuild: {},
} satisfies PluginBuilderExt as PluginBuilder);
if (setupResult && $isPromise(setupResult)) {
if ($getPromiseInternalField(setupResult, $promiseFieldFlags) & $promiseStateFulfilled) {
setupResult = $getPromiseInternalField(setupResult, $promiseFieldReactionsOrResult);
} else {
return setupResult.$then(processSetupResult);
}
}
return processSetupResult();
}
export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespace, importer, internalID, kindId) {
// Must be kept in sync with ImportRecord.label
const kind = $ImportKindIdToLabel[kindId];
var promiseResult: any = (async (inputPath, inputNamespace, importer, kind) => {
var { onResolve, onLoad } = this;
var results = onResolve.$get(inputNamespace);
if (!results) {
this.onResolveAsync(internalID, null, null, null);
return null;
}
for (let [filter, callback] of results) {
if (filter.test(inputPath)) {
var result = callback({
path: inputPath,
importer,
namespace: inputNamespace,
// resolveDir
kind,
// pluginData
});
while (
result &&
$isPromise(result) &&
($getPromiseInternalField(result, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled
) {
result = $getPromiseInternalField(result, $promiseFieldReactionsOrResult);
}
if (result && $isPromise(result)) {
result = await result;
}
if (!result || !$isObject(result)) {
continue;
}
var { path, namespace: userNamespace = inputNamespace, external } = result;
if (!(typeof path === "string") || !(typeof userNamespace === "string")) {
throw new TypeError("onResolve plugins must return an object with a string 'path' and string 'loader' field");
}
if (!path) {
continue;
}
if (!userNamespace) {
userNamespace = inputNamespace;
}
if (typeof external !== "boolean" && !$isUndefinedOrNull(external)) {
throw new TypeError('onResolve plugins "external" field must be boolean or unspecified');
}
if (!external) {
if (userNamespace === "file") {
if (process.platform !== "win32") {
if (path[0] !== "/" || path.includes("..")) {
throw new TypeError('onResolve plugin "path" must be absolute when the namespace is "file"');
}
} else {
if (require("node:path").isAbsolute(path) === false || path.includes("..")) {
throw new TypeError('onResolve plugin "path" must be absolute when the namespace is "file"');
}
}
}
if (userNamespace === "dataurl") {
if (!path.startsWith("data:")) {
throw new TypeError('onResolve plugin "path" must start with "data:" when the namespace is "dataurl"');
}
}
if (userNamespace && userNamespace !== "file" && (!onLoad || !onLoad.$has(userNamespace))) {
throw new TypeError(`Expected onLoad plugin for namespace ${userNamespace} to exist`);
}
}
this.onResolveAsync(internalID, path, userNamespace, external);
return null;
}
}
this.onResolveAsync(internalID, null, null, null);
return null;
})(specifier, inputNamespace, importer, kind);
while (
promiseResult &&
$isPromise(promiseResult) &&
($getPromiseInternalField(promiseResult, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled
) {
promiseResult = $getPromiseInternalField(promiseResult, $promiseFieldReactionsOrResult);
}
if (promiseResult && $isPromise(promiseResult)) {
promiseResult.then(
() => {},
e => {
this.addError(internalID, e, 0);
},
);
}
}
export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespace, defaultLoaderId) {
const LOADERS_MAP = $LoaderLabelToId;
const loaderName = $LoaderIdToLabel[defaultLoaderId];
var promiseResult = (async (internalID, path, namespace, defaultLoader) => {
var results = this.onLoad.$get(namespace);
if (!results) {
this.onLoadAsync(internalID, null, null);
return null;
}
for (let [filter, callback] of results) {
if (filter.test(path)) {
var result = callback({
path,
namespace,
// suffix
// pluginData
loader: defaultLoader,
});
while (
result &&
$isPromise(result) &&
($getPromiseInternalField(result, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled
) {
result = $getPromiseInternalField(result, $promiseFieldReactionsOrResult);
}
if (result && $isPromise(result)) {
result = await result;
}
if (!result || !$isObject(result)) {
continue;
}
var { contents, loader = defaultLoader } = result as OnLoadResultSourceCode & OnLoadResultObject;
if (!(typeof contents === "string") && !$isTypedArrayView(contents)) {
throw new TypeError('onLoad plugins must return an object with "contents" as a string or Uint8Array');
}
if (!(typeof loader === "string")) {
throw new TypeError('onLoad plugins must return an object with "loader" as a string');
}
const chosenLoader = LOADERS_MAP[loader];
if (chosenLoader === undefined) {
throw new TypeError(`Loader ${loader} is not supported.`);
}
this.onLoadAsync(internalID, contents, chosenLoader);
return null;
}
}
this.onLoadAsync(internalID, null, null);
return null;
})(internalID, path, namespace, loaderName);
while (
promiseResult &&
$isPromise(promiseResult) &&
($getPromiseInternalField(promiseResult, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled
) {
promiseResult = $getPromiseInternalField(promiseResult, $promiseFieldReactionsOrResult);
}
if (promiseResult && $isPromise(promiseResult)) {
promiseResult.then(
() => {},
e => {
this.addError(internalID, e, 1);
},
);
}
}