@seed-design/cli 1.2.2 → 1.3.1
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 +23 -9
- package/package.json +1 -1
- package/src/commands/docs.ts +450 -0
- package/src/commands/upgrade.ts +387 -0
- package/src/index.ts +4 -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/fetch.ts +82 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { getPackageInfo } from "@/src/utils/get-package-info";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { coerce, valid } from "semver";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import type { CAC } from "cac";
|
|
7
|
+
import { BASE_URL } from "../constants";
|
|
8
|
+
import { analytics } from "../utils/analytics";
|
|
9
|
+
import { highlight } from "../utils/color";
|
|
10
|
+
import {
|
|
11
|
+
CliCancelError,
|
|
12
|
+
CliError,
|
|
13
|
+
handleCliError,
|
|
14
|
+
isCliCancelError,
|
|
15
|
+
isVerboseMode,
|
|
16
|
+
} from "../utils/error";
|
|
17
|
+
import { fetchChangelog, fetchLatestVersion } from "../utils/fetch";
|
|
18
|
+
|
|
19
|
+
const SEED_SCOPE = "@seed-design/";
|
|
20
|
+
|
|
21
|
+
const upgradeOptionsSchema = z.object({
|
|
22
|
+
packageName: z.string().optional(),
|
|
23
|
+
cwd: z.string(),
|
|
24
|
+
baseUrl: z.string(),
|
|
25
|
+
raw: z.boolean(),
|
|
26
|
+
all: z.boolean(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function toFullPackageName(input: string): string {
|
|
30
|
+
return input.startsWith(SEED_SCOPE) ? input : `${SEED_SCOPE}${input}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toSlug(packageName: string): string {
|
|
34
|
+
return packageName.replace(SEED_SCOPE, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findInstalledSeedPackages(cwd: string): Record<string, string> {
|
|
38
|
+
const packageInfo = getPackageInfo(cwd);
|
|
39
|
+
const allDeps = {
|
|
40
|
+
...packageInfo.dependencies,
|
|
41
|
+
...packageInfo.devDependencies,
|
|
42
|
+
...packageInfo.peerDependencies,
|
|
43
|
+
...packageInfo.optionalDependencies,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const seedPackages: Record<string, string> = {};
|
|
47
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
48
|
+
if (name.startsWith(SEED_SCOPE) && version) {
|
|
49
|
+
seedPackages[name] = version;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return seedPackages;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveExactVersion(versionSpec: string): string | null {
|
|
57
|
+
let normalized = versionSpec.trim();
|
|
58
|
+
|
|
59
|
+
if (normalized.startsWith("workspace:")) {
|
|
60
|
+
normalized = normalized.slice("workspace:".length).trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (normalized.startsWith("npm:")) {
|
|
64
|
+
const lastAt = normalized.lastIndexOf("@");
|
|
65
|
+
if (lastAt > 4) {
|
|
66
|
+
normalized = normalized.slice(lastAt + 1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (valid(normalized)) return normalized;
|
|
71
|
+
|
|
72
|
+
const coerced = coerce(normalized);
|
|
73
|
+
if (coerced) return coerced.version;
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface UpgradeOneResult {
|
|
79
|
+
package: string;
|
|
80
|
+
currentVersion: string;
|
|
81
|
+
latestVersion: string;
|
|
82
|
+
upToDate: boolean;
|
|
83
|
+
changelog: string | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function upgradeOne({
|
|
87
|
+
targetPackage,
|
|
88
|
+
currentVersionSpec,
|
|
89
|
+
baseUrl,
|
|
90
|
+
}: {
|
|
91
|
+
targetPackage: string;
|
|
92
|
+
currentVersionSpec: string;
|
|
93
|
+
baseUrl: string;
|
|
94
|
+
}): Promise<UpgradeOneResult> {
|
|
95
|
+
const currentVersion = resolveExactVersion(currentVersionSpec);
|
|
96
|
+
|
|
97
|
+
if (!currentVersion) {
|
|
98
|
+
throw new CliError({
|
|
99
|
+
message: `${targetPackage}의 버전을 파싱할 수 없어요: ${currentVersionSpec}`,
|
|
100
|
+
hint: "package.json에서 버전 형식을 확인해주세요.",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const latestVersion = await fetchLatestVersion(targetPackage);
|
|
105
|
+
|
|
106
|
+
if (currentVersion === latestVersion) {
|
|
107
|
+
return {
|
|
108
|
+
package: targetPackage,
|
|
109
|
+
currentVersion,
|
|
110
|
+
latestVersion,
|
|
111
|
+
upToDate: true,
|
|
112
|
+
changelog: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const slug = toSlug(targetPackage);
|
|
117
|
+
const changelog = await fetchChangelog({
|
|
118
|
+
baseUrl,
|
|
119
|
+
packageSlug: slug,
|
|
120
|
+
version: currentVersion,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
package: targetPackage,
|
|
125
|
+
currentVersion,
|
|
126
|
+
latestVersion,
|
|
127
|
+
upToDate: false,
|
|
128
|
+
changelog,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printResultRaw(result: UpgradeOneResult & { error?: string }) {
|
|
133
|
+
if (result.error) {
|
|
134
|
+
console.error(`## ${result.package}\n\nError: ${result.error}\n`);
|
|
135
|
+
} else if (result.upToDate) {
|
|
136
|
+
console.log(`${result.package}@${result.currentVersion} is already up to date.\n`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(result.changelog);
|
|
139
|
+
console.log("");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function printResultInteractive(result: UpgradeOneResult & { error?: string }) {
|
|
144
|
+
if (result.error) {
|
|
145
|
+
p.log.error(`${highlight(result.package)}: ${result.error}`);
|
|
146
|
+
} else if (result.upToDate) {
|
|
147
|
+
p.log.info(
|
|
148
|
+
`${highlight(result.package)}: ${highlight(result.currentVersion)} — 이미 최신 버전이에요.`,
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
p.log.info(
|
|
152
|
+
`${highlight(result.package)}: ${highlight(result.currentVersion)} → ${highlight(result.latestVersion)}`,
|
|
153
|
+
);
|
|
154
|
+
p.log.message(result.changelog ?? "");
|
|
155
|
+
p.log.info(
|
|
156
|
+
`업그레이드하려면: ${highlight(`bun add ${result.package}@${result.latestVersion}`)}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function trackResults(cwd: string, results: UpgradeOneResult[], startTime: number) {
|
|
162
|
+
try {
|
|
163
|
+
for (const result of results) {
|
|
164
|
+
await analytics.track(cwd, {
|
|
165
|
+
event: "upgrade",
|
|
166
|
+
properties: {
|
|
167
|
+
package: result.package,
|
|
168
|
+
current_version: result.currentVersion,
|
|
169
|
+
latest_version: result.latestVersion,
|
|
170
|
+
up_to_date: result.upToDate,
|
|
171
|
+
duration_ms: Date.now() - startTime,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const upgradeCommand = (cli: CAC) => {
|
|
179
|
+
cli
|
|
180
|
+
.command(
|
|
181
|
+
"upgrade [package-name]",
|
|
182
|
+
"패키지의 현재 버전과 최신 버전 사이의 변경사항을 확인합니다",
|
|
183
|
+
)
|
|
184
|
+
.option("-c, --cwd <cwd>", "작업 디렉토리. 기본값은 현재 디렉토리입니다.", {
|
|
185
|
+
default: process.cwd(),
|
|
186
|
+
})
|
|
187
|
+
.option("-u, --baseUrl <baseUrl>", "changelog를 조회할 base URL입니다.", { default: BASE_URL })
|
|
188
|
+
.option("--raw", "UI 없이 순수 마크다운만 출력합니다. LLM 파이프에 유용합니다.", {
|
|
189
|
+
default: false,
|
|
190
|
+
})
|
|
191
|
+
.option("-a, --all", "설치된 모든 @seed-design 패키지의 변경사항을 확인합니다.", {
|
|
192
|
+
default: false,
|
|
193
|
+
})
|
|
194
|
+
.example("seed-design upgrade")
|
|
195
|
+
.example("seed-design upgrade react")
|
|
196
|
+
.example("seed-design upgrade --all")
|
|
197
|
+
.example("seed-design upgrade --all --raw")
|
|
198
|
+
.action(async (packageName, opts) => {
|
|
199
|
+
const startTime = Date.now();
|
|
200
|
+
const verbose = isVerboseMode(opts);
|
|
201
|
+
const parsed = upgradeOptionsSchema.safeParse({ packageName, ...opts });
|
|
202
|
+
if (!parsed.success) {
|
|
203
|
+
if (opts.raw) {
|
|
204
|
+
console.error(parsed.error.message);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
p.intro("seed-design upgrade");
|
|
208
|
+
handleCliError(parsed.error, {
|
|
209
|
+
defaultMessage: "업그레이드 확인에 실패했어요.",
|
|
210
|
+
defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
|
|
211
|
+
verbose,
|
|
212
|
+
});
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { data: options } = parsed;
|
|
217
|
+
const { raw, all } = options;
|
|
218
|
+
|
|
219
|
+
if (!raw) p.intro("seed-design upgrade");
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const seedPackages = findInstalledSeedPackages(options.cwd);
|
|
223
|
+
const packageNames = Object.keys(seedPackages);
|
|
224
|
+
|
|
225
|
+
if (packageNames.length === 0) {
|
|
226
|
+
throw new CliError({
|
|
227
|
+
message: "프로젝트에 설치된 @seed-design 패키지를 찾을 수 없어요.",
|
|
228
|
+
hint: "`bun add @seed-design/react`로 패키지를 설치해보세요.",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (options.packageName && all) {
|
|
233
|
+
throw new CliError({
|
|
234
|
+
message: "패키지명과 --all 옵션을 동시에 사용할 수 없어요.",
|
|
235
|
+
hint: "`seed-design upgrade --all` 또는 `seed-design upgrade react` 중 하나만 사용해주세요.",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --all: iterate all packages
|
|
240
|
+
if (all) {
|
|
241
|
+
if (raw) {
|
|
242
|
+
const results = await Promise.all(
|
|
243
|
+
packageNames.map((name) =>
|
|
244
|
+
upgradeOne({
|
|
245
|
+
targetPackage: name,
|
|
246
|
+
currentVersionSpec: seedPackages[name],
|
|
247
|
+
baseUrl: options.baseUrl,
|
|
248
|
+
}).catch((error): UpgradeOneResult & { error: string } => ({
|
|
249
|
+
package: name,
|
|
250
|
+
currentVersion: seedPackages[name],
|
|
251
|
+
latestVersion: "unknown",
|
|
252
|
+
upToDate: false,
|
|
253
|
+
changelog: null,
|
|
254
|
+
error: error instanceof Error ? error.message : String(error),
|
|
255
|
+
})),
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
for (const result of results) {
|
|
260
|
+
printResultRaw(result);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await trackResults(options.cwd, results, startTime);
|
|
264
|
+
const hasErrors = results.some((r) => "error" in r && Boolean(r.error));
|
|
265
|
+
process.exit(hasErrors ? 1 : 0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --all interactive
|
|
269
|
+
const { start, stop } = p.spinner();
|
|
270
|
+
start("모든 패키지의 변경사항을 가져오고 있어요...");
|
|
271
|
+
const results = await Promise.all(
|
|
272
|
+
packageNames.map((name) =>
|
|
273
|
+
upgradeOne({
|
|
274
|
+
targetPackage: name,
|
|
275
|
+
currentVersionSpec: seedPackages[name],
|
|
276
|
+
baseUrl: options.baseUrl,
|
|
277
|
+
}).catch((error): UpgradeOneResult & { error: string } => ({
|
|
278
|
+
package: name,
|
|
279
|
+
currentVersion: seedPackages[name],
|
|
280
|
+
latestVersion: "unknown",
|
|
281
|
+
upToDate: false,
|
|
282
|
+
changelog: null,
|
|
283
|
+
error: error instanceof Error ? error.message : String(error),
|
|
284
|
+
})),
|
|
285
|
+
),
|
|
286
|
+
);
|
|
287
|
+
stop("변경사항을 가져왔어요.");
|
|
288
|
+
|
|
289
|
+
for (const result of results) {
|
|
290
|
+
printResultInteractive(result);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
p.outro("완료했어요.");
|
|
294
|
+
await trackResults(options.cwd, results, startTime);
|
|
295
|
+
const hasErrors = results.some((r) => "error" in r && Boolean(r.error));
|
|
296
|
+
process.exit(hasErrors ? 1 : 0);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// resolve target package
|
|
300
|
+
let targetPackage: string;
|
|
301
|
+
|
|
302
|
+
if (options.packageName) {
|
|
303
|
+
targetPackage = toFullPackageName(options.packageName);
|
|
304
|
+
|
|
305
|
+
if (!seedPackages[targetPackage]) {
|
|
306
|
+
throw new CliError({
|
|
307
|
+
message: `${highlight(targetPackage)}: 프로젝트에 설치되어 있지 않아요.`,
|
|
308
|
+
hint: `설치된 패키지: ${packageNames.map((n) => highlight(n)).join(", ")}`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// no package, no --all: interactive select
|
|
313
|
+
if (raw) {
|
|
314
|
+
throw new CliError({
|
|
315
|
+
message: "--raw 모드에서는 패키지명 또는 --all 옵션이 필요해요.",
|
|
316
|
+
hint: "예: `seed-design upgrade react --raw` 또는 `seed-design upgrade --all --raw`",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (packageNames.length === 1) {
|
|
321
|
+
targetPackage = packageNames[0];
|
|
322
|
+
} else {
|
|
323
|
+
const selected = await p.select({
|
|
324
|
+
message: "변경사항을 확인할 패키지를 선택해주세요",
|
|
325
|
+
options: packageNames.map((name) => ({
|
|
326
|
+
label: name,
|
|
327
|
+
value: name,
|
|
328
|
+
hint: seedPackages[name],
|
|
329
|
+
})),
|
|
330
|
+
});
|
|
331
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
332
|
+
targetPackage = selected;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// single package
|
|
337
|
+
if (raw) {
|
|
338
|
+
const result = await upgradeOne({
|
|
339
|
+
targetPackage,
|
|
340
|
+
currentVersionSpec: seedPackages[targetPackage],
|
|
341
|
+
baseUrl: options.baseUrl,
|
|
342
|
+
});
|
|
343
|
+
printResultRaw(result);
|
|
344
|
+
await trackResults(options.cwd, [result], startTime);
|
|
345
|
+
process.exit(0);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// single package interactive
|
|
349
|
+
const { start, stop } = p.spinner();
|
|
350
|
+
start("최신 버전을 확인하고 있어요...");
|
|
351
|
+
let result: UpgradeOneResult;
|
|
352
|
+
try {
|
|
353
|
+
result = await upgradeOne({
|
|
354
|
+
targetPackage,
|
|
355
|
+
currentVersionSpec: seedPackages[targetPackage],
|
|
356
|
+
baseUrl: options.baseUrl,
|
|
357
|
+
});
|
|
358
|
+
stop("변경사항을 가져왔어요.");
|
|
359
|
+
} catch (error) {
|
|
360
|
+
stop("변경사항을 가져오지 못했어요.");
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
printResultInteractive(result);
|
|
365
|
+
p.outro("완료했어요.");
|
|
366
|
+
await trackResults(options.cwd, [result], startTime);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (isCliCancelError(error)) {
|
|
369
|
+
if (!raw) p.outro(highlight(error.message));
|
|
370
|
+
process.exit(0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (raw) {
|
|
374
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
375
|
+
console.error(msg);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
handleCliError(error, {
|
|
380
|
+
defaultMessage: "업그레이드 확인에 실패했어요.",
|
|
381
|
+
defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
|
|
382
|
+
verbose,
|
|
383
|
+
});
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
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";
|
|
8
|
+
import { upgradeCommand } from "@/src/commands/upgrade";
|
|
7
9
|
import { getPackageInfo } from "@/src/utils/get-package-info";
|
|
8
10
|
import { cac } from "cac";
|
|
9
11
|
|
|
@@ -19,7 +21,9 @@ async function main() {
|
|
|
19
21
|
addCommand(CLI);
|
|
20
22
|
addAllCommand(CLI);
|
|
21
23
|
compatCommand(CLI);
|
|
24
|
+
docsCommand(CLI);
|
|
22
25
|
initCommand(CLI);
|
|
26
|
+
upgradeCommand(CLI);
|
|
23
27
|
|
|
24
28
|
CLI.version(packageInfo.version || "1.0.0", "-v, --version");
|
|
25
29
|
CLI.help();
|
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
|
|
package/src/utils/fetch.ts
CHANGED
|
@@ -7,7 +7,49 @@ import {
|
|
|
7
7
|
publicRegistryItemSchema,
|
|
8
8
|
type PublicAvailableRegistries,
|
|
9
9
|
publicAvailableRegistriesSchema,
|
|
10
|
+
type DocsIndex,
|
|
11
|
+
docsIndexSchema,
|
|
10
12
|
} from "@/src/schema";
|
|
13
|
+
import { CliError } from "@/src/utils/error";
|
|
14
|
+
|
|
15
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
16
|
+
|
|
17
|
+
async function fetchWithTimeout(url: string, timeoutMs = FETCH_TIMEOUT_MS): Promise<Response> {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
20
|
+
try {
|
|
21
|
+
return await fetch(url, { signal: controller.signal });
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
24
|
+
throw new CliError({
|
|
25
|
+
message: `요청 시간이 초과되었어요 (${timeoutMs}ms): ${url}`,
|
|
26
|
+
hint: "네트워크 상태를 확인하고 다시 시도해주세요.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
} finally {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function fetchDocsIndex({ baseUrl }: { baseUrl: string }): Promise<DocsIndex> {
|
|
36
|
+
const response = await fetch(`${baseUrl}/__docs__/index.json`);
|
|
37
|
+
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
throw new CliError({
|
|
40
|
+
message: `문서 목록을 가져오지 못했어요: ${response.status} ${response.statusText}`,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
const { success, data: parsed, error } = docsIndexSchema.safeParse(data);
|
|
45
|
+
|
|
46
|
+
if (!success)
|
|
47
|
+
throw new CliError({
|
|
48
|
+
message: `문서 목록 파싱에 실패했어요: ${error?.message}`,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
11
53
|
|
|
12
54
|
export async function fetchAvailableRegistries({
|
|
13
55
|
baseUrl,
|
|
@@ -79,6 +121,46 @@ async function fetchRegistryItem({
|
|
|
79
121
|
return parsedItem;
|
|
80
122
|
}
|
|
81
123
|
|
|
124
|
+
export async function fetchLatestVersion(packageName: string): Promise<string> {
|
|
125
|
+
const response = await fetchWithTimeout(`https://registry.npmjs.org/${packageName}/latest`);
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new CliError({
|
|
129
|
+
message: `${packageName}의 최신 버전을 가져오지 못했어요: ${response.status} ${response.statusText}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
if (!data || typeof data !== "object" || typeof data.version !== "string") {
|
|
135
|
+
throw new CliError({
|
|
136
|
+
message: `${packageName} 최신 버전 응답 형식이 올바르지 않아요.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return data.version;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function fetchChangelog({
|
|
143
|
+
baseUrl,
|
|
144
|
+
packageSlug,
|
|
145
|
+
version,
|
|
146
|
+
}: {
|
|
147
|
+
baseUrl: string;
|
|
148
|
+
packageSlug: string;
|
|
149
|
+
version: string;
|
|
150
|
+
}): Promise<string> {
|
|
151
|
+
const url = `${baseUrl}/llms/react/updates/changelog/${packageSlug}/${encodeURIComponent(version)}.txt`;
|
|
152
|
+
const response = await fetchWithTimeout(url);
|
|
153
|
+
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new CliError({
|
|
156
|
+
message: `변경사항을 가져오지 못했어요: ${response.status} ${response.statusText}`,
|
|
157
|
+
hint: `${url} 에 접근할 수 있는지 확인해주세요.`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return response.text();
|
|
162
|
+
}
|
|
163
|
+
|
|
82
164
|
export async function fetchRegistryItems({
|
|
83
165
|
baseUrl,
|
|
84
166
|
registryId,
|