@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.
@@ -1,9 +1,9 @@
1
1
  import * as p from "@clack/prompts";
2
- import fs from "fs-extra";
3
- import path from "path";
4
2
  import { z } from "zod";
5
3
  import { analytics } from "../utils/analytics";
6
4
  import { highlight } from "../utils/color";
5
+ import { handleCliError, isCliCancelError, isVerboseMode } from "../utils/error";
6
+ import { DEFAULT_INIT_CONFIG, promptInitConfig, writeInitConfigFile } from "../utils/init-config";
7
7
 
8
8
  import type { Config } from "@/src/utils/get-config";
9
9
 
@@ -13,6 +13,7 @@ import dedent from "dedent";
13
13
  const initOptionsSchema = z.object({
14
14
  cwd: z.string(),
15
15
  yes: z.boolean().optional(),
16
+ default: z.boolean().optional(),
16
17
  });
17
18
 
18
19
  export const initCommand = (cli: CAC) => {
@@ -22,64 +23,38 @@ export const initCommand = (cli: CAC) => {
22
23
  default: process.cwd(),
23
24
  })
24
25
  .option("-y, --yes", "모든 질문에 대해 기본값으로 답변합니다.")
26
+ .option("--default", "Deprecated. --yes와 동일하게 기본값으로 생성합니다.")
25
27
  .action(async (opts) => {
26
28
  const startTime = Date.now();
29
+ const verbose = isVerboseMode(opts);
27
30
  p.intro("seed-design.json 파일 생성");
28
31
 
29
- const options = initOptionsSchema.parse(opts);
30
- const isYesOption = options.yes;
31
- let config: Config = {
32
- rsc: false,
33
- tsx: true,
34
- path: "./seed-design",
35
- telemetry: true,
36
- };
37
-
38
- if (!isYesOption) {
39
- const group = await p.group(
40
- {
41
- tsx: () =>
42
- p.confirm({
43
- message: `${highlight("TypeScript")}를 사용중이신가요?`,
44
- initialValue: true,
45
- }),
46
- rsc: () =>
47
- p.confirm({
48
- message: `${highlight("React Server Components")}를 사용중이신가요?`,
49
- initialValue: false,
50
- }),
51
- path: () =>
52
- p.text({
53
- message: `${highlight("seed-design 폴더")} 경로를 입력해주세요. (기본값은 프로젝트 루트에 생성됩니다.)`,
54
- initialValue: "./seed-design",
55
- defaultValue: "./seed-design",
56
- placeholder: "./seed-design",
57
- }),
58
- telemetry: () =>
59
- p.confirm({
60
- message: `개선을 위해 ${highlight("익명 사용 데이터")}를 수집할까요?`,
61
- initialValue: true,
62
- }),
63
- },
64
- {
65
- onCancel: () => {
66
- p.cancel("작업이 취소됐어요.");
67
- process.exit(0);
68
- },
69
- },
70
- );
32
+ try {
33
+ const parsed = initOptionsSchema.safeParse(opts);
34
+ if (!parsed.success) {
35
+ throw parsed.error;
36
+ }
71
37
 
72
- config = {
73
- ...group,
74
- };
75
- }
38
+ const options = parsed.data;
39
+ const isDefaultMode = options.yes || options.default;
40
+ const config: Config = isDefaultMode ? DEFAULT_INIT_CONFIG : await promptInitConfig();
76
41
 
77
- try {
78
42
  const { start, stop } = p.spinner();
79
43
  start("seed-design.json 파일 생성중...");
80
- const targetPath = path.resolve(options.cwd, "seed-design.json");
81
- await fs.writeFile(targetPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
82
- const relativePath = path.relative(process.cwd(), targetPath);
44
+ const relativePath = await (async () => {
45
+ try {
46
+ const result = await writeInitConfigFile({
47
+ cwd: options.cwd,
48
+ config,
49
+ });
50
+
51
+ return result.relativePath;
52
+ } catch (error) {
53
+ stop("seed-design.json 파일 생성이 중단됐어요.");
54
+ throw error;
55
+ }
56
+ })();
57
+
83
58
  stop(`seed-design.json 파일이 ${highlight(relativePath)}에 생성됐어요.`);
84
59
 
85
60
  p.log.info(highlight("seed-design add {component} 명령어로 컴포넌트를 추가해보세요!"));
@@ -99,23 +74,37 @@ export const initCommand = (cli: CAC) => {
99
74
  );
100
75
 
101
76
  p.outro("작업이 완료됐어요.");
77
+
78
+ // init 성공 이벤트 추적
79
+ const duration = Date.now() - startTime;
80
+ try {
81
+ await analytics.track(options.cwd, {
82
+ event: "init",
83
+ properties: {
84
+ tsx: config.tsx,
85
+ rsc: config.rsc,
86
+ telemetry: config.telemetry,
87
+ yes_option: isDefaultMode,
88
+ duration_ms: duration,
89
+ },
90
+ });
91
+ } catch (telemetryError) {
92
+ if (verbose) {
93
+ console.error("[Telemetry] init tracking failed:", telemetryError);
94
+ }
95
+ }
102
96
  } catch (error) {
103
- p.log.error(`seed-design.json 파일 생성에 실패했어요. ${error}`);
104
- p.outro(highlight("작업이 취소됐어요."));
97
+ if (isCliCancelError(error)) {
98
+ p.outro(highlight(error.message));
99
+ process.exit(0);
100
+ }
101
+
102
+ handleCliError(error, {
103
+ defaultMessage: "seed-design.json 파일 생성에 실패했어요.",
104
+ defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
105
+ verbose,
106
+ });
105
107
  process.exit(1);
106
108
  }
107
-
108
- // init 성공 이벤트 추적
109
- const duration = Date.now() - startTime;
110
- await analytics.track(options.cwd, {
111
- event: "init",
112
- properties: {
113
- tsx: config.tsx,
114
- rsc: config.rsc,
115
- telemetry: config.telemetry,
116
- yes_option: isYesOption,
117
- duration_ms: duration,
118
- },
119
- });
120
109
  });
121
110
  };
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { addCommand } from "@/src/commands/add";
4
4
  import { addAllCommand } from "@/src/commands/add-all";
5
+ import { compatCommand } from "@/src/commands/compat";
5
6
  import { initCommand } from "@/src/commands/init";
6
7
  import { getPackageInfo } from "@/src/utils/get-package-info";
7
8
  import { cac } from "cac";
@@ -12,9 +13,12 @@ const CLI = cac(NAME);
12
13
  async function main() {
13
14
  const packageInfo = getPackageInfo();
14
15
 
16
+ CLI.option("--verbose", "오류 상세 정보를 출력합니다.");
17
+
15
18
  /* Commands */
16
19
  addCommand(CLI);
17
20
  addAllCommand(CLI);
21
+ compatCommand(CLI);
18
22
  initCommand(CLI);
19
23
 
20
24
  CLI.version(packageInfo.version || "1.0.0", "-v, --version");
package/src/schema.ts CHANGED
@@ -27,7 +27,13 @@ export const publicRegistryItemSchema = z.object({
27
27
 
28
28
  ///////////////////////////////////////////////////////////////
29
29
 
30
- snippets: z.array(z.object({ path: z.string(), content: z.string() })),
30
+ snippets: z.array(
31
+ z.object({
32
+ path: z.string(),
33
+ dependencies: z.record(z.string(), z.string()).optional(),
34
+ content: z.string(),
35
+ }),
36
+ ),
31
37
  });
32
38
 
33
39
  /**
@@ -41,7 +47,14 @@ export const publicRegistrySchema = z.object({
41
47
  items: z.array(
42
48
  publicRegistryItemSchema
43
49
  .omit({ snippets: true })
44
- .extend({ snippets: z.array(z.object({ path: z.string() })) }),
50
+ .extend({
51
+ snippets: z.array(
52
+ z.object({
53
+ path: z.string(),
54
+ dependencies: z.record(z.string(), z.string()).optional(),
55
+ }),
56
+ ),
57
+ }),
45
58
  ),
46
59
  });
47
60
 
@@ -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,4 +1,4 @@
1
- import { describe, it, expect } from "vitest";
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
 
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as p from "@clack/prompts";
3
- import { getConfig } from "./get-config";
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 getConfig(cwd);
27
+ const config = await getRawConfig(cwd);
28
28
  if (config?.telemetry === false) return false;
29
29
  } catch {
30
30
  // 설정 파일이 없거나 읽기 실패 시 기본값 사용
@@ -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
+ }