Skip to content

Commit

Permalink
Use ESBuild for minification
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Oct 17, 2021
1 parent 1c4d39c commit 43ed445
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 70 deletions.
4 changes: 3 additions & 1 deletion packages/webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,19 @@
"@types/supports-color": "^8.1.0",
"babel-loader": "^8.2.2",
"babel-preset-env": "^1.7.0",
"supports-color": "^8.1.0",
"css-loader": "^5.2.6",
"csso": "^4.2.0",
"debug": "^4.3.2",
"esbuild": "^0.13.7",
"esbuild-loader": "^2.16.0",
"fs-extra": "^9.1.0",
"jsdom": "^16.6.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^1.6.0",
"semver": "^7.3.5",
"source-map-url": "^0.4.1",
"style-loader": "^2.0.0",
"supports-color": "^8.1.0",
"terser": "^5.7.0",
"thread-loader": "^3.0.4"
},
Expand Down
84 changes: 66 additions & 18 deletions packages/webpack/src/ember-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,14 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import makeDebug from 'debug';
import { format } from 'util';
import { warmup as threadLoaderWarmup } from 'thread-loader';
import { Options, BabelLoaderOptions } from './options';
import { Options, BabelLoaderOptions, EsbuildMinifyOptions } from './options';
import crypto from 'crypto';
import type { HbsLoaderConfig } from '@embroider/hbs-loader';
import semverSatisfies from 'semver/functions/satisfies';
import supportsColor from 'supports-color';

const debug = makeDebug('embroider:debug');

// This is a type-only import, so it gets compiled away. At runtime, we load
// terser lazily so it's only loaded for production builds that use it. Don't
// add any non-type-only imports here.
import type { MinifyOptions } from 'terser';

interface AppInfo {
entrypoints: HTMLEntrypoint[];
otherAssets: string[];
Expand Down Expand Up @@ -76,6 +71,7 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
private publicAssetURL: string | undefined;
private extraThreadLoaderOptions: object | false | undefined;
private extraBabelLoaderOptions: BabelLoaderOptions | undefined;
private extraEsbuildMinifyOptions: EsbuildMinifyOptions | undefined;

constructor(
pathToVanillaApp: string,
Expand All @@ -93,12 +89,13 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
this.publicAssetURL = options?.publicAssetURL;
this.extraThreadLoaderOptions = options?.threadLoaderOptions;
this.extraBabelLoaderOptions = options?.babelLoaderOptions;
this.extraEsbuildMinifyOptions = options?.esbuildMinifyOptions;
warmUp(this.extraThreadLoaderOptions);
}

async build(): Promise<void> {
let appInfo = this.examineApp();
let webpack = this.getWebpack(appInfo);
let webpack = await this.getWebpack(appInfo);
let stats = this.summarizeStats(await this.runWebpack(webpack));
await this.writeFiles(stats, appInfo);
}
Expand All @@ -124,10 +121,10 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
return { entrypoints, otherAssets, templateCompiler, babel, rootURL, resolvableExtensions, publicAssetURL };
}

