@seed-design/cli 0.0.3 → 1.1.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,8 +1,6 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { cosmiconfig } from "cosmiconfig";
3
3
  import { execa } from "execa";
4
- import fs from "fs";
5
- import path from "path";
6
4
  import { z } from "zod";
7
5
  import { highlight } from "./color";
8
6
  import { getPackageManager } from "./get-package-manager";
@@ -13,74 +11,43 @@ const explorer = cosmiconfig(MODULE_NAME, {
13
11
  searchPlaces: [`${MODULE_NAME}.json`],
14
12
  });
15
13
 
16
- export const rawConfigSchema = z
14
+ export const configSchema = z
17
15
  .object({
18
16
  $schema: z.string().optional(),
19
17
  rsc: z.coerce.boolean().default(false),
20
18
  tsx: z.coerce.boolean().default(true),
21
19
  path: z.string(),
20
+ telemetry: z.coerce.boolean().optional().default(true),
22
21
  })
23
22
  .strict();
24
23
 
25
- export type RawConfig = z.infer<typeof rawConfigSchema>;
26
-
27
- export const configSchema = rawConfigSchema.extend({
28
- resolvedUIPaths: z.string(),
29
- resolvedLibPaths: z.string(),
30
- });
24
+ export type Config = z.infer<typeof configSchema>;
31
25
 
32
26
  export async function getConfig(cwd: string) {
33
27
  const config = await getRawConfig(cwd);
28
+ if (!config) return null;
34
29
 
35
- if (!config) {
36
- return null;
37
- }
38
-
39
- return await resolveConfigPaths(cwd, config);
30
+ return configSchema.parse(config);
40
31
  }
41
32
 
