@simplysm/sd-cli 12.15.68 → 12.15.70

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/SdCliCapacitor.d.ts +37 -0
  2. package/dist/entry/SdCliCapacitor.js +390 -0
  3. package/dist/pkg-builders/client/SdClientBuildRunner.d.ts +1 -0
  4. package/dist/pkg-builders/client/SdClientBuildRunner.js +14 -0
  5. package/dist/pkg-builders/client/SdNgBundler.js +3 -1
  6. package/dist/sd-cli-entry.js +18 -44
  7. package/dist/types/config/ISdProjectConfig.d.ts +31 -0
  8. package/package.json +5 -5
  9. package/src/entry/SdCliCapacitor.ts +560 -0
  10. package/src/pkg-builders/client/SdClientBuildRunner.ts +17 -0
  11. package/src/pkg-builders/client/SdNgBundler.ts +3 -1
  12. package/src/sd-cli-entry.ts +26 -56
  13. package/src/types/config/ISdProjectConfig.ts +34 -0
  14. package/dist/fix/convertPrivateToHash.d.ts +0 -1
  15. package/dist/fix/convertPrivateToHash.js +0 -58
  16. package/dist/fix/convertSdAngularSymbolNames.d.ts +0 -1
  17. package/dist/fix/convertSdAngularSymbolNames.js +0 -22
  18. package/dist/fix/core/convertSymbols.d.ts +0 -1
  19. package/dist/fix/core/convertSymbols.js +0 -101
  20. package/dist/fix/core/getTsMortphSourceFiles.d.ts +0 -1
  21. package/dist/fix/core/getTsMortphSourceFiles.js +0 -7
  22. package/dist/fix/core/removeSymbols.d.ts +0 -1
  23. package/dist/fix/core/removeSymbols.js +0 -76
  24. package/dist/fix/removeSdAngularSymbolNames.d.ts +0 -1
  25. package/dist/fix/removeSdAngularSymbolNames.js +0 -6
  26. package/dist/fix/removeUnusedImports.d.ts +0 -1
  27. package/dist/fix/removeUnusedImports.js +0 -41
  28. package/dist/fix/removeUnusedInjects.d.ts +0 -1
  29. package/dist/fix/removeUnusedInjects.js +0 -37
  30. package/dist/fix/removeUnusedProtectedReadonly.d.ts +0 -1
  31. package/dist/fix/removeUnusedProtectedReadonly.js +0 -57
  32. package/src/fix/convertPrivateToHash.ts +0 -74
  33. package/src/fix/convertSdAngularSymbolNames.ts +0 -27
  34. package/src/fix/core/convertSymbols.ts +0 -135
  35. package/src/fix/core/getTsMortphSourceFiles.ts +0 -9
  36. package/src/fix/core/removeSymbols.ts +0 -102
  37. package/src/fix/removeSdAngularSymbolNames.ts +0 -9
  38. package/src/fix/removeUnusedImports.ts +0 -50
  39. package/src/fix/removeUnusedInjects.ts +0 -50
  40. package/src/fix/removeUnusedProtectedReadonly.ts +0 -69
