@simplysm/sd-cli 14.0.10 → 14.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/README.md +58 -253
  2. package/dist/angular/client-transform-stylesheet.js +1 -1
  3. package/dist/angular/client-transform-stylesheet.js.map +1 -1
  4. package/dist/angular/vite-angular-plugin.d.ts +1 -1
  5. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  6. package/dist/angular/vite-angular-plugin.js +60 -34
  7. package/dist/angular/vite-angular-plugin.js.map +1 -1
  8. package/dist/angular/vite-postcss-inline-plugin.d.ts +1 -1
  9. package/dist/angular/vite-postcss-inline-plugin.js +1 -1
  10. package/dist/capacitor/capacitor.d.ts +14 -2
  11. package/dist/capacitor/capacitor.d.ts.map +1 -1
  12. package/dist/capacitor/capacitor.js +131 -17
  13. package/dist/capacitor/capacitor.js.map +1 -1
  14. package/dist/commands/build.d.ts +3 -10
  15. package/dist/commands/build.d.ts.map +1 -1
  16. package/dist/commands/build.js +3 -10
  17. package/dist/commands/build.js.map +1 -1
  18. package/dist/commands/check.js +3 -3
  19. package/dist/commands/check.js.map +1 -1
  20. package/dist/commands/dev.d.ts +3 -9
  21. package/dist/commands/dev.d.ts.map +1 -1
  22. package/dist/commands/dev.js +3 -9
  23. package/dist/commands/dev.js.map +1 -1
  24. package/dist/commands/device.d.ts +13 -0
  25. package/dist/commands/device.d.ts.map +1 -0
  26. package/dist/commands/device.js +53 -0
  27. package/dist/commands/device.js.map +1 -0
  28. package/dist/commands/publish.d.ts +1 -1
  29. package/dist/commands/publish.d.ts.map +1 -1
  30. package/dist/commands/publish.js +18 -26
  31. package/dist/commands/publish.js.map +1 -1
  32. package/dist/commands/replace-deps.d.ts +3 -3
  33. package/dist/commands/replace-deps.d.ts.map +1 -1
  34. package/dist/commands/replace-deps.js +1 -1
  35. package/dist/commands/typecheck.d.ts +4 -3
  36. package/dist/commands/typecheck.d.ts.map +1 -1
  37. package/dist/commands/typecheck.js +5 -11
  38. package/dist/commands/typecheck.js.map +1 -1
  39. package/dist/commands/watch.d.ts +9 -9
  40. package/dist/commands/watch.js +9 -9
  41. package/dist/electron/electron.d.ts.map +1 -1
  42. package/dist/electron/electron.js +42 -3
  43. package/dist/electron/electron.js.map +1 -1
  44. package/dist/engines/BaseEngine.d.ts +1 -1
  45. package/dist/engines/BaseEngine.d.ts.map +1 -1
  46. package/dist/engines/BaseEngine.js +3 -1
  47. package/dist/engines/BaseEngine.js.map +1 -1
  48. package/dist/engines/NgtscEngine.d.ts +7 -7
  49. package/dist/engines/NgtscEngine.d.ts.map +1 -1
  50. package/dist/engines/NgtscEngine.js +3 -3
  51. package/dist/engines/ServerEsbuildEngine.d.ts +7 -7
  52. package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
  53. package/dist/engines/ServerEsbuildEngine.js +3 -3
  54. package/dist/engines/TscEngine.d.ts +7 -7
  55. package/dist/engines/TscEngine.d.ts.map +1 -1
  56. package/dist/engines/TscEngine.js +3 -3
  57. package/dist/engines/ViteEngine.d.ts +1 -1
  58. package/dist/engines/ViteEngine.d.ts.map +1 -1
  59. package/dist/engines/ViteEngine.js +7 -12
  60. package/dist/engines/ViteEngine.js.map +1 -1
  61. package/dist/engines/index.d.ts +5 -5
  62. package/dist/engines/index.js +5 -5
  63. package/dist/engines/types.d.ts +20 -20
  64. package/dist/engines/types.d.ts.map +1 -1
  65. package/dist/infra/ResultCollector.d.ts +9 -9
  66. package/dist/infra/ResultCollector.js +8 -8
  67. package/dist/infra/SignalHandler.d.ts +7 -7
  68. package/dist/infra/SignalHandler.js +7 -7
  69. package/dist/infra/WorkerManager.d.ts +14 -14
  70. package/dist/infra/WorkerManager.js +14 -14
  71. package/dist/orchestrators/BuildOrchestrator.d.ts +25 -25
  72. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  73. package/dist/orchestrators/BuildOrchestrator.js +29 -29
  74. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  75. package/dist/orchestrators/DevWatchOrchestrator.d.ts +7 -7
  76. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  77. package/dist/orchestrators/DevWatchOrchestrator.js +35 -57
  78. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  79. package/dist/sd-cli-entry.d.ts +2 -2
  80. package/dist/sd-cli-entry.d.ts.map +1 -1
  81. package/dist/sd-cli-entry.js +45 -9
  82. package/dist/sd-cli-entry.js.map +1 -1
  83. package/dist/sd-cli.d.ts +3 -3
  84. package/dist/sd-cli.js +16 -16
  85. package/dist/sd-cli.js.map +1 -1
  86. package/dist/sd-config.types.d.ts +105 -105
  87. package/dist/sd-config.types.d.ts.map +1 -1
  88. package/dist/utils/angular-compiler.js +5 -5
  89. package/dist/utils/angular-compiler.js.map +1 -1
  90. package/dist/utils/build-env.d.ts +1 -1
  91. package/dist/utils/build-env.js +1 -1
  92. package/dist/utils/concurrency.d.ts +7 -7
  93. package/dist/utils/concurrency.js +7 -7
  94. package/dist/utils/copy-public.d.ts +9 -9
  95. package/dist/utils/copy-public.js +17 -17
  96. package/dist/utils/copy-public.js.map +1 -1
  97. package/dist/utils/copy-src.d.ts +9 -9
  98. package/dist/utils/copy-src.js +11 -11
  99. package/dist/utils/copy-src.js.map +1 -1
  100. package/dist/utils/engine-stop.d.ts +8 -9
  101. package/dist/utils/engine-stop.d.ts.map +1 -1
  102. package/dist/utils/engine-stop.js +9 -10
  103. package/dist/utils/engine-stop.js.map +1 -1
  104. package/dist/utils/esbuild-config.d.ts +23 -23
  105. package/dist/utils/esbuild-config.d.ts.map +1 -1
  106. package/dist/utils/esbuild-config.js +25 -25
  107. package/dist/utils/esbuild-config.js.map +1 -1
  108. package/dist/utils/lint-with-program.d.ts +15 -15
  109. package/dist/utils/lint-with-program.d.ts.map +1 -1
  110. package/dist/utils/lint-with-program.js +29 -29
  111. package/dist/utils/lint-with-program.js.map +1 -1
  112. package/dist/utils/ngtsc-build-core.d.ts +8 -8
  113. package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
  114. package/dist/utils/ngtsc-build-core.js +14 -14
  115. package/dist/utils/ngtsc-build-core.js.map +1 -1
  116. package/dist/utils/output-path-rewriter.d.ts +14 -14
  117. package/dist/utils/output-path-rewriter.js +18 -18
  118. package/dist/utils/output-path-rewriter.js.map +1 -1
  119. package/dist/utils/output-utils.d.ts +6 -6
  120. package/dist/utils/output-utils.js +11 -11
  121. package/dist/utils/output-utils.js.map +1 -1
  122. package/dist/utils/package-utils.d.ts +21 -21
  123. package/dist/utils/package-utils.d.ts.map +1 -1
  124. package/dist/utils/package-utils.js +56 -45
  125. package/dist/utils/package-utils.js.map +1 -1
  126. package/dist/utils/replace-deps.d.ts +25 -25
  127. package/dist/utils/replace-deps.d.ts.map +1 -1
  128. package/dist/utils/replace-deps.js +84 -65
  129. package/dist/utils/replace-deps.js.map +1 -1
  130. package/dist/utils/sd-config.d.ts +3 -3
  131. package/dist/utils/sd-config.js +3 -3
  132. package/dist/utils/tsc-build.d.ts +13 -13
  133. package/dist/utils/tsc-build.d.ts.map +1 -1
  134. package/dist/utils/tsc-build.js +9 -9
  135. package/dist/utils/tsc-build.js.map +1 -1
  136. package/dist/utils/tsconfig.d.ts +11 -9
  137. package/dist/utils/tsconfig.d.ts.map +1 -1
  138. package/dist/utils/tsconfig.js +11 -9
  139. package/dist/utils/tsconfig.js.map +1 -1
  140. package/dist/utils/typecheck-non-package.d.ts +5 -6
  141. package/dist/utils/typecheck-non-package.d.ts.map +1 -1
  142. package/dist/utils/typecheck-non-package.js +7 -8
  143. package/dist/utils/typecheck-non-package.js.map +1 -1
  144. package/dist/utils/typecheck-serialization.d.ts +8 -8
  145. package/dist/utils/typecheck-serialization.d.ts.map +1 -1
  146. package/dist/utils/typecheck-serialization.js +12 -16
  147. package/dist/utils/typecheck-serialization.js.map +1 -1
  148. package/dist/utils/vite-config.d.ts +8 -5
  149. package/dist/utils/vite-config.d.ts.map +1 -1
  150. package/dist/utils/vite-config.js +36 -29
  151. package/dist/utils/vite-config.js.map +1 -1
  152. package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
  153. package/dist/utils/vite-scope-watch-plugin.js +1 -1
  154. package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
  155. package/dist/utils/worker-events.d.ts +12 -12
  156. package/dist/utils/worker-events.d.ts.map +1 -1
  157. package/dist/utils/worker-events.js +10 -10
  158. package/dist/utils/worker-events.js.map +1 -1
  159. package/dist/utils/worker-utils.d.ts +12 -13
  160. package/dist/utils/worker-utils.d.ts.map +1 -1
  161. package/dist/utils/worker-utils.js +12 -13
  162. package/dist/utils/worker-utils.js.map +1 -1
  163. package/dist/vitest-plugin.d.ts.map +1 -1
  164. package/dist/vitest-plugin.js +5 -7
  165. package/dist/vitest-plugin.js.map +1 -1
  166. package/dist/workers/client.worker.d.ts +4 -2
  167. package/dist/workers/client.worker.d.ts.map +1 -1
  168. package/dist/workers/client.worker.js +209 -1
  169. package/dist/workers/client.worker.js.map +1 -1
  170. package/dist/workers/library-build.worker.d.ts +1 -1
  171. package/dist/workers/library-build.worker.d.ts.map +1 -1
  172. package/dist/workers/library-build.worker.js +7 -7
  173. package/dist/workers/library-build.worker.js.map +1 -1
  174. package/dist/workers/lint.worker.d.ts +2 -2
  175. package/dist/workers/lint.worker.js +2 -2
  176. package/dist/workers/ngtsc-build.worker.js +30 -30
  177. package/dist/workers/ngtsc-build.worker.js.map +1 -1
  178. package/dist/workers/server-build.worker.d.ts +17 -17
  179. package/dist/workers/server-build.worker.d.ts.map +1 -1
  180. package/dist/workers/server-build.worker.js +46 -46
  181. package/dist/workers/server-build.worker.js.map +1 -1
  182. package/dist/workers/server-runtime.worker.d.ts +7 -7
  183. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  184. package/dist/workers/server-runtime.worker.js +17 -17
  185. package/dist/workers/server-runtime.worker.js.map +1 -1
  186. package/docs/config.md +340 -0
  187. package/docs/publish-configuration-types.md +87 -0
  188. package/docs/pwa-configuration-types.md +55 -0
  189. package/docs/vitest-plugin.md +47 -0
  190. package/package.json +9 -7
  191. package/src/angular/client-transform-stylesheet.ts +1 -1
  192. package/src/angular/vite-angular-plugin.ts +70 -37
  193. package/src/angular/vite-postcss-inline-plugin.ts +1 -1
  194. package/src/capacitor/capacitor.ts +159 -23
  195. package/src/commands/build.ts +3 -10
  196. package/src/commands/check.ts +3 -3
  197. package/src/commands/dev.ts +3 -9
  198. package/src/commands/device.ts +65 -0
  199. package/src/commands/publish.ts +30 -26
  200. package/src/commands/replace-deps.ts +3 -3
  201. package/src/commands/typecheck.ts +7 -13
  202. package/src/commands/watch.ts +9 -9
  203. package/src/electron/electron.ts +49 -4
  204. package/src/engines/BaseEngine.ts +4 -1
  205. package/src/engines/NgtscEngine.ts +7 -7
  206. package/src/engines/ServerEsbuildEngine.ts +7 -7
  207. package/src/engines/TscEngine.ts +7 -7
  208. package/src/engines/ViteEngine.ts +8 -13
  209. package/src/engines/index.ts +5 -5
  210. package/src/engines/types.ts +20 -20
  211. package/src/infra/ResultCollector.ts +9 -9
  212. package/src/infra/SignalHandler.ts +7 -7
  213. package/src/infra/WorkerManager.ts +14 -14
  214. package/src/orchestrators/BuildOrchestrator.ts +37 -37
  215. package/src/orchestrators/DevWatchOrchestrator.ts +37 -61
  216. package/src/sd-cli-entry.ts +51 -9
  217. package/src/sd-cli.ts +16 -16
  218. package/src/sd-config.types.ts +107 -107
  219. package/src/utils/angular-compiler.ts +5 -5
  220. package/src/utils/build-env.ts +1 -1
  221. package/src/utils/concurrency.ts +7 -7
  222. package/src/utils/copy-public.ts +17 -17
  223. package/src/utils/copy-src.ts +11 -11
  224. package/src/utils/engine-stop.ts +9 -10
  225. package/src/utils/esbuild-config.ts +29 -29
  226. package/src/utils/lint-with-program.ts +34 -34
  227. package/src/utils/ngtsc-build-core.ts +17 -17
  228. package/src/utils/output-path-rewriter.ts +18 -18
  229. package/src/utils/output-utils.ts +11 -11
  230. package/src/utils/package-utils.ts +57 -45
  231. package/src/utils/replace-deps.ts +92 -67
  232. package/src/utils/sd-config.ts +3 -3
  233. package/src/utils/tsc-build.ts +18 -18
  234. package/src/utils/tsconfig.ts +11 -9
  235. package/src/utils/typecheck-non-package.ts +7 -8
  236. package/src/utils/typecheck-serialization.ts +13 -15
  237. package/src/utils/vite-config.ts +45 -35
  238. package/src/utils/vite-scope-watch-plugin.ts +6 -1
  239. package/src/utils/worker-events.ts +16 -16
  240. package/src/utils/worker-utils.ts +12 -13
  241. package/src/vitest-plugin.ts +5 -8
  242. package/src/workers/client.worker.ts +236 -2
  243. package/src/workers/library-build.worker.ts +8 -8
  244. package/src/workers/lint.worker.ts +2 -2
  245. package/src/workers/ngtsc-build.worker.ts +31 -31
  246. package/src/workers/server-build.worker.ts +60 -60
  247. package/src/workers/server-runtime.worker.ts +22 -22
  248. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +1 -0
  249. package/tests/angular/vite-angular-plugin-hmr.spec.ts +78 -0
  250. package/tests/angular/vite-angular-plugin.spec.ts +67 -0
  251. package/tests/capacitor/capacitor-build.spec.ts +6 -4
  252. package/tests/capacitor/capacitor-icon.spec.ts +7 -5
  253. package/tests/capacitor/capacitor-init.spec.ts +120 -10
  254. package/tests/capacitor/capacitor-run.spec.ts +14 -17
  255. package/tests/capacitor/capacitor-workspace.spec.ts +5 -3
  256. package/tests/commands/check.spec.ts +2 -2
  257. package/tests/commands/device.spec.ts +147 -0
  258. package/tests/commands/publish.spec.ts +2 -2
  259. package/tests/commands/typecheck.spec.ts +8 -0
  260. package/tests/electron/electron.spec.ts +12 -10
  261. package/tests/engines/base-engine.spec.ts +37 -0
  262. package/tests/engines/vite-engine.spec.ts +115 -3
  263. package/tests/orchestrators/dev-watch-orchestrator.spec.ts +21 -93
  264. package/tests/utils/vite-config.spec.ts +144 -90
  265. package/tests/workers/client-worker.spec.ts +690 -0
  266. package/tests/workers/server-build-worker.spec.ts +3 -3
