@simplysm/sd-cli 12.16.5 → 12.16.6

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.
@@ -21,7 +21,9 @@ export declare class SdCliCapacitor {
21
21
  private _managePluginsAsync;
22
22
  private _setupAndroidSignAsync;
23
23
  private _setupIconAndSplashScreenAsync;
24
+ private _createPaddedIconAsync;
24
25
  private _configureAndroidNativeAsync;
26
+ private _configureAndroidStylesAsync;
25
27
  private _configureAndroidGradlePropertiesAsync;
26
28
  private _findJava21;
27
29
  private _configureSdkPathAsync;
@@ -1,6 +1,7 @@
1
1
  import * as path from "path";
2
2
  import { FsUtils, PathUtils, SdLogger, SdProcess } from "@simplysm/sd-core-node";
3
3
  import { StringUtils, typescript } from "@simplysm/sd-core-common";
4
+ import sharp from "sharp";
4
5
  export class SdCliCapacitor {
5
6
  constructor(_opt) {
6
7
  this._opt = _opt;
@@ -41,7 +42,7 @@ export class SdCliCapacitor {
41
42
  }
42
43
  // 1. Capacitor 프로젝트 초기화
43
44
  async _initializeCapacitorProjectAsync(capacitorPath) {
44
- if (FsUtils.exists(capacitorPath)) {
45
+ if (FsUtils.exists(path.resolve(capacitorPath, "www"))) {
45
46
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
46
47
  // 버전 동기화
47
48
  await this._syncVersionAsync(capacitorPath);
@@ -88,7 +89,7 @@ export class SdCliCapacitor {
88
89
  if (pkgJson.version !== this._npmConfig.version) {
89
90
  pkgJson.version = this._npmConfig.version;
90
91
  await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
91
- SdCliCapacitor._logger.log(`버전 동기화: ${this._npmConfig.version}`);
92
+ SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
92
93
  }
93
94
  }
94
95
  }
@@ -120,6 +121,7 @@ export class SdCliCapacitor {
120
121
  },
121
122
  android: {
122
123
  allowMixedContent: true,
124
+ statusBarOverlaysWebView: false,
123
125
  },
124
126
  plugins: ${pluginsConfigStr},
125
127
  };
@@ -153,25 +155,25 @@ export class SdCliCapacitor {
153
155
  if (!usePlugins.includes(dep)) {
154
156
  // Capacitor 관련 플러그인만 제거
155
157
  if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
156
- try {
157
- await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
158
- SdCliCapacitor._logger.log(`플러그인 제거: ${dep}`);
159
- }
160
- catch {
161
- SdCliCapacitor._logger.warn(`플러그인 제거 실패: ${dep}`);
162
- }
158
+ await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
159
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
163
160
  }
164
161
  }
165
162
  }
166
163
  // 새 플러그인 설치
164
+ const mainPkgJson = this._npmConfig;
165
+ const mainDeps = {
166
+ ...mainPkgJson.dependencies,
167
+ ...mainPkgJson.devDependencies,
168
+ ...mainPkgJson.peerDependencies,
169
+ };
167
170
  for (const plugin of usePlugins) {
168
171
  if (!currentDeps.includes(plugin)) {
169
- await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
170
- SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
171
- /*try {
172
- } catch {
173
- SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
174
- }*/
172
+ // 메인 프로젝트에 버전이 있으면 그 버전으로 설치
173
+ const version = mainDeps[plugin];
174
+ const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
175
+ await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
176
+ SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
175
177
  }
176
178
  }
177
179
  }
@@ -188,14 +190,16 @@ export class SdCliCapacitor {
188
190
  // 6. 아이콘 및 스플래시 스크린 설정
189
191
  async _setupIconAndSplashScreenAsync(capacitorPath) {
190
192
  const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
191
- // ICON 파일 복사
192
193
  if (this._opt.config.icon != null) {
193
194
  await FsUtils.mkdirsAsync(iconDirPath);
194
195
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
195
- // icon.png, splash.png 같은 파일 사용
196
+ // Adaptive Icon용 여백 추가된 이미지 생성
197
+ // 1024x1024 중 680x680이 safe zone (약 66%)
198
+ const paddedIconPath = path.resolve(iconDirPath, "icon-only.png");
199
+ await this._createPaddedIconAsync(iconSource, paddedIconPath);
200
+ // splash, icon 복사
196
201
  await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "icon.png"));
197
202
  await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
198
- // @capacitor/assets로 아이콘/스플래시 리사이징
199
203
  try {
200
204
  await SdCliCapacitor._execAsync("npx", ["@capacitor/assets", "generate", "--android"], capacitorPath);
201
205
  }
@@ -207,6 +211,21 @@ export class SdCliCapacitor {
207
211
  await FsUtils.removeAsync(iconDirPath);
208
212
  }
209
213
  }
214
+ async _createPaddedIconAsync(sourcePath, outputPath) {
215
+ const size = 1024;
216
+ const iconSize = 680; // safe zone
217
+ const padding = Math.floor((size - iconSize) / 2); // 172px
218
+ await sharp(sourcePath)
219
+ .resize(iconSize, iconSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
220
+ .extend({
221
+ top: padding,
222
+ bottom: padding,
223
+ left: padding,
224
+ right: padding,
225
+ background: { r: 0, g: 0, b: 0, alpha: 0 }, // 투명
226
+ })
227
+ .toFile(outputPath);
228
+ }
210
229
  // 7. Android 네이티브 설정
211
230
  async _configureAndroidNativeAsync(capacitorPath) {
212
231
  const androidPath = path.resolve(capacitorPath, "android");
@@ -223,6 +242,20 @@ export class SdCliCapacitor {
223
242
  await this._configureAndroidBuildGradleAsync(androidPath);
224
243
  // strings.xml 앱 이름 수정
225
244
  await this._configureAndroidStringsAsync(androidPath);
245
+ // styles.xml 수정
246
+ await this._configureAndroidStylesAsync(androidPath);
247
+ }
248
+ async _configureAndroidStylesAsync(androidPath) {
249
+ const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
250
+ if (!FsUtils.exists(stylesPath)) {
251
+ return;
252
+ }
253
+ let stylesContent = await FsUtils.readFileAsync(stylesPath);
254
+ // Edge-to-Edge 비활성화
255
+ if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
256
+ stylesContent = stylesContent.replace(/(<style[^>]*AppTheme[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
257
+ }
258
+ await FsUtils.writeFileAsync(stylesPath, stylesContent);
226
259
  }
227
260
  async _configureAndroidGradlePropertiesAsync(androidPath) {
228
261
  const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
@@ -300,6 +333,20 @@ export class SdCliCapacitor {
300
333
  }
301
334
  }
302
335
  }
336
+ // intentFilters 설정
337
+ const intentFilters = this._opt.config.platform?.android?.intentFilters ?? [];
338
+ for (const filter of intentFilters) {
339
+ const filterKey = filter.action ?? filter.category ?? "";
340
+ if (filterKey && !manifestContent.includes(filterKey)) {
341
+ const actionLine = filter.action != null ? `<action android:name="${filter.action}"/>` : "";
342
+ const categoryLine = filter.category != null ? `<category android:name="${filter.category}"/>` : "";
343
+ manifestContent = manifestContent.replace(/(<activity[\s\S]*?android:name="\.MainActivity"[\s\S]*?>)/, `$1
344
+ <intent-filter>
345
+ ${actionLine}
346
+ ${categoryLine}
347
+ </intent-filter>`);
348
+ }
349
+ }
303
350
  await FsUtils.writeFileAsync(manifestPath, manifestContent);
304
351
  }
305
352
  async _configureAndroidBuildGradleAsync(androidPath) {
@@ -114,6 +114,10 @@ export interface ISdClientBuilderCapacitorConfig {
114
114
  android?: {
115
115
  config?: Record<string, string>;
116
116
  bundle?: boolean;
117
+ intentFilters?: {
118
+ action?: string;
119
+ category?: string;
120
+ }[];
117
121
  sign?: {
118
122
  keystore: string;
119
123
  storePassword: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/sd-cli",
3
- "version": "12.16.5",
3
+ "version": "12.16.6",
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.5",
21
- "@simplysm/sd-core-node": "12.16.5",
22
- "@simplysm/sd-service-server": "12.16.5",
23
- "@simplysm/sd-storage": "12.16.5",
20
+ "@simplysm/sd-core-common": "12.16.6",
21
+ "@simplysm/sd-core-node": "12.16.6",
22
+ "@simplysm/sd-service-server": "12.16.6",
23
+ "@simplysm/sd-storage": "12.16.6",
24
24
  "browserslist": "^4.28.1",
25
25
  "cordova": "^13.0.0",
26
26
  "electron": "^33.4.11",
@@ -33,6 +33,7 @@
33
33
  "rxjs": "^7.8.2",
34
34
  "sass-embedded": "^1.96.0",
35
35
  "semver": "^7.7.3",
36
+ "sharp": "^0.34.5",
36
37
  "specifier-resolution-node": "^1.1.4",
37
38
  "ts-morph": "^27.0.2",
38
39
  "tslib": "^2.8.1",
@@ -3,6 +3,7 @@ import { FsUtils, PathUtils, SdLogger, SdProcess } from "@simplysm/sd-core-node"
3
3
  import { ISdClientBuilderCapacitorConfig } from "../types/config/ISdProjectConfig";
4
4
  import { INpmConfig } from "../types/common-config/INpmConfig";
5
5
  import { StringUtils, typescript } from "@simplysm/sd-core-common";
6
+ import sharp from "sharp";
6
7
 
7
8
  export class SdCliCapacitor {
8
9
  // 상수 정의
@@ -62,7 +63,7 @@ export class SdCliCapacitor {
62
63
 
63
64
  // 1. Capacitor 프로젝트 초기화
64
65
  private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<void> {
65
- if (FsUtils.exists(capacitorPath)) {
66
+ if (FsUtils.exists(path.resolve(capacitorPath, "www"))) {
66
67
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
67
68
 
68
69
  // 버전 동기화
@@ -133,7 +134,7 @@ export class SdCliCapacitor {
133
134
  if (pkgJson.version !== this._npmConfig.version) {
134
135
  pkgJson.version = this._npmConfig.version;
135
136
  await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
136
- SdCliCapacitor._logger.log(`버전 동기화: ${this._npmConfig.version}`);
137
+ SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
137
138
  }
138
139
  }
139
140
  }
@@ -170,6 +171,7 @@ export class SdCliCapacitor {
170
171
  },
171
172
  android: {
172
173
  allowMixedContent: true,
174
+ statusBarOverlaysWebView: false,
173
175
  },
174
176
  plugins: ${pluginsConfigStr},
175
177
  };
@@ -211,25 +213,28 @@ export class SdCliCapacitor {
211
213
  if (!usePlugins.includes(dep)) {
212
214
  // Capacitor 관련 플러그인만 제거
213
215
  if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
214
- try {
215
- await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
216
- SdCliCapacitor._logger.log(`플러그인 제거: ${dep}`);
217
- } catch {
218
- SdCliCapacitor._logger.warn(`플러그인 제거 실패: ${dep}`);
219
- }
216
+ await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
217
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
220
218
  }
221
219
  }
222
220
  }
223
221
 
224
222
  // 새 플러그인 설치
223
+ const mainPkgJson = this._npmConfig;
224
+ const mainDeps = {
225
+ ...mainPkgJson.dependencies,
226
+ ...mainPkgJson.devDependencies,
227
+ ...mainPkgJson.peerDependencies,
228
+ };
229
+
225
230
  for (const plugin of usePlugins) {
226
231
  if (!currentDeps.includes(plugin)) {
227
- await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
228
- SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
229
- /*try {
230
- } catch {
231
- SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
232
- }*/
232
+ // 메인 프로젝트에 버전이 있으면 그 버전으로 설치
233
+ const version = mainDeps[plugin];
234
+ const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
235
+
236
+ await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
237
+ SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
233
238
  }
234
239
  }
235
240
  }
@@ -252,17 +257,20 @@ export class SdCliCapacitor {
252
257
  private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
253
258
  const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
254
259
 
255
- // ICON 파일 복사
256
260
  if (this._opt.config.icon != null) {
257
261
  await FsUtils.mkdirsAsync(iconDirPath);
258
262
 
259
263
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
260
264
 
261
- // icon.png, splash.png 같은 파일 사용
265
+ // Adaptive Icon용 여백 추가된 이미지 생성
266
+ // 1024x1024 중 680x680이 safe zone (약 66%)
267
+ const paddedIconPath = path.resolve(iconDirPath, "icon-only.png");
268
+ await this._createPaddedIconAsync(iconSource, paddedIconPath);
269
+
270
+ // splash, icon 복사
262
271
  await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "icon.png"));
263
272
  await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
264
273
 
265
- // @capacitor/assets로 아이콘/스플래시 리사이징
266
274
  try {
267
275
  await SdCliCapacitor._execAsync(
268
276
  "npx",
@@ -277,6 +285,23 @@ export class SdCliCapacitor {
277
285
  }
278
286
  }
279
287
 
288
+ private async _createPaddedIconAsync(sourcePath: string, outputPath: string): Promise<void> {
289
+ const size = 1024;
290
+ const iconSize = 680; // safe zone
291
+ const padding = Math.floor((size - iconSize) / 2); // 172px
292
+
293
+ await sharp(sourcePath)
294
+ .resize(iconSize, iconSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
295
+ .extend({
296
+ top: padding,
297
+ bottom: padding,
298
+ left: padding,
299
+ right: padding,
300
+ background: { r: 0, g: 0, b: 0, alpha: 0 }, // 투명
301
+ })
302
+ .toFile(outputPath);
303
+ }
304
+
280
305
  // 7. Android 네이티브 설정
281
306
  private async _configureAndroidNativeAsync(capacitorPath: string) {
282
307
  const androidPath = path.resolve(capacitorPath, "android");
@@ -299,6 +324,29 @@ export class SdCliCapacitor {
299
324
 
300
325
  // strings.xml 앱 이름 수정
301
326
  await this._configureAndroidStringsAsync(androidPath);
327
+
328
+ // styles.xml 수정
329
+ await this._configureAndroidStylesAsync(androidPath);
330
+ }
331
+
332
+ private async _configureAndroidStylesAsync(androidPath: string) {
333
+ const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
334
+
335
+ if (!FsUtils.exists(stylesPath)) {
336
+ return;
337
+ }
338
+
339
+ let stylesContent = await FsUtils.readFileAsync(stylesPath);
340
+
341
+ // Edge-to-Edge 비활성화
342
+ if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
343
+ stylesContent = stylesContent.replace(
344
+ /(<style[^>]*AppTheme[^>]*>)/,
345
+ `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
346
+ );
347
+ }
348
+
349
+ await FsUtils.writeFileAsync(stylesPath, stylesContent);
302
350
  }
303
351
 
304
352
  private async _configureAndroidGradlePropertiesAsync(androidPath: string) {
@@ -402,6 +450,26 @@ export class SdCliCapacitor {
402
450
  }
403
451
  }
404
452
 
453
+ // intentFilters 설정
454
+ const intentFilters = this._opt.config.platform?.android?.intentFilters ?? [];
455
+ for (const filter of intentFilters) {
456
+ const filterKey = filter.action ?? filter.category ?? "";
457
+ if (filterKey && !manifestContent.includes(filterKey)) {
458
+ const actionLine = filter.action != null ? `<action android:name="${filter.action}"/>` : "";
459
+ const categoryLine =
460
+ filter.category != null ? `<category android:name="${filter.category}"/>` : "";
461
+
462
+ manifestContent = manifestContent.replace(
463
+ /(<activity[\s\S]*?android:name="\.MainActivity"[\s\S]*?>)/,
464
+ `$1
465
+ <intent-filter>
466
+ ${actionLine}
467
+ ${categoryLine}
468
+ </intent-filter>`,
469
+ );
470
+ }
471
+ }
472
+
405
473
  await FsUtils.writeFileAsync(manifestPath, manifestContent);
406
474
  }
407
475
 
@@ -132,7 +132,6 @@ export interface ISdClientBuilderCordovaConfig {
132
132
  browserslist?: string[];
133
133
  }
134
134
 
135
-
136
135
  export interface ISdClientBuilderCapacitorConfig {
137
136
  appId: string;
138
137
  appName: string;
@@ -143,6 +142,10 @@ export interface ISdClientBuilderCapacitorConfig {
143
142
  android?: {
144
143
  config?: Record<string, string>;
145
144
  bundle?: boolean;
145
+ intentFilters?: {
146
+ action?: string;
147
+ category?: string;
148
+ }[];
146
149
  sign?: {
147
150
  keystore: string;
148
151
  storePassword: string;