/
import_factory.ts
226 lines (196 loc) · 7.3 KB
/
import_factory.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
* @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 { dirname, relative } from 'path';
import * as ts from 'typescript';
import { forwardSlashPath } from '../utils';
/**
* Given this original source code:
*
* import { NgModule } from '@angular/core';
* import { Routes, RouterModule } from '@angular/router';
*
* const routes: Routes = [{
* path: 'lazy',
* loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule).
* }];
*
* @NgModule({
* imports: [RouterModule.forRoot(routes)],
* exports: [RouterModule]
* })
* export class AppRoutingModule { }
*
* NGC (View Engine) will process it into:
*
* import { Routes } from '@angular/router';
* const ɵ0 = () => import('./lazy/lazy.module').then(m => m.LazyModule);
* const routes: Routes = [{
* path: 'lazy',
* loadChildren: ɵ0
* }];
* export class AppRoutingModule {
* }
* export { ɵ0 };
*
* The importFactory transformation will only see the AST after it is process by NGC.
* You can confirm this with the code below:
*
* const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, sourceFile, sourceFile);
* console.log(`### Original source: \n${sourceFile.text}\n###`);
* console.log(`### Current source: \n${currentText}\n###`);
*
* At this point it doesn't yet matter what the target (ES5/ES2015/etc) is, so the original
* constructs, like `class` and arrow functions, still remain.
*
*/
export function importFactory(
warningCb: (warning: string) => void,
getTypeChecker: () => ts.TypeChecker,
): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
// TODO(filipesilva): change the link to https://angular.io/guide/ivy once it is out.
return (sourceFile: ts.SourceFile) => {
const warning = `
Found 'loadChildren' with a non-string syntax in ${sourceFile.fileName} but could not transform it.
Make sure it matches the format below:
loadChildren: () => import('IMPORT_STRING').then(M => M.EXPORT_NAME)
Please note that only IMPORT_STRING, M, and EXPORT_NAME can be replaced in this format.
Visit https://next.angular.io/guide/ivy for more information on using Ivy.
`;
const emitWarning = () => warningCb(warning);
const visitVariableStatement: ts.Visitor = (node: ts.Node) => {
if (ts.isVariableDeclaration(node)) {
return replaceImport(node, context, emitWarning, sourceFile.fileName, getTypeChecker());
}
return ts.visitEachChild(node, visitVariableStatement, context);
};
const visitToplevelNodes: ts.Visitor = (node: ts.Node) => {
// We only care about finding variable declarations, which are found in this structure:
// VariableStatement -> VariableDeclarationList -> VariableDeclaration
if (ts.isVariableStatement(node)) {
return ts.visitEachChild(node, visitVariableStatement, context);
}
// There's no point in recursing into anything but variable statements, so return the node.
return node;
};
return ts.visitEachChild(sourceFile, visitToplevelNodes, context);
};
};
}
function replaceImport(
node: ts.VariableDeclaration,
context: ts.TransformationContext,
emitWarning: () => void,
fileName: string,
typeChecker: ts.TypeChecker,
): ts.Node {
// This ONLY matches the original source code format below:
// loadChildren: () => import('IMPORT_STRING').then(M => M.EXPORT_NAME)
// And expects that source code to be transformed by NGC (see comment for importFactory).
// It will not match nor alter variations, for instance:
// - not using arrow functions
// - using `await` instead of `then`
// - using a default export (https://github.com/angular/angular/issues/11402)
// The only parts that can change are the ones in caps: IMPORT_STRING, M and EXPORT_NAME.
// Exit early if the structure is not what we expect.
// ɵ0 = something
const name = node.name;
if (!(
ts.isIdentifier(name)
&& /ɵ\d+/.test(name.text)
)) {
return node;
}
const initializer = node.initializer;
if (initializer === undefined) {
return node;
}
// ɵ0 = () => something
if (!(
ts.isArrowFunction(initializer)
&& initializer.parameters.length === 0
)) {
return node;
}
// ɵ0 = () => something.then(something)
const topArrowFnBody = initializer.body;
if (!ts.isCallExpression(topArrowFnBody)) {
return node;
}
const topArrowFnBodyExpr = topArrowFnBody.expression;
if (!(
ts.isPropertyAccessExpression(topArrowFnBodyExpr)
&& ts.isIdentifier(topArrowFnBodyExpr.name)
)) {
return node;
}
if (topArrowFnBodyExpr.name.text != 'then') {
return node;
}
// ɵ0 = () => import('IMPORT_STRING').then(something)
const importCall = topArrowFnBodyExpr.expression;
if (!(
ts.isCallExpression(importCall)
&& importCall.expression.kind === ts.SyntaxKind.ImportKeyword
&& importCall.arguments.length === 1
&& ts.isStringLiteral(importCall.arguments[0])
)) {
return node;
}
// Now that we know it's both `ɵ0` (generated by NGC) and a `import()`, start emitting a warning
// if the structure isn't as expected to help users identify unusable syntax.
const warnAndBail = () => {
emitWarning();
return node;
};
// ɵ0 = () => import('IMPORT_STRING').then(m => m.EXPORT_NAME)
if (!(
topArrowFnBody.arguments.length === 1
&& ts.isArrowFunction(topArrowFnBody.arguments[0])
)) {
return warnAndBail();
}
const thenArrowFn = topArrowFnBody.arguments[0] as ts.ArrowFunction;
if (!(
thenArrowFn.parameters.length === 1
&& ts.isPropertyAccessExpression(thenArrowFn.body)
&& ts.isIdentifier(thenArrowFn.body.name)
)) {
return warnAndBail();
}
// At this point we know what are the nodes we need to replace.
const exportNameId = thenArrowFn.body.name;
const importStringLit = importCall.arguments[0] as ts.StringLiteral;
// Try to resolve the import. It might be a reexport from somewhere and the ngfactory will only
// be present next to the original module.
const exportedSymbol = typeChecker.getSymbolAtLocation(exportNameId);
if (!exportedSymbol) {
return warnAndBail();
}
const exportedSymbolDecl = exportedSymbol.getDeclarations();
if (!exportedSymbolDecl || exportedSymbolDecl.length === 0) {
return warnAndBail();
}
// Get the relative path from the containing module to the imported module.
const relativePath = relative(dirname(fileName), exportedSymbolDecl[0].getSourceFile().fileName);
// node's `relative` call doesn't actually add `./` so we add it here.
// Also replace the 'ts' extension with just 'ngfactory'.
const newImportString = `./${forwardSlashPath(relativePath)}`.replace(/ts$/, 'ngfactory');
// The easiest way to alter them is with a simple visitor.
const replacementVisitor: ts.Visitor = (node: ts.Node) => {
if (node === importStringLit) {
// Transform the import string.
return ts.createStringLiteral(newImportString);
} else if (node === exportNameId) {
// Transform the export name.
return ts.createIdentifier(exportNameId.text + 'NgFactory');
}
return ts.visitEachChild(node, replacementVisitor, context);
};
return ts.visitEachChild(node, replacementVisitor, context);
}