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