Skip to content

Commit fbd239c

Browse files
committed
feat(@angular-devkit/build-angular): implement bundle level downleveling to support differential loading
1 parent 18e6cc4 commit fbd239c

File tree

21 files changed

+1294
-215
lines changed

21 files changed

+1294
-215
lines changed

packages/angular_devkit/build_angular/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"@angular-devkit/build-optimizer": "0.0.0",
1212
"@angular-devkit/build-webpack": "0.0.0",
1313
"@angular-devkit/core": "0.0.0",
14+
"@babel/core": "7.5.5",
15+
"@babel/preset-env": "7.5.5",
1416
"@ngtools/webpack": "0.0.0",
1517
"ajv": "6.10.2",
1618
"autoprefixer": "9.6.1",
@@ -40,20 +42,23 @@
4042
"sass": "1.22.9",
4143
"sass-loader": "7.1.0",
4244
"semver": "6.3.0",
45+
"source-map": "0.7.3",
4346
"source-map-support": "0.5.13",
4447
"source-map-loader": "0.2.4",
4548
"speed-measure-webpack-plugin": "1.3.1",
4649
"style-loader": "0.23.1",
4750
"stylus": "0.54.5",
4851
"stylus-loader": "3.0.2",
4952
"tree-kill": "1.2.1",
53+
"terser": "4.1.3",
5054
"terser-webpack-plugin": "1.4.1",
5155
"webpack": "4.38.0",
5256
"webpack-dev-middleware": "3.7.0",
5357
"webpack-dev-server": "3.7.2",
5458
"webpack-merge": "4.2.1",
5559
"webpack-sources": "1.4.1",
5660
"webpack-subresource-integrity": "1.1.0-rc.6",
61+
"worker-farm": "1.7.0",
5762
"worker-plugin": "3.1.0"
5863
},
5964
"devDependencies": {

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from 'webpack';
2424
import { RawSource } from 'webpack-sources';
2525
import { AssetPatternClass, ExtraEntryPoint } from '../../../browser/schema';
26-
import { BuildBrowserFeatures } from '../../../utils/build-browser-features';
26+
import { BuildBrowserFeatures, fullDifferential } from '../../../utils';
2727
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
2828
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
2929
import { NamedLazyChunksPlugin } from '../../plugins/named-chunks-plugin';
@@ -59,19 +59,23 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
5959
const entryPoints: { [key: string]: string[] } = {};
6060

6161
const targetInFileName = getEsVersionForFileName(
62-
buildOptions.scriptTargetOverride,
62+
fullDifferential ? buildOptions.scriptTargetOverride : tsConfig.options.target,
6363
buildOptions.esVersionInFileName,
6464
);
6565

6666
if (buildOptions.main) {
6767
entryPoints['main'] = [path.resolve(root, buildOptions.main)];
6868
}
6969

70+
let differentialLoadingNeeded = false;
7071
if (wco.buildOptions.platform !== 'server') {
7172
const buildBrowserFeatures = new BuildBrowserFeatures(
7273
projectRoot,
7374
tsConfig.options.target || ScriptTarget.ES5,
7475
);
76+
77+
differentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded();
78+
7579
if ((buildOptions.scriptTargetOverride || tsConfig.options.target) === ScriptTarget.ES5) {
7680
if (
7781
buildOptions.es5BrowserSupport ||
@@ -90,16 +94,28 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
9094
: [noModuleScript];
9195
}
9296

93-
// For differential loading we don't need to generate a seperate polyfill file
97+
// For full build differential loading we don't need to generate a seperate polyfill file
9498
// because they will be loaded exclusivly based on module and nomodule
95-
const polyfillsChunkName = buildBrowserFeatures.isDifferentialLoadingNeeded()
96-
? 'polyfills'
97-
: 'polyfills-es5';
99+
const polyfillsChunkName =
100+
fullDifferential && differentialLoadingNeeded ? 'polyfills' : 'polyfills-es5';
98101

99102
entryPoints[polyfillsChunkName] = [path.join(__dirname, '..', 'es5-polyfills.js')];
103+
if (!fullDifferential && differentialLoadingNeeded) {
104+
// Add zone.js legacy support to the es5 polyfills
105+
// This is a noop execution-wise if zone-evergreen is not used.
106+
entryPoints[polyfillsChunkName].push('zone.js/dist/zone-legacy');
107+
}
100108
if (!buildOptions.aot) {
109+
// If not performing a full differential build the JIT polyfills need to be added to ES5
110+
if (!fullDifferential && differentialLoadingNeeded) {
111+
entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'jit-polyfills.js'));
112+
}
101113
entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'es5-jit-polyfills.js'));
102114
}
115+
// If not performing a full differential build the polyfills need to be added to ES5 bundle
116+
if (!fullDifferential && buildOptions.polyfills) {
117+
entryPoints[polyfillsChunkName].push(path.resolve(root, buildOptions.polyfills));
118+
}
103119
}
104120
}
105121

