@ethlete/query 6.0.0-next.6 → 6.0.0-next.60

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 (54) hide show
  1. package/CHANGELOG.md +939 -0
  2. package/fesm2022/ethlete-query-devtools-contract.mjs +1 -0
  3. package/fesm2022/ethlete-query-ngrx-toolkit.mjs +411 -0
  4. package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
  5. package/fesm2022/ethlete-query-testing.mjs +548 -169
  6. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  7. package/fesm2022/ethlete-query.mjs +14680 -7172
  8. package/fesm2022/ethlete-query.mjs.map +1 -1
  9. package/generators/deprecate-legacy-queries/migration.js +85 -0
  10. package/generators/deprecate-legacy-queries/schema.json +26 -0
  11. package/generators/generators.json +30 -0
  12. package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
  13. package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
  14. package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
  15. package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
  16. package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
  17. package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
  18. package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
  19. package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
  20. package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
  21. package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
  22. package/generators/migrate-query-client-features/migration.js +144 -0
  23. package/generators/migrate-query-client-features/report.js +136 -0
  24. package/generators/migrate-query-client-features/schema.json +26 -0
  25. package/generators/migrate-query-opt-in-features/migration.js +195 -0
  26. package/generators/migrate-query-opt-in-features/report.js +143 -0
  27. package/generators/migrate-query-opt-in-features/schema.json +31 -0
  28. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +160 -0
  29. package/generators/migrate-to-query-v3/cleanup-migration.js +247 -125
  30. package/generators/migrate-to-query-v3/http-client-check.js +93 -0
  31. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
  32. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +274 -152
  33. package/generators/migrate-to-query-v3/migration-scope.js +40 -0
  34. package/generators/migrate-to-query-v3/migration.js +19 -10
  35. package/generators/migrate-to-query-v3/module-graph.js +165 -0
  36. package/generators/migrate-to-query-v3/query-client-migration.js +342 -160
  37. package/generators/migrate-to-query-v3/rename-symbols.js +233 -0
  38. package/generators/migrate-to-query-v3/report.js +40 -7
  39. package/generators/migrate-to-query-v3/schema.json +12 -0
  40. package/generators/migrate-to-query-v3/shared.js +97 -3
  41. package/generators/move-devtools-contract-imports/migration.js +155 -0
  42. package/generators/move-devtools-contract-imports/moved-names.js +153 -0
  43. package/generators/move-devtools-contract-imports/schema.json +26 -0
  44. package/generators/prep-for-query-v3/migration.js +465 -65
  45. package/generators/report-legacy-query-apis/migration.js +256 -0
  46. package/generators/report-legacy-query-apis/schema.json +25 -0
  47. package/migrations/query-field-default-types.md +43 -0
  48. package/migrations/search-query-field-string.md +43 -0
  49. package/migrations.json +87 -0
  50. package/package.json +34 -14
  51. package/types/ethlete-query-devtools-contract.d.ts +2 -0
  52. package/types/ethlete-query-ngrx-toolkit.d.ts +131 -0
  53. package/types/ethlete-query-testing.d.ts +295 -160
  54. package/types/ethlete-query.d.ts +9755 -3682
