@simplysm/sd-cli 14.0.5 → 14.0.7
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/check.d.ts.map +1 -1
- package/dist/commands/check.js +10 -10
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +1 -4
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/replace-deps.js +1 -1
- package/dist/commands/replace-deps.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +5 -6
- package/dist/electron/electron.js.map +1 -1
- package/dist/engines/BaseEngine.d.ts.map +1 -1
- package/dist/engines/BaseEngine.js +1 -0
- package/dist/engines/BaseEngine.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/BuildOrchestrator.js +52 -45
- package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.js +32 -26
- package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
- package/dist/sd-cli.js +1 -1
- package/dist/sd-cli.js.map +1 -1
- package/dist/utils/esbuild-config.d.ts +7 -2
- package/dist/utils/esbuild-config.d.ts.map +1 -1
- package/dist/utils/esbuild-config.js +15 -12
- package/dist/utils/esbuild-config.js.map +1 -1
- package/dist/utils/output-utils.js +1 -1
- package/dist/utils/output-utils.js.map +1 -1
- package/dist/utils/package-utils.js +3 -3
- package/dist/utils/package-utils.js.map +1 -1
- package/dist/utils/rebuild-manager.js +3 -3
- package/dist/utils/rebuild-manager.js.map +1 -1
- package/dist/utils/replace-deps.js +11 -11
- package/dist/utils/replace-deps.js.map +1 -1
- package/dist/utils/vite-config.d.ts.map +1 -1
- package/dist/utils/vite-config.js +1 -2
- package/dist/utils/vite-config.js.map +1 -1
- package/dist/utils/worker-utils.js +1 -1
- package/dist/utils/worker-utils.js.map +1 -1
- package/dist/workers/server-build.worker.js +2 -2
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-runtime.worker.js +13 -13
- package/dist/workers/server-runtime.worker.js.map +1 -1
- package/package.json +4 -5
- package/src/commands/check.ts +12 -11
- package/src/commands/lint.ts +1 -3
- package/src/commands/replace-deps.ts +1 -1
- package/src/electron/electron.ts +7 -7
- package/src/engines/BaseEngine.ts +1 -0
- package/src/index.ts +3 -0
- package/src/orchestrators/BuildOrchestrator.ts +52 -45
- package/src/orchestrators/DevWatchOrchestrator.ts +33 -26
- package/src/sd-cli.ts +1 -1
- package/src/utils/esbuild-config.ts +16 -12
- package/src/utils/output-utils.ts +1 -1
- package/src/utils/package-utils.ts +3 -3
- package/src/utils/rebuild-manager.ts +3 -3
- package/src/utils/replace-deps.ts +11 -11
- package/src/utils/vite-config.ts +1 -2
- package/src/utils/worker-utils.ts +1 -1
- package/src/workers/server-build.worker.ts +2 -2
- package/src/workers/server-runtime.worker.ts +13 -13
- package/tests/commands/check.spec.ts +11 -26
- package/tests/electron/electron.spec.ts +42 -35
- package/tests/orchestrators/build-orchestrator.spec.ts +4 -9
- package/tests/orchestrators/dev-watch-orchestrator.spec.ts +3 -5
- package/tests/utils/esbuild-config.spec.ts +38 -8
- package/tests/utils/vite-config.spec.ts +13 -0
|
@@ -199,7 +199,7 @@ async function resolveAllReplaceDepEntries(
|
|
|
199
199
|
try {
|
|
200
200
|
await fs.promises.access(resolvedSourcePath);
|
|
201
201
|
} catch {
|
|
202
|
-
logger.warn(
|
|
202
|
+
logger.warn(`소스 경로가 존재하지 않아 건너뜀: ${resolvedSourcePath}`);
|
|
203
203
|
continue;
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -244,7 +244,7 @@ export async function setupReplaceDeps(
|
|
|
244
244
|
const logger = consola.withTag("sd:cli:replace-deps");
|
|
245
245
|
let setupCount = 0;
|
|
246
246
|
|
|
247
|
-
logger.start("
|
|
247
|
+
logger.start("replace-deps 설정 중...");
|
|
248
248
|
|
|
249
249
|
const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
|
|
250
250
|
|
|
@@ -255,11 +255,11 @@ export async function setupReplaceDeps(
|
|
|
255
255
|
|
|
256
256
|
setupCount += 1;
|
|
257
257
|
} catch (err) {
|
|
258
|
-
logger.error(`
|
|
258
|
+
logger.error(`[${targetName}] 복사 실패: ${err instanceof Error ? err.message : err}`);
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
logger.success(`
|
|
262
|
+
logger.success(`replace-deps 설정 완료 (${setupCount}개 의존성 교체)`);
|
|
263
263
|
|
|
264
264
|
// Run postinstall scripts from replaced packages
|
|
265
265
|
for (const { targetName, resolvedSourcePath, actualTargetPath } of entries) {
|
|
@@ -269,13 +269,13 @@ export async function setupReplaceDeps(
|
|
|
269
269
|
const postinstall = pkgJson.scripts?.postinstall as string | undefined;
|
|
270
270
|
if (postinstall == null) continue;
|
|
271
271
|
|
|
272
|
-
logger.warn(`
|
|
273
|
-
logger.start(`
|
|
272
|
+
logger.warn(`[${targetName}] postinstall 스크립트 실행: ${postinstall}`);
|
|
273
|
+
logger.start(`[${targetName}] postinstall 실행 중...`);
|
|
274
274
|
await promisify(exec)(postinstall, { cwd: actualTargetPath });
|
|
275
|
-
logger.success(`
|
|
275
|
+
logger.success(`[${targetName}] postinstall 실행 완료`);
|
|
276
276
|
} catch (err) {
|
|
277
277
|
logger.error(
|
|
278
|
-
`
|
|
278
|
+
`[${targetName}] postinstall 실패: ${err instanceof Error ? err.message : err}`,
|
|
279
279
|
);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
@@ -305,7 +305,7 @@ export async function watchReplaceDeps(
|
|
|
305
305
|
const watchers: FsWatcher[] = [];
|
|
306
306
|
const watchedSources = new Set<string>();
|
|
307
307
|
|
|
308
|
-
logger.start(`
|
|
308
|
+
logger.start(`replace-deps 워치 시작 중... (${entries.length}개 대상)`);
|
|
309
309
|
|
|
310
310
|
for (const entry of entries) {
|
|
311
311
|
if (watchedSources.has(entry.resolvedSourcePath)) continue;
|
|
@@ -359,7 +359,7 @@ export async function watchReplaceDeps(
|
|
|
359
359
|
}
|
|
360
360
|
} catch (err) {
|
|
361
361
|
logger.error(
|
|
362
|
-
`
|
|
362
|
+
`[${e.targetName}] 복사 실패 (${relativePath}): ${err instanceof Error ? err.message : err}`,
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
365
|
}
|
|
@@ -369,7 +369,7 @@ export async function watchReplaceDeps(
|
|
|
369
369
|
watchers.push(watcher);
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
logger.success(
|
|
372
|
+
logger.success("replace-deps 워치 준비 완료");
|
|
373
373
|
|
|
374
374
|
return {
|
|
375
375
|
entries,
|
package/src/utils/vite-config.ts
CHANGED
|
@@ -82,10 +82,9 @@ export async function createClientViteConfig(
|
|
|
82
82
|
: [options.browserslist]
|
|
83
83
|
: undefined;
|
|
84
84
|
|
|
85
|
-
// define: 환경변수 주입
|
|
85
|
+
// define: 환경변수 주입 (import.meta.env.KEY → Vite가 bare import.meta.env 객체를 자동 구성)
|
|
86
86
|
const define: Record<string, string> = {};
|
|
87
87
|
if (options.env != null) {
|
|
88
|
-
define["process.env"] = JSON.stringify(options.env);
|
|
89
88
|
for (const [key, value] of Object.entries(options.env)) {
|
|
90
89
|
define[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
91
90
|
}
|
|
@@ -142,7 +142,7 @@ async function cleanup(): Promise<void> {
|
|
|
142
142
|
* Uses single-pass dependency tree traversal via collectAllDependencyExternals.
|
|
143
143
|
*/
|
|
144
144
|
function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
|
|
145
|
-
logger.debug("
|
|
145
|
+
logger.debug("의존성 트리 스캔 중...");
|
|
146
146
|
const { optionalPeerDeps, nativeModules } = collectAllDependencyExternals(pkgDir);
|
|
147
147
|
|
|
148
148
|
const manual = manualExternals ?? [];
|
|
@@ -638,7 +638,7 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
|
|
|
638
638
|
const result = await rebuildAll();
|
|
639
639
|
sender.send("build", result);
|
|
640
640
|
} else {
|
|
641
|
-
logger.debug("
|
|
641
|
+
logger.debug("변경된 파일이 빌드에 포함되지 않아 리빌드 건너뜀");
|
|
642
642
|
}
|
|
643
643
|
} catch (err) {
|
|
644
644
|
sender.send("error", { message: errNs.message(err) });
|
|
@@ -63,7 +63,7 @@ async function cleanup(): Promise<void> {
|
|
|
63
63
|
// Catch runtime errors that occur after server listen() and send them as a custom "error" event
|
|
64
64
|
// (Without this handler, the worker will crash but dev.ts's buildResolver won't be called, causing listr to hang)
|
|
65
65
|
process.on("uncaughtException", (err) => {
|
|
66
|
-
logger.error("
|
|
66
|
+
logger.error("서버 런타임 미처리 에러", err);
|
|
67
67
|
sender.send("error", {
|
|
68
68
|
message: errNs.message(err),
|
|
69
69
|
});
|
|
@@ -72,7 +72,7 @@ process.on("uncaughtException", (err) => {
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
process.on("unhandledRejection", (reason) => {
|
|
75
|
-
logger.error("
|
|
75
|
+
logger.error("서버 런타임 미처리 Promise 거부", reason);
|
|
76
76
|
sender.send("error", {
|
|
77
77
|
message: errNs.message(reason),
|
|
78
78
|
});
|
|
@@ -127,10 +127,10 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// Import main.js (must export a server instance)
|
|
130
|
-
logger.debug("
|
|
130
|
+
logger.debug("main.js 임포트 중...");
|
|
131
131
|
let stepStart = performance.now();
|
|
132
132
|
const module = await import(pathToFileURL(info.mainJsPath).href);
|
|
133
|
-
logger.debug(`
|
|
133
|
+
logger.debug(`main.js 임포트 완료 (${Math.round(performance.now() - stepStart)}ms)`);
|
|
134
134
|
const server = module.server;
|
|
135
135
|
|
|
136
136
|
if (server == null) {
|
|
@@ -143,7 +143,7 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
|
|
|
143
143
|
// Register client proxies (before listen)
|
|
144
144
|
if (info.clientPorts != null && Object.keys(info.clientPorts).length > 0) {
|
|
145
145
|
for (const [name, port] of Object.entries(info.clientPorts)) {
|
|
146
|
-
logger.debug(
|
|
146
|
+
logger.debug(`프록시 등록: /${name} → http://127.0.0.1:${String(port)}`);
|
|
147
147
|
await server.fastify.register(proxy, {
|
|
148
148
|
prefix: `/${name}`,
|
|
149
149
|
upstream: `http://127.0.0.1:${port}`,
|
|
@@ -154,31 +154,31 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Find available port (auto-increment on port conflict)
|
|
157
|
-
logger.debug("
|
|
157
|
+
logger.debug("사용 가능한 포트 탐색 중...");
|
|
158
158
|
stepStart = performance.now();
|
|
159
159
|
const originalPort = server.options.port;
|
|
160
160
|
const availablePort = await findAvailablePort(originalPort);
|
|
161
161
|
if (availablePort !== originalPort) {
|
|
162
|
-
logger.info(
|
|
162
|
+
logger.info(`포트 ${originalPort} 사용 중, ${availablePort}로 변경`);
|
|
163
163
|
server.options.port = availablePort;
|
|
164
164
|
}
|
|
165
165
|
logger.debug(
|
|
166
|
-
|
|
166
|
+
`포트 ${String(availablePort)} 사용 가능 (${Math.round(performance.now() - stepStart)}ms)`,
|
|
167
167
|
);
|
|
168
168
|
|
|
169
|
-
//
|
|
170
|
-
logger.debug("
|
|
169
|
+
// 서버 시작
|
|
170
|
+
logger.debug("서버 리슨 시작...");
|
|
171
171
|
stepStart = performance.now();
|
|
172
172
|
await server.listen();
|
|
173
|
-
logger.debug(
|
|
173
|
+
logger.debug(`서버 리슨 완료 (${Math.round(performance.now() - stepStart)}ms)`);
|
|
174
174
|
|
|
175
175
|
logger.debug(
|
|
176
|
-
|
|
176
|
+
`런타임 총 시작 시간: ${Math.round(performance.now() - startTime)}ms`,
|
|
177
177
|
);
|
|
178
178
|
|
|
179
179
|
sender.send("serverReady", { port: server.options.port });
|
|
180
180
|
} catch (err) {
|
|
181
|
-
logger.error("
|
|
181
|
+
logger.error("서버 런타임 시작 실패", err);
|
|
182
182
|
sender.send("error", {
|
|
183
183
|
message: errNs.message(err),
|
|
184
184
|
});
|
|
@@ -117,21 +117,20 @@ describe("runCheck", () => {
|
|
|
117
117
|
writeSpy.mockRestore();
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it("runs typecheck+lint and test
|
|
120
|
+
it("runs typecheck+lint and test", async () => {
|
|
121
121
|
await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
|
|
122
122
|
|
|
123
123
|
expect(mocks.executeTypecheck).toHaveBeenCalled();
|
|
124
124
|
expect(mocks.execa).toHaveBeenCalled();
|
|
125
|
-
expect(stdoutOutput).toContain("ALL PASSED");
|
|
126
125
|
});
|
|
127
126
|
|
|
128
|
-
it("outputs results in TYPECHECK → LINT → TEST →
|
|
127
|
+
it("outputs results in TYPECHECK → LINT → TEST → 요약 order", async () => {
|
|
129
128
|
await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
|
|
130
129
|
|
|
131
130
|
const tcIdx = stdoutOutput.indexOf("TYPECHECK");
|
|
132
131
|
const lintIdx = stdoutOutput.indexOf("LINT");
|
|
133
132
|
const testIdx = stdoutOutput.indexOf("TEST");
|
|
134
|
-
const summaryIdx = stdoutOutput.indexOf("
|
|
133
|
+
const summaryIdx = stdoutOutput.indexOf("요약");
|
|
135
134
|
|
|
136
135
|
expect(tcIdx).toBeLessThan(lintIdx);
|
|
137
136
|
expect(lintIdx).toBeLessThan(testIdx);
|
|
@@ -146,7 +145,7 @@ describe("runCheck", () => {
|
|
|
146
145
|
expect(mocks.execa).toHaveBeenCalled();
|
|
147
146
|
});
|
|
148
147
|
|
|
149
|
-
it("
|
|
148
|
+
it("sets exitCode 1 when typecheck fails", async () => {
|
|
150
149
|
mocks.executeTypecheck.mockResolvedValue({
|
|
151
150
|
success: false, errorCount: 2, warningCount: 0, formattedOutput: "type errors",
|
|
152
151
|
lint: { success: true, errorCount: 0, warningCount: 0, formattedOutput: "" },
|
|
@@ -155,8 +154,6 @@ describe("runCheck", () => {
|
|
|
155
154
|
|
|
156
155
|
await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
|
|
157
156
|
|
|
158
|
-
expect(stdoutOutput).toContain("FAILED");
|
|
159
|
-
expect(stdoutOutput).toContain("typecheck");
|
|
160
157
|
expect(process.exitCode).toBe(1);
|
|
161
158
|
});
|
|
162
159
|
|
|
@@ -170,14 +167,13 @@ describe("runCheck", () => {
|
|
|
170
167
|
expect(process.exitCode).toBe(1);
|
|
171
168
|
});
|
|
172
169
|
|
|
173
|
-
it("
|
|
170
|
+
it("sets exitCode 1 when test fails", async () => {
|
|
174
171
|
mocks.execa.mockResolvedValue({
|
|
175
172
|
stdout: "3 tests failed", stderr: "", exitCode: 1,
|
|
176
173
|
});
|
|
177
174
|
|
|
178
175
|
await runCheck({ targets: [], types: ["test"], fix: false });
|
|
179
176
|
|
|
180
|
-
expect(stdoutOutput).toContain("3 failed");
|
|
181
177
|
expect(process.exitCode).toBe(1);
|
|
182
178
|
});
|
|
183
179
|
|
|
@@ -271,8 +267,8 @@ describe("runCheck", () => {
|
|
|
271
267
|
expect(mocks.runLintInWorker).not.toHaveBeenCalled();
|
|
272
268
|
});
|
|
273
269
|
|
|
274
|
-
// Scenario:
|
|
275
|
-
it("
|
|
270
|
+
// Scenario: lint 실패 시 exitCode 설정
|
|
271
|
+
it("sets exitCode 1 when engine lint fails", async () => {
|
|
276
272
|
mocks.executeTypecheck.mockResolvedValue({
|
|
277
273
|
success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
|
|
278
274
|
lint: { success: false, errorCount: 3, warningCount: 1, formattedOutput: "some lint output" },
|
|
@@ -281,10 +277,7 @@ describe("runCheck", () => {
|
|
|
281
277
|
|
|
282
278
|
await runCheck({ targets: [], types: ["typecheck", "lint"], fix: false });
|
|
283
279
|
|
|
284
|
-
expect(
|
|
285
|
-
expect(stdoutOutput).toContain("LINT");
|
|
286
|
-
expect(stdoutOutput).toContain("3 errors");
|
|
287
|
-
expect(stdoutOutput).toContain("1 warnings");
|
|
280
|
+
expect(process.exitCode).toBe(1);
|
|
288
281
|
});
|
|
289
282
|
|
|
290
283
|
// Scenario: check에서 scripts 패키지의 lint가 별도 실행된다
|
|
@@ -391,43 +384,35 @@ describe("runCheck", () => {
|
|
|
391
384
|
});
|
|
392
385
|
|
|
393
386
|
// Scenario: lint 에러 없음
|
|
394
|
-
it("
|
|
387
|
+
it("does not set exitCode when lint passes", async () => {
|
|
395
388
|
mocks.executeLint.mockResolvedValue({
|
|
396
389
|
success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
|
|
397
390
|
});
|
|
398
391
|
|
|
399
392
|
await runCheck({ targets: [], types: ["lint"], fix: false });
|
|
400
393
|
|
|
401
|
-
expect(stdoutOutput).toContain("LINT");
|
|
402
|
-
expect(stdoutOutput).toContain("✔ 0 errors, 0 warnings");
|
|
403
|
-
expect(stdoutOutput).toContain("✔ ALL PASSED");
|
|
404
394
|
expect(process.exitCode).toBeUndefined();
|
|
405
395
|
});
|
|
406
396
|
|
|
407
397
|
// Scenario: lint 에러 발생
|
|
408
|
-
it("
|
|
398
|
+
it("sets exitCode 1 when lint has errors", async () => {
|
|
409
399
|
mocks.executeLint.mockResolvedValue({
|
|
410
400
|
success: false, errorCount: 3, warningCount: 2, formattedOutput: "lint errors",
|
|
411
401
|
});
|
|
412
402
|
|
|
413
403
|
await runCheck({ targets: [], types: ["lint"], fix: false });
|
|
414
404
|
|
|
415
|
-
expect(stdoutOutput).toContain("LINT");
|
|
416
|
-
expect(stdoutOutput).toContain("✖ 3 errors, 2 warnings");
|
|
417
|
-
expect(stdoutOutput).toContain("✖ 1/1 FAILED (lint)");
|
|
418
405
|
expect(process.exitCode).toBe(1);
|
|
419
406
|
});
|
|
420
407
|
|
|
421
408
|
// Scenario: lint 대상 파일 없음
|
|
422
|
-
it("
|
|
409
|
+
it("does not set exitCode when no files to lint", async () => {
|
|
423
410
|
mocks.executeLint.mockResolvedValue({
|
|
424
411
|
success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
|
|
425
412
|
});
|
|
426
413
|
|
|
427
414
|
await runCheck({ targets: [], types: ["lint"], fix: false });
|
|
428
415
|
|
|
429
|
-
expect(stdoutOutput).toContain("✔ 0 errors, 0 warnings");
|
|
430
|
-
expect(stdoutOutput).toContain("✔ ALL PASSED");
|
|
431
416
|
expect(process.exitCode).toBeUndefined();
|
|
432
417
|
});
|
|
433
418
|
|
|
@@ -278,6 +278,25 @@ describe("Electron", () => {
|
|
|
278
278
|
|
|
279
279
|
await expect(electron.build("/fake/out")).rejects.toThrow("electron-main.ts");
|
|
280
280
|
});
|
|
281
|
+
|
|
282
|
+
it("config.env를 esbuild banner로 주입한다 (ELECTRON_DEV_URL 미포함)", async () => {
|
|
283
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
284
|
+
|
|
285
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
286
|
+
appId: "com.test.app",
|
|
287
|
+
env: { API_URL: "https://api.example.com" },
|
|
288
|
+
});
|
|
289
|
+
await electron.build("/fake/out");
|
|
290
|
+
|
|
291
|
+
const callArgs = mockEsbuildBuild.mock.calls[0][0];
|
|
292
|
+
const banner = callArgs.banner?.js as string;
|
|
293
|
+
expect(banner).toContain("process.env");
|
|
294
|
+
expect(banner).toContain("??=");
|
|
295
|
+
expect(banner).toContain("API_URL");
|
|
296
|
+
expect(banner).toContain("https://api.example.com");
|
|
297
|
+
expect(banner).not.toContain("ELECTRON_DEV_URL");
|
|
298
|
+
expect(callArgs.define).toBeUndefined();
|
|
299
|
+
});
|
|
281
300
|
});
|
|
282
301
|
|
|
283
302
|
//#endregion
|
|
@@ -440,7 +459,7 @@ describe("Electron", () => {
|
|
|
440
459
|
return { electronKill, resolveElectron: () => resolveElectron() };
|
|
441
460
|
}
|
|
442
461
|
|
|
443
|
-
it("creates esbuild context and spawns Electron
|
|
462
|
+
it("creates esbuild context with banner for env and spawns Electron", async () => {
|
|
444
463
|
const { resolveElectron } = setupExecaForRun();
|
|
445
464
|
|
|
446
465
|
const { Electron } = await import("../../src/electron/electron.js");
|
|
@@ -455,28 +474,20 @@ describe("Electron", () => {
|
|
|
455
474
|
resolveElectron();
|
|
456
475
|
await runPromise;
|
|
457
476
|
|
|
458
|
-
// esbuild.context was called
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
);
|
|
473
|
-
expect(electronCall).toBeDefined();
|
|
474
|
-
expect(electronCall![2].env).toEqual(
|
|
475
|
-
expect.objectContaining({
|
|
476
|
-
NODE_ENV: "development",
|
|
477
|
-
ELECTRON_DEV_URL: "http://localhost:4200",
|
|
478
|
-
}),
|
|
479
|
-
);
|
|
477
|
+
// esbuild.context was called with banner containing ELECTRON_DEV_URL
|
|
478
|
+
const callArgs = mockEsbuildContext.mock.calls[0][0];
|
|
479
|
+
const banner = callArgs.banner?.js as string;
|
|
480
|
+
expect(banner).toContain("process.env");
|
|
481
|
+
expect(banner).toContain("??=");
|
|
482
|
+
expect(banner).toContain("ELECTRON_DEV_URL");
|
|
483
|
+
expect(banner).toContain("http://localhost:4200");
|
|
484
|
+
expect(callArgs.define).toBeUndefined();
|
|
485
|
+
|
|
486
|
+
expect(callArgs.platform).toBe("node");
|
|
487
|
+
expect(callArgs.target).toBe("node20");
|
|
488
|
+
expect(callArgs.format).toBe("cjs");
|
|
489
|
+
expect(callArgs.bundle).toBe(true);
|
|
490
|
+
expect(callArgs.external).toContain("electron");
|
|
480
491
|
}, 10_000);
|
|
481
492
|
|
|
482
493
|
it("throws when electron-main.ts entry point is missing", async () => {
|
|
@@ -513,7 +524,7 @@ describe("Electron", () => {
|
|
|
513
524
|
});
|
|
514
525
|
|
|
515
526
|
describe("단위: run() 플러그인 동작", () => {
|
|
516
|
-
it("passes custom env and ELECTRON_DEV_URL
|
|
527
|
+
it("passes custom env and ELECTRON_DEV_URL via esbuild banner", async () => {
|
|
517
528
|
let resolveElectron: () => void = () => {};
|
|
518
529
|
mockExeca.mockImplementation((cmd: string) => {
|
|
519
530
|
if (typeof cmd === "string" && cmd.includes("electron")) {
|
|
@@ -537,17 +548,13 @@ describe("Electron", () => {
|
|
|
537
548
|
resolveElectron();
|
|
538
549
|
await runPromise;
|
|
539
550
|
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
);
|
|
543
|
-
expect(
|
|
544
|
-
expect(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
ELECTRON_DEV_URL: "http://localhost:5555",
|
|
548
|
-
CUSTOM_VAR: "test-value",
|
|
549
|
-
}),
|
|
550
|
-
);
|
|
551
|
+
const callArgs = mockEsbuildContext.mock.calls[0][0];
|
|
552
|
+
const banner = callArgs.banner?.js as string;
|
|
553
|
+
expect(banner).toContain("ELECTRON_DEV_URL");
|
|
554
|
+
expect(banner).toContain("http://localhost:5555");
|
|
555
|
+
expect(banner).toContain("CUSTOM_VAR");
|
|
556
|
+
expect(banner).toContain("test-value");
|
|
557
|
+
expect(callArgs.define).toBeUndefined();
|
|
551
558
|
}, 10_000);
|
|
552
559
|
|
|
553
560
|
it("calls initialize() before starting esbuild context", async () => {
|
|
@@ -221,7 +221,7 @@ describe("BuildOrchestrator.initialize", () => {
|
|
|
221
221
|
);
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
-
it("
|
|
224
|
+
it("returns false when only scripts packages exist", async () => {
|
|
225
225
|
setupDefaults({
|
|
226
226
|
packages: {
|
|
227
227
|
"sd-claude": { target: "scripts" } as any,
|
|
@@ -230,10 +230,9 @@ describe("BuildOrchestrator.initialize", () => {
|
|
|
230
230
|
|
|
231
231
|
const orchestrator = new BuildOrchestrator({ targets: [], options: [] });
|
|
232
232
|
await orchestrator.initialize();
|
|
233
|
+
const hasError = await orchestrator.start();
|
|
233
234
|
|
|
234
|
-
expect(
|
|
235
|
-
expect.stringContaining("No packages to build"),
|
|
236
|
-
);
|
|
235
|
+
expect(hasError).toBe(false);
|
|
237
236
|
});
|
|
238
237
|
});
|
|
239
238
|
|
|
@@ -403,10 +402,9 @@ describe("BuildOrchestrator.start", () => {
|
|
|
403
402
|
const hasError = await orchestrator.start();
|
|
404
403
|
|
|
405
404
|
expect(hasError).toBe(false);
|
|
406
|
-
expect(mockLogger.info).toHaveBeenCalledWith("Build completed");
|
|
407
405
|
});
|
|
408
406
|
|
|
409
|
-
it("returns true
|
|
407
|
+
it("returns true when any build fails", async () => {
|
|
410
408
|
setupDefaults({
|
|
411
409
|
packages: {
|
|
412
410
|
"core-common": { target: "neutral", publish: { type: "npm" } },
|
|
@@ -429,7 +427,6 @@ describe("BuildOrchestrator.start", () => {
|
|
|
429
427
|
const hasError = await orchestrator.start();
|
|
430
428
|
|
|
431
429
|
expect(hasError).toBe(true);
|
|
432
|
-
expect(mockLogger.error).toHaveBeenCalledWith("Build failed");
|
|
433
430
|
});
|
|
434
431
|
|
|
435
432
|
it("returns false when no packages to build", async () => {
|
|
@@ -658,7 +655,6 @@ describe("BuildOrchestrator client build", () => {
|
|
|
658
655
|
const hasError = await orchestrator.start();
|
|
659
656
|
|
|
660
657
|
expect(hasError).toBe(true);
|
|
661
|
-
expect(mockLogger.error).toHaveBeenCalledWith("Build failed");
|
|
662
658
|
});
|
|
663
659
|
|
|
664
660
|
// Acceptance: Scenario "BuildOrchestrator가 env를 ViteEngine에 전달" + "프로덕션 빌드의 baseEnv"
|
|
@@ -1049,7 +1045,6 @@ describe("BuildOrchestrator lint integration", () => {
|
|
|
1049
1045
|
const hasError = await orchestrator.start();
|
|
1050
1046
|
|
|
1051
1047
|
expect(hasError).toBe(true);
|
|
1052
|
-
expect(mockLogger.error).toHaveBeenCalledWith("Build failed");
|
|
1053
1048
|
});
|
|
1054
1049
|
|
|
1055
1050
|
// Scenario: build에서 scripts 패키지는 제외된다
|
|
@@ -300,7 +300,6 @@ describe("DevWatchOrchestrator", () => {
|
|
|
300
300
|
const orchestrator = new DevWatchOrchestrator({ mode: "watch", targets: [], options: [] });
|
|
301
301
|
await orchestrator.initialize();
|
|
302
302
|
|
|
303
|
-
expect(process.stdout.write).toHaveBeenCalledWith(expect.stringContaining("No packages"));
|
|
304
303
|
expect(createBuildEngine).not.toHaveBeenCalled();
|
|
305
304
|
});
|
|
306
305
|
|
|
@@ -872,7 +871,6 @@ describe("DevWatchOrchestrator", () => {
|
|
|
872
871
|
|
|
873
872
|
expect(mockBuildEngines[0].stop).toHaveBeenCalledOnce();
|
|
874
873
|
expect(mockRuntimeProxies[0].terminate).toHaveBeenCalled();
|
|
875
|
-
expect(process.stdout.write).toHaveBeenCalledWith(expect.stringContaining("Shutting down"));
|
|
876
874
|
});
|
|
877
875
|
|
|
878
876
|
// --- Acceptance: dev에서 replaceDeps 감시 ---
|
|
@@ -890,8 +888,8 @@ describe("DevWatchOrchestrator", () => {
|
|
|
890
888
|
expect(watchReplaceDeps).toHaveBeenCalledWith("/test-root", replaceDeps);
|
|
891
889
|
});
|
|
892
890
|
|
|
893
|
-
// Unit:
|
|
894
|
-
it("
|
|
891
|
+
// Unit: no server packages in dev mode — does not create runtime proxies
|
|
892
|
+
it("does not create runtime proxies when no server packages in dev mode", async () => {
|
|
895
893
|
setupDefaults(createConfig({
|
|
896
894
|
packages: { "core-common": { target: "node" } },
|
|
897
895
|
}));
|
|
@@ -899,7 +897,7 @@ describe("DevWatchOrchestrator", () => {
|
|
|
899
897
|
const orchestrator = new DevWatchOrchestrator({ mode: "dev", targets: [], options: [] });
|
|
900
898
|
await orchestrator.initialize();
|
|
901
899
|
|
|
902
|
-
expect(
|
|
900
|
+
expect(createBuildEngine).not.toHaveBeenCalled();
|
|
903
901
|
});
|
|
904
902
|
|
|
905
903
|
// Unit: multiple server packages
|
|
@@ -17,7 +17,7 @@ vi.mock("consola", () => ({
|
|
|
17
17
|
},
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
-
const { createServerEsbuildOptions, writeChangedOutputFiles } =
|
|
20
|
+
const { createServerEsbuildOptions, createEnvBanner, writeChangedOutputFiles } =
|
|
21
21
|
await import("../../src/utils/esbuild-config");
|
|
22
22
|
|
|
23
23
|
const { default: mockFs } = await import("fs/promises");
|
|
@@ -53,20 +53,25 @@ describe("createServerEsbuildOptions", () => {
|
|
|
53
53
|
expect((result.banner as Record<string, string>)["js"]).toContain("import.meta.url");
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it("
|
|
56
|
+
it("injects env vars via banner (process.env merge) instead of define", () => {
|
|
57
57
|
const result = createServerEsbuildOptions({
|
|
58
58
|
...baseOptions,
|
|
59
59
|
env: { API_URL: "https://api.example.com", NODE_ENV: "production" },
|
|
60
60
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const banner = (result.banner as Record<string, string>)["js"];
|
|
62
|
+
expect(banner).toContain("process.env");
|
|
63
|
+
expect(banner).toContain("??=");
|
|
64
|
+
expect(banner).toContain("API_URL");
|
|
65
|
+
expect(banner).toContain("https://api.example.com");
|
|
66
|
+
expect(result.define).toBeUndefined();
|
|
65
67
|
});
|
|
66
68
|
|
|
67
|
-
it("
|
|
69
|
+
it("does not include env code in banner when env is not provided", () => {
|
|
68
70
|
const result = createServerEsbuildOptions(baseOptions);
|
|
69
|
-
|
|
71
|
+
const banner = (result.banner as Record<string, string>)["js"];
|
|
72
|
+
expect(banner).toContain("createRequire");
|
|
73
|
+
expect(banner).not.toContain("??=");
|
|
74
|
+
expect(result.define).toBeUndefined();
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
it("passes external modules to esbuild", () => {
|
|
@@ -88,6 +93,31 @@ describe("createServerEsbuildOptions", () => {
|
|
|
88
93
|
});
|
|
89
94
|
});
|
|
90
95
|
|
|
96
|
+
describe("createEnvBanner", () => {
|
|
97
|
+
it("generates process.env merge code with ??= for runtime override", () => {
|
|
98
|
+
const banner = createEnvBanner({ API_URL: "https://api.example.com", NODE_ENV: "production" });
|
|
99
|
+
expect(banner).toContain("process.env");
|
|
100
|
+
expect(banner).toContain("??=");
|
|
101
|
+
expect(banner).toContain('"API_URL"');
|
|
102
|
+
expect(banner).toContain('"https://api.example.com"');
|
|
103
|
+
expect(banner).toContain('"NODE_ENV"');
|
|
104
|
+
expect(banner).toContain('"production"');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns empty string when env is undefined", () => {
|
|
108
|
+
expect(createEnvBanner()).toBe("");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns empty string when env is empty object", () => {
|
|
112
|
+
expect(createEnvBanner({})).toBe("");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("JSON-encodes special characters in values", () => {
|
|
116
|
+
const banner = createEnvBanner({ MSG: 'hello "world"' });
|
|
117
|
+
expect(banner).toContain('\\"world\\"');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
91
121
|
describe("writeChangedOutputFiles", () => {
|
|
92
122
|
beforeEach(() => {
|
|
93
123
|
vi.mocked(mockFs.readFile).mockReset();
|
|
@@ -67,6 +67,19 @@ afterEach(() => {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
describe("createClientViteConfig", () => {
|
|
70
|
+
// Acceptance: Scenario "define['process.env'] 제거"
|
|
71
|
+
it("does not include process.env in define, only import.meta.env keys", async () => {
|
|
72
|
+
const config = await createClientViteConfig({
|
|
73
|
+
...createDefaultOptions(),
|
|
74
|
+
env: { DEV: "true", VER: "1.0.0" },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const define = config.define as Record<string, string>;
|
|
78
|
+
expect(define).not.toHaveProperty("process.env");
|
|
79
|
+
expect(define["import.meta.env.DEV"]).toBe('"true"');
|
|
80
|
+
expect(define["import.meta.env.VER"]).toBe('"1.0.0"');
|
|
81
|
+
});
|
|
82
|
+
|
|
70
83
|
// Acceptance: Scenario "browserslist 미설정 시 최신 브라우저 유지"
|
|
71
84
|
it("uses es2022 esbuild target when no browserslist is provided", async () => {
|
|
72
85
|
const config = await createClientViteConfig(createDefaultOptions());
|