@simplysm/sd-cli 13.0.76 → 13.0.77

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/README.md +341 -16
  2. package/dist/builders/DtsBuilder.js +2 -2
  3. package/dist/builders/DtsBuilder.js.map +1 -1
  4. package/dist/builders/LibraryBuilder.d.ts +3 -3
  5. package/dist/builders/LibraryBuilder.d.ts.map +1 -1
  6. package/dist/builders/LibraryBuilder.js +2 -2
  7. package/dist/builders/LibraryBuilder.js.map +1 -1
  8. package/dist/builders/types.d.ts +7 -1
  9. package/dist/builders/types.d.ts.map +1 -1
  10. package/dist/capacitor/capacitor.d.ts +5 -0
  11. package/dist/capacitor/capacitor.d.ts.map +1 -1
  12. package/dist/capacitor/capacitor.js +59 -59
  13. package/dist/capacitor/capacitor.js.map +1 -1
  14. package/dist/commands/check.js +4 -4
  15. package/dist/commands/check.js.map +1 -1
  16. package/dist/commands/device.js +3 -3
  17. package/dist/commands/device.js.map +1 -1
  18. package/dist/commands/lint.js +4 -4
  19. package/dist/commands/lint.js.map +1 -1
  20. package/dist/commands/publish.js +20 -20
  21. package/dist/commands/publish.js.map +1 -1
  22. package/dist/commands/replace-deps.js +1 -1
  23. package/dist/commands/replace-deps.js.map +1 -1
  24. package/dist/commands/typecheck.js +9 -9
  25. package/dist/commands/typecheck.js.map +1 -1
  26. package/dist/electron/electron.js +16 -16
  27. package/dist/electron/electron.js.map +1 -1
  28. package/dist/orchestrators/BuildOrchestrator.js +6 -6
  29. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  30. package/dist/orchestrators/DevOrchestrator.d.ts +7 -6
  31. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  32. package/dist/orchestrators/DevOrchestrator.js +157 -203
  33. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  34. package/dist/orchestrators/WatchOrchestrator.d.ts.map +1 -1
  35. package/dist/orchestrators/WatchOrchestrator.js +3 -4
  36. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  37. package/dist/sd-cli.js +1 -1
  38. package/dist/sd-cli.js.map +1 -1
  39. package/dist/sd-config.types.d.ts +9 -3
  40. package/dist/sd-config.types.d.ts.map +1 -1
  41. package/dist/utils/copy-public.d.ts.map +1 -1
  42. package/dist/utils/copy-public.js +23 -27
  43. package/dist/utils/copy-public.js.map +1 -1
  44. package/dist/utils/copy-src.d.ts.map +1 -1
  45. package/dist/utils/copy-src.js +7 -7
  46. package/dist/utils/copy-src.js.map +1 -1
  47. package/dist/utils/esbuild-config.d.ts.map +1 -1
  48. package/dist/utils/esbuild-config.js +36 -42
  49. package/dist/utils/esbuild-config.js.map +1 -1
  50. package/dist/utils/replace-deps.js +7 -7
  51. package/dist/utils/replace-deps.js.map +1 -1
  52. package/dist/utils/sd-config.js +2 -2
  53. package/dist/utils/sd-config.js.map +1 -1
  54. package/dist/utils/template.js +7 -7
  55. package/dist/utils/template.js.map +1 -1
  56. package/dist/utils/tsconfig.d.ts +1 -2
  57. package/dist/utils/tsconfig.d.ts.map +1 -1
  58. package/dist/utils/tsconfig.js +5 -8
  59. package/dist/utils/tsconfig.js.map +1 -1
  60. package/dist/utils/typecheck-serialization.js +2 -2
  61. package/dist/utils/typecheck-serialization.js.map +1 -1
  62. package/dist/utils/vite-config.d.ts +2 -0
  63. package/dist/utils/vite-config.d.ts.map +1 -1
  64. package/dist/utils/vite-config.js +36 -3
  65. package/dist/utils/vite-config.js.map +1 -1
  66. package/dist/utils/worker-events.d.ts +11 -1
  67. package/dist/utils/worker-events.d.ts.map +1 -1
  68. package/dist/utils/worker-events.js +3 -5
  69. package/dist/utils/worker-events.js.map +1 -1
  70. package/dist/utils/worker-utils.d.ts +2 -2
  71. package/dist/utils/worker-utils.d.ts.map +1 -1
  72. package/dist/utils/worker-utils.js +1 -1
  73. package/dist/utils/worker-utils.js.map +1 -1
  74. package/dist/workers/client.worker.d.ts +1 -1
  75. package/dist/workers/client.worker.js +3 -3
  76. package/dist/workers/client.worker.js.map +1 -1
  77. package/dist/workers/dts.worker.d.ts +1 -1
  78. package/dist/workers/dts.worker.d.ts.map +1 -1
  79. package/dist/workers/dts.worker.js +13 -28
  80. package/dist/workers/dts.worker.js.map +1 -1
  81. package/dist/workers/library.worker.d.ts +1 -1
  82. package/dist/workers/library.worker.js +4 -4
  83. package/dist/workers/library.worker.js.map +1 -1
  84. package/dist/workers/lint.worker.d.ts +1 -1
  85. package/dist/workers/server-runtime.worker.d.ts +1 -1
  86. package/dist/workers/server-runtime.worker.js +4 -4
  87. package/dist/workers/server-runtime.worker.js.map +1 -1
  88. package/dist/workers/server.worker.d.ts +1 -1
  89. package/dist/workers/server.worker.js +6 -6
  90. package/dist/workers/server.worker.js.map +1 -1
  91. package/package.json +4 -4
  92. package/src/builders/DtsBuilder.ts +2 -2
  93. package/src/builders/LibraryBuilder.ts +7 -10
  94. package/src/builders/types.ts +6 -1
  95. package/src/capacitor/capacitor.ts +61 -60
  96. package/src/commands/check.ts +4 -4
  97. package/src/commands/device.ts +3 -3
  98. package/src/commands/lint.ts +4 -4
  99. package/src/commands/publish.ts +20 -20
  100. package/src/commands/replace-deps.ts +1 -1
  101. package/src/commands/typecheck.ts +9 -9
  102. package/src/electron/electron.ts +16 -16
  103. package/src/orchestrators/BuildOrchestrator.ts +6 -6
  104. package/src/orchestrators/DevOrchestrator.ts +210 -256
  105. package/src/orchestrators/WatchOrchestrator.ts +8 -10
  106. package/src/sd-cli.ts +1 -1
  107. package/src/sd-config.types.ts +10 -3
  108. package/src/utils/copy-public.ts +22 -26
  109. package/src/utils/copy-src.ts +7 -7
  110. package/src/utils/esbuild-config.ts +51 -63
  111. package/src/utils/replace-deps.ts +7 -7
  112. package/src/utils/sd-config.ts +2 -2
  113. package/src/utils/template.ts +7 -7
  114. package/src/utils/tsconfig.ts +6 -10
  115. package/src/utils/typecheck-serialization.ts +2 -2
  116. package/src/utils/vite-config.ts +376 -341
  117. package/src/utils/worker-events.ts +13 -10
  118. package/src/utils/worker-utils.ts +45 -45
  119. package/src/workers/client.worker.ts +3 -3
  120. package/src/workers/dts.worker.ts +451 -467
  121. package/src/workers/library.worker.ts +4 -4
  122. package/src/workers/server-runtime.worker.ts +4 -4
  123. package/src/workers/server.worker.ts +572 -572
  124. package/templates/init/package.json.hbs +2 -2
  125. package/templates/init/packages/client-admin/package.json.hbs +5 -5
  126. package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +2 -2
  127. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +1 -1
  128. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +1 -1
  129. package/templates/init/packages/db-main/package.json.hbs +2 -2
  130. package/templates/init/packages/server/package.json.hbs +4 -4
  131. package/templates/init/tests/e2e/package.json.hbs +1 -1
  132. package/tests/get-compiler-options-for-package.spec.ts +13 -27
  133. package/tests/get-types-from-package-json.spec.ts +15 -11
  134. package/tests/load-ignore-patterns.spec.ts +15 -11
  135. package/tests/load-sd-config.spec.ts +16 -14
  136. package/tests/publish-config-narrowing.spec.ts +20 -0
  137. package/tests/run-lint.spec.ts +38 -34
  138. package/tests/run-typecheck.spec.ts +194 -135
  139. package/tests/sd-public-dev-plugin-mime.spec.ts +19 -0
  140. package/dist/builders/index.d.ts +0 -5
  141. package/dist/builders/index.d.ts.map +0 -1
  142. package/dist/builders/index.js +0 -5
  143. package/dist/builders/index.js.map +0 -6
  144. package/dist/infra/index.d.ts +0 -4
  145. package/dist/infra/index.d.ts.map +0 -1
  146. package/dist/infra/index.js +0 -4
  147. package/dist/infra/index.js.map +0 -6
  148. package/dist/orchestrators/index.d.ts +0 -4
  149. package/dist/orchestrators/index.d.ts.map +0 -1
  150. package/dist/orchestrators/index.js +0 -4
  151. package/dist/orchestrators/index.js.map +0 -6
  152. package/src/builders/index.ts +0 -4
  153. package/src/infra/index.ts +0 -3
  154. package/src/orchestrators/index.ts +0 -3
@@ -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 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
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { execaSync } from "execa";
4
+ import esbuild from "esbuild";
5
+ import { createWorker, FsWatcher, pathx } from "@simplysm/core-node";
6
+ import { err as errNs } 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: [errNs.message(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) => pathx.norm(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: errNs.message(err),
543
+ });
544
+ }
545
+ });
546
+ } catch (err) {
547
+ sender.send("error", {
548
+ message: errNs.message(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