@simplysm/sd-cli 14.0.5 → 14.0.7

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 (72) hide show
  1. package/dist/commands/check.d.ts.map +1 -1
  2. package/dist/commands/check.js +10 -10
  3. package/dist/commands/check.js.map +1 -1
  4. package/dist/commands/lint.d.ts.map +1 -1
  5. package/dist/commands/lint.js +1 -4
  6. package/dist/commands/lint.js.map +1 -1
  7. package/dist/commands/replace-deps.js +1 -1
  8. package/dist/commands/replace-deps.js.map +1 -1
  9. package/dist/electron/electron.d.ts.map +1 -1
  10. package/dist/electron/electron.js +5 -6
  11. package/dist/electron/electron.js.map +1 -1
  12. package/dist/engines/BaseEngine.d.ts.map +1 -1
  13. package/dist/engines/BaseEngine.js +1 -0
  14. package/dist/engines/BaseEngine.js.map +1 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  20. package/dist/orchestrators/BuildOrchestrator.js +52 -45
  21. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  22. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  23. package/dist/orchestrators/DevWatchOrchestrator.js +32 -26
  24. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  25. package/dist/sd-cli.js +1 -1
  26. package/dist/sd-cli.js.map +1 -1
  27. package/dist/utils/esbuild-config.d.ts +7 -2
  28. package/dist/utils/esbuild-config.d.ts.map +1 -1
  29. package/dist/utils/esbuild-config.js +15 -12
  30. package/dist/utils/esbuild-config.js.map +1 -1
  31. package/dist/utils/output-utils.js +1 -1
  32. package/dist/utils/output-utils.js.map +1 -1
  33. package/dist/utils/package-utils.js +3 -3
  34. package/dist/utils/package-utils.js.map +1 -1
  35. package/dist/utils/rebuild-manager.js +3 -3
  36. package/dist/utils/rebuild-manager.js.map +1 -1
  37. package/dist/utils/replace-deps.js +11 -11
  38. package/dist/utils/replace-deps.js.map +1 -1
  39. package/dist/utils/vite-config.d.ts.map +1 -1
  40. package/dist/utils/vite-config.js +1 -2
  41. package/dist/utils/vite-config.js.map +1 -1
  42. package/dist/utils/worker-utils.js +1 -1
  43. package/dist/utils/worker-utils.js.map +1 -1
  44. package/dist/workers/server-build.worker.js +2 -2
  45. package/dist/workers/server-build.worker.js.map +1 -1
  46. package/dist/workers/server-runtime.worker.js +13 -13
  47. package/dist/workers/server-runtime.worker.js.map +1 -1
  48. package/package.json +4 -5
  49. package/src/commands/check.ts +12 -11
  50. package/src/commands/lint.ts +1 -3
  51. package/src/commands/replace-deps.ts +1 -1
  52. package/src/electron/electron.ts +7 -7
  53. package/src/engines/BaseEngine.ts +1 -0
  54. package/src/index.ts +3 -0
  55. package/src/orchestrators/BuildOrchestrator.ts +52 -45
  56. package/src/orchestrators/DevWatchOrchestrator.ts +33 -26
  57. package/src/sd-cli.ts +1 -1
  58. package/src/utils/esbuild-config.ts +16 -12
  59. package/src/utils/output-utils.ts +1 -1
  60. package/src/utils/package-utils.ts +3 -3
  61. package/src/utils/rebuild-manager.ts +3 -3
  62. package/src/utils/replace-deps.ts +11 -11
  63. package/src/utils/vite-config.ts +1 -2
  64. package/src/utils/worker-utils.ts +1 -1
  65. package/src/workers/server-build.worker.ts +2 -2
  66. package/src/workers/server-runtime.worker.ts +13 -13
  67. package/tests/commands/check.spec.ts +11 -26
  68. package/tests/electron/electron.spec.ts +42 -35
  69. package/tests/orchestrators/build-orchestrator.spec.ts +4 -9
  70. package/tests/orchestrators/dev-watch-orchestrator.spec.ts +3 -5
  71. package/tests/utils/esbuild-config.spec.ts +38 -8
  72. package/tests/utils/vite-config.spec.ts +13 -0
