@simplysm/sd-cli 12.16.4 → 12.16.6
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.
|
@@ -15,20 +15,26 @@ export declare class SdCliCapacitor {
|
|
|
15
15
|
private static _execAsync;
|
|
16
16
|
initializeAsync(): Promise<void>;
|
|
17
17
|
private _initializeCapacitorProjectAsync;
|
|
18
|
-
private
|
|
19
|
-
private
|
|
18
|
+
private _syncVersionAsync;
|
|
19
|
+
private _createCapacitorConfigAsync;
|
|
20
20
|
private _managePlatformsAsync;
|
|
21
21
|
private _managePluginsAsync;
|
|
22
|
-
private
|
|
22
|
+
private _setupAndroidSignAsync;
|
|
23
23
|
private _setupIconAndSplashScreenAsync;
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
24
|
+
private _createPaddedIconAsync;
|
|
25
|
+
private _configureAndroidNativeAsync;
|
|
26
|
+
private _configureAndroidStylesAsync;
|
|
27
|
+
private _configureAndroidGradlePropertiesAsync;
|
|
28
|
+
private _findJava21;
|
|
29
|
+
private _configureSdkPathAsync;
|
|
30
|
+
private _configureAndroidManifestAsync;
|
|
31
|
+
private _configureAndroidBuildGradleAsync;
|
|
32
|
+
private _configureAndroidStringsAsync;
|
|
28
33
|
buildAsync(outPath: string): Promise<void>;
|
|
29
34
|
private _buildPlatformAsync;
|
|
30
35
|
private _buildAndroidAsync;
|
|
31
|
-
private
|
|
36
|
+
private _copyAndroidBuildOutputAsync;
|
|
37
|
+
private _findAndroidSdk;
|
|
32
38
|
static runWebviewOnDeviceAsync(opt: {
|
|
33
39
|
platform: string;
|
|
34
40
|
package: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import { FsUtils, PathUtils, SdLogger, SdProcess } from "@simplysm/sd-core-node";
|
|
3
3
|
import { StringUtils, typescript } from "@simplysm/sd-core-common";
|
|
4
|
+
import sharp from "sharp";
|
|
4
5
|
export class SdCliCapacitor {
|
|
5
6
|
constructor(_opt) {
|
|
6
7
|
this._opt = _opt;
|
|
@@ -9,7 +10,7 @@ export class SdCliCapacitor {
|
|
|
9
10
|
this._CONFIG_FILE_NAME = "capacitor.config.ts";
|
|
10
11
|
this._KEYSTORE_FILE_NAME = "android.keystore";
|
|
11
12
|
this._ICON_DIR_PATH = "resources";
|
|
12
|
-
this._platforms = Object.keys(this._opt.config.platform ?? {
|
|
13
|
+
this._platforms = Object.keys(this._opt.config.platform ?? {});
|
|
13
14
|
this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
|
|
14
15
|
}
|
|
15
16
|
static { this._logger = SdLogger.get(["simplysm", "sd-cli", "SdCliCapacitor"]); }
|
|
@@ -23,65 +24,77 @@ export class SdCliCapacitor {
|
|
|
23
24
|
// 1. Capacitor 프로젝트 초기화
|
|
24
25
|
await this._initializeCapacitorProjectAsync(capacitorPath);
|
|
25
26
|
// 2. Capacitor 설정 파일 생성
|
|
26
|
-
this.
|
|
27
|
+
await this._createCapacitorConfigAsync(capacitorPath);
|
|
27
28
|
// 3. 플랫폼 관리
|
|
28
29
|
await this._managePlatformsAsync(capacitorPath);
|
|
29
30
|
// 4. 플러그인 관리
|
|
30
31
|
await this._managePluginsAsync(capacitorPath);
|
|
31
32
|
// 5. 안드로이드 서명 설정
|
|
32
|
-
this.
|
|
33
|
+
await this._setupAndroidSignAsync(capacitorPath);
|
|
33
34
|
// 6. 아이콘 및 스플래시 스크린 설정
|
|
34
35
|
await this._setupIconAndSplashScreenAsync(capacitorPath);
|
|
35
36
|
// 7. Android 네이티브 설정 (AndroidManifest.xml, build.gradle 등)
|
|
36
37
|
if (this._platforms.includes("android")) {
|
|
37
|
-
this.
|
|
38
|
+
await this._configureAndroidNativeAsync(capacitorPath);
|
|
38
39
|
}
|
|
39
40
|
// 8. 웹 자산 동기화
|
|
40
41
|
await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
|
|
41
42
|
}
|
|
42
43
|
// 1. Capacitor 프로젝트 초기화
|
|
43
44
|
async _initializeCapacitorProjectAsync(capacitorPath) {
|
|
44
|
-
if (FsUtils.exists(capacitorPath)) {
|
|
45
|
+
if (FsUtils.exists(path.resolve(capacitorPath, "www"))) {
|
|
45
46
|
SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
|
|
46
47
|
// 버전 동기화
|
|
47
|
-
this.
|
|
48
|
+
await this._syncVersionAsync(capacitorPath);
|
|
48
49
|
}
|
|
49
50
|
else {
|
|
50
|
-
FsUtils.
|
|
51
|
+
await FsUtils.mkdirsAsync(capacitorPath);
|
|
51
52
|
// package.json 생성
|
|
53
|
+
const projNpmConfig = await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "../../package.json"));
|
|
52
54
|
const pkgJson = {
|
|
53
55
|
name: this._opt.config.appId,
|
|
54
56
|
version: this._npmConfig.version,
|
|
55
57
|
private: true,
|
|
58
|
+
volta: projNpmConfig.volta,
|
|
56
59
|
dependencies: {
|
|
57
60
|
"@capacitor/core": "^7.0.0",
|
|
58
61
|
},
|
|
59
62
|
devDependencies: {
|
|
60
63
|
"@capacitor/cli": "^7.0.0",
|
|
61
64
|
"@capacitor/assets": "^3.0.0",
|
|
65
|
+
...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
|
|
62
66
|
},
|
|
63
67
|
};
|
|
64
|
-
FsUtils.
|
|
68
|
+
await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
|
|
69
|
+
space: 2,
|
|
70
|
+
});
|
|
71
|
+
// .yarnrc.yml 작성
|
|
72
|
+
await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
|
|
73
|
+
// yarn.lock 작성
|
|
74
|
+
await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
|
|
65
75
|
// yarn install
|
|
66
76
|
await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
|
|
67
77
|
// capacitor init
|
|
68
78
|
await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
|
|
69
79
|
}
|
|
80
|
+
// www/index.html 생성
|
|
81
|
+
const wwwPath = path.resolve(capacitorPath, "www");
|
|
82
|
+
await FsUtils.writeFileAsync(path.resolve(wwwPath, "index.html"), "<!DOCTYPE html><html><head></head><body></body></html>");
|
|
70
83
|
}
|
|
71
84
|
// 버전 동기화
|
|
72
|
-
|
|
85
|
+
async _syncVersionAsync(capacitorPath) {
|
|
73
86
|
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
74
87
|
if (FsUtils.exists(pkgJsonPath)) {
|
|
75
|
-
const pkgJson = FsUtils.
|
|
88
|
+
const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
|
|
76
89
|
if (pkgJson.version !== this._npmConfig.version) {
|
|
77
90
|
pkgJson.version = this._npmConfig.version;
|
|
78
|
-
FsUtils.
|
|
79
|
-
SdCliCapacitor._logger.
|
|
91
|
+
await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
|
|
92
|
+
SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
// 2. Capacitor 설정 파일 생성
|
|
84
|
-
|
|
97
|
+
async _createCapacitorConfigAsync(capacitorPath) {
|
|
85
98
|
const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
|
|
86
99
|
// 플러그인 옵션 생성
|
|
87
100
|
const pluginOptions = {};
|
|
@@ -108,13 +121,14 @@ export class SdCliCapacitor {
|
|
|
108
121
|
},
|
|
109
122
|
android: {
|
|
110
123
|
allowMixedContent: true,
|
|
124
|
+
statusBarOverlaysWebView: false,
|
|
111
125
|
},
|
|
112
126
|
plugins: ${pluginsConfigStr},
|
|
113
127
|
};
|
|
114
128
|
|
|
115
129
|
export default config;
|
|
116
130
|
`;
|
|
117
|
-
FsUtils.
|
|
131
|
+
await FsUtils.writeFileAsync(configFilePath, configContent);
|
|
118
132
|
}
|
|
119
133
|
// 3. 플랫폼 관리
|
|
120
134
|
async _managePlatformsAsync(capacitorPath) {
|
|
@@ -127,7 +141,7 @@ export class SdCliCapacitor {
|
|
|
127
141
|
// 4. 플러그인 관리
|
|
128
142
|
async _managePluginsAsync(capacitorPath) {
|
|
129
143
|
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
130
|
-
const pkgJson = FsUtils.
|
|
144
|
+
const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
|
|
131
145
|
const currentDeps = Object.keys(pkgJson.dependencies ?? {});
|
|
132
146
|
const usePlugins = Object.keys(this._opt.config.plugins ?? {});
|
|
133
147
|
// 사용하지 않는 플러그인 제거
|
|
@@ -141,50 +155,51 @@ export class SdCliCapacitor {
|
|
|
141
155
|
if (!usePlugins.includes(dep)) {
|
|
142
156
|
// Capacitor 관련 플러그인만 제거
|
|
143
157
|
if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
SdCliCapacitor._logger.log(`플러그인 제거: ${dep}`);
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
SdCliCapacitor._logger.warn(`플러그인 제거 실패: ${dep}`);
|
|
150
|
-
}
|
|
158
|
+
await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
|
|
159
|
+
SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
163
|
// 새 플러그인 설치
|
|
164
|
+
const mainPkgJson = this._npmConfig;
|
|
165
|
+
const mainDeps = {
|
|
166
|
+
...mainPkgJson.dependencies,
|
|
167
|
+
...mainPkgJson.devDependencies,
|
|
168
|
+
...mainPkgJson.peerDependencies,
|
|
169
|
+
};
|
|
155
170
|
for (const plugin of usePlugins) {
|
|
156
171
|
if (!currentDeps.includes(plugin)) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
|
|
163
|
-
}
|
|
172
|
+
// 메인 프로젝트에 버전이 있으면 그 버전으로 설치
|
|
173
|
+
const version = mainDeps[plugin];
|
|
174
|
+
const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
|
|
175
|
+
await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
|
|
176
|
+
SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
|
|
164
177
|
}
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
180
|
// 5. 안드로이드 서명 설정
|
|
168
|
-
|
|
181
|
+
async _setupAndroidSignAsync(capacitorPath) {
|
|
169
182
|
const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
|
|
170
183
|
if (this._opt.config.platform?.android?.sign) {
|
|
171
|
-
FsUtils.
|
|
184
|
+
await FsUtils.copyAsync(path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore), keystorePath);
|
|
172
185
|
}
|
|
173
186
|
else {
|
|
174
|
-
FsUtils.
|
|
187
|
+
await FsUtils.removeAsync(keystorePath);
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
190
|
// 6. 아이콘 및 스플래시 스크린 설정
|
|
178
191
|
async _setupIconAndSplashScreenAsync(capacitorPath) {
|
|
179
192
|
const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
|
|
180
|
-
// ICON 파일 복사
|
|
181
193
|
if (this._opt.config.icon != null) {
|
|
182
|
-
FsUtils.
|
|
194
|
+
await FsUtils.mkdirsAsync(iconDirPath);
|
|
183
195
|
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
// Adaptive Icon용 여백 추가된 이미지 생성
|
|
197
|
+
// 1024x1024 중 680x680이 safe zone (약 66%)
|
|
198
|
+
const paddedIconPath = path.resolve(iconDirPath, "icon-only.png");
|
|
199
|
+
await this._createPaddedIconAsync(iconSource, paddedIconPath);
|
|
200
|
+
// splash, icon 복사
|
|
201
|
+
await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "icon.png"));
|
|
202
|
+
await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
|
|
188
203
|
try {
|
|
189
204
|
await SdCliCapacitor._execAsync("npx", ["@capacitor/assets", "generate", "--android"], capacitorPath);
|
|
190
205
|
}
|
|
@@ -193,28 +208,102 @@ export class SdCliCapacitor {
|
|
|
193
208
|
}
|
|
194
209
|
}
|
|
195
210
|
else {
|
|
196
|
-
FsUtils.
|
|
211
|
+
await FsUtils.removeAsync(iconDirPath);
|
|
197
212
|
}
|
|
198
213
|
}
|
|
214
|
+
async _createPaddedIconAsync(sourcePath, outputPath) {
|
|
215
|
+
const size = 1024;
|
|
216
|
+
const iconSize = 680; // safe zone
|
|
217
|
+
const padding = Math.floor((size - iconSize) / 2); // 172px
|
|
218
|
+
await sharp(sourcePath)
|
|
219
|
+
.resize(iconSize, iconSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
220
|
+
.extend({
|
|
221
|
+
top: padding,
|
|
222
|
+
bottom: padding,
|
|
223
|
+
left: padding,
|
|
224
|
+
right: padding,
|
|
225
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }, // 투명
|
|
226
|
+
})
|
|
227
|
+
.toFile(outputPath);
|
|
228
|
+
}
|
|
199
229
|
// 7. Android 네이티브 설정
|
|
200
|
-
|
|
230
|
+
async _configureAndroidNativeAsync(capacitorPath) {
|
|
201
231
|
const androidPath = path.resolve(capacitorPath, "android");
|
|
202
232
|
if (!FsUtils.exists(androidPath)) {
|
|
203
233
|
return;
|
|
204
234
|
}
|
|
235
|
+
// JAVA_HOME 찾기
|
|
236
|
+
await this._configureAndroidGradlePropertiesAsync(androidPath);
|
|
237
|
+
// local.properties 생성
|
|
238
|
+
await this._configureSdkPathAsync(androidPath);
|
|
205
239
|
// AndroidManifest.xml 수정
|
|
206
|
-
this.
|
|
240
|
+
await this._configureAndroidManifestAsync(androidPath);
|
|
207
241
|
// build.gradle 수정 (필요시)
|
|
208
|
-
this.
|
|
242
|
+
await this._configureAndroidBuildGradleAsync(androidPath);
|
|
209
243
|
// strings.xml 앱 이름 수정
|
|
210
|
-
this.
|
|
244
|
+
await this._configureAndroidStringsAsync(androidPath);
|
|
245
|
+
// styles.xml 수정
|
|
246
|
+
await this._configureAndroidStylesAsync(androidPath);
|
|
247
|
+
}
|
|
248
|
+
async _configureAndroidStylesAsync(androidPath) {
|
|
249
|
+
const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
|
|
250
|
+
if (!FsUtils.exists(stylesPath)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
let stylesContent = await FsUtils.readFileAsync(stylesPath);
|
|
254
|
+
// Edge-to-Edge 비활성화
|
|
255
|
+
if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
|
|
256
|
+
stylesContent = stylesContent.replace(/(<style[^>]*AppTheme[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
|
|
257
|
+
}
|
|
258
|
+
await FsUtils.writeFileAsync(stylesPath, stylesContent);
|
|
259
|
+
}
|
|
260
|
+
async _configureAndroidGradlePropertiesAsync(androidPath) {
|
|
261
|
+
const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
|
|
262
|
+
if (!FsUtils.exists(gradlePropsPath)) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
let content = await FsUtils.readFileAsync(gradlePropsPath);
|
|
266
|
+
// Java 21 경로 자동 탐색
|
|
267
|
+
const java21Path = this._findJava21();
|
|
268
|
+
if (java21Path != null && !content.includes("org.gradle.java.home")) {
|
|
269
|
+
content += `\norg.gradle.java.home=${java21Path.replace(/\\/g, "\\\\")}\n`;
|
|
270
|
+
FsUtils.writeFile(gradlePropsPath, content);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
_findJava21() {
|
|
274
|
+
const patterns = [
|
|
275
|
+
"C:/Program Files/Amazon Corretto/jdk21*",
|
|
276
|
+
"C:/Program Files/Eclipse Adoptium/jdk-21*",
|
|
277
|
+
"C:/Program Files/Java/jdk-21*",
|
|
278
|
+
"C:/Program Files/Microsoft/jdk-21*",
|
|
279
|
+
];
|
|
280
|
+
for (const pattern of patterns) {
|
|
281
|
+
const matches = FsUtils.glob(pattern);
|
|
282
|
+
if (matches.length > 0) {
|
|
283
|
+
// 가장 최신 버전 선택 (정렬 후 마지막)
|
|
284
|
+
return matches.sort().at(-1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
211
288
|
}
|
|
212
|
-
|
|
289
|
+
async _configureSdkPathAsync(androidPath) {
|
|
290
|
+
// local.properties 생성
|
|
291
|
+
const localPropsPath = path.resolve(androidPath, "local.properties");
|
|
292
|
+
if (FsUtils.exists(localPropsPath)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// SDK 경로 탐색 (Cordova 방식과 유사)
|
|
296
|
+
const sdkPath = this._findAndroidSdk();
|
|
297
|
+
if (sdkPath != null) {
|
|
298
|
+
await FsUtils.writeFileAsync(localPropsPath, `sdk.dir=${sdkPath.replace(/\\/g, "/")}\n`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async _configureAndroidManifestAsync(androidPath) {
|
|
213
302
|
const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
|
|
214
303
|
if (!FsUtils.exists(manifestPath)) {
|
|
215
304
|
return;
|
|
216
305
|
}
|
|
217
|
-
let manifestContent = FsUtils.
|
|
306
|
+
let manifestContent = await FsUtils.readFileAsync(manifestPath);
|
|
218
307
|
// usesCleartextTraffic 설정
|
|
219
308
|
if (!manifestContent.includes("android:usesCleartextTraffic")) {
|
|
220
309
|
manifestContent = manifestContent.replace("<application", '<application android:usesCleartextTraffic="true"');
|
|
@@ -244,14 +333,28 @@ export class SdCliCapacitor {
|
|
|
244
333
|
}
|
|
245
334
|
}
|
|
246
335
|
}
|
|
247
|
-
|
|
336
|
+
// intentFilters 설정
|
|
337
|
+
const intentFilters = this._opt.config.platform?.android?.intentFilters ?? [];
|
|
338
|
+
for (const filter of intentFilters) {
|
|
339
|
+
const filterKey = filter.action ?? filter.category ?? "";
|
|
340
|
+
if (filterKey && !manifestContent.includes(filterKey)) {
|
|
341
|
+
const actionLine = filter.action != null ? `<action android:name="${filter.action}"/>` : "";
|
|
342
|
+
const categoryLine = filter.category != null ? `<category android:name="${filter.category}"/>` : "";
|
|
343
|
+
manifestContent = manifestContent.replace(/(<activity[\s\S]*?android:name="\.MainActivity"[\s\S]*?>)/, `$1
|
|
344
|
+
<intent-filter>
|
|
345
|
+
${actionLine}
|
|
346
|
+
${categoryLine}
|
|
347
|
+
</intent-filter>`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
await FsUtils.writeFileAsync(manifestPath, manifestContent);
|
|
248
351
|
}
|
|
249
|
-
|
|
352
|
+
async _configureAndroidBuildGradleAsync(androidPath) {
|
|
250
353
|
const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
|
|
251
354
|
if (!FsUtils.exists(buildGradlePath)) {
|
|
252
355
|
return;
|
|
253
356
|
}
|
|
254
|
-
let gradleContent = FsUtils.
|
|
357
|
+
let gradleContent = await FsUtils.readFileAsync(buildGradlePath);
|
|
255
358
|
// versionName, versionCode 설정
|
|
256
359
|
const version = this._npmConfig.version;
|
|
257
360
|
const versionParts = version.split(".");
|
|
@@ -292,19 +395,19 @@ export class SdCliCapacitor {
|
|
|
292
395
|
gradleContent = gradleContent.replace(/(buildTypes\s*\{[\s\S]*?release\s*\{)/, `$1\n signingConfig signingConfigs.release`);
|
|
293
396
|
}
|
|
294
397
|
}
|
|
295
|
-
FsUtils.
|
|
398
|
+
await FsUtils.writeFileAsync(buildGradlePath, gradleContent);
|
|
296
399
|
}
|
|
297
|
-
|
|
400
|
+
async _configureAndroidStringsAsync(androidPath) {
|
|
298
401
|
const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
|
|
299
402
|
if (!FsUtils.exists(stringsPath)) {
|
|
300
403
|
return;
|
|
301
404
|
}
|
|
302
|
-
let stringsContent = FsUtils.
|
|
405
|
+
let stringsContent = await FsUtils.readFileAsync(stringsPath);
|
|
303
406
|
stringsContent = stringsContent.replace(/<string name="app_name">[^<]+<\/string>/, `<string name="app_name">${this._opt.config.appName}</string>`);
|
|
304
407
|
stringsContent = stringsContent.replace(/<string name="title_activity_main">[^<]+<\/string>/, `<string name="title_activity_main">${this._opt.config.appName}</string>`);
|
|
305
408
|
stringsContent = stringsContent.replace(/<string name="package_name">[^<]+<\/string>/, `<string name="package_name">${this._opt.config.appId}</string>`);
|
|
306
409
|
stringsContent = stringsContent.replace(/<string name="custom_url_scheme">[^<]+<\/string>/, `<string name="custom_url_scheme">${this._opt.config.appId}</string>`);
|
|
307
|
-
FsUtils.
|
|
410
|
+
await FsUtils.writeFileAsync(stringsPath, stringsContent);
|
|
308
411
|
}
|
|
309
412
|
async buildAsync(outPath) {
|
|
310
413
|
const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
|
|
@@ -340,9 +443,9 @@ export class SdCliCapacitor {
|
|
|
340
443
|
const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
|
|
341
444
|
await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
|
|
342
445
|
// 빌드 결과물 복사
|
|
343
|
-
this.
|
|
446
|
+
await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
|
|
344
447
|
}
|
|
345
|
-
|
|
448
|
+
async _copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType) {
|
|
346
449
|
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
347
450
|
const isSigned = !!this._opt.config.platform?.android?.sign;
|
|
348
451
|
const ext = isBundle ? "aab" : "apk";
|
|
@@ -357,21 +460,41 @@ export class SdCliCapacitor {
|
|
|
357
460
|
return;
|
|
358
461
|
}
|
|
359
462
|
const outputFileName = `${this._opt.config.appName}${isSigned ? "" : "-unsigned"}-latest.${ext}`;
|
|
360
|
-
FsUtils.
|
|
361
|
-
FsUtils.
|
|
463
|
+
await FsUtils.mkdirsAsync(targetOutPath);
|
|
464
|
+
await FsUtils.copyAsync(actualPath, path.resolve(targetOutPath, outputFileName));
|
|
362
465
|
const updatesPath = path.resolve(targetOutPath, "updates");
|
|
363
|
-
FsUtils.
|
|
364
|
-
FsUtils.
|
|
466
|
+
await FsUtils.mkdirsAsync(updatesPath);
|
|
467
|
+
await FsUtils.copyAsync(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
|
|
468
|
+
}
|
|
469
|
+
_findAndroidSdk() {
|
|
470
|
+
// 1. 환경변수 확인
|
|
471
|
+
const fromEnv = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"];
|
|
472
|
+
if (fromEnv != null && FsUtils.exists(fromEnv)) {
|
|
473
|
+
return fromEnv;
|
|
474
|
+
}
|
|
475
|
+
// 2. 일반적인 설치 경로 탐색
|
|
476
|
+
const candidates = [
|
|
477
|
+
path.resolve(process.env["LOCALAPPDATA"] ?? "", "Android/Sdk"),
|
|
478
|
+
path.resolve(process.env["HOME"] ?? "", "Android/Sdk"),
|
|
479
|
+
"C:/Program Files/Android/Sdk",
|
|
480
|
+
"C:/Android/Sdk",
|
|
481
|
+
];
|
|
482
|
+
for (const candidate of candidates) {
|
|
483
|
+
if (FsUtils.exists(candidate)) {
|
|
484
|
+
return candidate;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return undefined;
|
|
365
488
|
}
|
|
366
489
|
static async runWebviewOnDeviceAsync(opt) {
|
|
367
|
-
const projNpmConf = FsUtils.
|
|
368
|
-
const allPkgPaths = projNpmConf.workspaces.
|
|
490
|
+
const projNpmConf = (await FsUtils.readJsonAsync(path.resolve(process.cwd(), "package.json")));
|
|
491
|
+
const allPkgPaths = await projNpmConf.workspaces.mapManyAsync(async (item) => await FsUtils.globAsync(PathUtils.posix(process.cwd(), item)));
|
|
369
492
|
const capacitorPath = path.resolve(allPkgPaths.single((item) => item.endsWith(opt.package)), ".capacitor");
|
|
370
493
|
if (opt.url !== undefined) {
|
|
371
494
|
// capacitor.config.ts의 server.url 설정 업데이트
|
|
372
495
|
const configPath = path.resolve(capacitorPath, "capacitor.config.ts");
|
|
373
496
|
if (FsUtils.exists(configPath)) {
|
|
374
|
-
let configContent = FsUtils.
|
|
497
|
+
let configContent = await FsUtils.readFileAsync(configPath);
|
|
375
498
|
const serverUrl = `${opt.url.replace(/\/$/, "")}/${opt.package}/capacitor/`;
|
|
376
499
|
// 기존 url 설정이 있으면 교체, 없으면 server 블록 첫 줄에 추가
|
|
377
500
|
if (configContent.includes("url:")) {
|
|
@@ -380,11 +503,16 @@ export class SdCliCapacitor {
|
|
|
380
503
|
else if (configContent.includes("server:")) {
|
|
381
504
|
configContent = configContent.replace(/server:\s*\{/, `server: {\n url: "${serverUrl}",`);
|
|
382
505
|
}
|
|
383
|
-
FsUtils.
|
|
506
|
+
await FsUtils.writeFileAsync(configPath, configContent);
|
|
384
507
|
}
|
|
385
508
|
}
|
|
386
509
|
// cap sync 후 run
|
|
387
510
|
await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
|
|
388
|
-
|
|
511
|
+
try {
|
|
512
|
+
await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
await SdProcess.spawnAsync("adb", ["kill-server"]);
|
|
516
|
+
}
|
|
389
517
|
}
|
|
390
518
|
}
|
|
@@ -114,6 +114,10 @@ export interface ISdClientBuilderCapacitorConfig {
|
|
|
114
114
|
android?: {
|
|
115
115
|
config?: Record<string, string>;
|
|
116
116
|
bundle?: boolean;
|
|
117
|
+
intentFilters?: {
|
|
118
|
+
action?: string;
|
|
119
|
+
category?: string;
|
|
120
|
+
}[];
|
|
117
121
|
sign?: {
|
|
118
122
|
keystore: string;
|
|
119
123
|
storePassword: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/sd-cli",
|
|
3
|
-
"version": "12.16.
|
|
3
|
+
"version": "12.16.6",
|
|
4
4
|
"description": "심플리즘 패키지 - CLI",
|
|
5
5
|
"author": "김석래",
|
|
6
6
|
"repository": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"@angular/compiler-cli": "^20.3.15",
|
|
18
18
|
"@anthropic-ai/sdk": "^0.71.2",
|
|
19
19
|
"@electron/rebuild": "^4.0.2",
|
|
20
|
-
"@simplysm/sd-core-common": "12.16.
|
|
21
|
-
"@simplysm/sd-core-node": "12.16.
|
|
22
|
-
"@simplysm/sd-service-server": "12.16.
|
|
23
|
-
"@simplysm/sd-storage": "12.16.
|
|
20
|
+
"@simplysm/sd-core-common": "12.16.6",
|
|
21
|
+
"@simplysm/sd-core-node": "12.16.6",
|
|
22
|
+
"@simplysm/sd-service-server": "12.16.6",
|
|
23
|
+
"@simplysm/sd-storage": "12.16.6",
|
|
24
24
|
"browserslist": "^4.28.1",
|
|
25
25
|
"cordova": "^13.0.0",
|
|
26
26
|
"electron": "^33.4.11",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"rxjs": "^7.8.2",
|
|
34
34
|
"sass-embedded": "^1.96.0",
|
|
35
35
|
"semver": "^7.7.3",
|
|
36
|
+
"sharp": "^0.34.5",
|
|
36
37
|
"specifier-resolution-node": "^1.1.4",
|
|
37
38
|
"ts-morph": "^27.0.2",
|
|
38
39
|
"tslib": "^2.8.1",
|
|
@@ -3,6 +3,7 @@ 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
5
|
import { StringUtils, typescript } from "@simplysm/sd-core-common";
|
|
6
|
+
import sharp from "sharp";
|
|
6
7
|
|
|
7
8
|
export class SdCliCapacitor {
|
|
8
9
|
// 상수 정의
|
|
@@ -18,7 +19,7 @@ export class SdCliCapacitor {
|
|
|
18
19
|
private readonly _npmConfig: INpmConfig;
|
|
19
20
|
|
|
20
21
|
constructor(private readonly _opt: { pkgPath: string; config: ISdClientBuilderCapacitorConfig }) {
|
|
21
|
-
this._platforms = Object.keys(this._opt.config.platform ?? {
|
|
22
|
+
this._platforms = Object.keys(this._opt.config.platform ?? {});
|
|
22
23
|
this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -37,7 +38,7 @@ export class SdCliCapacitor {
|
|
|
37
38
|
await this._initializeCapacitorProjectAsync(capacitorPath);
|
|
38
39
|
|
|
39
40
|
// 2. Capacitor 설정 파일 생성
|
|
40
|
-
this.
|
|
41
|
+
await this._createCapacitorConfigAsync(capacitorPath);
|
|
41
42
|
|
|
42
43
|
// 3. 플랫폼 관리
|
|
43
44
|
await this._managePlatformsAsync(capacitorPath);
|
|
@@ -46,14 +47,14 @@ export class SdCliCapacitor {
|
|
|
46
47
|
await this._managePluginsAsync(capacitorPath);
|
|
47
48
|
|
|
48
49
|
// 5. 안드로이드 서명 설정
|
|
49
|
-
this.
|
|
50
|
+
await this._setupAndroidSignAsync(capacitorPath);
|
|
50
51
|
|
|
51
52
|
// 6. 아이콘 및 스플래시 스크린 설정
|
|
52
53
|
await this._setupIconAndSplashScreenAsync(capacitorPath);
|
|
53
54
|
|
|
54
55
|
// 7. Android 네이티브 설정 (AndroidManifest.xml, build.gradle 등)
|
|
55
56
|
if (this._platforms.includes("android")) {
|
|
56
|
-
this.
|
|
57
|
+
await this._configureAndroidNativeAsync(capacitorPath);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// 8. 웹 자산 동기화
|
|
@@ -62,28 +63,47 @@ export class SdCliCapacitor {
|
|
|
62
63
|
|
|
63
64
|
// 1. Capacitor 프로젝트 초기화
|
|
64
65
|
private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<void> {
|
|
65
|
-
if (FsUtils.exists(capacitorPath)) {
|
|
66
|
+
if (FsUtils.exists(path.resolve(capacitorPath, "www"))) {
|
|
66
67
|
SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
|
|
67
68
|
|
|
68
69
|
// 버전 동기화
|
|
69
|
-
this.
|
|
70
|
+
await this._syncVersionAsync(capacitorPath);
|
|
70
71
|
} else {
|
|
71
|
-
FsUtils.
|
|
72
|
+
await FsUtils.mkdirsAsync(capacitorPath);
|
|
72
73
|
|
|
73
74
|
// package.json 생성
|
|
75
|
+
const projNpmConfig = await FsUtils.readJsonAsync(
|
|
76
|
+
path.resolve(this._opt.pkgPath, "../../package.json"),
|
|
77
|
+
);
|
|
74
78
|
const pkgJson = {
|
|
75
79
|
name: this._opt.config.appId,
|
|
76
80
|
version: this._npmConfig.version,
|
|
77
81
|
private: true,
|
|
82
|
+
volta: projNpmConfig.volta,
|
|
78
83
|
dependencies: {
|
|
79
84
|
"@capacitor/core": "^7.0.0",
|
|
80
85
|
},
|
|
81
86
|
devDependencies: {
|
|
82
87
|
"@capacitor/cli": "^7.0.0",
|
|
83
88
|
"@capacitor/assets": "^3.0.0",
|
|
89
|
+
...this._platforms.toObject(
|
|
90
|
+
(item) => `@capacitor/${item}`,
|
|
91
|
+
() => "^7.0.0",
|
|
92
|
+
),
|
|
84
93
|
},
|
|
85
94
|
};
|
|
86
|
-
FsUtils.
|
|
95
|
+
await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
|
|
96
|
+
space: 2,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// .yarnrc.yml 작성
|
|
100
|
+
await FsUtils.writeFileAsync(
|
|
101
|
+
path.resolve(capacitorPath, ".yarnrc.yml"),
|
|
102
|
+
"nodeLinker: node-modules",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// yarn.lock 작성
|
|
106
|
+
await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
|
|
87
107
|
|
|
88
108
|
// yarn install
|
|
89
109
|
await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
|
|
@@ -95,25 +115,32 @@ export class SdCliCapacitor {
|
|
|
95
115
|
capacitorPath,
|
|
96
116
|
);
|
|
97
117
|
}
|
|
118
|
+
|
|
119
|
+
// www/index.html 생성
|
|
120
|
+
const wwwPath = path.resolve(capacitorPath, "www");
|
|
121
|
+
await FsUtils.writeFileAsync(
|
|
122
|
+
path.resolve(wwwPath, "index.html"),
|
|
123
|
+
"<!DOCTYPE html><html><head></head><body></body></html>",
|
|
124
|
+
);
|
|
98
125
|
}
|
|
99
126
|
|
|
100
127
|
// 버전 동기화
|
|
101
|
-
private
|
|
128
|
+
private async _syncVersionAsync(capacitorPath: string) {
|
|
102
129
|
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
103
130
|
|
|
104
131
|
if (FsUtils.exists(pkgJsonPath)) {
|
|
105
|
-
const pkgJson = FsUtils.
|
|
132
|
+
const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
|
|
106
133
|
|
|
107
134
|
if (pkgJson.version !== this._npmConfig.version) {
|
|
108
135
|
pkgJson.version = this._npmConfig.version;
|
|
109
|
-
FsUtils.
|
|
110
|
-
SdCliCapacitor._logger.
|
|
136
|
+
await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
|
|
137
|
+
SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
|
|
111
138
|
}
|
|
112
139
|
}
|
|
113
140
|
}
|
|
114
141
|
|
|
115
142
|
// 2. Capacitor 설정 파일 생성
|
|
116
|
-
private
|
|
143
|
+
private async _createCapacitorConfigAsync(capacitorPath: string) {
|
|
117
144
|
const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
|
|
118
145
|
|
|
119
146
|
// 플러그인 옵션 생성
|
|
@@ -144,6 +171,7 @@ export class SdCliCapacitor {
|
|
|
144
171
|
},
|
|
145
172
|
android: {
|
|
146
173
|
allowMixedContent: true,
|
|
174
|
+
statusBarOverlaysWebView: false,
|
|
147
175
|
},
|
|
148
176
|
plugins: ${pluginsConfigStr},
|
|
149
177
|
};
|
|
@@ -151,7 +179,7 @@ export class SdCliCapacitor {
|
|
|
151
179
|
export default config;
|
|
152
180
|
`;
|
|
153
181
|
|
|
154
|
-
FsUtils.
|
|
182
|
+
await FsUtils.writeFileAsync(configFilePath, configContent);
|
|
155
183
|
}
|
|
156
184
|
|
|
157
185
|
// 3. 플랫폼 관리
|
|
@@ -166,7 +194,7 @@ export class SdCliCapacitor {
|
|
|
166
194
|
// 4. 플러그인 관리
|
|
167
195
|
private async _managePluginsAsync(capacitorPath: string): Promise<void> {
|
|
168
196
|
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
169
|
-
const pkgJson = FsUtils.
|
|
197
|
+
const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
|
|
170
198
|
const currentDeps = Object.keys(pkgJson.dependencies ?? {});
|
|
171
199
|
|
|
172
200
|
const usePlugins = Object.keys(this._opt.config.plugins ?? {});
|
|
@@ -185,40 +213,43 @@ export class SdCliCapacitor {
|
|
|
185
213
|
if (!usePlugins.includes(dep)) {
|
|
186
214
|
// Capacitor 관련 플러그인만 제거
|
|
187
215
|
if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
SdCliCapacitor._logger.log(`플러그인 제거: ${dep}`);
|
|
191
|
-
} catch {
|
|
192
|
-
SdCliCapacitor._logger.warn(`플러그인 제거 실패: ${dep}`);
|
|
193
|
-
}
|
|
216
|
+
await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
|
|
217
|
+
SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
|
|
194
218
|
}
|
|
195
219
|
}
|
|
196
220
|
}
|
|
197
221
|
|
|
198
222
|
// 새 플러그인 설치
|
|
223
|
+
const mainPkgJson = this._npmConfig;
|
|
224
|
+
const mainDeps = {
|
|
225
|
+
...mainPkgJson.dependencies,
|
|
226
|
+
...mainPkgJson.devDependencies,
|
|
227
|
+
...mainPkgJson.peerDependencies,
|
|
228
|
+
};
|
|
229
|
+
|
|
199
230
|
for (const plugin of usePlugins) {
|
|
200
231
|
if (!currentDeps.includes(plugin)) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
232
|
+
// 메인 프로젝트에 버전이 있으면 그 버전으로 설치
|
|
233
|
+
const version = mainDeps[plugin];
|
|
234
|
+
const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
|
|
235
|
+
|
|
236
|
+
await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
|
|
237
|
+
SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
|
|
207
238
|
}
|
|
208
239
|
}
|
|
209
240
|
}
|
|
210
241
|
|
|
211
242
|
// 5. 안드로이드 서명 설정
|
|
212
|
-
private
|
|
243
|
+
private async _setupAndroidSignAsync(capacitorPath: string) {
|
|
213
244
|
const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
|
|
214
245
|
|
|
215
246
|
if (this._opt.config.platform?.android?.sign) {
|
|
216
|
-
FsUtils.
|
|
247
|
+
await FsUtils.copyAsync(
|
|
217
248
|
path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore),
|
|
218
249
|
keystorePath,
|
|
219
250
|
);
|
|
220
251
|
} else {
|
|
221
|
-
FsUtils.
|
|
252
|
+
await FsUtils.removeAsync(keystorePath);
|
|
222
253
|
}
|
|
223
254
|
}
|
|
224
255
|
|
|
@@ -226,17 +257,20 @@ export class SdCliCapacitor {
|
|
|
226
257
|
private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
|
|
227
258
|
const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
|
|
228
259
|
|
|
229
|
-
// ICON 파일 복사
|
|
230
260
|
if (this._opt.config.icon != null) {
|
|
231
|
-
FsUtils.
|
|
261
|
+
await FsUtils.mkdirsAsync(iconDirPath);
|
|
232
262
|
|
|
233
263
|
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
234
264
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
265
|
+
// Adaptive Icon용 여백 추가된 이미지 생성
|
|
266
|
+
// 1024x1024 중 680x680이 safe zone (약 66%)
|
|
267
|
+
const paddedIconPath = path.resolve(iconDirPath, "icon-only.png");
|
|
268
|
+
await this._createPaddedIconAsync(iconSource, paddedIconPath);
|
|
269
|
+
|
|
270
|
+
// splash, icon 복사
|
|
271
|
+
await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "icon.png"));
|
|
272
|
+
await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
|
|
238
273
|
|
|
239
|
-
// @capacitor/assets로 아이콘/스플래시 리사이징
|
|
240
274
|
try {
|
|
241
275
|
await SdCliCapacitor._execAsync(
|
|
242
276
|
"npx",
|
|
@@ -247,36 +281,133 @@ export class SdCliCapacitor {
|
|
|
247
281
|
SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
|
|
248
282
|
}
|
|
249
283
|
} else {
|
|
250
|
-
FsUtils.
|
|
284
|
+
await FsUtils.removeAsync(iconDirPath);
|
|
251
285
|
}
|
|
252
286
|
}
|
|
253
287
|
|
|
288
|
+
private async _createPaddedIconAsync(sourcePath: string, outputPath: string): Promise<void> {
|
|
289
|
+
const size = 1024;
|
|
290
|
+
const iconSize = 680; // safe zone
|
|
291
|
+
const padding = Math.floor((size - iconSize) / 2); // 172px
|
|
292
|
+
|
|
293
|
+
await sharp(sourcePath)
|
|
294
|
+
.resize(iconSize, iconSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
295
|
+
.extend({
|
|
296
|
+
top: padding,
|
|
297
|
+
bottom: padding,
|
|
298
|
+
left: padding,
|
|
299
|
+
right: padding,
|
|
300
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }, // 투명
|
|
301
|
+
})
|
|
302
|
+
.toFile(outputPath);
|
|
303
|
+
}
|
|
304
|
+
|
|
254
305
|
// 7. Android 네이티브 설정
|
|
255
|
-
private
|
|
306
|
+
private async _configureAndroidNativeAsync(capacitorPath: string) {
|
|
256
307
|
const androidPath = path.resolve(capacitorPath, "android");
|
|
257
308
|
|
|
258
309
|
if (!FsUtils.exists(androidPath)) {
|
|
259
310
|
return;
|
|
260
311
|
}
|
|
261
312
|
|
|
313
|
+
// JAVA_HOME 찾기
|
|
314
|
+
await this._configureAndroidGradlePropertiesAsync(androidPath);
|
|
315
|
+
|
|
316
|
+
// local.properties 생성
|
|
317
|
+
await this._configureSdkPathAsync(androidPath);
|
|
318
|
+
|
|
262
319
|
// AndroidManifest.xml 수정
|
|
263
|
-
this.
|
|
320
|
+
await this._configureAndroidManifestAsync(androidPath);
|
|
264
321
|
|
|
265
322
|
// build.gradle 수정 (필요시)
|
|
266
|
-
this.
|
|
323
|
+
await this._configureAndroidBuildGradleAsync(androidPath);
|
|
267
324
|
|
|
268
325
|
// strings.xml 앱 이름 수정
|
|
269
|
-
this.
|
|
326
|
+
await this._configureAndroidStringsAsync(androidPath);
|
|
327
|
+
|
|
328
|
+
// styles.xml 수정
|
|
329
|
+
await this._configureAndroidStylesAsync(androidPath);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private async _configureAndroidStylesAsync(androidPath: string) {
|
|
333
|
+
const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
|
|
334
|
+
|
|
335
|
+
if (!FsUtils.exists(stylesPath)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let stylesContent = await FsUtils.readFileAsync(stylesPath);
|
|
340
|
+
|
|
341
|
+
// Edge-to-Edge 비활성화
|
|
342
|
+
if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
|
|
343
|
+
stylesContent = stylesContent.replace(
|
|
344
|
+
/(<style[^>]*AppTheme[^>]*>)/,
|
|
345
|
+
`$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await FsUtils.writeFileAsync(stylesPath, stylesContent);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private async _configureAndroidGradlePropertiesAsync(androidPath: string) {
|
|
353
|
+
const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
|
|
354
|
+
|
|
355
|
+
if (!FsUtils.exists(gradlePropsPath)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let content = await FsUtils.readFileAsync(gradlePropsPath);
|
|
360
|
+
|
|
361
|
+
// Java 21 경로 자동 탐색
|
|
362
|
+
const java21Path = this._findJava21();
|
|
363
|
+
if (java21Path != null && !content.includes("org.gradle.java.home")) {
|
|
364
|
+
content += `\norg.gradle.java.home=${java21Path.replace(/\\/g, "\\\\")}\n`;
|
|
365
|
+
FsUtils.writeFile(gradlePropsPath, content);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private _findJava21(): string | undefined {
|
|
370
|
+
const patterns = [
|
|
371
|
+
"C:/Program Files/Amazon Corretto/jdk21*",
|
|
372
|
+
"C:/Program Files/Eclipse Adoptium/jdk-21*",
|
|
373
|
+
"C:/Program Files/Java/jdk-21*",
|
|
374
|
+
"C:/Program Files/Microsoft/jdk-21*",
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
for (const pattern of patterns) {
|
|
378
|
+
const matches = FsUtils.glob(pattern);
|
|
379
|
+
if (matches.length > 0) {
|
|
380
|
+
// 가장 최신 버전 선택 (정렬 후 마지막)
|
|
381
|
+
return matches.sort().at(-1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private async _configureSdkPathAsync(androidPath: string) {
|
|
389
|
+
// local.properties 생성
|
|
390
|
+
const localPropsPath = path.resolve(androidPath, "local.properties");
|
|
391
|
+
|
|
392
|
+
if (FsUtils.exists(localPropsPath)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// SDK 경로 탐색 (Cordova 방식과 유사)
|
|
397
|
+
const sdkPath = this._findAndroidSdk();
|
|
398
|
+
if (sdkPath != null) {
|
|
399
|
+
await FsUtils.writeFileAsync(localPropsPath, `sdk.dir=${sdkPath.replace(/\\/g, "/")}\n`);
|
|
400
|
+
}
|
|
270
401
|
}
|
|
271
402
|
|
|
272
|
-
private
|
|
403
|
+
private async _configureAndroidManifestAsync(androidPath: string) {
|
|
273
404
|
const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
|
|
274
405
|
|
|
275
406
|
if (!FsUtils.exists(manifestPath)) {
|
|
276
407
|
return;
|
|
277
408
|
}
|
|
278
409
|
|
|
279
|
-
let manifestContent = FsUtils.
|
|
410
|
+
let manifestContent = await FsUtils.readFileAsync(manifestPath);
|
|
280
411
|
|
|
281
412
|
// usesCleartextTraffic 설정
|
|
282
413
|
if (!manifestContent.includes("android:usesCleartextTraffic")) {
|
|
@@ -319,17 +450,37 @@ export class SdCliCapacitor {
|
|
|
319
450
|
}
|
|
320
451
|
}
|
|
321
452
|
|
|
322
|
-
|
|
453
|
+
// intentFilters 설정
|
|
454
|
+
const intentFilters = this._opt.config.platform?.android?.intentFilters ?? [];
|
|
455
|
+
for (const filter of intentFilters) {
|
|
456
|
+
const filterKey = filter.action ?? filter.category ?? "";
|
|
457
|
+
if (filterKey && !manifestContent.includes(filterKey)) {
|
|
458
|
+
const actionLine = filter.action != null ? `<action android:name="${filter.action}"/>` : "";
|
|
459
|
+
const categoryLine =
|
|
460
|
+
filter.category != null ? `<category android:name="${filter.category}"/>` : "";
|
|
461
|
+
|
|
462
|
+
manifestContent = manifestContent.replace(
|
|
463
|
+
/(<activity[\s\S]*?android:name="\.MainActivity"[\s\S]*?>)/,
|
|
464
|
+
`$1
|
|
465
|
+
<intent-filter>
|
|
466
|
+
${actionLine}
|
|
467
|
+
${categoryLine}
|
|
468
|
+
</intent-filter>`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
await FsUtils.writeFileAsync(manifestPath, manifestContent);
|
|
323
474
|
}
|
|
324
475
|
|
|
325
|
-
private
|
|
476
|
+
private async _configureAndroidBuildGradleAsync(androidPath: string) {
|
|
326
477
|
const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
|
|
327
478
|
|
|
328
479
|
if (!FsUtils.exists(buildGradlePath)) {
|
|
329
480
|
return;
|
|
330
481
|
}
|
|
331
482
|
|
|
332
|
-
let gradleContent = FsUtils.
|
|
483
|
+
let gradleContent = await FsUtils.readFileAsync(buildGradlePath);
|
|
333
484
|
|
|
334
485
|
// versionName, versionCode 설정
|
|
335
486
|
const version = this._npmConfig.version;
|
|
@@ -384,17 +535,17 @@ export class SdCliCapacitor {
|
|
|
384
535
|
}
|
|
385
536
|
}
|
|
386
537
|
|
|
387
|
-
FsUtils.
|
|
538
|
+
await FsUtils.writeFileAsync(buildGradlePath, gradleContent);
|
|
388
539
|
}
|
|
389
540
|
|
|
390
|
-
private
|
|
541
|
+
private async _configureAndroidStringsAsync(androidPath: string) {
|
|
391
542
|
const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
|
|
392
543
|
|
|
393
544
|
if (!FsUtils.exists(stringsPath)) {
|
|
394
545
|
return;
|
|
395
546
|
}
|
|
396
547
|
|
|
397
|
-
let stringsContent = FsUtils.
|
|
548
|
+
let stringsContent = await FsUtils.readFileAsync(stringsPath);
|
|
398
549
|
stringsContent = stringsContent.replace(
|
|
399
550
|
/<string name="app_name">[^<]+<\/string>/,
|
|
400
551
|
`<string name="app_name">${this._opt.config.appName}</string>`,
|
|
@@ -412,7 +563,7 @@ export class SdCliCapacitor {
|
|
|
412
563
|
`<string name="custom_url_scheme">${this._opt.config.appId}</string>`,
|
|
413
564
|
);
|
|
414
565
|
|
|
415
|
-
FsUtils.
|
|
566
|
+
await FsUtils.writeFileAsync(stringsPath, stringsContent);
|
|
416
567
|
}
|
|
417
568
|
|
|
418
569
|
async buildAsync(outPath: string): Promise<void> {
|
|
@@ -470,14 +621,14 @@ export class SdCliCapacitor {
|
|
|
470
621
|
await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
|
|
471
622
|
|
|
472
623
|
// 빌드 결과물 복사
|
|
473
|
-
this.
|
|
624
|
+
await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
|
|
474
625
|
}
|
|
475
626
|
|
|
476
|
-
private
|
|
627
|
+
private async _copyAndroidBuildOutputAsync(
|
|
477
628
|
androidPath: string,
|
|
478
629
|
targetOutPath: string,
|
|
479
630
|
buildType: string,
|
|
480
|
-
)
|
|
631
|
+
) {
|
|
481
632
|
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
482
633
|
const isSigned = !!this._opt.config.platform?.android?.sign;
|
|
483
634
|
|
|
@@ -510,12 +661,39 @@ export class SdCliCapacitor {
|
|
|
510
661
|
|
|
511
662
|
const outputFileName = `${this._opt.config.appName}${isSigned ? "" : "-unsigned"}-latest.${ext}`;
|
|
512
663
|
|
|
513
|
-
FsUtils.
|
|
514
|
-
FsUtils.
|
|
664
|
+
await FsUtils.mkdirsAsync(targetOutPath);
|
|
665
|
+
await FsUtils.copyAsync(actualPath, path.resolve(targetOutPath, outputFileName));
|
|
515
666
|
|
|
516
667
|
const updatesPath = path.resolve(targetOutPath, "updates");
|
|
517
|
-
FsUtils.
|
|
518
|
-
FsUtils.
|
|
668
|
+
await FsUtils.mkdirsAsync(updatesPath);
|
|
669
|
+
await FsUtils.copyAsync(
|
|
670
|
+
actualPath,
|
|
671
|
+
path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private _findAndroidSdk(): string | undefined {
|
|
676
|
+
// 1. 환경변수 확인
|
|
677
|
+
const fromEnv = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"];
|
|
678
|
+
if (fromEnv != null && FsUtils.exists(fromEnv)) {
|
|
679
|
+
return fromEnv;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 2. 일반적인 설치 경로 탐색
|
|
683
|
+
const candidates = [
|
|
684
|
+
path.resolve(process.env["LOCALAPPDATA"] ?? "", "Android/Sdk"),
|
|
685
|
+
path.resolve(process.env["HOME"] ?? "", "Android/Sdk"),
|
|
686
|
+
"C:/Program Files/Android/Sdk",
|
|
687
|
+
"C:/Android/Sdk",
|
|
688
|
+
];
|
|
689
|
+
|
|
690
|
+
for (const candidate of candidates) {
|
|
691
|
+
if (FsUtils.exists(candidate)) {
|
|
692
|
+
return candidate;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return undefined;
|
|
519
697
|
}
|
|
520
698
|
|
|
521
699
|
static async runWebviewOnDeviceAsync(opt: {
|
|
@@ -523,9 +701,11 @@ export class SdCliCapacitor {
|
|
|
523
701
|
package: string;
|
|
524
702
|
url?: string;
|
|
525
703
|
}): Promise<void> {
|
|
526
|
-
const projNpmConf = FsUtils.
|
|
527
|
-
|
|
528
|
-
|
|
704
|
+
const projNpmConf = (await FsUtils.readJsonAsync(
|
|
705
|
+
path.resolve(process.cwd(), "package.json"),
|
|
706
|
+
)) as INpmConfig;
|
|
707
|
+
const allPkgPaths = await projNpmConf.workspaces!.mapManyAsync(
|
|
708
|
+
async (item) => await FsUtils.globAsync(PathUtils.posix(process.cwd(), item)),
|
|
529
709
|
);
|
|
530
710
|
|
|
531
711
|
const capacitorPath = path.resolve(
|
|
@@ -537,7 +717,7 @@ export class SdCliCapacitor {
|
|
|
537
717
|
// capacitor.config.ts의 server.url 설정 업데이트
|
|
538
718
|
const configPath = path.resolve(capacitorPath, "capacitor.config.ts");
|
|
539
719
|
if (FsUtils.exists(configPath)) {
|
|
540
|
-
let configContent = FsUtils.
|
|
720
|
+
let configContent = await FsUtils.readFileAsync(configPath);
|
|
541
721
|
const serverUrl = `${opt.url.replace(/\/$/, "")}/${opt.package}/capacitor/`;
|
|
542
722
|
|
|
543
723
|
// 기존 url 설정이 있으면 교체, 없으면 server 블록 첫 줄에 추가
|
|
@@ -549,12 +729,17 @@ export class SdCliCapacitor {
|
|
|
549
729
|
`server: {\n url: "${serverUrl}",`,
|
|
550
730
|
);
|
|
551
731
|
}
|
|
552
|
-
FsUtils.
|
|
732
|
+
await FsUtils.writeFileAsync(configPath, configContent);
|
|
553
733
|
}
|
|
554
734
|
}
|
|
555
735
|
|
|
556
736
|
// cap sync 후 run
|
|
557
737
|
await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
|
|
558
|
-
|
|
738
|
+
|
|
739
|
+
try {
|
|
740
|
+
await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
|
|
741
|
+
} catch {
|
|
742
|
+
await SdProcess.spawnAsync("adb", ["kill-server"]);
|
|
743
|
+
}
|
|
559
744
|
}
|
|
560
745
|
}
|
|
@@ -132,7 +132,6 @@ export interface ISdClientBuilderCordovaConfig {
|
|
|
132
132
|
browserslist?: string[];
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
136
135
|
export interface ISdClientBuilderCapacitorConfig {
|
|
137
136
|
appId: string;
|
|
138
137
|
appName: string;
|
|
@@ -143,6 +142,10 @@ export interface ISdClientBuilderCapacitorConfig {
|
|
|
143
142
|
android?: {
|
|
144
143
|
config?: Record<string, string>;
|
|
145
144
|
bundle?: boolean;
|
|
145
|
+
intentFilters?: {
|
|
146
|
+
action?: string;
|
|
147
|
+
category?: string;
|
|
148
|
+
}[];
|
|
146
149
|
sign?: {
|
|
147
150
|
keystore: string;
|
|
148
151
|
storePassword: string;
|