@ethlete/query 6.0.0-next.52 → 6.0.0-next.54
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 +53 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +96 -24
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -1
- package/fesm2022/ethlete-query-testing.mjs +67 -25
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +7872 -7354
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/migrate-from-ngrx-toolkit/consumers.js +70 -2
- package/generators/migrate-from-ngrx-toolkit/migration.js +2 -1
- package/generators/migrate-to-query-v3/cleanup-migration.js +142 -53
- package/generators/migrate-to-query-v3/migration-scope.js +2 -0
- package/generators/migrate-to-query-v3/migration.js +3 -3
- package/generators/migrate-to-query-v3/module-graph.js +39 -29
- package/generators/migrate-to-query-v3/query-client-migration.js +138 -124
- package/generators/migrate-to-query-v3/rename-symbols.js +38 -11
- package/generators/migrate-to-query-v3/report.js +40 -7
- package/generators/migrate-to-query-v3/shared.js +19 -1
- package/generators/prep-for-query-v3/migration.js +317 -56
- package/package.json +1 -1
- package/types/ethlete-query-testing.d.ts +28 -11
- package/types/ethlete-query.d.ts +681 -570
|
@@ -8,6 +8,7 @@ export const CONSUMER_TASK = {
|
|
|
8
8
|
UNCONVERTED_ACTION_GROUP: 'NTK-UNCONVERTED-ACTION-GROUP',
|
|
9
9
|
SELECT_BY_ACTION_ID: 'NTK-SELECT-BY-ACTION-ID',
|
|
10
10
|
AUTH_INTERCEPTOR: 'NTK-AUTH-INTERCEPTOR',
|
|
11
|
+
FACADE_ON: 'NTK-FACADE-ON',
|
|
11
12
|
};
|
|
12
13
|
const defineConsumerTasks = (report) => {
|
|
13
14
|
report.define(CONSUMER_TASK.UNSUPPORTED_IMPORT, {
|
|
@@ -16,12 +17,16 @@ const defineConsumerTasks = (report) => {
|
|
|
16
17
|
});
|
|
17
18
|
report.define(CONSUMER_TASK.UNCONVERTED_ACTION_GROUP, {
|
|
18
19
|
title: 'Consumer types name an action group that is still on the toolkit',
|
|
19
|
-
action: 'The file was left on `@tomtomb/ngrx-toolkit` because a `MappedEntityState` / `ActionCallArgs` type names an action group whose feature was not converted. Resolve that feature’s task, then re-run the generator.',
|
|
20
|
+
action: 'The file was left on `@tomtomb/ngrx-toolkit` because a `MappedEntityState` / `ActionCallArgs` type names an action group whose feature was not converted. Until then the file does not typecheck where it takes handles from facades that were migrated: the interop `MappedEntityState` has no `isPolling$`, `type$` or `entityId$`, and other generics. Resolve that feature’s task, then re-run the generator.',
|
|
20
21
|
});
|
|
21
22
|
report.define(CONSUMER_TASK.SELECT_BY_ACTION_ID, {
|
|
22
23
|
title: 'Handle looked up by action id',
|
|
23
24
|
action: 'The interop has no action ids. Pass the args instead and look the handle up with `toolkitSelect(creator, args, { injector })` - equal args return the handle `toolkitCall` created.',
|
|
24
25
|
});
|
|
26
|
+
report.define(CONSUMER_TASK.FACADE_ON, {
|
|
27
|
+
title: 'Facade `on()` / `once()` call on a migrated facade',
|
|
28
|
+
action: 'The migrated facade has no `on()` / `once()`, and the v3 creators have no `.success` / `.failure` actions, so the call does not typecheck. Rewrite it by hand: react to the `response$` / `error$` of the handle the facade method returns, or subscribe to the creator’s result.',
|
|
29
|
+
});
|
|
25
30
|
report.define(CONSUMER_TASK.AUTH_INTERCEPTOR, {
|
|
26
31
|
title: 'HTTP interceptor attaches the bearer token',
|
|
27
32
|
action: 'v3 secure creators already send the token, so this interceptor sends it twice, and it adds it to every other `HttpClient` call too - third-party hosts included. Delete it and its registration. Requests it skipped must use public creators: pass their prefixes as `--publicRoutes` and re-run.',
|
|
@@ -416,8 +421,70 @@ const findInterceptors = (tree, inScope, filePath, content) => {
|
|
|
416
421
|
visit(sourceFile);
|
|
417
422
|
return tasks;
|
|
418
423
|
};
|
|
424
|
+
const migratedFacadeNames = (graph, migratedFacades, filePath, sourceFile) => {
|
|
425
|
+
const names = new Set();
|
|
426
|
+
for (const statement of sourceFile.statements) {
|
|
427
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
428
|
+
continue;
|
|
429
|
+
const bindings = statement.importClause?.namedBindings;
|
|
430
|
+
if (!bindings || !ts.isNamedImports(bindings))
|
|
431
|
+
continue;
|
|
432
|
+
for (const element of bindings.elements) {
|
|
433
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
434
|
+
const declaringFile = graph.findDeclaringFile(filePath, statement.moduleSpecifier.text, imported);
|
|
435
|
+
if (declaringFile && migratedFacades.has(declaringFile))
|
|
436
|
+
names.add(element.name.text);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return names;
|
|
440
|
+
};
|
|
441
|
+
const findFacadeEventCalls = (graph, migratedFacades, filePath, content) => {
|
|
442
|
+
if (!/\.(on|once)\s*\(/.test(content))
|
|
443
|
+
return [];
|
|
444
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
445
|
+
const facadeNames = migratedFacadeNames(graph, migratedFacades, filePath, sourceFile);
|
|
446
|
+
if (facadeNames.size === 0)
|
|
447
|
+
return [];
|
|
448
|
+
const isFacadeType = (type) => !!type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && facadeNames.has(type.typeName.text);
|
|
449
|
+
const isFacadeInject = (node) => !!node && isCall(node, 'inject') && !!node.arguments[0] && facadeNames.has(node.arguments[0].getText(sourceFile));
|
|
450
|
+
const bound = new Set();
|
|
451
|
+
const collect = (node) => {
|
|
452
|
+
if ((ts.isPropertyDeclaration(node) || ts.isParameter(node) || ts.isVariableDeclaration(node)) &&
|
|
453
|
+
ts.isIdentifier(node.name) &&
|
|
454
|
+
(isFacadeType(node.type) || isFacadeInject(node.initializer))) {
|
|
455
|
+
bound.add(node.name.text);
|
|
456
|
+
}
|
|
457
|
+
ts.forEachChild(node, collect);
|
|
458
|
+
};
|
|
459
|
+
collect(sourceFile);
|
|
460
|
+
const isFacade = (receiver) => {
|
|
461
|
+
if (isFacadeInject(receiver))
|
|
462
|
+
return true;
|
|
463
|
+
if (ts.isIdentifier(receiver))
|
|
464
|
+
return bound.has(receiver.text);
|
|
465
|
+
return (ts.isPropertyAccessExpression(receiver) &&
|
|
466
|
+
receiver.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
467
|
+
bound.has(receiver.name.text));
|
|
468
|
+
};
|
|
469
|
+
const tasks = [];
|
|
470
|
+
const visit = (node) => {
|
|
471
|
+
if (ts.isCallExpression(node) &&
|
|
472
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
473
|
+
['on', 'once'].includes(node.expression.name.text) &&
|
|
474
|
+
isFacade(node.expression.expression)) {
|
|
475
|
+
tasks.push({
|
|
476
|
+
id: CONSUMER_TASK.FACADE_ON,
|
|
477
|
+
summary: `${code(node.getText(sourceFile).replace(/\s+/g, ' '))} listens to toolkit actions the migrated facade no longer emits.`,
|
|
478
|
+
locations: [{ filePath, line: lineOf(sourceFile, node.getStart(sourceFile)) }],
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
ts.forEachChild(node, visit);
|
|
482
|
+
};
|
|
483
|
+
visit(sourceFile);
|
|
484
|
+
return tasks;
|
|
485
|
+
};
|
|
419
486
|
/** Runs the consumer side of the migration on every scoped file that still imports `@tomtomb/ngrx-toolkit`. */
|
|
420
|
-
export const migrateToolkitConsumers = (tree, graph, inScope, report) => {
|
|
487
|
+
export const migrateToolkitConsumers = (tree, graph, inScope, report, migratedFacades) => {
|
|
421
488
|
defineConsumerTasks(report);
|
|
422
489
|
const stats = {
|
|
423
490
|
filesRewritten: 0,
|
|
@@ -434,6 +501,7 @@ export const migrateToolkitConsumers = (tree, graph, inScope, report) => {
|
|
|
434
501
|
const interceptors = findInterceptors(tree, inScope, filePath, content);
|
|
435
502
|
interceptors.forEach((task) => report.add(task));
|
|
436
503
|
stats.interceptorsFound += interceptors.length;
|
|
504
|
+
findFacadeEventCalls(graph, migratedFacades, filePath, content).forEach((task) => report.add(task));
|
|
437
505
|
if (!content.includes(TOOLKIT_MODULE))
|
|
438
506
|
continue;
|
|
439
507
|
const result = rewriteConsumer(tree, graph, filePath, content);
|
|
@@ -112,7 +112,8 @@ export const migrateToolkitStores = (tree, options, report) => {
|
|
|
112
112
|
ready.push(feature);
|
|
113
113
|
}
|
|
114
114
|
const removal = removeFeatureStores(tree, graph, inScope, ready, report);
|
|
115
|
-
const
|
|
115
|
+
const migratedFacades = new Set(ready.map((feature) => feature.files.facade).filter((file) => !!file));
|
|
116
|
+
const consumers = migrateToolkitConsumers(tree, createModuleGraph(tree), inScope, report, migratedFacades);
|
|
116
117
|
return {
|
|
117
118
|
featuresFound: discovered.features.length,
|
|
118
119
|
featuresConverted: ready.length,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import {
|
|
2
|
+
import { createModuleGraph } from './module-graph.js';
|
|
3
|
+
import { isShadowedInNestedScope } from './rename-symbols.js';
|
|
4
|
+
import { REMOVED_EXPERIMENTAL_QUERY_HELPERS, createSourceFile, ensureImportFromQuery, ensureNamedImports, findRemovedExperimentalQueryHelpers, getLineNumber, printImportDeclaration, } from './shared.js';
|
|
3
5
|
export const reportRemovedExperimentalQueryHelpers = (tree, scope, report) => {
|
|
4
6
|
scope.visit(tree, (filePath) => {
|
|
5
7
|
if (!filePath.endsWith('.ts')) {
|
|
@@ -90,8 +92,9 @@ export const replaceAnyQueryWithLegacy = (tree, scope) => {
|
|
|
90
92
|
console.log(`\n✅ Replaced legacy AnyV2Query aliases in ${updatedFiles.length} files`);
|
|
91
93
|
}
|
|
92
94
|
};
|
|
93
|
-
export const migrateEmptyPrepareCalls = (tree, scope) => {
|
|
95
|
+
export const migrateEmptyPrepareCalls = (tree, scope, report) => {
|
|
94
96
|
const updatedFiles = [];
|
|
97
|
+
const graph = createModuleGraph(tree);
|
|
95
98
|
scope.visit(tree, (filePath) => {
|
|
96
99
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
97
100
|
return;
|
|
@@ -100,7 +103,17 @@ export const migrateEmptyPrepareCalls = (tree, scope) => {
|
|
|
100
103
|
if (!content || !content.includes('.prepare()')) {
|
|
101
104
|
return;
|
|
102
105
|
}
|
|
103
|
-
const nextContent = transformEmptyPrepareCalls(content);
|
|
106
|
+
const { content: nextContent, unresolved } = transformEmptyPrepareCalls(tree, graph, filePath, content);
|
|
107
|
+
unresolved.forEach(({ line, receiver }) => {
|
|
108
|
+
report.addManualReview({
|
|
109
|
+
title: `Check the empty prepare() call on ${receiver}`,
|
|
110
|
+
summary: `The migration could not tell whether \`${receiver}\` is a legacy query creator, so it left \`${receiver}.prepare()\` as is. A legacy creator needs \`.prepare({})\` in v3.`,
|
|
111
|
+
action: `Change the call to \`${receiver}.prepare({})\` if \`${receiver}\` is a legacy query creator, otherwise leave it.`,
|
|
112
|
+
locations: [{ filePath, line }],
|
|
113
|
+
source: 'cleanup-migration',
|
|
114
|
+
dedupeKey: `unresolved-empty-prepare:${filePath}:${line}`,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
104
117
|
if (nextContent !== content) {
|
|
105
118
|
tree.write(filePath, nextContent);
|
|
106
119
|
updatedFiles.push(filePath);
|
|
@@ -111,31 +124,36 @@ export const migrateEmptyPrepareCalls = (tree, scope) => {
|
|
|
111
124
|
}
|
|
112
125
|
};
|
|
113
126
|
const migrateDevtoolsInFile = (content) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
127
|
+
const componentNames = localNamesImportedFromQuery(content, 'QueryDevtoolsComponent');
|
|
128
|
+
const providerNames = new Set([
|
|
129
|
+
'provideQueryClientForDevtools',
|
|
130
|
+
...localNamesImportedFromQuery(content, 'provideQueryClientForDevtools'),
|
|
131
|
+
]);
|
|
132
|
+
let result = replaceDevtoolsProviderCalls(content, providerNames);
|
|
116
133
|
const hasProvider = result.includes('provideQueryDevtools()');
|
|
117
134
|
result = dropLegacyDevtoolsImports(result);
|
|
118
135
|
if (hasProvider) {
|
|
119
136
|
result = ensureImportFromQuery(result, ['provideQueryDevtools']);
|
|
120
137
|
}
|
|
121
|
-
if (
|
|
138
|
+
if (componentNames.length > 0) {
|
|
122
139
|
result = ensureNamedImports({
|
|
123
140
|
content: result,
|
|
124
|
-
importsNeeded:
|
|
141
|
+
importsNeeded: componentNames.map((name) => name === 'QueryDevtoolsComponent' ? name : `QueryDevtoolsComponent as ${name}`),
|
|
125
142
|
moduleSpecifier: '@ethlete/query-devtools',
|
|
126
143
|
});
|
|
127
144
|
}
|
|
128
|
-
return { content: result, importsComponent };
|
|
129
|
-
};
|
|
130
|
-
const importsFromQuery = (content, name) => {
|
|
131
|
-
const sourceFile = createSourceFile(content);
|
|
132
|
-
return sourceFile.statements.some((node) => ts.isImportDeclaration(node) &&
|
|
133
|
-
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
134
|
-
node.moduleSpecifier.text === '@ethlete/query' &&
|
|
135
|
-
!!node.importClause?.namedBindings &&
|
|
136
|
-
ts.isNamedImports(node.importClause.namedBindings) &&
|
|
137
|
-
node.importClause.namedBindings.elements.some((element) => element.name.text === name));
|
|
145
|
+
return { content: result, importsComponent: componentNames.length > 0 };
|
|
138
146
|
};
|
|
147
|
+
const queryImports = (sourceFile) => sourceFile.statements.filter((node) => ts.isImportDeclaration(node) &&
|
|
148
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
149
|
+
node.moduleSpecifier.text === '@ethlete/query');
|
|
150
|
+
const namedImportElements = (node) => node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
|
|
151
|
+
? node.importClause.namedBindings.elements
|
|
152
|
+
: [];
|
|
153
|
+
const importedName = (element) => (element.propertyName ?? element.name).text;
|
|
154
|
+
const localNamesImportedFromQuery = (content, name) => queryImports(createSourceFile(content)).flatMap((node) => namedImportElements(node)
|
|
155
|
+
.filter((element) => importedName(element) === name)
|
|
156
|
+
.map((element) => element.name.text));
|
|
139
157
|
/**
|
|
140
158
|
* Turns every `provideQueryClientForDevtools({ client, displayName })` into a single
|
|
141
159
|
* `provideQueryDevtools()`.
|
|
@@ -144,13 +162,11 @@ const importsFromQuery = (content, name) => {
|
|
|
144
162
|
* first call site keeps its position - it is already where the app wanted its devtools - and the
|
|
145
163
|
* rest are removed along with the comma that separated them.
|
|
146
164
|
*/
|
|
147
|
-
const replaceDevtoolsProviderCalls = (content) => {
|
|
165
|
+
const replaceDevtoolsProviderCalls = (content, providerNames) => {
|
|
148
166
|
const sourceFile = createSourceFile(content);
|
|
149
167
|
const calls = [];
|
|
150
168
|
const visit = (node) => {
|
|
151
|
-
if (ts.isCallExpression(node) &&
|
|
152
|
-
ts.isIdentifier(node.expression) &&
|
|
153
|
-
node.expression.text === 'provideQueryClientForDevtools') {
|
|
169
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && providerNames.has(node.expression.text)) {
|
|
154
170
|
calls.push(node);
|
|
155
171
|
}
|
|
156
172
|
ts.forEachChild(node, visit);
|
|
@@ -190,30 +206,27 @@ const withSurroundingComma = (content, start, end) => {
|
|
|
190
206
|
}
|
|
191
207
|
return { start, end };
|
|
192
208
|
};
|
|
193
|
-
|
|
209
|
+
const LEGACY_DEVTOOLS_IMPORTS = new Set(['QueryDevtoolsComponent', 'provideQueryClientForDevtools']);
|
|
210
|
+
/** Drops the v2 devtools names from the `@ethlete/query` imports; both moved or were renamed. */
|
|
194
211
|
const dropLegacyDevtoolsImports = (content) => {
|
|
195
212
|
const sourceFile = createSourceFile(content);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
213
|
+
const replacements = [];
|
|
214
|
+
for (const node of queryImports(sourceFile)) {
|
|
215
|
+
const elements = namedImportElements(node);
|
|
216
|
+
const nextElements = elements.filter((element) => !LEGACY_DEVTOOLS_IMPORTS.has(importedName(element)));
|
|
217
|
+
if (nextElements.length === elements.length) {
|
|
218
|
+
continue;
|
|
202
219
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
const nextImport = printImportDeclaration(node, sourceFile, nextElements.map((element) => element.getText(sourceFile)));
|
|
221
|
+
const end = !nextImport && content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
|
|
222
|
+
replacements.push({ start: node.getStart(sourceFile), end, replacement: nextImport });
|
|
206
223
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return content.slice(0, importNode.getStart(sourceFile)) + content.slice(nextLineEnd);
|
|
214
|
-
}
|
|
215
|
-
const nextImport = `import { ${nextElements.map((element) => element.getText(sourceFile)).join(', ')} } from '@ethlete/query';`;
|
|
216
|
-
return content.slice(0, importNode.getStart(sourceFile)) + nextImport + content.slice(importNode.getEnd());
|
|
224
|
+
let result = content;
|
|
225
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
226
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
227
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
228
|
+
});
|
|
229
|
+
return result;
|
|
217
230
|
};
|
|
218
231
|
const replaceAnyQueryInFile = (content) => {
|
|
219
232
|
const sourceFile = createSourceFile(content);
|
|
@@ -245,20 +258,96 @@ const replaceAnyQueryInFile = (content) => {
|
|
|
245
258
|
});
|
|
246
259
|
return result;
|
|
247
260
|
};
|
|
248
|
-
const
|
|
249
|
-
const
|
|
261
|
+
const findTopLevelVariable = (sourceFile, name) => {
|
|
262
|
+
for (const statement of sourceFile.statements) {
|
|
263
|
+
if (!ts.isVariableStatement(statement))
|
|
264
|
+
continue;
|
|
265
|
+
const declaration = statement.declarationList.declarations.find((candidate) => ts.isIdentifier(candidate.name) && candidate.name.text === name);
|
|
266
|
+
if (declaration)
|
|
267
|
+
return declaration;
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
};
|
|
271
|
+
const findNamedImport = (sourceFile, localName) => {
|
|
272
|
+
for (const statement of sourceFile.statements) {
|
|
273
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
274
|
+
continue;
|
|
275
|
+
const element = namedImportElements(statement).find((candidate) => candidate.name.text === localName);
|
|
276
|
+
if (element)
|
|
277
|
+
return { specifier: statement.moduleSpecifier.text, importedName: importedName(element) };
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
};
|
|
281
|
+
const QUERY_CLIENT_CONSTRUCTORS = new Set(['V2QueryClient', 'QueryClient']);
|
|
282
|
+
/**
|
|
283
|
+
* Classifies what a top-level binding of `sourceFile` holds: a query creator (a call to an
|
|
284
|
+
* `@ethlete/query` export such as `createLegacyQueryCreator`, or a method call on a v2 client), something
|
|
285
|
+
* provably unrelated, or `unknown` when the binding cannot be followed.
|
|
286
|
+
*/
|
|
287
|
+
const classifyBinding = (tree, graph, filePath, sourceFile, name, isClient, depth) => {
|
|
288
|
+
if (depth > 4)
|
|
289
|
+
return 'unknown';
|
|
290
|
+
const declaration = findTopLevelVariable(sourceFile, name);
|
|
291
|
+
if (declaration) {
|
|
292
|
+
const initializer = declaration.initializer;
|
|
293
|
+
if (isClient) {
|
|
294
|
+
return initializer &&
|
|
295
|
+
ts.isNewExpression(initializer) &&
|
|
296
|
+
ts.isIdentifier(initializer.expression) &&
|
|
297
|
+
QUERY_CLIENT_CONSTRUCTORS.has(initializer.expression.text) &&
|
|
298
|
+
findNamedImport(sourceFile, initializer.expression.text)?.specifier === '@ethlete/query'
|
|
299
|
+
? 'query'
|
|
300
|
+
: 'foreign';
|
|
301
|
+
}
|
|
302
|
+
if (!initializer || !ts.isCallExpression(initializer))
|
|
303
|
+
return 'foreign';
|
|
304
|
+
const callee = initializer.expression;
|
|
305
|
+
if (ts.isIdentifier(callee)) {
|
|
306
|
+
return findNamedImport(sourceFile, callee.text)?.specifier === '@ethlete/query' ? 'query' : 'foreign';
|
|
307
|
+
}
|
|
308
|
+
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) {
|
|
309
|
+
return classifyBinding(tree, graph, filePath, sourceFile, callee.expression.text, true, depth + 1);
|
|
310
|
+
}
|
|
311
|
+
return 'foreign';
|
|
312
|
+
}
|
|
313
|
+
const binding = findNamedImport(sourceFile, name);
|
|
314
|
+
if (!binding || binding.specifier === '@ethlete/query')
|
|
315
|
+
return 'unknown';
|
|
316
|
+
const declaringFile = graph.findDeclaringFile(filePath, binding.specifier, binding.importedName);
|
|
317
|
+
if (!declaringFile)
|
|
318
|
+
return graph.isWorkspaceSpecifier(binding.specifier) ? 'unknown' : 'foreign';
|
|
319
|
+
const declaringContent = tree.read(declaringFile, 'utf-8');
|
|
320
|
+
if (!declaringContent)
|
|
321
|
+
return 'unknown';
|
|
322
|
+
return classifyBinding(tree, graph, declaringFile, createSourceFile(declaringContent, declaringFile), binding.importedName, isClient, depth + 1);
|
|
323
|
+
};
|
|
324
|
+
const classifyPrepareReceiver = (tree, graph, filePath, sourceFile, receiver) => {
|
|
325
|
+
if (!ts.isIdentifier(receiver) || isShadowedInNestedScope(receiver))
|
|
326
|
+
return 'unknown';
|
|
327
|
+
return classifyBinding(tree, graph, filePath, sourceFile, receiver.text, false, 0);
|
|
328
|
+
};
|
|
329
|
+
const transformEmptyPrepareCalls = (tree, graph, filePath, content) => {
|
|
330
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
250
331
|
const replacements = [];
|
|
332
|
+
const unresolved = [];
|
|
251
333
|
const visit = (node) => {
|
|
252
|
-
if (ts.isCallExpression(node) &&
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
334
|
+
if (ts.isCallExpression(node) &&
|
|
335
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
336
|
+
ts.isIdentifier(node.expression.name) &&
|
|
337
|
+
node.expression.name.text === 'prepare' &&
|
|
338
|
+
node.arguments.length === 0) {
|
|
339
|
+
const kind = classifyPrepareReceiver(tree, graph, filePath, sourceFile, node.expression.expression);
|
|
340
|
+
if (kind === 'query') {
|
|
258
341
|
replacements.push({
|
|
259
|
-
start: node.
|
|
260
|
-
end: node.
|
|
261
|
-
replacement:
|
|
342
|
+
start: node.arguments.pos,
|
|
343
|
+
end: node.arguments.end,
|
|
344
|
+
replacement: '{}',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
else if (kind === 'unknown') {
|
|
348
|
+
unresolved.push({
|
|
349
|
+
line: getLineNumber(node, sourceFile),
|
|
350
|
+
receiver: node.expression.expression.getText(sourceFile),
|
|
262
351
|
});
|
|
263
352
|
}
|
|
264
353
|
}
|
|
@@ -270,6 +359,6 @@ const transformEmptyPrepareCalls = (content) => {
|
|
|
270
359
|
replacements.forEach(({ start, end, replacement }) => {
|
|
271
360
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
272
361
|
});
|
|
273
|
-
return result;
|
|
362
|
+
return { content: result, unresolved };
|
|
274
363
|
};
|
|
275
364
|
//# sourceMappingURL=cleanup-migration.js.map
|
|
@@ -24,6 +24,7 @@ export const createMigrationScope = (tree, options) => {
|
|
|
24
24
|
return {
|
|
25
25
|
visit: (targetTree, callback) => visitNotIgnoredFiles(targetTree, '', callback),
|
|
26
26
|
describe: () => 'the whole workspace',
|
|
27
|
+
isScoped: false,
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
return {
|
|
@@ -33,6 +34,7 @@ export const createMigrationScope = (tree, options) => {
|
|
|
33
34
|
}
|
|
34
35
|
},
|
|
35
36
|
describe: () => roots.join(', '),
|
|
37
|
+
isScoped: true,
|
|
36
38
|
};
|
|
37
39
|
};
|
|
38
40
|
//# sourceMappingURL=migration-scope.js.map
|
|
@@ -19,18 +19,18 @@ export default async function migrate(tree, schema) {
|
|
|
19
19
|
updateLegacyCreatorImportsAndUsages(tree, scope, report);
|
|
20
20
|
}
|
|
21
21
|
if (variableRenames.size > 0) {
|
|
22
|
-
updateImportsAcrossWorkspace(tree, variableRenames, scope);
|
|
22
|
+
updateImportsAcrossWorkspace(tree, variableRenames, scope, report);
|
|
23
23
|
}
|
|
24
24
|
replaceAnyQueryWithLegacy(tree, scope);
|
|
25
25
|
migrateDevtoolsUsage(tree, scope, report);
|
|
26
|
-
migrateEmptyPrepareCalls(tree, scope);
|
|
26
|
+
migrateEmptyPrepareCalls(tree, scope, report);
|
|
27
27
|
migrateLegacyPrepareCalls(tree, report, scope);
|
|
28
28
|
reportRemovedExperimentalQueryHelpers(tree, scope, report);
|
|
29
29
|
if (queryClientFiles.size > 0) {
|
|
30
30
|
reportMissingHttpClientProviders(tree, report, scope);
|
|
31
31
|
reportDefaultHeaderUsages(tree, report, scope);
|
|
32
32
|
}
|
|
33
|
-
report.writeToTree(tree);
|
|
33
|
+
report.writeToTree(tree, scope.isScoped ? scope.describe() : undefined);
|
|
34
34
|
report.printSummary();
|
|
35
35
|
if (!schema.skipFormat) {
|
|
36
36
|
await formatFiles(tree);
|
|
@@ -15,8 +15,9 @@ const normalizePath = (path) => {
|
|
|
15
15
|
return segments.join('/');
|
|
16
16
|
};
|
|
17
17
|
const dirName = (path) => path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
18
|
-
const
|
|
18
|
+
const readTsConfig = (tree) => {
|
|
19
19
|
const paths = new Map();
|
|
20
|
+
let baseUrl = null;
|
|
20
21
|
for (const configPath of ['tsconfig.base.json', 'tsconfig.json']) {
|
|
21
22
|
const raw = tree.read(configPath, 'utf-8');
|
|
22
23
|
if (!raw)
|
|
@@ -25,6 +26,9 @@ const readTsConfigPaths = (tree) => {
|
|
|
25
26
|
// tsconfig files are JSONC in practice; the TS parser is the only thing that reliably reads
|
|
26
27
|
// them, comments and trailing commas included.
|
|
27
28
|
const parsed = ts.parseConfigFileTextToJson(configPath, raw).config;
|
|
29
|
+
const configBaseUrl = parsed?.compilerOptions?.baseUrl;
|
|
30
|
+
if (baseUrl === null && typeof configBaseUrl === 'string')
|
|
31
|
+
baseUrl = normalizePath(configBaseUrl);
|
|
28
32
|
Object.entries(parsed?.compilerOptions?.paths ?? {}).forEach(([key, targets]) => {
|
|
29
33
|
if (!paths.has(key))
|
|
30
34
|
paths.set(key, targets);
|
|
@@ -34,45 +38,49 @@ const readTsConfigPaths = (tree) => {
|
|
|
34
38
|
// A tsconfig we cannot parse just means fewer resolvable aliases, not a failed migration.
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
|
-
return paths;
|
|
41
|
+
return { paths, baseUrl };
|
|
38
42
|
};
|
|
43
|
+
/** The part of `specifier` the `*` of `pattern` stands for, `''` for an exact match, or `null`. */
|
|
44
|
+
const matchPathAlias = (pattern, specifier) => {
|
|
45
|
+
const wildcardIndex = pattern.indexOf('*');
|
|
46
|
+
if (wildcardIndex === -1)
|
|
47
|
+
return pattern === specifier ? '' : null;
|
|
48
|
+
const prefix = pattern.slice(0, wildcardIndex);
|
|
49
|
+
const patternSuffix = pattern.slice(wildcardIndex + 1);
|
|
50
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(patternSuffix))
|
|
51
|
+
return null;
|
|
52
|
+
return specifier.slice(prefix.length, specifier.length - patternSuffix.length);
|
|
53
|
+
};
|
|
54
|
+
const JS_EXTENSION = /\.(m?)js$/;
|
|
39
55
|
/** The files a specifier could point at, most specific first. */
|
|
40
|
-
const candidateFiles = (base) =>
|
|
41
|
-
base.endsWith('.ts')
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
const candidateFiles = (base) => {
|
|
57
|
+
if (base.endsWith('.ts'))
|
|
58
|
+
return [base];
|
|
59
|
+
const jsExtension = JS_EXTENSION.exec(base);
|
|
60
|
+
if (jsExtension) {
|
|
61
|
+
const stem = base.slice(0, -jsExtension[0].length);
|
|
62
|
+
return [`${stem}.${jsExtension[1]}ts`, `${stem}.ts`, `${stem}.d.ts`, `${base}/index.ts`];
|
|
63
|
+
}
|
|
64
|
+
return [`${base}.ts`, `${base}/index.ts`, `${base}.d.ts`];
|
|
65
|
+
};
|
|
45
66
|
export const createModuleGraph = (tree) => {
|
|
46
|
-
const tsConfigPaths =
|
|
67
|
+
const { paths: tsConfigPaths, baseUrl } = readTsConfig(tree);
|
|
68
|
+
const findCandidate = (base) => candidateFiles(base).find((candidate) => tree.exists(candidate)) ?? null;
|
|
47
69
|
const declaringFileCache = new Map();
|
|
48
70
|
const resolveEntryFile = (fromFile, specifier) => {
|
|
49
|
-
if (specifier.startsWith('.'))
|
|
50
|
-
|
|
51
|
-
return candidateFiles(base).find((candidate) => tree.exists(candidate)) ?? null;
|
|
52
|
-
}
|
|
71
|
+
if (specifier.startsWith('.'))
|
|
72
|
+
return findCandidate(normalizePath(`${dirName(fromFile)}/${specifier}`));
|
|
53
73
|
for (const [pattern, targets] of tsConfigPaths.entries()) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (pattern !== specifier)
|
|
58
|
-
continue;
|
|
59
|
-
suffix = '';
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
const prefix = pattern.slice(0, wildcardIndex);
|
|
63
|
-
const patternSuffix = pattern.slice(wildcardIndex + 1);
|
|
64
|
-
if (!specifier.startsWith(prefix) || !specifier.endsWith(patternSuffix))
|
|
65
|
-
continue;
|
|
66
|
-
suffix = specifier.slice(prefix.length, specifier.length - patternSuffix.length);
|
|
67
|
-
}
|
|
74
|
+
const wildcard = matchPathAlias(pattern, specifier);
|
|
75
|
+
if (wildcard === null)
|
|
76
|
+
continue;
|
|
68
77
|
for (const target of targets) {
|
|
69
|
-
const
|
|
70
|
-
const candidate = candidateFiles(resolvedTarget).find((option) => tree.exists(option));
|
|
78
|
+
const candidate = findCandidate(normalizePath(`${baseUrl ?? ''}/${target.replace('*', wildcard)}`));
|
|
71
79
|
if (candidate)
|
|
72
80
|
return candidate;
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
|
-
return null;
|
|
83
|
+
return baseUrl === null ? null : findCandidate(normalizePath(`${baseUrl}/${specifier}`));
|
|
76
84
|
};
|
|
77
85
|
const declaresSymbol = (sourceFile, symbolName) => {
|
|
78
86
|
let declares = false;
|
|
@@ -140,6 +148,8 @@ export const createModuleGraph = (tree) => {
|
|
|
140
148
|
return {
|
|
141
149
|
resolveFile: resolveEntryFile,
|
|
142
150
|
pathAliases: () => [...tsConfigPaths.keys()],
|
|
151
|
+
isWorkspaceSpecifier: (specifier) => specifier.startsWith('.') ||
|
|
152
|
+
[...tsConfigPaths.keys()].some((pattern) => matchPathAlias(pattern, specifier) !== null),
|
|
143
153
|
findDeclaringFile: (fromFile, specifier, symbolName) => {
|
|
144
154
|
const cacheKey = `${fromFile}|${specifier}|${symbolName}`;
|
|
145
155
|
const cached = declaringFileCache.get(cacheKey);
|