Skip to content

Commit b7c3570

Browse files
committed
fix(@angular-devkit/build-angular): used named chunks for dynamic imports
Fix angular#14251
1 parent 8089a3f commit b7c3570

File tree

8 files changed

+127
-60
lines changed

8 files changed

+127
-60
lines changed

packages/angular_devkit/architect/testing/test-logger.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { logging } from '@angular-devkit/core';
1313
* @deprecated
1414
*/
1515
export class TestLogger extends logging.Logger {
16-
private _latestEntries: logging.LogEntry[] = [];
16+
_latestEntries: logging.LogEntry[] = [];
1717
constructor(name: string, parent: logging.Logger | null = null) {
1818
super(name, parent);
1919
this.subscribe((entry) => this._latestEntries.push(entry));

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

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { AssetPatternClass } from '../../../browser/schema';
2020
import { isEs5SupportNeeded } from '../../../utils/differential-loading';
2121
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
2222
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
23+
import { NamedLazyChunksPlugin } from '../../plugins/named-chunks-plugin';
2324
import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin';
2425
import { findAllNodeModules, findUp } from '../../utilities/find-up';
2526
import { requireProjectModule } from '../../utilities/require-project-module';
@@ -202,6 +203,10 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
202203
extraPlugins.push(new StatsPlugin(`stats${targetInFileName}.json`, 'verbose'));
203204
}
204205

206+
if (buildOptions.namedChunks) {
207+
extraPlugins.push(new NamedLazyChunksPlugin());
208+
}
209+
205210
let sourceMapUseRule;
206211
if ((scriptsSourceMap || stylesSourceMap) && vendorSourceMap) {
207212
sourceMapUseRule = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { Compiler } from 'webpack';
9+
// Webpack doesn't export these so the deep imports can potentially break.
10+
// There doesn't seem to exist any ergonomic way to alter chunk names for non-context lazy chunks
11+
// (https://github.com/webpack/webpack/issues/9075) so this is the best alternative for now.
12+
const ImportDependency = require('webpack/lib/dependencies/ImportDependency');
13+
const ImportDependenciesBlock = require('webpack/lib/dependencies/ImportDependenciesBlock');
14+
const Template = require('webpack/lib/Template');
15+
16+
export class NamedLazyChunksPlugin {
17+
constructor() { }
18+
apply(compiler: Compiler): void {
19+
compiler.hooks.compilation.tap('named-lazy-chunks-plugin', compilation => {
20+
// The dependencyReference hook isn't in the webpack typings so we have to type it as any.
21+
// tslint:disable-next-line: no-any
22+
(compilation.hooks as any).dependencyReference.tap('named-lazy-chunks-plugin',
23+
// tslint:disable-next-line: no-any
24+
(_: any, dependency: any) => {
25+
if (
26+
// Check this dependency is from an `import()` statement.
27+
dependency instanceof ImportDependency
28+
&& dependency.block instanceof ImportDependenciesBlock
29+
// Don't rename chunks that already have a name.
30+
&& dependency.block.chunkName === null
31+
) {
32+
// Convert the request to a valid chunk name using the same logic used
33+
// in webpack/lib/ContextModule.js
34+
dependency.block.chunkName = Template.toPath(dependency.request);
35+
}
36+
});
37+
});
38+
}
39+
}

packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget';
1212
export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin';
1313
export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin';
1414
export { RemoveHashPlugin, RemoveHashPluginOptions } from './remove-hash-plugin';
15+
export { NamedLazyChunksPlugin as NamedChunksPlugin } from './named-chunks-plugin';
1516
export {
1617
default as PostcssCliResources,
1718
PostcssCliResourcesOptions,

packages/angular_devkit/build_angular/test/browser/lazy-module_spec_large.ts

+67-52
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import { Architect } from '@angular-devkit/architect';
1010
import { TestLogger } from '@angular-devkit/architect/testing';
1111
import { take, tap, timeout } from 'rxjs/operators';
12-
import { browserBuild, createArchitect, host, lazyModuleFiles, lazyModuleImport } from '../utils';
12+
import {
13+
browserBuild, createArchitect, host, lazyModuleFiles,
14+
lazyModuleFnImport, lazyModuleStringImport,
15+
} from '../utils';
1316

1417
// tslint:disable-next-line:no-big-function
1518
describe('Browser Builder lazy modules', () => {
@@ -22,46 +25,60 @@ describe('Browser Builder lazy modules', () => {
2225
});
2326
afterEach(async () => host.restore().toPromise());
2427

25-
it('supports lazy bundle for lazy routes with JIT', async () => {
26-
host.writeMultipleFiles(lazyModuleFiles);
27-
host.writeMultipleFiles(lazyModuleImport);
28-
29-
const { files } = await browserBuild(architect, host, target);
30-
expect('lazy-lazy-module.js' in files).toBe(true);
31-
});
32-
33-
it('should show error when lazy route is invalid on watch mode AOT', async () => {
34-
host.writeMultipleFiles(lazyModuleFiles);
35-
host.writeMultipleFiles(lazyModuleImport);
36-
host.replaceInFile(
37-
'src/app/app.module.ts',
38-
'lazy.module#LazyModule',
39-
'invalid.module#LazyModule',
40-
);
41-
42-
const logger = new TestLogger('rebuild-lazy-errors');
43-
const overrides = { watch: true, aot: true };
44-
const run = await architect.scheduleTarget(target, overrides, { logger });
45-
await run.output.pipe(
46-
timeout(15000),
47-
tap((buildEvent) => expect(buildEvent.success).toBe(false)),
48-
tap(() => {
49-
expect(logger.includes('Could not resolve module')).toBe(true);
50-
logger.clear();
51-
host.appendToFile('src/main.ts', ' ');
52-
}),
53-
take(2),
54-
).toPromise();
55-
await run.stop();
56-
});
57-
58-
it('supports lazy bundle for lazy routes with AOT', async () => {
59-
host.writeMultipleFiles(lazyModuleFiles);
60-
host.writeMultipleFiles(lazyModuleImport);
61-
62-
const { files } = await browserBuild(architect, host, target, { aot: true });
63-
expect(files['lazy-lazy-module-ngfactory.js']).not.toBeUndefined();
64-
});
28+
for (const [name, imports] of Object.entries({
29+
'string': lazyModuleStringImport,
30+
'function': lazyModuleFnImport,
31+
})) {
32+
describe(`Load children ${name} syntax`, () => {
33+
it('supports lazy bundle for lazy routes with JIT', async () => {
34+
host.writeMultipleFiles(lazyModuleFiles);
35+
host.writeMultipleFiles(imports);
36+
37+
const { files } = await browserBuild(architect, host, target);
38+
expect('lazy-lazy-module.js' in files).toBe(true);
39+
});
40+
41+
it('should show error when lazy route is invalid on watch mode AOT', async () => {
42+
host.writeMultipleFiles(lazyModuleFiles);
43+
host.writeMultipleFiles(imports);
44+
host.replaceInFile(
45+
'src/app/app.module.ts',
46+
'lazy.module',
47+
'invalid.module',
48+
);
49+
50+
const logger = new TestLogger('rebuild-lazy-errors');
51+
const overrides = { watch: true, aot: true };
52+
const run = await architect.scheduleTarget(target, overrides, { logger });
53+
await run.output.pipe(
54+
timeout(15000),
55+
tap((buildEvent) => expect(buildEvent.success).toBe(false)),
56+
tap(() => {
57+
// Webpack error when using loadchildren string syntax.
58+
const hasMissingModuleError = logger.includes('Could not resolve module')
59+
// TS type error when using import().
60+
|| logger.includes('Cannot find module')
61+
// Webpack error when using import() on a rebuild.
62+
// There is no TS error because the type checker is forked on rebuilds.
63+
|| logger.includes('Module not found');
64+
expect(hasMissingModuleError).toBe(true, 'Should show missing module error');
65+
logger.clear();
66+
host.appendToFile('src/main.ts', ' ');
67+
}),
68+
take(2),
69+
).toPromise();
70+
await run.stop();
71+
});
72+
73+
it('supports lazy bundle for lazy routes with AOT', async () => {
74+
host.writeMultipleFiles(lazyModuleFiles);
75+
host.writeMultipleFiles(imports);
76+
77+
const { files } = await browserBuild(architect, host, target, { aot: true });
78+
expect(files['lazy-lazy-module-ngfactory.js']).not.toBeUndefined();
79+
});
80+
});
81+
}
6582

6683
it(`supports lazy bundle for import() calls`, async () => {
6784
host.writeMultipleFiles({
@@ -72,7 +89,7 @@ describe('Browser Builder lazy modules', () => {
7289
host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`);
7390

7491
const { files } = await browserBuild(architect, host, target);
75-
expect(files['0.js']).not.toBeUndefined();
92+
expect(files['lazy-module.js']).not.toBeUndefined();
7693
});
7794

7895
it(`supports lazy bundle for dynamic import() calls`, async () => {
@@ -96,7 +113,7 @@ describe('Browser Builder lazy modules', () => {
96113
});
97114

98115
const { files } = await browserBuild(architect, host, target);
99-
expect(files['0.js']).not.toBeUndefined();
116+
expect(files['lazy-module.js']).not.toBeUndefined();
100117
});
101118

102119
it(`supports hiding lazy bundle module name`, async () => {
@@ -116,13 +133,12 @@ describe('Browser Builder lazy modules', () => {
116133
'src/two.ts': `import * as http from '@angular/common/http'; console.log(http);`,
117134
'src/main.ts': `import('./one'); import('./two');`,
118135
});
119-
host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`);
120136

121-
const { files } = await browserBuild(architect, host, target, { namedChunks: false });
122-
expect(files['0.js']).not.toBeUndefined();
123-
expect(files['1.js']).not.toBeUndefined();
137+
const { files } = await browserBuild(architect, host, target);
138+
expect(files['one.js']).not.toBeUndefined();
139+
expect(files['two.js']).not.toBeUndefined();
124140
// TODO: the chunk with common modules used to be called `common`, see why that changed.
125-
expect(files['2.js']).not.toBeUndefined();
141+
expect(files['default~one~two.js']).not.toBeUndefined();
126142
});
127143

128144
it(`supports disabling the common bundle`, async () => {
@@ -131,12 +147,11 @@ describe('Browser Builder lazy modules', () => {
131147
'src/two.ts': `import * as http from '@angular/common/http'; console.log(http);`,
132148
'src/main.ts': `import('./one'); import('./two');`,
133149
});
134-
host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`);
135150

136151
const { files } = await browserBuild(architect, host, target, { commonChunk: false });
137-
expect(files['0.js']).not.toBeUndefined();
138-
expect(files['1.js']).not.toBeUndefined();
139-
expect(files['2.js']).toBeUndefined();
152+
expect(files['one.js']).not.toBeUndefined();
153+
expect(files['two.js']).not.toBeUndefined();
154+
expect(files['common.js']).toBeUndefined();
140155
});
141156

142157
it(`supports extra lazy modules array in JIT`, async () => {

packages/angular_devkit/build_angular/test/browser/output-hashing_spec_large.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { Architect } from '@angular-devkit/architect';
1010
import { normalize } from '@angular-devkit/core';
11-
import { browserBuild, createArchitect, host, lazyModuleFiles, lazyModuleImport } from '../utils';
11+
import { browserBuild, createArchitect, host, lazyModuleFiles, lazyModuleStringImport } from '../utils';
1212

1313
describe('Browser Builder output hashing', () => {
1414
const target = { project: 'app', target: 'build' };
@@ -58,7 +58,7 @@ describe('Browser Builder output hashing', () => {
5858
let newHashes: Map<string, string>;
5959

6060
host.writeMultipleFiles(lazyModuleFiles);
61-
host.writeMultipleFiles(lazyModuleImport);
61+
host.writeMultipleFiles(lazyModuleStringImport);
6262

6363
const overrides = { outputHashing: 'all', extractCss: true };
6464

@@ -69,7 +69,7 @@ describe('Browser Builder output hashing', () => {
6969
// Save the current hashes.
7070
oldHashes = generateFileHashMap();
7171
host.writeMultipleFiles(lazyModuleFiles);
72-
host.writeMultipleFiles(lazyModuleImport);
72+
host.writeMultipleFiles(lazyModuleStringImport);
7373

7474
await browserBuild(architect, host, target, overrides);
7575
newHashes = generateFileHashMap();
@@ -107,7 +107,7 @@ describe('Browser Builder output hashing', () => {
107107
it('supports options', async () => {
108108
host.writeMultipleFiles({ 'src/styles.css': `h1 { background: url('./spectrum.png')}` });
109109
host.writeMultipleFiles(lazyModuleFiles);
110-
host.writeMultipleFiles(lazyModuleImport);
110+
host.writeMultipleFiles(lazyModuleStringImport);
111111

112112
// We must do several builds instead of a single one in watch mode, so that the output
113113
// path is deleted on each run and only contains the most recent files.

packages/angular_devkit/build_angular/test/browser/rebuild_spec_large.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Architect } from '@angular-devkit/architect';
1111
import { TestLogger } from '@angular-devkit/architect/testing';
1212
import { normalize, virtualFs } from '@angular-devkit/core';
1313
import { debounceTime, take, takeWhile, tap } from 'rxjs/operators';
14-
import { createArchitect, host, lazyModuleFiles, lazyModuleImport } from '../utils';
14+
import { createArchitect, host, lazyModuleFiles, lazyModuleStringImport } from '../utils';
1515

1616

1717
describe('Browser Builder rebuilds', () => {
@@ -82,7 +82,7 @@ describe('Browser Builder rebuilds', () => {
8282
// No lazy chunk should exist.
8383
if (!hasLazyChunk) {
8484
phase = 2;
85-
host.writeMultipleFiles({ ...lazyModuleFiles, ...lazyModuleImport });
85+
host.writeMultipleFiles({ ...lazyModuleFiles, ...lazyModuleStringImport });
8686
}
8787
break;
8888

packages/angular_devkit/build_angular/test/utils.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export const lazyModuleFiles: { [path: string]: string } = {
140140
`,
141141
};
142142

143-
export const lazyModuleImport: { [path: string]: string } = {
143+
export const lazyModuleStringImport: { [path: string]: string } = {
144144
'src/app/app.module.ts': `
145145
import { BrowserModule } from '@angular/platform-browser';
146146
import { NgModule } from '@angular/core';
@@ -164,3 +164,10 @@ export const lazyModuleImport: { [path: string]: string } = {
164164
export class AppModule { }
165165
`,
166166
};
167+
168+
export const lazyModuleFnImport: { [path: string]: string } = {
169+
'src/app/app.module.ts': lazyModuleStringImport['src/app/app.module.ts'].replace(
170+
`loadChildren: './lazy/lazy.module#LazyModule'`,
171+
`loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)`,
172+
),
173+
};

0 commit comments

Comments
 (0)