@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.
- package/dist/entry/SdCliCapacitor.d.ts +12 -20
- package/dist/entry/SdCliCapacitor.js +303 -293
- package/package.json +5 -5
- package/src/entry/SdCliCapacitor.ts +323 -366
|
@@ -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.
|
|
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, {
|
|
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
|
|
30
|
+
const changed = await this._initCapAsync();
|
|
26
31
|
// 2. Capacitor 설정 파일 생성
|
|
27
|
-
await this.
|
|
32
|
+
await this._writeCapConfAsync();
|
|
28
33
|
// 3. 플랫폼 관리
|
|
29
|
-
await this.
|
|
30
|
-
// 4.
|
|
31
|
-
|
|
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.
|
|
39
|
+
await this._configureAndroidAsync();
|
|
39
40
|
}
|
|
40
|
-
//
|
|
41
|
-
if (
|
|
42
|
-
await SdCliCapacitor._execAsync("npx", ["cap", "sync"],
|
|
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"],
|
|
46
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "copy"], this._capPath);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
// 1. Capacitor 프로젝트 초기화
|
|
49
|
-
async
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
},
|
|
78
|
-
devDependencies: {
|
|
79
|
-
"@capacitor/cli": "^7.0.0",
|
|
80
|
-
"@capacitor/assets": "^3.0.0",
|
|
81
|
-
...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.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(
|
|
56
|
+
await FsUtils.writeFileAsync(path.resolve(this._capPath, ".yarnrc.yml"), "nodeLinker: node-modules");
|
|
89
57
|
// 빈 yarn.lock 작성
|
|
90
|
-
await FsUtils.writeFileAsync(path.resolve(
|
|
58
|
+
await FsUtils.writeFileAsync(path.resolve(this._capPath, "yarn.lock"), "");
|
|
91
59
|
// yarn install
|
|
92
|
-
await SdCliCapacitor._execAsync("yarn", ["
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
102
|
-
const
|
|
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,
|
|
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,207 +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(
|
|
177
|
+
await FsUtils.writeFileAsync(confPath, configContent);
|
|
136
178
|
}
|
|
137
|
-
// 3. 플랫폼
|
|
138
|
-
async
|
|
179
|
+
// 3. 플랫폼 추가
|
|
180
|
+
async _addPlatformsAsync() {
|
|
139
181
|
for (const platform of this._platforms) {
|
|
140
|
-
if (FsUtils.exists(path.resolve(
|
|
182
|
+
if (FsUtils.exists(path.resolve(this._capPath, platform)))
|
|
141
183
|
continue;
|
|
142
|
-
await SdCliCapacitor._execAsync("npx", ["cap", "add", platform],
|
|
184
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], this._capPath);
|
|
143
185
|
}
|
|
144
186
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
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
|
-
// 사용하지 않는 플러그인 제거
|
|
157
|
-
for (const dep of Object.keys(currentDeps)) {
|
|
158
|
-
if (this._isCapacitorPlugin(dep) && !usePlugins.includes(dep)) {
|
|
159
|
-
delete currentDeps[dep];
|
|
160
|
-
changed = true;
|
|
161
|
-
SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// 새 플러그인 추가
|
|
165
|
-
for (const plugin of usePlugins) {
|
|
166
|
-
if (!(plugin in currentDeps)) {
|
|
167
|
-
const version = mainDeps[plugin] ?? "^7.0.0";
|
|
168
|
-
currentDeps[plugin] = version;
|
|
169
|
-
changed = true;
|
|
170
|
-
SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// 변경사항 있을 때만 저장 & install
|
|
174
|
-
if (changed) {
|
|
175
|
-
pkgJson.dependencies = currentDeps;
|
|
176
|
-
await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
|
|
177
|
-
await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
// 변경 없으면 아무것도 안 함 → 오프라인 OK
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
_isCapacitorPlugin(dep) {
|
|
184
|
-
// 기본 패키지 제외
|
|
185
|
-
const corePackages = [
|
|
186
|
-
"@capacitor/core",
|
|
187
|
-
"@capacitor/android",
|
|
188
|
-
"@capacitor/ios",
|
|
189
|
-
"@capacitor/app",
|
|
190
|
-
];
|
|
191
|
-
if (corePackages.includes(dep))
|
|
192
|
-
return false;
|
|
193
|
-
return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
|
|
194
|
-
}
|
|
195
|
-
// 5. 안드로이드 서명 설정
|
|
196
|
-
async _setupAndroidSignAsync(capacitorPath) {
|
|
197
|
-
const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
|
|
198
|
-
if (this._opt.config.platform?.android?.sign) {
|
|
199
|
-
await FsUtils.copyAsync(path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore), keystorePath);
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
await FsUtils.removeAsync(keystorePath);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
async _setupIconAndSplashScreenAsync(capacitorPath) {
|
|
206
|
-
const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_NAME);
|
|
187
|
+
// 4. 아이콘 설정
|
|
188
|
+
async _setupIconAsync() {
|
|
189
|
+
const assetsDirPath = path.resolve(this._capPath, "assets");
|
|
207
190
|
if (this._opt.config.icon != null) {
|
|
208
|
-
await FsUtils.mkdirsAsync(
|
|
191
|
+
await FsUtils.mkdirsAsync(assetsDirPath);
|
|
209
192
|
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
210
|
-
//
|
|
211
|
-
const logoPath = path.resolve(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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);
|
|
228
219
|
}
|
|
229
220
|
else {
|
|
230
|
-
await FsUtils.removeAsync(
|
|
221
|
+
await FsUtils.removeAsync(assetsDirPath);
|
|
231
222
|
}
|
|
232
223
|
}
|
|
233
|
-
// 중앙에 로고를 배치한 이미지 생성
|
|
234
|
-
async _createCenteredImageAsync(sourcePath, outputPath, outputSize, logoRatio) {
|
|
235
|
-
const logoSize = Math.floor(outputSize * logoRatio);
|
|
236
|
-
const padding = Math.floor((outputSize - logoSize) / 2);
|
|
237
|
-
await sharp(sourcePath)
|
|
238
|
-
.resize(logoSize, logoSize, {
|
|
239
|
-
fit: "contain",
|
|
240
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
241
|
-
})
|
|
242
|
-
.extend({
|
|
243
|
-
top: padding,
|
|
244
|
-
bottom: padding,
|
|
245
|
-
left: padding,
|
|
246
|
-
right: padding,
|
|
247
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
248
|
-
})
|
|
249
|
-
.toFile(outputPath);
|
|
250
|
-
}
|
|
251
224
|
// 기존 아이콘 파일 삭제
|
|
252
|
-
async _cleanupExistingIconsAsync(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
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);
|
|
284
256
|
// AndroidManifest.xml 수정
|
|
285
257
|
await this._configureAndroidManifestAsync(androidPath);
|
|
286
258
|
// build.gradle 수정 (필요시)
|
|
287
259
|
await this._configureAndroidBuildGradleAsync(androidPath);
|
|
288
|
-
// strings.xml 앱 이름
|
|
289
|
-
await this._configureAndroidStringsAsync(androidPath);
|
|
290
|
-
// styles.xml
|
|
291
|
-
await this._configureAndroidStylesAsync(androidPath);
|
|
292
|
-
}
|
|
293
|
-
async _configureAndroidStylesAsync(androidPath) {
|
|
294
|
-
const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
|
|
295
|
-
if (!FsUtils.exists(stylesPath)) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
let stylesContent = await FsUtils.readFileAsync(stylesPath);
|
|
299
|
-
// Edge-to-Edge 비활성화만
|
|
300
|
-
if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
|
|
301
|
-
stylesContent = stylesContent.replace(/(<style[^>]*name="AppTheme"[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
|
|
302
|
-
}
|
|
303
|
-
// NoActionBarLaunch를 단순 NoActionBar로
|
|
304
|
-
stylesContent = stylesContent.replace(/(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/, `$1Theme.AppCompat.Light.NoActionBar$2`);
|
|
305
|
-
// splash 관련 전부 제거
|
|
306
|
-
stylesContent = stylesContent.replace(/\s*<item name="android:background">@drawable\/splash<\/item>/g, "");
|
|
307
|
-
stylesContent = stylesContent.replace(/\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g, "");
|
|
308
|
-
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);
|
|
309
264
|
}
|
|
310
|
-
|
|
265
|
+
// JAVA_HOME 경로 설정
|
|
266
|
+
async _configureAndroidJavaHomePathAsync(androidPath) {
|
|
311
267
|
const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
|
|
312
|
-
if (!FsUtils.exists(gradlePropsPath)) {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
268
|
let content = await FsUtils.readFileAsync(gradlePropsPath);
|
|
316
269
|
// Java 21 경로 자동 탐색
|
|
317
|
-
const java21Path = this.
|
|
270
|
+
const java21Path = await this._findJava21Async();
|
|
318
271
|
if (java21Path != null && !content.includes("org.gradle.java.home")) {
|
|
319
272
|
content += `\norg.gradle.java.home=${java21Path.replace(/\\/g, "\\\\")}\n`;
|
|
320
273
|
FsUtils.writeFile(gradlePropsPath, content);
|
|
321
274
|
}
|
|
322
275
|
}
|
|
323
|
-
|
|
276
|
+
async _findJava21Async() {
|
|
324
277
|
const patterns = [
|
|
325
278
|
"C:/Program Files/Amazon Corretto/jdk21*",
|
|
326
279
|
"C:/Program Files/Eclipse Adoptium/jdk-21*",
|
|
@@ -328,7 +281,7 @@ export class SdCliCapacitor {
|
|
|
328
281
|
"C:/Program Files/Microsoft/jdk-21*",
|
|
329
282
|
];
|
|
330
283
|
for (const pattern of patterns) {
|
|
331
|
-
const matches = FsUtils.
|
|
284
|
+
const matches = await FsUtils.globAsync(pattern);
|
|
332
285
|
if (matches.length > 0) {
|
|
333
286
|
// 가장 최신 버전 선택 (정렬 후 마지막)
|
|
334
287
|
return matches.sort().at(-1);
|
|
@@ -336,23 +289,38 @@ export class SdCliCapacitor {
|
|
|
336
289
|
}
|
|
337
290
|
return undefined;
|
|
338
291
|
}
|
|
339
|
-
|
|
340
|
-
|
|
292
|
+
// SDK 경로 설정
|
|
293
|
+
async _configureAndroidSdkPathAsync(androidPath) {
|
|
341
294
|
const localPropsPath = path.resolve(androidPath, "local.properties");
|
|
342
|
-
if (FsUtils.exists(localPropsPath)) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
295
|
// SDK 경로 탐색 (Cordova 방식과 유사)
|
|
346
296
|
const sdkPath = this._findAndroidSdk();
|
|
347
297
|
if (sdkPath != null) {
|
|
348
298
|
await FsUtils.writeFileAsync(localPropsPath, `sdk.dir=${sdkPath.replace(/\\/g, "/")}\n`);
|
|
349
299
|
}
|
|
350
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 수정
|
|
351
322
|
async _configureAndroidManifestAsync(androidPath) {
|
|
352
323
|
const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
|
|
353
|
-
if (!FsUtils.exists(manifestPath)) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
324
|
let manifestContent = await FsUtils.readFileAsync(manifestPath);
|
|
357
325
|
// usesCleartextTraffic 설정
|
|
358
326
|
if (!manifestContent.includes("android:usesCleartextTraffic")) {
|
|
@@ -399,11 +367,9 @@ export class SdCliCapacitor {
|
|
|
399
367
|
}
|
|
400
368
|
await FsUtils.writeFileAsync(manifestPath, manifestContent);
|
|
401
369
|
}
|
|
370
|
+
// build.gradle 수정 (필요시)
|
|
402
371
|
async _configureAndroidBuildGradleAsync(androidPath) {
|
|
403
372
|
const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
|
|
404
|
-
if (!FsUtils.exists(buildGradlePath)) {
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
373
|
let gradleContent = await FsUtils.readFileAsync(buildGradlePath);
|
|
408
374
|
// versionName, versionCode 설정
|
|
409
375
|
const version = this._npmConfig.version;
|
|
@@ -416,13 +382,21 @@ export class SdCliCapacitor {
|
|
|
416
382
|
// SDK 버전 설정
|
|
417
383
|
if (this._opt.config.platform?.android?.sdkVersion != null) {
|
|
418
384
|
const sdkVersion = this._opt.config.platform.android.sdkVersion;
|
|
419
|
-
gradleContent = gradleContent.replace(/minSdkVersion
|
|
420
|
-
gradleContent = gradleContent.replace(/targetSdkVersion
|
|
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`);
|
|
421
391
|
}
|
|
422
392
|
// Signing 설정
|
|
393
|
+
const keystorePath = path.resolve(this._capPath, this._ANDROID_KEYSTORE_FILE_NAME);
|
|
423
394
|
const signConfig = this._opt.config.platform?.android?.sign;
|
|
424
395
|
if (signConfig) {
|
|
425
|
-
|
|
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, "/");
|
|
426
400
|
const keystoreType = signConfig.keystoreType ?? "jks";
|
|
427
401
|
// signingConfigs 블록 추가
|
|
428
402
|
if (!gradleContent.includes("signingConfigs")) {
|
|
@@ -445,34 +419,90 @@ export class SdCliCapacitor {
|
|
|
445
419
|
gradleContent = gradleContent.replace(/(buildTypes\s*\{[\s\S]*?release\s*\{)/, `$1\n signingConfig signingConfigs.release`);
|
|
446
420
|
}
|
|
447
421
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
|
|
452
|
-
if (!FsUtils.exists(stringsPath)) {
|
|
453
|
-
return;
|
|
422
|
+
else {
|
|
423
|
+
//TODO: gradleContent에서 signingConfigs 관련 부분 삭제
|
|
424
|
+
await FsUtils.removeAsync(keystorePath);
|
|
454
425
|
}
|
|
455
|
-
|
|
456
|
-
stringsContent = stringsContent.replace(/<string name="app_name">[^<]+<\/string>/, `<string name="app_name">${this._opt.config.appName}</string>`);
|
|
457
|
-
stringsContent = stringsContent.replace(/<string name="title_activity_main">[^<]+<\/string>/, `<string name="title_activity_main">${this._opt.config.appName}</string>`);
|
|
458
|
-
stringsContent = stringsContent.replace(/<string name="package_name">[^<]+<\/string>/, `<string name="package_name">${this._opt.config.appId}</string>`);
|
|
459
|
-
stringsContent = stringsContent.replace(/<string name="custom_url_scheme">[^<]+<\/string>/, `<string name="custom_url_scheme">${this._opt.config.appId}</string>`);
|
|
460
|
-
await FsUtils.writeFileAsync(stringsPath, stringsContent);
|
|
426
|
+
await FsUtils.writeFileAsync(buildGradlePath, gradleContent);
|
|
461
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
|
+
}*/
|
|
462
490
|
async buildAsync(outPath) {
|
|
463
|
-
const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
|
|
464
491
|
const buildType = this._opt.config.debug ? "debug" : "release";
|
|
465
492
|
// 플랫폼별 빌드
|
|
466
493
|
await Promise.all(this._platforms.map(async (platform) => {
|
|
467
494
|
// 해당 플랫폼만 copy
|
|
468
|
-
await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform],
|
|
495
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], this._capPath);
|
|
469
496
|
if (platform === "android") {
|
|
470
|
-
await this._buildAndroidAsync(
|
|
497
|
+
await this._buildAndroidAsync(outPath, buildType);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
throw new NotImplementError();
|
|
471
501
|
}
|
|
472
502
|
}));
|
|
473
503
|
}
|
|
474
|
-
async _buildAndroidAsync(
|
|
475
|
-
const androidPath = path.resolve(
|
|
504
|
+
async _buildAndroidAsync(outPath, buildType) {
|
|
505
|
+
const androidPath = path.resolve(this._capPath, "android");
|
|
476
506
|
const targetOutPath = path.resolve(outPath, "android");
|
|
477
507
|
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
478
508
|
const gradleTask = buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
|
|
@@ -502,26 +532,6 @@ export class SdCliCapacitor {
|
|
|
502
532
|
await FsUtils.mkdirsAsync(updatesPath);
|
|
503
533
|
await FsUtils.copyAsync(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
|
|
504
534
|
}
|
|
505
|
-
_findAndroidSdk() {
|
|
506
|
-
// 1. 환경변수 확인
|
|
507
|
-
const fromEnv = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"];
|
|
508
|
-
if (fromEnv != null && FsUtils.exists(fromEnv)) {
|
|
509
|
-
return fromEnv;
|
|
510
|
-
}
|
|
511
|
-
// 2. 일반적인 설치 경로 탐색
|
|
512
|
-
const candidates = [
|
|
513
|
-
path.resolve(process.env["LOCALAPPDATA"] ?? "", "Android/Sdk"),
|
|
514
|
-
path.resolve(process.env["HOME"] ?? "", "Android/Sdk"),
|
|
515
|
-
"C:/Program Files/Android/Sdk",
|
|
516
|
-
"C:/Android/Sdk",
|
|
517
|
-
];
|
|
518
|
-
for (const candidate of candidates) {
|
|
519
|
-
if (FsUtils.exists(candidate)) {
|
|
520
|
-
return candidate;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
return undefined;
|
|
524
|
-
}
|
|
525
535
|
static async runWebviewOnDeviceAsync(opt) {
|
|
526
536
|
const projNpmConf = (await FsUtils.readJsonAsync(path.resolve(process.cwd(), "package.json")));
|
|
527
537
|
const allPkgPaths = await projNpmConf.workspaces.mapManyAsync(async (item) => await FsUtils.globAsync(PathUtils.posix(process.cwd(), item)));
|
|
@@ -548,7 +558,7 @@ export class SdCliCapacitor {
|
|
|
548
558
|
await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
|
|
549
559
|
}
|
|
550
560
|
catch (err) {
|
|
551
|
-
await
|
|
561
|
+
await this._execAsync("adb", ["kill-server"], capacitorPath);
|
|
552
562
|
throw err;
|
|
553
563
|
}
|
|
554
564
|
}
|