Skip to content

Commit 7c63a0d

Browse files
committed
feat: add --client flag to generate messenger docs from client dependency tree
1 parent 15f276f commit 7c63a0d

2 files changed

Lines changed: 183 additions & 51 deletions

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@
2222
"changelog:validate": "yarn workspaces foreach --all --no-private --parallel --interlaced --verbose run changelog:validate",
2323
"create-package": "tsx scripts/create-package",
2424
"docs:messenger:build": "yarn docs:messenger:generate && npm --prefix docs-site install && npm --prefix docs-site run build",
25+
"docs:messenger:build:client": "yarn docs:messenger:generate:client && npm --prefix docs-site install && npm --prefix docs-site run build",
2526
"docs:messenger:dev": "yarn docs:messenger:generate && npm --prefix docs-site install && npm --prefix docs-site start",
27+
"docs:messenger:dev:client": "yarn docs:messenger:generate:client && npm --prefix docs-site install && npm --prefix docs-site start",
2628
"docs:messenger:generate": "tsx scripts/generate-messenger-docs.ts",
29+
"docs:messenger:generate:client": "tsx scripts/generate-messenger-docs.ts --client",
2730
"docs:messenger:serve": "yarn docs:messenger:build && npm --prefix docs-site run serve",
31+
"docs:messenger:serve:client": "yarn docs:messenger:build:client && npm --prefix docs-site run serve",
2832
"generate-method-action-types": "yarn workspaces foreach --all --parallel --interlaced --verbose run generate-method-action-types",
2933
"lint": "yarn lint:eslint && echo && yarn lint:misc --check && yarn constraints && yarn lint:dependencies && yarn lint:teams && yarn generate-method-action-types --check",
3034
"lint:dependencies": "depcheck && yarn dedupe --check",

scripts/generate-messenger-docs.ts

Lines changed: 179 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -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)?js$/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(/node_modules\/(@metamask\/[^/]+)/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
*/
758821
function 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
*/
907988
function 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

Comments
 (0)