@fumadocs/cli 0.0.8 → 0.1.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.
@@ -1,7 +1,12 @@
1
+ import * as ts_morph from 'ts-morph';
2
+
1
3
  interface Component {
2
4
  name: string;
3
5
  description?: string;
4
- files: string[];
6
+ files: (string | {
7
+ in: string;
8
+ out: string;
9
+ })[];
5
10
  /**
6
11
  * Don't list the component in registry index file
7
12
  */
@@ -11,9 +16,12 @@ interface Component {
11
16
  */
12
17
  mapImportPath?: Record<string, string | {
13
18
  type: 'component';
14
- registry: string;
15
19
  name: string;
16
20
  file: string;
21
+ /**
22
+ * Registry of the component, refer to the current registry if not specified
23
+ */
24
+ registry?: string;
17
25
  }>;
18
26
  }
19
27
  type NamespaceType = 'components' | 'hooks' | 'lib';
@@ -23,9 +31,9 @@ interface PackageJson {
23
31
  }
24
32
  interface Registry {
25
33
  /**
26
- * The path of registry, needed to resolve relative paths
34
+ * The directory of registry, needed to resolve relative paths
27
35
  */
28
- path: string;
36
+ dir: string;
29
37
  /**
30
38
  * Extend on existing registry
31
39
  */
@@ -78,23 +86,31 @@ interface DependencyInfo {
78
86
  type: 'runtime' | 'dev';
79
87
  version?: string;
80
88
  }
81
- interface ComponentBuilder {
82
- registryDir: string;
83
- resolveDep: (specifier: string) => DependencyInfo & {
84
- name: string;
85
- };
86
- resolveOutputPath: (path: string, namespace?: string) => string;
87
- getSubComponent: (path: string) => {
88
- component: Component;
89
- } | undefined;
90
- }
89
+ type GetComponentResult = {
90
+ type: 'local';
91
+ registryName?: string;
92
+ component: Component;
93
+ } | {
94
+ type: 'remote';
95
+ registryName: string;
96
+ component: OutputComponent;
97
+ };
98
+ type ComponentBuilder = ReturnType<typeof createComponentBuilder>;
91
99
  /**
92
100
  * @param registry registry object
93
101
  * @param packageJson parsed package json object
94
- * @param registryDir directory of registry config file
95
- * @param sourceDir source directory of project (e.g. `/src`), used to resolve the output paths of component files
96
102
  */
97
- declare function createComponentBuilder(registry: Registry, packageJson: PackageJson | undefined, registryDir: string, sourceDir: string): ComponentBuilder;
103
+ declare function createComponentBuilder(registry: Registry, packageJson: PackageJson | undefined): {
104
+ registryDir: string;
105
+ registry: Registry;
106
+ resolveDep(specifier: string): DependencyInfo & {
107
+ name: string;
108
+ };
109
+ createSourceFile(file: string): Promise<ts_morph.SourceFile>;
110
+ getComponentByName(name: string, registryName?: string): GetComponentResult | undefined;
111
+ resolveOutputPath(file: string, registryName?: string, namespace?: string): string;
112
+ getSubComponent(file: string): Component | undefined;
113
+ };
98
114
 
99
115
  declare function writeOutput(dir: string, out: Output, options?: {
100
116
  /**
@@ -3,29 +3,21 @@ import {
3
3
  } from "../chunk-WBCEM7RC.js";
4
4
 
5
5
  // src/build/index.ts
6
- import * as fs2 from "node:fs/promises";
6
+ import * as fs3 from "node:fs/promises";
7
7
  import * as path4 from "node:path";
8
8
  import picocolors from "picocolors";
9
9
 
10
10
  // src/build/build-registry.ts
11
- import * as fs from "node:fs/promises";
11
+ import * as fs2 from "node:fs/promises";
12
12
  import * as path3 from "node:path";
13
- import { Project } from "ts-morph";
14
13
 
15
14
  // src/build/build-file.ts
16
15
  import * as path from "node:path";
17
- async function buildFile(filePath, sourceFile, builder, comp, processedFiles) {
18
- processedFiles.add(filePath);
16
+ async function buildFile(inputPath, outputPath, builder, comp, onReference) {
19
17
  const out = {
20
18
  imports: {},
21
19
  content: "",
22
- path: filePath
23
- };
24
- const processed = {
25
- files: [out],
26
- dependencies: /* @__PURE__ */ new Map(),
27
- devDependencies: /* @__PURE__ */ new Map(),
28
- subComponents: /* @__PURE__ */ new Set()
20
+ path: outputPath
29
21
  };
30
22
  async function process2(specifier, getSpecifiedFile) {
31
23
  let specifiedFile = getSpecifiedFile();
@@ -38,43 +30,55 @@ async function buildFile(filePath, sourceFile, builder, comp, processedFiles) {
38
30
  specifiedFile = getSpecifiedFile();
39
31
  if (!specifiedFile) return;
40
32
  } else {
41
- processed.subComponents.add(resolver.name);
42
- out.imports[specifier.getLiteralValue()] = resolver.file;
33
+ const sub2 = builder.getComponentByName(
34
+ resolver.name,
35
+ resolver.registry
36
+ );
37
+ if (!sub2)
38
+ throw new Error(`Failed to resolve sub component ${resolver.name}`);
39
+ const value2 = onReference({
40
+ type: "sub-component",
41
+ resolved: sub2,
42
+ targetFile: resolver.file
43
+ });
44
+ if (value2) out.imports[specifier.getLiteralValue()] = value2;
43
45
  return;
44
46
  }
45
47
  }
46
48
  if (specifiedFile.isInNodeModules() || specifiedFile.isDeclarationFile()) {
47
49
  const info = builder.resolveDep(specifier.getLiteralValue());
48
- if (info.type === "dev") {
49
- processed.devDependencies.set(info.name, info.version ?? "");
50
- } else {
51
- processed.dependencies.set(info.name, info.version ?? "");
52
- }
50
+ const value2 = onReference({
51
+ type: "dependency",
52
+ name: info.name,
53
+ version: info.version ?? "",
54
+ isDev: info.type === "dev"
55
+ });
56
+ if (value2) out.imports[specifier.getLiteralValue()] = value2;
53
57
  return;
54
58
  }
55
59
  const sub = builder.getSubComponent(specifiedFile.getFilePath());
56
60
  if (sub) {
57
- processed.subComponents.add(sub.component.name);
58
- out.imports[specifier.getLiteralValue()] = builder.resolveOutputPath(
59
- specifiedFile.getFilePath()
60
- );
61
+ const value2 = onReference({
62
+ type: "sub-component",
63
+ resolved: {
64
+ type: "local",
65
+ component: sub
66
+ },
67
+ targetFile: path.relative(
68
+ builder.registryDir,
69
+ specifiedFile.getFilePath()
70
+ )
71
+ });
72
+ if (value2) out.imports[specifier.getLiteralValue()] = value2;
61
73
  return;
62
74
  }
63
- const referenceOutputPath = builder.resolveOutputPath(
64
- specifiedFile.getFilePath()
65
- );
66
- if (!processedFiles.has(referenceOutputPath)) {
67
- const outFile = await buildFile(
68
- referenceOutputPath,
69
- specifiedFile,
70
- builder,
71
- comp,
72
- processedFiles
73
- );
74
- merge(processed, outFile);
75
- }
76
- out.imports[specifier.getLiteralValue()] = referenceOutputPath;
75
+ const value = onReference({
76
+ type: "file",
77
+ file: specifiedFile.getFilePath()
78
+ });
79
+ if (value) out.imports[specifier.getLiteralValue()] = value;
77
80
  }
81
+ const sourceFile = await builder.createSourceFile(inputPath);
78
82
  for (const item of sourceFile.getImportDeclarations()) {
79
83
  await process2(
80
84
  item.getModuleSpecifier(),
@@ -87,34 +91,32 @@ async function buildFile(filePath, sourceFile, builder, comp, processedFiles) {
87
91
  await process2(specifier, () => item.getModuleSpecifierSourceFile());
88
92
  }
89
93
  out.content = sourceFile.getFullText();
90
- return processed;
91
- }
92
- function merge(to, from) {
93
- to.files.push(...from.files);
94
- for (const [k, v] of from.dependencies.entries()) {
95
- to.dependencies.set(k, v);
96
- }
97
- for (const [k, v] of from.devDependencies.entries()) {
98
- to.devDependencies.set(k, v);
99
- }
100
- from.subComponents.forEach((item) => to.subComponents.add(item));
94
+ return out;
101
95
  }
102
96
 
103
97
  // src/build/component-builder.ts
104
98
  import path2 from "node:path";
105
- function createComponentBuilder(registry, packageJson, registryDir, sourceDir) {
99
+ import { Project } from "ts-morph";
100
+ import * as fs from "fs/promises";
101
+ function createComponentBuilder(registry, packageJson) {
102
+ const rootDir = path2.join(registry.dir, registry.rootDir);
103
+ const project = new Project({
104
+ tsConfigFilePath: registry.tsconfigPath ? path2.join(rootDir, registry.tsconfigPath) : path2.join(rootDir, "tsconfig.json")
105
+ });
106
106
  const fileToComponent = /* @__PURE__ */ new Map();
107
107
  for (const comp of registry.components) {
108
108
  for (const file of comp.files) {
109
- if (fileToComponent.has(file))
109
+ const filePath = typeof file === "string" ? file : file.in;
110
+ if (fileToComponent.has(filePath))
110
111
  console.warn(
111
112
  `the same file ${file} exists in multiple component, you should make the shared file a separate component.`
112
113
  );
113
- fileToComponent.set(file, comp);
114
+ fileToComponent.set(filePath, comp);
114
115
  }
115
116
  }
116
117
  return {
117
- registryDir,
118
+ registryDir: registry.dir,
119
+ registry,
118
120
  resolveDep(specifier) {
119
121
  const name = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
120
122
  if (registry.dependencies && name in registry.dependencies)
@@ -138,64 +140,89 @@ function createComponentBuilder(registry, packageJson, registryDir, sourceDir) {
138
140
  }
139
141
  return { type: "runtime", name };
140
142
  },
141
- resolveOutputPath(file, forcedNamespace) {
142
- const relativeFile = path2.relative(registryDir, file);
143
- if (forcedNamespace) {
144
- return `${forcedNamespace}:${path2.relative(sourceDir, file)}`;
143
+ async createSourceFile(file) {
144
+ const content = await fs.readFile(file);
145
+ return project.createSourceFile(file, content.toString(), {
146
+ overwrite: true
147
+ });
148
+ },
149
+ getComponentByName(name, registryName) {
150
+ if (registryName) {
151
+ const child = registry.on[registryName];
152
+ const comp2 = child.registry.components.find(
153
+ (comp3) => comp3.name === name
154
+ );
155
+ if (comp2) {
156
+ return {
157
+ type: child.type,
158
+ registryName,
159
+ component: comp2
160
+ };
161
+ }
162
+ return;
163
+ }
164
+ const comp = registry.components.find((comp2) => comp2.name === name);
165
+ if (comp) {
166
+ return {
167
+ type: "local",
168
+ registryName,
169
+ component: comp
170
+ };
171
+ }
172
+ },
173
+ resolveOutputPath(file, registryName, namespace) {
174
+ let targetRegistry = registry;
175
+ if (registryName && registry.on[registryName].type === "local") {
176
+ targetRegistry = registry.on[registryName].registry;
177
+ }
178
+ const parsed = file.split(":", 2);
179
+ if (parsed.length > 1) {
180
+ namespace ??= parsed[0];
181
+ file = parsed[1];
145
182
  }
146
- if (registry.namespaces)
147
- for (const namespace of Object.keys(registry.namespaces)) {
148
- const relativePath = path2.relative(namespace, relativeFile);
149
- if (!relativePath.startsWith("../") && !path2.isAbsolute(relativePath)) {
150
- return `${registry.namespaces[namespace]}:${relativePath}`;
183
+ if (!path2.isAbsolute(file)) {
184
+ file = path2.join(targetRegistry.dir, file);
185
+ }
186
+ const rootDir2 = path2.join(targetRegistry.dir, targetRegistry.rootDir);
187
+ if (namespace) {
188
+ return `${namespace}:${path2.relative(rootDir2, file)}`;
189
+ }
190
+ if (targetRegistry.namespaces)
191
+ for (const namespace2 in targetRegistry.namespaces) {
192
+ const relativePath = path2.relative(
193
+ path2.join(targetRegistry.dir, namespace2),
194
+ file
195
+ );
196
+ if (!relativePath.startsWith("../")) {
197
+ return `${targetRegistry.namespaces[namespace2]}:${relativePath}`;
151
198
  }
152
199
  }
153
- return path2.relative(sourceDir, file);
200
+ return path2.relative(rootDir2, file);
154
201
  },
155
202
  getSubComponent(file) {
156
- const relativeFile = path2.relative(registryDir, file);
203
+ const relativeFile = path2.relative(registry.dir, file);
157
204
  const comp = fileToComponent.get(relativeFile);
158
205
  if (!comp) return;
159
- return {
160
- component: comp
161
- };
206
+ return comp;
162
207
  }
163
208
  };
164
209
  }
165
210
 
166
- // src/build/get-path-namespace.ts
167
- function getFileNamespace(file) {
168
- const parsed = file.split(":", 2);
169
- if (parsed.length > 1) return { namespace: parsed[0], path: parsed[1] };
170
- return { path: file };
171
- }
172
-
173
211
  // src/build/build-registry.ts
174
212
  async function build(registry) {
175
- const registryDir = path3.dirname(registry.path);
176
- const rootDir = path3.join(registryDir, registry.rootDir);
177
- const useSrc = await exists(path3.join(rootDir, "src"));
178
213
  const output = {
179
214
  index: [],
180
215
  components: []
181
216
  };
182
- const project = new Project({
183
- tsConfigFilePath: registry.tsconfigPath ? path3.join(registryDir, registry.tsconfigPath) : path3.join(rootDir, "tsconfig.json")
184
- });
185
217
  function readPackageJson() {
186
218
  if (typeof registry.packageJson !== "string" && registry.packageJson)
187
219
  return registry.packageJson;
188
- return fs.readFile(
189
- registry.packageJson ? path3.join(registryDir, registry.packageJson) : path3.join(rootDir, "package.json")
220
+ return fs2.readFile(
221
+ registry.packageJson ? path3.join(registry.dir, registry.packageJson) : path3.join(registry.dir, registry.rootDir, "package.json")
190
222
  ).then((res) => JSON.parse(res.toString())).catch(() => void 0);
191
223
  }
192
224
  const packageJson = await readPackageJson();
193
- const builder = createComponentBuilder(
194
- registry,
195
- packageJson,
196
- registryDir,
197
- useSrc ? path3.join(rootDir, "src") : rootDir
198
- );
225
+ const builder = createComponentBuilder(registry, packageJson);
199
226
  const buildExtendRegistries = Object.values(registry.on ?? {}).map(
200
227
  async (schema) => {
201
228
  if (schema.type === "remote") {
@@ -208,55 +235,10 @@ async function build(registry) {
208
235
  output.components.push(...built.components);
209
236
  output.index.push(...built.index);
210
237
  }
211
- const buildComps = registry.components.map(async (component) => {
212
- const processedFiles = /* @__PURE__ */ new Set();
213
- const collect = {
214
- files: [],
215
- subComponents: /* @__PURE__ */ new Set(),
216
- devDependencies: /* @__PURE__ */ new Map(),
217
- dependencies: /* @__PURE__ */ new Map()
218
- };
219
- const read = component.files.map(async (sourcePath) => {
220
- const parsed = getFileNamespace(sourcePath);
221
- parsed.path = path3.join(registryDir, parsed.path);
222
- const content = await fs.readFile(parsed.path);
223
- const sourceFile = project.createSourceFile(
224
- parsed.path,
225
- content.toString(),
226
- {
227
- overwrite: true
228
- }
229
- );
230
- const outputPath = builder.resolveOutputPath(
231
- parsed.path,
232
- parsed.namespace
233
- );
234
- if (processedFiles.has(outputPath)) return;
235
- return buildFile(
236
- outputPath,
237
- sourceFile,
238
- builder,
239
- component,
240
- processedFiles
241
- );
242
- });
243
- const outFiles = await Promise.all(read);
244
- for (const file of outFiles) {
245
- if (!file) continue;
246
- merge(collect, file);
247
- }
248
- return [
249
- component,
250
- {
251
- name: component.name,
252
- files: collect.files,
253
- subComponents: Array.from(collect.subComponents),
254
- dependencies: Object.fromEntries(collect.dependencies),
255
- devDependencies: Object.fromEntries(collect.devDependencies)
256
- }
257
- ];
258
- });
259
- for (const [input, comp] of await Promise.all(buildComps)) {
238
+ const builtComps = await Promise.all(
239
+ registry.components.map((component) => buildComponent(component, builder))
240
+ );
241
+ for (const [input, comp] of builtComps) {
260
242
  if (!input.unlisted) {
261
243
  output.index.push({
262
244
  name: input.name,
@@ -267,12 +249,88 @@ async function build(registry) {
267
249
  }
268
250
  return output;
269
251
  }
252
+ async function buildComponent(component, builder) {
253
+ const processedFiles = /* @__PURE__ */ new Set();
254
+ const subComponents = /* @__PURE__ */ new Set();
255
+ const devDependencies = /* @__PURE__ */ new Map();
256
+ const dependencies = /* @__PURE__ */ new Map();
257
+ async function build2(file) {
258
+ let inputPath;
259
+ let outputPath;
260
+ if (typeof file === "string") {
261
+ let namespace;
262
+ const parsed = file.split(":", 2);
263
+ if (parsed.length > 1) {
264
+ namespace = parsed[0];
265
+ inputPath = path3.join(builder.registryDir, parsed[1]);
266
+ } else {
267
+ inputPath = path3.join(builder.registryDir, file);
268
+ }
269
+ outputPath = builder.resolveOutputPath(file, void 0, namespace);
270
+ } else {
271
+ inputPath = path3.join(builder.registryDir, file.in);
272
+ outputPath = file.out;
273
+ }
274
+ if (processedFiles.has(inputPath)) return [];
275
+ processedFiles.add(inputPath);
276
+ const queue = [];
277
+ const result = await buildFile(
278
+ inputPath,
279
+ outputPath,
280
+ builder,
281
+ component,
282
+ (reference) => {
283
+ if (reference.type === "file") {
284
+ queue.push(path3.relative(builder.registryDir, reference.file));
285
+ return builder.resolveOutputPath(reference.file);
286
+ }
287
+ if (reference.type === "sub-component") {
288
+ const resolved = reference.resolved;
289
+ subComponents.add(resolved.component.name);
290
+ if (resolved.type === "remote") {
291
+ return reference.targetFile;
292
+ }
293
+ for (const childFile of resolved.component.files) {
294
+ if (typeof childFile === "string" && childFile === reference.targetFile) {
295
+ return builder.resolveOutputPath(
296
+ childFile,
297
+ reference.resolved.registryName
298
+ );
299
+ }
300
+ if (typeof childFile === "object" && childFile.in === reference.targetFile) {
301
+ return childFile.out;
302
+ }
303
+ }
304
+ throw new Error(
305
+ `Failed to find sub component ${resolved.component.name}'s ${reference.targetFile} referenced by ${inputPath}`
306
+ );
307
+ }
308
+ if (reference.type === "dependency") {
309
+ if (reference.isDev)
310
+ devDependencies.set(reference.name, reference.version);
311
+ else dependencies.set(reference.name, reference.version);
312
+ }
313
+ }
314
+ );
315
+ return [result, ...(await Promise.all(queue.map(build2))).flat()];
316
+ }
317
+ return [
318
+ component,
319
+ {
320
+ name: component.name,
321
+ files: (await Promise.all(component.files.map(build2))).flat(),
322
+ subComponents: Array.from(subComponents),
323
+ dependencies: Object.fromEntries(dependencies),
324
+ devDependencies: Object.fromEntries(devDependencies)
325
+ }
326
+ ];
327
+ }
270
328
 
271
329
  // src/build/index.ts
272
330
  async function writeOutput(dir, out, options = {}) {
273
331
  const { log = true } = options;
274
332
  if (options.cleanDir && await exists(dir)) {
275
- await fs2.rm(dir, {
333
+ await fs3.rm(dir, {
276
334
  recursive: true
277
335
  });
278
336
  if (log) {
@@ -281,8 +339,8 @@ async function writeOutput(dir, out, options = {}) {
281
339
  }
282
340
  async function writeFile2(file, content) {
283
341
  if (!log) return;
284
- await fs2.mkdir(path4.dirname(file), { recursive: true });
285
- await fs2.writeFile(file, content);
342
+ await fs3.mkdir(path4.dirname(file), { recursive: true });
343
+ await fs3.writeFile(file, content);
286
344
  const size = (Buffer.byteLength(content) / 1024).toFixed(2);
287
345
  console.log(
288
346
  `${picocolors.greenBright("+")} ${path4.relative(process.cwd(), file)} ${picocolors.dim(`${size.toString()} KB`)}`
package/dist/index.js CHANGED
@@ -8,13 +8,13 @@ import {
8
8
  import fs10 from "node:fs/promises";
9
9
  import path9 from "node:path";
10
10
  import { Command } from "commander";
11
- import picocolors5 from "picocolors";
11
+ import picocolors6 from "picocolors";
12
12
  import {
13
13
  isCancel as isCancel3,
14
- log as log5,
14
+ log as log6,
15
15
  multiselect,
16
16
  outro as outro2,
17
- select,
17
+ select as select2,
18
18
  spinner as spinner3
19
19
  } from "@clack/prompts";
20
20
 
@@ -424,7 +424,7 @@ function localResolver(dir) {
424
424
  import picocolors3 from "picocolors";
425
425
 
426
426
  // src/generated.js
427
- 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" };
427
+ var generated = { "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/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", "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" };
428
428
 
429
429
  // src/plugins/og-image.ts
430
430
  function isI18nEnabled(ctx) {
@@ -908,7 +908,7 @@ async function runTree(args) {
908
908
  // package.json
909
909
  var package_default = {
910
910
  name: "@fumadocs/cli",
911
- version: "0.0.8",
911
+ version: "0.1.1",
912
912
  description: "The CLI tool for Fumadocs",
913
913
  keywords: [
914
914
  "NextJs",
@@ -945,32 +945,142 @@ var package_default = {
945
945
  "@clack/prompts": "^0.10.0",
946
946
  commander: "^13.1.0",
947
947
  execa: "^9.5.2",
948
- "package-manager-detector": "^0.2.9",
948
+ "package-manager-detector": "^1.1.0",
949
949
  picocolors: "^1.1.1",
950
950
  "ts-morph": "^25.0.1"
951
951
  },
952
952
  devDependencies: {
953
953
  "@types/cross-spawn": "^6.0.6",
954
- "@types/node": "22.13.1",
955
- "@types/react": "^19.0.8",
954
+ "@types/node": "22.14.0",
955
+ "@types/react": "^19.1.0",
956
956
  "eslint-config-custom": "workspace:*",
957
957
  "fast-glob": "^3.3.3",
958
958
  tsconfig: "workspace:*",
959
- tsx: "^4.19.2"
959
+ tsx: "^4.19.3"
960
960
  },
961
961
  publishConfig: {
962
962
  access: "public"
963
963
  }
964
964
  };
965
965
 
966
+ // src/commands/customise.ts
967
+ import { cancel as cancel2, group, intro as intro3, select, confirm as confirm3, log as log5 } from "@clack/prompts";
968
+ import picocolors5 from "picocolors";
969
+ async function customise(resolver, config) {
970
+ intro3(picocolors5.bgBlack(picocolors5.whiteBright("Customise Fumadocs UI")));
971
+ const result = await group(
972
+ {
973
+ target: () => select({
974
+ message: "What do you want to customise?",
975
+ options: [
976
+ {
977
+ label: "Docs Layout",
978
+ value: "docs",
979
+ hint: "main UI of your docs"
980
+ },
981
+ {
982
+ label: "Home Layout",
983
+ value: "home",
984
+ hint: "the navbar for your other pages"
985
+ }
986
+ ]
987
+ }),
988
+ mode: (v) => {
989
+ if (v.results.target !== "docs") return;
990
+ return select({
991
+ message: "Which variant do you want to start from?",
992
+ options: [
993
+ {
994
+ label: "Start from minimal styles",
995
+ value: "minimal",
996
+ hint: "for those who want to build their own variant from ground up."
997
+ },
998
+ {
999
+ label: "Start from default layout",
1000
+ value: "full-default",
1001
+ hint: "useful for adjusting small details."
1002
+ },
1003
+ {
1004
+ label: "Start from Notebook layout",
1005
+ value: "full-notebook",
1006
+ hint: "useful for adjusting small details."
1007
+ }
1008
+ ]
1009
+ });
1010
+ },
1011
+ page: async (v) => {
1012
+ if (v.results.target !== "docs" || v.results.mode === "minimal")
1013
+ return false;
1014
+ return confirm3({
1015
+ message: "Do you want to customise the page component too?"
1016
+ });
1017
+ }
1018
+ },
1019
+ {
1020
+ onCancel: () => {
1021
+ cancel2("Installation Stopped.");
1022
+ process.exit(0);
1023
+ }
1024
+ }
1025
+ );
1026
+ if (result.target === "docs") {
1027
+ let pageAdded = false;
1028
+ if (result.mode === "minimal") {
1029
+ await add("layouts/docs-min", resolver, config);
1030
+ pageAdded = true;
1031
+ } else {
1032
+ if (result.page) {
1033
+ await add("layouts/page", resolver, config);
1034
+ pageAdded = true;
1035
+ }
1036
+ await add(
1037
+ result.mode === "full-default" ? "layouts/docs" : "layouts/notebook",
1038
+ resolver,
1039
+ config
1040
+ );
1041
+ }
1042
+ log5.info(
1043
+ [
1044
+ picocolors5.bold("What is Next?"),
1045
+ "You can check the installed components in `components/layouts`.",
1046
+ picocolors5.dim("---"),
1047
+ "Open your `layout.tsx` files, replace the imports of components:",
1048
+ picocolors5.greenBright(
1049
+ "`fumadocs-ui/layouts/docs` -> `@/components/layouts/docs`"
1050
+ ),
1051
+ pageAdded ? picocolors5.greenBright(
1052
+ "`fumadocs-ui/page` -> `@/components/layouts/page`"
1053
+ ) : ""
1054
+ ].join("\n")
1055
+ );
1056
+ }
1057
+ if (result.target === "home") {
1058
+ await add("layouts/home", resolver, config);
1059
+ log5.info(
1060
+ [
1061
+ picocolors5.bold("What is Next?"),
1062
+ "You can check the installed components in `components/layouts`.",
1063
+ picocolors5.dim("---"),
1064
+ "Open your `layout.tsx` files, replace the imports of components:",
1065
+ picocolors5.greenBright(
1066
+ "`fumadocs-ui/layouts/home` -> `@/components/layouts/home`"
1067
+ )
1068
+ ].join("\n")
1069
+ );
1070
+ }
1071
+ }
1072
+
966
1073
  // src/index.ts
967
- var program = new Command();
968
- program.name("fumadocs").description("CLI to setup Fumadocs").version(package_default.version);
969
- program.command("config").description("init a config for Fumadocs CLI").action(async () => {
1074
+ var program = new Command().option("--config <string>");
1075
+ program.name("fumadocs").description("CLI to setup Fumadocs, init a config ").version(package_default.version).action(async () => {
970
1076
  await initConfig();
971
- console.log(picocolors5.green("Successful: ./cli.json"));
1077
+ console.log(picocolors6.green("Initialized a `./cli.json` config file."));
972
1078
  });
973
- program.command("init").description("init a new plugin to your docs").argument("[string]", "plugin name").option("--config <string>").action(async (str, { config }) => {
1079
+ program.command("customise").alias("customize").description("simple way to customise layouts with Fumadocs UI").option("--dir <string>", "the root url or directory to resolve registry").action(async (options) => {
1080
+ const resolver = getResolverFromDir(options.dir);
1081
+ await customise(resolver, await loadConfig(options.config));
1082
+ });
1083
+ program.command("init").description("init a new plugin to your docs").argument("[string]", "plugin name").action(async (str, { config }) => {
974
1084
  const loadedConfig = await loadConfig(config);
975
1085
  if (str) {
976
1086
  const plugin = str in plugins ? plugins[str] : void 0;
@@ -978,7 +1088,7 @@ program.command("init").description("init a new plugin to your docs").argument("
978
1088
  await init(plugin, loadedConfig);
979
1089
  return;
980
1090
  }
981
- const value = await select({
1091
+ const value = await select2({
982
1092
  message: "Select components to install",
983
1093
  options: Object.keys(plugins).map((c) => ({
984
1094
  label: c,
@@ -994,19 +1104,19 @@ program.command("init").description("init a new plugin to your docs").argument("
994
1104
  var dirShortcuts = {
995
1105
  ":dev": "https://fumadocs-dev.vercel.app/registry"
996
1106
  };
997
- 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(
1107
+ 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(
998
1108
  async (input, options) => {
999
- let dir = options.dir ?? "https://fumadocs.vercel.app/registry";
1000
- if (dir in dirShortcuts) dir = dirShortcuts[dir];
1001
- const resolver = dir.startsWith("http://") || dir.startsWith("https://") ? remoteResolver(dir) : localResolver(dir);
1109
+ const resolver = getResolverFromDir(options.dir);
1002
1110
  let target = input;
1003
1111
  if (input.length === 0) {
1004
1112
  const spin = spinner3();
1005
1113
  spin.start("fetching registry");
1006
1114
  const registry = await resolver("_registry.json");
1007
- spin.stop(picocolors5.bold(picocolors5.greenBright("registry fetched")));
1115
+ spin.stop(picocolors6.bold(picocolors6.greenBright("registry fetched")));
1008
1116
  if (!registry) {
1009
- log5.error(`Failed to fetch '_registry.json' file from ${dir}`);
1117
+ log6.error(
1118
+ `Failed to fetch '_registry.json' file from ${options.dir ?? "registry"}`
1119
+ );
1010
1120
  throw new Error(`Failed to fetch registry`);
1011
1121
  }
1012
1122
  const value = await multiselect({
@@ -1055,4 +1165,8 @@ program.command("tree").argument(
1055
1165
  }
1056
1166
  }
1057
1167
  );
1168
+ function getResolverFromDir(dir = "https://fumadocs.vercel.app/registry") {
1169
+ if (dir in dirShortcuts) dir = dirShortcuts[dir];
1170
+ return dir.startsWith("http://") || dir.startsWith("https://") ? remoteResolver(dir) : localResolver(dir);
1171
+ }
1058
1172
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fumadocs/cli",
3
- "version": "0.0.8",
3
+ "version": "0.1.1",
4
4
  "description": "The CLI tool for Fumadocs",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -29,16 +29,16 @@
29
29
  "@clack/prompts": "^0.10.0",
30
30
  "commander": "^13.1.0",
31
31
  "execa": "^9.5.2",
32
- "package-manager-detector": "^0.2.9",
32
+ "package-manager-detector": "^1.1.0",
33
33
  "picocolors": "^1.1.1",
34
34
  "ts-morph": "^25.0.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/cross-spawn": "^6.0.6",
38
- "@types/node": "22.13.1",
39
- "@types/react": "^19.0.8",
38
+ "@types/node": "22.14.0",
39
+ "@types/react": "^19.1.0",
40
40
  "fast-glob": "^3.3.3",
41
- "tsx": "^4.19.2",
41
+ "tsx": "^4.19.3",
42
42
  "eslint-config-custom": "0.0.0",
43
43
  "tsconfig": "0.0.0"
44
44
  },