UNPKG

fusion-cli

Version:
221 lines (204 loc) • 6.96 kB
// @flow /* eslint-env node */ // Adapted from https://github.com/privatenumber/esbuild-loader/blob/6bae9a77af3529216b7377658ff942e5895dca62/src/minify-plugin.ts const {version, transform, buildSync} = require('esbuild'); const {CachedSource, SourceMapSource, RawSource} = require('webpack').sources; const JavascriptModulesPlugin = require('webpack/lib/javascript/JavascriptModulesPlugin'); const ModuleFilenameHelpers = require('webpack/lib/ModuleFilenameHelpers.js'); const isJsFile = /\.[cm]?js(\?.*)?$/i; const pluginName = 'esbuild-minify'; const granularMinifyConfigs = [ 'minifyIdentifiers', 'minifySyntax', 'minifyWhitespace', ]; /*:: type EsbuildTransformOptions = { format?: string; target?: string; sourcemap?: boolean | "external"; keepNames?: boolean; minify?: boolean; minifyWhitespace?: boolean; minifyIdentifiers?: boolean; minifySyntax?: boolean; } type EsbuildMinifyPluginOptions = { exclude?: string; include?: string; transformOptions?: EsbuildTransformOptions; } */ class EsbuildMinifyPlugin { /*:: options: EsbuildMinifyPluginOptions; */ constructor(options /*:EsbuildMinifyPluginOptions*/ = {}) { const {exclude, include, transformOptions = {}} = options; const hasGranularMinificationConfig = granularMinifyConfigs.some( (minifyConfig) => minifyConfig in transformOptions ); this.options = { exclude, include, transformOptions: { ...transformOptions, ...(hasGranularMinificationConfig ? null : { minify: true, }), }, }; } apply(compiler /*: any */) { const transformOptions /*: EsbuildTransformOptions*/ = { // esbuild may include some helper methods to the outer-most scope, // need to configure it to wrap the output in an IIFE, in order to // prevent global namespace pollution // $FlowFixMe ...(compiler.options.target === 'web' ? { format: 'iife', } : null), sourcemap: compiler.options.devtool && compiler.options.devtool.includes('source-map') ? 'external' : false, ...this.options.transformOptions, }; // Used for hashing const meta = JSON.stringify({ name: pluginName, version, transformOptions, }); // Original plugin taps into `compilation` hook that makes it impossible // to use different esbuild options between legacy / modern compilations. compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { // esbuild does not perform dead code elimination inside function bodies, // while webpack tree-shaking relies on the minifier to remove unused code. // Hence need to tap into individual module code generation, where content // is unwrapped so that we can let esbuild eliminate unused module exports. // @see: https://github.com/evanw/esbuild/issues/639#issuecomment-753683962 const moduleHooks = JavascriptModulesPlugin.getCompilationHooks(compilation); moduleHooks.renderModuleContent.tap( pluginName, (moduleSource, module, renderContext) => { const {source, map} = moduleSource.sourceAndMap(); const sourceAsString = source.toString(); const resource = module.resource || 'sourced-module.js'; const { // Ignore format option while transforming individual modules format, ...moduleTransformOptions } = transformOptions; // Unfortunately the hook we're tapping into is not async, which forces us // to use esbuild's sync API. We're also using build API with `bundle` option // enabled, because esbuild's transform API does not perform any kind of DCE. const result = buildSync({ ...moduleTransformOptions, bundle: true, external: ['*'], // format is set to `cjs` so there's no additional code addedd (i.e. IIFE) format: 'cjs', stdin: { contents: sourceAsString, sourcefile: resource, }, outfile: resource, write: false, }).outputFiles.reduce( (acc, file) => { if (file.path.endsWith('.map')) { acc.map = file.text; } else { acc.code = file.text; } return acc; }, { code: '', map: '', } ); const nextSource = result.map ? new SourceMapSource( result.code, resource, result.map, sourceAsString, map, true ) : new RawSource(result.code); return new CachedSource(nextSource); } ); moduleHooks.chunkHash.tap(pluginName, (chunk, hash) => hash.update(meta)); compilation.hooks.processAssets.tapPromise( { name: pluginName, stage: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, additionalAssets: true, }, async () => await this.transformAssets(compilation, transformOptions) ); compilation.hooks.statsPrinter.tap(pluginName, (statsPrinter) => { statsPrinter.hooks.print .for('asset.info.minimized') .tap(pluginName, (minimized, {green, formatFlag}) => minimized ? green(formatFlag('minimized')) : undefined ); }); compilation.hooks.chunkHash.tap(pluginName, (chunk, hash) => hash.update(meta) ); }); } async transformAssets( compilation /*: any*/, transformOptions /*: EsbuildTransformOptions */ ) { const {include, exclude} = this.options; const assets = compilation.getAssets().filter( (asset) => // Filter out already minimized !asset.info.minimized && // Filter out by file type isJsFile.test(asset.name) && ModuleFilenameHelpers.matchObject({include, exclude}, asset.name) ); await Promise.all( assets.map(async (asset) => { const {source, map} = asset.source.sourceAndMap(); const sourceAsString = source.toString(); const result = await transform(sourceAsString, { ...transformOptions, sourcefile: asset.name, }); compilation.updateAsset( asset.name, result.map ? new SourceMapSource( result.code, asset.name, result.map, sourceAsString, map, true ) : new RawSource(result.code), { ...asset.info, minimized: true, } ); }) ); } } module.exports = EsbuildMinifyPlugin;