@seed-design/cli 1.2.1 → 1.2.2
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 +11 -10
- package/package.json +3 -3
- package/src/AGENTS.md +17 -0
- package/src/commands/add-all.ts +160 -111
- package/src/commands/add.ts +181 -136
- package/src/commands/compat.ts +281 -0
- package/src/commands/init.ts +56 -67
- package/src/index.ts +4 -0
- package/src/schema.ts +15 -2
- package/src/tests/compatibility.test.ts +148 -0
- package/src/tests/resolve-dependencies.test.ts +1 -1
- package/src/utils/analytics.ts +2 -2
- package/src/utils/compatibility.ts +291 -0
- package/src/utils/error.ts +160 -0
- package/src/utils/get-config.ts +35 -22
- package/src/utils/get-package-info.ts +4 -4
- package/src/utils/init-config.ts +67 -0
- package/src/utils/install.ts +10 -3
package/src/commands/add.ts
CHANGED
|
@@ -11,6 +11,18 @@ import { BASE_URL } from "../constants";
|
|
|
11
11
|
import { highlight } from "../utils/color";
|
|
12
12
|
import { installDependencies } from "../utils/install";
|
|
13
13
|
import { analytics } from "../utils/analytics";
|
|
14
|
+
import {
|
|
15
|
+
analyzeRegistryItemCompatibility,
|
|
16
|
+
getProjectSeedPackageVersionSpecs,
|
|
17
|
+
logCompatibilityReport,
|
|
18
|
+
} from "../utils/compatibility";
|
|
19
|
+
import {
|
|
20
|
+
CliCancelError,
|
|
21
|
+
CliError,
|
|
22
|
+
handleCliError,
|
|
23
|
+
isCliCancelError,
|
|
24
|
+
isVerboseMode,
|
|
25
|
+
} from "../utils/error";
|
|
14
26
|
|
|
15
27
|
const addOptionsSchema = z.object({
|
|
16
28
|
itemIds: z.array(z.string()).optional(),
|
|
@@ -42,150 +54,168 @@ export const addCommand = (cli: CAC) => {
|
|
|
42
54
|
.example("seed-design add ui:alert-dialog")
|
|
43
55
|
.action(async (itemIds, opts) => {
|
|
44
56
|
const startTime = Date.now();
|
|
57
|
+
const verbose = isVerboseMode(opts);
|
|
45
58
|
p.intro("seed-design add");
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
try {
|
|
61
|
+
const parsed = addOptionsSchema.safeParse({ itemIds, ...opts });
|
|
62
|
+
if (!parsed.success) {
|
|
63
|
+
throw parsed.error;
|
|
64
|
+
}
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
const {
|
|
67
|
+
data: { all, ...options },
|
|
68
|
+
} = parsed;
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
if (all) {
|
|
71
|
+
throw new CliError({
|
|
72
|
+
message:
|
|
73
|
+
"`--all` 옵션은 더 이상 지원되지 않아요. 대신 `seed-design add-all` 명령어를 사용해주세요.",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
58
76
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
77
|
+
const cwd = options.cwd;
|
|
78
|
+
const baseUrl = options.baseUrl;
|
|
79
|
+
const config = await getConfig(cwd);
|
|
80
|
+
const rootPath = path.resolve(cwd, config.path);
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
const { start, stop } = p.spinner();
|
|
83
|
+
start("Registry를 가져오고 있어요...");
|
|
66
84
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const publicRegistries = await Promise.all(
|
|
77
|
-
(await fetchAvailableRegistries({ baseUrl })).map(async ({ id }) =>
|
|
78
|
-
fetchRegistry({ baseUrl, registryId: id }),
|
|
79
|
-
),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
stop("Registry를 가져왔어요.");
|
|
83
|
-
|
|
84
|
-
const selectedItemKeys: string[] = await (async () => {
|
|
85
|
-
if (options.itemIds.length > 0) return options.itemIds;
|
|
86
|
-
|
|
87
|
-
const selected = await p.multiselect({
|
|
88
|
-
message: "추가할 항목을 선택해주세요 (스페이스 바로 여러 개 선택 가능)",
|
|
89
|
-
options: publicRegistries
|
|
90
|
-
.filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
|
|
91
|
-
.flatMap(({ id: registryId, items }) =>
|
|
92
|
-
items
|
|
93
|
-
.filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
|
|
94
|
-
.sort((a, b) => a.id.localeCompare(b.id))
|
|
95
|
-
.map(({ id, description, deprecated }) => ({
|
|
96
|
-
label: `${deprecated ? "(deprecated) " : ""}${highlight(registryId)}:${id}`,
|
|
97
|
-
value: `${registryId}:${id}`,
|
|
98
|
-
hint: description,
|
|
99
|
-
|
|
100
|
-
// used for sorting
|
|
101
|
-
deprecated,
|
|
102
|
-
registryItemCount: items.length,
|
|
103
|
-
})),
|
|
104
|
-
)
|
|
105
|
-
.sort((a, b) => {
|
|
106
|
-
if (a.deprecated !== b.deprecated) return a.deprecated ? 1 : -1;
|
|
107
|
-
|
|
108
|
-
return b.registryItemCount - a.registryItemCount;
|
|
109
|
-
}),
|
|
110
|
-
});
|
|
85
|
+
const publicRegistries = await (async () => {
|
|
86
|
+
try {
|
|
87
|
+
const registries = await Promise.all(
|
|
88
|
+
(await fetchAvailableRegistries({ baseUrl })).map(async ({ id }) =>
|
|
89
|
+
fetchRegistry({ baseUrl, registryId: id }),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
stop("Registry를 가져왔어요.");
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
94
|
+
return registries;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
stop("Registry를 가져오지 못했어요.");
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
116
100
|
|
|
117
|
-
|
|
118
|
-
|
|
101
|
+
const selectedItemKeys: string[] = await (async () => {
|
|
102
|
+
if (options.itemIds?.length) {
|
|
103
|
+
return options.itemIds;
|
|
104
|
+
}
|
|
119
105
|
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
const selected = await p.multiselect({
|
|
107
|
+
message: "추가할 항목을 선택해주세요 (스페이스 바로 여러 개 선택 가능)",
|
|
108
|
+
options: publicRegistries
|
|
109
|
+
.filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
|
|
110
|
+
.flatMap(({ id: registryId, items }) =>
|
|
111
|
+
items
|
|
112
|
+
.filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
|
|
113
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
114
|
+
.map(({ id, description, deprecated }) => ({
|
|
115
|
+
label: `${deprecated ? "(deprecated) " : ""}${highlight(registryId)}:${id}`,
|
|
116
|
+
value: `${registryId}:${id}`,
|
|
117
|
+
hint: description,
|
|
118
|
+
|
|
119
|
+
// used for sorting
|
|
120
|
+
deprecated,
|
|
121
|
+
registryItemCount: items.length,
|
|
122
|
+
})),
|
|
123
|
+
)
|
|
124
|
+
.sort((a, b) => {
|
|
125
|
+
if (a.deprecated !== b.deprecated) return a.deprecated ? 1 : -1;
|
|
126
|
+
|
|
127
|
+
return b.registryItemCount - a.registryItemCount;
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
if (p.isCancel(selected)) {
|
|
132
|
+
throw new CliCancelError();
|
|
133
|
+
}
|
|
125
134
|
|
|
126
|
-
|
|
135
|
+
return selected;
|
|
136
|
+
})();
|
|
127
137
|
|
|
128
|
-
|
|
138
|
+
if (!selectedItemKeys?.length) {
|
|
139
|
+
throw new CliCancelError("추가할 항목이 선택되지 않았어요.");
|
|
140
|
+
}
|
|
129
141
|
|
|
130
|
-
|
|
131
|
-
const [registryId, ...rest] = itemKey.split(":");
|
|
132
|
-
const itemId = rest.join(":");
|
|
142
|
+
p.log.message(`선택된 항목: ${highlight(selectedItemKeys.join(", "))}`);
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
p.log.error(
|
|
136
|
-
`${highlight(itemKey)}: 항목 이름이 잘못되었어요. ${highlight("ui:action-button")}과 같은 형식으로 입력해보세요.`,
|
|
137
|
-
);
|
|
144
|
+
const filteredItemKeys: string[] = [];
|
|
138
145
|
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
for (const itemKey of selectedItemKeys) {
|
|
147
|
+
const [registryId, ...rest] = itemKey.split(":");
|
|
148
|
+
const itemId = rest.join(":");
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
if (!registryId || !itemId) {
|
|
151
|
+
throw new CliError({
|
|
152
|
+
message: `${highlight(itemKey)}: 항목 이름이 잘못되었어요.`,
|
|
153
|
+
hint: `${highlight("ui:action-button")}과 같은 형식으로 입력해보세요.`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
145
156
|
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
const foundItem = publicRegistries
|
|
158
|
+
.find((r) => r.id === registryId)
|
|
159
|
+
?.items.find((i) => i.id === itemId);
|
|
148
160
|
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
if (!foundItem) {
|
|
162
|
+
throw new CliError({
|
|
163
|
+
message: `${highlight(itemKey)}: 항목을 찾을 수 없어요.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
151
166
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
if (foundItem.deprecated) {
|
|
168
|
+
const confirm = await p.confirm({
|
|
169
|
+
message: `${highlight(foundItem.id)}: deprecated 되었어요. 추가할까요?`,
|
|
170
|
+
initialValue: false,
|
|
171
|
+
});
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
if (p.isCancel(confirm)) {
|
|
174
|
+
throw new CliCancelError();
|
|
175
|
+
}
|
|
160
176
|
|
|
161
|
-
|
|
177
|
+
if (confirm === false) {
|
|
178
|
+
p.log.info(`${highlight(foundItem.id)}: 추가하지 않을게요.`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
162
181
|
}
|
|
182
|
+
|
|
183
|
+
filteredItemKeys.push(itemKey);
|
|
163
184
|
}
|
|
164
185
|
|
|
165
|
-
|
|
166
|
-
|
|
186
|
+
const { registryItemsToAdd, npmDependenciesToAdd } = resolveDependencies({
|
|
187
|
+
selectedItemKeys: filteredItemKeys,
|
|
188
|
+
publicRegistries,
|
|
189
|
+
});
|
|
167
190
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
const compatibilityReport = analyzeRegistryItemCompatibility({
|
|
192
|
+
publicRegistries,
|
|
193
|
+
itemKeys: registryItemsToAdd.flatMap(({ registryId, items }) =>
|
|
194
|
+
items.map((item) => `${registryId}:${item.id}`),
|
|
195
|
+
),
|
|
196
|
+
projectPackageVersions: getProjectSeedPackageVersionSpecs(options.cwd),
|
|
197
|
+
});
|
|
172
198
|
|
|
173
|
-
|
|
174
|
-
|
|
199
|
+
logCompatibilityReport({
|
|
200
|
+
report: compatibilityReport,
|
|
201
|
+
title: "현재 프로젝트 버전과 호환되지 않을 수 있는 스니펫이 있어요.",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
p.log.info(
|
|
205
|
+
`추가할 항목: ${highlight(registryItemsToAdd.map((r) => r.items.map((i) => `${r.registryId}:${i.id}`).join(", ")).join(", ") || "없음")}
|
|
175
206
|
|
|
176
207
|
설치할 의존성: ${highlight(Array.from(npmDependenciesToAdd).join(", ") || "없음")}`,
|
|
177
|
-
|
|
208
|
+
);
|
|
178
209
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
210
|
+
await writeRegistryItemSnippets({
|
|
211
|
+
registryItemsToAdd,
|
|
212
|
+
rootPath,
|
|
213
|
+
cwd,
|
|
214
|
+
baseUrl,
|
|
215
|
+
config,
|
|
216
|
+
onDiff: options.onDiff,
|
|
217
|
+
});
|
|
187
218
|
|
|
188
|
-
try {
|
|
189
219
|
const { installed, filtered } = await installDependencies({
|
|
190
220
|
cwd,
|
|
191
221
|
deps: Array.from(npmDependenciesToAdd),
|
|
@@ -205,31 +235,46 @@ export const addCommand = (cli: CAC) => {
|
|
|
205
235
|
}
|
|
206
236
|
}
|
|
207
237
|
p.outro("완료했어요.");
|
|
238
|
+
|
|
239
|
+
// add 성공 이벤트 추적
|
|
240
|
+
const duration = Date.now() - startTime;
|
|
241
|
+
const uniqueRegistries = new Set(registryItemsToAdd.map((r) => r.registryId));
|
|
242
|
+
const hasDeprecated = selectedItemKeys.some((itemKey) => {
|
|
243
|
+
const [registryId, ...rest] = itemKey.split(":");
|
|
244
|
+
const itemId = rest.join(":");
|
|
245
|
+
return publicRegistries
|
|
246
|
+
.find((r) => r.id === registryId)
|
|
247
|
+
?.items.find((i) => i.id === itemId)?.deprecated;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await analytics.track(options.cwd, {
|
|
252
|
+
event: "add",
|
|
253
|
+
properties: {
|
|
254
|
+
items_count: filteredItemKeys.length,
|
|
255
|
+
registries: Array.from(uniqueRegistries),
|
|
256
|
+
has_deprecated: hasDeprecated,
|
|
257
|
+
dependencies_count: npmDependenciesToAdd.size,
|
|
258
|
+
duration_ms: duration,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
} catch (telemetryError) {
|
|
262
|
+
if (verbose) {
|
|
263
|
+
console.error("[Telemetry] add tracking failed:", telemetryError);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
208
266
|
} catch (error) {
|
|
209
|
-
|
|
210
|
-
|
|
267
|
+
if (isCliCancelError(error)) {
|
|
268
|
+
p.outro(highlight(error.message));
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
handleCliError(error, {
|
|
273
|
+
defaultMessage: "추가에 실패했어요.",
|
|
274
|
+
defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
|
|
275
|
+
verbose,
|
|
276
|
+
});
|
|
211
277
|
process.exit(1);
|
|
212
278
|
}
|
|
213
|
-
|
|
214
|
-
// add 성공 이벤트 추적
|
|
215
|
-
const duration = Date.now() - startTime;
|
|
216
|
-
const uniqueRegistries = new Set(registryItemsToAdd.map((r) => r.registryId));
|
|
217
|
-
const hasDeprecated = selectedItemKeys.some((itemKey) => {
|
|
218
|
-
const [registryId, ...rest] = itemKey.split(":");
|
|
219
|
-
const itemId = rest.join(":");
|
|
220
|
-
return publicRegistries.find((r) => r.id === registryId)?.items.find((i) => i.id === itemId)
|
|
221
|
-
?.deprecated;
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
await analytics.track(options.cwd, {
|
|
225
|
-
event: "add",
|
|
226
|
-
properties: {
|
|
227
|
-
items_count: filteredItemKeys.length,
|
|
228
|
-
registries: Array.from(uniqueRegistries),
|
|
229
|
-
has_deprecated: hasDeprecated,
|
|
230
|
-
dependencies_count: npmDependenciesToAdd.size,
|
|
231
|
-
duration_ms: duration,
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
279
|
});
|
|
235
280
|
};
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { fetchAvailableRegistries, fetchRegistry } from "@/src/utils/fetch";
|
|
2
|
+
import { getRawConfig } from "@/src/utils/get-config";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import type { CAC } from "cac";
|
|
8
|
+
import { BASE_URL } from "../constants";
|
|
9
|
+
import { analytics } from "../utils/analytics";
|
|
10
|
+
import { highlight } from "../utils/color";
|
|
11
|
+
import {
|
|
12
|
+
analyzeRegistryItemCompatibility,
|
|
13
|
+
findInstalledSnippetItemKeys,
|
|
14
|
+
getProjectSeedPackageVersionSpecs,
|
|
15
|
+
logCompatibilityReport,
|
|
16
|
+
} from "../utils/compatibility";
|
|
17
|
+
import { CliError, handleCliError, isCliCancelError, isVerboseMode } from "../utils/error";
|
|
18
|
+
|
|
19
|
+
const compatOptionsSchema = z.object({
|
|
20
|
+
itemIds: z.array(z.string()).optional(),
|
|
21
|
+
component: z.union([z.string(), z.array(z.string())]).optional(),
|
|
22
|
+
all: z.boolean(),
|
|
23
|
+
registry: z.string().optional(),
|
|
24
|
+
cwd: z.string(),
|
|
25
|
+
baseUrl: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function parseTargetInputs({
|
|
29
|
+
itemIds,
|
|
30
|
+
component,
|
|
31
|
+
}: {
|
|
32
|
+
itemIds?: string[];
|
|
33
|
+
component?: string | string[];
|
|
34
|
+
}) {
|
|
35
|
+
const normalizeInput = (value: string) => value.trim().replace(/\s+/g, "-");
|
|
36
|
+
const itemInputs = (itemIds ?? []).map(normalizeInput).filter(Boolean);
|
|
37
|
+
const componentInputs = (Array.isArray(component) ? component : [component])
|
|
38
|
+
.filter((value): value is string => !!value)
|
|
39
|
+
.flatMap((value) => value.split(","))
|
|
40
|
+
.map(normalizeInput)
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
|
|
43
|
+
return Array.from(new Set([...itemInputs, ...componentInputs]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveExplicitItemKeys({
|
|
47
|
+
publicRegistries,
|
|
48
|
+
targetInputs,
|
|
49
|
+
defaultRegistry,
|
|
50
|
+
}: {
|
|
51
|
+
publicRegistries: Array<{ id?: string; items?: Array<{ id?: string }> }>;
|
|
52
|
+
targetInputs: string[];
|
|
53
|
+
defaultRegistry?: string;
|
|
54
|
+
}) {
|
|
55
|
+
const allItemKeys = publicRegistries
|
|
56
|
+
.filter((registry): registry is { id: string; items: Array<{ id: string }> } => {
|
|
57
|
+
return typeof registry.id === "string" && Array.isArray(registry.items);
|
|
58
|
+
})
|
|
59
|
+
.flatMap((registry) =>
|
|
60
|
+
registry.items
|
|
61
|
+
.filter((item): item is { id: string } => typeof item.id === "string")
|
|
62
|
+
.map((item) => `${registry.id}:${item.id}`),
|
|
63
|
+
);
|
|
64
|
+
const result = new Set<string>();
|
|
65
|
+
|
|
66
|
+
for (const input of targetInputs) {
|
|
67
|
+
const itemKey = input.includes(":")
|
|
68
|
+
? input
|
|
69
|
+
: defaultRegistry
|
|
70
|
+
? `${defaultRegistry}:${input}`
|
|
71
|
+
: (() => {
|
|
72
|
+
const matchedItemKeys = allItemKeys.filter((itemKey) => itemKey.endsWith(`:${input}`));
|
|
73
|
+
if (!matchedItemKeys.length) {
|
|
74
|
+
throw new CliError({
|
|
75
|
+
message: `${highlight(input)}: 항목을 찾을 수 없어요.`,
|
|
76
|
+
hint: `${highlight("ui:action-button")}처럼 registry를 포함해서 입력해보세요.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (matchedItemKeys.length > 1) {
|
|
81
|
+
throw new CliError({
|
|
82
|
+
message: `${highlight(input)}: 같은 이름의 항목이 여러 레지스트리에 있어요.`,
|
|
83
|
+
details: matchedItemKeys.map((itemKey) => `- ${itemKey}`),
|
|
84
|
+
hint: `${highlight("ui:action-button")}처럼 registry를 포함해서 입력해보세요.`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return matchedItemKeys[0];
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
if (!allItemKeys.includes(itemKey)) {
|
|
92
|
+
throw new CliError({
|
|
93
|
+
message: `${highlight(itemKey)}: 항목을 찾을 수 없어요.`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
result.add(itemKey);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Array.from(result);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const compatCommand = (cli: CAC) => {
|
|
104
|
+
cli
|
|
105
|
+
.command("compat [...item-ids]", "check snippet compatibility")
|
|
106
|
+
.option("-c, --component <component>", "검사할 컴포넌트. 여러 번 또는 쉼표로 지정 가능")
|
|
107
|
+
.option("-a, --all", "모든 registry 항목을 검사", {
|
|
108
|
+
default: false,
|
|
109
|
+
})
|
|
110
|
+
.option("-r, --registry <registryId>", "컴포넌트 shorthand 입력 시 기본 registry")
|
|
111
|
+
.option("--cwd <cwd>", "the working directory. defaults to the current directory.", {
|
|
112
|
+
default: process.cwd(),
|
|
113
|
+
})
|
|
114
|
+
.option(
|
|
115
|
+
"-u, --baseUrl <baseUrl>",
|
|
116
|
+
"the base url of the registry. defaults to the current directory.",
|
|
117
|
+
{ default: BASE_URL },
|
|
118
|
+
)
|
|
119
|
+
.example("seed-design compat")
|
|
120
|
+
.example("seed-design compat -c action-button")
|
|
121
|
+
.example("seed-design compat ui:action-button ui:alert-dialog")
|
|
122
|
+
.example("seed-design compat --all")
|
|
123
|
+
.action(async (itemIds, opts) => {
|
|
124
|
+
const startTime = Date.now();
|
|
125
|
+
const verbose = isVerboseMode(opts);
|
|
126
|
+
p.intro("seed-design compat");
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const parsed = compatOptionsSchema.safeParse({ itemIds, ...opts });
|
|
130
|
+
if (!parsed.success) {
|
|
131
|
+
throw parsed.error;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { data: options } = parsed;
|
|
135
|
+
const { start, stop } = p.spinner();
|
|
136
|
+
|
|
137
|
+
start("Registry를 가져오고 있어요...");
|
|
138
|
+
const publicRegistries = await (async () => {
|
|
139
|
+
try {
|
|
140
|
+
const registries = await Promise.all(
|
|
141
|
+
(await fetchAvailableRegistries({ baseUrl: options.baseUrl })).map(async ({ id }) =>
|
|
142
|
+
fetchRegistry({ baseUrl: options.baseUrl, registryId: id }),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
stop("Registry를 가져왔어요.");
|
|
146
|
+
return registries;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
stop("Registry를 가져오지 못했어요.");
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
|
|
153
|
+
const targetInputs = parseTargetInputs({
|
|
154
|
+
itemIds: options.itemIds,
|
|
155
|
+
component: options.component,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const targetItemKeys = (() => {
|
|
159
|
+
if (options.all) {
|
|
160
|
+
return publicRegistries.flatMap((registry) =>
|
|
161
|
+
registry.items.map((item) => `${registry.id}:${item.id}`),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (targetInputs.length > 0) {
|
|
166
|
+
return resolveExplicitItemKeys({
|
|
167
|
+
publicRegistries,
|
|
168
|
+
targetInputs,
|
|
169
|
+
defaultRegistry: options.registry,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const rawConfigPromise = getRawConfig(options.cwd);
|
|
174
|
+
return rawConfigPromise;
|
|
175
|
+
})();
|
|
176
|
+
|
|
177
|
+
const resolvedTargetItemKeys = Array.isArray(targetItemKeys)
|
|
178
|
+
? targetItemKeys
|
|
179
|
+
: await (async () => {
|
|
180
|
+
const rawConfig = await targetItemKeys;
|
|
181
|
+
if (!rawConfig) {
|
|
182
|
+
throw new CliError({
|
|
183
|
+
message: "seed-design.json 파일이 없어 설치된 스니펫 경로를 알 수 없어요.",
|
|
184
|
+
hint: "`seed-design init`으로 설정을 만든 뒤 실행하거나, `--all`/`-c`로 검사 대상을 직접 지정해주세요.",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rootPath = path.resolve(options.cwd, rawConfig.path);
|
|
189
|
+
const installedItemKeys = findInstalledSnippetItemKeys({
|
|
190
|
+
publicRegistries,
|
|
191
|
+
rootPath,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!installedItemKeys.length) {
|
|
195
|
+
p.log.info(
|
|
196
|
+
`${highlight(path.relative(options.cwd, rootPath) || rawConfig.path)}에서 설치된 스니펫을 찾지 못했어요.`,
|
|
197
|
+
);
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return installedItemKeys;
|
|
202
|
+
})();
|
|
203
|
+
|
|
204
|
+
if (!resolvedTargetItemKeys.length) {
|
|
205
|
+
p.outro("검사할 스니펫이 없어요.");
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const projectPackageVersions = getProjectSeedPackageVersionSpecs(options.cwd);
|
|
210
|
+
const compatibilityReport = analyzeRegistryItemCompatibility({
|
|
211
|
+
publicRegistries,
|
|
212
|
+
itemKeys: resolvedTargetItemKeys,
|
|
213
|
+
projectPackageVersions,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
p.log.info(`검사 대상: ${highlight(compatibilityReport.checkedItemKeys.join(", "))}`);
|
|
217
|
+
|
|
218
|
+
if (!compatibilityReport.issues.length) {
|
|
219
|
+
p.outro("모든 스니펫이 현재 @seed-design/react, @seed-design/css와 호환돼요.");
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await analytics.track(options.cwd, {
|
|
223
|
+
event: "compat",
|
|
224
|
+
properties: {
|
|
225
|
+
checked_items_count: compatibilityReport.checkedItemKeys.length,
|
|
226
|
+
incompatible_items_count: 0,
|
|
227
|
+
duration_ms: Date.now() - startTime,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
} catch (telemetryError) {
|
|
231
|
+
if (verbose) {
|
|
232
|
+
console.error("[Telemetry] compat tracking failed:", telemetryError);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
logCompatibilityReport({
|
|
240
|
+
report: compatibilityReport,
|
|
241
|
+
title: "현재 프로젝트 버전과 호환되지 않는 스니펫을 찾았어요.",
|
|
242
|
+
});
|
|
243
|
+
p.log.info(
|
|
244
|
+
"필요한 버전으로 @seed-design/react 또는 @seed-design/css를 맞춘 뒤 다시 실행해보세요.",
|
|
245
|
+
);
|
|
246
|
+
p.outro("호환성 이슈가 있어요.");
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await analytics.track(options.cwd, {
|
|
250
|
+
event: "compat",
|
|
251
|
+
properties: {
|
|
252
|
+
checked_items_count: compatibilityReport.checkedItemKeys.length,
|
|
253
|
+
incompatible_items_count: new Set(
|
|
254
|
+
compatibilityReport.issues.map((issue) => issue.itemKey),
|
|
255
|
+
).size,
|
|
256
|
+
issue_count: compatibilityReport.issues.length,
|
|
257
|
+
duration_ms: Date.now() - startTime,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
} catch (telemetryError) {
|
|
261
|
+
if (verbose) {
|
|
262
|
+
console.error("[Telemetry] compat tracking failed:", telemetryError);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
process.exit(1);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (isCliCancelError(error)) {
|
|
269
|
+
p.outro(highlight(error.message));
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
handleCliError(error, {
|
|
274
|
+
defaultMessage: "호환성 검사에 실패했어요.",
|
|
275
|
+
defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
|
|
276
|
+
verbose,
|
|
277
|
+
});
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
};
|