@@ -0,0 +1,560 @@
1
+ import * as path from "path";
2
+ import { FsUtils, PathUtils, SdLogger, SdProcess } from "@simplysm/sd-core-node";
3
+ import { ISdClientBuilderCapacitorConfig } from "../types/config/ISdProjectConfig";
4
+ import { INpmConfig } from "../types/common-config/INpmConfig";
5
+ import { StringUtils, typescript } from "@simplysm/sd-core-common";
6
+
7
+ export class SdCliCapacitor {
8
+ // 상수 정의
9
+ private readonly _CAPACITOR_DIR_NAME = ".capacitor";
10
+ private readonly _CONFIG_FILE_NAME = "capacitor.config.ts";
11
+ private readonly _KEYSTORE_FILE_NAME = "android.keystore";
12
+ private readonly _ICON_DIR_PATH = "resources";
13
+
14
+ // private readonly _ANDROID_DIR_NAME = "android";
15
+ // private readonly _WWW_DIR_NAME = "www";
16
+
17
+ private readonly _platforms: string[];
18
+ private readonly _npmConfig: INpmConfig;
19
+
20
+ constructor(private readonly _opt: { pkgPath: string; config: ISdClientBuilderCapacitorConfig }) {
21
+ this._platforms = Object.keys(this._opt.config.platform ?? { android: {} });
22
+ this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
23
+ }
24
+
25
+ private static readonly _logger = SdLogger.get(["simplysm", "sd-cli", "SdCliCapacitor"]);
26
+
27
+ private static async _execAsync(cmd: string, args: string[], cwd: string): Promise<void> {
28
+ this._logger.debug(`실행 명령: ${cmd + " " + args.join(" ")}`);
29
+ const msg = await SdProcess.spawnAsync(cmd, args, { cwd });
30
+ this._logger.debug(`실행 결과: ${msg}`);
31
+ }
32
+
33
+ async initializeAsync(): Promise<void> {
34
+ const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
35
+
36
+ // 1. Capacitor 프로젝트 초기화
37
+ await this._initializeCapacitorProjectAsync(capacitorPath);
38
+
39
+ // 2. Capacitor 설정 파일 생성
40
+ this._createCapacitorConfig(capacitorPath);
41
+
42
+ // 3. 플랫폼 관리
43
+ await this._managePlatformsAsync(capacitorPath);
44
+
45
+ // 4. 플러그인 관리
46
+ await this._managePluginsAsync(capacitorPath);
47
+
48
+ // 5. 안드로이드 서명 설정
49
+ this._setupAndroidSign(capacitorPath);
50
+
51
+ // 6. 아이콘 및 스플래시 스크린 설정
52
+ await this._setupIconAndSplashScreenAsync(capacitorPath);
53
+
54
+ // 7. Android 네이티브 설정 (AndroidManifest.xml, build.gradle 등)
55
+ if (this._platforms.includes("android")) {
56
+ this._configureAndroidNative(capacitorPath);
57
+ }
58
+
59
+ // 8. 웹 자산 동기화
60
+ await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
61
+ }
62
+
63
+ // 1. Capacitor 프로젝트 초기화
64
+ private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<void> {
65
+ if (FsUtils.exists(capacitorPath)) {
66
+ SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
67
+
68
+ // 버전 동기화
69
+ this._syncVersion(capacitorPath);
70
+ } else {
71
+ FsUtils.mkdirs(capacitorPath);
72
+
73
+ // package.json 생성
74
+ const pkgJson = {
75
+ name: this._opt.config.appId,
76
+ version: this._npmConfig.version,
77
+ private: true,
78
+ dependencies: {
79
+ "@capacitor/core": "^7.0.0",
80
+ },
81
+ devDependencies: {
82
+ "@capacitor/cli": "^7.0.0",
83
+ "@capacitor/assets": "^3.0.0",
84
+ },
85
+ };
86
+ FsUtils.writeJson(path.resolve(capacitorPath, "package.json"), pkgJson, { space: 2 });
87
+
88
+ // yarn install
89
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
90
+
91
+ // capacitor init
92
+ await SdCliCapacitor._execAsync(
93
+ "npx",
94
+ ["cap", "init", this._opt.config.appName, this._opt.config.appId],
95
+ capacitorPath,
96
+ );
97
+ }
98
+ }
99
+
100
+ // 버전 동기화
101
+ private _syncVersion(capacitorPath: string): void {
102
+ const pkgJsonPath = path.resolve(capacitorPath, "package.json");
103
+
104
+ if (FsUtils.exists(pkgJsonPath)) {
105
+ const pkgJson = FsUtils.readJson(pkgJsonPath) as INpmConfig;
106
+
107
+ if (pkgJson.version !== this._npmConfig.version) {
108
+ pkgJson.version = this._npmConfig.version;
109
+ FsUtils.writeJson(pkgJsonPath, pkgJson, { space: 2 });
110
+ SdCliCapacitor._logger.log(`버전 동기화: ${this._npmConfig.version}`);
111
+ }
112
+ }
113
+ }
114
+
115
+ // 2. Capacitor 설정 파일 생성
116
+ private _createCapacitorConfig(capacitorPath: string): void {
117
+ const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
118
+
119
+ // 플러그인 옵션 생성
120
+ const pluginOptions: Record<string, Record<string, unknown>> = {};
121
+ for (const [pluginName, options] of Object.entries(this._opt.config.plugins ?? {})) {
122
+ if (options !== true) {
123
+ // @capacitor/splash-screen → SplashScreen 형태로 변환
124
+ const configKey = StringUtils.toPascalCase(pluginName.split("/").last()!);
125
+ pluginOptions[configKey] = options;
126
+ }
127
+ }
128
+
129
+ const pluginsConfigStr =
130
+ Object.keys(pluginOptions).length > 0
131
+ ? JSON.stringify(pluginOptions, null, 4).replace(/^/gm, " ").trim()
132
+ : "{}";
133
+
134
+ const configContent = typescript`
135
+ import type { CapacitorConfig } from "@capacitor/cli";
136
+
137
+ const config: CapacitorConfig = {
138
+ appId: "${this._opt.config.appId}",
139
+ appName: "${this._opt.config.appName}",
140
+ server: {
141
+ androidScheme: "http",
142
+ cleartext: true,
143
+ allowNavigation: ["*"],
144
+ },
145
+ android: {
146
+ allowMixedContent: true,
147
+ },
148
+ plugins: ${pluginsConfigStr},
149
+ };
150
+
151
+ export default config;
152
+ `;
153
+
154
+ FsUtils.writeFile(configFilePath, configContent);
155
+ }
156
+
157
+ // 3. 플랫폼 관리
158
+ private async _managePlatformsAsync(capacitorPath: string): Promise<void> {
159
+ for (const platform of this._platforms) {
160
+ if (FsUtils.exists(path.resolve(capacitorPath, platform))) continue;
161
+
162
+ await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], capacitorPath);
163
+ }
164
+ }
165
+
166
+ // 4. 플러그인 관리
167
+ private async _managePluginsAsync(capacitorPath: string): Promise<void> {
168
+ const pkgJsonPath = path.resolve(capacitorPath, "package.json");
169
+ const pkgJson = FsUtils.readJson(pkgJsonPath) as INpmConfig;
170
+ const currentDeps = Object.keys(pkgJson.dependencies ?? {});
171
+
172
+ const usePlugins = Object.keys(this._opt.config.plugins ?? {});
173
+
174
+ // 사용하지 않는 플러그인 제거
175
+ for (const dep of currentDeps) {
176
+ // @capacitor/core, @capacitor/android 등 기본 패키지는 제외
177
+ if (
178
+ dep.startsWith("@capacitor/") &&
179
+ ["core", "android", "ios"].some((p) => dep.endsWith(p))
180
+ ) {
181
+ continue;
182
+ }
183
+
184
+ // 플러그인 목록에 없는 패키지는 제거
185
+ if (!usePlugins.includes(dep)) {
186
+ // Capacitor 관련 플러그인만 제거
187
+ if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
188
+ try {
189
+ await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
190
+ SdCliCapacitor._logger.log(`플러그인 제거: ${dep}`);
191
+ } catch {
192
+ SdCliCapacitor._logger.warn(`플러그인 제거 실패: ${dep}`);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ // 새 플러그인 설치
199
+ for (const plugin of usePlugins) {
200
+ if (!currentDeps.includes(plugin)) {
201
+ try {
202
+ await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
203
+ SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
204
+ } catch {
205
+ SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ // 5. 안드로이드 서명 설정
212
+ private _setupAndroidSign(capacitorPath: string): void {
213
+ const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
214
+
215
+ if (this._opt.config.platform?.android?.sign) {
216
+ FsUtils.copy(
217
+ path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore),
218
+ keystorePath,
219
+ );
220
+ } else {
221
+ FsUtils.remove(keystorePath);
222
+ }
223
+ }
224
+
225
+ // 6. 아이콘 및 스플래시 스크린 설정
226
+ private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
227
+ const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
228
+
229
+ // ICON 파일 복사
230
+ if (this._opt.config.icon != null) {
231
+ FsUtils.mkdirs(iconDirPath);
232
+
233
+ const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
234
+
235
+ // icon.png, splash.png 둘 다 같은 파일 사용
236
+ FsUtils.copy(iconSource, path.resolve(iconDirPath, "icon.png"));
237
+ FsUtils.copy(iconSource, path.resolve(iconDirPath, "splash.png"));
238
+
239
+ // @capacitor/assets로 아이콘/스플래시 리사이징
240
+ try {
241
+ await SdCliCapacitor._execAsync(
242
+ "npx",
243
+ ["@capacitor/assets", "generate", "--android"],
244
+ capacitorPath,
245
+ );
246
+ } catch {
247
+ SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
248
+ }
249
+ } else {
250
+ FsUtils.remove(iconDirPath);
251
+ }
252
+ }
253
+
254
+ // 7. Android 네이티브 설정
255
+ private _configureAndroidNative(capacitorPath: string): void {
256
+ const androidPath = path.resolve(capacitorPath, "android");
257
+
258
+ if (!FsUtils.exists(androidPath)) {
259
+ return;
260
+ }
261
+
262
+ // AndroidManifest.xml 수정
263
+ this._configureAndroidManifest(androidPath);
264
+
265
+ // build.gradle 수정 (필요시)
266
+ this._configureAndroidBuildGradle(androidPath);
267
+
268
+ // strings.xml 앱 이름 수정
269
+ this._configureAndroidStrings(androidPath);
270
+ }
271
+
272
+ private _configureAndroidManifest(androidPath: string): void {
273
+ const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
274
+
275
+ if (!FsUtils.exists(manifestPath)) {
276
+ return;
277
+ }
278
+
279
+ let manifestContent = FsUtils.readFile(manifestPath);
280
+
281
+ // usesCleartextTraffic 설정
282
+ if (!manifestContent.includes("android:usesCleartextTraffic")) {
283
+ manifestContent = manifestContent.replace(
284
+ "<application",
285
+ '<application android:usesCleartextTraffic="true"',
286
+ );
287
+ }
288
+
289
+ // 추가 권한 설정
290
+ const permissions = this._opt.config.platform?.android?.permissions ?? [];
291
+ for (const perm of permissions) {
292
+ const permTag = `<uses-permission android:name="android.permission.${perm.name}"`;
293
+ if (!manifestContent.includes(permTag)) {
294
+ const maxSdkAttr =
295
+ perm.maxSdkVersion != null ? ` android:maxSdkVersion="${perm.maxSdkVersion}"` : "";
296
+ const ignoreAttr = perm.ignore != null ? ` tools:ignore="${perm.ignore}"` : "";
297
+ const permLine = ` ${permTag}${maxSdkAttr}${ignoreAttr} />\n`;
298
+
299
+ // tools 네임스페이스 추가
300
+ if (perm.ignore != null && !manifestContent.includes("xmlns:tools=")) {
301
+ manifestContent = manifestContent.replace(
302
+ "<manifest xmlns:android",
303
+ '<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android',
304
+ );
305
+ }
306
+
307
+ manifestContent = manifestContent.replace("</manifest>", `${permLine}</manifest>`);
308
+ }
309
+ }
310
+
311
+ // 추가 application 설정
312
+ const appConfig = this._opt.config.platform?.android?.config;
313
+ if (appConfig) {
314
+ for (const [key, value] of Object.entries(appConfig)) {
315
+ const attr = `android:${key}="${value}"`;
316
+ if (!manifestContent.includes(`android:${key}=`)) {
317
+ manifestContent = manifestContent.replace("<application", `<application ${attr}`);
318
+ }
319
+ }
320
+ }
321
+
322
+ FsUtils.writeFile(manifestPath, manifestContent);
323
+ }
324
+
325
+ private _configureAndroidBuildGradle(androidPath: string): void {
326
+ const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
327
+
328
+ if (!FsUtils.exists(buildGradlePath)) {
329
+ return;
330
+ }
331
+
332
+ let gradleContent = FsUtils.readFile(buildGradlePath);
333
+
334
+ // versionName, versionCode 설정
335
+ const version = this._npmConfig.version;
336
+ const versionParts = version.split(".");
337
+ const versionCode =
338
+ parseInt(versionParts[0] ?? "0") * 10000 +
339
+ parseInt(versionParts[1] ?? "0") * 100 +
340
+ parseInt(versionParts[2] ?? "0");
341
+
342
+ gradleContent = gradleContent.replace(/versionCode \d+/, `versionCode ${versionCode}`);
343
+ gradleContent = gradleContent.replace(/versionName "[^"]+"/, `versionName "${version}"`);
344
+
345
+ // SDK 버전 설정
346
+ if (this._opt.config.platform?.android?.sdkVersion != null) {
347
+ const sdkVersion = this._opt.config.platform.android.sdkVersion;
348
+ gradleContent = gradleContent.replace(/minSdkVersion \d+/, `minSdkVersion ${sdkVersion}`);
349
+ gradleContent = gradleContent.replace(
350
+ /targetSdkVersion \d+/,
351
+ `targetSdkVersion ${sdkVersion}`,
352
+ );
353
+ }
354
+
355
+ // Signing 설정
356
+ const signConfig = this._opt.config.platform?.android?.sign;
357
+ if (signConfig) {
358
+ const keystoreRelativePath = `../${this._KEYSTORE_FILE_NAME}`;
359
+ const keystoreType = signConfig.keystoreType ?? "jks";
360
+
361
+ // signingConfigs 블록 추가
362
+ if (!gradleContent.includes("signingConfigs")) {
363
+ const signingConfigsBlock = `
364
+ signingConfigs {
365
+ release {
366
+ storeFile file("${keystoreRelativePath}")
367
+ storePassword "${signConfig.storePassword}"
368
+ keyAlias "${signConfig.alias}"
369
+ keyPassword "${signConfig.password}"
370
+ storeType "${keystoreType}"
371
+ }
372
+ }
373
+ `;
374
+ // android { 블록 내부에 추가
375
+ gradleContent = gradleContent.replace(/(android\s*\{)/, `$1${signingConfigsBlock}`);
376
+ }
377
+
378
+ // buildTypes.release에 signingConfig 추가
379
+ if (!gradleContent.includes("signingConfig signingConfigs.release")) {
380
+ gradleContent = gradleContent.replace(
381
+ /(buildTypes\s*\{[\s\S]*?release\s*\{)/,
382
+ `$1\n signingConfig signingConfigs.release`,
383
+ );
384
+ }
385
+ }
386
+
387
+ FsUtils.writeFile(buildGradlePath, gradleContent);
388
+ }
389
+
390
+ private _configureAndroidStrings(androidPath: string): void {
391
+ const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
392
+
393
+ if (!FsUtils.exists(stringsPath)) {
394
+ return;
395
+ }
396
+
397
+ let stringsContent = FsUtils.readFile(stringsPath);
398
+ stringsContent = stringsContent.replace(
399
+ /<string name="app_name">[^<]+<\/string>/,
400
+ `<string name="app_name">${this._opt.config.appName}</string>`,
401
+ );
402
+ stringsContent = stringsContent.replace(
403
+ /<string name="title_activity_main">[^<]+<\/string>/,
404
+ `<string name="title_activity_main">${this._opt.config.appName}</string>`,
405
+ );
406
+ stringsContent = stringsContent.replace(
407
+ /<string name="package_name">[^<]+<\/string>/,
408
+ `<string name="package_name">${this._opt.config.appId}</string>`,
409
+ );
410
+ stringsContent = stringsContent.replace(
411
+ /<string name="custom_url_scheme">[^<]+<\/string>/,
412
+ `<string name="custom_url_scheme">${this._opt.config.appId}</string>`,
413
+ );
414
+
415
+ FsUtils.writeFile(stringsPath, stringsContent);
416
+ }
417
+
418
+ async buildAsync(outPath: string): Promise<void> {
419
+ const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
420
+ const buildType = this._opt.config.debug ? "debug" : "release";
421
+
422
+ // 웹 자산 동기화
423
+ await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
424
+
425
+ // 플랫폼별 빌드
426
+ await Promise.all(
427
+ this._platforms.map((platform) =>
428
+ this._buildPlatformAsync(capacitorPath, outPath, platform, buildType),
429
+ ),
430
+ );
431
+ }
432
+
433
+ private async _buildPlatformAsync(
434
+ capacitorPath: string,
435
+ outPath: string,
436
+ platform: string,
437
+ buildType: string,
438
+ ): Promise<void> {
439
+ if (platform === "android") {
440
+ await this._buildAndroidAsync(capacitorPath, outPath, buildType);
441
+ }
442
+ // iOS 지원 시 추가
443
+ }
444
+
445
+ private async _buildAndroidAsync(
446
+ capacitorPath: string,
447
+ outPath: string,
448
+ buildType: string,
449
+ ): Promise<void> {
450
+ const androidPath = path.resolve(capacitorPath, "android");
451
+ const targetOutPath = path.resolve(outPath, "android");
452
+
453
+ // Gradle wrapper로 빌드
454
+ const isBundle = this._opt.config.platform?.android?.bundle;
455
+ const gradleTask =
456
+ buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
457
+
458
+ // gradlew 실행 권한 부여 (Linux/Mac)
459
+ const gradlewPath = path.resolve(androidPath, "gradlew");
460
+ if (FsUtils.exists(gradlewPath)) {
461
+ try {
462
+ await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
463
+ } catch {
464
+ // Windows에서는 무시
465
+ }
466
+ }
467
+
468
+ // Gradle 빌드 실행
469
+ const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
470
+ await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
471
+
472
+ // 빌드 결과물 복사
473
+ this._copyAndroidBuildOutput(androidPath, targetOutPath, buildType);
474
+ }
475
+
476
+ private _copyAndroidBuildOutput(
477
+ androidPath: string,
478
+ targetOutPath: string,
479
+ buildType: string,
480
+ ): void {
481
+ const isBundle = this._opt.config.platform?.android?.bundle;
482
+ const isSigned = !!this._opt.config.platform?.android?.sign;
483
+
484
+ const ext = isBundle ? "aab" : "apk";
485
+ const outputType = isBundle ? "bundle" : "apk";
486
+ const fileName = isSigned ? `app-${buildType}.${ext}` : `app-${buildType}-unsigned.${ext}`;
487
+
488
+ const sourcePath = path.resolve(
489
+ androidPath,
490
+ "app/build/outputs",
491
+ outputType,
492
+ buildType,
493
+ fileName,
494
+ );
495
+
496
+ const actualPath = FsUtils.exists(sourcePath)
497
+ ? sourcePath
498
+ : path.resolve(
499
+ androidPath,
500
+ "app/build/outputs",
501
+ outputType,
502
+ buildType,
503
+ `app-${buildType}.${ext}`,
504
+ );
505
+
506
+ if (!FsUtils.exists(actualPath)) {
507
+ SdCliCapacitor._logger.warn(`빌드 결과물을 찾을 수 없습니다: ${actualPath}`);
508
+ return;
509
+ }
510
+
511
+ const outputFileName = `${this._opt.config.appName}${isSigned ? "" : "-unsigned"}-latest.${ext}`;
512
+
513
+ FsUtils.mkdirs(targetOutPath);
514
+ FsUtils.copy(actualPath, path.resolve(targetOutPath, outputFileName));
515
+
516
+ const updatesPath = path.resolve(targetOutPath, "updates");
517
+ FsUtils.mkdirs(updatesPath);
518
+ FsUtils.copy(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
519
+ }
520
+
521
+ static async runWebviewOnDeviceAsync(opt: {
522
+ platform: string;
523
+ package: string;
524
+ url?: string;
525
+ }): Promise<void> {
526
+ const projNpmConf = FsUtils.readJson(path.resolve(process.cwd(), "package.json")) as INpmConfig;
527
+ const allPkgPaths = projNpmConf.workspaces!.mapMany((item) =>
528
+ FsUtils.glob(PathUtils.posix(process.cwd(), item)),
529
+ );
530
+
531
+ const capacitorPath = path.resolve(
532
+ allPkgPaths.single((item) => item.endsWith(opt.package))!,
533
+ ".capacitor",
534
+ );
535
+
536
+ if (opt.url !== undefined) {
537
+ // capacitor.config.ts의 server.url 설정 업데이트
538
+ const configPath = path.resolve(capacitorPath, "capacitor.config.ts");
539
+ if (FsUtils.exists(configPath)) {
540
+ let configContent = FsUtils.readFile(configPath);
541
+ const serverUrl = `${opt.url.replace(/\/$/, "")}/${opt.package}/capacitor/`;
542
+
543
+ // 기존 url 설정이 있으면 교체, 없으면 server 블록 첫 줄에 추가
544
+ if (configContent.includes("url:")) {
545
+ configContent = configContent.replace(/url:\s*"[^"]*"/, `url: "${serverUrl}"`);
546
+ } else if (configContent.includes("server:")) {
547
+ configContent = configContent.replace(
548
+ /server:\s*\{/,
549
+ `server: {\n url: "${serverUrl}",`,
550
+ );
551
+ }
552
+ FsUtils.writeFile(configPath, configContent);
553
+ }
554
+ }
555
+
556
+ // cap sync 후 run
557
+ await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
558
+ await this._execAsync("npx", ["cap", "run", opt.platform, "--target", "device"], capacitorPath);
559
+ }
560
+ }
@@ -8,12 +8,14 @@ import { INpmConfig } from "../../types/common-config/INpmConfig";
8
8
  import { SdCliNgRoutesFileGenerator } from "./SdCliNgRoutesFileGenerator";
9
9
  import { SdCliElectron } from "../../entry/SdCliElectron";
10
10
  import { ISdClientPackageConfig } from "../../types/config/ISdProjectConfig";
11
+ import { SdCliCapacitor } from "../../entry/SdCliCapacitor";
11
12
 
12
13
  export class SdClientBuildRunner extends SdBuildRunnerBase<"client"> {
13
14
  protected override _logger = SdLogger.get(["simplysm", "sd-cli", "SdClientBuildRunner"]);
14
15
 
15
16
  private _ngBundlers?: SdNgBundler[];
16
17
  private _cordova?: SdCliCordova;
18
+ private _capacitor?: SdCliCapacitor
17
19
 
18
20
  protected override async _runAsync(modifiedFileSet?: Set<TNormPath>): Promise<ISdBuildResult> {
19
21
  // 최초 한번
@@ -37,6 +39,16 @@ export class SdClientBuildRunner extends SdBuildRunnerBase<"client"> {
37
39
  await this._cordova.initializeAsync();
38
40
  }
39
41
 
42
+ // capacitor
43
+ if (this._pkgConf.builder?.capacitor) {
44
+ this._debug("Preparing Capacitor...");
45
+ this._capacitor = new SdCliCapacitor({
46
+ pkgPath: this._opt.pkgPath,
47
+ config: this._pkgConf.builder.capacitor,
48
+ });
49
+ await this._capacitor.initializeAsync();
50
+ }
51
+
40
52
  // routes
41
53
  const npmConf = (await FsUtils.readJsonAsync(
42
54
  path.resolve(this._opt.pkgPath, "package.json"),
@@ -118,6 +130,11 @@ export class SdClientBuildRunner extends SdBuildRunnerBase<"client"> {
118
130
  await this._cordova.buildAsync(path.resolve(this._opt.pkgPath, "dist"));
119
131
  }
120
132
 
133
+ if (!this._opt.watch?.dev && this._capacitor) {
134
+ this._debug("Building Capacitor...");
135
+ await this._capacitor.buildAsync(path.resolve(this._opt.pkgPath, "dist"));
136
+ }
137
+
121
138
  if (!this._opt.watch?.dev && this._pkgConf.builder?.electron) {
122
139
  this._debug("Bulding Electron...");
123
140
  await SdCliElectron.buildAsync({
@@ -90,7 +90,9 @@ export class SdNgBundler {
90
90
  ? PathUtils.norm(this._opt.pkgPath, ".electron/src")
91
91
  : this._conf.builderType === "cordova" && !this._opt.watch?.dev
92
92
  ? PathUtils.norm(this._opt.pkgPath, ".cordova/www")
93
- : PathUtils.norm(this._opt.pkgPath, "dist", this._conf.builderType);
93
+ : this._conf.builderType === "capacitor" && !this._opt.watch?.dev
94
+ ? PathUtils.norm(this._opt.pkgPath, ".capacitor/www")
95
+ : PathUtils.norm(this._opt.pkgPath, "dist", this._conf.builderType);
94
96
  }
95
97
 
96
98
  markForChanges(filePaths: string[]): void {