@simplysm/sd-cli 13.0.68 → 13.0.70

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 (201) hide show
  1. package/README.md +10 -957
  2. package/dist/builders/BaseBuilder.d.ts +23 -23
  3. package/dist/builders/BaseBuilder.d.ts.map +1 -1
  4. package/dist/builders/BaseBuilder.js +15 -15
  5. package/dist/builders/DtsBuilder.d.ts +4 -4
  6. package/dist/builders/DtsBuilder.js +1 -1
  7. package/dist/builders/LibraryBuilder.d.ts +3 -3
  8. package/dist/builders/types.d.ts +10 -10
  9. package/dist/capacitor/capacitor.d.ts +36 -36
  10. package/dist/capacitor/capacitor.js +63 -63
  11. package/dist/capacitor/capacitor.js.map +1 -1
  12. package/dist/commands/add-client.d.ts +8 -8
  13. package/dist/commands/add-client.js +15 -15
  14. package/dist/commands/add-client.js.map +1 -1
  15. package/dist/commands/add-server.d.ts +9 -9
  16. package/dist/commands/add-server.js +13 -13
  17. package/dist/commands/add-server.js.map +1 -1
  18. package/dist/commands/build.d.ts +9 -9
  19. package/dist/commands/check.js +3 -3
  20. package/dist/commands/check.js.map +1 -1
  21. package/dist/commands/dev.d.ts +9 -9
  22. package/dist/commands/device.d.ts +9 -9
  23. package/dist/commands/device.d.ts.map +1 -1
  24. package/dist/commands/device.js +17 -17
  25. package/dist/commands/device.js.map +1 -1
  26. package/dist/commands/init.d.ts +6 -6
  27. package/dist/commands/init.js +12 -12
  28. package/dist/commands/init.js.map +1 -1
  29. package/dist/commands/lint.d.ts +23 -23
  30. package/dist/commands/lint.d.ts.map +1 -1
  31. package/dist/commands/lint.js +25 -25
  32. package/dist/commands/lint.js.map +1 -1
  33. package/dist/commands/publish.d.ts +13 -13
  34. package/dist/commands/publish.d.ts.map +1 -1
  35. package/dist/commands/publish.js +61 -61
  36. package/dist/commands/publish.js.map +1 -1
  37. package/dist/commands/replace-deps.d.ts +3 -3
  38. package/dist/commands/replace-deps.d.ts.map +1 -1
  39. package/dist/commands/replace-deps.js +1 -1
  40. package/dist/commands/replace-deps.js.map +1 -1
  41. package/dist/commands/typecheck.d.ts +20 -20
  42. package/dist/commands/typecheck.d.ts.map +1 -1
  43. package/dist/commands/typecheck.js +20 -20
  44. package/dist/commands/typecheck.js.map +1 -1
  45. package/dist/commands/watch.d.ts +7 -7
  46. package/dist/electron/electron.d.ts +27 -27
  47. package/dist/electron/electron.js +32 -32
  48. package/dist/electron/electron.js.map +1 -1
  49. package/dist/infra/ResultCollector.d.ts +9 -9
  50. package/dist/infra/ResultCollector.js +5 -5
  51. package/dist/infra/SignalHandler.d.ts +7 -7
  52. package/dist/infra/SignalHandler.js +4 -4
  53. package/dist/infra/WorkerManager.d.ts +14 -14
  54. package/dist/infra/WorkerManager.js +11 -11
  55. package/dist/orchestrators/BuildOrchestrator.d.ts +19 -19
  56. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  57. package/dist/orchestrators/BuildOrchestrator.js +26 -26
  58. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  59. package/dist/orchestrators/DevOrchestrator.d.ts +25 -25
  60. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  61. package/dist/orchestrators/DevOrchestrator.js +30 -30
  62. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  63. package/dist/orchestrators/WatchOrchestrator.d.ts +13 -13
  64. package/dist/orchestrators/WatchOrchestrator.js +17 -17
  65. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  66. package/dist/sd-cli-entry.d.ts +2 -2
  67. package/dist/sd-cli-entry.js +38 -38
  68. package/dist/sd-cli-entry.js.map +1 -1
  69. package/dist/sd-cli.d.ts +2 -2
  70. package/dist/sd-cli.js +1 -1
  71. package/dist/sd-cli.js.map +1 -1
  72. package/dist/sd-config.types.d.ts +84 -84
  73. package/dist/sd-config.types.d.ts.map +1 -1
  74. package/dist/utils/build-env.d.ts +1 -1
  75. package/dist/utils/config-editor.d.ts +5 -5
  76. package/dist/utils/config-editor.js +2 -2
  77. package/dist/utils/config-editor.js.map +1 -1
  78. package/dist/utils/copy-public.d.ts +9 -9
  79. package/dist/utils/copy-src.d.ts +9 -9
  80. package/dist/utils/esbuild-config.d.ts +30 -30
  81. package/dist/utils/esbuild-config.d.ts.map +1 -1
  82. package/dist/utils/output-utils.d.ts +6 -6
  83. package/dist/utils/package-utils.d.ts +6 -6
  84. package/dist/utils/package-utils.js +1 -1
  85. package/dist/utils/package-utils.js.map +1 -1
  86. package/dist/utils/rebuild-manager.js +3 -3
  87. package/dist/utils/rebuild-manager.js.map +1 -1
  88. package/dist/utils/replace-deps.d.ts +25 -25
  89. package/dist/utils/replace-deps.js +3 -3
  90. package/dist/utils/replace-deps.js.map +1 -1
  91. package/dist/utils/sd-config.d.ts +3 -3
  92. package/dist/utils/sd-config.js +3 -3
  93. package/dist/utils/sd-config.js.map +1 -1
  94. package/dist/utils/tailwind-config-deps.d.ts +3 -3
  95. package/dist/utils/template.d.ts +8 -8
  96. package/dist/utils/tsconfig.d.ts +16 -16
  97. package/dist/utils/tsconfig.js +2 -2
  98. package/dist/utils/tsconfig.js.map +1 -1
  99. package/dist/utils/typecheck-serialization.d.ts +8 -8
  100. package/dist/utils/vite-config.d.ts +8 -8
  101. package/dist/utils/vite-config.d.ts.map +1 -1
  102. package/dist/utils/vite-config.js +3 -3
  103. package/dist/utils/worker-events.d.ts +12 -12
  104. package/dist/utils/worker-events.d.ts.map +1 -1
  105. package/dist/utils/worker-utils.d.ts +3 -3
  106. package/dist/utils/worker-utils.js +2 -2
  107. package/dist/utils/worker-utils.js.map +1 -1
  108. package/dist/workers/client.worker.d.ts +14 -14
  109. package/dist/workers/client.worker.d.ts.map +1 -1
  110. package/dist/workers/client.worker.js +1 -1
  111. package/dist/workers/client.worker.js.map +1 -1
  112. package/dist/workers/dts.worker.d.ts +13 -13
  113. package/dist/workers/dts.worker.d.ts.map +1 -1
  114. package/dist/workers/dts.worker.js +3 -3
  115. package/dist/workers/dts.worker.js.map +1 -1
  116. package/dist/workers/library.worker.d.ts +12 -12
  117. package/dist/workers/library.worker.js +1 -1
  118. package/dist/workers/library.worker.js.map +1 -1
  119. package/dist/workers/lint.worker.d.ts +1 -1
  120. package/dist/workers/server-runtime.worker.d.ts +6 -6
  121. package/dist/workers/server-runtime.worker.js +6 -6
  122. package/dist/workers/server-runtime.worker.js.map +1 -1
  123. package/dist/workers/server.worker.d.ts +20 -20
  124. package/dist/workers/server.worker.d.ts.map +1 -1
  125. package/dist/workers/server.worker.js +6 -6
  126. package/dist/workers/server.worker.js.map +1 -1
  127. package/package.json +8 -7
  128. package/src/builders/BaseBuilder.ts +33 -33
  129. package/src/builders/DtsBuilder.ts +5 -5
  130. package/src/builders/LibraryBuilder.ts +9 -9
  131. package/src/builders/types.ts +10 -10
  132. package/src/capacitor/capacitor.ts +119 -119
  133. package/src/commands/add-client.ts +31 -31
  134. package/src/commands/add-server.ts +34 -34
  135. package/src/commands/build.ts +9 -9
  136. package/src/commands/check.ts +5 -5
  137. package/src/commands/dev.ts +9 -9
  138. package/src/commands/device.ts +30 -30
  139. package/src/commands/init.ts +25 -25
  140. package/src/commands/lint.ts +64 -64
  141. package/src/commands/publish.ts +139 -139
  142. package/src/commands/replace-deps.ts +4 -4
  143. package/src/commands/typecheck.ts +74 -74
  144. package/src/commands/watch.ts +7 -7
  145. package/src/electron/electron.ts +51 -51
  146. package/src/infra/ResultCollector.ts +9 -9
  147. package/src/infra/SignalHandler.ts +7 -7
  148. package/src/infra/WorkerManager.ts +14 -14
  149. package/src/orchestrators/BuildOrchestrator.ts +76 -76
  150. package/src/orchestrators/DevOrchestrator.ts +88 -88
  151. package/src/orchestrators/WatchOrchestrator.ts +39 -39
  152. package/src/sd-cli-entry.ts +43 -43
  153. package/src/sd-cli.ts +15 -15
  154. package/src/sd-config.types.ts +85 -85
  155. package/src/utils/build-env.ts +1 -1
  156. package/src/utils/config-editor.ts +19 -19
  157. package/src/utils/copy-public.ts +17 -17
  158. package/src/utils/copy-src.ts +11 -11
  159. package/src/utils/esbuild-config.ts +33 -33
  160. package/src/utils/output-utils.ts +11 -11
  161. package/src/utils/package-utils.ts +12 -12
  162. package/src/utils/rebuild-manager.ts +3 -3
  163. package/src/utils/replace-deps.ts +361 -361
  164. package/src/utils/sd-config.ts +44 -44
  165. package/src/utils/tailwind-config-deps.ts +98 -98
  166. package/src/utils/template.ts +56 -56
  167. package/src/utils/tsconfig.ts +127 -127
  168. package/src/utils/typecheck-serialization.ts +86 -86
  169. package/src/utils/vite-config.ts +341 -341
  170. package/src/utils/worker-events.ts +16 -16
  171. package/src/utils/worker-utils.ts +45 -45
  172. package/src/workers/client.worker.ts +34 -34
  173. package/src/workers/dts.worker.ts +467 -467
  174. package/src/workers/library.worker.ts +314 -314
  175. package/src/workers/lint.worker.ts +16 -16
  176. package/src/workers/server-runtime.worker.ts +157 -157
  177. package/src/workers/server.worker.ts +572 -572
  178. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  179. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  180. package/templates/init/package.json.hbs +3 -3
  181. package/tests/config-editor.spec.ts +160 -0
  182. package/tests/copy-src.spec.ts +50 -0
  183. package/tests/get-compiler-options-for-package.spec.ts +139 -0
  184. package/tests/get-package-source-files.spec.ts +181 -0
  185. package/tests/get-types-from-package-json.spec.ts +107 -0
  186. package/tests/infra/ResultCollector.spec.ts +39 -0
  187. package/tests/infra/SignalHandler.spec.ts +38 -0
  188. package/tests/infra/WorkerManager.spec.ts +97 -0
  189. package/tests/load-ignore-patterns.spec.ts +188 -0
  190. package/tests/load-sd-config.spec.ts +137 -0
  191. package/tests/package-utils.spec.ts +188 -0
  192. package/tests/parse-root-tsconfig.spec.ts +89 -0
  193. package/tests/replace-deps.spec.ts +308 -0
  194. package/tests/run-lint.spec.ts +415 -0
  195. package/tests/run-typecheck.spec.ts +653 -0
  196. package/tests/run-watch.spec.ts +75 -0
  197. package/tests/sd-cli.spec.ts +330 -0
  198. package/tests/tailwind-config-deps.spec.ts +30 -0
  199. package/tests/template.spec.ts +70 -0
  200. package/tests/utils/rebuild-manager.spec.ts +43 -0
  201. package/tests/write-changed-output-files.spec.ts +97 -0
