@simplysm/sd-cli 14.0.15 → 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 (216) hide show
  1. package/README.md +4 -3
  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 +24 -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 +1 -1
  17. package/dist/commands/device.d.ts.map +1 -1
  18. package/dist/commands/device.js +61 -12
  19. package/dist/commands/device.js.map +1 -1
  20. package/dist/commands/lint.d.ts +0 -1
  21. package/dist/commands/lint.d.ts.map +1 -1
  22. package/dist/commands/lint.js +2 -3
  23. package/dist/commands/lint.js.map +1 -1
  24. package/dist/commands/publish.js +3 -3
  25. package/dist/commands/publish.js.map +1 -1
  26. package/dist/commands/replace-deps.js +1 -1
  27. package/dist/commands/replace-deps.js.map +1 -1
  28. package/dist/commands/typecheck.d.ts.map +1 -1
  29. package/dist/commands/typecheck.js +1 -2
  30. package/dist/commands/typecheck.js.map +1 -1
  31. package/dist/electron/electron.d.ts +3 -2
  32. package/dist/electron/electron.d.ts.map +1 -1
  33. package/dist/electron/electron.js +54 -31
  34. package/dist/electron/electron.js.map +1 -1
  35. package/dist/engines/BaseEngine.js +1 -1
  36. package/dist/engines/BaseEngine.js.map +1 -1
  37. package/dist/engines/NgtscEngine.d.ts.map +1 -1
  38. package/dist/engines/NgtscEngine.js +0 -1
  39. package/dist/engines/NgtscEngine.js.map +1 -1
  40. package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
  41. package/dist/engines/ServerEsbuildEngine.js +0 -1
  42. package/dist/engines/ServerEsbuildEngine.js.map +1 -1
  43. package/dist/engines/TscEngine.d.ts.map +1 -1
  44. package/dist/engines/TscEngine.js +0 -1
  45. package/dist/engines/TscEngine.js.map +1 -1
  46. package/dist/engines/ViteEngine.d.ts.map +1 -1
  47. package/dist/engines/ViteEngine.js +10 -1
  48. package/dist/engines/ViteEngine.js.map +1 -1
  49. package/dist/engines/index.d.ts +0 -10
  50. package/dist/engines/index.d.ts.map +1 -1
  51. package/dist/engines/index.js +0 -5
  52. package/dist/engines/index.js.map +1 -1
  53. package/dist/engines/types.d.ts +0 -1
  54. package/dist/engines/types.d.ts.map +1 -1
  55. package/dist/infra/SignalHandler.d.ts +1 -6
  56. package/dist/infra/SignalHandler.d.ts.map +1 -1
  57. package/dist/infra/SignalHandler.js +4 -13
  58. package/dist/infra/SignalHandler.js.map +1 -1
  59. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  60. package/dist/orchestrators/BuildOrchestrator.js +7 -12
  61. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  62. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  63. package/dist/orchestrators/DevWatchOrchestrator.js +18 -11
  64. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  65. package/dist/sd-cli-entry.d.ts +0 -1
  66. package/dist/sd-cli-entry.d.ts.map +1 -1
  67. package/dist/sd-cli-entry.js +13 -16
  68. package/dist/sd-cli-entry.js.map +1 -1
  69. package/dist/sd-cli.js +1 -1
  70. package/dist/sd-cli.js.map +1 -1
  71. package/dist/sd-config.types.d.ts +12 -2
  72. package/dist/sd-config.types.d.ts.map +1 -1
  73. package/dist/utils/angular-compiler.d.ts.map +1 -1
  74. package/dist/utils/angular-compiler.js +20 -13
  75. package/dist/utils/angular-compiler.js.map +1 -1
  76. package/dist/utils/esbuild-config.d.ts +1 -1
  77. package/dist/utils/esbuild-config.d.ts.map +1 -1
  78. package/dist/utils/esbuild-config.js +1 -4
  79. package/dist/utils/esbuild-config.js.map +1 -1
  80. package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
  81. package/dist/utils/ngtsc-build-core.js +3 -0
  82. package/dist/utils/ngtsc-build-core.js.map +1 -1
  83. package/dist/utils/orchestrator-utils.js +1 -1
  84. package/dist/utils/orchestrator-utils.js.map +1 -1
  85. package/dist/utils/tsc-build.d.ts +5 -0
  86. package/dist/utils/tsc-build.d.ts.map +1 -1
  87. package/dist/utils/tsc-build.js +2 -1
  88. package/dist/utils/tsc-build.js.map +1 -1
  89. package/dist/utils/vite-config.d.ts +1 -1
  90. package/dist/utils/vite-config.d.ts.map +1 -1
  91. package/dist/utils/vite-config.js +22 -53
  92. package/dist/utils/vite-config.js.map +1 -1
  93. package/dist/utils/vite-pwa-plugin.d.ts +9 -0
  94. package/dist/utils/vite-pwa-plugin.d.ts.map +1 -0
  95. package/dist/utils/vite-pwa-plugin.js +139 -0
  96. package/dist/utils/vite-pwa-plugin.js.map +1 -0
  97. package/dist/utils/worker-utils.d.ts +2 -5
  98. package/dist/utils/worker-utils.d.ts.map +1 -1
  99. package/dist/utils/worker-utils.js +5 -11
  100. package/dist/utils/worker-utils.js.map +1 -1
  101. package/dist/workers/client.worker.d.ts.map +1 -1
  102. package/dist/workers/client.worker.js +9 -3
  103. package/dist/workers/client.worker.js.map +1 -1
  104. package/dist/workers/library-build.worker.d.ts.map +1 -1
  105. package/dist/workers/library-build.worker.js +6 -2
  106. package/dist/workers/library-build.worker.js.map +1 -1
  107. package/dist/workers/ngtsc-build.worker.js +2 -2
  108. package/dist/workers/ngtsc-build.worker.js.map +1 -1
  109. package/dist/workers/server-build.worker.d.ts.map +1 -1
  110. package/dist/workers/server-build.worker.js +6 -2
  111. package/dist/workers/server-build.worker.js.map +1 -1
  112. package/dist/workers/server-runtime.worker.js +4 -4
  113. package/dist/workers/server-runtime.worker.js.map +1 -1
  114. package/docs/config.md +30 -2
  115. package/docs/pwa-configuration-types.md +1 -1
  116. package/package.json +8 -10
  117. package/src/angular/client-transform-stylesheet.ts +104 -2
  118. package/src/angular/vite-angular-plugin.ts +92 -31
  119. package/src/capacitor/capacitor.ts +25 -26
  120. package/src/commands/check.ts +8 -11
  121. package/src/commands/device.ts +71 -17
  122. package/src/commands/lint.ts +2 -3
  123. package/src/commands/publish.ts +3 -3
  124. package/src/commands/replace-deps.ts +1 -1
  125. package/src/commands/typecheck.ts +1 -2
  126. package/src/electron/electron.ts +62 -43
  127. package/src/engines/BaseEngine.ts +1 -1
  128. package/src/engines/NgtscEngine.ts +0 -1
  129. package/src/engines/ServerEsbuildEngine.ts +0 -1
  130. package/src/engines/TscEngine.ts +0 -1
  131. package/src/engines/ViteEngine.ts +9 -1
  132. package/src/engines/index.ts +0 -10
  133. package/src/engines/types.ts +0 -1
  134. package/src/infra/SignalHandler.ts +4 -14
  135. package/src/orchestrators/BuildOrchestrator.ts +7 -9
  136. package/src/orchestrators/DevWatchOrchestrator.ts +22 -10
  137. package/src/sd-cli-entry.ts +17 -24
  138. package/src/sd-cli.ts +1 -1
  139. package/src/sd-config.types.ts +13 -2
  140. package/src/utils/angular-compiler.ts +21 -21
  141. package/src/utils/esbuild-config.ts +2 -5
  142. package/src/utils/ngtsc-build-core.ts +7 -0
  143. package/src/utils/orchestrator-utils.ts +1 -1
  144. package/src/utils/tsc-build.ts +7 -0
  145. package/src/utils/vite-config.ts +23 -55
  146. package/src/utils/vite-pwa-plugin.ts +168 -0
  147. package/src/utils/worker-utils.ts +5 -11
  148. package/src/workers/client.worker.ts +11 -3
  149. package/src/workers/library-build.worker.ts +6 -2
  150. package/src/workers/ngtsc-build.worker.ts +2 -2
  151. package/src/workers/server-build.worker.ts +7 -2
  152. package/src/workers/server-runtime.worker.ts +4 -4
  153. package/tests/angular/client-transform-stylesheet.spec.ts +43 -0
  154. package/tests/angular/find-affected-by-scss.spec.ts +37 -0
  155. package/tests/angular/fixtures/basic-app/scss/_colors.scss +1 -0
  156. package/tests/angular/fixtures/basic-app/scss/_variables.scss +3 -0
  157. package/tests/angular/fixtures/basic-app/src/styled.component.ts +14 -0
  158. package/tests/angular/linker-disk-cache.spec.ts +158 -0
  159. package/tests/angular/scss-disk-cache.spec.ts +162 -0
  160. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
  161. package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
  162. package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
  163. package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +87 -0
  164. package/tests/angular/vite-angular-plugin.spec.ts +15 -15
  165. package/tests/capacitor/capacitor-icon.spec.ts +2 -4
  166. package/tests/capacitor/capacitor-init.spec.ts +2 -4
  167. package/tests/capacitor/capacitor-workspace.spec.ts +2 -4
  168. package/tests/commands/device.spec.ts +108 -8
  169. package/tests/commands/publish.spec.ts +2 -2
  170. package/tests/commands/typecheck.spec.ts +1 -1
  171. package/tests/electron/electron.spec.ts +24 -17
  172. package/tests/engines/ngtsc-engine.spec.ts +0 -3
  173. package/tests/engines/server-esbuild-engine.spec.ts +0 -3
  174. package/tests/engines/tsc-engine.spec.ts +1 -2
  175. package/tests/engines/vite-engine.spec.ts +0 -2
  176. package/tests/infra/signal-handler.spec.ts +1 -12
  177. package/tests/orchestrators/build-orchestrator.spec.ts +1 -7
  178. package/tests/orchestrators/dev-watch-orchestrator.spec.ts +24 -66
  179. package/tests/utils/angular-compiler.spec.ts +1396 -32
  180. package/tests/utils/esbuild-config.spec.ts +4 -7
  181. package/tests/utils/{ngtsc-build-core-angular-compiler.spec.ts → ngtsc-build-core.spec.ts} +142 -11
  182. package/tests/utils/orchestrator-utils.spec.ts +2 -2
  183. package/tests/utils/sd-config.spec.ts +2 -2
  184. package/tests/utils/tsc-build.spec.ts +4 -1
  185. package/tests/utils/vite-config.spec.ts +130 -261
  186. package/tests/utils/vite-pwa-plugin.acc.spec.ts +143 -0
  187. package/tests/utils/vite-pwa-plugin.spec.ts +350 -0
  188. package/tests/utils/worker-utils.spec.ts +8 -7
  189. package/tests/workers/client-worker.spec.ts +50 -1
  190. package/tests/workers/dev-port-file.verify.md +6 -0
  191. package/tests/workers/library-build-lint.spec.ts +1 -1
  192. package/tests/workers/library-build-worker.spec.ts +1 -1
  193. package/tests/workers/ngtsc-build-lint.spec.ts +1 -1
  194. package/tests/workers/server-build-lint.spec.ts +1 -1
  195. package/tests/workers/server-build-worker.spec.ts +1 -1
  196. package/tests/workers/server-runtime-worker.spec.ts +8 -1
  197. package/dist/infra/WorkerManager.d.ts +0 -40
  198. package/dist/infra/WorkerManager.d.ts.map +0 -1
  199. package/dist/infra/WorkerManager.js +0 -59
  200. package/dist/infra/WorkerManager.js.map +0 -1
  201. package/dist/utils/SdCliReporter.d.ts +0 -18
  202. package/dist/utils/SdCliReporter.d.ts.map +0 -1
  203. package/dist/utils/SdCliReporter.js +0 -144
  204. package/dist/utils/SdCliReporter.js.map +0 -1
  205. package/src/infra/WorkerManager.ts +0 -65
  206. package/src/utils/SdCliReporter.ts +0 -177
  207. package/tests/angular/scss-compiler-async.spec.ts +0 -54
  208. package/tests/commands/dev.spec.ts +0 -53
  209. package/tests/commands/watch.spec.ts +0 -53
  210. package/tests/infra/worker-manager.spec.ts +0 -63
  211. package/tests/utils/angular-compiler-emit.spec.ts +0 -570
  212. package/tests/utils/angular-compiler-init.spec.ts +0 -705
  213. package/tests/utils/angular-compiler-update.spec.ts +0 -293
  214. package/tests/utils/build-env.spec.ts +0 -33
  215. package/tests/utils/ngtsc-build-core-transform-stylesheet.spec.ts +0 -124
  216. package/tests/utils/ngtsc-scss-refactor.spec.ts +0 -47
