Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12.2.x] fix(@angular-devkit/build-angular): support WASM-based esbuild optimizer fallback #21789

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@
"css-loader": "6.2.0",
"css-minimizer-webpack-plugin": "3.0.2",
"debug": "^4.1.1",
"esbuild": "0.12.24",
"esbuild": "0.12.29",
"esbuild-wasm": "0.12.29",
"eslint": "7.31.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-header": "3.1.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/angular_devkit/build_angular/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ ts_library(
include = [
"package.json",
"builders.json",
"esbuild-check.js",
"src/**/schema.json",
"src/**/*.js",
"src/**/*.html",
Expand Down Expand Up @@ -150,6 +151,7 @@ ts_library(
"@npm//css-loader",
"@npm//css-minimizer-webpack-plugin",
"@npm//esbuild",
"@npm//esbuild-wasm",
"@npm//find-cache-dir",
"@npm//glob",
"@npm//https-proxy-agent",
Expand Down
16 changes: 16 additions & 0 deletions packages/angular_devkit/build_angular/esbuild-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

// If the platform does not support the native variant of esbuild, this will crash.
// This script can then be spawned by the CLI to determine if native usage is supported.
require('esbuild')
.formatMessages([], { kind: 'error ' })
.then(
() => {},
() => {},
);
5 changes: 4 additions & 1 deletion packages/angular_devkit/build_angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"critters": "0.0.10",
"css-loader": "6.2.0",
"css-minimizer-webpack-plugin": "3.0.2",
"esbuild": "0.12.24",
"esbuild-wasm": "0.12.29",
"find-cache-dir": "3.3.1",
"glob": "7.1.7",
"https-proxy-agent": "5.0.0",
Expand Down Expand Up @@ -76,6 +76,9 @@
"webpack-merge": "5.8.0",
"webpack-subresource-integrity": "1.5.2"
},
"optionalDependencies": {
"esbuild": "0.12.29"
},
"peerDependencies": {
"@angular/compiler-cli": "^12.0.0",
"@angular/localize": "^12.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
RemoveHashPlugin,
SuppressExtractedTextChunksWebpackPlugin,
} from '../plugins';
import { EsbuildExecutor } from '../plugins/esbuild-executor';
import {
assetNameTemplateFactory,
getOutputHashFormat,
Expand Down Expand Up @@ -277,7 +278,7 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio
const extraMinimizers = [];
if (buildOptions.optimization.styles.minify) {
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const esbuild = require('esbuild') as typeof import('esbuild');
const esbuild = new EsbuildExecutor();

const cssnanoOptions = {
preset: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { spawnSync } from 'child_process';
import type {
FormatMessagesOptions,
PartialMessage,
TransformOptions,
TransformResult,
} from 'esbuild';
import * as path from 'path';

/**
* Provides the ability to execute esbuild regardless of the current platform's support
* for using the native variant of esbuild. The native variant will be preferred (assuming
* the `alwaysUseWasm` constructor option is `false) due to its inherent performance advantages.
* At first use of esbuild, a supportability test will be automatically performed and the
* WASM-variant will be used if needed by the platform.
*/
export class EsbuildExecutor
implements Pick<typeof import('esbuild'), 'transform' | 'formatMessages'>
{
private esbuildTransform: this['transform'];
private esbuildFormatMessages: this['formatMessages'];
private initialized = false;

/**
* Constructs an instance of the `EsbuildExecutor` class.
*
* @param alwaysUseWasm If true, the WASM-variant will be preferred and no support test will be
* performed; if false (default), the native variant will be preferred.
*/
constructor(private alwaysUseWasm = false) {
this.esbuildTransform = this.esbuildFormatMessages = () => {
throw new Error('esbuild implementation missing');
};
}

/**
* Determines whether the native variant of esbuild can be used on the current platform.
*
* @returns True, if the native variant of esbuild is support; False, if the WASM variant is required.
*/
static hasNativeSupport(): boolean {
// Try to use native variant to ensure it is functional for the platform.
// Spawning a separate esbuild check process is used to determine if the native
// variant is viable. If check fails, the WASM variant is initialized instead.
// Attempting to call one of the native esbuild functions is not a viable test
// currently since esbuild spawn errors are currently not propagated through the
// call stack for the esbuild function. If this limitation is removed in the future
// then the separate process spawn check can be removed in favor of a direct function
// call check.
try {
const { status, error } = spawnSync(process.execPath, [
path.join(__dirname, '../../../esbuild-check.js'),
]);

return status === 0 && error === undefined;
} catch {
return false;
}
}

/**
* Initializes the esbuild transform and format messages functions.
*
* @returns A promise that fulfills when esbuild has been loaded and available for use.
*/
private async ensureEsbuild(): Promise<void> {
if (this.initialized) {
return;
}

// If the WASM variant was preferred at class construction or native is not supported, use WASM
if (this.alwaysUseWasm || !EsbuildExecutor.hasNativeSupport()) {
await this.useWasm();
this.initialized = true;

return;
}

try {
// Use the faster native variant if available.
const { transform, formatMessages } = await import('esbuild');

this.esbuildTransform = transform;
this.esbuildFormatMessages = formatMessages;
} catch {
// If the native variant is not installed then use the WASM-based variant
await this.useWasm();
}

this.initialized = true;
}

/**
* Transitions an executor instance to use the WASM-variant of esbuild.
*/
private async useWasm(): Promise<void> {
const { transform, formatMessages } = await import('esbuild-wasm');
this.esbuildTransform = transform;
this.esbuildFormatMessages = formatMessages;

// The ESBUILD_BINARY_PATH environment variable cannot exist when attempting to use the
// WASM variant. If it is then the binary located at the specified path will be used instead
// of the WASM variant.
delete process.env.ESBUILD_BINARY_PATH;

this.alwaysUseWasm = true;
}

async transform(input: string, options?: TransformOptions): Promise<TransformResult> {
await this.ensureEsbuild();

return this.esbuildTransform(input, options);
}

async formatMessages(
messages: PartialMessage[],
options: FormatMessagesOptions,
): Promise<string[]> {
await this.ensureEsbuild();

return this.esbuildFormatMessages(messages, options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Piscina from 'piscina';
import { ScriptTarget } from 'typescript';
import type { Compiler, sources } from 'webpack';
import { maxWorkers } from '../../utils/environment-options';
import { EsbuildExecutor } from './esbuild-executor';

/**
* The maximum number of Workers that will be created to execute optimize tasks.
Expand Down Expand Up @@ -160,6 +161,10 @@ export class JavaScriptOptimizerPlugin {
target,
removeLicenses: this.options.removeLicenses,
advanced: this.options.advanced,
// Perform a single native esbuild support check.
// This removes the need for each worker to perform the check which would
// otherwise require spawning a separate process per worker.
alwaysUseWasm: !EsbuildExecutor.hasNativeSupport(),
};

// Sort scripts so larger scripts start first - worker pool uses a FIFO queue
Expand Down