Skip to content
Merged
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 @@ -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
Expand Down
121 changes: 121 additions & 0 deletions packages/platform-api-docs/src/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
12 changes: 11 additions & 1 deletion 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 @@ -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(
Expand Down
Loading