@simplysm/sd-cli 14.0.11 → 14.0.13

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 (265) 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 +4 -2
  5. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  6. package/dist/angular/vite-angular-plugin.js +73 -36
  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 +20 -2
  11. package/dist/capacitor/capacitor.d.ts.map +1 -1
  12. package/dist/capacitor/capacitor.js +155 -28
  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 +3 -3
  25. package/dist/commands/device.js +5 -5
  26. package/dist/commands/device.js.map +1 -1
  27. package/dist/commands/publish.d.ts +1 -1
  28. package/dist/commands/publish.d.ts.map +1 -1
  29. package/dist/commands/publish.js +24 -26
  30. package/dist/commands/publish.js.map +1 -1
  31. package/dist/commands/replace-deps.d.ts +3 -3
  32. package/dist/commands/replace-deps.d.ts.map +1 -1
  33. package/dist/commands/replace-deps.js +1 -1
  34. package/dist/commands/typecheck.d.ts +4 -3
  35. package/dist/commands/typecheck.d.ts.map +1 -1
  36. package/dist/commands/typecheck.js +5 -11
  37. package/dist/commands/typecheck.js.map +1 -1
  38. package/dist/commands/watch.d.ts +9 -9
  39. package/dist/commands/watch.js +9 -9
  40. package/dist/electron/electron.d.ts.map +1 -1
  41. package/dist/electron/electron.js +42 -3
  42. package/dist/electron/electron.js.map +1 -1
  43. package/dist/engines/BaseEngine.d.ts +1 -1
  44. package/dist/engines/BaseEngine.d.ts.map +1 -1
  45. package/dist/engines/BaseEngine.js +3 -1
  46. package/dist/engines/BaseEngine.js.map +1 -1
  47. package/dist/engines/NgtscEngine.d.ts +7 -7
  48. package/dist/engines/NgtscEngine.d.ts.map +1 -1
  49. package/dist/engines/NgtscEngine.js +3 -3
  50. package/dist/engines/ServerEsbuildEngine.d.ts +7 -7
  51. package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
  52. package/dist/engines/ServerEsbuildEngine.js +3 -3
  53. package/dist/engines/TscEngine.d.ts +7 -7
  54. package/dist/engines/TscEngine.d.ts.map +1 -1
  55. package/dist/engines/TscEngine.js +3 -3
  56. package/dist/engines/ViteEngine.d.ts +7 -1
  57. package/dist/engines/ViteEngine.d.ts.map +1 -1
  58. package/dist/engines/ViteEngine.js +13 -12
  59. package/dist/engines/ViteEngine.js.map +1 -1
  60. package/dist/engines/index.d.ts +9 -5
  61. package/dist/engines/index.d.ts.map +1 -1
  62. package/dist/engines/index.js +7 -5
  63. package/dist/engines/index.js.map +1 -1
  64. package/dist/engines/types.d.ts +20 -20
  65. package/dist/engines/types.d.ts.map +1 -1
  66. package/dist/infra/ResultCollector.d.ts +9 -9
  67. package/dist/infra/ResultCollector.js +8 -8
  68. package/dist/infra/SignalHandler.d.ts +7 -7
  69. package/dist/infra/SignalHandler.js +7 -7
  70. package/dist/infra/WorkerManager.d.ts +14 -14
  71. package/dist/infra/WorkerManager.js +14 -14
  72. package/dist/orchestrators/BuildOrchestrator.d.ts +25 -25
  73. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  74. package/dist/orchestrators/BuildOrchestrator.js +34 -30
  75. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  76. package/dist/orchestrators/DevWatchOrchestrator.d.ts +7 -7
  77. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  78. package/dist/orchestrators/DevWatchOrchestrator.js +34 -34
  79. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  80. package/dist/sd-cli-entry.d.ts +2 -2
  81. package/dist/sd-cli-entry.d.ts.map +1 -1
  82. package/dist/sd-cli-entry.js +15 -8
  83. package/dist/sd-cli-entry.js.map +1 -1
  84. package/dist/sd-cli.d.ts +3 -3
  85. package/dist/sd-cli.js +16 -16
  86. package/dist/sd-cli.js.map +1 -1
  87. package/dist/sd-config.types.d.ts +105 -105
  88. package/dist/sd-config.types.d.ts.map +1 -1
  89. package/dist/utils/angular-compiler.js +5 -5
  90. package/dist/utils/angular-compiler.js.map +1 -1
  91. package/dist/utils/build-env.d.ts +1 -1
  92. package/dist/utils/build-env.js +1 -1
  93. package/dist/utils/concurrency.d.ts +7 -7
  94. package/dist/utils/concurrency.js +7 -7
  95. package/dist/utils/copy-public.d.ts +9 -9
  96. package/dist/utils/copy-public.js +17 -17
  97. package/dist/utils/copy-public.js.map +1 -1
  98. package/dist/utils/copy-src.d.ts +9 -9
  99. package/dist/utils/copy-src.js +11 -11
  100. package/dist/utils/copy-src.js.map +1 -1
  101. package/dist/utils/engine-stop.d.ts +8 -9
  102. package/dist/utils/engine-stop.d.ts.map +1 -1
  103. package/dist/utils/engine-stop.js +9 -10
  104. package/dist/utils/engine-stop.js.map +1 -1
  105. package/dist/utils/esbuild-config.d.ts +23 -23
  106. package/dist/utils/esbuild-config.d.ts.map +1 -1
  107. package/dist/utils/esbuild-config.js +25 -25
  108. package/dist/utils/esbuild-config.js.map +1 -1
  109. package/dist/utils/lint-with-program.d.ts +15 -15
  110. package/dist/utils/lint-with-program.d.ts.map +1 -1
  111. package/dist/utils/lint-with-program.js +29 -29
  112. package/dist/utils/lint-with-program.js.map +1 -1
  113. package/dist/utils/ngtsc-build-core.d.ts +8 -8
  114. package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
  115. package/dist/utils/ngtsc-build-core.js +14 -14
  116. package/dist/utils/ngtsc-build-core.js.map +1 -1
  117. package/dist/utils/output-path-rewriter.d.ts +14 -14
  118. package/dist/utils/output-path-rewriter.js +18 -18
  119. package/dist/utils/output-path-rewriter.js.map +1 -1
  120. package/dist/utils/output-utils.d.ts +6 -6
  121. package/dist/utils/output-utils.js +11 -11
  122. package/dist/utils/output-utils.js.map +1 -1
  123. package/dist/utils/package-utils.d.ts +21 -21
  124. package/dist/utils/package-utils.d.ts.map +1 -1
  125. package/dist/utils/package-utils.js +56 -45
  126. package/dist/utils/package-utils.js.map +1 -1
  127. package/dist/utils/replace-deps.d.ts +25 -25
  128. package/dist/utils/replace-deps.d.ts.map +1 -1
  129. package/dist/utils/replace-deps.js +84 -65
  130. package/dist/utils/replace-deps.js.map +1 -1
  131. package/dist/utils/sd-config.d.ts +3 -3
  132. package/dist/utils/sd-config.js +3 -3
  133. package/dist/utils/tsc-build.d.ts +13 -13
  134. package/dist/utils/tsc-build.d.ts.map +1 -1
  135. package/dist/utils/tsc-build.js +9 -9
  136. package/dist/utils/tsc-build.js.map +1 -1
  137. package/dist/utils/tsconfig.d.ts +11 -9
  138. package/dist/utils/tsconfig.d.ts.map +1 -1
  139. package/dist/utils/tsconfig.js +11 -9
  140. package/dist/utils/tsconfig.js.map +1 -1
  141. package/dist/utils/typecheck-non-package.d.ts +5 -6
  142. package/dist/utils/typecheck-non-package.d.ts.map +1 -1
  143. package/dist/utils/typecheck-non-package.js +7 -8
  144. package/dist/utils/typecheck-non-package.js.map +1 -1
  145. package/dist/utils/typecheck-serialization.d.ts +8 -8
  146. package/dist/utils/typecheck-serialization.d.ts.map +1 -1
  147. package/dist/utils/typecheck-serialization.js +12 -16
  148. package/dist/utils/typecheck-serialization.js.map +1 -1
  149. package/dist/utils/vite-config.d.ts +12 -5
  150. package/dist/utils/vite-config.d.ts.map +1 -1
  151. package/dist/utils/vite-config.js +95 -41
  152. package/dist/utils/vite-config.js.map +1 -1
  153. package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
  154. package/dist/utils/vite-scope-watch-plugin.js +1 -1
  155. package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
  156. package/dist/utils/worker-events.d.ts +12 -12
  157. package/dist/utils/worker-events.d.ts.map +1 -1
  158. package/dist/utils/worker-events.js +10 -10
  159. package/dist/utils/worker-events.js.map +1 -1
  160. package/dist/utils/worker-utils.d.ts +12 -13
  161. package/dist/utils/worker-utils.d.ts.map +1 -1
  162. package/dist/utils/worker-utils.js +12 -13
  163. package/dist/utils/worker-utils.js.map +1 -1
  164. package/dist/vitest-plugin.d.ts.map +1 -1
  165. package/dist/vitest-plugin.js +5 -7
  166. package/dist/vitest-plugin.js.map +1 -1
  167. package/dist/workers/client.worker.d.ts +8 -2
  168. package/dist/workers/client.worker.d.ts.map +1 -1
  169. package/dist/workers/client.worker.js +215 -6
  170. package/dist/workers/client.worker.js.map +1 -1
  171. package/dist/workers/library-build.worker.d.ts +1 -1
  172. package/dist/workers/library-build.worker.d.ts.map +1 -1
  173. package/dist/workers/library-build.worker.js +7 -7
  174. package/dist/workers/library-build.worker.js.map +1 -1
  175. package/dist/workers/lint.worker.d.ts +2 -2
  176. package/dist/workers/lint.worker.js +2 -2
  177. package/dist/workers/ngtsc-build.worker.js +30 -30
  178. package/dist/workers/ngtsc-build.worker.js.map +1 -1
  179. package/dist/workers/server-build.worker.d.ts +17 -17
  180. package/dist/workers/server-build.worker.d.ts.map +1 -1
  181. package/dist/workers/server-build.worker.js +46 -46
  182. package/dist/workers/server-build.worker.js.map +1 -1
  183. package/dist/workers/server-runtime.worker.d.ts +7 -7
  184. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  185. package/dist/workers/server-runtime.worker.js +17 -17
  186. package/dist/workers/server-runtime.worker.js.map +1 -1
  187. package/docs/config.md +340 -0
  188. package/docs/publish-configuration-types.md +87 -0
  189. package/docs/pwa-configuration-types.md +55 -0
  190. package/docs/vitest-plugin.md +47 -0
  191. package/package.json +9 -7
  192. package/src/angular/client-transform-stylesheet.ts +1 -1
  193. package/src/angular/vite-angular-plugin.ts +89 -39
  194. package/src/angular/vite-postcss-inline-plugin.ts +1 -1
  195. package/src/capacitor/capacitor.ts +185 -38
  196. package/src/commands/build.ts +3 -10
  197. package/src/commands/check.ts +3 -3
  198. package/src/commands/dev.ts +3 -9
  199. package/src/commands/device.ts +5 -5
  200. package/src/commands/publish.ts +30 -26
  201. package/src/commands/replace-deps.ts +3 -3
  202. package/src/commands/typecheck.ts +7 -13
  203. package/src/commands/watch.ts +9 -9
  204. package/src/electron/electron.ts +49 -4
  205. package/src/engines/BaseEngine.ts +4 -1
  206. package/src/engines/NgtscEngine.ts +7 -7
  207. package/src/engines/ServerEsbuildEngine.ts +7 -7
  208. package/src/engines/TscEngine.ts +7 -7
  209. package/src/engines/ViteEngine.ts +18 -13
  210. package/src/engines/index.ts +11 -5
  211. package/src/engines/types.ts +20 -20
  212. package/src/infra/ResultCollector.ts +9 -9
  213. package/src/infra/SignalHandler.ts +7 -7
  214. package/src/infra/WorkerManager.ts +14 -14
  215. package/src/orchestrators/BuildOrchestrator.ts +42 -38
  216. package/src/orchestrators/DevWatchOrchestrator.ts +36 -36
  217. package/src/sd-cli-entry.ts +15 -8
  218. package/src/sd-cli.ts +16 -16
  219. package/src/sd-config.types.ts +107 -107
  220. package/src/utils/angular-compiler.ts +5 -5
  221. package/src/utils/build-env.ts +1 -1
  222. package/src/utils/concurrency.ts +7 -7
  223. package/src/utils/copy-public.ts +17 -17
  224. package/src/utils/copy-src.ts +11 -11
  225. package/src/utils/engine-stop.ts +9 -10
  226. package/src/utils/esbuild-config.ts +29 -29
  227. package/src/utils/lint-with-program.ts +34 -34
  228. package/src/utils/ngtsc-build-core.ts +17 -17
  229. package/src/utils/output-path-rewriter.ts +18 -18
  230. package/src/utils/output-utils.ts +11 -11
  231. package/src/utils/package-utils.ts +57 -45
  232. package/src/utils/replace-deps.ts +92 -67
  233. package/src/utils/sd-config.ts +3 -3
  234. package/src/utils/tsc-build.ts +18 -18
  235. package/src/utils/tsconfig.ts +11 -9
  236. package/src/utils/typecheck-non-package.ts +7 -8
  237. package/src/utils/typecheck-serialization.ts +13 -15
  238. package/src/utils/vite-config.ts +108 -46
  239. package/src/utils/vite-scope-watch-plugin.ts +6 -1
  240. package/src/utils/worker-events.ts +16 -16
  241. package/src/utils/worker-utils.ts +12 -13
  242. package/src/vitest-plugin.ts +5 -8
  243. package/src/workers/client.worker.ts +246 -7
  244. package/src/workers/library-build.worker.ts +8 -8
  245. package/src/workers/lint.worker.ts +2 -2
  246. package/src/workers/ngtsc-build.worker.ts +31 -31
  247. package/src/workers/server-build.worker.ts +60 -60
  248. package/src/workers/server-runtime.worker.ts +22 -22
  249. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +1 -0
  250. package/tests/angular/vite-angular-plugin-hmr.spec.ts +78 -0
  251. package/tests/angular/vite-angular-plugin.spec.ts +67 -0
  252. package/tests/capacitor/capacitor-build.spec.ts +93 -11
  253. package/tests/capacitor/capacitor-icon.spec.ts +7 -5
  254. package/tests/capacitor/capacitor-init.spec.ts +124 -10
  255. package/tests/capacitor/capacitor-run.spec.ts +14 -17
  256. package/tests/capacitor/capacitor-workspace.spec.ts +5 -3
  257. package/tests/commands/check.spec.ts +2 -2
  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/utils/vite-config.spec.ts +162 -90
  264. package/tests/workers/client-worker.spec.ts +690 -0
  265. package/tests/workers/server-build-worker.spec.ts +3 -3
