@ethlete/query 6.0.0-next.4 ā 6.0.0-next.41
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 +593 -0
- package/fesm2022/ethlete-query-testing.mjs +314 -5
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +13767 -4881
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/deprecate-legacy-queries/migration.js +77 -0
- package/generators/deprecate-legacy-queries/schema.json +26 -0
- package/generators/generators.json +15 -0
- package/generators/migrate-query-client-features/migration.js +142 -0
- package/generators/migrate-query-client-features/report.js +136 -0
- package/generators/migrate-query-client-features/schema.json +26 -0
- package/generators/migrate-query-opt-in-features/migration.js +182 -0
- package/generators/migrate-query-opt-in-features/report.js +143 -0
- package/generators/migrate-query-opt-in-features/schema.json +31 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +131 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +128 -77
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +120 -28
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +158 -139
- package/generators/migrate-to-query-v3/migration-scope.js +41 -0
- package/generators/migrate-to-query-v3/migration.js +17 -9
- package/generators/migrate-to-query-v3/module-graph.js +153 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +58 -9
- package/generators/migrate-to-query-v3/rename-symbols.js +193 -0
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +45 -0
- package/generators/prep-for-query-v3/migration.js +3 -3
- package/package.json +26 -13
- package/types/ethlete-query-testing.d.ts +145 -12
- package/types/ethlete-query.d.ts +8429 -3138
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
1
|
import * as ts from 'typescript';
|
|
3
2
|
import { createSourceFile, ensureAngularCoreImports, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
|
|
4
|
-
|
|
3
|
+
/** Contexts that already have an injector of their own, so the call site needs no rewrite. */
|
|
4
|
+
const CONTEXTS_WITH_INJECTOR = [
|
|
5
|
+
'queryComputed',
|
|
6
|
+
'injection-context',
|
|
7
|
+
'class-field',
|
|
8
|
+
'constructor',
|
|
9
|
+
];
|
|
10
|
+
export const migrateLegacyPrepareCalls = (tree, report, scope) => {
|
|
5
11
|
console.log('\nš Checking for legacy query creator usages...');
|
|
6
12
|
const allUsages = [];
|
|
7
|
-
|
|
13
|
+
scope.visit(tree, (filePath) => {
|
|
8
14
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
9
15
|
return;
|
|
10
16
|
}
|
|
@@ -27,7 +33,7 @@ export const migrateLegacyPrepareCalls = (tree, report) => {
|
|
|
27
33
|
console.log(` - ${context}: ${count} usages`);
|
|
28
34
|
});
|
|
29
35
|
console.log('\nš Detecting polling usage...');
|
|
30
|
-
const pollingInfo = detectPollingUsage(tree);
|
|
36
|
+
const pollingInfo = detectPollingUsage(tree, scope);
|
|
31
37
|
if (pollingInfo.size > 0) {
|
|
32
38
|
console.log(`\nā ļø Found ${pollingInfo.size} queries with polling:`);
|
|
33
39
|
pollingInfo.forEach((info) => {
|
|
@@ -38,7 +44,7 @@ export const migrateLegacyPrepareCalls = (tree, report) => {
|
|
|
38
44
|
}
|
|
39
45
|
const classInjectors = findOrCreateInjectorMembers(tree, allUsages);
|
|
40
46
|
createInjectorMembers(tree, classInjectors);
|
|
41
|
-
transformLegacyCreatorPrepareCalls(tree, classInjectors, pollingInfo);
|
|
47
|
+
transformLegacyCreatorPrepareCalls(tree, classInjectors, pollingInfo, scope);
|
|
42
48
|
reportManualReviewLocations(tree, allUsages, report);
|
|
43
49
|
};
|
|
44
50
|
const findLegacyCreatorUsages = (content, filePath) => {
|
|
@@ -99,32 +105,88 @@ const findLegacyCreatorNames = (sourceFile) => {
|
|
|
99
105
|
visit(sourceFile);
|
|
100
106
|
return names;
|
|
101
107
|
};
|
|
108
|
+
/** Helpers that run their callback inside an injection context, so a `prepare()` in one needs nothing. */
|
|
109
|
+
const CONTEXT_PROVIDING_CALLEES = new Set([
|
|
110
|
+
'runInInjectionContext',
|
|
111
|
+
'queryComputed',
|
|
112
|
+
'queryComputedTillTruthy',
|
|
113
|
+
'queryArrayComputed',
|
|
114
|
+
]);
|
|
115
|
+
/**
|
|
116
|
+
* Array methods, which call back synchronously in the caller's own context. Matched only on a property
|
|
117
|
+
* access (`items.map(ā¦)`) - the bare-identifier form is an RxJS operator, whose callback runs later.
|
|
118
|
+
*/
|
|
119
|
+
const TRANSPARENT_ARRAY_METHODS = new Set([
|
|
120
|
+
'map',
|
|
121
|
+
'flatMap',
|
|
122
|
+
'filter',
|
|
123
|
+
'forEach',
|
|
124
|
+
'find',
|
|
125
|
+
'findLast',
|
|
126
|
+
'findIndex',
|
|
127
|
+
'some',
|
|
128
|
+
'every',
|
|
129
|
+
'reduce',
|
|
130
|
+
'reduceRight',
|
|
131
|
+
'sort',
|
|
132
|
+
'flat',
|
|
133
|
+
]);
|
|
134
|
+
const isCallbackFunction = (node) => ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node);
|
|
135
|
+
/** The call a function literal is being passed to, if it is an argument rather than a value. */
|
|
136
|
+
const getCallbackHost = (node) => {
|
|
137
|
+
const parent = node.parent;
|
|
138
|
+
if (!ts.isCallExpression(parent) || !parent.arguments.some((argument) => argument === node)) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return parent;
|
|
142
|
+
};
|
|
143
|
+
const isContextProvidingCallback = (node) => {
|
|
144
|
+
const host = getCallbackHost(node);
|
|
145
|
+
if (!host) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (ts.isIdentifier(host.expression)) {
|
|
149
|
+
return CONTEXT_PROVIDING_CALLEES.has(host.expression.text);
|
|
150
|
+
}
|
|
151
|
+
return ts.isPropertyAccessExpression(host.expression) && host.expression.name.text === 'runInContext';
|
|
152
|
+
};
|
|
153
|
+
const isTransparentCallback = (node) => {
|
|
154
|
+
const host = getCallbackHost(node);
|
|
155
|
+
return (!!host && ts.isPropertyAccessExpression(host.expression) && TRANSPARENT_ARRAY_METHODS.has(host.expression.name.text));
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Classifies the **innermost** function boundary the call sits behind, not the first structural ancestor.
|
|
159
|
+
* The difference is the whole point: `effect(() => prepare())` in a constructor and
|
|
160
|
+
* `computed(() => prepare())` at a class field both run long after the context that created them, so they
|
|
161
|
+
* need an injector just like a plain method does - reading the constructor or the field as the answer
|
|
162
|
+
* skipped exactly the call sites that go on to throw ET950.
|
|
163
|
+
*/
|
|
102
164
|
const determineUsageContext = (node) => {
|
|
103
165
|
let current = node;
|
|
104
166
|
while (current) {
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
ts.isIdentifier(
|
|
109
|
-
|
|
110
|
-
|
|
167
|
+
if (isCallbackFunction(current)) {
|
|
168
|
+
if (isContextProvidingCallback(current)) {
|
|
169
|
+
const host = getCallbackHost(current);
|
|
170
|
+
const callee = host && ts.isIdentifier(host.expression) ? host.expression.text : undefined;
|
|
171
|
+
return callee?.startsWith('query') ? 'queryComputed' : 'injection-context';
|
|
172
|
+
}
|
|
173
|
+
if (!isTransparentCallback(current)) {
|
|
174
|
+
return isInsideClass(current) ? 'method' : 'function';
|
|
111
175
|
}
|
|
112
176
|
}
|
|
177
|
+
// Reached without crossing a callback: the call really does run while the class is being built, which
|
|
178
|
+
// is an injection context.
|
|
113
179
|
if (ts.isConstructorDeclaration(current)) {
|
|
114
180
|
return 'constructor';
|
|
115
181
|
}
|
|
116
|
-
if (ts.isMethodDeclaration(current)
|
|
182
|
+
if (ts.isMethodDeclaration(current) ||
|
|
183
|
+
ts.isGetAccessorDeclaration(current) ||
|
|
184
|
+
ts.isSetAccessorDeclaration(current)) {
|
|
117
185
|
return 'method';
|
|
118
186
|
}
|
|
119
187
|
if (ts.isPropertyDeclaration(current)) {
|
|
120
188
|
return 'class-field';
|
|
121
189
|
}
|
|
122
|
-
if (ts.isFunctionDeclaration(current)) {
|
|
123
|
-
return 'function';
|
|
124
|
-
}
|
|
125
|
-
if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) && !isInsideClass(current)) {
|
|
126
|
-
return 'function';
|
|
127
|
-
}
|
|
128
190
|
current = current.parent;
|
|
129
191
|
}
|
|
130
192
|
return 'unknown';
|
|
@@ -178,7 +240,7 @@ const findOrCreateInjectorMembers = (tree, usages) => {
|
|
|
178
240
|
const classInjectors = new Map();
|
|
179
241
|
const usagesByFile = new Map();
|
|
180
242
|
usages.forEach((usage) => {
|
|
181
|
-
if (
|
|
243
|
+
if (CONTEXTS_WITH_INJECTOR.includes(usage.context)) {
|
|
182
244
|
return;
|
|
183
245
|
}
|
|
184
246
|
if (!usagesByFile.has(usage.filePath)) {
|
|
@@ -283,7 +345,7 @@ const addInjectorMemberToClass = (content, classInfo) => {
|
|
|
283
345
|
const injectorMember = `${indentation}private ${classInfo.injectorMemberName} = inject(Injector);\n\n`;
|
|
284
346
|
return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
|
|
285
347
|
};
|
|
286
|
-
const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo) => {
|
|
348
|
+
const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo, scope) => {
|
|
287
349
|
const updatedFiles = [];
|
|
288
350
|
const filesByPath = new Map();
|
|
289
351
|
classInjectors.forEach((classInfo) => {
|
|
@@ -292,7 +354,7 @@ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo) =
|
|
|
292
354
|
}
|
|
293
355
|
filesByPath.get(classInfo.filePath).push(classInfo);
|
|
294
356
|
});
|
|
295
|
-
|
|
357
|
+
scope.visit(tree, (filePath) => {
|
|
296
358
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts') || filesByPath.has(filePath)) {
|
|
297
359
|
return;
|
|
298
360
|
}
|
|
@@ -343,7 +405,7 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
|
|
|
343
405
|
return;
|
|
344
406
|
}
|
|
345
407
|
const context = determineUsageContext(node);
|
|
346
|
-
if (
|
|
408
|
+
if (CONTEXTS_WITH_INJECTOR.includes(context)) {
|
|
347
409
|
ts.forEachChild(node, visit);
|
|
348
410
|
return;
|
|
349
411
|
}
|
|
@@ -536,7 +598,7 @@ const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pol
|
|
|
536
598
|
}
|
|
537
599
|
nextArgumentProperties.push(`injector: ${injectorReference}`);
|
|
538
600
|
const queryVariableName = findQueryVariableNameForPrepareCall(callNode, sourceFile);
|
|
539
|
-
const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo);
|
|
601
|
+
const addDestroyOnResponse = shouldAddDestroyOnResponse(queryVariableName, pollingInfo, isDiscardedPrepareCall(callNode));
|
|
540
602
|
if (existingConfig && ts.isObjectLiteralExpression(existingConfig.initializer)) {
|
|
541
603
|
const configProperties = [];
|
|
542
604
|
existingConfig.initializer.properties.forEach((property) => {
|
|
@@ -569,7 +631,7 @@ const transformSinglePrepareCall = (callNode, injectorReference, sourceFile, pol
|
|
|
569
631
|
: `{\n ${nextArgumentProperties.join(',\n ')}\n}`;
|
|
570
632
|
return `${creatorName}.prepare(${nextArgumentString})`;
|
|
571
633
|
};
|
|
572
|
-
const findQueryVariableNameForPrepareCall = (prepareCallNode,
|
|
634
|
+
const findQueryVariableNameForPrepareCall = (prepareCallNode, _sourceFile) => {
|
|
573
635
|
let parent = prepareCallNode.parent;
|
|
574
636
|
while (parent) {
|
|
575
637
|
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
@@ -663,9 +725,9 @@ const containsInjectCall = (node) => {
|
|
|
663
725
|
visit(node);
|
|
664
726
|
return hasInject;
|
|
665
727
|
};
|
|
666
|
-
const detectPollingUsage = (tree) => {
|
|
728
|
+
const detectPollingUsage = (tree, scope) => {
|
|
667
729
|
const pollingInfo = new Map();
|
|
668
|
-
|
|
730
|
+
scope.visit(tree, (filePath) => {
|
|
669
731
|
if (filePath.endsWith('.spec.ts')) {
|
|
670
732
|
return;
|
|
671
733
|
}
|
|
@@ -764,9 +826,39 @@ const detectPollingInTemplate = (content, filePath, pollingInfo) => {
|
|
|
764
826
|
}
|
|
765
827
|
}
|
|
766
828
|
};
|
|
767
|
-
|
|
829
|
+
/**
|
|
830
|
+
* Whether the prepared query is thrown away - `legacyGetUsers.prepare().execute()` as a statement, with
|
|
831
|
+
* nothing holding on to it. Chained `.poll()` disqualifies it: that query is meant to keep running.
|
|
832
|
+
*/
|
|
833
|
+
const isDiscardedPrepareCall = (callNode) => {
|
|
834
|
+
let current = callNode;
|
|
835
|
+
while (current.parent) {
|
|
836
|
+
const parent = current.parent;
|
|
837
|
+
if (ts.isParenthesizedExpression(parent) || ts.isAwaitExpression(parent) || ts.isNonNullExpression(parent)) {
|
|
838
|
+
current = parent;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === current) {
|
|
842
|
+
if (parent.name.text === 'poll') {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
current = parent;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
// Only a chained call keeps it discarded; being an *argument* means something else receives the query.
|
|
849
|
+
if (ts.isCallExpression(parent) && parent.expression === current) {
|
|
850
|
+
current = parent;
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
return ts.isExpressionStatement(parent);
|
|
854
|
+
}
|
|
855
|
+
return false;
|
|
856
|
+
};
|
|
857
|
+
const shouldAddDestroyOnResponse = (queryVariableName, pollingInfo, isDiscarded) => {
|
|
768
858
|
if (!queryVariableName) {
|
|
769
|
-
|
|
859
|
+
// A discarded query is the one case where nothing else can clean it up: no variable for a container
|
|
860
|
+
// to hold, no caller to destroy it. That is exactly what `destroyOnResponse` is for.
|
|
861
|
+
return isDiscarded;
|
|
770
862
|
}
|
|
771
863
|
return !pollingInfo.get(queryVariableName)?.hasPolling;
|
|
772
864
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
1
|
import * as ts from 'typescript';
|
|
3
|
-
import {
|
|
2
|
+
import { collectV2BearerAuthConfigs, renderAuthProviderBody } from './auth-provider-scaffold.js';
|
|
3
|
+
import { createModuleGraph } from './module-graph.js';
|
|
4
|
+
import { pruneUnusedNamedImports, renameImportedReferences } from './rename-symbols.js';
|
|
5
|
+
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, legacyQueryDeprecationTag, } from './shared.js';
|
|
4
6
|
const LEGACY_HTTP_METHOD = {
|
|
5
7
|
GET: 'get',
|
|
6
8
|
POST: 'post',
|
|
@@ -11,16 +13,17 @@ const LEGACY_HTTP_METHOD = {
|
|
|
11
13
|
const isLegacyHttpMethod = (value) => {
|
|
12
14
|
return Object.values(LEGACY_HTTP_METHOD).includes(value);
|
|
13
15
|
};
|
|
14
|
-
export const createNewQueryCreators = (tree, queryClientFiles, report) => {
|
|
16
|
+
export const createNewQueryCreators = (tree, queryClientFiles, report, scope) => {
|
|
15
17
|
const createdFiles = [];
|
|
16
18
|
const authProvidersNeeded = new Map();
|
|
17
19
|
const clientNameToFilePath = new Map();
|
|
20
|
+
const creatorToClient = new Map();
|
|
18
21
|
queryClientFiles.forEach((configNames, filePath) => {
|
|
19
22
|
configNames.forEach((configName) => {
|
|
20
23
|
clientNameToFilePath.set(configName, filePath);
|
|
21
24
|
});
|
|
22
25
|
});
|
|
23
|
-
|
|
26
|
+
scope.visit(tree, (filePath) => {
|
|
24
27
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
@@ -33,6 +36,7 @@ export const createNewQueryCreators = (tree, queryClientFiles, report) => {
|
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
legacyCreators.forEach((creator) => {
|
|
39
|
+
creatorToClient.set(creator.name, creator.clientName);
|
|
36
40
|
if (!creator.secure) {
|
|
37
41
|
return;
|
|
38
42
|
}
|
|
@@ -48,17 +52,23 @@ export const createNewQueryCreators = (tree, queryClientFiles, report) => {
|
|
|
48
52
|
}
|
|
49
53
|
});
|
|
50
54
|
if (authProvidersNeeded.size > 0) {
|
|
51
|
-
createAuthProviders(
|
|
55
|
+
createAuthProviders({
|
|
56
|
+
tree,
|
|
57
|
+
authProvidersNeeded,
|
|
58
|
+
report,
|
|
59
|
+
v2Configs: collectV2BearerAuthConfigs(tree, scope),
|
|
60
|
+
creatorToClient,
|
|
61
|
+
});
|
|
52
62
|
}
|
|
53
63
|
if (createdFiles.length > 0) {
|
|
54
64
|
console.log('\nā
Created new query creators in:');
|
|
55
65
|
createdFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
56
66
|
}
|
|
57
67
|
};
|
|
58
|
-
export const updateLegacyCreatorImportsAndUsages = (tree) => {
|
|
68
|
+
export const updateLegacyCreatorImportsAndUsages = (tree, scope, report) => {
|
|
59
69
|
const legacyCreators = new Map();
|
|
60
70
|
const updatedFiles = [];
|
|
61
|
-
|
|
71
|
+
scope.visit(tree, (filePath) => {
|
|
62
72
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
@@ -66,16 +76,16 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
|
|
|
66
76
|
if (!content) {
|
|
67
77
|
return;
|
|
68
78
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
legacyCreators.set(originalName, legacyName);
|
|
79
|
+
findLegacyWrappers(content).forEach((legacyName, originalName) => {
|
|
80
|
+
legacyCreators.set(originalName, { legacyName, definedIn: filePath });
|
|
72
81
|
});
|
|
73
82
|
});
|
|
74
83
|
if (legacyCreators.size === 0) {
|
|
75
84
|
return;
|
|
76
85
|
}
|
|
77
86
|
console.log(`\nā
Found ${legacyCreators.size} legacy query creator wrappers`);
|
|
78
|
-
|
|
87
|
+
const moduleGraph = createModuleGraph(tree);
|
|
88
|
+
scope.visit(tree, (filePath) => {
|
|
79
89
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
80
90
|
return;
|
|
81
91
|
}
|
|
@@ -83,8 +93,13 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
|
|
|
83
93
|
if (!content || containsLegacyWrapperDefinitions(content)) {
|
|
84
94
|
return;
|
|
85
95
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
96
|
+
const nextContent = pointFileAtLegacyWrappers({
|
|
97
|
+
content,
|
|
98
|
+
filePath,
|
|
99
|
+
legacyCreators,
|
|
100
|
+
moduleGraph,
|
|
101
|
+
report,
|
|
102
|
+
});
|
|
88
103
|
if (nextContent !== content) {
|
|
89
104
|
tree.write(filePath, nextContent);
|
|
90
105
|
updatedFiles.push(filePath);
|
|
@@ -95,6 +110,89 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
|
|
|
95
110
|
updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
96
111
|
}
|
|
97
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* Rewrites this file's imports of migrated creators to the `legacy*` wrappers.
|
|
115
|
+
*
|
|
116
|
+
* Two rules keep the rewrite honest, and both exist because the first version of this pass did not
|
|
117
|
+
* have them:
|
|
118
|
+
*
|
|
119
|
+
* - An import is only touched when the module it comes from really does resolve to the file that
|
|
120
|
+
* defines the wrapper. A same-named export from an unrelated package stays as it is.
|
|
121
|
+
* - An alias is preserved (`legacyGetPerson as getPersonQuery`) instead of being dropped, so the
|
|
122
|
+
* call sites using that alias keep compiling.
|
|
123
|
+
*/
|
|
124
|
+
const pointFileAtLegacyWrappers = ({ content, filePath, legacyCreators, moduleGraph, report, }) => {
|
|
125
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
126
|
+
const replacements = [];
|
|
127
|
+
const referenceRenames = new Map();
|
|
128
|
+
const resolvesToWrapper = (specifier, importedName, registration) => moduleGraph.findDeclaringFile(filePath, specifier, importedName) === registration.definedIn;
|
|
129
|
+
const rewriteSpecifiers = (elements, moduleSpecifier) => {
|
|
130
|
+
let changed = false;
|
|
131
|
+
const nextElements = elements.map((element) => {
|
|
132
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
133
|
+
const registration = legacyCreators.get(importedName);
|
|
134
|
+
if (!registration || !resolvesToWrapper(moduleSpecifier, importedName, registration)) {
|
|
135
|
+
return element.getText(sourceFile);
|
|
136
|
+
}
|
|
137
|
+
changed = true;
|
|
138
|
+
// Aliased: only the imported half changes, so nothing in the file body has to move.
|
|
139
|
+
if (element.propertyName) {
|
|
140
|
+
return `${registration.legacyName} as ${element.name.text}`;
|
|
141
|
+
}
|
|
142
|
+
referenceRenames.set(element.name.text, registration.legacyName);
|
|
143
|
+
return registration.legacyName;
|
|
144
|
+
});
|
|
145
|
+
return changed ? nextElements : null;
|
|
146
|
+
};
|
|
147
|
+
for (const node of sourceFile.statements) {
|
|
148
|
+
if (ts.isImportDeclaration(node) &&
|
|
149
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
150
|
+
node.importClause?.namedBindings &&
|
|
151
|
+
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
152
|
+
const nextElements = rewriteSpecifiers(node.importClause.namedBindings.elements, node.moduleSpecifier.text);
|
|
153
|
+
if (nextElements) {
|
|
154
|
+
replacements.push({
|
|
155
|
+
start: node.getStart(sourceFile),
|
|
156
|
+
end: node.getEnd(),
|
|
157
|
+
replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Re-exports need the same treatment, or a barrel keeps pointing at the v3 creator.
|
|
163
|
+
if (ts.isExportDeclaration(node) &&
|
|
164
|
+
node.moduleSpecifier &&
|
|
165
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
166
|
+
node.exportClause &&
|
|
167
|
+
ts.isNamedExports(node.exportClause)) {
|
|
168
|
+
const nextElements = rewriteSpecifiers(node.exportClause.elements, node.moduleSpecifier.text);
|
|
169
|
+
if (nextElements) {
|
|
170
|
+
replacements.push({
|
|
171
|
+
start: node.getStart(sourceFile),
|
|
172
|
+
end: node.getEnd(),
|
|
173
|
+
replacement: `export { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
let result = content;
|
|
179
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
180
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
181
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
182
|
+
});
|
|
183
|
+
const renamed = renameImportedReferences(result, referenceRenames);
|
|
184
|
+
renamed.shadowed.forEach((name) => {
|
|
185
|
+
report.addManualReview({
|
|
186
|
+
title: `Point ${name} at its legacy wrapper by hand`,
|
|
187
|
+
summary: `${filePath} imports the migrated creator \`${name}\` but also declares something with that name, so the migration left its usages alone to avoid rewriting the wrong symbol.`,
|
|
188
|
+
action: `Rename the local declaration or the import alias, then point the query usages at \`${legacyCreators.get(name)?.legacyName ?? `legacy${capitalizeFirstLetter(name)}`}\`.`,
|
|
189
|
+
locations: [{ filePath }],
|
|
190
|
+
source: 'legacy-query-creator-migration',
|
|
191
|
+
dedupeKey: `shadowed-rename:${filePath}:${name}`,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
return renamed.content;
|
|
195
|
+
};
|
|
98
196
|
const toLegacyName = (name) => `legacy${capitalizeFirstLetter(name)}`;
|
|
99
197
|
const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report) => {
|
|
100
198
|
const sourceFile = createSourceFile(content, filePath);
|
|
@@ -233,7 +331,10 @@ const transformLegacyQueryCreators = (content, legacyCreators) => {
|
|
|
233
331
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
234
332
|
});
|
|
235
333
|
result = ensureImportFromQuery(result, ['createLegacyQueryCreator']);
|
|
236
|
-
|
|
334
|
+
result = addCreatorImports(result, creatorsByClient);
|
|
335
|
+
// The rewritten creators no longer go through `def<T>()` or the v2 client object, so whatever is
|
|
336
|
+
// left over from those constructs would land as a lint error the moment `formatFiles` runs.
|
|
337
|
+
return pruneUnusedNamedImports(result, 'all');
|
|
237
338
|
};
|
|
238
339
|
const addCreatorImports = (content, creatorsByClient) => {
|
|
239
340
|
const sourceFile = createSourceFile(content);
|
|
@@ -340,9 +441,11 @@ const getTypeParameter = (creator) => {
|
|
|
340
441
|
return '';
|
|
341
442
|
};
|
|
342
443
|
const generateLegacyWrapper = (creator) => {
|
|
343
|
-
|
|
444
|
+
// `name` is what lets a `prepare()` called outside an injection context name itself in the error
|
|
445
|
+
// instead of surfacing as a bare NG0203.
|
|
446
|
+
return `/**\n * ${legacyQueryDeprecationTag(creator.name)}\n */\nexport const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ name: '${toLegacyName(creator.name)}', creator: ${creator.name} });`;
|
|
344
447
|
};
|
|
345
|
-
const createAuthProviders = (tree, authProvidersNeeded, report) => {
|
|
448
|
+
const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, creatorToClient, }) => {
|
|
346
449
|
const createdProviders = [];
|
|
347
450
|
authProvidersNeeded.forEach((filePath, clientName) => {
|
|
348
451
|
const content = tree.read(filePath, 'utf-8');
|
|
@@ -353,19 +456,46 @@ const createAuthProviders = (tree, authProvidersNeeded, report) => {
|
|
|
353
456
|
if (content.includes(`export const ${authProviderName} = createBearerAuthProvider(`)) {
|
|
354
457
|
return;
|
|
355
458
|
}
|
|
356
|
-
|
|
459
|
+
// Match on the refresh creator: it is the one thing a v2 provider config always names, and it
|
|
460
|
+
// belongs to exactly one client.
|
|
461
|
+
const v2Config = v2Configs.find((candidate) => candidate.refreshCreatorName && creatorToClient.get(candidate.refreshCreatorName) === clientName);
|
|
462
|
+
const body = renderAuthProviderBody(v2Config);
|
|
463
|
+
const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body), clientName);
|
|
357
464
|
if (nextContent === content) {
|
|
358
465
|
return;
|
|
359
466
|
}
|
|
360
467
|
tree.write(filePath, nextContent);
|
|
361
468
|
createdProviders.push(filePath);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
469
|
+
if (body.isScaffolded) {
|
|
470
|
+
report.addFollowUp({
|
|
471
|
+
title: `Finish the scaffolded auth provider ${authProviderName}`,
|
|
472
|
+
summary: `${authProviderName} was scaffolded from the v2 BearerAuthProvider at ${v2Config?.filePath}:${v2Config?.line}. The refresh query and cookie config carried over; the adapters were copied verbatim and their parameter shapes changed between v2 and v3.`,
|
|
473
|
+
action: 'Resolve the TODO(query-v3) comments in the generated provider, add the login query via withAuthenticationQuery, and delete the v2 provider once the app logs in again.',
|
|
474
|
+
locations: [{ filePath }, ...(v2Config ? [{ filePath: v2Config.filePath, line: v2Config.line }] : [])],
|
|
475
|
+
source: 'legacy-query-creator-migration',
|
|
476
|
+
dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
report.addFollowUp({
|
|
481
|
+
title: `Configure auth queries for ${authProviderName}`,
|
|
482
|
+
summary: `The migration created ${authProviderName} with an empty \`queries\` array because no v2 BearerAuthProvider config could be matched to this client.`,
|
|
483
|
+
action: 'Add authQuery/tokenRefreshQuery builders, verify token extraction, and remove the placeholder empty array once the provider is configured.',
|
|
484
|
+
locations: [{ filePath }],
|
|
485
|
+
source: 'legacy-query-creator-migration',
|
|
486
|
+
dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
// The generated layout only works if the auth queries live next to the provider: the provider
|
|
490
|
+
// needs the login/refresh creators, and the secure creators need the provider. Leaving them in
|
|
491
|
+
// an `auth.queries.ts` that imports the client's `post` creator closes the loop.
|
|
492
|
+
report.addManualReview({
|
|
493
|
+
title: `Keep the auth queries in ${filePath}`,
|
|
494
|
+
summary: `${authProviderName} needs the login and refresh creators, and the secure creators below it need the provider. If those creators stay in a separate auth queries file that imports this client, the two files form an import cycle.`,
|
|
495
|
+
action: `Move the login/refresh creators into ${filePath} above the provider, and have the auth queries file import them back for its legacy* wrappers.`,
|
|
366
496
|
locations: [{ filePath }],
|
|
367
497
|
source: 'legacy-query-creator-migration',
|
|
368
|
-
dedupeKey: `auth-provider:${filePath}
|
|
498
|
+
dedupeKey: `auth-provider-cycle:${filePath}`,
|
|
369
499
|
});
|
|
370
500
|
});
|
|
371
501
|
if (createdProviders.length > 0) {
|
|
@@ -373,8 +503,8 @@ const createAuthProviders = (tree, authProvidersNeeded, report) => {
|
|
|
373
503
|
createdProviders.forEach((filePath) => console.log(` - ${filePath}`));
|
|
374
504
|
}
|
|
375
505
|
};
|
|
376
|
-
const addAuthProviderToFile = (content, clientName) => {
|
|
377
|
-
const nextContent = ensureImportFromQuery(content, ['createBearerAuthProvider']);
|
|
506
|
+
const addAuthProviderToFile = (content, clientName, body) => {
|
|
507
|
+
const nextContent = ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), ['toInjectFn', 'toProvideFn']);
|
|
378
508
|
const sourceFile = createSourceFile(nextContent);
|
|
379
509
|
const configName = ensureConfigSuffix(clientName);
|
|
380
510
|
const configPosition = findVariableStatementEnd(sourceFile, configName, 'createQueryClient');
|
|
@@ -388,10 +518,12 @@ const addAuthProviderToFile = (content, clientName) => {
|
|
|
388
518
|
`export const ${authProviderName} = createBearerAuthProvider({`,
|
|
389
519
|
` name: '${clientName}',`,
|
|
390
520
|
` queryClientRef: ${configName},`,
|
|
391
|
-
|
|
521
|
+
body.queries,
|
|
522
|
+
...(body.features ? [body.features] : []),
|
|
392
523
|
'});',
|
|
393
524
|
'',
|
|
394
|
-
`export const
|
|
525
|
+
`export const provide${capitalizedClientName}AuthProvider = toProvideFn(${authProviderName});`,
|
|
526
|
+
`export const inject${capitalizedClientName}AuthProvider = toInjectFn(${authProviderName});`,
|
|
395
527
|
].join('\n');
|
|
396
528
|
return nextContent.slice(0, configPosition) + authProviderBlock + nextContent.slice(configPosition);
|
|
397
529
|
};
|
|
@@ -473,117 +605,4 @@ const findLegacyWrappers = (content) => {
|
|
|
473
605
|
visit(sourceFile);
|
|
474
606
|
return wrappers;
|
|
475
607
|
};
|
|
476
|
-
const updateLegacyCreatorImports = (content, legacyCreators) => {
|
|
477
|
-
const sourceFile = createSourceFile(content);
|
|
478
|
-
const replacements = [];
|
|
479
|
-
const visit = (node) => {
|
|
480
|
-
if (ts.isImportDeclaration(node) &&
|
|
481
|
-
node.importClause?.namedBindings &&
|
|
482
|
-
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
483
|
-
const nextElements = node.importClause.namedBindings.elements.map((element) => {
|
|
484
|
-
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
485
|
-
const legacyName = legacyCreators.get(importedName);
|
|
486
|
-
return legacyName ?? element.getText(sourceFile);
|
|
487
|
-
});
|
|
488
|
-
const hasChanges = node.importClause.namedBindings.elements.some((element) => {
|
|
489
|
-
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
490
|
-
return legacyCreators.has(importedName);
|
|
491
|
-
});
|
|
492
|
-
if (hasChanges) {
|
|
493
|
-
replacements.push({
|
|
494
|
-
start: node.getStart(sourceFile),
|
|
495
|
-
end: node.getEnd(),
|
|
496
|
-
replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
ts.forEachChild(node, visit);
|
|
501
|
-
};
|
|
502
|
-
visit(sourceFile);
|
|
503
|
-
let result = content;
|
|
504
|
-
replacements.sort((left, right) => right.start - left.start);
|
|
505
|
-
replacements.forEach(({ start, end, replacement }) => {
|
|
506
|
-
result = result.slice(0, start) + replacement + result.slice(end);
|
|
507
|
-
});
|
|
508
|
-
return result;
|
|
509
|
-
};
|
|
510
|
-
const updateLegacyCreatorUsages = (content, legacyCreators) => {
|
|
511
|
-
const sourceFile = createSourceFile(content);
|
|
512
|
-
const replacements = [];
|
|
513
|
-
const visit = (node) => {
|
|
514
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
515
|
-
node.properties.forEach((property) => {
|
|
516
|
-
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) {
|
|
517
|
-
const nextName = legacyCreators.get(property.initializer.text);
|
|
518
|
-
if (nextName) {
|
|
519
|
-
replacements.push({
|
|
520
|
-
start: property.initializer.getStart(sourceFile),
|
|
521
|
-
end: property.initializer.getEnd(),
|
|
522
|
-
replacement: nextName,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (ts.isShorthandPropertyAssignment(property) && legacyCreators.has(property.name.text)) {
|
|
527
|
-
replacements.push({
|
|
528
|
-
start: property.getStart(sourceFile),
|
|
529
|
-
end: property.getEnd(),
|
|
530
|
-
replacement: `${property.name.text}: ${legacyCreators.get(property.name.text)}`,
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
if (ts.isIdentifier(node) && legacyCreators.has(node.text)) {
|
|
536
|
-
const parent = node.parent;
|
|
537
|
-
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
538
|
-
ts.forEachChild(node, visit);
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
if (ts.isPropertyAssignment(parent) && (parent.name === node || parent.initializer === node)) {
|
|
542
|
-
ts.forEachChild(node, visit);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
|
|
546
|
-
ts.forEachChild(node, visit);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
if (ts.isVariableDeclaration(parent) && parent.name === node) {
|
|
550
|
-
ts.forEachChild(node, visit);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (ts.isImportSpecifier(parent)) {
|
|
554
|
-
ts.forEachChild(node, visit);
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (ts.isFunctionDeclaration(parent) && parent.name === node) {
|
|
558
|
-
ts.forEachChild(node, visit);
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (ts.isMethodDeclaration(parent) && parent.name === node) {
|
|
562
|
-
ts.forEachChild(node, visit);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
if (ts.isParameter(parent) && parent.name === node) {
|
|
566
|
-
ts.forEachChild(node, visit);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
if (ts.isPropertyDeclaration(parent) && parent.name === node) {
|
|
570
|
-
ts.forEachChild(node, visit);
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
replacements.push({
|
|
574
|
-
start: node.getStart(sourceFile),
|
|
575
|
-
end: node.getEnd(),
|
|
576
|
-
replacement: legacyCreators.get(node.text),
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
ts.forEachChild(node, visit);
|
|
580
|
-
};
|
|
581
|
-
visit(sourceFile);
|
|
582
|
-
let result = content;
|
|
583
|
-
replacements.sort((left, right) => right.start - left.start);
|
|
584
|
-
replacements.forEach(({ start, end, replacement }) => {
|
|
585
|
-
result = result.slice(0, start) + replacement + result.slice(end);
|
|
586
|
-
});
|
|
587
|
-
return result;
|
|
588
|
-
};
|
|
589
608
|
//# sourceMappingURL=legacy-query-creator-migration.js.map
|