@ethlete/query 6.0.0-next.51 → 6.0.0-next.53

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 (37) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +393 -0
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
  4. package/fesm2022/ethlete-query-testing.mjs +67 -25
  5. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  6. package/fesm2022/ethlete-query.mjs +7895 -7226
  7. package/fesm2022/ethlete-query.mjs.map +1 -1
  8. package/generators/generators.json +5 -0
  9. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
  10. package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
  11. package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
  12. package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
  13. package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
  14. package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
  15. package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
  16. package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
  17. package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
  18. package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
  19. package/generators/migrate-query-opt-in-features/migration.js +10 -9
  20. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
  21. package/generators/migrate-to-query-v3/cleanup-migration.js +163 -53
  22. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
  23. package/generators/migrate-to-query-v3/migration-scope.js +2 -0
  24. package/generators/migrate-to-query-v3/migration.js +5 -4
  25. package/generators/migrate-to-query-v3/module-graph.js +41 -29
  26. package/generators/migrate-to-query-v3/query-client-migration.js +209 -126
  27. package/generators/migrate-to-query-v3/rename-symbols.js +38 -11
  28. package/generators/migrate-to-query-v3/report.js +40 -7
  29. package/generators/migrate-to-query-v3/shared.js +46 -1
  30. package/generators/prep-for-query-v3/migration.js +400 -57
  31. package/migrations/query-field-default-types.md +43 -0
  32. package/migrations/search-query-field-string.md +43 -0
  33. package/migrations.json +16 -0
  34. package/package.json +5 -1
  35. package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
  36. package/types/ethlete-query-testing.d.ts +28 -11
  37. package/types/ethlete-query.d.ts +789 -584
@@ -1,5 +1,5 @@
1
1
  import { formatFiles } from '@nx/devkit';
2
- import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy } from './cleanup-migration.js';
2
+ import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy, reportRemovedExperimentalQueryHelpers, } from './cleanup-migration.js';
3
3
  import { reportDefaultHeaderUsages, reportMissingHttpClientProviders } from './http-client-check.js';
4
4
  import { migrateLegacyPrepareCalls } from './legacy-prepare-migration.js';
5
5
  import { createNewQueryCreators, updateLegacyCreatorImportsAndUsages } from './legacy-query-creator-migration.js';
@@ -19,17 +19,18 @@ export default async function migrate(tree, schema) {
19
19
  updateLegacyCreatorImportsAndUsages(tree, scope, report);
20
20
  }
21
21
  if (variableRenames.size > 0) {
22
- updateImportsAcrossWorkspace(tree, variableRenames, scope);
22
+ updateImportsAcrossWorkspace(tree, variableRenames, scope, report);
23
23
  }
24
24
  replaceAnyQueryWithLegacy(tree, scope);
25
25
  migrateDevtoolsUsage(tree, scope, report);
26
- migrateEmptyPrepareCalls(tree, scope);
26
+ migrateEmptyPrepareCalls(tree, scope, report);
27
27
  migrateLegacyPrepareCalls(tree, report, scope);
28
+ reportRemovedExperimentalQueryHelpers(tree, scope, report);
28
29
  if (queryClientFiles.size > 0) {
29
30
  reportMissingHttpClientProviders(tree, report, scope);
30
31
  reportDefaultHeaderUsages(tree, report, scope);
31
32
  }
32
- report.writeToTree(tree);
33
+ report.writeToTree(tree, scope.isScoped ? scope.describe() : undefined);
33
34
  report.printSummary();
