@metamask-previews/platform-api-docs 0.0.0-preview-1275d0fda

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 (62) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +6 -0
  3. package/LICENSE.APACHE2 +201 -0
  4. package/LICENSE.MIT +21 -0
  5. package/README.md +39 -0
  6. package/dist/cli.cjs +214 -0
  7. package/dist/cli.cjs.map +1 -0
  8. package/dist/cli.d.cts +3 -0
  9. package/dist/cli.d.cts.map +1 -0
  10. package/dist/cli.d.mts +3 -0
  11. package/dist/cli.d.mts.map +1 -0
  12. package/dist/cli.mjs +212 -0
  13. package/dist/cli.mjs.map +1 -0
  14. package/dist/discovery.cjs +53 -0
  15. package/dist/discovery.cjs.map +1 -0
  16. package/dist/discovery.d.cts +22 -0
  17. package/dist/discovery.d.cts.map +1 -0
  18. package/dist/discovery.d.mts +22 -0
  19. package/dist/discovery.d.mts.map +1 -0
  20. package/dist/discovery.mjs +48 -0
  21. package/dist/discovery.mjs.map +1 -0
  22. package/dist/extraction.cjs +745 -0
  23. package/dist/extraction.cjs.map +1 -0
  24. package/dist/extraction.d.cts +40 -0
  25. package/dist/extraction.d.cts.map +1 -0
  26. package/dist/extraction.d.mts +40 -0
  27. package/dist/extraction.d.mts.map +1 -0
  28. package/dist/extraction.mjs +716 -0
  29. package/dist/extraction.mjs.map +1 -0
  30. package/dist/generate.cjs +373 -0
  31. package/dist/generate.cjs.map +1 -0
  32. package/dist/generate.d.cts +37 -0
  33. package/dist/generate.d.cts.map +1 -0
  34. package/dist/generate.d.mts +37 -0
  35. package/dist/generate.d.mts.map +1 -0
  36. package/dist/generate.mjs +346 -0
  37. package/dist/generate.mjs.map +1 -0
  38. package/dist/markdown.cjs +239 -0
  39. package/dist/markdown.cjs.map +1 -0
  40. package/dist/markdown.d.cts +52 -0
  41. package/dist/markdown.d.cts.map +1 -0
  42. package/dist/markdown.d.mts +52 -0
  43. package/dist/markdown.d.mts.map +1 -0
  44. package/dist/markdown.mjs +232 -0
  45. package/dist/markdown.mjs.map +1 -0
  46. package/dist/types.cjs +3 -0
  47. package/dist/types.cjs.map +1 -0
  48. package/dist/types.d.cts +63 -0
  49. package/dist/types.d.cts.map +1 -0
  50. package/dist/types.d.mts +63 -0
  51. package/dist/types.d.mts.map +1 -0
  52. package/dist/types.mjs +2 -0
  53. package/dist/types.mjs.map +1 -0
  54. package/package.json +75 -0
  55. package/site/docusaurus.config.ts +103 -0
  56. package/site/src/css/custom.css +336 -0
  57. package/site/static/fonts/MM-Sans/MM_Sans_Mono-Regular.woff2 +0 -0
  58. package/site/static/img/favicons/favicon-96x96.png +0 -0
  59. package/site/static/img/metamask-fox.svg +12 -0
  60. package/site/static/img/metamask-logo-dark.svg +3 -0
  61. package/site/static/img/metamask-logo.svg +3 -0
  62. package/site/tsconfig.json +13 -0
