@openpkg-ts/doc-generator 0.1.0

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,324 @@
1
+ # @openpkg-ts/doc-generator
2
+
3
+ API documentation generator consuming OpenPkg specs. A modern alternative to TypeDoc for doc frameworks like Fumadocs, Mintlify, and Docusaurus.
4
+
5
+ ## Features
6
+
7
+ - **Multiple output formats**: Markdown/MDX, HTML, JSON
8
+ - **React components**: Headless and pre-styled (Tailwind v4)
9
+ - **Search indexes**: Pagefind and Algolia compatible
10
+ - **Framework adapters**: Fumadocs, Docusaurus, and generic navigation
11
+ - **CLI tooling**: Generate static docs or dev server
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @openpkg-ts/doc-generator
17
+ # or
18
+ bun add @openpkg-ts/doc-generator
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Programmatic API
24
+
25
+ ```ts
26
+ import { createDocs } from '@openpkg-ts/doc-generator'
27
+
28
+ // Load from file or object
29
+ const docs = createDocs('./openpkg.json')
30
+
31
+ // Query exports
32
+ docs.getExport('useState')
33
+ docs.getExportsByKind('function')
34
+ docs.getExportsByTag('@beta')
35
+ docs.search('hook')
36
+
37
+ // Render
38
+ docs.toMarkdown() // Full MDX
39
+ docs.toMarkdown({ export: 'useState' }) // Single export
40
+ docs.toHTML() // Standalone HTML
41
+ docs.toJSON() // Structured JSON
42
+ docs.toNavigation() // Sidebar structure
43
+ docs.toSearchIndex() // Search records
44
+ ```
45
+
46
+ ### CLI
47
+
48
+ ```bash
49
+ # Generate MDX files for existing site
50
+ npx openpkg-docs generate ./openpkg.json --out ./docs/api
51
+
52
+ # Generate JSON for custom rendering
53
+ npx openpkg-docs generate ./openpkg.json --format json --out ./api.json
54
+
55
+ # Generate with navigation
56
+ npx openpkg-docs generate ./openpkg.json --out ./docs/api --nav fumadocs
57
+
58
+ # Build standalone site
59
+ npx openpkg-docs build ./openpkg.json --out ./api-docs
60
+
61
+ # Dev server with hot reload
62
+ npx openpkg-docs dev ./openpkg.json --port 3001
63
+ ```
64
+
65
+ ### React Components
66
+
67
+ ```tsx
68
+ // Headless (unstyled, composable)
69
+ import { Signature, ParamTable, ExampleBlock } from '@openpkg-ts/doc-generator/react'
70
+
71
+ <Signature signature={fn.signatures[0]} />
72
+ <ParamTable params={fn.signatures[0].parameters} />
73
+ <ExampleBlock examples={fn.examples} />
74
+ ```
75
+
76
+ ```tsx
77
+ // Pre-styled (Tailwind v4)
78
+ import { FunctionPage, ClassPage } from '@openpkg-ts/doc-generator/react/styled'
79
+
80
+ <FunctionPage export={fn} />
81
+ <ClassPage export={cls} />
82
+ ```
83
+
84
+ ## API Reference
85
+
86
+ ### Core Functions
87
+
88
+ #### `createDocs(input: string | OpenPkg): DocsInstance`
89
+
90
+ Create a docs instance from file path or spec object.
91
+
92
+ ```ts
93
+ const docs = createDocs('./openpkg.json')
94
+ // or
95
+ const docs = createDocs(specObject)
96
+ ```
97
+
98
+ #### `loadSpec(spec: OpenPkg): DocsInstance`
99
+
100
+ Create a docs instance from spec object directly.
101
+
102
+ ### DocsInstance Methods
103
+
104
+ | Method | Description |
105
+ |--------|-------------|
106
+ | `getExport(id)` | Get export by ID |
107
+ | `getExportsByKind(kind)` | Get exports of specific kind |
108
+ | `getExportsByTag(tag)` | Get exports with JSDoc tag |
109
+ | `search(query)` | Search by name/description |
110
+ | `getDeprecated()` | Get deprecated exports |
111
+ | `groupByKind()` | Group exports by kind |
112
+ | `toMarkdown(options?)` | Render to MDX |
113
+ | `toHTML(options?)` | Render to HTML |
114
+ | `toJSON(options?)` | Render to JSON |
115
+ | `toNavigation(options?)` | Generate navigation |
116
+ | `toSearchIndex(options?)` | Generate search index |
117
+
118
+ ### Query Utilities
119
+
120
+ ```ts
121
+ import {
122
+ formatSchema,
123
+ buildSignatureString,
124
+ getMethods,
125
+ getProperties,
126
+ groupByVisibility,
127
+ sortByKindThenName,
128
+ } from '@openpkg-ts/doc-generator'
129
+
130
+ formatSchema({ type: 'string' }) // 'string'
131
+ buildSignatureString(fn) // 'function greet(name: string): string'
132
+ getMethods(classExport.members) // [{ name: 'foo', signatures: [...] }]
133
+ ```
134
+
135
+ ### Render Options
136
+
137
+ #### Markdown Options
138
+
139
+ ```ts
140
+ docs.toMarkdown({
141
+ export: 'greet', // Single export mode
142
+ frontmatter: true, // Include YAML frontmatter
143
+ codeSignatures: true, // Use code blocks for signatures
144
+ headingOffset: 1, // Start at h2 instead of h1
145
+ sections: {
146
+ signature: true,
147
+ description: true,
148
+ parameters: true,
149
+ returns: true,
150
+ examples: true,
151
+ },
152
+ })
153
+ ```
154
+
155
+ #### HTML Options
156
+
157
+ ```ts
158
+ docs.toHTML({
159
+ export: 'greet', // Single export mode
160
+ fullDocument: true, // Wrap in HTML document
161
+ includeStyles: true, // Include default CSS
162
+ customCSS: '.custom {}', // Custom CSS to inject
163
+ title: 'API Reference', // Page title
164
+ })
165
+ ```
166
+
167
+ #### Navigation Options
168
+
169
+ ```ts
170
+ docs.toNavigation({
171
+ format: 'fumadocs', // 'fumadocs' | 'docusaurus' | 'generic'
172
+ groupBy: 'kind', // 'kind' | 'module' | 'tag' | 'none'
173
+ basePath: '/api', // Base URL for links
174
+ sortAlphabetically: true, // Sort exports by name
175
+ })
176
+ ```
177
+
178
+ #### Search Options
179
+
180
+ ```ts
181
+ docs.toSearchIndex({
182
+ baseUrl: '/docs/api',
183
+ includeMembers: true,
184
+ includeParameters: true,
185
+ weights: {
186
+ name: 10,
187
+ description: 5,
188
+ signature: 3,
189
+ },
190
+ })
191
+ ```
192
+
193
+ ## React Components
194
+
195
+ ### Headless Components
196
+
197
+ Unstyled, composable primitives for building custom UIs:
198
+
199
+ | Component | Props | Description |
200
+ |-----------|-------|-------------|
201
+ | `Signature` | `SignatureProps` | Render type signature |
202
+ | `ParamTable` | `ParamTableProps` | Parameter table |
203
+ | `TypeTable` | `TypeTableProps` | Type properties table |
204
+ | `MembersTable` | `MembersTableProps` | Class/interface members |
205
+ | `ExampleBlock` | `ExampleBlockProps` | Code examples |
206
+ | `CollapsibleMethod` | `CollapsibleMethodProps` | Expandable method |
207
+ | `ExpandableProperty` | `ExpandablePropertyProps` | Nested properties |
208
+
209
+ ### Styled Components
210
+
211
+ Pre-styled with Tailwind v4:
212
+
213
+ | Component | Props | Description |
214
+ |-----------|-------|-------------|
215
+ | `FunctionPage` | `FunctionPageProps` | Function documentation |
216
+ | `ClassPage` | `ClassPageProps` | Class documentation |
217
+ | `InterfacePage` | `InterfacePageProps` | Interface documentation |
218
+ | `EnumPage` | `EnumPageProps` | Enum documentation |
219
+ | `VariablePage` | `VariablePageProps` | Variable documentation |
220
+ | `APIPage` | `APIPageProps` | Full API page wrapper |
221
+
222
+ ## CLI Commands
223
+
224
+ ### `generate`
225
+
226
+ Generate MDX or JSON files from OpenPkg spec.
227
+
228
+ ```bash
229
+ openpkg-docs generate <spec> [options]
230
+
231
+ Options:
232
+ -o, --out <dir> Output directory (default: ./api-docs)
233
+ -f, --format <type> Output format: mdx or json (default: mdx)
234
+ --nav <format> Navigation: fumadocs, docusaurus, generic
235
+ --flat Flat file structure
236
+ --group-by <type> Group by: kind, module, tag, none (default: kind)
237
+ --base-path <path> Base path for links (default: /api)
238
+ --verbose Verbose output
239
+ ```
240
+
241
+ ### `build`
242
+
243
+ Build standalone HTML documentation site.
244
+
245
+ ```bash
246
+ openpkg-docs build <spec> [options]
247
+
248
+ Options:
249
+ -o, --out <dir> Output directory (default: ./docs)
250
+ --title <title> Site title
251
+ --search Enable search index generation
252
+ --verbose Verbose output
253
+ ```
254
+
255
+ ### `dev`
256
+
257
+ Start development server with hot reload.
258
+
259
+ ```bash
260
+ openpkg-docs dev <spec> [options]
261
+
262
+ Options:
263
+ -p, --port <port> Port number (default: 3000)
264
+ --open Open browser automatically
265
+ ```
266
+
267
+ ## Framework Integration
268
+
269
+ ### Fumadocs
270
+
271
+ ```ts
272
+ import { toFumadocsMetaJSON } from '@openpkg-ts/doc-generator'
273
+
274
+ const meta = toFumadocsMetaJSON(spec, { groupBy: 'kind' })
275
+ fs.writeFileSync('docs/api/meta.json', meta)
276
+ ```
277
+
278
+ ### Docusaurus
279
+
280
+ ```ts
281
+ import { toDocusaurusSidebarJS } from '@openpkg-ts/doc-generator'
282
+
283
+ const sidebar = toDocusaurusSidebarJS(spec, { basePath: 'api' })
284
+ fs.writeFileSync('sidebars.js', sidebar)
285
+ ```
286
+
287
+ ## Search Integration
288
+
289
+ ### Pagefind
290
+
291
+ ```ts
292
+ import { toPagefindRecords } from '@openpkg-ts/doc-generator'
293
+
294
+ const records = toPagefindRecords(spec, {
295
+ baseUrl: '/docs/api',
296
+ weights: { name: 10, description: 5 },
297
+ })
298
+ ```
299
+
300
+ ### Algolia
301
+
302
+ ```ts
303
+ import { toAlgoliaRecords } from '@openpkg-ts/doc-generator'
304
+
305
+ const records = toAlgoliaRecords(spec, { baseUrl: '/api' })
306
+ // Upload to Algolia index
307
+ ```
308
+
309
+ ## Migrating from TypeDoc
310
+
311
+ 1. Generate OpenPkg spec using `@openpkg-ts/extract` or `@doccov/sdk`
312
+ 2. Replace TypeDoc config with doc-generator CLI or API
313
+ 3. Use React components for custom rendering
314
+
315
+ | TypeDoc | doc-generator |
316
+ |---------|---------------|
317
+ | `typedoc.json` | `openpkg.json` (spec) |
318
+ | `--out ./docs` | `--out ./docs` |
319
+ | `--theme minimal` | `toHTML()` or React components |
320
+ | Plugin system | Framework adapters |
321
+
322
+ ## License
323
+
324
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createDocs,
4
+ exportToMarkdown,
5
+ toDocusaurusSidebarJS,
6
+ toFumadocsMetaJSON,
7
+ toHTML,
8
+ toJSONString,
9
+ toNavigation,
10
+ toPagefindRecords
11
+ } from "./shared/chunk-7hg53zpt.js";
12
+ import {
13
+ __require,
14
+ __toESM
15
+ } from "./shared/chunk-taeg9090.js";
16
+
17
+ // src/cli.ts
18
+ import { readFileSync } from "node:fs";
19
+ import * as path4 from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { Command } from "commander";
22
+
23
+ // src/cli/build.ts
24
+ import { exec } from "node:child_process";
25
+ import * as fs from "node:fs";
26
+ import * as path from "node:path";
27
+ import { promisify } from "node:util";
28
+ var execAsync = promisify(exec);
29
+ function registerBuildCommand(program) {
30
+ program.command("build <spec>").description("Build static HTML documentation site").option("-o, --out <dir>", "Output directory", "./api-docs").option("-t, --title <title>", "Site title override").option("--no-search", "Disable Pagefind search indexing").option("--verbose", "Verbose output").action(async (specPath, options) => {
31
+ try {
32
+ const resolvedSpec = path.resolve(specPath);
33
+ if (!fs.existsSync(resolvedSpec)) {
34
+ console.error(`Error: Spec file not found: ${resolvedSpec}`);
35
+ process.exit(1);
36
+ }
37
+ const docs = createDocs(resolvedSpec);
38
+ const outDir = path.resolve(options.out);
39
+ const exports = docs.getAllExports();
40
+ if (options.verbose) {
41
+ console.log(`Spec: ${resolvedSpec}`);
42
+ console.log(`Output: ${outDir}`);
43
+ console.log(`Exports: ${exports.length}`);
44
+ }
45
+ fs.mkdirSync(outDir, { recursive: true });
46
+ console.log("Generating HTML...");
47
+ const indexHtml = toHTML(docs.spec, {
48
+ title: options.title,
49
+ includeStyles: true,
50
+ fullDocument: true
51
+ });
52
+ fs.writeFileSync(path.join(outDir, "index.html"), indexHtml);
53
+ const pagesDir = path.join(outDir, "api");
54
+ fs.mkdirSync(pagesDir, { recursive: true });
55
+ for (const exp of exports) {
56
+ const exportHtml = toHTML(docs.spec, {
57
+ export: exp.name,
58
+ title: options.title ? `${exp.name} | ${options.title}` : undefined,
59
+ includeStyles: true,
60
+ fullDocument: true,
61
+ headContent: `<link rel="canonical" href="./api/${slugify(exp.name)}.html">`
62
+ });
63
+ const filename = `${slugify(exp.name)}.html`;
64
+ fs.writeFileSync(path.join(pagesDir, filename), exportHtml);
65
+ if (options.verbose) {
66
+ console.log(` api/${filename}`);
67
+ }
68
+ }
69
+ console.log(`Generated ${exports.length + 1} HTML files`);
70
+ if (options.search !== false) {
71
+ console.log("Building search index...");
72
+ try {
73
+ await execAsync(`npx pagefind --site ${outDir} --output-subdir _pagefind`, {
74
+ cwd: process.cwd()
75
+ });
76
+ console.log("Search index created");
77
+ } catch (err) {
78
+ if (options.verbose) {
79
+ console.log("Pagefind not available, generating search.json fallback");
80
+ }
81
+ const records = toPagefindRecords(docs.spec, {
82
+ baseUrl: "/api"
83
+ });
84
+ fs.writeFileSync(path.join(outDir, "search.json"), JSON.stringify(records, null, 2));
85
+ console.log("Generated search.json fallback");
86
+ }
87
+ }
88
+ console.log(`
89
+ Build complete: ${outDir}`);
90
+ console.log(`
91
+ To serve locally:`);
92
+ console.log(` npx serve ${outDir}`);
93
+ } catch (err) {
94
+ console.error("Error:", err instanceof Error ? err.message : err);
95
+ process.exit(1);
96
+ }
97
+ });
98
+ }
99
+ function slugify(name) {
100
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
101
+ }
102
+
103
+ // src/cli/dev.ts
104
+ import * as fs2 from "node:fs";
105
+ import * as http from "node:http";
106
+ import * as path2 from "node:path";
107
+ function registerDevCommand(program) {
108
+ program.command("dev <spec>").description("Start dev server with hot reload").option("-p, --port <port>", "Port to serve on", "3001").option("-h, --host <host>", "Host to bind to", "localhost").option("--open", "Open browser on start").option("--verbose", "Verbose output").action(async (specPath, options) => {
109
+ try {
110
+ const resolvedSpec = path2.resolve(specPath);
111
+ if (!fs2.existsSync(resolvedSpec)) {
112
+ console.error(`Error: Spec file not found: ${resolvedSpec}`);
113
+ process.exit(1);
114
+ }
115
+ const port = Number.parseInt(options.port, 10);
116
+ const host = options.host ?? "localhost";
117
+ let docs = createDocs(resolvedSpec);
118
+ let lastMtime = fs2.statSync(resolvedSpec).mtimeMs;
119
+ console.log(`
120
+ Starting dev server...`);
121
+ console.log(` Spec: ${resolvedSpec}`);
122
+ console.log(` Exports: ${docs.getAllExports().length}`);
123
+ const watcher = fs2.watch(resolvedSpec, (eventType) => {
124
+ if (eventType === "change") {
125
+ const currentMtime = fs2.statSync(resolvedSpec).mtimeMs;
126
+ if (currentMtime !== lastMtime) {
127
+ lastMtime = currentMtime;
128
+ try {
129
+ docs = createDocs(resolvedSpec);
130
+ console.log(`
131
+ [${new Date().toLocaleTimeString()}] Spec reloaded (${docs.getAllExports().length} exports)`);
132
+ } catch (err) {
133
+ console.error(`
134
+ [${new Date().toLocaleTimeString()}] Error reloading spec:`, err instanceof Error ? err.message : err);
135
+ }
136
+ }
137
+ }
138
+ });
139
+ const server = http.createServer((req, res) => {
140
+ const url = new URL(req.url || "/", `http://${host}:${port}`);
141
+ const pathname = url.pathname;
142
+ if (options.verbose) {
143
+ console.log(`${req.method} ${pathname}`);
144
+ }
145
+ try {
146
+ if (pathname.startsWith("/api/")) {
147
+ const exportName = pathname.replace("/api/", "").replace(".html", "").replace(/-/g, "");
148
+ const exp = findExport(docs, exportName);
149
+ if (exp) {
150
+ const html = toHTML(docs.spec, {
151
+ export: exp.name,
152
+ includeStyles: true,
153
+ fullDocument: true,
154
+ headContent: hotReloadScript(port)
155
+ });
156
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
157
+ res.end(html);
158
+ return;
159
+ }
160
+ }
161
+ if (pathname === "/" || pathname === "/index.html") {
162
+ const html = toHTML(docs.spec, {
163
+ includeStyles: true,
164
+ fullDocument: true,
165
+ headContent: hotReloadScript(port)
166
+ });
167
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
168
+ res.end(html);
169
+ return;
170
+ }
171
+ if (pathname === "/__reload") {
172
+ res.writeHead(200, {
173
+ "Content-Type": "text/event-stream",
174
+ "Cache-Control": "no-cache",
175
+ Connection: "keep-alive"
176
+ });
177
+ const interval = setInterval(() => {
178
+ res.write(`data: ping
179
+
180
+ `);
181
+ }, 2000);
182
+ const reloadWatcher = fs2.watch(resolvedSpec, () => {
183
+ res.write(`data: reload
184
+
185
+ `);
186
+ });
187
+ req.on("close", () => {
188
+ clearInterval(interval);
189
+ reloadWatcher.close();
190
+ });
191
+ return;
192
+ }
193
+ res.writeHead(404, { "Content-Type": "text/html" });
194
+ res.end("<h1>404 Not Found</h1>");
195
+ } catch (err) {
196
+ console.error("Server error:", err);
197
+ res.writeHead(500, { "Content-Type": "text/html" });
198
+ res.end(`<h1>500 Server Error</h1><pre>${err instanceof Error ? err.message : err}</pre>`);
199
+ }
200
+ });
201
+ server.listen(port, host, () => {
202
+ console.log(`
203
+ Local: http://${host}:${port}/`);
204
+ console.log(`
205
+ Watching for spec changes...`);
206
+ console.log(` Press Ctrl+C to stop
207
+ `);
208
+ if (options.open) {
209
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
210
+ import("node:child_process").then(({ exec: exec2 }) => {
211
+ exec2(`${openCmd} http://${host}:${port}/`);
212
+ });
213
+ }
214
+ });
215
+ process.on("SIGINT", () => {
216
+ console.log(`
217
+
218
+ Shutting down...`);
219
+ watcher.close();
220
+ server.close();
221
+ process.exit(0);
222
+ });
223
+ } catch (err) {
224
+ console.error("Error:", err instanceof Error ? err.message : err);
225
+ process.exit(1);
226
+ }
227
+ });
228
+ }
229
+ function findExport(docs, searchName) {
230
+ const lowerSearch = searchName.toLowerCase();
231
+ return docs.getAllExports().find((exp) => {
232
+ const lowerName = exp.name.toLowerCase().replace(/[^a-z0-9]/g, "");
233
+ return lowerName === lowerSearch || exp.id === searchName;
234
+ });
235
+ }
236
+ function hotReloadScript(port) {
237
+ return `
238
+ <script>
239
+ (function() {
240
+ const source = new EventSource('http://localhost:${port}/__reload');
241
+ source.onmessage = function(e) {
242
+ if (e.data === 'reload') {
243
+ window.location.reload();
244
+ }
245
+ };
246
+ source.onerror = function() {
247
+ source.close();
248
+ setTimeout(() => window.location.reload(), 1000);
249
+ };
250
+ })();
251
+ </script>`;
252
+ }
253
+
254
+ // src/cli/generate.ts
255
+ import * as fs3 from "node:fs";
256
+ import * as path3 from "node:path";
257
+ function slugify2(name) {
258
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
259
+ }
260
+ function registerGenerateCommand(program) {
261
+ program.command("generate <spec>").description("Generate MDX or JSON files from OpenPkg spec").option("-o, --out <dir>", "Output directory", "./api-docs").option("-f, --format <format>", "Output format: mdx or json", "mdx").option("--nav <format>", "Navigation format: fumadocs, docusaurus, generic").option("--flat", "Flat file structure (no grouping folders)").option("--group-by <groupBy>", "Group by: kind, module, tag, none", "kind").option("--base-path <path>", "Base path for navigation links", "/api").option("--verbose", "Verbose output").action(async (specPath, options) => {
262
+ try {
263
+ const resolvedSpec = path3.resolve(specPath);
264
+ if (!fs3.existsSync(resolvedSpec)) {
265
+ console.error(`Error: Spec file not found: ${resolvedSpec}`);
266
+ process.exit(1);
267
+ }
268
+ const docs = createDocs(resolvedSpec);
269
+ const exports = docs.getAllExports();
270
+ const outDir = path3.resolve(options.out);
271
+ if (options.verbose) {
272
+ console.log(`Spec: ${resolvedSpec}`);
273
+ console.log(`Output: ${outDir}`);
274
+ console.log(`Format: ${options.format}`);
275
+ console.log(`Exports: ${exports.length}`);
276
+ }
277
+ fs3.mkdirSync(outDir, { recursive: true });
278
+ if (options.format === "json") {
279
+ const jsonContent = toJSONString(docs.spec, { pretty: true });
280
+ const jsonPath = path3.join(outDir, "api.json");
281
+ fs3.writeFileSync(jsonPath, jsonContent);
282
+ console.log(`Generated ${jsonPath}`);
283
+ } else {
284
+ const groupBy = options.groupBy ?? "kind";
285
+ const isFlat = options.flat ?? false;
286
+ const groups = new Map;
287
+ for (const exp of exports) {
288
+ let groupKey;
289
+ if (isFlat || groupBy === "none") {
290
+ groupKey = "";
291
+ } else if (groupBy === "kind") {
292
+ groupKey = `${exp.kind}s`;
293
+ } else if (groupBy === "module") {
294
+ groupKey = extractModule(exp.source?.file) || "core";
295
+ } else if (groupBy === "tag") {
296
+ const categoryTag = exp.tags?.find((t) => t.name === "category" || t.name === "@category");
297
+ groupKey = categoryTag?.text || "other";
298
+ } else {
299
+ groupKey = "";
300
+ }
301
+ const existing = groups.get(groupKey) ?? [];
302
+ existing.push(exp);
303
+ groups.set(groupKey, existing);
304
+ }
305
+ let fileCount = 0;
306
+ for (const [group, groupExports] of groups) {
307
+ const groupDir = group ? path3.join(outDir, slugify2(group)) : outDir;
308
+ if (group) {
309
+ fs3.mkdirSync(groupDir, { recursive: true });
310
+ }
311
+ for (const exp of groupExports) {
312
+ const mdx = exportToMarkdown(exp, {
313
+ frontmatter: true,
314
+ codeSignatures: true
315
+ });
316
+ const filename = `${slugify2(exp.name)}.mdx`;
317
+ const filePath = path3.join(groupDir, filename);
318
+ fs3.writeFileSync(filePath, mdx);
319
+ fileCount++;
320
+ if (options.verbose) {
321
+ console.log(` ${group ? `${group}/` : ""}${filename}`);
322
+ }
323
+ }
324
+ }
325
+ console.log(`Generated ${fileCount} MDX files in ${outDir}`);
326
+ }
327
+ if (options.nav) {
328
+ const basePath = options.basePath ?? "/api";
329
+ const navOptions = {
330
+ format: options.nav,
331
+ groupBy: options.groupBy ?? "kind",
332
+ basePath
333
+ };
334
+ let navContent;
335
+ let navFilename;
336
+ switch (options.nav) {
337
+ case "fumadocs":
338
+ navContent = toFumadocsMetaJSON(docs.spec, navOptions);
339
+ navFilename = "meta.json";
340
+ break;
341
+ case "docusaurus":
342
+ navContent = toDocusaurusSidebarJS(docs.spec, navOptions);
343
+ navFilename = "sidebars.js";
344
+ break;
345
+ default:
346
+ navContent = JSON.stringify(toNavigation(docs.spec, navOptions), null, 2);
347
+ navFilename = "nav.json";
348
+ break;
349
+ }
350
+ const navPath = path3.join(outDir, navFilename);
351
+ fs3.writeFileSync(navPath, navContent);
352
+ console.log(`Generated ${navPath}`);
353
+ }
354
+ } catch (err) {
355
+ console.error("Error:", err instanceof Error ? err.message : err);
356
+ process.exit(1);
357
+ }
358
+ });
359
+ }
360
+ function extractModule(filePath) {
361
+ if (!filePath)
362
+ return;
363
+ const parts = filePath.split("/");
364
+ const lastPart = parts[parts.length - 1];
365
+ if (lastPart === "index.ts" || lastPart === "index.tsx") {
366
+ return parts[parts.length - 2];
367
+ }
368
+ return lastPart.replace(/\.[jt]sx?$/, "");
369
+ }
370
+
371
+ // src/cli.ts
372
+ var __filename2 = fileURLToPath(import.meta.url);
373
+ var __dirname2 = path4.dirname(__filename2);
374
+ var version = "0.0.1";
375
+ try {
376
+ const packageJson = JSON.parse(readFileSync(path4.join(__dirname2, "../package.json"), "utf-8"));
377
+ version = packageJson.version;
378
+ } catch {}
379
+ var program = new Command;
380
+ program.name("openpkg-docs").description("Generate API documentation from OpenPkg specs").version(version).option("-c, --config <path>", "Config file path").option("-v, --verbose", "Verbose output");
381
+ registerBuildCommand(program);
382
+ registerGenerateCommand(program);
383
+ registerDevCommand(program);
384
+ program.command("*", { hidden: true }).action(() => {
385
+ program.outputHelp();
386
+ });
387
+ program.parseAsync().catch(() => {
388
+ process.exit(1);
389
+ });