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

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 (31) hide show
  1. package/CHANGELOG.md +772 -0
  2. package/fesm2022/ethlete-query-testing.mjs +441 -38
  3. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  4. package/fesm2022/ethlete-query.mjs +15961 -5330
  5. package/fesm2022/ethlete-query.mjs.map +1 -1
  6. package/generators/deprecate-legacy-queries/migration.js +77 -0
  7. package/generators/deprecate-legacy-queries/schema.json +26 -0
  8. package/generators/generators.json +15 -0
  9. package/generators/migrate-query-client-features/migration.js +142 -0
  10. package/generators/migrate-query-client-features/report.js +136 -0
  11. package/generators/migrate-query-client-features/schema.json +26 -0
  12. package/generators/migrate-query-opt-in-features/migration.js +194 -0
  13. package/generators/migrate-query-opt-in-features/report.js +143 -0
  14. package/generators/migrate-query-opt-in-features/schema.json +31 -0
  15. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +133 -0
  16. package/generators/migrate-to-query-v3/cleanup-migration.js +111 -99
  17. package/generators/migrate-to-query-v3/http-client-check.js +93 -0
  18. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
  19. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +237 -152
  20. package/generators/migrate-to-query-v3/migration-scope.js +38 -0
  21. package/generators/migrate-to-query-v3/migration.js +17 -9
  22. package/generators/migrate-to-query-v3/module-graph.js +153 -0
  23. package/generators/migrate-to-query-v3/query-client-migration.js +152 -53
  24. package/generators/migrate-to-query-v3/rename-symbols.js +206 -0
  25. package/generators/migrate-to-query-v3/schema.json +12 -0
  26. package/generators/migrate-to-query-v3/shared.js +51 -2
  27. package/generators/prep-for-query-v3/migration.js +71 -14
  28. package/migrations.json +44 -0
  29. package/package.json +29 -13
  30. package/types/ethlete-query-testing.d.ts +179 -12
  31. package/types/ethlete-query.d.ts +8935 -3203
