@fumadocs/cli 1.2.4 → 1.2.6

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/dist/index.js CHANGED
@@ -1,75 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { n as transformSpecifiers, r as typescriptExtensions, t as toImportSpecifier } from "./ast-BS3xj9uY.js";
2
+ import { t as exists } from "./fs-CigSthjp.js";
3
+ import { i as initConfig, n as createOrLoadConfig } from "./config-DH5Ggyir.js";
4
+ import "./ast-BRNdmLn5.js";
5
+ import { n as LocalRegistryClient, t as HttpRegistryClient } from "./client-YTcWP1iz.js";
6
+ import { t as ComponentInstaller } from "./installer-DQzu7o9x.js";
3
7
  import fs from "node:fs/promises";
4
8
  import path from "node:path";
5
9
  import { Command } from "commander";
6
10
  import picocolors from "picocolors";
7
- import { z } from "zod";
8
11
  import { x } from "tinyexec";
9
12
  import { autocompleteMultiselect, box, cancel, confirm, group, intro, isCancel, log, outro, select, spinner } from "@clack/prompts";
10
- import { parse } from "oxc-parser";
11
- import { detect } from "package-manager-detector";
12
- import MagicString from "magic-string";
13
-
14
- //#region src/utils/fs.ts
15
- async function exists(pathLike) {
16
- try {
17
- await fs.access(pathLike);
18
- return true;
19
- } catch {
20
- return false;
21
- }
22
- }
23
-
24
- //#endregion
25
- //#region src/utils/is-src.ts
26
- async function isSrc() {
27
- return exists("./src");
28
- }
29
-
30
- //#endregion
31
- //#region src/config.ts
32
- function createConfigSchema(isSrc) {
33
- const defaultAliases = {
34
- uiDir: "./components/ui",
35
- componentsDir: "./components",
36
- blockDir: "./components",
37
- cssDir: "./styles",
38
- libDir: "./lib"
39
- };
40
- return z.object({
41
- $schema: z.string().default(isSrc ? "node_modules/@fumadocs/cli/dist/schema/src.json" : "node_modules/@fumadocs/cli/dist/schema/default.json").optional(),
42
- aliases: z.object({
43
- uiDir: z.string().default(defaultAliases.uiDir),
44
- componentsDir: z.string().default(defaultAliases.uiDir),
45
- blockDir: z.string().default(defaultAliases.blockDir),
46
- cssDir: z.string().default(defaultAliases.componentsDir),
47
- libDir: z.string().default(defaultAliases.libDir)
48
- }).default(defaultAliases),
49
- baseDir: z.string().default(isSrc ? "src" : ""),
50
- uiLibrary: z.enum(["radix-ui", "base-ui"]).default("radix-ui"),
51
- commands: z.object({ format: z.string().optional() }).default({})
52
- });
53
- }
54
- async function createOrLoadConfig(file = "./cli.json") {
55
- const inited = await initConfig(file);
56
- if (inited) return inited;
57
- const content = (await fs.readFile(file)).toString();
58
- return createConfigSchema(await isSrc()).parse(JSON.parse(content));
59
- }
60
- /**
61
- * Write new config, skip if a config already exists
62
- *
63
- * @returns the created config, `undefined` if not created
64
- */
65
- async function initConfig(file = "./cli.json", src) {
66
- if (await fs.stat(file).then(() => true).catch(() => false)) return;
67
- const defaultConfig = createConfigSchema(src ?? await isSrc()).parse({});
68
- await fs.writeFile(file, JSON.stringify(defaultConfig, null, 2));
69
- return defaultConfig;
70
- }
71
-
72
- //#endregion
13
+ import { exec } from "node:child_process";
14
+ import { promisify } from "node:util";
73
15
  //#region src/commands/file-tree.ts