@@ -0,0 +1,521 @@
1
+ import * as ts from 'typescript';
2
+ import { createSourceFile, ensureNamedImports } from '../migrate-to-query-v3/shared.js';
3
+ import { INTEROP_MODULE } from './facade.js';
4
+ import { TOOLKIT_MODULE } from './feature.js';
5
+ import { applyEdits, collectUsedIdentifiers, lineOf, removeStatement } from './ts-edits.js';
6
+ export const CONSUMER_TASK = {
7
+ UNSUPPORTED_IMPORT: 'NTK-UNSUPPORTED-IMPORT',
8
+ UNCONVERTED_ACTION_GROUP: 'NTK-UNCONVERTED-ACTION-GROUP',
9
+ SELECT_BY_ACTION_ID: 'NTK-SELECT-BY-ACTION-ID',
10
+ AUTH_INTERCEPTOR: 'NTK-AUTH-INTERCEPTOR',
11
+ FACADE_ON: 'NTK-FACADE-ON',
12
+ };
13
+ const defineConsumerTasks = (report) => {
14
+ report.define(CONSUMER_TASK.UNSUPPORTED_IMPORT, {
15
+ title: 'Toolkit symbol without an interop counterpart',
16
+ action: '`@ethlete/query/ngrx-toolkit` does not export the listed symbols, so the file still imports them from `@tomtomb/ngrx-toolkit`. Rewrite the code that uses them, then drop the import.',
17
+ });
18
+ report.define(CONSUMER_TASK.UNCONVERTED_ACTION_GROUP, {
19
+ title: 'Consumer types name an action group that is still on the toolkit',
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.',
21
+ });
22
+ report.define(CONSUMER_TASK.SELECT_BY_ACTION_ID, {
23
+ title: 'Handle looked up by action id',
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.',
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
+ });
30
+ report.define(CONSUMER_TASK.AUTH_INTERCEPTOR, {
31
+ title: 'HTTP interceptor attaches the bearer token',
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.',
33
+ });
34
+ };
35
+ const INTEROP_SYMBOLS = new Set([
36
+ 'toolkitCall',
37
+ 'toolkitSelect',
38
+ 'MappedEntityState',
39
+ 'ActionCallArgs',
40
+ 'ToolkitError',
41
+ 'CallState',
42
+ 'SuspensePipe',
43
+ 'SuspenseMultiPipe',
44
+ 'NgRxToolkitModule',
45
+ 'joinLoading',
46
+ 'joinErrors',
47
+ ]);
48
+ const RENAMED_SYMBOLS = new Map([['Error', 'ToolkitError']]);
49
+ const STORE_SIDE_SYMBOLS = new Set([
50
+ 'FacadeBase',
51
+ 'EffectBase',
52
+ 'ServiceBase',
53
+ 'createHttpActionGroup',
54
+ 'createReducerSlice',
55
+ 'createEntitySelectors',
56
+ 'defineArgTypes',
57
+ 'defineResponseType',
58
+ ]);
59
+ const HANDLE_TYPES = new Set(['MappedEntityState', 'ActionCallArgs']);
60
+ const code = (text) => (text.includes('`') ? `\`\` ${text} \`\`` : `\`${text}\``);
61
+ const toolkitImportsOf = (sourceFile) => sourceFile.statements.filter((statement) => ts.isImportDeclaration(statement) &&
62
+ ts.isStringLiteral(statement.moduleSpecifier) &&
63
+ statement.moduleSpecifier.text === TOOLKIT_MODULE);
64
+ const elementsOf = (declaration) => {
65
+ const bindings = declaration.importClause?.namedBindings;
66
+ if (!bindings || !ts.isNamedImports(bindings))
67
+ return [];
68
+ return bindings.elements.map((element) => ({
69
+ imported: element.propertyName?.text ?? element.name.text,
70
+ local: element.name.text,
71
+ typeOnly: element.isTypeOnly || !!declaration.importClause?.isTypeOnly,
72
+ }));
73
+ };
74
+ const namedImportOf = (sourceFile, local) => {
75
+ for (const statement of sourceFile.statements) {
76
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
77
+ continue;
78
+ const bindings = statement.importClause?.namedBindings;
79
+ if (bindings && ts.isNamespaceImport(bindings) && bindings.name.text === local) {
80
+ return { statement, specifier: statement.moduleSpecifier.text, namespace: true, imported: local };
81
+ }
82
+ if (bindings && ts.isNamedImports(bindings)) {
83
+ const element = bindings.elements.find((candidate) => candidate.name.text === local);
84
+ if (element) {
85
+ return {
86
+ statement,
87
+ specifier: statement.moduleSpecifier.text,
88
+ namespace: false,
89
+ imported: element.propertyName?.text ?? local,
90
+ };
91
+ }
92
+ }
93
+ }
94
+ return null;
95
+ };
96
+ const isToolkitActionsFile = (tree, filePath) => !!filePath && filePath.endsWith('.actions.ts') && !!tree.read(filePath, 'utf-8')?.includes('createHttpActionGroup');
97
+ const unconvertedTypeQueries = (tree, graph, filePath, sourceFile, handleTypeNames) => {
98
+ const unconverted = [];
99
+ const resolveQuery = (query) => {
100
+ const name = query.exprName;
101
+ const local = ts.isIdentifier(name) ? name.text : ts.isIdentifier(name.left) ? name.left.text : null;
102
+ const origin = local ? namedImportOf(sourceFile, local) : null;
103
+ if (!origin || origin.namespace === ts.isIdentifier(name))
104
+ return;
105
+ const declaringFile = origin.namespace
106
+ ? graph.resolveFile(filePath, origin.specifier)
107
+ : graph.findDeclaringFile(filePath, origin.specifier, origin.imported);
108
+ if (isToolkitActionsFile(tree, declaringFile))
109
+ unconverted.push(name.getText(sourceFile));
110
+ };
111
+ const visit = (node) => {
112
+ if (ts.isTypeReferenceNode(node) &&
113
+ ts.isIdentifier(node.typeName) &&
114
+ handleTypeNames.has(node.typeName.text) &&
115
+ node.typeArguments) {
116
+ const visitArgument = (inner) => {
117
+ if (ts.isTypeQueryNode(inner))
118
+ resolveQuery(inner);
119
+ ts.forEachChild(inner, visitArgument);
120
+ };
121
+ node.typeArguments.forEach(visitArgument);
122
+ }
123
+ ts.forEachChild(node, visit);
124
+ };
125
+ visit(sourceFile);
126
+ return unconverted;
127
+ };
128
+ const isCall = (node, name) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name;
129
+ const readActionId = (node, createActionId) => {
130
+ if (!node || !isCall(node, createActionId) || node.arguments.length !== 1)
131
+ return null;
132
+ const inner = node.arguments[0];
133
+ if (!ts.isCallExpression(inner) ||
134
+ !ts.isPropertyAccessExpression(inner.expression) ||
135
+ !ts.isIdentifier(inner.expression.expression) ||
136
+ inner.expression.name.text !== 'call' ||
137
+ inner.arguments.length !== 1) {
138
+ return null;
139
+ }
140
+ const options = inner.arguments[0];
141
+ if (!ts.isObjectLiteralExpression(options) || options.properties.length !== 1)
142
+ return null;
143
+ const property = options.properties[0];
144
+ if (!ts.isPropertyAssignment(property) || property.name.getText() !== 'args')
145
+ return null;
146
+ return { group: inner.expression.expression.text, args: property.initializer };
147
+ };
148
+ const selectArgsText = (args, sourceFile) => {
149
+ if (!ts.isObjectLiteralExpression(args))
150
+ return args.getText(sourceFile);
151
+ const kept = args.properties.filter((property) => property.name?.getText(sourceFile) !== 'skipCache');
152
+ if (kept.length === args.properties.length)
153
+ return args.getText(sourceFile);
154
+ return `{ ${kept.map((property) => property.getText(sourceFile)).join(', ')} }`;
155
+ };
156
+ const isFacadeSelect = (node) => ts.isCallExpression(node) &&
157
+ ts.isPropertyAccessExpression(node.expression) &&
158
+ node.expression.name.text === 'select' &&
159
+ node.arguments.length === 2 &&
160
+ ts.isIdentifier(node.arguments[0]) &&
161
+ /facade/i.test(node.expression.expression.getText()) &&
162
+ !/router/i.test(node.expression.expression.getText());
163
+ const countIdentifier = (root, name) => {
164
+ let count = 0;
165
+ const visit = (node) => {
166
+ if (ts.isIdentifier(node) && node.text === name)
167
+ count += 1;
168
+ ts.forEachChild(node, visit);
169
+ };
170
+ visit(root);
171
+ return count;
172
+ };
173
+ const injectorOf = (classNode) => {
174
+ for (const member of classNode.members) {
175
+ if (ts.isPropertyDeclaration(member) &&
176
+ ts.isIdentifier(member.name) &&
177
+ member.initializer &&
178
+ isCall(member.initializer, 'inject') &&
179
+ member.initializer.arguments[0]?.getText() === 'Injector') {
180
+ return member.name.text;
181
+ }
182
+ if (ts.isConstructorDeclaration(member)) {
183
+ const parameter = member.parameters.find((candidate) => candidate.type?.getText() === 'Injector' &&
184
+ ts.isIdentifier(candidate.name) &&
185
+ !!ts
186
+ .getModifiers(candidate)
187
+ ?.some((modifier) => [ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.PublicKeyword].includes(modifier.kind)));
188
+ if (parameter)
189
+ return parameter.name.text;
190
+ }
191
+ }
192
+ return null;
193
+ };
194
+ const planRefreshSites = (filePath, sourceFile, content, createActionId) => {
195
+ const plan = { edits: [], rewritten: 0, needsInjectorImport: false, tasks: [] };
196
+ const consumedActionIds = new Set();
197
+ const classInjectors = new Map();
198
+ const task = (node, summary) => plan.tasks.push({
199
+ id: CONSUMER_TASK.SELECT_BY_ACTION_ID,
200
+ summary,
201
+ locations: [{ filePath, line: lineOf(sourceFile, node.getStart(sourceFile)) }],
202
+ });
203
+ const injectorFor = (node) => {
204
+ let current = node.parent;
205
+ while (current && !ts.isClassLike(current))
206
+ current = current.parent;
207
+ if (!current)
208
+ return null;
209
+ const classNode = current;
210
+ const known = classInjectors.get(classNode) ?? injectorOf(classNode);
211
+ if (known) {
212
+ classInjectors.set(classNode, known);
213
+ return known;
214
+ }
215
+ const taken = classNode.members.some((member) => member.name?.getText(sourceFile) === 'injector');
216
+ const name = taken ? 'toolkitInjector' : 'injector';
217
+ classInjectors.set(classNode, name);
218
+ plan.needsInjectorImport = true;
219
+ plan.edits.push({
220
+ start: classNode.members.pos,
221
+ end: classNode.members.pos,
222
+ text: `\n private ${name} = inject(Injector);\n`,
223
+ });
224
+ return name;
225
+ };
226
+ const localActionId = (select, identifier) => {
227
+ let block = select.parent;
228
+ while (block && !ts.isBlock(block))
229
+ block = block.parent;
230
+ if (!block || !ts.isBlock(block) || countIdentifier(block, identifier.text) !== 2)
231
+ return null;
232
+ for (const statement of block.statements) {
233
+ if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1)
234
+ continue;
235
+ const declaration = statement.declarationList.declarations[0];
236
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === identifier.text) {
237
+ const actionId = createActionId ? readActionId(declaration.initializer, createActionId) : null;
238
+ return actionId ? { ...actionId, statement, initializer: declaration.initializer } : null;
239
+ }
240
+ }
241
+ return null;
242
+ };
243
+ const visit = (node) => {
244
+ if (isFacadeSelect(node)) {
245
+ const group = node.arguments[0].text;
246
+ const idArgument = node.arguments[1];
247
+ const inline = createActionId ? readActionId(idArgument, createActionId) : null;
248
+ const local = !inline && ts.isIdentifier(idArgument) ? localActionId(node, idArgument) : null;
249
+ const source = inline ?? local;
250
+ if (source && source.group === group) {
251
+ const injector = injectorFor(node);
252
+ if (injector) {
253
+ plan.edits.push({
254
+ start: node.getStart(sourceFile),
255
+ end: node.getEnd(),
256
+ text: `toolkitSelect(${group}, ${selectArgsText(source.args, sourceFile)}, { injector: this.${injector} })`,
257
+ });
258
+ consumedActionIds.add(inline ? idArgument : local.initializer);
259
+ if (local)
260
+ plan.edits.push(removeStatement(local.statement, content));
261
+ plan.rewritten += 1;
262
+ return;
263
+ }
264
+ }
265
+ task(node, `${code(node.getText(sourceFile).replace(/\s+/g, ' '))} looks up a handle by an action id the generator cannot trace to its args.`);
266
+ return;
267
+ }
268
+ ts.forEachChild(node, visit);
269
+ };
270
+ const visitActionIds = (node) => {
271
+ if (createActionId && isCall(node, createActionId) && !consumedActionIds.has(node)) {
272
+ task(node, `\`${createActionId}(…)\` builds an action id; pass the args on and call \`toolkitSelect\` where the handle is looked up.`);
273
+ }
274
+ ts.forEachChild(node, visitActionIds);
275
+ };
276
+ visit(sourceFile);
277
+ visitActionIds(sourceFile);
278
+ return plan;
279
+ };
280
+ /** Moves one consumer file from `@tomtomb/ngrx-toolkit` to `@ethlete/query/ngrx-toolkit`. */
281
+ export const rewriteConsumer = (tree, graph, filePath, content) => {
282
+ const sourceFile = createSourceFile(content, filePath);
283
+ const toolkitImports = toolkitImportsOf(sourceFile);
284
+ if (toolkitImports.length === 0)
285
+ return null;
286
+ const elements = toolkitImports.flatMap(elementsOf);
287
+ const isInterop = (element) => INTEROP_SYMBOLS.has(RENAMED_SYMBOLS.get(element.imported) ?? element.imported);
288
+ const hasStoreSide = elements.some((element) => STORE_SIDE_SYMBOLS.has(element.imported));
289
+ if (hasStoreSide && !elements.some(isInterop))
290
+ return null;
291
+ const handleTypeNames = new Set(elements.filter((element) => HANDLE_TYPES.has(element.imported)).map((element) => element.local));
292
+ const unconverted = unconvertedTypeQueries(tree, graph, filePath, sourceFile, handleTypeNames);
293
+ if (unconverted.length > 0) {
294
+ if (hasStoreSide)
295
+ return null;
296
+ return {
297
+ content,
298
+ rewritten: false,
299
+ leftOnToolkit: true,
300
+ 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
+ ],
308
+ };
309
+ }
310
+ const createActionId = elements.find((element) => element.imported === 'createActionId')?.local ?? null;
311
+ const refresh = planRefreshSites(filePath, sourceFile, content, createActionId);
312
+ let result = applyEdits(content, refresh.edits);
313
+ const rewrittenSource = createSourceFile(result, filePath);
314
+ const used = collectUsedIdentifiers(rewrittenSource);
315
+ const moved = [];
316
+ const kept = [];
317
+ for (const element of elements) {
318
+ if (!used.has(element.local))
319
+ continue;
320
+ const target = RENAMED_SYMBOLS.get(element.imported) ?? element.imported;
321
+ const binding = target === element.local ? target : `${target} as ${element.local}`;
322
+ if (INTEROP_SYMBOLS.has(target)) {
323
+ moved.push(binding);
324
+ }
325
+ else {
326
+ kept.push(element.imported === element.local ? element.local : `${element.imported} as ${element.local}`);
327
+ }
328
+ }
329
+ if (refresh.rewritten > 0)
330
+ moved.push('toolkitSelect');
331
+ const hasInteropImport = rewrittenSource.statements.some((statement) => ts.isImportDeclaration(statement) &&
332
+ ts.isStringLiteral(statement.moduleSpecifier) &&
333
+ statement.moduleSpecifier.text === INTEROP_MODULE);
334
+ const replacement = [
335
+ moved.length > 0 && !hasInteropImport ? `import { ${moved.join(', ')} } from '${INTEROP_MODULE}';` : null,
336
+ kept.length > 0 ? `import { ${kept.join(', ')} } from '${TOOLKIT_MODULE}';` : null,
337
+ ].filter((line) => line !== null);
338
+ const importEdits = toolkitImportsOf(rewrittenSource).map((statement, index) => index === 0 && replacement.length > 0
339
+ ? { start: statement.getStart(rewrittenSource), end: statement.getEnd(), text: replacement.join('\n') }
340
+ : removeStatement(statement, result));
341
+ result = applyEdits(result, importEdits);
342
+ if (refresh.needsInjectorImport) {
343
+ result = ensureNamedImports({
344
+ content: result,
345
+ importsNeeded: ['inject', 'Injector'],
346
+ moduleSpecifier: '@angular/core',
347
+ });
348
+ }
349
+ if (moved.length > 0 && hasInteropImport) {
350
+ result = ensureNamedImports({ content: result, importsNeeded: moved, moduleSpecifier: INTEROP_MODULE });
351
+ }
352
+ const tasks = [...refresh.tasks];
353
+ if (kept.length > 0) {
354
+ tasks.push({
355
+ id: CONSUMER_TASK.UNSUPPORTED_IMPORT,
356
+ summary: `Still imports ${kept.map((name) => `\`${name}\``).join(', ')} from \`${TOOLKIT_MODULE}\`.`,
357
+ locations: [{ filePath }],
358
+ });
359
+ }
360
+ return {
361
+ content: result,
362
+ rewritten: result !== content,
363
+ leftOnToolkit: kept.length > 0,
364
+ refreshSitesRewritten: refresh.rewritten,
365
+ tasks,
366
+ };
367
+ };
368
+ const isInterceptor = (node, sourceFile) => {
369
+ if (ts.isClassDeclaration(node)) {
370
+ return !!node.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText(sourceFile) === 'HttpInterceptor'));
371
+ }
372
+ if (ts.isVariableDeclaration(node))
373
+ return node.type?.getText(sourceFile) === 'HttpInterceptorFn';
374
+ return false;
375
+ };
376
+ const skippedRequests = (node, sourceFile) => {
377
+ const conditions = [];
378
+ const prefixes = new Set();
379
+ const visit = (inner) => {
380
+ if (ts.isIfStatement(inner) && /\burl\b/i.test(inner.expression.getText(sourceFile))) {
381
+ const condition = inner.expression.getText(sourceFile).replace(/\s+/g, ' ');
382
+ conditions.push(condition);
383
+ const collectPrefixes = (part) => {
384
+ const text = ts.isStringLiteralLike(part) ? part.text : ts.isTemplateSpan(part) ? part.literal.text : null;
385
+ const prefix = text && /^\/[\w\-/]*/.exec(text)?.[0];
386
+ if (prefix && prefix.length > 1)
387
+ prefixes.add(prefix);
388
+ ts.forEachChild(part, collectPrefixes);
389
+ };
390
+ collectPrefixes(inner.expression);
391
+ }
392
+ ts.forEachChild(inner, visit);
393
+ };
394
+ visit(node);
395
+ return { conditions, prefixes: [...prefixes] };
396
+ };
397
+ const findInterceptors = (tree, inScope, filePath, content) => {
398
+ if (!/HttpInterceptor/.test(content) || !/authorization|bearer/i.test(content))
399
+ return [];
400
+ const sourceFile = createSourceFile(content, filePath);
401
+ const tasks = [];
402
+ const visit = (node) => {
403
+ if (isInterceptor(node, sourceFile) && /authorization|bearer/i.test(node.getText(sourceFile))) {
404
+ const name = node.name?.getText(sourceFile) ?? 'interceptor';
405
+ const { conditions, prefixes } = skippedRequests(node, sourceFile);
406
+ const registrations = [...inScope]
407
+ .filter((candidate) => candidate !== filePath && tree.exists(candidate))
408
+ .filter((candidate) => new RegExp(`\\b${name}\\b`).test(tree.read(candidate, 'utf-8') ?? ''))
409
+ .map((candidate) => ({ filePath: candidate }));
410
+ const skipped = conditions.length > 0 ? ` It skips requests where ${conditions.map(code).join('; ')}.` : '';
411
+ const suggestion = prefixes.length > 0 ? ` Candidate \`--publicRoutes=${prefixes.join(',')}\`.` : '';
412
+ tasks.push({
413
+ id: CONSUMER_TASK.AUTH_INTERCEPTOR,
414
+ summary: `\`${name}\` adds an \`Authorization\` header.${skipped}${suggestion}`,
415
+ locations: [{ filePath, line: lineOf(sourceFile, node.getStart(sourceFile)) }, ...registrations],
416
+ });
417
+ return;
418
+ }
419
+ ts.forEachChild(node, visit);
420
+ };
421
+ visit(sourceFile);
422
+ return tasks;
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
+ };
486
+ /** Runs the consumer side of the migration on every scoped file that still imports `@tomtomb/ngrx-toolkit`. */
487
+ export const migrateToolkitConsumers = (tree, graph, inScope, report, migratedFacades) => {
488
+ defineConsumerTasks(report);
489
+ const stats = {
490
+ filesRewritten: 0,
491
+ filesLeftOnToolkit: 0,
492
+ refreshSitesRewritten: 0,
493
+ interceptorsFound: 0,
494
+ };
495
+ for (const filePath of inScope) {
496
+ if (filePath.endsWith('.d.ts'))
497
+ continue;
498
+ const content = tree.read(filePath, 'utf-8');
499
+ if (!content)
500
+ continue;
501
+ const interceptors = findInterceptors(tree, inScope, filePath, content);
502
+ interceptors.forEach((task) => report.add(task));
503
+ stats.interceptorsFound += interceptors.length;
504
+ findFacadeEventCalls(graph, migratedFacades, filePath, content).forEach((task) => report.add(task));
505
+ if (!content.includes(TOOLKIT_MODULE))
506
+ continue;
507
+ const result = rewriteConsumer(tree, graph, filePath, content);
508
+ if (!result)
509
+ continue;
510
+ result.tasks.forEach((task) => report.add(task));
511
+ if (result.rewritten) {
512
+ tree.write(filePath, result.content);
513
+ stats.filesRewritten += 1;
514
+ }
515
+ if (result.leftOnToolkit)
516
+ stats.filesLeftOnToolkit += 1;
517
+ stats.refreshSitesRewritten += result.refreshSitesRewritten;
518
+ }
519
+ return stats;
520
+ };
521
+ //# sourceMappingURL=consumers.js.map