Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/platform-api-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,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
Expand Down
71 changes: 71 additions & 0 deletions packages/platform-api-docs/src/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
30 changes: 16 additions & 14 deletions packages/platform-api-docs/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -353,36 +360,31 @@ async function scanSources(
sources: ScanSources,
): Promise<MessengerCapabilityPacket[]> {
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) {
const root = await toGlobPath(sources.packagesDir);
// 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.
Expand Down