@fumadocs/cli 0.0.1
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/LICENSE +21 -0
- package/dist/build/index.d.ts +104 -0
- package/dist/build/index.js +284 -0
- package/dist/chunk-WBCEM7RC.js +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1044 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
exists,
|
|
4
|
+
isRelative
|
|
5
|
+
} from "./chunk-WBCEM7RC.js";
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
import fs10 from "node:fs/promises";
|
|
9
|
+
import path9 from "node:path";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import picocolors5 from "picocolors";
|
|
12
|
+
import {
|
|
13
|
+
isCancel as isCancel3,
|
|
14
|
+
log as log5,
|
|
15
|
+
multiselect,
|
|
16
|
+
outro as outro2,
|
|
17
|
+
select,
|
|
18
|
+
spinner as spinner3
|
|
19
|
+
} from "@clack/prompts";
|
|
20
|
+
|
|
21
|
+
// src/commands/init.ts
|
|
22
|
+
import * as process2 from "node:process";
|
|
23
|
+
import path3 from "node:path";
|
|
24
|
+
import {
|
|
25
|
+
intro,
|
|
26
|
+
confirm,
|
|
27
|
+
isCancel,
|
|
28
|
+
cancel,
|
|
29
|
+
spinner,
|
|
30
|
+
log,
|
|
31
|
+
note
|
|
32
|
+
} from "@clack/prompts";
|
|
33
|
+
import picocolors from "picocolors";
|
|
34
|
+
import { execa } from "execa";
|
|
35
|
+
|
|
36
|
+
// src/utils/get-package-manager.ts
|
|
37
|
+
import { detect } from "package-manager-detector";
|
|
38
|
+
async function getPackageManager() {
|
|
39
|
+
const result = await detect();
|
|
40
|
+
return result?.name ?? "npm";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/utils/is-src.ts
|
|
44
|
+
import path from "node:path";
|
|
45
|
+
async function isSrc() {
|
|
46
|
+
return exists("./src");
|
|
47
|
+
}
|
|
48
|
+
function resolveAppPath(filePath, src2) {
|
|
49
|
+
return src2 ? path.join("./src", filePath) : filePath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/utils/transform-references.ts
|
|
53
|
+
import path2 from "node:path";
|
|
54
|
+
|
|
55
|
+
// src/config.ts
|
|
56
|
+
import fs from "node:fs/promises";
|
|
57
|
+
var src = await isSrc();
|
|
58
|
+
var defaultConfig = {
|
|
59
|
+
aliases: {
|
|
60
|
+
cn: src ? "./src/lib/utils.ts" : "./lib/utils.ts",
|
|
61
|
+
componentsDir: src ? "./src/components" : "./components",
|
|
62
|
+
uiDir: src ? "./src/components/ui" : "./components/ui",
|
|
63
|
+
libDir: src ? "./src/lib" : "./lib"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
async function loadConfig(file = "./cli.json") {
|
|
67
|
+
try {
|
|
68
|
+
const content = await fs.readFile(file);
|
|
69
|
+
return JSON.parse(content.toString());
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function initConfig(file = "./cli.json") {
|
|
75
|
+
await fs.writeFile(file, JSON.stringify(defaultConfig, null, 2));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/constants.ts
|
|
79
|
+
var typescriptExtensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
80
|
+
|
|
81
|
+
// src/utils/transform-references.ts
|
|
82
|
+
function getOutputPath(ref, config) {
|
|
83
|
+
if (path2.isAbsolute(ref)) throw new Error(`path cannot be absolute: ${ref}`);
|
|
84
|
+
if (ref === "utils/cn" || ref === "utils/cn.ts") {
|
|
85
|
+
return config.aliases?.cn ?? defaultConfig.aliases.cn;
|
|
86
|
+
}
|
|
87
|
+
if (ref.startsWith("components")) {
|
|
88
|
+
return path2.join(
|
|
89
|
+
config.aliases?.componentsDir ?? defaultConfig.aliases.componentsDir,
|
|
90
|
+
path2.relative("components", ref)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (ref.startsWith("lib") || ref.startsWith("utils")) {
|
|
94
|
+
return path2.join(
|
|
95
|
+
config.aliases?.libDir ?? defaultConfig.aliases.libDir,
|
|
96
|
+
path2.relative("lib", ref)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return ref;
|
|
100
|
+
}
|
|
101
|
+
async function transformReferences(file, resolver, transform) {
|
|
102
|
+
for (const item of file.getImportDeclarations()) {
|
|
103
|
+
const ref = item.getModuleSpecifier().getLiteralValue();
|
|
104
|
+
const result = await transform(resolveReference(ref, resolver), ref);
|
|
105
|
+
if (!result) continue;
|
|
106
|
+
item.getModuleSpecifier().setLiteralValue(result);
|
|
107
|
+
}
|
|
108
|
+
for (const item of file.getExportDeclarations()) {
|
|
109
|
+
const specifier = item.getModuleSpecifier();
|
|
110
|
+
if (!specifier) continue;
|
|
111
|
+
const ref = specifier.getLiteralValue();
|
|
112
|
+
const result = await transform(resolveReference(ref, resolver), ref);
|
|
113
|
+
if (!result) continue;
|
|
114
|
+
specifier.setLiteralValue(result);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function toReferencePath(sourceFile, referenceFile) {
|
|
118
|
+
const extname = path2.extname(referenceFile);
|
|
119
|
+
const importPath = path2.relative(
|
|
120
|
+
path2.dirname(sourceFile),
|
|
121
|
+
path2.join(
|
|
122
|
+
path2.dirname(referenceFile),
|
|
123
|
+
path2.basename(
|
|
124
|
+
referenceFile,
|
|
125
|
+
typescriptExtensions.includes(extname) ? extname : void 0
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
return importPath.startsWith("../") ? importPath : `./${importPath}`;
|
|
130
|
+
}
|
|
131
|
+
function resolveReference(ref, resolver) {
|
|
132
|
+
if (ref.startsWith("./") || ref.startsWith("../")) {
|
|
133
|
+
return {
|
|
134
|
+
type: "file",
|
|
135
|
+
path: path2.join(resolver.relativeTo, ref)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (ref.startsWith("@/")) {
|
|
139
|
+
const rest = ref.slice("@/".length);
|
|
140
|
+
if (!resolver.alias) throw new Error("alias resolver is not configured");
|
|
141
|
+
return {
|
|
142
|
+
type: "file",
|
|
143
|
+
path: path2.join(resolver.alias.dir, rest)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (ref.startsWith("@")) {
|
|
147
|
+
const segments = ref.split("/");
|
|
148
|
+
return {
|
|
149
|
+
type: "dep",
|
|
150
|
+
name: segments.slice(0, 2).join("/")
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
type: "dep",
|
|
155
|
+
name: ref.split("/")[0]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/utils/typescript.ts
|
|
160
|
+
import { Project } from "ts-morph";
|
|
161
|
+
function createEmptyProject() {
|
|
162
|
+
return new Project({
|
|
163
|
+
compilerOptions: {}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/commands/init.ts
|
|
168
|
+
async function init(plugin, config = {}) {
|
|
169
|
+
intro(
|
|
170
|
+
picocolors.bgCyan(picocolors.black(picocolors.bold("Installing Plugins")))
|
|
171
|
+
);
|
|
172
|
+
const ctx = {
|
|
173
|
+
src: await isSrc(),
|
|
174
|
+
outFileMap: /* @__PURE__ */ new Map(),
|
|
175
|
+
...config
|
|
176
|
+
};
|
|
177
|
+
const files = await plugin.files(ctx);
|
|
178
|
+
const project = createEmptyProject();
|
|
179
|
+
for (const [name, content] of Object.entries(files)) {
|
|
180
|
+
const file = getOutputPath(name, ctx);
|
|
181
|
+
ctx.outFileMap.set(name, file);
|
|
182
|
+
log.step(picocolors.green(`Writing ${file} \u2605`));
|
|
183
|
+
if (await exists(file)) {
|
|
184
|
+
const value = await confirm({
|
|
185
|
+
message: `${file} already exists`,
|
|
186
|
+
active: "Override",
|
|
187
|
+
inactive: "Skip"
|
|
188
|
+
});
|
|
189
|
+
if (isCancel(value)) {
|
|
190
|
+
cancel("Operation cancelled.");
|
|
191
|
+
process2.exit(0);
|
|
192
|
+
}
|
|
193
|
+
if (!value) continue;
|
|
194
|
+
}
|
|
195
|
+
const sourceFile = project.createSourceFile(file, content, {
|
|
196
|
+
overwrite: true
|
|
197
|
+
});
|
|
198
|
+
await transformReferences(
|
|
199
|
+
sourceFile,
|
|
200
|
+
{
|
|
201
|
+
alias: {
|
|
202
|
+
type: "append",
|
|
203
|
+
dir: ctx.src ? "src" : ""
|
|
204
|
+
},
|
|
205
|
+
relativeTo: path3.dirname(file)
|
|
206
|
+
},
|
|
207
|
+
(resolved) => {
|
|
208
|
+
if (resolved.type !== "file") return;
|
|
209
|
+
return toReferencePath(file, getOutputPath(resolved.path, ctx));
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
await sourceFile.save();
|
|
213
|
+
}
|
|
214
|
+
if (plugin.dependencies.length > 0) {
|
|
215
|
+
const manager = await getPackageManager();
|
|
216
|
+
const value = await confirm({
|
|
217
|
+
message: `This plugin contains additional dependencies, do you want to install them? (detected: ${manager})`
|
|
218
|
+
});
|
|
219
|
+
if (isCancel(value)) {
|
|
220
|
+
cancel("Operation cancelled.");
|
|
221
|
+
process2.exit(0);
|
|
222
|
+
}
|
|
223
|
+
if (value) {
|
|
224
|
+
const spin = spinner();
|
|
225
|
+
spin.start("Installing dependencies");
|
|
226
|
+
await execa(manager, ["install", ...plugin.dependencies]);
|
|
227
|
+
spin.stop("Successfully installed.");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (plugin.transform) {
|
|
231
|
+
const value = await confirm({
|
|
232
|
+
message: "This plugin contains changes to your files, do you want to apply them?"
|
|
233
|
+
});
|
|
234
|
+
if (isCancel(value)) {
|
|
235
|
+
cancel("Operation cancelled.");
|
|
236
|
+
process2.exit(0);
|
|
237
|
+
}
|
|
238
|
+
if (value) {
|
|
239
|
+
await plugin.transform(ctx);
|
|
240
|
+
note(
|
|
241
|
+
`You can format the output with Prettier or other code formating tools
|
|
242
|
+
prettier . --write`,
|
|
243
|
+
picocolors.bold(picocolors.green("Changes Applied"))
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
await plugin.transformRejected?.(ctx);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const instructions = await plugin.instructions(ctx);
|
|
250
|
+
for (const text of instructions) {
|
|
251
|
+
if (text.type === "text") {
|
|
252
|
+
log.message(text.text, {
|
|
253
|
+
symbol: "\u25CB"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (text.type === "code") {
|
|
257
|
+
note(text.code, text.title);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/commands/add.ts
|
|
263
|
+
import path4 from "node:path";
|
|
264
|
+
import fs3 from "node:fs/promises";
|
|
265
|
+
import { log as log2, confirm as confirm2, isCancel as isCancel2, outro, spinner as spinner2, intro as intro2 } from "@clack/prompts";
|
|
266
|
+
import picocolors2 from "picocolors";
|
|
267
|
+
import { execa as execa2 } from "execa";
|
|
268
|
+
|
|
269
|
+
// src/utils/add/get-dependencies.ts
|
|
270
|
+
import fs2 from "node:fs/promises";
|
|
271
|
+
async function getDependencies() {
|
|
272
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
273
|
+
if (!await exists("package.json")) return dependencies;
|
|
274
|
+
const content = await fs2.readFile("package.json");
|
|
275
|
+
const parsed = JSON.parse(content.toString());
|
|
276
|
+
if ("dependencies" in parsed && typeof parsed.dependencies === "object") {
|
|
277
|
+
const records = parsed.dependencies;
|
|
278
|
+
Object.entries(records).forEach(([k, v]) => {
|
|
279
|
+
dependencies.set(k, v);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if ("devDependencies" in parsed && typeof parsed.devDependencies === "object") {
|
|
283
|
+
const records = parsed.devDependencies;
|
|
284
|
+
Object.entries(records).forEach(([k, v]) => {
|
|
285
|
+
dependencies.set(k, v);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return dependencies;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/commands/add.ts
|
|
292
|
+
var downloadedFiles = /* @__PURE__ */ new Set();
|
|
293
|
+
async function add(name, resolver, config = {}) {
|
|
294
|
+
intro2(
|
|
295
|
+
picocolors2.bold(
|
|
296
|
+
picocolors2.inverse(picocolors2.cyanBright(`Add Component: ${name}`))
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
const project = createEmptyProject();
|
|
300
|
+
const result = await downloadComponent(name, {
|
|
301
|
+
project,
|
|
302
|
+
config,
|
|
303
|
+
resolver
|
|
304
|
+
});
|
|
305
|
+
if (!result) {
|
|
306
|
+
log2.error(`Component: ${name} not found`);
|
|
307
|
+
process.exit(0);
|
|
308
|
+
}
|
|
309
|
+
const installed = await getDependencies();
|
|
310
|
+
const deps = Object.entries(result.dependencies).filter(([k]) => !installed.has(k)).map(([k, v]) => v.length === 0 ? k : `${k}@${v}`);
|
|
311
|
+
const devDeps = Object.entries(result.devDependencies).filter(([k]) => !installed.has(k)).map(([k, v]) => v.length === 0 ? k : `${k}@${v}`);
|
|
312
|
+
if (deps.length > 0 || devDeps.length > 0) {
|
|
313
|
+
const manager = await getPackageManager();
|
|
314
|
+
const value = await confirm2({
|
|
315
|
+
message: `This component requires dependencies (${[...deps, ...devDeps].join(" ")}), install them with ${manager}?`
|
|
316
|
+
});
|
|
317
|
+
if (isCancel2(value)) {
|
|
318
|
+
outro(picocolors2.bold(picocolors2.greenBright("Component downloaded")));
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
if (value) {
|
|
322
|
+
const spin = spinner2();
|
|
323
|
+
spin.start("Installing dependencies...");
|
|
324
|
+
if (deps.length > 0) await execa2(manager, ["install", ...deps]);
|
|
325
|
+
if (devDeps.length > 0)
|
|
326
|
+
await execa2(manager, ["install", ...devDeps, "-D"]);
|
|
327
|
+
spin.stop("Dependencies installed.");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
outro(picocolors2.bold(picocolors2.greenBright("Component installed")));
|
|
331
|
+
}
|
|
332
|
+
var downloadedComps = /* @__PURE__ */ new Map();
|
|
333
|
+
async function downloadComponent(name, ctx) {
|
|
334
|
+
const cached = downloadedComps.get(name);
|
|
335
|
+
if (cached) return cached;
|
|
336
|
+
const comp = await ctx.resolver(`${name}.json`);
|
|
337
|
+
if (!comp) return;
|
|
338
|
+
downloadedComps.set(name, comp);
|
|
339
|
+
for (const file of comp.files) {
|
|
340
|
+
if (downloadedFiles.has(file.path)) continue;
|
|
341
|
+
const outPath = resolveOutputPath(file.path, ctx.config);
|
|
342
|
+
const output = typescriptExtensions.includes(path4.extname(file.path)) ? transformTypeScript(outPath, file, ctx) : file.content;
|
|
343
|
+
let canWrite = true;
|
|
344
|
+
const requireOverride = await fs3.readFile(outPath).then((res) => res.toString() !== output).catch(() => false);
|
|
345
|
+
if (requireOverride) {
|
|
346
|
+
const value = await confirm2({
|
|
347
|
+
message: `Do you want to override ${outPath}?`
|
|
348
|
+
});
|
|
349
|
+
if (isCancel2(value)) {
|
|
350
|
+
outro("Ended");
|
|
351
|
+
process.exit(0);
|
|
352
|
+
}
|
|
353
|
+
canWrite = value;
|
|
354
|
+
}
|
|
355
|
+
if (canWrite) {
|
|
356
|
+
await fs3.mkdir(path4.dirname(outPath), { recursive: true });
|
|
357
|
+
await fs3.writeFile(outPath, output);
|
|
358
|
+
log2.step(`downloaded ${outPath}`);
|
|
359
|
+
}
|
|
360
|
+
downloadedFiles.add(file.path);
|
|
361
|
+
}
|
|
362
|
+
for (const sub of comp.subComponents) {
|
|
363
|
+
const downloaded = await downloadComponent(sub, ctx);
|
|
364
|
+
if (!downloaded) continue;
|
|
365
|
+
Object.assign(comp.dependencies, downloaded.dependencies);
|
|
366
|
+
Object.assign(comp.devDependencies, downloaded.devDependencies);
|
|
367
|
+
}
|
|
368
|
+
return comp;
|
|
369
|
+
}
|
|
370
|
+
function resolveOutputPath(ref, config) {
|
|
371
|
+
const sep = ref.indexOf(":");
|
|
372
|
+
if (sep === -1) return ref;
|
|
373
|
+
const namespace = ref.slice(0, sep), file = ref.slice(sep + 1);
|
|
374
|
+
if (namespace === "components") {
|
|
375
|
+
return path4.join(
|
|
376
|
+
config.aliases?.componentsDir ?? defaultConfig.aliases.componentsDir,
|
|
377
|
+
file
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return path4.join(
|
|
381
|
+
config.aliases?.libDir ?? defaultConfig.aliases.libDir,
|
|
382
|
+
file
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
function transformTypeScript(filePath, file, ctx) {
|
|
386
|
+
const sourceFile = ctx.project.createSourceFile(filePath, file.content, {
|
|
387
|
+
overwrite: true
|
|
388
|
+
});
|
|
389
|
+
for (const item of sourceFile.getImportDeclarations()) {
|
|
390
|
+
const ref = item.getModuleSpecifier().getLiteralValue();
|
|
391
|
+
if (ref in file.imports) {
|
|
392
|
+
const outputPath = resolveOutputPath(file.imports[ref], ctx.config);
|
|
393
|
+
item.getModuleSpecifier().setLiteralValue(toReferencePath(filePath, outputPath));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const item of sourceFile.getExportDeclarations()) {
|
|
397
|
+
const specifier = item.getModuleSpecifier();
|
|
398
|
+
if (!specifier) continue;
|
|
399
|
+
const ref = specifier.getLiteralValue();
|
|
400
|
+
if (ref in file.imports) {
|
|
401
|
+
const outputPath = resolveOutputPath(file.imports[ref], ctx.config);
|
|
402
|
+
specifier.setLiteralValue(toReferencePath(filePath, outputPath));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return sourceFile.getFullText();
|
|
406
|
+
}
|
|
407
|
+
function remoteResolver(url) {
|
|
408
|
+
return async (file) => {
|
|
409
|
+
const res = await fetch(`${url}/${file}`);
|
|
410
|
+
if (!res.ok) return;
|
|
411
|
+
return res.json();
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function localResolver(dir) {
|
|
415
|
+
return async (file) => {
|
|
416
|
+
return await fs3.readFile(path4.join(dir, file)).then((res) => JSON.parse(res.toString())).catch(() => void 0);
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/plugins/og-image.ts
|
|
421
|
+
import picocolors3 from "picocolors";
|
|
422
|
+
|
|
423
|
+
// src/generated.js
|
|
424
|
+
var generated = { "lib/metadata": "import { createMetadataImage } from 'fumadocs-core/server';\nimport { source } from '@/lib/source';\n\nexport const metadataImage = createMetadataImage({\n imageRoute: '/docs-og',\n source,\n});\n", "app/docs-og/[...slug]/route": "import { generateOGImage } from 'fumadocs-ui/og';\nimport { metadataImage } from '@/lib/metadata';\n\nexport const GET = metadataImage.createAPI((page) => {\n return generateOGImage({\n title: page.data.title,\n description: page.data.description,\n site: 'My App',\n });\n});\n\nexport function generateStaticParams() {\n return metadataImage.generateParams();\n}\n", "lib/i18n": "import type { I18nConfig } from 'fumadocs-core/i18n';\n\nexport const i18n: I18nConfig = {\n defaultLanguage: 'en',\n languages: ['en', 'cn'],\n};\n", "middleware": "import { createI18nMiddleware } from 'fumadocs-core/i18n';\nimport { i18n } from '@/lib/i18n';\n\nexport default createI18nMiddleware(i18n);\n\nexport const config = {\n // Matcher ignoring `/_next/` and `/api/`\n matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],\n};\n", "scripts/generate-docs": "import * as OpenAPI from 'fumadocs-openapi';\nimport { rimrafSync } from 'rimraf';\n\nconst out = './content/docs/(api)';\n\n// clean generated files\nrimrafSync(out, {\n filter(v) {\n return !v.endsWith('index.mdx') && !v.endsWith('meta.json');\n },\n});\n\nvoid OpenAPI.generateFiles({\n // input files\n input: ['./openapi.json'],\n output: out,\n groupBy: 'tag',\n});\n" };
|
|
425
|
+
|
|
426
|
+
// src/plugins/og-image.ts
|
|
427
|
+
function isI18nEnabled(ctx) {
|
|
428
|
+
return exists(getOutputPath("lib/i18n.ts", ctx));
|
|
429
|
+
}
|
|
430
|
+
var ogImagePlugin = {
|
|
431
|
+
files: async (ctx) => {
|
|
432
|
+
const route = await isI18nEnabled(ctx) ? "app/[lang]/docs-og/[...slug]/route.tsx" : "app/docs-og/[...slug]/route.tsx";
|
|
433
|
+
return {
|
|
434
|
+
"lib/metadata.ts": generated["lib/metadata"],
|
|
435
|
+
[route]: generated["app/docs-og/[...slug]/route"]
|
|
436
|
+
};
|
|
437
|
+
},
|
|
438
|
+
dependencies: [],
|
|
439
|
+
instructions: (ctx) => [
|
|
440
|
+
{
|
|
441
|
+
type: "text",
|
|
442
|
+
text: picocolors3.cyanBright(picocolors3.bold("Import the utils like:"))
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
type: "code",
|
|
446
|
+
title: "ts",
|
|
447
|
+
code: 'import { metadataImage } from "@/lib/metadata";'
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
type: "text",
|
|
451
|
+
text: picocolors3.cyanBright(
|
|
452
|
+
picocolors3.bold("Add the images to your metadata:")
|
|
453
|
+
)
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
type: "code",
|
|
457
|
+
title: resolveAppPath("app/docs/[[...slug]]/page.tsx", ctx.src),
|
|
458
|
+
code: `
|
|
459
|
+
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
|
|
460
|
+
const page = source.getPage(params.slug);
|
|
461
|
+
|
|
462
|
+
if (!page) notFound();
|
|
463
|
+
|
|
464
|
+
${picocolors3.bold(picocolors3.underline("return metadataImage.withImage(page.slugs, {"))}
|
|
465
|
+
title: page.data.title,
|
|
466
|
+
description: page.data.description,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
`.trim()
|
|
470
|
+
}
|
|
471
|
+
]
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// src/plugins/i18n.ts
|
|
475
|
+
import path7 from "node:path";
|
|
476
|
+
import picocolors4 from "picocolors";
|
|
477
|
+
import { log as log3 } from "@clack/prompts";
|
|
478
|
+
|
|
479
|
+
// src/utils/i18n/transform-layout-config.ts
|
|
480
|
+
import fs4 from "node:fs/promises";
|
|
481
|
+
import { SyntaxKind } from "ts-morph";
|
|
482
|
+
async function transformLayoutConfig(project, filePath) {
|
|
483
|
+
let content;
|
|
484
|
+
try {
|
|
485
|
+
content = await fs4.readFile(filePath).then((res) => res.toString());
|
|
486
|
+
} catch {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const sourceFile = project.createSourceFile(filePath, content, {
|
|
490
|
+
overwrite: true
|
|
491
|
+
});
|
|
492
|
+
const configExport = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration).find((node) => node.getName() === "baseOptions");
|
|
493
|
+
if (!configExport) return;
|
|
494
|
+
const init2 = configExport.getInitializerIfKind(
|
|
495
|
+
SyntaxKind.ObjectLiteralExpression
|
|
496
|
+
);
|
|
497
|
+
if (!init2) return;
|
|
498
|
+
if (init2.getProperty("i18n")) return;
|
|
499
|
+
init2.addPropertyAssignment({
|
|
500
|
+
name: "i18n",
|
|
501
|
+
initializer: "true"
|
|
502
|
+
});
|
|
503
|
+
return sourceFile.save();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/utils/move-files.ts
|
|
507
|
+
import fs5 from "node:fs/promises";
|
|
508
|
+
import path5 from "node:path";
|
|
509
|
+
var transformExtensions = [".js", ".ts", ".tsx", ".jsx"];
|
|
510
|
+
async function moveFiles(from, to, filter, project, src2, originalDir = from) {
|
|
511
|
+
const stats = await fs5.lstat(from).catch(() => void 0);
|
|
512
|
+
if (!stats) return;
|
|
513
|
+
if (stats.isDirectory()) {
|
|
514
|
+
const items = await fs5.readdir(from);
|
|
515
|
+
await Promise.all(
|
|
516
|
+
items.map(async (item) => {
|
|
517
|
+
await moveFiles(
|
|
518
|
+
path5.resolve(from, item),
|
|
519
|
+
path5.resolve(to, item),
|
|
520
|
+
filter,
|
|
521
|
+
project,
|
|
522
|
+
src2,
|
|
523
|
+
originalDir
|
|
524
|
+
);
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
await fs5.rmdir(from).catch(() => {
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
if (!stats.isFile()) return;
|
|
531
|
+
const allowed = await filter(path5.resolve(from));
|
|
532
|
+
if (!allowed) return;
|
|
533
|
+
if (transformExtensions.includes(path5.extname(from))) {
|
|
534
|
+
const content = await fs5.readFile(from);
|
|
535
|
+
const sourceFile = project.createSourceFile(from, content.toString(), {
|
|
536
|
+
overwrite: true
|
|
537
|
+
});
|
|
538
|
+
await transformReferences(
|
|
539
|
+
sourceFile,
|
|
540
|
+
{
|
|
541
|
+
alias: {
|
|
542
|
+
type: "append",
|
|
543
|
+
dir: src2 ? "src" : ""
|
|
544
|
+
},
|
|
545
|
+
relativeTo: path5.dirname(from)
|
|
546
|
+
},
|
|
547
|
+
(resolved) => {
|
|
548
|
+
if (resolved.type !== "file") return;
|
|
549
|
+
if (isRelative(originalDir, from) && filter(resolved.path)) return;
|
|
550
|
+
return toReferencePath(to, resolved.path);
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
await sourceFile.save();
|
|
554
|
+
}
|
|
555
|
+
await fs5.mkdir(path5.dirname(to), { recursive: true });
|
|
556
|
+
await fs5.rename(from, to);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/utils/i18n/transform-source-i18n.ts
|
|
560
|
+
import * as fs6 from "node:fs/promises";
|
|
561
|
+
import path6 from "node:path";
|
|
562
|
+
import { StructureKind, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
563
|
+
async function transformSourceI18n(project, filePath, config) {
|
|
564
|
+
let content;
|
|
565
|
+
try {
|
|
566
|
+
content = await fs6.readFile(filePath).then((res) => res.toString());
|
|
567
|
+
} catch {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const sourceFile = project.createSourceFile(filePath, content, {
|
|
571
|
+
overwrite: true
|
|
572
|
+
});
|
|
573
|
+
sourceFile.addImportDeclaration({
|
|
574
|
+
kind: StructureKind.ImportDeclaration,
|
|
575
|
+
moduleSpecifier: toReferencePath(
|
|
576
|
+
filePath,
|
|
577
|
+
path6.join(config.aliases?.libDir ?? defaultConfig.aliases.libDir, "i18n")
|
|
578
|
+
),
|
|
579
|
+
namedImports: ["i18n"]
|
|
580
|
+
});
|
|
581
|
+
const sourceExport = sourceFile.getDescendantsOfKind(SyntaxKind2.VariableDeclaration).find((node) => node.getName() === "source");
|
|
582
|
+
if (!sourceExport) return;
|
|
583
|
+
const loaderCall = sourceExport.getFirstDescendantByKind(
|
|
584
|
+
SyntaxKind2.ObjectLiteralExpression
|
|
585
|
+
);
|
|
586
|
+
if (!loaderCall || loaderCall.getProperty("i18n")) return;
|
|
587
|
+
loaderCall.addPropertyAssignment({
|
|
588
|
+
name: "i18n",
|
|
589
|
+
initializer: "i18n"
|
|
590
|
+
});
|
|
591
|
+
return sourceFile.save();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/utils/i18n/transform-root-layout.ts
|
|
595
|
+
import fs7 from "node:fs/promises";
|
|
596
|
+
import { StructureKind as StructureKind2, ts } from "ts-morph";
|
|
597
|
+
var ScriptKind = ts.ScriptKind;
|
|
598
|
+
var SyntaxKind3 = ts.SyntaxKind;
|
|
599
|
+
async function transformRootLayout(project, filePath) {
|
|
600
|
+
let content;
|
|
601
|
+
try {
|
|
602
|
+
content = await fs7.readFile(filePath).then((res) => res.toString());
|
|
603
|
+
} catch {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const sourceFile = project.createSourceFile(filePath, content, {
|
|
607
|
+
overwrite: true,
|
|
608
|
+
scriptKind: ScriptKind.TSX
|
|
609
|
+
});
|
|
610
|
+
runTransform(sourceFile);
|
|
611
|
+
return sourceFile.save();
|
|
612
|
+
}
|
|
613
|
+
function runTransform(sourceFile) {
|
|
614
|
+
const rootProvider = sourceFile.getDescendantsOfKind(SyntaxKind3.JsxElement).find(
|
|
615
|
+
(node) => node.getOpeningElement().getTagNameNode().getFullText() === "RootProvider"
|
|
616
|
+
);
|
|
617
|
+
if (!rootProvider) return;
|
|
618
|
+
const parent = rootProvider.getParentIfKind(SyntaxKind3.JsxElement);
|
|
619
|
+
if (parent) {
|
|
620
|
+
const inner = parent.getJsxChildren().map((v) => v.getFullText()).filter((v) => v.length > 0).join("\n");
|
|
621
|
+
parent.setBodyText(
|
|
622
|
+
`<I18nProvider locale={params.lang} locales={[
|
|
623
|
+
{ locale: 'en', name: 'English' }
|
|
624
|
+
]}>
|
|
625
|
+
${inner.trim()}
|
|
626
|
+
</I18nProvider>`
|
|
627
|
+
);
|
|
628
|
+
sourceFile.addImportDeclaration({
|
|
629
|
+
kind: StructureKind2.ImportDeclaration,
|
|
630
|
+
moduleSpecifier: "fumadocs-ui/i18n",
|
|
631
|
+
namedImports: ["I18nProvider"]
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
const func = sourceFile.getDescendantsOfKind(SyntaxKind3.FunctionDeclaration).find((v) => v.isDefaultExport());
|
|
635
|
+
const param = func?.getParameters().at(0);
|
|
636
|
+
param?.setType(`{ params: { lang: string }, children: ReactNode }`);
|
|
637
|
+
param?.set({
|
|
638
|
+
name: `{ params, children }`
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/plugins/i18n.ts
|
|
643
|
+
var i18nPlugin = {
|
|
644
|
+
files: () => ({
|
|
645
|
+
"lib/i18n.ts": generated["lib/i18n"],
|
|
646
|
+
"middleware.ts": generated.middleware
|
|
647
|
+
}),
|
|
648
|
+
dependencies: [],
|
|
649
|
+
instructions: () => [
|
|
650
|
+
{
|
|
651
|
+
type: "text",
|
|
652
|
+
text: "Make sure to update the params of page.tsx and route.ts (if necessary):"
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
type: "code",
|
|
656
|
+
title: "page.tsx",
|
|
657
|
+
code: `
|
|
658
|
+
export default function Page({
|
|
659
|
+
params,
|
|
660
|
+
}: {
|
|
661
|
+
${picocolors4.underline(picocolors4.bold("params: { lang: string; slug?: string[] };"))}
|
|
662
|
+
})
|
|
663
|
+
`.trim()
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
type: "text",
|
|
667
|
+
text: "Update the usages to `source` with:"
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
type: "code",
|
|
671
|
+
title: "page.tsx",
|
|
672
|
+
code: `const page = source.getPage(params.slug, params.lang);
|
|
673
|
+
const pages = source.getPage(params.lang);`
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
type: "code",
|
|
677
|
+
title: "layout.tsx",
|
|
678
|
+
code: `const tree = source.pageTree[params.lang];`
|
|
679
|
+
}
|
|
680
|
+
],
|
|
681
|
+
async transform(ctx) {
|
|
682
|
+
const project = createEmptyProject();
|
|
683
|
+
await Promise.all([
|
|
684
|
+
transformLayoutConfig(
|
|
685
|
+
project,
|
|
686
|
+
resolveAppPath("./app/layout.config.tsx", ctx.src)
|
|
687
|
+
),
|
|
688
|
+
transformRootLayout(project, resolveAppPath("./app/layout.tsx", ctx.src)),
|
|
689
|
+
transformSourceI18n(
|
|
690
|
+
project,
|
|
691
|
+
path7.join(
|
|
692
|
+
ctx.aliases?.libDir ?? defaultConfig.aliases.libDir,
|
|
693
|
+
"source.ts"
|
|
694
|
+
),
|
|
695
|
+
ctx
|
|
696
|
+
)
|
|
697
|
+
]);
|
|
698
|
+
await moveFiles(
|
|
699
|
+
resolveAppPath("./app", ctx.src),
|
|
700
|
+
resolveAppPath("./app/[lang]", ctx.src),
|
|
701
|
+
(v) => {
|
|
702
|
+
return path7.basename(v, path7.extname(v)) !== "layout.config" && !isRelative("./app/api", v);
|
|
703
|
+
},
|
|
704
|
+
project,
|
|
705
|
+
ctx.src
|
|
706
|
+
);
|
|
707
|
+
log3.success(
|
|
708
|
+
"Moved the ./app files to a [lang] route group, and modified your root layout to add `<I18nProvider />`."
|
|
709
|
+
);
|
|
710
|
+
},
|
|
711
|
+
transformRejected() {
|
|
712
|
+
log3.info(
|
|
713
|
+
`Please create a [lang] route group and move all special files into the folder.
|
|
714
|
+
See https://nextjs.org/docs/app/building-your-application/routing/internationalization for more info.`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/plugins/openapi.ts
|
|
720
|
+
import fs9 from "node:fs/promises";
|
|
721
|
+
import path8 from "node:path";
|
|
722
|
+
import { StructureKind as StructureKind3 } from "ts-morph";
|
|
723
|
+
|
|
724
|
+
// src/utils/transform-tailwind.ts
|
|
725
|
+
import fs8 from "node:fs/promises";
|
|
726
|
+
import { SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
727
|
+
import { log as log4 } from "@clack/prompts";
|
|
728
|
+
var tailwindConfigPaths = [
|
|
729
|
+
"tailwind.config.js",
|
|
730
|
+
"tailwind.config.mjs",
|
|
731
|
+
"tailwind.config.ts",
|
|
732
|
+
"tailwind.config.mts"
|
|
733
|
+
];
|
|
734
|
+
async function findTailwindConfig() {
|
|
735
|
+
for (const configPath of tailwindConfigPaths) {
|
|
736
|
+
if (await exists(configPath)) {
|
|
737
|
+
return configPath;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function transformTailwind(project, options) {
|
|
742
|
+
const file = await findTailwindConfig();
|
|
743
|
+
if (!file) {
|
|
744
|
+
log4.error(
|
|
745
|
+
"Cannot find Tailwind CSS configuration file, Tailwind CSS is required for this."
|
|
746
|
+
);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const configFile = project.createSourceFile(
|
|
750
|
+
file,
|
|
751
|
+
await fs8.readFile(file).then((res) => res.toString()),
|
|
752
|
+
{ overwrite: true }
|
|
753
|
+
);
|
|
754
|
+
const exports = configFile.getExportAssignments();
|
|
755
|
+
if (exports.length === 0) return;
|
|
756
|
+
const contentNode = exports[0].getDescendantsOfKind(SyntaxKind4.PropertyAssignment).find((a) => a.getName() === "content");
|
|
757
|
+
if (!contentNode) throw new Error("No `content` detected");
|
|
758
|
+
const arr = contentNode.getFirstDescendantByKind(
|
|
759
|
+
SyntaxKind4.ArrayLiteralExpression
|
|
760
|
+
);
|
|
761
|
+
arr?.addElements(options.addContents.map((v) => JSON.stringify(v)));
|
|
762
|
+
await configFile.save();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/plugins/openapi.ts
|
|
766
|
+
var openapiPlugin = {
|
|
767
|
+
files: () => ({
|
|
768
|
+
"scripts/generate-docs.mjs": generated["scripts/generate-docs"]
|
|
769
|
+
}),
|
|
770
|
+
dependencies: ["fumadocs-openapi", "rimraf", "shiki"],
|
|
771
|
+
async transform(ctx) {
|
|
772
|
+
const project = createEmptyProject();
|
|
773
|
+
await Promise.all([
|
|
774
|
+
transformSource(project, ctx),
|
|
775
|
+
transformTailwind(project, {
|
|
776
|
+
addContents: [`./node_modules/fumadocs-openapi/dist/**/*.js`]
|
|
777
|
+
}),
|
|
778
|
+
addScript()
|
|
779
|
+
]);
|
|
780
|
+
},
|
|
781
|
+
instructions: async () => [
|
|
782
|
+
{
|
|
783
|
+
type: "text",
|
|
784
|
+
text: `I've made some changes to your Tailwind CSS config.
|
|
785
|
+
You can add the APIPage component to your page.tsx:`
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
type: "code",
|
|
789
|
+
title: "page.tsx",
|
|
790
|
+
code: `import defaultMdxComponents from 'fumadocs-ui/mdx';
|
|
791
|
+
import { openapi } from '@/lib/source';
|
|
792
|
+
|
|
793
|
+
<MDX
|
|
794
|
+
components={{
|
|
795
|
+
...defaultMdxComponents,
|
|
796
|
+
APIPage: openapi.APIPage,
|
|
797
|
+
}}
|
|
798
|
+
/>;`
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
type: "text",
|
|
802
|
+
text: `Paste your OpenAPI schema to ./openapi.json, and use this script to generate docs:`
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
type: "code",
|
|
806
|
+
title: "Terminal",
|
|
807
|
+
code: `${await getPackageManager()} run build:docs`
|
|
808
|
+
}
|
|
809
|
+
]
|
|
810
|
+
};
|
|
811
|
+
async function addScript() {
|
|
812
|
+
const content = await fs9.readFile("package.json");
|
|
813
|
+
const parsed = JSON.parse(content.toString());
|
|
814
|
+
if (typeof parsed.scripts !== "object") return;
|
|
815
|
+
parsed.scripts ??= {};
|
|
816
|
+
Object.assign(parsed.scripts ?? {}, {
|
|
817
|
+
"build:docs": "node ./scripts/generate-docs.mjs"
|
|
818
|
+
});
|
|
819
|
+
await fs9.writeFile("package.json", JSON.stringify(parsed, null, 2));
|
|
820
|
+
}
|
|
821
|
+
async function transformSource(project, config) {
|
|
822
|
+
const source = path8.join(
|
|
823
|
+
config.aliases?.libDir ?? defaultConfig.aliases.libDir,
|
|
824
|
+
"source.ts"
|
|
825
|
+
);
|
|
826
|
+
const content = await fs9.readFile(source).catch(() => "");
|
|
827
|
+
const file = project.createSourceFile(source, content.toString(), {
|
|
828
|
+
overwrite: true
|
|
829
|
+
});
|
|
830
|
+
file.addImportDeclaration({
|
|
831
|
+
kind: StructureKind3.ImportDeclaration,
|
|
832
|
+
namedImports: ["createOpenAPI"],
|
|
833
|
+
moduleSpecifier: "fumadocs-openapi/server"
|
|
834
|
+
});
|
|
835
|
+
file.addStatements(`export const openapi = createOpenAPI();`);
|
|
836
|
+
await file.save();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/plugins/index.ts
|
|
840
|
+
var plugins = {
|
|
841
|
+
"og-image": ogImagePlugin,
|
|
842
|
+
i18n: i18nPlugin,
|
|
843
|
+
openapi: openapiPlugin
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
// src/commands/file-tree.ts
|
|
847
|
+
var scanned = ["file", "directory", "link"];
|
|
848
|
+
function treeToMdx(input, noRoot = false) {
|
|
849
|
+
function toNode(item) {
|
|
850
|
+
if (item.type === "file" || item.type === "link") {
|
|
851
|
+
return `<File name=${JSON.stringify(item.name)} />`;
|
|
852
|
+
}
|
|
853
|
+
if (item.type === "directory") {
|
|
854
|
+
if (item.contents.length === 1 && "name" in item.contents[0]) {
|
|
855
|
+
const child = item.contents[0];
|
|
856
|
+
return toNode({
|
|
857
|
+
...child,
|
|
858
|
+
name: `${item.name}/${child.name}`
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return `<Folder name=${JSON.stringify(item.name)}>
|
|
862
|
+
${item.contents.map(toNode).filter(Boolean).join("\n")}
|
|
863
|
+
</Folder>`;
|
|
864
|
+
}
|
|
865
|
+
return "";
|
|
866
|
+
}
|
|
867
|
+
let children = input.filter((v) => scanned.includes(v.type));
|
|
868
|
+
if (noRoot && children.length === 1 && input[0].type === "directory") {
|
|
869
|
+
children = input[0].contents;
|
|
870
|
+
}
|
|
871
|
+
return `<Files>
|
|
872
|
+
${children.map(toNode).filter(Boolean).join("\n")}
|
|
873
|
+
</Files>`;
|
|
874
|
+
}
|
|
875
|
+
function treeToJavaScript(input, noRoot, importName = "fumadocs-ui/components/files") {
|
|
876
|
+
return `import { File, Files, Folder } from ${JSON.stringify(importName)}
|
|
877
|
+
|
|
878
|
+
export default (${treeToMdx(input, noRoot)})`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/utils/file-tree/run-tree.ts
|
|
882
|
+
import { execa as execa3 } from "execa";
|
|
883
|
+
async function runTree(args) {
|
|
884
|
+
const out = await execa3("tree", [args, "--gitignore", "--prune", "-J"]);
|
|
885
|
+
try {
|
|
886
|
+
return JSON.parse(out.stdout);
|
|
887
|
+
} catch (e) {
|
|
888
|
+
throw new Error("failed to run `tree` command", {
|
|
889
|
+
cause: e
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// package.json
|
|
895
|
+
var package_default = {
|
|
896
|
+
name: "@fumadocs/cli",
|
|
897
|
+
version: "0.0.1",
|
|
898
|
+
description: "The CLI tool for Fumadocs",
|
|
899
|
+
keywords: [
|
|
900
|
+
"NextJs",
|
|
901
|
+
"Docs",
|
|
902
|
+
"Fumadocs"
|
|
903
|
+
],
|
|
904
|
+
homepage: "https://fumadocs.vercel.app",
|
|
905
|
+
repository: "github:fuma-nama/fumadocs",
|
|
906
|
+
license: "MIT",
|
|
907
|
+
author: "Fuma Nama",
|
|
908
|
+
type: "module",
|
|
909
|
+
exports: {
|
|
910
|
+
"./build": {
|
|
911
|
+
import: "./dist/build/index.js",
|
|
912
|
+
types: "./dist/build/index.d.ts"
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
main: "./dist/index.js",
|
|
916
|
+
bin: {
|
|
917
|
+
fumadocs: "./dist/index.js"
|
|
918
|
+
},
|
|
919
|
+
files: [
|
|
920
|
+
"dist/*"
|
|
921
|
+
],
|
|
922
|
+
scripts: {
|
|
923
|
+
build: "tsup",
|
|
924
|
+
clean: "rimraf dist",
|
|
925
|
+
dev: "tsup --watch",
|
|
926
|
+
lint: "eslint .",
|
|
927
|
+
sync: "tsx ./scripts/sync.ts",
|
|
928
|
+
"types:check": "tsc --noEmit"
|
|
929
|
+
},
|
|
930
|
+
dependencies: {
|
|
931
|
+
"@clack/prompts": "^0.7.0",
|
|
932
|
+
commander: "^12.1.0",
|
|
933
|
+
execa: "^9.4.1",
|
|
934
|
+
"package-manager-detector": "^0.2.2",
|
|
935
|
+
picocolors: "^1.1.1",
|
|
936
|
+
"ts-morph": "^24.0.0"
|
|
937
|
+
},
|
|
938
|
+
devDependencies: {
|
|
939
|
+
"@types/cross-spawn": "^6.0.6",
|
|
940
|
+
"@types/node": "22.7.8",
|
|
941
|
+
"@types/react": "^18.3.11",
|
|
942
|
+
"eslint-config-custom": "workspace:*",
|
|
943
|
+
"fast-glob": "^3.3.1",
|
|
944
|
+
tsconfig: "workspace:*",
|
|
945
|
+
tsx: "^4.19.1"
|
|
946
|
+
},
|
|
947
|
+
publishConfig: {
|
|
948
|
+
access: "public"
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/index.ts
|
|
953
|
+
var program = new Command();
|
|
954
|
+
program.name("fumadocs").description("CLI to setup Fumadocs").version(package_default.version);
|
|
955
|
+
program.command("config").description("init a config for Fumadocs CLI").action(async () => {
|
|
956
|
+
await initConfig();
|
|
957
|
+
console.log(picocolors5.green("Successful: ./cli.json"));
|
|
958
|
+
});
|
|
959
|
+
program.command("init").description("init a new plugin to your docs").argument("[string]", "plugin name").option("--config <string>").action(async (str, { config }) => {
|
|
960
|
+
const loadedConfig = await loadConfig(config);
|
|
961
|
+
if (str) {
|
|
962
|
+
const plugin = str in plugins ? plugins[str] : void 0;
|
|
963
|
+
if (!plugin) throw new Error(`Plugin not found: ${str}`);
|
|
964
|
+
await init(plugin, loadedConfig);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const value = await select({
|
|
968
|
+
message: "Select components to install",
|
|
969
|
+
options: Object.keys(plugins).map((c) => ({
|
|
970
|
+
label: c,
|
|
971
|
+
value: c
|
|
972
|
+
}))
|
|
973
|
+
});
|
|
974
|
+
if (isCancel3(value)) {
|
|
975
|
+
outro2("Ended");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
await init(plugins[value], loadedConfig);
|
|
979
|
+
});
|
|
980
|
+
var dirShortcuts = {
|
|
981
|
+
":dev": "https://fumadocs-dev.vercel.app/registry"
|
|
982
|
+
};
|
|
983
|
+
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").option("--config <string>").action(
|
|
984
|
+
async (input, options) => {
|
|
985
|
+
let dir = options.dir ?? "https://fumadocs.vercel.app/registry";
|
|
986
|
+
if (dir in dirShortcuts) dir = dirShortcuts[dir];
|
|
987
|
+
const resolver = dir.startsWith("http://") || dir.startsWith("https://") ? remoteResolver(dir) : localResolver(dir);
|
|
988
|
+
let target = input;
|
|
989
|
+
if (input.length === 0) {
|
|
990
|
+
const spin = spinner3();
|
|
991
|
+
spin.start("fetching registry");
|
|
992
|
+
const registry = await resolver("_registry.json");
|
|
993
|
+
spin.stop(picocolors5.bold(picocolors5.greenBright("registry fetched")));
|
|
994
|
+
if (!registry) {
|
|
995
|
+
log5.error(`Failed to fetch '_registry.json' file from ${dir}`);
|
|
996
|
+
throw new Error(`Failed to fetch registry`);
|
|
997
|
+
}
|
|
998
|
+
const value = await multiselect({
|
|
999
|
+
message: "Select components to install",
|
|
1000
|
+
options: registry.map((item) => ({
|
|
1001
|
+
label: item.name,
|
|
1002
|
+
value: item.name,
|
|
1003
|
+
hint: item.description
|
|
1004
|
+
}))
|
|
1005
|
+
});
|
|
1006
|
+
if (isCancel3(value)) {
|
|
1007
|
+
outro2("Ended");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
target = value;
|
|
1011
|
+
}
|
|
1012
|
+
const loadedConfig = await loadConfig(options.config);
|
|
1013
|
+
for (const name of target) {
|
|
1014
|
+
await add(name, resolver, loadedConfig);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
);
|
|
1018
|
+
program.command("tree").argument(
|
|
1019
|
+
"[json_or_args]",
|
|
1020
|
+
"JSON output of `tree` command or arguments for the `tree` command"
|
|
1021
|
+
).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(
|
|
1022
|
+
async (str, output, {
|
|
1023
|
+
js,
|
|
1024
|
+
root,
|
|
1025
|
+
importName
|
|
1026
|
+
}) => {
|
|
1027
|
+
const jsExtensions = [".js", ".tsx", ".jsx"];
|
|
1028
|
+
const noRoot = !root;
|
|
1029
|
+
let nodes;
|
|
1030
|
+
try {
|
|
1031
|
+
nodes = JSON.parse(str ?? "");
|
|
1032
|
+
} catch {
|
|
1033
|
+
nodes = await runTree(str ?? "./");
|
|
1034
|
+
}
|
|
1035
|
+
const out = js || output && jsExtensions.includes(path9.extname(output)) ? treeToJavaScript(nodes, noRoot, importName) : treeToMdx(nodes, noRoot);
|
|
1036
|
+
if (output) {
|
|
1037
|
+
await fs10.mkdir(path9.dirname(output), { recursive: true });
|
|
1038
|
+
await fs10.writeFile(output, out);
|
|
1039
|
+
} else {
|
|
1040
|
+
console.log(out);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
program.parse();
|