@@ -49,18 +49,30 @@ function extractStringConstants(
4949 continue ;
5050 }
5151 for ( const decl of statement . declarationList . declarations ) {
52- if ( ! ts . isIdentifier ( decl . name ) || ! decl . initializer ) {
52+ if ( ! ts . isIdentifier ( decl . name ) ) {
5353 continue ;
5454 }
5555
56- const init = decl . initializer ;
57- if ( ts . isStringLiteral ( init ) ) {
58- names . set ( decl . name . text , init . text ) ;
59- } else if (
60- ts . isAsExpression ( init ) &&
61- ts . isStringLiteral ( init . expression )
56+ if ( decl . initializer ) {
57+ const init = decl . initializer ;
58+ if ( ts . isStringLiteral ( init ) ) {
59+ names . set ( decl . name . text , init . text ) ;
60+ } else if (
61+ ts . isAsExpression ( init ) &&
62+ ts . isStringLiteral ( init . expression )
63+ ) {
64+ names . set ( decl . name . text , init . expression . text ) ;
65+ }
66+ }
67+
68+ // Handle `declare const x: "value"` (common in .d.cts files)
69+ if (
70+ ! decl . initializer &&
71+ decl . type &&
72+ ts . isLiteralTypeNode ( decl . type ) &&
73+ ts . isStringLiteral ( decl . type . literal )
6274 ) {
63- names . set ( decl . name . text , init . expression . text ) ;
75+ names . set ( decl . name . text , decl . type . literal . text ) ;
6476 }
6577 }
6678 }
@@ -99,10 +111,17 @@ function resolveControllerName(
99111 }
100112
101113 const dir = path . dirname ( filePath ) ;
102- const candidates = [
103- path . join ( dir , `${ spec } .ts` ) ,
104- path . join ( dir , spec , 'index.ts' ) ,
105- ] ;
114+ const isDts = filePath . endsWith ( '.d.cts' ) || filePath . endsWith ( '.d.ts' ) ;
115+ // Strip .cjs/.js extension from specifier for .d.cts resolution
116+ const bareSpec = spec . replace ( / \. ( c | m ) ? j s $ / u, '' ) ;
117+ const candidates = isDts
118+ ? [
119+ path . join ( dir , `${ bareSpec } .d.cts` ) ,
120+ path . join ( dir , bareSpec , 'index.d.cts' ) ,
121+ path . join ( dir , `${ bareSpec } .d.ts` ) ,
122+ path . join ( dir , bareSpec , 'index.d.ts' ) ,
123+ ]
124+ : [ path . join ( dir , `${ spec } .ts` ) , path . join ( dir , spec , 'index.ts' ) ] ;
106125
107126 for ( const candidate of candidates ) {
108127 if ( ! fs . existsSync ( candidate ) ) {
@@ -478,9 +497,13 @@ function getPropertyText(
478497 * Extract messenger action/event type definitions from a single TypeScript file.
479498 *
480499 * @param filePath - The absolute path to the TypeScript file.
500+ * @param relBase - Optional base path for computing relative source paths (defaults to ROOT).
481501 * @returns An array of extracted messenger item docs.
482502 */
483- function extractFromFile ( filePath : string ) : MessengerItemDoc [ ] {
503+ function extractFromFile (
504+ filePath : string ,
505+ relBase ?: string ,
506+ ) : MessengerItemDoc [ ] {
484507 const content = fs . readFileSync ( filePath , 'utf8' ) ;
485508 const sourceFile = ts . createSourceFile (
486509 filePath ,
@@ -492,7 +515,7 @@ function extractFromFile(filePath: string): MessengerItemDoc[] {
492515 const constants = resolveControllerName ( sourceFile , filePath ) ;
493516 const classMethods = collectClassMethods ( sourceFile ) ;
494517 const items : MessengerItemDoc [ ] = [ ] ;
495- const relPath = path . relative ( ROOT , filePath ) ;
518+ const relPath = path . relative ( relBase ?? ROOT , filePath ) ;
496519
497520 // Type aliases are always top-level statements — no need for deep recursion
498521 for ( const statement of sourceFile . statements ) {
@@ -699,6 +722,34 @@ function findTsFiles(dir: string): string[] {
699722 return results ;
700723}
701724
725+ /**
726+ * Recursively find all `.d.cts` declaration files in a directory.
727+ * Skips nested `node_modules` subdirectories.
728+ *
729+ * @param dir - The directory to search.
730+ * @returns An array of absolute file paths.
731+ */
732+ function findDtsFiles ( dir : string ) : string [ ] {
733+ const results : string [ ] = [ ] ;
734+
735+ function walk ( directory : string ) : void {
736+ for ( const entry of fs . readdirSync ( directory , { withFileTypes : true } ) ) {
737+ const full = path . join ( directory , entry . name ) ;
738+ if ( entry . isDirectory ( ) ) {
739+ if ( entry . name === 'node_modules' ) {
740+ continue ;
741+ }
742+ walk ( full ) ;
743+ } else if ( entry . name . endsWith ( '.d.cts' ) ) {
744+ results . push ( full ) ;
745+ }
746+ }
747+ }
748+
749+ walk ( dir ) ;
750+ return results ;
751+ }
752+
702753// ---------------------------------------------------------------------------
703754// Markdown generation
704755// ---------------------------------------------------------------------------
@@ -707,9 +758,13 @@ function findTsFiles(dir: string): string[] {
707758 * Generate markdown documentation for a single messenger item.
708759 *
709760 * @param item - The messenger item to document.
761+ * @param clientMode - Whether we are generating docs from a client's dependency tree.
710762 * @returns The generated markdown string.
711763 */
712- function generateItemMarkdown ( item : MessengerItemDoc ) : string {
764+ function generateItemMarkdown (
765+ item : MessengerItemDoc ,
766+ clientMode : boolean ,
767+ ) : string {
713768 const parts : string [ ] = [ ] ;
714769
715770 parts . push ( `### \`${ item . typeString } \`` ) ;
@@ -720,8 +775,15 @@ function generateItemMarkdown(item: MessengerItemDoc): string {
720775 parts . push ( '' ) ;
721776 }
722777
723- const ghUrl = `https://github.com/MetaMask/core/blob/main/${ item . sourceFile } #L${ item . line } ` ;
724- parts . push ( `**Source**: [${ item . sourceFile } :${ item . line } ](${ ghUrl } )` ) ;
778+ if ( clientMode ) {
779+ const pkgMatch = item . sourceFile . match ( / n o d e _ m o d u l e s \/ ( @ m e t a m a s k \/ [ ^ / ] + ) / u) ;
780+ const pkgName = pkgMatch ? pkgMatch [ 1 ] : item . sourceFile ;
781+ const npmUrl = `https://www.npmjs.com/package/${ pkgName } ` ;
782+ parts . push ( `**Package**: [\`${ pkgName } \`](${ npmUrl } )` ) ;
783+ } else {
784+ const ghUrl = `https://github.com/MetaMask/core/blob/main/${ item . sourceFile } #L${ item . line } ` ;
785+ parts . push ( `**Source**: [${ item . sourceFile } :${ item . line } ](${ ghUrl } )` ) ;
786+ }
725787 parts . push ( '' ) ;
726788
727789 if ( item . jsDoc ) {
@@ -753,11 +815,13 @@ function generateItemMarkdown(item: MessengerItemDoc): string {
753815 *
754816 * @param ns - The namespace group to generate a page for.
755817 * @param kind - Whether to generate the actions or events page.
818+ * @param clientMode - Whether we are generating docs from a client's dependency tree.
756819 * @returns The generated markdown string.
757820 */
758821function generateNamespacePage (
759822 ns : NamespaceGroup ,
760823 kind : 'action' | 'event' ,
824+ clientMode : boolean ,
761825) : string {
762826 const items = kind === 'action' ? ns . actions : ns . events ;
763827 const title = kind === 'action' ? 'Actions' : 'Events' ;
@@ -797,7 +861,7 @@ function generateNamespacePage(
797861 parts . push ( '' ) ;
798862
799863 for ( const item of items ) {
800- parts . push ( generateItemMarkdown ( item ) ) ;
864+ parts . push ( generateItemMarkdown ( item , clientMode ) ) ;
801865 parts . push ( '---' ) ;
802866 parts . push ( '' ) ;
803867 }
@@ -809,26 +873,43 @@ function generateNamespacePage(
809873 * Generate the index/overview page listing all namespaces.
810874 *
811875 * @param namespaces - All namespace groups sorted alphabetically.
876+ * @param clientName - Optional client name for client-mode docs.
812877 * @returns The generated markdown string.
813878 */
814- function generateIndexPage ( namespaces : NamespaceGroup [ ] ) : string {
879+ function generateIndexPage (
880+ namespaces : NamespaceGroup [ ] ,
881+ clientName ?: string ,
882+ ) : string {
815883 const totalActions = namespaces . reduce (
816884 ( sum , ns ) => sum + ns . actions . length ,
817885 0 ,
818886 ) ;
819887 const totalEvents = namespaces . reduce ( ( sum , ns ) => sum + ns . events . length , 0 ) ;
820888
821889 const parts : string [ ] = [ ] ;
822- parts . push ( '---' ) ;
823- parts . push ( 'title: "Messenger API Reference"' ) ;
824- parts . push ( 'slug: "/"' ) ;
825- parts . push ( '---' ) ;
826- parts . push ( '' ) ;
827- parts . push ( '# MetaMask Core Messenger API' ) ;
828- parts . push ( '' ) ;
829- parts . push (
830- 'This site documents every action and event registered on the Messenger — the type-safe message bus used across all controllers in `@metamask/core`.' ,
831- ) ;
890+ if ( clientName ) {
891+ parts . push ( '---' ) ;
892+ parts . push ( `title: "${ clientName } Messenger API Reference"` ) ;
893+ parts . push ( 'slug: "/"' ) ;
894+ parts . push ( '---' ) ;
895+ parts . push ( '' ) ;
896+ parts . push ( `# ${ clientName } Messenger API` ) ;
897+ parts . push ( '' ) ;
898+ parts . push (
899+ `This site documents every action and event available in the \`${ clientName } \` dependency tree — the type-safe message bus used across all controllers.` ,
900+ ) ;
901+ } else {
902+ parts . push ( '---' ) ;
903+ parts . push ( 'title: "Messenger API Reference"' ) ;
904+ parts . push ( 'slug: "/"' ) ;
905+ parts . push ( '---' ) ;
906+ parts . push ( '' ) ;
907+ parts . push ( '# MetaMask Core Messenger API' ) ;
908+ parts . push ( '' ) ;
909+ parts . push (
910+ 'This site documents every action and event registered on the Messenger — the type-safe message bus used across all controllers in `@metamask/core`.' ,
911+ ) ;
912+ }
832913 parts . push ( '' ) ;
833914 parts . push ( `- **${ namespaces . length } ** namespaces` ) ;
834915 parts . push ( `- **${ totalActions } ** actions` ) ;
@@ -905,30 +986,77 @@ function deduplicationScore(item: MessengerItemDoc): number {
905986 * Main entry point: scans packages, extracts messenger types, and generates docs.
906987 */
907988function main ( ) : void {
908- console . log ( 'Scanning packages for Messenger action/event types...' ) ;
909-
910- const packagesDir = path . join ( ROOT , 'packages' ) ;
911- const packageDirs = fs
912- . readdirSync ( packagesDir , { withFileTypes : true } )
913- . filter ( ( dirent ) => dirent . isDirectory ( ) )
914- . map ( ( dirent ) => path . join ( packagesDir , dirent . name , 'src' ) ) ;
989+ // Parse --client flag
990+ const clientIdx = process . argv . indexOf ( '--client' ) ;
991+ const clientPath = clientIdx !== - 1 ? process . argv [ clientIdx + 1 ] : undefined ;
992+ const clientMode = Boolean ( clientPath ) ;
993+ const clientName = clientPath ? path . basename ( clientPath ) : undefined ;
915994
916995 const allItems : MessengerItemDoc [ ] = [ ] ;
917996
918- for ( const srcDir of packageDirs ) {
919- if ( ! fs . existsSync ( srcDir ) ) {
920- continue ;
997+ if ( clientMode ) {
998+ console . log (
999+ `Scanning ${ clientName } dependencies for Messenger action/event types...` ,
1000+ ) ;
1001+
1002+ const nmDir = path . join ( clientPath as string , 'node_modules' , '@metamask' ) ;
1003+ if ( ! fs . existsSync ( nmDir ) ) {
1004+ console . error ( `Error: ${ nmDir } does not exist.` ) ;
1005+ process . exit ( 1 ) ;
9211006 }
9221007
923- const tsFiles = findTsFiles ( srcDir ) ;
924- for ( const file of tsFiles ) {
925- try {
926- const items = extractFromFile ( file ) ;
927- allItems . push ( ...items ) ;
928- } catch ( error ) {
929- console . warn (
930- `Warning: failed to parse ${ path . relative ( ROOT , file ) } : ${ String ( error ) } ` ,
931- ) ;
1008+ // Find @metamask packages that contain "controller" or "service" in name
1009+ const pkgDirs = fs
1010+ . readdirSync ( nmDir , { withFileTypes : true } )
1011+ . filter (
1012+ ( dirent ) =>
1013+ dirent . isDirectory ( ) &&
1014+ ( dirent . name . includes ( 'controller' ) ||
1015+ dirent . name . includes ( 'service' ) ) ,
1016+ )
1017+ . map ( ( dirent ) => path . join ( nmDir , dirent . name , 'dist' ) ) ;
1018+
1019+ for ( const distDir of pkgDirs ) {
1020+ if ( ! fs . existsSync ( distDir ) ) {
1021+ continue ;
1022+ }
1023+
1024+ const dtsFiles = findDtsFiles ( distDir ) ;
1025+ for ( const file of dtsFiles ) {
1026+ try {
1027+ const items = extractFromFile ( file , clientPath ) ;
1028+ allItems . push ( ...items ) ;
1029+ } catch ( error ) {
1030+ console . warn (
1031+ `Warning: failed to parse ${ path . relative ( clientPath as string , file ) } : ${ String ( error ) } ` ,
1032+ ) ;
1033+ }
1034+ }
1035+ }
1036+ } else {
1037+ console . log ( 'Scanning packages for Messenger action/event types...' ) ;
1038+
1039+ const packagesDir = path . join ( ROOT , 'packages' ) ;
1040+ const packageDirs = fs
1041+ . readdirSync ( packagesDir , { withFileTypes : true } )
1042+ . filter ( ( dirent ) => dirent . isDirectory ( ) )
1043+ . map ( ( dirent ) => path . join ( packagesDir , dirent . name , 'src' ) ) ;
1044+
1045+ for ( const srcDir of packageDirs ) {
1046+ if ( ! fs . existsSync ( srcDir ) ) {
1047+ continue ;
1048+ }
1049+
1050+ const tsFiles = findTsFiles ( srcDir ) ;
1051+ for ( const file of tsFiles ) {
1052+ try {
1053+ const items = extractFromFile ( file ) ;
1054+ allItems . push ( ...items ) ;
1055+ } catch ( error ) {
1056+ console . warn (
1057+ `Warning: failed to parse ${ path . relative ( ROOT , file ) } : ${ String ( error ) } ` ,
1058+ ) ;
1059+ }
9321060 }
9331061 }
9341062 }
@@ -1006,22 +1134,22 @@ function main(): void {
10061134 if ( ns . actions . length > 0 ) {
10071135 fs . writeFileSync (
10081136 path . join ( nsDir , 'actions.md' ) ,
1009- generateNamespacePage ( ns , 'action' ) ,
1137+ generateNamespacePage ( ns , 'action' , clientMode ) ,
10101138 ) ;
10111139 }
10121140
10131141 if ( ns . events . length > 0 ) {
10141142 fs . writeFileSync (
10151143 path . join ( nsDir , 'events.md' ) ,
1016- generateNamespacePage ( ns , 'event' ) ,
1144+ generateNamespacePage ( ns , 'event' , clientMode ) ,
10171145 ) ;
10181146 }
10191147 }
10201148
10211149 // Generate index page
10221150 fs . writeFileSync (
10231151 path . join ( docsDir , 'index.md' ) ,
1024- generateIndexPage ( namespaces ) ,
1152+ generateIndexPage ( namespaces , clientName ) ,
10251153 ) ;
10261154
10271155 // Generate sidebars
0 commit comments