@fumadocs/cli 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,241 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs5 from "fs/promises";
5
- import path4 from "path";
4
+ import fs6 from "fs/promises";
5
+ import path5 from "path";
6
6
  import { Command } from "commander";
7
7
  import picocolors3 from "picocolors";
8
8
 
9
- // src/utils/add/install-component.ts
10
- import path2 from "path";
11
- import fs from "fs/promises";
12
- import { confirm, isCancel, log, outro } from "@clack/prompts";
13
-
14
- // src/utils/typescript.ts
15
- import { Project } from "ts-morph";
16
- function createEmptyProject() {
17
- return new Project({
18
- compilerOptions: {}
19
- });
20
- }
21
-
22
- // src/constants.ts
23
- var typescriptExtensions = [".ts", ".tsx", ".js", ".jsx"];
24
-
25
- // src/utils/transform-references.ts
26
- import path from "path";
27
- function transformReferences(file, transform) {
28
- for (const specifier of file.getImportStringLiterals()) {
29
- const result = transform(specifier.getLiteralValue());
30
- if (!result) continue;
31
- specifier.setLiteralValue(result);
32
- }
33
- }
34
- function toImportSpecifier(sourceFile, referenceFile) {
35
- const extname = path.extname(referenceFile);
36
- const removeExt = typescriptExtensions.includes(extname);
37
- const importPath = path.relative(
38
- path.dirname(sourceFile),
39
- removeExt ? referenceFile.substring(0, referenceFile.length - extname.length) : referenceFile
40
- ).replaceAll(path.sep, "/");
41
- return importPath.startsWith("../") ? importPath : `./${importPath}`;
42
- }
43
-
44
- // src/registry/schema.ts
45
- import { z } from "zod";
46
- var namespaces = [
47
- "components",
48
- "lib",
49
- "css",
50
- "route",
51
- "ui",
52
- "block"
53
- ];
54
- var indexSchema = z.object({
55
- name: z.string(),
56
- title: z.string().optional(),
57
- description: z.string().optional()
58
- });
59
- var fileSchema = z.object({
60
- type: z.literal(namespaces),
61
- path: z.string(),
62
- target: z.string().optional(),
63
- content: z.string()
64
- });
65
- var componentSchema = z.object({
66
- name: z.string(),
67
- title: z.string().optional(),
68
- description: z.string().optional(),
69
- files: z.array(fileSchema),
70
- dependencies: z.record(z.string(), z.string().or(z.null())),
71
- devDependencies: z.record(z.string(), z.string().or(z.null())),
72
- subComponents: z.array(z.string()).default([])
73
- });
74
- var rootSchema = z.object({
75
- name: z.string(),
76
- index: z.array(indexSchema),
77
- components: z.array(componentSchema)
78
- });
79
-
80
- // src/registry/client.ts
81
- import { z as z2 } from "zod";
82
- function validateRegistryIndex(indexes) {
83
- return z2.array(indexSchema).parse(indexes);
84
- }
85
- function validateRegistryComponent(component) {
86
- return componentSchema.parse(component);
87
- }
88
-
89
- // src/utils/add/install-component.ts
90
- function createComponentInstaller(options) {
91
- const { config, resolver } = options;
92
- const project = createEmptyProject();
93
- const installedFiles = /* @__PURE__ */ new Set();
94
- const downloadedComps = /* @__PURE__ */ new Map();
95
- function buildFileList(downloaded) {
96
- const map = /* @__PURE__ */ new Map();
97
- for (const item of downloaded) {
98
- for (const file of item.files) {
99
- const filePath = file.target ?? file.path;
100
- if (map.has(filePath)) {
101
- console.warn(
102
- `noticed duplicated output file for ${filePath}, ignoring for now.`
103
- );
104
- continue;
105
- }
106
- map.set(filePath, file);
107
- }
108
- }
109
- return Array.from(map.values());
110
- }
111
- return {
112
- async install(name) {
113
- const downloaded = await this.download(name);
114
- const dependencies = {};
115
- const devDependencies = {};
116
- for (const item of downloaded) {
117
- Object.assign(dependencies, item.dependencies);
118
- Object.assign(devDependencies, item.devDependencies);
119
- }
120
- const fileList = buildFileList(downloaded);
121
- for (const file of fileList) {
122
- const filePath = file.target ?? file.path;
123
- if (installedFiles.has(filePath)) continue;
124
- const outPath = this.resolveOutputPath(file);
125
- const output = typescriptExtensions.includes(path2.extname(filePath)) ? this.transform(outPath, file, fileList) : file.content;
126
- const status = await fs.readFile(outPath).then((res) => {
127
- if (res.toString() === output) return "ignore";
128
- return "need-update";
129
- }).catch(() => "write");
130
- installedFiles.add(filePath);
131
- if (status === "ignore") continue;
132
- if (status === "need-update") {
133
- const override = await confirm({
134
- message: `Do you want to override ${outPath}?`,
135
- initialValue: false
136
- });
137
- if (isCancel(override)) {
138
- outro("Ended");
139
- process.exit(0);
140
- }
141
- if (!override) continue;
142
- }
143
- await fs.mkdir(path2.dirname(outPath), { recursive: true });
144
- await fs.writeFile(outPath, output);
145
- log.step(`downloaded ${outPath}`);
146
- }
147
- return {
148
- dependencies,
149
- devDependencies
150
- };
151
- },
152
- /**
153
- * return a list of components, merged with child components.
154
- */
155
- async download(name) {
156
- const cached = downloadedComps.get(name);
157
- if (cached) return cached;
158
- const comp = validateRegistryComponent(
159
- await resolver(`${name}.json`).catch((e) => {
160
- log.error(`component ${name} not found:`);
161
- log.error(String(e));
162
- process.exit(1);
163
- })
164
- );
165
- const result = [comp];
166
- downloadedComps.set(name, result);
167
- for (const sub of comp.subComponents) {
168
- result.push(...await this.download(sub));
169
- }
170
- return result;
171
- },
172
- transform(filePath, file, fileList) {
173
- const sourceFile = project.createSourceFile(filePath, file.content, {
174
- overwrite: true
175
- });
176
- transformReferences(sourceFile, (specifier) => {
177
- const prefix = "@/";
178
- if (specifier.startsWith(prefix)) {
179
- const lookup = specifier.substring(prefix.length);
180
- const target = fileList.find((item) => {
181
- const filePath2 = item.target ?? item.path;
182
- return filePath2 === lookup;
183
- });
184
- if (!target) {
185
- console.warn(`cannot find the referenced file of ${specifier}`);
186
- return specifier;
187
- }
188
- return toImportSpecifier(filePath, this.resolveOutputPath(target));
189
- }
190
- });
191
- return sourceFile.getFullText();
192
- },
193
- resolveOutputPath(ref) {
194
- if (ref.target) {
195
- return path2.join(config.baseDir, ref.target);
196
- }
197
- const base = path2.basename(ref.path);
198
- const dir = {
199
- components: config.aliases.componentsDir,
200
- block: config.aliases.blockDir,
201
- ui: config.aliases.uiDir,
202
- css: config.aliases.cssDir,
203
- lib: config.aliases.libDir,
204
- route: "./"
205
- }[ref.type];
206
- return path2.join(config.baseDir, dir, base);
207
- }
208
- };
209
- }
210
- function remoteResolver(url) {
211
- return async (file) => {
212
- const res = await fetch(`${url}/${file}`);
213
- if (!res.ok) {
214
- throw new Error(`failed to fetch ${url}/${file}: ${res.statusText}`);
215
- }
216
- return await res.json();
217
- };
218
- }
219
- function localResolver(dir) {
220
- return async (file) => {
221
- const filePath = path2.join(dir, file);
222
- return await fs.readFile(filePath).then((res) => JSON.parse(res.toString())).catch((e) => {
223
- throw new Error(`failed to resolve local file "${filePath}"`, {
224
- cause: e
225
- });
226
- });
227
- };
228
- }
229
-
230
9
  // src/config.ts
