@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.
- package/dist/entry/SdCliCapacitor.d.ts +12 -19
- package/dist/entry/SdCliCapacitor.js +305 -289
- package/package.json +5 -5
- package/src/entry/SdCliCapacitor.ts +326 -365
|
@@ -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
|
-
...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(
|
|
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,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(
|
|
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],
|
|
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
|
-
//
|
|
190
|
-
async
|
|
191
|
-
const
|
|
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(
|
|
191
|
+
await FsUtils.mkdirsAsync(assetsDirPath);
|
|
203
192
|
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
204
|
-
//
|
|
205
|
-
const logoPath = path.resolve(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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(
|
|
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(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
334
|
-
|
|
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
|
|
414
|
-
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`);
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
+
for (const platform of this._platforms) {
|
|
461
494
|
// 해당 플랫폼만 copy
|
|
462
|
-
await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform],
|
|
495
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], this._capPath);
|
|
463
496
|
if (platform === "android") {
|
|
464
|
-
await this._buildAndroidAsync(
|
|
497
|
+
await this._buildAndroidAsync(outPath, buildType);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
throw new NotImplementError();
|
|
465
501
|
}
|
|
466
|
-
}
|
|
502
|
+
}
|
|
467
503
|
}
|
|
468
|
-
async _buildAndroidAsync(
|
|
469
|
-
const androidPath = path.resolve(
|
|
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
|
|
561
|
+
await this._execAsync("adb", ["kill-server"], capacitorPath);
|
|
546
562
|
throw err;
|
|
547
563
|
}
|
|
548
564
|
}
|