@ethlete/query 6.0.0-next.61 → 6.0.0-next.63

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 (28) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/fesm2022/ethlete-query-devtools-contract.mjs +1 -1
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +30 -7
  4. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -1
  5. package/fesm2022/ethlete-query.mjs +183 -43
  6. package/fesm2022/ethlete-query.mjs.map +1 -1
  7. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +55 -8
  8. package/generators/migrate-from-ngrx-toolkit/consumers.js +133 -37
  9. package/generators/migrate-from-ngrx-toolkit/creators.js +8 -4
  10. package/generators/migrate-from-ngrx-toolkit/feature.js +31 -4
  11. package/generators/migrate-from-ngrx-toolkit/migration.js +3 -1
  12. package/generators/migrate-from-ngrx-toolkit/module-resolution.js +23 -0
  13. package/generators/migrate-from-ngrx-toolkit/store-removal.js +64 -11
  14. package/generators/migrate-to-query-v3/auth-consumers-migration.js +64 -0
  15. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +93 -1
  16. package/generators/migrate-to-query-v3/cleanup-migration.js +135 -15
  17. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +275 -27
  18. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +171 -25
  19. package/generators/migrate-to-query-v3/migration.js +9 -3
  20. package/generators/migrate-to-query-v3/module-resolution.js +131 -0
  21. package/generators/migrate-to-query-v3/query-client-migration.js +82 -14
  22. package/generators/migrate-to-query-v3/rename-symbols.js +16 -11
  23. package/generators/move-devtools-contract-imports/moved-names.js +1 -0
  24. package/migrations.json +10 -0
  25. package/package.json +1 -1
  26. package/types/ethlete-query-devtools-contract.d.ts +2 -2
  27. package/types/ethlete-query-ngrx-toolkit.d.ts +13 -0
  28. package/types/ethlete-query.d.ts +40 -6
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { collectV2BearerAuthConfigs, renderAuthProviderBody } from './auth-provider-scaffold.js';
2
+ import { collectV2BearerAuthConfigs, rebaseModuleSpecifier, renderAuthProviderBody, } from './auth-provider-scaffold.js';
3
3
  import { createModuleGraph } from './module-graph.js';
4
4
  import { pruneUnusedNamedImports, renameImportedReferences } from './rename-symbols.js';
5
5
  import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, legacyQueryDeprecationTag, } from './shared.js';
