@ethlete/query 5.41.5 → 5.42.1

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,2952 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = migrate;
4
+ const tslib_1 = require("tslib");
5
+ const devkit_1 = require("@nx/devkit");
6
+ const ts = tslib_1.__importStar(require("typescript"));
7
+ async function migrate(tree, schema) {
8
+ const queryClientFiles = new Map(); // filepath -> config names
9
+ const variableRenames = new Map(); // old name -> new name
10
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
11
+ if (!filePath.endsWith('.ts'))
12
+ return;
13
+ const content = tree.read(filePath, 'utf-8');
14
+ if (!content)
15
+ return;
16
+ let modified = false;
17
+ let newContent = content;
18
+ if (needsExperimentalQueryImport(content)) {
19
+ newContent = addExperimentalQueryImport(newContent);
20
+ modified = true;
21
+ }
22
+ if (hasQueryClientInstantiation(content)) {
23
+ const renames = new Map();
24
+ newContent = migrateQueryClientToConfig(newContent, renames);
25
+ modified = true;
26
+ // Store the renames for updating imports in other files
27
+ renames.forEach((newName, oldName) => {
28
+ variableRenames.set(oldName, newName);
29
+ });
30
+ const configNames = extractClientConfigNames(newContent);
31
+ if (configNames.length > 0) {
32
+ queryClientFiles.set(filePath, configNames);
33
+ }
34
+ }
35
+ if (modified) {
36
+ tree.write(filePath, newContent);
37
+ }
38
+ });
39
+ if (queryClientFiles.size > 0) {
40
+ console.log('\n✅ Migrated QueryClient instantiations in:');
41
+ queryClientFiles.forEach((configs, file) => {
42
+ console.log(` - ${file} (${configs.join(', ')})`);
43
+ });
44
+ // Generate query creators for the configs
45
+ generateQueryCreators(tree, queryClientFiles);
46
+ // Generate inject functions for the configs
47
+ generateInjectFunctions(tree, queryClientFiles);
48
+ // Create new query creators based on legacy ones (this also creates legacy wrappers)
49
+ createNewQueryCreators(tree, queryClientFiles);
50
+ // Update imports to include legacy wrappers where needed
51
+ updateLegacyCreatorImportsAndUsages(tree, queryClientFiles);
52
+ // Find and update dependent apps
53
+ updateDependentApps(tree, queryClientFiles);
54
+ }
55
+ // Update imports in all files
56
+ if (variableRenames.size > 0) {
57
+ updateImportsAcrossWorkspace(tree, variableRenames);
58
+ }
59
+ // Replace AnyQuery with E.AnyLegacyQuery everywhere
60
+ replaceAnyQueryWithLegacy(tree);
61
+ // Remove devtools usage
62
+ removeDevtoolsUsage(tree);
63
+ // Migrate empty .prepare() calls to .prepare({})
64
+ migrateEmptyPrepareCalls(tree);
65
+ // Check for legacy query creator usage (step 1: just collect and report)
66
+ checkLegacyQueryCreatorUsage(tree);
67
+ if (!schema.skipFormat) {
68
+ await (0, devkit_1.formatFiles)(tree);
69
+ }
70
+ }
71
+ //#endregion
72
+ //#region import ExperimentalQuery as E
73
+ function needsExperimentalQueryImport(content) {
74
+ // Check if file imports from @ethlete/query
75
+ const hasQueryImport = /from\s+['"]@ethlete\/query['"]/.test(content);
76
+ // Check if already has ExperimentalQuery import
77
+ const hasExperimentalQueryImport = /import\s*{[^}]*ExperimentalQuery/.test(content);
78
+ return hasQueryImport && !hasExperimentalQueryImport;
79
+ }
80
+ function addExperimentalQueryImport(content) {
81
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
82
+ let queryImportNode;
83
+ ts.forEachChild(sourceFile, (node) => {
84
+ if (ts.isImportDeclaration(node) &&
85
+ ts.isStringLiteral(node.moduleSpecifier) &&
86
+ node.moduleSpecifier.text === '@ethlete/query') {
87
+ queryImportNode = node;
88
+ }
89
+ });
90
+ if (!queryImportNode || !queryImportNode.importClause?.namedBindings) {
91
+ return content;
92
+ }
93
+ const namedBindings = queryImportNode.importClause.namedBindings;
94
+ if (!ts.isNamedImports(namedBindings)) {
95
+ return content;
96
+ }
97
+ const importStart = queryImportNode.getStart(sourceFile);
98
+ const importEnd = queryImportNode.getEnd();
99
+ const existingImports = namedBindings.elements.map((el) => el.getText(sourceFile));
100
+ const newImports = [...existingImports, 'ExperimentalQuery as E'];
101
+ const newImportStatement = `import { ${newImports.join(', ')} } from '@ethlete/query';`;
102
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
103
+ }
104
+ //#endregion
105
+ //#region Query client to config
106
+ function hasQueryClientInstantiation(content) {
107
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
108
+ let hasQueryClientImport = false;
109
+ let hasNewQueryClient = false;
110
+ ts.forEachChild(sourceFile, function visit(node) {
111
+ if (ts.isImportDeclaration(node) &&
112
+ ts.isStringLiteral(node.moduleSpecifier) &&
113
+ node.moduleSpecifier.text === '@ethlete/query' &&
114
+ node.importClause?.namedBindings &&
115
+ ts.isNamedImports(node.importClause.namedBindings)) {
116
+ const hasQueryClient = node.importClause.namedBindings.elements.some((el) => el.name.text === 'QueryClient');
117
+ if (hasQueryClient) {
118
+ hasQueryClientImport = true;
119
+ }
120
+ }
121
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'QueryClient') {
122
+ hasNewQueryClient = true;
123
+ }
124
+ ts.forEachChild(node, visit);
125
+ });
126
+ return hasQueryClientImport && hasNewQueryClient;
127
+ }
128
+ function migrateQueryClientToConfig(content, renamesOut) {
129
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
130
+ let result = content;
131
+ const replacements = [];
132
+ const variableRenames = new Map(); // old name -> new name
133
+ function visit(node) {
134
+ // Find: new QueryClient({ baseRoute: '...' })
135
+ if (ts.isNewExpression(node) &&
136
+ ts.isIdentifier(node.expression) &&
137
+ node.expression.text === 'QueryClient' &&
138
+ node.arguments &&
139
+ node.arguments.length > 0) {
140
+ const configArg = node.arguments[0];
141
+ if (ts.isObjectLiteralExpression(configArg)) {
142
+ const oldVariableName = getVariableNameForQueryClient(node, sourceFile);
143
+ const newVariableName = ensureConfigSuffix(oldVariableName);
144
+ if (oldVariableName !== newVariableName) {
145
+ variableRenames.set(oldVariableName, newVariableName);
146
+ if (renamesOut) {
147
+ renamesOut.set(oldVariableName, newVariableName);
148
+ }
149
+ }
150
+ const migrated = migrateConfigObject(configArg, node, sourceFile);
151
+ replacements.push({
152
+ start: node.getStart(sourceFile),
153
+ end: node.getEnd(),
154
+ replacement: migrated,
155
+ });
156
+ }
157
+ }
158
+ ts.forEachChild(node, visit);
159
+ }
160
+ visit(sourceFile);
161
+ // Apply replacements in reverse order to maintain positions
162
+ replacements.sort((a, b) => b.start - a.start);
163
+ for (const { start, end, replacement } of replacements) {
164
+ result = result.slice(0, start) + replacement + result.slice(end);
165
+ }
166
+ // Remove QueryClient from imports
167
+ result = removeQueryClientImport(result);
168
+ // Rename variables throughout the file
169
+ result = renameVariables(result, variableRenames);
170
+ return result;
171
+ }
172
+ function ensureConfigSuffix(name) {
173
+ // If already ends with Config, return as is
174
+ if (/Config$/i.test(name)) {
175
+ return name;
176
+ }
177
+ // Just add Config to the end, keeping everything including "Client"
178
+ return `${name}Config`;
179
+ }
180
+ function removeQueryClientImport(content) {
181
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
182
+ let queryImportNode;
183
+ ts.forEachChild(sourceFile, (node) => {
184
+ if (ts.isImportDeclaration(node) &&
185
+ ts.isStringLiteral(node.moduleSpecifier) &&
186
+ node.moduleSpecifier.text === '@ethlete/query') {
187
+ queryImportNode = node;
188
+ }
189
+ });
190
+ if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
191
+ return content;
192
+ }
193
+ const namedBindings = queryImportNode.importClause.namedBindings;
194
+ const elements = namedBindings.elements.filter((el) => el.name.text !== 'QueryClient');
195
+ // If no imports left, remove the entire import statement
196
+ if (elements.length === 0) {
197
+ const importStart = queryImportNode.getStart(sourceFile);
198
+ const importEnd = queryImportNode.getEnd();
199
+ // Also remove the newline after the import
200
+ const nextChar = content[importEnd];
201
+ const endPos = nextChar === '\n' ? importEnd + 1 : importEnd;
202
+ return content.slice(0, importStart) + content.slice(endPos);
203
+ }
204
+ // Otherwise, reconstruct import without QueryClient
205
+ const importStart = queryImportNode.getStart(sourceFile);
206
+ const importEnd = queryImportNode.getEnd();
207
+ const newImports = elements.map((el) => el.getText(sourceFile));
208
+ const newImportStatement = `import { ${newImports.join(', ')} } from '@ethlete/query';`;
209
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
210
+ }
211
+ function renameVariables(content, renames) {
212
+ if (renames.size === 0)
213
+ return content;
214
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
215
+ const replacements = [];
216
+ function visit(node) {
217
+ // Rename variable declarations
218
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
219
+ const oldName = node.name.text;
220
+ const newName = renames.get(oldName);
221
+ if (newName) {
222
+ replacements.push({
223
+ start: node.name.getStart(sourceFile),
224
+ end: node.name.getEnd(),
225
+ replacement: newName,
226
+ });
227
+ }
228
+ }
229
+ else if (ts.isIdentifier(node)) {
230
+ const oldName = node.text;
231
+ const newName = renames.get(oldName);
232
+ if (newName) {
233
+ // Make sure we're not renaming property names in object literals
234
+ const parent = node.parent;
235
+ if (!ts.isPropertyAssignment(parent) || parent.name !== node) {
236
+ replacements.push({
237
+ start: node.getStart(sourceFile),
238
+ end: node.getEnd(),
239
+ replacement: newName,
240
+ });
241
+ }
242
+ }
243
+ }
244
+ ts.forEachChild(node, visit);
245
+ }
246
+ visit(sourceFile);
247
+ // Apply replacements in reverse order
248
+ let result = content;
249
+ // remove duplicates from replacements
250
+ const seen = new Set();
251
+ const uniqueReplacements = replacements.filter((r) => {
252
+ const key = `${r.start}-${r.end}`;
253
+ if (seen.has(key))
254
+ return false;
255
+ seen.add(key);
256
+ return true;
257
+ });
258
+ uniqueReplacements.sort((a, b) => b.start - a.start);
259
+ for (const { start, end, replacement } of uniqueReplacements) {
260
+ result = result.slice(0, start) + replacement + result.slice(end);
261
+ }
262
+ return result;
263
+ }
264
+ function migrateConfigObject(configArg, node, sourceFile) {
265
+ const variableName = getVariableNameForQueryClient(node, sourceFile);
266
+ const newConfig = [];
267
+ // Extract baseRoute -> baseUrl
268
+ const baseRouteProp = configArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'baseRoute');
269
+ if (baseRouteProp) {
270
+ const baseRouteValue = baseRouteProp.initializer.getText(sourceFile);
271
+ const oldBseUrlAsCast = 'as `https://${string}`';
272
+ const cleanedBaseRouteValue = baseRouteValue.replace(oldBseUrlAsCast, '').trim();
273
+ newConfig.push(`baseUrl: ${cleanedBaseRouteValue}`);
274
+ }
275
+ newConfig.push(`name: '${variableName}'`);
276
+ // Extract request.queryParams -> queryString
277
+ const requestProp = configArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'request');
278
+ if (requestProp && ts.isObjectLiteralExpression(requestProp.initializer)) {
279
+ const requestObj = requestProp.initializer;
280
+ const queryParamsProp = requestObj.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'queryParams');
281
+ if (queryParamsProp) {
282
+ const queryParamsValue = queryParamsProp.initializer.getText(sourceFile);
283
+ newConfig.push(`queryString: ${queryParamsValue}`);
284
+ }
285
+ const cacheAdapterProp = requestObj.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'cacheAdapter');
286
+ if (cacheAdapterProp) {
287
+ const cacheAdapterValue = cacheAdapterProp.initializer.getText(sourceFile);
288
+ newConfig.push(`cacheAdapter: ${cacheAdapterValue}`);
289
+ }
290
+ const retryFnProp = requestObj.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'retryFn');
291
+ if (retryFnProp) {
292
+ const retryFnValue = retryFnProp.initializer.getText(sourceFile);
293
+ newConfig.push(`retryFn: ${retryFnValue}`);
294
+ }
295
+ }
296
+ return `E.createQueryClientConfig({\n ${newConfig.join(',\n ')}\n})`;
297
+ }
298
+ function getVariableNameForQueryClient(node, sourceFile) {
299
+ let parent = node.parent;
300
+ // Walk up to find variable declaration
301
+ while (parent) {
302
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
303
+ return parent.name.text;
304
+ }
305
+ parent = parent.parent;
306
+ }
307
+ return 'client';
308
+ }
309
+ function extractClientConfigNames(content) {
310
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
311
+ const configNames = [];
312
+ function visit(node) {
313
+ // Find: const XXX = E.createQueryClientConfig(...)
314
+ if (ts.isVariableDeclaration(node) &&
315
+ ts.isIdentifier(node.name) &&
316
+ node.initializer &&
317
+ ts.isCallExpression(node.initializer)) {
318
+ const callExpr = node.initializer;
319
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
320
+ ts.isIdentifier(callExpr.expression.expression) &&
321
+ callExpr.expression.expression.text === 'E' &&
322
+ ts.isIdentifier(callExpr.expression.name) &&
323
+ callExpr.expression.name.text === 'createQueryClientConfig') {
324
+ // This extracts the NEW variable name (apiClientConfig)
325
+ // But we should extract the name from the config object instead!
326
+ const nameArg = callExpr.arguments[0];
327
+ if (nameArg && ts.isObjectLiteralExpression(nameArg)) {
328
+ const nameProp = nameArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'name');
329
+ if (nameProp && ts.isStringLiteral(nameProp.initializer)) {
330
+ configNames.push(nameProp.initializer.text); // Extract from 'name' property
331
+ }
332
+ }
333
+ }
334
+ }
335
+ ts.forEachChild(node, visit);
336
+ }
337
+ visit(sourceFile);
338
+ return configNames;
339
+ }
340
+ //#endregion
341
+ //#region Add query client providers to apps
342
+ function updateDependentApps(tree, queryClientFiles) {
343
+ const projects = (0, devkit_1.getProjects)(tree);
344
+ const updatedApps = [];
345
+ const warnedApps = [];
346
+ // Find all apps
347
+ for (const [projectName, projectConfig] of projects.entries()) {
348
+ if (projectConfig.projectType !== 'application')
349
+ continue;
350
+ const configPaths = [`${projectConfig.root}/src/app/app.config.ts`, `${projectConfig.root}/src/main.ts`];
351
+ let foundImport = false;
352
+ for (const configPath of configPaths) {
353
+ if (!tree.exists(configPath))
354
+ continue;
355
+ const content = tree.read(configPath, 'utf-8');
356
+ if (!content)
357
+ continue;
358
+ // Check if this app imports any of the migrated client configs
359
+ const importedConfigs = findImportedClientConfigs(content, queryClientFiles);
360
+ if (importedConfigs.length > 0) {
361
+ const newContent = addQueryClientProviders(content, importedConfigs);
362
+ if (newContent !== content) {
363
+ tree.write(configPath, newContent);
364
+ }
365
+ updatedApps.push(projectName);
366
+ foundImport = true;
367
+ break;
368
+ }
369
+ }
370
+ if (!foundImport) {
371
+ warnedApps.push(projectName);
372
+ }
373
+ }
374
+ if (updatedApps.length > 0) {
375
+ console.log('\n✅ Updated applications with query client providers:');
376
+ updatedApps.forEach((app) => console.log(` - ${app}`));
377
+ }
378
+ if (warnedApps.length > 0) {
379
+ console.warn('\n⚠️ The following applications may need manual updates for query client providers:');
380
+ warnedApps.forEach((app) => console.warn(` - ${app}`));
381
+ }
382
+ }
383
+ function findImportedClientConfigs(content, queryClientFiles) {
384
+ const importedConfigs = [];
385
+ // Get all config names from migrated files (these are the OLD names like 'apiClient')
386
+ const allConfigNames = new Set();
387
+ queryClientFiles.forEach((configs) => configs.forEach((c) => allConfigNames.add(c)));
388
+ // Check if any of these configs are imported in this file
389
+ for (const configName of allConfigNames) {
390
+ const importRegex = new RegExp(`\\b${configName}\\b`);
391
+ if (importRegex.test(content)) {
392
+ // We need to add the Config suffix because the actual variable name is apiClientConfig
393
+ const configNameWithSuffix = ensureConfigSuffix(configName);
394
+ importedConfigs.push(configNameWithSuffix);
395
+ }
396
+ }
397
+ return importedConfigs;
398
+ }
399
+ function addQueryClientProviders(content, clientConfigs) {
400
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
401
+ let providersArrayNode;
402
+ function visit(node) {
403
+ if (ts.isPropertyAssignment(node) &&
404
+ ts.isIdentifier(node.name) &&
405
+ node.name.text === 'providers' &&
406
+ ts.isArrayLiteralExpression(node.initializer)) {
407
+ providersArrayNode = node.initializer;
408
+ }
409
+ ts.forEachChild(node, visit);
410
+ }
411
+ visit(sourceFile);
412
+ if (!providersArrayNode)
413
+ return content;
414
+ const newProviders = clientConfigs.map((config) => `E.provideQueryClient(${config})`);
415
+ const elements = providersArrayNode.elements;
416
+ if (elements.length > 0) {
417
+ const insertPosition = elements[elements.length - 1].getEnd();
418
+ const insertion = `,\n ${newProviders.join(',\n ')}`;
419
+ return content.slice(0, insertPosition) + insertion + content.slice(insertPosition);
420
+ }
421
+ else {
422
+ const insertPosition = providersArrayNode.getStart(sourceFile) + 1;
423
+ const insertion = `\n ${newProviders.join(',\n ')}\n `;
424
+ return content.slice(0, insertPosition) + insertion + content.slice(insertPosition);
425
+ }
426
+ }
427
+ //#endregion
428
+ //#region Update imports across workspace
429
+ function updateImportsAcrossWorkspace(tree, renames) {
430
+ const updatedFiles = [];
431
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
432
+ if (!filePath.endsWith('.ts'))
433
+ return;
434
+ const content = tree.read(filePath, 'utf-8');
435
+ if (!content)
436
+ return;
437
+ const newContent = updateImportsInFile(content, renames);
438
+ if (newContent !== content) {
439
+ tree.write(filePath, newContent);
440
+ updatedFiles.push(filePath);
441
+ }
442
+ });
443
+ }
444
+ function updateImportsInFile(content, renames) {
445
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
446
+ const replacements = [];
447
+ function visit(node) {
448
+ // Update named imports
449
+ if (ts.isImportDeclaration(node) &&
450
+ node.importClause?.namedBindings &&
451
+ ts.isNamedImports(node.importClause.namedBindings)) {
452
+ const namedBindings = node.importClause.namedBindings;
453
+ let hasChanges = false;
454
+ const newElements = [];
455
+ namedBindings.elements.forEach((element) => {
456
+ const importedName = element.name.text;
457
+ const newName = renames.get(importedName);
458
+ if (newName) {
459
+ hasChanges = true;
460
+ // If there's an alias, keep it: import { old as alias } -> import { new as alias }
461
+ if (element.propertyName) {
462
+ newElements.push(`${newName} as ${element.name.text}`);
463
+ }
464
+ else {
465
+ newElements.push(newName);
466
+ }
467
+ }
468
+ else {
469
+ newElements.push(element.getText(sourceFile));
470
+ }
471
+ });
472
+ if (hasChanges) {
473
+ const moduleSpecifier = node.moduleSpecifier.text;
474
+ const newImportStatement = `import { ${newElements.join(', ')} } from '${moduleSpecifier}';`;
475
+ replacements.push({
476
+ start: node.getStart(sourceFile),
477
+ end: node.getEnd(),
478
+ replacement: newImportStatement,
479
+ });
480
+ }
481
+ }
482
+ ts.forEachChild(node, visit);
483
+ }
484
+ visit(sourceFile);
485
+ // Apply replacements in reverse order
486
+ let result = content;
487
+ replacements.sort((a, b) => b.start - a.start);
488
+ for (const { start, end, replacement } of replacements) {
489
+ result = result.slice(0, start) + replacement + result.slice(end);
490
+ }
491
+ return result;
492
+ }
493
+ //#endregion
494
+ //#region Generate query creator templates for the new query client configs
495
+ function generateQueryCreators(tree, queryClientFiles) {
496
+ const generatedFiles = [];
497
+ for (const [filePath, configNames] of queryClientFiles.entries()) {
498
+ const content = tree.read(filePath, 'utf-8');
499
+ if (!content)
500
+ continue;
501
+ // Generate creators for each config in this file
502
+ const newContent = addQueryCreatorsToFile(content, configNames);
503
+ if (newContent !== content) {
504
+ tree.write(filePath, newContent);
505
+ generatedFiles.push(filePath);
506
+ }
507
+ }
508
+ if (generatedFiles.length > 0) {
509
+ console.log('\n✅ Generated query creators in:');
510
+ generatedFiles.forEach((file) => console.log(` - ${file}`));
511
+ }
512
+ }
513
+ function addQueryCreatorsToFile(content, configNames) {
514
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
515
+ // Find where each config is declared
516
+ const configPositions = new Map();
517
+ function visit(node) {
518
+ if (ts.isVariableDeclaration(node) &&
519
+ ts.isIdentifier(node.name) &&
520
+ node.initializer &&
521
+ ts.isCallExpression(node.initializer)) {
522
+ const callExpr = node.initializer;
523
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
524
+ ts.isIdentifier(callExpr.expression.expression) &&
525
+ callExpr.expression.expression.text === 'E' &&
526
+ ts.isIdentifier(callExpr.expression.name) &&
527
+ callExpr.expression.name.text === 'createQueryClientConfig') {
528
+ const configName = node.name.text;
529
+ // Find the variable statement (const/let/var line)
530
+ let parent = node.parent;
531
+ while (parent && !ts.isVariableStatement(parent)) {
532
+ parent = parent.parent;
533
+ }
534
+ if (parent) {
535
+ configPositions.set(configName, parent.getEnd());
536
+ }
537
+ }
538
+ }
539
+ ts.forEachChild(node, visit);
540
+ }
541
+ visit(sourceFile);
542
+ // Generate creators for each config and insert after the config declaration
543
+ let result = content;
544
+ const insertions = [];
545
+ for (const configName of configNames) {
546
+ const position = configPositions.get(`${configName}Config`);
547
+ if (!position)
548
+ continue;
549
+ const creators = generateCreatorsForConfig(configName);
550
+ insertions.push({ position, text: `\n\n${creators}` });
551
+ }
552
+ // Apply insertions in reverse order to maintain positions
553
+ insertions.sort((a, b) => b.position - a.position);
554
+ for (const { position, text } of insertions) {
555
+ result = result.slice(0, position) + text + result.slice(position);
556
+ }
557
+ return result;
558
+ }
559
+ function generateCreatorsForConfig(configName) {
560
+ const configNameWithoutClientSuffix = configName.replace('Client', '');
561
+ const creators = [
562
+ `export const ${configNameWithoutClientSuffix}Get = E.createGetQuery(${configName}Config);`,
563
+ `export const ${configNameWithoutClientSuffix}Post = E.createPostQuery(${configName}Config);`,
564
+ `export const ${configNameWithoutClientSuffix}Put = E.createPutQuery(${configName}Config);`,
565
+ `export const ${configNameWithoutClientSuffix}Patch = E.createPatchQuery(${configName}Config);`,
566
+ `export const ${configNameWithoutClientSuffix}Delete = E.createDeleteQuery(${configName}Config);`,
567
+ ];
568
+ return creators.join('\n');
569
+ }
570
+ function toLegacyName(name) {
571
+ // Capitalize first letter after 'legacy'
572
+ const capitalized = name.charAt(0).toUpperCase() + name.slice(1);
573
+ return `legacy${capitalized}`;
574
+ }
575
+ function createNewQueryCreators(tree, queryClientFiles) {
576
+ const createdFiles = [];
577
+ const authProvidersNeeded = new Map(); // clientName -> client file path
578
+ // Build a map of client names to their file paths
579
+ const clientNameToFilePath = new Map();
580
+ queryClientFiles.forEach((configNames, filePath) => {
581
+ configNames.forEach((name) => {
582
+ clientNameToFilePath.set(name, filePath);
583
+ });
584
+ });
585
+ // Step 1: Find all legacy query creators and analyze them
586
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
587
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
588
+ return;
589
+ const content = tree.read(filePath, 'utf-8');
590
+ if (!content)
591
+ return;
592
+ const legacyCreators = analyzeLegacyQueryCreators(content, queryClientFiles);
593
+ if (legacyCreators.length === 0)
594
+ return;
595
+ // Check if any creators need auth and map to their client file
596
+ legacyCreators.forEach((creator) => {
597
+ if (creator.secure) {
598
+ const clientFilePath = clientNameToFilePath.get(creator.clientName);
599
+ if (clientFilePath) {
600
+ authProvidersNeeded.set(creator.clientName, clientFilePath);
601
+ }
602
+ }
603
+ });
604
+ const newContent = transformLegacyQueryCreators(content, legacyCreators, queryClientFiles);
605
+ if (newContent !== content) {
606
+ tree.write(filePath, newContent);
607
+ createdFiles.push(filePath);
608
+ }
609
+ });
610
+ // Step 2: Create auth providers where needed
611
+ if (authProvidersNeeded.size > 0) {
612
+ createAuthProviders(tree, authProvidersNeeded, queryClientFiles);
613
+ }
614
+ if (createdFiles.length > 0) {
615
+ console.log('\n✅ Created new query creators in:');
616
+ createdFiles.forEach((file) => console.log(` - ${file}`));
617
+ }
618
+ }
619
+ function analyzeLegacyQueryCreators(content, queryClientFiles) {
620
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
621
+ const creators = [];
622
+ // Get all old client names
623
+ const oldClientNames = new Set();
624
+ queryClientFiles.forEach((configNames) => {
625
+ configNames.forEach((name) => oldClientNames.add(name));
626
+ });
627
+ function visit(node) {
628
+ // Find: const getUsers = apiClient.get({ ... })
629
+ if (ts.isVariableDeclaration(node) &&
630
+ ts.isIdentifier(node.name) &&
631
+ node.initializer &&
632
+ ts.isCallExpression(node.initializer)) {
633
+ const callExpr = node.initializer;
634
+ if (ts.isPropertyAccessExpression(callExpr.expression)) {
635
+ const objExpr = callExpr.expression.expression;
636
+ const methodName = callExpr.expression.name;
637
+ if (ts.isIdentifier(objExpr) &&
638
+ ts.isIdentifier(methodName) &&
639
+ oldClientNames.has(objExpr.text) &&
640
+ ['get', 'post', 'put', 'patch', 'delete'].includes(methodName.text)) {
641
+ const info = {
642
+ name: node.name.text,
643
+ clientName: objExpr.text,
644
+ method: methodName.text,
645
+ route: '',
646
+ secure: false,
647
+ httpOptions: new Map(),
648
+ position: {
649
+ start: node.getStart(sourceFile),
650
+ end: node.getEnd(),
651
+ },
652
+ };
653
+ // Parse the config object
654
+ if (callExpr.arguments.length > 0 && ts.isObjectLiteralExpression(callExpr.arguments[0])) {
655
+ const configObj = callExpr.arguments[0];
656
+ configObj.properties.forEach((prop) => {
657
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
658
+ return;
659
+ const propName = prop.name.text;
660
+ // Extract route
661
+ if (propName === 'route') {
662
+ info.route = prop.initializer.getText(sourceFile);
663
+ }
664
+ // Extract secure flag
665
+ if (propName === 'secure' && prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
666
+ info.secure = true;
667
+ }
668
+ // Extract types
669
+ if (propName === 'types' && ts.isObjectLiteralExpression(prop.initializer)) {
670
+ prop.initializer.properties.forEach((typeProp) => {
671
+ if (!ts.isPropertyAssignment(typeProp) || !ts.isIdentifier(typeProp.name))
672
+ return;
673
+ const typeName = typeProp.name.text;
674
+ const typeValue = extractTypeFromDef(typeProp.initializer, sourceFile);
675
+ if (typeName === 'args')
676
+ info.typeArgs = typeValue;
677
+ if (typeName === 'response')
678
+ info.typeResponse = typeValue;
679
+ if (typeName === 'body')
680
+ info.typeBody = typeValue;
681
+ });
682
+ }
683
+ // Extract HTTP options
684
+ if (['reportProgress', 'responseType', 'transferCache', 'withCredentials'].includes(propName)) {
685
+ info.httpOptions.set(propName, prop.initializer.getText(sourceFile));
686
+ }
687
+ });
688
+ }
689
+ if (info.route) {
690
+ creators.push(info);
691
+ }
692
+ }
693
+ }
694
+ }
695
+ ts.forEachChild(node, visit);
696
+ }
697
+ visit(sourceFile);
698
+ return creators;
699
+ }
700
+ function extractTypeFromDef(node, sourceFile) {
701
+ // Extract type from def<Type>() call
702
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'def') {
703
+ if (node.typeArguments && node.typeArguments.length > 0) {
704
+ return node.typeArguments[0].getText(sourceFile);
705
+ }
706
+ }
707
+ return undefined;
708
+ }
709
+ function transformLegacyQueryCreators(content, legacyCreators, queryClientFiles) {
710
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
711
+ // Group creators by client
712
+ const creatorsByClient = new Map();
713
+ legacyCreators.forEach((creator) => {
714
+ if (!creatorsByClient.has(creator.clientName)) {
715
+ creatorsByClient.set(creator.clientName, []);
716
+ }
717
+ creatorsByClient.get(creator.clientName).push(creator);
718
+ });
719
+ const replacements = [];
720
+ // Transform each legacy creator
721
+ legacyCreators.forEach((creator) => {
722
+ const newCreatorCode = generateNewQueryCreator(creator, sourceFile);
723
+ const legacyWrapperCode = generateLegacyWrapper(creator);
724
+ const fullReplacement = `${newCreatorCode}\n${legacyWrapperCode}`;
725
+ // Find the variable statement (which includes 'export const')
726
+ let variableStatement;
727
+ function findVariableStatement(node) {
728
+ if (ts.isVariableStatement(node)) {
729
+ const declaration = node.declarationList.declarations[0];
730
+ if (declaration && ts.isIdentifier(declaration.name) && declaration.name.text === creator.name) {
731
+ variableStatement = node;
732
+ }
733
+ }
734
+ ts.forEachChild(node, findVariableStatement);
735
+ }
736
+ findVariableStatement(sourceFile);
737
+ if (variableStatement) {
738
+ replacements.push({
739
+ start: variableStatement.getStart(sourceFile),
740
+ end: variableStatement.getEnd(),
741
+ replacement: fullReplacement,
742
+ });
743
+ }
744
+ });
745
+ // Apply replacements in reverse order
746
+ let result = content;
747
+ replacements.sort((a, b) => b.start - a.start);
748
+ for (const { start, end, replacement } of replacements) {
749
+ result = result.slice(0, start) + replacement + result.slice(end);
750
+ }
751
+ // Update imports to include necessary creator functions
752
+ result = addCreatorImports(result, creatorsByClient);
753
+ return result;
754
+ }
755
+ function addCreatorImports(content, creatorsByClient) {
756
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
757
+ const replacements = [];
758
+ // Build a map of import source -> set of all imports needed from that source
759
+ const importsBySource = new Map();
760
+ creatorsByClient.forEach((creators, clientName) => {
761
+ const clientNameWithoutClient = clientName.replace(/Client$/, '');
762
+ const configName = `${clientName}Config`;
763
+ // Collect unique methods
764
+ const methodsUsed = new Set();
765
+ let hasSecure = false;
766
+ creators.forEach((creator) => {
767
+ methodsUsed.add(creator.method);
768
+ if (creator.secure) {
769
+ hasSecure = true;
770
+ }
771
+ });
772
+ // Build the list of imports needed for this client
773
+ const importsNeeded = new Set();
774
+ importsNeeded.add(configName);
775
+ methodsUsed.forEach((method) => {
776
+ const methodCapitalized = method.charAt(0).toUpperCase() + method.slice(1);
777
+ // Add non-secure creator
778
+ importsNeeded.add(`${clientNameWithoutClient}${methodCapitalized}`);
779
+ // Add secure creator if needed
780
+ if (hasSecure) {
781
+ importsNeeded.add(`${clientNameWithoutClient}${methodCapitalized}Secure`);
782
+ }
783
+ });
784
+ // Store these imports for later (we'll determine the source file when we find the import)
785
+ // For now, associate with the client name
786
+ importsBySource.set(clientName, importsNeeded);
787
+ });
788
+ // Update import statements
789
+ function visit(node) {
790
+ if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
791
+ const moduleSpecifier = node.moduleSpecifier.text;
792
+ // Get existing imports from this statement
793
+ const existingImports = new Set();
794
+ const clientsInThisImport = [];
795
+ if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
796
+ node.importClause.namedBindings.elements.forEach((el) => {
797
+ const importName = el.name.text;
798
+ existingImports.add(importName);
799
+ // Check if this is one of our client names
800
+ if (importsBySource.has(importName)) {
801
+ clientsInThisImport.push(importName);
802
+ }
803
+ });
804
+ }
805
+ if (clientsInThisImport.length === 0) {
806
+ // This import doesn't contain any of our clients
807
+ ts.forEachChild(node, visit);
808
+ return;
809
+ }
810
+ // Accumulate all imports needed from all clients in this import statement
811
+ const allImportsNeeded = new Set();
812
+ clientsInThisImport.forEach((clientName) => {
813
+ const importsForClient = importsBySource.get(clientName);
814
+ if (importsForClient) {
815
+ importsForClient.forEach((imp) => allImportsNeeded.add(imp));
816
+ }
817
+ });
818
+ // Remove the old client names from the final import list
819
+ clientsInThisImport.forEach((clientName) => {
820
+ allImportsNeeded.delete(clientName);
821
+ });
822
+ // Keep other existing imports that aren't client names
823
+ existingImports.forEach((imp) => {
824
+ if (!clientsInThisImport.includes(imp)) {
825
+ allImportsNeeded.add(imp);
826
+ }
827
+ });
828
+ // Create new import statement with all accumulated imports
829
+ const importsList = Array.from(allImportsNeeded).sort().join(', ');
830
+ const newImportStatement = `import { ${importsList} } from '${moduleSpecifier}';`;
831
+ replacements.push({
832
+ start: node.getStart(sourceFile),
833
+ end: node.getEnd(),
834
+ replacement: newImportStatement,
835
+ });
836
+ }
837
+ ts.forEachChild(node, visit);
838
+ }
839
+ visit(sourceFile);
840
+ // Apply replacements in reverse order
841
+ let result = content;
842
+ replacements.sort((a, b) => b.start - a.start);
843
+ for (const { start, end, replacement } of replacements) {
844
+ result = result.slice(0, start) + replacement + result.slice(end);
845
+ }
846
+ return result;
847
+ }
848
+ function generateNewQueryCreator(creator, sourceFile) {
849
+ const clientNameWithoutClient = creator.clientName.replace(/Client$/, '');
850
+ const methodCapitalized = creator.method.charAt(0).toUpperCase() + creator.method.slice(1);
851
+ // Build type parameter
852
+ let typeParam = '';
853
+ if (creator.typeArgs && (creator.typeBody || creator.typeResponse)) {
854
+ // Case 1: Has args + other types -> use intersection
855
+ // Example: GetCollectionsArgs & { response: Paginated<BaseCollectionView> }
856
+ const additionalTypes = [];
857
+ if (creator.typeBody) {
858
+ additionalTypes.push(`body: ${creator.typeBody}`);
859
+ }
860
+ if (creator.typeResponse) {
861
+ additionalTypes.push(`response: ${creator.typeResponse}`);
862
+ }
863
+ typeParam = `<${creator.typeArgs} & { ${additionalTypes.join('; ')} }>`;
864
+ }
865
+ else if (creator.typeArgs) {
866
+ // Case 2: Only args -> use directly
867
+ // Example: PostCollectionAcceptAllWithoutStatusArgs
868
+ typeParam = `<${creator.typeArgs}>`;
869
+ }
870
+ else if (creator.typeBody || creator.typeResponse) {
871
+ // Case 3: No args, but has body/response -> wrap in object
872
+ // Example: { body: CreateUserDto; response: User }
873
+ const typeProps = [];
874
+ if (creator.typeBody) {
875
+ typeProps.push(`body: ${creator.typeBody}`);
876
+ }
877
+ if (creator.typeResponse) {
878
+ typeProps.push(`response: ${creator.typeResponse}`);
879
+ }
880
+ typeParam = `<{ ${typeProps.join('; ')} }>`;
881
+ }
882
+ // Case 4: No types at all -> no type parameter
883
+ // Build options object
884
+ const options = [];
885
+ creator.httpOptions.forEach((value, key) => {
886
+ options.push(`${key}: ${value}`);
887
+ });
888
+ const optionsStr = options.length > 0 ? `, {\n ${options.join(',\n ')}\n}` : '';
889
+ // Choose the right creator function
890
+ const creatorFn = creator.secure
891
+ ? `${clientNameWithoutClient}${methodCapitalized}Secure`
892
+ : `${clientNameWithoutClient}${methodCapitalized}`;
893
+ // Now we include 'export const' since we're replacing the entire statement
894
+ return `export const ${creator.name} = ${creatorFn}${typeParam}(${creator.route}${optionsStr});`;
895
+ }
896
+ function generateLegacyWrapper(creator) {
897
+ const legacyName = toLegacyName(creator.name);
898
+ return `export const ${legacyName} = E.createLegacyQueryCreator({ creator: ${creator.name} });`;
899
+ }
900
+ function createAuthProviders(tree, authProvidersNeeded, queryClientFiles) {
901
+ const createdProviders = [];
902
+ authProvidersNeeded.forEach((filePath, clientName) => {
903
+ const content = tree.read(filePath, 'utf-8');
904
+ if (!content)
905
+ return;
906
+ // Check if auth provider already exists
907
+ const authProviderName = `${clientName}AuthProviderConfig`;
908
+ if (content.includes(authProviderName)) {
909
+ return; // Already exists
910
+ }
911
+ const configName = `${clientName}Config`;
912
+ let newContent = addAuthProviderToFile(content, clientName, configName);
913
+ if (newContent !== content) {
914
+ // Generate secure query creators
915
+ newContent = generateSecureQueryCreators(newContent, clientName);
916
+ // Generate inject function for auth provider
917
+ newContent = addAuthProviderInjectFunction(newContent, clientName);
918
+ tree.write(filePath, newContent);
919
+ createdProviders.push(filePath);
920
+ }
921
+ });
922
+ if (createdProviders.length > 0) {
923
+ console.log('\n✅ Created auth providers in:');
924
+ createdProviders.forEach((file) => console.log(` - ${file}`));
925
+ console.warn('\n⚠️ Make sure to provide them in the respective app.config/main.ts providers array using E.provideBearerAuthProvider(yourConfigName)');
926
+ }
927
+ }
928
+ function addAuthProviderInjectFunction(content, clientName) {
929
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
930
+ const authProviderName = `${clientName}AuthProviderConfig`;
931
+ // Find where the auth provider is declared
932
+ let authProviderPosition;
933
+ function visit(node) {
934
+ if (ts.isVariableDeclaration(node) &&
935
+ ts.isIdentifier(node.name) &&
936
+ node.name.text === authProviderName &&
937
+ node.initializer &&
938
+ ts.isCallExpression(node.initializer)) {
939
+ let parent = node.parent;
940
+ while (parent && !ts.isVariableStatement(parent)) {
941
+ parent = parent.parent;
942
+ }
943
+ if (parent) {
944
+ authProviderPosition = parent.getEnd();
945
+ }
946
+ }
947
+ ts.forEachChild(node, visit);
948
+ }
949
+ visit(sourceFile);
950
+ if (!authProviderPosition)
951
+ return content;
952
+ // Check if inject function already exists
953
+ const injectFunctionName = `inject${clientName.charAt(0).toUpperCase() + clientName.slice(1)}AuthProvider`;
954
+ if (content.includes(injectFunctionName)) {
955
+ return content;
956
+ }
957
+ // Ensure inject is imported
958
+ let result = content;
959
+ const hasInjectImport = result.includes('import { inject }') || (result.includes("from '@angular/core'") && result.includes('inject'));
960
+ if (!hasInjectImport) {
961
+ result = addInjectImport(result);
962
+ }
963
+ // Generate inject function for auth provider
964
+ const injectFunction = `\n\nexport const ${injectFunctionName} = () => inject(${authProviderName}.token);`;
965
+ return result.slice(0, authProviderPosition) + injectFunction + result.slice(authProviderPosition);
966
+ }
967
+ function addAuthProviderToFile(content, clientName, configName) {
968
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
969
+ // Find where the config is declared
970
+ let configPosition;
971
+ function visit(node) {
972
+ if (ts.isVariableDeclaration(node) &&
973
+ ts.isIdentifier(node.name) &&
974
+ node.name.text === configName &&
975
+ node.initializer &&
976
+ ts.isCallExpression(node.initializer)) {
977
+ // Find the variable statement
978
+ let parent = node.parent;
979
+ while (parent && !ts.isVariableStatement(parent)) {
980
+ parent = parent.parent;
981
+ }
982
+ if (parent) {
983
+ configPosition = parent.getEnd();
984
+ }
985
+ }
986
+ ts.forEachChild(node, visit);
987
+ }
988
+ visit(sourceFile);
989
+ if (!configPosition)
990
+ return content;
991
+ const authProviderCode = `
992
+
993
+ export const ${clientName}AuthProviderConfig = E.createBearerAuthProviderConfig({
994
+ name: '${clientName}',
995
+ queryClientRef: ${configName}.token,
996
+ });`;
997
+ return content.slice(0, configPosition) + authProviderCode + content.slice(configPosition);
998
+ }
999
+ function generateSecureQueryCreators(content, clientName) {
1000
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1001
+ // Find where the auth provider is declared
1002
+ let authProviderPosition;
1003
+ const authProviderName = `${clientName}AuthProviderConfig`;
1004
+ function visit(node) {
1005
+ if (ts.isVariableDeclaration(node) &&
1006
+ ts.isIdentifier(node.name) &&
1007
+ node.name.text === authProviderName &&
1008
+ node.initializer &&
1009
+ ts.isCallExpression(node.initializer)) {
1010
+ let parent = node.parent;
1011
+ while (parent && !ts.isVariableStatement(parent)) {
1012
+ parent = parent.parent;
1013
+ }
1014
+ if (parent) {
1015
+ authProviderPosition = parent.getEnd();
1016
+ }
1017
+ }
1018
+ ts.forEachChild(node, visit);
1019
+ }
1020
+ visit(sourceFile);
1021
+ if (!authProviderPosition)
1022
+ return content;
1023
+ const clientNameWithoutClient = clientName.replace('Client', '');
1024
+ const configName = `${clientName}Config`;
1025
+ const secureCreators = [
1026
+ `export const ${clientNameWithoutClient}GetSecure = E.createSecureGetQuery(${configName}, ${authProviderName});`,
1027
+ `export const ${clientNameWithoutClient}PostSecure = E.createSecurePostQuery(${configName}, ${authProviderName});`,
1028
+ `export const ${clientNameWithoutClient}PutSecure = E.createSecurePutQuery(${configName}, ${authProviderName});`,
1029
+ `export const ${clientNameWithoutClient}PatchSecure = E.createSecurePatchQuery(${configName}, ${authProviderName});`,
1030
+ `export const ${clientNameWithoutClient}DeleteSecure = E.createSecureDeleteQuery(${configName}, ${authProviderName});`,
1031
+ ].join('\n');
1032
+ return content.slice(0, authProviderPosition) + '\n\n' + secureCreators + content.slice(authProviderPosition);
1033
+ }
1034
+ //#endregion
1035
+ //#region Update legacy query creator imports and usages
1036
+ function updateLegacyCreatorImportsAndUsages(tree, queryClientFiles) {
1037
+ const legacyCreators = new Map(); // original name -> legacy name
1038
+ const updatedFiles = [];
1039
+ // Step 1: Find all legacy wrappers that were created
1040
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1041
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
1042
+ return;
1043
+ const content = tree.read(filePath, 'utf-8');
1044
+ if (!content)
1045
+ return;
1046
+ const wrappers = findLegacyWrappers(content);
1047
+ wrappers.forEach((legacyName, originalName) => {
1048
+ legacyCreators.set(originalName, legacyName);
1049
+ });
1050
+ });
1051
+ if (legacyCreators.size === 0)
1052
+ return;
1053
+ console.log(`\n✅ Found ${legacyCreators.size} legacy query creator wrappers`);
1054
+ // Step 2: Update imports and usages across the workspace
1055
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1056
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
1057
+ return;
1058
+ const content = tree.read(filePath, 'utf-8');
1059
+ if (!content)
1060
+ return;
1061
+ // Skip files that define the legacy wrappers themselves
1062
+ if (containsLegacyWrapperDefinitions(content)) {
1063
+ return;
1064
+ }
1065
+ let newContent = content;
1066
+ // Update imports to add legacy names
1067
+ newContent = updateLegacyCreatorImports(newContent, legacyCreators);
1068
+ // Update usages to use legacy names
1069
+ newContent = updateLegacyCreatorUsages(newContent, legacyCreators);
1070
+ if (newContent !== content) {
1071
+ tree.write(filePath, newContent);
1072
+ updatedFiles.push(filePath);
1073
+ }
1074
+ });
1075
+ if (updatedFiles.length > 0) {
1076
+ console.log('\n✅ Updated legacy creator imports and usages in:');
1077
+ updatedFiles.forEach((file) => console.log(` - ${file}`));
1078
+ }
1079
+ }
1080
+ function containsLegacyWrapperDefinitions(content) {
1081
+ return content.includes('E.createLegacyQueryCreator');
1082
+ }
1083
+ function findLegacyWrappers(content) {
1084
+ const wrappers = new Map(); // original name -> legacy name
1085
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1086
+ function visit(node) {
1087
+ // Find: export const legacyGetUsers = E.createLegacyQueryCreator({ creator: getUsers });
1088
+ if (ts.isVariableDeclaration(node) &&
1089
+ ts.isIdentifier(node.name) &&
1090
+ node.name.text.startsWith('legacy') &&
1091
+ node.initializer &&
1092
+ ts.isCallExpression(node.initializer)) {
1093
+ const callExpr = node.initializer;
1094
+ // Check if it's E.createLegacyQueryCreator
1095
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
1096
+ ts.isIdentifier(callExpr.expression.expression) &&
1097
+ callExpr.expression.expression.text === 'E' &&
1098
+ ts.isIdentifier(callExpr.expression.name) &&
1099
+ callExpr.expression.name.text === 'createLegacyQueryCreator') {
1100
+ // Extract the original creator name from the config object
1101
+ if (callExpr.arguments.length > 0 && ts.isObjectLiteralExpression(callExpr.arguments[0])) {
1102
+ const configObj = callExpr.arguments[0];
1103
+ configObj.properties.forEach((prop) => {
1104
+ if (ts.isPropertyAssignment(prop) &&
1105
+ ts.isIdentifier(prop.name) &&
1106
+ prop.name.text === 'creator' &&
1107
+ ts.isIdentifier(prop.initializer)) {
1108
+ const originalName = prop.initializer.text;
1109
+ const legacyName = node.name.text;
1110
+ wrappers.set(originalName, legacyName);
1111
+ }
1112
+ });
1113
+ }
1114
+ }
1115
+ }
1116
+ ts.forEachChild(node, visit);
1117
+ }
1118
+ visit(sourceFile);
1119
+ return wrappers;
1120
+ }
1121
+ function updateLegacyCreatorImports(content, legacyCreators) {
1122
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1123
+ const replacements = [];
1124
+ function visit(node) {
1125
+ // Update named imports to include legacy wrappers
1126
+ if (ts.isImportDeclaration(node) &&
1127
+ node.importClause?.namedBindings &&
1128
+ ts.isNamedImports(node.importClause.namedBindings)) {
1129
+ const namedBindings = node.importClause.namedBindings;
1130
+ const importedNames = new Set();
1131
+ const newElements = [];
1132
+ let hasChanges = false;
1133
+ namedBindings.elements.forEach((element) => {
1134
+ const importedName = element.propertyName ? element.propertyName.text : element.name.text;
1135
+ const legacyName = legacyCreators.get(importedName);
1136
+ if (legacyName) {
1137
+ // Replace the original import with the legacy one
1138
+ hasChanges = true;
1139
+ newElements.push(legacyName);
1140
+ importedNames.add(legacyName);
1141
+ }
1142
+ else {
1143
+ // Keep imports that don't have legacy versions
1144
+ newElements.push(element.getText(sourceFile));
1145
+ importedNames.add(importedName);
1146
+ }
1147
+ });
1148
+ if (hasChanges) {
1149
+ const moduleSpecifier = node.moduleSpecifier.text;
1150
+ const newImportStatement = `import { ${newElements.join(', ')} } from '${moduleSpecifier}';`;
1151
+ replacements.push({
1152
+ start: node.getStart(sourceFile),
1153
+ end: node.getEnd(),
1154
+ replacement: newImportStatement,
1155
+ });
1156
+ }
1157
+ }
1158
+ ts.forEachChild(node, visit);
1159
+ }
1160
+ visit(sourceFile);
1161
+ // Apply replacements in reverse order
1162
+ let result = content;
1163
+ replacements.sort((a, b) => b.start - a.start);
1164
+ for (const { start, end, replacement } of replacements) {
1165
+ result = result.slice(0, start) + replacement + result.slice(end);
1166
+ }
1167
+ return result;
1168
+ }
1169
+ function updateLegacyCreatorUsages(content, legacyCreators) {
1170
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1171
+ const replacements = [];
1172
+ function visit(node) {
1173
+ // Handle object literal properties
1174
+ if (ts.isObjectLiteralExpression(node)) {
1175
+ node.properties.forEach((prop) => {
1176
+ if (ts.isPropertyAssignment(prop)) {
1177
+ // Case: { get: getUsers } -> { get: legacyGetUsers }
1178
+ if (ts.isIdentifier(prop.initializer) && legacyCreators.has(prop.initializer.text)) {
1179
+ const oldName = prop.initializer.text;
1180
+ const newName = legacyCreators.get(oldName);
1181
+ replacements.push({
1182
+ start: prop.initializer.getStart(sourceFile),
1183
+ end: prop.initializer.getEnd(),
1184
+ replacement: newName,
1185
+ });
1186
+ }
1187
+ }
1188
+ else if (ts.isShorthandPropertyAssignment(prop)) {
1189
+ // Case: { getUsers } -> { getUsers: legacyGetUsers }
1190
+ if (ts.isIdentifier(prop.name) && legacyCreators.has(prop.name.text)) {
1191
+ const oldName = prop.name.text;
1192
+ const newName = legacyCreators.get(oldName);
1193
+ const replacement = `${oldName}: ${newName}`;
1194
+ replacements.push({
1195
+ start: prop.getStart(sourceFile),
1196
+ end: prop.getEnd(),
1197
+ replacement,
1198
+ });
1199
+ }
1200
+ }
1201
+ });
1202
+ }
1203
+ // Handle regular identifier references - but skip declarations and property access names
1204
+ if (ts.isIdentifier(node) && legacyCreators.has(node.text)) {
1205
+ const parent = node.parent;
1206
+ // Skip if it's a property name in PropertyAccessExpression (e.g., obj.methodName)
1207
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
1208
+ ts.forEachChild(node, visit);
1209
+ return;
1210
+ }
1211
+ // Skip if it's a property name in an object literal (already handled above)
1212
+ if (ts.isPropertyAssignment(parent) && parent.name === node) {
1213
+ ts.forEachChild(node, visit);
1214
+ return;
1215
+ }
1216
+ // Skip if it's the initializer in a property assignment (already handled above)
1217
+ if (ts.isPropertyAssignment(parent) && parent.initializer === node) {
1218
+ ts.forEachChild(node, visit);
1219
+ return;
1220
+ }
1221
+ // Skip if it's a shorthand property (already handled above)
1222
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
1223
+ ts.forEachChild(node, visit);
1224
+ return;
1225
+ }
1226
+ // Skip if it's a variable declaration name
1227
+ if (ts.isVariableDeclaration(parent) && parent.name === node) {
1228
+ ts.forEachChild(node, visit);
1229
+ return;
1230
+ }
1231
+ // Skip if it's an import specifier
1232
+ if (ts.isImportSpecifier(parent)) {
1233
+ ts.forEachChild(node, visit);
1234
+ return;
1235
+ }
1236
+ // Skip if it's a function/method name declaration
1237
+ if (ts.isFunctionDeclaration(parent) && parent.name === node) {
1238
+ ts.forEachChild(node, visit);
1239
+ return;
1240
+ }
1241
+ if (ts.isMethodDeclaration(parent) && parent.name === node) {
1242
+ ts.forEachChild(node, visit);
1243
+ return;
1244
+ }
1245
+ // Skip if it's a parameter name
1246
+ if (ts.isParameter(parent) && parent.name === node) {
1247
+ ts.forEachChild(node, visit);
1248
+ return;
1249
+ }
1250
+ // Skip if it's a property declaration name (class field)
1251
+ if (ts.isPropertyDeclaration(parent) && parent.name === node) {
1252
+ ts.forEachChild(node, visit);
1253
+ return;
1254
+ }
1255
+ // Replace with legacy name
1256
+ const newName = legacyCreators.get(node.text);
1257
+ replacements.push({
1258
+ start: node.getStart(sourceFile),
1259
+ end: node.getEnd(),
1260
+ replacement: newName,
1261
+ });
1262
+ }
1263
+ ts.forEachChild(node, visit);
1264
+ }
1265
+ visit(sourceFile);
1266
+ // Apply replacements in reverse order
1267
+ let result = content;
1268
+ replacements.sort((a, b) => b.start - a.start);
1269
+ for (const { start, end, replacement } of replacements) {
1270
+ result = result.slice(0, start) + replacement + result.slice(end);
1271
+ }
1272
+ return result;
1273
+ }
1274
+ //#endregion
1275
+ //#region Remove provideQueryClientForDevtools and QueryDevtoolsComponent in ts / et-query-devtools in html usage
1276
+ function removeDevtoolsUsage(tree) {
1277
+ const projects = (0, devkit_1.getProjects)(tree);
1278
+ const removedFromFiles = [];
1279
+ // Only check app config files for provideQueryClientForDevtools
1280
+ for (const [, projectConfig] of projects.entries()) {
1281
+ if (projectConfig.projectType !== 'application')
1282
+ continue;
1283
+ const configPaths = [`${projectConfig.root}/src/app/app.config.ts`, `${projectConfig.root}/src/main.ts`];
1284
+ for (const configPath of configPaths) {
1285
+ if (!tree.exists(configPath))
1286
+ continue;
1287
+ const content = tree.read(configPath, 'utf-8');
1288
+ if (!content)
1289
+ continue;
1290
+ const newContent = removeProvideQueryClientForDevtools(content);
1291
+ if (newContent !== content) {
1292
+ tree.write(configPath, newContent);
1293
+ removedFromFiles.push(configPath);
1294
+ }
1295
+ }
1296
+ }
1297
+ // Check all TypeScript files for QueryDevtoolsComponent imports
1298
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1299
+ if (!filePath.endsWith('.ts'))
1300
+ return;
1301
+ const content = tree.read(filePath, 'utf-8');
1302
+ if (!content)
1303
+ return;
1304
+ // Only process if it mentions QueryDevtoolsComponent
1305
+ if (!content.includes('QueryDevtoolsComponent'))
1306
+ return;
1307
+ const newContent = removeQueryDevtoolsComponent(content);
1308
+ if (newContent !== content) {
1309
+ tree.write(filePath, newContent);
1310
+ if (!removedFromFiles.includes(filePath)) {
1311
+ removedFromFiles.push(filePath);
1312
+ }
1313
+ }
1314
+ });
1315
+ // Check all HTML files for et-query-devtools
1316
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1317
+ if (!filePath.endsWith('.html'))
1318
+ return;
1319
+ const content = tree.read(filePath, 'utf-8');
1320
+ if (!content)
1321
+ return;
1322
+ // Only process if it mentions et-query-devtools
1323
+ if (!content.includes('et-query-devtools'))
1324
+ return;
1325
+ const newContent = removeDevtoolsFromHtml(content);
1326
+ if (newContent !== content) {
1327
+ tree.write(filePath, newContent);
1328
+ removedFromFiles.push(filePath);
1329
+ }
1330
+ });
1331
+ if (removedFromFiles.length > 0) {
1332
+ console.log('\n✅ Removed devtools usage from:');
1333
+ removedFromFiles.forEach((file) => console.log(` - ${file}`));
1334
+ console.warn('\n⚠️ Make sure to check the app.config.ts/main.ts files for now unneeded logic inside the providers array (like ...(isDevMode() ? [] : []))');
1335
+ console.warn('\n⚠️ Make sure to check the the component in which the devtools component was placed for no longer needed logic (like an empty @if block)');
1336
+ }
1337
+ }
1338
+ function removeProvideQueryClientForDevtools(content) {
1339
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1340
+ const replacements = [];
1341
+ function visit(node) {
1342
+ // Remove provideQueryClientForDevtools from provider arrays
1343
+ if (ts.isCallExpression(node)) {
1344
+ if (ts.isIdentifier(node.expression) && node.expression.text === 'provideQueryClientForDevtools') {
1345
+ // Find the array this call is in
1346
+ let parent = node.parent;
1347
+ // Walk up until we find an array literal expression
1348
+ while (parent) {
1349
+ if (ts.isArrayLiteralExpression(parent)) {
1350
+ // Find which element contains our call expression
1351
+ const elementIndex = parent.elements.findIndex((el) => {
1352
+ // Check if this element or any of its descendants is our node
1353
+ let found = false;
1354
+ function search(n) {
1355
+ if (n === node) {
1356
+ found = true;
1357
+ return;
1358
+ }
1359
+ ts.forEachChild(n, search);
1360
+ }
1361
+ search(el);
1362
+ return found;
1363
+ });
1364
+ if (elementIndex !== -1) {
1365
+ const element = parent.elements[elementIndex];
1366
+ let start = element.getStart(sourceFile);
1367
+ let end = element.getEnd();
1368
+ // Handle comma and whitespace removal
1369
+ if (elementIndex < parent.elements.length - 1) {
1370
+ // Not the last element - remove trailing comma and whitespace
1371
+ const nextElement = parent.elements[elementIndex + 1];
1372
+ const textBetween = content.slice(end, nextElement.getStart(sourceFile));
1373
+ const commaIndex = textBetween.indexOf(',');
1374
+ if (commaIndex !== -1) {
1375
+ end += commaIndex + 1;
1376
+ // Also remove whitespace after comma
1377
+ while (end < content.length && /\s/.test(content[end])) {
1378
+ end++;
1379
+ }
1380
+ }
1381
+ }
1382
+ else if (elementIndex > 0) {
1383
+ // Last element - remove leading comma and whitespace
1384
+ const prevElement = parent.elements[elementIndex - 1];
1385
+ const textBetween = content.slice(prevElement.getEnd(), start);
1386
+ const commaIndex = textBetween.lastIndexOf(',');
1387
+ if (commaIndex !== -1) {
1388
+ start = prevElement.getEnd() + commaIndex;
1389
+ }
1390
+ }
1391
+ // Remove leading whitespace/newlines
1392
+ while (start > 0 && /\s/.test(content[start - 1])) {
1393
+ start--;
1394
+ }
1395
+ replacements.push({ start, end, replacement: '' });
1396
+ }
1397
+ break;
1398
+ }
1399
+ parent = parent.parent;
1400
+ }
1401
+ }
1402
+ }
1403
+ ts.forEachChild(node, visit);
1404
+ }
1405
+ visit(sourceFile);
1406
+ // Apply replacements in reverse order
1407
+ let result = content;
1408
+ replacements.sort((a, b) => b.start - a.start);
1409
+ for (const { start, end, replacement } of replacements) {
1410
+ result = result.slice(0, start) + replacement + result.slice(end);
1411
+ }
1412
+ // Remove the import if it exists
1413
+ return removeDevtoolsImports(result);
1414
+ }
1415
+ function removeQueryDevtoolsComponent(content) {
1416
+ let result = content;
1417
+ // Remove from imports array in components
1418
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1419
+ const replacements = [];
1420
+ function visit(node) {
1421
+ // Remove QueryDevtoolsComponent from imports arrays in components
1422
+ if (ts.isPropertyAssignment(node) &&
1423
+ ts.isIdentifier(node.name) &&
1424
+ node.name.text === 'imports' &&
1425
+ ts.isArrayLiteralExpression(node.initializer)) {
1426
+ const arrayExpr = node.initializer;
1427
+ arrayExpr.elements.forEach((element, index) => {
1428
+ if (ts.isIdentifier(element) && element.text === 'QueryDevtoolsComponent') {
1429
+ let start = element.getStart(sourceFile);
1430
+ let end = element.getEnd();
1431
+ // Handle comma removal
1432
+ if (index < arrayExpr.elements.length - 1) {
1433
+ const nextElement = arrayExpr.elements[index + 1];
1434
+ const textBetween = content.slice(end, nextElement.getStart(sourceFile));
1435
+ const commaIndex = textBetween.indexOf(',');
1436
+ if (commaIndex !== -1) {
1437
+ end += commaIndex + 1;
1438
+ // Also remove whitespace after comma
1439
+ while (end < content.length && /\s/.test(content[end])) {
1440
+ end++;
1441
+ }
1442
+ }
1443
+ }
1444
+ else if (index > 0) {
1445
+ const prevElement = arrayExpr.elements[index - 1];
1446
+ const textBetween = content.slice(prevElement.getEnd(), start);
1447
+ const commaIndex = textBetween.lastIndexOf(',');
1448
+ if (commaIndex !== -1) {
1449
+ start = prevElement.getEnd() + commaIndex;
1450
+ }
1451
+ }
1452
+ // Remove surrounding whitespace
1453
+ while (start > 0 && /\s/.test(content[start - 1])) {
1454
+ start--;
1455
+ }
1456
+ replacements.push({ start, end, replacement: '' });
1457
+ }
1458
+ });
1459
+ }
1460
+ ts.forEachChild(node, visit);
1461
+ }
1462
+ visit(sourceFile);
1463
+ // Apply replacements in reverse order
1464
+ replacements.sort((a, b) => b.start - a.start);
1465
+ for (const { start, end, replacement } of replacements) {
1466
+ result = result.slice(0, start) + replacement + result.slice(end);
1467
+ }
1468
+ // Remove the import if it exists
1469
+ return removeDevtoolsImports(result);
1470
+ }
1471
+ function removeDevtoolsImports(content) {
1472
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1473
+ let queryImportNode;
1474
+ ts.forEachChild(sourceFile, (node) => {
1475
+ if (ts.isImportDeclaration(node) &&
1476
+ ts.isStringLiteral(node.moduleSpecifier) &&
1477
+ node.moduleSpecifier.text === '@ethlete/query' &&
1478
+ node.importClause?.namedBindings &&
1479
+ ts.isNamedImports(node.importClause.namedBindings)) {
1480
+ queryImportNode = node;
1481
+ }
1482
+ });
1483
+ if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
1484
+ return content;
1485
+ }
1486
+ const namedBindings = queryImportNode.importClause.namedBindings;
1487
+ const elementsToKeep = namedBindings.elements.filter((el) => el.name.text !== 'QueryDevtoolsComponent' && el.name.text !== 'provideQueryClientForDevtools');
1488
+ if (elementsToKeep.length === namedBindings.elements.length) {
1489
+ // Nothing to remove
1490
+ return content;
1491
+ }
1492
+ const importStart = queryImportNode.getStart(sourceFile);
1493
+ const importEnd = queryImportNode.getEnd();
1494
+ if (elementsToKeep.length === 0) {
1495
+ // Remove entire import statement
1496
+ const start = importStart;
1497
+ let end = importEnd;
1498
+ // Also remove the newline after the import
1499
+ if (content[end] === '\n')
1500
+ end++;
1501
+ return content.slice(0, start) + content.slice(end);
1502
+ }
1503
+ // Reconstruct import without devtools
1504
+ const newImports = elementsToKeep.map((el) => el.getText(sourceFile));
1505
+ const moduleSpecifier = queryImportNode.moduleSpecifier.text;
1506
+ const newImportStatement = `import { ${newImports.join(', ')} } from '${moduleSpecifier}';`;
1507
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
1508
+ }
1509
+ function removeDevtoolsFromHtml(content) {
1510
+ let result = content;
1511
+ // Remove self-closing tags
1512
+ result = result.replace(/<et-query-devtools\s*\/>/g, '');
1513
+ // Remove opening and closing tags with content
1514
+ result = result.replace(/<et-query-devtools[^>]*>[\s\S]*?<\/et-query-devtools>/g, '');
1515
+ return result;
1516
+ }
1517
+ //#endregion
1518
+ //#region Turn AnyQuery into E.AnyLegacyQuery everywhere and do the same with AnyQueryCreator
1519
+ function replaceAnyQueryWithLegacy(tree) {
1520
+ const updatedFiles = [];
1521
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1522
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
1523
+ return;
1524
+ const content = tree.read(filePath, 'utf-8');
1525
+ if (!content)
1526
+ return;
1527
+ // Only process if it contains AnyQuery or AnyQueryCreator
1528
+ if (!content.includes('AnyQuery') && !content.includes('AnyQueryCreator'))
1529
+ return;
1530
+ const newContent = replaceAnyQueryInFile(content);
1531
+ if (newContent !== content) {
1532
+ tree.write(filePath, newContent);
1533
+ updatedFiles.push(filePath);
1534
+ }
1535
+ });
1536
+ if (updatedFiles.length > 0) {
1537
+ console.log('\n✅ Replaced AnyQuery and AnyQueryCreator with E.AnyLegacyQuery and E.AnyLegacyQueryCreator in:');
1538
+ updatedFiles.forEach((file) => console.log(` - ${file}`));
1539
+ }
1540
+ }
1541
+ function replaceAnyQueryInFile(content) {
1542
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1543
+ const replacements = [];
1544
+ function visit(node) {
1545
+ // Find type references to AnyQuery and AnyQueryCreator
1546
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
1547
+ if (node.typeName.text === 'AnyQuery') {
1548
+ replacements.push({
1549
+ start: node.getStart(sourceFile),
1550
+ end: node.getEnd(),
1551
+ replacement: 'E.AnyLegacyQuery',
1552
+ });
1553
+ }
1554
+ else if (node.typeName.text === 'AnyQueryCreator') {
1555
+ replacements.push({
1556
+ start: node.getStart(sourceFile),
1557
+ end: node.getEnd(),
1558
+ replacement: 'E.AnyLegacyQueryCreator',
1559
+ });
1560
+ }
1561
+ }
1562
+ // Find identifier references (for runtime usage)
1563
+ if (ts.isIdentifier(node) && (node.text === 'AnyQuery' || node.text === 'AnyQueryCreator')) {
1564
+ // Check if this is part of a type reference (already handled above)
1565
+ const parent = node.parent;
1566
+ if (ts.isTypeReferenceNode(parent) && parent.typeName === node) {
1567
+ ts.forEachChild(node, visit);
1568
+ return;
1569
+ }
1570
+ // Check if it's in an import statement
1571
+ if (ts.isImportSpecifier(parent)) {
1572
+ ts.forEachChild(node, visit);
1573
+ return;
1574
+ }
1575
+ // Replace standalone references
1576
+ const replacement = node.text === 'AnyQuery' ? 'E.AnyLegacyQuery' : 'E.AnyLegacyQueryCreator';
1577
+ replacements.push({
1578
+ start: node.getStart(sourceFile),
1579
+ end: node.getEnd(),
1580
+ replacement,
1581
+ });
1582
+ }
1583
+ ts.forEachChild(node, visit);
1584
+ }
1585
+ visit(sourceFile);
1586
+ // Apply replacements in reverse order
1587
+ let result = content;
1588
+ replacements.sort((a, b) => b.start - a.start);
1589
+ for (const { start, end, replacement } of replacements) {
1590
+ result = result.slice(0, start) + replacement + result.slice(end);
1591
+ }
1592
+ // Remove AnyQuery and AnyQueryCreator from imports and ensure ExperimentalQuery as E exists
1593
+ result = removeAnyQueryFromImports(result);
1594
+ // Make sure we have ExperimentalQuery as E import
1595
+ if (!result.includes('ExperimentalQuery as E')) {
1596
+ result = addExperimentalQueryImport(result);
1597
+ }
1598
+ return result;
1599
+ }
1600
+ function removeAnyQueryFromImports(content) {
1601
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1602
+ let queryImportNode;
1603
+ ts.forEachChild(sourceFile, (node) => {
1604
+ if (ts.isImportDeclaration(node) &&
1605
+ ts.isStringLiteral(node.moduleSpecifier) &&
1606
+ node.moduleSpecifier.text === '@ethlete/query' &&
1607
+ node.importClause?.namedBindings &&
1608
+ ts.isNamedImports(node.importClause.namedBindings)) {
1609
+ queryImportNode = node;
1610
+ }
1611
+ });
1612
+ if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
1613
+ return content;
1614
+ }
1615
+ const namedBindings = queryImportNode.importClause.namedBindings;
1616
+ const elementsToKeep = namedBindings.elements.filter((el) => el.name.text !== 'AnyQuery' && el.name.text !== 'AnyQueryCreator');
1617
+ if (elementsToKeep.length === namedBindings.elements.length) {
1618
+ // Nothing to remove
1619
+ return content;
1620
+ }
1621
+ const importStart = queryImportNode.getStart(sourceFile);
1622
+ const importEnd = queryImportNode.getEnd();
1623
+ if (elementsToKeep.length === 0) {
1624
+ // Remove entire import statement
1625
+ const start = importStart;
1626
+ let end = importEnd;
1627
+ // Also remove the newline after the import
1628
+ if (content[end] === '\n')
1629
+ end++;
1630
+ return content.slice(0, start) + content.slice(end);
1631
+ }
1632
+ // Reconstruct import without AnyQuery and AnyQueryCreator
1633
+ const newImports = elementsToKeep.map((el) => el.getText(sourceFile));
1634
+ const moduleSpecifier = queryImportNode.moduleSpecifier.text;
1635
+ const newImportStatement = `import { ${newImports.join(', ')} } from '${moduleSpecifier}';`;
1636
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
1637
+ }
1638
+ //#endregion
1639
+ //#region Generate inject functions for query client configs
1640
+ function generateInjectFunctions(tree, queryClientFiles) {
1641
+ const generatedFiles = [];
1642
+ for (const [filePath, configNames] of queryClientFiles.entries()) {
1643
+ const content = tree.read(filePath, 'utf-8');
1644
+ if (!content)
1645
+ continue;
1646
+ const newContent = addInjectFunctionsToFile(content, configNames);
1647
+ if (newContent !== content) {
1648
+ tree.write(filePath, newContent);
1649
+ generatedFiles.push(filePath);
1650
+ }
1651
+ }
1652
+ if (generatedFiles.length > 0) {
1653
+ console.log('\n✅ Generated inject functions in:');
1654
+ generatedFiles.forEach((file) => console.log(` - ${file}`));
1655
+ }
1656
+ }
1657
+ function addInjectFunctionsToFile(content, configNames) {
1658
+ // Add inject import if needed FIRST, before doing anything else
1659
+ let result = content;
1660
+ const hasInjectImport = result.includes('import { inject }') || (result.includes("from '@angular/core'") && result.includes('inject'));
1661
+ if (!hasInjectImport && configNames.length > 0) {
1662
+ result = addInjectImport(result);
1663
+ }
1664
+ // NOW parse the updated content to find config positions
1665
+ const sourceFile = ts.createSourceFile('temp.ts', result, ts.ScriptTarget.Latest, true);
1666
+ // Find where each config is declared
1667
+ const configPositions = new Map();
1668
+ function visit(node) {
1669
+ if (ts.isVariableDeclaration(node) &&
1670
+ ts.isIdentifier(node.name) &&
1671
+ node.initializer &&
1672
+ ts.isCallExpression(node.initializer)) {
1673
+ const callExpr = node.initializer;
1674
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
1675
+ ts.isIdentifier(callExpr.expression.expression) &&
1676
+ callExpr.expression.expression.text === 'E' &&
1677
+ ts.isIdentifier(callExpr.expression.name) &&
1678
+ callExpr.expression.name.text === 'createQueryClientConfig') {
1679
+ const configName = node.name.text;
1680
+ // Find the variable statement
1681
+ let parent = node.parent;
1682
+ while (parent && !ts.isVariableStatement(parent)) {
1683
+ parent = parent.parent;
1684
+ }
1685
+ if (parent) {
1686
+ configPositions.set(configName, parent.getEnd());
1687
+ }
1688
+ }
1689
+ }
1690
+ ts.forEachChild(node, visit);
1691
+ }
1692
+ visit(sourceFile);
1693
+ // Generate inject functions for each config
1694
+ const insertions = [];
1695
+ for (const configName of configNames) {
1696
+ const position = configPositions.get(`${configName}Config`);
1697
+ if (!position)
1698
+ continue;
1699
+ // Check if inject function already exists
1700
+ const nameWithoutConfig = configName.replace(/Config$/, '');
1701
+ const capitalizedName = nameWithoutConfig.charAt(0).toUpperCase() + nameWithoutConfig.slice(1);
1702
+ const injectFunctionName = `inject${capitalizedName}`;
1703
+ if (result.includes(injectFunctionName))
1704
+ continue;
1705
+ const injectFunction = generateInjectFunction(configName);
1706
+ insertions.push({ position, text: `\n\n${injectFunction}` });
1707
+ }
1708
+ // Apply insertions in reverse order to maintain positions
1709
+ insertions.sort((a, b) => b.position - a.position);
1710
+ for (const { position, text } of insertions) {
1711
+ result = result.slice(0, position) + text + result.slice(position);
1712
+ }
1713
+ return result;
1714
+ }
1715
+ function addInjectImport(content) {
1716
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1717
+ // Find if there's already an import from @angular/core
1718
+ let angularCoreImport;
1719
+ ts.forEachChild(sourceFile, (node) => {
1720
+ if (ts.isImportDeclaration(node) &&
1721
+ ts.isStringLiteral(node.moduleSpecifier) &&
1722
+ node.moduleSpecifier.text === '@angular/core') {
1723
+ angularCoreImport = node;
1724
+ }
1725
+ });
1726
+ if (angularCoreImport) {
1727
+ // Add inject to existing @angular/core import
1728
+ if (angularCoreImport.importClause?.namedBindings &&
1729
+ ts.isNamedImports(angularCoreImport.importClause.namedBindings)) {
1730
+ const namedBindings = angularCoreImport.importClause.namedBindings;
1731
+ const existingImports = namedBindings.elements.map((el) => el.name.text);
1732
+ // Check if inject is already imported
1733
+ if (existingImports.includes('inject')) {
1734
+ return content;
1735
+ }
1736
+ const newImports = [...existingImports, 'inject'].sort();
1737
+ const newImportStatement = `import { ${newImports.join(', ')} } from '@angular/core';`;
1738
+ const importStart = angularCoreImport.getStart(sourceFile);
1739
+ const importEnd = angularCoreImport.getEnd();
1740
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
1741
+ }
1742
+ }
1743
+ // Find the last import statement to insert after it
1744
+ let lastImportEnd = 0;
1745
+ ts.forEachChild(sourceFile, (node) => {
1746
+ if (ts.isImportDeclaration(node)) {
1747
+ const end = node.getEnd();
1748
+ if (end > lastImportEnd) {
1749
+ lastImportEnd = end;
1750
+ }
1751
+ }
1752
+ });
1753
+ if (lastImportEnd > 0) {
1754
+ // Add after the last import
1755
+ const injectImport = `\nimport { inject } from '@angular/core';`;
1756
+ return content.slice(0, lastImportEnd) + injectImport + content.slice(lastImportEnd);
1757
+ }
1758
+ // No imports found, add at the beginning
1759
+ return `import { inject } from '@angular/core';\n\n${content}`;
1760
+ }
1761
+ function generateInjectFunction(configName) {
1762
+ // Convert apiClientConfig -> injectApiClient
1763
+ const nameWithoutConfig = configName.replace(/Config$/, '');
1764
+ const capitalizedName = nameWithoutConfig.charAt(0).toUpperCase() + nameWithoutConfig.slice(1);
1765
+ const functionName = `inject${capitalizedName}`;
1766
+ return `export const ${functionName} = () => inject(${configName}Config.token);`;
1767
+ }
1768
+ function checkLegacyQueryCreatorUsage(tree) {
1769
+ console.log('\n🔍 Checking for legacy query creator usages...');
1770
+ // Step 1: Collect all usages BEFORE any transformations
1771
+ const allUsages = [];
1772
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1773
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
1774
+ return;
1775
+ const content = tree.read(filePath, 'utf-8');
1776
+ if (!content)
1777
+ return;
1778
+ const usages = findLegacyCreatorUsages(content, filePath);
1779
+ allUsages.push(...usages);
1780
+ });
1781
+ if (allUsages.length === 0) {
1782
+ console.log('\n✅ No legacy query creator usages found');
1783
+ return;
1784
+ }
1785
+ console.log(`\n📊 Found ${allUsages.length} legacy query creator usages`);
1786
+ // Log statistics
1787
+ const contextCounts = new Map();
1788
+ allUsages.forEach((usage) => {
1789
+ contextCounts.set(usage.context, (contextCounts.get(usage.context) || 0) + 1);
1790
+ });
1791
+ contextCounts.forEach((count, context) => {
1792
+ console.log(` - ${context}: ${count} usages`);
1793
+ });
1794
+ // Step 1.5: Detect polling usage BEFORE any transformations
1795
+ console.log('\n🔍 Detecting polling usage...');
1796
+ const pollingInfo = detectPollingUsage(tree);
1797
+ if (pollingInfo.size > 0) {
1798
+ console.log(`\n⚠️ Found ${pollingInfo.size} queries with polling:`);
1799
+ pollingInfo.forEach((info) => {
1800
+ console.log(` ${info.queryVariableName}:`);
1801
+ info.locations.forEach((loc) => console.log(` - ${loc}`));
1802
+ });
1803
+ console.log('\n → These queries will NOT have destroyOnResponse: true added');
1804
+ }
1805
+ // Step 2: Find or create injector members in classes
1806
+ const classInjectors = findOrCreateInjectorMembers(tree, allUsages);
1807
+ createInjectorMembers(tree, classInjectors);
1808
+ // Step 3: Transform prepare() calls with polling info
1809
+ transformLegacyCreatorPrepareCall(tree, classInjectors, pollingInfo);
1810
+ // Step 4: Report manual review locations (using original line numbers from allUsages)
1811
+ reportManualReviewLocations(tree, allUsages);
1812
+ }
1813
+ function findLegacyCreatorUsages(content, filePath) {
1814
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
1815
+ const usages = [];
1816
+ // First, find all legacy query creator names imported/defined in this file
1817
+ const legacyCreatorNames = findLegacyCreatorNames(sourceFile, content);
1818
+ if (legacyCreatorNames.size === 0) {
1819
+ return usages;
1820
+ }
1821
+ // Now find all .prepare() calls on these creators
1822
+ function visit(node) {
1823
+ // Look for: legacyCreator.prepare()
1824
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
1825
+ const propAccess = node.expression;
1826
+ if (ts.isIdentifier(propAccess.name) && propAccess.name.text === 'prepare') {
1827
+ // Check if the object is a legacy creator
1828
+ let creatorName;
1829
+ if (ts.isIdentifier(propAccess.expression)) {
1830
+ creatorName = propAccess.expression.text;
1831
+ }
1832
+ if (creatorName && legacyCreatorNames.has(creatorName)) {
1833
+ const context = determineUsageContext(node, sourceFile);
1834
+ const hasInjector = checkForExistingInjector(node, sourceFile);
1835
+ const lineNumber = getLineNumber(node, sourceFile);
1836
+ usages.push({
1837
+ filePath,
1838
+ creatorName,
1839
+ line: lineNumber,
1840
+ position: {
1841
+ start: node.getStart(sourceFile),
1842
+ end: node.getEnd(),
1843
+ },
1844
+ context,
1845
+ hasExistingInjector: hasInjector,
1846
+ });
1847
+ }
1848
+ }
1849
+ }
1850
+ ts.forEachChild(node, visit);
1851
+ }
1852
+ visit(sourceFile);
1853
+ return usages;
1854
+ }
1855
+ function findLegacyCreatorNames(sourceFile, content) {
1856
+ const names = new Set();
1857
+ function visit(node) {
1858
+ // Find imports of legacy creators (names starting with 'legacy')
1859
+ if (ts.isImportDeclaration(node) &&
1860
+ node.importClause?.namedBindings &&
1861
+ ts.isNamedImports(node.importClause.namedBindings)) {
1862
+ node.importClause.namedBindings.elements.forEach((element) => {
1863
+ const name = element.name.text;
1864
+ if (name.startsWith('legacy')) {
1865
+ names.add(name);
1866
+ }
1867
+ });
1868
+ }
1869
+ // Find local legacy creator definitions
1870
+ if (ts.isVariableDeclaration(node) &&
1871
+ ts.isIdentifier(node.name) &&
1872
+ node.name.text.startsWith('legacy') &&
1873
+ node.initializer &&
1874
+ ts.isCallExpression(node.initializer)) {
1875
+ const callExpr = node.initializer;
1876
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
1877
+ ts.isIdentifier(callExpr.expression.expression) &&
1878
+ callExpr.expression.expression.text === 'E' &&
1879
+ ts.isIdentifier(callExpr.expression.name) &&
1880
+ callExpr.expression.name.text === 'createLegacyQueryCreator') {
1881
+ names.add(node.name.text);
1882
+ }
1883
+ }
1884
+ ts.forEachChild(node, visit);
1885
+ }
1886
+ visit(sourceFile);
1887
+ return names;
1888
+ }
1889
+ function determineUsageContext(node, sourceFile) {
1890
+ let current = node;
1891
+ while (current) {
1892
+ // Check for queryComputed FIRST (before checking for generic functions)
1893
+ // We need to look at the parent of arrow/function expressions
1894
+ if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
1895
+ const parent = current.parent;
1896
+ // Check if this function is a callback to queryComputed
1897
+ if (ts.isCallExpression(parent)) {
1898
+ const expr = parent.expression;
1899
+ // Check for queryComputed()
1900
+ if (ts.isIdentifier(expr) && expr.text === 'queryComputed') {
1901
+ return 'queryComputed';
1902
+ }
1903
+ // Check for E.queryComputed()
1904
+ if (ts.isPropertyAccessExpression(expr) &&
1905
+ ts.isIdentifier(expr.expression) &&
1906
+ expr.expression.text === 'E' &&
1907
+ ts.isIdentifier(expr.name) &&
1908
+ expr.name.text === 'queryComputed') {
1909
+ return 'queryComputed';
1910
+ }
1911
+ }
1912
+ }
1913
+ // Check for constructor
1914
+ if (ts.isConstructorDeclaration(current)) {
1915
+ return 'constructor';
1916
+ }
1917
+ // Check for method
1918
+ if (ts.isMethodDeclaration(current)) {
1919
+ return 'method';
1920
+ }
1921
+ // Check for class field
1922
+ if (ts.isPropertyDeclaration(current)) {
1923
+ return 'class-field';
1924
+ }
1925
+ // Check for function declarations
1926
+ if (ts.isFunctionDeclaration(current)) {
1927
+ return 'function';
1928
+ }
1929
+ // Check for arrow functions and function expressions (standalone, not callbacks)
1930
+ // But make sure we're not inside a class (which would make it a method/field)
1931
+ if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) && !isInsideClass(current)) {
1932
+ return 'function';
1933
+ }
1934
+ current = current.parent;
1935
+ }
1936
+ return 'unknown';
1937
+ }
1938
+ function isInsideClass(node) {
1939
+ let current = node.parent;
1940
+ while (current) {
1941
+ if (ts.isClassDeclaration(current)) {
1942
+ return true;
1943
+ }
1944
+ current = current.parent;
1945
+ }
1946
+ return false;
1947
+ }
1948
+ function checkForExistingInjector(node, sourceFile) {
1949
+ // Walk up to find the containing class
1950
+ let current = node;
1951
+ while (current && !ts.isClassDeclaration(current)) {
1952
+ current = current.parent;
1953
+ }
1954
+ if (!current || !ts.isClassDeclaration(current)) {
1955
+ return false;
1956
+ }
1957
+ const classDecl = current;
1958
+ // Check if class has any member that calls inject(Injector)
1959
+ for (const member of classDecl.members) {
1960
+ if (ts.isPropertyDeclaration(member) && member.initializer) {
1961
+ if (hasInjectInjectorCall(member.initializer)) {
1962
+ return true;
1963
+ }
1964
+ }
1965
+ if (ts.isConstructorDeclaration(member)) {
1966
+ for (const param of member.parameters) {
1967
+ if (param.initializer && hasInjectInjectorCall(param.initializer)) {
1968
+ return true;
1969
+ }
1970
+ }
1971
+ }
1972
+ }
1973
+ return false;
1974
+ }
1975
+ function hasInjectInjectorCall(node) {
1976
+ let found = false;
1977
+ function visit(n) {
1978
+ if (found)
1979
+ return;
1980
+ // Look for: inject(Injector)
1981
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === 'inject') {
1982
+ if (n.arguments.length > 0 && ts.isIdentifier(n.arguments[0]) && n.arguments[0].text === 'Injector') {
1983
+ found = true;
1984
+ return;
1985
+ }
1986
+ }
1987
+ ts.forEachChild(n, visit);
1988
+ }
1989
+ visit(node);
1990
+ return found;
1991
+ }
1992
+ function getLineNumber(node, sourceFile) {
1993
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
1994
+ return line + 1;
1995
+ }
1996
+ function findOrCreateInjectorMembers(tree, usages) {
1997
+ const classInjectors = new Map();
1998
+ // Group usages by file and class
1999
+ const usagesByFile = new Map();
2000
+ usages.forEach((usage) => {
2001
+ // Skip usages that don't need injector (queryComputed, class-field, constructor)
2002
+ if (['queryComputed', 'class-field', 'constructor'].includes(usage.context)) {
2003
+ return;
2004
+ }
2005
+ if (!usagesByFile.has(usage.filePath)) {
2006
+ usagesByFile.set(usage.filePath, []);
2007
+ }
2008
+ usagesByFile.get(usage.filePath).push(usage);
2009
+ });
2010
+ // Process each file
2011
+ usagesByFile.forEach((fileUsages, filePath) => {
2012
+ const content = tree.read(filePath, 'utf-8');
2013
+ if (!content)
2014
+ return;
2015
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
2016
+ // Find all classes in this file that have usages
2017
+ const classesNeedingInjector = findClassesNeedingInjector(sourceFile, fileUsages);
2018
+ classesNeedingInjector.forEach((classInfo) => {
2019
+ const key = `${filePath}:${classInfo.className}`;
2020
+ classInjectors.set(key, {
2021
+ ...classInfo,
2022
+ filePath,
2023
+ });
2024
+ });
2025
+ });
2026
+ return classInjectors;
2027
+ }
2028
+ function findClassesNeedingInjector(sourceFile, usages) {
2029
+ const classes = [];
2030
+ const actualFilePath = usages[0]?.filePath || sourceFile.fileName; // Get the real file path from usages
2031
+ function visit(node) {
2032
+ if (ts.isClassDeclaration(node)) {
2033
+ const className = node.name?.text || 'UnnamedClass';
2034
+ // Check if any usage is within this class
2035
+ const usagesInClass = usages.filter((usage) => {
2036
+ return usage.position.start >= node.getStart(sourceFile) && usage.position.end <= node.getEnd();
2037
+ });
2038
+ if (usagesInClass.length === 0) {
2039
+ ts.forEachChild(node, visit);
2040
+ return;
2041
+ }
2042
+ // Check if class already has an injector member
2043
+ const existingInjector = findExistingInjectorMember(node, sourceFile);
2044
+ if (existingInjector) {
2045
+ classes.push({
2046
+ filePath: actualFilePath,
2047
+ className,
2048
+ injectorMemberName: existingInjector.name,
2049
+ needsToCreate: false,
2050
+ classStart: node.getStart(sourceFile),
2051
+ classEnd: node.getEnd(),
2052
+ });
2053
+ }
2054
+ else {
2055
+ // Need to create one
2056
+ classes.push({
2057
+ filePath: actualFilePath,
2058
+ className,
2059
+ injectorMemberName: 'injector', // Default name
2060
+ needsToCreate: true,
2061
+ classStart: node.getStart(sourceFile),
2062
+ classEnd: node.getEnd(),
2063
+ });
2064
+ }
2065
+ }
2066
+ ts.forEachChild(node, visit);
2067
+ }
2068
+ visit(sourceFile);
2069
+ return classes;
2070
+ }
2071
+ function findExistingInjectorMember(classNode, sourceFile) {
2072
+ for (const member of classNode.members) {
2073
+ if (ts.isPropertyDeclaration(member) && member.initializer) {
2074
+ // Check if it's: inject(Injector)
2075
+ if (hasInjectInjectorCall(member.initializer)) {
2076
+ const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
2077
+ if (memberName) {
2078
+ return { name: memberName, node: member };
2079
+ }
2080
+ }
2081
+ }
2082
+ }
2083
+ return undefined;
2084
+ }
2085
+ function createInjectorMembers(tree, classInjectors) {
2086
+ const filesToUpdate = new Map();
2087
+ // Group by file
2088
+ classInjectors.forEach((classInfo) => {
2089
+ if (!classInfo.needsToCreate)
2090
+ return;
2091
+ if (!filesToUpdate.has(classInfo.filePath)) {
2092
+ filesToUpdate.set(classInfo.filePath, []);
2093
+ }
2094
+ filesToUpdate.get(classInfo.filePath).push(classInfo);
2095
+ });
2096
+ // Update each file
2097
+ filesToUpdate.forEach((classes, filePath) => {
2098
+ let content = tree.read(filePath, 'utf-8');
2099
+ if (!content)
2100
+ return;
2101
+ // Add inject and Injector imports if needed
2102
+ content = ensureInjectAndInjectorImports(content);
2103
+ // Add injector member to each class (in reverse order to maintain positions)
2104
+ const sortedClasses = [...classes].sort((a, b) => b.classStart - a.classStart);
2105
+ for (const classInfo of sortedClasses) {
2106
+ content = addInjectorMemberToClass(content, classInfo);
2107
+ }
2108
+ tree.write(filePath, content);
2109
+ });
2110
+ if (filesToUpdate.size > 0) {
2111
+ console.log(`\n✅ Added injector members to ${Array.from(filesToUpdate.values()).flat().length} classes`);
2112
+ }
2113
+ }
2114
+ function ensureInjectAndInjectorImports(content) {
2115
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2116
+ let angularCoreImport;
2117
+ ts.forEachChild(sourceFile, (node) => {
2118
+ if (ts.isImportDeclaration(node) &&
2119
+ ts.isStringLiteral(node.moduleSpecifier) &&
2120
+ node.moduleSpecifier.text === '@angular/core') {
2121
+ angularCoreImport = node;
2122
+ }
2123
+ });
2124
+ const neededImports = new Set();
2125
+ let hasInject = false;
2126
+ let hasInjector = false;
2127
+ if (angularCoreImport?.importClause?.namedBindings &&
2128
+ ts.isNamedImports(angularCoreImport.importClause.namedBindings)) {
2129
+ const namedBindings = angularCoreImport.importClause.namedBindings;
2130
+ namedBindings.elements.forEach((el) => {
2131
+ if (el.name.text === 'inject')
2132
+ hasInject = true;
2133
+ if (el.name.text === 'Injector')
2134
+ hasInjector = true;
2135
+ });
2136
+ }
2137
+ if (!hasInject)
2138
+ neededImports.add('inject');
2139
+ if (!hasInjector)
2140
+ neededImports.add('Injector');
2141
+ if (neededImports.size === 0) {
2142
+ return content; // Already has both imports
2143
+ }
2144
+ if (angularCoreImport?.importClause?.namedBindings &&
2145
+ ts.isNamedImports(angularCoreImport.importClause.namedBindings)) {
2146
+ // Add to existing import
2147
+ const namedBindings = angularCoreImport.importClause.namedBindings;
2148
+ const existingImports = namedBindings.elements.map((el) => el.name.text);
2149
+ const allImports = [...existingImports, ...Array.from(neededImports)].sort();
2150
+ const newImportStatement = `import { ${allImports.join(', ')} } from '@angular/core';`;
2151
+ const importStart = angularCoreImport.getStart(sourceFile);
2152
+ const importEnd = angularCoreImport.getEnd();
2153
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
2154
+ }
2155
+ // Find last import to add after it
2156
+ let lastImportEnd = 0;
2157
+ ts.forEachChild(sourceFile, (node) => {
2158
+ if (ts.isImportDeclaration(node)) {
2159
+ const end = node.getEnd();
2160
+ if (end > lastImportEnd) {
2161
+ lastImportEnd = end;
2162
+ }
2163
+ }
2164
+ });
2165
+ const newImports = Array.from(neededImports).sort();
2166
+ const newImportStatement = `\nimport { ${newImports.join(', ')} } from '@angular/core';`;
2167
+ if (lastImportEnd > 0) {
2168
+ return content.slice(0, lastImportEnd) + newImportStatement + content.slice(lastImportEnd);
2169
+ }
2170
+ return `import { ${newImports.join(', ')} } from '@angular/core';\n\n${content}`;
2171
+ }
2172
+ function addInjectorMemberToClass(content, classInfo) {
2173
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2174
+ // Find the class
2175
+ let targetClass;
2176
+ function visit(node) {
2177
+ if (ts.isClassDeclaration(node)) {
2178
+ const className = node.name?.text;
2179
+ if (className === classInfo.className) {
2180
+ targetClass = node;
2181
+ }
2182
+ }
2183
+ ts.forEachChild(node, visit);
2184
+ }
2185
+ visit(sourceFile);
2186
+ if (!targetClass)
2187
+ return content;
2188
+ // Find where to insert the injector member (after the opening brace)
2189
+ const classBodyStart = targetClass.members.length > 0 ? targetClass.members[0].getStart(sourceFile) : targetClass.getEnd() - 1; // Before closing brace
2190
+ const indentation = getIndentation(content, classBodyStart);
2191
+ const injectorMember = `${indentation}private ${classInfo.injectorMemberName} = inject(Injector);\n\n`;
2192
+ return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
2193
+ }
2194
+ function getIndentation(content, position) {
2195
+ // Walk backwards to find the start of the line
2196
+ let lineStart = position;
2197
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
2198
+ lineStart--;
2199
+ }
2200
+ // Count spaces/tabs
2201
+ let indentation = '';
2202
+ for (let i = lineStart; i < position && /\s/.test(content[i]); i++) {
2203
+ indentation += content[i];
2204
+ }
2205
+ return indentation || ' '; // Default to 2 spaces if no indentation found
2206
+ }
2207
+ //#endregion
2208
+ //#region Step 3: Transform legacy query creator prepare() calls to add injector
2209
+ function transformLegacyCreatorPrepareCall(tree, classInjectors, pollingInfo) {
2210
+ const updatedFiles = [];
2211
+ // Get all files that have classes with injector OR have standalone function usages
2212
+ const filesByPath = new Map();
2213
+ classInjectors.forEach((classInfo) => {
2214
+ if (!filesByPath.has(classInfo.filePath)) {
2215
+ filesByPath.set(classInfo.filePath, []);
2216
+ }
2217
+ filesByPath.get(classInfo.filePath).push(classInfo);
2218
+ });
2219
+ // Also check files with standalone function usages
2220
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
2221
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
2222
+ return;
2223
+ if (filesByPath.has(filePath))
2224
+ return; // Already processing
2225
+ const content = tree.read(filePath, 'utf-8');
2226
+ if (!content)
2227
+ return;
2228
+ // Only analyze files that have .prepare() calls
2229
+ if (!content.includes('.prepare('))
2230
+ return;
2231
+ const usages = findLegacyCreatorUsages(content, filePath);
2232
+ const functionUsages = usages.filter((u) => u.context === 'function');
2233
+ if (functionUsages.length > 0) {
2234
+ filesByPath.set(filePath, []); // Empty array means standalone functions only
2235
+ }
2236
+ });
2237
+ filesByPath.forEach((classes, filePath) => {
2238
+ let content = tree.read(filePath, 'utf-8');
2239
+ if (!content)
2240
+ return;
2241
+ // Ensure imports are added (for both classes and functions)
2242
+ content = ensureInjectAndInjectorImports(content);
2243
+ // Transform prepare calls for each class and functions
2244
+ const newContent = transformPrepareCallsInFile(content, classes, pollingInfo);
2245
+ if (newContent !== content) {
2246
+ tree.write(filePath, newContent);
2247
+ updatedFiles.push(filePath);
2248
+ }
2249
+ });
2250
+ if (updatedFiles.length > 0) {
2251
+ console.log(`\n✅ Transformed legacy query creator prepare() calls in ${updatedFiles.length} files`);
2252
+ }
2253
+ }
2254
+ function transformPrepareCallsInFile(content, classes, pollingInfo) {
2255
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2256
+ const replacements = [];
2257
+ // Find all legacy creator names
2258
+ const legacyCreatorNames = findLegacyCreatorNames(sourceFile, content);
2259
+ if (legacyCreatorNames.size === 0)
2260
+ return content;
2261
+ // Build a map of class NAME to their injector member name
2262
+ const classNameToInjector = new Map();
2263
+ classes.forEach((classInfo) => {
2264
+ classNameToInjector.set(classInfo.className, classInfo.injectorMemberName);
2265
+ });
2266
+ // Track standalone functions that need injector by their position
2267
+ const functionsNeedingInjector = new Map();
2268
+ function visit(node) {
2269
+ // Find: legacyCreator.prepare(...)
2270
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
2271
+ const propAccess = node.expression;
2272
+ if (ts.isIdentifier(propAccess.name) && propAccess.name.text === 'prepare') {
2273
+ let creatorName;
2274
+ if (ts.isIdentifier(propAccess.expression)) {
2275
+ creatorName = propAccess.expression.text;
2276
+ }
2277
+ if (!creatorName || !legacyCreatorNames.has(creatorName)) {
2278
+ ts.forEachChild(node, visit);
2279
+ return;
2280
+ }
2281
+ // Check if this is in a context that needs injector
2282
+ const context = determineUsageContext(node, sourceFile);
2283
+ if (['queryComputed', 'constructor', 'class-field'].includes(context)) {
2284
+ // Safe contexts that don't need injector or destroyOnResponse
2285
+ ts.forEachChild(node, visit);
2286
+ return;
2287
+ }
2288
+ // Find the containing class or function
2289
+ const containingClass = findContainingClass(node, sourceFile);
2290
+ const containingFunction = findContainingStandaloneFunction(node, sourceFile);
2291
+ if (containingClass) {
2292
+ // Handle class method (NOT class fields - they were filtered above)
2293
+ const className = containingClass.name?.text;
2294
+ if (!className) {
2295
+ ts.forEachChild(node, visit);
2296
+ return;
2297
+ }
2298
+ const injectorMember = classNameToInjector.get(className);
2299
+ if (!injectorMember) {
2300
+ ts.forEachChild(node, visit);
2301
+ return;
2302
+ }
2303
+ // Transform the prepare call
2304
+ const transformedCall = transformSinglePrepareCall(node, `this.${injectorMember}`, sourceFile, pollingInfo);
2305
+ if (transformedCall) {
2306
+ replacements.push({
2307
+ start: node.getStart(sourceFile),
2308
+ end: node.getEnd(),
2309
+ replacement: transformedCall,
2310
+ });
2311
+ }
2312
+ }
2313
+ else if (containingFunction) {
2314
+ // Handle standalone function - find the outermost function with inject()
2315
+ const functionWithInject = findOutermostFunctionWithInject(containingFunction, sourceFile);
2316
+ if (functionWithInject) {
2317
+ const funcPosition = functionWithInject.getStart(sourceFile);
2318
+ // Check if we haven't already processed this function
2319
+ if (!functionsNeedingInjector.has(funcPosition)) {
2320
+ // Check if the function already has an injector parameter
2321
+ const existingInjectorParam = findInjectorParameter(functionWithInject);
2322
+ if (existingInjectorParam) {
2323
+ // Use the existing parameter name
2324
+ functionsNeedingInjector.set(funcPosition, {
2325
+ injectorName: existingInjectorParam,
2326
+ bodyStart: -1, // No declaration needed
2327
+ needsDeclaration: false,
2328
+ });
2329
+ }
2330
+ else {
2331
+ // Need to declare injector
2332
+ let bodyStart;
2333
+ if ((ts.isFunctionDeclaration(functionWithInject) || ts.isFunctionExpression(functionWithInject)) &&
2334
+ functionWithInject.body &&
2335
+ ts.isBlock(functionWithInject.body)) {
2336
+ if (functionWithInject.body.statements.length > 0) {
2337
+ bodyStart = functionWithInject.body.statements[0].getStart(sourceFile);
2338
+ }
2339
+ else {
2340
+ bodyStart = functionWithInject.body.getStart(sourceFile) + 1;
2341
+ }
2342
+ }
2343
+ else if (ts.isArrowFunction(functionWithInject) &&
2344
+ functionWithInject.body &&
2345
+ ts.isBlock(functionWithInject.body)) {
2346
+ if (functionWithInject.body.statements.length > 0) {
2347
+ bodyStart = functionWithInject.body.statements[0].getStart(sourceFile);
2348
+ }
2349
+ else {
2350
+ bodyStart = functionWithInject.body.getStart(sourceFile) + 1;
2351
+ }
2352
+ }
2353
+ if (bodyStart !== undefined) {
2354
+ functionsNeedingInjector.set(funcPosition, {
2355
+ injectorName: 'injector',
2356
+ bodyStart,
2357
+ needsDeclaration: true,
2358
+ });
2359
+ }
2360
+ }
2361
+ }
2362
+ const injectorInfo = functionsNeedingInjector.get(funcPosition);
2363
+ const injectorName = injectorInfo?.injectorName || 'injector';
2364
+ // Transform the prepare call
2365
+ const transformedCall = transformSinglePrepareCall(node, injectorName, sourceFile, pollingInfo);
2366
+ if (transformedCall) {
2367
+ replacements.push({
2368
+ start: node.getStart(sourceFile),
2369
+ end: node.getEnd(),
2370
+ replacement: transformedCall,
2371
+ });
2372
+ }
2373
+ }
2374
+ }
2375
+ }
2376
+ }
2377
+ ts.forEachChild(node, visit);
2378
+ }
2379
+ visit(sourceFile);
2380
+ // Apply injector declarations first (in reverse order) - only for functions that need them
2381
+ let result = content;
2382
+ const injectorDeclarations = [];
2383
+ functionsNeedingInjector.forEach(({ injectorName, bodyStart, needsDeclaration }) => {
2384
+ if (needsDeclaration && bodyStart > 0) {
2385
+ const indentation = getIndentation(content, bodyStart);
2386
+ const injectorDecl = `${indentation}const ${injectorName} = inject(Injector);\n`;
2387
+ injectorDeclarations.push({
2388
+ position: bodyStart,
2389
+ text: injectorDecl,
2390
+ });
2391
+ }
2392
+ });
2393
+ injectorDeclarations.sort((a, b) => b.position - a.position);
2394
+ for (const { position, text } of injectorDeclarations) {
2395
+ result = result.slice(0, position) + text + result.slice(position);
2396
+ }
2397
+ // Then apply prepare call replacements (need to adjust positions for injector insertions)
2398
+ const sortedReplacements = [...replacements].sort((a, b) => b.start - a.start);
2399
+ for (const { start, end, replacement } of sortedReplacements) {
2400
+ // Calculate position adjustment from injector declarations that came before this replacement
2401
+ let adjustment = 0;
2402
+ for (const decl of injectorDeclarations) {
2403
+ if (decl.position <= start) {
2404
+ adjustment += decl.text.length;
2405
+ }
2406
+ }
2407
+ result = result.slice(0, start + adjustment) + replacement + result.slice(end + adjustment);
2408
+ }
2409
+ return result;
2410
+ }
2411
+ function findInjectorParameter(func) {
2412
+ if (!func.parameters || func.parameters.length === 0) {
2413
+ return undefined;
2414
+ }
2415
+ // Look for a parameter named 'injector' or with a type containing 'Injector'
2416
+ for (const param of func.parameters) {
2417
+ if (ts.isIdentifier(param.name)) {
2418
+ const paramName = param.name.text;
2419
+ // Check if parameter is named 'injector'
2420
+ if (paramName.toLowerCase() === 'injector') {
2421
+ return paramName;
2422
+ }
2423
+ // Check if parameter type contains 'Injector'
2424
+ if (param.type) {
2425
+ const typeText = param.type.getText();
2426
+ if (typeText.includes('Injector')) {
2427
+ return paramName;
2428
+ }
2429
+ }
2430
+ }
2431
+ }
2432
+ return undefined;
2433
+ }
2434
+ function findOutermostFunctionWithInject(startFunction, sourceFile) {
2435
+ let current = startFunction;
2436
+ let outermostWithInject;
2437
+ // Walk up the tree looking for functions with inject() calls
2438
+ while (current) {
2439
+ // Stop if we hit a class - we don't want to go outside the function scope
2440
+ if (ts.isClassDeclaration(current)) {
2441
+ break;
2442
+ }
2443
+ // Check if this is a function with inject()
2444
+ if ((ts.isFunctionDeclaration(current) || ts.isArrowFunction(current) || ts.isFunctionExpression(current)) &&
2445
+ containsInjectCall(current)) {
2446
+ outermostWithInject = current;
2447
+ }
2448
+ current = current.parent;
2449
+ }
2450
+ return outermostWithInject;
2451
+ }
2452
+ function findContainingStandaloneFunction(node, sourceFile) {
2453
+ let current = node;
2454
+ while (current) {
2455
+ // Check if we hit a class first - then it's not a standalone function
2456
+ if (ts.isClassDeclaration(current)) {
2457
+ return undefined;
2458
+ }
2459
+ // Check for function declarations, arrow functions, or function expressions
2460
+ if (ts.isFunctionDeclaration(current) || ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
2461
+ return current;
2462
+ }
2463
+ current = current.parent;
2464
+ }
2465
+ return undefined;
2466
+ }
2467
+ function findOrCreateInjectorInFunction(func, sourceFile, functionsNeedingInjector) {
2468
+ // Check if function already has an injector variable
2469
+ const existingInjector = findExistingInjectorVariable(func, sourceFile);
2470
+ if (existingInjector) {
2471
+ return existingInjector;
2472
+ }
2473
+ // Check if we already marked this function for injector creation
2474
+ const existing = functionsNeedingInjector.get(func);
2475
+ if (existing) {
2476
+ return existing;
2477
+ }
2478
+ // Mark for creation
2479
+ const injectorName = 'injector';
2480
+ functionsNeedingInjector.set(func, injectorName);
2481
+ return injectorName;
2482
+ }
2483
+ function findExistingInjectorVariable(func, sourceFile) {
2484
+ const body = func.body;
2485
+ if (!body || !ts.isBlock(body)) {
2486
+ return undefined;
2487
+ }
2488
+ for (const statement of body.statements) {
2489
+ if (ts.isVariableStatement(statement)) {
2490
+ for (const declaration of statement.declarationList.declarations) {
2491
+ if (ts.isIdentifier(declaration.name) &&
2492
+ declaration.initializer &&
2493
+ hasInjectInjectorCall(declaration.initializer)) {
2494
+ return declaration.name.text;
2495
+ }
2496
+ }
2497
+ }
2498
+ }
2499
+ return undefined;
2500
+ }
2501
+ function findContainingClass(node, sourceFile) {
2502
+ let current = node;
2503
+ while (current) {
2504
+ if (ts.isClassDeclaration(current)) {
2505
+ return current;
2506
+ }
2507
+ current = current.parent;
2508
+ }
2509
+ return undefined;
2510
+ }
2511
+ function transformSinglePrepareCall(callNode, injectorReference, sourceFile, pollingInfo) {
2512
+ const propAccess = callNode.expression;
2513
+ const creatorName = propAccess.expression.text;
2514
+ // Get existing arguments
2515
+ const existingArgs = callNode.arguments.length > 0 ? callNode.arguments[0] : undefined;
2516
+ // Build new argument object
2517
+ const newArgProperties = [];
2518
+ let existingConfig;
2519
+ let needsSpread = false;
2520
+ let spreadExpression;
2521
+ // If there's an existing argument and it's an object literal, extract its properties
2522
+ if (existingArgs) {
2523
+ if (ts.isObjectLiteralExpression(existingArgs)) {
2524
+ existingArgs.properties.forEach((prop) => {
2525
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
2526
+ const propName = prop.name.text;
2527
+ // Handle config specially - we need to merge it
2528
+ if (propName === 'config') {
2529
+ existingConfig = prop;
2530
+ return; // Don't add it yet
2531
+ }
2532
+ const propValue = prop.initializer.getText(sourceFile);
2533
+ newArgProperties.push(`${propName}: ${propValue}`);
2534
+ }
2535
+ else if (ts.isShorthandPropertyAssignment(prop)) {
2536
+ const propName = prop.name.text;
2537
+ newArgProperties.push(propName);
2538
+ }
2539
+ else if (ts.isSpreadAssignment(prop)) {
2540
+ // Existing spread - preserve it
2541
+ spreadExpression = prop.expression.getText(sourceFile);
2542
+ needsSpread = true;
2543
+ }
2544
+ });
2545
+ }
2546
+ else {
2547
+ // If it's not an object literal (e.g., a variable or expression), we need to spread it
2548
+ spreadExpression = existingArgs.getText(sourceFile);
2549
+ needsSpread = true;
2550
+ }
2551
+ }
2552
+ // Add injector
2553
+ newArgProperties.push(`injector: ${injectorReference}`);
2554
+ // Conditionally add or merge config
2555
+ const queryVariableName = findQueryVariableNameForPrepareCall(callNode, sourceFile);
2556
+ const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo);
2557
+ if (existingConfig) {
2558
+ // Merge with existing config
2559
+ const existingConfigObj = existingConfig.initializer;
2560
+ if (ts.isObjectLiteralExpression(existingConfigObj)) {
2561
+ const configProps = [];
2562
+ // Add existing config properties
2563
+ existingConfigObj.properties.forEach((prop) => {
2564
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
2565
+ const propName = prop.name.text;
2566
+ const propValue = prop.initializer.getText(sourceFile);
2567
+ configProps.push(`${propName}: ${propValue}`);
2568
+ }
2569
+ else if (ts.isShorthandPropertyAssignment(prop)) {
2570
+ const propName = prop.name.text;
2571
+ configProps.push(propName);
2572
+ }
2573
+ });
2574
+ // Add destroyOnResponse if needed
2575
+ if (addDestroyOnResponse) {
2576
+ configProps.push('destroyOnResponse: true');
2577
+ }
2578
+ if (configProps.length > 2) {
2579
+ newArgProperties.push(`config: {\n ${configProps.join(',\n ')}\n }`);
2580
+ }
2581
+ else {
2582
+ newArgProperties.push(`config: { ${configProps.join(', ')} }`);
2583
+ }
2584
+ }
2585
+ else {
2586
+ // Config is not an object literal (e.g., a variable) - can't merge
2587
+ // Just keep the existing config
2588
+ newArgProperties.push(`config: ${existingConfigObj.getText(sourceFile)}`);
2589
+ }
2590
+ }
2591
+ else if (addDestroyOnResponse) {
2592
+ // No existing config - add destroyOnResponse
2593
+ newArgProperties.push(`config: { destroyOnResponse: true }`);
2594
+ }
2595
+ // Format based on whether we need to spread and number of properties
2596
+ let argsString;
2597
+ if (needsSpread && spreadExpression) {
2598
+ // Need to spread the existing argument
2599
+ if (newArgProperties.length <= 2 && !newArgProperties.some((p) => p.includes('\n'))) {
2600
+ // Single line for short objects
2601
+ argsString = `{ ...${spreadExpression}, ${newArgProperties.join(', ')} }`;
2602
+ }
2603
+ else {
2604
+ // Multi-line for longer objects
2605
+ argsString = `{\n ...${spreadExpression},\n ${newArgProperties.join(',\n ')}\n}`;
2606
+ }
2607
+ }
2608
+ else {
2609
+ // No spread needed
2610
+ if (newArgProperties.length <= 2 && !newArgProperties.some((p) => p.includes('\n'))) {
2611
+ // Single line for short objects
2612
+ argsString = `{ ${newArgProperties.join(', ')} }`;
2613
+ }
2614
+ else {
2615
+ // Multi-line for longer objects
2616
+ argsString = `{\n ${newArgProperties.join(',\n ')}\n}`;
2617
+ }
2618
+ }
2619
+ return `${creatorName}.prepare(${argsString})`;
2620
+ }
2621
+ function findQueryVariableNameForPrepareCall(prepareCallNode, sourceFile) {
2622
+ // Walk up to find if this prepare call is assigned to a variable
2623
+ // e.g., const query = legacyGetUsers.prepare()
2624
+ let parent = prepareCallNode.parent;
2625
+ while (parent) {
2626
+ // Check if assigned to a variable
2627
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
2628
+ return parent.name.text;
2629
+ }
2630
+ // Check if assigned to a class property
2631
+ if (ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name)) {
2632
+ return parent.name.text;
2633
+ }
2634
+ // Check if it's a return statement - look for the method/function name
2635
+ if (ts.isReturnStatement(parent)) {
2636
+ const containingFunction = findContainingMethod(parent, sourceFile);
2637
+ if (containingFunction) {
2638
+ return containingFunction;
2639
+ }
2640
+ }
2641
+ parent = parent.parent;
2642
+ }
2643
+ return undefined;
2644
+ }
2645
+ function findContainingMethod(node, sourceFile) {
2646
+ let current = node;
2647
+ while (current) {
2648
+ if (ts.isMethodDeclaration(current) && ts.isIdentifier(current.name)) {
2649
+ return current.name.text;
2650
+ }
2651
+ if (ts.isFunctionDeclaration(current) && current.name && ts.isIdentifier(current.name)) {
2652
+ return current.name.text;
2653
+ }
2654
+ if (ts.isArrowFunction(current)) {
2655
+ // Check if it's assigned to a variable
2656
+ const parent = current.parent;
2657
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
2658
+ return parent.name.text;
2659
+ }
2660
+ }
2661
+ current = current.parent;
2662
+ }
2663
+ return undefined;
2664
+ }
2665
+ function reportManualReviewLocations(tree, allUsages) {
2666
+ const needsReview = [];
2667
+ allUsages.forEach((usage) => {
2668
+ // Check if this usage needs manual review
2669
+ if (usage.context === 'function') {
2670
+ // For function context, we need to check if it was in a function WITHOUT inject()
2671
+ // This means it should have been flagged for manual review during transformation
2672
+ // We can check if the function had inject by looking at the original file
2673
+ // (before transformation) since we need to report the original line number anyway
2674
+ const originalContent = tree.read(usage.filePath, 'utf-8');
2675
+ if (!originalContent)
2676
+ return;
2677
+ // Parse the CURRENT file to check if inject was added
2678
+ // But use the ORIGINAL line number from usage
2679
+ const sourceFile = ts.createSourceFile(usage.filePath, originalContent, ts.ScriptTarget.Latest, true);
2680
+ // Find any function that contains a .prepare() call around the original position
2681
+ let foundFunctionWithoutInject = false;
2682
+ function checkFunctions(node) {
2683
+ // Look for functions that might contain our usage
2684
+ if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
2685
+ const funcStart = node.getStart(sourceFile);
2686
+ const funcEnd = node.getEnd();
2687
+ // Check if our usage position is within this function
2688
+ if (usage.position.start >= funcStart && usage.position.end <= funcEnd) {
2689
+ // Check if this function or any parent function has inject()
2690
+ if (!containsInjectCall(node) && !findOutermostFunctionWithInject(node, sourceFile)) {
2691
+ foundFunctionWithoutInject = true;
2692
+ }
2693
+ }
2694
+ }
2695
+ ts.forEachChild(node, checkFunctions);
2696
+ }
2697
+ checkFunctions(sourceFile);
2698
+ if (foundFunctionWithoutInject) {
2699
+ needsReview.push({
2700
+ filePath: usage.filePath,
2701
+ line: usage.line, // Use the original line number from Step 1
2702
+ creatorName: usage.creatorName,
2703
+ reason: 'Used in standalone function without inject() - may need manual injector passing',
2704
+ });
2705
+ }
2706
+ }
2707
+ else if (usage.context === 'unknown') {
2708
+ needsReview.push({
2709
+ filePath: usage.filePath,
2710
+ line: usage.line, // Use the original line number from Step 1
2711
+ creatorName: usage.creatorName,
2712
+ reason: 'Could not determine execution context - please verify manually',
2713
+ });
2714
+ }
2715
+ });
2716
+ if (needsReview.length > 0) {
2717
+ console.warn(`\n⚠️ Found ${needsReview.length} locations that may need manual review:`);
2718
+ // Group by file for cleaner output
2719
+ const byFile = new Map();
2720
+ needsReview.forEach((location) => {
2721
+ if (!byFile.has(location.filePath)) {
2722
+ byFile.set(location.filePath, []);
2723
+ }
2724
+ byFile.get(location.filePath).push(location);
2725
+ });
2726
+ byFile.forEach((locations, filePath) => {
2727
+ console.warn(`\n 📄 ${filePath}:`);
2728
+ locations.forEach((location) => {
2729
+ console.warn(` Line ${location.line}: ${location.creatorName}`);
2730
+ console.warn(` → ${location.reason}`);
2731
+ });
2732
+ });
2733
+ console.warn('\n💡 Solve these warnings by:');
2734
+ console.warn(' - [HIGHLY RECOMMENDED] Passing an injector as a parameter to the prepare method call alongside a config: { destroyOnResponse: true }');
2735
+ console.warn(' - Using queryComputed() if appropriate');
2736
+ console.warn(' - Putting the prepare call into an injection context');
2737
+ }
2738
+ }
2739
+ //#endregion
2740
+ //#region Helper functions for inject detection
2741
+ function containsInjectCall(node) {
2742
+ let hasInject = false;
2743
+ function visit(n) {
2744
+ if (hasInject)
2745
+ return;
2746
+ // Check for direct inject() call
2747
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === 'inject') {
2748
+ hasInject = true;
2749
+ return;
2750
+ }
2751
+ // Check for any function call that starts with "inject"
2752
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
2753
+ if (n.expression.text.startsWith('inject')) {
2754
+ hasInject = true;
2755
+ return;
2756
+ }
2757
+ }
2758
+ ts.forEachChild(n, visit);
2759
+ }
2760
+ visit(node);
2761
+ return hasInject;
2762
+ }
2763
+ //#endregion
2764
+ //#region Migrate empty .prepare() calls to .prepare({})
2765
+ function migrateEmptyPrepareCalls(tree) {
2766
+ const updatedFiles = [];
2767
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
2768
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
2769
+ return;
2770
+ const content = tree.read(filePath, 'utf-8');
2771
+ if (!content)
2772
+ return;
2773
+ // Only analyze files that have .prepare() calls
2774
+ if (!content.includes('.prepare()'))
2775
+ return;
2776
+ const newContent = transformEmptyPrepareCalls(content);
2777
+ if (newContent !== content) {
2778
+ tree.write(filePath, newContent);
2779
+ updatedFiles.push(filePath);
2780
+ }
2781
+ });
2782
+ if (updatedFiles.length > 0) {
2783
+ console.log(`\n✅ Migrated empty .prepare() calls to .prepare({}) in ${updatedFiles.length} files`);
2784
+ }
2785
+ }
2786
+ function transformEmptyPrepareCalls(content) {
2787
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
2788
+ const replacements = [];
2789
+ function visit(node) {
2790
+ // Find: something.prepare()
2791
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
2792
+ const propAccess = node.expression;
2793
+ if (ts.isIdentifier(propAccess.name) && propAccess.name.text === 'prepare') {
2794
+ // Check if it has no arguments
2795
+ if (node.arguments.length === 0) {
2796
+ // Get the full text of the call: "something.prepare()"
2797
+ const callStart = node.getStart(sourceFile);
2798
+ const callEnd = node.getEnd();
2799
+ const callText = content.slice(callStart, callEnd);
2800
+ // Replace with: "something.prepare({})"
2801
+ const replacement = callText.replace(/\.prepare\(\)/, '.prepare({})');
2802
+ replacements.push({
2803
+ start: callStart,
2804
+ end: callEnd,
2805
+ replacement,
2806
+ });
2807
+ }
2808
+ }
2809
+ }
2810
+ ts.forEachChild(node, visit);
2811
+ }
2812
+ visit(sourceFile);
2813
+ // Apply replacements in reverse order
2814
+ let result = content;
2815
+ replacements.sort((a, b) => b.start - a.start);
2816
+ for (const { start, end, replacement } of replacements) {
2817
+ result = result.slice(0, start) + replacement + result.slice(end);
2818
+ }
2819
+ return result;
2820
+ }
2821
+ function detectPollingUsage(tree) {
2822
+ const pollingInfo = new Map();
2823
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
2824
+ if (filePath.endsWith('.spec.ts'))
2825
+ return;
2826
+ const content = tree.read(filePath, 'utf-8');
2827
+ if (!content)
2828
+ return;
2829
+ // Check TypeScript files
2830
+ if (filePath.endsWith('.ts')) {
2831
+ detectPollingInTypeScript(content, filePath, pollingInfo);
2832
+ }
2833
+ // Check HTML templates
2834
+ if (filePath.endsWith('.html')) {
2835
+ detectPollingInTemplate(content, filePath, pollingInfo);
2836
+ }
2837
+ });
2838
+ return pollingInfo;
2839
+ }
2840
+ function detectPollingInTypeScript(content, filePath, pollingInfo) {
2841
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
2842
+ function visit(node) {
2843
+ // Find .poll() or .stopPolling() calls
2844
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
2845
+ const propAccess = node.expression;
2846
+ const methodName = propAccess.name;
2847
+ if (ts.isIdentifier(methodName) && (methodName.text === 'poll' || methodName.text === 'stopPolling')) {
2848
+ // Walk back to find the root variable name
2849
+ const queryVariableName = getQueryVariableName(propAccess.expression, sourceFile);
2850
+ if (queryVariableName) {
2851
+ const existing = pollingInfo.get(queryVariableName);
2852
+ if (existing) {
2853
+ existing.hasPolling = true;
2854
+ existing.locations.push(`${filePath}:${getLineNumber(node, sourceFile)} (.${methodName.text})`);
2855
+ }
2856
+ else {
2857
+ pollingInfo.set(queryVariableName, {
2858
+ queryVariableName,
2859
+ hasPolling: true,
2860
+ locations: [`${filePath}:${getLineNumber(node, sourceFile)} (.${methodName.text})`],
2861
+ });
2862
+ }
2863
+ }
2864
+ }
2865
+ }
2866
+ ts.forEachChild(node, visit);
2867
+ }
2868
+ visit(sourceFile);
2869
+ }
2870
+ function getQueryVariableName(expr, sourceFile) {
2871
+ // Walk back through the chain to find the root variable
2872
+ let current = expr;
2873
+ while (true) {
2874
+ if (ts.isIdentifier(current)) {
2875
+ return current.text;
2876
+ }
2877
+ if (ts.isPropertyAccessExpression(current)) {
2878
+ current = current.expression;
2879
+ continue;
2880
+ }
2881
+ if (ts.isCallExpression(current)) {
2882
+ // For chained calls like query.prepare().execute().poll()
2883
+ // We want to get the root query variable
2884
+ current = current.expression;
2885
+ continue;
2886
+ }
2887
+ // Can't determine the root variable
2888
+ return undefined;
2889
+ }
2890
+ }
2891
+ function detectPollingInTemplate(content, filePath, pollingInfo) {
2892
+ // Use regex to find .poll() or .stopPolling() in templates
2893
+ // This is not perfect but catches most cases
2894
+ const pollRegex = /(\w+)\.poll\(\)/g;
2895
+ const stopPollingRegex = /(\w+)\.stopPolling\(\)/g;
2896
+ let match;
2897
+ // Find .poll() calls
2898
+ while ((match = pollRegex.exec(content)) !== null) {
2899
+ const queryVariableName = match[1];
2900
+ const lineNumber = getLineNumberFromPosition(content, match.index);
2901
+ const existing = pollingInfo.get(queryVariableName);
2902
+ if (existing) {
2903
+ existing.hasPolling = true;
2904
+ existing.locations.push(`${filePath}:${lineNumber} (.poll() in template)`);
2905
+ }
2906
+ else {
2907
+ pollingInfo.set(queryVariableName, {
2908
+ queryVariableName,
2909
+ hasPolling: true,
2910
+ locations: [`${filePath}:${lineNumber} (.poll() in template)`],
2911
+ });
2912
+ }
2913
+ }
2914
+ // Find .stopPolling() calls
2915
+ while ((match = stopPollingRegex.exec(content)) !== null) {
2916
+ const queryVariableName = match[1];
2917
+ const lineNumber = getLineNumberFromPosition(content, match.index);
2918
+ const existing = pollingInfo.get(queryVariableName);
2919
+ if (existing) {
2920
+ existing.hasPolling = true;
2921
+ existing.locations.push(`${filePath}:${lineNumber} (.stopPolling() in template)`);
2922
+ }
2923
+ else {
2924
+ pollingInfo.set(queryVariableName, {
2925
+ queryVariableName,
2926
+ hasPolling: true,
2927
+ locations: [`${filePath}:${lineNumber} (.stopPolling() in template)`],
2928
+ });
2929
+ }
2930
+ }
2931
+ }
2932
+ function getLineNumberFromPosition(content, position) {
2933
+ const lines = content.slice(0, position).split('\n');
2934
+ return lines.length;
2935
+ }
2936
+ function shouldAddDestroyOnResponse(queryVariableName, pollingInfo) {
2937
+ if (!queryVariableName) {
2938
+ // Can't determine - be conservative and don't add destroyOnResponse
2939
+ return false;
2940
+ }
2941
+ const info = pollingInfo.get(queryVariableName);
2942
+ // If we found polling usage, don't add destroyOnResponse
2943
+ if (info && info.hasPolling) {
2944
+ return false;
2945
+ }
2946
+ // No polling detected - safe to add destroyOnResponse
2947
+ return true;
2948
+ }
2949
+ //#endregion
2950
+ // TODO:
2951
+ // - Only add destroyOnResponse if we are 00% sure there is no .poll / stopPolling calls on it
2952
+ //# sourceMappingURL=migration.js.map