@metamask-previews/platform-api-docs 0.0.0-preview-6235c3779 → 0.0.0-preview-a3545aef8

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,7 +23,7 @@ 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
  // ---------------------------------------------------------------------------
@@ -146,57 +146,63 @@ function hasDeprecatedJsDocTag(node) {
146
146
  // Type-resolution helpers (powered by ts-morph's type checker)
147
147
  // ---------------------------------------------------------------------------
148
148
  /**
149
- * If `typeNode` is `Class['method']`, resolve `Class` to its declaration
150
- * (across files if needed via the type checker) and look up the method by
151
- * name. Returns the method declaration when found.
149
+ * Locates the type that represents a method on a class (e.g.
150
+ * `Class['method']`), which itself comes from a messenger action handler.
152
151
  *
153
- * @param typeNode - The handler's type node.
154
- * @returns The method declaration, or null.
152
+ * @param typeNode - The node that represents the indexed access.
153
+ * @returns The found method declaration, or null.
155
154
  */
156
- function resolveIndexedAccessMethod(typeNode) {
157
- 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)) {
158
158
  return null;
159
159
  }
160
+ // The type that represents the class being accessed.
161
+ // EXAMPLE:
162
+ // FooController['someMethod']
163
+ // ^^^^^^^^^^^^^
160
164
  const objectType = typeNode.getObjectTypeNode();
165
+ // The type that represents the property being accessed.
166
+ // EXAMPLE:
167
+ // FooController['someMethod']
168
+ // ^^^^^^^^^^^^
161
169
  const indexType = typeNode.getIndexTypeNode();
162
- // istanbul ignore next: handler indexed-access types in messenger fixtures
163
- // always reference a class via TypeReference.
164
- if (!ts_morph_1.Node.isTypeReference(objectType)) {
165
- return null;
166
- }
167
- // istanbul ignore next: the index in `Class['method']` is always a literal
168
- // type node in valid handler syntax.
169
- 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)) {
170
173
  return null;
171
174
  }
172
175
  const indexLiteral = indexType.getLiteral();
173
- // The index can be written as `'method'`, `"method"`, or `` `method` ``.
174
- // The first two land as `StringLiteral`; the bare template literal is a
175
- // `NoSubstitutionTemplateLiteral`.
176
- // istanbul ignore next: numeric/boolean indices aren't valid method names.
176
+ // Names of methods must be static strings; they cannot be template strings.
177
177
  if (!ts_morph_1.Node.isStringLiteral(indexLiteral) &&
178
178
  !ts_morph_1.Node.isNoSubstitutionTemplateLiteral(indexLiteral)) {
179
179
  return null;
180
180
  }
181
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
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^
182
188
  const classNameNode = objectType.getTypeName();
183
- // istanbul ignore next: qualified-name class references aren't used in
184
- // messenger handler types.
185
189
  if (!ts_morph_1.Node.isIdentifier(classNameNode)) {
186
190
  return null;
187
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
188
194
  const localSymbol = classNameNode.getSymbol();
189
- // istanbul ignore next: a referenced class name always resolves to a symbol
190
- // in a typechecked project.
191
- if (!localSymbol) {
192
- return null;
193
- }
194
- // Follow the import alias (if any) so we can find the class declaration in
195
- // 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
+ // ^^^^^^^^^^^^^
196
202
  const symbol = localSymbol.getAliasedSymbol() ?? localSymbol;
197
- // istanbul ignore next: a resolved symbol always exposes its declarations
198
- // array in a typechecked project.
199
- 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.
200
206
  if (ts_morph_1.Node.isClassDeclaration(declaration)) {
201
207
  const method = declaration.getMethod(methodName);
202
208
  if (method) {
@@ -204,40 +210,37 @@ function resolveIndexedAccessMethod(typeNode) {
204
210
  }
205
211
  }
206
212
  }
207
- // istanbul ignore next: only reached if the indexed method isn't declared
208
- // on any of the resolved class declarations, which doesn't happen in valid
209
- // handler types.
210
213
  return null;
211
214
  }
212
215
  // ---------------------------------------------------------------------------
213
216
  // Method info
214
217
  // ---------------------------------------------------------------------------
