@plugjs/plug
Version:
PlugJS Build System ===================
193 lines • 8.63 kB
JavaScript
import { basename } from 'node:path';
import { build } from 'esbuild';
import { assert } from "../asserts.js";
import { Files } from "../files.js";
import { readFile } from "../fs.js";
import { $p, ERROR, WARN } from "../logging.js";
import { getAbsoluteParent, resolveAbsolutePath } from "../paths.js";
import { install } from "../pipe.js";
export * from "./esbuild/bundle-locals.js";
export * from "./esbuild/fix-extensions.js";
/* ========================================================================== *
* INSTALLATION / IMPLEMENTATION *
* ========================================================================== */
class ESBuild {
_options;
constructor(_options) {
this._options = _options;
}
async pipe(files, context, override) {
const entryPoints = [...files];
const absWorkingDir = files.directory;
const options = {
/* 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 base directory */
outbase: absWorkingDir,
/* Merge in the caller's options */
...this._options,
...override,
/* 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;
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;
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) {
const e = error;
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, message, directory) {
const record = { 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();
/**
* 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, log) {
/* 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) {
/* 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;
}
}
/* ========================================================================== *
* ESM TO CJS CONVERSION *
* ========================================================================== */
class ModuleToCommonJS extends ESBuild {
constructor(options = {}) {
const { minify, outDir, outExtension = '.cjs' } = options;
super({
outdir: outDir,
format: 'cjs',
platform: 'node',
target: 'node22',
sourcemap: 'inline',
minify: !!minify,
outExtension: { '.js': outExtension },
banner: {
// Our banner for ESM to CJS conversion, to provide a bare-bones
// support for `import.meta.url` in CJS modules.
js: `var __importMetaStub = {
get url() { return require("node:url").pathToFileURL(__filename) }
};`.replaceAll(/\n\s+/g, ' '),
},
define: {
'import.meta.url': '__importMetaStub.url',
'import.meta.filename': '__filename',
'import.meta.dirname': '__dirname',
},
});
}
async pipe(files, context) {
// If a directory was not specified at construction time, we default the
// output directory to the same as the input files...
const override = this._options.outdir ? {} : {
outdir: files.directory,
};
// Do It!
return super.pipe(files, context, override);
}
}
/* ========================================================================== *
* INSTALL PLUGS *
* ========================================================================== */
install('esbuild', ESBuild);
install('esm2cjs', ModuleToCommonJS);
//# sourceMappingURL=esbuild.js.map