@simplysm/sd-cli 13.0.11 → 13.0.12

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 (154) hide show
  1. package/dist/builders/BaseBuilder.d.ts +15 -4
  2. package/dist/builders/BaseBuilder.d.ts.map +1 -1
  3. package/dist/builders/BaseBuilder.js +50 -0
  4. package/dist/builders/BaseBuilder.js.map +1 -1
  5. package/dist/builders/DtsBuilder.d.ts.map +1 -1
  6. package/dist/builders/DtsBuilder.js +2 -39
  7. package/dist/builders/DtsBuilder.js.map +1 -1
  8. package/dist/builders/LibraryBuilder.d.ts.map +1 -1
  9. package/dist/builders/LibraryBuilder.js +2 -39
  10. package/dist/builders/LibraryBuilder.js.map +1 -1
  11. package/dist/builders/types.d.ts +2 -2
  12. package/dist/builders/types.d.ts.map +1 -1
  13. package/dist/capacitor/capacitor.d.ts.map +1 -1
  14. package/dist/capacitor/capacitor.js +2 -1
  15. package/dist/capacitor/capacitor.js.map +1 -1
  16. package/dist/commands/add-client.d.ts.map +1 -1
  17. package/dist/commands/add-client.js +1 -9
  18. package/dist/commands/add-client.js.map +1 -1
  19. package/dist/commands/add-server.d.ts.map +1 -1
  20. package/dist/commands/add-server.js +1 -9
  21. package/dist/commands/add-server.js.map +1 -1
  22. package/dist/commands/build.d.ts +1 -2
  23. package/dist/commands/build.d.ts.map +1 -1
  24. package/dist/commands/build.js +12 -311
  25. package/dist/commands/build.js.map +1 -1
  26. package/dist/commands/dev.d.ts +1 -1
  27. package/dist/commands/dev.d.ts.map +1 -1
  28. package/dist/commands/dev.js +11 -432
  29. package/dist/commands/dev.js.map +1 -1
  30. package/dist/commands/device.d.ts.map +1 -1
  31. package/dist/commands/device.js +17 -32
  32. package/dist/commands/device.js.map +1 -1
  33. package/dist/commands/init.d.ts.map +1 -1
  34. package/dist/commands/init.js +1 -9
  35. package/dist/commands/init.js.map +1 -1
  36. package/dist/commands/lint.d.ts +1 -1
  37. package/dist/commands/lint.d.ts.map +1 -1
  38. package/dist/commands/lint.js +71 -118
  39. package/dist/commands/lint.js.map +2 -2
  40. package/dist/commands/publish.d.ts.map +1 -1
  41. package/dist/commands/publish.js +47 -69
  42. package/dist/commands/publish.js.map +1 -1
  43. package/dist/commands/typecheck.d.ts +1 -1
  44. package/dist/commands/typecheck.d.ts.map +1 -1
  45. package/dist/commands/typecheck.js +11 -24
  46. package/dist/commands/typecheck.js.map +1 -1
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/infra/ResultCollector.d.ts +1 -1
  52. package/dist/infra/ResultCollector.d.ts.map +1 -1
  53. package/dist/infra/ResultCollector.js +1 -1
  54. package/dist/infra/ResultCollector.js.map +1 -1
  55. package/dist/orchestrators/BuildOrchestrator.d.ts +53 -0
  56. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -0
  57. package/dist/orchestrators/BuildOrchestrator.js +338 -0
  58. package/dist/orchestrators/BuildOrchestrator.js.map +6 -0
  59. package/dist/orchestrators/DevOrchestrator.d.ts +64 -0
  60. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -0
  61. package/dist/orchestrators/DevOrchestrator.js +524 -0
  62. package/dist/orchestrators/DevOrchestrator.js.map +6 -0
  63. package/dist/orchestrators/WatchOrchestrator.d.ts +1 -1
  64. package/dist/orchestrators/WatchOrchestrator.d.ts.map +1 -1
  65. package/dist/orchestrators/WatchOrchestrator.js +30 -21
  66. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  67. package/dist/orchestrators/index.d.ts +2 -0
  68. package/dist/orchestrators/index.d.ts.map +1 -1
  69. package/dist/orchestrators/index.js +4 -0
  70. package/dist/orchestrators/index.js.map +1 -1
  71. package/dist/sd-cli-entry.js +14 -14
  72. package/dist/sd-cli-entry.js.map +1 -1
  73. package/dist/sd-cli.js +6 -4
  74. package/dist/sd-cli.js.map +1 -1
  75. package/dist/utils/output-utils.d.ts +0 -1
  76. package/dist/utils/output-utils.d.ts.map +1 -1
  77. package/dist/utils/output-utils.js +1 -1
  78. package/dist/utils/output-utils.js.map +1 -1
  79. package/dist/utils/package-utils.d.ts +5 -1
  80. package/dist/utils/package-utils.d.ts.map +1 -1
  81. package/dist/utils/package-utils.js +12 -0
  82. package/dist/utils/package-utils.js.map +1 -1
  83. package/dist/utils/rebuild-manager.d.ts +15 -0
  84. package/dist/utils/rebuild-manager.d.ts.map +1 -0
  85. package/dist/utils/rebuild-manager.js +50 -0
  86. package/dist/utils/rebuild-manager.js.map +6 -0
  87. package/dist/utils/replace-deps.d.ts.map +1 -1
  88. package/dist/utils/replace-deps.js +7 -13
  89. package/dist/utils/replace-deps.js.map +1 -1
  90. package/dist/utils/vite-config.d.ts.map +1 -1
  91. package/dist/utils/vite-config.js +43 -5
  92. package/dist/utils/vite-config.js.map +1 -1
  93. package/dist/utils/worker-events.d.ts +5 -4
  94. package/dist/utils/worker-events.d.ts.map +1 -1
  95. package/dist/utils/worker-events.js +4 -0
  96. package/dist/utils/worker-events.js.map +1 -1
  97. package/dist/utils/worker-utils.d.ts +13 -0
  98. package/dist/utils/worker-utils.d.ts.map +1 -0
  99. package/dist/utils/worker-utils.js +15 -0
  100. package/dist/utils/worker-utils.js.map +6 -0
  101. package/dist/workers/client.worker.d.ts.map +1 -1
  102. package/dist/workers/client.worker.js +2 -14
  103. package/dist/workers/client.worker.js.map +1 -1
  104. package/dist/workers/library.worker.d.ts.map +1 -1
  105. package/dist/workers/library.worker.js +2 -14
  106. package/dist/workers/library.worker.js.map +1 -1
  107. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  108. package/dist/workers/server-runtime.worker.js +11 -11
  109. package/dist/workers/server-runtime.worker.js.map +1 -1
  110. package/dist/workers/server.worker.d.ts.map +1 -1
  111. package/dist/workers/server.worker.js +2 -14
  112. package/dist/workers/server.worker.js.map +1 -1
  113. package/package.json +4 -5
  114. package/src/builders/BaseBuilder.ts +71 -4
  115. package/src/builders/DtsBuilder.ts +2 -49
  116. package/src/builders/LibraryBuilder.ts +2 -51
  117. package/src/builders/types.ts +2 -2
  118. package/src/capacitor/capacitor.ts +2 -1
  119. package/src/commands/add-client.ts +1 -14
  120. package/src/commands/add-server.ts +1 -13
  121. package/src/commands/build.ts +13 -443
  122. package/src/commands/dev.ts +12 -582
  123. package/src/commands/device.ts +17 -34
  124. package/src/commands/init.ts +1 -13
  125. package/src/commands/lint.ts +85 -146
  126. package/src/commands/publish.ts +58 -76
  127. package/src/commands/typecheck.ts +13 -46
  128. package/src/index.ts +1 -0
  129. package/src/infra/ResultCollector.ts +2 -2
  130. package/src/orchestrators/BuildOrchestrator.ts +499 -0
  131. package/src/orchestrators/DevOrchestrator.ts +703 -0
  132. package/src/orchestrators/WatchOrchestrator.ts +42 -25
  133. package/src/orchestrators/index.ts +2 -0
  134. package/src/sd-cli-entry.ts +14 -14
  135. package/src/sd-cli.ts +6 -4
  136. package/src/utils/output-utils.ts +1 -2
  137. package/src/utils/package-utils.ts +16 -1
  138. package/src/utils/rebuild-manager.ts +65 -0
  139. package/src/utils/replace-deps.ts +25 -23
  140. package/src/utils/vite-config.ts +84 -6
  141. package/src/utils/worker-events.ts +13 -5
  142. package/src/utils/worker-utils.ts +26 -0
  143. package/src/workers/client.worker.ts +3 -19
  144. package/src/workers/library.worker.ts +2 -19
  145. package/src/workers/server-runtime.worker.ts +16 -17
  146. package/src/workers/server.worker.ts +2 -19
  147. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  148. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  149. package/templates/init/package.json.hbs +3 -3
  150. package/dist/utils/listr-manager.d.ts +0 -37
  151. package/dist/utils/listr-manager.d.ts.map +0 -1
  152. package/dist/utils/listr-manager.js +0 -59
  153. package/dist/utils/listr-manager.js.map +0 -6
  154. package/src/utils/listr-manager.ts +0 -89
