UNPKG

@plugjs/plug

Version:
207 lines (167 loc) 7.62 kB
import { basename } from 'node:path' import { build } from 'esbuild' import { assert } from '../asserts' import { Files } from '../files' import { readFile } from '../fs' import { $p, ERROR, WARN } from '../logging' import { getAbsoluteParent, resolveAbsolutePath } from '../paths' import { install } from '../pipe' import type { BuildFailure, BuildOptions, BuildResult, Format, Message, Metafile } from 'esbuild' import type { FilesBuilder } from '../files' import type { Logger, ReportLevel, ReportRecord } from '../logging' import type { AbsolutePath } from '../paths' import type { Context, PipeParameters, Plug } from '../pipe' export type ESBuildOptions = Omit<BuildOptions, 'absWorkingDir' | 'entryPoints' | 'watch'> export * from './esbuild/bundle-locals' export * from './esbuild/fix-extensions' /* * Type definition for `WebAssembly`. This is normally provided to TypeScript * by `lib.dom.d.ts`, and is not defined by Node's own types. * * https://github.com/evanw/esbuild/issues/2388 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars declare const WebAssembly: { Module: any, } declare module '../index' { export interface Pipe { /** * Transpile and bundle with {@link https://esbuild.github.io/ esbuild}. * * For documentation on the _options_ to pass to _esbuild_ refer to its * {@link https://esbuild.github.io/api/#build-api documentation}. * * @param options Build {@link ESBuildOptions | options} to pass to esbuild. * */ esbuild(options: ESBuildOptions): Pipe } } /* ========================================================================== * * INSTALLATION / IMPLEMENTATION * * ========================================================================== */ install('esbuild', class ESBuild implements Plug<Files> { constructor(...args: PipeParameters<'esbuild'>) constructor(private readonly _options: ESBuildOptions) {} async pipe(files: Files, context: Context): Promise<Files> { const entryPoints = [ ...files ] const absWorkingDir = files.directory const options: BuildOptions = { /* Default our platform/target to NodeJS, current major version */ platform: 'node', target: `node${process.versions['node'].split('.')[0]}`, /* The default format (if not specified in options) is from package.json */ format: this._options.format || await _moduleFormat(files.directory, context.log), /* Output bese directory */ outbase: absWorkingDir, /* Merge in the caller's options */ ...this._options, /* Always override */ absWorkingDir, entryPoints, logLevel: 'silent', } if (options.format === 'cjs') { options.define = Object.assign({ __fileurl: '__filename' }, options.define) } else if (options.format === 'esm') { options.define = Object.assign({ __fileurl: 'import.meta.url' }, options.define) } /* Sanity check on output file/directory */ assert(!(options.outdir && options.outfile), 'Options "outfile" and "outdir" can not coexist') /* Where to write, where to write? */ let builder: FilesBuilder if (options.bundle && options.outfile && (entryPoints.length === 1)) { builder = Files.builder(absWorkingDir) const outputFile = resolveAbsolutePath(absWorkingDir, options.outfile) const entryPoint = resolveAbsolutePath(absWorkingDir, entryPoints[0]!) options.outfile = outputFile context.log.debug('Bundling', $p(entryPoint), 'into', $p(outputFile)) } else { assert(options.outdir, 'Option "outdir" must be specified') builder = Files.builder(context.resolve(options.outdir)) options.outdir = builder.directory const message = options.bundle ? 'Bundling' : 'Transpiling' context.log.debug(message, entryPoints.length, 'files to', $p(builder.directory)) } const report = context.log.report('ESBuild Report') context.log.trace('Running ESBuild', options) let esbuild: undefined | (BuildResult & { metafile: Metafile }) try { esbuild = await build({ ...options, metafile: true }) context.log.trace('ESBuild Results', esbuild) report.add(...esbuild.warnings.map((m) => convertMessage(WARN, m, absWorkingDir))) report.add(...esbuild.errors.map((m) => convertMessage(ERROR, m, absWorkingDir))) } catch (error: any) { const e = error as BuildFailure if (e.warnings) report.add(...e.warnings.map((m) => convertMessage(WARN, m, absWorkingDir))) if (e.errors) report.add(...e.errors.map((m) => convertMessage(ERROR, m, absWorkingDir))) } await report.loadSources() report.done() assert(esbuild, 'ESBuild did not produce any result') for (const file in esbuild.metafile.outputs) { builder.add(resolveAbsolutePath(absWorkingDir, file)) } const result = builder.build() context.log.info('ESBuild produced', result.length, 'files into', $p(result.directory)) return result } }) function convertMessage(level: ReportLevel, message: Message, directory: AbsolutePath): ReportRecord { const record: ReportRecord = { level, message: message.text } record.tags = [ message.id, message.pluginName ].filter((tag) => !! tag) if (message.location) { record.line = message.location.line record.column = message.location.column + 1 record.length = message.location.length record.file = resolveAbsolutePath(directory, message.location.file) } return record } /* ========================================================================== * * DEFAULT MODULE FORMAT FROM PACKAGE.JSON * * ========================================================================== */ /** Cache for directory to module format as discovered in "package.json" */ const _moduleFormatCache = new Map<AbsolutePath, Format>() /** * Figures out the _default_ module type for a directory, looking into the * `package.json`'s `type` field (either `commonjs` or `module`) */ async function _moduleFormat(directory: AbsolutePath, log: Logger): Promise<Format> { /* Before doing anything else, check our cache */ const type = _moduleFormatCache.get(directory) if (type) return type /* Try to read the "package.json" file from this directory */ const file = resolveAbsolutePath(directory, 'package.json') try { const json = await readFile(file, 'utf-8') const data = JSON.parse(json) /* Be liberal in what you accept? Default to CommonJS if none found */ const type = data.type === 'module' ? 'esm' : 'cjs' log.debug(`File "${file}" defines module type as "${data.type}" (${type})`) _moduleFormatCache.set(directory, type) return type } catch (cause: any) { /* We _accept_ a couple of errors, file not found, or file is directory */ if ((cause.code !== 'ENOENT') && (cause.code !== 'EISDIR')) throw cause } /* * We couldn't find "package.json" in this directory, go up if we can! * * That said, if we are at a directory called "node_modules" we stop here, * as we don't want to inherit the default type from an _importing_ package, * into the _imported_ one... */ const name = basename(directory) const parent = getAbsoluteParent(directory) if ((name === 'node_modules') || (parent === directory)) { _moduleFormatCache.set(directory, 'cjs') // default return 'cjs' } else { /* We also cache back, on the way up */ const type = await _moduleFormat(parent, log) _moduleFormatCache.set(directory, type) return type } }