@simplysm/sd-cli 13.0.66 → 13.0.68

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 (133) hide show
  1. package/dist/builders/BaseBuilder.d.ts.map +1 -1
  2. package/dist/builders/BaseBuilder.js +2 -7
  3. package/dist/builders/BaseBuilder.js.map +1 -1
  4. package/dist/builders/DtsBuilder.d.ts.map +1 -1
  5. package/dist/builders/DtsBuilder.js +4 -3
  6. package/dist/builders/DtsBuilder.js.map +1 -1
  7. package/dist/builders/LibraryBuilder.d.ts.map +1 -1
  8. package/dist/builders/LibraryBuilder.js +2 -1
  9. package/dist/builders/LibraryBuilder.js.map +1 -1
  10. package/dist/capacitor/capacitor.js +2 -2
  11. package/dist/capacitor/capacitor.js.map +1 -1
  12. package/dist/commands/add-client.js +2 -2
  13. package/dist/commands/add-server.js +2 -2
  14. package/dist/commands/build.d.ts +2 -10
  15. package/dist/commands/build.d.ts.map +1 -1
  16. package/dist/commands/build.js +1 -5
  17. package/dist/commands/build.js.map +1 -1
  18. package/dist/commands/check.d.ts.map +1 -1
  19. package/dist/commands/check.js +26 -37
  20. package/dist/commands/check.js.map +1 -1
  21. package/dist/commands/dev.d.ts +2 -9
  22. package/dist/commands/dev.d.ts.map +1 -1
  23. package/dist/commands/dev.js +1 -5
  24. package/dist/commands/dev.js.map +1 -1
  25. package/dist/commands/init.js +5 -5
  26. package/dist/commands/publish.js +16 -16
  27. package/dist/commands/publish.js.map +1 -1
  28. package/dist/commands/typecheck.d.ts +0 -1
  29. package/dist/commands/typecheck.d.ts.map +1 -1
  30. package/dist/commands/typecheck.js +5 -5
  31. package/dist/commands/typecheck.js.map +1 -1
  32. package/dist/commands/watch.d.ts +2 -8
  33. package/dist/commands/watch.d.ts.map +1 -1
  34. package/dist/commands/watch.js +1 -5
  35. package/dist/commands/watch.js.map +1 -1
  36. package/dist/electron/electron.js +2 -2
  37. package/dist/electron/electron.js.map +1 -1
  38. package/dist/infra/ResultCollector.d.ts +0 -21
  39. package/dist/infra/ResultCollector.d.ts.map +1 -1
  40. package/dist/infra/ResultCollector.js +0 -31
  41. package/dist/infra/ResultCollector.js.map +1 -1
  42. package/dist/orchestrators/BuildOrchestrator.d.ts +0 -1
  43. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  44. package/dist/orchestrators/BuildOrchestrator.js +10 -19
  45. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  46. package/dist/orchestrators/DevOrchestrator.d.ts +12 -0
  47. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  48. package/dist/orchestrators/DevOrchestrator.js +178 -167
  49. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  50. package/dist/sd-cli-entry.js +14 -14
  51. package/dist/sd-cli-entry.js.map +1 -1
  52. package/dist/sd-cli.js +8 -13
  53. package/dist/sd-cli.js.map +1 -1
  54. package/dist/utils/output-utils.d.ts +4 -7
  55. package/dist/utils/output-utils.d.ts.map +1 -1
  56. package/dist/utils/output-utils.js +13 -5
  57. package/dist/utils/output-utils.js.map +1 -1
  58. package/dist/utils/package-utils.d.ts +0 -11
  59. package/dist/utils/package-utils.d.ts.map +1 -1
  60. package/dist/utils/package-utils.js.map +1 -1
  61. package/dist/utils/replace-deps.d.ts.map +1 -1
  62. package/dist/utils/replace-deps.js +21 -48
  63. package/dist/utils/replace-deps.js.map +1 -1
  64. package/dist/utils/sd-config.d.ts +2 -6
  65. package/dist/utils/sd-config.d.ts.map +1 -1
  66. package/dist/utils/sd-config.js.map +1 -1
  67. package/dist/utils/vite-config.d.ts.map +1 -1
  68. package/dist/utils/vite-config.js +7 -1
  69. package/dist/utils/vite-config.js.map +1 -1
  70. package/dist/utils/worker-events.d.ts +5 -5
  71. package/dist/utils/worker-events.d.ts.map +1 -1
  72. package/dist/utils/worker-events.js +14 -17
  73. package/dist/utils/worker-events.js.map +1 -1
  74. package/dist/utils/worker-utils.d.ts +7 -0
  75. package/dist/utils/worker-utils.d.ts.map +1 -1
  76. package/dist/utils/worker-utils.js +10 -0
  77. package/dist/utils/worker-utils.js.map +1 -1
  78. package/dist/workers/client.worker.d.ts.map +1 -1
  79. package/dist/workers/client.worker.js +11 -9
  80. package/dist/workers/client.worker.js.map +1 -1
  81. package/dist/workers/dts.worker.d.ts +4 -4
  82. package/dist/workers/dts.worker.d.ts.map +1 -1
  83. package/dist/workers/dts.worker.js +10 -11
  84. package/dist/workers/dts.worker.js.map +1 -1
  85. package/dist/workers/library.worker.d.ts.map +1 -1
  86. package/dist/workers/library.worker.js +7 -9
  87. package/dist/workers/library.worker.js.map +1 -1
  88. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  89. package/dist/workers/server-runtime.worker.js +4 -3
  90. package/dist/workers/server-runtime.worker.js.map +1 -1
  91. package/dist/workers/server.worker.d.ts.map +1 -1
  92. package/dist/workers/server.worker.js +10 -13
  93. package/dist/workers/server.worker.js.map +1 -1
  94. package/package.json +5 -4
  95. package/src/builders/BaseBuilder.ts +2 -7
  96. package/src/builders/DtsBuilder.ts +4 -3
  97. package/src/builders/LibraryBuilder.ts +2 -1
  98. package/src/capacitor/capacitor.ts +2 -2
  99. package/src/commands/add-client.ts +2 -2
  100. package/src/commands/add-server.ts +2 -2
  101. package/src/commands/build.ts +2 -17
  102. package/src/commands/check.ts +31 -44
  103. package/src/commands/dev.ts +2 -16
  104. package/src/commands/init.ts +5 -5
  105. package/src/commands/publish.ts +16 -16
  106. package/src/commands/typecheck.ts +5 -5
  107. package/src/commands/watch.ts +2 -15
  108. package/src/electron/electron.ts +2 -2
  109. package/src/infra/ResultCollector.ts +0 -36
  110. package/src/orchestrators/BuildOrchestrator.ts +12 -21
  111. package/src/orchestrators/DevOrchestrator.ts +221 -201
  112. package/src/sd-cli-entry.ts +14 -14
  113. package/src/sd-cli.ts +9 -14
  114. package/src/utils/output-utils.ts +15 -11
  115. package/src/utils/package-utils.ts +0 -12
  116. package/src/utils/replace-deps.ts +61 -88
  117. package/src/utils/sd-config.ts +2 -6
  118. package/src/utils/vite-config.ts +9 -1
  119. package/src/utils/worker-events.ts +22 -25
  120. package/src/utils/worker-utils.ts +16 -0
  121. package/src/workers/client.worker.ts +12 -11
  122. package/src/workers/dts.worker.ts +13 -15
  123. package/src/workers/library.worker.ts +7 -10
  124. package/src/workers/server-runtime.worker.ts +4 -3
  125. package/src/workers/server.worker.ts +10 -14
  126. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  127. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  128. package/templates/init/package.json.hbs +3 -3
  129. package/dist/utils/spawn.d.ts +0 -26
  130. package/dist/utils/spawn.d.ts.map +0 -1
  131. package/dist/utils/spawn.js +0 -50
  132. package/dist/utils/spawn.js.map +0 -6
  133. package/src/utils/spawn.ts +0 -80
