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

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