@simplysm/sd-cli 14.0.16 → 14.0.17

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 (202) hide show
  1. package/README.md +2 -1
  2. package/dist/angular/client-transform-stylesheet.d.ts +2 -0
  3. package/dist/angular/client-transform-stylesheet.d.ts.map +1 -1
  4. package/dist/angular/client-transform-stylesheet.js +88 -2
  5. package/dist/angular/client-transform-stylesheet.js.map +1 -1
  6. package/dist/angular/vite-angular-plugin.d.ts +7 -0
  7. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  8. package/dist/angular/vite-angular-plugin.js +78 -16
  9. package/dist/angular/vite-angular-plugin.js.map +1 -1
  10. package/dist/capacitor/capacitor.d.ts.map +1 -1
  11. package/dist/capacitor/capacitor.js +9 -13
  12. package/dist/capacitor/capacitor.js.map +1 -1
  13. package/dist/commands/check.d.ts.map +1 -1
  14. package/dist/commands/check.js +8 -9
  15. package/dist/commands/check.js.map +1 -1
  16. package/dist/commands/device.d.ts.map +1 -1
  17. package/dist/commands/device.js +33 -1
  18. package/dist/commands/device.js.map +1 -1
  19. package/dist/commands/lint.d.ts +0 -1
  20. package/dist/commands/lint.d.ts.map +1 -1
  21. package/dist/commands/lint.js +2 -3
  22. package/dist/commands/lint.js.map +1 -1
  23. package/dist/commands/publish.js +2 -2
  24. package/dist/commands/publish.js.map +1 -1
  25. package/dist/commands/typecheck.d.ts.map +1 -1
  26. package/dist/commands/typecheck.js +0 -1
  27. package/dist/commands/typecheck.js.map +1 -1
  28. package/dist/electron/electron.d.ts +3 -2
  29. package/dist/electron/electron.d.ts.map +1 -1
  30. package/dist/electron/electron.js +54 -31
  31. package/dist/electron/electron.js.map +1 -1
  32. package/dist/engines/BaseEngine.js +1 -1
  33. package/dist/engines/BaseEngine.js.map +1 -1
  34. package/dist/engines/NgtscEngine.d.ts.map +1 -1
  35. package/dist/engines/NgtscEngine.js +0 -1
  36. package/dist/engines/NgtscEngine.js.map +1 -1
  37. package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
  38. package/dist/engines/ServerEsbuildEngine.js +0 -1
  39. package/dist/engines/ServerEsbuildEngine.js.map +1 -1
  40. package/dist/engines/TscEngine.d.ts.map +1 -1
  41. package/dist/engines/TscEngine.js +0 -1
  42. package/dist/engines/TscEngine.js.map +1 -1
  43. package/dist/engines/ViteEngine.d.ts.map +1 -1
  44. package/dist/engines/ViteEngine.js +8 -1
  45. package/dist/engines/ViteEngine.js.map +1 -1
  46. package/dist/engines/index.d.ts +0 -10
  47. package/dist/engines/index.d.ts.map +1 -1
  48. package/dist/engines/index.js +0 -5
  49. package/dist/engines/index.js.map +1 -1
  50. package/dist/engines/types.d.ts +0 -1
  51. package/dist/engines/types.d.ts.map +1 -1
  52. package/dist/infra/SignalHandler.d.ts +1 -6
  53. package/dist/infra/SignalHandler.d.ts.map +1 -1
  54. package/dist/infra/SignalHandler.js +4 -13
  55. package/dist/infra/SignalHandler.js.map +1 -1
  56. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  57. package/dist/orchestrators/BuildOrchestrator.js +7 -12
  58. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  59. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  60. package/dist/orchestrators/DevWatchOrchestrator.js +17 -10
  61. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  62. package/dist/sd-cli-entry.d.ts +0 -1
  63. package/dist/sd-cli-entry.d.ts.map +1 -1
  64. package/dist/sd-cli-entry.js +7 -8
  65. package/dist/sd-cli-entry.js.map +1 -1
  66. package/dist/sd-config.types.d.ts +11 -1
  67. package/dist/sd-config.types.d.ts.map +1 -1
  68. package/dist/utils/angular-compiler.d.ts.map +1 -1
  69. package/dist/utils/angular-compiler.js +20 -13
  70. package/dist/utils/angular-compiler.js.map +1 -1
  71. package/dist/utils/esbuild-config.d.ts +1 -1
  72. package/dist/utils/esbuild-config.d.ts.map +1 -1
  73. package/dist/utils/esbuild-config.js +1 -4
  74. package/dist/utils/esbuild-config.js.map +1 -1
  75. package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
  76. package/dist/utils/ngtsc-build-core.js +3 -0
  77. package/dist/utils/ngtsc-build-core.js.map +1 -1
  78. package/dist/utils/tsc-build.d.ts +5 -0
  79. package/dist/utils/tsc-build.d.ts.map +1 -1
  80. package/dist/utils/tsc-build.js +2 -1
  81. package/dist/utils/tsc-build.js.map +1 -1
  82. package/dist/utils/vite-config.d.ts +1 -1
  83. package/dist/utils/vite-config.d.ts.map +1 -1
  84. package/dist/utils/vite-config.js +22 -53
  85. package/dist/utils/vite-config.js.map +1 -1
  86. package/dist/utils/vite-pwa-plugin.d.ts +9 -0
  87. package/dist/utils/vite-pwa-plugin.d.ts.map +1 -0
  88. package/dist/utils/vite-pwa-plugin.js +139 -0
  89. package/dist/utils/vite-pwa-plugin.js.map +1 -0
  90. package/dist/utils/worker-utils.d.ts +2 -5
  91. package/dist/utils/worker-utils.d.ts.map +1 -1
  92. package/dist/utils/worker-utils.js +5 -11
  93. package/dist/utils/worker-utils.js.map +1 -1
  94. package/dist/workers/client.worker.d.ts.map +1 -1
  95. package/dist/workers/client.worker.js +9 -3
  96. package/dist/workers/client.worker.js.map +1 -1
  97. package/dist/workers/library-build.worker.d.ts.map +1 -1
  98. package/dist/workers/library-build.worker.js +6 -2
  99. package/dist/workers/library-build.worker.js.map +1 -1
  100. package/dist/workers/ngtsc-build.worker.js +2 -2
  101. package/dist/workers/ngtsc-build.worker.js.map +1 -1
  102. package/dist/workers/server-build.worker.d.ts.map +1 -1
  103. package/dist/workers/server-build.worker.js +6 -2
  104. package/dist/workers/server-build.worker.js.map +1 -1
  105. package/dist/workers/server-runtime.worker.js +4 -4
  106. package/dist/workers/server-runtime.worker.js.map +1 -1
  107. package/docs/config.md +26 -0
  108. package/docs/pwa-configuration-types.md +1 -1
  109. package/package.json +8 -10
  110. package/src/angular/client-transform-stylesheet.ts +104 -2
  111. package/src/angular/vite-angular-plugin.ts +92 -31
  112. package/src/capacitor/capacitor.ts +10 -26
  113. package/src/commands/check.ts +8 -11
  114. package/src/commands/device.ts +38 -3
  115. package/src/commands/lint.ts +2 -3
  116. package/src/commands/publish.ts +2 -2
  117. package/src/commands/typecheck.ts +0 -1
  118. package/src/electron/electron.ts +62 -43
  119. package/src/engines/BaseEngine.ts +1 -1
  120. package/src/engines/NgtscEngine.ts +0 -1
  121. package/src/engines/ServerEsbuildEngine.ts +0 -1
  122. package/src/engines/TscEngine.ts +0 -1
  123. package/src/engines/ViteEngine.ts +7 -1
  124. package/src/engines/index.ts +0 -10
  125. package/src/engines/types.ts +0 -1
  126. package/src/infra/SignalHandler.ts +4 -14
  127. package/src/orchestrators/BuildOrchestrator.ts +7 -9
  128. package/src/orchestrators/DevWatchOrchestrator.ts +21 -9
  129. package/src/sd-cli-entry.ts +11 -16
  130. package/src/sd-config.types.ts +12 -1
  131. package/src/utils/angular-compiler.ts +21 -21
  132. package/src/utils/esbuild-config.ts +2 -5
  133. package/src/utils/ngtsc-build-core.ts +7 -0
  134. package/src/utils/tsc-build.ts +7 -0
  135. package/src/utils/vite-config.ts +23 -55
  136. package/src/utils/vite-pwa-plugin.ts +168 -0
  137. package/src/utils/worker-utils.ts +5 -11
  138. package/src/workers/client.worker.ts +11 -3
  139. package/src/workers/library-build.worker.ts +6 -2
  140. package/src/workers/ngtsc-build.worker.ts +2 -2
  141. package/src/workers/server-build.worker.ts +7 -2
  142. package/src/workers/server-runtime.worker.ts +4 -4
  143. package/tests/angular/client-transform-stylesheet.spec.ts +43 -0
  144. package/tests/angular/find-affected-by-scss.spec.ts +37 -0
  145. package/tests/angular/fixtures/basic-app/scss/_colors.scss +1 -0
  146. package/tests/angular/fixtures/basic-app/scss/_variables.scss +3 -0
  147. package/tests/angular/fixtures/basic-app/src/styled.component.ts +14 -0
  148. package/tests/angular/linker-disk-cache.spec.ts +158 -0
  149. package/tests/angular/scss-disk-cache.spec.ts +162 -0
  150. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
  151. package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
  152. package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
  153. package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +87 -0
  154. package/tests/angular/vite-angular-plugin.spec.ts +15 -15
  155. package/tests/capacitor/capacitor-icon.spec.ts +2 -4
  156. package/tests/capacitor/capacitor-init.spec.ts +2 -4
  157. package/tests/capacitor/capacitor-workspace.spec.ts +2 -4
  158. package/tests/commands/device.spec.ts +100 -0
  159. package/tests/electron/electron.spec.ts +24 -17
  160. package/tests/engines/ngtsc-engine.spec.ts +0 -3
  161. package/tests/engines/server-esbuild-engine.spec.ts +0 -3
  162. package/tests/engines/tsc-engine.spec.ts +1 -2
  163. package/tests/engines/vite-engine.spec.ts +0 -2
  164. package/tests/infra/signal-handler.spec.ts +1 -12
  165. package/tests/orchestrators/build-orchestrator.spec.ts +0 -6
  166. package/tests/orchestrators/dev-watch-orchestrator.spec.ts +24 -66
  167. package/tests/utils/angular-compiler.spec.ts +1396 -32
  168. package/tests/utils/esbuild-config.spec.ts +4 -7
  169. package/tests/utils/{ngtsc-build-core-angular-compiler.spec.ts → ngtsc-build-core.spec.ts} +142 -11
  170. package/tests/utils/tsc-build.spec.ts +4 -1
  171. package/tests/utils/vite-config.spec.ts +130 -261
  172. package/tests/utils/vite-pwa-plugin.acc.spec.ts +143 -0
  173. package/tests/utils/vite-pwa-plugin.spec.ts +350 -0
  174. package/tests/utils/worker-utils.spec.ts +8 -7
  175. package/tests/workers/client-worker.spec.ts +50 -1
  176. package/tests/workers/dev-port-file.verify.md +6 -0
  177. package/tests/workers/library-build-lint.spec.ts +1 -1
  178. package/tests/workers/library-build-worker.spec.ts +1 -1
  179. package/tests/workers/ngtsc-build-lint.spec.ts +1 -1
  180. package/tests/workers/server-build-lint.spec.ts +1 -1
  181. package/tests/workers/server-build-worker.spec.ts +1 -1
  182. package/tests/workers/server-runtime-worker.spec.ts +8 -1
  183. package/dist/infra/WorkerManager.d.ts +0 -40
  184. package/dist/infra/WorkerManager.d.ts.map +0 -1
  185. package/dist/infra/WorkerManager.js +0 -59
  186. package/dist/infra/WorkerManager.js.map +0 -1
  187. package/dist/utils/SdCliReporter.d.ts +0 -18
  188. package/dist/utils/SdCliReporter.d.ts.map +0 -1
  189. package/dist/utils/SdCliReporter.js +0 -144
  190. package/dist/utils/SdCliReporter.js.map +0 -1
  191. package/src/infra/WorkerManager.ts +0 -65
  192. package/src/utils/SdCliReporter.ts +0 -177
  193. package/tests/angular/scss-compiler-async.spec.ts +0 -54
  194. package/tests/commands/dev.spec.ts +0 -53
  195. package/tests/commands/watch.spec.ts +0 -53
  196. package/tests/infra/worker-manager.spec.ts +0 -63
  197. package/tests/utils/angular-compiler-emit.spec.ts +0 -570
  198. package/tests/utils/angular-compiler-init.spec.ts +0 -705
  199. package/tests/utils/angular-compiler-update.spec.ts +0 -293
  200. package/tests/utils/build-env.spec.ts +0 -33
  201. package/tests/utils/ngtsc-build-core-transform-stylesheet.spec.ts +0 -124
  202. package/tests/utils/ngtsc-scss-refactor.spec.ts +0 -47
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import path from "path";
3
+ import { sdAngularPlugin } from "../../src/angular/vite-angular-plugin.js";
4
+
5
+ const FIXTURE_DIR = path.resolve(import.meta.dirname, "fixtures/basic-app");
6
+ const TSCONFIG_PATH = path.join(FIXTURE_DIR, "tsconfig.json");
7
+
8
+ function mockEnvironmentContext() {
9
+ return {
10
+ environment: {
11
+ moduleGraph: {
12
+ getModulesByFile: (file: string) => {
13
+ return new Set([{ file, id: file }]);
14
+ },
15
+ },
16
+ },
17
+ };
18
+ }
19
+
20
+ describe("sdAngularPlugin SCSS @use HMR", () => {
21
+ let plugin: ReturnType<typeof sdAngularPlugin>;
22
+
23
+ beforeAll(async () => {
24
+ plugin = sdAngularPlugin({ tsconfig: TSCONFIG_PATH, dev: true });
25
+ await (plugin as any).buildStart?.call({});
26
+ });
27
+
28
+ afterAll(async () => {
29
+ await (plugin as any).buildEnd?.call({});
30
+ });
31
+
32
+ // Acceptance: inline styles의 직접 @use 의존성 변경 시 재컴파일
33
+ it("recompiles when inline SCSS @use dependency changes", async () => {
34
+ const variablesPath = path
35
+ .join(FIXTURE_DIR, "scss/_variables.scss")
36
+ .replace(/\\/g, "/");
37
+
38
+ const ctx = mockEnvironmentContext();
39
+ const result = await (plugin as any).hotUpdate?.call(ctx, {
40
+ file: variablesPath,
41
+ modules: [],
42
+ server: {},
43
+ timestamp: Date.now(),
44
+ read: () => Promise.resolve(""),
45
+ });
46
+
47
+ expect(result).toBeDefined();
48
+ expect(result!.length).toBeGreaterThan(0);
49
+ });
50
+
51
+ // Acceptance: inline styles의 간접 @use 의존성 변경 시 재컴파일 (체이닝)
52
+ it("recompiles when chained @use dependency changes", async () => {
53
+ const colorsPath = path
54
+ .join(FIXTURE_DIR, "scss/_colors.scss")
55
+ .replace(/\\/g, "/");
56
+
57
+ const ctx = mockEnvironmentContext();
58
+ const result = await (plugin as any).hotUpdate?.call(ctx, {
59
+ file: colorsPath,
60
+ modules: [],
61
+ server: {},
62
+ timestamp: Date.now(),
63
+ read: () => Promise.resolve(""),
64
+ });
65
+
66
+ expect(result).toBeDefined();
67
+ expect(result!.length).toBeGreaterThan(0);
68
+ });
69
+
70
+ // Acceptance: 무관한 SCSS 변경 시 재빌드하지 않음
71
+ it("does not recompile when unrelated SCSS changes", async () => {
72
+ const unrelatedPath = path
73
+ .join(FIXTURE_DIR, "scss/_unrelated.scss")
74
+ .replace(/\\/g, "/");
75
+
76
+ const ctx = mockEnvironmentContext();
77
+ const result = await (plugin as any).hotUpdate?.call(ctx, {
78
+ file: unrelatedPath,
79
+ modules: [],
80
+ server: {},
81
+ timestamp: Date.now(),
82
+ read: () => Promise.resolve(""),
83
+ });
84
+
85
+ expect(result).toBeUndefined();
86
+ });
87
+ });
@@ -54,7 +54,7 @@ describe("sdAngularPlugin", () => {
54
54
  });
