@simplysm/sd-cli 14.1.9 → 14.1.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/capacitor/capacitor-icon.js +2 -2
- package/dist/capacitor/capacitor-icon.js.map +1 -1
- package/dist/capacitor/capacitor-npm-config.d.ts +1 -1
- package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
- package/dist/capacitor/capacitor-npm-config.js +11 -18
- package/dist/capacitor/capacitor-npm-config.js.map +1 -1
- package/dist/capacitor/capacitor.d.ts +1 -1
- package/dist/capacitor/capacitor.js +2 -2
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +1 -0
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/device.js +2 -2
- package/dist/commands/device.js.map +1 -1
- package/dist/commands/init/generators/root.d.ts.map +1 -1
- package/dist/commands/init/generators/root.js +0 -1
- package/dist/commands/init/generators/root.js.map +1 -1
- package/dist/commands/init/init-client.js +1 -1
- package/dist/commands/init/init-client.js.map +1 -1
- package/dist/commands/init/init.js +2 -2
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/publish/deployment-phase.d.ts.map +1 -1
- package/dist/commands/publish/deployment-phase.js +1 -0
- package/dist/commands/publish/deployment-phase.js.map +1 -1
- package/dist/commands/publish/npm-publisher.js +3 -3
- package/dist/commands/publish/npm-publisher.js.map +1 -1
- package/dist/commands/publish/post-publish-phase.d.ts.map +1 -1
- package/dist/commands/publish/post-publish-phase.js +1 -0
- package/dist/commands/publish/post-publish-phase.js.map +1 -1
- package/dist/commands/publish/publish-command.d.ts.map +1 -1
- package/dist/commands/publish/publish-command.js +7 -12
- package/dist/commands/publish/publish-command.js.map +1 -1
- package/dist/deps/replace-deps/collect-deps.js +4 -4
- package/dist/deps/replace-deps/collect-deps.js.map +1 -1
- package/dist/deps/replace-deps/replace-deps-resolve.d.ts +4 -12
- package/dist/deps/replace-deps/replace-deps-resolve.d.ts.map +1 -1
- package/dist/deps/replace-deps/replace-deps-resolve.js +13 -49
- package/dist/deps/replace-deps/replace-deps-resolve.js.map +1 -1
- package/dist/deps/replace-deps/replace-deps.d.ts +2 -2
- package/dist/deps/replace-deps/replace-deps.js +3 -3
- package/dist/deps/server-externals/server-production-files.d.ts +3 -3
- package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
- package/dist/deps/server-externals/server-production-files.js +24 -17
- package/dist/deps/server-externals/server-production-files.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +6 -11
- package/dist/electron/electron.js.map +1 -1
- package/dist/engines/BaseEngine.d.ts +1 -0
- package/dist/engines/BaseEngine.d.ts.map +1 -1
- package/dist/engines/BaseEngine.js +2 -1
- package/dist/engines/BaseEngine.js.map +1 -1
- package/dist/esbuild/esbuild-config.d.ts +6 -2
- package/dist/esbuild/esbuild-config.d.ts.map +1 -1
- package/dist/esbuild/esbuild-config.js +4 -3
- package/dist/esbuild/esbuild-config.js.map +1 -1
- package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-tsc-plugin.js +3 -1
- package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -1
- package/dist/orchestrators/BaseOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/BaseOrchestrator.js +1 -0
- package/dist/orchestrators/BaseOrchestrator.js.map +1 -1
- package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/BuildOrchestrator.js +3 -5
- package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
- package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevOrchestrator.js +1 -0
- package/dist/orchestrators/DevOrchestrator.js.map +1 -1
- package/dist/orchestrators/ServerRuntimeManager.d.ts.map +1 -1
- package/dist/orchestrators/ServerRuntimeManager.js +4 -0
- package/dist/orchestrators/ServerRuntimeManager.js.map +1 -1
- package/dist/orchestrators/TypecheckOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/TypecheckOrchestrator.js +4 -6
- package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -1
- package/dist/runtime/engine-watch-events.d.ts.map +1 -1
- package/dist/runtime/engine-watch-events.js +5 -0
- package/dist/runtime/engine-watch-events.js.map +1 -1
- package/dist/runtime/worker-events.d.ts +1 -0
- package/dist/runtime/worker-events.d.ts.map +1 -1
- package/dist/sd-cli-entry.d.ts.map +1 -1
- package/dist/sd-cli-entry.js +0 -4
- package/dist/sd-cli-entry.js.map +1 -1
- package/dist/sd-cli.js +2 -0
- package/dist/sd-cli.js.map +1 -1
- package/dist/typecheck/typecheck-non-package.d.ts.map +1 -1
- package/dist/typecheck/typecheck-non-package.js +10 -0
- package/dist/typecheck/typecheck-non-package.js.map +1 -1
- package/dist/utils/package-utils.d.ts +8 -6
- package/dist/utils/package-utils.d.ts.map +1 -1
- package/dist/utils/package-utils.js +26 -24
- package/dist/utils/package-utils.js.map +1 -1
- package/dist/utils/workspace-utils.d.ts +17 -0
- package/dist/utils/workspace-utils.d.ts.map +1 -0
- package/dist/utils/workspace-utils.js +95 -0
- package/dist/utils/workspace-utils.js.map +1 -0
- package/dist/workers/client.worker.d.ts +1 -0
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +8 -3
- package/dist/workers/client.worker.js.map +1 -1
- package/dist/workers/library-build.worker.d.ts +1 -0
- package/dist/workers/library-build.worker.d.ts.map +1 -1
- package/dist/workers/library-build.worker.js +3 -2
- package/dist/workers/library-build.worker.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts +1 -0
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +12 -12
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-esbuild-context.d.ts.map +1 -1
- package/dist/workers/server-esbuild-context.js +4 -2
- package/dist/workers/server-esbuild-context.js.map +1 -1
- package/dist/workers/server-runtime.worker.d.ts +1 -0
- package/dist/workers/server-runtime.worker.d.ts.map +1 -1
- package/dist/workers/server-runtime.worker.js +9 -3
- package/dist/workers/server-runtime.worker.js.map +1 -1
- package/dist/workers/server-watch-manager.d.ts +1 -1
- package/dist/workers/server-watch-manager.d.ts.map +1 -1
- package/dist/workers/server-watch-manager.js +2 -2
- package/dist/workers/server-watch-manager.js.map +1 -1
- package/package.json +11 -11
- package/src/capacitor/capacitor-icon.ts +2 -2
- package/src/capacitor/capacitor-npm-config.ts +10 -19
- package/src/capacitor/capacitor.ts +2 -2
- package/src/commands/check.ts +1 -0
- package/src/commands/device.ts +2 -2
- package/src/commands/init/generators/root.ts +0 -1
- package/src/commands/init/init-client.ts +1 -1
- package/src/commands/init/init.ts +2 -2
- package/src/commands/init/templates/workspace-root/mise.toml.hbs +1 -1
- package/src/commands/publish/deployment-phase.ts +1 -0
- package/src/commands/publish/npm-publisher.ts +3 -3
- package/src/commands/publish/post-publish-phase.ts +1 -0
- package/src/commands/publish/publish-command.ts +7 -15
- package/src/deps/replace-deps/collect-deps.ts +4 -4
- package/src/deps/replace-deps/replace-deps-resolve.ts +13 -56
- package/src/deps/replace-deps/replace-deps.ts +3 -3
- package/src/deps/server-externals/server-production-files.ts +31 -18
- package/src/electron/electron.ts +7 -13
- package/src/engines/BaseEngine.ts +3 -2
- package/src/esbuild/esbuild-config.ts +12 -3
- package/src/esbuild/esbuild-tsc-plugin.ts +4 -1
- package/src/orchestrators/BaseOrchestrator.ts +1 -0
- package/src/orchestrators/BuildOrchestrator.ts +3 -5
- package/src/orchestrators/DevOrchestrator.ts +1 -0
- package/src/orchestrators/ServerRuntimeManager.ts +4 -0
- package/src/orchestrators/TypecheckOrchestrator.ts +4 -6
- package/src/runtime/engine-watch-events.ts +7 -1
- package/src/runtime/worker-events.ts +1 -0
- package/src/sd-cli-entry.ts +0 -9
- package/src/sd-cli.ts +2 -0
- package/src/typecheck/typecheck-non-package.ts +11 -0
- package/src/utils/package-utils.ts +30 -23
- package/src/utils/workspace-utils.ts +117 -0
- package/src/workers/client.worker.ts +9 -4
- package/src/workers/library-build.worker.ts +4 -3
- package/src/workers/server-build.worker.ts +13 -13
- package/src/workers/server-esbuild-context.ts +5 -2
- package/src/workers/server-runtime.worker.ts +10 -3
- package/src/workers/server-watch-manager.ts +3 -3
- package/tests/capacitor/capacitor-build.spec.ts +142 -142
- package/tests/capacitor/capacitor-init.spec.ts +181 -181
- package/tests/capacitor/capacitor-npm-config.acc.spec.ts +114 -114
- package/tests/capacitor/capacitor-npm-config.spec.ts +94 -94
- package/tests/commands/publish-manifest.acc.spec.ts +67 -0
- package/tests/deps/replace-deps/collect-deps.acc.spec.ts +16 -1
- package/tests/deps/replace-deps/replace-deps-resolve.acc.spec.ts +9 -5
- package/tests/deps/replace-deps/replace-deps-setup.acc.spec.ts +3 -3
- package/tests/deps/server-externals/server-production-files.spec.ts +68 -0
- package/tests/electron/electron.spec.ts +608 -608
- package/tests/ts-compiler/fixtures/non-angular-pkg/.cache/typecheck-browser.tsbuildinfo +1 -1
- package/tests/ts-compiler/fixtures/non-angular-pkg/.cache/typecheck-node.tsbuildinfo +1 -1
- package/tests/ts-compiler/fixtures/non-angular-pkg/.cache/typecheck.tsbuildinfo +1 -1
- package/tests/utils/engine-watch-events.spec.ts +17 -0
- package/tests/utils/esbuild-config.spec.ts +15 -0
- package/tests/utils/package-utils.spec.ts +36 -4
- package/tests/utils/replace-deps-watch.acc.spec.ts +4 -4
- package/tests/utils/replace-deps-watch.spec.ts +3 -3
- package/tests/utils/replace-deps.spec.ts +1 -35
- package/tests/utils/workspace-utils.spec.ts +87 -0
- package/tests/workers/library-build-worker.spec.ts +1 -1
- package/tests/workers/server-esbuild-context.spec.ts +4 -3
- package/dist/commands/reinstall.d.ts +0 -13
- package/dist/commands/reinstall.d.ts.map +0 -1
- package/dist/commands/reinstall.js +0 -56
- package/dist/commands/reinstall.js.map +0 -1
- package/src/commands/init/templates/workspace-root/pnpm-workspace.yaml +0 -5
- package/src/commands/reinstall.ts +0 -63
- package/tests/angular/angular-compiler-hmr-removal.verify.md +0 -16
- package/tests/angular/onbuild-lint-removal.verify.md +0 -8
- package/tests/angular/vite-angular-plugin-sdtscompiler.verify.md +0 -13
- package/tests/angular/vite-angular-plugin-vitest.verify.md +0 -20
- package/tests/capacitor/capacitor-android-exports.verify.md +0 -11
- package/tests/commands/publish-npm-local-split.verify.md +0 -9
- package/tests/commands/publish-responsibility-split.verify.md +0 -13
- package/tests/commands/publish-set.verify.md +0 -7
- package/tests/commands/publish-storage-split.verify.md +0 -8
- package/tests/commands/slice3-severity-cleanup.verify.md +0 -12
- package/tests/deps/deps-directory-separation.verify.md +0 -15
- package/tests/deps/replace-deps/replace-deps-perf.verify.md +0 -15
- package/tests/deps/server-externals/mise-toml-parse-intent.verify.md +0 -16
- package/tests/electron/electron-symlink-cleanup.verify.md +0 -8
- package/tests/engines/engine-duplicate-output-removal.verify.md +0 -10
- package/tests/engines/engine-typecheck-selection.verify.md +0 -8
- package/tests/engines/esbuild-client-engine.verify.md +0 -15
- package/tests/engines/normalize-result.verify.md +0 -9
- package/tests/engines/vite-dependency-cleanup.verify.md +0 -24
- package/tests/esbuild/esbuild-angular-compiler-plugin-hmr.verify.md +0 -23
- package/tests/esbuild/esbuild-angular-compiler-plugin-onload.verify.md +0 -21
- package/tests/esbuild/esbuild-angular-compiler-plugin-onstart-extraction.verify.md +0 -16
- package/tests/esbuild/esbuild-angular-compiler-plugin-sdtscompiler.verify.md +0 -15
- package/tests/esbuild/esbuild-angular-compiler-plugin-stylesheet.verify.md +0 -31
- package/tests/esbuild/esbuild-angular-compiler-plugin-worker.verify.md +0 -59
- package/tests/esbuild/esbuild-angular-compiler-plugin.verify.md +0 -21
- package/tests/esbuild/esbuild-postcss-plugin-chunking.verify.md +0 -17
- package/tests/esbuild/esbuild-tsc-plugin-imports.verify.md +0 -13
- package/tests/esbuild/esbuild-worker-plugin-node.verify.md +0 -12
- package/tests/esbuild/esbuild-worker-plugin.verify.md +0 -7
- package/tests/orchestrators/dist-delete-watcher.verify.md +0 -10
- package/tests/orchestrators/orchestrator-baseenv.verify.md +0 -10
- package/tests/orchestrators/orchestrator-diagnostic-formatting.verify.md +0 -10
- package/tests/orchestrators/orchestrator-initializemode-signature.verify.md +0 -9
- package/tests/orchestrators/slice1-stdout-to-consola.verify.md +0 -10
- package/tests/sd-cli-catch-all.verify.md +0 -7
- package/tests/sd-cli-log-tag.verify.md +0 -11
- package/tests/ts-compiler/SdTsCompiler-affected-files.verify.md +0 -8
- package/tests/ts-compiler/SdTsCompiler-crash-handling.verify.md +0 -24
- package/tests/ts-compiler/SdTsCompiler-diagnostics.verify.md +0 -12
- package/tests/ts-compiler/SdTsCompiler-emit.verify.md +0 -9
- package/tests/ts-compiler/SdTsCompiler.verify.md +0 -41
- package/tests/ts-compiler/scss-lint-integration.verify.md +0 -14
- package/tests/utils/copy-public-outdir.verify.md +0 -8
- package/tests/utils/dev-http-server.verify.md +0 -8
- package/tests/utils/engine-watch-events.verify.md +0 -17
- package/tests/utils/esbuild-client-config-integration.verify.md +0 -9
- package/tests/utils/esbuild-client-config-postcss.verify.md +0 -6
- package/tests/utils/esbuild-client-config.verify.md +0 -26
- package/tests/utils/esbuild-index-html.verify.md +0 -10
- package/tests/utils/esbuild-pwa.verify.md +0 -9
- package/tests/utils/esbuild-scss-plugin.verify.md +0 -8
- package/tests/utils/hmr-service.verify.md +0 -17
- package/tests/utils/lint-core-import-paths.verify.md +0 -10
- package/tests/utils/replace-deps-split.verify.md +0 -15
- package/tests/utils/replace-deps-watch.verify.md +0 -9
- package/tests/utils/server-production-files-import-paths.verify.md +0 -14
- package/tests/utils/vite-config-cleanup.verify.md +0 -7
- package/tests/workers/build-watch-paths-library.verify.md +0 -10
- package/tests/workers/build-watch-paths-ngtsc-server.verify.md +0 -12
- package/tests/workers/client-worker-browser-support.verify.md +0 -7
- package/tests/workers/client-worker-cleanup.verify.md +0 -8
- package/tests/workers/client-worker-initial-build-error.verify.md +0 -7
- package/tests/workers/client-worker-initial-build-warnings.verify.md +0 -7
- package/tests/workers/client-worker-mtime-incremental.verify.md +0 -10
- package/tests/workers/client-worker-onend-sync.verify.md +0 -7
- package/tests/workers/client-worker-refactor.verify.md +0 -22
- package/tests/workers/client-worker-ts-cache-invalidation.verify.md +0 -12
- package/tests/workers/dev-port-file.verify.md +0 -6
- package/tests/workers/ngtsc-build-rootnames-refresh.verify.md +0 -8
- package/tests/workers/server-build-context-dispose.verify.md +0 -8
- package/tests/workers/server-build-worker-plugin.verify.md +0 -9
- package/tests/workers/server-build-worker-refactoring.verify.md +0 -14
- package/tests/workers/server-esbuild-context-integration.verify.md +0 -10
- package/tests/workers/server-esbuild-context-tsc.verify.md +0 -7
|
@@ -1,608 +1,608 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import * as coreCommon from "@simplysm/core-common";
|
|
3
|
-
import { fsx, cpx } from "@simplysm/core-node";
|
|
4
|
-
|
|
5
|
-
// esbuild는 외부 npm으로 ESM namespace immutable이라 vi.mock 유지
|
|
6
|
-
const mockEsbuildBuild = vi.fn().mockResolvedValue({});
|
|
7
|
-
let mockEsbuildOnEndCallback: ((result: { errors: unknown[] }) => void | Promise<void>) | null =
|
|
8
|
-
null;
|
|
9
|
-
const mockEsbuildContext = vi.fn().mockImplementation((options: any) => {
|
|
10
|
-
const plugin = options?.plugins?.find((p: any) => p.name === "electron-restart");
|
|
11
|
-
if (plugin != null) {
|
|
12
|
-
plugin.setup({
|
|
13
|
-
onEnd: (cb: (result: { errors: unknown[] }) => void | Promise<void>) => {
|
|
14
|
-
mockEsbuildOnEndCallback = cb;
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
watch: vi.fn().mockImplementation(async () => {
|
|
20
|
-
if (mockEsbuildOnEndCallback != null) {
|
|
21
|
-
await mockEsbuildOnEndCallback({ errors: [] });
|
|
22
|
-
}
|
|
23
|
-
}),
|
|
24
|
-
dispose: vi.fn(),
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
vi.mock("esbuild", () => ({
|
|
28
|
-
build: mockEsbuildBuild,
|
|
29
|
-
context: mockEsbuildContext,
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
// @simplysm/core-node fsx, cpx는 spy로 전환
|
|
33
|
-
const mockFsxExists = vi.spyOn(fsx, "exists");
|
|
34
|
-
const mockFsxReadJson = vi.spyOn(fsx, "readJson");
|
|
35
|
-
const mockFsxWriteJson = vi.spyOn(fsx, "writeJson").mockResolvedValue(undefined);
|
|
36
|
-
vi.spyOn(fsx, "write").mockResolvedValue(undefined);
|
|
37
|
-
vi.spyOn(fsx, "mkdir").mockResolvedValue(undefined);
|
|
38
|
-
const mockFsxCopy = vi.spyOn(fsx, "copy").mockResolvedValue(undefined);
|
|
39
|
-
const mockFsxReaddir = vi.spyOn(fsx, "readdir");
|
|
40
|
-
const mockFsxGlob = vi.spyOn(fsx, "glob");
|
|
41
|
-
|
|
42
|
-
const mockCpxSpawn = vi.spyOn(cpx, "spawn").mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
43
|
-
vi.spyOn(cpx, "spawnSync").mockReturnValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
44
|
-
|
|
45
|
-
// createLogger spy
|
|
46
|
-
const mockLoggerDebug = vi.fn();
|
|
47
|
-
const mockLoggerWarn = vi.fn();
|
|
48
|
-
const mockLoggerInfo = vi.fn();
|
|
49
|
-
const mockLoggerStart = vi.fn();
|
|
50
|
-
const mockLoggerSuccess = vi.fn();
|
|
51
|
-
const mockLoggerError = vi.fn();
|
|
52
|
-
vi.spyOn(coreCommon, "createLogger").mockReturnValue({ debug: mockLoggerDebug, warn: mockLoggerWarn, info: mockLoggerInfo, start: mockLoggerStart, success: mockLoggerSuccess, error: mockLoggerError } as any);
|
|
53
|
-
|
|
54
|
-
//#endregion
|
|
55
|
-
|
|
56
|
-
//#region Helpers
|
|
57
|
-
|
|
58
|
-
const PKG_PATH = "/fake/pkg";
|
|
59
|
-
|
|
60
|
-
function setupDefaultMocks() {
|
|
61
|
-
mockFsxExists.mockResolvedValue(true);
|
|
62
|
-
mockFsxReadJson.mockResolvedValue({
|
|
63
|
-
name: "@myorg/my-app",
|
|
64
|
-
version: "1.0.0",
|
|
65
|
-
description: "My App",
|
|
66
|
-
dependencies: {
|
|
67
|
-
"better-sqlite3": "^11.0.0",
|
|
68
|
-
"sharp": "^0.34.0",
|
|
69
|
-
},
|
|
70
|
-
devDependencies: {
|
|
71
|
-
"electron": "^35.0.0",
|
|
72
|
-
"@electron/rebuild": "^4.0.0",
|
|
73
|
-
"electron-builder": "^26.0.0",
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
mockFsxReaddir.mockResolvedValue(["index.html", "assets", "electron"]);
|
|
77
|
-
// Default: glob returns one exe file matching the builder output
|
|
78
|
-
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
79
|
-
const normalized = pattern.replace(/\\/g, "/");
|
|
80
|
-
if (normalized.endsWith(".electron/dist/*.exe")) {
|
|
81
|
-
const dir = normalized.replace("/*.exe", "");
|
|
82
|
-
return Promise.resolve([dir + "/My App Setup 1.0.0.exe"]);
|
|
83
|
-
}
|
|
84
|
-
return Promise.resolve([]);
|
|
85
|
-
});
|
|
86
|
-
mockCpxSpawn.mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
87
|
-
mockEsbuildBuild.mockResolvedValue({});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function findWriteJson(pathFragment: string): Record<string, unknown> | undefined {
|
|
91
|
-
const call = mockFsxWriteJson.mock.calls.find(
|
|
92
|
-
(c) =>
|
|
93
|
-
typeof c[0] === "string" &&
|
|
94
|
-
c[0].replace(/\\/g, "/").includes(pathFragment),
|
|
95
|
-
);
|
|
96
|
-
return call ? (call[1] as Record<string, unknown>) : undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function findElectronPackageJson(): Record<string, unknown> | undefined {
|
|
100
|
-
return findWriteJson(".electron/src/package.json");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function findBuilderConfig(): Record<string, unknown> | undefined {
|
|
104
|
-
return findWriteJson("builder-config.json");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function normalizedCopyCalls(): string[][] {
|
|
108
|
-
return mockFsxCopy.mock.calls.map((c) => [
|
|
109
|
-
c[0].replace(/\\/g, "/"),
|
|
110
|
-
c[1].replace(/\\/g, "/"),
|
|
111
|
-
]);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
//#endregion
|
|
115
|
-
|
|
116
|
-
describe("Electron", () => {
|
|
117
|
-
beforeEach(() => {
|
|
118
|
-
vi.clearAllMocks();
|
|
119
|
-
mockEsbuildOnEndCallback = null;
|
|
120
|
-
setupDefaultMocks();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
//#region Rule: 설정을 검증한다
|
|
124
|
-
|
|
125
|
-
describe("Acceptance: appId가 없으면 에러가 발생한다", () => {
|
|
126
|
-
it("appId가 빈 문자열이면 에러를 던진다", async () => {
|
|
127
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
128
|
-
|
|
129
|
-
await expect(
|
|
130
|
-
Electron.create(PKG_PATH, { appId: "" }),
|
|
131
|
-
).rejects.toThrow("appId");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("appId가 공백만 있으면 에러를 던진다", async () => {
|
|
135
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
136
|
-
|
|
137
|
-
await expect(
|
|
138
|
-
Electron.create(PKG_PATH, { appId: " " }),
|
|
139
|
-
).rejects.toThrow("appId");
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
//#endregion
|
|
144
|
-
|
|
145
|
-
//#region Rule: Electron 프로젝트를 초기화한다
|
|
146
|
-
|
|
147
|
-
describe("인수 테스트: 초기화", () => {
|
|
148
|
-
it("package.json 생성 +
|
|
149
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
150
|
-
|
|
151
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
152
|
-
appId: "com.test.app",
|
|
153
|
-
reinstallDependencies: ["better-sqlite3"],
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
await electron.initialize();
|
|
157
|
-
|
|
158
|
-
expect(findElectronPackageJson()).toBeDefined();
|
|
159
|
-
|
|
160
|
-
const spawnCalls = mockCpxSpawn.mock.calls;
|
|
161
|
-
const installCall = spawnCalls.find(
|
|
162
|
-
(c) => c[0] === "
|
|
163
|
-
);
|
|
164
|
-
expect(installCall).toBeDefined();
|
|
165
|
-
expect(installCall?.[2]).toEqual(expect.objectContaining({ shell: true }));
|
|
166
|
-
expect(
|
|
167
|
-
spawnCalls.find(
|
|
168
|
-
(c) => c[0] === "
|
|
169
|
-
),
|
|
170
|
-
).toBeDefined();
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("reinstallDependencies가 비어있으면 electron-rebuild를 건너뛴다", async () => {
|
|
174
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
175
|
-
|
|
176
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
177
|
-
await electron.initialize();
|
|
178
|
-
|
|
179
|
-
const rebuildCall = mockCpxSpawn.mock.calls.find(
|
|
180
|
-
(c) => c[0] === "
|
|
181
|
-
);
|
|
182
|
-
expect(rebuildCall).toBeUndefined();
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe("단위: _setupPackageJson", () => {
|
|
187
|
-
it("스코프 패키지명을 정규화한다", async () => {
|
|
188
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
189
|
-
|
|
190
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
191
|
-
await electron.initialize();
|
|
192
|
-
|
|
193
|
-
expect(findElectronPackageJson()!["name"]).toBe("myorg-my-app");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("reinstallDependencies 패키지를 dependencies에 포함한다", async () => {
|
|
197
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
198
|
-
|
|
199
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
200
|
-
appId: "com.test.app",
|
|
201
|
-
reinstallDependencies: ["better-sqlite3"],
|
|
202
|
-
});
|
|
203
|
-
await electron.initialize();
|
|
204
|
-
|
|
205
|
-
const deps = findElectronPackageJson()!["dependencies"] as Record<string, string>;
|
|
206
|
-
expect(deps["better-sqlite3"]).toBe("^11.0.0");
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("exclude 패키지를 dependencies에 포함한다", async () => {
|
|
210
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
211
|
-
|
|
212
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" }, ["sharp"]);
|
|
213
|
-
await electron.initialize();
|
|
214
|
-
|
|
215
|
-
const deps = findElectronPackageJson()!["dependencies"] as Record<string, string>;
|
|
216
|
-
expect(deps["sharp"]).toBe("^0.34.0");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("package.json에 type: module이 설정된다", async () => {
|
|
220
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
221
|
-
|
|
222
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
223
|
-
await electron.initialize();
|
|
224
|
-
|
|
225
|
-
expect(findElectronPackageJson()!["type"]).toBe("module");
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("postInstallScript가 설정되면 scripts.postinstall에 포함한다", async () => {
|
|
229
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
230
|
-
|
|
231
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
232
|
-
appId: "com.test.app",
|
|
233
|
-
postInstallScript: "node rebuild.js",
|
|
234
|
-
});
|
|
235
|
-
await electron.initialize();
|
|
236
|
-
|
|
237
|
-
const scripts = findElectronPackageJson()!["scripts"] as Record<string, string>;
|
|
238
|
-
expect(scripts["postinstall"]).toBe("node rebuild.js");
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
//#endregion
|
|
243
|
-
|
|
244
|
-
//#region Rule: 메인 프로세스를 번들링한다
|
|
245
|
-
|
|
246
|
-
describe("Acceptance: build — esbuild 번들링", () => {
|
|
247
|
-
it("src/electron-main.ts를 esbuild로 번들링한다", async () => {
|
|
248
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
249
|
-
|
|
250
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
251
|
-
appId: "com.test.app",
|
|
252
|
-
reinstallDependencies: ["better-sqlite3"],
|
|
253
|
-
});
|
|
254
|
-
await electron.build("/fake/out");
|
|
255
|
-
|
|
256
|
-
expect(mockEsbuildBuild).toHaveBeenCalledWith(
|
|
257
|
-
expect.objectContaining({
|
|
258
|
-
platform: "node",
|
|
259
|
-
target: "node24",
|
|
260
|
-
format: "esm",
|
|
261
|
-
bundle: true,
|
|
262
|
-
external: expect.arrayContaining(["electron", "better-sqlite3"]),
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("electron-main.ts가 없으면 에러가 발생한다", async () => {
|
|
268
|
-
mockFsxExists.mockImplementation((p: string) => {
|
|
269
|
-
if (typeof p === "string" && p.includes("electron-main.ts")) return Promise.resolve(false);
|
|
270
|
-
return Promise.resolve(true);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
274
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
275
|
-
|
|
276
|
-
await expect(electron.build("/fake/out")).rejects.toThrow("electron-main.ts");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("config.env를 esbuild banner로 주입한다 (ELECTRON_DEV_URL 미포함)", async () => {
|
|
280
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
281
|
-
|
|
282
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
283
|
-
appId: "com.test.app",
|
|
284
|
-
env: { API_URL: "https://api.example.com" },
|
|
285
|
-
});
|
|
286
|
-
await electron.build("/fake/out");
|
|
287
|
-
|
|
288
|
-
const callArgs = mockEsbuildBuild.mock.calls[0][0];
|
|
289
|
-
const banner = callArgs.banner?.js as string;
|
|
290
|
-
expect(banner).toContain("process.env");
|
|
291
|
-
expect(banner).toContain("??=");
|
|
292
|
-
expect(banner).toContain("API_URL");
|
|
293
|
-
expect(banner).toContain("https://api.example.com");
|
|
294
|
-
expect(banner).not.toContain("ELECTRON_DEV_URL");
|
|
295
|
-
expect(callArgs.define).toBeUndefined();
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("ESM 배너에 createRequire shim이 포함된다", async () => {
|
|
299
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
300
|
-
|
|
301
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
302
|
-
await electron.build("/fake/out");
|
|
303
|
-
|
|
304
|
-
const callArgs = mockEsbuildBuild.mock.calls[0][0];
|
|
305
|
-
const banner = callArgs.banner?.js as string;
|
|
306
|
-
expect(banner).toContain("createRequire");
|
|
307
|
-
expect(banner).toContain("import.meta.url");
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
//#endregion
|
|
312
|
-
|
|
313
|
-
//#region Rule: electron-builder로 패키징한다
|
|
314
|
-
|
|
315
|
-
describe("Unit: Web assets 복사", () => {
|
|
316
|
-
it("dist 내 파일을 .electron/src에 복사하되 electron 디렉토리는 제외한다", async () => {
|
|
317
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
318
|
-
|
|
319
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
320
|
-
await electron.build("/fake/out");
|
|
321
|
-
|
|
322
|
-
const copies = normalizedCopyCalls();
|
|
323
|
-
expect(copies.some((c) => c[0].includes("/fake/out/index.html"))).toBe(true);
|
|
324
|
-
expect(copies.some((c) => c[0].includes("/fake/out/assets"))).toBe(true);
|
|
325
|
-
expect(copies.some((c) => c[0].includes("/fake/out/electron"))).toBe(false);
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
describe("Acceptance: electron-builder 설정 생성", () => {
|
|
330
|
-
it("portable=true이면 portable 타겟으로 설정한다", async () => {
|
|
331
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
332
|
-
|
|
333
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
334
|
-
appId: "com.test.app",
|
|
335
|
-
portable: true,
|
|
336
|
-
});
|
|
337
|
-
await electron.build("/fake/out");
|
|
338
|
-
|
|
339
|
-
const config = findBuilderConfig()!;
|
|
340
|
-
expect(config["appId"]).toBe("com.test.app");
|
|
341
|
-
expect((config["win"] as Record<string, unknown>)["target"]).toBe("portable");
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it("portable 미설정이면 nsis 타겟으로 설정한다", async () => {
|
|
345
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
346
|
-
|
|
347
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
348
|
-
await electron.build("/fake/out");
|
|
349
|
-
|
|
350
|
-
const config = findBuilderConfig()!;
|
|
351
|
-
expect((config["win"] as Record<string, unknown>)["target"]).toBe("nsis");
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("installerIcon이 설정되면 icon에 포함한다", async () => {
|
|
355
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
356
|
-
|
|
357
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
358
|
-
appId: "com.test.app",
|
|
359
|
-
installerIcon: "assets/icon.ico",
|
|
360
|
-
});
|
|
361
|
-
await electron.build("/fake/out");
|
|
362
|
-
|
|
363
|
-
const config = findBuilderConfig()!;
|
|
364
|
-
expect((config["icon"] as string).replace(/\\/g, "/")).toContain("assets/icon.ico");
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it("nsisOptions가 설정되면 nsis에 반영한다", async () => {
|
|
368
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
369
|
-
|
|
370
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
371
|
-
appId: "com.test.app",
|
|
372
|
-
nsisOptions: { oneClick: false, allowToChangeInstallationDirectory: true },
|
|
373
|
-
});
|
|
374
|
-
await electron.build("/fake/out");
|
|
375
|
-
|
|
376
|
-
const nsis = findBuilderConfig()!["nsis"] as Record<string, unknown>;
|
|
377
|
-
expect(nsis["oneClick"]).toBe(false);
|
|
378
|
-
expect(nsis["allowToChangeInstallationDirectory"]).toBe(true);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
//#endregion
|
|
383
|
-
|
|
384
|
-
//#region Rule: 빌드 산출물을 관리한다
|
|
385
|
-
|
|
386
|
-
describe("Unit: 빌드 산출물 복사", () => {
|
|
387
|
-
it("portable 빌드 시 {description}-portable-latest.exe로 복사한다", async () => {
|
|
388
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
389
|
-
|
|
390
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
391
|
-
appId: "com.test.app",
|
|
392
|
-
portable: true,
|
|
393
|
-
});
|
|
394
|
-
await electron.build("/fake/out");
|
|
395
|
-
|
|
396
|
-
const latestCopy = normalizedCopyCalls().find(
|
|
397
|
-
(c) => c[1].includes("electron") && c[1].includes("latest"),
|
|
398
|
-
);
|
|
399
|
-
expect(latestCopy).toBeDefined();
|
|
400
|
-
expect(latestCopy![1]).toContain("-portable-latest.exe");
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it("NSIS 빌드 시 {description}-latest.exe로 복사한다", async () => {
|
|
404
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
405
|
-
|
|
406
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
407
|
-
await electron.build("/fake/out");
|
|
408
|
-
|
|
409
|
-
const latestCopy = normalizedCopyCalls().find(
|
|
410
|
-
(c) => c[1].includes("electron") && c[1].includes("latest"),
|
|
411
|
-
);
|
|
412
|
-
expect(latestCopy).toBeDefined();
|
|
413
|
-
expect(latestCopy![1]).not.toContain("-portable-");
|
|
414
|
-
expect(latestCopy![1]).toContain("-latest.exe");
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("updates/{version}.exe에 버전별 복사를 한다", async () => {
|
|
418
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
419
|
-
|
|
420
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
421
|
-
await electron.build("/fake/out");
|
|
422
|
-
|
|
423
|
-
expect(
|
|
424
|
-
normalizedCopyCalls().find((c) => c[1].includes("updates/1.0.0.exe")),
|
|
425
|
-
).toBeDefined();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it("빌드 산출물이 없으면 경고를 출력한다", async () => {
|
|
429
|
-
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
430
|
-
const normalized = pattern.replace(/\\/g, "/");
|
|
431
|
-
if (normalized.endsWith(".electron/dist/*.exe")) return Promise.resolve([]);
|
|
432
|
-
return Promise.resolve([]);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
436
|
-
|
|
437
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
438
|
-
await electron.build("/fake/out");
|
|
439
|
-
|
|
440
|
-
expect(mockLoggerWarn).toHaveBeenCalledWith(expect.stringContaining("빌드 산출물"));
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
//#endregion
|
|
445
|
-
|
|
446
|
-
//#region Rule: 개발 모드에서 Electron 앱을 실행한다
|
|
447
|
-
|
|
448
|
-
describe("인수 테스트: run()", () => {
|
|
449
|
-
// Helper: creates a deferred cpx mock where Electron process can be controlled
|
|
450
|
-
function setupCpxForRun(): {
|
|
451
|
-
electronKill: ReturnType<typeof vi.fn>;
|
|
452
|
-
resolveElectron: () => void;
|
|
453
|
-
} {
|
|
454
|
-
const electronKill = vi.fn();
|
|
455
|
-
let resolveElectron: () => void = () => {};
|
|
456
|
-
|
|
457
|
-
mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
458
|
-
//
|
|
459
|
-
if (cmd === "
|
|
460
|
-
const p = new Promise<void>((resolve) => {
|
|
461
|
-
resolveElectron = resolve;
|
|
462
|
-
}) as any;
|
|
463
|
-
p.kill = electronKill;
|
|
464
|
-
return p;
|
|
465
|
-
}
|
|
466
|
-
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
return { electronKill, resolveElectron: () => resolveElectron() };
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
it("creates esbuild context with banner for env and spawns Electron", async () => {
|
|
473
|
-
const { resolveElectron } = setupCpxForRun();
|
|
474
|
-
|
|
475
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
476
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
477
|
-
|
|
478
|
-
const runPromise = electron.run("http://localhost:4200");
|
|
479
|
-
|
|
480
|
-
// Give event loop time to set up
|
|
481
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
482
|
-
|
|
483
|
-
// Resolve Electron process (simulating Electron exit)
|
|
484
|
-
resolveElectron();
|
|
485
|
-
await runPromise;
|
|
486
|
-
|
|
487
|
-
// esbuild.context was called with banner containing ELECTRON_DEV_URL
|
|
488
|
-
const callArgs = mockEsbuildContext.mock.calls[0][0];
|
|
489
|
-
const banner = callArgs.banner?.js as string;
|
|
490
|
-
expect(banner).toContain("process.env");
|
|
491
|
-
expect(banner).toContain("??=");
|
|
492
|
-
expect(banner).toContain("ELECTRON_DEV_URL");
|
|
493
|
-
expect(banner).toContain("http://localhost:4200");
|
|
494
|
-
expect(callArgs.define).toBeUndefined();
|
|
495
|
-
|
|
496
|
-
expect(callArgs.platform).toBe("node");
|
|
497
|
-
expect(callArgs.target).toBe("node24");
|
|
498
|
-
expect(callArgs.format).toBe("esm");
|
|
499
|
-
expect(callArgs.bundle).toBe(true);
|
|
500
|
-
expect(callArgs.external).toContain("electron");
|
|
501
|
-
const electronCall = mockCpxSpawn.mock.calls.find(
|
|
502
|
-
(c) => c[0] === "
|
|
503
|
-
);
|
|
504
|
-
expect(electronCall?.[2]).toEqual(expect.objectContaining({ shell: true, reject: false }));
|
|
505
|
-
|
|
506
|
-
// ESM 배너에 createRequire shim 포함
|
|
507
|
-
expect(banner).toContain("createRequire");
|
|
508
|
-
expect(banner).toContain("import.meta.url");
|
|
509
|
-
}, 10_000);
|
|
510
|
-
|
|
511
|
-
it("throws when electron-main.ts entry point is missing", async () => {
|
|
512
|
-
mockFsxExists.mockImplementation((p: string) => {
|
|
513
|
-
if (typeof p === "string" && p.includes("electron-main.ts")) return Promise.resolve(false);
|
|
514
|
-
return Promise.resolve(true);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
518
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
519
|
-
|
|
520
|
-
await expect(electron.run("http://localhost:4200")).rejects.toThrow("electron-main.ts");
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
it("resolves on SIGINT signal", async () => {
|
|
524
|
-
const { electronKill } = setupCpxForRun();
|
|
525
|
-
|
|
526
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
527
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
528
|
-
|
|
529
|
-
const runPromise = electron.run("http://localhost:4200");
|
|
530
|
-
|
|
531
|
-
// Give the event loop time to set up signal handlers
|
|
532
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
533
|
-
|
|
534
|
-
// Emit SIGINT to trigger cleanup
|
|
535
|
-
process.emit("SIGINT" as any);
|
|
536
|
-
|
|
537
|
-
// run() should resolve after signal
|
|
538
|
-
await runPromise;
|
|
539
|
-
|
|
540
|
-
expect(electronKill).toHaveBeenCalled();
|
|
541
|
-
}, 10_000);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
describe("단위: run() 플러그인 동작", () => {
|
|
545
|
-
it("passes custom env and ELECTRON_DEV_URL via esbuild banner", async () => {
|
|
546
|
-
let resolveElectron: () => void = () => {};
|
|
547
|
-
mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
548
|
-
if (cmd === "
|
|
549
|
-
const p = new Promise<void>((resolve) => {
|
|
550
|
-
resolveElectron = resolve;
|
|
551
|
-
}) as any;
|
|
552
|
-
p.kill = vi.fn();
|
|
553
|
-
return p;
|
|
554
|
-
}
|
|
555
|
-
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
559
|
-
const electron = await Electron.create(PKG_PATH, {
|
|
560
|
-
appId: "com.test.app",
|
|
561
|
-
env: { CUSTOM_VAR: "test-value" },
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
const runPromise = electron.run("http://localhost:5555");
|
|
565
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
566
|
-
resolveElectron();
|
|
567
|
-
await runPromise;
|
|
568
|
-
|
|
569
|
-
const callArgs = mockEsbuildContext.mock.calls[0][0];
|
|
570
|
-
const banner = callArgs.banner?.js as string;
|
|
571
|
-
expect(banner).toContain("ELECTRON_DEV_URL");
|
|
572
|
-
expect(banner).toContain("http://localhost:5555");
|
|
573
|
-
expect(banner).toContain("CUSTOM_VAR");
|
|
574
|
-
expect(banner).toContain("test-value");
|
|
575
|
-
expect(callArgs.define).toBeUndefined();
|
|
576
|
-
}, 10_000);
|
|
577
|
-
|
|
578
|
-
it("calls initialize() before starting esbuild context", async () => {
|
|
579
|
-
let resolveElectron: () => void = () => {};
|
|
580
|
-
mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
581
|
-
if (cmd === "
|
|
582
|
-
const p = new Promise<void>((resolve) => {
|
|
583
|
-
resolveElectron = resolve;
|
|
584
|
-
}) as any;
|
|
585
|
-
p.kill = vi.fn();
|
|
586
|
-
return p;
|
|
587
|
-
}
|
|
588
|
-
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
const { Electron } = await import("../../src/electron/electron.js");
|
|
592
|
-
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
593
|
-
|
|
594
|
-
const runPromise = electron.run("http://localhost:4200");
|
|
595
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
596
|
-
resolveElectron();
|
|
597
|
-
await runPromise;
|
|
598
|
-
|
|
599
|
-
// initialize calls
|
|
600
|
-
const
|
|
601
|
-
(c: any[]) => c[0] === "
|
|
602
|
-
);
|
|
603
|
-
expect(
|
|
604
|
-
}, 10_000);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
//#endregion
|
|
608
|
-
});
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import * as coreCommon from "@simplysm/core-common";
|
|
3
|
+
import { fsx, cpx } from "@simplysm/core-node";
|
|
4
|
+
|
|
5
|
+
// esbuild는 외부 npm으로 ESM namespace immutable이라 vi.mock 유지
|
|
6
|
+
const mockEsbuildBuild = vi.fn().mockResolvedValue({});
|
|
7
|
+
let mockEsbuildOnEndCallback: ((result: { errors: unknown[] }) => void | Promise<void>) | null =
|
|
8
|
+
null;
|
|
9
|
+
const mockEsbuildContext = vi.fn().mockImplementation((options: any) => {
|
|
10
|
+
const plugin = options?.plugins?.find((p: any) => p.name === "electron-restart");
|
|
11
|
+
if (plugin != null) {
|
|
12
|
+
plugin.setup({
|
|
13
|
+
onEnd: (cb: (result: { errors: unknown[] }) => void | Promise<void>) => {
|
|
14
|
+
mockEsbuildOnEndCallback = cb;
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
watch: vi.fn().mockImplementation(async () => {
|
|
20
|
+
if (mockEsbuildOnEndCallback != null) {
|
|
21
|
+
await mockEsbuildOnEndCallback({ errors: [] });
|
|
22
|
+
}
|
|
23
|
+
}),
|
|
24
|
+
dispose: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
vi.mock("esbuild", () => ({
|
|
28
|
+
build: mockEsbuildBuild,
|
|
29
|
+
context: mockEsbuildContext,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// @simplysm/core-node fsx, cpx는 spy로 전환
|
|
33
|
+
const mockFsxExists = vi.spyOn(fsx, "exists");
|
|
34
|
+
const mockFsxReadJson = vi.spyOn(fsx, "readJson");
|
|
35
|
+
const mockFsxWriteJson = vi.spyOn(fsx, "writeJson").mockResolvedValue(undefined);
|
|
36
|
+
vi.spyOn(fsx, "write").mockResolvedValue(undefined);
|
|
37
|
+
vi.spyOn(fsx, "mkdir").mockResolvedValue(undefined);
|
|
38
|
+
const mockFsxCopy = vi.spyOn(fsx, "copy").mockResolvedValue(undefined);
|
|
39
|
+
const mockFsxReaddir = vi.spyOn(fsx, "readdir");
|
|
40
|
+
const mockFsxGlob = vi.spyOn(fsx, "glob");
|
|
41
|
+
|
|
42
|
+
const mockCpxSpawn = vi.spyOn(cpx, "spawn").mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
43
|
+
vi.spyOn(cpx, "spawnSync").mockReturnValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
44
|
+
|
|
45
|
+
// createLogger spy
|
|
46
|
+
const mockLoggerDebug = vi.fn();
|
|
47
|
+
const mockLoggerWarn = vi.fn();
|
|
48
|
+
const mockLoggerInfo = vi.fn();
|
|
49
|
+
const mockLoggerStart = vi.fn();
|
|
50
|
+
const mockLoggerSuccess = vi.fn();
|
|
51
|
+
const mockLoggerError = vi.fn();
|
|
52
|
+
vi.spyOn(coreCommon, "createLogger").mockReturnValue({ debug: mockLoggerDebug, warn: mockLoggerWarn, info: mockLoggerInfo, start: mockLoggerStart, success: mockLoggerSuccess, error: mockLoggerError } as any);
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
|
|
56
|
+
//#region Helpers
|
|
57
|
+
|
|
58
|
+
const PKG_PATH = "/fake/pkg";
|
|
59
|
+
|
|
60
|
+
function setupDefaultMocks() {
|
|
61
|
+
mockFsxExists.mockResolvedValue(true);
|
|
62
|
+
mockFsxReadJson.mockResolvedValue({
|
|
63
|
+
name: "@myorg/my-app",
|
|
64
|
+
version: "1.0.0",
|
|
65
|
+
description: "My App",
|
|
66
|
+
dependencies: {
|
|
67
|
+
"better-sqlite3": "^11.0.0",
|
|
68
|
+
"sharp": "^0.34.0",
|
|
69
|
+
},
|
|
70
|
+
devDependencies: {
|
|
71
|
+
"electron": "^35.0.0",
|
|
72
|
+
"@electron/rebuild": "^4.0.0",
|
|
73
|
+
"electron-builder": "^26.0.0",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
mockFsxReaddir.mockResolvedValue(["index.html", "assets", "electron"]);
|
|
77
|
+
// Default: glob returns one exe file matching the builder output
|
|
78
|
+
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
79
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
80
|
+
if (normalized.endsWith(".electron/dist/*.exe")) {
|
|
81
|
+
const dir = normalized.replace("/*.exe", "");
|
|
82
|
+
return Promise.resolve([dir + "/My App Setup 1.0.0.exe"]);
|
|
83
|
+
}
|
|
84
|
+
return Promise.resolve([]);
|
|
85
|
+
});
|
|
86
|
+
mockCpxSpawn.mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
87
|
+
mockEsbuildBuild.mockResolvedValue({});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findWriteJson(pathFragment: string): Record<string, unknown> | undefined {
|
|
91
|
+
const call = mockFsxWriteJson.mock.calls.find(
|
|
92
|
+
(c) =>
|
|
93
|
+
typeof c[0] === "string" &&
|
|
94
|
+
c[0].replace(/\\/g, "/").includes(pathFragment),
|
|
95
|
+
);
|
|
96
|
+
return call ? (call[1] as Record<string, unknown>) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findElectronPackageJson(): Record<string, unknown> | undefined {
|
|
100
|
+
return findWriteJson(".electron/src/package.json");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findBuilderConfig(): Record<string, unknown> | undefined {
|
|
104
|
+
return findWriteJson("builder-config.json");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizedCopyCalls(): string[][] {
|
|
108
|
+
return mockFsxCopy.mock.calls.map((c) => [
|
|
109
|
+
c[0].replace(/\\/g, "/"),
|
|
110
|
+
c[1].replace(/\\/g, "/"),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
|
|
116
|
+
describe("Electron", () => {
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
vi.clearAllMocks();
|
|
119
|
+
mockEsbuildOnEndCallback = null;
|
|
120
|
+
setupDefaultMocks();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
//#region Rule: 설정을 검증한다
|
|
124
|
+
|
|
125
|
+
describe("Acceptance: appId가 없으면 에러가 발생한다", () => {
|
|
126
|
+
it("appId가 빈 문자열이면 에러를 던진다", async () => {
|
|
127
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
128
|
+
|
|
129
|
+
await expect(
|
|
130
|
+
Electron.create(PKG_PATH, { appId: "" }),
|
|
131
|
+
).rejects.toThrow("appId");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("appId가 공백만 있으면 에러를 던진다", async () => {
|
|
135
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
Electron.create(PKG_PATH, { appId: " " }),
|
|
139
|
+
).rejects.toThrow("appId");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
|
|
145
|
+
//#region Rule: Electron 프로젝트를 초기화한다
|
|
146
|
+
|
|
147
|
+
describe("인수 테스트: 초기화", () => {
|
|
148
|
+
it("package.json 생성 + bun install + electron-rebuild를 실행한다", async () => {
|
|
149
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
150
|
+
|
|
151
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
152
|
+
appId: "com.test.app",
|
|
153
|
+
reinstallDependencies: ["better-sqlite3"],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await electron.initialize();
|
|
157
|
+
|
|
158
|
+
expect(findElectronPackageJson()).toBeDefined();
|
|
159
|
+
|
|
160
|
+
const spawnCalls = mockCpxSpawn.mock.calls;
|
|
161
|
+
const installCall = spawnCalls.find(
|
|
162
|
+
(c) => c[0] === "bun" && c[1].includes("install"),
|
|
163
|
+
);
|
|
164
|
+
expect(installCall).toBeDefined();
|
|
165
|
+
expect(installCall?.[2]).toEqual(expect.objectContaining({ shell: true }));
|
|
166
|
+
expect(
|
|
167
|
+
spawnCalls.find(
|
|
168
|
+
(c) => c[0] === "bun" && c[1].includes("electron-rebuild"),
|
|
169
|
+
),
|
|
170
|
+
).toBeDefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("reinstallDependencies가 비어있으면 electron-rebuild를 건너뛴다", async () => {
|
|
174
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
175
|
+
|
|
176
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
177
|
+
await electron.initialize();
|
|
178
|
+
|
|
179
|
+
const rebuildCall = mockCpxSpawn.mock.calls.find(
|
|
180
|
+
(c) => c[0] === "bun" && c[1].includes("electron-rebuild"),
|
|
181
|
+
);
|
|
182
|
+
expect(rebuildCall).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("단위: _setupPackageJson", () => {
|
|
187
|
+
it("스코프 패키지명을 정규화한다", async () => {
|
|
188
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
189
|
+
|
|
190
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
191
|
+
await electron.initialize();
|
|
192
|
+
|
|
193
|
+
expect(findElectronPackageJson()!["name"]).toBe("myorg-my-app");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("reinstallDependencies 패키지를 dependencies에 포함한다", async () => {
|
|
197
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
198
|
+
|
|
199
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
200
|
+
appId: "com.test.app",
|
|
201
|
+
reinstallDependencies: ["better-sqlite3"],
|
|
202
|
+
});
|
|
203
|
+
await electron.initialize();
|
|
204
|
+
|
|
205
|
+
const deps = findElectronPackageJson()!["dependencies"] as Record<string, string>;
|
|
206
|
+
expect(deps["better-sqlite3"]).toBe("^11.0.0");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("exclude 패키지를 dependencies에 포함한다", async () => {
|
|
210
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
211
|
+
|
|
212
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" }, ["sharp"]);
|
|
213
|
+
await electron.initialize();
|
|
214
|
+
|
|
215
|
+
const deps = findElectronPackageJson()!["dependencies"] as Record<string, string>;
|
|
216
|
+
expect(deps["sharp"]).toBe("^0.34.0");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("package.json에 type: module이 설정된다", async () => {
|
|
220
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
221
|
+
|
|
222
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
223
|
+
await electron.initialize();
|
|
224
|
+
|
|
225
|
+
expect(findElectronPackageJson()!["type"]).toBe("module");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("postInstallScript가 설정되면 scripts.postinstall에 포함한다", async () => {
|
|
229
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
230
|
+
|
|
231
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
232
|
+
appId: "com.test.app",
|
|
233
|
+
postInstallScript: "node rebuild.js",
|
|
234
|
+
});
|
|
235
|
+
await electron.initialize();
|
|
236
|
+
|
|
237
|
+
const scripts = findElectronPackageJson()!["scripts"] as Record<string, string>;
|
|
238
|
+
expect(scripts["postinstall"]).toBe("node rebuild.js");
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
//#endregion
|
|
243
|
+
|
|
244
|
+
//#region Rule: 메인 프로세스를 번들링한다
|
|
245
|
+
|
|
246
|
+
describe("Acceptance: build — esbuild 번들링", () => {
|
|
247
|
+
it("src/electron-main.ts를 esbuild로 번들링한다", async () => {
|
|
248
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
249
|
+
|
|
250
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
251
|
+
appId: "com.test.app",
|
|
252
|
+
reinstallDependencies: ["better-sqlite3"],
|
|
253
|
+
});
|
|
254
|
+
await electron.build("/fake/out");
|
|
255
|
+
|
|
256
|
+
expect(mockEsbuildBuild).toHaveBeenCalledWith(
|
|
257
|
+
expect.objectContaining({
|
|
258
|
+
platform: "node",
|
|
259
|
+
target: "node24",
|
|
260
|
+
format: "esm",
|
|
261
|
+
bundle: true,
|
|
262
|
+
external: expect.arrayContaining(["electron", "better-sqlite3"]),
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("electron-main.ts가 없으면 에러가 발생한다", async () => {
|
|
268
|
+
mockFsxExists.mockImplementation((p: string) => {
|
|
269
|
+
if (typeof p === "string" && p.includes("electron-main.ts")) return Promise.resolve(false);
|
|
270
|
+
return Promise.resolve(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
274
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
275
|
+
|
|
276
|
+
await expect(electron.build("/fake/out")).rejects.toThrow("electron-main.ts");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("config.env를 esbuild banner로 주입한다 (ELECTRON_DEV_URL 미포함)", async () => {
|
|
280
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
281
|
+
|
|
282
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
283
|
+
appId: "com.test.app",
|
|
284
|
+
env: { API_URL: "https://api.example.com" },
|
|
285
|
+
});
|
|
286
|
+
await electron.build("/fake/out");
|
|
287
|
+
|
|
288
|
+
const callArgs = mockEsbuildBuild.mock.calls[0][0];
|
|
289
|
+
const banner = callArgs.banner?.js as string;
|
|
290
|
+
expect(banner).toContain("process.env");
|
|
291
|
+
expect(banner).toContain("??=");
|
|
292
|
+
expect(banner).toContain("API_URL");
|
|
293
|
+
expect(banner).toContain("https://api.example.com");
|
|
294
|
+
expect(banner).not.toContain("ELECTRON_DEV_URL");
|
|
295
|
+
expect(callArgs.define).toBeUndefined();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("ESM 배너에 createRequire shim이 포함된다", async () => {
|
|
299
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
300
|
+
|
|
301
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
302
|
+
await electron.build("/fake/out");
|
|
303
|
+
|
|
304
|
+
const callArgs = mockEsbuildBuild.mock.calls[0][0];
|
|
305
|
+
const banner = callArgs.banner?.js as string;
|
|
306
|
+
expect(banner).toContain("createRequire");
|
|
307
|
+
expect(banner).toContain("import.meta.url");
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
//#endregion
|
|
312
|
+
|
|
313
|
+
//#region Rule: electron-builder로 패키징한다
|
|
314
|
+
|
|
315
|
+
describe("Unit: Web assets 복사", () => {
|
|
316
|
+
it("dist 내 파일을 .electron/src에 복사하되 electron 디렉토리는 제외한다", async () => {
|
|
317
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
318
|
+
|
|
319
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
320
|
+
await electron.build("/fake/out");
|
|
321
|
+
|
|
322
|
+
const copies = normalizedCopyCalls();
|
|
323
|
+
expect(copies.some((c) => c[0].includes("/fake/out/index.html"))).toBe(true);
|
|
324
|
+
expect(copies.some((c) => c[0].includes("/fake/out/assets"))).toBe(true);
|
|
325
|
+
expect(copies.some((c) => c[0].includes("/fake/out/electron"))).toBe(false);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("Acceptance: electron-builder 설정 생성", () => {
|
|
330
|
+
it("portable=true이면 portable 타겟으로 설정한다", async () => {
|
|
331
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
332
|
+
|
|
333
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
334
|
+
appId: "com.test.app",
|
|
335
|
+
portable: true,
|
|
336
|
+
});
|
|
337
|
+
await electron.build("/fake/out");
|
|
338
|
+
|
|
339
|
+
const config = findBuilderConfig()!;
|
|
340
|
+
expect(config["appId"]).toBe("com.test.app");
|
|
341
|
+
expect((config["win"] as Record<string, unknown>)["target"]).toBe("portable");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("portable 미설정이면 nsis 타겟으로 설정한다", async () => {
|
|
345
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
346
|
+
|
|
347
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
348
|
+
await electron.build("/fake/out");
|
|
349
|
+
|
|
350
|
+
const config = findBuilderConfig()!;
|
|
351
|
+
expect((config["win"] as Record<string, unknown>)["target"]).toBe("nsis");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("installerIcon이 설정되면 icon에 포함한다", async () => {
|
|
355
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
356
|
+
|
|
357
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
358
|
+
appId: "com.test.app",
|
|
359
|
+
installerIcon: "assets/icon.ico",
|
|
360
|
+
});
|
|
361
|
+
await electron.build("/fake/out");
|
|
362
|
+
|
|
363
|
+
const config = findBuilderConfig()!;
|
|
364
|
+
expect((config["icon"] as string).replace(/\\/g, "/")).toContain("assets/icon.ico");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("nsisOptions가 설정되면 nsis에 반영한다", async () => {
|
|
368
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
369
|
+
|
|
370
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
371
|
+
appId: "com.test.app",
|
|
372
|
+
nsisOptions: { oneClick: false, allowToChangeInstallationDirectory: true },
|
|
373
|
+
});
|
|
374
|
+
await electron.build("/fake/out");
|
|
375
|
+
|
|
376
|
+
const nsis = findBuilderConfig()!["nsis"] as Record<string, unknown>;
|
|
377
|
+
expect(nsis["oneClick"]).toBe(false);
|
|
378
|
+
expect(nsis["allowToChangeInstallationDirectory"]).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
//#endregion
|
|
383
|
+
|
|
384
|
+
//#region Rule: 빌드 산출물을 관리한다
|
|
385
|
+
|
|
386
|
+
describe("Unit: 빌드 산출물 복사", () => {
|
|
387
|
+
it("portable 빌드 시 {description}-portable-latest.exe로 복사한다", async () => {
|
|
388
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
389
|
+
|
|
390
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
391
|
+
appId: "com.test.app",
|
|
392
|
+
portable: true,
|
|
393
|
+
});
|
|
394
|
+
await electron.build("/fake/out");
|
|
395
|
+
|
|
396
|
+
const latestCopy = normalizedCopyCalls().find(
|
|
397
|
+
(c) => c[1].includes("electron") && c[1].includes("latest"),
|
|
398
|
+
);
|
|
399
|
+
expect(latestCopy).toBeDefined();
|
|
400
|
+
expect(latestCopy![1]).toContain("-portable-latest.exe");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("NSIS 빌드 시 {description}-latest.exe로 복사한다", async () => {
|
|
404
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
405
|
+
|
|
406
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
407
|
+
await electron.build("/fake/out");
|
|
408
|
+
|
|
409
|
+
const latestCopy = normalizedCopyCalls().find(
|
|
410
|
+
(c) => c[1].includes("electron") && c[1].includes("latest"),
|
|
411
|
+
);
|
|
412
|
+
expect(latestCopy).toBeDefined();
|
|
413
|
+
expect(latestCopy![1]).not.toContain("-portable-");
|
|
414
|
+
expect(latestCopy![1]).toContain("-latest.exe");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("updates/{version}.exe에 버전별 복사를 한다", async () => {
|
|
418
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
419
|
+
|
|
420
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
421
|
+
await electron.build("/fake/out");
|
|
422
|
+
|
|
423
|
+
expect(
|
|
424
|
+
normalizedCopyCalls().find((c) => c[1].includes("updates/1.0.0.exe")),
|
|
425
|
+
).toBeDefined();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("빌드 산출물이 없으면 경고를 출력한다", async () => {
|
|
429
|
+
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
430
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
431
|
+
if (normalized.endsWith(".electron/dist/*.exe")) return Promise.resolve([]);
|
|
432
|
+
return Promise.resolve([]);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
436
|
+
|
|
437
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
438
|
+
await electron.build("/fake/out");
|
|
439
|
+
|
|
440
|
+
expect(mockLoggerWarn).toHaveBeenCalledWith(expect.stringContaining("빌드 산출물"));
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
//#endregion
|
|
445
|
+
|
|
446
|
+
//#region Rule: 개발 모드에서 Electron 앱을 실행한다
|
|
447
|
+
|
|
448
|
+
describe("인수 테스트: run()", () => {
|
|
449
|
+
// Helper: creates a deferred cpx mock where Electron process can be controlled
|
|
450
|
+
function setupCpxForRun(): {
|
|
451
|
+
electronKill: ReturnType<typeof vi.fn>;
|
|
452
|
+
resolveElectron: () => void;
|
|
453
|
+
} {
|
|
454
|
+
const electronKill = vi.fn();
|
|
455
|
+
let resolveElectron: () => void = () => {};
|
|
456
|
+
|
|
457
|
+
mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
458
|
+
// bun run electron . → Electron 프로세스
|
|
459
|
+
if (cmd === "bun" && args[0] === "run" && args[1] === "electron" && args[2] === ".") {
|
|
460
|
+
const p = new Promise<void>((resolve) => {
|
|
461
|
+
resolveElectron = resolve;
|
|
462
|
+
}) as any;
|
|
463
|
+
p.kill = electronKill;
|
|
464
|
+
return p;
|
|
465
|
+
}
|
|
466
|
+
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return { electronKill, resolveElectron: () => resolveElectron() };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
it("creates esbuild context with banner for env and spawns Electron", async () => {
|
|
473
|
+
const { resolveElectron } = setupCpxForRun();
|
|
474
|
+
|
|
475
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
476
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
477
|
+
|
|
478
|
+
const runPromise = electron.run("http://localhost:4200");
|
|
479
|
+
|
|
480
|
+
// Give event loop time to set up
|
|
481
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
482
|
+
|
|
483
|
+
// Resolve Electron process (simulating Electron exit)
|
|
484
|
+
resolveElectron();
|
|
485
|
+
await runPromise;
|
|
486
|
+
|
|
487
|
+
// esbuild.context was called with banner containing ELECTRON_DEV_URL
|
|
488
|
+
const callArgs = mockEsbuildContext.mock.calls[0][0];
|
|
489
|
+
const banner = callArgs.banner?.js as string;
|
|
490
|
+
expect(banner).toContain("process.env");
|
|
491
|
+
expect(banner).toContain("??=");
|
|
492
|
+
expect(banner).toContain("ELECTRON_DEV_URL");
|
|
493
|
+
expect(banner).toContain("http://localhost:4200");
|
|
494
|
+
expect(callArgs.define).toBeUndefined();
|
|
495
|
+
|
|
496
|
+
expect(callArgs.platform).toBe("node");
|
|
497
|
+
expect(callArgs.target).toBe("node24");
|
|
498
|
+
expect(callArgs.format).toBe("esm");
|
|
499
|
+
expect(callArgs.bundle).toBe(true);
|
|
500
|
+
expect(callArgs.external).toContain("electron");
|
|
501
|
+
const electronCall = mockCpxSpawn.mock.calls.find(
|
|
502
|
+
(c) => c[0] === "bun" && c[1].includes("electron"),
|
|
503
|
+
);
|
|
504
|
+
expect(electronCall?.[2]).toEqual(expect.objectContaining({ shell: true, reject: false }));
|
|
505
|
+
|
|
506
|
+
// ESM 배너에 createRequire shim 포함
|
|
507
|
+
expect(banner).toContain("createRequire");
|
|
508
|
+
expect(banner).toContain("import.meta.url");
|
|
509
|
+
}, 10_000);
|
|
510
|
+
|
|
511
|
+
it("throws when electron-main.ts entry point is missing", async () => {
|
|
512
|
+
mockFsxExists.mockImplementation((p: string) => {
|
|
513
|
+
if (typeof p === "string" && p.includes("electron-main.ts")) return Promise.resolve(false);
|
|
514
|
+
return Promise.resolve(true);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
518
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
519
|
+
|
|
520
|
+
await expect(electron.run("http://localhost:4200")).rejects.toThrow("electron-main.ts");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("resolves on SIGINT signal", async () => {
|
|
524
|
+
const { electronKill } = setupCpxForRun();
|
|
525
|
+
|
|
526
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
527
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
528
|
+
|
|
529
|
+
const runPromise = electron.run("http://localhost:4200");
|
|
530
|
+
|
|
531
|
+
// Give the event loop time to set up signal handlers
|
|
532
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
533
|
+
|
|
534
|
+
// Emit SIGINT to trigger cleanup
|
|
535
|
+
process.emit("SIGINT" as any);
|
|
536
|
+
|
|
537
|
+
// run() should resolve after signal
|
|
538
|
+
await runPromise;
|
|
539
|
+
|
|
540
|
+
expect(electronKill).toHaveBeenCalled();
|
|
541
|
+
}, 10_000);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe("단위: run() 플러그인 동작", () => {
|
|
545
|
+
it("passes custom env and ELECTRON_DEV_URL via esbuild banner", async () => {
|
|
546
|
+
let resolveElectron: () => void = () => {};
|
|
547
|
+
mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
548
|
+
if (cmd === "bun" && args[0] === "run" && args[1] === "electron" && args[2] === ".") {
|
|
549
|
+
const p = new Promise<void>((resolve) => {
|
|
550
|
+
resolveElectron = resolve;
|
|
551
|
+
}) as any;
|
|
552
|
+
p.kill = vi.fn();
|
|
553
|
+
return p;
|
|
554
|
+
}
|
|
555
|
+
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
559
|
+
const electron = await Electron.create(PKG_PATH, {
|
|
560
|
+
appId: "com.test.app",
|
|
561
|
+
env: { CUSTOM_VAR: "test-value" },
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const runPromise = electron.run("http://localhost:5555");
|
|
565
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
566
|
+
resolveElectron();
|
|
567
|
+
await runPromise;
|
|
568
|
+
|
|
569
|
+
const callArgs = mockEsbuildContext.mock.calls[0][0];
|
|
570
|
+
const banner = callArgs.banner?.js as string;
|
|
571
|
+
expect(banner).toContain("ELECTRON_DEV_URL");
|
|
572
|
+
expect(banner).toContain("http://localhost:5555");
|
|
573
|
+
expect(banner).toContain("CUSTOM_VAR");
|
|
574
|
+
expect(banner).toContain("test-value");
|
|
575
|
+
expect(callArgs.define).toBeUndefined();
|
|
576
|
+
}, 10_000);
|
|
577
|
+
|
|
578
|
+
it("calls initialize() before starting esbuild context", async () => {
|
|
579
|
+
let resolveElectron: () => void = () => {};
|
|
580
|
+
mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
581
|
+
if (cmd === "bun" && args[0] === "run" && args[1] === "electron" && args[2] === ".") {
|
|
582
|
+
const p = new Promise<void>((resolve) => {
|
|
583
|
+
resolveElectron = resolve;
|
|
584
|
+
}) as any;
|
|
585
|
+
p.kill = vi.fn();
|
|
586
|
+
return p;
|
|
587
|
+
}
|
|
588
|
+
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const { Electron } = await import("../../src/electron/electron.js");
|
|
592
|
+
const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
|
|
593
|
+
|
|
594
|
+
const runPromise = electron.run("http://localhost:4200");
|
|
595
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
596
|
+
resolveElectron();
|
|
597
|
+
await runPromise;
|
|
598
|
+
|
|
599
|
+
// initialize calls bun install
|
|
600
|
+
const bunInstallCall = mockCpxSpawn.mock.calls.find(
|
|
601
|
+
(c: any[]) => c[0] === "bun" && (c[1] as string[]).includes("install"),
|
|
602
|
+
);
|
|
603
|
+
expect(bunInstallCall).toBeDefined();
|
|
604
|
+
}, 10_000);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
//#endregion
|
|
608
|
+
});
|