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