215
218
  /**
216
- * Build a {@link MethodInfo} record for a class method — its textual
217
- * signature plus the JSDoc description, `@param`, and `@returns` extracted
218
- * 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.
219
223
  *
220
224
  * @param method - The method declaration.
221
- * @returns The method info.
225
+ * @returns The signature, e.g. `(id: number) => Promise<string>`.
222
226
  */
223
- function buildMethodInfo(method) {
227
+ function buildMethodSignature(method) {
224
228
  const signatureParams = method
225
229
  .getParameters()
226
230
  .map((param) => {
231
+ const rest = param.isRestParameter() ? '...' : '';
227
232
  const paramName = param.getNameNode().getText();
228
233
  const optional = param.hasQuestionToken() ? '?' : '';
229
234
  const typeNode = param.getTypeNode();
230
235
  const paramType = typeNode ? typeNode.getText() : 'unknown';
231
- return `${paramName}${optional}: ${paramType}`;
236
+ return `${rest}${paramName}${optional}: ${paramType}`;
232
237
  })
233
238
  .join(', ');
234
239
  const returnTypeNode = method.getReturnTypeNode();
235
240
  const returnType = returnTypeNode ? returnTypeNode.getText() : 'void';
236
241
  // For async methods, the declared return type already includes `Promise<>`,
237
242
  // so we don't need to wrap again.
238
- const signature = `(${signatureParams}) => ${returnType}`;
239
- const { description, params, returns } = extractJsDoc(method);
240
- return { jsDoc: description, params, returns, signature };
243
+ return `(${signatureParams}) => ${returnType}`;
241
244
  }
242
245
  /**
243
246
  * Looks for Messenger types in the source file (that is, those that are type
@@ -338,13 +341,16 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
338
341
  // ^^^^^^^
339
342
  // // Good
340
343
  // type Actions = FooControllerSomeAction;
344
+ // ^^^^^^^^^^^^^^^^^^^^^^^
341
345
  // // Good
342
346
  // type Actions = ControllerGetStateAction<...>;
347
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
343
348
  if (!ts_morph_1.Node.isTypeReference(node)) {
344
349
  return result;
345
350
  }
346
351
  const nameNode = node.getTypeName();
347
- // Reject qualified-name type references, as we can't follow those.
352
+ // Reject qualified-name type names, as we need a plain identifier to
353
+ // resolve the symbol.
348
354
  // EXAMPLE:
349
355
  // import * as somePackage from '...';
350
356
  // type Actions = somePackage.FooControllerSomeAction;
@@ -356,8 +362,8 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
356
362
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
357
363
  const localSymbol = nameNode.getSymbol();
358
364
  // If we have a type imported from another file, ensure that when we access
359
- // the declaration, it's the *type* declaration in the other file, not the
360
- // *import* declaration in this file.
365
+ // the declaration, it's the type declaration in the other file, not the
366
+ // import declaration in this file.
361
367
  // EXAMPLE:
362
368
  // import { FooControllerSomeAction } from '@metamask/foo-controller';
363
369
  // type Actions = FooControllerSomeAction;
@@ -376,14 +382,22 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
376
382
  // If we have a type alias, then we have to handle a few scenarios.
377
383
  // EXAMPLES:
378
384
  // type FooControllerMethodActions = ...
385
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
379
386
  // type FooControllerSomeAction = ...
387
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
380
388
  if (ts_morph_1.Node.isTypeAliasDeclaration(declaration)) {
381
389
  const body = declaration.getTypeNode();
382
- // If we have a union type, then walk each type in the union.
383
- // EXAMPLE:
390
+ // If the body is a union type or a plain type reference (not a utility
391
+ // type), walk it.
392
+ // EXAMPLES:
384
393
  // type FooControllerMethodActions = FooControllerSomeAction | FooControllerSomeOtherAction
385
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
386
- if (body && ts_morph_1.Node.isUnionTypeNode(body)) {
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))) {
387
401
  const innerResult = recursivelyFindMessengerCapabilityTypeDeclarations(body, kind, result.visitedTypeDeclarations);
388
402
  result.capabilityTypeDeclarations.push(...innerResult.capabilityTypeDeclarations);
389
403
  for (const typeDeclaration of innerResult.visitedTypeDeclarations) {
@@ -391,32 +405,56 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
391
405
  }
392
406
  continue;
393
407
  }
394
- // TODO: Is this necessary?
395
- // A bare TypeReference body with no type arguments (e.g.
396
- // `type AllowedActions = ConnectivityControllerGetStateAction`) is a
397
- // plain re-export — leave the target to be documented from its home
398
- // package, not this one. TypeReferences *with* type arguments are
399
- // capability-type-constructor invocations (e.g.
400
- // `ControllerGetStateAction<typeof name, State>`) and are recorded.
401
- if (body &&
402
- ts_morph_1.Node.isTypeReference(body) &&
403
- body.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
+ }
404
433
  continue;
405
434
  }
406
- // We finally found a messenger capability type! Capture the whole thing.
407
- // (We will parse the body later.)
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.
408
438
  // EXAMPLE:
409
439
  // type FooControllerSomeAction = { ... }
410
440
  // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
411
- result.capabilityTypeDeclarations.push({ declaration, kind });
441
+ result.capabilityTypeDeclarations.push({
442
+ bodyShape: 'object',
443
+ kind,
444
+ declaration,
445
+ });
412
446
  }
413
- // If we have an interface, we don't have any scenarios to handle, so
414
- // capture it as is.
447
+ // Interfaces always carry their members directly tag for the literal
448
+ // extractor.
415
449
  // EXAMPLE:
416
450
  // interface FooControllerSomeAction { ... }
417
451
  // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
418
452
  else if (ts_morph_1.Node.isInterfaceDeclaration(declaration)) {
419
- result.capabilityTypeDeclarations.push({ declaration, kind });
453
+ result.capabilityTypeDeclarations.push({
454
+ bodyShape: 'object',
455
+ kind,
456
+ declaration,
457
+ });
420
458
  }
421
459
  }
422
460
  return result;
@@ -436,8 +474,10 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
436
474
  * (may be `null` if the type is ineligible for extraction).
437
475
  */
438
476
  function extractFromMessengerCapabilityTypeDeclaration(capabilityTypeDeclaration, projectPath) {
439
- return (tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath) ??
440
- tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath));
477
+ if (capabilityTypeDeclaration.bodyShape === 'constructor') {
478
+ return tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath);
479
+ }
480
+ return tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath);
441
481
  }
