@ethlete/query 6.0.0-next.3 → 6.0.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,773 @@
1
+ import { visitNotIgnoredFiles } from '@nx/devkit';
2
+ import * as ts from 'typescript';
3
+ import { createSourceFile, ensureAngularCoreImports, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
4
+ export const migrateLegacyPrepareCalls = (tree, report) => {
5
+ console.log('\nšŸ” Checking for legacy query creator usages...');
6
+ const allUsages = [];
7
+ visitNotIgnoredFiles(tree, '', (filePath) => {
8
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
9
+ return;
10
+ }
11
+ const content = tree.read(filePath, 'utf-8');
12
+ if (!content) {
13
+ return;
14
+ }
15
+ allUsages.push(...findLegacyCreatorUsages(content, filePath));
16
+ });
17
+ if (allUsages.length === 0) {
18
+ console.log('\nāœ… No legacy query creator usages found');
19
+ return;
20
+ }
21
+ console.log(`\nšŸ“Š Found ${allUsages.length} legacy query creator usages`);
22
+ const contextCounts = new Map();
23
+ allUsages.forEach((usage) => {
24
+ contextCounts.set(usage.context, (contextCounts.get(usage.context) || 0) + 1);
25
+ });
26
+ contextCounts.forEach((count, context) => {
27
+ console.log(` - ${context}: ${count} usages`);
28
+ });
29
+ console.log('\nšŸ” Detecting polling usage...');
30
+ const pollingInfo = detectPollingUsage(tree);
31
+ if (pollingInfo.size > 0) {
32
+ console.log(`\nāš ļø Found ${pollingInfo.size} queries with polling:`);
33
+ pollingInfo.forEach((info) => {
34
+ console.log(` ${info.queryVariableName}:`);
35
+ info.locations.forEach((location) => console.log(` - ${location}`));
36
+ });
37
+ console.log('\n → These queries will NOT have destroyOnResponse: true added');
38
+ }
39
+ const classInjectors = findOrCreateInjectorMembers(tree, allUsages);
40
+ createInjectorMembers(tree, classInjectors);
41
+ transformLegacyCreatorPrepareCalls(tree, classInjectors, pollingInfo);
42
+ reportManualReviewLocations(tree, allUsages, report);
43
+ };
44
+ const findLegacyCreatorUsages = (content, filePath) => {
45
+ const sourceFile = createSourceFile(content, filePath);
46
+ const usages = [];
47
+ const legacyCreatorNames = findLegacyCreatorNames(sourceFile);
48
+ if (legacyCreatorNames.size === 0) {
49
+ return usages;
50
+ }
51
+ const visit = (node) => {
52
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
53
+ const propertyAccess = node.expression;
54
+ if (ts.isIdentifier(propertyAccess.name) && propertyAccess.name.text === 'prepare') {
55
+ const creatorName = ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression.text : undefined;
56
+ if (creatorName && legacyCreatorNames.has(creatorName)) {
57
+ usages.push({
58
+ filePath,
59
+ creatorName,
60
+ line: getLineNumber(node, sourceFile),
61
+ position: {
62
+ start: node.getStart(sourceFile),
63
+ end: node.getEnd(),
64
+ },
65
+ context: determineUsageContext(node),
66
+ hasExistingInjector: checkForExistingInjector(node),
67
+ });
68
+ }
69
+ }
70
+ }
71
+ ts.forEachChild(node, visit);
72
+ };
73
+ visit(sourceFile);
74
+ return usages;
75
+ };
76
+ const findLegacyCreatorNames = (sourceFile) => {
77
+ const names = new Set();
78
+ const visit = (node) => {
79
+ if (ts.isImportDeclaration(node) &&
80
+ node.importClause?.namedBindings &&
81
+ ts.isNamedImports(node.importClause.namedBindings)) {
82
+ node.importClause.namedBindings.elements.forEach((element) => {
83
+ if (element.name.text.startsWith('legacy')) {
84
+ names.add(element.name.text);
85
+ }
86
+ });
87
+ }
88
+ if (ts.isVariableDeclaration(node) &&
89
+ ts.isIdentifier(node.name) &&
90
+ node.name.text.startsWith('legacy') &&
91
+ node.initializer &&
92
+ ts.isCallExpression(node.initializer) &&
93
+ ts.isIdentifier(node.initializer.expression) &&
94
+ node.initializer.expression.text === 'createLegacyQueryCreator') {
95
+ names.add(node.name.text);
96
+ }
97
+ ts.forEachChild(node, visit);
98
+ };
99
+ visit(sourceFile);
100
+ return names;
101
+ };
102
+ const determineUsageContext = (node) => {
103
+ let current = node;
104
+ while (current) {
105
+ if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
106
+ const parent = current.parent;
107
+ if (ts.isCallExpression(parent) &&
108
+ ts.isIdentifier(parent.expression) &&
109
+ parent.expression.text === 'queryComputed') {
110
+ return 'queryComputed';
111
+ }
112
+ }
113
+ if (ts.isConstructorDeclaration(current)) {
114
+ return 'constructor';
115
+ }
116
+ if (ts.isMethodDeclaration(current)) {
117
+ return 'method';
118
+ }
119
+ if (ts.isPropertyDeclaration(current)) {
120
+ return 'class-field';
121
+ }
122
+ if (ts.isFunctionDeclaration(current)) {
123
+ return 'function';
124
+ }
125
+ if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) && !isInsideClass(current)) {
126
+ return 'function';
127
+ }
128
+ current = current.parent;
129
+ }
130
+ return 'unknown';
131
+ };
132
+ const isInsideClass = (node) => {
133
+ let current = node.parent;
134
+ while (current) {
135
+ if (ts.isClassDeclaration(current)) {
136
+ return true;
137
+ }
138
+ current = current.parent;
139
+ }
140
+ return false;
141
+ };
142
+ const checkForExistingInjector = (node) => {
143
+ let current = node;
144
+ while (current && !ts.isClassDeclaration(current)) {
145
+ current = current.parent;
146
+ }
147
+ if (!current || !ts.isClassDeclaration(current)) {
148
+ return false;
149
+ }
150
+ for (const member of current.members) {
151
+ if (ts.isPropertyDeclaration(member) && member.initializer && hasInjectInjectorCall(member.initializer)) {
152
+ return true;
153
+ }
154
+ }
155
+ return false;
156
+ };
157
+ const hasInjectInjectorCall = (node) => {
158
+ let found = false;
159
+ const visit = (currentNode) => {
160
+ if (found) {
161
+ return;
162
+ }
163
+ if (ts.isCallExpression(currentNode) &&
164
+ ts.isIdentifier(currentNode.expression) &&
165
+ currentNode.expression.text === 'inject') {
166
+ const firstArgument = currentNode.arguments[0];
167
+ if (firstArgument && ts.isIdentifier(firstArgument) && firstArgument.text === 'Injector') {
168
+ found = true;
169
+ return;
170
+ }
171
+ }
172
+ ts.forEachChild(currentNode, visit);
173
+ };
174
+ visit(node);
175
+ return found;
176
+ };
177
+ const findOrCreateInjectorMembers = (tree, usages) => {
178
+ const classInjectors = new Map();
179
+ const usagesByFile = new Map();
180
+ usages.forEach((usage) => {
181
+ if (['queryComputed', 'class-field', 'constructor'].includes(usage.context)) {
182
+ return;
183
+ }
184
+ if (!usagesByFile.has(usage.filePath)) {
185
+ usagesByFile.set(usage.filePath, []);
186
+ }
187
+ usagesByFile.get(usage.filePath).push(usage);
188
+ });
189
+ usagesByFile.forEach((fileUsages, filePath) => {
190
+ const content = tree.read(filePath, 'utf-8');
191
+ if (!content) {
192
+ return;
193
+ }
194
+ const sourceFile = createSourceFile(content, filePath);
195
+ findClassesNeedingInjector(sourceFile, fileUsages).forEach((classInfo) => {
196
+ classInjectors.set(`${filePath}:${classInfo.className}`, classInfo);
197
+ });
198
+ });
199
+ return classInjectors;
200
+ };
201
+ const findClassesNeedingInjector = (sourceFile, usages) => {
202
+ const classes = [];
203
+ const filePath = usages[0]?.filePath ?? sourceFile.fileName;
204
+ const visit = (node) => {
205
+ if (ts.isClassDeclaration(node)) {
206
+ const className = node.name?.text ?? 'UnnamedClass';
207
+ const usagesInClass = usages.filter((usage) => {
208
+ return usage.position.start >= node.getStart(sourceFile) && usage.position.end <= node.getEnd();
209
+ });
210
+ if (usagesInClass.length === 0) {
211
+ ts.forEachChild(node, visit);
212
+ return;
213
+ }
214
+ const existingInjector = findExistingInjectorMember(node);
215
+ classes.push({
216
+ filePath,
217
+ className,
218
+ injectorMemberName: existingInjector?.name ?? 'injector',
219
+ needsToCreate: !existingInjector,
220
+ classStart: node.getStart(sourceFile),
221
+ classEnd: node.getEnd(),
222
+ });
223
+ }
224
+ ts.forEachChild(node, visit);
225
+ };
226
+ visit(sourceFile);
227
+ return classes;
228
+ };
229
+ const findExistingInjectorMember = (classNode) => {
230
+ for (const member of classNode.members) {
231
+ if (ts.isPropertyDeclaration(member) && member.initializer && hasInjectInjectorCall(member.initializer)) {
232
+ const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
233
+ if (memberName) {
234
+ return { name: memberName, node: member };
235
+ }
236
+ }
237
+ }
238
+ return undefined;
239
+ };
240
+ const createInjectorMembers = (tree, classInjectors) => {
241
+ const filesToUpdate = new Map();
242
+ classInjectors.forEach((classInfo) => {
243
+ if (!classInfo.needsToCreate) {
244
+ return;
245
+ }
246
+ if (!filesToUpdate.has(classInfo.filePath)) {
247
+ filesToUpdate.set(classInfo.filePath, []);
248
+ }
249
+ filesToUpdate.get(classInfo.filePath).push(classInfo);
250
+ });
251
+ filesToUpdate.forEach((classes, filePath) => {
252
+ const content = tree.read(filePath, 'utf-8');
253
+ if (!content) {
254
+ return;
255
+ }
256
+ let nextContent = ensureAngularCoreImports(content, ['inject', 'Injector']);
257
+ [...classes]
258
+ .sort((left, right) => right.classStart - left.classStart)
259
+ .forEach((classInfo) => {
260
+ nextContent = addInjectorMemberToClass(nextContent, classInfo);
261
+ });
262
+ tree.write(filePath, nextContent);
263
+ });
264
+ if (filesToUpdate.size > 0) {
265
+ console.log(`\nāœ… Added injector members to ${Array.from(filesToUpdate.values()).flat().length} classes`);
266
+ }
267
+ };
268
+ const addInjectorMemberToClass = (content, classInfo) => {
269
+ const sourceFile = createSourceFile(content);
270
+ let targetClass;
271
+ const visit = (node) => {
272
+ if (ts.isClassDeclaration(node) && node.name?.text === classInfo.className) {
273
+ targetClass = node;
274
+ }
275
+ ts.forEachChild(node, visit);
276
+ };
277
+ visit(sourceFile);
278
+ if (!targetClass) {
279
+ return content;
280
+ }
281
+ const classBodyStart = targetClass.members.length > 0 ? targetClass.members[0].getStart(sourceFile) : targetClass.getEnd() - 1;
282
+ const indentation = getIndentation(content, classBodyStart);
283
+ const injectorMember = `${indentation}private ${classInfo.injectorMemberName} = inject(Injector);\n\n`;
284
+ return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
285
+ };
286
+ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo) => {
287
+ const updatedFiles = [];
288
+ const filesByPath = new Map();
289
+ classInjectors.forEach((classInfo) => {
290
+ if (!filesByPath.has(classInfo.filePath)) {
291
+ filesByPath.set(classInfo.filePath, []);
292
+ }
293
+ filesByPath.get(classInfo.filePath).push(classInfo);
294
+ });
295
+ visitNotIgnoredFiles(tree, '', (filePath) => {
296
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts') || filesByPath.has(filePath)) {
297
+ return;
298
+ }
299
+ const content = tree.read(filePath, 'utf-8');
300
+ if (!content || !content.includes('.prepare(')) {
301
+ return;
302
+ }
303
+ const usages = findLegacyCreatorUsages(content, filePath);
304
+ if (usages.some((usage) => usage.context === 'function')) {
305
+ filesByPath.set(filePath, []);
306
+ }
307
+ });
308
+ filesByPath.forEach((classes, filePath) => {
309
+ let content = tree.read(filePath, 'utf-8');
310
+ if (!content) {
311
+ return;
312
+ }
313
+ content = ensureAngularCoreImports(content, ['inject', 'Injector']);
314
+ const nextContent = transformPrepareCallsInFile(content, classes, pollingInfo);
315
+ if (nextContent !== content) {
316
+ tree.write(filePath, nextContent);
317
+ updatedFiles.push(filePath);
318
+ }
319
+ });
320
+ if (updatedFiles.length > 0) {
321
+ console.log(`\nāœ… Transformed legacy query creator prepare() calls in ${updatedFiles.length} files`);
322
+ }
323
+ };
324
+ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
325
+ const sourceFile = createSourceFile(content);
326
+ const replacements = [];
327
+ const legacyCreatorNames = findLegacyCreatorNames(sourceFile);
328
+ const classNameToInjector = new Map();
329
+ const functionsNeedingInjector = new Map();
330
+ if (legacyCreatorNames.size === 0) {
331
+ return content;
332
+ }
333
+ classes.forEach((classInfo) => {
334
+ classNameToInjector.set(classInfo.className, classInfo.injectorMemberName);
335
+ });
336
+ const visit = (node) => {
337
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
338
+ const propertyAccess = node.expression;
339
+ if (ts.isIdentifier(propertyAccess.name) && propertyAccess.name.text === 'prepare') {
340
+ const creatorName = ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression.text : undefined;
341
+ if (!creatorName || !legacyCreatorNames.has(creatorName)) {
342
+ ts.forEachChild(node, visit);
343
+ return;
344
+ }
345
+ const context = determineUsageContext(node);
346
+ if (['queryComputed', 'constructor', 'class-field'].includes(context)) {
347
+ ts.forEachChild(node, visit);
348
+ return;
349
+ }
350
+ const containingClass = findContainingClass(node);
351
+ const containingFunction = findContainingStandaloneFunction(node);
352
+ if (containingClass) {
353
+ const className = containingClass.name?.text;
354
+ if (!className) {
355
+ ts.forEachChild(node, visit);
356
+ return;
357
+ }
358
+ const injectorMember = classNameToInjector.get(className);
359
+ if (!injectorMember) {
360
+ ts.forEachChild(node, visit);
361
+ return;
362
+ }
363
+ const transformedCall = transformSinglePrepareCall(node, `this.${injectorMember}`, sourceFile, pollingInfo);
364
+ if (transformedCall) {
365
+ replacements.push({
366
+ start: node.getStart(sourceFile),
367
+ end: node.getEnd(),
368
+ replacement: transformedCall,
369
+ });
370
+ }
371
+ }
372
+ else if (containingFunction) {
373
+ const functionWithInject = findOutermostFunctionWithInject(containingFunction);
374
+ if (!functionWithInject) {
375
+ ts.forEachChild(node, visit);
376
+ return;
377
+ }
378
+ const functionPosition = functionWithInject.getStart(sourceFile);
379
+ if (!functionsNeedingInjector.has(functionPosition)) {
380
+ const existingInjectorParameter = findInjectorParameter(functionWithInject);
381
+ if (existingInjectorParameter) {
382
+ functionsNeedingInjector.set(functionPosition, {
383
+ injectorName: existingInjectorParameter,
384
+ bodyStart: -1,
385
+ needsDeclaration: false,
386
+ });
387
+ }
388
+ else {
389
+ const bodyStart = getFunctionBodyStart(functionWithInject, sourceFile);
390
+ if (bodyStart !== undefined) {
391
+ functionsNeedingInjector.set(functionPosition, {
392
+ injectorName: 'injector',
393
+ bodyStart,
394
+ needsDeclaration: true,
395
+ });
396
+ }
397
+ }
398
+ }
399
+ const injectorName = functionsNeedingInjector.get(functionPosition)?.injectorName ?? 'injector';
400
+ const transformedCall = transformSinglePrepareCall(node, injectorName, sourceFile, pollingInfo);
401
+ if (transformedCall) {
402
+ replacements.push({
403
+ start: node.getStart(sourceFile),
404
+ end: node.getEnd(),
405
+ replacement: transformedCall,
406
+ });
407
+ }
408
+ }
409
+ }
410
+ }
411
+ ts.forEachChild(node, visit);
412
+ };
413
+ visit(sourceFile);
414
+ let result = content;
415
+ const injectorDeclarations = [];
416
+ functionsNeedingInjector.forEach(({ injectorName, bodyStart, needsDeclaration }) => {
417
+ if (!needsDeclaration || bodyStart <= 0) {
418
+ return;
419
+ }
420
+ injectorDeclarations.push({
421
+ position: bodyStart,
422
+ text: `${getIndentation(content, bodyStart)}const ${injectorName} = inject(Injector);\n`,
423
+ });
424
+ });
425
+ injectorDeclarations.sort((left, right) => right.position - left.position);
426
+ injectorDeclarations.forEach(({ position, text }) => {
427
+ result = result.slice(0, position) + text + result.slice(position);
428
+ });
429
+ [...replacements]
430
+ .sort((left, right) => right.start - left.start)
431
+ .forEach(({ start, end, replacement }) => {
432
+ let adjustment = 0;
433
+ injectorDeclarations.forEach((declaration) => {
434
+ if (declaration.position <= start) {
435
+ adjustment += declaration.text.length;
436
+ }
437
+ });
438
+ result = result.slice(0, start + adjustment) + replacement + result.slice(end + adjustment);
439
+ });
440
+ return result;
441
+ };
442
+ const findInjectorParameter = (func) => {
443
+ for (const parameter of func.parameters) {
444
+ if (!ts.isIdentifier(parameter.name)) {
445
+ continue;
446
+ }
447
+ if (parameter.name.text.toLowerCase() === 'injector') {
448
+ return parameter.name.text;
449
+ }
450
+ if (parameter.type?.getText().includes('Injector')) {
451
+ return parameter.name.text;
452
+ }
453
+ }
454
+ return undefined;
455
+ };
456
+ const getFunctionBodyStart = (func, sourceFile) => {
457
+ if (!func.body || !ts.isBlock(func.body)) {
458
+ return undefined;
459
+ }
460
+ if (func.body.statements.length > 0) {
461
+ return func.body.statements[0].getStart(sourceFile);
462
+ }
463
+ return func.body.getStart(sourceFile) + 1;
464
+ };
465
+ const findOutermostFunctionWithInject = (startFunction) => {
466
+ let current = startFunction;
467
+ let outermostWithInject;
468
+ while (current) {
469
+ if (ts.isClassDeclaration(current)) {
470
+ break;
471
+ }
472
+ if ((ts.isFunctionDeclaration(current) || ts.isArrowFunction(current) || ts.isFunctionExpression(current)) &&
473
+ containsInjectCall(current)) {
474
+ outermostWithInject = current;
475
+ }
476
+ current = current.parent;
477
+ }
478
+ return outermostWithInject;
479
+ };
480
+ const findContainingStandaloneFunction = (node) => {
481
+ let current = node;
482
+ while (current) {
483
+ if (ts.isClassDeclaration(current)) {
484
+ return undefined;
485
+ }
486
+ if (ts.isFunctionDeclaration(current) || ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
487
+ return current;
488
+ }
489
+ current = current.parent;
490
+ }
491
+ return undefined;
492
+ };
493
+ const findContainingClass = (node) => {
494
+ let current = node;
495
+ while (current) {
496
+ if (ts.isClassDeclaration(current)) {
497
+ return current;
498
+ }
499
+ current = current.parent;
500
+ }
501
+ return undefined;
502
+ };
503
+ const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pollingInfo) => {
504
+ const propertyAccess = callNode.expression;
505
+ const creatorName = propertyAccess.expression.text;
506
+ const existingArgument = callNode.arguments.length > 0 ? callNode.arguments[0] : undefined;
507
+ const nextArgumentProperties = [];
508
+ let existingConfig;
509
+ let needsSpread = false;
510
+ let spreadExpression;
511
+ if (existingArgument) {
512
+ if (ts.isObjectLiteralExpression(existingArgument)) {
513
+ existingArgument.properties.forEach((property) => {
514
+ if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
515
+ if (property.name.text === 'config') {
516
+ existingConfig = property;
517
+ return;
518
+ }
519
+ nextArgumentProperties.push(`${property.name.text}: ${property.initializer.getText(sourceFile)}`);
520
+ return;
521
+ }
522
+ if (ts.isShorthandPropertyAssignment(property)) {
523
+ nextArgumentProperties.push(property.name.text);
524
+ return;
525
+ }
526
+ if (ts.isSpreadAssignment(property)) {
527
+ needsSpread = true;
528
+ spreadExpression = property.expression.getText(sourceFile);
529
+ }
530
+ });
531
+ }
532
+ else {
533
+ needsSpread = true;
534
+ spreadExpression = existingArgument.getText(sourceFile);
535
+ }
536
+ }
537
+ nextArgumentProperties.push(`injector: ${injectorReference}`);
538
+ const queryVariableName = findQueryVariableNameForPrepareCall(callNode, sourceFile);
539
+ const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo);
540
+ if (existingConfig && ts.isObjectLiteralExpression(existingConfig.initializer)) {
541
+ const configProperties = [];
542
+ existingConfig.initializer.properties.forEach((property) => {
543
+ if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
544
+ configProperties.push(`${property.name.text}: ${property.initializer.getText(sourceFile)}`);
545
+ }
546
+ if (ts.isShorthandPropertyAssignment(property)) {
547
+ configProperties.push(property.name.text);
548
+ }
549
+ });
550
+ if (addDestroyOnResponse) {
551
+ configProperties.push('destroyOnResponse: true');
552
+ }
553
+ nextArgumentProperties.push(configProperties.length > 2
554
+ ? `config: {\n ${configProperties.join(',\n ')}\n }`
555
+ : `config: { ${configProperties.join(', ')} }`);
556
+ }
557
+ else if (existingConfig) {
558
+ nextArgumentProperties.push(`config: ${existingConfig.initializer.getText(sourceFile)}`);
559
+ }
560
+ else if (addDestroyOnResponse) {
561
+ nextArgumentProperties.push('config: { destroyOnResponse: true }');
562
+ }
563
+ const nextArgumentString = needsSpread && spreadExpression
564
+ ? nextArgumentProperties.length <= 2 && !nextArgumentProperties.some((property) => property.includes('\n'))
565
+ ? `{ ...${spreadExpression}, ${nextArgumentProperties.join(', ')} }`
566
+ : `{\n ...${spreadExpression},\n ${nextArgumentProperties.join(',\n ')}\n}`
567
+ : nextArgumentProperties.length <= 2 && !nextArgumentProperties.some((property) => property.includes('\n'))
568
+ ? `{ ${nextArgumentProperties.join(', ')} }`
569
+ : `{\n ${nextArgumentProperties.join(',\n ')}\n}`;
570
+ return `${creatorName}.prepare(${nextArgumentString})`;
571
+ };
572
+ const findQueryVariableNameForPrepareCall = (prepareCallNode, sourceFile) => {
573
+ let parent = prepareCallNode.parent;
574
+ while (parent) {
575
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
576
+ return parent.name.text;
577
+ }
578
+ if (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name)) {
579
+ return parent.name.text;
580
+ }
581
+ if (ts.isReturnStatement(parent)) {
582
+ return findContainingMethod(parent);
583
+ }
584
+ parent = parent.parent;
585
+ }
586
+ return undefined;
587
+ };
588
+ const findContainingMethod = (node) => {
589
+ let current = node;
590
+ while (current) {
591
+ if (ts.isMethodDeclaration(current) && ts.isIdentifier(current.name)) {
592
+ return current.name.text;
593
+ }
594
+ if (ts.isFunctionDeclaration(current) && current.name && ts.isIdentifier(current.name)) {
595
+ return current.name.text;
596
+ }
597
+ if (ts.isArrowFunction(current)) {
598
+ const parent = current.parent;
599
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
600
+ return parent.name.text;
601
+ }
602
+ }
603
+ current = current.parent;
604
+ }
605
+ return undefined;
606
+ };
607
+ const reportManualReviewLocations = (tree, allUsages, report) => {
608
+ allUsages.forEach((usage) => {
609
+ if (usage.context === 'function') {
610
+ const content = tree.read(usage.filePath, 'utf-8');
611
+ if (!content) {
612
+ return;
613
+ }
614
+ const sourceFile = createSourceFile(content, usage.filePath);
615
+ let foundFunctionWithoutInject = false;
616
+ const visit = (node) => {
617
+ if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
618
+ const isUsageInsideNode = usage.position.start >= node.getStart(sourceFile) && usage.position.end <= node.getEnd();
619
+ if (isUsageInsideNode && !containsInjectCall(node) && !findOutermostFunctionWithInject(node)) {
620
+ foundFunctionWithoutInject = true;
621
+ }
622
+ }
623
+ ts.forEachChild(node, visit);
624
+ };
625
+ visit(sourceFile);
626
+ if (foundFunctionWithoutInject) {
627
+ report.addManualReview({
628
+ title: `Review standalone prepare() usage for ${usage.creatorName}`,
629
+ summary: `${usage.creatorName}.prepare() is used in a standalone function without an obvious injection context.`,
630
+ action: 'Pass an Injector explicitly, move the call into queryComputed(), or wrap the logic in a function that already uses inject().',
631
+ locations: [{ filePath: usage.filePath, line: usage.line }],
632
+ source: 'legacy-prepare-migration',
633
+ dedupeKey: `manual-review:${usage.filePath}:${usage.line}:${usage.creatorName}`,
634
+ });
635
+ }
636
+ }
637
+ if (usage.context === 'unknown') {
638
+ report.addManualReview({
639
+ title: `Verify execution context for ${usage.creatorName}`,
640
+ summary: `The migration could not classify the execution context of ${usage.creatorName}.prepare().`,
641
+ action: 'Inspect the surrounding code and confirm that the generated injector handling and destroyOnResponse behavior are correct.',
642
+ locations: [{ filePath: usage.filePath, line: usage.line }],
643
+ source: 'legacy-prepare-migration',
644
+ dedupeKey: `unknown-context:${usage.filePath}:${usage.line}:${usage.creatorName}`,
645
+ });
646
+ }
647
+ });
648
+ };
649
+ const containsInjectCall = (node) => {
650
+ let hasInject = false;
651
+ const visit = (currentNode) => {
652
+ if (hasInject) {
653
+ return;
654
+ }
655
+ if (ts.isCallExpression(currentNode) && ts.isIdentifier(currentNode.expression)) {
656
+ if (currentNode.expression.text === 'inject' || currentNode.expression.text.startsWith('inject')) {
657
+ hasInject = true;
658
+ return;
659
+ }
660
+ }
661
+ ts.forEachChild(currentNode, visit);
662
+ };
663
+ visit(node);
664
+ return hasInject;
665
+ };
666
+ const detectPollingUsage = (tree) => {
667
+ const pollingInfo = new Map();
668
+ visitNotIgnoredFiles(tree, '', (filePath) => {
669
+ if (filePath.endsWith('.spec.ts')) {
670
+ return;
671
+ }
672
+ const content = tree.read(filePath, 'utf-8');
673
+ if (!content) {
674
+ return;
675
+ }
676
+ if (filePath.endsWith('.ts')) {
677
+ detectPollingInTypeScript(content, filePath, pollingInfo);
678
+ }
679
+ if (filePath.endsWith('.html')) {
680
+ detectPollingInTemplate(content, filePath, pollingInfo);
681
+ }
682
+ });
683
+ return pollingInfo;
684
+ };
685
+ const detectPollingInTypeScript = (content, filePath, pollingInfo) => {
686
+ const sourceFile = createSourceFile(content, filePath);
687
+ const visit = (node) => {
688
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
689
+ const methodName = node.expression.name;
690
+ if (ts.isIdentifier(methodName) && (methodName.text === 'poll' || methodName.text === 'stopPolling')) {
691
+ const queryVariableName = getQueryVariableName(node.expression.expression);
692
+ if (queryVariableName) {
693
+ const existing = pollingInfo.get(queryVariableName);
694
+ const location = `${filePath}:${getLineNumber(node, sourceFile)} (.${methodName.text})`;
695
+ if (existing) {
696
+ existing.locations.push(location);
697
+ existing.hasPolling = true;
698
+ }
699
+ else {
700
+ pollingInfo.set(queryVariableName, {
701
+ queryVariableName,
702
+ hasPolling: true,
703
+ locations: [location],
704
+ });
705
+ }
706
+ }
707
+ }
708
+ }
709
+ ts.forEachChild(node, visit);
710
+ };
711
+ visit(sourceFile);
712
+ };
713
+ const getQueryVariableName = (expression) => {
714
+ let currentExpression = expression;
715
+ while (true) {
716
+ if (ts.isIdentifier(currentExpression)) {
717
+ return currentExpression.text;
718
+ }
719
+ if (ts.isPropertyAccessExpression(currentExpression)) {
720
+ currentExpression = currentExpression.expression;
721
+ continue;
722
+ }
723
+ if (ts.isCallExpression(currentExpression)) {
724
+ currentExpression = currentExpression.expression;
725
+ continue;
726
+ }
727
+ return undefined;
728
+ }
729
+ };
730
+ const detectPollingInTemplate = (content, filePath, pollingInfo) => {
731
+ const pollRegex = /(\w+)\.poll\(\)/g;
732
+ const stopPollingRegex = /(\w+)\.stopPolling\(\)/g;
733
+ let match;
734
+ while ((match = pollRegex.exec(content)) !== null) {
735
+ const queryVariableName = match[1];
736
+ const location = `${filePath}:${getLineNumberFromPosition(content, match.index)} (.poll() in template)`;
737
+ const existing = pollingInfo.get(queryVariableName);
738
+ if (existing) {
739
+ existing.locations.push(location);
740
+ existing.hasPolling = true;
741
+ }
742
+ else {
743
+ pollingInfo.set(queryVariableName, {
744
+ queryVariableName,
745
+ hasPolling: true,
746
+ locations: [location],
747
+ });
748
+ }
749
+ }
750
+ while ((match = stopPollingRegex.exec(content)) !== null) {
751
+ const queryVariableName = match[1];
752
+ const location = `${filePath}:${getLineNumberFromPosition(content, match.index)} (.stopPolling() in template)`;
753
+ const existing = pollingInfo.get(queryVariableName);
754
+ if (existing) {
755
+ existing.locations.push(location);
756
+ existing.hasPolling = true;
757
+ }
758
+ else {
759
+ pollingInfo.set(queryVariableName, {
760
+ queryVariableName,
761
+ hasPolling: true,
762
+ locations: [location],
763
+ });
764
+ }
765
+ }
766
+ };
767
+ const shouldAddDestroyOnResponse = (queryVariableName, pollingInfo) => {
768
+ if (!queryVariableName) {
769
+ return false;
770
+ }
771
+ return !pollingInfo.get(queryVariableName)?.hasPolling;
772
+ };
773
+ //# sourceMappingURL=legacy-prepare-migration.js.map