@fumadocs/cli 0.0.7 → 0.1.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.
@@ -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,92 @@ 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
+ const files = [];
258
+ async function build2(file) {
259
+ let inputPath;
260
+ let outputPath;
261
+ if (typeof file === "string") {
262
+ let namespace;
263
+ const parsed = file.split(":", 2);
264
+ if (parsed.length > 1) {
265
+ namespace = parsed[0];
266
+ inputPath = path3.join(builder.registryDir, parsed[1]);
267
+ } else {
268
+ inputPath = path3.join(builder.registryDir, file);
269
+ }
270
+ outputPath = builder.resolveOutputPath(file, void 0, namespace);
271
+ } else {
272
+ inputPath = path3.join(builder.registryDir, file.in);
273
+ outputPath = file.out;
274
+ }
275
+ if (processedFiles.has(inputPath)) return;
276
+ processedFiles.add(inputPath);
277
+ const queue = [];
278
+ files.push(
279
+ await buildFile(
280
+ inputPath,
281
+ outputPath,
282
+ builder,
283
+ component,
284
+ (reference) => {
285
+ if (reference.type === "file") {
286
+ queue.push(path3.relative(builder.registryDir, reference.file));
287
+ return builder.resolveOutputPath(reference.file);
288
+ }
289
+ if (reference.type === "sub-component") {
290
+ const resolved = reference.resolved;
291
+ subComponents.add(resolved.component.name);
292
+ if (resolved.type === "remote") {
293
+ return reference.targetFile;
294
+ }
295
+ for (const childFile of resolved.component.files) {
296
+ if (typeof childFile === "string" && childFile === reference.targetFile) {
297
+ return builder.resolveOutputPath(
298
+ childFile,
299
+ reference.resolved.registryName
300
+ );
301
+ }
302
+ if (typeof childFile === "object" && childFile.in === reference.targetFile) {
303
+ return childFile.out;
304
+ }
305
+ }
306
+ throw new Error(
307
+ `Failed to find sub component ${resolved.component.name}'s ${reference.targetFile} referenced by ${inputPath}`
308
+ );
309
+ }
310
+ if (reference.type === "dependency") {
311
+ if (reference.isDev)
312
+ devDependencies.set(reference.name, reference.version);
313
+ else dependencies.set(reference.name, reference.version);
314
+ }
315
+ }
316
+ )
317
+ );
318
+ await Promise.all(queue.map(build2));
319
+ }
320
+ await Promise.all(component.files.map(build2));
321
+ return [
322
+ component,
323
+ {
324
+ name: component.name,
325
+ files,
326
+ subComponents: Array.from(subComponents),
327
+ dependencies: Object.fromEntries(dependencies),
328
+ devDependencies: Object.fromEntries(devDependencies)
329
+ }
330
+ ];
331
+ }
270
332
 
271
333
  // src/build/index.ts
