vitepress-api-references 0.0.0 → 0.2.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 CHANGED
@@ -2,9 +2,83 @@
2
2
 
3
3
  [![npm][npm-src]][npm-href] [![CI][ci-src]][ci-href]
4
4
 
5
- > [!WARNING] WIP
5
+ Enable JSDoc API Reference for VitePress.
6
6
 
7
- Enable JSDoc API Reference.
7
+ ## Usage
8
+
9
+ ### VitePress integration
10
+
11
+ This repository uses the VitePress integration to generate and serve its own API reference docs. The generated API reference site is published at <https://kazupon.github.io/vitepress-api-references/>. See [`docs/.vitepress/config.ts`](./docs/.vitepress/config.ts) and the generated [`docs/api`](./docs/api) directory for a working self-hosted example.
12
+
13
+ ```ts
14
+ import { defineConfig } from 'vitepress'
15
+ import { withOxContentApiDocs } from 'vitepress-api-references'
16
+
17
+ export default defineConfig(
18
+ await withOxContentApiDocs({
19
+ themeConfig: {
20
+ sidebar: [{ text: 'Guide', link: '/' }, { text: 'API References' }]
21
+ },
22
+ apiDocs: {
23
+ entryPoints: [{ path: 'src/index.ts' }],
24
+ outDir: 'docs/api',
25
+ basePath: '/api',
26
+ markdown: {
27
+ pathStrategy: 'typedoc',
28
+ singleEntryRoot: 'flatten',
29
+ renderStyle: 'markdown',
30
+ indexFormat: 'table'
31
+ },
32
+ nav: {
33
+ section: { text: 'API References' },
34
+ collapsed: true,
35
+ insert: 'replace',
36
+ replaceText: 'API References'
37
+ }
38
+ }
39
+ })
40
+ )
41
+ ```
42
+
43
+ ### Standalone markdown generation
44
+
45
+ Use `generateOxContentApiDocs` when you want to generate markdown files directly without wiring the result into VitePress. See [`standalone/generate.ts`](./standalone/generate.ts) for a runnable example.
46
+
47
+ ```ts
48
+ import { generateOxContentApiDocs } from 'vitepress-api-references'
49
+
50
+ const result = await generateOxContentApiDocs({
51
+ entryPoints: [{ path: 'src/index.ts', name: 'main' }],
52
+ outDir: 'standalone',
53
+ basePath: '/standalone',
54
+ markdown: {
55
+ pathStrategy: 'typedoc',
56
+ renderStyle: 'markdown',
57
+ indexFormat: 'table'
58
+ }
59
+ })
60
+
61
+ console.log(`Generated ${result.generatedFiles.length} files`)
62
+ ```
63
+
64
+ Run the example with:
65
+
66
+ ```sh
67
+ vp run docs:standalone
68
+ ```
69
+
70
+ ## Local API reference docs
71
+
72
+ This repository uses its own public root API as the docs source under `docs/`.
73
+
74
+ ```sh
75
+ vp run docs:build
76
+ vp run docs:dev
77
+ ```
78
+
79
+ ## Credits
80
+
81
+ Built on top of [Ox Content](https://github.com/ubugeeei-prod/ox-content), which extracts JSDoc API metadata and renders the generated markdown.
8
82
 
9
83
  ## License
10
84
 
package/dist/index.d.mts CHANGED
@@ -1,9 +1,395 @@
1
- //#region src/index.d.ts
1
+ import { JsDocsMarkdownModule, JsDocsMarkdownOptions, JsEntryPointDocsOptions, JsEntryPointSpec, JsExternalPackageSource } from "@ox-content/napi";
2
+ import { UserConfig } from "vitepress";
3
+
4
+ //#region src/types.d.ts
5
+ /**
6
+ * @author kazuya kawaguchi (a.k.a. kazupon)
7
+ * @license MIT
8
+ */
9
+ type EntryPointInput = string | {
10
+ path: string | URL;
11
+ name?: string;
12
+ };
13
+ interface ExternalPackageSourceInput {
14
+ /** Package name whose exported docs can be linked from generated references. */
15
+ package: string;
16
+ /** Entry point file or URL used to extract metadata for the external package. */
17
+ entry: string | URL;
18
+ }
19
+ interface VitePressSidebarSectionOptions {
20
+ /** Label shown for the generated top-level sidebar section. */
21
+ text: string;
22
+ /**
23
+ * Whether the generated top-level sidebar section starts collapsed in VitePress.
24
+ * When omitted, a boolean `nav.collapsed` value is used.
25
+ *
26
+ * @default undefined
27
+ */
28
+ collapsed?: boolean;
29
+ }
30
+ interface ResolvedOxContentExtractionOptions extends Omit<JsEntryPointDocsOptions, 'root' | 'tsconfig'> {
31
+ /** External package sources converted to the structure expected by ox-content. */
32
+ externalPackageSources?: JsExternalPackageSource[];
33
+ }
34
+ /** Controls which declarations and metadata ox-content extracts from entry points. */
35
+ interface OxContentExtractionOptions {
36
+ /**
37
+ * Include declarations marked as private.
38
+ *
39
+ * @default false
40
+ */
41
+ private?: boolean;
42
+ /**
43
+ * Include declarations marked as internal.
44
+ *
45
+ * @default false
46
+ */
47
+ internal?: boolean;
48
+ /**
49
+ * Preserve external documentation links and metadata from source declarations.
50
+ *
51
+ * @default false
52
+ */
53
+ externalDocs?: boolean;
54
+ /**
55
+ * Include generic type parameter declarations and descriptions in generated docs.
56
+ *
57
+ * @default false
58
+ */
59
+ typeParameters?: boolean;
60
+ /**
61
+ * External packages used to resolve cross-package references in generated docs.
62
+ *
63
+ * @default undefined
64
+ */
65
+ externalPackageSources?: ExternalPackageSourceInput[];
66
+ }
67
+ /** Markdown rendering options forwarded to ox-content after project defaults are applied. */
68
+ interface OxContentMarkdownOptions extends Omit<JsDocsMarkdownOptions, 'basePath' | 'githubUrl'> {}
69
+ type VitePressSidebarInsert = 'append' | 'prepend' | 'replace' | ((sidebar: unknown, generated: VitePressSidebarItem) => unknown);
70
+ /** Configures generated VitePress sidebar data and optional navigation artifacts. */
71
+ interface VitePressNavOptions {
72
+ /**
73
+ * Whether generated navigation integration is enabled.
74
+ *
75
+ * @default true
76
+ */
77
+ enabled?: boolean;
78
+ /**
79
+ * Optional top-level sidebar section that wraps all generated API doc items.
80
+ *
81
+ * @default undefined
82
+ */
83
+ section?: VitePressSidebarSectionOptions;
84
+ /**
85
+ * Positioning strategy used when merging generated items into an existing sidebar.
86
+ *
87
+ * @default 'append'
88
+ */
89
+ insert?: VitePressSidebarInsert;
90
+ /**
91
+ * Sidebar item text to replace when `insert` is set to `replace`.
92
+ *
93
+ * @default undefined
94
+ */
95
+ replaceText?: string;
96
+ /**
97
+ * Route key to update when the existing VitePress sidebar is a route map.
98
+ *
99
+ * @default undefined
100
+ */
101
+ sidebarRoute?: string;
102
+ /**
103
+ * Collapsed state for generated sidebar branches, or a resolver called per item.
104
+ * A boolean value also controls the top-level section when `section.collapsed` is omitted.
105
+ *
106
+ * @default undefined
107
+ */
108
+ collapsed?: boolean | ((item: ApiDocsNavItem, depth: number) => boolean | undefined);
109
+ /**
110
+ * File path for generated navigation code, or `false` to skip writing it.
111
+ *
112
+ * @default undefined
113
+ */
114
+ outputFile?: string | false;
115
+ /**
116
+ * Virtual module id that exposes generated navigation data, or `false` to disable it.
117
+ *
118
+ * @default false
119
+ */
120
+ virtualModule?: string | false;
121
+ /**
122
+ * Named export used in the generated navigation code file.
123
+ *
124
+ * @default 'apiDocsNav'
125
+ */
126
+ exportName?: string;
127
+ }
128
+ /** User-facing options for generating API reference markdown and VitePress integration. */
129
+ interface OxContentApiDocsOptions {
130
+ /**
131
+ * Project root used to resolve relative paths.
132
+ *
133
+ * @default process.cwd()
134
+ */
135
+ root?: string | URL;
136
+ /**
137
+ * TypeScript configuration file used for declaration extraction.
138
+ *
139
+ * @default undefined
140
+ */
141
+ tsconfig?: string | URL;
142
+ /** Source entry points whose exported declarations become API docs pages. */
143
+ entryPoints: EntryPointInput[];
144
+ /** Directory where generated markdown and optional artifacts are written. */
145
+ outDir: string | URL;
146
+ /** Base route used when generating links between API docs pages. */
147
+ basePath: string;
148
+ /**
149
+ * GitHub repository URL used to generate source links for declarations.
150
+ *
151
+ * @default undefined
152
+ */
153
+ githubUrl?: string;
154
+ /**
155
+ * Whether generated output should be cleaned before writing.
156
+ *
157
+ * @default undefined
158
+ */
159
+ clean?: boolean;
160
+ /**
161
+ * Whether generated files and artifacts are written to disk.
162
+ *
163
+ * @default true
164
+ */
165
+ write?: boolean;
166
+ /**
167
+ * Controls which declarations and metadata are extracted from entry points.
168
+ *
169
+ * @default {}
170
+ */
171
+ extraction?: OxContentExtractionOptions;
172
+ /**
173
+ * Controls markdown page generation, grouping, sorting, and rendering details.
174
+ *
175
+ * @default {}
176
+ */
177
+ markdown?: OxContentMarkdownOptions;
178
+ /**
179
+ * Controls generated VitePress sidebar integration and optional nav artifacts.
180
+ *
181
+ * @default { enabled: true, insert: 'append', virtualModule: false }
182
+ */
183
+ nav?: VitePressNavOptions;
184
+ /**
185
+ * Whether to write docs JSON, or the output path to write it to.
186
+ *
187
+ * @default undefined
188
+ */
189
+ docsJson?: boolean | string;
190
+ /**
191
+ * Escape `<` and `>` in markdown heading lines to avoid HTML parsing in VitePress.
192
+ *
193
+ * @default false
194
+ */
195
+ escapeHeadingAngleBrackets?: boolean;
196
+ }
197
+ /** API docs options after defaults are applied and all paths are normalized. */
198
+ interface ResolvedOxContentApiDocsOptions extends Omit<OxContentApiDocsOptions, 'root' | 'tsconfig' | 'entryPoints' | 'outDir' | 'extraction' | 'nav'> {
199
+ /** Absolute project root used as the base for all relative inputs. */
200
+ root: string;
201
+ /**
202
+ * Absolute TypeScript configuration path, when configured.
203
+ *
204
+ * @default undefined
205
+ */
206
+ tsconfig?: string;
207
+ /** Entry point specs with paths resolved to absolute filesystem paths. */
208
+ entryPoints: JsEntryPointSpec[];
209
+ /** Absolute output directory for generated markdown and optional artifacts. */
210
+ outDir: string;
211
+ /** Extraction options with defaults applied and external package paths resolved. */
212
+ extraction: ResolvedOxContentExtractionOptions;
213
+ /** VitePress navigation options with default integration settings applied. */
214
+ nav: Required<Pick<VitePressNavOptions, 'enabled' | 'insert' | 'virtualModule'>> & Omit<VitePressNavOptions, 'enabled' | 'insert' | 'virtualModule'>;
215
+ }
216
+ /** Navigation item generated from API docs metadata. */
217
+ interface ApiDocsNavItem {
218
+ /** Human-readable title shown in generated navigation and sidebar items. */
219
+ title: string;
220
+ /** VitePress link path for the generated API docs page. */
221
+ path: string;
222
+ /** Nested navigation items for grouped modules or declaration hierarchies. */
223
+ children?: ApiDocsNavItem[];
224
+ }
225
+ /** Minimal VitePress sidebar item shape used by generated API docs. */
226
+ interface VitePressSidebarItem {
227
+ /** Text label displayed in the VitePress sidebar. */
228
+ text?: string;
229
+ /** VitePress route path opened when the sidebar item is selected. */
230
+ link?: string;
231
+ /** Whether this item's nested children start collapsed. */
232
+ collapsed?: boolean;
233
+ /** Nested sidebar items displayed below this item. */
234
+ items?: VitePressSidebarItem[];
235
+ }
236
+ /** Options for merging generated sidebar items into an existing VitePress sidebar. */
237
+ interface MergeVitePressSidebarOptions {
238
+ /** Positioning strategy used when inserting generated items into the sidebar. */
239
+ insert?: VitePressSidebarInsert;
240
+ /** Existing sidebar item text to replace when `insert` is set to `replace`. */
241
+ replaceText?: string;
242
+ /** Route key to update when the existing VitePress sidebar is a route map. */
243
+ sidebarRoute?: string;
244
+ }
245
+ /** Result produced by API docs generation. */
246
+ interface OxContentApiDocsResult {
247
+ /** Generated markdown and artifact contents keyed by relative output path. */
248
+ files: Record<string, string>;
249
+ /** Navigation metadata derived from generated docs. */
250
+ nav: ApiDocsNavItem[];
251
+ /** Markdown module data generated from extracted declarations. */
252
+ docs: JsDocsMarkdownModule[];
253
+ /** Absolute paths for files written to disk or planned when `write` is `false`. */
254
+ generatedFiles: string[];
255
+ /** Diagnostic messages reported while extracting docs. */
256
+ diagnostics: string[];
257
+ /** Hash representing the current generation options and watched input files. */
258
+ hash: string;
259
+ /** Normalized options used for this generation run. */
260
+ resolvedOptions: ResolvedOxContentApiDocsOptions;
261
+ }
262
+ //#endregion
263
+ //#region src/generate.d.ts
264
+ /**
265
+ * Generates API reference markdown, navigation metadata, and optional artifacts.
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * import { generateOxContentApiDocs } from 'vitepress-api-references'
270
+ *
271
+ * const result = await generateOxContentApiDocs({
272
+ * entryPoints: ['src/index.ts'],
273
+ * outDir: 'docs/api',
274
+ * basePath: '/api'
275
+ * })
276
+ *
277
+ * console.log(result.generatedFiles)
278
+ * ```
279
+ *
280
+ * @param options - API docs generation options.
281
+ * @returns Generated API docs files, metadata, diagnostics, and resolved options.
282
+ */
283
+ declare function generateOxContentApiDocs(options: OxContentApiDocsOptions): Promise<OxContentApiDocsResult>;
284
+ //#endregion
285
+ //#region src/vitepress.d.ts
286
+ declare module 'vitepress' {
287
+ interface UserConfig {
288
+ apiDocs?: OxContentApiDocsOptions | false;
289
+ }
290
+ }
2
291
  /**
3
- * vitepress api references entry point
292
+ * Adds generated API docs pages, sidebar data, and watch support to a VitePress config.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * import { defineConfig } from 'vitepress'
297
+ * import { withOxContentApiDocs } from 'vitepress-api-references'
298
+ *
299
+ * export default defineConfig(
300
+ * await withOxContentApiDocs({
301
+ * title: 'My Library',
302
+ * themeConfig: {
303
+ * sidebar: [{ text: 'Guide', link: '/' }, { text: 'API Reference' }]
304
+ * },
305
+ * apiDocs: {
306
+ * entryPoints: [{ path: 'src/index.ts', name: 'default' }],
307
+ * outDir: 'docs/api',
308
+ * basePath: '/api',
309
+ * nav: {
310
+ * section: { text: 'API Reference', collapsed: false },
311
+ * insert: 'replace',
312
+ * replaceText: 'API Reference'
313
+ * }
314
+ * }
315
+ * })
316
+ * )
317
+ * ```
4
318
  *
5
- * @module default
319
+ * @param config - VitePress user configuration.
320
+ * @param override - Optional API docs options applied over the configured options.
321
+ * @returns Updated VitePress user configuration.
6
322
  */
7
- declare function fn(): string;
323
+ declare function withOxContentApiDocs(config: UserConfig, override?: OxContentApiDocsOptions): Promise<UserConfig>;
324
+ //#endregion
325
+ //#region src/sidebar.d.ts
326
+ /**
327
+ * Converts API docs navigation metadata into VitePress sidebar items.
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * import { toVitePressSidebarItems } from 'vitepress-api-references'
332
+ *
333
+ * const sidebarItems = toVitePressSidebarItems([
334
+ * {
335
+ * title: 'API',
336
+ * path: '/api/',
337
+ * children: [{ title: 'withOxContentApiDocs', path: '/api/with-ox-content-api-docs' }]
338
+ * }
339
+ * ])
340
+ * ```
341
+ *
342
+ * @param nav - API docs navigation metadata.
343
+ * @param options - VitePress navigation options.
344
+ * @returns Generated VitePress sidebar items.
345
+ */
346
+ declare function toVitePressSidebarItems(nav: ApiDocsNavItem[], options?: VitePressNavOptions): VitePressSidebarItem[];
347
+ /**
348
+ * Creates a VitePress sidebar section from generated API docs navigation.
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * import { createVitePressSidebarSection } from 'vitepress-api-references'
353
+ *
354
+ * const apiSection = createVitePressSidebarSection(apiDocsNav, {
355
+ * section: {
356
+ * text: 'API Reference',
357
+ * collapsed: false
358
+ * }
359
+ * })
360
+ * ```
361
+ *
362
+ * @param nav - API docs navigation metadata.
363
+ * @param options - VitePress navigation options.
364
+ * @returns Generated VitePress sidebar section.
365
+ */
366
+ declare function createVitePressSidebarSection(nav: ApiDocsNavItem[], options?: VitePressNavOptions): VitePressSidebarItem;
367
+ /**
368
+ * Merges generated API docs sidebar data into an existing VitePress sidebar.
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * import { mergeVitePressSidebar } from 'vitepress-api-references'
373
+ *
374
+ * const sidebar = mergeVitePressSidebar(
375
+ * [{ text: 'Guide', link: '/guide/' }],
376
+ * { text: 'API Reference', items: [{ text: 'Config', link: '/api/config' }] },
377
+ * { insert: 'append' }
378
+ * )
379
+ * ```
380
+ *
381
+ * @param sidebar - Existing VitePress sidebar configuration.
382
+ * @param generated - Generated API docs sidebar section.
383
+ * @param options - Sidebar merge options.
384
+ * @returns Updated VitePress sidebar configuration.
385
+ */
386
+ declare function mergeVitePressSidebar(sidebar: unknown, generated: VitePressSidebarItem, options?: MergeVitePressSidebarOptions): unknown;
387
+ //#endregion
388
+ //#region src/index.d.ts
389
+ /**
390
+ * @author kazuya kawaguchi (a.k.a. kazupon)
391
+ * @license MIT
392
+ */
393
+
8
394
  //#endregion
9
- export { fn };
395
+ export { type ApiDocsNavItem, type MergeVitePressSidebarOptions, type OxContentApiDocsOptions, type OxContentApiDocsResult, type OxContentExtractionOptions, type OxContentMarkdownOptions, type ResolvedOxContentApiDocsOptions, type VitePressNavOptions, type VitePressSidebarItem, createVitePressSidebarSection, generateOxContentApiDocs, mergeVitePressSidebar, toVitePressSidebarItems, withOxContentApiDocs };
package/dist/index.mjs CHANGED
@@ -1,18 +1,567 @@
1
- import { createDebug } from "obug";
2
- //#region src/index.ts
1
+ import path from "node:path";
2
+ import { extractDocsFromEntryPoints, generateDocsDataJson, generateDocsMarkdown, generateDocsNavCode, generateDocsNavMetadataFromDocs } from "@ox-content/napi";
3
+ import fs from "node:fs/promises";
4
+ import { createHash } from "node:crypto";
5
+ import { fileURLToPath } from "node:url";
6
+ //#region src/files.ts
7
+ /**
8
+ * @author kazuya kawaguchi (a.k.a. kazupon)
9
+ * @license MIT
10
+ */
11
+ /**
12
+ * Writes generated content only when the target file content changes.
13
+ *
14
+ * @param filePath - Output file path.
15
+ * @param content - File content to write.
16
+ * @returns Whether the file was written.
17
+ */
18
+ async function writeGeneratedFile(filePath, content) {
19
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
20
+ try {
21
+ if (await fs.readFile(filePath, "utf8") === content) return false;
22
+ } catch (error) {
23
+ if (!isNotFoundError(error)) throw error;
24
+ }
25
+ await fs.writeFile(filePath, content);
26
+ return true;
27
+ }
28
+ /**
29
+ * Writes multiple generated files into an output directory.
30
+ *
31
+ * @param files - Generated file contents keyed by relative path.
32
+ * @param outDir - Output directory used to resolve relative paths.
33
+ * @returns Absolute paths for generated files.
34
+ */
35
+ async function writeGeneratedFiles(files, outDir) {
36
+ const generatedFiles = [];
37
+ for (const [relativePath, content] of Object.entries(files)) {
38
+ const filePath = path.resolve(outDir, relativePath);
39
+ await writeGeneratedFile(filePath, content);
40
+ generatedFiles.push(filePath);
41
+ }
42
+ return generatedFiles;
43
+ }
44
+ function isNotFoundError(error) {
45
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
46
+ }
47
+ //#endregion
48
+ //#region src/hash.ts
49
+ /**
50
+ * @author kazuya kawaguchi (a.k.a. kazupon)
51
+ * @license MIT
52
+ */
53
+ /**
54
+ * Creates a stable hash for API docs generation inputs.
55
+ *
56
+ * @param options - Resolved API docs options.
57
+ * @returns Hash string for the current generation inputs.
58
+ */
59
+ async function createGenerationHash(options) {
60
+ const hash = createHash("sha256");
61
+ hash.update(JSON.stringify(toHashableOptions(options)));
62
+ for (const filePath of collectInputFiles(options)) {
63
+ hash.update(filePath);
64
+ hash.update(await statHash(filePath));
65
+ }
66
+ return hash.digest("hex");
67
+ }
68
+ function toHashableOptions(options) {
69
+ return {
70
+ root: options.root,
71
+ tsconfig: options.tsconfig,
72
+ entryPoints: options.entryPoints,
73
+ outDir: options.outDir,
74
+ basePath: options.basePath,
75
+ githubUrl: options.githubUrl,
76
+ extraction: options.extraction,
77
+ markdown: options.markdown,
78
+ docsJson: options.docsJson,
79
+ escapeHeadingAngleBrackets: options.escapeHeadingAngleBrackets
80
+ };
81
+ }
82
+ function collectInputFiles(options) {
83
+ return [
84
+ ...options.entryPoints.map((entry) => entry.path),
85
+ ...options.tsconfig ? [options.tsconfig] : [],
86
+ ...options.extraction.externalPackageSources?.map((source) => source.entry) ?? []
87
+ ];
88
+ }
89
+ async function statHash(filePath) {
90
+ try {
91
+ const stat = await fs.stat(filePath);
92
+ return `${stat.mtimeMs}:${stat.size}`;
93
+ } catch {
94
+ return "missing";
95
+ }
96
+ }
97
+ //#endregion
98
+ //#region src/options.ts
3
99
  /**
4
- * vitepress api references entry point
100
+ * @author kazuya kawaguchi (a.k.a. kazupon)
101
+ * @license MIT
102
+ */
103
+ /**
104
+ * Resolves user API docs options into absolute paths and default values.
105
+ *
106
+ * @param options - User-facing API docs options.
107
+ * @returns Resolved API docs options.
108
+ */
109
+ function resolveApiDocsOptions(options) {
110
+ const root = toAbsolutePath(options.root ?? process.cwd(), process.cwd());
111
+ const outDir = toAbsolutePath(options.outDir, root);
112
+ const tsconfig = options.tsconfig ? toAbsolutePath(options.tsconfig, root) : void 0;
113
+ const extraction = options.extraction ?? {};
114
+ return {
115
+ ...options,
116
+ root,
117
+ tsconfig,
118
+ outDir,
119
+ basePath: normalizeBasePath(options.basePath),
120
+ entryPoints: options.entryPoints.map((entry) => {
121
+ if (typeof entry === "string") return { path: toAbsolutePath(entry, root) };
122
+ return {
123
+ path: toAbsolutePath(entry.path, root),
124
+ name: entry.name
125
+ };
126
+ }),
127
+ extraction: {
128
+ private: extraction.private ?? false,
129
+ internal: extraction.internal ?? false,
130
+ externalDocs: extraction.externalDocs ?? false,
131
+ typeParameters: extraction.typeParameters ?? false,
132
+ externalPackageSources: extraction.externalPackageSources?.map((source) => ({
133
+ package: source.package,
134
+ entry: toAbsolutePath(source.entry, root)
135
+ }))
136
+ },
137
+ markdown: options.markdown ?? {},
138
+ nav: {
139
+ enabled: options.nav?.enabled ?? true,
140
+ insert: options.nav?.insert ?? "append",
141
+ virtualModule: options.nav?.virtualModule ?? false,
142
+ ...options.nav
143
+ },
144
+ write: options.write ?? true,
145
+ escapeHeadingAngleBrackets: options.escapeHeadingAngleBrackets ?? false
146
+ };
147
+ }
148
+ /**
149
+ * Merges base API docs options with override options.
5
150
  *
6
- * @module default
151
+ * @param base - Base options from VitePress configuration.
152
+ * @param override - Override options applied on top of the base options.
153
+ * @returns Merged API docs options.
7
154
  */
155
+ function mergeApiDocsOptions(base, override) {
156
+ if (!override) return base;
157
+ const section = override.nav?.section ?? base.nav?.section;
158
+ const nav = {
159
+ ...base.nav,
160
+ ...override.nav
161
+ };
162
+ return {
163
+ ...base,
164
+ ...override,
165
+ extraction: {
166
+ ...base.extraction,
167
+ ...override.extraction
168
+ },
169
+ markdown: {
170
+ ...base.markdown,
171
+ ...override.markdown
172
+ },
173
+ nav: section ? {
174
+ ...nav,
175
+ section
176
+ } : nav
177
+ };
178
+ }
179
+ function normalizeBasePath(basePath) {
180
+ const normalized = basePath.startsWith("/") ? basePath : `/${basePath}`;
181
+ return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
182
+ }
183
+ function toAbsolutePath(value, baseDir) {
184
+ if (value instanceof URL) return fileURLToPath(value);
185
+ return path.isAbsolute(value) ? value : path.resolve(baseDir, value);
186
+ }
187
+ //#endregion
188
+ //#region src/generate.ts
8
189
  /**
9
190
  * @author kazuya kawaguchi (a.k.a. kazupon)
10
191
  * @license MIT
11
192
  */
12
- const debug = createDebug("vitepress-api-references");
13
- function fn() {
14
- debug("fn called");
15
- return "Hello, tsdown!";
193
+ /**
194
+ * Generates API reference markdown, navigation metadata, and optional artifacts.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * import { generateOxContentApiDocs } from 'vitepress-api-references'
199
+ *
200
+ * const result = await generateOxContentApiDocs({
201
+ * entryPoints: ['src/index.ts'],
202
+ * outDir: 'docs/api',
203
+ * basePath: '/api'
204
+ * })
205
+ *
206
+ * console.log(result.generatedFiles)
207
+ * ```
208
+ *
209
+ * @param options - API docs generation options.
210
+ * @returns Generated API docs files, metadata, diagnostics, and resolved options.
211
+ */
212
+ async function generateOxContentApiDocs(options) {
213
+ const resolvedOptions = resolveApiDocsOptions(options);
214
+ const extractedDocs = extractDocsFromEntryPoints(resolvedOptions.entryPoints, {
215
+ root: resolvedOptions.root,
216
+ tsconfig: resolvedOptions.tsconfig,
217
+ ...resolvedOptions.extraction
218
+ });
219
+ const docs = extractedDocs.map((module) => ({
220
+ file: module.name,
221
+ description: module.description,
222
+ sourcePath: module.sourcePath,
223
+ examples: module.examples,
224
+ tags: module.tags,
225
+ entries: module.entries.map((entry) => ({
226
+ ...entry,
227
+ tags: entry.tags ? Object.entries(entry.tags).map(([tag, value]) => ({
228
+ tag,
229
+ value
230
+ })) : void 0,
231
+ hasBody: entry.hasBody ?? false
232
+ }))
233
+ }));
234
+ const rawFiles = generateDocsMarkdown(docs, {
235
+ ...resolvedOptions.markdown,
236
+ basePath: resolvedOptions.basePath,
237
+ githubUrl: resolvedOptions.githubUrl
238
+ });
239
+ const files = resolvedOptions.escapeHeadingAngleBrackets ? escapeHeadingAngleBrackets(rawFiles) : rawFiles;
240
+ const nav = generateDocsNavMetadataFromDocs(docs, {
241
+ basePath: resolvedOptions.basePath,
242
+ pathStrategy: resolvedOptions.markdown?.pathStrategy,
243
+ singleEntryRoot: resolvedOptions.markdown?.singleEntryRoot,
244
+ groupOrder: resolvedOptions.markdown?.groupOrder,
245
+ sort: resolvedOptions.markdown?.sort,
246
+ sortEntryPoints: resolvedOptions.markdown?.sortEntryPoints,
247
+ kindSortOrder: resolvedOptions.markdown?.kindSortOrder
248
+ });
249
+ const generatedFiles = resolvedOptions.write ? await writeGeneratedFiles(files, resolvedOptions.outDir) : Object.keys(files).map((file) => path.resolve(resolvedOptions.outDir, file));
250
+ if (resolvedOptions.write) await writeOptionalArtifacts(resolvedOptions, docs, nav, generatedFiles);
251
+ return {
252
+ files,
253
+ nav,
254
+ docs,
255
+ generatedFiles,
256
+ diagnostics: extractedDocs.flatMap((module) => module.diagnostics.map((diagnostic) => diagnostic.message)),
257
+ hash: await createGenerationHash(resolvedOptions),
258
+ resolvedOptions
259
+ };
16
260
  }
261
+ async function writeOptionalArtifacts(options, docs, nav, generatedFiles) {
262
+ if (options.docsJson) {
263
+ const docsJsonPath = typeof options.docsJson === "string" ? options.docsJson : "docs.json";
264
+ const outputPath = path.isAbsolute(docsJsonPath) ? docsJsonPath : path.join(options.outDir, docsJsonPath);
265
+ await writeGeneratedFile(outputPath, `${generateDocsDataJson(docs, (/* @__PURE__ */ new Date()).toISOString())}\n`);
266
+ generatedFiles.push(outputPath);
267
+ }
268
+ if (options.nav.outputFile) {
269
+ const outputPath = path.isAbsolute(options.nav.outputFile) ? options.nav.outputFile : path.join(options.outDir, options.nav.outputFile);
270
+ await writeGeneratedFile(outputPath, generateDocsNavCode(nav, options.nav.exportName ?? "apiDocsNav"));
271
+ generatedFiles.push(outputPath);
272
+ }
273
+ }
274
+ function escapeHeadingAngleBrackets(files) {
275
+ return Object.fromEntries(Object.entries(files).map(([filePath, content]) => [filePath, content.split("\n").map((line) => /^#{1,6}\s/.test(line) ? line.replaceAll("<", "&lt;").replaceAll(">", "&gt;") : line).join("\n")]));
276
+ }
277
+ //#endregion
278
+ //#region src/sidebar.ts
279
+ /**
280
+ * @author kazuya kawaguchi (a.k.a. kazupon)
281
+ * @license MIT
282
+ */
283
+ /**
284
+ * Converts API docs navigation metadata into VitePress sidebar items.
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * import { toVitePressSidebarItems } from 'vitepress-api-references'
289
+ *
290
+ * const sidebarItems = toVitePressSidebarItems([
291
+ * {
292
+ * title: 'API',
293
+ * path: '/api/',
294
+ * children: [{ title: 'withOxContentApiDocs', path: '/api/with-ox-content-api-docs' }]
295
+ * }
296
+ * ])
297
+ * ```
298
+ *
299
+ * @param nav - API docs navigation metadata.
300
+ * @param options - VitePress navigation options.
301
+ * @returns Generated VitePress sidebar items.
302
+ */
303
+ function toVitePressSidebarItems(nav, options = {}) {
304
+ return nav.map((item) => toSidebarItem(item, options, 0));
305
+ }
306
+ /**
307
+ * Creates a VitePress sidebar section from generated API docs navigation.
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * import { createVitePressSidebarSection } from 'vitepress-api-references'
312
+ *
313
+ * const apiSection = createVitePressSidebarSection(apiDocsNav, {
314
+ * section: {
315
+ * text: 'API Reference',
316
+ * collapsed: false
317
+ * }
318
+ * })
319
+ * ```
320
+ *
321
+ * @param nav - API docs navigation metadata.
322
+ * @param options - VitePress navigation options.
323
+ * @returns Generated VitePress sidebar section.
324
+ */
325
+ function createVitePressSidebarSection(nav, options = {}) {
326
+ const items = toVitePressSidebarItems(nav, options);
327
+ const section = options.section;
328
+ if (!section) return { items };
329
+ return {
330
+ text: section.text,
331
+ collapsed: resolveSectionCollapsed(options),
332
+ items
333
+ };
334
+ }
335
+ /**
336
+ * Merges generated API docs sidebar data into an existing VitePress sidebar.
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * import { mergeVitePressSidebar } from 'vitepress-api-references'
341
+ *
342
+ * const sidebar = mergeVitePressSidebar(
343
+ * [{ text: 'Guide', link: '/guide/' }],
344
+ * { text: 'API Reference', items: [{ text: 'Config', link: '/api/config' }] },
345
+ * { insert: 'append' }
346
+ * )
347
+ * ```
348
+ *
349
+ * @param sidebar - Existing VitePress sidebar configuration.
350
+ * @param generated - Generated API docs sidebar section.
351
+ * @param options - Sidebar merge options.
352
+ * @returns Updated VitePress sidebar configuration.
353
+ */
354
+ function mergeVitePressSidebar(sidebar, generated, options = {}) {
355
+ const insert = options.insert ?? "append";
356
+ if (typeof insert === "function") return insert(sidebar, generated);
357
+ if (isSidebarMulti(sidebar)) {
358
+ const route = options.sidebarRoute;
359
+ if (!route) return sidebar;
360
+ return {
361
+ ...sidebar,
362
+ [route]: mergeVitePressSidebar(sidebar[route], generated, options)
363
+ };
364
+ }
365
+ const items = Array.isArray(sidebar) ? sidebar : [];
366
+ if (insert === "prepend") return [generated, ...items];
367
+ if (insert === "replace") return replaceSidebarItem(items, generated, options.replaceText);
368
+ return [...items, generated];
369
+ }
370
+ function toSidebarItem(item, options, depth) {
371
+ const children = item.children?.map((child) => toSidebarItem(child, options, depth + 1));
372
+ const sidebarItem = { text: item.title };
373
+ if (children?.length) {
374
+ const collapsed = resolveCollapsed(item, options, depth);
375
+ if (collapsed !== void 0) sidebarItem.collapsed = collapsed;
376
+ sidebarItem.items = children;
377
+ if (depth === 0 && item.path) sidebarItem.link = item.path;
378
+ } else if (item.path) sidebarItem.link = item.path;
379
+ return sidebarItem;
380
+ }
381
+ function replaceSidebarItem(items, generated, replaceText) {
382
+ const result = replaceSidebarItemRecursive(items, generated, replaceText);
383
+ return result.replaced ? result.items : [...items, generated];
384
+ }
385
+ function replaceSidebarItemRecursive(items, generated, replaceText) {
386
+ let replaced = false;
387
+ return {
388
+ items: items.map((item) => {
389
+ if (!isSidebarItem(item)) return item;
390
+ if (replaceText && item.text === replaceText) {
391
+ replaced = true;
392
+ return generated;
393
+ }
394
+ if (replaceText && Array.isArray(item.items)) {
395
+ const nested = replaceSidebarItemRecursive(item.items, generated, replaceText);
396
+ if (nested.replaced) {
397
+ replaced = true;
398
+ return {
399
+ ...item,
400
+ items: nested.items
401
+ };
402
+ }
403
+ }
404
+ return item;
405
+ }),
406
+ replaced
407
+ };
408
+ }
409
+ function resolveSectionCollapsed(options) {
410
+ return options.section?.collapsed ?? (typeof options.collapsed === "boolean" ? options.collapsed : void 0);
411
+ }
412
+ function resolveCollapsed(item, options, depth) {
413
+ if (typeof options.collapsed === "function") return options.collapsed(item, depth);
414
+ return options.collapsed;
415
+ }
416
+ function isSidebarItem(value) {
417
+ return typeof value === "object" && value !== null;
418
+ }
419
+ function isSidebarMulti(value) {
420
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((item) => Array.isArray(item));
421
+ }
422
+ //#endregion
423
+ //#region src/watch.ts
424
+ /**
425
+ * @author kazuya kawaguchi (a.k.a. kazupon)
426
+ * @license MIT
427
+ */
428
+ /**
429
+ * Creates a Vite plugin that regenerates API docs during development.
430
+ *
431
+ * @param options - API docs generation options.
432
+ * @param pluginOptions - Initial plugin state options.
433
+ * @returns Vite plugin for API docs generation and reloads.
434
+ */
435
+ function createApiDocsVitePlugin(options, pluginOptions = {}) {
436
+ const resolvedOptions = resolveApiDocsOptions(options);
437
+ let lastHash = pluginOptions.initialHash;
438
+ let navItems = pluginOptions.initialNav ?? [];
439
+ let debounceTimer;
440
+ async function regenerate(force = false) {
441
+ const nextHash = await createGenerationHash(resolvedOptions);
442
+ if (!force && lastHash === nextHash) return;
443
+ const result = await generateOxContentApiDocs(options);
444
+ lastHash = result.hash;
445
+ navItems = result.nav;
446
+ }
447
+ return {
448
+ name: "vitepress-api-references:api-docs",
449
+ enforce: "post",
450
+ async buildStart() {
451
+ await regenerate();
452
+ },
453
+ configureServer(server) {
454
+ const watchedFiles = getWatchedFiles(options);
455
+ server.watcher.add(watchedFiles);
456
+ server.watcher.on("all", (_event, filePath) => {
457
+ if (!watchedFiles.includes(filePath)) return;
458
+ queueRegenerate(server, () => regenerate(true));
459
+ });
460
+ },
461
+ resolveId(id) {
462
+ if (resolvedOptions.nav.virtualModule && id === resolvedOptions.nav.virtualModule) return id;
463
+ },
464
+ load(id) {
465
+ if (resolvedOptions.nav.virtualModule && id === resolvedOptions.nav.virtualModule) return `export const navItems = ${JSON.stringify(navItems)}\n`;
466
+ }
467
+ };
468
+ function queueRegenerate(server, run) {
469
+ if (debounceTimer) clearTimeout(debounceTimer);
470
+ debounceTimer = setTimeout(() => {
471
+ run().then(() => {
472
+ server.ws.send({ type: "full-reload" });
473
+ }).catch((error) => {
474
+ console.error(error);
475
+ });
476
+ }, 100);
477
+ }
478
+ }
479
+ function getWatchedFiles(options) {
480
+ const resolvedOptions = resolveApiDocsOptions(options);
481
+ return [...resolvedOptions.entryPoints.map((entry) => entry.path), ...resolvedOptions.extraction.externalPackageSources?.map((source) => source.entry) ?? []];
482
+ }
483
+ //#endregion
484
+ //#region src/vitepress.ts
485
+ /**
486
+ * @author kazuya kawaguchi (a.k.a. kazupon)
487
+ * @license MIT
488
+ */
489
+ /**
490
+ * Adds generated API docs pages, sidebar data, and watch support to a VitePress config.
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * import { defineConfig } from 'vitepress'
495
+ * import { withOxContentApiDocs } from 'vitepress-api-references'
496
+ *
497
+ * export default defineConfig(
498
+ * await withOxContentApiDocs({
499
+ * title: 'My Library',
500
+ * themeConfig: {
501
+ * sidebar: [{ text: 'Guide', link: '/' }, { text: 'API Reference' }]
502
+ * },
503
+ * apiDocs: {
504
+ * entryPoints: [{ path: 'src/index.ts', name: 'default' }],
505
+ * outDir: 'docs/api',
506
+ * basePath: '/api',
507
+ * nav: {
508
+ * section: { text: 'API Reference', collapsed: false },
509
+ * insert: 'replace',
510
+ * replaceText: 'API Reference'
511
+ * }
512
+ * }
513
+ * })
514
+ * )
515
+ * ```
516
+ *
517
+ * @param config - VitePress user configuration.
518
+ * @param override - Optional API docs options applied over the configured options.
519
+ * @returns Updated VitePress user configuration.
520
+ */
521
+ async function withOxContentApiDocs(config, override) {
522
+ const configuredOptions = config.apiDocs;
523
+ if (configuredOptions === false && !override) return config;
524
+ const baseOptions = configuredOptions === false ? void 0 : configuredOptions;
525
+ const apiDocsOptions = override ? baseOptions ? mergeApiDocsOptions(baseOptions, override) : override : baseOptions;
526
+ if (!apiDocsOptions) return config;
527
+ const result = await generateOxContentApiDocs(apiDocsOptions);
528
+ const sidebarSection = removeMissingBranchLinks(createVitePressSidebarSection(result.nav, result.resolvedOptions.nav), createGeneratedRoutePathSet(result.files, result.resolvedOptions.basePath));
529
+ config.themeConfig ??= {};
530
+ config.themeConfig.sidebar = mergeVitePressSidebar(config.themeConfig.sidebar, sidebarSection, result.resolvedOptions.nav);
531
+ config.vite ??= {};
532
+ config.vite.plugins = [...Array.isArray(config.vite.plugins) ? config.vite.plugins : [], createApiDocsVitePlugin(apiDocsOptions, {
533
+ initialHash: result.hash,
534
+ initialNav: result.nav
535
+ })];
536
+ return config;
537
+ }
538
+ function createGeneratedRoutePathSet(files, basePath) {
539
+ const routes = /* @__PURE__ */ new Set();
540
+ for (const filePath of Object.keys(files)) {
541
+ if (!filePath.endsWith(".md")) continue;
542
+ const routePath = filePath.slice(0, -3);
543
+ if (routePath === "index") routes.add(normalizeRoutePath(basePath));
544
+ else if (routePath.endsWith("/index")) routes.add(normalizeRoutePath(`${basePath}/${routePath.slice(0, -6)}`));
545
+ else routes.add(normalizeRoutePath(`${basePath}/${routePath}`));
546
+ }
547
+ return routes;
548
+ }
549
+ function removeMissingBranchLinks(item, routes) {
550
+ const next = { ...item };
551
+ if (item.items) next.items = item.items.map((child) => removeMissingBranchLinks(child, routes));
552
+ if (next.items?.length && next.link && !routes.has(normalizeRoutePath(next.link))) delete next.link;
553
+ return next;
554
+ }
555
+ function normalizeRoutePath(routePath) {
556
+ const withoutMarkdown = routePath.endsWith(".md") ? routePath.slice(0, -3) : routePath;
557
+ const normalized = withoutMarkdown.startsWith("/") ? withoutMarkdown : `/${withoutMarkdown}`;
558
+ return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
559
+ }
560
+ //#endregion
561
+ //#region src/index.ts
562
+ /**
563
+ * @author kazuya kawaguchi (a.k.a. kazupon)
564
+ * @license MIT
565
+ */
17
566
  //#endregion
18
- export { fn };
567
+ export { createVitePressSidebarSection, generateOxContentApiDocs, mergeVitePressSidebar, toVitePressSidebarItems, withOxContentApiDocs };
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "vitepress-api-references",
3
- "version": "0.0.0",
4
- "description": "Enable JSDoc API Reference",
3
+ "version": "0.2.0",
4
+ "description": "Enable JSDoc API Reference for VitePress",
5
+ "keywords": [
6
+ "api-references",
7
+ "docs",
8
+ "documentation",
9
+ "markdown",
10
+ "vitepress"
11
+ ],
5
12
  "homepage": "https://github.com/kazupon/vitepress-api-references#readme",
6
13
  "bugs": {
7
14
  "url": "https://github.com/kazupon/vitepress-api-references/issues"
@@ -38,7 +45,12 @@
38
45
  "build": "vp pack",
39
46
  "knip": "knip",
40
47
  "dev": "vp pack --watch",
48
+ "docs:build": "vp exec vitepress build docs",
49
+ "docs:preview": "vp exec vitepress preview docs",
50
+ "docs:dev": "vp exec vitepress dev docs",
51
+ "docs:standalone": "node standalone/generate.ts",
41
52
  "test": "vp test",
53
+ "test:fixtures": "vp exec vitepress build tests/fixtures/basic/docs",
42
54
  "check": "vp check && knip",
43
55
  "prepublishOnly": "vp run build",
44
56
  "release": "bumpp --commit \"release: v%s\" --all --push --tag",
@@ -46,18 +58,25 @@
46
58
  "prepare": "vp config"
47
59
  },
48
60
  "dependencies": {
49
- "obug": "^2.1.2"
61
+ "@ox-content/napi": "^2.65.0"
50
62
  },
51
63
  "devDependencies": {
52
- "@kazupon/eslint-plugin": "^0.7.1",
53
- "@kazupon/vp-config": "^0.2.0",
64
+ "@kazupon/vp-config": "^0.3.2",
54
65
  "@types/node": "^25.9.1",
55
- "@typescript/native-preview": "7.0.0-dev.20260509.2",
66
+ "@typescript/native-preview": "7.0.0-dev.20260601.1",
56
67
  "bumpp": "^11.1.0",
57
68
  "gh-changelogen": "^0.2.8",
58
69
  "knip": "^6.16.0",
70
+ "oxc-minify": "^0.134.0",
71
+ "pkg-pr-new": "^0.0.75",
59
72
  "typescript": "^6.0.3",
60
- "vite-plus": "catalog:"
73
+ "vite": "catalog:",
74
+ "vite-plus": "catalog:",
75
+ "vitepress": "2.0.0-alpha.17"
76
+ },
77
+ "peerDependencies": {
78
+ "vite": ">=5",
79
+ "vitepress": ">=2.0.0-alpha.17"
61
80
  },
62
81
  "engines": {
63
82
  "node": ">= 22"