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