@@ -0,0 +1,153 @@
1
+ import * as ts from 'typescript';
2
+ import { createSourceFile } from './shared.js';
3
+ const MAX_BARREL_DEPTH = 8;
4
+ const normalizePath = (path) => {
5
+ const segments = [];
6
+ for (const segment of path.split('/')) {
7
+ if (segment === '' || segment === '.')
8
+ continue;
9
+ if (segment === '..') {
10
+ segments.pop();
11
+ continue;
12
+ }
13
+ segments.push(segment);
14
+ }
15
+ return segments.join('/');
16
+ };
17
+ const dirName = (path) => path.slice(0, Math.max(0, path.lastIndexOf('/')));
18
+ const readTsConfigPaths = (tree) => {
19
+ const paths = new Map();
20
+ for (const configPath of ['tsconfig.base.json', 'tsconfig.json']) {
21
+ const raw = tree.read(configPath, 'utf-8');
22
+ if (!raw)
23
+ continue;
24
+ try {
25
+ // tsconfig files are JSONC in practice; the TS parser is the only thing that reliably reads
26
+ // them, comments and trailing commas included.
27
+ const parsed = ts.parseConfigFileTextToJson(configPath, raw).config;
28
+ Object.entries(parsed?.compilerOptions?.paths ?? {}).forEach(([key, targets]) => {
29
+ if (!paths.has(key))
30
+ paths.set(key, targets);
31
+ });
32
+ }
33
+ catch {
34
+ // A tsconfig we cannot parse just means fewer resolvable aliases, not a failed migration.
35
+ }
36
+ }
37
+ return paths;
38
+ };
39
+ /** 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
+ ];
45
+ export const createModuleGraph = (tree) => {
46
+ const tsConfigPaths = readTsConfigPaths(tree);
47
+ const declaringFileCache = new Map();
48
+ 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
+ }
53
+ 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
+ }
68
+ for (const target of targets) {
69
+ const resolvedTarget = normalizePath(target.replace('*', suffix));
70
+ const candidate = candidateFiles(resolvedTarget).find((option) => tree.exists(option));
71
+ if (candidate)
72
+ return candidate;
73
+ }
74
+ }
75
+ return null;
76
+ };
77
+ const declaresSymbol = (sourceFile, symbolName) => {
78
+ let declares = false;
79
+ const isExported = (node) => ts.canHaveModifiers(node) &&
80
+ ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
81
+ ts.forEachChild(sourceFile, (node) => {
82
+ if (declares)
83
+ return;
84
+ if (ts.isVariableStatement(node) && isExported(node)) {
85
+ declares = node.declarationList.declarations.some((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === symbolName);
86
+ return;
87
+ }
88
+ if ((ts.isFunctionDeclaration(node) ||
89
+ ts.isClassDeclaration(node) ||
90
+ ts.isInterfaceDeclaration(node) ||
91
+ ts.isTypeAliasDeclaration(node) ||
92
+ ts.isEnumDeclaration(node)) &&
93
+ isExported(node) &&
94
+ node.name?.text === symbolName) {
95
+ declares = true;
96
+ }
97
+ });
98
+ return declares;
99
+ };
100
+ const findDeclaringFileIn = (entryFile, symbolName, depth, seen) => {
101
+ if (depth > MAX_BARREL_DEPTH || seen.has(`${entryFile}#${symbolName}`))
102
+ return null;
103
+ seen.add(`${entryFile}#${symbolName}`);
104
+ const content = tree.read(entryFile, 'utf-8');
105
+ if (!content)
106
+ return null;
107
+ const sourceFile = createSourceFile(content, entryFile);
108
+ if (declaresSymbol(sourceFile, symbolName))
109
+ return entryFile;
110
+ const starExports = [];
111
+ for (const node of sourceFile.statements) {
112
+ if (!ts.isExportDeclaration(node) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier)) {
113
+ continue;
114
+ }
115
+ const nextEntry = resolveEntryFile(entryFile, node.moduleSpecifier.text);
116
+ if (!nextEntry)
117
+ continue;
118
+ if (!node.exportClause) {
119
+ starExports.push(nextEntry);
120
+ continue;
121
+ }
122
+ if (!ts.isNamedExports(node.exportClause))
123
+ continue;
124
+ for (const element of node.exportClause.elements) {
125
+ if (element.name.text !== symbolName)
126
+ continue;
127
+ const originalName = element.propertyName?.text ?? symbolName;
128
+ const found = findDeclaringFileIn(nextEntry, originalName, depth + 1, seen);
129
+ if (found)
130
+ return found;
131
+ }
132
+ }
133
+ for (const starExport of starExports) {
134
+ const found = findDeclaringFileIn(starExport, symbolName, depth + 1, seen);
135
+ if (found)
136
+ return found;
137
+ }
138
+ return null;
139
+ };
140
+ return {
141
+ findDeclaringFile: (fromFile, specifier, symbolName) => {
142
+ const cacheKey = `${fromFile}|${specifier}|${symbolName}`;
143
+ const cached = declaringFileCache.get(cacheKey);
144
+ if (cached !== undefined)
145
+ return cached;
146
+ const entryFile = resolveEntryFile(fromFile, specifier);
147
+ const declaringFile = entryFile ? findDeclaringFileIn(entryFile, symbolName, 0, new Set()) : null;
148
+ declaringFileCache.set(cacheKey, declaringFile);
149
+ return declaringFile;
150
+ },
151
+ };
152
+ };
153
+ //# sourceMappingURL=module-graph.js.map
@@ -1,10 +1,9 @@
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 { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
3
+ export const migrateQueryClients = (tree, report, scope) => {
5
4
  const queryClientFiles = new Map();
6
5
  const variableRenames = new Map();
7
- visitNotIgnoredFiles(tree, '', (filePath) => {
6
+ scope.visit(tree, (filePath) => {
8
7
  if (!filePath.endsWith('.ts')) {
9
8
  return;
10
9
  }
@@ -69,11 +68,11 @@ export const generateQueryCreators = (tree, queryClientFiles) => {
69
68
  generatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
70
69
  }
71
70
  };
72
- export const updateImportsAcrossWorkspace = (tree, renames) => {
71
+ export const updateImportsAcrossWorkspace = (tree, renames, scope) => {
73
72
  if (renames.size === 0) {
74
73
  return;
75
74
  }
76
- visitNotIgnoredFiles(tree, '', (filePath) => {
75
+ scope.visit(tree, (filePath) => {
77
76
  if (!filePath.endsWith('.ts')) {
78
77
  return;
79
78
  }
@@ -116,17 +115,25 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
116
115
  const sourceFile = createSourceFile(content, filePath);
117
116
  const replacements = [];
118
117
  const variableRenames = new Map();
118
+ let hasUnmigratedClient = false;
119
119
  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)) {
120
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'V2QueryClient') {
121
+ const configArgument = node.arguments?.[0];
122
+ if (!configArgument || !ts.isObjectLiteralExpression(configArgument)) {
123
+ hasUnmigratedClient = true;
124
+ report.addManualReview({
125
+ title: 'Migrate a V2QueryClient whose config is not an object literal',
126
+ summary: `\`${node.getText(sourceFile)}\` passes its config indirectly, so the migration cannot read it and left the client on v2.`,
127
+ 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.',
128
+ locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
129
+ source: 'query-client-migration',
130
+ dedupeKey: `unmigrated-client:${filePath}:${node.getStart(sourceFile)}`,
131
+ });
132
+ }
133
+ else {
127
134
  const oldVariableName = getVariableNameForQueryClient(node);
128
- const newVariableName = ensureConfigSuffix(oldVariableName);
129
- if (oldVariableName !== newVariableName) {
135
+ const newVariableName = oldVariableName && ensureConfigSuffix(oldVariableName);
136
+ if (oldVariableName && newVariableName && oldVariableName !== newVariableName) {
130
137
  const collisionRegex = new RegExp(`\\b(?:const|let|var)\\s+${newVariableName}\\b`);
131
138
  if (collisionRegex.test(content)) {
132
139
  report.addWarning({
@@ -143,6 +150,7 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
143
150
  renamesOut.set(oldVariableName, newVariableName);
144
151
  }
145
152
  }
153
+ reportDroppedClientOptions({ configArgument, sourceFile, filePath, report });
146
154
  replacements.push({
147
155
  start: node.getStart(sourceFile),
148
156
  end: node.getEnd(),
@@ -158,16 +166,20 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
158
166
  replacements.forEach(({ start, end, replacement }) => {
159
167
  result = result.slice(0, start) + replacement + result.slice(end);
160
168
  });
161
- result = removeQueryClientImport(result);
169
+ result = removeQueryClientImport(result, hasUnmigratedClient);
162
170
  return renameVariables(result, variableRenames);
163
171
  };
164
- const removeQueryClientImport = (content) => {
172
+ const removeQueryClientImport = (content, keepLegacyClient) => {
165
173
  const sourceFile = createSourceFile(content);
166
174
  let queryImportNode;
167
175
  ts.forEachChild(sourceFile, (node) => {
168
- if (ts.isImportDeclaration(node) &&
176
+ if (!queryImportNode &&
177
+ ts.isImportDeclaration(node) &&
169
178
  ts.isStringLiteral(node.moduleSpecifier) &&
170
- node.moduleSpecifier.text === '@ethlete/query') {
179
+ node.moduleSpecifier.text === '@ethlete/query' &&
180
+ node.importClause?.namedBindings &&
181
+ ts.isNamedImports(node.importClause.namedBindings) &&
182
+ node.importClause.namedBindings.elements.some((element) => element.name.text === 'V2QueryClient')) {
171
183
  queryImportNode = node;
172
184
  }
173
185
  });
@@ -183,10 +195,10 @@ const removeQueryClientImport = (content) => {
183
195
  return ensureImportFromQuery(content, requiredImports);
184
196
  }
185
197
  const existingElements = queryImportNode.importClause.namedBindings.elements.filter((element) => {
186
- return element.name.text !== 'V2QueryClient';
198
+ return keepLegacyClient || element.name.text !== 'V2QueryClient';
187
199
  });
188
200
  const nextImports = new Set(requiredImports);
189
- existingElements.forEach((element) => nextImports.add(element.name.text));
201
+ existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
190
202
  const nextImportStatement = `import { ${Array.from(nextImports).sort().join(', ')} } from '@ethlete/query';`;
191
203
  return (content.slice(0, queryImportNode.getStart(sourceFile)) +
192
204
  nextImportStatement +
@@ -241,40 +253,113 @@ const renameVariables = (content, renames) => {
241
253
  });
242
254
  return result;
243
255
  };
256
+ /**
257
+ * What each v2 client option turns into, or why it has nowhere to go.
258
+ *
259
+ * Silently dropping these is how two apps here lost their default headers without a single warning.
260
+ * A dropped option that shows up in the report is a five-minute fix; one that doesn't is a bug
261
+ * report from production.
262
+ */
263
+ 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.',
266
+ 'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
267
+ 'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
268
+ 'request.autoRefreshOn': 'No direct equivalent. Re-run the affected queries yourself with `withAutoRefresh` or `refreshQueriesInUse()`.',
269
+ };
270
+ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, report, }) => {
271
+ const migrated = new Set(['baseRoute', 'queryParams', 'cacheAdapter', 'retryFn', 'request', 'logging']);
272
+ const raise = (optionPath, node) => {
273
+ report.addWarning({
274
+ title: `Reconfigure dropped query client option "${optionPath}"`,
275
+ summary: `The v2 client option \`${optionPath}\` has no place in \`createQueryClient\` and was dropped by the migration.`,
276
+ action: DROPPED_CLIENT_OPTIONS[optionPath] ?? 'Check whether the behaviour is still needed and reimplement it.',
277
+ locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
278
+ source: 'query-client-migration',
279
+ dedupeKey: `dropped-client-option:${filePath}:${optionPath}`,
280
+ });
281
+ };
282
+ const raiseSpread = (spread) => {
283
+ const text = spread.getText(sourceFile);
284
+ report.addWarning({
285
+ title: `Reconfigure spread query client options "${text}"`,
286
+ summary: `The v2 client config spreads \`${text}\`. The migration cannot see which options it holds, so none of them were carried over to \`createQueryClient\`.`,
287
+ action: 'Inline the spread options that are still needed (`baseRoute` becomes `baseUrl`) and re-check the result.',
288
+ locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(spread.getStart(sourceFile)).line + 1 }],
289
+ source: 'query-client-migration',
290
+ dedupeKey: `dropped-client-spread:${filePath}:${spread.getStart(sourceFile)}`,
291
+ });
292
+ };
293
+ configArgument.properties.forEach((property) => {
294
+ if (ts.isSpreadAssignment(property)) {
295
+ raiseSpread(property);
296
+ return;
297
+ }
298
+ if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) ||
299
+ !ts.isIdentifier(property.name)) {
300
+ return;
301
+ }
302
+ const name = property.name.text;
303
+ if ((name === 'request' || name === 'logging') &&
304
+ ts.isPropertyAssignment(property) &&
305
+ ts.isObjectLiteralExpression(property.initializer)) {
306
+ property.initializer.properties.forEach((nested) => {
307
+ if (ts.isSpreadAssignment(nested)) {
308
+ raiseSpread(nested);
309
+ return;
310
+ }
311
+ if ((!ts.isPropertyAssignment(nested) && !ts.isShorthandPropertyAssignment(nested)) ||
312
+ !ts.isIdentifier(nested.name)) {
313
+ return;
314
+ }
315
+ if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
316
+ return;
317
+ }
318
+ raise(`${name}.${nested.name.text}`, nested);
319
+ });
320
+ return;
321
+ }
322
+ if (!migrated.has(name)) {
323
+ raise(name, property);
324
+ }
325
+ });
326
+ };
327
+ const getPropertyValueText = (property, sourceFile) => {
328
+ if (ts.isShorthandPropertyAssignment(property)) {
329
+ return property.name.text;
330
+ }
331
+ return ts.isPropertyAssignment(property) ? property.initializer.getText(sourceFile) : undefined;
332
+ };
333
+ const findProperty = (objectLiteral, name) => objectLiteral.properties.find((property) => {
334
+ return ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
335
+ ts.isIdentifier(property.name) &&
336
+ property.name.text === name);
337
+ });
244
338
  const migrateConfigObject = (configArgument, node, sourceFile) => {
245
- const variableName = getVariableNameForQueryClient(node);
339
+ const variableName = getVariableNameForQueryClient(node) ?? 'client';
246
340
  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}`);
341
+ const baseRouteProperty = findProperty(configArgument, 'baseRoute');
342
+ const baseRouteText = baseRouteProperty && getPropertyValueText(baseRouteProperty, sourceFile);
343
+ if (baseRouteText) {
344
+ nextConfig.push(`baseUrl: ${baseRouteText.replace('as `https://${string}`', '').trim()}`);
256
345
  }
257
346
  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
- });
347
+ const requestProperty = findProperty(configArgument, 'request');
261
348
  if (requestProperty &&
262
349
  ts.isPropertyAssignment(requestProperty) &&
263
350
  ts.isObjectLiteralExpression(requestProperty.initializer)) {
264
- requestProperty.initializer.properties.forEach((property) => {
265
- if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
266
- return;
351
+ const requestConfig = requestProperty.initializer;
352
+ for (const [from, to] of [
353
+ ['queryParams', 'queryString'],
354
+ ['cacheAdapter', 'cacheAdapter'],
355
+ ['retryFn', 'retryFn'],
356
+ ]) {
357
+ const property = findProperty(requestConfig, from);
358
+ const valueText = property && getPropertyValueText(property, sourceFile);
359
+ if (valueText) {
360
+ nextConfig.push(`${to}: ${valueText}`);
267
361
  }
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
- });
362
+ }
278
363
  }
279
364
  return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
280
365
  };
@@ -286,7 +371,7 @@ const getVariableNameForQueryClient = (node) => {
286
371
  }
287
372
  parent = parent.parent;
288
373
  }
289
- return 'client';
374
+ return undefined;
290
375
  };
291
376
  const extractClientConfigNames = (content) => {
292
377
  const sourceFile = createSourceFile(content);
@@ -326,19 +411,20 @@ const addProviderAliasesToFile = (content, configNames) => {
326
411
  return;
327
412
  }
328
413
  const { injectName, provideName } = getProviderAliasNames(variableName);
329
- if (result.includes(`export const [${provideName}, ${injectName}] = ${variableName};`)) {
414
+ if (result.includes(`export const ${injectName} = toInjectFn(${variableName});`)) {
330
415
  return;
331
416
  }
332
417
  insertions.push({
333
418
  position,
334
- text: `\n\nexport const [${provideName}, ${injectName}] = ${variableName};`,
419
+ text: `\n\nexport const ${provideName} = toProvideFn(${variableName});` +
420
+ `\nexport const ${injectName} = toInjectFn(${variableName});`,
335
421
  });
336
422
  });
337
423
  insertions.sort((left, right) => right.position - left.position);
338
424
  insertions.forEach(({ position, text }) => {
339
425
  result = result.slice(0, position) + text + result.slice(position);
340
426
  });
341
- return result;
427
+ return insertions.length > 0 ? ensureImportFromEthleteCore(result, ['toInjectFn', 'toProvideFn']) : result;
342
428
  };
343
429
  const getProviderAliasNames = (configVariableName) => {
344
430
  const baseName = configVariableName.replace(/Config$/, '');
@@ -384,6 +470,19 @@ const generateCreatorsForConfig = (configName) => {
384
470
  `export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
