@ethlete/query 6.0.0-next.6 → 6.0.0-next.61
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 +945 -0
- package/fesm2022/ethlete-query-devtools-contract.mjs +1 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +411 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
- package/fesm2022/ethlete-query-testing.mjs +548 -169
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +14680 -7172
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/deprecate-legacy-queries/migration.js +85 -0
- package/generators/deprecate-legacy-queries/schema.json +26 -0
- package/generators/generators.json +30 -0
- package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
- package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
- package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
- package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
- package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
- package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
- package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
- package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
- package/generators/migrate-query-client-features/migration.js +144 -0
- package/generators/migrate-query-client-features/report.js +136 -0
- package/generators/migrate-query-client-features/schema.json +26 -0
- package/generators/migrate-query-opt-in-features/migration.js +195 -0
- package/generators/migrate-query-opt-in-features/report.js +143 -0
- package/generators/migrate-query-opt-in-features/schema.json +31 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +160 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +247 -125
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +274 -152
- package/generators/migrate-to-query-v3/migration-scope.js +40 -0
- package/generators/migrate-to-query-v3/migration.js +19 -10
- package/generators/migrate-to-query-v3/module-graph.js +165 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +342 -160
- package/generators/migrate-to-query-v3/rename-symbols.js +233 -0
- package/generators/migrate-to-query-v3/report.js +40 -7
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +97 -3
- package/generators/move-devtools-contract-imports/migration.js +155 -0
- package/generators/move-devtools-contract-imports/moved-names.js +153 -0
- package/generators/move-devtools-contract-imports/schema.json +26 -0
- package/generators/prep-for-query-v3/migration.js +465 -65
- package/generators/report-legacy-query-apis/migration.js +256 -0
- package/generators/report-legacy-query-apis/schema.json +25 -0
- package/migrations/query-field-default-types.md +43 -0
- package/migrations/search-query-field-string.md +43 -0
- package/migrations.json +87 -0
- package/package.json +34 -14
- package/types/ethlete-query-devtools-contract.d.ts +2 -0
- package/types/ethlete-query-ngrx-toolkit.d.ts +131 -0
- package/types/ethlete-query-testing.d.ts +295 -160
- package/types/ethlete-query.d.ts +9755 -3682
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { createSourceFile, printImportDeclaration } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether an identifier is a *name* rather than a *reference* - a declaration, a property key, the
|
|
5
|
+
* right-hand side of a member access. Renaming those is what turned `PeopleDetailDataSource.getPerson`
|
|
6
|
+
* and the `postLogin` property of a config type into `legacy…` during the first migration run.
|
|
7
|
+
*/
|
|
8
|
+
export const isNamePosition = (node) => {
|
|
9
|
+
const parent = node.parent;
|
|
10
|
+
if (!parent)
|
|
11
|
+
return false;
|
|
12
|
+
// `foo.bar` / `Foo.Bar` - the part after the dot is never the imported binding.
|
|
13
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node)
|
|
14
|
+
return true;
|
|
15
|
+
if (ts.isQualifiedName(parent) && parent.right === node)
|
|
16
|
+
return true;
|
|
17
|
+
// Object literal / type member keys, enum members, JSX-ish attribute names.
|
|
18
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node)
|
|
19
|
+
return true;
|
|
20
|
+
if (ts.isPropertySignature(parent) && parent.name === node)
|
|
21
|
+
return true;
|
|
22
|
+
if (ts.isMethodSignature(parent) && parent.name === node)
|
|
23
|
+
return true;
|
|
24
|
+
if (ts.isEnumMember(parent) && parent.name === node)
|
|
25
|
+
return true;
|
|
26
|
+
if (ts.isBindingElement(parent) && parent.propertyName === node)
|
|
27
|
+
return true;
|
|
28
|
+
// Declarations of any kind.
|
|
29
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node)
|
|
30
|
+
return true;
|
|
31
|
+
if (ts.isFunctionDeclaration(parent) && parent.name === node)
|
|
32
|
+
return true;
|
|
33
|
+
if (ts.isClassDeclaration(parent) && parent.name === node)
|
|
34
|
+
return true;
|
|
35
|
+
if (ts.isInterfaceDeclaration(parent) && parent.name === node)
|
|
36
|
+
return true;
|
|
37
|
+
if (ts.isTypeAliasDeclaration(parent) && parent.name === node)
|
|
38
|
+
return true;
|
|
39
|
+
if (ts.isEnumDeclaration(parent) && parent.name === node)
|
|
40
|
+
return true;
|
|
41
|
+
if (ts.isModuleDeclaration(parent) && parent.name === node)
|
|
42
|
+
return true;
|
|
43
|
+
if (ts.isMethodDeclaration(parent) && parent.name === node)
|
|
44
|
+
return true;
|
|
45
|
+
if (ts.isPropertyDeclaration(parent) && parent.name === node)
|
|
46
|
+
return true;
|
|
47
|
+
if (ts.isGetAccessorDeclaration(parent) && parent.name === node)
|
|
48
|
+
return true;
|
|
49
|
+
if (ts.isSetAccessorDeclaration(parent) && parent.name === node)
|
|
50
|
+
return true;
|
|
51
|
+
if (ts.isParameter(parent) && parent.name === node)
|
|
52
|
+
return true;
|
|
53
|
+
if (ts.isTypeParameterDeclaration(parent) && parent.name === node)
|
|
54
|
+
return true;
|
|
55
|
+
if (ts.isLabeledStatement(parent) && parent.label === node)
|
|
56
|
+
return true;
|
|
57
|
+
// Import / export clauses are rewritten by the caller, never by the reference pass.
|
|
58
|
+
if (ts.isImportSpecifier(parent))
|
|
59
|
+
return true;
|
|
60
|
+
if (ts.isExportSpecifier(parent) &&
|
|
61
|
+
(parent.parent.parent.moduleSpecifier || (parent.propertyName && parent.name === node))) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (ts.isImportClause(parent) || ts.isNamespaceImport(parent))
|
|
65
|
+
return true;
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
export const renamedReference = (node, nextName) => {
|
|
69
|
+
const parent = node.parent;
|
|
70
|
+
if (ts.isShorthandPropertyAssignment(parent))
|
|
71
|
+
return `${node.text}: ${nextName}`;
|
|
72
|
+
if (ts.isExportSpecifier(parent) && !parent.propertyName)
|
|
73
|
+
return `${nextName} as ${node.text}`;
|
|
74
|
+
return nextName;
|
|
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
|
+
};
|
|
108
|
+
/** Every name the file declares itself - a local by the same name means the import is shadowed. */
|
|
109
|
+
const collectLocalDeclarations = (sourceFile) => {
|
|
110
|
+
const declared = new Set();
|
|
111
|
+
const add = (name) => {
|
|
112
|
+
if (name && ts.isIdentifier(name))
|
|
113
|
+
declared.add(name.text);
|
|
114
|
+
};
|
|
115
|
+
const visit = (node) => {
|
|
116
|
+
if (ts.isVariableDeclaration(node))
|
|
117
|
+
add(node.name);
|
|
118
|
+
if (ts.isFunctionDeclaration(node))
|
|
119
|
+
add(node.name);
|
|
120
|
+
if (ts.isClassDeclaration(node))
|
|
121
|
+
add(node.name);
|
|
122
|
+
if (ts.isInterfaceDeclaration(node))
|
|
123
|
+
add(node.name);
|
|
124
|
+
if (ts.isTypeAliasDeclaration(node))
|
|
125
|
+
add(node.name);
|
|
126
|
+
if (ts.isEnumDeclaration(node))
|
|
127
|
+
add(node.name);
|
|
128
|
+
if (ts.isParameter(node))
|
|
129
|
+
add(node.name);
|
|
130
|
+
if (ts.isBindingElement(node))
|
|
131
|
+
add(node.name);
|
|
132
|
+
ts.forEachChild(node, visit);
|
|
133
|
+
};
|
|
134
|
+
visit(sourceFile);
|
|
135
|
+
return declared;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Renames references to imported bindings, and only those.
|
|
139
|
+
*
|
|
140
|
+
* `renames` maps a *local binding name* to its new name. The caller is responsible for having
|
|
141
|
+
* verified that the binding really is the symbol being migrated (see `ModuleGraph`); this pass only
|
|
142
|
+
* makes sure the rewrite lands on references rather than on same-named properties, declarations or
|
|
143
|
+
* type members. A name the file also declares locally is skipped entirely and reported back, since
|
|
144
|
+
* a rename there would silently change which value the code reads.
|
|
145
|
+
*/
|
|
146
|
+
export const renameImportedReferences = (content, renames) => {
|
|
147
|
+
if (renames.size === 0)
|
|
148
|
+
return { content, shadowed: [] };
|
|
149
|
+
const sourceFile = createSourceFile(content);
|
|
150
|
+
const declaredLocally = collectLocalDeclarations(sourceFile);
|
|
151
|
+
const shadowed = Array.from(renames.keys()).filter((name) => declaredLocally.has(name));
|
|
152
|
+
const applicable = new Map(Array.from(renames.entries()).filter(([name]) => !declaredLocally.has(name)));
|
|
153
|
+
if (applicable.size === 0)
|
|
154
|
+
return { content, shadowed };
|
|
155
|
+
const replacements = [];
|
|
156
|
+
const visit = (node) => {
|
|
157
|
+
if (ts.isIdentifier(node)) {
|
|
158
|
+
const nextName = applicable.get(node.text);
|
|
159
|
+
if (nextName && !isNamePosition(node)) {
|
|
160
|
+
replacements.push({
|
|
161
|
+
start: node.getStart(sourceFile),
|
|
162
|
+
end: node.getEnd(),
|
|
163
|
+
replacement: renamedReference(node, nextName),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
ts.forEachChild(node, visit);
|
|
168
|
+
};
|
|
169
|
+
visit(sourceFile);
|
|
170
|
+
let result = content;
|
|
171
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
172
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
173
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
174
|
+
});
|
|
175
|
+
return { content: result, shadowed };
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Drops named imports whose local name no longer appears anywhere else in the file.
|
|
179
|
+
*
|
|
180
|
+
* The migration rewrites away the constructs that needed `def`, `AnyLegacyQueryCreator` and friends;
|
|
181
|
+
* leaving the imports behind turns every touched file into a lint error, and `formatFiles` runs in
|
|
182
|
+
* the same generator invocation so they land that way. Identifiers inside template strings count as
|
|
183
|
+
* usage too - a component referenced only from an inline template must not be pruned.
|
|
184
|
+
*/
|
|
185
|
+
export const pruneUnusedNamedImports = (content) => {
|
|
186
|
+
const sourceFile = createSourceFile(content);
|
|
187
|
+
const used = new Set();
|
|
188
|
+
const addWordsFrom = (text) => {
|
|
189
|
+
for (const word of text.match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
190
|
+
used.add(word);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const visit = (node) => {
|
|
194
|
+
if (ts.isIdentifier(node) && !ts.isImportSpecifier(node.parent) && !ts.isImportClause(node.parent)) {
|
|
195
|
+
used.add(node.text);
|
|
196
|
+
}
|
|
197
|
+
// A symbol referenced only from an inline template or a `styleUrl`-style string is still used.
|
|
198
|
+
if (ts.isStringLiteralLike(node) && !ts.isImportDeclaration(node.parent)) {
|
|
199
|
+
addWordsFrom(node.text);
|
|
200
|
+
}
|
|
201
|
+
if (ts.isTemplateLiteralToken(node)) {
|
|
202
|
+
addWordsFrom(node.text);
|
|
203
|
+
}
|
|
204
|
+
ts.forEachChild(node, visit);
|
|
205
|
+
};
|
|
206
|
+
visit(sourceFile);
|
|
207
|
+
const replacements = [];
|
|
208
|
+
for (const node of sourceFile.statements) {
|
|
209
|
+
if (!ts.isImportDeclaration(node) ||
|
|
210
|
+
!ts.isStringLiteral(node.moduleSpecifier) ||
|
|
211
|
+
!node.importClause?.namedBindings ||
|
|
212
|
+
!ts.isNamedImports(node.importClause.namedBindings)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const kept = node.importClause.namedBindings.elements.filter((element) => used.has(element.name.text));
|
|
216
|
+
if (kept.length === node.importClause.namedBindings.elements.length)
|
|
217
|
+
continue;
|
|
218
|
+
const nextImport = printImportDeclaration(node, sourceFile, kept.map((element) => element.getText(sourceFile)));
|
|
219
|
+
if (!nextImport) {
|
|
220
|
+
const end = content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
|
|
221
|
+
replacements.push({ start: node.getStart(sourceFile), end, replacement: '' });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement: nextImport });
|
|
225
|
+
}
|
|
226
|
+
let result = content;
|
|
227
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
228
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
229
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
230
|
+
});
|
|
231
|
+
return result;
|
|
232
|
+
};
|
|
233
|
+
//# sourceMappingURL=rename-symbols.js.map
|
|
@@ -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}\n${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
|
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
"title": "Migrate to Query v3",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"properties": {
|
|
7
|
+
"projects": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": { "type": "string" },
|
|
10
|
+
"description": "Nx projects to migrate. Defaults to every project in the workspace. Use this to adopt v3 one app or library at a time instead of rewriting the whole monorepo in one run - but keep a query client and the creators built from it in the same run, since they are migrated together.",
|
|
11
|
+
"alias": "project"
|
|
12
|
+
},
|
|
13
|
+
"include": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": { "type": "string" },
|
|
16
|
+
"description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/queries'). Unioned with `projects`. Defaults to the whole workspace.",
|
|
17
|
+
"alias": "includePath"
|
|
18
|
+
},
|
|
7
19
|
"skipFormat": {
|
|
8
20
|
"type": "boolean",
|
|
9
21
|
"description": "Skip formatting files after migration",
|
|
@@ -13,13 +13,34 @@ 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;
|
|
19
36
|
ts.forEachChild(sourceFile, (node) => {
|
|
20
|
-
if (
|
|
37
|
+
if (!importNode &&
|
|
38
|
+
ts.isImportDeclaration(node) &&
|
|
21
39
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
22
|
-
node.moduleSpecifier.text === moduleSpecifier
|
|
40
|
+
node.moduleSpecifier.text === moduleSpecifier &&
|
|
41
|
+
!node.importClause?.isTypeOnly &&
|
|
42
|
+
node.importClause?.namedBindings &&
|
|
43
|
+
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
23
44
|
importNode = node;
|
|
24
45
|
}
|
|
25
46
|
});
|
|
@@ -28,13 +49,14 @@ export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier })
|
|
|
28
49
|
const namedBindings = importNode.importClause.namedBindings;
|
|
29
50
|
namedBindings.elements.forEach((element) => {
|
|
30
51
|
missingImports.delete(element.name.text);
|
|
52
|
+
missingImports.delete(importSpecifierText(element));
|
|
31
53
|
});
|
|
32
54
|
if (missingImports.size === 0) {
|
|
33
55
|
return content;
|
|
34
56
|
}
|
|
35
57
|
const existingImports = namedBindings.elements.map((element) => element.getText(sourceFile));
|
|
36
58
|
const nextImports = [...existingImports, ...Array.from(missingImports).sort()].sort();
|
|
37
|
-
const nextImportStatement =
|
|
59
|
+
const nextImportStatement = printImportDeclaration(importNode, sourceFile, nextImports);
|
|
38
60
|
return content.slice(0, importNode.getStart(sourceFile)) + nextImportStatement + content.slice(importNode.getEnd());
|
|
39
61
|
}
|
|
40
62
|
let lastImportEnd = 0;
|
|
@@ -55,6 +77,8 @@ export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier })
|
|
|
55
77
|
};
|
|
56
78
|
export const ensureImportFromQuery = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@ethlete/query' });
|
|
57
79
|
export const ensureAngularCoreImports = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@angular/core' });
|
|
80
|
+
/** `toProvideFn` / `toInjectFn` - the provider-definition extractors every generated ref pair needs. */
|
|
81
|
+
export const ensureImportFromEthleteCore = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@ethlete/core' });
|
|
58
82
|
export const getVariableStatementEnd = (sourceFile, variableName, initializerName) => {
|
|
59
83
|
let variableStatementEnd;
|
|
60
84
|
const visit = (node) => {
|
|
@@ -94,4 +118,74 @@ export const getLineNumber = (node, sourceFile) => {
|
|
|
94
118
|
return line + 1;
|
|
95
119
|
};
|
|
96
120
|
export const getLineNumberFromPosition = (content, position) => content.slice(0, position).split('\n').length;
|
|
121
|
+
const MIGRATION_GUIDE_URL = 'https://ethlete-sdk-docs.web.app/query/migrating-from-v2';
|
|
122
|
+
/**
|
|
123
|
+
* The tag every `createLegacyQueryCreator(…)` wrapper carries. The wrapper is a migration
|
|
124
|
+
* scaffold, so marking it is what turns "which call sites are still on v2" from a grep into
|
|
125
|
+
* strikethrough in the editor.
|
|
126
|
+
*/
|
|
127
|
+
export const legacyQueryDeprecationTag = (creatorName) => `@deprecated Legacy (v2) query wrapper. Migrate the call sites to ${creatorName ? `\`${creatorName}\`` : 'the query creator it wraps'} and delete this wrapper - see ${MIGRATION_GUIDE_URL}.`;
|
|
128
|
+
/**
|
|
129
|
+
* Adds a tag to `node`'s JSDoc, creating the block when there is none. Returns `null` when the
|
|
130
|
+
* tag is already there, so a caller can tell "nothing to do" from "rewritten".
|
|
131
|
+
*
|
|
132
|
+
* Leading `//` comments stay glued to the node - an `eslint-disable-next-line` only works from
|
|
133
|
+
* the line directly above.
|
|
134
|
+
*/
|
|
135
|
+
export const addJsDocTag = (content, sourceFile, node, tag) => {
|
|
136
|
+
const jsDocs = node.jsDoc ?? [];
|
|
137
|
+
const existing = jsDocs[jsDocs.length - 1];
|
|
138
|
+
const tagName = /^@\w+/.exec(tag)?.[0];
|
|
139
|
+
if (existing && tagName && new RegExp(`${tagName}\\b`).test(existing.getText())) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (existing) {
|
|
143
|
+
const start = existing.getStart(sourceFile);
|
|
144
|
+
const indentation = ' '.repeat(sourceFile.getLineAndCharacterOfPosition(start).character);
|
|
145
|
+
const text = existing.getText();
|
|
146
|
+
const body = text.slice(0, text.lastIndexOf('*/')).replace(/\s+$/, '');
|
|
147
|
+
// A one-line `/** … */` has to become a block before a tag line can be appended to it.
|
|
148
|
+
const opened = body.includes('\n') ? body : `/**\n${indentation} * ${body.slice(3).trim()}`;
|
|
149
|
+
return (content.slice(0, start) +
|
|
150
|
+
`${opened}\n${indentation} *\n${indentation} * ${tag}\n${indentation} */` +
|
|
151
|
+
content.slice(existing.getEnd()));
|
|
152
|
+
}
|
|
153
|
+
let start = node.getStart(sourceFile, false);
|
|
154
|
+
for (const comment of (ts.getLeadingCommentRanges(content, node.getFullStart()) ?? []).reverse()) {
|
|
155
|
+
if (comment.kind !== ts.SyntaxKind.SingleLineCommentTrivia)
|
|
156
|
+
break;
|
|
157
|
+
if (!/^\n[ \t]*$/.test(content.slice(comment.end, start)))
|
|
158
|
+
break;
|
|
159
|
+
start = comment.pos;
|
|
160
|
+
}
|
|
161
|
+
const indentation = ' '.repeat(sourceFile.getLineAndCharacterOfPosition(start).character);
|
|
162
|
+
return (content.slice(0, start) + `/**\n${indentation} * ${tag}\n${indentation} */\n${indentation}` + content.slice(start));
|
|
163
|
+
};
|
|
164
|
+
const TUPLE_PROVIDER_ACTION = 'Delete the call: a v3 client, auth provider or socket is root-provided the moment something injects it. For a test or an override, provide it with `toProvideFn(ref)` from `@ethlete/core`.';
|
|
165
|
+
export const REMOVED_EXPERIMENTAL_QUERY_HELPERS = {
|
|
166
|
+
createQueryClientConfig: 'Replace it with `createQueryClient({ name, baseUrl })`, which returns the provider tuple, and derive `toProvideFn` / `toInjectFn` from it.',
|
|
167
|
+
createBearerAuthProviderConfig: 'Replace it with `createBearerAuthProvider({ name, queryClientRef, queries, features })`, which returns the provider tuple, and derive `toProvideFn` / `toInjectFn` from it.',
|
|
168
|
+
provideQueryClient: TUPLE_PROVIDER_ACTION,
|
|
169
|
+
provideBearerAuthProvider: TUPLE_PROVIDER_ACTION,
|
|
170
|
+
provideWebSocketClient: `Build the socket with \`createWebSocketClient({ name, url, io })\`. ${TUPLE_PROVIDER_ACTION}`,
|
|
171
|
+
};
|
|
172
|
+
export const findRemovedExperimentalQueryHelpers = (sourceFile) => {
|
|
173
|
+
const found = [];
|
|
174
|
+
sourceFile.statements.forEach((statement) => {
|
|
175
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
176
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
177
|
+
statement.moduleSpecifier.text !== '@ethlete/query' ||
|
|
178
|
+
!statement.importClause?.namedBindings ||
|
|
179
|
+
!ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
statement.importClause.namedBindings.elements.forEach((element) => {
|
|
183
|
+
const name = (element.propertyName ?? element.name).text;
|
|
184
|
+
if (name in REMOVED_EXPERIMENTAL_QUERY_HELPERS) {
|
|
185
|
+
found.push({ name, line: getLineNumber(element, sourceFile) });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
return found;
|
|
190
|
+
};
|
|
97
191
|
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { formatFiles } from '@nx/devkit';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { createMigrationScope } from '../migrate-to-query-v3/migration-scope.js';
|
|
4
|
+
import { createSourceFile } from '../migrate-to-query-v3/shared.js';
|
|
5
|
+
import { MOVED_DEVTOOLS_CONTRACT_NAMES } from './moved-names.js';
|
|
6
|
+
const QUERY_MODULE = '@ethlete/query';
|
|
7
|
+
const CONTRACT_MODULE = '@ethlete/query/devtools-contract';
|
|
8
|
+
const isMigratableFile = (filePath) => /\.(ts|mts|cts|tsx)$/.test(filePath) && !filePath.endsWith('.d.ts');
|
|
9
|
+
const specifierName = (specifier) => (specifier.propertyName ?? specifier.name).text;
|
|
10
|
+
const isMoved = (specifier) => MOVED_DEVTOOLS_CONTRACT_NAMES.has(specifierName(specifier));
|
|
11
|
+
const moduleOf = (statement) => {
|
|
12
|
+
if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
|
|
13
|
+
return undefined;
|
|
14
|
+
const specifier = statement.moduleSpecifier;
|
|
15
|
+
return specifier && ts.isStringLiteral(specifier) ? specifier : undefined;
|
|
16
|
+
};
|
|
17
|
+
const namedBindingsOf = (statement) => {
|
|
18
|
+
if (ts.isExportDeclaration(statement)) {
|
|
19
|
+
return statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause : undefined;
|
|
20
|
+
}
|
|
21
|
+
const bindings = statement.importClause?.namedBindings;
|
|
22
|
+
return bindings && ts.isNamedImports(bindings) ? bindings : undefined;
|
|
23
|
+
};
|
|
24
|
+
const isTypeOnlyDeclaration = (statement) => ts.isExportDeclaration(statement) ? statement.isTypeOnly : !!statement.importClause?.isTypeOnly;
|
|
25
|
+
const declarationKey = (statement) => `${ts.isImportDeclaration(statement) ? 'import' : 'export'}${isTypeOnlyDeclaration(statement) ? ' type' : ''}`;
|
|
26
|
+
const findNamespaceUses = (sourceFile, content) => {
|
|
27
|
+
const uses = [];
|
|
28
|
+
for (const statement of sourceFile.statements) {
|
|
29
|
+
if (!ts.isImportDeclaration(statement) || moduleOf(statement)?.text !== QUERY_MODULE)
|
|
30
|
+
continue;
|
|
31
|
+
const bindings = statement.importClause?.namedBindings;
|
|
32
|
+
if (!bindings || !ts.isNamespaceImport(bindings))
|
|
33
|
+
continue;
|
|
34
|
+
for (const name of MOVED_DEVTOOLS_CONTRACT_NAMES) {
|
|
35
|
+
if (content.includes(`${bindings.name.text}.${name}`))
|
|
36
|
+
uses.push(`${bindings.name.text}.${name}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return uses;
|
|
40
|
+
};
|
|
41
|
+
export const moveDevtoolsContractImportsInFile = (content, filePath) => {
|
|
42
|
+
if (!content.includes(QUERY_MODULE))
|
|
43
|
+
return null;
|
|
44
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
45
|
+
const edits = [];
|
|
46
|
+
const contractTargets = new Map();
|
|
47
|
+
for (const statement of sourceFile.statements) {
|
|
48
|
+
if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
|
|
49
|
+
continue;
|
|
50
|
+
if (moduleOf(statement)?.text !== CONTRACT_MODULE)
|
|
51
|
+
continue;
|
|
52
|
+
const bindings = namedBindingsOf(statement);
|
|
53
|
+
const key = declarationKey(statement);
|
|
54
|
+
if (bindings && !contractTargets.has(key))
|
|
55
|
+
contractTargets.set(key, { bindings, added: [] });
|
|
56
|
+
}
|
|
57
|
+
for (const statement of sourceFile.statements) {
|
|
58
|
+
if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
|
|
59
|
+
continue;
|
|
60
|
+
const moduleSpecifier = moduleOf(statement);
|
|
61
|
+
if (moduleSpecifier?.text !== QUERY_MODULE)
|
|
62
|
+
continue;
|
|
63
|
+
const bindings = namedBindingsOf(statement);
|
|
64
|
+
if (!bindings)
|
|
65
|
+
continue;
|
|
66
|
+
const elements = bindings.elements;
|
|
67
|
+
const moved = elements.filter(isMoved);
|
|
68
|
+
if (moved.length === 0)
|
|
69
|
+
continue;
|
|
70
|
+
const kept = elements.filter((specifier) => !isMoved(specifier));
|
|
71
|
+
const movedText = moved.map((specifier) => specifier.getText(sourceFile));
|
|
72
|
+
const hasDefaultImport = ts.isImportDeclaration(statement) && !!statement.importClause?.name;
|
|
73
|
+
const target = contractTargets.get(declarationKey(statement));
|
|
74
|
+
if (target) {
|
|
75
|
+
target.added.push(...movedText);
|
|
76
|
+
}
|
|
77
|
+
if (kept.length === 0 && !hasDefaultImport) {
|
|
78
|
+
if (target) {
|
|
79
|
+
edits.push({ start: statement.getFullStart(), end: statement.getEnd(), text: '' });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const quote = moduleSpecifier.getText(sourceFile)[0];
|
|
83
|
+
edits.push({
|
|
84
|
+
start: moduleSpecifier.getStart(sourceFile),
|
|
85
|
+
end: moduleSpecifier.getEnd(),
|
|
86
|
+
text: `${quote}${CONTRACT_MODULE}${quote}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
edits.push({
|
|
92
|
+
start: bindings.getStart(sourceFile),
|
|
93
|
+
end: bindings.getEnd(),
|
|
94
|
+
text: kept.length ? `{ ${kept.map((specifier) => specifier.getText(sourceFile)).join(', ')} }` : '{}',
|
|
95
|
+
});
|
|
96
|
+
if (!target) {
|
|
97
|
+
const quote = moduleSpecifier.getText(sourceFile)[0];
|
|
98
|
+
edits.push({
|
|
99
|
+
start: statement.getEnd(),
|
|
100
|
+
end: statement.getEnd(),
|
|
101
|
+
text: `\n${declarationKey(statement)} { ${movedText.join(', ')} } from ${quote}${CONTRACT_MODULE}${quote};`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const { bindings, added } of contractTargets.values()) {
|
|
106
|
+
if (added.length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
const last = bindings.elements.at(-1);
|
|
109
|
+
const position = last ? last.getEnd() : bindings.getStart(sourceFile) + 1;
|
|
110
|
+
edits.push({ start: position, end: position, text: last ? `, ${added.join(', ')}` : ` ${added.join(', ')} ` });
|
|
111
|
+
}
|
|
112
|
+
if (edits.length === 0)
|
|
113
|
+
return null;
|
|
114
|
+
return edits
|
|
115
|
+
.sort((a, b) => b.start - a.start)
|
|
116
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end), content);
|
|
117
|
+
};
|
|
118
|
+
export const moveDevtoolsContractImports = (tree, scope) => {
|
|
119
|
+
const touchedFiles = [];
|
|
120
|
+
const manualUses = [];
|
|
121
|
+
scope.visit(tree, (filePath) => {
|
|
122
|
+
if (!isMigratableFile(filePath))
|
|
123
|
+
return;
|
|
124
|
+
const content = tree.read(filePath, 'utf-8');
|
|
125
|
+
if (!content?.includes(QUERY_MODULE))
|
|
126
|
+
return;
|
|
127
|
+
for (const use of findNamespaceUses(createSourceFile(content, filePath), content)) {
|
|
128
|
+
manualUses.push(`${filePath}: ${use}`);
|
|
129
|
+
}
|
|
130
|
+
const next = moveDevtoolsContractImportsInFile(content, filePath);
|
|
131
|
+
if (next === null)
|
|
132
|
+
return;
|
|
133
|
+
tree.write(filePath, next);
|
|
134
|
+
touchedFiles.push(filePath);
|
|
135
|
+
});
|
|
136
|
+
return { touchedFiles, manualUses };
|
|
137
|
+
};
|
|
138
|
+
export default async function migrate(tree, schema) {
|
|
139
|
+
console.log(`\n🔄 Moving devtools contract imports to ${CONTRACT_MODULE}...`);
|
|
140
|
+
const scope = createMigrationScope(tree, schema);
|
|
141
|
+
console.log(` Scope: ${scope.describe()}`);
|
|
142
|
+
const { touchedFiles, manualUses } = moveDevtoolsContractImports(tree, scope);
|
|
143
|
+
console.log(` Rewrote imports in ${touchedFiles.length} file${touchedFiles.length === 1 ? '' : 's'}.`);
|
|
144
|
+
if (manualUses.length > 0) {
|
|
145
|
+
console.log(`\n⚠️ Namespace imports of ${QUERY_MODULE} use names that moved to ${CONTRACT_MODULE}:`);
|
|
146
|
+
for (const use of manualUses) {
|
|
147
|
+
console.log(` ${use}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!schema.skipFormat) {
|
|
151
|
+
await formatFiles(tree);
|
|
152
|
+
}
|
|
153
|
+
console.log('\n✅ Devtools contract imports moved.');
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=migration.js.map
|