UNPKG

@build-script/heft-esbuild-plugin

Version:

lowlevel esbuild plugin for [heft]

210 lines (181 loc) 6.58 kB
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; }