@ethlete/query 5.41.5 → 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.
- package/CHANGELOG.md +6 -0
- package/fesm2022/ethlete-query.mjs +30 -27
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/generators.json +9 -0
- package/generators/migrate-to-query-v3/migration.js +1744 -0
- package/generators/migrate-to-query-v3/schema.json +14 -0
- package/index.d.ts +1 -0
- package/package.json +9 -6
|
@@ -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
|