@simplysm/sd-cli 13.0.0-beta.45 → 13.0.0-beta.47

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.
Files changed (102) hide show
  1. package/README.md +3 -3
  2. package/dist/builders/BaseBuilder.js.map +0 -1
  3. package/dist/builders/DtsBuilder.js.map +0 -1
  4. package/dist/builders/LibraryBuilder.js.map +0 -1
  5. package/dist/builders/index.js.map +0 -1
  6. package/dist/builders/types.js.map +0 -1
  7. package/dist/capacitor/capacitor.js.map +0 -1
  8. package/dist/commands/add-client.js.map +0 -1
  9. package/dist/commands/add-server.js.map +0 -1
  10. package/dist/commands/build.js.map +0 -1
  11. package/dist/commands/dev.js.map +0 -1
  12. package/dist/commands/device.js.map +0 -1
  13. package/dist/commands/init.js.map +0 -1
  14. package/dist/commands/lint.js.map +0 -1
  15. package/dist/commands/publish.js.map +0 -1
  16. package/dist/commands/typecheck.js.map +0 -1
  17. package/dist/commands/watch.js.map +0 -1
  18. package/dist/electron/electron.js.map +0 -1
  19. package/dist/index.js.map +0 -1
  20. package/dist/infra/ResultCollector.js.map +0 -1
  21. package/dist/infra/SignalHandler.js.map +0 -1
  22. package/dist/infra/WorkerManager.js.map +0 -1
  23. package/dist/infra/index.js.map +0 -1
  24. package/dist/orchestrators/WatchOrchestrator.js.map +0 -1
  25. package/dist/orchestrators/index.js.map +0 -1
  26. package/dist/sd-cli.js.map +0 -1
  27. package/dist/sd-config.types.js.map +0 -1
  28. package/dist/utils/build-env.js.map +0 -1
  29. package/dist/utils/config-editor.js.map +0 -1
  30. package/dist/utils/copy-src.js.map +0 -1
  31. package/dist/utils/esbuild-config.d.ts +1 -0
  32. package/dist/utils/esbuild-config.d.ts.map +1 -1
  33. package/dist/utils/esbuild-config.js +13 -2
  34. package/dist/utils/esbuild-config.js.map +1 -2
  35. package/dist/utils/listr-manager.js.map +0 -1
  36. package/dist/utils/output-utils.js.map +0 -1
  37. package/dist/utils/package-utils.js.map +0 -1
  38. package/dist/utils/replace-deps.js.map +0 -1
  39. package/dist/utils/sd-config.js.map +0 -1
  40. package/dist/utils/spawn.js.map +0 -1
  41. package/dist/utils/tailwind-config-deps.js.map +0 -1
  42. package/dist/utils/template.js.map +0 -1
  43. package/dist/utils/tsconfig.js.map +0 -1
  44. package/dist/utils/typecheck-serialization.js.map +0 -1
  45. package/dist/utils/vite-config.js.map +0 -1
  46. package/dist/utils/worker-events.js.map +0 -1
  47. package/dist/workers/client.worker.js.map +0 -1
  48. package/dist/workers/dts.worker.js.map +0 -1
  49. package/dist/workers/library.worker.js.map +0 -1
  50. package/dist/workers/server-runtime.worker.js.map +0 -1
  51. package/dist/workers/server.worker.js.map +0 -1
  52. package/package.json +6 -4
  53. package/src/builders/BaseBuilder.ts +141 -0
  54. package/src/builders/DtsBuilder.ts +138 -0
  55. package/src/builders/LibraryBuilder.ts +161 -0
  56. package/src/builders/index.ts +4 -0
  57. package/src/builders/types.ts +55 -0
  58. package/src/capacitor/capacitor.ts +827 -0
  59. package/src/commands/add-client.ts +135 -0
  60. package/src/commands/add-server.ts +150 -0
  61. package/src/commands/build.ts +475 -0
  62. package/src/commands/dev.ts +602 -0
  63. package/src/commands/device.ts +151 -0
  64. package/src/commands/init.ts +104 -0
  65. package/src/commands/lint.ts +216 -0
  66. package/src/commands/publish.ts +836 -0
  67. package/src/commands/typecheck.ts +329 -0
  68. package/src/commands/watch.ts +38 -0
  69. package/src/electron/electron.ts +329 -0
  70. package/src/index.ts +1 -0
  71. package/src/infra/ResultCollector.ts +81 -0
  72. package/src/infra/SignalHandler.ts +52 -0
  73. package/src/infra/WorkerManager.ts +65 -0
  74. package/src/infra/index.ts +3 -0
  75. package/src/orchestrators/WatchOrchestrator.ts +211 -0
  76. package/src/orchestrators/index.ts +1 -0
  77. package/src/sd-cli.ts +307 -0
  78. package/src/sd-config.types.ts +271 -0
  79. package/src/utils/build-env.ts +12 -0
  80. package/src/utils/config-editor.ts +131 -0
  81. package/src/utils/copy-src.ts +60 -0
  82. package/src/utils/esbuild-config.ts +263 -0
  83. package/src/utils/listr-manager.ts +89 -0
  84. package/src/utils/output-utils.ts +61 -0
  85. package/src/utils/package-utils.ts +63 -0
  86. package/src/utils/replace-deps.ts +163 -0
  87. package/src/utils/sd-config.ts +44 -0
  88. package/src/utils/spawn.ts +79 -0
  89. package/src/utils/tailwind-config-deps.ts +95 -0
  90. package/src/utils/template.ts +51 -0
  91. package/src/utils/tsconfig.ts +111 -0
  92. package/src/utils/typecheck-serialization.ts +82 -0
  93. package/src/utils/vite-config.ts +184 -0
  94. package/src/utils/worker-events.ts +102 -0
  95. package/src/workers/client.worker.ts +236 -0
  96. package/src/workers/dts.worker.ts +416 -0
  97. package/src/workers/library.worker.ts +245 -0
  98. package/src/workers/server-runtime.worker.ts +154 -0
  99. package/src/workers/server.worker.ts +435 -0
  100. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  101. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  102. package/templates/init/package.json.hbs +3 -3
