@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.
@@ -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
+ };
@@ -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 { DEFAULT_INIT_CONFIG, promptInitConfig, writeInitConfigFile } from "../utils/init-config";
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 ? DEFAULT_INIT_CONFIG : await promptInitConfig();
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
- .omit({ snippets: true })
50
- .extend({
51
- snippets: z.array(
52
- z.object({
53
- path: z.string(),
54
- dependencies: z.record(z.string(), z.string()).optional(),
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("should resolve a simple item without dependencies", () => {
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("should resolve npm dependencies", () => {
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("should resolve inner dependencies recursively", () => {
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("should handle multiple selected items", () => {
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("should prevent duplicate items", () => {
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
- // Select both dialog and button, but button is already a dependency of dialog
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
- // Button should appear only once
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("should handle nested inner dependencies", () => {
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("should throw error for invalid snippet format", () => {
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("should throw error for non-existent snippet", () => {
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("should throw error for missing inner dependency", () => {
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("should handle multiple registries with multiple items", () => {
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("should collect all npm dependencies from nested dependencies", () => {
381
+ it("중첩 의존성의 npm 패키지를 모두 수집해야 한다", () => {
382
382
  const publicRegistries: PublicRegistry[] = [
383
383
  {
384
384
  id: "ui",
@@ -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
- export const COMPAT_PACKAGE_NAMES = ["@seed-design/react", "@seed-design/css"] as const;
10
- export type CompatPackageName = (typeof COMPAT_PACKAGE_NAMES)[number];
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 COMPAT_PACKAGE_NAMES) {
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 COMPAT_PACKAGE_NAMES) {
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
- `현재 프로젝트 버전: ${COMPAT_PACKAGE_NAMES.map((packageName) => `${packageName}@${highlight(report.projectPackageVersions[packageName] ?? "미설치")}`).join(", ")}`,
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(item: PublicRegistry["items"][number]) {
230
+ function collectRequiredRangesByPackage(
231
+ item: PublicRegistry["items"][number],
232
+ framework = "react",
233
+ ) {
234
+ const compatPackages = getCompatPackageNames(framework);
211
235
  const requiredRangesByPackage = Object.fromEntries(
212
- COMPAT_PACKAGE_NAMES.map((packageName) => [packageName, new Set<string>()]),
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(packageName: string): packageName is CompatPackageName {
290
- return COMPAT_PACKAGE_NAMES.includes(packageName as CompatPackageName);
313
+ function isCompatPackageName(
314
+ packageName: string,
315
+ framework = "react",
316
+ ): packageName is CompatPackageName {
317
+ return (getCompatPackageNames(framework) as readonly string[]).includes(packageName);
291
318
  }