@ethlete/query 6.0.0-next.6 → 6.0.0-next.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +939 -0
  2. package/fesm2022/ethlete-query-devtools-contract.mjs +1 -0
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +411 -0
  4. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
  5. package/fesm2022/ethlete-query-testing.mjs +548 -169
  6. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  7. package/fesm2022/ethlete-query.mjs +14680 -7172
  8. package/fesm2022/ethlete-query.mjs.map +1 -1
  9. package/generators/deprecate-legacy-queries/migration.js +85 -0
  10. package/generators/deprecate-legacy-queries/schema.json +26 -0
  11. package/generators/generators.json +30 -0
  12. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
  13. package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
  14. package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
  15. package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
  16. package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
  17. package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
  18. package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
  19. package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
  20. package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
  21. package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
  22. package/generators/migrate-query-client-features/migration.js +144 -0
  23. package/generators/migrate-query-client-features/report.js +136 -0
  24. package/generators/migrate-query-client-features/schema.json +26 -0
  25. package/generators/migrate-query-opt-in-features/migration.js +195 -0
  26. package/generators/migrate-query-opt-in-features/report.js +143 -0
  27. package/generators/migrate-query-opt-in-features/schema.json +31 -0
  28. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +160 -0
  29. package/generators/migrate-to-query-v3/cleanup-migration.js +247 -125
  30. package/generators/migrate-to-query-v3/http-client-check.js +93 -0
  31. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
  32. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +274 -152
  33. package/generators/migrate-to-query-v3/migration-scope.js +40 -0
  34. package/generators/migrate-to-query-v3/migration.js +19 -10
  35. package/generators/migrate-to-query-v3/module-graph.js +165 -0
  36. package/generators/migrate-to-query-v3/query-client-migration.js +342 -160
  37. package/generators/migrate-to-query-v3/rename-symbols.js +233 -0
  38. package/generators/migrate-to-query-v3/report.js +40 -7
  39. package/generators/migrate-to-query-v3/schema.json +12 -0
  40. package/generators/migrate-to-query-v3/shared.js +97 -3
  41. package/generators/move-devtools-contract-imports/migration.js +155 -0
  42. package/generators/move-devtools-contract-imports/moved-names.js +153 -0
  43. package/generators/move-devtools-contract-imports/schema.json +26 -0
  44. package/generators/prep-for-query-v3/migration.js +465 -65
  45. package/generators/report-legacy-query-apis/migration.js +256 -0
  46. package/generators/report-legacy-query-apis/schema.json +25 -0
  47. package/migrations/query-field-default-types.md +43 -0
  48. package/migrations/search-query-field-string.md +43 -0
  49. package/migrations.json +87 -0
  50. package/package.json +34 -14
  51. package/types/ethlete-query-devtools-contract.d.ts +2 -0
  52. package/types/ethlete-query-ngrx-toolkit.d.ts +131 -0
  53. package/types/ethlete-query-testing.d.ts +295 -160
  54. package/types/ethlete-query.d.ts +9755 -3682
@@ -1,10 +1,12 @@
1
- import { visitNotIgnoredFiles } from '@nx/devkit';
2
1
  import * as ts from 'typescript';
