@simplysm/sd-cli 12.16.14 → 12.16.16

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