@simplysm/sd-cli 12.16.7 → 12.16.9

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.
@@ -19,9 +19,11 @@ export declare class SdCliCapacitor {
19
19
  private _createCapacitorConfigAsync;
20
20
  private _managePlatformsAsync;
21
21
  private _managePluginsAsync;
22
+ private _isCapacitorPlugin;
22
23
  private _setupAndroidSignAsync;
23
24
  private _setupIconAndSplashScreenAsync;
24
- private _createPaddedIconAsync;
25
+ private _createCenteredImageAsync;
26
+ private _cleanupExistingIconsAsync;
25
27
  private _configureAndroidNativeAsync;
26
28
  private _configureAndroidStylesAsync;
27
29
  private _configureAndroidGradlePropertiesAsync;
@@ -22,13 +22,13 @@ export class SdCliCapacitor {
22
22
  async initializeAsync() {
23
23
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
24
24
  // 1. Capacitor 프로젝트 초기화
25
- await this._initializeCapacitorProjectAsync(capacitorPath);
25
+ const isNewProject = await this._initializeCapacitorProjectAsync(capacitorPath);
26
26
  // 2. Capacitor 설정 파일 생성
27
27
  await this._createCapacitorConfigAsync(capacitorPath);
28
28
  // 3. 플랫폼 관리
29
29
  await this._managePlatformsAsync(capacitorPath);
30
30
  // 4. 플러그인 관리
31
- await this._managePluginsAsync(capacitorPath);
31
+ const pluginsChanged = await this._managePluginsAsync(capacitorPath);
32
32
  // 5. 안드로이드 서명 설정
33
33
  await this._setupAndroidSignAsync(capacitorPath);
34
34
  // 6. 아이콘 및 스플래시 스크린 설정
@@ -38,7 +38,12 @@ export class SdCliCapacitor {
38
38
  await this._configureAndroidNativeAsync(capacitorPath);
39
39
  }
40
40
  // 8. 웹 자산 동기화
41
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
41
+ if (isNewProject || pluginsChanged) {
42
+ await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
43
+ }
44
+ else {
45
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy"], capacitorPath);
46
+ }
42
47
  }
43
48
  // 1. Capacitor 프로젝트 초기화
44
49
  async _initializeCapacitorProjectAsync(capacitorPath) {
@@ -46,40 +51,41 @@ export class SdCliCapacitor {
46
51
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
47
52
  // 버전 동기화
48
53
  await this._syncVersionAsync(capacitorPath);
49
- }
50
- else {
51
- await FsUtils.mkdirsAsync(capacitorPath);
52
- // package.json 생성
53
- const projNpmConfig = await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "../../package.json"));
54
- const pkgJson = {
55
- name: this._opt.config.appId,
56
- version: this._npmConfig.version,
57
- private: true,
58
- volta: projNpmConfig.volta,
59
- dependencies: {
60
- "@capacitor/core": "^7.0.0",
61
- },
62
- devDependencies: {
63
- "@capacitor/cli": "^7.0.0",
64
- "@capacitor/assets": "^3.0.0",
65
- ...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
66
- },
67
- };
68
- await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
69
- space: 2,
70
- });
71
- // .yarnrc.yml 작성
72
- await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
73
- // yarn.lock 작성
74
- await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
75
- // yarn install
76
- await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
77
- // capacitor init
78
- await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
79
- }
54
+ return false;
55
+ }
56
+ await FsUtils.mkdirsAsync(capacitorPath);
57
+ // package.json 생성
58
+ const projNpmConfig = await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "../../package.json"));
59
+ const pkgJson = {
60
+ name: this._opt.config.appId,
61
+ version: this._npmConfig.version,
62
+ private: true,
63
+ volta: projNpmConfig.volta,
64
+ dependencies: {
65
+ "@capacitor/core": "^7.0.0",
66
+ "@capacitor/app": "^7.0.0",
67
+ },
68
+ devDependencies: {
69
+ "@capacitor/cli": "^7.0.0",
70
+ "@capacitor/assets": "^3.0.0",
71
+ ...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
72
+ },
73
+ };
74
+ await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
75
+ space: 2,
76
+ });
77
+ // .yarnrc.yml 작성
78
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
79
+ // yarn.lock 작성
80
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
81
+ // yarn install
82
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
83
+ // capacitor init
84
+ await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
80
85
  // www/index.html 생성
