@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.
Files changed (79) hide show
  1. package/dist/capacitor/capacitor-android.d.ts +2 -0
  2. package/dist/capacitor/capacitor-android.d.ts.map +1 -1
  3. package/dist/capacitor/capacitor-android.js +13 -0
  4. package/dist/capacitor/capacitor-android.js.map +1 -1
  5. package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
  6. package/dist/capacitor/capacitor-npm-config.js +2 -6
  7. package/dist/capacitor/capacitor-npm-config.js.map +1 -1
  8. package/dist/electron/electron.d.ts.map +1 -1
  9. package/dist/electron/electron.js +1 -2
  10. package/dist/electron/electron.js.map +1 -1
  11. package/package.json +8 -8
  12. package/src/capacitor/capacitor-android.ts +14 -0
  13. package/src/capacitor/capacitor-npm-config.ts +2 -6
  14. package/src/electron/electron.ts +1 -2
  15. package/tests/angular/ngtsc-build-core.acc.spec.ts +36 -94
  16. package/tests/capacitor/capacitor-android.spec.ts +65 -28
  17. package/tests/capacitor/capacitor-build.spec.ts +40 -385
  18. package/tests/capacitor/capacitor-config-writer.acc.spec.ts +3 -17
  19. package/tests/capacitor/capacitor-config-writer.spec.ts +3 -17
  20. package/tests/capacitor/capacitor-init.spec.ts +40 -636
  21. package/tests/capacitor/capacitor-npm-config.acc.spec.ts +38 -168
  22. package/tests/capacitor/capacitor-npm-config.spec.ts +33 -71
  23. package/tests/commands/check.spec.ts +25 -36
  24. package/tests/commands/deployment-phase.acc.spec.ts +17 -26
  25. package/tests/commands/git-phase.acc.spec.ts +13 -112
  26. package/tests/commands/lint.spec.ts +7 -24
  27. package/tests/commands/post-publish-phase.acc.spec.ts +5 -10
  28. package/tests/commands/typecheck.spec.ts +43 -65
  29. package/tests/electron/electron.spec.ts +22 -46
  30. package/tests/engines/base-engine.spec.ts +4 -13
  31. package/tests/engines/engine-selection.spec.ts +14 -17
  32. package/tests/engines/engine-typecheck-selection.acc.spec.ts +13 -16
  33. package/tests/engines/esbuild-client-engine.acc.spec.ts +36 -40
  34. package/tests/engines/esbuild-client-engine.spec.ts +4 -23
  35. package/tests/engines/ngtsc-engine.spec.ts +3 -10
  36. package/tests/engines/server-esbuild-engine.spec.ts +3 -10
  37. package/tests/engines/tsc-engine.spec.ts +3 -10
  38. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +3 -8
  39. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +3 -8
  40. package/tests/orchestrators/build-orchestrator.spec.ts +57 -102
  41. package/tests/orchestrators/dev-orchestrator.spec.ts +68 -109
  42. package/tests/orchestrators/typecheck-orchestrator.spec.ts +25 -57
  43. package/tests/orchestrators/watch-orchestrator.spec.ts +73 -99
  44. package/tests/sd-cli-entry.spec.ts +17 -20
  45. package/tests/utils/angular-source-file-cache.spec.ts +4 -8
  46. package/tests/utils/copy-src.spec.ts +9 -20
  47. package/tests/utils/esbuild-client-config.acc.spec.ts +9 -15
  48. package/tests/utils/esbuild-client-config.spec.ts +12 -24
  49. package/tests/utils/esbuild-config.spec.ts +51 -42
  50. package/tests/utils/lint-core.spec.ts +13 -19
  51. package/tests/utils/lint-utils.spec.ts +8 -15
  52. package/tests/utils/lint-with-program.spec.ts +3 -7
  53. package/tests/utils/ngtsc-build-core.spec.ts +2 -99
  54. package/tests/utils/orchestrator-utils.spec.ts +7 -20
  55. package/tests/utils/output-utils.spec.ts +5 -11
  56. package/tests/utils/sd-config.spec.ts +4 -12
  57. package/tests/utils/typecheck-env.spec.ts +49 -77
  58. package/tests/utils/typecheck-non-package.spec.ts +23 -16
  59. package/tests/workers/build-watch-paths.acc.spec.ts +4 -10
  60. package/tests/workers/build-watch-paths.spec.ts +4 -9
  61. package/tests/workers/client-worker.acc.spec.ts +64 -137
  62. package/tests/workers/client-worker.spec.ts +63 -89
  63. package/tests/workers/library-build-lint.spec.ts +19 -30
  64. package/tests/workers/library-build-worker.spec.ts +28 -55
  65. package/tests/workers/server-esbuild-context.acc.spec.ts +6 -15
  66. package/tests/workers/server-esbuild-context.spec.ts +7 -16
  67. package/tests/workers/server-runtime-worker.spec.ts +8 -10
  68. package/tests/workers/shared-worker-lifecycle.acc.spec.ts +3 -5
  69. package/tests/workers/shared-worker-lifecycle.spec.ts +4 -5
  70. package/tests/capacitor/capacitor-icon.spec.ts +0 -285
  71. package/tests/capacitor/capacitor-run.spec.ts +0 -256
  72. package/tests/capacitor/capacitor-workspace.spec.ts +0 -203
  73. package/tests/commands/device.spec.ts +0 -237
  74. package/tests/commands/publish.spec.ts +0 -1183
  75. package/tests/utils/external-modules.spec.ts +0 -217
  76. package/tests/workers/server-build-lint.spec.ts +0 -201
  77. package/tests/workers/server-build-worker.spec.ts +0 -765
  78. package/tests/workers/server-watch-manager.acc.spec.ts +0 -162
  79. 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
- //#region Mocks
5
-
6
- const mockFsxExists = vi.fn();
7
- const mockFsxRead = vi.fn();
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 { consola } from "consola";
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
- //#region Mocks
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
- // consola mock (logger assertion 필요)
68
- const mockLoggerWarn = vi.fn();
69
- vi.spyOn(consola, "withTag").mockReturnValue({ debug: vi.fn(), warn: mockLoggerWarn, error: vi.fn(), info: vi.fn(), success: vi.fn(), start: vi.fn() } as any);
70
-
71
- //#endregion
72
-
73
- //#region Helpers
74
-
75
- const PKG_PATH = "/fake/pkg";
76
-
77
- /** Gradle 실행 명령을 찾는다 (Windows: cmd /c gradlew.bat, 외: gradlew) */
78
- function findGradleCall(calls: { command: string; args: string[] }[]) {
79
- return calls.find(
80
- (c) => c.command.includes("gradlew") || (c.command === "cmd" && c.args.includes("gradlew.bat")),
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
- function findGradleCallIndex(calls: { command: string; args: string[] }[]) {
85
- return calls.findIndex(
86
- (c) => c.command.includes("gradlew") || (c.command === "cmd" && c.args.includes("gradlew.bat")),
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
- //#region Mocks
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
- //#region Mocks
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