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
2 changes: 1 addition & 1 deletion packages/platform-api-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Initial release of the platform-api-docs package ([#8012](https://github.com/MetaMask/core/pull/8012))
- Initial release of the platform-api-docs package ([#8012](https://github.com/MetaMask/core/pull/8012), [#9913](https://github.com/MetaMask/core/pull/9913), [#9990](https://github.com/MetaMask/core/pull/9990))

[Unreleased]: https://github.com/MetaMask/core/
35 changes: 34 additions & 1 deletion packages/platform-api-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,45 @@ Options:
--build Generate docs and build static site
--serve Generate docs, build, and serve static site
--dev Generate docs and start dev server with hot reload
--scan-dir <dir> Extra source directory to scan (repeatable)
--strategy <name> How to find actions and events: "scan" (default) or
"root-messenger" (see below)
--scan-dir <dir> Extra source directory to scan (repeatable; --strategy scan only)
--root-actions <ref> Type aliasing the union of every action, as "<file>#<TypeName>"
(required with --strategy root-messenger)
--root-events <ref> Type aliasing the union of every event, as "<file>#<TypeName>"
(required with --strategy root-messenger)
--output <dir> Output directory (default: <project-path>/.platform-api-docs)
--project-label <label> Short label identifying the project (e.g. "Core", "Extension")
--help Show this help message
```

## Strategies

Which strategy to use depends on whether the project has a single messenger carrying every action and event.

### `scan` (default)

Parses every TypeScript source and declaration file it can find — the scan directories, `packages/*/src`, and `node_modules/@metamask/*/dist` — and reads every `*Messenger` type alias it encounters.

Use this when no single messenger aggregates every capability, as in a monorepo of independently published packages.

### `root-messenger`

Resolves the two types the project declares for its root messenger capabilities — the collection of every action and the collection of every event — and lets the TypeScript type checker walk them. Only the files named on the command line are opened.

Use this when the project has one root messenger carrying every action and event, as a client application built on these packages does. It is substantially faster than `scan`, because it reads what the project already declares instead of re-deriving it, and it documents only what is reachable through that messenger.

```
platform-api-docs \
--strategy root-messenger \
--root-actions 'src/messenger.ts#RootActions' \
--root-events 'src/messenger.ts#RootEvents'
```

Each reference names a type alias, written by hand or computed — the type checker resolves either.

The docs contain exactly what the named types contain, so those types should be the ones carrying every capability rather than a narrowed subset. Capability types that can't be documented are reported rather than dropped silently: those declared inline in the capability collection type (with no name or JSDoc to document), and those whose shape can't be read (most often a `type` property that isn't a namespaced string literal).

## Contributing

This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
1 change: 0 additions & 1 deletion packages/platform-api-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"@mdx-js/react": "^3.1.1",
"@metamask/utils": "^11.11.0",
"execa": "^5.0.0",
"glob": "^13.0.6",
"npm-which": "^3.0.1",
"prism-react-renderer": "^2.4.1",
"react": "^19.0.0",
Expand Down
171 changes: 171 additions & 0 deletions packages/platform-api-docs/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,175 @@ export type QuxMessenger = Messenger<'Qux', QuxAction, never>;
expect(result.all).toContain('No scannable directories found');
});
});

describe('--strategy root-messenger', () => {
/**
* Write a project whose root messenger unions live in `app/types.ts`.
*
* @param directoryPath - The sandbox root.
*/
async function writeRootMessengerProject(
directoryPath: string,
): Promise<void> {
const appDir = path.join(directoryPath, 'app');
await fs.promises.mkdir(appDir, { recursive: true });
await fs.promises.writeFile(
path.join(appDir, 'types.ts'),
`
export type FooControllerGetStateAction = {
type: 'FooController:getState';
handler: () => FooState;
};

export type FooControllerStateChangeEvent = {
type: 'FooController:stateChange';
payload: [FooState, Patch[]];
};

export type GlobalActions = FooControllerGetStateAction;
export type GlobalEvents = FooControllerStateChangeEvent;
`,
);
}

it('generates docs from the named root messenger unions', async () => {
expect.assertions(3);

await withinSandbox(async ({ directoryPath }) => {
await writeRootMessengerProject(directoryPath);

const result = await runCLI([
directoryPath,
'--strategy',
'root-messenger',
'--root-actions',
'app/types.ts#GlobalActions',
'--root-events',
'app/types.ts#GlobalEvents',
]);

expect(result.exitCode).toBe(0);
expect(result.all).toContain('Found 2 messenger items total');
expect(result.all).toContain('Generated docs for 1 namespace');
});
});

it('exits with error when the root type references are missing', async () => {
expect.assertions(2);

await withinSandbox(async ({ directoryPath }) => {
await writeRootMessengerProject(directoryPath);

const result = await runCLI([
directoryPath,
'--strategy',
'root-messenger',
]);

expect(result.exitCode).not.toBe(0);
expect(result.all).toContain(
'requires both --root-actions and --root-events',
);
});
});

it('exits with error when a root type reference is malformed', async () => {
expect.assertions(2);

await withinSandbox(async ({ directoryPath }) => {
await writeRootMessengerProject(directoryPath);

const result = await runCLI([
directoryPath,
'--strategy',
'root-messenger',
'--root-actions',
'app/types.ts',
'--root-events',
'app/types.ts#GlobalEvents',
]);

expect(result.exitCode).not.toBe(0);
expect(result.all).toContain(
'Expected a reference of the form "<file>#<TypeName>"',
);
});
});

it('exits with error when the named type is not declared', async () => {
expect.assertions(2);

await withinSandbox(async ({ directoryPath }) => {
await writeRootMessengerProject(directoryPath);

const result = await runCLI([
directoryPath,
'--strategy',
'root-messenger',
'--root-actions',
'app/types.ts#NotDeclared',
'--root-events',
'app/types.ts#GlobalEvents',
]);

expect(result.exitCode).not.toBe(0);
expect(result.all).toContain('No type alias named "NotDeclared"');
});
});

it('exits with error when --scan-dir is combined with it', async () => {
expect.assertions(2);

await withinSandbox(async ({ directoryPath }) => {
await writeRootMessengerProject(directoryPath);

const result = await runCLI([
directoryPath,
'--strategy',
'root-messenger',
'--root-actions',
'app/types.ts#GlobalActions',
'--root-events',
'app/types.ts#GlobalEvents',
'--scan-dir',
'app',
]);

expect(result.exitCode).not.toBe(0);
expect(result.all).toContain(
'--scan-dir only applies to --strategy scan',
);
});
});

it('exits with error when root type references are used with --strategy scan', async () => {
expect.assertions(2);

await withinSandbox(async ({ directoryPath }) => {
await writeRootMessengerProject(directoryPath);

const result = await runCLI([
directoryPath,
'--root-actions',
'app/types.ts#GlobalActions',
]);

expect(result.exitCode).not.toBe(0);
expect(result.all).toContain(
'--root-actions and --root-events only apply to --strategy root-messenger',
);
});
});

it('rejects an unknown strategy', async () => {
expect.assertions(2);

await withinSandbox(async ({ directoryPath }) => {
const result = await runCLI([directoryPath, '--strategy', 'telepathy']);

expect(result.exitCode).not.toBe(0);
expect(result.all).toContain('Invalid values');
});
});
});
});
Loading