@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 +324 -0
- package/dist/cli.js +389 -0
- package/dist/index.d.ts +827 -0
- package/dist/index.js +79 -0
- package/dist/react-styled.d.ts +588 -0
- package/dist/react-styled.js +766 -0
- package/dist/react.d.ts +221 -0
- package/dist/react.js +35 -0
- package/dist/shared/chunk-7hg53zpt.js +1160 -0
- package/dist/shared/chunk-e5fkh3kh.js +679 -0
- package/dist/shared/chunk-taeg9090.js +171 -0
- package/package.json +84 -0
- package/templates/embedded/README.md +81 -0
- package/templates/embedded/docusaurus/sidebars.js +38 -0
- package/templates/embedded/fumadocs/meta.json +26 -0
- package/templates/standalone/index.html +34 -0
- package/templates/standalone/package.json +17 -0
- package/templates/standalone/src/main.ts +223 -0
- package/templates/standalone/src/styles.css +43 -0
- package/templates/standalone/vite.config.ts +9 -0
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
|
+
});
|