@simplysm/sd-cli 13.0.0-beta.45 → 13.0.0-beta.47
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/README.md +3 -3
- package/dist/builders/BaseBuilder.js.map +0 -1
- package/dist/builders/DtsBuilder.js.map +0 -1
- package/dist/builders/LibraryBuilder.js.map +0 -1
- package/dist/builders/index.js.map +0 -1
- package/dist/builders/types.js.map +0 -1
- package/dist/capacitor/capacitor.js.map +0 -1
- package/dist/commands/add-client.js.map +0 -1
- package/dist/commands/add-server.js.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/device.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/lint.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/typecheck.js.map +0 -1
- package/dist/commands/watch.js.map +0 -1
- package/dist/electron/electron.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infra/ResultCollector.js.map +0 -1
- package/dist/infra/SignalHandler.js.map +0 -1
- package/dist/infra/WorkerManager.js.map +0 -1
- package/dist/infra/index.js.map +0 -1
- package/dist/orchestrators/WatchOrchestrator.js.map +0 -1
- package/dist/orchestrators/index.js.map +0 -1
- package/dist/sd-cli.js.map +0 -1
- package/dist/sd-config.types.js.map +0 -1
- package/dist/utils/build-env.js.map +0 -1
- package/dist/utils/config-editor.js.map +0 -1
- package/dist/utils/copy-src.js.map +0 -1
- package/dist/utils/esbuild-config.d.ts +1 -0
- package/dist/utils/esbuild-config.d.ts.map +1 -1
- package/dist/utils/esbuild-config.js +13 -2
- package/dist/utils/esbuild-config.js.map +1 -2
- package/dist/utils/listr-manager.js.map +0 -1
- package/dist/utils/output-utils.js.map +0 -1
- package/dist/utils/package-utils.js.map +0 -1
- package/dist/utils/replace-deps.js.map +0 -1
- package/dist/utils/sd-config.js.map +0 -1
- package/dist/utils/spawn.js.map +0 -1
- package/dist/utils/tailwind-config-deps.js.map +0 -1
- package/dist/utils/template.js.map +0 -1
- package/dist/utils/tsconfig.js.map +0 -1
- package/dist/utils/typecheck-serialization.js.map +0 -1
- package/dist/utils/vite-config.js.map +0 -1
- package/dist/utils/worker-events.js.map +0 -1
- package/dist/workers/client.worker.js.map +0 -1
- package/dist/workers/dts.worker.js.map +0 -1
- package/dist/workers/library.worker.js.map +0 -1
- package/dist/workers/server-runtime.worker.js.map +0 -1
- package/dist/workers/server.worker.js.map +0 -1
- package/package.json +6 -4
- package/src/builders/BaseBuilder.ts +141 -0
- package/src/builders/DtsBuilder.ts +138 -0
- package/src/builders/LibraryBuilder.ts +161 -0
- package/src/builders/index.ts +4 -0
- package/src/builders/types.ts +55 -0
- package/src/capacitor/capacitor.ts +827 -0
- package/src/commands/add-client.ts +135 -0
- package/src/commands/add-server.ts +150 -0
- package/src/commands/build.ts +475 -0
- package/src/commands/dev.ts +602 -0
- package/src/commands/device.ts +151 -0
- package/src/commands/init.ts +104 -0
- package/src/commands/lint.ts +216 -0
- package/src/commands/publish.ts +836 -0
- package/src/commands/typecheck.ts +329 -0
- package/src/commands/watch.ts +38 -0
- package/src/electron/electron.ts +329 -0
- package/src/index.ts +1 -0
- package/src/infra/ResultCollector.ts +81 -0
- package/src/infra/SignalHandler.ts +52 -0
- package/src/infra/WorkerManager.ts +65 -0
- package/src/infra/index.ts +3 -0
- package/src/orchestrators/WatchOrchestrator.ts +211 -0
- package/src/orchestrators/index.ts +1 -0
- package/src/sd-cli.ts +307 -0
- package/src/sd-config.types.ts +271 -0
- package/src/utils/build-env.ts +12 -0
- package/src/utils/config-editor.ts +131 -0
- package/src/utils/copy-src.ts +60 -0
- package/src/utils/esbuild-config.ts +263 -0
- package/src/utils/listr-manager.ts +89 -0
- package/src/utils/output-utils.ts +61 -0
- package/src/utils/package-utils.ts +63 -0
- package/src/utils/replace-deps.ts +163 -0
- package/src/utils/sd-config.ts +44 -0
- package/src/utils/spawn.ts +79 -0
- package/src/utils/tailwind-config-deps.ts +95 -0
- package/src/utils/template.ts +51 -0
- package/src/utils/tsconfig.ts +111 -0
- package/src/utils/typecheck-serialization.ts +82 -0
- package/src/utils/vite-config.ts +184 -0
- package/src/utils/worker-events.ts +102 -0
- package/src/workers/client.worker.ts +236 -0
- package/src/workers/dts.worker.ts +416 -0
- package/src/workers/library.worker.ts +245 -0
- package/src/workers/server-runtime.worker.ts +154 -0
- package/src/workers/server.worker.ts +435 -0
- package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
- package/templates/add-server/__SERVER__/package.json.hbs +2 -2
- package/templates/init/package.json.hbs +3 -3
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fsExists, fsMkdir, fsRead, fsReadJson, fsWrite, fsWriteJson, fsGlob, fsCopy, fsRm } from "@simplysm/core-node";
|
|
3
|
+
import { env } from "@simplysm/core-common";
|
|
4
|
+
import { consola } from "consola";
|
|
5
|
+
import sharp from "sharp";
|
|
6
|
+
import type { SdCapacitorConfig } from "../sd-config.types";
|
|
7
|
+
import { spawn } from "../utils/spawn";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* package.json 타입
|
|
11
|
+
*/
|
|
12
|
+
interface NpmConfig {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
dependencies?: Record<string, string>;
|
|
16
|
+
devDependencies?: Record<string, string>;
|
|
17
|
+
peerDependencies?: Record<string, string>;
|
|
18
|
+
volta?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 설정 검증 에러
|
|
23
|
+
*/
|
|
24
|
+
class CapacitorConfigError extends Error {
|
|
25
|
+
constructor(message: string) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "CapacitorConfigError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Capacitor 프로젝트 관리 클래스
|
|
33
|
+
*
|
|
34
|
+
* - Capacitor 프로젝트 초기화
|
|
35
|
+
* - Android APK/AAB 빌드
|
|
36
|
+
* - 디바이스에서 앱 실행
|
|
37
|
+
*/
|
|
38
|
+
export class Capacitor {
|
|
39
|
+
private static readonly _ANDROID_KEYSTORE_FILE_NAME = "android.keystore";
|
|
40
|
+
private static readonly _LOCK_FILE_NAME = ".capacitor.lock";
|
|
41
|
+
private static readonly _logger = consola.withTag("sd:cli:capacitor");
|
|
42
|
+
|
|
43
|
+
private readonly _capPath: string;
|
|
44
|
+
private readonly _platforms: string[];
|
|
45
|
+
private readonly _npmConfig: NpmConfig;
|
|
46
|
+
|
|
47
|
+
private constructor(
|
|
48
|
+
private readonly _pkgPath: string,
|
|
49
|
+
private readonly _config: SdCapacitorConfig,
|
|
50
|
+
npmConfig: NpmConfig,
|
|
51
|
+
) {
|
|
52
|
+
this._platforms = Object.keys(this._config.platform ?? {});
|
|
53
|
+
this._npmConfig = npmConfig;
|
|
54
|
+
this._capPath = path.resolve(this._pkgPath, ".capacitor");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Capacitor 인스턴스 생성 (설정 검증 포함)
|
|
59
|
+
*/
|
|
60
|
+
static async create(pkgPath: string, config: SdCapacitorConfig): Promise<Capacitor> {
|
|
61
|
+
// F5: 런타임 설정 검증
|
|
62
|
+
Capacitor._validateConfig(config);
|
|
63
|
+
|
|
64
|
+
const npmConfig = await fsReadJson<NpmConfig>(path.resolve(pkgPath, "package.json"));
|
|
65
|
+
return new Capacitor(pkgPath, config, npmConfig);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* F5: 설정 검증
|
|
70
|
+
*/
|
|
71
|
+
private static _validateConfig(config: SdCapacitorConfig): void {
|
|
72
|
+
if (typeof config.appId !== "string" || config.appId.trim() === "") {
|
|
73
|
+
throw new CapacitorConfigError("capacitor.appId는 필수입니다.");
|
|
74
|
+
}
|
|
75
|
+
if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/i.test(config.appId)) {
|
|
76
|
+
throw new CapacitorConfigError(`capacitor.appId 형식이 올바르지 않습니다: ${config.appId}`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof config.appName !== "string" || config.appName.trim() === "") {
|
|
79
|
+
throw new CapacitorConfigError("capacitor.appName은 필수입니다.");
|
|
80
|
+
}
|
|
81
|
+
if (config.platform != null) {
|
|
82
|
+
const platforms = Object.keys(config.platform);
|
|
83
|
+
for (const p of platforms) {
|
|
84
|
+
if (p !== "android") {
|
|
85
|
+
throw new CapacitorConfigError(`지원하지 않는 플랫폼: ${p} (현재 android만 지원)`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 명령어 실행 (로깅 포함)
|
|
93
|
+
*/
|
|
94
|
+
private async _exec(cmd: string, args: string[], cwd: string): Promise<string> {
|
|
95
|
+
Capacitor._logger.debug(`실행 명령: ${cmd} ${args.join(" ")}`);
|
|
96
|
+
const result = await spawn(cmd, args, { cwd });
|
|
97
|
+
Capacitor._logger.debug(`실행 결과: ${result}`);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* F10: 동시 실행 방지를 위한 잠금 획득
|
|
103
|
+
*/
|
|
104
|
+
private async _acquireLock(): Promise<void> {
|
|
105
|
+
const lockPath = path.resolve(this._capPath, Capacitor._LOCK_FILE_NAME);
|
|
106
|
+
if (await fsExists(lockPath)) {
|
|
107
|
+
const lockContent = await fsRead(lockPath);
|
|
108
|
+
throw new Error(
|
|
109
|
+
`다른 Capacitor 작업이 진행 중입니다 (PID: ${lockContent}). ` + `문제가 있다면 ${lockPath} 파일을 삭제하세요.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
await fsMkdir(this._capPath);
|
|
113
|
+
await fsWrite(lockPath, String(process.pid));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* F10: 잠금 해제
|
|
118
|
+
*/
|
|
119
|
+
private async _releaseLock(): Promise<void> {
|
|
120
|
+
const lockPath = path.resolve(this._capPath, Capacitor._LOCK_FILE_NAME);
|
|
121
|
+
await fsRm(lockPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* F4: 외부 도구 검증
|
|
126
|
+
*/
|
|
127
|
+
private async _validateTools(): Promise<void> {
|
|
128
|
+
// Android SDK 확인
|
|
129
|
+
const sdkPath = await this._findAndroidSdk();
|
|
130
|
+
if (sdkPath == null) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"Android SDK를 찾을 수 없습니다.\n" +
|
|
133
|
+
"1. Android Studio를 설치하거나\n" +
|
|
134
|
+
"2. ANDROID_HOME 또는 ANDROID_SDK_ROOT 환경변수를 설정하세요.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Java 확인 (android 플랫폼일 때만)
|
|
139
|
+
if (this._platforms.includes("android")) {
|
|
140
|
+
const javaPath = await this._findJava21();
|
|
141
|
+
if (javaPath == null) {
|
|
142
|
+
Capacitor._logger.warn("Java 21을 찾을 수 없습니다. Gradle이 내장 JDK를 사용하거나 빌드가 실패할 수 있습니다.");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Capacitor 프로젝트 초기화
|
|
149
|
+
*
|
|
150
|
+
* 1. package.json 생성 및 의존성 설치
|
|
151
|
+
* 2. capacitor.config.ts 생성
|
|
152
|
+
* 3. 플랫폼 추가 (android)
|
|
153
|
+
* 4. 아이콘 설정
|
|
154
|
+
* 5. Android 네이티브 설정
|
|
155
|
+
* 6. cap sync 또는 cap copy 실행
|
|
156
|
+
*/
|
|
157
|
+
async initialize(): Promise<void> {
|
|
158
|
+
await this._acquireLock();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// F4: 외부 도구 검증
|
|
162
|
+
await this._validateTools();
|
|
163
|
+
|
|
164
|
+
// 1. Capacitor 프로젝트 초기화
|
|
165
|
+
const changed = await this._initCap();
|
|
166
|
+
|
|
167
|
+
// 2. Capacitor 설정 파일 생성
|
|
168
|
+
await this._writeCapConf();
|
|
169
|
+
|
|
170
|
+
// 3. 플랫폼 관리 (F12: 멱등성 - 이미 존재하면 스킵)
|
|
171
|
+
await this._addPlatforms();
|
|
172
|
+
|
|
173
|
+
// 4. 아이콘 설정 (F6: 에러 복구)
|
|
174
|
+
await this._setupIcon();
|
|
175
|
+
|
|
176
|
+
// 5. Android 네이티브 설정
|
|
177
|
+
if (this._platforms.includes("android")) {
|
|
178
|
+
await this._configureAndroid();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 6. 웹 자산 동기화
|
|
182
|
+
if (changed) {
|
|
183
|
+
await this._exec("npx", ["cap", "sync"], this._capPath);
|
|
184
|
+
} else {
|
|
185
|
+
await this._exec("npx", ["cap", "copy"], this._capPath);
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
await this._releaseLock();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Android APK/AAB 빌드
|
|
194
|
+
*/
|
|
195
|
+
async build(outPath: string): Promise<void> {
|
|
196
|
+
await this._acquireLock();
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const buildType = this._config.debug ? "debug" : "release";
|
|
200
|
+
|
|
201
|
+
for (const platform of this._platforms) {
|
|
202
|
+
await this._exec("npx", ["cap", "copy", platform], this._capPath);
|
|
203
|
+
|
|
204
|
+
if (platform === "android") {
|
|
205
|
+
await this._buildAndroid(outPath, buildType);
|
|
206
|
+
} else {
|
|
207
|
+
throw new Error(`지원하지 않는 플랫폼: ${platform}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
await this._releaseLock();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 디바이스에서 앱 실행 (WebView를 개발 서버로 연결)
|
|
217
|
+
*/
|
|
218
|
+
async runOnDevice(url?: string): Promise<void> {
|
|
219
|
+
// F11: URL 검증
|
|
220
|
+
if (url != null) {
|
|
221
|
+
this._validateUrl(url);
|
|
222
|
+
await this._updateServerUrl(url);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const platform of this._platforms) {
|
|
226
|
+
await this._exec("npx", ["cap", "copy", platform], this._capPath);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
await this._exec("npx", ["cap", "run", platform], this._capPath);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (platform === "android") {
|
|
232
|
+
try {
|
|
233
|
+
await this._exec("adb", ["kill-server"], this._capPath);
|
|
234
|
+
} catch {
|
|
235
|
+
// adb kill-server 실패는 무시
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* F11: URL 검증
|
|
245
|
+
*/
|
|
246
|
+
private _validateUrl(url: string): void {
|
|
247
|
+
try {
|
|
248
|
+
const parsed = new URL(url);
|
|
249
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
250
|
+
throw new Error(`지원하지 않는 프로토콜: ${parsed.protocol}`);
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof TypeError) {
|
|
254
|
+
throw new Error(`유효하지 않은 URL: ${url}`);
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
//#region Private - 초기화
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Capacitor 프로젝트 기본 초기화 (package.json, npm install, cap init)
|
|
264
|
+
*/
|
|
265
|
+
private async _initCap(): Promise<boolean> {
|
|
266
|
+
const depChanged = await this._setupNpmConf();
|
|
267
|
+
if (!depChanged) return false;
|
|
268
|
+
|
|
269
|
+
// pnpm install
|
|
270
|
+
const installResult = await this._exec("pnpm", ["install"], this._capPath);
|
|
271
|
+
Capacitor._logger.debug(`pnpm install 완료: ${installResult}`);
|
|
272
|
+
|
|
273
|
+
// F12: cap init 멱등성 - capacitor.config.ts가 없을 때만 실행
|
|
274
|
+
const configPath = path.resolve(this._capPath, "capacitor.config.ts");
|
|
275
|
+
if (!(await fsExists(configPath))) {
|
|
276
|
+
await this._exec("npx", ["cap", "init", this._config.appName, this._config.appId], this._capPath);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 기본 www/index.html 생성
|
|
280
|
+
const wwwPath = path.resolve(this._capPath, "www");
|
|
281
|
+
await fsMkdir(wwwPath);
|
|
282
|
+
await fsWrite(path.resolve(wwwPath, "index.html"), "<!DOCTYPE html><html><head></head><body></body></html>");
|
|
283
|
+
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* package.json 설정
|
|
289
|
+
*/
|
|
290
|
+
private async _setupNpmConf(): Promise<boolean> {
|
|
291
|
+
const projNpmConfigPath = path.resolve(this._pkgPath, "../../package.json");
|
|
292
|
+
|
|
293
|
+
// F3: 파일 존재 확인
|
|
294
|
+
if (!(await fsExists(projNpmConfigPath))) {
|
|
295
|
+
throw new Error(`루트 package.json을 찾을 수 없습니다: ${projNpmConfigPath}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const projNpmConfig = await fsReadJson<NpmConfig>(projNpmConfigPath);
|
|
299
|
+
|
|
300
|
+
const capNpmConfPath = path.resolve(this._capPath, "package.json");
|
|
301
|
+
const orgCapNpmConf: NpmConfig = (await fsExists(capNpmConfPath))
|
|
302
|
+
? await fsReadJson<NpmConfig>(capNpmConfPath)
|
|
303
|
+
: { name: "", version: "" };
|
|
304
|
+
|
|
305
|
+
const capNpmConf: NpmConfig = { ...orgCapNpmConf };
|
|
306
|
+
capNpmConf.name = this._config.appId;
|
|
307
|
+
capNpmConf.version = this._npmConfig.version;
|
|
308
|
+
if (projNpmConfig.volta != null) {
|
|
309
|
+
capNpmConf.volta = projNpmConfig.volta;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 기본 의존성
|
|
313
|
+
capNpmConf.dependencies = capNpmConf.dependencies ?? {};
|
|
314
|
+
capNpmConf.dependencies["@capacitor/core"] = "^7.0.0";
|
|
315
|
+
capNpmConf.dependencies["@capacitor/app"] = "^7.0.0";
|
|
316
|
+
for (const platform of this._platforms) {
|
|
317
|
+
capNpmConf.dependencies[`@capacitor/${platform}`] = "^7.0.0";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
capNpmConf.devDependencies = capNpmConf.devDependencies ?? {};
|
|
321
|
+
capNpmConf.devDependencies["@capacitor/cli"] = "^7.0.0";
|
|
322
|
+
capNpmConf.devDependencies["@capacitor/assets"] = "^3.0.0";
|
|
323
|
+
|
|
324
|
+
// 플러그인 패키지 설정
|
|
325
|
+
const mainDeps = {
|
|
326
|
+
...this._npmConfig.dependencies,
|
|
327
|
+
...this._npmConfig.devDependencies,
|
|
328
|
+
...this._npmConfig.peerDependencies,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const usePlugins = Object.keys(this._config.plugins ?? {});
|
|
332
|
+
|
|
333
|
+
const prevPlugins = Object.keys(capNpmConf.dependencies).filter(
|
|
334
|
+
(item) => !["@capacitor/core", "@capacitor/android", "@capacitor/ios", "@capacitor/app"].includes(item),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// 사용하지 않는 플러그인 제거
|
|
338
|
+
for (const prevPlugin of prevPlugins) {
|
|
339
|
+
if (!usePlugins.includes(prevPlugin)) {
|
|
340
|
+
delete capNpmConf.dependencies[prevPlugin];
|
|
341
|
+
Capacitor._logger.debug(`플러그인 제거: ${prevPlugin}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 새 플러그인 추가
|
|
346
|
+
for (const plugin of usePlugins) {
|
|
347
|
+
if (!(plugin in capNpmConf.dependencies)) {
|
|
348
|
+
const version = mainDeps[plugin] ?? "*";
|
|
349
|
+
capNpmConf.dependencies[plugin] = version;
|
|
350
|
+
Capacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 저장
|
|
355
|
+
await fsMkdir(this._capPath);
|
|
356
|
+
await fsWriteJson(capNpmConfPath, capNpmConf, { space: 2 });
|
|
357
|
+
|
|
358
|
+
// 의존성 변경 여부 확인
|
|
359
|
+
const isChanged =
|
|
360
|
+
orgCapNpmConf.volta !== capNpmConf.volta ||
|
|
361
|
+
JSON.stringify(orgCapNpmConf.dependencies) !== JSON.stringify(capNpmConf.dependencies) ||
|
|
362
|
+
JSON.stringify(orgCapNpmConf.devDependencies) !== JSON.stringify(capNpmConf.devDependencies);
|
|
363
|
+
|
|
364
|
+
return isChanged;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* capacitor.config.ts 생성
|
|
369
|
+
*/
|
|
370
|
+
private async _writeCapConf(): Promise<void> {
|
|
371
|
+
const confPath = path.resolve(this._capPath, "capacitor.config.ts");
|
|
372
|
+
|
|
373
|
+
// 플러그인 옵션 생성
|
|
374
|
+
const pluginOptions: Record<string, Record<string, unknown>> = {};
|
|
375
|
+
for (const [pluginName, options] of Object.entries(this._config.plugins ?? {})) {
|
|
376
|
+
if (options !== true) {
|
|
377
|
+
const configKey = this._toPascalCase(pluginName.split("/").at(-1)!);
|
|
378
|
+
pluginOptions[configKey] = options;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const pluginsConfigStr =
|
|
383
|
+
Object.keys(pluginOptions).length > 0 ? JSON.stringify(pluginOptions, null, 2).replace(/^/gm, " ").trim() : "{}";
|
|
384
|
+
|
|
385
|
+
const configContent = `import type { CapacitorConfig } from "@capacitor/cli";
|
|
386
|
+
|
|
387
|
+
const config: CapacitorConfig = {
|
|
388
|
+
appId: "${this._config.appId}",
|
|
389
|
+
appName: "${this._config.appName}",
|
|
390
|
+
server: {
|
|
391
|
+
androidScheme: "http",
|
|
392
|
+
cleartext: true
|
|
393
|
+
},
|
|
394
|
+
android: {},
|
|
395
|
+
plugins: ${pluginsConfigStr},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
export default config;
|
|
399
|
+
`;
|
|
400
|
+
|
|
401
|
+
await fsWrite(confPath, configContent);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 플랫폼 추가 (F12: 멱등성 보장)
|
|
406
|
+
*/
|
|
407
|
+
private async _addPlatforms(): Promise<void> {
|
|
408
|
+
for (const platform of this._platforms) {
|
|
409
|
+
const platformPath = path.resolve(this._capPath, platform);
|
|
410
|
+
if (await fsExists(platformPath)) {
|
|
411
|
+
Capacitor._logger.debug(`플랫폼 이미 존재: ${platform}`);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
await this._exec("npx", ["cap", "add", platform], this._capPath);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 아이콘 설정 (F6: 에러 복구)
|
|
421
|
+
*/
|
|
422
|
+
private async _setupIcon(): Promise<void> {
|
|
423
|
+
const assetsDirPath = path.resolve(this._capPath, "assets");
|
|
424
|
+
|
|
425
|
+
if (this._config.icon != null) {
|
|
426
|
+
const iconSource = path.resolve(this._pkgPath, this._config.icon);
|
|
427
|
+
|
|
428
|
+
// F6: 소스 아이콘 존재 확인
|
|
429
|
+
if (!(await fsExists(iconSource))) {
|
|
430
|
+
Capacitor._logger.warn(`아이콘 파일을 찾을 수 없습니다: ${iconSource}. 기본 아이콘을 사용합니다.`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
await fsMkdir(assetsDirPath);
|
|
436
|
+
|
|
437
|
+
// 아이콘 생성
|
|
438
|
+
const logoPath = path.resolve(assetsDirPath, "logo.png");
|
|
439
|
+
|
|
440
|
+
const logoSize = Math.floor(1024 * 0.6);
|
|
441
|
+
const padding = Math.floor((1024 - logoSize) / 2);
|
|
442
|
+
|
|
443
|
+
// F6: sharp 에러 처리
|
|
444
|
+
await sharp(iconSource)
|
|
445
|
+
.resize(logoSize, logoSize, {
|
|
446
|
+
fit: "contain",
|
|
447
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
448
|
+
})
|
|
449
|
+
.extend({
|
|
450
|
+
top: padding,
|
|
451
|
+
bottom: padding,
|
|
452
|
+
left: padding,
|
|
453
|
+
right: padding,
|
|
454
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
455
|
+
})
|
|
456
|
+
.toFile(logoPath);
|
|
457
|
+
|
|
458
|
+
await this._exec(
|
|
459
|
+
"npx",
|
|
460
|
+
["@capacitor/assets", "generate", "--iconBackgroundColor", "#ffffff", "--splashBackgroundColor", "#ffffff"],
|
|
461
|
+
this._capPath,
|
|
462
|
+
);
|
|
463
|
+
} catch (err) {
|
|
464
|
+
Capacitor._logger.warn(
|
|
465
|
+
`아이콘 생성 실패: ${err instanceof Error ? err.message : err}. 기본 아이콘을 사용합니다.`,
|
|
466
|
+
);
|
|
467
|
+
// F6: 실패해도 계속 진행 (기본 아이콘 사용)
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
await fsRm(assetsDirPath);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
//#endregion
|
|
475
|
+
|
|
476
|
+
//#region Private - Android 설정
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Android 네이티브 설정
|
|
480
|
+
*/
|
|
481
|
+
private async _configureAndroid(): Promise<void> {
|
|
482
|
+
const androidPath = path.resolve(this._capPath, "android");
|
|
483
|
+
|
|
484
|
+
// F3: Android 디렉토리 존재 확인
|
|
485
|
+
if (!(await fsExists(androidPath))) {
|
|
486
|
+
throw new Error(`Android 프로젝트 디렉토리가 없습니다: ${androidPath}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
await this._configureAndroidJavaHomePath(androidPath);
|
|
490
|
+
await this._configureAndroidSdkPath(androidPath);
|
|
491
|
+
await this._configureAndroidManifest(androidPath);
|
|
492
|
+
await this._configureAndroidBuildGradle(androidPath);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* JAVA_HOME 경로 설정 (gradle.properties)
|
|
497
|
+
*/
|
|
498
|
+
private async _configureAndroidJavaHomePath(androidPath: string): Promise<void> {
|
|
499
|
+
const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
|
|
500
|
+
|
|
501
|
+
// F3: 파일 존재 확인
|
|
502
|
+
if (!(await fsExists(gradlePropsPath))) {
|
|
503
|
+
Capacitor._logger.warn(`gradle.properties 파일이 없습니다: ${gradlePropsPath}`);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let content = await fsRead(gradlePropsPath);
|
|
508
|
+
|
|
509
|
+
const java21Path = await this._findJava21();
|
|
510
|
+
if (java21Path != null && !content.includes("org.gradle.java.home")) {
|
|
511
|
+
// F9: Windows 경로 이스케이프 개선
|
|
512
|
+
const escapedPath = java21Path.replace(/\\/g, "\\\\");
|
|
513
|
+
content += `\norg.gradle.java.home=${escapedPath}\n`;
|
|
514
|
+
await fsWrite(gradlePropsPath, content);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Java 21 경로 자동 탐색
|
|
520
|
+
*/
|
|
521
|
+
private async _findJava21(): Promise<string | undefined> {
|
|
522
|
+
const patterns = [
|
|
523
|
+
"C:/Program Files/Amazon Corretto/jdk21*",
|
|
524
|
+
"C:/Program Files/Eclipse Adoptium/jdk-21*",
|
|
525
|
+
"C:/Program Files/Java/jdk-21*",
|
|
526
|
+
"C:/Program Files/Microsoft/jdk-21*",
|
|
527
|
+
"/usr/lib/jvm/java-21*",
|
|
528
|
+
"/usr/lib/jvm/temurin-21*",
|
|
529
|
+
];
|
|
530
|
+
|
|
531
|
+
for (const pattern of patterns) {
|
|
532
|
+
const matches = await fsGlob(pattern);
|
|
533
|
+
if (matches.length > 0) {
|
|
534
|
+
return matches.sort().at(-1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Android SDK 경로 설정 (local.properties)
|
|
543
|
+
*/
|
|
544
|
+
private async _configureAndroidSdkPath(androidPath: string): Promise<void> {
|
|
545
|
+
const localPropsPath = path.resolve(androidPath, "local.properties");
|
|
546
|
+
|
|
547
|
+
const sdkPath = await this._findAndroidSdk();
|
|
548
|
+
if (sdkPath != null) {
|
|
549
|
+
// F9: 항상 forward slash 사용 (Gradle 호환)
|
|
550
|
+
await fsWrite(localPropsPath, `sdk.dir=${sdkPath.replace(/\\/g, "/")}\n`);
|
|
551
|
+
} else {
|
|
552
|
+
throw new Error(
|
|
553
|
+
"Android SDK를 찾을 수 없습니다.\n" +
|
|
554
|
+
"1. Android Studio를 설치하거나\n" +
|
|
555
|
+
"2. ANDROID_HOME 또는 ANDROID_SDK_ROOT 환경변수를 설정하세요.",
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Android SDK 경로 탐색
|
|
562
|
+
*/
|
|
563
|
+
private async _findAndroidSdk(): Promise<string | undefined> {
|
|
564
|
+
const fromEnv = (env["ANDROID_HOME"] ?? env["ANDROID_SDK_ROOT"]) as string | undefined;
|
|
565
|
+
if (fromEnv != null && (await fsExists(fromEnv))) {
|
|
566
|
+
return fromEnv;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const candidates = [
|
|
570
|
+
path.resolve((env["LOCALAPPDATA"] as string | undefined) ?? "", "Android/Sdk"),
|
|
571
|
+
path.resolve((env["HOME"] as string | undefined) ?? "", "Android/Sdk"),
|
|
572
|
+
"C:/Program Files/Android/Sdk",
|
|
573
|
+
"C:/Android/Sdk",
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
for (const candidate of candidates) {
|
|
577
|
+
if (await fsExists(candidate)) {
|
|
578
|
+
return candidate;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* AndroidManifest.xml 수정 (F3: 에러 처리 추가)
|
|
587
|
+
*/
|
|
588
|
+
private async _configureAndroidManifest(androidPath: string): Promise<void> {
|
|
589
|
+
const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
|
|
590
|
+
|
|
591
|
+
// F3: 파일 존재 확인
|
|
592
|
+
if (!(await fsExists(manifestPath))) {
|
|
593
|
+
throw new Error(`AndroidManifest.xml 파일이 없습니다: ${manifestPath}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let content = await fsRead(manifestPath);
|
|
597
|
+
|
|
598
|
+
// usesCleartextTraffic 설정
|
|
599
|
+
if (!content.includes("android:usesCleartextTraffic")) {
|
|
600
|
+
content = content.replace("<application", '<application android:usesCleartextTraffic="true"');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 추가 권한 설정
|
|
604
|
+
const permissions = this._config.platform?.android?.permissions ?? [];
|
|
605
|
+
for (const perm of permissions) {
|
|
606
|
+
const permTag = `<uses-permission android:name="android.permission.${perm.name}"`;
|
|
607
|
+
if (!content.includes(permTag)) {
|
|
608
|
+
const maxSdkAttr = perm.maxSdkVersion != null ? ` android:maxSdkVersion="${perm.maxSdkVersion}"` : "";
|
|
609
|
+
const ignoreAttr = perm.ignore != null ? ` tools:ignore="${perm.ignore}"` : "";
|
|
610
|
+
const permLine = ` ${permTag}${maxSdkAttr}${ignoreAttr} />\n`;
|
|
611
|
+
|
|
612
|
+
if (perm.ignore != null && !content.includes("xmlns:tools=")) {
|
|
613
|
+
content = content.replace(
|
|
614
|
+
"<manifest xmlns:android",
|
|
615
|
+
'<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android',
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
content = content.replace("</manifest>", `${permLine}</manifest>`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 추가 application 설정
|
|
624
|
+
const appConfig = this._config.platform?.android?.config;
|
|
625
|
+
if (appConfig) {
|
|
626
|
+
for (const [key, value] of Object.entries(appConfig)) {
|
|
627
|
+
const attr = `android:${key}="${value}"`;
|
|
628
|
+
if (!content.includes(`android:${key}=`)) {
|
|
629
|
+
content = content.replace("<application", `<application ${attr}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// intentFilters 설정
|
|
635
|
+
const intentFilters = this._config.platform?.android?.intentFilters ?? [];
|
|
636
|
+
for (const filter of intentFilters) {
|
|
637
|
+
const filterKey = filter.action ?? filter.category ?? "";
|
|
638
|
+
if (filterKey && !content.includes(filterKey)) {
|
|
639
|
+
const actionLine = filter.action != null ? `<action android:name="${filter.action}"/>` : "";
|
|
640
|
+
const categoryLine = filter.category != null ? `<category android:name="${filter.category}"/>` : "";
|
|
641
|
+
|
|
642
|
+
content = content.replace(
|
|
643
|
+
/(<activity[\s\S]*?android:name="\.MainActivity"[\s\S]*?>)/,
|
|
644
|
+
`$1
|
|
645
|
+
<intent-filter>
|
|
646
|
+
${actionLine}
|
|
647
|
+
${categoryLine}
|
|
648
|
+
</intent-filter>`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
await fsWrite(manifestPath, content);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* build.gradle 수정 (F3: 에러 처리 추가)
|
|
658
|
+
*/
|
|
659
|
+
private async _configureAndroidBuildGradle(androidPath: string): Promise<void> {
|
|
660
|
+
const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
|
|
661
|
+
|
|
662
|
+
// F3: 파일 존재 확인
|
|
663
|
+
if (!(await fsExists(buildGradlePath))) {
|
|
664
|
+
throw new Error(`build.gradle 파일이 없습니다: ${buildGradlePath}`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let content = await fsRead(buildGradlePath);
|
|
668
|
+
|
|
669
|
+
// versionName, versionCode 설정
|
|
670
|
+
const version = this._npmConfig.version;
|
|
671
|
+
const versionParts = version.split(".");
|
|
672
|
+
const versionCode =
|
|
673
|
+
parseInt(versionParts[0] ?? "0") * 10000 +
|
|
674
|
+
parseInt(versionParts[1] ?? "0") * 100 +
|
|
675
|
+
parseInt(versionParts[2] ?? "0");
|
|
676
|
+
|
|
677
|
+
content = content.replace(/versionCode \d+/, `versionCode ${versionCode}`);
|
|
678
|
+
content = content.replace(/versionName "[^"]+"/, `versionName "${version}"`);
|
|
679
|
+
|
|
680
|
+
// SDK 버전 설정
|
|
681
|
+
if (this._config.platform?.android?.sdkVersion != null) {
|
|
682
|
+
const sdkVersion = this._config.platform.android.sdkVersion;
|
|
683
|
+
content = content.replace(/minSdkVersion .+/, `minSdkVersion ${sdkVersion}`);
|
|
684
|
+
content = content.replace(/targetSdkVersion .+/, `targetSdkVersion ${sdkVersion}`);
|
|
685
|
+
} else {
|
|
686
|
+
content = content.replace(/minSdkVersion .+/, `minSdkVersion rootProject.ext.minSdkVersion`);
|
|
687
|
+
content = content.replace(/targetSdkVersion .+/, `targetSdkVersion rootProject.ext.targetSdkVersion`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Signing 설정
|
|
691
|
+
const keystorePath = path.resolve(this._capPath, Capacitor._ANDROID_KEYSTORE_FILE_NAME);
|
|
692
|
+
const signConfig = this._config.platform?.android?.sign;
|
|
693
|
+
if (signConfig) {
|
|
694
|
+
const keystoreSource = path.resolve(this._pkgPath, signConfig.keystore);
|
|
695
|
+
// F3: keystore 파일 존재 확인
|
|
696
|
+
if (!(await fsExists(keystoreSource))) {
|
|
697
|
+
throw new Error(`keystore 파일을 찾을 수 없습니다: ${keystoreSource}`);
|
|
698
|
+
}
|
|
699
|
+
await fsCopy(keystoreSource, keystorePath);
|
|
700
|
+
|
|
701
|
+
// F9: 상대 경로를 forward slash로 변환
|
|
702
|
+
const keystoreRelativePath = path.relative(path.dirname(buildGradlePath), keystorePath).replace(/\\/g, "/");
|
|
703
|
+
const keystoreType = signConfig.keystoreType ?? "jks";
|
|
704
|
+
|
|
705
|
+
if (!content.includes("signingConfigs")) {
|
|
706
|
+
const signingConfigsBlock = `
|
|
707
|
+
signingConfigs {
|
|
708
|
+
release {
|
|
709
|
+
storeFile file("${keystoreRelativePath}")
|
|
710
|
+
storePassword '${signConfig.storePassword}'
|
|
711
|
+
keyAlias '${signConfig.alias}'
|
|
712
|
+
keyPassword '${signConfig.password}'
|
|
713
|
+
storeType "${keystoreType}"
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
`;
|
|
717
|
+
content = content.replace(/(android\s*\{)/, (match) => `${match}${signingConfigsBlock}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!content.includes("signingConfig signingConfigs.release")) {
|
|
721
|
+
content = content.replace(
|
|
722
|
+
/(buildTypes\s*\{[\s\S]*?release\s*\{)/,
|
|
723
|
+
`$1\n signingConfig signingConfigs.release`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
await fsRm(keystorePath);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
await fsWrite(buildGradlePath, content);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
//#endregion
|
|
734
|
+
|
|
735
|
+
//#region Private - 빌드
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Android 빌드
|
|
739
|
+
*/
|
|
740
|
+
private async _buildAndroid(outPath: string, buildType: string): Promise<void> {
|
|
741
|
+
const androidPath = path.resolve(this._capPath, "android");
|
|
742
|
+
const targetOutPath = path.resolve(outPath, "android");
|
|
743
|
+
|
|
744
|
+
const isBundle = this._config.platform?.android?.bundle;
|
|
745
|
+
const gradleTask = buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
|
|
746
|
+
|
|
747
|
+
// Gradle 빌드 실행 (크로스 플랫폼)
|
|
748
|
+
// F9: Windows에서 cmd.exe를 통해 실행 (shell: false 이므로)
|
|
749
|
+
if (process.platform === "win32") {
|
|
750
|
+
await this._exec("cmd", ["/c", "gradlew.bat", gradleTask, "--no-daemon"], androidPath);
|
|
751
|
+
} else {
|
|
752
|
+
await this._exec("sh", ["./gradlew", gradleTask, "--no-daemon"], androidPath);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// 빌드 결과물 복사
|
|
756
|
+
await this._copyAndroidBuildOutput(androidPath, targetOutPath, buildType);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Android 빌드 결과물 복사
|
|
761
|
+
*/
|
|
762
|
+
private async _copyAndroidBuildOutput(androidPath: string, targetOutPath: string, buildType: string): Promise<void> {
|
|
763
|
+
const isBundle = this._config.platform?.android?.bundle;
|
|
764
|
+
const isSigned = Boolean(this._config.platform?.android?.sign);
|
|
765
|
+
|
|
766
|
+
const ext = isBundle ? "aab" : "apk";
|
|
767
|
+
const outputType = isBundle ? "bundle" : "apk";
|
|
768
|
+
const fileName = isSigned ? `app-${buildType}.${ext}` : `app-${buildType}-unsigned.${ext}`;
|
|
769
|
+
|
|
770
|
+
const sourcePath = path.resolve(androidPath, "app/build/outputs", outputType, buildType, fileName);
|
|
771
|
+
|
|
772
|
+
const actualPath = (await fsExists(sourcePath))
|
|
773
|
+
? sourcePath
|
|
774
|
+
: path.resolve(androidPath, "app/build/outputs", outputType, buildType, `app-${buildType}.${ext}`);
|
|
775
|
+
|
|
776
|
+
if (!(await fsExists(actualPath))) {
|
|
777
|
+
Capacitor._logger.warn(`빌드 결과물을 찾을 수 없습니다: ${actualPath}`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const outputFileName = `${this._config.appName}${isSigned ? "" : "-unsigned"}-latest.${ext}`;
|
|
782
|
+
|
|
783
|
+
await fsMkdir(targetOutPath);
|
|
784
|
+
await fsCopy(actualPath, path.resolve(targetOutPath, outputFileName));
|
|
785
|
+
|
|
786
|
+
// 버전별 저장
|
|
787
|
+
const updatesPath = path.resolve(targetOutPath, "updates");
|
|
788
|
+
await fsMkdir(updatesPath);
|
|
789
|
+
await fsCopy(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
//#endregion
|
|
793
|
+
|
|
794
|
+
//#region Private - 디바이스 실행
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* capacitor.config.ts의 server.url 업데이트
|
|
798
|
+
*/
|
|
799
|
+
private async _updateServerUrl(url: string): Promise<void> {
|
|
800
|
+
const configPath = path.resolve(this._capPath, "capacitor.config.ts");
|
|
801
|
+
|
|
802
|
+
if (!(await fsExists(configPath))) return;
|
|
803
|
+
|
|
804
|
+
let content = await fsRead(configPath);
|
|
805
|
+
|
|
806
|
+
if (content.includes("url:")) {
|
|
807
|
+
content = content.replace(/url:\s*"[^"]*"/, `url: "${url}"`);
|
|
808
|
+
} else if (content.includes("server:")) {
|
|
809
|
+
content = content.replace(/server:\s*\{/, `server: {\n url: "${url}",`);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
await fsWrite(configPath, content);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
//#endregion
|
|
816
|
+
|
|
817
|
+
//#region Private - 유틸리티
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* 문자열을 PascalCase로 변환
|
|
821
|
+
*/
|
|
822
|
+
private _toPascalCase(str: string): string {
|
|
823
|
+
return str.replace(/[-_](.)/g, (_, c: string) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase());
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
//#endregion
|
|
827
|
+
}
|