@ethlete/core 4.31.0 → 5.0.0-next.0

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.
@@ -0,0 +1,1064 @@
1
+ import * as ts from 'typescript';
2
+ function getPropertyMaps() {
3
+ // Property map for signals (getters for latest value)
4
+ const signalPropertyMap = {
5
+ route: 'injectRoute',
6
+ state: 'injectRouterState',
7
+ data: 'injectRouteData',
8
+ pathParams: 'injectPathParams',
9
+ queryParams: 'injectQueryParams',
10
+ title: 'injectRouteTitle',
11
+ fragment: 'injectFragment',
12
+ latestEvent: 'injectRouterEvent',
13
+ };
14
+ // Property map for observables
15
+ const observablePropertyMap = {
16
+ route$: 'injectRoute',
17
+ state$: 'injectRouterState',
18
+ data$: 'injectRouteData',
19
+ pathParams$: 'injectPathParams',
20
+ queryParams$: 'injectQueryParams',
21
+ title$: 'injectRouteTitle',
22
+ fragment$: 'injectFragment',
23
+ queryParamChanges$: 'injectQueryParamChanges',
24
+ pathParamChanges$: 'injectPathParamChanges',
25
+ };
26
+ return { signalPropertyMap, observablePropertyMap };
27
+ }
28
+ function findRouterStateServiceVariables(sourceFile) {
29
+ const variables = [];
30
+ function visit(node) {
31
+ // Check for inject(RouterStateService) pattern
32
+ if (ts.isCallExpression(node) &&
33
+ ts.isIdentifier(node.expression) &&
34
+ node.expression.text === 'inject' &&
35
+ node.arguments.length > 0) {
36
+ const arg = node.arguments[0];
37
+ if (ts.isIdentifier(arg) && arg.text === 'RouterStateService') {
38
+ // Find the variable name
39
+ let parent = node.parent;
40
+ while (parent) {
41
+ if (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name)) {
42
+ variables.push(parent.name.text);
43
+ break;
44
+ }
45
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
46
+ variables.push(parent.name.text);
47
+ break;
48
+ }
49
+ parent = parent.parent;
50
+ }
51
+ }
52
+ }
53
+ // Check for constructor parameter injection
54
+ if (ts.isParameter(node)) {
55
+ const typeNode = node.type;
56
+ if (typeNode && ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
57
+ if (typeNode.typeName.text === 'RouterStateService' && node.name && ts.isIdentifier(node.name)) {
58
+ variables.push(node.name.text);
59
+ }
60
+ }
61
+ }
62
+ ts.forEachChild(node, visit);
63
+ }
64
+ visit(sourceFile);
65
+ return variables;
66
+ }
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
+ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
91
+ const context = {
92
+ routerStateServiceVar,
93
+ existingMembers: new Set(),
94
+ membersToAdd: [],
95
+ replacements: new Map(),
96
+ importsNeeded: new Set(),
97
+ constructorCalls: [], // Initialize
98
+ };
99
+ // Collect existing member names and detect property initializers
100
+ const propertyInitializers = new Map();
101
+ classNode.members.forEach((member) => {
102
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
103
+ const memberName = member.name.text;
104
+ context.existingMembers.add(memberName);
105
+ // Check if this property has an initializer that uses RouterStateService
106
+ if (member.initializer) {
107
+ const initializerText = member.initializer.getText(sourceFile);
108
+ if (initializerText.includes(routerStateServiceVar)) {
109
+ propertyInitializers.set(memberName, member);
110
+ }
111
+ }
112
+ }
113
+ else if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
114
+ context.existingMembers.add(member.name.text);
115
+ }
116
+ });
117
+ const { signalPropertyMap, observablePropertyMap } = getPropertyMaps();
118
+ // Method map
119
+ const methodMap = {
120
+ selectQueryParam: { injectFn: 'injectQueryParam', type: 'observable', requiresArgs: true },
121
+ selectPathParam: { injectFn: 'injectPathParam', type: 'observable', requiresArgs: true },
122
+ selectData: { injectFn: 'injectRouteDataItem', type: 'observable', requiresArgs: true },
123
+ enableScrollEnhancements: {
124
+ injectFn: 'setupScrollRestoration',
125
+ type: 'signal',
126
+ requiresArgs: false,
127
+ needsInjectionContext: true,
128
+ },
129
+ };
130
+ // Track usages
131
+ const usagesWrappedInToSignal = new Map();
132
+ const usagesOutsideToSignal = new Map();
133
+ const usagesInPropertyInitializers = new Map();
134
+ // First pass: detect toSignal usages, property initializers, and count all usages
135
+ function detectUsages(node, insideToSignal = false, currentProperty) {
136
+ // Handle toSignal wrapper
137
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
138
+ const arg = node.arguments[0];
139
+ if (arg) {
140
+ detectUsages(arg, true, currentProperty);
141
+ }
142
+ return;
143
+ }
144
+ // Track RouterStateService property access
145
+ if (ts.isPropertyAccessExpression(node)) {
146
+ const propertyName = node.name.text;
147
+ const isRouterStateAccess = (ts.isIdentifier(node.expression) && node.expression.text === routerStateServiceVar) ||
148
+ (ts.isPropertyAccessExpression(node.expression) &&
149
+ node.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
150
+ node.expression.name.text === routerStateServiceVar);
151
+ if (isRouterStateAccess && (signalPropertyMap[propertyName] || observablePropertyMap[propertyName])) {
152
+ const fullAccess = node.getText(sourceFile);
153
+ if (currentProperty) {
154
+ const propertyDecl = propertyInitializers.get(currentProperty);
155
+ if (propertyDecl) {
156
+ usagesInPropertyInitializers.set(fullAccess, {
157
+ propertyName: currentProperty,
158
+ usage: fullAccess,
159
+ propertyDecl,
160
+ });
161
+ // Also track if it's wrapped in toSignal
162
+ if (insideToSignal) {
163
+ usagesWrappedInToSignal.set(fullAccess, (usagesWrappedInToSignal.get(fullAccess) || 0) + 1);
164
+ }
165
+ }
166
+ }
167
+ else if (insideToSignal) {
168
+ usagesWrappedInToSignal.set(fullAccess, (usagesWrappedInToSignal.get(fullAccess) || 0) + 1);
169
+ }
170
+ else {
171
+ usagesOutsideToSignal.set(fullAccess, (usagesOutsideToSignal.get(fullAccess) || 0) + 1);
172
+ }
173
+ }
174
+ }
175
+ // Track method calls
176
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
177
+ const methodName = node.expression.name.text;
178
+ if (methodName !== 'pipe' && methodName !== 'subscribe') {
179
+ const isRouterStateAccess = (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === routerStateServiceVar) ||
180
+ (ts.isPropertyAccessExpression(node.expression.expression) &&
181
+ node.expression.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
182
+ node.expression.expression.name.text === routerStateServiceVar);
183
+ if (isRouterStateAccess && methodMap[methodName]) {
184
+ const fullCall = node.getText(sourceFile);
185
+ if (currentProperty) {
186
+ const propertyDecl = propertyInitializers.get(currentProperty);
187
+ if (propertyDecl) {
188
+ usagesInPropertyInitializers.set(fullCall, {
189
+ propertyName: currentProperty,
190
+ usage: fullCall,
191
+ propertyDecl,
192
+ });
193
+ // Also track if it's wrapped in toSignal
194
+ if (insideToSignal) {
195
+ usagesWrappedInToSignal.set(fullCall, (usagesWrappedInToSignal.get(fullCall) || 0) + 1);
196
+ }
197
+ }
198
+ }
199
+ else if (insideToSignal) {
200
+ usagesWrappedInToSignal.set(fullCall, (usagesWrappedInToSignal.get(fullCall) || 0) + 1);
201
+ }
202
+ else {
203
+ usagesOutsideToSignal.set(fullCall, (usagesOutsideToSignal.get(fullCall) || 0) + 1);
204
+ }
205
+ }
206
+ }
207
+ }
208
+ ts.forEachChild(node, (child) => detectUsages(child, insideToSignal, currentProperty));
209
+ }
210
+ // Detect usages in property initializers
211
+ classNode.members.forEach((member) => {
212
+ if (ts.isPropertyDeclaration(member) && member.initializer && ts.isIdentifier(member.name)) {
213
+ const propertyName = member.name.text;
214
+ if (propertyName !== routerStateServiceVar) {
215
+ detectUsages(member.initializer, false, propertyName);
216
+ }
217
+ }
218
+ else if (ts.isPropertyDeclaration(member) &&
219
+ ts.isIdentifier(member.name) &&
220
+ member.name.text === routerStateServiceVar) {
221
+ // Skip the RouterStateService property itself
222
+ return;
223
+ }
224
+ else {
225
+ detectUsages(member);
226
+ }
227
+ });
228
+ // Group property initializer usages by usage pattern
229
+ const propertyInitializerUsages = new Map();
230
+ for (const [usage, info] of usagesInPropertyInitializers) {
231
+ // Determine if this is a property access or method call
232
+ const isMethodCall = usage.includes('(');
233
+ let usageKey;
234
+ let injectFn;
235
+ let type;
236
+ let args;
237
+ let genericType = '';
238
+ if (isMethodCall) {
239
+ // Extract method name, generic type, and args
240
+ // Remove whitespace and newlines for matching
241
+ const normalizedUsage = usage.replace(/\s+/g, ' ');
242
+ const match = normalizedUsage.match(/\.(\w+)(?:<([^>]+)>)?\(([^)]*)\)/);
243
+ if (!match)
244
+ continue;
245
+ const methodName = match[1];
246
+ genericType = match[2] ? `<${match[2]}>` : '';
247
+ args = match[3].trim();
248
+ const methodInfo = methodMap[methodName];
249
+ if (!methodInfo)
250
+ continue;
251
+ usageKey = `${methodName}${genericType}(${args})`;
252
+ injectFn = methodInfo.injectFn;
253
+ type = methodInfo.type;
254
+ }
255
+ else {
256
+ // Extract property name
257
+ const match = usage.match(/\.(\w+\$?)$/);
258
+ if (!match)
259
+ continue;
260
+ const propertyName = match[1];
261
+ injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
262
+ type = propertyName.endsWith('$') ? 'observable' : 'signal';
263
+ usageKey = propertyName;
264
+ }
265
+ if (!propertyInitializerUsages.has(usageKey)) {
266
+ propertyInitializerUsages.set(usageKey, {
267
+ injectFn,
268
+ args,
269
+ genericType,
270
+ type,
271
+ properties: [],
272
+ });
273
+ }
274
+ const wrappedInToSignal = usagesWrappedInToSignal.has(usage);
275
+ propertyInitializerUsages.get(usageKey).properties.push({
276
+ propertyDecl: info.propertyDecl,
277
+ wrappedInToSignal,
278
+ });
279
+ }
280
+ // Process property initializer usages
281
+ for (const [usageKey, usageInfo] of propertyInitializerUsages) {
282
+ const { injectFn, args, type, genericType, properties } = usageInfo;
283
+ // Check if this same usage is used outside property initializers
284
+ const isUsedOutsideInitializers = usagesOutsideToSignal.has(usageKey) ||
285
+ Array.from(usagesOutsideToSignal.keys()).some((key) => {
286
+ if (args) {
287
+ return key.includes(usageKey.split('(')[0]) && key.includes(args);
288
+ }
289
+ return key.includes(usageKey);
290
+ });
291
+ if (isUsedOutsideInitializers && type === 'observable') {
292
+ // Create a shared member
293
+ const baseName = usageKey.endsWith('$') ? usageKey : `${usageKey}$`;
294
+ const memberName = findAvailableMemberName(baseName, context.existingMembers);
295
+ const memberInfo = {
296
+ name: memberName,
297
+ type,
298
+ injectFn,
299
+ originalProperty: usageKey,
300
+ args,
301
+ wrappedInToSignal: false,
302
+ };
303
+ context.membersToAdd.push(memberInfo);
304
+ context.existingMembers.add(memberName);
305
+ // For each property, replace the usage with the member reference
306
+ for (const { propertyDecl } of properties) {
307
+ const initializerText = propertyDecl.initializer.getText(sourceFile);
308
+ // Create pattern to match only the RouterStateService access part
309
+ const isMethodCall = usageKey.includes('(');
310
+ if (isMethodCall) {
311
+ const methodNameOnly = usageKey.split(/[<(]/)[0];
312
+ const escapedVar = escapeRegExp(routerStateServiceVar);
313
+ const escapedMethod = escapeRegExp(methodNameOnly);
314
+ const pattern = new RegExp(`(this\\.)?${escapedVar}\\s*\\.\\s*${escapedMethod}(?:<[^>]+>)?\\s*\\([^)]*\\)`, 'gs');
315
+ const match = initializerText.match(pattern);
316
+ if (match && match[0]) {
317
+ // Check if the original has 'this.' prefix
318
+ const hasThisPrefix = match[0].startsWith('this.');
319
+ const replacement = hasThisPrefix ? `this.${memberName}` : memberName;
320
+ context.replacements.set(match[0], replacement);
321
+ }
322
+ }
323
+ else {
324
+ const pattern = new RegExp(`(this\\.)?${escapeRegExp(routerStateServiceVar)}\\.${escapeRegExp(usageKey)}`, 'g');
325
+ const matches = initializerText.match(pattern);
326
+ if (matches && matches[0]) {
327
+ // Check if the original has 'this.' prefix
328
+ const hasThisPrefix = matches[0].startsWith('this.');
329
+ const replacement = hasThisPrefix ? `this.${memberName}` : memberName;
330
+ context.replacements.set(matches[0], replacement);
331
+ }
332
+ }
333
+ }
334
+ context.importsNeeded.add(injectFn);
335
+ }
336
+ else {
337
+ // Replace each property initializer directly
338
+ for (const { propertyDecl, wrappedInToSignal } of properties) {
339
+ const initializerText = propertyDecl.initializer.getText(sourceFile);
340
+ const injectCall = args ? `${injectFn}${genericType || ''}(${args})` : `${injectFn}()`;
341
+ const propertyName = ts.isIdentifier(propertyDecl.name) ? propertyDecl.name.text : '';
342
+ const isObservableProperty = propertyName.endsWith('$');
343
+ const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, propertyDecl);
344
+ const hasChainedCalls = initializerText.includes('.pipe(') ||
345
+ initializerText.includes('.subscribe(') ||
346
+ 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
+ const needsToObservable = (type === 'observable' && !wrappedInToSignal && (isObservableProperty || isPropertyUsedElsewhere)) ||
351
+ (wrappedInToSignal && hasChainedCalls);
352
+ const wrappedInjectCall = needsToObservable ? `toObservable(${injectCall})` : injectCall;
353
+ const isMethodCall = usageKey.includes('(');
354
+ if (hasChainedCalls) {
355
+ if (isMethodCall) {
356
+ const methodNameOnly = usageKey.split(/[<(]/)[0];
357
+ const escapedVar = escapeRegExp(routerStateServiceVar);
358
+ const escapedMethod = escapeRegExp(methodNameOnly);
359
+ const pattern = new RegExp(`(this\\.)?${escapedVar}\\s*\\.\\s*${escapedMethod}(?:<[^>]+>)?\\s*\\([^)]*\\)`, 'gs');
360
+ const match = initializerText.match(pattern);
361
+ if (match && match[0]) {
362
+ context.replacements.set(match[0], wrappedInjectCall);
363
+ }
364
+ }
365
+ else {
366
+ const pattern = new RegExp(`(this\\.)?${escapeRegExp(routerStateServiceVar)}\\.${escapeRegExp(usageKey)}`, 'g');
367
+ const matches = initializerText.match(pattern);
368
+ if (matches && matches[0]) {
369
+ context.replacements.set(matches[0], wrappedInjectCall);
370
+ }
371
+ }
372
+ }
373
+ else {
374
+ context.replacements.set(initializerText, wrappedInjectCall);
375
+ }
376
+ context.importsNeeded.add(injectFn);
377
+ }
378
+ }
379
+ }
380
+ // Handle direct usages (not in property initializers)
381
+ for (const [usage] of usagesWrappedInToSignal) {
382
+ if (!usagesInPropertyInitializers.has(usage)) {
383
+ const isMethodCall = usage.includes('(');
384
+ let injectFn;
385
+ let args;
386
+ if (isMethodCall) {
387
+ const match = usage.match(/\.(\w+)\((.*)\)/);
388
+ if (!match)
389
+ continue;
390
+ const methodName = match[1];
391
+ args = match[2];
392
+ const methodInfo = methodMap[methodName];
393
+ if (!methodInfo)
394
+ continue;
395
+ injectFn = methodInfo.injectFn;
396
+ }
397
+ else {
398
+ const match = usage.match(/\.(\w+\$?)$/);
399
+ if (!match)
400
+ continue;
401
+ const propertyName = match[1];
402
+ injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
403
+ }
404
+ const injectCall = args ? `${injectFn}(${args})` : `${injectFn}()`;
405
+ context.replacements.set(usage, injectCall);
406
+ context.importsNeeded.add(injectFn);
407
+ }
408
+ }
409
+ // Handle direct usages in method bodies (outside toSignal, not in property initializers)
410
+ for (const [usage] of usagesOutsideToSignal) {
411
+ // Skip if already handled in property initializers
412
+ if (usagesInPropertyInitializers.has(usage))
413
+ continue;
414
+ const isMethodCall = usage.includes('(');
415
+ let injectFn;
416
+ let args;
417
+ let genericType;
418
+ let type;
419
+ let needsInjectionContext;
420
+ if (isMethodCall) {
421
+ const normalizedUsage = usage.replace(/\s+/g, ' ');
422
+ const match = normalizedUsage.match(/\.(\w+)(?:<([^>]+)>)?\(([^)]*)\)/);
423
+ if (!match)
424
+ continue;
425
+ const methodName = match[1];
426
+ genericType = match[2] ? `<${match[2]}>` : '';
427
+ args = match[3].trim();
428
+ const methodInfo = methodMap[methodName];
429
+ if (!methodInfo)
430
+ continue;
431
+ injectFn = methodInfo.injectFn;
432
+ type = methodInfo.type;
433
+ needsInjectionContext = methodInfo.needsInjectionContext || false;
434
+ if (needsInjectionContext) {
435
+ // Add to constructor calls
436
+ const injectCall = args ? `${injectFn}${genericType}(${args})` : `${injectFn}()`;
437
+ 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
+ context.replacements.set(usage + ';', '');
443
+ context.importsNeeded.add(injectFn);
444
+ continue; // Don't create a member for this
445
+ }
446
+ }
447
+ else {
448
+ const match = usage.match(/\.(\w+\$?)$/);
449
+ if (!match)
450
+ continue;
451
+ const propertyName = match[1];
452
+ injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
453
+ type = propertyName.endsWith('$') ? 'observable' : 'signal';
454
+ }
455
+ // For properties used in method bodies, create a shared member
456
+ const baseName = usage.split('.').pop();
457
+ const memberName = findAvailableMemberName(baseName, context.existingMembers);
458
+ const memberInfo = {
459
+ name: memberName,
460
+ type,
461
+ injectFn,
462
+ originalProperty: baseName,
463
+ args,
464
+ wrappedInToSignal: false,
465
+ };
466
+ context.membersToAdd.push(memberInfo);
467
+ 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
+ const hasThisPrefix = usage.includes('this.');
473
+ const baseReplacement = type === 'signal' ? `${memberName}()` : `${memberName}`;
474
+ const replacement = hasThisPrefix ? `this.${baseReplacement}` : baseReplacement;
475
+ context.replacements.set(usage, replacement);
476
+ context.importsNeeded.add(injectFn);
477
+ }
478
+ return context;
479
+ }
480
+ function addOrUpdateConstructor(sourceFile, content, classNode, constructorCalls) {
481
+ if (constructorCalls.length === 0)
482
+ return content;
483
+ // Find existing constructor
484
+ const existingConstructor = classNode.members.find((member) => ts.isConstructorDeclaration(member));
485
+ const callStatements = constructorCalls.map((call) => ` ${call};`).join('\n');
486
+ if (existingConstructor) {
487
+ // Add calls to existing constructor
488
+ const constructorBody = existingConstructor.body;
489
+ if (!constructorBody)
490
+ return content;
491
+ const insertPos = constructorBody.getStart(sourceFile) + 1; // After opening brace
492
+ const indent = '\n ';
493
+ return content.slice(0, insertPos) + indent + callStatements + content.slice(insertPos);
494
+ }
495
+ else {
496
+ // Create new constructor
497
+ const firstMethod = classNode.members.find((member) => ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member));
498
+ const constructorText = `\n\n constructor() {\n${callStatements}\n }`;
499
+ if (firstMethod) {
500
+ const insertPos = firstMethod.getStart(sourceFile);
501
+ return content.slice(0, insertPos) + constructorText + '\n\n ' + content.slice(insertPos);
502
+ }
503
+ else {
504
+ // Insert at end of class, before closing brace
505
+ const classEnd = classNode.getEnd() - 1;
506
+ return content.slice(0, classEnd) + constructorText + '\n' + content.slice(classEnd);
507
+ }
508
+ }
509
+ }
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
+ function findAvailableMemberName(baseName, existingMembers) {
517
+ let name = baseName;
518
+ let counter = 1;
519
+ while (existingMembers.has(name)) {
520
+ name = `${baseName}${counter}`;
521
+ counter++;
522
+ }
523
+ return name;
524
+ }
525
+ function createReplacementsForMember(sourceFile, classNode, routerStateServiceVar, usageKey, memberName, type, context, args) {
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) {
542
+ let usedElsewhere = false;
543
+ function visit(node) {
544
+ if (node === propertyDecl)
545
+ return;
546
+ if (ts.isPropertyAccessExpression(node)) {
547
+ if (node.expression.kind === ts.SyntaxKind.ThisKeyword && node.name.text === propertyName) {
548
+ usedElsewhere = true;
549
+ }
550
+ }
551
+ ts.forEachChild(node, visit);
552
+ }
553
+ classNode.members.forEach((member) => {
554
+ if (member !== propertyDecl) {
555
+ visit(member);
556
+ }
557
+ });
558
+ return usedElsewhere;
559
+ }
560
+ function escapeRegExp(string) {
561
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
562
+ }
563
+ function addMembersToClass(sourceFile, content, classNode, members, routerStateServiceVar) {
564
+ // Find the RouterStateService property to insert after
565
+ const routerStateProperty = classNode.members.find((member) => ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === routerStateServiceVar);
566
+ if (!routerStateProperty) {
567
+ console.warn('Could not find RouterStateService property to insert new members after');
568
+ return content;
569
+ }
570
+ const insertPosition = routerStateProperty.getEnd();
571
+ // Generate member declarations
572
+ const memberDeclarations = members.map((member) => {
573
+ const injectCall = member.args ? `${member.injectFn}(${member.args})` : `${member.injectFn}()`;
574
+ const value = member.type === 'observable' ? `toObservable(${injectCall})` : injectCall;
575
+ return `\n private ${member.name} = ${value};`;
576
+ });
577
+ return content.slice(0, insertPosition) + memberDeclarations.join('') + content.slice(insertPosition);
578
+ }
579
+ function handleInlineInjectPatterns(sourceFile, content) {
580
+ const importsNeeded = new Set();
581
+ let updatedContent = content;
582
+ const { signalPropertyMap, observablePropertyMap } = getPropertyMaps();
583
+ // Method map
584
+ const methodMap = {
585
+ selectQueryParam: { injectFn: 'injectQueryParam', type: 'observable', requiresArgs: true },
586
+ selectPathParam: { injectFn: 'injectPathParam', type: 'observable', requiresArgs: true },
587
+ selectData: { injectFn: 'injectRouteDataItem', type: 'observable', requiresArgs: true },
588
+ enableScrollEnhancements: {
589
+ injectFn: 'setupScrollRestoration',
590
+ type: 'signal',
591
+ requiresArgs: true,
592
+ needsInjectionContext: true,
593
+ },
594
+ };
595
+ // Track which nodes are already inside toSignal to avoid double processing
596
+ const processedNodes = new Set();
597
+ function isInsideToSignal(node) {
598
+ let current = node.parent;
599
+ while (current) {
600
+ if (ts.isCallExpression(current) &&
601
+ ts.isIdentifier(current.expression) &&
602
+ current.expression.text === 'toSignal') {
603
+ return true;
604
+ }
605
+ current = current.parent;
606
+ }
607
+ return false;
608
+ }
609
+ function visitNode(node) {
610
+ // Look for: toSignal(inject(RouterStateService).property)
611
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
612
+ const arg = node.arguments[0];
613
+ if (arg && ts.isPropertyAccessExpression(arg)) {
614
+ const propertyName = arg.name.text;
615
+ if (ts.isCallExpression(arg.expression) &&
616
+ ts.isIdentifier(arg.expression.expression) &&
617
+ arg.expression.expression.text === 'inject' &&
618
+ arg.expression.arguments.length > 0) {
619
+ const injectArg = arg.expression.arguments[0];
620
+ if (ts.isIdentifier(injectArg) && injectArg.text === 'RouterStateService') {
621
+ const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
622
+ if (injectFn) {
623
+ const oldText = node.getText(sourceFile);
624
+ const newText = `${injectFn}()`;
625
+ updatedContent = updatedContent.replace(oldText, newText);
626
+ importsNeeded.add(injectFn);
627
+ // Mark this property access as processed
628
+ processedNodes.add(arg);
629
+ }
630
+ }
631
+ }
632
+ }
633
+ // NEW: Look for: toSignal(inject(RouterStateService).methodCall(...))
634
+ if (arg && ts.isCallExpression(arg) && ts.isPropertyAccessExpression(arg.expression)) {
635
+ const methodName = arg.expression.name.text;
636
+ const methodInfo = methodMap[methodName];
637
+ if (methodInfo &&
638
+ ts.isCallExpression(arg.expression.expression) &&
639
+ ts.isIdentifier(arg.expression.expression.expression) &&
640
+ arg.expression.expression.expression.text === 'inject' &&
641
+ arg.expression.expression.arguments.length > 0) {
642
+ const injectArg = arg.expression.expression.arguments[0];
643
+ if (ts.isIdentifier(injectArg) && injectArg.text === 'RouterStateService') {
644
+ const oldText = node.getText(sourceFile);
645
+ // Extract generic type and args
646
+ const innerCallText = arg.getText(sourceFile);
647
+ const genericMatch = innerCallText.match(new RegExp(`${methodName}<([^>]+)>`));
648
+ const genericType = genericMatch ? `<${genericMatch[1]}>` : '';
649
+ // Get the arguments from the call
650
+ const args = arg.arguments.map((a) => a.getText(sourceFile)).join(', ');
651
+ const injectCall = `${methodInfo.injectFn}${genericType}(${args})`;
652
+ // Since it's wrapped in toSignal and returns a signal, no need for toObservable
653
+ const newText = injectCall;
654
+ updatedContent = updatedContent.replace(oldText, newText);
655
+ importsNeeded.add(methodInfo.injectFn);
656
+ // Mark this call as processed
657
+ processedNodes.add(arg);
658
+ }
659
+ }
660
+ }
661
+ }
662
+ // Look for: inject(RouterStateService).selectQueryParam() or other method calls (NOT inside toSignal)
663
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
664
+ // Skip if already processed by toSignal handler
665
+ if (processedNodes.has(node)) {
666
+ ts.forEachChild(node, visitNode);
667
+ return;
668
+ }
669
+ const methodName = node.expression.name.text;
670
+ const methodInfo = methodMap[methodName];
671
+ if (methodInfo &&
672
+ ts.isCallExpression(node.expression.expression) &&
673
+ ts.isIdentifier(node.expression.expression.expression) &&
674
+ node.expression.expression.expression.text === 'inject' &&
675
+ node.expression.expression.arguments.length > 0) {
676
+ const injectArg = node.expression.expression.arguments[0];
677
+ if (ts.isIdentifier(injectArg) && injectArg.text === 'RouterStateService') {
678
+ // Check if inside toSignal
679
+ if (isInsideToSignal(node)) {
680
+ ts.forEachChild(node, visitNode);
681
+ return;
682
+ }
683
+ const oldText = node.getText(sourceFile);
684
+ // Extract generic type and args
685
+ const genericMatch = oldText.match(new RegExp(`${methodName}<([^>]+)>`));
686
+ const genericType = genericMatch ? `<${genericMatch[1]}>` : '';
687
+ // Get the arguments from the call
688
+ const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
689
+ const injectCall = `${methodInfo.injectFn}${genericType}(${args})`;
690
+ const newText = methodInfo.type === 'observable' ? `toObservable(${injectCall})` : injectCall;
691
+ updatedContent = updatedContent.replace(oldText, newText);
692
+ importsNeeded.add(methodInfo.injectFn);
693
+ if (methodInfo.type === 'observable') {
694
+ importsNeeded.add('toObservable');
695
+ }
696
+ }
697
+ }
698
+ }
699
+ // Look for: inject(RouterStateService).property (not wrapped in toSignal)
700
+ // This handles: inject(RouterStateService).pathParams$.pipe(...)
701
+ if (ts.isPropertyAccessExpression(node)) {
702
+ // Skip if already processed by toSignal handler
703
+ if (processedNodes.has(node)) {
704
+ ts.forEachChild(node, visitNode);
705
+ return;
706
+ }
707
+ const propertyName = node.name.text;
708
+ if (ts.isCallExpression(node.expression) &&
709
+ ts.isIdentifier(node.expression.expression) &&
710
+ node.expression.expression.text === 'inject' &&
711
+ node.expression.arguments.length > 0) {
712
+ const injectArg = node.expression.arguments[0];
713
+ if (ts.isIdentifier(injectArg) && injectArg.text === 'RouterStateService') {
714
+ const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
715
+ if (injectFn) {
716
+ // Check if this is inside a toSignal wrapper
717
+ const insideToSignal = isInsideToSignal(node);
718
+ // Skip if inside toSignal - it will be handled by the toSignal pattern above
719
+ if (insideToSignal) {
720
+ ts.forEachChild(node, visitNode);
721
+ return;
722
+ }
723
+ const oldText = node.getText(sourceFile);
724
+ const type = observablePropertyMap[propertyName] ? 'observable' : 'signal';
725
+ const injectCall = `${injectFn}()`;
726
+ const newText = type === 'observable' ? `toObservable(${injectCall})` : injectCall;
727
+ updatedContent = updatedContent.replace(oldText, newText);
728
+ importsNeeded.add(injectFn);
729
+ if (type === 'observable') {
730
+ importsNeeded.add('toObservable');
731
+ }
732
+ }
733
+ }
734
+ }
735
+ }
736
+ ts.forEachChild(node, visitNode);
737
+ }
738
+ sourceFile.forEachChild(visitNode);
739
+ return { content: updatedContent, importsNeeded };
740
+ }
741
+ function addImportsToPackage(sourceFile, content, imports, packageName) {
742
+ const importsList = Array.from(imports).sort();
743
+ // Check if import already exists
744
+ let existingImport;
745
+ sourceFile.forEachChild((node) => {
746
+ if (ts.isImportDeclaration(node) &&
747
+ ts.isStringLiteral(node.moduleSpecifier) &&
748
+ node.moduleSpecifier.text === packageName) {
749
+ existingImport = node;
750
+ }
751
+ });
752
+ if (existingImport?.importClause?.namedBindings && ts.isNamedImports(existingImport.importClause.namedBindings)) {
753
+ // Add to existing import
754
+ const existingImports = existingImport.importClause.namedBindings.elements.map((el) => el.name.text);
755
+ const newImports = importsList.filter((imp) => !existingImports.includes(imp));
756
+ if (newImports.length === 0)
757
+ return content;
758
+ const allImports = [...existingImports, ...newImports].sort();
759
+ const newImportText = `import { ${allImports.join(', ')} } from '${packageName}';`;
760
+ const oldImportText = existingImport.getText(sourceFile);
761
+ return content.replace(oldImportText, newImportText);
762
+ }
763
+ else {
764
+ // Add new import at the top
765
+ const newImportText = `import { ${importsList.join(', ')} } from '${packageName}';\n`;
766
+ const firstImport = sourceFile.statements.find((stmt) => ts.isImportDeclaration(stmt));
767
+ if (firstImport) {
768
+ const insertPos = firstImport.getStart(sourceFile);
769
+ return content.slice(0, insertPos) + newImportText + content.slice(insertPos);
770
+ }
771
+ else {
772
+ return newImportText + content;
773
+ }
774
+ }
775
+ }
776
+ function removeRouterStateServiceInjection(sourceFile, content, routerStateServiceVars, filePath) {
777
+ let updatedContent = content;
778
+ const modifications = [];
779
+ sourceFile.forEachChild((node) => {
780
+ if (ts.isClassDeclaration(node)) {
781
+ // Collect property removals
782
+ node.members.forEach((member) => {
783
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
784
+ const memberName = member.name.text;
785
+ if (routerStateServiceVars.includes(memberName)) {
786
+ const memberStart = member.getStart(sourceFile, true);
787
+ const memberEnd = member.getEnd();
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: '' });
800
+ }
801
+ }
802
+ });
803
+ // Collect constructor parameter modifications
804
+ node.members.forEach((member) => {
805
+ if (ts.isConstructorDeclaration(member) && member.parameters.length > 0) {
806
+ const newParams = [];
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;
822
+ }
823
+ const constructorStart = member.getStart(sourceFile);
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
+ });
832
+ }
833
+ });
834
+ }
835
+ });
836
+ modifications.sort((a, b) => b.start - a.start);
837
+ for (const mod of modifications) {
838
+ updatedContent = updatedContent.slice(0, mod.start) + mod.replacement + updatedContent.slice(mod.end);
839
+ }
840
+ return updatedContent;
841
+ }
842
+ function findMatchingParen(text, openIndex) {
843
+ let depth = 1;
844
+ let i = openIndex + 1;
845
+ while (i < text.length && depth > 0) {
846
+ if (text[i] === '(') {
847
+ depth++;
848
+ }
849
+ else if (text[i] === ')') {
850
+ depth--;
851
+ }
852
+ i++;
853
+ }
854
+ return depth === 0 ? i - 1 : -1;
855
+ }
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
+ function removeUnusedImports(sourceFile, content) {
918
+ let updatedContent = content;
919
+ // Check if toSignal is still used
920
+ const hasToSignal = content.includes('toSignal(');
921
+ if (!hasToSignal) {
922
+ sourceFile.forEachChild((node) => {
923
+ if (ts.isImportDeclaration(node) &&
924
+ ts.isStringLiteral(node.moduleSpecifier) &&
925
+ node.moduleSpecifier.text === '@angular/core/rxjs-interop') {
926
+ if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
927
+ const imports = node.importClause.namedBindings.elements;
928
+ const hasOnlyToSignal = imports.length === 1 && imports[0].name.text === 'toSignal';
929
+ if (hasOnlyToSignal) {
930
+ const importStart = node.getStart(sourceFile);
931
+ let lineEnd = node.getEnd();
932
+ while (lineEnd < content.length && content[lineEnd] !== '\n') {
933
+ lineEnd++;
934
+ }
935
+ if (content[lineEnd] === '\n') {
936
+ lineEnd++;
937
+ }
938
+ updatedContent = updatedContent.slice(0, importStart) + updatedContent.slice(lineEnd);
939
+ }
940
+ }
941
+ }
942
+ });
943
+ }
944
+ return updatedContent;
945
+ }
946
+ export default async function migrateRouterStateService(tree) {
947
+ console.log('\n🔄 Migrating RouterStateService usage...\n');
948
+ const tsFiles = [];
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('.');
964
+ let filesModified = 0;
965
+ let routerStateServiceUsed = false;
966
+ for (const filePath of tsFiles) {
967
+ const content = tree.read(filePath, 'utf-8');
968
+ if (!content)
969
+ continue;
970
+ if (!content.includes('RouterStateService'))
971
+ continue;
972
+ console.log(`Processing: ${filePath}`);
973
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
974
+ const inlineResult = handleInlineInjectPatterns(sourceFile, content);
975
+ let updatedContent = inlineResult.content;
976
+ const updatedSourceFile = updatedContent !== content
977
+ ? ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true)
978
+ : sourceFile;
979
+ const routerStateServiceVars = findRouterStateServiceVariables(updatedSourceFile);
980
+ if (routerStateServiceVars.length === 0 && inlineResult.importsNeeded.size === 0) {
981
+ continue;
982
+ }
983
+ routerStateServiceUsed = true;
984
+ const allImportsNeeded = {
985
+ '@ethlete/core': new Set(),
986
+ '@angular/core/rxjs-interop': new Set(),
987
+ };
988
+ for (const importName of inlineResult.importsNeeded) {
989
+ if (importName === 'toObservable') {
990
+ allImportsNeeded['@angular/core/rxjs-interop'].add(importName);
991
+ }
992
+ else {
993
+ allImportsNeeded['@ethlete/core'].add(importName);
994
+ }
995
+ }
996
+ for (const routerStateServiceVar of routerStateServiceVars) {
997
+ const classNode = findClassForRouterStateService(updatedSourceFile, routerStateServiceVar);
998
+ if (!classNode)
999
+ continue;
1000
+ const context = analyzeClassMigration(updatedSourceFile, classNode, routerStateServiceVar);
1001
+ context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
1002
+ context.membersToAdd.forEach((member) => {
1003
+ allImportsNeeded['@ethlete/core'].add(member.injectFn);
1004
+ if (member.type === 'observable' && !member.wrappedInToSignal) {
1005
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
1006
+ }
1007
+ });
1008
+ for (const [original, replacement] of context.replacements) {
1009
+ if (replacement.includes('toObservable(')) {
1010
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
1011
+ }
1012
+ if (replacement.includes('toSignal(')) {
1013
+ allImportsNeeded['@angular/core/rxjs-interop'].add('toSignal');
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);
1032
+ }
1033
+ }
1034
+ }
1035
+ for (const [packageName, importsSet] of Object.entries(allImportsNeeded)) {
1036
+ if (importsSet.size > 0) {
1037
+ const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1038
+ updatedContent = addImportsToPackage(sourceFileUpdated, updatedContent, importsSet, packageName);
1039
+ }
1040
+ }
1041
+ const sourceFileFinal = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1042
+ updatedContent = removeRouterStateServiceInjection(sourceFileFinal, updatedContent, routerStateServiceVars, filePath);
1043
+ const sourceFileAfterRemoval = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1044
+ if (!checkIfRouterStateServiceStillUsed(sourceFileAfterRemoval, routerStateServiceVars)) {
1045
+ updatedContent = removeRouterStateServiceImport(sourceFileAfterRemoval, updatedContent);
1046
+ }
1047
+ const sourceFileAfterCleanup = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
1048
+ updatedContent = removeUnusedImports(sourceFileAfterCleanup, updatedContent);
1049
+ if (updatedContent !== content) {
1050
+ tree.write(filePath, updatedContent);
1051
+ filesModified++;
1052
+ }
1053
+ }
1054
+ if (filesModified > 0) {
1055
+ console.log(`\n✅ Successfully migrated RouterStateService in ${filesModified} file(s)\n`);
1056
+ }
1057
+ else if (routerStateServiceUsed) {
1058
+ console.log('\nℹ️ RouterStateService detected but no migrations needed\n');
1059
+ }
1060
+ else {
1061
+ console.log('\nℹ️ No RouterStateService usage found\n');
1062
+ }
1063
+ }
1064
+ //# sourceMappingURL=router-state-service.js.map