@simplysm/sd-cli 13.0.0-beta.46 → 13.0.0-beta.48

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 (102) hide show
  1. package/README.md +3 -3
  2. package/dist/builders/BaseBuilder.js.map +0 -1
  3. package/dist/builders/DtsBuilder.js.map +0 -1
  4. package/dist/builders/LibraryBuilder.js.map +0 -1
  5. package/dist/builders/index.js.map +0 -1
  6. package/dist/builders/types.js.map +0 -1
  7. package/dist/capacitor/capacitor.js.map +0 -1
  8. package/dist/commands/add-client.js.map +0 -1
  9. package/dist/commands/add-server.js.map +0 -1
  10. package/dist/commands/build.js.map +0 -1
  11. package/dist/commands/dev.js.map +0 -1
  12. package/dist/commands/device.js.map +0 -1
  13. package/dist/commands/init.js.map +0 -1
  14. package/dist/commands/lint.js.map +0 -1
  15. package/dist/commands/publish.js.map +0 -1
  16. package/dist/commands/typecheck.js.map +0 -1
  17. package/dist/commands/watch.js.map +0 -1
  18. package/dist/electron/electron.js.map +0 -1
  19. package/dist/index.js.map +0 -1
  20. package/dist/infra/ResultCollector.js.map +0 -1
  21. package/dist/infra/SignalHandler.js.map +0 -1
  22. package/dist/infra/WorkerManager.js.map +0 -1
  23. package/dist/infra/index.js.map +0 -1
  24. package/dist/orchestrators/WatchOrchestrator.js.map +0 -1
  25. package/dist/orchestrators/index.js.map +0 -1
  26. package/dist/sd-cli.js.map +0 -1
  27. package/dist/sd-config.types.js.map +0 -1
  28. package/dist/utils/build-env.js.map +0 -1
  29. package/dist/utils/config-editor.js.map +0 -1
  30. package/dist/utils/copy-src.js.map +0 -1
  31. package/dist/utils/esbuild-config.d.ts +1 -0
  32. package/dist/utils/esbuild-config.d.ts.map +1 -1
  33. package/dist/utils/esbuild-config.js +2 -1
  34. package/dist/utils/esbuild-config.js.map +1 -2
  35. package/dist/utils/listr-manager.js.map +0 -1
  36. package/dist/utils/output-utils.js.map +0 -1
  37. package/dist/utils/package-utils.js.map +0 -1
  38. package/dist/utils/replace-deps.js.map +0 -1
  39. package/dist/utils/sd-config.js.map +0 -1
  40. package/dist/utils/spawn.js.map +0 -1
  41. package/dist/utils/tailwind-config-deps.js.map +0 -1
  42. package/dist/utils/template.js.map +0 -1
  43. package/dist/utils/tsconfig.js.map +0 -1
  44. package/dist/utils/typecheck-serialization.js.map +0 -1
  45. package/dist/utils/vite-config.js.map +0 -1
  46. package/dist/utils/worker-events.js.map +0 -1
  47. package/dist/workers/client.worker.js.map +0 -1
  48. package/dist/workers/dts.worker.js.map +0 -1
  49. package/dist/workers/library.worker.js.map +0 -1
  50. package/dist/workers/server-runtime.worker.js.map +0 -1
  51. package/dist/workers/server.worker.js.map +0 -1
  52. package/package.json +5 -4
  53. package/src/builders/BaseBuilder.ts +141 -0
  54. package/src/builders/DtsBuilder.ts +138 -0
  55. package/src/builders/LibraryBuilder.ts +161 -0
  56. package/src/builders/index.ts +4 -0
  57. package/src/builders/types.ts +55 -0
  58. package/src/capacitor/capacitor.ts +827 -0
  59. package/src/commands/add-client.ts +135 -0
  60. package/src/commands/add-server.ts +150 -0
  61. package/src/commands/build.ts +475 -0
  62. package/src/commands/dev.ts +602 -0
  63. package/src/commands/device.ts +151 -0
  64. package/src/commands/init.ts +104 -0
  65. package/src/commands/lint.ts +216 -0
  66. package/src/commands/publish.ts +836 -0
  67. package/src/commands/typecheck.ts +329 -0
  68. package/src/commands/watch.ts +38 -0
  69. package/src/electron/electron.ts +329 -0
  70. package/src/index.ts +1 -0
  71. package/src/infra/ResultCollector.ts +81 -0
  72. package/src/infra/SignalHandler.ts +52 -0
  73. package/src/infra/WorkerManager.ts +65 -0
  74. package/src/infra/index.ts +3 -0
  75. package/src/orchestrators/WatchOrchestrator.ts +211 -0
  76. package/src/orchestrators/index.ts +1 -0
  77. package/src/sd-cli.ts +307 -0
  78. package/src/sd-config.types.ts +271 -0
  79. package/src/utils/build-env.ts +12 -0
  80. package/src/utils/config-editor.ts +131 -0
  81. package/src/utils/copy-src.ts +60 -0
  82. package/src/utils/esbuild-config.ts +263 -0
  83. package/src/utils/listr-manager.ts +89 -0
  84. package/src/utils/output-utils.ts +61 -0
  85. package/src/utils/package-utils.ts +63 -0
  86. package/src/utils/replace-deps.ts +163 -0
  87. package/src/utils/sd-config.ts +44 -0
  88. package/src/utils/spawn.ts +79 -0
  89. package/src/utils/tailwind-config-deps.ts +95 -0
  90. package/src/utils/template.ts +51 -0
  91. package/src/utils/tsconfig.ts +111 -0
  92. package/src/utils/typecheck-serialization.ts +82 -0
  93. package/src/utils/vite-config.ts +184 -0
  94. package/src/utils/worker-events.ts +102 -0
  95. package/src/workers/client.worker.ts +236 -0
  96. package/src/workers/dts.worker.ts +416 -0
  97. package/src/workers/library.worker.ts +245 -0
  98. package/src/workers/server-runtime.worker.ts +154 -0
  99. package/src/workers/server.worker.ts +435 -0
  100. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  101. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  102. package/templates/init/package.json.hbs +3 -3
