This guide explains how to create new documentation generators for @doc-kit/core.
Generators in doc-kit transform API documentation through a pipeline. Each generator:
- Takes input from a previous generator or raw files
- Processes the data into a different format
- Yields output for the next generator or final output
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.
A generator is defined as a module exporting an object conforming to the GeneratorMetadata interface.
Create a new directory in your project:
/ ├── 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
Create a types.d.ts file containing a Generator export. Use this when typing your generator.
export type Generator = GeneratorMetadata< { // If your generator supports a custom configuration, // define it here myCustomOption: string; }, Generate<InputToMyGenerator, Promise<OutputOfMyGenerator>>, // If your generator supports parallel processing: ProcessChunk< InputToMyParallelProcessor, OutputOfMyParallelProcessor, DependenciesOfMyParallelProcessor > >;
A generator module's default export is a plain object with its metadata and
implementation. Create it in index.mjs:
import { generate } from './generate.mjs'; /** * Generates output in MyFormat. * * @type {import('./types').Generator} */ export default { name: 'my-format', 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. dependsOn: '@doc-kit/core/metadata', defaultConfiguration: { // If your generator supports a custom configuration, define the defaults here myCustomOption: 'myDefaultValue', // All generators support options in the GlobalConfiguration object // To override the defaults, they can be specified here ref: 'overriddenRef', }, generate, };
Create the generator implementation in 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<MetadataEntry>} 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'); }
Generators are loaded dynamically by import specifier. Anything that resolves
to a module whose default export is a generator works as a --target:
# 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 ...
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:
export const publicGenerators = { 'json-simple': '@doc-kit/core/json-simple', 'my-format': '@doc-kit/core/my-format', // Add this // ... other generators };
If the generator lives in this repository, also add a matching subpath to the
exports map of its package's package.json.
For generators processing large datasets, implement parallel processing using worker threads.
First, define the generator metadata in index.mjs:
import { generate, processChunk } from './generate.mjs'; /** * @type {import('./types').Generator} */ export default { name: 'parallel-generator', description: 'Processes data in parallel', dependsOn: '@doc-kit/core/metadata', // Indicates this generator has a processChunk implementation hasParallelProcessor: true, generate, processChunk, };
Then, implement both processChunk and generate in 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); } return results; } /** * 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; } }
processChunkexecutes in worker threads - No access to main thread state- Only serializable data can be passed to workers (no functions, classes, etc.)
fullInputanditemIndices- Workers receive full input but only process specified indicesdepsmust be serializable - Pass only JSON-compatible data
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
Generators can yield results as they're produced using async generators.
Define the generator metadata in index.mjs:
import { generate, processChunk } from './generate.mjs'; /** * @type {import('./types').Generator} */ export default { name: 'streaming-generator', description: 'Streams results as they are ready', dependsOn: '@doc-kit/core/metadata', hasParallelProcessor: true, generate, processChunk, };
Implement the generator in 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; } }
- 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
Some generators must collect all input before processing.
Generator metadata in index.mjs:
import { generate } from './generate.mjs'; /** * @type {import('./types').Generator} */ export default { name: 'batch-generator', description: 'Requires all input at once', dependsOn: '@doc-kit/generator-react/jsx-ast', generate, };
Implementation in 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); // Process everything together const result = processBatch(allData); return result; }
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
In index.mjs:
import { generate } from './generate.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. dependsOn: '@doc-kit/core/metadata', // ... other metadata generate, };
In generate.mjs:
export async function generate(input, worker) { // input contains the output from 'metadata' generator }
In generate.mjs:
import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import getConfig from '../../utils/configuration/index.mjs'; export async function generate(input, worker) { const config = getConfig('my-format'); if (!config.output) { // Return data without writing return result; } // Ensure directory exists await mkdir(config.output, { recursive: true }); // 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' ); } return result; }
import { cp } from 'node:fs/promises'; import { join } from 'node:path'; import getConfig from '../../utils/configuration/index.mjs'; export async function generate(input, worker) { const config = getConfig('my-format'); if (config.output) { // Copy asset directory await cp( new URL('./assets', import.meta.url), join(config.output, 'assets'), { recursive: true } ); } return result; }