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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ Options:

Commands:
generate [options] Generate API docs
watch [options] Generate API docs, rebuilding whenever an input file
changes
help [command] display help for command
```

Expand All @@ -78,8 +80,8 @@ Options:
(json-simple, legacy-html, legacy-html-all,
man-page, legacy-json, legacy-json-all,
addon-verify, api-links, orama-db, llms-txt,
sitemap, web) or an import specifier for a custom
generator
sitemap, html) or an import specifier for a
custom generator
--ignore <patterns...> Ignore file patterns (glob)
-o, --output <directory> The output directory
-p, --threads <number> Number of threads to use (minimum: 1)
Expand All @@ -91,9 +93,29 @@ Options:
--index <url> index.md URL or path
--minify Minify?
--type-map <url> Type map URL or path
--no-cache Disable the on-disk build cache entirely
--force Ignore existing cache entries (the cache is still
written)
--cache-dir <path> Build cache directory
-h, --help display help for command
```

### `watch`

`watch` takes the same options as `generate`. It builds once, then rebuilds
whenever a file matching `--input` changes, until you interrupt it.

```sh
npx doc-kit watch \
--input "doc/api/*.md" \
--target json-simple \
--output out
```

Rebuilds go through the same [build cache](docs/caching.md) as `generate`, so a
rebuild only redoes the work the change actually affected. A document that
fails to parse is reported without ending the session.

## Examples

### Legacy
Expand Down
92 changes: 92 additions & 0 deletions docs/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Caching and Incremental Builds

doc-kit keeps a durable on-disk cache between runs so that rebuilds only redo
the work a change actually affects. Caching is **on by default** and designed
to be invisible: every cache failure — corruption, version mismatch, deleted
files — silently degrades to a full, correct rebuild. If you ever get a wrong
output out of a cached build, that is a bug; please report it rather than
scripting around it.

## What you get

- **No-change rebuilds are skipped entirely.** When the input files,
configuration, and generator code are unchanged and every previously
written output still verifies on disk, the run finishes in well under a
second without loading any generator.
- **Partial rebuilds.** After editing one document, `legacy-html` re-renders
(and re-highlights, and re-minifies) only that document; the react `html`
generator rebuilds only the edited page's JSX and server-rendered HTML.
Pages whose bytes did not change are not rewritten, so their mtimes are
stable for downstream tooling.
- **Honest floors.** Markdown parsing and metadata extraction always re-run
(they are as fast as reading the cache would be), the synthetic `all` page
is rebuilt whenever anything changed (it folds every module by design), and
the client Vite build always runs over the full graph (partial input sets
change chunk hashing). For fast dev loops on large corpora, disable the all
page:

```js
export default {
'jsx-ast': { generateAllPage: false },
};
```

## How invalidation works

Cache keys are content hashes — never timestamps. Every key is salted with
the resolved configuration (including the parsed changelog, index, and the
fetched `typeMap` bytes) and a cache schema version.

Anything the cache cannot fully account for (for example a theme `imports`
alias pointing at a directory) makes the affected entries uncacheable rather
than possibly stale.

Generator _code_ is identified by the cache schema version alone: doc-kit
bumps it in releases whose generated output changes, so releases that don't
change output keep existing caches valid. When developing doc-kit or a custom
generator locally, code edits do not invalidate the cache on their own —
build with `--force` while iterating.

## Configuration

```js
export default {
cache: {
enabled: true, // default
dir: 'node_modules/.cache/doc-kit', // default (falls back to .doc-kit-cache)
maxAgeDays: 7, // age-based object pruning
},
};
```

CLI flags:

| Flag | Effect |
| -------------------- | --------------------------------------------- |
| `--no-cache` | Disable reads and writes for this run |
| `--force` | Ignore existing entries; still write new ones |
| `--cache-dir <path>` | Override the cache directory |

## CI usage

The cache is relocatable: keys contain no absolute paths, and outputs are
never inputs. Restoring the cache directory (for example with
`actions/cache`, keyed however you like — the cache self-invalidates by
content) turns unchanged-doc CI builds into sub-second no-ops:

```yaml
- uses: actions/cache@v4
with:
path: node_modules/.cache/doc-kit
key: doc-kit-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
```

The output directory can always be deleted independently of the cache; a warm
run regenerates it byte-for-byte.

## Guarantees

Cold builds are byte-identical across runs and across threading/chunking
topologies; cached builds are byte-identical to `--no-cache` builds; a wiped
output directory or a corrupted cache silently recovers; and one-file edits
rebuild exactly the affected outputs.
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 2 additions & 59 deletions packages/core/bin/commands/generate.mjs
Original file line number Diff line number Diff line change
@@ -1,71 +1,14 @@
import { Command, Option } from 'commander';

import { publicGenerators } from '../../src/generators/index.mjs';
import createGenerator from '../../src/generators.mjs';
import {
assertRunnableOptions,
setConfig,
} from '../../src/utils/configuration/index.mjs';
import { errorWrap } from '../utils.mjs';
import { createCommonCommand, errorWrap } from '../utils.mjs';

const { runGenerators } = createGenerator();

/**
* @typedef {Object} CLIOptions
* @property {string} configFile
* @property {string[]} input
* @property {string[]} target
* @property {string[]} ignore
* @property {string} output
* @property {number} threads
* @property {number} chunkSize
* @property {string} version
* @property {string} changelog
* @property {string} gitRef
* @property {string} index
* @property {boolean} minify
* @property {string} typeMap
*/

export default new Command('generate')
export default createCommonCommand('generate')
.description('Generate API docs')
.addOption(new Option('--config-file <path>', 'Config file'))

// Options that need to be converted into a configuration
.addOption(
new Option('-i, --input <patterns...>', 'Input file patterns (glob)')
)
.addOption(
new Option(
'-t, --target <generator...>',
'Target generator(s): a built-in name ' +
`(${Object.keys(publicGenerators).join(', ')}) ` +
'or an import specifier for a custom generator'
)
)
.addOption(
new Option('--ignore <patterns...>', 'Ignore file patterns (glob)')
)
.addOption(new Option('-o, --output <directory>', 'The output directory'))
.addOption(
new Option(
'-p, --threads <number>',
'Number of threads to use (minimum: 1)'
)
)
.addOption(
new Option(
'--chunk-size <number>',
'Number of items to process per worker thread (minimum: 1)'
)
)
.addOption(new Option('-v, --version <semver>', 'Target Node.js version'))
.addOption(new Option('-c, --changelog <url>', 'Changelog URL or path'))
.addOption(new Option('--git-ref <ref>', 'Git ref'))
.addOption(new Option('--index <url>', 'index.md URL or path'))
.addOption(new Option('--minify', 'Minify?'))
.addOption(new Option('--type-map <url>', 'Type map URL or path'))

.action(
errorWrap(async opts => {
const config = await setConfig(opts);
Expand Down
3 changes: 2 additions & 1 deletion packages/core/bin/commands/index.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import generate from './generate.mjs';
import watch from './watch.mjs';

export default [generate];
export default [generate, watch];
Loading
Loading