@@ -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() };
@@ -221,8 +221,8 @@ describe("createClientViteConfig", () => {
221
221
 
222
222
  // --- legacyModule (Feature 1.1) ---
223
223
 
224
- // Acceptance: Scenario "legacyModule 활성화한다"
225
- it("enables inlineDynamicImports and import.meta plugin when legacyModule is true", async () => {
224
+ // Acceptance: Scenario "legacyModule 활성화 시 inlineDynamicImports만 설정한다"
225
+ it("enables inlineDynamicImports without import.meta plugin when legacyModule is true", async () => {
226
226
  const config = await createClientViteConfig({
227
227
  ...createDefaultOptions(),
228
228
  legacyModule: true,
@@ -230,26 +230,22 @@ describe("createClientViteConfig", () => {
230
230
 
231
231
  // inlineDynamicImports가 활성화된다
232
232
  expect((config.build as any)?.rollupOptions?.output?.inlineDynamicImports).toBe(true);
233
- // import.meta 치환 플러그인이 활성화된다
233
+ // import.meta 치환 플러그인이 없다 (esbuild target이 자동 치환)
234
234
  const plugins = config.plugins as Array<{ name: string }>;
235
235
  const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
236
- expect(legacyPlugin).toBeDefined();
236
+ expect(legacyPlugin).toBeUndefined();
237
237
  });
238
238
 
239
- // Acceptance: Scenario "legacyModule 설정하지 않는다"
240
- it("does not set inlineDynamicImports or import.meta plugin when legacyModule is not set", async () => {
239
+ // Acceptance: Scenario "legacyModule 미설정 시 코드 분할이 기본 동작한다"
240
+ it("does not set inlineDynamicImports when legacyModule is not set", async () => {
241
241
  const config = await createClientViteConfig(createDefaultOptions());
242
242
 
243
243
  // 코드 분할이 기본 동작한다
244
244
  expect(config.build?.rollupOptions).toBeUndefined();
245
- // import.meta가 그대로 유지된다
246
- const plugins = config.plugins as Array<{ name: string }>;
247
- const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
248
- expect(legacyPlugin).toBeUndefined();
249
245
  });
250
246
 
251
- // Acceptance: Scenario "기존 splitting 옵션을 legacyModule로 마이그레이션한다"
252
- it("legacyModule: true provides inlineDynamicImports plus import.meta plugin (splitting replacement)", async () => {
247
+ // Acceptance: Scenario "legacyModule: true는 inlineDynamicImports를 활성화한다"
248
+ it("legacyModule: true provides inlineDynamicImports (splitting replacement)", async () => {
253
249
  const config = await createClientViteConfig({
254
250
  ...createDefaultOptions(),
255
251
  legacyModule: true,
@@ -257,95 +253,31 @@ describe("createClientViteConfig", () => {
257
253
 
258
254
  // 기존 splitting: false와 동일한 inlineDynamicImports 동작
259
255
  expect((config.build as any)?.rollupOptions?.output?.inlineDynamicImports).toBe(true);
260
- // 추가로 import.meta 치환이 활성화된다
261
- const plugins = config.plugins as Array<{ name: string }>;
262
- const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
263
- expect(legacyPlugin).toBeDefined();
264
256
  });
265
257
 
266
- // Unit: enforce: "post" 설정 확인 (D4)
267
- it("sd-legacy-import-meta plugin has enforce: post", async () => {
268
- const config = await createClientViteConfig({
269
- ...createDefaultOptions(),
270
- legacyModule: true,
271
- });
272
-
273
- const plugins = config.plugins as Array<{ name: string; enforce?: string }>;
274
- const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
275
- expect(legacyPlugin?.enforce).toBe("post");
276
- });
258
+ // --- legacyModule esbuild.supported override (Feature 1.4) ---
277
259
 
278
- // Acceptance: Scenario "사용자 코드의 import.meta.url을 치환한다"
279
- it("sd-legacy-import-meta plugin replaces import.meta.url with module URL", async () => {
260
+ // Acceptance: Scenario "legacyModule: true일 때 esbuild.supported에 import-meta false 설정"
261
+ it("sets esbuild.supported to disable import-meta when legacyModule is true", async () => {
280
262
  const config = await createClientViteConfig({
281
263
  ...createDefaultOptions(),
282
264
  legacyModule: true,
283
265
  });
284
266
 
285
- const plugins = config.plugins as Array<{ name: string; enforce?: string; transform?: Function }>;
286
- const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
287
- expect(legacyPlugin).toBeDefined();
288
-
289
- const code = 'const url = import.meta.url;';
290
- const id = "/packages/my-client/src/app.ts";
291
- const result = legacyPlugin!.transform!(code, id);
292
-
293
- expect(result).toBeDefined();
294
- expect(result.code).toContain(JSON.stringify(id));
295
- expect(result.code).not.toContain("import.meta");
296
- });
297
-
298
- // Acceptance: Scenario "Vite가 주입한 import.meta.hot을 치환한다"
299
- it("sd-legacy-import-meta plugin replaces import.meta.hot injected by Vite", async () => {
300
- const config = await createClientViteConfig({
301
- ...createDefaultOptions(),
302
- legacyModule: true,
303
- });
304
-
305
- const plugins = config.plugins as Array<{ name: string; transform?: Function }>;
306
- const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
307
-
308
- const code = 'import.meta.hot = createHotContext("/src/app.ts");';
309
- const id = "/packages/my-client/src/app.ts";
310
- const result = legacyPlugin!.transform!(code, id);
311
-
312
- expect(result).toBeDefined();
313
- expect(result.code).not.toContain("import.meta");
314
- });
315
-
316
- // Acceptance: Scenario "/@vite/client의 import.meta를 치환한다"
317
- it("sd-legacy-import-meta plugin replaces import.meta in /@vite/client", async () => {
318
- const config = await createClientViteConfig({
319
- ...createDefaultOptions(),
320
- legacyModule: true,
321
- });
322
-
323
- const plugins = config.plugins as Array<{ name: string; transform?: Function }>;
324
- const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
325
-
326
- const code = 'const base = import.meta.url;';
327
- const id = "/@vite/client";
328
- const result = legacyPlugin!.transform!(code, id);
329
-
330
- expect(result).toBeDefined();
331
- expect(result.code).not.toContain("import.meta");
267
+ const esbuildOpts = config.esbuild as Record<string, unknown> | undefined;
268
+ expect(esbuildOpts?.["supported"]).toEqual(
269
+ expect.objectContaining({
270
+ "import-meta": false,
271
+ }),
272
+ );
332
273
  });
333
274
 
334
- // Acceptance: Scenario "import.meta가 없는 모듈은 변환하지 않는다"
335
- it("sd-legacy-import-meta plugin returns undefined for modules without import.meta", async () => {
336
- const config = await createClientViteConfig({
337
- ...createDefaultOptions(),
338
- legacyModule: true,
339
- });
340
-
341
- const plugins = config.plugins as Array<{ name: string; transform?: Function }>;
342
- const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
343
-
344
- const code = 'const x = 1 + 2;';
345
- const id = "/packages/my-client/src/utils.ts";
346
- const result = legacyPlugin!.transform!(code, id);
275
+ // Acceptance: Scenario "legacyModule 미설정 esbuild.supported 변경 없음"
276
+ it("does not set esbuild.supported when legacyModule is not specified", async () => {
277
+ const config = await createClientViteConfig(createDefaultOptions());
347
278
 
348
- expect(result).toBeUndefined();
279
+ const esbuildOpts = config.esbuild as Record<string, unknown> | undefined;
280
+ expect(esbuildOpts?.["supported"]).toBeUndefined();
349
281
  });
350
282
 
351
283
  // --- PWA (Feature 5.2) ---
@@ -505,6 +437,146 @@ describe("createClientViteConfig", () => {
505
437
  );
506
438
  });
507
439
 
440
+ // --- watch option (Feature 1.2: legacy dev mode) ---
441
+
442
+ // Acceptance: Scenario "watch: true 시 build.watch 설정 및 emptyOutDir: false"
443
+ it("sets build.watch and emptyOutDir: false when watch is true in build mode", async () => {
444
+ const config = await createClientViteConfig({
445
+ ...createDefaultOptions(),
446
+ mode: "build",
447
+ watch: true,
448
+ });
449
+
450
+ expect(config.build?.watch).toEqual({});
451
+ expect(config.build?.emptyOutDir).toBe(false);
452
+ expect(config.logLevel).toBeUndefined();
453
+ });
454
+
455
+ // Acceptance: Scenario "watch: true + replaceDeps 시 sdScopeWatchPlugin 포함"
456
+ it("includes sdScopeWatchPlugin when watch is true with replaceDeps in build mode", async () => {
457
+ const { sdScopeWatchPlugin } = await import("../../src/utils/vite-scope-watch-plugin");
458
+
459
+ const config = await createClientViteConfig({
460
+ ...createDefaultOptions(),
461
+ mode: "build",
462
+ watch: true,
463
+ replaceDeps: [{ packageName: "@scope/core", sourcePath: "/packages/core" }],
464
+ });
465
+
466
+ const plugins = config.plugins as Array<{ name: string }>;
467
+ const scopePlugin = plugins.find((p) => p.name === "sd-scope-watch-plugin");
468
+ expect(scopePlugin).toBeDefined();
469
+ expect(sdScopeWatchPlugin).toHaveBeenCalled();
470
+ });
471
+
472
+ // Acceptance: Scenario "watch 미설정 시 기존 build 동작 유지"
473
+ it("sets emptyOutDir: true and logLevel: silent when watch is not set in build mode", async () => {
474
+ const config = await createClientViteConfig(createDefaultOptions());
475
+
476
+ expect(config.build?.emptyOutDir).toBe(true);
477
+ expect(config.logLevel).toBe("silent");
478
+ expect(config.build?.watch).toBeUndefined();
479
+ });
480
+
481
+ // Unit: watch: true without replaceDeps does not add sdScopeWatchPlugin
482
+ it("does not add sdScopeWatchPlugin in watch mode without replaceDeps", async () => {
483
+ const config = await createClientViteConfig({
484
+ ...createDefaultOptions(),
485
+ mode: "build",
486
+ watch: true,
487
+ });
488
+
489
+ const plugins = config.plugins as Array<{ name: string }>;
490
+ const scopePlugin = plugins.find((p) => p.name === "sd-scope-watch-plugin");
491
+ expect(scopePlugin).toBeUndefined();
492
+ });
493
+
494
+ // Unit: watch: true still sets outDir
495
+ it("sets outDir in watch mode", async () => {
496
+ const config = await createClientViteConfig({
497
+ ...createDefaultOptions(),
498
+ mode: "build",
499
+ watch: true,
500
+ });
501
+
502
+ expect(config.build?.outDir).toContain("my-client");
503
+ expect(config.build?.outDir).toMatch(/dist$/);
504
+ });
505
+
506
+ // --- outDir override ---
507
+
508
+ // Acceptance: Scenario "outDir 설정 시 해당 경로로 빌드 출력"
509
+ it("uses custom outDir when provided", async () => {
510
+ const config = await createClientViteConfig({
511
+ ...createDefaultOptions(),
512
+ outDir: "/packages/my-client/.capacitor/www",
513
+ });
514
+
515
+ expect(config.build?.outDir).toBe("/packages/my-client/.capacitor/www");
516
+ });
517
+
518
+ // Acceptance: Scenario "outDir 미설정 시 pkgDir/dist 사용"
519
+ it("defaults outDir to pkgDir/dist when not provided", async () => {
520
+ const config = await createClientViteConfig(createDefaultOptions());
521
+
522
+ expect(config.build?.outDir).toMatch(/my-client[\\/]dist$/);
523
+ });
524
+
525
+ // --- exclude (Feature 1.1: vite-exclude-passthrough) ---
526
+
527
+ // Acceptance: Scenario "exclude에 패키지를 지정하면 pre-bundling에서 제외된다"
528
+ it("sets optimizeDeps.exclude when exclude is provided", async () => {
529
+ const config = await createClientViteConfig({
530
+ ...createDefaultOptions(),
531
+ mode: "dev",
532
+ exclude: ["jeep-sqlite"],
533
+ });
534
+
535
+ expect(config.optimizeDeps?.exclude).toEqual(["jeep-sqlite"]);
536
+ });
537
+
538
+ // Acceptance: Scenario "exclude 미설정 시 기존 동작과 동일하다"
539
+ it("does not set optimizeDeps.exclude when exclude is not provided", async () => {
540
+ const config = await createClientViteConfig({
541
+ ...createDefaultOptions(),
542
+ mode: "dev",
543
+ });
544
+
545
+ expect(config.optimizeDeps?.exclude).toBeUndefined();
546
+ });
547
+
548
+ // Acceptance: Scenario "exclude와 replaceDeps가 모두 있으면 둘 다 제외된다"
549
+ it("sets optimizeDeps.exclude from exclude while sdScopeWatchPlugin handles replaceDeps", async () => {
550
+ const { sdScopeWatchPlugin } = await import("../../src/utils/vite-scope-watch-plugin");
551
+
552
+ const config = await createClientViteConfig({
553
+ ...createDefaultOptions(),
554
+ mode: "dev",
555
+ exclude: ["jeep-sqlite"],
556
+ replaceDeps: [{ packageName: "@scope/core", sourcePath: "/packages/core" }],
557
+ });
558
+
559
+ // Base config에 exclude 설정
560
+ expect(config.optimizeDeps?.exclude).toEqual(["jeep-sqlite"]);
561
+ // sdScopeWatchPlugin도 호출됨 (replaceDeps용 exclude는 plugin이 처리)
562
+ expect(sdScopeWatchPlugin).toHaveBeenCalled();
563
+ });
564
+
565
+ // Acceptance: Scenario "exclude만 있고 replaceDeps가 없으면 exclude만 제외된다"
566
+ it("sets optimizeDeps.exclude from exclude when no replaceDeps", async () => {
567
+ const config = await createClientViteConfig({
568
+ ...createDefaultOptions(),
569
+ mode: "dev",
570
+ exclude: ["jeep-sqlite"],
571
+ });
572
+
573
+ expect(config.optimizeDeps?.exclude).toEqual(["jeep-sqlite"]);
574
+ // sdScopeWatchPlugin은 호출되지 않음
575
+ const plugins = config.plugins as Array<{ name: string }>;
576
+ const scopePlugin = plugins.find((p) => p.name === "sd-scope-watch-plugin");
577
+ expect(scopePlugin).toBeUndefined();
578
+ });
579
+
508
580
  // Acceptance: Scenario "pwa 필드 미설정 시 기본값"
509
581
  it("uses default manifest values from pkgName when pwa is undefined", async () => {
510
582
  await createClientViteConfig(createDefaultOptions());