@seed-design/cli 0.0.3 → 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,10 +1,10 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import fs from "fs-extra";
3
3
  import path from "path";
4
- import color from "picocolors";
4
+ import { highlight } from "../utils/color";
5
5
  import { z } from "zod";
6
6
 
7
- import type { RawConfig } from "@/src/utils/get-config";
7
+ import type { Config } from "@/src/utils/get-config";
8
8
 
9
9
  import type { CAC } from "cac";
10
10
 
@@ -21,12 +21,11 @@ export const initCommand = (cli: CAC) => {
21
21
  })
22
22
  .option("-y, --yes", "모든 질문에 대해 기본값으로 답변합니다.")
23
23
  .action(async (opts) => {
24
- const highlight = (text: string) => color.cyan(text);
25
- p.intro(color.bgCyan("seed-design.json 파일 생성"));
24
+ p.intro("seed-design.json 파일 생성");
26
25
 
27
26
  const options = initOptionsSchema.parse(opts);
28
27
  const isYesOption = options.yes;
29
- let config: RawConfig = {
28
+ let config: Config = {
30
29
  rsc: false,
31
30
  tsx: true,
32
31
  path: "./seed-design",
@@ -73,14 +72,14 @@ export const initCommand = (cli: CAC) => {
73
72
  await fs.writeFile(targetPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
74
73
  const relativePath = path.relative(process.cwd(), targetPath);
75
74
  stop(`seed-design.json 파일이 ${highlight(relativePath)}에 생성됐어요.`);
76
- p.log.info(color.gray("seed-design add {component} 명령어로 컴포넌트를 추가해보세요!"));
75
+ p.log.info(highlight("seed-design add {component} 명령어로 컴포넌트를 추가해보세요!"));
77
76
  p.log.info(
78
- color.gray("seed-design add 명령어로 추가할 수 있는 모든 컴포넌트를 확인해보세요."),
77
+ highlight("seed-design add 명령어로 추가할 수 있는 모든 컴포넌트를 확인해보세요."),
79
78
  );
80
79
  p.outro("작업이 완료됐어요.");
81
80
  } catch (error) {
82
81
  p.log.error(`seed-design.json 파일 생성에 실패했어요. ${error}`);
83
- p.outro(color.bgRed("작업이 취소됐어요."));
82
+ p.outro(highlight("작업이 취소됐어요."));
84
83
  process.exit(1);
85
84
  }
86
85
  });
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { addCommand } from "@/src/commands/add";
4
+ import { addAllCommand } from "@/src/commands/add-all";
4
5
  import { initCommand } from "@/src/commands/init";
5
6
  import { getPackageInfo } from "@/src/utils/get-package-info";
6
7
  import { cac } from "cac";
@@ -13,6 +14,7 @@ async function main() {
13
14
 
14
15
  /* Commands */
15
16
  addCommand(CLI);
17
+ addAllCommand(CLI);
16
18
  initCommand(CLI);
17
19
 
18
20
  CLI.version(packageInfo.version || "1.0.0", "-v, --version");
package/src/schema.ts CHANGED
@@ -1,81 +1,55 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const registryType = z.union([z.literal("ui"), z.literal("lib")]);
4
-
5
- export const registryUIItemSchema = z.object({
6
- /**
7
- * @description 레지스트리 이름
8
- * @example chip-tabs, alert-dialog
9
- */
10
- name: z.string(),
3
+ /**
4
+ * this should be in sync with `docs/registry/schema.ts`
5
+ */
6
+ export const publicRegistryItemSchema = z.object({
7
+ id: z.string(),
11
8
 
12
9
  description: z.string().optional(),
13
10
 
14
- /**
15
- * @description 레지스트리 의존성
16
- * @example @seed-design/react-tabs
17
- */
18
- dependencies: z.array(z.string()).optional(),
11
+ deprecated: z.boolean().optional(),
19
12
 
20
- /**
21
- * @description 레지스트리 개발 의존성
22
- */
23
- devDependencies: z.array(z.string()).optional(),
13
+ hideFromCLICatalog: z.boolean().optional(),
14
+
15
+ ///////////////////////////////////////////////////////////////
16
+
17
+ dependencies: z.array(z.string()).optional(),
24
18
 
25
- /**
26
- * @description 레지스트리 내부의 Seed Design 컴포넌트 의존성
27
- * * `:`를 기준으로 왼쪽은 {registryType}, 오른쪽은 파일 이름
28
- * @example ui:action-button
29
- * @example lib:manner-temp-level
30
- */
31
- innerDependencies: z.array(z.string()).optional(),
19
+ innerDependencies: z
20
+ .array(
21
+ z.object({
22
+ registryId: z.string(),
23
+ itemIds: z.array(z.string()),
24
+ }),
25
+ )
26
+ .optional(),
32
27
 
33
- /**
34
- * @description
35
- * 컴포넌트 코드 스니펫 경로, 여러 파일이 될 수 있어서 배열로 정의
36
- * `:`를 기준으로 왼쪽은 {registryType}, 오른쪽은 파일 이름
37
- * @example ui:alert-dialog.tsx
38
- * @example ui:use-dismissible.ts
39
- * @example lib:manner-temp-level.ts
40
- */
41
- files: z.array(z.string()),
28
+ ///////////////////////////////////////////////////////////////
42
29
 
43
- /**
44
- * @description 컴포넌트 deprecated 여부
45
- */
46
- deprecated: z.literal(true).optional(),
30
+ snippets: z.array(z.object({ path: z.string(), content: z.string() })),
47
31
  });
48
- export const registryUISchema = z.array(registryUIItemSchema);
49
32
 
50
33
  /**
51
- * @description 머신이 생성한 registry component schema
34
+ * this should be in sync with `packages/cli/src/schema.ts`
52
35
  */
53
- const omittedRegistryUISchema = registryUIItemSchema.omit({
54
- files: true,
55
- });
56
- export const registryUIItemMachineGeneratedSchema = omittedRegistryUISchema.extend({
57
- registries: z.array(
58
- z.object({
59
- name: z.string(),
60
- type: registryType,
61
- content: z.string(),
62
- }),
36
+ export const publicRegistrySchema = z.object({
37
+ id: z.string(),
38
+
39
+ hideFromCLICatalog: z.boolean().optional(),
40
+
41
+ items: z.array(
42
+ publicRegistryItemSchema
43
+ .omit({ snippets: true })
44
+ .extend({ snippets: z.array(z.object({ path: z.string() })) }),
63
45
  ),
64
46
  });
65
- export const registryComponentMachineGeneratedSchema = z.array(
66
- registryUIItemMachineGeneratedSchema,
67
- );
68
47
 
69
- // NOTE: 현재는 lib이 ui와 타입이 동일하지만, 따로 가져가야한다면 타입을 변경해야해요.
70
- export const registryLibItemMachineGeneratedSchema = registryUIItemMachineGeneratedSchema;
71
-
72
- // NOTE: 현재는 lib이 ui와 타입이 동일하지만, 따로 가져가야한다면 타입을 변경해야해요.
73
- export type RegistryLibItem = z.infer<typeof registryUIItemSchema>;
74
- export type RegistryLib = z.infer<typeof registryUISchema>;
75
- export type RegistryUIItem = z.infer<typeof registryUIItemSchema>;
76
- export type RegistryUI = z.infer<typeof registryUISchema>;
48
+ /**
49
+ * this should be in sync with `packages/cli/src/schema.ts`
50
+ */
51
+ export const publicAvailableRegistriesSchema = z.array(z.object({ id: z.string() }));
77
52
 
78
- export type RegistryUIItemMachineGenerated = z.infer<typeof registryUIItemMachineGeneratedSchema>;
79
- export type RegistryUIMachineGenerated = z.infer<typeof registryComponentMachineGeneratedSchema>;
80
- export type RegistryLibItemMachineGenerated = z.infer<typeof registryLibItemMachineGeneratedSchema>;
81
- export type RegistryLibMachineGenerated = z.infer<typeof registryComponentMachineGeneratedSchema>;
53
+ export type PublicRegistryItem = z.infer<typeof publicRegistryItemSchema>;
54
+ export type PublicRegistry = z.infer<typeof publicRegistrySchema>;
55
+ export type PublicAvailableRegistries = z.infer<typeof publicAvailableRegistriesSchema>;
@@ -0,0 +1,424 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveDependencies } from "../utils/resolve-dependencies";
3
+ import type { PublicRegistry } from "@/src/schema";
4
+
5
+ describe("resolveDependencies", () => {
6
+ it("should resolve a simple item without dependencies", () => {
7
+ const publicRegistries: PublicRegistry[] = [
8
+ {
9
+ id: "ui",
10
+ items: [
11
+ {
12
+ id: "button",
13
+ description: "Button component",
14
+ snippets: [{ path: "button.tsx" }],
15
+ },
16
+ ],
17
+ },
18
+ ];
19
+
20
+ const result = resolveDependencies({
21
+ selectedItemKeys: ["ui:button"],
22
+ publicRegistries,
23
+ });
24
+
25
+ expect(result.registryItemsToAdd).toHaveLength(1);
26
+ expect(result.registryItemsToAdd[0]).toEqual({
27
+ registryId: "ui",
28
+ items: [
29
+ {
30
+ id: "button",
31
+ description: "Button component",
32
+ files: [{ path: "button.tsx" }],
33
+ },
34
+ ],
35
+ });
36
+ expect(result.npmDependenciesToAdd.size).toBe(0);
37
+ });
38
+
39
+ it("should resolve npm dependencies", () => {
40
+ const publicRegistries: PublicRegistry[] = [
41
+ {
42
+ id: "ui",
43
+ items: [
44
+ {
45
+ id: "tabs",
46
+ description: "Tabs component",
47
+ snippets: [{ path: "tabs.tsx" }],
48
+ dependencies: ["@seed-design/react-tabs", "clsx"],
49
+ },
50
+ ],
51
+ },
52
+ ];
53
+
54
+ const result = resolveDependencies({
55
+ selectedItemKeys: ["ui:tabs"],
56
+ publicRegistries,
57
+ });
58
+
59
+ expect(result.npmDependenciesToAdd.size).toBe(2);
60
+ expect(result.npmDependenciesToAdd.has("@seed-design/react-tabs")).toBe(true);
61
+ expect(result.npmDependenciesToAdd.has("clsx")).toBe(true);
62
+ });
63
+
64
+ it("should resolve inner dependencies recursively", () => {
65
+ const publicRegistries: PublicRegistry[] = [
66
+ {
67
+ id: "ui",
68
+ items: [
69
+ {
70
+ id: "dialog",
71
+ description: "Dialog component",
72
+ snippets: [{ path: "dialog.tsx" }],
73
+ innerDependencies: [
74
+ {
75
+ registryId: "breeze",
76
+ itemIds: ["animate-number"],
77
+ },
78
+ ],
79
+ },
80
+ ],
81
+ },
82
+ {
83
+ id: "breeze",
84
+ items: [
85
+ {
86
+ id: "animate-number",
87
+ description: "Animate number utility",
88
+ snippets: [{ path: "animate-number.ts" }],
89
+ dependencies: ["framer-motion"],
90
+ },
91
+ ],
92
+ },
93
+ ];
94
+
95
+ const result = resolveDependencies({
96
+ selectedItemKeys: ["ui:dialog"],
97
+ publicRegistries,
98
+ });
99
+
100
+ expect(result.registryItemsToAdd).toHaveLength(2);
101
+ expect(result.registryItemsToAdd).toEqual([
102
+ {
103
+ registryId: "ui",
104
+ items: [
105
+ {
106
+ id: "dialog",
107
+ description: "Dialog component",
108
+ files: [{ path: "dialog.tsx" }],
109
+ innerDependencies: [
110
+ {
111
+ registryId: "breeze",
112
+ itemIds: ["animate-number"],
113
+ },
114
+ ],
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ registryId: "breeze",
120
+ items: [
121
+ {
122
+ id: "animate-number",
123
+ description: "Animate number utility",
124
+ files: [{ path: "animate-number.ts" }],
125
+ dependencies: ["framer-motion"],
126
+ },
127
+ ],
128
+ },
129
+ ]);
130
+ expect(result.npmDependenciesToAdd.size).toBe(1);
131
+ expect(result.npmDependenciesToAdd.has("framer-motion")).toBe(true);
132
+ });
133
+
134
+ it("should handle multiple selected items", () => {
135
+ const publicRegistries: PublicRegistry[] = [
136
+ {
137
+ id: "ui",
138
+ items: [
139
+ {
140
+ id: "button",
141
+ description: "Button component",
142
+ snippets: [{ path: "button.tsx" }],
143
+ },
144
+ {
145
+ id: "chip",
146
+ description: "Chip component",
147
+ snippets: [{ path: "chip.tsx" }],
148
+ },
149
+ ],
150
+ },
151
+ ];
152
+
153
+ const result = resolveDependencies({
154
+ selectedItemKeys: ["ui:button", "ui:chip"],
155
+ publicRegistries,
156
+ });
157
+
158
+ expect(result.registryItemsToAdd).toHaveLength(1);
159
+ expect(result.registryItemsToAdd[0].registryId).toBe("ui");
160
+ expect(result.registryItemsToAdd[0].items).toHaveLength(2);
161
+ expect(result.registryItemsToAdd[0].items.map((i) => i.id)).toEqual(["button", "chip"]);
162
+ });
163
+
164
+ it("should prevent duplicate items", () => {
165
+ const publicRegistries: PublicRegistry[] = [
166
+ {
167
+ id: "ui",
168
+ items: [
169
+ {
170
+ id: "dialog",
171
+ description: "Dialog component",
172
+ snippets: [{ path: "dialog.tsx" }],
173
+ innerDependencies: [
174
+ {
175
+ registryId: "ui",
176
+ itemIds: ["button"],
177
+ },
178
+ ],
179
+ },
180
+ {
181
+ id: "button",
182
+ description: "Button component",
183
+ snippets: [{ path: "button.tsx" }],
184
+ },
185
+ ],
186
+ },
187
+ ];
188
+
189
+ // Select both dialog and button, but button is already a dependency of dialog
190
+ const result = resolveDependencies({
191
+ selectedItemKeys: ["ui:dialog", "ui:button"],
192
+ publicRegistries,
193
+ });
194
+
195
+ expect(result.registryItemsToAdd).toHaveLength(1);
196
+ expect(result.registryItemsToAdd[0].items).toHaveLength(2);
197
+ // Button should appear only once
198
+ const buttonCount = result.registryItemsToAdd[0].items.filter((i) => i.id === "button").length;
199
+ expect(buttonCount).toBe(1);
200
+ });
201
+
202
+ it("should handle nested inner dependencies", () => {
203
+ const publicRegistries: PublicRegistry[] = [
204
+ {
205
+ id: "ui",
206
+ items: [
207
+ {
208
+ id: "complex",
209
+ description: "Complex component",
210
+ snippets: [{ path: "complex.tsx" }],
211
+ innerDependencies: [
212
+ {
213
+ registryId: "ui",
214
+ itemIds: ["dialog"],
215
+ },
216
+ ],
217
+ },
218
+ {
219
+ id: "dialog",
220
+ description: "Dialog component",
221
+ snippets: [{ path: "dialog.tsx" }],
222
+ innerDependencies: [
223
+ {
224
+ registryId: "breeze",
225
+ itemIds: ["animate"],
226
+ },
227
+ ],
228
+ },
229
+ ],
230
+ },
231
+ {
232
+ id: "breeze",
233
+ items: [
234
+ {
235
+ id: "animate",
236
+ description: "Animate utility",
237
+ snippets: [{ path: "animate.ts" }],
238
+ innerDependencies: [
239
+ {
240
+ registryId: "lib",
241
+ itemIds: ["utils"],
242
+ },
243
+ ],
244
+ },
245
+ ],
246
+ },
247
+ {
248
+ id: "lib",
249
+ items: [
250
+ {
251
+ id: "utils",
252
+ description: "Utility functions",
253
+ snippets: [{ path: "utils.ts" }],
254
+ dependencies: ["lodash"],
255
+ },
256
+ ],
257
+ },
258
+ ];
259
+
260
+ const result = resolveDependencies({
261
+ selectedItemKeys: ["ui:complex"],
262
+ publicRegistries,
263
+ });
264
+
265
+ expect(result.registryItemsToAdd).toHaveLength(3);
266
+ expect(result.registryItemsToAdd.map((r) => r.registryId)).toEqual(["ui", "breeze", "lib"]);
267
+ expect(result.npmDependenciesToAdd.size).toBe(1);
268
+ expect(result.npmDependenciesToAdd.has("lodash")).toBe(true);
269
+ });
270
+
271
+ it("should throw error for invalid snippet format", () => {
272
+ const publicRegistries: PublicRegistry[] = [];
273
+
274
+ expect(() =>
275
+ resolveDependencies({
276
+ selectedItemKeys: ["invalid-format"],
277
+ publicRegistries,
278
+ }),
279
+ ).toThrowError('Invalid snippet format: "invalid-format"');
280
+ });
281
+
282
+ it("should throw error for non-existent snippet", () => {
283
+ const publicRegistries: PublicRegistry[] = [
284
+ {
285
+ id: "ui",
286
+ items: [],
287
+ },
288
+ ];
289
+
290
+ expect(() =>
291
+ resolveDependencies({
292
+ selectedItemKeys: ["ui:non-existent"],
293
+ publicRegistries,
294
+ }),
295
+ ).toThrowError('Cannot find snippet: "ui:non-existent"');
296
+ });
297
+
298
+ it("should throw error for missing inner dependency", () => {
299
+ const publicRegistries: PublicRegistry[] = [
300
+ {
301
+ id: "ui",
302
+ items: [
303
+ {
304
+ id: "broken",
305
+ description: "Broken component",
306
+ snippets: [{ path: "broken.tsx" }],
307
+ innerDependencies: [
308
+ {
309
+ registryId: "breeze",
310
+ itemIds: ["missing"],
311
+ },
312
+ ],
313
+ },
314
+ ],
315
+ },
316
+ {
317
+ id: "breeze",
318
+ items: [],
319
+ },
320
+ ];
321
+
322
+ expect(() =>
323
+ resolveDependencies({
324
+ selectedItemKeys: ["ui:broken"],
325
+ publicRegistries,
326
+ }),
327
+ ).toThrowError("Cannot find dependency item: breeze:missing");
328
+ });
329
+
330
+ it("should handle multiple registries with multiple items", () => {
331
+ const publicRegistries: PublicRegistry[] = [
332
+ {
333
+ id: "ui",
334
+ items: [
335
+ {
336
+ id: "button",
337
+ description: "Button component",
338
+ snippets: [{ path: "button.tsx" }],
339
+ dependencies: ["clsx"],
340
+ },
341
+ {
342
+ id: "chip",
343
+ description: "Chip component",
344
+ snippets: [{ path: "chip.tsx" }],
345
+ },
346
+ ],
347
+ },
348
+ {
349
+ id: "breeze",
350
+ items: [
351
+ {
352
+ id: "animate",
353
+ description: "Animate utility",
354
+ snippets: [{ path: "animate.ts" }],
355
+ dependencies: ["framer-motion"],
356
+ },
357
+ ],
358
+ },
359
+ ];
360
+
361
+ const result = resolveDependencies({
362
+ selectedItemKeys: ["ui:button", "breeze:animate", "ui:chip"],
363
+ publicRegistries,
364
+ });
365
+
366
+ expect(result.registryItemsToAdd).toHaveLength(2);
367
+
368
+ const uiRegistry = result.registryItemsToAdd.find((r) => r.registryId === "ui");
369
+ expect(uiRegistry?.items).toHaveLength(2);
370
+ expect(uiRegistry?.items.map((i) => i.id)).toEqual(["button", "chip"]);
371
+
372
+ const breezeRegistry = result.registryItemsToAdd.find((r) => r.registryId === "breeze");
373
+ expect(breezeRegistry?.items).toHaveLength(1);
374
+ expect(breezeRegistry?.items[0].id).toBe("animate");
375
+
376
+ expect(result.npmDependenciesToAdd.size).toBe(2);
377
+ expect(result.npmDependenciesToAdd.has("clsx")).toBe(true);
378
+ expect(result.npmDependenciesToAdd.has("framer-motion")).toBe(true);
379
+ });
380
+
381
+ it("should collect all npm dependencies from nested dependencies", () => {
382
+ const publicRegistries: PublicRegistry[] = [
383
+ {
384
+ id: "ui",
385
+ items: [
386
+ {
387
+ id: "rich",
388
+ description: "Rich component",
389
+ snippets: [{ path: "rich.tsx" }],
390
+ dependencies: ["react-hook-form"],
391
+ innerDependencies: [
392
+ {
393
+ registryId: "ui",
394
+ itemIds: ["field", "label"],
395
+ },
396
+ ],
397
+ },
398
+ {
399
+ id: "field",
400
+ description: "Field component",
401
+ snippets: [{ path: "field.tsx" }],
402
+ dependencies: ["clsx", "tailwind-merge"],
403
+ },
404
+ {
405
+ id: "label",
406
+ description: "Label component",
407
+ snippets: [{ path: "label.tsx" }],
408
+ dependencies: ["clsx"],
409
+ },
410
+ ],
411
+ },
412
+ ];
413
+
414
+ const result = resolveDependencies({
415
+ selectedItemKeys: ["ui:rich"],
416
+ publicRegistries,
417
+ });
418
+
419
+ expect(result.npmDependenciesToAdd.size).toBe(3);
420
+ expect(result.npmDependenciesToAdd.has("react-hook-form")).toBe(true);
421
+ expect(result.npmDependenciesToAdd.has("clsx")).toBe(true);
422
+ expect(result.npmDependenciesToAdd.has("tailwind-merge")).toBe(true);
423
+ });
424
+ });