385
471
  ].join('\n');
386
472
  };
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
+ };
387
486
  const updateImportsInFile = (content, renames) => {
388
487
  const sourceFile = createSourceFile(content);
389
488
  const replacements = [];
@@ -394,7 +493,7 @@ const updateImportsInFile = (content, renames) => {
394
493
  const nextElements = [];
395
494
  let hasChanges = false;
396
495
  node.importClause.namedBindings.elements.forEach((element) => {
397
- const nextName = renames.get(element.name.text);
496
+ const nextName = renames.get(element.propertyName?.text ?? element.name.text);
398
497
  if (!nextName) {
399
498
  nextElements.push(element.getText(sourceFile));
400
499
  return;
@@ -445,7 +544,7 @@ const updateImportsInFile = (content, renames) => {
445
544
  ts.forEachChild(node, visit);
446
545
  return;
447
546
  }
448
- if (ts.isPropertyAssignment(parent) && parent.name === node) {
547
+ if ((ts.isPropertyAssignment(parent) && parent.name === node) || isShadowedByLocalDeclaration(node)) {
449
548
  ts.forEachChild(node, visit);
450
549
  return;
451
550
  }
@@ -0,0 +1,206 @@
1
+ import * as ts from 'typescript';
2
+ import { createSourceFile } from './shared.js';
3
+ /**
4
+ * Whether an identifier is a *name* rather than a *reference* - a declaration, a property key, the
5
+ * right-hand side of a member access. Renaming those is what turned `PeopleDetailDataSource.getPerson`
6
+ * and the `postLogin` property of a config type into `legacy…` during the first migration run.
7
+ */
8
+ const isNamePosition = (node) => {
9
+ const parent = node.parent;
10
+ if (!parent)
11
+ return false;
12
+ // `foo.bar` / `Foo.Bar` - the part after the dot is never the imported binding.
13
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node)
14
+ return true;
15
+ if (ts.isQualifiedName(parent) && parent.right === node)
16
+ return true;
17
+ // Object literal / type member keys, enum members, JSX-ish attribute names.
18
+ if (ts.isPropertyAssignment(parent) && parent.name === node)
19
+ return true;
20
+ if (ts.isPropertySignature(parent) && parent.name === node)
21
+ return true;
22
+ if (ts.isMethodSignature(parent) && parent.name === node)
23
+ return true;
24
+ if (ts.isEnumMember(parent) && parent.name === node)
25
+ return true;
26
+ if (ts.isBindingElement(parent) && parent.propertyName === node)
27
+ return true;
28
+ // Declarations of any kind.
29
+ if (ts.isVariableDeclaration(parent) && parent.name === node)
30
+ return true;
31
+ if (ts.isFunctionDeclaration(parent) && parent.name === node)
32
+ return true;
33
+ if (ts.isClassDeclaration(parent) && parent.name === node)
34
+ return true;
35
+ if (ts.isInterfaceDeclaration(parent) && parent.name === node)
36
+ return true;
37
+ if (ts.isTypeAliasDeclaration(parent) && parent.name === node)
38
+ return true;
39
+ if (ts.isEnumDeclaration(parent) && parent.name === node)
40
+ return true;
41
+ if (ts.isModuleDeclaration(parent) && parent.name === node)
42
+ return true;
43
+ if (ts.isMethodDeclaration(parent) && parent.name === node)
44
+ return true;
45
+ if (ts.isPropertyDeclaration(parent) && parent.name === node)
46
+ return true;
47
+ if (ts.isGetAccessorDeclaration(parent) && parent.name === node)
48
+ return true;
49
+ if (ts.isSetAccessorDeclaration(parent) && parent.name === node)
50
+ return true;
51
+ if (ts.isParameter(parent) && parent.name === node)
52
+ return true;
53
+ if (ts.isTypeParameterDeclaration(parent) && parent.name === node)
54
+ return true;
55
+ if (ts.isLabeledStatement(parent) && parent.label === node)
56
+ return true;
57
+ // Import / export clauses are rewritten by the caller, never by the reference pass.
58
+ if (ts.isImportSpecifier(parent))
59
+ return true;
60
+ if (ts.isExportSpecifier(parent) &&
61
+ (parent.parent.parent.moduleSpecifier || (parent.propertyName && parent.name === node))) {
62
+ return true;
63
+ }
64
+ if (ts.isImportClause(parent) || ts.isNamespaceImport(parent))
65
+ return true;
66
+ return false;
67
+ };
68
+ const renamedReference = (node, nextName) => {
69
+ const parent = node.parent;
70
+ if (ts.isShorthandPropertyAssignment(parent))
71
+ return `${node.text}: ${nextName}`;
72
+ if (ts.isExportSpecifier(parent) && !parent.propertyName)
73
+ return `${nextName} as ${node.text}`;
74
+ return nextName;
75
+ };
76
+ /** Every name the file declares itself - a local by the same name means the import is shadowed. */
77
+ const collectLocalDeclarations = (sourceFile) => {
78
+ const declared = new Set();
79
+ const add = (name) => {
80
+ if (name && ts.isIdentifier(name))
81
+ declared.add(name.text);
82
+ };
83
+ const visit = (node) => {
84
+ if (ts.isVariableDeclaration(node))
85
+ add(node.name);
86
+ if (ts.isFunctionDeclaration(node))
87
+ add(node.name);
88
+ if (ts.isClassDeclaration(node))
89
+ add(node.name);
90
+ if (ts.isInterfaceDeclaration(node))
91
+ add(node.name);
92
+ if (ts.isTypeAliasDeclaration(node))
93
+ add(node.name);
94
+ if (ts.isEnumDeclaration(node))
95
+ add(node.name);
96
+ if (ts.isParameter(node))
97
+ add(node.name);
98
+ if (ts.isBindingElement(node))
99
+ add(node.name);
100
+ ts.forEachChild(node, visit);
101
+ };
102
+ visit(sourceFile);
103
+ return declared;
104
+ };
105
+ /**
106
+ * Renames references to imported bindings, and only those.
107
+ *
108
+ * `renames` maps a *local binding name* to its new name. The caller is responsible for having
109
+ * verified that the binding really is the symbol being migrated (see `ModuleGraph`); this pass only
110
+ * makes sure the rewrite lands on references rather than on same-named properties, declarations or
111
+ * type members. A name the file also declares locally is skipped entirely and reported back, since
112
+ * a rename there would silently change which value the code reads.
113
+ */
114
+ export const renameImportedReferences = (content, renames) => {
115
+ if (renames.size === 0)
116
+ return { content, shadowed: [] };
117
+ const sourceFile = createSourceFile(content);
118
+ const declaredLocally = collectLocalDeclarations(sourceFile);
119
+ const shadowed = Array.from(renames.keys()).filter((name) => declaredLocally.has(name));
120
+ const applicable = new Map(Array.from(renames.entries()).filter(([name]) => !declaredLocally.has(name)));
121
+ if (applicable.size === 0)
122
+ return { content, shadowed };
123
+ const replacements = [];
124
+ const visit = (node) => {
125
+ if (ts.isIdentifier(node)) {
126
+ const nextName = applicable.get(node.text);
127
+ if (nextName && !isNamePosition(node)) {
128
+ replacements.push({
129
+ start: node.getStart(sourceFile),
130
+ end: node.getEnd(),
131
+ replacement: renamedReference(node, nextName),
132
+ });
133
+ }
134
+ }
135
+ ts.forEachChild(node, visit);
136
+ };
137
+ visit(sourceFile);
138
+ let result = content;
139
+ replacements.sort((left, right) => right.start - left.start);
140
+ replacements.forEach(({ start, end, replacement }) => {
141
+ result = result.slice(0, start) + replacement + result.slice(end);
142
+ });
143
+ return { content: result, shadowed };
144
+ };
145
+ /**
146
+ * Drops named imports whose local name no longer appears anywhere else in the file.
147
+ *
148
+ * The migration rewrites away the constructs that needed `def`, `AnyLegacyQueryCreator` and friends;
149
+ * leaving the imports behind turns every touched file into a lint error, and `formatFiles` runs in
150
+ * the same generator invocation so they land that way. Identifiers inside template strings count as
151
+ * usage too - a component referenced only from an inline template must not be pruned.
152
+ */
153
+ export const pruneUnusedNamedImports = (content) => {
154
+ const sourceFile = createSourceFile(content);
155
+ const used = new Set();
156
+ const addWordsFrom = (text) => {
157
+ for (const word of text.match(/[A-Za-z_$][\w$]*/g) ?? []) {
158
+ used.add(word);
159
+ }
160
+ };
161
+ const visit = (node) => {
162
+ if (ts.isIdentifier(node) && !ts.isImportSpecifier(node.parent) && !ts.isImportClause(node.parent)) {
163
+ used.add(node.text);
164
+ }
165
+ // A symbol referenced only from an inline template or a `styleUrl`-style string is still used.
166
+ if (ts.isStringLiteralLike(node) && !ts.isImportDeclaration(node.parent)) {
167
+ addWordsFrom(node.text);
168
+ }
169
+ if (ts.isTemplateLiteralToken(node)) {
170
+ addWordsFrom(node.text);
171
+ }
172
+ ts.forEachChild(node, visit);
173
+ };
174
+ visit(sourceFile);
175
+ const replacements = [];
176
+ for (const node of sourceFile.statements) {
177
+ if (!ts.isImportDeclaration(node) ||
178
+ !ts.isStringLiteral(node.moduleSpecifier) ||
179
+ !node.importClause?.namedBindings ||
180
+ !ts.isNamedImports(node.importClause.namedBindings)) {
181
+ continue;
182
+ }
183
+ const kept = node.importClause.namedBindings.elements.filter((element) => used.has(element.name.text));
184
+ if (kept.length === node.importClause.namedBindings.elements.length)
185
+ continue;
186
+ const hasDefaultImport = !!node.importClause.name;
187
+ if (kept.length === 0 && !hasDefaultImport) {
188
+ const end = content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
189
+ replacements.push({ start: node.getStart(sourceFile), end, replacement: '' });
190
+ continue;
191
+ }
192
+ const defaultPart = hasDefaultImport ? `${node.importClause.name.text}, ` : '';
193
+ replacements.push({
194
+ start: node.getStart(sourceFile),
195
+ end: node.getEnd(),
196
+ replacement: `import ${defaultPart}{ ${kept.map((element) => element.getText(sourceFile)).join(', ')} } from '${node.moduleSpecifier.text}';`,
197
+ });
198
+ }
199
+ let result = content;
200
+ replacements.sort((left, right) => right.start - left.start);
201
+ replacements.forEach(({ start, end, replacement }) => {
202
+ result = result.slice(0, start) + replacement + result.slice(end);
203
+ });
204
+ return result;
205
+ };
206
+ //# sourceMappingURL=rename-symbols.js.map