@@ -15,7 +15,7 @@ const HTTP_OPTION_NAMES = ['reportProgress', 'responseType', 'transferCache', 'w
15
15
  const isLegacyHttpMethod = (value) => {
16
16
  return Object.values(LEGACY_HTTP_METHOD).includes(value);
17
17
  };
18
- export const createNewQueryCreators = (tree, queryClientFiles, report, scope) => {
18
+ export const createNewQueryCreators = (tree, queryClientFiles, report, scope, gqlGetClients = new Set()) => {
19
19
  const createdFiles = [];
20
20
  const authProvidersNeeded = new Map();
21
21
  const clientNameToFilePath = new Map();
@@ -33,7 +33,7 @@ export const createNewQueryCreators = (tree, queryClientFiles, report, scope) =>
33
33
  if (!content) {
34
34
  return;
35
35
  }
36
- const legacyCreators = analyzeLegacyQueryCreators(content, queryClientFiles, filePath, report);
36
+ const legacyCreators = analyzeLegacyQueryCreators(content, queryClientFiles, filePath, report, gqlGetClients);
37
37
  if (legacyCreators.length === 0) {
38
38
  return;
39
39
  }
@@ -79,7 +79,10 @@ export const updateLegacyCreatorImportsAndUsages = (tree, scope, report) => {
79
79
  return;
80
80
  }
81
81
  findLegacyWrappers(content).forEach((legacyName, originalName) => {
82
- legacyCreators.set(originalName, { legacyName, definedIn: filePath });
82
+ legacyCreators.set(originalName, [
83
+ ...(legacyCreators.get(originalName) ?? []),
84
+ { legacyName, definedIn: filePath },
85
+ ]);
83
86
  });
84
87
  });
85
88
  if (legacyCreators.size === 0) {
@@ -137,8 +140,10 @@ const pointFileAtLegacyWrappers = ({ content, filePath, legacyCreators, moduleGr
137
140
  let changed = false;
138
141
  const nextElements = elements.map((element) => {
139
142
  const importedName = element.propertyName?.text ?? element.name.text;
140
- const registration = legacyCreators.get(importedName);
141
- if (!registration || !resolvesToWrapper(moduleSpecifier, importedName, registration)) {
143
+ const registration = legacyCreators
144
+ .get(importedName)
145
+ ?.find((candidate) => resolvesToWrapper(moduleSpecifier, importedName, candidate));
146
+ if (!registration) {
142
147
  return element.getText(sourceFile);
143
148
  }
144
149
  changed = true;
@@ -192,7 +197,7 @@ const pointFileAtLegacyWrappers = ({ content, filePath, legacyCreators, moduleGr
192
197
  report.addManualReview({
193
198
  title: `Point ${name} at its legacy wrapper by hand`,
194
199
  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.`,
195
- action: `Rename the local declaration or the import alias, then point the query usages at \`${legacyCreators.get(name)?.legacyName ?? `legacy${capitalizeFirstLetter(name)}`}\`.`,
200
+ action: `Rename the local declaration or the import alias, then point the query usages at \`${legacyCreators.get(name)?.[0]?.legacyName ?? `legacy${capitalizeFirstLetter(name)}`}\`.`,
196
201
  locations: [{ filePath }],
197
202
  source: 'legacy-query-creator-migration',
198
203
  dedupeKey: `shadowed-rename:${filePath}:${name}`,
@@ -201,13 +206,17 @@ const pointFileAtLegacyWrappers = ({ content, filePath, legacyCreators, moduleGr
201
206
  return renamed.content;
202
207
  };
203
208
  const toLegacyName = (name) => `legacy${capitalizeFirstLetter(name)}`;
204
- const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report) => {
209
+ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report, gqlGetClients) => {
205
210
  const sourceFile = createSourceFile(content, filePath);
206
211
  const creators = [];
207
212
  const oldClientNames = new Set();
213
+ const renamedOwnClients = new Map();
208
214
  queryClientFiles.forEach((configNames) => {
209
215
  configNames.forEach((configName) => oldClientNames.add(configName));
210
216
  });
217
+ queryClientFiles.get(filePath)?.forEach((configName) => {
218
+ renamedOwnClients.set(ensureConfigSuffix(configName), configName);
219
+ });
211
220
  const visit = (node) => {
212
221
  if (ts.isVariableDeclaration(node) &&
213
222
  ts.isIdentifier(node.name) &&
@@ -220,24 +229,15 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
220
229
  ts.forEachChild(node, visit);
221
230
  return;
222
231
  }
223
- if (oldClientNames.has(objectExpression.text) && LEGACY_GQL_METHODS.includes(methodExpression.text)) {
224
- report.addManualReview({
225
- title: `Rewrite the GraphQL creator ${node.name.text} by hand`,
226
- summary: `${node.name.text} is built with \`${objectExpression.text}.${methodExpression.text}()\`. The migration only rewrites REST creators, so this one still calls a method the migrated client no longer has.`,
227
- action: `Rebuild it on the v3 client with \`${methodExpression.text === 'gqlQuery' ? 'createGqlQueryViaPost' : 'createGqlMutationViaPost'}\` (or the \`ViaGet\` variant) and a \`gql\` document, then wrap it in \`createLegacyQueryCreator\` if its call sites stay on v2. See https://ethlete-sdk-docs.web.app/query/gql.`,
228
- locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
229
- source: 'legacy-query-creator-migration',
230
- dedupeKey: `gql-creator:${filePath}:${node.name.text}`,
231
- });
232
- }
233
- if (!oldClientNames.has(objectExpression.text) || !isLegacyHttpMethod(methodExpression.text)) {
232
+ const clientName = renamedOwnClients.get(objectExpression.text) ?? objectExpression.text;
233
+ if (!oldClientNames.has(clientName) || !isLegacyHttpMethod(methodExpression.text)) {
234
234
  ts.forEachChild(node, visit);
235
235
  return;
236
236
  }
237
237
  const creatorName = node.name.text;
238
238
  const info = {
239
239
  name: creatorName,
240
- clientName: objectExpression.text,
240
+ clientName,
241
241
  method: methodExpression.text,
242
242
  route: '',
243
243
  secure: false,
@@ -363,8 +363,109 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
363
363
  ts.forEachChild(node, visit);
364
364
  };
365
365
  visit(sourceFile);
366
+ reportLegacyGqlCalls(sourceFile, filePath, oldClientNames, gqlGetClients, report);
366
367
  return creators;
367
368
  };
369
+ const collectClientAliases = (sourceFile, oldClientNames) => {
370
+ const aliases = new Map();
371
+ const resolve = (expression) => {
372
+ if (!ts.isIdentifier(expression)) {
373
+ return undefined;
374
+ }
375
+ return aliases.get(expression.text) ?? (oldClientNames.has(expression.text) ? expression.text : undefined);
376
+ };
377
+ const visitImports = (node) => {
378
+ if (ts.isImportSpecifier(node) && node.propertyName && oldClientNames.has(node.propertyName.text)) {
379
+ aliases.set(node.name.text, node.propertyName.text);
380
+ }
381
+ ts.forEachChild(node, visitImports);
382
+ };
383
+ visitImports(sourceFile);
384
+ const aliasDeclarations = [];
385
+ const collectDeclarations = (node) => {
386
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
387
+ aliasDeclarations.push({ name: node.name.text, initializer: node.initializer });
388
+ }
389
+ ts.forEachChild(node, collectDeclarations);
390
+ };
391
+ collectDeclarations(sourceFile);
392
+ const addAlias = ({ name, initializer }) => {
393
+ const clientName = resolve(initializer);
394
+ if (!clientName || clientName === name || aliases.has(name)) {
395
+ return false;
396
+ }
397
+ aliases.set(name, clientName);
398
+ return true;
399
+ };
400
+ while (aliasDeclarations.filter(addAlias).length > 0) {
401
+ continue;
402
+ }
403
+ return resolve;
404
+ };
405
+ const describeGqlCall = (call, sourceFile) => {
406
+ const path = [];
407
+ let node = call;
408
+ while (node.parent && !ts.isSourceFile(node.parent)) {
409
+ node = node.parent;
410
+ if (ts.isPropertyAssignment(node)) {
411
+ path.unshift(node.name.getText(sourceFile));
412
+ continue;
413
+ }
414
+ if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node)) {
415
+ if (node.name && ts.isIdentifier(node.name)) {
416
+ path.unshift(node.name.text);
417
+ }
418
+ break;
419
+ }
420
+ if (ts.isPropertyDeclaration(node) || ts.isMethodDeclaration(node)) {
421
+ path.unshift(node.name.getText(sourceFile));
422
+ if (ts.isClassLike(node.parent) && node.parent.name) {
423
+ path.unshift(node.parent.name.text);
424
+ }
425
+ break;
426
+ }
427
+ }
428
+ return path.length > 0
429
+ ? path.join('.')
430
+ : `at line ${sourceFile.getLineAndCharacterOfPosition(call.getStart(sourceFile)).line + 1}`;
431
+ };
432
+ const reportLegacyGqlCalls = (sourceFile, filePath, oldClientNames, gqlGetClients, report) => {
433
+ const resolveClient = collectClientAliases(sourceFile, oldClientNames);
434
+ const visit = (node) => {
435
+ if (ts.isCallExpression(node) &&
436
+ ts.isPropertyAccessExpression(node.expression) &&
437
+ LEGACY_GQL_METHODS.includes(node.expression.name.text)) {
438
+ const receiver = node.expression.expression;
439
+ const receiverText = receiver.getText(sourceFile);
440
+ const method = node.expression.name.text;
441
+ const clientName = resolveClient(receiver);
442
+ const name = describeGqlCall(node, sourceFile);
443
+ const isQuery = method === 'gqlQuery';
444
+ const viaGet = isQuery && !!clientName && gqlGetClients.has(clientName);
445
+ const creator = !isQuery
446
+ ? '`createGqlMutationViaPost`'
447
+ : viaGet
448
+ ? '`createGqlQueryViaGet`'
449
+ : clientName || gqlGetClients.size === 0
450
+ ? '`createGqlQueryViaPost`'
451
+ : "`createGqlQueryViaPost` (or `createGqlQueryViaGet` if its client set `request.gql.transferVia: 'GET'`)";
452
+ const builtWith = clientName
453
+ ? `\`${receiverText}.${method}()\``
454
+ : `\`${receiverText}.${method}()\`, which the migration takes for a v2 query client`;
455
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
456
+ report.addManualReview({
457
+ title: `Rewrite the GraphQL creator ${name} by hand`,
458
+ summary: `${name} is built with ${builtWith}. The migration only rewrites REST creators, so this one still calls a method the migrated client no longer has, and the callback parameters in its config lose their types: expect implicit-any errors (TS7006) there until it is rebuilt.${viaGet ? " The v2 client sent GraphQL queries with `request.gql.transferVia: 'GET'`, which v3 sets per creator." : ''}`,
459
+ action: `Rebuild it on the v3 client with ${creator} and a \`gql\` document, then wrap it in \`createLegacyQueryCreator\` if its call sites stay on v2. See https://ethlete-sdk-docs.web.app/query/gql.`,
460
+ locations: [{ filePath, line }],
461
+ source: 'legacy-query-creator-migration',
462
+ dedupeKey: `gql-creator:${filePath}:${name}:${line}`,
463
+ });
464
+ }
465
+ ts.forEachChild(node, visit);
466
+ };
467
+ visit(sourceFile);
468
+ };
368
469
  const extractTypeFromDef = (node, sourceFile) => {
369
470
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'def') {
370
471
  if (node.typeArguments && node.typeArguments.length > 0) {
@@ -505,8 +606,11 @@ const generateNewQueryCreator = (creator) => {
505
606
  ? `${clientBaseName}${capitalizedMethod}Secure`
506
607
  : `${clientBaseName}${capitalizedMethod}`;
507
608
  const typeParameter = getTypeParameter(creator);
508
- const options = Array.from(creator.httpOptions.entries()).map(([key, value]) => `${key}: ${value}`);
509
- const optionsBlock = options.length > 0 ? `, {\n ${options.join(',\n ')}\n}` : '';
609
+ const options = [
610
+ 'reportErrors: false',
611
+ ...Array.from(creator.httpOptions.entries()).map(([key, value]) => `${key}: ${value}`),
612
+ ];
613
+ const optionsBlock = `, {\n ${options.join(',\n ')}\n}`;
510
614
  return `${creatorFunction}${typeParameter}(${creator.route}${optionsBlock})`;
511
615
  };
512
616
  const getTypeParameter = (creator) => {
@@ -555,12 +659,22 @@ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, cre
555
659
  // belongs to exactly one client.
556
660
  const v2Config = v2Configs.find((candidate) => candidate.refreshCreatorName && creatorToClient.get(candidate.refreshCreatorName) === clientName);
557
661
  const body = renderAuthProviderBody(v2Config);
558
- const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body), clientName);
662
+ const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body, v2Config, filePath), clientName);
559
663
  if (nextContent === content) {
560
664
  return;
561
665
  }
562
666
  tree.write(filePath, nextContent);
563
667
  createdProviders.push(filePath);
668
+ if (v2Config && v2Config.localNames.length > 0) {
669
+ report.addWarning({
670
+ title: 'Import the names the scaffolded auth provider copied',
671
+ summary: `The scaffolded provider uses ${v2Config.localNames.join(', ')}, declared in ${v2Config.filePath} and not exported to ${filePath}.`,
672
+ action: 'Move each declaration into the provider file, or export it and import it there.',
673
+ locations: [{ filePath }, { filePath: v2Config.filePath, line: v2Config.line }],
674
+ source: 'legacy-query-creator-migration',
675
+ dedupeKey: `auth-provider-local-names:${filePath}:${authProviderName}`,
676
+ });
677
+ }
564
678
  body.warnings.forEach((warning) => {
565
679
  report.addWarning({
566
680
  ...warning,
@@ -606,8 +720,40 @@ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, cre
606
720
  createdProviders.forEach((filePath) => console.log(` - ${filePath}`));
607
721
  }
608
722
  };
609
- const addAuthProviderToFile = (content, clientName, body) => {
610
- const nextContent = ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), ['toInjectFn', 'toProvideFn']);
723
+ const addCopiedImports = (content, v2Config, filePath) => {
724
+ if (!v2Config || v2Config.filePath === filePath) {
725
+ return content;
726
+ }
727
+ const sourceFile = createSourceFile(content);
728
+ const bound = new Set();
729
+ let lastImportEnd = 0;
730
+ sourceFile.statements.forEach((statement) => {
731
+ if (!ts.isImportDeclaration(statement)) {
732
+ return;
733
+ }
734
+ lastImportEnd = Math.max(lastImportEnd, statement.getEnd());
735
+ const clause = statement.importClause;
736
+ if (clause?.name)
737
+ bound.add(clause.name.text);
738
+ if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings))
739
+ bound.add(clause.namedBindings.name.text);
740
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
741
+ clause.namedBindings.elements.forEach((element) => bound.add(element.name.text));
742
+ }
743
+ });
744
+ const statements = v2Config.copiedImports
745
+ .filter(({ names }) => names.every((name) => !bound.has(name)))
746
+ .map(({ moduleSpecifier, statement }) => statement(rebaseModuleSpecifier(moduleSpecifier, v2Config.filePath, filePath)));
747
+ if (statements.length === 0) {
748
+ return content;
749
+ }
750
+ return content.slice(0, lastImportEnd) + `\n${statements.join('\n')}` + content.slice(lastImportEnd);
751
+ };
752
+ const addAuthProviderToFile = (content, clientName, body, v2Config, filePath) => {
753
+ const nextContent = addCopiedImports(ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), [
754
+ 'toInjectFn',
755
+ 'toProvideFn',
756
+ ]), v2Config, filePath);
611
757
  const sourceFile = createSourceFile(nextContent);
612
758
  const configName = ensureConfigSuffix(clientName);
613
759
  const configPosition = findVariableStatementEnd(sourceFile, configName, 'createQueryClient');
@@ -1,21 +1,23 @@
1
1
  import { formatFiles } from '@nx/devkit';
2
- import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy, reportRemovedExperimentalQueryHelpers, } from './cleanup-migration.js';
2
+ import { reportAuthConsumers } from './auth-consumers-migration.js';
3
+ import { migrateDevtoolsUsage, reportLeftoverLegacyDevtools, reportLeftoverV2Clients, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy, reportRemovedExperimentalQueryHelpers, } from './cleanup-migration.js';
3
4
  import { reportDefaultHeaderUsages, reportMissingHttpClientProviders } from './http-client-check.js';
4
5
  import { migrateLegacyPrepareCalls } from './legacy-prepare-migration.js';
5
6
  import { createNewQueryCreators, updateLegacyCreatorImportsAndUsages } from './legacy-query-creator-migration.js';
6
7
  import { createMigrationScope } from './migration-scope.js';
7
8
  import { generateProviderAliases, generateQueryCreators, migrateQueryClients, updateImportsAcrossWorkspace, } from './query-client-migration.js';
9
+ import { reportLegacyModuleResolution } from './module-resolution.js';
8
10
  import { QueryV3MigrationReport } from './report.js';
9
11
  export default async function migrate(tree, schema) {
10
12
  console.log('\n🔄 Starting query v3 migration...');
11
13
  const scope = createMigrationScope(tree, schema);
12
14
  console.log(` Scope: ${scope.describe()}`);
13
15
  const report = new QueryV3MigrationReport();
14
- const { queryClientFiles, variableRenames } = migrateQueryClients(tree, report, scope);
16
+ const { queryClientFiles, variableRenames, gqlGetClients } = migrateQueryClients(tree, report, scope);
15
17
  if (queryClientFiles.size > 0) {
16
18
  generateProviderAliases(tree, queryClientFiles);
17
19
  generateQueryCreators(tree, queryClientFiles);
18
- createNewQueryCreators(tree, queryClientFiles, report, scope);
20
+ createNewQueryCreators(tree, queryClientFiles, report, scope, gqlGetClients);
19
21
  updateLegacyCreatorImportsAndUsages(tree, scope, report);
20
22
  }
21
23
  if (variableRenames.size > 0) {
@@ -26,10 +28,14 @@ export default async function migrate(tree, schema) {
26
28
  migrateEmptyPrepareCalls(tree, scope, report);
27
29
  migrateLegacyPrepareCalls(tree, report, scope);
28
30
  reportRemovedExperimentalQueryHelpers(tree, scope, report);
31
+ reportLeftoverV2Clients(tree, scope, report);
32
+ reportLeftoverLegacyDevtools(tree, scope, report);
33
+ reportAuthConsumers(tree, scope, report);
29
34
  if (queryClientFiles.size > 0) {
30
35
  reportMissingHttpClientProviders(tree, report, scope);
31
36
  reportDefaultHeaderUsages(tree, report, scope);
32
37
  }
38
+ reportLegacyModuleResolution(tree, scope, report);
33
39
  report.writeToTree(tree, scope.isScoped ? scope.describe() : undefined);
34
40
  report.printSummary();
35
41
  if (!schema.skipFormat) {
@@ -0,0 +1,131 @@
1
+ import { joinPathFragments, readJson, visitNotIgnoredFiles } from '@nx/devkit';
2
+ import { dirname } from 'node:path';
3
+ import { createModuleGraph } from './module-graph.js';
4
+ const PROJECT_TSCONFIGS = ['tsconfig.lib.json', 'tsconfig.app.json', 'tsconfig.spec.json'];
5
+ const LEGACY_RESOLUTIONS = new Set(['node', 'node10']);
6
+ const MODULE_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*)['"]([^'"]+)['"]/g;
7
+ const projectRootOf = (tree, filePath) => {
8
+ let dir = dirname(filePath);
9
+ while (dir !== '.' && dir !== '/' && dir !== '') {
10
+ if (tree.exists(joinPathFragments(dir, 'project.json')))
11
+ return dir;
12
+ dir = dirname(dir);
13
+ }
14
+ return null;
15
+ };
16
+ const effectiveModuleResolution = (tree, configPath, seen = new Set()) => {
17
+ if (seen.has(configPath) || !tree.exists(configPath))
18
+ return null;
19
+ seen.add(configPath);
20
+ const config = readJson(tree, configPath);
21
+ const own = config.compilerOptions?.moduleResolution;
22
+ if (own)
23
+ return own.toLowerCase();
24
+ const parents = config.extends === undefined ? [] : [config.extends].flat();
25
+ for (const parent of [...parents].reverse()) {
26
+ if (!parent.startsWith('.'))
27
+ continue;
28
+ const resolved = effectiveModuleResolution(tree, joinPathFragments(dirname(configPath), parent.endsWith('.json') ? parent : `${parent}.json`), seen);
29
+ if (resolved)
30
+ return resolved;
31
+ }
32
+ return null;
33
+ };
34
+ const collectAliasEdges = (tree, graph) => {
35
+ const edges = new Map();
36
+ visitNotIgnoredFiles(tree, '', (filePath) => {
37
+ if (!filePath.endsWith('.ts') || filePath.includes('node_modules/'))
38
+ return;
39
+ const fromRoot = projectRootOf(tree, filePath);
40
+ if (!fromRoot)
41
+ return;
42
+ for (const [, specifier] of (tree.read(filePath, 'utf-8') ?? '').matchAll(MODULE_SPECIFIER)) {
43
+ if (!specifier || specifier.startsWith('.') || !graph.isWorkspaceSpecifier(specifier))
44
+ continue;
45
+ const target = graph.resolveFile(filePath, specifier);
46
+ const targetRoot = target ? projectRootOf(tree, target) : null;
47
+ if (!targetRoot || targetRoot === fromRoot)
48
+ continue;
49
+ const list = edges.get(fromRoot) ?? [];
50
+ if (!list.some((edge) => edge.alias === specifier))
51
+ list.push({ alias: specifier, targetRoot });
52
+ edges.set(fromRoot, list);
53
+ }
54
+ });
55
+ edges.forEach((list) => list.sort((a, b) => a.alias.localeCompare(b.alias)));
56
+ return edges;
57
+ };
58
+ const projectsReaching = (tree, graph, importingRoots) => {
59
+ const reached = new Map([...importingRoots].map((root) => [root, null]));
60
+ const edges = collectAliasEdges(tree, graph);
61
+ const roots = [...edges.keys()].sort();
62
+ let grew = true;
63
+ while (grew) {
64
+ grew = false;
65
+ for (const root of roots) {
66
+ if (reached.has(root))
67
+ continue;
68
+ const edge = edges.get(root).find(({ targetRoot }) => reached.has(targetRoot));
69
+ if (!edge)
70
+ continue;
71
+ reached.set(root, edge.alias);
72
+ grew = true;
73
+ }
74
+ }
75
+ return reached;
76
+ };
77
+ /**
78
+ * Lists every project tsconfig that resolves modules with `node` / `node10` while the project imports a specifier
79
+ * `matches` accepts, directly or through a workspace path alias. That resolution ignores the `exports` map, so a
80
+ * package subpath like `@ethlete/query/testing` does not resolve.
81
+ */
82
+ export const findLegacyModuleResolutions = (tree, graph, files, matches) => {
83
+ const directSpecifiers = new Map();
84
+ for (const filePath of files) {
85
+ if (!filePath.endsWith('.ts'))
86
+ continue;
87
+ const root = projectRootOf(tree, filePath);
88
+ if (!root || directSpecifiers.has(root))
89
+ continue;
90
+ const specifier = Array.from((tree.read(filePath, 'utf-8') ?? '').matchAll(MODULE_SPECIFIER), ([, s]) => s).find((s) => s !== undefined && matches(s));
91
+ if (specifier)
92
+ directSpecifiers.set(root, specifier);
93
+ }
94
+ if (!directSpecifiers.size)
95
+ return [];
96
+ const reached = projectsReaching(tree, graph, new Set(directSpecifiers.keys()));
97
+ const findings = [];
98
+ for (const root of [...reached.keys()].sort()) {
99
+ const alias = reached.get(root) ?? null;
100
+ for (const name of PROJECT_TSCONFIGS) {
101
+ const configPath = joinPathFragments(root, name);
102
+ const resolution = effectiveModuleResolution(tree, configPath);
103
+ if (!resolution || !LEGACY_RESOLUTIONS.has(resolution))
104
+ continue;
105
+ findings.push({ configPath, resolution, specifier: directSpecifiers.get(root) ?? null, alias });
106
+ }
107
+ }
108
+ return findings;
109
+ };
110
+ const ETHLETE_SUBPATH = /^@ethlete\/[^/]+\/./;
111
+ /**
112
+ * Records a follow-up for every node-resolving project tsconfig whose project imports an `@ethlete/*` subpath
113
+ * (`@ethlete/query/testing`, `@ethlete/query-devtools/lazy`, ...), directly or through a workspace path alias.
114
+ * Runs after the rewrites, so it also sees the subpaths the migration itself adds.
115
+ */
116
+ export const reportLegacyModuleResolution = (tree, scope, report) => {
117
+ const files = [];
118
+ scope.visit(tree, (filePath) => files.push(filePath));
119
+ for (const { configPath, resolution, specifier, alias } of findLegacyModuleResolutions(tree, createModuleGraph(tree), files, (candidate) => ETHLETE_SUBPATH.test(candidate))) {
120
+ report.addFollowUp({
121
+ title: 'Project resolves modules with `node`, which cannot find `@ethlete/*` subpaths',
122
+ summary: alias
123
+ ? `${configPath} resolves modules with \`${resolution}\` and reaches an \`@ethlete/*\` subpath through \`${alias}\`.`
124
+ : `${configPath} resolves modules with \`${resolution}\` and imports \`${specifier}\`.`,
125
+ action: '`moduleResolution: node` (`node10`) ignores the `exports` map of the `@ethlete/*` packages, so subpath imports like `@ethlete/query-devtools/lazy` do not resolve - also when they sit in another project this one imports through a path alias. Set `moduleResolution` to `bundler` (or `node16` / `nodenext`) in the listed tsconfig or the base config it inherits it from.',
126
+ locations: [{ filePath: configPath }],
127
+ source: 'module-resolution',
128
+ });
129
+ }
130
+ };
131
+ //# sourceMappingURL=module-resolution.js.map