@simplysm/sd-cli 12.16.5 → 12.16.7

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,24 @@ 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 mainDeps = {
165
+ ...this._npmConfig.dependencies,
166
+ ...this._npmConfig.devDependencies,
167
+ ...this._npmConfig.peerDependencies,
168
+ };
167
169
  for (const plugin of usePlugins) {
168
170
  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
- }*/
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}`);
175
176
  }
176
177
  }
177
178
  }
@@ -188,14 +189,14 @@ export class SdCliCapacitor {
188
189
  // 6. 아이콘 및 스플래시 스크린 설정
189
190
  async _setupIconAndSplashScreenAsync(capacitorPath) {
190
191
  const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
191
- // ICON 파일 복사
192
192
  if (this._opt.config.icon != null) {
193
193
  await FsUtils.mkdirsAsync(iconDirPath);
194
194
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
195
- // icon.png, splash.png 같은 파일 사용
196
- await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "icon.png"));
195
+ // icon.png 자체를 여백 포함으로 생성
196
+ const iconPath = path.resolve(iconDirPath, "icon.png");
197
+ await this._createPaddedIconAsync(iconSource, iconPath);
198
+ // splash는 원본 사용
197
199
  await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
198
- // @capacitor/assets로 아이콘/스플래시 리사이징
199
200
  try {
200
201
  await SdCliCapacitor._execAsync("npx", ["@capacitor/assets", "generate", "--android"], capacitorPath);
201
202
  }
@@ -207,6 +208,25 @@ export class SdCliCapacitor {
207
208
  await FsUtils.removeAsync(iconDirPath);
208
209
  }
209
210
  }
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로 리사이즈 후 여백 추가
216
+ await sharp(sourcePath)
217
+ .resize(iconSize, iconSize, {
218
+ fit: "contain",
219
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
220
+ })
221
+ .extend({
222
+ top: padding,
223
+ bottom: padding,
224
+ left: padding,
225
+ right: padding,
226
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
227
+ })
228
+ .toFile(outputPath);
229
+ }
210
230
  // 7. Android 네이티브 설정
211
231
  async _configureAndroidNativeAsync(capacitorPath) {
212
232
  const androidPath = path.resolve(capacitorPath, "android");
@@ -223,6 +243,20 @@ export class SdCliCapacitor {
223
243
  await this._configureAndroidBuildGradleAsync(androidPath);
224
244
  // strings.xml 앱 이름 수정
225
245
  await this._configureAndroidStringsAsync(androidPath);
246
+ // styles.xml 수정
247
+ await this._configureAndroidStylesAsync(androidPath);
248
+ }
249
+ async _configureAndroidStylesAsync(androidPath) {
250
+ const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
251
+ if (!FsUtils.exists(stylesPath)) {
252
+ return;
253
+ }
254
+ let stylesContent = await FsUtils.readFileAsync(stylesPath);
255
+ // Edge-to-Edge 비활성화
256
+ if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
257
+ stylesContent = stylesContent.replace(/(<style[^>]*AppTheme[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
258
+ }
259
+ await FsUtils.writeFileAsync(stylesPath, stylesContent);
226
260
  }
227
261
  async _configureAndroidGradlePropertiesAsync(androidPath) {
228
262
  const gradlePropsPath = path.resolve(androidPath, "gradle.properties");
@@ -300,6 +334,20 @@ export class SdCliCapacitor {
300
334
  }
301
335
  }
302
336
  }
337
+ // intentFilters 설정
338
+ const intentFilters = this._opt.config.platform?.android?.intentFilters ?? [];
339
+ for (const filter of intentFilters) {
340
+ const filterKey = filter.action ?? filter.category ?? "";
341
+ if (filterKey && !manifestContent.includes(filterKey)) {
342
+ const actionLine = filter.action != null ? `<action android:name="${filter.action}"/>` : "";
343
+ const categoryLine = filter.category != null ? `<category android:name="${filter.category}"/>` : "";
344
+ manifestContent = manifestContent.replace(/(<activity[\s\S]*?android:name="\.MainActivity"[\s\S]*?>)/, `$1
345
+ <intent-filter>
346
+ ${actionLine}
347
+ ${categoryLine}
348
+ </intent-filter>`);
349
+ }
350
+ }
303
351
  await FsUtils.writeFileAsync(manifestPath, manifestContent);
304
352
  }
305
353
  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.7",
4
4
  "description": "심플리즘 패키지 - CLI",
5
5
  "author": "김석래",
6
6
  "repository": {
@@ -17,22 +17,23 @@
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.7",
21
+ "@simplysm/sd-core-node": "12.16.7",
22
+ "@simplysm/sd-service-server": "12.16.7",
23
+ "@simplysm/sd-storage": "12.16.7",
24
24
  "browserslist": "^4.28.1",
25
25
  "cordova": "^13.0.0",
26
26
  "electron": "^33.4.11",
27
27
  "electron-builder": "^25.1.8",
28
28
  "esbuild": "0.25.9",
29
29
  "esbuild-sass-plugin": "^3.3.1",
30
- "eslint": "^9.39.1",
30
+ "eslint": "^9.39.2",
31
31
  "glob": "^13.0.0",
32
32
  "node-stdlib-browser": "^1.3.1",
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,27 @@ 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 mainDeps = {
224
+ ...this._npmConfig.dependencies,
225
+ ...this._npmConfig.devDependencies,
226
+ ...this._npmConfig.peerDependencies,
227
+ };
228
+
225
229
  for (const plugin of usePlugins) {
226
230
  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
- }*/
231
+ // 메인 프로젝트에 버전이 있으면 그 버전으로 설치
232
+ const version = mainDeps[plugin];
233
+ const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
234
+
235
+ await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
236
+ SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
233
237
  }
234
238
  }
235
239
  }
@@ -252,17 +256,18 @@ export class SdCliCapacitor {
252
256
  private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
253
257
  const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
254
258
 
255
- // ICON 파일 복사
256
259
  if (this._opt.config.icon != null) {
257
260
  await FsUtils.mkdirsAsync(iconDirPath);
258
261
 
259
262
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
260
263
 
261
- // icon.png, splash.png 같은 파일 사용
262
- await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "icon.png"));
264
+ // icon.png 자체를 여백 포함으로 생성
265
+ const iconPath = path.resolve(iconDirPath, "icon.png");
266
+ await this._createPaddedIconAsync(iconSource, iconPath);
267
+
268
+ // splash는 원본 사용
263
269
  await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
264
270
 
265
- // @capacitor/assets로 아이콘/스플래시 리사이징
266
271
  try {
267
272
  await SdCliCapacitor._execAsync(
268
273
  "npx",
@@ -277,6 +282,27 @@ export class SdCliCapacitor {
277
282
  }
278
283
  }
279
284
 
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
289
+
290
+ // 원본 크기 상관없이 iconSize로 리사이즈 후 여백 추가
291
+ await sharp(sourcePath)
292
+ .resize(iconSize, iconSize, {
293
+ fit: "contain",
294
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
295
+ })
296
+ .extend({
297
+ top: padding,
298
+ bottom: padding,
299
+ left: padding,
300
+ right: padding,
301
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
302
+ })
303
+ .toFile(outputPath);
304
+ }
305
+
280
306
  // 7. Android 네이티브 설정
281
307
  private async _configureAndroidNativeAsync(capacitorPath: string) {
282
308
  const androidPath = path.resolve(capacitorPath, "android");
@@ -299,6 +325,29 @@ export class SdCliCapacitor {
299
325
 
300
326
  // strings.xml 앱 이름 수정
301
327
  await this._configureAndroidStringsAsync(androidPath);
328
+
329
+ // styles.xml 수정
330
+ await this._configureAndroidStylesAsync(androidPath);
331
+ }
332
+
333
+ private async _configureAndroidStylesAsync(androidPath: string) {
334
+ const stylesPath = path.resolve(androidPath, "app/src/main/res/values/styles.xml");
335
+
336
+ if (!FsUtils.exists(stylesPath)) {
337
+ return;
338
+ }
339
+
340
+ let stylesContent = await FsUtils.readFileAsync(stylesPath);
341
+
342
+ // Edge-to-Edge 비활성화
343
+ if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
344
+ stylesContent = stylesContent.replace(
345
+ /(<style[^>]*AppTheme[^>]*>)/,
346
+ `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
347
+ );
348
+ }
349
+
350
+ await FsUtils.writeFileAsync(stylesPath, stylesContent);
302
351
  }
303
352
 
304
353
  private async _configureAndroidGradlePropertiesAsync(androidPath: string) {
@@ -402,6 +451,26 @@ export class SdCliCapacitor {
402
451
  }
403
452
  }
404
453
 
454
+ // intentFilters 설정
455
+ const intentFilters = this._opt.config.platform?.android?.intentFilters ?? [];
456
+ for (const filter of intentFilters) {
457
+ const filterKey = filter.action ?? filter.category ?? "";
458
+ if (filterKey && !manifestContent.includes(filterKey)) {
459
+ const actionLine = filter.action != null ? `<action android:name="${filter.action}"/>` : "";
460
+ const categoryLine =
461
+ filter.category != null ? `<category android:name="${filter.category}"/>` : "";
462
+
463
+ manifestContent = manifestContent.replace(
464
+ /(<activity[\s\S]*?android:name="\.MainActivity"[\s\S]*?>)/,
465
+ `$1
466
+ <intent-filter>
467
+ ${actionLine}
468
+ ${categoryLine}
469
+ </intent-filter>`,
470
+ );
471
+ }
472
+ }
473
+
405
474
  await FsUtils.writeFileAsync(manifestPath, manifestContent);
406
475
  }
407
476
 
@@ -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;