@@ -0,0 +1,163 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { glob } from "glob";
4
+ import { consola } from "consola";
5
+
6
+ /**
7
+ * replaceDeps 설정의 glob 패턴과 대상 패키지 목록을 매칭하여
8
+ * { targetName, sourcePath } 쌍을 반환한다.
9
+ *
10
+ * @param replaceDeps - sd.config.ts의 replaceDeps 설정 (키: glob 패턴, 값: 소스 경로)
11
+ * @param targetNames - node_modules에서 찾은 패키지 이름 목록 (예: ["@simplysm/solid", ...])
12
+ * @returns 매칭된 { targetName, sourcePath } 배열
13
+ */
14
+ export function resolveReplaceDepEntries(
15
+ replaceDeps: Record<string, string>,
16
+ targetNames: string[],
17
+ ): Array<{ targetName: string; sourcePath: string }> {
18
+ const results: Array<{ targetName: string; sourcePath: string }> = [];
19
+
20
+ for (const [pattern, sourceTemplate] of Object.entries(replaceDeps)) {
21
+ // glob 패턴을 정규식으로 변환: * → (.*), . → \., / → [\\/]
22
+ const regexpText = pattern.replace(/[\\/.+*]/g, (ch) => {
23
+ if (ch === "*") return "(.*)";
24
+ if (ch === ".") return "\\.";
25
+ if (ch === "/" || ch === "\\") return "[\\\\/]";
26
+ if (ch === "+") return "\\+";
27
+ return ch;
28
+ });
29
+ const regex = new RegExp(`^${regexpText}$`);
30
+ const hasWildcard = pattern.includes("*");
31
+
32
+ for (const targetName of targetNames) {
33
+ const match = regex.exec(targetName);
34
+ if (match == null) continue;
35
+
36
+ // 캡처 그룹이 있으면 소스 경로의 *에 치환
37
+ const sourcePath = hasWildcard ? sourceTemplate.replace(/\*/g, match[1]) : sourceTemplate;
38
+
39
+ results.push({ targetName, sourcePath });
40
+ }
41
+ }
42
+
43
+ return results;
44
+ }
45
+
46
+ /**
47
+ * pnpm-workspace.yaml 내용을 파싱하여 workspace packages glob 배열을 반환한다.
48
+ * 별도 YAML 라이브러리 없이 간단한 라인 파싱으로 처리한다.
49
+ *
50
+ * @param content - pnpm-workspace.yaml 파일 내용
51
+ * @returns glob 패턴 배열 (예: ["packages/*", "tools/*"])
52
+ */
53
+ export function parseWorkspaceGlobs(content: string): string[] {
54
+ const lines = content.split("\n");
55
+ const globs: string[] = [];
56
+ let inPackages = false;
57
+
58
+ for (const line of lines) {
59
+ const trimmed = line.trim();
60
+
61
+ if (trimmed === "packages:") {
62
+ inPackages = true;
63
+ continue;
64
+ }
65
+
66
+ // packages 섹션 내의 리스트 항목
67
+ if (inPackages && trimmed.startsWith("- ")) {
68
+ const value = trimmed
69
+ .slice(2)
70
+ .trim()
71
+ .replace(/^["']|["']$/g, "");
72
+ globs.push(value);
73
+ continue;
74
+ }
75
+
76
+ // 다른 섹션이 시작되면 종료
77
+ if (inPackages && trimmed !== "" && !trimmed.startsWith("#")) {
78
+ break;
79
+ }
80
+ }
81
+
82
+ return globs;
83
+ }
84
+
85
+ /**
86
+ * replaceDeps 설정에 따라 node_modules 내 패키지를 소스 디렉토리로 symlink 교체한다.
87
+ *
88
+ * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
89
+ * 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
90
+ * 3. 기존 symlink/디렉토리 제거 → 소스 경로로 symlink 생성
91
+ *
92
+ * @param projectRoot - 프로젝트 루트 경로
93
+ * @param replaceDeps - sd.config.ts의 replaceDeps 설정
94
+ */
95
+ export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<string, string>): Promise<void> {
96
+ const logger = consola.withTag("sd:cli:replace-deps");
97
+
98
+ // 1. Workspace 패키지 경로 목록 수집
99
+ const searchRoots = [projectRoot];
100
+
101
+ const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
102
+ try {
103
+ const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf-8");
104
+ const workspaceGlobs = parseWorkspaceGlobs(yamlContent);
105
+
106
+ for (const pattern of workspaceGlobs) {
107
+ const dirs = await glob(pattern, { cwd: projectRoot, absolute: true });
108
+ searchRoots.push(...dirs);
109
+ }
110
+ } catch {
111
+ // pnpm-workspace.yaml가 없으면 루트만 처리
112
+ }
113
+
114
+ // 2. 각 searchRoot의 node_modules에서 매칭되는 패키지 찾기
115
+ for (const searchRoot of searchRoots) {
116
+ const nodeModulesDir = path.join(searchRoot, "node_modules");
117
+
118
+ try {
119
+ await fs.promises.access(nodeModulesDir);
120
+ } catch {
121
+ continue; // node_modules 없으면 스킵
122
+ }
123
+
124
+ // replaceDeps의 각 glob 패턴으로 node_modules 내 디렉토리 탐색
125
+ const targetNames: string[] = [];
126
+ for (const pattern of Object.keys(replaceDeps)) {
127
+ const matches = await glob(pattern, { cwd: nodeModulesDir });
128
+ targetNames.push(...matches);
129
+ }
130
+
131
+ if (targetNames.length === 0) continue;
132
+
133
+ // 패턴 매칭 및 경로 해석
134
+ const entries = resolveReplaceDepEntries(replaceDeps, targetNames);
135
+
136
+ // 3. Symlink 교체
137
+ for (const { targetName, sourcePath } of entries) {
138
+ const targetPath = path.join(nodeModulesDir, targetName);
139
+ const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
140
+
141
+ // 소스 경로 존재 확인
142
+ try {
143
+ await fs.promises.access(resolvedSourcePath);
144
+ } catch {
145
+ logger.warn(`소스 경로가 존재하지 않아 스킵합니다: ${resolvedSourcePath}`);
146
+ continue;
147
+ }
148
+
149
+ try {
150
+ // 기존 symlink/디렉토리 제거
151
+ await fs.promises.rm(targetPath, { recursive: true, force: true });
152
+
153
+ // 상대 경로로 symlink 생성
154
+ const relativePath = path.relative(path.dirname(targetPath), resolvedSourcePath);
155
+ await fs.promises.symlink(relativePath, targetPath, "dir");
156
+
157
+ logger.info(`${targetName} → ${sourcePath}`);
158
+ } catch (err) {
159
+ logger.error(`symlink 교체 실패 (${targetName}): ${err instanceof Error ? err.message : err}`);
160
+ }
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,44 @@
1
+ import path from "path";
2
+ import { createJiti } from "jiti";
3
+ import { SdError } from "@simplysm/core-common";
4
+ import { fsExists } from "@simplysm/core-node";
5
+ import type { SdConfig } from "../sd-config.types";
6
+
7
+ /**
8
+ * sd.config.ts 로드
9
+ * @returns SdConfig 객체
10
+ * @throws sd.config.ts가 없거나 형식이 잘못된 경우
11
+ */
12
+ export async function loadSdConfig(params: { cwd: string; dev: boolean; opt: string[] }): Promise<SdConfig> {
13
+ const sdConfigPath = path.resolve(params.cwd, "sd.config.ts");
14
+
15
+ if (!(await fsExists(sdConfigPath))) {
16
+ throw new SdError(`sd.config.ts 파일을 찾을 수 없습니다: ${sdConfigPath}`);
17
+ }
18
+
19
+ const jiti = createJiti(import.meta.url);
20
+ const sdConfigModule = await jiti.import(sdConfigPath);
21
+
22
+ if (
23
+ sdConfigModule == null ||
24
+ typeof sdConfigModule !== "object" ||
25
+ !("default" in sdConfigModule) ||
26
+ typeof sdConfigModule.default !== "function"
27
+ ) {
28
+ throw new SdError(`sd.config.ts는 함수를 default export해야 합니다: ${sdConfigPath}`);
29
+ }
30
+
31
+ const config = await sdConfigModule.default(params);
32
+
33
+ if (
34
+ config == null ||
35
+ typeof config !== "object" ||
36
+ !("packages" in config) ||
37
+ config.packages == null ||
38
+ typeof config.packages !== "object" ||
39
+ Array.isArray(config.packages)
40
+ ) {
41
+ throw new SdError(`sd.config.ts의 반환값이 올바른 형식이 아닙니다: ${sdConfigPath}`);
42
+ }
43
+ return config as SdConfig;
44
+ }
@@ -0,0 +1,79 @@
1
+ import { spawn as cpSpawn, type SpawnOptions as CpSpawnOptions } from "child_process";
2
+
3
+ /**
4
+ * spawn 옵션
5
+ */
6
+ export interface SpawnOptions {
7
+ /** 작업 디렉토리 */
8
+ cwd?: string;
9
+ /** 환경변수 (process.env와 병합) */
10
+ env?: Record<string, string | undefined>;
11
+ /** 색상 출력 강제 여부 (기본값: NO_COLOR 환경변수 존중) */
12
+ forceColor?: boolean;
13
+ }
14
+
15
+ /**
16
+ * child_process.spawn을 Promise로 래핑한 함수.
17
+ *
18
+ * - stdout/stderr를 캡처하여 반환
19
+ * - NO_COLOR 환경변수 존중 (forceColor로 오버라이드 가능)
20
+ * - 종료 코드가 0이 아니면 에러 throw
21
+ * - Windows에서도 shell 없이 실행 (보안상 shell: true 사용 안 함)
22
+ *
23
+ * @param cmd - 실행할 명령어
24
+ * @param args - 명령어 인자
25
+ * @param options - 실행 옵션
26
+ * @returns stdout 출력 (stderr는 stdout에 병합됨)
27
+ */
28
+ export async function spawn(cmd: string, args: string[], options?: SpawnOptions): Promise<string> {
29
+ return new Promise<string>((resolve, reject) => {
30
+ // NO_COLOR 환경변수 존중 (https://no-color.org/)
31
+ const noColor = process.env["NO_COLOR"] != null;
32
+ const useColor = options?.forceColor ?? !noColor;
33
+
34
+ const colorEnv = useColor
35
+ ? {
36
+ FORCE_COLOR: "1",
37
+ CLICOLOR_FORCE: "1",
38
+ COLORTERM: "truecolor",
39
+ }
40
+ : {};
41
+
42
+ const spawnOptions: CpSpawnOptions = {
43
+ cwd: options?.cwd,
44
+ env: {
45
+ ...process.env,
46
+ ...colorEnv,
47
+ ...options?.env,
48
+ },
49
+ // shell: false for security (avoid shell injection)
50
+ // Windows .bat/.cmd files are handled by calling cmd.exe explicitly in capacitor.ts
51
+ shell: false,
52
+ stdio: ["ignore", "pipe", "pipe"],
53
+ };
54
+
55
+ const child = cpSpawn(cmd, args, spawnOptions);
56
+
57
+ let output = "";
58
+
59
+ child.stdout?.on("data", (data: Buffer) => {
60
+ output += data.toString();
61
+ });
62
+
63
+ child.stderr?.on("data", (data: Buffer) => {
64
+ output += data.toString();
65
+ });
66
+
67
+ child.on("error", (err) => {
68
+ reject(new Error(`spawn 실패 (${cmd}): ${err.message}`));
69
+ });
70
+
71
+ child.on("close", (code) => {
72
+ if (code === 0) {
73
+ resolve(output);
74
+ } else {
75
+ reject(new Error(`명령어 실패 (${cmd} ${args.join(" ")})\n종료 코드: ${code}\n출력:\n${output}`));
76
+ }
77
+ });
78
+ });
79
+ }
@@ -0,0 +1,95 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const jsExtensions = [".js", ".cjs", ".mjs"];
5
+
6
+ const jsResolutionOrder = ["", ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts", ".jsx", ".tsx"];
7
+ const tsResolutionOrder = ["", ".ts", ".cts", ".mts", ".tsx", ".js", ".cjs", ".mjs", ".jsx"];
8
+
9
+ function resolveWithExtension(file: string, extensions: string[]): string | null {
10
+ for (const ext of extensions) {
11
+ const full = `${file}${ext}`;
12
+ if (fs.existsSync(full) && fs.statSync(full).isFile()) {
13
+ return full;
14
+ }
15
+ }
16
+ for (const ext of extensions) {
17
+ const full = `${file}/index${ext}`;
18
+ if (fs.existsSync(full) && fs.statSync(full).isFile()) {
19
+ return full;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function resolvePackageFile(specifier: string, fromDir: string): string | null {
26
+ const parts = specifier.split("/");
27
+ const pkgName = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
28
+ const subPath = specifier.startsWith("@") ? parts.slice(2).join("/") : parts.slice(1).join("/");
29
+
30
+ let searchDir = fromDir;
31
+ while (true) {
32
+ const candidate = path.join(searchDir, "node_modules", pkgName);
33
+ if (fs.existsSync(candidate)) {
34
+ const realDir = fs.realpathSync(candidate);
35
+ if (subPath) {
36
+ return resolveWithExtension(path.join(realDir, subPath), tsResolutionOrder);
37
+ }
38
+ return resolveWithExtension(path.join(realDir, "index"), tsResolutionOrder);
39
+ }
40
+ const parent = path.dirname(searchDir);
41
+ if (parent === searchDir) break;
42
+ searchDir = parent;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Tailwind config 파일의 의존성을 재귀적으로 수집한다.
49
+ *
50
+ * Tailwind 내장 `getModuleDependencies`는 상대 경로 import만 추적하지만,
51
+ * 이 함수는 지정된 scope의 패키지 경로도 `node_modules` symlink를 풀어 실제 파일을 추적한다.
52
+ */
53
+ export function getTailwindConfigDeps(configPath: string, scopes: string[]): string[] {
54
+ const scopePrefixes = scopes.map((s) => (s.endsWith("/") ? s : s + "/"));
55
+ const seen = new Set<string>();
56
+
57
+ function walk(absoluteFile: string): void {
58
+ if (seen.has(absoluteFile)) return;
59
+ if (!fs.existsSync(absoluteFile)) return;
60
+ seen.add(absoluteFile);
61
+
62
+ const base = path.dirname(absoluteFile);
63
+ const ext = path.extname(absoluteFile);
64
+ const extensions = jsExtensions.includes(ext) ? jsResolutionOrder : tsResolutionOrder;
65
+
66
+ let contents: string;
67
+ try {
68
+ contents = fs.readFileSync(absoluteFile, "utf-8");
69
+ } catch {
70
+ return;
71
+ }
72
+
73
+ for (const match of [
74
+ ...contents.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi),
75
+ ...contents.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),
76
+ ...contents.matchAll(/require\(['"`](.+)['"`]\)/gi),
77
+ ]) {
78
+ const specifier = match[1];
79
+ let resolved: string | null = null;
80
+
81
+ if (specifier.startsWith(".")) {
82
+ resolved = resolveWithExtension(path.resolve(base, specifier), extensions);
83
+ } else if (scopePrefixes.some((p) => specifier.startsWith(p))) {
84
+ resolved = resolvePackageFile(specifier, base);
85
+ }
86
+
87
+ if (resolved != null) {
88
+ walk(resolved);
89
+ }
90
+ }
91
+ }
92
+
93
+ walk(path.resolve(configPath));
94
+ return [...seen];
95
+ }
@@ -0,0 +1,51 @@
1
+ import path from "path";
2
+ import Handlebars from "handlebars";
3
+ import { fsCopy, fsMkdir, fsRead, fsReaddir, fsStat, fsWrite } from "@simplysm/core-node";
4
+
5
+ /**
6
+ * 템플릿 디렉토리를 재귀적으로 순회하며 Handlebars 렌더링 후 파일을 생성한다.
7
+ *
8
+ * - `.hbs` 확장자 파일: Handlebars 컴파일 → `.hbs` 제거한 이름으로 저장
9
+ * - `.hbs` 결과가 빈 문자열/공백만이면: 파일 생성 스킵
10
+ * - 나머지 파일: 바이너리로 그대로 복사
11
+ *
12
+ * @param srcDir - 템플릿 소스 디렉토리
13
+ * @param destDir - 출력 대상 디렉토리
14
+ * @param context - Handlebars 템플릿 변수
15
+ * @param dirReplacements - 디렉토리 이름 치환 맵 (예: `{ __CLIENT__: "client-admin" }`)
16
+ */
17
+ export async function renderTemplateDir(
18
+ srcDir: string,
19
+ destDir: string,
20
+ context: Record<string, unknown>,
21
+ dirReplacements?: Record<string, string>,
22
+ ): Promise<void> {
23
+ await fsMkdir(destDir);
24
+
25
+ const entries = await fsReaddir(srcDir);
26
+
27
+ for (const entry of entries) {
28
+ const srcPath = path.join(srcDir, entry);
29
+ const stat = await fsStat(srcPath);
30
+
31
+ if (stat.isDirectory()) {
32
+ // 디렉토리 이름 치환 적용
33
+ const destName = dirReplacements?.[entry] ?? entry;
34
+ await renderTemplateDir(path.join(srcDir, entry), path.join(destDir, destName), context, dirReplacements);
35
+ } else if (entry.endsWith(".hbs")) {
36
+ // Handlebars 템플릿 렌더링
37
+ const source = await fsRead(srcPath);
38
+ const template = Handlebars.compile(source, { noEscape: true });
39
+ const result = template(context);
40
+
41
+ // 빈 결과면 파일 생성 스킵
42
+ if (result.trim().length === 0) continue;
43
+
44
+ const destFileName = entry.slice(0, -4); // .hbs 제거
45
+ await fsWrite(path.join(destDir, destFileName), result);
46
+ } else {
47
+ // 바이너리 파일은 그대로 복사
48
+ await fsCopy(srcPath, path.join(destDir, entry));
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,111 @@
1
+ import ts from "typescript";
2
+ import path from "path";
3
+ import { fsExists, fsReadJson } from "@simplysm/core-node";
4
+ import { SdError } from "@simplysm/core-common";
5
+
6
+ /**
7
+ * DOM 관련 lib 패턴 - 브라우저 API를 포함하는 lib들
8
+ * node 환경에서 제외되어야 하는 lib을 필터링할 때 사용 (lib.dom.d.ts, lib.webworker.d.ts 등)
9
+ */
10
+ const DOM_LIB_PATTERNS = ["dom", "webworker"] as const;
11
+
12
+ /**
13
+ * 패키지의 package.json에서 @types/* devDependencies를 읽어 types 목록을 반환합니다.
14
+ */
15
+ export async function getTypesFromPackageJson(packageDir: string): Promise<string[]> {
16
+ const packageJsonPath = path.join(packageDir, "package.json");
17
+ if (!(await fsExists(packageJsonPath))) {
18
+ return [];
19
+ }
20
+
21
+ const packageJson = await fsReadJson<{ devDependencies?: Record<string, string> }>(packageJsonPath);
22
+ const devDeps = packageJson.devDependencies ?? {};
23
+
24
+ return Object.keys(devDeps)
25
+ .filter((dep) => dep.startsWith("@types/"))
26
+ .map((dep) => dep.replace("@types/", ""));
27
+ }
28
+
29
+ /**
30
+ * 타입체크 환경
31
+ * - node: DOM lib 제거 + node 타입 추가
32
+ * - browser: node 타입 제거
33
+ * - neutral: DOM lib 유지 + node 타입 추가 (Node/브라우저 공용 패키지용)
34
+ */
35
+ export type TypecheckEnv = "node" | "browser" | "neutral";
36
+
37
+ /**
38
+ * 패키지용 컴파일러 옵션 생성
39
+ *
40
+ * @param baseOptions 루트 tsconfig의 컴파일러 옵션
41
+ * @param env 타입체크 환경 (node: DOM lib 제거 + node 타입 추가, browser: node 타입 제거)
42
+ * @param packageDir 패키지 디렉토리 경로
43
+ *
44
+ * @remarks
45
+ * types 옵션은 baseOptions.types를 무시하고 패키지별로 새로 구성한다.
46
+ * 이는 루트 tsconfig의 전역 타입이 패키지 환경에 맞지 않을 수 있기 때문이다.
47
+ * (예: browser 패키지에 node 타입이 포함되는 것을 방지)
48
+ */
49
+ export async function getCompilerOptionsForPackage(
50
+ baseOptions: ts.CompilerOptions,
51
+ env: TypecheckEnv,
52
+ packageDir: string,
53
+ ): Promise<ts.CompilerOptions> {
54
+ const options = { ...baseOptions };
55
+ const packageTypes = await getTypesFromPackageJson(packageDir);
56
+
57
+ // pnpm 환경: 패키지별 node_modules/@types와 루트 node_modules/@types 모두 검색
58
+ options.typeRoots = [
59
+ path.join(packageDir, "node_modules", "@types"),
60
+ path.join(process.cwd(), "node_modules", "@types"),
61
+ ];
62
+
63
+ switch (env) {
64
+ case "node":
65
+ options.lib = options.lib?.filter(
66
+ (lib) => !DOM_LIB_PATTERNS.some((pattern) => lib.toLowerCase().includes(pattern)),
67
+ );
68
+ options.types = [...new Set([...packageTypes, "node"])];
69
+ break;
70
+ case "browser":
71
+ options.types = packageTypes.filter((t) => t !== "node");
72
+ break;
73
+ case "neutral":
74
+ options.types = [...new Set([...packageTypes, "node"])];
75
+ break;
76
+ }
77
+
78
+ return options;
79
+ }
80
+
81
+ /**
82
+ * 루트 tsconfig 파싱
83
+ * @throws tsconfig.json을 읽거나 파싱할 수 없는 경우
84
+ */
85
+ export function parseRootTsconfig(cwd: string): ts.ParsedCommandLine {
86
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
87
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
88
+
89
+ if (configFile.error) {
90
+ const message = ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
91
+ throw new SdError(`tsconfig.json 읽기 실패: ${message}`);
92
+ }
93
+
94
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, cwd);
95
+
96
+ if (parsed.errors.length > 0) {
97
+ const messages = parsed.errors.map((e) => ts.flattenDiagnosticMessageText(e.messageText, "\n"));
98
+ throw new SdError(`tsconfig.json 파싱 실패: ${messages.join("; ")}`);
99
+ }
100
+
101
+ return parsed;
102
+ }
103
+
104
+ /**
105
+ * 패키지의 소스 파일 목록 가져오기 (tsconfig 기반)
106
+ */
107
+ export function getPackageSourceFiles(pkgDir: string, parsedConfig: ts.ParsedCommandLine): string[] {
108
+ // 경로 구분자까지 포함하여 비교 (packages/core와 packages/core-common 구분)
109
+ const pkgSrcPrefix = path.join(pkgDir, "src") + path.sep;
110
+ return parsedConfig.fileNames.filter((f) => f.startsWith(pkgSrcPrefix));
111
+ }
@@ -0,0 +1,82 @@
1
+ import ts from "typescript";
2
+ import { fsExistsSync, fsReadSync } from "@simplysm/core-node";
3
+
4
+ /**
5
+ * Worker로 전달 가능한 직렬화된 Diagnostic
6
+ */
7
+ export interface SerializedDiagnostic {
8
+ category: number;
9
+ code: number;
10
+ messageText: string;
11
+ file?: {
12
+ fileName: string;
13
+ };
14
+ start?: number;
15
+ length?: number;
16
+ }
17
+
18
+ /**
19
+ * Diagnostic을 직렬화 가능한 형태로 변환
20
+ * (Worker thread 간 structured clone 통신을 위해 순환 참조/함수 제거)
21
+ */
22
+ export function serializeDiagnostic(diagnostic: ts.Diagnostic): SerializedDiagnostic {
23
+ // DiagnosticMessageChain인 경우 전체 체인을 평탄화하여 모든 컨텍스트 정보 보존
24
+ const messageText = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
25
+
26
+ return {
27
+ category: diagnostic.category,
28
+ code: diagnostic.code,
29
+ messageText,
30
+ file: diagnostic.file
31
+ ? {
32
+ fileName: diagnostic.file.fileName,
33
+ }
34
+ : undefined,
35
+ start: diagnostic.start,
36
+ length: diagnostic.length,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * 파일명에서 TypeScript ScriptKind 결정
42
+ */
43
+ function getScriptKind(fileName: string): ts.ScriptKind {
44
+ if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
45
+ if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
46
+ if (fileName.endsWith(".js") || fileName.endsWith(".mjs") || fileName.endsWith(".cjs")) return ts.ScriptKind.JS;
47
+ return ts.ScriptKind.TS;
48
+ }
49
+
50
+ /**
51
+ * SerializedDiagnostic을 ts.Diagnostic으로 복원
52
+ * 실제 파일 내용을 읽어 formatDiagnosticsWithColorAndContext에서 소스 코드 컨텍스트가 표시되도록 함
53
+ * @param serialized 직렬화된 진단 정보
54
+ * @param fileCache 파일 내용 캐시 (동일 파일 중복 읽기 방지)
55
+ * @returns 복원된 ts.Diagnostic 객체
56
+ */
57
+ export function deserializeDiagnostic(serialized: SerializedDiagnostic, fileCache: Map<string, string>): ts.Diagnostic {
58
+ let file: ts.SourceFile | undefined;
59
+ if (serialized.file != null) {
60
+ const fileName = serialized.file.fileName;
61
+
62
+ // 캐시된 파일 내용 가져오기 (없으면 읽어서 캐시)
63
+ // 파일이 삭제되었거나 접근 불가능한 경우 빈 내용으로 처리
64
+ // (소스 코드 컨텍스트는 표시되지 않지만 진단 메시지는 정상 출력됨)
65
+ if (!fileCache.has(fileName)) {
66
+ fileCache.set(fileName, fsExistsSync(fileName) ? fsReadSync(fileName) : "");
67
+ }
68
+ const content = fileCache.get(fileName)!;
69
+
70
+ const scriptKind = getScriptKind(fileName);
71
+ file = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, false, scriptKind);
72
+ }
73
+
74
+ return {
75
+ category: serialized.category,
76
+ code: serialized.code,
77
+ messageText: serialized.messageText,
78
+ file,
79
+ start: serialized.start,
80
+ length: serialized.length,
81
+ };
82
+ }