@angular-devkit/build-angular 0.803.4 → 0.803.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@angular-devkit/build-angular",
3
- "version": "0.803.4",
3
+ "version": "0.803.8",
4
4
  "description": "Angular Webpack Build Facade",
5
5
  "experimental": true,
6
6
  "main": "src/index.js",
7
7
  "typings": "src/index.d.ts",
8
8
  "builders": "builders.json",
9
9
  "dependencies": {
10
- "@angular-devkit/architect": "0.803.4",
11
- "@angular-devkit/build-optimizer": "0.803.4",
12
- "@angular-devkit/build-webpack": "0.803.4",
13
- "@angular-devkit/core": "8.3.4",
10
+ "@angular-devkit/architect": "0.803.8",
11
+ "@angular-devkit/build-optimizer": "0.803.8",
12
+ "@angular-devkit/build-webpack": "0.803.8",
13
+ "@angular-devkit/core": "8.3.8",
14
14
  "@babel/core": "7.5.5",
15
15
  "@babel/preset-env": "7.5.5",
16
- "@ngtools/webpack": "8.3.4",
16
+ "@ngtools/webpack": "8.3.8",
17
17
  "ajv": "6.10.2",
18
18
  "autoprefixer": "9.6.1",
19
19
  "browserslist": "4.6.6",
@@ -27,6 +27,7 @@
27
27
  "find-cache-dir": "3.0.0",
28
28
  "glob": "7.1.4",
29
29
  "istanbul-instrumenter-loader": "3.0.1",
30
+ "jest-worker": "24.9.0",
30
31
  "karma-source-map-support": "1.4.0",
31
32
  "less": "3.9.0",
32
33
  "less-loader": "5.0.0",
@@ -61,7 +62,6 @@
61
62
  "webpack-merge": "4.2.1",
62
63
  "webpack-sources": "1.4.3",
63
64
  "webpack-subresource-integrity": "1.1.0-rc.6",
64
- "worker-farm": "1.7.0",
65
65
  "worker-plugin": "3.2.0"
66
66
  },
