Add doc-weaver 0.1.0: TypeScript documentation compiler
Some checks failed
continuous-integration/drone/tag Build is failing
Some checks failed
continuous-integration/drone/tag Build is failing
TypeDoc + JSDoc (RDFC) compiler producing a TypeDoc site and a
doc-forge-shaped MCP bundle for React/TypeScript packages.
- TypeDoc loader + human-docs and MCP renderers, signature-level
parameter docs and default-export surfacing
- MCPServer over @modelcontextprotocol/sdk (docs://index, docs://nav,
docs://modules/{module}, ping tool)
- doc-weaver build / serve CLI
- RDFC convention guide and hand-written wiki
- packaged as @aetoskia/doc-weaver with an npm release pipeline
(build-and-publish-npm + backfill-npm-from-tags)
This commit is contained in:
156
.drone.yml
Normal file
156
.drone.yml
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build-and-publish-npm
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
workspace:
|
||||||
|
path: /drone/src
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: check-version
|
||||||
|
image: node:22-alpine
|
||||||
|
environment:
|
||||||
|
NPM_REPO_URL:
|
||||||
|
from_secret: NPM_REPO_URL
|
||||||
|
NPM_USERNAME:
|
||||||
|
from_secret: NPM_USERNAME
|
||||||
|
NPM_PASSWORD:
|
||||||
|
from_secret: NPM_PASSWORD
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||||
|
- VERSION=$(node -p "require('./package.json').version")
|
||||||
|
- |
|
||||||
|
if [ "$DRONE_TAG" != "$VERSION" ]; then
|
||||||
|
echo "Tag '$DRONE_TAG' does not match package.json version '$VERSION'; refusing to publish."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||||
|
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||||
|
- echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
|
||||||
|
- |
|
||||||
|
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
|
||||||
|
echo "$PACKAGE_NAME@$VERSION already exists; skipping build."
|
||||||
|
exit 78
|
||||||
|
else
|
||||||
|
echo "New version detected: $PACKAGE_NAME@$VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: quality-gate
|
||||||
|
image: node:22-alpine
|
||||||
|
environment:
|
||||||
|
NPM_REPO_URL:
|
||||||
|
from_secret: NPM_REPO_URL
|
||||||
|
NPM_USERNAME:
|
||||||
|
from_secret: NPM_USERNAME
|
||||||
|
NPM_PASSWORD:
|
||||||
|
from_secret: NPM_PASSWORD
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
if [ -z "$NPM_REPO_URL" ] || [ -z "$NPM_USERNAME" ] || [ -z "$NPM_PASSWORD" ]; then
|
||||||
|
echo "NPM_REPO_URL / NPM_USERNAME / NPM_PASSWORD are not set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- echo "registry=$NPM_REPO_URL" > .npmrc
|
||||||
|
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||||
|
- npm install
|
||||||
|
- echo "Running quality gate..."
|
||||||
|
- npm run quality-gate
|
||||||
|
|
||||||
|
- name: build-package
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- npm install
|
||||||
|
- echo "Building npm package..."
|
||||||
|
- npm run build
|
||||||
|
- ls -l dist
|
||||||
|
|
||||||
|
- name: publish-to-private-npm
|
||||||
|
image: node:22-alpine
|
||||||
|
environment:
|
||||||
|
NPM_REPO_URL:
|
||||||
|
from_secret: NPM_REPO_URL
|
||||||
|
NPM_USERNAME:
|
||||||
|
from_secret: NPM_USERNAME
|
||||||
|
NPM_PASSWORD:
|
||||||
|
from_secret: NPM_PASSWORD
|
||||||
|
commands:
|
||||||
|
- echo "registry=$NPM_REPO_URL" > .npmrc
|
||||||
|
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
|
||||||
|
- echo "Publishing to private npm at $NPM_REPO_URL ..."
|
||||||
|
- npm publish --access restricted
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: backfill-npm-from-tags
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
workspace:
|
||||||
|
path: /drone/src
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: fetch-tags
|
||||||
|
image: alpine/git
|
||||||
|
commands:
|
||||||
|
- git fetch --tags --force
|
||||||
|
|
||||||
|
- name: build-and-publish-missing
|
||||||
|
image: node:22-alpine
|
||||||
|
environment:
|
||||||
|
NPM_REPO_URL:
|
||||||
|
from_secret: NPM_REPO_URL
|
||||||
|
NPM_USERNAME:
|
||||||
|
from_secret: NPM_USERNAME
|
||||||
|
NPM_PASSWORD:
|
||||||
|
from_secret: NPM_PASSWORD
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache git
|
||||||
|
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
|
||||||
|
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||||
|
echo "Package: $PACKAGE_NAME"
|
||||||
|
|
||||||
|
for TAG in $(git tag --sort=version:refname); do
|
||||||
|
VERSION="$TAG"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
|
||||||
|
echo "Exists, skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout --force "$TAG"
|
||||||
|
|
||||||
|
TAG_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
if [ "$TAG" != "$TAG_VERSION" ]; then
|
||||||
|
echo "Tag $TAG != package.json version $TAG_VERSION at that commit; skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building $VERSION"
|
||||||
|
rm -rf dist
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
echo "Publishing $VERSION"
|
||||||
|
npm publish --access restricted --userconfig /tmp/.npmrc
|
||||||
|
done
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- custom
|
||||||
35
CHANGELOG.md
Normal file
35
CHANGELOG.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-10-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Initial npm release as `@aetoskia/doc-weaver` (npm.aetoskia.com).
|
||||||
|
- 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.
|
||||||
|
- Signature-level parameter rendering and default-export surfacing in MCP
|
||||||
|
bundles.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
[Unreleased]: https://git.aetoskia.com/aetos/doc-weaver/compare/0.1.0...HEAD
|
||||||
|
[0.1.0]: https://git.aetoskia.com/aetos/doc-weaver/releases/tag/0.1.0
|
||||||
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
39
package.json
Normal file
39
package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@aetoskia/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"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin",
|
||||||
|
"dist",
|
||||||
|
"README.md",
|
||||||
|
"CHANGELOG.md"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"quality-gate": "tsc -p tsconfig.json --noEmit",
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
|
"prepare": "tsc -p tsconfig.json",
|
||||||
|
"dev": "tsx src/cli/index.ts",
|
||||||
|
"start": "node bin/doc-weaver.js"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://npm.aetoskia.com"
|
||||||
|
},
|
||||||
|
"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);
|
||||||
|
}
|
||||||
156
src/renderers/docstring.ts
Normal file
156
src/renderers/docstring.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { ReflectionKind } from 'typedoc';
|
||||||
|
|
||||||
|
/** A single documented parameter (name + rendered description). */
|
||||||
|
export interface ParamDoc {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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). It moves `@param`/`@typeParam`
|
||||||
|
* descriptions onto the signature's parameters, so those are passed in
|
||||||
|
* separately as `params`/`typeParams` and rendered as grouped sections.
|
||||||
|
*
|
||||||
|
* The result is a stable markdown string so both MkDocs and MCP consumers
|
||||||
|
* receive the same human-readable documentation.
|
||||||
|
*/
|
||||||
|
export function renderComment(
|
||||||
|
comment: any,
|
||||||
|
params?: ParamDoc[],
|
||||||
|
typeParams?: ParamDoc[]
|
||||||
|
): string {
|
||||||
|
const documentedParams = (params || []).filter((p) => (p.description || '').trim());
|
||||||
|
const documentedTypeParams = (typeParams || []).filter((p) => (p.description || '').trim());
|
||||||
|
if (!comment && !documentedParams.length && !documentedTypeParams.length) return '';
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
const summary = renderParts(comment?.summary);
|
||||||
|
if (summary.trim()) lines.push(summary.trim());
|
||||||
|
|
||||||
|
if (documentedTypeParams.length) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('**Type Parameters:**');
|
||||||
|
lines.push('');
|
||||||
|
for (const p of documentedTypeParams) {
|
||||||
|
lines.push(`- \`${p.name}\` — ${oneLine(p.description)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (documentedParams.length) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('**Parameters:**');
|
||||||
|
lines.push('');
|
||||||
|
for (const p of documentedParams) {
|
||||||
|
lines.push(`- \`${p.name}\` — ${oneLine(p.description)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of comment?.blockTags || []) {
|
||||||
|
const tagName = (tag.tag || '').replace(/^@/, '');
|
||||||
|
// @param / @typeParam are rendered from signature parameters above.
|
||||||
|
if (tagName === 'param' || tagName === 'typeParam' || tagName === 'template') continue;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapse a possibly multi-line description to a single line. */
|
||||||
|
function oneLine(text: string): string {
|
||||||
|
return (text || '').trim().replace(/\s*\n\s*/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
178
src/renderers/mcp-renderer.ts
Normal file
178
src/renderers/mcp-renderer.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
RefKind,
|
||||||
|
reflectionKindName,
|
||||||
|
resolveModulePath,
|
||||||
|
collectModules
|
||||||
|
} from '../models/typedoc.js';
|
||||||
|
import { renderComment, renderParts, mapKind, classifyReact, ParamDoc } 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> = {};
|
||||||
|
const children = (mod.children || []).filter((c: any) => c.kind !== RefKind.Module);
|
||||||
|
const siblingNames = new Set(children.map((c: any) => c.name));
|
||||||
|
const baseName = moduleBaseName(modPath);
|
||||||
|
for (const child of children) {
|
||||||
|
// TypeDoc names default exports "default"; surface them under the
|
||||||
|
// module's own name (a React component/module convention) unless a
|
||||||
|
// sibling already claims that name.
|
||||||
|
const name = child.name === 'default' && !siblingNames.has(baseName) ? baseName : child.name;
|
||||||
|
objects[name] = this.renderObject(child, modPath, name);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: modPath,
|
||||||
|
docstring: renderComment(mod.comment),
|
||||||
|
objects
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderObject(ref: any, parentPath: string, nameOverride?: string): Record<string, unknown> {
|
||||||
|
const name = nameOverride ?? ref.name;
|
||||||
|
const kindName = classifyReact(name, mapKind(ref.kind));
|
||||||
|
const path = `${parentPath}.${name}`;
|
||||||
|
|
||||||
|
const data: Record<string, unknown> = {
|
||||||
|
name,
|
||||||
|
kind: kindName,
|
||||||
|
path,
|
||||||
|
signature: bestSignature(ref),
|
||||||
|
docstring: this.renderDocstring(ref)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a reflection's docstring, sourcing the summary/blockTags from the
|
||||||
|
* signature when TypeDoc has moved them there (functions, method-like
|
||||||
|
* type aliases) and attaching parameter documentation.
|
||||||
|
*/
|
||||||
|
private renderDocstring(ref: any): string {
|
||||||
|
const sig = bestSignatureRef(ref);
|
||||||
|
const refComment = ref.comment;
|
||||||
|
const sigComment = sig?.comment;
|
||||||
|
|
||||||
|
const params: ParamDoc[] = (sig?.parameters || [])
|
||||||
|
.map((p: any) => ({ name: p.name, description: renderParts(p.comment?.summary) }))
|
||||||
|
.filter((p: ParamDoc) => (p.description || '').trim().length > 0);
|
||||||
|
|
||||||
|
const typeParams: ParamDoc[] = (sig?.typeParameters || [])
|
||||||
|
.map((p: any) => ({ name: p.name, description: renderParts(p.comment?.summary) }))
|
||||||
|
.filter((p: ParamDoc) => (p.description || '').trim().length > 0);
|
||||||
|
|
||||||
|
const summary =
|
||||||
|
refComment?.summary && refComment.summary.length
|
||||||
|
? refComment.summary
|
||||||
|
: sigComment?.summary;
|
||||||
|
|
||||||
|
const blockTags = dedupeTags([
|
||||||
|
...(refComment?.blockTags || []),
|
||||||
|
...(sigComment?.blockTags || [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return renderComment({ summary, blockTags }, params, typeParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Last segment of a (dotted) module path, used to name default exports. */
|
||||||
|
function moduleBaseName(modPath: string): string {
|
||||||
|
const parts = modPath.split('.');
|
||||||
|
return parts[parts.length - 1] || modPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find the signature TypeDoc attached comment data to, if any. */
|
||||||
|
function bestSignatureRef(ref: any): any {
|
||||||
|
if (ref?.signatures?.length) return ref.signatures[0];
|
||||||
|
const decl = ref?.type?.declaration;
|
||||||
|
if (decl?.signatures?.length) return decl.signatures[0];
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove duplicate block tags (same tag + same content) across sources. */
|
||||||
|
function dedupeTags(tags: any[]): any[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: any[] = [];
|
||||||
|
for (const tag of tags) {
|
||||||
|
const key = `${tag?.tag || ''}|${(tag?.content || []).map((p: any) => p.text ?? '').join('')}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
out.push(tag);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
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