@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
|
@@ -18,6 +18,12 @@ export type ToolkitSide = OptionalNullAsUndefined<{
|
|
|
18
18
|
export type CreatorSide = OptionalNullAsUndefined<{
|
|
19
19
|
[K in keyof CreatorArgs as K extends 'queryParams' | 'body' ? K : never]: CreatorArgs[K];
|
|
20
20
|
}>;
|
|
21
|
+
export type ToolkitRaw = {
|
|
22
|
+
[K in keyof ToolkitArgs as K extends 'params' ? 'queryParams' : K extends 'body' ? K : never]: ToolkitArgs[K];
|
|
23
|
+
};
|
|
24
|
+
export type CreatorRaw = {
|
|
25
|
+
[K in keyof CreatorArgs as K extends 'queryParams' | 'body' ? K : never]: CreatorArgs[K];
|
|
26
|
+
};
|
|
21
27
|
`;
|
|
22
28
|
const COMPILER_OPTIONS = {
|
|
23
29
|
strict: true,
|
|
@@ -59,14 +65,46 @@ const hasEveryKey = (checker, source, target, seen, depth) => {
|
|
|
59
65
|
return true;
|
|
60
66
|
seen.add(from);
|
|
61
67
|
return checker.getPropertiesOfType(from).every((property) => {
|
|
62
|
-
const counterpart = checker.
|
|
68
|
+
const counterpart = checker.getPropertiesOfType(to).find((each) => each.escapedName === property.escapedName);
|
|
63
69
|
return (!!counterpart &&
|
|
64
70
|
hasEveryKey(checker, checker.getTypeOfSymbol(property), checker.getTypeOfSymbol(counterpart), seen, depth + 1));
|
|
65
71
|
});
|
|
66
72
|
};
|
|
67
|
-
|
|
73
|
+
const isOptional = (property) => !!(property.flags & ts.SymbolFlags.Optional);
|
|
74
|
+
const describeType = (checker, type) => {
|
|
75
|
+
const text = `\`${checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation)}\``;
|
|
76
|
+
const declaration = (type.aliasSymbol ?? type.getSymbol())?.declarations?.[0];
|
|
77
|
+
return declaration ? `${text} from ${toTreePath(declaration.getSourceFile().fileName)}` : text;
|
|
78
|
+
};
|
|
79
|
+
const firstDifference = (checker, toolkit, creator, path, depth) => {
|
|
80
|
+
const from = checker.getNonNullableType(toolkit);
|
|
81
|
+
const to = checker.getNonNullableType(creator);
|
|
82
|
+
if (depth > MAX_DEPTH)
|
|
83
|
+
return null;
|
|
84
|
+
if (!isRecord(checker, from) || !isRecord(checker, to)) {
|
|
85
|
+
return checker.isTypeAssignableTo(from, to)
|
|
86
|
+
? null
|
|
87
|
+
: `\`${path}\` is ${describeType(checker, to)} there and ${describeType(checker, from)} in the action`;
|
|
88
|
+
}
|
|
89
|
+
const creatorProperties = checker.getPropertiesOfType(to);
|
|
90
|
+
const toolkitProperties = checker.getPropertiesOfType(from);
|
|
91
|
+
for (const property of toolkitProperties) {
|
|
92
|
+
const name = `${path ? `${path}.` : ''}${property.name}`;
|
|
93
|
+
const counterpart = creatorProperties.find((each) => each.escapedName === property.escapedName);
|
|
94
|
+
if (!counterpart)
|
|
95
|
+
return `\`${name}\` is sent by the action but missing there`;
|
|
96
|
+
if (isOptional(property) && !isOptional(counterpart))
|
|
97
|
+
return `\`${name}\` is required there and optional in the action`;
|
|
98
|
+
const nested = firstDifference(checker, checker.getTypeOfSymbol(property), checker.getTypeOfSymbol(counterpart), name, depth + 1);
|
|
99
|
+
if (nested)
|
|
100
|
+
return nested;
|
|
101
|
+
}
|
|
102
|
+
const extra = creatorProperties.find((property) => !isOptional(property) && !toolkitProperties.some((each) => each.escapedName === property.escapedName));
|
|
103
|
+
return extra ? `\`${path ? `${path}.` : ''}${extra.name}\` is required there but not sent by the action` : null;
|
|
104
|
+
};
|
|
105
|
+
export const compareToolkitArgs = (tree, graph, toolkit, creator) => {
|
|
68
106
|
if (toolkit.file === creator.file)
|
|
69
|
-
return false;
|
|
107
|
+
return { fits: false, difference: null };
|
|
70
108
|
const appended = new Map([
|
|
71
109
|
[toolkit.file, `\nexport type ${TOOLKIT_PROBE} = ${toolkit.typeText ?? '{}'};\n`],
|
|
72
110
|
[creator.file, `\nexport type ${CREATOR_PROBE} = ${creator.typeText ?? '{}'};\n`],
|
|
@@ -125,16 +163,25 @@ export const toolkitArgsFitCreator = (tree, graph, toolkit, creator) => {
|
|
|
125
163
|
const toolkitSide = typeOf('ToolkitSide');
|
|
126
164
|
const creatorSide = typeOf('CreatorSide');
|
|
127
165
|
if (!toolkitSide || !creatorSide)
|
|
128
|
-
return false;
|
|
129
|
-
if (containsAny(checker, toolkitSide, new Set(), 0) || containsAny(checker, creatorSide, new Set(), 0))
|
|
130
|
-
return false;
|
|
166
|
+
return { fits: false, difference: null };
|
|
167
|
+
if (containsAny(checker, toolkitSide, new Set(), 0) || containsAny(checker, creatorSide, new Set(), 0)) {
|
|
168
|
+
return { fits: false, difference: null };
|
|
169
|
+
}
|
|
131
170
|
const keys = (type) => checker
|
|
132
171
|
.getPropertiesOfType(type)
|
|
133
172
|
.map((property) => property.name)
|
|
134
173
|
.sort()
|
|
135
174
|
.join(',');
|
|
136
|
-
|
|
175
|
+
const fits = keys(toolkitSide) === keys(creatorSide) &&
|
|
137
176
|
checker.isTypeAssignableTo(toolkitSide, creatorSide) &&
|
|
138
|
-
hasEveryKey(checker, toolkitSide, creatorSide, new Set(), 0)
|
|
177
|
+
hasEveryKey(checker, toolkitSide, creatorSide, new Set(), 0);
|
|
178
|
+
if (fits)
|
|
179
|
+
return { fits, difference: null };
|
|
180
|
+
const toolkitRaw = typeOf('ToolkitRaw');
|
|
181
|
+
const creatorRaw = typeOf('CreatorRaw');
|
|
182
|
+
return {
|
|
183
|
+
fits,
|
|
184
|
+
difference: toolkitRaw && creatorRaw ? firstDifference(checker, toolkitRaw, creatorRaw, '', 0) : null,
|
|
185
|
+
};
|
|
139
186
|
};
|
|
140
187
|
//# sourceMappingURL=args-assignability.js.map
|
|
@@ -17,7 +17,7 @@ const defineConsumerTasks = (report) => {
|
|
|
17
17
|
});
|
|
18
18
|
report.define(CONSUMER_TASK.UNCONVERTED_ACTION_GROUP, {
|
|
19
19
|
title: 'Consumer types name an action group that is still on the toolkit',
|
|
20
|
-
action: '
|
|
20
|
+
action: 'A `MappedEntityState` / `ActionCallArgs` type names an action group whose feature was not converted. In a file that also names converted creators, only these types stay on the toolkit, as `ToolkitMappedEntityState` / `ToolkitActionCallArgs`. Otherwise the whole file was left on `@tomtomb/ngrx-toolkit`. 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.',
|
|
21
21
|
});
|
|
22
22
|
report.define(CONSUMER_TASK.SELECT_BY_ACTION_ID, {
|
|
23
23
|
title: 'Handle looked up by action id',
|
|
@@ -96,34 +96,47 @@ const namedImportOf = (sourceFile, local) => {
|
|
|
96
96
|
const isToolkitActionsFile = (tree, filePath) => !!filePath && filePath.endsWith('.actions.ts') && !!tree.read(filePath, 'utf-8')?.includes('createHttpActionGroup');
|
|
97
97
|
const unconvertedTypeQueries = (tree, graph, filePath, sourceFile, handleTypeNames) => {
|
|
98
98
|
const unconverted = [];
|
|
99
|
+
const unconvertedReferences = [];
|
|
100
|
+
let convertedReferences = 0;
|
|
99
101
|
const resolveQuery = (query) => {
|
|
100
102
|
const name = query.exprName;
|
|
101
103
|
const local = ts.isIdentifier(name) ? name.text : ts.isIdentifier(name.left) ? name.left.text : null;
|
|
102
104
|
const origin = local ? namedImportOf(sourceFile, local) : null;
|
|
103
105
|
if (!origin || origin.namespace === ts.isIdentifier(name))
|
|
104
|
-
return;
|
|
106
|
+
return false;
|
|
105
107
|
const declaringFile = origin.namespace
|
|
106
108
|
? graph.resolveFile(filePath, origin.specifier)
|
|
107
109
|
: graph.findDeclaringFile(filePath, origin.specifier, origin.imported);
|
|
108
|
-
if (isToolkitActionsFile(tree, declaringFile))
|
|
109
|
-
|
|
110
|
+
if (!isToolkitActionsFile(tree, declaringFile))
|
|
111
|
+
return false;
|
|
112
|
+
unconverted.push(name.getText(sourceFile));
|
|
113
|
+
return true;
|
|
110
114
|
};
|
|
111
115
|
const visit = (node) => {
|
|
112
116
|
if (ts.isTypeReferenceNode(node) &&
|
|
113
117
|
ts.isIdentifier(node.typeName) &&
|
|
114
118
|
handleTypeNames.has(node.typeName.text) &&
|
|
115
119
|
node.typeArguments) {
|
|
120
|
+
let queries = 0;
|
|
121
|
+
let unconvertedQueries = 0;
|
|
116
122
|
const visitArgument = (inner) => {
|
|
117
|
-
if (ts.isTypeQueryNode(inner))
|
|
118
|
-
|
|
123
|
+
if (ts.isTypeQueryNode(inner)) {
|
|
124
|
+
queries += 1;
|
|
125
|
+
if (resolveQuery(inner))
|
|
126
|
+
unconvertedQueries += 1;
|
|
127
|
+
}
|
|
119
128
|
ts.forEachChild(inner, visitArgument);
|
|
120
129
|
};
|
|
121
130
|
node.typeArguments.forEach(visitArgument);
|
|
131
|
+
if (unconvertedQueries > 0)
|
|
132
|
+
unconvertedReferences.push(node);
|
|
133
|
+
else if (queries > 0)
|
|
134
|
+
convertedReferences += 1;
|
|
122
135
|
}
|
|
123
136
|
ts.forEachChild(node, visit);
|
|
124
137
|
};
|
|
125
138
|
visit(sourceFile);
|
|
126
|
-
return unconverted;
|
|
139
|
+
return { names: unconverted, references: unconvertedReferences, convertedReferences };
|
|
127
140
|
};
|
|
128
141
|
const isCall = (node, name) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name;
|
|
129
142
|
const readActionId = (node, createActionId) => {
|
|
@@ -191,15 +204,9 @@ const injectorOf = (classNode) => {
|
|
|
191
204
|
}
|
|
192
205
|
return null;
|
|
193
206
|
};
|
|
194
|
-
const
|
|
195
|
-
const plan = { edits: [], rewritten: 0, needsInjectorImport: false, tasks: [] };
|
|
196
|
-
const consumedActionIds = new Set();
|
|
207
|
+
const classInjectorResolver = (sourceFile) => {
|
|
197
208
|
const classInjectors = new Map();
|
|
198
|
-
const
|
|
199
|
-
id: CONSUMER_TASK.SELECT_BY_ACTION_ID,
|
|
200
|
-
summary,
|
|
201
|
-
locations: [{ filePath, line: lineOf(sourceFile, node.getStart(sourceFile)) }],
|
|
202
|
-
});
|
|
209
|
+
const edits = [];
|
|
203
210
|
const injectorFor = (node) => {
|
|
204
211
|
let current = node.parent;
|
|
205
212
|
while (current && !ts.isClassLike(current))
|
|
@@ -215,14 +222,25 @@ const planRefreshSites = (filePath, sourceFile, content, createActionId) => {
|
|
|
215
222
|
const taken = classNode.members.some((member) => member.name?.getText(sourceFile) === 'injector');
|
|
216
223
|
const name = taken ? 'toolkitInjector' : 'injector';
|
|
217
224
|
classInjectors.set(classNode, name);
|
|
218
|
-
|
|
219
|
-
plan.edits.push({
|
|
225
|
+
edits.push({
|
|
220
226
|
start: classNode.members.pos,
|
|
221
227
|
end: classNode.members.pos,
|
|
222
228
|
text: `\n private ${name} = inject(Injector);\n`,
|
|
223
229
|
});
|
|
224
230
|
return name;
|
|
225
231
|
};
|
|
232
|
+
return { injectorFor, edits };
|
|
233
|
+
};
|
|
234
|
+
const planRefreshSites = (filePath, sourceFile, content, createActionId) => {
|
|
235
|
+
const plan = { edits: [], rewritten: 0, needsInjectorImport: false, tasks: [] };
|
|
236
|
+
const consumedActionIds = new Set();
|
|
237
|
+
const task = (node, summary) => plan.tasks.push({
|
|
238
|
+
id: CONSUMER_TASK.SELECT_BY_ACTION_ID,
|
|
239
|
+
summary,
|
|
240
|
+
locations: [{ filePath, line: lineOf(sourceFile, node.getStart(sourceFile)) }],
|
|
241
|
+
});
|
|
242
|
+
const injectors = classInjectorResolver(sourceFile);
|
|
243
|
+
const injectorFor = injectors.injectorFor;
|
|
226
244
|
const localActionId = (select, identifier) => {
|
|
227
245
|
let block = select.parent;
|
|
228
246
|
while (block && !ts.isBlock(block))
|
|
@@ -275,6 +293,8 @@ const planRefreshSites = (filePath, sourceFile, content, createActionId) => {
|
|
|
275
293
|
};
|
|
276
294
|
visit(sourceFile);
|
|
277
295
|
visitActionIds(sourceFile);
|
|
296
|
+
plan.edits.push(...injectors.edits);
|
|
297
|
+
plan.needsInjectorImport = injectors.edits.length > 0;
|
|
278
298
|
return plan;
|
|
279
299
|
};
|
|
280
300
|
/** Moves one consumer file from `@tomtomb/ngrx-toolkit` to `@ethlete/query/ngrx-toolkit`. */
|
|
@@ -290,7 +310,12 @@ export const rewriteConsumer = (tree, graph, filePath, content) => {
|
|
|
290
310
|
return null;
|
|
291
311
|
const handleTypeNames = new Set(elements.filter((element) => HANDLE_TYPES.has(element.imported)).map((element) => element.local));
|
|
292
312
|
const unconverted = unconvertedTypeQueries(tree, graph, filePath, sourceFile, handleTypeNames);
|
|
293
|
-
|
|
313
|
+
const unconvertedTask = {
|
|
314
|
+
id: CONSUMER_TASK.UNCONVERTED_ACTION_GROUP,
|
|
315
|
+
summary: `${[...new Set(unconverted.names)].map((name) => `\`${name}\``).join(', ')} still name toolkit action groups.`,
|
|
316
|
+
locations: [{ filePath }],
|
|
317
|
+
};
|
|
318
|
+
if (unconverted.names.length > 0 && (hasStoreSide || unconverted.convertedReferences === 0)) {
|
|
294
319
|
if (hasStoreSide)
|
|
295
320
|
return null;
|
|
296
321
|
return {
|
|
@@ -298,18 +323,23 @@ export const rewriteConsumer = (tree, graph, filePath, content) => {
|
|
|
298
323
|
rewritten: false,
|
|
299
324
|
leftOnToolkit: true,
|
|
300
325
|
refreshSitesRewritten: 0,
|
|
301
|
-
tasks: [
|
|
302
|
-
{
|
|
303
|
-
id: CONSUMER_TASK.UNCONVERTED_ACTION_GROUP,
|
|
304
|
-
summary: `${[...new Set(unconverted)].map((name) => `\`${name}\``).join(', ')} still name toolkit action groups.`,
|
|
305
|
-
locations: [{ filePath }],
|
|
306
|
-
},
|
|
307
|
-
],
|
|
326
|
+
tasks: [unconvertedTask],
|
|
308
327
|
};
|
|
309
328
|
}
|
|
329
|
+
const importedOf = new Map(elements.map((element) => [element.local, element.imported]));
|
|
330
|
+
const aliased = new Set();
|
|
331
|
+
const aliasEdits = unconverted.references.map((reference) => {
|
|
332
|
+
const imported = importedOf.get(reference.typeName.getText(sourceFile));
|
|
333
|
+
aliased.add(imported);
|
|
334
|
+
return {
|
|
335
|
+
start: reference.typeName.getStart(sourceFile),
|
|
336
|
+
end: reference.typeName.getEnd(),
|
|
337
|
+
text: `Toolkit${imported}`,
|
|
338
|
+
};
|
|
339
|
+
});
|
|
310
340
|
const createActionId = elements.find((element) => element.imported === 'createActionId')?.local ?? null;
|
|
311
341
|
const refresh = planRefreshSites(filePath, sourceFile, content, createActionId);
|
|
312
|
-
let result = applyEdits(content, refresh.edits);
|
|
342
|
+
let result = applyEdits(content, [...refresh.edits, ...aliasEdits]);
|
|
313
343
|
const rewrittenSource = createSourceFile(result, filePath);
|
|
314
344
|
const used = collectUsedIdentifiers(rewrittenSource);
|
|
315
345
|
const moved = [];
|
|
@@ -333,7 +363,9 @@ export const rewriteConsumer = (tree, graph, filePath, content) => {
|
|
|
333
363
|
statement.moduleSpecifier.text === INTEROP_MODULE);
|
|
334
364
|
const replacement = [
|
|
335
365
|
moved.length > 0 && !hasInteropImport ? `import { ${moved.join(', ')} } from '${INTEROP_MODULE}';` : null,
|
|
336
|
-
kept.length
|
|
366
|
+
kept.length + aliased.size > 0
|
|
367
|
+
? `import { ${[...kept, ...[...aliased].map((name) => `${name} as Toolkit${name}`)].join(', ')} } from '${TOOLKIT_MODULE}';`
|
|
368
|
+
: null,
|
|
337
369
|
].filter((line) => line !== null);
|
|
338
370
|
const importEdits = toolkitImportsOf(rewrittenSource).map((statement, index) => index === 0 && replacement.length > 0
|
|
339
371
|
? { start: statement.getStart(rewrittenSource), end: statement.getEnd(), text: replacement.join('\n') }
|
|
@@ -350,6 +382,8 @@ export const rewriteConsumer = (tree, graph, filePath, content) => {
|
|
|
350
382
|
result = ensureNamedImports({ content: result, importsNeeded: moved, moduleSpecifier: INTEROP_MODULE });
|
|
351
383
|
}
|
|
352
384
|
const tasks = [...refresh.tasks];
|
|
385
|
+
if (aliased.size > 0)
|
|
386
|
+
tasks.push(unconvertedTask);
|
|
353
387
|
if (kept.length > 0) {
|
|
354
388
|
tasks.push({
|
|
355
389
|
id: CONSUMER_TASK.UNSUPPORTED_IMPORT,
|
|
@@ -360,12 +394,12 @@ export const rewriteConsumer = (tree, graph, filePath, content) => {
|
|
|
360
394
|
return {
|
|
361
395
|
content: result,
|
|
362
396
|
rewritten: result !== content,
|
|
363
|
-
leftOnToolkit: kept.length > 0,
|
|
397
|
+
leftOnToolkit: kept.length + aliased.size > 0,
|
|
364
398
|
refreshSitesRewritten: refresh.rewritten,
|
|
365
399
|
tasks,
|
|
366
400
|
};
|
|
367
401
|
};
|
|
368
|
-
const isInterceptor = (node, sourceFile) => {
|
|
402
|
+
export const isInterceptor = (node, sourceFile) => {
|
|
369
403
|
if (ts.isClassDeclaration(node)) {
|
|
370
404
|
return !!node.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText(sourceFile) === 'HttpInterceptor'));
|
|
371
405
|
}
|
|
@@ -438,13 +472,14 @@ const migratedFacadeNames = (graph, migratedFacades, filePath, sourceFile) => {
|
|
|
438
472
|
}
|
|
439
473
|
return names;
|
|
440
474
|
};
|
|
441
|
-
const
|
|
475
|
+
const rewriteFacadeEventCalls = (tree, graph, migratedFacades, filePath, content) => {
|
|
476
|
+
const unchanged = { content, rewritten: 0, tasks: [] };
|
|
442
477
|
if (!/\.(on|once)\s*\(/.test(content))
|
|
443
|
-
return
|
|
478
|
+
return unchanged;
|
|
444
479
|
const sourceFile = createSourceFile(content, filePath);
|
|
445
480
|
const facadeNames = migratedFacadeNames(graph, migratedFacades, filePath, sourceFile);
|
|
446
481
|
if (facadeNames.size === 0)
|
|
447
|
-
return
|
|
482
|
+
return unchanged;
|
|
448
483
|
const isFacadeType = (type) => !!type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && facadeNames.has(type.typeName.text);
|
|
449
484
|
const isFacadeInject = (node) => !!node && isCall(node, 'inject') && !!node.arguments[0] && facadeNames.has(node.arguments[0].getText(sourceFile));
|
|
450
485
|
const bound = new Set();
|
|
@@ -466,12 +501,50 @@ const findFacadeEventCalls = (graph, migratedFacades, filePath, content) => {
|
|
|
466
501
|
receiver.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
467
502
|
bound.has(receiver.name.text));
|
|
468
503
|
};
|
|
504
|
+
const isConvertedCreator = (local) => {
|
|
505
|
+
const origin = namedImportOf(sourceFile, local);
|
|
506
|
+
if (!origin || origin.namespace)
|
|
507
|
+
return false;
|
|
508
|
+
const declaringFile = graph.findDeclaringFile(filePath, origin.specifier, origin.imported);
|
|
509
|
+
return !!declaringFile && !isToolkitActionsFile(tree, declaringFile);
|
|
510
|
+
};
|
|
511
|
+
const successCreatorsText = (call) => {
|
|
512
|
+
const argument = call.arguments[0];
|
|
513
|
+
if (call.arguments.length !== 1 || !argument)
|
|
514
|
+
return null;
|
|
515
|
+
const items = ts.isArrayLiteralExpression(argument) ? [...argument.elements] : [argument];
|
|
516
|
+
const creators = [];
|
|
517
|
+
for (const item of items) {
|
|
518
|
+
if (!ts.isPropertyAccessExpression(item) ||
|
|
519
|
+
item.name.text !== 'success' ||
|
|
520
|
+
!ts.isIdentifier(item.expression) ||
|
|
521
|
+
!isConvertedCreator(item.expression.text)) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
creators.push(item.expression.text);
|
|
525
|
+
}
|
|
526
|
+
if (creators.length === 0)
|
|
527
|
+
return null;
|
|
528
|
+
return ts.isArrayLiteralExpression(argument) ? `[${creators.join(', ')}]` : creators[0];
|
|
529
|
+
};
|
|
530
|
+
const injectors = classInjectorResolver(sourceFile);
|
|
531
|
+
const edits = [];
|
|
469
532
|
const tasks = [];
|
|
470
533
|
const visit = (node) => {
|
|
471
534
|
if (ts.isCallExpression(node) &&
|
|
472
535
|
ts.isPropertyAccessExpression(node.expression) &&
|
|
473
536
|
['on', 'once'].includes(node.expression.name.text) &&
|
|
474
537
|
isFacade(node.expression.expression)) {
|
|
538
|
+
const creators = node.expression.name.text === 'on' ? successCreatorsText(node) : null;
|
|
539
|
+
const injector = creators ? injectors.injectorFor(node) : null;
|
|
540
|
+
if (creators && injector) {
|
|
541
|
+
edits.push({
|
|
542
|
+
start: node.getStart(sourceFile),
|
|
543
|
+
end: node.getEnd(),
|
|
544
|
+
text: `toolkitOnSuccess(${creators}, { injector: this.${injector} })`,
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
475
548
|
tasks.push({
|
|
476
549
|
id: CONSUMER_TASK.FACADE_ON,
|
|
477
550
|
summary: `${code(node.getText(sourceFile).replace(/\s+/g, ' '))} listens to toolkit actions the migrated facade no longer emits.`,
|
|
@@ -481,7 +554,22 @@ const findFacadeEventCalls = (graph, migratedFacades, filePath, content) => {
|
|
|
481
554
|
ts.forEachChild(node, visit);
|
|
482
555
|
};
|
|
483
556
|
visit(sourceFile);
|
|
484
|
-
|
|
557
|
+
if (edits.length === 0)
|
|
558
|
+
return { ...unchanged, tasks };
|
|
559
|
+
let result = applyEdits(content, [...edits, ...injectors.edits]);
|
|
560
|
+
result = ensureNamedImports({
|
|
561
|
+
content: result,
|
|
562
|
+
importsNeeded: ['toolkitOnSuccess'],
|
|
563
|
+
moduleSpecifier: INTEROP_MODULE,
|
|
564
|
+
});
|
|
565
|
+
if (injectors.edits.length > 0) {
|
|
566
|
+
result = ensureNamedImports({
|
|
567
|
+
content: result,
|
|
568
|
+
importsNeeded: ['inject', 'Injector'],
|
|
569
|
+
moduleSpecifier: '@angular/core',
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return { content: result, rewritten: edits.length, tasks };
|
|
485
573
|
};
|
|
486
574
|
/** Runs the consumer side of the migration on every scoped file that still imports `@tomtomb/ngrx-toolkit`. */
|
|
487
575
|
export const migrateToolkitConsumers = (tree, graph, inScope, report, migratedFacades) => {
|
|
@@ -490,6 +578,7 @@ export const migrateToolkitConsumers = (tree, graph, inScope, report, migratedFa
|
|
|
490
578
|
filesRewritten: 0,
|
|
491
579
|
filesLeftOnToolkit: 0,
|
|
492
580
|
refreshSitesRewritten: 0,
|
|
581
|
+
facadeListenersRewritten: 0,
|
|
493
582
|
interceptorsFound: 0,
|
|
494
583
|
};
|
|
495
584
|
for (const filePath of inScope) {
|
|
@@ -501,16 +590,23 @@ export const migrateToolkitConsumers = (tree, graph, inScope, report, migratedFa
|
|
|
501
590
|
const interceptors = findInterceptors(tree, inScope, filePath, content);
|
|
502
591
|
interceptors.forEach((task) => report.add(task));
|
|
503
592
|
stats.interceptorsFound += interceptors.length;
|
|
504
|
-
|
|
505
|
-
|
|
593
|
+
const events = rewriteFacadeEventCalls(tree, graph, migratedFacades, filePath, content);
|
|
594
|
+
events.tasks.forEach((task) => report.add(task));
|
|
595
|
+
stats.facadeListenersRewritten += events.rewritten;
|
|
596
|
+
if (events.rewritten > 0) {
|
|
597
|
+
tree.write(filePath, events.content);
|
|
598
|
+
stats.filesRewritten += 1;
|
|
599
|
+
}
|
|
600
|
+
if (!events.content.includes(TOOLKIT_MODULE))
|
|
506
601
|
continue;
|
|
507
|
-
const result = rewriteConsumer(tree, graph, filePath, content);
|
|
602
|
+
const result = rewriteConsumer(tree, graph, filePath, events.content);
|
|
508
603
|
if (!result)
|
|
509
604
|
continue;
|
|
510
605
|
result.tasks.forEach((task) => report.add(task));
|
|
511
606
|
if (result.rewritten) {
|
|
512
607
|
tree.write(filePath, result.content);
|
|
513
|
-
|
|
608
|
+
if (events.rewritten === 0)
|
|
609
|
+
stats.filesRewritten += 1;
|
|
514
610
|
}
|
|
515
611
|
if (result.leftOnToolkit)
|
|
516
612
|
stats.filesLeftOnToolkit += 1;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProjects } from '@nx/devkit';
|
|
2
2
|
import * as ts from 'typescript';
|
|
3
|
-
import {
|
|
3
|
+
import { compareToolkitArgs } from './args-assignability.js';
|
|
4
4
|
import { capitalizeFirstLetter, createSourceFile } from '../migrate-to-query-v3/shared.js';
|
|
5
5
|
import { HTTP_VERBS, findTypeDeclaration } from './feature.js';
|
|
6
6
|
import { TOOLKIT_TASK } from './report.js';
|
|
@@ -226,8 +226,8 @@ const decideReuse = (tree, graph, feature, call, existing) => {
|
|
|
226
226
|
const response = call.responseType ? normalizeType(call.responseType) : null;
|
|
227
227
|
const argTexts = toolkitArgTexts(call);
|
|
228
228
|
const argMismatch = (creator) => COMPARED_ARG_KEYS.find((key) => creator.args?.[key] !== argTexts[key]) ?? null;
|
|
229
|
-
const
|
|
230
|
-
|
|
229
|
+
const compare = (creator) => compareToolkitArgs(tree, graph, { file: feature.files.actions, typeText: call.args?.typeText ?? null }, { file: creator.filePath, typeText: creator.argsTypeText });
|
|
230
|
+
const argsFit = (creator) => argMismatch(creator) === null || compare(creator).fits;
|
|
231
231
|
const match = sameRoute.find((creator) => creator.responseType === response &&
|
|
232
232
|
creator.pathParams.join(',') === call.route.pathParams.join(',') &&
|
|
233
233
|
argsFit(creator));
|
|
@@ -240,6 +240,10 @@ const decideReuse = (tree, graph, feature, call, existing) => {
|
|
|
240
240
|
const type = text.replace(/^\?/, '');
|
|
241
241
|
return `\`${type.length > 120 ? `${type.slice(0, 120)}…` : type}\``;
|
|
242
242
|
};
|
|
243
|
+
const argsDifference = (creator) => {
|
|
244
|
+
const difference = differingKey ? compare(creator).difference : null;
|
|
245
|
+
return difference ? `: ${difference}` : '';
|
|
246
|
+
};
|
|
243
247
|
const reason = other.pathParams.join(',') !== call.route.pathParams.join(',')
|
|
244
248
|
? 'its path params are named differently'
|
|
245
249
|
: other.responseType === null
|
|
@@ -248,7 +252,7 @@ const decideReuse = (tree, graph, feature, call, existing) => {
|
|
|
248
252
|
? `its response is \`${other.responseType}\`, not \`${response ?? 'unknown'}\``
|
|
249
253
|
: other.args === null
|
|
250
254
|
? 'its args type is not an interface or type literal, so it cannot be compared'
|
|
251
|
-
: `its ${differingKey === 'body' ? 'body' : 'query params'} are ${describe(other.args[differingKey])}, not ${describe(argTexts[differingKey])}`;
|
|
255
|
+
: `its ${differingKey === 'body' ? 'body' : 'query params'} are ${describe(other.args[differingKey])}, not ${describe(argTexts[differingKey])}${argsDifference(other)}`;
|
|
252
256
|
return {
|
|
253
257
|
mismatch: `\`${call.name}\` got its own creator: \`${other.name}\` (${other.filePath}:${other.line}) has the same ${call.verb.toUpperCase()} route, but ${reason}.`,
|
|
254
258
|
};
|
|
@@ -245,6 +245,27 @@ const parseRoute = (node, sourceFile) => {
|
|
|
245
245
|
}
|
|
246
246
|
return { text, normalized, pathParams, staticPrefix: body.head.text };
|
|
247
247
|
};
|
|
248
|
+
const isEmptyValue = (node) => node.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(node) && node.text === 'undefined');
|
|
249
|
+
const describeHeaders = (value, method, sourceFile) => {
|
|
250
|
+
const text = value.getText(sourceFile);
|
|
251
|
+
const owner = method.parent;
|
|
252
|
+
const property = ts.isPropertyAccessExpression(value) && value.expression.kind === ts.SyntaxKind.ThisKeyword && ts.isClassLike(owner)
|
|
253
|
+
? owner.members.find((member) => ts.isPropertyDeclaration(member) && member.name.getText(sourceFile) === value.name.text)
|
|
254
|
+
: undefined;
|
|
255
|
+
const resolved = property?.initializer ?? value;
|
|
256
|
+
const source = property ? ` (from \`${text}\`)` : '';
|
|
257
|
+
if (ts.isConditionalExpression(resolved)) {
|
|
258
|
+
const condition = resolved.condition.getText(sourceFile);
|
|
259
|
+
if (isEmptyValue(resolved.whenFalse)) {
|
|
260
|
+
return `\`${resolved.whenTrue.getText(sourceFile)}\`${source} only when \`${condition}\``;
|
|
261
|
+
}
|
|
262
|
+
if (isEmptyValue(resolved.whenTrue)) {
|
|
263
|
+
return `\`${resolved.whenFalse.getText(sourceFile)}\`${source} only when \`${condition}\` is false`;
|
|
264
|
+
}
|
|
265
|
+
return `\`${resolved.whenTrue.getText(sourceFile)}\` or \`${resolved.whenFalse.getText(sourceFile)}\`${source} depending on \`${condition}\``;
|
|
266
|
+
}
|
|
267
|
+
return `\`${resolved.getText(sourceFile)}\`${source}`;
|
|
268
|
+
};
|
|
248
269
|
const parseServiceMethod = (method, sourceFile) => {
|
|
249
270
|
const statements = method.body?.statements ?? [];
|
|
250
271
|
const statement = statements[0];
|
|
@@ -265,7 +286,7 @@ const parseServiceMethod = (method, sourceFile) => {
|
|
|
265
286
|
return 'The request options are not an object literal.';
|
|
266
287
|
}
|
|
267
288
|
let route = 'The method has no `apiRoute`.';
|
|
268
|
-
let headers =
|
|
289
|
+
let headers = null;
|
|
269
290
|
let cacheExpiry = false;
|
|
270
291
|
for (const property of options.properties) {
|
|
271
292
|
if (!ts.isPropertyAssignment(property))
|
|
@@ -288,7 +309,13 @@ const parseServiceMethod = (method, sourceFile) => {
|
|
|
288
309
|
(ts.isPropertyAssignment(element) && element.name.getText(sourceFile) === 'headers'));
|
|
289
310
|
if (!spreadsArgs)
|
|
290
311
|
return `Unsupported \`httpOpts\`: \`${value.getText(sourceFile)}\`.`;
|
|
291
|
-
|
|
312
|
+
const header = value.properties.find((element) => ts.isPropertyAssignment(element) && element.name.getText(sourceFile) === 'headers');
|
|
313
|
+
if (header) {
|
|
314
|
+
headers = {
|
|
315
|
+
line: lineOf(sourceFile, header.getStart(sourceFile)),
|
|
316
|
+
description: describeHeaders(header.initializer, method, sourceFile),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
292
319
|
}
|
|
293
320
|
else if (name === 'extras') {
|
|
294
321
|
if (!ts.isObjectLiteralExpression(value))
|
|
@@ -450,8 +477,8 @@ const analyzeFeature = (tree, graph, input) => {
|
|
|
450
477
|
if (method.headers) {
|
|
451
478
|
warnings.push({
|
|
452
479
|
id: TOOLKIT_TASK.SERVICE_HEADERS,
|
|
453
|
-
summary: `\`${serviceMethod}\` added headers
|
|
454
|
-
locations: [{ filePath: files.service }],
|
|
480
|
+
summary: `\`${serviceMethod}\` added the headers ${method.headers.description}; the creator \`${group.name}\` does not.`,
|
|
481
|
+
locations: [{ filePath: files.service, line: method.headers.line }],
|
|
455
482
|
});
|
|
456
483
|
}
|
|
457
484
|
if (method.cacheExpiry) {
|
|
@@ -6,6 +6,7 @@ import { migrateToolkitConsumers } from './consumers.js';
|
|
|
6
6
|
import { rewriteFacade } from './facade.js';
|
|
7
7
|
import { discoverFeatures } from './feature.js';
|
|
8
8
|
import { TOOLKIT_TASK, ToolkitMigrationReport } from './report.js';
|
|
9
|
+
import { reportLegacyModuleResolution } from './module-resolution.js';
|
|
9
10
|
import { removeFeatureStores } from './store-removal.js';
|
|
10
11
|
const parsePublicRoutes = (value) => {
|
|
11
12
|
if (value === undefined)
|
|
@@ -114,6 +115,7 @@ export const migrateToolkitStores = (tree, options, report) => {
|
|
|
114
115
|
const removal = removeFeatureStores(tree, graph, inScope, ready, report);
|
|
115
116
|
const migratedFacades = new Set(ready.map((feature) => feature.files.facade).filter((file) => !!file));
|
|
116
117
|
const consumers = migrateToolkitConsumers(tree, createModuleGraph(tree), inScope, report, migratedFacades);
|
|
118
|
+
reportLegacyModuleResolution(tree, createModuleGraph(tree), inScope, report);
|
|
117
119
|
return {
|
|
118
120
|
featuresFound: discovered.features.length,
|
|
119
121
|
featuresConverted: ready.length,
|
|
@@ -130,7 +132,7 @@ export default async function migrate(tree, schema) {
|
|
|
130
132
|
const report = new ToolkitMigrationReport();
|
|
131
133
|
const stats = migrateToolkitStores(tree, schema, report);
|
|
132
134
|
console.log(` ${stats.featuresConverted}/${stats.featuresFound} features converted, ${stats.creatorsWritten} creators written, ${stats.creatorsReused} reused, ${stats.deletedFiles.length} files deleted.`);
|
|
133
|
-
console.log(` ${stats.consumers.filesRewritten} consumer files moved to @ethlete/query/ngrx-toolkit, ${stats.consumers.refreshSitesRewritten} refresh sites rewritten.`);
|
|
135
|
+
console.log(` ${stats.consumers.filesRewritten} consumer files moved to @ethlete/query/ngrx-toolkit, ${stats.consumers.refreshSitesRewritten} refresh sites and ${stats.consumers.facadeListenersRewritten} facade listeners rewritten.`);
|
|
134
136
|
report.writeToTree(tree);
|
|
135
137
|
report.printSummary();
|
|
136
138
|
if (!schema.skipFormat) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { findLegacyModuleResolutions } from '../migrate-to-query-v3/module-resolution.js';
|
|
2
|
+
import { INTEROP_MODULE } from './facade.js';
|
|
3
|
+
export const MODULE_RESOLUTION_TASK = 'NTK-MODULE-RESOLUTION';
|
|
4
|
+
/**
|
|
5
|
+
* Records a task for every project tsconfig that resolves modules with `node` / `node10` while the project imports
|
|
6
|
+
* `@ethlete/query/ngrx-toolkit`, directly or through a workspace path alias.
|
|
7
|
+
*/
|
|
8
|
+
export const reportLegacyModuleResolution = (tree, graph, files, report) => {
|
|
9
|
+
report.define(MODULE_RESOLUTION_TASK, {
|
|
10
|
+
title: 'Project resolves modules with `node`, which cannot find `@ethlete/query/ngrx-toolkit`',
|
|
11
|
+
action: '`moduleResolution: node` (`node10`) ignores the `exports` map of `@ethlete/query`, so the `@ethlete/query/ngrx-toolkit` import does not resolve - also when it sits in another project this one imports through a path alias. Set `moduleResolution` to `bundler` in the listed tsconfig (or the base config it inherits it from).',
|
|
12
|
+
});
|
|
13
|
+
for (const { configPath, resolution, alias } of findLegacyModuleResolutions(tree, graph, files, (specifier) => specifier === INTEROP_MODULE)) {
|
|
14
|
+
report.add({
|
|
15
|
+
id: MODULE_RESOLUTION_TASK,
|
|
16
|
+
summary: alias
|
|
17
|
+
? `${configPath} resolves modules with \`${resolution}\` and reaches \`${INTEROP_MODULE}\` through \`${alias}\`.`
|
|
18
|
+
: `${configPath} resolves modules with \`${resolution}\`.`,
|
|
19
|
+
locations: [{ filePath: configPath }],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=module-resolution.js.map
|