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