@seed-design/cli 1.2.0 → 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 +162 -115
- package/src/commands/add.ts +183 -140
- 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/utils/write.ts +26 -24
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(),
|
|
@@ -20,7 +32,7 @@ const addOptionsSchema = z.object({
|
|
|
20
32
|
all: z.boolean(),
|
|
21
33
|
cwd: z.string(),
|
|
22
34
|
baseUrl: z.string().optional(),
|
|
23
|
-
|
|
35
|
+
onDiff: z.enum(["overwrite", "backup"]).optional(),
|
|
24
36
|
});
|
|
25
37
|
|
|
26
38
|
export const addCommand = (cli: CAC) => {
|
|
@@ -37,157 +49,173 @@ export const addCommand = (cli: CAC) => {
|
|
|
37
49
|
"the base url of the registry. defaults to the current directory.",
|
|
38
50
|
{ default: BASE_URL },
|
|
39
51
|
)
|
|
40
|
-
.option("--
|
|
41
|
-
default: false,
|
|
42
|
-
})
|
|
52
|
+
.option("--on-diff <mode>", "Action when file differs: overwrite or backup")
|
|
43
53
|
.example("seed-design add ui:action-button")
|
|
44
54
|
.example("seed-design add ui:alert-dialog")
|
|
45
55
|
.action(async (itemIds, opts) => {
|
|
46
56
|
const startTime = Date.now();
|
|
57
|
+
const verbose = isVerboseMode(opts);
|
|
47
58
|
p.intro("seed-design add");
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
try {
|
|
61
|
+
const parsed = addOptionsSchema.safeParse({ itemIds, ...opts });
|
|
62
|
+
if (!parsed.success) {
|
|
63
|
+
throw parsed.error;
|
|
64
|
+
}
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
const {
|
|
67
|
+
data: { all, ...options },
|
|
68
|
+
} = parsed;
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
if (all) {
|
|
71
|
+
throw new CliError({
|
|
72
|
+
message:
|
|
73
|
+
"`--all` 옵션은 더 이상 지원되지 않아요. 대신 `seed-design add-all` 명령어를 사용해주세요.",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
77
|
+
const cwd = options.cwd;
|
|
78
|
+
const baseUrl = options.baseUrl;
|
|
79
|
+
const config = await getConfig(cwd);
|
|
80
|
+
const rootPath = path.resolve(cwd, config.path);
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
const { start, stop } = p.spinner();
|
|
83
|
+
start("Registry를 가져오고 있어요...");
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const publicRegistries = await Promise.all(
|
|
79
|
-
(await fetchAvailableRegistries({ baseUrl })).map(async ({ id }) =>
|
|
80
|
-
fetchRegistry({ baseUrl, registryId: id }),
|
|
81
|
-
),
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
stop("Registry를 가져왔어요.");
|
|
85
|
-
|
|
86
|
-
const selectedItemKeys: string[] = await (async () => {
|
|
87
|
-
if (options.itemIds.length > 0) return options.itemIds;
|
|
88
|
-
|
|
89
|
-
const selected = await p.multiselect({
|
|
90
|
-
message: "추가할 항목을 선택해주세요 (스페이스 바로 여러 개 선택 가능)",
|
|
91
|
-
options: publicRegistries
|
|
92
|
-
.filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
|
|
93
|
-
.flatMap(({ id: registryId, items }) =>
|
|
94
|
-
items
|
|
95
|
-
.filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
|
|
96
|
-
.sort((a, b) => a.id.localeCompare(b.id))
|
|
97
|
-
.map(({ id, description, deprecated }) => ({
|
|
98
|
-
label: `${deprecated ? "(deprecated) " : ""}${highlight(registryId)}:${id}`,
|
|
99
|
-
value: `${registryId}:${id}`,
|
|
100
|
-
hint: description,
|
|
101
|
-
|
|
102
|
-
// used for sorting
|
|
103
|
-
deprecated,
|
|
104
|
-
registryItemCount: items.length,
|
|
105
|
-
})),
|
|
106
|
-
)
|
|
107
|
-
.sort((a, b) => {
|
|
108
|
-
if (a.deprecated !== b.deprecated) return a.deprecated ? 1 : -1;
|
|
109
|
-
|
|
110
|
-
return b.registryItemCount - a.registryItemCount;
|
|
111
|
-
}),
|
|
112
|
-
});
|
|
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를 가져왔어요.");
|
|
113
93
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
94
|
+
return registries;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
stop("Registry를 가져오지 못했어요.");
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
118
100
|
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
const selectedItemKeys: string[] = await (async () => {
|
|
102
|
+
if (options.itemIds?.length) {
|
|
103
|
+
return options.itemIds;
|
|
104
|
+
}
|
|
121
105
|
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
});
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
if (p.isCancel(selected)) {
|
|
132
|
+
throw new CliCancelError();
|
|
133
|
+
}
|
|
127
134
|
|
|
128
|
-
|
|
135
|
+
return selected;
|
|
136
|
+
})();
|
|
129
137
|
|
|
130
|
-
|
|
138
|
+
if (!selectedItemKeys?.length) {
|
|
139
|
+
throw new CliCancelError("추가할 항목이 선택되지 않았어요.");
|
|
140
|
+
}
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
const [registryId, ...rest] = itemKey.split(":");
|
|
134
|
-
const itemId = rest.join(":");
|
|
142
|
+
p.log.message(`선택된 항목: ${highlight(selectedItemKeys.join(", "))}`);
|
|
135
143
|
|
|
136
|
-
|
|
137
|
-
p.log.error(
|
|
138
|
-
`${highlight(itemKey)}: 항목 이름이 잘못되었어요. ${highlight("ui:action-button")}과 같은 형식으로 입력해보세요.`,
|
|
139
|
-
);
|
|
144
|
+
const filteredItemKeys: string[] = [];
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
for (const itemKey of selectedItemKeys) {
|
|
147
|
+
const [registryId, ...rest] = itemKey.split(":");
|
|
148
|
+
const itemId = rest.join(":");
|
|
143
149
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
if (!registryId || !itemId) {
|
|
151
|
+
throw new CliError({
|
|
152
|
+
message: `${highlight(itemKey)}: 항목 이름이 잘못되었어요.`,
|
|
153
|
+
hint: `${highlight("ui:action-button")}과 같은 형식으로 입력해보세요.`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
const foundItem = publicRegistries
|
|
158
|
+
.find((r) => r.id === registryId)
|
|
159
|
+
?.items.find((i) => i.id === itemId);
|
|
150
160
|
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
if (!foundItem) {
|
|
162
|
+
throw new CliError({
|
|
163
|
+
message: `${highlight(itemKey)}: 항목을 찾을 수 없어요.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
153
166
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
167
|
+
if (foundItem.deprecated) {
|
|
168
|
+
const confirm = await p.confirm({
|
|
169
|
+
message: `${highlight(foundItem.id)}: deprecated 되었어요. 추가할까요?`,
|
|
170
|
+
initialValue: false,
|
|
171
|
+
});
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
if (p.isCancel(confirm)) {
|
|
174
|
+
throw new CliCancelError();
|
|
175
|
+
}
|
|
162
176
|
|
|
163
|
-
|
|
177
|
+
if (confirm === false) {
|
|
178
|
+
p.log.info(`${highlight(foundItem.id)}: 추가하지 않을게요.`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
164
181
|
}
|
|
182
|
+
|
|
183
|
+
filteredItemKeys.push(itemKey);
|
|
165
184
|
}
|
|
166
185
|
|
|
167
|
-
|
|
168
|
-
|
|
186
|
+
const { registryItemsToAdd, npmDependenciesToAdd } = resolveDependencies({
|
|
187
|
+
selectedItemKeys: filteredItemKeys,
|
|
188
|
+
publicRegistries,
|
|
189
|
+
});
|
|
190
|
+
|
|
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
|
+
});
|
|
169
198
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
logCompatibilityReport({
|
|
200
|
+
report: compatibilityReport,
|
|
201
|
+
title: "현재 프로젝트 버전과 호환되지 않을 수 있는 스니펫이 있어요.",
|
|
202
|
+
});
|
|
174
203
|
|
|
175
|
-
|
|
176
|
-
|
|
204
|
+
p.log.info(
|
|
205
|
+
`추가할 항목: ${highlight(registryItemsToAdd.map((r) => r.items.map((i) => `${r.registryId}:${i.id}`).join(", ")).join(", ") || "없음")}
|
|
177
206
|
|
|
178
207
|
설치할 의존성: ${highlight(Array.from(npmDependenciesToAdd).join(", ") || "없음")}`,
|
|
179
|
-
|
|
208
|
+
);
|
|
180
209
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
await writeRegistryItemSnippets({
|
|
211
|
+
registryItemsToAdd,
|
|
212
|
+
rootPath,
|
|
213
|
+
cwd,
|
|
214
|
+
baseUrl,
|
|
215
|
+
config,
|
|
216
|
+
onDiff: options.onDiff,
|
|
217
|
+
});
|
|
189
218
|
|
|
190
|
-
try {
|
|
191
219
|
const { installed, filtered } = await installDependencies({
|
|
192
220
|
cwd,
|
|
193
221
|
deps: Array.from(npmDependenciesToAdd),
|
|
@@ -207,31 +235,46 @@ export const addCommand = (cli: CAC) => {
|
|
|
207
235
|
}
|
|
208
236
|
}
|
|
209
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
|
+
}
|
|
210
266
|
} catch (error) {
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
});
|
|
213
277
|
process.exit(1);
|
|
214
278
|
}
|
|
215
|
-
|
|
216
|
-
// add 성공 이벤트 추적
|
|
217
|
-
const duration = Date.now() - startTime;
|
|
218
|
-
const uniqueRegistries = new Set(registryItemsToAdd.map((r) => r.registryId));
|
|
219
|
-
const hasDeprecated = selectedItemKeys.some((itemKey) => {
|
|
220
|
-
const [registryId, ...rest] = itemKey.split(":");
|
|
221
|
-
const itemId = rest.join(":");
|
|
222
|
-
return publicRegistries.find((r) => r.id === registryId)?.items.find((i) => i.id === itemId)
|
|
223
|
-
?.deprecated;
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
await analytics.track(options.cwd, {
|
|
227
|
-
event: "add",
|
|
228
|
-
properties: {
|
|
229
|
-
items_count: filteredItemKeys.length,
|
|
230
|
-
registries: Array.from(uniqueRegistries),
|
|
231
|
-
has_deprecated: hasDeprecated,
|
|
232
|
-
dependencies_count: npmDependenciesToAdd.size,
|
|
233
|
-
duration_ms: duration,
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
279
|
});
|
|
237
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
|
+
};
|