34
35
  if (!schema.skipFormat) {
35
36
  await formatFiles(tree);
@@ -15,8 +15,9 @@ const normalizePath = (path) => {
15
15
  return segments.join('/');
16
16
  };
17
17
  const dirName = (path) => path.slice(0, Math.max(0, path.lastIndexOf('/')));
18
- const readTsConfigPaths = (tree) => {
18
+ const readTsConfig = (tree) => {
19
19
  const paths = new Map();
20
+ let baseUrl = null;
20
21
  for (const configPath of ['tsconfig.base.json', 'tsconfig.json']) {
21
22
  const raw = tree.read(configPath, 'utf-8');
22
23
  if (!raw)
@@ -25,6 +26,9 @@ const readTsConfigPaths = (tree) => {
25
26
  // tsconfig files are JSONC in practice; the TS parser is the only thing that reliably reads
26
27
  // them, comments and trailing commas included.
27
28
  const parsed = ts.parseConfigFileTextToJson(configPath, raw).config;
29
+ const configBaseUrl = parsed?.compilerOptions?.baseUrl;
30
+ if (baseUrl === null && typeof configBaseUrl === 'string')
31
+ baseUrl = normalizePath(configBaseUrl);
28
32
  Object.entries(parsed?.compilerOptions?.paths ?? {}).forEach(([key, targets]) => {
29
33
  if (!paths.has(key))
30
34
  paths.set(key, targets);
@@ -34,45 +38,49 @@ const readTsConfigPaths = (tree) => {
34
38
  // A tsconfig we cannot parse just means fewer resolvable aliases, not a failed migration.
35
39
  }
36
40
  }
37
- return paths;
41
+ return { paths, baseUrl };
38
42
  };
43
+ /** The part of `specifier` the `*` of `pattern` stands for, `''` for an exact match, or `null`. */
44
+ const matchPathAlias = (pattern, specifier) => {
45
+ const wildcardIndex = pattern.indexOf('*');
46
+ if (wildcardIndex === -1)
47
+ return pattern === specifier ? '' : null;
48
+ const prefix = pattern.slice(0, wildcardIndex);
49
+ const patternSuffix = pattern.slice(wildcardIndex + 1);
50
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(patternSuffix))
51
+ return null;
52
+ return specifier.slice(prefix.length, specifier.length - patternSuffix.length);
53
+ };
54
+ const JS_EXTENSION = /\.(m?)js$/;
39
55
  /** The files a specifier could point at, most specific first. */
40
- const candidateFiles = (base) => [
41
- base.endsWith('.ts') ? base : `${base}.ts`,
42
- `${base}/index.ts`,
43
- `${base}.d.ts`,
44
- ];
56
+ const candidateFiles = (base) => {
57
+ if (base.endsWith('.ts'))
58
+ return [base];
59
+ const jsExtension = JS_EXTENSION.exec(base);
60
+ if (jsExtension) {
61
+ const stem = base.slice(0, -jsExtension[0].length);
62
+ return [`${stem}.${jsExtension[1]}ts`, `${stem}.ts`, `${stem}.d.ts`, `${base}/index.ts`];
63
+ }
64
+ return [`${base}.ts`, `${base}/index.ts`, `${base}.d.ts`];
65
+ };
45
66
  export const createModuleGraph = (tree) => {
46
- const tsConfigPaths = readTsConfigPaths(tree);
67
+ const { paths: tsConfigPaths, baseUrl } = readTsConfig(tree);
68
+ const findCandidate = (base) => candidateFiles(base).find((candidate) => tree.exists(candidate)) ?? null;
47
69
  const declaringFileCache = new Map();
48
70
  const resolveEntryFile = (fromFile, specifier) => {
49
- if (specifier.startsWith('.')) {
50
- const base = normalizePath(`${dirName(fromFile)}/${specifier}`);
51
- return candidateFiles(base).find((candidate) => tree.exists(candidate)) ?? null;
52
- }
71
+ if (specifier.startsWith('.'))
72
+ return findCandidate(normalizePath(`${dirName(fromFile)}/${specifier}`));
53
73
  for (const [pattern, targets] of tsConfigPaths.entries()) {
54
- const wildcardIndex = pattern.indexOf('*');
55
- let suffix;
56
- if (wildcardIndex === -1) {
57
- if (pattern !== specifier)
58
- continue;
59
- suffix = '';
60
- }
61
- else {
62
- const prefix = pattern.slice(0, wildcardIndex);
63
- const patternSuffix = pattern.slice(wildcardIndex + 1);
64
- if (!specifier.startsWith(prefix) || !specifier.endsWith(patternSuffix))
65
- continue;
66
- suffix = specifier.slice(prefix.length, specifier.length - patternSuffix.length);
67
- }
74
+ const wildcard = matchPathAlias(pattern, specifier);
75
+ if (wildcard === null)
76
+ continue;
68
77
  for (const target of targets) {
69
- const resolvedTarget = normalizePath(target.replace('*', suffix));
70
- const candidate = candidateFiles(resolvedTarget).find((option) => tree.exists(option));
78
+ const candidate = findCandidate(normalizePath(`${baseUrl ?? ''}/${target.replace('*', wildcard)}`));
71
79
  if (candidate)
72
80
  return candidate;
73
81
  }
74
82
  }
75
- return null;
83
+ return baseUrl === null ? null : findCandidate(normalizePath(`${baseUrl}/${specifier}`));
76
84
  };
77
85
  const declaresSymbol = (sourceFile, symbolName) => {
78
86
  let declares = false;
@@ -138,6 +146,10 @@ export const createModuleGraph = (tree) => {
138
146
  return null;
139
147
  };
140
148
  return {
149
+ resolveFile: resolveEntryFile,
150
+ pathAliases: () => [...tsConfigPaths.keys()],
151
+ isWorkspaceSpecifier: (specifier) => specifier.startsWith('.') ||
152
+ [...tsConfigPaths.keys()].some((pattern) => matchPathAlias(pattern, specifier) !== null),
141
153
  findDeclaringFile: (fromFile, specifier, symbolName) => {
142
154
  const cacheKey = `${fromFile}|${specifier}|${symbolName}`;
143
155
  const cached = declaringFileCache.get(cacheKey);
@@ -1,5 +1,8 @@
1
1
  import * as ts from 'typescript';
2
- import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
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';
3
6
  export const migrateQueryClients = (tree, report, scope) => {
4
7
  const queryClientFiles = new Map();
5
8
  const variableRenames = new Map();
@@ -13,9 +16,9 @@ export const migrateQueryClients = (tree, report, scope) => {
13
16
  }
14
17
  const renames = new Map();
15
18
  const nextContent = migrateQueryClientToConfig({ content, filePath, renamesOut: renames, report });
16
- renames.forEach((newName, oldName) => {
17
- variableRenames.set(oldName, newName);
18
- });
19
+ if (renames.size > 0) {
20
+ variableRenames.set(filePath, { originalContent: content, renames });
21
+ }
19
22
  const configNames = extractClientConfigNames(nextContent);
20
23
  if (configNames.length > 0) {
21
24
  queryClientFiles.set(filePath, configNames);
@@ -68,10 +71,12 @@ export const generateQueryCreators = (tree, queryClientFiles) => {
68
71
  generatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
69
72
  }
70
73
  };
71
- export const updateImportsAcrossWorkspace = (tree, renames, scope) => {
74
+ export const updateImportsAcrossWorkspace = (tree, renames, scope, report) => {
72
75
  if (renames.size === 0) {
73
76
  return;
74
77
  }
78
+ const originalContents = new Map(Array.from(renames.entries()).map(([filePath, { originalContent }]) => [filePath, originalContent]));
79
+ const graph = createModuleGraph(withOriginalContents(tree, originalContents));
75
80
  scope.visit(tree, (filePath) => {
76
81
  if (!filePath.endsWith('.ts')) {
77
82
  return;
@@ -80,12 +85,42 @@ export const updateImportsAcrossWorkspace = (tree, renames, scope) => {
80
85
  if (!content) {
81
86
  return;
82
87
  }
83
- 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
+ }
84
99
  if (nextContent !== content) {
100
+ originalContents.set(filePath, content);
85
101
  tree.write(filePath, nextContent);
86
102
  }
87
103
  });
88
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
+ });
89
124
  const hasQueryClientInstantiation = (content) => {
90
125
  const sourceFile = createSourceFile(content);
91
126
  let hasQueryClientImport = false;
@@ -151,6 +186,7 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
151
186
  }
152
187
  }
153
188
  reportDroppedClientOptions({ configArgument, sourceFile, filePath, report });
189
+ reportChangedClientDefaults({ configArgument, sourceFile, filePath, report });
154
190
  replacements.push({
155
191
  start: node.getStart(sourceFile),
156
192
  end: node.getEnd(),
@@ -190,6 +226,7 @@ const removeQueryClientImport = (content, keepLegacyClient) => {
190
226
  'createPostQuery',
191
227
  'createPutQuery',
192
228
  'createQueryClient',
229
+ CLIENT_FEATURE,
193
230
  ];
194
231
  if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
195
232
  return ensureImportFromQuery(content, requiredImports);
@@ -199,11 +236,31 @@ const removeQueryClientImport = (content, keepLegacyClient) => {
199
236
  });
200
237
  const nextImports = new Set(requiredImports);
201
238
  existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
202
- const nextImportStatement = `import { ${Array.from(nextImports).sort().join(', ')} } from '@ethlete/query';`;
239
+ const nextImportStatement = printImportDeclaration(queryImportNode, sourceFile, Array.from(nextImports).sort());
203
240
  return (content.slice(0, queryImportNode.getStart(sourceFile)) +
204
241
  nextImportStatement +
205
242
  content.slice(queryImportNode.getEnd()));
206
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
+ };
207
264
  const renameVariables = (content, renames) => {
208
265
  if (renames.size === 0) {
209
266
  return content;
@@ -211,48 +268,31 @@ const renameVariables = (content, renames) => {
211
268
  const sourceFile = createSourceFile(content);
212
269
  const replacements = [];
213
270
  const visit = (node) => {
214
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
215
- const nextName = renames.get(node.name.text);
216
- if (nextName) {
217
- replacements.push({
218
- start: node.name.getStart(sourceFile),
219
- end: node.name.getEnd(),
220
- replacement: nextName,
221
- });
222
- }
223
- }
224
- else if (ts.isIdentifier(node)) {
225
- const nextName = renames.get(node.text);
226
- if (nextName) {
227
- const parent = node.parent;
228
- if (!ts.isPropertyAssignment(parent) || parent.name !== node) {
229
- replacements.push({
230
- start: node.getStart(sourceFile),
231
- end: node.getEnd(),
232
- replacement: nextName,
233
- });
234
- }
235
- }
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 });
236
275
  }
237
276
  ts.forEachChild(node, visit);
238
277
  };
239
278
  visit(sourceFile);
240
- const seen = new Set();
241
- const uniqueReplacements = replacements.filter((replacement) => {
242
- const key = `${replacement.start}:${replacement.end}`;
243
- if (seen.has(key)) {
244
- return false;
245
- }
246
- seen.add(key);
247
- return true;
248
- });
279
+ return applyReplacements(content, replacements);
280
+ };
281
+ const applyReplacements = (content, replacements) => {
249
282
  let result = content;
250
- uniqueReplacements.sort((left, right) => right.start - left.start);
251
- uniqueReplacements.forEach(({ start, end, replacement }) => {
283
+ [...replacements]
284
+ .sort((left, right) => right.start - left.start)
285
+ .forEach(({ start, end, replacement }) => {
252
286
  result = result.slice(0, start) + replacement + result.slice(end);
253
287
  });
254
288
  return result;
255
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
+ };
256
296
  /**
257
297
  * What each v2 client option turns into, or why it has nowhere to go.
258
298
  *
@@ -261,8 +301,8 @@ const renameVariables = (content, renames) => {
261
301
  * report from production.
262
302
  */
263
303
  const DROPPED_CLIENT_OPTIONS = {
264
- 'request.autoRefreshQueriesOnWindowFocus': 'No direct equivalent. Re-run the affected queries yourself - `withAutoRefresh({ onSignalChanges: [windowFocusSignal] })` per query, or `injectMyClient().refreshQueriesInUse()` from a focus listener.',
265
- 'request.enableSmartPolling': 'No direct equivalent. `withPolling({ interval })` polls unconditionally; pause it yourself when the tab is hidden if that matters.',
304
+ 'request.autoRefreshQueriesOnWindowFocus': WINDOW_FOCUS_REPLACEMENT,
305
+ 'request.enableSmartPolling': SMART_POLLING_REPLACEMENT,
266
306
  'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
267
307
  'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
268
308
  'request.autoRefreshOn': 'No direct equivalent. Re-run the affected queries yourself with `withAutoRefresh` or `refreshQueriesInUse()`.',
@@ -315,6 +355,12 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
315
355
  if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
316
356
  return;
317
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
+ }
318
364
  raise(`${name}.${nested.name.text}`, nested);
319
365
  });
320
366
  return;
@@ -324,6 +370,59 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
324
370
  }
325
371
  });
326
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
+ };
327
426
  const getPropertyValueText = (property, sourceFile) => {
328
427
  if (ts.isShorthandPropertyAssignment(property)) {
329
428
  return property.name.text;
@@ -361,6 +460,7 @@ const migrateConfigObject = (configArgument, node, sourceFile) => {
361
460
  }
362
461
  }
363
462
  }
463
+ nextConfig.push(`features: [${CLIENT_FEATURE}()]`);
364
464
  return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
365
465
  };
366
466
  const getVariableNameForQueryClient = (node) => {
@@ -470,107 +570,90 @@ const generateCreatorsForConfig = (configName) => {
470
570
  `export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
471
571
  ].join('\n');
472
572
  };
473
- const declaresName = (declarations, name) => declarations.some((declaration) => declaration.name && ts.isIdentifier(declaration.name) && declaration.name.text === name);
474
- const isShadowedByLocalDeclaration = (identifier) => {
475
- for (let scope = identifier.parent; scope && !ts.isSourceFile(scope); scope = scope.parent) {
476
- if (ts.isFunctionLike(scope) && declaresName(scope.parameters, identifier.text)) {
477
- return true;
478
- }
479
- if (ts.isBlock(scope) &&
480
- scope.statements.some((statement) => ts.isVariableStatement(statement) && declaresName(statement.declarationList.declarations, identifier.text))) {
481
- return true;
482
- }
483
- }
484
- return false;
485
- };
486
- const updateImportsInFile = (content, renames) => {
487
- 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);
488
581
  const replacements = [];
489
- const visit = (node) => {
490
- if (ts.isImportDeclaration(node) &&
491
- node.importClause?.namedBindings &&
492
- ts.isNamedImports(node.importClause.namedBindings)) {
493
- 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;
494
602
  let hasChanges = false;
495
- node.importClause.namedBindings.elements.forEach((element) => {
496
- const nextName = renames.get(element.propertyName?.text ?? element.name.text);
603
+ const nextElements = statement.importClause.namedBindings.elements.map((element) => {
604
+ const nextName = resolveRename(specifier, (element.propertyName ?? element.name).text);
497
605
  if (!nextName) {
498
- nextElements.push(element.getText(sourceFile));
499
- return;
606
+ return element.getText(sourceFile);
500
607
  }
501
608
  hasChanges = true;
502
- 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}`;
503
615
  });
504
616
  if (hasChanges) {
505
617
  replacements.push({
506
- start: node.getStart(sourceFile),
507
- end: node.getEnd(),
508
- replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
618
+ start: statement.getStart(sourceFile),
619
+ end: statement.getEnd(),
620
+ replacement: printImportDeclaration(statement, sourceFile, nextElements),
509
621
  });
510
622
  }
511
623
  }
512
- if (ts.isIdentifier(node)) {
513
- const nextName = renames.get(node.text);
514
- if (!nextName) {
515
- ts.forEachChild(node, visit);
516
- return;
517
- }
518
- const parent = node.parent;
519
- if (ts.isImportSpecifier(parent)) {
520
- ts.forEachChild(node, visit);
521
- return;
522
- }
523
- if (ts.isVariableDeclaration(parent) && parent.name === node) {
524
- ts.forEachChild(node, visit);
525
- return;
526
- }
527
- if (ts.isFunctionDeclaration(parent) && parent.name === node) {
528
- ts.forEachChild(node, visit);
529
- return;
530
- }
531
- if (ts.isMethodDeclaration(parent) && parent.name === node) {
532
- ts.forEachChild(node, visit);
533
- return;
534
- }
535
- if (ts.isParameter(parent) && parent.name === node) {
536
- ts.forEachChild(node, visit);
537
- return;
538
- }
539
- if (ts.isPropertyDeclaration(parent) && parent.name === node) {
540
- ts.forEachChild(node, visit);
541
- return;
542
- }
543
- if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
544
- ts.forEachChild(node, visit);
545
- return;
546
- }
547
- if ((ts.isPropertyAssignment(parent) && parent.name === node) || isShadowedByLocalDeclaration(node)) {
548
- ts.forEachChild(node, visit);
549
- return;
550
- }
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)) {
551
646
  replacements.push({
552
647
  start: node.getStart(sourceFile),
553
648
  end: node.getEnd(),
554
- replacement: nextName,
649
+ replacement: renamedReference(node, nextName),
555
650
  });
556
651
  }
557
652
  ts.forEachChild(node, visit);
558
653
  };
559
- visit(sourceFile);
560
- let result = content;
561
- const seen = new Set();
562
- const uniqueReplacements = replacements.filter((replacement) => {
563
- const key = `${replacement.start}:${replacement.end}`;
564
- if (seen.has(key)) {
565
- return false;
566
- }
567
- seen.add(key);
568
- return true;
569
- });
570
- uniqueReplacements.sort((left, right) => right.start - left.start);
571
- uniqueReplacements.forEach(({ start, end, replacement }) => {
572
- result = result.slice(0, start) + replacement + result.slice(end);
573
- });
574
- return result;
654
+ if (localRenames.size > 0) {
655
+ visit(sourceFile);
656
+ }
657
+ return { content: applyReplacements(content, replacements), renamedByName };
575
658
  };
576
659
  //# sourceMappingURL=query-client-migration.js.map