@@ -1,3 +1,6 @@
1
+ import fs from "node:fs";
2
+ import http from "node:http";
3
+ import path from "path";
1
4
  import { consola } from "consola";
2
5
  import { pathx } from "@simplysm/core-node";
3
6
  import { SdError } from "@simplysm/core-common";
@@ -8,7 +11,7 @@ import { Electron } from "../electron/electron";
8
11
  const logger = consola.withTag("sd:cli:device");
9
12
 
10
13
  export interface DeviceOptions {
11
- package: string;
14
+ target?: string;
12
15
  url?: string;
13
16
  options: string[];
14
17
  }
@@ -21,45 +24,96 @@ export interface DeviceOptions {
21
24
  */
22
25
  export async function runDevice(options: DeviceOptions): Promise<void> {
23
26
  const cwd = process.cwd();
24
- const sdConfig = await loadSdConfig({ cwd, dev: true, options: options.options });
27
+ const sdConfig = await loadSdConfig({ cwd, dev: true, opt: options.options });
25
28
 
26
- const pkgConfig = sdConfig.packages[options.package];
29
+ // target 결정: 미지정 시 유일한 client 패키지 자동 선택
30
+ let targetName: string;
31
+ if (options.target != null) {
32
+ targetName = options.target;
33
+ } else {
34
+ const clientNames = Object.entries(sdConfig.packages)
35
+ .filter(([, cfg]) => cfg != null && cfg.target === "client")
36
+ .map(([name]) => name);
37
+ if (clientNames.length === 0) {
38
+ throw new SdError("device 실행 가능한 client 패키지가 없습니다.");
39
+ }
40
+ if (clientNames.length > 1) {
41
+ throw new SdError(
42
+ `client 패키지가 여러 개입니다. target을 지정해주세요: ${clientNames.join(", ")}`,
43
+ );
44
+ }
45
+ targetName = clientNames[0];
46
+ }
47
+
48
+ const pkgConfig = sdConfig.packages[targetName];
27
49
  if (pkgConfig == null) {
28
- throw new SdError(`패키지를 찾을 수 없습니다: ${options.package}`);
50
+ throw new SdError(`패키지를 찾을 수 없습니다: ${targetName}`);
29
51
  }
30
52
  if (pkgConfig.target !== "client") {
31
- throw new SdError(`client 패키지만 device 실행이 가능합니다: ${options.package} (target: ${pkgConfig.target})`);
53
+ throw new SdError(
54
+ `client 패키지만 device 실행이 가능합니다: ${targetName} (target: ${pkgConfig.target})`,
55
+ );
32
56
  }
33
57
 
34
58
  const clientConfig = pkgConfig;
35
- const pkgDir = pathx.posixResolve(cwd, "packages", options.package);
59
+ const pkgDir = pathx.posixResolve(cwd, "packages", targetName);
36
60
 
37
61
  // 서버 URL 결정
38
62
  let serverUrl = options.url;
39
63
  if (serverUrl == null) {
40
64
  if (typeof clientConfig.server === "number") {
41
- serverUrl = `http://localhost:${clientConfig.server}`;
65
+ serverUrl = `http://localhost:${clientConfig.server}/${targetName}/`;
42
66
  } else {
43
- throw new SdError(
44
- `--url 옵션이 필요합니다. server가 패키지명으로 설정되어 있습니다: ${clientConfig.server}`,
45
- );
67
+ // server가 패키지명(string)인 경우: .dev-port 파일에서 포트 자동 탐지
68
+ const portFile = path.join(pkgDir, "dist", ".dev-port");
69
+ let portStr: string;
70
+ try {
71
+ portStr = fs.readFileSync(portFile, "utf-8").trim();
72
+ } catch {
73
+ throw new SdError(
74
+ "dev 서버가 실행 중이 아닙니다. 먼저 pnpm dev를 실행해주세요.",
75
+ );
76
+ }
77
+ const port = Number(portStr);
78
+ serverUrl = `http://localhost:${port}/${targetName}/`;
79
+
80
+ // HTTP 헬스체크
81
+ const alive = await checkDevServer(serverUrl);
82
+ if (!alive) {
83
+ throw new SdError(
84
+ "dev 서버가 응답하지 않습니다. pnpm dev를 다시 실행해주세요.",
85
+ );
86
+ }
46
87
  }
47
88
  }
48
89
 
49
90
  // Electron이 Capacitor보다 우선
50
91
  if (clientConfig.electron != null) {
51
- logger.start(`${options.package} (electron) 실행 중...`);
92
+ logger.start(`${targetName} (electron) 실행 중...`);
52
93
  const electron = await Electron.create(pkgDir, clientConfig.electron, clientConfig.exclude);
53
94
  await electron.run(serverUrl);
54
- logger.success(`${options.package} (electron) 실행 완료`);
95
+ logger.success(`${targetName} (electron) 실행 완료`);
55
96
  } else if (clientConfig.capacitor != null) {
56
- logger.start(`${options.package} (capacitor) 실행 중...`);
97
+ logger.start(`${targetName} (capacitor) 실행 중...`);
57
98
  const cap = await Capacitor.create(pkgDir, clientConfig.capacitor, clientConfig.exclude);
58
99
  await cap.run(serverUrl);
59
- logger.success(`${options.package} (capacitor) 실행 완료`);
100
+ logger.success(`${targetName} (capacitor) 실행 완료`);
60
101
  } else {
61
- throw new SdError(
62
- `${options.package}에 capacitor 또는 electron 설정이 없습니다.`,
63
- );
102
+ throw new SdError(`${targetName}에 capacitor 또는 electron 설정이 없습니다.`);
64
103
  }
65
104
  }
105
+
106
+ /** dev 서버가 응답하는지 HTTP GET으로 확인한다. */
107
+ function checkDevServer(url: string): Promise<boolean> {
108
+ return new Promise((resolve) => {
109
+ const req = http.get(url, (res) => {
110
+ res.resume();
111
+ resolve(true);
112
+ });
113
+ req.on("error", () => resolve(false));
114
+ req.setTimeout(3000, () => {
115
+ req.destroy();
116
+ resolve(false);
117
+ });
118
+ });
119
+ }
@@ -2,8 +2,7 @@ import { ESLint } from "eslint";
2
2
  import { createJiti } from "jiti";
3
3
  import path from "path";
4
4
  import { fsx, pathx } from "@simplysm/core-node";
5
- import "@simplysm/core-common";
6
- import { SdError } from "@simplysm/core-common";
5
+ import { env, SdError } from "@simplysm/core-common";
7
6
  import { consola } from "consola";
8
7
 
9
8
  //#region Types
@@ -128,7 +127,7 @@ export async function executeLint(options: LintOptions): Promise<LintResult> {
128
127
 
129
128
  // TIMING 환경변수 설정
130
129
  if (timing) {
131
- process.env["TIMING"] = "1";
130
+ env("TIMING", "1");
132
131
  }
133
132
 
134
133
  // ESLint 설정 로드
@@ -61,7 +61,7 @@ function replaceEnvVariables(str: string, version: string, projectPath: string):
61
61
  if (envName === "PROJECT") {
62
62
  return projectPath;
63
63
  }
64
- return (env[envName] as string | undefined) ?? match;
64
+ return env(envName) ?? match;
65
65
  });