@@ -27,8 +27,8 @@ vi.mock("@simplysm/core-node", () => ({
27
27
  copy: mockFsxCopy,
28
28
  },
29
29
  cpx: {
30
- exec: mockCpxExec,
31
- execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
30
+ spawn: mockCpxSpawn,
31
+ spawnSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
32
32
  },
33
33
  pathx: {
34
34
  posixResolve: (...args: string[]) => path.resolve(...args).replace(/\\/g, "/"),
@@ -49,7 +49,7 @@ const execaCalls: { command: string; args: string[] }[] = [];
49
49
  let execaFactory: (...args: unknown[]) => Promise<{ stdout: string; stderr: string; exitCode: number }> = () =>
50
50
  Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
51
51
 
52
- const mockCpxExec = vi.fn((...args: unknown[]) => {
52
+ const mockCpxSpawn = vi.fn((...args: unknown[]) => {
53
53
  execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
54
54
  return execaFactory(...args);
55
55
  });
@@ -84,7 +84,9 @@ vi.mock("consola", () => ({
84
84
  warn: mockLoggerWarn,
85
85
  info: vi.fn(),
86
86
  }),
87
+ level: 0,
87
88
  },
89
+ LogLevels: { debug: 4 },
88
90
  }));
89
91
 
90
92
  //#endregion
@@ -175,14 +177,14 @@ describe("Capacitor.run()", () => {
175
177
 
176
178
  // cap copy android + cap run android
177
179
  const capCmds = execaCalls.filter(
178
- (c) => c.command === "npx" && c.args.includes("cap"),
180
+ (c) => c.command === "pnpm" && c.args.includes("cap"),
179
181
  );
180
182
  expect(capCmds.some((c) => c.args.includes("copy") && c.args.includes("android"))).toBe(true);
181
183
  expect(capCmds.some((c) => c.args.includes("run") && c.args.includes("android"))).toBe(true);
182
184
  });
183
185
 
184
- // Unit: adb kill-server retry on cap run failure
185
- it("retries cap run after adb kill-server on android platform failure", async () => {
186
+ // Unit: cap run 실패 시 adb kill-server 호출 에러를 re-throw한다
187
+ it("calls adb kill-server and re-throws on android platform cap run failure", async () => {
186
188
  const { Capacitor } = await import("../../src/capacitor/capacitor.js");
187
189
 
188
190
  let capRunCallCount = 0;
@@ -191,16 +193,13 @@ describe("Capacitor.run()", () => {
191
193
  const cmdArgs = (args[1] as string[] | undefined) ?? [];
192
194
 
193
195
  if (
194
- cmd === "npx" &&
196
+ cmd === "pnpm" &&
195
197
  cmdArgs.includes("cap") &&
196
198
  cmdArgs.includes("run") &&
197
199
  cmdArgs.includes("android")
198
200
  ) {
199
201
  capRunCallCount++;
200
- if (capRunCallCount === 1) {
201
- // First cap run fails
202
- return Promise.reject(new Error("cap run failed"));
203
- }
202
+ return Promise.reject(new Error("cap run failed"));
204
203
  }
205
204
  return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
206
205
  };
@@ -211,16 +210,14 @@ describe("Capacitor.run()", () => {
211
210
  platform: { android: {} },
212
211
  });
213
212
 
214
- await cap.run("http://localhost:4200");
213
+ await expect(cap.run("http://localhost:4200")).rejects.toThrow("cap run failed");
215
214
 
216
- // adb kill-server should have been called between the two cap run attempts
215
+ // adb kill-server should have been called
217
216
  expect(
218
217
  execaCalls.some((c) => c.command === "adb" && c.args.includes("kill-server")),
219
218
  ).toBe(true);
220
- expect(capRunCallCount).toBe(2);
221
- expect(mockLoggerWarn).toHaveBeenCalledWith(
222
- expect.stringContaining("adb kill-server"),
223
- );
219
+ // cap run은 한 번만 호출 (재시도 없음)
220
+ expect(capRunCallCount).toBe(1);
224
221
  });
225
222
 
226
223
  // Unit: _updateServerUrl replaces existing url
@@ -26,8 +26,8 @@ vi.mock("@simplysm/core-node", () => ({
26
26
  copy: mockFsxCopy,
27
27
  },
28
28
  cpx: {
29
- exec: mockCpxExec,
30
- execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
29
+ spawn: mockCpxSpawn,
30
+ spawnSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
31
31
  },
32
32
  pathx: {
33
33
  posixResolve: (...args: string[]) => path.resolve(...args).replace(/\\/g, "/"),
@@ -43,7 +43,7 @@ vi.mock("@simplysm/core-common", () => ({
43
43
  }));
44
44
 
45
45
  const execaCalls: { command: string; args: string[] }[] = [];
46
- const mockCpxExec = vi.fn((...args: unknown[]) => {
46
+ const mockCpxSpawn = vi.fn((...args: unknown[]) => {
47
47
  execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
48
48
  return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
49
49
  });
@@ -75,7 +75,9 @@ vi.mock("fs/promises", () => ({
75
75
  vi.mock("consola", () => ({
76
76
  consola: {
77
77
  withTag: () => ({ debug: vi.fn(), warn: vi.fn() }),
78
+ level: 0,
78
79
  },
80
+ LogLevels: { debug: 4 },
79
81
  }));
80
82
 
81
83
  //#endregion
@@ -37,8 +37,8 @@ vi.mock("../../src/utils/sd-config", () => ({
37
37
 
38
38
  vi.mock("@simplysm/core-node", () => ({
39
39
  cpx: {
40
- exec: mocks.execa,
41
- execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
40
+ spawn: mocks.execa,
41
+ spawnSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
42
42
  },
43
43
  }));
44
44
 
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // Capacitor mock
4
+ const mockCapacitorInstance = {
5
+ initialize: vi.fn().mockResolvedValue(undefined),
6
+ run: vi.fn().mockResolvedValue(undefined),
7
+ build: vi.fn().mockResolvedValue(undefined),
8
+ };
9
+
10
+ vi.mock("../../src/capacitor/capacitor", () => ({
11
+ Capacitor: {
12
+ create: vi.fn().mockResolvedValue(mockCapacitorInstance),
13
+ },
14
+ }));
15
+
16
+ // Electron mock
17
+ const mockElectronInstance = {
18
+ initialize: vi.fn().mockResolvedValue(undefined),
19
+ run: vi.fn().mockResolvedValue(undefined),
20
+ build: vi.fn().mockResolvedValue(undefined),
21
+ };
22
+
23
+ vi.mock("../../src/electron/electron", () => ({
24
+ Electron: {
25
+ create: vi.fn().mockResolvedValue(mockElectronInstance),
26
+ },
27
+ }));
28
+
29
+ // loadSdConfig mock
30
+ vi.mock("../../src/utils/sd-config", () => ({
31
+ loadSdConfig: vi.fn(),
32
+ }));
33
+
34
+ const { Capacitor } = await import("../../src/capacitor/capacitor");
35
+ const { Electron } = await import("../../src/electron/electron");
36
+ const { loadSdConfig } = await import("../../src/utils/sd-config");
37
+ const { runDevice } = await import("../../src/commands/device");
38
+
39
+ describe("runDevice", () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ mockCapacitorInstance.run.mockResolvedValue(undefined);
43
+ mockElectronInstance.run.mockResolvedValue(undefined);
44
+ });
45
+
46
+ // Acceptance: Scenario "device 명령어로 Capacitor 앱 실행"
47
+ it("runs Capacitor.create + run when capacitor config exists", async () => {
48
+ vi.mocked(loadSdConfig).mockResolvedValue({
49
+ packages: {
50
+ "client-pda": {
51
+ target: "client",
52
+ server: 40480,
53
+ capacitor: { appId: "com.test.app", appName: "TestApp" },
54
+ },
55
+ },
56
+ });
57
+
58
+ await runDevice({ package: "client-pda", options: [] });
59
+
60
+ expect(Capacitor.create).toHaveBeenCalledWith(
61
+ expect.stringContaining("client-pda"),
62
+ { appId: "com.test.app", appName: "TestApp" },
63
+ undefined,
64
+ );
65
+ expect(mockCapacitorInstance.run).toHaveBeenCalledWith("http://localhost:40480");
66
+ });
67
+
68
+ // Acceptance: Scenario "device 명령어로 Electron 앱 실행"
69
+ it("runs Electron.create + run when electron config exists", async () => {
70
+ vi.mocked(loadSdConfig).mockResolvedValue({
71
+ packages: {
72
+ "my-client": {
73
+ target: "client",
74
+ server: 4200,
75
+ electron: { appId: "com.test.electron" },
76
+ },
77
+ },
78
+ });
79
+
80
+ await runDevice({ package: "my-client", options: [] });
81
+
82
+ expect(Electron.create).toHaveBeenCalledWith(
83
+ expect.stringContaining("my-client"),
84
+ { appId: "com.test.electron" },
85
+ undefined,
86
+ );
87
+ expect(mockElectronInstance.run).toHaveBeenCalledWith("http://localhost:4200");
88
+ });
89
+
90
+ // Acceptance: Scenario "device 명령어에 URL 옵션 지정"
91
+ it("uses provided URL instead of auto-generated one", async () => {
92
+ vi.mocked(loadSdConfig).mockResolvedValue({
93
+ packages: {
94
+ "client-pda": {
95
+ target: "client",
96
+ server: 40480,
97
+ capacitor: { appId: "com.test.app", appName: "TestApp" },
98
+ },
99
+ },
100
+ });
101
+
102
+ await runDevice({ package: "client-pda", url: "http://192.168.1.100:4200", options: [] });
103
+
104
+ expect(mockCapacitorInstance.run).toHaveBeenCalledWith("http://192.168.1.100:4200");
105
+ });
106
+
107
+ // Unit: electron이 capacitor보다 우선 (v13 동작)
108
+ it("prefers electron over capacitor when both are configured", async () => {
109
+ vi.mocked(loadSdConfig).mockResolvedValue({
110
+ packages: {
111
+ "my-client": {
112
+ target: "client",
113
+ server: 4200,
114
+ capacitor: { appId: "com.test.app", appName: "TestApp" },
115
+ electron: { appId: "com.test.electron" },
116
+ },
117
+ },
118
+ });
119
+
120
+ await runDevice({ package: "my-client", options: [] });
121
+
122
+ expect(Electron.create).toHaveBeenCalled();
123
+ expect(Capacitor.create).not.toHaveBeenCalled();
124
+ });
125
+
126
+ // Unit: 존재하지 않는 패키지 에러
127
+ it("throws when package does not exist", async () => {
128
+ vi.mocked(loadSdConfig).mockResolvedValue({
129
+ packages: {
130
+ "other-pkg": { target: "node" },
131
+ },
132
+ });
133
+
134
+ await expect(runDevice({ package: "nonexistent", options: [] })).rejects.toThrow();
135
+ });
136
+
137
+ // Unit: client가 아닌 패키지 에러
138
+ it("throws when package is not a client target", async () => {
139
+ vi.mocked(loadSdConfig).mockResolvedValue({
140
+ packages: {
141
+ "my-server": { target: "server" },
142
+ },
143
+ });
144
+
145
+ await expect(runDevice({ package: "my-server", options: [] })).rejects.toThrow();
146
+ });
147
+ });
@@ -51,8 +51,8 @@ vi.mock("../../src/utils/replace-deps", () => ({
51
51
  vi.mock("@simplysm/core-node", () => ({
52
52
  fsx: mocks.fsx,
53
53
  cpx: {
54
- exec: mocks.execa,
55
- execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
54
+ spawn: mocks.execa,
55
+ spawnSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
56
56
  },
57
57
  }));
58
58
 
@@ -493,6 +493,14 @@ describe("executeTypecheck", () => {
493
493
 
494
494
  //#endregion
495
495
 
496
+ it("throws when loadSdConfig fails (fail fast)", async () => {
497
+ mocks.loadSdConfig.mockRejectedValue(new Error("sd.config.ts not found"));
498
+ mocks.discoverWorkspacePackages.mockReturnValue(new Map());
499
+ mocks.mergeTestsPackagesIntoConfig.mockReturnValue({ merged: {}, pathMap: new Map() });
500
+
501
+ await expect(executeTypecheck({ targets: [], options: [] })).rejects.toThrow("sd.config.ts not found");
502
+ });
503
+
496
504
  //#region Slice 1: executeTypecheck lint integration (Feature 3.2)
497
505
 
498
506
  describe("lint integration", () => {
@@ -23,8 +23,8 @@ vi.mock("@simplysm/core-node", () => ({
23
23
  glob: mockFsxGlob,
24
24
  },
25
25
  cpx: {
26
- exec: mockCpxExec,
27
- execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
26
+ spawn: mockCpxSpawn,
27
+ spawnSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
28
28
  },
29
29
  pathx: {
30
30
  posixResolve: (...args: string[]) => path.resolve(...args).replace(/\\/g, "/"),
@@ -33,7 +33,7 @@ vi.mock("@simplysm/core-node", () => ({
33
33
  }));
34
34
 
35
35
  // cpx mock (was execa)
36
- const mockCpxExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
36
+ const mockCpxSpawn = vi.fn().mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
37
37
 
38
38
  // esbuild mock
39
39
  const mockEsbuildBuild = vi.fn().mockResolvedValue({});
@@ -75,7 +75,9 @@ vi.mock("consola", () => ({
75
75
  warn: mockLoggerWarn,
76
76
  info: mockLoggerInfo,
77
77
  }),
78
+ level: 0,
78
79
  },
80
+ LogLevels: { debug: 4 },
79
81
  }));
80
82
 
81
83
  //#endregion
@@ -105,7 +107,7 @@ function setupDefaultMocks() {
105
107
  }
106
108
  return Promise.resolve([]);
107
109
  });
108
- mockCpxExec.mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
110
+ mockCpxSpawn.mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
109
111
  mockEsbuildBuild.mockResolvedValue({});
110
112
  }
111
113
 
@@ -179,7 +181,7 @@ describe("Electron", () => {
179
181
 
180
182
  expect(findElectronPackageJson()).toBeDefined();
181
183
 
182
- const execaCalls = mockCpxExec.mock.calls;
184
+ const execaCalls = mockCpxSpawn.mock.calls;
183
185
  expect(
184
186
  execaCalls.find((c) => c[0] === "npm" && (c[1] as string[]).includes("install")),
185
187
  ).toBeDefined();
@@ -196,7 +198,7 @@ describe("Electron", () => {
196
198
  const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
197
199
  await electron.initialize();
198
200
 
199
- const rebuildCall = mockCpxExec.mock.calls.find(
201
+ const rebuildCall = mockCpxSpawn.mock.calls.find(
200
202
  (c) => typeof c[0] === "string" && c[0].includes("electron-rebuild"),
201
203
  );
202
204
  expect(rebuildCall).toBeUndefined();
@@ -453,7 +455,7 @@ describe("Electron", () => {
453
455
  const electronKill = vi.fn();
454
456
  let resolveElectron: () => void = () => {};
455
457
 
456
- mockCpxExec.mockImplementation((cmd: string) => {
458
+ mockCpxSpawn.mockImplementation((cmd: string) => {
457
459
  if (typeof cmd === "string" && cmd.includes("electron")) {
458
460
  // Electron process: create a deferred promise we can resolve externally
459
461
  const p = new Promise<void>((resolve) => {
@@ -535,7 +537,7 @@ describe("Electron", () => {
535
537
  describe("단위: run() 플러그인 동작", () => {
536
538
  it("passes custom env and ELECTRON_DEV_URL via esbuild banner", async () => {
537
539
  let resolveElectron: () => void = () => {};
538
- mockCpxExec.mockImplementation((cmd: string) => {
540
+ mockCpxSpawn.mockImplementation((cmd: string) => {
539
541
  if (typeof cmd === "string" && cmd.includes("electron")) {
540
542
  const p = new Promise<void>((resolve) => {
541
543
  resolveElectron = resolve;
@@ -568,7 +570,7 @@ describe("Electron", () => {
568
570
 
569
571
  it("calls initialize() before starting esbuild context", async () => {
570
572
  let resolveElectron: () => void = () => {};
571
- mockCpxExec.mockImplementation((cmd: string) => {
573
+ mockCpxSpawn.mockImplementation((cmd: string) => {
572
574
  if (typeof cmd === "string" && cmd.includes("electron")) {
573
575
  const p = new Promise<void>((resolve) => {
574
576
  resolveElectron = resolve;
@@ -588,7 +590,7 @@ describe("Electron", () => {
588
590
  await runPromise;
589
591
 
590
592
  // initialize calls npm install -> execa should have been called with npm install
591
- const npmInstallCall = mockCpxExec.mock.calls.find(
593
+ const npmInstallCall = mockCpxSpawn.mock.calls.find(
592
594
  (c: any[]) => c[0] === "npm" && (c[1] as string[]).includes("install"),
593
595
  );
594
596
  expect(npmInstallCall).toBeDefined();
@@ -263,6 +263,43 @@ describe("BaseEngine", () => {
263
263
  await engine.stop();
264
264
  });
265
265
 
266
+ it("calls resolver on error event to release RebuildManager batch", async () => {
267
+ const mockResolver = vi.fn();
268
+ const mockRebuildManager = { registerBuild: vi.fn(() => mockResolver) };
269
+
270
+ mockWorker.startWatch.mockImplementation(() => {
271
+ // Trigger initial build to move past isInitialBuild
272
+ const buildHandler = mockWorker.on.mock.calls.find(
273
+ (call: any[]) => call[0] === "build",
274
+ )?.[1];
275
+ buildHandler?.({ build: { success: true } });
276
+ });
277
+
278
+ const engine = new TscEngine({
279
+ cwd: "/root",
280
+ pkg: createMockPkg(),
281
+ rebuildManager: mockRebuildManager as any,
282
+ });
283
+
284
+ await engine.startWatch({ js: true, dts: true });
285
+
286
+ // Simulate rebuild cycle: buildStart -> error (no build event)
287
+ const buildStartHandler = mockWorker.on.mock.calls.find(
288
+ (call: any[]) => call[0] === "buildStart",
289
+ )?.[1];
290
+ const errorHandler = mockWorker.on.mock.calls.find(
291
+ (call: any[]) => call[0] === "error",
292
+ )?.[1];
293
+
294
+ buildStartHandler?.({});
295
+ expect(mockRebuildManager.registerBuild).toHaveBeenCalled();
296
+
297
+ errorHandler?.({ message: "Worker crashed" });
298
+ expect(mockResolver).toHaveBeenCalled();
299
+
300
+ await engine.stop();
301
+ });
302
+
266
303
  it("uses _getTarget() for BuildResult target", async () => {
267
304
  const mockResultCollector = { add: vi.fn() };
268
305
 
@@ -191,6 +191,30 @@ describe("ViteEngine", () => {
191
191
  await engine.stop();
192
192
  });
193
193
 
194
+ // Acceptance: Scenario "exclude 전달 (build)"
195
+ it("passes exclude from config to worker build call", async () => {
196
+ mockWorker.build.mockResolvedValue({ success: true });
197
+
198
+ const engine = new ViteEngine({
199
+ cwd: "/root",
200
+ pkg: createMockPkg({
201
+ config: {
202
+ target: "client",
203
+ server: "my-server",
204
+ exclude: ["jeep-sqlite"],
205
+ } as any,
206
+ }),
207
+ });
208
+ await engine.run({ js: true, dts: false });
209
+
210
+ expect(mockWorker.build).toHaveBeenCalledWith(
211
+ expect.objectContaining({
212
+ exclude: ["jeep-sqlite"],
213
+ }),
214
+ );
215
+ await engine.stop();
216
+ });
217
+
194
218
  // Unit: build failure reflects in result
195
219
  it("reflects build failure in result", async () => {
196
220
  mockWorker.build.mockResolvedValue({
@@ -265,11 +289,18 @@ describe("ViteEngine", () => {
265
289
  await engine.stop();
266
290
  });
267
291
 
268
- // Acceptance: Scenario "ResultCollector에 결과 보고"
269
- it("reports build result to ResultCollector", async () => {
292
+ // Acceptance: Scenario "ResultCollector에 결과 보고 — build 이벤트 경유"
293
+ it("reports build result to ResultCollector via build event only", async () => {
270
294
  const mockResultCollector = { add: vi.fn() };
271
295
 
272
- mockWorker.startWatch.mockResolvedValue({ success: true });
296
+ mockWorker.startWatch.mockImplementation(() => {
297
+ // Simulate "build" event during startWatch (Angular plugin buildStart)
298
+ const buildHandler = mockWorker.on.mock.calls.find(
299
+ (call: any[]) => call[0] === "build",
300
+ )?.[1];
301
+ buildHandler?.({ success: true });
302
+ return Promise.resolve({ success: true });
303
+ });
273
304
 
274
305
  const engine = new ViteEngine({
275
306
  cwd: "/root",
@@ -388,6 +419,32 @@ describe("ViteEngine", () => {
388
419
  await engine.stop();
389
420
  });
390
421
 
422
+ // Acceptance: Scenario "exclude 전달 (watch)"
423
+ it("passes exclude from config to worker startWatch call", async () => {
424
+ mockWorker.startWatch.mockResolvedValue({ success: true });
425
+
426
+ const engine = new ViteEngine({
427
+ cwd: "/root",
428
+ pkg: createMockPkg({
429
+ config: {
430
+ target: "client",
431
+ server: "my-server",
432
+ exclude: ["jeep-sqlite", "another-pkg"],
433
+ } as any,
434
+ }),
435
+ });
436
+
437
+ await engine.startWatch({ js: true, dts: false });
438
+
439
+ expect(mockWorker.startWatch).toHaveBeenCalledWith(
440
+ expect.objectContaining({
441
+ exclude: ["jeep-sqlite", "another-pkg"],
442
+ }),
443
+ );
444
+
445
+ await engine.stop();
446
+ });
447
+
391
448
  // Unit: server: string does not pass port
392
449
  it("does not pass port when config.server is a string", async () => {
393
450
  mockWorker.startWatch.mockResolvedValue({ success: true });
@@ -480,6 +537,61 @@ describe("ViteEngine", () => {
480
537
  await engine.stop();
481
538
  });
482
539
 
540
+ // Unit: mock worker가 serverReady를 발행하지 않으면 port가 undefined로 남는다
541
+ it("leaves port undefined when worker mock does not emit serverReady for legacyModule", async () => {
542
+ mockWorker.startWatch.mockResolvedValue({ success: true });
543
+
544
+ const engine = new ViteEngine({
545
+ cwd: "/root",
546
+ pkg: createMockPkg({
547
+ config: {
548
+ target: "client",
549
+ server: "my-server",
550
+ browserSupport: { legacyModule: true },
551
+ } as any,
552
+ }),
553
+ });
554
+
555
+ await engine.startWatch({ js: true, dts: false });
556
+
557
+ // serverReady is subscribed but never emitted — port stays undefined
558
+ expect(engine.port).toBeUndefined();
559
+
560
+ // buildStart/build event handlers are still registered
561
+ expect(mockWorker.on).toHaveBeenCalledWith("buildStart", expect.any(Function));
562
+ expect(mockWorker.on).toHaveBeenCalledWith("build", expect.any(Function));
563
+
564
+ await engine.stop();
565
+ });
566
+
567
+ // Unit: initial build result is reported exactly once (not duplicated by startWatch return)
568
+ it("reports initial build result exactly once to ResultCollector", async () => {
569
+ const mockResultCollector = { add: vi.fn() };
570
+
571
+ mockWorker.startWatch.mockImplementation(() => {
572
+ const buildHandler = mockWorker.on.mock.calls.find(
573
+ (call: any[]) => call[0] === "build",
574
+ )?.[1];
575
+ buildHandler?.({ success: true });
576
+ return Promise.resolve({ success: true });
577
+ });
578
+
579
+ const engine = new ViteEngine({
580
+ cwd: "/root",
581
+ pkg: createMockPkg(),
582
+ resultCollector: mockResultCollector as any,
583
+ });
584
+
585
+ await engine.startWatch({ js: true, dts: false });
586
+
587
+ const buildAddCalls = mockResultCollector.add.mock.calls.filter(
588
+ (c: any[]) => c[0].type === "build",
589
+ );
590
+ expect(buildAddCalls).toHaveLength(1);
591
+
592
+ await engine.stop();
593
+ });
594
+
483
595
  // Unit: error event reports to ResultCollector
484
596
  it("reports error from error event to ResultCollector", async () => {
485
597
  const mockResultCollector = { add: vi.fn() };