private configureWebpack(
private async configureWebpack(
{ entrypoints, templateCompiler, babel, resolvableExtensions, publicAssetURL }: AppInfo,
variant: Variant
): Configuration {
): Promise<Configuration> {
let entry: { [name: string]: string } = {};
for (let entrypoint of entrypoints) {
for (let moduleName of entrypoint.modules) {
Expand All @@ -140,7 +137,7 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
variant,
};

return {
const config: Configuration = {
mode: variant.optimizeForProduction ? 'production' : 'development',
context: this.pathToVanillaApp,
entry,
Expand Down Expand Up @@ -212,19 +209,43 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
},
},
};

if (variant.optimizeForProduction) {
let [esBuildLoader, esbuild] = await Promise.all([import('esbuild-loader'), import('esbuild')]);

let optimization = config.optimization as Required<Configuration>['optimization'];

optimization.minimizer = [
// there are settings to this plugin that might be very important,
// depending upon the app -- legalComments, sourcemap, exclude, etc.
// if an app wishes to customize those, they'll have to override
// the minimizer plugin and re-declare this whole array
new esBuildLoader.ESBuildMinifyPlugin({
minify: variant.optimizeForProduction,
css: true,
// So we can contral when esbuild updates
implementation: esbuild,
}),
];
}

return config;
}

private lastAppInfo: AppInfo | undefined;
private lastWebpack: webpack.MultiCompiler | undefined;

private getWebpack(appInfo: AppInfo) {
private async getWebpack(appInfo: AppInfo) {
if (this.lastWebpack && this.lastAppInfo && equalAppInfo(appInfo, this.lastAppInfo)) {
debug(`reusing webpack config`);
return this.lastWebpack;
}
debug(`configuring webpack`);
let config = this.variants.map(variant =>
mergeWith({}, this.configureWebpack(appInfo, variant), this.extraConfig, appendArrays)

let config = await Promise.all(
this.variants.map(async variant =>
mergeWith({}, await this.configureWebpack(appInfo, variant), this.extraConfig, appendArrays)
)
);
this.lastAppInfo = appInfo;
return (this.lastWebpack = webpack(config));
Expand All @@ -238,15 +259,20 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager

// loading these lazily here so they never load in non-production builds.
// The node cache will ensures we only load them once.
const [Terser, srcURL] = await Promise.all([import('terser'), import('source-map-url')]);
let [esbuild, srcURL] = await Promise.all([import('esbuild'), import('source-map-url')]);

let inCode = readFileSync(join(this.pathToVanillaApp, script), 'utf8');
let terserOpts: MinifyOptions = {};
let inFile = join(this.pathToVanillaApp, script);
let inCode = readFileSync(inFile, 'utf8');
let esbuildMinifyOptions: EsbuildMinifyOptions = {
...this.extraEsbuildMinifyOptions,
};
let fileRelativeSourceMapURL;
let appRelativeSourceMapURL;

if (srcURL.default.existsIn(inCode)) {
fileRelativeSourceMapURL = srcURL.default.getFrom(inCode)!;
appRelativeSourceMapURL = join(dirname(script), fileRelativeSourceMapURL);

let content;
try {
content = readJsonSync(join(this.pathToVanillaApp, appRelativeSourceMapURL));
Expand All @@ -255,17 +281,39 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
// the map out.
}
if (content) {
terserOpts.sourceMap = { content, url: fileRelativeSourceMapURL };
esbuildMinifyOptions.sourcemap = 'external';
esbuildMinifyOptions.sourceRoot = fileRelativeSourceMapURL;
}
}
let { code: outCode, map: outMap } = await Terser.default.minify(inCode, terserOpts);

let outFile = inFile + '.out';
// convention from esbuild: https://esbuild.github.io/api/#sourcemap
let outMapFile = outFile + '.map';
// NOTE: the awaited return value from the build api only contains lists of errors and/or warnings
// (no output file info)
await esbuild.build({
entryPoints: [inFile],
outfile: outFile,
// gives us (not very helpful) info about the output files
metafile: true,
// code is alreday as bundled as it needs to be
bundle: false,
minify: true,
// treeShaking: true, // should only be enabled on full static builds
...esbuildMinifyOptions,
});

let outCode = readFileSync(outFile, 'utf-8');
let outMap = readFileSync(outMapFile, 'utf-8');
let finalFilename = this.getFingerprintedFilename(script, outCode!);
outputFileSync(join(this.outputPath, finalFilename), outCode!);
written.add(script);

if (appRelativeSourceMapURL && outMap) {
outputFileSync(join(this.outputPath, appRelativeSourceMapURL), outMap);
written.add(appRelativeSourceMapURL);
}

return finalFilename;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/webpack/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Configuration } from 'webpack';

import type * as esbuild from 'esbuild';

// [babel-loader](https://webpack.js.org/loaders/babel-loader/#options) specific options.
// This does not include the babel configuration, which is pulled from the app, only the
// additional options that `babel-loader` supports.
Expand All @@ -10,6 +12,12 @@ export interface BabelLoaderOptions {
customize?: string;
}

export interface EsbuildMinifyOptions {
publicPath?: string;
sourceRoot?: string;
sourcemap?: esbuild.BuildOptions['sourcemap'];
}

export interface Options {
webpackConfig: Configuration;

Expand All @@ -30,4 +38,5 @@ export interface Options {
threadLoaderOptions?: object | false;

babelLoaderOptions?: BabelLoaderOptions;
esbuildMinifyOptions?: EsbuildMinifyOptions;
}
Loading

0 comments on commit 43ed445

Please sign in to comment.