@ethlete/query 6.0.0-next.61 → 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 +60 -0
- package/fesm2022/ethlete-query-devtools-contract.mjs +1 -1
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +30 -7
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +183 -43
- 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 +133 -37
- 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-consumers-migration.js +64 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +93 -1
- package/generators/migrate-to-query-v3/cleanup-migration.js +135 -15
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +275 -27
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +171 -25
- package/generators/migrate-to-query-v3/migration.js +9 -3
- package/generators/migrate-to-query-v3/module-resolution.js +131 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +82 -14
- package/generators/migrate-to-query-v3/rename-symbols.js +16 -11
- package/generators/move-devtools-contract-imports/moved-names.js +1 -0
- package/migrations.json +10 -0
- package/package.json +1 -1
- package/types/ethlete-query-devtools-contract.d.ts +2 -2
- package/types/ethlete-query-ngrx-toolkit.d.ts +13 -0
- package/types/ethlete-query.d.ts +40 -6
|
@@ -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) => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { isInterceptor } from '../migrate-from-ngrx-toolkit/consumers.js';
|
|
3
|
+
import { createSourceFile } from './shared.js';
|
|
4
|
+
const V2_AUTH_MEMBERS = new Set(['setAuthProvider', 'clearAuthProvider', 'authProvider$', 'isBearerAuthProvider']);
|
|
5
|
+
const lineOf = (sourceFile, position) => sourceFile.getLineAndCharacterOfPosition(position).line + 1;
|
|
6
|
+
/**
|
|
7
|
+
* Reports the code around a v2 auth setup that the migration does not rewrite: every use of the v2
|
|
8
|
+
* auth provider hooks, and every `HttpInterceptor` that sets an `Authorization` header.
|
|
9
|
+
*
|
|
10
|
+
* A v3 query already sends its bearer token, so an interceptor that adds one too sends it twice.
|
|
11
|
+
*/
|
|
12
|
+
export const reportAuthConsumers = (tree, scope, report) => {
|
|
13
|
+
scope.visit(tree, (filePath) => {
|
|
14
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const content = tree.read(filePath, 'utf-8');
|
|
18
|
+
if (!content) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const hasAuthMember = [...V2_AUTH_MEMBERS].some((name) => content.includes(name));
|
|
22
|
+
const hasInterceptor = /HttpInterceptor/.test(content) && /authorization|bearer/i.test(content);
|
|
23
|
+
if (!hasAuthMember && !hasInterceptor) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
27
|
+
const members = new Map();
|
|
28
|
+
const visit = (node) => {
|
|
29
|
+
if (isInterceptor(node, sourceFile) && /authorization|bearer/i.test(node.getText(sourceFile))) {
|
|
30
|
+
const name = node.name?.getText(sourceFile) ?? 'interceptor';
|
|
31
|
+
report.addManualReview({
|
|
32
|
+
title: `Check the Authorization interceptor ${name}`,
|
|
33
|
+
summary: `\`${name}\` adds an \`Authorization\` header. A v3 query sends its own bearer token, so a request from a migrated query that passes through this interceptor carries the header twice.`,
|
|
34
|
+
action: 'Skip the requests of migrated clients in the interceptor, or remove the interceptor if the auth provider now covers every request.',
|
|
35
|
+
locations: [{ filePath, line: lineOf(sourceFile, node.getStart(sourceFile)) }],
|
|
36
|
+
source: 'auth-consumers-migration',
|
|
37
|
+
dedupeKey: `auth-interceptor:${filePath}:${name}`,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const memberName = ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)
|
|
42
|
+
? node.name.text
|
|
43
|
+
: ts.isIdentifier(node) && ts.isCallExpression(node.parent) && node.parent.expression === node
|
|
44
|
+
? node.text
|
|
45
|
+
: undefined;
|
|
46
|
+
if (memberName && V2_AUTH_MEMBERS.has(memberName) && !members.has(memberName)) {
|
|
47
|
+
members.set(memberName, lineOf(sourceFile, node.getStart(sourceFile)));
|
|
48
|
+
}
|
|
49
|
+
ts.forEachChild(node, visit);
|
|
50
|
+
};
|
|
51
|
+
visit(sourceFile);
|
|
52
|
+
members.forEach((line, name) => {
|
|
53
|
+
report.addManualReview({
|
|
54
|
+
title: `Replace the v2 auth hook ${name}`,
|
|
55
|
+
summary: `\`${name}\` belongs to the v2 bearer auth setup. The migration leaves the call site as it is, and a v3 auth provider has no member of this name.`,
|
|
56
|
+
action: 'Use the v3 auth provider of the client instead: inject it with its inject function and call its methods (see the auth guide).',
|
|
57
|
+
locations: [{ filePath, line }],
|
|
58
|
+
source: 'auth-consumers-migration',
|
|
59
|
+
dedupeKey: `v2-auth-hook:${filePath}:${name}`,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=auth-consumers-migration.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dirname, join, relative } from 'node:path/posix';
|
|
1
2
|
import * as ts from 'typescript';
|
|
2
3
|
import { createSourceFile } from './shared.js';
|
|
3
4
|
const REFRESH_CONFIG_KEYS = new Set([
|
|
@@ -43,7 +44,10 @@ const readConfig = (configArgument, sourceFile, filePath, node) => {
|
|
|
43
44
|
const config = {
|
|
44
45
|
filePath,
|
|
45
46
|
line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
|
|
47
|
+
copiedImports: [],
|
|
48
|
+
localNames: [],
|
|
46
49
|
};
|
|
50
|
+
const copiedNodes = [];
|
|
47
51
|
const refreshConfig = configArgument.properties.find((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'refreshConfig');
|
|
48
52
|
if (!refreshConfig ||
|
|
49
53
|
!ts.isPropertyAssignment(refreshConfig) ||
|
|
@@ -68,9 +72,88 @@ const readConfig = (configArgument, sourceFile, filePath, node) => {
|
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
74
|
config[key] = text;
|
|
75
|
+
copiedNodes.push(value);
|
|
71
76
|
});
|
|
77
|
+
resolveCopiedNames(config, copiedNodes, sourceFile);
|
|
72
78
|
return config;
|
|
73
79
|
};
|
|
80
|
+
const collectIdentifierNames = (node, names) => {
|
|
81
|
+
if (ts.isIdentifier(node)) {
|
|
82
|
+
const parent = node.parent;
|
|
83
|
+
const isMemberName = (ts.isPropertyAccessExpression(parent) && parent.name === node) ||
|
|
84
|
+
(ts.isPropertyAssignment(parent) && parent.name === node);
|
|
85
|
+
if (!isMemberName) {
|
|
86
|
+
names.add(node.text);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
ts.forEachChild(node, (child) => collectIdentifierNames(child, names));
|
|
90
|
+
};
|
|
91
|
+
const resolveCopiedNames = (config, copiedNodes, sourceFile) => {
|
|
92
|
+
const used = new Set();
|
|
93
|
+
copiedNodes.forEach((node) => collectIdentifierNames(node, used));
|
|
94
|
+
const importedNames = new Set();
|
|
95
|
+
sourceFile.statements.forEach((statement) => {
|
|
96
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const clause = statement.importClause;
|
|
100
|
+
if (!clause || clause.isTypeOnly) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const names = [];
|
|
104
|
+
const defaultName = clause.name && used.has(clause.name.text) ? clause.name.text : undefined;
|
|
105
|
+
const namedBindings = clause.namedBindings;
|
|
106
|
+
let namedText;
|
|
107
|
+
if (defaultName)
|
|
108
|
+
names.push(defaultName);
|
|
109
|
+
if (namedBindings && ts.isNamespaceImport(namedBindings) && used.has(namedBindings.name.text)) {
|
|
110
|
+
names.push(namedBindings.name.text);
|
|
111
|
+
namedText = `* as ${namedBindings.name.text}`;
|
|
112
|
+
}
|
|
113
|
+
else if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
114
|
+
const elements = namedBindings.elements.filter((element) => !element.isTypeOnly && used.has(element.name.text));
|
|
115
|
+
elements.forEach((element) => names.push(element.name.text));
|
|
116
|
+
if (elements.length > 0) {
|
|
117
|
+
namedText = `{ ${elements.map((element) => element.getText(sourceFile)).join(', ')} }`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (names.length === 0) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
names.forEach((name) => importedNames.add(name));
|
|
124
|
+
const bindings = [defaultName, namedText].filter(Boolean).join(', ');
|
|
125
|
+
config.copiedImports.push({
|
|
126
|
+
moduleSpecifier: statement.moduleSpecifier.text,
|
|
127
|
+
names,
|
|
128
|
+
statement: (moduleSpecifier) => `import ${bindings} from '${moduleSpecifier}';`,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
sourceFile.statements.forEach((statement) => {
|
|
132
|
+
const declared = [];
|
|
133
|
+
if (ts.isVariableStatement(statement)) {
|
|
134
|
+
statement.declarationList.declarations.forEach((declaration) => {
|
|
135
|
+
if (ts.isIdentifier(declaration.name))
|
|
136
|
+
declared.push(declaration.name.text);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) {
|
|
140
|
+
declared.push(statement.name.text);
|
|
141
|
+
}
|
|
142
|
+
declared.forEach((name) => {
|
|
143
|
+
if (used.has(name) && !importedNames.has(name) && !config.localNames.includes(name)) {
|
|
144
|
+
config.localNames.push(name);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
/** Re-points a relative specifier of the v2 file at the file that now holds the provider. */
|
|
150
|
+
export const rebaseModuleSpecifier = (moduleSpecifier, fromFilePath, toFilePath) => {
|
|
151
|
+
if (!moduleSpecifier.startsWith('.')) {
|
|
152
|
+
return moduleSpecifier;
|
|
153
|
+
}
|
|
154
|
+
const rebased = relative(dirname(toFilePath), join(dirname(fromFilePath), moduleSpecifier));
|
|
155
|
+
return rebased.startsWith('.') ? rebased : `./${rebased}`;
|
|
156
|
+
};
|
|
74
157
|
const REFRESH_QUERY_KEY = 'tokenRefresh';
|
|
75
158
|
/**
|
|
76
159
|
* Renders the `queries` / `features` arguments of a v3 provider from a v2 config.
|
|
@@ -93,7 +176,7 @@ export const renderAuthProviderBody = (config) => {
|
|
|
93
176
|
}
|
|
94
177
|
const refreshEntries = [` queryCreator: ${config.refreshCreatorName},`];
|
|
95
178
|
if (config.responseAdapter) {
|
|
96
|
-
refreshEntries.push(
|
|
179
|
+
refreshEntries.push(' extractTokens: (response) => {', ` const { token, refreshToken } = (${config.responseAdapter})(response) as { token: string; refreshToken: string };`, '', ' return { accessToken: token, refreshToken };', ' },');
|
|
97
180
|
}
|
|
98
181
|
if (config.requestArgsAdapter) {
|
|
99
182
|
refreshEntries.push(` // TODO(query-v3): v2 requestArgsAdapter took { token, refreshToken }; buildArgs takes the refresh token alone.`, ` buildArgs: ${config.requestArgsAdapter},`);
|
|
@@ -111,6 +194,14 @@ export const renderAuthProviderBody = (config) => {
|
|
|
111
194
|
action: 'If the cookie was only written after an opt-in (a "remember me" box), drive `setRememberMe()` from it. If it was never written, remove `withPersistentAuth` from the scaffold.',
|
|
112
195
|
});
|
|
113
196
|
}
|
|
197
|
+
refreshEntries.push(' retryConfig: { maxAttempts: 8 },');
|
|
198
|
+
if (config.cookieName && !config.cookieDomain) {
|
|
199
|
+
warnings.push({
|
|
200
|
+
title: 'Check the cookie host: v3 keeps the login cookie per host',
|
|
201
|
+
summary: 'The v2 provider set no `cookieDomain`. `withPersistentAuth` keys the cookie per origin, so a login does not reach a sibling host.',
|
|
202
|
+
action: 'If the app runs on more than one host, check that each host signs in on its own.',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
114
205
|
const queries = [
|
|
115
206
|
' queries: [',
|
|
116
207
|
` // Derived from the v2 BearerAuthProvider in ${config.filePath}:${config.line}. Verify before shipping.`,
|
|
@@ -139,6 +230,7 @@ export const renderAuthProviderBody = (config) => {
|
|
|
139
230
|
' cookie: {',
|
|
140
231
|
...cookieEntries,
|
|
141
232
|
' },',
|
|
233
|
+
' defaultRememberMe: true,',
|
|
142
234
|
' autoLogin: {',
|
|
143
235
|
` queryKey: '${REFRESH_QUERY_KEY}',`,
|
|
144
236
|
` // TODO(query-v3): shape the auto-login request from the cookie value.`,
|
|
@@ -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,7 +34,22 @@ 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;
|
|
37
38
|
scope.visit(tree, (filePath) => {
|
|
39
|
+
if (filePath.endsWith('.ts') && tree.read(filePath, 'utf-8')?.includes('provideQueryClientForDevtools')) {
|
|
40
|
+
mountsFromProvider = true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
scope.visit(tree, (filePath) => {
|
|
44
|
+
if (filePath.endsWith('.html')) {
|
|
45
|
+
const html = tree.read(filePath, 'utf-8');
|
|
46
|
+
const nextHtml = html && (mountsFromProvider ? removeDevtoolsTags(html) : selectLazyDevtools(html));
|
|
47
|
+
if (nextHtml && nextHtml !== html) {
|
|
48
|
+
tree.write(filePath, nextHtml);
|
|
49
|
+
updatedFiles.push(filePath);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
38
53
|
if (!filePath.endsWith('.ts')) {
|
|
39
54
|
return;
|
|
40
55
|
}
|
|
@@ -43,13 +58,14 @@ export const migrateDevtoolsUsage = (tree, scope, report) => {
|
|
|
43
58
|
(!content.includes('provideQueryClientForDevtools') && !content.includes('QueryDevtoolsComponent'))) {
|
|
44
59
|
return;
|
|
45
60
|
}
|
|
46
|
-
const { content:
|
|
61
|
+
const { content: migrated, importsComponent, mountsDevtools } = migrateDevtoolsInFile(content, mountsFromProvider);
|
|
62
|
+
const nextContent = pruneUnusedNamedImports(mountsFromProvider ? removeDevtoolsTags(migrated) : migrated, findUsedNames(content));
|
|
47
63
|
if (nextContent === content) {
|
|
48
64
|
return;
|
|
49
65
|
}
|
|
50
66
|
tree.write(filePath, nextContent);
|
|
51
67
|
updatedFiles.push(filePath);
|
|
52
|
-
if (importsComponent) {
|
|
68
|
+
if (importsComponent || mountsDevtools) {
|
|
53
69
|
filesNeedingDevtoolsPackage.push(filePath);
|
|
54
70
|
}
|
|
55
71
|
});
|
|
@@ -64,7 +80,7 @@ export const migrateDevtoolsUsage = (tree, scope, report) => {
|
|
|
64
80
|
// `@ethlete/query-devtools`.
|
|
65
81
|
report.addManualReview({
|
|
66
82
|
title: 'Add @ethlete/query-devtools for the query devtools',
|
|
67
|
-
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.",
|
|
68
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.',
|
|
69
85
|
locations: filesNeedingDevtoolsPackage.map((filePath) => ({ filePath })),
|
|
70
86
|
source: 'cleanup-migration',
|
|
@@ -92,6 +108,48 @@ export const replaceAnyQueryWithLegacy = (tree, scope) => {
|
|
|
92
108
|
console.log(`\n✅ Replaced legacy AnyV2Query aliases in ${updatedFiles.length} files`);
|
|
93
109
|
}
|
|
94
110
|
};
|
|
111
|
+
export const reportLeftoverV2Clients = (tree, scope, report) => {
|
|
112
|
+
scope.visit(tree, (filePath) => {
|
|
113
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const content = tree.read(filePath, 'utf-8');
|
|
117
|
+
const index = content?.search(/new\s+V2QueryClient\s*[(<]/);
|
|
118
|
+
if (!content || index === undefined || index < 0) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const line = content.slice(0, index).split('\n').length;
|
|
122
|
+
report.addManualReview({
|
|
123
|
+
title: 'Migrator error: a V2QueryClient is left',
|
|
124
|
+
summary: 'The migration should leave no `V2QueryClient` behind, so it missed this client. Report this case as a migrator bug.',
|
|
125
|
+
action: 'Migrate the client by hand to `createQueryClient`, then report the shape the migration did not handle.',
|
|
126
|
+
locations: [{ filePath, line }],
|
|
127
|
+
source: 'cleanup-migration',
|
|
128
|
+
dedupeKey: `leftover-v2-client:${filePath}:${line}`,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
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
|
+
};
|
|
95
153
|
export const migrateEmptyPrepareCalls = (tree, scope, report) => {
|
|
96
154
|
const updatedFiles = [];
|
|
97
155
|
const graph = createModuleGraph(tree);
|
|
@@ -123,26 +181,79 @@ export const migrateEmptyPrepareCalls = (tree, scope, report) => {
|
|
|
123
181
|
console.log(`\n✅ Migrated empty .prepare() calls in ${updatedFiles.length} files`);
|
|
124
182
|
}
|
|
125
183
|
};
|
|
126
|
-
const
|
|
184
|
+
const LAZY_DEVTOOLS_COMPONENT = 'QueryDevtoolsLazyComponent';
|
|
185
|
+
const DEVTOOLS_UI_PROVIDER_CALL = "provideQueryDevtools({ ui: () => import('@ethlete/query-devtools/lazy') })";
|
|
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
|
+
};
|
|
211
|
+
const renameIdentifiers = (content, names, replacement) => {
|
|
212
|
+
const sourceFile = createSourceFile(content);
|
|
213
|
+
const ranges = [];
|
|
214
|
+
const visit = (node) => {
|
|
215
|
+
if (ts.isImportDeclaration(node)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (ts.isIdentifier(node) && names.has(node.text)) {
|
|
219
|
+
ranges.push({ start: node.getStart(sourceFile), end: node.getEnd() });
|
|
220
|
+
}
|
|
221
|
+
ts.forEachChild(node, visit);
|
|
222
|
+
};
|
|
223
|
+
visit(sourceFile);
|
|
224
|
+
return ranges
|
|
225
|
+
.sort((left, right) => right.start - left.start)
|
|
226
|
+
.reduce((result, { start, end }) => result.slice(0, start) + replacement + result.slice(end), content);
|
|
227
|
+
};
|
|
228
|
+
const migrateDevtoolsInFile = (content, mountsFromProvider) => {
|
|
127
229
|
const componentNames = localNamesImportedFromQuery(content, 'QueryDevtoolsComponent');
|
|
128
230
|
const providerNames = new Set([
|
|
129
231
|
'provideQueryClientForDevtools',
|
|
130
232
|
...localNamesImportedFromQuery(content, 'provideQueryClientForDevtools'),
|
|
131
233
|
]);
|
|
132
|
-
|
|
133
|
-
|
|
234
|
+
const providerCall = mountsFromProvider ? DEVTOOLS_UI_PROVIDER_CALL : 'provideQueryDevtools()';
|
|
235
|
+
let result = replaceDevtoolsProviderCalls(content, providerNames, providerCall);
|
|
236
|
+
const hasProvider = result.includes(providerCall);
|
|
134
237
|
result = dropLegacyDevtoolsImports(result);
|
|
135
238
|
if (hasProvider) {
|
|
136
239
|
result = ensureImportFromQuery(result, ['provideQueryDevtools']);
|
|
137
240
|
}
|
|
138
|
-
if (componentNames.length > 0) {
|
|
241
|
+
if (componentNames.length > 0 && mountsFromProvider) {
|
|
242
|
+
result = removeFromImportsArrays(result, new Set(componentNames));
|
|
243
|
+
}
|
|
244
|
+
else if (componentNames.length > 0) {
|
|
245
|
+
result = selectLazyDevtools(renameIdentifiers(result, new Set(componentNames), LAZY_DEVTOOLS_COMPONENT));
|
|
139
246
|
result = ensureNamedImports({
|
|
140
247
|
content: result,
|
|
141
|
-
importsNeeded:
|
|
142
|
-
moduleSpecifier: '@ethlete/query-devtools',
|
|
248
|
+
importsNeeded: [LAZY_DEVTOOLS_COMPONENT],
|
|
249
|
+
moduleSpecifier: '@ethlete/query-devtools/lazy',
|
|
143
250
|
});
|
|
144
251
|
}
|
|
145
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
content: result,
|
|
254
|
+
importsComponent: componentNames.length > 0 && !mountsFromProvider,
|
|
255
|
+
mountsDevtools: hasProvider && mountsFromProvider,
|
|
256
|
+
};
|
|
146
257
|
};
|
|
147
258
|
const queryImports = (sourceFile) => sourceFile.statements.filter((node) => ts.isImportDeclaration(node) &&
|
|
148
259
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
@@ -162,7 +273,7 @@ const localNamesImportedFromQuery = (content, name) => queryImports(createSource
|
|
|
162
273
|
* first call site keeps its position - it is already where the app wanted its devtools - and the
|
|
163
274
|
* rest are removed along with the comma that separated them.
|
|
164
275
|
*/
|
|
165
|
-
const replaceDevtoolsProviderCalls = (content, providerNames) => {
|
|
276
|
+
const replaceDevtoolsProviderCalls = (content, providerNames, providerCall) => {
|
|
166
277
|
const sourceFile = createSourceFile(content);
|
|
167
278
|
const calls = [];
|
|
168
279
|
const visit = (node) => {
|
|
@@ -179,7 +290,7 @@ const replaceDevtoolsProviderCalls = (content, providerNames) => {
|
|
|
179
290
|
const start = call.getStart(sourceFile);
|
|
180
291
|
const end = call.getEnd();
|
|
181
292
|
if (index === 0) {
|
|
182
|
-
return { start, end, replacement:
|
|
293
|
+
return { start, end, replacement: providerCall };
|
|
183
294
|
}
|
|
184
295
|
return { ...withSurroundingComma(content, start, end), replacement: '' };
|
|
185
296
|
});
|
|
@@ -231,16 +342,25 @@ const dropLegacyDevtoolsImports = (content) => {
|
|
|
231
342
|
const replaceAnyQueryInFile = (content) => {
|
|
232
343
|
const sourceFile = createSourceFile(content);
|
|
233
344
|
const replacements = [];
|
|
345
|
+
const names = new Set();
|
|
346
|
+
const collect = (node) => {
|
|
347
|
+
if (ts.isIdentifier(node))
|
|
348
|
+
names.add(node.text);
|
|
349
|
+
ts.forEachChild(node, collect);
|
|
350
|
+
};
|
|
351
|
+
collect(sourceFile);
|
|
352
|
+
const renamesQuery = !names.has('AnyLegacyQuery');
|
|
353
|
+
const renamesCreator = !names.has('AnyLegacyQueryCreator');
|
|
234
354
|
const visit = (node) => {
|
|
235
355
|
if (ts.isIdentifier(node)) {
|
|
236
|
-
if (node.text === 'AnyV2Query') {
|
|
356
|
+
if (renamesQuery && node.text === 'AnyV2Query') {
|
|
237
357
|
replacements.push({
|
|
238
358
|
start: node.getStart(sourceFile),
|
|
239
359
|
end: node.getEnd(),
|
|
240
360
|
replacement: 'AnyLegacyQuery',
|
|
241
361
|
});
|
|
242
362
|
}
|
|
243
|
-
if (node.text === 'AnyV2QueryCreator') {
|
|
363
|
+
if (renamesCreator && node.text === 'AnyV2QueryCreator') {
|
|
244
364
|
replacements.push({
|
|
245
365
|
start: node.getStart(sourceFile),
|
|
246
366
|
end: node.getEnd(),
|