74
16
  const scanned = [
75
17
  "file",
@@ -104,7 +46,6 @@ function treeToJavaScript(input, noRoot, importName = "fumadocs-ui/components/fi
104
46
 
105
47
  export default (${treeToMdx(input, noRoot)})`;
106
48
  }
107
-
108
49
  //#endregion
109
50
  //#region src/utils/file-tree/run-tree.ts
110
51
  async function runTree(args) {
@@ -120,336 +61,15 @@ async function runTree(args) {
120
61
  throw new Error("failed to run `tree` command", { cause: e });
121
62
  }
122
63
  }
123
-
124
64
  //#endregion
125
65
  //#region package.json
126
- var version = "1.2.4";
127
-
128
- //#endregion
129
- //#region src/registry/schema.ts
130
- const namespaces = [
131
- "components",
132
- "lib",
133
- "css",
134
- "route",
135
- "ui",
136
- "block"
137
- ];
138
- const indexSchema = z.object({
139
- name: z.string(),
140
- title: z.string().optional(),
141
- description: z.string().optional()
142
- });
143
- const fileSchema = z.object({
144
- type: z.literal(namespaces),
145
- path: z.string(),
146
- target: z.string().optional(),
147
- content: z.string()
148
- });
149
- const httpSubComponent = z.object({
150
- type: z.literal("http"),
151
- baseUrl: z.string(),
152
- component: z.string()
153
- });
154
- const componentSchema = z.object({
155
- name: z.string(),
156
- title: z.string().optional(),
157
- description: z.string().optional(),
158
- files: z.array(fileSchema),
159
- dependencies: z.record(z.string(), z.string().or(z.null())),
160
- devDependencies: z.record(z.string(), z.string().or(z.null())),
161
- subComponents: z.array(z.string().or(httpSubComponent)).default([])
162
- });
163
- const registryInfoSchema = z.object({
164
- variables: z.record(z.string(), z.object({
165
- description: z.string().optional(),
166
- default: z.unknown().optional()
167
- })).optional(),
168
- env: z.record(z.string(), z.unknown()).optional(),
169
- indexes: z.array(indexSchema).default([]),
170
- registries: z.array(z.string()).optional()
171
- });
172
-
173
- //#endregion
174
- //#region src/utils/cache.ts
175
- /**
176
- * cache for async resources, finished promises will be resolved into original value, otherwise wrapped with a promise.
177
- */
178
- function createCache(store = /* @__PURE__ */ new Map()) {
179
- return {
180
- cached(key, fn) {
181
- let cached = store.get(key);
182
- if (cached) return cached;
183
- cached = fn((v) => store.set(key, v));
184
- if (cached instanceof Promise) cached = cached.then((out) => {
185
- if (store.has(key)) store.set(key, out);
186
- return out;
187
- });
188
- store.set(key, cached);
189
- return cached;
190
- },
191
- invalidate(key) {
192
- store.delete(key);
193
- },
194
- $value() {
195
- return this;
196
- }
197
- };
198
- }
199
-
200
- //#endregion
201
- //#region src/registry/client.ts
202
- const fetchCache = createCache();
203
- var HttpRegistryClient = class HttpRegistryClient {
204
- constructor(baseUrl, config) {
205
- this.baseUrl = baseUrl;
206
- this.config = config;
207
- this.registryId = baseUrl;
208
- }
209
- async fetchRegistryInfo(baseUrl = this.baseUrl) {
210
- const url = new URL("_registry.json", `${baseUrl}/`);
211
- return fetchCache.$value().cached(url.href, async () => {
212
- const res = await fetch(url);
213
- if (!res.ok) throw new Error(`failed to fetch ${url.href}: ${res.statusText}`);
214
- return registryInfoSchema.parse(await res.json());
215
- });
216
- }
217
- async fetchComponent(name) {
218
- const url = new URL(`${name}.json`, `${this.baseUrl}/`);
219
- return fetchCache.$value().cached(url.href, async () => {
220
- const res = await fetch(`${this.baseUrl}/${name}.json`);
221
- if (!res.ok) {
222
- if (res.status === 404) throw new Error(`component ${name} not found at ${url.href}`);
223
- throw new Error(await res.text());
224
- }
225
- return componentSchema.parse(await res.json());
226
- });
227
- }
228
- async hasComponent(name) {
229
- const url = new URL(`${name}.json`, `${this.baseUrl}/`);
230
- return (await fetch(url, { method: "HEAD" })).ok;
231
- }
232
- createLinkedRegistryClient(name) {
233
- return new HttpRegistryClient(`${this.baseUrl}/${name}`, this.config);
234
- }
235
- };
236
- var LocalRegistryClient = class LocalRegistryClient {
237
- constructor(dir, config) {
238
- this.dir = dir;
239
- this.config = config;
240
- this.registryId = dir;
241
- }
242
- async fetchRegistryInfo(dir = this.dir) {
243
- if (this.registryInfo) return this.registryInfo;
244
- const filePath = path.join(dir, "_registry.json");
245
- const out = await fs.readFile(filePath).then((res) => JSON.parse(res.toString())).catch((e) => {
246
- throw new Error(`failed to resolve local file "${filePath}"`, { cause: e });
247
- });
248
- return this.registryInfo = registryInfoSchema.parse(out);
249
- }
250
- async fetchComponent(name) {
251
- const filePath = path.join(this.dir, `${name}.json`);
252
- const out = await fs.readFile(filePath).then((res) => JSON.parse(res.toString())).catch((e) => {
253
- throw new Error(`component ${name} not found at ${filePath}`, { cause: e });
254
- });
255
- return componentSchema.parse(out);
256
- }
257
- async hasComponent(name) {
258
- const filePath = path.join(this.dir, `${name}.json`);
259
- try {
260
- await fs.stat(filePath);
261
- return true;
262
- } catch {
263
- return false;
264
- }
265
- }
266
- createLinkedRegistryClient(name) {
267
- return new LocalRegistryClient(path.join(this.dir, name), this.config);
268
- }
269
- };
270
-
271
- //#endregion
272
- //#region src/utils/get-package-manager.ts
273
- async function getPackageManager() {
274
- return (await detect())?.name ?? "npm";
275
- }
276
-
277
- //#endregion
278
- //#region src/registry/installer/dep-manager.ts
279
- var DependencyManager = class {
280
- constructor() {
281
- this.installedDeps = /* @__PURE__ */ new Map();
282
- this.dependencies = [];
283
- this.devDependencies = [];
284
- this.packageManager = "npm";
285
- }
286
- async init(deps, devDeps) {
287
- this.installedDeps.clear();
288
- if (await exists("package.json")) {
289
- const content = await fs.readFile("package.json");
290
- const parsed = JSON.parse(content.toString());
291
- if ("dependencies" in parsed && typeof parsed.dependencies === "object") {
292
- const records = parsed.dependencies;
293
- for (const [k, v] of Object.entries(records)) this.installedDeps.set(k, v);
294
- }
295
- if ("devDependencies" in parsed && typeof parsed.devDependencies === "object") {
296
- const records = parsed.devDependencies;
297
- for (const [k, v] of Object.entries(records)) this.installedDeps.set(k, v);
298
- }
299
- }
300
- this.dependencies = this.resolveRequiredDependencies(deps);
301
- this.devDependencies = this.resolveRequiredDependencies(devDeps);
302
- this.packageManager = await getPackageManager();
303
- }
304
- resolveRequiredDependencies(deps) {
305
- return Object.entries(deps).filter(([k]) => !this.installedDeps.has(k)).map(([k, v]) => v === null || v.length === 0 ? k : `${k}@${v}`);
306
- }
307
- hasRequired() {
308
- return this.dependencies.length > 0 || this.devDependencies.length > 0;
309
- }
310
- async installRequired() {
311
- if (this.dependencies.length > 0) await x(this.packageManager, ["install", ...this.dependencies]);
312
- if (this.devDependencies.length > 0) await x(this.packageManager, [
313
- "install",
314
- ...this.devDependencies,
315
- "-D"
316
- ]);
317
- }
318
- };
319
-
320
- //#endregion
321
- //#region src/registry/installer/index.ts
322
- var ComponentInstaller = class {
323
- constructor(rootClient, plugins = []) {
324
- this.rootClient = rootClient;
325
- this.plugins = plugins;
326
- this.installedFiles = /* @__PURE__ */ new Set();
327
- this.downloadCache = createCache();
328
- this.dependencies = {};
329
- this.devDependencies = {};
330
- this.pathToFileCache = createCache();
331
- }
332
- async install(name, io) {
333
- let downloaded;
334
- const info = await this.rootClient.fetchRegistryInfo();
335
- for (const registry of info.registries ?? []) if (name.startsWith(`${registry}/`)) {
336
- downloaded = await this.download(name.slice(registry.length + 1), this.rootClient.createLinkedRegistryClient(registry));
337
- break;
338
- }
339
- downloaded ??= await this.download(name, this.rootClient);
340
- for (const item of downloaded) {
341
- Object.assign(this.dependencies, item.dependencies);
342
- Object.assign(this.devDependencies, item.devDependencies);
343
- }
344
- for (const comp of downloaded) for (const file of comp.files) {
345
- const outPath = this.resolveOutputPath(file);
346
- if (this.installedFiles.has(outPath)) continue;
347
- this.installedFiles.add(outPath);
348
- const output = typescriptExtensions.includes(path.extname(outPath)) ? await this.transform(io, name, file, comp, downloaded) : file.content;
349
- const status = await fs.readFile(outPath).then((res) => {
350
- if (res.toString() === output) return "ignore";
351
- return "need-update";
352
- }).catch(() => "write");
353
- if (status === "ignore") continue;
354
- if (status === "need-update") {
355
- if (!await io.confirmFileOverride({ path: outPath })) continue;
356
- }
357
- await fs.mkdir(path.dirname(outPath), { recursive: true });
358
- await fs.writeFile(outPath, output);
359
- io.onFileDownloaded({
360
- path: outPath,
361
- file,
362
- component: comp
363
- });
364
- }
365
- }
366
- async deps() {
367
- const manager = new DependencyManager();
368
- await manager.init(this.dependencies, this.devDependencies);
369
- return manager;
370
- }
371
- async onEnd() {
372
- const config = this.rootClient.config;
373
- if (config.commands.format) await x(config.commands.format);
374
- }
375
- /**
376
- * return a list of components, merged with child components & variables.
377
- */
378
- async download(name, client, contextVariables) {
379
- const hash = `${client.registryId} ${name}`;
380
- const info = await client.fetchRegistryInfo();
381
- const variables = {
382
- ...contextVariables,
383
- ...info.env
384
- };
385
- for (const [k, v] of Object.entries(info.variables ?? {})) variables[k] ??= v.default;
386
- return (await this.downloadCache.cached(hash, async (presolve) => {
387
- const comp = await client.fetchComponent(name);
388
- const result = [comp];
389
- presolve(result);
390
- const child = await Promise.all(comp.subComponents.map((sub) => {
391
- if (typeof sub === "string") return this.download(sub, client);
392
- const baseUrl = this.rootClient instanceof HttpRegistryClient ? new URL(sub.baseUrl, `${this.rootClient.baseUrl}/`).href : sub.baseUrl;
393
- return this.download(sub.component, new HttpRegistryClient(baseUrl, client.config), variables);
394
- }));
395
- for (const sub of child) result.push(...sub);
396
- return result;
397
- })).map((file) => ({
398
- ...file,
399
- variables
400
- }));
401
- }
402
- async transform(io, taskId, file, component, allComponents) {
403
- const filePath = this.resolveOutputPath(file);
404
- const parsed = await parse(filePath, file.content);
405
- const s = new MagicString(file.content);
406
- const prefix = "@/";
407
- const variables = Object.entries(component.variables ?? {});
408
- const pathToFile = await this.pathToFileCache.cached(taskId, () => {
409
- const map = /* @__PURE__ */ new Map();
410
- for (const comp of allComponents) for (const file of comp.files) map.set(file.target ?? file.path, file);
411
- return map;
412
- });
413
- transformSpecifiers(parsed.program, s, (specifier) => {
414
- for (const [k, v] of variables) specifier = specifier.replaceAll(`<${k}>`, v);
415
- if (specifier.startsWith(prefix)) {
416
- const lookup = specifier.substring(2);
417
- const target = pathToFile.get(lookup);
418
- if (target) specifier = toImportSpecifier(filePath, this.resolveOutputPath(target));
419
- else io.onWarn(`cannot find the referenced file of ${specifier}`);
420
- }
421
- return specifier;
422
- });
423
- for (const plugin of this.plugins) await plugin.transformFile?.({
424
- s,
425
- parsed,
426
- file,
427
- component
428
- });
429
- return s.toString();
430
- }
431
- resolveOutputPath(file) {
432
- const config = this.rootClient.config;
433
- const dir = {
434
- components: config.aliases.componentsDir,
435
- block: config.aliases.blockDir,
436
- ui: config.aliases.uiDir,
437
- css: config.aliases.cssDir,
438
- lib: config.aliases.libDir,
439
- route: "./"
440
- }[file.type];
441
- if (file.target) return path.join(config.baseDir, file.target.replace("<dir>", dir));
442
- return path.join(config.baseDir, dir, path.basename(file.path));
443
- }
444
- };
445
-
66
+ var version = "1.2.6";
446
67
  //#endregion
447
68
  //#region src/commands/shared.ts
448
69
  const UIRegistries = {
449
70
  "base-ui": "fumadocs/base-ui",
450
71
  "radix-ui": "fumadocs/radix-ui"
451
72
  };
452
-
453
73
  //#endregion
454
74
  //#region src/commands/add.ts
455
75
  async function add(input, client) {
@@ -537,7 +157,6 @@ async function install(target, installer) {
537
157
  await installer.onEnd();
538
158
  outro(picocolors.bold(picocolors.greenBright("Successful")));
539
159
  }
540
-
541
160
  //#endregion
542
161
  //#region src/commands/customise.ts
543
162
  async function customise(client) {
@@ -570,7 +189,7 @@ async function customise(client) {
570
189
  label: "Start from minimal styles",
571
190
  hint: "for those who want to build their own variant from ground up.",
572
191
  value: {
573
- target: ["fumadocs/ui/layouts/docs-min"],
192
+ target: ["layouts/docs-min"],
574
193
  replace: [["fumadocs-ui/layouts/docs", "@/components/layout/docs"], ["fumadocs-ui/layouts/docs/page", "@/components/layout/docs/page"]]
575
194
  }
576
195
  },
@@ -618,7 +237,192 @@ function printNext(...maps) {
618
237
  ...maps.map(([from, to]) => picocolors.greenBright(`"${from}" -> "${to}"`))
619
238
  ].join("\n"));
620
239
  }
240
+ //#endregion
241
+ //#region src/commands/export-epub.ts
242
+ const execAsync = promisify(exec);
243
+ async function readPackageJson(cwd) {
244
+ try {
245
+ const raw = await fs.readFile(path.join(cwd, "package.json"), "utf-8");
246
+ return JSON.parse(raw);
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+ /** Path of pre-rendered EPUB, choose one according to your React framework. Next.js fetches from the running server instead. */
252
+ const EPUB_BUILD_PATHS = {
253
+ next: "",
254
+ "tanstack-start": ".output/public/export/epub",
255
+ "tanstack-start-spa": "dist/client/export/epub",
256
+ "react-router": "build/client/export/epub",
257
+ "react-router-spa": "build/client/export/epub",
258
+ waku: "dist/public/export/epub"
259
+ };
260
+ const API_ROUTE_TEMPLATE = `import { source } from '@/lib/source';
261
+ import { exportEpub } from 'fumadocs-epub';
621
262
 
263
+ export const revalidate = false;
264
+
265
+ export async function GET(request: Request): Promise<Response> {
266
+ // Require EXPORT_SECRET to prevent unauthenticated abuse. Pass via Authorization: Bearer <secret>
267
+ const secret = process.env.EXPORT_SECRET;
268
+ if (!secret) {
269
+ return new Response('EXPORT_SECRET is not configured. Set it in your environment to protect this endpoint.', { status: 503 });
270
+ }
271
+ const authHeader = request.headers.get('authorization');
272
+ const token = authHeader?.replace(/^Bearer\\s+/i, '') ?? '';
273
+ if (token !== secret) {
274
+ return new Response('Unauthorized', { status: authHeader ? 403 : 401 });
275
+ }
276
+ const buffer = await exportEpub({
277
+ source,
278
+ title: 'Documentation',
279
+ author: 'Your Team',
280
+ description: 'Exported documentation',
281
+ cover: '/cover.png',
282
+ });
283
+ return new Response(new Uint8Array(buffer), {
284
+ headers: {
285
+ 'Content-Type': 'application/epub+zip',
286
+ 'Content-Disposition': 'attachment; filename="docs.epub"',
287
+ },
288
+ });
289
+ }
290
+ `;
291
+ async function findAppDir(cwd) {
292
+ for (const appPath of ["app", "src/app"]) {
293
+ const fullPath = path.join(cwd, appPath);
294
+ if (await exists(fullPath)) return fullPath;
295
+ }
296
+ return null;
297
+ }
298
+ async function scaffoldEpubRoute(cwd) {
299
+ const appDir = await findAppDir(cwd);
300
+ if (!appDir) {
301
+ console.error(picocolors.red("Could not find app directory (app/ or src/app/)"));
302
+ return false;
303
+ }
304
+ const routePath = path.join(appDir, "export", "epub", "route.ts");
305
+ if (await exists(routePath)) {
306
+ console.log(picocolors.yellow("EPUB route already exists at"), routePath);
307
+ return true;
308
+ }
309
+ await fs.mkdir(path.dirname(routePath), { recursive: true });
310
+ await fs.writeFile(routePath, API_ROUTE_TEMPLATE);
311
+ console.log(picocolors.green("Created EPUB route at"), routePath);
312
+ return true;
313
+ }
314
+ async function exportEpub(options) {
315
+ const cwd = process.cwd();
316
+ const outputPath = path.resolve(cwd, options.output ?? "docs.epub");
317
+ const framework = options.framework;
318
+ const spin = spinner();
319
+ const buildPath = EPUB_BUILD_PATHS[framework];
320
+ if (!(framework in EPUB_BUILD_PATHS)) {
321
+ const valid = Object.keys(EPUB_BUILD_PATHS).join(", ");
322
+ console.error(picocolors.red(`Invalid --framework "${framework}". Must be one of: ${valid}`));
323
+ process.exit(1);
324
+ }
325
+ const pkg = await readPackageJson(cwd);
326
+ const hasNextConfig = await exists(path.join(cwd, "next.config.js")) || await exists(path.join(cwd, "next.config.ts")) || await exists(path.join(cwd, "next.config.mjs"));
327
+ const hasNextInPkg = !!(pkg ? {
328
+ ...pkg.dependencies,
329
+ ...pkg.devDependencies,
330
+ ...pkg.peerDependencies
331
+ } : {})?.next;
332
+ const hasAppOrPages = await exists(path.join(cwd, "app")) || await exists(path.join(cwd, "pages")) || await exists(path.join(cwd, "src", "app")) || await exists(path.join(cwd, "src", "pages"));
333
+ if (!(hasNextConfig || hasNextInPkg && hasAppOrPages) && framework === "next") {
334
+ console.error(picocolors.red("Next.js project not found. Run this command from a Fumadocs Next.js project root."));
335
+ process.exit(1);
336
+ }
337
+ if (framework === "next") {
338
+ spin.start("Scaffolding EPUB route");
339
+ const scaffolded = await scaffoldEpubRoute(cwd);
340
+ spin.stop(scaffolded ? "EPUB route ready" : "Scaffolding failed");
341
+ if (!scaffolded) process.exit(1);
342
+ }
343
+ if (options.scaffoldOnly) {
344
+ console.log(picocolors.cyan("\nTo export:"));
345
+ console.log(" 1. Add fumadocs-epub to your dependencies: pnpm add fumadocs-epub");
346
+ console.log(" 2. Ensure includeProcessedMarkdown: true in your docs collection config");
347
+ if (framework === "next") {
348
+ console.log(" 3. Set EXPORT_SECRET in your environment to protect the /export/epub endpoint");
349
+ console.log(" 4. Run production build: pnpm build");
350
+ console.log(" 5. Start the server (e.g. pnpm start) and keep it running");
351
+ console.log(" 6. Run: fumadocs export epub --framework next");
352
+ } else {
353
+ console.log(` 3. Add a prerender route that outputs EPUB to ${buildPath}`);
354
+ console.log(" 4. Run production build: pnpm build");
355
+ console.log(` 5. Run: fumadocs export epub --framework ${framework}`);
356
+ }
357
+ return;
358
+ }
359
+ if (!pkg) {
360
+ console.error(picocolors.red("Cannot read or parse package.json. Ensure it exists and is valid JSON."));
361
+ process.exit(1);
362
+ }
363
+ if (!{
364
+ ...pkg.dependencies,
365
+ ...pkg.devDependencies
366
+ }["fumadocs-epub"]) {
367
+ console.log(picocolors.yellow("\nInstalling fumadocs-epub..."));
368
+ const installCmd = `${process.env.npm_execpath?.includes("pnpm") ? "pnpm" : process.env.npm_execpath?.includes("bun") ? "bun" : "npm"} add fumadocs-epub`;
369
+ try {
370
+ await execAsync(installCmd, { cwd });
371
+ } catch (err) {
372
+ const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr) : "";
373
+ console.error(picocolors.red(`Failed to install fumadocs-epub. Command: ${installCmd}`));
374
+ if (stderr) console.error(stderr);
375
+ process.exit(1);
376
+ }
377
+ }
378
+ if (framework === "next") {
379
+ const secret = process.env.EXPORT_SECRET;
380
+ if (!secret) {
381
+ console.error(picocolors.red("EXPORT_SECRET is required for Next.js export. Set it in your environment."));
382
+ process.exit(1);
383
+ }
384
+ const port = process.env.PORT || "3000";
385
+ const url = `http://localhost:${port}/export/epub`;
386
+ spin.start("Fetching EPUB from server");
387
+ const controller = new AbortController();
388
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
389
+ try {
390
+ const res = await fetch(url, {
391
+ headers: { Authorization: `Bearer ${secret}` },
392
+ signal: controller.signal
393
+ });
394
+ if (!res.ok) {
395
+ if (res.status === 401 || res.status === 403) console.error(picocolors.red("Auth failed. Check that EXPORT_SECRET matches the value in your app."));
396
+ else console.error(picocolors.red(`Server returned ${res.status}. Ensure the app is running (e.g. pnpm start) on port ${port}.`));
397
+ process.exit(1);
398
+ }
399
+ const buffer = Buffer.from(await res.arrayBuffer());
400
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
401
+ await fs.writeFile(outputPath, buffer);
402
+ spin.stop(picocolors.green(`EPUB saved to ${outputPath}`));
403
+ } catch (err) {
404
+ if (err instanceof Error && err.name === "AbortError") console.error(picocolors.red("Request timed out after 30 seconds."));
405
+ else {
406
+ const msg = err instanceof Error ? err.message : String(err);
407
+ console.error(picocolors.red(`Could not fetch EPUB: ${msg}`));
408
+ }
409
+ console.error(picocolors.yellow(`Ensure the server is running (e.g. pnpm start) on port ${port}.`));
410
+ process.exit(1);
411
+ } finally {
412
+ clearTimeout(timeoutId);
413
+ }
414
+ return;
415
+ }
416
+ const fullBuildPath = path.join(cwd, buildPath);
417
+ if (!await exists(fullBuildPath)) {
418
+ console.error(picocolors.red(`EPUB not found at ${buildPath}. Run production build first (e.g. pnpm build).`));
419
+ process.exit(1);
420
+ }
421
+ spin.start("Copying EPUB");
422
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
423
+ await fs.copyFile(fullBuildPath, outputPath);
424
+ spin.stop(picocolors.green(`EPUB saved to ${outputPath}`));
425
+ }
622
426
  //#endregion