@@ -0,0 +1,703 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Worker, type WorkerProxy } from "@simplysm/core-node";
4
+ import type { SdConfig, SdClientPackageConfig, SdServerPackageConfig } from "../sd-config.types";
5
+ import { consola } from "consola";
6
+ import { loadSdConfig } from "../utils/sd-config";
7
+ import { getVersion } from "../utils/build-env";
8
+ import { setupReplaceDeps, watchReplaceDeps, type WatchReplaceDepResult } from "../utils/replace-deps";
9
+ import type * as ClientWorkerModule from "../workers/client.worker";
10
+ import type * as ServerWorkerModule from "../workers/server.worker";
11
+ import type * as ServerRuntimeWorkerModule from "../workers/server-runtime.worker";
12
+ import { Capacitor } from "../capacitor/capacitor";
13
+ import { filterPackagesByTargets, getWatchScopes, type PackageResult } from "../utils/package-utils";
14
+ import { printErrors, printServers } from "../utils/output-utils";
15
+ import { RebuildManager } from "../utils/rebuild-manager";
16
+ import {
17
+ registerWorkerEventHandlers,
18
+ type BaseWorkerInfo,
19
+ type ServerReadyEventData,
20
+ type ServerBuildEventData,
21
+ type ErrorEventData,
22
+ } from "../utils/worker-events";
23
+ import { SignalHandler } from "../infra/SignalHandler";
24
+
25
+ //#region Types
26
+
27
+ /**
28
+ * Dev Orchestrator 옵션
29
+ */
30
+ export interface DevOrchestratorOptions {
31
+ /** dev할 패키지 필터 (빈 배열이면 모든 패키지) */
32
+ targets: string[];
33
+ options: string[];
34
+ }
35
+
36
+ /**
37
+ * Client Worker 정보 (Vite dev server용)
38
+ */
39
+ interface ClientWorkerInfo {
40
+ name: string;
41
+ config: SdClientPackageConfig;
42
+ worker: WorkerProxy<typeof ClientWorkerModule>;
43
+ isInitialBuild: boolean;
44
+ buildResolver: (() => void) | undefined;
45
+ }
46
+
47
+ //#endregion
48
+
49
+ //#region DevOrchestrator
50
+
51
+ /**
52
+ * Dev 모드 실행을 조율하는 Orchestrator
53
+ *
54
+ * Client 및 Server 패키지의 개발 모드 실행을 관리한다.
55
+ * - `client` 타겟: Vite dev server 시작
56
+ * - `server` 타겟: Server Build Worker + Server Runtime Worker
57
+ * - Server-Client 프록시 연결 지원
58
+ * - Capacitor 초기화 지원
59
+ * - SIGINT/SIGTERM 시그널로 종료
60
+ */
61
+ export class DevOrchestrator {
62
+ private readonly _options: DevOrchestratorOptions;
63
+ private readonly _logger = consola.withTag("sd:cli:dev");
64
+ private readonly _cwd: string;
65
+
66
+ // Config
67
+ private _sdConfig: SdConfig | undefined;
68
+ private _baseEnv: { VER: string; DEV: string } | undefined;
69
+ private _watchScopes: string[] = [];
70
+
71
+ // Package classification
72
+ private readonly _serverPackages: Array<{ name: string; config: SdServerPackageConfig }> = [];
73
+ private readonly _clientPackages: Array<{ name: string; config: SdClientPackageConfig }> = [];
74
+ private readonly _serverClientsMap = new Map<string, string[]>();
75
+ private _hasPackages = false;
76
+
77
+ // Workers
78
+ private _standaloneClientWorkers: ClientWorkerInfo[] = [];
79
+ private _viteClientWorkers: ClientWorkerInfo[] = [];
80
+ private readonly _serverBuildWorkers = new Map<
81
+ string,
82
+ {
83
+ worker: WorkerProxy<typeof ServerWorkerModule>;
84
+ buildPromise: Promise<void>;
85
+ buildResolver: () => void;
86
+ mainJsPath?: string;
87
+ }
88
+ >();
89
+ private readonly _serverRuntimeWorkers = new Map<string, WorkerProxy<typeof ServerRuntimeWorkerModule>>();
90
+
91
+ // State
92
+ private readonly _results = new Map<string, PackageResult>();
93
+ private _clientPorts: Record<string, number> = {};
94
+ private _rebuildManager!: RebuildManager;
95
+ private _signalHandler!: SignalHandler;
96
+ private _replaceDepWatcher: WatchReplaceDepResult | undefined;
97
+
98
+ constructor(options: DevOrchestratorOptions) {
99
+ this._cwd = process.cwd();
100
+ this._options = options;
101
+ }
102
+
103
+ /**
104
+ * Orchestrator 초기화
105
+ * - sd.config.ts 로드
106
+ * - 패키지 분류
107
+ * - 환경변수 준비
108
+ */
109
+ async initialize(): Promise<void> {
110
+ const { targets } = this._options;
111
+ this._logger.debug("dev 시작", { targets });
112
+
113
+ // sd.config.ts 로드 (dev는 패키지 빌드 정보가 필요하므로 필수)
114
+ try {
115
+ this._sdConfig = await loadSdConfig({ cwd: this._cwd, dev: true, opt: this._options.options });
116
+ this._logger.debug("sd.config.ts 로드 완료");
117
+ } catch (err) {
118
+ this._logger.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
119
+ process.exitCode = 1;
120
+ throw err;
121
+ }
122
+
123
+ // replaceDeps 설정이 있으면 symlink 교체
124
+ if (this._sdConfig.replaceDeps != null) {
125
+ await setupReplaceDeps(this._cwd, this._sdConfig.replaceDeps);
126
+ this._replaceDepWatcher = await watchReplaceDeps(this._cwd, this._sdConfig.replaceDeps);
127
+ }
128
+
129
+ // VER, DEV 환경변수 준비
130
+ const version = await getVersion(this._cwd);
131
+ this._baseEnv = { VER: version, DEV: "true" };
132
+
133
+ // watchScopes 생성 (루트 package.json에서 scope 추출)
134
+ const rootPkgJsonPath = path.join(this._cwd, "package.json");
135
+ const rootPkgName = JSON.parse(fs.readFileSync(rootPkgJsonPath, "utf-8")).name as string;
136
+ this._watchScopes = getWatchScopes(rootPkgName);
137
+
138
+ // targets 필터링
139
+ const allPackages = filterPackagesByTargets(this._sdConfig.packages, targets);
140
+
141
+ // client/server 패키지만 필터링
142
+ for (const [name, config] of Object.entries(allPackages)) {
143
+ if (config.target === "server") {
144
+ this._serverPackages.push({ name, config });
145
+ } else if (config.target === "client") {
146
+ this._clientPackages.push({ name, config });
147
+ }
148
+ }
149
+
150
+ if (this._serverPackages.length === 0 && this._clientPackages.length === 0) {
151
+ process.stdout.write("⚠ dev할 client/server 패키지가 없습니다.\n");
152
+ return;
153
+ }
154
+
155
+ this._hasPackages = true;
156
+
157
+ // 서버와 연결된 클라이언트 찾기 (서버가 dev 대상인 경우만)
158
+ const serverNames = new Set(this._serverPackages.map(({ name }) => name));
159
+ for (const { name, config } of this._clientPackages) {
160
+ if (typeof config.server === "string" && serverNames.has(config.server)) {
161
+ const clients = this._serverClientsMap.get(config.server) ?? [];
162
+ clients.push(name);
163
+ this._serverClientsMap.set(config.server, clients);
164
+ }
165
+ }
166
+
167
+ // 인프라 초기화
168
+ this._rebuildManager = new RebuildManager(this._logger);
169
+ this._signalHandler = new SignalHandler();
170
+
171
+ // 배치 완료 시 에러와 서버 URL 출력
172
+ this._rebuildManager.on("batchComplete", () => {
173
+ printErrors(this._results);
174
+ printServers(this._results, this._serverClientsMap);
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Dev 모드 시작
180
+ * - Worker 생성
181
+ * - 이벤트 핸들러 등록
182
+ * - 초기 빌드 및 서버 시작
183
+ * - Capacitor 초기화
184
+ */
185
+ async start(): Promise<void> {
186
+ if (!this._hasPackages) {
187
+ return;
188
+ }
189
+
190
+ const serverNames = new Set(this._serverPackages.map(({ name }) => name));
191
+
192
+ // Worker 경로
193
+ const clientWorkerPath = import.meta.resolve("../workers/client.worker");
194
+ const serverWorkerPath = import.meta.resolve("../workers/server.worker");
195
+ const serverRuntimeWorkerPath = import.meta.resolve("../workers/server-runtime.worker");
196
+
197
+ // 클라이언트가 단독 실행인 경우:
198
+ // - server가 숫자인 경우
199
+ // - server가 문자열이지만 해당 서버가 dev 대상이 아닌 경우
200
+ this._standaloneClientWorkers = this._clientPackages
201
+ .filter(
202
+ ({ config }) =>
203
+ typeof config.server === "number" || (typeof config.server === "string" && !serverNames.has(config.server)),
204
+ )
205
+ .map(({ name, config }) => ({
206
+ name,
207
+ config,
208
+ worker: Worker.create<typeof ClientWorkerModule>(clientWorkerPath),
209
+ isInitialBuild: true,
210
+ buildResolver: undefined,
211
+ }));
212
+
213
+ // 서버에 연결된 클라이언트의 Vite Worker (서버가 dev 대상인 경우만)
214
+ this._viteClientWorkers = this._clientPackages
215
+ .filter(({ config }) => typeof config.server === "string" && serverNames.has(config.server))
216
+ .map(({ name, config }) => ({
217
+ name,
218
+ config,
219
+ worker: Worker.create<typeof ClientWorkerModule>(clientWorkerPath),
220
+ isInitialBuild: true,
221
+ buildResolver: undefined,
222
+ }));
223
+
224
+ // Standalone client 빌드 Promise 미리 생성
225
+ const standaloneClientBuildPromises = new Map<string, Promise<void>>();
226
+ for (const workerInfo of this._standaloneClientWorkers) {
227
+ standaloneClientBuildPromises.set(
228
+ workerInfo.name,
229
+ new Promise<void>((resolve) => {
230
+ workerInfo.buildResolver = resolve;
231
+ }),
232
+ );
233
+ }
234
+
235
+ // Vite client 빌드 Promise 미리 생성 (서버 연결 클라이언트)
236
+ const viteClientBuildPromises = new Map<string, Promise<void>>();
237
+ const viteClientReadyPromises = new Map<string, { promise: Promise<void>; resolver: () => void }>();
238
+ for (const workerInfo of this._viteClientWorkers) {
239
+ viteClientBuildPromises.set(
240
+ workerInfo.name,
241
+ new Promise<void>((resolve) => {
242
+ workerInfo.buildResolver = resolve;
243
+ }),
244
+ );
245
+ // Vite 서버 준비 완료 Promise (서버가 클라이언트 포트를 알 때까지 대기)
246
+ let readyResolver!: () => void;
247
+ const readyPromise = new Promise<void>((resolve) => {
248
+ readyResolver = resolve;
249
+ });
250
+ viteClientReadyPromises.set(workerInfo.name, { promise: readyPromise, resolver: readyResolver });
251
+ }
252
+
253
+ // Server Build Worker 및 Promise 생성
254
+ for (const { name } of this._serverPackages) {
255
+ let resolver!: () => void;
256
+ const promise = new Promise<void>((resolve) => {
257
+ resolver = resolve;
258
+ });
259
+ this._serverBuildWorkers.set(name, {
260
+ worker: Worker.create<typeof ServerWorkerModule>(serverWorkerPath),
261
+ buildPromise: promise,
262
+ buildResolver: resolver,
263
+ });
264
+ }
265
+
266
+ // Server Runtime Promise (초기 서버 시작 완료 대기용)
267
+ const serverRuntimePromises = new Map<string, { promise: Promise<void>; resolver: () => void }>();
268
+ for (const { name } of this._serverPackages) {
269
+ let resolver!: () => void;
270
+ const promise = new Promise<void>((resolve) => {
271
+ resolver = resolve;
272
+ });
273
+ serverRuntimePromises.set(name, { promise, resolver });
274
+ }
275
+
276
+ // Standalone client 이벤트 핸들러 등록 및 completeTask 함수 저장
277
+ const clientCompleteTasks = new Map<string, (result: PackageResult) => void>();
278
+ for (const workerInfo of this._standaloneClientWorkers) {
279
+ const completeTask = registerWorkerEventHandlers(
280
+ workerInfo as unknown as BaseWorkerInfo,
281
+ {
282
+ resultKey: `${workerInfo.name}:build`,
283
+ listrTitle: `${workerInfo.name} (client)`,
284
+ resultType: "build",
285
+ },
286
+ this._results,
287
+ this._rebuildManager,
288
+ );
289
+ clientCompleteTasks.set(workerInfo.name, completeTask);
290
+
291
+ // serverReady (Vite dev server)
292
+ workerInfo.worker.on("serverReady", (data) => {
293
+ const event = data as ServerReadyEventData;
294
+ completeTask({
295
+ name: workerInfo.name,
296
+ target: workerInfo.config.target,
297
+ type: "server",
298
+ status: "running",
299
+ port: event.port,
300
+ });
301
+ });
302
+ }
303
+
304
+ // Vite client (서버 연결) 이벤트 핸들러 등록
305
+ for (const workerInfo of this._viteClientWorkers) {
306
+ const completeTask = registerWorkerEventHandlers(
307
+ workerInfo as unknown as BaseWorkerInfo,
308
+ {
309
+ resultKey: `${workerInfo.name}:build`,
310
+ listrTitle: `${workerInfo.name} (client)`,
311
+ resultType: "build",
312
+ },
313
+ this._results,
314
+ this._rebuildManager,
315
+ );
316
+ clientCompleteTasks.set(workerInfo.name, completeTask);
317
+
318
+ // serverReady - Vite 포트를 clientPorts에 저장 (URL은 서버를 통해 출력)
319
+ workerInfo.worker.on("serverReady", (data) => {
320
+ const event = data as ServerReadyEventData;
321
+ this._logger.debug(`[${workerInfo.name}] Vite serverReady (port: ${String(event.port)})`);
322
+ this._clientPorts[workerInfo.name] = event.port;
323
+ // Vite 서버 준비 완료 알림 (서버가 프록시 설정을 위해 대기 중)
324
+ viteClientReadyPromises.get(workerInfo.name)?.resolver();
325
+ // 빌드 완료를 위해 completeTask 호출 (Vite는 build 이벤트를 발생시키지 않음)
326
+ completeTask({
327
+ name: workerInfo.name,
328
+ target: workerInfo.config.target,
329
+ type: "build",
330
+ status: "success",
331
+ });
332
+ });
333
+
334
+ // Vite client error 시에도 viteClientReadyPromises resolve
335
+ // (서버가 await Promise.all(clientReadyPromises)에서 무한 대기하지 않도록)
336
+ workerInfo.worker.on("error", () => {
337
+ viteClientReadyPromises.get(workerInfo.name)?.resolver();
338
+ });
339
+ }
340
+
341
+ // Server Build Worker 이벤트 핸들러 등록
342
+ for (const { name } of this._serverPackages) {
343
+ const serverBuild = this._serverBuildWorkers.get(name)!;
344
+ let isFirstBuild = true;
345
+
346
+ serverBuild.worker.on("buildStart", () => {
347
+ if (!isFirstBuild) {
348
+ // 리빌드 시 RebuildManager에 등록
349
+ const resolver = this._rebuildManager.registerBuild(`${name}:server`, `${name} (server)`);
350
+ this._serverBuildWorkers.set(name, {
351
+ ...serverBuild,
352
+ buildResolver: resolver,
353
+ });
354
+ }
355
+ });
356
+
357
+ serverBuild.worker.on("build", (data) => {
358
+ const event = data as ServerBuildEventData;
359
+ this._logger.debug(`[${name}] server build: success=${String(event.success)}`);
360
+
361
+ if (!event.success) {
362
+ this._results.set(`${name}:build`, {
363
+ name,
364
+ target: "server",
365
+ type: "build",
366
+ status: "error",
367
+ message: event.errors?.join("\n"),
368
+ });
369
+
370
+ if (isFirstBuild) {
371
+ isFirstBuild = false;
372
+ serverRuntimePromises.get(name)?.resolver();
373
+ }
374
+ serverBuild.buildResolver();
375
+ return;
376
+ }
377
+
378
+ // 빌드 성공 시 런타임 워커 시작 (async 로직은 별도 함수로 분리하여 에러 전파 방지)
379
+ void startServerRuntime(name, event.mainJsPath).catch((err: unknown) => {
380
+ const message = err instanceof Error ? err.message : String(err);
381
+ this._logger.error(`[${name}] Server Runtime 시작 중 오류:`, message);
382
+
383
+ this._results.set(`${name}:server`, {
384
+ name,
385
+ target: "server",
386
+ type: "server",
387
+ status: "error",
388
+ message,
389
+ });
390
+
391
+ if (isFirstBuild) {
392
+ isFirstBuild = false;
393
+ serverRuntimePromises.get(name)?.resolver();
394
+ }
395
+ const updatedBuild = this._serverBuildWorkers.get(name)!;
396
+ updatedBuild.buildResolver();
397
+ });
398
+ });
399
+
400
+ /**
401
+ * 서버 런타임 워커 시작.
402
+ * async 이벤트 핸들러의 에러를 catch할 수 있도록 별도 함수로 분리.
403
+ */
404
+ const startServerRuntime = async (serverName: string, mainJsPath: string): Promise<void> => {
405
+ this._logger.debug(`[${serverName}] startServerRuntime: ${mainJsPath}`);
406
+ const updatedBuild = this._serverBuildWorkers.get(serverName)!;
407
+ updatedBuild.mainJsPath = mainJsPath;
408
+
409
+ // 기존 Server Runtime Worker 종료
410
+ const existingRuntime = this._serverRuntimeWorkers.get(serverName);
411
+ if (existingRuntime != null) {
412
+ this._logger.info(`[${serverName}] 서버 재시작 중...`);
413
+ await existingRuntime.terminate();
414
+ }
415
+
416
+ // 새 Server Runtime Worker 생성 및 시작
417
+ const runtimeWorker = Worker.create<typeof ServerRuntimeWorkerModule>(serverRuntimeWorkerPath);
418
+ this._serverRuntimeWorkers.set(serverName, runtimeWorker);
419
+
420
+ // 이 서버에 연결된 클라이언트들의 Vite 서버가 준비될 때까지 대기
421
+ const connectedClients = this._serverClientsMap.get(serverName) ?? [];
422
+ const clientReadyPromises = connectedClients
423
+ .map((clientName) => viteClientReadyPromises.get(clientName)?.promise)
424
+ .filter((p): p is Promise<void> => p != null);
425
+ this._logger.debug(`[${serverName}] 클라이언트 대기: ${String(clientReadyPromises.length)}개`);
426
+ if (clientReadyPromises.length > 0) {
427
+ await Promise.all(clientReadyPromises);
428
+ }
429
+
430
+ // 이 서버에 연결된 클라이언트 포트 수집
431
+ const serverClientPorts: Record<string, number> = {};
432
+ for (const clientName of connectedClients) {
433
+ if (clientName in this._clientPorts) {
434
+ serverClientPorts[clientName] = this._clientPorts[clientName];
435
+ }
436
+ }
437
+
438
+ // Server Runtime 이벤트 핸들러
439
+ runtimeWorker.on("serverReady", (readyData) => {
440
+ const readyEvent = readyData as ServerReadyEventData;
441
+ this._results.set(`${serverName}:server`, {
442
+ name: serverName,
443
+ target: "server",
444
+ type: "server",
445
+ status: "running",
446
+ port: readyEvent.port,
447
+ });
448
+
449
+ if (isFirstBuild) {
450
+ isFirstBuild = false;
451
+ serverRuntimePromises.get(serverName)?.resolver();
452
+ }
453
+ updatedBuild.buildResolver();
454
+ });
455
+
456
+ runtimeWorker.on("error", (errorData) => {
457
+ const errorEvent = errorData as ErrorEventData;
458
+ this._results.set(`${serverName}:server`, {
459
+ name: serverName,
460
+ target: "server",
461
+ type: "server",
462
+ status: "error",
463
+ message: errorEvent.message,
464
+ });
465
+
466
+ if (isFirstBuild) {
467
+ isFirstBuild = false;
468
+ serverRuntimePromises.get(serverName)?.resolver();
469
+ }
470
+ updatedBuild.buildResolver();
471
+ });
472
+
473
+ // Server Runtime 시작
474
+ // Worker가 크래시하면 "serverReady"/"error" 이벤트 없이 종료되므로
475
+ // promise rejection을 catch하여 무한 대기를 방지
476
+ runtimeWorker
477
+ .start({
478
+ mainJsPath,
479
+ clientPorts: serverClientPorts,
480
+ })
481
+ .catch((err: unknown) => {
482
+ const message = err instanceof Error ? err.message : String(err);
483
+ this._logger.error(`[${serverName}] Server Runtime Worker 크래시:`, message);
484
+
485
+ this._results.set(`${serverName}:server`, {
486
+ name: serverName,
487
+ target: "server",
488
+ type: "server",
489
+ status: "error",
490
+ message,
491
+ });
492
+
493
+ if (isFirstBuild) {
494
+ isFirstBuild = false;
495
+ serverRuntimePromises.get(serverName)?.resolver();
496
+ }
497
+ updatedBuild.buildResolver();
498
+ });
499
+ };
500
+
501
+ serverBuild.worker.on("error", (data) => {
502
+ const event = data as ErrorEventData;
503
+ this._results.set(`${name}:build`, {
504
+ name,
505
+ target: "server",
506
+ type: "build",
507
+ status: "error",
508
+ message: event.message,
509
+ });
510
+ if (isFirstBuild) {
511
+ isFirstBuild = false;
512
+ serverRuntimePromises.get(name)?.resolver();
513
+ }
514
+ serverBuild.buildResolver();
515
+ });
516
+ }
517
+
518
+ // Standalone client 워커 시작
519
+ for (const workerInfo of this._standaloneClientWorkers) {
520
+ const pkgDir = path.join(this._cwd, "packages", workerInfo.name);
521
+ const completeTask = clientCompleteTasks.get(workerInfo.name)!;
522
+ const clientConfig: SdClientPackageConfig = {
523
+ ...workerInfo.config,
524
+ env: { ...this._baseEnv, ...workerInfo.config.env },
525
+ };
526
+ workerInfo.worker
527
+ .startWatch({
528
+ name: workerInfo.name,
529
+ config: clientConfig,
530
+ cwd: this._cwd,
531
+ pkgDir,
532
+ watchScopes: this._watchScopes,
533
+ })
534
+ .catch((err: unknown) => {
535
+ completeTask({
536
+ name: workerInfo.name,
537
+ target: workerInfo.config.target,
538
+ type: "build",
539
+ status: "error",
540
+ message: err instanceof Error ? err.message : String(err),
541
+ });
542
+ });
543
+ }
544
+
545
+ // Vite client 워커 시작 (서버 연결) - Vite 자동 포트 사용
546
+ for (const workerInfo of this._viteClientWorkers) {
547
+ const pkgDir = path.join(this._cwd, "packages", workerInfo.name);
548
+ const completeTask = clientCompleteTasks.get(workerInfo.name)!;
549
+ // Vite가 자동으로 포트를 할당하도록 설정
550
+ const viteConfig: SdClientPackageConfig = {
551
+ ...workerInfo.config,
552
+ server: 0, // Vite가 자동으로 포트 할당
553
+ env: { ...this._baseEnv, ...workerInfo.config.env },
554
+ };
555
+ workerInfo.worker
556
+ .startWatch({
557
+ name: workerInfo.name,
558
+ config: viteConfig,
559
+ cwd: this._cwd,
560
+ pkgDir,
561
+ watchScopes: this._watchScopes,
562
+ })
563
+ .catch((err: unknown) => {
564
+ completeTask({
565
+ name: workerInfo.name,
566
+ target: workerInfo.config.target,
567
+ type: "build",
568
+ status: "error",
569
+ message: err instanceof Error ? err.message : String(err),
570
+ });
571
+ });
572
+ }
573
+
574
+ // Server Build 워커 시작
575
+ for (const { name, config } of this._serverPackages) {
576
+ const pkgDir = path.join(this._cwd, "packages", name);
577
+ const serverBuild = this._serverBuildWorkers.get(name)!;
578
+ serverBuild.worker
579
+ .startWatch({
580
+ name,
581
+ cwd: this._cwd,
582
+ pkgDir,
583
+ env: { ...this._baseEnv, ...config.env },
584
+ configs: config.configs,
585
+ externals: config.externals,
586
+ })
587
+ .catch((err: unknown) => {
588
+ this._results.set(`${name}:build`, {
589
+ name,
590
+ target: "server",
591
+ type: "build",
592
+ status: "error",
593
+ message: err instanceof Error ? err.message : String(err),
594
+ });
595
+ serverRuntimePromises.get(name)?.resolver();
596
+ serverBuild.buildResolver();
597
+ });
598
+ }
599
+
600
+ // 초기 빌드 완료까지 대기 (병렬 실행)
601
+ this._logger.debug("초기 빌드 시작 (Promise.allSettled)");
602
+ const initialBuildPromises: Array<{ name: string; promise: Promise<void> }> = [
603
+ // Standalone client
604
+ ...this._standaloneClientWorkers.map((workerInfo) => ({
605
+ name: `${workerInfo.name} (client)`,
606
+ promise: standaloneClientBuildPromises.get(workerInfo.name) ?? Promise.resolve(),
607
+ })),
608
+ // Vite client (서버 연결)
609
+ ...this._viteClientWorkers.map((workerInfo) => ({
610
+ name: `${workerInfo.name} (client)`,
611
+ promise: viteClientBuildPromises.get(workerInfo.name) ?? Promise.resolve(),
612
+ })),
613
+ // Server 빌드 + 런타임 시작
614
+ ...this._serverPackages.map(({ name }) => ({
615
+ name: `${name} (server)`,
616
+ promise: serverRuntimePromises.get(name)?.promise ?? Promise.resolve(),
617
+ })),
618
+ ];
619
+
620
+ const initialResults = await Promise.allSettled(initialBuildPromises.map((item) => item.promise));
621
+
622
+ initialResults.forEach((result, index) => {
623
+ const taskName = initialBuildPromises[index].name;
624
+ if (result.status === "rejected") {
625
+ this._logger.debug(`[${taskName}] 초기 빌드 실패:`, result.reason);
626
+ } else {
627
+ this._logger.debug(`[${taskName}] 초기 빌드 완료`);
628
+ }
629
+ });
630
+
631
+ // Capacitor 초기화 (client 타겟 중 capacitor 설정이 있는 패키지)
632
+ const capacitorPackages: Array<[string, SdClientPackageConfig]> = [];
633
+ for (const { name, config } of this._clientPackages) {
634
+ if (config.capacitor != null) {
635
+ capacitorPackages.push([name, config]);
636
+ }
637
+ }
638
+
639
+ if (capacitorPackages.length > 0) {
640
+ for (const [name, config] of capacitorPackages) {
641
+ const taskName = `${name} (capacitor)`;
642
+ this._logger.start(taskName);
643
+ const pkgDir = path.join(this._cwd, "packages", name);
644
+ try {
645
+ const cap = await Capacitor.create(pkgDir, config.capacitor!);
646
+ await cap.initialize();
647
+ this._results.set(`${name}:capacitor`, {
648
+ name,
649
+ target: "client",
650
+ type: "capacitor",
651
+ status: "success",
652
+ });
653
+ this._logger.success(taskName);
654
+ } catch (err) {
655
+ this._results.set(`${name}:capacitor`, {
656
+ name,
657
+ target: "client",
658
+ type: "capacitor",
659
+ status: "error",
660
+ message: err instanceof Error ? err.message : String(err),
661
+ });
662
+ this._logger.fail(taskName);
663
+ }
664
+ }
665
+ }
666
+
667
+ // 초기 빌드 결과 출력
668
+ printErrors(this._results);
669
+ printServers(this._results, this._serverClientsMap);
670
+ }
671
+
672
+ /**
673
+ * 종료 시그널 대기
674
+ */
675
+ async awaitTermination(): Promise<void> {
676
+ if (!this._hasPackages) {
677
+ return;
678
+ }
679
+ await this._signalHandler.waitForTermination();
680
+ }
681
+
682
+ /**
683
+ * Orchestrator 종료
684
+ */
685
+ async shutdown(): Promise<void> {
686
+ if (!this._hasPackages) {
687
+ return;
688
+ }
689
+
690
+ // Worker 종료 (모든 워커)
691
+ process.stdout.write("⏳ 종료 중...\n");
692
+ await Promise.all([
693
+ ...this._standaloneClientWorkers.map(({ worker }) => worker.terminate()),
694
+ ...this._viteClientWorkers.map(({ worker }) => worker.terminate()),
695
+ ...[...this._serverBuildWorkers.values()].map(({ worker }) => worker.terminate()),
696
+ ...[...this._serverRuntimeWorkers.values()].map((worker) => worker.terminate()),
697
+ ]);
698
+ this._replaceDepWatcher?.dispose();
699
+ process.stdout.write("✔ 완료\n");
700
+ }
701
+ }
702
+
703
+ //#endregion