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

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.
@@ -23,16 +23,22 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.extractFromFile = exports.extractFromSourceFile = exports.createExtractionProject = void 0;
26
+ exports.extractFromSourceFile = exports.createExtractionProject = void 0;
27
27
  const path = __importStar(require("node:path"));
28
28
  const ts_morph_1 = require("ts-morph");
29
29
  // ---------------------------------------------------------------------------
30
+ // NOTE: `ts-morph` is used heavily in this file to parse and extract
31
+ // information from TypeScript files. Although this library is not well
32
+ // documented, it wraps the TypeScript AST fairly well, and you can get a good
33
+ // sense of the AST by using this website: <https://ts-ast-viewer.com>
34
+ // ---------------------------------------------------------------------------
35
+ // ---------------------------------------------------------------------------
30
36
  // JSDoc utilities
31
37
  // ---------------------------------------------------------------------------
32
38
  /**
33
- * Convert `{@link X}` references inside a string to plain backtick code spans,
34
- * and escape any remaining (out-of-backtick) curly braces so the output is
35
- * safe to drop into MDX.
39
+ * Convert `{@link X}` references inside a JSDoc comment string to plain
40
+ * backtick code spans and escape any remaining (out-of-backtick) curly braces.
41
+ * This way the output is safe to render in a MDX document.
36
42
  *
37
43
  * @param text - The raw text to normalize.
38
44
  * @returns The text with `@link` resolved and stray braces escaped.
@@ -50,32 +56,30 @@ function escapeJsDocTextForMdx(text) {
50
56
  });
51
57
  }
52
58
  /**
53
- * Extract a JSDoc tag's comment text, normalizing whitespace so continuation
54
- * lines are joined with single spaces. ts-morph returns the raw comment with
55
- * embedded newlines preserved; we collapse those for the markdown output.
59
+ * Extract the comment text of a JSDoc tag the part that comes after the tag
60
+ * and any identifier, e.g. "Some param" in "@param foo Some param"
61
+ * normalizing whitespace to a single space so we can better control how it's
62
+ * rendered within the site.
56
63
  *
57
64
  * @param tag - The JSDoc tag.
58
65
  * @returns The flattened comment text.
59
66
  */
60
67
  function extractJsDocTagComment(tag) {
61
- // istanbul ignore next: ts-morph returns null for tags without comment text,
62
- // but every fixture tag we extract from carries a comment.
63
68
  return (tag.getCommentText() ?? '').replace(/\s+/gu, ' ').trim();
64
69
  }
65
70
  /**
66
71
  * Strip the conventional `- ` separator from the start of a `@param` tag's
67
- * comment. JSDoc style is `@param name - description`, and ts-morph hands us
68
- * back `- description` for the comment. The hyphen is purely cosmetic; we'd
69
- * rather render the description without it.
72
+ * comment.
70
73
  *
71
74
  * @param comment - The flattened comment text from a `@param` tag.
72
75
  * @returns The comment with any leading `- ` (or `– `, `— `) removed.
73
76
  */