66
66
 
67
67
  // 치환되지 않은 환경변수가 남아있으면 에러 발생
@@ -147,7 +147,7 @@ async function ensureSshAuth(
147
147
  // 개인키 파싱 시도 (암호화 또는 형식 오류 시 Error 반환)
148
148
  const parsed = utils.parseKey(privateKeyData);
149
149
  const isKeyEncrypted = parsed instanceof Error;
150
- const sshAgent = process.env["SSH_AUTH_SOCK"];
150
+ const sshAgent = env("SSH_AUTH_SOCK");
151
151
 
152
152
  // 각 서버에 대해 키 인증 검증
153
153
  for (const [label, target] of sshTargets) {
@@ -483,7 +483,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
483
483
  // sd.config.ts 로드
484
484
  let sdConfig: SdConfig;
485
485
  try {
486
- sdConfig = await loadSdConfig({ cwd, dev: false, options: options.options });
486
+ sdConfig = await loadSdConfig({ cwd, dev: false, opt: options.options });
487
487
  logger.debug("sd.config.ts 로드 완료");
488
488
  } catch (err) {
489
489
  logger.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
@@ -16,7 +16,7 @@ export interface ReplaceDepsOptions {
16
16
  export async function runReplaceDeps(opts: ReplaceDepsOptions): Promise<void> {
17
17
  const cwd = process.cwd();
18
18
 
19
- const sdConfig = await loadSdConfig({ cwd, dev: false, options: opts.options });
19
+ const sdConfig = await loadSdConfig({ cwd, dev: false, opt: opts.options });
20
20
 
21
21
  if (sdConfig.replaceDeps == null) {
22
22
  consola.warn("sd.config.ts에 replaceDeps 설정이 없습니다.");
@@ -97,7 +97,7 @@ export async function executeTypecheck(options: TypecheckOptions): Promise<Typec
97
97
  };
98
98
 
99
99
  // sd.config.ts 로드
100
- const sdConfig = await loadSdConfig({ cwd, dev: false, options: options.options });
100
+ const sdConfig = await loadSdConfig({ cwd, dev: false, opt: options.options });
101
101
  logger.debug("sd.config.ts 로드 완료");
102
102
 
103
103
  // 워크스페이스 패키지 탐색 및 tests/를 설정에 병합
@@ -189,7 +189,6 @@ export async function executeTypecheck(options: TypecheckOptions): Promise<Typec
189
189
  logger.debug(`[${label}] 스택 트레이스:\n${stack}`);
190
190
  }
191
191
  return {
192
- success: false,
193
192
  build: {
194
193
  success: false,
195
194
  errors: [`[${label}] ${message}`],
@@ -3,20 +3,14 @@ import fs from "fs";
3
3
  import module from "module";
4
4
  import { cpx, fsx, pathx } from "@simplysm/core-node";
5
5
  import { consola, LogLevels } from "consola";
6
- import type { SdElectronConfig } from "../sd-config.types.js";
6
+ import type { NpmConfig, SdElectronConfig } from "../sd-config.types.js";
7
7
  import { createEnvBanner } from "../utils/esbuild-config.js";
8
8
 
9
- interface NpmConfig {
10
- name: string;
11
- version: string;
12
- description?: string;
13
- dependencies?: Record<string, string>;
14
- }
15
-
16
9
  export class Electron {
17
10
  private static readonly _logger = consola.withTag("sd:cli:electron");
18
11
 
19
12
  private readonly _electronPath: string;
13
+ private readonly _srcPath: string;
20
14
 
21
15
  private constructor(
22
16
  private readonly _pkgPath: string,
@@ -25,6 +19,7 @@ export class Electron {
25
19
  private readonly _exclude: string[],
26
20
  ) {
27
21
  this._electronPath = pathx.posixResolve(this._pkgPath, ".electron");
22
+ this._srcPath = pathx.posixResolve(this._electronPath, "src");
28
23
  }
29
24
 
30
25
  static async create(
@@ -44,10 +39,6 @@ export class Electron {
44
39
  }
45
40
  }
46
41
 
47
- private _localBin(name: string): string {
48
- return pathx.posixResolve(this._pkgPath, "node_modules/.bin", name);
49
- }
50
-
51
42
  private async _exec(
52
43
  cmd: string,
53
44
  args: string[],
@@ -70,20 +61,26 @@ export class Electron {
70
61
 
71
62
  async initialize(): Promise<void> {
72
63
  Electron._logger.debug("initialize 시작");
73
- const srcPath = pathx.posixResolve(this._electronPath, "src");
74
64
 
75
65
  Electron._logger.debug("package.json 설정 시작");
76
- await this._setupPackageJson(srcPath);
66
+ await this._setupNpmConf();
77
67
  Electron._logger.debug("package.json 설정 완료");
78
68
 
79
- Electron._logger.debug("npm install 시작");
80
- await this._exec("npm", ["install"], srcPath);
81
- Electron._logger.debug("npm install 완료");
69
+ // pnpm-workspace.yaml 생성 (상위 workspace 탐색 차단)
70
+ const workspaceYamlPath = pathx.posixResolve(this._srcPath, "pnpm-workspace.yaml");
71
+ if (!(await fsx.exists(workspaceYamlPath))) {
72
+ await fsx.write(workspaceYamlPath, "");
73
+ }
74
+
75
+ Electron._logger.debug("pnpm install 시작");
76
+ await this._exec("pnpm", ["install"], this._srcPath);
77
+ await this._exec("pnpm", ["approve-builds", "--all"], this._srcPath);
78
+ Electron._logger.debug("pnpm install 완료");
82
79
 
83
80
  const reinstallDeps = this._config.reinstallDependencies ?? [];
84
81
  if (reinstallDeps.length > 0) {
85
82
  Electron._logger.debug(`electron-rebuild 시작 (${reinstallDeps.join(", ")})`);
86
- await this._exec(this._localBin("electron-rebuild"), [], srcPath);
83
+ await this._exec("pnpm", ["exec", "electron-rebuild"], this._srcPath);
87
84
  Electron._logger.debug("electron-rebuild 완료");
88
85
  }
89
86
  Electron._logger.debug("initialize 완료");
@@ -91,9 +88,9 @@ export class Electron {
91
88
 
92
89
  async run(url: string): Promise<void> {
93
90
  Electron._logger.debug(`run 시작 (url: ${url})`);
94
- const srcPath = pathx.posixResolve(this._electronPath, "src");
95
91
 
96
92
  await this.initialize();
93
+ await this._copyPublicAssets();
97
94
 
98
95
  const esbuild = await import("esbuild");
99
96
  const entryPoint = pathx.posixResolve(this._pkgPath, "src/electron-main.ts");
@@ -104,7 +101,6 @@ export class Electron {
104
101
 
105
102
  const builtinModules = module.builtinModules.flatMap((m) => [m, `node:${m}`]);
106
103
  const reinstallDeps = this._config.reinstallDependencies ?? [];
107
- await fsx.mkdir(srcPath);
108
104
 
109
105
  let currentElectron: cpx.SpawnProcess | null = null;
110
106
  let isRestarting = false;
@@ -112,9 +108,10 @@ export class Electron {
112
108
 
113
109
  const spawnElectron = () => {
114
110
  Electron._logger.debug("Electron 프로세스 시작");
115
- currentElectron = cpx.spawn(this._localBin("electron"), ["."], {
116
- cwd: srcPath,
111
+ currentElectron = cpx.spawn("pnpm", ["exec", "electron", "."], {
112
+ cwd: this._srcPath,
117
113
  stdio: "inherit",
114
+ shell: true,
118
115
  reject: false,
119
116
  });
120
117
 
@@ -132,7 +129,7 @@ export class Electron {
132
129
  Electron._logger.debug("esbuild context 생성 시작");
133
130
  const ctx = await esbuild.context({
134
131
  entryPoints: [entryPoint],
135
- outfile: pathx.posixResolve(srcPath, "electron-main.js"),
132
+ outfile: pathx.posixResolve(this._srcPath, "electron-main.js"),
136
133
  platform: "node",
137
134
  target: "node20",
138
135
  format: "cjs",
@@ -205,18 +202,19 @@ export class Electron {
205
202
 
206
203
  async build(outPath: string): Promise<void> {
207
204
  Electron._logger.debug("build 시작");
208
- const srcPath = pathx.posixResolve(this._electronPath, "src");
205
+
206
+ await this.initialize();
209
207
 
210
208
  Electron._logger.debug("메인 프로세스 번들링 시작");
211
- await this._bundleMainProcess(srcPath);
209
+ await this._bundleMainProcess();
212
210
  Electron._logger.debug("메인 프로세스 번들링 완료");
213
211
 
214
212
  Electron._logger.debug("웹 에셋 복사 시작");
215
- await this._copyWebAssets(outPath, srcPath);
213
+ await this._copyWebAssets(outPath);
216
214
  Electron._logger.debug("웹 에셋 복사 완료");
217
215
 
218
216
  Electron._logger.debug("electron-builder 실행 시작");
219
- await this._runElectronBuilder(srcPath);
217
+ await this._runElectronBuilder();
220
218
  Electron._logger.debug("electron-builder 실행 완료");
221
219
 
222
220
  Electron._logger.debug("빌드 산출물 복사 시작");
@@ -230,14 +228,19 @@ export class Electron {
230
228
 
231
229
  //#region Private - Initialization
232
230
 
233
- private async _setupPackageJson(srcPath: string): Promise<void> {
234
- await fsx.mkdir(srcPath);
231
+ private async _setupNpmConf(): Promise<void> {
232
+ await fsx.mkdir(this._srcPath);
233
+
234
+ const mainDeps: Record<string, string | undefined> = {
235
+ ...this._npmConfig.dependencies,
236
+ ...this._npmConfig.devDependencies,
237
+ };
235
238
 
236
239
  const reinstallDeps = this._config.reinstallDependencies ?? [];
237
240
 
238
241
  const dependencies: Record<string, string> = {};
239
242
  for (const dep of reinstallDeps) {
240
- const version = this._npmConfig.dependencies?.[dep];
243
+ const version = mainDeps[dep];
241
244
  if (version != null) {
242
245
  dependencies[dep] = version;
243
246
  }
@@ -245,33 +248,39 @@ export class Electron {
245
248
 
246
249
  for (const excludePkg of this._exclude) {
247
250
  if (!(excludePkg in dependencies)) {
248
- const version = this._npmConfig.dependencies?.[excludePkg];
251
+ const version = mainDeps[excludePkg];
249
252
  if (version != null) {
250
253
  dependencies[excludePkg] = version;
251
254
  }
252
255
  }
253
256
  }
254
257
 
258
+ const devDependencies: Record<string, string> = {};
259
+ devDependencies["electron"] = "^41";
260
+ devDependencies["@electron/rebuild"] = "^4";
261
+ devDependencies["electron-builder"] = "^26";
262
+
255
263
  const packageJson: Record<string, unknown> = {
256
264
  name: this._npmConfig.name.replace(/^@/, "").replace(/\//, "-"),
257
265
  version: this._npmConfig.version,
258
266
  description: this._npmConfig.description,
259
267
  main: "electron-main.js",
260
268
  dependencies,
269
+ devDependencies,
261
270
  };
262
271
 
263
272
  if (this._config.postInstallScript != null) {
264
273
  packageJson["scripts"] = { postinstall: this._config.postInstallScript };
265
274
  }
266
275
 
267
- await fsx.writeJson(pathx.posixResolve(srcPath, "package.json"), packageJson, { space: 2 });
276
+ await fsx.writeJson(pathx.posixResolve(this._srcPath, "package.json"), packageJson, { space: 2 });
268
277
  }
269
278
 
270
279
  //#endregion
271
280
 
272
281
  //#region Private - Bundling
273
282
 
274
- private async _bundleMainProcess(outDir: string): Promise<void> {
283
+ private async _bundleMainProcess(): Promise<void> {
275
284
  const esbuild = await import("esbuild");
276
285
  const entryPoint = pathx.posixResolve(this._pkgPath, "src/electron-main.ts");
277
286
 
@@ -282,14 +291,12 @@ export class Electron {
282
291
  const builtinModules = module.builtinModules.flatMap((m) => [m, `node:${m}`]);
283
292
  const reinstallDeps = this._config.reinstallDependencies ?? [];
284
293
 
285
- await fsx.mkdir(outDir);
286
-
287
294
  const envBanner = createEnvBanner(this._config.env);
288
295
 
289
296
  Electron._logger.debug(`esbuild 번들링: ${entryPoint}`);
290
297
  await esbuild.build({
291
298
  entryPoints: [entryPoint],
292
- outfile: pathx.posixResolve(outDir, "electron-main.js"),
299
+ outfile: pathx.posixResolve(this._srcPath, "electron-main.js"),
293
300
  platform: "node",
294
301
  target: "node20",
295
302
  format: "cjs",
@@ -301,15 +308,27 @@ export class Electron {
301
308
 
302
309
  //#endregion
303
310
 
311
+ private async _copyPublicAssets(): Promise<void> {
312
+ const publicPath = pathx.posixResolve(this._pkgPath, "public");
313
+ if (!(await fsx.exists(publicPath))) return;
314
+
315
+ const items = await fsx.readdir(publicPath);
316
+ for (const item of items) {
317
+ const source = pathx.posixResolve(publicPath, item);
318
+ const dest = pathx.posixResolve(this._srcPath, item);
319
+ await fsx.copy(source, dest);
320
+ }
321
+ }
322
+
304
323
  //#region Private - Build
305
324
 
306
- private async _copyWebAssets(outPath: string, srcPath: string): Promise<void> {
325
+ private async _copyWebAssets(outPath: string): Promise<void> {
307
326
  const items = await fsx.readdir(outPath);
308
327
  for (const item of items) {
309
328
  if (item === "electron") continue;
310
329
 
311
330
  const source = pathx.posixResolve(outPath, item);
312
- const dest = pathx.posixResolve(srcPath, item);
331
+ const dest = pathx.posixResolve(this._srcPath, item);
313
332
  await fsx.copy(source, dest);
314
333
  }
315
334
  }
@@ -331,7 +350,7 @@ export class Electron {
331
350
  }
332
351
  }
333
352
 
334
- private async _runElectronBuilder(srcPath: string): Promise<void> {
353
+ private async _runElectronBuilder(): Promise<void> {
335
354
  if (!Electron._canCreateSymlink()) {
336
355
  throw new Error(
337
356
  "Symlink 생성 권한이 필요합니다. Windows 개발자 모드를 활성화하세요.",
@@ -349,7 +368,7 @@ export class Electron {
349
368
  },
350
369
  nsis: this._config.nsisOptions ?? {},
351
370
  directories: {
352
- app: srcPath,
371
+ app: this._srcPath,
353
372
  output: distPath,
354
373
  },
355
374
  removePackageScripts: false,
@@ -366,9 +385,9 @@ export class Electron {
366
385
 
367
386
  Electron._logger.debug(`electron-builder 설정: ${configFilePath}`);
368
387
  await this._exec(
369
- this._localBin("electron-builder"),
370
- ["--win", "--config", configFilePath],
371
- this._pkgPath,
388
+ "pnpm",
389
+ ["exec", "electron-builder", "--win", "--config", configFilePath],
390
+ this._srcPath,
372
391
  );
373
392
  }
374
393
 
@@ -112,7 +112,7 @@ export abstract class BaseEngine<
112
112
  logger.debug(`[${this._pkg.name}] run 시작 (js: ${output.js}, dts: ${output.dts}, env: ${output.env ?? "none"})`);
113
113
  this._createWorker();
114
114
  const result = await this._callBuild(output);
115
- logger.debug(`[${this._pkg.name}] run 완료 (success: ${result.success})`);
115
+ logger.debug(`[${this._pkg.name}] run 완료 (success: ${result.build.success})`);
116
116
  return result;
117
117
  }
118
118
 
@@ -53,7 +53,6 @@ export class NgtscEngine extends BaseEngine<
53
53
  });
54
54
 
55
55
  return {
56
- success: result.build.success,
57
56
  build: {
58
57
  success: result.build.success,
59
58
  errors: result.build.errors ?? [],
@@ -58,7 +58,6 @@ export class ServerEsbuildEngine extends BaseEngine<
58
58
  });
59
59
 
60
60
  return {
61
- success: result.build.success,
62
61
  build: {
63
62
  success: result.build.success,
64
63
  errors: result.build.errors ?? [],
@@ -54,7 +54,6 @@ export class TscEngine extends BaseEngine<
54
54
  });
55
55
 
56
56
  return {
57
- success: result.build.success,
58
57
  build: {
59
58
  success: result.build.success,
60
59
  errors: result.build.errors ?? [],
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "path";
1
3
  import { Worker, type WorkerProxy } from "@simplysm/core-node";
2
4
  import { consola } from "consola";
3
5
  import type * as ClientWorkerModule from "../workers/client.worker";
@@ -73,6 +75,7 @@ export class ViteEngine implements BuildEngine {
73
75
  configs: this._pkg.config.configs,
74
76
  browserSupport: this._pkg.config.browserSupport,
75
77
  enableLint: output.lint,
78
+ pwa: this._pkg.config.pwa,
76
79
  exclude: this._pkg.config.exclude,
77
80
  outDir: this._outDir,
78
81
  base: this._base,
@@ -80,7 +83,6 @@ export class ViteEngine implements BuildEngine {
80
83
 
81
84
  logger.debug(`[${this._pkg.name}] ViteEngine.run 완료 (success: ${result.success})`);
82
85
  return {
83
- success: result.success,
84
86
  build: {
85
87
  success: result.success,
86
88
  errors: result.errors ?? [],
@@ -185,6 +187,7 @@ export class ViteEngine implements BuildEngine {
185
187
  replaceDeps: this._replaceDeps,
186
188
  browserSupport: this._pkg.config.browserSupport,
187
189
  enableLint: output.lint,
190
+ pwa: this._pkg.config.pwa,
188
191
  exclude: this._pkg.config.exclude,
189
192
  });
190
193
  }
@@ -194,6 +197,11 @@ export class ViteEngine implements BuildEngine {
194
197
  */
195
198
  async stop(): Promise<void> {
196
199
  logger.debug(`[${this._pkg.name}] ViteEngine stop 시작`);
200
+
201
+ // .dev-port 파일 삭제
202
+ const portFile = path.join(this._pkg.dir, "dist", ".dev-port");
203
+ try { fs.unlinkSync(portFile); } catch { /* 파일 없으면 무시 */ }
204
+
197
205
  await stopEngineWorker(this._worker, this._isWatchMode);
198
206
  this._worker = undefined;
199
207
  logger.debug(`[${this._pkg.name}] ViteEngine stop 완료`);
@@ -10,16 +10,6 @@ import type { BuildEngine, BuildPackageInfo, ClientPackageInfo, ServerPackageInf
10
10
 
11
11
  const logger = consola.withTag("sd:cli:engine");
12
12
 
13
- export { BaseEngine } from "./BaseEngine";
14
- export type { BaseEngineOptions, CommonBuildWorkerEvents, CommonBuildWorkerModule } from "./BaseEngine";
15
- export { NgtscEngine } from "./NgtscEngine";
16
- export type { NgtscEngineOptions } from "./NgtscEngine";
17
- export { ServerEsbuildEngine } from "./ServerEsbuildEngine";
18
- export type { ServerEsbuildEngineOptions } from "./ServerEsbuildEngine";
19
- export { TscEngine } from "./TscEngine";
20
- export type { TscEngineOptions } from "./TscEngine";
21
- export { ViteEngine } from "./ViteEngine";
22
- export type { ViteEngineOptions } from "./ViteEngine";
23
13
  export type { BuildEngine, BuildOutput, BuildPackageInfo, ClientPackageInfo, EngineResult, PackageInfo, ServerPackageInfo } from "./types";
24
14
 
25
15
  /**
@@ -45,7 +45,6 @@ export interface BuildOutput {
45
45
  * BuildEngine.run() 반환값
46
46
  */
47
47
  export interface EngineResult {
48
- success: boolean;
49
48
  build: {
50
49
  success: boolean;
51
50
  errors: string[];
@@ -7,7 +7,6 @@
7
7
  export class SignalHandler {
8
8
  private _terminateResolver: (() => void) | null = null;
9
9
  private readonly _terminatePromise: Promise<void>;
10
- private _terminated = false;
11
10
 
12
11
  constructor() {
13
12
  this._terminatePromise = new Promise((resolve) => {
@@ -17,8 +16,8 @@ export class SignalHandler {
17
16
  const handler = () => {
18
17
  process.off("SIGINT", handler);
19
18
  process.off("SIGTERM", handler);
20
- this._terminated = true;
21
19
  this._terminateResolver?.();
20
+ this._terminateResolver = null;
22
21
  };
23
22
 
24
23
  process.on("SIGINT", handler);
@@ -32,21 +31,12 @@ export class SignalHandler {
32
31
  return this._terminatePromise;
33
32
  }
34
33
 
35
- /**
36
- * 종료 여부를 확인한다
37
- */
38
- isTerminated(): boolean {
39
- return this._terminated;
40
- }
41
-
42
34
  /**
43
35
  * 프로그래밍 방식으로 종료를 요청한다
44
- * (테스트나 외부에서 종료를 트리거할 때 사용)
36
+ * (테스트에서 종료를 트리거할 때 사용)
45
37
  */
46
38
  requestTermination(): void {
47
- if (!this._terminated) {
48
- this._terminated = true;
49
- this._terminateResolver?.();
50
- }
39
+ this._terminateResolver?.();
40
+ this._terminateResolver = null;
51
41
  }
52
42
  }