From cb733ac00889015ce4e196c520af5801b8cabf5f Mon Sep 17 00:00:00 2001
From: Charles Lyding <19598772+clydin@users.noreply.github.com>
Date: Fri, 27 Sep 2019 15:01:56 -0400
Subject: [PATCH 1/5] build: add @babel/core typings
---
package.json | 1 +
yarn.lock | 47 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+)
diff --git a/package.json b/package.json
index 816491f29e25..e03357aeb8e5 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,7 @@
"@bazel/jasmine": "0.35.0",
"@bazel/karma": "0.35.0",
"@bazel/typescript": "0.35.0",
+ "@types/babel__core": "7.1.3",
"@types/browserslist": "^4.4.0",
"@types/caniuse-lite": "^1.0.0",
"@types/clean-css": "^4.2.1",
diff --git a/yarn.lock b/yarn.lock
index a064ff3134c6..bfaefdb7ba65 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -324,6 +324,11 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
+"@babel/parser@^7.1.0":
+ version "7.6.2"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1"
+ integrity sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==
+
"@babel/parser@^7.1.2":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.3.tgz#2c92469bac2b7fbff810b67fca07bd138b48af77"
@@ -772,6 +777,15 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
+"@babel/types@^7.3.0":
+ version "7.6.1"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648"
+ integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.13"
+ to-fast-properties "^2.0.0"
+
"@bazel/bazel-darwin_x64@0.28.1":
version "0.28.1"
resolved "https://registry.yarnpkg.com/@bazel/bazel-darwin_x64/-/bazel-darwin_x64-0.28.1.tgz#415658785e1dbd6f7ab5c8f2b98c1c99c614e1d5"
@@ -940,6 +954,39 @@
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
+"@types/babel__core@7.1.3":
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
+ integrity sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.6.0"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.0.tgz#f1ec1c104d1bb463556ecb724018ab788d0c172a"
+ integrity sha512-c1mZUu4up5cp9KROs/QAw0gTeHrw/x7m52LcnvMxxOZ03DmLwPV0MlGmlgzV3cnSdjhJOZsj7E7FHeioai+egw==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307"
+ integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*":
+ version "7.0.7"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f"
+ integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw==
+ dependencies:
+ "@babel/types" "^7.3.0"
+
"@types/bluebird@*":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.26.tgz#a38c438ae84fa02431d6892edf86e46edcbca291"
From 4687cea09d8319e3f7d4365c5ec10943c4294765 Mon Sep 17 00:00:00 2001
From: Charles Lyding <19598772+clydin@users.noreply.github.com>
Date: Mon, 30 Sep 2019 21:10:05 -0400
Subject: [PATCH 2/5] refactor(@angular-devkit/build-angular): process bundle
code quality improvements
---
.../src/browser/action-executor.ts | 4 +-
.../build_angular/src/browser/index.ts | 3 +-
.../build_angular/src/utils/process-bundle.ts | 480 +++++++++++-------
3 files changed, 292 insertions(+), 195 deletions(-)
diff --git a/packages/angular_devkit/build_angular/src/browser/action-executor.ts b/packages/angular_devkit/build_angular/src/browser/action-executor.ts
index 29f9d7a39b89..7471e885d626 100644
--- a/packages/angular_devkit/build_angular/src/browser/action-executor.ts
+++ b/packages/angular_devkit/build_angular/src/browser/action-executor.ts
@@ -14,16 +14,18 @@ export class ActionExecutor {
private smallThreshold = 32 * 1024;
- constructor(actionFile: string, private readonly actionName: string) {
+ constructor(actionFile: string, private readonly actionName: string, setupOptions?: unknown) {
// larger files are processed in a separate process to limit memory usage in the main process
this.largeWorker = new JestWorker(actionFile, {
exposedMethods: [actionName],
+ setupArgs: setupOptions === undefined ? undefined : [setupOptions],
});
// small files are processed in a limited number of threads to improve speed
// The limited number also prevents a large increase in memory usage for an otherwise short operation
this.smallWorker = new JestWorker(actionFile, {
exposedMethods: [actionName],
+ setupArgs: setupOptions === undefined ? undefined : [setupOptions],
numWorkers: os.cpus().length < 2 ? 1 : 2,
// Will automatically fallback to processes if not supported
enableWorkerThreads: true,
diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts
index 82ef60250734..e9b03a712534 100644
--- a/packages/angular_devkit/build_angular/src/browser/index.ts
+++ b/packages/angular_devkit/build_angular/src/browser/index.ts
@@ -554,13 +554,11 @@ export function buildWebpackBrowser(
processRuntimeAction = {
...action,
cacheKeys,
- cachePath: cacheDownlevelPath || undefined,
};
} else {
processActions.push({
...action,
cacheKeys,
- cachePath: cacheDownlevelPath || undefined,
});
}
}
@@ -604,6 +602,7 @@ export function buildWebpackBrowser(
? workerFile
: require.resolve('../utils/process-bundle-bootstrap'),
'process',
+ { cachePath: cacheDownlevelPath },
);
try {
diff --git a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts
index f0e9f5c07600..82da8993e5c3 100644
--- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts
+++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts
@@ -5,14 +5,14 @@
* 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 { transformAsync } from '@babel/core';
import { createHash } from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
-import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
+import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map';
import { minify } from 'terser';
import { manglingDisabled } from './mangle-options';
-const { transformAsync } = require('@babel/core');
const cacache = require('cacache');
export interface ProcessBundleOptions {
@@ -28,7 +28,6 @@ export interface ProcessBundleOptions {
optimizeOnly?: boolean;
ignoreOriginal?: boolean;
cacheKeys?: (string | null)[];
- cachePath?: string;
integrityAlgorithm?: 'sha256' | 'sha384' | 'sha512';
runtimeData?: ProcessBundleResult[];
}
@@ -57,180 +56,124 @@ export const enum CacheKey {
DownlevelMap = 3,
}
+let cachePath: string | undefined;
+
+export function setup(options: { cachePath: string }): void {
+ cachePath = options.cachePath;
+}
+
+async function cachePut(content: string, key: string | null, integrity?: string): Promise {
+ if (cachePath && key) {
+ await cacache.put(cachePath, key, content, {
+ metadata: { integrity },
+ });
+ }
+}
+
export async function process(options: ProcessBundleOptions): Promise {
if (!options.cacheKeys) {
options.cacheKeys = [];
}
- // If no downlevelling required than just mangle code and return
- if (options.optimizeOnly) {
- const result: ProcessBundleResult = { name: options.name };
- if (options.integrityAlgorithm) {
- result.integrity = generateIntegrityValue(options.integrityAlgorithm, options.code);
- }
-
- // Replace integrity hashes with updated values
- // NOTE: This should eventually be a babel plugin
- if (options.runtime && options.integrityAlgorithm && options.runtimeData) {
- for (const data of options.runtimeData) {
- if (!data.integrity || !data.original || !data.original.integrity) {
- continue;
- }
-
- options.code = options.code.replace(data.integrity, data.original.integrity);
- }
- }
-
- result.original = await mangleOriginal(options);
+ const result: ProcessBundleResult = { name: options.name };
+ if (options.integrityAlgorithm) {
+ // Store unmodified code integrity value -- used for SRI value replacement
+ result.integrity = generateIntegrityValue(options.integrityAlgorithm, options.code);
+ }
- return result;
+ // Runtime chunk requires specialized handling
+ if (options.runtime) {
+ return { ...result, ...(await processRuntime(options)) };
}
+ const basePath = path.dirname(options.filename);
+ const filename = path.basename(options.filename);
+ const downlevelFilename = filename.replace('es2015', 'es5');
+ const downlevel = !options.optimizeOnly;
+
// if code size is larger than 500kB, manually handle sourcemaps with newer source-map package.
// babel currently uses an older version that still supports sync calls
- const codeSize = Buffer.byteLength(options.code, 'utf8');
- const mapSize = options.map ? Buffer.byteLength(options.map, 'utf8') : 0;
+ const codeSize = Buffer.byteLength(options.code);
+ const mapSize = options.map ? Buffer.byteLength(options.map) : 0;
const manualSourceMaps = codeSize >= 500 * 1024 || mapSize >= 500 * 1024;
- // downlevel the bundle
- let { code, map } = await transformAsync(options.code, {
- filename: options.filename,
- inputSourceMap: !manualSourceMaps && options.map !== undefined && JSON.parse(options.map),
- babelrc: false,
- // modules aren't needed since the bundles use webpack's custom module loading
- // 'transform-typeof-symbol' generates slower code
- presets: [
- ['@babel/preset-env', { modules: false, exclude: ['transform-typeof-symbol'] }],
- ],
- minified: options.optimize,
- // `false` ensures it is disabled and prevents large file warnings
- compact: options.optimize || false,
- sourceMaps: options.sourceMaps,
- });
-
- const newFilePath = options.filename.replace('es2015', 'es5');
-
- // Adjust lazy loaded scripts to point to the proper variant
- // Extra spacing is intentional to align source line positions
- if (options.runtime) {
- code = code.replace('"-es2015.', ' "-es5.');
-
- // Replace integrity hashes with updated values
- // NOTE: This should eventually be a babel plugin
- if (options.integrityAlgorithm && options.runtimeData) {
- for (const data of options.runtimeData) {
- if (!data.integrity || !data.downlevel || !data.downlevel.integrity) {
- continue;
- }
+ const sourceCode = options.code;
+ const sourceMap = options.map ? JSON.parse(options.map) : undefined;
+
+ let downlevelCode;
+ let downlevelMap;
+ if (downlevel) {
+ // Downlevel the bundle
+ const transformResult = await transformAsync(sourceCode, {
+ filename: options.filename,
+ inputSourceMap: manualSourceMaps ? undefined : sourceMap,
+ babelrc: false,
+ // modules aren't needed since the bundles use webpack's custom module loading
+ // 'transform-typeof-symbol' generates slower code
+ presets: [['@babel/preset-env', { modules: false, exclude: ['transform-typeof-symbol'] }]],
+ minified: options.optimize,
+ // `false` ensures it is disabled and prevents large file warnings
+ compact: options.optimize || false,
+ sourceMaps: !!sourceMap,
+ });
- code = code.replace(data.integrity, data.downlevel.integrity);
- }
+ if (!transformResult || !transformResult.code) {
+ throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`);
}
- }
+ downlevelCode = transformResult.code;
- if (options.sourceMaps && manualSourceMaps && options.map) {
- const generator = new SourceMapGenerator();
- let sourceRoot;
- await SourceMapConsumer.with(options.map, null, originalConsumer => {
- sourceRoot = 'sourceRoot' in originalConsumer ? originalConsumer.sourceRoot : undefined;
-
- return SourceMapConsumer.with(map, null, newConsumer => {
- newConsumer.eachMapping(mapping => {
- if (mapping.originalLine === null) {
- return;
- }
-
- const originalPosition = originalConsumer.originalPositionFor({
- line: mapping.originalLine,
- column: mapping.originalColumn,
- });
-
- if (
- originalPosition.line === null ||
- originalPosition.column === null ||
- originalPosition.source === null
- ) {
- return;
- }
-
- generator.addMapping({
- generated: {
- line: mapping.generatedLine,
- column: mapping.generatedColumn,
- },
- name: originalPosition.name || undefined,
- original: {
- line: originalPosition.line,
- column: originalPosition.column,
- },
- source: originalPosition.source,
- });
- });
- });
- });
-
- map = generator.toJSON();
- map.file = path.basename(newFilePath);
- map.sourceRoot = sourceRoot;
+ if (manualSourceMaps && sourceMap && transformResult.map) {
+ downlevelMap = await mergeSourcemaps(sourceMap, transformResult.map);
+ } else {
+ // undefined is needed here to normalize the property type
+ downlevelMap = transformResult.map || undefined;
+ }
}
- const result: ProcessBundleResult = { name: options.name };
-
if (options.optimize) {
- // Note: Investigate converting the AST instead of re-parsing
- // estree -> terser is already supported; need babel -> estree/terser
-
- // Mangle downlevel code
- const minifyOutput = minify(code, {
- compress: true,
- ecma: 5,
- mangle: !manglingDisabled,
- safari10: true,
- output: {
- ascii_only: true,
- webkit: true,
- },
- sourceMap: options.sourceMaps && {
- filename: path.basename(newFilePath),
- content: map,
- },
- });
-
- if (minifyOutput.error) {
- throw minifyOutput.error;
+ if (downlevelCode) {
+ const minifyResult = terserMangle(downlevelCode, {
+ filename: downlevelFilename,
+ map: downlevelMap,
+ compress: true,
+ });
+ downlevelCode = minifyResult.code;
+ downlevelMap = minifyResult.map;
}
- code = minifyOutput.code;
- map = minifyOutput.map;
-
- // Mangle original code
if (!options.ignoreOriginal) {
result.original = await mangleOriginal(options);
}
- } else if (map) {
- map = JSON.stringify(map);
}
- if (map) {
- if (!options.hiddenSourceMaps) {
- code += `\n//# sourceMappingURL=${path.basename(newFilePath)}.map`;
- }
+ if (downlevelCode) {
+ const downlevelPath = path.join(basePath, downlevelFilename);
- if (options.cachePath && options.cacheKeys[CacheKey.DownlevelMap]) {
- await cacache.put(options.cachePath, options.cacheKeys[CacheKey.DownlevelMap], map);
- }
+ let mapContent;
+ if (downlevelMap) {
+ if (!options.hiddenSourceMaps) {
+ downlevelCode += `\n//# sourceMappingURL=${downlevelFilename}.map`;
+ }
- fs.writeFileSync(newFilePath + '.map', map);
- }
+ mapContent = JSON.stringify(downlevelMap);
+ await cachePut(mapContent, options.cacheKeys[CacheKey.DownlevelMap]);
+ fs.writeFileSync(downlevelPath + '.map', mapContent);
+ }
- result.downlevel = createFileEntry(newFilePath, code, map, options.integrityAlgorithm);
+ result.downlevel = createFileEntry(
+ downlevelFilename,
+ downlevelCode,
+ mapContent,
+ options.integrityAlgorithm,
+ );
- if (options.cachePath && options.cacheKeys[CacheKey.DownlevelCode]) {
- await cacache.put(options.cachePath, options.cacheKeys[CacheKey.DownlevelCode], code, {
- metadata: { integrity: result.downlevel.integrity },
- });
+ await cachePut(
+ downlevelCode,
+ options.cacheKeys[CacheKey.DownlevelCode],
+ result.downlevel.integrity,
+ );
+ fs.writeFileSync(downlevelPath, downlevelCode);
}
- fs.writeFileSync(newFilePath, code);
// If original was not processed, add info
if (!result.original && !options.ignoreOriginal) {
@@ -242,74 +185,138 @@ export async function process(options: ProcessBundleOptions): Promise {
+ return SourceMapConsumer.with(second, null, newConsumer => {
+ newConsumer.eachMapping(mapping => {
+ if (mapping.originalLine === null) {
+ return;
+ }
+ const originalPosition = originalConsumer.originalPositionFor({
+ line: mapping.originalLine,
+ column: mapping.originalColumn,
+ });
+ if (
+ originalPosition.line === null ||
+ originalPosition.column === null ||
+ originalPosition.source === null
+ ) {
+ return;
+ }
+ generator.addMapping({
+ generated: {
+ line: mapping.generatedLine,
+ column: mapping.generatedColumn,
+ },
+ name: originalPosition.name || undefined,
+ original: {
+ line: originalPosition.line,
+ column: originalPosition.column,
+ },
+ source: originalPosition.source,
+ });
+ });
+ });
+ });
+
+ const map = generator.toJSON();
+ map.file = second.file;
+ map.sourceRoot = sourceRoot;
+
+ // Put the sourceRoot back
+ if (sourceRoot) {
+ first.sourceRoot = sourceRoot;
}
- return result;
+ return map;
}
async function mangleOriginal(options: ProcessBundleOptions): Promise {
- const resultOriginal = minify(options.code, {
- compress: false,
+ const result = terserMangle(options.code, {
+ filename: path.basename(options.filename),
+ map: options.map ? JSON.parse(options.map) : undefined,
ecma: 6,
- mangle: !manglingDisabled,
- safari10: true,
- output: {
- ascii_only: true,
- webkit: true,
- },
- sourceMap: options.sourceMaps &&
- options.map !== undefined && {
- filename: path.basename(options.filename),
- content: JSON.parse(options.map),
- },
});
- if (resultOriginal.error) {
- throw resultOriginal.error;
- }
-
- if (resultOriginal.map) {
+ let mapContent;
+ if (result.map) {
if (!options.hiddenSourceMaps) {
- resultOriginal.code += `\n//# sourceMappingURL=${path.basename(options.filename)}.map`;
+ result.code += `\n//# sourceMappingURL=${path.basename(options.filename)}.map`;
}
- if (options.cachePath && options.cacheKeys && options.cacheKeys[CacheKey.OriginalMap]) {
- await cacache.put(
- options.cachePath,
- options.cacheKeys[CacheKey.OriginalMap],
- resultOriginal.map,
- );
- }
+ mapContent = JSON.stringify(result.map);
- fs.writeFileSync(options.filename + '.map', resultOriginal.map);
+ await cachePut(
+ mapContent,
+ (options.cacheKeys && options.cacheKeys[CacheKey.OriginalMap]) || null,
+ );
+ fs.writeFileSync(options.filename + '.map', mapContent);
}
const fileResult = createFileEntry(
options.filename,
- // tslint:disable-next-line: no-non-null-assertion
- resultOriginal.code!,
- resultOriginal.map as string,
+ result.code,
+ mapContent,
options.integrityAlgorithm,
);
- if (options.cachePath && options.cacheKeys && options.cacheKeys[CacheKey.OriginalCode]) {
- await cacache.put(
- options.cachePath,
- options.cacheKeys[CacheKey.OriginalCode],
- resultOriginal.code,
- {
- metadata: { integrity: fileResult.integrity },
- },
- );
- }
-
- fs.writeFileSync(options.filename, resultOriginal.code);
+ await cachePut(
+ result.code,
+ (options.cacheKeys && options.cacheKeys[CacheKey.OriginalCode]) || null,
+ fileResult.integrity,
+ );
+ fs.writeFileSync(options.filename, result.code);
return fileResult;
}
+function terserMangle(
+ code: string,
+ options: { filename?: string; map?: RawSourceMap; compress?: boolean; ecma?: 5 | 6 } = {},
+) {
+ // Note: Investigate converting the AST instead of re-parsing
+ // estree -> terser is already supported; need babel -> estree/terser
+
+ // Mangle downlevel code
+ const minifyOutput = minify(code, {
+ compress: options.compress || false,
+ ecma: options.ecma || 5,
+ mangle: !manglingDisabled,
+ safari10: true,
+ output: {
+ ascii_only: true,
+ webkit: true,
+ },
+ sourceMap:
+ !!options.map &&
+ ({
+ filename: options.filename,
+ // terser uses an old version of the sourcemap typings
+ // tslint:disable-next-line: no-any
+ content: options.map as any,
+ asObject: true,
+ // typings don't include asObject option
+ // tslint:disable-next-line: no-any
+ } as any),
+ });
+
+ if (minifyOutput.error) {
+ throw minifyOutput.error;
+ }
+
+ // tslint:disable-next-line: no-non-null-assertion
+ return { code: minifyOutput.code!, map: minifyOutput.map as RawSourceMap | undefined };
+}
+
function createFileEntry(
filename: string,
code: string,
@@ -338,3 +345,92 @@ function generateIntegrityValue(hashAlgorithm: string, code: string) {
.digest('base64')
);
}
+
+// The webpack runtime chunk is already ES5.
+// However, two variants are still needed due to lazy routing and SRI differences
+// NOTE: This should eventually be a babel plugin
+async function processRuntime(
+ options: ProcessBundleOptions,
+): Promise> {
+ let originalCode = options.code;
+ let downlevelCode = options.code;
+
+ // Replace integrity hashes with updated values
+ if (options.integrityAlgorithm && options.runtimeData) {
+ for (const data of options.runtimeData) {
+ if (!data.integrity) {
+ continue;
+ }
+
+ if (data.original && data.original.integrity) {
+ originalCode = originalCode.replace(data.integrity, data.original.integrity);
+ }
+ if (data.downlevel && data.downlevel.integrity) {
+ downlevelCode = downlevelCode.replace(data.integrity, data.downlevel.integrity);
+ }
+ }
+ }
+
+ // Adjust lazy loaded scripts to point to the proper variant
+ // Extra spacing is intentional to align source line positions
+ downlevelCode = downlevelCode.replace('"-es2015.', ' "-es5.');
+
+ const downlevelFilePath = options.filename.replace('es2015', 'es5');
+ let downlevelMap;
+ let result;
+ if (options.optimize) {
+ const minifiyResults = terserMangle(downlevelCode, {
+ filename: path.basename(downlevelFilePath),
+ map: options.map === undefined ? undefined : JSON.parse(options.map),
+ });
+ downlevelCode = minifiyResults.code;
+ downlevelMap = JSON.stringify(minifiyResults.map);
+
+ result = {
+ original: await mangleOriginal({ ...options, code: originalCode }),
+ downlevel: createFileEntry(
+ downlevelFilePath,
+ downlevelCode,
+ downlevelMap,
+ options.integrityAlgorithm,
+ ),
+ };
+ } else {
+ if (options.map) {
+ const rawMap = JSON.parse(options.map) as RawSourceMap;
+ rawMap.file = path.basename(downlevelFilePath);
+ downlevelMap = JSON.stringify(rawMap);
+ }
+
+ result = {
+ original: createFileEntry(
+ options.filename,
+ originalCode,
+ options.map,
+ options.integrityAlgorithm,
+ ),
+ downlevel: createFileEntry(
+ downlevelFilePath,
+ downlevelCode,
+ downlevelMap,
+ options.integrityAlgorithm,
+ ),
+ };
+ }
+
+ if (downlevelMap) {
+ await cachePut(
+ downlevelMap,
+ (options.cacheKeys && options.cacheKeys[CacheKey.DownlevelMap]) || null,
+ );
+ fs.writeFileSync(downlevelFilePath + '.map', downlevelMap);
+ downlevelCode += `\n//# sourceMappingURL=${path.basename(downlevelFilePath)}.map`;
+ }
+ await cachePut(
+ downlevelCode,
+ (options.cacheKeys && options.cacheKeys[CacheKey.DownlevelCode]) || null,
+ );
+ fs.writeFileSync(downlevelFilePath, downlevelCode);
+
+ return result;
+}
From 62617ee964f0cd6c50eeff6c3d2be97aaba26800 Mon Sep 17 00:00:00 2001
From: Charles Lyding <19598772+clydin@users.noreply.github.com>
Date: Thu, 3 Oct 2019 20:00:13 -0400
Subject: [PATCH 3/5] refactor(@angular-devkit/build-angular): reorganize
bundle processing for browser builder
---
.../build_angular/src/browser/action-cache.ts | 191 +++++++++++++++
.../src/browser/action-executor.ts | 111 +++++++--
.../build_angular/src/browser/index.ts | 226 ++----------------
3 files changed, 296 insertions(+), 232 deletions(-)
create mode 100644 packages/angular_devkit/build_angular/src/browser/action-cache.ts
diff --git a/packages/angular_devkit/build_angular/src/browser/action-cache.ts b/packages/angular_devkit/build_angular/src/browser/action-cache.ts
new file mode 100644
index 000000000000..f04e4053b07b
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/action-cache.ts
@@ -0,0 +1,191 @@
+/**
+ * @license
+ * Copyright Google Inc. 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 { createHash } from 'crypto';
+import * as findCacheDirectory from 'find-cache-dir';
+import * as fs from 'fs';
+import { manglingDisabled } from '../utils/mangle-options';
+import { CacheKey, ProcessBundleOptions, ProcessBundleResult } from '../utils/process-bundle';
+
+const cacache = require('cacache');
+const cacheDownlevelPath = findCacheDirectory({ name: 'angular-build-dl' });
+const packageVersion = require('../../package.json').version;
+
+// Workaround Node.js issue prior to 10.16 with copyFile on macOS
+// https://github.com/angular/angular-cli/issues/15544 & https://github.com/nodejs/node/pull/27241
+let copyFileWorkaround = false;
+if (process.platform === 'darwin') {
+ const version = process.versions.node.split('.').map(part => Number(part));
+ if (version[0] < 10 || version[0] === 11 || (version[0] === 10 && version[1] < 16)) {
+ copyFileWorkaround = true;
+ }
+}
+
+export interface CacheEntry {
+ path: string;
+ size: number;
+ integrity?: string;
+}
+
+export class BundleActionCache {
+ constructor(private readonly integrityAlgorithm?: string) {}
+
+ static copyEntryContent(entry: CacheEntry | string, dest: fs.PathLike): void {
+ if (copyFileWorkaround) {
+ try {
+ fs.unlinkSync(dest);
+ } catch {}
+ }
+
+ fs.copyFileSync(
+ typeof entry === 'string' ? entry : entry.path,
+ dest,
+ fs.constants.COPYFILE_FICLONE,
+ );
+ if (process.platform !== 'win32') {
+ // The cache writes entries as readonly and when using copyFile the permissions will also be copied.
+ // See: https://github.com/npm/cacache/blob/073fbe1a9f789ba42d9a41de7b8429c93cf61579/lib/util/move-file.js#L36
+ fs.chmodSync(dest, 0o644);
+ }
+ }
+
+ generateBaseCacheKey(content: string): string {
+ // Create base cache key with elements:
+ // * package version - different build-angular versions cause different final outputs
+ // * code length/hash - ensure cached version matches the same input code
+ const algorithm = this.integrityAlgorithm || 'sha1';
+ const codeHash = createHash(algorithm)
+ .update(content)
+ .digest('base64');
+ let baseCacheKey = `${packageVersion}|${content.length}|${algorithm}-${codeHash}`;
+ if (manglingDisabled) {
+ baseCacheKey += '|MD';
+ }
+
+ return baseCacheKey;
+ }
+
+ generateCacheKeys(action: ProcessBundleOptions): string[] {
+ const baseCacheKey = this.generateBaseCacheKey(action.code);
+
+ // Postfix added to sourcemap cache keys when vendor sourcemaps are present
+ // Allows non-destructive caching of both variants
+ const SourceMapVendorPostfix = !!action.sourceMaps && action.vendorSourceMaps ? '|vendor' : '';
+
+ // Determine cache entries required based on build settings
+ const cacheKeys = [];
+
+ // If optimizing and the original is not ignored, add original as required
+ if ((action.optimize || action.optimizeOnly) && !action.ignoreOriginal) {
+ cacheKeys[CacheKey.OriginalCode] = baseCacheKey + '|orig';
+
+ // If sourcemaps are enabled, add original sourcemap as required
+ if (action.sourceMaps) {
+ cacheKeys[CacheKey.OriginalMap] = baseCacheKey + SourceMapVendorPostfix + '|orig-map';
+ }
+ }
+ // If not only optimizing, add downlevel as required
+ if (!action.optimizeOnly) {
+ cacheKeys[CacheKey.DownlevelCode] = baseCacheKey + '|dl';
+
+ // If sourcemaps are enabled, add downlevel sourcemap as required
+ if (action.sourceMaps) {
+ cacheKeys[CacheKey.DownlevelMap] = baseCacheKey + SourceMapVendorPostfix + '|dl-map';
+ }
+ }
+
+ return cacheKeys;
+ }
+
+ async getCacheEntries(cacheKeys: (string | null)[]): Promise<(CacheEntry | null)[] | false> {
+ // Attempt to get required cache entries
+ const cacheEntries = [];
+ for (const key of cacheKeys) {
+ if (key) {
+ const entry = await cacache.get.info(cacheDownlevelPath, key);
+ if (!entry) {
+ return false;
+ }
+ cacheEntries.push({
+ path: entry.path,
+ size: entry.size,
+ integrity: entry.metadata && entry.metadata.integrity,
+ });
+ } else {
+ cacheEntries.push(null);
+ }
+ }
+
+ return cacheEntries;
+ }
+
+ async getCachedBundleResult(action: ProcessBundleOptions): Promise {
+ const entries = action.cacheKeys && await this.getCacheEntries(action.cacheKeys);
+ if (!entries) {
+ return null;
+ }
+
+ const result: ProcessBundleResult = { name: action.name };
+
+ let cacheEntry = entries[CacheKey.OriginalCode];
+ if (cacheEntry) {
+ result.original = {
+ filename: action.filename,
+ size: cacheEntry.size,
+ integrity: cacheEntry.integrity,
+ };
+
+ BundleActionCache.copyEntryContent(cacheEntry, result.original.filename);
+
+ cacheEntry = entries[CacheKey.OriginalMap];
+ if (cacheEntry) {
+ result.original.map = {
+ filename: action.filename + '.map',
+ size: cacheEntry.size,
+ };
+
+ BundleActionCache.copyEntryContent(cacheEntry, result.original.filename + '.map');
+ }
+ } else if (!action.ignoreOriginal) {
+ // If the original wasn't processed (and therefore not cached), add info
+ result.original = {
+ filename: action.filename,
+ size: Buffer.byteLength(action.code, 'utf8'),
+ map:
+ action.map === undefined
+ ? undefined
+ : {
+ filename: action.filename + '.map',
+ size: Buffer.byteLength(action.map, 'utf8'),
+ },
+ };
+ }
+
+ cacheEntry = entries[CacheKey.DownlevelCode];
+ if (cacheEntry) {
+ result.downlevel = {
+ filename: action.filename.replace('es2015', 'es5'),
+ size: cacheEntry.size,
+ integrity: cacheEntry.integrity,
+ };
+
+ BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename);
+
+ cacheEntry = entries[CacheKey.DownlevelMap];
+ if (cacheEntry) {
+ result.downlevel.map = {
+ filename: action.filename.replace('es2015', 'es5') + '.map',
+ size: cacheEntry.size,
+ };
+
+ BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename + '.map');
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/packages/angular_devkit/build_angular/src/browser/action-executor.ts b/packages/angular_devkit/build_angular/src/browser/action-executor.ts
index 7471e885d626..8e24c3d5d27e 100644
--- a/packages/angular_devkit/build_angular/src/browser/action-executor.ts
+++ b/packages/angular_devkit/build_angular/src/browser/action-executor.ts
@@ -7,49 +7,110 @@
*/
import JestWorker from 'jest-worker';
import * as os from 'os';
+import * as path from 'path';
+import { ProcessBundleOptions, ProcessBundleResult } from '../utils/process-bundle';
+import { BundleActionCache } from './action-cache';
-export class ActionExecutor {
- private largeWorker: JestWorker;
- private smallWorker: JestWorker;
+let workerFile = require.resolve('../utils/process-bundle');
+workerFile =
+ path.extname(workerFile) === '.ts'
+ ? require.resolve('../utils/process-bundle-bootstrap')
+ : workerFile;
- private smallThreshold = 32 * 1024;
+export class BundleActionExecutor {
+ private largeWorker?: JestWorker;
+ private smallWorker?: JestWorker;
+ private cache: BundleActionCache;
+
+ constructor(
+ private workerOptions: unknown,
+ integrityAlgorithm?: string,
+ private readonly sizeThreshold = 32 * 1024,
+ ) {
+ this.cache = new BundleActionCache(integrityAlgorithm);
+ }
+
+ private static executeMethod(worker: JestWorker, method: string, input: unknown): Promise {
+ return ((worker as unknown) as Record Promise>)[method](input);
+ }
+
+ private ensureLarge(): JestWorker {
+ if (this.largeWorker) {
+ return this.largeWorker;
+ }
- constructor(actionFile: string, private readonly actionName: string, setupOptions?: unknown) {
// larger files are processed in a separate process to limit memory usage in the main process
- this.largeWorker = new JestWorker(actionFile, {
- exposedMethods: [actionName],
- setupArgs: setupOptions === undefined ? undefined : [setupOptions],
- });
+ return (this.largeWorker = new JestWorker(workerFile, {
+ exposedMethods: ['process'],
+ setupArgs: [this.workerOptions],
+ }));
+ }
+
+ private ensureSmall(): JestWorker {
+ if (this.smallWorker) {
+ return this.smallWorker;
+ }
// small files are processed in a limited number of threads to improve speed
// The limited number also prevents a large increase in memory usage for an otherwise short operation
- this.smallWorker = new JestWorker(actionFile, {
- exposedMethods: [actionName],
- setupArgs: setupOptions === undefined ? undefined : [setupOptions],
+ return (this.smallWorker = new JestWorker(workerFile, {
+ exposedMethods: ['process'],
+ setupArgs: [this.workerOptions],
numWorkers: os.cpus().length < 2 ? 1 : 2,
// Will automatically fallback to processes if not supported
enableWorkerThreads: true,
- });
+ }));
}
- execute(options: Input): Promise