@ethlete/query 6.0.0-next.51 → 6.0.0-next.53

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +393 -0
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
  4. package/fesm2022/ethlete-query-testing.mjs +67 -25
  5. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  6. package/fesm2022/ethlete-query.mjs +7895 -7226
  7. package/fesm2022/ethlete-query.mjs.map +1 -1
  8. package/generators/generators.json +5 -0
  9. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
  10. package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
  11. package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
  12. package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
  13. package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
  14. package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
  15. package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
  16. package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
  17. package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
  18. package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
  19. package/generators/migrate-query-opt-in-features/migration.js +10 -9
  20. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
  21. package/generators/migrate-to-query-v3/cleanup-migration.js +163 -53
  22. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
  23. package/generators/migrate-to-query-v3/migration-scope.js +2 -0
  24. package/generators/migrate-to-query-v3/migration.js +5 -4
  25. package/generators/migrate-to-query-v3/module-graph.js +41 -29
  26. package/generators/migrate-to-query-v3/query-client-migration.js +209 -126
  27. package/generators/migrate-to-query-v3/rename-symbols.js +38 -11
  28. package/generators/migrate-to-query-v3/report.js +40 -7
  29. package/generators/migrate-to-query-v3/shared.js +46 -1
  30. package/generators/prep-for-query-v3/migration.js +400 -57
  31. package/migrations/query-field-default-types.md +43 -0
  32. package/migrations/search-query-field-string.md +43 -0
  33. package/migrations.json +16 -0
  34. package/package.json +5 -1
  35. package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
  36. package/types/ethlete-query-testing.d.ts +28 -11
  37. package/types/ethlete-query.d.ts +789 -584