442
482
  /**
443
483
  * If a messenger capability type is a type alias or interface and its body is
@@ -457,7 +497,7 @@ function extractFromMessengerCapabilityTypeDeclaration(capabilityTypeDeclaration
457
497
  */
458
498
  function tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath) {
459
499
  const { declaration, kind } = capabilityTypeDeclaration;
460
- // Reject empty type aliases or interfaces.
500
+ // We must have a object type alias or an interface, and the body must not be empty.
461
501
  // EXAMPLES:
462
502
  // // Good
463
503
  // type FooControllerSomeAction = {
@@ -504,25 +544,14 @@ function tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaratio
504
544
  let handlerOrPayloadSignature = handlerOrPayloadPropertyTypeNode
505
545
  .getText()
506
546
  .trim();
507
- let { description: jsDoc, params, returns } = extractJsDoc(declaration);
508
- // For actions, if the handler resolves to a class method (e.g.
509
- // `FooController['method']`), inherit its signature plus any JSDoc fields the
510
- // type alias itself doesn't already provide.
511
- // TODO: We don't want to do this.
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>`).
512
551
  if (kind === 'action') {
513
- const resolvedMethod = resolveIndexedAccessMethod(handlerOrPayloadPropertyTypeNode);
514
- if (resolvedMethod) {
515
- const info = buildMethodInfo(resolvedMethod);
516
- handlerOrPayloadSignature = info.signature;
517
- if (!jsDoc && info.jsDoc) {
518
- jsDoc = info.jsDoc;
519
- }
520
- if (params.length === 0 && info.params.length > 0) {
521
- params = info.params;
522
- }
523
- if (!returns && info.returns) {
524
- returns = info.returns;
525
- }
552
+ const methodDeclaration = findClassMethodDeclaration(handlerOrPayloadPropertyTypeNode);
553
+ if (methodDeclaration) {
554
+ handlerOrPayloadSignature = buildMethodSignature(methodDeclaration);
526
555
  }
527
556
  }
528
557
  const sourceFile = declaration.getSourceFile();
@@ -571,7 +600,7 @@ function getMessengerCapabilityTypeString(capabilityTypeProperties) {
571
600
  // }
572
601
  // type FooControllerSomeAction = {
573
602
  // type: `${typeof CONTROLLER_NAME}:someAction`;
574
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
603
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
575
604
  // }
576
605
  const resolvedType = typeNode.getType();
577
606
  if (resolvedType.isStringLiteral()) {
@@ -618,37 +647,7 @@ function findProperty(propertySignatures, name) {
618
647
  * doesn't match.
619
648
  */
620
649
  function tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath) {
621
- const { declaration, kind } = capabilityTypeDeclaration;
622
- // Basic check: we need a type alias, otherwise the rest doesn't make sense.
623
- // EXAMPLE:
624
- // type FooControllerSomeAction = ...
625
- if (!ts_morph_1.Node.isTypeAliasDeclaration(declaration)) {
626
- return null;
627
- }
628
- // We want our type alias to be a reference to a utility type.
629
- // Non-null assertion: We can assume the type has a body of some kind,
630
- // otherwise it wouldn't compile.
631
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
632
- const body = declaration.getTypeNode();
633
- // We already determined in
634
- // `recursivelyFindMessengerCapabilityTypeDeclarations` that the declaration
635
- // is a type reference.
636
- // TODO: Capture the following information ahead of time so we don't need to
637
- // check this again.
638
- // istanbul ignore next
639
- if (!ts_morph_1.Node.isTypeReference(body)) {
640
- return null;
641
- }
642
- const typeName = body.getTypeName();
643
- // We already determined in
644
- // `recursivelyFindMessengerCapabilityTypeDeclarations` that the type is an
645
- // identifier.
646
- // TODO: Capture the following information ahead of time so we don't need to
647
- // check this again.
648
- // istanbul ignore next
649
- if (!ts_morph_1.Node.isIdentifier(typeName)) {
650
- return null;
651
- }
650
+ const { declaration, kind, body, typeName } = capabilityTypeDeclaration;
652
651
  // The name of the utility type should be either `ControllerGetStateAction`
653
652
  // (for actions) or `ControllerStateChangeEvent` (for events).
654
653
  // EXAMPLES:
@@ -752,34 +751,4 @@ function extractFromSourceFile(sourceFile, projectPath) {
752
751
  return messengerCapabilityPackets;
753
752
  }
754
753
  exports.extractFromSourceFile = extractFromSourceFile;
755
- /**
756
- * Convenience wrapper: extract from a single file by path. Loads the file
757
- * and any sibling files in its parent directory into a fresh Project so
758
- * relative imports resolve.
759
- *
760
- * For batch operations across many files, prefer
761
- * {@link createExtractionProject} + {@link extractFromSourceFile} so one
762
- * Project amortizes the type-checker setup across the whole run.
763
- *
764
- * @param filePath - The absolute path to the TypeScript file.
765
- * @param projectPath - Base path for computing relative source paths.
766
- * @returns A promise that resolves to the extracted capability list.
767
- */
768
- async function extractFromFile(filePath, projectPath) {
769
- const project = createExtractionProject();
770
- const parentDir = path.dirname(filePath);
771
- // Load the file's directory so single-hop relative imports resolve.
772
- project.addSourceFilesAtPaths([
773
- path.join(parentDir, '**/*.ts'),
774
- path.join(parentDir, '**/*.d.cts'),
775
- ]);
776
- const sourceFile = project.getSourceFile(filePath);
777
- // istanbul ignore next: the path always exists since the caller just
778
- // wrote the file or it was scanned from disk.
779
- if (!sourceFile) {
780
- return [];
781
- }
782
- return extractFromSourceFile(sourceFile, projectPath);
783
- }
784
- exports.extractFromFile = extractFromFile;
785
754
  //# sourceMappingURL=extraction.cjs.map