@simplysm/sd-cli 14.0.6 → 14.0.8
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/capacitor/capacitor.d.ts +1 -1
- package/dist/capacitor/capacitor.d.ts.map +1 -1
- package/dist/capacitor/capacitor.js +3 -4
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +12 -13
- package/dist/commands/publish.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +24 -19
- 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/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.js +9 -3
- package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
- package/dist/sd-cli.js +6 -6
- 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/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/vitest-plugin.d.ts.map +1 -1
- package/dist/vitest-plugin.js +2 -0
- package/dist/vitest-plugin.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +2 -3
- package/dist/workers/server-build.worker.js.map +1 -1
- package/package.json +4 -6
- package/src/capacitor/capacitor.ts +3 -4
- package/src/commands/check.ts +2 -2
- package/src/commands/publish.ts +12 -13
- package/src/electron/electron.ts +28 -21
- package/src/engines/BaseEngine.ts +1 -0
- package/src/orchestrators/DevWatchOrchestrator.ts +10 -3
- package/src/sd-cli.ts +9 -6
- package/src/utils/esbuild-config.ts +16 -12
- package/src/utils/vite-config.ts +1 -2
- package/src/vitest-plugin.ts +5 -0
- package/src/workers/server-build.worker.ts +2 -3
- package/tests/capacitor/capacitor-build.spec.ts +9 -7
- package/tests/capacitor/capacitor-icon.spec.ts +9 -7
- package/tests/capacitor/capacitor-init.spec.ts +8 -6
- package/tests/capacitor/capacitor-run.spec.ts +13 -11
- package/tests/capacitor/capacitor-workspace.spec.ts +8 -6
- package/tests/commands/check.spec.ts +16 -28
- package/tests/commands/publish.spec.ts +4 -4
- package/tests/electron/electron.spec.ts +69 -58
- 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
- package/tests/workers/server-build-worker.spec.ts +6 -3
|
@@ -35,8 +35,11 @@ vi.mock("../../src/utils/sd-config", () => ({
|
|
|
35
35
|
loadSdConfig: mocks.loadSdConfig,
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
|
-
vi.mock("
|
|
39
|
-
|
|
38
|
+
vi.mock("@simplysm/core-node", () => ({
|
|
39
|
+
cpx: {
|
|
40
|
+
exec: mocks.execa,
|
|
41
|
+
execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
|
|
42
|
+
},
|
|
40
43
|
}));
|
|
41
44
|
|
|
42
45
|
vi.mock("../../src/utils/package-utils", async (importOriginal) => {
|
|
@@ -117,21 +120,20 @@ describe("runCheck", () => {
|
|
|
117
120
|
writeSpy.mockRestore();
|
|
118
121
|
});
|
|
119
122
|
|
|
120
|
-
it("runs typecheck+lint and test
|
|
123
|
+
it("runs typecheck+lint and test", async () => {
|
|
121
124
|
await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
|
|
122
125
|
|
|
123
126
|
expect(mocks.executeTypecheck).toHaveBeenCalled();
|
|
124
127
|
expect(mocks.execa).toHaveBeenCalled();
|
|
125
|
-
expect(stdoutOutput).toContain("ALL PASSED");
|
|
126
128
|
});
|
|
127
129
|
|
|
128
|
-
it("outputs results in TYPECHECK → LINT → TEST →
|
|
130
|
+
it("outputs results in TYPECHECK → LINT → TEST → 요약 order", async () => {
|
|
129
131
|
await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
|
|
130
132
|
|
|
131
133
|
const tcIdx = stdoutOutput.indexOf("TYPECHECK");
|
|
132
134
|
const lintIdx = stdoutOutput.indexOf("LINT");
|
|
133
135
|
const testIdx = stdoutOutput.indexOf("TEST");
|
|
134
|
-
const summaryIdx = stdoutOutput.indexOf("
|
|
136
|
+
const summaryIdx = stdoutOutput.indexOf("요약");
|
|
135
137
|
|
|
136
138
|
expect(tcIdx).toBeLessThan(lintIdx);
|
|
137
139
|
expect(lintIdx).toBeLessThan(testIdx);
|
|
@@ -146,7 +148,7 @@ describe("runCheck", () => {
|
|
|
146
148
|
expect(mocks.execa).toHaveBeenCalled();
|
|
147
149
|
});
|
|
148
150
|
|
|
149
|
-
it("
|
|
151
|
+
it("sets exitCode 1 when typecheck fails", async () => {
|
|
150
152
|
mocks.executeTypecheck.mockResolvedValue({
|
|
151
153
|
success: false, errorCount: 2, warningCount: 0, formattedOutput: "type errors",
|
|
152
154
|
lint: { success: true, errorCount: 0, warningCount: 0, formattedOutput: "" },
|
|
@@ -155,8 +157,6 @@ describe("runCheck", () => {
|
|
|
155
157
|
|
|
156
158
|
await runCheck({ targets: [], types: ["typecheck", "lint", "test"], fix: false });
|
|
157
159
|
|
|
158
|
-
expect(stdoutOutput).toContain("FAILED");
|
|
159
|
-
expect(stdoutOutput).toContain("typecheck");
|
|
160
160
|
expect(process.exitCode).toBe(1);
|
|
161
161
|
});
|
|
162
162
|
|
|
@@ -170,14 +170,13 @@ describe("runCheck", () => {
|
|
|
170
170
|
expect(process.exitCode).toBe(1);
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
it("
|
|
173
|
+
it("sets exitCode 1 when test fails", async () => {
|
|
174
174
|
mocks.execa.mockResolvedValue({
|
|
175
175
|
stdout: "3 tests failed", stderr: "", exitCode: 1,
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
await runCheck({ targets: [], types: ["test"], fix: false });
|
|
179
179
|
|
|
180
|
-
expect(stdoutOutput).toContain("3 failed");
|
|
181
180
|
expect(process.exitCode).toBe(1);
|
|
182
181
|
});
|
|
183
182
|
|
|
@@ -271,8 +270,8 @@ describe("runCheck", () => {
|
|
|
271
270
|
expect(mocks.runLintInWorker).not.toHaveBeenCalled();
|
|
272
271
|
});
|
|
273
272
|
|
|
274
|
-
// Scenario:
|
|
275
|
-
it("
|
|
273
|
+
// Scenario: lint 실패 시 exitCode 설정
|
|
274
|
+
it("sets exitCode 1 when engine lint fails", async () => {
|
|
276
275
|
mocks.executeTypecheck.mockResolvedValue({
|
|
277
276
|
success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
|
|
278
277
|
lint: { success: false, errorCount: 3, warningCount: 1, formattedOutput: "some lint output" },
|
|
@@ -281,10 +280,7 @@ describe("runCheck", () => {
|
|
|
281
280
|
|
|
282
281
|
await runCheck({ targets: [], types: ["typecheck", "lint"], fix: false });
|
|
283
282
|
|
|
284
|
-
expect(
|
|
285
|
-
expect(stdoutOutput).toContain("LINT");
|
|
286
|
-
expect(stdoutOutput).toContain("3 errors");
|
|
287
|
-
expect(stdoutOutput).toContain("1 warnings");
|
|
283
|
+
expect(process.exitCode).toBe(1);
|
|
288
284
|
});
|
|
289
285
|
|
|
290
286
|
// Scenario: check에서 scripts 패키지의 lint가 별도 실행된다
|
|
@@ -391,43 +387,35 @@ describe("runCheck", () => {
|
|
|
391
387
|
});
|
|
392
388
|
|
|
393
389
|
// Scenario: lint 에러 없음
|
|
394
|
-
it("
|
|
390
|
+
it("does not set exitCode when lint passes", async () => {
|
|
395
391
|
mocks.executeLint.mockResolvedValue({
|
|
396
392
|
success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
|
|
397
393
|
});
|
|
398
394
|
|
|
399
395
|
await runCheck({ targets: [], types: ["lint"], fix: false });
|
|
400
396
|
|
|
401
|
-
expect(stdoutOutput).toContain("LINT");
|
|
402
|
-
expect(stdoutOutput).toContain("✔ 0 errors, 0 warnings");
|
|
403
|
-
expect(stdoutOutput).toContain("✔ ALL PASSED");
|
|
404
397
|
expect(process.exitCode).toBeUndefined();
|
|
405
398
|
});
|
|
406
399
|
|
|
407
400
|
// Scenario: lint 에러 발생
|
|
408
|
-
it("
|
|
401
|
+
it("sets exitCode 1 when lint has errors", async () => {
|
|
409
402
|
mocks.executeLint.mockResolvedValue({
|
|
410
403
|
success: false, errorCount: 3, warningCount: 2, formattedOutput: "lint errors",
|
|
411
404
|
});
|
|
412
405
|
|
|
413
406
|
await runCheck({ targets: [], types: ["lint"], fix: false });
|
|
414
407
|
|
|
415
|
-
expect(stdoutOutput).toContain("LINT");
|
|
416
|
-
expect(stdoutOutput).toContain("✖ 3 errors, 2 warnings");
|
|
417
|
-
expect(stdoutOutput).toContain("✖ 1/1 FAILED (lint)");
|
|
418
408
|
expect(process.exitCode).toBe(1);
|
|
419
409
|
});
|
|
420
410
|
|
|
421
411
|
// Scenario: lint 대상 파일 없음
|
|
422
|
-
it("
|
|
412
|
+
it("does not set exitCode when no files to lint", async () => {
|
|
423
413
|
mocks.executeLint.mockResolvedValue({
|
|
424
414
|
success: true, errorCount: 0, warningCount: 0, formattedOutput: "",
|
|
425
415
|
});
|
|
426
416
|
|
|
427
417
|
await runCheck({ targets: [], types: ["lint"], fix: false });
|
|
428
418
|
|
|
429
|
-
expect(stdoutOutput).toContain("✔ 0 errors, 0 warnings");
|
|
430
|
-
expect(stdoutOutput).toContain("✔ ALL PASSED");
|
|
431
419
|
expect(process.exitCode).toBeUndefined();
|
|
432
420
|
});
|
|
433
421
|
|
|
@@ -48,12 +48,12 @@ vi.mock("../../src/utils/replace-deps", () => ({
|
|
|
48
48
|
parseWorkspaceGlobs: mocks.parseWorkspaceGlobs,
|
|
49
49
|
}));
|
|
50
50
|
|
|
51
|
-
vi.mock("execa", () => ({
|
|
52
|
-
execa: mocks.execa,
|
|
53
|
-
}));
|
|
54
|
-
|
|
55
51
|
vi.mock("@simplysm/core-node", () => ({
|
|
56
52
|
fsx: mocks.fsx,
|
|
53
|
+
cpx: {
|
|
54
|
+
exec: mocks.execa,
|
|
55
|
+
execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
|
|
56
|
+
},
|
|
57
57
|
}));
|
|
58
58
|
|
|
59
59
|
vi.mock("@simplysm/core-common", () => {
|
|
@@ -21,13 +21,14 @@ vi.mock("@simplysm/core-node", () => ({
|
|
|
21
21
|
readdir: mockFsxReaddir,
|
|
22
22
|
glob: mockFsxGlob,
|
|
23
23
|
},
|
|
24
|
+
cpx: {
|
|
25
|
+
exec: mockCpxExec,
|
|
26
|
+
execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
|
|
27
|
+
},
|
|
24
28
|
}));
|
|
25
29
|
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
vi.mock("execa", () => ({
|
|
29
|
-
execa: mockExeca,
|
|
30
|
-
}));
|
|
30
|
+
// cpx mock (was execa)
|
|
31
|
+
const mockCpxExec = vi.fn().mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
31
32
|
|
|
32
33
|
// esbuild mock
|
|
33
34
|
const mockEsbuildBuild = vi.fn().mockResolvedValue({});
|
|
@@ -91,13 +92,15 @@ function setupDefaultMocks() {
|
|
|
91
92
|
});
|
|
92
93
|
mockFsxReaddir.mockResolvedValue(["index.html", "assets", "electron"]);
|
|
93
94
|
// Default: glob returns one exe file matching the builder output
|
|
94
|
-
mockFsxGlob.mockImplementation((pattern: string
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
96
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
97
|
+
if (normalized.endsWith(".electron/dist/*.exe")) {
|
|
98
|
+
const dir = normalized.replace("/*.exe", "");
|
|
99
|
+
return Promise.resolve([dir + "/My App Setup 1.0.0.exe"]);
|
|
97
100
|
}
|
|
98
101
|
return Promise.resolve([]);
|
|
99
102
|
});
|
|
100
|
-
|
|
103
|
+
mockCpxExec.mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
101
104
|
mockEsbuildBuild.mockResolvedValue({});
|
|
102
105
|
}
|
|
103
106
|
|
|
@@ -171,7 +174,7 @@ describe("Electron", () => {
|
|
|
171
174
|
|
|
172
175
|
expect(findElectronPackageJson()).toBeDefined();
|
|
173
176
|
|
|
174
|
-
const execaCalls =
|
|
177
|
+
const execaCalls = mockCpxExec.mock.calls;
|
|
175
178
|
expect(
|
|
176
179
|
execaCalls.find((c) => c[0] === "npm" && (c[1] as string[]).includes("install")),
|
|
177
180
|
).toBeDefined();
|
|
@@ -188,7 +191,7 @@ describe("Electron", () => {
|
|
|
188
191
|
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
189
192
|
await electron.initialize();
|
|
190
193
|
|
|
191
|
-
const rebuildCall =
|
|
194
|
+
const rebuildCall = mockCpxExec.mock.calls.find(
|
|
192
195
|
(c) => typeof c[0] === "string" && c[0].includes("electron-rebuild"),
|
|
193
196
|
);
|
|
194
197
|
expect(rebuildCall).toBeUndefined();
|
|
@@ -278,6 +281,25 @@ describe("Electron", () => {
|
|
|
278
281
|
|
|
279
282
|
await expect(electron.build("/fake/out")).rejects.toThrow("electron-main.ts");
|
|
280
283
|
});
|
|
284
|
+
|
|
285
|
+
it("config.env를 esbuild banner로 주입한다 (ELECTRON_DEV_URL 미포함)", async () => {
|
|
286
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
287
|
+
|
|
288
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
289
|
+
appId: "com.test.app",
|
|
290
|
+
env: { API_URL: "https://api.example.com" },
|
|
291
|
+
});
|
|
292
|
+
await electron.build("/fake/out");
|
|
293
|
+
|
|
294
|
+
const callArgs = mockEsbuildBuild.mock.calls[0][0];
|
|
295
|
+
const banner = callArgs.banner?.js as string;
|
|
296
|
+
expect(banner).toContain("process.env");
|
|
297
|
+
expect(banner).toContain("??=");
|
|
298
|
+
expect(banner).toContain("API_URL");
|
|
299
|
+
expect(banner).toContain("https://api.example.com");
|
|
300
|
+
expect(banner).not.toContain("ELECTRON_DEV_URL");
|
|
301
|
+
expect(callArgs.define).toBeUndefined();
|
|
302
|
+
});
|
|
281
303
|
});
|
|
282
304
|
|
|
283
305
|
//#endregion
|
|
@@ -399,7 +421,8 @@ describe("Electron", () => {
|
|
|
399
421
|
|
|
400
422
|
it("빌드 산출물이 없으면 경고를 출력한다", async () => {
|
|
401
423
|
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
402
|
-
|
|
424
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
425
|
+
if (normalized.endsWith(".electron/dist/*.exe")) return Promise.resolve([]);
|
|
403
426
|
return Promise.resolve([]);
|
|
404
427
|
});
|
|
405
428
|
|
|
@@ -417,15 +440,15 @@ describe("Electron", () => {
|
|
|
417
440
|
//#region Rule: 개발 모드에서 Electron 앱을 실행한다
|
|
418
441
|
|
|
419
442
|
describe("인수 테스트: run()", () => {
|
|
420
|
-
// Helper: creates a deferred
|
|
421
|
-
function
|
|
443
|
+
// Helper: creates a deferred cpx mock where Electron process can be controlled
|
|
444
|
+
function setupCpxForRun(): {
|
|
422
445
|
electronKill: ReturnType<typeof vi.fn>;
|
|
423
446
|
resolveElectron: () => void;
|
|
424
447
|
} {
|
|
425
448
|
const electronKill = vi.fn();
|
|
426
449
|
let resolveElectron: () => void = () => {};
|
|
427
450
|
|
|
428
|
-
|
|
451
|
+
mockCpxExec.mockImplementation((cmd: string) => {
|
|
429
452
|
if (typeof cmd === "string" && cmd.includes("electron")) {
|
|
430
453
|
// Electron process: create a deferred promise we can resolve externally
|
|
431
454
|
const p = new Promise<void>((resolve) => {
|
|
@@ -434,14 +457,14 @@ describe("Electron", () => {
|
|
|
434
457
|
p.kill = electronKill;
|
|
435
458
|
return p;
|
|
436
459
|
}
|
|
437
|
-
return Promise.resolve({ stdout: "" });
|
|
460
|
+
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
438
461
|
});
|
|
439
462
|
|
|
440
463
|
return { electronKill, resolveElectron: () => resolveElectron() };
|
|
441
464
|
}
|
|
442
465
|
|
|
443
|
-
it("creates esbuild context and spawns Electron
|
|
444
|
-
const { resolveElectron } =
|
|
466
|
+
it("creates esbuild context with banner for env and spawns Electron", async () => {
|
|
467
|
+
const { resolveElectron } = setupCpxForRun();
|
|
445
468
|
|
|
446
469
|
const { Electron } = await import("../../src/electron/electron.js");
|
|
447
470
|
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
@@ -455,28 +478,20 @@ describe("Electron", () => {
|
|
|
455
478
|
resolveElectron();
|
|
456
479
|
await runPromise;
|
|
457
480
|
|
|
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
|
-
);
|
|
481
|
+
// esbuild.context was called with banner containing ELECTRON_DEV_URL
|
|
482
|
+
const callArgs = mockEsbuildContext.mock.calls[0][0];
|
|
483
|
+
const banner = callArgs.banner?.js as string;
|
|
484
|
+
expect(banner).toContain("process.env");
|
|
485
|
+
expect(banner).toContain("??=");
|
|
486
|
+
expect(banner).toContain("ELECTRON_DEV_URL");
|
|
487
|
+
expect(banner).toContain("http://localhost:4200");
|
|
488
|
+
expect(callArgs.define).toBeUndefined();
|
|
489
|
+
|
|
490
|
+
expect(callArgs.platform).toBe("node");
|
|
491
|
+
expect(callArgs.target).toBe("node20");
|
|
492
|
+
expect(callArgs.format).toBe("cjs");
|
|
493
|
+
expect(callArgs.bundle).toBe(true);
|
|
494
|
+
expect(callArgs.external).toContain("electron");
|
|
480
495
|
}, 10_000);
|
|
481
496
|
|
|
482
497
|
it("throws when electron-main.ts entry point is missing", async () => {
|
|
@@ -492,7 +507,7 @@ describe("Electron", () => {
|
|
|
492
507
|
});
|
|
493
508
|
|
|
494
509
|
it("resolves on SIGINT signal", async () => {
|
|
495
|
-
const { electronKill } =
|
|
510
|
+
const { electronKill } = setupCpxForRun();
|
|
496
511
|
|
|
497
512
|
const { Electron } = await import("../../src/electron/electron.js");
|
|
498
513
|
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
@@ -513,9 +528,9 @@ describe("Electron", () => {
|
|
|
513
528
|
});
|
|
514
529
|
|
|
515
530
|
describe("단위: run() 플러그인 동작", () => {
|
|
516
|
-
it("passes custom env and ELECTRON_DEV_URL
|
|
531
|
+
it("passes custom env and ELECTRON_DEV_URL via esbuild banner", async () => {
|
|
517
532
|
let resolveElectron: () => void = () => {};
|
|
518
|
-
|
|
533
|
+
mockCpxExec.mockImplementation((cmd: string) => {
|
|
519
534
|
if (typeof cmd === "string" && cmd.includes("electron")) {
|
|
520
535
|
const p = new Promise<void>((resolve) => {
|
|
521
536
|
resolveElectron = resolve;
|
|
@@ -523,7 +538,7 @@ describe("Electron", () => {
|
|
|
523
538
|
p.kill = vi.fn();
|
|
524
539
|
return p;
|
|
525
540
|
}
|
|
526
|
-
return Promise.resolve({ stdout: "" });
|
|
541
|
+
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
527
542
|
});
|
|
528
543
|
|
|
529
544
|
const { Electron } = await import("../../src/electron/electron.js");
|
|
@@ -537,22 +552,18 @@ describe("Electron", () => {
|
|
|
537
552
|
resolveElectron();
|
|
538
553
|
await runPromise;
|
|
539
554
|
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
);
|
|
543
|
-
expect(
|
|
544
|
-
expect(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
ELECTRON_DEV_URL: "http://localhost:5555",
|
|
548
|
-
CUSTOM_VAR: "test-value",
|
|
549
|
-
}),
|
|
550
|
-
);
|
|
555
|
+
const callArgs = mockEsbuildContext.mock.calls[0][0];
|
|
556
|
+
const banner = callArgs.banner?.js as string;
|
|
557
|
+
expect(banner).toContain("ELECTRON_DEV_URL");
|
|
558
|
+
expect(banner).toContain("http://localhost:5555");
|
|
559
|
+
expect(banner).toContain("CUSTOM_VAR");
|
|
560
|
+
expect(banner).toContain("test-value");
|
|
561
|
+
expect(callArgs.define).toBeUndefined();
|
|
551
562
|
}, 10_000);
|
|
552
563
|
|
|
553
564
|
it("calls initialize() before starting esbuild context", async () => {
|
|
554
565
|
let resolveElectron: () => void = () => {};
|
|
555
|
-
|
|
566
|
+
mockCpxExec.mockImplementation((cmd: string) => {
|
|
556
567
|
if (typeof cmd === "string" && cmd.includes("electron")) {
|
|
557
568
|
const p = new Promise<void>((resolve) => {
|
|
558
569
|
resolveElectron = resolve;
|
|
@@ -560,7 +571,7 @@ describe("Electron", () => {
|
|
|
560
571
|
p.kill = vi.fn();
|
|
561
572
|
return p;
|
|
562
573
|
}
|
|
563
|
-
return Promise.resolve({ stdout: "" });
|
|
574
|
+
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
564
575
|
});
|
|
565
576
|
|
|
566
577
|
const { Electron } = await import("../../src/electron/electron.js");
|
|
@@ -572,7 +583,7 @@ describe("Electron", () => {
|
|
|
572
583
|
await runPromise;
|
|
573
584
|
|
|
574
585
|
// initialize calls npm install -> execa should have been called with npm install
|
|
575
|
-
const npmInstallCall =
|
|
586
|
+
const npmInstallCall = mockCpxExec.mock.calls.find(
|
|
576
587
|
(c: any[]) => c[0] === "npm" && (c[1] as string[]).includes("install"),
|
|
577
588
|
);
|
|
578
589
|
expect(npmInstallCall).toBeDefined();
|
|
@@ -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());
|
|
@@ -35,6 +35,8 @@ const mockRunTscPackageBuild = vi.fn(() => ({
|
|
|
35
35
|
warningCount: 0,
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
|
+
const mockCpxExecSync = vi.fn().mockReturnValue({ stdout: "v20.11.0", stderr: "", exitCode: 0 });
|
|
39
|
+
|
|
38
40
|
vi.mock("@simplysm/core-node", () => ({
|
|
39
41
|
createWorker: vi.fn((fns: Record<string, Function>) => {
|
|
40
42
|
workerFns = fns as any;
|
|
@@ -50,6 +52,10 @@ vi.mock("@simplysm/core-node", () => ({
|
|
|
50
52
|
pathx: {
|
|
51
53
|
norm: vi.fn((...args: string[]) => path.resolve(...args).replace(/\\/g, "/")),
|
|
52
54
|
},
|
|
55
|
+
cpx: {
|
|
56
|
+
exec: vi.fn().mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 }),
|
|
57
|
+
execSync: mockCpxExecSync,
|
|
58
|
+
},
|
|
53
59
|
}));
|
|
54
60
|
|
|
55
61
|
vi.mock("@simplysm/core-common", () => ({
|
|
@@ -95,9 +101,6 @@ vi.mock("fs", () => ({
|
|
|
95
101
|
existsSync: (...args: unknown[]) => mockExistsSync(...(args as [string])),
|
|
96
102
|
}));
|
|
97
103
|
|
|
98
|
-
vi.mock("execa", () => ({
|
|
99
|
-
execaSync: vi.fn(() => ({ stdout: "v20.11.0" })),
|
|
100
|
-
}));
|
|
101
104
|
|
|
102
105
|
// Mock lockfile content for resolveLockedVersion
|
|
103
106
|
let mockLockfileContent = "";
|