@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
@@ -0,0 +1,690 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ //#region Mocks
4
+
5
+ let workerFns: Record<string, (...args: any[]) => any>;
6
+ let mockSend: ReturnType<typeof vi.fn>;
7
+
8
+ // vite mocks
9
+ const mockCreateServer = vi.fn();
10
+ const mockViteBuild = vi.fn();
11
+
12
+ vi.mock("vite", () => ({
13
+ createServer: (...args: any[]) => mockCreateServer(...args),
14
+ build: (...args: any[]) => mockViteBuild(...args),
15
+ }));
16
+
17
+ // createClientViteConfig mock
18
+ const mockCreateClientViteConfig = vi.fn();
19
+ vi.mock("../../src/utils/vite-config.js", () => ({
20
+ createClientViteConfig: (...args: any[]) => mockCreateClientViteConfig(...args),
21
+ }));
22
+
23
+ // vite-scope-watch-plugin mock
24
+ vi.mock("../../src/utils/vite-scope-watch-plugin.js", () => ({
25
+ sdScopeWatchPlugin: vi.fn(),
26
+ }));
27
+
28
+ // fs mock
29
+ const mockRmSync = vi.fn();
30
+ const mockMkdirSync = vi.fn();
31
+ const mockWriteFileSync = vi.fn();
32
+ const mockExistsSync = vi.fn().mockReturnValue(false);
33
+ const mockReadFileSync = vi.fn().mockReturnValue('{"name": "@scope/my-client"}');
34
+ const mockStatSync = vi.fn().mockReturnValue({ isDirectory: () => false });
35
+
36
+ vi.mock("node:fs", () => ({
37
+ default: {
38
+ rmSync: (...args: any[]) => mockRmSync(...args),
39
+ mkdirSync: (...args: any[]) => mockMkdirSync(...args),
40
+ writeFileSync: (...args: any[]) => mockWriteFileSync(...args),
41
+ existsSync: (...args: any[]) => mockExistsSync(...args),
42
+ readFileSync: (...args: any[]) => mockReadFileSync(...args),
43
+ statSync: (...args: any[]) => mockStatSync(...args),
44
+ },
45
+ rmSync: (...args: any[]) => mockRmSync(...args),
46
+ mkdirSync: (...args: any[]) => mockMkdirSync(...args),
47
+ writeFileSync: (...args: any[]) => mockWriteFileSync(...args),
48
+ existsSync: (...args: any[]) => mockExistsSync(...args),
49
+ readFileSync: (...args: any[]) => mockReadFileSync(...args),
50
+ statSync: (...args: any[]) => mockStatSync(...args),
51
+ }));
52
+
53
+ vi.mock("@simplysm/core-node", () => ({
54
+ createWorker: vi.fn((fns: Record<string, Function>) => {
55
+ workerFns = fns as any;
56
+ mockSend = vi.fn();
57
+ return { send: mockSend };
58
+ }),
59
+ }));
60
+
61
+ vi.mock("@simplysm/core-common", () => ({
62
+ err: { message: (e: any) => e?.message ?? String(e) },
63
+ }));
64
+
65
+ vi.mock("consola", () => ({
66
+ consola: {
67
+ withTag: vi.fn(() => ({
68
+ debug: vi.fn(),
69
+ warn: vi.fn(),
70
+ error: vi.fn(),
71
+ info: vi.fn(),
72
+ })),
73
+ },
74
+ }));
75
+
76
+ vi.mock("../../src/utils/worker-utils.js", () => ({
77
+ registerCleanupHandlers: vi.fn(),
78
+ applyDebugLevel: vi.fn(),
79
+ }));
80
+
81
+ //#endregion
82
+
83
+ // Dynamic import after mocking
84
+ await import("../../src/workers/client.worker");
85
+
86
+ //#region Helpers
87
+
88
+ function createBaseInfo() {
89
+ return {
90
+ name: "my-client",
91
+ cwd: "/workspace",
92
+ pkgDir: "/workspace/packages/my-client",
93
+ };
94
+ }
95
+
96
+ /** RollupWatcher mock 생성 */
97
+ function createMockWatcher() {
98
+ const handlers: Record<string, Function[] | undefined> = {};
99
+ return {
100
+ on: vi.fn((event: string, handler: Function) => {
101
+ handlers[event] ??= [];
102
+ handlers[event].push(handler);
103
+ }),
104
+ close: vi.fn().mockResolvedValue(undefined),
105
+ emit(event: string, data: unknown) {
106
+ for (const handler of handlers[event] ?? []) {
107
+ handler(data);
108
+ }
109
+ },
110
+ };
111
+ }
112
+
113
+ //#endregion
114
+
115
+ beforeEach(() => {
116
+ vi.clearAllMocks();
117
+ mockExistsSync.mockReturnValue(false);
118
+ mockReadFileSync.mockReturnValue('{"name": "@scope/my-client"}');
119
+ mockCreateClientViteConfig.mockResolvedValue({ plugins: [] });
120
+ });
121
+
122
+ describe("client.worker", () => {
123
+ describe("startWatch — legacy mode", () => {
124
+ // Acceptance: Scenario "legacyModule 활성화 + startWatch 호출 시 viteBuild watch 모드 실행"
125
+ it("calls viteBuild instead of createServer when legacyModule is true", async () => {
126
+ const mockWatcher = createMockWatcher();
127
+ mockViteBuild.mockImplementation(() => {
128
+ // Simulate immediate END event (first build complete)
129
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
130
+ return Promise.resolve(mockWatcher);
131
+ });
132
+
133
+ const result = await workerFns["startWatch"]({
134
+ ...createBaseInfo(),
135
+ browserSupport: { legacyModule: true },
136
+ });
137
+
138
+ expect(mockViteBuild).toHaveBeenCalled();
139
+ expect(mockCreateServer).not.toHaveBeenCalled();
140
+ expect(result.success).toBe(true);
141
+ });
142
+
143
+ // Acceptance: Scenario "legacyModule 미설정 + startWatch 호출 시 기존 dev server 실행"
144
+ it("calls createServer when legacyModule is not set", async () => {
145
+ const mockServer = {
146
+ listen: vi.fn().mockResolvedValue(undefined),
147
+ close: vi.fn().mockResolvedValue(undefined),
148
+ httpServer: { address: () => ({ port: 4200 }) },
149
+ };
150
+ mockCreateServer.mockResolvedValue(mockServer);
151
+
152
+ await workerFns["startWatch"]({
153
+ ...createBaseInfo(),
154
+ port: 4200,
155
+ });
156
+
157
+ expect(mockCreateServer).toHaveBeenCalled();
158
+ expect(mockViteBuild).not.toHaveBeenCalled();
159
+
160
+ // cleanup module-level viteServer
161
+ await workerFns["stopWatch"]();
162
+ });
163
+
164
+ // Acceptance: Scenario "createClientViteConfig에 onBuildStart/onBuild 전달"
165
+ it("sends buildStart and build events via onBuildStart/onBuild callbacks", async () => {
166
+ const mockWatcher = createMockWatcher();
167
+ mockViteBuild.mockImplementation(() => {
168
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
169
+ return Promise.resolve(mockWatcher);
170
+ });
171
+
172
+ await workerFns["startWatch"]({
173
+ ...createBaseInfo(),
174
+ browserSupport: { legacyModule: true },
175
+ });
176
+
177
+ // Verify createClientViteConfig was called with callbacks
178
+ const configCall = mockCreateClientViteConfig.mock.calls[0][0];
179
+ expect(configCall.onBuildStart).toBeTypeOf("function");
180
+ expect(configCall.onBuild).toBeTypeOf("function");
181
+
182
+ // Simulate callbacks firing (as sdAngularPlugin would)
183
+ configCall.onBuildStart();
184
+ expect(mockSend).toHaveBeenCalledWith("buildStart", {});
185
+
186
+ const buildResult = { success: true, errors: [] };
187
+ configCall.onBuild(buildResult);
188
+ expect(mockSend).toHaveBeenCalledWith("build", buildResult);
189
+ });
190
+
191
+ // Acceptance: Scenario "watcher ERROR 이벤트 수신 시 error 발행"
192
+ it("sends error event and resolves with failure on watcher ERROR", async () => {
193
+ const mockWatcher = createMockWatcher();
194
+ mockViteBuild.mockImplementation(() => {
195
+ setTimeout(
196
+ () =>
197
+ mockWatcher.emit("event", {
198
+ code: "ERROR",
199
+ error: { message: "Build failed" },
200
+ }),
201
+ 0,
202
+ );
203
+ return Promise.resolve(mockWatcher);
204
+ });
205
+
206
+ const result = await workerFns["startWatch"]({
207
+ ...createBaseInfo(),
208
+ browserSupport: { legacyModule: true },
209
+ });
210
+
211
+ expect(result.success).toBe(false);
212
+ expect(result.errors).toContain("Build failed");
213
+ expect(mockSend).toHaveBeenCalledWith("error", { message: "Build failed" });
214
+ });
215
+
216
+ // Acceptance: Scenario "config에 watch: true, pwa: false 전달"
217
+ it("passes watch: true and pwa: false to createClientViteConfig", async () => {
218
+ const mockWatcher = createMockWatcher();
219
+ mockViteBuild.mockImplementation(() => {
220
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
221
+ return Promise.resolve(mockWatcher);
222
+ });
223
+
224
+ await workerFns["startWatch"]({
225
+ ...createBaseInfo(),
226
+ browserSupport: { legacyModule: true },
227
+ });
228
+
229
+ expect(mockCreateClientViteConfig).toHaveBeenCalledWith(
230
+ expect.objectContaining({
231
+ mode: "build",
232
+ watch: true,
233
+ pwa: false,
234
+ }),
235
+ );
236
+ });
237
+
238
+ // Acceptance: Scenario "첫 빌드 시 dist 디렉토리를 비운다"
239
+ it("clears dist directory before starting legacy watch", async () => {
240
+ const mockWatcher = createMockWatcher();
241
+ mockViteBuild.mockImplementation(() => {
242
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
243
+ return Promise.resolve(mockWatcher);
244
+ });
245
+
246
+ await workerFns["startWatch"]({
247
+ ...createBaseInfo(),
248
+ browserSupport: { legacyModule: true },
249
+ });
250
+
251
+ expect(mockRmSync).toHaveBeenCalledWith(
252
+ expect.stringContaining("dist"),
253
+ expect.objectContaining({ recursive: true, force: true }),
254
+ );
255
+ });
256
+ });
257
+
258
+ describe("stopWatch — legacy mode", () => {
259
+ // Acceptance: Scenario "stopWatch 호출 시 watcher를 닫는다"
260
+ it("closes RollupWatcher on stopWatch", async () => {
261
+ const mockWatcher = createMockWatcher();
262
+ mockViteBuild.mockImplementation(() => {
263
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
264
+ return Promise.resolve(mockWatcher);
265
+ });
266
+
267
+ await workerFns["startWatch"]({
268
+ ...createBaseInfo(),
269
+ browserSupport: { legacyModule: true },
270
+ });
271
+
272
+ await workerFns["stopWatch"]();
273
+
274
+ expect(mockWatcher.close).toHaveBeenCalled();
275
+ });
276
+
277
+ // Acceptance: Scenario "watcher 없는 상태에서 stopWatch 호출 시 안전하게 무시한다"
278
+ it("handles stopWatch without prior startWatch", async () => {
279
+ await expect(workerFns["stopWatch"]()).resolves.toBeUndefined();
280
+ });
281
+ });
282
+
283
+ describe("startWatch — legacy HTTP server (Feature 1.3)", () => {
284
+ // Acceptance: Scenario "HTTP 서버로 index.html 접속"
285
+ it("serves dist/index.html on /{name}/ request", async () => {
286
+ const mockWatcher = createMockWatcher();
287
+ mockViteBuild.mockImplementation(() => {
288
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
289
+ return Promise.resolve(mockWatcher);
290
+ });
291
+
292
+ // dist/index.html을 읽을 수 있도록 fs mock 설정
293
+ mockExistsSync.mockImplementation((p: string) => {
294
+ if (typeof p === "string" && p.endsWith("index.html")) return true;
295
+ if (typeof p === "string" && p.endsWith("polyfills.ts")) return false;
296
+ return false;
297
+ });
298
+ const htmlContent = "<html><body><h1>Hello</h1></body></html>";
299
+ mockReadFileSync.mockImplementation((p: string) => {
300
+ if (typeof p === "string" && p.endsWith("package.json")) return '{"name": "@scope/my-client"}';
301
+ if (typeof p === "string" && p.endsWith("index.html")) return htmlContent;
302
+ return "";
303
+ });
304
+
305
+ const result = await workerFns["startWatch"]({
306
+ ...createBaseInfo(),
307
+ browserSupport: { legacyModule: true },
308
+ port: 0,
309
+ });
310
+
311
+ expect(result.success).toBe(true);
312
+
313
+ // serverReady 이벤트가 port와 함께 발행되어야 함
314
+ expect(mockSend).toHaveBeenCalledWith("serverReady", expect.objectContaining({ port: expect.any(Number) }));
315
+
316
+ // 실제 HTTP 요청으로 검증
317
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
318
+ expect(port).toBeTypeOf("number");
319
+
320
+ const res = await fetch(`http://127.0.0.1:${port}/my-client/`);
321
+ expect(res.status).toBe(200);
322
+ const body = await res.text();
323
+ expect(body).toContain("<h1>Hello</h1>");
324
+
325
+ // cleanup
326
+ await workerFns["stopWatch"]();
327
+ });
328
+
329
+ // Acceptance: Scenario "정적 파일(JS, CSS, 이미지) 요청"
330
+ it("serves static files with correct Content-Type", async () => {
331
+ const mockWatcher = createMockWatcher();
332
+ mockViteBuild.mockImplementation(() => {
333
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
334
+ return Promise.resolve(mockWatcher);
335
+ });
336
+
337
+ const jsContent = "console.log('hello');";
338
+ mockExistsSync.mockImplementation((p: string) => {
339
+ if (typeof p === "string" && p.endsWith("main.js")) return true;
340
+ if (typeof p === "string" && p.endsWith("polyfills.ts")) return false;
341
+ return false;
342
+ });
343
+ mockReadFileSync.mockImplementation((p: string) => {
344
+ if (typeof p === "string" && p.endsWith("package.json")) return '{"name": "@scope/my-client"}';
345
+ if (typeof p === "string" && p.endsWith("main.js")) return jsContent;
346
+ return "";
347
+ });
348
+
349
+ await workerFns["startWatch"]({
350
+ ...createBaseInfo(),
351
+ browserSupport: { legacyModule: true },
352
+ port: 0,
353
+ });
354
+
355
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
356
+
357
+ const res = await fetch(`http://127.0.0.1:${port}/my-client/main.js`);
358
+ expect(res.status).toBe(200);
359
+ expect(res.headers.get("content-type")).toContain("text/javascript");
360
+ const body = await res.text();
361
+ expect(body).toBe(jsContent);
362
+
363
+ await workerFns["stopWatch"]();
364
+ });
365
+
366
+ // Acceptance: Scenario "존재하지 않는 파일 요청 시 SPA fallback"
367
+ it("falls back to index.html for non-existent paths", async () => {
368
+ const mockWatcher = createMockWatcher();
369
+ mockViteBuild.mockImplementation(() => {
370
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
371
+ return Promise.resolve(mockWatcher);
372
+ });
373
+
374
+ const htmlContent = "<html><body>SPA</body></html>";
375
+ mockExistsSync.mockImplementation((p: string) => {
376
+ if (typeof p === "string" && p.endsWith("index.html")) return true;
377
+ if (typeof p === "string" && p.endsWith("polyfills.ts")) return false;
378
+ return false;
379
+ });
380
+ mockReadFileSync.mockImplementation((p: string) => {
381
+ if (typeof p === "string" && p.endsWith("package.json")) return '{"name": "@scope/my-client"}';
382
+ if (typeof p === "string" && p.endsWith("index.html")) return htmlContent;
383
+ return "";
384
+ });
385
+
386
+ await workerFns["startWatch"]({
387
+ ...createBaseInfo(),
388
+ browserSupport: { legacyModule: true },
389
+ port: 0,
390
+ });
391
+
392
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
393
+
394
+ const res = await fetch(`http://127.0.0.1:${port}/my-client/some/route`);
395
+ expect(res.status).toBe(200);
396
+ const body = await res.text();
397
+ expect(body).toContain("SPA");
398
+
399
+ await workerFns["stopWatch"]();
400
+ });
401
+
402
+ // Acceptance: Scenario "HTTP 서버 listen 완료 시 serverReady 이벤트 발행"
403
+ it("emits serverReady event with port after HTTP server listen", async () => {
404
+ const mockWatcher = createMockWatcher();
405
+ mockViteBuild.mockImplementation(() => {
406
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
407
+ return Promise.resolve(mockWatcher);
408
+ });
409
+
410
+ mockExistsSync.mockReturnValue(false);
411
+
412
+ await workerFns["startWatch"]({
413
+ ...createBaseInfo(),
414
+ browserSupport: { legacyModule: true },
415
+ port: 0,
416
+ });
417
+
418
+ const serverReadyCalls = mockSend.mock.calls.filter((c: any[]) => c[0] === "serverReady");
419
+ expect(serverReadyCalls.length).toBe(1);
420
+ expect(serverReadyCalls[0][1].port).toBeTypeOf("number");
421
+ expect(serverReadyCalls[0][1].port).toBeGreaterThan(0);
422
+
423
+ await workerFns["stopWatch"]();
424
+ });
425
+
426
+ // Acceptance: Scenario "stopWatch 호출 시 HTTP 서버와 RollupWatcher 모두 정리"
427
+ it("closes both HTTP server and RollupWatcher on stopWatch", async () => {
428
+ const mockWatcher = createMockWatcher();
429
+ mockViteBuild.mockImplementation(() => {
430
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
431
+ return Promise.resolve(mockWatcher);
432
+ });
433
+
434
+ mockExistsSync.mockReturnValue(false);
435
+
436
+ await workerFns["startWatch"]({
437
+ ...createBaseInfo(),
438
+ browserSupport: { legacyModule: true },
439
+ port: 0,
440
+ });
441
+
442
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
443
+
444
+ await workerFns["stopWatch"]();
445
+
446
+ expect(mockWatcher.close).toHaveBeenCalled();
447
+
448
+ // HTTP 서버가 닫혔으므로 연결 불가해야 함
449
+ await expect(fetch(`http://127.0.0.1:${port}/my-client/`)).rejects.toThrow();
450
+ });
451
+ });
452
+
453
+ describe("startWatch — legacy HTTP server MIME type (mime library)", () => {
454
+ // Acceptance: Scenario ".wasm 파일이 올바른 MIME 타입으로 서빙된다"
455
+ it("serves .wasm files with application/wasm Content-Type", async () => {
456
+ const mockWatcher = createMockWatcher();
457
+ mockViteBuild.mockImplementation(() => {
458
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
459
+ return Promise.resolve(mockWatcher);
460
+ });
461
+
462
+ const wasmContent = new Uint8Array([0, 97, 115, 109]); // minimal wasm magic bytes
463
+ mockExistsSync.mockImplementation((p: string) => {
464
+ if (typeof p === "string" && p.endsWith("module.wasm")) return true;
465
+ if (typeof p === "string" && p.endsWith("polyfills.ts")) return false;
466
+ return false;
467
+ });
468
+ mockReadFileSync.mockImplementation((p: string) => {
469
+ if (typeof p === "string" && p.endsWith("package.json")) return '{"name": "@scope/my-client"}';
470
+ if (typeof p === "string" && p.endsWith("module.wasm")) return wasmContent;
471
+ return "";
472
+ });
473
+
474
+ await workerFns["startWatch"]({
475
+ ...createBaseInfo(),
476
+ browserSupport: { legacyModule: true },
477
+ port: 0,
478
+ });
479
+
480
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
481
+
482
+ const res = await fetch(`http://127.0.0.1:${port}/my-client/module.wasm`);
483
+ expect(res.status).toBe(200);
484
+ expect(res.headers.get("content-type")).toBe("application/wasm");
485
+
486
+ await workerFns["stopWatch"]();
487
+ });
488
+
489
+ // Acceptance: Scenario "알 수 없는 확장자는 octet-stream으로 fallback한다"
490
+ it("falls back to application/octet-stream for unknown extensions", async () => {
491
+ const mockWatcher = createMockWatcher();
492
+ mockViteBuild.mockImplementation(() => {
493
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
494
+ return Promise.resolve(mockWatcher);
495
+ });
496
+
497
+ mockExistsSync.mockImplementation((p: string) => {
498
+ if (typeof p === "string" && p.endsWith("data.xyz123")) return true;
499
+ if (typeof p === "string" && p.endsWith("polyfills.ts")) return false;
500
+ return false;
501
+ });
502
+ mockReadFileSync.mockImplementation((p: string) => {
503
+ if (typeof p === "string" && p.endsWith("package.json")) return '{"name": "@scope/my-client"}';
504
+ if (typeof p === "string" && p.endsWith("data.xyz123")) return "binary data";
505
+ return "";
506
+ });
507
+
508
+ await workerFns["startWatch"]({
509
+ ...createBaseInfo(),
510
+ browserSupport: { legacyModule: true },
511
+ port: 0,
512
+ });
513
+
514
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
515
+
516
+ const res = await fetch(`http://127.0.0.1:${port}/my-client/data.xyz123`);
517
+ expect(res.status).toBe(200);
518
+ expect(res.headers.get("content-type")).toBe("application/octet-stream");
519
+
520
+ await workerFns["stopWatch"]();
521
+ });
522
+ });
523
+
524
+ describe("startWatch — legacy live reload (Feature 1.3)", () => {
525
+ // Acceptance: Scenario "index.html 서빙 시 reload 스크립트 자동 삽입"
526
+ it("injects live reload script into HTML responses", async () => {
527
+ const mockWatcher = createMockWatcher();
528
+ mockViteBuild.mockImplementation(() => {
529
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
530
+ return Promise.resolve(mockWatcher);
531
+ });
532
+
533
+ const htmlContent = "<html><body><h1>App</h1></body></html>";
534
+ mockExistsSync.mockImplementation((p: string) => {
535
+ if (typeof p === "string" && p.endsWith("index.html")) return true;
536
+ if (typeof p === "string" && p.endsWith("polyfills.ts")) return false;
537
+ return false;
538
+ });
539
+ mockReadFileSync.mockImplementation((p: string) => {
540
+ if (typeof p === "string" && p.endsWith("package.json")) return '{"name": "@scope/my-client"}';
541
+ if (typeof p === "string" && p.endsWith("index.html")) return htmlContent;
542
+ return "";
543
+ });
544
+
545
+ await workerFns["startWatch"]({
546
+ ...createBaseInfo(),
547
+ browserSupport: { legacyModule: true },
548
+ port: 0,
549
+ });
550
+
551
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
552
+
553
+ const res = await fetch(`http://127.0.0.1:${port}/my-client/`);
554
+ const body = await res.text();
555
+
556
+ // live reload 스크립트가 주입되어야 함
557
+ expect(body).toContain("__live-reload");
558
+ expect(body).toContain("<script>");
559
+ // 원본 HTML 내용도 유지되어야 함
560
+ expect(body).toContain("<h1>App</h1>");
561
+ // 스크립트가 </body> 직전에 삽입되어야 함
562
+ expect(body).toMatch(/<script>[\s\S]*<\/script>\s*<\/body>/);
563
+
564
+ await workerFns["stopWatch"]();
565
+ });
566
+
567
+ // Acceptance: Scenario "비-HTML 파일 서빙 시 스크립트 미주입"
568
+ it("does not inject script into non-HTML files", async () => {
569
+ const mockWatcher = createMockWatcher();
570
+ mockViteBuild.mockImplementation(() => {
571
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
572
+ return Promise.resolve(mockWatcher);
573
+ });
574
+
575
+ const jsContent = "console.log('hello');";
576
+ mockExistsSync.mockImplementation((p: string) => {
577
+ if (typeof p === "string" && p.endsWith("main.js")) return true;
578
+ if (typeof p === "string" && p.endsWith("polyfills.ts")) return false;
579
+ return false;
580
+ });
581
+ mockReadFileSync.mockImplementation((p: string) => {
582
+ if (typeof p === "string" && p.endsWith("package.json")) return '{"name": "@scope/my-client"}';
583
+ if (typeof p === "string" && p.endsWith("main.js")) return jsContent;
584
+ return "";
585
+ });
586
+
587
+ await workerFns["startWatch"]({
588
+ ...createBaseInfo(),
589
+ browserSupport: { legacyModule: true },
590
+ port: 0,
591
+ });
592
+
593
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
594
+
595
+ const res = await fetch(`http://127.0.0.1:${port}/my-client/main.js`);
596
+ const body = await res.text();
597
+ expect(body).toBe(jsContent);
598
+ expect(body).not.toContain("<script>");
599
+
600
+ await workerFns["stopWatch"]();
601
+ });
602
+
603
+ // Acceptance: Scenario "RollupWatcher END 이벤트 시 연결된 브라우저가 reload"
604
+ it("sends reload signal via SSE on RollupWatcher END event", async () => {
605
+ const http = await import("node:http");
606
+
607
+ const mockWatcher = createMockWatcher();
608
+ mockViteBuild.mockImplementation(() => {
609
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
610
+ return Promise.resolve(mockWatcher);
611
+ });
612
+
613
+ mockExistsSync.mockReturnValue(false);
614
+
615
+ await workerFns["startWatch"]({
616
+ ...createBaseInfo(),
617
+ browserSupport: { legacyModule: true },
618
+ port: 0,
619
+ });
620
+
621
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
622
+
623
+ // SSE 연결 수립 (node:http 사용)
624
+ const sseData = await new Promise<string>((resolve) => {
625
+ const req = http.get(`http://127.0.0.1:${port}/my-client/__live-reload`, (res) => {
626
+ expect(res.headers["content-type"]).toContain("text/event-stream");
627
+
628
+ res.on("data", (chunk: Buffer) => {
629
+ const text = chunk.toString();
630
+ if (text.includes("reload")) {
631
+ resolve(text);
632
+ res.destroy();
633
+ }
634
+ });
635
+ });
636
+ req.on("error", () => { /* SSE 연결 종료 시 무시 */ });
637
+
638
+ // SSE 연결이 수립된 후 재빌드 END 이벤트 발행 (비동기 지연)
639
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 100);
640
+ });
641
+
642
+ expect(sseData).toContain("data: reload");
643
+
644
+ await workerFns["stopWatch"]();
645
+ });
646
+
647
+ // Acceptance: Scenario "RollupWatcher ERROR 이벤트 시 reload 미전송"
648
+ it("does not send reload signal on RollupWatcher ERROR event", async () => {
649
+ const http = await import("node:http");
650
+
651
+ const mockWatcher = createMockWatcher();
652
+ mockViteBuild.mockImplementation(() => {
653
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
654
+ return Promise.resolve(mockWatcher);
655
+ });
656
+
657
+ mockExistsSync.mockReturnValue(false);
658
+
659
+ await workerFns["startWatch"]({
660
+ ...createBaseInfo(),
661
+ browserSupport: { legacyModule: true },
662
+ port: 0,
663
+ });
664
+
665
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
666
+
667
+ // SSE 연결 수립 (node:http 사용)
668
+ const receivedData: string[] = [];
669
+ const req = http.get(`http://127.0.0.1:${port}/my-client/__live-reload`, (res) => {
670
+ res.on("data", (chunk: Buffer) => {
671
+ receivedData.push(chunk.toString());
672
+ });
673
+
674
+ // SSE 연결 후 ERROR 이벤트 발생 (reload 미전송이어야 함)
675
+ mockWatcher.emit("event", { code: "ERROR", error: { message: "test error" } });
676
+ });
677
+ req.on("error", () => { /* SSE 연결 종료 시 무시 */ });
678
+
679
+ // 잠시 대기하여 데이터 수신 확인
680
+ await new Promise((resolve) => setTimeout(resolve, 100));
681
+ req.destroy();
682
+
683
+ // reload 데이터가 없어야 함
684
+ const allData = receivedData.join("");
685
+ expect(allData).not.toContain("reload");
686
+
687
+ await workerFns["stopWatch"]();
688
+ });
689
+ });
690
+ });