@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.
- package/CHANGELOG.md +32 -0
- package/fesm2022/ethlete-query-devtools-contract.mjs +1 -1
- package/fesm2022/ethlete-query.mjs +74 -17
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/migrate-from-ngrx-toolkit/consumers.js +1 -1
- package/generators/migrate-to-query-v3/auth-consumers-migration.js +64 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +93 -1
- package/generators/migrate-to-query-v3/cleanup-migration.js +64 -5
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +234 -19
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +68 -15
- package/generators/migrate-to-query-v3/migration.js +6 -3
- package/generators/migrate-to-query-v3/query-client-migration.js +82 -14
- package/generators/move-devtools-contract-imports/moved-names.js +1 -0
- package/migrations.json +10 -0
- package/package.json +1 -1
- package/types/ethlete-query-devtools-contract.d.ts +2 -2
- package/types/ethlete-query.d.ts +18 -3
|
@@ -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
|
|
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
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
689
|
-
summary: `${usage.creatorName}.
|
|
690
|
-
action: '
|
|
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}.
|
|
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
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
|
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,
|
|
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
|
|
141
|
-
|
|
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 \`${
|
|
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 =
|
|
509
|
-
|
|
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
|
|
610
|
-
|
|
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 {
|
|
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);
|