@fumadocs/cli 1.2.5 → 1.3.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/dist/{ast-BS3xj9uY.js → ast-BRNdmLn5.js} +2 -4
- package/dist/{ast-BS3xj9uY.js.map → ast-BRNdmLn5.js.map} +1 -1
- package/dist/build/index.d.ts +7 -37
- package/dist/build/index.d.ts.map +1 -1
- package/dist/build/index.js +18 -13
- package/dist/build/index.js.map +1 -1
- package/dist/client-YTcWP1iz.js +102 -0
- package/dist/client-YTcWP1iz.js.map +1 -0
- package/dist/config-DH5Ggyir.js +55 -0
- package/dist/config-DH5Ggyir.js.map +1 -0
- package/dist/config-ndMKrpJz.d.ts +49 -0
- package/dist/config-ndMKrpJz.d.ts.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/fs-CigSthjp.js +18 -0
- package/dist/fs-CigSthjp.js.map +1 -0
- package/dist/index.js +249 -391
- package/dist/index.js.map +1 -1
- package/dist/installer-BS70ExnQ.js +248 -0
- package/dist/installer-BS70ExnQ.js.map +1 -0
- package/dist/registry/client.d.ts +94 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/client.js +2 -0
- package/dist/registry/installer/index.d.ts +106 -0
- package/dist/registry/installer/index.d.ts.map +1 -0
- package/dist/registry/installer/index.js +4 -0
- package/dist/registry/schema.d.ts +2 -0
- package/dist/registry/schema.js +46 -0
- package/dist/registry/schema.js.map +1 -0
- package/dist/schema-CKNbRBjk.d.ts +61 -0
- package/dist/schema-CKNbRBjk.d.ts.map +1 -0
- package/package.json +14 -13
package/dist/index.js
CHANGED
|
@@ -1,75 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { t as exists } from "./fs-CigSthjp.js";
|
|
3
|
+
import { i as initConfig, n as createOrLoadConfig } from "./config-DH5Ggyir.js";
|
|
4
|
+
import { n as transformSpecifiers } from "./ast-BRNdmLn5.js";
|
|
5
|
+
import { n as LocalRegistryClient, t as HttpRegistryClient } from "./client-YTcWP1iz.js";
|
|
6
|
+
import { t as ComponentInstaller } from "./installer-BS70ExnQ.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 {
|
|
11
|
-
import {
|
|
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,342 +61,68 @@ 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.
|
|
127
|
-
|
|
66
|
+
var version = "1.3.0";
|
|
128
67
|
//#endregion
|
|
129
|
-
//#region src/
|
|
130
|
-
const
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
|
|
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
|
-
|
|
68
|
+
//#region src/commands/shared.ts
|
|
69
|
+
const UIRegistries = {
|
|
70
|
+
"base-ui": "fumadocs/base-ui",
|
|
71
|
+
"radix-ui": "fumadocs/radix-ui"
|
|
72
|
+
};
|
|
173
73
|
//#endregion
|
|
174
|
-
//#region src/
|
|
74
|
+
//#region src/registry/plugins/preserve.ts
|
|
175
75
|
/**
|
|
176
|
-
*
|
|
76
|
+
* keep references to `fumadocs-ui/layouts/*` components as original, unless the user is installing them direclty.
|
|
177
77
|
*/
|
|
178
|
-
function
|
|
78
|
+
function pluginPreserveLayouts() {
|
|
79
|
+
const layoutNames = [
|
|
80
|
+
"layouts/home",
|
|
81
|
+
"layouts/flux",
|
|
82
|
+
"layouts/notebook",
|
|
83
|
+
"layouts/docs",
|
|
84
|
+
"layouts/shared"
|
|
85
|
+
];
|
|
86
|
+
const layoutComps = {
|
|
87
|
+
"@/<dir>/layout/home/index.tsx": "layouts/home",
|
|
88
|
+
"@/<dir>/layout/shared/index.tsx": "layouts/shared",
|
|
89
|
+
"@/<dir>/layout/shared/client.tsx": "layouts/shared",
|
|
90
|
+
"@/<dir>/layout/notebook/index.tsx": "layouts/notebook",
|
|
91
|
+
"@/<dir>/layout/notebook/client.tsx": "layouts/notebook",
|
|
92
|
+
"@/<dir>/layout/notebook/page/index.tsx": "layouts/notebook/page",
|
|
93
|
+
"@/<dir>/layout/notebook/page/client.tsx": "layouts/notebook/page",
|
|
94
|
+
"@/<dir>/layout/docs/index.tsx": "layouts/docs",
|
|
95
|
+
"@/<dir>/layout/docs/client.tsx": "layouts/docs",
|
|
96
|
+
"@/<dir>/layout/docs/page/index.tsx": "layouts/docs/page",
|
|
97
|
+
"@/<dir>/layout/docs/page/client.tsx": "layouts/docs/page",
|
|
98
|
+
"@/<dir>/layout/flux/index.tsx": "layouts/flux",
|
|
99
|
+
"@/<dir>/layout/flux/page/index.tsx": "layouts/flux/page",
|
|
100
|
+
"@/<dir>/layout/flux/page/client.tsx": "layouts/flux/page"
|
|
101
|
+
};
|
|
102
|
+
const layoutNameSet = new Set(layoutNames);
|
|
179
103
|
return {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return out;
|
|
187
|
-
});
|
|
188
|
-
store.set(key, cached);
|
|
189
|
-
return cached;
|
|
104
|
+
beforeInstall(comp, { stack }) {
|
|
105
|
+
if (layoutNameSet.has(stack[0].name)) return;
|
|
106
|
+
return {
|
|
107
|
+
...comp,
|
|
108
|
+
$subComponents: comp.$subComponents.filter((child) => !layoutNameSet.has(child.name))
|
|
109
|
+
};
|
|
190
110
|
},
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
111
|
+
beforeTransform({ parsed, s, stack }) {
|
|
112
|
+
if (layoutNameSet.has(stack[0].name)) return;
|
|
113
|
+
transformSpecifiers(parsed.program, s, (specifier) => {
|
|
114
|
+
if (!(specifier in layoutComps)) return specifier;
|
|
115
|
+
return `fumadocs-ui/${layoutComps[specifier]}`;
|
|
116
|
+
});
|
|
196
117
|
}
|
|
197
118
|
};
|
|
198
119
|
}
|
|
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
|
-
|
|
446
|
-
//#endregion
|
|
447
|
-
//#region src/commands/shared.ts
|
|
448
|
-
const UIRegistries = {
|
|
449
|
-
"base-ui": "fumadocs/base-ui",
|
|
450
|
-
"radix-ui": "fumadocs/radix-ui"
|
|
451
|
-
};
|
|
452
|
-
|
|
453
120
|
//#endregion
|
|
454
121
|
//#region src/commands/add.ts
|
|
455
122
|
async function add(input, client) {
|
|
456
123
|
const config = client.config;
|
|
457
124
|
let target;
|
|
458
|
-
const installer = new ComponentInstaller(client);
|
|
125
|
+
const installer = new ComponentInstaller(client, { plugins: [pluginPreserveLayouts()] });
|
|
459
126
|
const registry = UIRegistries[config.uiLibrary];
|
|
460
127
|
if (input.length === 0) {
|
|
461
128
|
const spin = spinner();
|
|
@@ -537,13 +204,12 @@ async function install(target, installer) {
|
|
|
537
204
|
await installer.onEnd();
|
|
538
205
|
outro(picocolors.bold(picocolors.greenBright("Successful")));
|
|
539
206
|
}
|
|
540
|
-
|
|
541
207
|
//#endregion
|
|
542
208
|
//#region src/commands/customise.ts
|
|
543
209
|
async function customise(client) {
|
|
544
210
|
intro(picocolors.bgBlack(picocolors.whiteBright("Customise Fumadocs UI")));
|
|
545
211
|
const config = client.config;
|
|
546
|
-
const installer = new ComponentInstaller(client);
|
|
212
|
+
const installer = new ComponentInstaller(client, { plugins: [pluginPreserveLayouts()] });
|
|
547
213
|
const registry = UIRegistries[config.uiLibrary];
|
|
548
214
|
const target = (await group({
|
|
549
215
|
layout: () => select({
|
|
@@ -570,7 +236,7 @@ async function customise(client) {
|
|
|
570
236
|
label: "Start from minimal styles",
|
|
571
237
|
hint: "for those who want to build their own variant from ground up.",
|
|
572
238
|
value: {
|
|
573
|
-
target: ["
|
|
239
|
+
target: ["layouts/docs-min"],
|
|
574
240
|
replace: [["fumadocs-ui/layouts/docs", "@/components/layout/docs"], ["fumadocs-ui/layouts/docs/page", "@/components/layout/docs/page"]]
|
|
575
241
|
}
|
|
576
242
|
},
|
|
@@ -618,7 +284,192 @@ function printNext(...maps) {
|
|
|
618
284
|
...maps.map(([from, to]) => picocolors.greenBright(`"${from}" -> "${to}"`))
|
|
619
285
|
].join("\n"));
|
|
620
286
|
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/commands/export-epub.ts
|
|
289
|
+
const execAsync = promisify(exec);
|
|
290
|
+
async function readPackageJson(cwd) {
|
|
291
|
+
try {
|
|
292
|
+
const raw = await fs.readFile(path.join(cwd, "package.json"), "utf-8");
|
|
293
|
+
return JSON.parse(raw);
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Path of pre-rendered EPUB, choose one according to your React framework. Next.js fetches from the running server instead. */
|
|
299
|
+
const EPUB_BUILD_PATHS = {
|
|
300
|
+
next: "",
|
|
301
|
+
"tanstack-start": ".output/public/export/epub",
|
|
302
|
+
"tanstack-start-spa": "dist/client/export/epub",
|
|
303
|
+
"react-router": "build/client/export/epub",
|
|
304
|
+
"react-router-spa": "build/client/export/epub",
|
|
305
|
+
waku: "dist/public/export/epub"
|
|
306
|
+
};
|
|
307
|
+
const API_ROUTE_TEMPLATE = `import { source } from '@/lib/source';
|
|
308
|
+
import { exportEpub } from 'fumadocs-epub';
|
|
309
|
+
|
|
310
|
+
export const revalidate = false;
|
|
621
311
|
|
|
312
|
+
export async function GET(request: Request): Promise<Response> {
|
|
313
|
+
// Require EXPORT_SECRET to prevent unauthenticated abuse. Pass via Authorization: Bearer <secret>
|
|
314
|
+
const secret = process.env.EXPORT_SECRET;
|
|
315
|
+
if (!secret) {
|
|
316
|
+
return new Response('EXPORT_SECRET is not configured. Set it in your environment to protect this endpoint.', { status: 503 });
|
|
317
|
+
}
|
|
318
|
+
const authHeader = request.headers.get('authorization');
|
|
319
|
+
const token = authHeader?.replace(/^Bearer\\s+/i, '') ?? '';
|
|
320
|
+
if (token !== secret) {
|
|
321
|
+
return new Response('Unauthorized', { status: authHeader ? 403 : 401 });
|
|
322
|
+
}
|
|
323
|
+
const buffer = await exportEpub({
|
|
324
|
+
source,
|
|
325
|
+
title: 'Documentation',
|
|
326
|
+
author: 'Your Team',
|
|
327
|
+
description: 'Exported documentation',
|
|
328
|
+
cover: '/cover.png',
|
|
329
|
+
});
|
|
330
|
+
return new Response(new Uint8Array(buffer), {
|
|
331
|
+
headers: {
|
|
332
|
+
'Content-Type': 'application/epub+zip',
|
|
333
|
+
'Content-Disposition': 'attachment; filename="docs.epub"',
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
`;
|
|
338
|
+
async function findAppDir(cwd) {
|
|
339
|
+
for (const appPath of ["app", "src/app"]) {
|
|
340
|
+
const fullPath = path.join(cwd, appPath);
|
|
341
|
+
if (await exists(fullPath)) return fullPath;
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
async function scaffoldEpubRoute(cwd) {
|
|
346
|
+
const appDir = await findAppDir(cwd);
|
|
347
|
+
if (!appDir) {
|
|
348
|
+
console.error(picocolors.red("Could not find app directory (app/ or src/app/)"));
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
const routePath = path.join(appDir, "export", "epub", "route.ts");
|
|
352
|
+
if (await exists(routePath)) {
|
|
353
|
+
console.log(picocolors.yellow("EPUB route already exists at"), routePath);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
await fs.mkdir(path.dirname(routePath), { recursive: true });
|
|
357
|
+
await fs.writeFile(routePath, API_ROUTE_TEMPLATE);
|
|
358
|
+
console.log(picocolors.green("Created EPUB route at"), routePath);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
async function exportEpub(options) {
|
|
362
|
+
const cwd = process.cwd();
|
|
363
|
+
const outputPath = path.resolve(cwd, options.output ?? "docs.epub");
|
|
364
|
+
const framework = options.framework;
|
|
365
|
+
const spin = spinner();
|
|
366
|
+
const buildPath = EPUB_BUILD_PATHS[framework];
|
|
367
|
+
if (!(framework in EPUB_BUILD_PATHS)) {
|
|
368
|
+
const valid = Object.keys(EPUB_BUILD_PATHS).join(", ");
|
|
369
|
+
console.error(picocolors.red(`Invalid --framework "${framework}". Must be one of: ${valid}`));
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
const pkg = await readPackageJson(cwd);
|
|
373
|
+
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"));
|
|
374
|
+
const hasNextInPkg = !!(pkg ? {
|
|
375
|
+
...pkg.dependencies,
|
|
376
|
+
...pkg.devDependencies,
|
|
377
|
+
...pkg.peerDependencies
|
|
378
|
+
} : {})?.next;
|
|
379
|
+
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"));
|
|
380
|
+
if (!(hasNextConfig || hasNextInPkg && hasAppOrPages) && framework === "next") {
|
|
381
|
+
console.error(picocolors.red("Next.js project not found. Run this command from a Fumadocs Next.js project root."));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
if (framework === "next") {
|
|
385
|
+
spin.start("Scaffolding EPUB route");
|
|
386
|
+
const scaffolded = await scaffoldEpubRoute(cwd);
|
|
387
|
+
spin.stop(scaffolded ? "EPUB route ready" : "Scaffolding failed");
|
|
388
|
+
if (!scaffolded) process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
if (options.scaffoldOnly) {
|
|
391
|
+
console.log(picocolors.cyan("\nTo export:"));
|
|
392
|
+
console.log(" 1. Add fumadocs-epub to your dependencies: pnpm add fumadocs-epub");
|
|
393
|
+
console.log(" 2. Ensure includeProcessedMarkdown: true in your docs collection config");
|
|
394
|
+
if (framework === "next") {
|
|
395
|
+
console.log(" 3. Set EXPORT_SECRET in your environment to protect the /export/epub endpoint");
|
|
396
|
+
console.log(" 4. Run production build: pnpm build");
|
|
397
|
+
console.log(" 5. Start the server (e.g. pnpm start) and keep it running");
|
|
398
|
+
console.log(" 6. Run: fumadocs export epub --framework next");
|
|
399
|
+
} else {
|
|
400
|
+
console.log(` 3. Add a prerender route that outputs EPUB to ${buildPath}`);
|
|
401
|
+
console.log(" 4. Run production build: pnpm build");
|
|
402
|
+
console.log(` 5. Run: fumadocs export epub --framework ${framework}`);
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (!pkg) {
|
|
407
|
+
console.error(picocolors.red("Cannot read or parse package.json. Ensure it exists and is valid JSON."));
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
if (!{
|
|
411
|
+
...pkg.dependencies,
|
|
412
|
+
...pkg.devDependencies
|
|
413
|
+
}["fumadocs-epub"]) {
|
|
414
|
+
console.log(picocolors.yellow("\nInstalling fumadocs-epub..."));
|
|
415
|
+
const installCmd = `${process.env.npm_execpath?.includes("pnpm") ? "pnpm" : process.env.npm_execpath?.includes("bun") ? "bun" : "npm"} add fumadocs-epub`;
|
|
416
|
+
try {
|
|
417
|
+
await execAsync(installCmd, { cwd });
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr) : "";
|
|
420
|
+
console.error(picocolors.red(`Failed to install fumadocs-epub. Command: ${installCmd}`));
|
|
421
|
+
if (stderr) console.error(stderr);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (framework === "next") {
|
|
426
|
+
const secret = process.env.EXPORT_SECRET;
|
|
427
|
+
if (!secret) {
|
|
428
|
+
console.error(picocolors.red("EXPORT_SECRET is required for Next.js export. Set it in your environment."));
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
const port = process.env.PORT || "3000";
|
|
432
|
+
const url = `http://localhost:${port}/export/epub`;
|
|
433
|
+
spin.start("Fetching EPUB from server");
|
|
434
|
+
const controller = new AbortController();
|
|
435
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
436
|
+
try {
|
|
437
|
+
const res = await fetch(url, {
|
|
438
|
+
headers: { Authorization: `Bearer ${secret}` },
|
|
439
|
+
signal: controller.signal
|
|
440
|
+
});
|
|
441
|
+
if (!res.ok) {
|
|
442
|
+
if (res.status === 401 || res.status === 403) console.error(picocolors.red("Auth failed. Check that EXPORT_SECRET matches the value in your app."));
|
|
443
|
+
else console.error(picocolors.red(`Server returned ${res.status}. Ensure the app is running (e.g. pnpm start) on port ${port}.`));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
447
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
448
|
+
await fs.writeFile(outputPath, buffer);
|
|
449
|
+
spin.stop(picocolors.green(`EPUB saved to ${outputPath}`));
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (err instanceof Error && err.name === "AbortError") console.error(picocolors.red("Request timed out after 30 seconds."));
|
|
452
|
+
else {
|
|
453
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
454
|
+
console.error(picocolors.red(`Could not fetch EPUB: ${msg}`));
|
|
455
|
+
}
|
|
456
|
+
console.error(picocolors.yellow(`Ensure the server is running (e.g. pnpm start) on port ${port}.`));
|
|
457
|
+
process.exit(1);
|
|
458
|
+
} finally {
|
|
459
|
+
clearTimeout(timeoutId);
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const fullBuildPath = path.join(cwd, buildPath);
|
|
464
|
+
if (!await exists(fullBuildPath)) {
|
|
465
|
+
console.error(picocolors.red(`EPUB not found at ${buildPath}. Run production build first (e.g. pnpm build).`));
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
spin.start("Copying EPUB");
|
|
469
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
470
|
+
await fs.copyFile(fullBuildPath, outputPath);
|
|
471
|
+
spin.stop(picocolors.green(`EPUB saved to ${outputPath}`));
|
|
472
|
+
}
|
|
622
473
|
//#endregion
|
|
623
474
|
//#region src/index.ts
|
|
624
475
|
const program = new Command().option("--config <string>");
|
|
@@ -636,6 +487,13 @@ const dirShortcuts = {
|
|
|
636
487
|
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
488
|
await add(input, createClientFromDir(options.dir, await createOrLoadConfig(options.config)));
|
|
638
489
|
});
|
|
490
|
+
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) => {
|
|
491
|
+
await exportEpub({
|
|
492
|
+
output: options.output,
|
|
493
|
+
framework: options.framework,
|
|
494
|
+
scaffoldOnly: options.scaffoldOnly
|
|
495
|
+
});
|
|
496
|
+
});
|
|
639
497
|
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
498
|
const jsExtensions = [
|
|
641
499
|
".js",
|
|
@@ -660,7 +518,7 @@ function createClientFromDir(dir = "https://fumadocs.dev/registry", config) {
|
|
|
660
518
|
return dir.startsWith("http://") || dir.startsWith("https://") ? new HttpRegistryClient(dir, config) : new LocalRegistryClient(dir, config);
|
|
661
519
|
}
|
|
662
520
|
program.parse();
|
|
663
|
-
|
|
664
521
|
//#endregion
|
|
665
|
-
export {
|
|
522
|
+
export {};
|
|
523
|
+
|
|
666
524
|
//# sourceMappingURL=index.js.map
|