@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.
- package/CHANGELOG.md +67 -0
- package/fesm2022/ethlete-core.mjs +3996 -4793
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/generators.json +14 -0
- package/generators/migrate-to-v5/create-provider.js +158 -0
- package/generators/migrate-to-v5/migration.js +28 -0
- package/generators/migrate-to-v5/router-state-service.js +1064 -0
- package/generators/migrate-to-v5/schema.json +29 -0
- package/generators/migrate-to-v5/viewport-service.js +1678 -0
- package/generators/tailwind-4-theme/generator.js +490 -0
- package/generators/tailwind-4-theme/schema.json +32 -0
- package/package.json +18 -11
- package/types/ethlete-core.d.ts +2161 -0
- package/index.d.ts +0 -1975
|
@@ -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
|