@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.
@@ -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(` // TODO(query-v3): v2 responseAdapter. It must return { accessToken, refreshToken } - v2 returned { token, refreshToken }.`, ` extractTokens: ${config.responseAdapter},`);
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 lives in `@ethlete/query-devtools`. The imports were rewritten, but the package may not be a dependency of these projects yet.',
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: componentNames.map((name) => name === 'QueryDevtoolsComponent' ? name : `QueryDevtoolsComponent as ${name}`),
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(),