@ethlete/query 6.0.0-next.61 → 6.0.0-next.62
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 +32 -0
- package/fesm2022/ethlete-query-devtools-contract.mjs +1 -1
- package/fesm2022/ethlete-query.mjs +74 -17
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/migrate-from-ngrx-toolkit/consumers.js +1 -1
- 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 +64 -5
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +234 -19
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +68 -15
- package/generators/migrate-to-query-v3/migration.js +6 -3
- package/generators/migrate-to-query-v3/query-client-migration.js +82 -14
- 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.d.ts +18 -3
|
@@ -365,7 +365,7 @@ export const rewriteConsumer = (tree, graph, filePath, content) => {
|
|
|
365
365
|
tasks,
|
|
366
366
|
};
|
|
367
367
|
};
|
|
368
|
-
const isInterceptor = (node, sourceFile) => {
|
|
368
|
+
export const isInterceptor = (node, sourceFile) => {
|
|
369
369
|
if (ts.isClassDeclaration(node)) {
|
|
370
370
|
return !!node.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText(sourceFile) === 'HttpInterceptor'));
|
|
371
371
|
}
|
|
@@ -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);`, '', ' 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.`,
|
|
@@ -35,6 +35,15 @@ export const migrateDevtoolsUsage = (tree, scope, report) => {
|
|
|
35
35
|
const updatedFiles = [];
|
|
36
36
|
const filesNeedingDevtoolsPackage = [];
|
|
37
37
|
scope.visit(tree, (filePath) => {
|
|
38
|
+
if (filePath.endsWith('.html')) {
|
|
39
|
+
const html = tree.read(filePath, 'utf-8');
|
|
40
|
+
const nextHtml = html && selectLazyDevtools(html);
|
|
41
|
+
if (nextHtml && nextHtml !== html) {
|
|
42
|
+
tree.write(filePath, nextHtml);
|
|
43
|
+
updatedFiles.push(filePath);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
38
47
|
if (!filePath.endsWith('.ts')) {
|
|
39
48
|
return;
|
|
40
49
|
}
|
|
@@ -64,7 +73,7 @@ export const migrateDevtoolsUsage = (tree, scope, report) => {
|
|
|
64
73
|
// `@ethlete/query-devtools`.
|
|
65
74
|
report.addManualReview({
|
|
66
75
|
title: 'Add @ethlete/query-devtools for the query devtools',
|
|
67
|
-
summary: '`QueryDevtoolsComponent` now
|
|
76
|
+
summary: '`QueryDevtoolsComponent` is now `QueryDevtoolsLazyComponent` from `@ethlete/query-devtools/lazy`, and `<et-query-devtools>` is `<et-query-devtools-lazy>`. The imports and templates were rewritten, but the package may not be a dependency of these projects yet.',
|
|
68
77
|
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
78
|
locations: filesNeedingDevtoolsPackage.map((filePath) => ({ filePath })),
|
|
70
79
|
source: 'cleanup-migration',
|
|
@@ -92,6 +101,27 @@ export const replaceAnyQueryWithLegacy = (tree, scope) => {
|
|
|
92
101
|
console.log(`\n✅ Replaced legacy AnyV2Query aliases in ${updatedFiles.length} files`);
|
|
93
102
|
}
|
|
94
103
|
};
|
|
104
|
+
export const reportLeftoverV2Clients = (tree, scope, report) => {
|
|
105
|
+
scope.visit(tree, (filePath) => {
|
|
106
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const content = tree.read(filePath, 'utf-8');
|
|
110
|
+
const index = content?.search(/new\s+V2QueryClient\s*[(<]/);
|
|
111
|
+
if (!content || index === undefined || index < 0) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const line = content.slice(0, index).split('\n').length;
|
|
115
|
+
report.addManualReview({
|
|
116
|
+
title: 'Migrator error: a V2QueryClient is left',
|
|
117
|
+
summary: 'The migration should leave no `V2QueryClient` behind, so it missed this client. Report this case as a migrator bug.',
|
|
118
|
+
action: 'Migrate the client by hand to `createQueryClient`, then report the shape the migration did not handle.',
|
|
119
|
+
locations: [{ filePath, line }],
|
|
120
|
+
source: 'cleanup-migration',
|
|
121
|
+
dedupeKey: `leftover-v2-client:${filePath}:${line}`,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
};
|
|
95
125
|
export const migrateEmptyPrepareCalls = (tree, scope, report) => {
|
|
96
126
|
const updatedFiles = [];
|
|
97
127
|
const graph = createModuleGraph(tree);
|
|
@@ -123,6 +153,25 @@ export const migrateEmptyPrepareCalls = (tree, scope, report) => {
|
|
|
123
153
|
console.log(`\n✅ Migrated empty .prepare() calls in ${updatedFiles.length} files`);
|
|
124
154
|
}
|
|
125
155
|
};
|
|
156
|
+
const LAZY_DEVTOOLS_COMPONENT = 'QueryDevtoolsLazyComponent';
|
|
157
|
+
const selectLazyDevtools = (content) => content.replace(/(<\/?)et-query-devtools(?![\w-])/g, '$1et-query-devtools-lazy');
|
|
158
|
+
const renameIdentifiers = (content, names, replacement) => {
|
|
159
|
+
const sourceFile = createSourceFile(content);
|
|
160
|
+
const ranges = [];
|
|
161
|
+
const visit = (node) => {
|
|
162
|
+
if (ts.isImportDeclaration(node)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (ts.isIdentifier(node) && names.has(node.text)) {
|
|
166
|
+
ranges.push({ start: node.getStart(sourceFile), end: node.getEnd() });
|
|
167
|
+
}
|
|
168
|
+
ts.forEachChild(node, visit);
|
|
169
|
+
};
|
|
170
|
+
visit(sourceFile);
|
|
171
|
+
return ranges
|
|
172
|
+
.sort((left, right) => right.start - left.start)
|
|
173
|
+
.reduce((result, { start, end }) => result.slice(0, start) + replacement + result.slice(end), content);
|
|
174
|
+
};
|
|
126
175
|
const migrateDevtoolsInFile = (content) => {
|
|
127
176
|
const componentNames = localNamesImportedFromQuery(content, 'QueryDevtoolsComponent');
|
|
128
177
|
const providerNames = new Set([
|
|
@@ -136,10 +185,11 @@ const migrateDevtoolsInFile = (content) => {
|
|
|
136
185
|
result = ensureImportFromQuery(result, ['provideQueryDevtools']);
|
|
137
186
|
}
|
|
138
187
|
if (componentNames.length > 0) {
|
|
188
|
+
result = selectLazyDevtools(renameIdentifiers(result, new Set(componentNames), LAZY_DEVTOOLS_COMPONENT));
|
|
139
189
|
result = ensureNamedImports({
|
|
140
190
|
content: result,
|
|
141
|
-
importsNeeded:
|
|
142
|
-
moduleSpecifier: '@ethlete/query-devtools',
|
|
191
|
+
importsNeeded: [LAZY_DEVTOOLS_COMPONENT],
|
|
192
|
+
moduleSpecifier: '@ethlete/query-devtools/lazy',
|
|
143
193
|
});
|
|
144
194
|
}
|
|
145
195
|
return { content: result, importsComponent: componentNames.length > 0 };
|
|
@@ -231,16 +281,25 @@ const dropLegacyDevtoolsImports = (content) => {
|
|
|
231
281
|
const replaceAnyQueryInFile = (content) => {
|
|
232
282
|
const sourceFile = createSourceFile(content);
|
|
233
283
|
const replacements = [];
|
|
284
|
+
const names = new Set();
|
|
285
|
+
const collect = (node) => {
|
|
286
|
+
if (ts.isIdentifier(node))
|
|
287
|
+
names.add(node.text);
|
|
288
|
+
ts.forEachChild(node, collect);
|
|
289
|
+
};
|
|
290
|
+
collect(sourceFile);
|
|
291
|
+
const renamesQuery = !names.has('AnyLegacyQuery');
|
|
292
|
+
const renamesCreator = !names.has('AnyLegacyQueryCreator');
|
|
234
293
|
const visit = (node) => {
|
|
235
294
|
if (ts.isIdentifier(node)) {
|
|
236
|
-
if (node.text === 'AnyV2Query') {
|
|
295
|
+
if (renamesQuery && node.text === 'AnyV2Query') {
|
|
237
296
|
replacements.push({
|
|
238
297
|
start: node.getStart(sourceFile),
|
|
239
298
|
end: node.getEnd(),
|
|
240
299
|
replacement: 'AnyLegacyQuery',
|
|
241
300
|
});
|
|
242
301
|
}
|
|
243
|
-
if (node.text === 'AnyV2QueryCreator') {
|
|
302
|
+
if (renamesCreator && node.text === 'AnyV2QueryCreator') {
|
|
244
303
|
replacements.push({
|
|
245
304
|
start: node.getStart(sourceFile),
|
|
246
305
|
end: node.getEnd(),
|