@simplysm/sd-cli 12.16.12 → 12.16.14
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.
- package/dist/entry/SdCliCapacitor.d.ts +1 -4
- package/dist/entry/SdCliCapacitor.js +59 -76
- package/dist/pkg-builders/client/SdNgBundler.js +1 -1
- package/dist/pkg-builders/commons/SdWorkerPathPlugin.js +0 -1
- package/package.json +5 -5
- package/src/entry/SdCliCapacitor.ts +90 -91
- package/src/pkg-builders/client/SdNgBundler.ts +1 -1
- package/src/pkg-builders/commons/SdWorkerPathPlugin.ts +0 -1
|
@@ -4,7 +4,7 @@ export declare class SdCliCapacitor {
|
|
|
4
4
|
private readonly _CAPACITOR_DIR_NAME;
|
|
5
5
|
private readonly _CONFIG_FILE_NAME;
|
|
6
6
|
private readonly _KEYSTORE_FILE_NAME;
|
|
7
|
-
private readonly
|
|
7
|
+
private readonly _ICON_DIR_NAME;
|
|
8
8
|
private readonly _platforms;
|
|
9
9
|
private readonly _npmConfig;
|
|
10
10
|
constructor(_opt: {
|
|
@@ -15,11 +15,9 @@ export declare class SdCliCapacitor {
|
|
|
15
15
|
private static _execAsync;
|
|
16
16
|
initializeAsync(): Promise<void>;
|
|
17
17
|
private _initializeCapacitorProjectAsync;
|
|
18
|
-
private _syncVersionAsync;
|
|
19
18
|
private _createCapacitorConfigAsync;
|
|
20
19
|
private _managePlatformsAsync;
|
|
21
20
|
private _managePluginsAsync;
|
|
22
|
-
private _isCapacitorPlugin;
|
|
23
21
|
private _setupAndroidSignAsync;
|
|
24
22
|
private _setupIconAndSplashScreenAsync;
|
|
25
23
|
private _createCenteredImageAsync;
|
|
@@ -33,7 +31,6 @@ export declare class SdCliCapacitor {
|
|
|
33
31
|
private _configureAndroidBuildGradleAsync;
|
|
34
32
|
private _configureAndroidStringsAsync;
|
|
35
33
|
buildAsync(outPath: string): Promise<void>;
|
|
36
|
-
private _buildPlatformAsync;
|
|
37
34
|
private _buildAndroidAsync;
|
|
38
35
|
private _copyAndroidBuildOutputAsync;
|
|
39
36
|
private _findAndroidSdk;
|
|
@@ -9,7 +9,7 @@ export class SdCliCapacitor {
|
|
|
9
9
|
this._CAPACITOR_DIR_NAME = ".capacitor";
|
|
10
10
|
this._CONFIG_FILE_NAME = "capacitor.config.ts";
|
|
11
11
|
this._KEYSTORE_FILE_NAME = "android.keystore";
|
|
12
|
-
this.
|
|
12
|
+
this._ICON_DIR_NAME = "resources";
|
|
13
13
|
this._platforms = Object.keys(this._opt.config.platform ?? {});
|
|
14
14
|
this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
|
|
15
15
|
}
|
|
@@ -47,13 +47,23 @@ export class SdCliCapacitor {
|
|
|
47
47
|
}
|
|
48
48
|
// 1. Capacitor 프로젝트 초기화
|
|
49
49
|
async _initializeCapacitorProjectAsync(capacitorPath) {
|
|
50
|
-
|
|
50
|
+
const wwwPath = path.resolve(capacitorPath, "www");
|
|
51
|
+
if (FsUtils.exists(wwwPath)) {
|
|
51
52
|
SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
|
|
52
53
|
// 버전 동기화
|
|
53
|
-
|
|
54
|
+
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
55
|
+
if (FsUtils.exists(pkgJsonPath)) {
|
|
56
|
+
const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
|
|
57
|
+
if (pkgJson.version !== this._npmConfig.version) {
|
|
58
|
+
pkgJson.version = this._npmConfig.version;
|
|
59
|
+
await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
|
|
60
|
+
SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
54
63
|
return false;
|
|
55
64
|
}
|
|
56
|
-
|
|
65
|
+
// www 폴더 생성
|
|
66
|
+
await FsUtils.mkdirsAsync(wwwPath);
|
|
57
67
|
// package.json 생성
|
|
58
68
|
const projNpmConfig = await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "../../package.json"));
|
|
59
69
|
const pkgJson = {
|
|
@@ -64,12 +74,11 @@ export class SdCliCapacitor {
|
|
|
64
74
|
dependencies: {
|
|
65
75
|
"@capacitor/core": "^7.0.0",
|
|
66
76
|
"@capacitor/app": "^7.0.0",
|
|
67
|
-
|
|
77
|
+
...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
|
|
68
78
|
},
|
|
69
79
|
devDependencies: {
|
|
70
80
|
"@capacitor/cli": "^7.0.0",
|
|
71
81
|
"@capacitor/assets": "^3.0.0",
|
|
72
|
-
...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
|
|
73
82
|
},
|
|
74
83
|
};
|
|
75
84
|
await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
|
|
@@ -77,29 +86,17 @@ export class SdCliCapacitor {
|
|
|
77
86
|
});
|
|
78
87
|
// .yarnrc.yml 작성
|
|
79
88
|
await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
|
|
80
|
-
// yarn.lock 작성
|
|
89
|
+
// 빈 yarn.lock 작성
|
|
81
90
|
await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
|
|
82
91
|
// yarn install
|
|
92
|
+
await SdCliCapacitor._execAsync("yarn", ["dlx", "npm-check-updates", "-u", "--target", "semver"], capacitorPath);
|
|
83
93
|
await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
|
|
84
94
|
// capacitor init
|
|
85
95
|
await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
|
|
86
96
|
// www/index.html 생성
|
|
87
|
-
const wwwPath = path.resolve(capacitorPath, "www");
|
|
88
97
|
await FsUtils.writeFileAsync(path.resolve(wwwPath, "index.html"), "<!DOCTYPE html><html><head></head><body></body></html>");
|
|
89
98
|
return true;
|
|
90
99
|
}
|
|
91
|
-
// 버전 동기화
|
|
92
|
-
async _syncVersionAsync(capacitorPath) {
|
|
93
|
-
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
94
|
-
if (FsUtils.exists(pkgJsonPath)) {
|
|
95
|
-
const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
|
|
96
|
-
if (pkgJson.version !== this._npmConfig.version) {
|
|
97
|
-
pkgJson.version = this._npmConfig.version;
|
|
98
|
-
await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
|
|
99
|
-
SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
100
|
// 2. Capacitor 설정 파일 생성
|
|
104
101
|
async _createCapacitorConfigAsync(capacitorPath) {
|
|
105
102
|
const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
|
|
@@ -156,18 +153,24 @@ export class SdCliCapacitor {
|
|
|
156
153
|
const usePlugins = Object.keys(this._opt.config.plugins ?? {});
|
|
157
154
|
const currentDeps = pkgJson.dependencies ?? {};
|
|
158
155
|
let changed = false;
|
|
156
|
+
const prevPlugins = Object.keys(currentDeps).filter(item => ![
|
|
157
|
+
"@capacitor/core",
|
|
158
|
+
"@capacitor/android",
|
|
159
|
+
"@capacitor/ios",
|
|
160
|
+
"@capacitor/app",
|
|
161
|
+
].includes(item));
|
|
159
162
|
// 사용하지 않는 플러그인 제거
|
|
160
|
-
for (const
|
|
161
|
-
if (
|
|
162
|
-
delete currentDeps[
|
|
163
|
+
for (const prevPlugin of prevPlugins) {
|
|
164
|
+
if (!usePlugins.includes(prevPlugin)) {
|
|
165
|
+
delete currentDeps[prevPlugin];
|
|
163
166
|
changed = true;
|
|
164
|
-
SdCliCapacitor._logger.debug(`플러그인 제거: ${
|
|
167
|
+
SdCliCapacitor._logger.debug(`플러그인 제거: ${prevPlugin}`);
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
170
|
// 새 플러그인 추가
|
|
168
171
|
for (const plugin of usePlugins) {
|
|
169
172
|
if (!(plugin in currentDeps)) {
|
|
170
|
-
const version = mainDeps[plugin] ?? "
|
|
173
|
+
const version = mainDeps[plugin] ?? "*";
|
|
171
174
|
currentDeps[plugin] = version;
|
|
172
175
|
changed = true;
|
|
173
176
|
SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
|
|
@@ -183,18 +186,6 @@ export class SdCliCapacitor {
|
|
|
183
186
|
// 변경 없으면 아무것도 안 함 → 오프라인 OK
|
|
184
187
|
return false;
|
|
185
188
|
}
|
|
186
|
-
_isCapacitorPlugin(dep) {
|
|
187
|
-
// 기본 패키지 제외
|
|
188
|
-
const corePackages = [
|
|
189
|
-
"@capacitor/core",
|
|
190
|
-
"@capacitor/android",
|
|
191
|
-
"@capacitor/ios",
|
|
192
|
-
"@capacitor/app",
|
|
193
|
-
];
|
|
194
|
-
if (corePackages.includes(dep))
|
|
195
|
-
return false;
|
|
196
|
-
return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
|
|
197
|
-
}
|
|
198
189
|
// 5. 안드로이드 서명 설정
|
|
199
190
|
async _setupAndroidSignAsync(capacitorPath) {
|
|
200
191
|
const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
|
|
@@ -205,21 +196,14 @@ export class SdCliCapacitor {
|
|
|
205
196
|
await FsUtils.removeAsync(keystorePath);
|
|
206
197
|
}
|
|
207
198
|
}
|
|
208
|
-
// 6. 아이콘 및 스플래시 스크린 설정
|
|
209
199
|
async _setupIconAndSplashScreenAsync(capacitorPath) {
|
|
210
|
-
const resourcesDirPath = path.resolve(capacitorPath, this.
|
|
200
|
+
const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_NAME);
|
|
211
201
|
if (this._opt.config.icon != null) {
|
|
212
202
|
await FsUtils.mkdirsAsync(resourcesDirPath);
|
|
213
203
|
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
214
|
-
//
|
|
215
|
-
// 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
|
|
204
|
+
// 아이콘만 생성
|
|
216
205
|
const logoPath = path.resolve(resourcesDirPath, "logo.png");
|
|
217
206
|
await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
|
|
218
|
-
// splash.png: 2732x2732
|
|
219
|
-
// 로고 크기 약 35% - 화면 중앙에 적당한 크기로
|
|
220
|
-
const splashPath = path.resolve(resourcesDirPath, "splash.png");
|
|
221
|
-
await this._createCenteredImageAsync(iconSource, splashPath, 2732, 0.35);
|
|
222
|
-
// 기존 아이콘 삭제 (겹침 방지)
|
|
223
207
|
await this._cleanupExistingIconsAsync(capacitorPath);
|
|
224
208
|
try {
|
|
225
209
|
await SdCliCapacitor._execAsync("npx", [
|
|
@@ -232,8 +216,8 @@ export class SdCliCapacitor {
|
|
|
232
216
|
"#ffffff",
|
|
233
217
|
], capacitorPath);
|
|
234
218
|
}
|
|
235
|
-
catch {
|
|
236
|
-
SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
|
|
219
|
+
catch (e) {
|
|
220
|
+
SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용", e);
|
|
237
221
|
}
|
|
238
222
|
}
|
|
239
223
|
else {
|
|
@@ -263,13 +247,23 @@ export class SdCliCapacitor {
|
|
|
263
247
|
const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
|
|
264
248
|
if (!FsUtils.exists(androidResPath))
|
|
265
249
|
return;
|
|
250
|
+
// mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
|
|
266
251
|
const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
|
|
267
252
|
for (const dir of mipmapDirs) {
|
|
268
|
-
const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher
|
|
253
|
+
const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
|
|
269
254
|
for (const file of iconFiles) {
|
|
270
255
|
await FsUtils.removeAsync(file);
|
|
271
256
|
}
|
|
272
257
|
}
|
|
258
|
+
// drawable 폴더의 splash/icon 관련 파일도 삭제
|
|
259
|
+
const drawableDirs = await FsUtils.globAsync(path.resolve(androidResPath, "drawable*"));
|
|
260
|
+
for (const dir of drawableDirs) {
|
|
261
|
+
const splashFiles = await FsUtils.globAsync(path.resolve(dir, "splash*"));
|
|
262
|
+
const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*"));
|
|
263
|
+
for (const file of [...splashFiles, ...iconFiles]) {
|
|
264
|
+
await FsUtils.removeAsync(file);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
273
267
|
}
|
|
274
268
|
// 7. Android 네이티브 설정
|
|
275
269
|
async _configureAndroidNativeAsync(capacitorPath) {
|
|
@@ -296,10 +290,15 @@ export class SdCliCapacitor {
|
|
|
296
290
|
return;
|
|
297
291
|
}
|
|
298
292
|
let stylesContent = await FsUtils.readFileAsync(stylesPath);
|
|
299
|
-
// Edge-to-Edge
|
|
293
|
+
// Edge-to-Edge 비활성화만
|
|
300
294
|
if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
|
|
301
|
-
stylesContent = stylesContent.replace(/(<style[^>]*AppTheme[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
|
|
295
|
+
stylesContent = stylesContent.replace(/(<style[^>]*name="AppTheme"[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
|
|
302
296
|
}
|
|
297
|
+
// NoActionBarLaunch를 단순 NoActionBar로
|
|
298
|
+
stylesContent = stylesContent.replace(/(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/, `$1Theme.AppCompat.Light.NoActionBar$2`);
|
|
299
|
+
// splash 관련 전부 제거
|
|
300
|
+
stylesContent = stylesContent.replace(/\s*<item name="android:background">@drawable\/splash<\/item>/g, "");
|
|
301
|
+
stylesContent = stylesContent.replace(/\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g, "");
|
|
303
302
|
await FsUtils.writeFileAsync(stylesPath, stylesContent);
|
|
304
303
|
}
|
|
305
304
|
async _configureAndroidGradlePropertiesAsync(androidPath) {
|
|
@@ -417,7 +416,7 @@ export class SdCliCapacitor {
|
|
|
417
416
|
// Signing 설정
|
|
418
417
|
const signConfig = this._opt.config.platform?.android?.sign;
|
|
419
418
|
if (signConfig) {
|
|
420
|
-
const keystoreRelativePath =
|
|
419
|
+
const keystoreRelativePath = `../../${this._KEYSTORE_FILE_NAME}`;
|
|
421
420
|
const keystoreType = signConfig.keystoreType ?? "jks";
|
|
422
421
|
// signingConfigs 블록 추가
|
|
423
422
|
if (!gradleContent.includes("signingConfigs")) {
|
|
@@ -425,15 +424,15 @@ export class SdCliCapacitor {
|
|
|
425
424
|
signingConfigs {
|
|
426
425
|
release {
|
|
427
426
|
storeFile file("${keystoreRelativePath}")
|
|
428
|
-
storePassword
|
|
429
|
-
keyAlias
|
|
430
|
-
keyPassword
|
|
427
|
+
storePassword '${signConfig.storePassword}'
|
|
428
|
+
keyAlias '${signConfig.alias}'
|
|
429
|
+
keyPassword '${signConfig.password}'
|
|
431
430
|
storeType "${keystoreType}"
|
|
432
431
|
}
|
|
433
432
|
}
|
|
434
433
|
`;
|
|
435
434
|
// android { 블록 내부에 추가
|
|
436
|
-
gradleContent = gradleContent.replace(/(android\s*\{)/, `$
|
|
435
|
+
gradleContent = gradleContent.replace(/(android\s*\{)/, (match) => `${match}${signingConfigsBlock}`);
|
|
437
436
|
}
|
|
438
437
|
// buildTypes.release에 signingConfig 추가
|
|
439
438
|
if (!gradleContent.includes("signingConfig signingConfigs.release")) {
|
|
@@ -461,34 +460,18 @@ export class SdCliCapacitor {
|
|
|
461
460
|
await Promise.all(this._platforms.map(async (platform) => {
|
|
462
461
|
// 해당 플랫폼만 copy
|
|
463
462
|
await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
|
|
464
|
-
|
|
463
|
+
if (platform === "android") {
|
|
464
|
+
await this._buildAndroidAsync(capacitorPath, outPath, buildType);
|
|
465
|
+
}
|
|
465
466
|
}));
|
|
466
467
|
}
|
|
467
|
-
async _buildPlatformAsync(capacitorPath, outPath, platform, buildType) {
|
|
468
|
-
if (platform === "android") {
|
|
469
|
-
await this._buildAndroidAsync(capacitorPath, outPath, buildType);
|
|
470
|
-
}
|
|
471
|
-
// iOS 지원 시 추가
|
|
472
|
-
}
|
|
473
468
|
async _buildAndroidAsync(capacitorPath, outPath, buildType) {
|
|
474
469
|
const androidPath = path.resolve(capacitorPath, "android");
|
|
475
470
|
const targetOutPath = path.resolve(outPath, "android");
|
|
476
|
-
// Gradle wrapper로 빌드
|
|
477
471
|
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
478
472
|
const gradleTask = buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
|
|
479
|
-
// gradlew 실행 권한 부여 (Linux/Mac)
|
|
480
|
-
const gradlewPath = path.resolve(androidPath, "gradlew");
|
|
481
|
-
if (FsUtils.exists(gradlewPath)) {
|
|
482
|
-
try {
|
|
483
|
-
await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
// Windows에서는 무시
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
473
|
// Gradle 빌드 실행
|
|
490
|
-
|
|
491
|
-
await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
|
|
474
|
+
await SdCliCapacitor._execAsync("cmd", ["/c", "gradlew.bat", gradleTask, "--no-daemon"], androidPath);
|
|
492
475
|
// 빌드 결과물 복사
|
|
493
476
|
await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
|
|
494
477
|
}
|
|
@@ -435,7 +435,7 @@ export class SdNgBundler {
|
|
|
435
435
|
...(this._conf.builderType === "electron"
|
|
436
436
|
? []
|
|
437
437
|
: [nodeStdLibBrowserPlugin(nodeStdLibBrowser)]),
|
|
438
|
-
SdWorkerPathPlugin(
|
|
438
|
+
SdWorkerPathPlugin(this._outputPath),
|
|
439
439
|
// {
|
|
440
440
|
// name: "log-circular",
|
|
441
441
|
// setup(build) {
|
|
@@ -24,7 +24,6 @@ export function SdWorkerPathPlugin(outdir) {
|
|
|
24
24
|
return match;
|
|
25
25
|
}
|
|
26
26
|
// 2. 출력될 워커 파일명 결정 (캐싱 및 중복 방지를 위해 해시 사용 권장)
|
|
27
|
-
// 예: dist/workers/protocol.worker-X7A8B9.js
|
|
28
27
|
const fileContent = await FsUtils.readFileBufferAsync(resolvedWorkerPath);
|
|
29
28
|
const hash = HashUtils.get(fileContent).substring(0, 8);
|
|
30
29
|
const workerBaseName = path.basename(resolvedWorkerPath, path.extname(resolvedWorkerPath));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/sd-cli",
|
|
3
|
-
"version": "12.16.
|
|
3
|
+
"version": "12.16.14",
|
|
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.
|
|
21
|
-
"@simplysm/sd-core-node": "12.16.
|
|
22
|
-
"@simplysm/sd-service-server": "12.16.
|
|
23
|
-
"@simplysm/sd-storage": "12.16.
|
|
20
|
+
"@simplysm/sd-core-common": "12.16.14",
|
|
21
|
+
"@simplysm/sd-core-node": "12.16.14",
|
|
22
|
+
"@simplysm/sd-service-server": "12.16.14",
|
|
23
|
+
"@simplysm/sd-storage": "12.16.14",
|
|
24
24
|
"browserslist": "^4.28.1",
|
|
25
25
|
"cordova": "^13.0.0",
|
|
26
26
|
"electron": "^33.4.11",
|
|
@@ -10,7 +10,7 @@ export class SdCliCapacitor {
|
|
|
10
10
|
private readonly _CAPACITOR_DIR_NAME = ".capacitor";
|
|
11
11
|
private readonly _CONFIG_FILE_NAME = "capacitor.config.ts";
|
|
12
12
|
private readonly _KEYSTORE_FILE_NAME = "android.keystore";
|
|
13
|
-
private readonly
|
|
13
|
+
private readonly _ICON_DIR_NAME = "resources";
|
|
14
14
|
|
|
15
15
|
// private readonly _ANDROID_DIR_NAME = "android";
|
|
16
16
|
// private readonly _WWW_DIR_NAME = "www";
|
|
@@ -67,16 +67,28 @@ export class SdCliCapacitor {
|
|
|
67
67
|
|
|
68
68
|
// 1. Capacitor 프로젝트 초기화
|
|
69
69
|
private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<boolean> {
|
|
70
|
-
|
|
70
|
+
const wwwPath = path.resolve(capacitorPath, "www");
|
|
71
|
+
|
|
72
|
+
if (FsUtils.exists(wwwPath)) {
|
|
71
73
|
SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
|
|
72
74
|
|
|
73
75
|
// 버전 동기화
|
|
74
|
-
|
|
76
|
+
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
77
|
+
|
|
78
|
+
if (FsUtils.exists(pkgJsonPath)) {
|
|
79
|
+
const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
|
|
80
|
+
|
|
81
|
+
if (pkgJson.version !== this._npmConfig.version) {
|
|
82
|
+
pkgJson.version = this._npmConfig.version;
|
|
83
|
+
await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
|
|
84
|
+
SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
75
87
|
return false;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
await FsUtils.mkdirsAsync(
|
|
90
|
+
// www 폴더 생성
|
|
91
|
+
await FsUtils.mkdirsAsync(wwwPath);
|
|
80
92
|
|
|
81
93
|
// package.json 생성
|
|
82
94
|
const projNpmConfig = await FsUtils.readJsonAsync(
|
|
@@ -90,16 +102,15 @@ export class SdCliCapacitor {
|
|
|
90
102
|
dependencies: {
|
|
91
103
|
"@capacitor/core": "^7.0.0",
|
|
92
104
|
"@capacitor/app": "^7.0.0",
|
|
93
|
-
"@capacitor/splash-screen": "^7.0.0",
|
|
94
|
-
},
|
|
95
|
-
devDependencies: {
|
|
96
|
-
"@capacitor/cli": "^7.0.0",
|
|
97
|
-
"@capacitor/assets": "^3.0.0",
|
|
98
105
|
...this._platforms.toObject(
|
|
99
106
|
(item) => `@capacitor/${item}`,
|
|
100
107
|
() => "^7.0.0",
|
|
101
108
|
),
|
|
102
109
|
},
|
|
110
|
+
devDependencies: {
|
|
111
|
+
"@capacitor/cli": "^7.0.0",
|
|
112
|
+
"@capacitor/assets": "^3.0.0",
|
|
113
|
+
},
|
|
103
114
|
};
|
|
104
115
|
await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
|
|
105
116
|
space: 2,
|
|
@@ -111,10 +122,15 @@ export class SdCliCapacitor {
|
|
|
111
122
|
"nodeLinker: node-modules",
|
|
112
123
|
);
|
|
113
124
|
|
|
114
|
-
// yarn.lock 작성
|
|
125
|
+
// 빈 yarn.lock 작성
|
|
115
126
|
await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
|
|
116
127
|
|
|
117
128
|
// yarn install
|
|
129
|
+
await SdCliCapacitor._execAsync(
|
|
130
|
+
"yarn",
|
|
131
|
+
["dlx", "npm-check-updates", "-u", "--target", "semver"],
|
|
132
|
+
capacitorPath,
|
|
133
|
+
);
|
|
118
134
|
await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
|
|
119
135
|
|
|
120
136
|
// capacitor init
|
|
@@ -125,7 +141,6 @@ export class SdCliCapacitor {
|
|
|
125
141
|
);
|
|
126
142
|
|
|
127
143
|
// www/index.html 생성
|
|
128
|
-
const wwwPath = path.resolve(capacitorPath, "www");
|
|
129
144
|
await FsUtils.writeFileAsync(
|
|
130
145
|
path.resolve(wwwPath, "index.html"),
|
|
131
146
|
"<!DOCTYPE html><html><head></head><body></body></html>",
|
|
@@ -134,21 +149,6 @@ export class SdCliCapacitor {
|
|
|
134
149
|
return true;
|
|
135
150
|
}
|
|
136
151
|
|
|
137
|
-
// 버전 동기화
|
|
138
|
-
private async _syncVersionAsync(capacitorPath: string) {
|
|
139
|
-
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
140
|
-
|
|
141
|
-
if (FsUtils.exists(pkgJsonPath)) {
|
|
142
|
-
const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
|
|
143
|
-
|
|
144
|
-
if (pkgJson.version !== this._npmConfig.version) {
|
|
145
|
-
pkgJson.version = this._npmConfig.version;
|
|
146
|
-
await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
|
|
147
|
-
SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
152
|
// 2. Capacitor 설정 파일 생성
|
|
153
153
|
private async _createCapacitorConfigAsync(capacitorPath: string) {
|
|
154
154
|
const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
|
|
@@ -216,19 +216,26 @@ export class SdCliCapacitor {
|
|
|
216
216
|
|
|
217
217
|
let changed = false;
|
|
218
218
|
|
|
219
|
+
const prevPlugins = Object.keys(currentDeps).filter(item => ![
|
|
220
|
+
"@capacitor/core",
|
|
221
|
+
"@capacitor/android",
|
|
222
|
+
"@capacitor/ios",
|
|
223
|
+
"@capacitor/app",
|
|
224
|
+
].includes(item));
|
|
225
|
+
|
|
219
226
|
// 사용하지 않는 플러그인 제거
|
|
220
|
-
for (const
|
|
221
|
-
if (
|
|
222
|
-
delete currentDeps[
|
|
227
|
+
for (const prevPlugin of prevPlugins) {
|
|
228
|
+
if (!usePlugins.includes(prevPlugin)) {
|
|
229
|
+
delete currentDeps[prevPlugin];
|
|
223
230
|
changed = true;
|
|
224
|
-
SdCliCapacitor._logger.debug(`플러그인 제거: ${
|
|
231
|
+
SdCliCapacitor._logger.debug(`플러그인 제거: ${prevPlugin}`);
|
|
225
232
|
}
|
|
226
233
|
}
|
|
227
234
|
|
|
228
235
|
// 새 플러그인 추가
|
|
229
236
|
for (const plugin of usePlugins) {
|
|
230
237
|
if (!(plugin in currentDeps)) {
|
|
231
|
-
const version = mainDeps[plugin] ?? "
|
|
238
|
+
const version = mainDeps[plugin] ?? "*";
|
|
232
239
|
currentDeps[plugin] = version;
|
|
233
240
|
changed = true;
|
|
234
241
|
SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
|
|
@@ -246,19 +253,6 @@ export class SdCliCapacitor {
|
|
|
246
253
|
return false;
|
|
247
254
|
}
|
|
248
255
|
|
|
249
|
-
private _isCapacitorPlugin(dep: string): boolean {
|
|
250
|
-
// 기본 패키지 제외
|
|
251
|
-
const corePackages = [
|
|
252
|
-
"@capacitor/core",
|
|
253
|
-
"@capacitor/android",
|
|
254
|
-
"@capacitor/ios",
|
|
255
|
-
"@capacitor/app",
|
|
256
|
-
];
|
|
257
|
-
if (corePackages.includes(dep)) return false;
|
|
258
|
-
|
|
259
|
-
return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
|
|
260
|
-
}
|
|
261
|
-
|
|
262
256
|
// 5. 안드로이드 서명 설정
|
|
263
257
|
private async _setupAndroidSignAsync(capacitorPath: string) {
|
|
264
258
|
const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
|
|
@@ -273,26 +267,18 @@ export class SdCliCapacitor {
|
|
|
273
267
|
}
|
|
274
268
|
}
|
|
275
269
|
|
|
276
|
-
// 6. 아이콘 및 스플래시 스크린 설정
|
|
277
270
|
private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
|
|
278
|
-
const resourcesDirPath = path.resolve(capacitorPath, this.
|
|
271
|
+
const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_NAME);
|
|
279
272
|
|
|
280
273
|
if (this._opt.config.icon != null) {
|
|
281
274
|
await FsUtils.mkdirsAsync(resourcesDirPath);
|
|
282
275
|
|
|
283
276
|
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
284
277
|
|
|
285
|
-
//
|
|
286
|
-
// 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
|
|
278
|
+
// 아이콘만 생성
|
|
287
279
|
const logoPath = path.resolve(resourcesDirPath, "logo.png");
|
|
288
280
|
await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
|
|
289
281
|
|
|
290
|
-
// splash.png: 2732x2732
|
|
291
|
-
// 로고 크기 약 35% - 화면 중앙에 적당한 크기로
|
|
292
|
-
const splashPath = path.resolve(resourcesDirPath, "splash.png");
|
|
293
|
-
await this._createCenteredImageAsync(iconSource, splashPath, 2732, 0.35);
|
|
294
|
-
|
|
295
|
-
// 기존 아이콘 삭제 (겹침 방지)
|
|
296
282
|
await this._cleanupExistingIconsAsync(capacitorPath);
|
|
297
283
|
|
|
298
284
|
try {
|
|
@@ -309,8 +295,8 @@ export class SdCliCapacitor {
|
|
|
309
295
|
],
|
|
310
296
|
capacitorPath,
|
|
311
297
|
);
|
|
312
|
-
} catch {
|
|
313
|
-
SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
|
|
298
|
+
} catch (e) {
|
|
299
|
+
SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용", e);
|
|
314
300
|
}
|
|
315
301
|
} else {
|
|
316
302
|
await FsUtils.removeAsync(resourcesDirPath);
|
|
@@ -348,13 +334,24 @@ export class SdCliCapacitor {
|
|
|
348
334
|
|
|
349
335
|
if (!FsUtils.exists(androidResPath)) return;
|
|
350
336
|
|
|
337
|
+
// mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
|
|
351
338
|
const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
|
|
352
339
|
for (const dir of mipmapDirs) {
|
|
353
|
-
const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher
|
|
340
|
+
const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
|
|
354
341
|
for (const file of iconFiles) {
|
|
355
342
|
await FsUtils.removeAsync(file);
|
|
356
343
|
}
|
|
357
344
|
}
|
|
345
|
+
|
|
346
|
+
// drawable 폴더의 splash/icon 관련 파일도 삭제
|
|
347
|
+
const drawableDirs = await FsUtils.globAsync(path.resolve(androidResPath, "drawable*"));
|
|
348
|
+
for (const dir of drawableDirs) {
|
|
349
|
+
const splashFiles = await FsUtils.globAsync(path.resolve(dir, "splash*"));
|
|
350
|
+
const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*"));
|
|
351
|
+
for (const file of [...splashFiles, ...iconFiles]) {
|
|
352
|
+
await FsUtils.removeAsync(file);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
358
355
|
}
|
|
359
356
|
|
|
360
357
|
// 7. Android 네이티브 설정
|
|
@@ -393,14 +390,30 @@ export class SdCliCapacitor {
|
|
|
393
390
|
|
|
394
391
|
let stylesContent = await FsUtils.readFileAsync(stylesPath);
|
|
395
392
|
|
|
396
|
-
// Edge-to-Edge
|
|
393
|
+
// Edge-to-Edge 비활성화만
|
|
397
394
|
if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
|
|
398
395
|
stylesContent = stylesContent.replace(
|
|
399
|
-
/(<style[^>]*AppTheme[^>]*>)/,
|
|
396
|
+
/(<style[^>]*name="AppTheme"[^>]*>)/,
|
|
400
397
|
`$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
|
|
401
398
|
);
|
|
402
399
|
}
|
|
403
400
|
|
|
401
|
+
// NoActionBarLaunch를 단순 NoActionBar로
|
|
402
|
+
stylesContent = stylesContent.replace(
|
|
403
|
+
/(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/,
|
|
404
|
+
`$1Theme.AppCompat.Light.NoActionBar$2`,
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// splash 관련 전부 제거
|
|
408
|
+
stylesContent = stylesContent.replace(
|
|
409
|
+
/\s*<item name="android:background">@drawable\/splash<\/item>/g,
|
|
410
|
+
"",
|
|
411
|
+
);
|
|
412
|
+
stylesContent = stylesContent.replace(
|
|
413
|
+
/\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g,
|
|
414
|
+
"",
|
|
415
|
+
);
|
|
416
|
+
|
|
404
417
|
await FsUtils.writeFileAsync(stylesPath, stylesContent);
|
|
405
418
|
}
|
|
406
419
|
|
|
@@ -561,7 +574,7 @@ export class SdCliCapacitor {
|
|
|
561
574
|
// Signing 설정
|
|
562
575
|
const signConfig = this._opt.config.platform?.android?.sign;
|
|
563
576
|
if (signConfig) {
|
|
564
|
-
const keystoreRelativePath =
|
|
577
|
+
const keystoreRelativePath = `../../${this._KEYSTORE_FILE_NAME}`;
|
|
565
578
|
const keystoreType = signConfig.keystoreType ?? "jks";
|
|
566
579
|
|
|
567
580
|
// signingConfigs 블록 추가
|
|
@@ -570,15 +583,18 @@ export class SdCliCapacitor {
|
|
|
570
583
|
signingConfigs {
|
|
571
584
|
release {
|
|
572
585
|
storeFile file("${keystoreRelativePath}")
|
|
573
|
-
storePassword
|
|
574
|
-
keyAlias
|
|
575
|
-
keyPassword
|
|
586
|
+
storePassword '${signConfig.storePassword}'
|
|
587
|
+
keyAlias '${signConfig.alias}'
|
|
588
|
+
keyPassword '${signConfig.password}'
|
|
576
589
|
storeType "${keystoreType}"
|
|
577
590
|
}
|
|
578
591
|
}
|
|
579
592
|
`;
|
|
580
593
|
// android { 블록 내부에 추가
|
|
581
|
-
gradleContent = gradleContent.replace(
|
|
594
|
+
gradleContent = gradleContent.replace(
|
|
595
|
+
/(android\s*\{)/,
|
|
596
|
+
(match) => `${match}${signingConfigsBlock}`,
|
|
597
|
+
);
|
|
582
598
|
}
|
|
583
599
|
|
|
584
600
|
// buildTypes.release에 signingConfig 추가
|
|
@@ -630,23 +646,14 @@ export class SdCliCapacitor {
|
|
|
630
646
|
this._platforms.map(async (platform) => {
|
|
631
647
|
// 해당 플랫폼만 copy
|
|
632
648
|
await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
|
|
633
|
-
|
|
649
|
+
|
|
650
|
+
if (platform === "android") {
|
|
651
|
+
await this._buildAndroidAsync(capacitorPath, outPath, buildType);
|
|
652
|
+
}
|
|
634
653
|
}),
|
|
635
654
|
);
|
|
636
655
|
}
|
|
637
656
|
|
|
638
|
-
private async _buildPlatformAsync(
|
|
639
|
-
capacitorPath: string,
|
|
640
|
-
outPath: string,
|
|
641
|
-
platform: string,
|
|
642
|
-
buildType: string,
|
|
643
|
-
): Promise<void> {
|
|
644
|
-
if (platform === "android") {
|
|
645
|
-
await this._buildAndroidAsync(capacitorPath, outPath, buildType);
|
|
646
|
-
}
|
|
647
|
-
// iOS 지원 시 추가
|
|
648
|
-
}
|
|
649
|
-
|
|
650
657
|
private async _buildAndroidAsync(
|
|
651
658
|
capacitorPath: string,
|
|
652
659
|
outPath: string,
|
|
@@ -655,24 +662,16 @@ export class SdCliCapacitor {
|
|
|
655
662
|
const androidPath = path.resolve(capacitorPath, "android");
|
|
656
663
|
const targetOutPath = path.resolve(outPath, "android");
|
|
657
664
|
|
|
658
|
-
// Gradle wrapper로 빌드
|
|
659
665
|
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
660
666
|
const gradleTask =
|
|
661
667
|
buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
|
|
662
668
|
|
|
663
|
-
// gradlew 실행 권한 부여 (Linux/Mac)
|
|
664
|
-
const gradlewPath = path.resolve(androidPath, "gradlew");
|
|
665
|
-
if (FsUtils.exists(gradlewPath)) {
|
|
666
|
-
try {
|
|
667
|
-
await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
|
|
668
|
-
} catch {
|
|
669
|
-
// Windows에서는 무시
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
669
|
// Gradle 빌드 실행
|
|
674
|
-
|
|
675
|
-
|
|
670
|
+
await SdCliCapacitor._execAsync(
|
|
671
|
+
"cmd",
|
|
672
|
+
["/c", "gradlew.bat", gradleTask, "--no-daemon"],
|
|
673
|
+
androidPath,
|
|
674
|
+
);
|
|
676
675
|
|
|
677
676
|
// 빌드 결과물 복사
|
|
678
677
|
await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
|
|
@@ -560,7 +560,7 @@ export class SdNgBundler {
|
|
|
560
560
|
...(this._conf.builderType === "electron"
|
|
561
561
|
? []
|
|
562
562
|
: [nodeStdLibBrowserPlugin(nodeStdLibBrowser)]),
|
|
563
|
-
SdWorkerPathPlugin(
|
|
563
|
+
SdWorkerPathPlugin(this._outputPath),
|
|
564
564
|
// {
|
|
565
565
|
// name: "log-circular",
|
|
566
566
|
// setup(build) {
|
|
@@ -33,7 +33,6 @@ export function SdWorkerPathPlugin(outdir: string): esbuild.Plugin {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// 2. 출력될 워커 파일명 결정 (캐싱 및 중복 방지를 위해 해시 사용 권장)
|
|
36
|
-
// 예: dist/workers/protocol.worker-X7A8B9.js
|
|
37
36
|
const fileContent = await FsUtils.readFileBufferAsync(resolvedWorkerPath);
|
|
38
37
|
const hash = HashUtils.get(fileContent).substring(0, 8);
|
|
39
38
|
const workerBaseName = path.basename(
|