@simplysm/sd-cli 12.8.20 → 12.8.22

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 (40) hide show
  1. package/dist/entry/sd-cli-cordova.d.ts +30 -0
  2. package/dist/entry/sd-cli-cordova.js +307 -246
  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/pkg-builders/server/sd-server.bundler.js +11 -10
  18. package/dist/pkg-builders/server/sd-server.bundler.js.map +1 -1
  19. package/dist/ts-compiler/sd-ts-compiler.d.ts +25 -2
  20. package/dist/ts-compiler/sd-ts-compiler.js +306 -575
  21. package/dist/ts-compiler/sd-ts-compiler.js.map +1 -1
  22. package/dist/ts-compiler/sd-ts-dependency-analyzer.d.ts +6 -0
  23. package/dist/ts-compiler/sd-ts-dependency-analyzer.js +141 -0
  24. package/dist/ts-compiler/sd-ts-dependency-analyzer.js.map +1 -0
  25. package/dist/types/ts-compiler.types.d.ts +12 -7
  26. package/dist/types/worker.types.d.ts +13 -0
  27. package/dist/utils/sd-cli-performance-time.js +1 -1
  28. package/package.json +6 -8
  29. package/src/entry/sd-cli-cordova.ts +393 -280
  30. package/src/entry/sd-cli-project.ts +11 -23
  31. package/src/index.ts +1 -0
  32. package/src/pkg-builders/client/sd-ng.bundler.ts +67 -69
  33. package/src/pkg-builders/client/sd-ng.plugin-creator.ts +47 -27
  34. package/src/pkg-builders/lib/sd-ts-lib.builder.ts +14 -7
  35. package/src/pkg-builders/server/sd-server.bundler.ts +22 -12
  36. package/src/ts-compiler/sd-ts-compiler.ts +379 -704
  37. package/src/ts-compiler/sd-ts-dependency-analyzer.ts +185 -0
  38. package/src/types/ts-compiler.types.ts +11 -6
  39. package/src/types/worker.types.ts +7 -6
  40. 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,171 @@ 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
+ }
98
+
99
+ // 2. 플랫폼 관리
100
+ private async _managePlatformsAsync(cordovaPath: string): Promise<void> {
101
+ const alreadyPlatforms = FsUtils.readdir(path.resolve(cordovaPath, this.PLATFORMS_DIR_NAME));
59
102
 
60
103
  // 미설치 빌드 플랫폼 신규 생성
61
- const alreadyPlatforms = FsUtils.readdir(path.resolve(cordovaPath, "platforms"));
62
104
  for (const platform of this._platforms) {
63
- if (!alreadyPlatforms.includes(platform)) {
64
- // await this._execAsync(`${BIN_PATH} platform add ${platform}`, cordovaPath);
65
- if (platform === "android") {
66
- await this._execAsync(`npx cordova platform add ${platform}@12.0.0`, cordovaPath);
67
- }
68
- else {
69
- await this._execAsync(`npx cordova platform add ${platform}`, cordovaPath);
70
- }
105
+ if (alreadyPlatforms.includes(platform)) continue;
106
+
107
+ if (platform === "android") {
108
+ await this._execAsync(
109
+ `npx cordova platform add ${platform}@${this.ANDROID_PLATFORM_VERSION}`,
110
+ cordovaPath,
111
+ );
112
+ }
113
+ else {
114
+ await this._execAsync(`npx cordova platform add ${platform}`, cordovaPath);
71
115
  }
72
116
  }
117
+ }
73
118
 
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
- }*/
119
+ // 3. 플러그인 관리
120
+ private async _managePluginsAsync(cordovaPath: string): Promise<void> {
121
+ const pluginsFetchPath = path.resolve(
122
+ cordovaPath,
123
+ this.PLUGINS_DIR_NAME,
124
+ this.PLUGINS_FETCH_FILE,
125
+ );
126
+ const pluginsFetch = FsUtils.exists(pluginsFetchPath)
127
+ ? FsUtils.readJson(pluginsFetchPath)
128
+ : {};
129
+
130
+ const alreadyPlugins: Array<{
131
+ name: string;
132
+ id: string;
133
+ dependencies?: string[]
134
+ }> = Object.keys(pluginsFetch).map(key => ({
135
+ name: key,
136
+ id: pluginsFetch[key].source.id,
137
+ dependencies: pluginsFetch[key].dependencies,
138
+ }));
80
139
 
81
- // 설치된 미사용 플러그인 삭제
82
- const pluginsFetch = FsUtils.exists(path.resolve(cordovaPath, "plugins/fetch.json"))
83
- ? FsUtils.readJson(path.resolve(cordovaPath, "plugins/fetch.json"))
84
- : undefined;
140
+ const usePlugins = (this._opt.config.plugins ?? []).distinct();
85
141
 
86
- const alreadyPlugins: { name: string; id: string }[] = [];
142
+ // 사용하지 않는 플러그인 제거 플러그인 설치 - 의존성 때문에 순차 처리
143
+ await this._removeUnusedPluginsAsync(cordovaPath, alreadyPlugins, usePlugins);
144
+ await this._installNewPluginsAsync(cordovaPath, alreadyPlugins, usePlugins);
145
+ }
87
146
 
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
- }
147
+ private async _removeUnusedPluginsAsync(
148
+ cordovaPath: string,
149
+ alreadyPlugins: Array<{ name: string; id: string; dependencies?: string[] }>,
150
+ usePlugins: string[],
151
+ ): Promise<void> {
152
+ for (const alreadyPlugin of alreadyPlugins) {
153
+ // 사용하지 않는 플러그인 제거 시 의존성 검사
154
+ const isPluginUsed = usePlugins.some(
155
+ usePlugin => usePlugin === alreadyPlugin.id || usePlugin === alreadyPlugin.name,
156
+ );
96
157
 
97
- const usePlugins = (this._opt.config.plugins ?? []).distinct();
158
+ if (!isPluginUsed) {
159
+ const isDependencyExists = alreadyPlugins.some(
160
+ plugin => plugin.dependencies?.includes(alreadyPlugin.name),
161
+ );
98
162
 
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;
163
+ if (!isDependencyExists) {
164
+ await this._execAsync(`npx cordova plugin remove ${alreadyPlugin.name}`, cordovaPath);
106
165
  }
107
166
  }
167
+ }
168
+ }
108
169
 
