@seed-design/cli 1.2.2 → 1.4.0-alpha.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.
- package/bin/index.mjs +12 -11
- package/package.json +1 -1
- package/src/commands/add-all.ts +9 -3
- package/src/commands/add.ts +9 -3
- package/src/commands/compat.ts +15 -7
- package/src/commands/docs.ts +312 -0
- package/src/commands/init.ts +9 -2
- package/src/index.ts +2 -0
- package/src/schema.ts +46 -10
- package/src/tests/resolve-dependencies.test.ts +13 -13
- package/src/utils/analytics.ts +1 -2
- package/src/utils/compatibility.ts +38 -11
- package/src/utils/fetch.ts +39 -6
- package/src/utils/get-config.ts +1 -0
- package/src/utils/init-config.ts +29 -1
- package/src/utils/write.ts +3 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { fetchDocsIndex } from "@/src/utils/fetch";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import type { CAC } from "cac";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { BASE_URL } from "../constants";
|
|
6
|
+
import { analytics } from "../utils/analytics";
|
|
7
|
+
import { highlight } from "../utils/color";
|
|
8
|
+
import {
|
|
9
|
+
CliCancelError,
|
|
10
|
+
CliError,
|
|
11
|
+
handleCliError,
|
|
12
|
+
isCliCancelError,
|
|
13
|
+
isVerboseMode,
|
|
14
|
+
} from "../utils/error";
|
|
15
|
+
import type { DocsCategory, DocsItem, DocsSection } from "../schema";
|
|
16
|
+
|
|
17
|
+
const GITHUB_SNIPPET_BASE =
|
|
18
|
+
"https://raw.githubusercontent.com/daangn/seed-design/refs/heads/dev/docs/registry";
|
|
19
|
+
|
|
20
|
+
const docsOptionsSchema = z.object({
|
|
21
|
+
query: z.string().optional(),
|
|
22
|
+
baseUrl: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function buildSnippetUrl(registryId: string, snippetPath: string): string {
|
|
26
|
+
return `${GITHUB_SNIPPET_BASE}/${registryId}/${snippetPath}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printDocsResult(item: DocsItem, baseUrl: string) {
|
|
30
|
+
const docLink = `${baseUrl}${item.docUrl}`;
|
|
31
|
+
const llmsLink = `${baseUrl}/llms${item.docUrl}.txt`;
|
|
32
|
+
|
|
33
|
+
const lines = [item.id, `- docs: ${docLink}`, `- llms.txt: ${llmsLink}`];
|
|
34
|
+
|
|
35
|
+
if (item.snippetKey && item.snippets && item.snippets.length > 0) {
|
|
36
|
+
const [registryId] = item.snippetKey.split(":");
|
|
37
|
+
if (registryId === "ui" || registryId === "breeze") {
|
|
38
|
+
if (item.snippets.length === 1) {
|
|
39
|
+
lines.push(`- snippet: ${buildSnippetUrl(registryId, item.snippets[0].path)}`);
|
|
40
|
+
} else {
|
|
41
|
+
lines.push("- snippet:");
|
|
42
|
+
for (const snippet of item.snippets) {
|
|
43
|
+
lines.push(` - ${snippet.label}: ${buildSnippetUrl(registryId, snippet.path)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
p.log.message(lines.join("\n"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse a path-style query into segments.
|
|
54
|
+
* e.g. "react/components/action-button" → ["react", "components", "action-button"]
|
|
55
|
+
*/
|
|
56
|
+
function parseQueryPath(query: string): string[] {
|
|
57
|
+
return query
|
|
58
|
+
.split("/")
|
|
59
|
+
.map((s) => s.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Search all items across all categories/sections.
|
|
65
|
+
*/
|
|
66
|
+
function searchAllItems(
|
|
67
|
+
categories: DocsCategory[],
|
|
68
|
+
query: string,
|
|
69
|
+
): { item: DocsItem; categoryLabel: string; sectionLabel: string }[] {
|
|
70
|
+
const q = query.toLowerCase();
|
|
71
|
+
return categories.flatMap((cat) =>
|
|
72
|
+
cat.sections.flatMap((sec) =>
|
|
73
|
+
sec.items
|
|
74
|
+
.filter((item) => item.id.toLowerCase().includes(q) || item.title.toLowerCase().includes(q))
|
|
75
|
+
.map((item) => ({
|
|
76
|
+
item,
|
|
77
|
+
categoryLabel: cat.label,
|
|
78
|
+
sectionLabel: sec.label,
|
|
79
|
+
})),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function selectItem(items: DocsItem[]): Promise<DocsItem> {
|
|
85
|
+
const selected = await p.select({
|
|
86
|
+
message: "항목을 선택해주세요",
|
|
87
|
+
options: items.map((item) => ({
|
|
88
|
+
label: `${item.deprecated ? "(deprecated) " : ""}${item.title}`,
|
|
89
|
+
value: item,
|
|
90
|
+
hint: item.description,
|
|
91
|
+
})),
|
|
92
|
+
});
|
|
93
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
94
|
+
return selected;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function selectSection(sections: DocsSection[]): Promise<DocsSection> {
|
|
98
|
+
const selected = await p.select({
|
|
99
|
+
message: "섹션을 선택해주세요",
|
|
100
|
+
options: sections.map((sec) => ({
|
|
101
|
+
label: sec.label,
|
|
102
|
+
value: sec,
|
|
103
|
+
hint: `${sec.items.length}개 항목`,
|
|
104
|
+
})),
|
|
105
|
+
});
|
|
106
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
107
|
+
return selected;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function selectCategory(categories: DocsCategory[]): Promise<DocsCategory> {
|
|
111
|
+
const selected = await p.select({
|
|
112
|
+
message: "카테고리를 선택해주세요",
|
|
113
|
+
options: categories.map((cat) => ({
|
|
114
|
+
label: cat.label,
|
|
115
|
+
value: cat,
|
|
116
|
+
hint: `${cat.sections.reduce((sum, s) => sum + s.items.length, 0)}개 항목`,
|
|
117
|
+
})),
|
|
118
|
+
});
|
|
119
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
120
|
+
return selected;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const docsCommand = (cli: CAC) => {
|
|
124
|
+
cli
|
|
125
|
+
.command("docs [query]", "문서 링크, llms.txt 링크, 스니펫 링크를 조회합니다")
|
|
126
|
+
.option("-u, --baseUrl <baseUrl>", `레지스트리의 기본 URL (기본값: ${BASE_URL})`, {
|
|
127
|
+
default: BASE_URL,
|
|
128
|
+
})
|
|
129
|
+
.example("seed-design docs")
|
|
130
|
+
.example("seed-design docs action-button")
|
|
131
|
+
.example("seed-design docs react")
|
|
132
|
+
.example("seed-design docs react/components")
|
|
133
|
+
.example("seed-design docs react/components/action-button")
|
|
134
|
+
.action(async (query, opts) => {
|
|
135
|
+
const startTime = Date.now();
|
|
136
|
+
const verbose = isVerboseMode(opts);
|
|
137
|
+
p.intro("seed-design docs");
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const parsed = docsOptionsSchema.safeParse({ query, ...opts });
|
|
141
|
+
if (!parsed.success) {
|
|
142
|
+
throw parsed.error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { data: options } = parsed;
|
|
146
|
+
const baseUrl = options.baseUrl ?? BASE_URL;
|
|
147
|
+
|
|
148
|
+
const { start, stop } = p.spinner();
|
|
149
|
+
start("문서 목록을 가져오고 있어요...");
|
|
150
|
+
|
|
151
|
+
const docsIndex = await (async () => {
|
|
152
|
+
try {
|
|
153
|
+
const index = await fetchDocsIndex({ baseUrl });
|
|
154
|
+
stop("문서 목록을 가져왔어요.");
|
|
155
|
+
return index;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
stop("문서 목록을 가져오지 못했어요.");
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
|
|
162
|
+
const { categories } = docsIndex;
|
|
163
|
+
let selectedItem: DocsItem;
|
|
164
|
+
|
|
165
|
+
if (options.query) {
|
|
166
|
+
const segments = parseQueryPath(options.query);
|
|
167
|
+
|
|
168
|
+
// Try to resolve as path: category / section / item
|
|
169
|
+
const matchedCategory = categories.find((c) => c.id === segments[0]);
|
|
170
|
+
|
|
171
|
+
if (matchedCategory && segments.length >= 2) {
|
|
172
|
+
const matchedSection = matchedCategory.sections.find((s) => s.id === segments[1]);
|
|
173
|
+
|
|
174
|
+
if (matchedSection && segments.length >= 3) {
|
|
175
|
+
// Full path: category/section/item
|
|
176
|
+
const matchedItem = matchedSection.items.find((i) => i.id === segments[2]);
|
|
177
|
+
if (matchedItem) {
|
|
178
|
+
selectedItem = matchedItem;
|
|
179
|
+
} else {
|
|
180
|
+
// Item not found in section — search within the section
|
|
181
|
+
const q = segments[2].toLowerCase();
|
|
182
|
+
const matched = matchedSection.items.filter(
|
|
183
|
+
(i) => i.id.toLowerCase().includes(q) || i.title.toLowerCase().includes(q),
|
|
184
|
+
);
|
|
185
|
+
if (matched.length === 0) {
|
|
186
|
+
throw new CliError({
|
|
187
|
+
message: `${highlight(options.query)}: 문서를 찾을 수 없어요.`,
|
|
188
|
+
hint: `\`seed-design docs ${matchedCategory.id}/${matchedSection.id}\`로 목록을 확인해보세요.`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (matched.length === 1) {
|
|
192
|
+
selectedItem = matched[0];
|
|
193
|
+
} else {
|
|
194
|
+
selectedItem = await selectItem(matched);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else if (matchedSection) {
|
|
198
|
+
// category/section — select item within section
|
|
199
|
+
selectedItem = await selectItem(matchedSection.items);
|
|
200
|
+
} else {
|
|
201
|
+
// category/??? — search within category
|
|
202
|
+
const q = segments[1].toLowerCase();
|
|
203
|
+
const matched = matchedCategory.sections.flatMap((s) =>
|
|
204
|
+
s.items
|
|
205
|
+
.filter(
|
|
206
|
+
(i) => i.id.toLowerCase().includes(q) || i.title.toLowerCase().includes(q),
|
|
207
|
+
)
|
|
208
|
+
.map((item) => ({ item, sectionLabel: s.label })),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (matched.length === 0) {
|
|
212
|
+
throw new CliError({
|
|
213
|
+
message: `${highlight(options.query)}: 문서를 찾을 수 없어요.`,
|
|
214
|
+
hint: `\`seed-design docs ${matchedCategory.id}\`로 목록을 확인해보세요.`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (matched.length === 1) {
|
|
218
|
+
selectedItem = matched[0].item;
|
|
219
|
+
} else {
|
|
220
|
+
const selected = await p.select({
|
|
221
|
+
message: `${highlight(segments[1])}에 해당하는 항목을 선택해주세요`,
|
|
222
|
+
options: matched.map(({ item, sectionLabel }) => ({
|
|
223
|
+
label: `[${sectionLabel}] ${item.title}`,
|
|
224
|
+
value: item,
|
|
225
|
+
hint: item.description,
|
|
226
|
+
})),
|
|
227
|
+
});
|
|
228
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
229
|
+
selectedItem = selected;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else if (matchedCategory) {
|
|
233
|
+
// Single segment matching a category — drill into it
|
|
234
|
+
if (matchedCategory.sections.length === 1) {
|
|
235
|
+
selectedItem = await selectItem(matchedCategory.sections[0].items);
|
|
236
|
+
} else {
|
|
237
|
+
const section = await selectSection(matchedCategory.sections);
|
|
238
|
+
selectedItem = await selectItem(section.items);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// No category match — global search
|
|
242
|
+
const matched = searchAllItems(categories, options.query);
|
|
243
|
+
|
|
244
|
+
if (matched.length === 0) {
|
|
245
|
+
throw new CliError({
|
|
246
|
+
message: `${highlight(options.query)}: 문서를 찾을 수 없어요.`,
|
|
247
|
+
hint: "`seed-design docs`로 전체 목록을 확인해보세요.",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (matched.length === 1) {
|
|
251
|
+
selectedItem = matched[0].item;
|
|
252
|
+
} else {
|
|
253
|
+
const selected = await p.select({
|
|
254
|
+
message: `${highlight(options.query)}에 해당하는 항목을 선택해주세요`,
|
|
255
|
+
options: matched.map(({ item, categoryLabel, sectionLabel }) => ({
|
|
256
|
+
label: `[${categoryLabel} > ${sectionLabel}] ${item.title}`,
|
|
257
|
+
value: item,
|
|
258
|
+
hint: item.description,
|
|
259
|
+
})),
|
|
260
|
+
});
|
|
261
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
262
|
+
selectedItem = selected;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
// Full interactive flow: category → section → item
|
|
267
|
+
const category = await selectCategory(categories);
|
|
268
|
+
|
|
269
|
+
let section: DocsSection;
|
|
270
|
+
if (category.sections.length === 1) {
|
|
271
|
+
section = category.sections[0];
|
|
272
|
+
} else {
|
|
273
|
+
section = await selectSection(category.sections);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
selectedItem = await selectItem(section.items);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
printDocsResult(selectedItem, baseUrl);
|
|
280
|
+
p.outro("완료했어요.");
|
|
281
|
+
|
|
282
|
+
const duration = Date.now() - startTime;
|
|
283
|
+
try {
|
|
284
|
+
await analytics.track(process.cwd(), {
|
|
285
|
+
event: "docs",
|
|
286
|
+
properties: {
|
|
287
|
+
query: options.query ?? null,
|
|
288
|
+
item_id: selectedItem.id,
|
|
289
|
+
has_snippet: !!(selectedItem.snippets && selectedItem.snippets.length > 0),
|
|
290
|
+
duration_ms: duration,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
} catch (telemetryError) {
|
|
294
|
+
if (verbose) {
|
|
295
|
+
console.error("[Telemetry] docs tracking failed:", telemetryError);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (isCliCancelError(error)) {
|
|
300
|
+
p.outro(highlight(error.message));
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
handleCliError(error, {
|
|
305
|
+
defaultMessage: "문서 조회에 실패했어요.",
|
|
306
|
+
defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
|
|
307
|
+
verbose,
|
|
308
|
+
});
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
};
|
package/src/commands/init.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { z } from "zod";
|
|
|
3
3
|
import { analytics } from "../utils/analytics";
|
|
4
4
|
import { highlight } from "../utils/color";
|
|
5
5
|
import { handleCliError, isCliCancelError, isVerboseMode } from "../utils/error";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_INIT_CONFIG,
|
|
8
|
+
detectFramework,
|
|
9
|
+
promptInitConfig,
|
|
10
|
+
writeInitConfigFile,
|
|
11
|
+
} from "../utils/init-config";
|
|
7
12
|
|
|
8
13
|
import type { Config } from "@/src/utils/get-config";
|
|
9
14
|
|
|
@@ -37,7 +42,9 @@ export const initCommand = (cli: CAC) => {
|
|
|
37
42
|
|
|
38
43
|
const options = parsed.data;
|
|
39
44
|
const isDefaultMode = options.yes || options.default;
|
|
40
|
-
const config: Config = isDefaultMode
|
|
45
|
+
const config: Config = isDefaultMode
|
|
46
|
+
? { ...DEFAULT_INIT_CONFIG, framework: detectFramework(options.cwd) }
|
|
47
|
+
: await promptInitConfig(options.cwd);
|
|
41
48
|
|
|
42
49
|
const { start, stop } = p.spinner();
|
|
43
50
|
start("seed-design.json 파일 생성중...");
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { addCommand } from "@/src/commands/add";
|
|
4
4
|
import { addAllCommand } from "@/src/commands/add-all";
|
|
5
5
|
import { compatCommand } from "@/src/commands/compat";
|
|
6
|
+
import { docsCommand } from "@/src/commands/docs";
|
|
6
7
|
import { initCommand } from "@/src/commands/init";
|
|
7
8
|
import { getPackageInfo } from "@/src/utils/get-package-info";
|
|
8
9
|
import { cac } from "cac";
|
|
@@ -19,6 +20,7 @@ async function main() {
|
|
|
19
20
|
addCommand(CLI);
|
|
20
21
|
addAllCommand(CLI);
|
|
21
22
|
compatCommand(CLI);
|
|
23
|
+
docsCommand(CLI);
|
|
22
24
|
initCommand(CLI);
|
|
23
25
|
|
|
24
26
|
CLI.version(packageInfo.version || "1.0.0", "-v, --version");
|
package/src/schema.ts
CHANGED
|
@@ -45,16 +45,14 @@ export const publicRegistrySchema = z.object({
|
|
|
45
45
|
hideFromCLICatalog: z.boolean().optional(),
|
|
46
46
|
|
|
47
47
|
items: z.array(
|
|
48
|
-
publicRegistryItemSchema
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
z.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
),
|
|
57
|
-
}),
|
|
48
|
+
publicRegistryItemSchema.omit({ snippets: true }).extend({
|
|
49
|
+
snippets: z.array(
|
|
50
|
+
z.object({
|
|
51
|
+
path: z.string(),
|
|
52
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
}),
|
|
58
56
|
),
|
|
59
57
|
});
|
|
60
58
|
|
|
@@ -66,3 +64,41 @@ export const publicAvailableRegistriesSchema = z.array(z.object({ id: z.string()
|
|
|
66
64
|
export type PublicRegistryItem = z.infer<typeof publicRegistryItemSchema>;
|
|
67
65
|
export type PublicRegistry = z.infer<typeof publicRegistrySchema>;
|
|
68
66
|
export type PublicAvailableRegistries = z.infer<typeof publicAvailableRegistriesSchema>;
|
|
67
|
+
|
|
68
|
+
///////////////////////////////////////////////////////////////
|
|
69
|
+
|
|
70
|
+
export const docsSnippetSchema = z.object({
|
|
71
|
+
label: z.string(),
|
|
72
|
+
path: z.string(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const docsItemSchema = z.object({
|
|
76
|
+
id: z.string(),
|
|
77
|
+
title: z.string(),
|
|
78
|
+
description: z.string().optional(),
|
|
79
|
+
docUrl: z.string(),
|
|
80
|
+
deprecated: z.boolean().optional(),
|
|
81
|
+
snippetKey: z.string().optional(),
|
|
82
|
+
snippets: z.array(docsSnippetSchema).optional(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const docsSectionSchema = z.object({
|
|
86
|
+
id: z.string(),
|
|
87
|
+
label: z.string(),
|
|
88
|
+
items: z.array(docsItemSchema),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const docsCategorySchema = z.object({
|
|
92
|
+
id: z.string(),
|
|
93
|
+
label: z.string(),
|
|
94
|
+
sections: z.array(docsSectionSchema),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const docsIndexSchema = z.object({
|
|
98
|
+
categories: z.array(docsCategorySchema),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export type DocsItem = z.infer<typeof docsItemSchema>;
|
|
102
|
+
export type DocsSection = z.infer<typeof docsSectionSchema>;
|
|
103
|
+
export type DocsCategory = z.infer<typeof docsCategorySchema>;
|
|
104
|
+
export type DocsIndex = z.infer<typeof docsIndexSchema>;
|
|
@@ -3,7 +3,7 @@ import { resolveDependencies } from "../utils/resolve-dependencies";
|
|
|
3
3
|
import type { PublicRegistry } from "@/src/schema";
|
|
4
4
|
|
|
5
5
|
describe("resolveDependencies", () => {
|
|
6
|
-
it("
|
|
6
|
+
it("의존성이 없는 단일 아이템을 해석해야 한다", () => {
|
|
7
7
|
const publicRegistries: PublicRegistry[] = [
|
|
8
8
|
{
|
|
9
9
|
id: "ui",
|
|
@@ -36,7 +36,7 @@ describe("resolveDependencies", () => {
|
|
|
36
36
|
expect(result.npmDependenciesToAdd.size).toBe(0);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
it("
|
|
39
|
+
it("npm 의존성을 수집해야 한다", () => {
|
|
40
40
|
const publicRegistries: PublicRegistry[] = [
|
|
41
41
|
{
|
|
42
42
|
id: "ui",
|
|
@@ -61,7 +61,7 @@ describe("resolveDependencies", () => {
|
|
|
61
61
|
expect(result.npmDependenciesToAdd.has("clsx")).toBe(true);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
it("
|
|
64
|
+
it("innerDependencies를 재귀적으로 해석해야 한다", () => {
|
|
65
65
|
const publicRegistries: PublicRegistry[] = [
|
|
66
66
|
{
|
|
67
67
|
id: "ui",
|
|
@@ -131,7 +131,7 @@ describe("resolveDependencies", () => {
|
|
|
131
131
|
expect(result.npmDependenciesToAdd.has("framer-motion")).toBe(true);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
it("
|
|
134
|
+
it("여러 선택 아이템을 함께 처리해야 한다", () => {
|
|
135
135
|
const publicRegistries: PublicRegistry[] = [
|
|
136
136
|
{
|
|
137
137
|
id: "ui",
|
|
@@ -161,7 +161,7 @@ describe("resolveDependencies", () => {
|
|
|
161
161
|
expect(result.registryItemsToAdd[0].items.map((i) => i.id)).toEqual(["button", "chip"]);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
it("
|
|
164
|
+
it("중복 아이템을 제거해야 한다", () => {
|
|
165
165
|
const publicRegistries: PublicRegistry[] = [
|
|
166
166
|
{
|
|
167
167
|
id: "ui",
|
|
@@ -186,7 +186,7 @@ describe("resolveDependencies", () => {
|
|
|
186
186
|
},
|
|
187
187
|
];
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// dialog와 button을 동시에 선택해도 button은 dialog 의존성으로 이미 포함된다.
|
|
190
190
|
const result = resolveDependencies({
|
|
191
191
|
selectedItemKeys: ["ui:dialog", "ui:button"],
|
|
192
192
|
publicRegistries,
|
|
@@ -194,12 +194,12 @@ describe("resolveDependencies", () => {
|
|
|
194
194
|
|
|
195
195
|
expect(result.registryItemsToAdd).toHaveLength(1);
|
|
196
196
|
expect(result.registryItemsToAdd[0].items).toHaveLength(2);
|
|
197
|
-
//
|
|
197
|
+
// button은 한 번만 포함되어야 한다.
|
|
198
198
|
const buttonCount = result.registryItemsToAdd[0].items.filter((i) => i.id === "button").length;
|
|
199
199
|
expect(buttonCount).toBe(1);
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
it("
|
|
202
|
+
it("중첩된 innerDependencies를 처리해야 한다", () => {
|
|
203
203
|
const publicRegistries: PublicRegistry[] = [
|
|
204
204
|
{
|
|
205
205
|
id: "ui",
|
|
@@ -268,7 +268,7 @@ describe("resolveDependencies", () => {
|
|
|
268
268
|
expect(result.npmDependenciesToAdd.has("lodash")).toBe(true);
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
-
it("
|
|
271
|
+
it("잘못된 스니펫 포맷이면 에러를 던져야 한다", () => {
|
|
272
272
|
const publicRegistries: PublicRegistry[] = [];
|
|
273
273
|
|
|
274
274
|
expect(() =>
|
|
@@ -279,7 +279,7 @@ describe("resolveDependencies", () => {
|
|
|
279
279
|
).toThrowError('Invalid snippet format: "invalid-format"');
|
|
280
280
|
});
|
|
281
281
|
|
|
282
|
-
it("
|
|
282
|
+
it("존재하지 않는 스니펫이면 에러를 던져야 한다", () => {
|
|
283
283
|
const publicRegistries: PublicRegistry[] = [
|
|
284
284
|
{
|
|
285
285
|
id: "ui",
|
|
@@ -295,7 +295,7 @@ describe("resolveDependencies", () => {
|
|
|
295
295
|
).toThrowError('Cannot find snippet: "ui:non-existent"');
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
-
it("
|
|
298
|
+
it("inner dependency가 누락되면 에러를 던져야 한다", () => {
|
|
299
299
|
const publicRegistries: PublicRegistry[] = [
|
|
300
300
|
{
|
|
301
301
|
id: "ui",
|
|
@@ -327,7 +327,7 @@ describe("resolveDependencies", () => {
|
|
|
327
327
|
).toThrowError("Cannot find dependency item: breeze:missing");
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
-
it("
|
|
330
|
+
it("여러 레지스트리와 아이템을 함께 처리해야 한다", () => {
|
|
331
331
|
const publicRegistries: PublicRegistry[] = [
|
|
332
332
|
{
|
|
333
333
|
id: "ui",
|
|
@@ -378,7 +378,7 @@ describe("resolveDependencies", () => {
|
|
|
378
378
|
expect(result.npmDependenciesToAdd.has("framer-motion")).toBe(true);
|
|
379
379
|
});
|
|
380
380
|
|
|
381
|
-
it("
|
|
381
|
+
it("중첩 의존성의 npm 패키지를 모두 수집해야 한다", () => {
|
|
382
382
|
const publicRegistries: PublicRegistry[] = [
|
|
383
383
|
{
|
|
384
384
|
id: "ui",
|
package/src/utils/analytics.ts
CHANGED
|
@@ -60,9 +60,8 @@ async function track(cwd: string, { event, properties = {} }: TrackOptions): Pro
|
|
|
60
60
|
|
|
61
61
|
const fullEvent = `${EVENT_PREFIX}.${event}`;
|
|
62
62
|
|
|
63
|
-
// Dev 모드:
|
|
63
|
+
// Dev 모드: 텔레메트리 전송 생략
|
|
64
64
|
if (process.env.NODE_ENV === "dev") {
|
|
65
|
-
console.log(`📊 [Telemetry] ${fullEvent}`, properties);
|
|
66
65
|
return;
|
|
67
66
|
}
|
|
68
67
|
|
|
@@ -6,8 +6,19 @@ import path from "path";
|
|
|
6
6
|
import { intersects, satisfies, valid, validRange } from "semver";
|
|
7
7
|
import { highlight } from "./color";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const REACT_COMPAT_PACKAGES = ["@seed-design/react", "@seed-design/css"] as const;
|
|
10
|
+
const LYNX_COMPAT_PACKAGES = ["@seed-design/lynx-react", "@seed-design/lynx-css"] as const;
|
|
11
|
+
|
|
12
|
+
/** @deprecated Use getCompatPackageNames(framework) instead */
|
|
13
|
+
export const COMPAT_PACKAGE_NAMES = REACT_COMPAT_PACKAGES;
|
|
14
|
+
|
|
15
|
+
export type CompatPackageName =
|
|
16
|
+
| (typeof REACT_COMPAT_PACKAGES)[number]
|
|
17
|
+
| (typeof LYNX_COMPAT_PACKAGES)[number];
|
|
18
|
+
|
|
19
|
+
export function getCompatPackageNames(framework: string): readonly CompatPackageName[] {
|
|
20
|
+
return framework === "lynx" ? LYNX_COMPAT_PACKAGES : REACT_COMPAT_PACKAGES;
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
const WORKSPACE_VERSION_PREFIX = "workspace:";
|
|
13
24
|
const NPM_ALIAS_PREFIX = "npm:";
|
|
@@ -28,6 +39,7 @@ export interface CompatibilityReport {
|
|
|
28
39
|
|
|
29
40
|
export function getProjectSeedPackageVersionSpecs(
|
|
30
41
|
cwd: string,
|
|
42
|
+
framework = "react",
|
|
31
43
|
): Partial<Record<CompatPackageName, string>> {
|
|
32
44
|
try {
|
|
33
45
|
const packageInfo = getPackageInfo(cwd);
|
|
@@ -38,8 +50,9 @@ export function getProjectSeedPackageVersionSpecs(
|
|
|
38
50
|
...packageInfo.optionalDependencies,
|
|
39
51
|
};
|
|
40
52
|
const result: Partial<Record<CompatPackageName, string>> = {};
|
|
53
|
+
const compatPackages = getCompatPackageNames(framework);
|
|
41
54
|
|
|
42
|
-
for (const packageName of
|
|
55
|
+
for (const packageName of compatPackages) {
|
|
43
56
|
const value = packageDeps[packageName];
|
|
44
57
|
if (typeof value === "string") {
|
|
45
58
|
result[packageName] = value;
|
|
@@ -56,10 +69,12 @@ export function analyzeRegistryItemCompatibility({
|
|
|
56
69
|
publicRegistries,
|
|
57
70
|
itemKeys,
|
|
58
71
|
projectPackageVersions,
|
|
72
|
+
framework = "react",
|
|
59
73
|
}: {
|
|
60
74
|
publicRegistries: PublicRegistry[];
|
|
61
75
|
itemKeys: string[];
|
|
62
76
|
projectPackageVersions: Partial<Record<CompatPackageName, string>>;
|
|
77
|
+
framework?: string;
|
|
63
78
|
}): CompatibilityReport {
|
|
64
79
|
const checkedItemKeys = Array.from(new Set(itemKeys));
|
|
65
80
|
const itemMap = new Map<string, PublicRegistry["items"][number]>(
|
|
@@ -69,14 +84,15 @@ export function analyzeRegistryItemCompatibility({
|
|
|
69
84
|
);
|
|
70
85
|
|
|
71
86
|
const issues: CompatibilityIssue[] = [];
|
|
87
|
+
const compatPackages = getCompatPackageNames(framework);
|
|
72
88
|
|
|
73
89
|
for (const itemKey of checkedItemKeys) {
|
|
74
90
|
const item = itemMap.get(itemKey);
|
|
75
91
|
if (!item) continue;
|
|
76
92
|
|
|
77
|
-
const requiredRangesByPackage = collectRequiredRangesByPackage(item);
|
|
93
|
+
const requiredRangesByPackage = collectRequiredRangesByPackage(item, framework);
|
|
78
94
|
|
|
79
|
-
for (const packageName of
|
|
95
|
+
for (const packageName of compatPackages) {
|
|
80
96
|
const requiredRanges = Array.from(requiredRangesByPackage[packageName] ?? []);
|
|
81
97
|
|
|
82
98
|
if (!requiredRanges.length) continue;
|
|
@@ -135,15 +151,19 @@ export function analyzeRegistryItemCompatibility({
|
|
|
135
151
|
export function logCompatibilityReport({
|
|
136
152
|
report,
|
|
137
153
|
title,
|
|
154
|
+
framework = "react",
|
|
138
155
|
}: {
|
|
139
156
|
report: CompatibilityReport;
|
|
140
157
|
title: string;
|
|
158
|
+
framework?: string;
|
|
141
159
|
}) {
|
|
142
160
|
if (!report.issues.length) return;
|
|
143
161
|
|
|
162
|
+
const compatPackages = getCompatPackageNames(framework);
|
|
163
|
+
|
|
144
164
|
p.log.warn(title);
|
|
145
165
|
p.log.info(
|
|
146
|
-
`현재 프로젝트 버전: ${
|
|
166
|
+
`현재 프로젝트 버전: ${compatPackages.map((packageName) => `${packageName}@${highlight(report.projectPackageVersions[packageName] ?? "미설치")}`).join(", ")}`,
|
|
147
167
|
);
|
|
148
168
|
|
|
149
169
|
const issuesByItem = new Map<string, CompatibilityIssue[]>();
|
|
@@ -207,14 +227,18 @@ export function findInstalledSnippetItemKeys({
|
|
|
207
227
|
return installedItemKeys;
|
|
208
228
|
}
|
|
209
229
|
|
|
210
|
-
function collectRequiredRangesByPackage(
|
|
230
|
+
function collectRequiredRangesByPackage(
|
|
231
|
+
item: PublicRegistry["items"][number],
|
|
232
|
+
framework = "react",
|
|
233
|
+
) {
|
|
234
|
+
const compatPackages = getCompatPackageNames(framework);
|
|
211
235
|
const requiredRangesByPackage = Object.fromEntries(
|
|
212
|
-
|
|
236
|
+
compatPackages.map((packageName) => [packageName, new Set<string>()]),
|
|
213
237
|
) as Record<CompatPackageName, Set<string>>;
|
|
214
238
|
|
|
215
239
|
for (const snippet of item.snippets) {
|
|
216
240
|
for (const [packageName, requiredRange] of Object.entries(snippet.dependencies ?? {})) {
|
|
217
|
-
if (!isCompatPackageName(packageName)) continue;
|
|
241
|
+
if (!isCompatPackageName(packageName, framework)) continue;
|
|
218
242
|
requiredRangesByPackage[packageName].add(requiredRange);
|
|
219
243
|
}
|
|
220
244
|
}
|
|
@@ -286,6 +310,9 @@ function getSnippetPathCandidates(originalPath: string): string[] {
|
|
|
286
310
|
return Array.from(candidates);
|
|
287
311
|
}
|
|
288
312
|
|
|
289
|
-
function isCompatPackageName(
|
|
290
|
-
|
|
313
|
+
function isCompatPackageName(
|
|
314
|
+
packageName: string,
|
|
315
|
+
framework = "react",
|
|
316
|
+
): packageName is CompatPackageName {
|
|
317
|
+
return (getCompatPackageNames(framework) as readonly string[]).includes(packageName);
|
|
291
318
|
}
|