@@ -1,572 +1,572 @@
1
- import path from "path";
2
- import fs from "fs";
3
- import { execaSync } from "execa";
4
- import esbuild from "esbuild";
5
- import { createWorker, FsWatcher, pathNorm } from "@simplysm/core-node";
6
- import { errorMessage } from "@simplysm/core-common";
7
- import { consola } from "consola";
8
- import {
9
- parseRootTsconfig,
10
- getPackageSourceFiles,
11
- getCompilerOptionsForPackage,
12
- } from "../utils/tsconfig";
13
- import {
14
- createServerEsbuildOptions,
15
- collectUninstalledOptionalPeerDeps,
16
- collectNativeModuleExternals,
17
- writeChangedOutputFiles,
18
- } from "../utils/esbuild-config";
19
- import { registerCleanupHandlers, createOnceGuard } from "../utils/worker-utils";
20
- import { collectDeps } from "../utils/package-utils";
21
- import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
22
-
23
- //#region Types
24
-
25
- /**
26
- * Server 빌드 정보 (일회성 빌드용)
27
- */
28
- export interface ServerBuildInfo {
29
- name: string;
30
- cwd: string;
31
- pkgDir: string;
32
- /** 빌드 치환할 환경변수 */
33
- env?: Record<string, string>;
34
- /** 런타임 설정 (dist/.config.json에 기록) */
35
- configs?: Record<string, unknown>;
36
- /** sd.config.ts에서 수동 지정한 external 모듈 */
37
- externals?: string[];
38
- /** PM2 설정 (지정 dist/pm2.config.cjs 생성) */
39
- pm2?: {
40
- name?: string;
41
- ignoreWatchPaths?: string[];
42
- };
43
- /** Package manager to use (affects mise.toml or volta settings generation) */
44
- packageManager?: "volta" | "mise";
45
- }
46
-
47
- /**
48
- * Server 빌드 결과
49
- */
50
- export interface ServerBuildResult {
51
- success: boolean;
52
- mainJsPath: string;
53
- errors?: string[];
54
- warnings?: string[];
55
- }
56
-
57
- /**
58
- * Server Watch 정보
59
- */
60
- export interface ServerWatchInfo {
61
- name: string;
62
- cwd: string;
63
- pkgDir: string;
64
- /** 빌드 치환할 환경변수 */
65
- env?: Record<string, string>;
66
- /** 런타임 설정 (dist/.config.json에 기록) */
67
- configs?: Record<string, unknown>;
68
- /** sd.config.ts에서 수동 지정한 external 모듈 */
69
- externals?: string[];
70
- /** sd.config.ts replaceDeps 설정 */
71
- replaceDeps?: Record<string, string>;
72
- }
73
-
74
- /**
75
- * 빌드 이벤트
76
- */
77
- export interface ServerBuildEvent {
78
- success: boolean;
79
- mainJsPath: string;
80
- errors?: string[];
81
- warnings?: string[];
82
- }
83
-
84
- /**
85
- * 에러 이벤트
86
- */
87
- export interface ServerErrorEvent {
88
- message: string;
89
- }
90
-
91
- /**
92
- * Worker 이벤트 타입
93
- */
94
- export interface ServerWorkerEvents extends Record<string, unknown> {
95
- buildStart: Record<string, never>;
96
- build: ServerBuildEvent;
97
- error: ServerErrorEvent;
98
- }
99
-
100
- //#endregion
101
-
102
- //#region 리소스 관리
103
-
104
- const logger = consola.withTag("sd:cli:server:worker");
105
-
106
- /** esbuild build context (정리 대상) */
107
- let esbuildContext: esbuild.BuildContext | undefined;
108
-
109
- /** 마지막 빌드의 metafile (rebuild 변경 파일 필터링용) */
110
- let lastMetafile: esbuild.Metafile | undefined;
111
-
112
- /** public 파일 watcher (정리 대상) */
113
- let publicWatcher: FsWatcher | undefined;
114
-
115
- /** 소스 + scope 패키지 watcher (정리 대상) */
116
- let srcWatcher: FsWatcher | undefined;
117
-
118
- /**
119
- * 리소스 정리
120
- */
121
- async function cleanup(): Promise<void> {
122
- // 전역 변수를 임시 변수로 캡처 초기화
123
- // (Promise.all 대기 다른 호출에서 전역 변수를 수정할 있으므로)
124
- const contextToDispose = esbuildContext;
125
- esbuildContext = undefined;
126
- lastMetafile = undefined;
127
-
128
- const watcherToClose = publicWatcher;
129
- publicWatcher = undefined;
130
-
131
- const srcWatcherToClose = srcWatcher;
132
- srcWatcher = undefined;
133
-
134
- if (contextToDispose != null) {
135
- await contextToDispose.dispose();
136
- }
137
-
138
- if (watcherToClose != null) {
139
- await watcherToClose.close();
140
- }
141
-
142
- if (srcWatcherToClose != null) {
143
- await srcWatcherToClose.close();
144
- }
145
- }
146
-
147
- /**
148
- * 가지 소스에서 external 모듈을 수집하여 합친다.
149
- * 1. 미설치 optional peer deps
150
- * 2. binding.gyp 네이티브 모듈
151
- * 3. sd.config.ts 수동 지정
152
- */
153
- function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
154
- const optionalPeerDeps = collectUninstalledOptionalPeerDeps(pkgDir);
155
- const nativeModules = collectNativeModuleExternals(pkgDir);
156
- const manual = manualExternals ?? [];
157
-
158
- const merged = [...new Set([...optionalPeerDeps, ...nativeModules, ...manual])];
159
-
160
- if (optionalPeerDeps.length > 0) {
161
- logger.debug("미설치 optional peer deps (external):", optionalPeerDeps);
162
- }
163
- if (nativeModules.length > 0) {
164
- logger.debug("네이티브 모듈 (external):", nativeModules);
165
- }
166
- if (manual.length > 0) {
167
- logger.debug("수동 지정 (external):", manual);
168
- }
169
-
170
- return merged;
171
- }
172
-
173
- /**
174
- * 프로덕션 배포용 파일 생성 (일회성 빌드에서만 호출)
175
- *
176
- * - dist/package.json: external 모듈을 dependencies 포함 (volta 사용 volta 필드 추가)
177
- * - dist/mise.toml: Node 버전 지정 (packageManager === "mise"일 때만)
178
- * - dist/openssl.cnf: 레거시 OpenSSL 프로바이더 활성화
179
- * - dist/pm2.config.cjs: PM2 프로세스 설정 (pm2 옵션이 있을 때만)
180
- */
181
- function generateProductionFiles(info: ServerBuildInfo, externals: string[]): void {
182
- const distDir = path.join(info.pkgDir, "dist");
183
- const pkgJson = JSON.parse(fs.readFileSync(path.join(info.pkgDir, "package.json"), "utf-8"));
184
-
185
- // dist/package.json
186
- logger.debug("GEN package.json...");
187
- const distPkgJson: Record<string, unknown> = {
188
- name: pkgJson.name,
189
- version: pkgJson.version,
190
- type: pkgJson.type,
191
- };
192
- if (externals.length > 0) {
193
- const deps: Record<string, string> = {};
194
- for (const ext of externals) {
195
- deps[ext] = "*";
196
- }
197
- distPkgJson["dependencies"] = deps;
198
- }
199
- if (info.packageManager === "volta") {
200
- const nodeVersion = execaSync("node", ["-v"]).stdout.trim();
201
- distPkgJson["volta"] = { node: nodeVersion };
202
- }
203
- fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify(distPkgJson, undefined, 2));
204
-
205
- // dist/mise.toml (packageManager === "mise"일 때만)
206
- if (info.packageManager === "mise") {
207
- logger.debug("GEN mise.toml...");
208
- const rootMiseTomlPath = path.join(info.cwd, "mise.toml");
209
- let nodeVersion = "20";
210
- if (fs.existsSync(rootMiseTomlPath)) {
211
- const miseContent = fs.readFileSync(rootMiseTomlPath, "utf-8");
212
- const match = /node\s*=\s*"([^"]+)"/.exec(miseContent);
213
- if (match != null) {
214
- nodeVersion = match[1];
215
- }
216
- }
217
- fs.writeFileSync(path.join(distDir, "mise.toml"), `[tools]\nnode = "${nodeVersion}"\n`);
218
- }
219
-
220
- // dist/openssl.cnf
221
- logger.debug("GEN openssl.cnf...");
222
- fs.writeFileSync(
223
- path.join(distDir, "openssl.cnf"),
224
- [
225
- "nodejs_conf = openssl_init",
226
- "",
227
- "[openssl_init]",
228
- "providers = provider_sect",
229
- "ssl_conf = ssl_sect",
230
- "",
231
- "[provider_sect]",
232
- "default = default_sect",
233
- "legacy = legacy_sect",
234
- "",
235
- "[default_sect]",
236
- "activate = 1",
237
- "",
238
- "[legacy_sect]",
239
- "activate = 1",
240
- "",
241
- "[ssl_sect]",
242
- "system_default = system_default_sect",
243
- "",
244
- "[system_default_sect]",
245
- "Options = UnsafeLegacyRenegotiation",
246
- ].join("\n"),
247
- );
248
-
249
- // dist/pm2.config.cjs (pm2 설정이 있을 때만)
250
- if (info.pm2 != null) {
251
- logger.debug("GEN pm2.config.cjs...");
252
-
253
- const pm2Name = info.pm2.name ?? pkgJson.name.replace(/@/g, "").replace(/[/\\]/g, "-");
254
- const ignoreWatch = JSON.stringify([
255
- "node_modules",
256
- "www",
257
- ...(info.pm2.ignoreWatchPaths ?? []),
258
- ]);
259
- const envObj: Record<string, string> = {
260
- NODE_ENV: "production",
261
- TZ: "Asia/Seoul",
262
- ...(info.env ?? {}),
263
- };
264
- const envStr = JSON.stringify(envObj, undefined, 4);
265
-
266
- const interpreterLine =
267
- info.packageManager === "volta"
268
- ? ""
269
- : ` interpreter: cp.execSync("mise which node").toString().trim(),\n`;
270
-
271
- const pm2Config = [
272
- ...(info.packageManager !== "volta" ? [`const cp = require("child_process");`, ``] : []),
273
- `module.exports = {`,
274
- ` name: ${JSON.stringify(pm2Name)},`,
275
- ` script: "main.js",`,
276
- ` watch: true,`,
277
- ` watch_delay: 2000,`,
278
- ` ignore_watch: ${ignoreWatch},`,
279
- interpreterLine.trimEnd(),
280
- ` interpreter_args: "--openssl-config=openssl.cnf",`,
281
- ` env: ${envStr.replace(/\n/g, "\n ")},`,
282
- ` arrayProcess: "concat",`,
283
- ` useDelTargetNull: true,`,
284
- `};`,
285
- ]
286
- .filter((line) => line !== "")
287
- .join("\n");
288
-
289
- fs.writeFileSync(path.join(distDir, "pm2.config.cjs"), pm2Config);
290
- }
291
- }
292
-
293
- // 프로세스 종료 리소스 정리 (SIGTERM/SIGINT)
294
- // 주의: worker.terminate() 핸들러들을 호출하지 않고 즉시 종료됨.
295
- // 그러나 watch 모드에서 정상 종료는 메인 프로세스의 SIGINT/SIGTERM 통해 이루어지므로 문제없음.
296
- registerCleanupHandlers(cleanup, logger);
297
-
298
- //#endregion
299
-
300
- //#region Worker
301
-
302
- /**
303
- * 일회성 빌드
304
- */
305
- async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
306
- const mainJsPath = path.join(info.pkgDir, "dist", "main.js");
307
-
308
- try {
309
- // tsconfig 파싱
310
- const parsedConfig = parseRootTsconfig(info.cwd);
311
- const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
312
-
313
- // 서버는 node 환경
314
- const compilerOptions = await getCompilerOptionsForPackage(
315
- parsedConfig.options,
316
- "node",
317
- info.pkgDir,
318
- );
319
-
320
- // 모든 external 수집 (optional peer deps + native modules + manual)
321
- const external = collectAllExternals(info.pkgDir, info.externals);
322
-
323
- // esbuild 일회성 빌드
324
- const esbuildOptions = createServerEsbuildOptions({
325
- pkgDir: info.pkgDir,
326
- entryPoints,
327
- compilerOptions,
328
- env: info.env,
329
- external,
330
- });
331
-
332
- const result = await esbuild.build(esbuildOptions);
333
-
334
- // Generate .config.json
335
- const confDistPath = path.join(info.pkgDir, "dist", ".config.json");
336
- fs.writeFileSync(confDistPath, JSON.stringify(info.configs ?? {}, undefined, 2));
337
-
338
- // Copy public/ to dist/ (production build: no public-dev)
339
- await copyPublicFiles(info.pkgDir, false);
340
-
341
- // Generate production files (package.json, mise.toml, openssl.cnf, pm2.config.cjs)
342
- generateProductionFiles(info, external);
343
-
344
- const errors = result.errors.map((e) => e.text);
345
- const warnings = result.warnings.map((w) => w.text);
346
- return {
347
- success: result.errors.length === 0,
348
- mainJsPath,
349
- errors: errors.length > 0 ? errors : undefined,
350
- warnings: warnings.length > 0 ? warnings : undefined,
351
- };
352
- } catch (err) {
353
- return {
354
- success: false,
355
- mainJsPath,
356
- errors: [errorMessage(err)],
357
- };
358
- }
359
- }
360
-
361
- const guardStartWatch = createOnceGuard("startWatch");
362
-
363
- /**
364
- * esbuild context 생성 초기 빌드 수행
365
- */
366
- async function createAndBuildContext(
367
- info: ServerWatchInfo,
368
- isFirstBuild: boolean,
369
- resolveFirstBuild?: () => void,
370
- ): Promise<esbuild.BuildContext> {
371
- const parsedConfig = parseRootTsconfig(info.cwd);
372
- const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
373
- const compilerOptions = await getCompilerOptionsForPackage(
374
- parsedConfig.options,
375
- "node",
376
- info.pkgDir,
377
- );
378
-
379
- const mainJsPath = path.join(info.pkgDir, "dist", "main.js");
380
- const external = collectAllExternals(info.pkgDir, info.externals);
381
- const baseOptions = createServerEsbuildOptions({
382
- pkgDir: info.pkgDir,
383
- entryPoints,
384
- compilerOptions,
385
- env: info.env,
386
- external,
387
- });
388
-
389
- let isBuildFirstTime = isFirstBuild;
390
-
391
- const context = await esbuild.context({
392
- ...baseOptions,
393
- metafile: true,
394
- write: false,
395
- plugins: [
396
- {
397
- name: "watch-notify",
398
- setup(pluginBuild) {
399
- pluginBuild.onStart(() => {
400
- sender.send("buildStart", {});
401
- });
402
-
403
- pluginBuild.onEnd(async (result) => {
404
- // metafile 저장
405
- if (result.metafile != null) {
406
- lastMetafile = result.metafile;
407
- }
408
-
409
- const errors = result.errors.map((e) => e.text);
410
- const warnings = result.warnings.map((w) => w.text);
411
- const success = result.errors.length === 0;
412
-
413
- // output 파일 쓰기 변경 여부 확인
414
- let hasOutputChange = false;
415
- if (success && result.outputFiles != null) {
416
- hasOutputChange = await writeChangedOutputFiles(result.outputFiles);
417
- }
418
-
419
- if (isBuildFirstTime && success) {
420
- const confDistPath = path.join(info.pkgDir, "dist", ".config.json");
421
- fs.writeFileSync(confDistPath, JSON.stringify(info.configs ?? {}, undefined, 2));
422
- }
423
-
424
- // 빌드이거나, output이 변경되었거나, 에러인 경우에만 build 이벤트 발생
425
- if (isBuildFirstTime || hasOutputChange || !success) {
426
- sender.send("build", {
427
- success,
428
- mainJsPath,
429
- errors: errors.length > 0 ? errors : undefined,
430
- warnings: warnings.length > 0 ? warnings : undefined,
431
- });
432
- } else {
433
- logger.debug("output 변경 없음, 서버 재시작 skip");
434
- }
435
-
436
- if (isBuildFirstTime) {
437
- isBuildFirstTime = false;
438
- resolveFirstBuild?.();
439
- }
440
- });
441
- },
442
- },
443
- ],
444
- });
445
-
446
- await context.rebuild();
447
-
448
- return context;
449
- }
450
-
451
- /**
452
- * watch 시작
453
- * @remarks 함수는 Worker당 번만 호출되어야 합니다.
454
- * @throws 이미 watch 시작된 경우
455
- */
456
- async function startWatch(info: ServerWatchInfo): Promise<void> {
457
- guardStartWatch();
458
-
459
- try {
460
- // 번째 빌드 완료 대기를 위한 Promise
461
- let resolveFirstBuild!: () => void;
462
- const firstBuildPromise = new Promise<void>((resolve) => {
463
- resolveFirstBuild = resolve;
464
- });
465
-
466
- // 초기 esbuild context 생성 및 빌드
467
- esbuildContext = await createAndBuildContext(info, true, resolveFirstBuild);
468
-
469
- // 번째 빌드 완료 대기
470
- await firstBuildPromise;
471
-
472
- // Watch public/ and public-dev/ (dev mode includes public-dev)
473
- publicWatcher = await watchPublicFiles(info.pkgDir, true);
474
-
475
- // 의존성 기반 감시 경로 수집
476
- const { workspaceDeps, replaceDeps } = collectDeps(info.pkgDir, info.cwd, info.replaceDeps);
477
-
478
- const watchPaths: string[] = [];
479
-
480
- // 1) 서버 패키지 자신 + workspace 의존 패키지 소스
481
- const watchDirs = [
482
- info.pkgDir,
483
- ...workspaceDeps.map((d) => path.join(info.cwd, "packages", d)),
484
- ];
485
- for (const dir of watchDirs) {
486
- watchPaths.push(path.join(dir, "src", "**", "*"));
487
- watchPaths.push(path.join(dir, "*.{ts,js,css}"));
488
- }
489
-
490
- // 2) replaceDeps 의존 패키지 dist (루트 + 패키지 node_modules)
491
- for (const pkg of replaceDeps) {
492
- watchPaths.push(path.join(info.cwd, "node_modules", ...pkg.split("/"), "dist", "**", "*.js"));
493
- watchPaths.push(
494
- path.join(info.pkgDir, "node_modules", ...pkg.split("/"), "dist", "**", "*.js"),
495
- );
496
- }
497
-
498
- // FsWatcher 시작
499
- srcWatcher = await FsWatcher.watch(watchPaths);
500
-
501
- // 파일 변경 감지 시 처리
502
- srcWatcher.onChange({ delay: 300 }, async (changes) => {
503
- try {
504
- // 파일 추가/삭제가 있으면 context 재생성 (import graph 변경 가능)
505
- const hasFileAddOrRemove = changes.some((c) => c.event === "add" || c.event === "unlink");
506
-
507
- if (hasFileAddOrRemove) {
508
- logger.debug("파일 추가/삭제 감지, context 재생성");
509
-
510
- const oldContext = esbuildContext;
511
- esbuildContext = await createAndBuildContext(info, false);
512
-
513
- if (oldContext != null) {
514
- await oldContext.dispose();
515
- }
516
- return;
517
- }
518
-
519
- // 파일 변경만 있는 경우: metafile 필터링
520
- if (esbuildContext == null) return;
521
-
522
- // metafile 없으면 ( 빌드 ) 무조건 rebuild
523
- if (lastMetafile == null) {
524
- await esbuildContext.rebuild();
525
- return;
526
- }
527
-
528
- // metafile.inputs 키를 절대경로(NormPath) 변환하여 비교
529
- const metafileAbsPaths = new Set(
530
- Object.keys(lastMetafile.inputs).map((key) => pathNorm(info.cwd, key)),
531
- );
532
-
533
- const hasRelevantChange = changes.some((c) => metafileAbsPaths.has(c.path));
534
-
535
- if (hasRelevantChange) {
536
- await esbuildContext.rebuild();
537
- } else {
538
- logger.debug("변경된 파일이 빌드에 포함되지 않음, rebuild skip");
539
- }
540
- } catch (err) {
541
- sender.send("error", {
542
- message: errorMessage(err),
543
- });
544
- }
545
- });
546
- } catch (err) {
547
- sender.send("error", {
548
- message: errorMessage(err),
549
- });
550
- }
551
- }
552
-
553
- /**
554
- * watch 중지
555
- * @remarks esbuild context를 정리합니다.
556
- */
557
- async function stopWatch(): Promise<void> {
558
- await cleanup();
559
- }
560
-
561
- const sender = createWorker<
562
- { build: typeof build; startWatch: typeof startWatch; stopWatch: typeof stopWatch },
563
- ServerWorkerEvents
564
- >({
565
- build,
566
- startWatch,
567
- stopWatch,
568
- });
569
-
570
- export default sender;
571
-
572
- //#endregion
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { execaSync } from "execa";
4
+ import esbuild from "esbuild";
5
+ import { createWorker, FsWatcher, pathNorm } from "@simplysm/core-node";
6
+ import { errorMessage } from "@simplysm/core-common";
7
+ import { consola } from "consola";
8
+ import {
9
+ parseRootTsconfig,
10
+ getPackageSourceFiles,
11
+ getCompilerOptionsForPackage,
12
+ } from "../utils/tsconfig";
13
+ import {
14
+ createServerEsbuildOptions,
15
+ collectUninstalledOptionalPeerDeps,
16
+ collectNativeModuleExternals,
17
+ writeChangedOutputFiles,
18
+ } from "../utils/esbuild-config";
19
+ import { registerCleanupHandlers, createOnceGuard } from "../utils/worker-utils";
20
+ import { collectDeps } from "../utils/package-utils";
21
+ import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
22
+
23
+ //#region Types
24
+
25
+ /**
26
+ * Server build information (for one-time build)
27
+ */
28
+ export interface ServerBuildInfo {
29
+ name: string;
30
+ cwd: string;
31
+ pkgDir: string;
32
+ /** Environment variables to substitute during build */
33
+ env?: Record<string, string>;
34
+ /** Runtime configuration (recorded in dist/.config.json) */
35
+ configs?: Record<string, unknown>;
36
+ /** External modules manually specified in sd.config.ts */
37
+ externals?: string[];
38
+ /** PM2 configuration (generates dist/pm2.config.cjs when specified) */
39
+ pm2?: {
40
+ name?: string;
41
+ ignoreWatchPaths?: string[];
42
+ };
43
+ /** Package manager to use (affects mise.toml or volta settings generation) */
44
+ packageManager?: "volta" | "mise";
45
+ }
46
+
47
+ /**
48
+ * Server build result
49
+ */
50
+ export interface ServerBuildResult {
51
+ success: boolean;
52
+ mainJsPath: string;
53
+ errors?: string[];
54
+ warnings?: string[];
55
+ }
56
+
57
+ /**
58
+ * Server watch information
59
+ */
60
+ export interface ServerWatchInfo {
61
+ name: string;
62
+ cwd: string;
63
+ pkgDir: string;
64
+ /** Environment variables to substitute during build */
65
+ env?: Record<string, string>;
66
+ /** Runtime configuration (recorded in dist/.config.json) */
67
+ configs?: Record<string, unknown>;
68
+ /** External modules manually specified in sd.config.ts */
69
+ externals?: string[];
70
+ /** replaceDeps configuration from sd.config.ts */
71
+ replaceDeps?: Record<string, string>;
72
+ }
73
+
74
+ /**
75
+ * Build event
76
+ */
77
+ export interface ServerBuildEvent {
78
+ success: boolean;
79
+ mainJsPath: string;
80
+ errors?: string[];
81
+ warnings?: string[];
82
+ }
83
+
84
+ /**
85
+ * Error event
86
+ */
87
+ export interface ServerErrorEvent {
88
+ message: string;
89
+ }
90
+
91
+ /**
92
+ * Worker event types
93
+ */
94
+ export interface ServerWorkerEvents extends Record<string, unknown> {
95
+ buildStart: Record<string, never>;
96
+ build: ServerBuildEvent;
97
+ error: ServerErrorEvent;
98
+ }
99
+
100
+ //#endregion
101
+
102
+ //#region Resource Management
103
+
104
+ const logger = consola.withTag("sd:cli:server:worker");
105
+
106
+ /** esbuild build context (to be cleaned up) */
107
+ let esbuildContext: esbuild.BuildContext | undefined;
108
+
109
+ /** Last build metafile (for filtering changed files on rebuild) */
110
+ let lastMetafile: esbuild.Metafile | undefined;
111
+
112
+ /** Public files watcher (to be cleaned up) */
113
+ let publicWatcher: FsWatcher | undefined;
114
+
115
+ /** Source + scope packages watcher (to be cleaned up) */
116
+ let srcWatcher: FsWatcher | undefined;
117
+
118
+ /**
119
+ * Clean up resources
120
+ */
121
+ async function cleanup(): Promise<void> {
122
+ // Capture global variables to temporary variables and initialize
123
+ // (other calls can modify global variables while Promise.all is waiting)
124
+ const contextToDispose = esbuildContext;
125
+ esbuildContext = undefined;
126
+ lastMetafile = undefined;
127
+
128
+ const watcherToClose = publicWatcher;
129
+ publicWatcher = undefined;
130
+
131
+ const srcWatcherToClose = srcWatcher;
132
+ srcWatcher = undefined;
133
+
134
+ if (contextToDispose != null) {
135
+ await contextToDispose.dispose();
136
+ }
137
+
138
+ if (watcherToClose != null) {
139
+ await watcherToClose.close();
140
+ }
141
+
142
+ if (srcWatcherToClose != null) {
143
+ await srcWatcherToClose.close();
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Collect external modules from three sources and merge them.
149
+ * 1. Uninstalled optional peer dependencies
150
+ * 2. Native modules from binding.gyp
151
+ * 3. Manually specified in sd.config.ts
152
+ */
153
+ function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
154
+ const optionalPeerDeps = collectUninstalledOptionalPeerDeps(pkgDir);
155
+ const nativeModules = collectNativeModuleExternals(pkgDir);
156
+ const manual = manualExternals ?? [];
157
+
158
+ const merged = [...new Set([...optionalPeerDeps, ...nativeModules, ...manual])];
159
+
160
+ if (optionalPeerDeps.length > 0) {
161
+ logger.debug("Uninstalled optional peer deps (external):", optionalPeerDeps);
162
+ }
163
+ if (nativeModules.length > 0) {
164
+ logger.debug("Native modules (external):", nativeModules);
165
+ }
166
+ if (manual.length > 0) {
167
+ logger.debug("Manually specified (external):", manual);
168
+ }
169
+
170
+ return merged;
171
+ }
172
+
173
+ /**
174
+ * Generate files for production deployment (called only in one-time build)
175
+ *
176
+ * - dist/package.json: include external modules as dependencies (add volta field if volta is used)
177
+ * - dist/mise.toml: specify Node version (only when packageManager === "mise")
178
+ * - dist/openssl.cnf: 레거시 OpenSSL 프로바이더 활성화
179
+ * - dist/pm2.config.cjs: PM2 프로세스 설정 (pm2 옵션이 있을 때만)
180
+ */
181
+ function generateProductionFiles(info: ServerBuildInfo, externals: string[]): void {
182
+ const distDir = path.join(info.pkgDir, "dist");
183
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(info.pkgDir, "package.json"), "utf-8"));
184
+
185
+ // dist/package.json
186
+ logger.debug("GEN package.json...");
187
+ const distPkgJson: Record<string, unknown> = {
188
+ name: pkgJson.name,
189
+ version: pkgJson.version,
190
+ type: pkgJson.type,
191
+ };
192
+ if (externals.length > 0) {
193
+ const deps: Record<string, string> = {};
194
+ for (const ext of externals) {
195
+ deps[ext] = "*";
196
+ }
197
+ distPkgJson["dependencies"] = deps;
198
+ }
199
+ if (info.packageManager === "volta") {
200
+ const nodeVersion = execaSync("node", ["-v"]).stdout.trim();
201
+ distPkgJson["volta"] = { node: nodeVersion };
202
+ }
203
+ fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify(distPkgJson, undefined, 2));
204
+
205
+ // dist/mise.toml (packageManager === "mise"일 때만)
206
+ if (info.packageManager === "mise") {
207
+ logger.debug("GEN mise.toml...");
208
+ const rootMiseTomlPath = path.join(info.cwd, "mise.toml");
209
+ let nodeVersion = "20";
210
+ if (fs.existsSync(rootMiseTomlPath)) {
211
+ const miseContent = fs.readFileSync(rootMiseTomlPath, "utf-8");
212
+ const match = /node\s*=\s*"([^"]+)"/.exec(miseContent);
213
+ if (match != null) {
214
+ nodeVersion = match[1];
215
+ }
216
+ }
217
+ fs.writeFileSync(path.join(distDir, "mise.toml"), `[tools]\nnode = "${nodeVersion}"\n`);
218
+ }
219
+
220
+ // dist/openssl.cnf
221
+ logger.debug("GEN openssl.cnf...");
222
+ fs.writeFileSync(
223
+ path.join(distDir, "openssl.cnf"),
224
+ [
225
+ "nodejs_conf = openssl_init",
226
+ "",
227
+ "[openssl_init]",
228
+ "providers = provider_sect",
229
+ "ssl_conf = ssl_sect",
230
+ "",
231
+ "[provider_sect]",
232
+ "default = default_sect",
233
+ "legacy = legacy_sect",
234
+ "",
235
+ "[default_sect]",
236
+ "activate = 1",
237
+ "",
238
+ "[legacy_sect]",
239
+ "activate = 1",
240
+ "",
241
+ "[ssl_sect]",
242
+ "system_default = system_default_sect",
243
+ "",
244
+ "[system_default_sect]",
245
+ "Options = UnsafeLegacyRenegotiation",
246
+ ].join("\n"),
247
+ );
248
+
249
+ // dist/pm2.config.cjs (only when pm2 option is present)
250
+ if (info.pm2 != null) {
251
+ logger.debug("GEN pm2.config.cjs...");
252
+
253
+ const pm2Name = info.pm2.name ?? pkgJson.name.replace(/@/g, "").replace(/[/\\]/g, "-");
254
+ const ignoreWatch = JSON.stringify([
255
+ "node_modules",
256
+ "www",
257
+ ...(info.pm2.ignoreWatchPaths ?? []),
258
+ ]);
259
+ const envObj: Record<string, string> = {
260
+ NODE_ENV: "production",
261
+ TZ: "Asia/Seoul",
262
+ ...(info.env ?? {}),
263
+ };
264
+ const envStr = JSON.stringify(envObj, undefined, 4);
265
+
266
+ const interpreterLine =
267
+ info.packageManager === "volta"
268
+ ? ""
269
+ : ` interpreter: cp.execSync("mise which node").toString().trim(),\n`;
270
+
271
+ const pm2Config = [
272
+ ...(info.packageManager !== "volta" ? [`const cp = require("child_process");`, ``] : []),
273
+ `module.exports = {`,
274
+ ` name: ${JSON.stringify(pm2Name)},`,
275
+ ` script: "main.js",`,
276
+ ` watch: true,`,
277
+ ` watch_delay: 2000,`,
278
+ ` ignore_watch: ${ignoreWatch},`,
279
+ interpreterLine.trimEnd(),
280
+ ` interpreter_args: "--openssl-config=openssl.cnf",`,
281
+ ` env: ${envStr.replace(/\n/g, "\n ")},`,
282
+ ` arrayProcess: "concat",`,
283
+ ` useDelTargetNull: true,`,
284
+ `};`,
285
+ ]
286
+ .filter((line) => line !== "")
287
+ .join("\n");
288
+
289
+ fs.writeFileSync(path.join(distDir, "pm2.config.cjs"), pm2Config);
290
+ }
291
+ }
292
+
293
+ // Clean up resources before process termination (SIGTERM/SIGINT)
294
+ // Note: worker.terminate() does not call these handlers and terminates immediately.
295
+ // However, normal shutdown in watch mode is handled via SIGINT/SIGTERM from the main process, so this is fine.
296
+ registerCleanupHandlers(cleanup, logger);
297
+
298
+ //#endregion
299
+
300
+ //#region Worker
301
+
302
+ /**
303
+ * One-time build
304
+ */
305
+ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
306
+ const mainJsPath = path.join(info.pkgDir, "dist", "main.js");
307
+
308
+ try {
309
+ // Parse tsconfig
310
+ const parsedConfig = parseRootTsconfig(info.cwd);
311
+ const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
312
+
313
+ // Server target is node environment
314
+ const compilerOptions = await getCompilerOptionsForPackage(
315
+ parsedConfig.options,
316
+ "node",
317
+ info.pkgDir,
318
+ );
319
+
320
+ // Collect all externals (optional peer deps + native modules + manual)
321
+ const external = collectAllExternals(info.pkgDir, info.externals);
322
+
323
+ // One-time esbuild
324
+ const esbuildOptions = createServerEsbuildOptions({
325
+ pkgDir: info.pkgDir,
326
+ entryPoints,
327
+ compilerOptions,
328
+ env: info.env,
329
+ external,
330
+ });
331
+
332
+ const result = await esbuild.build(esbuildOptions);
333
+
334
+ // Generate .config.json
335
+ const confDistPath = path.join(info.pkgDir, "dist", ".config.json");
336
+ fs.writeFileSync(confDistPath, JSON.stringify(info.configs ?? {}, undefined, 2));
337
+
338
+ // Copy public/ to dist/ (production build: no public-dev)
339
+ await copyPublicFiles(info.pkgDir, false);
340
+
341
+ // Generate production files (package.json, mise.toml, openssl.cnf, pm2.config.cjs)
342
+ generateProductionFiles(info, external);
343
+
344
+ const errors = result.errors.map((e) => e.text);
345
+ const warnings = result.warnings.map((w) => w.text);
346
+ return {
347
+ success: result.errors.length === 0,
348
+ mainJsPath,
349
+ errors: errors.length > 0 ? errors : undefined,
350
+ warnings: warnings.length > 0 ? warnings : undefined,
351
+ };
352
+ } catch (err) {
353
+ return {
354
+ success: false,
355
+ mainJsPath,
356
+ errors: [errorMessage(err)],
357
+ };
358
+ }
359
+ }
360
+
361
+ const guardStartWatch = createOnceGuard("startWatch");
362
+
363
+ /**
364
+ * Create esbuild context and perform initial build
365
+ */
366
+ async function createAndBuildContext(
367
+ info: ServerWatchInfo,
368
+ isFirstBuild: boolean,
369
+ resolveFirstBuild?: () => void,
370
+ ): Promise<esbuild.BuildContext> {
371
+ const parsedConfig = parseRootTsconfig(info.cwd);
372
+ const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
373
+ const compilerOptions = await getCompilerOptionsForPackage(
374
+ parsedConfig.options,
375
+ "node",
376
+ info.pkgDir,
377
+ );
378
+
379
+ const mainJsPath = path.join(info.pkgDir, "dist", "main.js");
380
+ const external = collectAllExternals(info.pkgDir, info.externals);
381
+ const baseOptions = createServerEsbuildOptions({
382
+ pkgDir: info.pkgDir,
383
+ entryPoints,
384
+ compilerOptions,
385
+ env: info.env,
386
+ external,
387
+ });
388
+
389
+ let isBuildFirstTime = isFirstBuild;
390
+
391
+ const context = await esbuild.context({
392
+ ...baseOptions,
393
+ metafile: true,
394
+ write: false,
395
+ plugins: [
396
+ {
397
+ name: "watch-notify",
398
+ setup(pluginBuild) {
399
+ pluginBuild.onStart(() => {
400
+ sender.send("buildStart", {});
401
+ });
402
+
403
+ pluginBuild.onEnd(async (result) => {
404
+ // Save metafile
405
+ if (result.metafile != null) {
406
+ lastMetafile = result.metafile;
407
+ }
408
+
409
+ const errors = result.errors.map((e) => e.text);
410
+ const warnings = result.warnings.map((w) => w.text);
411
+ const success = result.errors.length === 0;
412
+
413
+ // Write output files and check for changes
414
+ let hasOutputChange = false;
415
+ if (success && result.outputFiles != null) {
416
+ hasOutputChange = await writeChangedOutputFiles(result.outputFiles);
417
+ }
418
+
419
+ if (isBuildFirstTime && success) {
420
+ const confDistPath = path.join(info.pkgDir, "dist", ".config.json");
421
+ fs.writeFileSync(confDistPath, JSON.stringify(info.configs ?? {}, undefined, 2));
422
+ }
423
+
424
+ // Only emit build event on first build, output change, or error
425
+ if (isBuildFirstTime || hasOutputChange || !success) {
426
+ sender.send("build", {
427
+ success,
428
+ mainJsPath,
429
+ errors: errors.length > 0 ? errors : undefined,
430
+ warnings: warnings.length > 0 ? warnings : undefined,
431
+ });
432
+ } else {
433
+ logger.debug("No output changes, skipping server restart");
434
+ }
435
+
436
+ if (isBuildFirstTime) {
437
+ isBuildFirstTime = false;
438
+ resolveFirstBuild?.();
439
+ }
440
+ });
441
+ },
442
+ },
443
+ ],
444
+ });
445
+
446
+ await context.rebuild();
447
+
448
+ return context;
449
+ }
450
+
451
+ /**
452
+ * Start watch
453
+ * @remarks This function should be called only once per Worker.
454
+ * @throws If watch has already been started
455
+ */
456
+ async function startWatch(info: ServerWatchInfo): Promise<void> {
457
+ guardStartWatch();
458
+
459
+ try {
460
+ // Promise to wait for first build completion
461
+ let resolveFirstBuild!: () => void;
462
+ const firstBuildPromise = new Promise<void>((resolve) => {
463
+ resolveFirstBuild = resolve;
464
+ });
465
+
466
+ // Create initial esbuild context and build
467
+ esbuildContext = await createAndBuildContext(info, true, resolveFirstBuild);
468
+
469
+ // Wait for first build completion
470
+ await firstBuildPromise;
471
+
472
+ // Watch public/ and public-dev/ (dev mode includes public-dev)
473
+ publicWatcher = await watchPublicFiles(info.pkgDir, true);
474
+
475
+ // Collect watch paths based on dependencies
476
+ const { workspaceDeps, replaceDeps } = collectDeps(info.pkgDir, info.cwd, info.replaceDeps);
477
+
478
+ const watchPaths: string[] = [];
479
+
480
+ // 1) Server package itself + workspace dependency packages source
481
+ const watchDirs = [
482
+ info.pkgDir,
483
+ ...workspaceDeps.map((d) => path.join(info.cwd, "packages", d)),
484
+ ];
485
+ for (const dir of watchDirs) {
486
+ watchPaths.push(path.join(dir, "src", "**", "*"));
487
+ watchPaths.push(path.join(dir, "*.{ts,js,css}"));
488
+ }
489
+
490
+ // 2) ReplaceDeps dependency packages dist (root + package node_modules)
491
+ for (const pkg of replaceDeps) {
492
+ watchPaths.push(path.join(info.cwd, "node_modules", ...pkg.split("/"), "dist", "**", "*.js"));
493
+ watchPaths.push(
494
+ path.join(info.pkgDir, "node_modules", ...pkg.split("/"), "dist", "**", "*.js"),
495
+ );
496
+ }
497
+
498
+ // Start FsWatcher
499
+ srcWatcher = await FsWatcher.watch(watchPaths);
500
+
501
+ // Handle file changes
502
+ srcWatcher.onChange({ delay: 300 }, async (changes) => {
503
+ try {
504
+ // If files are added/removed, recreate context (import graph may change)
505
+ const hasFileAddOrRemove = changes.some((c) => c.event === "add" || c.event === "unlink");
506
+
507
+ if (hasFileAddOrRemove) {
508
+ logger.debug("File add/remove detected, recreating context");
509
+
510
+ const oldContext = esbuildContext;
511
+ esbuildContext = await createAndBuildContext(info, false);
512
+
513
+ if (oldContext != null) {
514
+ await oldContext.dispose();
515
+ }
516
+ return;
517
+ }
518
+
519
+ // Only file changes: filter by metafile
520
+ if (esbuildContext == null) return;
521
+
522
+ // If no metafile (before first build), always rebuild
523
+ if (lastMetafile == null) {
524
+ await esbuildContext.rebuild();
525
+ return;
526
+ }
527
+
528
+ // Convert metafile.inputs keys to absolute paths (NormPath) for comparison
529
+ const metafileAbsPaths = new Set(
530
+ Object.keys(lastMetafile.inputs).map((key) => pathNorm(info.cwd, key)),
531
+ );
532
+
533
+ const hasRelevantChange = changes.some((c) => metafileAbsPaths.has(c.path));
534
+
535
+ if (hasRelevantChange) {
536
+ await esbuildContext.rebuild();
537
+ } else {
538
+ logger.debug("Changed files not included in build, skipping rebuild");
539
+ }
540
+ } catch (err) {
541
+ sender.send("error", {
542
+ message: errorMessage(err),
543
+ });
544
+ }
545
+ });
546
+ } catch (err) {
547
+ sender.send("error", {
548
+ message: errorMessage(err),
549
+ });
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Stop watch
555
+ * @remarks Cleans up esbuild context.
556
+ */
557
+ async function stopWatch(): Promise<void> {
558
+ await cleanup();
559
+ }
560
+
561
+ const sender = createWorker<
562
+ { build: typeof build; startWatch: typeof startWatch; stopWatch: typeof stopWatch },
563
+ ServerWorkerEvents
564
+ >({
565
+ build,
566
+ startWatch,
567
+ stopWatch,
568
+ });
569
+
570
+ export default sender;
571
+
572
+ //#endregion