67
67
  "peerDependencies": {
@@ -26,6 +26,7 @@ export declare function countOccurrences(source: string, match: string, wordBrea
26
26
  * Holder of statistics related to the build.
27
27
  */
28
28
  declare class AnalyticsBuildStats {
29
+ isIvy: boolean;
29
30
  errors: string[];
30
31
  numberOfNgOnInit: number;
31
32
  numberOfComponents: number;
@@ -51,7 +52,7 @@ export declare class NgBuildAnalyticsPlugin {
51
52
  constructor(_projectRoot: string, _analytics: analytics.Analytics, _category: string);
52
53
  protected _reset(): void;
53
54
  protected _getMetrics(stats: Stats): (string | number)[];
54
- protected _getDimensions(stats: Stats): (string | number)[];
55
+ protected _getDimensions(stats: Stats): import("../../../../../dist-schema/packages/angular/cli/commands/config").Value[];
55
56
  protected _reportBuildMetrics(stats: Stats): void;
56
57
  protected _reportRebuildMetrics(stats: Stats): void;
57
58
  protected _checkTsNormalModule(module: NormalModule): void;
@@ -54,6 +54,7 @@ exports.countOccurrences = countOccurrences;
54
54
  */
55
55
  class AnalyticsBuildStats {
56
56
  constructor() {
57
+ this.isIvy = false;
57
58
  this.errors = [];
58
59
  this.numberOfNgOnInit = 0;
59
60
  this.numberOfComponents = 0;
@@ -106,6 +107,7 @@ class NgBuildAnalyticsPlugin {
106
107
  // Adding commas before and after so the regex are easier to define filters.
107
108
  dimensions[core_1.analytics.NgCliAnalyticsDimensions.BuildErrors] = `,${this._stats.errors.join()},`;
108
109
  }
110
+ dimensions[core_1.analytics.NgCliAnalyticsDimensions.NgIvyEnabled] = this._stats.isIvy;
109
111
  return dimensions;
110
112
  }
111
113
  _reportBuildMetrics(stats) {
@@ -129,7 +131,14 @@ class NgBuildAnalyticsPlugin {
129
131
  // This does not include View Engine AOT compilation, we use the ngfactory for it.
130
132
  this._stats.numberOfComponents += countOccurrences(module._source.source(), ' Component({');
131
133
  // For Ivy we just count ngComponentDef.
132
- this._stats.numberOfComponents += countOccurrences(module._source.source(), 'ngComponentDef', true);
134
+ const numIvyComponents = countOccurrences(module._source.source(), 'ngComponentDef', true);
135
+ this._stats.numberOfComponents += numIvyComponents;
136
+ // Check whether this is an Ivy app so that it can reported as part of analytics.
137
+ if (!this._stats.isIvy) {
138
+ if (numIvyComponents > 0 || module._source.source().includes('ngModuleDef')) {
139
+ this._stats.isIvy = true;
140
+ }
141
+ }
133
142
  }
134
143
  }
135
144
  _checkNgFactoryNormalModule(module) {
@@ -254,11 +254,13 @@ function getCommonConfig(wco) {
254
254
  };
255
255
  }
256
256
  }
257
+ // TODO: Investigate why this fails for some packages: wco.supportES2015 ? 6 : 5;
258
+ const terserEcma = 5;
257
259
  const terserOptions = {
258
260
  warnings: !!buildOptions.verbose,
259
261
  safari10: true,
260
262
  output: {
261
- ecma: wco.supportES2015 ? 6 : 5,
263
+ ecma: terserEcma,
262
264
  comments: false,
263
265
  webkit: true,
264
266
  },
@@ -266,12 +268,12 @@ function getCommonConfig(wco) {
266
268
  // to remove dev code, and ngI18nClosureMode to remove Closure compiler i18n code
267
269
  compress: buildOptions.platform == 'server'
268
270
  ? {
269
- ecma: wco.supportES2015 ? 6 : 5,
271
+ ecma: terserEcma,
270
272
  global_defs: angularGlobalDefinitions,
271
273
  keep_fnames: true,
272
274
  }
273
275
  : {
274
- ecma: wco.supportES2015 ? 6 : 5,
276
+ ecma: terserEcma,
275
277
  pure_getters: buildOptions.buildOptimizer,
276
278
  // PURE comments work best with 3 passes.
277
279
  // See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926.
@@ -375,6 +377,8 @@ function getCommonConfig(wco) {
375
377
  },
376
378
  {
377
379
  test: /\.js$/,
380
+ // Factory files are processed by BO in the rules added in typescript.ts.
381
+ exclude: /(ngfactory|ngstyle)\.js$/,
378
382
  ...buildOptimizerUseRule,
379
383
  },
380
384
  {
@@ -12,11 +12,13 @@ function generateEntryPoints(appConfig) {
12
12
  };
13
13
  const entryPoints = [
14
14
  'polyfills-nomodule-es5',
15
+ 'runtime',
15
16
  'polyfills-es5',
16
17
  'polyfills',
17
18
  'sw-register',
18
19
  ...extraEntryPoints(appConfig.styles, 'styles'),
19
20
  ...extraEntryPoints(appConfig.scripts, 'scripts'),
21
+ 'vendor',
20
22
  'main',
21
23
  ];
22
24
  const duplicates = [
@@ -1,4 +1,14 @@
1
1
  export declare function formatSize(size: number): string;
2
+ export declare function generateBundleStats(info: {
3
+ id: string | number;
4
+ size?: number;
5
+ files: string[];
6
+ names?: string[];
7
+ entry: boolean;
8
+ initial: boolean;
9
+ rendered?: boolean;
10
+ }, colors: boolean): string;
11
+ export declare function generateBuildStats(hash: string, time: number, colors: boolean): string;
2
12
  export declare function statsToString(json: any, statsConfig: any): string;
3
13
  export declare function statsWarningsToString(json: any, statsConfig: any): string;
4
14
  export declare function statsErrorsToString(json: any, statsConfig: any): string;
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  // tslint:disable
11
11
  // TODO: cleanup this file, it's copied as is from Angular CLI.
12
12
  const core_1 = require("@angular-devkit/core");
13
+ const path = require("path");
13
14
  const { bold, green, red, reset, white, yellow } = core_1.terminal;
14
15
  function formatSize(size) {
15
16
  if (size <= 0) {
@@ -20,24 +21,33 @@ function formatSize(size) {
20
21
  return `${+(size / Math.pow(1024, index)).toPrecision(3)} ${abbreviations[index]}`;
21
22
  }
22
23
  exports.formatSize = formatSize;
24
+ function generateBundleStats(info, colors) {
25
+ const g = (x) => (colors ? bold(green(x)) : x);
26
+ const y = (x) => (colors ? bold(yellow(x)) : x);
27
+ const size = typeof info.size === 'number' ? ` ${formatSize(info.size)}` : '';
28
+ const files = info.files.map(f => path.basename(f)).join(', ');
29
+ const names = info.names ? ` (${info.names.join(', ')})` : '';
30
+ const initial = y(info.entry ? '[entry]' : info.initial ? '[initial]' : '');
31
+ const flags = ['rendered', 'recorded']
32
+ .map(f => (f && info[f] ? g(` [${f}]`) : ''))
33
+ .join('');
34
+ return `chunk {${y(info.id.toString())}} ${g(files)}${names}${size} ${initial}${flags}`;
35
+ }
36
+ exports.generateBundleStats = generateBundleStats;
37
+ function generateBuildStats(hash, time, colors) {
38
+ const w = (x) => colors ? bold(white(x)) : x;
39
+ return `Date: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w('' + time)}ms`;
40
+ }
41
+ exports.generateBuildStats = generateBuildStats;
23
42
  function statsToString(json, statsConfig) {
24
43
  const colors = statsConfig.colors;
25
44
  const rs = (x) => colors ? reset(x) : x;
26
45
  const w = (x) => colors ? bold(white(x)) : x;
27
- const g = (x) => colors ? bold(green(x)) : x;
28
- const y = (x) => colors ? bold(yellow(x)) : x;
29
46
  const changedChunksStats = json.chunks
30
47
  .filter((chunk) => chunk.rendered)
31
48
  .map((chunk) => {
32
49
  const asset = json.assets.filter((x) => x.name == chunk.files[0])[0];
33
- const size = asset ? ` ${formatSize(asset.size)}` : '';
34
- const files = chunk.files.join(', ');
35
- const names = chunk.names ? ` (${chunk.names.join(', ')})` : '';
36
- const initial = y(chunk.entry ? '[entry]' : chunk.initial ? '[initial]' : '');
37
- const flags = ['rendered', 'recorded']
38
- .map(f => f && chunk[f] ? g(` [${f}]`) : '')
39
- .join('');
40
- return `chunk {${y(chunk.id)}} ${g(files)}${names}${size} ${initial}${flags}`;
50
+ return generateBundleStats({ ...chunk, size: asset && asset.size }, colors);
41
51
  });
42
52
  const unchangedChunkNumber = json.chunks.length - changedChunksStats.length;
43
53
  if (unchangedChunkNumber > 0) {
@@ -0,0 +1,12 @@
1
+ export declare class ActionExecutor<Input extends {
2
+ size: number;
3
+ }, Output> {
4
+ private readonly actionName;
5
+ private largeWorker;
6
+ private smallWorker;
7
+ private smallThreshold;
8
+ constructor(actionFile: string, actionName: string);
9
+ execute(options: Input): Promise<Output>;
10
+ executeAll(options: Input[]): Promise<Output[]>;
11
+ stop(): void;
12
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * @license
5
+ * Copyright Google Inc. All Rights Reserved.
6
+ *
7
+ * Use of this source code is governed by an MIT-style license that can be
8
+ * found in the LICENSE file at https://angular.io/license
9
+ */
10
+ const jest_worker_1 = require("jest-worker");
11
+ const os = require("os");
12
+ class ActionExecutor {
13
+ constructor(actionFile, actionName) {
14
+ this.actionName = actionName;
15
+ this.smallThreshold = 32 * 1024;
16
+ // larger files are processed in a separate process to limit memory usage in the main process
17
+ this.largeWorker = new jest_worker_1.default(actionFile, {
18
+ exposedMethods: [actionName],
19
+ });
20
+ // small files are processed in a limited number of threads to improve speed
21
+ // The limited number also prevents a large increase in memory usage for an otherwise short operation
22
+ this.smallWorker = new jest_worker_1.default(actionFile, {
23
+ exposedMethods: [actionName],
24
+ numWorkers: os.cpus().length < 2 ? 1 : 2,
25
+ // Will automatically fallback to processes if not supported
26
+ enableWorkerThreads: true,
27
+ });
28
+ }
29
+ execute(options) {
30
+ if (options.size > this.smallThreshold) {
31
+ return this.largeWorker[this.actionName](options);
32
+ }
33
+ else {
34
+ return this.smallWorker[this.actionName](options);
35
+ }
36
+ }
37
+ executeAll(options) {
38
+ return Promise.all(options.map(o => this.execute(o)));
39
+ }
40
+ stop() {
41
+ this.largeWorker.end();
42
+ this.smallWorker.end();
43
+ }
44
+ }
45
+ exports.ActionExecutor = ActionExecutor;
@@ -18,7 +18,6 @@ const path = require("path");
18
18
  const rxjs_1 = require("rxjs");
19
19
  const operators_1 = require("rxjs/operators");
20
20
  const typescript_1 = require("typescript");
21
- const workerFarm = require("worker-farm");
22
21
  const analytics_1 = require("../../plugins/webpack/analytics");
23
22
  const webpack_configs_1 = require("../angular-cli-files/models/webpack-configs");
24
23
  const write_index_html_1 = require("../angular-cli-files/utilities/index-file/write-index-html");
@@ -26,8 +25,10 @@ const read_tsconfig_1 = require("../angular-cli-files/utilities/read-tsconfig");
26
25
  const service_worker_1 = require("../angular-cli-files/utilities/service-worker");
27
26
  const stats_1 = require("../angular-cli-files/utilities/stats");
28
27
  const utils_1 = require("../utils");
28
+ const mangle_options_1 = require("../utils/mangle-options");
29
29
  const version_1 = require("../utils/version");
30
30
  const webpack_browser_config_1 = require("../utils/webpack-browser-config");
31
+ const action_executor_1 = require("./action-executor");
31
32
  const cacache = require('cacache');
32
33
  const cacheDownlevelPath = findCacheDirectory({ name: 'angular-build-dl' });
33
34
  const packageVersion = require('../../package.json').version;
@@ -104,7 +105,6 @@ function buildWebpackBrowser(options, context, transforms = {}) {
104
105
  const root = core_1.normalize(context.workspaceRoot);
105
106
  // Check Angular version.
106
107
  version_1.assertCompatibleAngularVersion(context.workspaceRoot, context.logger);
107
- const loggingFn = transforms.logging || createBrowserLoggingCallback(!!options.verbose, context.logger);
108
108
  return rxjs_1.from(initialize(options, context, host, transforms.webpackConfiguration)).pipe(
109
109
  // tslint:disable-next-line: no-big-function
110
110
  operators_1.switchMap(({ workspace, config: configs }) => {
@@ -126,13 +126,20 @@ function buildWebpackBrowser(options, context, transforms = {}) {
126
126
  referenced with script[type="module"] but they may not support ES2016+ syntax.
127
127
  `);
128
128
  }
129
+ const useBundleDownleveling = isDifferentialLoadingNeeded && !(utils_1.fullDifferential || options.watch);
130
+ const startTime = Date.now();
129
131
  return rxjs_1.from(configs).pipe(
130
132
  // the concurrency parameter (3rd parameter of mergeScan) is deliberately
131
133
  // set to 1 to make sure the build steps are executed in sequence.
132
134
  operators_1.mergeScan((lastResult, config) => {
133
135
  // Make sure to only run the 2nd build step, if 1st one succeeded
134
136
  if (lastResult.success) {
135
- return build_webpack_1.runWebpack(config, context, { logging: loggingFn });
137
+ return build_webpack_1.runWebpack(config, context, {
138
+ logging: transforms.logging ||
139
+ (useBundleDownleveling
140
+ ? () => { }
141
+ : createBrowserLoggingCallback(!!options.verbose, context.logger)),
142
+ });
136
143
  }
137
144
  else {
138
145
  return rxjs_1.of();
@@ -142,7 +149,19 @@ function buildWebpackBrowser(options, context, transforms = {}) {
142
149
  operators_1.switchMap(async (buildEvents) => {
143
150
  configs.length = 0;
144
151
  const success = buildEvents.every(r => r.success);
145
- if (success) {
152
+ if (!success && useBundleDownleveling) {
153
+ // If using bundle downleveling then there is only one build
154
+ // If it fails show any diagnostic messages and bail
155
+ const webpackStats = buildEvents[0].webpackStats;
156
+ if (webpackStats && webpackStats.warnings.length > 0) {
157
+ context.logger.warn(stats_1.statsWarningsToString(webpackStats, { colors: true }));
158
+ }
159
+ if (webpackStats && webpackStats.errors.length > 0) {
160
+ context.logger.error(stats_1.statsErrorsToString(webpackStats, { colors: true }));
161
+ }
162
+ return { success };
163
+ }
164
+ else if (success) {
146
165
  let noModuleFiles;
147
166
  let moduleFiles;
148
167
  let files;
@@ -156,7 +175,7 @@ function buildWebpackBrowser(options, context, transforms = {}) {
156
175
  }
157
176
  }
158
177
  else if (isDifferentialLoadingNeeded && !utils_1.fullDifferential) {
159
- const { emittedFiles = [] } = firstBuild;
178
+ const { emittedFiles = [], webpackStats } = firstBuild;
160
179
  moduleFiles = [];
161
180
  noModuleFiles = [];
162
181
  // Common options for all bundle process actions
@@ -166,6 +185,7 @@ function buildWebpackBrowser(options, context, transforms = {}) {
166
185
  sourceMaps: sourceMapOptions.scripts,
167
186
  hiddenSourceMaps: sourceMapOptions.hidden,
168
187
  vendorSourceMaps: sourceMapOptions.vendor,
188
+ integrityAlgorithm: options.subresourceIntegrity ? 'sha384' : undefined,
169
189
  };
170
190
  const actions = [];
171
191
  const seen = new Set();
@@ -189,8 +209,9 @@ function buildWebpackBrowser(options, context, transforms = {}) {
189
209
  }
190
210
  seen.add(file.file);
191
211
  // All files at this point except ES5 polyfills are module scripts
192
- const es5Polyfills = file.file.startsWith('polyfills-es5');
193
- if (!es5Polyfills && !file.file.startsWith('polyfills-nomodule-es5')) {
212
+ const es5Polyfills = file.file.startsWith('polyfills-es5') ||
213
+ file.file.startsWith('polyfills-nomodule-es5');
214
+ if (!es5Polyfills) {
194
215
  moduleFiles.push(file);
195
216
  }
196
217
  // If not optimizing then ES2015 polyfills do not need processing
@@ -223,6 +244,9 @@ function buildWebpackBrowser(options, context, transforms = {}) {
223
244
  filename,
224
245
  code,
225
246
  map,
247
+ // id is always present for non-assets
248
+ // tslint:disable-next-line: no-non-null-assertion
249
+ name: file.id,
226
250
  optimizeOnly: true,
227
251
  });
228
252
  continue;
@@ -234,6 +258,9 @@ function buildWebpackBrowser(options, context, transforms = {}) {
234
258
  filename,
235
259
  code,
236
260
  map,
261
+ // id is always present for non-assets
262
+ // tslint:disable-next-line: no-non-null-assertion
263
+ name: file.id,
237
264
  runtime: file.file.startsWith('runtime'),
238
265
  ignoreOriginal: es5Polyfills,
239
266
  });
@@ -246,15 +273,21 @@ function buildWebpackBrowser(options, context, transforms = {}) {
246
273
  // Execute the bundle processing actions
247
274
  context.logger.info('Generating ES5 bundles for differential loading...');
248
275
  const processActions = [];
276
+ let processRuntimeAction;
249
277
  const cacheActions = [];
278
+ const processResults = [];
250
279
  for (const action of actions) {
251
280
  // Create base cache key with elements:
252
281
  // * package version - different build-angular versions cause different final outputs
253
282
  // * code length/hash - ensure cached version matches the same input code
254
- const codeHash = crypto_1.createHash('sha1')
283
+ const algorithm = action.integrityAlgorithm || 'sha1';
284
+ const codeHash = crypto_1.createHash(algorithm)
255
285
  .update(action.code)
256
- .digest('hex');
257
- const baseCacheKey = `${packageVersion}|${action.code.length}|${codeHash}`;
286
+ .digest('base64');
287
+ let baseCacheKey = `${packageVersion}|${action.code.length}|${algorithm}-${codeHash}`;
288
+ if (mangle_options_1.manglingDisabled) {
289
+ baseCacheKey += '|MD';
290
+ }
258
291
  // Postfix added to sourcemap cache keys when vendor sourcemaps are present
259
292
  // Allows non-destructive caching of both variants
260
293
  const SourceMapVendorPostfix = !!action.sourceMaps && action.vendorSourceMaps ? '|vendor' : '';
@@ -280,49 +313,99 @@ function buildWebpackBrowser(options, context, transforms = {}) {
280
313
  }
281
314
  // Attempt to get required cache entries
282
315
  const cacheEntries = [];
316
+ let cached = cacheKeys.length > 0;
283
317
  for (const key of cacheKeys) {
284
318
  if (key) {
285
- cacheEntries.push(await cacache.get.info(cacheDownlevelPath, key));
319
+ const entry = await cacache.get.info(cacheDownlevelPath, key);
320
+ if (!entry) {
321
+ cached = false;
322
+ break;
323
+ }
324
+ cacheEntries.push(entry);
286
325
  }
287
326
  else {
288
327
  cacheEntries.push(null);
289
328
  }
290
329
  }
291
- // Check if required cache entries are present
292
- let cached = cacheKeys.length > 0;
293
- for (let i = 0; i < cacheKeys.length; ++i) {
294
- if (cacheKeys[i] && !cacheEntries[i]) {
295
- cached = false;
296
- break;
297
- }
298
- }
299
330
  // If all required cached entries are present, use the cached entries
300
331
  // Otherwise process the files
301
- if (cached) {
302
- if (cacheEntries[0 /* OriginalCode */]) {
332
+ // If SRI is enabled always process the runtime bundle
333
+ // Lazy route integrity values are stored in the runtime bundle
334
+ if (action.integrityAlgorithm && action.runtime) {
335
+ processRuntimeAction = action;
336
+ }
337
+ else if (cached) {
338
+ const result = { name: action.name };
339
+ if (action.integrityAlgorithm) {
340
+ result.integrity = `${action.integrityAlgorithm}-${codeHash}`;
341
+ }
342
+ let cacheEntry = cacheEntries[0 /* OriginalCode */];
343
+ if (cacheEntry) {
303
344
  cacheActions.push({
304
- src: cacheEntries[0 /* OriginalCode */].path,
345
+ src: cacheEntry.path,
305
346
  dest: action.filename,
306
347
  });
348
+ result.original = {
349
+ filename: action.filename,
350
+ size: cacheEntry.size,
351
+ integrity: cacheEntry.metadata && cacheEntry.metadata.integrity,
352
+ };
353
+ cacheEntry = cacheEntries[1 /* OriginalMap */];
354
+ if (cacheEntry) {
355
+ cacheActions.push({
356
+ src: cacheEntry.path,
357
+ dest: action.filename + '.map',
358
+ });
359
+ result.original.map = {
360
+ filename: action.filename + '.map',
361
+ size: cacheEntry.size,
362
+ };
363
+ }
307
364
  }
308
- if (cacheEntries[1 /* OriginalMap */]) {
309
- cacheActions.push({
310
- src: cacheEntries[1 /* OriginalMap */].path,
311
- dest: action.filename + '.map',
312
- });
365
+ else if (!action.ignoreOriginal) {
366
+ // If the original wasn't processed (and therefore not cached), add info
367
+ result.original = {
368
+ filename: action.filename,
369
+ size: Buffer.byteLength(action.code, 'utf8'),
370
+ map: action.map === undefined
371
+ ? undefined
372
+ : {
373
+ filename: action.filename + '.map',
374
+ size: Buffer.byteLength(action.map, 'utf8'),
375
+ },
376
+ };
313
377
  }
314
- if (cacheEntries[2 /* DownlevelCode */]) {
378
+ cacheEntry = cacheEntries[2 /* DownlevelCode */];
379
+ if (cacheEntry) {
315
380
  cacheActions.push({
316
- src: cacheEntries[2 /* DownlevelCode */].path,
381
+ src: cacheEntry.path,
317
382
  dest: action.filename.replace('es2015', 'es5'),
318
383
  });
384
+ result.downlevel = {
385
+ filename: action.filename.replace('es2015', 'es5'),
386
+ size: cacheEntry.size,
387
+ integrity: cacheEntry.metadata && cacheEntry.metadata.integrity,
388
+ };
389
+ cacheEntry = cacheEntries[3 /* DownlevelMap */];
390
+ if (cacheEntry) {
391
+ cacheActions.push({
392
+ src: cacheEntry.path,
393
+ dest: action.filename.replace('es2015', 'es5') + '.map',
394
+ });
395
+ result.downlevel.map = {
396
+ filename: action.filename.replace('es2015', 'es5') + '.map',
397
+ size: cacheEntry.size,
398
+ };
399
+ }
319
400
  }
320
- if (cacheEntries[3 /* DownlevelMap */]) {
321
- cacheActions.push({
322
- src: cacheEntries[3 /* DownlevelMap */].path,
323
- dest: action.filename.replace('es2015', 'es5') + '.map',
324
- });
325
- }
401
+ processResults.push(result);
402
+ }
403
+ else if (action.runtime) {
404
+ processRuntimeAction = {
405
+ ...action,
406
+ cacheKeys,
407
+ cachePath: cacheDownlevelPath || undefined,
408
+ };
326
409
  }
327
410
  else {
328
411
  processActions.push({
@@ -332,7 +415,24 @@ function buildWebpackBrowser(options, context, transforms = {}) {
332
415
  });
333
416
  }
334
417
  }
418
+ // Workaround Node.js issue prior to 10.16 with copyFile on macOS
419
+ // https://github.com/angular/angular-cli/issues/15544 & https://github.com/nodejs/node/pull/27241
420
+ let copyFileWorkaround = false;
421
+ if (process.platform === 'darwin') {
422
+ const version = process.versions.node.split('.').map(part => Number(part));
423
+ if (version[0] < 10 ||
424
+ version[0] === 11 ||
425
+ (version[0] === 10 && version[1] < 16)) {
426
+ copyFileWorkaround = true;
427
+ }
428
+ }
335
429
  for (const action of cacheActions) {
430
+ if (copyFileWorkaround) {
431
+ try {
432
+ fs.unlinkSync(action.dest);
433
+ }
434
+ catch (_b) { }
435
+ }
336
436
  fs.copyFileSync(action.src, action.dest, fs.constants.COPYFILE_FICLONE);
337
437
  if (process.platform !== 'win32') {
338
438
  // The cache writes entries as readonly and when using copyFile the permissions will also be copied.
@@ -341,28 +441,74 @@ function buildWebpackBrowser(options, context, transforms = {}) {
341
441
  }
342
442
  }
343
443
  if (processActions.length > 0) {
344
- await new Promise((resolve, reject) => {
345
- const workerFile = require.resolve('../utils/process-bundle');
346
- const workers = workerFarm({
347
- maxRetries: 1,
348
- }, path.extname(workerFile) !== '.ts'
349
- ? workerFile
350
- : require.resolve('../utils/process-bundle-bootstrap'), ['process']);
351
- let completed = 0;
352
- const workCallback = (error) => {
353
- if (error) {
354
- workerFarm.end(workers);
355
- reject(error);
356
- }
357
- else if (++completed === processActions.length) {
358
- workerFarm.end(workers);
359
- resolve();
360
- }
361
- };
362
- processActions.forEach(action => workers['process'](action, workCallback));
363
- });
444
+ const workerFile = require.resolve('../utils/process-bundle');
445
+ const executor = new action_executor_1.ActionExecutor(path.extname(workerFile) !== '.ts'
446
+ ? workerFile
447
+ : require.resolve('../utils/process-bundle-bootstrap'), 'process');
448
+ try {
449
+ const results = await executor.executeAll(processActions.map(a => ({ ...a, size: a.code.length })));
450
+ results.forEach(result => processResults.push(result));
451
+ }
452
+ finally {
453
+ executor.stop();
454
+ }
455
+ }
456
+ // Runtime must be processed after all other files
457
+ if (processRuntimeAction) {
458
+ const runtimeOptions = {
459
+ ...processRuntimeAction,
460
+ runtimeData: processResults,
461
+ };
462
+ processResults.push(await Promise.resolve().then(() => require('../utils/process-bundle')).then(m => m.process(runtimeOptions)));
364
463
  }
365
464
  context.logger.info('ES5 bundle generation complete.');
465
+ function generateBundleInfoStats(id, bundle, chunk) {
466
+ return stats_1.generateBundleStats({
467
+ id,
468
+ size: bundle.size,
469
+ files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename],
470
+ names: chunk && chunk.names,
471
+ entry: !!chunk && chunk.names.includes('runtime'),
472
+ initial: !!chunk && chunk.initial,
473
+ rendered: true,
474
+ }, true);
475
+ }
476
+ let bundleInfoText = '';
477
+ const processedNames = new Set();
478
+ for (const result of processResults) {
479
+ processedNames.add(result.name);
480
+ const chunk = webpackStats &&
481
+ webpackStats.chunks &&
482
+ webpackStats.chunks.find(c => result.name === c.id.toString());
483
+ if (result.original) {
484
+ bundleInfoText +=
485
+ '\n' + generateBundleInfoStats(result.name, result.original, chunk);
486
+ }
487
+ if (result.downlevel) {
488
+ bundleInfoText +=
489
+ '\n' + generateBundleInfoStats(result.name, result.downlevel, chunk);
490
+ }
491
+ }
492
+ if (webpackStats && webpackStats.chunks) {
493
+ for (const chunk of webpackStats.chunks) {
494
+ if (processedNames.has(chunk.id.toString())) {
495
+ continue;
496
+ }
497
+ const asset = webpackStats.assets && webpackStats.assets.find(a => a.name === chunk.files[0]);
498
+ bundleInfoText +=
499
+ '\n' + stats_1.generateBundleStats({ ...chunk, size: asset && asset.size }, true);
500
+ }
501
+ }
502
+ bundleInfoText +=
503
+ '\n' +
504
+ stats_1.generateBuildStats((webpackStats && webpackStats.hash) || '<unknown>', Date.now() - startTime, true);
505
+ context.logger.info(bundleInfoText);
506
+ if (webpackStats && webpackStats.warnings.length > 0) {
507
+ context.logger.warn(stats_1.statsWarningsToString(webpackStats, { colors: true }));
508
+ }
509
+ if (webpackStats && webpackStats.errors.length > 0) {
510
+ context.logger.error(stats_1.statsErrorsToString(webpackStats, { colors: true }));
511
+ }
366
512
  }
367
513
  else {
368
514
  const { emittedFiles = [] } = firstBuild;
@@ -71,7 +71,7 @@ async function execute(options, context) {
71
71
  if (options.webdriverUpdate) {
72
72
  await updateWebdriver();
73
73
  }
74
- let baseUrl;
74
+ let baseUrl = options.baseUrl;
75
75
  let server;
76
76
  if (options.devServerTarget) {
77
77
  const target = architect_1.targetFromTargetString(options.devServerTarget);
@@ -2,15 +2,33 @@ export interface ProcessBundleOptions {
2
2
  filename: string;
3
3
  code: string;
4
4
  map?: string;
5
+ name: string;
5
6
  sourceMaps?: boolean;
6
7
  hiddenSourceMaps?: boolean;
7
8
  vendorSourceMaps?: boolean;
8
9
  runtime?: boolean;
9
- optimize: boolean;
10
+ optimize?: boolean;
10
11
  optimizeOnly?: boolean;
11
12
  ignoreOriginal?: boolean;
12
13
  cacheKeys?: (string | null)[];
13
14
  cachePath?: string;
15
+ integrityAlgorithm?: 'sha256' | 'sha384' | 'sha512';
16
+ runtimeData?: ProcessBundleResult[];
17
+ }
18
+ export interface ProcessBundleResult {
19
+ name: string;
20
+ integrity?: string;
21
+ original?: ProcessBundleFile;
22
+ downlevel?: ProcessBundleFile;
23
+ }
24
+ export interface ProcessBundleFile {
25
+ filename: string;
26
+ size: number;
27
+ integrity?: string;
28
+ map?: {
29
+ filename: string;
30
+ size: number;
31
+ };
14
32
  }
15
33
  export declare const enum CacheKey {
16
34
  OriginalCode = 0,
@@ -18,4 +36,4 @@ export declare const enum CacheKey {
18
36
  DownlevelCode = 2,
19
37
  DownlevelMap = 3
20
38
  }
21
- export declare function process(options: ProcessBundleOptions, callback: (error: Error | null, result?: {}) => void): void;
39
+ export declare function process(options: ProcessBundleOptions): Promise<ProcessBundleResult>;
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  * Use of this source code is governed by an MIT-style license that can be
8
8
  * found in the LICENSE file at https://angular.io/license
9
9
  */
10
+ const crypto_1 = require("crypto");
10
11
  const fs = require("fs");
11
12
  const path = require("path");
12
13
  const source_map_1 = require("source-map");
@@ -14,34 +15,47 @@ const terser_1 = require("terser");
14
15
  const mangle_options_1 = require("./mangle-options");
15
16
  const { transformAsync } = require('@babel/core');
16
17
  const cacache = require('cacache');
17
- function process(options, callback) {
18
- processWorker(options).then(() => callback(null, {}), error => callback(error));
19
- }
20
- exports.process = process;
21
- async function processWorker(options) {
18
+ async function process(options) {
22
19
  if (!options.cacheKeys) {
23
20
  options.cacheKeys = [];
24
21
  }
25
22
  // If no downlevelling required than just mangle code and return
26
23
  if (options.optimizeOnly) {
27
- return mangleOriginal(options);
24
+ const result = { name: options.name };
25
+ if (options.integrityAlgorithm) {
26
+ result.integrity = generateIntegrityValue(options.integrityAlgorithm, options.code);
27
+ }
28
+ // Replace integrity hashes with updated values
29
+ // NOTE: This should eventually be a babel plugin
30
+ if (options.runtime && options.integrityAlgorithm && options.runtimeData) {
31
+ for (const data of options.runtimeData) {
32
+ if (!data.integrity || !data.original || !data.original.integrity) {
33
+ continue;
34
+ }
35
+ options.code = options.code.replace(data.integrity, data.original.integrity);
36
+ }
37
+ }
38
+ result.original = await mangleOriginal(options);
39
+ return result;
28
40
  }
29
41
  // if code size is larger than 500kB, manually handle sourcemaps with newer source-map package.
30
42
  // babel currently uses an older version that still supports sync calls
31
43
  const codeSize = Buffer.byteLength(options.code, 'utf8');
32
- const manualSourceMaps = codeSize >= 500 * 1024;
44
+ const mapSize = options.map ? Buffer.byteLength(options.map, 'utf8') : 0;
45
+ const manualSourceMaps = codeSize >= 500 * 1024 || mapSize >= 500 * 1024;
33
46
  // downlevel the bundle
34
47
  let { code, map } = await transformAsync(options.code, {
35
48
  filename: options.filename,
36
49
  inputSourceMap: !manualSourceMaps && options.map !== undefined && JSON.parse(options.map),
37
50
  babelrc: false,
38
- // modules aren't needed since the bundles use webpacks custom module loading
39
- // loose generates more ES5-like code but does not strictly adhere to the ES2015 spec (Typescript is loose)
51
+ // modules aren't needed since the bundles use webpack's custom module loading
40
52
  // 'transform-typeof-symbol' generates slower code
41
53
  presets: [
42
- ['@babel/preset-env', { modules: false, loose: true, exclude: ['transform-typeof-symbol'] }],
54
+ ['@babel/preset-env', { modules: false, exclude: ['transform-typeof-symbol'] }],
43
55
  ],
44
- minified: true,
56
+ minified: options.optimize,
57
+ // `false` ensures it is disabled and prevents large file warnings
58
+ compact: options.optimize || false,
45
59
  sourceMaps: options.sourceMaps,
46
60
  });
47
61
  const newFilePath = options.filename.replace('es2015', 'es5');
@@ -49,6 +63,16 @@ async function processWorker(options) {
49
63
  // Extra spacing is intentional to align source line positions
50
64
  if (options.runtime) {
51
65
  code = code.replace('"-es2015.', ' "-es5.');
66
+ // Replace integrity hashes with updated values
67
+ // NOTE: This should eventually be a babel plugin
68
+ if (options.integrityAlgorithm && options.runtimeData) {
69
+ for (const data of options.runtimeData) {
70
+ if (!data.integrity || !data.downlevel || !data.downlevel.integrity) {
71
+ continue;
72
+ }
73
+ code = code.replace(data.integrity, data.downlevel.integrity);
74
+ }
75
+ }
52
76
  }
53
77
  if (options.sourceMaps && manualSourceMaps && options.map) {
54
78
  const generator = new source_map_1.SourceMapGenerator();
@@ -88,11 +112,12 @@ async function processWorker(options) {
88
112
  map.file = path.basename(newFilePath);
89
113
  map.sourceRoot = sourceRoot;
90
114
  }
115
+ const result = { name: options.name };
91
116
  if (options.optimize) {
92
117
  // Note: Investigate converting the AST instead of re-parsing
93
118
  // estree -> terser is already supported; need babel -> estree/terser
94
119
  // Mangle downlevel code
95
- const result = terser_1.minify(code, {
120
+ const minifyOutput = terser_1.minify(code, {
96
121
  compress: true,
97
122
  ecma: 5,
98
123
  mangle: !mangle_options_1.manglingDisabled,
@@ -106,14 +131,14 @@ async function processWorker(options) {
106
131
  content: map,
107
132
  },
108
133
  });
109
- if (result.error) {
110
- throw result.error;
134
+ if (minifyOutput.error) {
135
+ throw minifyOutput.error;
111
136
  }
112
- code = result.code;
113
- map = result.map;
137
+ code = minifyOutput.code;
138
+ map = minifyOutput.map;
114
139
  // Mangle original code
115
140
  if (!options.ignoreOriginal) {
116
- await mangleOriginal(options);
141
+ result.original = await mangleOriginal(options);
117
142
  }
118
143
  }
119
144
  else if (map) {
@@ -128,11 +153,23 @@ async function processWorker(options) {
128
153
  }
129
154
  fs.writeFileSync(newFilePath + '.map', map);
130
155
  }
156
+ result.downlevel = createFileEntry(newFilePath, code, map, options.integrityAlgorithm);
131
157
  if (options.cachePath && options.cacheKeys[2 /* DownlevelCode */]) {
132
- await cacache.put(options.cachePath, options.cacheKeys[2 /* DownlevelCode */], code);
158
+ await cacache.put(options.cachePath, options.cacheKeys[2 /* DownlevelCode */], code, {
159
+ metadata: { integrity: result.downlevel.integrity },
160
+ });
133
161
  }
134
162
  fs.writeFileSync(newFilePath, code);
163
+ // If original was not processed, add info
164
+ if (!result.original && !options.ignoreOriginal) {
165
+ result.original = createFileEntry(options.filename, options.code, options.map, options.integrityAlgorithm);
166
+ }
167
+ if (options.integrityAlgorithm) {
168
+ result.integrity = generateIntegrityValue(options.integrityAlgorithm, options.code);
169
+ }
170
+ return result;
135
171
  }
172
+ exports.process = process;
136
173
  async function mangleOriginal(options) {
137
174
  const resultOriginal = terser_1.minify(options.code, {
138
175
  compress: false,
@@ -161,8 +198,34 @@ async function mangleOriginal(options) {
161
198
  }
162
199
  fs.writeFileSync(options.filename + '.map', resultOriginal.map);
163
200
  }
201
+ const fileResult = createFileEntry(options.filename,
202
+ // tslint:disable-next-line: no-non-null-assertion
203
+ resultOriginal.code, resultOriginal.map, options.integrityAlgorithm);
164
204
  if (options.cachePath && options.cacheKeys && options.cacheKeys[0 /* OriginalCode */]) {
165
- await cacache.put(options.cachePath, options.cacheKeys[0 /* OriginalCode */], resultOriginal.code);
205
+ await cacache.put(options.cachePath, options.cacheKeys[0 /* OriginalCode */], resultOriginal.code, {
206
+ metadata: { integrity: fileResult.integrity },
207
+ });
166
208
  }
167
209
  fs.writeFileSync(options.filename, resultOriginal.code);
210
+ return fileResult;
211
+ }
212
+ function createFileEntry(filename, code, map, integrityAlgorithm) {
213
+ return {
214
+ filename: filename,
215
+ size: Buffer.byteLength(code),
216
+ integrity: integrityAlgorithm && generateIntegrityValue(integrityAlgorithm, code),
217
+ map: !map
218
+ ? undefined
219
+ : {
220
+ filename: filename + '.map',
221
+ size: Buffer.byteLength(map),
222
+ },
223
+ };
224
+ }
225
+ function generateIntegrityValue(hashAlgorithm, code) {
226
+ return (hashAlgorithm +
227
+ '-' +
228
+ crypto_1.createHash(hashAlgorithm)
229
+ .update(code)
230
+ .digest('base64'));
168
231
  }