623
427
  //#region src/index.ts
624
428
  const program = new Command().option("--config <string>");
@@ -636,6 +440,13 @@ const dirShortcuts = {
636
440
  program.command("add").description("add a new component to your docs").argument("[components...]", "components to download").option("--dir <string>", "the root url or directory to resolve registry").action(async (input, options) => {
637
441
  await add(input, createClientFromDir(options.dir, await createOrLoadConfig(options.config)));
638
442
  });
443
+ program.command("export").description("export documentation to various formats").command("epub").description("export documentation to EPUB format (run after production build)").requiredOption("--framework <name>", "React framework: next, tanstack-start, react-router, waku").option("--output <path>", "output file path", "docs.epub").option("--scaffold-only", "only scaffold the EPUB route, do not copy").action(async (options) => {
444
+ await exportEpub({
445
+ output: options.output,
446
+ framework: options.framework,
447
+ scaffoldOnly: options.scaffoldOnly
448
+ });
449
+ });
639
450
  program.command("tree").argument("[json_or_args]", "JSON output of `tree` command or arguments for the `tree` command").argument("[output]", "output path of file").option("--js", "output as JavaScript file").option("--no-root", "remove the root node").option("--import-name <name>", "where to import components (JS only)").action(async (str, output, { js, root, importName }) => {
640
451
  const jsExtensions = [
641
452
  ".js",
@@ -660,7 +471,7 @@ function createClientFromDir(dir = "https://fumadocs.dev/registry", config) {
660
471
  return dir.startsWith("http://") || dir.startsWith("https://") ? new HttpRegistryClient(dir, config) : new LocalRegistryClient(dir, config);
661
472
  }
662
473
  program.parse();
663
-
664
474
  //#endregion
665
- export { };
475
+ export {};
476
+
666
477
  //# sourceMappingURL=index.js.map