@fumadocs/cli 0.2.1 → 1.0.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,12 +1,97 @@
1
- import {
2
- exists
3
- } from "../chunk-DG6SFM2G.js";
4
-
5
1
  // src/build/index.ts
6
2
  import * as fs3 from "fs/promises";
7
3
  import * as path4 from "path";
8
4
  import picocolors from "picocolors";
9
5
 
6
+ // src/build/shadcn.ts
7
+ function mapDeps(deps) {
8
+ return Object.entries(deps).map(([k, v]) => {
9
+ if (v) return `${k}@${v}`;
10
+ return k;
11
+ });
12
+ }
13
+ function escapeName(name) {
14
+ return name;
15
+ }
16
+ function toShadcnRegistry(out, baseUrl) {
17
+ return {
18
+ homepage: baseUrl,
19
+ name: out.name,
20
+ items: out.components.map((comp) => componentToShadcn(comp, baseUrl))
21
+ };
22
+ }
23
+ function componentToShadcn(comp, baseUrl) {
24
+ return {
25
+ extends: "none",
26
+ type: "registry:block",
27
+ name: escapeName(comp.name),
28
+ title: comp.title ?? comp.name,
29
+ description: comp.description,
30
+ dependencies: mapDeps(comp.dependencies),
31
+ devDependencies: mapDeps(comp.devDependencies),
32
+ registryDependencies: comp.subComponents.map((comp2) => {
33
+ if (comp2.startsWith("https://") || comp2.startsWith("http://"))
34
+ return comp2;
35
+ return new URL(`/r/${escapeName(comp2)}.json`, baseUrl).toString();
36
+ }),
37
+ files: comp.files.map((file) => {
38
+ return {
39
+ type: {
40
+ components: "registry:component",
41
+ lib: "registry:lib",
42
+ css: "registry:style",
43
+ route: "registry:page",
44
+ ui: "registry:ui",
45
+ block: "registry:block"
46
+ }[file.type],
47
+ content: file.content,
48
+ path: file.path,
49
+ target: file.target
50
+ };
51
+ })
52
+ };
53
+ }
54
+
55
+ // src/build/validate.ts
56
+ function validateOutput(registry) {
57
+ function validateComponent(comp, ctx = {}) {
58
+ const { stack = /* @__PURE__ */ new Map() } = ctx;
59
+ for (const file of comp.files) {
60
+ const parents = stack.get(file.path);
61
+ if (parents) {
62
+ parents.add(comp.name);
63
+ } else {
64
+ stack.set(file.path, /* @__PURE__ */ new Set([comp.name]));
65
+ }
66
+ }
67
+ for (const name of comp.subComponents) {
68
+ const subComp = registry.components.find((item) => item.name === name);
69
+ if (!subComp) {
70
+ console.warn(`skipped component ${name}: not found`);
71
+ continue;
72
+ }
73
+ validateComponent(subComp, {
74
+ stack
75
+ });
76
+ }
77
+ for (const file of comp.files) {
78
+ const parents = stack.get(file.path);
79
+ if (!parents) continue;
80
+ if (parents.size <= 1) continue;
81
+ throw new Error(
82
+ `Duplicated file in same component ${Array.from(parents).join(", ")}: ${file.path}`
83
+ );
84
+ }
85
+ }
86
+ const compSet = /* @__PURE__ */ new Set();
87
+ for (const comp of registry.components) {
88
+ if (compSet.has(comp.name))
89
+ throw new Error(`duplicated component name ${comp.name}`);
90
+ compSet.add(comp.name);
91
+ validateComponent(comp);
92
+ }
93
+ }
94
+
10
95
  // src/build/build-registry.ts
11
96
  import * as fs2 from "fs/promises";
12
97
  import * as path3 from "path";
@@ -14,86 +99,51 @@ import * as path3 from "path";
14
99
  // src/build/build-file.ts
15
100
  import * as path from "path";
16
101
  import { ts } from "ts-morph";