@@ -0,0 +1,329 @@
1
+ import ts from "typescript";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { Listr } from "listr2";
5
+ import { pathPosix, pathFilterByTargets, Worker, type WorkerProxy } from "@simplysm/core-node";
6
+ import "@simplysm/core-common";
7
+ import { consola, LogLevels } from "consola";
8
+ import type { SdConfig } from "../sd-config.types";
9
+ import { parseRootTsconfig, type TypecheckEnv } from "../utils/tsconfig";
10
+ import { loadSdConfig } from "../utils/sd-config";
11
+ import { deserializeDiagnostic } from "../utils/typecheck-serialization";
12
+ import type { DtsBuildInfo, DtsBuildResult } from "../workers/dts.worker";
13
+ import type * as DtsWorkerModule from "../workers/dts.worker";
14
+
15
+ //#region Types
16
+
17
+ /**
18
+ * TypeScript 타입체크 옵션
19
+ */
20
+ export interface TypecheckOptions {
21
+ /** 타입체크할 경로 필터 (예: `packages/core-common`). 빈 배열이면 tsconfig.json에 정의된 모든 파일 대상 */
22
+ targets: string[];
23
+ /** sd.config.ts에 전달할 추가 옵션 */
24
+ options: string[];
25
+ }
26
+
27
+ // 패키지 정보 (packages/* 하위 파일 분류용)
28
+ interface PackageInfo {
29
+ name: string;
30
+ dir: string;
31
+ envs: TypecheckEnv[]; // neutral은 ["node", "browser"], 나머지는 단일 환경
32
+ }
33
+
34
+ // 타입체크 작업 정보 (내부 사용)
35
+ interface TypecheckTask {
36
+ /** 작업 표시 이름 (예: "패키지: core-common [node]") */
37
+ displayName: string;
38
+ /** dts.worker에 전달할 정보 */
39
+ buildInfo: DtsBuildInfo;
40
+ }
41
+
42
+ //#endregion
43
+
44
+ //#region Utilities
45
+
46
+ /** 경로 분류용 정규표현식 */
47
+ const PATH_PATTERNS = {
48
+ /** packages/{pkg}/... */
49
+ PACKAGE: /^packages\/([^/]+)\//,
50
+ } as const;
51
+
52
+ /**
53
+ * 패키지 타겟을 타입체크 환경 목록으로 변환합니다.
54
+ * - node/browser: 해당 환경만
55
+ * - neutral: node + browser 둘 다
56
+ * - client: browser로 처리
57
+ * @param target 패키지 빌드 타겟
58
+ * @returns 타입체크 환경 목록
59
+ */
60
+ function toTypecheckEnvs(target: string | undefined): TypecheckEnv[] {
61
+ if (target === "node") return ["node"];
62
+ if (target === "browser" || target === "client") return ["browser"];
63
+ // neutral 또는 미지정은 둘 다
64
+ return ["node", "browser"];
65
+ }
66
+
67
+ /**
68
+ * 파일 경로에서 패키지 정보를 추출합니다.
69
+ * scripts 타겟 패키지는 제외합니다.
70
+ * @internal 테스트용으로 export
71
+ */
72
+ export function extractPackages(fileNames: string[], cwd: string, config: SdConfig): Map<string, PackageInfo> {
73
+ const packages = new Map<string, PackageInfo>();
74
+
75
+ for (const fileName of fileNames) {
76
+ const relativePath = pathPosix(path.relative(cwd, fileName));
77
+
78
+ // packages/{pkg}/...
79
+ const packageMatch = relativePath.match(PATH_PATTERNS.PACKAGE);
80
+ if (packageMatch) {
81
+ const pkgName = packageMatch[1];
82
+ // scripts 타겟 패키지는 제외
83
+ if (config.packages[pkgName]?.target === "scripts") continue;
84
+
85
+ if (!packages.has(pkgName)) {
86
+ packages.set(pkgName, {
87
+ name: pkgName,
88
+ dir: path.resolve(cwd, "packages", pkgName),
89
+ envs: toTypecheckEnvs(config.packages[pkgName]?.target),
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ return packages;
96
+ }
97
+
98
+ /**
99
+ * 패키지 정보로부터 타입체크 작업 목록을 생성합니다.
100
+ * neutral 패키지는 node/browser 두 환경으로 분리하여 각각 체크합니다.
101
+ * @param packages 패키지 정보 맵
102
+ * @param cwd 현재 작업 디렉토리
103
+ * @returns 타입체크 작업 정보 배열
104
+ */
105
+ function createTypecheckTasks(packages: Map<string, PackageInfo>, cwd: string): TypecheckTask[] {
106
+ const tasks: TypecheckTask[] = [];
107
+
108
+ // packages/* - 각 env마다 별도 task 생성
109
+ for (const info of packages.values()) {
110
+ for (const env of info.envs) {
111
+ const envSuffix = info.envs.length > 1 ? ` [${env}]` : "";
112
+ tasks.push({
113
+ displayName: `패키지: ${info.name}${envSuffix}`,
114
+ buildInfo: {
115
+ name: info.name,
116
+ cwd,
117
+ pkgDir: info.dir,
118
+ env,
119
+ emit: false, // 타입체크만 수행 (dts 생성 안 함)
120
+ },
121
+ });
122
+ }
123
+ }
124
+
125
+ return tasks;
126
+ }
127
+
128
+ //#endregion
129
+
130
+ //#region Main
131
+
132
+ /**
133
+ * TypeScript 타입체크를 실행한다.
134
+ *
135
+ * - `tsconfig.json`을 로드하여 컴파일러 옵션 적용
136
+ * - `sd.config.ts`를 로드하여 패키지별 타겟 정보 확인 (없으면 기본값 사용)
137
+ * - Worker threads를 사용하여 실제 병렬 타입체크 수행
138
+ * - incremental 컴파일 사용 (`.cache/typecheck-{env}.tsbuildinfo`)
139
+ * - listr2를 사용하여 진행 상황 표시
140
+ * - 에러 발생 시 `process.exitCode = 1` 설정
141
+ *
142
+ * @param options - 타입체크 실행 옵션
143
+ * @returns 완료 시 resolve. 에러 발견 시 `process.exitCode`를 1로 설정하고 resolve (throw하지 않음)
144
+ */
145
+ export async function runTypecheck(options: TypecheckOptions): Promise<void> {
146
+ const { targets } = options;
147
+ const cwd = process.cwd();
148
+ const logger = consola.withTag("sd:cli:typecheck");
149
+
150
+ logger.debug("타입체크 시작", { targets });
151
+
152
+ const formatHost: ts.FormatDiagnosticsHost = {
153
+ getCanonicalFileName: (f) => f,
154
+ getCurrentDirectory: () => cwd,
155
+ getNewLine: () => ts.sys.newLine,
156
+ };
157
+
158
+ // tsconfig.json 로드 및 파싱
159
+ let parsedConfig: ts.ParsedCommandLine;
160
+ try {
161
+ parsedConfig = parseRootTsconfig(cwd);
162
+ } catch (err) {
163
+ consola.error(err instanceof Error ? err.message : err);
164
+ process.exitCode = 1;
165
+ return;
166
+ }
167
+
168
+ // sd.config.ts 로드
169
+ let sdConfig: SdConfig;
170
+ try {
171
+ sdConfig = await loadSdConfig({ cwd, dev: false, opt: options.options });
172
+ logger.debug("sd.config.ts 로드 완료");
173
+ } catch (err) {
174
+ // sd.config.ts가 없거나 로드 실패 시 기본값 사용
175
+ sdConfig = { packages: {} };
176
+ logger.debug("sd.config.ts 로드 실패, 기본값 사용", err);
177
+ }
178
+
179
+ // targets가 지정되면 fileNames 필터링
180
+ const fileNames = pathFilterByTargets(parsedConfig.fileNames, targets, cwd);
181
+
182
+ if (fileNames.length === 0) {
183
+ process.stdout.write("✔ 타입체크할 파일이 없습니다.\n");
184
+ logger.info("타입체크할 파일 없음");
185
+ return;
186
+ }
187
+
188
+ // 패키지 정보 추출
189
+ const packages = extractPackages(fileNames, cwd, sdConfig);
190
+ logger.debug("패키지 추출 완료", {
191
+ packageCount: packages.size,
192
+ packages: [...packages.keys()],
193
+ });
194
+
195
+ // 타입체크 작업 생성
196
+ const tasks = createTypecheckTasks(packages, cwd);
197
+
198
+ if (tasks.length === 0) {
199
+ process.stdout.write("✔ 타입체크할 패키지가 없습니다.\n");
200
+ return;
201
+ }
202
+
203
+ // 동시성 설정: CPU 코어의 7/8만 사용 (일반적인 병렬 빌드 도구의 기본값, OS/다른 프로세스 여유분 확보, 최소 1, 작업 수 이하)
204
+ const maxConcurrency = Math.max(Math.floor((os.cpus().length * 7) / 8), 1);
205
+ const concurrency = Math.min(maxConcurrency, tasks.length);
206
+ logger.debug("동시성 설정", { concurrency, maxConcurrency, taskCount: tasks.length });
207
+
208
+ // Worker 풀 생성 (작업 수만큼만 생성)
209
+ const workerPath = import.meta.resolve("../workers/dts.worker");
210
+ const workers: WorkerProxy<typeof DtsWorkerModule>[] = [];
211
+ for (let i = 0; i < concurrency; i++) {
212
+ workers.push(Worker.create<typeof DtsWorkerModule>(workerPath));
213
+ }
214
+
215
+ // 결과 수집용
216
+ const allResults: { displayName: string; result: DtsBuildResult }[] = [];
217
+
218
+ // listr2-Worker 연동 패턴:
219
+ // 1. listr2의 각 task는 Promise를 반환하고, 해당 Promise가 resolve되면 task가 완료됨
220
+ // 2. taskResolvers 맵에 task별 resolve 함수를 저장
221
+ // 3. Worker가 작업 완료 시 해당 task의 resolver를 호출하여 listr2 UI 업데이트
222
+ // 4. Worker 풀은 독립적으로 작업 큐에서 task를 가져와 실행
223
+ const taskResolvers = new Map<string, () => void>();
224
+
225
+ try {
226
+ // 작업 큐
227
+ let taskIndex = 0;
228
+
229
+ // Worker에서 작업 실행
230
+ async function runNextTask(worker: WorkerProxy<typeof DtsWorkerModule>): Promise<void> {
231
+ while (taskIndex < tasks.length) {
232
+ const currentIndex = taskIndex++;
233
+ const task = tasks[currentIndex];
234
+
235
+ try {
236
+ const result = await worker.buildDts(task.buildInfo);
237
+
238
+ allResults.push({ displayName: task.displayName, result });
239
+ } catch (err) {
240
+ // Worker 오류 로깅 및 결과로 변환
241
+ logger.error(`Worker 오류: ${task.displayName}`, {
242
+ error: err instanceof Error ? err.message : String(err),
243
+ });
244
+ allResults.push({
245
+ displayName: task.displayName,
246
+ result: {
247
+ success: false,
248
+ errors: [err instanceof Error ? err.message : String(err)],
249
+ diagnostics: [],
250
+ errorCount: 1,
251
+ warningCount: 0,
252
+ },
253
+ });
254
+ } finally {
255
+ // 성공/실패 모두 task 완료 처리
256
+ taskResolvers.get(task.displayName)?.();
257
+ }
258
+ }
259
+ }
260
+
261
+ // listr2로 진행 상황 표시
262
+ const listr = new Listr(
263
+ tasks.map((task) => ({
264
+ title: task.displayName,
265
+ task: () =>
266
+ new Promise<void>((resolve) => {
267
+ taskResolvers.set(task.displayName, resolve);
268
+ }),
269
+ })),
270
+ {
271
+ concurrent: concurrency,
272
+ exitOnError: false,
273
+ renderer: consola.level >= LogLevels.debug ? "verbose" : "default",
274
+ },
275
+ );
276
+
277
+ // 병렬로 모든 worker 실행
278
+ const workerPromises = workers.map((worker) => runNextTask(worker));
279
+
280
+ // listr와 worker 동시 실행
281
+ await Promise.all([listr.run(), ...workerPromises]);
282
+ } finally {
283
+ // 미해결 resolver 정리 (타임아웃/비정상 종료 대비)
284
+ for (const resolver of taskResolvers.values()) {
285
+ resolver();
286
+ }
287
+ // Worker 종료 (성공/실패 모두)
288
+ await Promise.all(workers.map((w) => w.terminate()));
289
+ }
290
+
291
+ // 결과 출력
292
+ const allDiagnostics: ts.Diagnostic[] = [];
293
+ let totalErrorCount = 0;
294
+ let totalWarningCount = 0;
295
+ const fileCache = new Map<string, string>(); // 파일 내용 캐시 (동일 파일 중복 읽기 방지)
296
+ for (const { result } of allResults) {
297
+ totalErrorCount += result.errorCount;
298
+ totalWarningCount += result.warningCount;
299
+ for (const serialized of result.diagnostics) {
300
+ allDiagnostics.push(deserializeDiagnostic(serialized, fileCache));
301
+ }
302
+ }
303
+
304
+ if (totalErrorCount > 0) {
305
+ logger.error("타입체크 에러 발생", {
306
+ errorCount: totalErrorCount,
307
+ warningCount: totalWarningCount,
308
+ });
309
+ } else if (totalWarningCount > 0) {
310
+ logger.info("타입체크 완료 (경고 있음)", {
311
+ errorCount: totalErrorCount,
312
+ warningCount: totalWarningCount,
313
+ });
314
+ } else {
315
+ logger.info("타입체크 완료", { errorCount: totalErrorCount, warningCount: totalWarningCount });
316
+ }
317
+
318
+ if (allDiagnostics.length > 0) {
319
+ const uniqueDiagnostics = ts.sortAndDeduplicateDiagnostics(allDiagnostics);
320
+ const message = ts.formatDiagnosticsWithColorAndContext(uniqueDiagnostics, formatHost);
321
+ process.stdout.write(message);
322
+ }
323
+
324
+ if (totalErrorCount > 0) {
325
+ process.exitCode = 1;
326
+ }
327
+ }
328
+
329
+ //#endregion
@@ -0,0 +1,38 @@
1
+ // packages/cli/src/commands/watch.ts
2
+ import { WatchOrchestrator, type WatchOrchestratorOptions } from "../orchestrators/WatchOrchestrator";
3
+
4
+ /**
5
+ * Watch 명령 옵션 (하위 호환성)
6
+ */
7
+ export interface WatchOptions {
8
+ targets: string[];
9
+ options: string[];
10
+ }
11
+
12
+ /**
13
+ * Library 패키지를 watch 모드로 빌드한다.
14
+ *
15
+ * - `sd.config.ts`를 로드하여 패키지별 빌드 타겟 정보 확인 (필수)
16
+ * - `node`/`browser`/`neutral` 타겟: esbuild watch 모드로 빌드 + .d.ts 생성
17
+ * - 파일 변경 시 자동 리빌드
18
+ * - SIGINT/SIGTERM 시그널로 종료
19
+ *
20
+ * @param options - watch 실행 옵션
21
+ * @returns 종료 시그널 수신 시 resolve
22
+ */
23
+ export async function runWatch(options: WatchOptions): Promise<void> {
24
+ const orchestratorOptions: WatchOrchestratorOptions = {
25
+ targets: options.targets,
26
+ options: options.options,
27
+ };
28
+
29
+ const orchestrator = new WatchOrchestrator(orchestratorOptions);
30
+
31
+ try {
32
+ await orchestrator.initialize();
33
+ await orchestrator.start();
34
+ await orchestrator.awaitTermination();
35
+ } finally {
36
+ await orchestrator.shutdown();
37
+ }
38
+ }
@@ -0,0 +1,329 @@
1
+ import path from "path";
2
+ import os from "os";
3
+ import fs from "fs";
4
+ import module from "module";
5
+ import { fsExists, fsMkdir, fsCopy, fsReaddir, fsReadJson, fsWriteJson } from "@simplysm/core-node";
6
+ import { consola } from "consola";
7
+ import type { SdElectronConfig } from "../sd-config.types";
8
+ import { spawn } from "../utils/spawn";
9
+
10
+ /**
11
+ * package.json 타입
12
+ */
13
+ interface NpmConfig {
14
+ name: string;
15
+ version: string;
16
+ description?: string;
17
+ dependencies?: Record<string, string>;
18
+ }
19
+
20
+ /**
21
+ * Electron 프로젝트 관리 클래스
22
+ *
23
+ * - Electron 프로젝트 초기화 (package.json 생성, 의존성 설치, native 모듈 재빌드)
24
+ * - Windows 실행 파일 빌드 (electron-builder)
25
+ * - 개발 모드 실행 (Vite dev server URL 로드)
26
+ */
27
+ export class Electron {
28
+ private static readonly _logger = consola.withTag("sd:cli:electron");
29
+
30
+ private readonly _electronPath: string;
31
+ private readonly _npmConfig: NpmConfig;
32
+
33
+ private constructor(
34
+ private readonly _pkgPath: string,
35
+ private readonly _config: SdElectronConfig,
36
+ npmConfig: NpmConfig,
37
+ ) {
38
+ this._npmConfig = npmConfig;
39
+ this._electronPath = path.resolve(this._pkgPath, ".electron");
40
+ }
41
+
42
+ /**
43
+ * Electron 인스턴스 생성 (설정 검증 포함)
44
+ */
45
+ static async create(pkgPath: string, config: SdElectronConfig): Promise<Electron> {
46
+ Electron._validateConfig(config);
47
+
48
+ const npmConfig = await fsReadJson<NpmConfig>(path.resolve(pkgPath, "package.json"));
49
+ return new Electron(pkgPath, config, npmConfig);
50
+ }
51
+
52
+ /**
53
+ * 설정 검증
54
+ */
55
+ private static _validateConfig(config: SdElectronConfig): void {
56
+ if (typeof config.appId !== "string" || config.appId.trim() === "") {
57
+ throw new Error("electron.appId는 필수입니다.");
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 명령어 실행 (로깅 포함)
63
+ */
64
+ private async _exec(cmd: string, args: string[], cwd: string, env?: Record<string, string>): Promise<string> {
65
+ Electron._logger.debug(`실행 명령: ${cmd} ${args.join(" ")}`);
66
+ const result = await spawn(cmd, args, { cwd, env });
67
+ Electron._logger.debug(`실행 결과: ${result}`);
68
+ return result;
69
+ }
70
+
71
+ //#region Public Methods
72
+
73
+ /**
74
+ * Electron 프로젝트 초기화
75
+ *
76
+ * 1. .electron/src/package.json 생성
77
+ * 2. npm install 실행
78
+ * 3. electron-rebuild 실행 (native 모듈 재빌드)
79
+ */
80
+ async initialize(): Promise<void> {
81
+ const srcPath = path.resolve(this._electronPath, "src");
82
+
83
+ // 1. package.json 생성
84
+ await this._setupPackageJson(srcPath);
85
+
86
+ // 2. npm install
87
+ await this._exec("npm", ["install"], srcPath);
88
+
89
+ // 3. native 모듈 재빌드
90
+ const reinstallDeps = this._config.reinstallDependencies ?? [];
91
+ if (reinstallDeps.length > 0) {
92
+ await this._exec("npx", ["electron-rebuild"], srcPath);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * 프로덕션 빌드
98
+ *
99
+ * 1. esbuild로 electron-main.ts 번들링
100
+ * 2. 웹 에셋 복사
101
+ * 3. electron-builder 설정 생성
102
+ * 4. electron-builder 실행
103
+ * 5. 결과물 복사
104
+ */
105
+ async build(outPath: string): Promise<void> {
106
+ const srcPath = path.resolve(this._electronPath, "src");
107
+
108
+ // 1. electron-main.ts 번들링
109
+ await this._bundleMainProcess(srcPath);
110
+
111
+ // 2. 웹 에셋 복사 (outPath → .electron/src/)
112
+ await this._copyWebAssets(outPath, srcPath);
113
+
114
+ // 3. electron-builder 설정 생성 + 실행
115
+ await this._runElectronBuilder(srcPath);
116
+
117
+ // 4. 결과물 복사
118
+ await this._copyBuildOutput(outPath);
119
+ }
120
+
121
+ /**
122
+ * 개발 모드 실행
123
+ *
124
+ * 1. esbuild로 electron-main.ts 번들링
125
+ * 2. dist/electron/package.json 생성
126
+ * 3. npx electron . 실행
127
+ */
128
+ async run(url?: string): Promise<void> {
129
+ const electronRunPath = path.resolve(this._pkgPath, "dist/electron");
130
+
131
+ // 1. electron-main.ts 번들링
132
+ await this._bundleMainProcess(electronRunPath);
133
+
134
+ // 2. package.json 생성
135
+ await fsMkdir(electronRunPath);
136
+ await fsWriteJson(
137
+ path.resolve(electronRunPath, "package.json"),
138
+ { name: this._npmConfig.name, version: this._npmConfig.version, main: "electron-main.js" },
139
+ { space: 2 },
140
+ );
141
+
142
+ // 3. Electron 실행
143
+ const runEnv: Record<string, string> = {
144
+ NODE_ENV: "development",
145
+ ...this._config.env,
146
+ };
147
+
148
+ if (url != null) {
149
+ runEnv["ELECTRON_DEV_URL"] = url;
150
+ }
151
+
152
+ await this._exec("npx", ["electron", "."], electronRunPath, runEnv);
153
+ }
154
+
155
+ //#endregion
156
+
157
+ //#region Private - 초기화
158
+
159
+ /**
160
+ * .electron/src/package.json 생성
161
+ */
162
+ private async _setupPackageJson(srcPath: string): Promise<void> {
163
+ await fsMkdir(srcPath);
164
+
165
+ const reinstallDeps = this._config.reinstallDependencies ?? [];
166
+
167
+ // 메인 package.json에서 reinstallDependencies에 해당하는 버전 추출
168
+ const dependencies: Record<string, string> = {};
169
+ for (const dep of reinstallDeps) {
170
+ const version = this._npmConfig.dependencies?.[dep];
171
+ if (version != null) {
172
+ dependencies[dep] = version;
173
+ }
174
+ }
175
+
176
+ const packageJson: Record<string, unknown> = {
177
+ name: this._npmConfig.name.replace(/^@/, "").replace(/\//, "-"),
178
+ version: this._npmConfig.version,
179
+ description: this._npmConfig.description,
180
+ main: "electron-main.js",
181
+ dependencies,
182
+ };
183
+
184
+ if (this._config.postInstallScript != null) {
185
+ packageJson["scripts"] = { postinstall: this._config.postInstallScript };
186
+ }
187
+
188
+ await fsWriteJson(path.resolve(srcPath, "package.json"), packageJson, { space: 2 });
189
+ }
190
+
191
+ //#endregion
192
+
193
+ //#region Private - 번들링
194
+
195
+ /**
196
+ * esbuild로 electron-main.ts 번들링
197
+ */
198
+ private async _bundleMainProcess(outDir: string): Promise<void> {
199
+ const esbuild = await import("esbuild");
200
+ const entryPoint = path.resolve(this._pkgPath, "src/electron-main.ts");
201
+
202
+ if (!(await fsExists(entryPoint))) {
203
+ throw new Error(`electron-main.ts 파일을 찾을 수 없습니다: ${entryPoint}`);
204
+ }
205
+
206
+ const builtinModules = module.builtinModules.flatMap((m) => [m, `node:${m}`]);
207
+ const reinstallDeps = this._config.reinstallDependencies ?? [];
208
+
209
+ await fsMkdir(outDir);
210
+
211
+ await esbuild.build({
212
+ entryPoints: [entryPoint],
213
+ outfile: path.resolve(outDir, "electron-main.js"),
214
+ platform: "node",
215
+ target: "node20",
216
+ format: "cjs",
217
+ bundle: true,
218
+ external: ["electron", ...builtinModules, ...reinstallDeps],
219
+ });
220
+ }
221
+
222
+ //#endregion
223
+
224
+ //#region Private - 빌드
225
+
226
+ /**
227
+ * 웹 에셋 복사 (빌드 결과물 → .electron/src/)
228
+ */
229
+ private async _copyWebAssets(outPath: string, srcPath: string): Promise<void> {
230
+ const items = await fsReaddir(outPath);
231
+ for (const item of items) {
232
+ // electron/ 하위는 제외 (자기 자신 복사 방지)
233
+ if (item === "electron") continue;
234
+
235
+ const source = path.resolve(outPath, item);
236
+ const dest = path.resolve(srcPath, item);
237
+ await fsCopy(source, dest);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Symlink 생성 가능 여부 확인 (Windows 빌드 요구사항)
243
+ */
244
+ private static _canCreateSymlink(): boolean {
245
+ const tmpDir = os.tmpdir();
246
+ const testTarget = path.join(tmpDir, "sd-electron-symlink-test-target.txt");
247
+ const testLink = path.join(tmpDir, "sd-electron-symlink-test-link.txt");
248
+
249
+ try {
250
+ fs.writeFileSync(testTarget, "test");
251
+ fs.symlinkSync(testTarget, testLink, "file");
252
+ const isSymlink = fs.lstatSync(testLink).isSymbolicLink();
253
+ fs.unlinkSync(testLink);
254
+ fs.unlinkSync(testTarget);
255
+ return isSymlink;
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * electron-builder 실행
263
+ */
264
+ private async _runElectronBuilder(srcPath: string): Promise<void> {
265
+ if (!Electron._canCreateSymlink()) {
266
+ throw new Error("Electron 빌드를 위해서는 Symlink 생성 권한이 필요합니다. 윈도우의 개발자모드를 활성화하세요.");
267
+ }
268
+
269
+ const distPath = path.resolve(this._electronPath, "dist");
270
+
271
+ const builderConfig: Record<string, unknown> = {
272
+ appId: this._config.appId,
273
+ productName: this._npmConfig.description,
274
+ asar: false,
275
+ win: {
276
+ target: this._config.portable ? "portable" : "nsis",
277
+ },
278
+ nsis: this._config.nsisOptions ?? {},
279
+ directories: {
280
+ app: srcPath,
281
+ output: distPath,
282
+ },
283
+ removePackageScripts: false,
284
+ npmRebuild: false,
285
+ forceCodeSigning: false,
286
+ };
287
+
288
+ if (this._config.installerIcon != null) {
289
+ builderConfig["icon"] = path.resolve(this._pkgPath, this._config.installerIcon);
290
+ }
291
+
292
+ const configFilePath = path.resolve(this._electronPath, "builder-config.json");
293
+ await fsWriteJson(configFilePath, builderConfig, { space: 2 });
294
+
295
+ await this._exec("npx", ["electron-builder", "--win", "--config", configFilePath], this._pkgPath);
296
+ }
297
+
298
+ /**
299
+ * 빌드 결과물 복사 (.electron/dist/ → dist/electron/)
300
+ */
301
+ private async _copyBuildOutput(outPath: string): Promise<void> {
302
+ const distPath = path.resolve(this._electronPath, "dist");
303
+ const electronOutPath = path.resolve(outPath, "electron");
304
+ await fsMkdir(electronOutPath);
305
+
306
+ const description = this._npmConfig.description ?? this._npmConfig.name;
307
+ const version = this._npmConfig.version;
308
+ const isPortable = this._config.portable === true;
309
+
310
+ // electron-builder 출력 파일명
311
+ const builderFileName = `${description} ${isPortable ? "" : "Setup "}${version}.exe`;
312
+ const sourcePath = path.resolve(distPath, builderFileName);
313
+
314
+ if (await fsExists(sourcePath)) {
315
+ // latest 파일 복사
316
+ const latestFileName = `${description}${isPortable ? "-portable" : ""}-latest.exe`;
317
+ await fsCopy(sourcePath, path.resolve(electronOutPath, latestFileName));
318
+
319
+ // updates/ 버전별 파일 복사
320
+ const updatesPath = path.resolve(electronOutPath, "updates");
321
+ await fsMkdir(updatesPath);
322
+ await fsCopy(sourcePath, path.resolve(updatesPath, `${version}.exe`));
323
+ } else {
324
+ Electron._logger.warn(`빌드 결과물을 찾을 수 없습니다: ${sourcePath}`);
325
+ }
326
+ }
327
+
328
+ //#endregion
329
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./sd-config.types";