@ethlete/query 6.0.0-next.62 ā 6.0.0-next.63
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 +28 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +30 -7
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +112 -29
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/migrate-from-ngrx-toolkit/args-assignability.js +55 -8
- package/generators/migrate-from-ngrx-toolkit/consumers.js +132 -36
- package/generators/migrate-from-ngrx-toolkit/creators.js +8 -4
- package/generators/migrate-from-ngrx-toolkit/feature.js +31 -4
- package/generators/migrate-from-ngrx-toolkit/migration.js +3 -1
- package/generators/migrate-from-ngrx-toolkit/module-resolution.js +23 -0
- package/generators/migrate-from-ngrx-toolkit/store-removal.js +64 -11
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +1 -1
- package/generators/migrate-to-query-v3/cleanup-migration.js +73 -12
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +54 -21
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +108 -15
- package/generators/migrate-to-query-v3/migration.js +4 -1
- package/generators/migrate-to-query-v3/module-resolution.js +131 -0
- package/generators/migrate-to-query-v3/rename-symbols.js +16 -11
- package/package.json +1 -1
- package/types/ethlete-query-ngrx-toolkit.d.ts +13 -0
- package/types/ethlete-query.d.ts +23 -4
|
@@ -349,11 +349,37 @@ export const removeFeatureStores = (tree, graph, inScope, features, report) => {
|
|
|
349
349
|
const content = tree.read(file, 'utf-8');
|
|
350
350
|
return content ? createSourceFile(content, file).statements.flatMap(declaredNames) : [];
|
|
351
351
|
};
|
|
352
|
-
|
|
352
|
+
const importClosure = (feature, roots) => {
|
|
353
|
+
const storeFiles = new Set(storeFilesOf(feature));
|
|
354
|
+
const closure = new Set(roots);
|
|
355
|
+
const queue = [...closure];
|
|
356
|
+
for (let file = queue.pop(); file; file = queue.pop()) {
|
|
357
|
+
const content = tree.read(file, 'utf-8');
|
|
358
|
+
if (!content)
|
|
359
|
+
continue;
|
|
360
|
+
for (const statement of createSourceFile(content, file).statements) {
|
|
361
|
+
if ((!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) ||
|
|
362
|
+
!statement.moduleSpecifier ||
|
|
363
|
+
!ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const resolved = graph.resolveFile(file, statement.moduleSpecifier.text);
|
|
367
|
+
if (resolved && storeFiles.has(resolved) && !closure.has(resolved)) {
|
|
368
|
+
closure.add(resolved);
|
|
369
|
+
queue.push(resolved);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return closure;
|
|
374
|
+
};
|
|
375
|
+
const converting = new Set(features);
|
|
353
376
|
let scan;
|
|
354
377
|
const keptLocations = new Map();
|
|
378
|
+
const keptFiles = new Map();
|
|
379
|
+
const keptVia = new Map();
|
|
380
|
+
const removableFilesOf = (feature) => storeFilesOf(feature).filter((file) => !keptFiles.has(file));
|
|
355
381
|
for (;;) {
|
|
356
|
-
const deletedFiles = new Set([...converting].flatMap(
|
|
382
|
+
const deletedFiles = new Set([...converting].flatMap(removableFilesOf));
|
|
357
383
|
const movedFiles = new Map([...converting].map((feature) => [feature.files.actions, feature.queriesFile]));
|
|
358
384
|
const needles = [
|
|
359
385
|
...new Set([...deletedFiles].flatMap((file) => [...declaredIn(file), file.split('/').pop().replace(/\.ts$/, '')])),
|
|
@@ -371,20 +397,34 @@ export const removeFeatureStores = (tree, graph, inScope, features, report) => {
|
|
|
371
397
|
movedFiles,
|
|
372
398
|
needles,
|
|
373
399
|
});
|
|
374
|
-
const
|
|
400
|
+
const usedOutside = new Map();
|
|
375
401
|
for (const reference of scan.references) {
|
|
376
402
|
const feature = featureOfFile.get(reference.declaringFile);
|
|
377
403
|
if (!feature || reference.target || !converting.has(feature))
|
|
378
404
|
continue;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
]);
|
|
405
|
+
const files = usedOutside.get(feature) ?? new Map();
|
|
406
|
+
const location = { filePath: reference.file, line: reference.line };
|
|
407
|
+
files.set(reference.declaringFile, [...(files.get(reference.declaringFile) ?? []), location]);
|
|
408
|
+
usedOutside.set(feature, files);
|
|
409
|
+
keptLocations.set(feature, [...(keptLocations.get(feature) ?? []), location]);
|
|
384
410
|
}
|
|
385
|
-
if (
|
|
411
|
+
if (usedOutside.size === 0)
|
|
386
412
|
break;
|
|
387
|
-
|
|
413
|
+
for (const [feature, files] of usedOutside) {
|
|
414
|
+
const closure = importClosure(feature, [...keptFiles.keys(), ...files.keys()].filter((file) => featureOfFile.get(file) === feature));
|
|
415
|
+
if (closure.has(feature.files.actions) || (feature.files.reducer && closure.has(feature.files.reducer))) {
|
|
416
|
+
converting.delete(feature);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
for (const [file, locations] of files)
|
|
420
|
+
keptFiles.set(file, [...(keptFiles.get(file) ?? []), ...locations]);
|
|
421
|
+
for (const file of closure) {
|
|
422
|
+
if (keptFiles.has(file))
|
|
423
|
+
continue;
|
|
424
|
+
keptFiles.set(file, []);
|
|
425
|
+
keptVia.set(file, [...files.keys()][0]);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
388
428
|
}
|
|
389
429
|
for (const [feature, locations] of keptLocations) {
|
|
390
430
|
if (converting.has(feature))
|
|
@@ -395,7 +435,20 @@ export const removeFeatureStores = (tree, graph, inScope, features, report) => {
|
|
|
395
435
|
locations: [{ filePath: feature.files.actions }, ...locations],
|
|
396
436
|
});
|
|
397
437
|
}
|
|
398
|
-
const
|
|
438
|
+
for (const [file, locations] of keptFiles) {
|
|
439
|
+
const feature = featureOfFile.get(file);
|
|
440
|
+
if (!converting.has(feature))
|
|
441
|
+
continue;
|
|
442
|
+
const via = keptVia.get(file);
|
|
443
|
+
report.add({
|
|
444
|
+
id: TOOLKIT_TASK.STORE_FILE_KEPT,
|
|
445
|
+
summary: via
|
|
446
|
+
? `\`${file}\` of \`${feature.base}\` is kept because the kept \`${via}\` imports it.`
|
|
447
|
+
: `\`${file}\` of \`${feature.base}\` is kept because code outside the feature uses it.`,
|
|
448
|
+
locations: [{ filePath: file }, ...locations],
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const deletedFiles = new Set([...converting].flatMap(removableFilesOf));
|
|
399
452
|
const movedFiles = new Map([...converting].map((feature) => [feature.files.actions, feature.queriesFile]));
|
|
400
453
|
const editedFiles = new Set();
|
|
401
454
|
const applyScan = (result, extraEdits) => {
|
|
@@ -176,7 +176,7 @@ export const renderAuthProviderBody = (config) => {
|
|
|
176
176
|
}
|
|
177
177
|
const refreshEntries = [` queryCreator: ${config.refreshCreatorName},`];
|
|
178
178
|
if (config.responseAdapter) {
|
|
179
|
-
refreshEntries.push(' extractTokens: (response) => {', ` const { token, refreshToken } = (${config.responseAdapter})(response);`, '', ' return { accessToken: token, refreshToken };', ' },');
|
|
179
|
+
refreshEntries.push(' extractTokens: (response) => {', ` const { token, refreshToken } = (${config.responseAdapter})(response) as { token: string; refreshToken: string };`, '', ' return { accessToken: token, refreshToken };', ' },');
|
|
180
180
|
}
|
|
181
181
|
if (config.requestArgsAdapter) {
|
|
182
182
|
refreshEntries.push(` // TODO(query-v3): v2 requestArgsAdapter took { token, refreshToken }; buildArgs takes the refresh token alone.`, ` buildArgs: ${config.requestArgsAdapter},`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { createModuleGraph } from './module-graph.js';
|
|
3
|
-
import { isShadowedInNestedScope } from './rename-symbols.js';
|
|
3
|
+
import { findUsedNames, isShadowedInNestedScope, pruneUnusedNamedImports } from './rename-symbols.js';
|
|
4
4
|
import { REMOVED_EXPERIMENTAL_QUERY_HELPERS, createSourceFile, ensureImportFromQuery, ensureNamedImports, findRemovedExperimentalQueryHelpers, getLineNumber, printImportDeclaration, } from './shared.js';
|
|
5
5
|
export const reportRemovedExperimentalQueryHelpers = (tree, scope, report) => {
|
|
6
6
|
scope.visit(tree, (filePath) => {
|
|
@@ -34,10 +34,16 @@ export const reportRemovedExperimentalQueryHelpers = (tree, scope, report) => {
|
|
|
34
34
|
export const migrateDevtoolsUsage = (tree, scope, report) => {
|
|
35
35
|
const updatedFiles = [];
|
|
36
36
|
const filesNeedingDevtoolsPackage = [];
|
|
37
|
+
let mountsFromProvider = false;
|
|
38
|
+
scope.visit(tree, (filePath) => {
|
|
39
|
+
if (filePath.endsWith('.ts') && tree.read(filePath, 'utf-8')?.includes('provideQueryClientForDevtools')) {
|
|
40
|
+
mountsFromProvider = true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
37
43
|
scope.visit(tree, (filePath) => {
|
|
38
44
|
if (filePath.endsWith('.html')) {
|
|
39
45
|
const html = tree.read(filePath, 'utf-8');
|
|
40
|
-
const nextHtml = html && selectLazyDevtools(html);
|
|
46
|
+
const nextHtml = html && (mountsFromProvider ? removeDevtoolsTags(html) : selectLazyDevtools(html));
|
|
41
47
|
if (nextHtml && nextHtml !== html) {
|
|
42
48
|
tree.write(filePath, nextHtml);
|
|
43
49
|
updatedFiles.push(filePath);
|
|
@@ -52,13 +58,14 @@ export const migrateDevtoolsUsage = (tree, scope, report) => {
|
|
|
52
58
|
(!content.includes('provideQueryClientForDevtools') && !content.includes('QueryDevtoolsComponent'))) {
|
|
53
59
|
return;
|
|
54
60
|
}
|
|
55
|
-
const { content:
|
|
61
|
+
const { content: migrated, importsComponent, mountsDevtools } = migrateDevtoolsInFile(content, mountsFromProvider);
|
|
62
|
+
const nextContent = pruneUnusedNamedImports(mountsFromProvider ? removeDevtoolsTags(migrated) : migrated, findUsedNames(content));
|
|
56
63
|
if (nextContent === content) {
|
|
57
64
|
return;
|
|
58
65
|
}
|
|
59
66
|
tree.write(filePath, nextContent);
|
|
60
67
|
updatedFiles.push(filePath);
|
|
61
|
-
if (importsComponent) {
|
|
68
|
+
if (importsComponent || mountsDevtools) {
|
|
62
69
|
filesNeedingDevtoolsPackage.push(filePath);
|
|
63
70
|
}
|
|
64
71
|
});
|
|
@@ -73,7 +80,7 @@ export const migrateDevtoolsUsage = (tree, scope, report) => {
|
|
|
73
80
|
// `@ethlete/query-devtools`.
|
|
74
81
|
report.addManualReview({
|
|
75
82
|
title: 'Add @ethlete/query-devtools for the query devtools',
|
|
76
|
-
summary:
|
|
83
|
+
summary: "`provideQueryDevtools()` now mounts the devtools through `ui: () => import('@ethlete/query-devtools/lazy')`, so `<et-query-devtools>` and its component import were removed. Where no devtools provider was found, `QueryDevtoolsLazyComponent` and `<et-query-devtools-lazy>` replace them. The package may not be a dependency of these projects yet.",
|
|
77
84
|
action: 'Run `yarn add @ethlete/query-devtools` where needed, then `yarn install` and re-lint the affected libs so the `@nx/dependency-checks` rule sees the new dependency.',
|
|
78
85
|
locations: filesNeedingDevtoolsPackage.map((filePath) => ({ filePath })),
|
|
79
86
|
source: 'cleanup-migration',
|
|
@@ -122,6 +129,27 @@ export const reportLeftoverV2Clients = (tree, scope, report) => {
|
|
|
122
129
|
});
|
|
123
130
|
});
|
|
124
131
|
};
|
|
132
|
+
export const reportLeftoverLegacyDevtools = (tree, scope, report) => {
|
|
133
|
+
scope.visit(tree, (filePath) => {
|
|
134
|
+
if (filePath.endsWith('.spec.ts') || (!filePath.endsWith('.ts') && !filePath.endsWith('.html'))) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const content = tree.read(filePath, 'utf-8');
|
|
138
|
+
const index = content?.search(/<et-query-devtools(?![\w-])|\b(QueryDevtoolsComponent|provideQueryClientForDevtools)\b/);
|
|
139
|
+
if (!content || index === undefined || index < 0) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const line = content.slice(0, index).split('\n').length;
|
|
143
|
+
report.addManualReview({
|
|
144
|
+
title: 'Migrator error: legacy devtools usage is left',
|
|
145
|
+
summary: 'The migration should leave no legacy `<et-query-devtools>`, `QueryDevtoolsComponent` or `provideQueryClientForDevtools` behind, so it missed this one. Report this case as a migrator bug.',
|
|
146
|
+
action: 'Switch it by hand to `provideQueryDevtools()` and `QueryDevtoolsLazyComponent` from `@ethlete/query-devtools/lazy`, then report the shape the migration did not handle.',
|
|
147
|
+
locations: [{ filePath, line }],
|
|
148
|
+
source: 'cleanup-migration',
|
|
149
|
+
dedupeKey: `leftover-legacy-devtools:${filePath}:${line}`,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
};
|
|
125
153
|
export const migrateEmptyPrepareCalls = (tree, scope, report) => {
|
|
126
154
|
const updatedFiles = [];
|
|
127
155
|
const graph = createModuleGraph(tree);
|
|
@@ -154,7 +182,32 @@ export const migrateEmptyPrepareCalls = (tree, scope, report) => {
|
|
|
154
182
|
}
|
|
155
183
|
};
|
|
156
184
|
const LAZY_DEVTOOLS_COMPONENT = 'QueryDevtoolsLazyComponent';
|
|
185
|
+
const DEVTOOLS_UI_PROVIDER_CALL = "provideQueryDevtools({ ui: () => import('@ethlete/query-devtools/lazy') })";
|
|
157
186
|
const selectLazyDevtools = (content) => content.replace(/(<\/?)et-query-devtools(?![\w-])/g, '$1et-query-devtools-lazy');
|
|
187
|
+
const removeDevtoolsTags = (content) => content.replace(/[ \t]*<et-query-devtools(?![\w-])[^>]*?(?:\/>|>\s*<\/et-query-devtools>)[ \t]*\n?/g, '');
|
|
188
|
+
const removeFromImportsArrays = (content, names) => {
|
|
189
|
+
const sourceFile = createSourceFile(content);
|
|
190
|
+
const ranges = [];
|
|
191
|
+
const visit = (node) => {
|
|
192
|
+
if (ts.isPropertyAssignment(node) &&
|
|
193
|
+
ts.isIdentifier(node.name) &&
|
|
194
|
+
node.name.text === 'imports' &&
|
|
195
|
+
ts.isArrayLiteralExpression(node.initializer)) {
|
|
196
|
+
node.initializer.elements
|
|
197
|
+
.filter((element) => ts.isIdentifier(element) && names.has(element.text))
|
|
198
|
+
.forEach((element) => {
|
|
199
|
+
const range = withSurroundingComma(content, element.getStart(sourceFile), element.getEnd());
|
|
200
|
+
const padding = content[range.start - 1] === '[' ? (/^[ \t]*/.exec(content.slice(range.end))?.[0] ?? '') : '';
|
|
201
|
+
ranges.push({ start: range.start, end: range.end + padding.length });
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
ts.forEachChild(node, visit);
|
|
205
|
+
};
|
|
206
|
+
visit(sourceFile);
|
|
207
|
+
return ranges
|
|
208
|
+
.sort((left, right) => right.start - left.start)
|
|
209
|
+
.reduce((result, { start, end }) => result.slice(0, start) + result.slice(end), content);
|
|
210
|
+
};
|
|
158
211
|
const renameIdentifiers = (content, names, replacement) => {
|
|
159
212
|
const sourceFile = createSourceFile(content);
|
|
160
213
|
const ranges = [];
|
|
@@ -172,19 +225,23 @@ const renameIdentifiers = (content, names, replacement) => {
|
|
|
172
225
|
.sort((left, right) => right.start - left.start)
|
|
173
226
|
.reduce((result, { start, end }) => result.slice(0, start) + replacement + result.slice(end), content);
|
|
174
227
|
};
|
|
175
|
-
const migrateDevtoolsInFile = (content) => {
|
|
228
|
+
const migrateDevtoolsInFile = (content, mountsFromProvider) => {
|
|
176
229
|
const componentNames = localNamesImportedFromQuery(content, 'QueryDevtoolsComponent');
|
|
177
230
|
const providerNames = new Set([
|
|
178
231
|
'provideQueryClientForDevtools',
|
|
179
232
|
...localNamesImportedFromQuery(content, 'provideQueryClientForDevtools'),
|
|
180
233
|
]);
|
|
181
|
-
|
|
182
|
-
|
|
234
|
+
const providerCall = mountsFromProvider ? DEVTOOLS_UI_PROVIDER_CALL : 'provideQueryDevtools()';
|
|
235
|
+
let result = replaceDevtoolsProviderCalls(content, providerNames, providerCall);
|
|
236
|
+
const hasProvider = result.includes(providerCall);
|
|
183
237
|
result = dropLegacyDevtoolsImports(result);
|
|
184
238
|
if (hasProvider) {
|
|
185
239
|
result = ensureImportFromQuery(result, ['provideQueryDevtools']);
|
|
186
240
|
}
|
|
187
|
-
if (componentNames.length > 0) {
|
|
241
|
+
if (componentNames.length > 0 && mountsFromProvider) {
|
|
242
|
+
result = removeFromImportsArrays(result, new Set(componentNames));
|
|
243
|
+
}
|
|
244
|
+
else if (componentNames.length > 0) {
|
|
188
245
|
result = selectLazyDevtools(renameIdentifiers(result, new Set(componentNames), LAZY_DEVTOOLS_COMPONENT));
|
|
189
246
|
result = ensureNamedImports({
|
|
190
247
|
content: result,
|
|
@@ -192,7 +249,11 @@ const migrateDevtoolsInFile = (content) => {
|
|
|
192
249
|
moduleSpecifier: '@ethlete/query-devtools/lazy',
|
|
193
250
|
});
|
|
194
251
|
}
|
|
195
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
content: result,
|
|
254
|
+
importsComponent: componentNames.length > 0 && !mountsFromProvider,
|
|
255
|
+
mountsDevtools: hasProvider && mountsFromProvider,
|
|
256
|
+
};
|
|
196
257
|
};
|
|
197
258
|
const queryImports = (sourceFile) => sourceFile.statements.filter((node) => ts.isImportDeclaration(node) &&
|
|
198
259
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
@@ -212,7 +273,7 @@ const localNamesImportedFromQuery = (content, name) => queryImports(createSource
|
|
|
212
273
|
* first call site keeps its position - it is already where the app wanted its devtools - and the
|
|
213
274
|
* rest are removed along with the comma that separated them.
|
|
214
275
|
*/
|
|
215
|
-
const replaceDevtoolsProviderCalls = (content, providerNames) => {
|
|
276
|
+
const replaceDevtoolsProviderCalls = (content, providerNames, providerCall) => {
|
|
216
277
|
const sourceFile = createSourceFile(content);
|
|
217
278
|
const calls = [];
|
|
218
279
|
const visit = (node) => {
|
|
@@ -229,7 +290,7 @@ const replaceDevtoolsProviderCalls = (content, providerNames) => {
|
|
|
229
290
|
const start = call.getStart(sourceFile);
|
|
230
291
|
const end = call.getEnd();
|
|
231
292
|
if (index === 0) {
|
|
232
|
-
return { start, end, replacement:
|
|
293
|
+
return { start, end, replacement: providerCall };
|
|
233
294
|
}
|
|
234
295
|
return { ...withSurroundingComma(content, start, end), replacement: '' };
|
|
235
296
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { findUsedNames, pruneUnusedNamedImports } from './rename-symbols.js';
|
|
2
3
|
import { createSourceFile, ensureAngularCoreImports, ensureImportFromQuery, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
|
|
3
4
|
/** The creator methods that resolve an injector, and so throw `ET950` when called without one. */
|
|
4
5
|
const INJECTOR_METHODS = new Set(['prepare', 'createSubject', 'createSignal', 'behaviorSubject']);
|
|
@@ -412,12 +413,11 @@ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo, s
|
|
|
412
413
|
}
|
|
413
414
|
});
|
|
414
415
|
filesByPath.forEach((classes, filePath) => {
|
|
415
|
-
|
|
416
|
+
const content = tree.read(filePath, 'utf-8');
|
|
416
417
|
if (!content) {
|
|
417
418
|
return;
|
|
418
419
|
}
|
|
419
|
-
|
|
420
|
-
const nextContent = transformPrepareCallsInFile(content, classes, pollingInfo);
|
|
420
|
+
const nextContent = pruneUnusedNamedImports(transformPrepareCallsInFile(ensureAngularCoreImports(content, ['inject', 'Injector']), classes, pollingInfo), new Set([...findUsedNames(content), 'inject', 'Injector']));
|
|
421
421
|
if (nextContent !== content) {
|
|
422
422
|
tree.write(filePath, nextContent);
|
|
423
423
|
updatedFiles.push(filePath);
|
|
@@ -493,15 +493,22 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
|
|
|
493
493
|
else if (containingFunction) {
|
|
494
494
|
const functionWithInject = findOutermostFunctionWithInject(containingFunction);
|
|
495
495
|
if (!functionWithInject) {
|
|
496
|
+
if (propertyAccess.name.text === 'prepare' && isInsideAsyncValidator(node)) {
|
|
497
|
+
replacements.push({
|
|
498
|
+
start: node.getStart(sourceFile),
|
|
499
|
+
end: node.getEnd(),
|
|
500
|
+
replacement: transformSinglePrepareCall(node, undefined, sourceFile, pollingInfo),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
496
503
|
ts.forEachChild(node, visit);
|
|
497
504
|
return;
|
|
498
505
|
}
|
|
499
506
|
const functionPosition = functionWithInject.getStart(sourceFile);
|
|
500
507
|
if (!functionsNeedingInjector.has(functionPosition)) {
|
|
501
|
-
const
|
|
502
|
-
if (
|
|
508
|
+
const existingInjector = findExistingInjector(functionWithInject);
|
|
509
|
+
if (existingInjector) {
|
|
503
510
|
functionsNeedingInjector.set(functionPosition, {
|
|
504
|
-
injectorName:
|
|
511
|
+
injectorName: existingInjector,
|
|
505
512
|
bodyStart: -1,
|
|
506
513
|
needsDeclaration: false,
|
|
507
514
|
});
|
|
@@ -561,7 +568,7 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
|
|
|
561
568
|
});
|
|
562
569
|
return rewrittenComputeds.size > 0 ? ensureImportFromQuery(result, ['queryComputed']) : result;
|
|
563
570
|
};
|
|
564
|
-
const
|
|
571
|
+
const findExistingInjector = (func) => {
|
|
565
572
|
for (const parameter of func.parameters) {
|
|
566
573
|
if (!ts.isIdentifier(parameter.name)) {
|
|
567
574
|
continue;
|
|
@@ -573,6 +580,23 @@ const findInjectorParameter = (func) => {
|
|
|
573
580
|
return parameter.name.text;
|
|
574
581
|
}
|
|
575
582
|
}
|
|
583
|
+
if (!func.body || !ts.isBlock(func.body))
|
|
584
|
+
return undefined;
|
|
585
|
+
for (const statement of func.body.statements) {
|
|
586
|
+
if (!ts.isVariableStatement(statement))
|
|
587
|
+
continue;
|
|
588
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
589
|
+
const initializer = declaration.initializer;
|
|
590
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
591
|
+
initializer &&
|
|
592
|
+
ts.isCallExpression(initializer) &&
|
|
593
|
+
ts.isIdentifier(initializer.expression) &&
|
|
594
|
+
initializer.expression.text === 'inject' &&
|
|
595
|
+
initializer.arguments[0]?.getText() === 'Injector') {
|
|
596
|
+
return declaration.name.text;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
576
600
|
return undefined;
|
|
577
601
|
};
|
|
578
602
|
const getFunctionBodyStart = (func, sourceFile) => {
|
|
@@ -676,7 +700,8 @@ const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pol
|
|
|
676
700
|
spreadExpression = existingArgument.getText(sourceFile);
|
|
677
701
|
}
|
|
678
702
|
}
|
|
679
|
-
|
|
703
|
+
if (injectorReference)
|
|
704
|
+
nextArgumentProperties.push(`injector: ${injectorReference}`);
|
|
680
705
|
const queryVariableName = findQueryVariableNameForPrepareCall(callNode, sourceFile);
|
|
681
706
|
const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo, isDiscardedPrepareCall(callNode), isOneShotLocalVariable(callNode) || isInsideAsyncValidator(callNode));
|
|
682
707
|
if (existingConfig && ts.isObjectLiteralExpression(existingConfig.initializer)) {
|
|
@@ -984,22 +1009,30 @@ const getPlainComputedHost = (node) => {
|
|
|
984
1009
|
const ASYNC_VALIDATOR_PROPERTIES = new Set(['asyncValidator', 'asyncValidators']);
|
|
985
1010
|
const ASYNC_VALIDATOR_SETTERS = new Set(['setAsyncValidators', 'addAsyncValidators']);
|
|
986
1011
|
/**
|
|
987
|
-
* Whether
|
|
988
|
-
*
|
|
1012
|
+
* Whether a function around the call is an Angular async validator: a value of an `asyncValidator(s)` property or
|
|
1013
|
+
* setter argument, an `AsyncValidatorFn` or a factory of one, a function returning `Observable`/`Promise` of
|
|
1014
|
+
* `ValidationErrors`, or `validate` of an `AsyncValidator`.
|
|
989
1015
|
*/
|
|
990
1016
|
const isInsideAsyncValidator = (callNode) => {
|
|
991
|
-
let
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
if (ts.isMethodDeclaration(fn)) {
|
|
997
|
-
const owner = fn.parent;
|
|
998
|
-
return (ts.isIdentifier(fn.name) &&
|
|
999
|
-
fn.name.text === 'validate' &&
|
|
1000
|
-
ts.isClassDeclaration(owner) &&
|
|
1001
|
-
!!owner.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText() === 'AsyncValidator')));
|
|
1017
|
+
for (let node = callNode.parent; node; node = node.parent) {
|
|
1018
|
+
if (ts.isMethodDeclaration(node))
|
|
1019
|
+
return isAsyncValidatorMethod(node);
|
|
1020
|
+
if (isCallbackFunction(node) && isAsyncValidatorFunction(node))
|
|
1021
|
+
return true;
|
|
1002
1022
|
}
|
|
1023
|
+
return false;
|
|
1024
|
+
};
|
|
1025
|
+
const isAsyncValidatorMethod = (method) => {
|
|
1026
|
+
const owner = method.parent;
|
|
1027
|
+
return (ts.isIdentifier(method.name) &&
|
|
1028
|
+
method.name.text === 'validate' &&
|
|
1029
|
+
ts.isClassDeclaration(owner) &&
|
|
1030
|
+
!!owner.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText() === 'AsyncValidator')));
|
|
1031
|
+
};
|
|
1032
|
+
const ASYNC_VALIDATOR_SIGNATURE = /\bAsyncValidatorFn\b|\b(?:Observable|Promise)<\s*ValidationErrors\b/;
|
|
1033
|
+
const isAsyncValidatorFunction = (fn) => {
|
|
1034
|
+
if (fn.type && ASYNC_VALIDATOR_SIGNATURE.test(fn.type.getText()))
|
|
1035
|
+
return true;
|
|
1003
1036
|
let value = fn;
|
|
1004
1037
|
if (ts.isArrayLiteralExpression(value.parent))
|
|
1005
1038
|
value = value.parent;
|
|
@@ -210,9 +210,13 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report,
|
|
|
210
210
|
const sourceFile = createSourceFile(content, filePath);
|
|
211
211
|
const creators = [];
|
|
212
212
|
const oldClientNames = new Set();
|
|
213
|
+
const renamedOwnClients = new Map();
|
|
213
214
|
queryClientFiles.forEach((configNames) => {
|
|
214
215
|
configNames.forEach((configName) => oldClientNames.add(configName));
|
|
215
216
|
});
|
|
217
|
+
queryClientFiles.get(filePath)?.forEach((configName) => {
|
|
218
|
+
renamedOwnClients.set(ensureConfigSuffix(configName), configName);
|
|
219
|
+
});
|
|
216
220
|
const visit = (node) => {
|
|
217
221
|
if (ts.isVariableDeclaration(node) &&
|
|
218
222
|
ts.isIdentifier(node.name) &&
|
|
@@ -225,27 +229,15 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report,
|
|
|
225
229
|
ts.forEachChild(node, visit);
|
|
226
230
|
return;
|
|
227
231
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const viaGet = isQuery && gqlGetClients.has(objectExpression.text);
|
|
231
|
-
const creator = isQuery ? `createGqlQueryVia${viaGet ? 'Get' : 'Post'}` : 'createGqlMutationViaPost';
|
|
232
|
-
report.addManualReview({
|
|
233
|
-
title: `Rewrite the GraphQL creator ${node.name.text} by hand`,
|
|
234
|
-
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.${viaGet ? " The v2 client sent GraphQL queries with `request.gql.transferVia: 'GET'`, which v3 sets per creator." : ''}`,
|
|
235
|
-
action: `Rebuild it on the v3 client with \`${creator}\` 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.`,
|
|
236
|
-
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
237
|
-
source: 'legacy-query-creator-migration',
|
|
238
|
-
dedupeKey: `gql-creator:${filePath}:${node.name.text}`,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
if (!oldClientNames.has(objectExpression.text) || !isLegacyHttpMethod(methodExpression.text)) {
|
|
232
|
+
const clientName = renamedOwnClients.get(objectExpression.text) ?? objectExpression.text;
|
|
233
|
+
if (!oldClientNames.has(clientName) || !isLegacyHttpMethod(methodExpression.text)) {
|
|
242
234
|
ts.forEachChild(node, visit);
|
|
243
235
|
return;
|
|
244
236
|
}
|
|
245
237
|
const creatorName = node.name.text;
|
|
246
238
|
const info = {
|
|
247
239
|
name: creatorName,
|
|
248
|
-
clientName
|
|
240
|
+
clientName,
|
|
249
241
|
method: methodExpression.text,
|
|
250
242
|
route: '',
|
|
251
243
|
secure: false,
|
|
@@ -371,8 +363,109 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report,
|
|
|
371
363
|
ts.forEachChild(node, visit);
|
|
372
364
|
};
|
|
373
365
|
visit(sourceFile);
|
|
366
|
+
reportLegacyGqlCalls(sourceFile, filePath, oldClientNames, gqlGetClients, report);
|
|
374
367
|
return creators;
|
|
375
368
|
};
|
|
369
|
+
const collectClientAliases = (sourceFile, oldClientNames) => {
|
|
370
|
+
const aliases = new Map();
|
|
371
|
+
const resolve = (expression) => {
|
|
372
|
+
if (!ts.isIdentifier(expression)) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
return aliases.get(expression.text) ?? (oldClientNames.has(expression.text) ? expression.text : undefined);
|
|
376
|
+
};
|
|
377
|
+
const visitImports = (node) => {
|
|
378
|
+
if (ts.isImportSpecifier(node) && node.propertyName && oldClientNames.has(node.propertyName.text)) {
|
|
379
|
+
aliases.set(node.name.text, node.propertyName.text);
|
|
380
|
+
}
|
|
381
|
+
ts.forEachChild(node, visitImports);
|
|
382
|
+
};
|
|
383
|
+
visitImports(sourceFile);
|
|
384
|
+
const aliasDeclarations = [];
|
|
385
|
+
const collectDeclarations = (node) => {
|
|
386
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
387
|
+
aliasDeclarations.push({ name: node.name.text, initializer: node.initializer });
|
|
388
|
+
}
|
|
389
|
+
ts.forEachChild(node, collectDeclarations);
|
|
390
|
+
};
|
|
391
|
+
collectDeclarations(sourceFile);
|
|
392
|
+
const addAlias = ({ name, initializer }) => {
|
|
393
|
+
const clientName = resolve(initializer);
|
|
394
|
+
if (!clientName || clientName === name || aliases.has(name)) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
aliases.set(name, clientName);
|
|
398
|
+
return true;
|
|
399
|
+
};
|
|
400
|
+
while (aliasDeclarations.filter(addAlias).length > 0) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
return resolve;
|
|
404
|
+
};
|
|
405
|
+
const describeGqlCall = (call, sourceFile) => {
|
|
406
|
+
const path = [];
|
|
407
|
+
let node = call;
|
|
408
|
+
while (node.parent && !ts.isSourceFile(node.parent)) {
|
|
409
|
+
node = node.parent;
|
|
410
|
+
if (ts.isPropertyAssignment(node)) {
|
|
411
|
+
path.unshift(node.name.getText(sourceFile));
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node)) {
|
|
415
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
416
|
+
path.unshift(node.name.text);
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
if (ts.isPropertyDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
421
|
+
path.unshift(node.name.getText(sourceFile));
|
|
422
|
+
if (ts.isClassLike(node.parent) && node.parent.name) {
|
|
423
|
+
path.unshift(node.parent.name.text);
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return path.length > 0
|
|
429
|
+
? path.join('.')
|
|
430
|
+
: `at line ${sourceFile.getLineAndCharacterOfPosition(call.getStart(sourceFile)).line + 1}`;
|
|
431
|
+
};
|
|
432
|
+
const reportLegacyGqlCalls = (sourceFile, filePath, oldClientNames, gqlGetClients, report) => {
|
|
433
|
+
const resolveClient = collectClientAliases(sourceFile, oldClientNames);
|
|
434
|
+
const visit = (node) => {
|
|
435
|
+
if (ts.isCallExpression(node) &&
|
|
436
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
437
|
+
LEGACY_GQL_METHODS.includes(node.expression.name.text)) {
|
|
438
|
+
const receiver = node.expression.expression;
|
|
439
|
+
const receiverText = receiver.getText(sourceFile);
|
|
440
|
+
const method = node.expression.name.text;
|
|
441
|
+
const clientName = resolveClient(receiver);
|
|
442
|
+
const name = describeGqlCall(node, sourceFile);
|
|
443
|
+
const isQuery = method === 'gqlQuery';
|
|
444
|
+
const viaGet = isQuery && !!clientName && gqlGetClients.has(clientName);
|
|
445
|
+
const creator = !isQuery
|
|
446
|
+
? '`createGqlMutationViaPost`'
|
|
447
|
+
: viaGet
|
|
448
|
+
? '`createGqlQueryViaGet`'
|
|
449
|
+
: clientName || gqlGetClients.size === 0
|
|
450
|
+
? '`createGqlQueryViaPost`'
|
|
451
|
+
: "`createGqlQueryViaPost` (or `createGqlQueryViaGet` if its client set `request.gql.transferVia: 'GET'`)";
|
|
452
|
+
const builtWith = clientName
|
|
453
|
+
? `\`${receiverText}.${method}()\``
|
|
454
|
+
: `\`${receiverText}.${method}()\`, which the migration takes for a v2 query client`;
|
|
455
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
456
|
+
report.addManualReview({
|
|
457
|
+
title: `Rewrite the GraphQL creator ${name} by hand`,
|
|
458
|
+
summary: `${name} is built with ${builtWith}. The migration only rewrites REST creators, so this one still calls a method the migrated client no longer has, and the callback parameters in its config lose their types: expect implicit-any errors (TS7006) there until it is rebuilt.${viaGet ? " The v2 client sent GraphQL queries with `request.gql.transferVia: 'GET'`, which v3 sets per creator." : ''}`,
|
|
459
|
+
action: `Rebuild it on the v3 client with ${creator} 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.`,
|
|
460
|
+
locations: [{ filePath, line }],
|
|
461
|
+
source: 'legacy-query-creator-migration',
|
|
462
|
+
dedupeKey: `gql-creator:${filePath}:${name}:${line}`,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
ts.forEachChild(node, visit);
|
|
466
|
+
};
|
|
467
|
+
visit(sourceFile);
|
|
468
|
+
};
|
|
376
469
|
const extractTypeFromDef = (node, sourceFile) => {
|
|
377
470
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'def') {
|
|
378
471
|
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { formatFiles } from '@nx/devkit';
|
|
2
2
|
import { reportAuthConsumers } from './auth-consumers-migration.js';
|
|
3
|
-
import { migrateDevtoolsUsage, reportLeftoverV2Clients, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy, reportRemovedExperimentalQueryHelpers, } from './cleanup-migration.js';
|
|
3
|
+
import { migrateDevtoolsUsage, reportLeftoverLegacyDevtools, reportLeftoverV2Clients, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy, reportRemovedExperimentalQueryHelpers, } from './cleanup-migration.js';
|
|
4
4
|
import { reportDefaultHeaderUsages, reportMissingHttpClientProviders } from './http-client-check.js';
|
|
5
5
|
import { migrateLegacyPrepareCalls } from './legacy-prepare-migration.js';
|
|
6
6
|
import { createNewQueryCreators, updateLegacyCreatorImportsAndUsages } from './legacy-query-creator-migration.js';
|
|
7
7
|
import { createMigrationScope } from './migration-scope.js';
|
|
8
8
|
import { generateProviderAliases, generateQueryCreators, migrateQueryClients, updateImportsAcrossWorkspace, } from './query-client-migration.js';
|
|
9
|
+
import { reportLegacyModuleResolution } from './module-resolution.js';
|
|
9
10
|
import { QueryV3MigrationReport } from './report.js';
|
|
10
11
|
export default async function migrate(tree, schema) {
|
|
11
12
|
console.log('\nš Starting query v3 migration...');
|
|
@@ -28,11 +29,13 @@ export default async function migrate(tree, schema) {
|
|
|
28
29
|
migrateLegacyPrepareCalls(tree, report, scope);
|
|
29
30
|
reportRemovedExperimentalQueryHelpers(tree, scope, report);
|
|
30
31
|
reportLeftoverV2Clients(tree, scope, report);
|
|
32
|
+
reportLeftoverLegacyDevtools(tree, scope, report);
|
|
31
33
|
reportAuthConsumers(tree, scope, report);
|
|
32
34
|
if (queryClientFiles.size > 0) {
|
|
33
35
|
reportMissingHttpClientProviders(tree, report, scope);
|
|
34
36
|
reportDefaultHeaderUsages(tree, report, scope);
|
|
35
37
|
}
|
|
38
|
+
reportLegacyModuleResolution(tree, scope, report);
|
|
36
39
|
report.writeToTree(tree, scope.isScoped ? scope.describe() : undefined);
|
|
37
40
|
report.printSummary();
|
|
38
41
|
if (!schema.skipFormat) {
|