@@ -0,0 +1,513 @@
1
+ import * as ts from 'typescript';
2
+ import { createSourceFile } from '../migrate-to-query-v3/shared.js';
3
+ import { TOOLKIT_TASK } from './report.js';
4
+ import { lineOf } from './ts-edits.js';
5
+ export const TOOLKIT_MODULE = '@tomtomb/ngrx-toolkit';
6
+ export const HTTP_VERBS = ['get', 'post', 'put', 'patch', 'delete'];
7
+ const TOOLKIT_ARGS_KEYS = ['queryParams', 'params', 'body'];
8
+ const FEATURE_FILE_SUFFIX = '.actions.ts';
9
+ const siblingFile = (tree, dir, base, kind) => {
10
+ const path = `${dir}/${base}.${kind}.ts`;
11
+ return tree.exists(path) ? path : null;
12
+ };
13
+ const propertyNamed = (object, name) => object.properties.find((property) => ts.isPropertyAssignment(property) && property.name.getText() === name);
14
+ const typeLiteralMember = (literal, name) => literal.members.find((member) => ts.isPropertySignature(member) && member.name.getText() === name);
15
+ const isNullType = (node) => node.kind === ts.SyntaxKind.NullKeyword ||
16
+ node.kind === ts.SyntaxKind.UndefinedKeyword ||
17
+ (ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword);
18
+ const parseActionGroup = (declaration, sourceFile) => {
19
+ const initializer = declaration.initializer;
20
+ if (!initializer ||
21
+ !ts.isCallExpression(initializer) ||
22
+ !ts.isIdentifier(initializer.expression) ||
23
+ initializer.expression.text !== 'createHttpActionGroup' ||
24
+ !ts.isIdentifier(declaration.name)) {
25
+ return null;
26
+ }
27
+ const options = initializer.arguments[0];
28
+ const argsTypes = options && ts.isObjectLiteralExpression(options) ? propertyNamed(options, 'argsTypes') : undefined;
29
+ const defineCall = argsTypes?.initializer;
30
+ const typeArgument = defineCall && ts.isCallExpression(defineCall) ? defineCall.typeArguments?.[0] : undefined;
31
+ const literal = typeArgument && ts.isTypeLiteralNode(typeArgument) ? typeArgument : undefined;
32
+ return {
33
+ name: declaration.name.text,
34
+ argsType: literal ? (typeLiteralMember(literal, 'args')?.type ?? null) : null,
35
+ responseType: literal ? (typeLiteralMember(literal, 'response')?.type ?? null) : null,
36
+ line: lineOf(sourceFile, declaration.getStart(sourceFile)),
37
+ };
38
+ };
39
+ const parseActionsFile = (sourceFile) => {
40
+ const groups = [];
41
+ const imports = [];
42
+ const foreign = [];
43
+ const recordCandidates = [];
44
+ for (const statement of sourceFile.statements) {
45
+ if (ts.isImportDeclaration(statement)) {
46
+ imports.push(statement);
47
+ continue;
48
+ }
49
+ if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1) {
50
+ foreign.push(statement);
51
+ continue;
52
+ }
53
+ const declaration = statement.declarationList.declarations[0];
54
+ const initializer = declaration.initializer;
55
+ const group = parseActionGroup(declaration, sourceFile);
56
+ if (group) {
57
+ groups.push(group);
58
+ }
59
+ else if (initializer && (ts.isStringLiteral(initializer) || ts.isNoSubstitutionTemplateLiteral(initializer))) {
60
+ continue;
61
+ }
62
+ else if (initializer && ts.isObjectLiteralExpression(initializer)) {
63
+ recordCandidates.push(initializer);
64
+ }
65
+ else {
66
+ foreign.push(statement);
67
+ }
68
+ }
69
+ const groupNames = new Set(groups.map((group) => group.name));
70
+ for (const record of recordCandidates) {
71
+ const onlyGroups = record.properties.every((property) => ts.isShorthandPropertyAssignment(property)
72
+ ? groupNames.has(property.name.text)
73
+ : ts.isPropertyAssignment(property) &&
74
+ ts.isIdentifier(property.initializer) &&
75
+ groupNames.has(property.initializer.text));
76
+ if (!onlyGroups)
77
+ foreign.push(record.parent.parent.parent);
78
+ }
79
+ return { groups, imports, foreign };
80
+ };
81
+ export const findTypeDeclaration = (sourceFile, name) => sourceFile.statements.find((statement) => (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) && statement.name.text === name);
82
+ const membersToArgs = (typeText, members, memberTypeText) => {
83
+ const mapped = new Map();
84
+ const extraKeys = [];
85
+ for (const member of members) {
86
+ if (!ts.isPropertySignature(member) || !member.type) {
87
+ extraKeys.push(member.name?.getText() ?? '?');
88
+ continue;
89
+ }
90
+ const key = member.name.getText();
91
+ if (TOOLKIT_ARGS_KEYS.includes(key)) {
92
+ mapped.set(key, {
93
+ typeText: memberTypeText(member, key),
94
+ declaredText: member.type.getText(),
95
+ optional: !!member.questionToken,
96
+ });
97
+ }
98
+ else {
99
+ extraKeys.push(key);
100
+ }
101
+ }
102
+ return { typeText, members: mapped, extraKeys };
103
+ };
104
+ const resolveArgs = (tree, graph, actionsFile, actionsSource, argsType) => {
105
+ const typeText = argsType.getText(actionsSource);
106
+ if (ts.isTypeLiteralNode(argsType)) {
107
+ return {
108
+ args: membersToArgs(typeText, argsType.members, (member) => member.type.getText(actionsSource)),
109
+ inherited: false,
110
+ };
111
+ }
112
+ if (!ts.isTypeReferenceNode(argsType) || argsType.typeArguments) {
113
+ return { unresolved: `\`${typeText}\` is neither a type literal nor a plain type reference.` };
114
+ }
115
+ const typeName = argsType.typeName;
116
+ const [namespaceName, symbolName] = ts.isQualifiedName(typeName)
117
+ ? [ts.isIdentifier(typeName.left) ? typeName.left.text : null, typeName.right.text]
118
+ : [null, typeName.text];
119
+ let declaringFile = null;
120
+ if (!namespaceName && findTypeDeclaration(actionsSource, symbolName)) {
121
+ declaringFile = actionsFile;
122
+ }
123
+ for (const statement of actionsSource.statements) {
124
+ if (declaringFile || !ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
125
+ continue;
126
+ const bindings = statement.importClause?.namedBindings;
127
+ const specifier = statement.moduleSpecifier.text;
128
+ if (namespaceName && bindings && ts.isNamespaceImport(bindings) && bindings.name.text === namespaceName) {
129
+ declaringFile = graph.findDeclaringFile(actionsFile, specifier, symbolName);
130
+ }
131
+ if (!namespaceName && bindings && ts.isNamedImports(bindings)) {
132
+ const element = bindings.elements.find((candidate) => candidate.name.text === symbolName);
133
+ if (element) {
134
+ declaringFile = graph.findDeclaringFile(actionsFile, specifier, element.propertyName?.text ?? symbolName);
135
+ }
136
+ }
137
+ }
138
+ const declaringContent = declaringFile ? tree.read(declaringFile, 'utf-8') : null;
139
+ if (!declaringFile || !declaringContent) {
140
+ return { unresolved: `\`${typeText}\` does not resolve to a declaration in the workspace.` };
141
+ }
142
+ const declaration = findTypeDeclaration(createSourceFile(declaringContent, declaringFile), symbolName);
143
+ const indexed = (_member, key) => `${typeText}['${key}']`;
144
+ if (declaration && ts.isInterfaceDeclaration(declaration) && !declaration.typeParameters) {
145
+ return {
146
+ args: membersToArgs(typeText, declaration.members, indexed),
147
+ inherited: !!declaration.heritageClauses?.length,
148
+ };
149
+ }
150
+ if (declaration && ts.isTypeAliasDeclaration(declaration) && ts.isTypeLiteralNode(declaration.type)) {
151
+ return { args: membersToArgs(typeText, declaration.type.members, indexed), inherited: false };
152
+ }
153
+ return { unresolved: `\`${typeText}\` is not an interface or type literal.` };
154
+ };
155
+ const actionNameOf = (expression) => {
156
+ if (ts.isIdentifier(expression))
157
+ return expression.text;
158
+ if (ts.isPropertyAccessExpression(expression))
159
+ return expression.name.text;
160
+ return null;
161
+ };
162
+ const parseEffectsFile = (sourceFile) => {
163
+ const serviceMethods = new Map();
164
+ const foreign = [];
165
+ for (const statement of sourceFile.statements) {
166
+ if (!ts.isClassDeclaration(statement))
167
+ continue;
168
+ for (const member of statement.members) {
169
+ if (ts.isConstructorDeclaration(member))
170
+ continue;
171
+ const initializer = ts.isPropertyDeclaration(member) ? member.initializer : undefined;
172
+ const callee = initializer && ts.isCallExpression(initializer) ? initializer.expression : undefined;
173
+ const options = initializer && ts.isCallExpression(initializer) ? initializer.arguments[0] : undefined;
174
+ if (!callee ||
175
+ !ts.isPropertyAccessExpression(callee) ||
176
+ callee.expression.kind !== ts.SyntaxKind.ThisKeyword ||
177
+ !/^onAction(Merge|Switch|Exhaust|Concat)Map$/.test(callee.name.text) ||
178
+ !options ||
179
+ !ts.isObjectLiteralExpression(options) ||
180
+ options.properties.length !== 2) {
181
+ foreign.push(member);
182
+ continue;
183
+ }
184
+ const action = propertyNamed(options, 'action')?.initializer;
185
+ const serviceCall = propertyNamed(options, 'serviceCall')?.initializer;
186
+ const actionName = action ? actionNameOf(action) : null;
187
+ if (!actionName || !serviceCall || !ts.isPropertyAccessExpression(serviceCall)) {
188
+ foreign.push(member);
189
+ continue;
190
+ }
191
+ serviceMethods.set(actionName, serviceCall.name.text);
192
+ }
193
+ }
194
+ return { serviceMethods, foreign };
195
+ };
196
+ const isReducerPlainSlice = (content) => {
197
+ const sourceFile = createSourceFile(content);
198
+ let slice = false;
199
+ let foreign = false;
200
+ const visit = (node) => {
201
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
202
+ if (node.expression.text === 'createReducerSlice') {
203
+ slice = true;
204
+ const options = node.arguments[0];
205
+ const extra = options && ts.isObjectLiteralExpression(options) ? propertyNamed(options, 'initialStateExtra') : null;
206
+ if (extra && !(ts.isObjectLiteralExpression(extra.initializer) && extra.initializer.properties.length === 0)) {
207
+ foreign = true;
208
+ }
209
+ }
210
+ if (node.expression.text === 'createReducer' || node.expression.text === 'on')
211
+ foreign = true;
212
+ }
213
+ ts.forEachChild(node, visit);
214
+ };
215
+ visit(sourceFile);
216
+ return slice && !foreign;
217
+ };
218
+ const parseRoute = (node, sourceFile) => {
219
+ const text = node.getText(sourceFile);
220
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
221
+ return { text, normalized: node.text, pathParams: [], staticPrefix: node.text };
222
+ }
223
+ if (!ts.isArrowFunction(node) || node.parameters.length !== 1 || !ts.isIdentifier(node.parameters[0].name)) {
224
+ return `The route \`${text}\` is not a string or a \`(p) => \\\`…\\\`\` function.`;
225
+ }
226
+ const paramName = node.parameters[0].name.text;
227
+ const body = node.body;
228
+ if (ts.isStringLiteral(body) || ts.isNoSubstitutionTemplateLiteral(body)) {
229
+ return { text, normalized: body.text, pathParams: [], staticPrefix: body.text };
230
+ }
231
+ if (!ts.isTemplateExpression(body)) {
232
+ return `The route \`${text}\` does not return a template string.`;
233
+ }
234
+ const pathParams = [];
235
+ let normalized = body.head.text;
236
+ for (const span of body.templateSpans) {
237
+ const expression = span.expression;
238
+ if (!ts.isPropertyAccessExpression(expression) ||
239
+ !ts.isIdentifier(expression.expression) ||
240
+ expression.expression.text !== paramName) {
241
+ return `The route \`${text}\` reads something other than its path params.`;
242
+ }
243
+ pathParams.push(expression.name.text);
244
+ normalized += `{}${span.literal.text}`;
245
+ }
246
+ return { text, normalized, pathParams, staticPrefix: body.head.text };
247
+ };
248
+ const parseServiceMethod = (method, sourceFile) => {
249
+ const statements = method.body?.statements ?? [];
250
+ const statement = statements[0];
251
+ const call = statement && ts.isReturnStatement(statement) ? statement.expression : undefined;
252
+ const paramName = method.parameters.length === 1 && ts.isIdentifier(method.parameters[0].name)
253
+ ? method.parameters[0].name.text
254
+ : null;
255
+ if (statements.length !== 1 ||
256
+ !call ||
257
+ !ts.isCallExpression(call) ||
258
+ !ts.isPropertyAccessExpression(call.expression) ||
259
+ call.expression.expression.kind !== ts.SyntaxKind.ThisKeyword ||
260
+ !HTTP_VERBS.includes(call.expression.name.text)) {
261
+ return 'The method is not a single `return this.get|post|put|patch|delete({ … })`.';
262
+ }
263
+ const options = call.arguments[0];
264
+ if (!options || !ts.isObjectLiteralExpression(options)) {
265
+ return 'The request options are not an object literal.';
266
+ }
267
+ let route = 'The method has no `apiRoute`.';
268
+ let headers = false;
269
+ let cacheExpiry = false;
270
+ for (const property of options.properties) {
271
+ if (!ts.isPropertyAssignment(property))
272
+ return `Unsupported option \`${property.getText(sourceFile)}\`.`;
273
+ const name = property.name.getText(sourceFile);
274
+ const value = property.initializer;
275
+ if (name === 'apiRoute') {
276
+ route = parseRoute(value, sourceFile);
277
+ }
278
+ else if (name === 'responseType') {
279
+ continue;
280
+ }
281
+ else if (name === 'httpOpts') {
282
+ if (ts.isIdentifier(value) && value.text === paramName)
283
+ continue;
284
+ const spreadsArgs = ts.isObjectLiteralExpression(value) &&
285
+ value.properties.every((element) => (ts.isSpreadAssignment(element) &&
286
+ ts.isIdentifier(element.expression) &&
287
+ element.expression.text === paramName) ||
288
+ (ts.isPropertyAssignment(element) && element.name.getText(sourceFile) === 'headers'));
289
+ if (!spreadsArgs)
290
+ return `Unsupported \`httpOpts\`: \`${value.getText(sourceFile)}\`.`;
291
+ headers = true;
292
+ }
293
+ else if (name === 'extras') {
294
+ if (!ts.isObjectLiteralExpression(value))
295
+ return `Unsupported \`extras\`: \`${value.getText(sourceFile)}\`.`;
296
+ for (const extra of value.properties) {
297
+ const extraName = extra.name?.getText(sourceFile);
298
+ if (extraName === 'skipCache')
299
+ continue;
300
+ if (extraName === 'cacheExpiresIn') {
301
+ cacheExpiry = true;
302
+ continue;
303
+ }
304
+ if (extraName === 'apiBaseOverride')
305
+ return 'apiBaseOverride';
306
+ return `Unsupported extra \`${extra.getText(sourceFile)}\`.`;
307
+ }
308
+ }
309
+ else {
310
+ return `Unsupported option \`${name}\`.`;
311
+ }
312
+ }
313
+ if (typeof route === 'string')
314
+ return route;
315
+ return { route, verb: call.expression.name.text, headers, cacheExpiry };
316
+ };
317
+ const parseServiceFile = (sourceFile) => {
318
+ const methods = new Map();
319
+ let apiBase = null;
320
+ let baseConfig = false;
321
+ const findSuper = (node) => {
322
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.SuperKeyword) {
323
+ apiBase = node.arguments[1]?.getText(sourceFile) ?? null;
324
+ baseConfig = node.arguments.length > 2;
325
+ }
326
+ ts.forEachChild(node, findSuper);
327
+ };
328
+ for (const statement of sourceFile.statements) {
329
+ if (!ts.isClassDeclaration(statement))
330
+ continue;
331
+ for (const member of statement.members) {
332
+ if (ts.isConstructorDeclaration(member))
333
+ findSuper(member);
334
+ if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
335
+ methods.set(member.name.text, parseServiceMethod(member, sourceFile));
336
+ }
337
+ }
338
+ }
339
+ return { apiBase, baseConfig, methods };
340
+ };
341
+ const hasToolkitImport = (sourceFile) => sourceFile.statements.some((statement) => ts.isImportDeclaration(statement) &&
342
+ ts.isStringLiteral(statement.moduleSpecifier) &&
343
+ statement.moduleSpecifier.text === TOOLKIT_MODULE);
344
+ const extendsFacadeBase = (sourceFile) => sourceFile.statements.some((statement) => ts.isClassDeclaration(statement) &&
345
+ !!statement.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText(sourceFile) === 'FacadeBase')));
346
+ /** Finds every `x.actions.ts` built with `createHttpActionGroup` and reads the feature around it. */
347
+ export const discoverFeatures = (tree, graph, visit) => {
348
+ const features = [];
349
+ const facadesWithoutHttp = [];
350
+ const commentedOut = [];
351
+ visit((filePath) => {
352
+ if (!filePath.endsWith(FEATURE_FILE_SUFFIX))
353
+ return;
354
+ const content = tree.read(filePath, 'utf-8');
355
+ if (!content)
356
+ return;
357
+ const sourceFile = createSourceFile(content, filePath);
358
+ const dir = filePath.slice(0, filePath.lastIndexOf('/'));
359
+ const base = filePath.slice(dir.length + 1, -FEATURE_FILE_SUFFIX.length);
360
+ const parsed = parseActionsFile(sourceFile);
361
+ if (parsed.groups.length === 0) {
362
+ const facade = siblingFile(tree, dir, base, 'facade');
363
+ const facadeContent = facade ? tree.read(facade, 'utf-8') : null;
364
+ if (facade && facadeContent && extendsFacadeBase(createSourceFile(facadeContent, facade))) {
365
+ facadesWithoutHttp.push(facade);
366
+ }
367
+ else if (/createHttpActionGroup/.test(content) && !hasToolkitImport(sourceFile)) {
368
+ commentedOut.push(filePath);
369
+ }
370
+ return;
371
+ }
372
+ features.push(analyzeFeature(tree, graph, { filePath, sourceFile, dir, base, parsed }));
373
+ });
374
+ return { features, facadesWithoutHttp, commentedOut };
375
+ };
376
+ const analyzeFeature = (tree, graph, input) => {
377
+ const { filePath, sourceFile, dir, base, parsed } = input;
378
+ const files = {
379
+ actions: filePath,
380
+ service: siblingFile(tree, dir, base, 'service'),
381
+ effects: siblingFile(tree, dir, base, 'effects'),
382
+ reducer: siblingFile(tree, dir, base, 'reducer'),
383
+ selectors: siblingFile(tree, dir, base, 'selectors'),
384
+ facade: siblingFile(tree, dir, base, 'facade'),
385
+ models: siblingFile(tree, dir, base, 'models'),
386
+ };
387
+ const blockers = [];
388
+ const warnings = [];
389
+ const calls = [];
390
+ for (const statement of parsed.foreign) {
391
+ blockers.push({
392
+ id: TOOLKIT_TASK.MIXED_ACTIONS,
393
+ summary: `\`${statement.getText(sourceFile).split('\n')[0]}\` is not an HTTP action group.`,
394
+ locations: [{ filePath, line: lineOf(sourceFile, statement.getStart(sourceFile)) }],
395
+ });
396
+ }
397
+ const readSource = (path) => {
398
+ const content = path ? tree.read(path, 'utf-8') : null;
399
+ return path && content ? createSourceFile(content, path) : null;
400
+ };
401
+ const effectsSource = readSource(files.effects);
402
+ const effects = effectsSource ? parseEffectsFile(effectsSource) : { serviceMethods: new Map(), foreign: [] };
403
+ for (const member of effects.foreign) {
404
+ blockers.push({
405
+ id: TOOLKIT_TASK.CUSTOM_EFFECT,
406
+ summary: `\`${member.getText(effectsSource).split('\n')[0]}\` is not a plain \`onAction*Map\` effect.`,
407
+ locations: [{ filePath: files.effects, line: lineOf(effectsSource, member.getStart(effectsSource)) }],
408
+ });
409
+ }
410
+ const reducerContent = files.reducer ? tree.read(files.reducer, 'utf-8') : null;
411
+ if (files.reducer && reducerContent && !isReducerPlainSlice(reducerContent)) {
412
+ blockers.push({
413
+ id: TOOLKIT_TASK.CUSTOM_REDUCER,
414
+ summary: `The reducer of \`${base}\` is not a plain \`createReducerSlice\` with an empty \`initialStateExtra\`.`,
415
+ locations: [{ filePath: files.reducer }],
416
+ });
417
+ }
418
+ const serviceSource = readSource(files.service);
419
+ const service = serviceSource ? parseServiceFile(serviceSource) : null;
420
+ if (service?.baseConfig) {
421
+ blockers.push({
422
+ id: TOOLKIT_TASK.SERVICE_API_BASE,
423
+ summary: 'The service passes a base config to `ServiceBase`.',
424
+ locations: [{ filePath: files.service }],
425
+ });
426
+ }
427
+ for (const group of parsed.groups) {
428
+ const location = { filePath, line: group.line };
429
+ const serviceMethod = effects.serviceMethods.get(group.name);
430
+ if (!serviceMethod) {
431
+ blockers.push({
432
+ id: TOOLKIT_TASK.ACTION_WITHOUT_EFFECT,
433
+ summary: `\`${group.name}\` has no effect in ${files.effects ?? `${base}.effects.ts`}.`,
434
+ locations: [location],
435
+ });
436
+ continue;
437
+ }
438
+ const method = service?.methods.get(serviceMethod);
439
+ if (!method || typeof method === 'string') {
440
+ const apiBaseOverride = method === 'apiBaseOverride';
441
+ blockers.push({
442
+ id: apiBaseOverride ? TOOLKIT_TASK.SERVICE_API_BASE : TOOLKIT_TASK.SERVICE_UNSUPPORTED,
443
+ summary: apiBaseOverride
444
+ ? `\`${serviceMethod}\` overrides the API base.`
445
+ : `\`${serviceMethod}\`: ${method ?? 'the service has no such method.'}`,
446
+ locations: [{ filePath: files.service ?? filePath }],
447
+ });
448
+ continue;
449
+ }
450
+ if (method.headers) {
451
+ warnings.push({
452
+ id: TOOLKIT_TASK.SERVICE_HEADERS,
453
+ summary: `\`${serviceMethod}\` added headers to the request; the creator \`${group.name}\` does not.`,
454
+ locations: [{ filePath: files.service }],
455
+ });
456
+ }
457
+ if (method.cacheExpiry) {
458
+ warnings.push({
459
+ id: TOOLKIT_TASK.SERVICE_CACHE_EXPIRY,
460
+ summary: `\`${serviceMethod}\` cached its response with \`cacheExpiresIn\`.`,
461
+ locations: [{ filePath: files.service }],
462
+ });
463
+ }
464
+ let args = null;
465
+ if (group.argsType && !isNullType(group.argsType)) {
466
+ const resolution = resolveArgs(tree, graph, filePath, sourceFile, group.argsType);
467
+ if ('unresolved' in resolution) {
468
+ blockers.push({ id: TOOLKIT_TASK.ARGS_UNRESOLVED, summary: resolution.unresolved, locations: [location] });
469
+ continue;
470
+ }
471
+ if (resolution.inherited) {
472
+ warnings.push({
473
+ id: TOOLKIT_TASK.ARGS_INHERITED,
474
+ summary: `\`${resolution.args.typeText}\` (args of \`${group.name}\`) extends another type.`,
475
+ locations: [location],
476
+ });
477
+ }
478
+ args = resolution.args;
479
+ }
480
+ if (method.route.pathParams.length > 0 && !args?.members.has('queryParams')) {
481
+ blockers.push({
482
+ id: TOOLKIT_TASK.ARGS_UNRESOLVED,
483
+ summary: `The route of \`${group.name}\` has path params, but its args have no \`queryParams\`.`,
484
+ locations: [location],
485
+ });
486
+ continue;
487
+ }
488
+ calls.push({
489
+ name: group.name,
490
+ verb: method.verb,
491
+ args,
492
+ responseType: group.responseType ? group.responseType.getText(sourceFile) : null,
493
+ route: method.route,
494
+ serviceMethod,
495
+ line: group.line,
496
+ });
497
+ }
498
+ const actionImports = parsed.imports
499
+ .filter((statement) => !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== TOOLKIT_MODULE)
500
+ .map((statement) => statement.getText(sourceFile));
501
+ return {
502
+ dir,
503
+ base,
504
+ files,
505
+ queriesFile: `${dir}/${base}.queries.ts`,
506
+ calls,
507
+ actionImports,
508
+ apiBase: service?.apiBase ?? null,
509
+ blockers,
510
+ warnings,
511
+ };
512
+ };
513
+ //# sourceMappingURL=feature.js.map
@@ -0,0 +1,140 @@
1
+ import { formatFiles } from '@nx/devkit';
2
+ import { createMigrationScope } from '../migrate-to-query-v3/migration-scope.js';
3
+ import { createModuleGraph } from '../migrate-to-query-v3/module-graph.js';
4
+ import { clientBaseName, helperName, indexExistingCreators, isPublicRoute, writeQueriesFile, } from './creators.js';
5
+ import { migrateToolkitConsumers } from './consumers.js';
6
+ import { rewriteFacade } from './facade.js';
7
+ import { discoverFeatures } from './feature.js';
8
+ import { TOOLKIT_TASK, ToolkitMigrationReport } from './report.js';
9
+ import { removeFeatureStores } from './store-removal.js';
10
+ const parsePublicRoutes = (value) => {
11
+ if (value === undefined)
12
+ return null;
13
+ const routes = (Array.isArray(value) ? value : value.split(','))
14
+ .map((route) => route.trim())
15
+ .filter((route) => route.length > 0);
16
+ return routes.length > 0 ? routes : null;
17
+ };
18
+ const assertHelpersExist = (tree, graph, helpers, names) => {
19
+ const isFile = tree.exists(helpers.importFrom);
20
+ const fromFile = isFile ? helpers.importFrom : '';
21
+ const specifier = isFile ? `./${helpers.importFrom.split('/').pop()}` : helpers.importFrom;
22
+ if (!graph.resolveFile(fromFile, specifier))
23
+ return;
24
+ const missing = [...names].filter((name) => !graph.findDeclaringFile(fromFile, specifier, name));
25
+ if (missing.length > 0) {
26
+ throw new Error(`${helpers.importFrom} does not export ${missing.join(', ')}. Run \`@ethlete/query:migrate-to-query-v3\` first, or check --client and --clientImport.`);
27
+ }
28
+ };
29
+ const resolveApiBase = (features, option) => {
30
+ if (option)
31
+ return option;
32
+ const bases = [...new Set(features.map((feature) => feature.apiBase).filter((base) => !!base))];
33
+ if (bases.length > 1) {
34
+ throw new Error(`The toolkit services use ${bases.length} API bases: ${bases.join(', ')}. Pass the one --client points at with --serviceApiBase; features on the others are left alone.`);
35
+ }
36
+ return bases[0] ?? null;
37
+ };
38
+ /** Runs the migration on `tree` - store side, then consumers - and records follow-up tasks in `report`. */
39
+ export const migrateToolkitStores = (tree, options, report) => {
40
+ if (!options.client || !options.clientImport) {
41
+ throw new Error('Pass --client (e.g. apiClient) and --clientImport (e.g. @app/queries).');
42
+ }
43
+ const scope = createMigrationScope(tree, options);
44
+ const graph = createModuleGraph(tree);
45
+ const inScope = new Set();
46
+ scope.visit(tree, (filePath) => {
47
+ if (filePath.endsWith('.ts'))
48
+ inScope.add(filePath);
49
+ });
50
+ const visit = (callback) => inScope.forEach(callback);
51
+ const helpers = {
52
+ baseName: clientBaseName(options.client),
53
+ importFrom: options.clientImport,
54
+ publicRoutes: parsePublicRoutes(options.publicRoutes),
55
+ };
56
+ const discovered = discoverFeatures(tree, graph, visit);
57
+ discovered.commentedOut.forEach((filePath) => report.add({
58
+ id: TOOLKIT_TASK.COMMENTED_FEATURE,
59
+ summary: `${filePath} only contains commented-out action groups.`,
60
+ locations: [{ filePath }],
61
+ }));
62
+ discovered.facadesWithoutHttp.forEach((filePath) => report.add({
63
+ id: TOOLKIT_TASK.FACADE_WITHOUT_HTTP,
64
+ summary: `${filePath} extends \`FacadeBase\` without action groups.`,
65
+ locations: [{ filePath }],
66
+ }));
67
+ const apiBase = resolveApiBase(discovered.features, options.serviceApiBase);
68
+ for (const feature of discovered.features) {
69
+ if (feature.apiBase && apiBase && feature.apiBase !== apiBase) {
70
+ feature.blockers.push({
71
+ id: TOOLKIT_TASK.SERVICE_API_BASE,
72
+ summary: `The service uses \`${feature.apiBase}\`, not \`${apiBase}\`.`,
73
+ locations: [{ filePath: feature.files.service }],
74
+ });
75
+ }
76
+ }
77
+ const neededHelpers = new Set(discovered.features
78
+ .filter((feature) => feature.blockers.length === 0)
79
+ .flatMap((feature) => feature.calls.map((call) => helperName(helpers, call.verb, !isPublicRoute(helpers, call.route.staticPrefix)))));
80
+ assertHelpersExist(tree, graph, helpers, neededHelpers);
81
+ const existing = indexExistingCreators(tree, graph, helpers, visit);
82
+ const ready = [];
83
+ let creatorsWritten = 0;
84
+ let creatorsReused = 0;
85
+ for (const feature of discovered.features) {
86
+ if (feature.blockers.length > 0) {
87
+ feature.blockers.forEach((blocker) => report.add(blocker));
88
+ continue;
89
+ }
90
+ const storeFiles = new Set([
91
+ feature.files.actions,
92
+ feature.files.reducer,
93
+ feature.files.effects,
94
+ feature.files.selectors,
95
+ feature.files.service,
96
+ ].filter((file) => !!file));
97
+ let facadeContent = null;
98
+ if (feature.files.facade) {
99
+ const rewrite = rewriteFacade(tree.read(feature.files.facade, 'utf-8'), feature, graph, storeFiles);
100
+ if ('blockers' in rewrite) {
101
+ rewrite.blockers.forEach((blocker) => report.add(blocker));
102
+ continue;
103
+ }
104
+ facadeContent = rewrite.content;
105
+ }
106
+ const written = writeQueriesFile(tree, graph, feature, helpers, existing, report);
107
+ creatorsWritten += written.written;
108
+ creatorsReused += written.reused;
109
+ if (feature.files.facade && facadeContent)
110
+ tree.write(feature.files.facade, facadeContent);
111
+ feature.warnings.forEach((warning) => report.add(warning));
112
+ ready.push(feature);
113
+ }
114
+ const removal = removeFeatureStores(tree, graph, inScope, ready, report);
115
+ const migratedFacades = new Set(ready.map((feature) => feature.files.facade).filter((file) => !!file));
116
+ const consumers = migrateToolkitConsumers(tree, createModuleGraph(tree), inScope, report, migratedFacades);
117
+ return {
118
+ featuresFound: discovered.features.length,
119
+ featuresConverted: ready.length,
120
+ featuresSkipped: discovered.features.length - ready.length,
121
+ storeFilesKept: removal.keptFeatures.length,
122
+ creatorsWritten,
123
+ creatorsReused,
124
+ deletedFiles: removal.deletedFiles,
125
+ consumers,
126
+ };
127
+ };
128
+ export default async function migrate(tree, schema) {
129
+ console.log('\nšŸ”„ Migrating @tomtomb/ngrx-toolkit stores to @ethlete/query...');
130
+ const report = new ToolkitMigrationReport();
131
+ const stats = migrateToolkitStores(tree, schema, report);
132
+ 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.`);
134
+ report.writeToTree(tree);
135
+ report.printSummary();
136
+ if (!schema.skipFormat) {
137
+ await formatFiles(tree);
138
+ }
139
+ }
140
+ //# sourceMappingURL=migration.js.map