@fumadocs/cli 0.2.1 → 1.0.1

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