Skip to content

Commit 9b19009

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 eed4b0d commit 9b19009

File tree

5 files changed

+216
-3
lines changed

5 files changed

+216
-3
lines changed

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
import { tags } from '@angular-devkit/core';
99
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
1010
import * as path from 'path';
11-
import { Configuration, HashedModuleIdsPlugin, Output, debug } from 'webpack';
11+
import {
12+
Configuration,
13+
ContextReplacementPlugin,
14+
HashedModuleIdsPlugin,
15+
Output,
16+
debug,
17+
} from 'webpack';
1218
import { AssetPatternClass } from '../../../browser/schema';
1319
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
1420
import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin';
@@ -345,6 +351,12 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
345351
...extraMinimizers,
346352
],
347353
},
348-
plugins: extraPlugins,
354+
plugins: [
355+
// Always replace the context for the System.import in angular/core to prevent warnings.
356+
// https://github.com/angular/angular/issues/11580
357+
// With VE the correct context is added in @ngtools/webpack, but Ivy doesn't need it at all.
358+
new ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)/),
359+
...extraPlugins,
360+
],
349361
};
350362
}

packages/ngtools/webpack/src/angular_compiler_plugin.ts

Lines changed: 18 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,
@@ -104,6 +105,7 @@ export interface AngularCompilerPluginOptions {
104105
logger?: logging.Logger;
105106
directTemplateLoading?: boolean;
106107
discoverLazyRoutes?: boolean;
108+
importFactories?: boolean;
107109

108110
// added to the list of lazy routes
109111
additionalLazyModules?: { [module: string]: string };
@@ -136,6 +138,7 @@ export class AngularCompilerPlugin {
136138
private _moduleResolutionCache: ts.ModuleResolutionCache;
137139
private _resourceLoader?: WebpackResourceLoader;
138140
private _discoverLazyRoutes = true;
141+
private _importFactories = false;
139142
// Contains `moduleImportPath#exportName` => `fullModulePath`.
140143
private _lazyRoutes: LazyRouteMap = {};
141144
private _tsConfigPath: string;
@@ -294,10 +297,20 @@ export class AngularCompilerPlugin {
294297
this._platformTransformers = options.platformTransformers;
295298
}
296299

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

309+
if (!this._compilerOptions.enableIvy && options.importFactories === true) {
310+
// Only transform imports to use factories with View Engine.
311+
this._importFactories = true;
312+
}
313+
301314
// Default ContextElementDependency to the one we can import from here.
302315
// Failing to use the right ContextElementDependency will throw the error below:
303316
// "No module factory available for dependency type: ContextElementDependency"
@@ -876,6 +889,10 @@ export class AngularCompilerPlugin {
876889
} else {
877890
// Remove unneeded angular decorators.
878891
this._transformers.push(removeDecorators(isAppPath, getTypeChecker));
892+
// Import ngfactory in loadChildren import syntax
893+
if (this._importFactories) {
894+
this._transformers.push(importFactory());
895+
}
879896
}
880897

881898
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)