@simplysm/sd-cli 12.8.18 → 12.8.21

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.
Files changed (37) hide show
  1. package/dist/entry/sd-cli-cordova.d.ts +30 -0
  2. package/dist/entry/sd-cli-cordova.js +309 -249
  3. package/dist/entry/sd-cli-cordova.js.map +1 -1
  4. package/dist/entry/sd-cli-project.d.ts +1 -1
  5. package/dist/entry/sd-cli-project.js +8 -9
  6. package/dist/entry/sd-cli-project.js.map +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/pkg-builders/client/sd-ng.bundler.d.ts +15 -1
  11. package/dist/pkg-builders/client/sd-ng.bundler.js +60 -70
  12. package/dist/pkg-builders/client/sd-ng.bundler.js.map +1 -1
  13. package/dist/pkg-builders/client/sd-ng.plugin-creator.js +49 -29
  14. package/dist/pkg-builders/client/sd-ng.plugin-creator.js.map +1 -1
  15. package/dist/pkg-builders/lib/sd-ts-lib.builder.js +7 -4
  16. package/dist/pkg-builders/lib/sd-ts-lib.builder.js.map +1 -1
  17. package/dist/ts-compiler/sd-ts-compiler.d.ts +24 -2
  18. package/dist/ts-compiler/sd-ts-compiler.js +267 -575
  19. package/dist/ts-compiler/sd-ts-compiler.js.map +1 -1
  20. package/dist/ts-compiler/sd-ts-dependency-analyzer.d.ts +10 -0
  21. package/dist/ts-compiler/sd-ts-dependency-analyzer.js +140 -0
  22. package/dist/ts-compiler/sd-ts-dependency-analyzer.js.map +1 -0
  23. package/dist/types/ts-compiler.types.d.ts +12 -7
  24. package/dist/types/worker.types.d.ts +13 -0
  25. package/dist/utils/sd-cli-performance-time.js +1 -1
  26. package/package.json +10 -12
  27. package/src/entry/sd-cli-cordova.ts +394 -281
  28. package/src/entry/sd-cli-project.ts +11 -23
  29. package/src/index.ts +1 -0
  30. package/src/pkg-builders/client/sd-ng.bundler.ts +70 -69
  31. package/src/pkg-builders/client/sd-ng.plugin-creator.ts +47 -27
  32. package/src/pkg-builders/lib/sd-ts-lib.builder.ts +14 -7
  33. package/src/ts-compiler/sd-ts-compiler.ts +334 -705
  34. package/src/ts-compiler/sd-ts-dependency-analyzer.ts +176 -0
  35. package/src/types/ts-compiler.types.ts +11 -6
  36. package/src/types/worker.types.ts +7 -6
  37. package/src/utils/sd-cli-performance-time.ts +1 -1
@@ -1,13 +1,28 @@
1
1
  import * as path from "path";
2
2
  import { FsUtils, SdLogger, SdProcess } from "@simplysm/sd-core-node";
3
- import xml2js from "xml2js";
4
- import JSZip from "jszip";
5
3
  import { INpmConfig } from "../types/common-configs.types";
6
4
  import { ISdClientBuilderCordovaConfig } from "../types/config.types";
7
-
8
- // const BIN_PATH = path.resolve(process.cwd(), "node_modules/.bin/cordova.cmd");
5
+ import { SdZip, XmlConvert } from "@simplysm/sd-core-common";
9
6
 
