@ethlete/query 6.0.0-next.46 → 6.0.0-next.48
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 +21 -0
- package/fesm2022/ethlete-query.mjs +3 -14
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/migrate-query-opt-in-features/migration.js +13 -1
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +6 -4
- package/generators/migrate-to-query-v3/cleanup-migration.js +2 -42
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +9 -12
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +81 -15
- package/generators/migrate-to-query-v3/migration-scope.js +0 -3
- package/generators/migrate-to-query-v3/query-client-migration.js +97 -47
- package/generators/migrate-to-query-v3/rename-symbols.js +19 -6
- package/generators/migrate-to-query-v3/shared.js +6 -2
- package/generators/prep-for-query-v3/migration.js +68 -11
- package/package.json +1 -1
- package/types/ethlete-query.d.ts +2 -4
|
@@ -8,6 +8,12 @@ const CLIENT_FEATURE = 'withEthleteApiErrors';
|
|
|
8
8
|
const AUTH_FEATURE = 'withBearerAuthMultiTabSync';
|
|
9
9
|
const isMigratableFile = (filePath) => /\.(ts|mts|cts)$/.test(filePath) && !filePath.endsWith('.d.ts');
|
|
10
10
|
const propertyNamed = (config, sourceFile, name) => config.properties.find((property) => ts.isPropertyAssignment(property) && property.name.getText(sourceFile) === name);
|
|
11
|
+
const hasFeature = (config, sourceFile, feature) => {
|
|
12
|
+
const features = propertyNamed(config, sourceFile, 'features')?.initializer;
|
|
13
|
+
return (!!features &&
|
|
14
|
+
ts.isArrayLiteralExpression(features) &&
|
|
15
|
+
features.elements.some((element) => ts.isCallExpression(element) && ts.isIdentifier(element.expression) && element.expression.text === feature));
|
|
16
|
+
};
|
|
11
17
|
/** `{ enabled: false, channelName: 'x' }` -> `withBearerAuthMultiTabSync({ channelName: 'x' })`, or `null` when off. */
|
|
12
18
|
const authFeatureCallFor = (property, sourceFile) => {
|
|
13
19
|
if (!property)
|
|
@@ -76,6 +82,9 @@ const collectEdits = (filePath, sourceFile, report) => {
|
|
|
76
82
|
dedupeKey: `spread-config:${callee}`,
|
|
77
83
|
});
|
|
78
84
|
}
|
|
85
|
+
else if (isClient && hasFeature(config, sourceFile, CLIENT_FEATURE)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
79
88
|
else if (isClient) {
|
|
80
89
|
const feature = `${CLIENT_FEATURE}()`;
|
|
81
90
|
report.addFollowUp({
|
|
@@ -95,6 +104,9 @@ const collectEdits = (filePath, sourceFile, report) => {
|
|
|
95
104
|
}
|
|
96
105
|
else {
|
|
97
106
|
const syncProperty = propertyNamed(config, sourceFile, 'multiTabSync');
|
|
107
|
+
const alreadyAdded = hasFeature(config, sourceFile, AUTH_FEATURE);
|
|
108
|
+
if (alreadyAdded && !syncProperty)
|
|
109
|
+
return;
|
|
98
110
|
const value = syncProperty?.initializer;
|
|
99
111
|
if (value &&
|
|
100
112
|
value.kind !== ts.SyntaxKind.TrueKeyword &&
|
|
@@ -109,7 +121,7 @@ const collectEdits = (filePath, sourceFile, report) => {
|
|
|
109
121
|
dedupeKey: 'non-literal-multi-tab-sync',
|
|
110
122
|
});
|
|
111
123
|
}
|
|
112
|
-
const feature = authFeatureCallFor(syncProperty, sourceFile);
|
|
124
|
+
const feature = alreadyAdded ? null : authFeatureCallFor(syncProperty, sourceFile);
|
|
113
125
|
if (feature) {
|
|
114
126
|
report.addFollowUp({
|
|
115
127
|
title: 'Auth provider kept multi-tab sync',
|
|
@@ -47,17 +47,19 @@ const readConfig = (configArgument, sourceFile, filePath, node) => {
|
|
|
47
47
|
return config;
|
|
48
48
|
}
|
|
49
49
|
refreshConfig.initializer.properties.forEach((property) => {
|
|
50
|
-
|
|
50
|
+
const isShorthand = ts.isShorthandPropertyAssignment(property);
|
|
51
|
+
if ((!ts.isPropertyAssignment(property) && !isShorthand) || !ts.isIdentifier(property.name)) {
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
const key = property.name.text;
|
|
54
55
|
if (!REFRESH_CONFIG_KEYS.has(key)) {
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
|
-
const
|
|
58
|
+
const value = isShorthand ? property.name : property.initializer;
|
|
59
|
+
const text = value.getText(sourceFile);
|
|
58
60
|
if (key === 'queryCreator') {
|
|
59
|
-
if (ts.isIdentifier(
|
|
60
|
-
config.refreshCreatorName =
|
|
61
|
+
if (ts.isIdentifier(value)) {
|
|
62
|
+
config.refreshCreatorName = value.text;
|
|
61
63
|
}
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { createSourceFile, ensureImportFromQuery, ensureNamedImports
|
|
2
|
+
import { createSourceFile, ensureImportFromQuery, ensureNamedImports } from './shared.js';
|
|
3
3
|
/**
|
|
4
4
|
* Points existing devtools usage at the v3 components instead of deleting it.
|
|
5
5
|
*
|
|
@@ -222,43 +222,7 @@ const replaceAnyQueryInFile = (content) => {
|
|
|
222
222
|
replacements.forEach(({ start, end, replacement }) => {
|
|
223
223
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
224
224
|
});
|
|
225
|
-
return
|
|
226
|
-
};
|
|
227
|
-
const removeAnyQueryFromImports = (content) => {
|
|
228
|
-
const sourceFile = createSourceFile(content);
|
|
229
|
-
let importNode;
|
|
230
|
-
ts.forEachChild(sourceFile, (node) => {
|
|
231
|
-
if (ts.isImportDeclaration(node) &&
|
|
232
|
-
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
233
|
-
node.moduleSpecifier.text === '@ethlete/query') {
|
|
234
|
-
importNode = node;
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
if (!importNode?.importClause?.namedBindings || !ts.isNamedImports(importNode.importClause.namedBindings)) {
|
|
238
|
-
return content;
|
|
239
|
-
}
|
|
240
|
-
const replaced = new Map([
|
|
241
|
-
['AnyV2Query', 'AnyLegacyQuery'],
|
|
242
|
-
['AnyV2QueryCreator', 'AnyLegacyQueryCreator'],
|
|
243
|
-
]);
|
|
244
|
-
const names = new Set();
|
|
245
|
-
let changed = false;
|
|
246
|
-
importNode.importClause.namedBindings.elements.forEach((element) => {
|
|
247
|
-
const replacement = replaced.get(element.name.text);
|
|
248
|
-
if (!replacement) {
|
|
249
|
-
names.add(element.getText(sourceFile));
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
// Only add the alias that was actually imported. Adding both unconditionally is what put
|
|
253
|
-
// `AnyLegacyQuery` / `AnyLegacyQueryCreator` into hundreds of files that never referenced them.
|
|
254
|
-
changed = true;
|
|
255
|
-
names.add(replacement);
|
|
256
|
-
});
|
|
257
|
-
if (!changed) {
|
|
258
|
-
return content;
|
|
259
|
-
}
|
|
260
|
-
const nextImport = `import { ${Array.from(names).sort().join(', ')} } from '@ethlete/query';`;
|
|
261
|
-
return content.slice(0, importNode.getStart(sourceFile)) + nextImport + content.slice(importNode.getEnd());
|
|
225
|
+
return result;
|
|
262
226
|
};
|
|
263
227
|
const transformEmptyPrepareCalls = (content) => {
|
|
264
228
|
const sourceFile = createSourceFile(content);
|
|
@@ -287,8 +251,4 @@ const transformEmptyPrepareCalls = (content) => {
|
|
|
287
251
|
});
|
|
288
252
|
return result;
|
|
289
253
|
};
|
|
290
|
-
export const describeTemplateLine = (content, position, suffix) => {
|
|
291
|
-
const lineNumber = getLineNumberFromPosition(content, position);
|
|
292
|
-
return `${lineNumber} (${suffix})`;
|
|
293
|
-
};
|
|
294
254
|
//# sourceMappingURL=cleanup-migration.js.map
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { createSourceFile, ensureAngularCoreImports, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
|
|
3
|
+
/** `default` can never be a class name, so it cannot collide with a named class of the same file. */
|
|
4
|
+
const ANONYMOUS_DEFAULT_CLASS_KEY = 'default';
|
|
5
|
+
const getClassKey = (node) => node.name?.text ?? ANONYMOUS_DEFAULT_CLASS_KEY;
|
|
3
6
|
/** Contexts that already have an injector of their own, so the call site needs no rewrite. */
|
|
4
7
|
const CONTEXTS_WITH_INJECTOR = [
|
|
5
8
|
'queryComputed',
|
|
@@ -265,7 +268,7 @@ const findClassesNeedingInjector = (sourceFile, usages) => {
|
|
|
265
268
|
const filePath = usages[0]?.filePath ?? sourceFile.fileName;
|
|
266
269
|
const visit = (node) => {
|
|
267
270
|
if (ts.isClassDeclaration(node)) {
|
|
268
|
-
const className = node
|
|
271
|
+
const className = getClassKey(node);
|
|
269
272
|
const usagesInClass = usages.filter((usage) => {
|
|
270
273
|
return usage.position.start >= node.getStart(sourceFile) && usage.position.end <= node.getEnd();
|
|
271
274
|
});
|
|
@@ -331,7 +334,7 @@ const addInjectorMemberToClass = (content, classInfo) => {
|
|
|
331
334
|
const sourceFile = createSourceFile(content);
|
|
332
335
|
let targetClass;
|
|
333
336
|
const visit = (node) => {
|
|
334
|
-
if (ts.isClassDeclaration(node) && node
|
|
337
|
+
if (ts.isClassDeclaration(node) && getClassKey(node) === classInfo.className) {
|
|
335
338
|
targetClass = node;
|
|
336
339
|
}
|
|
337
340
|
ts.forEachChild(node, visit);
|
|
@@ -342,7 +345,7 @@ const addInjectorMemberToClass = (content, classInfo) => {
|
|
|
342
345
|
}
|
|
343
346
|
const classBodyStart = targetClass.members.length > 0 ? targetClass.members[0].getStart(sourceFile) : targetClass.getEnd() - 1;
|
|
344
347
|
const indentation = getIndentation(content, classBodyStart);
|
|
345
|
-
const injectorMember =
|
|
348
|
+
const injectorMember = `private ${classInfo.injectorMemberName} = inject(Injector);\n\n${indentation}`;
|
|
346
349
|
return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
|
|
347
350
|
};
|
|
348
351
|
const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo, scope) => {
|
|
@@ -412,12 +415,7 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
|
|
|
412
415
|
const containingClass = findContainingClass(node);
|
|
413
416
|
const containingFunction = findContainingStandaloneFunction(node);
|
|
414
417
|
if (containingClass) {
|
|
415
|
-
const
|
|
416
|
-
if (!className) {
|
|
417
|
-
ts.forEachChild(node, visit);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
const injectorMember = classNameToInjector.get(className);
|
|
418
|
+
const injectorMember = classNameToInjector.get(getClassKey(containingClass));
|
|
421
419
|
if (!injectorMember) {
|
|
422
420
|
ts.forEachChild(node, visit);
|
|
423
421
|
return;
|
|
@@ -481,7 +479,7 @@ const transformPrepareCallsInFile = (content, classes, pollingInfo) => {
|
|
|
481
479
|
}
|
|
482
480
|
injectorDeclarations.push({
|
|
483
481
|
position: bodyStart,
|
|
484
|
-
text:
|
|
482
|
+
text: `const ${injectorName} = inject(Injector);\n${getIndentation(content, bodyStart)}`,
|
|
485
483
|
});
|
|
486
484
|
});
|
|
487
485
|
injectorDeclarations.sort((left, right) => right.position - left.position);
|
|
@@ -779,8 +777,7 @@ const getQueryVariableName = (expression) => {
|
|
|
779
777
|
return currentExpression.text;
|
|
780
778
|
}
|
|
781
779
|
if (ts.isPropertyAccessExpression(currentExpression)) {
|
|
782
|
-
|
|
783
|
-
continue;
|
|
780
|
+
return currentExpression.name.text;
|
|
784
781
|
}
|
|
785
782
|
if (ts.isCallExpression(currentExpression)) {
|
|
786
783
|
currentExpression = currentExpression.expression;
|
|
@@ -10,6 +10,7 @@ const LEGACY_HTTP_METHOD = {
|
|
|
10
10
|
PATCH: 'patch',
|
|
11
11
|
DELETE: 'delete',
|
|
12
12
|
};
|
|
13
|
+
const HTTP_OPTION_NAMES = ['reportProgress', 'responseType', 'transferCache', 'withCredentials'];
|
|
13
14
|
const isLegacyHttpMethod = (value) => {
|
|
14
15
|
return Object.values(LEGACY_HTTP_METHOD).includes(value);
|
|
15
16
|
};
|
|
@@ -85,6 +86,7 @@ export const updateLegacyCreatorImportsAndUsages = (tree, scope, report) => {
|
|
|
85
86
|
}
|
|
86
87
|
console.log(`\n✅ Found ${legacyCreators.size} legacy query creator wrappers`);
|
|
87
88
|
const moduleGraph = createModuleGraph(tree);
|
|
89
|
+
const pendingWrites = new Map();
|
|
88
90
|
scope.visit(tree, (filePath) => {
|
|
89
91
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
90
92
|
return;
|
|
@@ -101,10 +103,14 @@ export const updateLegacyCreatorImportsAndUsages = (tree, scope, report) => {
|
|
|
101
103
|
report,
|
|
102
104
|
});
|
|
103
105
|
if (nextContent !== content) {
|
|
104
|
-
|
|
105
|
-
updatedFiles.push(filePath);
|
|
106
|
+
pendingWrites.set(filePath, nextContent);
|
|
106
107
|
}
|
|
107
108
|
});
|
|
109
|
+
// Writing a barrel mid-pass would hide its original re-exports from consumers visited after it.
|
|
110
|
+
pendingWrites.forEach((nextContent, filePath) => {
|
|
111
|
+
tree.write(filePath, nextContent);
|
|
112
|
+
updatedFiles.push(filePath);
|
|
113
|
+
});
|
|
108
114
|
if (updatedFiles.length > 0) {
|
|
109
115
|
console.log('\n✅ Updated legacy creator imports and usages in:');
|
|
110
116
|
updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
@@ -226,17 +232,62 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
226
232
|
secure: false,
|
|
227
233
|
httpOptions: new Map(),
|
|
228
234
|
};
|
|
235
|
+
const warnUnreadableConfigValue = (property, passedAs) => {
|
|
236
|
+
const propertyName = property.name.getText(sourceFile);
|
|
237
|
+
report.addWarning({
|
|
238
|
+
title: `Carry over \`${propertyName}\` of ${creatorName}`,
|
|
239
|
+
summary: `${creatorName} passes \`${propertyName}\` ${passedAs} in its v2 config. The migration cannot read its value, so it was not carried over to the v3 creator.`,
|
|
240
|
+
action: propertyName === 'secure'
|
|
241
|
+
? 'If the value is true, switch the generated creator to the matching *Secure creator of the client by hand.'
|
|
242
|
+
: 'Move what it configures onto the generated creator by hand.',
|
|
243
|
+
locations: [
|
|
244
|
+
{ filePath, line: sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile)).line + 1 },
|
|
245
|
+
],
|
|
246
|
+
source: 'legacy-query-creator-migration',
|
|
247
|
+
dedupeKey: `unreadable-config:${filePath}:${creatorName}:${propertyName}`,
|
|
248
|
+
});
|
|
249
|
+
};
|
|
229
250
|
const configObject = node.initializer.arguments[0];
|
|
230
251
|
if (configObject && ts.isObjectLiteralExpression(configObject)) {
|
|
231
252
|
configObject.properties.forEach((property) => {
|
|
253
|
+
if (ts.isSpreadAssignment(property)) {
|
|
254
|
+
report.addWarning({
|
|
255
|
+
title: `Restore the spread config of ${creatorName}`,
|
|
256
|
+
summary: `${creatorName} spreads \`${property.expression.getText(sourceFile)}\` into its v2 config. The migration cannot see what it contains, so none of it was carried over to the v3 creator.`,
|
|
257
|
+
action: 'Move the route, secure flag, types and http options it provides onto the generated creator by hand.',
|
|
258
|
+
locations: [
|
|
259
|
+
{ filePath, line: sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile)).line + 1 },
|
|
260
|
+
],
|
|
261
|
+
source: 'legacy-query-creator-migration',
|
|
262
|
+
dedupeKey: `spread-config:${filePath}:${creatorName}`,
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
267
|
+
if (property.name.text === 'route') {
|
|
268
|
+
info.route = property.name.text;
|
|
269
|
+
}
|
|
270
|
+
if (HTTP_OPTION_NAMES.includes(property.name.text)) {
|
|
271
|
+
info.httpOptions.set(property.name.text, property.name.text);
|
|
272
|
+
}
|
|
273
|
+
if (property.name.text !== 'route' && !HTTP_OPTION_NAMES.includes(property.name.text)) {
|
|
274
|
+
warnUnreadableConfigValue(property, 'as a shorthand property');
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
232
278
|
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
233
279
|
return;
|
|
234
280
|
}
|
|
235
281
|
if (property.name.text === 'route') {
|
|
236
282
|
info.route = property.initializer.getText(sourceFile);
|
|
237
283
|
}
|
|
238
|
-
if (property.name.text === 'secure'
|
|
239
|
-
|
|
284
|
+
if (property.name.text === 'secure') {
|
|
285
|
+
if (property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
286
|
+
info.secure = true;
|
|
287
|
+
}
|
|
288
|
+
else if (property.initializer.kind !== ts.SyntaxKind.FalseKeyword) {
|
|
289
|
+
warnUnreadableConfigValue(property, 'with a value that is not a literal');
|
|
290
|
+
}
|
|
240
291
|
}
|
|
241
292
|
if (property.name.text === 'types' && ts.isObjectLiteralExpression(property.initializer)) {
|
|
242
293
|
property.initializer.properties.forEach((typeProperty) => {
|
|
@@ -271,7 +322,7 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
271
322
|
}
|
|
272
323
|
});
|
|
273
324
|
}
|
|
274
|
-
if (
|
|
325
|
+
if (HTTP_OPTION_NAMES.includes(property.name.text)) {
|
|
275
326
|
info.httpOptions.set(property.name.text, property.initializer.getText(sourceFile));
|
|
276
327
|
}
|
|
277
328
|
});
|
|
@@ -303,27 +354,42 @@ const transformLegacyQueryCreators = (content, legacyCreators) => {
|
|
|
303
354
|
}
|
|
304
355
|
creatorsByClient.get(creator.clientName).push(creator);
|
|
305
356
|
});
|
|
357
|
+
const wrappersByStatement = new Map();
|
|
306
358
|
legacyCreators.forEach((creator) => {
|
|
307
|
-
const nextCreator = generateNewQueryCreator(creator);
|
|
308
|
-
const legacyWrapper = generateLegacyWrapper(creator);
|
|
309
359
|
let variableStatement;
|
|
360
|
+
let declaration;
|
|
310
361
|
const visit = (node) => {
|
|
362
|
+
if (variableStatement)
|
|
363
|
+
return;
|
|
311
364
|
if (ts.isVariableStatement(node)) {
|
|
312
|
-
|
|
313
|
-
if (declaration
|
|
365
|
+
declaration = node.declarationList.declarations.find((candidate) => ts.isIdentifier(candidate.name) && candidate.name.text === creator.name);
|
|
366
|
+
if (declaration) {
|
|
314
367
|
variableStatement = node;
|
|
368
|
+
return;
|
|
315
369
|
}
|
|
316
370
|
}
|
|
317
371
|
ts.forEachChild(node, visit);
|
|
318
372
|
};
|
|
319
373
|
visit(sourceFile);
|
|
320
|
-
if (variableStatement)
|
|
374
|
+
if (!variableStatement || !declaration)
|
|
375
|
+
return;
|
|
376
|
+
const isTopLevel = ts.isSourceFile(variableStatement.parent);
|
|
377
|
+
const exportPrefix = isTopLevel ? 'export ' : '';
|
|
378
|
+
const nextCreator = `${creator.name} = ${generateNewQueryCreator(creator)}`;
|
|
379
|
+
const legacyWrapper = generateLegacyWrapper(creator, exportPrefix);
|
|
380
|
+
if (variableStatement.declarationList.declarations.length === 1) {
|
|
321
381
|
replacements.push({
|
|
322
382
|
start: variableStatement.getStart(sourceFile),
|
|
323
383
|
end: variableStatement.getEnd(),
|
|
324
|
-
replacement: `${nextCreator}
|
|
384
|
+
replacement: `${exportPrefix}const ${nextCreator};\n${legacyWrapper}`,
|
|
325
385
|
});
|
|
386
|
+
return;
|
|
326
387
|
}
|
|
388
|
+
replacements.push({ start: declaration.getStart(sourceFile), end: declaration.getEnd(), replacement: nextCreator });
|
|
389
|
+
wrappersByStatement.set(variableStatement, [...(wrappersByStatement.get(variableStatement) ?? []), legacyWrapper]);
|
|
390
|
+
});
|
|
391
|
+
wrappersByStatement.forEach((wrappers, statement) => {
|
|
392
|
+
replacements.push({ start: statement.getEnd(), end: statement.getEnd(), replacement: `\n${wrappers.join('\n')}` });
|
|
327
393
|
});
|
|
328
394
|
let result = content;
|
|
329
395
|
replacements.sort((left, right) => right.start - left.start);
|
|
@@ -334,7 +400,7 @@ const transformLegacyQueryCreators = (content, legacyCreators) => {
|
|
|
334
400
|
result = addCreatorImports(result, creatorsByClient);
|
|
335
401
|
// The rewritten creators no longer go through `def<T>()` or the v2 client object, so whatever is
|
|
336
402
|
// left over from those constructs would land as a lint error the moment `formatFiles` runs.
|
|
337
|
-
return pruneUnusedNamedImports(result
|
|
403
|
+
return pruneUnusedNamedImports(result);
|
|
338
404
|
};
|
|
339
405
|
const addCreatorImports = (content, creatorsByClient) => {
|
|
340
406
|
const sourceFile = createSourceFile(content);
|
|
@@ -412,7 +478,7 @@ const generateNewQueryCreator = (creator) => {
|
|
|
412
478
|
const typeParameter = getTypeParameter(creator);
|
|
413
479
|
const options = Array.from(creator.httpOptions.entries()).map(([key, value]) => `${key}: ${value}`);
|
|
414
480
|
const optionsBlock = options.length > 0 ? `, {\n ${options.join(',\n ')}\n}` : '';
|
|
415
|
-
return
|
|
481
|
+
return `${creatorFunction}${typeParameter}(${creator.route}${optionsBlock})`;
|
|
416
482
|
};
|
|
417
483
|
const getTypeParameter = (creator) => {
|
|
418
484
|
if (creator.typeArgs && (creator.typeBody || creator.typeResponse)) {
|
|
@@ -440,10 +506,10 @@ const getTypeParameter = (creator) => {
|
|
|
440
506
|
}
|
|
441
507
|
return '';
|
|
442
508
|
};
|
|
443
|
-
const generateLegacyWrapper = (creator) => {
|
|
509
|
+
const generateLegacyWrapper = (creator, exportPrefix) => {
|
|
444
510
|
// `name` is what lets a `prepare()` called outside an injection context name itself in the error
|
|
445
511
|
// instead of surfacing as a bare NG0203.
|
|
446
|
-
return `/**\n * ${legacyQueryDeprecationTag(creator.name)}\n */\
|
|
512
|
+
return `/**\n * ${legacyQueryDeprecationTag(creator.name)}\n */\n${exportPrefix}const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ name: '${toLegacyName(creator.name)}', creator: ${creator.name} });`;
|
|
447
513
|
};
|
|
448
514
|
const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, creatorToClient, }) => {
|
|
449
515
|
const createdProviders = [];
|
|
@@ -23,18 +23,15 @@ export const createMigrationScope = (tree, options) => {
|
|
|
23
23
|
if (roots.length === 0) {
|
|
24
24
|
return {
|
|
25
25
|
visit: (targetTree, callback) => visitNotIgnoredFiles(targetTree, '', callback),
|
|
26
|
-
includes: () => true,
|
|
27
26
|
describe: () => 'the whole workspace',
|
|
28
27
|
};
|
|
29
28
|
}
|
|
30
|
-
const includes = (filePath) => roots.some((root) => filePath === root || filePath.startsWith(`${root}/`));
|
|
31
29
|
return {
|
|
32
30
|
visit: (targetTree, callback) => {
|
|
33
31
|
for (const root of roots) {
|
|
34
32
|
visitNotIgnoredFiles(targetTree, root, callback);
|
|
35
33
|
}
|
|
36
34
|
},
|
|
37
|
-
includes,
|
|
38
35
|
describe: () => roots.join(', '),
|
|
39
36
|
};
|
|
40
37
|
};
|
|
@@ -115,17 +115,25 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
115
115
|
const sourceFile = createSourceFile(content, filePath);
|
|
116
116
|
const replacements = [];
|
|
117
117
|
const variableRenames = new Map();
|
|
118
|
+
let hasUnmigratedClient = false;
|
|
118
119
|
const visit = (node) => {
|
|
119
|
-
if (ts.isNewExpression(node) &&
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'V2QueryClient') {
|
|
121
|
+
const configArgument = node.arguments?.[0];
|
|
122
|
+
if (!configArgument || !ts.isObjectLiteralExpression(configArgument)) {
|
|
123
|
+
hasUnmigratedClient = true;
|
|
124
|
+
report.addManualReview({
|
|
125
|
+
title: 'Migrate a V2QueryClient whose config is not an object literal',
|
|
126
|
+
summary: `\`${node.getText(sourceFile)}\` passes its config indirectly, so the migration cannot read it and left the client on v2.`,
|
|
127
|
+
action: 'Inline the config object and re-run the migration, or rewrite the client by hand as `createQueryClient({ baseUrl, name })` with provider aliases and creators.',
|
|
128
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
129
|
+
source: 'query-client-migration',
|
|
130
|
+
dedupeKey: `unmigrated-client:${filePath}:${node.getStart(sourceFile)}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
126
134
|
const oldVariableName = getVariableNameForQueryClient(node);
|
|
127
|
-
const newVariableName = ensureConfigSuffix(oldVariableName);
|
|
128
|
-
if (oldVariableName !== newVariableName) {
|
|
135
|
+
const newVariableName = oldVariableName && ensureConfigSuffix(oldVariableName);
|
|
136
|
+
if (oldVariableName && newVariableName && oldVariableName !== newVariableName) {
|
|
129
137
|
const collisionRegex = new RegExp(`\\b(?:const|let|var)\\s+${newVariableName}\\b`);
|
|
130
138
|
if (collisionRegex.test(content)) {
|
|
131
139
|
report.addWarning({
|
|
@@ -158,16 +166,20 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
158
166
|
replacements.forEach(({ start, end, replacement }) => {
|
|
159
167
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
160
168
|
});
|
|
161
|
-
result = removeQueryClientImport(result);
|
|
169
|
+
result = removeQueryClientImport(result, hasUnmigratedClient);
|
|
162
170
|
return renameVariables(result, variableRenames);
|
|
163
171
|
};
|
|
164
|
-
const removeQueryClientImport = (content) => {
|
|
172
|
+
const removeQueryClientImport = (content, keepLegacyClient) => {
|
|
165
173
|
const sourceFile = createSourceFile(content);
|
|
166
174
|
let queryImportNode;
|
|
167
175
|
ts.forEachChild(sourceFile, (node) => {
|
|
168
|
-
if (
|
|
176
|
+
if (!queryImportNode &&
|
|
177
|
+
ts.isImportDeclaration(node) &&
|
|
169
178
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
170
|
-
node.moduleSpecifier.text === '@ethlete/query'
|
|
179
|
+
node.moduleSpecifier.text === '@ethlete/query' &&
|
|
180
|
+
node.importClause?.namedBindings &&
|
|
181
|
+
ts.isNamedImports(node.importClause.namedBindings) &&
|
|
182
|
+
node.importClause.namedBindings.elements.some((element) => element.name.text === 'V2QueryClient')) {
|
|
171
183
|
queryImportNode = node;
|
|
172
184
|
}
|
|
173
185
|
});
|
|
@@ -183,10 +195,10 @@ const removeQueryClientImport = (content) => {
|
|
|
183
195
|
return ensureImportFromQuery(content, requiredImports);
|
|
184
196
|
}
|
|
185
197
|
const existingElements = queryImportNode.importClause.namedBindings.elements.filter((element) => {
|
|
186
|
-
return element.name.text !== 'V2QueryClient';
|
|
198
|
+
return keepLegacyClient || element.name.text !== 'V2QueryClient';
|
|
187
199
|
});
|
|
188
200
|
const nextImports = new Set(requiredImports);
|
|
189
|
-
existingElements.forEach((element) => nextImports.add(element.
|
|
201
|
+
existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
|
|
190
202
|
const nextImportStatement = `import { ${Array.from(nextImports).sort().join(', ')} } from '@ethlete/query';`;
|
|
191
203
|
return (content.slice(0, queryImportNode.getStart(sourceFile)) +
|
|
192
204
|
nextImportStatement +
|
|
@@ -267,14 +279,37 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
|
|
|
267
279
|
dedupeKey: `dropped-client-option:${filePath}:${optionPath}`,
|
|
268
280
|
});
|
|
269
281
|
};
|
|
282
|
+
const raiseSpread = (spread) => {
|
|
283
|
+
const text = spread.getText(sourceFile);
|
|
284
|
+
report.addWarning({
|
|
285
|
+
title: `Reconfigure spread query client options "${text}"`,
|
|
286
|
+
summary: `The v2 client config spreads \`${text}\`. The migration cannot see which options it holds, so none of them were carried over to \`createQueryClient\`.`,
|
|
287
|
+
action: 'Inline the spread options that are still needed (`baseRoute` becomes `baseUrl`) and re-check the result.',
|
|
288
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(spread.getStart(sourceFile)).line + 1 }],
|
|
289
|
+
source: 'query-client-migration',
|
|
290
|
+
dedupeKey: `dropped-client-spread:${filePath}:${spread.getStart(sourceFile)}`,
|
|
291
|
+
});
|
|
292
|
+
};
|
|
270
293
|
configArgument.properties.forEach((property) => {
|
|
271
|
-
if (
|
|
294
|
+
if (ts.isSpreadAssignment(property)) {
|
|
295
|
+
raiseSpread(property);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) ||
|
|
299
|
+
!ts.isIdentifier(property.name)) {
|
|
272
300
|
return;
|
|
273
301
|
}
|
|
274
302
|
const name = property.name.text;
|
|
275
|
-
if ((name === 'request' || name === 'logging') &&
|
|
303
|
+
if ((name === 'request' || name === 'logging') &&
|
|
304
|
+
ts.isPropertyAssignment(property) &&
|
|
305
|
+
ts.isObjectLiteralExpression(property.initializer)) {
|
|
276
306
|
property.initializer.properties.forEach((nested) => {
|
|
277
|
-
if (
|
|
307
|
+
if (ts.isSpreadAssignment(nested)) {
|
|
308
|
+
raiseSpread(nested);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if ((!ts.isPropertyAssignment(nested) && !ts.isShorthandPropertyAssignment(nested)) ||
|
|
312
|
+
!ts.isIdentifier(nested.name)) {
|
|
278
313
|
return;
|
|
279
314
|
}
|
|
280
315
|
if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
|
|
@@ -289,40 +324,42 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
|
|
|
289
324
|
}
|
|
290
325
|
});
|
|
291
326
|
};
|
|
327
|
+
const getPropertyValueText = (property, sourceFile) => {
|
|
328
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
329
|
+
return property.name.text;
|
|
330
|
+
}
|
|
331
|
+
return ts.isPropertyAssignment(property) ? property.initializer.getText(sourceFile) : undefined;
|
|
332
|
+
};
|
|
333
|
+
const findProperty = (objectLiteral, name) => objectLiteral.properties.find((property) => {
|
|
334
|
+
return ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
|
335
|
+
ts.isIdentifier(property.name) &&
|
|
336
|
+
property.name.text === name);
|
|
337
|
+
});
|
|
292
338
|
const migrateConfigObject = (configArgument, node, sourceFile) => {
|
|
293
|
-
const variableName = getVariableNameForQueryClient(node);
|
|
339
|
+
const variableName = getVariableNameForQueryClient(node) ?? 'client';
|
|
294
340
|
const nextConfig = [];
|
|
295
|
-
const baseRouteProperty = configArgument
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const baseRouteValue = baseRouteProperty.initializer
|
|
300
|
-
.getText(sourceFile)
|
|
301
|
-
.replace('as `https://${string}`', '')
|
|
302
|
-
.trim();
|
|
303
|
-
nextConfig.push(`baseUrl: ${baseRouteValue}`);
|
|
341
|
+
const baseRouteProperty = findProperty(configArgument, 'baseRoute');
|
|
342
|
+
const baseRouteText = baseRouteProperty && getPropertyValueText(baseRouteProperty, sourceFile);
|
|
343
|
+
if (baseRouteText) {
|
|
344
|
+
nextConfig.push(`baseUrl: ${baseRouteText.replace('as `https://${string}`', '').trim()}`);
|
|
304
345
|
}
|
|
305
346
|
nextConfig.push(`name: '${variableName}'`);
|
|
306
|
-
const requestProperty = configArgument
|
|
307
|
-
return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'request';
|
|
308
|
-
});
|
|
347
|
+
const requestProperty = findProperty(configArgument, 'request');
|
|
309
348
|
if (requestProperty &&
|
|
310
349
|
ts.isPropertyAssignment(requestProperty) &&
|
|
311
350
|
ts.isObjectLiteralExpression(requestProperty.initializer)) {
|
|
312
|
-
requestProperty.initializer
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
351
|
+
const requestConfig = requestProperty.initializer;
|
|
352
|
+
for (const [from, to] of [
|
|
353
|
+
['queryParams', 'queryString'],
|
|
354
|
+
['cacheAdapter', 'cacheAdapter'],
|
|
355
|
+
['retryFn', 'retryFn'],
|
|
356
|
+
]) {
|
|
357
|
+
const property = findProperty(requestConfig, from);
|
|
358
|
+
const valueText = property && getPropertyValueText(property, sourceFile);
|
|
359
|
+
if (valueText) {
|
|
360
|
+
nextConfig.push(`${to}: ${valueText}`);
|
|
321
361
|
}
|
|
322
|
-
|
|
323
|
-
nextConfig.push(`retryFn: ${property.initializer.getText(sourceFile)}`);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
362
|
+
}
|
|
326
363
|
}
|
|
327
364
|
return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
|
|
328
365
|
};
|
|
@@ -334,7 +371,7 @@ const getVariableNameForQueryClient = (node) => {
|
|
|
334
371
|
}
|
|
335
372
|
parent = parent.parent;
|
|
336
373
|
}
|
|
337
|
-
return
|
|
374
|
+
return undefined;
|
|
338
375
|
};
|
|
339
376
|
const extractClientConfigNames = (content) => {
|
|
340
377
|
const sourceFile = createSourceFile(content);
|
|
@@ -433,6 +470,19 @@ const generateCreatorsForConfig = (configName) => {
|
|
|
433
470
|
`export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
|
|
434
471
|
].join('\n');
|
|
435
472
|
};
|
|
473
|
+
const declaresName = (declarations, name) => declarations.some((declaration) => declaration.name && ts.isIdentifier(declaration.name) && declaration.name.text === name);
|
|
474
|
+
const isShadowedByLocalDeclaration = (identifier) => {
|
|
475
|
+
for (let scope = identifier.parent; scope && !ts.isSourceFile(scope); scope = scope.parent) {
|
|
476
|
+
if (ts.isFunctionLike(scope) && declaresName(scope.parameters, identifier.text)) {
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
if (ts.isBlock(scope) &&
|
|
480
|
+
scope.statements.some((statement) => ts.isVariableStatement(statement) && declaresName(statement.declarationList.declarations, identifier.text))) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
};
|
|
436
486
|
const updateImportsInFile = (content, renames) => {
|
|
437
487
|
const sourceFile = createSourceFile(content);
|
|
438
488
|
const replacements = [];
|
|
@@ -443,7 +493,7 @@ const updateImportsInFile = (content, renames) => {
|
|
|
443
493
|
const nextElements = [];
|
|
444
494
|
let hasChanges = false;
|
|
445
495
|
node.importClause.namedBindings.elements.forEach((element) => {
|
|
446
|
-
const nextName = renames.get(element.name.text);
|
|
496
|
+
const nextName = renames.get(element.propertyName?.text ?? element.name.text);
|
|
447
497
|
if (!nextName) {
|
|
448
498
|
nextElements.push(element.getText(sourceFile));
|
|
449
499
|
return;
|
|
@@ -494,7 +544,7 @@ const updateImportsInFile = (content, renames) => {
|
|
|
494
544
|
ts.forEachChild(node, visit);
|
|
495
545
|
return;
|
|
496
546
|
}
|
|
497
|
-
if (ts.isPropertyAssignment(parent) && parent.name === node) {
|
|
547
|
+
if ((ts.isPropertyAssignment(parent) && parent.name === node) || isShadowedByLocalDeclaration(node)) {
|
|
498
548
|
ts.forEachChild(node, visit);
|
|
499
549
|
return;
|
|
500
550
|
}
|