81
86
  const wwwPath = path.resolve(capacitorPath, "www");
82
87
  await FsUtils.writeFileAsync(path.resolve(wwwPath, "index.html"), "<!DOCTYPE html><html><head></head><body></body></html>");
88
+ return true;
83
89
  }
84
90
  // 버전 동기화
85
91
  async _syncVersionAsync(capacitorPath) {
@@ -138,43 +144,55 @@ export class SdCliCapacitor {
138
144
  await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], capacitorPath);
139
145
  }
140
146
  }
141
- // 4. 플러그인 관리
142
147
  async _managePluginsAsync(capacitorPath) {
143
148
  const pkgJsonPath = path.resolve(capacitorPath, "package.json");
144
149
  const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
145
- const currentDeps = Object.keys(pkgJson.dependencies ?? {});
146
- const usePlugins = Object.keys(this._opt.config.plugins ?? {});
147
- // 사용하지 않는 플러그인 제거
148
- for (const dep of currentDeps) {
149
- // @capacitor/core, @capacitor/android 등 기본 패키지는 제외
150
- if (dep.startsWith("@capacitor/") &&
151
- ["core", "android", "ios"].some((p) => dep.endsWith(p))) {
152
- continue;
153
- }
154
- // 플러그인 목록에 없는 패키지는 제거
155
- if (!usePlugins.includes(dep)) {
156
- // Capacitor 관련 플러그인만 제거
157
- if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
158
- await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
159
- SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
160
- }
161
- }
162
- }
163
- // 새 플러그인 설치
164
150
  const mainDeps = {
165
151
  ...this._npmConfig.dependencies,
166
152
  ...this._npmConfig.devDependencies,
167
153
  ...this._npmConfig.peerDependencies,
168
154
  };
155
+ const usePlugins = Object.keys(this._opt.config.plugins ?? {});
156
+ const currentDeps = pkgJson.dependencies ?? {};
157
+ let changed = false;
158
+ // 사용하지 않는 플러그인 제거
159
+ for (const dep of Object.keys(currentDeps)) {
160
+ if (this._isCapacitorPlugin(dep) && !usePlugins.includes(dep)) {
161
+ delete currentDeps[dep];
162
+ changed = true;
163
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
164
+ }
165
+ }
166
+ // 새 플러그인 추가
169
167
  for (const plugin of usePlugins) {
170
- if (!currentDeps.includes(plugin)) {
171
- // 메인 프로젝트에 버전이 있으면 그 버전으로 설치
172
- const version = mainDeps[plugin];
173
- const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
174
- await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
175
- SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
168
+ if (!(plugin in currentDeps)) {
169
+ const version = mainDeps[plugin] ?? "^7.0.0";
170
+ currentDeps[plugin] = version;
171
+ changed = true;
172
+ SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
176
173
  }
177
174
  }
175
+ // 변경사항 있을 때만 저장 & install
176
+ if (changed) {
177
+ pkgJson.dependencies = currentDeps;
178
+ await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
179
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
180
+ return true;
181
+ }
182
+ // 변경 없으면 아무것도 안 함 → 오프라인 OK
183
+ return false;
184
+ }
185
+ _isCapacitorPlugin(dep) {
186
+ // 기본 패키지 제외
187
+ const corePackages = [
188
+ "@capacitor/core",
189
+ "@capacitor/android",
190
+ "@capacitor/ios",
191
+ "@capacitor/app",
192
+ ];
193
+ if (corePackages.includes(dep))
194
+ return false;
195
+ return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
178
196
  }
179
197
  // 5. 안드로이드 서명 설정
