Skip to content

Commit d1488e6

Browse files
clydinKeen Yee Liau
authored and
Keen Yee Liau
committed
refactor(@angular-devkit/build-angular): cache downlevel bundles
1 parent 0aae147 commit d1488e6

File tree

6 files changed

+347
-50
lines changed

6 files changed

+347
-50
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"@types/clean-css": "^4.2.1",
9393
"@types/copy-webpack-plugin": "^4.4.1",
9494
"@types/express": "^4.16.0",
95+
"@types/find-cache-dir": "^2.0.0",
9596
"@types/glob": "^7.0.0",
9697
"@types/inquirer": "^0.0.44",
9798
"@types/jasmine": "~3.4.0",

packages/angular_devkit/build_angular/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
"ajv": "6.10.2",
1818
"autoprefixer": "9.6.1",
1919
"browserslist": "4.6.6",
20+
"cacache": "12.0.2",
2021
"caniuse-lite": "1.0.30000989",
2122
"circular-dependency-plugin": "5.2.0",
2223
"clean-css": "4.2.1",
2324
"copy-webpack-plugin": "5.0.4",
2425
"core-js": "3.2.1",
2526
"file-loader": "4.2.0",
27+
"find-cache-dir": "3.0.0",
2628
"glob": "7.1.4",
2729
"istanbul-instrumenter-loader": "3.0.1",
2830
"karma-source-map-support": "1.4.0",

packages/angular_devkit/build_angular/src/browser/index.ts

