@angular/core 19.0.0-next.2 → 19.0.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/core.mjs +193 -20
- package/fesm2022/core.mjs.map +1 -1
- package/fesm2022/primitives/event-dispatch.mjs +1 -1
- package/fesm2022/primitives/signals.mjs +1 -1
- package/fesm2022/rxjs-interop.mjs +1 -1
- package/fesm2022/testing.mjs +4 -4
- package/index.d.ts +113 -4
- package/package.json +1 -1
- package/primitives/event-dispatch/index.d.ts +1 -1
- package/primitives/signals/index.d.ts +1 -1
- package/rxjs-interop/index.d.ts +1 -1
- package/schematics/bundles/compiler_host-bbb5d8fd.js +44719 -0
- package/schematics/bundles/control-flow-migration.js +1847 -0
- package/schematics/bundles/inject-migration.js +926 -0
- package/schematics/bundles/nodes-ddfa1613.js +151 -0
- package/schematics/bundles/project_tsconfig_paths-e9ccccbf.js +90 -0
- package/schematics/bundles/route-lazy-loading.js +411 -0
- package/schematics/bundles/standalone-migration.js +22339 -0
- package/schematics/collection.json +7 -14
- package/schematics/migrations.json +1 -17
- package/testing/index.d.ts +1 -1
- package/schematics/migrations/after-render-phase/bundle.js +0 -27333
- package/schematics/migrations/http-providers/bundle.js +0 -27582
- package/schematics/migrations/invalid-two-way-bindings/bundle.js +0 -23808
- package/schematics/ng-generate/control-flow-migration/bundle.js +0 -28076
- package/schematics/ng-generate/inject-migration/bundle.js +0 -27777
- package/schematics/ng-generate/route-lazy-loading/bundle.js +0 -27478
- package/schematics/ng-generate/standalone-migration/bundle.js +0 -52161
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* @license Angular v19.0.0-next.3
|
|
4
|
+
* (c) 2010-2024 Google LLC. https://angular.io/
|
|
5
|
+
* License: MIT
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
10
|
+
|
|
11
|
+
var schematics = require('@angular-devkit/schematics');
|
|
12
|
+
var p = require('path');
|
|
13
|
+
var compiler_host = require('./compiler_host-bbb5d8fd.js');
|
|
14
|
+
var ts = require('typescript');
|
|
15
|
+
var nodes = require('./nodes-ddfa1613.js');
|
|
16
|
+
require('os');
|
|
17
|
+
require('fs');
|
|
18
|
+
require('module');
|
|
19
|
+
require('url');
|
|
20
|
+
|
|
21
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
22
|
+
|
|
23
|
+
var ts__default = /*#__PURE__*/_interopDefaultLegacy(ts);
|
|
24
|
+
|
|
25
|
+
/*!
|
|
26
|
+
* @license
|
|
27
|
+
* Copyright Google LLC All Rights Reserved.
|
|
28
|
+
*
|
|
29
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
30
|
+
* found in the LICENSE file at https://angular.io/license
|
|
31
|
+
*/
|
|
32
|
+
/** Names of decorators that enable DI on a class declaration. */
|
|
33
|
+
const DECORATORS_SUPPORTING_DI = new Set([
|
|
34
|
+
'Component',
|
|
35
|
+
'Directive',
|
|
36
|
+
'Pipe',
|
|
37
|
+
'NgModule',
|
|
38
|
+
'Injectable',
|
|
39
|
+
]);
|
|
40
|
+
/** Names of symbols used for DI on parameters. */
|
|
41
|
+
const DI_PARAM_SYMBOLS = new Set([
|
|
42
|
+
'Inject',
|
|
43
|
+
'Attribute',
|
|
44
|
+
'Optional',
|
|
45
|
+
'SkipSelf',
|
|
46
|
+
'Self',
|
|
47
|
+
'Host',
|
|
48
|
+
'forwardRef',
|
|
49
|
+
]);
|
|
50
|
+
/**
|
|
51
|
+
* Finds the necessary information for the `inject` migration in a file.
|
|
52
|
+
* @param sourceFile File which to analyze.
|
|
53
|
+
* @param localTypeChecker Type checker scoped to the specific file.
|
|
54
|
+
*/
|
|
55
|
+
function analyzeFile(sourceFile, localTypeChecker) {
|
|
56
|
+
const coreSpecifiers = nodes.getNamedImports(sourceFile, '@angular/core');
|
|
57
|
+
// Exit early if there are no Angular imports.
|
|
58
|
+
if (coreSpecifiers === null || coreSpecifiers.elements.length === 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const classes = [];
|
|
62
|
+
const nonDecoratorReferences = {};
|
|
63
|
+
const importsToSpecifiers = coreSpecifiers.elements.reduce((map, specifier) => {
|
|
64
|
+
const symbolName = (specifier.propertyName || specifier.name).text;
|
|
65
|
+
if (DI_PARAM_SYMBOLS.has(symbolName)) {
|
|
66
|
+
map.set(symbolName, specifier);
|
|
67
|
+
}
|
|
68
|
+
return map;
|
|
69
|
+
}, new Map());
|
|
70
|
+
sourceFile.forEachChild(function walk(node) {
|
|
71
|
+
// Skip import declarations since they can throw off the identifier
|
|
72
|
+
// could below and we don't care about them in this migration.
|
|
73
|
+
if (ts__default["default"].isImportDeclaration(node)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Only visit the initializer of parameters, because we won't exclude
|
|
77
|
+
// their decorators from the identifier counting result below.
|
|
78
|
+
if (ts__default["default"].isParameter(node)) {
|
|
79
|
+
if (node.initializer) {
|
|
80
|
+
walk(node.initializer);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (ts__default["default"].isIdentifier(node) && importsToSpecifiers.size > 0) {
|
|
85
|
+
let symbol;
|
|
86
|
+
for (const [name, specifier] of importsToSpecifiers) {
|
|
87
|
+
const localName = (specifier.propertyName || specifier.name).text;
|
|
88
|
+
// Quick exit if the two symbols don't match up.
|
|
89
|
+
if (localName === node.text) {
|
|
90
|
+
if (!symbol) {
|
|
91
|
+
symbol = localTypeChecker.getSymbolAtLocation(node);
|
|
92
|
+
// If the symbol couldn't be resolved the first time, it won't be resolved the next
|
|
93
|
+
// time either. Stop the loop since we won't be able to get an accurate result.
|
|
94
|
+
if (!symbol || !symbol.declarations) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
else if (symbol.declarations.some((decl) => decl === specifier)) {
|
|
98
|
+
nonDecoratorReferences[name] = (nonDecoratorReferences[name] || 0) + 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (ts__default["default"].isClassDeclaration(node)) {
|
|
105
|
+
const decorators = nodes.getAngularDecorators(localTypeChecker, ts__default["default"].getDecorators(node) || []);
|
|
106
|
+
const supportsDI = decorators.some((dec) => DECORATORS_SUPPORTING_DI.has(dec.name));
|
|
107
|
+
const constructorNode = node.members.find((member) => ts__default["default"].isConstructorDeclaration(member) &&
|
|
108
|
+
member.body != null &&
|
|
109
|
+
member.parameters.length > 0);
|
|
110
|
+
if (supportsDI && constructorNode) {
|
|
111
|
+
classes.push({
|
|
112
|
+
node,
|
|
113
|
+
constructor: constructorNode,
|
|
114
|
+
superCall: node.heritageClauses ? findSuperCall(constructorNode) : null,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
node.forEachChild(walk);
|
|
119
|
+
});
|
|
120
|
+
return { classes, nonDecoratorReferences };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Returns the parameters of a function that aren't used within its body.
|
|
124
|
+
* @param declaration Function in which to search for unused parameters.
|
|
125
|
+
* @param localTypeChecker Type checker scoped to the file in which the function was declared.
|
|
126
|
+
* @param removedStatements Statements that were already removed from the constructor.
|
|
127
|
+
*/
|
|
128
|
+
function getConstructorUnusedParameters(declaration, localTypeChecker, removedStatements) {
|
|
129
|
+
const accessedTopLevelParameters = new Set();
|
|
130
|
+
const topLevelParameters = new Set();
|
|
131
|
+
const topLevelParameterNames = new Set();
|
|
132
|
+
const unusedParams = new Set();
|
|
133
|
+
// Prepare the parameters for quicker checks further down.
|
|
134
|
+
for (const param of declaration.parameters) {
|
|
135
|
+
if (ts__default["default"].isIdentifier(param.name)) {
|
|
136
|
+
topLevelParameters.add(param);
|
|
137
|
+
topLevelParameterNames.add(param.name.text);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!declaration.body) {
|
|
141
|
+
return topLevelParameters;
|
|
142
|
+
}
|
|
143
|
+
declaration.body.forEachChild(function walk(node) {
|
|
144
|
+
// Don't descend into statements that were removed already.
|
|
145
|
+
if (removedStatements && ts__default["default"].isStatement(node) && removedStatements.has(node)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!ts__default["default"].isIdentifier(node) || !topLevelParameterNames.has(node.text)) {
|
|
149
|
+
node.forEachChild(walk);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Don't consider `this.<name>` accesses as being references to
|
|
153
|
+
// parameters since they'll be moved to property declarations.
|
|
154
|
+
if (isAccessedViaThis(node)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
localTypeChecker.getSymbolAtLocation(node)?.declarations?.forEach((decl) => {
|
|
158
|
+
if (ts__default["default"].isParameter(decl) && topLevelParameters.has(decl)) {
|
|
159
|
+
accessedTopLevelParameters.add(decl);
|
|
160
|
+
}
|
|
161
|
+
if (ts__default["default"].isShorthandPropertyAssignment(decl)) {
|
|
162
|
+
const symbol = localTypeChecker.getShorthandAssignmentValueSymbol(decl);
|
|
163
|
+
if (symbol && symbol.valueDeclaration && ts__default["default"].isParameter(symbol.valueDeclaration)) {
|
|
164
|
+
accessedTopLevelParameters.add(symbol.valueDeclaration);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
for (const param of topLevelParameters) {
|
|
170
|
+
if (!accessedTopLevelParameters.has(param)) {
|
|
171
|
+
unusedParams.add(param);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return unusedParams;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Determines which parameters of a function declaration are used within its `super` call.
|
|
178
|
+
* @param declaration Function whose parameters to search for.
|
|
179
|
+
* @param superCall `super()` call within the function.
|
|
180
|
+
* @param localTypeChecker Type checker scoped to the file in which the function is declared.
|
|
181
|
+
*/
|
|
182
|
+
function getSuperParameters(declaration, superCall, localTypeChecker) {
|
|
183
|
+
const usedParams = new Set();
|
|
184
|
+
const topLevelParameters = new Set();
|
|
185
|
+
const topLevelParameterNames = new Set();
|
|
186
|
+
// Prepare the parameters for quicker checks further down.
|
|
187
|
+
for (const param of declaration.parameters) {
|
|
188
|
+
if (ts__default["default"].isIdentifier(param.name)) {
|
|
189
|
+
topLevelParameters.add(param);
|
|
190
|
+
topLevelParameterNames.add(param.name.text);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
superCall.forEachChild(function walk(node) {
|
|
194
|
+
if (ts__default["default"].isIdentifier(node) && topLevelParameterNames.has(node.text)) {
|
|
195
|
+
localTypeChecker.getSymbolAtLocation(node)?.declarations?.forEach((decl) => {
|
|
196
|
+
if (ts__default["default"].isParameter(decl) && topLevelParameters.has(decl)) {
|
|
197
|
+
usedParams.add(decl);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
node.forEachChild(walk);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return usedParams;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Gets the indentation text of a node. Can be used to
|
|
209
|
+
* output text with the same level of indentation.
|
|
210
|
+
* @param node Node for which to get the indentation level.
|
|
211
|
+
*/
|
|
212
|
+
function getNodeIndentation(node) {
|
|
213
|
+
const fullText = node.getFullText();
|
|
214
|
+
const end = fullText.indexOf(node.getText());
|
|
215
|
+
let result = '';
|
|
216
|
+
for (let i = end - 1; i > -1; i--) {
|
|
217
|
+
// Note: LF line endings are `\n` while CRLF are `\r\n`. This logic should cover both, because
|
|
218
|
+
// we start from the beginning of the node and go backwards so will always hit `\n` first.
|
|
219
|
+
if (fullText[i] !== '\n') {
|
|
220
|
+
result = fullText[i] + result;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
/** Checks whether a parameter node declares a property on its class. */
|
|
229
|
+
function parameterDeclaresProperty(node) {
|
|
230
|
+
return !!node.modifiers?.some(({ kind }) => kind === ts__default["default"].SyntaxKind.PublicKeyword ||
|
|
231
|
+
kind === ts__default["default"].SyntaxKind.PrivateKeyword ||
|
|
232
|
+
kind === ts__default["default"].SyntaxKind.ProtectedKeyword ||
|
|
233
|
+
kind === ts__default["default"].SyntaxKind.ReadonlyKeyword);
|
|
234
|
+
}
|
|
235
|
+
/** Checks whether a type node is nullable. */
|
|
236
|
+
function isNullableType(node) {
|
|
237
|
+
// Apparently `foo: null` is `Parameter<TypeNode<NullKeyword>>`,
|
|
238
|
+
// while `foo: undefined` is `Parameter<UndefinedKeyword>`...
|
|
239
|
+
if (node.kind === ts__default["default"].SyntaxKind.UndefinedKeyword || node.kind === ts__default["default"].SyntaxKind.VoidKeyword) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (ts__default["default"].isLiteralTypeNode(node)) {
|
|
243
|
+
return node.literal.kind === ts__default["default"].SyntaxKind.NullKeyword;
|
|
244
|
+
}
|
|
245
|
+
if (ts__default["default"].isUnionTypeNode(node)) {
|
|
246
|
+
return node.types.some(isNullableType);
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
/** Checks whether a type node has generic arguments. */
|
|
251
|
+
function hasGenerics(node) {
|
|
252
|
+
if (ts__default["default"].isTypeReferenceNode(node)) {
|
|
253
|
+
return node.typeArguments != null && node.typeArguments.length > 0;
|
|
254
|
+
}
|
|
255
|
+
if (ts__default["default"].isUnionTypeNode(node)) {
|
|
256
|
+
return node.types.some(hasGenerics);
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
/** Checks whether an identifier is accessed through `this`, e.g. `this.<some identifier>`. */
|
|
261
|
+
function isAccessedViaThis(node) {
|
|
262
|
+
return (ts__default["default"].isPropertyAccessExpression(node.parent) &&
|
|
263
|
+
node.parent.expression.kind === ts__default["default"].SyntaxKind.ThisKeyword &&
|
|
264
|
+
node.parent.name === node);
|
|
265
|
+
}
|
|
266
|
+
/** Finds a `super` call inside of a specific node. */
|
|
267
|
+
function findSuperCall(root) {
|
|
268
|
+
let result = null;
|
|
269
|
+
root.forEachChild(function find(node) {
|
|
270
|
+
if (ts__default["default"].isCallExpression(node) && node.expression.kind === ts__default["default"].SyntaxKind.SuperKeyword) {
|
|
271
|
+
result = node;
|
|
272
|
+
}
|
|
273
|
+
else if (result === null) {
|
|
274
|
+
node.forEachChild(find);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/*!
|
|
281
|
+
* @license
|
|
282
|
+
* Copyright Google LLC All Rights Reserved.
|
|
283
|
+
*
|
|
284
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
285
|
+
* found in the LICENSE file at https://angular.io/license
|
|
286
|
+
*/
|
|
287
|
+
/**
|
|
288
|
+
* Finds class property declarations without initializers whose constructor-based initialization
|
|
289
|
+
* can be inlined into the declaration spot after migrating to `inject`. For example:
|
|
290
|
+
*
|
|
291
|
+
* ```
|
|
292
|
+
* private foo: number;
|
|
293
|
+
*
|
|
294
|
+
* constructor(private service: MyService) {
|
|
295
|
+
* this.foo = this.service.getFoo();
|
|
296
|
+
* }
|
|
297
|
+
* ```
|
|
298
|
+
*
|
|
299
|
+
* The initializer of `foo` can be inlined, because `service` will be initialized
|
|
300
|
+
* before it after the `inject` migration has finished running.
|
|
301
|
+
*
|
|
302
|
+
* @param node Class declaration that is being migrated.
|
|
303
|
+
* @param constructor Constructor declaration of the class being migrated.
|
|
304
|
+
* @param localTypeChecker Type checker scoped to the current file.
|
|
305
|
+
*/
|
|
306
|
+
function findUninitializedPropertiesToCombine(node, constructor, localTypeChecker) {
|
|
307
|
+
let result = null;
|
|
308
|
+
const membersToDeclarations = new Map();
|
|
309
|
+
for (const member of node.members) {
|
|
310
|
+
if (ts__default["default"].isPropertyDeclaration(member) &&
|
|
311
|
+
!member.initializer &&
|
|
312
|
+
!ts__default["default"].isComputedPropertyName(member.name)) {
|
|
313
|
+
membersToDeclarations.set(member.name.text, member);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (membersToDeclarations.size === 0) {
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
const memberInitializers = getMemberInitializers(constructor);
|
|
320
|
+
if (memberInitializers === null) {
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
for (const [name, initializer] of memberInitializers.entries()) {
|
|
324
|
+
if (membersToDeclarations.has(name) &&
|
|
325
|
+
!hasLocalReferences(initializer, constructor, localTypeChecker)) {
|
|
326
|
+
result = result || new Map();
|
|
327
|
+
result.set(membersToDeclarations.get(name), initializer);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Finds the expressions from the constructor that initialize class members, for example:
|
|
334
|
+
*
|
|
335
|
+
* ```
|
|
336
|
+
* private foo: number;
|
|
337
|
+
*
|
|
338
|
+
* constructor() {
|
|
339
|
+
* this.foo = 123;
|
|
340
|
+
* }
|
|
341
|
+
* ```
|
|
342
|
+
*
|
|
343
|
+
* @param constructor Constructor declaration being analyzed.
|
|
344
|
+
*/
|
|
345
|
+
function getMemberInitializers(constructor) {
|
|
346
|
+
let memberInitializers = null;
|
|
347
|
+
if (!constructor.body) {
|
|
348
|
+
return memberInitializers;
|
|
349
|
+
}
|
|
350
|
+
// Only look at top-level constructor statements.
|
|
351
|
+
for (const node of constructor.body.statements) {
|
|
352
|
+
// Only look for statements in the form of `this.<name> = <expr>;` or `this[<name>] = <expr>;`.
|
|
353
|
+
if (!ts__default["default"].isExpressionStatement(node) ||
|
|
354
|
+
!ts__default["default"].isBinaryExpression(node.expression) ||
|
|
355
|
+
node.expression.operatorToken.kind !== ts__default["default"].SyntaxKind.EqualsToken ||
|
|
356
|
+
(!ts__default["default"].isPropertyAccessExpression(node.expression.left) &&
|
|
357
|
+
!ts__default["default"].isElementAccessExpression(node.expression.left)) ||
|
|
358
|
+
node.expression.left.expression.kind !== ts__default["default"].SyntaxKind.ThisKeyword) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
let name;
|
|
362
|
+
if (ts__default["default"].isPropertyAccessExpression(node.expression.left)) {
|
|
363
|
+
name = node.expression.left.name.text;
|
|
364
|
+
}
|
|
365
|
+
else if (ts__default["default"].isElementAccessExpression(node.expression.left)) {
|
|
366
|
+
name = ts__default["default"].isStringLiteralLike(node.expression.left.argumentExpression)
|
|
367
|
+
? node.expression.left.argumentExpression.text
|
|
368
|
+
: undefined;
|
|
369
|
+
}
|
|
370
|
+
// If the member is initialized multiple times, take the first one.
|
|
371
|
+
if (name && (!memberInitializers || !memberInitializers.has(name))) {
|
|
372
|
+
memberInitializers = memberInitializers || new Map();
|
|
373
|
+
memberInitializers.set(name, node.expression.right);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return memberInitializers;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Determines if a node has references to local symbols defined in the constructor.
|
|
380
|
+
* @param root Expression to check for local references.
|
|
381
|
+
* @param constructor Constructor within which the expression is used.
|
|
382
|
+
* @param localTypeChecker Type checker scoped to the current file.
|
|
383
|
+
*/
|
|
384
|
+
function hasLocalReferences(root, constructor, localTypeChecker) {
|
|
385
|
+
const sourceFile = root.getSourceFile();
|
|
386
|
+
let hasLocalRefs = false;
|
|
387
|
+
root.forEachChild(function walk(node) {
|
|
388
|
+
// Stop searching if we know that it has local references.
|
|
389
|
+
if (hasLocalRefs) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Skip identifiers that are accessed via `this` since they're accessing class members
|
|
393
|
+
// that aren't local to the constructor. This is here primarily to catch cases like this
|
|
394
|
+
// where `foo` is defined inside the constructor, but is a class member:
|
|
395
|
+
// ```
|
|
396
|
+
// constructor(private foo: Foo) {
|
|
397
|
+
// this.bar = this.foo.getFoo();
|
|
398
|
+
// }
|
|
399
|
+
// ```
|
|
400
|
+
if (ts__default["default"].isIdentifier(node) && !isAccessedViaThis(node)) {
|
|
401
|
+
const declarations = localTypeChecker.getSymbolAtLocation(node)?.declarations;
|
|
402
|
+
const isReferencingLocalSymbol = declarations?.some((decl) =>
|
|
403
|
+
// The source file check is a bit redundant since the type checker
|
|
404
|
+
// is local to the file, but it's inexpensive and it can prevent
|
|
405
|
+
// bugs in the future if we decide to use a full type checker.
|
|
406
|
+
decl.getSourceFile() === sourceFile &&
|
|
407
|
+
decl.getStart() >= constructor.getStart() &&
|
|
408
|
+
decl.getEnd() <= constructor.getEnd() &&
|
|
409
|
+
!isInsideInlineFunction(decl, constructor));
|
|
410
|
+
if (isReferencingLocalSymbol) {
|
|
411
|
+
hasLocalRefs = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!hasLocalRefs) {
|
|
415
|
+
node.forEachChild(walk);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
return hasLocalRefs;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Determines if a node is defined inside of an inline function.
|
|
422
|
+
* @param startNode Node from which to start checking for inline functions.
|
|
423
|
+
* @param boundary Node at which to stop searching.
|
|
424
|
+
*/
|
|
425
|
+
function isInsideInlineFunction(startNode, boundary) {
|
|
426
|
+
let current = startNode;
|
|
427
|
+
while (current) {
|
|
428
|
+
if (current === boundary) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
if (ts__default["default"].isFunctionDeclaration(current) ||
|
|
432
|
+
ts__default["default"].isFunctionExpression(current) ||
|
|
433
|
+
ts__default["default"].isArrowFunction(current)) {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
current = current.parent;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Placeholder used to represent expressions inside the AST.
|
|
443
|
+
* Includes Unicode characters to reduce the chance of collisions.
|
|
444
|
+
*/
|
|
445
|
+
const PLACEHOLDER = 'ɵɵngGeneratePlaceholderɵɵ';
|
|
446
|
+
/**
|
|
447
|
+
* Migrates all of the classes in a `SourceFile` away from constructor injection.
|
|
448
|
+
* @param sourceFile File to be migrated.
|
|
449
|
+
* @param options Options that configure the migration.
|
|
450
|
+
*/
|
|
451
|
+
function migrateFile(sourceFile, options) {
|
|
452
|
+
// Note: even though externally we have access to the full program with a proper type
|
|
453
|
+
// checker, we create a new one that is local to the file for a couple of reasons:
|
|
454
|
+
// 1. Not having to depend on a program makes running the migration internally faster and easier.
|
|
455
|
+
// 2. All the necessary information for this migration is local so using a file-specific type
|
|
456
|
+
// checker should speed up the lookups.
|
|
457
|
+
const localTypeChecker = getLocalTypeChecker(sourceFile);
|
|
458
|
+
const analysis = analyzeFile(sourceFile, localTypeChecker);
|
|
459
|
+
if (analysis === null || analysis.classes.length === 0) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
const printer = ts__default["default"].createPrinter();
|
|
463
|
+
const tracker = new compiler_host.ChangeTracker(printer);
|
|
464
|
+
analysis.classes.forEach(({ node, constructor, superCall }) => {
|
|
465
|
+
let removedStatements = null;
|
|
466
|
+
if (options._internalCombineMemberInitializers) {
|
|
467
|
+
findUninitializedPropertiesToCombine(node, constructor, localTypeChecker)?.forEach((initializer, property) => {
|
|
468
|
+
const statement = nodes.closestNode(initializer, ts__default["default"].isStatement);
|
|
469
|
+
if (!statement) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const newProperty = ts__default["default"].factory.createPropertyDeclaration(cloneModifiers(property.modifiers), cloneName(property.name), property.questionToken, property.type, initializer);
|
|
473
|
+
tracker.replaceText(statement.getSourceFile(), statement.getFullStart(), statement.getFullWidth(), '');
|
|
474
|
+
tracker.replaceNode(property, newProperty);
|
|
475
|
+
removedStatements = removedStatements || new Set();
|
|
476
|
+
removedStatements.add(statement);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
migrateClass(node, constructor, superCall, options, removedStatements, localTypeChecker, printer, tracker);
|
|
480
|
+
});
|
|
481
|
+
DI_PARAM_SYMBOLS.forEach((name) => {
|
|
482
|
+
// Both zero and undefined are fine here.
|
|
483
|
+
if (!analysis.nonDecoratorReferences[name]) {
|
|
484
|
+
tracker.removeImport(sourceFile, name, '@angular/core');
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
return tracker.recordChanges().get(sourceFile) || [];
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Migrates a class away from constructor injection.
|
|
491
|
+
* @param node Class to be migrated.
|
|
492
|
+
* @param constructor Reference to the class' constructor node.
|
|
493
|
+
* @param superCall Reference to the constructor's `super()` call, if any.
|
|
494
|
+
* @param options Options used to configure the migration.
|
|
495
|
+
* @param removedStatements Statements that have been removed from the constructor already.
|
|
496
|
+
* @param localTypeChecker Type checker set up for the specific file.
|
|
497
|
+
* @param printer Printer used to output AST nodes as strings.
|
|
498
|
+
* @param tracker Object keeping track of the changes made to the file.
|
|
499
|
+
*/
|
|
500
|
+
function migrateClass(node, constructor, superCall, options, removedStatements, localTypeChecker, printer, tracker) {
|
|
501
|
+
const isAbstract = !!node.modifiers?.some((m) => m.kind === ts__default["default"].SyntaxKind.AbstractKeyword);
|
|
502
|
+
// Don't migrate abstract classes by default, because
|
|
503
|
+
// their parameters aren't guaranteed to be injectable.
|
|
504
|
+
if (isAbstract && !options.migrateAbstractClasses) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const sourceFile = node.getSourceFile();
|
|
508
|
+
const unusedParameters = getConstructorUnusedParameters(constructor, localTypeChecker, removedStatements);
|
|
509
|
+
const superParameters = superCall
|
|
510
|
+
? getSuperParameters(constructor, superCall, localTypeChecker)
|
|
511
|
+
: null;
|
|
512
|
+
const memberIndentation = getNodeIndentation(node.members[0]);
|
|
513
|
+
const removedStatementCount = removedStatements?.size || 0;
|
|
514
|
+
const innerReference = superCall ||
|
|
515
|
+
constructor.body?.statements.find((statement) => !removedStatements?.has(statement)) ||
|
|
516
|
+
constructor;
|
|
517
|
+
const innerIndentation = getNodeIndentation(innerReference);
|
|
518
|
+
const propsToAdd = [];
|
|
519
|
+
const prependToConstructor = [];
|
|
520
|
+
const afterSuper = [];
|
|
521
|
+
const removedMembers = new Set();
|
|
522
|
+
for (const param of constructor.parameters) {
|
|
523
|
+
const usedInSuper = superParameters !== null && superParameters.has(param);
|
|
524
|
+
const usedInConstructor = !unusedParameters.has(param);
|
|
525
|
+
migrateParameter(param, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, memberIndentation, innerIndentation, prependToConstructor, propsToAdd, afterSuper);
|
|
526
|
+
}
|
|
527
|
+
// Delete all of the constructor overloads since below we're either going to
|
|
528
|
+
// remove the implementation, or we're going to delete all of the parameters.
|
|
529
|
+
for (const member of node.members) {
|
|
530
|
+
if (ts__default["default"].isConstructorDeclaration(member) && member !== constructor) {
|
|
531
|
+
removedMembers.add(member);
|
|
532
|
+
tracker.replaceText(sourceFile, member.getFullStart(), member.getFullWidth(), '');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!options.backwardsCompatibleConstructors &&
|
|
536
|
+
(!constructor.body || constructor.body.statements.length - removedStatementCount === 0)) {
|
|
537
|
+
// Drop the constructor if it was empty.
|
|
538
|
+
removedMembers.add(constructor);
|
|
539
|
+
tracker.replaceText(sourceFile, constructor.getFullStart(), constructor.getFullWidth(), '');
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
// If the constructor contains any statements, only remove the parameters.
|
|
543
|
+
// We always do this no matter what is passed into `backwardsCompatibleConstructors`.
|
|
544
|
+
stripConstructorParameters(constructor, tracker);
|
|
545
|
+
if (prependToConstructor.length > 0) {
|
|
546
|
+
tracker.insertText(sourceFile, innerReference.getFullStart(), `\n${prependToConstructor.join('\n')}\n`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (afterSuper.length > 0 && superCall !== null) {
|
|
550
|
+
tracker.insertText(sourceFile, superCall.getEnd() + 1, `\n${afterSuper.join('\n')}\n`);
|
|
551
|
+
}
|
|
552
|
+
// Need to resolve this once all constructor signatures have been removed.
|
|
553
|
+
const memberReference = node.members.find((m) => !removedMembers.has(m)) || node.members[0];
|
|
554
|
+
// If `backwardsCompatibleConstructors` is enabled, we maintain
|
|
555
|
+
// backwards compatibility by adding a catch-all signature.
|
|
556
|
+
if (options.backwardsCompatibleConstructors) {
|
|
557
|
+
const extraSignature = `\n${memberIndentation}/** Inserted by Angular inject() migration for backwards compatibility */\n` +
|
|
558
|
+
`${memberIndentation}constructor(...args: unknown[]);`;
|
|
559
|
+
// The new signature always has to be right before the constructor implementation.
|
|
560
|
+
if (memberReference === constructor) {
|
|
561
|
+
propsToAdd.push(extraSignature);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
tracker.insertText(sourceFile, constructor.getFullStart(), '\n' + extraSignature);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (propsToAdd.length > 0) {
|
|
568
|
+
if (removedMembers.size === node.members.length) {
|
|
569
|
+
tracker.insertText(sourceFile, constructor.getEnd() + 1, `${propsToAdd.join('\n')}\n`);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// Insert the new properties after the first member that hasn't been deleted.
|
|
573
|
+
tracker.insertText(sourceFile, memberReference.getFullStart(), `\n${propsToAdd.join('\n')}\n`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Migrates a single parameter to `inject()` DI.
|
|
579
|
+
* @param node Parameter to be migrated.
|
|
580
|
+
* @param options Options used to configure the migration.
|
|
581
|
+
* @param localTypeChecker Type checker set up for the specific file.
|
|
582
|
+
* @param printer Printer used to output AST nodes as strings.
|
|
583
|
+
* @param tracker Object keeping track of the changes made to the file.
|
|
584
|
+
* @param superCall Call to `super()` from the class' constructor.
|
|
585
|
+
* @param usedInSuper Whether the parameter is referenced inside of `super`.
|
|
586
|
+
* @param usedInConstructor Whether the parameter is referenced inside the body of the constructor.
|
|
587
|
+
* @param memberIndentation Indentation string to use when inserting new class members.
|
|
588
|
+
* @param innerIndentation Indentation string to use when inserting new constructor statements.
|
|
589
|
+
* @param prependToConstructor Statements to be prepended to the constructor.
|
|
590
|
+
* @param propsToAdd Properties to be added to the class.
|
|
591
|
+
* @param afterSuper Statements to be added after the `super` call.
|
|
592
|
+
*/
|
|
593
|
+
function migrateParameter(node, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, memberIndentation, innerIndentation, prependToConstructor, propsToAdd, afterSuper) {
|
|
594
|
+
if (!ts__default["default"].isIdentifier(node.name)) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const name = node.name.text;
|
|
598
|
+
const replacementCall = createInjectReplacementCall(node, options, localTypeChecker, printer, tracker);
|
|
599
|
+
const declaresProp = parameterDeclaresProperty(node);
|
|
600
|
+
// If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
|
|
601
|
+
if (declaresProp) {
|
|
602
|
+
const prop = ts__default["default"].factory.createPropertyDeclaration(cloneModifiers(node.modifiers?.filter((modifier) => {
|
|
603
|
+
// Strip out the DI decorators, as well as `public` which is redundant.
|
|
604
|
+
return !ts__default["default"].isDecorator(modifier) && modifier.kind !== ts__default["default"].SyntaxKind.PublicKeyword;
|
|
605
|
+
})), name,
|
|
606
|
+
// Don't add the question token to private properties since it won't affect interface implementation.
|
|
607
|
+
node.modifiers?.some((modifier) => modifier.kind === ts__default["default"].SyntaxKind.PrivateKeyword)
|
|
608
|
+
? undefined
|
|
609
|
+
: node.questionToken,
|
|
610
|
+
// We can't initialize the property if it's referenced within a `super` call.
|
|
611
|
+
// See the logic further below for the initialization.
|
|
612
|
+
usedInSuper ? node.type : undefined, usedInSuper ? undefined : ts__default["default"].factory.createIdentifier(PLACEHOLDER));
|
|
613
|
+
propsToAdd.push(memberIndentation +
|
|
614
|
+
replaceNodePlaceholder(node.getSourceFile(), prop, replacementCall, printer));
|
|
615
|
+
}
|
|
616
|
+
// If the parameter is referenced within the constructor, we need to declare it as a variable.
|
|
617
|
+
if (usedInConstructor) {
|
|
618
|
+
if (usedInSuper) {
|
|
619
|
+
// Usages of `this` aren't allowed before `super` calls so we need to
|
|
620
|
+
// create a variable which calls `inject()` directly instead...
|
|
621
|
+
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
|
|
622
|
+
// ...then we can initialize the property after the `super` call.
|
|
623
|
+
if (declaresProp) {
|
|
624
|
+
afterSuper.push(`${innerIndentation}this.${name} = ${name};`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
else if (declaresProp) {
|
|
628
|
+
// If the parameter declares a property (`private foo: foo`) and is used inside the class
|
|
629
|
+
// at the same time, we need to ensure that it's initialized to the value from the variable
|
|
630
|
+
// and that we only reference `this` after the `super` call.
|
|
631
|
+
const initializer = `${innerIndentation}const ${name} = this.${name};`;
|
|
632
|
+
if (superCall === null) {
|
|
633
|
+
prependToConstructor.push(initializer);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
afterSuper.push(initializer);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// If the parameter is only referenced in the constructor, we
|
|
641
|
+
// don't need to declare any new properties.
|
|
642
|
+
prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Creates a replacement `inject` call from a function parameter.
|
|
648
|
+
* @param param Parameter for which to generate the `inject` call.
|
|
649
|
+
* @param options Options used to configure the migration.
|
|
650
|
+
* @param localTypeChecker Type checker set up for the specific file.
|
|
651
|
+
* @param printer Printer used to output AST nodes as strings.
|
|
652
|
+
* @param tracker Object keeping track of the changes made to the file.
|
|
653
|
+
*/
|
|
654
|
+
function createInjectReplacementCall(param, options, localTypeChecker, printer, tracker) {
|
|
655
|
+
const moduleName = '@angular/core';
|
|
656
|
+
const sourceFile = param.getSourceFile();
|
|
657
|
+
const decorators = nodes.getAngularDecorators(localTypeChecker, ts__default["default"].getDecorators(param) || []);
|
|
658
|
+
const literalProps = [];
|
|
659
|
+
const type = param.type;
|
|
660
|
+
let injectedType = '';
|
|
661
|
+
let typeArguments = type && hasGenerics(type) ? [type] : undefined;
|
|
662
|
+
let hasOptionalDecorator = false;
|
|
663
|
+
if (type) {
|
|
664
|
+
// Remove the type arguments from generic type references, because
|
|
665
|
+
// they'll be specified as type arguments to `inject()`.
|
|
666
|
+
if (ts__default["default"].isTypeReferenceNode(type) && type.typeArguments && type.typeArguments.length > 0) {
|
|
667
|
+
injectedType = type.typeName.getText();
|
|
668
|
+
}
|
|
669
|
+
else if (ts__default["default"].isUnionTypeNode(type)) {
|
|
670
|
+
injectedType = (type.types.find((t) => !ts__default["default"].isLiteralTypeNode(t)) || type.types[0]).getText();
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
injectedType = type.getText();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for (const decorator of decorators) {
|
|
677
|
+
if (decorator.moduleName !== moduleName) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const firstArg = decorator.node.expression.arguments[0];
|
|
681
|
+
switch (decorator.name) {
|
|
682
|
+
case 'Inject':
|
|
683
|
+
if (firstArg) {
|
|
684
|
+
const injectResult = migrateInjectDecorator(firstArg, type, localTypeChecker);
|
|
685
|
+
injectedType = injectResult.injectedType;
|
|
686
|
+
if (injectResult.typeArguments) {
|
|
687
|
+
typeArguments = injectResult.typeArguments;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
break;
|
|
691
|
+
case 'Attribute':
|
|
692
|
+
if (firstArg) {
|
|
693
|
+
const constructorRef = tracker.addImport(sourceFile, 'HostAttributeToken', moduleName);
|
|
694
|
+
const expression = ts__default["default"].factory.createNewExpression(constructorRef, undefined, [firstArg]);
|
|
695
|
+
injectedType = printer.printNode(ts__default["default"].EmitHint.Unspecified, expression, sourceFile);
|
|
696
|
+
typeArguments = undefined;
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
case 'Optional':
|
|
700
|
+
hasOptionalDecorator = true;
|
|
701
|
+
literalProps.push(ts__default["default"].factory.createPropertyAssignment('optional', ts__default["default"].factory.createTrue()));
|
|
702
|
+
break;
|
|
703
|
+
case 'SkipSelf':
|
|
704
|
+
literalProps.push(ts__default["default"].factory.createPropertyAssignment('skipSelf', ts__default["default"].factory.createTrue()));
|
|
705
|
+
break;
|
|
706
|
+
case 'Self':
|
|
707
|
+
literalProps.push(ts__default["default"].factory.createPropertyAssignment('self', ts__default["default"].factory.createTrue()));
|
|
708
|
+
break;
|
|
709
|
+
case 'Host':
|
|
710
|
+
literalProps.push(ts__default["default"].factory.createPropertyAssignment('host', ts__default["default"].factory.createTrue()));
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// The injected type might be a `TypeNode` which we can't easily convert into an `Expression`.
|
|
715
|
+
// Since the value gets passed through directly anyway, we generate the call using a placeholder
|
|
716
|
+
// which we then replace with the raw text of the `TypeNode`.
|
|
717
|
+
const injectRef = tracker.addImport(param.getSourceFile(), 'inject', moduleName);
|
|
718
|
+
const args = [ts__default["default"].factory.createIdentifier(PLACEHOLDER)];
|
|
719
|
+
if (literalProps.length > 0) {
|
|
720
|
+
args.push(ts__default["default"].factory.createObjectLiteralExpression(literalProps));
|
|
721
|
+
}
|
|
722
|
+
let expression = ts__default["default"].factory.createCallExpression(injectRef, typeArguments, args);
|
|
723
|
+
if (hasOptionalDecorator && options.nonNullableOptional) {
|
|
724
|
+
const hasNullableType = param.questionToken != null || (param.type != null && isNullableType(param.type));
|
|
725
|
+
// Only wrap the expression if the type wasn't already nullable.
|
|
726
|
+
// If it was, the app was likely accounting for it already.
|
|
727
|
+
if (!hasNullableType) {
|
|
728
|
+
expression = ts__default["default"].factory.createNonNullExpression(expression);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return replaceNodePlaceholder(param.getSourceFile(), expression, injectedType, printer);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Migrates a parameter based on its `@Inject()` decorator.
|
|
735
|
+
* @param firstArg First argument to `@Inject()`.
|
|
736
|
+
* @param type Type of the parameter.
|
|
737
|
+
* @param localTypeChecker Type checker set up for the specific file.
|
|
738
|
+
*/
|
|
739
|
+
function migrateInjectDecorator(firstArg, type, localTypeChecker) {
|
|
740
|
+
let injectedType = firstArg.getText();
|
|
741
|
+
let typeArguments = null;
|
|
742
|
+
// `inject` no longer officially supports string injection so we need
|
|
743
|
+
// to cast to any. We maintain the type by passing it as a generic.
|
|
744
|
+
if (ts__default["default"].isStringLiteralLike(firstArg)) {
|
|
745
|
+
typeArguments = [type || ts__default["default"].factory.createKeywordTypeNode(ts__default["default"].SyntaxKind.AnyKeyword)];
|
|
746
|
+
injectedType += ' as any';
|
|
747
|
+
}
|
|
748
|
+
else if (ts__default["default"].isCallExpression(firstArg) &&
|
|
749
|
+
ts__default["default"].isIdentifier(firstArg.expression) &&
|
|
750
|
+
firstArg.arguments.length === 1) {
|
|
751
|
+
const callImport = nodes.getImportOfIdentifier(localTypeChecker, firstArg.expression);
|
|
752
|
+
const arrowFn = firstArg.arguments[0];
|
|
753
|
+
// If the first parameter is a `forwardRef`, unwrap it for a more
|
|
754
|
+
// accurate type and because it's no longer necessary.
|
|
755
|
+
if (callImport !== null &&
|
|
756
|
+
callImport.name === 'forwardRef' &&
|
|
757
|
+
callImport.importModule === '@angular/core' &&
|
|
758
|
+
ts__default["default"].isArrowFunction(arrowFn)) {
|
|
759
|
+
if (ts__default["default"].isBlock(arrowFn.body)) {
|
|
760
|
+
const returnStatement = arrowFn.body.statements.find((stmt) => ts__default["default"].isReturnStatement(stmt));
|
|
761
|
+
if (returnStatement && returnStatement.expression) {
|
|
762
|
+
injectedType = returnStatement.expression.getText();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
injectedType = arrowFn.body.getText();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
else if (
|
|
771
|
+
// Pass the type for cases like `@Inject(FOO_TOKEN) foo: Foo`, because:
|
|
772
|
+
// 1. It guarantees that the type stays the same as before.
|
|
773
|
+
// 2. Avoids leaving unused imports behind.
|
|
774
|
+
// We only do this for type references since the `@Inject` pattern above is fairly common and
|
|
775
|
+
// apps don't necessarily type their injection tokens correctly, whereas doing it for literal
|
|
776
|
+
// types will add a lot of noise to the generated code.
|
|
777
|
+
type &&
|
|
778
|
+
(ts__default["default"].isTypeReferenceNode(type) ||
|
|
779
|
+
(ts__default["default"].isUnionTypeNode(type) && type.types.some(ts__default["default"].isTypeReferenceNode)))) {
|
|
780
|
+
typeArguments = [type];
|
|
781
|
+
}
|
|
782
|
+
return { injectedType, typeArguments };
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Removes the parameters from a constructor. This is a bit more complex than just replacing an AST
|
|
786
|
+
* node, because `NodeArray.pos` includes any leading whitespace, but `NodeArray.end` does **not**
|
|
787
|
+
* include trailing whitespace. Since we want to produce somewhat formatted code, we need to find
|
|
788
|
+
* the end of the arguments ourselves. We do it by finding the next parenthesis after the last
|
|
789
|
+
* parameter.
|
|
790
|
+
* @param node Constructor from which to remove the parameters.
|
|
791
|
+
* @param tracker Object keeping track of the changes made to the file.
|
|
792
|
+
*/
|
|
793
|
+
function stripConstructorParameters(node, tracker) {
|
|
794
|
+
if (node.parameters.length === 0) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const constructorText = node.getText();
|
|
798
|
+
const lastParamText = node.parameters[node.parameters.length - 1].getText();
|
|
799
|
+
const lastParamStart = constructorText.indexOf(lastParamText);
|
|
800
|
+
const whitespacePattern = /\s/;
|
|
801
|
+
let trailingCharacters = 0;
|
|
802
|
+
if (lastParamStart > -1) {
|
|
803
|
+
let lastParamEnd = lastParamStart + lastParamText.length;
|
|
804
|
+
let closeParenIndex = -1;
|
|
805
|
+
for (let i = lastParamEnd; i < constructorText.length; i++) {
|
|
806
|
+
const char = constructorText[i];
|
|
807
|
+
if (char === ')') {
|
|
808
|
+
closeParenIndex = i;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
else if (!whitespacePattern.test(char)) {
|
|
812
|
+
// The end of the last parameter won't include
|
|
813
|
+
// any trailing commas which we need to account for.
|
|
814
|
+
lastParamEnd = i + 1;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (closeParenIndex > -1) {
|
|
818
|
+
trailingCharacters = closeParenIndex - lastParamEnd;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
tracker.replaceText(node.getSourceFile(), node.parameters.pos, node.parameters.end - node.parameters.pos + trailingCharacters, '');
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Creates a type checker scoped to a specific file.
|
|
825
|
+
* @param sourceFile File for which to create the type checker.
|
|
826
|
+
*/
|
|
827
|
+
function getLocalTypeChecker(sourceFile) {
|
|
828
|
+
const options = { noEmit: true, skipLibCheck: true };
|
|
829
|
+
const host = ts__default["default"].createCompilerHost(options);
|
|
830
|
+
host.getSourceFile = (fileName) => (fileName === sourceFile.fileName ? sourceFile : undefined);
|
|
831
|
+
const program = ts__default["default"].createProgram({
|
|
832
|
+
rootNames: [sourceFile.fileName],
|
|
833
|
+
options,
|
|
834
|
+
host,
|
|
835
|
+
});
|
|
836
|
+
return program.getTypeChecker();
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Prints out an AST node and replaces the placeholder inside of it.
|
|
840
|
+
* @param sourceFile File in which the node will be inserted.
|
|
841
|
+
* @param node Node to be printed out.
|
|
842
|
+
* @param replacement Replacement for the placeholder.
|
|
843
|
+
* @param printer Printer used to output AST nodes as strings.
|
|
844
|
+
*/
|
|
845
|
+
function replaceNodePlaceholder(sourceFile, node, replacement, printer) {
|
|
846
|
+
const result = printer.printNode(ts__default["default"].EmitHint.Unspecified, node, sourceFile);
|
|
847
|
+
return result.replace(PLACEHOLDER, replacement);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Clones an optional array of modifiers. Can be useful to
|
|
851
|
+
* strip the comments from a node with modifiers.
|
|
852
|
+
*/
|
|
853
|
+
function cloneModifiers(modifiers) {
|
|
854
|
+
return modifiers?.map((modifier) => {
|
|
855
|
+
return ts__default["default"].isDecorator(modifier)
|
|
856
|
+
? ts__default["default"].factory.createDecorator(modifier.expression)
|
|
857
|
+
: ts__default["default"].factory.createModifier(modifier.kind);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Clones the name of a property. Can be useful to strip away
|
|
862
|
+
* the comments of a property without modifiers.
|
|
863
|
+
*/
|
|
864
|
+
function cloneName(node) {
|
|
865
|
+
switch (node.kind) {
|
|
866
|
+
case ts__default["default"].SyntaxKind.Identifier:
|
|
867
|
+
return ts__default["default"].factory.createIdentifier(node.text);
|
|
868
|
+
case ts__default["default"].SyntaxKind.StringLiteral:
|
|
869
|
+
return ts__default["default"].factory.createStringLiteral(node.text, node.getText()[0] === `'`);
|
|
870
|
+
case ts__default["default"].SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
871
|
+
return ts__default["default"].factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText);
|
|
872
|
+
case ts__default["default"].SyntaxKind.NumericLiteral:
|
|
873
|
+
return ts__default["default"].factory.createNumericLiteral(node.text);
|
|
874
|
+
case ts__default["default"].SyntaxKind.ComputedPropertyName:
|
|
875
|
+
return ts__default["default"].factory.createComputedPropertyName(node.expression);
|
|
876
|
+
case ts__default["default"].SyntaxKind.PrivateIdentifier:
|
|
877
|
+
return ts__default["default"].factory.createPrivateIdentifier(node.text);
|
|
878
|
+
default:
|
|
879
|
+
return node;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function migrate(options) {
|
|
884
|
+
return async (tree) => {
|
|
885
|
+
const basePath = process.cwd();
|
|
886
|
+
const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
|
|
887
|
+
let allPaths = [];
|
|
888
|
+
if (pathToMigrate.trim() !== '') {
|
|
889
|
+
allPaths.push(pathToMigrate);
|
|
890
|
+
}
|
|
891
|
+
if (!allPaths.length) {
|
|
892
|
+
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the inject migration.');
|
|
893
|
+
}
|
|
894
|
+
for (const tsconfigPath of allPaths) {
|
|
895
|
+
runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
|
|
900
|
+
if (schematicOptions.path.startsWith('..')) {
|
|
901
|
+
throw new schematics.SchematicsException('Cannot run inject migration outside of the current project.');
|
|
902
|
+
}
|
|
903
|
+
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
|
|
904
|
+
const sourceFiles = program
|
|
905
|
+
.getSourceFiles()
|
|
906
|
+
.filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
|
|
907
|
+
compiler_host.canMigrateFile(basePath, sourceFile, program));
|
|
908
|
+
if (sourceFiles.length === 0) {
|
|
909
|
+
throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the inject migration.`);
|
|
910
|
+
}
|
|
911
|
+
for (const sourceFile of sourceFiles) {
|
|
912
|
+
const changes = migrateFile(sourceFile, schematicOptions);
|
|
913
|
+
if (changes.length > 0) {
|
|
914
|
+
const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
|
|
915
|
+
for (const change of changes) {
|
|
916
|
+
if (change.removeLength != null) {
|
|
917
|
+
update.remove(change.start, change.removeLength);
|
|
918
|
+
}
|
|
919
|
+
update.insertRight(change.start, change.text);
|
|
920
|
+
}
|
|
921
|
+
tree.commitUpdate(update);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
exports.migrate = migrate;
|