@ethlete/core 5.0.0-next.7 → 5.0.0-next.71
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 +849 -0
- package/README.md +28 -4
- package/design/interactive-styles.ts +5 -0
- package/design/theme-css.js +392 -0
- package/design/theme-css.ts +547 -0
- package/fesm2022/ethlete-core.mjs +9357 -5667
- 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 +41 -5
- package/generators/migrate-color-theme-variants/color-theme-variants.js +136 -0
- package/generators/migrate-color-theme-variants/migration.js +46 -0
- package/generators/migrate-color-theme-variants/schema.json +26 -0
- package/generators/migrate-create-destroy/create-destroy.js +317 -0
- package/generators/migrate-create-destroy/migration.js +46 -0
- package/generators/migrate-create-destroy/schema.json +26 -0
- package/generators/migrate-legacy-color-themes/legacy-color-themes.js +123 -0
- package/generators/migrate-legacy-color-themes/migration.js +93 -0
- package/generators/migrate-legacy-color-themes/schema.json +26 -0
- 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 +161 -0
- package/generators/migrate-to-v5/router-state-service.js +125 -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/sync-observable-consumers.js +80 -0
- package/generators/migrate-to-v5/viewport-service.js +226 -491
- package/generators/tailwind-4-color-theme/generator.js +411 -0
- package/generators/tailwind-4-color-theme/schema.json +44 -0
- package/generators/tailwind-4-surface-theme/generator.js +379 -0
- package/generators/tailwind-4-surface-theme/schema.json +48 -0
- package/migrations/legacy-color-themes.md +38 -0
- package/migrations/seo-directive-removed.md +81 -0
- package/migrations/surface-theme-regenerate.md +20 -0
- package/migrations.json +59 -0
- package/package.json +38 -21
- package/src/lib/animations/easing-tokens-styles.component.css +155 -0
- package/src/lib/providers/visually-hidden-styles.component.css +13 -0
- package/src/lib/resize-handles/resize-handles.component.css +137 -0
- package/src/lib/theming/color-interactive-styles.component.css +147 -0
- package/src/lib/theming/surface-interactive-styles.component.css +42 -0
- package/types/ethlete-core.d.ts +3842 -2205
- package/generators/tailwind-4-theme/generator.js +0 -514
- package/generators/tailwind-4-theme/schema.json +0 -32
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { logger } from '@nx/devkit';
|
|
1
2
|
import * as ts from 'typescript';
|
|
3
|
+
import { collectFiles } from './migration-files.js';
|
|
4
|
+
import { findSyncObservableConsumers } from './sync-observable-consumers.js';
|
|
5
|
+
import { applyReplacementsInClass, collectClasses, findServiceClasses, getServiceImport, isServiceParameter, isServiceProperty, isServiceStillUsed, removeServiceImport, } from './service-classes.js';
|
|
2
6
|
function getPropertyMaps() {
|
|
3
|
-
// Property map for signals (getters for latest value)
|
|
4
7
|
const signalPropertyMap = {
|
|
5
8
|
route: 'injectRoute',
|
|
6
9
|
state: 'injectRouterState',
|
|
@@ -11,7 +14,6 @@ function getPropertyMaps() {
|
|
|
11
14
|
fragment: 'injectFragment',
|
|
12
15
|
latestEvent: 'injectRouterEvent',
|
|
13
16
|
};
|
|
14
|
-
// Property map for observables
|
|
15
17
|
const observablePropertyMap = {
|
|
16
18
|
route$: 'injectRoute',
|
|
17
19
|
state$: 'injectRouterState',
|
|
@@ -25,17 +27,15 @@ function getPropertyMaps() {
|
|
|
25
27
|
};
|
|
26
28
|
return { signalPropertyMap, observablePropertyMap };
|
|
27
29
|
}
|
|
28
|
-
function findRouterStateServiceVariables(sourceFile) {
|
|
30
|
+
function findRouterStateServiceVariables(sourceFile, serviceName) {
|
|
29
31
|
const variables = [];
|
|
30
32
|
function visit(node) {
|
|
31
|
-
// Check for inject(RouterStateService) pattern
|
|
32
33
|
if (ts.isCallExpression(node) &&
|
|
33
34
|
ts.isIdentifier(node.expression) &&
|
|
34
35
|
node.expression.text === 'inject' &&
|
|
35
36
|
node.arguments.length > 0) {
|
|
36
37
|
const arg = node.arguments[0];
|
|
37
|
-
if (ts.isIdentifier(arg) && arg.text ===
|
|
38
|
-
// Find the variable name
|
|
38
|
+
if (ts.isIdentifier(arg) && arg.text === serviceName) {
|
|
39
39
|
let parent = node.parent;
|
|
40
40
|
while (parent) {
|
|
41
41
|
if (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
@@ -50,11 +50,10 @@ function findRouterStateServiceVariables(sourceFile) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
// Check for constructor parameter injection
|
|
54
53
|
if (ts.isParameter(node)) {
|
|
55
54
|
const typeNode = node.type;
|
|
56
55
|
if (typeNode && ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
57
|
-
if (typeNode.typeName.text ===
|
|
56
|
+
if (typeNode.typeName.text === serviceName && node.name && ts.isIdentifier(node.name)) {
|
|
58
57
|
variables.push(node.name.text);
|
|
59
58
|
}
|
|
60
59
|
}
|
|
@@ -64,29 +63,6 @@ function findRouterStateServiceVariables(sourceFile) {
|
|
|
64
63
|
visit(sourceFile);
|
|
65
64
|
return variables;
|
|
66
65
|
}
|
|
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
66
|
function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
91
67
|
const context = {
|
|
92
68
|
routerStateServiceVar,
|
|
@@ -94,15 +70,13 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
94
70
|
membersToAdd: [],
|
|
95
71
|
replacements: new Map(),
|
|
96
72
|
importsNeeded: new Set(),
|
|
97
|
-
constructorCalls: [],
|
|
73
|
+
constructorCalls: [],
|
|
98
74
|
};
|
|
99
|
-
// Collect existing member names and detect property initializers
|
|
100
75
|
const propertyInitializers = new Map();
|
|
101
76
|
classNode.members.forEach((member) => {
|
|
102
77
|
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
103
78
|
const memberName = member.name.text;
|
|
104
79
|
context.existingMembers.add(memberName);
|
|
105
|
-
// Check if this property has an initializer that uses RouterStateService
|
|
106
80
|
if (member.initializer) {
|
|
107
81
|
const initializerText = member.initializer.getText(sourceFile);
|
|
108
82
|
if (initializerText.includes(routerStateServiceVar)) {
|
|
@@ -115,7 +89,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
115
89
|
}
|
|
116
90
|
});
|
|
117
91
|
const { signalPropertyMap, observablePropertyMap } = getPropertyMaps();
|
|
118
|
-
// Method map
|
|
119
92
|
const methodMap = {
|
|
120
93
|
selectQueryParam: { injectFn: 'injectQueryParam', type: 'observable', requiresArgs: true },
|
|
121
94
|
selectPathParam: { injectFn: 'injectPathParam', type: 'observable', requiresArgs: true },
|
|
@@ -127,13 +100,10 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
127
100
|
needsInjectionContext: true,
|
|
128
101
|
},
|
|
129
102
|
};
|
|
130
|
-
// Track usages
|
|
131
103
|
const usagesWrappedInToSignal = new Map();
|
|
132
104
|
const usagesOutsideToSignal = new Map();
|
|
133
105
|
const usagesInPropertyInitializers = new Map();
|
|
134
|
-
// First pass: detect toSignal usages, property initializers, and count all usages
|
|
135
106
|
function detectUsages(node, insideToSignal = false, currentProperty) {
|
|
136
|
-
// Handle toSignal wrapper
|
|
137
107
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
|
|
138
108
|
const arg = node.arguments[0];
|
|
139
109
|
if (arg) {
|
|
@@ -141,7 +111,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
141
111
|
}
|
|
142
112
|
return;
|
|
143
113
|
}
|
|
144
|
-
// Track RouterStateService property access
|
|
145
114
|
if (ts.isPropertyAccessExpression(node)) {
|
|
146
115
|
const propertyName = node.name.text;
|
|
147
116
|
const isRouterStateAccess = (ts.isIdentifier(node.expression) && node.expression.text === routerStateServiceVar) ||
|
|
@@ -158,7 +127,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
158
127
|
usage: fullAccess,
|
|
159
128
|
propertyDecl,
|
|
160
129
|
});
|
|
161
|
-
// Also track if it's wrapped in toSignal
|
|
162
130
|
if (insideToSignal) {
|
|
163
131
|
usagesWrappedInToSignal.set(fullAccess, (usagesWrappedInToSignal.get(fullAccess) || 0) + 1);
|
|
164
132
|
}
|
|
@@ -172,7 +140,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
172
140
|
}
|
|
173
141
|
}
|
|
174
142
|
}
|
|
175
|
-
// Track method calls
|
|
176
143
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
177
144
|
const methodName = node.expression.name.text;
|
|
178
145
|
if (methodName !== 'pipe' && methodName !== 'subscribe') {
|
|
@@ -190,7 +157,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
190
157
|
usage: fullCall,
|
|
191
158
|
propertyDecl,
|
|
192
159
|
});
|
|
193
|
-
// Also track if it's wrapped in toSignal
|
|
194
160
|
if (insideToSignal) {
|
|
195
161
|
usagesWrappedInToSignal.set(fullCall, (usagesWrappedInToSignal.get(fullCall) || 0) + 1);
|
|
196
162
|
}
|
|
@@ -207,7 +173,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
207
173
|
}
|
|
208
174
|
ts.forEachChild(node, (child) => detectUsages(child, insideToSignal, currentProperty));
|
|
209
175
|
}
|
|
210
|
-
// Detect usages in property initializers
|
|
211
176
|
classNode.members.forEach((member) => {
|
|
212
177
|
if (ts.isPropertyDeclaration(member) && member.initializer && ts.isIdentifier(member.name)) {
|
|
213
178
|
const propertyName = member.name.text;
|
|
@@ -218,17 +183,14 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
218
183
|
else if (ts.isPropertyDeclaration(member) &&
|
|
219
184
|
ts.isIdentifier(member.name) &&
|
|
220
185
|
member.name.text === routerStateServiceVar) {
|
|
221
|
-
// Skip the RouterStateService property itself
|
|
222
186
|
return;
|
|
223
187
|
}
|
|
224
188
|
else {
|
|
225
189
|
detectUsages(member);
|
|
226
190
|
}
|
|
227
191
|
});
|
|
228
|
-
// Group property initializer usages by usage pattern
|
|
229
192
|
const propertyInitializerUsages = new Map();
|
|
230
193
|
for (const [usage, info] of usagesInPropertyInitializers) {
|
|
231
|
-
// Determine if this is a property access or method call
|
|
232
194
|
const isMethodCall = usage.includes('(');
|
|
233
195
|
let usageKey;
|
|
234
196
|
let injectFn;
|
|
@@ -236,8 +198,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
236
198
|
let args;
|
|
237
199
|
let genericType = '';
|
|
238
200
|
if (isMethodCall) {
|
|
239
|
-
// Extract method name, generic type, and args
|
|
240
|
-
// Remove whitespace and newlines for matching
|
|
241
201
|
const normalizedUsage = usage.replace(/\s+/g, ' ');
|
|
242
202
|
const match = normalizedUsage.match(/\.(\w+)(?:<([^>]+)>)?\(([^)]*)\)/);
|
|
243
203
|
if (!match)
|
|
@@ -253,7 +213,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
253
213
|
type = methodInfo.type;
|
|
254
214
|
}
|
|
255
215
|
else {
|
|
256
|
-
// Extract property name
|
|
257
216
|
const match = usage.match(/\.(\w+\$?)$/);
|
|
258
217
|
if (!match)
|
|
259
218
|
continue;
|
|
@@ -277,10 +236,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
277
236
|
wrappedInToSignal,
|
|
278
237
|
});
|
|
279
238
|
}
|
|
280
|
-
// Process property initializer usages
|
|
281
239
|
for (const [usageKey, usageInfo] of propertyInitializerUsages) {
|
|
282
240
|
const { injectFn, args, type, genericType, properties } = usageInfo;
|
|
283
|
-
// Check if this same usage is used outside property initializers
|
|
284
241
|
const isUsedOutsideInitializers = usagesOutsideToSignal.has(usageKey) ||
|
|
285
242
|
Array.from(usagesOutsideToSignal.keys()).some((key) => {
|
|
286
243
|
if (args) {
|
|
@@ -289,7 +246,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
289
246
|
return key.includes(usageKey);
|
|
290
247
|
});
|
|
291
248
|
if (isUsedOutsideInitializers && type === 'observable') {
|
|
292
|
-
// Create a shared member
|
|
293
249
|
const baseName = usageKey.endsWith('$') ? usageKey : `${usageKey}$`;
|
|
294
250
|
const memberName = findAvailableMemberName(baseName, context.existingMembers);
|
|
295
251
|
const memberInfo = {
|
|
@@ -302,10 +258,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
302
258
|
};
|
|
303
259
|
context.membersToAdd.push(memberInfo);
|
|
304
260
|
context.existingMembers.add(memberName);
|
|
305
|
-
// For each property, replace the usage with the member reference
|
|
306
261
|
for (const { propertyDecl } of properties) {
|
|
307
262
|
const initializerText = propertyDecl.initializer.getText(sourceFile);
|
|
308
|
-
// Create pattern to match only the RouterStateService access part
|
|
309
263
|
const isMethodCall = usageKey.includes('(');
|
|
310
264
|
if (isMethodCall) {
|
|
311
265
|
const methodNameOnly = usageKey.split(/[<(]/)[0];
|
|
@@ -314,7 +268,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
314
268
|
const pattern = new RegExp(`(this\\.)?${escapedVar}\\s*\\.\\s*${escapedMethod}(?:<[^>]+>)?\\s*\\([^)]*\\)`, 'gs');
|
|
315
269
|
const match = initializerText.match(pattern);
|
|
316
270
|
if (match && match[0]) {
|
|
317
|
-
// Check if the original has 'this.' prefix
|
|
318
271
|
const hasThisPrefix = match[0].startsWith('this.');
|
|
319
272
|
const replacement = hasThisPrefix ? `this.${memberName}` : memberName;
|
|
320
273
|
context.replacements.set(match[0], replacement);
|
|
@@ -324,7 +277,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
324
277
|
const pattern = new RegExp(`(this\\.)?${escapeRegExp(routerStateServiceVar)}\\.${escapeRegExp(usageKey)}`, 'g');
|
|
325
278
|
const matches = initializerText.match(pattern);
|
|
326
279
|
if (matches && matches[0]) {
|
|
327
|
-
// Check if the original has 'this.' prefix
|
|
328
280
|
const hasThisPrefix = matches[0].startsWith('this.');
|
|
329
281
|
const replacement = hasThisPrefix ? `this.${memberName}` : memberName;
|
|
330
282
|
context.replacements.set(matches[0], replacement);
|
|
@@ -334,19 +286,15 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
334
286
|
context.importsNeeded.add(injectFn);
|
|
335
287
|
}
|
|
336
288
|
else {
|
|
337
|
-
// Replace each property initializer directly
|
|
338
289
|
for (const { propertyDecl, wrappedInToSignal } of properties) {
|
|
339
290
|
const initializerText = propertyDecl.initializer.getText(sourceFile);
|
|
340
291
|
const injectCall = args ? `${injectFn}${genericType || ''}(${args})` : `${injectFn}()`;
|
|
341
292
|
const propertyName = ts.isIdentifier(propertyDecl.name) ? propertyDecl.name.text : '';
|
|
342
293
|
const isObservableProperty = propertyName.endsWith('$');
|
|
343
|
-
const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(
|
|
294
|
+
const isPropertyUsedElsewhere = checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl);
|
|
344
295
|
const hasChainedCalls = initializerText.includes('.pipe(') ||
|
|
345
296
|
initializerText.includes('.subscribe(') ||
|
|
346
297
|
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
298
|
const needsToObservable = (type === 'observable' && !wrappedInToSignal && (isObservableProperty || isPropertyUsedElsewhere)) ||
|
|
351
299
|
(wrappedInToSignal && hasChainedCalls);
|
|
352
300
|
const wrappedInjectCall = needsToObservable ? `toObservable(${injectCall})` : injectCall;
|
|
@@ -377,7 +325,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
377
325
|
}
|
|
378
326
|
}
|
|
379
327
|
}
|
|
380
|
-
// Handle direct usages (not in property initializers)
|
|
381
328
|
for (const [usage] of usagesWrappedInToSignal) {
|
|
382
329
|
if (!usagesInPropertyInitializers.has(usage)) {
|
|
383
330
|
const isMethodCall = usage.includes('(');
|
|
@@ -406,9 +353,7 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
406
353
|
context.importsNeeded.add(injectFn);
|
|
407
354
|
}
|
|
408
355
|
}
|
|
409
|
-
// Handle direct usages in method bodies (outside toSignal, not in property initializers)
|
|
410
356
|
for (const [usage] of usagesOutsideToSignal) {
|
|
411
|
-
// Skip if already handled in property initializers
|
|
412
357
|
if (usagesInPropertyInitializers.has(usage))
|
|
413
358
|
continue;
|
|
414
359
|
const isMethodCall = usage.includes('(');
|
|
@@ -432,13 +377,8 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
432
377
|
type = methodInfo.type;
|
|
433
378
|
needsInjectionContext = methodInfo.needsInjectionContext || false;
|
|
434
379
|
if (needsInjectionContext) {
|
|
435
|
-
// Add to constructor calls
|
|
436
380
|
const injectCall = args ? `${injectFn}${genericType}(${args})` : `${injectFn}()`;
|
|
437
381
|
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
382
|
context.replacements.set(usage + ';', '');
|
|
443
383
|
context.importsNeeded.add(injectFn);
|
|
444
384
|
continue; // Don't create a member for this
|
|
@@ -452,7 +392,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
452
392
|
injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
|
|
453
393
|
type = propertyName.endsWith('$') ? 'observable' : 'signal';
|
|
454
394
|
}
|
|
455
|
-
// For properties used in method bodies, create a shared member
|
|
456
395
|
const baseName = usage.split('.').pop();
|
|
457
396
|
const memberName = findAvailableMemberName(baseName, context.existingMembers);
|
|
458
397
|
const memberInfo = {
|
|
@@ -465,10 +404,6 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
465
404
|
};
|
|
466
405
|
context.membersToAdd.push(memberInfo);
|
|
467
406
|
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
407
|
const hasThisPrefix = usage.includes('this.');
|
|
473
408
|
const baseReplacement = type === 'signal' ? `${memberName}()` : `${memberName}`;
|
|
474
409
|
const replacement = hasThisPrefix ? `this.${baseReplacement}` : baseReplacement;
|
|
@@ -480,20 +415,17 @@ function analyzeClassMigration(sourceFile, classNode, routerStateServiceVar) {
|
|
|
480
415
|
function addOrUpdateConstructor(sourceFile, content, classNode, constructorCalls) {
|
|
481
416
|
if (constructorCalls.length === 0)
|
|
482
417
|
return content;
|
|
483
|
-
// Find existing constructor
|
|
484
418
|
const existingConstructor = classNode.members.find((member) => ts.isConstructorDeclaration(member));
|
|
485
419
|
const callStatements = constructorCalls.map((call) => ` ${call};`).join('\n');
|
|
486
420
|
if (existingConstructor) {
|
|
487
|
-
// Add calls to existing constructor
|
|
488
421
|
const constructorBody = existingConstructor.body;
|
|
489
422
|
if (!constructorBody)
|
|
490
423
|
return content;
|
|
491
|
-
const insertPos = constructorBody.getStart(sourceFile) + 1;
|
|
424
|
+
const insertPos = constructorBody.getStart(sourceFile) + 1;
|
|
492
425
|
const indent = '\n ';
|
|
493
426
|
return content.slice(0, insertPos) + indent + callStatements + content.slice(insertPos);
|
|
494
427
|
}
|
|
495
428
|
else {
|
|
496
|
-
// Create new constructor
|
|
497
429
|
const firstMethod = classNode.members.find((member) => ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member));
|
|
498
430
|
const constructorText = `\n\n constructor() {\n${callStatements}\n }`;
|
|
499
431
|
if (firstMethod) {
|
|
@@ -501,18 +433,11 @@ function addOrUpdateConstructor(sourceFile, content, classNode, constructorCalls
|
|
|
501
433
|
return content.slice(0, insertPos) + constructorText + '\n\n ' + content.slice(insertPos);
|
|
502
434
|
}
|
|
503
435
|
else {
|
|
504
|
-
// Insert at end of class, before closing brace
|
|
505
436
|
const classEnd = classNode.getEnd() - 1;
|
|
506
437
|
return content.slice(0, classEnd) + constructorText + '\n' + content.slice(classEnd);
|
|
507
438
|
}
|
|
508
439
|
}
|
|
509
440
|
}
|
|
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
441
|
function findAvailableMemberName(baseName, existingMembers) {
|
|
517
442
|
let name = baseName;
|
|
518
443
|
let counter = 1;
|
|
@@ -522,23 +447,7 @@ function findAvailableMemberName(baseName, existingMembers) {
|
|
|
522
447
|
}
|
|
523
448
|
return name;
|
|
524
449
|
}
|
|
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) {
|
|
450
|
+
function checkIfPropertyIsUsedElsewhere(classNode, propertyName, propertyDecl) {
|
|
542
451
|
let usedElsewhere = false;
|
|
543
452
|
function visit(node) {
|
|
544
453
|
if (node === propertyDecl)
|
|
@@ -560,15 +469,13 @@ function checkIfPropertyIsUsedElsewhere(sourceFile, classNode, propertyName, pro
|
|
|
560
469
|
function escapeRegExp(string) {
|
|
561
470
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
562
471
|
}
|
|
563
|
-
function addMembersToClass(
|
|
564
|
-
// Find the RouterStateService property to insert after
|
|
472
|
+
function addMembersToClass(content, classNode, members, routerStateServiceVar) {
|
|
565
473
|
const routerStateProperty = classNode.members.find((member) => ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text === routerStateServiceVar);
|
|
566
474
|
if (!routerStateProperty) {
|
|
567
|
-
|
|
475
|
+
logger.warn('Could not find RouterStateService property to insert new members after');
|
|
568
476
|
return content;
|
|
569
477
|
}
|
|
570
478
|
const insertPosition = routerStateProperty.getEnd();
|
|
571
|
-
// Generate member declarations
|
|
572
479
|
const memberDeclarations = members.map((member) => {
|
|
573
480
|
const injectCall = member.args ? `${member.injectFn}(${member.args})` : `${member.injectFn}()`;
|
|
574
481
|
const value = member.type === 'observable' ? `toObservable(${injectCall})` : injectCall;
|
|
@@ -576,11 +483,10 @@ function addMembersToClass(sourceFile, content, classNode, members, routerStateS
|
|
|
576
483
|
});
|
|
577
484
|
return content.slice(0, insertPosition) + memberDeclarations.join('') + content.slice(insertPosition);
|
|
578
485
|
}
|
|
579
|
-
function handleInlineInjectPatterns(sourceFile, content) {
|
|
486
|
+
function handleInlineInjectPatterns(sourceFile, content, serviceName) {
|
|
580
487
|
const importsNeeded = new Set();
|
|
581
|
-
|
|
488
|
+
const edits = [];
|
|
582
489
|
const { signalPropertyMap, observablePropertyMap } = getPropertyMaps();
|
|
583
|
-
// Method map
|
|
584
490
|
const methodMap = {
|
|
585
491
|
selectQueryParam: { injectFn: 'injectQueryParam', type: 'observable', requiresArgs: true },
|
|
586
492
|
selectPathParam: { injectFn: 'injectPathParam', type: 'observable', requiresArgs: true },
|
|
@@ -592,7 +498,6 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
592
498
|
needsInjectionContext: true,
|
|
593
499
|
},
|
|
594
500
|
};
|
|
595
|
-
// Track which nodes are already inside toSignal to avoid double processing
|
|
596
501
|
const processedNodes = new Set();
|
|
597
502
|
function isInsideToSignal(node) {
|
|
598
503
|
let current = node.parent;
|
|
@@ -607,7 +512,6 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
607
512
|
return false;
|
|
608
513
|
}
|
|
609
514
|
function visitNode(node) {
|
|
610
|
-
// Look for: toSignal(inject(RouterStateService).property)
|
|
611
515
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'toSignal') {
|
|
612
516
|
const arg = node.arguments[0];
|
|
613
517
|
if (arg && ts.isPropertyAccessExpression(arg)) {
|
|
@@ -617,20 +521,17 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
617
521
|
arg.expression.expression.text === 'inject' &&
|
|
618
522
|
arg.expression.arguments.length > 0) {
|
|
619
523
|
const injectArg = arg.expression.arguments[0];
|
|
620
|
-
if (ts.isIdentifier(injectArg) && injectArg.text ===
|
|
524
|
+
if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
|
|
621
525
|
const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
|
|
622
526
|
if (injectFn) {
|
|
623
|
-
const oldText = node.getText(sourceFile);
|
|
624
527
|
const newText = `${injectFn}()`;
|
|
625
|
-
|
|
528
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
|
|
626
529
|
importsNeeded.add(injectFn);
|
|
627
|
-
// Mark this property access as processed
|
|
628
530
|
processedNodes.add(arg);
|
|
629
531
|
}
|
|
630
532
|
}
|
|
631
533
|
}
|
|
632
534
|
}
|
|
633
|
-
// NEW: Look for: toSignal(inject(RouterStateService).methodCall(...))
|
|
634
535
|
if (arg && ts.isCallExpression(arg) && ts.isPropertyAccessExpression(arg.expression)) {
|
|
635
536
|
const methodName = arg.expression.name.text;
|
|
636
537
|
const methodInfo = methodMap[methodName];
|
|
@@ -640,28 +541,21 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
640
541
|
arg.expression.expression.expression.text === 'inject' &&
|
|
641
542
|
arg.expression.expression.arguments.length > 0) {
|
|
642
543
|
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
|
|
544
|
+
if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
|
|
646
545
|
const innerCallText = arg.getText(sourceFile);
|
|
647
546
|
const genericMatch = innerCallText.match(new RegExp(`${methodName}<([^>]+)>`));
|
|
648
547
|
const genericType = genericMatch ? `<${genericMatch[1]}>` : '';
|
|
649
|
-
// Get the arguments from the call
|
|
650
548
|
const args = arg.arguments.map((a) => a.getText(sourceFile)).join(', ');
|
|
651
549
|
const injectCall = `${methodInfo.injectFn}${genericType}(${args})`;
|
|
652
|
-
// Since it's wrapped in toSignal and returns a signal, no need for toObservable
|
|
653
550
|
const newText = injectCall;
|
|
654
|
-
|
|
551
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
|
|
655
552
|
importsNeeded.add(methodInfo.injectFn);
|
|
656
|
-
// Mark this call as processed
|
|
657
553
|
processedNodes.add(arg);
|
|
658
554
|
}
|
|
659
555
|
}
|
|
660
556
|
}
|
|
661
557
|
}
|
|
662
|
-
// Look for: inject(RouterStateService).selectQueryParam() or other method calls (NOT inside toSignal)
|
|
663
558
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
664
|
-
// Skip if already processed by toSignal handler
|
|
665
559
|
if (processedNodes.has(node)) {
|
|
666
560
|
ts.forEachChild(node, visitNode);
|
|
667
561
|
return;
|
|
@@ -674,21 +568,18 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
674
568
|
node.expression.expression.expression.text === 'inject' &&
|
|
675
569
|
node.expression.expression.arguments.length > 0) {
|
|
676
570
|
const injectArg = node.expression.expression.arguments[0];
|
|
677
|
-
if (ts.isIdentifier(injectArg) && injectArg.text ===
|
|
678
|
-
// Check if inside toSignal
|
|
571
|
+
if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
|
|
679
572
|
if (isInsideToSignal(node)) {
|
|
680
573
|
ts.forEachChild(node, visitNode);
|
|
681
574
|
return;
|
|
682
575
|
}
|
|
683
576
|
const oldText = node.getText(sourceFile);
|
|
684
|
-
// Extract generic type and args
|
|
685
577
|
const genericMatch = oldText.match(new RegExp(`${methodName}<([^>]+)>`));
|
|
686
578
|
const genericType = genericMatch ? `<${genericMatch[1]}>` : '';
|
|
687
|
-
// Get the arguments from the call
|
|
688
579
|
const args = node.arguments.map((arg) => arg.getText(sourceFile)).join(', ');
|
|
689
580
|
const injectCall = `${methodInfo.injectFn}${genericType}(${args})`;
|
|
690
581
|
const newText = methodInfo.type === 'observable' ? `toObservable(${injectCall})` : injectCall;
|
|
691
|
-
|
|
582
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
|
|
692
583
|
importsNeeded.add(methodInfo.injectFn);
|
|
693
584
|
if (methodInfo.type === 'observable') {
|
|
694
585
|
importsNeeded.add('toObservable');
|
|
@@ -696,10 +587,7 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
696
587
|
}
|
|
697
588
|
}
|
|
698
589
|
}
|
|
699
|
-
// Look for: inject(RouterStateService).property (not wrapped in toSignal)
|
|
700
|
-
// This handles: inject(RouterStateService).pathParams$.pipe(...)
|
|
701
590
|
if (ts.isPropertyAccessExpression(node)) {
|
|
702
|
-
// Skip if already processed by toSignal handler
|
|
703
591
|
if (processedNodes.has(node)) {
|
|
704
592
|
ts.forEachChild(node, visitNode);
|
|
705
593
|
return;
|
|
@@ -710,21 +598,18 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
710
598
|
node.expression.expression.text === 'inject' &&
|
|
711
599
|
node.expression.arguments.length > 0) {
|
|
712
600
|
const injectArg = node.expression.arguments[0];
|
|
713
|
-
if (ts.isIdentifier(injectArg) && injectArg.text ===
|
|
601
|
+
if (ts.isIdentifier(injectArg) && injectArg.text === serviceName) {
|
|
714
602
|
const injectFn = signalPropertyMap[propertyName] || observablePropertyMap[propertyName];
|
|
715
603
|
if (injectFn) {
|
|
716
|
-
// Check if this is inside a toSignal wrapper
|
|
717
604
|
const insideToSignal = isInsideToSignal(node);
|
|
718
|
-
// Skip if inside toSignal - it will be handled by the toSignal pattern above
|
|
719
605
|
if (insideToSignal) {
|
|
720
606
|
ts.forEachChild(node, visitNode);
|
|
721
607
|
return;
|
|
722
608
|
}
|
|
723
|
-
const oldText = node.getText(sourceFile);
|
|
724
609
|
const type = observablePropertyMap[propertyName] ? 'observable' : 'signal';
|
|
725
610
|
const injectCall = `${injectFn}()`;
|
|
726
611
|
const newText = type === 'observable' ? `toObservable(${injectCall})` : injectCall;
|
|
727
|
-
|
|
612
|
+
edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: newText });
|
|
728
613
|
importsNeeded.add(injectFn);
|
|
729
614
|
if (type === 'observable') {
|
|
730
615
|
importsNeeded.add('toObservable');
|
|
@@ -736,11 +621,10 @@ function handleInlineInjectPatterns(sourceFile, content) {
|
|
|
736
621
|
ts.forEachChild(node, visitNode);
|
|
737
622
|
}
|
|
738
623
|
sourceFile.forEachChild(visitNode);
|
|
739
|
-
return { content:
|
|
624
|
+
return { content: applyInlineEdits(content, edits), importsNeeded };
|
|
740
625
|
}
|
|
741
626
|
function addImportsToPackage(sourceFile, content, imports, packageName) {
|
|
742
627
|
const importsList = Array.from(imports).sort();
|
|
743
|
-
// Check if import already exists
|
|
744
628
|
let existingImport;
|
|
745
629
|
sourceFile.forEachChild((node) => {
|
|
746
630
|
if (ts.isImportDeclaration(node) &&
|
|
@@ -750,18 +634,18 @@ function addImportsToPackage(sourceFile, content, imports, packageName) {
|
|
|
750
634
|
}
|
|
751
635
|
});
|
|
752
636
|
if (existingImport?.importClause?.namedBindings && ts.isNamedImports(existingImport.importClause.namedBindings)) {
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
const
|
|
637
|
+
const existingElements = existingImport.importClause.namedBindings.elements;
|
|
638
|
+
const existingImportedNames = existingElements.map((el) => el.propertyName?.text ?? el.name.text);
|
|
639
|
+
const existingImports = existingElements.map((el) => el.getText(sourceFile));
|
|
640
|
+
const newImports = importsList.filter((imp) => !existingImportedNames.includes(imp));
|
|
756
641
|
if (newImports.length === 0)
|
|
757
642
|
return content;
|
|
758
643
|
const allImports = [...existingImports, ...newImports].sort();
|
|
759
644
|
const newImportText = `import { ${allImports.join(', ')} } from '${packageName}';`;
|
|
760
645
|
const oldImportText = existingImport.getText(sourceFile);
|
|
761
|
-
return content.replace(oldImportText, newImportText);
|
|
646
|
+
return content.replace(oldImportText, () => newImportText);
|
|
762
647
|
}
|
|
763
648
|
else {
|
|
764
|
-
// Add new import at the top
|
|
765
649
|
const newImportText = `import { ${importsList.join(', ')} } from '${packageName}';\n`;
|
|
766
650
|
const firstImport = sourceFile.statements.find((stmt) => ts.isImportDeclaration(stmt));
|
|
767
651
|
if (firstImport) {
|
|
@@ -773,65 +657,54 @@ function addImportsToPackage(sourceFile, content, imports, packageName) {
|
|
|
773
657
|
}
|
|
774
658
|
}
|
|
775
659
|
}
|
|
776
|
-
function removeRouterStateServiceInjection(sourceFile, content,
|
|
660
|
+
function removeRouterStateServiceInjection(sourceFile, content, serviceName, filePath) {
|
|
777
661
|
let updatedContent = content;
|
|
778
662
|
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: '' });
|
|
663
|
+
collectClasses(sourceFile).forEach((node) => {
|
|
664
|
+
node.members.forEach((member) => {
|
|
665
|
+
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
666
|
+
if (isServiceProperty(member, serviceName)) {
|
|
667
|
+
const memberStart = member.getStart(sourceFile, true);
|
|
668
|
+
const memberEnd = member.getEnd();
|
|
669
|
+
let lineStart = memberStart;
|
|
670
|
+
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
|
|
671
|
+
lineStart--;
|
|
800
672
|
}
|
|
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;
|
|
673
|
+
let lineEnd = memberEnd;
|
|
674
|
+
while (lineEnd < content.length && content[lineEnd] !== '\n') {
|
|
675
|
+
lineEnd++;
|
|
676
|
+
}
|
|
677
|
+
if (content[lineEnd] === '\n') {
|
|
678
|
+
lineEnd++;
|
|
822
679
|
}
|
|
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
|
-
});
|
|
680
|
+
modifications.push({ start: lineStart, end: lineEnd, replacement: '' });
|
|
832
681
|
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
node.members.forEach((member) => {
|
|
685
|
+
if (ts.isConstructorDeclaration(member) &&
|
|
686
|
+
member.parameters.some((param) => isServiceParameter(param, serviceName))) {
|
|
687
|
+
const newParams = member.parameters
|
|
688
|
+
.filter((param) => !isServiceParameter(param, serviceName))
|
|
689
|
+
.map((param) => param.getText(sourceFile));
|
|
690
|
+
const constructorText = member.getText(sourceFile);
|
|
691
|
+
const openParenIndex = constructorText.indexOf('(');
|
|
692
|
+
const closeParenIndex = findMatchingParen(constructorText, openParenIndex);
|
|
693
|
+
if (openParenIndex === -1 || closeParenIndex === -1) {
|
|
694
|
+
logger.warn(`Could not find constructor parameters in ${filePath}`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const constructorStart = member.getStart(sourceFile);
|
|
698
|
+
const paramListStart = constructorStart + openParenIndex + 1;
|
|
699
|
+
const paramListEnd = constructorStart + closeParenIndex;
|
|
700
|
+
const newParamsText = newParams.join(', ');
|
|
701
|
+
modifications.push({
|
|
702
|
+
start: paramListStart,
|
|
703
|
+
end: paramListEnd,
|
|
704
|
+
replacement: newParamsText,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
});
|
|
835
708
|
});
|
|
836
709
|
modifications.sort((a, b) => b.start - a.start);
|
|
837
710
|
for (const mod of modifications) {
|
|
@@ -853,70 +726,8 @@ function findMatchingParen(text, openIndex) {
|
|
|
853
726
|
}
|
|
854
727
|
return depth === 0 ? i - 1 : -1;
|
|
855
728
|
}
|
|
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
729
|
function removeUnusedImports(sourceFile, content) {
|
|
918
730
|
let updatedContent = content;
|
|
919
|
-
// Check if toSignal is still used
|
|
920
731
|
const hasToSignal = content.includes('toSignal(');
|
|
921
732
|
if (!hasToSignal) {
|
|
922
733
|
sourceFile.forEachChild((node) => {
|
|
@@ -943,44 +754,31 @@ function removeUnusedImports(sourceFile, content) {
|
|
|
943
754
|
}
|
|
944
755
|
return updatedContent;
|
|
945
756
|
}
|
|
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('.');
|
|
757
|
+
export default async function migrateRouterStateService(tree, scope) {
|
|
758
|
+
const tsFiles = collectFiles(tree, scope, ['.ts']);
|
|
759
|
+
const review = [];
|
|
964
760
|
let filesModified = 0;
|
|
965
|
-
let routerStateServiceUsed = false;
|
|
966
761
|
for (const filePath of tsFiles) {
|
|
967
762
|
const content = tree.read(filePath, 'utf-8');
|
|
968
763
|
if (!content)
|
|
969
764
|
continue;
|
|
970
765
|
if (!content.includes('RouterStateService'))
|
|
971
766
|
continue;
|
|
972
|
-
console.log(`Processing: ${filePath}`);
|
|
973
767
|
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
974
|
-
const
|
|
768
|
+
const serviceImport = getServiceImport(sourceFile, 'RouterStateService');
|
|
769
|
+
if (!serviceImport?.packageName.startsWith('@ethlete/'))
|
|
770
|
+
continue;
|
|
771
|
+
const serviceName = serviceImport.localName;
|
|
772
|
+
const inlineResult = handleInlineInjectPatterns(sourceFile, content, serviceName);
|
|
975
773
|
let updatedContent = inlineResult.content;
|
|
976
774
|
const updatedSourceFile = updatedContent !== content
|
|
977
775
|
? ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true)
|
|
978
776
|
: sourceFile;
|
|
979
|
-
const routerStateServiceVars = findRouterStateServiceVariables(updatedSourceFile);
|
|
777
|
+
const routerStateServiceVars = findRouterStateServiceVariables(updatedSourceFile, serviceName);
|
|
778
|
+
const serviceClasses = findServiceClasses(updatedSourceFile, serviceName);
|
|
980
779
|
if (routerStateServiceVars.length === 0 && inlineResult.importsNeeded.size === 0) {
|
|
981
780
|
continue;
|
|
982
781
|
}
|
|
983
|
-
routerStateServiceUsed = true;
|
|
984
782
|
const allImportsNeeded = {
|
|
985
783
|
'@ethlete/core': new Set(),
|
|
986
784
|
'@angular/core/rxjs-interop': new Set(),
|
|
@@ -993,42 +791,34 @@ export default async function migrateRouterStateService(tree) {
|
|
|
993
791
|
allImportsNeeded['@ethlete/core'].add(importName);
|
|
994
792
|
}
|
|
995
793
|
}
|
|
996
|
-
for (const
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
794
|
+
for (const { index, fields } of serviceClasses) {
|
|
795
|
+
for (const routerStateServiceVar of fields) {
|
|
796
|
+
const currentSourceFile = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
797
|
+
const classNode = collectClasses(currentSourceFile)[index];
|
|
798
|
+
const context = analyzeClassMigration(currentSourceFile, classNode, routerStateServiceVar);
|
|
799
|
+
context.importsNeeded.forEach((imp) => allImportsNeeded['@ethlete/core'].add(imp));
|
|
800
|
+
context.membersToAdd.forEach((member) => {
|
|
801
|
+
allImportsNeeded['@ethlete/core'].add(member.injectFn);
|
|
802
|
+
if (member.type === 'observable' && !member.wrappedInToSignal) {
|
|
803
|
+
allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
for (const replacement of context.replacements.values()) {
|
|
807
|
+
if (replacement.includes('toObservable(')) {
|
|
808
|
+
allImportsNeeded['@angular/core/rxjs-interop'].add('toObservable');
|
|
809
|
+
}
|
|
810
|
+
if (replacement.includes('toSignal(')) {
|
|
811
|
+
allImportsNeeded['@angular/core/rxjs-interop'].add('toSignal');
|
|
812
|
+
}
|
|
1006
813
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
814
|
+
updatedContent = applyReplacementsInClass(updatedContent, currentSourceFile, classNode, context.replacements);
|
|
815
|
+
if (context.membersToAdd.length > 0) {
|
|
816
|
+
const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
817
|
+
updatedContent = addMembersToClass(updatedContent, collectClasses(sourceFileUpdated)[index], context.membersToAdd, routerStateServiceVar);
|
|
1011
818
|
}
|
|
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);
|
|
819
|
+
if (context.constructorCalls.length > 0) {
|
|
820
|
+
const sourceFileUpdated = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
821
|
+
updatedContent = addOrUpdateConstructor(sourceFileUpdated, updatedContent, collectClasses(sourceFileUpdated)[index], context.constructorCalls);
|
|
1032
822
|
}
|
|
1033
823
|
}
|
|
1034
824
|
}
|
|
@@ -1039,26 +829,31 @@ export default async function migrateRouterStateService(tree) {
|
|
|
1039
829
|
}
|
|
1040
830
|
}
|
|
1041
831
|
const sourceFileFinal = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
1042
|
-
updatedContent = removeRouterStateServiceInjection(sourceFileFinal, updatedContent,
|
|
832
|
+
updatedContent = removeRouterStateServiceInjection(sourceFileFinal, updatedContent, serviceName, filePath);
|
|
1043
833
|
const sourceFileAfterRemoval = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
1044
|
-
if (
|
|
1045
|
-
|
|
834
|
+
if (isServiceStillUsed(sourceFileAfterRemoval, serviceName, serviceClasses)) {
|
|
835
|
+
review.push(`${filePath}: RouterStateService is still used. Migrate the remaining usages manually.`);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
updatedContent = removeServiceImport(sourceFileAfterRemoval, updatedContent, 'RouterStateService');
|
|
1046
839
|
}
|
|
1047
840
|
const sourceFileAfterCleanup = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
|
|
1048
841
|
updatedContent = removeUnusedImports(sourceFileAfterCleanup, updatedContent);
|
|
842
|
+
review.push(...findSyncObservableConsumers(filePath, content, updatedContent));
|
|
1049
843
|
if (updatedContent !== content) {
|
|
1050
844
|
tree.write(filePath, updatedContent);
|
|
1051
845
|
filesModified++;
|
|
1052
846
|
}
|
|
1053
847
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
848
|
+
return { filesChanged: filesModified, review };
|
|
849
|
+
}
|
|
850
|
+
function applyInlineEdits(content, edits) {
|
|
851
|
+
const accepted = [];
|
|
852
|
+
for (const edit of [...edits].sort((a, b) => a.start - b.start || b.end - a.end)) {
|
|
853
|
+
const last = accepted.at(-1);
|
|
854
|
+
if (last && edit.start < last.end)
|
|
855
|
+
continue;
|
|
856
|
+
accepted.push(edit);
|
|
1062
857
|
}
|
|
858
|
+
return accepted.reduceRight((result, edit) => result.slice(0, edit.start) + edit.text + result.slice(edit.end), content);
|
|
1063
859
|
}
|
|
1064
|
-
//# sourceMappingURL=router-state-service.js.map
|