@ethlete/query 5.41.4 → 5.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1744 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = migrate;
4
+ const tslib_1 = require("tslib");
5
+ const devkit_1 = require("@nx/devkit");
6
+ const ts = tslib_1.__importStar(require("typescript"));
7
+ async function migrate(tree, schema) {
8
+ const queryClientFiles = new Map(); // filepath -> config names
9
+ const variableRenames = new Map(); // old name -> new name
10
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
11
+ if (!filePath.endsWith('.ts'))
12
+ return;
13
+ const content = tree.read(filePath, 'utf-8');
14
+ if (!content)
15
+ return;
16
+ let modified = false;
17
+ let newContent = content;
18
+ if (needsExperimentalQueryImport(content)) {
19
+ newContent = addExperimentalQueryImport(newContent);
20
+ modified = true;
21
+ }
22
+ if (hasQueryClientInstantiation(content)) {
23
+ const renames = new Map();
24
+ newContent = migrateQueryClientToConfig(newContent, renames);
25
+ modified = true;
26
+ // Store the renames for updating imports in other files
27
+ renames.forEach((newName, oldName) => {
28
+ variableRenames.set(oldName, newName);
29
+ });
30
+ const configNames = extractClientConfigNames(newContent);
31
+ if (configNames.length > 0) {
32
+ queryClientFiles.set(filePath, configNames);
33
+ }
34
+ }
35
+ if (modified) {
36
+ tree.write(filePath, newContent);
37
+ }
38
+ });
39
+ if (queryClientFiles.size > 0) {
40
+ console.log('\n✅ Migrated QueryClient instantiations in:');
41
+ queryClientFiles.forEach((configs, file) => {
42
+ console.log(` - ${file} (${configs.join(', ')})`);
43
+ });
44
+ // Generate query creators for the configs
45
+ generateQueryCreators(tree, queryClientFiles);
46
+ // Generate inject functions for the configs
47
+ generateInjectFunctions(tree, queryClientFiles);
48
+ // Create new query creators based on legacy ones (this also creates legacy wrappers)
49
+ createNewQueryCreators(tree, queryClientFiles);
50
+ // Update imports to include legacy wrappers where needed
51
+ updateLegacyCreatorImportsAndUsages(tree, queryClientFiles);
52
+ // Find and update dependent apps
53
+ updateDependentApps(tree, queryClientFiles);
54
+ }
55
+ // Update imports in all files
56
+ if (variableRenames.size > 0) {
57
+ updateImportsAcrossWorkspace(tree, variableRenames);
58
+ }
59
+ // Replace AnyQuery with E.AnyLegacyQuery everywhere
60
+ replaceAnyQueryWithLegacy(tree);
61
+ // Remove devtools usage
62
+ removeDevtoolsUsage(tree);
63
+ if (!schema.skipFormat) {
64
+ await (0, devkit_1.formatFiles)(tree);
65
+ }
66
+ }
67
+ //#endregion
68
+ //#region import ExperimentalQuery as E
69
+ function needsExperimentalQueryImport(content) {
70
+ // Check if file imports from @ethlete/query
71
+ const hasQueryImport = /from\s+['"]@ethlete\/query['"]/.test(content);
72
+ // Check if already has ExperimentalQuery import
73
+ const hasExperimentalQueryImport = /import\s*{[^}]*ExperimentalQuery/.test(content);
74
+ return hasQueryImport && !hasExperimentalQueryImport;
75
+ }
76
+ function addExperimentalQueryImport(content) {
77
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
78
+ let queryImportNode;
79
+ ts.forEachChild(sourceFile, (node) => {
80
+ if (ts.isImportDeclaration(node) &&
81
+ ts.isStringLiteral(node.moduleSpecifier) &&
82
+ node.moduleSpecifier.text === '@ethlete/query') {
83
+ queryImportNode = node;
84
+ }
85
+ });
86
+ if (!queryImportNode || !queryImportNode.importClause?.namedBindings) {
87
+ return content;
88
+ }
89
+ const namedBindings = queryImportNode.importClause.namedBindings;
90
+ if (!ts.isNamedImports(namedBindings)) {
91
+ return content;
92
+ }
93
+ const importStart = queryImportNode.getStart(sourceFile);
94
+ const importEnd = queryImportNode.getEnd();
95
+ const existingImports = namedBindings.elements.map((el) => el.getText(sourceFile));
96
+ const newImports = [...existingImports, 'ExperimentalQuery as E'];
97
+ const newImportStatement = `import { ${newImports.join(', ')} } from '@ethlete/query';`;
98
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
99
+ }
100
+ //#endregion
101
+ //#region Query client to config
102
+ function hasQueryClientInstantiation(content) {
103
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
104
+ let hasQueryClientImport = false;
105
+ let hasNewQueryClient = false;
106
+ ts.forEachChild(sourceFile, function visit(node) {
107
+ if (ts.isImportDeclaration(node) &&
108
+ ts.isStringLiteral(node.moduleSpecifier) &&
109
+ node.moduleSpecifier.text === '@ethlete/query' &&
110
+ node.importClause?.namedBindings &&
111
+ ts.isNamedImports(node.importClause.namedBindings)) {
112
+ const hasQueryClient = node.importClause.namedBindings.elements.some((el) => el.name.text === 'QueryClient');
113
+ if (hasQueryClient) {
114
+ hasQueryClientImport = true;
115
+ }
116
+ }
117
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'QueryClient') {
118
+ hasNewQueryClient = true;
119
+ }
120
+ ts.forEachChild(node, visit);
121
+ });
122
+ return hasQueryClientImport && hasNewQueryClient;
123
+ }
124
+ function migrateQueryClientToConfig(content, renamesOut) {
125
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
126
+ let result = content;
127
+ const replacements = [];
128
+ const variableRenames = new Map(); // old name -> new name
129
+ function visit(node) {
130
+ // Find: new QueryClient({ baseRoute: '...' })
131
+ if (ts.isNewExpression(node) &&
132
+ ts.isIdentifier(node.expression) &&
133
+ node.expression.text === 'QueryClient' &&
134
+ node.arguments &&
135
+ node.arguments.length > 0) {
136
+ const configArg = node.arguments[0];
137
+ if (ts.isObjectLiteralExpression(configArg)) {
138
+ const oldVariableName = getVariableNameForQueryClient(node, sourceFile);
139
+ const newVariableName = ensureConfigSuffix(oldVariableName);
140
+ if (oldVariableName !== newVariableName) {
141
+ variableRenames.set(oldVariableName, newVariableName);
142
+ if (renamesOut) {
143
+ renamesOut.set(oldVariableName, newVariableName);
144
+ }
145
+ }
146
+ const migrated = migrateConfigObject(configArg, node, sourceFile);
147
+ replacements.push({
148
+ start: node.getStart(sourceFile),
149
+ end: node.getEnd(),
150
+ replacement: migrated,
151
+ });
152
+ }
153
+ }
154
+ ts.forEachChild(node, visit);
155
+ }
156
+ visit(sourceFile);
157
+ // Apply replacements in reverse order to maintain positions
158
+ replacements.sort((a, b) => b.start - a.start);
159
+ for (const { start, end, replacement } of replacements) {
160
+ result = result.slice(0, start) + replacement + result.slice(end);
161
+ }
162
+ // Remove QueryClient from imports
163
+ result = removeQueryClientImport(result);
164
+ // Rename variables throughout the file
165
+ result = renameVariables(result, variableRenames);
166
+ return result;
167
+ }
168
+ function ensureConfigSuffix(name) {
169
+ // If already ends with Config, return as is
170
+ if (/Config$/i.test(name)) {
171
+ return name;
172
+ }
173
+ // Just add Config to the end, keeping everything including "Client"
174
+ return `${name}Config`;
175
+ }
176
+ function removeQueryClientImport(content) {
177
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
178
+ let queryImportNode;
179
+ ts.forEachChild(sourceFile, (node) => {
180
+ if (ts.isImportDeclaration(node) &&
181
+ ts.isStringLiteral(node.moduleSpecifier) &&
182
+ node.moduleSpecifier.text === '@ethlete/query') {
183
+ queryImportNode = node;
184
+ }
185
+ });
186
+ if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
187
+ return content;
188
+ }
189
+ const namedBindings = queryImportNode.importClause.namedBindings;
190
+ const elements = namedBindings.elements.filter((el) => el.name.text !== 'QueryClient');
191
+ // If no imports left, remove the entire import statement
192
+ if (elements.length === 0) {
193
+ const importStart = queryImportNode.getStart(sourceFile);
194
+ const importEnd = queryImportNode.getEnd();
195
+ // Also remove the newline after the import
196
+ const nextChar = content[importEnd];
197
+ const endPos = nextChar === '\n' ? importEnd + 1 : importEnd;
198
+ return content.slice(0, importStart) + content.slice(endPos);
199
+ }
200
+ // Otherwise, reconstruct import without QueryClient
201
+ const importStart = queryImportNode.getStart(sourceFile);
202
+ const importEnd = queryImportNode.getEnd();
203
+ const newImports = elements.map((el) => el.getText(sourceFile));
204
+ const newImportStatement = `import { ${newImports.join(', ')} } from '@ethlete/query';`;
205
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
206
+ }
207
+ function renameVariables(content, renames) {
208
+ if (renames.size === 0)
209
+ return content;
210
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
211
+ const replacements = [];
212
+ function visit(node) {
213
+ // Rename variable declarations
214
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
215
+ const oldName = node.name.text;
216
+ const newName = renames.get(oldName);
217
+ if (newName) {
218
+ replacements.push({
219
+ start: node.name.getStart(sourceFile),
220
+ end: node.name.getEnd(),
221
+ replacement: newName,
222
+ });
223
+ }
224
+ }
225
+ else if (ts.isIdentifier(node)) {
226
+ const oldName = node.text;
227
+ const newName = renames.get(oldName);
228
+ if (newName) {
229
+ // Make sure we're not renaming property names in object literals
230
+ const parent = node.parent;
231
+ if (!ts.isPropertyAssignment(parent) || parent.name !== node) {
232
+ replacements.push({
233
+ start: node.getStart(sourceFile),
234
+ end: node.getEnd(),
235
+ replacement: newName,
236
+ });
237
+ }
238
+ }
239
+ }
240
+ ts.forEachChild(node, visit);
241
+ }
242
+ visit(sourceFile);
243
+ // Apply replacements in reverse order
244
+ let result = content;
245
+ // remove duplicates from replacements
246
+ const seen = new Set();
247
+ const uniqueReplacements = replacements.filter((r) => {
248
+ const key = `${r.start}-${r.end}`;
249
+ if (seen.has(key))
250
+ return false;
251
+ seen.add(key);
252
+ return true;
253
+ });
254
+ uniqueReplacements.sort((a, b) => b.start - a.start);
255
+ for (const { start, end, replacement } of uniqueReplacements) {
256
+ result = result.slice(0, start) + replacement + result.slice(end);
257
+ }
258
+ return result;
259
+ }
260
+ function migrateConfigObject(configArg, node, sourceFile) {
261
+ const variableName = getVariableNameForQueryClient(node, sourceFile);
262
+ const newConfig = [];
263
+ // Extract baseRoute -> baseUrl
264
+ const baseRouteProp = configArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'baseRoute');
265
+ if (baseRouteProp) {
266
+ const baseRouteValue = baseRouteProp.initializer.getText(sourceFile);
267
+ const oldBseUrlAsCast = 'as `https://${string}`';
268
+ const cleanedBaseRouteValue = baseRouteValue.replace(oldBseUrlAsCast, '').trim();
269
+ newConfig.push(`baseUrl: ${cleanedBaseRouteValue}`);
270
+ }
271
+ newConfig.push(`name: '${variableName}'`);
272
+ // Extract request.queryParams -> queryString
273
+ const requestProp = configArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'request');
274
+ if (requestProp && ts.isObjectLiteralExpression(requestProp.initializer)) {
275
+ const requestObj = requestProp.initializer;
276
+ const queryParamsProp = requestObj.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'queryParams');
277
+ if (queryParamsProp) {
278
+ const queryParamsValue = queryParamsProp.initializer.getText(sourceFile);
279
+ newConfig.push(`queryString: ${queryParamsValue}`);
280
+ }
281
+ const cacheAdapterProp = requestObj.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'cacheAdapter');
282
+ if (cacheAdapterProp) {
283
+ const cacheAdapterValue = cacheAdapterProp.initializer.getText(sourceFile);
284
+ newConfig.push(`cacheAdapter: ${cacheAdapterValue}`);
285
+ }
286
+ const retryFnProp = requestObj.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'retryFn');
287
+ if (retryFnProp) {
288
+ const retryFnValue = retryFnProp.initializer.getText(sourceFile);
289
+ newConfig.push(`retryFn: ${retryFnValue}`);
290
+ }
291
+ }
292
+ return `E.createQueryClientConfig({\n ${newConfig.join(',\n ')}\n})`;
293
+ }
294
+ function getVariableNameForQueryClient(node, sourceFile) {
295
+ let parent = node.parent;
296
+ // Walk up to find variable declaration
297
+ while (parent) {
298
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
299
+ return parent.name.text;
300
+ }
301
+ parent = parent.parent;
302
+ }
303
+ return 'client';
304
+ }
305
+ function extractClientConfigNames(content) {
306
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
307
+ const configNames = [];
308
+ function visit(node) {
309
+ // Find: const XXX = E.createQueryClientConfig(...)
310
+ if (ts.isVariableDeclaration(node) &&
311
+ ts.isIdentifier(node.name) &&
312
+ node.initializer &&
313
+ ts.isCallExpression(node.initializer)) {
314
+ const callExpr = node.initializer;
315
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
316
+ ts.isIdentifier(callExpr.expression.expression) &&
317
+ callExpr.expression.expression.text === 'E' &&
318
+ ts.isIdentifier(callExpr.expression.name) &&
319
+ callExpr.expression.name.text === 'createQueryClientConfig') {
320
+ // This extracts the NEW variable name (apiClientConfig)
321
+ // But we should extract the name from the config object instead!
322
+ const nameArg = callExpr.arguments[0];
323
+ if (nameArg && ts.isObjectLiteralExpression(nameArg)) {
324
+ const nameProp = nameArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'name');
325
+ if (nameProp && ts.isStringLiteral(nameProp.initializer)) {
326
+ configNames.push(nameProp.initializer.text); // Extract from 'name' property
327
+ }
328
+ }
329
+ }
330
+ }
331
+ ts.forEachChild(node, visit);
332
+ }
333
+ visit(sourceFile);
334
+ return configNames;
335
+ }
336
+ //#endregion
337
+ //#region Add query client providers to apps
338
+ function updateDependentApps(tree, queryClientFiles) {
339
+ const projects = (0, devkit_1.getProjects)(tree);
340
+ const updatedApps = [];
341
+ const warnedApps = [];
342
+ // Find all apps
343
+ for (const [projectName, projectConfig] of projects.entries()) {
344
+ if (projectConfig.projectType !== 'application')
345
+ continue;
346
+ const configPaths = [`${projectConfig.root}/src/app/app.config.ts`, `${projectConfig.root}/src/main.ts`];
347
+ let foundImport = false;
348
+ for (const configPath of configPaths) {
349
+ if (!tree.exists(configPath))
350
+ continue;
351
+ const content = tree.read(configPath, 'utf-8');
352
+ if (!content)
353
+ continue;
354
+ // Check if this app imports any of the migrated client configs
355
+ const importedConfigs = findImportedClientConfigs(content, queryClientFiles);
356
+ if (importedConfigs.length > 0) {
357
+ const newContent = addQueryClientProviders(content, importedConfigs);
358
+ if (newContent !== content) {
359
+ tree.write(configPath, newContent);
360
+ }
361
+ updatedApps.push(projectName);
362
+ foundImport = true;
363
+ break;
364
+ }
365
+ }
366
+ if (!foundImport) {
367
+ warnedApps.push(projectName);
368
+ }
369
+ }
370
+ if (updatedApps.length > 0) {
371
+ console.log('\n✅ Updated applications with query client providers:');
372
+ updatedApps.forEach((app) => console.log(` - ${app}`));
373
+ }
374
+ if (warnedApps.length > 0) {
375
+ console.warn('\n⚠️ The following applications may need manual updates for query client providers:');
376
+ warnedApps.forEach((app) => console.warn(` - ${app}`));
377
+ }
378
+ }
379
+ function findImportedClientConfigs(content, queryClientFiles) {
380
+ const importedConfigs = [];
381
+ // Get all config names from migrated files (these are the OLD names like 'apiClient')
382
+ const allConfigNames = new Set();
383
+ queryClientFiles.forEach((configs) => configs.forEach((c) => allConfigNames.add(c)));
384
+ // Check if any of these configs are imported in this file
385
+ for (const configName of allConfigNames) {
386
+ const importRegex = new RegExp(`\\b${configName}\\b`);
387
+ if (importRegex.test(content)) {
388
+ // We need to add the Config suffix because the actual variable name is apiClientConfig
389
+ const configNameWithSuffix = ensureConfigSuffix(configName);
390
+ importedConfigs.push(configNameWithSuffix);
391
+ }
392
+ }
393
+ return importedConfigs;
394
+ }
395
+ function addQueryClientProviders(content, clientConfigs) {
396
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
397
+ let providersArrayNode;
398
+ function visit(node) {
399
+ if (ts.isPropertyAssignment(node) &&
400
+ ts.isIdentifier(node.name) &&
401
+ node.name.text === 'providers' &&
402
+ ts.isArrayLiteralExpression(node.initializer)) {
403
+ providersArrayNode = node.initializer;
404
+ }
405
+ ts.forEachChild(node, visit);
406
+ }
407
+ visit(sourceFile);
408
+ if (!providersArrayNode)
409
+ return content;
410
+ const newProviders = clientConfigs.map((config) => `E.provideQueryClient(${config})`);
411
+ const elements = providersArrayNode.elements;
412
+ if (elements.length > 0) {
413
+ const insertPosition = elements[elements.length - 1].getEnd();
414
+ const insertion = `,\n ${newProviders.join(',\n ')}`;
415
+ return content.slice(0, insertPosition) + insertion + content.slice(insertPosition);
416
+ }
417
+ else {
418
+ const insertPosition = providersArrayNode.getStart(sourceFile) + 1;
419
+ const insertion = `\n ${newProviders.join(',\n ')}\n `;
420
+ return content.slice(0, insertPosition) + insertion + content.slice(insertPosition);
421
+ }
422
+ }
423
+ //#endregion
424
+ //#region Update imports across workspace
425
+ function updateImportsAcrossWorkspace(tree, renames) {
426
+ const updatedFiles = [];
427
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
428
+ if (!filePath.endsWith('.ts'))
429
+ return;
430
+ const content = tree.read(filePath, 'utf-8');
431
+ if (!content)
432
+ return;
433
+ const newContent = updateImportsInFile(content, renames);
434
+ if (newContent !== content) {
435
+ tree.write(filePath, newContent);
436
+ updatedFiles.push(filePath);
437
+ }
438
+ });
439
+ }
440
+ function updateImportsInFile(content, renames) {
441
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
442
+ const replacements = [];
443
+ function visit(node) {
444
+ // Update named imports
445
+ if (ts.isImportDeclaration(node) &&
446
+ node.importClause?.namedBindings &&
447
+ ts.isNamedImports(node.importClause.namedBindings)) {
448
+ const namedBindings = node.importClause.namedBindings;
449
+ let hasChanges = false;
450
+ const newElements = [];
451
+ namedBindings.elements.forEach((element) => {
452
+ const importedName = element.name.text;
453
+ const newName = renames.get(importedName);
454
+ if (newName) {
455
+ hasChanges = true;
456
+ // If there's an alias, keep it: import { old as alias } -> import { new as alias }
457
+ if (element.propertyName) {
458
+ newElements.push(`${newName} as ${element.name.text}`);
459
+ }
460
+ else {
461
+ newElements.push(newName);
462
+ }
463
+ }
464
+ else {
465
+ newElements.push(element.getText(sourceFile));
466
+ }
467
+ });
468
+ if (hasChanges) {
469
+ const moduleSpecifier = node.moduleSpecifier.text;
470
+ const newImportStatement = `import { ${newElements.join(', ')} } from '${moduleSpecifier}';`;
471
+ replacements.push({
472
+ start: node.getStart(sourceFile),
473
+ end: node.getEnd(),
474
+ replacement: newImportStatement,
475
+ });
476
+ }
477
+ }
478
+ ts.forEachChild(node, visit);
479
+ }
480
+ visit(sourceFile);
481
+ // Apply replacements in reverse order
482
+ let result = content;
483
+ replacements.sort((a, b) => b.start - a.start);
484
+ for (const { start, end, replacement } of replacements) {
485
+ result = result.slice(0, start) + replacement + result.slice(end);
486
+ }
487
+ return result;
488
+ }
489
+ //#endregion
490
+ //#region Generate query creator templates for the new query client configs
491
+ function generateQueryCreators(tree, queryClientFiles) {
492
+ const generatedFiles = [];
493
+ for (const [filePath, configNames] of queryClientFiles.entries()) {
494
+ const content = tree.read(filePath, 'utf-8');
495
+ if (!content)
496
+ continue;
497
+ // Generate creators for each config in this file
498
+ const newContent = addQueryCreatorsToFile(content, configNames);
499
+ if (newContent !== content) {
500
+ tree.write(filePath, newContent);
501
+ generatedFiles.push(filePath);
502
+ }
503
+ }
504
+ if (generatedFiles.length > 0) {
505
+ console.log('\n✅ Generated query creators in:');
506
+ generatedFiles.forEach((file) => console.log(` - ${file}`));
507
+ }
508
+ }
509
+ function addQueryCreatorsToFile(content, configNames) {
510
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
511
+ // Find where each config is declared
512
+ const configPositions = new Map();
513
+ function visit(node) {
514
+ if (ts.isVariableDeclaration(node) &&
515
+ ts.isIdentifier(node.name) &&
516
+ node.initializer &&
517
+ ts.isCallExpression(node.initializer)) {
518
+ const callExpr = node.initializer;
519
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
520
+ ts.isIdentifier(callExpr.expression.expression) &&
521
+ callExpr.expression.expression.text === 'E' &&
522
+ ts.isIdentifier(callExpr.expression.name) &&
523
+ callExpr.expression.name.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 = E.createGetQuery(${configName}Config);`,
559
+ `export const ${configNameWithoutClientSuffix}Post = E.createPostQuery(${configName}Config);`,
560
+ `export const ${configNameWithoutClientSuffix}Put = E.createPutQuery(${configName}Config);`,
561
+ `export const ${configNameWithoutClientSuffix}Patch = E.createPatchQuery(${configName}Config);`,
562
+ `export const ${configNameWithoutClientSuffix}Delete = E.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
+ (0, devkit_1.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
+ // Update imports to include necessary creator functions
748
+ result = addCreatorImports(result, creatorsByClient);
749
+ return result;
750
+ }
751
+ function addCreatorImports(content, creatorsByClient) {
752
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
753
+ const replacements = [];
754
+ // Build a map of import source -> set of all imports needed from that source
755
+ const importsBySource = new Map();
756
+ creatorsByClient.forEach((creators, clientName) => {
757
+ const clientNameWithoutClient = clientName.replace(/Client$/, '');
758
+ const configName = `${clientName}Config`;
759
+ // Collect unique methods
760
+ const methodsUsed = new Set();
761
+ let hasSecure = false;
762
+ creators.forEach((creator) => {
763
+ methodsUsed.add(creator.method);
764
+ if (creator.secure) {
765
+ hasSecure = true;
766
+ }
767
+ });
768
+ // Build the list of imports needed for this client
769
+ const importsNeeded = new Set();
770
+ importsNeeded.add(configName);
771
+ methodsUsed.forEach((method) => {
772
+ const methodCapitalized = method.charAt(0).toUpperCase() + method.slice(1);
773
+ // Add non-secure creator
774
+ importsNeeded.add(`${clientNameWithoutClient}${methodCapitalized}`);
775
+ // Add secure creator if needed
776
+ if (hasSecure) {
777
+ importsNeeded.add(`${clientNameWithoutClient}${methodCapitalized}Secure`);
778
+ }
779
+ });
780
+ // Store these imports for later (we'll determine the source file when we find the import)
781
+ // For now, associate with the client name
782
+ importsBySource.set(clientName, importsNeeded);
783
+ });
784
+ // Update import statements
785
+ function visit(node) {
786
+ if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
787
+ const moduleSpecifier = node.moduleSpecifier.text;
788
+ // Get existing imports from this statement
789
+ const existingImports = new Set();
790
+ const clientsInThisImport = [];
791
+ if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
792
+ node.importClause.namedBindings.elements.forEach((el) => {
793
+ const importName = el.name.text;
794
+ existingImports.add(importName);
795
+ // Check if this is one of our client names
796
+ if (importsBySource.has(importName)) {
797
+ clientsInThisImport.push(importName);
798
+ }
799
+ });
800
+ }
801
+ if (clientsInThisImport.length === 0) {
802
+ // This import doesn't contain any of our clients
803
+ ts.forEachChild(node, visit);
804
+ return;
805
+ }
806
+ // Accumulate all imports needed from all clients in this import statement
807
+ const allImportsNeeded = new Set();
808
+ clientsInThisImport.forEach((clientName) => {
809
+ const importsForClient = importsBySource.get(clientName);
810
+ if (importsForClient) {
811
+ importsForClient.forEach((imp) => allImportsNeeded.add(imp));
812
+ }
813
+ });
814
+ // Remove the old client names from the final import list
815
+ clientsInThisImport.forEach((clientName) => {
816
+ allImportsNeeded.delete(clientName);
817
+ });
818
+ // Keep other existing imports that aren't client names
819
+ existingImports.forEach((imp) => {
820
+ if (!clientsInThisImport.includes(imp)) {
821
+ allImportsNeeded.add(imp);
822
+ }
823
+ });
824
+ // Create new import statement with all accumulated imports
825
+ const importsList = Array.from(allImportsNeeded).sort().join(', ');
826
+ const newImportStatement = `import { ${importsList} } from '${moduleSpecifier}';`;
827
+ replacements.push({
828
+ start: node.getStart(sourceFile),
829
+ end: node.getEnd(),
830
+ replacement: newImportStatement,
831
+ });
832
+ }
833
+ ts.forEachChild(node, visit);
834
+ }
835
+ visit(sourceFile);
836
+ // Apply replacements in reverse order
837
+ let result = content;
838
+ replacements.sort((a, b) => b.start - a.start);
839
+ for (const { start, end, replacement } of replacements) {
840
+ result = result.slice(0, start) + replacement + result.slice(end);
841
+ }
842
+ return result;
843
+ }
844
+ function generateNewQueryCreator(creator, sourceFile) {
845
+ const clientNameWithoutClient = creator.clientName.replace(/Client$/, '');
846
+ const methodCapitalized = creator.method.charAt(0).toUpperCase() + creator.method.slice(1);
847
+ // Build type parameter
848
+ let typeParam = '';
849
+ if (creator.typeArgs && (creator.typeBody || creator.typeResponse)) {
850
+ // Case 1: Has args + other types -> use intersection
851
+ // Example: GetCollectionsArgs & { response: Paginated<BaseCollectionView> }
852
+ const additionalTypes = [];
853
+ if (creator.typeBody) {
854
+ additionalTypes.push(`body: ${creator.typeBody}`);
855
+ }
856
+ if (creator.typeResponse) {
857
+ additionalTypes.push(`response: ${creator.typeResponse}`);
858
+ }
859
+ typeParam = `<${creator.typeArgs} & { ${additionalTypes.join('; ')} }>`;
860
+ }
861
+ else if (creator.typeArgs) {
862
+ // Case 2: Only args -> use directly
863
+ // Example: PostCollectionAcceptAllWithoutStatusArgs
864
+ typeParam = `<${creator.typeArgs}>`;
865
+ }
866
+ else if (creator.typeBody || creator.typeResponse) {
867
+ // Case 3: No args, but has body/response -> wrap in object
868
+ // Example: { body: CreateUserDto; response: User }
869
+ const typeProps = [];
870
+ if (creator.typeBody) {
871
+ typeProps.push(`body: ${creator.typeBody}`);
872
+ }
873
+ if (creator.typeResponse) {
874
+ typeProps.push(`response: ${creator.typeResponse}`);
875
+ }
876
+ typeParam = `<{ ${typeProps.join('; ')} }>`;
877
+ }
878
+ // Case 4: No types at all -> no type parameter
879
+ // Build options object
880
+ const options = [];
881
+ creator.httpOptions.forEach((value, key) => {
882
+ options.push(`${key}: ${value}`);
883
+ });
884
+ const optionsStr = options.length > 0 ? `, {\n ${options.join(',\n ')}\n}` : '';
885
+ // Choose the right creator function
886
+ const creatorFn = creator.secure
887
+ ? `${clientNameWithoutClient}${methodCapitalized}Secure`
888
+ : `${clientNameWithoutClient}${methodCapitalized}`;
889
+ // Now we include 'export const' since we're replacing the entire statement
890
+ return `export const ${creator.name} = ${creatorFn}${typeParam}(${creator.route}${optionsStr});`;
891
+ }
892
+ function generateLegacyWrapper(creator) {
893
+ const legacyName = toLegacyName(creator.name);
894
+ return `export const ${legacyName} = E.createLegacyQueryCreator({ creator: ${creator.name} });`;
895
+ }
896
+ function createAuthProviders(tree, authProvidersNeeded, queryClientFiles) {
897
+ const createdProviders = [];
898
+ authProvidersNeeded.forEach((filePath, clientName) => {
899
+ const content = tree.read(filePath, 'utf-8');
900
+ if (!content)
901
+ return;
902
+ // Check if auth provider already exists
903
+ const authProviderName = `${clientName}AuthProviderConfig`;
904
+ if (content.includes(authProviderName)) {
905
+ return; // Already exists
906
+ }
907
+ const configName = `${clientName}Config`;
908
+ let newContent = addAuthProviderToFile(content, clientName, configName);
909
+ if (newContent !== content) {
910
+ // Generate secure query creators
911
+ newContent = generateSecureQueryCreators(newContent, clientName);
912
+ // Generate inject function for auth provider
913
+ newContent = addAuthProviderInjectFunction(newContent, clientName);
914
+ tree.write(filePath, newContent);
915
+ createdProviders.push(filePath);
916
+ }
917
+ });
918
+ if (createdProviders.length > 0) {
919
+ console.log('\n✅ Created auth providers in:');
920
+ createdProviders.forEach((file) => console.log(` - ${file}`));
921
+ console.warn('\n⚠️ Make sure to provide them in the respective app.config/main.ts providers array using E.provideBearerAuthProvider(yourConfigName)');
922
+ }
923
+ }
924
+ function addAuthProviderInjectFunction(content, clientName) {
925
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
926
+ const authProviderName = `${clientName}AuthProviderConfig`;
927
+ // Find where the auth provider is declared
928
+ let authProviderPosition;
929
+ function visit(node) {
930
+ if (ts.isVariableDeclaration(node) &&
931
+ ts.isIdentifier(node.name) &&
932
+ node.name.text === authProviderName &&
933
+ node.initializer &&
934
+ ts.isCallExpression(node.initializer)) {
935
+ let parent = node.parent;
936
+ while (parent && !ts.isVariableStatement(parent)) {
937
+ parent = parent.parent;
938
+ }
939
+ if (parent) {
940
+ authProviderPosition = parent.getEnd();
941
+ }
942
+ }
943
+ ts.forEachChild(node, visit);
944
+ }
945
+ visit(sourceFile);
946
+ if (!authProviderPosition)
947
+ return content;
948
+ // Check if inject function already exists
949
+ const injectFunctionName = `inject${clientName.charAt(0).toUpperCase() + clientName.slice(1)}AuthProvider`;
950
+ if (content.includes(injectFunctionName)) {
951
+ return content;
952
+ }
953
+ // Ensure inject is imported
954
+ let result = content;
955
+ const hasInjectImport = result.includes('import { inject }') || (result.includes("from '@angular/core'") && result.includes('inject'));
956
+ if (!hasInjectImport) {
957
+ result = addInjectImport(result);
958
+ }
959
+ // Generate inject function for auth provider
960
+ const injectFunction = `\n\nexport const ${injectFunctionName} = () => inject(${authProviderName}.token);`;
961
+ return result.slice(0, authProviderPosition) + injectFunction + result.slice(authProviderPosition);
962
+ }
963
+ function addAuthProviderToFile(content, clientName, configName) {
964
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
965
+ // Find where the config is declared
966
+ let configPosition;
967
+ function visit(node) {
968
+ if (ts.isVariableDeclaration(node) &&
969
+ ts.isIdentifier(node.name) &&
970
+ node.name.text === configName &&
971
+ node.initializer &&
972
+ ts.isCallExpression(node.initializer)) {
973
+ // Find the variable statement
974
+ let parent = node.parent;
975
+ while (parent && !ts.isVariableStatement(parent)) {
976
+ parent = parent.parent;
977
+ }
978
+ if (parent) {
979
+ configPosition = parent.getEnd();
980
+ }
981
+ }
982
+ ts.forEachChild(node, visit);
983
+ }
984
+ visit(sourceFile);
985
+ if (!configPosition)
986
+ return content;
987
+ const authProviderCode = `
988
+
989
+ export const ${clientName}AuthProviderConfig = E.createBearerAuthProviderConfig({
990
+ name: '${clientName}',
991
+ queryClientRef: ${configName}.token,
992
+ });`;
993
+ return content.slice(0, configPosition) + authProviderCode + content.slice(configPosition);
994
+ }
995
+ function generateSecureQueryCreators(content, clientName) {
996
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
997
+ // Find where the auth provider is declared
998
+ let authProviderPosition;
999
+ const authProviderName = `${clientName}AuthProviderConfig`;
1000
+ function visit(node) {
1001
+ if (ts.isVariableDeclaration(node) &&
1002
+ ts.isIdentifier(node.name) &&
1003
+ node.name.text === authProviderName &&
1004
+ node.initializer &&
1005
+ ts.isCallExpression(node.initializer)) {
1006
+ let parent = node.parent;
1007
+ while (parent && !ts.isVariableStatement(parent)) {
1008
+ parent = parent.parent;
1009
+ }
1010
+ if (parent) {
1011
+ authProviderPosition = parent.getEnd();
1012
+ }
1013
+ }
1014
+ ts.forEachChild(node, visit);
1015
+ }
1016
+ visit(sourceFile);
1017
+ if (!authProviderPosition)
1018
+ return content;
1019
+ const clientNameWithoutClient = clientName.replace('Client', '');
1020
+ const configName = `${clientName}Config`;
1021
+ const secureCreators = [
1022
+ `export const ${clientNameWithoutClient}GetSecure = E.createSecureGetQuery(${configName}, ${authProviderName});`,
1023
+ `export const ${clientNameWithoutClient}PostSecure = E.createSecurePostQuery(${configName}, ${authProviderName});`,
1024
+ `export const ${clientNameWithoutClient}PutSecure = E.createSecurePutQuery(${configName}, ${authProviderName});`,
1025
+ `export const ${clientNameWithoutClient}PatchSecure = E.createSecurePatchQuery(${configName}, ${authProviderName});`,
1026
+ `export const ${clientNameWithoutClient}DeleteSecure = E.createSecureDeleteQuery(${configName}, ${authProviderName});`,
1027
+ ].join('\n');
1028
+ return content.slice(0, authProviderPosition) + '\n\n' + secureCreators + content.slice(authProviderPosition);
1029
+ }
1030
+ //#endregion
1031
+ //#region Update legacy query creator imports and usages
1032
+ function updateLegacyCreatorImportsAndUsages(tree, queryClientFiles) {
1033
+ const legacyCreators = new Map(); // original name -> legacy name
1034
+ const updatedFiles = [];
1035
+ // Step 1: Find all legacy wrappers that were created
1036
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1037
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
1038
+ return;
1039
+ const content = tree.read(filePath, 'utf-8');
1040
+ if (!content)
1041
+ return;
1042
+ const wrappers = findLegacyWrappers(content);
1043
+ wrappers.forEach((legacyName, originalName) => {
1044
+ legacyCreators.set(originalName, legacyName);
1045
+ });
1046
+ });
1047
+ if (legacyCreators.size === 0)
1048
+ return;
1049
+ console.log(`\n✅ Found ${legacyCreators.size} legacy query creator wrappers`);
1050
+ // Step 2: Update imports and usages across the workspace
1051
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1052
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
1053
+ return;
1054
+ const content = tree.read(filePath, 'utf-8');
1055
+ if (!content)
1056
+ return;
1057
+ // Skip files that define the legacy wrappers themselves
1058
+ if (containsLegacyWrapperDefinitions(content)) {
1059
+ return;
1060
+ }
1061
+ let newContent = content;
1062
+ // Update imports to add legacy names
1063
+ newContent = updateLegacyCreatorImports(newContent, legacyCreators);
1064
+ // Update usages to use legacy names
1065
+ newContent = updateLegacyCreatorUsages(newContent, legacyCreators);
1066
+ if (newContent !== content) {
1067
+ tree.write(filePath, newContent);
1068
+ updatedFiles.push(filePath);
1069
+ }
1070
+ });
1071
+ if (updatedFiles.length > 0) {
1072
+ console.log('\n✅ Updated legacy creator imports and usages in:');
1073
+ updatedFiles.forEach((file) => console.log(` - ${file}`));
1074
+ }
1075
+ }
1076
+ function containsLegacyWrapperDefinitions(content) {
1077
+ return content.includes('E.createLegacyQueryCreator');
1078
+ }
1079
+ function findLegacyWrappers(content) {
1080
+ const wrappers = new Map(); // original name -> legacy name
1081
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1082
+ function visit(node) {
1083
+ // Find: export const legacyGetUsers = E.createLegacyQueryCreator({ creator: getUsers });
1084
+ if (ts.isVariableDeclaration(node) &&
1085
+ ts.isIdentifier(node.name) &&
1086
+ node.name.text.startsWith('legacy') &&
1087
+ node.initializer &&
1088
+ ts.isCallExpression(node.initializer)) {
1089
+ const callExpr = node.initializer;
1090
+ // Check if it's E.createLegacyQueryCreator
1091
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
1092
+ ts.isIdentifier(callExpr.expression.expression) &&
1093
+ callExpr.expression.expression.text === 'E' &&
1094
+ ts.isIdentifier(callExpr.expression.name) &&
1095
+ callExpr.expression.name.text === 'createLegacyQueryCreator') {
1096
+ // Extract the original creator name from the config object
1097
+ if (callExpr.arguments.length > 0 && ts.isObjectLiteralExpression(callExpr.arguments[0])) {
1098
+ const configObj = callExpr.arguments[0];
1099
+ configObj.properties.forEach((prop) => {
1100
+ if (ts.isPropertyAssignment(prop) &&
1101
+ ts.isIdentifier(prop.name) &&
1102
+ prop.name.text === 'creator' &&
1103
+ ts.isIdentifier(prop.initializer)) {
1104
+ const originalName = prop.initializer.text;
1105
+ const legacyName = node.name.text;
1106
+ wrappers.set(originalName, legacyName);
1107
+ }
1108
+ });
1109
+ }
1110
+ }
1111
+ }
1112
+ ts.forEachChild(node, visit);
1113
+ }
1114
+ visit(sourceFile);
1115
+ return wrappers;
1116
+ }
1117
+ function updateLegacyCreatorImports(content, legacyCreators) {
1118
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1119
+ const replacements = [];
1120
+ function visit(node) {
1121
+ // Update named imports to include legacy wrappers
1122
+ if (ts.isImportDeclaration(node) &&
1123
+ node.importClause?.namedBindings &&
1124
+ ts.isNamedImports(node.importClause.namedBindings)) {
1125
+ const namedBindings = node.importClause.namedBindings;
1126
+ const importedNames = new Set();
1127
+ const newElements = [];
1128
+ let hasChanges = false;
1129
+ namedBindings.elements.forEach((element) => {
1130
+ const importedName = element.propertyName ? element.propertyName.text : element.name.text;
1131
+ const legacyName = legacyCreators.get(importedName);
1132
+ if (legacyName) {
1133
+ // Replace the original import with the legacy one
1134
+ hasChanges = true;
1135
+ newElements.push(legacyName);
1136
+ importedNames.add(legacyName);
1137
+ }
1138
+ else {
1139
+ // Keep imports that don't have legacy versions
1140
+ newElements.push(element.getText(sourceFile));
1141
+ importedNames.add(importedName);
1142
+ }
1143
+ });
1144
+ if (hasChanges) {
1145
+ const moduleSpecifier = node.moduleSpecifier.text;
1146
+ const newImportStatement = `import { ${newElements.join(', ')} } from '${moduleSpecifier}';`;
1147
+ replacements.push({
1148
+ start: node.getStart(sourceFile),
1149
+ end: node.getEnd(),
1150
+ replacement: newImportStatement,
1151
+ });
1152
+ }
1153
+ }
1154
+ ts.forEachChild(node, visit);
1155
+ }
1156
+ visit(sourceFile);
1157
+ // Apply replacements in reverse order
1158
+ let result = content;
1159
+ replacements.sort((a, b) => b.start - a.start);
1160
+ for (const { start, end, replacement } of replacements) {
1161
+ result = result.slice(0, start) + replacement + result.slice(end);
1162
+ }
1163
+ return result;
1164
+ }
1165
+ function updateLegacyCreatorUsages(content, legacyCreators) {
1166
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1167
+ const replacements = [];
1168
+ function visit(node) {
1169
+ // Handle object literal properties
1170
+ if (ts.isObjectLiteralExpression(node)) {
1171
+ node.properties.forEach((prop) => {
1172
+ if (ts.isPropertyAssignment(prop)) {
1173
+ // Case: { get: getUsers } -> { get: legacyGetUsers }
1174
+ if (ts.isIdentifier(prop.initializer) && legacyCreators.has(prop.initializer.text)) {
1175
+ const oldName = prop.initializer.text;
1176
+ const newName = legacyCreators.get(oldName);
1177
+ replacements.push({
1178
+ start: prop.initializer.getStart(sourceFile),
1179
+ end: prop.initializer.getEnd(),
1180
+ replacement: newName,
1181
+ });
1182
+ }
1183
+ }
1184
+ else if (ts.isShorthandPropertyAssignment(prop)) {
1185
+ // Case: { getUsers } -> { getUsers: legacyGetUsers }
1186
+ if (ts.isIdentifier(prop.name) && legacyCreators.has(prop.name.text)) {
1187
+ const oldName = prop.name.text;
1188
+ const newName = legacyCreators.get(oldName);
1189
+ const replacement = `${oldName}: ${newName}`;
1190
+ replacements.push({
1191
+ start: prop.getStart(sourceFile),
1192
+ end: prop.getEnd(),
1193
+ replacement,
1194
+ });
1195
+ }
1196
+ }
1197
+ });
1198
+ }
1199
+ // Handle regular identifier references
1200
+ if (ts.isIdentifier(node) && legacyCreators.has(node.text)) {
1201
+ const parent = node.parent;
1202
+ // Skip if it's a property name in an object literal (already handled above)
1203
+ if (ts.isPropertyAssignment(parent) && parent.name === node) {
1204
+ ts.forEachChild(node, visit);
1205
+ return;
1206
+ }
1207
+ // Skip if it's the initializer in a property assignment (already handled above)
1208
+ if (ts.isPropertyAssignment(parent) && parent.initializer === node) {
1209
+ ts.forEachChild(node, visit);
1210
+ return;
1211
+ }
1212
+ // Skip if it's a shorthand property (already handled above)
1213
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
1214
+ ts.forEachChild(node, visit);
1215
+ return;
1216
+ }
1217
+ // Skip if it's a variable declaration name
1218
+ if (ts.isVariableDeclaration(parent) && parent.name === node) {
1219
+ ts.forEachChild(node, visit);
1220
+ return;
1221
+ }
1222
+ // Skip if it's an import specifier
1223
+ if (ts.isImportSpecifier(parent)) {
1224
+ ts.forEachChild(node, visit);
1225
+ return;
1226
+ }
1227
+ // Replace with legacy name
1228
+ const newName = legacyCreators.get(node.text);
1229
+ replacements.push({
1230
+ start: node.getStart(sourceFile),
1231
+ end: node.getEnd(),
1232
+ replacement: newName,
1233
+ });
1234
+ }
1235
+ ts.forEachChild(node, visit);
1236
+ }
1237
+ visit(sourceFile);
1238
+ // Apply replacements in reverse order
1239
+ let result = content;
1240
+ replacements.sort((a, b) => b.start - a.start);
1241
+ for (const { start, end, replacement } of replacements) {
1242
+ result = result.slice(0, start) + replacement + result.slice(end);
1243
+ }
1244
+ return result;
1245
+ }
1246
+ //#endregion
1247
+ //#region Check all files for usage of legacy query creators and add injector if needed
1248
+ // TODO
1249
+ //#endregion
1250
+ //#region Remove provideQueryClientForDevtools and QueryDevtoolsComponent in ts / et-query-devtools in html usage
1251
+ function removeDevtoolsUsage(tree) {
1252
+ const projects = (0, devkit_1.getProjects)(tree);
1253
+ const removedFromFiles = [];
1254
+ // Only check app config files for provideQueryClientForDevtools
1255
+ for (const [, projectConfig] of projects.entries()) {
1256
+ if (projectConfig.projectType !== 'application')
1257
+ continue;
1258
+ const configPaths = [`${projectConfig.root}/src/app/app.config.ts`, `${projectConfig.root}/src/main.ts`];
1259
+ for (const configPath of configPaths) {
1260
+ if (!tree.exists(configPath))
1261
+ continue;
1262
+ const content = tree.read(configPath, 'utf-8');
1263
+ if (!content)
1264
+ continue;
1265
+ const newContent = removeProvideQueryClientForDevtools(content);
1266
+ if (newContent !== content) {
1267
+ tree.write(configPath, newContent);
1268
+ removedFromFiles.push(configPath);
1269
+ }
1270
+ }
1271
+ }
1272
+ // Check all TypeScript files for QueryDevtoolsComponent imports
1273
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1274
+ if (!filePath.endsWith('.ts'))
1275
+ return;
1276
+ const content = tree.read(filePath, 'utf-8');
1277
+ if (!content)
1278
+ return;
1279
+ // Only process if it mentions QueryDevtoolsComponent
1280
+ if (!content.includes('QueryDevtoolsComponent'))
1281
+ return;
1282
+ const newContent = removeQueryDevtoolsComponent(content);
1283
+ if (newContent !== content) {
1284
+ tree.write(filePath, newContent);
1285
+ if (!removedFromFiles.includes(filePath)) {
1286
+ removedFromFiles.push(filePath);
1287
+ }
1288
+ }
1289
+ });
1290
+ // Check all HTML files for et-query-devtools
1291
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1292
+ if (!filePath.endsWith('.html'))
1293
+ return;
1294
+ const content = tree.read(filePath, 'utf-8');
1295
+ if (!content)
1296
+ return;
1297
+ // Only process if it mentions et-query-devtools
1298
+ if (!content.includes('et-query-devtools'))
1299
+ return;
1300
+ const newContent = removeDevtoolsFromHtml(content);
1301
+ if (newContent !== content) {
1302
+ tree.write(filePath, newContent);
1303
+ removedFromFiles.push(filePath);
1304
+ }
1305
+ });
1306
+ if (removedFromFiles.length > 0) {
1307
+ console.log('\n✅ Removed devtools usage from:');
1308
+ removedFromFiles.forEach((file) => console.log(` - ${file}`));
1309
+ console.warn('\n⚠️ Make sure to check the app.config.ts/main.ts files for now unneeded logic inside the providers array (like ...(isDevMode() ? [] : []))');
1310
+ 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)');
1311
+ }
1312
+ }
1313
+ function removeProvideQueryClientForDevtools(content) {
1314
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1315
+ const replacements = [];
1316
+ function visit(node) {
1317
+ // Remove provideQueryClientForDevtools from provider arrays
1318
+ if (ts.isCallExpression(node)) {
1319
+ if (ts.isIdentifier(node.expression) && node.expression.text === 'provideQueryClientForDevtools') {
1320
+ // Find the array this call is in
1321
+ let parent = node.parent;
1322
+ // Walk up until we find an array literal expression
1323
+ while (parent) {
1324
+ if (ts.isArrayLiteralExpression(parent)) {
1325
+ // Find which element contains our call expression
1326
+ const elementIndex = parent.elements.findIndex((el) => {
1327
+ // Check if this element or any of its descendants is our node
1328
+ let found = false;
1329
+ function search(n) {
1330
+ if (n === node) {
1331
+ found = true;
1332
+ return;
1333
+ }
1334
+ ts.forEachChild(n, search);
1335
+ }
1336
+ search(el);
1337
+ return found;
1338
+ });
1339
+ if (elementIndex !== -1) {
1340
+ const element = parent.elements[elementIndex];
1341
+ let start = element.getStart(sourceFile);
1342
+ let end = element.getEnd();
1343
+ // Handle comma and whitespace removal
1344
+ if (elementIndex < parent.elements.length - 1) {
1345
+ // Not the last element - remove trailing comma and whitespace
1346
+ const nextElement = parent.elements[elementIndex + 1];
1347
+ const textBetween = content.slice(end, nextElement.getStart(sourceFile));
1348
+ const commaIndex = textBetween.indexOf(',');
1349
+ if (commaIndex !== -1) {
1350
+ end += commaIndex + 1;
1351
+ // Also remove whitespace after comma
1352
+ while (end < content.length && /\s/.test(content[end])) {
1353
+ end++;
1354
+ }
1355
+ }
1356
+ }
1357
+ else if (elementIndex > 0) {
1358
+ // Last element - remove leading comma and whitespace
1359
+ const prevElement = parent.elements[elementIndex - 1];
1360
+ const textBetween = content.slice(prevElement.getEnd(), start);
1361
+ const commaIndex = textBetween.lastIndexOf(',');
1362
+ if (commaIndex !== -1) {
1363
+ start = prevElement.getEnd() + commaIndex;
1364
+ }
1365
+ }
1366
+ // Remove leading whitespace/newlines
1367
+ while (start > 0 && /\s/.test(content[start - 1])) {
1368
+ start--;
1369
+ }
1370
+ replacements.push({ start, end, replacement: '' });
1371
+ }
1372
+ break;
1373
+ }
1374
+ parent = parent.parent;
1375
+ }
1376
+ }
1377
+ }
1378
+ ts.forEachChild(node, visit);
1379
+ }
1380
+ visit(sourceFile);
1381
+ // Apply replacements in reverse order
1382
+ let result = content;
1383
+ replacements.sort((a, b) => b.start - a.start);
1384
+ for (const { start, end, replacement } of replacements) {
1385
+ result = result.slice(0, start) + replacement + result.slice(end);
1386
+ }
1387
+ // Remove the import if it exists
1388
+ return removeDevtoolsImports(result);
1389
+ }
1390
+ function removeQueryDevtoolsComponent(content) {
1391
+ let result = content;
1392
+ // Remove from imports array in components
1393
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1394
+ const replacements = [];
1395
+ function visit(node) {
1396
+ // Remove QueryDevtoolsComponent from imports arrays in components
1397
+ if (ts.isPropertyAssignment(node) &&
1398
+ ts.isIdentifier(node.name) &&
1399
+ node.name.text === 'imports' &&
1400
+ ts.isArrayLiteralExpression(node.initializer)) {
1401
+ const arrayExpr = node.initializer;
1402
+ arrayExpr.elements.forEach((element, index) => {
1403
+ if (ts.isIdentifier(element) && element.text === 'QueryDevtoolsComponent') {
1404
+ let start = element.getStart(sourceFile);
1405
+ let end = element.getEnd();
1406
+ // Handle comma removal
1407
+ if (index < arrayExpr.elements.length - 1) {
1408
+ const nextElement = arrayExpr.elements[index + 1];
1409
+ const textBetween = content.slice(end, nextElement.getStart(sourceFile));
1410
+ const commaIndex = textBetween.indexOf(',');
1411
+ if (commaIndex !== -1) {
1412
+ end += commaIndex + 1;
1413
+ // Also remove whitespace after comma
1414
+ while (end < content.length && /\s/.test(content[end])) {
1415
+ end++;
1416
+ }
1417
+ }
1418
+ }
1419
+ else if (index > 0) {
1420
+ const prevElement = arrayExpr.elements[index - 1];
1421
+ const textBetween = content.slice(prevElement.getEnd(), start);
1422
+ const commaIndex = textBetween.lastIndexOf(',');
1423
+ if (commaIndex !== -1) {
1424
+ start = prevElement.getEnd() + commaIndex;
1425
+ }
1426
+ }
1427
+ // Remove surrounding whitespace
1428
+ while (start > 0 && /\s/.test(content[start - 1])) {
1429
+ start--;
1430
+ }
1431
+ replacements.push({ start, end, replacement: '' });
1432
+ }
1433
+ });
1434
+ }
1435
+ ts.forEachChild(node, visit);
1436
+ }
1437
+ visit(sourceFile);
1438
+ // Apply replacements in reverse order
1439
+ replacements.sort((a, b) => b.start - a.start);
1440
+ for (const { start, end, replacement } of replacements) {
1441
+ result = result.slice(0, start) + replacement + result.slice(end);
1442
+ }
1443
+ // Remove the import if it exists
1444
+ return removeDevtoolsImports(result);
1445
+ }
1446
+ function removeDevtoolsImports(content) {
1447
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1448
+ let queryImportNode;
1449
+ ts.forEachChild(sourceFile, (node) => {
1450
+ if (ts.isImportDeclaration(node) &&
1451
+ ts.isStringLiteral(node.moduleSpecifier) &&
1452
+ node.moduleSpecifier.text === '@ethlete/query' &&
1453
+ node.importClause?.namedBindings &&
1454
+ ts.isNamedImports(node.importClause.namedBindings)) {
1455
+ queryImportNode = node;
1456
+ }
1457
+ });
1458
+ if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
1459
+ return content;
1460
+ }
1461
+ const namedBindings = queryImportNode.importClause.namedBindings;
1462
+ const elementsToKeep = namedBindings.elements.filter((el) => el.name.text !== 'QueryDevtoolsComponent' && el.name.text !== 'provideQueryClientForDevtools');
1463
+ if (elementsToKeep.length === namedBindings.elements.length) {
1464
+ // Nothing to remove
1465
+ return content;
1466
+ }
1467
+ const importStart = queryImportNode.getStart(sourceFile);
1468
+ const importEnd = queryImportNode.getEnd();
1469
+ if (elementsToKeep.length === 0) {
1470
+ // Remove entire import statement
1471
+ const start = importStart;
1472
+ let end = importEnd;
1473
+ // Also remove the newline after the import
1474
+ if (content[end] === '\n')
1475
+ end++;
1476
+ return content.slice(0, start) + content.slice(end);
1477
+ }
1478
+ // Reconstruct import without devtools
1479
+ const newImports = elementsToKeep.map((el) => el.getText(sourceFile));
1480
+ const moduleSpecifier = queryImportNode.moduleSpecifier.text;
1481
+ const newImportStatement = `import { ${newImports.join(', ')} } from '${moduleSpecifier}';`;
1482
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
1483
+ }
1484
+ function removeDevtoolsFromHtml(content) {
1485
+ let result = content;
1486
+ // Remove self-closing tags
1487
+ result = result.replace(/<et-query-devtools\s*\/>/g, '');
1488
+ // Remove opening and closing tags with content
1489
+ result = result.replace(/<et-query-devtools[^>]*>[\s\S]*?<\/et-query-devtools>/g, '');
1490
+ return result;
1491
+ }
1492
+ //#endregion
1493
+ //#region Turn AnyQuery into E.AnyLegacyQuery everywhere and do the same with AnyQueryCreator
1494
+ function replaceAnyQueryWithLegacy(tree) {
1495
+ const updatedFiles = [];
1496
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '', (filePath) => {
1497
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
1498
+ return;
1499
+ const content = tree.read(filePath, 'utf-8');
1500
+ if (!content)
1501
+ return;
1502
+ // Only process if it contains AnyQuery or AnyQueryCreator
1503
+ if (!content.includes('AnyQuery') && !content.includes('AnyQueryCreator'))
1504
+ return;
1505
+ const newContent = replaceAnyQueryInFile(content);
1506
+ if (newContent !== content) {
1507
+ tree.write(filePath, newContent);
1508
+ updatedFiles.push(filePath);
1509
+ }
1510
+ });
1511
+ if (updatedFiles.length > 0) {
1512
+ console.log('\n✅ Replaced AnyQuery and AnyQueryCreator with E.AnyLegacyQuery and E.AnyLegacyQueryCreator in:');
1513
+ updatedFiles.forEach((file) => console.log(` - ${file}`));
1514
+ }
1515
+ }
1516
+ function replaceAnyQueryInFile(content) {
1517
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1518
+ const replacements = [];
1519
+ function visit(node) {
1520
+ // Find type references to AnyQuery and AnyQueryCreator
1521
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
1522
+ if (node.typeName.text === 'AnyQuery') {
1523
+ replacements.push({
1524
+ start: node.getStart(sourceFile),
1525
+ end: node.getEnd(),
1526
+ replacement: 'E.AnyLegacyQuery',
1527
+ });
1528
+ }
1529
+ else if (node.typeName.text === 'AnyQueryCreator') {
1530
+ replacements.push({
1531
+ start: node.getStart(sourceFile),
1532
+ end: node.getEnd(),
1533
+ replacement: 'E.AnyLegacyQueryCreator',
1534
+ });
1535
+ }
1536
+ }
1537
+ // Find identifier references (for runtime usage)
1538
+ if (ts.isIdentifier(node) && (node.text === 'AnyQuery' || node.text === 'AnyQueryCreator')) {
1539
+ // Check if this is part of a type reference (already handled above)
1540
+ const parent = node.parent;
1541
+ if (ts.isTypeReferenceNode(parent) && parent.typeName === node) {
1542
+ ts.forEachChild(node, visit);
1543
+ return;
1544
+ }
1545
+ // Check if it's in an import statement
1546
+ if (ts.isImportSpecifier(parent)) {
1547
+ ts.forEachChild(node, visit);
1548
+ return;
1549
+ }
1550
+ // Replace standalone references
1551
+ const replacement = node.text === 'AnyQuery' ? 'E.AnyLegacyQuery' : 'E.AnyLegacyQueryCreator';
1552
+ replacements.push({
1553
+ start: node.getStart(sourceFile),
1554
+ end: node.getEnd(),
1555
+ replacement,
1556
+ });
1557
+ }
1558
+ ts.forEachChild(node, visit);
1559
+ }
1560
+ visit(sourceFile);
1561
+ // Apply replacements in reverse order
1562
+ let result = content;
1563
+ replacements.sort((a, b) => b.start - a.start);
1564
+ for (const { start, end, replacement } of replacements) {
1565
+ result = result.slice(0, start) + replacement + result.slice(end);
1566
+ }
1567
+ // Remove AnyQuery and AnyQueryCreator from imports and ensure ExperimentalQuery as E exists
1568
+ result = removeAnyQueryFromImports(result);
1569
+ // Make sure we have ExperimentalQuery as E import
1570
+ if (!result.includes('ExperimentalQuery as E')) {
1571
+ result = addExperimentalQueryImport(result);
1572
+ }
1573
+ return result;
1574
+ }
1575
+ function removeAnyQueryFromImports(content) {
1576
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1577
+ let queryImportNode;
1578
+ ts.forEachChild(sourceFile, (node) => {
1579
+ if (ts.isImportDeclaration(node) &&
1580
+ ts.isStringLiteral(node.moduleSpecifier) &&
1581
+ node.moduleSpecifier.text === '@ethlete/query' &&
1582
+ node.importClause?.namedBindings &&
1583
+ ts.isNamedImports(node.importClause.namedBindings)) {
1584
+ queryImportNode = node;
1585
+ }
1586
+ });
1587
+ if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
1588
+ return content;
1589
+ }
1590
+ const namedBindings = queryImportNode.importClause.namedBindings;
1591
+ const elementsToKeep = namedBindings.elements.filter((el) => el.name.text !== 'AnyQuery' && el.name.text !== 'AnyQueryCreator');
1592
+ if (elementsToKeep.length === namedBindings.elements.length) {
1593
+ // Nothing to remove
1594
+ return content;
1595
+ }
1596
+ const importStart = queryImportNode.getStart(sourceFile);
1597
+ const importEnd = queryImportNode.getEnd();
1598
+ if (elementsToKeep.length === 0) {
1599
+ // Remove entire import statement
1600
+ const start = importStart;
1601
+ let end = importEnd;
1602
+ // Also remove the newline after the import
1603
+ if (content[end] === '\n')
1604
+ end++;
1605
+ return content.slice(0, start) + content.slice(end);
1606
+ }
1607
+ // Reconstruct import without AnyQuery and AnyQueryCreator
1608
+ const newImports = elementsToKeep.map((el) => el.getText(sourceFile));
1609
+ const moduleSpecifier = queryImportNode.moduleSpecifier.text;
1610
+ const newImportStatement = `import { ${newImports.join(', ')} } from '${moduleSpecifier}';`;
1611
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
1612
+ }
1613
+ //#endregion
1614
+ //#region Generate inject functions for query client configs
1615
+ function generateInjectFunctions(tree, queryClientFiles) {
1616
+ const generatedFiles = [];
1617
+ for (const [filePath, configNames] of queryClientFiles.entries()) {
1618
+ const content = tree.read(filePath, 'utf-8');
1619
+ if (!content)
1620
+ continue;
1621
+ const newContent = addInjectFunctionsToFile(content, configNames);
1622
+ if (newContent !== content) {
1623
+ tree.write(filePath, newContent);
1624
+ generatedFiles.push(filePath);
1625
+ }
1626
+ }
1627
+ if (generatedFiles.length > 0) {
1628
+ console.log('\n✅ Generated inject functions in:');
1629
+ generatedFiles.forEach((file) => console.log(` - ${file}`));
1630
+ }
1631
+ }
1632
+ function addInjectFunctionsToFile(content, configNames) {
1633
+ // Add inject import if needed FIRST, before doing anything else
1634
+ let result = content;
1635
+ const hasInjectImport = result.includes('import { inject }') || (result.includes("from '@angular/core'") && result.includes('inject'));
1636
+ if (!hasInjectImport && configNames.length > 0) {
1637
+ result = addInjectImport(result);
1638
+ }
1639
+ // NOW parse the updated content to find config positions
1640
+ const sourceFile = ts.createSourceFile('temp.ts', result, ts.ScriptTarget.Latest, true);
1641
+ // Find where each config is declared
1642
+ const configPositions = new Map();
1643
+ function visit(node) {
1644
+ if (ts.isVariableDeclaration(node) &&
1645
+ ts.isIdentifier(node.name) &&
1646
+ node.initializer &&
1647
+ ts.isCallExpression(node.initializer)) {
1648
+ const callExpr = node.initializer;
1649
+ if (ts.isPropertyAccessExpression(callExpr.expression) &&
1650
+ ts.isIdentifier(callExpr.expression.expression) &&
1651
+ callExpr.expression.expression.text === 'E' &&
1652
+ ts.isIdentifier(callExpr.expression.name) &&
1653
+ callExpr.expression.name.text === 'createQueryClientConfig') {
1654
+ const configName = node.name.text;
1655
+ // Find the variable statement
1656
+ let parent = node.parent;
1657
+ while (parent && !ts.isVariableStatement(parent)) {
1658
+ parent = parent.parent;
1659
+ }
1660
+ if (parent) {
1661
+ configPositions.set(configName, parent.getEnd());
1662
+ }
1663
+ }
1664
+ }
1665
+ ts.forEachChild(node, visit);
1666
+ }
1667
+ visit(sourceFile);
1668
+ // Generate inject functions for each config
1669
+ const insertions = [];
1670
+ for (const configName of configNames) {
1671
+ const position = configPositions.get(`${configName}Config`);
1672
+ if (!position)
1673
+ continue;
1674
+ // Check if inject function already exists
1675
+ const nameWithoutConfig = configName.replace(/Config$/, '');
1676
+ const capitalizedName = nameWithoutConfig.charAt(0).toUpperCase() + nameWithoutConfig.slice(1);
1677
+ const injectFunctionName = `inject${capitalizedName}`;
1678
+ if (result.includes(injectFunctionName))
1679
+ continue;
1680
+ const injectFunction = generateInjectFunction(configName);
1681
+ insertions.push({ position, text: `\n\n${injectFunction}` });
1682
+ }
1683
+ // Apply insertions in reverse order to maintain positions
1684
+ insertions.sort((a, b) => b.position - a.position);
1685
+ for (const { position, text } of insertions) {
1686
+ result = result.slice(0, position) + text + result.slice(position);
1687
+ }
1688
+ return result;
1689
+ }
1690
+ function addInjectImport(content) {
1691
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
1692
+ // Find if there's already an import from @angular/core
1693
+ let angularCoreImport;
1694
+ ts.forEachChild(sourceFile, (node) => {
1695
+ if (ts.isImportDeclaration(node) &&
1696
+ ts.isStringLiteral(node.moduleSpecifier) &&
1697
+ node.moduleSpecifier.text === '@angular/core') {
1698
+ angularCoreImport = node;
1699
+ }
1700
+ });
1701
+ if (angularCoreImport) {
1702
+ // Add inject to existing @angular/core import
1703
+ if (angularCoreImport.importClause?.namedBindings &&
1704
+ ts.isNamedImports(angularCoreImport.importClause.namedBindings)) {
1705
+ const namedBindings = angularCoreImport.importClause.namedBindings;
1706
+ const existingImports = namedBindings.elements.map((el) => el.name.text);
1707
+ // Check if inject is already imported
1708
+ if (existingImports.includes('inject')) {
1709
+ return content;
1710
+ }
1711
+ const newImports = [...existingImports, 'inject'].sort();
1712
+ const newImportStatement = `import { ${newImports.join(', ')} } from '@angular/core';`;
1713
+ const importStart = angularCoreImport.getStart(sourceFile);
1714
+ const importEnd = angularCoreImport.getEnd();
1715
+ return content.slice(0, importStart) + newImportStatement + content.slice(importEnd);
1716
+ }
1717
+ }
1718
+ // Find the last import statement to insert after it
1719
+ let lastImportEnd = 0;
1720
+ ts.forEachChild(sourceFile, (node) => {
1721
+ if (ts.isImportDeclaration(node)) {
1722
+ const end = node.getEnd();
1723
+ if (end > lastImportEnd) {
1724
+ lastImportEnd = end;
1725
+ }
1726
+ }
1727
+ });
1728
+ if (lastImportEnd > 0) {
1729
+ // Add after the last import
1730
+ const injectImport = `\nimport { inject } from '@angular/core';`;
1731
+ return content.slice(0, lastImportEnd) + injectImport + content.slice(lastImportEnd);
1732
+ }
1733
+ // No imports found, add at the beginning
1734
+ return `import { inject } from '@angular/core';\n\n${content}`;
1735
+ }
1736
+ function generateInjectFunction(configName) {
1737
+ // Convert apiClientConfig -> injectApiClient
1738
+ const nameWithoutConfig = configName.replace(/Config$/, '');
1739
+ const capitalizedName = nameWithoutConfig.charAt(0).toUpperCase() + nameWithoutConfig.slice(1);
1740
+ const functionName = `inject${capitalizedName}`;
1741
+ return `export const ${functionName} = () => inject(${configName}Config.token);`;
1742
+ }
1743
+ //#endregion
1744
+ //# sourceMappingURL=migration.js.map