+137-23
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import {
2424
virtualFs,
2525
} from '@angular-devkit/core';
2626
import { NodeJsSyncHost } from '@angular-devkit/core/node';
27+
import { createHash } from 'crypto';
28+
import * as findCacheDirectory from 'find-cache-dir';
2729
import * as fs from 'fs';
30+
import * as os from 'os';
2831
import * as path from 'path';
2932
import { from, of } from 'rxjs';
3033
import { bufferCount, catchError, concatMap, map, mergeScan, switchMap } from 'rxjs/operators';
@@ -62,6 +65,7 @@ import {
6265
normalizeOptimization,
6366
normalizeSourceMaps,
6467
} from '../utils';
68+
import { CacheKey, ProcessBundleOptions } from '../utils/process-bundle';
6569
import { assertCompatibleAngularVersion } from '../utils/version';
6670
import {
6771
generateBrowserWebpackConfigFromContext,
@@ -70,6 +74,10 @@ import {
7074
} from '../utils/webpack-browser-config';
7175
import { Schema as BrowserBuilderSchema } from './schema';
7276

77+
const cacache = require('cacache');
78+
const cacheDownlevelPath = findCacheDirectory({ name: 'angular-build-dl' });
79+
const packageVersion = require('../../package.json').version;
80+
7381
export type BrowserBuilderOutput = json.JsonObject &
7482
BuilderOutput & {
7583
outputPath: string;
@@ -240,6 +248,7 @@ export function buildWebpackBrowser(
240248
1,
241249
),
242250
bufferCount(configs.length),
251+
// tslint:disable-next-line: no-big-function
243252
switchMap(async buildEvents => {
244253
configs.length = 0;
245254
const success = buildEvents.every(r => r.success);
@@ -274,9 +283,10 @@ export function buildWebpackBrowser(
274283
optimize: normalizeOptimization(options.optimization).scripts,
275284
sourceMaps: sourceMapOptions.scripts,
276285
hiddenSourceMaps: sourceMapOptions.hidden,
286+
vendorSourceMaps: sourceMapOptions.vendor,
277287
};
278288

279-
const actions: {}[] = [];
289+
const actions: ProcessBundleOptions[] = [];
280290
const seen = new Set<string>();
281291
for (const file of emittedFiles) {
282292
// Scripts and non-javascript files are not processed
@@ -348,6 +358,7 @@ export function buildWebpackBrowser(
348358
code,
349359
map,
350360
runtime: file.file.startsWith('runtime'),
361+
ignoreOriginal: es5Polyfills,
351362
});
352363

353364
// Add the newly created ES5 bundles to the index as nomodule scripts
@@ -359,30 +370,133 @@ export function buildWebpackBrowser(
359370

360371
// Execute the bundle processing actions
361372
context.logger.info('Generating ES5 bundles for differential loading...');
362-
await new Promise<void>((resolve, reject) => {
363-
const workerFile = require.resolve('../utils/process-bundle');
364-
const workers = workerFarm(
365-
{
366-
maxRetries: 1,
367-
},
368-
path.extname(workerFile) !== '.ts'
369-
? workerFile
370-
: require.resolve('../utils/process-bundle-bootstrap'),
371-
['process'],
372-
);
373-
let completed = 0;
374-
const workCallback = (error: Error | null) => {
375-
if (error) {
376-
workerFarm.end(workers);
377-
reject(error);
378-
} else if (++completed === actions.length) {
379-
workerFarm.end(workers);
380-
resolve();
373+
374+
const processActions: typeof actions = [];
375+
const cacheActions: { src: string; dest: string }[] = [];
376+
for (const action of actions) {
377+
// Create base cache key with elements:
378+
// * package version - different build-angular versions cause different final outputs
379+
// * code length/hash - ensure cached version matches the same input code
380+
const codeHash = createHash('sha1')
381+
.update(action.code)
382+
.digest('hex');
383+
const baseCacheKey = `${packageVersion}|${action.code.length}|${codeHash}`;
384+
385+
// Postfix added to sourcemap cache keys when vendor sourcemaps are present
386+
// Allows non-destructive caching of both variants
387+
const SourceMapVendorPostfix =
388+
!!action.sourceMaps && action.vendorSourceMaps ? '|vendor' : '';
389+
390+
// Determine cache entries required based on build settings
391+
const cacheKeys = [];
392+
393+
// If optimizing and the original is not ignored, add original as required
394+
if ((action.optimize || action.optimizeOnly) && !action.ignoreOriginal) {
395+
cacheKeys[CacheKey.OriginalCode] = baseCacheKey + '|orig';
396+
397+
// If sourcemaps are enabled, add original sourcemap as required
398+
if (action.sourceMaps) {
399+
cacheKeys[CacheKey.OriginalMap] =
400+
baseCacheKey + SourceMapVendorPostfix + '|orig-map';
401+
}
402+
}
403+
// If not only optimizing, add downlevel as required
404+
if (!action.optimizeOnly) {
405+
cacheKeys[CacheKey.DownlevelCode] = baseCacheKey + '|dl';
406+
407+
// If sourcemaps are enabled, add downlevel sourcemap as required
408+
if (action.sourceMaps) {
409+
cacheKeys[CacheKey.DownlevelMap] =
410+
baseCacheKey + SourceMapVendorPostfix + '|dl-map';
411+
}
412+
}
413+
414+
// Attempt to get required cache entries
415+
const cacheEntries = [];
416+
for (const key of cacheKeys) {
417+
if (key) {
418+
cacheEntries.push(await cacache.get.info(cacheDownlevelPath, key));
419+
} else {
420+
cacheEntries.push(null);
421+
}
422+
}
423+
424+
// Check if required cache entries are present
425+
let cached = cacheKeys.length > 0;
426+
for (let i = 0; i < cacheKeys.length; ++i) {
427+
if (cacheKeys[i] && !cacheEntries[i]) {
428+
cached = false;
429+
break;
430+
}
431+
}
432+
433+
// If all required cached entries are present, use the cached entries
434+
// Otherwise process the files
435+
if (cached) {
436+
if (cacheEntries[CacheKey.OriginalCode]) {
437+
cacheActions.push({
438+
src: cacheEntries[CacheKey.OriginalCode].path,
439+
dest: action.filename,
440+
});
381441
}
382-
};
442+
if (cacheEntries[CacheKey.OriginalMap]) {
443+
cacheActions.push({
444+
src: cacheEntries[CacheKey.OriginalMap].path,
445+
dest: action.filename + '.map',
446+
});
447+
}
448+
if (cacheEntries[CacheKey.DownlevelCode]) {
449+
cacheActions.push({
450+
src: cacheEntries[CacheKey.DownlevelCode].path,
451+
dest: action.filename.replace('es2015', 'es5'),
452+
});
453+
}
454+
if (cacheEntries[CacheKey.DownlevelMap]) {
455+
cacheActions.push({
456+
src: cacheEntries[CacheKey.DownlevelMap].path,
457+
dest: action.filename.replace('es2015', 'es5') + '.map',
458+
});
459+
}
460+
} else {
461+
processActions.push({
462+
...action,
463+
cacheKeys,
464+
cachePath: cacheDownlevelPath || undefined,
465+
});
466+
}
467+
}
468+
469+
for (const action of cacheActions) {
470+
fs.copyFileSync(action.src, action.dest, fs.constants.COPYFILE_FICLONE);
471+
}
472+
473+
if (processActions.length > 0) {
474+
await new Promise<void>((resolve, reject) => {
475+
const workerFile = require.resolve('../utils/process-bundle');
476+
const workers = workerFarm(
477+
{
478+
maxRetries: 1,
479+
},
480+
path.extname(workerFile) !== '.ts'
481+
? workerFile
482+
: require.resolve('../utils/process-bundle-bootstrap'),
483+
['process'],
484+
);
485+
let completed = 0;
486+
const workCallback = (error: Error | null) => {
487+
if (error) {
488+
workerFarm.end(workers);
489+
reject(error);
490+
} else if (++completed === processActions.length) {
491+
workerFarm.end(workers);
492+
resolve();
493+
}
494+
};
495+
496+
processActions.forEach(action => workers['process'](action, workCallback));
497+
});
498+
}
383499

384-
actions.forEach(action => workers['process'](action, workCallback));
385-
});
386500
context.logger.info('ES5 bundle generation complete.');
387501
} else {
388502
const { emittedFiles = [] } = firstBuild;

packages/angular_devkit/build_angular/src/utils/process-bundle.ts

+48-6
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,28 @@ import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
1111
import { minify } from 'terser';
1212

1313
const { transformAsync } = require('@babel/core');
14+
const cacache = require('cacache');
1415

15-
interface ProcessBundleOptions {
16+
export interface ProcessBundleOptions {
1617
filename: string;
1718
code: string;
1819
map?: string;
19-
sourceMaps: boolean;
20-
hiddenSourceMaps: boolean;
21-
runtime: boolean;
20+
sourceMaps?: boolean;
21+
hiddenSourceMaps?: boolean;
22+
vendorSourceMaps?: boolean;
23+
runtime?: boolean;
2224
optimize: boolean;
2325
optimizeOnly?: boolean;
26+
ignoreOriginal?: boolean;
27+
cacheKeys?: (string | null)[];
28+
cachePath?: string;
29+
}
30+
31+
export const enum CacheKey {
32+
OriginalCode = 0,
33+
OriginalMap = 1,
34+
DownlevelCode = 2,
35+
DownlevelMap = 3,
2436
}
2537

2638
export function process(
@@ -31,6 +43,10 @@ export function process(
3143
}
3244

3345
async function processWorker(options: ProcessBundleOptions): Promise<void> {
46+
if (!options.cacheKeys) {
47+
options.cacheKeys = [];
48+
}
49+
3450
// If no downlevelling required than just mangle code and return
3551
if (options.optimizeOnly) {
3652
return mangleOriginal(options);
@@ -139,7 +155,9 @@ async function processWorker(options: ProcessBundleOptions): Promise<void> {
139155
map = result.map;
140156

141157
// Mangle original code
142-
mangleOriginal(options);
158+
if (!options.ignoreOriginal) {
159+
await mangleOriginal(options);
160+
}
143161
} else if (map) {
144162
map = JSON.stringify(map);
145163
}
@@ -149,13 +167,20 @@ async function processWorker(options: ProcessBundleOptions): Promise<void> {
149167
code += `\n//# sourceMappingURL=${path.basename(newFilePath)}.map`;
150168
}
151169

170+
if (options.cachePath && options.cacheKeys[CacheKey.DownlevelMap]) {
171+
await cacache.put(options.cachePath, options.cacheKeys[CacheKey.DownlevelMap], map);
172+
}
173+
152174
fs.writeFileSync(newFilePath + '.map', map);
153175
}
154176

177+
if (options.cachePath && options.cacheKeys[CacheKey.DownlevelCode]) {
178+
await cacache.put(options.cachePath, options.cacheKeys[CacheKey.DownlevelCode], code);
179+
}
155180
fs.writeFileSync(newFilePath, code);
156181
}
157182

158-
function mangleOriginal(options: ProcessBundleOptions): void {
183+
async function mangleOriginal(options: ProcessBundleOptions): Promise<void> {
159184
const resultOriginal = minify(options.code, {
160185
compress: false,
161186
ecma: 6,
@@ -176,8 +201,25 @@ function mangleOriginal(options: ProcessBundleOptions): void {
176201
throw resultOriginal.error;
177202
}
178203

204+
if (options.cachePath && options.cacheKeys && options.cacheKeys[CacheKey.OriginalCode]) {
205+
await cacache.put(
206+
options.cachePath,
207+
options.cacheKeys[CacheKey.OriginalCode],
208+
resultOriginal.code,
209+
);
210+
}
211+
179212
fs.writeFileSync(options.filename, resultOriginal.code);
213+
180214
if (resultOriginal.map) {
215+
if (options.cachePath && options.cacheKeys && options.cacheKeys[CacheKey.OriginalMap]) {
216+
await cacache.put(
217+
options.cachePath,
218+
options.cacheKeys[CacheKey.OriginalMap],
219+
resultOriginal.map,
220+
);
221+
}
222+
181223
fs.writeFileSync(options.filename + '.map', resultOriginal.map);
182224
}
183225
}

0 commit comments

Comments
 (0)