@simplysm/sd-cli 14.0.64 → 14.0.65
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-android.d.ts +2 -0
- package/dist/capacitor/capacitor-android.d.ts.map +1 -1
- package/dist/capacitor/capacitor-android.js +13 -0
- package/dist/capacitor/capacitor-android.js.map +1 -1
- package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
- package/dist/capacitor/capacitor-npm-config.js +2 -6
- package/dist/capacitor/capacitor-npm-config.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +1 -2
- package/dist/electron/electron.js.map +1 -1
- package/package.json +8 -8
- package/src/capacitor/capacitor-android.ts +14 -0
- package/src/capacitor/capacitor-npm-config.ts +2 -6
- package/src/electron/electron.ts +1 -2
- package/tests/angular/ngtsc-build-core.acc.spec.ts +36 -94
- package/tests/capacitor/capacitor-android.spec.ts +65 -28
- package/tests/capacitor/capacitor-build.spec.ts +40 -385
- package/tests/capacitor/capacitor-config-writer.acc.spec.ts +3 -17
- package/tests/capacitor/capacitor-config-writer.spec.ts +3 -17
- package/tests/capacitor/capacitor-init.spec.ts +40 -636
- package/tests/capacitor/capacitor-npm-config.acc.spec.ts +38 -168
- package/tests/capacitor/capacitor-npm-config.spec.ts +33 -71
- package/tests/commands/check.spec.ts +25 -36
- package/tests/commands/deployment-phase.acc.spec.ts +17 -26
- package/tests/commands/git-phase.acc.spec.ts +13 -112
- package/tests/commands/lint.spec.ts +7 -24
- package/tests/commands/post-publish-phase.acc.spec.ts +5 -10
- package/tests/commands/typecheck.spec.ts +43 -65
- package/tests/electron/electron.spec.ts +22 -46
- package/tests/engines/base-engine.spec.ts +4 -13
- package/tests/engines/engine-selection.spec.ts +14 -17
- package/tests/engines/engine-typecheck-selection.acc.spec.ts +13 -16
- package/tests/engines/esbuild-client-engine.acc.spec.ts +36 -40
- package/tests/engines/esbuild-client-engine.spec.ts +4 -23
- package/tests/engines/ngtsc-engine.spec.ts +3 -10
- package/tests/engines/server-esbuild-engine.spec.ts +3 -10
- package/tests/engines/tsc-engine.spec.ts +3 -10
- package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +3 -8
- package/tests/esbuild/esbuild-tsc-plugin.spec.ts +3 -8
- package/tests/orchestrators/build-orchestrator.spec.ts +57 -102
- package/tests/orchestrators/dev-orchestrator.spec.ts +68 -109
- package/tests/orchestrators/typecheck-orchestrator.spec.ts +25 -57
- package/tests/orchestrators/watch-orchestrator.spec.ts +73 -99
- package/tests/sd-cli-entry.spec.ts +17 -20
- package/tests/utils/angular-source-file-cache.spec.ts +4 -8
- package/tests/utils/copy-src.spec.ts +9 -20
- package/tests/utils/esbuild-client-config.acc.spec.ts +9 -15
- package/tests/utils/esbuild-client-config.spec.ts +12 -24
- package/tests/utils/esbuild-config.spec.ts +51 -42
- package/tests/utils/lint-core.spec.ts +13 -19
- package/tests/utils/lint-utils.spec.ts +8 -15
- package/tests/utils/lint-with-program.spec.ts +3 -7
- package/tests/utils/ngtsc-build-core.spec.ts +2 -99
- package/tests/utils/orchestrator-utils.spec.ts +7 -20
- package/tests/utils/output-utils.spec.ts +5 -11
- package/tests/utils/sd-config.spec.ts +4 -12
- package/tests/utils/typecheck-env.spec.ts +49 -77
- package/tests/utils/typecheck-non-package.spec.ts +23 -16
- package/tests/workers/build-watch-paths.acc.spec.ts +4 -10
- package/tests/workers/build-watch-paths.spec.ts +4 -9
- package/tests/workers/client-worker.acc.spec.ts +64 -137
- package/tests/workers/client-worker.spec.ts +63 -89
- package/tests/workers/library-build-lint.spec.ts +19 -30
- package/tests/workers/library-build-worker.spec.ts +28 -55
- package/tests/workers/server-esbuild-context.acc.spec.ts +6 -15
- package/tests/workers/server-esbuild-context.spec.ts +7 -16
- package/tests/workers/server-runtime-worker.spec.ts +8 -10
- package/tests/workers/shared-worker-lifecycle.acc.spec.ts +3 -5
- package/tests/workers/shared-worker-lifecycle.spec.ts +4 -5
- package/tests/capacitor/capacitor-icon.spec.ts +0 -285
- package/tests/capacitor/capacitor-run.spec.ts +0 -256
- package/tests/capacitor/capacitor-workspace.spec.ts +0 -203
- package/tests/commands/device.spec.ts +0 -237
- package/tests/commands/publish.spec.ts +0 -1183
- package/tests/utils/external-modules.spec.ts +0 -217
- package/tests/workers/server-build-lint.spec.ts +0 -201
- package/tests/workers/server-build-worker.spec.ts +0 -765
- package/tests/workers/server-watch-manager.acc.spec.ts +0 -162
- package/tests/workers/server-watch-manager.spec.ts +0 -199
|
@@ -1,40 +1,77 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-properties -- 테스트 환경변수 조작 필요 */
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
import { fsx } from "@simplysm/core-node";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const mockFsxWrite = vi.fn().mockResolvedValue(undefined);
|
|
9
|
-
const mockFsxGlob = vi.fn();
|
|
10
|
-
|
|
11
|
-
vi.mock("@simplysm/core-node", async (importOriginal) => {
|
|
12
|
-
const original = await importOriginal<typeof import("@simplysm/core-node")>();
|
|
13
|
-
return {
|
|
14
|
-
...original,
|
|
15
|
-
fsx: {
|
|
16
|
-
exists: mockFsxExists,
|
|
17
|
-
read: mockFsxRead,
|
|
18
|
-
write: mockFsxWrite,
|
|
19
|
-
glob: mockFsxGlob,
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
//#endregion
|
|
5
|
+
const mockFsxExists = vi.spyOn(fsx, "exists");
|
|
6
|
+
const mockFsxRead = vi.spyOn(fsx, "read");
|
|
7
|
+
const mockFsxWrite = vi.spyOn(fsx, "write").mockResolvedValue(undefined);
|
|
8
|
+
const mockFsxGlob = vi.spyOn(fsx, "glob");
|
|
25
9
|
|
|
26
10
|
describe("findJava21", () => {
|
|
11
|
+
let savedEnv: Record<string, string | undefined>;
|
|
27
12
|
beforeEach(() => {
|
|
28
13
|
vi.clearAllMocks();
|
|
14
|
+
savedEnv = { ...process.env };
|
|
15
|
+
delete process.env["JAVA_HOME"];
|
|
16
|
+
mockFsxExists.mockResolvedValue(false);
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
process.env = savedEnv;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("JAVA_HOME이 Java 21이면 최우선으로 사용한다", async () => {
|
|
23
|
+
process.env["JAVA_HOME"] = "C:\\Program Files\\Java\\jdk-21.0.5";
|
|
24
|
+
mockFsxExists.mockImplementation(((p: string) =>
|
|
25
|
+
p === "C:/Program Files/Java/jdk-21.0.5/release") as never);
|
|
26
|
+
mockFsxRead.mockResolvedValue(
|
|
27
|
+
'IMPLEMENTOR="Oracle"\nJAVA_VERSION="21.0.5"\nJAVA_VERSION_DATE="2024-10-15"\n',
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const { findJava21 } = await import("../../src/capacitor/capacitor-android.js");
|
|
31
|
+
const result = await findJava21();
|
|
32
|
+
expect(result).toBe("C:/Program Files/Java/jdk-21.0.5");
|
|
33
|
+
expect(mockFsxGlob).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("JAVA_HOME이 21이 아니면 glob 패턴 폴백으로 진행한다", async () => {
|
|
37
|
+
process.env["JAVA_HOME"] = "C:\\Program Files\\Java\\jdk-17.0.10";
|
|
38
|
+
mockFsxExists.mockImplementation(((p: string) =>
|
|
39
|
+
p === "C:/Program Files/Java/jdk-17.0.10/release") as never);
|
|
40
|
+
mockFsxRead.mockResolvedValue('JAVA_VERSION="17.0.10"\n');
|
|
41
|
+
mockFsxGlob.mockImplementation(((pattern: string) => {
|
|
42
|
+
if (pattern.includes("Amazon Corretto")) {
|
|
43
|
+
return ["C:/Program Files/Amazon Corretto/jdk21.0.1"];
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
}) as never);
|
|
47
|
+
|
|
48
|
+
const { findJava21 } = await import("../../src/capacitor/capacitor-android.js");
|
|
49
|
+
const result = await findJava21();
|
|
50
|
+
expect(result).toBe("C:/Program Files/Amazon Corretto/jdk21.0.1");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("JAVA_HOME에 release 파일이 없으면 glob 패턴 폴백으로 진행한다", async () => {
|
|
54
|
+
process.env["JAVA_HOME"] = "C:\\nonexistent";
|
|
55
|
+
mockFsxExists.mockResolvedValue(false);
|
|
56
|
+
mockFsxGlob.mockImplementation(((pattern: string) => {
|
|
57
|
+
if (pattern.includes("Eclipse Adoptium")) {
|
|
58
|
+
return ["C:/Program Files/Eclipse Adoptium/jdk-21.0.4"];
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}) as never);
|
|
62
|
+
|
|
63
|
+
const { findJava21 } = await import("../../src/capacitor/capacitor-android.js");
|
|
64
|
+
const result = await findJava21();
|
|
65
|
+
expect(result).toBe("C:/Program Files/Eclipse Adoptium/jdk-21.0.4");
|
|
29
66
|
});
|
|
30
67
|
|
|
31
68
|
it("여러 패턴 중 첫 매치를 사용하고 마지막 정렬 결과를 반환한다", async () => {
|
|
32
|
-
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
69
|
+
mockFsxGlob.mockImplementation(((pattern: string) => {
|
|
33
70
|
if (pattern.includes("Amazon Corretto")) {
|
|
34
71
|
return ["C:/Program Files/Amazon Corretto/jdk21.0.1", "C:/Program Files/Amazon Corretto/jdk21.0.3"];
|
|
35
72
|
}
|
|
36
73
|
return [];
|
|
37
|
-
});
|
|
74
|
+
}) as never);
|
|
38
75
|
|
|
39
76
|
const { findJava21 } = await import("../../src/capacitor/capacitor-android.js");
|
|
40
77
|
const result = await findJava21();
|
|
@@ -62,7 +99,7 @@ describe("findAndroidSdk", () => {
|
|
|
62
99
|
|
|
63
100
|
it("ANDROID_SDK_ROOT로 SDK를 감지한다", async () => {
|
|
64
101
|
process.env["ANDROID_SDK_ROOT"] = "D:/Android/Sdk";
|
|
65
|
-
mockFsxExists.mockImplementation((p: string) => p === "D:/Android/Sdk");
|
|
102
|
+
mockFsxExists.mockImplementation(((p: string) => p === "D:/Android/Sdk") as never);
|
|
66
103
|
|
|
67
104
|
const { findAndroidSdk } = await import("../../src/capacitor/capacitor-android.js");
|
|
68
105
|
const result = await findAndroidSdk();
|
|
@@ -103,7 +140,7 @@ describe("configureAndroid", () => {
|
|
|
103
140
|
it("모든 Android 설정을 순서대로 수행한다", async () => {
|
|
104
141
|
mockFsxExists.mockResolvedValue(true);
|
|
105
142
|
mockFsxGlob.mockResolvedValue(["C:/Program Files/Amazon Corretto/jdk21.0.1"]);
|
|
106
|
-
mockFsxRead.mockImplementation((p: string) => {
|
|
143
|
+
mockFsxRead.mockImplementation(((p: string) => {
|
|
107
144
|
if (p.includes("gradle.properties")) return "org.gradle.jvmargs=-Xmx2048m";
|
|
108
145
|
if (p.includes("local.properties")) return "";
|
|
109
146
|
if (p.includes("AndroidManifest.xml")) {
|
|
@@ -131,7 +168,7 @@ describe("configureAndroid", () => {
|
|
|
131
168
|
</resources>`;
|
|
132
169
|
}
|
|
133
170
|
return "";
|
|
134
|
-
});
|
|
171
|
+
}) as never);
|
|
135
172
|
|
|
136
173
|
const { configureAndroid } = await import("../../src/capacitor/capacitor-android.js");
|
|
137
174
|
await configureAndroid(
|
|
@@ -175,7 +212,7 @@ describe("configureAndroid", () => {
|
|
|
175
212
|
it("minor/patch >= 100인 버전에서 versionCode가 충돌하지 않는다", async () => {
|
|
176
213
|
mockFsxExists.mockResolvedValue(true);
|
|
177
214
|
mockFsxGlob.mockResolvedValue(["C:/Program Files/Amazon Corretto/jdk21.0.1"]);
|
|
178
|
-
mockFsxRead.mockImplementation((p: string) => {
|
|
215
|
+
mockFsxRead.mockImplementation(((p: string) => {
|
|
179
216
|
if (p.includes("gradle.properties")) return "";
|
|
180
217
|
if (p.includes("local.properties")) return "";
|
|
181
218
|
if (p.includes("AndroidManifest.xml")) {
|
|
@@ -189,7 +226,7 @@ describe("configureAndroid", () => {
|
|
|
189
226
|
return '<resources><style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen"></style></resources>';
|
|
190
227
|
}
|
|
191
228
|
return "";
|
|
192
|
-
});
|
|
229
|
+
}) as never);
|
|
193
230
|
|
|
194
231
|
const { configureAndroid } = await import("../../src/capacitor/capacitor-android.js");
|
|
195
232
|
|
|
@@ -1,59 +1,11 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-properties -- 테스트 환경변수 조작 필요 */
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
-
import {
|
|
2
|
+
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fsx, cpx } from "@simplysm/core-node";
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// fsx mock
|
|
8
|
-
const mockFsxExists = vi.fn();
|
|
9
|
-
const mockFsxRead = vi.fn();
|
|
10
|
-
const mockFsxWrite = vi.fn().mockResolvedValue(undefined);
|
|
11
|
-
const mockFsxReadJson = vi.fn();
|
|
12
|
-
const mockFsxWriteJson = vi.fn().mockResolvedValue(undefined);
|
|
13
|
-
const mockFsxMkdir = vi.fn().mockResolvedValue(undefined);
|
|
14
|
-
const mockFsxRm = vi.fn().mockResolvedValue(undefined);
|
|
15
|
-
const mockFsxGlob = vi.fn().mockResolvedValue([]);
|
|
16
|
-
const mockFsxCopy = vi.fn().mockResolvedValue(undefined);
|
|
17
|
-
|
|
18
|
-
vi.mock("@simplysm/core-node", async (importOriginal) => {
|
|
19
|
-
const original = await importOriginal<typeof import("@simplysm/core-node")>();
|
|
20
|
-
return {
|
|
21
|
-
...original,
|
|
22
|
-
fsx: {
|
|
23
|
-
exists: mockFsxExists,
|
|
24
|
-
read: mockFsxRead,
|
|
25
|
-
write: mockFsxWrite,
|
|
26
|
-
readJson: mockFsxReadJson,
|
|
27
|
-
writeJson: mockFsxWriteJson,
|
|
28
|
-
mkdir: mockFsxMkdir,
|
|
29
|
-
rm: mockFsxRm,
|
|
30
|
-
glob: mockFsxGlob,
|
|
31
|
-
copy: mockFsxCopy,
|
|
32
|
-
},
|
|
33
|
-
cpx: {
|
|
34
|
-
spawn: mockCpxSpawn,
|
|
35
|
-
spawnSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// cpx mock (was execa)
|
|
41
|
-
const execaCalls: { command: string; args: string[] }[] = [];
|
|
42
|
-
const mockCpxSpawn = vi.fn((...args: unknown[]) => {
|
|
43
|
-
execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
|
|
44
|
-
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const mockFsWriteFile = vi.fn().mockResolvedValue(undefined);
|
|
48
|
-
vi.mock("node:fs", () => ({
|
|
49
|
-
default: { promises: { writeFile: (...args: unknown[]) => mockFsWriteFile(...args) } },
|
|
50
|
-
existsSync: (p: string) => {
|
|
51
|
-
if (p.includes("pnpm-workspace.yaml")) return true;
|
|
52
|
-
return false;
|
|
53
|
-
},
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
// sharp mock
|
|
8
|
+
// sharp는 외부 npm 네이티브 라이브러리로 이미지 처리 시간이 크고 결정적 검증에 mock 필요
|
|
57
9
|
vi.mock("sharp", () => ({
|
|
58
10
|
default: vi.fn().mockReturnValue({
|
|
59
11
|
resize: vi.fn().mockReturnThis(),
|
|
@@ -64,33 +16,37 @@ vi.mock("sharp", () => ({
|
|
|
64
16
|
}),
|
|
65
17
|
}));
|
|
66
18
|
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
vi.spyOn(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
19
|
+
const mockFsxExists = vi.spyOn(fsx, "exists");
|
|
20
|
+
const mockFsxRead = vi.spyOn(fsx, "read");
|
|
21
|
+
vi.spyOn(fsx, "write").mockResolvedValue(undefined);
|
|
22
|
+
const mockFsxReadJson = vi.spyOn(fsx, "readJson");
|
|
23
|
+
vi.spyOn(fsx, "writeJson").mockResolvedValue(undefined);
|
|
24
|
+
vi.spyOn(fsx, "mkdir").mockResolvedValue(undefined);
|
|
25
|
+
vi.spyOn(fsx, "rm").mockResolvedValue(undefined);
|
|
26
|
+
const mockFsxGlob = vi.spyOn(fsx, "glob").mockResolvedValue([]);
|
|
27
|
+
vi.spyOn(fsx, "copy").mockResolvedValue(undefined);
|
|
28
|
+
|
|
29
|
+
vi.spyOn(cpx, "spawn").mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
30
|
+
vi.spyOn(cpx, "spawnSync").mockReturnValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
31
|
+
|
|
32
|
+
let tmpRoot: string;
|
|
33
|
+
let PKG_PATH: string;
|
|
34
|
+
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
tmpRoot = mkdtempSync(path.join(tmpdir(), "cap-build-"));
|
|
37
|
+
writeFileSync(path.join(tmpRoot, "pnpm-workspace.yaml"), "");
|
|
38
|
+
PKG_PATH = path.join(tmpRoot, "pkg");
|
|
39
|
+
mkdirSync(path.join(PKG_PATH, ".capacitor"), { recursive: true });
|
|
40
|
+
});
|
|
83
41
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
}
|
|
42
|
+
afterAll(() => {
|
|
43
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
44
|
+
});
|
|
89
45
|
|
|
90
46
|
function setupDefaultMocks() {
|
|
91
47
|
mockFsxExists.mockResolvedValue(true);
|
|
92
48
|
|
|
93
|
-
mockFsxReadJson.mockImplementation((p: string) => {
|
|
49
|
+
mockFsxReadJson.mockImplementation(((p: string) => {
|
|
94
50
|
const normalized = p.replace(/\\/g, "/");
|
|
95
51
|
if (normalized.includes(".capacitor/package.json")) {
|
|
96
52
|
return {
|
|
@@ -108,9 +64,9 @@ function setupDefaultMocks() {
|
|
|
108
64
|
};
|
|
109
65
|
}
|
|
110
66
|
return { name: "test-pkg", version: "1.2.3" };
|
|
111
|
-
});
|
|
67
|
+
}) as never);
|
|
112
68
|
|
|
113
|
-
mockFsxRead.mockImplementation((p: string) => {
|
|
69
|
+
mockFsxRead.mockImplementation(((p: string) => {
|
|
114
70
|
if (p.includes("AndroidManifest.xml")) {
|
|
115
71
|
return '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n<application>\n</application>\n</manifest>';
|
|
116
72
|
}
|
|
@@ -122,41 +78,25 @@ function setupDefaultMocks() {
|
|
|
122
78
|
minSdkVersion rootProject.ext.minSdkVersion
|
|
123
79
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
124
80
|
}
|
|
125
|
-
buildTypes {
|
|
126
|
-
release {
|
|
127
|
-
}
|
|
128
|
-
}
|
|
81
|
+
buildTypes { release { } }
|
|
129
82
|
}`;
|
|
130
83
|
}
|
|
131
84
|
if (p.includes("gradle.properties")) {
|
|
132
85
|
return "org.gradle.jvmargs=-Xmx2048m";
|
|
133
86
|
}
|
|
134
87
|
return "";
|
|
135
|
-
});
|
|
88
|
+
}) as never);
|
|
136
89
|
|
|
137
|
-
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
90
|
+
mockFsxGlob.mockImplementation(((pattern: string) => {
|
|
138
91
|
if (pattern.includes("Corretto") || pattern.includes("jdk")) {
|
|
139
92
|
return ["C:/Program Files/Amazon Corretto/jdk21.0.1"];
|
|
140
93
|
}
|
|
141
|
-
// Gradle 빌드 출력 파일 검색
|
|
142
|
-
if (pattern.includes("app-release.apk") || pattern.includes("app-*.apk")) {
|
|
143
|
-
return ["/fake/pkg/.capacitor/android/app/build/outputs/apk/release/app-release-unsigned.apk"];
|
|
144
|
-
}
|
|
145
|
-
if (pattern.includes("app-release.aab") || pattern.includes("app-*.aab")) {
|
|
146
|
-
return ["/fake/pkg/.capacitor/android/app/build/outputs/bundle/release/app-release.aab"];
|
|
147
|
-
}
|
|
148
94
|
return [];
|
|
149
|
-
});
|
|
95
|
+
}) as never);
|
|
150
96
|
|
|
151
97
|
process.env["ANDROID_HOME"] = "C:/Android/Sdk";
|
|
152
|
-
|
|
153
|
-
execaCalls.length = 0;
|
|
154
|
-
mockFsWriteFile.mockReset();
|
|
155
|
-
mockFsWriteFile.mockResolvedValue(undefined);
|
|
156
98
|
}
|
|
157
99
|
|
|
158
|
-
//#endregion
|
|
159
|
-
|
|
160
100
|
let savedEnv: Record<string, string | undefined>;
|
|
161
101
|
beforeEach(() => {
|
|
162
102
|
savedEnv = { ...process.env };
|
|
@@ -171,221 +111,15 @@ describe("Capacitor 빌드", () => {
|
|
|
171
111
|
setupDefaultMocks();
|
|
172
112
|
});
|
|
173
113
|
|
|
174
|
-
describe("인수 테스트", () => {
|
|
175
|
-
it("AAB 번들 빌드: bundle=true일 때 Gradle bundleRelease task를 실행한다", async () => {
|
|
176
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
177
|
-
|
|
178
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
179
|
-
appId: "com.test.app",
|
|
180
|
-
appName: "Test App",
|
|
181
|
-
platform: { android: { bundle: true } },
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
await cap.build("/fake/out");
|
|
185
|
-
|
|
186
|
-
const gradleCmd = findGradleCall(execaCalls);
|
|
187
|
-
expect(gradleCmd).toBeDefined();
|
|
188
|
-
expect(gradleCmd!.args).toContain("bundleRelease");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("APK release 빌드: bundle=false일 때 Gradle assembleRelease task를 실행한다", async () => {
|
|
192
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
193
|
-
|
|
194
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
195
|
-
appId: "com.test.app",
|
|
196
|
-
appName: "Test App",
|
|
197
|
-
platform: { android: {} },
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
await cap.build("/fake/out");
|
|
201
|
-
|
|
202
|
-
const gradleCmd = findGradleCall(execaCalls);
|
|
203
|
-
expect(gradleCmd).toBeDefined();
|
|
204
|
-
expect(gradleCmd!.args).toContain("assembleRelease");
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("APK debug 빌드: debug=true일 때 Gradle assembleDebug task를 실행한다", async () => {
|
|
208
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
209
|
-
|
|
210
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
211
|
-
appId: "com.test.app",
|
|
212
|
-
appName: "Test App",
|
|
213
|
-
debug: true,
|
|
214
|
-
platform: { android: {} },
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
await cap.build("/fake/out");
|
|
218
|
-
|
|
219
|
-
const gradleCmd = findGradleCall(execaCalls);
|
|
220
|
-
expect(gradleCmd).toBeDefined();
|
|
221
|
-
expect(gradleCmd!.args).toContain("assembleDebug");
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("서명 설정이 없으면 unsigned 빌드를 생성하고 경고한다", async () => {
|
|
225
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
226
|
-
|
|
227
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
228
|
-
appId: "com.test.app",
|
|
229
|
-
appName: "Test App",
|
|
230
|
-
platform: { android: {} },
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
await cap.build("/fake/out");
|
|
234
|
-
|
|
235
|
-
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
|
236
|
-
expect.stringContaining("서명"),
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("빌드 산출물을 outPath/android/ 경로에 복사한다", async () => {
|
|
241
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
242
|
-
|
|
243
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
244
|
-
appId: "com.test.app",
|
|
245
|
-
appName: "Test App",
|
|
246
|
-
platform: { android: {} },
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
await cap.build("/fake/out");
|
|
250
|
-
|
|
251
|
-
// outPath/android/ 디렉토리 생성 확인
|
|
252
|
-
const mkdirCalls = mockFsxMkdir.mock.calls.map((c) =>
|
|
253
|
-
(c[0] as string).replace(/\\/g, "/"),
|
|
254
|
-
);
|
|
255
|
-
const androidDir = mkdirCalls.find((p) => p.includes("/fake/out/android"));
|
|
256
|
-
expect(androidDir).toBeDefined();
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe("단위", () => {
|
|
261
|
-
it("build 전에 cap copy로 웹 에셋을 네이티브 프로젝트에 동기화한다", async () => {
|
|
262
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
263
|
-
|
|
264
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
265
|
-
appId: "com.test.app",
|
|
266
|
-
appName: "Test App",
|
|
267
|
-
platform: { android: {} },
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
await cap.build("/fake/out");
|
|
271
|
-
|
|
272
|
-
// cap copy가 gradlew보다 먼저 실행되는지 확인
|
|
273
|
-
const capCopyIndex = execaCalls.findIndex(
|
|
274
|
-
(c) => c.command === "pnpm" && c.args.includes("cap") && c.args.includes("copy"),
|
|
275
|
-
);
|
|
276
|
-
const gradlewIndex = findGradleCallIndex(execaCalls);
|
|
277
|
-
expect(capCopyIndex).toBeGreaterThanOrEqual(0);
|
|
278
|
-
expect(gradlewIndex).toBeGreaterThan(capCopyIndex);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("Windows에서 cmd /c gradlew.bat으로 Gradle을 실행한다", async () => {
|
|
282
|
-
const originalPlatform = process.platform;
|
|
283
|
-
Object.defineProperty(process, "platform", { value: "win32" });
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
287
|
-
|
|
288
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
289
|
-
appId: "com.test.app",
|
|
290
|
-
appName: "Test App",
|
|
291
|
-
platform: { android: {} },
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
await cap.build("/fake/out");
|
|
295
|
-
|
|
296
|
-
const gradleCmd = execaCalls.find((c) => c.command === "cmd");
|
|
297
|
-
expect(gradleCmd).toBeDefined();
|
|
298
|
-
expect(gradleCmd!.args[0]).toBe("/c");
|
|
299
|
-
expect(gradleCmd!.args[1]).toBe("gradlew.bat");
|
|
300
|
-
expect(gradleCmd!.args).toContain("assembleRelease");
|
|
301
|
-
expect(gradleCmd!.args).toContain("--no-daemon");
|
|
302
|
-
} finally {
|
|
303
|
-
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("Linux/macOS에서 gradlew를 직접 실행한다", async () => {
|
|
308
|
-
const originalPlatform = process.platform;
|
|
309
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
313
|
-
|
|
314
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
315
|
-
appId: "com.test.app",
|
|
316
|
-
appName: "Test App",
|
|
317
|
-
platform: { android: {} },
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
await cap.build("/fake/out");
|
|
321
|
-
|
|
322
|
-
const gradleCmd = findGradleCall(execaCalls);
|
|
323
|
-
expect(gradleCmd).toBeDefined();
|
|
324
|
-
expect(gradleCmd!.command).toContain("gradlew");
|
|
325
|
-
expect(gradleCmd!.command).not.toContain("gradlew.bat");
|
|
326
|
-
expect(gradleCmd!.args).toContain("assembleRelease");
|
|
327
|
-
} finally {
|
|
328
|
-
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
114
|
describe("서명", () => {
|
|
334
|
-
it("서명 설정이 있으면 build.gradle에 signingConfigs를 추가한다", async () => {
|
|
335
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
336
|
-
|
|
337
|
-
// keystore 파일 존재하도록 설정
|
|
338
|
-
mockFsxExists.mockResolvedValue(true);
|
|
339
|
-
|
|
340
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
341
|
-
appId: "com.test.app",
|
|
342
|
-
appName: "Test App",
|
|
343
|
-
platform: {
|
|
344
|
-
android: {
|
|
345
|
-
sign: {
|
|
346
|
-
keystore: "my-release.keystore",
|
|
347
|
-
storePassword: "store123",
|
|
348
|
-
alias: "my-key",
|
|
349
|
-
password: "key123",
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
await cap.build("/fake/out");
|
|
356
|
-
|
|
357
|
-
// build.gradle에 signingConfigs가 쓰여졌는지 확인
|
|
358
|
-
const writeCalls = mockFsxWrite.mock.calls;
|
|
359
|
-
const gradleWrite = writeCalls.find(
|
|
360
|
-
(call) =>
|
|
361
|
-
typeof call[0] === "string" &&
|
|
362
|
-
call[0].includes("build.gradle") &&
|
|
363
|
-
typeof call[1] === "string" &&
|
|
364
|
-
call[1].includes("signingConfigs"),
|
|
365
|
-
);
|
|
366
|
-
expect(gradleWrite).toBeDefined();
|
|
367
|
-
|
|
368
|
-
// build.gradle에 비밀번호가 포함되어 있는지 확인 (.capacitor는 gitignore 대상)
|
|
369
|
-
const gradleContent = gradleWrite![1] as string;
|
|
370
|
-
expect(gradleContent).toContain("storePassword 'store123'");
|
|
371
|
-
expect(gradleContent).toContain("keyPassword 'key123'");
|
|
372
|
-
expect(gradleContent).toContain("keyAlias 'my-key'");
|
|
373
|
-
|
|
374
|
-
// unsigned 경고가 출력되지 않아야 한다
|
|
375
|
-
const unsignedWarn = mockLoggerWarn.mock.calls.find(
|
|
376
|
-
(call) => typeof call[0] === "string" && call[0].includes("서명"),
|
|
377
|
-
);
|
|
378
|
-
expect(unsignedWarn).toBeUndefined();
|
|
379
|
-
});
|
|
380
|
-
|
|
381
115
|
it("keystore 파일이 없으면 에러가 발생한다", async () => {
|
|
382
116
|
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
383
117
|
|
|
384
118
|
// keystore 파일만 존재하지 않도록 설정
|
|
385
|
-
mockFsxExists.mockImplementation((p: string) => {
|
|
119
|
+
mockFsxExists.mockImplementation(((p: string) => {
|
|
386
120
|
if (p.includes("my-release.keystore")) return false;
|
|
387
121
|
return true;
|
|
388
|
-
});
|
|
122
|
+
}) as never);
|
|
389
123
|
|
|
390
124
|
const cap = await Capacitor.create(PKG_PATH, {
|
|
391
125
|
appId: "com.test.app",
|
|
@@ -404,84 +138,5 @@ describe("Capacitor 빌드", () => {
|
|
|
404
138
|
|
|
405
139
|
await expect(cap.build("/fake/out")).rejects.toThrow("keystore");
|
|
406
140
|
});
|
|
407
|
-
|
|
408
|
-
it("비밀번호에 $, \\, ' 등 특수문자가 있으면 Groovy 이스케이프하여 build.gradle에 기록한다", async () => {
|
|
409
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
410
|
-
mockFsxExists.mockResolvedValue(true);
|
|
411
|
-
|
|
412
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
413
|
-
appId: "com.test.app",
|
|
414
|
-
appName: "Test App",
|
|
415
|
-
platform: {
|
|
416
|
-
android: {
|
|
417
|
-
sign: {
|
|
418
|
-
keystore: "my-release.keystore",
|
|
419
|
-
storePassword: "12tlavmf#$",
|
|
420
|
-
alias: "my-key",
|
|
421
|
-
password: "pass\\'word",
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
await cap.build("/fake/out");
|
|
428
|
-
|
|
429
|
-
const writeCalls = mockFsxWrite.mock.calls;
|
|
430
|
-
const gradleWrite = writeCalls.find(
|
|
431
|
-
(call) =>
|
|
432
|
-
typeof call[0] === "string" &&
|
|
433
|
-
call[0].includes("build.gradle") &&
|
|
434
|
-
typeof call[1] === "string" &&
|
|
435
|
-
call[1].includes("signingConfigs"),
|
|
436
|
-
);
|
|
437
|
-
expect(gradleWrite).toBeDefined();
|
|
438
|
-
|
|
439
|
-
const gradleContent = gradleWrite![1] as string;
|
|
440
|
-
// $ 는 Groovy single-quoted string에서 그대로 유지
|
|
441
|
-
expect(gradleContent).toContain("storePassword '12tlavmf#$'");
|
|
442
|
-
// \ → \\, ' → \' 이스케이프
|
|
443
|
-
expect(gradleContent).toContain("keyPassword 'pass\\\\\\'word'");
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it("signed 빌드 산출물이 unsigned 접미사 없이 복사된다", async () => {
|
|
447
|
-
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
448
|
-
|
|
449
|
-
// signed 빌드 출력 설정
|
|
450
|
-
mockFsxGlob.mockImplementation((pattern: string) => {
|
|
451
|
-
if (pattern.includes("Corretto")) return ["C:/Program Files/Amazon Corretto/jdk21.0.1"];
|
|
452
|
-
if (pattern.includes("app-")) {
|
|
453
|
-
return ["/fake/pkg/.capacitor/android/app/build/outputs/apk/release/app-release.apk"];
|
|
454
|
-
}
|
|
455
|
-
return [];
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
const cap = await Capacitor.create(PKG_PATH, {
|
|
459
|
-
appId: "com.test.app",
|
|
460
|
-
appName: "Test App",
|
|
461
|
-
platform: {
|
|
462
|
-
android: {
|
|
463
|
-
sign: {
|
|
464
|
-
keystore: "my-release.keystore",
|
|
465
|
-
storePassword: "store123",
|
|
466
|
-
alias: "my-key",
|
|
467
|
-
password: "key123",
|
|
468
|
-
},
|
|
469
|
-
},
|
|
470
|
-
},
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
await cap.build("/fake/out");
|
|
474
|
-
|
|
475
|
-
// latest 파일명에 unsigned가 없는지 확인
|
|
476
|
-
const copyCalls = mockFsxCopy.mock.calls.map((c) => [
|
|
477
|
-
(c[0] as string).replace(/\\/g, "/"),
|
|
478
|
-
(c[1] as string).replace(/\\/g, "/"),
|
|
479
|
-
]);
|
|
480
|
-
const latestCopy = copyCalls.find(
|
|
481
|
-
(c) => c[1].includes("latest"),
|
|
482
|
-
);
|
|
483
|
-
expect(latestCopy).toBeDefined();
|
|
484
|
-
expect(latestCopy![1]).not.toContain("unsigned");
|
|
485
|
-
});
|
|
486
141
|
});
|
|
487
142
|
});
|
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { fsx } from "@simplysm/core-node";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const mockFsxWrite = vi.fn().mockResolvedValue(undefined);
|
|
6
|
-
const mockFsxRead = vi.fn();
|
|
7
|
-
|
|
8
|
-
vi.mock("@simplysm/core-node", async (importOriginal) => {
|
|
9
|
-
const original = await importOriginal<typeof import("@simplysm/core-node")>();
|
|
10
|
-
return {
|
|
11
|
-
...original,
|
|
12
|
-
fsx: {
|
|
13
|
-
write: mockFsxWrite,
|
|
14
|
-
read: mockFsxRead,
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
//#endregion
|
|
4
|
+
const mockFsxWrite = vi.spyOn(fsx, "write").mockResolvedValue(undefined);
|
|
5
|
+
const mockFsxRead = vi.spyOn(fsx, "read");
|
|
20
6
|
|
|
21
7
|
const CAP_PATH = "/fake/.capacitor";
|
|
22
8
|
|
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { fsx } from "@simplysm/core-node";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const mockFsxWrite = vi.fn().mockResolvedValue(undefined);
|
|
6
|
-
const mockFsxRead = vi.fn();
|
|
7
|
-
|
|
8
|
-
vi.mock("@simplysm/core-node", async (importOriginal) => {
|
|
9
|
-
const original = await importOriginal<typeof import("@simplysm/core-node")>();
|
|
10
|
-
return {
|
|
11
|
-
...original,
|
|
12
|
-
fsx: {
|
|
13
|
-
write: mockFsxWrite,
|
|
14
|
-
read: mockFsxRead,
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
//#endregion
|
|
4
|
+
const mockFsxWrite = vi.spyOn(fsx, "write").mockResolvedValue(undefined);
|
|
5
|
+
const mockFsxRead = vi.spyOn(fsx, "read");
|
|
20
6
|
|
|
21
7
|
const CAP_PATH = "/fake/.capacitor";
|
|
22
8
|
|