@@ -313,7 +329,6 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
313329
warnings: !!buildOptions.verbose,
314330
safari10: true,
315331
output: {
316-
ascii_only: true,
317332
comments: false,
318333
webkit: true,
319334
},
@@ -332,7 +347,10 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
332347
global_defs: angularGlobalDefinitions,
333348
},
334349
// We also want to avoid mangling on server.
335-
...(buildOptions.platform == 'server' ? { mangle: false } : {}),
350+
// Name mangling is handled within the browser builder
351+
mangle:
352+
buildOptions.platform !== 'server' &&
353+
(!differentialLoadingNeeded || (differentialLoadingNeeded && fullDifferential)),
336354
};
337355

338356
extraMinimizers.push(

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
NgToolsLoader,
1616
PLATFORM
1717
} from '@ngtools/webpack';
18+
import { fullDifferential } from '../../../utils';
1819
import { WebpackConfigOptions, BuildOptions } from '../build-options';
1920

2021
function _pluginOptionsOverrides(
@@ -32,7 +33,7 @@ function _pluginOptionsOverrides(
3233
}
3334
}
3435

35-
if (buildOptions.scriptTargetOverride) {
36+
if (fullDifferential && buildOptions.scriptTargetOverride) {
3637
compilerOptions.target = buildOptions.scriptTargetOverride;
3738
}
3839

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/index-file/augment-index-html.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
150150
const isModuleType = moduleFiles.some(scriptPredictor);
151151

152152
if (isNoModuleType && !isModuleType) {
153-
attrs.push({ name: 'nomodule', value: null }, { name: 'defer', value: null });
153+
attrs.push({ name: 'nomodule', value: null });
154+
if (!script.startsWith('polyfills-nomodule-es5')) {
155+
attrs.push({ name: 'defer', value: null });
156+
}
154157
} else if (isModuleType && !isNoModuleType) {
155158
attrs.push({ name: 'type', value: 'module' });
156159
} else {

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

+168-27
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { from, of } from 'rxjs';
3030
import { bufferCount, catchError, concatMap, map, mergeScan, switchMap } from 'rxjs/operators';
3131
import { ScriptTarget } from 'typescript';
3232
import * as webpack from 'webpack';
33+
import * as workerFarm from 'worker-farm';
3334
import { NgBuildAnalyticsPlugin } from '../../plugins/webpack/analytics';
3435
import { WebpackConfigOptions } from '../angular-cli-files/models/build-options';
3536
import {
@@ -54,7 +55,12 @@ import {
5455
statsWarningsToString,
5556
} from '../angular-cli-files/utilities/stats';
5657
import { ExecutionTransformer } from '../transforms';
57-
import { BuildBrowserFeatures, deleteOutputDir } from '../utils';
58+
import {
59+
BuildBrowserFeatures,
60+
deleteOutputDir,
61+
fullDifferential,
62+
normalizeOptimization,
63+
} from '../utils';
5864
import { assertCompatibleAngularVersion } from '../utils/version';
5965
import {
6066
generateBrowserWebpackConfigFromContext,
@@ -168,6 +174,7 @@ async function initialize(
168174
return { config: transformedConfig || config, workspace };
169175
}
170176

177+
// tslint:disable-next-line: no-big-function
171178
export function buildWebpackBrowser(
172179
options: BrowserBuilderSchema,
173180
context: BuilderContext,
@@ -187,6 +194,7 @@ export function buildWebpackBrowser(
187194
transforms.logging || createBrowserLoggingCallback(!!options.verbose, context.logger);
188195

189196
return from(initialize(options, context, host, transforms.webpackConfiguration)).pipe(
197+
// tslint:disable-next-line: no-big-function
190198
switchMap(({ workspace, config: configs }) => {
191199
const projectName = context.target
192200
? context.target.project
@@ -231,50 +239,183 @@ export function buildWebpackBrowser(
231239
1,
232240
),
233241
bufferCount(configs.length),
234-
switchMap(buildEvents => {
242+
switchMap(async buildEvents => {
243+
configs.length = 0;
235244
const success = buildEvents.every(r => r.success);
236-
if (success && options.index) {
245+
if (success) {
237246
let noModuleFiles: EmittedFiles[] | undefined;
238247
let moduleFiles: EmittedFiles[] | undefined;
239248
let files: EmittedFiles[] | undefined;
240249

241-
const [firstBuild, secondBuild] = buildEvents;
242-
if (isDifferentialLoadingNeeded) {
243-
const scriptsEntryPointName = normalizeExtraEntryPoints(options.scripts || [], 'scripts')
244-
.map(x => x.bundleName);
250+
const scriptsEntryPointName = normalizeExtraEntryPoints(
251+
options.scripts || [],
252+
'scripts',
253+
).map(x => x.bundleName);
245254

255+
const [firstBuild, secondBuild] = buildEvents;
256+
if (isDifferentialLoadingNeeded && (fullDifferential || options.watch)) {
246257
moduleFiles = firstBuild.emittedFiles || [];
247-
files = moduleFiles.filter(x => x.extension === '.css' || (x.name && scriptsEntryPointName.includes(x.name)));
258+
files = moduleFiles.filter(
259+
x => x.extension === '.css' || (x.name && scriptsEntryPointName.includes(x.name)),
260+
);
248261

249262
if (buildEvents.length === 2) {
250263
noModuleFiles = secondBuild.emittedFiles;
251264
}
265+
} else if (isDifferentialLoadingNeeded && !fullDifferential) {
266+
const { emittedFiles = [] } = firstBuild;
267+
moduleFiles = [];
268+
noModuleFiles = [];
269+
270+
// Common options for all bundle process actions
271+
const actionOptions = {
272+
optimize: normalizeOptimization(options.optimization).scripts,
273+
sourceMaps:
274+
options.sourceMap === true ||
275+
(typeof options.sourceMap === 'object' && options.sourceMap.scripts),
276+
};
277+
278+
const actions: {}[] = [];
279+
const seen = new Set<string>();
280+
for (const file of emittedFiles) {
281+
// Scripts and non-javascript files are not processed
282+
if (
283+
file.extension !== '.js' ||
284+
(file.name && scriptsEntryPointName.includes(file.name))
285+
) {
286+
if (files === undefined) {
287+
files = [];
288+
}
289+
files.push(file);
290+
continue;
291+
}
292+
293+
// Ignore already processed files; emittedFiles can contain duplicates
294+
if (seen.has(file.file)) {
295+
continue;
296+
}
297+
seen.add(file.file);
298+
299+
// All files at this point except ES5 polyfills are module scripts
300+
const es5Polyfills = file.file.startsWith('polyfills-es5');
301+
if (!es5Polyfills && !file.file.startsWith('polyfills-nomodule-es5')) {
302+
moduleFiles.push(file);
303+
}
304+
// If not optimizing then ES2015 polyfills do not need processing
305+
// Unlike other module scripts, it is never downleveled
306+
if (!actionOptions.optimize && file.file.startsWith('polyfills-es2015')) {
307+
continue;
308+
}
309+
310+
// Retrieve the content/map for the file
311+
// NOTE: Additional future optimizations will read directly from memory
312+
let filename = path.resolve(getSystemPath(root), options.outputPath, file.file);
313+
const code = fs.readFileSync(filename, 'utf8');
314+
let map;
315+
if (actionOptions.sourceMaps) {
316+
try {
317+
map = fs.readFileSync(filename + '.map', 'utf8');
318+
if (es5Polyfills) {
319+
fs.unlinkSync(filename + '.map');
320+
}
321+
} catch {}
322+
}
323+
324+
if (es5Polyfills) {
325+
fs.unlinkSync(filename);
326+
filename = filename.replace('-es2015', '');
327+
}
328+
329+
// ES2015 polyfills are only optimized; optimization check was performed above
330+
if (file.file.startsWith('polyfills-es2015')) {
331+
actions.push({
332+
...actionOptions,
333+
filename,
334+
code,
335+
map,
336+
optimizeOnly: true,
337+
});
338+
339+
continue;
340+
}
341+
342+
// Record the bundle processing action
343+
// The runtime chunk gets special processing for lazy loaded files
344+
actions.push({
345+
...actionOptions,
346+
filename,
347+
code,
348+
map,
349+
runtime: file.file.startsWith('runtime'),
350+
});
351+
352+
// Add the newly created ES5 bundles to the index as nomodule scripts
353+
const newFilename = es5Polyfills
354+
? file.file.replace('-es2015', '')
355+
: file.file.replace('es2015', 'es5');
356+
noModuleFiles.push({ ...file, file: newFilename });
357+
}
358+
359+
// Execute the bundle processing actions
360+
await new Promise<void>((resolve, reject) => {
361+
const workerFile = require.resolve('../utils/process-bundle');
362+
const workers = workerFarm(
363+
{
364+
maxRetries: 1,
365+
},
366+
path.extname(workerFile) !== '.ts'
367+
? workerFile
368+
: require.resolve('../utils/process-bundle-bootstrap'),
369+
['process'],
370+
);
371+
let completed = 0;
372+
const workCallback = (error: Error | null) => {
373+
if (error) {
374+
workerFarm.end(workers);
375+
reject(error);
376+
} else if (++completed === actions.length) {
377+
workerFarm.end(workers);
378+
resolve();
379+
}
380+
};
381+
382+
actions.forEach(action => workers['process'](action, workCallback));
383+
});
252384
} else {
253385
const { emittedFiles = [] } = firstBuild;
254386
files = emittedFiles.filter(x => x.name !== 'polyfills-es5');
255387
noModuleFiles = emittedFiles.filter(x => x.name === 'polyfills-es5');
256388
}
257389

258-
return writeIndexHtml({
259-
host,
260-
outputPath: resolve(root, join(normalize(options.outputPath), getIndexOutputFile(options))),
261-
indexPath: join(root, getIndexInputFile(options)),
262-
files,
263-
noModuleFiles,
264-
moduleFiles,
265-
baseHref: options.baseHref,
266-
deployUrl: options.deployUrl,
267-
sri: options.subresourceIntegrity,
268-
scripts: options.scripts,
269-
styles: options.styles,
270-
postTransform: transforms.indexHtml,
271-
crossOrigin: options.crossOrigin,
272-
}).pipe(
273-
map(() => ({ success: true })),
274-
catchError(error => of({ success: false, error: mapErrorToMessage(error) })),
275-
);
390+
if (options.index) {
391+
return writeIndexHtml({
392+
host,
393+
outputPath: resolve(
394+
root,
395+
join(normalize(options.outputPath), getIndexOutputFile(options)),
396+
),
397+
indexPath: join(root, getIndexInputFile(options)),
398+
files,
399+
noModuleFiles,
400+
moduleFiles,
401+
baseHref: options.baseHref,
402+
deployUrl: options.deployUrl,
403+
sri: options.subresourceIntegrity,
404+
scripts: options.scripts,
405+
styles: options.styles,
406+
postTransform: transforms.indexHtml,
407+
crossOrigin: options.crossOrigin,
408+
})
409+
.pipe(
410+
map(() => ({ success: true })),
411+
catchError(error => of({ success: false, error: mapErrorToMessage(error) })),
412+
)
413+
.toPromise();
414+
} else {
415+
return { success };
416+
}
276417
} else {
277-
return of({ success });
418+
return { success };
278419
}
279420
}),
280421
concatMap(buildEvent => {

0 commit comments

Comments
 (0)