Add doc-weaver: TypeScript documentation compiler
TypeDoc + JSDoc (RDFC) compiler mirroring doc-forge. Emits a doc-forge-shaped MCP JSON bundle and a TypeDoc human site, with an MCP server (official SDK, stdio), a build/serve CLI, and React-aware kind classification. Includes the RDFC wiki (docs/wiki) and MkDocs config.
This commit is contained in:
24
CHANGELOG.md
Normal file
24
CHANGELOG.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `doc-weaver` are documented here.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Added
|
||||
|
||||
- TypeDoc-based loader (`runTypeDocJson`) producing serialized reflection JSON.
|
||||
- `TypeDocJsonToMCP` renderer emitting a doc-forge-compatible MCP bundle:
|
||||
`index.json`, `nav.json`, and per-module JSON under `modules/` with dotted
|
||||
module paths and `docs://` resource URIs.
|
||||
- `generateHumanDocs` renderer producing the TypeDoc HTML site (the MkDocs
|
||||
parallel).
|
||||
- `MCPServer` built on the official `@modelcontextprotocol/sdk`, exposing
|
||||
`docs://index`, `docs://nav`, and `docs://modules/{module}` plus a `ping`
|
||||
tool.
|
||||
- `doc-weaver build` and `doc-weaver serve` CLI commands.
|
||||
- React-aware kind classification (`component`, `hook`) and Google-style
|
||||
docstring rendering.
|
||||
- RDFC convention guide under `docs/wiki/02_rdfc.md`, plus a hand-written
|
||||
wiki (`docs/wiki/`) built with MkDocs into `site/wiki/`.
|
||||
- Self-documentation: `docs/mcp/` bundle and `site/lib/` TypeDoc site for
|
||||
`doc-weaver` itself.
|
||||
186
README.md
Normal file
186
README.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# doc-weaver
|
||||
|
||||
TypeScript documentation compiler that mirrors [`doc-forge`](../doc-forge).
|
||||
|
||||
`doc-weaver` reads **TypeScript + JSDoc** and emits documentation for both
|
||||
humans and machines:
|
||||
|
||||
- **Humans** – a [TypeDoc](https://typedoc.org) site (the parallel to
|
||||
doc-forge's MkDocs site).
|
||||
- **Machines** – an MCP JSON bundle (`index.json`, `nav.json`, `modules/`)
|
||||
whose shape is byte-compatible with the bundle `doc-forge` produces for
|
||||
Python, so the same consumers (agents, `docs://` clients, the standards
|
||||
registry) can query it unchanged.
|
||||
|
||||
Where `doc-forge` uses `mkdocs` + Google-style docstrings (`GSDFC`),
|
||||
`doc-weaver` uses `TypeDoc` + Google-style JSDoc (`RDFC`).
|
||||
|
||||
---
|
||||
|
||||
## Parity map
|
||||
|
||||
| Concern | Python (`doc-forge`) | TypeScript (`doc-weaver`) |
|
||||
| --- | --- | --- |
|
||||
| Source of truth | Python docstrings (GSDFC) | TypeScript types + JSDoc (RDFC) |
|
||||
| Front-end (loader) | Griffe static analysis | TypeDoc (TypeScript compiler API) |
|
||||
| Middle-end (model) | `Project -> Module -> DocObject` | `Project -> Module -> DocObject` |
|
||||
| Human renderer | MkDocs (`site/lib`) | TypeDoc (`site/lib`) |
|
||||
| Machine renderer | `MCPRenderer` JSON | `TypeDocJsonToMCP` JSON |
|
||||
| Convention | GSDFC (Google-style docstrings) | RDFC (Google-style JSDoc) |
|
||||
| CLI | `doc-forge build/serve` | `doc-weaver build/serve` |
|
||||
| MCP server | `docforge.servers.MCPServer` | `docweaver.servers.MCPServer` |
|
||||
|
||||
### MCP bundle (identical shape to doc-forge)
|
||||
|
||||
```
|
||||
docs/mcp/
|
||||
index.json { project, type: "docforge-model", modules_count, source }
|
||||
nav.json [ { module, resource: "docs://modules/<module>" } ]
|
||||
modules/<module>.json { module, content: { path, docstring, objects } }
|
||||
```
|
||||
|
||||
Each object is rendered recursively as:
|
||||
|
||||
```
|
||||
{ name, kind, path, signature, docstring, members? }
|
||||
```
|
||||
|
||||
Resource URIs use the same `docs://` scheme as doc-forge:
|
||||
|
||||
- `docs://index`
|
||||
- `docs://nav`
|
||||
- `docs://modules/{module}`
|
||||
|
||||
Module paths use **dotted** form (`components.ResourceList`) so a single URI
|
||||
segment matches, exactly like Python's dotted module paths.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
This produces the `doc-weaver` binary in `dist/` and `bin/`.
|
||||
|
||||
---
|
||||
|
||||
## CLI usage
|
||||
|
||||
```bash
|
||||
# Machine-readable MCP JSON
|
||||
doc-weaver build --mcp --module src/index.ts --project-name my-app
|
||||
|
||||
# Human-facing TypeDoc site (MkDocs parallel)
|
||||
doc-weaver build --docs --module src/index.ts --project-name my-app
|
||||
|
||||
# Both in one pass
|
||||
doc-weaver build --mcp --docs --module src/index.ts
|
||||
|
||||
# Serve a generated MCP bundle
|
||||
doc-weaver serve --mcp --module my-app --out-dir docs/mcp
|
||||
```
|
||||
|
||||
Flags:
|
||||
|
||||
| Flag | Meaning | Default |
|
||||
| --- | --- | --- |
|
||||
| `--mcp` | Emit MCP JSON bundle | – |
|
||||
| `--docs` / `--mkdocs` / `--lib` | Emit the TypeDoc human site | – |
|
||||
| `--module <path>` / `--entry <file>` | Entry point for TypeDoc | required |
|
||||
| `--project-name <name>` / `--name <name>` | Project name | `package.json` name |
|
||||
| `--tsconfig <file>` | tsconfig to use | auto-discovered |
|
||||
| `--out-dir <dir>` | MCP output directory | `docs/mcp` |
|
||||
| `--out <dir>` | Human docs output directory | `site/lib` |
|
||||
| `--plugin <name...>` | Extra TypeDoc plugins | – |
|
||||
| `--strict` | Fail on TypeScript errors instead of generating anyway | off |
|
||||
|
||||
By default doc-weaver **skips TypeScript error checking** (`skipErrorChecking`),
|
||||
so documentation is produced even when a project has unrelated type errors.
|
||||
Pass `--strict` to make errors fatal.
|
||||
|
||||
`--api` and `--wiki` are reserved for parity with doc-forge and are not yet
|
||||
implemented.
|
||||
|
||||
---
|
||||
|
||||
## Adding it to a React package
|
||||
|
||||
Add a script to the package's `package.json` (pointing at the package entry):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"scripts": {
|
||||
"docs:mcp": "doc-weaver build --mcp --module index.ts --out-dir docs/mcp",
|
||||
"docs:site": "doc-weaver build --docs --module index.ts --out site/lib",
|
||||
"docs:serve": "doc-weaver serve --mcp --module my-app --out-dir docs/mcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Packages in this workspace (`react-openapi`, `react-auth`, `vet`) export their
|
||||
public surface from `index.ts`, so a single `--module index.ts` documents the
|
||||
package. To document **per-file modules**, pass a directory or several entry
|
||||
points instead:
|
||||
|
||||
```bash
|
||||
doc-weaver build --mcp --module src
|
||||
doc-weaver build --mcp --module src/a.ts --module src/b.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
TypeScript + JSDoc
|
||||
|
|
||||
TypeDoc (loader) -> serialized reflection JSON
|
||||
|
|
||||
TypeDocJsonToMCP (renderer) -> MCP bundle
|
||||
generateHumanDocs (renderer)-> TypeDoc HTML site
|
||||
```
|
||||
|
||||
```
|
||||
src/
|
||||
cli/ commands, mcp-utils
|
||||
loaders/ typedoc-loader (static analysis)
|
||||
models/ semantic model + TypeDoc helpers
|
||||
renderers/ mcp-renderer, human-docs, docstring, signature
|
||||
servers/ mcp-server (official MCP SDK)
|
||||
templates/ typedoc.json baseline
|
||||
```
|
||||
|
||||
Like doc-forge, `doc-weaver` is renderer-agnostic: the MCP renderer and the
|
||||
TypeDoc renderer never depend on each other, and no user code is executed to
|
||||
produce documentation semantics.
|
||||
|
||||
---
|
||||
|
||||
## Convention (RDFC)
|
||||
|
||||
See [`docs/wiki/02_rdfc.md`](docs/wiki/02_rdfc.md) for the *React Doc-forge
|
||||
Convention* (RDFC) — Google-style JSDoc that keeps docstrings the single source
|
||||
of truth and makes them machine-parsable.
|
||||
|
||||
The hand-written docs live under `docs/wiki/` and are built with MkDocs into
|
||||
`site/wiki/` (mirroring doc-forge), while the API site is built by TypeDoc into
|
||||
`site/lib/`:
|
||||
|
||||
```bash
|
||||
# Wiki (MkDocs) -> site/wiki/
|
||||
mkdocs build -f docs/mkdocs.wiki.yml
|
||||
|
||||
# API site (TypeDoc) + MCP bundle for doc-weaver itself
|
||||
node bin/doc-weaver.js build --mcp --docs \
|
||||
--module src --project-name doc-weaver \
|
||||
--out-dir docs/mcp --out site/lib
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
3
bin/doc-weaver.js
Normal file
3
bin/doc-weaver.js
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
import { main } from '../dist/cli/index.js';
|
||||
main();
|
||||
63
docs/mkdocs.wiki.yml
Normal file
63
docs/mkdocs.wiki.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
scheme: slate
|
||||
primary: blue grey
|
||||
accent: teal
|
||||
font:
|
||||
text: Roboto
|
||||
code: JetBrains Mono
|
||||
features:
|
||||
- navigation.sections
|
||||
- navigation.expand
|
||||
- navigation.top
|
||||
- navigation.instant
|
||||
- navigation.tracking
|
||||
- navigation.indexes
|
||||
- content.code.copy
|
||||
- content.code.annotate
|
||||
- content.tabs.link
|
||||
- content.action.edit
|
||||
- search.highlight
|
||||
- search.share
|
||||
- search.suggest
|
||||
- navigation.tabs
|
||||
- toc.integrate
|
||||
- header.autohide
|
||||
- content.code.select
|
||||
- content.code.line_numbers
|
||||
- content.tooltips
|
||||
icon:
|
||||
logo: material/file-document-multiple
|
||||
repo: fontawesome/brands/github
|
||||
markdown_extensions:
|
||||
- pymdownx.superfences
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.highlight:
|
||||
linenums: true
|
||||
anchor_linenums: true
|
||||
line_spans: __span
|
||||
pygments_lang_class: true
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- tables
|
||||
- footnotes
|
||||
- pymdownx.caret
|
||||
- pymdownx.tilde
|
||||
- pymdownx.mark
|
||||
extra_css:
|
||||
- https://unpkg.com/dracula-prism/dist/css/dracula-prism.css
|
||||
plugins:
|
||||
- search
|
||||
site_name: doc-weaver
|
||||
docs_dir: wiki
|
||||
site_dir: ../site/wiki
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Overview: 01_overview.md
|
||||
- RDFC Guide: 02_rdfc.md
|
||||
60
docs/wiki/01_overview.md
Normal file
60
docs/wiki/01_overview.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Library Overview
|
||||
|
||||
`doc-weaver` turns RDFC-compliant TypeScript JSDoc into maintainable reference
|
||||
documentation. It never edits source comments; it reads them, renders them, and
|
||||
assembles documentation from all available material — exactly as `doc-forge`
|
||||
does for Python docstrings.
|
||||
|
||||
Analysis is static: source is parsed by the TypeScript compiler API via TypeDoc
|
||||
and is never executed.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Which Build Should You Use?
|
||||
|
||||
| Flag | Source | Output |
|
||||
|-----------------------|-----------------------|---------------------------------------------------|
|
||||
| `--docs` (`--mkdocs`, `--lib`) | JSDoc comments | `site/lib/**` TypeDoc HTML (the MkDocs parallel) |
|
||||
| `--mcp` | JSDoc comments + model | `docs/mcp/**` doc-forge-shaped JSON |
|
||||
| `--api` | OpenAPI spec | reserved |
|
||||
| `--wiki` | Hand-written markdown | `docs/wiki/**` (unchanged) |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
TypeScript + JSDoc
|
||||
|
|
||||
TypeDoc (loader) -> serialized reflection JSON
|
||||
|
|
||||
TypeDocJsonToMCP (renderer) -> MCP bundle (docs/mcp/)
|
||||
generateHumanDocs (renderer) -> TypeDoc HTML site (site/lib/)
|
||||
```
|
||||
|
||||
The MCP renderer and the human renderer never depend on each other: both read
|
||||
the same serialized reflections, so the machine bundle and the human site can
|
||||
never disagree about a symbol.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 MCP bundle shape
|
||||
|
||||
`docs/mcp/**` is a self-contained, machine-readable reference whose shape is
|
||||
byte-compatible with `doc-forge`:
|
||||
|
||||
```
|
||||
docs/mcp/
|
||||
index.json { project, type: "docforge-model", modules_count, source }
|
||||
nav.json [ { module, resource: "docs://modules/<module>" } ]
|
||||
modules/<module>.json { module, content: { path, docstring, objects } }
|
||||
```
|
||||
|
||||
`doc-weaver serve --mcp --module doc-weaver` serves it over stdio. See the
|
||||
[RDFC Guide](02_rdfc.md) for how to write the JSDoc that fills it.
|
||||
|
||||
---
|
||||
|
||||
## ➡️ Read Next
|
||||
|
||||
- [02 – RDFC Guide](02_rdfc.md)
|
||||
173
docs/wiki/02_rdfc.md
Normal file
173
docs/wiki/02_rdfc.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# RDFC Guide
|
||||
|
||||
RDFC — the **React Doc-forge Convention** — is the TypeScript counterpart to
|
||||
doc-forge's **GSDFC**. This page is the practical companion to the convention:
|
||||
how to write JSDoc so it renders correctly in the TypeDoc site **and** stays
|
||||
machine-parsable in the MCP bundle and by AI tooling.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Overview
|
||||
|
||||
- Documentation is the single source of truth and lives in JSDoc comments.
|
||||
- `doc-weaver` compiles comments; it never rewrites or invents them.
|
||||
- Structure follows the TypeScript export hierarchy.
|
||||
- Every public export should have a complete and accurate JSDoc comment.
|
||||
- Types live in signatures; do not duplicate them in prose unless a tag
|
||||
requires it.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Module (file) level
|
||||
|
||||
Document the file with a leading block comment. It becomes the module
|
||||
`docstring` in the MCP bundle.
|
||||
|
||||
```ts
|
||||
/**
|
||||
* # Resource list
|
||||
*
|
||||
* Renders, filters, and paginates a resource collection.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Notes:
|
||||
* - Reads configuration from the OpenAPI spec.
|
||||
* - Pagination is server-driven.
|
||||
*/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Components
|
||||
|
||||
Document the component function and its props. Components are classified
|
||||
automatically as `kind: "component"` (PascalCase name).
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Render a single resource row with actions.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @param props.item - The resource record to display.
|
||||
* @param props.onSelect - Invoked when the row is selected.
|
||||
* @returns The rendered row element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ResourceList item={record} onSelect={handleSelect} />
|
||||
* ```
|
||||
*/
|
||||
export function ResourceList(props: ListCellProps) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Hooks
|
||||
|
||||
Hooks are classified automatically as `kind: "hook"` (name starts with `use`).
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Subscribe to server-sent events for an item.
|
||||
*
|
||||
* @param url - SSE endpoint, or `null` to disable.
|
||||
* @param handlers - Event handlers keyed by event name.
|
||||
* @returns Connection state and the latest payload.
|
||||
*
|
||||
* @remarks
|
||||
* The subscription is torn down on unmount.
|
||||
*/
|
||||
export function useItemSse(url: string | null, handlers: ItemSseHandlers) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Types, interfaces, and members
|
||||
|
||||
Document the type and each member. Members become nested `members` entries in
|
||||
the MCP bundle.
|
||||
|
||||
```ts
|
||||
/**
|
||||
* A single display field extracted from an OpenAPI schema.
|
||||
*/
|
||||
export interface FieldConfig {
|
||||
/** Property name in the payload. */
|
||||
name: string;
|
||||
/** `x-edit-as: date` — edit a `date-time` field with a date input. */
|
||||
editAs?: string;
|
||||
/** Schema-level `default` used to seed create-form state. */
|
||||
defaultValue?: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Notes subsection grouping
|
||||
|
||||
Group related information using labeled subsections (Markdown bold):
|
||||
|
||||
```
|
||||
Notes:
|
||||
**Guarantees:**
|
||||
|
||||
- deterministic output
|
||||
|
||||
**Lifecycle:**
|
||||
|
||||
- created during initialization
|
||||
- reused across renders
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Heading map
|
||||
|
||||
`doc-weaver` renders JSDoc block tags into stable markdown headings so both
|
||||
TypeDoc and the MCP bundle agree:
|
||||
|
||||
| JSDoc tag | Rendered heading |
|
||||
|-----------|------------------|
|
||||
| `@param` | `**Parameters:**` |
|
||||
| `@returns` / `@return` | `**Returns:**` |
|
||||
| `@throws` | `**Throws:**` |
|
||||
| `@example` | `**Example:**` |
|
||||
| `@deprecated` | `**Deprecated:**` |
|
||||
| `@see` | `**See also:**` |
|
||||
| `@remarks` | `**Remarks:**` |
|
||||
| *other* | `**@tag:**` |
|
||||
|
||||
---
|
||||
|
||||
## 📦 Mapping to output kinds
|
||||
|
||||
| TS construct | `kind` |
|
||||
|--------------|--------|
|
||||
| interface / type alias | `interface` / `type-alias` |
|
||||
| function component (PascalCase) | `component` |
|
||||
| custom hook (`use*`) | `hook` |
|
||||
| plain function | `function` |
|
||||
| object / class member | `property` / `method` |
|
||||
| const / let / var | `variable` |
|
||||
|
||||
An explicit `@kind` tag overrides the inferred kind.
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Enforcement
|
||||
|
||||
Unlike doc-forge (which runs `pydoclint` in CI), `doc-weaver` performs no
|
||||
signature linting: TypeDoc extracts whatever JSDoc is present. Keep comments
|
||||
complete and accurate — they are the single source of truth for both the human
|
||||
site and the machine bundle.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [01 – Overview](01_overview.md)
|
||||
55
docs/wiki/index.md
Normal file
55
docs/wiki/index.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 🪡 doc-weaver — TypeScript Documentation Weaver
|
||||
|
||||
`doc-weaver` is the TypeScript counterpart to `doc-forge`. It compiles
|
||||
JSDoc-annotated TypeScript into a **TypeDoc** site (the MkDocs parallel) and a
|
||||
**doc-forge-shaped MCP JSON bundle**, so React packages document themselves for
|
||||
humans and agents alike.
|
||||
|
||||
> **Doc model:** this wiki is written for humans — guides and conventions. The
|
||||
> authoritative API contracts live in the code (JSDoc) and the machine-readable
|
||||
> bundle under `docs/mcp/`.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Key Features
|
||||
|
||||
- 🧵 JSDoc extraction via TypeDoc (the TypeScript compiler API)
|
||||
- 🧬 doc-forge-compatible MCP bundle (`index.json`, `nav.json`, `modules/*.json`)
|
||||
- ⚛️ React-aware kinds — `component` (PascalCase) and `hook` (`use*`)
|
||||
- 📚 RDFC (React Doc-forge Convention) — the JSDoc parallel to GSDFC
|
||||
- 🔌 MCP server over stdio exposing `docs://index`, `docs://nav`, and
|
||||
`docs://modules/{module}`
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
This produces the `doc-weaver` binary under `bin/` and `dist/`.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Documentation Structure
|
||||
|
||||
| Section | Description |
|
||||
|-----------------------------------------|------------------------------------------------|
|
||||
| [Overview](01_overview.md) | What doc-weaver is and how it fits |
|
||||
| [RDFC Guide](02_rdfc.md) | How to write RDFC-conformant JSDoc |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Resources
|
||||
|
||||
- **Source Code:** the `doc-weaver` repository
|
||||
- **MCP Bundle:** generated by `doc-weaver build --mcp --module src/index.ts`
|
||||
into `docs/mcp/`
|
||||
- **Library Registry:** registered in the standards hub as `doc-weaver`
|
||||
(MCP `:8010`)
|
||||
|
||||
---
|
||||
|
||||
© Aetoskia Internal — `doc-weaver` 0.1.0
|
||||
1903
package-lock.json
generated
Normal file
1903
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "doc-weaver",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "TypeScript documentation compiler using TypeDoc + JSDoc (Google-style), generating MkDocs and MCP JSON in doc-forge shape.",
|
||||
"bin": {
|
||||
"doc-weaver": "./bin/doc-weaver.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"prepare": "tsc -p tsconfig.json",
|
||||
"dev": "tsx src/cli/index.ts",
|
||||
"start": "node bin/doc-weaver.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.17.4",
|
||||
"commander": "^12.1.0",
|
||||
"fs-extra": "^11.3.1",
|
||||
"typedoc": "^0.27.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^20.19.11",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "~5.6.3"
|
||||
}
|
||||
}
|
||||
138
src/cli/commands.ts
Normal file
138
src/cli/commands.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Command } from 'commander';
|
||||
import * as path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { runTypeDocJson } from '../loaders/typedoc-loader.js';
|
||||
import { generateHumanDocs } from '../renderers/human-docs.js';
|
||||
import { generateMcpResources, serveMcp } from './mcp-utils.js';
|
||||
|
||||
/**
|
||||
* Build documentation.
|
||||
*
|
||||
* Mirrors doc-forge's `build` command:
|
||||
* doc-weaver build --mcp --docs --module <entry>
|
||||
*/
|
||||
export function buildCommand(): Command {
|
||||
return new Command('build')
|
||||
.description('Build documentation (MCP JSON and/or a TypeDoc human site)')
|
||||
.option('--mcp', 'Generate MCP JSON resources (index/nav/modules)')
|
||||
.option('--docs', 'Generate the human-facing TypeDoc site')
|
||||
.option('--mkdocs', 'Alias for --docs (MkDocs parallel)')
|
||||
.option('--lib', 'Alias for --docs (library site)')
|
||||
.option('--api', 'API docs from an OpenAPI spec (reserved)')
|
||||
.option('--wiki', 'Hand-written wiki build (reserved)')
|
||||
.option('--module <path>', 'Entry point (file or directory) for TypeDoc')
|
||||
.option('--entry <file>', 'Entry file (alternative to --module)')
|
||||
.option('--project-name <name>', 'Project name override')
|
||||
.option('--name <name>', 'Site name (alias of --project-name)')
|
||||
.option('--tsconfig <file>', 'tsconfig.json path')
|
||||
.option('--out-dir <dir>', 'MCP output directory', 'docs/mcp')
|
||||
.option('--out <dir>', 'Human docs output directory', 'site/lib')
|
||||
.option('--plugin <name...>', 'TypeDoc plugins to enable')
|
||||
.option('--strict', 'Fail on TypeScript errors instead of generating anyway')
|
||||
.action(async (opts) => {
|
||||
const entry = opts.module || opts.entry;
|
||||
if (!entry) {
|
||||
throw new Error('Must specify --module or --entry');
|
||||
}
|
||||
|
||||
const entryAbs = path.resolve(entry);
|
||||
const projectName =
|
||||
opts.projectName ||
|
||||
opts.name ||
|
||||
(await inferProjectName(entryAbs));
|
||||
|
||||
const wantMcp = opts.mcp;
|
||||
const wantDocs = opts.docs || opts.mkdocs || opts.lib;
|
||||
const wantApi = opts.api;
|
||||
const wantWiki = opts.wiki;
|
||||
|
||||
if (!wantMcp && !wantDocs && !wantApi && !wantWiki) {
|
||||
throw new Error('Must specify at least one of --mcp, --docs, --api, --wiki');
|
||||
}
|
||||
if (wantApi || wantWiki) {
|
||||
throw new Error('--api and --wiki are reserved and not yet implemented');
|
||||
}
|
||||
|
||||
const tsconfig = resolveTsconfig(entryAbs, opts.tsconfig);
|
||||
// Directory entries expand to every file; a single file uses resolve.
|
||||
const entryPointStrategy = fs.statSync(entryAbs).isDirectory() ? 'expand' : 'resolve';
|
||||
|
||||
if (wantMcp) {
|
||||
const json = await runTypeDocJson({
|
||||
entryPoints: [entryAbs],
|
||||
tsconfig,
|
||||
name: projectName,
|
||||
skipErrorChecking: !opts.strict,
|
||||
entryPointStrategy
|
||||
});
|
||||
const outDir = path.resolve(opts.outDir);
|
||||
await generateMcpResources(json, projectName, outDir);
|
||||
console.log(`MCP resources written to ${outDir}`);
|
||||
}
|
||||
|
||||
if (wantDocs) {
|
||||
const out = await generateHumanDocs({
|
||||
entryPoints: [entryAbs],
|
||||
tsconfig,
|
||||
name: projectName,
|
||||
out: path.resolve(opts.out),
|
||||
plugin: opts.plugin,
|
||||
skipErrorChecking: !opts.strict,
|
||||
entryPointStrategy
|
||||
});
|
||||
console.log(`Human docs written to ${out}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Serve documentation locally. */
|
||||
export function serveCommand(): Command {
|
||||
return new Command('serve')
|
||||
.description('Serve documentation locally')
|
||||
.option('--mcp', 'Serve the generated MCP bundle')
|
||||
.option('--module <name>', 'Module name used to label the MCP server')
|
||||
.option('--out-dir <dir>', 'MCP root directory', 'docs/mcp')
|
||||
.action(async (opts) => {
|
||||
if (!opts.mcp) {
|
||||
throw new Error('Must specify --mcp');
|
||||
}
|
||||
if (!opts.module) {
|
||||
throw new Error('--module is required for MCP serve');
|
||||
}
|
||||
await serveMcp(opts.module, path.resolve(opts.outDir));
|
||||
});
|
||||
}
|
||||
|
||||
function resolveTsconfig(entryAbs: string, explicit?: string): string | undefined {
|
||||
if (explicit) {
|
||||
return fs.existsSync(explicit) ? path.resolve(explicit) : undefined;
|
||||
}
|
||||
let dir = path.dirname(entryAbs);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const candidate = path.join(dir, 'tsconfig.json');
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function inferProjectName(entryAbs: string): Promise<string> {
|
||||
let dir = path.dirname(entryAbs);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const pkg = path.join(dir, 'package.json');
|
||||
if (await fs.pathExists(pkg)) {
|
||||
try {
|
||||
const data = await fs.readJSON(pkg);
|
||||
if (data?.name) return data.name;
|
||||
} catch {
|
||||
// ignore malformed package.json
|
||||
}
|
||||
}
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return path.basename(path.dirname(entryAbs));
|
||||
}
|
||||
24
src/cli/index.ts
Normal file
24
src/cli/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Command } from 'commander';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { buildCommand, serveCommand } from './commands.js';
|
||||
|
||||
export function main(): void {
|
||||
const program = new Command();
|
||||
program
|
||||
.name('doc-weaver')
|
||||
.description('TypeScript documentation compiler: MkDocs + MCP (doc-forge compatible)')
|
||||
.version('0.1.0');
|
||||
|
||||
program.addCommand(buildCommand());
|
||||
program.addCommand(serveCommand());
|
||||
|
||||
program.parseAsync(process.argv).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Allow `tsx src/cli/index.ts` and `node dist/cli/index.js` to run directly.
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main();
|
||||
}
|
||||
41
src/cli/mcp-utils.ts
Normal file
41
src/cli/mcp-utils.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { TypeDocJsonToMCP } from '../renderers/mcp-renderer.js';
|
||||
import { MCPServer } from '../servers/mcp-server.js';
|
||||
import * as path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
/** Generate the doc-forge-compatible MCP bundle from TypeDoc reflection JSON. */
|
||||
export async function generateMcpResources(
|
||||
typedocJson: any,
|
||||
projectName: string | undefined,
|
||||
outDir: string
|
||||
): Promise<void> {
|
||||
const transformer = new TypeDocJsonToMCP(typedocJson, projectName);
|
||||
const { index, nav, modules } = transformer.transform();
|
||||
|
||||
await fs.ensureDir(path.join(outDir, 'modules'));
|
||||
await fs.writeJSON(path.join(outDir, 'index.json'), index, { spaces: 2 });
|
||||
await fs.writeJSON(path.join(outDir, 'nav.json'), nav, { spaces: 2 });
|
||||
|
||||
for (const [modPath, modData] of Object.entries(modules)) {
|
||||
const outPath = path.join(outDir, 'modules', modPath + '.json');
|
||||
await fs.ensureDir(path.dirname(outPath));
|
||||
await fs.writeJSON(outPath, modData, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
/** Serve a pre-generated MCP bundle over stdio/HTTP. */
|
||||
export async function serveMcp(module: string, mcpRoot: string): Promise<void> {
|
||||
const root = path.resolve(mcpRoot);
|
||||
const required = [
|
||||
path.join(root, 'index.json'),
|
||||
path.join(root, 'nav.json'),
|
||||
path.join(root, 'modules')
|
||||
];
|
||||
for (const r of required) {
|
||||
if (!(await fs.pathExists(r))) {
|
||||
throw new Error('Invalid MCP bundle, missing: ' + path.basename(r));
|
||||
}
|
||||
}
|
||||
const server = new MCPServer(root, module + '-mcp');
|
||||
await server.run();
|
||||
}
|
||||
29
src/index.ts
Normal file
29
src/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* # doc-weaver
|
||||
*
|
||||
* Public library surface for doc-weaver, the TypeScript counterpart to
|
||||
* `doc-forge`. It compiles TypeDoc reflection (built from TypeScript + JSDoc)
|
||||
* into two renderers:
|
||||
*
|
||||
* - a human-facing TypeDoc site (the MkDocs parallel), and
|
||||
* - a machine-facing MCP JSON bundle (`index.json`, `nav.json`, `modules/`)
|
||||
* whose shape is byte-compatible with `doc-forge`.
|
||||
*
|
||||
* Programmatic usage:
|
||||
*
|
||||
* import { runTypeDocJson, TypeDocJsonToMCP } from 'doc-weaver';
|
||||
* const json = await runTypeDocJson({ entryPoints: ['src/index.ts'] });
|
||||
* const bundle = new TypeDocJsonToMCP(json, 'my-app').transform();
|
||||
*/
|
||||
|
||||
export { main } from './cli/index.js';
|
||||
export { runTypeDocJson, defaultTsconfigFor } from './loaders/typedoc-loader.js';
|
||||
export type { TypeDocOptions } from './loaders/typedoc-loader.js';
|
||||
export { generateHumanDocs } from './renderers/human-docs.js';
|
||||
export type { HumanDocsOptions } from './renderers/human-docs.js';
|
||||
export { TypeDocJsonToMCP } from './renderers/mcp-renderer.js';
|
||||
export type { McpBundle } from './renderers/mcp-renderer.js';
|
||||
export { generateMcpResources, serveMcp } from './cli/mcp-utils.js';
|
||||
export { MCPServer } from './servers/mcp-server.js';
|
||||
export { collectModules, resolveModulePath, RefKind } from './models/typedoc.js';
|
||||
export type { DocObject, DocModule, DocProject, DocKind } from './models/index.js';
|
||||
61
src/loaders/typedoc-loader.ts
Normal file
61
src/loaders/typedoc-loader.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Application, TSConfigReader } from 'typedoc';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface TypeDocOptions {
|
||||
entryPoints: string[];
|
||||
tsconfig?: string;
|
||||
name?: string;
|
||||
/** Optional TypeDoc theme/plugin passthroughs */
|
||||
plugin?: string[];
|
||||
/**
|
||||
* Skip TypeScript error checking before conversion. Defaults to true so
|
||||
* documentation still generates when a project has unrelated type errors.
|
||||
*/
|
||||
skipErrorChecking?: boolean;
|
||||
/** TypeDoc entry-point strategy. Defaults to 'resolve'. */
|
||||
entryPointStrategy?: 'resolve' | 'expand' | 'merge' | 'packages';
|
||||
}
|
||||
|
||||
/**
|
||||
* Run TypeDoc programmatically and return the serialized reflection JSON.
|
||||
*
|
||||
* Mirrors doc-forge's static loader: no user code is executed for
|
||||
* documentation semantics; we rely on TypeScript's compiler API.
|
||||
*/
|
||||
export async function runTypeDocJson(opts: TypeDocOptions): Promise<any> {
|
||||
const basePath = opts.tsconfig ? path.dirname(opts.tsconfig) : opts.entryPoints[0];
|
||||
const app = await Application.bootstrapWithPlugins(
|
||||
{
|
||||
entryPoints: opts.entryPoints,
|
||||
tsconfig: opts.tsconfig,
|
||||
name: opts.name,
|
||||
plugin: opts.plugin,
|
||||
basePath,
|
||||
// Keep things deterministic and quiet
|
||||
readme: 'none',
|
||||
entryPointStrategy: opts.entryPointStrategy ?? 'resolve',
|
||||
excludePrivate: false,
|
||||
excludeInternal: false,
|
||||
skipErrorChecking: opts.skipErrorChecking ?? true
|
||||
},
|
||||
[new TSConfigReader()]
|
||||
);
|
||||
|
||||
const project = await app.convert();
|
||||
if (!project) {
|
||||
throw new Error('TypeDoc failed to produce a project reflection');
|
||||
}
|
||||
|
||||
// TypeDoc >= 0.26 exposes serializer.projectToObject(root, projectRoot).
|
||||
const serializer: any = (app as any).serializer;
|
||||
if (serializer && typeof serializer.projectToObject === 'function') {
|
||||
return serializer.projectToObject(project, basePath);
|
||||
}
|
||||
|
||||
throw new Error('Unsupported TypeDoc version: serializer.projectToObject is unavailable');
|
||||
}
|
||||
|
||||
/** Convenience: resolve tsconfig relative to a given project root. */
|
||||
export function defaultTsconfigFor(root: string): string {
|
||||
return path.join(root, 'tsconfig.json');
|
||||
}
|
||||
52
src/models/index.ts
Normal file
52
src/models/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Semantic documentation model for doc-weaver.
|
||||
*
|
||||
* This mirrors doc-forge's renderer-agnostic model: a project is a
|
||||
* collection of modules, each module contains named documentation objects,
|
||||
* and objects may contain nested members. Renderers (MkDocs, MCP) consume
|
||||
* this model so that output formats never depend on each other.
|
||||
*/
|
||||
|
||||
/** The set of documentable symbol kinds recognized by doc-weaver. */
|
||||
export type DocKind =
|
||||
| 'module'
|
||||
| 'namespace'
|
||||
| 'class'
|
||||
| 'interface'
|
||||
| 'enum'
|
||||
| 'enum-member'
|
||||
| 'type-alias'
|
||||
| 'variable'
|
||||
| 'function'
|
||||
| 'method'
|
||||
| 'accessor'
|
||||
| 'property'
|
||||
| 'constructor'
|
||||
| 'signature'
|
||||
| 'parameter'
|
||||
| 'component'
|
||||
| 'hook'
|
||||
| 'unknown';
|
||||
|
||||
/** A documentation object with an optional tree of members. */
|
||||
export interface DocObject {
|
||||
name: string;
|
||||
kind: DocKind;
|
||||
path: string;
|
||||
signature: string;
|
||||
docstring: string;
|
||||
members?: Record<string, DocObject>;
|
||||
}
|
||||
|
||||
/** A single module (typically one source file or package). */
|
||||
export interface DocModule {
|
||||
path: string;
|
||||
docstring: string;
|
||||
objects: Record<string, DocObject>;
|
||||
}
|
||||
|
||||
/** The root semantic model produced by the loader. */
|
||||
export interface DocProject {
|
||||
name: string;
|
||||
modules: DocModule[];
|
||||
}
|
||||
66
src/models/typedoc.ts
Normal file
66
src/models/typedoc.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ReflectionKind } from 'typedoc';
|
||||
|
||||
/** Re-export TypeDoc's numeric reflection kinds for internal use. */
|
||||
export const RefKind = ReflectionKind;
|
||||
|
||||
/**
|
||||
* Recursively collect every Module-kind reflection in a TypeDoc project.
|
||||
*
|
||||
* TypeDoc emits Module reflections for each resolved entry point (one per
|
||||
* source file/package). When a project has no module children (a single flat
|
||||
* entry point), the project itself is treated as a single module so that the
|
||||
* output always has at least one module - matching doc-forge's model.
|
||||
*/
|
||||
export function collectModules(project: any): any[] {
|
||||
const modules: any[] = [];
|
||||
const walk = (node: any) => {
|
||||
if (!node) return;
|
||||
if (node.kind === RefKind.Module) {
|
||||
modules.push(node);
|
||||
return;
|
||||
}
|
||||
for (const child of node.children || []) walk(child);
|
||||
};
|
||||
walk(project);
|
||||
|
||||
if (modules.length > 0) return modules;
|
||||
return [project];
|
||||
}
|
||||
|
||||
/** Derive a stable, filesystem-safe module path from a TypeDoc reflection. */
|
||||
export function resolveModulePath(mod: any, project: any): string {
|
||||
const source = mod.sources?.[0]?.fileName;
|
||||
if (typeof source === 'string' && source.length) {
|
||||
return normalizeSourcePath(source);
|
||||
}
|
||||
if (mod.kind === RefKind.Project) {
|
||||
return sanitize(mod.name || project?.name || 'project');
|
||||
}
|
||||
return sanitize(mod.name || 'module');
|
||||
}
|
||||
|
||||
function normalizeSourcePath(fileName: string): string {
|
||||
let p = fileName.replace(/\\/g, '/');
|
||||
p = p.replace(/^\.\//, '');
|
||||
p = p.replace(/^src\//, '');
|
||||
p = p.replace(/\.(tsx?|jsx?|mts|cts|mjs|cjs)$/, '');
|
||||
p = p.replace(/\/index$/, '');
|
||||
// Use dotted paths so MCP resource templates match a single URI segment,
|
||||
// mirroring doc-forge's dotted Python module paths.
|
||||
p = p.replace(/\//g, '.');
|
||||
return sanitize(p);
|
||||
}
|
||||
|
||||
/** Replace characters that are unsafe in a resource path / filename. */
|
||||
export function sanitize(value: string): string {
|
||||
return value
|
||||
.replace(/\\/g, '.')
|
||||
.replace(/[^A-Za-z0-9_.-]/g, '_')
|
||||
.replace(/^\.+/, '')
|
||||
.replace(/\.+$/, '');
|
||||
}
|
||||
|
||||
/** Human-readable name for a reflective kind (used in diagnostics). */
|
||||
export function reflectionKindName(kind: number): string {
|
||||
return ReflectionKind[kind] ?? String(kind);
|
||||
}
|
||||
115
src/renderers/docstring.ts
Normal file
115
src/renderers/docstring.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { ReflectionKind } from 'typedoc';
|
||||
|
||||
/**
|
||||
* Convert a TypeDoc serialized comment into a Google-style markdown docstring.
|
||||
*
|
||||
* TypeDoc stores comments as a list of display parts (text/code/inline-tag)
|
||||
* plus blockTags (e.g. @example, @returns, @param). This function renders
|
||||
* them back to a stable markdown string so both MkDocs and MCP consumers
|
||||
* receive the same human-readable documentation.
|
||||
*/
|
||||
export function renderComment(comment: any): string {
|
||||
if (!comment) return '';
|
||||
const lines: string[] = [];
|
||||
|
||||
const summary = renderParts(comment.summary);
|
||||
if (summary.trim()) lines.push(summary.trim());
|
||||
|
||||
for (const tag of comment.blockTags || []) {
|
||||
const tagName = (tag.tag || '').replace(/^@/, '');
|
||||
const content = renderParts(tag.content).trim();
|
||||
if (!content) continue;
|
||||
const heading = tagHeading(tagName);
|
||||
lines.push('');
|
||||
lines.push(heading);
|
||||
lines.push('');
|
||||
lines.push(content);
|
||||
}
|
||||
|
||||
return lines.join('\n').trim();
|
||||
}
|
||||
|
||||
function tagHeading(tag: string): string {
|
||||
switch (tag) {
|
||||
case 'param':
|
||||
return '**Parameters:**';
|
||||
case 'returns':
|
||||
case 'return':
|
||||
return '**Returns:**';
|
||||
case 'example':
|
||||
return '**Example:**';
|
||||
case 'throws':
|
||||
return '**Throws:**';
|
||||
case 'deprecated':
|
||||
return '**Deprecated:**';
|
||||
case 'see':
|
||||
return '**See also:**';
|
||||
case 'remarks':
|
||||
return '**Remarks:**';
|
||||
default:
|
||||
return '**@' + tag + ':**';
|
||||
}
|
||||
}
|
||||
|
||||
/** Render a list of TypeDoc comment display parts into markdown. */
|
||||
export function renderParts(parts: any[] | undefined): string {
|
||||
if (!parts || !Array.isArray(parts)) return '';
|
||||
const out: string[] = [];
|
||||
for (const part of parts) {
|
||||
if (typeof part === 'string') {
|
||||
out.push(part);
|
||||
continue;
|
||||
}
|
||||
if (!part) continue;
|
||||
out.push(part.text ?? '');
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
/** Map TypeDoc numerical ReflectionKind to doc-weaver DocKind. */
|
||||
export function mapKind(kind: number): string {
|
||||
switch (kind) {
|
||||
case ReflectionKind.Module:
|
||||
return 'module';
|
||||
case ReflectionKind.Namespace:
|
||||
return 'namespace';
|
||||
case ReflectionKind.Class:
|
||||
return 'class';
|
||||
case ReflectionKind.Interface:
|
||||
return 'interface';
|
||||
case ReflectionKind.Enum:
|
||||
return 'enum';
|
||||
case ReflectionKind.EnumMember:
|
||||
return 'enum-member';
|
||||
case ReflectionKind.TypeAlias:
|
||||
return 'type-alias';
|
||||
case ReflectionKind.Variable:
|
||||
return 'variable';
|
||||
case ReflectionKind.Function:
|
||||
return 'function';
|
||||
case ReflectionKind.Method:
|
||||
return 'method';
|
||||
case ReflectionKind.Accessor:
|
||||
return 'accessor';
|
||||
case ReflectionKind.Property:
|
||||
return 'property';
|
||||
case ReflectionKind.Constructor:
|
||||
return 'constructor';
|
||||
case ReflectionKind.CallSignature:
|
||||
case ReflectionKind.ConstructorSignature:
|
||||
return 'signature';
|
||||
case ReflectionKind.Parameter:
|
||||
return 'parameter';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/** Classify a symbol as a React component or hook where possible. */
|
||||
export function classifyReact(name: string, kind: string): string {
|
||||
if (kind === 'function' || kind === 'variable') {
|
||||
if (/^use[A-Z0-9]/.test(name)) return 'hook';
|
||||
if (/^[A-Z]/.test(name)) return 'component';
|
||||
}
|
||||
return kind;
|
||||
}
|
||||
50
src/renderers/human-docs.ts
Normal file
50
src/renderers/human-docs.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Application, TSConfigReader } from 'typedoc';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Generate the human-facing documentation site using TypeDoc.
|
||||
*
|
||||
* TypeDoc plays the same role for doc-weaver that MkDocs plays for
|
||||
* doc-forge: it renders the compiled model into a browsable site. The
|
||||
* machine-facing output (MCP JSON) is produced separately.
|
||||
*/
|
||||
export interface HumanDocsOptions {
|
||||
entryPoints: string[];
|
||||
tsconfig?: string;
|
||||
name?: string;
|
||||
out: string;
|
||||
/** Optional path to a typedoc.json config. */
|
||||
options?: string;
|
||||
/** Optional TypeDoc plugins (e.g. typedoc-plugin-markdown). */
|
||||
plugin?: string[];
|
||||
/** Skip TypeScript error checking. Defaults to true. */
|
||||
skipErrorChecking?: boolean;
|
||||
/** TypeDoc entry-point strategy. Defaults to 'resolve'. */
|
||||
entryPointStrategy?: 'resolve' | 'expand' | 'merge' | 'packages';
|
||||
}
|
||||
|
||||
export async function generateHumanDocs(opts: HumanDocsOptions): Promise<string> {
|
||||
const basePath = opts.tsconfig ? path.dirname(opts.tsconfig) : opts.entryPoints[0];
|
||||
const app = await Application.bootstrapWithPlugins(
|
||||
{
|
||||
entryPoints: opts.entryPoints,
|
||||
tsconfig: opts.tsconfig,
|
||||
name: opts.name,
|
||||
out: opts.out,
|
||||
options: opts.options,
|
||||
plugin: opts.plugin,
|
||||
basePath,
|
||||
readme: 'none',
|
||||
entryPointStrategy: opts.entryPointStrategy ?? 'resolve',
|
||||
skipErrorChecking: opts.skipErrorChecking ?? true
|
||||
},
|
||||
[new TSConfigReader()]
|
||||
);
|
||||
|
||||
const project = await app.convert();
|
||||
if (!project) {
|
||||
throw new Error('TypeDoc failed to convert the project');
|
||||
}
|
||||
await app.generateDocs(project, opts.out);
|
||||
return path.resolve(opts.out);
|
||||
}
|
||||
113
src/renderers/mcp-renderer.ts
Normal file
113
src/renderers/mcp-renderer.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
RefKind,
|
||||
reflectionKindName,
|
||||
resolveModulePath,
|
||||
collectModules
|
||||
} from '../models/typedoc.js';
|
||||
import { renderComment, mapKind, classifyReact } from './docstring.js';
|
||||
import { bestSignature } from './signature.js';
|
||||
|
||||
export interface McpBundle {
|
||||
index: Record<string, unknown>;
|
||||
nav: Array<{ module: string; resource: string }>;
|
||||
modules: Record<string, { module: string; content: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a TypeDoc serialized reflection JSON into a doc-forge-compatible
|
||||
* MCP bundle.
|
||||
*
|
||||
* The produced shape matches doc-forge exactly:
|
||||
*
|
||||
* - `index.json` -> `{ project, type, modules_count, source }`
|
||||
* - `nav.json` -> `[ { module, resource } ]` with `docs://modules/{module}`
|
||||
* - `modules/*.json` -> `{ module, content: { path, docstring, objects } }`
|
||||
*
|
||||
* Objects are rendered recursively with
|
||||
* `{ name, kind, path, signature, docstring, members? }`.
|
||||
*/
|
||||
export class TypeDocJsonToMCP {
|
||||
constructor(
|
||||
private readonly project: any,
|
||||
private readonly projectName?: string
|
||||
) {}
|
||||
|
||||
transform(): McpBundle {
|
||||
const name = this.projectName || this.project?.name || 'project';
|
||||
const moduleRefs = collectModules(this.project);
|
||||
|
||||
const nav: McpBundle['nav'] = [];
|
||||
const modules: McpBundle['modules'] = {};
|
||||
|
||||
for (const mod of moduleRefs) {
|
||||
const modPath = resolveModulePath(mod, this.project);
|
||||
nav.push({ module: modPath, resource: `docs://modules/${modPath}` });
|
||||
modules[modPath] = {
|
||||
module: modPath,
|
||||
content: this.renderModule(mod, modPath)
|
||||
};
|
||||
}
|
||||
|
||||
const index = {
|
||||
project: name,
|
||||
type: 'docforge-model',
|
||||
modules_count: nav.length,
|
||||
source: 'doc-weaver'
|
||||
};
|
||||
|
||||
return { index, nav, modules };
|
||||
}
|
||||
|
||||
private renderModule(mod: any, modPath: string): Record<string, unknown> {
|
||||
const objects: Record<string, unknown> = {};
|
||||
for (const child of mod.children || []) {
|
||||
if (child.kind === RefKind.Module) continue;
|
||||
objects[child.name] = this.renderObject(child, modPath);
|
||||
}
|
||||
return {
|
||||
path: modPath,
|
||||
docstring: renderComment(mod.comment),
|
||||
objects
|
||||
};
|
||||
}
|
||||
|
||||
private renderObject(ref: any, parentPath: string): Record<string, unknown> {
|
||||
const kindName = classifyReact(ref.name, mapKind(ref.kind));
|
||||
const path = `${parentPath}.${ref.name}`;
|
||||
|
||||
const data: Record<string, unknown> = {
|
||||
name: ref.name,
|
||||
kind: kindName,
|
||||
path,
|
||||
signature: bestSignature(ref),
|
||||
docstring: renderComment(ref.comment)
|
||||
};
|
||||
|
||||
// Prefer an explicit @kind tag when present (mirrors doc-forge kinds).
|
||||
const kindTag = findBlockTag(ref.comment, '@kind');
|
||||
if (kindTag) data.kind = kindTag;
|
||||
|
||||
const members = this.collectMembers(ref, path);
|
||||
if (Object.keys(members).length) data.members = members;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private collectMembers(ref: any, parentPath: string): Record<string, unknown> {
|
||||
const members: Record<string, unknown> = {};
|
||||
for (const child of ref.children || []) {
|
||||
members[child.name] = this.renderObject(child, parentPath);
|
||||
}
|
||||
// Enum members and similar leaf children are already handled by children.
|
||||
return members;
|
||||
}
|
||||
}
|
||||
|
||||
function findBlockTag(comment: any, tag: string): string | undefined {
|
||||
for (const bt of comment?.blockTags || []) {
|
||||
if (bt.tag === tag) {
|
||||
return (bt.content || []).map((p: any) => p.text ?? '').join('').trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
90
src/renderers/signature.ts
Normal file
90
src/renderers/signature.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/** Utilities for reconstructing human-readable signatures from TypeDoc JSON. */
|
||||
|
||||
/** Render a TypeDoc type object to a compact TypeScript-like string. */
|
||||
export function renderType(type: any): string {
|
||||
if (!type) return 'unknown';
|
||||
if (typeof type === 'string') return type;
|
||||
|
||||
switch (type.type) {
|
||||
case 'intrinsic':
|
||||
return type.name ?? 'unknown';
|
||||
case 'reference': {
|
||||
const name = type.name ?? 'unknown';
|
||||
const args = (type.typeArguments || []).map(renderType);
|
||||
const qualified =
|
||||
type.qualifiedName && type.qualifiedName !== name ? type.qualifiedName : name;
|
||||
return args.length ? `${qualified}<${args.join(', ')}>` : qualified;
|
||||
}
|
||||
case 'array':
|
||||
return `${renderType(type.elementType)}[]`;
|
||||
case 'union':
|
||||
return (type.types || []).map(renderType).join(' | ');
|
||||
case 'intersection':
|
||||
return (type.types || []).map(renderType).join(' & ');
|
||||
case 'tuple':
|
||||
return `[${(type.elements || []).map(renderType).join(', ')}]`;
|
||||
case 'literal':
|
||||
return type.value === null ? 'null' : JSON.stringify(type.value);
|
||||
case 'reflection':
|
||||
if (type.declaration?.signatures?.length) {
|
||||
const sig = type.declaration.signatures[0];
|
||||
return sig.type ? renderType(sig.type) : 'function';
|
||||
}
|
||||
if (type.declaration?.children?.length) {
|
||||
const members = type.declaration.children
|
||||
.map((c: any) => `${c.name}: ${renderType(c.type)}`)
|
||||
.join('; ');
|
||||
return `{ ${members} }`;
|
||||
}
|
||||
return 'object';
|
||||
case 'typeParameter':
|
||||
return type.name ?? 'T';
|
||||
case 'predicate':
|
||||
return type.name ?? 'boolean';
|
||||
case 'query':
|
||||
return `typeof ${renderType(type.queryType)}`;
|
||||
case 'rest':
|
||||
return `...${renderType(type.elementType)}`;
|
||||
case 'namedTupleMember':
|
||||
return `${type.name}: ${renderType(type.elementType)}`;
|
||||
case 'optional':
|
||||
return `${renderType(type.elementType)}?`;
|
||||
default:
|
||||
return type.name ?? 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/** Render a function/call signature to a TypeScript-like string. */
|
||||
export function renderSignature(signature: any, fallbackName?: string): string {
|
||||
if (!signature) return fallbackName ?? '';
|
||||
const name = signature.name ?? fallbackName ?? '';
|
||||
const params = (signature.parameters || [])
|
||||
.map((p: any) => {
|
||||
const optional = p.flags?.isOptional ? '?' : '';
|
||||
return `${p.name}${optional}: ${renderType(p.type)}`;
|
||||
})
|
||||
.join(', ');
|
||||
const returns = signature.type ? `: ${renderType(signature.type)}` : '';
|
||||
return `${name}(${params})${returns}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the best signature string for a reflection.
|
||||
*
|
||||
* Functions/methods/constructors keep their declared signatures; variables
|
||||
* and properties expose their type; classes fall back to their name.
|
||||
*/
|
||||
export function bestSignature(ref: any): string {
|
||||
if (ref.signatures?.length) {
|
||||
return ref.signatures.map((s: any) => renderSignature(s, ref.name)).join(' | ');
|
||||
}
|
||||
if (ref.getSignature || ref.setSignature) {
|
||||
const get = ref.getSignature ? renderSignature(ref.getSignature, ref.name) : '';
|
||||
const set = ref.setSignature ? `set ${renderSignature(ref.setSignature, ref.name)}` : '';
|
||||
return [get, set].filter(Boolean).join(' | ');
|
||||
}
|
||||
if (ref.type) {
|
||||
return `${ref.name}: ${renderType(ref.type)}`;
|
||||
}
|
||||
return ref.name ?? '';
|
||||
}
|
||||
82
src/servers/mcp-server.ts
Normal file
82
src/servers/mcp-server.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import * as path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
/**
|
||||
* MCP server serving a pre-generated doc-weaver bundle.
|
||||
*
|
||||
* Mirrors `docforge.servers.MCPServer`: it exposes read-only documentation
|
||||
* resources and a single health-check tool.
|
||||
*
|
||||
* Resources:
|
||||
* - `docs://index` - project metadata
|
||||
* - `docs://nav` - navigation structure
|
||||
* - `docs://modules/{module}` - individual module documentation
|
||||
*/
|
||||
export class MCPServer {
|
||||
private readonly server: McpServer;
|
||||
|
||||
constructor(
|
||||
private readonly mcpRoot: string,
|
||||
name: string
|
||||
) {
|
||||
this.server = new McpServer({ name, version: '0.1.0' });
|
||||
this.registerResources();
|
||||
this.registerTools();
|
||||
}
|
||||
|
||||
private readJson(
|
||||
relPath: string,
|
||||
uri: string
|
||||
): { contents: Array<{ uri: string; mimeType?: string; text: string }> } {
|
||||
const full = path.join(this.mcpRoot, relPath);
|
||||
let text: string;
|
||||
if (fs.existsSync(full)) {
|
||||
text = fs.readFileSync(full, 'utf-8');
|
||||
} else {
|
||||
text = JSON.stringify({ error: 'not_found', path: full }, null, 2);
|
||||
}
|
||||
return { contents: [{ uri, mimeType: 'application/json', text }] };
|
||||
}
|
||||
|
||||
private registerResources(): void {
|
||||
this.server.registerResource(
|
||||
'index',
|
||||
'docs://index',
|
||||
{ title: 'Index', mimeType: 'application/json' },
|
||||
async (uri) => this.readJson('index.json', uri.href)
|
||||
);
|
||||
|
||||
this.server.registerResource(
|
||||
'nav',
|
||||
'docs://nav',
|
||||
{ title: 'Navigation', mimeType: 'application/json' },
|
||||
async (uri) => this.readJson('nav.json', uri.href)
|
||||
);
|
||||
|
||||
const template = new ResourceTemplate('docs://modules/{module}', { list: undefined });
|
||||
this.server.registerResource(
|
||||
'module',
|
||||
template,
|
||||
{ title: 'Module', mimeType: 'application/json' },
|
||||
async (uri, variables) => {
|
||||
const module = String(variables.module);
|
||||
return this.readJson(path.join('modules', module + '.json'), uri.href);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private registerTools(): void {
|
||||
this.server.registerTool(
|
||||
'ping',
|
||||
{ description: 'Health check that returns "pong".' },
|
||||
async () => ({ content: [{ type: 'text', text: 'pong' }] })
|
||||
);
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
}
|
||||
}
|
||||
31
templates/typedoc.json
Normal file
31
templates/typedoc.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPointStrategy": "resolve",
|
||||
"entryPoints": ["index.ts"],
|
||||
"readme": "none",
|
||||
"excludePrivate": false,
|
||||
"excludeInternal": false,
|
||||
"categorizeByGroup": true,
|
||||
"navigation": {
|
||||
"includeCategories": true,
|
||||
"includeGroups": true
|
||||
},
|
||||
"plugin": [],
|
||||
"commentStyle": "jsdoc",
|
||||
"blockTags": [
|
||||
"@defaultValue",
|
||||
"@deprecated",
|
||||
"@example",
|
||||
"@param",
|
||||
"@remarks",
|
||||
"@returns",
|
||||
"@see",
|
||||
"@throws"
|
||||
],
|
||||
"inlineTags": ["@link", "@linkcode", "@linkplain"],
|
||||
"validation": {
|
||||
"notExported": false,
|
||||
"invalidLink": true,
|
||||
"notDocumented": false
|
||||
}
|
||||
}
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "templates"]
|
||||
}
|
||||
Reference in New Issue
Block a user