@seed-design/cli 0.0.2 → 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.
@@ -0,0 +1,122 @@
1
+ import type { PublicRegistry } from "@/src/schema";
2
+
3
+ import * as p from "@clack/prompts";
4
+ import {
5
+ type PublicRegistryItem,
6
+ publicRegistrySchema,
7
+ publicRegistryItemSchema,
8
+ type PublicAvailableRegistries,
9
+ publicAvailableRegistriesSchema,
10
+ } from "@/src/schema";
11
+
12
+ export async function fetchAvailableRegistries({
13
+ baseUrl,
14
+ }: {
15
+ baseUrl: string;
16
+ }): Promise<PublicAvailableRegistries> {
17
+ // TODO: make this file public
18
+ const response = await fetch(`${baseUrl}/__registry__/index.json`);
19
+
20
+ if (!response.ok)
21
+ throw new Error(`Failed to fetch registries: ${response.status} ${response.statusText}`);
22
+
23
+ const registries = await response.json();
24
+ const {
25
+ success,
26
+ data: parsedRegistries,
27
+ error,
28
+ } = publicAvailableRegistriesSchema.safeParse(registries);
29
+
30
+ if (!success) throw new Error(`Failed to parse registries: ${error?.message}`);
31
+
32
+ return parsedRegistries;
33
+ }
34
+
35
+ export async function fetchRegistry({
36
+ baseUrl,
37
+ registryId,
38
+ }: {
39
+ baseUrl: string;
40
+ registryId: PublicRegistry["id"];
41
+ }): Promise<PublicRegistry> {
42
+ const response = await fetch(`${baseUrl}/__registry__/${registryId}/index.json`);
43
+
44
+ if (!response.ok)
45
+ throw new Error(
46
+ `Failed to fetch ${registryId} registry: ${response.status} ${response.statusText}`,
47
+ );
48
+
49
+ const index = await response.json();
50
+ const { success, data: parsedIndex, error } = publicRegistrySchema.safeParse(index);
51
+
52
+ if (!success) throw new Error(`Failed to parse ${registryId} registry: ${error?.message}`);
53
+
54
+ return parsedIndex;
55
+ }
56
+
57
+ async function fetchRegistryItem({
58
+ baseUrl,
59
+ registryId,
60
+ registryItemId,
61
+ }: {
62
+ baseUrl: string;
63
+ registryId: PublicRegistry["id"];
64
+ registryItemId: PublicRegistryItem["id"];
65
+ }): Promise<PublicRegistryItem> {
66
+ const response = await fetch(`${baseUrl}/__registry__/${registryId}/${registryItemId}.json`);
67
+
68
+ if (!response.ok) {
69
+ throw new Error(`Failed to fetch ${registryItemId}: ${response.status} ${response.statusText}`);
70
+ }
71
+
72
+ const item = await response.json();
73
+ const { success, data: parsedItem, error } = publicRegistryItemSchema.safeParse(item);
74
+
75
+ if (!success) {
76
+ throw new Error(`Failed to parse ${registryItemId}: ${error?.message}`);
77
+ }
78
+
79
+ return parsedItem;
80
+ }
81
+
82
+ export async function fetchRegistryItems({
83
+ baseUrl,
84
+ registryId,
85
+ registryItemIds,
86
+ }: {
87
+ baseUrl: string;
88
+ registryId: PublicRegistry["id"];
89
+ registryItemIds: PublicRegistryItem["id"][];
90
+ }): Promise<PublicRegistryItem[]> {
91
+ return await Promise.all(
92
+ registryItemIds.map(async (itemId) => {
93
+ try {
94
+ return await fetchRegistryItem({ baseUrl, registryId, registryItemId: itemId });
95
+ } catch (error) {
96
+ // show available registry items in the registry
97
+ const response = await fetch(`${baseUrl}/__registry__/${registryId}/index.json`);
98
+
99
+ if (!response.ok)
100
+ throw new Error(
101
+ `${registryId} 레지스트리를 가져오지 못했어요: ${response.status} ${response.statusText}`,
102
+ );
103
+
104
+ const index = await response.json();
105
+ const { success, data: parsedIndex } = publicRegistrySchema.safeParse(index);
106
+
107
+ // fatal, should not happen
108
+ if (!success) throw new Error(`Failed to parse registry index for ${registryId}`);
109
+
110
+ p.log.error(`${itemId} 스니펫이 ${registryId} 레지스트리에 없어요.`);
111
+ p.log.info(
112
+ `${registryId} 레지스트리에 존재하는 스니펫:\n${parsedIndex.items
113
+ .map((component) => component.id)
114
+ .join("\n")}`,
115
+ );
116
+
117
+ // so fetchRegistryItems also can throw
118
+ throw error;
119
+ }
120
+ }),
121
+ );
122
+ }
@@ -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,7 +11,7 @@ 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),
@@ -22,65 +20,33 @@ export const rawConfigSchema = z
22
20
  })
23
21
  .strict();
24
22
 
25
- export type RawConfig = z.infer<typeof rawConfigSchema>;
26
-
27
- export const configSchema = rawConfigSchema.extend({
28
- resolvedUIPaths: z.string(),
29
- resolvedLibPaths: z.string(),
30
- });
23
+ export type Config = z.infer<typeof configSchema>;
31
24
 
32
25
  export async function getConfig(cwd: string) {
33
26
  const config = await getRawConfig(cwd);
27
+ if (!config) return null;
34
28
 
35
- if (!config) {
36
- return null;
37
- }
38
-
39
- return await resolveConfigPaths(cwd, config);
29
+ return configSchema.parse(config);
40
30
  }
41
31
 
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> {
32
+ export async function getRawConfig(cwd: string): Promise<Config | null> {
70
33
  try {
71
34
  const configResult = await explorer.search(cwd);
72
- return rawConfigSchema.parse(configResult.config);
35
+ return configSchema.parse(configResult.config);
73
36
  } catch {
74
37
  p.log.error("프로젝트 루트 경로에 `seed-design.json` 파일이 없어요.");
75
38
 
76
39
  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 {
40
+
41
+ if (!isConfirm) {
82
42
  p.outro(highlight("작업이 취소됐어요."));
83
43
  process.exit(1);
84
44
  }
45
+
46
+ const packageManager = await getPackageManager(cwd);
47
+
48
+ await execa(packageManager, ["seed-design", "init", "--default"], { cwd });
49
+
50
+ p.log.message("seed-design.json 파일이 생성됐어요.");
85
51
  }
86
52
  }
@@ -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
- }