Skip to content

Commit 08343ce

Browse files
committed
feat(@ngtools/webpack): support import syntax for loadChildren with ViewEngine
This feature ONLY matches the format below: ``` loadChildren: () => import('IMPORT_STRING').then(m => m.EXPORT_NAME) ``` It will not match nor alter variations, for instance: - not using arrow functions - not using `m` as the module argument - using `await` instead of `then` - using a default export (angular/angular#11402) The only parts that can change are the ones in caps: IMPORT_STRING and EXPORT_NAME.
1 parent e1ab7a8 commit 08343ce

File tree

5 files changed

+203
-3
lines changed

5 files changed

+203
-3
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { tags } from '@angular-devkit/core';
99
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
1010
import * as path from 'path';
11-
import { HashedModuleIdsPlugin, debug } from 'webpack';
11+
import { ContextReplacementPlugin, HashedModuleIdsPlugin, debug } from 'webpack';
1212
import { AssetPatternClass } from '../../../browser/schema';
1313
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
1414
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
@@ -343,6 +343,12 @@ export function getCommonConfig(wco: WebpackConfigOptions) {
343343
...extraMinimizers,
344344
],
345345
},
346-
plugins: extraPlugins,
346+
plugins: [
347+
// Always replace the context for the System.import in angular/core to prevent warnings.
348+
// https://github.com/angular/angular/issues/11580
349+
// With VE the correct context is added in @ngtools/webpack, but Ivy doesn't need it at all.
350+
new ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)/),
351+
...extraPlugins,
352+
],
347353
};
348354
}

packages/ngtools/webpack/src/angular_compiler_plugin.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
exportLazyModuleMap,
4747
exportNgFactory,
4848
findResources,
49+
importFactory,
4950
registerLocaleData,
5051
removeDecorators,
5152
replaceBootstrap,
@@ -294,7 +295,12 @@ export class AngularCompilerPlugin {
294295
this._platformTransformers = options.platformTransformers;
295296
}
296297

