diff --git a/packages/platform-api-docs/CHANGELOG.md b/packages/platform-api-docs/CHANGELOG.md index 5cbb9269d01..76d985b5df9 100644 --- a/packages/platform-api-docs/CHANGELOG.md +++ b/packages/platform-api-docs/CHANGELOG.md @@ -34,6 +34,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 ([#10085](https://github.com/MetaMask/core/pull/10085)) + ## [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..abca66f1fcf 100644 --- a/packages/platform-api-docs/src/generate.test.ts +++ b/packages/platform-api-docs/src/generate.test.ts @@ -227,6 +227,56 @@ export type TestMessenger = Messenger<'Test', TestGetAction, never>; }); }); + it('keeps published declaration files when a scan directory contains node_modules', async () => { + expect.assertions(2); + + await withinSandbox(async ({ directoryPath }) => { + const srcDir = path.join(directoryPath, 'src'); + await fs.promises.mkdir(srcDir, { recursive: true }); + await fs.promises.writeFile( + path.join(srcDir, 'Local.ts'), + ` +export type LocalGetAction = { + type: 'Local:get'; + handler: () => void; +}; + +export type LocalMessenger = Messenger<'Local', LocalGetAction, never>; +`, + ); + + const distDir = path.join( + directoryPath, + 'node_modules', + '@metamask', + 'published-pkg', + 'dist', + ); + await fs.promises.mkdir(distDir, { recursive: true }); + await fs.promises.writeFile( + path.join(distDir, 'index.d.cts'), + ` +export type PublishedGetAction = { + type: 'Published:get'; + handler: () => void; +}; + +export type PublishedMessenger = Messenger<'Published', PublishedGetAction, never>; +`, + ); + + const result = await generate({ + projectPath: directoryPath, + outputDir: path.join(directoryPath, '.docs'), + strategy: 'scan', + scanDirs: ['.'], + }); + + expect(result.namespaces).toBe(2); + expect(result.actions).toBe(2); + }); + }); + it('follows symlinked @metamask packages when scanning node_modules/@metamask/*/dist', async () => { expect.assertions(1); @@ -338,6 +388,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..494190d1894 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); @@ -355,6 +362,9 @@ async function scanSources( const project = createProject(); const sourceFiles = []; + // NOTE: We are calling `addSourceFiles` for each kind of source instead of + // calling it at the very end so that at each step we can make sure to exclude + // `node_modules` and `dist`. for (const dir of sources.scanDirs) { const root = await toGlobPath(projectPath, dir); sourceFiles.push(