@simplysm/sd-cli 12.15.67 → 12.15.69
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/SdCliAiCommand.js +13 -1
- package/dist/entry/SdCliCapacitor.d.ts +37 -0
- package/dist/entry/SdCliCapacitor.js +390 -0
- package/dist/pkg-builders/client/SdClientBuildRunner.d.ts +1 -0
- package/dist/pkg-builders/client/SdClientBuildRunner.js +14 -0
- package/dist/pkg-builders/client/SdNgBundler.js +3 -1
- package/dist/sd-cli-entry.js +18 -44
- package/dist/types/config/ISdProjectConfig.d.ts +31 -0
- package/package.json +5 -5
- package/src/entry/SdCliAiCommand.ts +14 -1
- package/src/entry/SdCliCapacitor.ts +560 -0
- package/src/pkg-builders/client/SdClientBuildRunner.ts +17 -0
- package/src/pkg-builders/client/SdNgBundler.ts +3 -1
- package/src/sd-cli-entry.ts +26 -56
- package/src/types/config/ISdProjectConfig.ts +34 -0
- package/dist/fix/convertPrivateToHash.d.ts +0 -1
- package/dist/fix/convertPrivateToHash.js +0 -58
- package/dist/fix/convertSdAngularSymbolNames.d.ts +0 -1
- package/dist/fix/convertSdAngularSymbolNames.js +0 -22
- package/dist/fix/core/convertSymbols.d.ts +0 -1
- package/dist/fix/core/convertSymbols.js +0 -101
- package/dist/fix/core/getTsMortphSourceFiles.d.ts +0 -1
- package/dist/fix/core/getTsMortphSourceFiles.js +0 -7
- package/dist/fix/core/removeSymbols.d.ts +0 -1
- package/dist/fix/core/removeSymbols.js +0 -76
- package/dist/fix/removeSdAngularSymbolNames.d.ts +0 -1
- package/dist/fix/removeSdAngularSymbolNames.js +0 -6
- package/dist/fix/removeUnusedImports.d.ts +0 -1
- package/dist/fix/removeUnusedImports.js +0 -41
- package/dist/fix/removeUnusedInjects.d.ts +0 -1
- package/dist/fix/removeUnusedInjects.js +0 -37
- package/dist/fix/removeUnusedProtectedReadonly.d.ts +0 -1
- package/dist/fix/removeUnusedProtectedReadonly.js +0 -57
- package/src/fix/convertPrivateToHash.ts +0 -74
- package/src/fix/convertSdAngularSymbolNames.ts +0 -27
- package/src/fix/core/convertSymbols.ts +0 -135
- package/src/fix/core/getTsMortphSourceFiles.ts +0 -9
- package/src/fix/core/removeSymbols.ts +0 -102
- package/src/fix/removeSdAngularSymbolNames.ts +0 -9
- package/src/fix/removeUnusedImports.ts +0 -50
- package/src/fix/removeUnusedInjects.ts +0 -50
- package/src/fix/removeUnusedProtectedReadonly.ts +0 -69
|
@@ -18,6 +18,7 @@ export class SdCliAiCommand {
|
|
|
18
18
|
"--find-renames",
|
|
19
19
|
"--find-copies",
|
|
20
20
|
"--diff-algorithm=histogram",
|
|
21
|
+
"--diff-filter=d",
|
|
21
22
|
"--",
|
|
22
23
|
".",
|
|
23
24
|
`:(exclude).*`,
|
|
@@ -27,6 +28,13 @@ export class SdCliAiCommand {
|
|
|
27
28
|
`:(exclude)packages/*/styles.css`,
|
|
28
29
|
`:(exclude)*.map`,
|
|
29
30
|
]);
|
|
31
|
+
// 삭제된 파일 목록만 따로
|
|
32
|
+
const deleted = await SdProcess.spawnAsync("git", [
|
|
33
|
+
"diff",
|
|
34
|
+
"--staged",
|
|
35
|
+
"--name-only",
|
|
36
|
+
"--diff-filter=D",
|
|
37
|
+
]);
|
|
30
38
|
if (StringUtils.isNullOrEmpty(diff.trim())) {
|
|
31
39
|
throw new Error("변경사항이 없습니다.");
|
|
32
40
|
}
|
|
@@ -63,7 +71,11 @@ ${stat}
|
|
|
63
71
|
|
|
64
72
|
<diff>
|
|
65
73
|
${diff}
|
|
66
|
-
</diff
|
|
74
|
+
</diff>
|
|
75
|
+
|
|
76
|
+
<deleted_files>
|
|
77
|
+
${deleted.trim() || "없음"}
|
|
78
|
+
</deleted_files>`,
|
|
67
79
|
},
|
|
68
80
|
],
|
|
69
81
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ISdClientBuilderCapacitorConfig } from "../types/config/ISdProjectConfig";
|
|
2
|
+
export declare class SdCliCapacitor {
|
|
3
|
+
private readonly _opt;
|
|
4
|
+
private readonly _CAPACITOR_DIR_NAME;
|
|
5
|
+
private readonly _CONFIG_FILE_NAME;
|
|
6
|
+
private readonly _KEYSTORE_FILE_NAME;
|
|
7
|
+
private readonly _ICON_DIR_PATH;
|
|
8
|
+
private readonly _platforms;
|
|
9
|
+
private readonly _npmConfig;
|
|
10
|
+
constructor(_opt: {
|
|
11
|
+
pkgPath: string;
|
|
12
|
+
config: ISdClientBuilderCapacitorConfig;
|
|
13
|
+
});
|
|
14
|
+
private static readonly _logger;
|
|
15
|
+
private static _execAsync;
|
|
16
|
+
initializeAsync(): Promise<void>;
|
|
17
|
+
private _initializeCapacitorProjectAsync;
|
|
18
|
+
private _syncVersion;
|
|
19
|
+
private _createCapacitorConfig;
|
|
20
|
+
private _managePlatformsAsync;
|
|
21
|
+
private _managePluginsAsync;
|
|
22
|
+
private _setupAndroidSign;
|
|
23
|
+
private _setupIconAndSplashScreenAsync;
|
|
24
|
+
private _configureAndroidNative;
|
|
25
|
+
private _configureAndroidManifest;
|
|
26
|
+
private _configureAndroidBuildGradle;
|
|
27
|
+
private _configureAndroidStrings;
|
|
28
|
+
buildAsync(outPath: string): Promise<void>;
|
|
29
|
+
private _buildPlatformAsync;
|
|
30
|
+
private _buildAndroidAsync;
|
|
31
|
+
private _copyAndroidBuildOutput;
|
|
32
|
+
static runWebviewOnDeviceAsync(opt: {
|
|
33
|
+
platform: string;
|
|
34
|
+
package: string;
|
|
35
|
+
url?: string;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { FsUtils, PathUtils, SdLogger, SdProcess } from "@simplysm/sd-core-node";
|
|
3
|
+
import { StringUtils, typescript } from "@simplysm/sd-core-common";
|
|
4
|
+
export class SdCliCapacitor {
|
|
5
|
+
constructor(_opt) {
|
|
6
|
+
this._opt = _opt;
|
|
7
|
+
// 상수 정의
|
|
8
|
+
this._CAPACITOR_DIR_NAME = ".capacitor";
|
|
9
|
+
this._CONFIG_FILE_NAME = "capacitor.config.ts";
|
|
10
|
+
this._KEYSTORE_FILE_NAME = "android.keystore";
|
|
11
|
+
this._ICON_DIR_PATH = "resources";
|
|
12
|
+
this._platforms = Object.keys(this._opt.config.platform ?? { android: {} });
|
|
13
|
+
this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
|
|
14
|
+
}
|
|
15
|
+
static { this._logger = SdLogger.get(["simplysm", "sd-cli", "SdCliCapacitor"]); }
|
|
16
|
+
static async _execAsync(cmd, args, cwd) {
|
|
17
|
+
this._logger.debug(`실행 명령: ${cmd + " " + args.join(" ")}`);
|
|
18
|
+
const msg = await SdProcess.spawnAsync(cmd, args, { cwd });
|
|
19
|
+
this._logger.debug(`실행 결과: ${msg}`);
|
|
20
|
+
}
|
|
21
|
+
async initializeAsync() {
|
|
22
|
+
const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
|
|
23
|
+
// 1. Capacitor 프로젝트 초기화
|
|
24
|
+
await this._initializeCapacitorProjectAsync(capacitorPath);
|
|
25
|
+
// 2. Capacitor 설정 파일 생성
|
|
26
|
+
this._createCapacitorConfig(capacitorPath);
|
|
27
|
+
// 3. 플랫폼 관리
|
|
28
|
+
await this._managePlatformsAsync(capacitorPath);
|
|
29
|
+
// 4. 플러그인 관리
|
|
30
|
+
await this._managePluginsAsync(capacitorPath);
|
|
31
|
+
// 5. 안드로이드 서명 설정
|
|
32
|
+
this._setupAndroidSign(capacitorPath);
|
|
33
|
+
// 6. 아이콘 및 스플래시 스크린 설정
|
|
34
|
+
await this._setupIconAndSplashScreenAsync(capacitorPath);
|
|
35
|
+
// 7. Android 네이티브 설정 (AndroidManifest.xml, build.gradle 등)
|
|
36
|
+
if (this._platforms.includes("android")) {
|
|
37
|
+
this._configureAndroidNative(capacitorPath);
|
|
38
|
+
}
|
|
39
|
+
// 8. 웹 자산 동기화
|
|
40
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
|
|
41
|
+
}
|
|
42
|
+
// 1. Capacitor 프로젝트 초기화
|
|
43
|
+
async _initializeCapacitorProjectAsync(capacitorPath) {
|
|
44
|
+
if (FsUtils.exists(capacitorPath)) {
|
|
45
|
+
SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
|
|
46
|
+
// 버전 동기화
|
|
47
|
+
this._syncVersion(capacitorPath);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
FsUtils.mkdirs(capacitorPath);
|
|
51
|
+
// package.json 생성
|
|
52
|
+
const pkgJson = {
|
|
53
|
+
name: this._opt.config.appId,
|
|
54
|
+
version: this._npmConfig.version,
|
|
55
|
+
private: true,
|
|
56
|
+
dependencies: {
|
|
57
|
+
"@capacitor/core": "^7.0.0",
|
|
58
|
+
},
|
|
59
|
+
devDependencies: {
|
|
60
|
+
"@capacitor/cli": "^7.0.0",
|
|
61
|
+
"@capacitor/assets": "^3.0.0",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
FsUtils.writeJson(path.resolve(capacitorPath, "package.json"), pkgJson, { space: 2 });
|
|
65
|
+
// yarn install
|
|
66
|
+
await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
|
|
67
|
+
// capacitor init
|
|
68
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 버전 동기화
|
|
72
|
+
_syncVersion(capacitorPath) {
|
|
73
|
+
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
74
|
+
if (FsUtils.exists(pkgJsonPath)) {
|
|
75
|
+
const pkgJson = FsUtils.readJson(pkgJsonPath);
|
|
76
|
+
if (pkgJson.version !== this._npmConfig.version) {
|
|
77
|
+
pkgJson.version = this._npmConfig.version;
|
|
78
|
+
FsUtils.writeJson(pkgJsonPath, pkgJson, { space: 2 });
|
|
79
|
+
SdCliCapacitor._logger.log(`버전 동기화: ${this._npmConfig.version}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 2. Capacitor 설정 파일 생성
|
|
84
|
+
_createCapacitorConfig(capacitorPath) {
|
|
85
|
+
const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
|
|
86
|
+
// 플러그인 옵션 생성
|
|
87
|
+
const pluginOptions = {};
|
|
88
|
+
for (const [pluginName, options] of Object.entries(this._opt.config.plugins ?? {})) {
|
|
89
|
+
if (options !== true) {
|
|
90
|
+
// @capacitor/splash-screen → SplashScreen 형태로 변환
|
|
91
|
+
const configKey = StringUtils.toPascalCase(pluginName.split("/").last());
|
|
92
|
+
pluginOptions[configKey] = options;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const pluginsConfigStr = Object.keys(pluginOptions).length > 0
|
|
96
|
+
? JSON.stringify(pluginOptions, null, 4).replace(/^/gm, " ").trim()
|
|
97
|
+
: "{}";
|
|
98
|
+
const configContent = typescript `
|
|
99
|
+
import type { CapacitorConfig } from "@capacitor/cli";
|
|
100
|
+
|
|
101
|
+
const config: CapacitorConfig = {
|
|
102
|
+
appId: "${this._opt.config.appId}",
|
|
103
|
+
appName: "${this._opt.config.appName}",
|
|
104
|
+
server: {
|
|
105
|
+
androidScheme: "http",
|
|
106
|
+
cleartext: true,
|
|
107
|
+
allowNavigation: ["*"],
|
|
108
|
+
},
|
|
109
|
+
android: {
|
|
110
|
+
allowMixedContent: true,
|
|
111
|
+
},
|
|
112
|
+
plugins: ${pluginsConfigStr},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default config;
|
|
116
|
+
`;
|
|
117
|
+
FsUtils.writeFile(configFilePath, configContent);
|
|
118
|
+
}
|
|
119
|
+
// 3. 플랫폼 관리
|
|
120
|
+
async _managePlatformsAsync(capacitorPath) {
|
|
121
|
+
for (const platform of this._platforms) {
|
|
122
|
+
if (FsUtils.exists(path.resolve(capacitorPath, platform)))
|
|
123
|
+
continue;
|
|
124
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], capacitorPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 4. 플러그인 관리
|
|
128
|
+
async _managePluginsAsync(capacitorPath) {
|
|
129
|
+
const pkgJsonPath = path.resolve(capacitorPath, "package.json");
|
|
130
|
+
const pkgJson = FsUtils.readJson(pkgJsonPath);
|
|
131
|
+
const currentDeps = Object.keys(pkgJson.dependencies ?? {});
|
|
132
|
+
const usePlugins = Object.keys(this._opt.config.plugins ?? {});
|
|
133
|
+
// 사용하지 않는 플러그인 제거
|
|
134
|
+
for (const dep of currentDeps) {
|
|
135
|
+
// @capacitor/core, @capacitor/android 등 기본 패키지는 제외
|
|
136
|
+
if (dep.startsWith("@capacitor/") &&
|
|
137
|
+
["core", "android", "ios"].some((p) => dep.endsWith(p))) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// 플러그인 목록에 없는 패키지는 제거
|
|
141
|
+
if (!usePlugins.includes(dep)) {
|
|
142
|
+
// Capacitor 관련 플러그인만 제거
|
|
143
|
+
if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
|
|
144
|
+
try {
|
|
145
|
+
await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
|
|
146
|
+
SdCliCapacitor._logger.log(`플러그인 제거: ${dep}`);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
SdCliCapacitor._logger.warn(`플러그인 제거 실패: ${dep}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 새 플러그인 설치
|
|
155
|
+
for (const plugin of usePlugins) {
|
|
156
|
+
if (!currentDeps.includes(plugin)) {
|
|
157
|
+
try {
|
|
158
|
+
await SdCliCapacitor._execAsync("yarn", ["add", plugin], capacitorPath);
|
|
159
|
+
SdCliCapacitor._logger.log(`플러그인 설치: ${plugin}`);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
SdCliCapacitor._logger.warn(`플러그인 설치 실패: ${plugin}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// 5. 안드로이드 서명 설정
|
|
168
|
+
_setupAndroidSign(capacitorPath) {
|
|
169
|
+
const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
|
|
170
|
+
if (this._opt.config.platform?.android?.sign) {
|
|
171
|
+
FsUtils.copy(path.resolve(this._opt.pkgPath, this._opt.config.platform.android.sign.keystore), keystorePath);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
FsUtils.remove(keystorePath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// 6. 아이콘 및 스플래시 스크린 설정
|
|
178
|
+
async _setupIconAndSplashScreenAsync(capacitorPath) {
|
|
179
|
+
const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
|
|
180
|
+
// ICON 파일 복사
|
|
181
|
+
if (this._opt.config.icon != null) {
|
|
182
|
+
FsUtils.mkdirs(iconDirPath);
|
|
183
|
+
const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
|
|
184
|
+
// icon.png, splash.png 둘 다 같은 파일 사용
|
|
185
|
+
FsUtils.copy(iconSource, path.resolve(iconDirPath, "icon.png"));
|
|
186
|
+
FsUtils.copy(iconSource, path.resolve(iconDirPath, "splash.png"));
|
|
187
|
+
// @capacitor/assets로 아이콘/스플래시 리사이징
|
|
188
|
+
try {
|
|
189
|
+
await SdCliCapacitor._execAsync("npx", ["@capacitor/assets", "generate", "--android"], capacitorPath);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
FsUtils.remove(iconDirPath);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// 7. Android 네이티브 설정
|
|
200
|
+
_configureAndroidNative(capacitorPath) {
|
|
201
|
+
const androidPath = path.resolve(capacitorPath, "android");
|
|
202
|
+
if (!FsUtils.exists(androidPath)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// AndroidManifest.xml 수정
|
|
206
|
+
this._configureAndroidManifest(androidPath);
|
|
207
|
+
// build.gradle 수정 (필요시)
|
|
208
|
+
this._configureAndroidBuildGradle(androidPath);
|
|
209
|
+
// strings.xml 앱 이름 수정
|
|
210
|
+
this._configureAndroidStrings(androidPath);
|
|
211
|
+
}
|
|
212
|
+
_configureAndroidManifest(androidPath) {
|
|
213
|
+
const manifestPath = path.resolve(androidPath, "app/src/main/AndroidManifest.xml");
|
|
214
|
+
if (!FsUtils.exists(manifestPath)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let manifestContent = FsUtils.readFile(manifestPath);
|
|
218
|
+
// usesCleartextTraffic 설정
|
|
219
|
+
if (!manifestContent.includes("android:usesCleartextTraffic")) {
|
|
220
|
+
manifestContent = manifestContent.replace("<application", '<application android:usesCleartextTraffic="true"');
|
|
221
|
+
}
|
|
222
|
+
// 추가 권한 설정
|
|
223
|
+
const permissions = this._opt.config.platform?.android?.permissions ?? [];
|
|
224
|
+
for (const perm of permissions) {
|
|
225
|
+
const permTag = `<uses-permission android:name="android.permission.${perm.name}"`;
|
|
226
|
+
if (!manifestContent.includes(permTag)) {
|
|
227
|
+
const maxSdkAttr = perm.maxSdkVersion != null ? ` android:maxSdkVersion="${perm.maxSdkVersion}"` : "";
|
|
228
|
+
const ignoreAttr = perm.ignore != null ? ` tools:ignore="${perm.ignore}"` : "";
|
|
229
|
+
const permLine = ` ${permTag}${maxSdkAttr}${ignoreAttr} />\n`;
|
|
230
|
+
// tools 네임스페이스 추가
|
|
231
|
+
if (perm.ignore != null && !manifestContent.includes("xmlns:tools=")) {
|
|
232
|
+
manifestContent = manifestContent.replace("<manifest xmlns:android", '<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android');
|
|
233
|
+
}
|
|
234
|
+
manifestContent = manifestContent.replace("</manifest>", `${permLine}</manifest>`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 추가 application 설정
|
|
238
|
+
const appConfig = this._opt.config.platform?.android?.config;
|
|
239
|
+
if (appConfig) {
|
|
240
|
+
for (const [key, value] of Object.entries(appConfig)) {
|
|
241
|
+
const attr = `android:${key}="${value}"`;
|
|
242
|
+
if (!manifestContent.includes(`android:${key}=`)) {
|
|
243
|
+
manifestContent = manifestContent.replace("<application", `<application ${attr}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
FsUtils.writeFile(manifestPath, manifestContent);
|
|
248
|
+
}
|
|
249
|
+
_configureAndroidBuildGradle(androidPath) {
|
|
250
|
+
const buildGradlePath = path.resolve(androidPath, "app/build.gradle");
|
|
251
|
+
if (!FsUtils.exists(buildGradlePath)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let gradleContent = FsUtils.readFile(buildGradlePath);
|
|
255
|
+
// versionName, versionCode 설정
|
|
256
|
+
const version = this._npmConfig.version;
|
|
257
|
+
const versionParts = version.split(".");
|
|
258
|
+
const versionCode = parseInt(versionParts[0] ?? "0") * 10000 +
|
|
259
|
+
parseInt(versionParts[1] ?? "0") * 100 +
|
|
260
|
+
parseInt(versionParts[2] ?? "0");
|
|
261
|
+
gradleContent = gradleContent.replace(/versionCode \d+/, `versionCode ${versionCode}`);
|
|
262
|
+
gradleContent = gradleContent.replace(/versionName "[^"]+"/, `versionName "${version}"`);
|
|
263
|
+
// SDK 버전 설정
|
|
264
|
+
if (this._opt.config.platform?.android?.sdkVersion != null) {
|
|
265
|
+
const sdkVersion = this._opt.config.platform.android.sdkVersion;
|
|
266
|
+
gradleContent = gradleContent.replace(/minSdkVersion \d+/, `minSdkVersion ${sdkVersion}`);
|
|
267
|
+
gradleContent = gradleContent.replace(/targetSdkVersion \d+/, `targetSdkVersion ${sdkVersion}`);
|
|
268
|
+
}
|
|
269
|
+
// Signing 설정
|
|
270
|
+
const signConfig = this._opt.config.platform?.android?.sign;
|
|
271
|
+
if (signConfig) {
|
|
272
|
+
const keystoreRelativePath = `../${this._KEYSTORE_FILE_NAME}`;
|
|
273
|
+
const keystoreType = signConfig.keystoreType ?? "jks";
|
|
274
|
+
// signingConfigs 블록 추가
|
|
275
|
+
if (!gradleContent.includes("signingConfigs")) {
|
|
276
|
+
const signingConfigsBlock = `
|
|
277
|
+
signingConfigs {
|
|
278
|
+
release {
|
|
279
|
+
storeFile file("${keystoreRelativePath}")
|
|
280
|
+
storePassword "${signConfig.storePassword}"
|
|
281
|
+
keyAlias "${signConfig.alias}"
|
|
282
|
+
keyPassword "${signConfig.password}"
|
|
283
|
+
storeType "${keystoreType}"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
// android { 블록 내부에 추가
|
|
288
|
+
gradleContent = gradleContent.replace(/(android\s*\{)/, `$1${signingConfigsBlock}`);
|
|
289
|
+
}
|
|
290
|
+
// buildTypes.release에 signingConfig 추가
|
|
291
|
+
if (!gradleContent.includes("signingConfig signingConfigs.release")) {
|
|
292
|
+
gradleContent = gradleContent.replace(/(buildTypes\s*\{[\s\S]*?release\s*\{)/, `$1\n signingConfig signingConfigs.release`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
FsUtils.writeFile(buildGradlePath, gradleContent);
|
|
296
|
+
}
|
|
297
|
+
_configureAndroidStrings(androidPath) {
|
|
298
|
+
const stringsPath = path.resolve(androidPath, "app/src/main/res/values/strings.xml");
|
|
299
|
+
if (!FsUtils.exists(stringsPath)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
let stringsContent = FsUtils.readFile(stringsPath);
|
|
303
|
+
stringsContent = stringsContent.replace(/<string name="app_name">[^<]+<\/string>/, `<string name="app_name">${this._opt.config.appName}</string>`);
|
|
304
|
+
stringsContent = stringsContent.replace(/<string name="title_activity_main">[^<]+<\/string>/, `<string name="title_activity_main">${this._opt.config.appName}</string>`);
|
|
305
|
+
stringsContent = stringsContent.replace(/<string name="package_name">[^<]+<\/string>/, `<string name="package_name">${this._opt.config.appId}</string>`);
|
|
306
|
+
stringsContent = stringsContent.replace(/<string name="custom_url_scheme">[^<]+<\/string>/, `<string name="custom_url_scheme">${this._opt.config.appId}</string>`);
|
|
307
|
+
FsUtils.writeFile(stringsPath, stringsContent);
|
|
308
|
+
}
|
|
309
|
+
async buildAsync(outPath) {
|
|
310
|
+
const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
|
|
311
|
+
const buildType = this._opt.config.debug ? "debug" : "release";
|
|
312
|
+
// 웹 자산 동기화
|
|
313
|
+
await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
|
|
314
|
+
// 플랫폼별 빌드
|
|
315
|
+
await Promise.all(this._platforms.map((platform) => this._buildPlatformAsync(capacitorPath, outPath, platform, buildType)));
|
|
316
|
+
}
|
|
317
|
+
async _buildPlatformAsync(capacitorPath, outPath, platform, buildType) {
|
|
318
|
+
if (platform === "android") {
|
|
319
|
+
await this._buildAndroidAsync(capacitorPath, outPath, buildType);
|
|
320
|
+
}
|
|
321
|
+
// iOS 지원 시 추가
|
|
322
|
+
}
|
|
323
|
+
async _buildAndroidAsync(capacitorPath, outPath, buildType) {
|
|
324
|
+
const androidPath = path.resolve(capacitorPath, "android");
|
|
325
|
+
const targetOutPath = path.resolve(outPath, "android");
|
|
326
|
+
// Gradle wrapper로 빌드
|
|
327
|
+
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
328
|
+
const gradleTask = buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
|
|
329
|
+
// gradlew 실행 권한 부여 (Linux/Mac)
|
|
330
|
+
const gradlewPath = path.resolve(androidPath, "gradlew");
|
|
331
|
+
if (FsUtils.exists(gradlewPath)) {
|
|
332
|
+
try {
|
|
333
|
+
await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Windows에서는 무시
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Gradle 빌드 실행
|
|
340
|
+
const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
|
|
341
|
+
await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
|
|
342
|
+
// 빌드 결과물 복사
|
|
343
|
+
this._copyAndroidBuildOutput(androidPath, targetOutPath, buildType);
|
|
344
|
+
}
|
|
345
|
+
_copyAndroidBuildOutput(androidPath, targetOutPath, buildType) {
|
|
346
|
+
const isBundle = this._opt.config.platform?.android?.bundle;
|
|
347
|
+
const isSigned = !!this._opt.config.platform?.android?.sign;
|
|
348
|
+
const ext = isBundle ? "aab" : "apk";
|
|
349
|
+
const outputType = isBundle ? "bundle" : "apk";
|
|
350
|
+
const fileName = isSigned ? `app-${buildType}.${ext}` : `app-${buildType}-unsigned.${ext}`;
|
|
351
|
+
const sourcePath = path.resolve(androidPath, "app/build/outputs", outputType, buildType, fileName);
|
|
352
|
+
const actualPath = FsUtils.exists(sourcePath)
|
|
353
|
+
? sourcePath
|
|
354
|
+
: path.resolve(androidPath, "app/build/outputs", outputType, buildType, `app-${buildType}.${ext}`);
|
|
355
|
+
if (!FsUtils.exists(actualPath)) {
|
|
356
|
+
SdCliCapacitor._logger.warn(`빌드 결과물을 찾을 수 없습니다: ${actualPath}`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const outputFileName = `${this._opt.config.appName}${isSigned ? "" : "-unsigned"}-latest.${ext}`;
|
|
360
|
+
FsUtils.mkdirs(targetOutPath);
|
|
361
|
+
FsUtils.copy(actualPath, path.resolve(targetOutPath, outputFileName));
|
|
362
|
+
const updatesPath = path.resolve(targetOutPath, "updates");
|
|
363
|
+
FsUtils.mkdirs(updatesPath);
|
|
364
|
+
FsUtils.copy(actualPath, path.resolve(updatesPath, `${this._npmConfig.version}.${ext}`));
|
|
365
|
+
}
|
|
366
|
+
static async runWebviewOnDeviceAsync(opt) {
|
|
367
|
+
const projNpmConf = FsUtils.readJson(path.resolve(process.cwd(), "package.json"));
|
|
368
|
+
const allPkgPaths = projNpmConf.workspaces.mapMany((item) => FsUtils.glob(PathUtils.posix(process.cwd(), item)));
|
|
369
|
+
const capacitorPath = path.resolve(allPkgPaths.single((item) => item.endsWith(opt.package)), ".capacitor");
|
|
370
|
+
if (opt.url !== undefined) {
|
|
371
|
+
// capacitor.config.ts의 server.url 설정 업데이트
|
|
372
|
+
const configPath = path.resolve(capacitorPath, "capacitor.config.ts");
|
|
373
|
+
if (FsUtils.exists(configPath)) {
|
|
374
|
+
let configContent = FsUtils.readFile(configPath);
|
|
375
|
+
const serverUrl = `${opt.url.replace(/\/$/, "")}/${opt.package}/capacitor/`;
|
|
376
|
+
// 기존 url 설정이 있으면 교체, 없으면 server 블록 첫 줄에 추가
|
|
377
|
+
if (configContent.includes("url:")) {
|
|
378
|
+
configContent = configContent.replace(/url:\s*"[^"]*"/, `url: "${serverUrl}"`);
|
|
379
|
+
}
|
|
380
|
+
else if (configContent.includes("server:")) {
|
|
381
|
+
configContent = configContent.replace(/server:\s*\{/, `server: {\n url: "${serverUrl}",`);
|
|
382
|
+
}
|
|
383
|
+
FsUtils.writeFile(configPath, configContent);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// cap sync 후 run
|
|
387
|
+
await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
|
|
388
|
+
await this._execAsync("npx", ["cap", "run", opt.platform, "--target", "device"], capacitorPath);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -5,6 +5,7 @@ import { SdNgBundler } from "./SdNgBundler";
|
|
|
5
5
|
import { SdCliCordova } from "../../entry/SdCliCordova";
|
|
6
6
|
import { SdCliNgRoutesFileGenerator } from "./SdCliNgRoutesFileGenerator";
|
|
7
7
|
import { SdCliElectron } from "../../entry/SdCliElectron";
|
|
8
|
+
import { SdCliCapacitor } from "../../entry/SdCliCapacitor";
|
|
8
9
|
export class SdClientBuildRunner extends SdBuildRunnerBase {
|
|
9
10
|
constructor() {
|
|
10
11
|
super(...arguments);
|
|
@@ -27,6 +28,15 @@ export class SdClientBuildRunner extends SdBuildRunnerBase {
|
|
|
27
28
|
});
|
|
28
29
|
await this._cordova.initializeAsync();
|
|
29
30
|
}
|
|
31
|
+
// capacitor
|
|
32
|
+
if (this._pkgConf.builder?.capacitor) {
|
|
33
|
+
this._debug("Preparing Capacitor...");
|
|
34
|
+
this._capacitor = new SdCliCapacitor({
|
|
35
|
+
pkgPath: this._opt.pkgPath,
|
|
36
|
+
config: this._pkgConf.builder.capacitor,
|
|
37
|
+
});
|
|
38
|
+
await this._capacitor.initializeAsync();
|
|
39
|
+
}
|
|
30
40
|
// routes
|
|
31
41
|
const npmConf = (await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "package.json")));
|
|
32
42
|
if ("@angular/router" in (npmConf.dependencies ?? {})) {
|
|
@@ -80,6 +90,10 @@ export class SdClientBuildRunner extends SdBuildRunnerBase {
|
|
|
80
90
|
this._debug("Building Cordova...");
|
|
81
91
|
await this._cordova.buildAsync(path.resolve(this._opt.pkgPath, "dist"));
|
|
82
92
|
}
|
|
93
|
+
if (!this._opt.watch?.dev && this._capacitor) {
|
|
94
|
+
this._debug("Building Capacitor...");
|
|
95
|
+
await this._capacitor.buildAsync(path.resolve(this._opt.pkgPath, "dist"));
|
|
96
|
+
}
|
|
83
97
|
if (!this._opt.watch?.dev && this._pkgConf.builder?.electron) {
|
|
84
98
|
this._debug("Bulding Electron...");
|
|
85
99
|
await SdCliElectron.buildAsync({
|
|
@@ -54,7 +54,9 @@ export class SdNgBundler {
|
|
|
54
54
|
? PathUtils.norm(this._opt.pkgPath, ".electron/src")
|
|
55
55
|
: this._conf.builderType === "cordova" && !this._opt.watch?.dev
|
|
56
56
|
? PathUtils.norm(this._opt.pkgPath, ".cordova/www")
|
|
57
|
-
:
|
|
57
|
+
: this._conf.builderType === "capacitor" && !this._opt.watch?.dev
|
|
58
|
+
? PathUtils.norm(this._opt.pkgPath, ".capacitor/www")
|
|
59
|
+
: PathUtils.norm(this._opt.pkgPath, "dist", this._conf.builderType);
|
|
58
60
|
}
|
|
59
61
|
markForChanges(filePaths) {
|
|
60
62
|
for (const filePath of filePaths) {
|
package/dist/sd-cli-entry.js
CHANGED
|
@@ -10,12 +10,7 @@ import { SdCliElectron } from "./entry/SdCliElectron";
|
|
|
10
10
|
import { SdCliLocalUpdate } from "./entry/SdCliLocalUpdate";
|
|
11
11
|
import { SdCliPostInstall } from "./entry/SdCliPostInstall";
|
|
12
12
|
import { SdCliProject } from "./entry/SdCliProject";
|
|
13
|
-
import
|
|
14
|
-
import removeSdAngularSymbolNames from "./fix/removeSdAngularSymbolNames";
|
|
15
|
-
import convertSdAngularSymbolNames from "./fix/convertSdAngularSymbolNames";
|
|
16
|
-
import { removeUnusedInjects } from "./fix/removeUnusedInjects";
|
|
17
|
-
import removeUnusedProtectedReadonly from "./fix/removeUnusedProtectedReadonly";
|
|
18
|
-
import { removeUnusedImports } from "./fix/removeUnusedImports";
|
|
13
|
+
import { SdCliCapacitor } from "./entry/SdCliCapacitor";
|
|
19
14
|
Error.stackTraceLimit = Infinity;
|
|
20
15
|
EventEmitter.defaultMaxListeners = 0;
|
|
21
16
|
await yargs(hideBin(process.argv))
|
|
@@ -199,48 +194,27 @@ await yargs(hideBin(process.argv))
|
|
|
199
194
|
describe: "Webview로 오픈할 URL",
|
|
200
195
|
demandOption: true,
|
|
201
196
|
}), async (argv) => await SdCliCordova.runWebviewOnDeviceAsync(argv))
|
|
202
|
-
.command("
|
|
203
|
-
.command("postinstall", "설치후 자동실행할 작업", (cmd) => cmd.version(false).hide("help").hide("debug"), () => SdCliPostInstall.run())
|
|
204
|
-
.command("fix", "가능한 내용 자동 수정", (cmd) => cmd
|
|
197
|
+
.command("run-capacitor <platform> <package> [url]", "변경감지중인 플랫폼을 Capacitor 디바이스에 앱 형태로 띄웁니다.", (cmd) => cmd
|
|
205
198
|
.version(false)
|
|
206
199
|
.hide("help")
|
|
207
200
|
.hide("debug")
|
|
208
|
-
.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
default: false,
|
|
213
|
-
},
|
|
214
|
-
}), (argv) => {
|
|
215
|
-
// GIT 사용중일 경우, 커밋되지 않은 수정사항이 있는지 확인
|
|
216
|
-
/*if (FsUtils.exists(path.resolve(process.cwd(), ".git"))) {
|
|
217
|
-
const gitStatusResult = await SdProcess.spawnAsync("git status");
|
|
218
|
-
if (gitStatusResult.includes("Changes") || gitStatusResult.includes("Untracked")) {
|
|
219
|
-
throw new Error("커밋되지 않은 정보가 있습니다. FIX오류시 롤백이 불가능하므로, 미리 커밋을 해놔야 합니다.\n" + gitStatusResult);
|
|
220
|
-
}
|
|
221
|
-
}*/
|
|
222
|
-
convertPrivateToHash();
|
|
223
|
-
//-- 심볼정리
|
|
224
|
-
removeSdAngularSymbolNames();
|
|
225
|
-
convertSdAngularSymbolNames();
|
|
226
|
-
//-- inject/import 정리
|
|
227
|
-
removeUnusedInjects();
|
|
228
|
-
removeUnusedProtectedReadonly();
|
|
229
|
-
removeUnusedImports();
|
|
230
|
-
if (argv.library)
|
|
231
|
-
return;
|
|
232
|
-
// convertSdSheetBindingsSafely();
|
|
233
|
-
// convertSetupCumulateSelectedKeysToObjectParam();
|
|
234
|
-
// convertExtendsSdModalBaseToInterface();
|
|
235
|
-
// convertModalShowParams();
|
|
236
|
-
// convertExtendsSdPrintTemplateBaseToInterface();
|
|
237
|
-
// convertPrintParams();
|
|
238
|
-
// convertToUsePermsSignal();
|
|
239
|
-
// convertGetMenusToUsableMenus();
|
|
240
|
-
// convertFlatPagesToUsableFlatMenus();
|
|
241
|
-
// convertSdIconToFaIcon();
|
|
242
|
-
// convertSelectModalButtonToSelectButton();
|
|
201
|
+
.positional("platform", {
|
|
202
|
+
type: "string",
|
|
203
|
+
describe: "빌드 플랫폼(android,...)",
|
|
204
|
+
demandOption: true,
|
|
243
205
|
})
|
|
206
|
+
.positional("package", {
|
|
207
|
+
type: "string",
|
|
208
|
+
describe: "패키지명",
|
|
209
|
+
demandOption: true,
|
|
210
|
+
})
|
|
211
|
+
.positional("url", {
|
|
212
|
+
type: "string",
|
|
213
|
+
describe: "Webview로 오픈할 URL",
|
|
214
|
+
demandOption: true,
|
|
215
|
+
}), async (argv) => await SdCliCapacitor.runWebviewOnDeviceAsync(argv))
|
|
216
|
+
.command("commit", "AI를 통해 변경사항에 대한 커밋 메시지를 작성하여, 커밋 및 푸쉬를 수행합니다.", (cmd) => cmd.version(false).hide("help").hide("debug"), async () => await SdCliAiCommand.commitAsync())
|
|
217
|
+
.command("postinstall", "설치후 자동실행할 작업", (cmd) => cmd.version(false).hide("help").hide("debug"), () => SdCliPostInstall.run())
|
|
244
218
|
.strict()
|
|
245
219
|
.recommendCommands()
|
|
246
220
|
.fail((msg, err, cmd) => {
|
|
@@ -45,6 +45,8 @@ export interface ISdClientPackageConfig {
|
|
|
45
45
|
builder?: {
|
|
46
46
|
web?: ISdClientBuilderWebConfig;
|
|
47
47
|
electron?: ISdClientBuilderElectronConfig;
|
|
48
|
+
capacitor?: ISdClientBuilderCapacitorConfig;
|
|
49
|
+
/** @deprecated */
|
|
48
50
|
cordova?: ISdClientBuilderCordovaConfig;
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -72,6 +74,7 @@ export interface ISdClientBuilderElectronConfig {
|
|
|
72
74
|
export interface ISdClientBuilderWebConfig {
|
|
73
75
|
env?: Record<string, string>;
|
|
74
76
|
}
|
|
77
|
+
/** @deprecated */
|
|
75
78
|
export interface ISdClientBuilderCordovaConfig {
|
|
76
79
|
appId: string;
|
|
77
80
|
appName: string;
|
|
@@ -101,6 +104,34 @@ export interface ISdClientBuilderCordovaConfig {
|
|
|
101
104
|
env?: Record<string, string>;
|
|
102
105
|
browserslist?: string[];
|
|
103
106
|
}
|
|
107
|
+
export interface ISdClientBuilderCapacitorConfig {
|
|
108
|
+
appId: string;
|
|
109
|
+
appName: string;
|
|
110
|
+
plugins?: Record<string, Record<string, unknown> | true>;
|
|
111
|
+
icon?: string;
|
|
112
|
+
debug?: boolean;
|
|
113
|
+
platform?: {
|
|
114
|
+
android?: {
|
|
115
|
+
config?: Record<string, string>;
|
|
116
|
+
bundle?: boolean;
|
|
117
|
+
sign?: {
|
|
118
|
+
keystore: string;
|
|
119
|
+
storePassword: string;
|
|
120
|
+
alias: string;
|
|
121
|
+
password: string;
|
|
122
|
+
keystoreType?: string;
|
|
123
|
+
};
|
|
124
|
+
sdkVersion?: number;
|
|
125
|
+
permissions?: {
|
|
126
|
+
name: string;
|
|
127
|
+
maxSdkVersion?: number;
|
|
128
|
+
ignore?: string;
|
|
129
|
+
}[];
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
env?: Record<string, string>;
|
|
133
|
+
browserslist?: string[];
|
|
134
|
+
}
|
|
104
135
|
export type TSdPostPublishConfig = ISdPostPublishScriptConfig;
|
|
105
136
|
export interface ISdPostPublishScriptConfig {
|
|
106
137
|
type: "script";
|