297-
if (options.discoverLazyRoutes !== undefined) {
298+
// Determine if lazy route discovery via Compiler CLI private API should be attempted.
299+
if (this._compilerOptions.enableIvy) {
300+
// Never try to discover lazy routes with Ivy.
301+
this._discoverLazyRoutes = false;
302+
} else if (options.discoverLazyRoutes !== undefined) {
303+
// The default is to discover routes, but it can be overriden.
298304
this._discoverLazyRoutes = options.discoverLazyRoutes;
299305
}
300306

@@ -876,6 +882,10 @@ export class AngularCompilerPlugin {
876882
} else {
877883
// Remove unneeded angular decorators.
878884
this._transformers.push(removeDecorators(isAppPath, getTypeChecker));
885+
// Import ngfactory in loadChildren import syntax
886+
if (!this._compilerOptions.enableIvy) {
887+
this._transformers.push(importFactory());
888+
}
879889
}
880890

881891
if (this._platformTransformers !== null) {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 * as ts from 'typescript';
9+
10+
export function importFactory(): ts.TransformerFactory<ts.SourceFile> {
11+
return (context: ts.TransformationContext) => {
12+
const visitNode: ts.Visitor = (node: ts.Node) => {
13+
// Only try to transform 'loadChildren' property assignments.
14+
if (ts.isPropertyAssignment(node)) {
15+
return replaceImport(node, context);
16+
}
17+
18+
return ts.visitEachChild(node, visitNode, context);
19+
};
20+
21+
// Only transform files that contain `loadChildren`.
22+
return (sourceFile: ts.SourceFile) => (
23+
/\bloadChildren\b/.test(sourceFile.text)
24+
? ts.visitNode(sourceFile, visitNode)
25+
: sourceFile
26+
);
27+
};
28+
}
29+
30+
function replaceImport(node: ts.PropertyAssignment, context: ts.TransformationContext): ts.Node {
31+
// This ONLY matches the format below:
32+
// loadChildren: () => import('IMPORT_STRING').then(m => m.EXPORT_NAME)
33+
// It will not match nor alter variations, for instance:
34+
// - not using arrow functions
35+
// - not using `m` as the module argument
36+
// - using `await` instead of `then`
37+
// - using a default export (https://github.com/angular/angular/issues/11402)
38+
// The only parts that can change are the ones in caps: IMPORT_STRING and EXPORT_NAME.
39+
40+
41+
// Exit early if the structure is not what we expect.
42+
43+
// loadChildren: something
44+
const name = node.name;
45+
if (!(
46+
ts.isIdentifier(name)
47+
&& name.text == 'loadChildren'
48+
)) {
49+
return node;
50+
}
51+
52+
// loadChildren: () => something
53+
const initializer = node.initializer;
54+
if (!(
55+
ts.isArrowFunction(initializer)
56+
&& initializer.parameters.length === 0
57+
)) {
58+
return node;
59+
}
60+
61+
// loadChildren: () => something.then(something)
62+
const topArrowFnBody = initializer.body;
63+
if (!ts.isCallExpression(topArrowFnBody)) { return node; }
64+
65+
const topArrowFnBodyExpr = topArrowFnBody.expression;
66+
if (!(
67+
ts.isPropertyAccessExpression(topArrowFnBodyExpr)
68+
&& ts.isIdentifier(topArrowFnBodyExpr.name)
69+
)) {
70+
return node;
71+
}
72+
if (topArrowFnBodyExpr.name.text != 'then') { return node; }
73+
74+
// loadChildren: () => import('IMPORT_STRING').then(something)
75+
const importCall = topArrowFnBodyExpr.expression;
76+
if (!(
77+
ts.isCallExpression(importCall)
78+
&& importCall.expression.kind === ts.SyntaxKind.ImportKeyword
79+
&& importCall.arguments.length === 1
80+
&& ts.isStringLiteral(importCall.arguments[0])
81+
)) {
82+
return node;
83+
}
84+
85+
// loadChildren: () => import('IMPORT_STRING').then(m => m.EXPORT_NAME)
86+
if (!(
87+
topArrowFnBody.arguments.length === 1
88+
&& ts.isArrowFunction(topArrowFnBody.arguments[0])
89+
)) {
90+
return node;
91+
}
92+
93+
const thenArrowFn = topArrowFnBody.arguments[0] as ts.ArrowFunction;
94+
if (!(
95+
thenArrowFn.parameters.length === 1
96+
&& ts.isPropertyAccessExpression(thenArrowFn.body)
97+
&& ts.isIdentifier(thenArrowFn.body.name)
98+
)) {
99+
return node;
100+
}
101+
102+
// At this point we know what are the nodes we need to replace.
103+
const importStringLit = importCall.arguments[0] as ts.StringLiteral;
104+
const exportNameId = thenArrowFn.body.name;
105+
106+
// The easiest way to alter them is with a simple visitor.
107+
const replacementVisitor: ts.Visitor = (node: ts.Node) => {
108+
if (node === importStringLit) {
109+
// Transform the import string.
110+
return ts.createStringLiteral(importStringLit.text + '.ngfactory');
111+
} else if (node === exportNameId) {
112+
// Transform the export name.
113+
return ts.createIdentifier(exportNameId.text + 'NgFactory');
114+
}
115+
116+
return ts.visitEachChild(node, replacementVisitor, context);
117+
};
118+
119+
return ts.visitEachChild(node, replacementVisitor, context);
120+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 { tags } from '@angular-devkit/core';
9+
import { transformTypescript } from './ast_helpers';
10+
import { importFactory } from './import_factory';
11+
12+
describe('@ngtools/webpack transformers', () => {
13+
describe('import-factory', () => {
14+
it('should support arrow functions', () => {
15+
const input = tags.stripIndent`
16+
const routes = [{
17+
path: "lazy",
18+
loadChildren: () => import("./lazy/lazy.module").then(m => m.LazyModule)
19+
}];
20+
`;
21+
const output = tags.stripIndent`
22+
const routes = [{
23+
path: "lazy",
24+
loadChildren: () =>
25+
import("./lazy/lazy.module.ngfactory").then(m => m.LazyModuleNgFactory)
26+
}];
27+
`;
28+
29+
const transformer = importFactory();
30+
const result = transformTypescript(input, [transformer]);
31+
32+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`);
33+
});
34+
35+
it('should not transform if the format is different than expected', () => {
36+
const input = tags.stripIndent`
37+
const routes = [{
38+
path: "lazy",
39+
loadChildren: function () { return import("./lazy/lazy.module").then(m => m.LazyModule); }
40+
}];
41+
`;
42+
43+
const transformer = importFactory();
44+
const result = transformTypescript(input, [transformer]);
45+
46+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`);
47+
});
48+
49+
it('should not transform if the property is not loadChildren', () => {
50+
const input = tags.stripIndent`
51+
const routes = [{
52+
path: "lazy",
53+
loadFoo: () => import("./lazy/lazy.module").then(m => m.LazyModule)
54+
}];
55+
`;
56+
57+
const transformer = importFactory();
58+
const result = transformTypescript(input, [transformer]);
59+
60+
expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`);
61+
});
62+
});
63+
});

packages/ngtools/webpack/src/transformers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export * from './register_locale_data';
1818
export * from './replace_resources';
1919
export * from './remove_decorators';
2020
export * from './find_resources';
21+
export * from './import_factory';

0 commit comments

Comments
 (0)