@ethlete/query 6.0.0-next.52 ā 6.0.0-next.53
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 +43 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +96 -24
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -1
- package/fesm2022/ethlete-query-testing.mjs +67 -25
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +7816 -7351
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/migrate-from-ngrx-toolkit/consumers.js +70 -2
- package/generators/migrate-from-ngrx-toolkit/migration.js +2 -1
- package/generators/migrate-to-query-v3/cleanup-migration.js +142 -53
- package/generators/migrate-to-query-v3/migration-scope.js +2 -0
- package/generators/migrate-to-query-v3/migration.js +3 -3
- package/generators/migrate-to-query-v3/module-graph.js +39 -29
- package/generators/migrate-to-query-v3/query-client-migration.js +138 -124
- package/generators/migrate-to-query-v3/rename-symbols.js +38 -11
- package/generators/migrate-to-query-v3/report.js +40 -7
- package/generators/migrate-to-query-v3/shared.js +19 -1
- package/generators/prep-for-query-v3/migration.js +317 -56
- package/package.json +1 -1
- package/types/ethlete-query-testing.d.ts +28 -11
- package/types/ethlete-query.d.ts +679 -569
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { CLIENT_FEATURE, reportClientErrorPipelineAdded } from '../migrate-query-opt-in-features/migration.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createModuleGraph } from './module-graph.js';
|
|
4
|
+
import { isNamePosition, isShadowedInNestedScope, renamedReference } from './rename-symbols.js';
|
|
5
|
+
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, getVariableStatementEnd, printImportDeclaration, } from './shared.js';
|
|
4
6
|
export const migrateQueryClients = (tree, report, scope) => {
|
|
5
7
|
const queryClientFiles = new Map();
|
|
6
8
|
const variableRenames = new Map();
|
|
@@ -14,9 +16,9 @@ export const migrateQueryClients = (tree, report, scope) => {
|
|
|
14
16
|
}
|
|
15
17
|
const renames = new Map();
|
|
16
18
|
const nextContent = migrateQueryClientToConfig({ content, filePath, renamesOut: renames, report });
|
|
17
|
-
renames.
|
|
18
|
-
variableRenames.set(
|
|
19
|
-
}
|
|
19
|
+
if (renames.size > 0) {
|
|
20
|
+
variableRenames.set(filePath, { originalContent: content, renames });
|
|
21
|
+
}
|
|
20
22
|
const configNames = extractClientConfigNames(nextContent);
|
|
21
23
|
if (configNames.length > 0) {
|
|
22
24
|
queryClientFiles.set(filePath, configNames);
|
|
@@ -69,10 +71,12 @@ export const generateQueryCreators = (tree, queryClientFiles) => {
|
|
|
69
71
|
generatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
70
72
|
}
|
|
71
73
|
};
|
|
72
|
-
export const updateImportsAcrossWorkspace = (tree, renames, scope) => {
|
|
74
|
+
export const updateImportsAcrossWorkspace = (tree, renames, scope, report) => {
|
|
73
75
|
if (renames.size === 0) {
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
78
|
+
const originalContents = new Map(Array.from(renames.entries()).map(([filePath, { originalContent }]) => [filePath, originalContent]));
|
|
79
|
+
const graph = createModuleGraph(withOriginalContents(tree, originalContents));
|
|
76
80
|
scope.visit(tree, (filePath) => {
|
|
77
81
|
if (!filePath.endsWith('.ts')) {
|
|
78
82
|
return;
|
|
@@ -81,12 +85,42 @@ export const updateImportsAcrossWorkspace = (tree, renames, scope) => {
|
|
|
81
85
|
if (!content) {
|
|
82
86
|
return;
|
|
83
87
|
}
|
|
84
|
-
const nextContent = updateImportsInFile(content, renames);
|
|
88
|
+
const { content: nextContent, renamedByName } = updateImportsInFile(content, filePath, renames, graph);
|
|
89
|
+
if (renamedByName.size > 0) {
|
|
90
|
+
report.addManualReview({
|
|
91
|
+
title: `Check the client rename in ${filePath}`,
|
|
92
|
+
summary: `The migration could not resolve ${[...renamedByName].map((specifier) => `\`${specifier}\``).join(', ')} to a file, so it renamed the imported client by its name alone.`,
|
|
93
|
+
action: 'Confirm the renamed import really is the migrated query client, and revert it if it is not.',
|
|
94
|
+
locations: [{ filePath }],
|
|
95
|
+
source: 'query-client-migration',
|
|
96
|
+
dedupeKey: `client-rename-by-name:${filePath}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
85
99
|
if (nextContent !== content) {
|
|
100
|
+
originalContents.set(filePath, content);
|
|
86
101
|
tree.write(filePath, nextContent);
|
|
87
102
|
}
|
|
88
103
|
});
|
|
89
104
|
};
|
|
105
|
+
/**
|
|
106
|
+
* A view of `tree` that still reads the pre-migration content of `originalContents`, so the module
|
|
107
|
+
* graph resolves a client import by the name it had when the import was written.
|
|
108
|
+
*/
|
|
109
|
+
const withOriginalContents = (tree, originalContents) => new Proxy(tree, {
|
|
110
|
+
get: (target, property) => {
|
|
111
|
+
if (property === 'read') {
|
|
112
|
+
return (filePath, encoding) => {
|
|
113
|
+
const original = originalContents.get(filePath);
|
|
114
|
+
if (original === undefined) {
|
|
115
|
+
return encoding ? target.read(filePath, encoding) : target.read(filePath);
|
|
116
|
+
}
|
|
117
|
+
return encoding ? original : Buffer.from(original);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const value = Reflect.get(target, property, target);
|
|
121
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
90
124
|
const hasQueryClientInstantiation = (content) => {
|
|
91
125
|
const sourceFile = createSourceFile(content);
|
|
92
126
|
let hasQueryClientImport = false;
|
|
@@ -202,11 +236,31 @@ const removeQueryClientImport = (content, keepLegacyClient) => {
|
|
|
202
236
|
});
|
|
203
237
|
const nextImports = new Set(requiredImports);
|
|
204
238
|
existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
|
|
205
|
-
const nextImportStatement =
|
|
239
|
+
const nextImportStatement = printImportDeclaration(queryImportNode, sourceFile, Array.from(nextImports).sort());
|
|
206
240
|
return (content.slice(0, queryImportNode.getStart(sourceFile)) +
|
|
207
241
|
nextImportStatement +
|
|
208
242
|
content.slice(queryImportNode.getEnd()));
|
|
209
243
|
};
|
|
244
|
+
const isTopLevelVariableName = (node) => {
|
|
245
|
+
const declaration = node.parent;
|
|
246
|
+
return (ts.isVariableDeclaration(declaration) &&
|
|
247
|
+
declaration.name === node &&
|
|
248
|
+
ts.isVariableStatement(declaration.parent.parent) &&
|
|
249
|
+
ts.isSourceFile(declaration.parent.parent.parent));
|
|
250
|
+
};
|
|
251
|
+
const renameInDeclaringFile = (node, nextName) => {
|
|
252
|
+
const parent = node.parent;
|
|
253
|
+
if (isTopLevelVariableName(node)) {
|
|
254
|
+
return nextName;
|
|
255
|
+
}
|
|
256
|
+
if (ts.isExportSpecifier(parent) && !parent.parent.parent.moduleSpecifier) {
|
|
257
|
+
return (parent.propertyName ?? parent.name) === node ? nextName : undefined;
|
|
258
|
+
}
|
|
259
|
+
if (isNamePosition(node) || isShadowedInNestedScope(node)) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
return renamedReference(node, nextName);
|
|
263
|
+
};
|
|
210
264
|
const renameVariables = (content, renames) => {
|
|
211
265
|
if (renames.size === 0) {
|
|
212
266
|
return content;
|
|
@@ -214,44 +268,21 @@ const renameVariables = (content, renames) => {
|
|
|
214
268
|
const sourceFile = createSourceFile(content);
|
|
215
269
|
const replacements = [];
|
|
216
270
|
const visit = (node) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
start: node.name.getStart(sourceFile),
|
|
222
|
-
end: node.name.getEnd(),
|
|
223
|
-
replacement: nextName,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
else if (ts.isIdentifier(node)) {
|
|
228
|
-
const nextName = renames.get(node.text);
|
|
229
|
-
if (nextName) {
|
|
230
|
-
const parent = node.parent;
|
|
231
|
-
if (!ts.isPropertyAssignment(parent) || parent.name !== node) {
|
|
232
|
-
replacements.push({
|
|
233
|
-
start: node.getStart(sourceFile),
|
|
234
|
-
end: node.getEnd(),
|
|
235
|
-
replacement: nextName,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
271
|
+
const nextName = ts.isIdentifier(node) ? renames.get(node.text) : undefined;
|
|
272
|
+
const replacement = nextName && renameInDeclaringFile(node, nextName);
|
|
273
|
+
if (replacement) {
|
|
274
|
+
replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement });
|
|
239
275
|
}
|
|
240
276
|
ts.forEachChild(node, visit);
|
|
241
277
|
};
|
|
242
278
|
visit(sourceFile);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (seen.has(key)) {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
seen.add(key);
|
|
250
|
-
return true;
|
|
251
|
-
});
|
|
279
|
+
return applyReplacements(content, replacements);
|
|
280
|
+
};
|
|
281
|
+
const applyReplacements = (content, replacements) => {
|
|
252
282
|
let result = content;
|
|
253
|
-
|
|
254
|
-
|
|
283
|
+
[...replacements]
|
|
284
|
+
.sort((left, right) => right.start - left.start)
|
|
285
|
+
.forEach(({ start, end, replacement }) => {
|
|
255
286
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
256
287
|
});
|
|
257
288
|
return result;
|
|
@@ -539,107 +570,90 @@ const generateCreatorsForConfig = (configName) => {
|
|
|
539
570
|
`export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
|
|
540
571
|
].join('\n');
|
|
541
572
|
};
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
return true;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
return false;
|
|
554
|
-
};
|
|
555
|
-
const updateImportsInFile = (content, renames) => {
|
|
556
|
-
const sourceFile = createSourceFile(content);
|
|
573
|
+
/**
|
|
574
|
+
* Points imports and re-exports of a renamed client at its new name, and renames the references to
|
|
575
|
+
* each unaliased import. Only a binding the module graph resolves to the client's declaring file is
|
|
576
|
+
* touched; a same-named symbol from any other module stays as it is. A workspace specifier the graph
|
|
577
|
+
* cannot resolve falls back to the rename by name and is returned in `renamedByName` for the report.
|
|
578
|
+
*/
|
|
579
|
+
const updateImportsInFile = (content, filePath, renames, graph) => {
|
|
580
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
557
581
|
const replacements = [];
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
582
|
+
const localRenames = new Map();
|
|
583
|
+
const renamedByName = new Set();
|
|
584
|
+
const resolveRename = (specifier, importedName) => {
|
|
585
|
+
const declaringFile = graph.findDeclaringFile(filePath, specifier, importedName);
|
|
586
|
+
if (declaringFile) {
|
|
587
|
+
return declaringFile !== filePath ? renames.get(declaringFile)?.renames.get(importedName) : undefined;
|
|
588
|
+
}
|
|
589
|
+
if (!graph.isWorkspaceSpecifier(specifier))
|
|
590
|
+
return undefined;
|
|
591
|
+
const nextName = [...renames.values()].find((entry) => entry.renames.has(importedName))?.renames.get(importedName);
|
|
592
|
+
if (nextName)
|
|
593
|
+
renamedByName.add(specifier);
|
|
594
|
+
return nextName;
|
|
595
|
+
};
|
|
596
|
+
for (const statement of sourceFile.statements) {
|
|
597
|
+
if (ts.isImportDeclaration(statement) &&
|
|
598
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
599
|
+
statement.importClause?.namedBindings &&
|
|
600
|
+
ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
601
|
+
const specifier = statement.moduleSpecifier.text;
|
|
563
602
|
let hasChanges = false;
|
|
564
|
-
|
|
565
|
-
const nextName =
|
|
603
|
+
const nextElements = statement.importClause.namedBindings.elements.map((element) => {
|
|
604
|
+
const nextName = resolveRename(specifier, (element.propertyName ?? element.name).text);
|
|
566
605
|
if (!nextName) {
|
|
567
|
-
|
|
568
|
-
return;
|
|
606
|
+
return element.getText(sourceFile);
|
|
569
607
|
}
|
|
570
608
|
hasChanges = true;
|
|
571
|
-
|
|
609
|
+
const typePrefix = element.isTypeOnly ? 'type ' : '';
|
|
610
|
+
if (element.propertyName) {
|
|
611
|
+
return `${typePrefix}${nextName} as ${element.name.text}`;
|
|
612
|
+
}
|
|
613
|
+
localRenames.set(element.name.text, nextName);
|
|
614
|
+
return `${typePrefix}${nextName}`;
|
|
572
615
|
});
|
|
573
616
|
if (hasChanges) {
|
|
574
617
|
replacements.push({
|
|
575
|
-
start:
|
|
576
|
-
end:
|
|
577
|
-
replacement:
|
|
618
|
+
start: statement.getStart(sourceFile),
|
|
619
|
+
end: statement.getEnd(),
|
|
620
|
+
replacement: printImportDeclaration(statement, sourceFile, nextElements),
|
|
578
621
|
});
|
|
579
622
|
}
|
|
580
623
|
}
|
|
581
|
-
if (ts.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
ts.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
if (ts.isParameter(parent) && parent.name === node) {
|
|
605
|
-
ts.forEachChild(node, visit);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
if (ts.isPropertyDeclaration(parent) && parent.name === node) {
|
|
609
|
-
ts.forEachChild(node, visit);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
613
|
-
ts.forEachChild(node, visit);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if ((ts.isPropertyAssignment(parent) && parent.name === node) || isShadowedByLocalDeclaration(node)) {
|
|
617
|
-
ts.forEachChild(node, visit);
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
624
|
+
if (ts.isExportDeclaration(statement) &&
|
|
625
|
+
statement.moduleSpecifier &&
|
|
626
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
627
|
+
statement.exportClause &&
|
|
628
|
+
ts.isNamedExports(statement.exportClause)) {
|
|
629
|
+
const specifier = statement.moduleSpecifier.text;
|
|
630
|
+
statement.exportClause.elements.forEach((element) => {
|
|
631
|
+
const exportedFrom = element.propertyName ?? element.name;
|
|
632
|
+
const nextName = ts.isIdentifier(exportedFrom) ? resolveRename(specifier, exportedFrom.text) : undefined;
|
|
633
|
+
if (nextName) {
|
|
634
|
+
replacements.push({
|
|
635
|
+
start: exportedFrom.getStart(sourceFile),
|
|
636
|
+
end: exportedFrom.getEnd(),
|
|
637
|
+
replacement: nextName,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const visit = (node) => {
|
|
644
|
+
const nextName = ts.isIdentifier(node) ? localRenames.get(node.text) : undefined;
|
|
645
|
+
if (nextName && !isNamePosition(node) && !isShadowedInNestedScope(node)) {
|
|
620
646
|
replacements.push({
|
|
621
647
|
start: node.getStart(sourceFile),
|
|
622
648
|
end: node.getEnd(),
|
|
623
|
-
replacement: nextName,
|
|
649
|
+
replacement: renamedReference(node, nextName),
|
|
624
650
|
});
|
|
625
651
|
}
|
|
626
652
|
ts.forEachChild(node, visit);
|
|
627
653
|
};
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const key = `${replacement.start}:${replacement.end}`;
|
|
633
|
-
if (seen.has(key)) {
|
|
634
|
-
return false;
|
|
635
|
-
}
|
|
636
|
-
seen.add(key);
|
|
637
|
-
return true;
|
|
638
|
-
});
|
|
639
|
-
uniqueReplacements.sort((left, right) => right.start - left.start);
|
|
640
|
-
uniqueReplacements.forEach(({ start, end, replacement }) => {
|
|
641
|
-
result = result.slice(0, start) + replacement + result.slice(end);
|
|
642
|
-
});
|
|
643
|
-
return result;
|
|
654
|
+
if (localRenames.size > 0) {
|
|
655
|
+
visit(sourceFile);
|
|
656
|
+
}
|
|
657
|
+
return { content: applyReplacements(content, replacements), renamedByName };
|
|
644
658
|
};
|
|
645
659
|
//# sourceMappingURL=query-client-migration.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { createSourceFile } from './shared.js';
|
|
2
|
+
import { createSourceFile, printImportDeclaration } from './shared.js';
|
|
3
3
|
/**
|
|
4
4
|
* Whether an identifier is a *name* rather than a *reference* - a declaration, a property key, the
|
|
5
5
|
* right-hand side of a member access. Renaming those is what turned `PeopleDetailDataSource.getPerson`
|
|
6
6
|
* and the `postLogin` property of a config type into `legacyā¦` during the first migration run.
|
|
7
7
|
*/
|
|
8
|
-
const isNamePosition = (node) => {
|
|
8
|
+
export const isNamePosition = (node) => {
|
|
9
9
|
const parent = node.parent;
|
|
10
10
|
if (!parent)
|
|
11
11
|
return false;
|
|
@@ -65,7 +65,7 @@ const isNamePosition = (node) => {
|
|
|
65
65
|
return true;
|
|
66
66
|
return false;
|
|
67
67
|
};
|
|
68
|
-
const renamedReference = (node, nextName) => {
|
|
68
|
+
export const renamedReference = (node, nextName) => {
|
|
69
69
|
const parent = node.parent;
|
|
70
70
|
if (ts.isShorthandPropertyAssignment(parent))
|
|
71
71
|
return `${node.text}: ${nextName}`;
|
|
@@ -73,6 +73,38 @@ const renamedReference = (node, nextName) => {
|
|
|
73
73
|
return `${nextName} as ${node.text}`;
|
|
74
74
|
return nextName;
|
|
75
75
|
};
|
|
76
|
+
const bindingDeclares = (name, text) => ts.isIdentifier(name)
|
|
77
|
+
? name.text === text
|
|
78
|
+
: name.elements.some((element) => !ts.isOmittedExpression(element) && bindingDeclares(element.name, text));
|
|
79
|
+
const statementDeclares = (statement, text) => (ts.isVariableStatement(statement) &&
|
|
80
|
+
statement.declarationList.declarations.some((declaration) => bindingDeclares(declaration.name, text))) ||
|
|
81
|
+
((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement)) &&
|
|
82
|
+
statement.name?.text === text);
|
|
83
|
+
export const isShadowedInNestedScope = (identifier) => {
|
|
84
|
+
const text = identifier.text;
|
|
85
|
+
for (let scope = identifier.parent; scope && !ts.isSourceFile(scope); scope = scope.parent) {
|
|
86
|
+
if (ts.isFunctionLike(scope) && scope.parameters.some((parameter) => bindingDeclares(parameter.name, text))) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if ((ts.isFunctionExpression(scope) || ts.isClassExpression(scope)) && scope.name?.text === text) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if ((ts.isBlock(scope) || ts.isModuleBlock(scope) || ts.isCaseClause(scope) || ts.isDefaultClause(scope)) &&
|
|
93
|
+
scope.statements.some((statement) => statementDeclares(statement, text))) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if ((ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) &&
|
|
97
|
+
scope.initializer &&
|
|
98
|
+
ts.isVariableDeclarationList(scope.initializer) &&
|
|
99
|
+
scope.initializer.declarations.some((declaration) => bindingDeclares(declaration.name, text))) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (ts.isCatchClause(scope) && scope.variableDeclaration && bindingDeclares(scope.variableDeclaration.name, text)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
};
|
|
76
108
|
/** Every name the file declares itself - a local by the same name means the import is shadowed. */
|
|
77
109
|
const collectLocalDeclarations = (sourceFile) => {
|
|
78
110
|
const declared = new Set();
|
|
@@ -183,18 +215,13 @@ export const pruneUnusedNamedImports = (content) => {
|
|
|
183
215
|
const kept = node.importClause.namedBindings.elements.filter((element) => used.has(element.name.text));
|
|
184
216
|
if (kept.length === node.importClause.namedBindings.elements.length)
|
|
185
217
|
continue;
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
218
|
+
const nextImport = printImportDeclaration(node, sourceFile, kept.map((element) => element.getText(sourceFile)));
|
|
219
|
+
if (!nextImport) {
|
|
188
220
|
const end = content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
|
|
189
221
|
replacements.push({ start: node.getStart(sourceFile), end, replacement: '' });
|
|
190
222
|
continue;
|
|
191
223
|
}
|
|
192
|
-
|
|
193
|
-
replacements.push({
|
|
194
|
-
start: node.getStart(sourceFile),
|
|
195
|
-
end: node.getEnd(),
|
|
196
|
-
replacement: `import ${defaultPart}{ ${kept.map((element) => element.getText(sourceFile)).join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
197
|
-
});
|
|
224
|
+
replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement: nextImport });
|
|
198
225
|
}
|
|
199
226
|
let result = content;
|
|
200
227
|
replacements.sort((left, right) => right.start - left.start);
|
|
@@ -25,7 +25,7 @@ export class QueryV3MigrationReport {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
const nextTask = {
|
|
28
|
-
id:
|
|
28
|
+
id: formatTaskId(this.tasks.length + 1),
|
|
29
29
|
kind: input.kind,
|
|
30
30
|
title: input.title,
|
|
31
31
|
summary: input.summary,
|
|
@@ -58,11 +58,37 @@ export class QueryV3MigrationReport {
|
|
|
58
58
|
});
|
|
59
59
|
console.warn(`\nš See ${QUERY_V3_MIGRATION_REPORT_PATH} for the AI-ready follow-up list.`);
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Writes the report. A scoped run (`scopeLabel` set) replaces its own section of an existing report
|
|
63
|
+
* or appends one, so the tasks of earlier runs over other projects survive.
|
|
64
|
+
*/
|
|
65
|
+
writeToTree(tree, scopeLabel) {
|
|
66
|
+
const existing = scopeLabel ? tree.read(QUERY_V3_MIGRATION_REPORT_PATH, 'utf-8') : null;
|
|
67
|
+
if (!scopeLabel || !existing) {
|
|
68
|
+
tree.write(QUERY_V3_MIGRATION_REPORT_PATH, renderMigrationReportMarkdown(this.tasks, scopeLabel));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
tree.write(QUERY_V3_MIGRATION_REPORT_PATH, mergeScopedSection(existing, this.tasks, scopeLabel));
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
|
-
|
|
74
|
+
const sectionStart = (scopeLabel) => `<!-- migrate-to-query-v3 scope: ${scopeLabel} -->`;
|
|
75
|
+
const SECTION_END = '<!-- /migrate-to-query-v3 scope -->';
|
|
76
|
+
const mergeScopedSection = (existing, tasks, scopeLabel) => {
|
|
77
|
+
const start = sectionStart(scopeLabel);
|
|
78
|
+
const startIndex = existing.indexOf(start);
|
|
79
|
+
const endIndex = startIndex === -1 ? -1 : existing.indexOf(SECTION_END, startIndex);
|
|
80
|
+
const [before, after] = startIndex === -1 || endIndex === -1
|
|
81
|
+
? [existing.trimEnd(), '']
|
|
82
|
+
: [existing.slice(0, startIndex).trimEnd(), existing.slice(endIndex + SECTION_END.length).trimStart()];
|
|
83
|
+
const highestId = Math.max(0, ...Array.from(`${before}\n${after}`.matchAll(/QV3-(\d+)/g), (match) => Number(match[1])));
|
|
84
|
+
const renumbered = tasks.map((task, index) => ({ ...task, id: formatTaskId(highestId + index + 1) }));
|
|
85
|
+
return [before, renderTasksSection(renumbered, scopeLabel), after]
|
|
86
|
+
.filter((part) => part.length > 0)
|
|
87
|
+
.join('\n\n')
|
|
88
|
+
.concat('\n');
|
|
89
|
+
};
|
|
90
|
+
const formatTaskId = (index) => `QV3-${String(index).padStart(3, '0')}`;
|
|
91
|
+
export const renderMigrationReportMarkdown = (tasks, scopeLabel) => {
|
|
66
92
|
const header = [
|
|
67
93
|
'# Query V3 Migration Follow-Up',
|
|
68
94
|
'',
|
|
@@ -83,8 +109,16 @@ export const renderMigrationReportMarkdown = (tasks) => {
|
|
|
83
109
|
'- `Source`: which migration phase raised the task.',
|
|
84
110
|
'',
|
|
85
111
|
].join('\n');
|
|
112
|
+
return `${header}${renderTasksSection(tasks, scopeLabel)}\n`;
|
|
113
|
+
};
|
|
114
|
+
const renderTasksSection = (tasks, scopeLabel) => {
|
|
115
|
+
const heading = scopeLabel ? `## Tasks (scope: ${scopeLabel})` : '## Tasks';
|
|
116
|
+
const body = renderTasksBody(tasks);
|
|
117
|
+
return scopeLabel ? `${sectionStart(scopeLabel)}\n${heading}\n\n${body}\n${SECTION_END}` : `${heading}\n\n${body}`;
|
|
118
|
+
};
|
|
119
|
+
const renderTasksBody = (tasks) => {
|
|
86
120
|
if (tasks.length === 0) {
|
|
87
|
-
return
|
|
121
|
+
return 'No open follow-up tasks were recorded during this migration run.';
|
|
88
122
|
}
|
|
89
123
|
const tasksSection = tasks
|
|
90
124
|
.map((task) => {
|
|
@@ -129,8 +163,7 @@ export const renderMigrationReportMarkdown = (tasks) => {
|
|
|
129
163
|
];
|
|
130
164
|
}),
|
|
131
165
|
'```',
|
|
132
|
-
'',
|
|
133
166
|
].join('\n');
|
|
134
|
-
return `${
|
|
167
|
+
return `${tasksSection}${machineSection}`;
|
|
135
168
|
};
|
|
136
169
|
//# sourceMappingURL=report.js.map
|
|
@@ -13,6 +13,23 @@ export const ensureConfigSuffix = (name) => {
|
|
|
13
13
|
}
|
|
14
14
|
return `${name}Config`;
|
|
15
15
|
};
|
|
16
|
+
const importSpecifierText = (element) => element.propertyName ? `${element.propertyName.text} as ${element.name.text}` : element.name.text;
|
|
17
|
+
/**
|
|
18
|
+
* Prints `node` with `namedElements` as its named bindings, keeping the default import, the
|
|
19
|
+
* `import type` modifier, the quote style and any import attributes. Returns an empty string when
|
|
20
|
+
* nothing would be left to import.
|
|
21
|
+
*/
|
|
22
|
+
export const printImportDeclaration = (node, sourceFile, namedElements) => {
|
|
23
|
+
const clause = node.importClause;
|
|
24
|
+
const defaultName = clause?.name?.text;
|
|
25
|
+
const bindings = [defaultName, namedElements.length > 0 ? `{ ${namedElements.join(', ')} }` : undefined].filter((part) => !!part);
|
|
26
|
+
if (bindings.length === 0) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
const typePrefix = clause?.isTypeOnly ? 'type ' : '';
|
|
30
|
+
const attributes = node.attributes ? ` ${node.attributes.getText(sourceFile)}` : '';
|
|
31
|
+
return `import ${typePrefix}${bindings.join(', ')} from ${node.moduleSpecifier.getText(sourceFile)}${attributes};`;
|
|
32
|
+
};
|
|
16
33
|
export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier }) => {
|
|
17
34
|
const sourceFile = createSourceFile(content);
|
|
18
35
|
let importNode;
|
|
@@ -32,13 +49,14 @@ export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier })
|
|
|
32
49
|
const namedBindings = importNode.importClause.namedBindings;
|
|
33
50
|
namedBindings.elements.forEach((element) => {
|
|
34
51
|
missingImports.delete(element.name.text);
|
|
52
|
+
missingImports.delete(importSpecifierText(element));
|
|
35
53
|
});
|
|
36
54
|
if (missingImports.size === 0) {
|
|
37
55
|
return content;
|
|
38
56
|
}
|
|
39
57
|
const existingImports = namedBindings.elements.map((element) => element.getText(sourceFile));
|
|
40
58
|
const nextImports = [...existingImports, ...Array.from(missingImports).sort()].sort();
|
|
41
|
-
const nextImportStatement =
|
|
59
|
+
const nextImportStatement = printImportDeclaration(importNode, sourceFile, nextImports);
|
|
42
60
|
return content.slice(0, importNode.getStart(sourceFile)) + nextImportStatement + content.slice(importNode.getEnd());
|
|
43
61
|
}
|
|
44
62
|
let lastImportEnd = 0;
|