3
- import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
4
- export const migrateQueryClients = (tree, report) => {
2
+ import { CLIENT_FEATURE, reportClientErrorPipelineAdded } from '../migrate-query-opt-in-features/migration.js';
3
+ import { createModuleGraph } from './module-graph.js';
4
+ import { isNamePosition, isShadowedInNestedScope, renamedReference } from './rename-symbols.js';
5
+ import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, getVariableStatementEnd, printImportDeclaration, } from './shared.js';
6
+ export const migrateQueryClients = (tree, report, scope) => {
5
7
  const queryClientFiles = new Map();
6
8
  const variableRenames = new Map();
7
- visitNotIgnoredFiles(tree, '', (filePath) => {
9
+ scope.visit(tree, (filePath) => {
8
10
  if (!filePath.endsWith('.ts')) {
9
11
  return;
10
12
  }
@@ -14,9 +16,9 @@ export const migrateQueryClients = (tree, report) => {
14
16
  }
15
17
  const renames = new Map();
16
18
  const nextContent = migrateQueryClientToConfig({ content, filePath, renamesOut: renames, report });
17
- renames.forEach((newName, oldName) => {
18
- variableRenames.set(oldName, newName);
19
- });
19
+ if (renames.size > 0) {
20
+ variableRenames.set(filePath, { originalContent: content, renames });
21
+ }
20
22
  const configNames = extractClientConfigNames(nextContent);
21
23
  if (configNames.length > 0) {
22
24
  queryClientFiles.set(filePath, configNames);
@@ -69,11 +71,13 @@ export const generateQueryCreators = (tree, queryClientFiles) => {
69
71
  generatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
70
72
  }
71
73
  };
72
- export const updateImportsAcrossWorkspace = (tree, renames) => {
74
+ export const updateImportsAcrossWorkspace = (tree, renames, scope, report) => {
73
75
  if (renames.size === 0) {
74
76
  return;
75
77
  }
76
- visitNotIgnoredFiles(tree, '', (filePath) => {
78
+ const originalContents = new Map(Array.from(renames.entries()).map(([filePath, { originalContent }]) => [filePath, originalContent]));
79
+ const graph = createModuleGraph(withOriginalContents(tree, originalContents));
80
+ scope.visit(tree, (filePath) => {
77
81
  if (!filePath.endsWith('.ts')) {
78
82
  return;
79
83
  }
@@ -81,12 +85,42 @@ export const updateImportsAcrossWorkspace = (tree, renames) => {
81
85
  if (!content) {
82
86
  return;
83
87
  }
84
- const nextContent = updateImportsInFile(content, renames);
88
+ const { content: nextContent, renamedByName } = updateImportsInFile(content, filePath, renames, graph);
89
+ if (renamedByName.size > 0) {
90
+ report.addManualReview({
91
+ title: `Check the client rename in ${filePath}`,
92
+ summary: `The migration could not resolve ${[...renamedByName].map((specifier) => `\`${specifier}\``).join(', ')} to a file, so it renamed the imported client by its name alone.`,
93
+ action: 'Confirm the renamed import really is the migrated query client, and revert it if it is not.',
94
+ locations: [{ filePath }],
95
+ source: 'query-client-migration',
96
+ dedupeKey: `client-rename-by-name:${filePath}`,
97
+ });
98
+ }
85
99
  if (nextContent !== content) {
100
+ originalContents.set(filePath, content);
86
101
  tree.write(filePath, nextContent);
87
102
  }
88
103
  });
89
104
  };
105
+ /**
106
+ * A view of `tree` that still reads the pre-migration content of `originalContents`, so the module
107
+ * graph resolves a client import by the name it had when the import was written.
108
+ */
109
+ const withOriginalContents = (tree, originalContents) => new Proxy(tree, {
110
+ get: (target, property) => {
111
+ if (property === 'read') {
112
+ return (filePath, encoding) => {
113
+ const original = originalContents.get(filePath);
114
+ if (original === undefined) {
115
+ return encoding ? target.read(filePath, encoding) : target.read(filePath);
116
+ }
117
+ return encoding ? original : Buffer.from(original);
118
+ };
119
+ }
120
+ const value = Reflect.get(target, property, target);
121
+ return typeof value === 'function' ? value.bind(target) : value;
122
+ },
123
+ });
90
124
  const hasQueryClientInstantiation = (content) => {
91
125
  const sourceFile = createSourceFile(content);
92
126
  let hasQueryClientImport = false;
@@ -116,17 +150,25 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
116
150
  const sourceFile = createSourceFile(content, filePath);
117
151
  const replacements = [];
118
152
  const variableRenames = new Map();
153
+ let hasUnmigratedClient = false;
119
154
  const visit = (node) => {
120
- if (ts.isNewExpression(node) &&
121
- ts.isIdentifier(node.expression) &&
122
- node.expression.text === 'V2QueryClient' &&
123
- node.arguments &&
124
- node.arguments.length > 0) {
125
- const configArgument = node.arguments[0];
126
- if (ts.isObjectLiteralExpression(configArgument)) {
155
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'V2QueryClient') {
156
+ const configArgument = node.arguments?.[0];
157
+ if (!configArgument || !ts.isObjectLiteralExpression(configArgument)) {
158
+ hasUnmigratedClient = true;
159
+ report.addManualReview({
160
+ title: 'Migrate a V2QueryClient whose config is not an object literal',
161
+ summary: `\`${node.getText(sourceFile)}\` passes its config indirectly, so the migration cannot read it and left the client on v2.`,
162
+ action: 'Inline the config object and re-run the migration, or rewrite the client by hand as `createQueryClient({ baseUrl, name })` with provider aliases and creators.',
163
+ locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
164
+ source: 'query-client-migration',
165
+ dedupeKey: `unmigrated-client:${filePath}:${node.getStart(sourceFile)}`,
166
+ });
167
+ }
168
+ else {
127
169
  const oldVariableName = getVariableNameForQueryClient(node);
128
- const newVariableName = ensureConfigSuffix(oldVariableName);
129
- if (oldVariableName !== newVariableName) {
170
+ const newVariableName = oldVariableName && ensureConfigSuffix(oldVariableName);
171
+ if (oldVariableName && newVariableName && oldVariableName !== newVariableName) {
130
172
  const collisionRegex = new RegExp(`\\b(?:const|let|var)\\s+${newVariableName}\\b`);
131
173
  if (collisionRegex.test(content)) {
132
174
  report.addWarning({
@@ -143,6 +185,8 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
143
185
  renamesOut.set(oldVariableName, newVariableName);
144
186
  }
145
187
  }
188
+ reportDroppedClientOptions({ configArgument, sourceFile, filePath, report });
189
+ reportChangedClientDefaults({ configArgument, sourceFile, filePath, report });
146
190
  replacements.push({
147
191
  start: node.getStart(sourceFile),
148
192
  end: node.getEnd(),
@@ -158,16 +202,20 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
158
202
  replacements.forEach(({ start, end, replacement }) => {
159
203
  result = result.slice(0, start) + replacement + result.slice(end);
160
204
  });
161
- result = removeQueryClientImport(result);
205
+ result = removeQueryClientImport(result, hasUnmigratedClient);
162
206
  return renameVariables(result, variableRenames);
163
207
  };
164
- const removeQueryClientImport = (content) => {
208
+ const removeQueryClientImport = (content, keepLegacyClient) => {
165
209
  const sourceFile = createSourceFile(content);
166
210
  let queryImportNode;
167
211
  ts.forEachChild(sourceFile, (node) => {
168
- if (ts.isImportDeclaration(node) &&
212
+ if (!queryImportNode &&
213
+ ts.isImportDeclaration(node) &&
169
214
  ts.isStringLiteral(node.moduleSpecifier) &&
170
- node.moduleSpecifier.text === '@ethlete/query') {
215
+ node.moduleSpecifier.text === '@ethlete/query' &&
216
+ node.importClause?.namedBindings &&
217
+ ts.isNamedImports(node.importClause.namedBindings) &&
218
+ node.importClause.namedBindings.elements.some((element) => element.name.text === 'V2QueryClient')) {
171
219
  queryImportNode = node;
172
220
  }
173
221
  });
@@ -178,20 +226,41 @@ const removeQueryClientImport = (content) => {
178
226
  'createPostQuery',
179
227
  'createPutQuery',
180
228
  'createQueryClient',
229
+ CLIENT_FEATURE,
181
230
  ];
182
231
  if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
183
232
  return ensureImportFromQuery(content, requiredImports);
184
233
  }
185
234
  const existingElements = queryImportNode.importClause.namedBindings.elements.filter((element) => {
186
- return element.name.text !== 'V2QueryClient';
235
+ return keepLegacyClient || element.name.text !== 'V2QueryClient';
187
236
  });
188
237
  const nextImports = new Set(requiredImports);
189
- existingElements.forEach((element) => nextImports.add(element.name.text));
190
- const nextImportStatement = `import { ${Array.from(nextImports).sort().join(', ')} } from '@ethlete/query';`;
238
+ existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
239
+ const nextImportStatement = printImportDeclaration(queryImportNode, sourceFile, Array.from(nextImports).sort());
191
240
  return (content.slice(0, queryImportNode.getStart(sourceFile)) +
192
241
  nextImportStatement +
193
242
  content.slice(queryImportNode.getEnd()));
194
243
  };
244
+ const isTopLevelVariableName = (node) => {
245
+ const declaration = node.parent;
246
+ return (ts.isVariableDeclaration(declaration) &&
247
+ declaration.name === node &&
248
+ ts.isVariableStatement(declaration.parent.parent) &&
249
+ ts.isSourceFile(declaration.parent.parent.parent));
250
+ };
251
+ const renameInDeclaringFile = (node, nextName) => {
252
+ const parent = node.parent;
253
+ if (isTopLevelVariableName(node)) {
254
+ return nextName;
255
+ }
256
+ if (ts.isExportSpecifier(parent) && !parent.parent.parent.moduleSpecifier) {
257
+ return (parent.propertyName ?? parent.name) === node ? nextName : undefined;
258
+ }
259
+ if (isNamePosition(node) || isShadowedInNestedScope(node)) {
260
+ return undefined;
261
+ }
262
+ return renamedReference(node, nextName);
263
+ };
195
264
  const renameVariables = (content, renames) => {
196
265
  if (renames.size === 0) {
197
266
  return content;
@@ -199,83 +268,199 @@ const renameVariables = (content, renames) => {
199
268
  const sourceFile = createSourceFile(content);
200
269
  const replacements = [];
201
270
  const visit = (node) => {
202
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
203
- const nextName = renames.get(node.name.text);
204
- if (nextName) {
205
- replacements.push({
206
- start: node.name.getStart(sourceFile),
207
- end: node.name.getEnd(),
208
- replacement: nextName,
209
- });
210
- }
211
- }
212
- else if (ts.isIdentifier(node)) {
213
- const nextName = renames.get(node.text);
214
- if (nextName) {
215
- const parent = node.parent;
216
- if (!ts.isPropertyAssignment(parent) || parent.name !== node) {
217
- replacements.push({
218
- start: node.getStart(sourceFile),
219
- end: node.getEnd(),
220
- replacement: nextName,
221
- });
222
- }
223
- }
271
+ const nextName = ts.isIdentifier(node) ? renames.get(node.text) : undefined;
272
+ const replacement = nextName && renameInDeclaringFile(node, nextName);
273
+ if (replacement) {
274
+ replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement });
224
275
  }
225
276
  ts.forEachChild(node, visit);
226
277
  };
227
278
  visit(sourceFile);
228
- const seen = new Set();
229
- const uniqueReplacements = replacements.filter((replacement) => {
230
- const key = `${replacement.start}:${replacement.end}`;
231
- if (seen.has(key)) {
232
- return false;
233
- }
234
- seen.add(key);
235
- return true;
236
- });
279
+ return applyReplacements(content, replacements);
280
+ };
281
+ const applyReplacements = (content, replacements) => {
237
282
  let result = content;
238
- uniqueReplacements.sort((left, right) => right.start - left.start);
239
- uniqueReplacements.forEach(({ start, end, replacement }) => {
283
+ [...replacements]
284
+ .sort((left, right) => right.start - left.start)
285
+ .forEach(({ start, end, replacement }) => {
240
286
  result = result.slice(0, start) + replacement + result.slice(end);
241
287
  });
242
288
  return result;
243
289
  };
290
+ const WINDOW_FOCUS_REPLACEMENT = 'Opt in per query: `withPolling({ interval, refetchOnFocus: true })` on a polled query, `withAutoRefresh({ onSignalChanges: [windowFocusSignal] })` on any other, or `injectMyClient().refreshQueriesInUse()` from a focus listener.';
291
+ const SMART_POLLING_REPLACEMENT = 'Opt in per query: `withPolling({ interval, pauseWhileHidden: true })`. Without it v3 keeps polling in a hidden tab.';
292
+ const V2_ON_BY_DEFAULT_REQUEST_OPTIONS = {
293
+ autoRefreshQueriesOnWindowFocus: WINDOW_FOCUS_REPLACEMENT,
294
+ enableSmartPolling: SMART_POLLING_REPLACEMENT,
295
+ };
296
+ /**
297
+ * What each v2 client option turns into, or why it has nowhere to go.
298
+ *
299
+ * Silently dropping these is how two apps here lost their default headers without a single warning.
300
+ * A dropped option that shows up in the report is a five-minute fix; one that doesn't is a bug
301
+ * report from production.
302
+ */
303
+ const DROPPED_CLIENT_OPTIONS = {
304
+ 'request.autoRefreshQueriesOnWindowFocus': WINDOW_FOCUS_REPLACEMENT,
305
+ 'request.enableSmartPolling': SMART_POLLING_REPLACEMENT,
306
+ 'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
307
+ 'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
308
+ 'request.autoRefreshOn': 'No direct equivalent. Re-run the affected queries yourself with `withAutoRefresh` or `refreshQueriesInUse()`.',
309
+ };
310
+ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, report, }) => {
311
+ const migrated = new Set(['baseRoute', 'queryParams', 'cacheAdapter', 'retryFn', 'request', 'logging']);
312
+ const raise = (optionPath, node) => {
313
+ report.addWarning({
314
+ title: `Reconfigure dropped query client option "${optionPath}"`,
315
+ summary: `The v2 client option \`${optionPath}\` has no place in \`createQueryClient\` and was dropped by the migration.`,
316
+ action: DROPPED_CLIENT_OPTIONS[optionPath] ?? 'Check whether the behaviour is still needed and reimplement it.',
317
+ locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
318
+ source: 'query-client-migration',
319
+ dedupeKey: `dropped-client-option:${filePath}:${optionPath}`,
320
+ });
321
+ };
322
+ const raiseSpread = (spread) => {
323
+ const text = spread.getText(sourceFile);
324
+ report.addWarning({
325
+ title: `Reconfigure spread query client options "${text}"`,
326
+ summary: `The v2 client config spreads \`${text}\`. The migration cannot see which options it holds, so none of them were carried over to \`createQueryClient\`.`,
327
+ action: 'Inline the spread options that are still needed (`baseRoute` becomes `baseUrl`) and re-check the result.',
328
+ locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(spread.getStart(sourceFile)).line + 1 }],
329
+ source: 'query-client-migration',
330
+ dedupeKey: `dropped-client-spread:${filePath}:${spread.getStart(sourceFile)}`,
331
+ });
332
+ };
333
+ configArgument.properties.forEach((property) => {
334
+ if (ts.isSpreadAssignment(property)) {
335
+ raiseSpread(property);
336
+ return;
337
+ }
338
+ if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) ||
339
+ !ts.isIdentifier(property.name)) {
340
+ return;
341
+ }
342
+ const name = property.name.text;
343
+ if ((name === 'request' || name === 'logging') &&
344
+ ts.isPropertyAssignment(property) &&
345
+ ts.isObjectLiteralExpression(property.initializer)) {
346
+ property.initializer.properties.forEach((nested) => {
347
+ if (ts.isSpreadAssignment(nested)) {
348
+ raiseSpread(nested);
349
+ return;
350
+ }
351
+ if ((!ts.isPropertyAssignment(nested) && !ts.isShorthandPropertyAssignment(nested)) ||
352
+ !ts.isIdentifier(nested.name)) {
353
+ return;
354
+ }
355
+ if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
356
+ return;
357
+ }
358
+ if (name === 'request' &&
359
+ nested.name.text in V2_ON_BY_DEFAULT_REQUEST_OPTIONS &&
360
+ ts.isPropertyAssignment(nested) &&
361
+ nested.initializer.kind === ts.SyntaxKind.FalseKeyword) {
362
+ return;
363
+ }
364
+ raise(`${name}.${nested.name.text}`, nested);
365
+ });
366
+ return;
367
+ }
368
+ if (!migrated.has(name)) {
369
+ raise(name, property);
370
+ }
371
+ });
372
+ };
373
+ const reportChangedClientDefaults = ({ configArgument, sourceFile, filePath, report, }) => {
374
+ const location = {
375
+ filePath,
376
+ line: sourceFile.getLineAndCharacterOfPosition(configArgument.getStart(sourceFile)).line + 1,
377
+ };
378
+ const requestProperty = findProperty(configArgument, 'request');
379
+ const requestConfig = requestProperty &&
380
+ ts.isPropertyAssignment(requestProperty) &&
381
+ ts.isObjectLiteralExpression(requestProperty.initializer)
382
+ ? requestProperty.initializer
383
+ : undefined;
384
+ const hasUnreadableRequestConfig = (!!requestProperty && !requestConfig) || !!requestConfig?.properties.some(ts.isSpreadAssignment);
385
+ reportClientErrorPipelineAdded(report, location, 'query-client-migration', `\`${CLIENT_FEATURE}()\` was added so failed requests still retry and Symfony / HTML error bodies are still parsed, as they were by default in v2.`);
386
+ if (!requestConfig || !findProperty(requestConfig, 'retryFn')) {
387
+ report.addWarning({
388
+ title: 'Check the retry behaviour of the migrated client',
389
+ summary: 'v2 retried every request method on a 5xx (500 included), 408, 425 and 429, up to four times with a linear delay. The v3 default policy that `withEthleteApiErrors()` installs retries three times with an exponential, jittered delay, never retries a `POST`, a `PATCH` or a plain `500`, and also retries a connection failure.',
390
+ action: 'Accept the v3 policy, or pass `withEthleteApiErrors({ retry: { maxAttempts, retryableStatusCodes, retryNonIdempotent } })` - only set `retryNonIdempotent` for endpoints that deduplicate on their own.',
391
+ locations: [location],
392
+ source: 'query-client-migration',
393
+ dedupeKey: `changed-retry-default:${filePath}`,
394
+ });
395
+ }
396
+ const cacheAdapter = requestConfig && findProperty(requestConfig, 'cacheAdapter');
397
+ if (cacheAdapter) {
398
+ report.addWarning({
399
+ title: 'Check the carried-over cacheAdapter',
400
+ summary: 'The v2 `cacheAdapter` was carried over as is and still sets the freshness TTL: a TTL of `0` means an `execute({ options: { allowCache: true } })` always hits the server, as a v2 `execute()` did. Two things differ: v3 passes Angular `HttpHeaders` (read them with `headers.get(name)`) where v2 passed a plain record, and v3 keeps an entry for `keepUnusedFor` (5 minutes) after its last consumer, rendering the old response on a remount while it refetches.',
401
+ action: 'Rewrite an adapter that reads headers for `HttpHeaders`. If the adapter was meant to switch caching off, also set `keepUnusedFor: 0` on the client.',
402
+ locations: [
403
+ { filePath, line: sourceFile.getLineAndCharacterOfPosition(cacheAdapter.getStart(sourceFile)).line + 1 },
404
+ ],
405
+ source: 'query-client-migration',
406
+ dedupeKey: `carried-cache-adapter:${filePath}`,
407
+ });
408
+ }
409
+ if (hasUnreadableRequestConfig) {
410
+ return;
411
+ }
412
+ Object.entries(V2_ON_BY_DEFAULT_REQUEST_OPTIONS).forEach(([option, action]) => {
413
+ if (requestConfig && findProperty(requestConfig, option)) {
414
+ return;
415
+ }
416
+ report.addWarning({
417
+ title: `Restore the v2 default "request.${option}"`,
418
+ summary: `The v2 client did not set \`request.${option}\`, so it ran with the v2 default \`true\`. v3 has no client-wide equivalent and the behaviour is off for every migrated query.`,
419
+ action,
420
+ locations: [location],
421
+ source: 'query-client-migration',
422
+ dedupeKey: `dropped-client-default:${filePath}:${option}`,
423
+ });
424
+ });
425
+ };
426
+ const getPropertyValueText = (property, sourceFile) => {
427
+ if (ts.isShorthandPropertyAssignment(property)) {
428
+ return property.name.text;
429
+ }
430
+ return ts.isPropertyAssignment(property) ? property.initializer.getText(sourceFile) : undefined;
431
+ };
432
+ const findProperty = (objectLiteral, name) => objectLiteral.properties.find((property) => {
433
+ return ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
434
+ ts.isIdentifier(property.name) &&
435
+ property.name.text === name);
436
+ });
244
437
  const migrateConfigObject = (configArgument, node, sourceFile) => {
245
- const variableName = getVariableNameForQueryClient(node);
438
+ const variableName = getVariableNameForQueryClient(node) ?? 'client';
246
439
  const nextConfig = [];
247
- const baseRouteProperty = configArgument.properties.find((property) => {
248
- return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'baseRoute';
249
- });
250
- if (baseRouteProperty && ts.isPropertyAssignment(baseRouteProperty)) {
251
- const baseRouteValue = baseRouteProperty.initializer
252
- .getText(sourceFile)
253
- .replace('as `https://${string}`', '')
254
- .trim();
255
- nextConfig.push(`baseUrl: ${baseRouteValue}`);
440
+ const baseRouteProperty = findProperty(configArgument, 'baseRoute');
441
+ const baseRouteText = baseRouteProperty && getPropertyValueText(baseRouteProperty, sourceFile);
442
+ if (baseRouteText) {
443
+ nextConfig.push(`baseUrl: ${baseRouteText.replace('as `https://${string}`', '').trim()}`);
256
444
  }
257
445
  nextConfig.push(`name: '${variableName}'`);
258
- const requestProperty = configArgument.properties.find((property) => {
259
- return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'request';
260
- });
446
+ const requestProperty = findProperty(configArgument, 'request');
261
447
  if (requestProperty &&
262
448
  ts.isPropertyAssignment(requestProperty) &&
263
449
  ts.isObjectLiteralExpression(requestProperty.initializer)) {
264
- requestProperty.initializer.properties.forEach((property) => {
265
- if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
266
- return;
450
+ const requestConfig = requestProperty.initializer;
451
+ for (const [from, to] of [
452
+ ['queryParams', 'queryString'],
453
+ ['cacheAdapter', 'cacheAdapter'],
454
+ ['retryFn', 'retryFn'],
455
+ ]) {
456
+ const property = findProperty(requestConfig, from);
457
+ const valueText = property && getPropertyValueText(property, sourceFile);
458
+ if (valueText) {
459
+ nextConfig.push(`${to}: ${valueText}`);
267
460
  }
268
- if (property.name.text === 'queryParams') {
269
- nextConfig.push(`queryString: ${property.initializer.getText(sourceFile)}`);
270
- }
271
- if (property.name.text === 'cacheAdapter') {
272
- nextConfig.push(`cacheAdapter: ${property.initializer.getText(sourceFile)}`);
273
- }
274
- if (property.name.text === 'retryFn') {
275
- nextConfig.push(`retryFn: ${property.initializer.getText(sourceFile)}`);
276
- }
277
- });
461
+ }
278
462
  }
463
+ nextConfig.push(`features: [${CLIENT_FEATURE}()]`);
279
464
  return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
280
465
  };
281
466
  const getVariableNameForQueryClient = (node) => {
@@ -286,7 +471,7 @@ const getVariableNameForQueryClient = (node) => {
286
471
  }
287
472
  parent = parent.parent;
288
473
  }
289
- return 'client';
474
+ return undefined;
290
475
  };
291
476
  const extractClientConfigNames = (content) => {
292
477
  const sourceFile = createSourceFile(content);
@@ -326,19 +511,20 @@ const addProviderAliasesToFile = (content, configNames) => {
326
511
  return;
327
512
  }
328
513
  const { injectName, provideName } = getProviderAliasNames(variableName);
329
- if (result.includes(`export const [${provideName}, ${injectName}] = ${variableName};`)) {
514
+ if (result.includes(`export const ${injectName} = toInjectFn(${variableName});`)) {
330
515
  return;
331
516
  }
332
517
  insertions.push({
333
518
  position,
334
- text: `\n\nexport const [${provideName}, ${injectName}] = ${variableName};`,
519
+ text: `\n\nexport const ${provideName} = toProvideFn(${variableName});` +
520
+ `\nexport const ${injectName} = toInjectFn(${variableName});`,
335
521
  });
336
522
  });
337
523
  insertions.sort((left, right) => right.position - left.position);
338
524
  insertions.forEach(({ position, text }) => {
339
525
  result = result.slice(0, position) + text + result.slice(position);
340
526
  });
341
- return result;
527
+ return insertions.length > 0 ? ensureImportFromEthleteCore(result, ['toInjectFn', 'toProvideFn']) : result;
342
528
  };
343
529
  const getProviderAliasNames = (configVariableName) => {
344
530
  const baseName = configVariableName.replace(/Config$/, '');
@@ -384,94 +570,90 @@ const generateCreatorsForConfig = (configName) => {
384
570
  `export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
385
571
  ].join('\n');
386
572
  };
387
- const updateImportsInFile = (content, renames) => {
388
- const sourceFile = createSourceFile(content);
573
+ /**
574
+ * Points imports and re-exports of a renamed client at its new name, and renames the references to
575
+ * each unaliased import. Only a binding the module graph resolves to the client's declaring file is
576
+ * touched; a same-named symbol from any other module stays as it is. A workspace specifier the graph
577
+ * cannot resolve falls back to the rename by name and is returned in `renamedByName` for the report.
578
+ */
579
+ const updateImportsInFile = (content, filePath, renames, graph) => {
580
+ const sourceFile = createSourceFile(content, filePath);
389
581
  const replacements = [];
390
- const visit = (node) => {
391
- if (ts.isImportDeclaration(node) &&
392
- node.importClause?.namedBindings &&
393
- ts.isNamedImports(node.importClause.namedBindings)) {
394
- const nextElements = [];
582
+ const localRenames = new Map();
583
+ const renamedByName = new Set();
584
+ const resolveRename = (specifier, importedName) => {
585
+ const declaringFile = graph.findDeclaringFile(filePath, specifier, importedName);
586
+ if (declaringFile) {
587
+ return declaringFile !== filePath ? renames.get(declaringFile)?.renames.get(importedName) : undefined;
588
+ }
589
+ if (!graph.isWorkspaceSpecifier(specifier))
590
+ return undefined;
591
+ const nextName = [...renames.values()].find((entry) => entry.renames.has(importedName))?.renames.get(importedName);
592
+ if (nextName)
593
+ renamedByName.add(specifier);
594
+ return nextName;
595
+ };
596
+ for (const statement of sourceFile.statements) {
597
+ if (ts.isImportDeclaration(statement) &&
598
+ ts.isStringLiteral(statement.moduleSpecifier) &&
599
+ statement.importClause?.namedBindings &&
600
+ ts.isNamedImports(statement.importClause.namedBindings)) {
601
+ const specifier = statement.moduleSpecifier.text;
395
602
  let hasChanges = false;
396
- node.importClause.namedBindings.elements.forEach((element) => {
397
- const nextName = renames.get(element.name.text);
603
+ const nextElements = statement.importClause.namedBindings.elements.map((element) => {
604
+ const nextName = resolveRename(specifier, (element.propertyName ?? element.name).text);
398
605
  if (!nextName) {
399
- nextElements.push(element.getText(sourceFile));
400
- return;
606
+ return element.getText(sourceFile);
401
607
  }
402
608
  hasChanges = true;
403
- nextElements.push(element.propertyName ? `${nextName} as ${element.name.text}` : nextName);
609
+ const typePrefix = element.isTypeOnly ? 'type ' : '';
610
+ if (element.propertyName) {
611
+ return `${typePrefix}${nextName} as ${element.name.text}`;
612
+ }
613
+ localRenames.set(element.name.text, nextName);
614
+ return `${typePrefix}${nextName}`;
404
615
  });
405
616
  if (hasChanges) {
406
617
  replacements.push({
407
- start: node.getStart(sourceFile),
408
- end: node.getEnd(),
409
- replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
618
+ start: statement.getStart(sourceFile),
619
+ end: statement.getEnd(),
620
+ replacement: printImportDeclaration(statement, sourceFile, nextElements),
410
621
  });
411
622
  }
412
623
  }
413
- if (ts.isIdentifier(node)) {
414
- const nextName = renames.get(node.text);
415
- if (!nextName) {
416
- ts.forEachChild(node, visit);
417
- return;
418
- }
419
- const parent = node.parent;
420
- if (ts.isImportSpecifier(parent)) {
421
- ts.forEachChild(node, visit);
422
- return;
423
- }
424
- if (ts.isVariableDeclaration(parent) && parent.name === node) {
425
- ts.forEachChild(node, visit);
426
- return;
427
- }
428
- if (ts.isFunctionDeclaration(parent) && parent.name === node) {
429
- ts.forEachChild(node, visit);
430
- return;
431
- }
432
- if (ts.isMethodDeclaration(parent) && parent.name === node) {
433
- ts.forEachChild(node, visit);
434
- return;
435
- }
436
- if (ts.isParameter(parent) && parent.name === node) {
437
- ts.forEachChild(node, visit);
438
- return;
439
- }
440
- if (ts.isPropertyDeclaration(parent) && parent.name === node) {
441
- ts.forEachChild(node, visit);
442
- return;
443
- }
444
- if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
445
- ts.forEachChild(node, visit);
446
- return;
447
- }
448
- if (ts.isPropertyAssignment(parent) && parent.name === node) {
449
- ts.forEachChild(node, visit);
450
- return;
451
- }
624
+ if (ts.isExportDeclaration(statement) &&
625
+ statement.moduleSpecifier &&
626
+ ts.isStringLiteral(statement.moduleSpecifier) &&
627
+ statement.exportClause &&
628
+ ts.isNamedExports(statement.exportClause)) {
629
+ const specifier = statement.moduleSpecifier.text;
630
+ statement.exportClause.elements.forEach((element) => {
631
+ const exportedFrom = element.propertyName ?? element.name;
632
+ const nextName = ts.isIdentifier(exportedFrom) ? resolveRename(specifier, exportedFrom.text) : undefined;
633
+ if (nextName) {
634
+ replacements.push({
635
+ start: exportedFrom.getStart(sourceFile),
636
+ end: exportedFrom.getEnd(),
637
+ replacement: nextName,
638
+ });
639
+ }
640
+ });
641
+ }
642
+ }
643
+ const visit = (node) => {
644
+ const nextName = ts.isIdentifier(node) ? localRenames.get(node.text) : undefined;
645
+ if (nextName && !isNamePosition(node) && !isShadowedInNestedScope(node)) {
452
646
  replacements.push({
453
647
  start: node.getStart(sourceFile),
454
648
  end: node.getEnd(),
455
- replacement: nextName,
649
+ replacement: renamedReference(node, nextName),
456
650
  });
457
651
  }
458
652
  ts.forEachChild(node, visit);
459
653
  };
460
- visit(sourceFile);
461
- let result = content;
462
- const seen = new Set();
463
- const uniqueReplacements = replacements.filter((replacement) => {
464
- const key = `${replacement.start}:${replacement.end}`;
465
- if (seen.has(key)) {
466
- return false;
467
- }
468
- seen.add(key);
469
- return true;
470
- });
471
- uniqueReplacements.sort((left, right) => right.start - left.start);
472
- uniqueReplacements.forEach(({ start, end, replacement }) => {
473
- result = result.slice(0, start) + replacement + result.slice(end);
474
- });
475
- return result;
654
+ if (localRenames.size > 0) {
655
+ visit(sourceFile);
656
+ }
657
+ return { content: applyReplacements(content, replacements), renamedByName };
476
658
  };
477
659
  //# sourceMappingURL=query-client-migration.js.map