@metamask-previews/platform-api-docs 0.0.0-preview-6235c3779 → 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.
@@ -120,57 +120,63 @@ function hasDeprecatedJsDocTag(node) {
120
120
  // Type-resolution helpers (powered by ts-morph's type checker)
121
121
  // ---------------------------------------------------------------------------
122
122
  /**
123
- * If `typeNode` is `Class['method']`, resolve `Class` to its declaration
124
- * (across files if needed via the type checker) and look up the method by
125
- * name. Returns the method declaration when found.
123
+ * Locates the type that represents a method on a class (e.g.
124
+ * `Class['method']`), which itself comes from a messenger action handler.
126
125
  *
127
- * @param typeNode - The handler's type node.
128
- * @returns The method declaration, or null.
126
+ * @param typeNode - The node that represents the indexed access.
127
+ * @returns The found method declaration, or null.
129
128
  */
130
- function resolveIndexedAccessMethod(typeNode) {
131
- 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)) {
132
132
  return null;
133
133
  }
134
+ // The type that represents the class being accessed.
135
+ // EXAMPLE:
136
+ // FooController['someMethod']
137
+ // ^^^^^^^^^^^^^
134
138
  const objectType = typeNode.getObjectTypeNode();
139
+ // The type that represents the property being accessed.
140
+ // EXAMPLE:
141
+ // FooController['someMethod']
142
+ // ^^^^^^^^^^^^
135
143
  const indexType = typeNode.getIndexTypeNode();
136
- // istanbul ignore next: handler indexed-access types in messenger fixtures
137
- // always reference a class via TypeReference.
138
- if (!NodeGuards.isTypeReference(objectType)) {
139
- return null;
140
- }
141
- // istanbul ignore next: the index in `Class['method']` is always a literal
142
- // type node in valid handler syntax.
143
- 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)) {
144
147
  return null;
145
148
  }
146
149
  const indexLiteral = indexType.getLiteral();
147
- // The index can be written as `'method'`, `"method"`, or `` `method` ``.
148
- // The first two land as `StringLiteral`; the bare template literal is a
149
- // `NoSubstitutionTemplateLiteral`.
150
- // istanbul ignore next: numeric/boolean indices aren't valid method names.
150
+ // Names of methods must be static strings; they cannot be template strings.
151
151
  if (!NodeGuards.isStringLiteral(indexLiteral) &&
152
152
  !NodeGuards.isNoSubstitutionTemplateLiteral(indexLiteral)) {
153
153
  return null;
154
154
  }
155
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
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^
156
162
  const classNameNode = objectType.getTypeName();
157
- // istanbul ignore next: qualified-name class references aren't used in
158
- // messenger handler types.
159
163
  if (!NodeGuards.isIdentifier(classNameNode)) {
160
164
  return null;
161
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
162
168
  const localSymbol = classNameNode.getSymbol();
163
- // istanbul ignore next: a referenced class name always resolves to a symbol
164
- // in a typechecked project.
165
- if (!localSymbol) {
166
- return null;
167
- }
168
- // Follow the import alias (if any) so we can find the class declaration in
169
- // 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
+ // ^^^^^^^^^^^^^
170
176
  const symbol = localSymbol.getAliasedSymbol() ?? localSymbol;
171
- // istanbul ignore next: a resolved symbol always exposes its declarations
172
- // array in a typechecked project.
173
- 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.
174
180
  if (NodeGuards.isClassDeclaration(declaration)) {
175
181
  const method = declaration.getMethod(methodName);
176
182
  if (method) {
@@ -178,40 +184,37 @@ function resolveIndexedAccessMethod(typeNode) {
178
184
  }
179
185
  }
180
186
  }
181
- // istanbul ignore next: only reached if the indexed method isn't declared
182
- // on any of the resolved class declarations, which doesn't happen in valid
183
- // handler types.
184
187
  return null;
185
188
  }
186
189
  // ---------------------------------------------------------------------------
187
190
  // Method info
188
191
  // ---------------------------------------------------------------------------
189
192
  /**
190
- * Build a {@link MethodInfo} record for a class method — its textual
191
- * signature plus the JSDoc description, `@param`, and `@returns` extracted
192
- * 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.
193
197
  *
194
198
  * @param method - The method declaration.
195
- * @returns The method info.
199
+ * @returns The signature, e.g. `(id: number) => Promise<string>`.
196
200
  */
