@seed-design/cli 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.mjs +12 -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/docs.ts +312 -0
- package/src/commands/init.ts +56 -67
- package/src/index.ts +6 -0
- package/src/schema.ts +53 -4
- package/src/tests/compatibility.test.ts +148 -0
- package/src/tests/resolve-dependencies.test.ts +14 -14
- package/src/utils/analytics.ts +3 -4
- package/src/utils/compatibility.ts +291 -0
- package/src/utils/error.ts +160 -0
- package/src/utils/fetch.ts +22 -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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import type { PublicRegistry } from "@/src/schema";
|
|
6
|
+
import {
|
|
7
|
+
analyzeRegistryItemCompatibility,
|
|
8
|
+
findInstalledSnippetItemKeys,
|
|
9
|
+
} from "../utils/compatibility";
|
|
10
|
+
|
|
11
|
+
const registries: PublicRegistry[] = [
|
|
12
|
+
{
|
|
13
|
+
id: "ui",
|
|
14
|
+
items: [
|
|
15
|
+
{
|
|
16
|
+
id: "action-button",
|
|
17
|
+
snippets: [
|
|
18
|
+
{
|
|
19
|
+
path: "action-button.tsx",
|
|
20
|
+
dependencies: {
|
|
21
|
+
"@seed-design/react": "~1.0.0",
|
|
22
|
+
"@seed-design/css": "~1.0.0",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "checkbox",
|
|
29
|
+
snippets: [
|
|
30
|
+
{
|
|
31
|
+
path: "checkbox.tsx",
|
|
32
|
+
dependencies: {
|
|
33
|
+
"@seed-design/react": "~1.2.0",
|
|
34
|
+
"@seed-design/css": "~1.2.0",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
describe("analyzeRegistryItemCompatibility", () => {
|
|
44
|
+
it("정확한 버전이 모두 호환되면 이슈가 없어야 함", () => {
|
|
45
|
+
const report = analyzeRegistryItemCompatibility({
|
|
46
|
+
publicRegistries: registries,
|
|
47
|
+
itemKeys: ["ui:action-button"],
|
|
48
|
+
projectPackageVersions: {
|
|
49
|
+
"@seed-design/react": "1.0.9",
|
|
50
|
+
"@seed-design/css": "1.0.2",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(report.issues).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("요구 범위를 만족하지 못하면 incompatible 이슈를 리턴해야 함", () => {
|
|
58
|
+
const report = analyzeRegistryItemCompatibility({
|
|
59
|
+
publicRegistries: registries,
|
|
60
|
+
itemKeys: ["ui:checkbox"],
|
|
61
|
+
projectPackageVersions: {
|
|
62
|
+
"@seed-design/react": "1.1.0",
|
|
63
|
+
"@seed-design/css": "1.2.1",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(report.issues).toHaveLength(1);
|
|
68
|
+
expect(report.issues[0]).toMatchObject({
|
|
69
|
+
itemKey: "ui:checkbox",
|
|
70
|
+
packageName: "@seed-design/react",
|
|
71
|
+
type: "incompatible-version",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("패키지가 없으면 missing-package 이슈를 리턴해야 함", () => {
|
|
76
|
+
const report = analyzeRegistryItemCompatibility({
|
|
77
|
+
publicRegistries: registries,
|
|
78
|
+
itemKeys: ["ui:action-button"],
|
|
79
|
+
projectPackageVersions: {
|
|
80
|
+
"@seed-design/react": "1.0.9",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(report.issues).toHaveLength(1);
|
|
85
|
+
expect(report.issues[0]).toMatchObject({
|
|
86
|
+
itemKey: "ui:action-button",
|
|
87
|
+
packageName: "@seed-design/css",
|
|
88
|
+
type: "missing-package",
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("workspace range처럼 버전 스펙이 range여도 교집합이 있으면 호환으로 처리해야 함", () => {
|
|
93
|
+
const report = analyzeRegistryItemCompatibility({
|
|
94
|
+
publicRegistries: registries,
|
|
95
|
+
itemKeys: ["ui:action-button"],
|
|
96
|
+
projectPackageVersions: {
|
|
97
|
+
"@seed-design/react": "workspace:^1.0.0",
|
|
98
|
+
"@seed-design/css": "workspace:^1.0.0",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(report.issues).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("해석할 수 없는 버전 스펙이면 invalid-version-spec 이슈를 리턴해야 함", () => {
|
|
106
|
+
const report = analyzeRegistryItemCompatibility({
|
|
107
|
+
publicRegistries: registries,
|
|
108
|
+
itemKeys: ["ui:action-button"],
|
|
109
|
+
projectPackageVersions: {
|
|
110
|
+
"@seed-design/react": "workspace:*",
|
|
111
|
+
"@seed-design/css": "1.0.2",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(report.issues).toHaveLength(1);
|
|
116
|
+
expect(report.issues[0]).toMatchObject({
|
|
117
|
+
itemKey: "ui:action-button",
|
|
118
|
+
packageName: "@seed-design/react",
|
|
119
|
+
type: "invalid-version-spec",
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("findInstalledSnippetItemKeys", () => {
|
|
125
|
+
const tempDirs: string[] = [];
|
|
126
|
+
|
|
127
|
+
afterEach(async () => {
|
|
128
|
+
while (tempDirs.length > 0) {
|
|
129
|
+
const dir = tempDirs.pop();
|
|
130
|
+
if (dir) await fs.remove(dir);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("jsx/js 변환 케이스도 설치된 스니펫으로 인식해야 함", async () => {
|
|
135
|
+
const rootPath = await fs.mkdtemp(path.join(os.tmpdir(), "seed-cli-compat-"));
|
|
136
|
+
tempDirs.push(rootPath);
|
|
137
|
+
|
|
138
|
+
await fs.ensureDir(path.join(rootPath, "ui"));
|
|
139
|
+
await fs.writeFile(path.join(rootPath, "ui", "action-button.jsx"), "export {};");
|
|
140
|
+
|
|
141
|
+
const installed = findInstalledSnippetItemKeys({
|
|
142
|
+
publicRegistries: registries,
|
|
143
|
+
rootPath,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(installed).toEqual(["ui:action-button"]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect } from "
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
2
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
|
-
import {
|
|
3
|
+
import { getRawConfig } from "./get-config";
|
|
4
4
|
|
|
5
5
|
const EVENT_PREFIX = "seed_cli";
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ async function isTelemetryEnabled(cwd: string): Promise<boolean> {
|
|
|
24
24
|
|
|
25
25
|
// 2. seed-design.json 체크
|
|
26
26
|
try {
|
|
27
|
-
const config = await
|
|
27
|
+
const config = await getRawConfig(cwd);
|
|
28
28
|
if (config?.telemetry === false) return false;
|
|
29
29
|
} catch {
|
|
30
30
|
// 설정 파일이 없거나 읽기 실패 시 기본값 사용
|
|
@@ -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
|
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type { PublicRegistry } from "@/src/schema";
|
|
2
|
+
import { getPackageInfo } from "@/src/utils/get-package-info";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { intersects, satisfies, valid, validRange } from "semver";
|
|
7
|
+
import { highlight } from "./color";
|
|
8
|
+
|
|
9
|
+
export const COMPAT_PACKAGE_NAMES = ["@seed-design/react", "@seed-design/css"] as const;
|
|
10
|
+
export type CompatPackageName = (typeof COMPAT_PACKAGE_NAMES)[number];
|
|
11
|
+
|
|
12
|
+
const WORKSPACE_VERSION_PREFIX = "workspace:";
|
|
13
|
+
const NPM_ALIAS_PREFIX = "npm:";
|
|
14
|
+
|
|
15
|
+
export interface CompatibilityIssue {
|
|
16
|
+
itemKey: string;
|
|
17
|
+
packageName: CompatPackageName;
|
|
18
|
+
requiredRanges: string[];
|
|
19
|
+
installedVersionSpec?: string;
|
|
20
|
+
type: "missing-package" | "invalid-version-spec" | "incompatible-version";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CompatibilityReport {
|
|
24
|
+
checkedItemKeys: string[];
|
|
25
|
+
projectPackageVersions: Partial<Record<CompatPackageName, string>>;
|
|
26
|
+
issues: CompatibilityIssue[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getProjectSeedPackageVersionSpecs(
|
|
30
|
+
cwd: string,
|
|
31
|
+
): Partial<Record<CompatPackageName, string>> {
|
|
32
|
+
try {
|
|
33
|
+
const packageInfo = getPackageInfo(cwd);
|
|
34
|
+
const packageDeps = {
|
|
35
|
+
...packageInfo.dependencies,
|
|
36
|
+
...packageInfo.devDependencies,
|
|
37
|
+
...packageInfo.peerDependencies,
|
|
38
|
+
...packageInfo.optionalDependencies,
|
|
39
|
+
};
|
|
40
|
+
const result: Partial<Record<CompatPackageName, string>> = {};
|
|
41
|
+
|
|
42
|
+
for (const packageName of COMPAT_PACKAGE_NAMES) {
|
|
43
|
+
const value = packageDeps[packageName];
|
|
44
|
+
if (typeof value === "string") {
|
|
45
|
+
result[packageName] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
} catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function analyzeRegistryItemCompatibility({
|
|
56
|
+
publicRegistries,
|
|
57
|
+
itemKeys,
|
|
58
|
+
projectPackageVersions,
|
|
59
|
+
}: {
|
|
60
|
+
publicRegistries: PublicRegistry[];
|
|
61
|
+
itemKeys: string[];
|
|
62
|
+
projectPackageVersions: Partial<Record<CompatPackageName, string>>;
|
|
63
|
+
}): CompatibilityReport {
|
|
64
|
+
const checkedItemKeys = Array.from(new Set(itemKeys));
|
|
65
|
+
const itemMap = new Map<string, PublicRegistry["items"][number]>(
|
|
66
|
+
publicRegistries.flatMap((registry) =>
|
|
67
|
+
registry.items.map((item) => [`${registry.id}:${item.id}`, item] as const),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const issues: CompatibilityIssue[] = [];
|
|
72
|
+
|
|
73
|
+
for (const itemKey of checkedItemKeys) {
|
|
74
|
+
const item = itemMap.get(itemKey);
|
|
75
|
+
if (!item) continue;
|
|
76
|
+
|
|
77
|
+
const requiredRangesByPackage = collectRequiredRangesByPackage(item);
|
|
78
|
+
|
|
79
|
+
for (const packageName of COMPAT_PACKAGE_NAMES) {
|
|
80
|
+
const requiredRanges = Array.from(requiredRangesByPackage[packageName] ?? []);
|
|
81
|
+
|
|
82
|
+
if (!requiredRanges.length) continue;
|
|
83
|
+
|
|
84
|
+
const installedVersionSpec = projectPackageVersions[packageName];
|
|
85
|
+
|
|
86
|
+
if (!installedVersionSpec) {
|
|
87
|
+
issues.push({
|
|
88
|
+
itemKey,
|
|
89
|
+
packageName,
|
|
90
|
+
requiredRanges,
|
|
91
|
+
type: "missing-package",
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const normalizedVersionSpec = normalizeVersionSpec(installedVersionSpec);
|
|
97
|
+
|
|
98
|
+
if (!normalizedVersionSpec) {
|
|
99
|
+
issues.push({
|
|
100
|
+
itemKey,
|
|
101
|
+
packageName,
|
|
102
|
+
requiredRanges,
|
|
103
|
+
installedVersionSpec,
|
|
104
|
+
type: "invalid-version-spec",
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isRangeCompatible = requiredRanges.every((requiredRange) =>
|
|
110
|
+
isVersionCompatible({
|
|
111
|
+
currentVersionSpec: normalizedVersionSpec,
|
|
112
|
+
requiredRange,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (!isRangeCompatible) {
|
|
117
|
+
issues.push({
|
|
118
|
+
itemKey,
|
|
119
|
+
packageName,
|
|
120
|
+
requiredRanges,
|
|
121
|
+
installedVersionSpec,
|
|
122
|
+
type: "incompatible-version",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
checkedItemKeys,
|
|
130
|
+
projectPackageVersions,
|
|
131
|
+
issues,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function logCompatibilityReport({
|
|
136
|
+
report,
|
|
137
|
+
title,
|
|
138
|
+
}: {
|
|
139
|
+
report: CompatibilityReport;
|
|
140
|
+
title: string;
|
|
141
|
+
}) {
|
|
142
|
+
if (!report.issues.length) return;
|
|
143
|
+
|
|
144
|
+
p.log.warn(title);
|
|
145
|
+
p.log.info(
|
|
146
|
+
`현재 프로젝트 버전: ${COMPAT_PACKAGE_NAMES.map((packageName) => `${packageName}@${highlight(report.projectPackageVersions[packageName] ?? "미설치")}`).join(", ")}`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const issuesByItem = new Map<string, CompatibilityIssue[]>();
|
|
150
|
+
|
|
151
|
+
for (const issue of report.issues) {
|
|
152
|
+
const found = issuesByItem.get(issue.itemKey) ?? [];
|
|
153
|
+
found.push(issue);
|
|
154
|
+
issuesByItem.set(issue.itemKey, found);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const [itemKey, issues] of issuesByItem.entries()) {
|
|
158
|
+
p.log.warn(highlight(itemKey));
|
|
159
|
+
|
|
160
|
+
for (const issue of issues) {
|
|
161
|
+
const required = issue.requiredRanges.join(" | ");
|
|
162
|
+
|
|
163
|
+
if (issue.type === "missing-package") {
|
|
164
|
+
p.log.info(
|
|
165
|
+
` - ${issue.packageName}: 패키지가 설치되어 있지 않아요. 필요 범위: ${required}`,
|
|
166
|
+
);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (issue.type === "invalid-version-spec") {
|
|
171
|
+
p.log.info(
|
|
172
|
+
` - ${issue.packageName}: 현재 버전 형식을 해석하지 못했어요 (${issue.installedVersionSpec}). 필요 범위: ${required}`,
|
|
173
|
+
);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
p.log.info(
|
|
178
|
+
` - ${issue.packageName}: 현재 ${issue.installedVersionSpec}, 필요 범위 ${required}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function findInstalledSnippetItemKeys({
|
|
185
|
+
publicRegistries,
|
|
186
|
+
rootPath,
|
|
187
|
+
}: {
|
|
188
|
+
publicRegistries: PublicRegistry[];
|
|
189
|
+
rootPath: string;
|
|
190
|
+
}): string[] {
|
|
191
|
+
const installedItemKeys: string[] = [];
|
|
192
|
+
|
|
193
|
+
for (const registry of publicRegistries) {
|
|
194
|
+
for (const item of registry.items) {
|
|
195
|
+
const isInstalled = item.snippets.some((snippet) =>
|
|
196
|
+
getSnippetPathCandidates(snippet.path).some((snippetPath) =>
|
|
197
|
+
fs.existsSync(path.join(rootPath, registry.id, snippetPath)),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (isInstalled) {
|
|
202
|
+
installedItemKeys.push(`${registry.id}:${item.id}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return installedItemKeys;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function collectRequiredRangesByPackage(item: PublicRegistry["items"][number]) {
|
|
211
|
+
const requiredRangesByPackage = Object.fromEntries(
|
|
212
|
+
COMPAT_PACKAGE_NAMES.map((packageName) => [packageName, new Set<string>()]),
|
|
213
|
+
) as Record<CompatPackageName, Set<string>>;
|
|
214
|
+
|
|
215
|
+
for (const snippet of item.snippets) {
|
|
216
|
+
for (const [packageName, requiredRange] of Object.entries(snippet.dependencies ?? {})) {
|
|
217
|
+
if (!isCompatPackageName(packageName)) continue;
|
|
218
|
+
requiredRangesByPackage[packageName].add(requiredRange);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return requiredRangesByPackage;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizeVersionSpec(versionSpec: string): string | null {
|
|
226
|
+
let normalized = versionSpec.trim();
|
|
227
|
+
|
|
228
|
+
if (normalized.startsWith(WORKSPACE_VERSION_PREFIX)) {
|
|
229
|
+
normalized = normalized.slice(WORKSPACE_VERSION_PREFIX.length).trim();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (normalized.startsWith(NPM_ALIAS_PREFIX)) {
|
|
233
|
+
const aliasVersionToken = normalized.split("@").at(-1);
|
|
234
|
+
if (!aliasVersionToken) return null;
|
|
235
|
+
normalized = aliasVersionToken;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!normalized || normalized === "*") return null;
|
|
239
|
+
|
|
240
|
+
if (valid(normalized)) return normalized;
|
|
241
|
+
if (validRange(normalized)) return normalized;
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isVersionCompatible({
|
|
247
|
+
currentVersionSpec,
|
|
248
|
+
requiredRange,
|
|
249
|
+
}: {
|
|
250
|
+
currentVersionSpec: string;
|
|
251
|
+
requiredRange: string;
|
|
252
|
+
}) {
|
|
253
|
+
const normalizedRequiredRange = validRange(requiredRange);
|
|
254
|
+
if (!normalizedRequiredRange) return false;
|
|
255
|
+
|
|
256
|
+
if (valid(currentVersionSpec)) {
|
|
257
|
+
return satisfies(currentVersionSpec, normalizedRequiredRange, {
|
|
258
|
+
includePrerelease: true,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return intersects(currentVersionSpec, normalizedRequiredRange, {
|
|
263
|
+
includePrerelease: true,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getSnippetPathCandidates(originalPath: string): string[] {
|
|
268
|
+
const candidates = new Set([originalPath]);
|
|
269
|
+
|
|
270
|
+
if (originalPath.endsWith(".tsx")) {
|
|
271
|
+
candidates.add(`${originalPath.slice(0, -4)}.jsx`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (originalPath.endsWith(".ts")) {
|
|
275
|
+
candidates.add(`${originalPath.slice(0, -3)}.js`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (originalPath.endsWith(".jsx")) {
|
|
279
|
+
candidates.add(`${originalPath.slice(0, -4)}.tsx`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (originalPath.endsWith(".js")) {
|
|
283
|
+
candidates.add(`${originalPath.slice(0, -3)}.ts`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return Array.from(candidates);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isCompatPackageName(packageName: string): packageName is CompatPackageName {
|
|
290
|
+
return COMPAT_PACKAGE_NAMES.includes(packageName as CompatPackageName);
|
|
291
|
+
}
|