231
- import fs3 from "fs/promises";
10
+ import fs2 from "fs/promises";
232
11
 
233
12
  // src/utils/fs.ts
234
- import fs2 from "fs/promises";
235
- import path3 from "path";
13
+ import fs from "fs/promises";
14
+ import path from "path";
236
15
  async function exists(pathLike) {
237
16
  try {
238
- await fs2.access(pathLike);
17
+ await fs.access(pathLike);
239
18
  return true;
240
19
  } catch {
241
20
  return false;
@@ -248,7 +27,7 @@ async function isSrc() {
248
27
  }
249
28
 
250
29
  // src/config.ts
251
- import { z as z3 } from "zod";
30
+ import { z } from "zod";
252
31
  function createConfigSchema(isSrc2) {
253
32
  const defaultAliases = {
254
33
  uiDir: "./components/ui",
@@ -257,38 +36,43 @@ function createConfigSchema(isSrc2) {
257
36
  cssDir: "./styles",
258
37
  libDir: "./lib"
259
38
  };
260
- return z3.object({
261
- aliases: z3.object({
262
- uiDir: z3.string().default(defaultAliases.uiDir),
263
- componentsDir: z3.string().default(defaultAliases.uiDir),
264
- blockDir: z3.string().default(defaultAliases.blockDir),
265
- cssDir: z3.string().default(defaultAliases.componentsDir),
266
- libDir: z3.string().default(defaultAliases.libDir)
39
+ return z.object({
40
+ $schema: z.string().default(
41
+ isSrc2 ? "node_modules/@fumadocs/cli/dist/schema/src.json" : "node_modules/@fumadocs/cli/dist/schema/default.json"
42
+ ).optional(),
43
+ aliases: z.object({
44
+ uiDir: z.string().default(defaultAliases.uiDir),
45
+ componentsDir: z.string().default(defaultAliases.uiDir),
46
+ blockDir: z.string().default(defaultAliases.blockDir),
47
+ cssDir: z.string().default(defaultAliases.componentsDir),
48
+ libDir: z.string().default(defaultAliases.libDir)
267
49
  }).default(defaultAliases),
268
- baseDir: z3.string().default(isSrc2 ? "src" : ""),
269
- commands: z3.object({
50
+ baseDir: z.string().default(isSrc2 ? "src" : ""),
51
+ uiLibrary: z.enum(["radix-ui", "base-ui"]).default("radix-ui"),
52
+ commands: z.object({
270
53
  /**
271
54
  * command to format output code automatically
272
55
  */
273
- format: z3.string().optional()
56
+ format: z.string().optional()
274
57
  }).default({})
275
58
  });
276
59
  }
277
60
  async function createOrLoadConfig(file = "./cli.json") {
278
61
  const inited = await initConfig(file);
279
62
  if (inited) return inited;
280
- const content = (await fs3.readFile(file)).toString();
63
+ const content = (await fs2.readFile(file)).toString();
281
64
  const src = await isSrc();
282
65
  const configSchema = createConfigSchema(src);
283
66
  return configSchema.parse(JSON.parse(content));
284
67
  }
285
- async function initConfig(file = "./cli.json") {
286
- if (await fs3.stat(file).then(() => true).catch(() => false)) {
68
+ async function initConfig(file = "./cli.json", src) {
69
+ if (await fs2.stat(file).then(() => true).catch(() => false)) {
287
70
  return;
288
71
  }
289
- const src = await isSrc();
290
- const defaultConfig = createConfigSchema(src).parse({});
291
- await fs3.writeFile(file, JSON.stringify(defaultConfig, null, 2));
72
+ const defaultConfig = createConfigSchema(src ?? await isSrc()).parse(
73
+ {}
74
+ );
75
+ await fs2.writeFile(file, JSON.stringify(defaultConfig, null, 2));
292
76
  return defaultConfig;
293
77
  }
294
78
 
@@ -343,7 +127,7 @@ async function runTree(args) {
343
127
  // package.json
344
128
  var package_default = {
345
129
  name: "@fumadocs/cli",
346
- version: "1.0.3",
130
+ version: "1.2.0",
347
131
  description: "The CLI tool for Fumadocs",
348
132
  keywords: [
349
133
  "NextJs",
@@ -377,17 +161,17 @@ var package_default = {
377
161
  },
378
162
  dependencies: {
379
163
  "@clack/prompts": "^0.11.0",
380
- commander: "^14.0.1",
381
- "package-manager-detector": "^1.5.0",
164
+ commander: "^14.0.2",
165
+ "package-manager-detector": "^1.6.0",
382
166
  picocolors: "^1.1.1",
383
- tinyexec: "^1.0.1",
167
+ tinyexec: "^1.0.2",
384
168
  "ts-morph": "^27.0.2",
385
- zod: "^4.1.12"
169
+ zod: "^4.2.1"
386
170
  },
387
171
  devDependencies: {
388
- "@types/node": "24.9.1",
172
+ "@types/node": "24.10.2",
389
173
  "eslint-config-custom": "workspace:*",
390
- shadcn: "3.4.2",
174
+ shadcn: "3.6.2",
391
175
  tsconfig: "workspace:*"
392
176
  },
393
177
  publishConfig: {
@@ -396,28 +180,209 @@ var package_default = {
396
180
  };
397
181
 
398
182
  // src/commands/customise.ts
399
- import {
400
- cancel,
401
- confirm as confirm3,
402
- group,
403
- intro as intro2,
404
- log as log3,
405
- outro as outro3,
406
- select
407
- } from "@clack/prompts";
183
+ import { cancel, group, intro as intro2, log as log4, outro as outro3, select } from "@clack/prompts";
408
184
  import picocolors2 from "picocolors";
409
185
 
410
186
  // src/commands/add.ts
411
187
  import {
412
188
  intro,
413
189
  isCancel as isCancel3,
414
- log as log2,
190
+ log as log3,
415
191
  multiselect,
416
192
  outro as outro2,
417
193
  spinner as spinner2
418
194
  } from "@clack/prompts";
419
195
  import picocolors from "picocolors";
420
196
 
197
+ // src/registry/installer/index.ts
198
+ import path4 from "path";
199
+ import fs5 from "fs/promises";
200
+ import { confirm as confirm2, isCancel as isCancel2, log as log2, outro } from "@clack/prompts";
201
+
202
+ // src/utils/typescript.ts
203
+ import { Project } from "ts-morph";
204
+ function createEmptyProject() {
205
+ return new Project({
206
+ compilerOptions: {}
207
+ });
208
+ }
209
+
210
+ // src/constants.ts
211
+ var typescriptExtensions = [".ts", ".tsx", ".js", ".jsx"];
212
+
213
+ // src/utils/ast.ts
214
+ import path2 from "path";
215
+ function toImportSpecifier(sourceFile, referenceFile) {
216
+ const extname = path2.extname(referenceFile);
217
+ const removeExt = typescriptExtensions.includes(extname);
218
+ let importPath = path2.relative(
219
+ path2.dirname(sourceFile),
220
+ removeExt ? referenceFile.substring(0, referenceFile.length - extname.length) : referenceFile
221
+ ).replaceAll(path2.sep, "/");
222
+ if (removeExt && importPath.endsWith("/index")) {
223
+ importPath = importPath.slice(0, -"/index".length);
224
+ }
225
+ return importPath.startsWith("../") ? importPath : `./${importPath}`;
226
+ }
227
+
228
+ // src/registry/schema.ts
229
+ import { z as z2 } from "zod";
230
+ var namespaces = [
231
+ "components",
232
+ "lib",
233
+ "css",
234
+ "route",
235
+ "ui",
236
+ "block"
237
+ ];
238
+ var indexSchema = z2.object({
239
+ name: z2.string(),
240
+ title: z2.string().optional(),
241
+ description: z2.string().optional()
242
+ });
243
+ var fileSchema = z2.object({
244
+ type: z2.literal(namespaces),
245
+ path: z2.string(),
246
+ target: z2.string().optional(),
247
+ content: z2.string()
248
+ });
249
+ var httpSubComponent = z2.object({
250
+ type: z2.literal("http"),
251
+ baseUrl: z2.string(),
252
+ component: z2.string()
253
+ });
254
+ var componentSchema = z2.object({
255
+ name: z2.string(),
256
+ title: z2.string().optional(),
257
+ description: z2.string().optional(),
258
+ files: z2.array(fileSchema),
259
+ dependencies: z2.record(z2.string(), z2.string().or(z2.null())),
260
+ devDependencies: z2.record(z2.string(), z2.string().or(z2.null())),
261
+ /**
262
+ * list of sub components, either local (component name) or remote (registry info & component name)
263
+ */
264
+ subComponents: z2.array(z2.string().or(httpSubComponent)).default([])
265
+ });
266
+ var registryInfoSchema = z2.object({
267
+ /**
268
+ * define used variables, variables can be referenced in the import specifiers of component files.
269
+ */
270
+ variables: z2.record(
271
+ z2.string(),
272
+ z2.object({
273
+ description: z2.string().optional(),
274
+ default: z2.unknown().optional()
275
+ })
276
+ ).optional(),
277
+ /**
278
+ * provide variables to sub components
279
+ */
280
+ env: z2.record(z2.string(), z2.unknown()).optional(),
281
+ indexes: z2.array(indexSchema).default([]),
282
+ registries: z2.array(z2.string()).optional()
283
+ });
284
+
285
+ // src/registry/client.ts
286
+ import path3 from "path";
287
+ import fs3 from "fs/promises";
288
+ import { log } from "@clack/prompts";
289
+
290
+ // src/utils/cache.ts
291
+ var AsyncCache = class {
292
+ constructor() {
293
+ this.store = /* @__PURE__ */ new Map();
294
+ }
295
+ cached(key, fn) {
296
+ let cached = this.store.get(key);
297
+ if (cached !== void 0) return cached;
298
+ cached = fn();
299
+ this.store.set(key, cached);
300
+ return cached;
301
+ }
302
+ };
303
+
304
+ // src/registry/client.ts
305
+ var fetchCache = new AsyncCache();
306
+ var HttpRegistryClient = class _HttpRegistryClient {
307
+ constructor(baseUrl, config) {
308
+ this.baseUrl = baseUrl;
309
+ this.config = config;
310
+ this.registryId = baseUrl;
311
+ }
312
+ async fetchRegistryInfo(baseUrl = this.baseUrl) {
313
+ const url = new URL("_registry.json", `${baseUrl}/`);
314
+ return fetchCache.cached(url.href, async () => {
315
+ const res = await fetch(url);
316
+ if (!res.ok) {
317
+ throw new Error(`failed to fetch ${url.href}: ${res.statusText}`);
318
+ }
319
+ return registryInfoSchema.parse(await res.json());
320
+ });
321
+ }
322
+ async fetchComponent(name) {
323
+ const url = new URL(`${name}.json`, `${this.baseUrl}/`);
324
+ return fetchCache.cached(url.href, async () => {
325
+ const res = await fetch(`${this.baseUrl}/${name}.json`);
326
+ if (!res.ok) {
327
+ log.error(`component ${name} not found at ${url.href}`);
328
+ throw new Error(await res.text());
329
+ }
330
+ return componentSchema.parse(await res.json());
331
+ });
332
+ }
333
+ async hasComponent(name) {
334
+ const url = new URL(`${name}.json`, `${this.baseUrl}/`);
335
+ const res = await fetch(url, { method: "HEAD" });
336
+ return res.ok;
337
+ }
338
+ createLinkedRegistryClient(name) {
339
+ return new _HttpRegistryClient(`${this.baseUrl}/${name}`, this.config);
340
+ }
341
+ };
342
+ var LocalRegistryClient = class _LocalRegistryClient {
343
+ constructor(dir, config) {
344
+ this.dir = dir;
345
+ this.config = config;
346
+ this.registryId = dir;
347
+ }
348
+ async fetchRegistryInfo(dir = this.dir) {
349
+ if (this.registryInfo) return this.registryInfo;
350
+ const filePath = path3.join(dir, "_registry.json");
351
+ const out = await fs3.readFile(filePath).then((res) => JSON.parse(res.toString())).catch((e) => {
352
+ throw new Error(`failed to resolve local file "${filePath}"`, {
353
+ cause: e
354
+ });
355
+ });
356
+ return this.registryInfo = registryInfoSchema.parse(out);
357
+ }
358
+ async fetchComponent(name) {
359
+ const filePath = path3.join(this.dir, `${name}.json`);
360
+ const out = await fs3.readFile(filePath).then((res) => JSON.parse(res.toString())).catch((e) => {
361
+ log.error(`component ${name} not found at ${filePath}`);
362
+ throw e;
363
+ });
364
+ return componentSchema.parse(out);
365
+ }
366
+ async hasComponent(name) {
367
+ const filePath = path3.join(this.dir, `${name}.json`);
368
+ try {
369
+ await fs3.stat(filePath);
370
+ return true;
371
+ } catch {
372
+ return false;
373
+ }
374
+ }
375
+ createLinkedRegistryClient(name) {
376
+ return new _LocalRegistryClient(path3.join(this.dir, name), this.config);
377
+ }
378
+ };
379
+
380
+ // src/registry/installer/index.ts
381
+ import { x as x3 } from "tinyexec";
382
+
383
+ // src/registry/installer/dep-manager.ts
384
+ import fs4 from "fs/promises";
385
+
421
386
  // src/utils/get-package-manager.ts
422
387
  import { detect } from "package-manager-detector";
423
388
  async function getPackageManager() {
@@ -425,95 +390,266 @@ async function getPackageManager() {
425
390
  return result?.name ?? "npm";
426
391
  }
427
392
 
428
- // src/utils/add/install-deps.ts
429
- import { confirm as confirm2, isCancel as isCancel2, spinner } from "@clack/prompts";
393
+ // src/registry/installer/dep-manager.ts
394
+ import { confirm, isCancel, spinner } from "@clack/prompts";
430
395
  import { x as x2 } from "tinyexec";
431
-
432
- // src/utils/add/get-deps.ts
433
- import fs4 from "fs/promises";
434
- async function getDeps() {
435
- const dependencies = /* @__PURE__ */ new Map();
436
- if (!await exists("package.json")) return dependencies;
437
- const content = await fs4.readFile("package.json");
438
- const parsed = JSON.parse(content.toString());
439
- if ("dependencies" in parsed && typeof parsed.dependencies === "object") {
440
- const records = parsed.dependencies;
441
- Object.entries(records).forEach(([k, v]) => {
442
- dependencies.set(k, v);
443
- });
396
+ var DependencyManager = class {
397
+ /**
398
+ * Get dependencies from `package.json`
399
+ */
400
+ async getDeps() {
401
+ if (this.cachedInstalledDeps) return this.cachedInstalledDeps;
402
+ const dependencies = /* @__PURE__ */ new Map();
403
+ if (!await exists("package.json")) return dependencies;
404
+ const content = await fs4.readFile("package.json");
405
+ const parsed = JSON.parse(content.toString());
406
+ if ("dependencies" in parsed && typeof parsed.dependencies === "object") {
407
+ const records = parsed.dependencies;
408
+ for (const [k, v] of Object.entries(records)) {
409
+ dependencies.set(k, v);
410
+ }
411
+ }
412
+ if ("devDependencies" in parsed && typeof parsed.devDependencies === "object") {
413
+ const records = parsed.devDependencies;
414
+ for (const [k, v] of Object.entries(records)) {
415
+ dependencies.set(k, v);
416
+ }
417
+ }
418
+ return this.cachedInstalledDeps = dependencies;
444
419
  }
445
- if ("devDependencies" in parsed && typeof parsed.devDependencies === "object") {
446
- const records = parsed.devDependencies;
447
- Object.entries(records).forEach(([k, v]) => {
448
- dependencies.set(k, v);
449
- });
420
+ async resolveInstallDependencies(deps) {
421
+ const cachedInstalledDeps = await this.getDeps();
422
+ return Object.entries(deps).filter(([k]) => !cachedInstalledDeps.has(k)).map(([k, v]) => v === null || v.length === 0 ? k : `${k}@${v}`);
450
423
  }
451
- return dependencies;
452
- }
453
-
454
- // src/utils/add/install-deps.ts
455
- async function installDeps(deps, devDeps) {
456
- const installed = await getDeps();
457
- function toList(deps2) {
458
- return Object.entries(deps2).filter(([k]) => !installed.has(k)).map(([k, v]) => v === null || v.length === 0 ? k : `${k}@${v}`);
459
- }
460
- const items = toList(deps);
461
- const devItems = toList(devDeps);
462
- if (items.length > 0 || devItems.length > 0) {
424
+ async installDeps(deps, devDeps) {
425
+ const items = await this.resolveInstallDependencies(deps);
426
+ const devItems = await this.resolveInstallDependencies(devDeps);
427
+ if (items.length === 0 && devItems.length === 0) return;
463
428
  const manager = await getPackageManager();
464
- const value = await confirm2({
429
+ const value = await confirm({
465
430
  message: `Do you want to install with ${manager}?
466
431
  ${[...items, ...devItems].map((v) => `- ${v}`).join("\n")}`
467
432
  });
468
- if (isCancel2(value)) {
433
+ if (isCancel(value) || !value) {
469
434
  return;
470
435
  }
471
- if (value) {
472
- const spin = spinner();
473
- spin.start("Installing dependencies...");
474
- if (items.length > 0) await x2(manager, ["install", ...items]);
475
- if (devItems.length > 0) await x2(manager, ["install", ...devItems, "-D"]);
476
- spin.stop("Dependencies installed.");
436
+ const spin = spinner();
437
+ spin.start("Installing dependencies...");
438
+ if (items.length > 0) await x2(manager, ["install", ...items]);
439
+ if (devItems.length > 0) await x2(manager, ["install", ...devItems, "-D"]);
440
+ spin.stop("Dependencies installed.");
441
+ }
442
+ };
443
+
444
+ // src/registry/installer/index.ts
445
+ var ComponentInstaller = class {
446
+ constructor(rootClient, plugins = []) {
447
+ this.rootClient = rootClient;
448
+ this.plugins = plugins;
449
+ this.project = createEmptyProject();
450
+ this.installedFiles = /* @__PURE__ */ new Set();
451
+ this.downloadCache = new AsyncCache();
452
+ this.dependencies = {};
453
+ this.devDependencies = {};
454
+ this.pathToFileCache = new AsyncCache();
455
+ }
456
+ async install(name) {
457
+ let downloaded;
458
+ const info = await this.rootClient.fetchRegistryInfo();
459
+ for (const registry of info.registries ?? []) {
460
+ if (name.startsWith(`${registry}/`)) {
461
+ downloaded = await this.download(
462
+ name.slice(registry.length + 1),
463
+ this.rootClient.createLinkedRegistryClient(registry)
464
+ );
465
+ break;
466
+ }
467
+ }
468
+ downloaded ??= await this.download(name, this.rootClient);
469
+ for (const item of downloaded) {
470
+ Object.assign(this.dependencies, item.dependencies);
471
+ Object.assign(this.devDependencies, item.devDependencies);
472
+ }
473
+ for (const comp of downloaded) {
474
+ for (const file of comp.files) {
475
+ const outPath = this.resolveOutputPath(file);
476
+ if (this.installedFiles.has(outPath)) continue;
477
+ this.installedFiles.add(outPath);
478
+ const output = typescriptExtensions.includes(path4.extname(outPath)) ? await this.transform(name, file, comp, downloaded) : file.content;
479
+ const status = await fs5.readFile(outPath).then((res) => {
480
+ if (res.toString() === output) return "ignore";
481
+ return "need-update";
482
+ }).catch(() => "write");
483
+ if (status === "ignore") continue;
484
+ if (status === "need-update") {
485
+ const override = await confirm2({
486
+ message: `Do you want to override ${outPath}?`,
487
+ initialValue: false
488
+ });
489
+ if (isCancel2(override)) {
490
+ outro("Ended");
491
+ process.exit(0);
492
+ }
493
+ if (!override) continue;
494
+ }
495
+ await fs5.mkdir(path4.dirname(outPath), { recursive: true });
496
+ await fs5.writeFile(outPath, output);
497
+ log2.step(`downloaded ${outPath}`);
498
+ }
477
499
  }
478
500
  }
479
- }
501
+ async installDeps() {
502
+ await new DependencyManager().installDeps(
503
+ this.dependencies,
504
+ this.devDependencies
505
+ );
506
+ }
507
+ async onEnd() {
508
+ const config = this.rootClient.config;
509
+ if (config.commands.format) {
510
+ await x3(config.commands.format);
511
+ }
512
+ }
513
+ /**
514
+ * return a list of components, merged with child components & variables.
515
+ */
516
+ async download(name, client, contextVariables) {
517
+ const hash = `${client.registryId} ${name}`;
518
+ const info = await client.fetchRegistryInfo();
519
+ const variables = { ...contextVariables, ...info.env };
520
+ for (const [k, v] of Object.entries(info.variables ?? {})) {
521
+ variables[k] ??= v.default;
522
+ }
523
+ const out = await this.downloadCache.cached(hash, async () => {
524
+ const comp = await client.fetchComponent(name);
525
+ const result = [comp];
526
+ this.downloadCache.store.set(hash, result);
527
+ const child = await Promise.all(
528
+ comp.subComponents.map((sub) => {
529
+ if (typeof sub === "string") return this.download(sub, client);
530
+ const baseUrl = this.rootClient instanceof HttpRegistryClient ? new URL(sub.baseUrl, `${this.rootClient.baseUrl}/`).href : sub.baseUrl;
531
+ return this.download(
532
+ sub.component,
533
+ new HttpRegistryClient(baseUrl, client.config),
534
+ variables
535
+ );
536
+ })
537
+ );
538
+ for (const sub of child) result.push(...sub);
539
+ return result;
540
+ });
541
+ return out.map((file) => ({ ...file, variables }));
542
+ }
543
+ async transform(taskId, file, component, allComponents) {
544
+ const filePath = this.resolveOutputPath(file);
545
+ const sourceFile = this.project.createSourceFile(filePath, file.content, {
546
+ overwrite: true
547
+ });
548
+ const prefix = "@/";
549
+ const variables = Object.entries(component.variables ?? {});
550
+ const pathToFile = await this.pathToFileCache.cached(taskId, () => {
551
+ const map = /* @__PURE__ */ new Map();
552
+ for (const comp of allComponents) {
553
+ for (const file2 of comp.files) map.set(file2.target ?? file2.path, file2);
554
+ }
555
+ return map;
556
+ });
557
+ for (const specifier of sourceFile.getImportStringLiterals()) {
558
+ for (const [k, v] of variables) {
559
+ specifier.setLiteralValue(
560
+ specifier.getLiteralValue().replaceAll(`<${k}>`, v)
561
+ );
562
+ }
563
+ if (specifier.getLiteralValue().startsWith(prefix)) {
564
+ const lookup = specifier.getLiteralValue().substring(prefix.length);
565
+ const target = pathToFile.get(lookup);
566
+ if (target) {
567
+ specifier.setLiteralValue(
568
+ toImportSpecifier(filePath, this.resolveOutputPath(target))
569
+ );
570
+ } else {
571
+ console.warn(`cannot find the referenced file of ${specifier}`);
572
+ }
573
+ }
574
+ }
575
+ for (const plugin of this.plugins) {
576
+ await plugin.transform?.({
577
+ file: sourceFile,
578
+ componentFile: file,
579
+ component
580
+ });
581
+ }
582
+ return sourceFile.getFullText();
583
+ }
584
+ resolveOutputPath(file) {
585
+ const config = this.rootClient.config;
586
+ const dir = {
587
+ components: config.aliases.componentsDir,
588
+ block: config.aliases.blockDir,
589
+ ui: config.aliases.uiDir,
590
+ css: config.aliases.cssDir,
591
+ lib: config.aliases.libDir,
592
+ route: "./"
593
+ }[file.type];
594
+ if (file.target) {
595
+ return path4.join(config.baseDir, file.target.replace("<dir>", dir));
596
+ }
597
+ return path4.join(config.baseDir, dir, path4.basename(file.path));
598
+ }
599
+ };
600
+
601
+ // src/commands/shared.ts
602
+ var UIRegistries = {
603
+ "base-ui": "fumadocs/base-ui",
604
+ "radix-ui": "fumadocs/radix-ui"
605
+ };
480
606
 
481
607
  // src/commands/add.ts
482
- async function add(input, resolver, config) {
483
- const installer = createComponentInstaller({
484
- resolver,
485
- config
486
- });
487
- let target = input;
608
+ async function add(input, client) {
609
+ const config = client.config;
610
+ let target;
611
+ const installer = new ComponentInstaller(client);
612
+ const registry = UIRegistries[config.uiLibrary];
488
613
  if (input.length === 0) {
489
614
  const spin = spinner2();
490
615
  spin.start("fetching registry");
491
- const indexes = validateRegistryIndex(
492
- await resolver("_registry.json").catch((e) => {
493
- log2.error(String(e));
494
- process.exit(1);
495
- })
496
- );
616
+ const info = await client.fetchRegistryInfo();
617
+ const options = [];
618
+ for (const item of info.indexes) {
619
+ options.push({
620
+ label: item.title ?? item.name,
621
+ value: item.name,
622
+ hint: item.description
623
+ });
624
+ }
625
+ const { indexes } = await client.createLinkedRegistryClient(registry).fetchRegistryInfo();
626
+ for (const item of indexes) {
627
+ options.push({
628
+ label: item.title ?? item.name,
629
+ value: `${registry}/${item.name}`,
630
+ hint: item.description
631
+ });
632
+ }
497
633
  spin.stop(picocolors.bold(picocolors.greenBright("registry fetched")));
498
634
  const value = await multiselect({
499
635
  message: "Select components to install",
500
- options: indexes.map((item) => ({
501
- label: item.title,
502
- value: item.name,
503
- hint: item.description
504
- }))
636
+ options
505
637
  });
506
638
  if (isCancel3(value)) {
507
639
  outro2("Ended");
508
640
  return;
509
641
  }
510
642
  target = value;
643
+ } else {
644
+ target = await Promise.all(
645
+ input.map(
646
+ async (item) => await client.hasComponent(item) ? item : `${registry}/${item}`
647
+ )
648
+ );
511
649
  }
512
650
  await install(target, installer);
513
651
  }
514
652
  async function install(target, installer) {
515
- const dependencies = {};
516
- const devDependencies = {};
517
653
  for (const name of target) {
518
654
  intro(
519
655
  picocolors.bold(
@@ -521,27 +657,24 @@ async function install(target, installer) {
521
657
  )
522
658
  );
523
659
  try {
524
- const output = await installer.install(name);
525
- Object.assign(dependencies, output.dependencies);
526
- Object.assign(devDependencies, output.devDependencies);
660
+ await installer.install(name);
527
661
  outro2(picocolors.bold(picocolors.greenBright(`${name} installed`)));
528
662
  } catch (e) {
529
- log2.error(String(e));
663
+ log3.error(String(e));
530
664
  throw e;
531
665
  }
532
666
  }
533
667
  intro(picocolors.bold("New Dependencies"));
534
- await installDeps(dependencies, devDependencies);
668
+ await installer.installDeps();
669
+ await installer.onEnd();
535
670
  outro2(picocolors.bold(picocolors.greenBright("Successful")));
536
671
  }
537
672
 
538
673
  // src/commands/customise.ts
539
- async function customise(resolver, config) {
674
+ async function customise(client) {
540
675
  intro2(picocolors2.bgBlack(picocolors2.whiteBright("Customise Fumadocs UI")));
541
- const installer = createComponentInstaller({
542
- resolver,
543
- config
544
- });
676
+ const config = client.config;
677
+ const installer = new ComponentInstaller(client);
545
678
  const result = await group(
546
679
  {
547
680
  target: () => select({
@@ -581,13 +714,6 @@ async function customise(resolver, config) {
581
714
  }
582
715
  ]
583
716
  });
584
- },
585
- page: async (v) => {
586
- if (v.results.target !== "docs" || v.results.mode === "minimal")
587
- return false;
588
- return confirm3({
589
- message: "Do you want to customise the page component too?"
590
- });
591
717
  }
592
718
  },
593
719
  {
@@ -597,39 +723,38 @@ async function customise(resolver, config) {
597
723
  }
598
724
  }
599
725
  );
726
+ const registry = UIRegistries[config.uiLibrary];
600
727
  if (result.target === "docs") {
601
728
  const targets = [];
602
- let pageAdded = false;
603
729
  if (result.mode === "minimal") {
604
- targets.push("layouts/docs-min");
605
- pageAdded = true;
730
+ targets.push("fumadocs/ui/layouts/docs-min");
606
731
  } else {
607
- if (result.page) {
608
- targets.push("layouts/page");
609
- pageAdded = true;
610
- }
611
732
  targets.push(
612
- result.mode === "full-default" ? "layouts/docs" : "layouts/notebook"
733
+ result.mode === "full-default" ? `${registry}/layouts/docs` : `${registry}/layouts/notebook`
613
734
  );
614
735
  }
615
736
  await install(targets, installer);
616
- const maps = [
617
- ["fumadocs-ui/layouts/docs", "@/components/layout/docs"]
737
+ const maps = result.mode === "full-notebook" ? [
738
+ ["fumadocs-ui/layouts/notebook", "@/components/layout/notebook"],
739
+ [
740
+ "fumadocs-ui/layouts/notebook/page",
741
+ "@/components/layout/notebook/page"
742
+ ]
743
+ ] : [
744
+ ["fumadocs-ui/layouts/docs", "@/components/layout/docs"],
745
+ ["fumadocs-ui/layouts/docs/page", "@/components/layout/docs/page"]
618
746
  ];
619
- if (pageAdded) {
620
- maps.push(["fumadocs-ui/page", "@/components/layout/page"]);
621
- }
622
747
  printNext(...maps);
623
748
  }
624
749
  if (result.target === "home") {
625
- await install(["layouts/home"], installer);
750
+ await install([`${registry}/layouts/home`], installer);
626
751
  printNext(["fumadocs-ui/layouts/home", `@/components/layout/home`]);
627
752
  }
628
753
  outro3(picocolors2.bold("Have fun!"));
629
754
  }
630
755
  function printNext(...maps) {
631
756
  intro2(picocolors2.bold("What is Next?"));
632
- log3.info(
757
+ log4.info(
633
758
  [
634
759
  "You can check the installed components in `components`.",
635
760
  picocolors2.dim("---"),
@@ -651,17 +776,24 @@ program.name("fumadocs").description("CLI to setup Fumadocs, init a config").ver
651
776
  }
652
777
  });
653
778
  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) => {
654
- const resolver = getResolverFromDir(options.dir);
655
- await customise(resolver, await createOrLoadConfig(options.config));
779
+ await customise(
780
+ createClientFromDir(
781
+ options.dir,
782
+ await createOrLoadConfig(options.config)
783
+ )
784
+ );
656
785
  });
657
786
  var dirShortcuts = {
658
- ":dev": "https://preview.fumadocs.dev/registry",
659
- ":localhost": "http://localhost:3000/registry"
787
+ ":preview": "https://preview.fumadocs.dev/registry",
788
+ ":dev": "http://localhost:3000/registry"
660
789
  };
661
790
  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(
662
791
  async (input, options) => {
663
- const resolver = getResolverFromDir(options.dir);
664
- await add(input, resolver, await createOrLoadConfig(options.config));
792
+ const client = createClientFromDir(
793
+ options.dir,
794
+ await createOrLoadConfig(options.config)
795
+ );
796
+ await add(input, client);
665
797
  }
666
798
  );
667
799
  program.command("tree").argument(
@@ -681,17 +813,17 @@ program.command("tree").argument(
681
813
  } catch {
682
814
  nodes = await runTree(str ?? "./");
683
815
  }
684
- const out = js || output && jsExtensions.includes(path4.extname(output)) ? treeToJavaScript(nodes, noRoot, importName) : treeToMdx(nodes, noRoot);
816
+ const out = js || output && jsExtensions.includes(path5.extname(output)) ? treeToJavaScript(nodes, noRoot, importName) : treeToMdx(nodes, noRoot);
685
817
  if (output) {
686
- await fs5.mkdir(path4.dirname(output), { recursive: true });
687
- await fs5.writeFile(output, out);
818
+ await fs6.mkdir(path5.dirname(output), { recursive: true });
819
+ await fs6.writeFile(output, out);
688
820
  } else {
689
821
  console.log(out);
690
822
  }
691
823
  }
692
824
  );
693
- function getResolverFromDir(dir = "https://fumadocs.dev/registry") {
825
+ function createClientFromDir(dir = "https://fumadocs.dev/registry", config) {
694
826
  if (dir in dirShortcuts) dir = dirShortcuts[dir];
695
- return dir.startsWith("http://") || dir.startsWith("https://") ? remoteResolver(dir) : localResolver(dir);
827
+ return dir.startsWith("http://") || dir.startsWith("https://") ? new HttpRegistryClient(dir, config) : new LocalRegistryClient(dir, config);
696
828
  }
697
829
  program.parse();