@simplysm/sd-cli 12.15.68 → 12.15.70
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 +37 -0
- package/dist/entry/SdCliCapacitor.js +390 -0
- package/dist/pkg-builders/client/SdClientBuildRunner.d.ts +1 -0
- package/dist/pkg-builders/client/SdClientBuildRunner.js +14 -0
- package/dist/pkg-builders/client/SdNgBundler.js +3 -1
- package/dist/sd-cli-entry.js +18 -44
- package/dist/types/config/ISdProjectConfig.d.ts +31 -0
- package/package.json +5 -5
- package/src/entry/SdCliCapacitor.ts +560 -0
- package/src/pkg-builders/client/SdClientBuildRunner.ts +17 -0
- package/src/pkg-builders/client/SdNgBundler.ts +3 -1
- package/src/sd-cli-entry.ts +26 -56
- package/src/types/config/ISdProjectConfig.ts +34 -0
- package/dist/fix/convertPrivateToHash.d.ts +0 -1
- package/dist/fix/convertPrivateToHash.js +0 -58
- package/dist/fix/convertSdAngularSymbolNames.d.ts +0 -1
- package/dist/fix/convertSdAngularSymbolNames.js +0 -22
- package/dist/fix/core/convertSymbols.d.ts +0 -1
- package/dist/fix/core/convertSymbols.js +0 -101
- package/dist/fix/core/getTsMortphSourceFiles.d.ts +0 -1
- package/dist/fix/core/getTsMortphSourceFiles.js +0 -7
- package/dist/fix/core/removeSymbols.d.ts +0 -1
- package/dist/fix/core/removeSymbols.js +0 -76
- package/dist/fix/removeSdAngularSymbolNames.d.ts +0 -1
- package/dist/fix/removeSdAngularSymbolNames.js +0 -6
- package/dist/fix/removeUnusedImports.d.ts +0 -1
- package/dist/fix/removeUnusedImports.js +0 -41
- package/dist/fix/removeUnusedInjects.d.ts +0 -1
- package/dist/fix/removeUnusedInjects.js +0 -37
- package/dist/fix/removeUnusedProtectedReadonly.d.ts +0 -1
- package/dist/fix/removeUnusedProtectedReadonly.js +0 -57
- package/src/fix/convertPrivateToHash.ts +0 -74
- package/src/fix/convertSdAngularSymbolNames.ts +0 -27
- package/src/fix/core/convertSymbols.ts +0 -135
- package/src/fix/core/getTsMortphSourceFiles.ts +0 -9
- package/src/fix/core/removeSymbols.ts +0 -102
- package/src/fix/removeSdAngularSymbolNames.ts +0 -9
- package/src/fix/removeUnusedImports.ts +0 -50
- package/src/fix/removeUnusedInjects.ts +0 -50
- package/src/fix/removeUnusedProtectedReadonly.ts +0 -69
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { FsUtils, PathUtils, SdLogger, SdProcess } from "@simplysm/sd-core-node";
|
|
3
|
+
import { ISdClientBuilderCapacitorConfig } from "../types/config/ISdProjectConfig";
|
|
4
|
+
import { INpmConfig } from "../types/common-config/INpmConfig";
|
|
5
|
+
import { StringUtils, typescript } from "@simplysm/sd-core-common";
|
|
6
|
+
|
|
7
|
+
export class SdCliCapacitor {
|
|
8
|
+
// 상수 정의
|
|
9
|
+
private readonly _CAPACITOR_DIR_NAME = ".capacitor";
|
|
10
|
+
private readonly _CONFIG_FILE_NAME = "capacitor.config.ts";
|
|
11
|
+
private readonly _KEYSTORE_FILE_NAME = "android.keystore";
|
|
12
|
+
private readonly _ICON_DIR_PATH = "resources";
|
|
13
|
+
|
|
14
|
+
// private readonly _ANDROID_DIR_NAME = "android";
|
|
15
|
+
// private readonly _WWW_DIR_NAME = "www";
|
|
16
|
+
|
|
17
|
+
private readonly _platforms: string[];
|
|
18
|
+
private readonly _npmConfig: INpmConfig;
|
|
19
|
+
|
|
20
|
+
constructor(private readonly _opt: { pkgPath: string; config: ISdClientBuilderCapacitorConfig }) {
|
|
21
|
+
this._platforms = Object.keys(this._opt.config.platform ?? { android: {} });
|
|
22
|
+
this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private static readonly _logger = SdLogger.get(["simplysm", "sd-cli", "SdCliCapacitor"]);
|
|
26
|
+
|
|
27
|
+
private static async _execAsync(cmd: string, args: string[], cwd: string): Promise<void> {
|
|
28
|
+
this._logger.debug(`실행 명령: ${cmd + " " + args.join(" ")}`);
|
|
29
|
+
const msg = await SdProcess.spawnAsync(cmd, args, { cwd });
|
|
30
|
+
this._logger.debug(`실행 결과: ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async initializeAsync(): Promise<void> {
|
|
34
|
+
const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
|
|
35
|
+
|
|
36
|
+
// 1. Capacitor 프로젝트 초기화
|
|
37
|
+
await this._initializeCapacitorProjectAsync(capacitorPath);
|
|
38
|
+
|
|
39
|
+
// 2. Capacitor 설정 파일 생성
|
|
40
|
+
this._createCapacitorConfig(capacitorPath);
|
|
41
|
+
|
|
42
|
+
// 3. 플랫폼 관리
|
|
43
|
+
await this._managePlatformsAsync(capacitorPath);
|
|
44
|
+
|
|
45
|
+
// 4. 플러그인 관리
|
|
46
|
+
await this._managePluginsAsync(capacitorPath);
|
|
47
|
+
|
|
48
|
+
// 5. 안드로이드 서명 설정
|
|
49
|
+
this._setupAndroidSign(capacitorPath);
|
|
50
|
+
|
|
51
|
+
// 6. 아이콘 및 스플래시 스크린 설정
|
|
52
|
+
await this._setupIconAndSplashScreenAsync(capacitorPath);
|
|
53
|
+
|
|
54
|
+
// 7. Android 네이티브 설정 (AndroidManifest.xml, build.gradle 등)
|
|
55
|
+
if (this._platforms.includes("android")) {
|
|
56
|
+
this._configureAndroidNative(capacitorPath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 8. 웹 자산 동기화
|
|
60
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 1. Capacitor 프로젝트 초기화
|
|
64
|
+
private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<void> {
|
|
65
|
+
if (FsUtils.exists(capacitorPath)) {
|
|
66
|
+
SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
|
|
67
|
+
|
|
68
|
+
// 버전 동기화
|
|
69
|
+
this._syncVersion(capacitorPath);
|
|
70
|
+
} else {
|
|
71
|
+
FsUtils.mkdirs(capacitorPath);
|
|
72
|
+
|
|
73
|
+
// package.json 생성
|
|
74
|
+
const pkgJson = {
|
|
75
|
+
name: this._opt.config.appId,
|
|
76
|
+
version: this._npmConfig.version,
|
|
77
|
+
private: true,
|
|
78
|
+
dependencies: {
|
|
79
|
+
"@capacitor/core": "^7.0.0",
|
|
80
|
+
},
|
|
81
|
+
devDependencies: {
|
|
82
|
+
"@capacitor/cli": "^7.0.0",
|
|
83
|
+
"@capacitor/assets": "^3.0.0",
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
FsUtils.writeJson(path.resolve(capacitorPath, "package.json"), pkgJson, { space: 2 });
|
|
87
|
+
|
|
88
|
+
// yarn install
|
|
89
|
+
await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
|
|
90
|
+
|
|
91
|
+
// capacitor init
|
|
92
|
+
await SdCliCapacitor._execAsync(
|
|
93
|
+
"npx",
|
|
94
|
+
["cap", "init", this._opt.config.appName, this._opt.config.appId],
|
|
95
|
+
capacitorPath,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 버전 동기화
|
|
101
|
+
private _syncVersion(capacitorPath: string): void {
|
|
102
|
+
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
103
|
+
|
|
104
|
+
if (FsUtils.exists(pkgJsonPath)) {
|
|
105
|
+
const pkgJson = FsUtils.readJson(pkgJsonPath) as INpmConfig;
|
|
106
|
+
|
|
107
|
+
if (pkgJson.version !== this._npmConfig.version) {
|
|
108
|
+
pkgJson.version = this._npmConfig.version;
|
|
109
|
+
FsUtils.writeJson(pkgJsonPath, pkgJson, { space: 2 });
|
|
110
|
+
SdCliCapacitor._logger.log(`버전 동기화: ${this._npmConfig.version}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Capacitor 설정 파일 생성
|
|
116
|
+
private _createCapacitorConfig(capacitorPath: string): void {
|
|
117
|
+
const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
|
|
118
|
+
|
|
119
|
+
// 플러그인 옵션 생성
|
|
120
|
+
const pluginOptions: Record<string, Record<string, unknown>> = {};
|
|
121
|
+
for (const [pluginName, options] of Object.entries(this._opt.config.plugins ?? {})) {
|
|
122
|
+
if (options !== true) {
|
|
123
|
+
// @capacitor/splash-screen → SplashScreen 형태로 변환
|
|
124
|
+
const configKey = StringUtils.toPascalCase(pluginName.split("/").last()!);
|
|
125
|
+
pluginOptions[configKey] = options;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const pluginsConfigStr =
|
|
130
|
+
Object.keys(pluginOptions).length > 0
|
|
131
|
+
? JSON.stringify(pluginOptions, null, 4).replace(/^/gm, " ").trim()
|
|
132
|
+
: "{}";
|
|
133
|
+
|
|
134
|
+
const configContent = typescript`
|
|
135
|
+
import type { CapacitorConfig } from "@capacitor/cli";
|
|
136
|
+
|
|
137
|
+
const config: CapacitorConfig = {
|
|
138
|
+
appId: "${this._opt.config.appId}",
|
|
139
|
+
appName: "${this._opt.config.appName}",
|
|
140
|
+
server: {
|
|
141
|
+
androidScheme: "http",
|
|
142
|
+
cleartext: true,
|
|
143
|
+
allowNavigation: ["*"],
|
|
144
|
+
},
|
|
145
|
+
android: {
|
|
146
|
+
allowMixedContent: true,
|
|
147
|
+
},
|
|
148
|
+
plugins: ${pluginsConfigStr},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default config;
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
FsUtils.writeFile(configFilePath, configContent);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. 플랫폼 관리
|
|
158
|
+
private async _managePlatformsAsync(capacitorPath: string): Promise<void> {
|
|
159
|
+
for (const platform of this._platforms) {
|
|
160
|
+
if (FsUtils.exists(path.resolve(capacitorPath, platform))) continue;
|
|
161
|
+
|
|
162
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], capacitorPath);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 4. 플러그인 관리
|
|
167
|
+
private async _managePluginsAsync(capacitorPath: string): Promise<void> {
|
|
168
|
+
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
169
|
+
const pkgJson = FsUtils.readJson(pkgJsonPath) as INpmConfig;
|
|
170
|
+
const currentDeps = Object.keys(pkgJson.dependencies ?? {});
|
|
171
|
+
|
|
172
|
+
const usePlugins = Object.keys(this._opt.config.plugins ?? {});
|
|
173
|
+
|
|
174
|
+
// 사용하지 않는 플러그인 제거
|
|
175
|
+
for (const dep of currentDeps) {
|
|
176
|
+
// @capacitor/core, @capacitor/android 등 기본 패키지는 제외
|
|
177
|
+
if (
|
|
178
|
+
dep.startsWith("@capacitor/") &&
|
|
179
|
+
["core", "android", "ios"].some((p) => dep.endsWith(p))
|
|
180
|
+
) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 플러그인 목록에 없는 패키지는 제거
|
|
185
|
+
if (!usePlugins.includes(dep)) {
|
|
186
|
+
// Capacitor 관련 플러그인만 제거
|
|
187
|
+
if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
|
|
188
|
+
try {
|
|
189
|
+
await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
|
|
190
|
+
SdCliCapacitor._logger.log(`플러그인 제거: ${dep}`);
|
|
191
|
+
} catch {
|
|
192
|
+
SdCliCapacitor._logger.warn(`플러그인 제거 실패: ${dep}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 새 플러그인 설치
|
|
199
|
+
for (const plugin of usePlugins) {
|
|
200
|
+
if (!currentDeps.includes(plugin)) {
|
|
201
|
+
try {
|
|
202
|
+
await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
|
|
203
|
+
SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
|
|
204
|
+
} catch {
|
|
205
|
+
SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 5. 안드로이드 서명 설정
|
|
212
|
+
private _setupAndroidSign(capacitorPath: string): void {
|
|
213
|
+
const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
|
|
214
|
+
|
|
215
|
+
if (this._opt.config.platform?.android?.sign) {
|
|
216
|
+
FsUtils.copy(
|
|
217
|
+
path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore),
|
|
218
|
+
keystorePath,
|
|
219
|
+
);
|
|
220
|
+
} else {
|
|
221
|
+
FsUtils.remove(keystorePath);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 6. 아이콘 및 스플래시 스크린 설정
|
|
226
|
+
private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
|
|
227
|
+
const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
|
|
228
|
+
|
|
229
|
+
// ICON 파일 복사
|
|
230
|
+
if (this._opt.config.icon != null) {
|
|
231
|
+
FsUtils.mkdirs(iconDirPath);
|
|
232
|
+
|
|
233
|
+
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
234
|
+
|
|
235
|
+
// icon.png, splash.png 둘 다 같은 파일 사용
|
|
236
|
+
FsUtils.copy(iconSource, path.resolve(iconDirPath, "icon.png"));
|
|
237
|
+
FsUtils.copy(iconSource, path.resolve(iconDirPath, "splash.png"));
|
|
238
|
+
|
|
239
|
+
// @capacitor/assets로 아이콘/스플래시 리사이징
|
|
240
|
+
try {
|
|
241
|
+
await SdCliCapacitor._execAsync(
|
|
242
|
+
"npx",
|
|
243
|
+
["@capacitor/assets", "generate", "--android"],
|
|
244
|
+
capacitorPath,
|
|
245
|
+
);
|
|
246
|
+
} catch {
|
|
247
|
+
SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
FsUtils.remove(iconDirPath);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 7. Android 네이티브 설정
|
|
255
|
+
private _configureAndroidNative(capacitorPath: string): void {
|
|
256
|
+
const androidPath = path.resolve(capacitorPath, "android");
|
|
257
|
+
|
|
258
|
+
if (!FsUtils.exists(androidPath)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// AndroidManifest.xml 수정
|
|
263
|
+
this._configureAndroidManifest(androidPath);
|
|
264
|
+
|
|
265
|
+
// build.gradle 수정 (필요시)
|
|
266
|
+
this._configureAndroidBuildGradle(androidPath);
|
|
267
|
+
|
|
268
|
+
// strings.xml 앱 이름 수정
|
|
269
|
+
this._configureAndroidStrings(androidPath);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private _configureAndroidManifest(androidPath: string): void {
|
|
273
|
+
const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
|
|
274
|
+
|
|
275
|
+
if (!FsUtils.exists(manifestPath)) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let manifestContent = FsUtils.readFile(manifestPath);
|
|
280
|
+
|
|
281
|
+
// usesCleartextTraffic 설정
|
|
282
|
+
if (!manifestContent.includes("android:usesCleartextTraffic")) {
|
|
283
|
+
manifestContent = manifestContent.replace(
|
|
284
|
+
"<application",
|
|
285
|
+
'<application android:usesCleartextTraffic="true"',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 추가 권한 설정
|
|
290
|
+
const permissions = this._opt.config.platform?.android?.permissions ?? [];
|
|
291
|
+
for (const perm of permissions) {
|
|
292
|
+
const permTag = `<uses-permission android:name="android.permission.${perm.name}"`;
|
|
293
|
+
if (!manifestContent.includes(permTag)) {
|
|
294
|
+
const maxSdkAttr =
|
|
295
|
+
perm.maxSdkVersion != null ? ` android:maxSdkVersion="${perm.maxSdkVersion}"` : "";
|
|
296
|
+
const ignoreAttr = perm.ignore != null ? ` tools:ignore="${perm.ignore}"` : "";
|
|
297
|
+
const permLine = ` ${permTag}${maxSdkAttr}${ignoreAttr} />\n`;
|
|
298
|
+
|
|
299
|
+
// tools 네임스페이스 추가
|
|
300
|
+
if (perm.ignore != null && !manifestContent.includes("xmlns:tools=")) {
|
|
301
|
+
manifestContent = manifestContent.replace(
|
|
302
|
+
"<manifest xmlns:android",
|
|
303
|
+
'<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android',
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
manifestContent = manifestContent.replace("</manifest>", `${permLine}</manifest>`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 추가 application 설정
|
|
312
|
+
const appConfig = this._opt.config.platform?.android?.config;
|
|
313
|
+
if (appConfig) {
|
|
314
|
+
for (const [key, value] of Object.entries(appConfig)) {
|
|
315
|
+
const attr = `android:${key}="${value}"`;
|
|
316
|
+
if (!manifestContent.includes(`android:${key}=`)) {
|
|
317
|
+
manifestContent = manifestContent.replace("<application", `<application ${attr}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
FsUtils.writeFile(manifestPath, manifestContent);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private _configureAndroidBuildGradle(androidPath: string): void {
|
|
326
|
+
const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
|
|
327
|
+
|
|
328
|
+
if (!FsUtils.exists(buildGradlePath)) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let gradleContent = FsUtils.readFile(buildGradlePath);
|
|
333
|
+
|
|
334
|
+
// versionName, versionCode 설정
|
|
335
|
+
const version = this._npmConfig.version;
|
|
336
|
+
const versionParts = version.split(".");
|
|
337
|
+
const versionCode =
|
|
338
|
+
parseInt(versionParts[0] ?? "0") * 10000 +
|
|
339
|
+
parseInt(versionParts[1] ?? "0") * 100 +
|
|
340
|
+
parseInt(versionParts[2] ?? "0");
|
|
341
|
+
|
|
342
|
+
gradleContent = gradleContent.replace(/versionCode \d+/, `versionCode ${versionCode}`);
|
|
343
|
+
gradleContent = gradleContent.replace(/versionName "[^"]+"/, `versionName "${version}"`);
|
|
344
|
+
|
|
345
|
+
// SDK 버전 설정
|
|
346
|
+
if (this._opt.config.platform?.android?.sdkVersion != null) {
|
|
347
|
+
const sdkVersion = this._opt.config.platform.android.sdkVersion;
|
|
348
|
+
gradleContent = gradleContent.replace(/minSdkVersion \d+/, `minSdkVersion ${sdkVersion}`);
|
|
349
|
+
gradleContent = gradleContent.replace(
|
|
350
|
+
/targetSdkVersion \d+/,
|
|
351
|
+
`targetSdkVersion ${sdkVersion}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Signing 설정
|
|
356
|
+
const signConfig = this._opt.config.platform?.android?.sign;
|
|
357
|
+
if (signConfig) {
|
|
358
|
+
const keystoreRelativePath = `../${this._KEYSTORE_FILE_NAME}`;
|
|
359
|
+
const keystoreType = signConfig.keystoreType ?? "jks";
|
|
360
|
+
|
|
361
|
+
// signingConfigs 블록 추가
|
|
362
|
+
if (!gradleContent.includes("signingConfigs")) {
|
|
363
|
+
const signingConfigsBlock = `
|
|
364
|
+
signingConfigs {
|
|
365
|
+
release {
|
|
366
|
+
storeFile file("${keystoreRelativePath}")
|
|
367
|
+
storePassword "${signConfig.storePassword}"
|
|
368
|
+
keyAlias "${signConfig.alias}"
|
|
369
|
+
keyPassword "${signConfig.password}"
|
|
370
|
+
storeType "${keystoreType}"
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
`;
|
|
374
|
+
// android { 블록 내부에 추가
|
|
375
|
+
gradleContent = gradleContent.replace(/(android\s*\{)/, `$1${signingConfigsBlock}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// buildTypes.release에 signingConfig 추가
|
|
379
|
+
if (!gradleContent.includes("signingConfig signingConfigs.release")) {
|
|
380
|
+
gradleContent = gradleContent.replace(
|
|
381
|
+
/(buildTypes\s*\{[\s\S]*?release\s*\{)/,
|
|
382
|
+
`$1\n signingConfig signingConfigs.release`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
FsUtils.writeFile(buildGradlePath, gradleContent);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private _configureAndroidStrings(androidPath: string): void {
|
|
391
|
+
const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
|
|
392
|
+
|
|
393
|
+
if (!FsUtils.exists(stringsPath)) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let stringsContent = FsUtils.readFile(stringsPath);
|
|
398
|
+
stringsContent = stringsContent.replace(
|
|
399
|
+
/<string name="app_name">[^<]+<\/string>/,
|
|
400
|
+
`<string name="app_name">${this._opt.config.appName}</string>`,
|
|
401
|
+
);
|
|
402
|
+
stringsContent = stringsContent.replace(
|
|
403
|
+
/<string name="title_activity_main">[^<]+<\/string>/,
|
|
404
|
+
`<string name="title_activity_main">${this._opt.config.appName}</string>`,
|
|
405
|
+
);
|
|
406
|
+
stringsContent = stringsContent.replace(
|
|
407
|
+
/<string name="package_name">[^<]+<\/string>/,
|
|
408
|
+
`<string name="package_name">${this._opt.config.appId}</string>`,
|
|
409
|
+
);
|
|
410
|
+
stringsContent = stringsContent.replace(
|
|
411
|
+
/<string name="custom_url_scheme">[^<]+<\/string>/,
|
|
412
|
+
`<string name="custom_url_scheme">${this._opt.config.appId}</string>`,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
FsUtils.writeFile(stringsPath, stringsContent);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async buildAsync(outPath: string): Promise<void> {
|
|
419
|
+
const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
|
|
420
|
+
const buildType = this._opt.config.debug ? "debug" : "release";
|
|
421
|
+
|
|
422
|
+
// 웹 자산 동기화
|
|
423
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
|
|
424
|
+
|
|
425
|
+
// 플랫폼별 빌드
|
|
426
|
+
await Promise.all(
|
|
427
|
+
this._platforms.map((platform) =>
|
|
428
|
+
this._buildPlatformAsync(capacitorPath, outPath, platform, buildType),
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private async _buildPlatformAsync(
|
|
434
|
+
capacitorPath: string,
|
|
435
|
+
outPath: string,
|
|
436
|
+
platform: string,
|
|
437
|
+
buildType: string,
|
|
438
|
+
): Promise<void> {
|
|
439
|
+
if (platform === "android") {
|
|
440
|
+
await this._buildAndroidAsync(capacitorPath, outPath, buildType);
|
|
441
|
+
}
|
|
442
|
+
// iOS 지원 시 추가
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private async _buildAndroidAsync(
|
|
446
|
+
capacitorPath: string,
|
|
447
|
+
outPath: string,
|
|
448
|
+
buildType: string,
|
|
449
|
+
): Promise<void> {
|
|
450
|
+
const androidPath = path.resolve(capacitorPath, "android");
|
|
451
|
+
const targetOutPath = path.resolve(outPath, "android");
|
|
452
|
+
|
|
453
|
+
// Gradle wrapper로 빌드
|
|
454
|
+
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
455
|
+
const gradleTask =
|
|
456
|
+
buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
|
|
457
|
+
|
|
458
|
+
// gradlew 실행 권한 부여 (Linux/Mac)
|
|
459
|
+
const gradlewPath = path.resolve(androidPath, "gradlew");
|
|
460
|
+
if (FsUtils.exists(gradlewPath)) {
|
|
461
|
+
try {
|
|
462
|
+
await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
|
|
463
|
+
} catch {
|
|
464
|
+
// Windows에서는 무시
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Gradle 빌드 실행
|
|
469
|
+
const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
|
|
470
|
+
await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
|
|
471
|
+
|
|
472
|
+
// 빌드 결과물 복사
|
|
473
|
+
this._copyAndroidBuildOutput(androidPath, targetOutPath, buildType);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private _copyAndroidBuildOutput(
|
|
477
|
+
androidPath: string,
|
|
478
|
+
targetOutPath: string,
|
|
479
|
+
buildType: string,
|
|
480
|
+
): void {
|
|
481
|
+
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
482
|
+
const isSigned = !!this._opt.config.platform?.android?.sign;
|
|
483
|
+
|
|
484
|
+
const ext = isBundle ? "aab" : "apk";
|
|
485
|
+
const outputType = isBundle ? "bundle" : "apk";
|
|
486
|
+
const fileName = isSigned ? `app-${buildType}.${ext}` : `app-${buildType}-unsigned.${ext}`;
|
|
487
|
+
|
|
488
|
+
const sourcePath = path.resolve(
|
|
489
|
+
androidPath,
|
|
490
|
+
"app/build/outputs",
|
|
491
|
+
outputType,
|
|
492
|
+
buildType,
|
|
493
|
+
fileName,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const actualPath = FsUtils.exists(sourcePath)
|
|
497
|
+
? sourcePath
|
|
498
|
+
: path.resolve(
|
|
499
|
+
androidPath,
|
|
500
|
+
"app/build/outputs",
|
|
501
|
+
outputType,
|
|
502
|
+
buildType,
|
|
503
|
+
`app-${buildType}.${ext}`,
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (!FsUtils.exists(actualPath)) {
|
|
507
|
+
SdCliCapacitor._logger.warn(`빌드 결과물을 찾을 수 없습니다: ${actualPath}`);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const outputFileName = `${this._opt.config.appName}${isSigned ? "" : "-unsigned"}-latest.${ext}`;
|
|
512
|
+
|
|
513
|
+
FsUtils.mkdirs(targetOutPath);
|
|
514
|
+
FsUtils.copy(actualPath, path.resolve(targetOutPath, outputFileName));
|
|
515
|
+
|
|
516
|
+
const updatesPath = path.resolve(targetOutPath, "updates");
|
|
517
|
+
FsUtils.mkdirs(updatesPath);
|
|
518
|
+
FsUtils.copy(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
static async runWebviewOnDeviceAsync(opt: {
|
|
522
|
+
platform: string;
|
|
523
|
+
package: string;
|
|
524
|
+
url?: string;
|
|
525
|
+
}): Promise<void> {
|
|
526
|
+
const projNpmConf = FsUtils.readJson(path.resolve(process.cwd(), "package.json")) as INpmConfig;
|
|
527
|
+
const allPkgPaths = projNpmConf.workspaces!.mapMany((item) =>
|
|
528
|
+
FsUtils.glob(PathUtils.posix(process.cwd(), item)),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const capacitorPath = path.resolve(
|
|
532
|
+
allPkgPaths.single((item) => item.endsWith(opt.package))!,
|
|
533
|
+
".capacitor",
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
if (opt.url !== undefined) {
|
|
537
|
+
// capacitor.config.ts의 server.url 설정 업데이트
|
|
538
|
+
const configPath = path.resolve(capacitorPath, "capacitor.config.ts");
|
|
539
|
+
if (FsUtils.exists(configPath)) {
|
|
540
|
+
let configContent = FsUtils.readFile(configPath);
|
|
541
|
+
const serverUrl = `${opt.url.replace(/\/$/, "")}/${opt.package}/capacitor/`;
|
|
542
|
+
|
|
543
|
+
// 기존 url 설정이 있으면 교체, 없으면 server 블록 첫 줄에 추가
|
|
544
|
+
if (configContent.includes("url:")) {
|
|
545
|
+
configContent = configContent.replace(/url:\s*"[^"]*"/, `url: "${serverUrl}"`);
|
|
546
|
+
} else if (configContent.includes("server:")) {
|
|
547
|
+
configContent = configContent.replace(
|
|
548
|
+
/server:\s*\{/,
|
|
549
|
+
`server: {\n url: "${serverUrl}",`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
FsUtils.writeFile(configPath, configContent);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// cap sync 후 run
|
|
557
|
+
await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
|
|
558
|
+
await this._execAsync("npx", ["cap", "run", opt.platform, "--target", "device"], capacitorPath);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
@@ -8,12 +8,14 @@ import { INpmConfig } from "../../types/common-config/INpmConfig";
|
|
|
8
8
|
import { SdCliNgRoutesFileGenerator } from "./SdCliNgRoutesFileGenerator";
|
|
9
9
|
import { SdCliElectron } from "../../entry/SdCliElectron";
|
|
10
10
|
import { ISdClientPackageConfig } from "../../types/config/ISdProjectConfig";
|
|
11
|
+
import { SdCliCapacitor } from "../../entry/SdCliCapacitor";
|
|
11
12
|
|
|
12
13
|
export class SdClientBuildRunner extends SdBuildRunnerBase<"client"> {
|
|
13
14
|
protected override _logger = SdLogger.get(["simplysm", "sd-cli", "SdClientBuildRunner"]);
|
|
14
15
|
|
|
15
16
|
private _ngBundlers?: SdNgBundler[];
|
|
16
17
|
private _cordova?: SdCliCordova;
|
|
18
|
+
private _capacitor?: SdCliCapacitor
|
|
17
19
|
|
|
18
20
|
protected override async _runAsync(modifiedFileSet?: Set<TNormPath>): Promise<ISdBuildResult> {
|
|
19
21
|
// 최초 한번
|
|
@@ -37,6 +39,16 @@ export class SdClientBuildRunner extends SdBuildRunnerBase<"client"> {
|
|
|
37
39
|
await this._cordova.initializeAsync();
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
// capacitor
|
|
43
|
+
if (this._pkgConf.builder?.capacitor) {
|
|
44
|
+
this._debug("Preparing Capacitor...");
|
|
45
|
+
this._capacitor = new SdCliCapacitor({
|
|
46
|
+
pkgPath: this._opt.pkgPath,
|
|
47
|
+
config: this._pkgConf.builder.capacitor,
|
|
48
|
+
});
|
|
49
|
+
await this._capacitor.initializeAsync();
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
// routes
|
|
41
53
|
const npmConf = (await FsUtils.readJsonAsync(
|
|
42
54
|
path.resolve(this._opt.pkgPath, "package.json"),
|
|
@@ -118,6 +130,11 @@ export class SdClientBuildRunner extends SdBuildRunnerBase<"client"> {
|
|
|
118
130
|
await this._cordova.buildAsync(path.resolve(this._opt.pkgPath, "dist"));
|
|
119
131
|
}
|
|
120
132
|
|
|
133
|
+
if (!this._opt.watch?.dev && this._capacitor) {
|
|
134
|
+
this._debug("Building Capacitor...");
|
|
135
|
+
await this._capacitor.buildAsync(path.resolve(this._opt.pkgPath, "dist"));
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
if (!this._opt.watch?.dev && this._pkgConf.builder?.electron) {
|
|
122
139
|
this._debug("Bulding Electron...");
|
|
123
140
|
await SdCliElectron.buildAsync({
|
|
@@ -90,7 +90,9 @@ export class SdNgBundler {
|
|
|
90
90
|
? PathUtils.norm(this._opt.pkgPath, ".electron/src")
|
|
91
91
|
: this._conf.builderType === "cordova" && !this._opt.watch?.dev
|
|
92
92
|
? PathUtils.norm(this._opt.pkgPath, ".cordova/www")
|
|
93
|
-
:
|
|
93
|
+
: this._conf.builderType === "capacitor" && !this._opt.watch?.dev
|
|
94
|
+
? PathUtils.norm(this._opt.pkgPath, ".capacitor/www")
|
|
95
|
+
: PathUtils.norm(this._opt.pkgPath, "dist", this._conf.builderType);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
markForChanges(filePaths: string[]): void {
|