272
334
  async function writeOutput(dir, out, options = {}) {
273
335
  const { log = true } = options;
274
336
  if (options.cleanDir && await exists(dir)) {
275
- await fs2.rm(dir, {
337
+ await fs3.rm(dir, {
276
338
  recursive: true
277
339
  });
278
340
  if (log) {
@@ -281,8 +343,8 @@ async function writeOutput(dir, out, options = {}) {
281
343
  }
282
344
  async function writeFile2(file, content) {
283
345
  if (!log) return;
284
- await fs2.mkdir(path4.dirname(file), { recursive: true });
285
- await fs2.writeFile(file, content);
346
+ await fs3.mkdir(path4.dirname(file), { recursive: true });
347
+ await fs3.writeFile(file, content);
286
348
  const size = (Buffer.byteLength(content) / 1024).toFixed(2);
287
349
  console.log(
288
350
  `${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
 
@@ -238,7 +238,7 @@ async function init(plugin, config = {}) {
238
238
  if (value) {
239
239
  await plugin.transform(ctx);
240
240
  note(
241
- `You can format the output with Prettier or other code formating tools
241
+ `You can format the output with Prettier or other code formatting tools
242
242
  prettier . --write`,
243
243
  picocolors.bold(picocolors.green("Changes Applied"))
244
244
  );
@@ -510,6 +510,10 @@ import fs5 from "node:fs/promises";
510
510
  import path5 from "node:path";
511
511
  var transformExtensions = [".js", ".ts", ".tsx", ".jsx"];
512
512
  async function moveFiles(from, to, filter, project, src2, originalDir = from) {
513
+ function isIncluded(file) {
514
+ if (!transformExtensions.includes(path5.extname(file))) return false;
515
+ return filter(path5.resolve(file));
516
+ }
513
517
  const stats = await fs5.lstat(from).catch(() => void 0);
514
518
  if (!stats) return;
515
519
  if (stats.isDirectory()) {
@@ -517,8 +521,8 @@ async function moveFiles(from, to, filter, project, src2, originalDir = from) {
517
521
  await Promise.all(
518
522
  items.map(async (item) => {
519
523
  await moveFiles(
520
- path5.resolve(from, item),
521
- path5.resolve(to, item),
524
+ path5.join(from, item),
525
+ path5.join(to, item),
522
526
  filter,
523
527
  project,
524
528
  src2,
@@ -529,31 +533,31 @@ async function moveFiles(from, to, filter, project, src2, originalDir = from) {
529
533
  await fs5.rmdir(from).catch(() => {
530
534
  });
531
535
  }
532
- if (!stats.isFile()) return;
533
- const allowed = await filter(path5.resolve(from));
534
- if (!allowed) return;
535
- if (transformExtensions.includes(path5.extname(from))) {
536
- const content = await fs5.readFile(from);
537
- const sourceFile = project.createSourceFile(from, content.toString(), {
538
- overwrite: true
539
- });
540
- await transformReferences(
541
- sourceFile,
542
- {
543
- alias: {
544
- type: "append",
545
- dir: src2 ? "src" : ""
546
- },
547
- relativeTo: path5.dirname(from)
536
+ if (!stats.isFile() || !await isIncluded(from)) return;
537
+ const content = await fs5.readFile(from);
538
+ const sourceFile = project.createSourceFile(from, content.toString(), {
539
+ overwrite: true
540
+ });
541
+ await transformReferences(
542
+ sourceFile,
543
+ {
544
+ alias: {
545
+ type: "append",
546
+ dir: src2 ? "src" : ""
548
547
  },
549
- (resolved) => {
550
- if (resolved.type !== "file") return;
551
- if (isRelative(originalDir, from) && filter(resolved.path)) return;
552
- return toReferencePath(to, resolved.path);
553
- }
554
- );
555
- await sourceFile.save();
556
- }
548
+ relativeTo: path5.dirname(from)
549
+ },
550
+ async (resolved) => {
551
+ if (resolved.type !== "file") return;
552
+ if (
553
+ // ignore if the file is also moved
554
+ isRelative(originalDir, from) && await isIncluded(resolved.path)
555
+ )
556
+ return;
557
+ return toReferencePath(to, resolved.path);
558
+ }
559
+ );
560
+ await sourceFile.save();
557
561
  await fs5.mkdir(path5.dirname(to), { recursive: true });
558
562
  await fs5.rename(from, to);
559
563
  }
@@ -707,7 +711,9 @@ export default async function Page({
707
711
  resolveAppPath("./app", ctx.src),
708
712
  resolveAppPath("./app/[lang]", ctx.src),
709
713
  (v) => {
710
- return path7.basename(v, path7.extname(v)) !== "layout.config" && !isRelative("./app/api", v);
714
+ const parsed = path7.parse(v);
715
+ if (parsed.ext === ".css") return false;
716
+ return parsed.name !== "layout.config" && !isRelative("./app/api", v);
711
717
  },
712
718
  project,
713
719
  ctx.src
@@ -902,7 +908,7 @@ async function runTree(args) {
902
908
  // package.json
903
909
  var package_default = {
904
910
  name: "@fumadocs/cli",
905
- version: "0.0.7",
911
+ version: "0.1.0",
906
912
  description: "The CLI tool for Fumadocs",
907
913
  keywords: [
908
914
  "NextJs",
@@ -936,35 +942,121 @@ var package_default = {
936
942
  "types:check": "tsc --noEmit"
937
943
  },
938
944
  dependencies: {
939
- "@clack/prompts": "^0.9.1",
940
- commander: "^13.0.0",
945
+ "@clack/prompts": "^0.10.0",
946
+ commander: "^13.1.0",
941
947
  execa: "^9.5.2",
942
- "package-manager-detector": "^0.2.8",
948
+ "package-manager-detector": "^1.1.0",
943
949
  picocolors: "^1.1.1",
944
- "ts-morph": "^25.0.0"
950
+ "ts-morph": "^25.0.1"
945
951
  },
946
952
  devDependencies: {
947
953
  "@types/cross-spawn": "^6.0.6",
948
- "@types/node": "22.10.6",
949
- "@types/react": "^19.0.7",
954
+ "@types/node": "22.13.16",
955
+ "@types/react": "^19.0.12",
950
956
  "eslint-config-custom": "workspace:*",
951
957
  "fast-glob": "^3.3.3",
952
958
  tsconfig: "workspace:*",
953
- tsx: "^4.19.2"
959
+ tsx: "^4.19.3"
954
960
  },
955
961
  publishConfig: {
956
962
  access: "public"
957
963
  }
958
964
  };
959
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
+ }),
983
+ mode: (v) => {
984
+ if (v.results.target !== "docs") return;
985
+ return select({
986
+ message: "Which variant do you want to start from?",
987
+ options: [
988
+ {
989
+ label: "Start from minimal styles",
990
+ value: "minimal",
991
+ hint: "for those who want to build their own variant from ground up."
992
+ },
993
+ {
994
+ label: "Start from default layout",
995
+ value: "full-default",
996
+ hint: "useful for adjusting small details."
997
+ },
998
+ {
999
+ label: "Start from Notebook layout",
1000
+ value: "full-notebook",
1001
+ hint: "useful for adjusting small details."
1002
+ }
1003
+ ]
1004
+ });
1005
+ },
1006
+ page: async (v) => {
1007
+ if (v.results.target !== "docs" || v.results.mode === "minimal")
1008
+ return false;
1009
+ return confirm3({
1010
+ message: "Do you want to customise the page component too?"
1011
+ });
1012
+ }
1013
+ },
1014
+ {
1015
+ onCancel: () => {
1016
+ cancel2("Installation Stopped.");
1017
+ process.exit(0);
1018
+ }
1019
+ }
1020
+ );
1021
+ if (result.target === "docs" && result.mode) {
1022
+ if (result.page) await add("layouts/page", resolver, config);
1023
+ if (result.mode === "minimal") {
1024
+ await add("layouts/docs-min", resolver, config);
1025
+ } else {
1026
+ await add(
1027
+ result.mode === "full-default" ? "layouts/docs" : "layouts/notebook",
1028
+ resolver,
1029
+ config
1030
+ );
1031
+ }
1032
+ log5.info(
1033
+ [
1034
+ picocolors5.bold("What is Next?"),
1035
+ "You can check the installed components in `components/layouts`.",
1036
+ picocolors5.dim("---"),
1037
+ "Open your `layout.tsx` files, replace the imports of components:",
1038
+ picocolors5.greenBright(
1039
+ "`fumadocs-ui/layouts/docs` -> `@/components/layouts/docs`"
1040
+ ),
1041
+ result.page || result.mode === "minimal" ? picocolors5.greenBright(
1042
+ "`fumadocs-ui/page` -> `@/components/layouts/page`"
1043
+ ) : ""
1044
+ ].join("\n")
1045
+ );
1046
+ }
1047
+ }
1048
+
960
1049
  // src/index.ts
961
- var program = new Command();
962
- program.name("fumadocs").description("CLI to setup Fumadocs").version(package_default.version);
963
- program.command("config").description("init a config for Fumadocs CLI").action(async () => {
1050
+ var program = new Command().option("--config <string>");
1051
+ program.name("fumadocs").description("CLI to setup Fumadocs, init a config ").version(package_default.version).action(async () => {
964
1052
  await initConfig();
965
- console.log(picocolors5.green("Successful: ./cli.json"));
1053
+ console.log(picocolors6.green("Initialized a `./cli.json` config file."));
966
1054
  });
967
- program.command("init").description("init a new plugin to your docs").argument("[string]", "plugin name").option("--config <string>").action(async (str, { config }) => {
1055
+ 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) => {
1056
+ const resolver = getResolverFromDir(options.dir);
1057
+ await customise(resolver, await loadConfig(options.config));
1058
+ });
1059
+ program.command("init").description("init a new plugin to your docs").argument("[string]", "plugin name").action(async (str, { config }) => {
968
1060
  const loadedConfig = await loadConfig(config);
969
1061
  if (str) {
970
1062
  const plugin = str in plugins ? plugins[str] : void 0;
@@ -972,7 +1064,7 @@ program.command("init").description("init a new plugin to your docs").argument("
972
1064
  await init(plugin, loadedConfig);
973
1065
  return;
974
1066
  }
975
- const value = await select({
1067
+ const value = await select2({
976
1068
  message: "Select components to install",
977
1069
  options: Object.keys(plugins).map((c) => ({
978
1070
  label: c,
@@ -988,19 +1080,19 @@ program.command("init").description("init a new plugin to your docs").argument("
988
1080
  var dirShortcuts = {
989
1081
  ":dev": "https://fumadocs-dev.vercel.app/registry"
990
1082
  };
991
- 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(
1083
+ 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(
992
1084
  async (input, options) => {
993
- let dir = options.dir ?? "https://fumadocs.vercel.app/registry";
994
- if (dir in dirShortcuts) dir = dirShortcuts[dir];
995
- const resolver = dir.startsWith("http://") || dir.startsWith("https://") ? remoteResolver(dir) : localResolver(dir);
1085
+ const resolver = getResolverFromDir(options.dir);
996
1086
  let target = input;
997
1087
  if (input.length === 0) {
998
1088
  const spin = spinner3();
999
1089
  spin.start("fetching registry");
1000
1090
  const registry = await resolver("_registry.json");
1001
- spin.stop(picocolors5.bold(picocolors5.greenBright("registry fetched")));
1091
+ spin.stop(picocolors6.bold(picocolors6.greenBright("registry fetched")));
1002
1092
  if (!registry) {
1003
- log5.error(`Failed to fetch '_registry.json' file from ${dir}`);
1093
+ log6.error(
1094
+ `Failed to fetch '_registry.json' file from ${options.dir ?? "registry"}`
1095
+ );
1004
1096
  throw new Error(`Failed to fetch registry`);
1005
1097
  }
1006
1098
  const value = await multiselect({
@@ -1049,4 +1141,8 @@ program.command("tree").argument(
1049
1141
  }
1050
1142
  }
1051
1143
  );
1144
+ function getResolverFromDir(dir = "https://fumadocs.vercel.app/registry") {
1145
+ if (dir in dirShortcuts) dir = dirShortcuts[dir];
1146
+ return dir.startsWith("http://") || dir.startsWith("https://") ? remoteResolver(dir) : localResolver(dir);
1147
+ }
1052
1148
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fumadocs/cli",
3
- "version": "0.0.7",
3
+ "version": "0.1.0",
4
4
  "description": "The CLI tool for Fumadocs",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -26,19 +26,19 @@
26
26
  "dist/*"
27
27
  ],
28
28
  "dependencies": {
29
- "@clack/prompts": "^0.9.1",
30
- "commander": "^13.0.0",
29
+ "@clack/prompts": "^0.10.0",
30
+ "commander": "^13.1.0",
31
31
  "execa": "^9.5.2",
32
- "package-manager-detector": "^0.2.8",
32
+ "package-manager-detector": "^1.1.0",
33
33
  "picocolors": "^1.1.1",
34
- "ts-morph": "^25.0.0"
34
+ "ts-morph": "^25.0.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/cross-spawn": "^6.0.6",
38
- "@types/node": "22.10.6",
39
- "@types/react": "^19.0.7",
38
+ "@types/node": "22.13.16",
39
+ "@types/react": "^19.0.12",
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
  },