@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 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
@@ -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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"}
@@ -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
+ }