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

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
1
  import * as ts from 'typescript';
2
- import { createSourceFile, ensureAngularCoreImports, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
2
+ import { createSourceFile, ensureAngularCoreImports, ensureImportFromQuery, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
3
+ /** The creator methods that resolve an injector, and so throw `ET950` when called without one. */
4
+ const INJECTOR_METHODS = new Set(['prepare', 'createSubject', 'createSignal', 'behaviorSubject']);
3
5
  /** `default` can never be a class name, so it cannot collide with a named class of the same file. */
4
6
  const ANONYMOUS_DEFAULT_CLASS_KEY = 'default';
5
7
  const getClassKey = (node) => node.name?.text ?? ANONYMOUS_DEFAULT_CLASS_KEY;
@@ -22,6 +24,7 @@ export const migrateLegacyPrepareCalls = (tree, report, scope) => {
22
24
  return;
23
25
  }
24
26
  allUsages.push(...findLegacyCreatorUsages(content, filePath));
27
+ reportUnresolvedReceivers(content, filePath, report);
25
28
  });
26
29
  if (allUsages.length === 0) {
27
30
  console.log('\n✅ No legacy query creator usages found');
@@ -60,12 +63,13 @@ const findLegacyCreatorUsages = (content, filePath) => {
60
63
  const visit = (node) => {
61
64
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
62
65
  const propertyAccess = node.expression;
63
- if (ts.isIdentifier(propertyAccess.name) && propertyAccess.name.text === 'prepare') {
66
+ if (ts.isIdentifier(propertyAccess.name) && INJECTOR_METHODS.has(propertyAccess.name.text)) {
64
67
  const creatorName = ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression.text : undefined;
65
68
  if (creatorName && legacyCreatorNames.has(creatorName)) {
66
69
  usages.push({
67
70
  filePath,
68
71
  creatorName,
72
+ method: propertyAccess.name.text,
69
73
  line: getLineNumber(node, sourceFile),
70
74
  position: {
71
75
  start: node.getStart(sourceFile),
@@ -82,6 +86,35 @@ const findLegacyCreatorUsages = (content, filePath) => {
82
86
  visit(sourceFile);
83
87
  return usages;
84
88
  };
89
+ const LEGACY_PROPERTY_NAME = /^legacy[A-Z]/;
90
+ const reportUnresolvedReceivers = (content, filePath, report) => {
91
+ if (!/\blegacy[A-Z]\w*\s*\.(prepare|createSubject|createSignal|behaviorSubject)\(/.test(content)) {
92
+ return;
93
+ }
94
+ const sourceFile = createSourceFile(content, filePath);
95
+ const visit = (node) => {
96
+ if (ts.isCallExpression(node) &&
97
+ ts.isPropertyAccessExpression(node.expression) &&
98
+ ts.isIdentifier(node.expression.name) &&
99
+ INJECTOR_METHODS.has(node.expression.name.text) &&
100
+ ts.isPropertyAccessExpression(node.expression.expression) &&
101
+ LEGACY_PROPERTY_NAME.test(node.expression.expression.name.text) &&
102
+ !CONTEXTS_WITH_INJECTOR.includes(determineUsageContext(node))) {
103
+ const line = getLineNumber(node, sourceFile);
104
+ const receiver = node.expression.expression.getText(sourceFile);
105
+ report.addManualReview({
106
+ title: `Check the injector of ${receiver}.${node.expression.name.text}()`,
107
+ summary: `${receiver}.${node.expression.name.text}() is called on a creator the migration cannot resolve, because the receiver is not a plain legacy* identifier. The call may run outside an injection context and then throws ET950.`,
108
+ action: 'Pass `{ injector }` to the call, or add provideLegacyPrepareFallback() to the app providers. A call in a constructor, a field initializer or an inject() function needs nothing.',
109
+ locations: [{ filePath, line }],
110
+ source: 'legacy-prepare-migration',
111
+ dedupeKey: `unresolved-receiver:${filePath}:${line}`,
112
+ });
113
+ }
114
+ ts.forEachChild(node, visit);
115
+ };
116
+ visit(sourceFile);
117
+ };
85
118
  const findLegacyCreatorNames = (sourceFile) => {
86
119
  const names = new Set();
87
120
  const visit = (node) => {
@@ -330,6 +363,12 @@ const createInjectorMembers = (tree, classInjectors) => {
330
363
  console.log(`\n✅ Added injector members to ${Array.from(filesToUpdate.values()).flat().length} classes`);
331
364
  }
332
365
  };
366
+ const getMemberStartWithComments = (content, member) => {
367
+ const ownLineComment = ts
368
+ .getLeadingCommentRanges(content, member.getFullStart())
369
+ ?.find((range) => /\n[ \t]*$/.test(content.slice(0, range.pos)));
370
+ return ownLineComment?.pos ?? member.getStart();
371
+ };
333
372
  const addInjectorMemberToClass = (content, classInfo) => {
334
373
  const sourceFile = createSourceFile(content);
335
374
  let targetClass;
@@ -343,7 +382,9 @@ const addInjectorMemberToClass = (content, classInfo) => {
343
382
  if (!targetClass) {
344
383
  return content;
345
384
  }
346
- const classBodyStart = targetClass.members.length > 0 ? targetClass.members[0].getStart(sourceFile) : targetClass.getEnd() - 1;
385
+ const classBodyStart = targetClass.members.length > 0
386
+ ? getMemberStartWithComments(content, targetClass.members[0])
387
+ : targetClass.getEnd() - 1;
347
388
  const indentation = getIndentation(content, classBodyStart);
348
389
  const injectorMember = `private ${classInfo.injectorMemberName} = inject(Injector);\n\n${indentation}`;
349
390
  return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
@@ -362,7 +403,7 @@ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo, s
362
403
  return;
363
404
  }
364
405
  const content = tree.read(filePath, 'utf-8');
365
- if (!content || !content.includes('.prepare(')) {
406
+ if (!content || !/\.(prepare|createSubject|createSignal|behaviorSubject)\(/.test(content)) {
366
407
  return;
367
408
  }
368
409
  const usages = findLegacyCreatorUsages(content, filePath);
@@ -391,6 +432,7 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
391
432
  const replacements = [];
392
433
  const legacyCreatorNames = findLegacyCreatorNames(sourceFile);
393
434
  const classNameToInjector = new Map();
435
+ const rewrittenComputeds = new Set();
394
436
  const functionsNeedingInjector = new Map();
395
437
  if (legacyCreatorNames.size === 0) {
396
438
  return content;
@@ -401,13 +443,31 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
401
443
  const visit = (node) => {
402
444
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
403
445
  const propertyAccess = node.expression;
404
- if (ts.isIdentifier(propertyAccess.name) && propertyAccess.name.text === 'prepare') {
446
+ if (ts.isIdentifier(propertyAccess.name) && INJECTOR_METHODS.has(propertyAccess.name.text)) {
405
447
  const creatorName = ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression.text : undefined;
406
448
  if (!creatorName || !legacyCreatorNames.has(creatorName)) {
407
449
  ts.forEachChild(node, visit);
408
450
  return;
409
451
  }
410
452
  const context = determineUsageContext(node);
453
+ const computedHost = findReturningPlainComputed(node);
454
+ const rewriteComputed = (injectorExpression) => {
455
+ if (!computedHost || rewrittenComputeds.has(computedHost))
456
+ return;
457
+ rewrittenComputeds.add(computedHost);
458
+ replacements.push({
459
+ start: computedHost.expression.getStart(sourceFile),
460
+ end: computedHost.expression.getEnd(),
461
+ replacement: 'queryComputed',
462
+ });
463
+ if (computedHost.arguments.length === 1) {
464
+ replacements.push({
465
+ start: computedHost.arguments[0].getEnd(),
466
+ end: computedHost.arguments[0].getEnd(),
467
+ replacement: `, { injector: ${injectorExpression} }`,
468
+ });
469
+ }
470
+ };
411
471
  if (CONTEXTS_WITH_INJECTOR.includes(context)) {
412
472
  ts.forEachChild(node, visit);
413
473
  return;
@@ -420,7 +480,8 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
420
480
  ts.forEachChild(node, visit);
421
481
  return;
422
482
  }
423
- const transformedCall = transformSinglePrepareCall(node, `this.${injectorMember}`, sourceFile, pollingInfo);
483
+ rewriteComputed(`this.${injectorMember}`);
484
+ const transformedCall = transformCall(node, `this.${injectorMember}`, sourceFile, pollingInfo);
424
485
  if (transformedCall) {
425
486
  replacements.push({
426
487
  start: node.getStart(sourceFile),
@@ -457,7 +518,8 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
457
518
  }
458
519
  }
459
520
  const injectorName = functionsNeedingInjector.get(functionPosition)?.injectorName ?? 'injector';
460
- const transformedCall = transformSinglePrepareCall(node, injectorName, sourceFile, pollingInfo);
521
+ rewriteComputed(injectorName);
522
+ const transformedCall = transformCall(node, injectorName, sourceFile, pollingInfo);
461
523
  if (transformedCall) {
462
524
  replacements.push({
463
525
  start: node.getStart(sourceFile),
@@ -497,7 +559,7 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
497
559
  });
498
560
  result = result.slice(0, start + adjustment) + replacement + result.slice(end + adjustment);
499
561
  });
500
- return result;
562
+ return rewrittenComputeds.size > 0 ? ensureImportFromQuery(result, ['queryComputed']) : result;
501
563
  };
502
564
  const findInjectorParameter = (func) => {
503
565
  for (const parameter of func.parameters) {
@@ -560,6 +622,26 @@ const findContainingClass = (node) => {
560
622
  }
561
623
  return undefined;
562
624
  };
625
+ const transformCall = (callNode, injectorReference, sourceFile, pollingInfo) => callNode.expression.name.text === 'prepare'
626
+ ? transformSinglePrepareCall(callNode, injectorReference, sourceFile, pollingInfo)
627
+ : transformContainerCall(callNode, injectorReference, sourceFile);
628
+ const transformContainerCall = (callNode, injectorReference, sourceFile) => {
629
+ const propertyAccess = callNode.expression;
630
+ const initialValue = callNode.arguments[0]?.getText(sourceFile) ?? 'null';
631
+ const config = callNode.arguments[1];
632
+ let nextConfig = `{ injector: ${injectorReference} }`;
633
+ if (config && ts.isObjectLiteralExpression(config)) {
634
+ const hasInjector = config.properties.some((property) => !!property.name && ts.isIdentifier(property.name) && property.name.text === 'injector');
635
+ if (hasInjector) {
636
+ return undefined;
637
+ }
638
+ nextConfig = `{ ${[...config.properties.map((property) => property.getText(sourceFile)), `injector: ${injectorReference}`].join(', ')} }`;
639
+ }
640
+ else if (config) {
641
+ nextConfig = `{ ...${config.getText(sourceFile)}, injector: ${injectorReference} }`;
642
+ }
643
+ return `${propertyAccess.expression.getText(sourceFile)}.${propertyAccess.name.text}(${initialValue}, ${nextConfig})`;
644
+ };
563
645
  const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pollingInfo) => {
564
646
  const propertyAccess = callNode.expression;
565
647
  const creatorName = propertyAccess.expression.text;
@@ -596,7 +678,7 @@ const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pol
596
678
  }
597
679
  nextArgumentProperties.push(`injector: ${injectorReference}`);
598
680
  const queryVariableName = findQueryVariableNameForPrepareCall(callNode, sourceFile);
599
- const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo, isDiscardedPrepareCall(callNode));
681
+ const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo, isDiscardedPrepareCall(callNode), isOneShotLocalVariable(callNode) || isInsideAsyncValidator(callNode));
600
682
  if (existingConfig && ts.isObjectLiteralExpression(existingConfig.initializer)) {
601
683
  const configProperties = [];
602
684
  existingConfig.initializer.properties.forEach((property) => {
@@ -685,9 +767,9 @@ const reportManualReviewLocations = (tree, allUsages, report) => {
685
767
  visit(sourceFile);
686
768
  if (foundFunctionWithoutInject) {
687
769
  report.addManualReview({
688
- title: `Review standalone prepare() usage for ${usage.creatorName}`,
689
- summary: `${usage.creatorName}.prepare() is used in a standalone function without an obvious injection context.`,
690
- action: 'Pass an Injector explicitly, move the call into queryComputed(), or wrap the logic in a function that already uses inject().',
770
+ title: `Review standalone ${usage.method}() usage for ${usage.creatorName}`,
771
+ summary: `${usage.creatorName}.${usage.method}() is used in a standalone function without an obvious injection context.`,
772
+ action: 'If the factory that returns this function runs in an injection context, add `const injector = inject(Injector);` at the top of the factory and pass `{ injector }` to the call. Otherwise pass an Injector from the caller, move the call into queryComputed(), or add provideLegacyPrepareFallback() to the app providers.',
691
773
  locations: [{ filePath: usage.filePath, line: usage.line }],
692
774
  source: 'legacy-prepare-migration',
693
775
  dedupeKey: `manual-review:${usage.filePath}:${usage.line}:${usage.creatorName}`,
@@ -697,7 +779,7 @@ const reportManualReviewLocations = (tree, allUsages, report) => {
697
779
  if (usage.context === 'unknown') {
698
780
  report.addManualReview({
699
781
  title: `Verify execution context for ${usage.creatorName}`,
700
- summary: `The migration could not classify the execution context of ${usage.creatorName}.prepare().`,
782
+ summary: `The migration could not classify the execution context of ${usage.creatorName}.${usage.method}().`,
701
783
  action: 'Inspect the surrounding code and confirm that the generated injector handling and destroyOnResponse behavior are correct.',
702
784
  locations: [{ filePath: usage.filePath, line: usage.line }],
703
785
  source: 'legacy-prepare-migration',
@@ -851,12 +933,145 @@ const isDiscardedPrepareCall = (callNode) => {
851
933
  }
852
934
  return false;
853
935
  };
854
- const shouldAddDestroyOnResponse = (queryVariableName, pollingInfo, isDiscarded) => {
855
- if (!queryVariableName) {
856
- // A discarded query is the one case where nothing else can clean it up: no variable for a container
857
- // to hold, no caller to destroy it. That is exactly what `destroyOnResponse` is for.
858
- return isDiscarded;
936
+ const unwrapChainEnd = (callNode) => {
937
+ let current = callNode;
938
+ while (current.parent) {
939
+ const parent = current.parent;
940
+ if (ts.isParenthesizedExpression(parent) || ts.isNonNullExpression(parent)) {
941
+ current = parent;
942
+ continue;
943
+ }
944
+ const call = parent.parent;
945
+ if (ts.isPropertyAccessExpression(parent) &&
946
+ parent.expression === current &&
947
+ parent.name.text === 'execute' &&
948
+ call &&
949
+ ts.isCallExpression(call) &&
950
+ call.expression === parent) {
951
+ current = call;
952
+ continue;
953
+ }
954
+ break;
859
955
  }
860
- return !pollingInfo.get(queryVariableName)?.hasPolling;
956
+ return current;
957
+ };
958
+ /**
959
+ * The Angular `computed(() => legacy.prepare(...).execute())` whose callback returns the prepared query.
960
+ * Such a computed builds one unmanaged query per input change, so it becomes `queryComputed`, whose
961
+ * container destroys the superseded one.
962
+ */
963
+ const findReturningPlainComputed = (callNode) => {
964
+ const chainEnd = unwrapChainEnd(callNode);
965
+ const callback = chainEnd.parent;
966
+ if (!callback || !ts.isArrowFunction(callback) || callback.body !== chainEnd) {
967
+ const returnStatement = chainEnd.parent;
968
+ if (!returnStatement || !ts.isReturnStatement(returnStatement))
969
+ return undefined;
970
+ let fn = returnStatement.parent;
971
+ while (fn && !isCallbackFunction(fn))
972
+ fn = fn.parent;
973
+ return fn && isCallbackFunction(fn) ? getPlainComputedHost(fn) : undefined;
974
+ }
975
+ return getPlainComputedHost(callback);
976
+ };
977
+ const getPlainComputedHost = (node) => {
978
+ const host = getCallbackHost(node);
979
+ if (!host || !ts.isIdentifier(host.expression) || host.expression.text !== 'computed' || host.arguments[0] !== node) {
980
+ return undefined;
981
+ }
982
+ return host;
983
+ };
984
+ const ASYNC_VALIDATOR_PROPERTIES = new Set(['asyncValidator', 'asyncValidators']);
985
+ const ASYNC_VALIDATOR_SETTERS = new Set(['setAsyncValidators', 'addAsyncValidators']);
986
+ /**
987
+ * Whether the innermost function around the call is an Angular async validator: a value of an
988
+ * `asyncValidator(s)` property or setter argument, an `AsyncValidatorFn`, or `validate` of an `AsyncValidator`.
989
+ */
990
+ const isInsideAsyncValidator = (callNode) => {
991
+ let fn = callNode.parent;
992
+ while (fn && !isCallbackFunction(fn) && !ts.isMethodDeclaration(fn))
993
+ fn = fn.parent;
994
+ if (!fn)
995
+ return false;
996
+ if (ts.isMethodDeclaration(fn)) {
997
+ const owner = fn.parent;
998
+ return (ts.isIdentifier(fn.name) &&
999
+ fn.name.text === 'validate' &&
1000
+ ts.isClassDeclaration(owner) &&
1001
+ !!owner.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText() === 'AsyncValidator')));
1002
+ }
1003
+ let value = fn;
1004
+ if (ts.isArrayLiteralExpression(value.parent))
1005
+ value = value.parent;
1006
+ const parent = value.parent;
1007
+ if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) {
1008
+ return ASYNC_VALIDATOR_PROPERTIES.has(parent.name.text);
1009
+ }
1010
+ if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression)) {
1011
+ return ASYNC_VALIDATOR_SETTERS.has(parent.expression.name.text);
1012
+ }
1013
+ if ((ts.isVariableDeclaration(parent) || ts.isPropertyDeclaration(parent)) && parent.type) {
1014
+ return parent.type.getText().includes('AsyncValidatorFn');
1015
+ }
1016
+ return false;
1017
+ };
1018
+ const ONE_SHOT_QUERY_MEMBERS = ['execute', 'onSuccess', 'onFailure'];
1019
+ const isOneShotLocalVariable = (callNode) => {
1020
+ let chainEnd = callNode;
1021
+ let executeCalls = 0;
1022
+ while (chainEnd.parent) {
1023
+ const parent = chainEnd.parent;
1024
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === chainEnd) {
1025
+ if (!ONE_SHOT_QUERY_MEMBERS.includes(parent.name.text))
1026
+ return false;
1027
+ if (parent.name.text === 'execute')
1028
+ executeCalls++;
1029
+ chainEnd = parent;
1030
+ continue;
1031
+ }
1032
+ if (ts.isCallExpression(parent) && parent.expression === chainEnd) {
1033
+ chainEnd = parent;
1034
+ continue;
1035
+ }
1036
+ break;
1037
+ }
1038
+ const declaration = chainEnd.parent;
1039
+ if (!declaration ||
1040
+ !ts.isVariableDeclaration(declaration) ||
1041
+ declaration.initializer !== chainEnd ||
1042
+ !ts.isIdentifier(declaration.name)) {
1043
+ return false;
1044
+ }
1045
+ const scope = declaration.parent.parent.parent;
1046
+ if (!ts.isVariableStatement(declaration.parent.parent) || !ts.isBlock(scope))
1047
+ return false;
1048
+ const variableName = declaration.name.text;
1049
+ let isOneShot = executeCalls <= 1;
1050
+ const visit = (node) => {
1051
+ if (!isOneShot)
1052
+ return;
1053
+ if (ts.isIdentifier(node) && node.text === variableName && node !== declaration.name) {
1054
+ const parent = node.parent;
1055
+ if (ts.isPropertyAccessExpression(parent) &&
1056
+ parent.expression === node &&
1057
+ ONE_SHOT_QUERY_MEMBERS.includes(parent.name.text)) {
1058
+ if (parent.name.text === 'execute')
1059
+ executeCalls++;
1060
+ if (executeCalls > 1)
1061
+ isOneShot = false;
1062
+ }
1063
+ else if (!(ts.isPropertyAccessExpression(parent) && parent.name === node)) {
1064
+ isOneShot = false;
1065
+ }
1066
+ }
1067
+ ts.forEachChild(node, visit);
1068
+ };
1069
+ visit(scope);
1070
+ return isOneShot;
1071
+ };
1072
+ const shouldAddDestroyOnResponse = (queryVariableName, pollingInfo, isDiscarded, isOneShotLocal) => {
1073
+ if (queryVariableName && pollingInfo.get(queryVariableName)?.hasPolling)
1074
+ return false;
1075
+ return isDiscarded || isOneShotLocal;
861
1076
  };
862
1077
  //# sourceMappingURL=legacy-prepare-migration.js.map
@@ -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,7 +206,7 @@ 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();
@@ -221,10 +226,13 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
221
226
  return;
222
227
  }
223
228
  if (oldClientNames.has(objectExpression.text) && LEGACY_GQL_METHODS.includes(methodExpression.text)) {
229
+ const isQuery = methodExpression.text === 'gqlQuery';
230
+ const viaGet = isQuery && gqlGetClients.has(objectExpression.text);
231
+ const creator = isQuery ? `createGqlQueryVia${viaGet ? 'Get' : 'Post'}` : 'createGqlMutationViaPost';
224
232
  report.addManualReview({
225
233
  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.`,
234
+ 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.${viaGet ? " The v2 client sent GraphQL queries with `request.gql.transferVia: 'GET'`, which v3 sets per creator." : ''}`,
235
+ 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.`,
228
236
  locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
229
237
  source: 'legacy-query-creator-migration',
230
238
  dedupeKey: `gql-creator:${filePath}:${node.name.text}`,
@@ -505,8 +513,11 @@ const generateNewQueryCreator = (creator) => {
505
513
  ? `${clientBaseName}${capitalizedMethod}Secure`
506
514
  : `${clientBaseName}${capitalizedMethod}`;
507
515
  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}` : '';
516
+ const options = [
517
+ 'reportErrors: false',
518
+ ...Array.from(creator.httpOptions.entries()).map(([key, value]) => `${key}: ${value}`),
519
+ ];
520
+ const optionsBlock = `, {\n ${options.join(',\n ')}\n}`;
510
521
  return `${creatorFunction}${typeParameter}(${creator.route}${optionsBlock})`;
511
522
  };
512
523
  const getTypeParameter = (creator) => {
@@ -555,12 +566,22 @@ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, cre
555
566
  // belongs to exactly one client.
556
567
  const v2Config = v2Configs.find((candidate) => candidate.refreshCreatorName && creatorToClient.get(candidate.refreshCreatorName) === clientName);
557
568
  const body = renderAuthProviderBody(v2Config);
558
- const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body), clientName);
569
+ const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body, v2Config, filePath), clientName);
559
570
  if (nextContent === content) {
560
571
  return;
561
572
  }
562
573
  tree.write(filePath, nextContent);
563
574
  createdProviders.push(filePath);
575
+ if (v2Config && v2Config.localNames.length > 0) {
576
+ report.addWarning({
577
+ title: 'Import the names the scaffolded auth provider copied',
578
+ summary: `The scaffolded provider uses ${v2Config.localNames.join(', ')}, declared in ${v2Config.filePath} and not exported to ${filePath}.`,
579
+ action: 'Move each declaration into the provider file, or export it and import it there.',
580
+ locations: [{ filePath }, { filePath: v2Config.filePath, line: v2Config.line }],
581
+ source: 'legacy-query-creator-migration',
582
+ dedupeKey: `auth-provider-local-names:${filePath}:${authProviderName}`,
583
+ });
584
+ }
564
585
  body.warnings.forEach((warning) => {
565
586
  report.addWarning({
566
587
  ...warning,
@@ -606,8 +627,40 @@ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, cre
606
627
  createdProviders.forEach((filePath) => console.log(` - ${filePath}`));
607
628
  }
608
629
  };
609
- const addAuthProviderToFile = (content, clientName, body) => {
610
- const nextContent = ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), ['toInjectFn', 'toProvideFn']);
630
+ const addCopiedImports = (content, v2Config, filePath) => {
631
+ if (!v2Config || v2Config.filePath === filePath) {
632
+ return content;
633
+ }
634
+ const sourceFile = createSourceFile(content);
635
+ const bound = new Set();
636
+ let lastImportEnd = 0;
637
+ sourceFile.statements.forEach((statement) => {
638
+ if (!ts.isImportDeclaration(statement)) {
639
+ return;
640
+ }
641
+ lastImportEnd = Math.max(lastImportEnd, statement.getEnd());
642
+ const clause = statement.importClause;
643
+ if (clause?.name)
644
+ bound.add(clause.name.text);
645
+ if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings))
646
+ bound.add(clause.namedBindings.name.text);
647
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
648
+ clause.namedBindings.elements.forEach((element) => bound.add(element.name.text));
649
+ }
650
+ });
651
+ const statements = v2Config.copiedImports
652
+ .filter(({ names }) => names.every((name) => !bound.has(name)))
653
+ .map(({ moduleSpecifier, statement }) => statement(rebaseModuleSpecifier(moduleSpecifier, v2Config.filePath, filePath)));
654
+ if (statements.length === 0) {
655
+ return content;
656
+ }
657
+ return content.slice(0, lastImportEnd) + `\n${statements.join('\n')}` + content.slice(lastImportEnd);
658
+ };
659
+ const addAuthProviderToFile = (content, clientName, body, v2Config, filePath) => {
660
+ const nextContent = addCopiedImports(ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), [
661
+ 'toInjectFn',
662
+ 'toProvideFn',
663
+ ]), v2Config, filePath);
611
664
  const sourceFile = createSourceFile(nextContent);
612
665
  const configName = ensureConfigSuffix(clientName);
613
666
  const configPosition = findVariableStatementEnd(sourceFile, configName, 'createQueryClient');
@@ -1,5 +1,6 @@
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, 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';
@@ -11,11 +12,11 @@ export default async function migrate(tree, schema) {
11
12
  const scope = createMigrationScope(tree, schema);
12
13
  console.log(` Scope: ${scope.describe()}`);
13
14
  const report = new QueryV3MigrationReport();
14
- const { queryClientFiles, variableRenames } = migrateQueryClients(tree, report, scope);
15
+ const { queryClientFiles, variableRenames, gqlGetClients } = migrateQueryClients(tree, report, scope);
15
16
  if (queryClientFiles.size > 0) {
16
17
  generateProviderAliases(tree, queryClientFiles);
17
18
  generateQueryCreators(tree, queryClientFiles);
18
- createNewQueryCreators(tree, queryClientFiles, report, scope);
19
+ createNewQueryCreators(tree, queryClientFiles, report, scope, gqlGetClients);
19
20
  updateLegacyCreatorImportsAndUsages(tree, scope, report);
20
21
  }
21
22
  if (variableRenames.size > 0) {
@@ -26,6 +27,8 @@ export default async function migrate(tree, schema) {
26
27
  migrateEmptyPrepareCalls(tree, scope, report);
27
28
  migrateLegacyPrepareCalls(tree, report, scope);
28
29
  reportRemovedExperimentalQueryHelpers(tree, scope, report);
30
+ reportLeftoverV2Clients(tree, scope, report);
31
+ reportAuthConsumers(tree, scope, report);
29
32
  if (queryClientFiles.size > 0) {
30
33
  reportMissingHttpClientProviders(tree, report, scope);
31
34
  reportDefaultHeaderUsages(tree, report, scope);