@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
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
|
|
4
|
+
export const migrateQueryClients = (tree, report) => {
|
|
5
|
+
const queryClientFiles = new Map();
|
|
6
|
+
const variableRenames = new Map();
|
|
7
|
+
visitNotIgnoredFiles(tree, '', (filePath) => {
|
|
8
|
+
if (!filePath.endsWith('.ts')) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const content = tree.read(filePath, 'utf-8');
|
|
12
|
+
if (!content || !hasQueryClientInstantiation(content)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const renames = new Map();
|
|
16
|
+
const nextContent = migrateQueryClientToConfig({ content, filePath, renamesOut: renames, report });
|
|
17
|
+
renames.forEach((newName, oldName) => {
|
|
18
|
+
variableRenames.set(oldName, newName);
|
|
19
|
+
});
|
|
20
|
+
const configNames = extractClientConfigNames(nextContent);
|
|
21
|
+
if (configNames.length > 0) {
|
|
22
|
+
queryClientFiles.set(filePath, configNames);
|
|
23
|
+
}
|
|
24
|
+
if (nextContent !== content) {
|
|
25
|
+
tree.write(filePath, nextContent);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
if (queryClientFiles.size > 0) {
|
|
29
|
+
console.log('\n✅ Migrated QueryClient instantiations in:');
|
|
30
|
+
queryClientFiles.forEach((configNames, filePath) => {
|
|
31
|
+
console.log(` - ${filePath} (${configNames.join(', ')})`);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return { queryClientFiles, variableRenames };
|
|
35
|
+
};
|
|
36
|
+
export const generateProviderAliases = (tree, queryClientFiles) => {
|
|
37
|
+
const updatedFiles = [];
|
|
38
|
+
for (const [filePath, configNames] of queryClientFiles.entries()) {
|
|
39
|
+
const content = tree.read(filePath, 'utf-8');
|
|
40
|
+
if (!content) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const nextContent = addProviderAliasesToFile(content, configNames);
|
|
44
|
+
if (nextContent !== content) {
|
|
45
|
+
tree.write(filePath, nextContent);
|
|
46
|
+
updatedFiles.push(filePath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (updatedFiles.length > 0) {
|
|
50
|
+
console.log('\n✅ Generated query client provider aliases in:');
|
|
51
|
+
updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
export const generateQueryCreators = (tree, queryClientFiles) => {
|
|
55
|
+
const generatedFiles = [];
|
|
56
|
+
for (const [filePath, configNames] of queryClientFiles.entries()) {
|
|
57
|
+
const content = tree.read(filePath, 'utf-8');
|
|
58
|
+
if (!content) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const nextContent = addQueryCreatorsToFile(content, configNames);
|
|
62
|
+
if (nextContent !== content) {
|
|
63
|
+
tree.write(filePath, nextContent);
|
|
64
|
+
generatedFiles.push(filePath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (generatedFiles.length > 0) {
|
|
68
|
+
console.log('\n✅ Generated query creators in:');
|
|
69
|
+
generatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
export const updateImportsAcrossWorkspace = (tree, renames) => {
|
|
73
|
+
if (renames.size === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
visitNotIgnoredFiles(tree, '', (filePath) => {
|
|
77
|
+
if (!filePath.endsWith('.ts')) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const content = tree.read(filePath, 'utf-8');
|
|
81
|
+
if (!content) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const nextContent = updateImportsInFile(content, renames);
|
|
85
|
+
if (nextContent !== content) {
|
|
86
|
+
tree.write(filePath, nextContent);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
const hasQueryClientInstantiation = (content) => {
|
|
91
|
+
const sourceFile = createSourceFile(content);
|
|
92
|
+
let hasQueryClientImport = false;
|
|
93
|
+
let hasNewQueryClient = false;
|
|
94
|
+
const visit = (node) => {
|
|
95
|
+
if (ts.isImportDeclaration(node) &&
|
|
96
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
97
|
+
node.moduleSpecifier.text === '@ethlete/query' &&
|
|
98
|
+
node.importClause?.namedBindings &&
|
|
99
|
+
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
100
|
+
const hasLegacyClientImport = node.importClause.namedBindings.elements.some((element) => {
|
|
101
|
+
return element.name.text === 'V2QueryClient';
|
|
102
|
+
});
|
|
103
|
+
if (hasLegacyClientImport) {
|
|
104
|
+
hasQueryClientImport = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'V2QueryClient') {
|
|
108
|
+
hasNewQueryClient = true;
|
|
109
|
+
}
|
|
110
|
+
ts.forEachChild(node, visit);
|
|
111
|
+
};
|
|
112
|
+
visit(sourceFile);
|
|
113
|
+
return hasQueryClientImport && hasNewQueryClient;
|
|
114
|
+
};
|
|
115
|
+
const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) => {
|
|
116
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
117
|
+
const replacements = [];
|
|
118
|
+
const variableRenames = new Map();
|
|
119
|
+
const visit = (node) => {
|
|
120
|
+
if (ts.isNewExpression(node) &&
|
|
121
|
+
ts.isIdentifier(node.expression) &&
|
|
122
|
+
node.expression.text === 'V2QueryClient' &&
|
|
123
|
+
node.arguments &&
|
|
124
|
+
node.arguments.length > 0) {
|
|
125
|
+
const configArgument = node.arguments[0];
|
|
126
|
+
if (ts.isObjectLiteralExpression(configArgument)) {
|
|
127
|
+
const oldVariableName = getVariableNameForQueryClient(node);
|
|
128
|
+
const newVariableName = ensureConfigSuffix(oldVariableName);
|
|
129
|
+
if (oldVariableName !== newVariableName) {
|
|
130
|
+
const collisionRegex = new RegExp(`\\b(?:const|let|var)\\s+${newVariableName}\\b`);
|
|
131
|
+
if (collisionRegex.test(content)) {
|
|
132
|
+
report.addWarning({
|
|
133
|
+
title: `Resolve query client rename collision for ${oldVariableName}`,
|
|
134
|
+
summary: `The migration renamed ${oldVariableName} to ${newVariableName}, but a declaration with the target name already exists.`,
|
|
135
|
+
action: `Verify the declarations in ${filePath} and rename one side manually before shipping the migrated client.`,
|
|
136
|
+
locations: [{ filePath }],
|
|
137
|
+
source: 'query-client-migration',
|
|
138
|
+
dedupeKey: `rename-collision:${filePath}:${oldVariableName}:${newVariableName}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
variableRenames.set(oldVariableName, newVariableName);
|
|
142
|
+
if (renamesOut) {
|
|
143
|
+
renamesOut.set(oldVariableName, newVariableName);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
replacements.push({
|
|
147
|
+
start: node.getStart(sourceFile),
|
|
148
|
+
end: node.getEnd(),
|
|
149
|
+
replacement: migrateConfigObject(configArgument, node, sourceFile),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
ts.forEachChild(node, visit);
|
|
154
|
+
};
|
|
155
|
+
visit(sourceFile);
|
|
156
|
+
let result = content;
|
|
157
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
158
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
159
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
160
|
+
});
|
|
161
|
+
result = removeQueryClientImport(result);
|
|
162
|
+
return renameVariables(result, variableRenames);
|
|
163
|
+
};
|
|
164
|
+
const removeQueryClientImport = (content) => {
|
|
165
|
+
const sourceFile = createSourceFile(content);
|
|
166
|
+
let queryImportNode;
|
|
167
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
168
|
+
if (ts.isImportDeclaration(node) &&
|
|
169
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
170
|
+
node.moduleSpecifier.text === '@ethlete/query') {
|
|
171
|
+
queryImportNode = node;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const requiredImports = [
|
|
175
|
+
'createDeleteQuery',
|
|
176
|
+
'createGetQuery',
|
|
177
|
+
'createPatchQuery',
|
|
178
|
+
'createPostQuery',
|
|
179
|
+
'createPutQuery',
|
|
180
|
+
'createQueryClient',
|
|
181
|
+
];
|
|
182
|
+
if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
|
|
183
|
+
return ensureImportFromQuery(content, requiredImports);
|
|
184
|
+
}
|
|
185
|
+
const existingElements = queryImportNode.importClause.namedBindings.elements.filter((element) => {
|
|
186
|
+
return element.name.text !== 'V2QueryClient';
|
|
187
|
+
});
|
|
188
|
+
const nextImports = new Set(requiredImports);
|
|
189
|
+
existingElements.forEach((element) => nextImports.add(element.name.text));
|
|
190
|
+
const nextImportStatement = `import { ${Array.from(nextImports).sort().join(', ')} } from '@ethlete/query';`;
|
|
191
|
+
return (content.slice(0, queryImportNode.getStart(sourceFile)) +
|
|
192
|
+
nextImportStatement +
|
|
193
|
+
content.slice(queryImportNode.getEnd()));
|
|
194
|
+
};
|
|
195
|
+
const renameVariables = (content, renames) => {
|
|
196
|
+
if (renames.size === 0) {
|
|
197
|
+
return content;
|
|
198
|
+
}
|
|
199
|
+
const sourceFile = createSourceFile(content);
|
|
200
|
+
const replacements = [];
|
|
201
|
+
const visit = (node) => {
|
|
202
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
203
|
+
const nextName = renames.get(node.name.text);
|
|
204
|
+
if (nextName) {
|
|
205
|
+
replacements.push({
|
|
206
|
+
start: node.name.getStart(sourceFile),
|
|
207
|
+
end: node.name.getEnd(),
|
|
208
|
+
replacement: nextName,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (ts.isIdentifier(node)) {
|
|
213
|
+
const nextName = renames.get(node.text);
|
|
214
|
+
if (nextName) {
|
|
215
|
+
const parent = node.parent;
|
|
216
|
+
if (!ts.isPropertyAssignment(parent) || parent.name !== node) {
|
|
217
|
+
replacements.push({
|
|
218
|
+
start: node.getStart(sourceFile),
|
|
219
|
+
end: node.getEnd(),
|
|
220
|
+
replacement: nextName,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
ts.forEachChild(node, visit);
|
|
226
|
+
};
|
|
227
|
+
visit(sourceFile);
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
const uniqueReplacements = replacements.filter((replacement) => {
|
|
230
|
+
const key = `${replacement.start}:${replacement.end}`;
|
|
231
|
+
if (seen.has(key)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
seen.add(key);
|
|
235
|
+
return true;
|
|
236
|
+
});
|
|
237
|
+
let result = content;
|
|
238
|
+
uniqueReplacements.sort((left, right) => right.start - left.start);
|
|
239
|
+
uniqueReplacements.forEach(({ start, end, replacement }) => {
|
|
240
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
241
|
+
});
|
|
242
|
+
return result;
|
|
243
|
+
};
|
|
244
|
+
const migrateConfigObject = (configArgument, node, sourceFile) => {
|
|
245
|
+
const variableName = getVariableNameForQueryClient(node);
|
|
246
|
+
const nextConfig = [];
|
|
247
|
+
const baseRouteProperty = configArgument.properties.find((property) => {
|
|
248
|
+
return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'baseRoute';
|
|
249
|
+
});
|
|
250
|
+
if (baseRouteProperty && ts.isPropertyAssignment(baseRouteProperty)) {
|
|
251
|
+
const baseRouteValue = baseRouteProperty.initializer
|
|
252
|
+
.getText(sourceFile)
|
|
253
|
+
.replace('as `https://${string}`', '')
|
|
254
|
+
.trim();
|
|
255
|
+
nextConfig.push(`baseUrl: ${baseRouteValue}`);
|
|
256
|
+
}
|
|
257
|
+
nextConfig.push(`name: '${variableName}'`);
|
|
258
|
+
const requestProperty = configArgument.properties.find((property) => {
|
|
259
|
+
return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'request';
|
|
260
|
+
});
|
|
261
|
+
if (requestProperty &&
|
|
262
|
+
ts.isPropertyAssignment(requestProperty) &&
|
|
263
|
+
ts.isObjectLiteralExpression(requestProperty.initializer)) {
|
|
264
|
+
requestProperty.initializer.properties.forEach((property) => {
|
|
265
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (property.name.text === 'queryParams') {
|
|
269
|
+
nextConfig.push(`queryString: ${property.initializer.getText(sourceFile)}`);
|
|
270
|
+
}
|
|
271
|
+
if (property.name.text === 'cacheAdapter') {
|
|
272
|
+
nextConfig.push(`cacheAdapter: ${property.initializer.getText(sourceFile)}`);
|
|
273
|
+
}
|
|
274
|
+
if (property.name.text === 'retryFn') {
|
|
275
|
+
nextConfig.push(`retryFn: ${property.initializer.getText(sourceFile)}`);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
|
|
280
|
+
};
|
|
281
|
+
const getVariableNameForQueryClient = (node) => {
|
|
282
|
+
let parent = node.parent;
|
|
283
|
+
while (parent) {
|
|
284
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
285
|
+
return parent.name.text;
|
|
286
|
+
}
|
|
287
|
+
parent = parent.parent;
|
|
288
|
+
}
|
|
289
|
+
return 'client';
|
|
290
|
+
};
|
|
291
|
+
const extractClientConfigNames = (content) => {
|
|
292
|
+
const sourceFile = createSourceFile(content);
|
|
293
|
+
const configNames = [];
|
|
294
|
+
const visit = (node) => {
|
|
295
|
+
if (ts.isVariableDeclaration(node) &&
|
|
296
|
+
ts.isIdentifier(node.name) &&
|
|
297
|
+
node.initializer &&
|
|
298
|
+
ts.isCallExpression(node.initializer) &&
|
|
299
|
+
ts.isIdentifier(node.initializer.expression) &&
|
|
300
|
+
node.initializer.expression.text === 'createQueryClient') {
|
|
301
|
+
const configArgument = node.initializer.arguments[0];
|
|
302
|
+
if (!configArgument || !ts.isObjectLiteralExpression(configArgument)) {
|
|
303
|
+
ts.forEachChild(node, visit);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const nameProperty = configArgument.properties.find((property) => {
|
|
307
|
+
return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'name';
|
|
308
|
+
});
|
|
309
|
+
if (nameProperty && ts.isPropertyAssignment(nameProperty) && ts.isStringLiteral(nameProperty.initializer)) {
|
|
310
|
+
configNames.push(nameProperty.initializer.text);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
ts.forEachChild(node, visit);
|
|
314
|
+
};
|
|
315
|
+
visit(sourceFile);
|
|
316
|
+
return configNames;
|
|
317
|
+
};
|
|
318
|
+
const addProviderAliasesToFile = (content, configNames) => {
|
|
319
|
+
const sourceFile = createSourceFile(content);
|
|
320
|
+
const insertions = [];
|
|
321
|
+
let result = content;
|
|
322
|
+
configNames.forEach((configName) => {
|
|
323
|
+
const variableName = ensureConfigSuffix(configName);
|
|
324
|
+
const position = getVariableStatementEnd(sourceFile, variableName, 'createQueryClient');
|
|
325
|
+
if (!position) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const { injectName, provideName } = getProviderAliasNames(variableName);
|
|
329
|
+
if (result.includes(`export const [${provideName}, ${injectName}] = ${variableName};`)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
insertions.push({
|
|
333
|
+
position,
|
|
334
|
+
text: `\n\nexport const [${provideName}, ${injectName}] = ${variableName};`,
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
insertions.sort((left, right) => right.position - left.position);
|
|
338
|
+
insertions.forEach(({ position, text }) => {
|
|
339
|
+
result = result.slice(0, position) + text + result.slice(position);
|
|
340
|
+
});
|
|
341
|
+
return result;
|
|
342
|
+
};
|
|
343
|
+
const getProviderAliasNames = (configVariableName) => {
|
|
344
|
+
const baseName = configVariableName.replace(/Config$/, '');
|
|
345
|
+
const capitalizedBaseName = capitalizeFirstLetter(baseName);
|
|
346
|
+
return {
|
|
347
|
+
provideName: `provide${capitalizedBaseName}`,
|
|
348
|
+
injectName: `inject${capitalizedBaseName}`,
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
const addQueryCreatorsToFile = (content, configNames) => {
|
|
352
|
+
const sourceFile = createSourceFile(content);
|
|
353
|
+
const insertions = [];
|
|
354
|
+
let result = content;
|
|
355
|
+
configNames.forEach((configName) => {
|
|
356
|
+
const variableName = ensureConfigSuffix(configName);
|
|
357
|
+
const position = getVariableStatementEnd(sourceFile, variableName, 'createQueryClient');
|
|
358
|
+
if (!position) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const creatorBlock = generateCreatorsForConfig(configName);
|
|
362
|
+
if (result.includes(creatorBlock.split('\n')[0])) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
insertions.push({
|
|
366
|
+
position,
|
|
367
|
+
text: `\n\n${creatorBlock}`,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
insertions.sort((left, right) => right.position - left.position);
|
|
371
|
+
insertions.forEach(({ position, text }) => {
|
|
372
|
+
result = result.slice(0, position) + text + result.slice(position);
|
|
373
|
+
});
|
|
374
|
+
return result;
|
|
375
|
+
};
|
|
376
|
+
const generateCreatorsForConfig = (configName) => {
|
|
377
|
+
const creatorBaseName = configName.replace('Client', '');
|
|
378
|
+
const configVariableName = ensureConfigSuffix(configName);
|
|
379
|
+
return [
|
|
380
|
+
`export const ${creatorBaseName}Get = createGetQuery(${configVariableName});`,
|
|
381
|
+
`export const ${creatorBaseName}Post = createPostQuery(${configVariableName});`,
|
|
382
|
+
`export const ${creatorBaseName}Put = createPutQuery(${configVariableName});`,
|
|
383
|
+
`export const ${creatorBaseName}Patch = createPatchQuery(${configVariableName});`,
|
|
384
|
+
`export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
|
|
385
|
+
].join('\n');
|
|
386
|
+
};
|
|
387
|
+
const updateImportsInFile = (content, renames) => {
|
|
388
|
+
const sourceFile = createSourceFile(content);
|
|
389
|
+
const replacements = [];
|
|
390
|
+
const visit = (node) => {
|
|
391
|
+
if (ts.isImportDeclaration(node) &&
|
|
392
|
+
node.importClause?.namedBindings &&
|
|
393
|
+
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
394
|
+
const nextElements = [];
|
|
395
|
+
let hasChanges = false;
|
|
396
|
+
node.importClause.namedBindings.elements.forEach((element) => {
|
|
397
|
+
const nextName = renames.get(element.name.text);
|
|
398
|
+
if (!nextName) {
|
|
399
|
+
nextElements.push(element.getText(sourceFile));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
hasChanges = true;
|
|
403
|
+
nextElements.push(element.propertyName ? `${nextName} as ${element.name.text}` : nextName);
|
|
404
|
+
});
|
|
405
|
+
if (hasChanges) {
|
|
406
|
+
replacements.push({
|
|
407
|
+
start: node.getStart(sourceFile),
|
|
408
|
+
end: node.getEnd(),
|
|
409
|
+
replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (ts.isIdentifier(node)) {
|
|
414
|
+
const nextName = renames.get(node.text);
|
|
415
|
+
if (!nextName) {
|
|
416
|
+
ts.forEachChild(node, visit);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const parent = node.parent;
|
|
420
|
+
if (ts.isImportSpecifier(parent)) {
|
|
421
|
+
ts.forEachChild(node, visit);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node) {
|
|
425
|
+
ts.forEachChild(node, visit);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (ts.isFunctionDeclaration(parent) && parent.name === node) {
|
|
429
|
+
ts.forEachChild(node, visit);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (ts.isMethodDeclaration(parent) && parent.name === node) {
|
|
433
|
+
ts.forEachChild(node, visit);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (ts.isParameter(parent) && parent.name === node) {
|
|
437
|
+
ts.forEachChild(node, visit);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (ts.isPropertyDeclaration(parent) && parent.name === node) {
|
|
441
|
+
ts.forEachChild(node, visit);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
445
|
+
ts.forEachChild(node, visit);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node) {
|
|
449
|
+
ts.forEachChild(node, visit);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
replacements.push({
|
|
453
|
+
start: node.getStart(sourceFile),
|
|
454
|
+
end: node.getEnd(),
|
|
455
|
+
replacement: nextName,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
ts.forEachChild(node, visit);
|
|
459
|
+
};
|
|
460
|
+
visit(sourceFile);
|
|
461
|
+
let result = content;
|
|
462
|
+
const seen = new Set();
|
|
463
|
+
const uniqueReplacements = replacements.filter((replacement) => {
|
|
464
|
+
const key = `${replacement.start}:${replacement.end}`;
|
|
465
|
+
if (seen.has(key)) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
seen.add(key);
|
|
469
|
+
return true;
|
|
470
|
+
});
|
|
471
|
+
uniqueReplacements.sort((left, right) => right.start - left.start);
|
|
472
|
+
uniqueReplacements.forEach(({ start, end, replacement }) => {
|
|
473
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
474
|
+
});
|
|
475
|
+
return result;
|
|
476
|
+
};
|
|
477
|
+
//# sourceMappingURL=query-client-migration.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const QUERY_V3_MIGRATION_REPORT_PATH = 'query-v3-migration-tasks.md';
|
|
2
|
+
export const MIGRATION_TASK_KIND = {
|
|
3
|
+
WARNING: 'warning',
|
|
4
|
+
MANUAL_REVIEW: 'manual-review',
|
|
5
|
+
FOLLOW_UP: 'follow-up',
|
|
6
|
+
};
|
|
7
|
+
const renderTaskLocations = (locations) => {
|
|
8
|
+
if (locations.length === 0) {
|
|
9
|
+
return '- None recorded';
|
|
10
|
+
}
|
|
11
|
+
return locations
|
|
12
|
+
.map((location) => {
|
|
13
|
+
const lineSuffix = location.line ? `:${location.line}` : '';
|
|
14
|
+
return `- ${location.filePath}${lineSuffix}`;
|
|
15
|
+
})
|
|
16
|
+
.join('\n');
|
|
17
|
+
};
|
|
18
|
+
export class QueryV3MigrationReport {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.tasks = [];
|
|
21
|
+
}
|
|
22
|
+
addTask(input) {
|
|
23
|
+
const dedupeKey = input.dedupeKey ?? `${input.kind}:${input.title}:${input.summary}:${input.action}`;
|
|
24
|
+
if (this.tasks.some((task) => task.dedupeKey === dedupeKey)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const nextTask = {
|
|
28
|
+
id: `QV3-${String(this.tasks.length + 1).padStart(3, '0')}`,
|
|
29
|
+
kind: input.kind,
|
|
30
|
+
title: input.title,
|
|
31
|
+
summary: input.summary,
|
|
32
|
+
action: input.action,
|
|
33
|
+
locations: input.locations ?? [],
|
|
34
|
+
source: input.source,
|
|
35
|
+
dedupeKey,
|
|
36
|
+
};
|
|
37
|
+
this.tasks.push(nextTask);
|
|
38
|
+
}
|
|
39
|
+
addWarning(input) {
|
|
40
|
+
this.addTask({ ...input, kind: MIGRATION_TASK_KIND.WARNING });
|
|
41
|
+
}
|
|
42
|
+
addManualReview(input) {
|
|
43
|
+
this.addTask({ ...input, kind: MIGRATION_TASK_KIND.MANUAL_REVIEW });
|
|
44
|
+
}
|
|
45
|
+
addFollowUp(input) {
|
|
46
|
+
this.addTask({ ...input, kind: MIGRATION_TASK_KIND.FOLLOW_UP });
|
|
47
|
+
}
|
|
48
|
+
printSummary() {
|
|
49
|
+
if (this.tasks.length === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.warn(`\n⚠️ Generated ${this.tasks.length} follow-up task${this.tasks.length === 1 ? '' : 's'}:`);
|
|
53
|
+
this.tasks.forEach((task) => {
|
|
54
|
+
const locations = task.locations
|
|
55
|
+
.map((location) => `${location.filePath}${location.line ? `:${location.line}` : ''}`)
|
|
56
|
+
.join(', ');
|
|
57
|
+
console.warn(` - ${task.id} [${task.kind}] ${task.title}${locations ? ` (${locations})` : ''}`);
|
|
58
|
+
});
|
|
59
|
+
console.warn(`\n📄 See ${QUERY_V3_MIGRATION_REPORT_PATH} for the AI-ready follow-up list.`);
|
|
60
|
+
}
|
|
61
|
+
writeToTree(tree) {
|
|
62
|
+
tree.write(QUERY_V3_MIGRATION_REPORT_PATH, renderMigrationReportMarkdown(this.tasks));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export const renderMigrationReportMarkdown = (tasks) => {
|
|
66
|
+
const header = [
|
|
67
|
+
'# Query V3 Migration Follow-Up',
|
|
68
|
+
'',
|
|
69
|
+
'This file is generated by the `migrate-to-query-v3` generator.',
|
|
70
|
+
'',
|
|
71
|
+
'## AI Instructions',
|
|
72
|
+
'',
|
|
73
|
+
'1. Re-read the referenced files before changing anything. The migration output is only a starting point.',
|
|
74
|
+
'2. Prefer minimal fixes that align with the current runtime APIs in `@ethlete/query`.',
|
|
75
|
+
'3. Update or remove a task only after the code and tests confirm that it is resolved.',
|
|
76
|
+
'4. If a task is still ambiguous after inspection, keep it open and append a short note instead of guessing.',
|
|
77
|
+
'5. Preserve unrelated user changes. Do not rewrite generated files wholesale unless required.',
|
|
78
|
+
'',
|
|
79
|
+
'## Task Format',
|
|
80
|
+
'',
|
|
81
|
+
'- `Status`: always treat generated tasks as `open` until verified manually or by tests.',
|
|
82
|
+
'- `Kind`: `warning`, `manual-review`, or `follow-up`.',
|
|
83
|
+
'- `Source`: which migration phase raised the task.',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n');
|
|
86
|
+
if (tasks.length === 0) {
|
|
87
|
+
return `${header}## Tasks\n\nNo open follow-up tasks were recorded during this migration run.\n`;
|
|
88
|
+
}
|
|
89
|
+
const tasksSection = tasks
|
|
90
|
+
.map((task) => {
|
|
91
|
+
return [
|
|
92
|
+
`### ${task.id} - ${task.title}`,
|
|
93
|
+
'',
|
|
94
|
+
'- Status: open',
|
|
95
|
+
`- Kind: ${task.kind}`,
|
|
96
|
+
`- Source: ${task.source}`,
|
|
97
|
+
`- Summary: ${task.summary}`,
|
|
98
|
+
`- Suggested action: ${task.action}`,
|
|
99
|
+
'- Locations:',
|
|
100
|
+
renderTaskLocations(task.locations),
|
|
101
|
+
'',
|
|
102
|
+
].join('\n');
|
|
103
|
+
})
|
|
104
|
+
.join('');
|
|
105
|
+
const machineSection = [
|
|
106
|
+
'## Machine Summary',
|
|
107
|
+
'',
|
|
108
|
+
'```yaml',
|
|
109
|
+
'tasks:',
|
|
110
|
+
...tasks.flatMap((task) => {
|
|
111
|
+
const locationLines = task.locations.length === 0 ? [' []'] : [];
|
|
112
|
+
const nestedLocations = task.locations.flatMap((location) => {
|
|
113
|
+
const lines = [` - filePath: ${location.filePath}`];
|
|
114
|
+
if (location.line) {
|
|
115
|
+
lines.push(` line: ${location.line}`);
|
|
116
|
+
}
|
|
117
|
+
return lines;
|
|
118
|
+
});
|
|
119
|
+
return [
|
|
120
|
+
` - id: ${task.id}`,
|
|
121
|
+
` status: open`,
|
|
122
|
+
` kind: ${task.kind}`,
|
|
123
|
+
` source: ${task.source}`,
|
|
124
|
+
` title: ${task.title}`,
|
|
125
|
+
` summary: ${task.summary}`,
|
|
126
|
+
` action: ${task.action}`,
|
|
127
|
+
` locations:`,
|
|
128
|
+
...(nestedLocations.length > 0 ? nestedLocations : locationLines),
|
|
129
|
+
];
|
|
130
|
+
}),
|
|
131
|
+
'```',
|
|
132
|
+
'',
|
|
133
|
+
].join('\n');
|
|
134
|
+
return `${header}## Tasks\n\n${tasksSection}${machineSection}`;
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=report.js.map
|