From 2a31ad5c5b32daa594bfec7d59c68a26a2593ee4 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Thu, 3 Sep 2026 10:35:39 +0200 Subject: [PATCH 1/2] fix(platform-api-docs): prefer source over build output when deduplicating --- packages/platform-api-docs/CHANGELOG.md | 4 ++ .../platform-api-docs/src/generate.test.ts | 71 +++++++++++++++++++ packages/platform-api-docs/src/generate.ts | 30 ++++---- 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/packages/platform-api-docs/CHANGELOG.md b/packages/platform-api-docs/CHANGELOG.md index 86e64ccb0e0..f73d18a2b0e 100644 --- a/packages/platform-api-docs/CHANGELOG.md +++ b/packages/platform-api-docs/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Speed up documentation generation by loading source files in bulk instead of one at a time ([#9990](https://github.com/MetaMask/core/pull/9990)) - Bump `@metamask/utils` from `^11.11.0` to `^11.12.0` ([#10076](https://github.com/MetaMask/core/pull/10076)) +### Fixed + +- Link a capability to its source rather than to the build output compiled from it, where both are available + ## [0.1.0] ### Added diff --git a/packages/platform-api-docs/src/generate.test.ts b/packages/platform-api-docs/src/generate.test.ts index fa284232fc9..e1971a9c6ec 100644 --- a/packages/platform-api-docs/src/generate.test.ts +++ b/packages/platform-api-docs/src/generate.test.ts @@ -338,6 +338,77 @@ export type FooMessenger = Messenger<'Foo', FooAction, never>; }); }); + it('deduplicates items preferring source over build output', async () => { + expect.assertions(2); + + await withinSandbox(async ({ directoryPath }) => { + // Reproduces how build output wins in a real monorepo. `a-controller` is + // scanned before `b-controller`, and its import of `@metamask/b-controller` + // resolves through node_modules to the published `.d.cts`, so the first + // declaration seen for `B:get` is the built one. The source declaration is + // reached later, when `b-controller/src` is scanned, and should win. + const declaration = ` +/** Gets b. */ +export type BGetAction = { + type: 'B:get'; + handler: () => string; +}; +`; + + const bSrc = path.join(directoryPath, 'packages', 'b-controller', 'src'); + await fs.promises.mkdir(bSrc, { recursive: true }); + await fs.promises.writeFile( + path.join(bSrc, 'BController.ts'), + `${declaration} +export type BMessenger = Messenger<'B', BGetAction, never>; +`, + ); + + const bDist = path.join( + directoryPath, + 'node_modules', + '@metamask', + 'b-controller', + 'dist', + ); + await fs.promises.mkdir(bDist, { recursive: true }); + await fs.promises.writeFile(path.join(bDist, 'index.d.cts'), declaration); + await fs.promises.writeFile( + path.join(bDist, '..', 'package.json'), + JSON.stringify({ + name: '@metamask/b-controller', + types: './dist/index.d.cts', + }), + ); + + const aSrc = path.join(directoryPath, 'packages', 'a-controller', 'src'); + await fs.promises.mkdir(aSrc, { recursive: true }); + await fs.promises.writeFile( + path.join(aSrc, 'AController.ts'), + ` +import type { BGetAction } from '@metamask/b-controller'; + +export type AMessenger = Messenger<'A', BGetAction, never>; +`, + ); + + const outputDir = path.join(directoryPath, '.docs'); + await generate({ + projectPath: directoryPath, + outputDir, + strategy: 'scan', + scanDirs: ['src'], + }); + + const actionsMd = await fs.promises.readFile( + path.join(outputDir, 'docs', 'B', 'actions.md'), + 'utf8', + ); + expect(actionsMd).toContain('packages/b-controller/src/BController.ts'); + expect(actionsMd).not.toContain('/dist/'); + }); + }); + it('returns zero counts for project with no messenger types', async () => { expect.assertions(3); diff --git a/packages/platform-api-docs/src/generate.ts b/packages/platform-api-docs/src/generate.ts index 61e7ecdf7ca..773be716908 100644 --- a/packages/platform-api-docs/src/generate.ts +++ b/packages/platform-api-docs/src/generate.ts @@ -105,7 +105,14 @@ function deduplicationScore(item: MessengerCapabilityPacket): number { item.sourceFile.toLowerCase().includes(namespacePrefix) ? 1 : 0; - return jsDocScore + homeScore; + // A capability declared in a package's own source is usually also visible in + // the `dist` built from it, and a cross-package import resolves to that + // `dist` rather than to the sibling's source. Prefer the source, which is + // what an engineer can actually read and edit. Projects that only ever see + // published packages score every candidate the same way, so nothing changes + // for them. + const sourceScore = /[\\/]dist[\\/]/u.test(item.sourceFile) ? 0 : 1; + return jsDocScore + homeScore + sourceScore; } const execFileAsync = promisify(execFile); @@ -353,16 +360,11 @@ async function scanSources( sources: ScanSources, ): Promise { const project = createProject(); - const sourceFiles = []; + const patterns: string[] = []; for (const dir of sources.scanDirs) { const root = await toGlobPath(projectPath, dir); - sourceFiles.push( - ...addSourceFiles(project, [ - `${root}/**/*.ts`, - ...buildTsSourceExclusions(root), - ]), - ); + patterns.push(`${root}/**/*.ts`, ...buildTsSourceExclusions(root)); } if (sources.packagesDir) { @@ -370,19 +372,19 @@ async function scanSources( // Anchored at each package's `src`, not at `packages` itself, so a package // whose name collides with an exclusion (`test`, `dist`) isn't dropped. const contentRoot = `${root}/*/src`; - sourceFiles.push( - ...addSourceFiles(project, [ - `${contentRoot}/**/*.ts`, - ...buildTsSourceExclusions(contentRoot), - ]), + patterns.push( + `${contentRoot}/**/*.ts`, + ...buildTsSourceExclusions(contentRoot), ); } if (sources.nodeModulesDir) { const root = await toGlobPath(sources.nodeModulesDir); - sourceFiles.push(...addSourceFiles(project, [`${root}/*/dist/**/*.d.cts`])); + patterns.push(`${root}/*/dist/**/*.d.cts`); } + const sourceFiles = addSourceFiles(project, patterns); + // Matched paths are fully resolved, so the root they are made relative to // has to be resolved the same way or every source link becomes a `../..` // walk out of the project. From 0b32e92e6782f5003f2924785ab113ca467c52c3 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Thu, 3 Sep 2026 10:40:04 +0200 Subject: [PATCH 2/2] docs(platform-api-docs): add PR link to changelog entry --- packages/platform-api-docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform-api-docs/CHANGELOG.md b/packages/platform-api-docs/CHANGELOG.md index f73d18a2b0e..d1980c49e6e 100644 --- a/packages/platform-api-docs/CHANGELOG.md +++ b/packages/platform-api-docs/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Link a capability to its source rather than to the build output compiled from it, where both are available +- Link a capability to its source rather than to the build output compiled from it, where both are available ([#10085](https://github.com/MetaMask/core/pull/10085)) ## [0.1.0]