@build-script/heft-esbuild-plugin
Version:
lowlevel esbuild plugin for [heft]
210 lines (181 loc) • 6.58 kB
text/typescript
import { normalizePath } from '@idlebox/common';
import type { HeftConfiguration, IHeftTaskPlugin, IHeftTaskSession } from '@rushstack/heft';
import { FileError } from '@rushstack/node-core-library';
import type { BuildContext, BuildOptions, BuildResult, Message, Note } from 'esbuild';
import { type FilterdBuildOptions, filterOptions } from './common/config.js';
import { executeConfigFile } from './common/execute-config.js';
import { s } from './common/misc.js';
import type { ESBuildPublicApi, IGlobalSession } from './common/type.js';
import { createEmitter, type IProjectEmitter } from './common/write.js';
export const PLUGIN_NAME = 'esbuild';
function emitResultErrors(result: BuildResult, session: IHeftTaskSession, rootDir: string) {
const createError = (item: Message | Note) => {
let e: Error;
let msg: string;
if ('id' in item) {
msg = `${item.pluginName} [${item.id}] ${item.text}`;
} else {
msg = `[Note] ${item.text}`;
}
if (item.location) {
e = new FileError(msg, {
projectFolder: rootDir,
absolutePath: item.location.file,
line: item.location.line,
column: item.location.column + 1,
});
} else {
e = new Error(msg);
}
e.stack = e.message;
return e;
};
for (const error of result.errors) {
session.logger.emitError(createError(error));
for (const note of error.notes) {
session.logger.emitWarning(createError(note));
}
}
for (const warn of result.warnings) {
session.logger.emitWarning(createError(warn));
for (const note of warn.notes) {
session.logger.emitWarning(createError(note));
}
}
}
export default class ESBuildPlugin implements IHeftTaskPlugin {
private declare rootDir: string;
static readonly PLUGIN_NAME = PLUGIN_NAME;
readonly PLUGIN_NAME = PLUGIN_NAME;
apply(session: IHeftTaskSession, configuration: HeftConfiguration, userInput: any): void {
this.rootDir = normalizePath(configuration.buildFolderPath);
session.hooks.run.tapPromise(PLUGIN_NAME, async () => {
const contexts = await this.getContext(session, configuration, userInput ?? {});
let writtenFiles = 0;
const allMeta = await Promise.all(
contexts.map(async ({ context, options, emitter }) => {
const result = await context.rebuild();
emitResultErrors(result, session, this.rootDir);
const ret = await emitter({
metafile: result.metafile,
outputFiles: result.outputFiles,
options,
session,
});
writtenFiles += ret.writtenFiles;
return result.metafile;
})
);
for (const e of allMeta) {
session.logger.terminal.writeLine(this._esbuild.analyzeMetafileSync(e));
}
showTip(contexts, writtenFiles);
await Promise.all(contexts.map(({ context }) => context.dispose()));
});
session.hooks.runIncremental.tapPromise(PLUGIN_NAME, async () => {
const contexts = await this.getContext(session, configuration, userInput ?? {});
let writtenFiles = 0;
const allMeta = await Promise.all(
contexts.map(async ({ context, options, emitter }) => {
const result = await context.rebuild();
emitResultErrors(result, session, this.rootDir);
const ret = await emitter({
metafile: result.metafile,
outputFiles: result.outputFiles,
options,
session,
});
writtenFiles += ret.writtenFiles;
return result.metafile;
})
);
for (const e of allMeta) {
session.logger.terminal.writeVerboseLine(this._esbuild.analyzeMetafileSync(e));
}
showTip(contexts, writtenFiles);
});
function showTip(contexts: IContext[], writtenFiles: number) {
const t1 = s(contexts.length, 'project');
const t2 = s(writtenFiles, 'file');
session.logger.terminal.writeLine(`complete build ${t1}, ${t2}.`);
}
}
private async _initOptions(
esbuild: ESBuildPublicApi,
session: IHeftTaskSession,
configuration: HeftConfiguration,
userInput: any
) {
let configFile: string | undefined;
for (const ext of ['.cts', '.mts', '.ts', '.cjs', '.mjs', '.json']) {
configFile = configuration.rigConfig.tryResolveConfigFilePath(`config/esbuild${ext}`);
if (configFile) break;
}
if (!configFile) {
throw new Error(
'missing config file: config/esbuild.{ts|cjs|mjs|json}, it must `export const options: import("esbuild").BuildOptions` (or it\'s array)'
);
}
const configSession: IGlobalSession = {
esbuild,
logger: session.logger,
taskName: session.taskName,
rootDir: this.rootDir,
options: userInput,
tempFolderPath: session.tempFolderPath,
resolve(packageName: string) {
return configuration.rigPackageResolver.resolvePackageAsync(packageName, session.logger.terminal);
},
rigConfig: configuration.rigConfig,
watchFiles(_files: string[]) {
//TODO
},
};
Object.assign(globalThis, { session: configSession });
const { onEmit, options } = await executeConfigFile(configFile, session, configuration);
Object.assign(globalThis, { session: undefined });
session.logger.terminal.writeVerboseLine(JSON.stringify(options, null, 4));
return {
optionsArray: options.map((e) => filterOptions(this.rootDir, e)),
onEmit,
};
}
private declare _esbuild: ESBuildPublicApi;
private async getEsbuild(session: IHeftTaskSession, configuration: HeftConfiguration): Promise<ESBuildPublicApi> {
if (!this._esbuild) {
session.logger.terminal.writeDebug('loading esbuild: ');
const esbuildPath = await configuration.rigPackageResolver.resolvePackageAsync(
'esbuild',
session.logger.terminal
);
session.logger.terminal.writeDebugLine(esbuildPath);
this._esbuild = require(esbuildPath);
}
return this._esbuild;
}
private _contexts?: Promise<IContext[]>;
getContext(session: IHeftTaskSession, configuration: HeftConfiguration, userInput: object) {
if (!this._contexts) {
this._contexts = this._getContext(session, configuration, userInput);
}
return this._contexts;
}
async _getContext(session: IHeftTaskSession, configuration: HeftConfiguration, userInput: object) {
const esbuild = await this.getEsbuild(session, configuration);
const { optionsArray, onEmit } = await this._initOptions(esbuild, session, configuration, userInput);
const contexts: IContext[] = [];
for (const options of optionsArray) {
contexts.push({
context: await esbuild.context<BuildOptions>(options),
options,
emitter: createEmitter(onEmit),
});
}
return contexts;
}
}
interface IContext {
readonly context: BuildContext<FilterdBuildOptions>;
readonly options: Readonly<FilterdBuildOptions>;
readonly emitter: IProjectEmitter;
}