180
198
  async _setupAndroidSignAsync(capacitorPath) {
@@ -188,33 +206,45 @@ export class SdCliCapacitor {
188
206
  }
189
207
  // 6. 아이콘 및 스플래시 스크린 설정
190
208
  async _setupIconAndSplashScreenAsync(capacitorPath) {
191
- const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
209
+ const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
192
210
  if (this._opt.config.icon != null) {
193
- await FsUtils.mkdirsAsync(iconDirPath);
211
+ await FsUtils.mkdirsAsync(resourcesDirPath);
194
212
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
195
- // icon.png 자체를 여백 포함으로 생성
196
- const iconPath = path.resolve(iconDirPath, "icon.png");
197
- await this._createPaddedIconAsync(iconSource, iconPath);
198
- // splash는 원본 사용
199
- await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
213
+ // logo.png: 1024x1024 (Easy Mode)
214
+ // 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
215
+ const logoPath = path.resolve(resourcesDirPath, "logo.png");
216
+ await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
217
+ // splash.png: 2732x2732
218
+ // 로고 크기 약 35% - 화면 중앙에 적당한 크기로
219
+ const splashPath = path.resolve(resourcesDirPath, "splash.png");
220
+ await this._createCenteredImageAsync(iconSource, splashPath, 2732, 0.35);
221
+ // 기존 아이콘 삭제 (겹침 방지)
222
+ await this._cleanupExistingIconsAsync(capacitorPath);
200
223
  try {
201
- await SdCliCapacitor._execAsync("npx", ["@capacitor/assets", "generate", "--android"], capacitorPath);
224
+ await SdCliCapacitor._execAsync("npx", [
225
+ "@capacitor/assets",
226
+ "generate",
227
+ "--android",
228
+ "--iconBackgroundColor",
229
+ "#ffffff",
230
+ "--splashBackgroundColor",
231
+ "#ffffff",
232
+ ], capacitorPath);
202
233
  }
203
234
  catch {
204
- SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
235
+ SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
205
236
  }
206
237
  }
207
238
  else {
208
- await FsUtils.removeAsync(iconDirPath);
239
+ await FsUtils.removeAsync(resourcesDirPath);
209
240
  }
210
241
  }
211
- async _createPaddedIconAsync(sourcePath, outputPath) {
212
- const outputSize = 1024;
213
- const iconSize = 680; // safe zone (66%)
214
- const padding = Math.floor((outputSize - iconSize) / 2); // 172px
215
- // 원본 크기 상관없이 iconSize로 리사이즈 후 여백 추가
242
+ // 중앙에 로고를 배치한 이미지 생성
243
+ async _createCenteredImageAsync(sourcePath, outputPath, outputSize, logoRatio) {
244
+ const logoSize = Math.floor(outputSize * logoRatio);
245
+ const padding = Math.floor((outputSize - logoSize) / 2);
216
246
  await sharp(sourcePath)
217
- .resize(iconSize, iconSize, {
247
+ .resize(logoSize, logoSize, {
218
248
  fit: "contain",
219
249
  background: { r: 0, g: 0, b: 0, alpha: 0 },
220
250
  })
@@ -227,6 +257,19 @@ export class SdCliCapacitor {
227
257
  })
228
258
  .toFile(outputPath);
229
259
  }
260
+ // 기존 아이콘 파일 삭제
261
+ async _cleanupExistingIconsAsync(capacitorPath) {
262
+ const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
263
+ if (!FsUtils.exists(androidResPath))
264
+ return;
265
+ const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
266
+ for (const dir of mipmapDirs) {
267
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*.png"));
268
+ for (const file of iconFiles) {
269
+ await FsUtils.removeAsync(file);
270
+ }
271
+ }
272
+ }
230
273
  // 7. Android 네이티브 설정
231
274
  async _configureAndroidNativeAsync(capacitorPath) {
232
275
  const androidPath = path.resolve(capacitorPath, "android");
@@ -413,10 +456,12 @@ export class SdCliCapacitor {
413
456
  async buildAsync(outPath) {
414
457
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
415
458
  const buildType = this._opt.config.debug ? "debug" : "release";
416
- // 웹 자산 동기화
417
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
418
459
  // 플랫폼별 빌드
419
- await Promise.all(this._platforms.map((platform) => this._buildPlatformAsync(capacitorPath, outPath, platform, buildType)));
460
+ await Promise.all(this._platforms.map(async (platform) => {
461
+ // 해당 플랫폼만 copy
462
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
463
+ await this._buildPlatformAsync(capacitorPath, outPath, platform, buildType);
464
+ }));
420
465
  }
421
466
  async _buildPlatformAsync(capacitorPath, outPath, platform, buildType) {
422
467
  if (platform === "android") {
@@ -508,12 +553,13 @@ export class SdCliCapacitor {
508
553
  }
509
554
  }
510
555
  // cap sync 후 run
511
- await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
556
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", opt.platform], capacitorPath);
512
557
  try {
513
558
  await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
514
559
  }
515
- catch {
560
+ catch (err) {
516
561
  await SdProcess.spawnAsync("adb", ["kill-server"]);
562
+ throw err;
517
563
  }
518
564
  }
519
565
  }
@@ -64,14 +64,16 @@ export class SdCliIndexFileGenerator {
64
64
  const indexFilePath = path.resolve(pkgPath, "src/index.ts");
65
65
  const tsconfig = await FsUtils.readJsonAsync(path.resolve(pkgPath, "tsconfig.json"));
66
66
  return [
67
- ...(tsconfig.excludes ?? []),
68
- ...(excludes ?? []),
69
67
  indexFilePath,
70
- path.resolve(pkgPath, "src/**/*.d.ts"),
71
- path.resolve(pkgPath, "src/index.ts"),
72
- path.resolve(pkgPath, "src/workers/**/*{.ts,.tsx}"),
73
- // TODO: index에 없는 파일은 watch가 안됨... 처리 필요함.
74
- // path.resolve(pkgPath, "src/internal/**/*{.ts,.tsx}"),
68
+ ...[
69
+ ...(tsconfig.excludes ?? []),
70
+ ...(excludes ?? []),
71
+ "src/**/*.d.ts",
72
+ "src/index.ts",
73
+ "src/workers/**/*{.ts,.tsx}",
74
+ // TODO: index에 없는 파일은 watch가 안됨... 처리 필요함.
75
+ // "src/internal/**/*{.ts,.tsx}",
76
+ ].map((item) => path.resolve(pkgPath, item)),
75
77
  ].map((item) => item.replace(/\\/g, "/"));
76
78
  }
77
79
  }
@@ -109,7 +109,7 @@ Options = UnsafeLegacyRenegotiation`.trim());
109
109
  arrayProcess: "concat",
110
110
  useDelTargetNull: true
111
111
  };`
112
- .replaceAll("\n ", "\n")
112
+ .replace(/\n {8}/g, "\n")
113
113
  .trim();
114
114
  FsUtils.writeFile(path.resolve(this._opt.pkgPath, "dist/pm2.config.cjs"), str);
115
115
  }
@@ -334,7 +334,7 @@ export class SdTsCompiler {
334
334
  (transformers.before ??= []).push(createWorkerTransformer((file, importer) => {
335
335
  const fullPath = path.resolve(path.dirname(importer), file);
336
336
  const relPath = path.relative(path.resolve(this._opt.pkgPath, "src"), fullPath);
337
- return relPath.replace(/\.ts$/, "").replaceAll("\\", "/") + ".js";
337
+ return relPath.replace(/\.ts$/, "").replace(/\\/, "/") + ".js";
338
338
  }));
339
339
  }
340
340
  this._debug(`파일 출력 중...`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/sd-cli",
3
- "version": "12.16.7",
3
+ "version": "12.16.9",
4
4
  "description": "심플리즘 패키지 - CLI",
5
5
  "author": "김석래",
6
6
  "repository": {
@@ -17,10 +17,10 @@
17
17
  "@angular/compiler-cli": "^20.3.15",
18
18
  "@anthropic-ai/sdk": "^0.71.2",
19
19
  "@electron/rebuild": "^4.0.2",
20
- "@simplysm/sd-core-common": "12.16.7",
21
- "@simplysm/sd-core-node": "12.16.7",
22
- "@simplysm/sd-service-server": "12.16.7",
23
- "@simplysm/sd-storage": "12.16.7",
20
+ "@simplysm/sd-core-common": "12.16.9",
21
+ "@simplysm/sd-core-node": "12.16.9",
22
+ "@simplysm/sd-service-server": "12.16.9",
23
+ "@simplysm/sd-storage": "12.16.9",
24
24
  "browserslist": "^4.28.1",
25
25
  "cordova": "^13.0.0",
26
26
  "electron": "^33.4.11",
@@ -31,7 +31,7 @@
31
31
  "glob": "^13.0.0",
32
32
  "node-stdlib-browser": "^1.3.1",
33
33
  "rxjs": "^7.8.2",
34
- "sass-embedded": "^1.96.0",
34
+ "sass-embedded": "^1.97.0",
35
35
  "semver": "^7.7.3",
36
36
  "sharp": "^0.34.5",
37
37
  "specifier-resolution-node": "^1.1.4",
@@ -35,7 +35,7 @@ export class SdCliCapacitor {
35
35
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
36
36
 
37
37
  // 1. Capacitor 프로젝트 초기화
38
- await this._initializeCapacitorProjectAsync(capacitorPath);
38
+ const isNewProject = await this._initializeCapacitorProjectAsync(capacitorPath);
39
39
 
40
40
  // 2. Capacitor 설정 파일 생성
41
41
  await this._createCapacitorConfigAsync(capacitorPath);
@@ -44,7 +44,7 @@ export class SdCliCapacitor {
44
44
  await this._managePlatformsAsync(capacitorPath);
45
45
 
46
46
  // 4. 플러그인 관리
47
- await this._managePluginsAsync(capacitorPath);
47
+ const pluginsChanged = await this._managePluginsAsync(capacitorPath);
48
48
 
49
49
  // 5. 안드로이드 서명 설정
50
50
  await this._setupAndroidSignAsync(capacitorPath);
@@ -58,63 +58,70 @@ export class SdCliCapacitor {
58
58
  }
59
59
 
60
60
  // 8. 웹 자산 동기화
61
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
61
+ if (isNewProject || pluginsChanged) {
62
+ await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
63
+ } else {
64
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy"], capacitorPath);
65
+ }
62
66
  }
63
67
 
64
68
  // 1. Capacitor 프로젝트 초기화
65
- private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<void> {
69
+ private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<boolean> {
66
70
  if (FsUtils.exists(path.resolve(capacitorPath, "www"))) {
67
71
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
68
72
 
69
73
  // 버전 동기화
70
74
  await this._syncVersionAsync(capacitorPath);
71
- } else {
72
- await FsUtils.mkdirsAsync(capacitorPath);
75
+ return false;
76
+ }
73
77
 
74
- // package.json 생성
75
- const projNpmConfig = await FsUtils.readJsonAsync(
76
- path.resolve(this._opt.pkgPath, "../../package.json"),
77
- );
78
- const pkgJson = {
79
- name: this._opt.config.appId,
80
- version: this._npmConfig.version,
81
- private: true,
82
- volta: projNpmConfig.volta,
83
- dependencies: {
84
- "@capacitor/core": "^7.0.0",
85
- },
86
- devDependencies: {
87
- "@capacitor/cli": "^7.0.0",
88
- "@capacitor/assets": "^3.0.0",
89
- ...this._platforms.toObject(
90
- (item) => `@capacitor/${item}`,
91
- () => "^7.0.0",
92
- ),
93
- },
94
- };
95
- await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
96
- space: 2,
97
- });
98
-
99
- // .yarnrc.yml 작성
100
- await FsUtils.writeFileAsync(
101
- path.resolve(capacitorPath, ".yarnrc.yml"),
102
- "nodeLinker: node-modules",
103
- );
104
78
 
105
- // yarn.lock 작성
106
- await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
79
+ await FsUtils.mkdirsAsync(capacitorPath);
107
80
 
108
- // yarn install
109
- await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
81
+ // package.json 생성
82
+ const projNpmConfig = await FsUtils.readJsonAsync(
83
+ path.resolve(this._opt.pkgPath, "../../package.json"),
84
+ );
85
+ const pkgJson = {
86
+ name: this._opt.config.appId,
87
+ version: this._npmConfig.version,
88
+ private: true,
89
+ volta: projNpmConfig.volta,
90
+ dependencies: {
91
+ "@capacitor/core": "^7.0.0",
92
+ "@capacitor/app": "^7.0.0",
93
+ },
94
+ devDependencies: {
95
+ "@capacitor/cli": "^7.0.0",
96
+ "@capacitor/assets": "^3.0.0",
97
+ ...this._platforms.toObject(
98
+ (item) => `@capacitor/${item}`,
99
+ () => "^7.0.0",
100
+ ),
101
+ },
102
+ };
103
+ await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
104
+ space: 2,
105
+ });
110
106
 
111
- // capacitor init
112
- await SdCliCapacitor._execAsync(
113
- "npx",
114
- ["cap", "init", this._opt.config.appName, this._opt.config.appId],
115
- capacitorPath,
116
- );
117
- }
107
+ // .yarnrc.yml 작성
108
+ await FsUtils.writeFileAsync(
109
+ path.resolve(capacitorPath, ".yarnrc.yml"),
110
+ "nodeLinker: node-modules",
111
+ );
112
+
113
+ // yarn.lock 작성
114
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
115
+
116
+ // yarn install
117
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
118
+
119
+ // capacitor init
120
+ await SdCliCapacitor._execAsync(
121
+ "npx",
122
+ ["cap", "init", this._opt.config.appName, this._opt.config.appId],
123
+ capacitorPath,
124
+ );
118
125
 
119
126
  // www/index.html 생성
120
127
  const wwwPath = path.resolve(capacitorPath, "www");
@@ -122,6 +129,8 @@ export class SdCliCapacitor {
122
129
  path.resolve(wwwPath, "index.html"),
123
130
  "<!DOCTYPE html><html><head></head><body></body></html>",
124
131
  );
132
+
133
+ return true;
125
134
  }
126
135
 
127
136
  // 버전 동기화
@@ -191,51 +200,62 @@ export class SdCliCapacitor {
191
200
  }
192
201
  }
193
202
 
194
- // 4. 플러그인 관리
195
- private async _managePluginsAsync(capacitorPath: string): Promise<void> {
203
+ private async _managePluginsAsync(capacitorPath: string): Promise<boolean> {
196
204
  const pkgJsonPath = path.resolve(capacitorPath, "package.json");
197
205
  const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
198
- const currentDeps = Object.keys(pkgJson.dependencies ?? {});
206
+
207
+ const mainDeps = {
208
+ ...this._npmConfig.dependencies,
209
+ ...this._npmConfig.devDependencies,
210
+ ...this._npmConfig.peerDependencies,
211
+ };
199
212
 
200
213
  const usePlugins = Object.keys(this._opt.config.plugins ?? {});
214
+ const currentDeps = pkgJson.dependencies ?? {};
215
+
216
+ let changed = false;
201
217
 
202
218
  // 사용하지 않는 플러그인 제거
203
- for (const dep of currentDeps) {
204
- // @capacitor/core, @capacitor/android 기본 패키지는 제외
205
- if (
206
- dep.startsWith("@capacitor/") &&
207
- ["core", "android", "ios"].some((p) => dep.endsWith(p))
208
- ) {
209
- continue;
219
+ for (const dep of Object.keys(currentDeps)) {
220
+ if (this._isCapacitorPlugin(dep) && !usePlugins.includes(dep)) {
221
+ delete currentDeps[dep];
222
+ changed = true;
223
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
210
224
  }
225
+ }
211
226
 
212
- // 플러그인 목록에 없는 패키지는 제거
213
- if (!usePlugins.includes(dep)) {
214
- // Capacitor 관련 플러그인만 제거
215
- if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
216
- await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
217
- SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
218
- }
227
+ // 플러그인 추가
228
+ for (const plugin of usePlugins) {
229
+ if (!(plugin in currentDeps)) {
230
+ const version = mainDeps[plugin] ?? "^7.0.0";
231
+ currentDeps[plugin] = version;
232
+ changed = true;
233
+ SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
219
234
  }
220
235
  }
221
236
 
222
- // 플러그인 설치
223
- const mainDeps = {
224
- ...this._npmConfig.dependencies,
225
- ...this._npmConfig.devDependencies,
226
- ...this._npmConfig.peerDependencies,
227
- };
237
+ // 변경사항 있을 때만 저장 & install
238
+ if (changed) {
239
+ pkgJson.dependencies = currentDeps;
240
+ await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
241
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
242
+ return true;
243
+ }
244
+ // 변경 없으면 아무것도 안 함 → 오프라인 OK
245
+ return false;
246
+ }
228
247
 
229
- for (const plugin of usePlugins) {
230
- if (!currentDeps.includes(plugin)) {
231
- // 메인 프로젝트에 버전이 있으면 그 버전으로 설치
232
- const version = mainDeps[plugin];
233
- const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
248
+ private _isCapacitorPlugin(dep: string): boolean {
249
+ // 기본 패키지 제외
250
+ const corePackages = [
251
+ "@capacitor/core",
252
+ "@capacitor/android",
253
+ "@capacitor/ios",
254
+ "@capacitor/app",
255
+ ];
256
+ if (corePackages.includes(dep)) return false;
234
257
 
235
- await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
236
- SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
237
- }
238
- }
258
+ return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
239
259
  }
240
260
 
241
261
  // 5. 안드로이드 서명 설정
@@ -254,42 +274,60 @@ export class SdCliCapacitor {
254
274
 
255
275
  // 6. 아이콘 및 스플래시 스크린 설정
256
276
  private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
257
- const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
277
+ const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
258
278
 
259
279
  if (this._opt.config.icon != null) {
260
- await FsUtils.mkdirsAsync(iconDirPath);
280
+ await FsUtils.mkdirsAsync(resourcesDirPath);
261
281
 
262
282
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
263
283
 
264
- // icon.png 자체를 여백 포함으로 생성
265
- const iconPath = path.resolve(iconDirPath, "icon.png");
266
- await this._createPaddedIconAsync(iconSource, iconPath);
284
+ // logo.png: 1024x1024 (Easy Mode)
285
+ // 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
286
+ const logoPath = path.resolve(resourcesDirPath, "logo.png");
287
+ await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
267
288
 
268
- // splash 원본 사용
269
- await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
289
+ // splash.png: 2732x2732
290
+ // 로고 크기 약 35% - 화면 중앙에 적당한 크기로
291
+ const splashPath = path.resolve(resourcesDirPath, "splash.png");
292
+ await this._createCenteredImageAsync(iconSource, splashPath, 2732, 0.35);
293
+
294
+ // 기존 아이콘 삭제 (겹침 방지)
295
+ await this._cleanupExistingIconsAsync(capacitorPath);
270
296
 
271
297
  try {
272
298
  await SdCliCapacitor._execAsync(
273
299
  "npx",
274
- ["@capacitor/assets", "generate", "--android"],
300
+ [
301
+ "@capacitor/assets",
302
+ "generate",
303
+ "--android",
304
+ "--iconBackgroundColor",
305
+ "#ffffff",
306
+ "--splashBackgroundColor",
307
+ "#ffffff",
308
+ ],
275
309
  capacitorPath,
276
310
  );
277
311
  } catch {
278
- SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
312
+ SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
279
313
  }
280
314
  } else {
281
- await FsUtils.removeAsync(iconDirPath);
315
+ await FsUtils.removeAsync(resourcesDirPath);
282
316
  }
283
317
  }
284
318
 
285
- private async _createPaddedIconAsync(sourcePath: string, outputPath: string): Promise<void> {
286
- const outputSize = 1024;
287
- const iconSize = 680; // safe zone (66%)
288
- const padding = Math.floor((outputSize - iconSize) / 2); // 172px
319
+ // 중앙에 로고를 배치한 이미지 생성
320
+ private async _createCenteredImageAsync(
321
+ sourcePath: string,
322
+ outputPath: string,
323
+ outputSize: number,
324
+ logoRatio: number, // 0.0 ~ 1.0
325
+ ): Promise<void> {
326
+ const logoSize = Math.floor(outputSize * logoRatio);
327
+ const padding = Math.floor((outputSize - logoSize) / 2);
289
328
 
290
- // 원본 크기 상관없이 iconSize로 리사이즈 후 여백 추가
291
329
  await sharp(sourcePath)
292
- .resize(iconSize, iconSize, {
330
+ .resize(logoSize, logoSize, {
293
331
  fit: "contain",
294
332
  background: { r: 0, g: 0, b: 0, alpha: 0 },
295
333
  })
@@ -303,6 +341,21 @@ export class SdCliCapacitor {
303
341
  .toFile(outputPath);
304
342
  }
305
343
 
344
+ // 기존 아이콘 파일 삭제
345
+ private async _cleanupExistingIconsAsync(capacitorPath: string): Promise<void> {
346
+ const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
347
+
348
+ if (!FsUtils.exists(androidResPath)) return;
349
+
350
+ const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
351
+ for (const dir of mipmapDirs) {
352
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*.png"));
353
+ for (const file of iconFiles) {
354
+ await FsUtils.removeAsync(file);
355
+ }
356
+ }
357
+ }
358
+
306
359
  // 7. Android 네이티브 설정
307
360
  private async _configureAndroidNativeAsync(capacitorPath: string) {
308
361
  const androidPath = path.resolve(capacitorPath, "android");
@@ -571,14 +624,13 @@ export class SdCliCapacitor {
571
624
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
572
625
  const buildType = this._opt.config.debug ? "debug" : "release";
573
626
 
574
- // 웹 자산 동기화
575
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
576
-
577
627
  // 플랫폼별 빌드
578
628
  await Promise.all(
579
- this._platforms.map((platform) =>
580
- this._buildPlatformAsync(capacitorPath, outPath, platform, buildType),
581
- ),
629
+ this._platforms.map(async (platform) => {
630
+ // 해당 플랫폼만 copy
631
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
632
+ await this._buildPlatformAsync(capacitorPath, outPath, platform, buildType);
633
+ }),
582
634
  );
583
635
  }
584
636
 
@@ -735,12 +787,13 @@ export class SdCliCapacitor {
735
787
  }
736
788
 
737
789
  // cap sync 후 run
738
- await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
790
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", opt.platform], capacitorPath);
739
791
 
740
792
  try {
741
793
  await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
742
- } catch {
794
+ } catch (err) {
743
795
  await SdProcess.spawnAsync("adb", ["kill-server"]);
796
+ throw err;
744
797
  }
745
798
  }
746
799
  }
@@ -78,15 +78,17 @@ export class SdCliIndexFileGenerator {
78
78
  const tsconfig = await FsUtils.readJsonAsync(path.resolve(pkgPath, "tsconfig.json"));
79
79
 
80
80
  return [
81
- ...(tsconfig.excludes ?? []),
82
- ...(excludes ?? []),
83
81
  indexFilePath,
84
- path.resolve(pkgPath, "src/**/*.d.ts"),
85
- path.resolve(pkgPath, "src/index.ts"),
86
- path.resolve(pkgPath, "src/workers/**/*{.ts,.tsx}"),
87
-
88
- // TODO: index에 없는 파일은 watch가 안됨... 처리 필요함.
89
- // path.resolve(pkgPath, "src/internal/**/*{.ts,.tsx}"),
82
+ ...[
83
+ ...(tsconfig.excludes ?? []),
84
+ ...(excludes ?? []),
85
+ "src/**/*.d.ts",
86
+ "src/index.ts",
87
+ "src/workers/**/*{.ts,.tsx}",
88
+
89
+ // TODO: index에 없는 파일은 watch가 안됨... 처리 필요함.
90
+ // "src/internal/**/*{.ts,.tsx}",
91
+ ].map((item) => path.resolve(pkgPath, item)),
90
92
  ].map((item) => item.replace(/\\/g, "/"));
91
93
  }
92
94
  }
@@ -136,7 +136,7 @@ Options = UnsafeLegacyRenegotiation`.trim(),
136
136
  arrayProcess: "concat",
137
137
  useDelTargetNull: true
138
138
  };`
139
- .replaceAll("\n ", "\n")
139
+ .replace(/\n {8}/g, "\n")
140
140
  .trim();
141
141
 
142
142
  FsUtils.writeFile(path.resolve(this._opt.pkgPath, "dist/pm2.config.cjs"), str);
@@ -499,7 +499,7 @@ export class SdTsCompiler {
499
499
  createWorkerTransformer((file, importer) => {
500
500
  const fullPath = path.resolve(path.dirname(importer), file);
501
501
  const relPath = path.relative(path.resolve(this._opt.pkgPath, "src"), fullPath);
502
- return relPath.replace(/\.ts$/, "").replaceAll("\\", "/") + ".js";
502
+ return relPath.replace(/\.ts$/, "").replace(/\\/, "/") + ".js";
503
503
  }),
504
504
  );
505
505
  }