55
55
 
56
56
  // Scenario: Angular 컴포넌트 .ts 파일 수정 시 컴포넌트 HMR (Acceptance — Feature 3.3)
57
- it("updates emit cache and returns affected modules when handleHotUpdate is called", async () => {
57
+ it("updates emit cache and returns affected modules when hotUpdate is called", async () => {
58
58
  const onBuildStart = vi.fn();
59
59
  const onBuild = vi.fn();
60
60
 
@@ -77,12 +77,12 @@ describe("sdAngularPlugin", () => {
77
77
  expect(initialResult).toBeDefined();
78
78
  expect(initialResult.code.length).toBeGreaterThan(0);
79
79
 
80
- // handleHotUpdate must exist
81
- expect((plugin as any).handleHotUpdate).toBeDefined();
80
+ // hotUpdate must exist
81
+ expect((plugin as any).hotUpdate).toBeDefined();
82
82
 
83
83
  // Simulate file change
84
84
  const mockModule = { file: appComponentPath, id: appComponentPath };
85
- const hmrResult = await (plugin as any).handleHotUpdate?.({
85
+ const hmrResult = await (plugin as any).hotUpdate?.({
86
86
  file: appComponentPath,
87
87
  modules: [mockModule],
88
88
  server: { watcher: { emit: vi.fn() } },
@@ -122,7 +122,7 @@ describe("sdAngularPlugin", () => {
122
122
  });
123
123
 
124
124
  // Scenario: 컴파일 에러 발생 및 복구 (Acceptance — Feature 3.3)
125
- it("calls onBuild with success=false when handleHotUpdate encounters compile error", async () => {
125
+ it("calls onBuild with success=false when hotUpdate encounters compile error", async () => {
126
126
  const onBuild = vi.fn();
127
127
  const plugin = sdAngularPlugin({
128
128
  tsconfig: TSCONFIG_PATH,
@@ -132,8 +132,8 @@ describe("sdAngularPlugin", () => {
132
132
 
133
133
  await (plugin as any).buildStart?.call({});
134
134
 
135
- // handleHotUpdate with a non-existent file — facade.update() should handle gracefully
136
- const _hmrResult = await (plugin as any).handleHotUpdate?.({
135
+ // hotUpdate with a non-existent file — facade.update() should handle gracefully
136
+ const _hmrResult = await (plugin as any).hotUpdate?.({
137
137
  file: path.join(FIXTURE_DIR, "src/nonexistent-file.ts").replace(/\\/g, "/"),
138
138
  modules: [],
139
139
  server: { watcher: { emit: vi.fn() } },
@@ -147,8 +147,8 @@ describe("sdAngularPlugin", () => {
147
147
  await (plugin as any).buildEnd?.call({});
148
148
  });
149
149
 
150
- // Scenario: non-Angular .ts 파일 수정 — handleHotUpdate passes through
151
- it("handleHotUpdate skips non-ts/html/scss files", async () => {
150
+ // Scenario: non-Angular .ts 파일 수정 — hotUpdate passes through
151
+ it("hotUpdate skips non-ts/html/scss files", async () => {
152
152
  const onBuildStart = vi.fn();
153
153
  const plugin = sdAngularPlugin({
154
154
  tsconfig: TSCONFIG_PATH,
@@ -159,7 +159,7 @@ describe("sdAngularPlugin", () => {
159
159
  await (plugin as any).buildStart?.call({});
160
160
 
161
161
  // .json file should be ignored
162
- const result = await (plugin as any).handleHotUpdate?.({
162
+ const result = await (plugin as any).hotUpdate?.({
163
163
  file: "/some/file.json",
164
164
  modules: [],
165
165
  server: {},
@@ -192,16 +192,16 @@ describe("sdAngularPlugin", () => {
192
192
  // (in real use, Vite server close triggers this)
193
193
  });
194
194
 
195
- // Scenario: optimizeDeps에 Angular Linker esbuild 플러그인이 등록된다 (Feature 1.1 Angular Linker)
196
- it("registers angular-vite-optimize-deps esbuild plugin in optimizeDeps config", () => {
195
+ // Scenario: optimizeDeps에 Angular Linker Rolldown 플러그인이 등록된다
196
+ it("registers angular-vite-optimize-deps rolldown plugin in optimizeDeps config", () => {
197
197
  const plugin = sdAngularPlugin({ tsconfig: TSCONFIG_PATH, dev: true });
198
198
  const config = (plugin as any).config?.();
199
199
 
200
- const esbuildPlugins = config?.optimizeDeps?.esbuildOptions?.plugins as
200
+ const rolldownPlugins = config?.optimizeDeps?.rolldownOptions?.plugins as
201
201
  | { name: string }[]
202
202
  | undefined;
203
- expect(esbuildPlugins).toBeDefined();
204
- expect(esbuildPlugins!.some((p) => p.name === "angular-vite-optimize-deps")).toBe(true);
203
+ expect(rolldownPlugins).toBeDefined();
204
+ expect(rolldownPlugins!.some((p) => p.name === "angular-vite-optimize-deps")).toBe(true);
205
205
  });
206
206
 
207
207
  // Scenario: .mjs 파일이 JavaScriptTransformer를 통과한다 (Feature 1.1 Angular Linker)
@@ -35,11 +35,9 @@ vi.mock("@simplysm/core-node", () => ({
35
35
  }));
36
36
 
37
37
  // env mock
38
- let mockEnv: Record<string, unknown> = {};
38
+ let mockEnv: Record<string, string | undefined> = {};
39
39
  vi.mock("@simplysm/core-common", () => ({
40
- env: new Proxy({} as Record<string, unknown>, {
41
- get: (_target, prop) => mockEnv[prop as string],
42
- }),
40
+ env: (key: string) => mockEnv[key],
43
41
  }));
44
42
 
45
43
  // cpx mock (was execa)
@@ -35,11 +35,9 @@ vi.mock("@simplysm/core-node", () => ({
35
35
  },
36
36
  }));
37
37
 
38
- let mockEnv: Record<string, unknown> = {};
38
+ let mockEnv: Record<string, string | undefined> = {};
39
39
  vi.mock("@simplysm/core-common", () => ({
40
- env: new Proxy({} as Record<string, unknown>, {
41
- get: (_target, prop) => mockEnv[prop as string],
42
- }),
40
+ env: (key: string) => mockEnv[key],
43
41
  }));
44
42
 
45
43
  const execaCalls: { command: string; args: string[] }[] = [];
@@ -35,11 +35,9 @@ vi.mock("@simplysm/core-node", () => ({
35
35
  },
36
36
  }));
37
37
 
38
- let mockEnv: Record<string, unknown> = {};
38
+ let mockEnv: Record<string, string | undefined> = {};
39
39
  vi.mock("@simplysm/core-common", () => ({
40
- env: new Proxy({} as Record<string, unknown>, {
41
- get: (_target, prop) => mockEnv[prop as string],
42
- }),
40
+ env: (key: string) => mockEnv[key],
43
41
  }));
44
42
 
45
43
  const execaCalls: { command: string; args: string[] }[] = [];
@@ -31,6 +31,27 @@ vi.mock("../../src/utils/sd-config", () => ({
31
31
  loadSdConfig: vi.fn(),
32
32
  }));
33
33
 
34
+ // fs mock (포트 파일 읽기용)
35
+ const mockReadFileSync = vi.fn();
36
+ const mockExistsSync = vi.fn().mockReturnValue(false);
37
+ vi.mock("node:fs", () => ({
38
+ default: {
39
+ readFileSync: (...args: any[]) => mockReadFileSync(...args),
40
+ existsSync: (...args: any[]) => mockExistsSync(...args),
41
+ },
42
+ readFileSync: (...args: any[]) => mockReadFileSync(...args),
43
+ existsSync: (...args: any[]) => mockExistsSync(...args),
44
+ }));
45
+
46
+ // http mock (헬스체크용)
47
+ const mockHttpGet = vi.fn();
48
+ vi.mock("node:http", () => ({
49
+ default: {
50
+ get: (...args: any[]) => mockHttpGet(...args),
51
+ },
52
+ get: (...args: any[]) => mockHttpGet(...args),
53
+ }));
54
+
34
55
  const { Capacitor } = await import("../../src/capacitor/capacitor");
35
56
  const { Electron } = await import("../../src/electron/electron");
36
57
  const { loadSdConfig } = await import("../../src/utils/sd-config");
@@ -144,4 +165,83 @@ describe("runDevice", () => {
144
165
 
145
166
  await expect(runDevice({ target: "my-server", options: [] })).rejects.toThrow();
146
167
  });
168
+
169
+ // Acceptance: Scenario "dev 서버 실행 중 device 실행 시 URL 자동 생성"
170
+ it("auto-detects URL from .dev-port when server is a string", async () => {
171
+ vi.mocked(loadSdConfig).mockResolvedValue({
172
+ packages: {
173
+ "client-devtool": {
174
+ target: "client",
175
+ server: "server",
176
+ electron: { appId: "com.test.electron" },
177
+ },
178
+ },
179
+ });
180
+
181
+ mockReadFileSync.mockReturnValue("5173");
182
+
183
+ // HTTP 헬스체크 성공 mock
184
+ mockHttpGet.mockImplementation((_url: string, cb: Function) => {
185
+ const res = { resume: vi.fn() };
186
+ cb(res);
187
+ return { on: vi.fn(), setTimeout: vi.fn() };
188
+ });
189
+
190
+ await runDevice({ target: "client-devtool", options: [] });
191
+
192
+ expect(mockElectronInstance.run).toHaveBeenCalledWith(
193
+ "http://localhost:5173/client-devtool/",
194
+ );
195
+ });
196
+
197
+ // Acceptance: Scenario "dev 서버 미실행 시 에러"
198
+ it("throws when .dev-port file does not exist and server is a string", async () => {
199
+ vi.mocked(loadSdConfig).mockResolvedValue({
200
+ packages: {
201
+ "client-devtool": {
202
+ target: "client",
203
+ server: "server",
204
+ electron: { appId: "com.test.electron" },
205
+ },
206
+ },
207
+ });
208
+
209
+ mockReadFileSync.mockImplementation(() => {
210
+ throw new Error("ENOENT");
211
+ });
212
+
213
+ await expect(runDevice({ target: "client-devtool", options: [] })).rejects.toThrow(
214
+ "dev 서버가 실행 중이 아닙니다",
215
+ );
216
+ });
217
+
218
+ // Acceptance: Scenario "stale 포트 파일 존재 시 헬스체크 실패 에러"
219
+ it("throws when .dev-port exists but health check fails", async () => {
220
+ vi.mocked(loadSdConfig).mockResolvedValue({
221
+ packages: {
222
+ "client-devtool": {
223
+ target: "client",
224
+ server: "server",
225
+ electron: { appId: "com.test.electron" },
226
+ },
227
+ },
228
+ });
229
+
230
+ mockReadFileSync.mockReturnValue("5173");
231
+
232
+ // HTTP 헬스체크 실패 mock
233
+ mockHttpGet.mockImplementation((_url: string, _cb: Function) => {
234
+ const req = {
235
+ on: vi.fn((event: string, handler: Function) => {
236
+ if (event === "error") handler(new Error("ECONNREFUSED"));
237
+ }),
238
+ setTimeout: vi.fn(),
239
+ };
240
+ return req;
241
+ });
242
+
243
+ await expect(runDevice({ target: "client-devtool", options: [] })).rejects.toThrow(
244
+ "dev 서버가 응답하지 않습니다",
245
+ );
246
+ });
147
247
  });
@@ -7,6 +7,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
7
7
  const mockFsxExists = vi.fn();
8
8
  const mockFsxReadJson = vi.fn();
9
9
  const mockFsxWriteJson = vi.fn().mockResolvedValue(undefined);
10
+ const mockFsxWrite = vi.fn().mockResolvedValue(undefined);
10
11
  const mockFsxMkdir = vi.fn().mockResolvedValue(undefined);
11
12
  const mockFsxCopy = vi.fn().mockResolvedValue(undefined);
12
13
  const mockFsxReaddir = vi.fn();
@@ -17,6 +18,7 @@ vi.mock("@simplysm/core-node", () => ({
17
18
  exists: mockFsxExists,
18
19
  readJson: mockFsxReadJson,
19
20
  writeJson: mockFsxWriteJson,
21
+ write: mockFsxWrite,
20
22
  mkdir: mockFsxMkdir,
21
23
  copy: mockFsxCopy,
22
24
  readdir: mockFsxReaddir,
@@ -96,6 +98,11 @@ function setupDefaultMocks() {
96
98
  "better-sqlite3": "^11.0.0",
97
99
  "sharp": "^0.34.0",
98
100
  },
101
+ devDependencies: {
102
+ "electron": "^35.0.0",
103
+ "@electron/rebuild": "^4.0.0",
104
+ "electron-builder": "^26.0.0",
105
+ },
99
106
  });
100
107
  mockFsxReaddir.mockResolvedValue(["index.html", "assets", "electron"]);
101
108
  // Default: glob returns one exe file matching the builder output
@@ -169,7 +176,7 @@ describe("Electron", () => {
169
176
  //#region Rule: Electron 프로젝트를 초기화한다
170
177
 
171
178
  describe("인수 테스트: 초기화", () => {
172
- it("package.json 생성 + npm install + electron-rebuild를 실행한다", async () => {
179
+ it("package.json 생성 + pnpm install + electron-rebuild를 실행한다", async () => {
173
180
  const { Electron } = await import("../../src/electron/electron.js");
174
181
 
175
182
  const electron = await Electron.create(PKG_PATH, {
@@ -181,13 +188,13 @@ describe("Electron", () => {
181
188
 
182
189
  expect(findElectronPackageJson()).toBeDefined();
183
190
 
184
- const execaCalls = mockCpxSpawn.mock.calls;
191
+ const spawnCalls = mockCpxSpawn.mock.calls;
185
192
  expect(
186
- execaCalls.find((c) => c[0] === "npm" && (c[1] as string[]).includes("install")),
193
+ spawnCalls.find((c) => c[0] === "pnpm" && (c[1] as string[]).includes("install")),
187
194
  ).toBeDefined();
188
195
  expect(
189
- execaCalls.find(
190
- (c) => typeof c[0] === "string" && c[0].includes("electron-rebuild"),
196
+ spawnCalls.find(
197
+ (c) => c[0] === "pnpm" && (c[1] as string[]).includes("electron-rebuild"),
191
198
  ),
192
199
  ).toBeDefined();
193
200
  });
@@ -199,7 +206,7 @@ describe("Electron", () => {
199
206
  await electron.initialize();
200
207
 
201
208
  const rebuildCall = mockCpxSpawn.mock.calls.find(
202
- (c) => typeof c[0] === "string" && c[0].includes("electron-rebuild"),
209
+ (c) => c[0] === "pnpm" && (c[1] as string[]).includes("electron-rebuild"),
203
210
  );
204
211
  expect(rebuildCall).toBeUndefined();
205
212
  });
@@ -455,9 +462,9 @@ describe("Electron", () => {
455
462
  const electronKill = vi.fn();
456
463
  let resolveElectron: () => void = () => {};
457
464
 
458
- mockCpxSpawn.mockImplementation((cmd: string) => {
459
- if (typeof cmd === "string" && cmd.includes("electron")) {
460
- // Electron process: create a deferred promise we can resolve externally
465
+ mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
466
+ // pnpm exec electron . Electron 프로세스
467
+ if (cmd === "pnpm" && args[0] === "exec" && args[1] === "electron" && args[2] === ".") {
461
468
  const p = new Promise<void>((resolve) => {
462
469
  resolveElectron = resolve;
463
470
  }) as any;
@@ -537,8 +544,8 @@ describe("Electron", () => {
537
544
  describe("단위: run() 플러그인 동작", () => {
538
545
  it("passes custom env and ELECTRON_DEV_URL via esbuild banner", async () => {
539
546
  let resolveElectron: () => void = () => {};
540
- mockCpxSpawn.mockImplementation((cmd: string) => {
541
- if (typeof cmd === "string" && cmd.includes("electron")) {
547
+ mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
548
+ if (cmd === "pnpm" && args[0] === "exec" && args[1] === "electron" && args[2] === ".") {
542
549
  const p = new Promise<void>((resolve) => {
543
550
  resolveElectron = resolve;
544
551
  }) as any;
@@ -570,8 +577,8 @@ describe("Electron", () => {
570
577
 
571
578
  it("calls initialize() before starting esbuild context", async () => {
572
579
  let resolveElectron: () => void = () => {};
573
- mockCpxSpawn.mockImplementation((cmd: string) => {
574
- if (typeof cmd === "string" && cmd.includes("electron")) {
580
+ mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
581
+ if (cmd === "pnpm" && args[0] === "exec" && args[1] === "electron" && args[2] === ".") {
575
582
  const p = new Promise<void>((resolve) => {
576
583
  resolveElectron = resolve;
577
584
  }) as any;
@@ -589,11 +596,11 @@ describe("Electron", () => {
589
596
  resolveElectron();
590
597
  await runPromise;
591
598
 
592
- // initialize calls npm install -> execa should have been called with npm install
593
- const npmInstallCall = mockCpxSpawn.mock.calls.find(
594
- (c: any[]) => c[0] === "npm" && (c[1] as string[]).includes("install"),
599
+ // initialize calls pnpm install
600
+ const pnpmInstallCall = mockCpxSpawn.mock.calls.find(
601
+ (c: any[]) => c[0] === "pnpm" && (c[1] as string[]).includes("install"),
595
602
  );
596
- expect(npmInstallCall).toBeDefined();
603
+ expect(pnpmInstallCall).toBeDefined();
597
604
  }, 10_000);
598
605
  });
599
606
 
@@ -78,7 +78,6 @@ describe("NgtscEngine", () => {
78
78
  output: { js: true, dts: true },
79
79
  }),
80
80
  );
81
- expect(result.success).toBe(true);
82
81
  expect(result.build.success).toBe(true);
83
82
  expect(result.build.diagnostics).toEqual([]);
84
83
  await engine.stop();
@@ -133,7 +132,6 @@ describe("NgtscEngine", () => {
133
132
  const engine = new NgtscEngine({ cwd: "/root", pkg: createMockPkg() });
134
133
  const result = await engine.run({ js: true, dts: true });
135
134
 
136
- expect(result.success).toBe(false);
137
135
  expect(result.build.success).toBe(false);
138
136
  expect(result.build.errors).toContain("TS2322: Type error");
139
137
  await engine.stop();
@@ -148,7 +146,6 @@ describe("NgtscEngine", () => {
148
146
  const engine = new NgtscEngine({ cwd: "/root", pkg: createMockPkg() });
149
147
  const result = await engine.run({ js: true, dts: true });
150
148
 
151
- expect(result.success).toBe(false);
152
149
  expect(result.build.success).toBe(false);
153
150
  expect(result.build.errors).toEqual(["ngtsc compilation error"]);
154
151
  await engine.stop();
@@ -89,7 +89,6 @@ describe("ServerEsbuildEngine", () => {
89
89
  packageManager: "mise",
90
90
  }),
91
91
  );
92
- expect(result.success).toBe(true);
93
92
  expect(result.build.success).toBe(true);
94
93
  await engine.stop();
95
94
  });
@@ -104,7 +103,6 @@ describe("ServerEsbuildEngine", () => {
104
103
  const engine = new ServerEsbuildEngine({ cwd: "/root", pkg: createMockPkg() });
105
104
  const result = await engine.run({ js: true, dts: false });
106
105
 
107
- expect(result.success).toBe(false);
108
106
  expect(result.build.warnings).toEqual(["warn1"]);
109
107
  expect(result.build.success).toBe(false);
110
108
  expect(result.build.diagnostics).toEqual([{ code: 2345, category: 1 }]);
@@ -121,7 +119,6 @@ describe("ServerEsbuildEngine", () => {
121
119
  const engine = new ServerEsbuildEngine({ cwd: "/root", pkg: createMockPkg() });
122
120
  const result = await engine.run({ js: true, dts: false });
123
121
 
124
- expect(result.success).toBe(false);
125
122
  expect(result.build.success).toBe(false);
126
123
  expect(result.build.errors).toEqual(["esbuild error"]);
127
124
  await engine.stop();
@@ -75,7 +75,6 @@ describe("TscEngine", () => {
75
75
  output: { js: true, dts: true },
76
76
  }),
77
77
  );
78
- expect(result.success).toBe(true);
79
78
  expect(result.build.success).toBe(true);
80
79
  await engine.stop();
81
80
  });
@@ -101,7 +100,7 @@ describe("TscEngine", () => {
101
100
  const engine = new TscEngine({ cwd: "/root", pkg: createMockPkg() });
102
101
  const result = await engine.run({ js: true, dts: true });
103
102
 
104
- expect(result.success).toBe(false);
103
+ expect(result.build.success).toBe(false);
105
104
  expect(result.build.errors).toEqual(["type error"]);
106
105
  expect(result.build.diagnostics).toHaveLength(1);
107
106
  await engine.stop();
@@ -74,7 +74,6 @@ describe("ViteEngine", () => {
74
74
  pkgDir: "/packages/my-client",
75
75
  }),
76
76
  );
77
- expect(result.success).toBe(true);
78
77
  expect(result.build.success).toBe(true);
79
78
  expect(result.build.errors).toEqual([]);
80
79
  expect(result.build.diagnostics).toEqual([]);
@@ -91,7 +90,6 @@ describe("ViteEngine", () => {
91
90
  const engine = new ViteEngine({ cwd: "/root", pkg: createMockPkg() });
92
91
  const result = await engine.run({ js: true, dts: false });
93
92
 
94
- expect(result.success).toBe(false);
95
93
  expect(result.build.success).toBe(false);
96
94
  expect(result.build.errors).toContain("TS2345: Argument of type...");
97
95
  await engine.stop();
@@ -1,23 +1,13 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it } from "vitest";
2
2
  import { SignalHandler } from "../../src/infra/SignalHandler";
3
3
 
4
4
  describe("SignalHandler", () => {
5
5
  it("resolves waitForTermination on requestTermination", async () => {
6
6
  const handler = new SignalHandler();
7
7
 
8
- expect(handler.isTerminated()).toBe(false);
9
-
10
8
  handler.requestTermination();
11
9
 
12
10
  await handler.waitForTermination();
13
- expect(handler.isTerminated()).toBe(true);
14
- });
15
-
16
- it("is not terminated initially", () => {
17
- const handler = new SignalHandler();
18
- expect(handler.isTerminated()).toBe(false);
19
- // Clean up: prevent dangling signal listeners
20
- handler.requestTermination();
21
11
  });
22
12
 
23
13
  it("handles double requestTermination gracefully", async () => {
@@ -27,6 +17,5 @@ describe("SignalHandler", () => {
27
17
  handler.requestTermination();
28
18
 
29
19
  await handler.waitForTermination();
30
- expect(handler.isTerminated()).toBe(true);
31
20
  });
32
21
  });
@@ -66,7 +66,6 @@ vi.mock("../../src/engines/index", () => ({
66
66
  createBuildEngine: vi.fn(() => {
67
67
  const engine = {
68
68
  run: vi.fn().mockResolvedValue({
69
- success: true,
70
69
  build: { success: true, errors: [], warnings: [], diagnostics: [] },
71
70
  }),
72
71
  startWatch: vi.fn().mockResolvedValue(undefined),
@@ -159,7 +158,6 @@ beforeEach(() => {
159
158
  vi.mocked(createBuildEngine).mockImplementation(() => {
160
159
  const engine = {
161
160
  run: vi.fn().mockResolvedValue({
162
- success: true,
163
161
  build: { success: true, errors: [], warnings: [], diagnostics: [] },
164
162
 
165
163
  }),
@@ -413,7 +411,6 @@ describe("BuildOrchestrator.start", () => {
413
411
  });
414
412
  vi.mocked(createBuildEngine).mockReturnValue({
415
413
  run: vi.fn().mockResolvedValue({
416
- success: false,
417
414
  build: { success: false, errors: ["Module not found"], warnings: [], diagnostics: [] },
418
415
  }),
419
416
  startWatch: vi.fn(),
@@ -603,7 +600,6 @@ describe("BuildOrchestrator client build", () => {
603
600
  });
604
601
  vi.mocked(createBuildEngine).mockReturnValue({
605
602
  run: vi.fn().mockResolvedValue({
606
- success: true,
607
603
  build: { success: true, errors: [], warnings: [], diagnostics: [] },
608
604
  }),
609
605
  startWatch: vi.fn(),
@@ -637,7 +633,6 @@ describe("BuildOrchestrator client build", () => {
637
633
  });
638
634
  vi.mocked(createBuildEngine).mockReturnValue({
639
635
  run: vi.fn().mockResolvedValue({
640
- success: false,
641
636
  build: { success: false, errors: ["Template error"], warnings: [], diagnostics: [] },
642
637
  }),
643
638
  startWatch: vi.fn(),
@@ -957,7 +952,6 @@ describe("BuildOrchestrator native build integration (Slice 1)", () => {
957
952
  });
958
953
  vi.mocked(createBuildEngine).mockReturnValue({
959
954
  run: vi.fn().mockResolvedValue({
960
- success: false,
961
955
  build: { success: false, errors: ["Build error"], warnings: [], diagnostics: [] },
962
956
  }),
963
957
  startWatch: vi.fn(),