109
- if (!hasPlugin) {
110
- await this._execAsync(`${BIN_PATH} plugin remove ${alreadyPlugin.name}`, cordovaPath);
111
- }
112
- }*/
113
-
114
- // 미설치 플러그인들 설치
170
+ private async _installNewPluginsAsync(
171
+ cordovaPath: string,
172
+ alreadyPlugins: Array<{ name: string; id: string; dependencies?: string[] }>,
173
+ usePlugins: string[],
174
+ ): Promise<void> {
175
+ // 병렬로 플러그인을 설치하면 충돌이 발생할 수 있으므로 순차 처리
115
176
  for (const usePlugin of usePlugins) {
116
- if (!alreadyPlugins.some((item) => usePlugin === item.id || usePlugin === item.name)) {
177
+ const isPluginAlreadyInstalled = alreadyPlugins.some(
178
+ plugin => usePlugin === plugin.id || usePlugin === plugin.name,
179
+ );
180
+
181
+ if (!isPluginAlreadyInstalled) {
117
182
  await this._execAsync(`npx cordova plugin add ${usePlugin}`, cordovaPath);
118
183
  }
119
184
  }
185
+ }
186
+
187
+ // 4. 안드로이드 서명 설정
188
+ private _setupAndroidSign(cordovaPath: string): void {
189
+ const keystorePath = path.resolve(cordovaPath, this.KEYSTORE_FILE_NAME);
190
+ const signingPropsPath = path.resolve(cordovaPath, this.ANDROID_SIGNING_PROP_PATH);
120
191
 
121
- // ANDROID SIGN 파일 복사
122
192
  if (this._opt.config.platform?.android?.sign) {
123
193
  FsUtils.copy(
124
194
  path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore),
125
- path.resolve(cordovaPath, "android.keystore"),
195
+ keystorePath,
126
196
  );
127
197
  }
128
198
  else {
129
- FsUtils.remove(path.resolve(cordovaPath, "android.keystore"));
199
+ FsUtils.remove(keystorePath);
130
200
  // SIGN을 안쓸경우 아래 파일이 생성되어 있으면 오류남
131
- FsUtils.remove(path.resolve(cordovaPath, "platforms/android/release-signing.properties"));
201
+ FsUtils.remove(signingPropsPath);
132
202
  }
203
+ }
133
204
 
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
- },
205
+ // 5. 빌드 설정 파일 생성
206
+ private _createBuildConfig(cordovaPath: string): void {
207
+ const buildJsonPath = path.resolve(cordovaPath, this.BUILD_JSON_FILE_NAME);
208
+ const keystorePath = path.resolve(cordovaPath, this.KEYSTORE_FILE_NAME);
209
+
210
+ const androidConfig = this._opt.config.platform?.android
211
+ ? {
212
+ android: {
213
+ release: {
214
+ packageType: this._opt.config.platform.android.bundle ? "bundle" : "apk",
215
+ ...(this._opt.config.platform.android.sign
216
+ ? {
217
+ keystore: keystorePath,
218
+ storePassword: this._opt.config.platform.android.sign.storePassword,
219
+ alias: this._opt.config.platform.android.sign.alias,
220
+ password: this._opt.config.platform.android.sign.password,
221
+ keystoreType: this._opt.config.platform.android.sign.keystoreType,
222
+ }
223
+ : {}),
151
224
  },
152
- }
153
- : {}),
154
- });
225
+ },
226
+ }
227
+ : {};
228
+
229
+ FsUtils.writeJson(buildJsonPath, androidConfig);
230
+ }
231
+
232
+ // 6. 아이콘 및 스플래시 스크린 설정
233
+ private _setupIconAndSplashScreen(cordovaPath: string): void {
234
+ const iconDirPath = path.resolve(cordovaPath, this.ICON_DIR_PATH);
235
+ const splashScreenPath = path.resolve(cordovaPath, this.SPLASH_SCREEN_DIR_PATH);
236
+ const splashScreenXmlPath = path.resolve(splashScreenPath, this.SPLASH_SCREEN_XML_FILE);
155
237
 
156
238
  // ICON 파일 복사
157
239
  if (this._opt.config.icon != null) {
240
+ FsUtils.mkdirs(iconDirPath);
158
241
  FsUtils.copy(
159
242
  path.resolve(this._opt.pkgPath, this._opt.config.icon),
160
- path.resolve(cordovaPath, "res/icons", path.basename(this._opt.config.icon)),
243
+ path.resolve(iconDirPath, path.basename(this._opt.config.icon)),
161
244
  );
162
245
  }
163
246
  else {
164
- FsUtils.remove(path.resolve(cordovaPath, "res/icons"));
247
+ FsUtils.remove(iconDirPath);
165
248
  }
166
249
 
167
250
  // SplashScreen 파일 생성
168
251
  if (this._opt.config.platform?.android && this._opt.config.icon != null) {
252
+ FsUtils.mkdirs(splashScreenPath);
169
253
  FsUtils.writeFile(
170
- path.resolve(cordovaPath, "res/screen/android/splashscreen.xml"),
254
+ splashScreenXmlPath,
171
255
  `
172
256
  <?xml version="1.0" encoding="utf-8"?>
173
257
  <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
@@ -179,240 +263,266 @@ export class SdCliCordova {
179
263
  </layer-list>`.trim(),
180
264
  );
181
265
  }
266
+ }
182
267
 