package/src/sd-cli.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * .js 실행 (배포): replaceDeps 실행 후 새 프로세스로 sd-cli-entry spawn
8
8
  */
9
9
 
10
- import { exec, spawn } from "child_process";
10
+ import { execa } from "execa";
11
11
  import os from "os";
12
12
  import path from "path";
13
13
  import { fileURLToPath } from "url";
@@ -40,7 +40,7 @@ if (isDev) {
40
40
 
41
41
  // Phase 2: 새 프로세스로 실제 CLI 실행 (모듈 캐시 초기화)
42
42
  const cliEntryFilePath = path.join(__dirname, "sd-cli-entry.js");
43
- const child = spawn(
43
+ const subprocess = execa(
44
44
  "node",
45
45
  [
46
46
  "--max-old-space-size=8192",
@@ -48,14 +48,11 @@ if (isDev) {
48
48
  cliEntryFilePath,
49
49
  ...process.argv.slice(2),
50
50
  ],
51
- { stdio: "inherit" },
51
+ { stdio: "inherit", reject: false },
52
52
  );
53
- child.on("spawn", () => {
54
- if (child.pid != null) configureAffinityAndPriority(child.pid);
55
- });
56
- child.on("exit", (code) => {
57
- process.exitCode = code ?? 0;
58
- });
53
+ if (subprocess.pid != null) configureAffinityAndPriority(subprocess.pid);
54
+ const result = await subprocess;
55
+ process.exitCode = result.exitCode ?? 0;
59
56
  }
60
57
 
61
58
  /**
@@ -97,10 +94,8 @@ function configureAffinityAndPriority(pid: number): void {
97
94
  command = `taskset -p ${mask} ${pid} && renice +10 -p ${pid}`;
98
95
  }
99
96
 
100
- exec(command, (err) => {
101
- if (err) {
102
- // eslint-disable-next-line no-console
103
- console.warn("CPU affinity/priority 설정 실패:", err.message);
104
- }
97
+ execa({ shell: true })`${command}`.catch((err: Error) => {
98
+ // eslint-disable-next-line no-console
99
+ console.warn("CPU affinity/priority 설정 실패:", err.message);
105
100
  });
106
101
  }
@@ -1,28 +1,32 @@
1
1
  import { consola } from "consola";
2
2
  import type { BuildResult } from "../infra/ResultCollector";
3
- import type { PackageResult } from "./package-utils";
4
3
 
5
4
  /**
6
- * printErrors에서 사용되는 결과 타입
7
- * PackageResult와 BuildResult 모두 지원
5
+ * 빌드 경고/에러 메시지를 포맷팅한다.
8
6
  */
9
- type ErrorResult = PackageResult | BuildResult;
7
+ export function formatBuildMessages(name: string, label: string, messages: string[]): string {
8
+ const lines: string[] = [`${name} (${label})`];
9
+ for (const msg of messages) {
10
+ for (const line of msg.split("\n")) {
11
+ lines.push(` → ${line}`);
12
+ }
13
+ }
14
+ return lines.join("\n");
15
+ }
10
16
 
11
17
  /**
12
18
  * 에러만 출력한다.
13
19
  * @param results 패키지별 빌드 결과 상태
14
20
  */
15
- export function printErrors(results: Map<string, ErrorResult>): void {
21
+ export function printErrors(results: Map<string, BuildResult>): void {
16
22
  for (const result of results.values()) {
17
23
  if (result.status === "error") {
18
24
  const typeLabel = result.type === "dts" ? "dts" : result.target;
19
- const errorLines: string[] = [`${result.name} (${typeLabel})`];
20
25
  if (result.message != null && result.message !== "") {
21
- for (const line of result.message.split("\n")) {
22
- errorLines.push(` → ${line}`);
23
- }
26
+ consola.error(formatBuildMessages(result.name, typeLabel, [result.message]));
27
+ } else {
28
+ consola.error(`${result.name} (${typeLabel})`);
24
29
  }
25
- consola.error(errorLines.join("\n"));
26
30
  }
27
31
  }
28
32
  }
@@ -33,7 +37,7 @@ export function printErrors(results: Map<string, ErrorResult>): void {
33
37
  * @param serverClientsMap 서버별 연결된 클라이언트 목록
34
38
  */
35
39
  export function printServers(
36
- results: Map<string, PackageResult>,
40
+ results: Map<string, BuildResult>,
37
41
  serverClientsMap?: Map<string, string[]>,
38
42
  ): void {
39
43
  // 서버 정보 수집
@@ -83,18 +83,6 @@ export function collectDeps(
83
83
  return { workspaceDeps, replaceDeps };
84
84
  }
85
85
 
86
- /**
87
- * 패키지 결과 상태
88
- */
89
- export interface PackageResult {
90
- name: string;
91
- target: string;
92
- type: "build" | "dts" | "server" | "capacitor";
93
- status: "success" | "error" | "running";
94
- message?: string;
95
- port?: number;
96
- }
97
-
98
86
  /**
99
87
  * 패키지 설정에서 targets 필터링 (scripts 타겟 제외)
100
88
  * @param packages 패키지 설정 맵
@@ -148,28 +148,26 @@ async function collectSearchRoots(projectRoot: string): Promise<string[]> {
148
148
  }
149
149
 
150
150
  /**
151
- * replaceDeps 설정에 따라 node_modules 패키지를 소스 디렉토리로 복사 교체한다.
151
+ * replaceDeps 설정에서 모든 교체 대상 항목을 해석한다.
152
152
  *
153
153
  * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
154
154
  * 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
155
- * 3. 기존 symlink/디렉토리 제거 소스 경로를 복사 (node_modules, package.json, .cache, tests 제외)
155
+ * 3. 패턴 매칭 + 소스 경로 존재 확인 + symlink 해석
156
156
  *
157
157
  * @param projectRoot - 프로젝트 루트 경로
158
158
  * @param replaceDeps - sd.config.ts의 replaceDeps 설정
159
+ * @param logger - consola 로거
160
+ * @returns 해석된 교체 대상 항목 배열
159
161
  */
160
- export async function setupReplaceDeps(
162
+ async function resolveAllReplaceDepEntries(
161
163
  projectRoot: string,
162
164
  replaceDeps: Record<string, string>,
163
- ): Promise<void> {
164
- const logger = consola.withTag("sd:cli:replace-deps");
165
- let setupCount = 0;
166
-
167
- logger.start("Setting up replace-deps");
165
+ logger: ReturnType<typeof consola.withTag>,
166
+ ): Promise<ReplaceDepEntry[]> {
167
+ const entries: ReplaceDepEntry[] = [];
168
168
 
169
- // 1. Workspace 패키지 경로 목록 수집
170
169
  const searchRoots = await collectSearchRoots(projectRoot);
171
170
 
172
- // 2. 각 searchRoot의 node_modules에서 매칭되는 패키지 찾기
173
171
  for (const searchRoot of searchRoots) {
174
172
  const nodeModulesDir = path.join(searchRoot, "node_modules");
175
173
 
@@ -189,10 +187,9 @@ export async function setupReplaceDeps(
189
187
  if (targetNames.length === 0) continue;
190
188
 
191
189
  // 패턴 매칭 및 경로 해석
192
- const entries = resolveReplaceDepEntries(replaceDeps, targetNames);
190
+ const matchedEntries = resolveReplaceDepEntries(replaceDeps, targetNames);
193
191
 
194
- // 3. 복사 교체
195
- for (const { targetName, sourcePath } of entries) {
192
+ for (const { targetName, sourcePath } of matchedEntries) {
196
193
  const targetPath = path.join(nodeModulesDir, targetName);
197
194
  const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
198
195
 
@@ -204,25 +201,59 @@ export async function setupReplaceDeps(
204
201
  continue;
205
202
  }
206
203
 
204
+ // targetPath가 symlink면 realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
205
+ let actualTargetPath = targetPath;
207
206
  try {
208
- // targetPath가 symlink면 realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
209
- let actualTargetPath = targetPath;
210
- try {
211
- const stat = await fs.promises.lstat(targetPath);
212
- if (stat.isSymbolicLink()) {
213
- actualTargetPath = await fs.promises.realpath(targetPath);
214
- }
215
- } catch {
216
- // targetPath가 존재하지 않으면 그대로 사용
207
+ const stat = await fs.promises.lstat(targetPath);
208
+ if (stat.isSymbolicLink()) {
209
+ actualTargetPath = await fs.promises.realpath(targetPath);
217
210
  }
211
+ } catch {
212
+ // targetPath가 존재하지 않으면 그대로 사용
213
+ }
218
214
 
219
- // 소스 파일을 actualTargetPath에 덮어쓰기 복사 (기존 디렉토리 유지, symlink 보존)
220
- await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
215
+ entries.push({
216
+ targetName,
217
+ sourcePath,
218
+ targetPath,
219
+ resolvedSourcePath,
220
+ actualTargetPath,
221
+ });
222
+ }
223
+ }
221
224
 
222
- setupCount += 1;
223
- } catch (err) {
224
- logger.error(`복사 교체 실패 (${targetName}): ${err instanceof Error ? err.message : err}`);
225
- }
225
+ return entries;
226
+ }
227
+
228
+ /**
229
+ * replaceDeps 설정에 따라 node_modules 내 패키지를 소스 디렉토리로 복사 교체한다.
230
+ *
231
+ * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
232
+ * 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
233
+ * 3. 기존 symlink/디렉토리 제거 → 소스 경로를 복사 (node_modules, package.json, .cache, tests 제외)
234
+ *
235
+ * @param projectRoot - 프로젝트 루트 경로
236
+ * @param replaceDeps - sd.config.ts의 replaceDeps 설정
237
+ */
238
+ export async function setupReplaceDeps(
239
+ projectRoot: string,
240
+ replaceDeps: Record<string, string>,
241
+ ): Promise<void> {
242
+ const logger = consola.withTag("sd:cli:replace-deps");
243
+ let setupCount = 0;
244
+
245
+ logger.start("Setting up replace-deps");
246
+
247
+ const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
248
+
249
+ for (const { targetName, resolvedSourcePath, actualTargetPath } of entries) {
250
+ try {
251
+ // 소스 파일을 actualTargetPath에 덮어쓰기 복사 (기존 디렉토리 유지, symlink 보존)
252
+ await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
253
+
254
+ setupCount += 1;
255
+ } catch (err) {
256
+ logger.error(`복사 교체 실패 (${targetName}): ${err instanceof Error ? err.message : err}`);
226
257
  }
227
258
  }
228
259
 
@@ -246,68 +277,10 @@ export async function watchReplaceDeps(
246
277
  replaceDeps: Record<string, string>,
247
278
  ): Promise<WatchReplaceDepResult> {
248
279
  const logger = consola.withTag("sd:cli:replace-deps:watch");
249
- const entries: ReplaceDepEntry[] = [];
250
280
 
251
- // 1. Workspace 패키지 경로 목록 수집
252
- const searchRoots = await collectSearchRoots(projectRoot);
253
-
254
- // 2. 각 searchRoot의 node_modules에서 매칭되는 패키지 찾기
255
- for (const searchRoot of searchRoots) {
256
- const nodeModulesDir = path.join(searchRoot, "node_modules");
257
-
258
- try {
259
- await fs.promises.access(nodeModulesDir);
260
- } catch {
261
- continue; // node_modules 없으면 스킵
262
- }
263
-
264
- // replaceDeps의 각 glob 패턴으로 node_modules 내 디렉토리 탐색
265
- const targetNames: string[] = [];
266
- for (const pattern of Object.keys(replaceDeps)) {
267
- const matches = await glob(pattern, { cwd: nodeModulesDir });
268
- targetNames.push(...matches);
269
- }
270
-
271
- if (targetNames.length === 0) continue;
272
-
273
- // 패턴 매칭 및 경로 해석
274
- const matchedEntries = resolveReplaceDepEntries(replaceDeps, targetNames);
275
-
276
- // 3. entry 정보 수집 (symlink 해석 포함)
277
- for (const { targetName, sourcePath } of matchedEntries) {
278
- const targetPath = path.join(nodeModulesDir, targetName);
279
- const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
280
-
281
- // 소스 경로 존재 확인
282
- try {
283
- await fs.promises.access(resolvedSourcePath);
284
- } catch {
285
- logger.warn(`소스 경로가 존재하지 않아 스킵합니다: ${resolvedSourcePath}`);
286
- continue;
287
- }
288
-
289
- // targetPath가 symlink면 realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
290
- let actualTargetPath = targetPath;
291
- try {
292
- const stat = await fs.promises.lstat(targetPath);
293
- if (stat.isSymbolicLink()) {
294
- actualTargetPath = await fs.promises.realpath(targetPath);
295
- }
296
- } catch {
297
- // targetPath가 존재하지 않으면 그대로 사용
298
- }
299
-
300
- entries.push({
301
- targetName,
302
- sourcePath,
303
- targetPath,
304
- resolvedSourcePath,
305
- actualTargetPath,
306
- });
307
- }
308
- }
281
+ const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
309
282
 
310
- // 4. 소스 디렉토리 watch 설정
283
+ // 소스 디렉토리 watch 설정
311
284
  const watchers: FsWatcher[] = [];
312
285
  const watchedSources = new Set<string>();
313
286
 
@@ -2,18 +2,14 @@ import path from "path";
2
2
  import { createJiti } from "jiti";
3
3
  import { SdError } from "@simplysm/core-common";
4
4
  import { fsExists } from "@simplysm/core-node";
5
- import type { SdConfig } from "../sd-config.types";
5
+ import type { SdConfig, SdConfigParams } from "../sd-config.types";
6
6
 
7
7
  /**
8
8
  * sd.config.ts 로드
9
9
  * @returns SdConfig 객체
10
10
  * @throws sd.config.ts가 없거나 형식이 잘못된 경우
11
11
  */
12
- export async function loadSdConfig(params: {
13
- cwd: string;
14
- dev: boolean;
15
- opt: string[];
16
- }): Promise<SdConfig> {
12
+ export async function loadSdConfig(params: SdConfigParams): Promise<SdConfig> {
17
13
  const sdConfigPath = path.resolve(params.cwd, "sd.config.ts");
18
14
 
19
15
  if (!(await fsExists(sdConfigPath))) {
@@ -111,7 +111,15 @@ function sdPublicDevPlugin(pkgDir: string): Plugin {
111
111
  urlPath = urlPath.slice(1);
112
112
  }
113
113
 
114
- const filePath = path.join(publicDevDir, urlPath);
114
+ // path traversal 방어: publicDevDir 범위 밖의 파일 접근 차단
115
+ const decodedPath = decodeURIComponent(urlPath);
116
+ const filePath = path.resolve(publicDevDir, decodedPath);
117
+ const normalizedRoot = path.resolve(publicDevDir);
118
+ if (!filePath.startsWith(normalizedRoot + path.sep) && filePath !== normalizedRoot) {
119
+ next();
120
+ return;
121
+ }
122
+
115
123
  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
116
124
  // sirv 대신 간단히 파일 스트림으로 응답
117
125
  const stream = fs.createReadStream(filePath);
@@ -1,7 +1,8 @@
1
1
  import { consola } from "consola";
2
- import type { PackageResult } from "./package-utils";
2
+ import type { BuildResult } from "../infra/ResultCollector";
3
3
  import type { SdPackageConfig } from "../sd-config.types";
4
4
  import type { RebuildManager } from "./rebuild-manager";
5
+ import { formatBuildMessages } from "./output-utils";
5
6
 
6
7
  const workerEventsLogger = consola.withTag("sd:cli:worker-events");
7
8
 
@@ -33,12 +34,12 @@ export interface ServerBuildEventData {
33
34
  /**
34
35
  * 기본 Worker 정보 타입
35
36
  */
36
- export interface BaseWorkerInfo<TEvents extends Record<string, any[]> = Record<string, any[]>> {
37
+ export interface BaseWorkerInfo<TEvents extends Record<string, unknown> = Record<string, unknown>> {
37
38
  name: string;
38
39
  config: SdPackageConfig;
39
40
  worker: {
40
- on<K extends keyof TEvents>(event: K, handler: (data: TEvents[K][0]) => void): void;
41
- send<K extends keyof TEvents>(event: K, data: TEvents[K][0]): void;
41
+ on<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): void;
42
+ send<K extends keyof TEvents>(event: K, data: TEvents[K]): void;
42
43
  };
43
44
  isInitialBuild: boolean;
44
45
  buildResolver: (() => void) | undefined;
@@ -63,15 +64,15 @@ export interface WorkerEventHandlerOptions {
63
64
  * @returns completeTask 함수 (결과를 저장하고 빌드 완료를 알림)
64
65
  */
65
66
  export function registerWorkerEventHandlers<
66
- TEvents extends Record<string, any[]>,
67
+ TEvents extends Record<string, unknown>,
67
68
  T extends BaseWorkerInfo<TEvents>,
68
69
  >(
69
70
  workerInfo: T,
70
71
  opts: WorkerEventHandlerOptions,
71
- results: Map<string, PackageResult>,
72
+ results: Map<string, BuildResult>,
72
73
  rebuildManager: RebuildManager,
73
- ): (result: PackageResult) => void {
74
- const completeTask = (result: PackageResult): void => {
74
+ ): (result: BuildResult) => void {
75
+ const completeTask = (result: BuildResult): void => {
75
76
  results.set(opts.resultKey, result);
76
77
  workerInfo.buildResolver?.();
77
78
  workerInfo.buildResolver = undefined;
@@ -86,40 +87,36 @@ export function registerWorkerEventHandlers<
86
87
  });
87
88
 
88
89
  // 빌드 완료
89
- workerInfo.worker.on("build", (data) => {
90
- const event = data as BuildEventData;
91
- workerEventsLogger.debug(`[${workerInfo.name}] build: success=${String(event.success)}`);
90
+ workerInfo.worker.on("build", (_data) => {
91
+ const data = _data as BuildEventData;
92
+ workerEventsLogger.debug(`[${workerInfo.name}] build: success=${String(data.success)}`);
92
93
 
93
94
  // warnings 출력
94
- if (event.warnings != null && event.warnings.length > 0) {
95
- const warnLines: string[] = [`${workerInfo.name} (${workerInfo.config.target})`];
96
- for (const warning of event.warnings) {
97
- for (const line of warning.split("\n")) {
98
- warnLines.push(` → ${line}`);
99
- }
100
- }
101
- workerEventsLogger.warn(warnLines.join("\n"));
95
+ if (data.warnings != null && data.warnings.length > 0) {
96
+ workerEventsLogger.warn(
97
+ formatBuildMessages(workerInfo.name, workerInfo.config.target, data.warnings),
98
+ );
102
99
  }
103
100
 
104
101
  completeTask({
105
102
  name: workerInfo.name,
106
103
  target: workerInfo.config.target,
107
104
  type: opts.resultType,
108
- status: event.success ? "success" : "error",
109
- message: event.errors?.join("\n"),
105
+ status: data.success ? "success" : "error",
106
+ message: data.errors?.join("\n"),
110
107
  });
111
108
  });
112
109
 
113
110
  // 에러
114
- workerInfo.worker.on("error", (data) => {
115
- const event = data as ErrorEventData;
116
- workerEventsLogger.debug(`[${workerInfo.name}] error: ${event.message}`);
111
+ workerInfo.worker.on("error", (_data) => {
112
+ const data = _data as ErrorEventData;
113
+ workerEventsLogger.debug(`[${workerInfo.name}] error: ${data.message}`);
117
114
  completeTask({
118
115
  name: workerInfo.name,
119
116
  target: workerInfo.config.target,
120
117
  type: opts.resultType,
121
118
  status: "error",
122
- message: event.message,
119
+ message: data.message,
123
120
  });
124
121
  });
125
122
 
@@ -27,3 +27,19 @@ export function registerCleanupHandlers(
27
27
  process.on("SIGTERM", handleSignal);
28
28
  process.on("SIGINT", handleSignal);
29
29
  }
30
+
31
+ /**
32
+ * Worker 함수의 중복 호출을 방지하는 가드를 생성한다.
33
+ *
34
+ * @param label - 에러 메시지에 사용할 함수명
35
+ * @returns 호출 시 중복이면 에러를 throw하는 가드 함수
36
+ */
37
+ export function createOnceGuard(label: string): () => void {
38
+ let called = false;
39
+ return () => {
40
+ if (called) {
41
+ throw new Error(`${label}는 Worker당 한 번만 호출할 수 있습니다.`);
42
+ }
43
+ called = true;
44
+ };
45
+ }
@@ -2,12 +2,13 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import { build as viteBuild, createServer, type ViteDevServer } from "vite";
4
4
  import { createWorker } from "@simplysm/core-node";
5
+ import { errorMessage } from "@simplysm/core-common";
5
6
  import { consola } from "consola";
6
7
  import type { SdClientPackageConfig } from "../sd-config.types";
7
8
  import { parseRootTsconfig, getCompilerOptionsForPackage } from "../utils/tsconfig";
8
9
  import { createViteConfig } from "../utils/vite-config";
9
10
  import { collectDeps } from "../utils/package-utils";
10
- import { registerCleanupHandlers } from "../utils/worker-utils";
11
+ import { registerCleanupHandlers, createOnceGuard } from "../utils/worker-utils";
11
12
 
12
13
  //#region Types
13
14
 
@@ -142,13 +143,12 @@ async function build(info: ClientBuildInfo): Promise<ClientBuildResult> {
142
143
  } catch (err) {
143
144
  return {
144
145
  success: false,
145
- errors: [err instanceof Error ? err.message : String(err)],
146
+ errors: [errorMessage(err)],
146
147
  };
147
148
  }
148
149
  }
149
150
 
150
- /** startWatch 호출 여부 플래그 */
151
- let isWatchStarted = false;
151
+ const guardStartWatch = createOnceGuard("startWatch");
152
152
 
153
153
  /**
154
154
  * watch 시작 (Vite dev server)
@@ -156,10 +156,7 @@ let isWatchStarted = false;
156
156
  * @throws 이미 watch가 시작된 경우
157
157
  */
158
158
  async function startWatch(info: ClientWatchInfo): Promise<void> {
159
- if (isWatchStarted) {
160
- throw new Error("startWatch는 Worker당 한 번만 호출할 수 있습니다.");
161
- }
162
- isWatchStarted = true;
159
+ guardStartWatch();
163
160
 
164
161
  try {
165
162
  // tsconfig 파싱
@@ -204,13 +201,17 @@ async function startWatch(info: ClientWatchInfo): Promise<void> {
204
201
 
205
202
  // 실제 할당된 포트 반환 (config.server.port는 설정값이므로 httpServer에서 실제 포트를 가져옴)
206
203
  const address = viteServer.httpServer?.address();
207
- const actualPort =
208
- typeof address === "object" && address != null ? address.port : viteServer.config.server.port;
204
+ const actualPort = typeof address === "object" && address != null ? address.port : undefined;
205
+
206
+ if (actualPort == null) {
207
+ sender.send("error", { message: "Vite dev server port를 확인할 수 없습니다." });
208
+ return;
209
+ }
209
210
 
210
211
  sender.send("serverReady", { port: actualPort });
211
212
  } catch (err) {
212
213
  sender.send("error", {
213
- message: err instanceof Error ? err.message : String(err),
214
+ message: errorMessage(err),
214
215
  });
215
216
  }
216
217
  }
@@ -1,6 +1,7 @@
1
1
  import path from "path";
2
2
  import ts from "typescript";
3
3
  import { createWorker, pathIsChildPath, pathNorm } from "@simplysm/core-node";
4
+ import { errorMessage } from "@simplysm/core-common";
4
5
  import { consola } from "consola";
5
6
  import {
6
7
  getCompilerOptionsForPackage,
@@ -10,6 +11,7 @@ import {
10
11
  type TypecheckEnv,
11
12
  } from "../utils/tsconfig";
12
13
  import { serializeDiagnostic, type SerializedDiagnostic } from "../utils/typecheck-serialization";
14
+ import { createOnceGuard } from "../utils/worker-utils";
13
15
 
14
16
  //#region Types
15
17
 
@@ -181,12 +183,12 @@ function createDtsPathRewriter(
181
183
 
182
184
  //#endregion
183
185
 
184
- //#region buildDts (일회성 빌드)
186
+ //#region build (일회성 빌드)
185
187
 
186
188
  /**
187
189
  * DTS 일회성 빌드 (타입체크 + dts 생성)
188
190
  */
189
- async function buildDts(info: DtsBuildInfo): Promise<DtsBuildResult> {
191
+ async function build(info: DtsBuildInfo): Promise<DtsBuildResult> {
190
192
  try {
191
193
  const parsedConfig = parseRootTsconfig(info.cwd);
192
194
 
@@ -327,7 +329,7 @@ async function buildDts(info: DtsBuildInfo): Promise<DtsBuildResult> {
327
329
  } catch (err) {
328
330
  return {
329
331
  success: false,
330
- errors: [err instanceof Error ? err.message : String(err)],
332
+ errors: [errorMessage(err)],
331
333
  diagnostics: [],
332
334
  errorCount: 1,
333
335
  warningCount: 0,
@@ -337,21 +339,17 @@ async function buildDts(info: DtsBuildInfo): Promise<DtsBuildResult> {
337
339
 
338
340
  //#endregion
339
341
 
340
- //#region startDtsWatch (watch 모드)
342
+ //#region startWatch (watch 모드)
341
343
 
342
- /** startDtsWatch 호출 여부 플래그 */
343
- let isWatchStarted = false;
344
+ const guardStartWatch = createOnceGuard("startWatch");
344
345
 
345
346
  /**
346
347
  * DTS watch 시작
347
348
  * @remarks 이 함수는 Worker당 한 번만 호출되어야 합니다.
348
349
  * @throws 이미 watch가 시작된 경우
349
350
  */
350
- async function startDtsWatch(info: DtsWatchInfo): Promise<void> {
351
- if (isWatchStarted) {
352
- throw new Error("startDtsWatch는 Worker당 한 번만 호출할 수 있습니다.");
353
- }
354
- isWatchStarted = true;
351
+ async function startWatch(info: DtsWatchInfo): Promise<void> {
352
+ guardStartWatch();
355
353
 
356
354
  try {
357
355
  const parsedConfig = parseRootTsconfig(info.cwd);
@@ -451,17 +449,17 @@ async function startDtsWatch(info: DtsWatchInfo): Promise<void> {
451
449
  tscWatchProgram = ts.createWatchProgram(host);
452
450
  } catch (err) {
453
451
  sender.send("error", {
454
- message: err instanceof Error ? err.message : String(err),
452
+ message: errorMessage(err),
455
453
  });
456
454
  }
457
455
  }
458
456
 
459
457
  const sender = createWorker<
460
- { startDtsWatch: typeof startDtsWatch; buildDts: typeof buildDts },
458
+ { startWatch: typeof startWatch; build: typeof build },
461
459
  DtsWorkerEvents
462
460
  >({
463
- startDtsWatch,
464
- buildDts,
461
+ startWatch,
462
+ build,
465
463
  });
466
464
 
467
465
  export default sender;