197
- function buildMethodInfo(method) {
201
+ function buildMethodSignature(method) {
198
202
  const signatureParams = method
199
203
  .getParameters()
200
204
  .map((param) => {
205
+ const rest = param.isRestParameter() ? '...' : '';
201
206
  const paramName = param.getNameNode().getText();
202
207
  const optional = param.hasQuestionToken() ? '?' : '';
203
208
  const typeNode = param.getTypeNode();
204
209
  const paramType = typeNode ? typeNode.getText() : 'unknown';
205
- return `${paramName}${optional}: ${paramType}`;
210
+ return `${rest}${paramName}${optional}: ${paramType}`;
206
211
  })
207
212
  .join(', ');
208
213
  const returnTypeNode = method.getReturnTypeNode();
209
214
  const returnType = returnTypeNode ? returnTypeNode.getText() : 'void';
210
215
  // For async methods, the declared return type already includes `Promise<>`,
211
216
  // so we don't need to wrap again.
212
- const signature = `(${signatureParams}) => ${returnType}`;
213
- const { description, params, returns } = extractJsDoc(method);
214
- return { jsDoc: description, params, returns, signature };
217
+ return `(${signatureParams}) => ${returnType}`;
215
218
  }
216
219
  /**
217
220
  * Looks for Messenger types in the source file (that is, those that are type
@@ -312,13 +315,16 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
312
315
  // ^^^^^^^
313
316
  // // Good
314
317
  // type Actions = FooControllerSomeAction;
318
+ // ^^^^^^^^^^^^^^^^^^^^^^^
315
319
  // // Good
316
320
  // type Actions = ControllerGetStateAction<...>;
321
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
317
322
  if (!NodeGuards.isTypeReference(node)) {
318
323
  return result;
319
324
  }
320
325
  const nameNode = node.getTypeName();
321
- // Reject qualified-name type references, as we can't follow those.
326
+ // Reject qualified-name type names, as we need a plain identifier to
327
+ // resolve the symbol.
322
328
  // EXAMPLE:
323
329
  // import * as somePackage from '...';
324
330
  // type Actions = somePackage.FooControllerSomeAction;
@@ -330,8 +336,8 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
330
336
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
331
337
  const localSymbol = nameNode.getSymbol();
332
338
  // If we have a type imported from another file, ensure that when we access
333
- // the declaration, it's the *type* declaration in the other file, not the
334
- // *import* declaration in this file.
339
+ // the declaration, it's the type declaration in the other file, not the
340
+ // import declaration in this file.
335
341
  // EXAMPLE:
336
342
  // import { FooControllerSomeAction } from '@metamask/foo-controller';
337
343
  // type Actions = FooControllerSomeAction;
@@ -350,14 +356,22 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
350
356
  // If we have a type alias, then we have to handle a few scenarios.
351
357
  // EXAMPLES:
352
358
  // type FooControllerMethodActions = ...
359
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
353
360
  // type FooControllerSomeAction = ...
361
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
354
362
  if (NodeGuards.isTypeAliasDeclaration(declaration)) {
355
363
  const body = declaration.getTypeNode();
356
- // If we have a union type, then walk each type in the union.
357
- // EXAMPLE:
364
+ // If the body is a union type or a plain type reference (not a utility
365
+ // type), walk it.
366
+ // EXAMPLES:
358
367
  // type FooControllerMethodActions = FooControllerSomeAction | FooControllerSomeOtherAction
359
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
360
- if (body && NodeGuards.isUnionTypeNode(body)) {
368
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
369
+ // type DelegationControllerMethodActions = DelegationControllerSignDelegationAction
370
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
371
+ if (body &&
372
+ (NodeGuards.isUnionTypeNode(body) ||
373
+ (NodeGuards.isTypeReference(body) &&
374
+ body.getTypeArguments().length === 0))) {
361
375
  const innerResult = recursivelyFindMessengerCapabilityTypeDeclarations(body, kind, result.visitedTypeDeclarations);
362
376
  result.capabilityTypeDeclarations.push(...innerResult.capabilityTypeDeclarations);
363
377
  for (const typeDeclaration of innerResult.visitedTypeDeclarations) {
@@ -365,32 +379,56 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
365
379
  }
366
380
  continue;
367
381
  }
368
- // TODO: Is this necessary?
369
- // A bare TypeReference body with no type arguments (e.g.
370
- // `type AllowedActions = ConnectivityControllerGetStateAction`) is a
371
- // plain re-export — leave the target to be documented from its home
372
- // package, not this one. TypeReferences *with* type arguments are
373
- // capability-type-constructor invocations (e.g.
374
- // `ControllerGetStateAction<typeof name, State>`) and are recorded.
375
- if (body &&
376
- NodeGuards.isTypeReference(body) &&
377
- body.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
+ }
378
407
  continue;
379
408
  }
380
- // We finally found a messenger capability type! Capture the whole thing.
381
- // (We will parse the body later.)
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.
382
412
  // EXAMPLE:
383
413
  // type FooControllerSomeAction = { ... }
384
414
  // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
385
- result.capabilityTypeDeclarations.push({ declaration, kind });
415
+ result.capabilityTypeDeclarations.push({
416
+ bodyShape: 'object',
417
+ kind,
418
+ declaration,
419
+ });
386
420
  }
387
- // If we have an interface, we don't have any scenarios to handle, so
388
- // capture it as is.
421
+ // Interfaces always carry their members directly tag for the literal
422
+ // extractor.
389
423
  // EXAMPLE:
390
424
  // interface FooControllerSomeAction { ... }
391
425
  // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
392
426
  else if (NodeGuards.isInterfaceDeclaration(declaration)) {
393
- result.capabilityTypeDeclarations.push({ declaration, kind });
427
+ result.capabilityTypeDeclarations.push({
428
+ bodyShape: 'object',
429
+ kind,
430
+ declaration,
431
+ });
394
432
  }
395
433
  }
396
434
  return result;
@@ -410,8 +448,10 @@ function recursivelyFindMessengerCapabilityTypeDeclarations(node, kind, visitedT
410
448
  * (may be `null` if the type is ineligible for extraction).
411
449
  */
412
450
  function extractFromMessengerCapabilityTypeDeclaration(capabilityTypeDeclaration, projectPath) {
413
- return (tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath) ??
414
- tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath));
451
+ if (capabilityTypeDeclaration.bodyShape === 'constructor') {
452
+ return tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath);
453
+ }
454
+ return tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath);
415
455
  }
