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