@simplysm/sd-cli 12.16.13 → 12.16.15

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.
@@ -2,18 +2,14 @@ import * as path from "path";
2
2
  import { FsUtils, PathUtils, SdLogger, SdProcess } from "@simplysm/sd-core-node";
3
3
  import { ISdClientBuilderCapacitorConfig } from "../types/config/ISdProjectConfig";
4
4
  import { INpmConfig } from "../types/common-config/INpmConfig";
5
- import { StringUtils, typescript } from "@simplysm/sd-core-common";
5
+ import { NotImplementError, ObjectUtils, StringUtils, typescript } from "@simplysm/sd-core-common";
6
6
  import sharp from "sharp";
7
7
 
8
8
  export class SdCliCapacitor {
9
9
  // 상수 정의
10
- private readonly _CAPACITOR_DIR_NAME = ".capacitor";
11
- private readonly _CONFIG_FILE_NAME = "capacitor.config.ts";
12
- private readonly _KEYSTORE_FILE_NAME = "android.keystore";
13
- private readonly _ICON_DIR_NAME = "resources";
10
+ private readonly _ANDROID_KEYSTORE_FILE_NAME = "android.keystore";
14
11
 
15
- // private readonly _ANDROID_DIR_NAME = "android";
16
- // private readonly _WWW_DIR_NAME = "www";
12
+ private readonly _capPath: string;
17
13
 
18
14
  private readonly _platforms: string[];
19
15
  private readonly _npmConfig: INpmConfig;
@@ -21,126 +17,83 @@ export class SdCliCapacitor {
21
17
  constructor(private readonly _opt: { pkgPath: string; config: ISdClientBuilderCapacitorConfig }) {
22
18
  this._platforms = Object.keys(this._opt.config.platform ?? {});
23
19
  this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
20
+ this._capPath = path.resolve(this._opt.pkgPath, ".capacitor");
24
21
  }
25
22
 
26
23
  private static readonly _logger = SdLogger.get(["simplysm", "sd-cli", "SdCliCapacitor"]);
27
24
 
28
- private static async _execAsync(cmd: string, args: string[], cwd: string): Promise<void> {
25
+ private static async _execAsync(cmd: string, args: string[], cwd: string): Promise<string> {
29
26
  this._logger.debug(`실행 명령: ${cmd + " " + args.join(" ")}`);
30
- const msg = await SdProcess.spawnAsync(cmd, args, { cwd });
27
+ const msg = await SdProcess.spawnAsync(cmd, args, {
28
+ cwd,
29
+ env: {
30
+ FORCE_COLOR: "1", // chalk, supports-color 계열
31
+ CLICOLOR_FORCE: "1", // 일부 Unix 도구
32
+ COLORTERM: "truecolor", // 추가 힌트
33
+ },
34
+ });
31
35
  this._logger.debug(`실행 결과: ${msg}`);
36
+ return msg;
32
37
  }
33
38
 
34
39
  async initializeAsync(): Promise<void> {
35
- const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
36
-
37
40
  // 1. Capacitor 프로젝트 초기화
38
- const isNewProject = await this._initializeCapacitorProjectAsync(capacitorPath);
41
+ const changed = await this._initCapAsync();
39
42
 
40
43
  // 2. Capacitor 설정 파일 생성
41
- await this._createCapacitorConfigAsync(capacitorPath);
44
+ await this._writeCapConfAsync();
42
45
 
43
46
  // 3. 플랫폼 관리
44
- await this._managePlatformsAsync(capacitorPath);
45
-
46
- // 4. 플러그인 관리
47
- const pluginsChanged = await this._managePluginsAsync(capacitorPath);
48
-
49
- // 5. 안드로이드 서명 설정
50
- await this._setupAndroidSignAsync(capacitorPath);
47
+ await this._addPlatformsAsync();
51
48
 
52
- // 6. 아이콘 및 스플래시 스크린 설정
53
- await this._setupIconAndSplashScreenAsync(capacitorPath);
49
+ // 4. 아이콘 설정
50
+ await this._setupIconAsync();
54
51
 
55
- // 7. Android 네이티브 설정 (AndroidManifest.xml, build.gradle 등)
52
+ // 5. Android 네이티브 설정
56
53
  if (this._platforms.includes("android")) {
57
- await this._configureAndroidNativeAsync(capacitorPath);
54
+ await this._configureAndroidAsync();
58
55
  }
59
56
 
60
- // 8. 웹 자산 동기화
61
- if (isNewProject || pluginsChanged) {
62
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
57
+ // 6. 웹 자산 동기화
58
+ if (changed) {
59
+ await SdCliCapacitor._execAsync("npx", ["cap", "sync"], this._capPath);
63
60
  } else {
64
- await SdCliCapacitor._execAsync("npx", ["cap", "copy"], capacitorPath);
61
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy"], this._capPath);
65
62
  }
66
63
  }
67
64
 
68
65
  // 1. Capacitor 프로젝트 초기화
69
- private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<boolean> {
70
- const wwwPath = path.resolve(capacitorPath, "www");
71
-
72
- if (FsUtils.exists(wwwPath)) {
73
- SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
74
-
75
- // 버전 동기화
76
- const pkgJsonPath = path.resolve(capacitorPath, "package.json");
77
-
78
- if (FsUtils.exists(pkgJsonPath)) {
79
- const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
80
-
81
- if (pkgJson.version !== this._npmConfig.version) {
82
- pkgJson.version = this._npmConfig.version;
83
- await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
84
- SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
85
- }
86
- }
87
- return false;
88
- }
89
-
90
- // www 폴더 생성
91
- await FsUtils.mkdirsAsync(wwwPath);
92
-
93
- // package.json 생성
94
- const projNpmConfig = await FsUtils.readJsonAsync(
95
- path.resolve(this._opt.pkgPath, "../../package.json"),
96
- );
97
- const pkgJson = {
98
- name: this._opt.config.appId,
99
- version: this._npmConfig.version,
100
- private: true,
101
- volta: projNpmConfig.volta,
102
- dependencies: {
103
- "@capacitor/core": "^7.0.0",
104
- "@capacitor/app": "^7.0.0",
105
- },
106
- devDependencies: {
107
- "@capacitor/cli": "^7.0.0",
108
- "@capacitor/assets": "^3.0.0",
109
- ...this._platforms.toObject(
110
- (item) => `@capacitor/${item}`,
111
- () => "^7.0.0",
112
- ),
113
- },
114
- };
115
- await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
116
- space: 2,
117
- });
66
+ private async _initCapAsync(): Promise<boolean> {
67
+ // package.json 파일 구성
68
+ const depChanged = await this._setupNpmConfAsync();
69
+ if (!depChanged) return false;
118
70
 
119
71
  // .yarnrc.yml 작성
120
72
  await FsUtils.writeFileAsync(
121
- path.resolve(capacitorPath, ".yarnrc.yml"),
73
+ path.resolve(this._capPath, ".yarnrc.yml"),
122
74
  "nodeLinker: node-modules",
123
75
  );
124
76
 
125
77
  // 빈 yarn.lock 작성
126
- await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
78
+ await FsUtils.writeFileAsync(path.resolve(this._capPath, "yarn.lock"), "");
127
79
 
128
80
  // yarn install
129
- await SdCliCapacitor._execAsync(
130
- "yarn",
131
- ["dlx", "npm-check-updates", "-u", "--target", "semver"],
132
- capacitorPath,
133
- );
134
- await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
81
+ const installResult = await SdCliCapacitor._execAsync("yarn", ["install"], this._capPath);
82
+ const errorLines = installResult.split("\n").filter((item) => item.includes("YN0002"));
83
+ // peer dependency 경고 감지
84
+ if (errorLines.length > 0) {
85
+ throw new Error(errorLines.join("\n"));
86
+ }
135
87
 
136
- // capacitor init
88
+ // cap init
137
89
  await SdCliCapacitor._execAsync(
138
90
  "npx",
139
91
  ["cap", "init", this._opt.config.appName, this._opt.config.appId],
140
- capacitorPath,
92
+ this._capPath,
141
93
  );
142
94
 
143
- // www/index.html 생성
95
+ // 기본 www/index.html 생성
96
+ const wwwPath = path.resolve(this._capPath, "www");
144
97
  await FsUtils.writeFileAsync(
145
98
  path.resolve(wwwPath, "index.html"),
146
99
  "<!DOCTYPE html><html><head></head><body></body></html>",
@@ -149,15 +102,111 @@ export class SdCliCapacitor {
149
102
  return true;
150
103
  }
151
104
 
105
+ private async _setupNpmConfAsync() {
106
+ const projNpmConfig = await FsUtils.readJsonAsync(
107
+ path.resolve(this._opt.pkgPath, "../../package.json"),
108
+ );
109
+
110
+ // -----------------------------
111
+ // 기본설정
112
+ // -----------------------------
113
+
114
+ const capNpmConfPath = path.resolve(this._capPath, "package.json");
115
+ const orgCapNpmConf = FsUtils.exists(capNpmConfPath)
116
+ ? await FsUtils.readJsonAsync(path.resolve(this._capPath, "package.json"))
117
+ : {};
118
+
119
+ const capNpmConf = ObjectUtils.clone(orgCapNpmConf);
120
+ capNpmConf.name = this._opt.config.appId;
121
+ capNpmConf.version = this._npmConfig.version;
122
+ capNpmConf.volta = projNpmConfig.volta;
123
+ capNpmConf.dependencies = capNpmConf.dependencies ?? {};
124
+ capNpmConf.dependencies["@capacitor/core"] = "^7.0.0";
125
+ capNpmConf.dependencies["@capacitor/app"] = "^7.0.0";
126
+ for (const platform of this._platforms) {
127
+ capNpmConf.dependencies[`@capacitor/${platform}`] = "^7.0.0";
128
+ }
129
+ capNpmConf.devDependencies = capNpmConf.devDependencies ?? {};
130
+ capNpmConf.devDependencies["@capacitor/cli"] = "^7.0.0";
131
+ capNpmConf.devDependencies["@capacitor/assets"] = "^3.0.0";
132
+
133
+ // -----------------------------
134
+ // 플러그인 패키지 설정
135
+ // -----------------------------
136
+
137
+ const mainDeps = {
138
+ ...this._npmConfig.dependencies,
139
+ ...this._npmConfig.devDependencies,
140
+ ...this._npmConfig.peerDependencies,
141
+ };
142
+
143
+ const usePlugins = Object.keys(this._opt.config.plugins ?? {});
144
+
145
+ const prevPlugins = Object.keys(capNpmConf.dependencies).filter(
146
+ (item) =>
147
+ !["@capacitor/core", "@capacitor/android", "@capacitor/ios", "@capacitor/app"].includes(
148
+ item,
149
+ ),
150
+ );
151
+
152
+ // 사용하지 않는 플러그인 제거
153
+ for (const prevPlugin of prevPlugins) {
154
+ if (!usePlugins.includes(prevPlugin)) {
155
+ delete capNpmConf.dependencies[prevPlugin];
156
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${prevPlugin}`);
157
+ }
158
+ }
159
+
160
+ // 새 플러그인 추가
161
+ for (const plugin of usePlugins) {
162
+ if (!(plugin in capNpmConf.dependencies)) {
163
+ const version = mainDeps[plugin] ?? "*";
164
+ capNpmConf.dependencies[plugin] = version;
165
+ SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
166
+ }
167
+ }
168
+
169
+ // -----------------------------
170
+ // 저장
171
+ // -----------------------------
172
+
173
+ await FsUtils.writeJsonAsync(capNpmConfPath, capNpmConf, { space: 2 });
174
+
175
+ return (
176
+ !ObjectUtils.equal(orgCapNpmConf.volta, capNpmConf.volta) ||
177
+ !ObjectUtils.equal(orgCapNpmConf.dependencies, capNpmConf.dependencies) ||
178
+ !ObjectUtils.equal(orgCapNpmConf.devDependencies, capNpmConf.devDependencies)
179
+ );
180
+
181
+ /*// volta, dep, devDep 이 변한 경우에만, yarn install
182
+ if (
183
+ !ObjectUtils.equal(orgCapNpmConf.volta, capNpmConf.volta) ||
184
+ !ObjectUtils.equal(orgCapNpmConf.dependencies, capNpmConf.dependencies) ||
185
+ !ObjectUtils.equal(orgCapNpmConf.devDependencies, capNpmConf.devDependencies)
186
+ ) {
187
+ const installResult = await SdCliCapacitor._execAsync("yarn", ["install"], this._capPath);
188
+
189
+ const errorLines = installResult.split("\n").filter((item) => item.includes("YN0002"));
190
+
191
+ // peer dependency 경고 감지
192
+ if (errorLines.length > 0) {
193
+ throw new Error(errorLines.join("\n"));
194
+ }
195
+
196
+ return true;
197
+ }
198
+ // 변경 없으면 아무것도 안 함 → 오프라인 OK
199
+ return false;*/
200
+ }
201
+
152
202
  // 2. Capacitor 설정 파일 생성
153
- private async _createCapacitorConfigAsync(capacitorPath: string) {
154
- const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
203
+ private async _writeCapConfAsync() {
204
+ const confPath = path.resolve(this._capPath, "capacitor.config.ts");
155
205
 
156
206
  // 플러그인 옵션 생성
157
207
  const pluginOptions: Record<string, Record<string, unknown>> = {};
158
208
  for (const [pluginName, options] of Object.entries(this._opt.config.plugins ?? {})) {
159
209
  if (options !== true) {
160
- // @capacitor/splash-screen → SplashScreen 형태로 변환
161
210
  const configKey = StringUtils.toPascalCase(pluginName.split("/").last()!);
162
211
  pluginOptions[configKey] = options;
163
212
  }
@@ -165,7 +214,7 @@ export class SdCliCapacitor {
165
214
 
166
215
  const pluginsConfigStr =
167
216
  Object.keys(pluginOptions).length > 0
168
- ? JSON.stringify(pluginOptions, null, 4).replace(/^/gm, " ").trim()
217
+ ? JSON.stringify(pluginOptions, null, 2).replace(/^/gm, " ").trim()
169
218
  : "{}";
170
219
 
171
220
  const configContent = typescript`
@@ -176,203 +225,110 @@ export class SdCliCapacitor {
176
225
  appName: "${this._opt.config.appName}",
177
226
  server: {
178
227
  androidScheme: "http",
179
- cleartext: true,
180
- allowNavigation: ["*"],
181
- },
182
- android: {
183
- allowMixedContent: true,
184
- statusBarOverlaysWebView: false,
228
+ cleartext: true
185
229
  },
230
+ android: {},
186
231
  plugins: ${pluginsConfigStr},
187
232
  };
188
233
 
189
234
  export default config;
190
235
  `;
191
236
 
192
- await FsUtils.writeFileAsync(configFilePath, configContent);
237
+ await FsUtils.writeFileAsync(confPath, configContent);
193
238
  }
194
239
 
195
- // 3. 플랫폼 관리
196
- private async _managePlatformsAsync(capacitorPath: string): Promise<void> {
240
+ // 3. 플랫폼 추가
241
+ private async _addPlatformsAsync(): Promise<void> {
197
242
  for (const platform of this._platforms) {
198
- if (FsUtils.exists(path.resolve(capacitorPath, platform))) continue;
199
-
200
- await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], capacitorPath);
201
- }
202
- }
203
-
204
- private async _managePluginsAsync(capacitorPath: string): Promise<boolean> {
205
- const pkgJsonPath = path.resolve(capacitorPath, "package.json");
206
- const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
207
-
208
- const mainDeps = {
209
- ...this._npmConfig.dependencies,
210
- ...this._npmConfig.devDependencies,
211
- ...this._npmConfig.peerDependencies,
212
- };
213
-
214
- const usePlugins = Object.keys(this._opt.config.plugins ?? {});
215
- const currentDeps = pkgJson.dependencies ?? {};
216
-
217
- let changed = false;
218
-
219
- // 사용하지 않는 플러그인 제거
220
- for (const dep of Object.keys(currentDeps)) {
221
- if (this._isCapacitorPlugin(dep) && !usePlugins.includes(dep)) {
222
- delete currentDeps[dep];
223
- changed = true;
224
- SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
225
- }
226
- }
227
-
228
- // 새 플러그인 추가
229
- for (const plugin of usePlugins) {
230
- if (!(plugin in currentDeps)) {
231
- const version = mainDeps[plugin] ?? "^7.0.0";
232
- currentDeps[plugin] = version;
233
- changed = true;
234
- SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
235
- }
236
- }
237
-
238
- // 변경사항 있을 때만 저장 & install
239
- if (changed) {
240
- pkgJson.dependencies = currentDeps;
241
- await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
242
- await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
243
- return true;
244
- }
245
- // 변경 없으면 아무것도 안 함 → 오프라인 OK
246
- return false;
247
- }
248
-
249
- private _isCapacitorPlugin(dep: string): boolean {
250
- // 기본 패키지 제외
251
- const corePackages = [
252
- "@capacitor/core",
253
- "@capacitor/android",
254
- "@capacitor/ios",
255
- "@capacitor/app",
256
- ];
257
- if (corePackages.includes(dep)) return false;
258
-
259
- return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
260
- }
243
+ if (FsUtils.exists(path.resolve(this._capPath, platform))) continue;
261
244
 
262
- // 5. 안드로이드 서명 설정
263
- private async _setupAndroidSignAsync(capacitorPath: string) {
264
- const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
265
-
266
- if (this._opt.config.platform?.android?.sign) {
267
- await FsUtils.copyAsync(
268
- path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore),
269
- keystorePath,
270
- );
271
- } else {
272
- await FsUtils.removeAsync(keystorePath);
245
+ await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], this._capPath);
273
246
  }
274
247
  }
275
248
 
276
- private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
277
- const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_NAME);
249
+ // 4. 아이콘 설정
250
+ private async _setupIconAsync(): Promise<void> {
251
+ const assetsDirPath = path.resolve(this._capPath, "assets");
278
252
 
279
253
  if (this._opt.config.icon != null) {
280
- await FsUtils.mkdirsAsync(resourcesDirPath);
254
+ await FsUtils.mkdirsAsync(assetsDirPath);
281
255
 
282
256
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
283
257
 
284
- // 아이콘만 생성
285
- const logoPath = path.resolve(resourcesDirPath, "logo.png");
286
- await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
287
-
288
- await this._cleanupExistingIconsAsync(capacitorPath);
289
-
290
- try {
291
- await SdCliCapacitor._execAsync(
292
- "npx",
293
- [
294
- "@capacitor/assets",
295
- "generate",
296
- "--android",
297
- "--iconBackgroundColor",
298
- "#ffffff",
299
- "--splashBackgroundColor",
300
- "#ffffff",
301
- ],
302
- capacitorPath,
303
- );
304
- } catch (e) {
305
- SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용", e);
306
- }
258
+ // 아이콘 생성
259
+ const logoPath = path.resolve(assetsDirPath, "logo.png");
260
+
261
+ const logoSize = Math.floor(1024 * 0.6);
262
+ const padding = Math.floor((1024 - logoSize) / 2);
263
+
264
+ await sharp(iconSource)
265
+ .resize(logoSize, logoSize, {
266
+ fit: "contain",
267
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
268
+ })
269
+ .extend({
270
+ top: padding,
271
+ bottom: padding,
272
+ left: padding,
273
+ right: padding,
274
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
275
+ })
276
+ .toFile(logoPath);
277
+
278
+ // await this._cleanupExistingIconsAsync();
279
+
280
+ await SdCliCapacitor._execAsync(
281
+ "npx",
282
+ [
283
+ "@capacitor/assets",
284
+ "generate",
285
+ "--iconBackgroundColor",
286
+ "#ffffff",
287
+ "--splashBackgroundColor",
288
+ "#ffffff",
289
+ ],
290
+ this._capPath,
291
+ );
307
292
  } else {
308
- await FsUtils.removeAsync(resourcesDirPath);
293
+ await FsUtils.removeAsync(assetsDirPath);
309
294
  }
310
295
  }
311
296
 
312
- // 중앙에 로고를 배치한 이미지 생성
313
- private async _createCenteredImageAsync(
314
- sourcePath: string,
315
- outputPath: string,
316
- outputSize: number,
317
- logoRatio: number, // 0.0 ~ 1.0
318
- ): Promise<void> {
319
- const logoSize = Math.floor(outputSize * logoRatio);
320
- const padding = Math.floor((outputSize - logoSize) / 2);
321
-
322
- await sharp(sourcePath)
323
- .resize(logoSize, logoSize, {
324
- fit: "contain",
325
- background: { r: 0, g: 0, b: 0, alpha: 0 },
326
- })
327
- .extend({
328
- top: padding,
329
- bottom: padding,
330
- left: padding,
331
- right: padding,
332
- background: { r: 0, g: 0, b: 0, alpha: 0 },
333
- })
334
- .toFile(outputPath);
335
- }
336
-
337
297
  // 기존 아이콘 파일 삭제
338
- private async _cleanupExistingIconsAsync(capacitorPath: string): Promise<void> {
339
- const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
340
-
341
- if (!FsUtils.exists(androidResPath)) return;
342
-
343
- // mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
344
- const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
345
- for (const dir of mipmapDirs) {
346
- const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
347
- for (const file of iconFiles) {
348
- await FsUtils.removeAsync(file);
349
- }
350
- }
351
-
352
- // drawable 폴더의 splash/icon 관련 파일도 삭제
353
- const drawableDirs = await FsUtils.globAsync(path.resolve(androidResPath, "drawable*"));
354
- for (const dir of drawableDirs) {
355
- const splashFiles = await FsUtils.globAsync(path.resolve(dir, "splash*"));
356
- const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*"));
357
- for (const file of [...splashFiles, ...iconFiles]) {
358
- await FsUtils.removeAsync(file);
359
- }
360
- }
361
- }
362
-
363
- // 7. Android 네이티브 설정
364
- private async _configureAndroidNativeAsync(capacitorPath: string) {
365
- const androidPath = path.resolve(capacitorPath, "android");
366
-
367
- if (!FsUtils.exists(androidPath)) {
368
- return;
369
- }
370
-
371
- // JAVA_HOME 찾기
372
- await this._configureAndroidGradlePropertiesAsync(androidPath);
373
-
374
- // local.properties 생성
375
- await this._configureSdkPathAsync(androidPath);
298
+ // private async _cleanupExistingIconsAsync(): Promise<void> {
299
+ // const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
300
+ //
301
+ // if (!FsUtils.exists(androidResPath)) return;
302
+ //
303
+ // // mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
304
+ // const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
305
+ // for (const dir of mipmapDirs) {
306
+ // const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
307
+ // for (const file of iconFiles) {
308
+ // await FsUtils.removeAsync(file);
309
+ // }
310
+ // }
311
+ //
312
+ // // drawable 폴더의 splash/icon 관련 파일도 삭제
313
+ // const drawableDirs = await FsUtils.globAsync(path.resolve(androidResPath, "drawable*"));
314
+ // for (const dir of drawableDirs) {
315
+ // const splashFiles = await FsUtils.globAsync(path.resolve(dir, "splash*"));
316
+ // const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*"));
317
+ // for (const file of [...splashFiles, ...iconFiles]) {
318
+ // await FsUtils.removeAsync(file);
319
+ // }
320
+ // }
321
+ // }
322
+
323
+ // 5. Android 네이티브 설정
324
+ private async _configureAndroidAsync() {
325
+ const androidPath = path.resolve(this._capPath, "android");
326
+
327
+ // JAVA_HOME 경로 설정
328
+ await this._configureAndroidJavaHomePathAsync(androidPath);
329
+
330
+ // SDK 경로 설정
331
+ await this._configureAndroidSdkPathAsync(androidPath);
376
332
 
377
333
  // AndroidManifest.xml 수정
378
334
  await this._configureAndroidManifestAsync(androidPath);
@@ -380,67 +336,28 @@ export class SdCliCapacitor {
380
336
  // build.gradle 수정 (필요시)
381
337
  await this._configureAndroidBuildGradleAsync(androidPath);
382
338
 
383
- // strings.xml 앱 이름 수정
384
- await this._configureAndroidStringsAsync(androidPath);
385
-
386
- // styles.xml 수정
387
- await this._configureAndroidStylesAsync(androidPath);
388
- }
389
-
390
- private async _configureAndroidStylesAsync(androidPath: string) {
391
- const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
392
-
393
- if (!FsUtils.exists(stylesPath)) {
394
- return;
395
- }
396
-
397
- let stylesContent = await FsUtils.readFileAsync(stylesPath);
398
-
399
- // Edge-to-Edge 비활성화만
400
- if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
401
- stylesContent = stylesContent.replace(
402
- /(<style[^>]*name="AppTheme"[^>]*>)/,
403
- `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
404
- );
405
- }
406
-
407
- // NoActionBarLaunch를 단순 NoActionBar로
408
- stylesContent = stylesContent.replace(
409
- /(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/,
410
- `$1Theme.AppCompat.Light.NoActionBar$2`,
411
- );
339
+ // TODO: strings.xml 앱 이름 수정?? WHY?
340
+ // await this._configureAndroidStringsAsync(androidPath);
412
341
 
413
- // splash 관련 전부 제거
414
- stylesContent = stylesContent.replace(
415
- /\s*<item name="android:background">@drawable\/splash<\/item>/g,
416
- "",
417
- );
418
- stylesContent = stylesContent.replace(
419
- /\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g,
420
- "",
421
- );
422
-
423
- await FsUtils.writeFileAsync(stylesPath, stylesContent);
342
+ // TODO: styles.xml 수정?? WHY?
343
+ // await this._configureAndroidStylesAsync(androidPath);
424
344
  }
425
345
 
426
- private async _configureAndroidGradlePropertiesAsync(androidPath: string) {
346
+ // JAVA_HOME 경로 설정
347
+ private async _configureAndroidJavaHomePathAsync(androidPath: string) {
427
348
  const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
428
349
 
429
- if (!FsUtils.exists(gradlePropsPath)) {
430
- return;
431
- }
432
-
433
350
  let content = await FsUtils.readFileAsync(gradlePropsPath);
434
351
 
435
352
  // Java 21 경로 자동 탐색
436
- const java21Path = this._findJava21();
353
+ const java21Path = await this._findJava21Async();
437
354
  if (java21Path != null && !content.includes("org.gradle.java.home")) {
438
355
  content += `\norg.gradle.java.home=${java21Path.replace(/\\/g, "\\\\")}\n`;
439
356
  FsUtils.writeFile(gradlePropsPath, content);
440
357
  }
441
358
  }
442
359
 
443
- private _findJava21(): string | undefined {
360
+ private async _findJava21Async(): Promise<string | undefined> {
444
361
  const patterns = [
445
362
  "C:/Program Files/Amazon Corretto/jdk21*",
446
363
  "C:/Program Files/Eclipse Adoptium/jdk-21*",
@@ -449,7 +366,7 @@ export class SdCliCapacitor {
449
366
  ];
450
367
 
451
368
  for (const pattern of patterns) {
452
- const matches = FsUtils.glob(pattern);
369
+ const matches = await FsUtils.globAsync(pattern);
453
370
  if (matches.length > 0) {
454
371
  // 가장 최신 버전 선택 (정렬 후 마지막)
455
372
  return matches.sort().at(-1);
@@ -459,14 +376,10 @@ export class SdCliCapacitor {
459
376
  return undefined;
460
377
  }
461
378
 
462
- private async _configureSdkPathAsync(androidPath: string) {
463
- // local.properties 생성
379
+ // SDK 경로 설정
380
+ private async _configureAndroidSdkPathAsync(androidPath: string) {
464
381
  const localPropsPath = path.resolve(androidPath, "local.properties");
465
382
 
466
- if (FsUtils.exists(localPropsPath)) {
467
- return;
468
- }
469
-
470
383
  // SDK 경로 탐색 (Cordova 방식과 유사)
471
384
  const sdkPath = this._findAndroidSdk();
472
385
  if (sdkPath != null) {
@@ -474,13 +387,34 @@ export class SdCliCapacitor {
474
387
  }
475
388
  }
476
389
 
477
- private async _configureAndroidManifestAsync(androidPath: string) {
478
- const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
390
+ private _findAndroidSdk(): string | undefined {
391
+ // 1. 환경변수 확인
392
+ const fromEnv = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"];
393
+ if (fromEnv != null && FsUtils.exists(fromEnv)) {
394
+ return fromEnv;
395
+ }
479
396
 
480
- if (!FsUtils.exists(manifestPath)) {
481
- return;
397
+ // 2. 일반적인 설치 경로 탐색
398
+ const candidates = [
399
+ path.resolve(process.env["LOCALAPPDATA"] ?? "", "Android/Sdk"),
400
+ path.resolve(process.env["HOME"] ?? "", "Android/Sdk"),
401
+ "C:/Program Files/Android/Sdk",
402
+ "C:/Android/Sdk",
403
+ ];
404
+
405
+ for (const candidate of candidates) {
406
+ if (FsUtils.exists(candidate)) {
407
+ return candidate;
408
+ }
482
409
  }
483
410
 
411
+ return undefined;
412
+ }
413
+
414
+ // AndroidManifest.xml 수정
415
+ private async _configureAndroidManifestAsync(androidPath: string) {
416
+ const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
417
+
484
418
  let manifestContent = await FsUtils.readFileAsync(manifestPath);
485
419
 
486
420
  // usesCleartextTraffic 설정
@@ -547,13 +481,10 @@ export class SdCliCapacitor {
547
481
  await FsUtils.writeFileAsync(manifestPath, manifestContent);
548
482
  }
549
483
 
484
+ // build.gradle 수정 (필요시)
550
485
  private async _configureAndroidBuildGradleAsync(androidPath: string) {
551
486
  const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
552
487
 
553
- if (!FsUtils.exists(buildGradlePath)) {
554
- return;
555
- }
556
-
557
488
  let gradleContent = await FsUtils.readFileAsync(buildGradlePath);
558
489
 
559
490
  // versionName, versionCode 설정
@@ -570,17 +501,31 @@ export class SdCliCapacitor {
570
501
  // SDK 버전 설정
571
502
  if (this._opt.config.platform?.android?.sdkVersion != null) {
572
503
  const sdkVersion = this._opt.config.platform.android.sdkVersion;
573
- gradleContent = gradleContent.replace(/minSdkVersion \d+/, `minSdkVersion ${sdkVersion}`);
504
+ gradleContent = gradleContent.replace(/minSdkVersion .+/, `minSdkVersion ${sdkVersion}`);
574
505
  gradleContent = gradleContent.replace(
575
- /targetSdkVersion \d+/,
506
+ /targetSdkVersion .+/,
576
507
  `targetSdkVersion ${sdkVersion}`,
577
508
  );
509
+ } else {
510
+ gradleContent = gradleContent.replace(
511
+ /minSdkVersion .+/,
512
+ `minSdkVersion rootProject.ext.minSdkVersion`,
513
+ );
514
+ gradleContent = gradleContent.replace(
515
+ /targetSdkVersion .+/,
516
+ `targetSdkVersion rootProject.ext.targetSdkVersion`,
517
+ );
578
518
  }
579
519
 
580
520
  // Signing 설정
521
+ const keystorePath = path.resolve(this._capPath, this._ANDROID_KEYSTORE_FILE_NAME);
581
522
  const signConfig = this._opt.config.platform?.android?.sign;
582
523
  if (signConfig) {
583
- const keystoreRelativePath = `../../${this._KEYSTORE_FILE_NAME}`;
524
+ await FsUtils.copyAsync(path.resolve(this._opt.pkgPath, signConfig.keystore), keystorePath);
525
+
526
+ const keystoreRelativePath = path
527
+ .relative(path.dirname(buildGradlePath), keystorePath)
528
+ .replace(/\\/g, "/");
584
529
  const keystoreType = signConfig.keystoreType ?? "jks";
585
530
 
586
531
  // signingConfigs 블록 추가
@@ -610,12 +555,15 @@ export class SdCliCapacitor {
610
555
  `$1\n signingConfig signingConfigs.release`,
611
556
  );
612
557
  }
558
+ } else {
559
+ //TODO: gradleContent에서 signingConfigs 관련 부분 삭제
560
+ await FsUtils.removeAsync(keystorePath);
613
561
  }
614
562
 
615
563
  await FsUtils.writeFileAsync(buildGradlePath, gradleContent);
616
564
  }
617
565
 
618
- private async _configureAndroidStringsAsync(androidPath: string) {
566
+ /*private async _configureAndroidStringsAsync(androidPath: string) {
619
567
  const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
620
568
 
621
569
  if (!FsUtils.exists(stringsPath)) {
@@ -641,31 +589,64 @@ export class SdCliCapacitor {
641
589
  );
642
590
 
643
591
  await FsUtils.writeFileAsync(stringsPath, stringsContent);
644
- }
592
+ }*/
593
+
594
+ /*private async _configureAndroidStylesAsync(androidPath: string) {
595
+ const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
596
+
597
+ if (!FsUtils.exists(stylesPath)) {
598
+ return;
599
+ }
600
+
601
+ let stylesContent = await FsUtils.readFileAsync(stylesPath);
602
+
603
+ // Edge-to-Edge 비활성화만
604
+ if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
605
+ stylesContent = stylesContent.replace(
606
+ /(<style[^>]*name="AppTheme"[^>]*>)/,
607
+ `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
608
+ );
609
+ }
610
+
611
+ // NoActionBarLaunch를 단순 NoActionBar로
612
+ stylesContent = stylesContent.replace(
613
+ /(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/,
614
+ `$1Theme.AppCompat.Light.NoActionBar$2`,
615
+ );
616
+
617
+ // splash 관련 전부 제거
618
+ stylesContent = stylesContent.replace(
619
+ /\s*<item name="android:background">@drawable\/splash<\/item>/g,
620
+ "",
621
+ );
622
+ stylesContent = stylesContent.replace(
623
+ /\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g,
624
+ "",
625
+ );
626
+
627
+ await FsUtils.writeFileAsync(stylesPath, stylesContent);
628
+ }*/
645
629
 
646
630
  async buildAsync(outPath: string): Promise<void> {
647
- const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
648
631
  const buildType = this._opt.config.debug ? "debug" : "release";
649
632
 
650
633
  // 플랫폼별 빌드
651
634
  await Promise.all(
652
635
  this._platforms.map(async (platform) => {
653
636
  // 해당 플랫폼만 copy
654
- await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
637
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], this._capPath);
655
638
 
656
639
  if (platform === "android") {
657
- await this._buildAndroidAsync(capacitorPath, outPath, buildType);
640
+ await this._buildAndroidAsync(outPath, buildType);
641
+ } else {
642
+ throw new NotImplementError();
658
643
  }
659
644
  }),
660
645
  );
661
646
  }
662
647
 
663
- private async _buildAndroidAsync(
664
- capacitorPath: string,
665
- outPath: string,
666
- buildType: string,
667
- ): Promise<void> {
668
- const androidPath = path.resolve(capacitorPath, "android");
648
+ private async _buildAndroidAsync(outPath: string, buildType: string): Promise<void> {
649
+ const androidPath = path.resolve(this._capPath, "android");
669
650
  const targetOutPath = path.resolve(outPath, "android");
670
651
 
671
652
  const isBundle = this._opt.config.platform?.android?.bundle;
@@ -731,30 +712,6 @@ export class SdCliCapacitor {
731
712
  );
732
713
  }
733
714
 
734
- private _findAndroidSdk(): string | undefined {
735
- // 1. 환경변수 확인
736
- const fromEnv = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"];
737
- if (fromEnv != null && FsUtils.exists(fromEnv)) {
738
- return fromEnv;
739
- }
740
-
741
- // 2. 일반적인 설치 경로 탐색
742
- const candidates = [
743
- path.resolve(process.env["LOCALAPPDATA"] ?? "", "Android/Sdk"),
744
- path.resolve(process.env["HOME"] ?? "", "Android/Sdk"),
745
- "C:/Program Files/Android/Sdk",
746
- "C:/Android/Sdk",
747
- ];
748
-
749
- for (const candidate of candidates) {
750
- if (FsUtils.exists(candidate)) {
751
- return candidate;
752
- }
753
- }
754
-
755
- return undefined;
756
- }
757
-
758
715
  static async runWebviewOnDeviceAsync(opt: {
759
716
  platform: string;
760
717
  package: string;
@@ -798,7 +755,7 @@ export class SdCliCapacitor {
798
755
  try {
799
756
  await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
800
757
  } catch (err) {
801
- await SdProcess.spawnAsync("adb", ["kill-server"]);
758
+ await this._execAsync("adb", ["kill-server"], capacitorPath);
802
759
  throw err;
803
760
  }
804
761
  }