@ethlete/query 6.0.0-next.51 → 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 +78 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +393 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
- package/fesm2022/ethlete-query-testing.mjs +67 -25
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +7895 -7226
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/generators.json +5 -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-opt-in-features/migration.js +10 -9
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
- package/generators/migrate-to-query-v3/cleanup-migration.js +163 -53
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
- package/generators/migrate-to-query-v3/migration-scope.js +2 -0
- package/generators/migrate-to-query-v3/migration.js +5 -4
- package/generators/migrate-to-query-v3/module-graph.js +41 -29
- package/generators/migrate-to-query-v3/query-client-migration.js +209 -126
- 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 +46 -1
- package/generators/prep-for-query-v3/migration.js +400 -57
- package/migrations/query-field-default-types.md +43 -0
- package/migrations/search-query-field-string.md +43 -0
- package/migrations.json +16 -0
- package/package.json +5 -1
- package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
- package/types/ethlete-query-testing.d.ts +28 -11
- package/types/ethlete-query.d.ts +789 -584
|
@@ -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;
|
|
@@ -143,4 +161,31 @@ export const addJsDocTag = (content, sourceFile, node, tag) => {
|
|
|
143
161
|
const indentation = ' '.repeat(sourceFile.getLineAndCharacterOfPosition(start).character);
|
|
144
162
|
return (content.slice(0, start) + `/**\n${indentation} * ${tag}\n${indentation} */\n${indentation}` + content.slice(start));
|
|
145
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
|
+
};
|
|
146
191
|
//# sourceMappingURL=shared.js.map
|