42
- export type Config = z.infer<typeof configSchema>;
43
-
44
- export async function resolveConfigPaths(cwd: string, config: RawConfig) {
45
- const seedComponentRootPath = path.resolve(cwd, config.path);
46
-
47
- if (!fs.existsSync(seedComponentRootPath)) {
48
- fs.mkdirSync(seedComponentRootPath, { recursive: true });
49
- }
50
-
51
- const resolvedUIPaths = path.join(seedComponentRootPath, "ui");
52
- const resolvedLibPaths = path.join(seedComponentRootPath, "lib");
53
-
54
- if (!fs.existsSync(resolvedUIPaths)) {
55
- fs.mkdirSync(resolvedUIPaths, { recursive: true });
56
- }
57
-
58
- if (!fs.existsSync(resolvedLibPaths)) {
59
- fs.mkdirSync(resolvedLibPaths, { recursive: true });
60
- }
61
-
62
- return configSchema.parse({
63
- ...config,
64
- resolvedUIPaths,
65
- resolvedLibPaths,
66
- });
67
- }
68
-
69
- export async function getRawConfig(cwd: string): Promise<RawConfig | null> {
33
+ export async function getRawConfig(cwd: string): Promise<Config | null> {
70
34
  try {
71
35
  const configResult = await explorer.search(cwd);
72
- return rawConfigSchema.parse(configResult.config);
36
+ return configSchema.parse(configResult.config);
73
37
  } catch {
74
38
  p.log.error("프로젝트 루트 경로에 `seed-design.json` 파일이 없어요.");
75
39
 
76
40
  const isConfirm = await p.confirm({ message: "seed-design.json 파일을 생성하시겠어요?" });
77
- if (isConfirm === true) {
78
- const packageManager = await getPackageManager(cwd);
79
- await execa(packageManager, ["seed-design", "init", "--default"], { cwd });
80
- p.log.message("seed-design.json 파일이 생성됐어요.");
81
- } else {
41
+
42
+ if (!isConfirm) {
82
43
  p.outro(highlight("작업이 취소됐어요."));
83
44
  process.exit(1);
84
45
  }
46
+
47
+ const packageManager = await getPackageManager(cwd);
48
+
49
+ await execa(packageManager, ["seed-design", "init", "--default"], { cwd });
50
+
51
+ p.log.message("seed-design.json 파일이 생성됐어요.");
85
52
  }
86
53
  }
@@ -1,6 +1,5 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { execa } from "execa";
3
- import color from "picocolors";
4
3
  import { getPackageManager } from "./get-package-manager";
5
4
  import { getPackageInfo } from "./get-package-info";
6
5
 
@@ -18,20 +17,16 @@ export async function installDependencies({ cwd, deps, dev = false }: InstallDep
18
17
  // 이미 설치된 의존성 필터링
19
18
  const existingDeps = {
20
19
  ...packageInfo.dependencies,
21
- ...packageInfo.devDependencies,
20
+ // ...packageInfo.devDependencies,
21
+ // commented out because stated dependencies should be installed as actual dependencies even though they are listed in devDependencies
22
22
  };
23
23
 
24
- const depsToInstall = deps.filter((dep) => !existingDeps[dep]);
25
- const filteredDeps = deps.filter((dep) => existingDeps[dep]);
24
+ const depsToInstall = new Set(deps.filter((dep) => !existingDeps[dep]));
25
+ const filteredDeps = new Set(deps.filter((dep) => existingDeps[dep]));
26
26
 
27
- if (!depsToInstall.length) {
28
- return {
29
- installed: new Set(),
30
- filtered: new Set(),
31
- };
32
- }
27
+ if (!depsToInstall.size) return { installed: new Set<string>(), filtered: depsToInstall };
33
28
 
34
- start(color.gray("의존성 설치중..."));
29
+ start("의존성 설치중...");
35
30
 
36
31
  const isDev = dev ? "-D" : null;
37
32
  const addCommand = packageManager === "npm" ? "install" : "add";
@@ -44,7 +39,7 @@ export async function installDependencies({ cwd, deps, dev = false }: InstallDep
44
39
  process.exit(1);
45
40
  }
46
41
 
47
- stop("의존성 설치 완료.");
42
+ stop("의존성 설치가 완료됐어요.");
48
43
 
49
44
  return {
50
45
  installed: depsToInstall,
@@ -0,0 +1,77 @@
1
+ import type { PublicRegistry, PublicRegistryItem } from "@/src/schema";
2
+
3
+ export function resolveDependencies({
4
+ selectedItemKeys,
5
+ publicRegistries,
6
+ }: {
7
+ /**
8
+ * @example ["breeze:animate-number", "ui:action-button"]
9
+ */
10
+ selectedItemKeys: string[];
11
+ publicRegistries: PublicRegistry[];
12
+ }) {
13
+ const registryItemsToAdd: { registryId: string; items: PublicRegistry["items"] }[] = [];
14
+ const npmDependenciesToAdd = new Set<string>();
15
+
16
+ function collectRegistryItemsToAdd(registryId: string, item: PublicRegistryItem) {
17
+ const registryFoundToAdd = registryItemsToAdd.find((r) => r.registryId === registryId);
18
+
19
+ // if already added, skip
20
+ if (registryFoundToAdd?.items.some((i) => i.id === item.id)) return;
21
+
22
+ // Add the item to the list
23
+ if (registryFoundToAdd) {
24
+ registryFoundToAdd.items.push(item);
25
+ } else {
26
+ registryItemsToAdd.push({ registryId, items: [item] });
27
+ }
28
+
29
+ // process dependencies
30
+ if (item.dependencies?.length) {
31
+ for (const dep of item.dependencies) {
32
+ npmDependenciesToAdd.add(dep);
33
+ }
34
+ }
35
+
36
+ // process innerDependencies
37
+ if (item.innerDependencies?.length) {
38
+ for (const dependency of item.innerDependencies) {
39
+ for (const depItemId of dependency.itemIds) {
40
+ const depItem = publicRegistries
41
+ .find((r) => r.id === dependency.registryId)
42
+ ?.items.find((i) => i.id === depItemId);
43
+
44
+ // should not happen
45
+ if (!depItem)
46
+ throw new Error(`Cannot find dependency item: ${dependency.registryId}:${depItemId}`);
47
+
48
+ collectRegistryItemsToAdd(dependency.registryId, depItem);
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ for (const item of selectedItemKeys) {
55
+ const [registryId, ...rest] = item.split(":");
56
+ const itemId = rest.join(":");
57
+
58
+ if (!registryId || !itemId) {
59
+ throw new Error(`Invalid snippet format: "${item}"`);
60
+ }
61
+
62
+ const foundItem = publicRegistries
63
+ .find((r) => r.id === registryId)
64
+ ?.items.find((i) => i.id === itemId);
65
+
66
+ if (!foundItem) {
67
+ throw new Error(`Cannot find snippet: "${item}"`);
68
+ }
69
+
70
+ collectRegistryItemsToAdd(registryId, foundItem);
71
+ }
72
+
73
+ return {
74
+ registryItemsToAdd,
75
+ npmDependenciesToAdd,
76
+ };
77
+ }
@@ -29,7 +29,8 @@ const project = new Project({
29
29
  });
30
30
 
31
31
  async function createTempSourceFile(filename: string) {
32
- const dir = await fs.mkdtemp(path.join(tmpdir(), "seed-deisgn-"));
32
+ const dir = await fs.mkdtemp(path.join(tmpdir(), "seed-design-"));
33
+
33
34
  return path.join(dir, filename);
34
35
  }
35
36
 
@@ -1,4 +1,3 @@
1
- // @ts-ignore
2
1
  import { transformFromAstSync } from "@babel/core";
3
2
  import transformTypescript from "@babel/plugin-transform-typescript";
4
3
  import * as recast from "recast";
@@ -8,10 +8,27 @@ export const transformRsc: Transformer = async ({ sourceFile, config }) => {
8
8
  }
9
9
 
10
10
  // Remove "use client" from the top of the file.
11
- const first = sourceFile.getFirstChildByKind(SyntaxKind.ExpressionStatement);
12
- if (first?.getText() === `"use client";`) {
13
- first.remove();
14
- }
11
+ // We need to be careful to only remove the directive itself, not any JSDoc comments
12
+
13
+ const firstExpressionStatement = sourceFile.getFirstChildByKind(SyntaxKind.ExpressionStatement);
14
+ if (!firstExpressionStatement) return sourceFile;
15
+
16
+ const expression = firstExpressionStatement.getExpression();
17
+ if (!expression) return sourceFile;
18
+
19
+ const expressionText = expression.getText().trim();
20
+
21
+ if (expressionText !== `"use client"` && expressionText !== `'use client'`) return sourceFile;
22
+
23
+ const expressionStatementText = firstExpressionStatement.getText();
24
+ const expressionStatementFullText = firstExpressionStatement.getFullText();
25
+
26
+ if (expressionStatementText.trim() === expressionStatementFullText.trim()) return sourceFile;
27
+
28
+ const triviaOnly = expressionStatementFullText.replace(expressionStatementText, "");
29
+ const cleanedTriviaOnly = triviaOnly.replace(/^\s*\n/, "").replace(/\n\s*$/, "");
30
+
31
+ firstExpressionStatement.replaceWithText(cleanedTriviaOnly);
15
32
 
16
33
  return sourceFile;
17
34
  };
@@ -0,0 +1,75 @@
1
+ import { fetchRegistryItems } from "@/src/utils/fetch";
2
+ import { transform } from "@/src/utils/transformers";
3
+ import * as p from "@clack/prompts";
4
+ import fs from "fs-extra";
5
+ import path from "path";
6
+ import { highlight } from "./color";
7
+ import type { Config } from "@/src/utils/get-config";
8
+ import type { PublicRegistry } from "@/src/schema";
9
+
10
+ export async function writeRegistryItemSnippets({
11
+ registryItemsToAdd,
12
+ rootPath,
13
+ cwd,
14
+ baseUrl,
15
+ config,
16
+ }: {
17
+ registryItemsToAdd: { registryId: string; items: PublicRegistry["items"] }[];
18
+ rootPath: string;
19
+ cwd: string;
20
+ baseUrl: string;
21
+ config: Config;
22
+ }) {
23
+ const registryResult: { name: string; path: string }[] = [];
24
+
25
+ for (const { registryId, items } of registryItemsToAdd) {
26
+ const registryPath = path.join(rootPath, registryId);
27
+
28
+ fs.ensureDirSync(registryPath);
29
+
30
+ const registryItems = await fetchRegistryItems({
31
+ baseUrl,
32
+ registryId,
33
+ registryItemIds: items.map((i) => i.id),
34
+ });
35
+
36
+ for (const { id, snippets } of registryItems) {
37
+ const transformedSnippets = await Promise.all(
38
+ snippets.map(async (file) => {
39
+ const content = await transform({ filename: file.path, config, raw: file.content });
40
+
41
+ let filePath = path.join(registryPath, file.path);
42
+ if (!config.tsx) {
43
+ filePath = filePath.replace(/\.tsx$/, ".jsx");
44
+ filePath = filePath.replace(/\.ts$/, ".js");
45
+ }
46
+
47
+ return {
48
+ filePath,
49
+ content,
50
+ relativePath: path.relative(cwd, filePath),
51
+ name: `${registryId}:${id}`,
52
+ };
53
+ }),
54
+ );
55
+
56
+ await Promise.all(
57
+ transformedSnippets.map(async ({ filePath, content }) => {
58
+ await fs.ensureDir(path.dirname(filePath));
59
+ await fs.writeFile(filePath, content);
60
+ }),
61
+ );
62
+
63
+ const snippetResults = transformedSnippets.map(({ name, relativePath }) => ({
64
+ name,
65
+ path: relativePath,
66
+ }));
67
+
68
+ registryResult.push(...snippetResults);
69
+
70
+ p.log.success(
71
+ `${highlight(`${registryId}:${id}`)} 관련 스니펫 다운로드 완료: ${highlight(snippetResults.map((r) => r.path).join(", "))}`,
72
+ );
73
+ }
74
+ }
75
+ }
@@ -1,182 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { addRelativeRegistries } from "../utils/add-relative-registries";
3
- import type { RegistryLib, RegistryUI } from "@/src/schema";
4
-
5
- const libConfig: RegistryLib = [
6
- {
7
- name: "a",
8
- files: ["a.tsx"],
9
- },
10
- ];
11
-
12
- const uiConfig: RegistryUI = [
13
- {
14
- name: "a",
15
- files: ["a.tsx"],
16
- },
17
- {
18
- name: "b",
19
- innerDependencies: ["ui:a"],
20
- files: ["b.tsx"],
21
- },
22
- {
23
- name: "c",
24
- innerDependencies: ["ui:b"],
25
- files: ["c.tsx"],
26
- },
27
- {
28
- name: "d",
29
- innerDependencies: ["ui:a", "ui:b"],
30
- files: ["d.tsx"],
31
- },
32
- {
33
- name: "e",
34
- innerDependencies: ["ui:d"],
35
- files: ["e.tsx"],
36
- },
37
- {
38
- name: "f",
39
- innerDependencies: ["lib:a"],
40
- files: ["f.tsx"],
41
- },
42
- ];
43
-
44
- describe("addRelativeRegistries", () => {
45
- test("4 deps test", () => {
46
- const userSelects = ["e"];
47
- const result = addRelativeRegistries({
48
- userSelects,
49
- uiRegistryIndex: uiConfig,
50
- libRegistryIndex: [],
51
- });
52
- expect(result).toEqual(
53
- expect.arrayContaining([
54
- {
55
- type: "ui",
56
- name: "e",
57
- },
58
- {
59
- type: "ui",
60
- name: "d",
61
- },
62
- {
63
- type: "ui",
64
- name: "a",
65
- },
66
- {
67
- type: "ui",
68
- name: "b",
69
- },
70
- ]),
71
- );
72
- });
73
-
74
- test("3 deps test", () => {
75
- const userSelects = ["d"];
76
- const result = addRelativeRegistries({
77
- userSelects,
78
- uiRegistryIndex: uiConfig,
79
- libRegistryIndex: [],
80
- });
81
- expect(result).toEqual(
82
- expect.arrayContaining([
83
- {
84
- type: "ui",
85
- name: "d",
86
- },
87
- {
88
- type: "ui",
89
- name: "a",
90
- },
91
- {
92
- type: "ui",
93
- name: "b",
94
- },
95
- ]),
96
- );
97
- });
98
-
99
- test("3 deps test", () => {
100
- const userSelects = ["c"];
101
- const result = addRelativeRegistries({
102
- userSelects,
103
- uiRegistryIndex: uiConfig,
104
- libRegistryIndex: [],
105
- });
106
- expect(result).toEqual(
107
- expect.arrayContaining([
108
- {
109
- type: "ui",
110
- name: "c",
111
- },
112
- {
113
- type: "ui",
114
- name: "b",
115
- },
116
- {
117
- type: "ui",
118
- name: "a",
119
- },
120
- ]),
121
- );
122
- });
123
-
124
- test("2 deps test", () => {
125
- const userSelects = ["b"];
126
- const result = addRelativeRegistries({
127
- userSelects,
128
- uiRegistryIndex: uiConfig,
129
- libRegistryIndex: [],
130
- });
131
- expect(result).toEqual(
132
- expect.arrayContaining([
133
- {
134
- type: "ui",
135
- name: "b",
136
- },
137
- {
138
- type: "ui",
139
- name: "a",
140
- },
141
- ]),
142
- );
143
- });
144
-
145
- test("1 deps test", () => {
146
- const userSelects = ["a"];
147
- const result = addRelativeRegistries({
148
- userSelects,
149
- uiRegistryIndex: uiConfig,
150
- libRegistryIndex: [],
151
- });
152
- expect(result).toEqual(
153
- expect.arrayContaining([
154
- {
155
- type: "ui",
156
- name: "a",
157
- },
158
- ]),
159
- );
160
- });
161
-
162
- test("lib deps test", () => {
163
- const userSelects = ["f"];
164
- const result = addRelativeRegistries({
165
- userSelects,
166
- uiRegistryIndex: uiConfig,
167
- libRegistryIndex: libConfig,
168
- });
169
- expect(result).toEqual(
170
- expect.arrayContaining([
171
- {
172
- type: "ui",
173
- name: "f",
174
- },
175
- {
176
- type: "lib",
177
- name: "a",
178
- },
179
- ]),
180
- );
181
- });
182
- });
@@ -1,43 +0,0 @@
1
- import type { RegistryLibMachineGenerated, RegistryUIMachineGenerated } from "@/src/schema";
2
-
3
- interface AddRelativeComponentsProps {
4
- userSelects: string[];
5
- uiRegistryIndex: RegistryUIMachineGenerated;
6
- libRegistryIndex: RegistryLibMachineGenerated;
7
- }
8
-
9
- export function addRelativeRegistries({
10
- userSelects,
11
- uiRegistryIndex,
12
- libRegistryIndex,
13
- }: AddRelativeComponentsProps) {
14
- const selectedComponents: { type: "ui" | "lib"; name: string }[] = [];
15
-
16
- function addSeedDependencies({
17
- registryName,
18
- type,
19
- }: { registryName: string; type: "ui" | "lib" }) {
20
- if (selectedComponents.some((c) => c.name === registryName && c.type === type)) return;
21
-
22
- selectedComponents.push({ type, name: registryName });
23
-
24
- const registry =
25
- type === "ui"
26
- ? uiRegistryIndex.find((c) => c.name === registryName)
27
- : libRegistryIndex.find((c) => c.name === registryName);
28
- if (!registry) return;
29
-
30
- if (registry.innerDependencies) {
31
- for (const dep of registry.innerDependencies) {
32
- const [depType, depName] = dep.split(":");
33
- addSeedDependencies({ registryName: depName, type: depType as "ui" | "lib" });
34
- }
35
- }
36
- }
37
-
38
- for (const registryName of userSelects) {
39
- addSeedDependencies({ registryName, type: "ui" });
40
- }
41
-
42
- return Array.from(selectedComponents);
43
- }
@@ -1,75 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import { registryUISchema, type RegistryUIMachineGenerated } from "@/src/schema";
3
-
4
- export async function fetchRegistryItems(
5
- fileNames?: string[],
6
- baseUrl?: string,
7
- type: "ui" | "lib" = "ui",
8
- ): Promise<RegistryUIMachineGenerated> {
9
- const results = await Promise.all(
10
- fileNames.map(async (fileName) => {
11
- try {
12
- const response = await fetch(`${baseUrl}/__registry__/${type}/${fileName}.json`);
13
- return await response.json();
14
- } catch (error) {
15
- const index = await fetch(`${baseUrl}/__registry__/${type}/index.json`).then((res) =>
16
- res.json(),
17
- );
18
- const parsedIndex = registryUISchema.parse(index);
19
- const availableComponents = parsedIndex.map((component) => component.name);
20
-
21
- p.log.error(`${type}:${fileName} 컴포넌트는 레지스트리에 존재하지 않아요.`);
22
- p.log.info(`사용 가능한 컴포넌트: ${availableComponents.join(", ")}`);
23
- p.log.info(
24
- JSON.stringify(
25
- {
26
- baseUrl,
27
- error: error.toString(),
28
- },
29
- null,
30
- 2,
31
- ),
32
- );
33
- process.exit(1);
34
- }
35
- }),
36
- );
37
-
38
- return results;
39
- }
40
-
41
- export async function fetchRegistryItem(
42
- fileName: string,
43
- baseUrl: string,
44
- type: "ui" | "lib" = "ui",
45
- ) {
46
- const [result] = await fetchRegistryItems([fileName], baseUrl, type);
47
- return result;
48
- }
49
-
50
- export async function getRegistryUIIndex(baseUrl?: string) {
51
- try {
52
- const [result] = await fetchRegistryItems(["index"], baseUrl, "ui");
53
-
54
- return registryUISchema.parse(result);
55
- } catch (error) {
56
- p.log.error("레지스트리 인덱스를 가져오는 데 실패했어요.");
57
- p.log.info(
58
- JSON.stringify(
59
- {
60
- baseUrl,
61
- error: error.toString(),
62
- },
63
- null,
64
- 2,
65
- ),
66
- );
67
- process.exit(1);
68
- }
69
- }
70
-
71
- export async function getRegistryLibIndex(baseUrl?: string) {
72
- const [result] = await fetchRegistryItems(["index"], baseUrl, "lib");
73
-
74
- return registryUISchema.parse(result);
75
- }