@contractspec/tool.docs-generator 0.0.0-canary-20260128200020
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/fs.mjs +36 -0
- package/dist/fs.mjs.map +1 -0
- package/dist/generate.mjs +163 -0
- package/dist/generate.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +29 -0
- package/dist/index.mjs.map +1 -0
- package/dist/markdown.mjs +21 -0
- package/dist/markdown.mjs.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ContractSpec Docs Generator
|
|
2
|
+
|
|
3
|
+
CLI tool for generating documentation artifacts from ContractSpec specs and DocBlocks.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bunx contractspec-docs generate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
From the monorepo root:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun docs:generate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Options
|
|
18
|
+
|
|
19
|
+
- `--source <dir>`: Source directory for generated markdown (default: `generated/docs`).
|
|
20
|
+
- `--out <dir>`: Output directory for generated artifacts (default: `packages/bundles/library/src/components/docs/generated`).
|
|
21
|
+
- `--content-root <dir>`: Root directory for docs content (default: `--source`).
|
|
22
|
+
- `--route-prefix <prefix>`: Route prefix for generated reference pages (default: `/docs/reference`).
|
|
23
|
+
- `--version <version>`: Output version subdirectory (e.g., `v1.0.0`).
|
|
24
|
+
- `--no-docblocks`: Skip DocBlocks from the docs registry.
|
|
25
|
+
|
|
26
|
+
## Output
|
|
27
|
+
|
|
28
|
+
The generator writes a typed index manifest plus markdown content.
|
|
29
|
+
|
|
30
|
+
Note: the `docblocks/` folder under the content root is ignored when scanning
|
|
31
|
+
source markdown to avoid re-indexing DocBlocks on subsequent runs.
|
|
32
|
+
|
|
33
|
+
Content is stored under `--content-root` (defaults to `--source`). If
|
|
34
|
+
`--version` is provided, both the index and content are nested under that
|
|
35
|
+
version subdirectory.
|
|
36
|
+
|
|
37
|
+
If `--version` is provided:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
<outDir>/<version>/docs-index.generated.ts
|
|
41
|
+
<outDir>/<version>/docs-index.manifest.json
|
|
42
|
+
<outDir>/<version>/docs-index.*.json
|
|
43
|
+
<contentRoot>/<version>/**/*.md
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Without `--version`:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
<outDir>/docs-index.generated.ts
|
|
50
|
+
<outDir>/docs-index.manifest.json
|
|
51
|
+
<outDir>/docs-index.*.json
|
|
52
|
+
<contentRoot>/**/*.md
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The index contains `docsIndex` entries and `docsIndexMeta`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export type DocsIndexEntry = {
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
summary?: string;
|
|
62
|
+
route?: string;
|
|
63
|
+
source: "generated" | "docblock";
|
|
64
|
+
contentPath?: string;
|
|
65
|
+
tags?: string[];
|
|
66
|
+
kind?: string;
|
|
67
|
+
visibility?: string;
|
|
68
|
+
version?: string;
|
|
69
|
+
owners?: string[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type DocsIndexManifest = {
|
|
73
|
+
generatedAt: string;
|
|
74
|
+
total: number;
|
|
75
|
+
version: string | null;
|
|
76
|
+
contentRoot: string | null;
|
|
77
|
+
chunks: { key: string; file: string; total: number }[];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const DOCS_INDEX_MANIFEST = "docs-index.manifest.json";
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Examples
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
bunx contractspec-docs generate \
|
|
87
|
+
--source generated/docs \
|
|
88
|
+
--out packages/bundles/library/src/components/docs/generated \
|
|
89
|
+
--route-prefix /docs/reference \
|
|
90
|
+
--version v1.0.0
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bunx contractspec-docs generate --no-docblocks
|
|
95
|
+
```
|
package/dist/fs.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { dirname, extname, join } from "node:path";
|
|
2
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
//#region src/fs.ts
|
|
5
|
+
async function ensureDir(path) {
|
|
6
|
+
await mkdir(path, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
async function readText(path) {
|
|
9
|
+
return readFile(path, "utf8");
|
|
10
|
+
}
|
|
11
|
+
async function writeText(path, content) {
|
|
12
|
+
await ensureDir(dirname(path));
|
|
13
|
+
await writeFile(path, content, "utf8");
|
|
14
|
+
}
|
|
15
|
+
async function listMarkdownFiles(rootDir, options) {
|
|
16
|
+
const files = [];
|
|
17
|
+
const exclude = new Set(options?.excludeDirs ?? []);
|
|
18
|
+
async function walk(current) {
|
|
19
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const fullPath = join(current, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
if (exclude.has(entry.name)) continue;
|
|
24
|
+
await walk(fullPath);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (entry.isFile() && extname(entry.name) === ".md") files.push(fullPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
await walk(rootDir);
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ensureDir, listMarkdownFiles, readText, writeText };
|
|
36
|
+
//# sourceMappingURL=fs.mjs.map
|
package/dist/fs.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.mjs","names":[],"sources":["../src/fs.ts"],"sourcesContent":["import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, extname, join } from 'node:path';\n\nexport async function ensureDir(path: string): Promise<void> {\n await mkdir(path, { recursive: true });\n}\n\nexport async function readText(path: string): Promise<string> {\n return readFile(path, 'utf8');\n}\n\nexport async function writeText(path: string, content: string): Promise<void> {\n await ensureDir(dirname(path));\n await writeFile(path, content, 'utf8');\n}\n\nexport async function copyTextFile(\n sourcePath: string,\n destPath: string\n): Promise<void> {\n const content = await readText(sourcePath);\n await writeText(destPath, content);\n}\n\nexport async function listMarkdownFiles(\n rootDir: string,\n options?: { excludeDirs?: string[] }\n): Promise<string[]> {\n const files: string[] = [];\n const exclude = new Set(options?.excludeDirs ?? []);\n\n async function walk(current: string): Promise<void> {\n const entries = await readdir(current, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(current, entry.name);\n if (entry.isDirectory()) {\n if (exclude.has(entry.name)) {\n continue;\n }\n await walk(fullPath);\n continue;\n }\n if (entry.isFile() && extname(entry.name) === '.md') {\n files.push(fullPath);\n }\n }\n }\n\n await walk(rootDir);\n return files;\n}\n"],"mappings":";;;;AAGA,eAAsB,UAAU,MAA6B;AAC3D,OAAM,MAAM,MAAM,EAAE,WAAW,MAAM,CAAC;;AAGxC,eAAsB,SAAS,MAA+B;AAC5D,QAAO,SAAS,MAAM,OAAO;;AAG/B,eAAsB,UAAU,MAAc,SAAgC;AAC5E,OAAM,UAAU,QAAQ,KAAK,CAAC;AAC9B,OAAM,UAAU,MAAM,SAAS,OAAO;;AAWxC,eAAsB,kBACpB,SACA,SACmB;CACnB,MAAM,QAAkB,EAAE;CAC1B,MAAM,UAAU,IAAI,IAAI,SAAS,eAAe,EAAE,CAAC;CAEnD,eAAe,KAAK,SAAgC;EAClD,MAAM,UAAU,MAAM,QAAQ,SAAS,EAAE,eAAe,MAAM,CAAC;AAE/D,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,KAAK,SAAS,MAAM,KAAK;AAC1C,OAAI,MAAM,aAAa,EAAE;AACvB,QAAI,QAAQ,IAAI,MAAM,KAAK,CACzB;AAEF,UAAM,KAAK,SAAS;AACpB;;AAEF,OAAI,MAAM,QAAQ,IAAI,QAAQ,MAAM,KAAK,KAAK,MAC5C,OAAM,KAAK,SAAS;;;AAK1B,OAAM,KAAK,QAAQ;AACnB,QAAO"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { ensureDir, listMarkdownFiles, readText, writeText } from "./fs.mjs";
|
|
2
|
+
import { extractSummary, extractTitle } from "./markdown.mjs";
|
|
3
|
+
import { defaultDocRegistry } from "@contractspec/lib.contracts/docs";
|
|
4
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/generate.ts
|
|
7
|
+
function normalizeId(pathValue) {
|
|
8
|
+
return pathValue.split(sep).join("/").replace(/\.md$/, "");
|
|
9
|
+
}
|
|
10
|
+
function buildGeneratedRoute(routePrefix, id) {
|
|
11
|
+
return `${routePrefix.endsWith("/") ? routePrefix.slice(0, -1) : routePrefix}/${id}`;
|
|
12
|
+
}
|
|
13
|
+
function buildIndexTypesFile() {
|
|
14
|
+
return [
|
|
15
|
+
"export type DocsIndexSource = 'generated' | 'docblock';",
|
|
16
|
+
"",
|
|
17
|
+
"export type DocsIndexEntry = {",
|
|
18
|
+
" id: string;",
|
|
19
|
+
" title: string;",
|
|
20
|
+
" summary?: string;",
|
|
21
|
+
" route?: string;",
|
|
22
|
+
" source: DocsIndexSource;",
|
|
23
|
+
" contentPath?: string;",
|
|
24
|
+
" tags?: string[];",
|
|
25
|
+
" kind?: string;",
|
|
26
|
+
" visibility?: string;",
|
|
27
|
+
" version?: string;",
|
|
28
|
+
" owners?: string[];",
|
|
29
|
+
"};",
|
|
30
|
+
"",
|
|
31
|
+
"export type DocsIndexChunk = {",
|
|
32
|
+
" key: string;",
|
|
33
|
+
" file: string;",
|
|
34
|
+
" total: number;",
|
|
35
|
+
"};",
|
|
36
|
+
"",
|
|
37
|
+
"export type DocsIndexManifest = {",
|
|
38
|
+
" generatedAt: string;",
|
|
39
|
+
" total: number;",
|
|
40
|
+
" version: string | null;",
|
|
41
|
+
" contentRoot: string | null;",
|
|
42
|
+
" chunks: DocsIndexChunk[];",
|
|
43
|
+
"};",
|
|
44
|
+
"",
|
|
45
|
+
"export const DOCS_INDEX_MANIFEST = \"docs-index.manifest.json\";",
|
|
46
|
+
""
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
49
|
+
function chunkKeyForId(id) {
|
|
50
|
+
if (!id) return "_common";
|
|
51
|
+
if (id.includes("/")) {
|
|
52
|
+
const [prefix] = id.split("/");
|
|
53
|
+
return prefix || "_common";
|
|
54
|
+
}
|
|
55
|
+
return "_common";
|
|
56
|
+
}
|
|
57
|
+
function buildChunkFileName(key, usedNames) {
|
|
58
|
+
const baseName = `docs-index.${key.replace(/[^a-zA-Z0-9-_]/g, "-") || "common"}.json`;
|
|
59
|
+
const count = usedNames.get(baseName) ?? 0;
|
|
60
|
+
const fileName = count === 0 ? baseName : baseName.replace(/\.json$/, `-${count}.json`);
|
|
61
|
+
usedNames.set(baseName, count + 1);
|
|
62
|
+
return fileName;
|
|
63
|
+
}
|
|
64
|
+
async function generateDocs(options) {
|
|
65
|
+
const outputDir = options.version ? join(options.outDir, options.version) : options.outDir;
|
|
66
|
+
const contentRootBase = options.contentRoot ?? options.sourceDir;
|
|
67
|
+
const contentRoot = options.version ? join(contentRootBase, options.version) : contentRootBase;
|
|
68
|
+
const resolvedOutputDir = resolve(outputDir);
|
|
69
|
+
const resolvedSourceDir = resolve(options.sourceDir);
|
|
70
|
+
const resolvedContentRoot = resolve(contentRoot);
|
|
71
|
+
const shouldCopyContent = resolvedContentRoot !== resolvedSourceDir;
|
|
72
|
+
const contentRootRelative = relative(resolvedOutputDir, resolvedContentRoot) || ".";
|
|
73
|
+
await ensureDir(outputDir);
|
|
74
|
+
if (shouldCopyContent) await ensureDir(contentRoot);
|
|
75
|
+
const markdownFiles = await listMarkdownFiles(options.sourceDir, { excludeDirs: ["docblocks"] });
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const filePath of markdownFiles) {
|
|
78
|
+
const relativePath = normalizeId(relative(options.sourceDir, filePath));
|
|
79
|
+
const contentPath = `${relativePath}.md`;
|
|
80
|
+
const targetPath = join(contentRoot, contentPath);
|
|
81
|
+
const content = await readText(filePath);
|
|
82
|
+
const title = extractTitle(content, relativePath);
|
|
83
|
+
const summary = extractSummary(content);
|
|
84
|
+
const route = buildGeneratedRoute(options.routePrefix, relativePath);
|
|
85
|
+
if (shouldCopyContent) await writeText(targetPath, content);
|
|
86
|
+
entries.push({
|
|
87
|
+
id: relativePath,
|
|
88
|
+
title,
|
|
89
|
+
summary,
|
|
90
|
+
route,
|
|
91
|
+
source: "generated",
|
|
92
|
+
contentPath
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
let docblockCount = 0;
|
|
96
|
+
if (options.includeDocblocks) {
|
|
97
|
+
const routes = defaultDocRegistry.list();
|
|
98
|
+
for (const entry of routes) {
|
|
99
|
+
if (!entry?.block) continue;
|
|
100
|
+
const { block, route } = entry;
|
|
101
|
+
if (!block.id) continue;
|
|
102
|
+
const docPath = `docblocks/${block.id.replace(/\./g, "/")}.md`;
|
|
103
|
+
await writeText(join(contentRoot, docPath), String(block.body ?? ""));
|
|
104
|
+
entries.push({
|
|
105
|
+
id: block.id,
|
|
106
|
+
title: block.title,
|
|
107
|
+
summary: block.summary,
|
|
108
|
+
route,
|
|
109
|
+
source: "docblock",
|
|
110
|
+
contentPath: docPath,
|
|
111
|
+
tags: block.tags,
|
|
112
|
+
kind: block.kind,
|
|
113
|
+
visibility: block.visibility,
|
|
114
|
+
version: block.version,
|
|
115
|
+
owners: block.owners
|
|
116
|
+
});
|
|
117
|
+
docblockCount += 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
121
|
+
const chunkMap = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const key = chunkKeyForId(entry.id);
|
|
124
|
+
const bucket = chunkMap.get(key) ?? [];
|
|
125
|
+
bucket.push(entry);
|
|
126
|
+
chunkMap.set(key, bucket);
|
|
127
|
+
}
|
|
128
|
+
const usedNames = /* @__PURE__ */ new Map();
|
|
129
|
+
const chunks = [];
|
|
130
|
+
for (const [key, chunkEntries] of chunkMap) {
|
|
131
|
+
chunkEntries.sort((a, b) => a.id.localeCompare(b.id));
|
|
132
|
+
const file = buildChunkFileName(key, usedNames);
|
|
133
|
+
await writeText(join(outputDir, file), JSON.stringify(chunkEntries, null, 2));
|
|
134
|
+
chunks.push({
|
|
135
|
+
key,
|
|
136
|
+
file,
|
|
137
|
+
total: chunkEntries.length
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
chunks.sort((a, b) => a.key.localeCompare(b.key));
|
|
141
|
+
const manifest = {
|
|
142
|
+
generatedAt,
|
|
143
|
+
total: entries.length,
|
|
144
|
+
version: options.version ?? null,
|
|
145
|
+
contentRoot: contentRootRelative || null,
|
|
146
|
+
chunks
|
|
147
|
+
};
|
|
148
|
+
await writeText(join(outputDir, "docs-index.manifest.json"), JSON.stringify(manifest, null, 2));
|
|
149
|
+
const indexTypes = buildIndexTypesFile();
|
|
150
|
+
await writeText(join(outputDir, "docs-index.generated.ts"), indexTypes);
|
|
151
|
+
return {
|
|
152
|
+
total: entries.length,
|
|
153
|
+
generated: markdownFiles.length,
|
|
154
|
+
docblocks: docblockCount,
|
|
155
|
+
outputDir,
|
|
156
|
+
contentRoot,
|
|
157
|
+
version: options.version
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
export { generateDocs };
|
|
163
|
+
//# sourceMappingURL=generate.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.mjs","names":[],"sources":["../src/generate.ts"],"sourcesContent":["import { defaultDocRegistry } from '@contractspec/lib.contracts/docs';\nimport { join, relative, resolve, sep } from 'node:path';\nimport { ensureDir, listMarkdownFiles, readText, writeText } from './fs';\nimport { extractSummary, extractTitle } from './markdown';\nimport type { DocsIndexEntry, GenerateOptions, GenerateResult } from './types';\n\nfunction normalizeId(pathValue: string): string {\n return pathValue.split(sep).join('/').replace(/\\.md$/, '');\n}\n\nfunction buildGeneratedRoute(routePrefix: string, id: string): string {\n const cleanPrefix = routePrefix.endsWith('/')\n ? routePrefix.slice(0, -1)\n : routePrefix;\n return `${cleanPrefix}/${id}`;\n}\n\ninterface DocsIndexChunk {\n key: string;\n file: string;\n total: number;\n}\n\ninterface DocsIndexManifest {\n generatedAt: string;\n total: number;\n version: string | null;\n contentRoot: string | null;\n chunks: DocsIndexChunk[];\n}\n\nfunction buildIndexTypesFile(): string {\n return [\n \"export type DocsIndexSource = 'generated' | 'docblock';\",\n '',\n 'export type DocsIndexEntry = {',\n ' id: string;',\n ' title: string;',\n ' summary?: string;',\n ' route?: string;',\n ' source: DocsIndexSource;',\n ' contentPath?: string;',\n ' tags?: string[];',\n ' kind?: string;',\n ' visibility?: string;',\n ' version?: string;',\n ' owners?: string[];',\n '};',\n '',\n 'export type DocsIndexChunk = {',\n ' key: string;',\n ' file: string;',\n ' total: number;',\n '};',\n '',\n 'export type DocsIndexManifest = {',\n ' generatedAt: string;',\n ' total: number;',\n ' version: string | null;',\n ' contentRoot: string | null;',\n ' chunks: DocsIndexChunk[];',\n '};',\n '',\n 'export const DOCS_INDEX_MANIFEST = \"docs-index.manifest.json\";',\n '',\n ].join('\\n');\n}\n\nfunction chunkKeyForId(id: string): string {\n if (!id) return '_common';\n if (id.includes('/')) {\n const [prefix] = id.split('/');\n return prefix || '_common';\n }\n return '_common';\n}\n\nfunction buildChunkFileName(\n key: string,\n usedNames: Map<string, number>\n): string {\n const safeKey = key.replace(/[^a-zA-Z0-9-_]/g, '-');\n const baseName = `docs-index.${safeKey || 'common'}.json`;\n const count = usedNames.get(baseName) ?? 0;\n const fileName =\n count === 0 ? baseName : baseName.replace(/\\.json$/, `-${count}.json`);\n usedNames.set(baseName, count + 1);\n return fileName;\n}\n\nexport async function generateDocs(\n options: GenerateOptions\n): Promise<GenerateResult> {\n const outputDir = options.version\n ? join(options.outDir, options.version)\n : options.outDir;\n const contentRootBase = options.contentRoot ?? options.sourceDir;\n const contentRoot = options.version\n ? join(contentRootBase, options.version)\n : contentRootBase;\n const resolvedOutputDir = resolve(outputDir);\n const resolvedSourceDir = resolve(options.sourceDir);\n const resolvedContentRoot = resolve(contentRoot);\n const shouldCopyContent = resolvedContentRoot !== resolvedSourceDir;\n const contentRootRelative =\n relative(resolvedOutputDir, resolvedContentRoot) || '.';\n await ensureDir(outputDir);\n if (shouldCopyContent) {\n await ensureDir(contentRoot);\n }\n\n const markdownFiles = await listMarkdownFiles(options.sourceDir, {\n excludeDirs: ['docblocks'],\n });\n const entries: DocsIndexEntry[] = [];\n\n for (const filePath of markdownFiles) {\n const relativePath = normalizeId(relative(options.sourceDir, filePath));\n const contentPath = `${relativePath}.md`;\n const targetPath = join(contentRoot, contentPath);\n\n const content = await readText(filePath);\n const title = extractTitle(content, relativePath);\n const summary = extractSummary(content);\n const route = buildGeneratedRoute(options.routePrefix, relativePath);\n\n if (shouldCopyContent) {\n await writeText(targetPath, content);\n }\n\n entries.push({\n id: relativePath,\n title,\n summary,\n route,\n source: 'generated',\n contentPath,\n });\n }\n\n let docblockCount = 0;\n if (options.includeDocblocks) {\n const routes = defaultDocRegistry.list();\n\n for (const entry of routes) {\n if (!entry?.block) {\n continue;\n }\n const { block, route } = entry;\n if (!block.id) {\n continue;\n }\n\n const docPath = `docblocks/${block.id.replace(/\\./g, '/')}.md`;\n const targetPath = join(contentRoot, docPath);\n await writeText(targetPath, String(block.body ?? ''));\n\n entries.push({\n id: block.id,\n title: block.title,\n summary: block.summary,\n route,\n source: 'docblock',\n contentPath: docPath,\n tags: block.tags,\n kind: block.kind,\n visibility: block.visibility,\n version: block.version,\n owners: block.owners,\n });\n docblockCount += 1;\n }\n }\n\n const generatedAt = new Date().toISOString();\n const chunkMap = new Map<string, DocsIndexEntry[]>();\n for (const entry of entries) {\n const key = chunkKeyForId(entry.id);\n const bucket = chunkMap.get(key) ?? [];\n bucket.push(entry);\n chunkMap.set(key, bucket);\n }\n\n const usedNames = new Map<string, number>();\n const chunks: DocsIndexChunk[] = [];\n for (const [key, chunkEntries] of chunkMap) {\n chunkEntries.sort((a, b) => a.id.localeCompare(b.id));\n const file = buildChunkFileName(key, usedNames);\n await writeText(\n join(outputDir, file),\n JSON.stringify(chunkEntries, null, 2)\n );\n chunks.push({\n key,\n file,\n total: chunkEntries.length,\n });\n }\n\n chunks.sort((a, b) => a.key.localeCompare(b.key));\n\n const manifest: DocsIndexManifest = {\n generatedAt,\n total: entries.length,\n version: options.version ?? null,\n contentRoot: contentRootRelative || null,\n chunks,\n };\n\n await writeText(\n join(outputDir, 'docs-index.manifest.json'),\n JSON.stringify(manifest, null, 2)\n );\n\n const indexTypes = buildIndexTypesFile();\n await writeText(join(outputDir, 'docs-index.generated.ts'), indexTypes);\n\n return {\n total: entries.length,\n generated: markdownFiles.length,\n docblocks: docblockCount,\n outputDir,\n contentRoot,\n version: options.version,\n };\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { generateDocs } from "./generate.mjs";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program.name("contractspec-docs").description("Generate documentation artifacts from ContractSpec specs").version("1.0.0");
|
|
8
|
+
program.command("generate").description("Generate docs index and content artifacts").option("--source <dir>", "Source directory for generated markdown", "generated/docs").option("--out <dir>", "Output directory for generated artifacts", "packages/bundles/library/src/components/docs/generated").option("--content-root <dir>", "Root directory for docs content (defaults to --source)").option("--route-prefix <prefix>", "Route prefix for generated reference pages", "/docs/reference").option("--version <version>", "Version label for generated docs output").option("--no-docblocks", "Skip DocBlocks registry entries").action(async (options) => {
|
|
9
|
+
const result = await generateDocs({
|
|
10
|
+
sourceDir: options.source,
|
|
11
|
+
outDir: options.out,
|
|
12
|
+
contentRoot: options.contentRoot ?? options.source,
|
|
13
|
+
includeDocblocks: Boolean(options.docblocks),
|
|
14
|
+
routePrefix: options.routePrefix,
|
|
15
|
+
version: options.version
|
|
16
|
+
});
|
|
17
|
+
console.log("✅ Docs generated");
|
|
18
|
+
console.log(`- Output: ${result.outputDir}`);
|
|
19
|
+
console.log(`- Content root: ${result.contentRoot}`);
|
|
20
|
+
console.log(`- Total entries: ${result.total}`);
|
|
21
|
+
console.log(`- Contract docs: ${result.generated}`);
|
|
22
|
+
console.log(`- DocBlocks: ${result.docblocks}`);
|
|
23
|
+
if (result.version) console.log(`- Version: ${result.version}`);
|
|
24
|
+
});
|
|
25
|
+
program.parseAsync();
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { };
|
|
29
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { generateDocs } from './generate';\n\nconst program = new Command();\n\nprogram\n .name('contractspec-docs')\n .description('Generate documentation artifacts from ContractSpec specs')\n .version('1.0.0');\n\nprogram\n .command('generate')\n .description('Generate docs index and content artifacts')\n .option(\n '--source <dir>',\n 'Source directory for generated markdown',\n 'generated/docs'\n )\n .option(\n '--out <dir>',\n 'Output directory for generated artifacts',\n 'packages/bundles/library/src/components/docs/generated'\n )\n .option(\n '--content-root <dir>',\n 'Root directory for docs content (defaults to --source)'\n )\n .option(\n '--route-prefix <prefix>',\n 'Route prefix for generated reference pages',\n '/docs/reference'\n )\n .option('--version <version>', 'Version label for generated docs output')\n .option('--no-docblocks', 'Skip DocBlocks registry entries')\n .action(async (options) => {\n const result = await generateDocs({\n sourceDir: options.source,\n outDir: options.out,\n contentRoot: options.contentRoot ?? options.source,\n includeDocblocks: Boolean(options.docblocks),\n routePrefix: options.routePrefix,\n version: options.version,\n });\n\n console.log('✅ Docs generated');\n console.log(`- Output: ${result.outputDir}`);\n console.log(`- Content root: ${result.contentRoot}`);\n console.log(`- Total entries: ${result.total}`);\n console.log(`- Contract docs: ${result.generated}`);\n console.log(`- DocBlocks: ${result.docblocks}`);\n if (result.version) {\n console.log(`- Version: ${result.version}`);\n }\n });\n\nprogram.parseAsync();\n"],"mappings":";;;;;AAKA,MAAM,UAAU,IAAI,SAAS;AAE7B,QACG,KAAK,oBAAoB,CACzB,YAAY,2DAA2D,CACvE,QAAQ,QAAQ;AAEnB,QACG,QAAQ,WAAW,CACnB,YAAY,4CAA4C,CACxD,OACC,kBACA,2CACA,iBACD,CACA,OACC,eACA,4CACA,yDACD,CACA,OACC,wBACA,yDACD,CACA,OACC,2BACA,8CACA,kBACD,CACA,OAAO,uBAAuB,0CAA0C,CACxE,OAAO,kBAAkB,kCAAkC,CAC3D,OAAO,OAAO,YAAY;CACzB,MAAM,SAAS,MAAM,aAAa;EAChC,WAAW,QAAQ;EACnB,QAAQ,QAAQ;EAChB,aAAa,QAAQ,eAAe,QAAQ;EAC5C,kBAAkB,QAAQ,QAAQ,UAAU;EAC5C,aAAa,QAAQ;EACrB,SAAS,QAAQ;EAClB,CAAC;AAEF,SAAQ,IAAI,mBAAmB;AAC/B,SAAQ,IAAI,aAAa,OAAO,YAAY;AAC5C,SAAQ,IAAI,mBAAmB,OAAO,cAAc;AACpD,SAAQ,IAAI,oBAAoB,OAAO,QAAQ;AAC/C,SAAQ,IAAI,oBAAoB,OAAO,YAAY;AACnD,SAAQ,IAAI,gBAAgB,OAAO,YAAY;AAC/C,KAAI,OAAO,QACT,SAAQ,IAAI,cAAc,OAAO,UAAU;EAE7C;AAEJ,QAAQ,YAAY"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/markdown.ts
|
|
2
|
+
const HEADING_MATCH = /^#\s+(.+)$/m;
|
|
3
|
+
function extractTitle(content, fallback) {
|
|
4
|
+
return content.match(HEADING_MATCH)?.[1]?.trim() || fallback;
|
|
5
|
+
}
|
|
6
|
+
function extractSummary(content) {
|
|
7
|
+
const filtered = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("<!--"));
|
|
8
|
+
if (!filtered.length) return void 0;
|
|
9
|
+
const summaryLines = [];
|
|
10
|
+
for (const line of filtered) {
|
|
11
|
+
if (summaryLines.length && line.length === 0) break;
|
|
12
|
+
summaryLines.push(line);
|
|
13
|
+
if (summaryLines.join(" ").length > 200) break;
|
|
14
|
+
}
|
|
15
|
+
const summary = summaryLines.join(" ").trim();
|
|
16
|
+
return summary.length ? summary : void 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { extractSummary, extractTitle };
|
|
21
|
+
//# sourceMappingURL=markdown.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.mjs","names":[],"sources":["../src/markdown.ts"],"sourcesContent":["const HEADING_MATCH = /^#\\s+(.+)$/m;\n\nexport function extractTitle(content: string, fallback: string): string {\n const match = content.match(HEADING_MATCH);\n return match?.[1]?.trim() || fallback;\n}\n\nexport function extractSummary(content: string): string | undefined {\n const lines = content.split(/\\r?\\n/).map((line) => line.trim());\n const filtered = lines.filter(\n (line) =>\n line.length > 0 && !line.startsWith('#') && !line.startsWith('<!--')\n );\n\n if (!filtered.length) return undefined;\n\n const summaryLines: string[] = [];\n for (const line of filtered) {\n if (summaryLines.length && line.length === 0) break;\n summaryLines.push(line);\n if (summaryLines.join(' ').length > 200) break;\n }\n\n const summary = summaryLines.join(' ').trim();\n return summary.length ? summary : undefined;\n}\n"],"mappings":";AAAA,MAAM,gBAAgB;AAEtB,SAAgB,aAAa,SAAiB,UAA0B;AAEtE,QADc,QAAQ,MAAM,cAAc,GAC3B,IAAI,MAAM,IAAI;;AAG/B,SAAgB,eAAe,SAAqC;CAElE,MAAM,WADQ,QAAQ,MAAM,QAAQ,CAAC,KAAK,SAAS,KAAK,MAAM,CAAC,CACxC,QACpB,SACC,KAAK,SAAS,KAAK,CAAC,KAAK,WAAW,IAAI,IAAI,CAAC,KAAK,WAAW,OAAO,CACvE;AAED,KAAI,CAAC,SAAS,OAAQ,QAAO;CAE7B,MAAM,eAAyB,EAAE;AACjC,MAAK,MAAM,QAAQ,UAAU;AAC3B,MAAI,aAAa,UAAU,KAAK,WAAW,EAAG;AAC9C,eAAa,KAAK,KAAK;AACvB,MAAI,aAAa,KAAK,IAAI,CAAC,SAAS,IAAK;;CAG3C,MAAM,UAAU,aAAa,KAAK,IAAI,CAAC,MAAM;AAC7C,QAAO,QAAQ,SAAS,UAAU"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contractspec/tool.docs-generator",
|
|
3
|
+
"version": "0.0.0-canary-20260128200020",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI tool for generating docs artifacts from ContractSpec specs and DocBlocks",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"contractspec",
|
|
8
|
+
"docs",
|
|
9
|
+
"generator",
|
|
10
|
+
"cli",
|
|
11
|
+
"documentation"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
15
|
+
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
16
|
+
"build": "bun build:types && bun build:bundle",
|
|
17
|
+
"build:bundle": "tsdown",
|
|
18
|
+
"build:types": "tsc --noEmit",
|
|
19
|
+
"docs:generate": "bun src/index.ts generate --source \"../../../generated/docs\" --out \"../../../packages/bundles/library/src/components/docs/generated\" --content-root \"../../../generated/docs\"",
|
|
20
|
+
"clean": "rimraf dist .turbo",
|
|
21
|
+
"lint": "bun lint:fix",
|
|
22
|
+
"lint:fix": "eslint src --fix",
|
|
23
|
+
"lint:check": "eslint src",
|
|
24
|
+
"test": "bun test"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"contractspec-docs": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.mjs",
|
|
31
|
+
"./*": "./*"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@contractspec/lib.contracts": "0.0.0-canary-20260128200020",
|
|
39
|
+
"commander": "^12.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@contractspec/tool.tsdown": "1.52.0",
|
|
43
|
+
"@contractspec/tool.typescript": "1.52.0",
|
|
44
|
+
"tsdown": "^0.19.0",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"registry": "https://registry.npmjs.org/"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/lssm-tech/contractspec.git",
|
|
55
|
+
"directory": "packages/tools/docs-generator"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://contractspec.io"
|
|
58
|
+
}
|