17
- async function buildFile(inputPath, outputPath, builder, comp, onReference) {
18
- const out = {
19
- imports: {},
20
- content: "",
21
- path: outputPath
22
- };
23
- const importMap = {
24
- ...builder.registry.mapImportPath,
25
- ...comp.mapImportPath
26
- };
27
- function process2(specifier, getSpecifiedFile) {
28
- let specifiedFile = getSpecifiedFile();
29
- if (!specifiedFile) return;
30
- const name = specifiedFile.isInNodeModules() ? builder.resolveDep(specifier.getLiteralValue()).name : path.relative(builder.registryDir, specifiedFile.getFilePath());
31
- if (name in importMap) {
32
- const resolver = importMap[name];
33
- if (typeof resolver === "string") {
34
- specifier.setLiteralValue(resolver);
35
- specifiedFile = getSpecifiedFile();
36
- if (!specifiedFile) return;
37
- } else if (resolver.type === "dependency") {
38
- const info = builder.resolveDep(resolver.name);
39
- const value2 = onReference({
40
- type: "dependency",
41
- name: info.name,
42
- version: info.version ?? "",
43
- isDev: info.type === "dev"
44
- });
45
- if (value2) out.imports[specifier.getLiteralValue()] = value2;
46
- return;
47
- } else {
48
- const sub2 = builder.getComponentByName(
49
- resolver.name,
50
- resolver.registry
51
- );
52
- if (!sub2)
53
- throw new Error(`Failed to resolve sub component ${resolver.name}`);
54
- const value2 = onReference({
55
- type: "sub-component",
56
- resolved: sub2,
57
- targetFile: resolver.file
58
- });
59
- if (value2) out.imports[specifier.getLiteralValue()] = value2;
60
- return;
61
- }
102
+ async function buildFile(file, builder, comp, writeReference) {
103
+ const sourceFilePath = path.join(builder.registryDir, file.path);
104
+ const defaultResolve = (specifier, specified) => {
105
+ let filePath;
106
+ if (specified) {
107
+ filePath = specified.getFilePath();
108
+ } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
109
+ filePath = path.join(path.dirname(sourceFilePath), specifier);
110
+ } else {
111
+ throw new Error("Unknown specifier " + specifier);
62
112
  }
63
- if (specifiedFile.isInNodeModules() || specifiedFile.isDeclarationFile()) {
64
- const info = builder.resolveDep(specifier.getLiteralValue());
65
- const value2 = onReference({
113
+ if (path.relative(builder.registryDir, filePath).startsWith("../")) {
114
+ return {
66
115
  type: "dependency",
67
- name: info.name,
68
- version: info.version ?? "",
69
- isDev: info.type === "dev"
70
- });
71
- if (value2) out.imports[specifier.getLiteralValue()] = value2;
72
- return;
116
+ dep: builder.getDepFromSpecifier(specifier),
117
+ specifier
118
+ };
73
119
  }
74
- const sub = builder.getSubComponent(specifiedFile.getFilePath());
120
+ const sub = builder.getSubComponent(filePath);
75
121
  if (sub) {
76
- const value2 = onReference({
122
+ return {
77
123
  type: "sub-component",
78
124
  resolved: {
79
125
  type: "local",
80
- component: sub
81
- },
82
- targetFile: path.relative(
83
- builder.registryDir,
84
- specifiedFile.getFilePath()
85
- )
86
- });
87
- if (value2) out.imports[specifier.getLiteralValue()] = value2;
88
- return;
126
+ component: sub.component,
127
+ file: sub.file
128
+ }
129
+ };
89
130
  }
90
- const value = onReference({
131
+ return {
91
132
  type: "file",
92
- file: specifiedFile.getFilePath()
93
- });
94
- if (value) out.imports[specifier.getLiteralValue()] = value;
133
+ file: filePath
134
+ };
135
+ };
136
+ function process2(specifier, getSpecifiedFile) {
137
+ const onResolve = comp.onResolve ?? builder.registry.onResolve;
138
+ let resolved = defaultResolve(
139
+ specifier.getLiteralValue(),
140
+ getSpecifiedFile()
141
+ );
142
+ if (onResolve) resolved = onResolve(resolved);
143
+ const out = writeReference(resolved);
144
+ if (out) specifier.setLiteralValue(out);
95
145
  }
96
- const sourceFile = await builder.createSourceFile(inputPath);
146
+ const sourceFile = await builder.createSourceFile(sourceFilePath);
97
147
  for (const item of sourceFile.getImportDeclarations()) {
98
148
  process2(
99
149
  item.getModuleSpecifier(),
@@ -116,8 +166,12 @@ async function buildFile(inputPath, outputPath, builder, comp, onReference) {
116
166
  );
117
167
  }
118
168
  }
119
- out.content = sourceFile.getFullText();
120
- return out;
169
+ return {
170
+ content: sourceFile.getFullText(),
171
+ type: file.type,
172
+ path: file.path,
173
+ target: file.target
174
+ };
121
175
  }
122
176
 
123
177
  // src/build/component-builder.ts
@@ -125,46 +179,47 @@ import path2 from "path";
125
179
  import { Project } from "ts-morph";
126
180
  import * as fs from "fs/promises";
127
181
  function createComponentBuilder(registry, packageJson) {
128
- const rootDir = path2.join(registry.dir, registry.rootDir);
129
182
  const project = new Project({
130
- tsConfigFilePath: registry.tsconfigPath ? path2.join(rootDir, registry.tsconfigPath) : path2.join(rootDir, "tsconfig.json")
183
+ tsConfigFilePath: path2.join(registry.dir, registry.tsconfigPath)
131
184
  });
132
185
  const fileToComponent = /* @__PURE__ */ new Map();
133
186
  for (const comp of registry.components) {
134
187
  for (const file of comp.files) {
135
- const filePath = typeof file === "string" ? file : file.in;
136
- if (fileToComponent.has(filePath))
188
+ if (fileToComponent.has(file.path))
137
189
  console.warn(
138
- `the same file ${file} exists in multiple component, you should make the shared file a separate component.`
190
+ `the same file ${file.path} exists in multiple component, you should make the shared file a separate component.`
139
191
  );
140
- fileToComponent.set(filePath, comp);
192
+ fileToComponent.set(file.path, [comp, file]);
141
193
  }
142
194
  }
195
+ const deps = {
196
+ ...packageJson?.dependencies,
197
+ ...registry.dependencies
198
+ };
199
+ const devDeps = {
200
+ ...packageJson?.devDependencies,
201
+ ...registry.devDependencies
202
+ };
143
203
  return {
144
204
  registryDir: registry.dir,
145
205
  registry,
146
- resolveDep(specifier) {
147
- const name = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
148
- if (registry.dependencies && name in registry.dependencies)
206
+ getDepFromSpecifier(specifier) {
207
+ return specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
208
+ },
209
+ getDepInfo(name) {
210
+ if (name in deps)
149
211
  return {
150
- ...registry.dependencies[name],
151
- name
212
+ name,
213
+ type: "runtime",
214
+ version: deps[name]
152
215
  };
153
- if (packageJson && name in packageJson.devDependencies) {
216
+ if (name in devDeps)
154
217
  return {
218
+ name,
155
219
  type: "dev",
156
- version: packageJson.devDependencies[name],
157
- name
220
+ version: devDeps[name]
158
221
  };
159
- }
160
- if (packageJson && name in packageJson.dependencies) {
161
- return {
162
- type: "runtime",
163
- version: packageJson.dependencies[name],
164
- name
165
- };
166
- }
167
- return { type: "runtime", name };
222
+ console.warn(`dep info for ${name} cannot be found`);
168
223
  },
169
224
  async createSourceFile(file) {
170
225
  const content = await fs.readFile(file);
@@ -172,64 +227,17 @@ function createComponentBuilder(registry, packageJson) {
172
227
  overwrite: true
173
228
  });
174
229
  },
175
- getComponentByName(name, registryName) {
176
- if (registryName) {
177
- const child = registry.on[registryName];
178
- const comp2 = child.registry.components.find(
179
- (comp3) => comp3.name === name
180
- );
181
- if (comp2) {
182
- return {
183
- type: child.type,
184
- registryName,
185
- component: comp2
186
- };
187
- }
188
- return;
189
- }
190
- const comp = registry.components.find((comp2) => comp2.name === name);
191
- if (comp) {
192
- return {
193
- type: "local",
194
- registryName,
195
- component: comp
196
- };
197
- }
198
- },
199
- resolveOutputPath(file, registryName, namespace) {
200
- let targetRegistry = registry;
201
- if (registryName && registry.on[registryName].type === "local") {
202
- targetRegistry = registry.on[registryName].registry;
203
- }
204
- const parsed = file.split(":", 2);
205
- if (parsed.length > 1) {
206
- namespace ??= parsed[0];
207
- file = parsed[1];
208
- }
209
- if (!path2.isAbsolute(file)) {
210
- file = path2.join(targetRegistry.dir, file);
211
- }
212
- const rootDir2 = path2.join(targetRegistry.dir, targetRegistry.rootDir);
213
- if (namespace) {
214
- return `${namespace}:${path2.relative(rootDir2, file)}`;
215
- }
216
- if (targetRegistry.namespaces)
217
- for (const namespace2 in targetRegistry.namespaces) {
218
- const relativePath = path2.relative(
219
- path2.join(targetRegistry.dir, namespace2),
220
- file
221
- );
222
- if (!relativePath.startsWith("../")) {
223
- return `${targetRegistry.namespaces[namespace2]}:${relativePath}`;
224
- }
225
- }
226
- return path2.relative(rootDir2, file);
230
+ getComponentByName(name) {
231
+ return registry.components.find((comp) => comp.name === name);
227
232
  },
228
233
  getSubComponent(file) {
229
234
  const relativeFile = path2.relative(registry.dir, file);
230
235
  const comp = fileToComponent.get(relativeFile);
231
236
  if (!comp) return;
232
- return comp;
237
+ return {
238
+ component: comp[0],
239
+ file: comp[1]
240
+ };
233
241
  }
234
242
  };
235
243
  }
@@ -237,30 +245,16 @@ function createComponentBuilder(registry, packageJson) {
237
245
  // src/build/build-registry.ts
238
246
  async function build(registry) {
239
247
  const output = {
248
+ name: registry.name,
240
249
  index: [],
241
250
  components: []
242
251
  };
243
252
  function readPackageJson() {
244
- if (typeof registry.packageJson !== "string" && registry.packageJson)
245
- return registry.packageJson;
246
- return fs2.readFile(
247
- registry.packageJson ? path3.join(registry.dir, registry.packageJson) : path3.join(registry.dir, registry.rootDir, "package.json")
248
- ).then((res) => JSON.parse(res.toString())).catch(() => void 0);
253
+ if (typeof registry.packageJson !== "string") return registry.packageJson;
254
+ return fs2.readFile(path3.join(registry.dir, registry.packageJson)).then((res) => JSON.parse(res.toString())).catch(() => void 0);
249
255
  }
250
256
  const packageJson = await readPackageJson();
251
257
  const builder = createComponentBuilder(registry, packageJson);
252
- const buildExtendRegistries = Object.values(registry.on ?? {}).map(
253
- async (schema) => {
254
- if (schema.type === "remote") {
255
- return schema.registry;
256
- }
257
- return await build(schema.registry);
258
- }
259
- );
260
- for (const built of await Promise.all(buildExtendRegistries)) {
261
- output.components.push(...built.components);
262
- output.index.push(...built.index);
263
- }
264
258
  const builtComps = await Promise.all(
265
259
  registry.components.map((component) => buildComponent(component, builder))
266
260
  );
@@ -268,11 +262,13 @@ async function build(registry) {
268
262
  if (!input.unlisted) {
269
263
  output.index.push({
270
264
  name: input.name,
265
+ title: input.title,
271
266
  description: input.description
272
267
  });
273
268
  }
274
269
  output.components.push(comp);
275
270
  }
271
+ validateOutput(output);
276
272
  return output;
277
273
  }
278
274
  async function buildComponent(component, builder) {
@@ -280,70 +276,48 @@ async function buildComponent(component, builder) {
280
276
  const subComponents = /* @__PURE__ */ new Set();
281
277
  const devDependencies = /* @__PURE__ */ new Map();
282
278
  const dependencies = /* @__PURE__ */ new Map();
279
+ function toImportPath(file) {
280
+ let filePath = file.target ?? file.path;
281
+ if (filePath.startsWith("./")) filePath = filePath.slice(2);
282
+ return `@/${filePath.replaceAll(path3.sep, "/")}`;
283
+ }
283
284
  async function build2(file) {
284
- let inputPath;
285
- let outputPath;
286
- if (typeof file === "string") {
287
- let namespace;
288
- const parsed = file.split(":", 2);
289
- if (parsed.length > 1) {
290
- namespace = parsed[0];
291
- inputPath = path3.join(builder.registryDir, parsed[1]);
292
- } else {
293
- inputPath = path3.join(builder.registryDir, file);
294
- }
295
- outputPath = builder.resolveOutputPath(file, void 0, namespace);
296
- } else {
297
- inputPath = path3.join(builder.registryDir, file.in);
298
- outputPath = file.out;
299
- }
300
- if (processedFiles.has(inputPath)) return [];
301
- processedFiles.add(inputPath);
285
+ if (processedFiles.has(file.path)) return [];
286
+ processedFiles.add(file.path);
302
287
  const queue = [];
303
- const result = await buildFile(
304
- inputPath,
305
- outputPath,
306
- builder,
307
- component,
308
- (reference) => {
309
- if (reference.type === "file") {
310
- queue.push(path3.relative(builder.registryDir, reference.file));
311
- return builder.resolveOutputPath(reference.file);
288
+ const result = await buildFile(file, builder, component, (reference) => {
289
+ if (reference.type === "custom") return reference.specifier;
290
+ if (reference.type === "file") {
291
+ const refFile = builder.registry.onUnknownFile?.(reference.file);
292
+ if (refFile) {
293
+ queue.push(refFile);
294
+ return toImportPath(refFile);
312
295
  }
313
- if (reference.type === "sub-component") {
314
- const resolved = reference.resolved;
296
+ throw new Error(
297
+ `Unknown file ${reference.file} referenced by ${file.path}`
298
+ );
299
+ }
300
+ if (reference.type === "sub-component") {
301
+ const resolved = reference.resolved;
302
+ if (resolved.component.name !== component.name)
315
303
  subComponents.add(resolved.component.name);
316
- if (resolved.type === "remote") {
317
- return reference.targetFile;
318
- }
319
- for (const childFile of resolved.component.files) {
320
- if (typeof childFile === "string" && childFile === reference.targetFile) {
321
- return builder.resolveOutputPath(
322
- childFile,
323
- reference.resolved.registryName
324
- );
325
- }
326
- if (typeof childFile === "object" && childFile.in === reference.targetFile) {
327
- return childFile.out;
328
- }
329
- }
330
- throw new Error(
331
- `Failed to find sub component ${resolved.component.name}'s ${reference.targetFile} referenced by ${inputPath}`
332
- );
333
- }
334
- if (reference.type === "dependency") {
335
- if (reference.isDev)
336
- devDependencies.set(reference.name, reference.version);
337
- else dependencies.set(reference.name, reference.version);
338
- }
304
+ return toImportPath(resolved.file);
339
305
  }
340
- );
306
+ const dep = builder.getDepInfo(reference.dep);
307
+ if (dep) {
308
+ const map = dep.type === "dev" ? devDependencies : dependencies;
309
+ map.set(dep.name, dep.version);
310
+ }
311
+ return reference.specifier;
312
+ });
341
313
  return [result, ...(await Promise.all(queue.map(build2))).flat()];
342
314
  }
343
315
  return [
344
316
  component,
345
317
  {
346
318
  name: component.name,
319
+ title: component.title,
320
+ description: component.description,
347
321
  files: (await Promise.all(component.files.map(build2))).flat(),
348
322
  subComponents: Array.from(subComponents),
349
323
  dependencies: Object.fromEntries(dependencies),
@@ -353,40 +327,72 @@ async function buildComponent(component, builder) {
353
327
  }
354
328
 
355
329
  // src/build/index.ts
356
- async function writeOutput(dir, out, options = {}) {
357
- const { log = true } = options;
358
- if (options.cleanDir && await exists(dir)) {
330
+ function combineRegistry(...items) {
331
+ const out = {
332
+ index: [],
333
+ components: [],
334
+ name: items[0].name
335
+ };
336
+ for (const item of items) {
337
+ out.components.push(...item.components);
338
+ out.index.push(...item.index);
339
+ }
340
+ validateOutput(out);
341
+ return out;
342
+ }
343
+ async function writeShadcnRegistry(out, options) {
344
+ const { dir, cleanDir = false, baseUrl } = options;
345
+ if (cleanDir) {
359
346
  await fs3.rm(dir, {
360
- recursive: true
347
+ recursive: true,
348
+ force: true
361
349
  });
362
- if (log) {
363
- console.log(picocolors.bold(picocolors.greenBright("Cleaned directory")));
364
- }
350
+ console.log(picocolors.bold(picocolors.greenBright("Cleaned directory")));
365
351
  }
366
- async function writeFile2(file, content) {
367
- if (!log) return;
368
- await fs3.mkdir(path4.dirname(file), { recursive: true });
369
- await fs3.writeFile(file, content);
370
- const size = (Buffer.byteLength(content) / 1024).toFixed(2);
371
- console.log(
372
- `${picocolors.greenBright("+")} ${path4.relative(process.cwd(), file)} ${picocolors.dim(`${size.toString()} KB`)}`
373
- );
352
+ await Promise.all(
353
+ toShadcnRegistry(out, baseUrl).items.map(async (item) => {
354
+ const file = path4.join(dir, `${item.name}.json`);
355
+ await writeFile2(file, JSON.stringify(item, null, 2));
356
+ })
357
+ );
358
+ }
359
+ async function writeFumadocsRegistry(out, options) {
360
+ const { dir, cleanDir = false, log = true } = options;
361
+ if (cleanDir) {
362
+ await fs3.rm(dir, {
363
+ recursive: true,
364
+ force: true
365
+ });
366
+ console.log(picocolors.bold(picocolors.greenBright("Cleaned directory")));
374
367
  }
375
- const write = out.components.map(async (comp) => {
376
- const file = path4.join(dir, `${comp.name}.json`);
377
- const json = JSON.stringify(comp, null, 2);
378
- await writeFile2(file, json);
379
- });
380
368
  async function writeIndex() {
381
369
  const file = path4.join(dir, "_registry.json");
382
370
  const json = JSON.stringify(out.index, null, 2);
383
- await writeFile2(file, json);
371
+ await writeFile2(file, json, log);
384
372
  }
373
+ const write = out.components.map(async (comp) => {
374
+ const file = path4.join(dir, `${comp.name}.json`);
375
+ const json = JSON.stringify(comp, null, 2);
376
+ await writeFile2(file, json, log);
377
+ });
385
378
  write.push(writeIndex());
386
379
  await Promise.all(write);
387
380
  }
381
+ async function writeFile2(file, content, log = true) {
382
+ await fs3.mkdir(path4.dirname(file), { recursive: true });
383
+ await fs3.writeFile(file, content);
384
+ if (log) {
385
+ const size = (Buffer.byteLength(content) / 1024).toFixed(2);
386
+ console.log(
387
+ `${picocolors.greenBright("+")} ${path4.relative(process.cwd(), file)} ${picocolors.dim(`${size} KB`)}`
388
+ );
389
+ }
390
+ }
388
391
  export {
389
392
  build,
393
+ combineRegistry,
390
394
  createComponentBuilder,
391
- writeOutput
395
+ toShadcnRegistry,
396
+ writeFumadocsRegistry,
397
+ writeShadcnRegistry
392
398
  };