416
456
  /**
417
457
  * If a messenger capability type is a type alias or interface and its body is
@@ -431,7 +471,7 @@ function extractFromMessengerCapabilityTypeDeclaration(capabilityTypeDeclaration
431
471
  */
432
472
  function tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaration, projectPath) {
433
473
  const { declaration, kind } = capabilityTypeDeclaration;
434
- // Reject empty type aliases or interfaces.
474
+ // We must have a object type alias or an interface, and the body must not be empty.
435
475
  // EXAMPLES:
436
476
  // // Good
437
477
  // type FooControllerSomeAction = {
@@ -478,25 +518,14 @@ function tryToExtractFromMessengerCapabilityTypeLiteral(capabilityTypeDeclaratio
478
518
  let handlerOrPayloadSignature = handlerOrPayloadPropertyTypeNode
479
519
  .getText()
480
520
  .trim();
481
- let { description: jsDoc, params, returns } = extractJsDoc(declaration);
482
- // For actions, if the handler resolves to a class method (e.g.
483
- // `FooController['method']`), inherit its signature plus any JSDoc fields the
484
- // type alias itself doesn't already provide.
485
- // TODO: We don't want to do this.
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>`).
486
525
  if (kind === 'action') {
487
- const resolvedMethod = resolveIndexedAccessMethod(handlerOrPayloadPropertyTypeNode);
488
- if (resolvedMethod) {
489
- const info = buildMethodInfo(resolvedMethod);
490
- handlerOrPayloadSignature = info.signature;
491
- if (!jsDoc && info.jsDoc) {
492
- jsDoc = info.jsDoc;
493
- }
494
- if (params.length === 0 && info.params.length > 0) {
495
- params = info.params;
496
- }
497
- if (!returns && info.returns) {
498
- returns = info.returns;
499
- }
526
+ const methodDeclaration = findClassMethodDeclaration(handlerOrPayloadPropertyTypeNode);
527
+ if (methodDeclaration) {
528
+ handlerOrPayloadSignature = buildMethodSignature(methodDeclaration);
500
529
  }
501
530
  }
502
531
  const sourceFile = declaration.getSourceFile();
@@ -545,7 +574,7 @@ function getMessengerCapabilityTypeString(capabilityTypeProperties) {
545
574
  // }
546
575
  // type FooControllerSomeAction = {
547
576
  // type: `${typeof CONTROLLER_NAME}:someAction`;
548
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
577
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
549
578
  // }
550
579
  const resolvedType = typeNode.getType();
551
580
  if (resolvedType.isStringLiteral()) {
@@ -592,37 +621,7 @@ function findProperty(propertySignatures, name) {
592
621
  * doesn't match.
593
622
  */
594
623
  function tryToExtractFromCapabilityTypeConstructor(capabilityTypeDeclaration, projectPath) {
595
- const { declaration, kind } = capabilityTypeDeclaration;
596
- // Basic check: we need a type alias, otherwise the rest doesn't make sense.
597
- // EXAMPLE:
598
- // type FooControllerSomeAction = ...
599
- if (!NodeGuards.isTypeAliasDeclaration(declaration)) {
600
- return null;
601
- }
602
- // We want our type alias to be a reference to a utility type.
603
- // Non-null assertion: We can assume the type has a body of some kind,
604
- // otherwise it wouldn't compile.
605
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
606
- const body = declaration.getTypeNode();
607
- // We already determined in
608
- // `recursivelyFindMessengerCapabilityTypeDeclarations` that the declaration
609
- // is a type reference.
610
- // TODO: Capture the following information ahead of time so we don't need to
611
- // check this again.
612
- // istanbul ignore next
613
- if (!NodeGuards.isTypeReference(body)) {
614
- return null;
615
- }
616
- const typeName = body.getTypeName();
617
- // We already determined in
618
- // `recursivelyFindMessengerCapabilityTypeDeclarations` that the type is an
619
- // identifier.
620
- // TODO: Capture the following information ahead of time so we don't need to
621
- // check this again.
622
- // istanbul ignore next
623
- if (!NodeGuards.isIdentifier(typeName)) {
624
- return null;
625
- }
624
+ const { declaration, kind, body, typeName } = capabilityTypeDeclaration;
626
625
  // The name of the utility type should be either `ControllerGetStateAction`
627
626
  // (for actions) or `ControllerStateChangeEvent` (for events).
628
627
  // EXAMPLES:
@@ -724,33 +723,4 @@ export function extractFromSourceFile(sourceFile, projectPath) {
724
723
  }
725
724
  return messengerCapabilityPackets;
726
725
  }
727
- /**
728
- * Convenience wrapper: extract from a single file by path. Loads the file
729
- * and any sibling files in its parent directory into a fresh Project so
730
- * relative imports resolve.
731
- *
732
- * For batch operations across many files, prefer
733
- * {@link createExtractionProject} + {@link extractFromSourceFile} so one
734
- * Project amortizes the type-checker setup across the whole run.
735
- *
736
- * @param filePath - The absolute path to the TypeScript file.
737
- * @param projectPath - Base path for computing relative source paths.
738
- * @returns A promise that resolves to the extracted capability list.
739
- */
740
- export async function extractFromFile(filePath, projectPath) {
741
- const project = createExtractionProject();
742
- const parentDir = path.dirname(filePath);
743
- // Load the file's directory so single-hop relative imports resolve.
744
- project.addSourceFilesAtPaths([
745
- path.join(parentDir, '**/*.ts'),
746
- path.join(parentDir, '**/*.d.cts'),
747
- ]);
748
- const sourceFile = project.getSourceFile(filePath);
749
- // istanbul ignore next: the path always exists since the caller just
750
- // wrote the file or it was scanned from disk.
751
- if (!sourceFile) {
752
- return [];
753
- }
754
- return extractFromSourceFile(sourceFile, projectPath);
755
- }
756
726
  //# sourceMappingURL=extraction.mjs.map