@ethlete/core 5.0.0-next.60 → 5.0.0-next.61
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/CHANGELOG.md +28 -0
- package/fesm2022/ethlete-core.mjs +269 -203
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/migrate-to-v5/apply-replacements.js +14 -0
- package/generators/migrate-to-v5/color-naming.js +11 -57
- package/generators/migrate-to-v5/create-provider.js +56 -44
- package/generators/migrate-to-v5/migration-files.js +13 -0
- package/generators/migrate-to-v5/migration.js +31 -27
- package/generators/migrate-to-v5/removed-exports.js +7 -27
- package/generators/migrate-to-v5/router-state-service.js +118 -350
- package/generators/migrate-to-v5/schema.json +12 -0
- package/generators/migrate-to-v5/service-classes.js +108 -0
- package/generators/migrate-to-v5/viewport-service.js +195 -443
- package/generators/tailwind-4-color-theme/generator.js +71 -85
- package/package.json +1 -1
- package/types/ethlete-core.d.ts +41 -34
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { logger } from '@nx/devkit';
|
|
1
2
|
import * as ts from 'typescript';
|
|
3
|
+
import { collectFiles } from './migration-files.js';
|
|
4
|
+
import { applyReplacementsInClass, collectClasses, findServiceClasses, getServiceImport, isServiceParameter, isServiceProperty, isServiceStillUsed, removeServiceImport, } from './service-classes.js';
|
|
2
5
|
function getPropertyMaps() {
|
|
3
|
-
// Property map for signals (getters for latest value)
|
|
4
6
|
const signalPropertyMap = {
|
|
5
7
|
route: 'injectRoute',
|
|
6
8
|
state: 'injectRouterState',
|
|
@@ -11,7 +13,6 @@ function getPropertyMaps() {
|
|
|
11
13
|
fragment: 'injectFragment',
|
|
12
14
|
latestEvent: 'injectRouterEvent',
|
|
13
15
|
};
|
|
14
|
-
// Property map for observables
|
|
15
16
|
const observablePropertyMap = {
|
|
16
17
|
route$: 'injectRoute',
|
|
17
18
|
state$: 'injectRouterState',
|
|
@@ -25,17 +26,15 @@ function getPropertyMaps() {
|
|
|
25
26
|
};
|
|
26
27
|
return { signalPropertyMap, observablePropertyMap };
|
|
27
28
|
}
|
|
28
|
-
function findRouterStateServiceVariables(sourceFile) {
|
|
29
|
+
function findRouterStateServiceVariables(sourceFile, serviceName) {
|
|
29
30
|
const variables = [];
|
|
30
31
|
function visit(node) {
|
|
31
|
-
// Check for inject(RouterStateService) pattern
|
|
32
32
|
if (ts.isCallExpression(node) &&
|
|
33
33
|
ts.isIdentifier(node.expression) &&
|
|
34
34
|
node.expression.text === 'inject' &&
|
|
35
35
|
node.arguments.length > 0) {
|
|
36
36
|
const arg = node.arguments[0];
|
|
37
|
-
if (ts.isIdentifier(arg) && arg.text ===
|
|
38
|
-
// Find the variable name
|
|
37
|
+
if (ts.isIdentifier(arg) && arg.text === serviceName) {
|
|
39
38
|
let parent = node.parent;
|
|
40
39
|
while (parent) {
|
|
41
40
|
if (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
@@ -50,11 +49,10 @@ function findRouterStateServiceVariables(sourceFile) {
|
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
|
-
// Check for constructor parameter injection
|
|
54
52
|
if (ts.isParameter(node)) {
|
|
55
53
|
const typeNode = node.type;
|
|
56
54
|
if (typeNode && ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
57
|
-
if (typeNode.typeName.text ===
|
|
55
|
+
if (typeNode.typeName.text === serviceName && node.name && ts.isIdentifier(node.name)) {
|
|
58
56
|
variables.push(node.name.text);
|
|
59
57
|
}
|
|
60
58
|
}
|
|
@@ -64,31 +62,6 @@ function findRouterStateServiceVariables(sourceFile) {
|
|
|
64
62
|
visit(sourceFile);
|
|
65
63
|
return variables;
|
|
66
64
|
}
|
|
67
|
-
function findClassForRouterStateService(sourceFile, routerStateServiceVar) {
|
|
68
|
-
let classNode = null;
|
|
69
|
-
function visit(node) {
|
|
70
|
-
if (classNode)
|
|
71
|
-
return;
|
|
72
|
-
if (ts.isClassDeclaration(node)) {
|
|
73
|
-
const hasRouterStateService = node.members.some((member) => {
|
|
74
|
-
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
75
|
-
return member.name.text === routerStateServiceVar;
|
|
76
|
-
}
|
|
77
|
-
if (ts.isConstructorDeclaration(member)) {
|
|
78
|
-
return member.parameters.some((param) => ts.isIdentifier(param.name) && param.name.text === routerStateServiceVar);
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
});
|
|
82
|
-
if (hasRouterStateService) {
|
|
83
|
-
classNode = node;
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
ts.forEachChild(node, visit);
|
|
88
|
-
}
|
|
89
|
-
visit(sourceFile);
|
|
90
|
-
return classNode;
|
|
91
|
-
}
|
|
92
65
|
function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
93
66
|
const context = {
|
|
94
67
|
routerStateServiceVar,
|
|
@@ -96,15 +69,13 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
96
69
|
membersToAdd: [],
|
|
97
70
|
replacements: new Map(),
|
|
98
71
|
importsNeeded: new Set(),
|
|
99
|
-
constructorCalls: [],
|
|
72
|
+
constructorCalls: [],
|
|
100
73
|
};
|
|
101
|
-
// Collect existing member names and detect property initializers
|
|
102
74
|
const propertyInitializers = new Map();
|
|
103
75
|
classNode.members.forEach((member) => {
|
|
104
76
|
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
105
77
|
const memberName = member.name.text;
|
|
106
78
|
context.existingMembers.add(memberName);
|
|
107
|
-
// Check if this property has an initializer that uses RouterStateService
|
|
108
79
|
if (member.initializer) {
|
|
109
80
|
const initializerText = member.initializer.getText(sourceFile);
|
|
110
81
|
if (initializerText.includes(routerStateServiceVar)) {
|
|
@@ -117,7 +88,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
117
88
|
}
|
|
118
89
|
});
|
|
119
90
|
const { signalPropertyMap, observablePropertyMap } = getPropertyMaps();
|
|
120
|
-
// Method map
|
|
121
91
|
const methodMap = {
|
|
122
92
|
selectQueryParam: { injectFn: 'injectQueryParam', type: 'observable', requiresArgs: true },
|
|
123
93
|
selectPathParam: { injectFn: 'injectPathParam', type: 'observable', requiresArgs: true },
|
|
@@ -129,13 +99,10 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
129
99
|
needsInjectionContext: true,
|
|
130
100
|
},
|
|
131
101
|
};
|
|
132
|
-
// Track usages
|
|
133
102
|
const usagesWrappedInToSignal = new Map();
|
|
134
103
|
const usagesOutsideToSignal = new Map();
|
|
135
104
|
const usagesInPropertyInitializers = new Map();
|
|
136
|
-
// First pass: detect toSignal usages, property initializers, and count all usages
|
|
137
105
|
function detectUsages(node, insideToSignal = false, currentProperty) {
|
|
138
|
-
// Handle toSignal wrapper
|
|
139
106
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
|
|
140
107
|
const arg = node.arguments[0];
|
|
141
108
|
if (arg) {
|
|
@@ -143,7 +110,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
143
110
|
}
|
|
144
111
|
return;
|
|
145
112
|
}
|
|
146
|
-
// Track RouterStateService property access
|
|
147
113
|
if (ts.isPropertyAccessExpression(node)) {
|
|
148
114
|
const propertyName = node.name.text;
|
|
149
115
|
const isRouterStateAccess = (ts.isIdentifier(node.expression) && node.expression.text === routerStateServiceVar) ||
|
|
@@ -160,7 +126,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
160
126
|
usage: fullAccess,
|
|
161
127
|
propertyDecl,
|
|
162
128
|
});
|
|
163
|
-
// Also track if it's wrapped in toSignal
|
|
164
129
|
if (insideToSignal) {
|
|
165
130
|
usagesWrappedInToSignal.set(fullAccess, (usagesWrappedInToSignal.get(fullAccess) || 0) + 1);
|
|
166
131
|
}
|
|
@@ -174,7 +139,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
174
139
|
}
|
|
175
140
|
}
|
|
176
141
|
}
|
|
177
|
-
// Track method calls
|
|
178
142
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
179
143
|
const methodName = node.expression.name.text;
|
|
180
144
|
if (methodName !== 'pipe' && methodName !== 'subscribe') {
|
|
@@ -192,7 +156,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
192
156
|
usage: fullCall,
|
|
193
157
|
propertyDecl,
|
|
194
158
|
});
|
|
195
|
-
// Also track if it's wrapped in toSignal
|
|
196
159
|
if (insideToSignal) {
|
|
197
160
|
usagesWrappedInToSignal.set(fullCall, (usagesWrappedInToSignal.get(fullCall) || 0) + 1);
|
|
198
161
|
}
|
|
@@ -209,7 +172,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
209
172
|
}
|
|
210
173
|
ts.forEachChild(node, (child) => detectUsages(child, insideToSignal, currentProperty));
|
|
211
174
|
}
|
|
212
|
-
// Detect usages in property initializers
|
|
213
175
|
classNode.members.forEach((member) => {
|
|
214
176
|
if (ts.isPropertyDeclaration(member) && member.initializer && ts.isIdentifier(member.name)) {
|
|
215
177
|
const propertyName = member.name.text;
|
|
@@ -220,17 +182,14 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
220
182
|
else if (ts.isPropertyDeclaration(member) &&
|
|
221
183
|
ts.isIdentifier(member.name) &&
|
|
222
184
|
member.name.text === routerStateServiceVar) {
|
|
223
|
-
// Skip the RouterStateService property itself
|
|
224
185
|
return;
|
|
225
186
|
}
|
|
226
187
|
else {
|
|
227
188
|
detectUsages(member);
|
|
228
189
|
}
|
|
229
190
|
});
|
|
230
|
-
// Group property initializer usages by usage pattern
|
|
231
191
|
const propertyInitializerUsages = new Map();
|
|
232
192
|
for (const [usage, info] of usagesInPropertyInitializers) {
|
|
233
|
-
// Determine if this is a property access or method call
|
|
234
193
|
const isMethodCall = usage.includes('(');
|
|
235
194
|
let usageKey;
|
|
236
195
|
let injectFn;
|
|
@@ -238,8 +197,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
238
197
|
let args;
|
|
239
198
|
let genericType = '';
|
|
240
199
|
if (isMethodCall) {
|
|
241
|
-
// Extract method name, generic type, and args
|
|
242
|
-
// Remove whitespace and newlines for matching
|
|
243
200
|
const normalizedUsage = usage.replace(/\s+/g, ' ');
|
|
244
201
|
const match = normalizedUsage.match(/\.(\w+)(?:<([^>]+)>)?\(([^)]*)\)/);
|
|
245
202
|
if (!match)
|
|
@@ -255,7 +212,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
255
212
|
type = methodInfo.type;
|
|
256
213
|
}
|
|
257
214
|
else {
|
|
258
|
-
// Extract property name
|
|
259
215
|
const match = usage.match(/\.(\w+\$?)$/);
|
|
260
216
|
if (!match)
|
|
261
217
|
continue;
|
|
@@ -279,10 +235,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
279
235
|
wrappedInToSignal,
|
|
280
236
|
});
|
|
281
237
|
}
|
|
282
|
-
// Process property initializer usages
|
|
283
238
|
for (const [usageKey, usageInfo] of propertyInitializerUsages) {
|
|
284
239
|
const { injectFn, args, type, genericType, properties } = usageInfo;
|
|
285
|
-
// Check if this same usage is used outside property initializers
|
|
286
240
|
const isUsedOutsideInitializers = usagesOutsideToSignal.has(usageKey) ||
|
|
287
241
|
Array.from(usagesOutsideToSignal.keys()).some((key) => {
|
|
288
242
|
if (args) {
|
|
@@ -291,7 +245,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
291
245
|
return key.includes(usageKey);
|
|
292
246
|
});
|
|
293
247
|
if (isUsedOutsideInitializers && type === 'observable') {
|
|
294
|
-
// Create a shared member
|
|
295
248
|
const baseName = usageKey.endsWith('$') ? usageKey : `${usageKey}$`;
|
|
296
249
|
const memberName = findAvailableMemberName(baseName, context.existingMembers);
|
|
297
250
|
const memberInfo = {
|
|
@@ -304,10 +257,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
304
257
|
};
|
|
305
258
|
context.membersToAdd.push(memberInfo);
|
|
306
259
|
context.existingMembers.add(memberName);
|
|
307
|
-
// For each property, replace the usage with the member reference
|
|
308
260
|
for (const { propertyDecl } of properties) {
|
|
309
261
|
const initializerText = propertyDecl.initializer.getText(sourceFile);
|
|
310
|
-
// Create pattern to match only the RouterStateService access part
|
|
311
262
|
const isMethodCall = usageKey.includes('(');
|
|
312
263
|
if (isMethodCall) {
|
|
313
264
|
const methodNameOnly = usageKey.split(/[<(]/)[0];
|
|
@@ -316,7 +267,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
316
267
|
const pattern = new RegExp(`(this\\.)?${escapedVar}\\s*\\.\\s*${escapedMethod}(?:<[^>]+>)?\\s*\\([^)]*\\)`, 'gs');
|
|
317
268
|
const match = initializerText.match(pattern);
|
|
318
269
|
if (match && match[0]) {
|
|
319
|
-
// Check if the original has 'this.' prefix
|
|
320
270
|
const hasThisPrefix = match[0].startsWith('this.');
|
|
321
271
|
const replacement = hasThisPrefix ? `this.${memberName}` : memberName;
|
|
322
272
|
context.replacements.set(match[0], replacement);
|
|
@@ -326,7 +276,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
326
276
|
const pattern = new RegExp(`(this\\.)?${escapeRegExp(routerStateServiceVar)}\\.${escapeRegExp(usageKey)}`, 'g');
|
|
327
277
|
const matches = initializerText.match(pattern);
|
|
328
278
|
if (matches && matches[0]) {
|
|
329
|
-
// Check if the original has 'this.' prefix
|
|
330
279
|
const hasThisPrefix = matches[0].startsWith('this.');
|
|
331
280
|
const replacement = hasThisPrefix ? `this.${memberName}` : memberName;
|
|
332
281
|
context.replacements.set(matches[0], replacement);
|
|
@@ -336,19 +285,15 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
336
285
|
context.importsNeeded.add(injectFn);
|
|
337
286
|
}
|
|
338
287
|
else {
|
|
339
|
-
// Replace each property initializer directly
|
|
340
288
|
for (const { propertyDecl, wrappedInToSignal } of properties) {
|
|
341
289
|
const initializerText = propertyDecl.initializer.getText(sourceFile);
|
|
342
290
|
const injectCall = args ? `${injectFn}${genericType || ''}(${args})` : `${injectFn}()`;
|
|
343
291
|
const propertyName = ts.isIdentifier(propertyDecl.name) ? propertyDecl.name.text : '';
|
|
344
292
|
const isObservableProperty = propertyName.endsWith('$');
|
|
345
|
-
const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(
|
|
293
|
+
const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl);
|
|
346
294
|
const hasChainedCalls = initializerText.includes('.pipe(') ||
|
|
347
295
|
initializerText.includes('.subscribe(') ||
|
|
348
296
|
initializerText.match(/\)\s*\./);
|
|
349
|
-
// Determine if we need toObservable wrapper:
|
|
350
|
-
// 1. If NOT wrapped in toSignal and type is observable and (property ends with $ or used elsewhere)
|
|
351
|
-
// 2. If wrapped in toSignal but has chained calls (pipe/subscribe), we need toObservable
|
|
352
297
|
const needsToObservable = (type === 'observable' && !wrappedInToSignal && (isObservableProperty || isPropertyUsedElsewhere)) ||
|
|
353
298
|
(wrappedInToSignal && hasChainedCalls);
|
|
354
299
|
const wrappedInjectCall = needsToObservable ? `toObservable(${injectCall})` : injectCall;
|
|
@@ -379,7 +324,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
379
324
|
}
|
|
380
325
|
}
|
|
381
326
|
}
|
|
382
|
-
// Handle direct usages (not in property initializers)
|
|
383
327
|
for (const [usage] of usagesWrappedInToSignal) {
|
|
384
328
|
if (!usagesInPropertyInitializers.has(usage)) {
|
|
385
329
|
const isMethodCall = usage.includes('(');
|
|
@@ -408,9 +352,7 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
408
352
|
context.importsNeeded.add(injectFn);
|
|
409
353
|
}
|
|
410
354
|
}
|
|
411
|
-
// Handle direct usages in method bodies (outside toSignal, not in property initializers)
|
|
412
355
|
for (const [usage] of usagesOutsideToSignal) {
|
|
413
|
-
// Skip if already handled in property initializers
|
|
414
356
|
if (usagesInPropertyInitializers.has(usage))
|
|
415
357
|
continue;
|
|
416
358
|
const isMethodCall = usage.includes('(');
|
|
@@ -434,13 +376,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
434
376
|
type = methodInfo.type;
|
|
435
377
|
needsInjectionContext = methodInfo.needsInjectionContext || false;
|
|
436
378
|
if (needsInjectionContext) {
|
|
437
|
-
// Add to constructor calls
|
|
438
379
|
const injectCall = args ? `${injectFn}${genericType}(${args})` : `${injectFn}()`;
|
|
439
380
|
context.constructorCalls.push(injectCall);
|
|
440
|
-
// Create pattern to match the full statement including semicolon and potential whitespace
|
|
441
|
-
const methodNameOnly = methodName;
|
|
442
|
-
const pattern = new RegExp(`(this\\.)?${escapeRegExp(routerStateServiceVar)}\\s*\\.\\s*${escapeRegExp(methodNameOnly)}(?:<[^>]+>)?\\s*\\([^)]*\\)\\s*;`, 'gs');
|
|
443
|
-
// Replace with empty string (removes the entire statement)
|
|
444
381
|
context.replacements.set(usage + ';', '');
|
|
445
382
|
context.importsNeeded.add(injectFn);
|
|
446
383
|
continue; // Don't create a member for this
|
|
@@ -454,7 +391,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
454
391
|
injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
|
|
455
392
|
type = propertyName.endsWith('$') ? 'observable' : 'signal';
|
|
456
393
|
}
|
|
457
|
-
// For properties used in method bodies, create a shared member
|
|
458
394
|
const baseName = usage.split('.').pop();
|
|
459
395
|
const memberName = findAvailableMemberName(baseName, context.existingMembers);
|
|
460
396
|
const memberInfo = {
|
|
@@ -467,10 +403,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
467
403
|
};
|
|
468
404
|
context.membersToAdd.push(memberInfo);
|
|
469
405
|
context.existingMembers.add(memberName);
|
|
470
|
-
// Replace usage with member reference
|
|
471
|
-
// For signals (non-observable), we need to call them: this.route()
|
|
472
|
-
// For observables, we just reference them: this.route$
|
|
473
|
-
// Check if the original usage has 'this.' prefix
|
|
474
406
|
const hasThisPrefix = usage.includes('this.');
|
|
475
407
|
const baseReplacement = type === 'signal' ? `${memberName}()` : `${memberName}`;
|
|
476
408
|
const replacement = hasThisPrefix ? `this.${baseReplacement}` : baseReplacement;
|
|
@@ -482,20 +414,17 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
482
414
|
function addOrUpdateConstructor(sourceFile, content, classNode, constructorCalls) {
|
|
483
415
|
if (constructorCalls.length === 0)
|
|
484
416
|
return content;
|
|
485
|
-
// Find existing constructor
|
|
486
417
|
const existingConstructor = classNode.members.find((member) => ts.isConstructorDeclaration(member));
|
|
487
418
|
const callStatements = constructorCalls.map((call) => ` ${call};`).join('\n');
|
|
488
419
|
if (existingConstructor) {
|
|
489
|
-
// Add calls to existing constructor
|
|
490
420
|
const constructorBody = existingConstructor.body;
|
|
491
421
|
if (!constructorBody)
|
|
492
422
|
return content;
|
|
493
|
-
const insertPos = constructorBody.getStart(sourceFile) + 1;
|
|
423
|
+
const insertPos = constructorBody.getStart(sourceFile) + 1;
|
|
494
424
|
const indent = '\n ';
|
|
495
425
|
return content.slice(0, insertPos) + indent + callStatements + content.slice(insertPos);
|
|
496
426
|
}
|
|
497
427
|
else {
|
|
498
|
-
// Create new constructor
|
|
499
428
|
const firstMethod = classNode.members.find((member) => ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member));
|
|
500
429
|
const constructorText = `\n\n constructor() {\n${callStatements}\n }`;
|
|
501
430
|
if (firstMethod) {
|
|
@@ -503,18 +432,11 @@ function addOrUpdateConstructor(sourceFile, content, classNode, constructorCalls
|
|
|
503
432
|
return content.slice(0, insertPos) + constructorText + '\n\n ' + content.slice(insertPos);
|
|
504
433
|
}
|
|
505
434
|
else {
|
|
506
|
-
// Insert at end of class, before closing brace
|
|
507
435
|
const classEnd = classNode.getEnd() - 1;
|
|
508
436
|
return content.slice(0, classEnd) + constructorText + '\n' + content.slice(classEnd);
|
|
509
437
|
}
|
|
510
438
|
}
|
|
511
439
|
}
|
|
512
|
-
function generateNameFromArgs(usageKey, args, type) {
|
|
513
|
-
const baseName = usageKey.split('(')[0].replace(/^\w/, (c) => c.toUpperCase());
|
|
514
|
-
const cleanArgs = args.replace(/['"]/g, '').replace(/[^\w]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
|
|
515
|
-
const name = `${baseName.charAt(0).toLowerCase()}${baseName.slice(1)}${cleanArgs.charAt(0).toUpperCase()}${cleanArgs.slice(1)}`;
|
|
516
|
-
return type === 'observable' ? `${name}$` : name;
|
|
517
|
-
}
|
|
518
440
|
function findAvailableMemberName(baseName, existingMembers) {
|
|
519
441
|
let name = baseName;
|
|
520
442
|
let counter = 1;
|
|
@@ -524,23 +446,7 @@ function findAvailableMemberName(baseName, existingMembers) {
|
|
|
524
446
|
}
|
|
525
447
|
return name;
|
|
526
448
|
}
|
|
527
|
-
function
|
|
528
|
-
// Find all occurrences outside property initializers
|
|
529
|
-
classNode.members.forEach((member) => {
|
|
530
|
-
if (ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member)) {
|
|
531
|
-
// Search in method bodies
|
|
532
|
-
const searchPattern = args
|
|
533
|
-
? `${routerStateServiceVar}.${usageKey.split('(')[0]}(${args})`
|
|
534
|
-
: `${routerStateServiceVar}.${usageKey}`;
|
|
535
|
-
const memberText = member.getText(sourceFile);
|
|
536
|
-
if (memberText.includes(searchPattern)) {
|
|
537
|
-
context.replacements.set(searchPattern, `this.${memberName}`);
|
|
538
|
-
context.replacements.set(`this.${searchPattern}`, `this.${memberName}`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl) {
|
|
449
|
+
function checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl) {
|
|
544
450
|
let usedElsewhere = false;
|
|
545
451
|
function visit(node) {
|
|
546
452
|
if (node === propertyDecl)
|
|
@@ -562,15 +468,13 @@ function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, pro
|
|
|
562
468
|
function escapeRegExp(string) {
|
|
563
469
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
564
470
|
}
|
|
565
|
-
function addMembersToClass(
|
|
566
|
-
// Find the RouterStateService property to insert after
|
|
471
|
+
function addMembersToClass(content, classNode, members, routerStateServiceVar) {
|
|
567
472
|
const routerStateProperty = classNode.members.find((member) => ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === routerStateServiceVar);
|
|
568
473
|
if (!routerStateProperty) {
|
|
569
|
-
|
|
474
|
+
logger.warn('Could not find RouterStateService property to insert new members after');
|
|
570
475
|
return content;
|
|
571
476
|
}
|
|
572
477
|
const insertPosition = routerStateProperty.getEnd();
|
|
573
|
-
// Generate member declarations
|
|
574
478
|
const memberDeclarations = members.map((member) => {
|
|
575
479
|
const injectCall = member.args ? `${member.injectFn}(${member.args})` : `${member.injectFn}()`;
|
|
576
480
|
const value = member.type === 'observable' ? `toObservable(${injectCall})` : injectCall;
|
|
@@ -578,11 +482,10 @@ function addMembersToClass(sourceFile, content, classNode, members, routerStateS
|
|
|
578
482
|
});
|
|
579
483
|
return content.slice(0, insertPosition) + memberDeclarations.join('') + content.slice(insertPosition);
|
|
580
484
|
}
|
|
581
|
-
function handleInlineInjectPatterns(sourceFile, content) {
|
|
485
|
+
function handleInlineInjectPatterns(sourceFile, content, serviceName) {
|
|
582
486
|
const importsNeeded = new Set();
|
|
583
|
-
|
|
487
|
+
const edits = [];
|
|
584
488
|
const { signalPropertyMap, observablePropertyMap } = getPropertyMaps();
|
|
585
|
-
// Method map
|
|
586
489
|
const methodMap = {
|
|
587
490
|
selectQueryParam: { injectFn: 'injectQueryParam', type: 'observable', requiresArgs: true },
|
|
588
491
|
selectPathParam: { injectFn: 'injectPathParam', type: 'observable', requiresArgs: true },
|
|
@@ -594,7 +497,6 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
594
497
|
needsInjectionContext: true,
|
|
595
498
|
},
|
|
596
499
|
};
|
|
597
|
-
// Track which nodes are already inside toSignal to avoid double processing
|
|
598
500
|
const processedNodes = new Set();
|
|
599
501
|
function isInsideToSignal(node) {
|
|
600
502
|
let current = node.parent;
|
|
@@ -609,7 +511,6 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
609
511
|
return false;
|
|
610
512
|
}
|
|
611
513
|
function visitNode(node) {
|
|
612
|
-
// Look for: toSignal(inject(RouterStateService).property)
|
|
613
514
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
|
|
614
515
|
const arg = node.arguments[0];
|
|
615
516
|
if (arg && ts.isPropertyAccessExpression(arg)) {
|
|
@@ -619,20 +520,17 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
619
520
|
arg.expression.expression.text === 'inject' &&
|
|
620
521
|
arg.expression.arguments.length > 0) {
|
|
621
522
|
const injectArg = arg.expression.arguments[0];
|
|
622
|
-
if (ts.isIdentifier(injectArg) && injectArg.text ===
|
|
523
|
+
if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
|
|
623
524
|
const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
|
|
624
525
|
if (injectFn) {
|
|
625
|
-
const oldText = node.getText(sourceFile);
|
|
626
526
|
const newText = `${injectFn}()`;
|
|
627
|
-
|
|
527
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
|
|
628
528
|
importsNeeded.add(injectFn);
|
|
629
|
-
// Mark this property access as processed
|
|
630
529
|
processedNodes.add(arg);
|
|
631
530
|
}
|
|
632
531
|
}
|
|
633
532
|
}
|
|
634
533
|
}
|
|
635
|
-
// NEW: Look for: toSignal(inject(RouterStateService).methodCall(...))
|
|
636
534
|
if (arg && ts.isCallExpression(arg) && ts.isPropertyAccessExpression(arg.expression)) {
|
|
637
535
|
const methodName = arg.expression.name.text;
|
|
638
536
|
const methodInfo = methodMap[methodName];
|
|
@@ -642,28 +540,21 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
642
540
|
arg.expression.expression.expression.text === 'inject' &&
|
|
643
541
|
arg.expression.expression.arguments.length > 0) {
|
|
644
542
|
const injectArg = arg.expression.expression.arguments[0];
|
|
645
|
-
if (ts.isIdentifier(injectArg) && injectArg.text ===
|
|
646
|
-
const oldText = node.getText(sourceFile);
|
|
647
|
-
// Extract generic type and args
|
|
543
|
+
if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
|
|
648
544
|
const innerCallText = arg.getText(sourceFile);
|
|
649
545
|
const genericMatch = innerCallText.match(new RegExp(`${methodName}<([^>]+)>`));
|
|
650
546
|
const genericType = genericMatch ? `<${genericMatch[1]}>` : '';
|
|
651
|
-
// Get the arguments from the call
|
|
652
547
|
const args = arg.arguments.map((a) => a.getText(sourceFile)).join(', ');
|
|
653
548
|
const injectCall = `${methodInfo.injectFn}${genericType}(${args})`;
|
|
654
|
-
// Since it's wrapped in toSignal and returns a signal, no need for toObservable
|
|
655
549
|
const newText = injectCall;
|
|
656
|
-
|
|
550
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
|
|
657
551
|
importsNeeded.add(methodInfo.injectFn);
|
|
658
|
-
// Mark this call as processed
|
|
659
552
|
processedNodes.add(arg);
|
|
660
553
|
}
|
|
661
554
|
}
|
|
662
555
|
}
|
|
663
556
|
}
|
|
664
|
-
// Look for: inject(RouterStateService).selectQueryParam() or other method calls (NOT inside toSignal)
|
|
665
557
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
666
|
-
// Skip if already processed by toSignal handler
|
|
667
558
|
if (processedNodes.has(node)) {
|
|
668
559
|
ts.forEachChild(node, visitNode);
|
|
669
560
|
return;
|
|
@@ -676,21 +567,18 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
676
567
|
node.expression.expression.expression.text === 'inject' &&
|
|
677
568
|
node.expression.expression.arguments.length > 0) {
|
|
678
569
|
const injectArg = node.expression.expression.arguments[0];
|
|
679
|
-
if (ts.isIdentifier(injectArg) && injectArg.text ===
|
|
680
|
-
// Check if inside toSignal
|
|
570
|
+
if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
|
|
681
571
|
if (isInsideToSignal(node)) {
|
|
682
572
|
ts.forEachChild(node, visitNode);
|
|
683
573
|
return;
|
|
684
574
|
}
|
|
685
575
|
const oldText = node.getText(sourceFile);
|
|
686
|
-
// Extract generic type and args
|
|
687
576
|
const genericMatch = oldText.match(new RegExp(`${methodName}<([^>]+)>`));
|
|
688
577
|
const genericType = genericMatch ? `<${genericMatch[1]}>` : '';
|
|
689
|
-
// Get the arguments from the call
|
|
690
578
|
const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
|
|
691
579
|
const injectCall = `${methodInfo.injectFn}${genericType}(${args})`;
|
|
692
580
|
const newText = methodInfo.type === 'observable' ? `toObservable(${injectCall})` : injectCall;
|
|
693
|
-
|
|
581
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
|
|
694
582
|
importsNeeded.add(methodInfo.injectFn);
|
|
695
583
|
if (methodInfo.type === 'observable') {
|
|
696
584
|
importsNeeded.add('toObservable');
|
|
@@ -698,10 +586,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
698
586
|
}
|
|
699
587
|
}
|
|
700
588
|
}
|
|
701
|
-
// Look for: inject(RouterStateService).property (not wrapped in toSignal)
|
|
702
|
-
// This handles: inject(RouterStateService).pathParams$.pipe(...)
|
|
703
589
|
if (ts.isPropertyAccessExpression(node)) {
|
|
704
|
-
// Skip if already processed by toSignal handler
|
|
705
590
|
if (processedNodes.has(node)) {
|
|
706
591
|
ts.forEachChild(node, visitNode);
|
|
707
592
|
return;
|
|
@@ -712,21 +597,18 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
712
597
|
node.expression.expression.text === 'inject' &&
|
|
713
598
|
node.expression.arguments.length > 0) {
|
|
714
599
|
const injectArg = node.expression.arguments[0];
|
|
715
|
-
if (ts.isIdentifier(injectArg) && injectArg.text ===
|
|
600
|
+
if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
|
|
716
601
|
const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
|
|
717
602
|
if (injectFn) {
|
|
718
|
-
// Check if this is inside a toSignal wrapper
|
|
719
603
|
const insideToSignal = isInsideToSignal(node);
|
|
720
|
-
// Skip if inside toSignal - it will be handled by the toSignal pattern above
|
|
721
604
|
if (insideToSignal) {
|
|
722
605
|
ts.forEachChild(node, visitNode);
|
|
723
606
|
return;
|
|
724
607
|
}
|
|
725
|
-
const oldText = node.getText(sourceFile);
|
|
726
608
|
const type = observablePropertyMap[propertyName] ? 'observable' : 'signal';
|
|
727
609
|
const injectCall = `${injectFn}()`;
|
|
728
610
|
const newText = type === 'observable' ? `toObservable(${injectCall})` : injectCall;
|
|
729
|
-
|
|
611
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
|
|
730
612
|
importsNeeded.add(injectFn);
|
|
731
613
|
if (type === 'observable') {
|
|
732
614
|
importsNeeded.add('toObservable');
|
|
@@ -738,11 +620,10 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
738
620
|
ts.forEachChild(node, visitNode);
|
|
739
621
|
}
|
|
740
622
|
sourceFile.forEachChild(visitNode);
|
|
741
|
-
return { content:
|
|
623
|
+
return { content: applyInlineEdits(content, edits), importsNeeded };
|
|
742
624
|
}
|
|
743
625
|
function addImportsToPackage(sourceFile, content, imports, packageName) {
|
|
744
626
|
const importsList = Array.from(imports).sort();
|
|
745
|
-
// Check if import already exists
|
|
746
627
|
let existingImport;
|
|
747
628
|
sourceFile.forEachChild((node) => {
|
|
748
629
|
if (ts.isImportDeclaration(node) &&
|
|
@@ -761,10 +642,9 @@ function addImportsToPackage(sourceFile, content, imports, packageName) {
|
|
|
761
642
|
const allImports = [...existingImports, ...newImports].sort();
|
|
762
643
|
const newImportText = `import { ${allImports.join(', ')} } from '${packageName}';`;
|
|
763
644
|
const oldImportText = existingImport.getText(sourceFile);
|
|
764
|
-
return content.replace(oldImportText, newImportText);
|
|
645
|
+
return content.replace(oldImportText, () => newImportText);
|
|
765
646
|
}
|
|
766
647
|
else {
|
|
767
|
-
// Add new import at the top
|
|
768
648
|
const newImportText = `import { ${importsList.join(', ')} } from '${packageName}';\n`;
|
|
769
649
|
const firstImport = sourceFile.statements.find((stmt) => ts.isImportDeclaration(stmt));
|
|
770
650
|
if (firstImport) {
|
|
@@ -776,78 +656,54 @@ function addImportsToPackage(sourceFile, content, imports, packageName) {
|
|
|
776
656
|
}
|
|
777
657
|
}
|
|
778
658
|
}
|
|
779
|
-
function removeRouterStateServiceInjection(sourceFile, content,
|
|
659
|
+
function removeRouterStateServiceInjection(sourceFile, content, serviceName, filePath) {
|
|
780
660
|
let updatedContent = content;
|
|
781
661
|
const modifications = [];
|
|
782
|
-
sourceFile.
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
member.initializer.expression.text === 'inject' &&
|
|
792
|
-
member.initializer.arguments.some((argument) => ts.isIdentifier(argument) && argument.text === 'RouterStateService');
|
|
793
|
-
const hasRouterStateType = member.type &&
|
|
794
|
-
ts.isTypeReferenceNode(member.type) &&
|
|
795
|
-
ts.isIdentifier(member.type.typeName) &&
|
|
796
|
-
member.type.typeName.text === 'RouterStateService';
|
|
797
|
-
if (routerStateServiceVars.includes(memberName) && (injectsRouterState || hasRouterStateType)) {
|
|
798
|
-
const memberStart = member.getStart(sourceFile, true);
|
|
799
|
-
const memberEnd = member.getEnd();
|
|
800
|
-
let lineStart = memberStart;
|
|
801
|
-
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
|
|
802
|
-
lineStart--;
|
|
803
|
-
}
|
|
804
|
-
let lineEnd = memberEnd;
|
|
805
|
-
while (lineEnd < content.length && content[lineEnd] !== '\n') {
|
|
806
|
-
lineEnd++;
|
|
807
|
-
}
|
|
808
|
-
if (content[lineEnd] === '\n') {
|
|
809
|
-
lineEnd++;
|
|
810
|
-
}
|
|
811
|
-
modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
|
|
662
|
+
collectClasses(sourceFile).forEach((node) => {
|
|
663
|
+
node.members.forEach((member) => {
|
|
664
|
+
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
665
|
+
if (isServiceProperty(member, serviceName)) {
|
|
666
|
+
const memberStart = member.getStart(sourceFile, true);
|
|
667
|
+
const memberEnd = member.getEnd();
|
|
668
|
+
let lineStart = memberStart;
|
|
669
|
+
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
|
|
670
|
+
lineStart--;
|
|
812
671
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
member.parameters.forEach((param) => {
|
|
820
|
-
if (ts.isIdentifier(param.name)) {
|
|
821
|
-
const paramName = param.name.text;
|
|
822
|
-
const hasRouterStateType = param.type &&
|
|
823
|
-
ts.isTypeReferenceNode(param.type) &&
|
|
824
|
-
ts.isIdentifier(param.type.typeName) &&
|
|
825
|
-
param.type.typeName.text === 'RouterStateService';
|
|
826
|
-
if (routerStateServiceVars.includes(paramName) && hasRouterStateType) {
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
newParams.push(param.getText(sourceFile));
|
|
831
|
-
});
|
|
832
|
-
const constructorText = member.getText(sourceFile);
|
|
833
|
-
const openParenIndex = constructorText.indexOf('(');
|
|
834
|
-
const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
|
|
835
|
-
if (openParenIndex === -1 || closeParenIndex === -1) {
|
|
836
|
-
console.warn(`Could not find constructor parameters in ${filePath}`);
|
|
837
|
-
return;
|
|
672
|
+
let lineEnd = memberEnd;
|
|
673
|
+
while (lineEnd < content.length && content[lineEnd] !== '\n') {
|
|
674
|
+
lineEnd++;
|
|
675
|
+
}
|
|
676
|
+
if (content[lineEnd] === '\n') {
|
|
677
|
+
lineEnd++;
|
|
838
678
|
}
|
|
839
|
-
|
|
840
|
-
const paramListStart = constructorStart + openParenIndex + 1;
|
|
841
|
-
const paramListEnd = constructorStart + closeParenIndex;
|
|
842
|
-
const newParamsText = newParams.join(', ');
|
|
843
|
-
modifications.push({
|
|
844
|
-
start: paramListStart,
|
|
845
|
-
end: paramListEnd,
|
|
846
|
-
replacement: newParamsText,
|
|
847
|
-
});
|
|
679
|
+
modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
|
|
848
680
|
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
node.members.forEach((member) => {
|
|
684
|
+
if (ts.isConstructorDeclaration(member) &&
|
|
685
|
+
member.parameters.some((param) => isServiceParameter(param, serviceName))) {
|
|
686
|
+
const newParams = member.parameters
|
|
687
|
+
.filter((param) => !isServiceParameter(param, serviceName))
|
|
688
|
+
.map((param) => param.getText(sourceFile));
|
|
689
|
+
const constructorText = member.getText(sourceFile);
|
|
690
|
+
const openParenIndex = constructorText.indexOf('(');
|
|
691
|
+
const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
|
|
692
|
+
if (openParenIndex === -1 || closeParenIndex === -1) {
|
|
693
|
+
logger.warn(`Could not find constructor parameters in ${filePath}`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const constructorStart = member.getStart(sourceFile);
|
|
697
|
+
const paramListStart = constructorStart + openParenIndex + 1;
|
|
698
|
+
const paramListEnd = constructorStart + closeParenIndex;
|
|
699
|
+
const newParamsText = newParams.join(', ');
|
|
700
|
+
modifications.push({
|
|
701
|
+
start: paramListStart,
|
|
702
|
+
end: paramListEnd,
|
|
703
|
+
replacement: newParamsText,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
});
|
|
851
707
|
});
|
|
852
708
|
modifications.sort((a, b) => b.start - a.start);
|
|
853
709
|
for (const mod of modifications) {
|
|
@@ -869,70 +725,8 @@ function findMatchingParen(text, openIndex) {
|
|
|
869
725
|
}
|
|
870
726
|
return depth === 0 ? i - 1 : -1;
|
|
871
727
|
}
|
|
872
|
-
function removeRouterStateServiceImport(sourceFile, content) {
|
|
873
|
-
let updatedContent = content;
|
|
874
|
-
sourceFile.forEachChild((node) => {
|
|
875
|
-
if (ts.isImportDeclaration(node)) {
|
|
876
|
-
const moduleSpecifier = node.moduleSpecifier;
|
|
877
|
-
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
878
|
-
if (!node.importClause?.namedBindings)
|
|
879
|
-
return;
|
|
880
|
-
if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
881
|
-
const imports = node.importClause.namedBindings.elements;
|
|
882
|
-
const routerStateServiceImport = imports.find((imp) => ts.isImportSpecifier(imp) && imp.name.text === 'RouterStateService');
|
|
883
|
-
if (!routerStateServiceImport)
|
|
884
|
-
return;
|
|
885
|
-
if (imports.length === 1) {
|
|
886
|
-
const importStart = node.getStart(sourceFile);
|
|
887
|
-
let lineEnd = node.getEnd();
|
|
888
|
-
while (lineEnd < content.length && content[lineEnd] !== '\n') {
|
|
889
|
-
lineEnd++;
|
|
890
|
-
}
|
|
891
|
-
if (content[lineEnd] === '\n') {
|
|
892
|
-
lineEnd++;
|
|
893
|
-
}
|
|
894
|
-
updatedContent = content.slice(0, importStart) + content.slice(lineEnd);
|
|
895
|
-
}
|
|
896
|
-
else {
|
|
897
|
-
const namedImportsText = node.importClause.namedBindings.getText(sourceFile);
|
|
898
|
-
const patterns = [
|
|
899
|
-
new RegExp(`RouterStateService,\\s*`, 'g'),
|
|
900
|
-
new RegExp(`,\\s*RouterStateService`, 'g'),
|
|
901
|
-
new RegExp(`\\s*RouterStateService\\s*`, 'g'),
|
|
902
|
-
];
|
|
903
|
-
let newNamedImports = namedImportsText;
|
|
904
|
-
for (const pattern of patterns) {
|
|
905
|
-
const temp = newNamedImports.replace(pattern, '');
|
|
906
|
-
if (temp !== newNamedImports) {
|
|
907
|
-
newNamedImports = temp;
|
|
908
|
-
break;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
newNamedImports = newNamedImports.replace(/,\s*,/g, ',').replace(/{\s*,/g, '{').replace(/,\s*}/g, '}');
|
|
912
|
-
updatedContent = updatedContent.replace(namedImportsText, newNamedImports);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
});
|
|
918
|
-
return updatedContent;
|
|
919
|
-
}
|
|
920
|
-
function checkIfRouterStateServiceStillUsed(sourceFile, routerStateServiceVars) {
|
|
921
|
-
let stillUsed = false;
|
|
922
|
-
function visit(node) {
|
|
923
|
-
if (ts.isIdentifier(node) && routerStateServiceVars.includes(node.text)) {
|
|
924
|
-
stillUsed = true;
|
|
925
|
-
}
|
|
926
|
-
if (!stillUsed) {
|
|
927
|
-
ts.forEachChild(node, visit);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
sourceFile.forEachChild(visit);
|
|
931
|
-
return stillUsed;
|
|
932
|
-
}
|
|
933
728
|
function removeUnusedImports(sourceFile, content) {
|
|
934
729
|
let updatedContent = content;
|
|
935
|
-
// Check if toSignal is still used
|
|
936
730
|
const hasToSignal = content.includes('toSignal(');
|
|
937
731
|
if (!hasToSignal) {
|
|
938
732
|
sourceFile.forEachChild((node) => {
|
|
@@ -959,54 +753,31 @@ function removeUnusedImports(sourceFile, content) {
|
|
|
959
753
|
}
|
|
960
754
|
return updatedContent;
|
|
961
755
|
}
|
|
962
|
-
export default async function migrateRouterStateService(tree) {
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
function findFiles(dir) {
|
|
966
|
-
const children = tree.children(dir);
|
|
967
|
-
for (const child of children) {
|
|
968
|
-
const path = dir === '.' ? child : `${dir}/${child}`;
|
|
969
|
-
if (tree.isFile(path)) {
|
|
970
|
-
if (path.endsWith('.ts') && !path.includes('node_modules')) {
|
|
971
|
-
tsFiles.push(path);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
else {
|
|
975
|
-
if (child === 'node_modules')
|
|
976
|
-
continue;
|
|
977
|
-
findFiles(path);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
findFiles('.');
|
|
756
|
+
export default async function migrateRouterStateService(tree, scope) {
|
|
757
|
+
const tsFiles = collectFiles(tree, scope, ['.ts']);
|
|
758
|
+
const review = [];
|
|
982
759
|
let filesModified = 0;
|
|
983
|
-
let routerStateServiceUsed = false;
|
|
984
760
|
for (const filePath of tsFiles) {
|
|
985
761
|
const content = tree.read(filePath, 'utf-8');
|
|
986
762
|
if (!content)
|
|
987
763
|
continue;
|
|
988
764
|
if (!content.includes('RouterStateService'))
|
|
989
765
|
continue;
|
|
990
|
-
console.log(`Processing: ${filePath}`);
|
|
991
766
|
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
statement.moduleSpecifier.text.startsWith('@ethlete/') &&
|
|
995
|
-
!!statement.importClause?.namedBindings &&
|
|
996
|
-
ts.isNamedImports(statement.importClause.namedBindings) &&
|
|
997
|
-
statement.importClause.namedBindings.elements.some((element) => (element.propertyName?.text ?? element.name.text) === 'RouterStateService'));
|
|
998
|
-
if (!hasEthleteImport)
|
|
767
|
+
const serviceImport = getServiceImport(sourceFile, 'RouterStateService');
|
|
768
|
+
if (!serviceImport?.packageName.startsWith('@ethlete/'))
|
|
999
769
|
continue;
|
|
1000
|
-
const
|
|
770
|
+
const serviceName = serviceImport.localName;
|
|
771
|
+
const inlineResult = handleInlineInjectPatterns(sourceFile, content, serviceName);
|
|
1001
772
|
let updatedContent = inlineResult.content;
|
|
1002
773
|
const updatedSourceFile = updatedContent !== content
|
|
1003
774
|
? ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true)
|
|
1004
775
|
: sourceFile;
|
|
1005
|
-
const routerStateServiceVars = findRouterStateServiceVariables(updatedSourceFile);
|
|
776
|
+
const routerStateServiceVars = findRouterStateServiceVariables(updatedSourceFile, serviceName);
|
|
777
|
+
const serviceClasses = findServiceClasses(updatedSourceFile, serviceName);
|
|
1006
778
|
if (routerStateServiceVars.length === 0 && inlineResult.importsNeeded.size === 0) {
|
|
1007
779
|
continue;
|
|
1008
780
|
}
|
|
1009
|
-
routerStateServiceUsed = true;
|
|
1010
781
|
const allImportsNeeded = {
|
|
1011
782
|
'@ethlete/core': new Set(),
|
|
1012
783
|
'@angular/core/rxjs-interop': new Set(),
|
|
@@ -1019,42 +790,34 @@ export default async function migrateRouterStateService(tree) {
|
|
|
1019
790
|
allImportsNeeded['@ethlete/core'].add(importName);
|
|
1020
791
|
}
|
|
1021
792
|
}
|
|
1022
|
-
for (const
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
793
|
+
for (const { index, fields } of serviceClasses) {
|
|
794
|
+
for (const routerStateServiceVar of fields) {
|
|
795
|
+
const currentSourceFile = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
796
|
+
const classNode = collectClasses(currentSourceFile)[index];
|
|
797
|
+
const context = analyzeClassMigration(currentSourceFile, classNode, routerStateServiceVar);
|
|
798
|
+
context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
|
|
799
|
+
context.membersToAdd.forEach((member) => {
|
|
800
|
+
allImportsNeeded['@ethlete/core'].add(member.injectFn);
|
|
801
|
+
if (member.type === 'observable' && !member.wrappedInToSignal) {
|
|
802
|
+
allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
for (const replacement of context.replacements.values()) {
|
|
806
|
+
if (replacement.includes('toObservable(')) {
|
|
807
|
+
allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
|
|
808
|
+
}
|
|
809
|
+
if (replacement.includes('toSignal(')) {
|
|
810
|
+
allImportsNeeded['@angular/core/rxjs-interop'].add('toSignal');
|
|
811
|
+
}
|
|
1040
812
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
if (context.membersToAdd.length > 0) {
|
|
1047
|
-
const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
1048
|
-
const classNodeUpdated = findClassForRouterStateService(sourceFileUpdated, routerStateServiceVar);
|
|
1049
|
-
if (classNodeUpdated) {
|
|
1050
|
-
updatedContent = addMembersToClass(sourceFileUpdated, updatedContent, classNodeUpdated, context.membersToAdd, routerStateServiceVar);
|
|
813
|
+
updatedContent = applyReplacementsInClass(updatedContent, currentSourceFile, classNode, context.replacements);
|
|
814
|
+
if (context.membersToAdd.length > 0) {
|
|
815
|
+
const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
816
|
+
updatedContent = addMembersToClass(updatedContent, collectClasses(sourceFileUpdated)[index], context.membersToAdd, routerStateServiceVar);
|
|
1051
817
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const classNodeUpdated = findClassForRouterStateService(sourceFileUpdated, routerStateServiceVar);
|
|
1056
|
-
if (classNodeUpdated) {
|
|
1057
|
-
updatedContent = addOrUpdateConstructor(sourceFileUpdated, updatedContent, classNodeUpdated, context.constructorCalls);
|
|
818
|
+
if (context.constructorCalls.length > 0) {
|
|
819
|
+
const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
820
|
+
updatedContent = addOrUpdateConstructor(sourceFileUpdated, updatedContent, collectClasses(sourceFileUpdated)[index], context.constructorCalls);
|
|
1058
821
|
}
|
|
1059
822
|
}
|
|
1060
823
|
}
|
|
@@ -1065,10 +828,13 @@ export default async function migrateRouterStateService(tree) {
|
|
|
1065
828
|
}
|
|
1066
829
|
}
|
|
1067
830
|
const sourceFileFinal = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
1068
|
-
updatedContent = removeRouterStateServiceInjection(sourceFileFinal, updatedContent,
|
|
831
|
+
updatedContent = removeRouterStateServiceInjection(sourceFileFinal, updatedContent, serviceName, filePath);
|
|
1069
832
|
const sourceFileAfterRemoval = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
833
|
+
if (isServiceStillUsed(sourceFileAfterRemoval, serviceName, serviceClasses)) {
|
|
834
|
+
review.push(`${filePath}: RouterStateService is still used. Migrate the remaining usages manually.`);
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
updatedContent = removeServiceImport(sourceFileAfterRemoval, updatedContent, 'RouterStateService');
|
|
1072
838
|
}
|
|
1073
839
|
const sourceFileAfterCleanup = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
1074
840
|
updatedContent = removeUnusedImports(sourceFileAfterCleanup, updatedContent);
|
|
@@ -1077,13 +843,15 @@ export default async function migrateRouterStateService(tree) {
|
|
|
1077
843
|
filesModified++;
|
|
1078
844
|
}
|
|
1079
845
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
846
|
+
return { filesChanged: filesModified, review };
|
|
847
|
+
}
|
|
848
|
+
function applyInlineEdits(content, edits) {
|
|
849
|
+
const accepted = [];
|
|
850
|
+
for (const edit of [...edits].sort((a, b) => a.start - b.start || b.end - a.end)) {
|
|
851
|
+
const last = accepted.at(-1);
|
|
852
|
+
if (last && edit.start < last.end)
|
|
853
|
+
continue;
|
|
854
|
+
accepted.push(edit);
|
|
1088
855
|
}
|
|
856
|
+
return accepted.reduceRight((result, edit) => result.slice(0, edit.start) + edit.text + result.slice(edit.end), content);
|
|
1089
857
|
}
|