@@ -199,7 +199,7 @@ async function resolveAllReplaceDepEntries(
199
199
  try {
200
200
  await fs.promises.access(resolvedSourcePath);
201
201
  } catch {
202
- logger.warn(`Source path does not exist, skipping: ${resolvedSourcePath}`);
202
+ logger.warn(`소스 경로가 존재하지 않아 건너뜀: ${resolvedSourcePath}`);
203
203
  continue;
204
204
  }
205
205
 
@@ -244,7 +244,7 @@ export async function setupReplaceDeps(
244
244
  const logger = consola.withTag("sd:cli:replace-deps");
245
245
  let setupCount = 0;
246
246
 
247
- logger.start("Setting up replace-deps");
247
+ logger.start("replace-deps 설정 중...");
248
248
 
249
249
  const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
250
250
 
@@ -255,11 +255,11 @@ export async function setupReplaceDeps(
255
255
 
256
256
  setupCount += 1;
257
257
  } catch (err) {
258
- logger.error(`Copy replace failed (${targetName}): ${err instanceof Error ? err.message : err}`);
258
+ logger.error(`[${targetName}] 복사 실패: ${err instanceof Error ? err.message : err}`);
259
259
  }
260
260
  }
261
261
 
262
- logger.success(`Replaced ${setupCount} dependencies`);
262
+ logger.success(`replace-deps 설정 완료 (${setupCount} 의존성 교체)`);
263
263
 
264
264
  // Run postinstall scripts from replaced packages
265
265
  for (const { targetName, resolvedSourcePath, actualTargetPath } of entries) {
@@ -269,13 +269,13 @@ export async function setupReplaceDeps(
269
269
  const postinstall = pkgJson.scripts?.postinstall as string | undefined;
270
270
  if (postinstall == null) continue;
271
271
 
272
- logger.warn(`Running postinstall script for ${targetName}: ${postinstall}`);
273
- logger.start(`Running postinstall for ${targetName}`);
272
+ logger.warn(`[${targetName}] postinstall 스크립트 실행: ${postinstall}`);
273
+ logger.start(`[${targetName}] postinstall 실행 중...`);
274
274
  await promisify(exec)(postinstall, { cwd: actualTargetPath });
275
- logger.success(`postinstall done: ${targetName}`);
275
+ logger.success(`[${targetName}] postinstall 실행 완료`);
276
276
  } catch (err) {
277
277
  logger.error(
278
- `postinstall failed (${targetName}): ${err instanceof Error ? err.message : err}`,
278
+ `[${targetName}] postinstall 실패: ${err instanceof Error ? err.message : err}`,
279
279
  );
280
280
  }
281
281
  }
@@ -305,7 +305,7 @@ export async function watchReplaceDeps(
305
305
  const watchers: FsWatcher[] = [];
306
306
  const watchedSources = new Set<string>();
307
307
 
308
- logger.start(`Watching ${entries.length} replace-deps target(s)`);
308
+ logger.start(`replace-deps 워치 시작 중... (${entries.length} 대상)`);
309
309
 
310
310
  for (const entry of entries) {
311
311
  if (watchedSources.has(entry.resolvedSourcePath)) continue;
@@ -359,7 +359,7 @@ export async function watchReplaceDeps(
359
359
  }
360
360
  } catch (err) {
361
361
  logger.error(
362
- `Copy failed (${e.targetName}/${relativePath}): ${err instanceof Error ? err.message : err}`,
362
+ `[${e.targetName}] 복사 실패 (${relativePath}): ${err instanceof Error ? err.message : err}`,
363
363
  );
364
364
  }
365
365
  }
@@ -369,7 +369,7 @@ export async function watchReplaceDeps(
369
369
  watchers.push(watcher);
370
370
  }
371
371
 
372
- logger.success(`Replace-deps watch ready`);
372
+ logger.success("replace-deps 워치 준비 완료");
373
373
 
374
374
  return {
375
375
  entries,
@@ -82,10 +82,9 @@ export async function createClientViteConfig(
82
82
  : [options.browserslist]
83
83
  : undefined;
84
84
 
85
- // define: 환경변수 주입
85
+ // define: 환경변수 주입 (import.meta.env.KEY → Vite가 bare import.meta.env 객체를 자동 구성)
86
86
  const define: Record<string, string> = {};
87
87
  if (options.env != null) {
88
- define["process.env"] = JSON.stringify(options.env);
89
88
  for (const [key, value] of Object.entries(options.env)) {
90
89
  define[`import.meta.env.${key}`] = JSON.stringify(value);
91
90
  }
@@ -33,7 +33,7 @@ export function registerCleanupHandlers(
33
33
  process.exit(0);
34
34
  })
35
35
  .catch((err) => {
36
- logger.error("cleanup failed", err);
36
+ logger.error("정리 작업 실패", err);
37
37
  process.exit(1);
38
38
  });
39
39
  };
@@ -142,7 +142,7 @@ async function cleanup(): Promise<void> {
142
142
  * Uses single-pass dependency tree traversal via collectAllDependencyExternals.
143
143
  */
144
144
  function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
145
- logger.debug("[externals] Scanning dependency tree (single pass)...");
145
+ logger.debug("의존성 트리 스캔 중...");
146
146
  const { optionalPeerDeps, nativeModules } = collectAllDependencyExternals(pkgDir);
147
147
 
148
148
  const manual = manualExternals ?? [];
@@ -638,7 +638,7 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
638
638
  const result = await rebuildAll();
639
639
  sender.send("build", result);
640
640
  } else {
641
- logger.debug("Changed files not included in build, skipping rebuild");
641
+ logger.debug("변경된 파일이 빌드에 포함되지 않아 리빌드 건너뜀");
642
642
  }
643
643
  } catch (err) {
644
644
  sender.send("error", { message: errNs.message(err) });
@@ -63,7 +63,7 @@ async function cleanup(): Promise<void> {
63
63
  // Catch runtime errors that occur after server listen() and send them as a custom "error" event
64
64
  // (Without this handler, the worker will crash but dev.ts's buildResolver won't be called, causing listr to hang)
65
65
  process.on("uncaughtException", (err) => {
66
- logger.error("Unhandled server runtime error", err);
66
+ logger.error("서버 런타임 미처리 에러", err);
67
67
  sender.send("error", {
68
68
  message: errNs.message(err),
69
69
  });
@@ -72,7 +72,7 @@ process.on("uncaughtException", (err) => {
72
72
  });
73
73
 
74
74
  process.on("unhandledRejection", (reason) => {
75
- logger.error("Unhandled server runtime promise rejection", reason);
75
+ logger.error("서버 런타임 미처리 Promise 거부", reason);
76
76
  sender.send("error", {
77
77
  message: errNs.message(reason),
78
78
  });
@@ -127,10 +127,10 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
127
127
  }
128
128
 
129
129
  // Import main.js (must export a server instance)
130
- logger.debug("[start] Importing main.js...");
130
+ logger.debug("main.js 임포트 중...");
131
131
  let stepStart = performance.now();
132
132
  const module = await import(pathToFileURL(info.mainJsPath).href);
133
- logger.debug(`[start] main.js imported (${Math.round(performance.now() - stepStart)}ms)`);
133
+ logger.debug(`main.js 임포트 완료 (${Math.round(performance.now() - stepStart)}ms)`);
134
134
  const server = module.server;
135
135
 
136
136
  if (server == null) {
@@ -143,7 +143,7 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
143
143
  // Register client proxies (before listen)
144
144
  if (info.clientPorts != null && Object.keys(info.clientPorts).length > 0) {
145
145
  for (const [name, port] of Object.entries(info.clientPorts)) {
146
- logger.debug(`[start] Registering proxy: /${name} -> http://127.0.0.1:${String(port)}`);
146
+ logger.debug(`프록시 등록: /${name} http://127.0.0.1:${String(port)}`);
147
147
  await server.fastify.register(proxy, {
148
148
  prefix: `/${name}`,
149
149
  upstream: `http://127.0.0.1:${port}`,
@@ -154,31 +154,31 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
154
154
  }
155
155
 
156
156
  // Find available port (auto-increment on port conflict)
157
- logger.debug("[start] Finding available port...");
157
+ logger.debug("사용 가능한 포트 탐색 중...");
158
158
  stepStart = performance.now();
159
159
  const originalPort = server.options.port;
160
160
  const availablePort = await findAvailablePort(originalPort);
161
161
  if (availablePort !== originalPort) {
162
- logger.info(`Port ${originalPort} in use, changing to ${availablePort}`);
162
+ logger.info(`포트 ${originalPort} 사용 중, ${availablePort}로 변경`);
163
163
  server.options.port = availablePort;
164
164
  }
165
165
  logger.debug(
166
- `[start] Port ${String(availablePort)} available (${Math.round(performance.now() - stepStart)}ms)`,
166
+ `포트 ${String(availablePort)} 사용 가능 (${Math.round(performance.now() - stepStart)}ms)`,
167
167
  );
168
168
 
169
- // Start server
170
- logger.debug("[start] Starting server listen...");
169
+ // 서버 시작
170
+ logger.debug("서버 리슨 시작...");
171
171
  stepStart = performance.now();
172
172
  await server.listen();
173
- logger.debug(`[start] Server listening (${Math.round(performance.now() - stepStart)}ms)`);
173
+ logger.debug(`서버 리슨 완료 (${Math.round(performance.now() - stepStart)}ms)`);
174
174
 
175
175
  logger.debug(
176
- `[start] Total runtime startup: ${Math.round(performance.now() - startTime)}ms`,
176
+ `런타임 시작 시간: ${Math.round(performance.now() - startTime)}ms`,
177
177
  );
178
178
 
179
179
  sender.send("serverReady", { port: server.options.port });
180
180
  } catch (err) {
181
- logger.error("Server Runtime startup failed", err);
181
+ logger.error("서버 런타임 시작 실패", err);
182
182
  sender.send("error", {
183
183
  message: errNs.message(err),
184
184
  });
@@ -117,21 +117,20 @@ describe("runCheck", () => {
117
117
  writeSpy.mockRestore();
118
118
  });
119
119
 
120
- it("runs typecheck+lint and test, outputs ALL PASSED", async () => {
120
+ it("runs typecheck+lint and test", async () => {
121
121
  await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
122
122
 
123
123
  expect(mocks.executeTypecheck).toHaveBeenCalled();
124
124
  expect(mocks.execa).toHaveBeenCalled();
125
- expect(stdoutOutput).toContain("ALL PASSED");
126
125
  });
127
126
 
128
- it("outputs results in TYPECHECK → LINT → TEST → SUMMARY order", async () => {
127
+ it("outputs results in TYPECHECK → LINT → TEST → 요약 order", async () => {
129
128
  await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
130
129
 
131
130
  const tcIdx = stdoutOutput.indexOf("TYPECHECK");
132
131
  const lintIdx = stdoutOutput.indexOf("LINT");
133
132
  const testIdx = stdoutOutput.indexOf("TEST");
134
- const summaryIdx = stdoutOutput.indexOf("SUMMARY");
133
+ const summaryIdx = stdoutOutput.indexOf("요약");
135
134
 
136
135
  expect(tcIdx).toBeLessThan(lintIdx);
137
136
  expect(lintIdx).toBeLessThan(testIdx);
@@ -146,7 +145,7 @@ describe("runCheck", () => {
146
145
  expect(mocks.execa).toHaveBeenCalled();
147
146
  });
148
147
 
149
- it("outputs FAILED with failing check names when some fail", async () => {
148
+ it("sets exitCode 1 when typecheck fails", async () => {
150
149
  mocks.executeTypecheck.mockResolvedValue({
151
150
  success: false, errorCount: 2, warningCount: 0, formattedOutput: "type errors",
152
151
  lint: { success: true, errorCount: 0, warningCount: 0, formattedOutput: "" },
@@ -155,8 +154,6 @@ describe("runCheck", () => {
155
154
 
156
155
  await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
157
156
 
158
- expect(stdoutOutput).toContain("FAILED");
159
- expect(stdoutOutput).toContain("typecheck");
160
157
  expect(process.exitCode).toBe(1);
161
158
  });
162
159
 
@@ -170,14 +167,13 @@ describe("runCheck", () => {
170
167
  expect(process.exitCode).toBe(1);
171
168
  });
172
169
 
173
- it("parses test failure count from vitest output", async () => {
170
+ it("sets exitCode 1 when test fails", async () => {
174
171
  mocks.execa.mockResolvedValue({
175
172
  stdout: "3 tests failed", stderr: "", exitCode: 1,
176
173
  });
177
174
 
178
175
  await runCheck({ targets: [], types: ["test"], fix: false });
179
176
 
180
- expect(stdoutOutput).toContain("3 failed");
181
177
  expect(process.exitCode).toBe(1);
182
178
  });
183
179
 
@@ -271,8 +267,8 @@ describe("runCheck", () => {
271
267
  expect(mocks.runLintInWorker).not.toHaveBeenCalled();
272
268
  });
273
269
 
274
- // Scenario: check 결과 출력에 LINT 섹션이 포함된다
275
- it("outputs separate TYPECHECK and LINT sections from engine results", async () => {
270
+ // Scenario: lint 실패 exitCode 설정
271
+ it("sets exitCode 1 when engine lint fails", async () => {
276
272
  mocks.executeTypecheck.mockResolvedValue({
277
273
  success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
278
274
  lint: { success: false, errorCount: 3, warningCount: 1, formattedOutput: "some lint output" },
@@ -281,10 +277,7 @@ describe("runCheck", () => {
281
277
 
282
278
  await runCheck({ targets: [], types: ["typecheck", "lint"], fix: false });
283
279
 
284
- expect(stdoutOutput).toContain("TYPECHECK");
285
- expect(stdoutOutput).toContain("LINT");
286
- expect(stdoutOutput).toContain("3 errors");
287
- expect(stdoutOutput).toContain("1 warnings");
280
+ expect(process.exitCode).toBe(1);
288
281
  });
289
282
 
290
283
  // Scenario: check에서 scripts 패키지의 lint가 별도 실행된다
@@ -391,43 +384,35 @@ describe("runCheck", () => {
391
384
  });
392
385
 
393
386
  // Scenario: lint 에러 없음
394
- it("outputs ALL PASSED when lint has no errors", async () => {
387
+ it("does not set exitCode when lint passes", async () => {
395
388
  mocks.executeLint.mockResolvedValue({
396
389
  success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
397
390
  });
398
391
 
399
392
  await runCheck({ targets: [], types: ["lint"], fix: false });
400
393
 
401
- expect(stdoutOutput).toContain("LINT");
402
- expect(stdoutOutput).toContain("✔ 0 errors, 0 warnings");
403
- expect(stdoutOutput).toContain("✔ ALL PASSED");
404
394
  expect(process.exitCode).toBeUndefined();
405
395
  });
406
396
 
407
397
  // Scenario: lint 에러 발생
408
- it("outputs FAILED when lint has errors", async () => {
398
+ it("sets exitCode 1 when lint has errors", async () => {
409
399
  mocks.executeLint.mockResolvedValue({
410
400
  success: false, errorCount: 3, warningCount: 2, formattedOutput: "lint errors",
411
401
  });
412
402
 
413
403
  await runCheck({ targets: [], types: ["lint"], fix: false });
414
404
 
415
- expect(stdoutOutput).toContain("LINT");
416
- expect(stdoutOutput).toContain("✖ 3 errors, 2 warnings");
417
- expect(stdoutOutput).toContain("✖ 1/1 FAILED (lint)");
418
405
  expect(process.exitCode).toBe(1);
419
406
  });
420
407
 
421
408
  // Scenario: lint 대상 파일 없음
422
- it("outputs ALL PASSED when no files to lint", async () => {
409
+ it("does not set exitCode when no files to lint", async () => {
423
410
  mocks.executeLint.mockResolvedValue({
424
411
  success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
425
412
  });
426
413
 
427
414
  await runCheck({ targets: [], types: ["lint"], fix: false });
428
415
 
429
- expect(stdoutOutput).toContain("✔ 0 errors, 0 warnings");
430
- expect(stdoutOutput).toContain("✔ ALL PASSED");
431
416
  expect(process.exitCode).toBeUndefined();
432
417
  });
433
418
 
@@ -278,6 +278,25 @@ describe("Electron", () => {
278
278
 
279
279
  await expect(electron.build("/fake/out")).rejects.toThrow("electron-main.ts");
280
280
  });
281
+
282
+ it("config.env를 esbuild banner로 주입한다 (ELECTRON_DEV_URL 미포함)", async () => {
283
+ const { Electron } = await import("../../src/electron/electron.js");
284
+
285
+ const electron = await Electron.create(PKG_PATH, {
286
+ appId: "com.test.app",
287
+ env: { API_URL: "https://api.example.com" },
288
+ });
289
+ await electron.build("/fake/out");
290
+
291
+ const callArgs = mockEsbuildBuild.mock.calls[0][0];
292
+ const banner = callArgs.banner?.js as string;
293
+ expect(banner).toContain("process.env");
294
+ expect(banner).toContain("??=");
295
+ expect(banner).toContain("API_URL");
296
+ expect(banner).toContain("https://api.example.com");
297
+ expect(banner).not.toContain("ELECTRON_DEV_URL");
298
+ expect(callArgs.define).toBeUndefined();
299
+ });
281
300
  });
282
301
 
283
302
  //#endregion
@@ -440,7 +459,7 @@ describe("Electron", () => {
440
459
  return { electronKill, resolveElectron: () => resolveElectron() };
441
460
  }
442
461
 
443
- it("creates esbuild context and spawns Electron on build success", async () => {
462
+ it("creates esbuild context with banner for env and spawns Electron", async () => {
444
463
  const { resolveElectron } = setupExecaForRun();
445
464
 
446
465
  const { Electron } = await import("../../src/electron/electron.js");
@@ -455,28 +474,20 @@ describe("Electron", () => {
455
474
  resolveElectron();
456
475
  await runPromise;
457
476
 
458
- // esbuild.context was called
459
- expect(mockEsbuildContext).toHaveBeenCalledWith(
460
- expect.objectContaining({
461
- platform: "node",
462
- target: "node20",
463
- format: "cjs",
464
- bundle: true,
465
- external: expect.arrayContaining(["electron"]),
466
- }),
467
- );
468
-
469
- // Electron was spawned via execa
470
- const electronCall = mockExeca.mock.calls.find(
471
- (c: any[]) => typeof c[0] === "string" && c[0].includes("electron"),
472
- );
473
- expect(electronCall).toBeDefined();
474
- expect(electronCall![2].env).toEqual(
475
- expect.objectContaining({
476
- NODE_ENV: "development",
477
- ELECTRON_DEV_URL: "http://localhost:4200",
478
- }),
479
- );
477
+ // esbuild.context was called with banner containing ELECTRON_DEV_URL
478
+ const callArgs = mockEsbuildContext.mock.calls[0][0];
479
+ const banner = callArgs.banner?.js as string;
480
+ expect(banner).toContain("process.env");
481
+ expect(banner).toContain("??=");
482
+ expect(banner).toContain("ELECTRON_DEV_URL");
483
+ expect(banner).toContain("http://localhost:4200");
484
+ expect(callArgs.define).toBeUndefined();
485
+
486
+ expect(callArgs.platform).toBe("node");
487
+ expect(callArgs.target).toBe("node20");
488
+ expect(callArgs.format).toBe("cjs");
489
+ expect(callArgs.bundle).toBe(true);
490
+ expect(callArgs.external).toContain("electron");
480
491
  }, 10_000);
481
492
 
482
493
  it("throws when electron-main.ts entry point is missing", async () => {
@@ -513,7 +524,7 @@ describe("Electron", () => {
513
524
  });
514
525
 
515
526
  describe("단위: run() 플러그인 동작", () => {
516
- it("passes custom env and ELECTRON_DEV_URL to spawned Electron process", async () => {
527
+ it("passes custom env and ELECTRON_DEV_URL via esbuild banner", async () => {
517
528
  let resolveElectron: () => void = () => {};
518
529
  mockExeca.mockImplementation((cmd: string) => {
519
530
  if (typeof cmd === "string" && cmd.includes("electron")) {
@@ -537,17 +548,13 @@ describe("Electron", () => {
537
548
  resolveElectron();
538
549
  await runPromise;
539
550
 
540
- const execaCall = mockExeca.mock.calls.find(
541
- (c: any[]) => typeof c[0] === "string" && c[0].includes("electron"),
542
- );
543
- expect(execaCall).toBeDefined();
544
- expect(execaCall![2].env).toEqual(
545
- expect.objectContaining({
546
- NODE_ENV: "development",
547
- ELECTRON_DEV_URL: "http://localhost:5555",
548
- CUSTOM_VAR: "test-value",
549
- }),
550
- );
551
+ const callArgs = mockEsbuildContext.mock.calls[0][0];
552
+ const banner = callArgs.banner?.js as string;
553
+ expect(banner).toContain("ELECTRON_DEV_URL");
554
+ expect(banner).toContain("http://localhost:5555");
555
+ expect(banner).toContain("CUSTOM_VAR");
556
+ expect(banner).toContain("test-value");
557
+ expect(callArgs.define).toBeUndefined();
551
558
  }, 10_000);
552
559
 
553
560
  it("calls initialize() before starting esbuild context", async () => {
@@ -221,7 +221,7 @@ describe("BuildOrchestrator.initialize", () => {
221
221
  );
222
222
  });
223
223
 
224
- it("outputs message when no packages to build", async () => {
224
+ it("returns false when only scripts packages exist", async () => {
225
225
  setupDefaults({
226
226
  packages: {
227
227
  "sd-claude": { target: "scripts" } as any,
@@ -230,10 +230,9 @@ describe("BuildOrchestrator.initialize", () => {
230
230
 
231
231
  const orchestrator = new BuildOrchestrator({ targets: [], options: [] });
232
232
  await orchestrator.initialize();
233
+ const hasError = await orchestrator.start();
233
234
 
234
- expect(process.stdout.write).toHaveBeenCalledWith(
235
- expect.stringContaining("No packages to build"),
236
- );
235
+ expect(hasError).toBe(false);
237
236
  });
238
237
  });
239
238
 
@@ -403,10 +402,9 @@ describe("BuildOrchestrator.start", () => {
403
402
  const hasError = await orchestrator.start();
404
403
 
405
404
  expect(hasError).toBe(false);
406
- expect(mockLogger.info).toHaveBeenCalledWith("Build completed");
407
405
  });
408
406
 
409
- it("returns true and logs error when any build fails", async () => {
407
+ it("returns true when any build fails", async () => {
410
408
  setupDefaults({
411
409
  packages: {
412
410
  "core-common": { target: "neutral", publish: { type: "npm" } },
@@ -429,7 +427,6 @@ describe("BuildOrchestrator.start", () => {
429
427
  const hasError = await orchestrator.start();
430
428
 
431
429
  expect(hasError).toBe(true);
432
- expect(mockLogger.error).toHaveBeenCalledWith("Build failed");
433
430
  });
434
431
 
435
432
  it("returns false when no packages to build", async () => {
@@ -658,7 +655,6 @@ describe("BuildOrchestrator client build", () => {
658
655
  const hasError = await orchestrator.start();
659
656
 
660
657
  expect(hasError).toBe(true);
661
- expect(mockLogger.error).toHaveBeenCalledWith("Build failed");
662
658
  });
663
659
 
664
660
  // Acceptance: Scenario "BuildOrchestrator가 env를 ViteEngine에 전달" + "프로덕션 빌드의 baseEnv"
@@ -1049,7 +1045,6 @@ describe("BuildOrchestrator lint integration", () => {
1049
1045
  const hasError = await orchestrator.start();
1050
1046
 
1051
1047
  expect(hasError).toBe(true);
1052
- expect(mockLogger.error).toHaveBeenCalledWith("Build failed");
1053
1048
  });
1054
1049
 
1055
1050
  // Scenario: build에서 scripts 패키지는 제외된다
@@ -300,7 +300,6 @@ describe("DevWatchOrchestrator", () => {
300
300
  const orchestrator = new DevWatchOrchestrator({ mode: "watch", targets: [], options: [] });
301
301
  await orchestrator.initialize();
302
302
 
303
- expect(process.stdout.write).toHaveBeenCalledWith(expect.stringContaining("No packages"));
304
303
  expect(createBuildEngine).not.toHaveBeenCalled();
305
304
  });
306
305
 
@@ -872,7 +871,6 @@ describe("DevWatchOrchestrator", () => {
872
871
 
873
872
  expect(mockBuildEngines[0].stop).toHaveBeenCalledOnce();
874
873
  expect(mockRuntimeProxies[0].terminate).toHaveBeenCalled();
875
- expect(process.stdout.write).toHaveBeenCalledWith(expect.stringContaining("Shutting down"));
876
874
  });
877
875
 
878
876
  // --- Acceptance: dev에서 replaceDeps 감시 ---
@@ -890,8 +888,8 @@ describe("DevWatchOrchestrator", () => {
890
888
  expect(watchReplaceDeps).toHaveBeenCalledWith("/test-root", replaceDeps);
891
889
  });
892
890
 
893
- // Unit: outputs warning when no server packages
894
- it("outputs warning when no server packages in dev mode", async () => {
891
+ // Unit: no server packages in dev mode — does not create runtime proxies
892
+ it("does not create runtime proxies when no server packages in dev mode", async () => {
895
893
  setupDefaults(createConfig({
896
894
  packages: { "core-common": { target: "node" } },
897
895
  }));
@@ -899,7 +897,7 @@ describe("DevWatchOrchestrator", () => {
899
897
  const orchestrator = new DevWatchOrchestrator({ mode: "dev", targets: [], options: [] });
900
898
  await orchestrator.initialize();
901
899
 
902
- expect(process.stdout.write).toHaveBeenCalledWith(expect.stringContaining("No"));
900
+ expect(createBuildEngine).not.toHaveBeenCalled();
903
901
  });
904
902
 
905
903
  // Unit: multiple server packages
@@ -17,7 +17,7 @@ vi.mock("consola", () => ({
17
17
  },
18
18
  }));
19
19
 
20
- const { createServerEsbuildOptions, writeChangedOutputFiles } =
20
+ const { createServerEsbuildOptions, createEnvBanner, writeChangedOutputFiles } =
21
21
  await import("../../src/utils/esbuild-config");
22
22
 
23
23
  const { default: mockFs } = await import("fs/promises");
@@ -53,20 +53,25 @@ describe("createServerEsbuildOptions", () => {
53
53
  expect((result.banner as Record<string, string>)["js"]).toContain("import.meta.url");
54
54
  });
55
55
 
56
- it("substitutes env vars via define (process.env.KEY constant)", () => {
56
+ it("injects env vars via banner (process.env merge) instead of define", () => {
57
57
  const result = createServerEsbuildOptions({
58
58
  ...baseOptions,
59
59
  env: { API_URL: "https://api.example.com", NODE_ENV: "production" },
60
60
  });
61
- expect(result.define).toEqual({
62
- "process.env.API_URL": '"https://api.example.com"',
63
- "process.env.NODE_ENV": '"production"',
64
- });
61
+ const banner = (result.banner as Record<string, string>)["js"];
62
+ expect(banner).toContain("process.env");
63
+ expect(banner).toContain("??=");
64
+ expect(banner).toContain("API_URL");
65
+ expect(banner).toContain("https://api.example.com");
66
+ expect(result.define).toBeUndefined();
65
67
  });
66
68
 
67
- it("produces empty define when env is not provided", () => {
69
+ it("does not include env code in banner when env is not provided", () => {
68
70
  const result = createServerEsbuildOptions(baseOptions);
69
- expect(result.define).toEqual({});
71
+ const banner = (result.banner as Record<string, string>)["js"];
72
+ expect(banner).toContain("createRequire");
73
+ expect(banner).not.toContain("??=");
74
+ expect(result.define).toBeUndefined();
70
75
  });
71
76
 
72
77
  it("passes external modules to esbuild", () => {
@@ -88,6 +93,31 @@ describe("createServerEsbuildOptions", () => {
88
93
  });
89
94
  });
90
95
 
96
+ describe("createEnvBanner", () => {
97
+ it("generates process.env merge code with ??= for runtime override", () => {
98
+ const banner = createEnvBanner({ API_URL: "https://api.example.com", NODE_ENV: "production" });
99
+ expect(banner).toContain("process.env");
100
+ expect(banner).toContain("??=");
101
+ expect(banner).toContain('"API_URL"');
102
+ expect(banner).toContain('"https://api.example.com"');
103
+ expect(banner).toContain('"NODE_ENV"');
104
+ expect(banner).toContain('"production"');
105
+ });
106
+
107
+ it("returns empty string when env is undefined", () => {
108
+ expect(createEnvBanner()).toBe("");
109
+ });
110
+
111
+ it("returns empty string when env is empty object", () => {
112
+ expect(createEnvBanner({})).toBe("");
113
+ });
114
+
115
+ it("JSON-encodes special characters in values", () => {
116
+ const banner = createEnvBanner({ MSG: 'hello "world"' });
117
+ expect(banner).toContain('\\"world\\"');
118
+ });
119
+ });
120
+
91
121
  describe("writeChangedOutputFiles", () => {
92
122
  beforeEach(() => {
93
123
  vi.mocked(mockFs.readFile).mockReset();
@@ -67,6 +67,19 @@ afterEach(() => {
67
67
  });
68
68
 
69
69
  describe("createClientViteConfig", () => {
70
+ // Acceptance: Scenario "define['process.env'] 제거"
71
+ it("does not include process.env in define, only import.meta.env keys", async () => {
72
+ const config = await createClientViteConfig({
73
+ ...createDefaultOptions(),
74
+ env: { DEV: "true", VER: "1.0.0" },
75
+ });
76
+
77
+ const define = config.define as Record<string, string>;
78
+ expect(define).not.toHaveProperty("process.env");
79
+ expect(define["import.meta.env.DEV"]).toBe('"true"');
80
+ expect(define["import.meta.env.VER"]).toBe('"1.0.0"');
81
+ });
82
+
70
83
  // Acceptance: Scenario "browserslist 미설정 시 최신 브라우저 유지"
71
84
  it("uses es2022 esbuild target when no browserslist is provided", async () => {
72
85
  const config = await createClientViteConfig(createDefaultOptions());