268
+ // 7. XML 설정 구성
269
+ private _configureXml(cordovaPath: string) {
183
270
  // CONFIG: 초기값 백업
184
- const configFilePath = path.resolve(cordovaPath, "config.xml");
185
- const configBackFilePath = path.resolve(cordovaPath, "config.xml.bak");
271
+ const configFilePath = path.resolve(cordovaPath, this.CONFIG_XML_FILE_NAME);
272
+ const configBackFilePath = path.resolve(cordovaPath, this.CONFIG_XML_BACKUP_FILE_NAME);
273
+
186
274
  if (!FsUtils.exists(configBackFilePath)) {
187
275
  FsUtils.copy(configFilePath, configBackFilePath);
188
276
  }
189
277
 
190
278
  // CONFIG: 초기값 읽기
191
279
  const configFileContent = FsUtils.readFile(configBackFilePath);
192
- const configXml = await xml2js.parseStringPromise(configFileContent);
280
+ const configXml = XmlConvert.parse(configFileContent);
281
+
282
+ // CONFIG: 기본 설정
283
+ this._configureBasicXmlSettings(configXml);
284
+
285
+ // CONFIG: 안드로이드 설정
286
+ if (this._opt.config.platform?.android) {
287
+ this._configureAndroidXmlSettings(configXml);
288
+ }
193
289
 
194
- // CONFIG: 버전 설정
290
+ // CONFIG: 파일 새로 쓰기
291
+ const configResultContent = XmlConvert.stringify(configXml, {
292
+ format: true
293
+ });
294
+ FsUtils.writeFile(configFilePath, configResultContent);
295
+ }
296
+
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
- ...[
279
- {
280
- $: {
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
- },
345
+ manifest: [
291
346
  {
292
347
  $: {
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);
468
+ // 자동업데이트를 위한 파일 생성
469
+ await this._createUpdateZipAsync(cordovaPath, outPath, platform);
470
+ }
364
471
 
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
- }
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
+ }
386
493
 
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
- }
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
+ }
411
519
 
412
- FsUtils.writeFile(
413
- path.resolve(path.resolve(outPath, platform, "updates"), this._npmConfig.version + ".zip"),
414
- await zip.generateAsync({ type: "nodebuffer" }),
415
- );
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
+ }