@@ -0,0 +1,716 @@
1
+ import * as path from "node:path";
2
+ import { Node as NodeGuards, Project, ts } from "ts-morph";
3
+ // ---------------------------------------------------------------------------
4
+ // JSDoc utilities
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Convert `{@link X}` references inside a string to plain backtick code spans,
8
+ * and escape any remaining (out-of-backtick) curly braces so the output is
9
+ * safe to drop into MDX.
10
+ *
11
+ * @param text - The raw text to normalize.
12
+ * @returns The text with `@link` resolved and stray braces escaped.
13
+ */
14
+ function escapeJsDocTextForMdx(text) {
15
+ const withLinksResolved = text.replace(/\{@link\s+([^}]+)\}/gu, '`$1`');
16
+ return withLinksResolved.replace(/`[^`]*`|(\{)|(\})/gu, (match, open, close) => {
17
+ if (open) {
18
+ return '\\{';
19
+ }
20
+ if (close) {
21
+ return '\\}';
22
+ }
23
+ return match;
24
+ });
25
+ }
26
+ /**
27
+ * Extract a JSDoc tag's comment text, normalizing whitespace so continuation
28
+ * lines are joined with single spaces. ts-morph returns the raw comment with
29
+ * embedded newlines preserved; we collapse those for the markdown output.
30
+ *
31
+ * @param tag - The JSDoc tag.
32
+ * @returns The flattened comment text.
33
+ */
34
+ function extractJsDocTagComment(tag) {
35
+ // istanbul ignore next: ts-morph returns null for tags without comment text,
36
+ // but every fixture tag we extract from carries a comment.
37
+ return (tag.getCommentText() ?? '').replace(/\s+/gu, ' ').trim();
38
+ }
39
+ /**
40
+ * Strip the conventional `- ` separator from the start of a `@param` tag's
41
+ * comment. JSDoc style is `@param name - description`, and ts-morph hands us
42
+ * back `- description` for the comment. The hyphen is purely cosmetic; we'd
43
+ * rather render the description without it.
44
+ *
45
+ * @param comment - The flattened comment text from a `@param` tag.
46
+ * @returns The comment with any leading `- ` (or `– `, `— `) removed.
47
+ */
48
+ function stripParamSeparator(comment) {
49
+ return comment.replace(/^[-–—]\s*/u, '');
50
+ }
51
+ /**
52
+ * Decompose a node's JSDoc into the parts the rendered docs need:
53
+ *
54
+ * - `description` — the body above the first tag, with `@deprecated` comments
55
+ * appended as `**Deprecated:** <comment>` lines and normalized for MDX
56
+ * (curly braces escaped, `{@link}` resolved),
57
+ * - `params` — every `@param` tag in source order, with name and description,
58
+ * - `returns` — the `@returns` tag's comment, or empty string if absent.
59
+ *
60
+ * Other tags (`@see`, `@throws`, `@template`, `@example`) are dropped.
61
+ *
62
+ * @param node - The AST node to extract JSDoc from.
63
+ * @returns The decomposed JSDoc; empty strings/arrays when the node has no JSDoc.
64
+ */
65
+ function extractJsDoc(node) {
66
+ const jsDocs = node.getJsDocs();
67
+ if (jsDocs.length === 0) {
68
+ return { description: '', params: [], returns: '' };
69
+ }
70
+ const jsDoc = jsDocs[0];
71
+ const descriptionBody = jsDoc.getDescription().trim();
72
+ const deprecatedLines = [];
73
+ const params = [];
74
+ let returns = '';
75
+ for (const tag of jsDoc.getTags()) {
76
+ const tagName = tag.getTagName();
77
+ if (tagName === 'deprecated') {
78
+ const comment = extractJsDocTagComment(tag);
79
+ // istanbul ignore next: bare `@deprecated` (without explanatory text)
80
+ // doesn't appear in messenger JSDoc in practice.
81
+ deprecatedLines.push(comment ? `**Deprecated:** ${comment}` : '**Deprecated:**');
82
+ }
83
+ else if (tagName === 'param' && NodeGuards.isJSDocParameterTag(tag)) {
84
+ const nameNode = tag.getNameNode();
85
+ // istanbul ignore next: `@param` tags without a name aren't valid JSDoc.
86
+ if (!nameNode) {
87
+ continue;
88
+ }
89
+ params.push({
90
+ name: nameNode.getText(),
91
+ description: escapeJsDocTextForMdx(stripParamSeparator(extractJsDocTagComment(tag))),
92
+ });
93
+ }
94
+ else if (tagName === 'returns' || tagName === 'return') {
95
+ returns = escapeJsDocTextForMdx(extractJsDocTagComment(tag));
96
+ }
97
+ }
98
+ const description = escapeJsDocTextForMdx([descriptionBody, ...deprecatedLines]
99
+ .filter((line) => line.length > 0)
100
+ .join('\n'));
101
+ return { description, params, returns };
102
+ }
103
+ /**
104
+ * Check whether a node has an `@deprecated` JSDoc tag.
105
+ *
106
+ * @param node - The AST node to check.
107
+ * @returns True if the node has an `@deprecated` tag.
108
+ */
109
+ function isDeprecated(node) {
110
+ return node
111
+ .getJsDocs()
112
+ .flatMap((jsDoc) => jsDoc.getTags())
113
+ .some((tag) => tag.getTagName() === 'deprecated');
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Type-resolution helpers (powered by ts-morph's type checker)
117
+ // ---------------------------------------------------------------------------
118
+ /**
119
+ * Resolve a TemplateLiteralTypeNode (e.g. `` `${typeof X}:foo` ``) to its
120
+ * concrete string value, using the type checker to follow `typeof X` to its
121
+ * literal type. Returns null if the type checker can't reduce it to a single
122
+ * string literal.
123
+ *
124
+ * @param node - The template literal type node.
125
+ * @returns The resolved string, or null.
126
+ */
127
+ function resolveTemplateLiteralType(node) {
128
+ const type = node.getType();
129
+ if (type.isStringLiteral()) {
130
+ return type.getLiteralValueOrThrow();
131
+ }
132
+ // istanbul ignore next: messenger fixtures always reduce template literal
133
+ // types to a single string literal via `typeof X` constants.
134
+ return null;
135
+ }
136
+ /**
137
+ * Resolve a capability-type-constructor's first generic argument to a
138
+ * namespace string. Accepts either `typeof X` (resolved via the type checker)
139
+ * or a string literal.
140
+ *
141
+ * @param typeArg - The first generic argument node.
142
+ * @returns The resolved namespace, or null.
143
+ */
144
+ function resolveNamespaceFromTypeArg(typeArg) {
145
+ if (NodeGuards.isTypeQuery(typeArg)) {
146
+ const type = typeArg.getType();
147
+ if (type.isStringLiteral()) {
148
+ return type.getLiteralValueOrThrow();
149
+ }
150
+ }
151
+ if (NodeGuards.isLiteralTypeNode(typeArg)) {
152
+ const literal = typeArg.getLiteral();
153
+ // istanbul ignore else: only string literals are valid namespace args.
154
+ if (NodeGuards.isStringLiteral(literal)) {
155
+ return literal.getLiteralValue();
156
+ }
157
+ }
158
+ // istanbul ignore next: namespace args are always a `typeof X` query or a
159
+ // string literal in valid messenger usage.
160
+ return null;
161
+ }
162
+ /**
163
+ * If `typeNode` is `Class['method']`, resolve `Class` to its declaration
164
+ * (across files if needed via the type checker) and look up the method by
165
+ * name. Returns the method declaration when found.
166
+ *
167
+ * @param typeNode - The handler's type node.
168
+ * @returns The method declaration, or null.
169
+ */
170
+ function resolveIndexedAccessMethod(typeNode) {
171
+ if (!typeNode || !NodeGuards.isIndexedAccessTypeNode(typeNode)) {
172
+ return null;
173
+ }
174
+ const objectType = typeNode.getObjectTypeNode();
175
+ const indexType = typeNode.getIndexTypeNode();
176
+ // istanbul ignore next: handler indexed-access types in messenger fixtures
177
+ // always reference a class via TypeReference.
178
+ if (!NodeGuards.isTypeReference(objectType)) {
179
+ return null;
180
+ }
181
+ // istanbul ignore next: the index in `Class['method']` is always a literal
182
+ // type node in valid handler syntax.
183
+ if (!NodeGuards.isLiteralTypeNode(indexType)) {
184
+ return null;
185
+ }
186
+ const indexLiteral = indexType.getLiteral();
187
+ // The index can be written as `'method'`, `"method"`, or `` `method` ``.
188
+ // The first two land as `StringLiteral`; the bare template literal is a
189
+ // `NoSubstitutionTemplateLiteral`.
190
+ // istanbul ignore next: numeric/boolean indices aren't valid method names.
191
+ if (!NodeGuards.isStringLiteral(indexLiteral) &&
192
+ !NodeGuards.isNoSubstitutionTemplateLiteral(indexLiteral)) {
193
+ return null;
194
+ }
195
+ const methodName = indexLiteral.getLiteralValue();
196
+ const classNameNode = objectType.getTypeName();
197
+ // istanbul ignore next: qualified-name class references aren't used in
198
+ // messenger handler types.
199
+ if (!NodeGuards.isIdentifier(classNameNode)) {
200
+ return null;
201
+ }
202
+ const localSymbol = classNameNode.getSymbol();
203
+ // istanbul ignore next: a referenced class name always resolves to a symbol
204
+ // in a typechecked project.
205
+ if (!localSymbol) {
206
+ return null;
207
+ }
208
+ // Follow the import alias (if any) so we can find the class declaration in
209
+ // its home file.
210
+ const symbol = localSymbol.getAliasedSymbol() ?? localSymbol;
211
+ // istanbul ignore next: a resolved symbol always exposes its declarations
212
+ // array in a typechecked project.
213
+ for (const declaration of symbol.getDeclarations() ?? []) {
214
+ if (NodeGuards.isClassDeclaration(declaration)) {
215
+ const method = declaration.getMethod(methodName);
216
+ if (method) {
217
+ return method;
218
+ }
219
+ }
220
+ }
221
+ // istanbul ignore next: only reached if the indexed method isn't declared
222
+ // on any of the resolved class declarations, which doesn't happen in valid
223
+ // handler types.
224
+ return null;
225
+ }
226
+ // ---------------------------------------------------------------------------
227
+ // Method info
228
+ // ---------------------------------------------------------------------------
229
+ /**
230
+ * Build a {@link MethodInfo} record for a class method — its textual
231
+ * signature plus the JSDoc description, `@param`, and `@returns` extracted
232
+ * from the method itself.
233
+ *
234
+ * @param method - The method declaration.
235
+ * @returns The method info.
236
+ */
237
+ function buildMethodInfo(method) {
238
+ const signatureParams = method
239
+ .getParameters()
240
+ .map((param) => {
241
+ const paramName = param.getNameNode().getText();
242
+ const optional = param.hasQuestionToken() ? '?' : '';
243
+ const typeNode = param.getTypeNode();
244
+ // istanbul ignore next: handler parameters in messenger fixtures always
245
+ // declare an explicit type.
246
+ const paramType = typeNode ? typeNode.getText() : 'unknown';
247
+ return `${paramName}${optional}: ${paramType}`;
248
+ })
249
+ .join(', ');
250
+ const returnTypeNode = method.getReturnTypeNode();
251
+ // istanbul ignore next: handler class methods in messenger fixtures always
252
+ // declare an explicit return type.
253
+ const returnType = returnTypeNode ? returnTypeNode.getText() : 'void';
254
+ // For async methods, the declared return type already includes `Promise<>`,
255
+ // so we don't need to wrap again.
256
+ const signature = `(${signatureParams}) => ${returnType}`;
257
+ const { description, params, returns } = extractJsDoc(method);
258
+ return { jsDoc: description, params, returns, signature };
259
+ }
260
+ /**
261
+ * Find every `*Messenger` type alias in a source file, parse its
262
+ * `Messenger<Namespace, Actions, Events>` generic arguments, and return the
263
+ * capability-type declarations referenced from the Actions and Events slots
264
+ * — paired with the kind under which they were referenced.
265
+ *
266
+ * The walker follows imported references via ts-morph's symbol resolution,
267
+ * so capability types declared in sibling files (e.g. the auto-generated
268
+ * `*-method-action-types.ts` files) are discovered even though only the
269
+ * `*Messenger` declaration is local to this file.
270
+ *
271
+ * @param sourceFile - The TypeScript source file to scan.
272
+ * @returns The list of locally- and transitively-referenced capability types.
273
+ */
274
+ function collectMessengerCapabilityTypeDeclarations(sourceFile) {
275
+ const declarations = [];
276
+ const seen = new Set();
277
+ for (const typeAlias of sourceFile.getTypeAliases()) {
278
+ if (!typeAlias.getName().endsWith('Messenger')) {
279
+ continue;
280
+ }
281
+ const body = typeAlias.getTypeNode();
282
+ if (!body || !NodeGuards.isTypeReference(body)) {
283
+ continue;
284
+ }
285
+ const typeArgs = body.getTypeArguments();
286
+ if (typeArgs.length < 3) {
287
+ continue;
288
+ }
289
+ walkCapabilityTypes(typeArgs[1], 'action', declarations, seen);
290
+ walkCapabilityTypes(typeArgs[2], 'event', declarations, seen);
291
+ }
292
+ return declarations;
293
+ }
294
+ /**
295
+ * Walk an Actions or Events type-argument tree and append the underlying
296
+ * capability-type declarations to `output`. Unions are expanded. Type
297
+ * references are resolved via ts-morph's symbol resolution (so imported
298
+ * names are followed across files); a resolved alias whose body is itself a
299
+ * union expands transparently, leaving the intermediate alias unrecorded.
300
+ *
301
+ * A resolved alias whose body is a single non-union type reference (e.g.
302
+ * `type AllowedActions = ConnectivityControllerGetStateAction`) is treated
303
+ * as an opaque re-export — the walk stops there, leaving the target to be
304
+ * documented from its home package by the dedup logic later.
305
+ *
306
+ * For example, given:
307
+ *
308
+ * ```typescript
309
+ * // NetworkController.ts
310
+ * import type { NetworkControllerMethodActions } from './NetworkController-method-action-types';
311
+ * type NetworkControllerActions =
312
+ * | NetworkControllerGetStateAction
313
+ * | NetworkControllerMethodActions;
314
+ * type NetworkControllerMessenger = Messenger<typeof name, NetworkControllerActions, ...>;
315
+ *
316
+ * // NetworkController-method-action-types.ts
317
+ * export type NetworkControllerAddNetworkAction = {
318
+ * type: 'NetworkController:addNetwork';
319
+ * handler: NetworkController['addNetwork'];
320
+ * };
321
+ * // ... more ...
322
+ * export type NetworkControllerMethodActions =
323
+ * | NetworkControllerAddNetworkAction
324
+ * | ...;
325
+ * ```
326
+ *
327
+ * walking the Actions slot yields each individual `NetworkController*Action`
328
+ * declaration — both the local `NetworkControllerGetStateAction` and the
329
+ * cross-file ones reached via `NetworkControllerMethodActions`.
330
+ *
331
+ * @param node - The Actions or Events type-argument node to walk.
332
+ * @param kind - Whether to tag found declarations as 'action' or 'event'.
333
+ * @param output - The list to append discovered declarations to.
334
+ * @param seen - Declaration nodes already visited (prevents cycles and
335
+ * duplicate work).
336
+ */
337
+ function walkCapabilityTypes(node, kind, output, seen) {
338
+ if (NodeGuards.isUnionTypeNode(node)) {
339
+ for (const member of node.getTypeNodes()) {
340
+ walkCapabilityTypes(member, kind, output, seen);
341
+ }
342
+ return;
343
+ }
344
+ if (!NodeGuards.isTypeReference(node)) {
345
+ return;
346
+ }
347
+ const nameNode = node.getTypeName();
348
+ // istanbul ignore next: qualified-name references aren't used in messenger
349
+ // generic arguments.
350
+ if (!NodeGuards.isIdentifier(nameNode)) {
351
+ return;
352
+ }
353
+ // For a TypeReference whose name was imported, `getSymbol()` returns the
354
+ // import alias symbol — its only declaration is the `ImportSpecifier`.
355
+ // `getAliasedSymbol()` follows the alias to the original declaration in
356
+ // the imported file. Use it when present; otherwise fall back to the
357
+ // (already-local) symbol.
358
+ const localSymbol = nameNode.getSymbol();
359
+ // istanbul ignore next: a referenced type name always resolves to a symbol
360
+ // in a typechecked project.
361
+ if (!localSymbol) {
362
+ return;
363
+ }
364
+ const symbol = localSymbol.getAliasedSymbol() ?? localSymbol;
365
+ // istanbul ignore next: a resolved symbol always exposes its declarations
366
+ // array in a typechecked project.
367
+ for (const declaration of symbol.getDeclarations() ?? []) {
368
+ if (seen.has(declaration)) {
369
+ continue;
370
+ }
371
+ seen.add(declaration);
372
+ if (NodeGuards.isTypeAliasDeclaration(declaration)) {
373
+ const aliasBody = declaration.getTypeNode();
374
+ if (aliasBody && NodeGuards.isUnionTypeNode(aliasBody)) {
375
+ // Umbrella union — descend through it without documenting the alias.
376
+ walkCapabilityTypes(aliasBody, kind, output, seen);
377
+ continue;
378
+ }
379
+ // A bare TypeReference body with no type arguments (e.g.
380
+ // `type AllowedActions = ConnectivityControllerGetStateAction`) is a
381
+ // plain re-export — leave the target to be documented from its home
382
+ // package, not this one. TypeReferences *with* type arguments are
383
+ // capability-type-constructor invocations (e.g.
384
+ // `ControllerGetStateAction<typeof name, State>`) and are recorded.
385
+ if (aliasBody &&
386
+ NodeGuards.isTypeReference(aliasBody) &&
387
+ aliasBody.getTypeArguments().length === 0) {
388
+ continue;
389
+ }
390
+ // TypeLiteral body, capability-type-constructor invocation, etc.
391
+ output.push({ statement: declaration, kind });
392
+ }
393
+ else if (NodeGuards.isInterfaceDeclaration(declaration)) {
394
+ output.push({ statement: declaration, kind });
395
+ }
396
+ }
397
+ }
398
+ // ---------------------------------------------------------------------------
399
+ // Per-statement extraction
400
+ // ---------------------------------------------------------------------------
401
+ /**
402
+ * Extract a single capability-type declaration to an
403
+ * {@link ExtractedMessengerCapabilityType}.
404
+ *
405
+ * @param statement - The type alias or interface declaration.
406
+ * @param kind - Whether this statement is referenced as an action or an event.
407
+ * @param projectPath - Project root, used for computing relative source paths.
408
+ * @returns The extracted capability, or null if no recognized pattern matches.
409
+ */
410
+ function extractFromMessengerCapabilityType(statement, kind, projectPath) {
411
+ const inlineCapability = extractFromInlineMessengerCapabilityType(statement, kind, projectPath);
412
+ if (inlineCapability) {
413
+ return inlineCapability;
414
+ }
415
+ if (NodeGuards.isTypeAliasDeclaration(statement)) {
416
+ return extractFromCapabilityTypeConstructor(statement, kind, projectPath);
417
+ }
418
+ // istanbul ignore next: interface declarations always have members and
419
+ // therefore an inline shape, so the inline branch above always returns
420
+ // non-null here.
421
+ return null;
422
+ }
423
+ /**
424
+ * Try the inline messenger-capability-type pattern:
425
+ * `{ type: '...'; handler: ... }` (action) or
426
+ * `{ type: '...'; payload: ... }` (event), expressed as either a type alias
427
+ * body or an interface body.
428
+ *
429
+ * @param statement - The type alias or interface declaration.
430
+ * @param kind - The expected kind (action or event).
431
+ * @param projectPath - Project root, used for computing relative source paths.
432
+ * @returns The extracted capability, or null if the shape doesn't match.
433
+ */
434
+ function extractFromInlineMessengerCapabilityType(statement, kind, projectPath) {
435
+ let members;
436
+ if (NodeGuards.isTypeAliasDeclaration(statement)) {
437
+ const body = statement.getTypeNode();
438
+ if (body && NodeGuards.isTypeLiteral(body)) {
439
+ members = body.getMembers();
440
+ }
441
+ }
442
+ else {
443
+ members = statement.getMembers();
444
+ }
445
+ if (!members) {
446
+ return null;
447
+ }
448
+ const typeString = resolveInlineTypeString(members);
449
+ // istanbul ignore next: capabilities reachable via the messenger walk
450
+ // always have a resolvable `Namespace:name` type string.
451
+ if (!typeString?.includes(':')) {
452
+ return null;
453
+ }
454
+ const handlerMember = getPropertyMember(members, 'handler');
455
+ const payloadMember = getPropertyMember(members, 'payload');
456
+ const rawSourceMember = kind === 'action' ? handlerMember : payloadMember;
457
+ // istanbul ignore next: actions always have `handler` and events always
458
+ // have `payload`; the messenger walk wouldn't have surfaced this
459
+ // declaration otherwise.
460
+ if (!rawSourceMember) {
461
+ return null;
462
+ }
463
+ const rawSourceTypeNode = rawSourceMember.getTypeNode();
464
+ // istanbul ignore next: property signatures we care about always have an
465
+ // explicit type.
466
+ if (!rawSourceTypeNode) {
467
+ return null;
468
+ }
469
+ let handlerOrPayload = rawSourceTypeNode.getText().trim();
470
+ let { description: jsDoc, params, returns } = extractJsDoc(statement);
471
+ // For actions, if the handler resolves to a class method (`Class['method']`
472
+ // — possibly in another file), inherit its signature plus any JSDoc fields
473
+ // the type alias itself doesn't already provide.
474
+ if (kind === 'action') {
475
+ const resolvedMethod = resolveIndexedAccessMethod(rawSourceTypeNode);
476
+ if (resolvedMethod) {
477
+ const info = buildMethodInfo(resolvedMethod);
478
+ handlerOrPayload = info.signature;
479
+ if (!jsDoc && info.jsDoc) {
480
+ jsDoc = info.jsDoc;
481
+ }
482
+ if (params.length === 0 && info.params.length > 0) {
483
+ params = info.params;
484
+ }
485
+ if (!returns && info.returns) {
486
+ returns = info.returns;
487
+ }
488
+ }
489
+ }
490
+ const sourceFile = statement.getSourceFile();
491
+ return {
492
+ typeName: statement.getName(),
493
+ typeString,
494
+ kind,
495
+ jsDoc,
496
+ params,
497
+ returns,
498
+ handlerOrPayload,
499
+ sourceFile: path.relative(projectPath, sourceFile.getFilePath()),
500
+ line: statement.getStartLineNumber(),
501
+ deprecated: isDeprecated(statement),
502
+ };
503
+ }
504
+ /**
505
+ * Resolve the literal value of an inline shape's `type` property — either a
506
+ * direct string literal or a template literal type with `typeof X`
507
+ * substitutions (resolved via the type checker).
508
+ *
509
+ * @param members - The type elements of the inline shape.
510
+ * @returns The resolved type string, or null if it can't be resolved.
511
+ */
512
+ function resolveInlineTypeString(members) {
513
+ for (const member of members) {
514
+ // istanbul ignore next: capability-type bodies only contain property
515
+ // signatures.
516
+ if (!NodeGuards.isPropertySignature(member)) {
517
+ continue;
518
+ }
519
+ const memberNameNode = member.getNameNode();
520
+ if (
521
+ // istanbul ignore next: `type` is always a plain identifier.
522
+ !NodeGuards.isIdentifier(memberNameNode) ||
523
+ memberNameNode.getText() !== 'type') {
524
+ continue;
525
+ }
526
+ const typeNode = member.getTypeNode();
527
+ // istanbul ignore next: a `type` property without an explicit type
528
+ // wouldn't compile.
529
+ if (!typeNode) {
530
+ continue;
531
+ }
532
+ if (NodeGuards.isLiteralTypeNode(typeNode)) {
533
+ const literal = typeNode.getLiteral();
534
+ // istanbul ignore next: messenger `type` fields are written as
535
+ // quoted string literals in real fixtures; backtick template literals
536
+ // are valid TypeScript but don't appear in practice.
537
+ if (NodeGuards.isStringLiteral(literal) ||
538
+ NodeGuards.isNoSubstitutionTemplateLiteral(literal)) {
539
+ return literal.getLiteralValue();
540
+ }
541
+ // istanbul ignore next: numeric/boolean literal types aren't valid as a
542
+ // messenger `type` and don't appear in real fixtures.
543
+ return null;
544
+ }
545
+ if (NodeGuards.isTemplateLiteralTypeNode(typeNode)) {
546
+ return resolveTemplateLiteralType(typeNode);
547
+ }
548
+ }
549
+ // istanbul ignore next: the inline shape always has a `type` member that
550
+ // produces either a literal or template-literal type.
551
+ return null;
552
+ }
553
+ /**
554
+ * Find the `PropertySignature` named `propName` in a type literal body.
555
+ *
556
+ * @param members - The type literal members to search.
557
+ * @param propName - The property name to find.
558
+ * @returns The property signature, or null.
559
+ */
560
+ function getPropertyMember(members, propName) {
561
+ for (const member of members) {
562
+ // istanbul ignore next: capability-type bodies only contain property
563
+ // signatures.
564
+ if (!NodeGuards.isPropertySignature(member)) {
565
+ continue;
566
+ }
567
+ const memberNameNode = member.getNameNode();
568
+ if (
569
+ // istanbul ignore next: capability properties always have identifier names.
570
+ !NodeGuards.isIdentifier(memberNameNode) ||
571
+ memberNameNode.getText() !== propName) {
572
+ continue;
573
+ }
574
+ return member;
575
+ }
576
+ return null;
577
+ }
578
+ /**
579
+ * Try the capability-type-constructor pattern:
580
+ * `ControllerGetStateAction<typeof X, State>` for actions, or
581
+ * `ControllerStateChangeEvent<typeof X, State>` for events.
582
+ *
583
+ * @param statement - The type alias declaration.
584
+ * @param kind - The expected kind (action or event).
585
+ * @param projectPath - Project root, used for computing relative source paths.
586
+ * @returns The extracted capability, or null if the constructor doesn't match.
587
+ */
588
+ function extractFromCapabilityTypeConstructor(statement, kind, projectPath) {
589
+ const aliasBody = statement.getTypeNode();
590
+ // istanbul ignore next: walker only records aliases whose body matches.
591
+ if (!aliasBody || !NodeGuards.isTypeReference(aliasBody)) {
592
+ return null;
593
+ }
594
+ const nameNode = aliasBody.getTypeName();
595
+ // istanbul ignore next: qualified-name type references aren't used.
596
+ if (!NodeGuards.isIdentifier(nameNode)) {
597
+ return null;
598
+ }
599
+ const constructorName = nameNode.getText();
600
+ const typeArgs = aliasBody.getTypeArguments();
601
+ if (typeArgs.length < 2) {
602
+ return null;
603
+ }
604
+ const expectedConstructor = kind === 'action'
605
+ ? 'ControllerGetStateAction'
606
+ : 'ControllerStateChangeEvent';
607
+ if (constructorName !== expectedConstructor) {
608
+ return null;
609
+ }
610
+ const namespace = resolveNamespaceFromTypeArg(typeArgs[0]);
611
+ // istanbul ignore next: recognized constructors always have resolvable args.
612
+ if (!namespace) {
613
+ return null;
614
+ }
615
+ const stateArgText = typeArgs[1].getText();
616
+ const { description, params, returns } = extractJsDoc(statement);
617
+ const sourceFile = statement.getSourceFile();
618
+ return {
619
+ typeName: statement.getName(),
620
+ typeString: kind === 'action' ? `${namespace}:getState` : `${namespace}:stateChange`,
621
+ kind,
622
+ jsDoc: description,
623
+ params,
624
+ returns,
625
+ handlerOrPayload: kind === 'action'
626
+ ? `() => ${stateArgText}`
627
+ : `[${stateArgText}, Patch[]]`,
628
+ sourceFile: path.relative(projectPath, sourceFile.getFilePath()),
629
+ line: statement.getStartLineNumber(),
630
+ deprecated: isDeprecated(statement),
631
+ };
632
+ }
633
+ // ---------------------------------------------------------------------------
634
+ // Public entry points
635
+ // ---------------------------------------------------------------------------
636
+ /**
637
+ * Create a ts-morph Project configured for messenger-docs extraction. The
638
+ * caller should add every source file that may be referenced (directly or
639
+ * transitively) before calling {@link extractFromSourceFile}, so the type
640
+ * checker can resolve cross-file references.
641
+ *
642
+ * @returns A new ts-morph Project.
643
+ */
644
+ export function createExtractionProject() {
645
+ return new Project({
646
+ compilerOptions: {
647
+ allowJs: false,
648
+ noEmit: true,
649
+ // Match the project's permissive defaults — we just need symbol
650
+ // resolution, not full typechecking.
651
+ strict: false,
652
+ skipLibCheck: true,
653
+ // Explicit module options so cross-file symbol resolution works
654
+ // regardless of the host process's tsconfig.
655
+ target: ts.ScriptTarget.ESNext,
656
+ module: ts.ModuleKind.ESNext,
657
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
658
+ },
659
+ });
660
+ }
661
+ /**
662
+ * Extract every messenger action/event type reachable from a single source
663
+ * file's `*Messenger` declarations.
664
+ *
665
+ * The caller is responsible for ensuring `sourceFile` (plus any files it
666
+ * imports from) belongs to a ts-morph Project so cross-file symbol resolution
667
+ * works.
668
+ *
669
+ * @param sourceFile - The TypeScript source file to extract from.
670
+ * @param projectPath - Project root, used for computing relative source paths.
671
+ * @returns The extracted capability list.
672
+ */
673
+ export function extractFromSourceFile(sourceFile, projectPath) {
674
+ const declarations = collectMessengerCapabilityTypeDeclarations(sourceFile);
675
+ if (declarations.length === 0) {
676
+ return [];
677
+ }
678
+ const items = [];
679
+ for (const { statement, kind } of declarations) {
680
+ const item = extractFromMessengerCapabilityType(statement, kind, projectPath);
681
+ if (item) {
682
+ items.push(item);
683
+ }
684
+ }
685
+ return items;
686
+ }
687
+ /**
688
+ * Convenience wrapper: extract from a single file by path. Loads the file
689
+ * and any sibling files in its parent directory into a fresh Project so
690
+ * relative imports resolve.
691
+ *
692
+ * For batch operations across many files, prefer
693
+ * {@link createExtractionProject} + {@link extractFromSourceFile} so one
694
+ * Project amortizes the type-checker setup across the whole run.
695
+ *
696
+ * @param filePath - The absolute path to the TypeScript file.
697
+ * @param projectPath - Base path for computing relative source paths.
698
+ * @returns A promise that resolves to the extracted capability list.
699
+ */
700
+ export async function extractFromFile(filePath, projectPath) {
701
+ const project = createExtractionProject();
702
+ const parentDir = path.dirname(filePath);
703
+ // Load the file's directory so single-hop relative imports resolve.
704
+ project.addSourceFilesAtPaths([
705
+ path.join(parentDir, '**/*.ts'),
706
+ path.join(parentDir, '**/*.d.cts'),
707
+ ]);
708
+ const sourceFile = project.getSourceFile(filePath);
709
+ // istanbul ignore next: the path always exists since the caller just
710
+ // wrote the file or it was scanned from disk.
711
+ if (!sourceFile) {
712
+ return [];
713
+ }
714
+ return extractFromSourceFile(sourceFile, projectPath);
715
+ }
716
+ //# sourceMappingURL=extraction.mjs.map