@fumadocs/cli 1.1.0 → 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.1.0",
130
+ version: "1.2.0",
347
131
  description: "The CLI tool for Fumadocs",
348
132
  keywords: [
349
133
  "NextJs",
@@ -378,16 +162,16 @@ var package_default = {
378
162
  dependencies: {
379
163
  "@clack/prompts": "^0.11.0",
380
164
  commander: "^14.0.2",
381
- "package-manager-detector": "^1.5.0",
165
+ "package-manager-detector": "^1.6.0",
382
166
  picocolors: "^1.1.1",
383
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.10.1",
172
+ "@types/node": "24.10.2",
389
173
  "eslint-config-custom": "workspace:*",
390
- shadcn: "3.5.0",
174
+ shadcn: "3.6.2",
391
175
  tsconfig: "workspace:*"
392
176
  },
393
177
  publishConfig: {
@@ -396,20 +180,209 @@ var package_default = {
396
180
  };
397
181
 
398
182
  // src/commands/customise.ts
399
- import { cancel, group, intro as intro2, log as log3, outro as outro3, select } from "@clack/prompts";
183
+ import { cancel, group, intro as intro2, log as log4, outro as outro3, select } from "@clack/prompts";
400
184
  import picocolors2 from "picocolors";
401
185
 
402
186
  // src/commands/add.ts
403
187
  import {
404
188
  intro,
405
189
  isCancel as isCancel3,
406
- log as log2,
190
+ log as log3,
407
191
  multiselect,
408
192
  outro as outro2,
409
193
  spinner as spinner2
410
194
  } from "@clack/prompts";
411
195
  import picocolors from "picocolors";
412
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
+
413
386
  // src/utils/get-package-manager.ts
414
387
  import { detect } from "package-manager-detector";
415
388
  async function getPackageManager() {
@@ -417,95 +390,266 @@ async function getPackageManager() {
417
390
  return result?.name ?? "npm";
418
391
  }
419
392
 
420
- // src/utils/add/install-deps.ts
421
- 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";
422
395
  import { x as x2 } from "tinyexec";
423
-
424
- // src/utils/add/get-deps.ts
425
- import fs4 from "fs/promises";
426
- async function getDeps() {
427
- const dependencies = /* @__PURE__ */ new Map();
428
- if (!await exists("package.json")) return dependencies;
429
- const content = await fs4.readFile("package.json");
430
- const parsed = JSON.parse(content.toString());
431
- if ("dependencies" in parsed && typeof parsed.dependencies === "object") {
432
- const records = parsed.dependencies;
433
- Object.entries(records).forEach(([k, v]) => {
434
- dependencies.set(k, v);
435
- });
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;
436
419
  }
437
- if ("devDependencies" in parsed && typeof parsed.devDependencies === "object") {
438
- const records = parsed.devDependencies;
439
- Object.entries(records).forEach(([k, v]) => {
440
- dependencies.set(k, v);
441
- });
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}`);
442
423
  }
443
- return dependencies;
444
- }
445
-
446
- // src/utils/add/install-deps.ts
447
- async function installDeps(deps, devDeps) {
448
- const installed = await getDeps();
449
- function toList(deps2) {
450
- return Object.entries(deps2).filter(([k]) => !installed.has(k)).map(([k, v]) => v === null || v.length === 0 ? k : `${k}@${v}`);
451
- }
452
- const items = toList(deps);
453
- const devItems = toList(devDeps);
454
- 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;
455
428
  const manager = await getPackageManager();
456
- const value = await confirm2({
429
+ const value = await confirm({
457
430
  message: `Do you want to install with ${manager}?
458
431
  ${[...items, ...devItems].map((v) => `- ${v}`).join("\n")}`
459
432
  });
460
- if (isCancel2(value)) {
433
+ if (isCancel(value) || !value) {
461
434
  return;
462
435
  }
463
- if (value) {
464
- const spin = spinner();
465
- spin.start("Installing dependencies...");
466
- if (items.length > 0) await x2(manager, ["install", ...items]);
467
- if (devItems.length > 0) await x2(manager, ["install", ...devItems, "-D"]);
468
- 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
+ }
499
+ }
500
+ }
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);
469
511
  }
470
512
  }
471
- }
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
+ };
472
606
 
473
607
  // src/commands/add.ts
474
- async function add(input, resolver, config) {
475
- const installer = createComponentInstaller({
476
- resolver,
477
- config
478
- });
479
- 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];
480
613
  if (input.length === 0) {
481
614
  const spin = spinner2();
482
615
  spin.start("fetching registry");
483
- const indexes = validateRegistryIndex(
484
- await resolver("_registry.json").catch((e) => {
485
- log2.error(String(e));
486
- process.exit(1);
487
- })
488
- );
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
+ }
489
633
  spin.stop(picocolors.bold(picocolors.greenBright("registry fetched")));
490
634
  const value = await multiselect({
491
635
  message: "Select components to install",
492
- options: indexes.map((item) => ({
493
- label: item.title,
494
- value: item.name,
495
- hint: item.description
496
- }))
636
+ options
497
637
  });
498
638
  if (isCancel3(value)) {
499
639
  outro2("Ended");
500
640
  return;
501
641
  }
502
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
+ );
503
649
  }
504
650
  await install(target, installer);
505
651
  }
506
652
  async function install(target, installer) {
507
- const dependencies = {};
508
- const devDependencies = {};
509
653
  for (const name of target) {
510
654
  intro(
511
655
  picocolors.bold(
@@ -513,27 +657,24 @@ async function install(target, installer) {
513
657
  )
514
658
  );
515
659
  try {
516
- const output = await installer.install(name);
517
- Object.assign(dependencies, output.dependencies);
518
- Object.assign(devDependencies, output.devDependencies);
660
+ await installer.install(name);
519
661
  outro2(picocolors.bold(picocolors.greenBright(`${name} installed`)));
520
662
  } catch (e) {
521
- log2.error(String(e));
663
+ log3.error(String(e));
522
664
  throw e;
523
665
  }
524
666
  }
525
667
  intro(picocolors.bold("New Dependencies"));
526
- await installDeps(dependencies, devDependencies);
668
+ await installer.installDeps();
669
+ await installer.onEnd();
527
670
  outro2(picocolors.bold(picocolors.greenBright("Successful")));
528
671
  }
529
672
 
530
673
  // src/commands/customise.ts
531
- async function customise(resolver, config) {
674
+ async function customise(client) {
532
675
  intro2(picocolors2.bgBlack(picocolors2.whiteBright("Customise Fumadocs UI")));
533
- const installer = createComponentInstaller({
534
- resolver,
535
- config
536
- });
676
+ const config = client.config;
677
+ const installer = new ComponentInstaller(client);
537
678
  const result = await group(
538
679
  {
539
680
  target: () => select({
@@ -582,13 +723,14 @@ async function customise(resolver, config) {
582
723
  }
583
724
  }
584
725
  );
726
+ const registry = UIRegistries[config.uiLibrary];
585
727
  if (result.target === "docs") {
586
728
  const targets = [];
587
729
  if (result.mode === "minimal") {
588
- targets.push("layouts/docs-min");
730
+ targets.push("fumadocs/ui/layouts/docs-min");
589
731
  } else {
590
732
  targets.push(
591
- result.mode === "full-default" ? "layouts/docs" : "layouts/notebook"
733
+ result.mode === "full-default" ? `${registry}/layouts/docs` : `${registry}/layouts/notebook`
592
734
  );
593
735
  }
594
736
  await install(targets, installer);
@@ -605,14 +747,14 @@ async function customise(resolver, config) {
605
747
  printNext(...maps);
606
748
  }
607
749
  if (result.target === "home") {
608
- await install(["layouts/home"], installer);
750
+ await install([`${registry}/layouts/home`], installer);
609
751
  printNext(["fumadocs-ui/layouts/home", `@/components/layout/home`]);
610
752
  }
611
753
  outro3(picocolors2.bold("Have fun!"));
612
754
  }
613
755
  function printNext(...maps) {
614
756
  intro2(picocolors2.bold("What is Next?"));
615
- log3.info(
757
+ log4.info(
616
758
  [
617
759
  "You can check the installed components in `components`.",
618
760
  picocolors2.dim("---"),
@@ -634,17 +776,24 @@ program.name("fumadocs").description("CLI to setup Fumadocs, init a config").ver
634
776
  }
635
777
  });
636
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) => {
637
- const resolver = getResolverFromDir(options.dir);
638
- await customise(resolver, await createOrLoadConfig(options.config));
779
+ await customise(
780
+ createClientFromDir(
781
+ options.dir,
782
+ await createOrLoadConfig(options.config)
783
+ )
784
+ );
639
785
  });
640
786
  var dirShortcuts = {
641
- ":dev": "https://preview.fumadocs.dev/registry",
642
- ":localhost": "http://localhost:3000/registry"
787
+ ":preview": "https://preview.fumadocs.dev/registry",
788
+ ":dev": "http://localhost:3000/registry"
643
789
  };
644
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(
645
791
  async (input, options) => {
646
- const resolver = getResolverFromDir(options.dir);
647
- 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);
648
797
  }
649
798
  );
650
799
  program.command("tree").argument(
@@ -664,17 +813,17 @@ program.command("tree").argument(
664
813
  } catch {
665
814
  nodes = await runTree(str ?? "./");
666
815
  }
667
- 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);
668
817
  if (output) {
669
- await fs5.mkdir(path4.dirname(output), { recursive: true });
670
- await fs5.writeFile(output, out);
818
+ await fs6.mkdir(path5.dirname(output), { recursive: true });
819
+ await fs6.writeFile(output, out);
671
820
  } else {
672
821
  console.log(out);
673
822
  }
674
823
  }
675
824
  );
676
- function getResolverFromDir(dir = "https://fumadocs.dev/registry") {
825
+ function createClientFromDir(dir = "https://fumadocs.dev/registry", config) {
677
826
  if (dir in dirShortcuts) dir = dirShortcuts[dir];
678
- 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);
679
828
  }
680
829
  program.parse();