'use strict'; var esbuild = require('esbuild'); var path = require('path'); var core = require('@babel/core'); function es5PluginInit() { return function es5Plugin(files, ms, done) { const isDev = ms.env('NODE_ENV') === 'development'; const transforms = []; const options = core.loadOptions({ envName: ms.env('NODE_ENV'), presets: [['@babel/preset-env', { useBuiltIns: 'entry', corejs: '3.22' }]], targets: '> 0.25%, not dead', minified: !isDev, comments: isDev, cwd: ms.directory() //inputSourceMap }); Object.values(files).forEach(file => { if (file.__babelPostProcess) { transforms.push(new Promise((resolve, reject) => { core.transform(file.contents.toString(), options, (err, result) => { if (err) reject(err); file.contents = Buffer.from(result.code); delete file.__babelPostProcess; resolve(); }); })); } }); Promise.all(transforms).then(() => done()).catch(done); }; } const debugNs = '@metalsmith/js-bundle'; /** * @typedef {import('esbuild').BuildOptions} Options */ /** * Normalize plugin options * @param {Options} [options] * @param {import('metalsmith').Metalsmith} metalsmith * @returns {Object} */ function normalizeOptions(options = {}, metalsmith, debug) { const entryPoints = options.entries || {}; const msDir = metalsmith.directory(); const isProd = metalsmith.env('NODE_ENV') !== 'development'; const define = Object.entries(options.define || metalsmith.env()).reduce((acc, [name, value]) => { if (typeof value === 'undefined') { debug.warn('Define option "%s" value is undefined', name); acc[`process.env.${name}`] = 'undefined'; return acc; } // see notes at https://esbuild.github.io/api/#define, string values require explicit quotes acc[`process.env.${name}`] = typeof value === 'string' ? `'${value}'` : ['boolean', 'number', 'function'].includes(typeof value) ? value.toString() : JSON.stringify(value); return acc; }, {}); /** @type {Options} */ const defaults = { bundle: true, minify: isProd, sourcemap: !isProd, platform: 'browser', target: 'es6', assetNames: '[dir]/[name]', drop: isProd ? ['console', 'debugger'] : [] }; /** @type {Options} */ const overwrites = { entryPoints, absWorkingDir: msDir, outdir: path.relative(msDir, metalsmith.destination()), write: false, metafile: true, define }; // eslint-disable-next-line no-unused-vars const { entries, ...otherOptions } = options; return { ...defaults, ...otherOptions, ...overwrites }; } /** * A metalsmith plugin that bundles your JS using [esbuild](https://esbuild.github.io) * @example * * metalsmith.use( * jsBundle({ entries: { * index: 'lib/index.js', * 'second/src/file': 'second/bundle/output.js' * }}) * ) * @param {Options} options * @returns {import('metalsmith').Plugin} */ function initJsBundle(options = {}) { // esbuild does not support ES5 compilation // to avoid it throw an error, force at least ES6 and let esbuild do all the bundling, then postprocess and minify with babel let babelPostProcess = false; if (options.target === 'es5') { babelPostProcess = true; options.target = 'es6'; options.minify = false; } return function jsBundle(files, metalsmith, done) { const debug = metalsmith.debug(debugNs); const normalizedOptions = normalizeOptions(options, metalsmith, debug); const entrypoints = normalizedOptions.entryPoints; debug('Running with options %O', normalizedOptions); if (Object.keys(entrypoints).length === 0) { debug.warn('No files to process, skipping.'); done(); return; } const src = metalsmith.source(); const dest = metalsmith.destination(); const isFullyInSource = Object.values(entrypoints).every(sourcepath => { return metalsmith.path(sourcepath).startsWith(src); }); if (isFullyInSource) { debug.info('All entries to bundle are in metalsmith.source(), setting `outbase` to metalsmith.source()'); normalizedOptions.outbase = src; } normalizedOptions.entryPoints = Object.entries(entrypoints).reduce((mapped, current) => { const [dest, src] = current; mapped[dest] = src; return mapped; }, {}); const sourceRelPath = path.relative(metalsmith.directory(), src); esbuild.build(normalizedOptions).then(result => { // the lines below until #L138 must be revisited, they can definitely be simplified. // esbuild outputFiles return absolute paths, while metafile.outputs has a { 'output/path': { inputs: { 'input/path' }, imports: {...}}} format // furthermore it looks like 'file' loader inputs will NOT be removed from the build debug('Finished processing files %O', result.outputFiles.map(o => path.relative(dest, o.path))); // first read esbuild metafile to remove the compilation inputs from the build Object.values(result.metafile.outputs).forEach(o => { if (o.inputs) { metalsmith.match(`${sourceRelPath}/**`, Object.keys(o.inputs)).forEach(input => { delete files[path.relative(src, metalsmith.path(input))]; }); } }); debug('Adding processed files to build'); result.outputFiles.forEach(file => { // strip the metalsmith.destination() const destPath = path.relative(dest, file.path); files[destPath] = { contents: Buffer.from(file.contents.buffer) }; if (babelPostProcess && path.extname(destPath) === '.js') { files[destPath].__babelPostProcess = true; } }); if (babelPostProcess) { debug('Post-processing with babel'); es5PluginInit()(files, metalsmith, done); } else { done(); } }).catch(err => { done(err); }); }; } module.exports = initJsBundle; //# sourceMappingURL=index.cjs.map