@ethlete/query 6.0.0-next.6 → 6.0.0-next.60
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 +939 -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
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
1
|
import * as ts from 'typescript';
|
|
3
|
-
import {
|
|
2
|
+
import { collectV2BearerAuthConfigs, renderAuthProviderBody } from './auth-provider-scaffold.js';
|
|
3
|
+
import { createModuleGraph } from './module-graph.js';
|
|
4
|
+
import { pruneUnusedNamedImports, renameImportedReferences } from './rename-symbols.js';
|
|
5
|
+
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, legacyQueryDeprecationTag, } from './shared.js';
|
|
4
6
|
const LEGACY_HTTP_METHOD = {
|
|
5
7
|
GET: 'get',
|
|
6
8
|
POST: 'post',
|
|
@@ -8,19 +10,22 @@ const LEGACY_HTTP_METHOD = {
|
|
|
8
10
|
PATCH: 'patch',
|
|
9
11
|
DELETE: 'delete',
|
|
10
12
|
};
|
|
13
|
+
const LEGACY_GQL_METHODS = ['gqlQuery', 'gqlMutate'];
|
|
14
|
+
const HTTP_OPTION_NAMES = ['reportProgress', 'responseType', 'transferCache', 'withCredentials'];
|
|
11
15
|
const isLegacyHttpMethod = (value) => {
|
|
12
16
|
return Object.values(LEGACY_HTTP_METHOD).includes(value);
|
|
13
17
|
};
|
|
14
|
-
export const createNewQueryCreators = (tree, queryClientFiles, report) => {
|
|
18
|
+
export const createNewQueryCreators = (tree, queryClientFiles, report, scope) => {
|
|
15
19
|
const createdFiles = [];
|
|
16
20
|
const authProvidersNeeded = new Map();
|
|
17
21
|
const clientNameToFilePath = new Map();
|
|
22
|
+
const creatorToClient = new Map();
|
|
18
23
|
queryClientFiles.forEach((configNames, filePath) => {
|
|
19
24
|
configNames.forEach((configName) => {
|
|
20
25
|
clientNameToFilePath.set(configName, filePath);
|
|
21
26
|
});
|
|
22
27
|
});
|
|
23
|
-
|
|
28
|
+
scope.visit(tree, (filePath) => {
|
|
24
29
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
25
30
|
return;
|
|
26
31
|
}
|
|
@@ -33,6 +38,7 @@ export const createNewQueryCreators = (tree, queryClientFiles, report) => {
|
|
|
33
38
|
return;
|
|
34
39
|
}
|
|
35
40
|
legacyCreators.forEach((creator) => {
|
|
41
|
+
creatorToClient.set(creator.name, creator.clientName);
|
|
36
42
|
if (!creator.secure) {
|
|
37
43
|
return;
|
|
38
44
|
}
|
|
@@ -48,17 +54,23 @@ export const createNewQueryCreators = (tree, queryClientFiles, report) => {
|
|
|
48
54
|
}
|
|
49
55
|
});
|
|
50
56
|
if (authProvidersNeeded.size > 0) {
|
|
51
|
-
createAuthProviders(
|
|
57
|
+
createAuthProviders({
|
|
58
|
+
tree,
|
|
59
|
+
authProvidersNeeded,
|
|
60
|
+
report,
|
|
61
|
+
v2Configs: collectV2BearerAuthConfigs(tree, scope),
|
|
62
|
+
creatorToClient,
|
|
63
|
+
});
|
|
52
64
|
}
|
|
53
65
|
if (createdFiles.length > 0) {
|
|
54
66
|
console.log('\n✅ Created new query creators in:');
|
|
55
67
|
createdFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
56
68
|
}
|
|
57
69
|
};
|
|
58
|
-
export const updateLegacyCreatorImportsAndUsages = (tree) => {
|
|
70
|
+
export const updateLegacyCreatorImportsAndUsages = (tree, scope, report) => {
|
|
59
71
|
const legacyCreators = new Map();
|
|
60
72
|
const updatedFiles = [];
|
|
61
|
-
|
|
73
|
+
scope.visit(tree, (filePath) => {
|
|
62
74
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
63
75
|
return;
|
|
64
76
|
}
|
|
@@ -66,16 +78,17 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
|
|
|
66
78
|
if (!content) {
|
|
67
79
|
return;
|
|
68
80
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
legacyCreators.set(originalName, legacyName);
|
|
81
|
+
findLegacyWrappers(content).forEach((legacyName, originalName) => {
|
|
82
|
+
legacyCreators.set(originalName, { legacyName, definedIn: filePath });
|
|
72
83
|
});
|
|
73
84
|
});
|
|
74
85
|
if (legacyCreators.size === 0) {
|
|
75
86
|
return;
|
|
76
87
|
}
|
|
77
88
|
console.log(`\n✅ Found ${legacyCreators.size} legacy query creator wrappers`);
|
|
78
|
-
|
|
89
|
+
const moduleGraph = createModuleGraph(tree);
|
|
90
|
+
const pendingWrites = new Map();
|
|
91
|
+
scope.visit(tree, (filePath) => {
|
|
79
92
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
80
93
|
return;
|
|
81
94
|
}
|
|
@@ -83,18 +96,110 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
|
|
|
83
96
|
if (!content || containsLegacyWrapperDefinitions(content)) {
|
|
84
97
|
return;
|
|
85
98
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
99
|
+
const nextContent = pointFileAtLegacyWrappers({
|
|
100
|
+
content,
|
|
101
|
+
filePath,
|
|
102
|
+
legacyCreators,
|
|
103
|
+
moduleGraph,
|
|
104
|
+
report,
|
|
105
|
+
});
|
|
88
106
|
if (nextContent !== content) {
|
|
89
|
-
|
|
90
|
-
updatedFiles.push(filePath);
|
|
107
|
+
pendingWrites.set(filePath, nextContent);
|
|
91
108
|
}
|
|
92
109
|
});
|
|
110
|
+
// Writing a barrel mid-pass would hide its original re-exports from consumers visited after it.
|
|
111
|
+
pendingWrites.forEach((nextContent, filePath) => {
|
|
112
|
+
tree.write(filePath, nextContent);
|
|
113
|
+
updatedFiles.push(filePath);
|
|
114
|
+
});
|
|
93
115
|
if (updatedFiles.length > 0) {
|
|
94
116
|
console.log('\n✅ Updated legacy creator imports and usages in:');
|
|
95
117
|
updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
96
118
|
}
|
|
97
119
|
};
|
|
120
|
+
/**
|
|
121
|
+
* Rewrites this file's imports of migrated creators to the `legacy*` wrappers.
|
|
122
|
+
*
|
|
123
|
+
* Two rules keep the rewrite honest, and both exist because the first version of this pass did not
|
|
124
|
+
* have them:
|
|
125
|
+
*
|
|
126
|
+
* - An import is only touched when the module it comes from really does resolve to the file that
|
|
127
|
+
* defines the wrapper. A same-named export from an unrelated package stays as it is.
|
|
128
|
+
* - An alias is preserved (`legacyGetPerson as getPersonQuery`) instead of being dropped, so the
|
|
129
|
+
* call sites using that alias keep compiling.
|
|
130
|
+
*/
|
|
131
|
+
const pointFileAtLegacyWrappers = ({ content, filePath, legacyCreators, moduleGraph, report, }) => {
|
|
132
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
133
|
+
const replacements = [];
|
|
134
|
+
const referenceRenames = new Map();
|
|
135
|
+
const resolvesToWrapper = (specifier, importedName, registration) => moduleGraph.findDeclaringFile(filePath, specifier, importedName) === registration.definedIn;
|
|
136
|
+
const rewriteSpecifiers = (elements, moduleSpecifier) => {
|
|
137
|
+
let changed = false;
|
|
138
|
+
const nextElements = elements.map((element) => {
|
|
139
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
140
|
+
const registration = legacyCreators.get(importedName);
|
|
141
|
+
if (!registration || !resolvesToWrapper(moduleSpecifier, importedName, registration)) {
|
|
142
|
+
return element.getText(sourceFile);
|
|
143
|
+
}
|
|
144
|
+
changed = true;
|
|
145
|
+
// Aliased: only the imported half changes, so nothing in the file body has to move.
|
|
146
|
+
if (element.propertyName) {
|
|
147
|
+
return `${registration.legacyName} as ${element.name.text}`;
|
|
148
|
+
}
|
|
149
|
+
referenceRenames.set(element.name.text, registration.legacyName);
|
|
150
|
+
return registration.legacyName;
|
|
151
|
+
});
|
|
152
|
+
return changed ? nextElements : null;
|
|
153
|
+
};
|
|
154
|
+
for (const node of sourceFile.statements) {
|
|
155
|
+
if (ts.isImportDeclaration(node) &&
|
|
156
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
157
|
+
node.importClause?.namedBindings &&
|
|
158
|
+
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
159
|
+
const nextElements = rewriteSpecifiers(node.importClause.namedBindings.elements, node.moduleSpecifier.text);
|
|
160
|
+
if (nextElements) {
|
|
161
|
+
replacements.push({
|
|
162
|
+
start: node.getStart(sourceFile),
|
|
163
|
+
end: node.getEnd(),
|
|
164
|
+
replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// Re-exports need the same treatment, or a barrel keeps pointing at the v3 creator.
|
|
170
|
+
if (ts.isExportDeclaration(node) &&
|
|
171
|
+
node.moduleSpecifier &&
|
|
172
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
173
|
+
node.exportClause &&
|
|
174
|
+
ts.isNamedExports(node.exportClause)) {
|
|
175
|
+
const nextElements = rewriteSpecifiers(node.exportClause.elements, node.moduleSpecifier.text);
|
|
176
|
+
if (nextElements) {
|
|
177
|
+
replacements.push({
|
|
178
|
+
start: node.getStart(sourceFile),
|
|
179
|
+
end: node.getEnd(),
|
|
180
|
+
replacement: `export { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let result = content;
|
|
186
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
187
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
188
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
189
|
+
});
|
|
190
|
+
const renamed = renameImportedReferences(result, referenceRenames);
|
|
191
|
+
renamed.shadowed.forEach((name) => {
|
|
192
|
+
report.addManualReview({
|
|
193
|
+
title: `Point ${name} at its legacy wrapper by hand`,
|
|
194
|
+
summary: `${filePath} imports the migrated creator \`${name}\` but also declares something with that name, so the migration left its usages alone to avoid rewriting the wrong symbol.`,
|
|
195
|
+
action: `Rename the local declaration or the import alias, then point the query usages at \`${legacyCreators.get(name)?.legacyName ?? `legacy${capitalizeFirstLetter(name)}`}\`.`,
|
|
196
|
+
locations: [{ filePath }],
|
|
197
|
+
source: 'legacy-query-creator-migration',
|
|
198
|
+
dedupeKey: `shadowed-rename:${filePath}:${name}`,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
return renamed.content;
|
|
202
|
+
};
|
|
98
203
|
const toLegacyName = (name) => `legacy${capitalizeFirstLetter(name)}`;
|
|
99
204
|
const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report) => {
|
|
100
205
|
const sourceFile = createSourceFile(content, filePath);
|
|
@@ -115,6 +220,16 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
115
220
|
ts.forEachChild(node, visit);
|
|
116
221
|
return;
|
|
117
222
|
}
|
|
223
|
+
if (oldClientNames.has(objectExpression.text) && LEGACY_GQL_METHODS.includes(methodExpression.text)) {
|
|
224
|
+
report.addManualReview({
|
|
225
|
+
title: `Rewrite the GraphQL creator ${node.name.text} by hand`,
|
|
226
|
+
summary: `${node.name.text} is built with \`${objectExpression.text}.${methodExpression.text}()\`. The migration only rewrites REST creators, so this one still calls a method the migrated client no longer has.`,
|
|
227
|
+
action: `Rebuild it on the v3 client with \`${methodExpression.text === 'gqlQuery' ? 'createGqlQueryViaPost' : 'createGqlMutationViaPost'}\` (or the \`ViaGet\` variant) and a \`gql\` document, then wrap it in \`createLegacyQueryCreator\` if its call sites stay on v2. See https://ethlete-sdk-docs.web.app/query/gql.`,
|
|
228
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
229
|
+
source: 'legacy-query-creator-migration',
|
|
230
|
+
dedupeKey: `gql-creator:${filePath}:${node.name.text}`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
118
233
|
if (!oldClientNames.has(objectExpression.text) || !isLegacyHttpMethod(methodExpression.text)) {
|
|
119
234
|
ts.forEachChild(node, visit);
|
|
120
235
|
return;
|
|
@@ -128,17 +243,68 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
128
243
|
secure: false,
|
|
129
244
|
httpOptions: new Map(),
|
|
130
245
|
};
|
|
246
|
+
const warnUnreadableConfigValue = (property, passedAs) => {
|
|
247
|
+
const propertyName = property.name.getText(sourceFile);
|
|
248
|
+
report.addWarning({
|
|
249
|
+
title: `Carry over \`${propertyName}\` of ${creatorName}`,
|
|
250
|
+
summary: `${creatorName} passes \`${propertyName}\` ${passedAs} in its v2 config. The migration cannot read its value, so it was not carried over to the v3 creator.`,
|
|
251
|
+
action: propertyName === 'secure'
|
|
252
|
+
? 'If the value is true, switch the generated creator to the matching *Secure creator of the client by hand.'
|
|
253
|
+
: 'Move what it configures onto the generated creator by hand.',
|
|
254
|
+
locations: [
|
|
255
|
+
{ filePath, line: sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile)).line + 1 },
|
|
256
|
+
],
|
|
257
|
+
source: 'legacy-query-creator-migration',
|
|
258
|
+
dedupeKey: `unreadable-config:${filePath}:${creatorName}:${propertyName}`,
|
|
259
|
+
});
|
|
260
|
+
};
|
|
131
261
|
const configObject = node.initializer.arguments[0];
|
|
132
262
|
if (configObject && ts.isObjectLiteralExpression(configObject)) {
|
|
133
263
|
configObject.properties.forEach((property) => {
|
|
264
|
+
if (ts.isSpreadAssignment(property)) {
|
|
265
|
+
report.addWarning({
|
|
266
|
+
title: `Restore the spread config of ${creatorName}`,
|
|
267
|
+
summary: `${creatorName} spreads \`${property.expression.getText(sourceFile)}\` into its v2 config. The migration cannot see what it contains, so none of it was carried over to the v3 creator.`,
|
|
268
|
+
action: 'Move the route, secure flag, types and http options it provides onto the generated creator by hand.',
|
|
269
|
+
locations: [
|
|
270
|
+
{ filePath, line: sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile)).line + 1 },
|
|
271
|
+
],
|
|
272
|
+
source: 'legacy-query-creator-migration',
|
|
273
|
+
dedupeKey: `spread-config:${filePath}:${creatorName}`,
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
278
|
+
if (property.name.text === 'route') {
|
|
279
|
+
info.route = property.name.text;
|
|
280
|
+
}
|
|
281
|
+
if (HTTP_OPTION_NAMES.includes(property.name.text)) {
|
|
282
|
+
info.httpOptions.set(property.name.text, property.name.text);
|
|
283
|
+
}
|
|
284
|
+
if (property.name.text === 'entity') {
|
|
285
|
+
info.entity = property.name.text;
|
|
286
|
+
}
|
|
287
|
+
if (!['route', 'entity'].includes(property.name.text) && !HTTP_OPTION_NAMES.includes(property.name.text)) {
|
|
288
|
+
warnUnreadableConfigValue(property, 'as a shorthand property');
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
134
292
|
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
135
293
|
return;
|
|
136
294
|
}
|
|
137
295
|
if (property.name.text === 'route') {
|
|
138
296
|
info.route = property.initializer.getText(sourceFile);
|
|
139
297
|
}
|
|
140
|
-
if (property.name.text === '
|
|
141
|
-
info.
|
|
298
|
+
if (property.name.text === 'entity') {
|
|
299
|
+
info.entity = property.initializer.getText(sourceFile);
|
|
300
|
+
}
|
|
301
|
+
if (property.name.text === 'secure') {
|
|
302
|
+
if (property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
303
|
+
info.secure = true;
|
|
304
|
+
}
|
|
305
|
+
else if (property.initializer.kind !== ts.SyntaxKind.FalseKeyword) {
|
|
306
|
+
warnUnreadableConfigValue(property, 'with a value that is not a literal');
|
|
307
|
+
}
|
|
142
308
|
}
|
|
143
309
|
if (property.name.text === 'types' && ts.isObjectLiteralExpression(property.initializer)) {
|
|
144
310
|
property.initializer.properties.forEach((typeProperty) => {
|
|
@@ -173,13 +339,25 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
173
339
|
}
|
|
174
340
|
});
|
|
175
341
|
}
|
|
176
|
-
if (
|
|
342
|
+
if (HTTP_OPTION_NAMES.includes(property.name.text)) {
|
|
177
343
|
info.httpOptions.set(property.name.text, property.initializer.getText(sourceFile));
|
|
178
344
|
}
|
|
179
345
|
});
|
|
180
346
|
}
|
|
181
347
|
if (info.route) {
|
|
182
348
|
creators.push(info);
|
|
349
|
+
if (info.entity) {
|
|
350
|
+
report.addFollowUp({
|
|
351
|
+
title: `Check the entity config carried onto ${toLegacyName(creatorName)}`,
|
|
352
|
+
summary: `${creatorName} had an \`entity\` config. It was moved onto the \`${toLegacyName(creatorName)}\` interop wrapper, which still writes to the EntityStore; the v3 creator \`${creatorName}\` does not, and v3 has no EntityStore.`,
|
|
353
|
+
action: 'Keep the call sites that rely on the store on the legacy wrapper. `set` now only runs on a real response, never on `prepare()` or a failure. To leave the store, declare `invalidates` on the mutation creators that change the entity and `tags` on the reads that show it, and add `withOptimisticUpdate` where the UI must change before the response - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2#entitystore-becomes-invalidates-and-tags.',
|
|
354
|
+
locations: [
|
|
355
|
+
{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 },
|
|
356
|
+
],
|
|
357
|
+
source: 'legacy-query-creator-migration',
|
|
358
|
+
dedupeKey: `entity-config:${filePath}:${creatorName}`,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
183
361
|
}
|
|
184
362
|
}
|
|
185
363
|
ts.forEachChild(node, visit);
|
|
@@ -205,27 +383,42 @@ const transformLegacyQueryCreators = (content, legacyCreators) => {
|
|
|
205
383
|
}
|
|
206
384
|
creatorsByClient.get(creator.clientName).push(creator);
|
|
207
385
|
});
|
|
386
|
+
const wrappersByStatement = new Map();
|
|
208
387
|
legacyCreators.forEach((creator) => {
|
|
209
|
-
const nextCreator = generateNewQueryCreator(creator);
|
|
210
|
-
const legacyWrapper = generateLegacyWrapper(creator);
|
|
211
388
|
let variableStatement;
|
|
389
|
+
let declaration;
|
|
212
390
|
const visit = (node) => {
|
|
391
|
+
if (variableStatement)
|
|
392
|
+
return;
|
|
213
393
|
if (ts.isVariableStatement(node)) {
|
|
214
|
-
|
|
215
|
-
if (declaration
|
|
394
|
+
declaration = node.declarationList.declarations.find((candidate) => ts.isIdentifier(candidate.name) && candidate.name.text === creator.name);
|
|
395
|
+
if (declaration) {
|
|
216
396
|
variableStatement = node;
|
|
397
|
+
return;
|
|
217
398
|
}
|
|
218
399
|
}
|
|
219
400
|
ts.forEachChild(node, visit);
|
|
220
401
|
};
|
|
221
402
|
visit(sourceFile);
|
|
222
|
-
if (variableStatement)
|
|
403
|
+
if (!variableStatement || !declaration)
|
|
404
|
+
return;
|
|
405
|
+
const isTopLevel = ts.isSourceFile(variableStatement.parent);
|
|
406
|
+
const exportPrefix = isTopLevel ? 'export ' : '';
|
|
407
|
+
const nextCreator = `${creator.name} = ${generateNewQueryCreator(creator)}`;
|
|
408
|
+
const legacyWrapper = generateLegacyWrapper(creator, exportPrefix);
|
|
409
|
+
if (variableStatement.declarationList.declarations.length === 1) {
|
|
223
410
|
replacements.push({
|
|
224
411
|
start: variableStatement.getStart(sourceFile),
|
|
225
412
|
end: variableStatement.getEnd(),
|
|
226
|
-
replacement: `${nextCreator}
|
|
413
|
+
replacement: `${exportPrefix}const ${nextCreator};\n${legacyWrapper}`,
|
|
227
414
|
});
|
|
415
|
+
return;
|
|
228
416
|
}
|
|
417
|
+
replacements.push({ start: declaration.getStart(sourceFile), end: declaration.getEnd(), replacement: nextCreator });
|
|
418
|
+
wrappersByStatement.set(variableStatement, [...(wrappersByStatement.get(variableStatement) ?? []), legacyWrapper]);
|
|
419
|
+
});
|
|
420
|
+
wrappersByStatement.forEach((wrappers, statement) => {
|
|
421
|
+
replacements.push({ start: statement.getEnd(), end: statement.getEnd(), replacement: `\n${wrappers.join('\n')}` });
|
|
229
422
|
});
|
|
230
423
|
let result = content;
|
|
231
424
|
replacements.sort((left, right) => right.start - left.start);
|
|
@@ -233,7 +426,10 @@ const transformLegacyQueryCreators = (content, legacyCreators) => {
|
|
|
233
426
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
234
427
|
});
|
|
235
428
|
result = ensureImportFromQuery(result, ['createLegacyQueryCreator']);
|
|
236
|
-
|
|
429
|
+
result = addCreatorImports(result, creatorsByClient);
|
|
430
|
+
// The rewritten creators no longer go through `def<T>()` or the v2 client object, so whatever is
|
|
431
|
+
// left over from those constructs would land as a lint error the moment `formatFiles` runs.
|
|
432
|
+
return pruneUnusedNamedImports(result);
|
|
237
433
|
};
|
|
238
434
|
const addCreatorImports = (content, creatorsByClient) => {
|
|
239
435
|
const sourceFile = createSourceFile(content);
|
|
@@ -311,7 +507,7 @@ const generateNewQueryCreator = (creator) => {
|
|
|
311
507
|
const typeParameter = getTypeParameter(creator);
|
|
312
508
|
const options = Array.from(creator.httpOptions.entries()).map(([key, value]) => `${key}: ${value}`);
|
|
313
509
|
const optionsBlock = options.length > 0 ? `, {\n ${options.join(',\n ')}\n}` : '';
|
|
314
|
-
return
|
|
510
|
+
return `${creatorFunction}${typeParameter}(${creator.route}${optionsBlock})`;
|
|
315
511
|
};
|
|
316
512
|
const getTypeParameter = (creator) => {
|
|
317
513
|
if (creator.typeArgs && (creator.typeBody || creator.typeResponse)) {
|
|
@@ -339,10 +535,12 @@ const getTypeParameter = (creator) => {
|
|
|
339
535
|
}
|
|
340
536
|
return '';
|
|
341
537
|
};
|
|
342
|
-
const generateLegacyWrapper = (creator) => {
|
|
343
|
-
|
|
538
|
+
const generateLegacyWrapper = (creator, exportPrefix) => {
|
|
539
|
+
// `name` is what lets a `prepare()` called outside an injection context name itself in the error
|
|
540
|
+
// instead of surfacing as a bare NG0203.
|
|
541
|
+
return `/**\n * ${legacyQueryDeprecationTag(creator.name)}\n */\n${exportPrefix}const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ name: '${toLegacyName(creator.name)}', creator: ${creator.name}${creator.entity ? `, entity: ${creator.entity}` : ''} });`;
|
|
344
542
|
};
|
|
345
|
-
const createAuthProviders = (tree, authProvidersNeeded, report) => {
|
|
543
|
+
const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, creatorToClient, }) => {
|
|
346
544
|
const createdProviders = [];
|
|
347
545
|
authProvidersNeeded.forEach((filePath, clientName) => {
|
|
348
546
|
const content = tree.read(filePath, 'utf-8');
|
|
@@ -353,19 +551,54 @@ const createAuthProviders = (tree, authProvidersNeeded, report) => {
|
|
|
353
551
|
if (content.includes(`export const ${authProviderName} = createBearerAuthProvider(`)) {
|
|
354
552
|
return;
|
|
355
553
|
}
|
|
356
|
-
|
|
554
|
+
// Match on the refresh creator: it is the one thing a v2 provider config always names, and it
|
|
555
|
+
// belongs to exactly one client.
|
|
556
|
+
const v2Config = v2Configs.find((candidate) => candidate.refreshCreatorName && creatorToClient.get(candidate.refreshCreatorName) === clientName);
|
|
557
|
+
const body = renderAuthProviderBody(v2Config);
|
|
558
|
+
const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body), clientName);
|
|
357
559
|
if (nextContent === content) {
|
|
358
560
|
return;
|
|
359
561
|
}
|
|
360
562
|
tree.write(filePath, nextContent);
|
|
361
563
|
createdProviders.push(filePath);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
564
|
+
body.warnings.forEach((warning) => {
|
|
565
|
+
report.addWarning({
|
|
566
|
+
...warning,
|
|
567
|
+
locations: [{ filePath }, ...(v2Config ? [{ filePath: v2Config.filePath, line: v2Config.line }] : [])],
|
|
568
|
+
source: 'legacy-query-creator-migration',
|
|
569
|
+
dedupeKey: `auth-provider-warning:${filePath}:${authProviderName}:${warning.title}`,
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
if (body.isScaffolded) {
|
|
573
|
+
report.addFollowUp({
|
|
574
|
+
title: `Finish the scaffolded auth provider ${authProviderName}`,
|
|
575
|
+
summary: `${authProviderName} was scaffolded from the v2 BearerAuthProvider at ${v2Config?.filePath}:${v2Config?.line}. The refresh query and cookie config carried over; the adapters were copied verbatim and their parameter shapes changed between v2 and v3.`,
|
|
576
|
+
action: 'Resolve the TODO(query-v3) comments in the generated provider, add the login query via withAuthenticationQuery, and delete the v2 provider once the app logs in again.',
|
|
577
|
+
locations: [{ filePath }, ...(v2Config ? [{ filePath: v2Config.filePath, line: v2Config.line }] : [])],
|
|
578
|
+
source: 'legacy-query-creator-migration',
|
|
579
|
+
dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
report.addFollowUp({
|
|
584
|
+
title: `Configure auth queries for ${authProviderName}`,
|
|
585
|
+
summary: `The migration created ${authProviderName} with an empty \`queries\` array because no v2 BearerAuthProvider config could be matched to this client.`,
|
|
586
|
+
action: 'Add authQuery/tokenRefreshQuery builders, verify token extraction, and remove the placeholder empty array once the provider is configured.',
|
|
587
|
+
locations: [{ filePath }],
|
|
588
|
+
source: 'legacy-query-creator-migration',
|
|
589
|
+
dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
// The generated layout only works if the auth queries live next to the provider: the provider
|
|
593
|
+
// needs the login/refresh creators, and the secure creators need the provider. Leaving them in
|
|
594
|
+
// an `auth.queries.ts` that imports the client's `post` creator closes the loop.
|
|
595
|
+
report.addManualReview({
|
|
596
|
+
title: `Keep the auth queries in ${filePath}`,
|
|
597
|
+
summary: `${authProviderName} needs the login and refresh creators, and the secure creators below it need the provider. If those creators stay in a separate auth queries file that imports this client, the two files form an import cycle.`,
|
|
598
|
+
action: `Move the login/refresh creators into ${filePath} above the provider, and have the auth queries file import them back for its legacy* wrappers.`,
|
|
366
599
|
locations: [{ filePath }],
|
|
367
600
|
source: 'legacy-query-creator-migration',
|
|
368
|
-
dedupeKey: `auth-provider:${filePath}
|
|
601
|
+
dedupeKey: `auth-provider-cycle:${filePath}`,
|
|
369
602
|
});
|
|
370
603
|
});
|
|
371
604
|
if (createdProviders.length > 0) {
|
|
@@ -373,8 +606,8 @@ const createAuthProviders = (tree, authProvidersNeeded, report) => {
|
|
|
373
606
|
createdProviders.forEach((filePath) => console.log(` - ${filePath}`));
|
|
374
607
|
}
|
|
375
608
|
};
|
|
376
|
-
const addAuthProviderToFile = (content, clientName) => {
|
|
377
|
-
const nextContent = ensureImportFromQuery(content, ['createBearerAuthProvider']);
|
|
609
|
+
const addAuthProviderToFile = (content, clientName, body) => {
|
|
610
|
+
const nextContent = ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), ['toInjectFn', 'toProvideFn']);
|
|
378
611
|
const sourceFile = createSourceFile(nextContent);
|
|
379
612
|
const configName = ensureConfigSuffix(clientName);
|
|
380
613
|
const configPosition = findVariableStatementEnd(sourceFile, configName, 'createQueryClient');
|
|
@@ -388,10 +621,12 @@ const addAuthProviderToFile = (content, clientName) => {
|
|
|
388
621
|
`export const ${authProviderName} = createBearerAuthProvider({`,
|
|
389
622
|
` name: '${clientName}',`,
|
|
390
623
|
` queryClientRef: ${configName},`,
|
|
391
|
-
|
|
624
|
+
body.queries,
|
|
625
|
+
...(body.features ? [body.features] : []),
|
|
392
626
|
'});',
|
|
393
627
|
'',
|
|
394
|
-
`export const
|
|
628
|
+
`export const provide${capitalizedClientName}AuthProvider = toProvideFn(${authProviderName});`,
|
|
629
|
+
`export const inject${capitalizedClientName}AuthProvider = toInjectFn(${authProviderName});`,
|
|
395
630
|
].join('\n');
|
|
396
631
|
return nextContent.slice(0, configPosition) + authProviderBlock + nextContent.slice(configPosition);
|
|
397
632
|
};
|
|
@@ -473,117 +708,4 @@ const findLegacyWrappers = (content) => {
|
|
|
473
708
|
visit(sourceFile);
|
|
474
709
|
return wrappers;
|
|
475
710
|
};
|
|
476
|
-
const updateLegacyCreatorImports = (content, legacyCreators) => {
|
|
477
|
-
const sourceFile = createSourceFile(content);
|
|
478
|
-
const replacements = [];
|
|
479
|
-
const visit = (node) => {
|
|
480
|
-
if (ts.isImportDeclaration(node) &&
|
|
481
|
-
node.importClause?.namedBindings &&
|
|
482
|
-
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
483
|
-
const nextElements = node.importClause.namedBindings.elements.map((element) => {
|
|
484
|
-
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
485
|
-
const legacyName = legacyCreators.get(importedName);
|
|
486
|
-
return legacyName ?? element.getText(sourceFile);
|
|
487
|
-
});
|
|
488
|
-
const hasChanges = node.importClause.namedBindings.elements.some((element) => {
|
|
489
|
-
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
490
|
-
return legacyCreators.has(importedName);
|
|
491
|
-
});
|
|
492
|
-
if (hasChanges) {
|
|
493
|
-
replacements.push({
|
|
494
|
-
start: node.getStart(sourceFile),
|
|
495
|
-
end: node.getEnd(),
|
|
496
|
-
replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
ts.forEachChild(node, visit);
|
|
501
|
-
};
|
|
502
|
-
visit(sourceFile);
|
|
503
|
-
let result = content;
|
|
504
|
-
replacements.sort((left, right) => right.start - left.start);
|
|
505
|
-
replacements.forEach(({ start, end, replacement }) => {
|
|
506
|
-
result = result.slice(0, start) + replacement + result.slice(end);
|
|
507
|
-
});
|
|
508
|
-
return result;
|
|
509
|
-
};
|
|
510
|
-
const updateLegacyCreatorUsages = (content, legacyCreators) => {
|
|
511
|
-
const sourceFile = createSourceFile(content);
|
|
512
|
-
const replacements = [];
|
|
513
|
-
const visit = (node) => {
|
|
514
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
515
|
-
node.properties.forEach((property) => {
|
|
516
|
-
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) {
|
|
517
|
-
const nextName = legacyCreators.get(property.initializer.text);
|
|
518
|
-
if (nextName) {
|
|
519
|
-
replacements.push({
|
|
520
|
-
start: property.initializer.getStart(sourceFile),
|
|
521
|
-
end: property.initializer.getEnd(),
|
|
522
|
-
replacement: nextName,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (ts.isShorthandPropertyAssignment(property) && legacyCreators.has(property.name.text)) {
|
|
527
|
-
replacements.push({
|
|
528
|
-
start: property.getStart(sourceFile),
|
|
529
|
-
end: property.getEnd(),
|
|
530
|
-
replacement: `${property.name.text}: ${legacyCreators.get(property.name.text)}`,
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
if (ts.isIdentifier(node) && legacyCreators.has(node.text)) {
|
|
536
|
-
const parent = node.parent;
|
|
537
|
-
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
538
|
-
ts.forEachChild(node, visit);
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
if (ts.isPropertyAssignment(parent) && (parent.name === node || parent.initializer === node)) {
|
|
542
|
-
ts.forEachChild(node, visit);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
|
|
546
|
-
ts.forEachChild(node, visit);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
if (ts.isVariableDeclaration(parent) && parent.name === node) {
|
|
550
|
-
ts.forEachChild(node, visit);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (ts.isImportSpecifier(parent)) {
|
|
554
|
-
ts.forEachChild(node, visit);
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (ts.isFunctionDeclaration(parent) && parent.name === node) {
|
|
558
|
-
ts.forEachChild(node, visit);
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (ts.isMethodDeclaration(parent) && parent.name === node) {
|
|
562
|
-
ts.forEachChild(node, visit);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
if (ts.isParameter(parent) && parent.name === node) {
|
|
566
|
-
ts.forEachChild(node, visit);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
if (ts.isPropertyDeclaration(parent) && parent.name === node) {
|
|
570
|
-
ts.forEachChild(node, visit);
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
replacements.push({
|
|
574
|
-
start: node.getStart(sourceFile),
|
|
575
|
-
end: node.getEnd(),
|
|
576
|
-
replacement: legacyCreators.get(node.text),
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
ts.forEachChild(node, visit);
|
|
580
|
-
};
|
|
581
|
-
visit(sourceFile);
|
|
582
|
-
let result = content;
|
|
583
|
-
replacements.sort((left, right) => right.start - left.start);
|
|
584
|
-
replacements.forEach(({ start, end, replacement }) => {
|
|
585
|
-
result = result.slice(0, start) + replacement + result.slice(end);
|
|
586
|
-
});
|
|
587
|
-
return result;
|
|
588
|
-
};
|
|
589
711
|
//# sourceMappingURL=legacy-query-creator-migration.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getProjects, visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
|
+
const stripTrailingSlash = (value) => (value.endsWith('/') ? value.slice(0, -1) : value);
|
|
3
|
+
/**
|
|
4
|
+
* Limits the migration to a set of projects or paths.
|
|
5
|
+
*
|
|
6
|
+
* Rewriting every app in a monorepo in one run is rarely how adoption actually goes, and a 300-file
|
|
7
|
+
* diff is not reviewable. Note the one coupling that cannot be split: a query client and the
|
|
8
|
+
* creators built on it have to be migrated together, because the creators are rewritten in terms of
|
|
9
|
+
* the client's generated `xGet` / `xPostSecure` helpers.
|
|
10
|
+
*/
|
|
11
|
+
export const createMigrationScope = (tree, options) => {
|
|
12
|
+
const roots = [];
|
|
13
|
+
for (const projectName of options.projects ?? []) {
|
|
14
|
+
const project = getProjects(tree).get(projectName);
|
|
15
|
+
if (!project) {
|
|
16
|
+
throw new Error(`Unknown project "${projectName}". Pass names as they appear in \`nx show projects\`, or use --include with a path prefix.`);
|
|
17
|
+
}
|
|
18
|
+
roots.push(stripTrailingSlash(project.root));
|
|
19
|
+
}
|
|
20
|
+
for (const includePath of options.include ?? []) {
|
|
21
|
+
roots.push(stripTrailingSlash(includePath));
|
|
22
|
+
}
|
|
23
|
+
if (roots.length === 0) {
|
|
24
|
+
return {
|
|
25
|
+
visit: (targetTree, callback) => visitNotIgnoredFiles(targetTree, '', callback),
|
|
26
|
+
describe: () => 'the whole workspace',
|
|
27
|
+
isScoped: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
visit: (targetTree, callback) => {
|
|
32
|
+
for (const root of roots) {
|
|
33
|
+
visitNotIgnoredFiles(targetTree, root, callback);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
describe: () => roots.join(', '),
|
|
37
|
+
isScoped: true,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=migration-scope.js.map
|