@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
|
@@ -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
|
-
...this._platforms.toObject(
|
|
106
|
-
(item) => `@capacitor/${item}`,
|
|
107
|
-
() => "^7.0.0",
|
|
108
|
-
),
|
|
109
|
-
},
|
|
110
|
-
devDependencies: {
|
|
111
|
-
"@capacitor/cli": "^7.0.0",
|
|
112
|
-
"@capacitor/assets": "^3.0.0",
|
|
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,61 +102,37 @@ export class SdCliCapacitor {
|
|
|
149
102
|
return true;
|
|
150
103
|
}
|
|
151
104
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// 플러그인 옵션 생성
|
|
157
|
-
const pluginOptions: Record<string, Record<string, unknown>> = {};
|
|
158
|
-
for (const [pluginName, options] of Object.entries(this._opt.config.plugins ?? {})) {
|
|
159
|
-
if (options !== true) {
|
|
160
|
-
// @capacitor/splash-screen → SplashScreen 형태로 변환
|
|
161
|
-
const configKey = StringUtils.toPascalCase(pluginName.split("/").last()!);
|
|
162
|
-
pluginOptions[configKey] = options;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const pluginsConfigStr =
|
|
167
|
-
Object.keys(pluginOptions).length > 0
|
|
168
|
-
? JSON.stringify(pluginOptions, null, 4).replace(/^/gm, " ").trim()
|
|
169
|
-
: "{}";
|
|
170
|
-
|
|
171
|
-
const configContent = typescript`
|
|
172
|
-
import type { CapacitorConfig } from "@capacitor/cli";
|
|
173
|
-
|
|
174
|
-
const config: CapacitorConfig = {
|
|
175
|
-
appId: "${this._opt.config.appId}",
|
|
176
|
-
appName: "${this._opt.config.appName}",
|
|
177
|
-
server: {
|
|
178
|
-
androidScheme: "http",
|
|
179
|
-
cleartext: true,
|
|
180
|
-
allowNavigation: ["*"],
|
|
181
|
-
},
|
|
182
|
-
android: {
|
|
183
|
-
allowMixedContent: true,
|
|
184
|
-
statusBarOverlaysWebView: false,
|
|
185
|
-
},
|
|
186
|
-
plugins: ${pluginsConfigStr},
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
export default config;
|
|
190
|
-
`;
|
|
191
|
-
|
|
192
|
-
await FsUtils.writeFileAsync(configFilePath, configContent);
|
|
193
|
-
}
|
|
105
|
+
private async _setupNpmConfAsync() {
|
|
106
|
+
const projNpmConfig = await FsUtils.readJsonAsync(
|
|
107
|
+
path.resolve(this._opt.pkgPath, "../../package.json"),
|
|
108
|
+
);
|
|
194
109
|
|
|
195
|
-
|
|
196
|
-
|
|
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";
|
|
197
126
|
for (const platform of this._platforms) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], capacitorPath);
|
|
127
|
+
capNpmConf.dependencies[`@capacitor/${platform}`] = "^7.0.0";
|
|
201
128
|
}
|
|
202
|
-
|
|
129
|
+
capNpmConf.devDependencies = capNpmConf.devDependencies ?? {};
|
|
130
|
+
capNpmConf.devDependencies["@capacitor/cli"] = "^7.0.0";
|
|
131
|
+
capNpmConf.devDependencies["@capacitor/assets"] = "^3.0.0";
|
|
203
132
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
133
|
+
// -----------------------------
|
|
134
|
+
// 플러그인 패키지 설정
|
|
135
|
+
// -----------------------------
|
|
207
136
|
|
|
208
137
|
const mainDeps = {
|
|
209
138
|
...this._npmConfig.dependencies,
|
|
@@ -212,229 +141,223 @@ export class SdCliCapacitor {
|
|
|
212
141
|
};
|
|
213
142
|
|
|
214
143
|
const usePlugins = Object.keys(this._opt.config.plugins ?? {});
|
|
215
|
-
const currentDeps = pkgJson.dependencies ?? {};
|
|
216
|
-
|
|
217
|
-
let changed = false;
|
|
218
144
|
|
|
219
|
-
const prevPlugins = Object.keys(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
145
|
+
const prevPlugins = Object.keys(capNpmConf.dependencies).filter(
|
|
146
|
+
(item) =>
|
|
147
|
+
!["@capacitor/core", "@capacitor/android", "@capacitor/ios", "@capacitor/app"].includes(
|
|
148
|
+
item,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
225
151
|
|
|
226
152
|
// 사용하지 않는 플러그인 제거
|
|
227
153
|
for (const prevPlugin of prevPlugins) {
|
|
228
154
|
if (!usePlugins.includes(prevPlugin)) {
|
|
229
|
-
delete
|
|
230
|
-
changed = true;
|
|
155
|
+
delete capNpmConf.dependencies[prevPlugin];
|
|
231
156
|
SdCliCapacitor._logger.debug(`플러그인 제거: ${prevPlugin}`);
|
|
232
157
|
}
|
|
233
158
|
}
|
|
234
159
|
|
|
235
160
|
// 새 플러그인 추가
|
|
236
161
|
for (const plugin of usePlugins) {
|
|
237
|
-
if (!(plugin in
|
|
162
|
+
if (!(plugin in capNpmConf.dependencies)) {
|
|
238
163
|
const version = mainDeps[plugin] ?? "*";
|
|
239
|
-
|
|
240
|
-
changed = true;
|
|
164
|
+
capNpmConf.dependencies[plugin] = version;
|
|
241
165
|
SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
|
|
242
166
|
}
|
|
243
167
|
}
|
|
244
168
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
|
|
249
|
-
await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
// 변경 없으면 아무것도 안 함 → 오프라인 OK
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
169
|
+
// -----------------------------
|
|
170
|
+
// 저장
|
|
171
|
+
// -----------------------------
|
|
255
172
|
|
|
256
|
-
|
|
257
|
-
private async _setupAndroidSignAsync(capacitorPath: string) {
|
|
258
|
-
const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
|
|
259
|
-
|
|
260
|
-
if (this._opt.config.platform?.android?.sign) {
|
|
261
|
-
await FsUtils.copyAsync(
|
|
262
|
-
path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore),
|
|
263
|
-
keystorePath,
|
|
264
|
-
);
|
|
265
|
-
} else {
|
|
266
|
-
await FsUtils.removeAsync(keystorePath);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
173
|
+
await FsUtils.writeJsonAsync(capNpmConfPath, capNpmConf, { space: 2 });
|
|
269
174
|
|
|
270
|
-
|
|
271
|
-
|
|
175
|
+
return (
|
|
176
|
+
!ObjectUtils.equal(orgCapNpmConf.volta, capNpmConf.volta) ||
|
|
177
|
+
!ObjectUtils.equal(orgCapNpmConf.dependencies, capNpmConf.dependencies) ||
|
|
178
|
+
!ObjectUtils.equal(orgCapNpmConf.devDependencies, capNpmConf.devDependencies)
|
|
179
|
+
);
|
|
272
180
|
|
|
273
|
-
|
|
274
|
-
|
|
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);
|
|
275
188
|
|
|
276
|
-
const
|
|
189
|
+
const errorLines = installResult.split("\n").filter((item) => item.includes("YN0002"));
|
|
277
190
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
await this._cleanupExistingIconsAsync(capacitorPath);
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
await SdCliCapacitor._execAsync(
|
|
286
|
-
"npx",
|
|
287
|
-
[
|
|
288
|
-
"@capacitor/assets",
|
|
289
|
-
"generate",
|
|
290
|
-
"--android",
|
|
291
|
-
"--iconBackgroundColor",
|
|
292
|
-
"#ffffff",
|
|
293
|
-
"--splashBackgroundColor",
|
|
294
|
-
"#ffffff",
|
|
295
|
-
],
|
|
296
|
-
capacitorPath,
|
|
297
|
-
);
|
|
298
|
-
} catch (e) {
|
|
299
|
-
SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용", e);
|
|
191
|
+
// peer dependency 경고 감지
|
|
192
|
+
if (errorLines.length > 0) {
|
|
193
|
+
throw new Error(errorLines.join("\n"));
|
|
300
194
|
}
|
|
301
|
-
} else {
|
|
302
|
-
await FsUtils.removeAsync(resourcesDirPath);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
195
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
outputSize: number,
|
|
311
|
-
logoRatio: number, // 0.0 ~ 1.0
|
|
312
|
-
): Promise<void> {
|
|
313
|
-
const logoSize = Math.floor(outputSize * logoRatio);
|
|
314
|
-
const padding = Math.floor((outputSize - logoSize) / 2);
|
|
315
|
-
|
|
316
|
-
await sharp(sourcePath)
|
|
317
|
-
.resize(logoSize, logoSize, {
|
|
318
|
-
fit: "contain",
|
|
319
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
320
|
-
})
|
|
321
|
-
.extend({
|
|
322
|
-
top: padding,
|
|
323
|
-
bottom: padding,
|
|
324
|
-
left: padding,
|
|
325
|
-
right: padding,
|
|
326
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
327
|
-
})
|
|
328
|
-
.toFile(outputPath);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
// 변경 없으면 아무것도 안 함 → 오프라인 OK
|
|
199
|
+
return false;*/
|
|
329
200
|
}
|
|
330
201
|
|
|
331
|
-
//
|
|
332
|
-
private async
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
if (!FsUtils.exists(androidResPath)) return;
|
|
336
|
-
|
|
337
|
-
// mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
|
|
338
|
-
const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
|
|
339
|
-
for (const dir of mipmapDirs) {
|
|
340
|
-
const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
|
|
341
|
-
for (const file of iconFiles) {
|
|
342
|
-
await FsUtils.removeAsync(file);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
202
|
+
// 2. Capacitor 설정 파일 생성
|
|
203
|
+
private async _writeCapConfAsync() {
|
|
204
|
+
const confPath = path.resolve(this._capPath, "capacitor.config.ts");
|
|
345
205
|
|
|
346
|
-
//
|
|
347
|
-
const
|
|
348
|
-
for (const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
await FsUtils.removeAsync(file);
|
|
206
|
+
// 플러그인 옵션 생성
|
|
207
|
+
const pluginOptions: Record<string, Record<string, unknown>> = {};
|
|
208
|
+
for (const [pluginName, options] of Object.entries(this._opt.config.plugins ?? {})) {
|
|
209
|
+
if (options !== true) {
|
|
210
|
+
const configKey = StringUtils.toPascalCase(pluginName.split("/").last()!);
|
|
211
|
+
pluginOptions[configKey] = options;
|
|
353
212
|
}
|
|
354
213
|
}
|
|
355
|
-
}
|
|
356
214
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (!FsUtils.exists(androidPath)) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
215
|
+
const pluginsConfigStr =
|
|
216
|
+
Object.keys(pluginOptions).length > 0
|
|
217
|
+
? JSON.stringify(pluginOptions, null, 2).replace(/^/gm, " ").trim()
|
|
218
|
+
: "{}";
|
|
364
219
|
|
|
365
|
-
|
|
366
|
-
|
|
220
|
+
const configContent = typescript`
|
|
221
|
+
import type { CapacitorConfig } from "@capacitor/cli";
|
|
367
222
|
|
|
368
|
-
|
|
369
|
-
|
|
223
|
+
const config: CapacitorConfig = {
|
|
224
|
+
appId: "${this._opt.config.appId}",
|
|
225
|
+
appName: "${this._opt.config.appName}",
|
|
226
|
+
server: {
|
|
227
|
+
androidScheme: "http",
|
|
228
|
+
cleartext: true
|
|
229
|
+
},
|
|
230
|
+
android: {},
|
|
231
|
+
plugins: ${pluginsConfigStr},
|
|
232
|
+
};
|
|
370
233
|
|
|
371
|
-
|
|
372
|
-
|
|
234
|
+
export default config;
|
|
235
|
+
`;
|
|
373
236
|
|
|
374
|
-
|
|
375
|
-
|
|
237
|
+
await FsUtils.writeFileAsync(confPath, configContent);
|
|
238
|
+
}
|
|
376
239
|
|
|
377
|
-
|
|
378
|
-
|
|
240
|
+
// 3. 플랫폼 추가
|
|
241
|
+
private async _addPlatformsAsync(): Promise<void> {
|
|
242
|
+
for (const platform of this._platforms) {
|
|
243
|
+
if (FsUtils.exists(path.resolve(this._capPath, platform))) continue;
|
|
379
244
|
|
|
380
|
-
|
|
381
|
-
|
|
245
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], this._capPath);
|
|
246
|
+
}
|
|
382
247
|
}
|
|
383
248
|
|
|
384
|
-
|
|
385
|
-
|
|
249
|
+
// 4. 아이콘 설정
|
|
250
|
+
private async _setupIconAsync(): Promise<void> {
|
|
251
|
+
const assetsDirPath = path.resolve(this._capPath, "assets");
|
|
386
252
|
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
}
|
|
253
|
+
if (this._opt.config.icon != null) {
|
|
254
|
+
await FsUtils.mkdirsAsync(assetsDirPath);
|
|
390
255
|
|
|
391
|
-
|
|
256
|
+
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
392
257
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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,
|
|
398
291
|
);
|
|
292
|
+
} else {
|
|
293
|
+
await FsUtils.removeAsync(assetsDirPath);
|
|
399
294
|
}
|
|
295
|
+
}
|
|
400
296
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
297
|
+
// 기존 아이콘 파일 삭제
|
|
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);
|
|
406
332
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
/\s*<item name="android:background">@drawable\/splash<\/item>/g,
|
|
410
|
-
"",
|
|
411
|
-
);
|
|
412
|
-
stylesContent = stylesContent.replace(
|
|
413
|
-
/\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g,
|
|
414
|
-
"",
|
|
415
|
-
);
|
|
333
|
+
// AndroidManifest.xml 수정
|
|
334
|
+
await this._configureAndroidManifestAsync(androidPath);
|
|
416
335
|
|
|
417
|
-
|
|
336
|
+
// build.gradle 수정 (필요시)
|
|
337
|
+
await this._configureAndroidBuildGradleAsync(androidPath);
|
|
338
|
+
|
|
339
|
+
// TODO: strings.xml 앱 이름 수정?? WHY?
|
|
340
|
+
// await this._configureAndroidStringsAsync(androidPath);
|
|
341
|
+
|
|
342
|
+
// TODO: styles.xml 수정?? WHY?
|
|
343
|
+
// await this._configureAndroidStylesAsync(androidPath);
|
|
418
344
|
}
|
|
419
345
|
|
|
420
|
-
|
|
346
|
+
// JAVA_HOME 경로 설정
|
|
347
|
+
private async _configureAndroidJavaHomePathAsync(androidPath: string) {
|
|
421
348
|
const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
|
|
422
349
|
|
|
423
|
-
if (!FsUtils.exists(gradlePropsPath)) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
350
|
let content = await FsUtils.readFileAsync(gradlePropsPath);
|
|
428
351
|
|
|
429
352
|
// Java 21 경로 자동 탐색
|
|
430
|
-
const java21Path = this.
|
|
353
|
+
const java21Path = await this._findJava21Async();
|
|
431
354
|
if (java21Path != null && !content.includes("org.gradle.java.home")) {
|
|
432
355
|
content += `\norg.gradle.java.home=${java21Path.replace(/\\/g, "\\\\")}\n`;
|
|
433
356
|
FsUtils.writeFile(gradlePropsPath, content);
|
|
434
357
|
}
|
|
435
358
|
}
|
|
436
359
|
|
|
437
|
-
private
|
|
360
|
+
private async _findJava21Async(): Promise<string | undefined> {
|
|
438
361
|
const patterns = [
|
|
439
362
|
"C:/Program Files/Amazon Corretto/jdk21*",
|
|
440
363
|
"C:/Program Files/Eclipse Adoptium/jdk-21*",
|
|
@@ -443,7 +366,7 @@ export class SdCliCapacitor {
|
|
|
443
366
|
];
|
|
444
367
|
|
|
445
368
|
for (const pattern of patterns) {
|
|
446
|
-
const matches = FsUtils.
|
|
369
|
+
const matches = await FsUtils.globAsync(pattern);
|
|
447
370
|
if (matches.length > 0) {
|
|
448
371
|
// 가장 최신 버전 선택 (정렬 후 마지막)
|
|
449
372
|
return matches.sort().at(-1);
|
|
@@ -453,14 +376,10 @@ export class SdCliCapacitor {
|
|
|
453
376
|
return undefined;
|
|
454
377
|
}
|
|
455
378
|
|
|
456
|
-
|
|
457
|
-
|
|
379
|
+
// SDK 경로 설정
|
|
380
|
+
private async _configureAndroidSdkPathAsync(androidPath: string) {
|
|
458
381
|
const localPropsPath = path.resolve(androidPath, "local.properties");
|
|
459
382
|
|
|
460
|
-
if (FsUtils.exists(localPropsPath)) {
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
383
|
// SDK 경로 탐색 (Cordova 방식과 유사)
|
|
465
384
|
const sdkPath = this._findAndroidSdk();
|
|
466
385
|
if (sdkPath != null) {
|
|
@@ -468,13 +387,34 @@ export class SdCliCapacitor {
|
|
|
468
387
|
}
|
|
469
388
|
}
|
|
470
389
|
|
|
471
|
-
private
|
|
472
|
-
|
|
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
|
+
}
|
|
473
396
|
|
|
474
|
-
|
|
475
|
-
|
|
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
|
+
}
|
|
476
409
|
}
|
|
477
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
|
+
|
|
478
418
|
let manifestContent = await FsUtils.readFileAsync(manifestPath);
|
|
479
419
|
|
|
480
420
|
// usesCleartextTraffic 설정
|
|
@@ -541,13 +481,10 @@ export class SdCliCapacitor {
|
|
|
541
481
|
await FsUtils.writeFileAsync(manifestPath, manifestContent);
|
|
542
482
|
}
|
|
543
483
|
|
|
484
|
+
// build.gradle 수정 (필요시)
|
|
544
485
|
private async _configureAndroidBuildGradleAsync(androidPath: string) {
|
|
545
486
|
const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
|
|
546
487
|
|
|
547
|
-
if (!FsUtils.exists(buildGradlePath)) {
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
488
|
let gradleContent = await FsUtils.readFileAsync(buildGradlePath);
|
|
552
489
|
|
|
553
490
|
// versionName, versionCode 설정
|
|
@@ -564,17 +501,31 @@ export class SdCliCapacitor {
|
|
|
564
501
|
// SDK 버전 설정
|
|
565
502
|
if (this._opt.config.platform?.android?.sdkVersion != null) {
|
|
566
503
|
const sdkVersion = this._opt.config.platform.android.sdkVersion;
|
|
567
|
-
gradleContent = gradleContent.replace(/minSdkVersion
|
|
504
|
+
gradleContent = gradleContent.replace(/minSdkVersion .+/, `minSdkVersion ${sdkVersion}`);
|
|
568
505
|
gradleContent = gradleContent.replace(
|
|
569
|
-
/targetSdkVersion
|
|
506
|
+
/targetSdkVersion .+/,
|
|
570
507
|
`targetSdkVersion ${sdkVersion}`,
|
|
571
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
|
+
);
|
|
572
518
|
}
|
|
573
519
|
|
|
574
520
|
// Signing 설정
|
|
521
|
+
const keystorePath = path.resolve(this._capPath, this._ANDROID_KEYSTORE_FILE_NAME);
|
|
575
522
|
const signConfig = this._opt.config.platform?.android?.sign;
|
|
576
523
|
if (signConfig) {
|
|
577
|
-
|
|
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, "/");
|
|
578
529
|
const keystoreType = signConfig.keystoreType ?? "jks";
|
|
579
530
|
|
|
580
531
|
// signingConfigs 블록 추가
|
|
@@ -604,12 +555,15 @@ export class SdCliCapacitor {
|
|
|
604
555
|
`$1\n signingConfig signingConfigs.release`,
|
|
605
556
|
);
|
|
606
557
|
}
|
|
558
|
+
} else {
|
|
559
|
+
//TODO: gradleContent에서 signingConfigs 관련 부분 삭제
|
|
560
|
+
await FsUtils.removeAsync(keystorePath);
|
|
607
561
|
}
|
|
608
562
|
|
|
609
563
|
await FsUtils.writeFileAsync(buildGradlePath, gradleContent);
|
|
610
564
|
}
|
|
611
565
|
|
|
612
|
-
private async _configureAndroidStringsAsync(androidPath: string) {
|
|
566
|
+
/*private async _configureAndroidStringsAsync(androidPath: string) {
|
|
613
567
|
const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
|
|
614
568
|
|
|
615
569
|
if (!FsUtils.exists(stringsPath)) {
|
|
@@ -635,31 +589,62 @@ export class SdCliCapacitor {
|
|
|
635
589
|
);
|
|
636
590
|
|
|
637
591
|
await FsUtils.writeFileAsync(stringsPath, stringsContent);
|
|
638
|
-
}
|
|
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
|
+
}*/
|
|
639
629
|
|
|
640
630
|
async buildAsync(outPath: string): Promise<void> {
|
|
641
|
-
const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
|
|
642
631
|
const buildType = this._opt.config.debug ? "debug" : "release";
|
|
643
632
|
|
|
644
633
|
// 플랫폼별 빌드
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
|
|
634
|
+
for (const platform of this._platforms) {
|
|
635
|
+
// 해당 플랫폼만 copy
|
|
636
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], this._capPath);
|
|
649
637
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
638
|
+
if (platform === "android") {
|
|
639
|
+
await this._buildAndroidAsync(outPath, buildType);
|
|
640
|
+
} else {
|
|
641
|
+
throw new NotImplementError();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
655
644
|
}
|
|
656
645
|
|
|
657
|
-
private async _buildAndroidAsync(
|
|
658
|
-
|
|
659
|
-
outPath: string,
|
|
660
|
-
buildType: string,
|
|
661
|
-
): Promise<void> {
|
|
662
|
-
const androidPath = path.resolve(capacitorPath, "android");
|
|
646
|
+
private async _buildAndroidAsync(outPath: string, buildType: string): Promise<void> {
|
|
647
|
+
const androidPath = path.resolve(this._capPath, "android");
|
|
663
648
|
const targetOutPath = path.resolve(outPath, "android");
|
|
664
649
|
|
|
665
650
|
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
@@ -725,30 +710,6 @@ export class SdCliCapacitor {
|
|
|
725
710
|
);
|
|
726
711
|
}
|
|
727
712
|
|
|
728
|
-
private _findAndroidSdk(): string | undefined {
|
|
729
|
-
// 1. 환경변수 확인
|
|
730
|
-
const fromEnv = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"];
|
|
731
|
-
if (fromEnv != null && FsUtils.exists(fromEnv)) {
|
|
732
|
-
return fromEnv;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// 2. 일반적인 설치 경로 탐색
|
|
736
|
-
const candidates = [
|
|
737
|
-
path.resolve(process.env["LOCALAPPDATA"] ?? "", "Android/Sdk"),
|
|
738
|
-
path.resolve(process.env["HOME"] ?? "", "Android/Sdk"),
|
|
739
|
-
"C:/Program Files/Android/Sdk",
|
|
740
|
-
"C:/Android/Sdk",
|
|
741
|
-
];
|
|
742
|
-
|
|
743
|
-
for (const candidate of candidates) {
|
|
744
|
-
if (FsUtils.exists(candidate)) {
|
|
745
|
-
return candidate;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
return undefined;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
713
|
static async runWebviewOnDeviceAsync(opt: {
|
|
753
714
|
platform: string;
|
|
754
715
|
package: string;
|
|
@@ -792,7 +753,7 @@ export class SdCliCapacitor {
|
|
|
792
753
|
try {
|
|
793
754
|
await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
|
|
794
755
|
} catch (err) {
|
|
795
|
-
await
|
|
756
|
+
await this._execAsync("adb", ["kill-server"], capacitorPath);
|
|
796
757
|
throw err;
|
|
797
758
|
}
|
|
798
759
|
}
|