diff --git a/.changeset/gentle-kittens-replay.md b/.changeset/gentle-kittens-replay.md new file mode 100644 index 000000000..1475d57e0 --- /dev/null +++ b/.changeset/gentle-kittens-replay.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/generator-react': patch +--- + +Replay the click or tap that hydrates a code block, so its first press switches tabs or copies the code diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 14c8e3181..3d07f6b19 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -29,6 +29,7 @@ "packages/node/src/api-links/__tests__/fixtures/", "*.snapshot", "packages/react/src/html/template.html", + "www/theme/template.html", "out/", "www/content/", "www/out/", diff --git a/.oxlintrc.json b/.oxlintrc.json index a794496da..115ebace0 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -207,6 +207,7 @@ "files": [ "packages/node-legacy/src/legacy-html/assets/*.js", "packages/react/src/html/ui/**/*", + "www/theme/components/**/*", "e2e/**/*.spec.js" ], "globals": { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afcd1b2b0..51de2fb14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,10 +85,12 @@ published package lives under `packages/`: - `packages/typedoc`: [`@doc-kit/typedoc`](packages/typedoc) — the TypeDoc plugin writing API references as doc-kit Markdown Everything else at the root supports the repo rather than shipping to npm: -`docs/` (every authored documentation page, from the landing page to the -reference docs), `www/` (the documentation site build: its configuration and -gitignored build artifacts), `scripts/` (build and comparison helpers), and -`e2e/` (Playwright tests). +`docs/` (the authored guides and reference docs, plus the illustrations in +`docs/diagrams/`), `www/` (the documentation site: its configuration, its theme +in `www/theme/`, site-only pages such as the home page in `www/pages/`, public +files in `www/public/`, and the gitignored build artifacts in `www/content/` +and `www/out/`), `scripts/` (build and comparison helpers), and `e2e/` +(Playwright tests). Contributor-facing deep dives — [creating CLI commands](docs/contributing/commands.md) and [creating diff --git a/docs/cli.md b/docs/cli.md index e82748e7d..f6f5f16d0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,42 +1,146 @@ # CLI -The `doc-kit` command-line interface. Every option that maps to -configuration can also live in a [configuration file](./configuration.md); -when both are present, CLI flags win (rule of specifity). +The `doc-kit` command builds your docs with `doc-kit generate` and serves +them locally with `doc-kit preview`. It comes from the +[`@doc-kit/cli`](./packages/cli.md) package; install it in your project as +shown in [Getting started](./getting-started.md), then run it with `npx`: ```bash -npx @doc-kit/cli [command] [options] +npx doc-kit [options] ``` -One option applies to every command: - -- `--log-level ` {string} `debug`, `info`, `warn`, `error`, or - `fatal`. **Default:** `'info'`. +`npx doc-kit --help` lists the commands, and `npx doc-kit generate --help` +the options of one. ## `doc-kit generate` +Runs the generators and writes their output. It needs at least one target +and an input, from flags or a [configuration file](#configuration-files): + +```bash +npx doc-kit generate -t html -t orama-db -i "docs/**/*.md" -o out +``` + +Quote glob patterns, so they reach doc-kit as written instead of being +expanded by your shell. Options that take a list accept several values after +one flag (`-t html orama-db`) or the flag repeated (`-t html -t orama-db`). +Each option except `--config-file` sets the configuration key named in +parentheses. + +- `-t`, `--target` {string[]} Generators to run (`target`): built-in names, + such as `html` or `orama-db` (see the [generators overview](./generators.md)), + or the import specifier or path of a + [custom generator](./creating-generators.md). Only `json`, `json-all`, and + `json-simple` come with the CLI. The others come from packages you install + next to it, such as [`@doc-kit/generator-react`](./packages/react.md) for + `html`, `orama-db`, and the other web generators, + [`@node-core/doc-kit`](./packages/node.md) for `man-page`, `api-links`, and + `addon-verify`, and [`@node-core/doc-kit-legacy`](./packages/node-legacy.md) + for the `legacy-*` generators. +- `-i`, `--input` {string[]} Glob patterns of the files to read + (`global.input`). +- `--ignore` {string[]} Glob patterns of files to leave out (`global.ignore`). +- `-o`, `--output` {string} Directory to write the output to + (`global.output`). +- `--config-file` {string} Path of the configuration file to load, instead + of searching the working directory for one. +- `-v`, `--version` {string} Version of the project being documented + (`global.version`), coerced to semver, so `22` means `22.0.0`. + **Default:** the version of Node.js running doc-kit. +- `-c`, `--changelog` {string} URL or path of a changelog in the format of + Node.js's `CHANGELOG.md`, which the version selector is built from + (`global.changelog`). Without one, the output covers a single version. +- `--index` {string} URL or path of an `index.md` whose list of links + (`* [Title](file.md)`) sets the order of the documents in the JSON bundle, + the legacy HTML sidebar and the legacy JSON bundle (`global.index`). +- `--git-ref` {string} Git branch, tag, or commit that links into your + repository point to, such as edit links and the JSON output's source URLs + (`global.ref`). The website's source code links follow + [`jsx-ast.ref`](./generators/jsx-ast.md#configuring) instead, which defaults + to `'main'` and is set in a configuration file. **Default:** `'HEAD'`. +- `--type-map` {string} URL or path of a JSON file mapping type names to the + URLs documenting them, used to link types (`metadata.typeMap`). +- `--minify` {boolean} Minify the output (`global.minify`). **Default:** + `false`. +- `-p`, `--threads` {number} Worker threads to use, at least 1 (`threads`). + **Default:** the number of CPUs, up to 4. +- `--chunk-size` {number} Items each worker processes per task, at least 1 + (`chunkSize`). **Default:** `10`. + +```bash +# Build everything the configuration file describes +npx doc-kit generate + +# Build only the JSON output, minified, into dist/json +npx doc-kit generate -t json -o dist/json --minify +``` + +When something fails, doc-kit logs the error and exits with code 1, so a +broken build also fails your CI job. + +## `doc-kit preview` + +Serves the output directory over HTTP at , so you can +check a build in the browser the way a host would serve it: + +```bash +npx doc-kit preview +``` + +What it serves depends on the options you pass: + +- **No `generate` options:** it serves `./out` as it is, without reading your + configuration file, and stops with an error if `./out` doesn't exist. +- **Any `generate` option:** it builds first, exactly as `doc-kit generate` + would, then serves the directory that build wrote to. To build from your + configuration file alone, pass it with `--config-file`. + +It builds once and doesn't watch for changes, so run it again after editing. +Directories serve their `index.html`, and paths that don't exist get the +output's `404.html`, when it has one. + +It takes these options, plus every option of +[`doc-kit generate`](#doc-kit-generate): + +- `--port` {number} Port to serve on; `0` picks a free one. **Default:** + `3000`. +- `--host` {string} Host to listen on; `0.0.0.0` makes the preview reachable + from other devices on your network. **Default:** `'localhost'`. + +```bash +# Build with the configuration file, then serve its output directory +npx doc-kit preview --config-file doc-kit.config.mjs + +# Build only the site, then serve it on another port +npx doc-kit preview -t html --port 8080 +``` + +> [!NOTE] +> The preview server is for local testing, not for production. To publish, +> upload the output directory to a static host; see +> [Publishing your docs](./publishing.md). + +## Global options + +These work with every command, before or after its name: + +- `--log-level` {string} Log only messages at this level or above: `debug`, + `info`, `warn`, `error`, or `fatal`. **Default:** `'info'`. + ```bash -npx @doc-kit/cli generate [options] +npx doc-kit generate --log-level debug ``` -Runs the generators and writes their output. Requires a `target` and an -`input`, from flags or the configuration file. - -- `--config-file ` {string} Use a specific configuration file instead - of searching. -- `-i, --input ` {string[]} Input file patterns (glob). -- `-t, --target ` {string[]} Generator name(s), or import - specifiers for custom generators. -- `--ignore ` {string[]} Input patterns to skip. -- `-o, --output ` {string} The output directory. -- `-v, --version ` {string} The version of the project being - documented. -- `-c, --changelog ` {string} Changelog URL or path (release history - for version selectors). -- `--git-ref ` {string} Git ref used in source links. -- `--index ` {string} `index.md` URL or path. -- `--minify` {boolean} Minify the output. -- `--type-map ` {string} Type map URL or path (custom type-name → URL - links). -- `-p, --threads ` {number} Worker threads to use (minimum 1). -- `--chunk-size ` {number} Items per worker thread (minimum 1). +## Configuration files + +Everything the `generate` flags set can also live in a configuration file, +along with what they don't cover, such as generator options, Markdown plugins, +and presets. Without `--config-file`, doc-kit looks for one in the working +directory, such as `doc-kit.config.mjs`, `.doc-kitrc.yml`, or a `doc-kit` key +in `package.json`. See +[Where configuration lives](./configuration.md#where-configuration-lives) for +every name it tries. + +Flags win over the file. A flag that takes a list, such as `--target`, +replaces the configured list instead of adding to it. See +[Configuration](./configuration.md) for the file's format and every key. diff --git a/docs/configuration.md b/docs/configuration.md index a0d750dd9..3560684e1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,168 +1,247 @@ # Configuration -`doc-kit` uses [`cosmiconfig`](https://github.com/cosmiconfig/cosmiconfig) to -discover and load configuration. Run the CLI from your project directory and it -will automatically look for a `doc-kit` property in `package.json`, rc files -such as `.doc-kitrc.yml`, and module files such as `doc-kit.config.mjs`. +A configuration file holds everything `doc-kit generate` needs: the targets +to build, where your sources are, and options for each generator, including +the ones no CLI flag covers, such as Markdown plugins and presets. With one in +place, `npx doc-kit generate` needs no flags at all. This page documents where +doc-kit finds the file, every key it takes, and how it combines with presets +and flags. -Use `--config-file ` to load a specific file instead of searching. +The smallest configuration names the generators to run and the files to read. +doc-kit needs both, from the file or from flags: -## Configuration File Format - -Configuration files can be either: - -- **JavaScript** (`.js`, `.mjs`, `.cjs`) -- **TypeScript** (`.ts`, loaded through Node.js type stripping, so only erasable syntax such as type annotations works) -- **JSON** (`.json`) -- **YAML** (`.yaml`, `.yml`, or an extensionless rc file) +```mjs displayName="doc-kit.config.mjs" +export default { + target: ['html'], + global: { + input: ['docs/**/*.md'], + output: 'out', + }, +}; +``` -JavaScript and TypeScript configuration files export the configuration object. -JSON and YAML files contain the object directly. A `package.json` configuration -uses the `doc-kit` property: +```yaml displayName=".doc-kitrc.yml" +target: + - html +global: + input: + - 'docs/**/*.md' + output: out +``` ```json displayName="package.json" { + "name": "my-library", "doc-kit": { - "target": ["json-simple"], + "target": ["html"], "global": { - "input": "doc/api/*.md", + "input": ["docs/**/*.md"], "output": "out" } } } ``` -### Basic Example +## Where configuration lives -```mjs displayName="doc-kit.config.mjs" -export default { - // Targets, alternatively supplied by command line flags. Each entry is - // either a built-in shorthand name or an import specifier resolving to a - // generator module (e.g. '@my-scope/my-package/my-generator'). - target: ['orama-db', 'html'], - global: { - project: 'My Project', - version: '1.2.0', - input: 'docs/**/*.md', - output: 'dist/', - ignore: ['node_modules/', 'test/'], - baseURL: 'https://example.com/docs/', - }, +doc-kit looks for configuration in the working directory with +[cosmiconfig](https://github.com/cosmiconfig/cosmiconfig). It takes the first +of these it finds, and doesn't look in parent directories: - threads: 4, - chunkSize: 10, +1. A `doc-kit` key in `package.json`. +2. `.doc-kitrc`, which holds YAML or JSON, or `.doc-kitrc` with a `.json`, + `.yaml`, `.yml`, `.js`, `.ts`, `.cjs`, or `.mjs` extension. +3. The same files in a `.config/` directory, without the leading dot, such as + `.config/doc-kitrc.json`. +4. `doc-kit.config.js`, `.ts`, `.cjs`, or `.mjs`. - // Generator-specific configurations - html: { - title: '{project} Documentation', - }, +To skip the search, pass `--config-file ` to load one file, relative to +the working directory. - metadata: { - typeMap: { - MyThing: 'https://example.com/docs/my-thing.html', - }, - }, -}; -``` +JavaScript files default-export the configuration (or assign it to +`module.exports`). TypeScript files work the same way: Node.js strips their +types as it loads them, so they can use type annotations and other erasable +syntax, but not features such as enums. JSON and YAML files hold the object +directly. -## Extending Presets +> [!IMPORTANT] +> Paths in your configuration, such as `input`, `output`, and generator +> options like `templatePath`, are relative to the working directory, not to +> the configuration file. The exceptions are `extends` and Markdown plugins, +> which resolve from the file that lists them. To run doc-kit from another +> directory with `--config-file`, make the paths absolute, for example with +> `import.meta.dirname`. -A configuration file may declare `extends`: one or more presets whose values -are merged underneath its own. Each entry is either an import specifier of a -module whose default export is a configuration object, or a path relative to -the configuration file: +## A complete example ```mjs displayName="doc-kit.config.mjs" +/** @type {import('@doc-kit/core/utils/configuration/types').Configuration} */ export default { - // Build the docs the way nodejs.org does — branding, URL layouts, - // and release history included - extends: '@node-core/doc-kit/config', + // The generators to run + target: ['html', 'orama-db', 'sitemap'], + + // Options every generator shares + global: { + project: 'My Library', + version: '2.1.0', + repository: 'my-org/my-library', + baseURL: 'https://my-library.dev/docs', + input: ['docs/**/*.md'], + ignore: ['docs/drafts/**'], + output: 'out', + minify: true, + }, + + // How the work is spread across threads + threads: 2, + // Options of one generator, under its name html: { - // Your own values still win over the preset - title: '{project} {version} API Reference', + title: '{project} {version} docs', + }, + + metadata: { + typeMap: 'docs/type-map.json', }, }; ``` -`extends` also accepts an array; later presets take precedence over earlier -ones, and the configuration file itself wins over all of them. +The configuration object takes these top-level keys: -The built-in defaults are deliberately project-neutral: no repository, -site URL, release history, or branding is assumed. The -[`@node-core/doc-kit/config`](https://github.com/nodejs/doc-kit/tree/main/packages/node) -preset opts back into everything Node.js-specific. +| Key | What it holds | Reference | +| ---------------------------------- | -------------------------------------- | --------------------------------------- | +| `target` | The generators to run | [Targets](#targets) | +| `global` | Options every generator shares | [Global options](#global-options) | +| `threads`, `chunkSize` | How the work is spread across threads | [Execution options](#execution-options) | +| A generator's name, such as `html` | Options of that generator alone | [Generator options](#generator-options) | +| `extends` | Presets your configuration builds upon | [Extending presets](#extending-presets) | -## Configuration Structure +## Targets -Three sources, in order of precedence: - -1. **CLI flags** (see the [CLI reference](./cli.md)) override -2. **the configuration file**, which overrides -3. **built-in defaults**. +- `target` {string|string[]} The generators to run: built-in names, such as + `html`, `orama-db`, or `json` (see the + [generators overview](./generators.md)), or the import specifier or path of + a [custom generator](./creating-generators.md). A path must be absolute or + start with `.`, and resolves against the working directory. The stages a + target depends on run without being listed. ## Global options -Everything under the `global` key applies to every generator: - -- `project` {string} Name of the project being documented, used in titles, - logos, and templated text. Defaults to the `name` in your `package.json`. -- `version` {string|SemVer} Documentation version. **Default:** - `process.version`. -- `minify` {boolean} Whether to minify output. **Default:** `false`. -- `repository` {string} GitHub repository in `owner/repo` format; without - one, repository UI is omitted. -- `ref` {string} Git reference (branch, tag, or commit SHA). **Default:** - `'HEAD'`. -- `baseURL` {string|URL} Base URL of the published site, used wherever - absolute URLs are needed. -- `input` {string[]} Input file patterns (glob). -- `output` {string} Output directory path. -- `ignore` {string[]} Patterns to ignore. **Default:** `[]`. -- `changelog` {string|URL|Array} Release history used for version selectors; - a URL or path to parse, or a pre-parsed array. **Default:** `[]` - (single-version output). -- `index` {string|URL|Array} Index URL. -- `markdown` {Object} [Markdown plugins](#markdown-plugins) for every - generator processing Markdown. - -A generator's own section (e.g., `html`, `legacy-json`) can override any of -these for that generator alone, except `markdown`, which it adds to. +Options under `global` apply to every generator. A generator's own section +can override any of them for that generator alone; see +[Generator options](#generator-options). + +- `input` {string[]} Glob patterns of the files to read: Markdown and MDX, and + `.js` files for generators that parse JavaScript. Always an array, even for + a single pattern. Required, here or with `--input`. +- `ignore` {string|string[]} Glob patterns of files to leave out of `input`. +- `output` {string} Directory to write to. +- `project` {string} The project's name, used in titles, logos, and templated + text. **Default:** the `name` in the working directory's `package.json`, or + `'API Docs'`. +- `version` {string} Version of the project being documented, coerced to + semver, so `'2'` means `2.0.0`. **Default:** the version of Node.js running + doc-kit. +- `repository` {string} GitHub repository as `owner/name`, used for source + links and the repository link in the site's navigation bar. Without one, + that link is left out. +- `ref` {string} Git branch, tag, or commit that links into the repository + point to, such as edit links. The site's source links follow `jsx-ast.ref` + instead, which defaults to `'main'` (see [Precedence](#precedence)). + **Default:** `'HEAD'`. +- `baseURL` {string} URL the docs are published at, without a trailing slash, + such as `'https://my-library.dev/docs'`. Generators build absolute URLs from + it, such as those in the sitemap and `llms.txt`. It must be a string: the + configuration is copied to worker threads, which can't receive a `URL` + object. +- `minify` {boolean} Minify the output where the generator supports it, such + as HTML pages, JSON files, and the search index. **Default:** `false`. +- `changelog` {string|Array} Release history for the site's version selector: + the path or URL of a changelog in the format of Node.js's `CHANGELOG.md`, or, + in a JavaScript file, an array of `{ version, isLts, isCurrent }` entries + with `SemVer` versions. See + [Publishing several versions](./publishing.md#publishing-several-versions). + **Default:** `[]`, which builds a single version without a selector. +- `index` {string|Array} The path or URL of an `index.md` whose links + (`* [Title](file.md)`) set the order of the documents in the JSON bundle, + the legacy HTML sidebar and the legacy JSON bundle, or, in a JavaScript + file, the parsed list of `{ section, api }` objects. +- `pathsToCopy` {Array} Files and directories the `html` generator copies into + the output directory. A string copies that path under its own name; an + object maps source paths to destinations inside the output, such as + `{ 'docs/images': 'images' }`. Paths that don't exist are skipped. + **Default:** `['assets', 'public', 'static']`. +- `markdown` {Object} Markdown plugins for every generator that processes + Markdown. See [Markdown plugins](#markdown-plugins). ## Execution options -Top-level, alongside `target` and `global`: +These sit at the top level, next to `target` and `global`: + +- `threads` {number} Worker threads to process with, at least 1. With `1`, + everything runs on the main thread. **Default:** the number of CPUs, up to + 4 (`1` on riscv64). +- `chunkSize` {number} Items each worker processes per task, at least 1. + **Default:** `10`. -- `threads` {number} Worker threads used for generation. Defaults to your - CPU count. Each worker's heap is limited to 512MB; to raise the limit, run - Node.js with `--max-old-space-size` (directly or through `NODE_OPTIONS`). -- `chunkSize` {number} Items processed per worker thread. **Default:** `10`. +Each worker's heap is limited to 512 MB. If a build runs out of memory, lower +`threads`, or raise the limit by running Node.js with `--max-old-space-size`, +directly or through `NODE_OPTIONS`. ## Generator options -Each generator documents its own options on its reference page — see the -[generators overview](./generators.md). Two commonly configured ones: +Each generator reads its options from the section named after it, which for +built-in generators is the target name. Every generator's reference page +lists its options: + +| Section | Its own options | Reference | +| -------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `html` | `title`, `pageURL`, `editURL`, `navigation`, `head`, `stylesheets`, `imports`, `components`, `templatePath`, and more | [Website](./generators/html.md), [Customizing the site](./customization.md) | +| `section-pages` | `maxDepth`, `exclude` | [Section pages](./generators/section-pages.md) | +| `sitemap` | `indexURL`, `pageURL` | [Sitemap](./generators/sitemap.md) | +| `llms-txt` | `templatePath`, `pageURL` | [llms.txt](./generators/llms-txt.md) | +| `llms-txt-full` | `pageURL` | [llms-full.txt](./generators/llms-txt-full.md) | +| `json`, `json-all` | `schemaURL` | [JSON](./generators/json.md), [JSON bundle](./generators/json-all.md) | +| `metadata` | `typeMap` | [API entries](./generators/metadata.md) | +| `jsx-ast` | `ref`, `generateNotFoundPage`, `showReadingTime` | [Page JSX](./generators/jsx-ast.md) | +| `man-page` | `fileName`, `templatePath`, `cliOptionsHeaderSlug`, `envVarsHeaderSlug` | [Man page](./generators/man-page.md) | +| `api-links` | `sourceURL` | [API links](./generators/api-links.md) | +| `legacy-html` | `templatePath`, `additionalPathsToCopy`, `ref`, `pageURL`, `editURL` | [Legacy HTML](./generators/legacy-html.md) | +| `legacy-html-all` | `templatePath` | [Legacy single-page HTML](./generators/legacy-html-all.md) | +| `legacy-json`, `legacy-json-all` | `sourceURL` | [Legacy JSON](./generators/legacy-json.md), [Legacy JSON bundle](./generators/legacy-json-all.md) | + +`metadata.typeMap`, in the complete example above, is a JSON file mapping type +names to the URLs documenting them, such as +`{ "MyThing": "https://example.com/my-thing" }`. + +A section also takes any global option, which then applies to that generator +alone. Its `markdown` is the exception: it adds to `global.markdown` instead +of replacing it. ```mjs displayName="doc-kit.config.mjs" export default { global: { - version: '1.2.0', minify: true, }, - metadata: { - typeMap: { - MyThing: 'https://example.com/docs/my-thing.html', - }, + // Keep the search index readable + 'orama-db': { + minify: false, }, }; ``` +Several options are templates: `{name}` is replaced by the configuration value +of that name, such as `{project}`, `{version}`, or `{baseURL}`, and +`{name|fallback}` uses `fallback` when the value isn't set. Each generator's +page lists the placeholders it adds, such as `{path}`. + ## Markdown plugins -`doc-kit` processes Markdown with [unified](https://unifiedjs.com/). -`global.markdown` adds plugins to every generator processing Markdown, and a -generator's own `markdown` adds plugins to it alone, after the global ones. +doc-kit processes Markdown with [unified](https://unifiedjs.com/). +`global.markdown` adds plugins to every generator that processes Markdown, +and a generator's own `markdown` adds plugins to that generator alone, after +the global ones: ```mjs displayName="doc-kit.config.mjs" export default { @@ -177,57 +256,82 @@ export default { markdown: { rehypePlugins: [ 'rehype-katex', - ['./plugins/rehype-diagrams.mjs', { theme: 'neutral' }], + ['./plugins/rehype-callouts.mjs', { theme: 'neutral' }], ], }, }, }; ``` -Each plugin is a module specifier (a package name, or a path relative to the -configuration file), alone or in a `[specifier, options]` pair. The module -default-exports the plugin, a list of plugins, or a unified preset. Markdown is -processed in worker threads, which import the plugins themselves, so options -must be serializable: configure a plugin taking functions in a module of your -own. - -- `remarkPlugins` {Array} The global ones run on each document once the `ast` - generator parses it, so every output sees their changes. A generator - rendering Markdown, such as `jsx-ast`, runs its own on what it renders. -- `rehypePlugins` {Array} Run on the HTML of the generators rendering - Markdown, such as `jsx-ast`, before code is highlighted. -- `recmaPlugins` {Array} Run on the JavaScript `jsx-ast` compiles the pages to. - -A generator only takes the plugins its pipeline has a place for: `jsx-ast` -takes all three kinds, `ast`, `metadata`, and `json` take remark plugins, and -generators such as `html` take none. Plugins listed for a generator that -doesn't take them are ignored with a warning. - -A generator runs each plugin once: listing one it already has, its own or a -global one, configures it instead, merging the options (arrays add up, and -objects merge). +`markdown` takes three lists, one for each kind of syntax tree: + +- `remarkPlugins` {Array} Plugins run on Markdown syntax trees. +- `rehypePlugins` {Array} Plugins run on HTML syntax trees. +- `recmaPlugins` {Array} Plugins run on JavaScript syntax trees. + +Each entry is a module specifier (a package name, or a path relative to the +file that lists it), alone or in a `[specifier, options]` pair. The module +default-exports the plugin, a list of plugins, or a unified preset. It can +instead export an async `load` function that takes the options and returns +the plugin, for setup a plugin can't do itself. + +Markdown is processed in worker threads, which import the plugins +themselves. Entries must therefore be specifiers, not imported functions, and +options must be plain data. To pass a plugin options that include functions, +configure it in a module of your own and list that module. + +### Where plugins run + +A generator only takes the lists its pipeline has a place for: + +| Generator | `remarkPlugins` | `rehypePlugins` | `recmaPlugins` | +| ----------------------------------- | ------------------------------------------------------ | ---------------------------------------------- | ---------------------------------------------- | +| `ast` | Global and its own, run on each document once parsed | No | No | +| `metadata`, `json`, `llms-txt-full` | Global and its own, to parse and serialize Markdown | No | No | +| `jsx-ast` | Its own only, run on each page before alerts are built | Global and its own, before code is highlighted | Global and its own, before the code is printed | + +Global remark plugins run on each document as `ast` parses it, so every +output sees their changes. The generators that parse or serialize Markdown +again load them too, so syntax extensions such as `remark-math` work there. +`jsx-ast` doesn't run them a second time. + +Plugins listed for a generator that doesn't take them, such as +`html.markdown`, are ignored with a warning. + +A generator runs each plugin once. Listing a plugin it already has, as one of +its own or a global one, configures that plugin instead of adding it again: +the options merge, with arrays added together and objects merged key by key. The plugins configured for `jsx-ast` don't apply to the fragments it renders -on their own: types, parameter descriptions, and change history notes. +on their own: types, the descriptions in typed lists, and change history +notes. ### Code highlighting `jsx-ast` highlights code with the `@doc-kit/core/plugins/shiki/rehype.mjs` -plugin, which runs [Shiki](https://shiki.style/) with every language it -bundles. To configure it, list it with options in -`jsx-ast.markdown.rehypePlugins` (with pnpm, add `@doc-kit/core` to your -dependencies, so your configuration file can resolve it): - -- `langs` {Array} More languages: grammars, or modules default-exporting them. -- `langAlias` {Object} Aliases of languages, such as `{ conf: 'ini' }`. -- `themes` {Object} The `light` and `dark` themes: names of themes Shiki - bundles, modules default-exporting themes, or themes. +plugin, which runs [Shiki](https://shiki.style/) with every language Shiki +bundles, loading each one the first time code uses it. Code in a language it +doesn't know is shown as plain text. + +To configure it, list it with options in `jsx-ast.markdown.rehypePlugins`. +Since `jsx-ast` already has the plugin, your options merge into it rather +than adding a second highlighter. With pnpm, add `@doc-kit/core` to your +dependencies so that your configuration file can resolve it. + +- `langs` {Array} More languages: grammars, or modules default-exporting a + grammar or a list of them. +- `langAlias` {Object} More names for languages, such as `{ conf: 'ini' }`. +- `themes` {Object} The themes code is shown in. **Default:** + `github-light-default` and a higher-contrast `nord`. + - `light` {string|Object} The light theme: the name of a theme Shiki + bundles, a module default-exporting a theme, or a theme. + - `dark` {string|Object} The dark theme, in the same forms. - `transformers` {Array} Modules default-exporting [Shiki transformers](https://shiki.style/guide/transformers), or lists of them. -These modules are paths relative to the working directory, or URLs, such as -the ones `import.meta.resolve()` gives: +The modules in these options are paths relative to the working directory, or +URLs, such as the ones `import.meta.resolve()` returns: ```mjs displayName="doc-kit.config.mjs" export default { @@ -248,31 +352,76 @@ export default { }; ``` -## Configuration Merging - -Configurations are merged in the following order (higher sources take -precedence): - -1. **CLI options** (command-line arguments) -2. **Configuration file** (discovered or selected with `--config-file`) -3. **Presets** (listed in the configuration file's `extends`) -4. **Default values** (built-in defaults) - -## CLI Options Mapping - -CLI options map to configuration properties: - -| CLI Option | Config Property | Example | -| ---------------------- | ------------------ | ------------------------- | -| `--input ` | `global.input` | `--input src/` | -| `--output ` | `global.output` | `--output dist/` | -| `--ignore ` | `global.ignore[]` | `--ignore test/` | -| `--minify` | `global.minify` | `--minify` | -| `--git-ref ` | `global.ref` | `--git-ref v20.0.0` | -| `--version ` | `global.version` | `--version 20.0.0` | -| `--changelog ` | `global.changelog` | `--changelog https://...` | -| `--index ` | `global.index` | `--index file://...` | -| `--type-map ` | `metadata.typeMap` | `--type-map file://...` | -| `--target ` | `target` | `--target json` | -| `--threads ` | `threads` | `--threads 4` | -| `--chunk-size ` | `chunkSize` | `--chunk-size 10` | +## Extending presets + +- `extends` {string|string[]} Presets to build your configuration on. A preset + is a JavaScript module whose default export is a configuration object, so a + team can share one as a package. Each entry is an import specifier or a path + relative to the configuration file. Later presets win over earlier ones, and + your own values win over all of them. + +```mjs displayName="doc-kit.config.mjs" +export default { + // Build the docs the way nodejs.org does: branding, URL layouts, and + // release history included + extends: '@node-core/doc-kit/config', + + global: { + input: ['doc/api/*.md'], + output: 'out', + }, + + html: { + // Your own values win over the preset's + title: '{project} {version} API reference', + }, +}; +``` + +Markdown plugins in a preset resolve from the preset's own file. A preset's +own `extends` isn't followed. + +The built-in defaults are project-neutral: no repository, site URL, release +history, or branding is assumed. The `@node-core/doc-kit/config` preset, from +the [`@node-core/doc-kit`](./packages/node.md) package, opts back into +everything Node.js-specific; install that package to use it. + +## Precedence + +When more than one source sets a value, the first of these wins: + +1. CLI flags. +2. The configuration file. +3. Presets from `extends`, later ones first. +4. Built-in defaults. + +Objects merge key by key. Arrays and other values replace the value beneath +them instead of adding to it, so `--input` replaces the configured `input`, +and a file's `global.markdown.remarkPlugins` replaces a preset's list. + +Each generator then reads its own section and takes from `global` only the +keys that section doesn't set. A generator's built-in defaults count as set: +`jsx-ast` defaults `ref` to `'main'`, so the site's source links point at +`main` unless you set `jsx-ast.ref`, whatever `global.ref` or `--git-ref` +says. + +## CLI flags + +Each [`doc-kit generate`](./cli.md#doc-kit-generate) flag sets one +configuration key, and wins over the file. `--config-file` picks the file +itself, and has no key. + +| Flag | Configuration key | +| ----------------------------- | ------------------ | +| `-t, --target ` | `target` | +| `-i, --input ` | `global.input` | +| `--ignore ` | `global.ignore` | +| `-o, --output ` | `global.output` | +| `-v, --version ` | `global.version` | +| `--git-ref ` | `global.ref` | +| `-c, --changelog ` | `global.changelog` | +| `--index ` | `global.index` | +| `--minify` | `global.minify` | +| `--type-map ` | `metadata.typeMap` | +| `-p, --threads ` | `threads` | +| `--chunk-size ` | `chunkSize` | diff --git a/docs/contributing/commands.md b/docs/contributing/commands.md index 611c5a221..46e86c26c 100644 --- a/docs/contributing/commands.md +++ b/docs/contributing/commands.md @@ -1,74 +1,159 @@ -# Creating Commands +# Adding a command -The `doc-kit` CLI is built on [Commander](https://www.npmjs.com/package/commander). -Each command is a module in `packages/cli/bin/commands/` whose default export -is a Commander `Command` instance; the CLI entry point registers every command -exported from `packages/cli/bin/commands/index.mjs`. +This page shows how to add a command to the `doc-kit` CLI +([`@doc-kit/cli`](../packages/cli.md), in `packages/cli`). Add one for a new +action. For a new output, write a [generator](../creating-generators.md) +instead: `doc-kit generate --target` already runs custom generators. -Today the CLI ships a single command, [`generate`](../cli.md). +The CLI is built on [Commander](https://github.com/tj/commander.js). Each +command is a module in `packages/cli/bin/commands/` whose default export is a +Commander `Command`. `packages/cli/bin/cli.mjs` adds every command listed in +`packages/cli/bin/commands/index.mjs` to the program. The commands that exist +today are documented on the [CLI](../cli.md) page. -## Creating a New Command +## Create the command -### Step 1: Create the Command File - -Create a new file in `packages/cli/bin/commands/` with your command name: - -```mjs displayName="packages/cli/bin/commands/my-command.mjs" +```js displayName="packages/cli/bin/commands/my-command.mjs" +import logger from '@doc-kit/core/logger/index.mjs'; import { Command, Option } from 'commander'; import { errorWrap } from '../utils.mjs'; export default new Command('my-command') .description('Does something useful') - .addOption(new Option('-f, --force', 'Force overwrite existing files')) + .addOption(new Option('-f, --force', 'Overwrite existing files')) .action( errorWrap(async opts => { - // Your command logic here + logger.info(`Running with force: ${Boolean(opts.force)}`); }) ); ``` -`errorWrap` catches both synchronous and asynchronous errors, logs them, and -exits with a non-zero status — wrap every action with it so failures are -reported consistently. +Wrap every action in `errorWrap` from `packages/cli/bin/utils.mjs`. It awaits +the action, logs anything it throws (synchronously or not) with the doc-kit +logger, and exits with code 1. To fail a command, throw an `Error`. + +Log through the logger from `@doc-kit/core/logger/index.mjs` rather than +`console`. A `preAction` hook in `cli.mjs` sets its level from `--log-level` +before any command runs. -### Step 2: Register the Command +## Register it -Add your command to the exports in `packages/cli/bin/commands/index.mjs`: +Import the command in `packages/cli/bin/commands/index.mjs` and add it to the +exported array: -```mjs displayName="packages/cli/bin/commands/index.mjs" +```js displayName="packages/cli/bin/commands/index.mjs" import generate from './generate.mjs'; -import myCommand from './my-command.mjs'; // Add this +import myCommand from './my-command.mjs'; +import preview from './preview.mjs'; -export default [ - generate, - myCommand, // Add this -]; +export default [generate, preview, myCommand]; ``` -The CLI in `packages/cli/bin/cli.mjs` registers every command in that array, -so no further changes are needed. +Nothing else needs to change. Commander builds the help text from the +command's description and options, so `doc-kit --help` lists the new command +and `doc-kit my-command --help` shows its options. -## Command Options +## Options and arguments -Options use Commander's `Option` class directly; see the [Commander -documentation](https://www.npmjs.com/package/commander#options) for the full -API. +Define options with Commander's `Option` class, as `generate.mjs` does, and +positional arguments with `.argument()`. The action receives the positional +arguments first, then the parsed options, then the `Command` itself. -```js -.addOption(new Option('-i, --input ', 'Input file patterns (glob)')) -.addOption(new Option('-o, --output ', 'The output directory')) -.addOption( - new Option('--log-level ', 'Log level').choices(['debug', 'info']) -) +```js displayName="packages/cli/bin/commands/my-command.mjs" +export default new Command('my-command') + .argument('', 'What to act on') + .addOption(new Option('-o, --output ', 'The output directory')) + .addOption(new Option('--count ', 'How many').argParser(Number)) + .addOption( + new Option('--mode ', 'How to run') + .choices(['fast', 'safe']) + .default('safe') + ) + .action( + errorWrap(async (name, opts) => { + // opts.output, opts.count, opts.mode + }) + ); ``` -### Flag Syntax +| Syntax | Meaning | +| -------------------- | ----------------------------------------------- | +| `--flag` | Boolean, `true` when present | +| `--name ` | Takes a required value | +| `--name [value]` | Takes an optional value | +| `--name ` | Takes one or more values, collected in an array | + +Values arrive as strings unless you add an `.argParser()`. Multi-word flags +are camelCased on the options object, so `--git-ref` becomes `opts.gitRef`. +See the [Commander options documentation](https://github.com/tj/commander.js#options) +for the full API. + +`--log-level` is defined on the program, not on each command, so it works on +either side of the command name (`doc-kit --log-level debug my-command` or +`doc-kit my-command --log-level debug`). Don't add it to your command. -- `` - Required argument -- `[value]` - Optional argument -- `` - Variadic (multiple values) -- `[values...]` - Optional variadic +## Running generators from a command + +A command that builds docs can follow what `generate` does: + +```js displayName="packages/cli/bin/commands/my-command.mjs" +import createGenerator from '@doc-kit/core'; +import { + assertRunnableOptions, + setConfig, +} from '@doc-kit/core/utils/configuration/index.mjs'; +import { Command, Option } from 'commander'; + +import { errorWrap } from '../utils.mjs'; + +const { runGenerators } = createGenerator(); + +export default new Command('my-command') + .description('Builds the docs, then does something useful') + .addOption(new Option('--config-file ', 'Config file')) + .addOption( + new Option('-i, --input ', 'Input file patterns (glob)') + ) + .addOption(new Option('-t, --target ', 'Target generator(s)')) + .action( + errorWrap(async opts => { + const config = await setConfig(opts); + assertRunnableOptions(config); + + await runGenerators(config); + }) + ); +``` -One global option, `--log-level`, is defined on the program itself in -`packages/cli/bin/cli.mjs` and applies to every command. +`setConfig` loads the +[configuration file](../configuration.md#where-configuration-lives) +(`opts.configFile`, or the first one it finds), merges in the command-line +options, and fills in the generators' defaults. `assertRunnableOptions` then +throws when no `target` or `input` is set. + +`setConfig` reads options by the names `generate` gives them (`configFile`, +`input`, `target`, `output`, `gitRef`, and so on), so declare the flags your +command needs with the same names. A flag you don't declare can't be passed, +and its value comes only from the configuration file. To turn a new option +into configuration, map it in `createConfigFromCLIOptions` +(`packages/core/src/utils/configuration/index.mjs`). + +## Before you open a pull request + +- **Try it** from the repository root with + `node --run run -- my-command --help`, which runs + `node packages/cli/bin/cli.mjs`. +- **Keep the command thin.** `@doc-kit/cli` publishes only `bin/` (see + `files` in `packages/cli/package.json`), and `node --run test` only picks up + tests under `packages/*/src/` and `scripts/`, never `bin/`. Put logic worth + testing in `@doc-kit/core`, with its tests in a `__tests__` directory next + to it. +- **Document it** with a section on the [CLI page](../cli.md) (`docs/cli.md`), + using the same option-list format as the existing commands, and in + `packages/cli/README.md` if it changes how people use the package. If it + adds a flag that sets configuration, also add the flag to the + [CLI flags](../configuration.md#cli-flags) table. +- **Add a changeset** with `node --run changeset`, since `@doc-kit/cli` is + published. See + [Adding a changeset](https://github.com/nodejs/doc-kit/blob/main/CONTRIBUTING.md#adding-a-changeset). diff --git a/docs/contributing/comparators.md b/docs/contributing/comparators.md index 2743a7e94..4d6c74349 100644 --- a/docs/contributing/comparators.md +++ b/docs/contributing/comparators.md @@ -1,183 +1,202 @@ -# Creating Comparators +# Adding a comparator + +A comparator is a small Node.js script that compares one generator's output on +`main` with its output on a pull request and prints a Markdown report. CI +collects these reports into a comment on the pull request, so you and the +reviewers can see what a change does to the generated docs, and to generation +time and memory, before it lands. + +Comparators live in `scripts/comparators/` and run only in this repository's +CI. They are not part of any published package. + +## How the comparison runs + +Two workflows work together: + +1. `.github/workflows/generate.yml` ("Generate and Compare Docs") runs on every + push to `main`. It records the latest nodejs/node commit, generates every + target in its matrix from that commit's docs, and uploads each target's + output directory as an artifact named after the target. +2. On a pull request, the same workflow finds the `main` run for the pull + request's base commit and reuses its Node.js commit. Both sides are built + from the same sources, so any difference comes from the pull request. +3. Each matrix entry generates into `out/`. Those with a `compare` field then + download the target's artifact from the `main` run into `base/` and run + `node scripts/comparators/.mjs > out/comparison.txt`. +4. When the workflow finishes, `.github/workflows/leave-comment.yml` joins + every `comparison.txt` and posts the result as a single comment on the pull + request (tagged `compared`, so later pushes update it). If every report is + empty, it posts nothing. + +Each generation runs under `/usr/bin/time`, which writes `out/benchmark.json` +with the elapsed time, user and system CPU time, and peak resident memory. +Comparators read it for their performance section and leave it out of the +output comparison. + +Matrix entries without `compare` (at the moment `man-page`, `addon-verify` and +`json-simple`) are generated and uploaded, but not compared. + +## Built-in comparators + +| Comparator | What it reports | Used by | +| ------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `file-size` | Files added, removed or changed in size, largest change first, with the net change, in a collapsible table | `web` (the former name of [`html`](../generators/html.md)), [`orama-db`](../generators/orama-db.md), [`llms-txt`](../generators/llms-txt.md), [`legacy-html`](../generators/legacy-html.md) | +| `object-assertion` | Files added or removed, and an `assert.deepStrictEqual` diff for each file whose parsed JSON differs, one per file | [`json`](../generators/json.md), [`api-links`](../generators/api-links.md), [`legacy-json`](../generators/legacy-json.md) | + +Both end with a **Performance estimate** section: the generation time and peak +memory of each side, read from `benchmark.json`. It comes from a single CI run, +so treat it as a rough signal. It's left out when either side has no +`benchmark.json`. + +Keep their limits in mind when you pick one: + +- `file-size` compares sizes only. An edit that keeps a file's size the same + doesn't show up. +- `object-assertion` parses every output file as JSON, so it only suits + generators whose output is all JSON. + +## Shared helpers + +| Import | From | What it gives you | +| ----------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `BASE`, `HEAD` | `scripts/constants.mjs` | The two build directories: `base/` and `out/` at the repository root (both gitignored), unless the `BASE` and `HEAD` environment variables point elsewhere. | +| `TITLE` | `scripts/constants.mjs` | The report heading, ``## `` Generator``, built from the `GENERATOR` environment variable, which CI sets to the target name. Set the `TITLE` variable to replace it. | +| `BENCHMARK_FILE`, `COMPARISON_FILE` | `scripts/constants.mjs` | The metadata file names, `benchmark.json` and `comparison.txt`. | +| `listOutputFiles(directory)` | `scripts/comparators/files.mjs` | Every file under `directory`, recursively, as sorted relative paths, without the two metadata files. | +| `comparePerformance()` | `scripts/comparators/performance.mjs` | The performance section as Markdown, or an empty string when either side has no `benchmark.json`. It throws if a benchmark value isn't a finite number. | + +## Writing a comparator + +A comparator is an ES module in `scripts/comparators/`. Its file name without +`.mjs` is the value of the `compare` field in the workflow matrix. It should: + +- Read the two builds from `BASE` and `HEAD`, listing their files with + `listOutputFiles`, which skips `benchmark.json` and the `comparison.txt` + that the report is being written to. +- Print Markdown to standard output, starting with `TITLE`, and print nothing + when there's nothing to report. +- Report differences instead of failing on them. A comparator that exits with + an error fails the CI job, so throw only when something is actually broken. +- End with `comparePerformance()`, like the built-in comparators, so the + timing stays visible. + +### Create the script + +This example lists every file whose bytes changed, which catches the +same-size edits that `file-size` misses: + +```mjs displayName="scripts/comparators/changed-files.mjs" +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; -This guide explains how to create build comparison scripts for `@doc-kit/core`. Comparators help identify differences between documentation builds, useful for CI/CD and regression testing. +import { BASE, HEAD, TITLE } from '../constants.mjs'; +import { listOutputFiles } from './files.mjs'; +import { comparePerformance } from './performance.mjs'; -## Comparator Concepts +const [baseFiles, headFiles] = await Promise.all( + [BASE, HEAD].map(directory => listOutputFiles(directory)) +); +const baseSet = new Set(baseFiles); +const headSet = new Set(headFiles); -Comparators are scripts that: +const describe = async file => { + if (!baseSet.has(file)) { + return `- \`${file}\` added`; + } -1. **Compare** generated documentation between two builds (base vs. head) -2. **Identify differences** in content, structure, file size, or performance -3. **Report results** in a format suitable for CI/CD systems -4. **Help catch regressions** before merging changes + if (!headSet.has(file)) { + return `- \`${file}\` removed`; + } -### When to Use Comparators + const [base, head] = await Promise.all( + [BASE, HEAD].map(directory => readFile(join(directory, file))) + ); -- **Verify backward compatibility** - Ensure new code produces same output -- **Track file size changes** - Monitor bundle size growth -- **Catch performance regressions** - Compare elapsed time, CPU time, and peak memory -- **Validate transformations** - Check that refactors don't alter output -- **Debug generation issues** - Understand what changed between versions + return base.equals(head) ? null : `- \`${file}\` changed`; +}; -## Comparator Structure +const files = [...new Set([...baseFiles, ...headFiles])].sort(); +const changes = (await Promise.all(files.map(describe))).filter(Boolean); -Comparators are standalone ESM scripts located in `scripts/comparators/`, -sharing the `BASE`, `HEAD`, and `TITLE` constants from `scripts/constants.mjs`: +const sections = []; -```text -scripts/ -├── constants.mjs # Shared constants (BASE, HEAD, TITLE) -└── comparators/ - ├── file-size.mjs # Compare file sizes and performance between builds - ├── files.mjs # Shared output-file listing helpers - ├── object-assertion.mjs # Deep equality assertion for JSON objects - ├── performance.mjs # Compare benchmark measurements - └── your-comparator.mjs # Your new comparator -``` +if (changes.length) { + sections.push( + `**Output:** ${changes.length} ${changes.length === 1 ? 'file differs' : 'files differ'}`, + changes.join('\n') + ); +} -### Naming Convention +const performance = await comparePerformance(); -Comparators can be reused across multiple generators. You specify which comparator to use in the workflow file using the `compare` field. For example: +if (performance) { + sections.push(performance); +} -- `file-size.mjs` can compare output from `html`, `legacy-html`, or any generator -- `object-assertion.mjs` can compare JSON output from `legacy-json`, `json-simple`, etc. -- `my-comparator.mjs` would be a custom comparator for specific needs +if (sections.length) { + console.log(`${TITLE}\n\n${sections.join('\n\n')}\n`); +} +``` -The generation workflow also stores timing, CPU, and peak resident memory in -`benchmark.json`. The built-in comparators include these measurements in their -Markdown report and exclude the metadata file from output comparisons. If a -base artifact predates performance measurement, the output comparison still -runs and the performance section is omitted. +### Run it locally -## Creating a Comparator +Build the same target from `main` into `base/` and from your branch into +`out/`, with the same Node.js checkout for both (see [Running the tool +locally](https://github.com/nodejs/doc-kit/blob/main/CONTRIBUTING.md#running-the-tool-locally) +for a sparse checkout of the Node.js docs), then run the script: -### Step 1: Create the Comparator File +```bash +git switch main +node packages/cli/bin/cli.mjs generate -t man-page -i ../node/doc/api/cli.md -o base -Create a new file in `scripts/comparators/` with the same name as your generator: +git switch my-branch +node packages/cli/bin/cli.mjs generate -t man-page -i ../node/doc/api/cli.md -o out -```mjs displayName="scripts/comparators/my-format.mjs" -import { readdir, readFile } from 'node:fs/promises'; -import { join } from 'node:path'; +GENERATOR=man-page node scripts/comparators/changed-files.mjs +``` -import { BASE, HEAD, TITLE } from '../constants.mjs'; +Local builds have no `benchmark.json`, so the report has no performance +section. -// Fetch files from both directories -const [baseFiles, headFiles] = await Promise.all( - [BASE, HEAD].map(dir => readdir(dir)) -); +### Add a test -// Find all unique files across both builds -const allFiles = [...new Set([...baseFiles, ...headFiles])]; - -/** - * Compare a single file between base and head - * @param {string} file - Filename to compare - * @returns {Promise} Difference object or null if identical - */ -const compareFile = async file => { - const basePath = join(BASE, file); - const headPath = join(HEAD, file); - - try { - const baseContent = await readFile(basePath, 'utf-8'); - const headContent = await readFile(headPath, 'utf-8'); - - if (baseContent !== headContent) { - return { - file, - type: 'modified', - baseSize: baseContent.length, - headSize: headContent.length, - }; - } - - return null; - } catch (error) { - // File missing in one of the builds - const exists = await Promise.all([ - readFile(basePath, 'utf-8') - .then(() => true) - .catch(() => false), - readFile(headPath, 'utf-8') - .then(() => true) - .catch(() => false), - ]); - - if (exists[0] && !exists[1]) { - return { file, type: 'removed' }; - } - if (!exists[0] && exists[1]) { - return { file, type: 'added' }; - } - - return { file, type: 'error', error: error.message }; - } -}; +Comparator tests live in `scripts/__tests__/comparators.test.mjs` and run with +`node --run test`. The file's `createDirectories` helper creates temporary base +and head directories, and `runComparator` runs a comparator against them as a +child process with `GENERATOR=test`: -// Compare all files in parallel -const results = await Promise.all(allFiles.map(compareFile)); - -// Filter out null results (identical files) -const differences = results.filter(Boolean); - -// Output markdown results -if (differences.length > 0) { - console.log(TITLE); - console.log(''); - console.log(`Found ${differences.length} difference(s):`); - console.log(''); - - // Group by type - const added = differences.filter(d => d.type === 'added'); - const removed = differences.filter(d => d.type === 'removed'); - const modified = differences.filter(d => d.type === 'modified'); - - if (added.length) { - console.log('### Added Files'); - console.log(''); - added.forEach(d => console.log(`- \`${d.file}\``)); - console.log(''); - } +```mjs displayName="scripts/__tests__/comparators.test.mjs" +test('changed-files comparator reports same-size edits', async t => { + const { base, head } = await createDirectories(t); - if (removed.length) { - console.log('### Removed Files'); - console.log(''); - removed.forEach(d => console.log(`- \`${d.file}\``)); - console.log(''); - } + await Promise.all([ + writeFile(path.join(base, 'node.1'), 'base', 'utf8'), + writeFile(path.join(head, 'node.1'), 'head', 'utf8'), + ]); - if (modified.length) { - console.log('### Modified Files'); - console.log(''); - console.log('| File | Base Size | Head Size | Diff |'); - console.log('|-|-|-|-|'); - modified.forEach(({ file, baseSize, headSize }) => { - const diff = headSize - baseSize; - const sign = diff > 0 ? '+' : ''; - console.log( - `| \`${file}\` | ${baseSize} | ${headSize} | ${sign}${diff} |` - ); - }); - console.log(''); - } -} -``` + const result = await runComparator('changed-files', base, head); -### Step 2: Test Locally + assert.match(result, /## `test` Generator/); + assert.match(result, /`node\.1` changed/); +}); +``` -Run your comparator locally to verify it works: +### Turn it on in CI -```bash -# Set up BASE and HEAD directories -export BASE=path/to/base/output -export HEAD=path/to/head/output +Set `compare` on the matrix entries in `.github/workflows/generate.yml` that +should use it. For example, to compare the man page: -# Run the comparator -node scripts/comparators/my-format.mjs +```yaml displayName=".github/workflows/generate.yml" +- target: man-page + input: './node/doc/api/cli.md' + compare: changed-files ``` -### Step 3: Integrate with CI/CD - -The comparator will automatically run in GitHub Actions when: +The compare step runs in the pull request's own checkout, so the pull request +that adds a comparator already uses it. The base side is the target's artifact +from the `main` run, which every matrix entry uploads whether or not it's +compared. -1. Your generator is configured with `compare: ` in the workflow, which tells the system which comparator script to run +A target that isn't in the matrix on `main` yet has no artifact there, so its +download step fails. Add its matrix entry without `compare` first, and turn +comparison on in a later pull request. diff --git a/docs/creating-generators.md b/docs/creating-generators.md index c2912d335..7eeb0d74f 100644 --- a/docs/creating-generators.md +++ b/docs/creating-generators.md @@ -1,592 +1,564 @@ -# Creating Generators +# Creating generators -This guide explains how to create new documentation generators for `@doc-kit/core`. +Every output of doc-kit is a generator, and yours can be one too: an RSS +feed, a changelog, TypeScript declarations, anything the API entries can +become. This guide starts with a generator in one file, then adds options, +worker threads and streaming, and ends with publishing it as a package. -## Generator Concepts +## Your first generator -Generators in `doc-kit` transform API documentation through a pipeline. Each generator: +A generator is a module whose default export gives its `name`, the generator +it reads from (`dependsOn`), and a `generate` function that receives that +generator's output. This one lists every experimental API: -1. **Takes input** from a previous generator or raw files -2. **Processes the data** into a different format -3. **Yields output** for the next generator or final output - -### Generator Pipeline - -```text -Raw Markdown Files - ↓ - [ast] - Parse to MDAST - ↓ - [metadata] - Extract structured metadata - ↓ - [jsx-ast] - Convert to JSX AST - ↓ - [html] - Generate HTML/CSS/JS bundles -``` - -Each generator declares its dependency using the `dependsOn` field, allowing automatic pipeline construction. Each stage is documented alongside the rest of the [generators](./generators.md#pipeline-stages). - -## Generator Structure - -A generator is defined as a module exporting an object conforming to the `GeneratorMetadata` interface. - -## Creating a Basic Generator - -### Step 1: Create the Generator Files - -Create a new directory in your project: - -```text -/ -├── index.mjs # Generator metadata (required) -├── generate.mjs # Generator implementation (required) -├── constants.mjs # Constants (optional) -├── types.d.ts # TypeScript types (required) -└── utils/ # Utility functions (optional) - └── formatter.mjs -``` - -### Step 2: Define Types - -Create a `types.d.ts` file containing a `Generator` export. Use this when typing your generator. - -```typescript displayName="types.d.ts" -export type Generator = GeneratorMetadata< - { - // If your generator supports a custom configuration, - // define it here - myCustomOption: string; - }, - Generate>, - // If your generator supports parallel processing: - ProcessChunk< - InputToMyParallelProcessor, - OutputOfMyParallelProcessor, - DependenciesOfMyParallelProcessor - > ->; -``` - -### Step 3: Define Generator Metadata - -A generator module's default export is a plain object with its metadata and -implementation. Create it in `index.mjs`: +```mjs displayName="experimental.mjs" +import { join } from 'node:path'; -```mjs displayName="index.mjs" -import { generate } from './generate.mjs'; +import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; +import { writeFile } from '@doc-kit/core/utils/file.mjs'; -/** - * Generates output in MyFormat. - * - * @type {import('./types').Generator} - */ export default { - name: 'my-format', + name: 'experimental', + description: 'Lists every experimental API', - description: 'Generates documentation in MyFormat', - - // This generator depends on the metadata generator. Dependencies are - // declared as import specifiers, so they can live in any package. + // The typed API entries, the same the site and the JSON are built from dependsOn: '@doc-kit/core/metadata', - defaultConfiguration: { - // If your generator supports a custom configuration, define the defaults here - myCustomOption: 'myDefaultValue', + async generate(entries) { + const { output } = getConfig('experimental'); - // All generators support options in the GlobalConfiguration object - // To override the defaults, they can be specified here - ref: 'overriddenRef', - }, + const list = entries + .filter(entry => entry.stability?.data.index === '1') + .map(entry => `- ${entry.heading.data.name} (${entry.api})`); - generate, + await writeFile(join(output, 'experimental.md'), list.join('\n')); + }, }; ``` -### Step 4: Implement the Generator Logic - -Create the generator implementation in `generate.mjs`: - -```mjs displayName="generate.mjs" -import { writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; - -import getConfig from '../../utils/configuration/index.mjs'; - -/** - * Main generation function - * - * @type {import('./types').Generator['generate']} - */ -export async function generate(input, worker) { - const config = getConfig('my-format'); - - // Transform input to your format - const result = transformToMyFormat(input, config.version); - - // Write to file if output directory specified - if (config.output) { - await writeFile( - join(config.output, 'documentation.myformat'), - result, - 'utf-8' - ); - } - - return result; -} - -/** - * Transform metadata entries to MyFormat - * @param {Array} entries - * @param {import('semver').SemVer} version - * @returns {string} - */ -function transformToMyFormat(entries, version) { - // Your transformation logic here - return entries - .map(entry => `${entry.api}: ${entry.heading.data.name}`) - .join('\n'); -} -``` - -### Step 5: Make the Generator Loadable - -Generators are loaded dynamically by import specifier. Anything that resolves -to a module whose default export is a generator works as a `--target`: +Pass its path as a [target](./cli.md#doc-kit-generate), next to any others. +Here it runs next to the website (`html`), which comes from +`@doc-kit/generator-react`, so [install](./getting-started.md#install) that +package beside the CLI: ```bash -# A package (subpath) export -npx @doc-kit/cli generate -t @my-scope/my-package/my-format ... - -# A local file -npx @doc-kit/cli generate -t ./generators/my-format/index.mjs ... +npx doc-kit generate -t html -t ./experimental.mjs -i "docs/**/*.md" -o out ``` -Built-in generators additionally get a shorthand alias in -`packages/core/src/generators/index.mjs`, which maps the name users type to -the import specifier it resolves to: +It reads the same parse of the docs as `html`, and writes +`out/experimental.md`. (With pnpm, add `@doc-kit/core` to your dependencies +so the module can import it.) -```mjs displayName="packages/core/src/generators/index.mjs" -export const publicGenerators = { - 'json-simple': '@doc-kit/core/json-simple', - 'my-format': '@doc-kit/core/my-format', // Add this - // ... other generators -}; -``` +## How generators fit together -If the generator lives in this repository, also add a matching subpath to the -`exports` map of its package's `package.json`. +Each generator declares the generator it reads from with `dependsOn`. doc-kit +follows those links from the targets it's given, loads every generator they +need, and runs the ones that don't depend on one another at the same time. +The shared stages are documented with the rest of the +[generators](./generators.md#pipeline-stages). -## Parallel Processing with Workers +Besides `name`, `dependsOn` and `generate`, a generator can declare default +options, a function to run in worker threads, and a Markdown pipeline. The +rest of this guide covers them one at a time, and +[`@doc-kit/core`](./packages/core.md#writing-a-generator) lists every property +in one place. -For generators processing large datasets, implement parallel processing using worker threads. +## Configuration -### Implementing Worker-Based Processing +`defaultConfiguration` holds the defaults of the generator's options. Users +override them in a section named after the generator in their +[configuration file](./configuration.md), and `getConfig(name)` returns the +result: -First, define the generator metadata in `index.mjs`: +```mjs displayName="experimental.mjs" +import { join } from 'node:path'; -```mjs displayName="index.mjs" -import { generate, processChunk } from './generate.mjs'; +import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; +import { writeFile } from '@doc-kit/core/utils/file.mjs'; -/** - * @type {import('./types').Generator} - */ export default { - name: 'parallel-generator', - - description: 'Processes data in parallel', - + name: 'experimental', + description: 'Lists every experimental API', dependsOn: '@doc-kit/core/metadata', - // Indicates this generator has a processChunk implementation - hasParallelProcessor: true, - - generate, - processChunk, -}; -``` + defaultConfiguration: { + // The file the list is written to, in the output directory + fileName: 'experimental.md', + }, -Then, implement both `processChunk` and `generate` in `generate.mjs`: - -```mjs displayName="generate.mjs" -import getConfig from '../../utils/configuration/index.mjs'; - -/** - * Process a chunk of items in a worker thread. - * This function runs in isolated worker threads. - * - * @type {import('./types').Generator['processChunk']} - */ -export async function processChunk(fullInput, itemIndices, deps) { - const results = []; - - // Process only the items at specified indices - for (const idx of itemIndices) { - const item = fullInput[idx]; - const result = await processItem(item, deps); - results.push(result); - } + async generate(entries) { + const { output, fileName } = getConfig('experimental'); - return results; -} + const list = entries + .filter(entry => entry.stability?.data.index === '1') + .map(entry => `- ${entry.heading.data.name} (${entry.api})`); -/** - * Main generation function that orchestrates worker threads - * - * @type {import('./types').Generator['generate']} - */ -export async function* generate(input, worker) { - // Configuration for this generator is based on its name - const config = getConfig('my-format'); - - // Prepare serializable dependencies - const deps = { - version: config.version, - // ...other config - }; - - // Stream chunks as they complete - for await (const chunkResult of worker.stream(input, deps)) { - // Process chunk result if needed - yield chunkResult; - } -} + await writeFile(join(output, fileName), list.join('\n')); + }, +}; ``` -### Key Points for Worker Processing - -1. **`processChunk` executes in worker threads** - No access to main thread state -2. **Only serializable data** can be passed to workers (no functions, classes, etc.) -3. **`fullInput` and `itemIndices`** - Workers receive full input but only process specified indices -4. **`deps` must be serializable** - Pass only JSON-compatible data - -### When to Use Workers - -Use parallel processing when: - -- Processing many independent items (files, modules, entries) -- Each item takes significant time to process -- Operations are CPU-intensive - -Don't use workers when: - -- Items have dependencies on each other -- Output must be in specific order -- Operation is I/O bound rather than CPU bound - -## Streaming Results - -Generators can yield results as they're produced using async generators. - -Define the generator metadata in `index.mjs`: - -```mjs displayName="index.mjs" -import { generate, processChunk } from './generate.mjs'; - -/** - * @type {import('./types').Generator} - */ +```mjs displayName="doc-kit.config.mjs" export default { - name: 'streaming-generator', - - description: 'Streams results as they are ready', - - dependsOn: '@doc-kit/core/metadata', - - hasParallelProcessor: true, - - generate, - processChunk, + experimental: { + fileName: 'unstable.md', + }, }; ``` -Implement the generator in `generate.mjs`: - -```mjs displayName="generate.mjs" -/** - * Process a chunk of data - * - * @type {import('./types').Generator['processChunk']} - */ -export async function processChunk(fullInput, itemIndices, deps) { - // Process chunk - return results; -} - -/** - * Generator function that yields results incrementally - * - * @type {import('./types').Generator['generate']} - */ -export async function* generate(input, worker) { - // Stream results as workers complete chunks - for await (const chunkResult of worker.stream(input, {})) { - // Yield immediately - downstream can start processing - yield chunkResult; - } -} -``` - -### Benefits of Streaming - -- **Reduced memory usage** - Process data in chunks -- **Earlier downstream starts** - Next generator can begin before this one finishes -- **Better parallelism** - Multiple generators can work simultaneously - -### Non-Streaming Generators - -Some generators must collect all input before processing. +- `getConfig(name)` is the default export of + `@doc-kit/core/utils/configuration/index.mjs`. Besides the generator's own + options, what it returns holds every + [global option](./configuration.md#global-options) the generator doesn't + set itself: `output`, `project`, `minify`, `ref`, and `version` (a `SemVer` + from the `semver` package) among them. +- A default for a global option wins over the global value for that + generator: `jsx-ast` defaults `ref` to `'main'`. Users still change it in + the generator's own section. +- `defaultConfiguration` can also be a function, which receives the user's + configuration (the configuration file and command-line options, with + targets resolved to import specifiers) and returns the defaults. The + [`html`](./generators/html.md) generator uses one to show its search box + only when the search index is also a target. +- `getConfig()` without a name returns the whole configuration on the main + thread. [Worker threads](#worker-threads) only get the generator's own + section. + +## Input and output + +`generate(input, worker)` receives the complete output of the generator it +depends on. When that generator streams, its chunks arrive collected in one +array. These are the stages most generators read from: + +| `dependsOn` | `input` | +| ---------------------------------- | --------------------------------------------------------------------------------------- | +| `@doc-kit/core/metadata` | The [API entries](./generators/metadata.md), one per heading. Most generators use this. | +| `@doc-kit/core/ast` | The [Markdown files](./generators/ast.md), each parsed as `{ tree, path, mdx }`. | +| `@doc-kit/core/ast-js` | The [`.js` files](./generators/ast-js.md) among the inputs, each parsed to ESTree. | +| `@doc-kit/generator-react/jsx-ast` | The [pages](./generators/jsx-ast.md), each with its content compiled to JSX code. | + +To read from a generator of your own, point `dependsOn` at its module with +`import.meta.resolve('./other.mjs')`, since a relative path resolves from the +working directory. Without a `dependsOn`, `generate` receives `undefined` and +reads its own sources, as [`ast`](./generators/ast.md) does. + +What `generate` returns, or resolves to, is what the generators depending on +it receive. The CLI doesn't print or save a target's result, so a generator +producing an output writes its files itself. Many do both: + +```mjs displayName="experimental.mjs" +import { join } from 'node:path'; -Generator metadata in `index.mjs`: +import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; +import { writeJSON } from '@doc-kit/core/utils/file.mjs'; -```mjs displayName="index.mjs" -import { generate } from './generate.mjs'; - -/** - * @type {import('./types').Generator} - */ export default { - name: 'batch-generator', + name: 'experimental', + description: 'Lists every experimental API', + dependsOn: '@doc-kit/core/metadata', - description: 'Requires all input at once', + async generate(entries) { + const { output, minify } = getConfig('experimental'); - dependsOn: '@doc-kit/generator-react/jsx-ast', + const experimental = entries.filter( + entry => entry.stability?.data.index === '1' + ); - generate, + if (output) { + await writeJSON(join(output, 'experimental.json'), experimental, minify); + } + + // What the generators reading from this one receive + return experimental; + }, }; ``` -Implementation in `generate.mjs`: +`@doc-kit/core/utils/file.mjs` has two helpers for writing: -```mjs displayName="generate.mjs" -/** - * Non-streaming - returns Promise instead of AsyncGenerator - * - * @type {import('./types').Generator['generate']} - */ -export async function generate(input, worker) { - // Collect all input (if dependency is streaming, this waits for completion) - const allData = await collectAll(input); +- `writeFile(file, data[, options])` writes a file as + `fs.promises.writeFile()` does, creating its directory first. +- `writeJSON(file, value[, minify])` writes a value as JSON, indented unless + `minify` is `true`. - // Process everything together - const result = processBatch(allData); +`output` is `undefined` when no output directory is given, which is why the +built-in generators check it before writing. To copy static files that ship +with your module, use `cp()` from `node:fs/promises`: - return result; -} +```mjs +await cp(new URL('./assets', import.meta.url), join(output, 'assets'), { + recursive: true, +}); ``` -Use non-streaming when: - -- You need all data to make decisions (e.g., code splitting, global analysis) -- Output format requires complete dataset -- Cross-references between items need resolution +## Worker threads -## Generator Dependencies +For heavy work on many independent items, give the generator a +`processChunk` and set `hasParallelProcessor`. `generate` then receives a +`worker`, whose `stream()` splits the items into chunks and runs +`processChunk` on them across worker threads: -### Declaring Dependencies - -In `index.mjs`: +```mjs displayName="experimental.mjs" +import { join } from 'node:path'; -```mjs displayName="index.mjs" -import { generate } from './generate.mjs'; +import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; +import { writeFile } from '@doc-kit/core/utils/file.mjs'; export default { - name: 'my-generator', - - // This generator requires the metadata generator's output. The dependency - // is an import specifier, so it may point at any installed package. + name: 'experimental', + description: 'Lists every experimental API', dependsOn: '@doc-kit/core/metadata', - // ... other metadata + hasParallelProcessor: true, - generate, -}; -``` + // Runs in a worker thread, once per chunk of entries + async processChunk(input, itemIndices, { marker }) { + return itemIndices + .map(index => input[index]) + .filter(entry => entry.stability?.data.index === '1') + .map(entry => `${marker} ${entry.heading.data.name} (${entry.api})`); + }, -In `generate.mjs`: + async generate(entries, worker) { + const { output } = getConfig('experimental'); + const lines = []; -```mjs displayName="generate.mjs" -export async function generate(input, worker) { - // input contains the output from 'metadata' generator -} -``` + for await (const chunk of worker.stream(entries, { marker: '-' })) { + lines.push(...chunk); + } -### Declaring a dependent + // Chunks arrive in the order they finish + lines.sort(); -`dependsOn` pulls another generator's output _in_. The inverse, `dependent`, -pushes a generator's output _into_ another generator's pipeline: + await writeFile(join(output, 'experimental.md'), lines.join('\n')); + }, +}; +``` -```mjs displayName="index.mjs" +Filtering entries is light work, but heavier work takes the same shape. + +- `worker.stream(items, deps)` splits `items` into chunks of + [`chunkSize`](./configuration.md#execution-options) items (10 by default), + runs them on up to `threads` workers, and yields each chunk's result as it + finishes, in the order they finish. Every `processChunk` call gets `deps`. +- `processChunk(input, itemIndices, deps)` processes the items at + `itemIndices` and resolves to an array of results. Read each item as + `input[index]`: in a worker, `input` holds only the chunk's items, but with + one thread, or two items or fewer, `processChunk` runs on the main thread + with the whole input. +- The items, `deps`, the results, and the generator's configuration section + are copied between threads with the + [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). + Functions can't be copied (passing one throws a `DataCloneError`), and class + instances arrive as plain objects. +- Each worker imports your module on its own, so `processChunk` sees nothing + `generate` set up: pass what it needs in `deps`. A module only + `processChunk` uses can be imported inside it, as the `metadata` generator + does, so the main thread never loads it. +- In a worker, `getConfig(name)` returns the generator's own section, with + the global options it inherits, and nothing else. Its `version` is a plain + object there, not a `SemVer`. + +## Streaming or collecting + +`generate` either resolves to its whole output, as in the examples above, or +streams it: as an async generator function, it yields its results in arrays, +usually each chunk as a worker finishes it. + +```mjs displayName="experimental.mjs" export default { - name: 'section-pages', - + name: 'experimental', + description: 'Lists every experimental API', dependsOn: '@doc-kit/core/metadata', - // Deliver this generator's output through `html` - dependent: '@doc-kit/generator-react/html', + hasParallelProcessor: true, - generate, + async processChunk(input, itemIndices, { marker }) { + return itemIndices + .map(index => input[index]) + .filter(entry => entry.stability?.data.index === '1') + .map(entry => `${marker} ${entry.heading.data.name} (${entry.api})`); + }, + + // Yields each chunk's lines as soon as a worker finishes them + async *generate(entries, worker) { + for await (const chunk of worker.stream(entries, { marker: '-' })) { + yield chunk; + } + }, }; ``` -The pipeline splices such a generator in front of the first generator on the -way to its dependent that consumes the same `dependsOn`. Here `html` depends on -`jsx-ast`, which depends on `metadata` — so `jsx-ast` is rewired to read from -`section-pages` instead: +doc-kit collects what a streaming generator yields into one array, once, and +hands that array to every generator reading from it: -```text -ast → metadata → section-pages → jsx-ast → html -``` +- Each value it yields must be an array, whose items are appended to the + output. +- The output keeps the order the values were yielded in, which for + `worker.stream()` is the order the chunks finished in. +- The generators reading from it start when the stream ends, with the + complete output. + +Streaming suits a stage other generators build on, whose output is just its +chunks' results, as `ast`, `metadata`, and `jsx-ast` are. Collect when the +generator needs every result before it can finish: to sort them, +cross-reference them, or write them to a single file. -Requesting a generator that declares a dependent runs the dependent's whole -pipeline, and the run's result is the dependent's output — `-t section-pages` -produces the `html` site. Several generators may splice in at the same point; -they form a chain. A generator whose dependent pipeline never consumes its -`dependsOn` is an error. +## Declaring a dependent -`dependent` also accepts an array. The generator is spliced into every listed -pipeline; when requested, it is delivered through the dependents that are -already part of the run, or through all of them when none is: +`dependsOn` pulls another generator's output in. The inverse, `dependent`, +pushes a generator's output into another generator's pipeline. Use it when a +generator transforms an intermediate representation (adding, filtering, or +rewriting entries) rather than producing an output of its own. The +[`section-pages`](./generators/section-pages.md) generator, which adds a page +for each section of a module, declares two: ```mjs displayName="index.mjs" export default { name: 'section-pages', + dependsOn: '@doc-kit/core/metadata', + + // Deliver this generator's output through `html` and `sitemap` dependent: [ '@doc-kit/generator-react/html', '@doc-kit/generator-react/sitemap', ], + generate, }; ``` -With this, `-t section-pages` builds the site and the sitemap, while -`-t html -t section-pages` builds just the site. +doc-kit walks up each dependent's pipeline, starting at the dependent itself, +and splices the generator in front of the first generator there that reads the +same `dependsOn`. `html` depends on +`jsx-ast`, which depends on `metadata`, so `jsx-ast` is rewired to read from +`section-pages` instead. `sitemap` reads `metadata` directly, so it's rewired +itself. -Use a dependent when a generator transforms an intermediate representation -(adding, filtering, or rewriting entries) rather than producing a new output -format of its own. See the [`section-pages`](./generators/section-pages.md) generator for -a worked example. +![A generator declaring a dependent is spliced into that dependent's pipeline](./diagrams/dependent.svg) + +- Requesting a generator that declares dependents delivers its output + through them: through the ones the run already includes, or through all of + them when it includes none. `-t section-pages` builds the site and the + sitemap, while `-t html -t section-pages` builds just the site. +- Several generators can splice in at the same point; they form a chain. +- A dependent whose pipeline never reads the generator's `dependsOn` is an + error. ## Markdown pipelines A generator processing Markdown declares its [unified](https://unifiedjs.com/) -pipeline as `markdown`, listing plugins as the -[`markdown` option](./configuration.md#markdown-plugins) does, with paths -relative to its module. The rehype list starts with `remark-rehype`, and the -recma list with `rehype-recma`. Each list takes the configured plugins in place -of its `'...'`, and a list without one takes none. As every thread running the -generator imports it, options may be anything, functions included. +pipeline as `markdown`, and gets a processor for it with `getProcessor(name)`: -```mjs displayName="index.mjs" -export default { - name: 'my-generator', +```mjs displayName="entries-html.mjs" +import { join } from 'node:path'; + +import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; +import { writeFile } from '@doc-kit/core/utils/file.mjs'; +import { getProcessor } from '@doc-kit/core/utils/markdown/processor.mjs'; +export default { + name: 'entries-html', + description: 'Renders every API entry to HTML', dependsOn: '@doc-kit/core/metadata', markdown: { - remarkPlugins: ['remark-parse', 'remark-gfm', '...'], + remarkPlugins: ['...'], rehypePlugins: [ ['remark-rehype', { allowDangerousHtml: true }], '...', + '@doc-kit/core/plugins/shiki/rehype.mjs', ['rehype-stringify', { allowDangerousHtml: true }], ], }, - generate, -}; -``` - -`getProcessor(name)` gives the generator's processor, on each thread it runs -on: - -```mjs displayName="generate.mjs" -import { getProcessor } from '@doc-kit/core/utils/markdown/processor.mjs'; + async generate(entries) { + const { output } = getConfig('entries-html'); + const processor = getProcessor('entries-html'); -export async function generate(input) { - const processor = getProcessor('my-generator'); + const html = await Promise.all( + entries.map(async ({ content }) => + processor.stringify(await processor.run(content)) + ) + ); - return Promise.all( - input.map(async ({ content }) => - processor.stringify(await processor.run(content)) - ) - ); -} + await writeFile(join(output, 'entries.html'), html.join('\n')); + }, +}; ``` -`getProcessor(name, { mdx: true })` returns an MDX processor (plugins can -check `this.data('mdx')`), and `getProcessor(name, { configured: false })` one -without the configured plugins. A generator rendering Markdown (with rehype or -recma plugins) only takes its own remark plugins, as the global ones already -ran when `ast` parsed the Markdown. - -A plugin needing asynchronous setup can export an async `load(options)` -returning the plugin, instead of a default export. The Shiki plugin does, and -`getHighlighter(name)` (from `@doc-kit/core/plugins/shiki/highlighter.mjs`) -returns the highlighter of a generator's Shiki plugin, to highlight code as its -pipeline does. It registers Shiki's bundled languages as code in them is -highlighted, so before using its `shiki` instance directly, pass the language -through its `resolveLanguage`. - -## File Output - -### Writing Output Files - -In `generate.mjs`: +Plugins are listed as in the +[`markdown` option](./configuration.md#markdown-plugins), and resolve from the +generator's module: paths relative to it, and packages it can import (this one +needs `remark-rehype` and `rehype-stringify` installed). + +- The lists run in order, on one processor: remark plugins on the Markdown + tree, rehype plugins on HTML, and recma plugins on JavaScript. So a rehype + list starts with `remark-rehype`, and a recma list with `rehype-recma`. +- `'...'` marks where a list takes the configured plugins, from `global` and + from the generator's own section. A list without one takes none, and doc-kit + warns about plugins configured for it. A generator rendering Markdown (one + with rehype or recma plugins) only takes the remark plugins of its own + section, as the global ones already ran when `ast` parsed the files. +- Each thread imports the generator, and its pipeline with it, so the options + in `markdown` can be anything, functions included. `getProcessor()` works + in `generate` and in `processChunk` alike. +- A plugin's module default-exports the plugin, a list of plugins, or a + preset. A plugin that needs asynchronous setup exports an async + `load(options)` returning the plugin instead. + +`getProcessor(name[, options])` takes these options: + +- `mdx` {boolean} Whether the processor parses MDX. Plugins can check it with + `this.data('mdx')`. **Default:** `false`. +- `configured` {boolean} Whether it runs the configured plugins, rather than + only the generator's own. **Default:** `true`. + +### Highlighting code + +The Shiki plugin, `@doc-kit/core/plugins/shiki/rehype.mjs`, highlights code +blocks in every language Shiki bundles; its options are described in +[Code highlighting](./configuration.md#code-highlighting). To highlight other +code the way the pipeline does, `getHighlighter(name)` returns the highlighter +of a generator's Shiki plugin, and throws when its pipeline has none: + +```mjs displayName="entries-html.mjs" +import { getHighlighter } from '@doc-kit/core/plugins/shiki/highlighter.mjs'; + +// In `generate` or `processChunk` +const highlighter = getHighlighter('entries-html'); + +const html = highlighter.highlightToHtml('const answer = 42;', 'js'); +``` -```mjs displayName="generate.mjs" -import { mkdir, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; +- `highlightToHtml` {Function} `highlightToHtml(code, lang[, meta])` returns + the inner HTML of the highlighted `` element. +- `highlightToHast` {Function} `highlightToHast(code, lang[, meta])` returns + the highlighted code as a HAST tree. +- `resolveLanguage` {Function} `resolveLanguage(lang)` returns the language + to highlight `lang` as, `'text'` when Shiki doesn't know it. It registers a + bundled language the first time, so pass a language through it before using + `shiki` directly. +- `shiki` {Object} The Shiki instance. +- `langs` {Array} The languages it highlights. + +## Typing a generator + +`@doc-kit/core` declares the types of a generator as globals, which importing +its `generators/types` module brings in: + +```ts displayName="types.d.ts" +import type {} from '@doc-kit/core/generators/types'; +import type { MetadataEntry } from '@doc-kit/core/generators/metadata/types'; +import type { GlobalConfiguration } from '@doc-kit/core/utils/configuration/types'; + +export type Configuration = { + fileName: string; +}; -import getConfig from '../../utils/configuration/index.mjs'; +// What `getConfig('experimental')` returns +export type Options = GlobalConfiguration & Configuration; + +export type Generator = Omit< + GeneratorMetadata< + // Its options + Configuration, + // What `generate` receives, and what it resolves to + Generate, void>, + // What `processChunk` gets per item, what it resolves to, and its `deps` + ProcessChunk, { marker: string }> + >, + // `GeneratorMetadata` only takes the names of the built-in generators + 'name' +> & { name: string }; +``` -export async function generate(input, worker) { - const config = getConfig('my-format'); +```mjs displayName="experimental.mjs" +import getConfig from '@doc-kit/core/utils/configuration/index.mjs'; - if (!config.output) { - // Return data without writing - return result; - } +/** @type {import('./types').Generator} */ +export default { + name: 'experimental', + description: 'Lists every experimental API', + dependsOn: '@doc-kit/core/metadata', + defaultConfiguration: { fileName: 'experimental.md' }, - // Ensure directory exists - await mkdir(config.output, { recursive: true }); + async generate(entries) { + /** @type {import('./types').Options} */ + // @ts-expect-error `getConfig()` only knows the built-in generators + const { output, fileName } = getConfig('experimental'); - // Write single file - await writeFile(join(config.output, 'output.txt'), content, 'utf-8'); + // ... + }, +}; +``` - // Write multiple files - for (const item of items) { - await writeFile( - join(config.output, `${item.name}.txt`), - item.content, - 'utf-8' - ); +- The type requires `description`, `dependsOn` and `defaultConfiguration`, + even though doc-kit doesn't: give a generator without options + `defaultConfiguration: {}`, and one that reads its own sources + `dependsOn: undefined`. +- `getConfig()` is typed for the built-in names only, so type checking flags a + call with your generator's name. Mark that call with `// @ts-expect-error` + and type its result yourself, as above. +- A generator that streams declares `AsyncGenerator>` as what + `generate` returns. + +## Publishing a generator + +A package exports each of its generators from a subpath: + +```json displayName="package.json" +{ + "name": "doc-kit-rss", + "version": "1.0.0", + "type": "module", + "exports": { + "./rss": "./src/rss/index.mjs" + }, + "peerDependencies": { + "@doc-kit/core": "^1.3.0" } - - return result; } ``` -### Copying Assets +Users install it next to the CLI and pass the subpath as a target, on the +command line or in the `target` of their configuration file: -```mjs displayName="generate.mjs" -import { cp } from 'node:fs/promises'; -import { join } from 'node:path'; +```bash +npm install --save-dev @doc-kit/cli doc-kit-rss +npx doc-kit generate -t doc-kit-rss/rss -i "docs/**/*.md" -o out +``` -import getConfig from '../../utils/configuration/index.mjs'; +- List `@doc-kit/core` as a peer dependency, so the generator shares the + CLI's copy: `getConfig()` reads the configuration the CLI gave that copy. +- A generator it depends on from another package, such as + `@doc-kit/generator-react/jsx-ast`, needs that package installed too. +- Its `name` is the section users configure it in, so pick one that doesn't + collide with the built-in generators. -export async function generate(input, worker) { - const config = getConfig('my-format'); +### Adding a built-in generator - if (config.output) { - // Copy asset directory - await cp( - new URL('./assets', import.meta.url), - join(config.output, 'assets'), - { recursive: true } - ); - } +In this repository, each generator has a directory of its own, with the object +in `index.mjs`, its functions in `generate.mjs` and its types in `types.d.ts`. +A new generator also gets a subpath in its package's `exports`, such as +`"./my-format": "./src/generators/my-format/index.mjs"` in +`packages/core/package.json`, and a shorthand name in +`packages/core/src/generators/index.mjs`, which maps the name users type to +that specifier. The key matches the generator's `name`: - return result; -} +```mjs displayName="packages/core/src/generators/index.mjs" +export const publicGenerators = { + json: '@doc-kit/core/json', + 'my-format': '@doc-kit/core/my-format', + // ... +}; ``` + +`publicGenerators` are the targets `doc-kit generate --help` lists. Stages +that produce no output of their own go in `internalGenerators` instead, which +the help leaves out. A `README.md` next to the generator's `index.mjs`, +documenting its options, becomes its page on this site; add it to the sidebar +in `www/doc-kit.config.mjs`. The +[contributing guide](https://github.com/nodejs/doc-kit/blob/main/CONTRIBUTING.md) +covers the rest, from running the tests to adding a changeset. diff --git a/docs/customization.md b/docs/customization.md index 19dbc901c..8b5b93b7a 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,175 +1,625 @@ # Customizing the site -This page walks the customizations most projects make, in the -order they usually make them. The complete option list lives in the -[`html` generator reference](./generators/html.md). +The site the `html` generator builds starts out neutral: your project's name +in place of a logo, a grey accent color, and a sidebar listing every page. This +guide makes it yours, from colors and a logo to components of your own and whole +new page layouts, starting from the site you built in +[Getting started](./getting-started.md). -All of it goes in the `html` section of your configuration file: +Most of this goes in the `html` section of your configuration file: + +```js displayName="doc-kit.config.mjs" +import { join } from 'node:path'; -```mjs displayName="doc-kit.config.mjs" export default { - target: ['html'], + target: ['html', 'orama-db'], + global: { - input: ['docs/**/*.md'], + project: 'Kittens', + version: '1.0.0', + input: ['docs/**/*.{md,mdx}'], output: 'out', }, html: { - // ... everything on this page + title: '{project} docs', + stylesheets: [join(import.meta.dirname, 'theme/styles.css')], + imports: { + '#theme/Logo': join(import.meta.dirname, 'theme/Logo.jsx'), + }, + }, +}; +``` + +Relative paths in the `html` section resolve against the directory you run +doc-kit from, so the examples build absolute ones from the configuration +file's location with `import.meta.dirname`. The +[Website reference](./generators/html.md) lists every option. + +## Name and title + +`project` names the site: the default logo shows it, and so does the version +selector. Set it under `global`, since other generators use it too, or under +`html` for the site alone. Without one, doc-kit uses the `name` in your +`package.json`. + +`title` is the template for the second half of each page's ``. The first +half is the page's own title, from its `title` frontmatter or its first +heading: + +```js displayName="doc-kit.config.mjs" +export default { + html: { + // "Getting started | Kittens docs" + title: '{project} docs', + }, +}; +``` + +Any option of the `html` section, global ones included, works as a +placeholder, such as `{project}`, or `{version}` for the version number. The +default is `'{project} {version} Documentation'`, so set `global.version` or +drop `{version}`: without one, it shows the version of Node.js running +doc-kit. An empty `title` leaves each page its own title alone. + +## Colors and fonts + +Restyle the site with a stylesheet of your own. `stylesheets` lists CSS files +that are bundled after doc-kit's own, so their rules and custom properties +win, and that go through the bundler like any other CSS (`@import` and `url()` +work). + +The UI takes its accent color, used for links, focus rings and active states, +from nine `--color-brand-*` custom properties. Redefine them all to rebrand the +site: + +```css displayName="theme/styles.css" +:root { + --color-brand-100: #f3f0ff; + --color-brand-200: #e4dcff; + --color-brand-300: #cbbcff; + --color-brand-400: #ac94fa; + --color-brand-500: #8e6cf0; + --color-brand-600: #7350d9; + --color-brand-700: #5c3db8; + --color-brand-800: #482f91; + --color-brand-900: #33216a; +} +``` + +```js displayName="doc-kit.config.mjs" +import { join } from 'node:path'; + +export default { + html: { + stylesheets: [join(import.meta.dirname, 'theme/styles.css')], + }, +}; +``` + +One palette serves both color schemes: the UI picks lighter shades in dark +mode. For styles of your own that differ in dark mode, use the +`[data-theme='dark']` selector, which matches once the `<html>` element is in +dark mode. + +The text uses the font in `--font-open-sans`, and code the one in +`--font-ibm-plex-mono`. They are named after doc-kit's default fonts, but hold +whatever font stack you give them: + +```css displayName="theme/styles.css" +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('./fonts/inter-latin-wght-normal.woff2') format('woff2'); +} + +:root { + --font-open-sans: 'Inter Variable', system-ui, sans-serif; +} +``` + +Prefer a stylesheet to a `<style>` tag in [the `<head>`](#the-head): it is +bundled, minified and cached with the rest of the site's CSS. + +> [!TIP] +> The default template preloads doc-kit's own fonts through `${preloads}`. If +> you replace them, [a template of your own](#the-page-template) can drop or +> filter `${preloads}`; this site's template keeps only the monospace one. + +## Logo + +The logo sits at the start of the navigation bar, inside a link to the home +page. By default it is the project's name as text. To replace it, point the +`#theme/Logo` alias at a component of your own: + +```js displayName="doc-kit.config.mjs" +import { join } from 'node:path'; + +export default { + html: { + imports: { + '#theme/Logo': join(import.meta.dirname, 'theme/Logo.jsx'), + }, }, }; ``` -## Name and titles +```jsx displayName="theme/Logo.jsx" +import { project } from '#theme/config'; -```js -html: { - project: 'My Project', // Used in page titles and the version selector - title: '{project} Documentation', // Browser-tab title template - // Suppress the Node.js announcement banner fetched at runtime - remoteConfigUrl: '', -}, +export default () => ( + <span class="logo"> + <svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"> + <circle cx="12" cy="12" r="12" fill="var(--color-brand-600)" /> + </svg> + {project} + </span> +); ``` -`title` supports `{project}` and `{version}` placeholders. +The logo is one of the [parts of the page](#replacing-parts-of-the-page) you +can replace, and `#theme/config` holds the +[build-time data](#the-themes-data) components can read. + +> [!NOTE] +> Theme components are [Preact](https://preactjs.com/) components, so +> `preact` has to resolve from your project, for their JSX and any hooks they +> import. npm usually hoists it from `@doc-kit/generator-react`; with pnpm, +> install it yourself. ## The `<head>` -`head` controls the markup injected into every page's `<head>`: - -```js -html: { - head: { - meta: [ - { name: 'description', content: 'My project documentation' }, - { property: 'og:image', content: 'https://example.com/og.png' }, - ], - links: [{ rel: 'icon', href: '/favicon.ico' }], - html: [ - `<style> - :root, :root.dark { - --color-brand-400: #b8adff; - --color-brand-600: #6e5cd9; - } - </style>`, - ], +`head` adds tags to every page's `<head>`: + +```js displayName="doc-kit.config.mjs" +export default { + html: { + head: { + meta: [ + { name: 'description', content: 'Kittens: an API for cats.' }, + { property: 'og:image', content: 'https://kittens.example/og.png' }, + ], + links: [{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }], + html: ['<script defer src="https://stats.example/script.js"></script>'], + }, + + // Copies what is in `public/` to the root of the output + pathsToCopy: [{ public: '.' }], }, -}, +}; ``` -The `<style>` override above re-brands the accent color: the UI derives its -palette from `--color-brand-*` custom properties, so a few lines of CSS -restyle the whole site. +`meta` and `links` take one object of attributes per tag, and `html` takes raw +markup, added as written. The tags are the same on every page, so link files +by absolute path or full URL. + +Files you link to have to be in the output. `pathsToCopy` copies them there: +by default the `assets`, `public` and `static` directories of the working +directory, under the same names. An object maps a source to a destination +inside the output instead, as `{ public: '.' }` does above to put +`favicon.svg` at the root. Setting `pathsToCopy` replaces the default list. + +## The page template + +Every page is written from an HTML template. It places the `head` tags and also +writes the `<title>`, the `og:title` and `og:type` tags, and the tags loading +the site's scripts and styles. For anything else, copy the default template +(`src/html/template.html` in `@doc-kit/generator-react`) and point +`templatePath` at your copy. Keep `${assets}` in its `<head>`, or the pages +load no scripts or styles. The +[Website reference](./generators/html.md#html-template) lists the template's +variables. ## Navigation -Without configuration, the sidebar is one group holding every page. The -`navigation` key allows you to customize it: +Without configuration, the sidebar has one group, `API Documentation`, listing +every page by its title (alphabetically, with `index` first), and the +navigation bar has no links. `navigation` replaces either: -```js -html: { - navigation: { - sidebar: [ - { - groupName: 'Guides', - items: [{ label: 'Getting started', link: '/getting-started' }], - }, - { - groupName: 'Reference', - items: [{ label: '`api`', link: '/api' }], - }, - ], - navbar: [ - { - text: 'GitHub', - link: 'https://github.com/nodejs/doc-kit', - target: '_blank', - }, - ], +```js displayName="doc-kit.config.mjs" +export default { + html: { + navigation: { + sidebar: [ + { + groupName: 'Guides', + items: [ + { label: 'Getting started', link: '/getting-started' }, + { + label: 'Recipes', + link: '/recipes', + items: [ + { label: 'Overview', link: '/recipes' }, + { label: 'Feeding', link: '/recipes/feeding' }, + ], + }, + ], + }, + { + groupName: 'Reference', + items: [{ label: 'The `Kitten` class', link: '/kitten' }], + }, + ], + navbar: [ + { text: 'Guide', link: '/getting-started' }, + { + text: 'GitHub', + link: 'https://github.com/example/kittens', + target: '_blank', + }, + ], + showCrossLinks: true, + }, }, -}, +}; ``` -Sidebar `link`s are page paths without extensions; backticked spans in a -`label` render as code. Items can nest through their own `items` array. The -sidebar also shows a version selector when your configuration provides a -[`changelog`](./configuration.md). +A sidebar `link` is a page's path without its extension, such as +`/getting-started`, and works wherever the site is served from. An item with +nested `items` opens and closes rather than linking anywhere, so list its own +page as its first nested item, as `Recipes` does. `showCrossLinks` ends each +page with links to the previous and next top-level items of your `sidebar`. +The [Website reference](./generators/html.md#navigation) lists every field. + +The navigation bar also holds the search box, the theme switch, and a link to +the GitHub repository when `global.repository` is set. The search box appears +when you build the [search index](./generators/orama-db.md) too; set +`showSearchBox` to `true` or `false` to decide yourself. The sidebar adds a +version selector when your configuration has a `changelog`, as +[Publishing several versions](./publishing.md#publishing-several-versions) +describes. + +## Replacing parts of the page -## Logo and theme components +Every part of the page around your content is a component, imported through a +`#theme/*` alias: -Every piece of the page surrounding your content is a component you can replace. Override any -`#theme/*` alias with a path to your own: +![The parts of the html generator's page, each an import alias you can swap](./diagrams/theme-slots.svg) -```js -html: { - imports: { - '#theme/Logo': './src/MyLogo.jsx', - '#theme/Footer': './src/MyFooter.jsx', +| Alias | What it renders | Default | +| ------------------- | ----------------------------------------------- | ---------------------------------------------------- | +| `#theme/Layout` | The whole page, around the content | The parts below, around the content | +| `#theme/Logo` | The logo, inside the navigation bar's home link | The project's name | +| `#theme/Navigation` | The navigation bar | Links, search box, theme switch, GitHub link | +| `#theme/Sidebar` | The sidebar | Page links and the version selector | +| `#theme/Metabar` | The column beside the content | Table of contents, reading time, view and edit links | +| `#theme/Footer` | The bottom of every page | Nothing | + +Point an alias at your own component in `imports`, and every page uses it. The +aliases you leave out keep their defaults: + +```js displayName="doc-kit.config.mjs" +import { join } from 'node:path'; + +export default { + html: { + imports: { + '#theme/Footer': join(import.meta.dirname, 'theme/Footer.jsx'), + }, }, -}, +}; ``` -```jsx displayName="src/MyLogo.jsx" +```jsx displayName="theme/Footer.jsx" +import { project, repository } from '#theme/config'; + export default () => ( - <svg height="30" width="30" viewBox="0 0 10 10"> - <circle cx="5" cy="5" r="5" fill="var(--color-brand-400)" /> - </svg> + <footer class="footer"> + {project} is released under the{' '} + <a href={`https://github.com/${repository}/blob/main/LICENSE`}> + MIT License + </a> + . + </footer> ); ``` -Available slots: `#theme/Logo`, `#theme/Navigation`, `#theme/Sidebar`, -`#theme/Metabar`, `#theme/Footer` (renders nothing by default), and -`#theme/Layout` (the outermost wrapper). Custom components can import -whatever build-time data they need — project name, version list, page index -— from the `#theme/config` virtual module; see the -[`html` reference](./generators/html.md) for its exports. +Your component receives the props of the one it replaces: the navigation bar, +the sidebar and the metabar get the page's `metadata`, and the metabar also +gets `headings` and `readingTime`. The logo and the footer get none. -## Custom components and MDX +To build on a default rather than start over, import it from +`@doc-kit/generator-react/html/ui/components/`: `NavBar.jsx`, +`SideBar/index.jsx`, `MetaBar/index.jsx` or `Layout/index.jsx`. Import the +other parts through their aliases, so your overrides still apply. -Register your own JSX components and use them directly in content: +### The theme's data -```js -html: { - components: { - Hero: '#theme/Hero', +Components read build-time data from the `#theme/config` module. Import only +what you need: + +```jsx displayName="theme/Footer.jsx" +import { project, version } from '#theme/config'; + +export default () => ( + <footer class="footer"> + {project} v{version.version} + </footer> +); +``` + +`version` is the version being built, and `version.version` its number. The +module also exports `repository`, `baseURL`, the list of `pages`, and most +other `html` options; the +[Website reference](./generators/html.md#theme-config-module) lists them all. + +For data of your own, add modules with `virtualImports`. Each key is an import +specifier, and each value the module's source code: + +```js displayName="doc-kit.config.mjs" +export default { + html: { + virtualImports: { + '#theme/links': 'export const chat = "https://chat.example/kittens";', + }, }, - imports: { - '#theme/Hero': './src/components/Hero.jsx', +}; +``` + +```jsx displayName="theme/Footer.jsx" +import { chat } from '#theme/links'; + +export default () => ( + <footer class="footer"> + Questions? <a href={chat}>Ask in the chat</a>. + </footer> +); +``` + +## Custom components and MDX + +Markdown pages have no components: their tags are plain HTML. To use +components in a page, write it in [MDX](./writing-docs.md#mdx-pages), register +your components in `components`, and use them as tags: + +```js displayName="doc-kit.config.mjs" +import { join } from 'node:path'; + +export default { + html: { + components: { + Hero: '#theme/Hero', + }, + imports: { + '#theme/Hero': join(import.meta.dirname, 'theme/Hero.jsx'), + }, }, -}, +}; +``` + +```jsx displayName="theme/Hero.jsx" +export default ({ title, children }) => ( + <section class="hero"> + <p class="hero__title">{title}</p> + {children} + </section> +); ``` -```md displayName="welcome.mdx" +```mdx displayName="docs/index.mdx" --- -title: Welcome +title: Kittens --- -# Welcome +# Kittens -<Hero title="My Project" /> +<Hero title="An API for cats"> + Feed, pet and schedule naps, from one small library. +</Hero> ``` -Author component-bearing pages as `.mdx` files (or set `mdx: true` in a -page's frontmatter). Regular `.md` files treat `<` and `{` literally, which -is what API documentation wants; MDX pages get real JSX and skip API-doc -signature parsing while keeping headings, TOC, and sidebar behavior. +Each key in `components` is a tag, and its value the module whose default +export is the component, usually a `#theme/*` alias you define in `imports`. +For a named export, see +[Components in MDX](./generators/html.md#components-in-mdx). Each page imports +only the components it uses, and some come built in and need no registering, +such as `<DocumentationIndex />`. + +## Interactive components + +Components render to HTML when the site is built, and load no JavaScript. A +component that has to run in the browser, with state, effects or event +handlers, has to be an island: doc-kit renders it at build time, then loads it +in the browser and hydrates it once a condition you choose is met. + +Wrap the component with `withIsland`, giving it a name and its conditions: + +```jsx displayName="theme/Counter.jsx" +import withIsland from '@doc-kit/generator-react/html/ui/islands/withIsland.jsx'; +import { useState } from 'preact/hooks'; + +const Counter = ({ start = 0 }) => { + const [count, setCount] = useState(start); + + return ( + <button type="button" onClick={() => setCount(count + 1)}> + Petted {count} times + </button> + ); +}; + +export default withIsland(Counter, { name: 'Counter', on: { idle: true } }); +``` + +Then list it in `components` under that same name, so the browser knows which +module to load for it. Do this even for an island that only other components +render, never a page: + +```js displayName="doc-kit.config.mjs" +import { join } from 'node:path'; + +export default { + html: { + components: { + Counter: '#theme/Counter', + }, + imports: { + '#theme/Counter': join(import.meta.dirname, 'theme/Counter.jsx'), + }, + }, +}; +``` + +```mdx displayName="docs/kitten.mdx" +<Counter start={3} /> +``` + +An island missing from `components` stays static, and the browser console +reports `[is-land] no component registered for "Counter"`. + +`on` takes [is-land](https://is-land.11ty.dev/)'s loading conditions, without +their `on:` prefix, such as `{ idle: true }` or `{ visible: true }`. `idle` +suits most islands, and it's what this site's own islands use. The +[Website reference](./generators/html.md#interactive-components) lists every +condition. + +With `interaction: true`, the click that hydrates an island reaches it before +its component has loaded, so the component never handles that click. An island +that has to handle its first click hydrates on +`interaction: 'pointerover,focusin,touchstart'` instead, as doc-kit's code +blocks do: it starts loading as the pointer or focus arrives, and a click that +lands before it has loaded is replayed once it has. + +Keep in mind that: + +- Props are written into the page as JSON, so pass data only: strings, + numbers, booleans, arrays and plain objects, not functions. +- Children are rendered at build time and kept as they are: the island + receives them as static HTML, which doesn't become interactive. +- The island's first render in the browser has to match the HTML built for it. + Read browser-only state, such as `localStorage`, in an effect. +- In browsers with the Navigation API, the site moves between its pages + without reloading, and islands are unmounted and hydrated again on every + page. Keep state that should outlast a page in module scope. + +## Page layouts + +`#theme/Layout` renders every page, so a layout of your own can give some +pages a different shape. This site's home page, for example, sets +`layout: home` in its frontmatter, and the site's layout renders it full width, +between the navigation bar and the footer, while every other page gets +doc-kit's documentation layout: + +```jsx displayName="theme/Layout.jsx" +import DocLayout from '@doc-kit/generator-react/html/ui/components/Layout/index.jsx'; + +import Footer from '#theme/Footer'; +import NavBar from '#theme/Navigation'; + +const HomeLayout = ({ metadata, children }) => ( + <> + <NavBar metadata={metadata} /> + <div class="home" role="main"> + {children} + </div> + <Footer /> + </> +); + +const LAYOUTS = { home: HomeLayout }; + +export default props => { + const Layout = LAYOUTS[props.metadata.layout] ?? DocLayout; + + return <Layout {...props} />; +}; +``` + +```mdx displayName="docs/index.mdx" +--- +title: Kittens +layout: home +--- + +<Hero title="An API for cats" /> +``` + +Point `#theme/Layout` at it in `imports`. A layout receives the page's +`metadata`, every field of its frontmatter included, its `headings`, its +`readingTime` and its content as `children`; the +[Website reference](./generators/html.md#layout-props) describes each. + +doc-kit also builds a `404.html` page, whose `metadata.api` is `'404'`: check +for it to give that page a layout of its own. + +> [!TIP] +> doc-kit's stylesheet styles the headings inside `<main>` as API entries. A +> layout for other kinds of pages can use another element with +> `role="main"`, as above. + +## Markdown plugins + +The pages' Markdown is rendered by the `jsx-ast` generator, which takes your +own [unified](https://unifiedjs.com/) plugins: remark plugins for the Markdown, +rehype plugins for the HTML, and recma plugins for the JavaScript it compiles +to. This site uses a rehype plugin to inline its diagrams as SVG, so they +follow the color scheme. See +[Markdown plugins](./configuration.md#markdown-plugins). ## The bundler -Vite builds the site by default, and accepts your plugins and options: +[Vite](https://vite.dev/) builds the site by default. To pass it plugins or +options, create its adapter yourself, with the Vite configuration to merge in: -```mjs displayName="doc-kit.config.mjs" +```js displayName="doc-kit.config.mjs" import { createViteBundler } from '@doc-kit/generator-react/html/bundlers/vite'; +import myVitePlugin from './my-vite-plugin.mjs'; + export default { + // A bundler adapter needs a single thread, see below + threads: 1, + html: { bundler: createViteBundler({ plugins: [myVitePlugin()], + define: { + 'import.meta.env.STATS_ID': JSON.stringify('kittens'), + }, }), }, }; ``` -A different bundler entirely (webpack, Rspack, …) can be supplied as a small -adapter object — see [Bundler adapters](./generators/html.md) in the -reference. +> [!WARNING] +> Worker threads can't receive functions, so a configuration that sets +> `bundler`, the Vite adapter included, has to build with `threads: 1` (or +> `--threads 1`). With more threads, the build fails with a `DataCloneError`. + +Your plugins run after doc-kit's own and see every module of both builds, but +not the HTML pages: change those through the [template](#the-page-template). +The scripts and styles are minified unless you set `build.minify: false`; the +`minify` option only covers the HTML. A few options, such as the base URL and +the JSX runtime, are doc-kit's to set; the +[Vite adapter](./generators/html.md#vite-adapter) reference lists them. + +Another bundler, such as webpack or Rspack, plugs in through a small adapter +of your own, which needs `threads: 1` as well. The +[Website reference](./generators/html.md#bundler-adapters) describes how to +write one. + +## A complete example + +This site is built with doc-kit, from the +[`www/`](https://github.com/nodejs/doc-kit/tree/main/www) directory of its +repository, and its +[configuration file](https://github.com/nodejs/doc-kit/blob/main/www/doc-kit.config.mjs) +uses most of this page: + +- A [layout](https://github.com/nodejs/doc-kit/blob/main/www/theme/Layout.jsx) + with a `home` layout for the + [home page](https://github.com/nodejs/doc-kit/blob/main/www/pages/index.mdx), + a 404 page, and doc-kit's layout for everything else. +- A logo and a footer of its own. +- Islands for the home page's two animations, and the sections the page is + laid out with, all listed in `components`. +- A stylesheet that swaps the typeface for Inter and keeps doc-kit's neutral + palette, and a template that preloads Inter instead of Open Sans. +- A `#theme/site` virtual module with doc-kit's version, for the footer. +- A rehype plugin that inlines the diagrams, and `pathsToCopy` for its public + files, fonts and JSON schemas. + +When your site looks right, [Publishing your docs](./publishing.md) covers +putting it online. diff --git a/docs/diagrams/build-output.svg b/docs/diagrams/build-output.svg new file mode 100644 index 000000000..4752d3b7a --- /dev/null +++ b/docs/diagrams/build-output.svg @@ -0,0 +1,28 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 440 360" width="528" height="432" fill="none" class="dk-illus dk-illus-build-output"> + <title>doc-kit generate writes kittens.html, all.html, 404.html, assets/ and the search index to out/, which any static host serves as is + + + + + + + orama-db.json + assets/ + all.html + kittens.html + 404.html + + out/ + + + + + + + ~/kittens + + $ npx doc-kit generate + $ + out/what generate writes + Any static hostserves out/ as is + diff --git a/docs/diagrams/dependent.svg b/docs/diagrams/dependent.svg new file mode 100644 index 000000000..a3186d941 --- /dev/null +++ b/docs/diagrams/dependent.svg @@ -0,0 +1,35 @@ + + A generator declaring a dependent is spliced into that dependent's pipeline + + + + section-pages/index.mjs + + name: 'section-pages', + dependsOn: '@doc-kit/core/metadata', + dependent: [ +   '@doc-kit/generator-react/html', +   '@doc-kit/generator-react/sitemap', + ], + + + + + + + + + + + + metadata + section-pages + jsx-ast + html + llms-txt + sitemap + + section-pages + + + diff --git a/docs/diagrams/entry-anatomy.svg b/docs/diagrams/entry-anatomy.svg new file mode 100644 index 000000000..a71b96791 --- /dev/null +++ b/docs/diagrams/entry-anatomy.svg @@ -0,0 +1,80 @@ + + The parts of an API entry in doc-kit Markdown, and what each becomes + + + docs/kittens.md + + + + + + + ## `kittens.adopt(name[, options])` + <!-- YAML + added: v1.0.0 + --> + > Stability: 1 - Experimental + - `name` {string} + - `options` {Object} +   - `color` {string} +     **Default:** `'tabby'`. + - Returns: {Promise} + Adopts a kitten. + ```js + await kittens.adopt('Mittens'); + ``` + 1 Heading + 2 History + 3 Stability + 4 Typed list + 5 Prose + 6 Code sample + + + + + + + + + Entry + kittens.html + + + + + + + + + + + + M + kittens.adopt + History + kittens.adopt(name, options?): Promise + Stability: 1 + Experimental + name: + string + + options: + Object + + color?: + string + + Default: 'tabby' + Returns: + Promise + + Adopts a kitten. + await kittens.adopt('Mittens'); + 1 + 2 + 3 + 4 + 5 + 6 + diff --git a/docs/diagrams/generator-graph.svg b/docs/diagrams/generator-graph.svg new file mode 100644 index 000000000..6326deeb3 --- /dev/null +++ b/docs/diagrams/generator-graph.svg @@ -0,0 +1,32 @@ + + Generators form a graph through dependsOn; picking html and orama-db runs only them and what they depend on, parsing your sources once + + + + + + + + + + + + ast + + jsx-ast + + json + orama-db + llms-txt + html + json-all + man-page + Your sourcesone parse for both + Every targetonly what you pick runs + -t orama-db + + + -t html + + + diff --git a/docs/diagrams/theme-slots.svg b/docs/diagrams/theme-slots.svg new file mode 100644 index 000000000..fa5f066fe --- /dev/null +++ b/docs/diagrams/theme-slots.svg @@ -0,0 +1,30 @@ + + The parts of the html generator's page, each an import alias you can swap + + + + + + + + + + + + + + + + + + #theme/*an alias for each part + Logo + Sidebar + Navigation + Metabar + Footer + renders nothing + Footer.jsx + Layoutthe whole page + Your pageMarkdown or MDX + diff --git a/docs/generators.md b/docs/generators.md index ab7270bb7..19d8a4af8 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -1,73 +1,136 @@ # Generators -A generator is an output format. +A generator makes one output from your documentation: the website, the search +index, JSON, a man page. You pick generators by their target name with +`--target` (`-t`) on the [CLI](./cli.md), or with `target` in the +[configuration file](./configuration.md). However many you pick, doc-kit parses +your sources once and every generator reads that result. ```bash -npx @doc-kit/cli generate -t html -t orama-db -t sitemap -i "docs/**/*.md" -o out +npx doc-kit generate -t html -t orama-db -i "docs/**/*.md" -o out ``` -## Built-in generators +![Generators form a graph through dependsOn; picking html and orama-db runs only them and what they depend on, parsing your sources once](./diagrams/generator-graph.svg) -### Web ([`@doc-kit/generator-react`](./packages/react.md)) +Each generator has a reference page laid out the same way: what it makes, how +to run it, what it reads and writes, its options, then the details. -| Target | Output | -| ------------------------------------------------ | -------------------------------------------------------------------- | -| [`html`](./generators/html.md) | The modern documentation site: server-rendered, hydrated, themeable. | -| [`orama-db`](./generators/orama-db.md) | The search index behind the `html` site's search box. | -| [`llms-txt`](./generators/llms-txt.md) | An [`llms.txt`](https://llmstxt.org/) index for language models. | -| [`llms-txt-full`](./generators/llms-txt-full.md) | Every page's Markdown in one `llms-full.txt`, for language models. | -| [`sitemap`](./generators/sitemap.md) | A `sitemap.xml` for search engines. | -| [`section-pages`](./generators/section-pages.md) | The `html` site and sitemap, plus one page per section of a module. | +## The website -### JSON ([`@doc-kit/core`](./packages/core.md)) +From [`@doc-kit/generator-react`](./packages/react.md), which you install next +to the CLI. -| Target | Output | -| -------------------------------------------- | ------------------------------------------------------------------ | -| [`json`](./generators/json.md) | One schema-described JSON document per source file. | -| [`json-all`](./generators/json-all.md) | Those documents bundled into a single `all.json`. | -| [`json-simple`](./generators/json-simple.md) | A dump of the parsed metadata entries, for debugging the pipeline. | +| Generator | Target | Output | +| ---------------------------------------------- | --------------- | --------------------------------------------------------------------------------------- | +| [Website](./generators/html.md) | `html` | The site: server-rendered pages with interactive islands, bundled with Vite by default. | +| [Section pages](./generators/section-pages.md) | `section-pages` | An extra page for each section of every module, added to the website and the sitemap. | +| [Search index](./generators/orama-db.md) | `orama-db` | `orama-db.json`, the Orama index that the website's search box loads. | +| [Sitemap](./generators/sitemap.md) | `sitemap` | `sitemap.xml`, listing the pages for search engines. | -### Legacy ([`@node-core/doc-kit-legacy`](./packages/node-legacy.md)) +- The search box shows only when the search index is built in the same run. + Set [`showSearchBox`](./generators/html.md#configuring) in the `html` section + to choose yourself. +- `section-pages` adds its pages to whichever of `html` and `sitemap` you also + ask for. Ask for it on its own and you get both. +- `web`, the former name of `html`, still works as a target. -1:1 matches for Node.js's original documentation tooling, for consumers of -the classic layouts. +## For language models -| Target | Output | -| ---------------------------------------------------- | ----------------------------------- | -| [`legacy-html`](./generators/legacy-html.md) | One classic HTML page per document. | -| [`legacy-html-all`](./generators/legacy-html-all.md) | The single-page `all.html` bundle. | -| [`legacy-json`](./generators/legacy-json.md) | The classic per-document JSON. | -| [`legacy-json-all`](./generators/legacy-json-all.md) | The single-file JSON bundle. | +From [`@doc-kit/generator-react`](./packages/react.md). -### Node.js-specific ([`@node-core/doc-kit`](./packages/node.md)) +| Generator | Target | Output | +| ---------------------------------------------- | --------------- | ----------------------------------------------------------------------- | +| [llms.txt](./generators/llms-txt.md) | `llms-txt` | An [`llms.txt`](https://llmstxt.org/) index linking to each page. | +| [llms-full.txt](./generators/llms-txt-full.md) | `llms-txt-full` | `llms-full.txt`: every page's Markdown in one file, each after its URL. | -Outputs consumed by the Node.js project's own build and release processes. +## Data -| Target | Output | -| ---------------------------------------------- | --------------------------------------------------- | -| [`man-page`](./generators/man-page.md) | The `node.1` manual page. | -| [`api-links`](./generators/api-links.md) | A map of API symbols to their source locations. | -| [`addon-verify`](./generators/addon-verify.md) | Extracted addon code samples, arranged for testing. | +From [`@doc-kit/core`](./packages/core.md), which comes with the CLI. + +| Generator | Target | Output | +| ----------------------------------------- | ------------- | ---------------------------------------------------------------------- | +| [JSON](./generators/json.md) | `json` | One JSON document per source file, described by a published schema. | +| [JSON bundle](./generators/json-all.md) | `json-all` | Every `json` document bundled into one `all.json`. | +| [Debug JSON](./generators/json-simple.md) | `json-simple` | `api-docs.json`, a dump of the API entries for debugging the pipeline. | + +## For Node.js + +From [`@node-core/doc-kit`](./packages/node.md), which you install next to the +CLI. These serve the Node.js project's own build, release and tests, and their +defaults match its layout, so they're mostly useful to Node.js and projects +shaped like it. + +| Generator | Target | Output | +| ---------------------------------------------- | -------------- | ------------------------------------------------------------------------------------ | +| [Man page](./generators/man-page.md) | `man-page` | A man page in mdoc format (`node.1` by default), from a command-line reference page. | +| [API links](./generators/api-links.md) | `api-links` | `apilinks.json`, mapping each exported API to the line defining it in the sources. | +| [Addon examples](./generators/addon-verify.md) | `addon-verify` | The C++ addon examples' code blocks, written out with a `binding.gyp` for the tests. | + +## Node.js's legacy formats + +From [`@node-core/doc-kit-legacy`](./packages/node-legacy.md), which you +install next to the CLI. These write the HTML pages and JSON files of the +Node.js API docs in the formats Node.js's original tooling made, for Node.js +and the tools that read them. For your own docs, use the website and JSON +generators above. + +| Generator | Target | Output | +| ---------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------ | +| [Legacy HTML](./generators/legacy-html.md) | `legacy-html` | A classic Node.js HTML page per source file, with its `assets/`. | +| [Legacy single-page HTML](./generators/legacy-html-all.md) | `legacy-html-all` | `all.html`, every legacy HTML page one after another. | +| [Legacy JSON](./generators/legacy-json.md) | `legacy-json` | A classic Node.js JSON file per source file, with descriptions in HTML. | +| [Legacy JSON bundle](./generators/legacy-json-all.md) | `legacy-json-all` | `all.json`, the sections of every legacy JSON file merged into one file. | ## Pipeline stages -The targets above are built on a few shared stages, each of which is itself a -generator. +The generators above read from a few shared stages, which are generators too. A +stage runs when a target needs it, once per run however many targets read it, +and writes no files. You don't ask for stages yourself, but you configure them +like any other generator: `--type-map`, for example, sets `metadata.typeMap`. + +| Stage | Name | Produces | +| -------------------------------------------- | ---------- | ----------------------------------------------------------------------------------- | +| [Parsing Markdown](./generators/ast.md) | `ast` | The Markdown and MDX files in `input`, parsed into syntax trees in worker threads. | +| [Parsing JavaScript](./generators/ast-js.md) | `ast-js` | The `.js` files in `input`, parsed with Acorn for `api-links`. | +| [API entries](./generators/metadata.md) | `metadata` | The flat list of API entries, one per heading with its kind, stability and history. | +| [Page JSX](./generators/jsx-ast.md) | `jsx-ast` | Each page as a JSX program, which `html` renders. | + +`jsx-ast` comes from `@doc-kit/generator-react`; the other stages come from +`@doc-kit/core`. + +## Options + +Each generator reads its options from the configuration section named after it, +and takes any option it doesn't set there from `global`. Each generator's page +lists its options, and [Configuration](./configuration.md) covers the file +itself. + +```js displayName="doc-kit.config.mjs" +export default { + target: ['html', 'orama-db', 'sitemap'], + global: { + input: ['docs/**/*.md'], + output: 'out', + baseURL: 'https://example.com', + }, + html: { + title: '{project} documentation', + }, +}; +``` -| Stage | Produces | -| -------------------------------------- | ----------------------------------------------------- | -| [`ast`](./generators/ast.md) | Markdown parsed into MDAST, across worker threads. | -| [`ast-js`](./generators/ast-js.md) | JavaScript sources parsed into an AST. | -| [`metadata`](./generators/metadata.md) | The flattened API entries every output is built from. | -| [`jsx-ast`](./generators/jsx-ast.md) | Those entries as JSX, ready for the web generators. | +With this file in place, `npx doc-kit generate` needs no flags. Targets you pass +with `-t` replace the file's `target` list rather than adding to it. -## Custom generators +## Your own -A target can also be an import specifier — a package export or a local file -whose default export is a generator: +A target can also be an import specifier: a package export, or the path to a +local file (starting with `.`, or absolute) whose default export is a generator. +It runs alongside the built-in ones, and reads its options from the section +named after its `name`. ```bash -npx @doc-kit/cli generate -t @my-scope/my-generator -t ./generators/rss.mjs ... +npx doc-kit generate -t html -t ./generators/rss.mjs -i "docs/**/*.md" -o out ``` -To build one, see [Creating generators](./creating-generators.md). +To write one, see [Creating generators](./creating-generators.md). diff --git a/docs/getting-started.md b/docs/getting-started.md index a4c8f5757..13dd6f8b3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,114 +1,165 @@ # Getting started -First, install doc-kit and a generator, like so: +This guide takes you from one Markdown file to a documentation site with +search, in a few minutes. Run the commands from your project's root, next to +its `package.json`. + +![doc-kit generate writes kittens.html, all.html, 404.html, assets/ and the search index to out/, which any static host serves as is](./diagrams/build-output.svg) + +## Install + +Install the CLI, and the generators that build the site: ```bash npm install --save-dev @doc-kit/cli @doc-kit/generator-react ``` -Then, create your configuration file set up for your project: +`@doc-kit/cli` gives you the `doc-kit` command and the core generators, such +as `json`. `@doc-kit/generator-react` adds the website and what comes with it: +the search index, `llms.txt` and a sitemap. + +## Write a page + +Create `docs/kittens.md`: + +```markdown displayName="docs/kittens.md" +# Kittens + +The `kittens` module adopts, feeds, and pets kittens. + +## `kittens.adopt(name[, options])` + + + +- `name` {string} The kitten's name. +- `options` {Object} + - `color` {string} Coat color. **Default:** `'tabby'`. +- Returns: {Promise} Fulfills with the adopted kitten. + +Adopts a kitten, once the paperwork is done. +``` + +It's regular Markdown, and reads as such on GitHub. doc-kit also reads the API +in it: the heading is a method with its signature, the list holds its typed +parameters, and the comment is its history. +[Writing documentation](./writing-docs.md) covers the rest. + +> [!TIP] +> Documenting a TypeScript or JavaScript project? +> [`@doc-kit/typedoc`](./packages/typedoc.md) writes these pages from your +> source code and its JSDoc comments; see +> [Documenting TypeScript](./typescript.md). + +## Configure + +Create `doc-kit.config.mjs` next to your `package.json`: -```mjs displayName="doc-kit.config.mjs" +```js displayName="doc-kit.config.mjs" /** @type {import('@doc-kit/core/utils/configuration/types').Configuration} */ export default { - target: ['html'], + target: ['html', 'orama-db'], global: { + project: 'Kittens', + version: '1.0.0', input: ['docs/**/*.md'], output: 'out', }, }; ``` -`doc-kit generate` reads that configuration; every option can also be passed as -a CLI flag. See the [configuration reference](./configuration.md) for -everything the file accepts. +- `target` lists the generators to run: `html` builds the site, and + `orama-db` its search index. +- `input` takes glob patterns, and `output` is the directory to write to. +- `project` and `version` go into page titles. Without them, doc-kit uses the + `name` in your `package.json` and the version of Node.js running it. -## Build and preview it +`target`, `input`, `output` and `version` also have [CLI](./cli.md) flags, +which win over the file. The [configuration reference](./configuration.md) +lists every other option. -With at least one Markdown file under `docs/`, build the site: +## Build and preview ```bash -npx @doc-kit/cli generate +npx doc-kit generate ``` -The pages land in `out/`. They use import maps and client-side hydration, so -serve them over HTTP rather than opening the files from disk; any static server -works: +The site is written to `out/`: one page per Markdown file, plus `all.html` +with every page on one. Its pages load their scripts as modules and hydrate in +the browser, so serve them over HTTP rather than opening the files from disk: ```bash -npx @doc-kit/cli generate -t html -t orama-db -i "docs/*.md" -o out +npx doc-kit preview ``` -## Preview it locally +Then open . `preview` serves what +`generate` built; pass it any `generate` option, such as +`--config-file doc-kit.config.mjs`, and it builds first. Any other static +server works too, such as `npx serve out`. -The `html` output uses import maps and client-side hydration, so it must be -served over HTTP — opening the files directly with `file://` will not work. Any -static server will do the trick; for example: +> [!TIP] +> Add a `docs/index.md` and it becomes the site's home page, `index.html`. -```bash -npx serve out -p 3000 -``` - -Then open the printed URL (usually ). The -`legacy-html-all` output from earlier has no such requirement — `out/all.html` -opens straight from disk. - -## Customize the `html` generator output +## Make it yours -The power of the `html` generator comes from its customization hooks. Let's walk -through a couple quick changes. +To give the site your own logo and colors, add an `html` section to the +configuration that points at a logo component and a stylesheet: -Create a `doc-kit.config.mjs` file at the root of the project. - -```mjs displayName="doc-kit.config.mjs" +```js displayName="doc-kit.config.mjs" import { join } from 'node:path'; -/** @type {import('@doc-kit/core/src/utils/configuration/types').Configuration} */ +/** @type {import('@doc-kit/core/utils/configuration/types').Configuration} */ export default { - global: { - project: 'My Project', // Project name used in titles, the logo, and templates - }, + // target and global as above html: { - head: { - html: [ - // re-write the brand color for effect - ``, - ], - }, - // use a custom logo component instead of the plain project name - // our logo.jsx file like this, just for the demo - // export default Logo = () => - // ; + // Your component in place of the project's name imports: { - '#theme/Logo': join(import.meta.dirname, './logo.jsx'), - // You can also change things such as `#theme/Layout`, - // and more! + '#theme/Logo': join(import.meta.dirname, 'theme/Logo.jsx'), }, + + // Bundled after doc-kit's own styles, so your rules win + stylesheets: [join(import.meta.dirname, 'theme/styles.css')], }, }; ``` -Open the printed URL (usually ) and your documentation is -on screen. +```jsx displayName="theme/Logo.jsx" +export default () => Kittens; +``` + +```css displayName="theme/styles.css" +:root { + --color-brand-100: #f7f1fb; + --color-brand-200: #ead9fb; + --color-brand-300: #dbbdf9; + --color-brand-400: #c79bf2; + --color-brand-500: #b07ae6; + --color-brand-600: #9756d6; + --color-brand-700: #7d3cbe; + --color-brand-800: #642b9e; + --color-brand-900: #361b52; +} +``` + +> [!NOTE] +> Theme components are compiled for Preact, so `preact` has to resolve from +> your project. npm usually hoists it there from `@doc-kit/generator-react`; +> with pnpm, add it yourself with `pnpm add --save-dev preact`. + +[Customizing the site](./customization.md) covers the other parts of the page +you can replace, and the [Website generator](./generators/html.md) lists every +option. ## Next steps -- [Writing documentation](./writing-docs.md) — the Markdown conventions that - make `doc-kit` more than a static-site generator. -- [Customizing the site](./customization.md) — your name, logo, navigation, - and components instead of the defaults. -- [Publishing your docs](./publishing.md) — production builds, base URLs, and - hosting. +- [Writing documentation](./writing-docs.md): the conventions that turn + Markdown into an API reference. +- [Customizing the site](./customization.md): navigation, components, MDX and + the bundler. +- [Generators](./generators.md): JSON, `llms.txt`, a sitemap, and every + other output doc-kit can build. +- [Publishing your docs](./publishing.md): hosting, caching and base URLs. +- [Creating generators](./creating-generators.md): outputs of your own. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 05229c9c9..000000000 --- a/docs/index.md +++ /dev/null @@ -1,20 +0,0 @@ -# `doc-kit` - -`doc-kit` turns API-shaped Markdown into documentation. We'd love -feedback in the [issue tracker][] or the [#nodejs-website channel][] -on [the OpenJS Slack][]. - -## Showcase - -A few places `doc-kit` is already in use. Feel free to PR yours. - -- - `legacy-html`, `legacy-json` -- - `llms-txt` -- - `html`, `orama-db`, `llms-txt` -- - `html`, `orama-db`, `llms-txt` -- - `html`, `orama-db`, `llms-txt` -- - `html`, `orama-db`, `llms-txt` - -[issue tracker]: https://github.com/nodejs/doc-kit/issues -[#nodejs-website channel]: https://openjs-foundation.slack.com/archives/CVAMEJ4UV -[the OpenJS Slack]: http://slack.openjsf.org/ diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 000000000..f55a86c5f --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,138 @@ +# Why doc-kit + +doc-kit is a documentation toolkit for APIs. It reads your API from Markdown, +MDX, TypeScript and JSDoc, and turns it into documentation: a website with +search, JSON described by a schema, `llms.txt` for language models, man pages, +and any other format a generator can write. + +It's the toolkit behind the [Node.js API reference](https://nodejs.org/api), +which taught it most of what it knows. This page explains what sets it apart, +and when another tool may suit you better. + +## Built around the API + +Most documentation tools see a page. doc-kit also sees the API in it. A +heading like `server.listen(port[, callback])` is a method with its signature, +the list under it holds its typed parameters, a YAML comment holds its +history, and a `Stability:` blockquote says where it is in its lifecycle. That +structure is part of [doc-kit's Markdown specification](./specification.md), +so every output gets it as data, not as text that happens to look like an +API: + +- types in braces link to their documentation: on your site, on MDN, or + wherever your type map points; +- pages show each entry's history, stability and source; +- the JSON output carries all of it, described by a published schema. + +TypeScript projects can skip most of the writing: the experimental +[`@doc-kit/typedoc`](./packages/typedoc.md) plugin for TypeDoc turns their +source and its JSDoc comments into the same Markdown. See +[Documenting TypeScript](./typescript.md). + +## One parse, every output + +Each output comes from a [generator](./generators.md): a module that names the +generator it reads from and writes its own format. doc-kit loads the generators +a build asks for, along with the ones they read from, wires them into a graph, +parses your sources once, and runs the generators that don't depend on one +another at the same time. + +A format of your own, such as an RSS feed, a changelog or TypeScript +declarations, is one more module reading the same typed API entries as the +website. You pass its path to `--target` like any built-in name. See +[Creating generators](./creating-generators.md). + +## Performance + +The heavy stages, from parsing Markdown to rendering the website, split their +work into chunks and run them on worker threads: one per CPU core, up to four, +unless you set [`--threads`](./cli.md). + +Memory stays low too. Each stage's result is dropped as soon as the last +generator reading it is done with it, and the website generator holds one page +per worker, so its memory follows your largest page rather than the size of +the whole site. + +Building this site, 40 pages and 50,225 words, with its own +[configuration](https://github.com/nodejs/doc-kit/blob/main/www/doc-kit.config.mjs): + +| Targets | Output | Time | +| --------------------------- | ------------------------------------- | ------ | +| `llms-txt` | `llms.txt` | 0.56 s | +| `orama-db` | The search index | 0.61 s | +| `json` | 40 JSON documents | 0.63 s | +| `html` | The site: 40 pages, bundled with Vite | 2.74 s | +| The configuration's targets | All of it, 214 files | 2.94 s | + +Most of the website's time is Vite bundling its scripts and styles once, a +cost that doesn't grow with the number of pages. The Node.js v26.12.1 API docs +show how the rest scales, with 70 files and 574,627 words, built with the +configuration of [the Node.js preview site](https://beta.docs.nodejs.org/): + +| Targets | Output | Time | +| ----------------------------------------------- | -------------------- | ------ | +| `llms-txt` | `llms.txt` | 2.4 s | +| `orama-db` | The search index | 2.8 s | +| `json` | 70 JSON documents | 2.9 s | +| `section-pages` | The site: 781 pages | 10.2 s | +| `section-pages`, `json`, `llms-txt`, `orama-db` | All of it, 923 files | 10.8 s | + +Each time is the median of five clean builds on an AMD Ryzen 9 8945HX +(32 threads, 4 used by doc-kit) with Node.js 24.18 and doc-kit +`b413a20`. To measure them on your machine, run this from a doc-kit +checkout with its dependencies installed. It checks out the Node.js docs +once, builds both sites, and writes the results to +`www/theme/benchmarks.json`, which the home page reads: + +```bash +cd www +node --run benchmark -- --runs 5 --node v26.12.1 +``` + +## Light by default + +The [website generator](./generators/html.md) renders every page to static +HTML at build time, and the content is never shipped as JavaScript. Only the +interactive parts load code, as islands: the search box, the theme switch, the +sidebar and announcement banners hydrate once the browser is idle, and a code +block hydrates the first time you point at, focus or touch it, for its copy +button and tabs. + +In browsers with the Navigation API, following a link to another page of the +site swaps it in place, without a full reload. Pages are prefetched when you +hover over or press a link, so most navigations don't wait on the network. + +## Used by + +A few places doc-kit is already in use. Feel free to open a pull request to +add yours. + +- , the Node.js API reference +- , its next version +- , the Node.js guides +- +- , a preview of webpack's docs +- , Undici's docs + +## When to use something else + +doc-kit is made for documenting APIs. If your site is mostly prose, without an +API behind it, a general-purpose site generator such as +[VitePress](https://vitepress.dev/), [Docusaurus](https://docusaurus.io/) or +[Eleventy](https://www.11ty.dev/) may suit you better. doc-kit handles guides +well too, as this site shows, but they aren't why it exists. + +If your docs have an API behind them, [Getting started](./getting-started.md) +takes you from one Markdown file to a site with search in a few minutes. + +## Get involved + +doc-kit is an open source project of the Node.js organization. Bug reports, +ideas and pull requests are welcome in the [issue tracker][], and the +[contributing guide][] explains how to set up the repository. You can find the +team in the [#nodejs-website channel][] on [the OpenJS Slack][]. + +[issue tracker]: https://github.com/nodejs/doc-kit/issues +[contributing guide]: https://github.com/nodejs/doc-kit/blob/main/CONTRIBUTING.md +[#nodejs-website channel]: https://openjs-foundation.slack.com/archives/CVAMEJ4UV +[the OpenJS Slack]: http://slack.openjsf.org/ diff --git a/docs/publishing.md b/docs/publishing.md index 359b95358..3ce2cc316 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -1,27 +1,157 @@ # Publishing your docs -`doc-kit generate` writes a fully static site, so publishing -is as simple as hosting the output directory. +`doc-kit generate` writes a static site to one directory: the HTML pages, +their scripts and stylesheets, and every other output you target. To publish +it, build with your production settings, then upload that directory to any +static host. This guide covers both, with recipes for common hosts. + +## Build for production + +In your configuration file, set where the site will live and which version it +documents: + +```js displayName="doc-kit.config.mjs" +export default { + target: ['html', 'orama-db', 'json', 'sitemap', 'llms-txt'], + + global: { + project: 'Kittens', + version: '1.0.0', + input: ['docs/**/*.md'], + output: 'out', + baseURL: 'https://kittens.example.com', + minify: true, + }, +}; +``` + +Then build: ```bash -npx @doc-kit/cli generate +npx doc-kit generate +``` + +Three settings matter most for a published site: + +- `baseURL` is the URL the site is published at, without a trailing slash. + The sitemap, `llms.txt` and absolute links are built from it. +- `version` is the version your docs describe, shown in page titles. Without + it, titles show the version of Node.js running the build. +- `minify` minifies the pages' HTML, and writes the JSON outputs, the search + index included, without whitespace. Scripts and stylesheets are minified + either way. The `--minify` flag sets it too. + +> [!WARNING] +> Without `baseURL`, `sitemap.xml` and `llms.txt` are still written, but every +> URL in them starts with a literal `{baseURL}`. + +The [configuration reference](./configuration.md#global-options) lists the +other global options. + +## Check it locally + +```bash +npx doc-kit preview +``` + +`preview` serves the output directory (`./out` by default) on +. Without options, it serves what is already there; pass +any `generate` option, such as `--config-file doc-kit.config.mjs`, and it builds +first. `--port` and `--host` change where it listens; the +[CLI reference](./cli.md) has the details. + +Always open the pages over HTTP, never straight from disk. The pages load +their scripts as JavaScript modules, request their stylesheet with CORS, and +fetch the search index. Browsers block all three for `file://` pages, which +then show up unstyled and inert. + +The preview server is for checking a build. It doesn't compress responses or +set cache headers, so leave those to your host. + +## Ship what the pages link to + +Besides its pages and `assets/`, the site links to files that other generators +write, or that you copy in yourself: + +| File | Used by | How it gets there | +| --------------- | ------------------------------------------------------------------ | ------------------------------ | +| `orama-db.json` | The search box | Add `orama-db` to `target`. | +| `.json` | The "View as JSON" link on each page | Add `json` to `target`. | +| `.md` | The "View as MD" link on each page, `llms.txt` and `llms-full.txt` | Copy your Markdown, see below. | +| `404.html` | Your host, for URLs that don't exist | Written by default. | + +The search box only appears when the search index is built in the same run as +the pages. If you build the index separately, set `html.showSearchBox` to +`true`. + +To copy files into the output as part of the build, list them in +`pathsToCopy`, relative to the working directory. A string copies a path under +its own name (`'public'` lands in `out/public/`), and an object maps each +source to a path inside the output. The default list is +`['assets', 'public', 'static']`, and setting `pathsToCopy` replaces it rather +than adding to it. This copies your Markdown next to the pages: + +```js displayName="doc-kit.config.mjs" +export default { + global: { + // docs/kittens.md is published as kittens.md, next to kittens.html + pathsToCopy: [{ docs: '.' }], + }, +}; ``` -Everything lands in your configured `output` directory (`out/` by -convention). Two things to know about the result: +## Page URLs + +Each page is written as `.html`, and the links doc-kit makes between +your pages carry that extension, so the site works on any static host without +configuration. Many hosts also serve a page at its extensionless URL +(`/kittens`), and some redirect `/kittens.html` there, such as Vercel with +`cleanUrls` and Cloudflare Workers by default. The site follows those +redirects when it navigates, so either setup works. + +Type links are the exception: doc-kit uses the paths in your type map as +written. Those that [`@doc-kit/typedoc`](./packages/typedoc.md) writes have no +extension, so they need a host that serves `/kittens` from `kittens.html`. -- **The `html` site must be served over HTTP.** Its pages use import maps - and client-side hydration, so opening the files with `file://` may show - unstyled or inert pages. Preview a production build the same way you'd - host it: +If your host redirects to extensionless URLs, list those in the sitemap, so +search engines don't crawl redirects: - ```bash - npx serve out - ``` +```js displayName="doc-kit.config.mjs" +export default { + sitemap: { + // `{path}` is the page's path without its extension, starting with `/` + pageURL: '{baseURL}{path}', + }, +}; +``` -- **Search requires its index.** If your site uses search, generate the - index alongside the pages by targeting both generators — `target: ['html', -'orama-db']`, so the search box has data to query. +The [sitemap](./generators/sitemap.md) and [`llms.txt`](./generators/llms-txt.md) +pages list their URL templates and defaults. + +### Hosting under a subpath + +Pages reach each other and their assets through relative URLs, so a site +works under any path, such as a GitHub Pages project site at +`https://.github.io//`. Two pages load their assets from the root +of the domain instead: `404.html`, which hosts serve at any missing URL, and +`all.html`. Turn on `useAbsoluteURLs`, and they load them from `baseURL`: + +```js displayName="doc-kit.config.mjs" +export default { + global: { + baseURL: 'https://owner.github.io/repo', + }, + + html: { + useAbsoluteURLs: true, + }, +}; +``` + +Every link then points at `baseURL`, so when you preview that build locally, +following a link takes you to the published site. The logo in the default +navigation bar links to the root of the domain either way; to change that, +replace `#theme/Navigation` (see [Customizing the site](./customization.md)). ## Cache the assets @@ -30,15 +160,117 @@ fonts) is named after a hash of its content, so a changed file always gets a new name. Serve that directory with a long-lived, immutable cache, and let everything else (the pages, the search index) revalidate: -``` +```text /assets/* Cache-Control: public, max-age=31536000, immutable ``` -Most hosts default to revalidating every file on every load instead, which -costs a request per asset each time a new tab opens the site. On Vercel: +Most hosts revalidate every file on every load by default, which costs a +request per asset each time someone opens the site. The Netlify, Vercel and +Cloudflare Workers [host recipes](#host-recipes) below set this header (GitHub +Pages doesn't let you). Within a visit, the site swaps pages in client-side in +browsers that support it, so moving between pages doesn't load the assets +again. + +> [!CAUTION] +> Keep your own files out of a project-level `assets/` directory. The default +> `pathsToCopy` copies it into `out/assets/` as is, without hashed names, so a +> changed file would stay cached for a year. + +## Compress the pages + +Pages, scripts and the search index are text, and shrink a lot when served +compressed (gzip, brotli or zstd). Cloudflare, Vercel, Netlify and GitHub +Pages compress on their own. If you serve the output yourself, turn +compression on: + +- **nginx:** `gzip on;` compresses HTML; add the other types with + `gzip_types text/css text/javascript application/javascript application/json image/svg+xml;`. +- **Caddy:** `encode zstd gzip`. +- **Apache:** with `mod_deflate`, + `AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json`. +- **Amazon S3:** serve the bucket through CloudFront, with **Compress objects + automatically** on. + +## Host recipes + +Each recipe assumes a `doc-kit.config.mjs` that writes to `out`, with +`@doc-kit/cli` (and `@doc-kit/generator-react`, for the site) in your +`devDependencies`. + +### GitHub Pages + +In your repository's settings, under **Pages**, set the source to **GitHub +Actions**, then add a workflow that builds the site and deploys it: + +```yaml displayName=".github/workflows/docs.yml" +name: Publish docs + +on: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - run: npm ci + - run: npx doc-kit generate + - uses: actions/upload-pages-artifact@v5 + with: + path: out + + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 +``` + +GitHub Pages serves `404.html` for missing pages, but doesn't let you set +response headers, so the assets keep its default caching. A project site +lives under a subpath; see [Hosting under a subpath](#hosting-under-a-subpath). + +### Netlify + +Netlify installs your dependencies, runs the build `command` and publishes the +`publish` directory: + +```toml displayName="netlify.toml" +[build] + command = "npx doc-kit generate" + publish = "out" + +[[headers]] + for = "/assets/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" +``` + +### Vercel ```json displayName="vercel.json" { + "buildCommand": "npx doc-kit generate", + "outputDirectory": "out", + "cleanUrls": true, "headers": [ { "source": "/assets/(.*)", @@ -53,61 +285,117 @@ costs a request per asset each time a new tab opens the site. On Vercel: } ``` -Within a visit, moving between pages loads no assets at all: the site -navigates client-side, swapping in the next page's content. +`cleanUrls` serves each page without its `.html` and redirects the `.html` URL +there. Drop it to keep the extensions; otherwise, set the sitemap's `pageURL` +to match (see [Page URLs](#page-urls)). -## Compress the pages +### Cloudflare Workers + +Cloudflare Workers serves the output as static assets. Its response headers +come from a `_headers` file at the root of the output, which `pathsToCopy` +can put there: -doc-kit doesn't minify its output by default: compressed (gzip or brotli), as -hosts serve it, it is far smaller than minifying would make it. +```jsonc displayName="wrangler.jsonc" +{ + "name": "kittens-docs", + "compatibility_date": "2026-10-01", + "assets": { + "directory": "./out", + "html_handling": "auto-trailing-slash", + "not_found_handling": "404-page", + }, +} +``` -Cloudflare, Vercel, Netlify, and GitHub Pages compress on their own, and so -does `npx serve out`. If you serve the output yourself, turn compression on: +```text displayName="public/_headers" +/assets/* + Cache-Control: public, max-age=31536000, immutable +``` -- **nginx:** `gzip on;` compresses HTML; add the other types with - `gzip_types text/css application/javascript application/json;`. -- **Caddy:** `encode zstd gzip`. -- **Apache:** with `mod_deflate`, - `AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json`. -- **Amazon S3:** serve the bucket through CloudFront, with **Compress objects - automatically** on. +```js displayName="doc-kit.config.mjs" +export default { + global: { + // Replaces the default list: copies public/ (with _headers) to the root of + // out/, and docs/ for the .md files the pages link to + pathsToCopy: [{ public: '.', docs: '.' }], + }, +}; +``` + +`auto-trailing-slash` serves `/kittens` from `kittens.html` and redirects +`/kittens.html` there, and `404-page` answers missing URLs with `404.html`. +As with Vercel, set the sitemap's `pageURL` to match (see +[Page URLs](#page-urls)). Build, then deploy with Wrangler: -## Tell doc-kit its public URL +```bash +npx doc-kit generate +npx wrangler deploy +``` -Set `baseURL` to where the site will live. Generators that emit absolute -URLs (e.g., `sitemap`, `llms-txt`) will need it in order to generate their -output. +## Publishing several versions -```mjs displayName="doc-kit.config.mjs" +The version selector in the sidebar lists the releases in `global.changelog` +(or `--changelog`); a site built without one has no selector. Build each +version on its own, with its `version` and `baseURL`, and publish it where +`html.pageURL` says it lives: + +```js displayName="doc-kit.config.mjs" export default { global: { - baseURL: 'https://example.com/docs', + version: '3.0.0', + baseURL: 'https://kittens.example.com/v3.x', + changelog: './CHANGELOG.md', }, html: { - // Where a page's canonical URL lives; {path} is filled per page - pageURL: 'https://example.com/docs{path}.html', + // Where each version's copy of a page lives + pageURL: 'https://kittens.example.com/{version}{path}.html', }, }; ``` -## Machine-readable companions +In `pageURL`, `{version}` becomes `v.x`, or `v..x` for a +release whose minor version isn't 0 (`v3.x`, `v2.1.x`), and `{path}` the +page's path. -Search engines and language models each get their own artifact — add the -generators and they share the same parse: +`changelog` takes a URL or a path to a Markdown file that lists releases one +per line, in the format of the +[Node.js changelog](https://github.com/nodejs/node/blob/main/CHANGELOG.md): -```mjs displayName="doc-kit.config.mjs" -target: ['html', 'orama-db', 'sitemap', 'llms-txt'], +```text displayName="CHANGELOG.md" +* [Node.js 3.0](https://kittens.example.com/v3.x/) Current +* [Node.js 2.1](https://kittens.example.com/v2.1.x/) **Long Term Support** ``` -`sitemap` emits `sitemap.xml`; `llms-txt` emits an -[`llms.txt`](https://llmstxt.org/) index of your pages. +Each line needs the `*` bullet, the literal `Node.js` label, whatever your +project is called, and some notes after the link. Lines in any other shape are +skipped without an error. The link's target is ignored, since the selector +builds its links from `html.pageURL`, and notes that mention "Long Term +Support" mark an LTS release. + +In a JavaScript configuration file, you can list the releases instead, each +as `{ version, isLts, isCurrent }` with a version from the `semver` package. +`isCurrent` adds "(Current)" to the release's label in the selector, which the +Markdown file can't do: + +```js displayName="doc-kit.config.mjs" +import { coerce } from 'semver'; + +export default { + global: { + changelog: [ + { version: coerce('3.0.0'), isLts: false, isCurrent: true }, + { version: coerce('2.1.0'), isLts: true }, + ], + }, +}; +``` -## Versioned docs +Docs built for an older release keep the list they were built with. To update +it without rebuilding them, point `html.remoteConfigUrl` at a JSON file whose +`versions` replace the built-in list; the +[website generator](./generators/html.md#remote-configuration) describes its +shape. -The version selector in the sidebar is built from your `changelog` — a -release history the generators parse into selectable versions. Point -`global.changelog` at a `CHANGELOG.md` (URL or path) that lists your -releases, and each build renders its `version` with links to the others. A -project without release history simply gets no selector. See the -[configuration reference](./configuration.md) for the accepted formats. +Once your site is online, [Creating generators](./creating-generators.md) +shows how to add outputs doc-kit doesn't ship, such as an RSS feed. diff --git a/docs/specification.md b/docs/specification.md index 7ac7b50e1..c47f8c646 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -5,19 +5,37 @@ **Authored By**: Aviv Keller () This document specifies the Markdown format consumed by -[`@doc-kit/core`][doc-kit]. It defines the structural, syntactic, and -semantic rules that documents MUST follow to be correctly parsed. The format -is a strict superset of [GitHub Flavored Markdown][gfm] (which itself is a -strict superset of [CommonMark][commonmark]), adding conventions for API -metadata, type annotations, stability indicators, and structured parameter -lists. - -Because this format is a superset, all constructs valid in [GFM][gfm] remain -valid here. This specification only defines the **additional** constraints and -semantics layered on top. Where a [GFM][gfm] construct is given special -meaning by this specification (e.g., blockquotes as stability indicators, -unordered lists as typed parameter lists), that special meaning is documented -in the relevant section. +[`@doc-kit/core`][doc-kit]: the structural, syntactic, and semantic rules that +documents MUST follow to be correctly parsed. The format is +[GitHub Flavored Markdown][gfm] (GFM), itself an extension of +[CommonMark][commonmark], with conventions for API metadata, type annotations, +stability indicators, and structured parameter lists. + +For a task-oriented introduction to the format, read +[Writing documentation](./writing-docs.md) first, and come back here for the +exact rules. + +## How to Read This Specification + +Sections 1 and 2 define the terms used throughout and what makes a document +conforming. Sections 3 to 11 each cover one construct: its rules, then an +example. Requirements are stated with the capitalized key words of +[§1.1][§1.1]; examples and the text explaining them are informative. Defined +terms, such as [entry][term-entry], link to their definitions in +[§1.2][§1.2]. + +Each construct at a glance: + +| Construct | Looks like | Defined in | +| -------------------- | -------------------------------------------------- | ------------------ | +| Entry heading | `` ### `fs.readFile(path[, options], callback)` `` | [§4][§4], [§5][§5] | +| YAML metadata block | `` | [§6][§6] | +| Simple directive | `` | [§6.2][§6.2] | +| Stability indicator | `> Stability: 1 - Experimental` | [§7][§7] | +| Type annotation | `{string}`, `{Promise}` | [§8][§8] | +| Typed list item | `` * `encoding` {string} **Default:** `'utf8'`. `` | [§9][§9] | +| Fenced code block | ` ```js displayName="Reading a file" ` | [§10][§10] | +| Reference-style link | ``[`fs.readFile()`][]`` | [§11][§11] | --- @@ -43,9 +61,9 @@ or one index listing. **Entry.** A discrete API element within a [document][term-document]. An [entry][term-entry] begins at a [heading][§4] and extends to the next heading of equal or lesser depth, or to the end of the file. An entry comprises a -heading, optional [YAML metadata][§6], optional -[stability indicator][§7], optional [typed parameter list][§9], and a prose -body. +heading, optional [YAML metadata][§6], an optional +[stability indicator][§7], an optional [typed parameter list][§9], and an +optional prose body. **Section.** A contiguous range of [entries][term-entry] sharing a common parent [heading][§4]. @@ -80,16 +98,17 @@ to hyperlinks. See [§8][§8]. ### 2.1. Base Grammar A conforming [document][term-document] MUST be valid [GitHub Flavored -Markdown][gfm] (GFM), which is itself a superset of [CommonMark][commonmark]. -This specification is a strict superset of [GFM][gfm]: all [GFM][gfm] -constructs are valid, and this specification layers additional structure and -semantics on top. +Markdown][gfm] (GFM). This specification layers structure and semantics on +top of [GFM][gfm] in two ways: -Where this specification assigns special meaning to a [GFM][gfm] construct -(for example, blockquotes used as [stability indicators][§7], or unordered -lists used as [typed parameter lists][§9]), that special meaning is defined in -the relevant section. All other [GFM][gfm] constructs retain their standard -behavior. +- It assigns special meaning to some constructs, such as blockquotes used as + [stability indicators][§7] and unordered lists used as + [typed parameter lists][§9]. Each special meaning is defined in the relevant + section. +- It restricts others: [Setext headings][§4.1], [indented code + blocks][§10.1], and [tab characters][§2.4] MUST NOT be used. + +All other [GFM][gfm] constructs are valid and retain their standard behavior. ### 2.2. Character Encoding @@ -114,13 +133,14 @@ reproduce literal output. ### 3.1. Top-Level Ordering -A conforming [document][term-document] MUST contain elements in the following -order. Items marked OPTIONAL MAY be omitted entirely. +The top-level elements of a [document][term-document] SHOULD appear in the +following order. Items marked OPTIONAL MAY be omitted entirely. -1. **Title heading** (OPTIONAL, RECOMMENDED) - An [ATX heading][cm-atx-heading] at depth 1. +1. **Title heading** (OPTIONAL, RECOMMENDED) - An + [ATX heading][cm-atx-heading] at depth 1. 2. **[Simple directives][§6.2]** (OPTIONAL) - Zero or more [HTML comment][cm-html-comment] directives providing - [document][term-document]-level metadata. These MUST immediately follow + [document][term-document]-level metadata. These SHOULD immediately follow the title heading. 3. **Module-level [stability indicator][§7]** (OPTIONAL) - A stability blockquote applying to the [document][term-document] as a whole. @@ -129,8 +149,9 @@ order. Items marked OPTIONAL MAY be omitted entirely. REPEATABLE) - One or more [headings][§4] at depth 2 or greater, each introducing an [entry][term-entry]. Each entry follows the sub-ordering in [§3.2][§3.2]. -6. **[Link reference definitions][§11.6]** (OPTIONAL) - Collected at the end - of the file, after all [entry][term-entry] [sections][term-section]. +6. **[Link reference definitions][§11.6]** (OPTIONAL) - These SHOULD be + collected at the end of the file, after all [entry][term-entry] + [sections][term-section]. ```markdown # Command-line API @@ -148,8 +169,8 @@ debugging, multiple ways to execute scripts, and other helpful runtime options. Within each API [entry][term-entry], elements MUST appear in this order: 1. **[Heading][§4]** (REQUIRED) - An [ATX heading][cm-atx-heading]. -2. **[YAML metadata block][§6]** (OPTIONAL) - Immediately following the - heading, with no intervening content. +2. **[YAML metadata block][§6]** (OPTIONAL) - This SHOULD immediately follow + the heading ([§6.7][§6.7]). 3. **[Stability indicator][§7]** (OPTIONAL) - A stability blockquote. 4. **[Typed parameter list][§9]** (OPTIONAL) - An unordered list describing parameters, return value, type, or extends clause. @@ -197,22 +218,28 @@ its text against the patterns below, evaluated in the order given. The first match wins. All pattern matching is performed on the plain text of the heading after stripping the ATX prefix. +Each type has a name, given in its subsection, which the +[`type` field][§6.3.5] of a YAML metadata block can use to override the +classification. + #### 4.3.1. Method -The heading text is a backtick-wrapped expression containing a dotted or -bracketed receiver, a member accessor, and parentheses. The parentheses -distinguish methods from [properties][§4.3.6]. +Type name: `method`. The heading text is a backtick-wrapped call: a function +name, optionally reached through a dotted or bracketed receiver, followed by +parentheses. The parentheses distinguish methods from [properties][§4.3.6]. ```markdown ### `fs.readFile(path[, options], callback)` ### `buf.write(string[, offset[, length]][, encoding])` ### `request[Symbol.asyncIterator]()` +### `require(id)` ``` #### 4.3.2. Event -The heading text begins with the literal prefix `Event:` followed by optional -whitespace and a backtick-wrapped, single-quoted event name. +Type name: `event`. The heading text begins with the literal prefix `Event:`, +a space, and a backtick-wrapped event name, which SHOULD be single-quoted +([§4.5.2][§4.5.2]). ```markdown ### Event: `'close'` @@ -221,8 +248,8 @@ whitespace and a backtick-wrapped, single-quoted event name. #### 4.3.3. Class -The heading text begins with the literal prefix `Class:` followed by optional -whitespace and a backtick-wrapped class identifier. The class name SHOULD begin +Type name: `class`. The heading text begins with the literal prefix `Class:`, +a space, and a backtick-wrapped class identifier. The class name SHOULD begin with an uppercase ASCII letter. ```markdown @@ -232,9 +259,9 @@ with an uppercase ASCII letter. #### 4.3.4. Constructor -The heading text is a backtick-wrapped expression beginning with the keyword -`new`, followed by whitespace, then an uppercase-initial class identifier and -parenthesized [parameters][§5]. +Type name: `ctor`. The heading text is a backtick-wrapped expression beginning +with the keyword `new`, followed by whitespace, then an uppercase-initial class +identifier and parenthesized [parameters][§5]. ```markdown ### `new Agent([options])` @@ -243,8 +270,8 @@ parenthesized [parameters][§5]. #### 4.3.5. Static Method -The heading text begins with the literal prefix `Static method:` followed by -optional whitespace and a backtick-wrapped qualified method call (with +Type name: `classMethod`. The heading text begins with the literal prefix +`Static method:`, a space, and a backtick-wrapped qualified method call (with parentheses). ```markdown @@ -254,9 +281,9 @@ parentheses). #### 4.3.6. Property -The heading text is a backtick-wrapped dotted or bracketed property access -that does not contain parentheses. The absence of parentheses distinguishes -properties from [methods][§4.3.1]. +Type name: `property`. The heading text is a backtick-wrapped dotted or +bracketed property access that does not contain parentheses. The absence of +parentheses distinguishes properties from [methods][§4.3.1]. ```markdown ### `buf.length` @@ -266,9 +293,13 @@ properties from [methods][§4.3.1]. #### 4.3.7. Default Classification -If no pattern matches, the [entry][term-entry] is classified as a +If no pattern matches, the [entry][term-entry] has no type and is a miscellaneous [section][term-section]. +```markdown +## Synopsis +``` + ### 4.4. Document Type and Heading Interpretation The [document][term-document]-level [`type` directive][§6.2.2] determines how @@ -281,7 +312,8 @@ all headings in the file. This context can be overridden at the When `type` is `module` (or when no `type` directive is present, as `module` is the default), classified [entries][term-entry] are treated as **exports of -the module** identified by the document's filename or [`module` simple directive][§6.2.5]. +the module** identified by the document's filename or +[`name` simple directive][§6.2.6]. A single module document MAY describe exports from more than one related module (e.g., `fs` and `fs/promises`). The module boundary does not affect @@ -364,7 +396,12 @@ or in `misc`-type documents that contain isolated module exports. Programmatic identifiers (method names, property names, class names, event names, flag names) MUST be wrapped in backtick code spans. Prose -[section][term-section] headings (e.g., `## Synopsis`) MUST NOT use backticks. +[section][term-section] headings MUST NOT use backticks. + +```markdown +### `buf.length` +## Synopsis +``` #### 4.5.2. Event Name Quoting @@ -439,8 +476,8 @@ documented in the [typed parameter list][§9]. ### 6.1. Block Syntax [YAML metadata][term-yaml-block] is embedded in [HTML comments][cm-html-comment]. -The opening delimiter is ``. The content between these delimiters MUST +The opening delimiter is ``. The content between these delimiters MUST be valid [YAML 1.2][yaml-1.2]. ```markdown @@ -460,9 +497,9 @@ object for that heading. uses a compact `key=value` syntax inside [HTML comments][cm-html-comment]. Each directive has the form ``. -Document-level directives (i.e. `introduced_in`) SHOULD appear immediately -after the depth-1 heading. Section-level directives (see [§6.2.5][§6.2.5]) -SHOULD appear immediately after the heading they annotate. +Document-level directives (such as `introduced_in` and `type`) SHOULD appear +immediately after the depth-1 heading. Section-level directives (see +[§6.2.5][§6.2.5]) SHOULD appear immediately after the heading they annotate. ```markdown # File System @@ -480,23 +517,31 @@ Each [directive][term-simple-directive] occupies exactly one A [version string][§6.4] indicating when the module was introduced. +- **Example:** `` + #### 6.2.2. `type` The [document][term-document] classification. Recognized values are `module`, `misc`, and `global`. Implementations MAY define additional values. If no `type` directive is present, the default is `module`. +- **Example:** `` + See [§4.4][§4.4] for how document type influences heading interpretation. #### 6.2.3. `source_link` A relative file path to the implementation source. Rendered as a link. +- **Example:** `` + #### 6.2.4. `llm_description` A plain-text description optimized for consumption by Large Language Models, used in preference to extracted prose where available. +- **Example:** `` + #### 6.2.5. `module` Overrides the owning module for all sub-entries beneath the annotated heading. @@ -511,6 +556,14 @@ See [§4.4.4][§4.4.4] for full semantics. ``` +#### 6.2.6. `name` + +The identifier of the [entry][term-entry] the directive annotates, used +instead of the name derived from its heading text. After the depth-1 heading, +it names the module the [document][term-document] describes. + +- **Example:** `` + ### 6.3. YAML Metadata Fields The following fields are defined within [YAML metadata blocks][term-yaml-block]. @@ -581,10 +634,14 @@ in [§4.4.5][§4.4.5]. Relative path to the implementation source file. Identical semantics to the [simple directive][§6.2.3] of the same name. +- **Example:** `source_link: lib/fs.js` + #### 6.3.7. `llm_description` Identical semantics to the [simple directive][§6.2.4] of the same name. +- **Example:** `llm_description: Reads a whole file into memory.` + ### 6.4. Version Strings Version strings MUST follow [semantic versioning][semver] with a lowercase @@ -654,20 +711,20 @@ begins with `Stability: ` followed by a numeric level, a dash separator Four [stability][term-stability] levels are defined. -#### 7.2.1. Level 0 - Deprecated +#### 7.2.1. Level 0: Deprecated The feature may emit warnings. Backward compatibility is not guaranteed. -#### 7.2.2. Level 1 - Experimental +#### 7.2.2. Level 1: Experimental Not subject to [semantic versioning][semver] rules. Non-backward-compatible changes or removal may occur in any future release. -#### 7.2.3. Level 2 - Stable +#### 7.2.3. Level 2: Stable Compatibility with the ecosystem is a high priority. -#### 7.2.4. Level 3 - Legacy +#### 7.2.4. Level 3: Legacy Still covered by [semantic versioning][semver] guarantees, but no longer actively maintained. Other alternatives are available. @@ -678,15 +735,19 @@ actively maintained. Other alternatives are available. experimental phase. This specification only defines semantics for decimal sub-levels of level 1. -#### 7.3.1. 1.0 - Early Development +```markdown +> Stability: 1.1 - Active development +``` + +#### 7.3.1. Level 1.0: Early Development Unfinished and subject to substantial change. -#### 7.3.2. 1.1 - Active Development +#### 7.3.2. Level 1.1: Active Development Nearing minimum viability. -#### 7.3.3. 1.2 - Release Candidate +#### 7.3.3. Level 1.2: Release Candidate Expected to become stable. @@ -711,9 +772,8 @@ The description MAY contain inline Markdown such as links and code spans: A [stability indicator][term-stability] MUST appear after the [YAML metadata block][§6] (if present) and before the -[typed parameter list][§9] and prose body. A [document][term-document] SHOULD -have at most one [stability indicator][term-stability] per -[entry][term-entry]: +[typed parameter list][§9] and prose body. An [entry][term-entry] SHOULD have +at most one [stability indicator][term-stability]: ```markdown ### `myModule.myMethod()` @@ -743,6 +803,13 @@ Annotations appear both in [typed parameter lists][§9] (as part of item structure) and in prose text (as inline references); both forms are parsed and rendered identically. +```markdown +* `data` {string|Buffer} The data to write. +* Returns: {Promise} + +The callback receives an {Error} or `null`. +``` + ### 8.2. Parsing Implementations MUST parse each annotation whose value is not a whole-value @@ -772,14 +839,15 @@ Each name resolves through the following tiers; the first match wins: 1. The configurable type map provided to the toolchain, which associates names with documentation URLs. An example type map is available for viewing [here][nodejs-typemap]. -2. JavaScript primitives and built-in (TC39) globals, matched - case-insensitively: `string`, `number`, `boolean`, `bigint`, `symbol`, - `null`, `undefined`, `integer`, `Array`, `Object`, `Function`, `Promise`, - and others. -3. Web platform APIs documented on MDN, matched case-insensitively. -4. Dotted-name heuristic: names containing `.` are split on the first `.` to - derive a module name and member identifier, producing a cross-document - link of the form `.md#`. +2. JavaScript primitives and language types, matched case-insensitively: + `string`, `number`, `boolean`, `bigint`, `symbol`, `null`, `undefined`, + `integer`, `any`, `void`, `this`, `Iterable`, and `AsyncIterable`. +3. JavaScript built-in objects and Web platform APIs documented on MDN, + matched case-insensitively, such as `Array`, `Promise`, and `AbortSignal`. +4. Dotted-name heuristic: a name made of dotted identifiers links to the + [document][term-document] of the module named before the first `.`, at + the anchor of the name's own heading. For example, `vm.Module` links to + the `vm` document's `Class: vm.Module` heading. A name that no tier resolves stays plain — this is not an error. @@ -826,7 +894,7 @@ The components, in order, are: 3. **[Type annotation][§8]** - A single `{...}` TypeScript type expression (a union belongs inside it, e.g. `{string|Buffer}`). OPTIONAL. 4. **Description** - Free-form prose. OPTIONAL. -5. **[Default value][§9.5]** - The exact pattern `**Default:** \`value\``. +5. **[Default value][§9.5]** - The exact pattern `` **Default:** `value` ``. OPTIONAL. ### 9.3. Special Prefixes @@ -959,10 +1027,13 @@ External links MUST use full URLs including the protocol: ### 11.4. System-Call Auto-Linking -References to system calls in the format `name(section)` -(e.g., `open(2)` links to https://man7.org/linux/man-pages/man2/open.2.html) -are automatically converted to links to the appropriate manual page -documentation by conforming implementations. +References to system calls in the format `name(section)`, such as `open(2)`, +are automatically converted by conforming implementations to links to the +matching manual page, such as the one on man7.org for `open(2)`. + +```markdown +Opens the file with open(2). +``` ### 11.5. Type Auto-Linking @@ -1004,12 +1075,6 @@ of the [document][term-document], after all content [cm-info-string]: https://spec.commonmark.org/0.31.2/#info-string [cm-html-comment]: https://spec.commonmark.org/0.31.2/#html-blocks [cm-link-ref-def]: https://spec.commonmark.org/0.31.2/#link-reference-definitions -[cm-emphasis]: https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis - - - -[gfm-tables]: https://github.github.com/gfm/#tables-extension- -[gfm-strikethrough]: https://github.github.com/gfm/#strikethrough-extension- @@ -1024,8 +1089,12 @@ of the [document][term-document], after all content +[§1.1]: #11-requirement-level-keywords +[§1.2]: #12-definitions +[§2.4]: #24-whitespace [§3.2]: #32-entry-ordering [§4]: #4-headings +[§4.1]: #41-style [§4.3]: #43-entry-type-classification [§4.3.1]: #431-method [§4.3.3]: #433-class @@ -1034,6 +1103,7 @@ of the [document][term-document], after all content [§4.4]: #44-document-type-and-heading-interpretation [§4.4.4]: #444-module-context-override [§4.4.5]: #445-per-entry-override +[§4.5.2]: #452-event-name-quoting [§5]: #5-signature-syntax [§6]: #6-yaml-comment-blocks [§6.2]: #62-simple-directives @@ -1041,10 +1111,12 @@ of the [document][term-document], after all content [§6.2.3]: #623-source_link [§6.2.4]: #624-llm_description [§6.2.5]: #625-module +[§6.2.6]: #626-name [§6.3.5]: #635-type-override [§6.4]: #64-version-strings +[§6.7]: #67-placement [§7]: #7-stability-indicators -[§7.2.2]: #722-level-1--experimental +[§7.2.2]: #722-level-1-experimental [§7.3]: #73-sub-levels [§8]: #8-type-annotations [§8.1]: #81-syntax @@ -1058,5 +1130,6 @@ of the [document][term-document], after all content [§9.3.3]: #933-type [§9.5]: #95-default-values [§10]: #10-code-blocks +[§10.1]: #101-fence-style [§11]: #11-links-and-cross-references [§11.6]: #116-link-reference-definitions diff --git a/docs/typescript.md b/docs/typescript.md new file mode 100644 index 000000000..d096f4af7 --- /dev/null +++ b/docs/typescript.md @@ -0,0 +1,339 @@ +# Documenting TypeScript + +If your API is written in TypeScript, or in JavaScript with JSDoc comments, +its reference can come from the source. [TypeDoc](https://typedoc.org) reads +your code and its doc comments, the `@doc-kit/typedoc` plugin writes them as +doc-kit Markdown (a page per export, with signatures, typed lists, stability +and source links), and doc-kit builds those pages into one site with your +hand-written guides. + +> [!NOTE] +> `@doc-kit/typedoc` is experimental: its output and options can still change. + +It takes two commands: `npx typedoc` writes the reference into `docs/api/`, +then `npx doc-kit generate` builds everything in `docs/`. + +## Install + +```bash +npm install --save-dev typedoc@0.28 @doc-kit/typedoc @doc-kit/cli @doc-kit/generator-react +``` + +`typedoc` and `@doc-kit/typedoc` write the reference; the plugin is built for +TypeDoc 0.28. `@doc-kit/cli` and `@doc-kit/generator-react` build the site, as +in [Getting started](./getting-started.md). + +## Configure TypeDoc + +Create `typedoc.config.mjs` next to your `package.json`: + +```mjs displayName="typedoc.config.mjs" +import { load } from '@doc-kit/typedoc'; + +export default { + plugin: [load], + entryPoints: ['src/index.ts'], + // Source links are relative to it: the root of your repository + basePath: '.', + docKit: 'docs/api', +}; +``` + +- `plugin: [load]` passes the plugin itself, not its package name. TypeDoc + imports plugins named by a string from its own installation, where package + managers such as pnpm hide your project's dependencies, so passing `load` + works with any package manager. +- `docKit` sets where the reference goes. With it set, TypeDoc skips its own + HTML output. +- `basePath` makes the source links relative to your repository's root, so + they match your files on GitHub. + +Everything else, such as entry points, the `tsconfig` and which exports to +include, is configured with TypeDoc's own options. + +## Generate the reference + +```bash +npx typedoc +``` + +TypeDoc finds `typedoc.config.mjs` in the working directory, and the plugin +writes the reference in TypeDoc's folder layout: + +```text +docs/api/ +├── functions/ +│ └── adopt.md +├── interfaces/ +│ └── Kitten.md +├── pages.json +└── type-map.json +``` + +- A page per exported class, interface, enum, type alias, function and + variable, in `classes/`, `interfaces/`, `enums/`, `types/`, `functions/` + and `variables/`. Namespaces, and modules when you have several entry + points, get a page in `modules/`. +- `type-map.json` maps each type name to its page, so doc-kit can link type + annotations to it. +- `pages.json` lists the pages, for your site's navigation. + +There's no page for the project itself: that one is yours to write, for +example as `docs/index.md`. + +> [!TIP] +> The plugin writes into `docs/api/` without emptying it first. Delete the +> folder before generating when you rename or remove exports, so their old +> pages don't stay behind. Since it's generated, you can also leave it out of +> version control. + +## From doc comments to Markdown + +Take this source file: + +```ts displayName="src/index.ts" +/** A kitten, adopted or not. */ +export interface Kitten { + /** The kitten's name. */ + name: string; +} + +/** + * Adopts a kitten, once the paperwork is done. + * + * @param name - The kitten's name. + * @param options - How to adopt it. + * @param options.color - Coat color. + * @param options.signal - Cancels the adoption. + * @returns The adopted kitten. + * @experimental + */ +export async function adopt( + name: string, + options: { color?: string; signal?: AbortSignal } = {} +): Promise { + return { name }; +} +``` + +The plugin writes `adopt()` as this page: + +```markdown displayName="docs/api/functions/adopt.md" +# `adopt(name[, options])` + + + +> Stability: 1 - Experimental + +- `name` {string} The kitten's name. +- `options` {Object} How to adopt it. **Default:** `{}`. + - `color` {string} Coat color. + - `signal` {AbortSignal} Cancels the adoption. +- Returns: {Promise} The adopted kitten. + +Adopts a kitten, once the paperwork is done. +``` + +It's the Markdown you would write by hand, as described in +[Writing documentation](./writing-docs.md): + +- The heading is the call, with `options` in brackets: it has a default + value, so it's optional. +- The YAML comment holds the page title and the link to the source. +- `@experimental` becomes a stability indicator. +- The typed list holds each parameter's type and `@param` description, the + properties of `options` nested under it, and the return value with its + `@returns` description. +- The summary follows, as the entry's description. + +Once doc-kit builds it, `Promise` links to the `Kitten` page through +the type map, and `Promise` and `AbortSignal` link to MDN. + +Other tags follow the same pattern: `@deprecated` becomes a +`Stability: 0 - Deprecated` indicator with its message, `@example` an +"Example" section, and `{@link}` a link to its target. TypeDoc's `@remarks` +and `@since` aren't rendered, so keep that text in the summary. + +Declarations follow doc-kit's conventions too: a class gets a `Class:` +heading, and a property an entry with a `Type:` item, named after its owner +with a lowercase first letter, such as `kitten.name`. The +[`@doc-kit/typedoc` reference](./packages/typedoc.md#how-code-and-comments-are-rendered) +lists every tag and declaration the plugin renders. + +## Build the site + +Point doc-kit at the whole `docs/` folder, and at the type map: + +```mjs displayName="doc-kit.config.mjs" +export default { + target: ['html', 'orama-db'], + + global: { + project: 'Kittens', + repository: 'you/kittens', + input: ['docs/**/*.md'], + output: 'out', + }, + + metadata: { + typeMap: 'docs/api/type-map.json', + }, +}; +``` + +- `input` reads the guides and the reference alike: `docs/**/*.md` includes + `docs/api/`. +- `metadata.typeMap` links every type annotation naming one of your types to + its page, in the reference and in your guides. +- `repository` (`owner/name`) turns the source links into links to your + files on GitHub. + +The type map's paths are relative to the folder your `input` glob starts from, +here `docs/`: + +```json displayName="docs/api/type-map.json" +{ + "Kitten": "api/interfaces/Kitten" +} +``` + +These paths have no extension, so type links need a host that serves +`/api/interfaces/Kitten` from `Kitten.html`, as GitHub Pages, Netlify and +Cloudflare do, and Vercel with `cleanUrls`. See +[Page URLs](./publishing.md#page-urls). + +If the reference sits deeper, such as in `docs/reference/api/`, set the +plugin's `docKitBasePath` option to `'reference/api'` so the paths still +match. + +In your guides, link to reference pages by their file, and name your types in +braces, as you would anywhere else: + +```markdown displayName="docs/index.md" +# Kittens + +Call [`adopt()`](./api/functions/adopt.md) to bring home a {Kitten}. +``` + +Run TypeDoc before each build, for example from a script in your +`package.json`: + +```json displayName="package.json" +{ + "scripts": { + "docs": "typedoc && doc-kit generate" + } +} +``` + +`npm run docs` then builds the site into `out/`; preview it as in +[Getting started](./getting-started.md). + +## Building the sidebar + +Without [`navigation.sidebar`](./generators/html.md#sidebar), the site's +sidebar lists every page in one "API Documentation" group. `pages.json` gives +you what you need to build your own: the name, kind, URL and category of each +reference page. + +```json displayName="docs/api/pages.json" +[ + { "name": "Kitten", "kind": "Interface", "url": "/api/interfaces/Kitten" }, + { "name": "adopt", "kind": "Function", "url": "/api/functions/adopt" } +] +``` + +TypeDoc categorizes the exports of each module and namespace separately: once +one of them has a `@category`, TypeDoc files the others under "Other" (its +`defaultCategory` option). When none has one, their pages have no `category`, +as above. + +Read the page list in your configuration, and turn it into sidebar groups. +This one makes a group per category, next to your guides: + +```mjs displayName="doc-kit.config.mjs" +import { readFileSync } from 'node:fs'; + +const pages = JSON.parse( + readFileSync(new URL('./docs/api/pages.json', import.meta.url), 'utf8') +); + +// A group per category, and "Other" for the pages without one +const categories = Object.groupBy(pages, ({ category }) => category ?? 'Other'); + +export default { + // ... + + html: { + navigation: { + sidebar: [ + { + groupName: 'Guides', + items: [{ label: 'Introduction', link: '/index' }], + }, + ...Object.entries(categories).map(([groupName, entries]) => ({ + groupName, + items: entries.map(({ name, url }) => ({ + label: `\`${name}\``, + link: url, + })), + })), + ], + }, + }, +}; +``` + +The configuration reads `pages.json` when it loads, so run TypeDoc first. +[Customizing the site](./customization.md#navigation) covers the rest of the +navigation. + +## JavaScript projects + +TypeDoc reads JavaScript too, taking the types from its JSDoc comments, once +TypeScript is allowed to: set `allowJs` (or `checkJs`) in your +`tsconfig.json`, and point `entryPoints` at your JavaScript, such as +`src/index.js`. + +```json displayName="tsconfig.json" +{ + "compilerOptions": { + "allowJs": true + }, + "include": ["src"] +} +``` + +The types then come from the JSDoc tags: + +```js displayName="src/index.js" +/** + * Adopts a kitten, once the paperwork is done. + * + * @param {string} name The kitten's name. + * @param {object} [options] How to adopt it. + * @param {string} [options.color] Coat color. + * @param {AbortSignal} [options.signal] Cancels the adoption. + * @returns {Promise<{ name: string }>} The adopted kitten. + * @experimental + */ +export async function adopt(name, options = {}) { + return { name }; +} +``` + +This gives the same page as the TypeScript version, apart from the return +type, which reads as written: `{Promise<{ name: string }>}`. A `@typedef` of +an object type gets a page of its own, as an interface would. + +## Going further + +The [`@doc-kit/typedoc` reference](./packages/typedoc.md#options) lists the +plugin's other options: `docKitMemberPages` gives each member of a large +options type a page of its own, `docKitReceivers` changes the name members +are documented on, and `docKitUrlAdapter` keeps the URLs of an earlier site +working. To shape the rest of the site, continue with +[Customizing the site](./customization.md). diff --git a/docs/writing-docs.md b/docs/writing-docs.md index c41974be4..c1c58227d 100644 --- a/docs/writing-docs.md +++ b/docs/writing-docs.md @@ -1,183 +1,447 @@ # Writing documentation -`doc-kit` reads GitHub Flavored Markdown, so everything you already know -applies. On top of it, a handful of conventions get special treatment. +doc-kit reads GitHub Flavored Markdown, plus a handful of conventions that +turn a page into an API reference: headings written as signatures, typed +parameter lists, version history and stability. This page covers what most +documents use. The [Markdown specification](./specification.md) is the full +contract. + +> [!TIP] +> Documenting a TypeScript or JavaScript project? +> [`@doc-kit/typedoc`](./packages/typedoc.md) writes this Markdown for you, +> from your source code and its JSDoc comments. See +> [Documenting TypeScript](./typescript.md). ## The smallest document -```markdown -# hello +```markdown displayName="docs/kittens.md" +# Kittens -A one-line description of the module. +The `kittens` module adopts, feeds and pets kittens. ``` -The level-one heading is the page's identity: it becomes the sidebar label -and the output filename. Every file needs exactly one, at the top. +Start every file with one level-one heading. Its text is the page's title: it +names the browser tab and, unless you configure the sidebar yourself, the +page's link in the sidebar. The website skips a file that has headings but no +level-one heading. The first paragraph is the page's description, which +[`llms.txt`](./generators/llms-txt.md) lists next to the link. + +The output file follows the source path, not the title: with `docs/**/*.md` as +input, `docs/kittens.md` becomes `kittens.html`. + +## What differs from GitHub Markdown + +Everything GitHub Markdown does still works. On top of it: + +- Text in curly braces is a [type annotation](#types-that-link-themselves): + `{string}` renders as code that links to the type's documentation. Put + literal braces in a code span. +- Headings written like signatures, such as `` `kittens.adopt(name)` ``, are + [API entries](#how-headings-are-classified). +- A list under an entry whose items start with a name and a type is a + [typed list](#parameters-return-values-and-defaults). +- HTML comments carry metadata: `` comments set + [page metadata](#page-metadata). +- A blockquote that starts with `Stability:` is a + [stability indicator](#stability). +- Two or more code blocks in a row become [tabs](#code-samples). +- Links to `.md` files [point to the generated pages](#linking-between-pages). -## Where this departs from plain Markdown +## Documenting an API -- `{curly braces}` in prose and lists are [type annotations](#types-that-link-themselves), - not literal text. -- Blockquotes beginning with `Stability:` are [stability indicators](#stability). -- HTML comments beginning with `YAML` (and `key=value` comments) are - [metadata](#recording-history), not ignored text. +Each heading below the title, down to `#####`, starts an _entry_, and deeper +headings start entries nested inside it. An entry has up to six parts, in this +order: a heading, its history, its stability, its typed list, prose, and code +samples. All but the heading come before the next heading, whatever its level. -Everything else is standard GFM. +![The parts of an API entry in doc-kit Markdown, and what each becomes](./diagrams/entry-anatomy.svg) -## Documenting an API +Every part after the heading is optional, and most entries use only a few: -Each heading below the title starts an _entry_. Write the heading the way -API docs are conventionally written, and `doc-kit` classifies it — no -annotation needed: +````markdown displayName="docs/kittens.md" +## `kittens.adopt(name[, options])` -```markdown -## Class: `http.Server` + + +- `name` {string} The kitten's name. +- `options` {Object} + - `color` {string} Coat color. **Default:** `'tabby'`. +- Returns: {Promise} Fulfills with the adopted kitten. -### `new Agent([options])` +Adopts a kitten, once the paperwork is done. -### `server.listen(port[, callback])` +```js +const kitten = await kittens.adopt('Mittens'); +``` +```` -### `server.maxHeadersCount` +The heading makes this a method and gives its signature, the comment records +when it was added, and the list becomes its parameters and return value. The +website renders a signature, a parameter table and a version history; the +[JSON output](./generators/json.md) keeps each part as data. + +## How headings are classified + +doc-kit reads an entry's kind from its heading, with no annotation needed: + +| Heading | Kind | +| -------------------------------------------- | ------------- | +| `` ## Class: `kittens.Kitten` `` | Class | +| `` ### `new kittens.Kitten(name)` `` | Constructor | +| `` ### Static method: `Kitten.from(json)` `` | Static method | +| `` ### `kitten.pet([times])` `` | Method | +| `` ### `kitten.name` `` | Property | +| `` ### Event: `'purr'` `` | Event | + +- Parentheses make a method, and their absence a property. A method needs only + a name, as in `` `adopt(name)` ``, but a property needs an owner: + `` `kitten.name` `` or `` `kitten[key]` ``. +- The `Class:`, `Static method:` and `Event:` prefixes mark those kinds, and + `new` marks a constructor. +- The code span must be the whole heading, after any prefix: + ``### `kitten.pet()` again`` is a plain section. +- Optional parameters go in square brackets, nested when one only makes sense + after another: `` `kitten.groom(brush[, side[, strokes]])` ``. A rest + parameter is written `[...names]`. Leave types and defaults out of the + heading; they belong in the typed list. +- Anything else, like `## Synopsis`, is a plain section. Write those headings + in plain words, without backticks. + +Entries nest by heading level, so put a class's constructor, methods, +properties and events one level below the class. + +## Parameters, return values and defaults + +Under the heading, and after any history or stability, a typed list describes +what the entry takes and returns. Each item is a name in a code span, a type +in curly braces, and a description: -### Static method: `Buffer.from(string[, encoding])` +```markdown +### `kitten.feed(food[, options])` -### Event: `'close'` +- `food` {string|string[]} What to serve. +- `options` {Object} + - `portion` {number} Grams per serving. **Default:** `50`. + - `signal` {AbortSignal} Cancels the meal. +- Returns: {Promise} Fulfills with `true` once the bowl is empty. ``` -Parentheses make a method; their absence makes a property; `Class:`, -`Static method:`, and `Event:` prefixes mark those kinds; `new X()` marks a -constructor. Optional parameters go in square brackets — `[, callback]` — -nesting where needed: `buf.write(string[, offset[, length]])`. +- Nested lists describe the properties of an object, to any depth. +- `**Default:**` followed by the value in a code span records a default and + makes the parameter optional. Square brackets in the heading make it + optional too. +- Three prefixes stand in for the name: `Returns:` for a return value, `Type:` + for a property's type, and `Extends:` for a class's parent class. -Headings that match none of these patterns (like `## Synopsis`) are ordinary -sections, which is exactly right for prose. Wrap identifiers in backticks; -leave prose headings unwrapped. +The same list works for the other kinds. An event's list describes the +arguments its listeners receive: -## Parameters and return values +```markdown +## Class: `kittens.Kitten` -Directly under an entry's heading, an unordered list describes its -parameters. Each item is a name, a type in braces, and prose: +- Extends: {EventTarget} -```markdown -### `fs.readFile(path[, options], callback)` +### `kitten.name` -- `path` {string|URL} The file to read. -- `options` {Object} - - `encoding` {string} **Default:** `'utf8'`. - - `signal` {AbortSignal} Allows aborting the read. -- `callback` {Function} -- Returns: {Promise} Fulfills with the file contents. +- Type: {string} + +### Event: `'purr'` + +- `volume` {number} How loud, from 1 to 10. ``` -- Nested lists document the properties of an object parameter, to any depth. -- `` **Default:** `value` `` marks a parameter optional and records its - default. -- `Returns:` describes the return value; `Extends:` names a class's - superclass; `Type:` gives a property's type. +doc-kit recognizes a typed list by its first item, which must start with a +name and a type, a bare type, or one of the prefixes. A list whose first item +has a name but no type stays a plain list, and plain bullets after the typed +items stay prose. -The list is lifted out of the prose and becomes structured data — rendered -as parameter tables in HTML, fields in JSON, and so on. +On the website, a typed list becomes a table, under the signature for methods +and constructors. In JSON, each item's name, type, description, default and +whether it's optional become fields. ## Types that link themselves -Anything in `{braces}` — in a list or mid-prose — is a TypeScript type -expression, rendered as code with each type name resolved to a link: +Curly braces hold a TypeScript type, in a typed list or anywhere in prose. +doc-kit renders it as code and links each type name in it to its +documentation: ```markdown -- `data` {string|Buffer} The payload. +- `food` {string|string[]} What to serve. + +`kittens.adopt()` returns a {Promise} that fulfills with a {kittens.Kitten}. +``` -On success, returns a {Promise} that fulfills with a {stream.Readable}. +Anything valid on the right of a TypeScript `type X =` works, including +unions, arrays and generics such as `{Promise}`. doc-kit looks each +name up in order, and the first match wins: + +1. Your [type map](#the-type-map). Its names are case-sensitive and may + contain spaces. +2. JavaScript primitives and keywords, such as `string`, `number`, `null` and + `any`, linked to MDN. +3. JavaScript's built-in objects and the Web APIs documented on MDN, such as + `Promise`, `Map`, `AbortSignal` and `URL`. +4. Dotted names, which link into your own docs: the part before the first dot + names the page. `{kittens.Kitten}` links to the `Class:` heading on the + `kittens` page (a capitalized last part means a class), and + `{kittens.breeds}` to the `kittens.breeds` property. + +Steps 2 and 3 ignore case. A name that nothing matches stays plain text inside +the code, which is fine. If the braces don't hold a valid type, doc-kit logs a +warning and renders the text as plain code. + +### The type map + +For your own types, and other projects' types, write a JSON file that maps +names to URLs: + +```json displayName="type-map.json" +{ + "Litter": "https://example.com/docs/litter", + "Adoption options": "kittens.html#kittensadoptname-options" +} ``` -Names resolve in tiers: your configured type map first, then JavaScript -built-ins, then Web APIs on MDN, and finally `module.Name` patterns link to -the matching page in your own docs. A name nothing recognizes stays plain -text — that's fine, not an error. +Then point `metadata.typeMap` at it, or pass it to the CLI's +[`--type-map`](./cli.md) option: + +```js displayName="doc-kit.config.mjs" +export default { + metadata: { + typeMap: './type-map.json', + }, +}; +``` + +`typeMap` takes a URL, or a path read from the directory you run doc-kit in. +A relative URL in the map is relative to the output root, and doc-kit adjusts +it for each page. ## Recording history -An HTML comment holding YAML, placed right after a heading, records an -entry's life story: +An HTML comment that starts with `YAML`, right after an entry's heading, +records the entry's history: ```markdown -### `hello.greet(name)` +### `kittens.adopt(name[, options])` ``` -`added`, `deprecated`, and `removed` take a version (or an array of versions, -for backports); `changes` is a chronological list of notable changes with -their pull requests. The HTML generator renders this as a version-history -widget; JSON consumers get it as data. +| Field | What it records | +| ------------ | ---------------------------------------------------------------------------------- | +| `added` | The version the entry first shipped in. | +| `deprecated` | The version that deprecated it. | +| `removed` | The version that removed it. | +| `changes` | Notable changes, each with a `version`, a `pr-url` and a `description` (Markdown). | + +A version is a string such as `v1.0.0`. Use a list of versions for a change +that was backported to older release lines. The website shows the history next +to the heading, and JSON keeps it as data. ## Stability -A blockquote in the exact form below marks an entry's lifecycle stage — -`0` (deprecated), `1` (experimental, with optional `1.0`/`1.1`/`1.2` -sub-levels), `2` (stable), or `3` (legacy): +A blockquote that starts with `Stability:`, a level and a label marks an +entry's stage in its lifecycle. The website renders it as a colored box, like +this one: + +> Stability: 2 - Stable. This box is a live stability indicator. + +Place it after the history comment, if there is one, and before the typed +list. The text after the level is yours: explain, link to an alternative, or +continue on more lines. ```markdown -### `hello.shout(name)` +### `kittens.adopt(name[, options])` + + + +> Stability: 1 - Experimental. The adoption fee may change. -> Stability: 1 - Experimental. May change without notice. +- `name` {string} The kitten's name. ``` -Place it after the YAML comment (if any) and before the parameter list. On -the title heading, it applies to the whole page. +| Level | Meaning | +| ----- | ------------------------------------------------------------------------- | +| 0 | Deprecated: may emit warnings, and compatibility isn't guaranteed. | +| 1 | Experimental: outside semantic versioning, and may change in any release. | +| 2 | Stable: compatibility is a priority. | +| 3 | Legacy: still covered by semantic versioning, but no longer maintained. | + +Level 1 takes sub-levels that say how close an API is to stable: `1.0` (early +development), `1.1` (active development) and `1.2` (release candidate). On the +title, a stability indicator applies to the whole page. ## Code samples -Use fenced code blocks with a language identifier. An optional `displayName` -labels the block in rendered output: +Fence code samples with a language, so the website can highlight them. Two or +more code blocks in a row become one block with a tab each. A tab is labelled +by its block's `displayName`, or by its language when it has none (`cjs` and +`mjs` read as CJS and ESM): ````markdown -```js displayName="Reading a file" -const data = await fs.readFile('/path/to/file'); +```bash displayName="npm" +npm install kittens +``` + +```bash displayName="pnpm" +pnpm add kittens ``` ```` +That renders as: + +```bash displayName="npm" +npm install kittens +``` + +```bash displayName="pnpm" +pnpm add kittens +``` + +To keep blocks apart, put a sentence between them. The JSON output lists each +entry's code samples, with their language and `displayName`. + +## Alerts + +GitHub's alert syntax renders as a callout, like the tip at the top of this +page: + +```markdown +> [!WARNING] +> Kittens adopted before eight weeks of age need extra care. +``` + +The five kinds are `NOTE`, `TIP`, `IMPORTANT`, `WARNING` and `CAUTION`. + ## Linking between pages -Link to other documents by their source filename — the `.md` extension is -rewritten to the right output extension at build time: +Link to other pages by their source file, with a relative path. doc-kit +rewrites the `.md` extension to the output's `.html`: + +```markdown +See [`kittens.adopt()`][] for the details, or the [care guide](care.md). + +[`kittens.adopt()`]: kittens.md#kittensadoptname-options +``` + +Anchors are heading slugs, made much as GitHub makes them: +`` ## `kittens.adopt(name[, options])` `` becomes `#kittensadoptname-options`, +and `` ## Class: `kittens.Kitten` `` becomes `#class-kittenskitten`. When in +doubt, copy the link from the heading on the rendered page. + +Reference-style links, with their definitions at the end of the file, keep +long pages readable. A system call written like `open(2)` in plain text (not +in a code span) links to its Linux manual page. + +## Page metadata + +One-line comments right after the title set metadata for the whole page: ```markdown -See [`hello.greet()`][] for the simple form. +# Kittens -[`hello.greet()`]: hello.md#hellogreetname + + + ``` -Reference-style links with the definitions collected at the bottom of the -file keep prose readable. References to system calls like `open(2)` link to -their man pages automatically. +| Directive | What it does | +| ----------------- | ---------------------------------------------------------------------------- | +| `introduced_in` | The version the module first shipped in, shown with the page's history. | +| `source_link` | Links the page to the file that implements it, in your repository on GitHub. | +| `llm_description` | Describes the page in `llms.txt`, in place of its first paragraph. | +| `type` | What the page documents: `module` (the default), `misc` or `global`. | +| `name` | The module's name in JSON output, when it isn't the file name. | + +A frontmatter block at the top of the file does the same job with YAML keys: +any of the directives above or the history fields, plus `title`, which +replaces the heading's text in the browser tab, and `mdx`, described in +[MDX pages](#mdx-pages). + +```markdown +--- +title: Adopting kittens +introduced_in: v1.0.0 +--- -## Pages that aren't API reference +# Kittens +``` -Guides and tutorials need nothing special, most of the time. Headings that -match none of the patterns above stay ordinary sections. +## Guides and API pages -The exception is a page whose headings _do_ look like signatures but aren't -module exports. A `type` directive under the title reclassifies the whole -document, so those entries render as conceptual sections instead: +Guides need nothing special: headings that don't look like an API stay plain +sections. Still, doc-kit treats every page as documenting a module unless its +`type` directive says otherwise: ```markdown -# C++ addons +# Caring for your kitten ``` -Frontmatter (`---`-delimited YAML) at the top of a file is also accepted and -treated as metadata. And if a page needs real components — heroes, tabs, live -examples — name it `.mdx` and use JSX directly; see +`misc` marks a guide or another conceptual page: JSON output records the +page's `type` as `misc`, with no module name. `global` marks a page of +globals, which need no import, and JSON gives its entries a global scope. + +The directive doesn't change how headings are classified: on a `misc` page, +`` ## `kitten.pet()` `` is still a method. When a heading looks like an API +but isn't an entry, write it in plain words, or override its kind in its YAML +comment so that it becomes a plain section: + +```markdown +### `kitten.whiskers` + + +``` + +## MDX pages + +When a page needs components, such as a landing page or a live demo, write it +in MDX: name the file `.mdx`, or set `mdx: true` in a Markdown file's +frontmatter (`mdx: false` turns an `.mdx` file back into Markdown). + +```mdx displayName="docs/index.mdx" +--- +title: Kittens +--- + +# Kittens + +Everything about adopting, feeding and petting kittens. + + +``` + +`` is built in: it lists every page with its stability +and description. Components you register yourself work too; see [Customizing the site](./customization.md#custom-components-and-mdx). -## The full contract +In MDX, curly braces are JavaScript expressions, not types, so typed lists +stay plain lists and type names don't link. HTML comments aren't valid MDX +either, so set page metadata in frontmatter, write comments as `{/* ... */}`, +and keep entries that need history in Markdown pages. Headings and stability +indicators work as they do in Markdown. + +## Learn more -This page covers what most documents use, but a full -[specification](./specification.md) is also available. +- The [Markdown specification](./specification.md) is the full contract for + the format, edge cases included. +- [Documenting TypeScript](./typescript.md) generates these pages from your + source code. +- [Customizing the site](./customization.md) covers navigation, the theme and + components. diff --git a/packages/cli/README.md b/packages/cli/README.md index 7c570b3b0..e10b240c5 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,45 +1,154 @@ # `@doc-kit/cli` -The command-line interface for [doc-kit](https://github.com/nodejs/doc-kit): -the `doc-kit` binary that runs the -[`@doc-kit/core`](https://www.npmjs.com/package/@doc-kit/core) engine to turn -API-shaped Markdown into documentation sites, JSON, man pages, and more. +The command-line interface of [doc-kit](https://doc-kit.nodejs.org), the +Node.js project's toolkit for API documentation. Its `doc-kit` command reads +your docs and runs generators that write a website, the search index, JSON, +`llms.txt`, man pages, or whatever a custom generator produces. Use it to +build your docs locally and in CI. -## Usage +## Install -```sh -npx @doc-kit/cli --help -npx @doc-kit/cli generate --help +```bash +npm install --save-dev @doc-kit/cli @doc-kit/generator-react ``` -You must provide an input and at least one target through command-line -options or a configuration file. Configuration is discovered automatically -using `cosmiconfig`, or you can select a file explicitly with -`--config-file`. +The CLI comes with [`@doc-kit/core`](https://doc-kit.nodejs.org/packages/core) +and its JSON generators. The other generators live in their own packages, +which you install next to the CLI. The command above adds the website +generators; leave `@doc-kit/generator-react` out if you only need JSON. -```sh -npx @doc-kit/cli generate \ - -t html \ - -i "path/to/docs/**/*.md" \ - -o out +| Package | Generators | +| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | +| [`@doc-kit/core`](https://doc-kit.nodejs.org/packages/core) | `json`, `json-all`, `json-simple` | +| [`@doc-kit/generator-react`](https://doc-kit.nodejs.org/packages/react) | `html`, `section-pages`, `orama-db`, `sitemap`, `llms-txt`, `llms-txt-full` | +| [`@node-core/doc-kit`](https://doc-kit.nodejs.org/packages/node) | `man-page`, `api-links`, `addon-verify` | +| [`@node-core/doc-kit-legacy`](https://doc-kit.nodejs.org/packages/node-legacy) | `legacy-html`, `legacy-html-all`, `legacy-json`, `legacy-json-all` | + +## Build your docs + +```bash +npx doc-kit generate -t html -t orama-db -i "docs/**/*.md" -o out ``` -Built-in generator names resolve to the [`@doc-kit/core` -generators](https://www.npmjs.com/package/@doc-kit/core) and its companion -generator packages, which must be installed alongside this one. Custom -generators load by import specifier — any module whose default export is a -generator works as a `--target`. +This builds the website and its search index from every Markdown file under +`docs/`, and writes them to `out/`. A run needs at least a target and an +input, from flags or a [configuration file](#configuration): + +- `-t, --target ` {string[]} The generators to run. Repeat the + flag, or list several names after it. +- `-i, --input ` {string[]} Glob patterns matching your source + files. Quote them, so your shell passes them on unexpanded. +- `-o, --output ` {string} The directory the generators write to. + +Run `npx doc-kit generate --help`, or see the +[CLI reference](https://doc-kit.nodejs.org/cli), for every option. + +## Preview the output + +```bash +npx doc-kit preview +``` + +This serves `./out` on as it is, without reading your +configuration file. Pass it any `generate` option and it builds first, then +serves the directory that build wrote to. If your build writes somewhere other +than `./out`, pass your configuration file (or `-o`) so it builds and serves +that directory: + +```bash +npx doc-kit preview --config-file doc-kit.config.mjs +``` + +It builds once and doesn't watch for changes, so run it again after editing. +Two options of its own control the server: + +- `--port ` {number} The port to serve on. **Default:** `3000`. +- `--host ` {string} The host to listen on. **Default:** `'localhost'`. + +> [!NOTE] +> The preview server is meant for checking your docs locally. To put them +> online, upload the output directory to a static host; see +> [Publishing your docs](https://doc-kit.nodejs.org/publishing). + +## Configuration + +Instead of passing flags on every run, put your options in a configuration +file: + +```mjs displayName="doc-kit.config.mjs" +export default { + target: ['html', 'orama-db'], + + global: { + input: ['docs/**/*.md'], + output: 'out', + }, +}; +``` + +With this file in place, `npx doc-kit generate` needs no flags. doc-kit looks +for it in the working directory, as `doc-kit.config.mjs` (or `.js`, `.cjs`), +a `.doc-kitrc` file in JSON, YAML or JavaScript, or a `doc-kit` key in +`package.json`. Pass `--config-file ` to load another file. A flag wins +over the file when both set the same option. + +The [configuration reference](https://doc-kit.nodejs.org/configuration) +covers every option, including each generator's own. + +## Custom generators + +`--target`, and `target` in the configuration file, also take a generator you +wrote or installed: a relative path such as `./my-generator.mjs` (resolved +from the working directory), an absolute path, or a package name. The +module's default export must have a `name` and a `generate` function. + +```bash +npx doc-kit generate -t ./my-generator.mjs -i "docs/**/*.md" -o out +``` + +[Creating generators](https://doc-kit.nodejs.org/creating-generators) shows +how to write one. + +## Log level + +`--log-level ` works with every command. It takes `debug`, `info`, +`warn`, `error` or `fatal`, and defaults to `info`: + +```bash +npx doc-kit --log-level debug generate +``` + +## Documentation + +- [Getting started](https://doc-kit.nodejs.org/getting-started): from an empty + folder to a site with search. +- [CLI](https://doc-kit.nodejs.org/cli): every command and option. +- [Configuration](https://doc-kit.nodejs.org/configuration): the + configuration file and how it merges with flags. +- [Generators](https://doc-kit.nodejs.org/generators): what each generator + writes. ## Contributing -This package lives in the [nodejs/doc-kit](https://github.com/nodejs/doc-kit) -monorepo. From this directory (or the repository root with -`npm run + + + + + + +
${dehydrated}
+ +