10
7
  export class SdCliCordova {
8
+ // 상수 정의
9
+ private readonly CORDOVA_DIR_NAME = ".cordova";
10
+ private readonly PLATFORMS_DIR_NAME = "platforms";
11
+ private readonly WWW_DIR_NAME = "www";
12
+
13
+ private readonly PLUGINS_DIR_NAME = "plugins";
14
+ private readonly PLUGINS_FETCH_FILE = "fetch.json";
15
+ private readonly ANDROID_PLATFORM_VERSION = "12.0.0";
16
+ private readonly ANDROID_SDK_VERSION = "33";
17
+ private readonly KEYSTORE_FILE_NAME = "android.keystore";
18
+ private readonly CONFIG_XML_FILE_NAME = "config.xml";
19
+ private readonly CONFIG_XML_BACKUP_FILE_NAME = "config.xml.bak";
20
+ private readonly BUILD_JSON_FILE_NAME = "build.json";
21
+ private readonly ANDROID_SIGNING_PROP_PATH = "platforms/android/release-signing.properties";
22
+ private readonly ICON_DIR_PATH = "res/icons";
23
+ private readonly SPLASH_SCREEN_DIR_PATH = "res/screen/android";
24
+ private readonly SPLASH_SCREEN_XML_FILE = "splashscreen.xml";
25
+
11
26
  private _logger = SdLogger.get(["simplysm", "sd-cli", "SdCliCordova"]);
12
27
 
13
28
  private _platforms: string[];
@@ -16,18 +31,51 @@ export class SdCliCordova {
16
31
  constructor(private readonly _opt: { pkgPath: string; config: ISdClientBuilderCordovaConfig }) {
17
32
  this._platforms = Object.keys(this._opt.config.platform ?? { browser: {} });
18
33
  this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
19
- // this._logger = Logger.get(["simplysm", "sd-cli", this.constructor.name, this._npmConfig.name]);
20
34
  }
21
35
 
22
36
  private async _execAsync(cmd: string, cwd: string): Promise<void> {
23
- this._logger.debug(cmd);
24
- const msg = await SdProcess.spawnAsync(cmd, { cwd });
25
- this._logger.debug(msg);
37
+ try {
38
+ this._logger.debug(`실행 명령: ${cmd}`);
39
+ const msg = await SdProcess.spawnAsync(cmd, { cwd });
40
+ this._logger.debug(`실행 결과: ${msg}`);
41
+ }
42
+ catch (err) {
43
+ this._logger.error(`명령 실행 실패: ${cmd}`);
44
+ this._logger.error(`오류: ${err instanceof Error ? err.message : String(err)}`);
45
+ throw err;
46
+ }
26
47
  }
27
48
 
28
49
  public async initializeAsync(): Promise<void> {
29
- const cordovaPath = path.resolve(this._opt.pkgPath, ".cordova");
50
+ const cordovaPath = path.resolve(this._opt.pkgPath, this.CORDOVA_DIR_NAME);
51
+
52
+ // 1. Cordova 프로젝트 초기화
53
+ await this._initializeCordovaProjectAsync(cordovaPath);
54
+
55
+ // 2. 플랫폼 관리
56
+ await this._managePlatformsAsync(cordovaPath);
57
+
58
+ // 3. 플러그인 관리
59
+ await this._managePluginsAsync(cordovaPath);
60
+
61
+ // 4. 안드로이드 서명 설정
62
+ this._setupAndroidSign(cordovaPath);
63
+
64
+ // 5. 빌드 설정 파일 생성
65
+ this._createBuildConfig(cordovaPath);
66
+
67
+ // 6. 아이콘 및 스플래시 스크린 설정
68
+ this._setupIconAndSplashScreen(cordovaPath);
30
69
 
70
+ // 7. XML 설정 구성
71
+ this._configureXml(cordovaPath);
72
+
73
+ // 8. 각 플랫폼 www 준비
74
+ await this._execAsync(`npx cordova prepare`, cordovaPath);
75
+ }
76
+
77
+ // 1. Cordova 프로젝트 초기화
78
+ private async _initializeCordovaProjectAsync(cordovaPath: string): Promise<void> {
31
79
  if (FsUtils.exists(cordovaPath)) {
32
80
  this._logger.log("이미 생성되어있는 '.cordova'를 사용합니다.");
33
81
  }
@@ -39,135 +87,173 @@ export class SdCliCordova {
39
87
  `npx cordova create "${cordovaPath}" "${this._opt.config.appId}" "${this._opt.config.appName}"`,
40
88
  process.cwd(),
41
89
  );
42
-
43
- // volta
44
- // await this._execAsync(`volta pin node@18`, cordovaPath);
45
-
46
- // package.json 수정
47
- /*const npmConfig = FsUtil.readJson(path.resolve(cordovaPath, "package.json"));
48
- npmConfig.volta = {
49
- node: process.version.substring(1)
50
- };
51
- FsUtil.writeJson(path.resolve(cordovaPath, "package.json"), npmConfig);*/
52
90
  }
53
91
 
54
92
  // platforms 폴더 혹시 없으면 생성
55
- FsUtils.mkdirs(path.resolve(cordovaPath, "platforms"));
93
+ FsUtils.mkdirs(path.resolve(cordovaPath, this.PLATFORMS_DIR_NAME));
56
94
 
57
95
  // www 폴더 혹시 없으면 생성
58
- FsUtils.mkdirs(path.resolve(cordovaPath, "www"));
96
+ FsUtils.mkdirs(path.resolve(cordovaPath, this.WWW_DIR_NAME));
97
+ }
59
98
 
60
- // 미설치 빌드 플랫폼 신규 생성
61
- const alreadyPlatforms = FsUtils.readdir(path.resolve(cordovaPath, "platforms"));
62
- for (const platform of this._platforms) {
63
- if (!alreadyPlatforms.includes(platform)) {
64
- // await this._execAsync(`${BIN_PATH} platform add ${platform}`, cordovaPath);
99
+ // 2. 플랫폼 관리
100
+ private async _managePlatformsAsync(cordovaPath: string): Promise<void> {
101
+ const alreadyPlatforms = FsUtils.readdir(path.resolve(cordovaPath, this.PLATFORMS_DIR_NAME));
102
+
103
+ // 미설치 빌드 플랫폼 신규 생성 - 병렬 처리
104
+ const platformInstallPromises = this._platforms
105
+ .filter(platform => !alreadyPlatforms.includes(platform))
106
+ .map(platform => {
65
107
  if (platform === "android") {
66
- await this._execAsync(`npx cordova platform add ${platform}@12.0.0`, cordovaPath);
108
+ return this._execAsync(
109
+ `npx cordova platform add ${platform}@${this.ANDROID_PLATFORM_VERSION}`,
110
+ cordovaPath,
111
+ );
67
112
  }
68
113
  else {
69
- await this._execAsync(`npx cordova platform add ${platform}`, cordovaPath);
114
+ return this._execAsync(`npx cordova platform add ${platform}`, cordovaPath);
70
115
  }
71
- }
72
- }
116
+ });
73
117
 
74
- // 설치 미빌드 플랫폼 삭제
75
- /*for (const alreadyPlatform of alreadyPlatforms) {
76
- if (!this._platforms.includes(alreadyPlatform)) {
77
- await this._execAsync(`${BIN_PATH} platform remove ${alreadyPlatform}`, cordovaPath);
78
- }
79
- }*/
118
+ await Promise.all(platformInstallPromises);
119
+ }
80
120
 
81
- // 설치된 미사용 플러그인 삭제
82
- const pluginsFetch = FsUtils.exists(path.resolve(cordovaPath, "plugins/fetch.json"))
83
- ? FsUtils.readJson(path.resolve(cordovaPath, "plugins/fetch.json"))
84
- : undefined;
121
+ // 3. 플러그인 관리
122
+ private async _managePluginsAsync(cordovaPath: string): Promise<void> {
123
+ const pluginsFetchPath = path.resolve(
124
+ cordovaPath,
125
+ this.PLUGINS_DIR_NAME,
126
+ this.PLUGINS_FETCH_FILE,
127
+ );
128
+ const pluginsFetch = FsUtils.exists(pluginsFetchPath)
129
+ ? FsUtils.readJson(pluginsFetchPath)
130
+ : {};
131
+
132
+ const alreadyPlugins: Array<{
133
+ name: string;
134
+ id: string;
135
+ dependencies?: string[]
136
+ }> = Object.keys(pluginsFetch).map(key => ({
137
+ name: key,
138
+ id: pluginsFetch[key].source.id,
139
+ dependencies: pluginsFetch[key].dependencies,
140
+ }));
85
141
 
86
- const alreadyPlugins: { name: string; id: string }[] = [];
142
+ const usePlugins = (this._opt.config.plugins ?? []).distinct();
87
143
 
88
- if (pluginsFetch != null) {
89
- for (const key of Object.keys(pluginsFetch)) {
90
- alreadyPlugins.push({
91
- name: key,
92
- id: pluginsFetch[key].source.id,
93
- });
94
- }
95
- }
144
+ // 사용하지 않는 플러그인 제거 및 새 플러그인 설치 - 의존성 때문에 순차 처리
145
+ await this._removeUnusedPluginsAsync(cordovaPath, alreadyPlugins, usePlugins);
146
+ await this._installNewPluginsAsync(cordovaPath, alreadyPlugins, usePlugins);
147
+ }
96
148
 
97
- const usePlugins = (this._opt.config.plugins ?? []).distinct();
149
+ private async _removeUnusedPluginsAsync(
150
+ cordovaPath: string,
151
+ alreadyPlugins: Array<{ name: string; id: string; dependencies?: string[] }>,
152
+ usePlugins: string[],
153
+ ): Promise<void> {
154
+ for (const alreadyPlugin of alreadyPlugins) {
155
+ // 사용하지 않는 플러그인 제거 시 의존성 검사
156
+ const isPluginUsed = usePlugins.some(
157
+ usePlugin => usePlugin === alreadyPlugin.id || usePlugin === alreadyPlugin.name,
158
+ );
98
159
 
99
- // TODO: Dependency에 의해 설치된 플러그인 삭제되면 안됨 android.json의 installed_plugin으로 변경하면 될지도?
100
- /*for (const alreadyPlugin of alreadyPlugins) {
101
- let hasPlugin = false;
102
- for (const usePlugin of usePlugins) {
103
- if (alreadyPlugin.name === usePlugin || alreadyPlugin.id === usePlugin) {
104
- hasPlugin = true;
105
- break;
106
- }
107
- }
160
+ if (!isPluginUsed) {
161
+ const isDependencyExists = alreadyPlugins.some(
162
+ plugin => plugin.dependencies?.includes(alreadyPlugin.name),
163
+ );
108
164
 
109
- if (!hasPlugin) {
110
- await this._execAsync(`${BIN_PATH} plugin remove ${alreadyPlugin.name}`, cordovaPath);
165
+ if (!isDependencyExists) {
166
+ await this._execAsync(`npx cordova plugin remove ${alreadyPlugin.name}`, cordovaPath);
167
+ }
111
168
  }
112
- }*/
169
+ }
170
+ }
113
171
 
114
- // 미설치 플러그인들 설치
172
+ private async _installNewPluginsAsync(
173
+ cordovaPath: string,
174
+ alreadyPlugins: Array<{ name: string; id: string; dependencies?: string[] }>,
175
+ usePlugins: string[],
176
+ ): Promise<void> {
177
+ // 병렬로 플러그인을 설치하면 충돌이 발생할 수 있으므로 순차 처리
115
178
  for (const usePlugin of usePlugins) {
116
- if (!alreadyPlugins.some((item) => usePlugin === item.id || usePlugin === item.name)) {
179
+ const isPluginAlreadyInstalled = alreadyPlugins.some(
180
+ plugin => usePlugin === plugin.id || usePlugin === plugin.name,
181
+ );
182
+
183
+ if (!isPluginAlreadyInstalled) {
117
184
  await this._execAsync(`npx cordova plugin add ${usePlugin}`, cordovaPath);
118
185
  }
119
186
  }
187
+ }
188
+
189
+ // 4. 안드로이드 서명 설정
190
+ private _setupAndroidSign(cordovaPath: string): void {
191
+ const keystorePath = path.resolve(cordovaPath, this.KEYSTORE_FILE_NAME);
192
+ const signingPropsPath = path.resolve(cordovaPath, this.ANDROID_SIGNING_PROP_PATH);
120
193
 
121
- // ANDROID SIGN 파일 복사
122
194
  if (this._opt.config.platform?.android?.sign) {
123
195
  FsUtils.copy(
124
196
  path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore),
125
- path.resolve(cordovaPath, "android.keystore"),
197
+ keystorePath,
126
198
  );
127
199
  }
128
200
  else {
129
- FsUtils.remove(path.resolve(cordovaPath, "android.keystore"));
201
+ FsUtils.remove(keystorePath);
130
202
  // SIGN을 안쓸경우 아래 파일이 생성되어 있으면 오류남
131
- FsUtils.remove(path.resolve(cordovaPath, "platforms/android/release-signing.properties"));
203
+ FsUtils.remove(signingPropsPath);
132
204
  }
205
+ }
133
206
 
134
- // 빌드 옵션 파일 생성
135
- FsUtils.writeJson(path.resolve(cordovaPath, "build.json"), {
136
- ...(this._opt.config.platform?.android
137
- ? {
138
- android: {
139
- release: {
140
- packageType: this._opt.config.platform.android.bundle ? "bundle" : "apk",
141
- ...(this._opt.config.platform.android.sign
142
- ? {
143
- keystore: path.resolve(cordovaPath, "android.keystore"),
144
- storePassword: this._opt.config.platform.android.sign.storePassword,
145
- alias: this._opt.config.platform.android.sign.alias,
146
- password: this._opt.config.platform.android.sign.password,
147
- keystoreType: this._opt.config.platform.android.sign.keystoreType,
148
- }
149
- : {}),
150
- },
207
+ // 5. 빌드 설정 파일 생성
208
+ private _createBuildConfig(cordovaPath: string): void {
209
+ const buildJsonPath = path.resolve(cordovaPath, this.BUILD_JSON_FILE_NAME);
210
+ const keystorePath = path.resolve(cordovaPath, this.KEYSTORE_FILE_NAME);
211
+
212
+ const androidConfig = this._opt.config.platform?.android
213
+ ? {
214
+ android: {
215
+ release: {
216
+ packageType: this._opt.config.platform.android.bundle ? "bundle" : "apk",
217
+ ...(this._opt.config.platform.android.sign
218
+ ? {
219
+ keystore: keystorePath,
220
+ storePassword: this._opt.config.platform.android.sign.storePassword,
221
+ alias: this._opt.config.platform.android.sign.alias,
222
+ password: this._opt.config.platform.android.sign.password,
223
+ keystoreType: this._opt.config.platform.android.sign.keystoreType,
224
+ }
225
+ : {}),
151
226
  },
152
- }
153
- : {}),
154
- });
227
+ },
228
+ }
229
+ : {};
230
+
231
+ FsUtils.writeJson(buildJsonPath, androidConfig);
232
+ }
233
+
234
+ // 6. 아이콘 및 스플래시 스크린 설정
235
+ private _setupIconAndSplashScreen(cordovaPath: string): void {
236
+ const iconDirPath = path.resolve(cordovaPath, this.ICON_DIR_PATH);
237
+ const splashScreenPath = path.resolve(cordovaPath, this.SPLASH_SCREEN_DIR_PATH);
238
+ const splashScreenXmlPath = path.resolve(splashScreenPath, this.SPLASH_SCREEN_XML_FILE);
155
239
 
156
240
  // ICON 파일 복사
157
241
  if (this._opt.config.icon != null) {
242
+ FsUtils.mkdirs(iconDirPath);
158
243
  FsUtils.copy(
159
244
  path.resolve(this._opt.pkgPath, this._opt.config.icon),
160
- path.resolve(cordovaPath, "res/icons", path.basename(this._opt.config.icon)),
245
+ path.resolve(iconDirPath, path.basename(this._opt.config.icon)),
161
246
  );
162
247
  }
163
248
  else {
164
- FsUtils.remove(path.resolve(cordovaPath, "res/icons"));
249
+ FsUtils.remove(iconDirPath);
165
250
  }
166
251
 
167
252
  // SplashScreen 파일 생성
168
253
  if (this._opt.config.platform?.android && this._opt.config.icon != null) {
254
+ FsUtils.mkdirs(splashScreenPath);
169
255
  FsUtils.writeFile(
170
- path.resolve(cordovaPath, "res/screen/android/splashscreen.xml"),
256
+ splashScreenXmlPath,
171
257
  `
172
258
  <?xml version="1.0" encoding="utf-8"?>
173
259
  <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
@@ -179,240 +265,264 @@ export class SdCliCordova {
179
265
  </layer-list>`.trim(),
180
266
  );
181
267
  }
268
+ }
182
269
 
270
+ // 7. XML 설정 구성
271
+ private _configureXml(cordovaPath: string) {
183
272
  // CONFIG: 초기값 백업
184
- const configFilePath = path.resolve(cordovaPath, "config.xml");
185
- const configBackFilePath = path.resolve(cordovaPath, "config.xml.bak");
273
+ const configFilePath = path.resolve(cordovaPath, this.CONFIG_XML_FILE_NAME);
274
+ const configBackFilePath = path.resolve(cordovaPath, this.CONFIG_XML_BACKUP_FILE_NAME);
275
+
186
276
  if (!FsUtils.exists(configBackFilePath)) {
187
277
  FsUtils.copy(configFilePath, configBackFilePath);
188
278
  }
189
279
 
190
280
  // CONFIG: 초기값 읽기
191
281
  const configFileContent = FsUtils.readFile(configBackFilePath);
192
- const configXml = await xml2js.parseStringPromise(configFileContent);
282
+ const configXml = XmlConvert.parse(configFileContent);
283
+
284
+ // CONFIG: 기본 설정
285
+ this._configureBasicXmlSettings(configXml);
286
+
287
+ // CONFIG: 안드로이드 설정
288
+ if (this._opt.config.platform?.android) {
289
+ this._configureAndroidXmlSettings(configXml);
290
+ }
291
+
292
+ // CONFIG: 파일 새로 쓰기
293
+ const configResultContent = XmlConvert.stringify(configXml);
294
+ FsUtils.writeFile(configFilePath, configResultContent);
295
+ }
193
296
 
194
- // CONFIG: 버전 설정
297
+ private _configureBasicXmlSettings(configXml: any): void {
298
+ // 버전 설정
195
299
  configXml.widget.$.version = this._npmConfig.version;
196
300
 
197
- // CONFIG: ICON 설정
301
+ // ICON 설정
198
302
  if (this._opt.config.icon != null) {
199
- configXml["widget"]["icon"] = [
303
+ configXml.widget.icon = [
200
304
  {
201
305
  $: {
202
- src: "res/icons/"
203
- + path.basename(this._opt.config.icon),
306
+ src: `${this.ICON_DIR_PATH}/${path.basename(this._opt.config.icon)}`,
204
307
  },
205
308
  },
206
309
  ];
207
310
  }
208
311
 
209
- // CONFIG: 접근허용 세팅
210
- // configXml["widget"]["content"] = [{ $: { src: "index.html" } }];
211
- configXml["widget"]["content"] = [{ $: { src: "http://localhost/index.html" } }];
212
- configXml["widget"]["access"] = [{ $: { origin: "*" } }];
213
- configXml["widget"]["allow-navigation"] = [{ $: { href: "*" } }];
214
- configXml["widget"]["allow-intent"] = [{ $: { href: "*" } }];
215
- // configXml["widget"]["preference"] = [{ $: { name: "Scheme", value: "http" } }];
216
- configXml["widget"]["preference"] = [{ $: { name: "MixedContentMode", value: "1" } }];
312
+ // 접근허용 세팅
313
+ configXml.widget.content = [{ $: { src: "http://localhost/index.html" } }];
314
+ configXml.widget.access = [{ $: { origin: "*" } }];
315
+ configXml.widget["allow-navigation"] = [{ $: { href: "*" } }];
316
+ configXml.widget["allow-intent"] = [{ $: { href: "*" } }];
317
+ configXml.widget.preference = [{ $: { name: "MixedContentMode", value: "1" } }];
318
+ }
217
319
 
218
- // CONFIG: ANDROID usesCleartextTraffic 설정 및 splashscreen 파일 설정
219
- if (this._opt.config.platform?.android) {
220
- configXml.widget.$["xmlns:android"] = "http://schemas.android.com/apk/res/android";
221
- configXml.widget.$["xmlns:tools"] = "http://schemas.android.com/tools";
320
+ private _configureAndroidXmlSettings(configXml: any): void {
321
+ configXml.widget.$["xmlns:android"] = "http://schemas.android.com/apk/res/android";
322
+ configXml.widget.$["xmlns:tools"] = "http://schemas.android.com/tools";
222
323
 
223
- configXml["widget"]["platform"] = configXml["widget"]["platform"] ?? [];
324
+ configXml.widget.platform = configXml.widget.platform ?? [];
224
325
 
225
- const androidPlatform = {
226
- "$": {
227
- name: "android",
228
- },
229
- "preference": [
230
- {
231
- $: {
232
- name: "AndroidWindowSplashScreenAnimatedIcon",
233
- value: "res/screen/android/splashscreen.xml",
234
- },
235
- },
236
- ],
237
- "edit-config": [
238
- {
239
- $: {
240
- file: "app/src/main/AndroidManifest.xml",
241
- mode: "merge",
242
- target: "/manifest",
243
- },
244
- manifest: [
245
- {
246
- $: {
247
- "xmlns:tools": "http://schemas.android.com/tools",
248
- },
249
- },
250
- ],
326
+ const androidPlatform = {
327
+ "$": {
328
+ name: "android",
329
+ },
330
+ "preference": [
331
+ {
332
+ $: {
333
+ name: "AndroidWindowSplashScreenAnimatedIcon",
334
+ value: `${this.SPLASH_SCREEN_DIR_PATH}/${this.SPLASH_SCREEN_XML_FILE}`,
251
335
  },
252
- {
253
- $: {
254
- file: "app/src/main/AndroidManifest.xml",
255
- mode: "merge",
256
- target: "/manifest/application",
257
- },
258
- application: [
259
- {
260
- $: {
261
- "android:usesCleartextTraffic": "true",
262
- ...this._opt.config.platform.android.config
263
- ? Object.keys(this._opt.config.platform.android.config)
264
- .toObject(
265
- key => "android:" + key,
266
- key => this._opt.config.platform!.android!.config![key],
267
- )
268
- : {},
269
- },
270
- },
271
- ],
336
+ },
337
+ ],
338
+ "edit-config": [
339
+ {
340
+ $: {
341
+ file: "app/src/main/AndroidManifest.xml",
342
+ mode: "merge",
343
+ target: "/manifest",
272
344
  },
273
- ],
274
- };
275
-
276
- if (this._opt.config.platform.android.sdkVersion != null) {
277
- androidPlatform.preference.push(
278
- ...[
345
+ manifest: [
279
346
  {
280
347
  $: {
281
- name: "android-maxSdkVersion",
282
- value: `${this._opt.config.platform.android.sdkVersion}`,
283
- },
284
- },
285
- {
286
- $: {
287
- name: "android-minSdkVersion",
288
- value: `${this._opt.config.platform.android.sdkVersion}`,
289
- },
290
- },
291
- {
292
- $: {
293
- name: "android-targetSdkVersion",
294
- value: `${this._opt.config.platform.android.sdkVersion}`,
348
+ "xmlns:tools": "http://schemas.android.com/tools",
295
349
  },
296
350
  },
351
+ ],
352
+ },
353
+ {
354
+ $: {
355
+ file: "app/src/main/AndroidManifest.xml",
356
+ mode: "merge",
357
+ target: "/manifest/application",
358
+ },
359
+ application: [
297
360
  {
298
361
  $: {
299
- name: "android-compileSdkVersion",
300
- value: `33`,
362
+ "android:usesCleartextTraffic": "true",
363
+ ...this._opt.config.platform!.android!.config
364
+ ? Object.keys(this._opt.config.platform!.android!.config)
365
+ .toObject(
366
+ key => "android:" + key,
367
+ key => this._opt.config.platform!.android!.config![key],
368
+ )
369
+ : {},
301
370
  },
302
371
  },
303
372
  ],
304
- );
305
- }
373
+ },
374
+ ],
375
+ };
306
376
 
307
- if (this._opt.config.platform.android.permissions) {
308
- androidPlatform["config-file"] = androidPlatform["config-file"] ?? [];
309
- androidPlatform["config-file"].push({
310
- "$": {
311
- target: "AndroidManifest.xml",
312
- parent: "/*",
377
+ // SDK 버전 설정
378
+ if (this._opt.config.platform!.android!.sdkVersion != null) {
379
+ androidPlatform.preference.push(
380
+ ...[
381
+ {
382
+ $: {
383
+ name: "android-maxSdkVersion",
384
+ value: `${this._opt.config.platform!.android!.sdkVersion}`,
385
+ },
313
386
  },
314
- "uses-permission": this._opt.config.platform.android.permissions.map((perm) => ({
387
+ {
315
388
  $: {
316
- "android:name": `android.permission.${perm.name}`,
317
- ...(perm.maxSdkVersion != null
318
- ? {
319
- "android:maxSdkVersion": `${perm.maxSdkVersion}`,
320
- }
321
- : {}),
322
- ...(perm.ignore != null
323
- ? {
324
- "tools:ignore": `${perm.ignore}`,
325
- }
326
- : {}),
389
+ name: "android-minSdkVersion",
390
+ value: `${this._opt.config.platform!.android!.sdkVersion}`,
327
391
  },
328
- })),
329
- });
330
- }
331
-
332
- configXml["widget"]["platform"].push(androidPlatform);
392
+ },
393
+ {
394
+ $: {
395
+ name: "android-targetSdkVersion",
396
+ value: `${this._opt.config.platform!.android!.sdkVersion}`,
397
+ },
398
+ },
399
+ {
400
+ $: {
401
+ name: "android-compileSdkVersion",
402
+ value: this.ANDROID_SDK_VERSION,
403
+ },
404
+ },
405
+ ],
406
+ );
333
407
  }
334
408
 
335
- // CONFIG: 파일 새로 쓰기
336
- const configResultContent = new xml2js.Builder().buildObject(configXml);
337
- FsUtils.writeFile(configFilePath, configResultContent);
338
-
339
- //android.json의 undefined 문제 해결
340
- /*const androidJsonFilePath = path.resolve(cordovaPath, "platforms/android/android.json");
341
- if (FsUtil.exists(androidJsonFilePath)) {
342
- const androidConf = FsUtil.readJson(androidJsonFilePath);
343
- if (androidConf.config_munge.files["undefined"] != null) {
344
- delete androidConf.config_munge.files["undefined"];
345
- }
346
- FsUtil.writeJson(androidJsonFilePath, androidConf, { space: 2 });
347
- }*/
409
+ // 권한 설정
410
+ if (this._opt.config.platform!.android!.permissions) {
411
+ androidPlatform["config-file"] = androidPlatform["config-file"] ?? [];
412
+ androidPlatform["config-file"].push({
413
+ "$": {
414
+ target: "AndroidManifest.xml",
415
+ parent: "/*",
416
+ },
417
+ "uses-permission": this._opt.config.platform!.android!.permissions.map((perm) => ({
418
+ $: {
419
+ "android:name": `android.permission.${perm.name}`,
420
+ ...(perm.maxSdkVersion != null
421
+ ? {
422
+ "android:maxSdkVersion": `${perm.maxSdkVersion}`,
423
+ }
424
+ : {}),
425
+ ...(perm.ignore != null
426
+ ? {
427
+ "tools:ignore": `${perm.ignore}`,
428
+ }
429
+ : {}),
430
+ },
431
+ })),
432
+ });
433
+ }
348
434
 
349
- // 각 플랫폼 www 준비
350
- await this._execAsync(`npx cordova prepare`, cordovaPath);
435
+ configXml.widget.platform.push(androidPlatform);
351
436
  }
352
437
 
353
438
  public async buildAsync(outPath: string): Promise<void> {
354
- const cordovaPath = path.resolve(this._opt.pkgPath, ".cordova");
439
+ const cordovaPath = path.resolve(this._opt.pkgPath, this.CORDOVA_DIR_NAME);
355
440
 
356
- // 실행
441
+ // 빌드 실행 - 병렬 처리로 개선
357
442
  const buildType = this._opt.config.debug ? "debug" : "release";
358
- for (const platform of this._platforms) {
359
- await this._execAsync(`npx cordova build ${platform} --${buildType}`, cordovaPath);
443
+
444
+ // 모든 플랫폼 동시에 빌드
445
+ await Promise.all(this._platforms.map(platform =>
446
+ this._execAsync(`npx cordova build ${platform} --${buildType}`, cordovaPath),
447
+ ));
448
+
449
+ // 결과물 복사 및 ZIP 파일 생성 - 병렬 처리
450
+ await Promise.all(Object.keys(this._opt.config.platform ?? {}).map(async platform => {
451
+ await this._processBuildOutputAsync(cordovaPath, outPath, platform, buildType);
452
+ }));
453
+ }
454
+
455
+ private async _processBuildOutputAsync(
456
+ cordovaPath: string,
457
+ outPath: string,
458
+ platform: string,
459
+ buildType: string,
460
+ ): Promise<void> {
461
+ const targetOutPath = path.resolve(outPath, platform);
462
+
463
+ // 결과물 복사: ANDROID
464
+ if (platform === "android") {
465
+ this._copyAndroidBuildOutput(cordovaPath, targetOutPath, buildType);
360
466
  }
361
467
 
362
- for (const platform of Object.keys(this._opt.config.platform ?? {})) {
363
- const targetOutPath = path.resolve(outPath, platform);
364
-
365
- // 결과물 복사: ANDROID
366
- if (platform === "android") {
367
- const apkFileName = this._opt.config.platform!.android!.sign
368
- ? `app-${buildType}.apk`
369
- : `app-${buildType}-unsigned.apk`;
370
- const latestDistApkFileName = path.basename(
371
- `${this._opt.config.appName}${this._opt.config.platform!.android!.sign
372
- ? ""
373
- : "-unsigned"}-latest.apk`,
374
- );
375
- FsUtils.mkdirs(targetOutPath);
376
- FsUtils.copy(
377
- path.resolve(
378
- cordovaPath,
379
- "platforms/android/app/build/outputs/apk",
380
- buildType,
381
- apkFileName,
382
- ),
383
- path.resolve(targetOutPath, latestDistApkFileName),
384
- );
385
- }
468
+ // 자동업데이트를 위한 파일 생성
469
+ await this._createUpdateZipAsync(cordovaPath, outPath, platform);
470
+ }
386
471
 
387
- // 자동업데이트를 위한 파일 쓰기 (ZIP)
388
- const zip = new JSZip();
389
- const wwwFiles = FsUtils.glob(path.resolve(cordovaPath, "www/**/*"), { nodir: true });
390
- for (const wwwFile of wwwFiles) {
391
- const relFilePath = path.relative(path.resolve(cordovaPath, "www"), wwwFile);
392
- const fileBuffer = FsUtils.readFileBuffer(wwwFile);
393
- zip.file(relFilePath, fileBuffer);
394
- }
395
- const platformWwwFiles = FsUtils.glob(path.resolve(
396
- cordovaPath,
397
- "platforms",
398
- platform,
399
- "platform_www/**/*",
400
- ), {
401
- nodir: true,
402
- });
403
- for (const platformWwwFile of platformWwwFiles) {
404
- const relFilePath = path.relative(
405
- path.resolve(cordovaPath, "platforms", platform, "platform_www"),
406
- platformWwwFile,
407
- );
408
- const fileBuffer = FsUtils.readFileBuffer(platformWwwFile);
409
- zip.file(relFilePath, fileBuffer);
410
- }
472
+ private _copyAndroidBuildOutput(
473
+ cordovaPath: string,
474
+ targetOutPath: string,
475
+ buildType: string,
476
+ ) {
477
+ const apkFileName = this._opt.config.platform!.android!.sign
478
+ ? `app-${buildType}.apk`
479
+ : `app-${buildType}-unsigned.apk`;
480
+
481
+ const latestDistApkFileName = path.basename(
482
+ `${this._opt.config.appName}${this._opt.config.platform!.android!.sign
483
+ ? ""
484
+ : "-unsigned"}-latest.apk`,
485
+ );
486
+
487
+ FsUtils.mkdirs(targetOutPath);
488
+ FsUtils.copy(
489
+ path.resolve(cordovaPath, "platforms/android/app/build/outputs/apk", buildType, apkFileName),
490
+ path.resolve(targetOutPath, latestDistApkFileName),
491
+ );
492
+ }
411
493
 
412
- FsUtils.writeFile(
413
- path.resolve(path.resolve(outPath, platform, "updates"), this._npmConfig.version + ".zip"),
414
- await zip.generateAsync({ type: "nodebuffer" }),
415
- );
494
+ private async _createUpdateZipAsync(
495
+ cordovaPath: string,
496
+ outPath: string,
497
+ platform: string,
498
+ ): Promise<void> {
499
+ const zip = new SdZip();
500
+ const wwwPath = path.resolve(cordovaPath, this.WWW_DIR_NAME);
501
+ const platformWwwPath = path.resolve(
502
+ cordovaPath,
503
+ this.PLATFORMS_DIR_NAME,
504
+ platform,
505
+ "platform_www",
506
+ );
507
+
508
+ this._addFilesToZip(zip, wwwPath);
509
+ this._addFilesToZip(zip, platformWwwPath);
510
+
511
+ // ZIP 파일 생성
512
+ const updateDirPath = path.resolve(outPath, platform, "updates");
513
+ FsUtils.mkdirs(updateDirPath);
514
+ FsUtils.writeFile(
515
+ path.resolve(updateDirPath, this._npmConfig.version + ".zip"),
516
+ await zip.compressAsync(),
517
+ );
518
+ }
519
+
520
+ private _addFilesToZip(zip: SdZip, dirPath: string) {
521
+ const files = FsUtils.glob(path.resolve(dirPath, "**/*"), { nodir: true });
522
+ for (const file of files) {
523
+ const relFilePath = path.relative(dirPath, file);
524
+ const fileBuffer = FsUtils.readFileBuffer(file);
525
+ zip.write(relFilePath, new Uint8Array(fileBuffer));
416
526
  }
417
527
  }
418
528
 
@@ -428,13 +538,16 @@ export class SdCliCordova {
428
538
  FsUtils.mkdirs(path.resolve(cordovaPath, "www"));
429
539
  FsUtils.writeFile(
430
540
  path.resolve(cordovaPath, "www/index.html"),
431
- `'${opt.url}'로 이동중... <script>setTimeout(function () {window.location.href = "${opt.url.replace(
432
- /\/$/,
433
- "",
434
- )}/${opt.pkgName}/cordova/"}, 3000);</script>`.trim(),
541
+ `
542
+ '${opt.url}'로 이동중...
543
+ <script>
544
+ setTimeout(function () {
545
+ window.location.href = "${opt.url.replace(/\/$/, "")}/${opt.pkgName}/cordova/";
546
+ }, 3000);
547
+ </script>`.trim(),
435
548
  );
436
549
  }
437
550
 
438
551
  await SdProcess.spawnAsync(`npx cordova run ${opt.platform} --device`, { cwd: cordovaPath });
439
552
  }
440
- }
553
+ }