74
- function stripParamSeparator(comment) {
77
+ function stripJsDocParamSeparator(comment) {
75
78
  return comment.replace(/^[-–—]\s*/u, '');
76
79
  }
77
80
  /**
78
- * Decompose a node's JSDoc into the parts the rendered docs need:
81
+ * Extract JSDoc from a TypeScript AST node and decompose it into the parts we
82
+ * need to render docs:
79
83
  *
80
84
  * - `description` — the body above the first tag, with `@deprecated` comments
81
85
  * appended as `**Deprecated:** <comment>` lines and normalized for MDX
@@ -85,7 +89,7 @@ function stripParamSeparator(comment) {
85
89
  *
86
90
  * Other tags (`@see`, `@throws`, `@template`, `@example`) are dropped.
87
91
  *
88
- * @param node - The AST node to extract JSDoc from.
92
+ * @param node - The AST node to extract JSDoc from (e.g. a type or a method).
89
93
  * @returns The decomposed JSDoc; empty strings/arrays when the node has no JSDoc.
90
94
  */
91
95
  function extractJsDoc(node) {
@@ -102,23 +106,23 @@ function extractJsDoc(node) {
102
106
  const tagName = tag.getTagName();
103
107
  if (tagName === 'deprecated') {
104
108
  const comment = extractJsDocTagComment(tag);
105
- // istanbul ignore next: bare `@deprecated` (without explanatory text)
106
- // doesn't appear in messenger JSDoc in practice.
107
109
  deprecatedLines.push(comment ? `**Deprecated:** ${comment}` : '**Deprecated:**');
108
110
  }
109
111
  else if (tagName === 'param' && ts_morph_1.Node.isJSDocParameterTag(tag)) {
110
112
  const nameNode = tag.getNameNode();
111
- // istanbul ignore next: `@param` tags without a name aren't valid JSDoc.
112
- if (!nameNode) {
113
+ const paramName = nameNode.getText();
114
+ if (!paramName) {
113
115
  continue;
114
116
  }
117
+ const comment = extractJsDocTagComment(tag);
115
118
  params.push({
116
- name: nameNode.getText(),
117
- description: escapeJsDocTextForMdx(stripParamSeparator(extractJsDocTagComment(tag))),
119
+ name: paramName,
120
+ description: escapeJsDocTextForMdx(stripJsDocParamSeparator(comment)),
118
121
  });
119
122
  }
120
123
  else if (tagName === 'returns' || tagName === 'return') {
121
- returns = escapeJsDocTextForMdx(extractJsDocTagComment(tag));
124
+ const comment = extractJsDocTagComment(tag);
125
+ returns = escapeJsDocTextForMdx(comment);
122
126
  }
123
127
  }
124
128
  const description = escapeJsDocTextForMdx([descriptionBody, ...deprecatedLines]
@@ -132,7 +136,7 @@ function extractJsDoc(node) {
132
136
  * @param node - The AST node to check.
133
137
  * @returns True if the node has an `@deprecated` tag.
134
138
  */
135
- function isDeprecated(node) {
139
+ function hasDeprecatedJsDocTag(node) {
136
140
  return node
137
141
  .getJsDocs()
138
142
  .flatMap((jsDoc) => jsDoc.getTags())
@@ -142,101 +146,63 @@ function isDeprecated(node) {
142
146
  // Type-resolution helpers (powered by ts-morph's type checker)
143
147
  // ---------------------------------------------------------------------------
144
148
  /**
145
- * Resolve a TemplateLiteralTypeNode (e.g. `` `${typeof X}:foo` ``) to its
146
- * concrete string value, using the type checker to follow `typeof X` to its
147
- * literal type. Returns null if the type checker can't reduce it to a single
148
- * string literal.
149
+ * Locates the type that represents a method on a class (e.g.
150
+ * `Class['method']`), which itself comes from a messenger action handler.
149
151
  *
150
- * @param node - The template literal type node.
151
- * @returns The resolved string, or null.
152
+ * @param typeNode - The node that represents the indexed access.
153
+ * @returns The found method declaration, or null.
152
154
  */
153
- function resolveTemplateLiteralType(node) {
154
- const type = node.getType();
155
- if (type.isStringLiteral()) {
156
- return type.getLiteralValueOrThrow();
157
- }
158
- // istanbul ignore next: messenger fixtures always reduce template literal
159
- // types to a single string literal via `typeof X` constants.
160
- return null;
161
- }
162
- /**
163
- * Resolve a capability-type-constructor's first generic argument to a
164
- * namespace string. Accepts either `typeof X` (resolved via the type checker)
165
- * or a string literal.
166
- *
167
- * @param typeArg - The first generic argument node.
168
- * @returns The resolved namespace, or null.
169
- */
170
- function resolveNamespaceFromTypeArg(typeArg) {
171
- if (ts_morph_1.Node.isTypeQuery(typeArg)) {
172
- const type = typeArg.getType();
173
- if (type.isStringLiteral()) {
174
- return type.getLiteralValueOrThrow();
175
- }
176
- }
177
- if (ts_morph_1.Node.isLiteralTypeNode(typeArg)) {
178
- const literal = typeArg.getLiteral();
179
- // istanbul ignore else: only string literals are valid namespace args.
180
- if (ts_morph_1.Node.isStringLiteral(literal)) {
181
- return literal.getLiteralValue();
182
- }
183
- }
184
- // istanbul ignore next: namespace args are always a `typeof X` query or a
185
- // string literal in valid messenger usage.
186
- return null;
187
- }
188
- /**
189
- * If `typeNode` is `Class['method']`, resolve `Class` to its declaration
190
- * (across files if needed via the type checker) and look up the method by
191
- * name. Returns the method declaration when found.
192
- *
193
- * @param typeNode - The handler's type node.
194
- * @returns The method declaration, or null.
195
- */
196
- function resolveIndexedAccessMethod(typeNode) {
197
- if (!typeNode || !ts_morph_1.Node.isIndexedAccessTypeNode(typeNode)) {
155
+ function findClassMethodDeclaration(typeNode) {
156
+ // Fundamental check: if we don't have `Class['method']`, we can't do anything.
157
+ if (!ts_morph_1.Node.isIndexedAccessTypeNode(typeNode)) {
198
158
  return null;
199
159
  }
160
+ // The type that represents the class being accessed.
161
+ // EXAMPLE:
162
+ // FooController['someMethod']
163
+ // ^^^^^^^^^^^^^
200
164
  const objectType = typeNode.getObjectTypeNode();
165
+ // The type that represents the property being accessed.
166
+ // EXAMPLE:
167
+ // FooController['someMethod']
168
+ // ^^^^^^^^^^^^
201
169
  const indexType = typeNode.getIndexTypeNode();
202
- // istanbul ignore next: handler indexed-access types in messenger fixtures
203
- // always reference a class via TypeReference.
204
- if (!ts_morph_1.Node.isTypeReference(objectType)) {
205
- return null;
206
- }
207
- // istanbul ignore next: the index in `Class['method']` is always a literal
208
- // type node in valid handler syntax.
209
- if (!ts_morph_1.Node.isLiteralTypeNode(indexType)) {
170
+ // To access a property on a type, it must be a type we can access properties of.
171
+ if (!ts_morph_1.Node.isTypeReference(objectType) ||
172
+ !ts_morph_1.Node.isLiteralTypeNode(indexType)) {
210
173
  return null;
211
174
  }
212
175
  const indexLiteral = indexType.getLiteral();
213
- // The index can be written as `'method'`, `"method"`, or `` `method` ``.
214
- // The first two land as `StringLiteral`; the bare template literal is a
215
- // `NoSubstitutionTemplateLiteral`.
216
- // istanbul ignore next: numeric/boolean indices aren't valid method names.
176
+ // Names of methods must be static strings; they cannot be template strings.
217
177
  if (!ts_morph_1.Node.isStringLiteral(indexLiteral) &&
218
178
  !ts_morph_1.Node.isNoSubstitutionTemplateLiteral(indexLiteral)) {
219
179
  return null;
220
180
  }
221
181
  const methodName = indexLiteral.getLiteralValue();
182
+ // Reject qualified-name type names, as we need a plain identifier to
183
+ // resolve the symbol.
184
+ // EXAMPLE:
185
+ // import * as somePackage from '...';
186
+ // somePackage.FooController['someMethod']
187
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^
222
188
  const classNameNode = objectType.getTypeName();
223
- // istanbul ignore next: qualified-name class references aren't used in
224
- // messenger handler types.
225
189
  if (!ts_morph_1.Node.isIdentifier(classNameNode)) {
226
190
  return null;
227
191
  }
192
+ // Since we know we have a type reference, we can assume that we have a symbol.
193
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
228
194
  const localSymbol = classNameNode.getSymbol();
229
- // istanbul ignore next: a referenced class name always resolves to a symbol
230
- // in a typechecked project.
231
- if (!localSymbol) {
232
- return null;
233
- }
234
- // Follow the import alias (if any) so we can find the class declaration in
235
- // its home file.
195
+ // If we have a type imported from another file, ensure that when we access
196
+ // the declaration, it's the type declaration in the other file, not the
197
+ // import declaration in this file.
198
+ // EXAMPLE:
199
+ // import { FooController } from '@metamask/foo-controller';
200
+ // FooController['someMethod']
201
+ // ^^^^^^^^^^^^^
236
202
  const symbol = localSymbol.getAliasedSymbol() ?? localSymbol;
237
- // istanbul ignore next: a resolved symbol always exposes its declarations
238
- // array in a typechecked project.
239
- for (const declaration of symbol.getDeclarations() ?? []) {
203
+ for (const declaration of symbol.getDeclarations()) {
204
+ // We must have a class to treat the property on the object type as a
205
+ // method.
240
206
  if (ts_morph_1.Node.isClassDeclaration(declaration)) {
241
207
  const method = declaration.getMethod(methodName);
242
208
  if (method) {
@@ -244,416 +210,490 @@ function resolveIndexedAccessMethod(typeNode) {
244
210
  }
245
211
  }
246
212
  }
247
- // istanbul ignore next: only reached if the indexed method isn't declared
248
- // on any of the resolved class declarations, which doesn't happen in valid
249
- // handler types.
250
213
  return null;
251
214
  }
252
215
  // ---------------------------------------------------------------------------
253
216
  // Method info
254
217
  // ---------------------------------------------------------------------------
255
218
  /**
256
- * Build a {@link MethodInfo} record for a class method — its textual
257
- * signature plus the JSDoc description, `@param`, and `@returns` extracted
258
- * from the method itself.
219
+ * Build the textual signature of a class method — its parameter list and
220
+ * return type expressed as a TypeScript function type — so a handler that
221
+ * references `Class['method']` can be rendered as `(arg: T) => R` instead of
222
+ * the bare indexed-access syntax.
259
223
  *
260
224
  * @param method - The method declaration.
261
- * @returns The method info.
225
+ * @returns The signature, e.g. `(id: number) => Promise<string>`.
262
226
  */
263
- function buildMethodInfo(method) {
227
+ function buildMethodSignature(method) {
264
228
  const signatureParams = method
265
229
  .getParameters()
266
230
  .map((param) => {
231
+ const rest = param.isRestParameter() ? '...' : '';
267
232
  const paramName = param.getNameNode().getText();
268
233
  const optional = param.hasQuestionToken() ? '?' : '';
269
234
  const typeNode = param.getTypeNode();
270
- // istanbul ignore next: handler parameters in messenger fixtures always
271
- // declare an explicit type.
272
235
  const paramType = typeNode ? typeNode.getText() : 'unknown';
273
- return `${paramName}${optional}: ${paramType}`;
236
+ return `${rest}${paramName}${optional}: ${paramType}`;
274
237
  })
275
238
  .join(', ');
276
239
  const returnTypeNode = method.getReturnTypeNode();
277
- // istanbul ignore next: handler class methods in messenger fixtures always
278
- // declare an explicit return type.
279
240
  const returnType = returnTypeNode ? returnTypeNode.getText() : 'void';
280
241
  // For async methods, the declared return type already includes `Promise<>`,
281
242
  // so we don't need to wrap again.
282
- const signature = `(${signatureParams}) => ${returnType}`;
283
- const { description, params, returns } = extractJsDoc(method);
284
- return { jsDoc: description, params, returns, signature };
243
+ return `(${signatureParams}) => ${returnType}`;
285
244
  }
286
245
  /**
287
- * Find every `*Messenger` type alias in a source file, parse its
288
- * `Messenger<Namespace, Actions, Events>` generic arguments, and return the
289
- * capability-type declarations referenced from the Actions and Events slots
290
- * — paired with the kind under which they were referenced.
291
- *
292
- * The walker follows imported references via ts-morph's symbol resolution,
293
- * so capability types declared in sibling files (e.g. the auto-generated
294
- * `*-method-action-types.ts` files) are discovered even though only the
295
- * `*Messenger` declaration is local to this file.
246
+ * Looks for Messenger types in the source file (that is, those that are type
247
+ * aliases whose names end with "Messenger"), then extracts the `Actions` and
248
+ * `Events` parameters from these types.
296
249
  *
297
250
  * @param sourceFile - The TypeScript source file to scan.
298
- * @returns The list of locally- and transitively-referenced capability types.
251
+ * @returns A list of objects that represent messenger types.
299
252
  */
300
- function collectMessengerCapabilityTypeDeclarations(sourceFile) {
301
- const declarations = [];
302
- const seen = new Set();
253
+ function findMessengerTypeAliases(sourceFile) {
254
+ const parsedMessengerTypeAliases = [];
303
255
  for (const typeAlias of sourceFile.getTypeAliases()) {
304
256
  if (!typeAlias.getName().endsWith('Messenger')) {
305
257
  continue;
306
258
  }
307
259
  const body = typeAlias.getTypeNode();
260
+ // Basic check
308
261
  if (!body || !ts_morph_1.Node.isTypeReference(body)) {
309
262
  continue;
310
263
  }
311
264
  const typeArgs = body.getTypeArguments();
265
+ // Messenger types always have 3 type parameters
266
+ // (e.g. `Messenger<'FooController', Actions, Events>`)
312
267
  if (typeArgs.length < 3) {
313
268
  continue;
314
269
  }
315
- walkCapabilityTypes(typeArgs[1], 'action', declarations, seen);
316
- walkCapabilityTypes(typeArgs[2], 'event', declarations, seen);
270
+ parsedMessengerTypeAliases.push({
271
+ actionsTypeParameter: typeArgs[1],
272
+ eventsTypeParameter: typeArgs[2],
273
+ });
317
274
  }
318
- return declarations;
275
+ return parsedMessengerTypeAliases;
319
276
  }
320
277
  /**
321
- * Walk an Actions or Events type-argument tree and append the underlying
322
- * capability-type declarations to `output`. Unions are expanded. Type
323
- * references are resolved via ts-morph's symbol resolution (so imported
324
- * names are followed across files); a resolved alias whose body is itself a
325
- * union expands transparently, leaving the intermediate alias unrecorded.
326
- *
327
- * A resolved alias whose body is a single non-union type reference (e.g.
328
- * `type AllowedActions = ConnectivityControllerGetStateAction`) is treated
329
- * as an opaque re-export — the walk stops there, leaving the target to be
330
- * documented from its home package by the dedup logic later.
331
- *
332
- * For example, given:
278
+ * Walks the `Actions` and `Events` type parameters of the given messenger
279
+ * types, extracted in a previous step, to find all type declarations (i.e.,
280
+ * statements) that represent individual messenger actions or events.
333
281
  *
334
- * ```typescript
335
- * // NetworkController.ts
336
- * import type { NetworkControllerMethodActions } from './NetworkController-method-action-types';
337
- * type NetworkControllerActions =
338
- * | NetworkControllerGetStateAction
339
- * | NetworkControllerMethodActions;
340
- * type NetworkControllerMessenger = Messenger<typeof name, NetworkControllerActions, ...>;
341
- *
342
- * // NetworkController-method-action-types.ts
343
- * export type NetworkControllerAddNetworkAction = {
344
- * type: 'NetworkController:addNetwork';
345
- * handler: NetworkController['addNetwork'];
346
- * };
347
- * // ... more ...
348
- * export type NetworkControllerMethodActions =
349
- * | NetworkControllerAddNetworkAction
350
- * | ...;
351
- * ```
352
- *
353
- * walking the Actions slot yields each individual `NetworkController*Action`
354
- * declaration — both the local `NetworkControllerGetStateAction` and the
355
- * cross-file ones reached via `NetworkControllerMethodActions`.
282
+ * @param parsedMessengerTypeAliases - The list of objects representing
283
+ * messenger types, parsed in a previous step.
284
+ * @returns The list of type aliases that represent messenger capabilities among
285
+ * the given messenger types.
286
+ */
287
+ function findAllMessengerCapabilityTypeDeclarations(parsedMessengerTypeAliases) {
288
+ const allCapabilityTypeDeclarations = [];
289
+ let allVisitedTypeDeclarations = new Set();
290
+ for (const { actionsTypeParameter, eventsTypeParameter, } of parsedMessengerTypeAliases) {
291
+ for (const [typeParameter, kind] of [
292
+ [actionsTypeParameter, 'action'],
293
+ [eventsTypeParameter, 'event'],
294
+ ]) {
295
+ const result = recursivelyFindMessengerCapabilityTypeDeclarations(typeParameter, kind, allVisitedTypeDeclarations);
296
+ allCapabilityTypeDeclarations.push(...result.capabilityTypeDeclarations);
297
+ allVisitedTypeDeclarations = result.visitedTypeDeclarations;
298
+ }
299
+ }
300
+ return allCapabilityTypeDeclarations;
301
+ }
302
+ /**
303
+ * Recursively walks a `ts-morph` AST node at first the `Actions` or `Events`
304
+ * type parameter of a messenger type, and then a node within that parameter —
305
+ * to find all type aliases that represent individual messenger actions or
306
+ * events, no matter how deeply the type aliases exist in the tree or in which
307
+ * file they are located.
356
308
  *
357
- * @param node - The Actions or Events type-argument node to walk.
358
- * @param kind - Whether to tag found declarations as 'action' or 'event'.
359
- * @param output - The list to append discovered declarations to.
360
- * @param seen - Declaration nodes already visited (prevents cycles and
361
- * duplicate work).
309
+ * @param node - The `ts-morph` AST node to walk.
310
+ * @param kind - Whether to tag found type aliases as 'action' or 'event'.
311
+ * @param visitedTypeDeclarations - A variable that tracks visited type aliases
312
+ * and prevents duplicates.
313
+ * @returns The list of extracted messenger capability type aliases as well as
314
+ * an updated version of `visitedTypeDeclarations`.
362
315
  */
363
- function walkCapabilityTypes(node, kind, output, seen) {
316
+ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedTypeDeclarations) {
317
+ const result = {
318
+ capabilityTypeDeclarations: [],
319
+ visitedTypeDeclarations: new Set([...visitedTypeDeclarations]),
320
+ };
321
+ // If `node` is a union type, walk each type within it.
322
+ // EXAMPLES:
323
+ // type Actions = FooControllerSomeAction | FooControllerSomeOtherAction
324
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
325
+ // type FooControllerActions = FooControllerSomeAction | FooControllerSomeOtherAction
326
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
364
327
  if (ts_morph_1.Node.isUnionTypeNode(node)) {
365
- for (const member of node.getTypeNodes()) {
366
- walkCapabilityTypes(member, kind, output, seen);
328
+ for (const typeNode of node.getTypeNodes()) {
329
+ const innerResult = recursivelyFindMessengerCapabilityTypeDeclarations(typeNode, kind, result.visitedTypeDeclarations);
330
+ result.capabilityTypeDeclarations.push(...innerResult.capabilityTypeDeclarations);
331
+ for (const typeDeclaration of innerResult.visitedTypeDeclarations) {
332
+ result.visitedTypeDeclarations.add(typeDeclaration);
333
+ }
367
334
  }
368
- return;
369
- }
335
+ return result;
336
+ }
337
+ // If `node` is not a type reference, don't walk it.
338
+ // EXAMPLE:
339
+ // // Bad
340
+ // type Actions = { ... }
341
+ // ^^^^^^^
342
+ // // Good
343
+ // type Actions = FooControllerSomeAction;
344
+ // ^^^^^^^^^^^^^^^^^^^^^^^
345
+ // // Good
346
+ // type Actions = ControllerGetStateAction<...>;
347
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
370
348
  if (!ts_morph_1.Node.isTypeReference(node)) {
371
- return;
349
+ return result;
372
350
  }
373
351
  const nameNode = node.getTypeName();
374
- // istanbul ignore next: qualified-name references aren't used in messenger
375
- // generic arguments.
352
+ // Reject qualified-name type names, as we need a plain identifier to
353
+ // resolve the symbol.
354
+ // EXAMPLE:
355
+ // import * as somePackage from '...';
356
+ // type Actions = somePackage.FooControllerSomeAction;
357
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
376
358
  if (!ts_morph_1.Node.isIdentifier(nameNode)) {
377
- return;
359
+ return result;
378
360
  }
379
- // For a TypeReference whose name was imported, `getSymbol()` returns the
380
- // import alias symbol — its only declaration is the `ImportSpecifier`.
381
- // `getAliasedSymbol()` follows the alias to the original declaration in
382
- // the imported file. Use it when present; otherwise fall back to the
383
- // (already-local) symbol.
361
+ // Since we know we have a type reference, we can assume that we have a symbol.
362
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
384
363
  const localSymbol = nameNode.getSymbol();
385
- // istanbul ignore next: a referenced type name always resolves to a symbol
386
- // in a typechecked project.
387
- if (!localSymbol) {
388
- return;
389
- }
364
+ // If we have a type imported from another file, ensure that when we access
365
+ // the declaration, it's the type declaration in the other file, not the
366
+ // import declaration in this file.
367
+ // EXAMPLE:
368
+ // import { FooControllerSomeAction } from '@metamask/foo-controller';
369
+ // type Actions = FooControllerSomeAction;
370
+ // ^^^^^^^^^^^^^^^^^^^^^^^
390
371
  const symbol = localSymbol.getAliasedSymbol() ?? localSymbol;
391
- // istanbul ignore next: a resolved symbol always exposes its declarations
392
- // array in a typechecked project.
393
- for (const declaration of symbol.getDeclarations() ?? []) {
394
- if (seen.has(declaration)) {
372
+ // At this point, we have a type *reference*, but we need to find the type
373
+ // *declaration*.
374
+ // For instance, if we have `FooControllerSomeAction`, we need to find
375
+ // the full `type FooControllerSomeAction = ...` statement.
376
+ for (const declaration of symbol.getDeclarations()) {
377
+ // Prevent duplicates
378
+ if (result.visitedTypeDeclarations.has(declaration)) {
395
379
  continue;
396
380
  }
397
- seen.add(declaration);
381
+ result.visitedTypeDeclarations.add(declaration);
382
+ // If we have a type alias, then we have to handle a few scenarios.
383
+ // EXAMPLES:
384
+ // type FooControllerMethodActions = ...
385
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
386
+ // type FooControllerSomeAction = ...
387
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
398
388
  if (ts_morph_1.Node.isTypeAliasDeclaration(declaration)) {
399
- const aliasBody = declaration.getTypeNode();
400
- if (aliasBody && ts_morph_1.Node.isUnionTypeNode(aliasBody)) {
401
- // Umbrella union — descend through it without documenting the alias.
402
- walkCapabilityTypes(aliasBody, kind, output, seen);
389
+ const body = declaration.getTypeNode();
390
+ // If the body is a union type or a plain type reference (not a utility
391
+ // type), walk it.
392
+ // EXAMPLES:
393
+ // type FooControllerMethodActions = FooControllerSomeAction | FooControllerSomeOtherAction
394
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
395
+ // type DelegationControllerMethodActions = DelegationControllerSignDelegationAction
396
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
397
+ if (body &&
398
+ (ts_morph_1.Node.isUnionTypeNode(body) ||
399
+ (ts_morph_1.Node.isTypeReference(body) &&
400
+ body.getTypeArguments().length === 0))) {
401
+ const innerResult = recursivelyFindMessengerCapabilityTypeDeclarations(body, kind, result.visitedTypeDeclarations);
402
+ result.capabilityTypeDeclarations.push(...innerResult.capabilityTypeDeclarations);
403
+ for (const typeDeclaration of innerResult.visitedTypeDeclarations) {
404
+ result.visitedTypeDeclarations.add(typeDeclaration);
405
+ }
403
406
  continue;
404
407
  }
405
- // A bare TypeReference body with no type arguments (e.g.
406
- // `type AllowedActions = ConnectivityControllerGetStateAction`) is a
407
- // plain re-export leave the target to be documented from its home
408
- // package, not this one. TypeReferences *with* type arguments are
409
- // capability-type-constructor invocations (e.g.
410
- // `ControllerGetStateAction<typeof name, State>`) and are recorded.
411
- if (aliasBody &&
412
- ts_morph_1.Node.isTypeReference(aliasBody) &&
413
- aliasBody.getTypeArguments().length === 0) {
408
+ // A TypeReference body with type arguments is a capability-type-
409
+ // constructor invocation (e.g. `ControllerGetStateAction<typeof name,
410
+ // State>`). Tag it so the constructor extractor can read `body`
411
+ // directly without re-checking its shape.
412
+ // EXAMPLE:
413
+ // type FooControllerSomeAction = ControllerGetStateAction<...>
414
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
415
+ if (body && ts_morph_1.Node.isTypeReference(body)) {
416
+ // Reject qualified-name constructor type names, as we need a plain
417
+ // identifier to match the constructor by name.
418
+ // EXAMPLE:
419
+ // // Bad
420
+ // import * as somePackage from '...';
421
+ // type FooControllerSomeAction = somePackage.ControllerGetStateAction<...>
422
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
423
+ const constructorTypeName = body.getTypeName();
424
+ if (ts_morph_1.Node.isIdentifier(constructorTypeName)) {
425
+ result.capabilityTypeDeclarations.push({
426
+ bodyShape: 'constructor',
427
+ kind,
428
+ declaration,
429
+ body,
430
+ typeName: constructorTypeName,
431
+ });
432
+ }
414
433
  continue;
415
434
  }
416
- // TypeLiteral body, capability-type-constructor invocation, etc.
417
- output.push({ statement: declaration, kind });
435
+ // Anything else (a type literal, intersection, conditional, …) gets
436
+ // tagged for the literal extractor, which knows how to read members
437
+ // off a type literal and rejects exotic shapes.
438
+ // EXAMPLE:
439
+ // type FooControllerSomeAction = { ... }
440
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
441
+ result.capabilityTypeDeclarations.push({
442
+ bodyShape: 'object',
443
+ kind,
444
+ declaration,
445
+ });
418
446
  }
447
+ // Interfaces always carry their members directly — tag for the literal
448
+ // extractor.
449
+ // EXAMPLE:
450
+ // interface FooControllerSomeAction { ... }
451
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
419
452
  else if (ts_morph_1.Node.isInterfaceDeclaration(declaration)) {
420
- output.push({ statement: declaration, kind });
453
+ result.capabilityTypeDeclarations.push({
454
+ bodyShape: 'object',
455
+ kind,
456
+ declaration,
457
+ });
421
458
  }
422
459
  }
460
+ return result;
423
461
  }
424
462
  // ---------------------------------------------------------------------------
425
463
  // Per-statement extraction
426
464
  // ---------------------------------------------------------------------------
427
465
  /**
428
- * Extract a single capability-type declaration to an
429
- * {@link ExtractedMessengerCapabilityType}.
466
+ * Given the declaration of a messenger capability type, extract information
467
+ * about it (action/event type string, handler/payload arguments and return
468
+ * type, etc.)
430
469
  *
431
- * @param statement - The type alias or interface declaration.
432
- * @param kind - Whether this statement is referenced as an action or an event.
470
+ * @param capabilityTypeDeclaration - The statement that declared the type for a
471
+ * messenger action or event, extracted in a previous step.
433
472
  * @param projectPath - Project root, used for computing relative source paths.
434
- * @returns The extracted capability, or null if no recognized pattern matches.
473
+ * @returns Information that may be extracted from the messenger capability type
474
+ * (may be `null` if the type is ineligible for extraction).
435
475
  */
436
- function extractFromMessengerCapabilityType(statement, kind, projectPath) {
437
- const inlineCapability = extractFromInlineMessengerCapabilityType(statement, kind, projectPath);
438
- if (inlineCapability) {
439
- return inlineCapability;
440
- }
441
- if (ts_morph_1.Node.isTypeAliasDeclaration(statement)) {
442
- return extractFromCapabilityTypeConstructor(statement, kind, projectPath);
476
+ function extractFromMessengerCapabilityTypeDeclaration(capabilityTypeDeclaration, projectPath) {
477
+ if (capabilityTypeDeclaration.bodyShape === 'constructor') {
478
+ return tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath);
443
479
  }
444
- // istanbul ignore next: interface declarations always have members and
445
- // therefore an inline shape, so the inline branch above always returns
446
- // non-null here.
447
- return null;
480
+ return tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath);
448
481
  }
449
482
  /**
450
- * Try the inline messenger-capability-type pattern:
451
- * `{ type: '...'; handler: ... }` (action) or
452
- * `{ type: '...'; payload: ... }` (event), expressed as either a type alias
453
- * body or an interface body.
483
+ * If a messenger capability type is a type alias or interface and its body is
484
+ * a literal object type i.e. one of:
485
+ *
486
+ * - `{ type: '...'; handler: ... }` (action)
487
+ * - `{ type: '...'; payload: ... }` (event)
488
+ *
489
+ * then this function extracts information about the type (action/event type
490
+ * string, handler/payload arguments and return type, etc.)
454
491
  *
455
- * @param statement - The type alias or interface declaration.
456
- * @param kind - The expected kind (action or event).
492
+ * @param capabilityTypeDeclaration - The statement that declared the type for a
493
+ * messenger action or event, extracted in a previous step.
457
494
  * @param projectPath - Project root, used for computing relative source paths.
458
- * @returns The extracted capability, or null if the shape doesn't match.
495
+ * @returns The extracted capability packet, or null if the shape of the type
496
+ * doesn't match.
459
497
  */
460
- function extractFromInlineMessengerCapabilityType(statement, kind, projectPath) {
498
+ function tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath) {
499
+ const { declaration, kind } = capabilityTypeDeclaration;
500
+ // We must have a object type alias or an interface, and the body must not be empty.
501
+ // EXAMPLES:
502
+ // // Good
503
+ // type FooControllerSomeAction = {
504
+ // type: 'FooController:getState';
505
+ // handler: FooController['getState'];
506
+ // }
507
+ // // Good
508
+ // interface FooControllerSomeAction {
509
+ // type: 'FooController:getState';
510
+ // handler: FooController['getState'];
511
+ // }
512
+ // // Bad
513
+ // type FooControllerSomeAction = {};
514
+ // // Bad
515
+ // interface FooControllerSomeAction {};
461
516
  let members;
462
- if (ts_morph_1.Node.isTypeAliasDeclaration(statement)) {
463
- const body = statement.getTypeNode();
517
+ if (ts_morph_1.Node.isTypeAliasDeclaration(declaration)) {
518
+ const body = declaration.getTypeNode();
464
519
  if (body && ts_morph_1.Node.isTypeLiteral(body)) {
465
520
  members = body.getMembers();
466
521
  }
467
522
  }
468
523
  else {
469
- members = statement.getMembers();
524
+ members = declaration.getMembers();
470
525
  }
471
526
  if (!members) {
472
527
  return null;
473
528
  }
474
- const typeString = resolveInlineTypeString(members);
475
- // istanbul ignore next: capabilities reachable via the messenger walk
476
- // always have a resolvable `Namespace:name` type string.
477
- if (!typeString?.includes(':')) {
529
+ const propertySignatures = members.filter(ts_morph_1.Node.isPropertySignature.bind(ts_morph_1.Node));
530
+ // Actions and events must have a `type`, and it must be a string.
531
+ const typeString = getMessengerCapabilityTypeString(propertySignatures);
532
+ if (!typeString) {
478
533
  return null;
479
534
  }
480
- const handlerMember = getPropertyMember(members, 'handler');
481
- const payloadMember = getPropertyMember(members, 'payload');
482
- const rawSourceMember = kind === 'action' ? handlerMember : payloadMember;
483
- // istanbul ignore next: actions always have `handler` and events always
484
- // have `payload`; the messenger walk wouldn't have surfaced this
485
- // declaration otherwise.
486
- if (!rawSourceMember) {
535
+ // Actions must have a `handler`, and events must have a `payload`.
536
+ const handlerOrPayloadProperty = findProperty(propertySignatures, kind === 'action' ? 'handler' : 'payload');
537
+ if (!handlerOrPayloadProperty) {
487
538
  return null;
488
539
  }
489
- const rawSourceTypeNode = rawSourceMember.getTypeNode();
490
- // istanbul ignore next: property signatures we care about always have an
491
- // explicit type.
492
- if (!rawSourceTypeNode) {
493
- return null;
494
- }
495
- let handlerOrPayload = rawSourceTypeNode.getText().trim();
496
- let { description: jsDoc, params, returns } = extractJsDoc(statement);
497
- // For actions, if the handler resolves to a class method (`Class['method']`
498
- // possibly in another file), inherit its signature plus any JSDoc fields
499
- // the type alias itself doesn't already provide.
540
+ const handlerOrPayloadPropertyTypeNode =
541
+ // We can assume the property has a type; otherwise it wouldn't compile.
542
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
543
+ handlerOrPayloadProperty.getTypeNode();
544
+ let handlerOrPayloadSignature = handlerOrPayloadPropertyTypeNode
545
+ .getText()
546
+ .trim();
547
+ const { description: jsDoc, params, returns } = extractJsDoc(declaration);
548
+ // For actions that represent methods (e.g. `Class['method']`), walk the
549
+ // handler type to find the underlying handler signature
550
+ // (e.g. `(id: number) => Promise<string>`).
500
551
  if (kind === 'action') {
501
- const resolvedMethod = resolveIndexedAccessMethod(rawSourceTypeNode);
502
- if (resolvedMethod) {
503
- const info = buildMethodInfo(resolvedMethod);
504
- handlerOrPayload = info.signature;
505
- if (!jsDoc && info.jsDoc) {
506
- jsDoc = info.jsDoc;
507
- }
508
- if (params.length === 0 && info.params.length > 0) {
509
- params = info.params;
510
- }
511
- if (!returns && info.returns) {
512
- returns = info.returns;
513
- }
552
+ const methodDeclaration = findClassMethodDeclaration(handlerOrPayloadPropertyTypeNode);
553
+ if (methodDeclaration) {
554
+ handlerOrPayloadSignature = buildMethodSignature(methodDeclaration);
514
555
  }
515
556
  }
516
- const sourceFile = statement.getSourceFile();
557
+ const sourceFile = declaration.getSourceFile();
517
558
  return {
518
- typeName: statement.getName(),
559
+ typeName: declaration.getName(),
519
560
  typeString,
520
561
  kind,
521
562
  jsDoc,
522
563
  params,
523
564
  returns,
524
- handlerOrPayload,
565
+ handlerOrPayload: handlerOrPayloadSignature,
525
566
  sourceFile: path.relative(projectPath, sourceFile.getFilePath()),
526
- line: statement.getStartLineNumber(),
527
- deprecated: isDeprecated(statement),
567
+ line: declaration.getStartLineNumber(),
568
+ deprecated: hasDeprecatedJsDocTag(declaration),
528
569
  };
529
570
  }
530
571
  /**
531
- * Resolve the literal value of an inline shape's `type` property — either a
532
- * direct string literal or a template literal type with `typeof X`
533
- * substitutions (resolved via the type checker).
572
+ * Searches the property signatures of a messenger capability type alias or
573
+ * interface to find the value of the `type` property, and then resolves it to a
574
+ * string (assuming it is already a string or a resolvable template literal).
534
575
  *
535
- * @param members - The type elements of the inline shape.
536
- * @returns The resolved type string, or null if it can't be resolved.
576
+ * @param capabilityTypeProperties - The property signatures of the messenger
577
+ * capability type.
578
+ * @returns The extracted capability type string, or null if `type` cannot be
579
+ * found in the members or it is an unexpected node.
537
580
  */
538
- function resolveInlineTypeString(members) {
539
- for (const member of members) {
540
- // istanbul ignore next: capability-type bodies only contain property
541
- // signatures.
542
- if (!ts_morph_1.Node.isPropertySignature(member)) {
543
- continue;
544
- }
545
- const memberNameNode = member.getNameNode();
546
- if (
547
- // istanbul ignore next: `type` is always a plain identifier.
548
- !ts_morph_1.Node.isIdentifier(memberNameNode) ||
549
- memberNameNode.getText() !== 'type') {
550
- continue;
551
- }
552
- const typeNode = member.getTypeNode();
553
- // istanbul ignore next: a `type` property without an explicit type
554
- // wouldn't compile.
555
- if (!typeNode) {
556
- continue;
557
- }
558
- if (ts_morph_1.Node.isLiteralTypeNode(typeNode)) {
559
- const literal = typeNode.getLiteral();
560
- // istanbul ignore next: messenger `type` fields are written as
561
- // quoted string literals in real fixtures; backtick template literals
562
- // are valid TypeScript but don't appear in practice.
563
- if (ts_morph_1.Node.isStringLiteral(literal) ||
564
- ts_morph_1.Node.isNoSubstitutionTemplateLiteral(literal)) {
565
- return literal.getLiteralValue();
566
- }
567
- // istanbul ignore next: numeric/boolean literal types aren't valid as a
568
- // messenger `type` and don't appear in real fixtures.
569
- return null;
570
- }
571
- if (ts_morph_1.Node.isTemplateLiteralTypeNode(typeNode)) {
572
- return resolveTemplateLiteralType(typeNode);
581
+ function getMessengerCapabilityTypeString(capabilityTypeProperties) {
582
+ const typeProperty = findProperty(capabilityTypeProperties, 'type');
583
+ if (!typeProperty) {
584
+ return null;
585
+ }
586
+ // A `type` property without an explicit type wouldn't compile.
587
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
588
+ const typeNode = typeProperty.getTypeNode();
589
+ // Ask the type checker to resolve the value of `type`. We're looking for
590
+ // `type` to be either a string literal or template literal.
591
+ //
592
+ // EXAMPLES:
593
+ // type FooControllerSomeAction = {
594
+ // type: 'FooController:someAction';
595
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^
596
+ // }
597
+ // type FooControllerSomeAction = {
598
+ // type: `FooController:someAction`;
599
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^
600
+ // }
601
+ // type FooControllerSomeAction = {
602
+ // type: `${typeof CONTROLLER_NAME}:someAction`;
603
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
604
+ // }
605
+ const resolvedType = typeNode.getType();
606
+ if (resolvedType.isStringLiteral()) {
607
+ // Type assertion: There aren't any type guards we can use to narrow this
608
+ // type further.
609
+ const literalValue = resolvedType.getLiteralValueOrThrow();
610
+ // Messenger action/event types need to be namespaced.
611
+ if (literalValue.includes(':')) {
612
+ return literalValue;
573
613
  }
574
614
  }
575
- // istanbul ignore next: the inline shape always has a `type` member that
576
- // produces either a literal or template-literal type.
577
615
  return null;
578
616
  }
579
617
  /**
580
- * Find the `PropertySignature` named `propName` in a type literal body.
618
+ * Finds a specific property in a list of property signatures for an object
619
+ * type.
581
620
  *
582
- * @param members - The type literal members to search.
583
- * @param propName - The property name to find.
621
+ * @param propertySignatures - The property signatures of the messenger
622
+ * capability type.
623
+ * @param name - The property name to find.
584
624
  * @returns The property signature, or null.
585
625
  */
586
- function getPropertyMember(members, propName) {
587
- for (const member of members) {
588
- // istanbul ignore next: capability-type bodies only contain property
589
- // signatures.
590
- if (!ts_morph_1.Node.isPropertySignature(member)) {
591
- continue;
592
- }
593
- const memberNameNode = member.getNameNode();
594
- if (
595
- // istanbul ignore next: capability properties always have identifier names.
596
- !ts_morph_1.Node.isIdentifier(memberNameNode) ||
597
- memberNameNode.getText() !== propName) {
626
+ function findProperty(propertySignatures, name) {
627
+ for (const property of propertySignatures) {
628
+ const propertyNameNode = property.getNameNode();
629
+ if (!ts_morph_1.Node.isIdentifier(propertyNameNode) ||
630
+ propertyNameNode.getText() !== name) {
598
631
  continue;
599
632
  }
600
- return member;
633
+ return property;
601
634
  }
602
635
  return null;
603
636
  }
604
637
  /**
605
- * Try the capability-type-constructor pattern:
606
- * `ControllerGetStateAction<typeof X, State>` for actions, or
607
- * `ControllerStateChangeEvent<typeof X, State>` for events.
638
+ * If a messenger capability type is a type alias for either the
639
+ * `ControllerGetStateAction` or `ControllerStateChangeEvent` type constructors,
640
+ * then this function extracts information about the type (action/event type
641
+ * string, handler/payload arguments and return type, etc.)
608
642
  *
609
- * @param statement - The type alias declaration.
610
- * @param kind - The expected kind (action or event).
643
+ * @param capabilityTypeDeclaration - The statement that declared the type for a
644
+ * messenger action or event, extracted in a previous step.
611
645
  * @param projectPath - Project root, used for computing relative source paths.
612
- * @returns The extracted capability, or null if the constructor doesn't match.
646
+ * @returns The extracted capability packet, or null if the shape of the type
647
+ * doesn't match.
613
648
  */
614
- function extractFromCapabilityTypeConstructor(statement, kind, projectPath) {
615
- const aliasBody = statement.getTypeNode();
616
- // istanbul ignore next: walker only records aliases whose body matches.
617
- if (!aliasBody || !ts_morph_1.Node.isTypeReference(aliasBody)) {
618
- return null;
619
- }
620
- const nameNode = aliasBody.getTypeName();
621
- // istanbul ignore next: qualified-name type references aren't used.
622
- if (!ts_morph_1.Node.isIdentifier(nameNode)) {
623
- return null;
624
- }
625
- const constructorName = nameNode.getText();
626
- const typeArgs = aliasBody.getTypeArguments();
627
- if (typeArgs.length < 2) {
628
- return null;
629
- }
649
+ function tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath) {
650
+ const { declaration, kind, body, typeName } = capabilityTypeDeclaration;
651
+ // The name of the utility type should be either `ControllerGetStateAction`
652
+ // (for actions) or `ControllerStateChangeEvent` (for events).
653
+ // EXAMPLES:
654
+ // type FooControllerSomeAction = ControllerGetStateAction<...>
655
+ // type FooControllerSomeEvent = ControllerStateChangeEvent<...>
630
656
  const expectedConstructor = kind === 'action'
631
657
  ? 'ControllerGetStateAction'
632
658
  : 'ControllerStateChangeEvent';
633
- if (constructorName !== expectedConstructor) {
659
+ if (typeName.getText() !== expectedConstructor) {
660
+ return null;
661
+ }
662
+ // The utility type should take two parameters.
663
+ // EXAMPLES:
664
+ // type FooControllerSomeAction = ControllerGetStateAction<..., ...>
665
+ // type FooControllerSomeEvent = ControllerStateChangeEvent<..., ...>
666
+ const typeArgs = body.getTypeArguments();
667
+ if (typeArgs.length < 2) {
634
668
  return null;
635
669
  }
636
- const namespace = resolveNamespaceFromTypeArg(typeArgs[0]);
637
- // istanbul ignore next: recognized constructors always have resolvable args.
638
- if (!namespace) {
670
+ const namespaceArgType = typeArgs[0].getType();
671
+ // The first parameter should be a string literal.
672
+ // EXAMPLES:
673
+ // type FooControllerSomeAction = ControllerGetStateAction<'FooController', ...>
674
+ // type FooControllerSomeAction = ControllerGetStateAction<typeof CONTROLLER_NAME, ...>
675
+ if (!namespaceArgType.isStringLiteral()) {
639
676
  return null;
640
677
  }
678
+ // Type assertion: There aren't any type guards we can use to narrow this type
679
+ // further.
680
+ const namespace = namespaceArgType.getLiteralValueOrThrow();
681
+ const typeString = kind === 'action' ? `${namespace}:getState` : `${namespace}:stateChange`;
641
682
  const stateArgText = typeArgs[1].getText();
642
- const { description, params, returns } = extractJsDoc(statement);
643
- const sourceFile = statement.getSourceFile();
683
+ const handlerOrPayload = kind === 'action' ? `() => ${stateArgText}` : `[${stateArgText}, Patch[]]`;
684
+ const { description, params, returns } = extractJsDoc(declaration);
685
+ const sourceFile = declaration.getSourceFile();
644
686
  return {
645
- typeName: statement.getName(),
646
- typeString: kind === 'action' ? `${namespace}:getState` : `${namespace}:stateChange`,
687
+ typeName: declaration.getName(),
688
+ typeString,
647
689
  kind,
648
690
  jsDoc: description,
649
691
  params,
650
692
  returns,
651
- handlerOrPayload: kind === 'action'
652
- ? `() => ${stateArgText}`
653
- : `[${stateArgText}, Patch[]]`,
693
+ handlerOrPayload,
654
694
  sourceFile: path.relative(projectPath, sourceFile.getFilePath()),
655
- line: statement.getStartLineNumber(),
656
- deprecated: isDeprecated(statement),
695
+ line: declaration.getStartLineNumber(),
696
+ deprecated: hasDeprecatedJsDocTag(declaration),
657
697
  };
658
698
  }
659
699
  // ---------------------------------------------------------------------------
@@ -686,60 +726,29 @@ function createExtractionProject() {
686
726
  }
687
727
  exports.createExtractionProject = createExtractionProject;
688
728
  /**
689
- * Extract every messenger action/event type reachable from a single source
690
- * file's `*Messenger` declarations.
729
+ * Extract information (action/event type string, handler/payload arguments and
730
+ * return type, etc.) about every messenger action or event type which is
731
+ * reachable through all of a source file's `*Messenger` type declarations.
691
732
  *
692
733
  * The caller is responsible for ensuring `sourceFile` (plus any files it
693
- * imports from) belongs to a ts-morph Project so cross-file symbol resolution
734
+ * imports from) belongs to a `ts-morph` Project so cross-file symbol resolution
694
735
  * works.
695
736
  *
696
737
  * @param sourceFile - The TypeScript source file to extract from.
697
738
  * @param projectPath - Project root, used for computing relative source paths.
698
- * @returns The extracted capability list.
739
+ * @returns The extracted information about actions and events.
699
740
  */
700
741
  function extractFromSourceFile(sourceFile, projectPath) {
701
- const declarations = collectMessengerCapabilityTypeDeclarations(sourceFile);
702
- if (declarations.length === 0) {
703
- return [];
704
- }
705
- const items = [];
706
- for (const { statement, kind } of declarations) {
707
- const item = extractFromMessengerCapabilityType(statement, kind, projectPath);
708
- if (item) {
709
- items.push(item);
742
+ const messengerTypeAliases = findMessengerTypeAliases(sourceFile);
743
+ const capabilityTypeDeclarations = findAllMessengerCapabilityTypeDeclarations(messengerTypeAliases);
744
+ const messengerCapabilityPackets = [];
745
+ for (const capabilityTypeDeclaration of capabilityTypeDeclarations) {
746
+ const messengerCapabilityPacket = extractFromMessengerCapabilityTypeDeclaration(capabilityTypeDeclaration, projectPath);
747
+ if (messengerCapabilityPacket) {
748
+ messengerCapabilityPackets.push(messengerCapabilityPacket);
710
749
  }
711
750
  }
712
- return items;
751
+ return messengerCapabilityPackets;
713
752
  }
714
753
  exports.extractFromSourceFile = extractFromSourceFile;
715
- /**
716
- * Convenience wrapper: extract from a single file by path. Loads the file
717
- * and any sibling files in its parent directory into a fresh Project so
718
- * relative imports resolve.
719
- *
720
- * For batch operations across many files, prefer
721
- * {@link createExtractionProject} + {@link extractFromSourceFile} so one
722
- * Project amortizes the type-checker setup across the whole run.
723
- *
724
- * @param filePath - The absolute path to the TypeScript file.
725
- * @param projectPath - Base path for computing relative source paths.
726
- * @returns A promise that resolves to the extracted capability list.
727
- */
728
- async function extractFromFile(filePath, projectPath) {
729
- const project = createExtractionProject();
730
- const parentDir = path.dirname(filePath);
731
- // Load the file's directory so single-hop relative imports resolve.
732
- project.addSourceFilesAtPaths([
733
- path.join(parentDir, '**/*.ts'),
734
- path.join(parentDir, '**/*.d.cts'),
735
- ]);
736
- const sourceFile = project.getSourceFile(filePath);
737
- // istanbul ignore next: the path always exists since the caller just
738
- // wrote the file or it was scanned from disk.
739
- if (!sourceFile) {
740
- return [];
741
- }
742
- return extractFromSourceFile(sourceFile, projectPath);
743
- }
744
- exports.extractFromFile = extractFromFile;
745
754
  //# sourceMappingURL=extraction.cjs.map