-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(@angular-devkit/build-angular): remove async files from
initial
…
… bundle budget. Refs #15792. Static files listed in `angular.json` were being accounted in the `initial` bundle budget even when they were deferred asynchronously with `"lazy": true` or `"inject": false`. Webpack belives these files to be `initial`, so this commit corrects that by finding all extra entry points and excluding ones which are explicitly marked by the application developer as asynchronous. One edge case would be that the main bundle might transitively depend on one of these static files, and thus pull it into the `initial` bundle. However, this is not possible because the files are not present until the end of the build and cannot be depended upon by a Webpack build step. Thus all files listed by the application developer can be safely assumed to truly be loaded asynchronously.
- Loading branch information
Showing
4 changed files
with
194 additions
and
3 deletions.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
packages/angular_devkit/build_angular/src/angular-cli-files/utilities/async-chunks.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/** | ||
* @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 * as webpack from 'webpack'; | ||
import { NormalizedEntryPoint } from '../models/webpack-configs'; | ||
|
||
/** | ||
* Webpack stats may incorrectly mark extra entry points `initial` chunks, when | ||
* they are actually loaded asynchronously and thus not in the main bundle. This | ||
* function finds extra entry points in Webpack stats and corrects this value | ||
* whereever necessary. Does not modify {@param webpackStats}. | ||
*/ | ||
export function markAsyncChunksNonInitial( | ||
webpackStats: webpack.Stats.ToJsonOutput, | ||
extraEntryPoints: NormalizedEntryPoint[], | ||
): Exclude<webpack.Stats.ToJsonOutput['chunks'], undefined> { | ||
const {chunks = [], entrypoints: entryPoints = {}} = webpackStats; | ||
|
||
// Find all Webpack chunk IDs not injected into the main bundle. We don't have | ||
// to worry about transitive dependencies because extra entry points cannot be | ||
// depended upon in Webpack, thus any extra entry point with `inject: false`, | ||
// **cannot** be loaded in main bundle. | ||
const asyncEntryPoints = extraEntryPoints.filter((entryPoint) => !entryPoint.inject); | ||
const asyncChunkIds = flatMap(asyncEntryPoints, | ||
(entryPoint) => entryPoints[entryPoint.bundleName].chunks); | ||
|
||
// Find chunks for each ID. | ||
const asyncChunks = asyncChunkIds.map((chunkId) => { | ||
const chunk = chunks.find((chunk) => chunk.id === chunkId); | ||
if (!chunk) { | ||
throw new Error(`Failed to find chunk (${chunkId}) in set:\n${ | ||
JSON.stringify(chunks)}`); | ||
} | ||
|
||
return chunk; | ||
}) | ||
// All Webpack chunks are dependent on `runtime`, which is never an async | ||
// entry point, simply ignore this one. | ||
.filter((chunk) => chunk.names.indexOf('runtime') === -1); | ||
|
||
// A chunk is considered `initial` only if Webpack already belives it to be initial | ||
// and the application developer did not mark it async via an extra entry point. | ||
return chunks.map((chunk) => ({ | ||
...chunk, | ||
initial: chunk.initial && !asyncChunks.find((asyncChunk) => asyncChunk === chunk), | ||
})); | ||
} | ||
|
||
function flatMap<T, R>(list: T[], mapper: (item: T, index: number, array: T[]) => R[]): R[] { | ||
return ([] as R[]).concat(...list.map(mapper)); | ||
} |
127 changes: 127 additions & 0 deletions
127
packages/angular_devkit/build_angular/src/angular-cli-files/utilities/async-chunks_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/** | ||
* @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 * as webpack from 'webpack'; | ||
import { markAsyncChunksNonInitial } from './async-chunks'; | ||
|
||
describe('async-chunks', () => { | ||
describe('markAsyncChunksNonInitial()', () => { | ||
it('sets `initial: false` for all extra entry points loaded asynchronously', () => { | ||
const chunks = [ | ||
{ | ||
id: 0, | ||
names: ['first'], | ||
initial: true, | ||
}, | ||
{ | ||
id: 1, | ||
names: ['second'], | ||
initial: true, | ||
}, | ||
{ | ||
id: 'third', // IDs can be strings too. | ||
names: ['third'], | ||
initial: true, | ||
}, | ||
]; | ||
const entrypoints = { | ||
first: { | ||
chunks: [0], | ||
}, | ||
second: { | ||
chunks: [1], | ||
}, | ||
third: { | ||
chunks: ['third'], | ||
}, | ||
}; | ||
const webpackStats = { chunks, entrypoints } as unknown as webpack.Stats.ToJsonOutput; | ||
|
||
const extraEntryPoints = [ | ||
{ | ||
bundleName: 'first', | ||
inject: false, // Loaded asynchronously. | ||
input: 'first.css', | ||
}, | ||
{ | ||
bundleName: 'second', | ||
inject: true, | ||
input: 'second.js', | ||
}, | ||
{ | ||
bundleName: 'third', | ||
inject: false, // Loaded asynchronously. | ||
input: 'third.js', | ||
}, | ||
]; | ||
|
||
const newChunks = markAsyncChunksNonInitial(webpackStats, extraEntryPoints); | ||
|
||
expect(newChunks).toEqual([ | ||
{ | ||
id: 0, | ||
names: ['first'], | ||
initial: false, // No longer initial because it was marked async. | ||
}, | ||
{ | ||
id: 1, | ||
names: ['second'], | ||
initial: true, | ||
}, | ||
{ | ||
id: 'third', | ||
names: ['third'], | ||
initial: false, // No longer initial because it was marked async. | ||
}, | ||
] as Exclude<webpack.Stats.ToJsonOutput['chunks'], undefined>); | ||
}); | ||
|
||
it('ignores runtime dependency of async chunks', () => { | ||
const chunks = [ | ||
{ | ||
id: 0, | ||
names: ['asyncStuff'], | ||
initial: true, | ||
}, | ||
{ | ||
id: 1, | ||
names: ['runtime'], | ||
initial: true, | ||
}, | ||
]; | ||
const entrypoints = { | ||
asyncStuff: { | ||
chunks: [0, 1], // Includes runtime as a dependency. | ||
}, | ||
}; | ||
const webpackStats = { chunks, entrypoints } as unknown as webpack.Stats.ToJsonOutput; | ||
|
||
const extraEntryPoints = [ | ||
{ | ||
bundleName: 'asyncStuff', | ||
inject: false, // Loaded asynchronously. | ||
input: 'asyncStuff.js', | ||
}, | ||
]; | ||
|
||
const newChunks = markAsyncChunksNonInitial(webpackStats, extraEntryPoints); | ||
|
||
expect(newChunks).toEqual([ | ||
{ | ||
id: 0, | ||
names: ['asyncStuff'], | ||
initial: false, // No longer initial because it was marked async. | ||
}, | ||
{ | ||
id: 1, | ||
names: ['runtime'], | ||
initial: true, // Still initial, even though its a dependency. | ||
}, | ||
] as Exclude<webpack.Stats.ToJsonOutput['chunks'], undefined>); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters