@ethlete/query 6.0.0-next.19 → 6.0.0-next.20

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.
@@ -1,5 +1,7 @@
1
- import { visitNotIgnoredFiles } from '@nx/devkit';
2
1
  import * as ts from 'typescript';
2
+ import { collectV2BearerAuthConfigs, renderAuthProviderBody } from './auth-provider-scaffold.js';
3
+ import { createModuleGraph } from './module-graph.js';
4
+ import { pruneUnusedNamedImports, renameImportedReferences } from './rename-symbols.js';
3
5
  import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromQuery } from './shared.js';
4
6
  const LEGACY_HTTP_METHOD = {
5
7
  GET: 'get',
@@ -11,16 +13,17 @@ const LEGACY_HTTP_METHOD = {
11
13
  const isLegacyHttpMethod = (value) => {
12
14
  return Object.values(LEGACY_HTTP_METHOD).includes(value);
13
15
  };
14
- export const createNewQueryCreators = (tree, queryClientFiles, report) => {
16
+ export const createNewQueryCreators = (tree, queryClientFiles, report, scope) => {
15
17
  const createdFiles = [];
16
18
  const authProvidersNeeded = new Map();
17
19
  const clientNameToFilePath = new Map();
20
+ const creatorToClient = new Map();
18
21
  queryClientFiles.forEach((configNames, filePath) => {
19
22
  configNames.forEach((configName) => {
20
23
  clientNameToFilePath.set(configName, filePath);
21
24
  });
22
25
  });
23
- visitNotIgnoredFiles(tree, '', (filePath) => {
26
+ scope.visit(tree, (filePath) => {
24
27
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
25
28
  return;
26
29
  }
@@ -33,6 +36,7 @@ export const createNewQueryCreators = (tree, queryClientFiles, report) => {
33
36
  return;
34
37
  }
35
38
  legacyCreators.forEach((creator) => {
39
+ creatorToClient.set(creator.name, creator.clientName);
36
40
  if (!creator.secure) {
37
41
  return;
38
42
  }
@@ -48,17 +52,23 @@ export const createNewQueryCreators = (tree, queryClientFiles, report) => {
48
52
  }
49
53
  });
50
54
  if (authProvidersNeeded.size > 0) {
51
- createAuthProviders(tree, authProvidersNeeded, report);
55
+ createAuthProviders({
56
+ tree,
57
+ authProvidersNeeded,
58
+ report,
59
+ v2Configs: collectV2BearerAuthConfigs(tree, scope),
60
+ creatorToClient,
61
+ });
52
62
  }
53
63
  if (createdFiles.length > 0) {
54
64
  console.log('\n✅ Created new query creators in:');
55
65
  createdFiles.forEach((filePath) => console.log(` - ${filePath}`));
56
66
  }
57
67
  };
58
- export const updateLegacyCreatorImportsAndUsages = (tree) => {
68
+ export const updateLegacyCreatorImportsAndUsages = (tree, scope, report) => {
59
69
  const legacyCreators = new Map();
60
70
  const updatedFiles = [];
61
- visitNotIgnoredFiles(tree, '', (filePath) => {
71
+ scope.visit(tree, (filePath) => {
62
72
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
63
73
  return;
64
74
  }
@@ -66,16 +76,16 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
66
76
  if (!content) {
67
77
  return;
68
78
  }
69
- const wrappers = findLegacyWrappers(content);
70
- wrappers.forEach((legacyName, originalName) => {
71
- legacyCreators.set(originalName, legacyName);
79
+ findLegacyWrappers(content).forEach((legacyName, originalName) => {
80
+ legacyCreators.set(originalName, { legacyName, definedIn: filePath });
72
81
  });
73
82
  });
74
83
  if (legacyCreators.size === 0) {
75
84
  return;
76
85
  }
77
86
  console.log(`\n✅ Found ${legacyCreators.size} legacy query creator wrappers`);
78
- visitNotIgnoredFiles(tree, '', (filePath) => {
87
+ const moduleGraph = createModuleGraph(tree);
88
+ scope.visit(tree, (filePath) => {
79
89
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
80
90
  return;
81
91
  }
@@ -83,8 +93,13 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
83
93
  if (!content || containsLegacyWrapperDefinitions(content)) {
84
94
  return;
85
95
  }
86
- const nextImports = updateLegacyCreatorImports(content, legacyCreators);
87
- const nextContent = updateLegacyCreatorUsages(nextImports, legacyCreators);
96
+ const nextContent = pointFileAtLegacyWrappers({
97
+ content,
98
+ filePath,
99
+ legacyCreators,
100
+ moduleGraph,
101
+ report,
102
+ });
88
103
  if (nextContent !== content) {
89
104
  tree.write(filePath, nextContent);
90
105
  updatedFiles.push(filePath);
@@ -95,6 +110,89 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
95
110
  updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
96
111
  }
97
112
  };
113
+ /**
114
+ * Rewrites this file's imports of migrated creators to the `legacy*` wrappers.
115
+ *
116
+ * Two rules keep the rewrite honest, and both exist because the first version of this pass did not
117
+ * have them:
118
+ *
119
+ * - An import is only touched when the module it comes from really does resolve to the file that
120
+ * defines the wrapper. A same-named export from an unrelated package stays as it is.
121
+ * - An alias is preserved (`legacyGetPerson as getPersonQuery`) instead of being dropped, so the
122
+ * call sites using that alias keep compiling.
123
+ */
124
+ const pointFileAtLegacyWrappers = ({ content, filePath, legacyCreators, moduleGraph, report, }) => {
125
+ const sourceFile = createSourceFile(content, filePath);
126
+ const replacements = [];
127
+ const referenceRenames = new Map();
128
+ const resolvesToWrapper = (specifier, importedName, registration) => moduleGraph.findDeclaringFile(filePath, specifier, importedName) === registration.definedIn;
129
+ const rewriteSpecifiers = (elements, moduleSpecifier) => {
130
+ let changed = false;
131
+ const nextElements = elements.map((element) => {
132
+ const importedName = element.propertyName?.text ?? element.name.text;
133
+ const registration = legacyCreators.get(importedName);
134
+ if (!registration || !resolvesToWrapper(moduleSpecifier, importedName, registration)) {
135
+ return element.getText(sourceFile);
136
+ }
137
+ changed = true;
138
+ // Aliased: only the imported half changes, so nothing in the file body has to move.
139
+ if (element.propertyName) {
140
+ return `${registration.legacyName} as ${element.name.text}`;
141
+ }
142
+ referenceRenames.set(element.name.text, registration.legacyName);
143
+ return registration.legacyName;
144
+ });
145
+ return changed ? nextElements : null;
146
+ };
147
+ for (const node of sourceFile.statements) {
148
+ if (ts.isImportDeclaration(node) &&
149
+ ts.isStringLiteral(node.moduleSpecifier) &&
150
+ node.importClause?.namedBindings &&
151
+ ts.isNamedImports(node.importClause.namedBindings)) {
152
+ const nextElements = rewriteSpecifiers(node.importClause.namedBindings.elements, node.moduleSpecifier.text);
153
+ if (nextElements) {
154
+ replacements.push({
155
+ start: node.getStart(sourceFile),
156
+ end: node.getEnd(),
157
+ replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
158
+ });
159
+ }
160
+ continue;
161
+ }
162
+ // Re-exports need the same treatment, or a barrel keeps pointing at the v3 creator.
163
+ if (ts.isExportDeclaration(node) &&
164
+ node.moduleSpecifier &&
165
+ ts.isStringLiteral(node.moduleSpecifier) &&
166
+ node.exportClause &&
167
+ ts.isNamedExports(node.exportClause)) {
168
+ const nextElements = rewriteSpecifiers(node.exportClause.elements, node.moduleSpecifier.text);
169
+ if (nextElements) {
170
+ replacements.push({
171
+ start: node.getStart(sourceFile),
172
+ end: node.getEnd(),
173
+ replacement: `export { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
174
+ });
175
+ }
176
+ }
177
+ }
178
+ let result = content;
179
+ replacements.sort((left, right) => right.start - left.start);
180
+ replacements.forEach(({ start, end, replacement }) => {
181
+ result = result.slice(0, start) + replacement + result.slice(end);
182
+ });
183
+ const renamed = renameImportedReferences(result, referenceRenames);
184
+ renamed.shadowed.forEach((name) => {
185
+ report.addManualReview({
186
+ title: `Point ${name} at its legacy wrapper by hand`,
187
+ summary: `${filePath} imports the migrated creator \`${name}\` but also declares something with that name, so the migration left its usages alone to avoid rewriting the wrong symbol.`,
188
+ action: `Rename the local declaration or the import alias, then point the query usages at \`${legacyCreators.get(name)?.legacyName ?? `legacy${capitalizeFirstLetter(name)}`}\`.`,
189
+ locations: [{ filePath }],
190
+ source: 'legacy-query-creator-migration',
191
+ dedupeKey: `shadowed-rename:${filePath}:${name}`,
192
+ });
193
+ });
194
+ return renamed.content;
195
+ };
98
196
  const toLegacyName = (name) => `legacy${capitalizeFirstLetter(name)}`;
99
197
  const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report) => {
100
198
  const sourceFile = createSourceFile(content, filePath);
@@ -233,7 +331,10 @@ const transformLegacyQueryCreators = (content, legacyCreators) => {
233
331
  result = result.slice(0, start) + replacement + result.slice(end);
234
332
  });
235
333
  result = ensureImportFromQuery(result, ['createLegacyQueryCreator']);
236
- return addCreatorImports(result, creatorsByClient);
334
+ result = addCreatorImports(result, creatorsByClient);
335
+ // The rewritten creators no longer go through `def<T>()` or the v2 client object, so whatever is
336
+ // left over from those constructs would land as a lint error the moment `formatFiles` runs.
337
+ return pruneUnusedNamedImports(result, 'all');
237
338
  };
238
339
  const addCreatorImports = (content, creatorsByClient) => {
239
340
  const sourceFile = createSourceFile(content);
@@ -340,9 +441,11 @@ const getTypeParameter = (creator) => {
340
441
  return '';
341
442
  };
342
443
  const generateLegacyWrapper = (creator) => {
343
- return `export const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ creator: ${creator.name} });`;
444
+ // `name` is what lets a `prepare()` called outside an injection context name itself in the error
445
+ // instead of surfacing as a bare NG0203.
446
+ return `export const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ name: '${toLegacyName(creator.name)}', creator: ${creator.name} });`;
344
447
  };
345
- const createAuthProviders = (tree, authProvidersNeeded, report) => {
448
+ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, creatorToClient, }) => {
346
449
  const createdProviders = [];
347
450
  authProvidersNeeded.forEach((filePath, clientName) => {
348
451
  const content = tree.read(filePath, 'utf-8');
@@ -353,19 +456,46 @@ const createAuthProviders = (tree, authProvidersNeeded, report) => {
353
456
  if (content.includes(`export const ${authProviderName} = createBearerAuthProvider(`)) {
354
457
  return;
355
458
  }
356
- const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName), clientName);
459
+ // Match on the refresh creator: it is the one thing a v2 provider config always names, and it
460
+ // belongs to exactly one client.
461
+ const v2Config = v2Configs.find((candidate) => candidate.refreshCreatorName && creatorToClient.get(candidate.refreshCreatorName) === clientName);
462
+ const body = renderAuthProviderBody(v2Config);
463
+ const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body), clientName);
357
464
  if (nextContent === content) {
358
465
  return;
359
466
  }
360
467
  tree.write(filePath, nextContent);
361
468
  createdProviders.push(filePath);
362
- report.addFollowUp({
363
- title: `Configure auth queries for ${authProviderName}`,
364
- summary: `The migration created ${authProviderName} with an empty \`queries\` array because it cannot infer login or refresh builders safely.`,
365
- action: 'Add authQuery/tokenRefreshQuery builders, verify token extraction, and remove the placeholder empty array once the provider is configured.',
469
+ if (body.isScaffolded) {
470
+ report.addFollowUp({
471
+ title: `Finish the scaffolded auth provider ${authProviderName}`,
472
+ summary: `${authProviderName} was scaffolded from the v2 BearerAuthProvider at ${v2Config?.filePath}:${v2Config?.line}. The refresh query and cookie config carried over; the adapters were copied verbatim and their parameter shapes changed between v2 and v3.`,
473
+ action: 'Resolve the TODO(query-v3) comments in the generated provider, add the login query via withAuthenticationQuery, and delete the v2 provider once the app logs in again.',
474
+ locations: [{ filePath }, ...(v2Config ? [{ filePath: v2Config.filePath, line: v2Config.line }] : [])],
475
+ source: 'legacy-query-creator-migration',
476
+ dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
477
+ });
478
+ }
479
+ else {
480
+ report.addFollowUp({
481
+ title: `Configure auth queries for ${authProviderName}`,
482
+ summary: `The migration created ${authProviderName} with an empty \`queries\` array because no v2 BearerAuthProvider config could be matched to this client.`,
483
+ action: 'Add authQuery/tokenRefreshQuery builders, verify token extraction, and remove the placeholder empty array once the provider is configured.',
484
+ locations: [{ filePath }],
485
+ source: 'legacy-query-creator-migration',
486
+ dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
487
+ });
488
+ }
489
+ // The generated layout only works if the auth queries live next to the provider: the provider
490
+ // needs the login/refresh creators, and the secure creators need the provider. Leaving them in
491
+ // an `auth.queries.ts` that imports the client's `post` creator closes the loop.
492
+ report.addManualReview({
493
+ title: `Keep the auth queries in ${filePath}`,
494
+ summary: `${authProviderName} needs the login and refresh creators, and the secure creators below it need the provider. If those creators stay in a separate auth queries file that imports this client, the two files form an import cycle.`,
495
+ action: `Move the login/refresh creators into ${filePath} above the provider, and have the auth queries file import them back for its legacy* wrappers.`,
366
496
  locations: [{ filePath }],
367
497
  source: 'legacy-query-creator-migration',
368
- dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
498
+ dedupeKey: `auth-provider-cycle:${filePath}`,
369
499
  });
370
500
  });
371
501
  if (createdProviders.length > 0) {
@@ -373,8 +503,8 @@ const createAuthProviders = (tree, authProvidersNeeded, report) => {
373
503
  createdProviders.forEach((filePath) => console.log(` - ${filePath}`));
374
504
  }
375
505
  };
376
- const addAuthProviderToFile = (content, clientName) => {
377
- const nextContent = ensureImportFromQuery(content, ['createBearerAuthProvider']);
506
+ const addAuthProviderToFile = (content, clientName, body) => {
507
+ const nextContent = ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]);
378
508
  const sourceFile = createSourceFile(nextContent);
379
509
  const configName = ensureConfigSuffix(clientName);
380
510
  const configPosition = findVariableStatementEnd(sourceFile, configName, 'createQueryClient');
@@ -388,7 +518,8 @@ const addAuthProviderToFile = (content, clientName) => {
388
518
  `export const ${authProviderName} = createBearerAuthProvider({`,
389
519
  ` name: '${clientName}',`,
390
520
  ` queryClientRef: ${configName},`,
391
- ` queries: [],`,
521
+ body.queries,
522
+ ...(body.features ? [body.features] : []),
392
523
  '});',
393
524
  '',
394
525
  `export const [provide${capitalizedClientName}AuthProvider, inject${capitalizedClientName}AuthProvider] = ${authProviderName};`,
@@ -473,117 +604,4 @@ const findLegacyWrappers = (content) => {
473
604
  visit(sourceFile);
474
605
  return wrappers;
475
606
  };
476
- const updateLegacyCreatorImports = (content, legacyCreators) => {
477
- const sourceFile = createSourceFile(content);
478
- const replacements = [];
479
- const visit = (node) => {
480
- if (ts.isImportDeclaration(node) &&
481
- node.importClause?.namedBindings &&
482
- ts.isNamedImports(node.importClause.namedBindings)) {
483
- const nextElements = node.importClause.namedBindings.elements.map((element) => {
484
- const importedName = element.propertyName ? element.propertyName.text : element.name.text;
485
- const legacyName = legacyCreators.get(importedName);
486
- return legacyName ?? element.getText(sourceFile);
487
- });
488
- const hasChanges = node.importClause.namedBindings.elements.some((element) => {
489
- const importedName = element.propertyName ? element.propertyName.text : element.name.text;
490
- return legacyCreators.has(importedName);
491
- });
492
- if (hasChanges) {
493
- replacements.push({
494
- start: node.getStart(sourceFile),
495
- end: node.getEnd(),
496
- replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
497
- });
498
- }
499
- }
500
- ts.forEachChild(node, visit);
501
- };
502
- visit(sourceFile);
503
- let result = content;
504
- replacements.sort((left, right) => right.start - left.start);
505
- replacements.forEach(({ start, end, replacement }) => {
506
- result = result.slice(0, start) + replacement + result.slice(end);
507
- });
508
- return result;
509
- };
510
- const updateLegacyCreatorUsages = (content, legacyCreators) => {
511
- const sourceFile = createSourceFile(content);
512
- const replacements = [];
513
- const visit = (node) => {
514
- if (ts.isObjectLiteralExpression(node)) {
515
- node.properties.forEach((property) => {
516
- if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) {
517
- const nextName = legacyCreators.get(property.initializer.text);
518
- if (nextName) {
519
- replacements.push({
520
- start: property.initializer.getStart(sourceFile),
521
- end: property.initializer.getEnd(),
522
- replacement: nextName,
523
- });
524
- }
525
- }
526
- if (ts.isShorthandPropertyAssignment(property) && legacyCreators.has(property.name.text)) {
527
- replacements.push({
528
- start: property.getStart(sourceFile),
529
- end: property.getEnd(),
530
- replacement: `${property.name.text}: ${legacyCreators.get(property.name.text)}`,
531
- });
532
- }
533
- });
534
- }
535
- if (ts.isIdentifier(node) && legacyCreators.has(node.text)) {
536
- const parent = node.parent;
537
- if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
538
- ts.forEachChild(node, visit);
539
- return;
540
- }
541
- if (ts.isPropertyAssignment(parent) && (parent.name === node || parent.initializer === node)) {
542
- ts.forEachChild(node, visit);
543
- return;
544
- }
545
- if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
546
- ts.forEachChild(node, visit);
547
- return;
548
- }
549
- if (ts.isVariableDeclaration(parent) && parent.name === node) {
550
- ts.forEachChild(node, visit);
551
- return;
552
- }
553
- if (ts.isImportSpecifier(parent)) {
554
- ts.forEachChild(node, visit);
555
- return;
556
- }
557
- if (ts.isFunctionDeclaration(parent) && parent.name === node) {
558
- ts.forEachChild(node, visit);
559
- return;
560
- }
561
- if (ts.isMethodDeclaration(parent) && parent.name === node) {
562
- ts.forEachChild(node, visit);
563
- return;
564
- }
565
- if (ts.isParameter(parent) && parent.name === node) {
566
- ts.forEachChild(node, visit);
567
- return;
568
- }
569
- if (ts.isPropertyDeclaration(parent) && parent.name === node) {
570
- ts.forEachChild(node, visit);
571
- return;
572
- }
573
- replacements.push({
574
- start: node.getStart(sourceFile),
575
- end: node.getEnd(),
576
- replacement: legacyCreators.get(node.text),
577
- });
578
- }
579
- ts.forEachChild(node, visit);
580
- };
581
- visit(sourceFile);
582
- let result = content;
583
- replacements.sort((left, right) => right.start - left.start);
584
- replacements.forEach(({ start, end, replacement }) => {
585
- result = result.slice(0, start) + replacement + result.slice(end);
586
- });
587
- return result;
588
- };
589
607
  //# sourceMappingURL=legacy-query-creator-migration.js.map
@@ -0,0 +1,41 @@
1
+ import { getProjects, visitNotIgnoredFiles } from '@nx/devkit';
2
+ const stripTrailingSlash = (value) => (value.endsWith('/') ? value.slice(0, -1) : value);
3
+ /**
4
+ * Limits the migration to a set of projects or paths.
5
+ *
6
+ * Rewriting every app in a monorepo in one run is rarely how adoption actually goes, and a 300-file
7
+ * diff is not reviewable. Note the one coupling that cannot be split: a query client and the
8
+ * creators built on it have to be migrated together, because the creators are rewritten in terms of
9
+ * the client's generated `xGet` / `xPostSecure` helpers.
10
+ */
11
+ export const createMigrationScope = (tree, options) => {
12
+ const roots = [];
13
+ for (const projectName of options.projects ?? []) {
14
+ const project = getProjects(tree).get(projectName);
15
+ if (!project) {
16
+ throw new Error(`Unknown project "${projectName}". Pass names as they appear in \`nx show projects\`, or use --include with a path prefix.`);
17
+ }
18
+ roots.push(stripTrailingSlash(project.root));
19
+ }
20
+ for (const includePath of options.include ?? []) {
21
+ roots.push(stripTrailingSlash(includePath));
22
+ }
23
+ if (roots.length === 0) {
24
+ return {
25
+ visit: (targetTree, callback) => visitNotIgnoredFiles(targetTree, '', callback),
26
+ includes: () => true,
27
+ describe: () => 'the whole workspace',
28
+ };
29
+ }
30
+ const includes = (filePath) => roots.some((root) => filePath === root || filePath.startsWith(`${root}/`));
31
+ return {
32
+ visit: (targetTree, callback) => {
33
+ for (const root of roots) {
34
+ visitNotIgnoredFiles(targetTree, root, callback);
35
+ }
36
+ },
37
+ includes,
38
+ describe: () => roots.join(', '),
39
+ };
40
+ };
41
+ //# sourceMappingURL=migration-scope.js.map
@@ -1,26 +1,34 @@
1
1
  import { formatFiles } from '@nx/devkit';
2
- import { migrateEmptyPrepareCalls, removeDevtoolsUsage, replaceAnyQueryWithLegacy } from './cleanup-migration.js';
2
+ import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy } from './cleanup-migration.js';
3
+ import { reportDefaultHeaderUsages, reportMissingHttpClientProviders } from './http-client-check.js';
3
4
  import { migrateLegacyPrepareCalls } from './legacy-prepare-migration.js';
4
5
  import { createNewQueryCreators, updateLegacyCreatorImportsAndUsages } from './legacy-query-creator-migration.js';
6
+ import { createMigrationScope } from './migration-scope.js';
5
7
  import { generateProviderAliases, generateQueryCreators, migrateQueryClients, updateImportsAcrossWorkspace, } from './query-client-migration.js';
6
8
  import { QueryV3MigrationReport } from './report.js';
7
9
  export default async function migrate(tree, schema) {
8
10
  console.log('\n🔄 Starting query v3 migration...');
11
+ const scope = createMigrationScope(tree, schema);
12
+ console.log(` Scope: ${scope.describe()}`);
9
13
  const report = new QueryV3MigrationReport();
10
- const { queryClientFiles, variableRenames } = migrateQueryClients(tree, report);
14
+ const { queryClientFiles, variableRenames } = migrateQueryClients(tree, report, scope);
11
15
  if (queryClientFiles.size > 0) {
12
16
  generateProviderAliases(tree, queryClientFiles);
13
17
  generateQueryCreators(tree, queryClientFiles);
14
- createNewQueryCreators(tree, queryClientFiles, report);
15
- updateLegacyCreatorImportsAndUsages(tree);
18
+ createNewQueryCreators(tree, queryClientFiles, report, scope);
19
+ updateLegacyCreatorImportsAndUsages(tree, scope, report);
16
20
  }
17
21
  if (variableRenames.size > 0) {
18
- updateImportsAcrossWorkspace(tree, variableRenames);
22
+ updateImportsAcrossWorkspace(tree, variableRenames, scope);
23
+ }
24
+ replaceAnyQueryWithLegacy(tree, scope);
25
+ migrateDevtoolsUsage(tree, scope, report);
26
+ migrateEmptyPrepareCalls(tree, scope);
27
+ migrateLegacyPrepareCalls(tree, report, scope);
28
+ if (queryClientFiles.size > 0) {
29
+ reportMissingHttpClientProviders(tree, report, scope);
30
+ reportDefaultHeaderUsages(tree, report, scope);
19
31
  }
20
- replaceAnyQueryWithLegacy(tree);
21
- removeDevtoolsUsage(tree);
22
- migrateEmptyPrepareCalls(tree);
23
- migrateLegacyPrepareCalls(tree, report);
24
32
  report.writeToTree(tree);
25
33
  report.printSummary();
26
34
  if (!schema.skipFormat) {
@@ -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