@simplysm/sd-cli 13.0.7 → 13.0.10
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/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +9 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/orchestrators/WatchOrchestrator.d.ts +1 -0
- package/dist/orchestrators/WatchOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/WatchOrchestrator.js +4 -1
- package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
- package/dist/sd-cli-entry.d.ts +9 -0
- package/dist/sd-cli-entry.d.ts.map +1 -0
- package/dist/sd-cli-entry.js +266 -0
- package/dist/sd-cli-entry.js.map +6 -0
- package/dist/sd-cli.d.ts +5 -5
- package/dist/sd-cli.d.ts.map +1 -1
- package/dist/sd-cli.js +56 -260
- package/dist/sd-cli.js.map +1 -1
- package/dist/utils/replace-deps.d.ts +32 -2
- package/dist/utils/replace-deps.d.ts.map +1 -1
- package/dist/utils/replace-deps.js +124 -5
- package/dist/utils/replace-deps.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/dev.ts +9 -1
- package/src/orchestrators/WatchOrchestrator.ts +4 -1
- package/src/sd-cli-entry.ts +330 -0
- package/src/sd-cli.ts +88 -319
- package/src/utils/replace-deps.ts +213 -9
- 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
package/src/sd-cli.ts
CHANGED
|
@@ -1,330 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import { runPublish } from "./commands/publish";
|
|
13
|
-
import { runReplaceDeps } from "./commands/replace-deps";
|
|
14
|
-
import { runDevice } from "./commands/device";
|
|
3
|
+
/**
|
|
4
|
+
* CLI Launcher
|
|
5
|
+
*
|
|
6
|
+
* .ts 실행 (개발): CPU affinity 적용 후 sd-cli-entry 직접 import
|
|
7
|
+
* .js 실행 (배포): replaceDeps 실행 후 새 프로세스로 sd-cli-entry spawn
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { exec, spawn } from "child_process";
|
|
11
|
+
import os from "os";
|
|
15
12
|
import path from "path";
|
|
16
|
-
import fs from "fs";
|
|
17
13
|
import { fileURLToPath } from "url";
|
|
18
|
-
import { EventEmitter } from "node:events";
|
|
19
|
-
import { consola, LogLevels } from "consola";
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
const cliEntryUrl = import.meta.resolve("./sd-cli-entry");
|
|
16
|
+
const cliEntryFilePath = fileURLToPath(cliEntryUrl);
|
|
17
|
+
|
|
18
|
+
if (path.extname(cliEntryFilePath) === ".ts") {
|
|
19
|
+
// 개발 모드 (.ts): affinity 적용 후 직접 실행
|
|
20
|
+
// import만으로는 메인 모듈 감지가 실패하므로 (process.argv[1] ≠ sd-cli-entry)
|
|
21
|
+
// createCliParser를 명시적으로 호출한다.
|
|
22
|
+
configureAffinityAndPriority(process.pid);
|
|
23
|
+
const { createCliParser } = await import(cliEntryUrl);
|
|
24
|
+
await createCliParser(process.argv.slice(2)).parse();
|
|
25
|
+
} else {
|
|
26
|
+
// 배포 모드 (.js): 2단계 실행
|
|
27
|
+
|
|
28
|
+
// Phase 1: replaceDeps (inline — 설치된 버전으로 복사)
|
|
29
|
+
try {
|
|
30
|
+
const { loadSdConfig } = await import("./utils/sd-config.js");
|
|
31
|
+
const { setupReplaceDeps } = await import("./utils/replace-deps.js");
|
|
32
|
+
const sdConfig = await loadSdConfig({ cwd: process.cwd(), dev: false, opt: [] });
|
|
33
|
+
if (sdConfig.replaceDeps != null) {
|
|
34
|
+
await setupReplaceDeps(process.cwd(), sdConfig.replaceDeps);
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// sd.config.ts 없거나 replaceDeps 미설정 시 스킵
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Phase 2: 새 프로세스로 실제 CLI 실행 (모듈 캐시 초기화)
|
|
41
|
+
const child = spawn(
|
|
42
|
+
"node",
|
|
43
|
+
["--max-old-space-size=8192", "--max-semi-space-size=16", cliEntryFilePath, ...process.argv.slice(2)],
|
|
44
|
+
{ stdio: "inherit" },
|
|
45
|
+
);
|
|
46
|
+
child.on("spawn", () => {
|
|
47
|
+
if (child.pid != null) configureAffinityAndPriority(child.pid);
|
|
48
|
+
});
|
|
49
|
+
child.on("exit", (code) => {
|
|
50
|
+
process.exitCode = code ?? 0;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
23
53
|
|
|
24
54
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
55
|
+
* CPU affinity mask 계산 (앞쪽 코어 제외)
|
|
56
|
+
*
|
|
57
|
+
* CPU 4개당 1개를 제외하고, 나머지 코어의 비트를 ON으로 설정한다.
|
|
58
|
+
* 예: 8코어 → 2개 제외 → 0xFC (코어 2~7)
|
|
27
59
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
default: false,
|
|
36
|
-
global: true,
|
|
37
|
-
})
|
|
38
|
-
.middleware((args) => {
|
|
39
|
-
if (args.debug) consola.level = LogLevels.debug;
|
|
40
|
-
})
|
|
41
|
-
.command(
|
|
42
|
-
"lint [targets..]",
|
|
43
|
-
"ESLint + Stylelint를 실행한다.",
|
|
44
|
-
(cmd) =>
|
|
45
|
-
cmd
|
|
46
|
-
.version(false)
|
|
47
|
-
.hide("help")
|
|
48
|
-
.positional("targets", {
|
|
49
|
-
type: "string",
|
|
50
|
-
array: true,
|
|
51
|
-
describe: "린트할 경로 (예: packages/core-common, tests/orm)",
|
|
52
|
-
default: [],
|
|
53
|
-
})
|
|
54
|
-
.options({
|
|
55
|
-
fix: {
|
|
56
|
-
type: "boolean",
|
|
57
|
-
describe: "자동 수정",
|
|
58
|
-
default: false,
|
|
59
|
-
},
|
|
60
|
-
timing: {
|
|
61
|
-
type: "boolean",
|
|
62
|
-
describe: "규칙별 실행 시간 출력",
|
|
63
|
-
default: false,
|
|
64
|
-
},
|
|
65
|
-
}),
|
|
66
|
-
async (args) => {
|
|
67
|
-
await runLint({
|
|
68
|
-
targets: args.targets,
|
|
69
|
-
fix: args.fix,
|
|
70
|
-
timing: args.timing,
|
|
71
|
-
});
|
|
72
|
-
},
|
|
73
|
-
)
|
|
74
|
-
.command(
|
|
75
|
-
"typecheck [targets..]",
|
|
76
|
-
"TypeScript 타입체크를 실행한다.",
|
|
77
|
-
(cmd) =>
|
|
78
|
-
cmd
|
|
79
|
-
.version(false)
|
|
80
|
-
.hide("help")
|
|
81
|
-
.positional("targets", {
|
|
82
|
-
type: "string",
|
|
83
|
-
array: true,
|
|
84
|
-
describe: "타입체크할 경로 (예: packages/core-common, tests/orm)",
|
|
85
|
-
default: [],
|
|
86
|
-
})
|
|
87
|
-
.options({
|
|
88
|
-
options: {
|
|
89
|
-
type: "string",
|
|
90
|
-
array: true,
|
|
91
|
-
alias: "o",
|
|
92
|
-
description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
|
|
93
|
-
default: [] as string[],
|
|
94
|
-
},
|
|
95
|
-
}),
|
|
96
|
-
async (args) => {
|
|
97
|
-
await runTypecheck({
|
|
98
|
-
targets: args.targets,
|
|
99
|
-
options: args.options,
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
)
|
|
103
|
-
.command(
|
|
104
|
-
"watch [targets..]",
|
|
105
|
-
"패키지를 watch 모드로 빌드한다.",
|
|
106
|
-
(cmd) =>
|
|
107
|
-
cmd
|
|
108
|
-
.version(false)
|
|
109
|
-
.hide("help")
|
|
110
|
-
.positional("targets", {
|
|
111
|
-
type: "string",
|
|
112
|
-
array: true,
|
|
113
|
-
describe: "watch할 패키지 (예: solid, solid-demo)",
|
|
114
|
-
default: [],
|
|
115
|
-
})
|
|
116
|
-
.options({
|
|
117
|
-
options: {
|
|
118
|
-
type: "string",
|
|
119
|
-
array: true,
|
|
120
|
-
alias: "o",
|
|
121
|
-
description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
|
|
122
|
-
default: [] as string[],
|
|
123
|
-
},
|
|
124
|
-
}),
|
|
125
|
-
async (args) => {
|
|
126
|
-
await runWatch({
|
|
127
|
-
targets: args.targets,
|
|
128
|
-
options: args.options,
|
|
129
|
-
});
|
|
130
|
-
},
|
|
131
|
-
)
|
|
132
|
-
.command(
|
|
133
|
-
"dev [targets..]",
|
|
134
|
-
"Client와 Server 패키지를 개발 모드로 실행한다.",
|
|
135
|
-
(cmd) =>
|
|
136
|
-
cmd
|
|
137
|
-
.version(false)
|
|
138
|
-
.hide("help")
|
|
139
|
-
.positional("targets", {
|
|
140
|
-
type: "string",
|
|
141
|
-
array: true,
|
|
142
|
-
describe: "실행할 패키지 (예: solid-demo)",
|
|
143
|
-
default: [],
|
|
144
|
-
})
|
|
145
|
-
.options({
|
|
146
|
-
options: {
|
|
147
|
-
type: "string",
|
|
148
|
-
array: true,
|
|
149
|
-
alias: "o",
|
|
150
|
-
description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
|
|
151
|
-
default: [] as string[],
|
|
152
|
-
},
|
|
153
|
-
}),
|
|
154
|
-
async (args) => {
|
|
155
|
-
await runDev({
|
|
156
|
-
targets: args.targets,
|
|
157
|
-
options: args.options,
|
|
158
|
-
});
|
|
159
|
-
},
|
|
160
|
-
)
|
|
161
|
-
.command(
|
|
162
|
-
"build [targets..]",
|
|
163
|
-
"프로덕션 빌드를 실행한다.",
|
|
164
|
-
(cmd) =>
|
|
165
|
-
cmd
|
|
166
|
-
.version(false)
|
|
167
|
-
.hide("help")
|
|
168
|
-
.positional("targets", {
|
|
169
|
-
type: "string",
|
|
170
|
-
array: true,
|
|
171
|
-
describe: "빌드할 패키지 (예: solid, core-common)",
|
|
172
|
-
default: [],
|
|
173
|
-
})
|
|
174
|
-
.options({
|
|
175
|
-
options: {
|
|
176
|
-
type: "string",
|
|
177
|
-
array: true,
|
|
178
|
-
alias: "o",
|
|
179
|
-
description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
|
|
180
|
-
default: [] as string[],
|
|
181
|
-
},
|
|
182
|
-
}),
|
|
183
|
-
async (args) => {
|
|
184
|
-
await runBuild({
|
|
185
|
-
targets: args.targets,
|
|
186
|
-
options: args.options,
|
|
187
|
-
});
|
|
188
|
-
},
|
|
189
|
-
)
|
|
190
|
-
.command(
|
|
191
|
-
"device",
|
|
192
|
-
"Android 디바이스에서 앱을 실행한다.",
|
|
193
|
-
(cmd) =>
|
|
194
|
-
cmd
|
|
195
|
-
.version(false)
|
|
196
|
-
.hide("help")
|
|
197
|
-
.options({
|
|
198
|
-
package: {
|
|
199
|
-
type: "string",
|
|
200
|
-
alias: "p",
|
|
201
|
-
describe: "패키지 이름",
|
|
202
|
-
demandOption: true,
|
|
203
|
-
},
|
|
204
|
-
url: {
|
|
205
|
-
type: "string",
|
|
206
|
-
alias: "u",
|
|
207
|
-
describe: "개발 서버 URL (미지정 시 sd.config.ts의 server 설정 사용)",
|
|
208
|
-
},
|
|
209
|
-
options: {
|
|
210
|
-
type: "string",
|
|
211
|
-
array: true,
|
|
212
|
-
alias: "o",
|
|
213
|
-
description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
|
|
214
|
-
default: [] as string[],
|
|
215
|
-
},
|
|
216
|
-
}),
|
|
217
|
-
async (args) => {
|
|
218
|
-
await runDevice({
|
|
219
|
-
package: args.package,
|
|
220
|
-
url: args.url,
|
|
221
|
-
options: args.options,
|
|
222
|
-
});
|
|
223
|
-
},
|
|
224
|
-
)
|
|
225
|
-
.command(
|
|
226
|
-
"init",
|
|
227
|
-
"새 프로젝트를 초기화한다.",
|
|
228
|
-
(cmd) => cmd.version(false).hide("help"),
|
|
229
|
-
async () => {
|
|
230
|
-
const { runInit } = await import("./commands/init.js");
|
|
231
|
-
await runInit({});
|
|
232
|
-
},
|
|
233
|
-
)
|
|
234
|
-
.command("add", "프로젝트에 패키지를 추가한다.", (cmd) =>
|
|
235
|
-
cmd
|
|
236
|
-
.version(false)
|
|
237
|
-
.hide("help")
|
|
238
|
-
.command(
|
|
239
|
-
"client",
|
|
240
|
-
"클라이언트 패키지를 추가한다.",
|
|
241
|
-
(subCmd) => subCmd.version(false).hide("help"),
|
|
242
|
-
async () => {
|
|
243
|
-
const { runAddClient } = await import("./commands/add-client.js");
|
|
244
|
-
await runAddClient({});
|
|
245
|
-
},
|
|
246
|
-
)
|
|
247
|
-
.command(
|
|
248
|
-
"server",
|
|
249
|
-
"서버 패키지를 추가한다.",
|
|
250
|
-
(subCmd) => subCmd.version(false).hide("help"),
|
|
251
|
-
async () => {
|
|
252
|
-
const { runAddServer } = await import("./commands/add-server.js");
|
|
253
|
-
await runAddServer({});
|
|
254
|
-
},
|
|
255
|
-
)
|
|
256
|
-
.demandCommand(1, "패키지 타입을 지정해주세요. (client, server)"),
|
|
257
|
-
)
|
|
258
|
-
.command(
|
|
259
|
-
"publish [targets..]",
|
|
260
|
-
"패키지를 배포한다.",
|
|
261
|
-
(cmd) =>
|
|
262
|
-
cmd
|
|
263
|
-
.version(false)
|
|
264
|
-
.hide("help")
|
|
265
|
-
.positional("targets", {
|
|
266
|
-
type: "string",
|
|
267
|
-
array: true,
|
|
268
|
-
describe: "배포할 패키지 (예: solid, core-common)",
|
|
269
|
-
default: [],
|
|
270
|
-
})
|
|
271
|
-
.options({
|
|
272
|
-
"build": {
|
|
273
|
-
type: "boolean",
|
|
274
|
-
describe: "빌드 실행 (--no-build로 스킵)",
|
|
275
|
-
default: true,
|
|
276
|
-
},
|
|
277
|
-
"dry-run": {
|
|
278
|
-
type: "boolean",
|
|
279
|
-
describe: "실제 배포 없이 시뮬레이션",
|
|
280
|
-
default: false,
|
|
281
|
-
},
|
|
282
|
-
"options": {
|
|
283
|
-
type: "string",
|
|
284
|
-
array: true,
|
|
285
|
-
alias: "o",
|
|
286
|
-
description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
|
|
287
|
-
default: [] as string[],
|
|
288
|
-
},
|
|
289
|
-
}),
|
|
290
|
-
async (args) => {
|
|
291
|
-
await runPublish({
|
|
292
|
-
targets: args.targets,
|
|
293
|
-
noBuild: !args.build,
|
|
294
|
-
dryRun: args.dryRun,
|
|
295
|
-
options: args.options,
|
|
296
|
-
});
|
|
297
|
-
},
|
|
298
|
-
)
|
|
299
|
-
.command(
|
|
300
|
-
"replace-deps",
|
|
301
|
-
"sd.config.ts의 replaceDeps 설정에 따라 node_modules 패키지를 로컬 소스로 symlink 교체한다.",
|
|
302
|
-
(cmd) =>
|
|
303
|
-
cmd
|
|
304
|
-
.version(false)
|
|
305
|
-
.hide("help")
|
|
306
|
-
.options({
|
|
307
|
-
options: {
|
|
308
|
-
type: "string",
|
|
309
|
-
array: true,
|
|
310
|
-
alias: "o",
|
|
311
|
-
description: "sd.config.ts에 전달할 옵션 (예: -o key=value)",
|
|
312
|
-
default: [] as string[],
|
|
313
|
-
},
|
|
314
|
-
}),
|
|
315
|
-
async (args) => {
|
|
316
|
-
await runReplaceDeps({
|
|
317
|
-
options: args.options,
|
|
318
|
-
});
|
|
319
|
-
},
|
|
320
|
-
)
|
|
321
|
-
.demandCommand(1, "명령어를 지정해주세요.")
|
|
322
|
-
.strict();
|
|
60
|
+
function calculateAffinityMask(cpuCount: number): string {
|
|
61
|
+
const exclude = cpuCount <= 1 ? 0 : Math.ceil(cpuCount / 4);
|
|
62
|
+
let mask = 0n;
|
|
63
|
+
for (let i = exclude; i < cpuCount; i++) {
|
|
64
|
+
mask |= 1n << BigInt(i);
|
|
65
|
+
}
|
|
66
|
+
return "0x" + mask.toString(16).toUpperCase();
|
|
323
67
|
}
|
|
324
68
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Cross-platform CPU affinity + priority 설정
|
|
71
|
+
*
|
|
72
|
+
* - Windows: PowerShell ProcessorAffinity + PriorityClass
|
|
73
|
+
* - Linux/WSL: taskset + renice
|
|
74
|
+
*
|
|
75
|
+
* 실패해도 경고만 출력하고 CLI 동작에는 영향 없음.
|
|
76
|
+
*/
|
|
77
|
+
function configureAffinityAndPriority(pid: number): void {
|
|
78
|
+
const cpuCount = os.cpus().length;
|
|
79
|
+
const mask = calculateAffinityMask(cpuCount);
|
|
80
|
+
|
|
81
|
+
let command: string;
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
const commands = [
|
|
84
|
+
`$p = Get-Process -Id ${pid}`,
|
|
85
|
+
`$p.ProcessorAffinity = ${mask}`,
|
|
86
|
+
`$p.PriorityClass = 'BelowNormal'`,
|
|
87
|
+
].join("; ");
|
|
88
|
+
command = `powershell -Command "${commands}"`;
|
|
89
|
+
} else {
|
|
90
|
+
command = `taskset -p ${mask} ${pid} && renice +10 -p ${pid}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
exec(command, (err) => {
|
|
94
|
+
if (err) {
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.warn("CPU affinity/priority 설정 실패:", err.message);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
330
99
|
}
|
|
@@ -2,6 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { glob } from "glob";
|
|
4
4
|
import { consola } from "consola";
|
|
5
|
+
import { fsCopy, fsMkdir, fsRm, FsWatcher } from "@simplysm/core-node";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* replaceDeps 설정의 glob 패턴과 대상 패키지 목록을 매칭하여
|
|
@@ -83,11 +84,47 @@ export function parseWorkspaceGlobs(content: string): string[] {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
|
-
*
|
|
87
|
+
* 복사 시 제외할 항목 이름들
|
|
88
|
+
*/
|
|
89
|
+
const EXCLUDED_NAMES = new Set(["node_modules", "package.json", ".cache", "tests"]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* replaceDeps 복사 시 사용할 필터 함수
|
|
93
|
+
* node_modules, package.json, .cache, tests를 제외한다.
|
|
94
|
+
*
|
|
95
|
+
* @param itemPath - 복사할 항목의 절대 경로
|
|
96
|
+
* @returns 복사 대상이면 true, 제외하면 false
|
|
97
|
+
*/
|
|
98
|
+
function replaceDepsCopyFilter(itemPath: string): boolean {
|
|
99
|
+
const basename = path.basename(itemPath);
|
|
100
|
+
return !EXCLUDED_NAMES.has(basename);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* replaceDeps 복사 교체 항목
|
|
105
|
+
*/
|
|
106
|
+
export interface ReplaceDepEntry {
|
|
107
|
+
targetName: string;
|
|
108
|
+
sourcePath: string;
|
|
109
|
+
targetPath: string;
|
|
110
|
+
resolvedSourcePath: string;
|
|
111
|
+
actualTargetPath: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* watchReplaceDeps 반환 타입
|
|
116
|
+
*/
|
|
117
|
+
export interface WatchReplaceDepResult {
|
|
118
|
+
entries: ReplaceDepEntry[];
|
|
119
|
+
dispose: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* replaceDeps 설정에 따라 node_modules 내 패키지를 소스 디렉토리로 복사 교체한다.
|
|
87
124
|
*
|
|
88
125
|
* 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
|
|
89
126
|
* 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
|
|
90
|
-
* 3. 기존 symlink/디렉토리 제거 → 소스
|
|
127
|
+
* 3. 기존 symlink/디렉토리 제거 → 소스 경로를 복사 (node_modules, package.json, .cache, tests 제외)
|
|
91
128
|
*
|
|
92
129
|
* @param projectRoot - 프로젝트 루트 경로
|
|
93
130
|
* @param replaceDeps - sd.config.ts의 replaceDeps 설정
|
|
@@ -133,7 +170,7 @@ export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<
|
|
|
133
170
|
// 패턴 매칭 및 경로 해석
|
|
134
171
|
const entries = resolveReplaceDepEntries(replaceDeps, targetNames);
|
|
135
172
|
|
|
136
|
-
// 3.
|
|
173
|
+
// 3. 복사 교체
|
|
137
174
|
for (const { targetName, sourcePath } of entries) {
|
|
138
175
|
const targetPath = path.join(nodeModulesDir, targetName);
|
|
139
176
|
const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
|
|
@@ -147,17 +184,184 @@ export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<
|
|
|
147
184
|
}
|
|
148
185
|
|
|
149
186
|
try {
|
|
150
|
-
//
|
|
151
|
-
|
|
187
|
+
// targetPath가 symlink면 realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
|
|
188
|
+
let actualTargetPath = targetPath;
|
|
189
|
+
try {
|
|
190
|
+
const stat = await fs.promises.lstat(targetPath);
|
|
191
|
+
if (stat.isSymbolicLink()) {
|
|
192
|
+
actualTargetPath = await fs.promises.realpath(targetPath);
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// targetPath가 존재하지 않으면 그대로 사용
|
|
196
|
+
}
|
|
152
197
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
await
|
|
198
|
+
// actualTargetPath의 기존 내용 제거 후 소스 복사 (symlink는 유지)
|
|
199
|
+
await fs.promises.rm(actualTargetPath, { recursive: true, force: true });
|
|
200
|
+
await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
|
|
156
201
|
|
|
157
202
|
logger.info(`${targetName} → ${sourcePath}`);
|
|
158
203
|
} catch (err) {
|
|
159
|
-
logger.error(
|
|
204
|
+
logger.error(`복사 교체 실패 (${targetName}): ${err instanceof Error ? err.message : err}`);
|
|
160
205
|
}
|
|
161
206
|
}
|
|
162
207
|
}
|
|
163
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* replaceDeps 설정에 따라 소스 디렉토리를 watch하여 변경 시 대상 경로로 복사한다.
|
|
212
|
+
*
|
|
213
|
+
* 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
|
|
214
|
+
* 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
|
|
215
|
+
* 3. 소스 디렉토리를 FsWatcher로 watch (300ms delay)
|
|
216
|
+
* 4. 변경 시 대상 경로로 복사 (node_modules, package.json, .cache, tests 제외)
|
|
217
|
+
*
|
|
218
|
+
* @param projectRoot - 프로젝트 루트 경로
|
|
219
|
+
* @param replaceDeps - sd.config.ts의 replaceDeps 설정
|
|
220
|
+
* @returns entries와 dispose 함수
|
|
221
|
+
*/
|
|
222
|
+
export async function watchReplaceDeps(
|
|
223
|
+
projectRoot: string,
|
|
224
|
+
replaceDeps: Record<string, string>,
|
|
225
|
+
): Promise<WatchReplaceDepResult> {
|
|
226
|
+
const logger = consola.withTag("sd:cli:replace-deps:watch");
|
|
227
|
+
const entries: ReplaceDepEntry[] = [];
|
|
228
|
+
|
|
229
|
+
// 1. Workspace 패키지 경로 목록 수집
|
|
230
|
+
const searchRoots = [projectRoot];
|
|
231
|
+
|
|
232
|
+
const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
|
|
233
|
+
try {
|
|
234
|
+
const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf-8");
|
|
235
|
+
const workspaceGlobs = parseWorkspaceGlobs(yamlContent);
|
|
236
|
+
|
|
237
|
+
for (const pattern of workspaceGlobs) {
|
|
238
|
+
const dirs = await glob(pattern, { cwd: projectRoot, absolute: true });
|
|
239
|
+
searchRoots.push(...dirs);
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// pnpm-workspace.yaml가 없으면 루트만 처리
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 2. 각 searchRoot의 node_modules에서 매칭되는 패키지 찾기
|
|
246
|
+
for (const searchRoot of searchRoots) {
|
|
247
|
+
const nodeModulesDir = path.join(searchRoot, "node_modules");
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await fs.promises.access(nodeModulesDir);
|
|
251
|
+
} catch {
|
|
252
|
+
continue; // node_modules 없으면 스킵
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// replaceDeps의 각 glob 패턴으로 node_modules 내 디렉토리 탐색
|
|
256
|
+
const targetNames: string[] = [];
|
|
257
|
+
for (const pattern of Object.keys(replaceDeps)) {
|
|
258
|
+
const matches = await glob(pattern, { cwd: nodeModulesDir });
|
|
259
|
+
targetNames.push(...matches);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (targetNames.length === 0) continue;
|
|
263
|
+
|
|
264
|
+
// 패턴 매칭 및 경로 해석
|
|
265
|
+
const matchedEntries = resolveReplaceDepEntries(replaceDeps, targetNames);
|
|
266
|
+
|
|
267
|
+
// 3. entry 정보 수집 (symlink 해석 포함)
|
|
268
|
+
for (const { targetName, sourcePath } of matchedEntries) {
|
|
269
|
+
const targetPath = path.join(nodeModulesDir, targetName);
|
|
270
|
+
const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
|
|
271
|
+
|
|
272
|
+
// 소스 경로 존재 확인
|
|
273
|
+
try {
|
|
274
|
+
await fs.promises.access(resolvedSourcePath);
|
|
275
|
+
} catch {
|
|
276
|
+
logger.warn(`소스 경로가 존재하지 않아 스킵합니다: ${resolvedSourcePath}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// targetPath가 symlink면 realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
|
|
281
|
+
let actualTargetPath = targetPath;
|
|
282
|
+
try {
|
|
283
|
+
const stat = await fs.promises.lstat(targetPath);
|
|
284
|
+
if (stat.isSymbolicLink()) {
|
|
285
|
+
actualTargetPath = await fs.promises.realpath(targetPath);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// targetPath가 존재하지 않으면 그대로 사용
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
entries.push({
|
|
292
|
+
targetName,
|
|
293
|
+
sourcePath,
|
|
294
|
+
targetPath,
|
|
295
|
+
resolvedSourcePath,
|
|
296
|
+
actualTargetPath,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 4. 소스 디렉토리 watch 설정
|
|
302
|
+
const watchers: FsWatcher[] = [];
|
|
303
|
+
const watchedSources = new Set<string>();
|
|
304
|
+
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
if (watchedSources.has(entry.resolvedSourcePath)) continue;
|
|
307
|
+
watchedSources.add(entry.resolvedSourcePath);
|
|
308
|
+
|
|
309
|
+
const watcher = await FsWatcher.watch([entry.resolvedSourcePath], { followSymlinks: false });
|
|
310
|
+
watcher.onChange({ delay: 300 }, async (changeInfos) => {
|
|
311
|
+
for (const { path: changedPath } of changeInfos) {
|
|
312
|
+
// 제외 항목 필터링
|
|
313
|
+
const basename = path.basename(changedPath);
|
|
314
|
+
if (EXCLUDED_NAMES.has(basename)) continue;
|
|
315
|
+
|
|
316
|
+
// 이 소스 경로를 사용하는 모든 entry에 대해 복사
|
|
317
|
+
for (const e of entries) {
|
|
318
|
+
if (e.resolvedSourcePath !== entry.resolvedSourcePath) continue;
|
|
319
|
+
|
|
320
|
+
// 소스 경로 기준 상대 경로 계산
|
|
321
|
+
const relativePath = path.relative(e.resolvedSourcePath, changedPath);
|
|
322
|
+
const destPath = path.join(e.actualTargetPath, relativePath);
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
// 소스가 존재하는지 확인
|
|
326
|
+
let sourceExists = false;
|
|
327
|
+
try {
|
|
328
|
+
await fs.promises.access(changedPath);
|
|
329
|
+
sourceExists = true;
|
|
330
|
+
} catch {
|
|
331
|
+
// 소스가 삭제됨
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (sourceExists) {
|
|
335
|
+
// 소스가 디렉토리인지 파일인지 확인
|
|
336
|
+
const stat = await fs.promises.stat(changedPath);
|
|
337
|
+
if (stat.isDirectory()) {
|
|
338
|
+
await fsMkdir(destPath);
|
|
339
|
+
} else {
|
|
340
|
+
await fsMkdir(path.dirname(destPath));
|
|
341
|
+
await fsCopy(changedPath, destPath, replaceDepsCopyFilter);
|
|
342
|
+
}
|
|
343
|
+
logger.info(`복사: ${relativePath} → ${e.targetName}`);
|
|
344
|
+
} else {
|
|
345
|
+
// 소스가 삭제됨 → 대상도 삭제
|
|
346
|
+
await fsRm(destPath);
|
|
347
|
+
logger.info(`삭제: ${relativePath} (${e.targetName})`);
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logger.error(`복사 실패 (${e.targetName}/${relativePath}): ${err instanceof Error ? err.message : err}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
watchers.push(watcher);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
entries,
|
|
361
|
+
dispose: () => {
|
|
362
|
+
for (const watcher of watchers) {
|
|
363
|
+
void watcher.close();
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|