Generate a static documentation site for a Bun/TS monorepo, straight from source — no build, no annotations beyond the code's own doc-comments.
Part of Suluk — one typed OpenAPI v4 contract projecting into every full-stack layer.
CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.
bun add @suluk/docs
It harvests a Bun/TS workspace into a documentation model and renders it to a flat, self-hosting site — the same "one source, many projections" discipline as the rest of Suluk, applied to the monorepo itself.
package.json (name / description / version / deps), the leading doc-comment of src/index.ts as the overview, the public exports of that barrel, and each module's leading doc-comment. No extra annotations — it uses the comments the code already carries..nojekyll, with relative links — drop the files on GitHub Pages (even a project-pages subpath) and it serves verbatim. Suluk documents itself this way.mdToHtml) and a D2 package-graph + kroki URL helper are exported on their own.Reach for it to document the monorepo / toolkit itself — the packages, how they depend on each other, what each one exports. It reads structure from package.json + barrel doc-comments, so it stays in sync as you add packages.
It does not document your API. For an OpenAPI v4 reference site use @suluk/reference / @suluk/scalar / @suluk/swagger (those project the v4 document; this projects the codebase).
The common case is two calls — harvest a packages directory into a FrameworkDoc, then generateSite it into files you write to disk.
import { harvest, generateSite } from "@suluk/docs";
import { writeFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
const fw = harvest({
packagesDir: join(import.meta.dir, "packages"),
title: "Suluk",
tagline: "One typed contract — projected into your entire stack.",
description: "**Suluk** derives the whole stack from one source.",
repoUrl: "https://github.com/MahmoodKhalil57/suluk",
architecturePath: join(import.meta.dir, "ARCHITECTURE.md"), // optional — prepends to the Architecture page
// excludePrivate: true, // drop demo/private packages from the public docs (default: include, flagged)
});
const out = join(import.meta.dir, "site");
for (const f of generateSite(fw)) {
const p = join(out, f.path);
mkdirSync(dirname(p), { recursive: true });
writeFileSync(p, f.content);
}
// → site/index.html, site/<pkg>.html, site/architecture.html, site/style.css, site/.nojekyll, …
generateSite returns SiteFile[] ({ path, content }) — no I/O of its own, so you own where the bytes land. Override the curated pages by passing Markdown:
generateSite(fw, {
gettingStarted: "# Get started\n\nYour own intro…",
contributing: "# Contributing\n\n…",
community: "# Community\n\n…",
});
The harvest and render steps are exposed individually:
import { harvestPackage, renderIndex, renderPackage } from "@suluk/docs";
const pkg = harvestPackage("./packages/core"); // → PackageDoc | null (null if no package.json)
const indexHtml = renderIndex(fw); // the landing page
const pageHtml = renderPackage(fw, fw.packages[0]); // one package page
import {
mdToHtml, parseExports, firstBlockComment,
packageGraphD2, krokiD2Url, STYLE,
} from "@suluk/docs";
mdToHtml("# Hi\n\n`code`, **bold**, and a [link](https://suluk.dev)"); // → HTML string
parseExports(`export { a, b as c } from "./x";`); // → ["a", "c"]
firstBlockComment("/**\n * one\n * two\n */"); // → "one\ntwo"
const d2 = packageGraphD2(fw.packages); // D2 of the @suluk dependency graph (private pkgs omitted)
krokiD2Url(d2); // → https://kroki.io/d2/svg/… (deflate+base64url, renders the diagram)
STYLE; // the site's single stylesheet, if you assemble pages yourself
| Export | What it does |
|---|---|
harvest(opts) |
Read a packages dir → FrameworkDoc (every package's name, overview, exports, deps, modules). |
harvestPackage(dir) |
Harvest a single package dir → PackageDoc | null. |
generateSite(fw, opts?) |
Assemble the whole site → SiteFile[] ({ path, content }); no I/O. |
renderIndex(fw) |
Render just the landing page → HTML. |
renderPackage(fw, p) |
Render one package page → HTML. |
renderMarkdownPage(fw, file, title, md) |
Render a curated Markdown page into the site chrome → HTML. |
mdToHtml(md) / inline(text) / escapeHtml(s) |
Dependency-free Markdown / inline-span / HTML-escape helpers. |
parseExports(src) |
Collect the public symbol names a barrel re-exports. |
firstBlockComment(src) |
Extract + clean the first JSDoc block comment. |
packageGraphD2(packages) |
D2 source for the @suluk package-dependency graph. |
krokiD2Url(d2) |
A kroki.io render URL for D2 source. |
STYLE |
The site's single stylesheet (string). |
Types: FrameworkDoc, PackageDoc, ModuleDoc, HarvestOptions, SiteOptions, SiteFile.
This package renders, never hosts (the L3 line). generateSite returns the files; you write them and serve them (GitHub Pages, any static host). It reads from a directory of packages and emits HTML strings — no network in the harvest/render path, and the only outbound URL it constructs is the kroki render link for the architecture diagram. What it documents is the codebase (package structure + doc-comments), not your runtime API — that projection belongs to @suluk/reference / @suluk/scalar / @suluk/swagger.
Apache-2.0