@simplysm/sd-cli 13.0.0-beta.46 → 13.0.0-beta.47
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.
- package/README.md +3 -3
- package/dist/builders/BaseBuilder.js.map +0 -1
- package/dist/builders/DtsBuilder.js.map +0 -1
- package/dist/builders/LibraryBuilder.js.map +0 -1
- package/dist/builders/index.js.map +0 -1
- package/dist/builders/types.js.map +0 -1
- package/dist/capacitor/capacitor.js.map +0 -1
- package/dist/commands/add-client.js.map +0 -1
- package/dist/commands/add-server.js.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/device.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/lint.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/typecheck.js.map +0 -1
- package/dist/commands/watch.js.map +0 -1
- package/dist/electron/electron.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infra/ResultCollector.js.map +0 -1
- package/dist/infra/SignalHandler.js.map +0 -1
- package/dist/infra/WorkerManager.js.map +0 -1
- package/dist/infra/index.js.map +0 -1
- package/dist/orchestrators/WatchOrchestrator.js.map +0 -1
- package/dist/orchestrators/index.js.map +0 -1
- package/dist/sd-cli.js.map +0 -1
- package/dist/sd-config.types.js.map +0 -1
- package/dist/utils/build-env.js.map +0 -1
- package/dist/utils/config-editor.js.map +0 -1
- package/dist/utils/copy-src.js.map +0 -1
- package/dist/utils/esbuild-config.d.ts +1 -0
- package/dist/utils/esbuild-config.d.ts.map +1 -1
- package/dist/utils/esbuild-config.js +2 -1
- package/dist/utils/esbuild-config.js.map +1 -2
- package/dist/utils/listr-manager.js.map +0 -1
- package/dist/utils/output-utils.js.map +0 -1
- package/dist/utils/package-utils.js.map +0 -1
- package/dist/utils/replace-deps.js.map +0 -1
- package/dist/utils/sd-config.js.map +0 -1
- package/dist/utils/spawn.js.map +0 -1
- package/dist/utils/tailwind-config-deps.js.map +0 -1
- package/dist/utils/template.js.map +0 -1
- package/dist/utils/tsconfig.js.map +0 -1
- package/dist/utils/typecheck-serialization.js.map +0 -1
- package/dist/utils/vite-config.js.map +0 -1
- package/dist/utils/worker-events.js.map +0 -1
- package/dist/workers/client.worker.js.map +0 -1
- package/dist/workers/dts.worker.js.map +0 -1
- package/dist/workers/library.worker.js.map +0 -1
- package/dist/workers/server-runtime.worker.js.map +0 -1
- package/dist/workers/server.worker.js.map +0 -1
- package/package.json +5 -4
- package/src/builders/BaseBuilder.ts +141 -0
- package/src/builders/DtsBuilder.ts +138 -0
- package/src/builders/LibraryBuilder.ts +161 -0
- package/src/builders/index.ts +4 -0
- package/src/builders/types.ts +55 -0
- package/src/capacitor/capacitor.ts +827 -0
- package/src/commands/add-client.ts +135 -0
- package/src/commands/add-server.ts +150 -0
- package/src/commands/build.ts +475 -0
- package/src/commands/dev.ts +602 -0
- package/src/commands/device.ts +151 -0
- package/src/commands/init.ts +104 -0
- package/src/commands/lint.ts +216 -0
- package/src/commands/publish.ts +836 -0
- package/src/commands/typecheck.ts +329 -0
- package/src/commands/watch.ts +38 -0
- package/src/electron/electron.ts +329 -0
- package/src/index.ts +1 -0
- package/src/infra/ResultCollector.ts +81 -0
- package/src/infra/SignalHandler.ts +52 -0
- package/src/infra/WorkerManager.ts +65 -0
- package/src/infra/index.ts +3 -0
- package/src/orchestrators/WatchOrchestrator.ts +211 -0
- package/src/orchestrators/index.ts +1 -0
- package/src/sd-cli.ts +307 -0
- package/src/sd-config.types.ts +271 -0
- package/src/utils/build-env.ts +12 -0
- package/src/utils/config-editor.ts +131 -0
- package/src/utils/copy-src.ts +60 -0
- package/src/utils/esbuild-config.ts +263 -0
- package/src/utils/listr-manager.ts +89 -0
- package/src/utils/output-utils.ts +61 -0
- package/src/utils/package-utils.ts +63 -0
- package/src/utils/replace-deps.ts +163 -0
- package/src/utils/sd-config.ts +44 -0
- package/src/utils/spawn.ts +79 -0
- package/src/utils/tailwind-config-deps.ts +95 -0
- package/src/utils/template.ts +51 -0
- package/src/utils/tsconfig.ts +111 -0
- package/src/utils/typecheck-serialization.ts +82 -0
- package/src/utils/vite-config.ts +184 -0
- package/src/utils/worker-events.ts +102 -0
- package/src/workers/client.worker.ts +236 -0
- package/src/workers/dts.worker.ts +416 -0
- package/src/workers/library.worker.ts +245 -0
- package/src/workers/server-runtime.worker.ts +154 -0
- package/src/workers/server.worker.ts +435 -0
- package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
- package/templates/add-server/__SERVER__/package.json.hbs +2 -2
- package/templates/init/package.json.hbs +3 -3
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import proxy from "@fastify/http-proxy";
|
|
2
|
+
import { createWorker } from "@simplysm/core-node";
|
|
3
|
+
import { consola } from "consola";
|
|
4
|
+
import net from "net";
|
|
5
|
+
|
|
6
|
+
//#region Types
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Server Runtime 시작 정보
|
|
10
|
+
*/
|
|
11
|
+
export interface ServerRuntimeStartInfo {
|
|
12
|
+
mainJsPath: string;
|
|
13
|
+
clientPorts: Record<string, number>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 서버 준비 이벤트
|
|
18
|
+
*/
|
|
19
|
+
export interface ServerRuntimeReadyEvent {
|
|
20
|
+
port: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 에러 이벤트
|
|
25
|
+
*/
|
|
26
|
+
export interface ServerRuntimeErrorEvent {
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Worker 이벤트 타입
|
|
32
|
+
*/
|
|
33
|
+
export interface ServerRuntimeWorkerEvents extends Record<string, unknown> {
|
|
34
|
+
serverReady: ServerRuntimeReadyEvent;
|
|
35
|
+
error: ServerRuntimeErrorEvent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
|
|
40
|
+
const logger = consola.withTag("sd:cli:server-runtime:worker");
|
|
41
|
+
|
|
42
|
+
/** 서버 인스턴스 (정리 대상) */
|
|
43
|
+
let serverInstance: { close: () => Promise<void> } | undefined;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 리소스 정리
|
|
47
|
+
*/
|
|
48
|
+
async function cleanup(): Promise<void> {
|
|
49
|
+
const server = serverInstance;
|
|
50
|
+
if (server != null) {
|
|
51
|
+
await server.close();
|
|
52
|
+
}
|
|
53
|
+
serverInstance = undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.on("SIGTERM", () => {
|
|
57
|
+
cleanup()
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
logger.error("cleanup 실패", err);
|
|
60
|
+
})
|
|
61
|
+
.finally(() => {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
process.on("SIGINT", () => {
|
|
67
|
+
cleanup()
|
|
68
|
+
.catch((err) => {
|
|
69
|
+
logger.error("cleanup 실패", err);
|
|
70
|
+
})
|
|
71
|
+
.finally(() => {
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 포트가 사용 가능한지 확인
|
|
78
|
+
*/
|
|
79
|
+
function isPortAvailable(port: number): Promise<boolean> {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const server = net.createServer();
|
|
82
|
+
server.once("error", () => resolve(false));
|
|
83
|
+
server.once("listening", () => {
|
|
84
|
+
server.close(() => resolve(true));
|
|
85
|
+
});
|
|
86
|
+
server.listen(port, "0.0.0.0");
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 시작 포트부터 사용 가능한 포트를 찾아 반환
|
|
92
|
+
*/
|
|
93
|
+
async function findAvailablePort(startPort: number, maxRetries = 20): Promise<number> {
|
|
94
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
95
|
+
const port = startPort + i;
|
|
96
|
+
if (await isPortAvailable(port)) {
|
|
97
|
+
return port;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return startPort;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Server Runtime 시작
|
|
105
|
+
* main.js를 import하고, Vite proxy를 설정한 후 listen
|
|
106
|
+
*/
|
|
107
|
+
async function start(info: ServerRuntimeStartInfo): Promise<void> {
|
|
108
|
+
try {
|
|
109
|
+
// main.js import (server 인스턴스를 export해야 함)
|
|
110
|
+
const module = await import(info.mainJsPath);
|
|
111
|
+
const server = module.server;
|
|
112
|
+
|
|
113
|
+
if (server == null) {
|
|
114
|
+
throw new Error("main.js에서 server 인스턴스를 export해야 합니다.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 서버 인스턴스 저장 (cleanup용)
|
|
118
|
+
serverInstance = server;
|
|
119
|
+
|
|
120
|
+
// 사용 가능한 포트 탐색 (포트 충돌 시 자동 증가)
|
|
121
|
+
const originalPort = server.options.port;
|
|
122
|
+
const availablePort = await findAvailablePort(originalPort);
|
|
123
|
+
if (availablePort !== originalPort) {
|
|
124
|
+
logger.info(`포트 ${originalPort} 사용 중 → ${availablePort}로 변경`);
|
|
125
|
+
server.options.port = availablePort;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Vite proxy 설정 (clientPorts가 있는 경우만)
|
|
129
|
+
for (const [name, port] of Object.entries(info.clientPorts)) {
|
|
130
|
+
await server.fastify.register(proxy, {
|
|
131
|
+
prefix: `/${name}`,
|
|
132
|
+
upstream: `http://127.0.0.1:${port}`,
|
|
133
|
+
rewritePrefix: `/${name}`,
|
|
134
|
+
websocket: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 서버 시작
|
|
139
|
+
await server.listen();
|
|
140
|
+
|
|
141
|
+
sender.send("serverReady", { port: server.options.port });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logger.error("Server Runtime 시작 실패", err);
|
|
144
|
+
sender.send("error", {
|
|
145
|
+
message: err instanceof Error ? err.message : String(err),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sender = createWorker<{ start: typeof start }, ServerRuntimeWorkerEvents>({
|
|
151
|
+
start,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export default sender;
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import cp from "child_process";
|
|
4
|
+
import esbuild from "esbuild";
|
|
5
|
+
import { createWorker } from "@simplysm/core-node";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
import { parseRootTsconfig, getPackageSourceFiles, getCompilerOptionsForPackage } from "../utils/tsconfig";
|
|
8
|
+
import {
|
|
9
|
+
createServerEsbuildOptions,
|
|
10
|
+
collectUninstalledOptionalPeerDeps,
|
|
11
|
+
collectNativeModuleExternals,
|
|
12
|
+
} from "../utils/esbuild-config";
|
|
13
|
+
|
|
14
|
+
//#region Types
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Server 빌드 정보 (일회성 빌드용)
|
|
18
|
+
*/
|
|
19
|
+
export interface ServerBuildInfo {
|
|
20
|
+
name: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
pkgDir: string;
|
|
23
|
+
/** 빌드 시 치환할 환경변수 */
|
|
24
|
+
env?: Record<string, string>;
|
|
25
|
+
/** 런타임 설정 (dist/.config.json에 기록) */
|
|
26
|
+
configs?: Record<string, unknown>;
|
|
27
|
+
/** sd.config.ts에서 수동 지정한 external 모듈 */
|
|
28
|
+
externals?: string[];
|
|
29
|
+
/** PM2 설정 (지정 시 dist/pm2.config.cjs 생성) */
|
|
30
|
+
pm2?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
ignoreWatchPaths?: string[];
|
|
33
|
+
};
|
|
34
|
+
/** Package manager to use (affects mise.toml or volta settings generation) */
|
|
35
|
+
packageManager?: "volta" | "mise";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Server 빌드 결과
|
|
40
|
+
*/
|
|
41
|
+
export interface ServerBuildResult {
|
|
42
|
+
success: boolean;
|
|
43
|
+
mainJsPath: string;
|
|
44
|
+
errors?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Server Watch 정보
|
|
49
|
+
*/
|
|
50
|
+
export interface ServerWatchInfo {
|
|
51
|
+
name: string;
|
|
52
|
+
cwd: string;
|
|
53
|
+
pkgDir: string;
|
|
54
|
+
/** 빌드 시 치환할 환경변수 */
|
|
55
|
+
env?: Record<string, string>;
|
|
56
|
+
/** 런타임 설정 (dist/.config.json에 기록) */
|
|
57
|
+
configs?: Record<string, unknown>;
|
|
58
|
+
/** sd.config.ts에서 수동 지정한 external 모듈 */
|
|
59
|
+
externals?: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 빌드 이벤트
|
|
64
|
+
*/
|
|
65
|
+
export interface ServerBuildEvent {
|
|
66
|
+
success: boolean;
|
|
67
|
+
mainJsPath: string;
|
|
68
|
+
errors?: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 에러 이벤트
|
|
73
|
+
*/
|
|
74
|
+
export interface ServerErrorEvent {
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Worker 이벤트 타입
|
|
80
|
+
*/
|
|
81
|
+
export interface ServerWorkerEvents extends Record<string, unknown> {
|
|
82
|
+
buildStart: Record<string, never>;
|
|
83
|
+
build: ServerBuildEvent;
|
|
84
|
+
error: ServerErrorEvent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
|
|
89
|
+
//#region 리소스 관리
|
|
90
|
+
|
|
91
|
+
const logger = consola.withTag("sd:cli:server:worker");
|
|
92
|
+
|
|
93
|
+
/** esbuild build context (정리 대상) */
|
|
94
|
+
let esbuildContext: esbuild.BuildContext | undefined;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 리소스 정리
|
|
98
|
+
*/
|
|
99
|
+
async function cleanup(): Promise<void> {
|
|
100
|
+
// 전역 변수를 임시 변수로 캡처 후 초기화
|
|
101
|
+
// (Promise.all 대기 중 다른 호출에서 전역 변수를 수정할 수 있으므로)
|
|
102
|
+
const contextToDispose = esbuildContext;
|
|
103
|
+
esbuildContext = undefined;
|
|
104
|
+
|
|
105
|
+
if (contextToDispose != null) {
|
|
106
|
+
await contextToDispose.dispose();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 세 가지 소스에서 external 모듈을 수집하여 합친다.
|
|
112
|
+
* 1. 미설치 optional peer deps
|
|
113
|
+
* 2. binding.gyp 네이티브 모듈
|
|
114
|
+
* 3. sd.config.ts 수동 지정
|
|
115
|
+
*/
|
|
116
|
+
function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
|
|
117
|
+
const optionalPeerDeps = collectUninstalledOptionalPeerDeps(pkgDir);
|
|
118
|
+
const nativeModules = collectNativeModuleExternals(pkgDir);
|
|
119
|
+
const manual = manualExternals ?? [];
|
|
120
|
+
|
|
121
|
+
const merged = [...new Set([...optionalPeerDeps, ...nativeModules, ...manual])];
|
|
122
|
+
|
|
123
|
+
if (optionalPeerDeps.length > 0) {
|
|
124
|
+
logger.debug("미설치 optional peer deps (external):", optionalPeerDeps);
|
|
125
|
+
}
|
|
126
|
+
if (nativeModules.length > 0) {
|
|
127
|
+
logger.debug("네이티브 모듈 (external):", nativeModules);
|
|
128
|
+
}
|
|
129
|
+
if (manual.length > 0) {
|
|
130
|
+
logger.debug("수동 지정 (external):", manual);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return merged;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 프로덕션 배포용 파일 생성 (일회성 빌드에서만 호출)
|
|
138
|
+
*
|
|
139
|
+
* - dist/package.json: external 모듈을 dependencies로 포함 (volta 사용 시 volta 필드 추가)
|
|
140
|
+
* - dist/mise.toml: Node 버전 지정 (packageManager === "mise"일 때만)
|
|
141
|
+
* - dist/openssl.cnf: 레거시 OpenSSL 프로바이더 활성화
|
|
142
|
+
* - dist/pm2.config.cjs: PM2 프로세스 설정 (pm2 옵션이 있을 때만)
|
|
143
|
+
*/
|
|
144
|
+
function generateProductionFiles(info: ServerBuildInfo, externals: string[]): void {
|
|
145
|
+
const distDir = path.join(info.pkgDir, "dist");
|
|
146
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(info.pkgDir, "package.json"), "utf-8"));
|
|
147
|
+
|
|
148
|
+
// dist/package.json
|
|
149
|
+
logger.debug("GEN package.json...");
|
|
150
|
+
const distPkgJson: Record<string, unknown> = {
|
|
151
|
+
name: pkgJson.name,
|
|
152
|
+
version: pkgJson.version,
|
|
153
|
+
type: pkgJson.type,
|
|
154
|
+
};
|
|
155
|
+
if (externals.length > 0) {
|
|
156
|
+
const deps: Record<string, string> = {};
|
|
157
|
+
for (const ext of externals) {
|
|
158
|
+
deps[ext] = "*";
|
|
159
|
+
}
|
|
160
|
+
distPkgJson["dependencies"] = deps;
|
|
161
|
+
}
|
|
162
|
+
if (info.packageManager === "volta") {
|
|
163
|
+
const nodeVersion = cp.execSync("node -v").toString().trim();
|
|
164
|
+
distPkgJson["volta"] = { node: nodeVersion };
|
|
165
|
+
}
|
|
166
|
+
fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify(distPkgJson, undefined, 2));
|
|
167
|
+
|
|
168
|
+
// dist/mise.toml (packageManager === "mise"일 때만)
|
|
169
|
+
if (info.packageManager === "mise") {
|
|
170
|
+
logger.debug("GEN mise.toml...");
|
|
171
|
+
const rootMiseTomlPath = path.join(info.cwd, "mise.toml");
|
|
172
|
+
let nodeVersion = "20";
|
|
173
|
+
if (fs.existsSync(rootMiseTomlPath)) {
|
|
174
|
+
const miseContent = fs.readFileSync(rootMiseTomlPath, "utf-8");
|
|
175
|
+
const match = /node\s*=\s*"([^"]+)"/.exec(miseContent);
|
|
176
|
+
if (match != null) {
|
|
177
|
+
nodeVersion = match[1];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
fs.writeFileSync(path.join(distDir, "mise.toml"), `[tools]\nnode = "${nodeVersion}"\n`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// dist/openssl.cnf
|
|
184
|
+
logger.debug("GEN openssl.cnf...");
|
|
185
|
+
fs.writeFileSync(
|
|
186
|
+
path.join(distDir, "openssl.cnf"),
|
|
187
|
+
[
|
|
188
|
+
"nodejs_conf = openssl_init",
|
|
189
|
+
"",
|
|
190
|
+
"[openssl_init]",
|
|
191
|
+
"providers = provider_sect",
|
|
192
|
+
"ssl_conf = ssl_sect",
|
|
193
|
+
"",
|
|
194
|
+
"[provider_sect]",
|
|
195
|
+
"default = default_sect",
|
|
196
|
+
"legacy = legacy_sect",
|
|
197
|
+
"",
|
|
198
|
+
"[default_sect]",
|
|
199
|
+
"activate = 1",
|
|
200
|
+
"",
|
|
201
|
+
"[legacy_sect]",
|
|
202
|
+
"activate = 1",
|
|
203
|
+
"",
|
|
204
|
+
"[ssl_sect]",
|
|
205
|
+
"system_default = system_default_sect",
|
|
206
|
+
"",
|
|
207
|
+
"[system_default_sect]",
|
|
208
|
+
"Options = UnsafeLegacyRenegotiation",
|
|
209
|
+
].join("\n"),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// dist/pm2.config.cjs (pm2 설정이 있을 때만)
|
|
213
|
+
if (info.pm2 != null) {
|
|
214
|
+
logger.debug("GEN pm2.config.cjs...");
|
|
215
|
+
|
|
216
|
+
const pm2Name = info.pm2.name ?? pkgJson.name.replace(/@/g, "").replace(/[/\\]/g, "-");
|
|
217
|
+
const ignoreWatch = JSON.stringify(["node_modules", "www", ...(info.pm2.ignoreWatchPaths ?? [])]);
|
|
218
|
+
const envObj: Record<string, string> = {
|
|
219
|
+
NODE_ENV: "production",
|
|
220
|
+
TZ: "Asia/Seoul",
|
|
221
|
+
...(info.env ?? {}),
|
|
222
|
+
};
|
|
223
|
+
const envStr = JSON.stringify(envObj, undefined, 4);
|
|
224
|
+
|
|
225
|
+
const interpreterLine =
|
|
226
|
+
info.packageManager === "volta" ? "" : ` interpreter: cp.execSync("mise which node").toString().trim(),\n`;
|
|
227
|
+
|
|
228
|
+
const pm2Config = [
|
|
229
|
+
`const cp = require("child_process");`,
|
|
230
|
+
``,
|
|
231
|
+
`module.exports = {`,
|
|
232
|
+
` name: ${JSON.stringify(pm2Name)},`,
|
|
233
|
+
` script: "main.js",`,
|
|
234
|
+
` watch: true,`,
|
|
235
|
+
` watch_delay: 2000,`,
|
|
236
|
+
` ignore_watch: ${ignoreWatch},`,
|
|
237
|
+
interpreterLine.trimEnd(),
|
|
238
|
+
` interpreter_args: "--openssl-config=openssl.cnf",`,
|
|
239
|
+
` env: ${envStr.replace(/\n/g, "\n ")},`,
|
|
240
|
+
` arrayProcess: "concat",`,
|
|
241
|
+
` useDelTargetNull: true,`,
|
|
242
|
+
`};`,
|
|
243
|
+
]
|
|
244
|
+
.filter((line) => line !== "")
|
|
245
|
+
.join("\n");
|
|
246
|
+
|
|
247
|
+
fs.writeFileSync(path.join(distDir, "pm2.config.cjs"), pm2Config);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 프로세스 종료 전 리소스 정리 (SIGTERM/SIGINT)
|
|
252
|
+
// 주의: worker.terminate()는 이 핸들러들을 호출하지 않고 즉시 종료됨.
|
|
253
|
+
// 그러나 watch 모드에서 정상 종료는 메인 프로세스의 SIGINT/SIGTERM을 통해 이루어지므로 문제없음.
|
|
254
|
+
process.on("SIGTERM", () => {
|
|
255
|
+
cleanup()
|
|
256
|
+
.catch((err) => {
|
|
257
|
+
logger.error("cleanup 실패", err);
|
|
258
|
+
})
|
|
259
|
+
.finally(() => {
|
|
260
|
+
process.exit(0);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
process.on("SIGINT", () => {
|
|
265
|
+
cleanup()
|
|
266
|
+
.catch((err) => {
|
|
267
|
+
logger.error("cleanup 실패", err);
|
|
268
|
+
})
|
|
269
|
+
.finally(() => {
|
|
270
|
+
process.exit(0);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
//#endregion
|
|
275
|
+
|
|
276
|
+
//#region Worker
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 일회성 빌드
|
|
280
|
+
*/
|
|
281
|
+
async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
|
|
282
|
+
const mainJsPath = path.join(info.pkgDir, "dist", "main.js");
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
// tsconfig 파싱
|
|
286
|
+
const parsedConfig = parseRootTsconfig(info.cwd);
|
|
287
|
+
const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
|
|
288
|
+
|
|
289
|
+
// 서버는 node 환경
|
|
290
|
+
const compilerOptions = await getCompilerOptionsForPackage(parsedConfig.options, "node", info.pkgDir);
|
|
291
|
+
|
|
292
|
+
// 모든 external 수집 (optional peer deps + native modules + manual)
|
|
293
|
+
const external = collectAllExternals(info.pkgDir, info.externals);
|
|
294
|
+
|
|
295
|
+
// esbuild 일회성 빌드
|
|
296
|
+
const esbuildOptions = createServerEsbuildOptions({
|
|
297
|
+
pkgDir: info.pkgDir,
|
|
298
|
+
entryPoints,
|
|
299
|
+
compilerOptions,
|
|
300
|
+
env: info.env,
|
|
301
|
+
external,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const result = await esbuild.build(esbuildOptions);
|
|
305
|
+
|
|
306
|
+
// Generate .config.json
|
|
307
|
+
const confDistPath = path.join(info.pkgDir, "dist", ".config.json");
|
|
308
|
+
fs.writeFileSync(confDistPath, JSON.stringify(info.configs ?? {}, undefined, 2));
|
|
309
|
+
|
|
310
|
+
// Generate production files (package.json, mise.toml, openssl.cnf, pm2.config.cjs)
|
|
311
|
+
generateProductionFiles(info, external);
|
|
312
|
+
|
|
313
|
+
const errors = result.errors.map((e) => e.text);
|
|
314
|
+
return {
|
|
315
|
+
success: result.errors.length === 0,
|
|
316
|
+
mainJsPath,
|
|
317
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
318
|
+
};
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
mainJsPath,
|
|
323
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** startWatch 호출 여부 플래그 */
|
|
329
|
+
let isWatchStarted = false;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* watch 시작
|
|
333
|
+
* @remarks 이 함수는 Worker당 한 번만 호출되어야 합니다.
|
|
334
|
+
* @throws 이미 watch가 시작된 경우
|
|
335
|
+
*/
|
|
336
|
+
async function startWatch(info: ServerWatchInfo): Promise<void> {
|
|
337
|
+
if (isWatchStarted) {
|
|
338
|
+
throw new Error("startWatch는 Worker당 한 번만 호출할 수 있습니다.");
|
|
339
|
+
}
|
|
340
|
+
isWatchStarted = true;
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
// tsconfig 파싱
|
|
344
|
+
const parsedConfig = parseRootTsconfig(info.cwd);
|
|
345
|
+
const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
|
|
346
|
+
|
|
347
|
+
// 서버는 node 환경
|
|
348
|
+
const compilerOptions = await getCompilerOptionsForPackage(parsedConfig.options, "node", info.pkgDir);
|
|
349
|
+
|
|
350
|
+
const mainJsPath = path.join(info.pkgDir, "dist", "main.js");
|
|
351
|
+
|
|
352
|
+
// 첫 번째 빌드 완료 대기를 위한 Promise
|
|
353
|
+
let resolveFirstBuild!: () => void;
|
|
354
|
+
const firstBuildPromise = new Promise<void>((resolve) => {
|
|
355
|
+
resolveFirstBuild = resolve;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
let isFirstBuild = true;
|
|
359
|
+
|
|
360
|
+
// 모든 external 수집 (optional peer deps + native modules + manual)
|
|
361
|
+
const external = collectAllExternals(info.pkgDir, info.externals);
|
|
362
|
+
|
|
363
|
+
// esbuild 기본 옵션 생성
|
|
364
|
+
const baseOptions = createServerEsbuildOptions({
|
|
365
|
+
pkgDir: info.pkgDir,
|
|
366
|
+
entryPoints,
|
|
367
|
+
compilerOptions,
|
|
368
|
+
env: info.env,
|
|
369
|
+
external,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// watch용 플러그인 추가
|
|
373
|
+
esbuildContext = await esbuild.context({
|
|
374
|
+
...baseOptions,
|
|
375
|
+
plugins: [
|
|
376
|
+
{
|
|
377
|
+
name: "watch-notify",
|
|
378
|
+
setup(pluginBuild) {
|
|
379
|
+
pluginBuild.onStart(() => {
|
|
380
|
+
sender.send("buildStart", {});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
pluginBuild.onEnd((result) => {
|
|
384
|
+
const errors = result.errors.map((e) => e.text);
|
|
385
|
+
const success = result.errors.length === 0;
|
|
386
|
+
|
|
387
|
+
// Generate .config.json on first successful build
|
|
388
|
+
if (isFirstBuild && success) {
|
|
389
|
+
const confDistPath = path.join(info.pkgDir, "dist", ".config.json");
|
|
390
|
+
fs.writeFileSync(confDistPath, JSON.stringify(info.configs ?? {}, undefined, 2));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
sender.send("build", { success, mainJsPath, errors: errors.length > 0 ? errors : undefined });
|
|
394
|
+
|
|
395
|
+
if (isFirstBuild) {
|
|
396
|
+
isFirstBuild = false;
|
|
397
|
+
resolveFirstBuild();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await esbuildContext.watch();
|
|
406
|
+
|
|
407
|
+
// 첫 번째 빌드 완료 대기
|
|
408
|
+
await firstBuildPromise;
|
|
409
|
+
} catch (err) {
|
|
410
|
+
sender.send("error", {
|
|
411
|
+
message: err instanceof Error ? err.message : String(err),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* watch 중지
|
|
418
|
+
* @remarks esbuild context를 정리합니다.
|
|
419
|
+
*/
|
|
420
|
+
async function stopWatch(): Promise<void> {
|
|
421
|
+
await cleanup();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const sender = createWorker<
|
|
425
|
+
{ build: typeof build; startWatch: typeof startWatch; stopWatch: typeof stopWatch },
|
|
426
|
+
ServerWorkerEvents
|
|
427
|
+
>({
|
|
428
|
+
build,
|
|
429
|
+
startWatch,
|
|
430
|
+
stopWatch,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
export default sender;
|
|
434
|
+
|
|
435
|
+
//#endregion
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"vitest": "vitest"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@simplysm/sd-cli": "~13.0.0-beta.
|
|
19
|
-
"@simplysm/claude": "~13.0.0-beta.
|
|
20
|
-
"@simplysm/eslint-plugin": "~13.0.0-beta.
|
|
18
|
+
"@simplysm/sd-cli": "~13.0.0-beta.47",
|
|
19
|
+
"@simplysm/claude": "~13.0.0-beta.47",
|
|
20
|
+
"@simplysm/eslint-plugin": "~13.0.0-beta.47",
|
|
21
21
|
"@types/node": "^20.19.33",
|
|
22
22
|
"eslint": "^9.39.2",
|
|
23
23
|
"prettier": "^3.8.1",
|