@simplysm/sd-cli 14.0.12 → 14.0.13
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/angular/vite-angular-plugin.d.ts +3 -1
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +15 -4
- package/dist/angular/vite-angular-plugin.js.map +1 -1
- package/dist/capacitor/capacitor.d.ts +10 -4
- package/dist/capacitor/capacitor.d.ts.map +1 -1
- package/dist/capacitor/capacitor.js +34 -21
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +7 -1
- package/dist/commands/publish.js.map +1 -1
- package/dist/engines/ViteEngine.d.ts +6 -0
- package/dist/engines/ViteEngine.d.ts.map +1 -1
- package/dist/engines/ViteEngine.js +6 -0
- package/dist/engines/ViteEngine.js.map +1 -1
- package/dist/engines/index.d.ts +4 -0
- package/dist/engines/index.d.ts.map +1 -1
- package/dist/engines/index.js +2 -0
- package/dist/engines/index.js.map +1 -1
- package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/BuildOrchestrator.js +5 -1
- package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
- package/dist/utils/vite-config.d.ts +4 -0
- package/dist/utils/vite-config.d.ts.map +1 -1
- package/dist/utils/vite-config.js +65 -18
- package/dist/utils/vite-config.js.map +1 -1
- package/dist/workers/client.worker.d.ts +4 -0
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +6 -5
- package/dist/workers/client.worker.js.map +1 -1
- package/package.json +4 -4
- package/src/angular/vite-angular-plugin.ts +22 -5
- package/src/capacitor/capacitor.ts +39 -28
- package/src/engines/ViteEngine.ts +10 -0
- package/src/engines/index.ts +6 -0
- package/src/orchestrators/BuildOrchestrator.ts +5 -1
- package/src/utils/vite-config.ts +70 -18
- package/src/workers/client.worker.ts +10 -5
- package/tests/capacitor/capacitor-build.spec.ts +87 -7
- package/tests/capacitor/capacitor-init.spec.ts +9 -5
- package/tests/utils/vite-config.spec.ts +21 -3
|
@@ -299,15 +299,6 @@ export class Capacitor {
|
|
|
299
299
|
Capacitor._logger.debug("cap init 완료");
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
// 기본 www/index.html 생성
|
|
303
|
-
const wwwPath = pathx.posixResolve(this._capPath, "www");
|
|
304
|
-
await fsx.mkdir(wwwPath);
|
|
305
|
-
await fsx.write(
|
|
306
|
-
pathx.posixResolve(wwwPath, "index.html"),
|
|
307
|
-
"<!DOCTYPE html><html><head></head><body></body></html>",
|
|
308
|
-
);
|
|
309
|
-
Capacitor._logger.debug("www/index.html 생성 완료");
|
|
310
|
-
|
|
311
302
|
return true;
|
|
312
303
|
}
|
|
313
304
|
|
|
@@ -798,11 +789,17 @@ export default config;
|
|
|
798
789
|
}
|
|
799
790
|
|
|
800
791
|
/**
|
|
801
|
-
* styles.xml의 스플래시 테마
|
|
792
|
+
* styles.xml의 스플래시 테마 수정
|
|
802
793
|
*
|
|
803
|
-
* Theme.SplashScreen
|
|
804
|
-
* android:
|
|
805
|
-
*
|
|
794
|
+
* 1. Theme.SplashScreen parent → Theme.AppCompat.DayNight.NoActionBar
|
|
795
|
+
* Theme.SplashScreen은 android:windowBackground에 compat_splash_screen을 설정하여
|
|
796
|
+
* android:background(@drawable/splash)와 이중 표시를 발생시킨다.
|
|
797
|
+
* installSplashScreen()을 호출하지 않으므로 Theme.SplashScreen 기능이 불필요하다.
|
|
798
|
+
*
|
|
799
|
+
* 2. android:background → android:windowBackground
|
|
800
|
+
* android:background는 View 레벨 속성으로 AppCompat 뷰 계층의 여러 View에 상속되어
|
|
801
|
+
* 동일한 splash 로고가 다중 레이어에 중복 렌더링된다.
|
|
802
|
+
* android:windowBackground는 Window의 DecorView에만 적용되어 단일 렌더링을 보장한다.
|
|
806
803
|
*/
|
|
807
804
|
private async _configureAndroidStyles(androidPath: string): Promise<void> {
|
|
808
805
|
const stylesPath = pathx.posixResolve(androidPath, "app/src/main/res/values/styles.xml");
|
|
@@ -813,17 +810,27 @@ export default config;
|
|
|
813
810
|
}
|
|
814
811
|
|
|
815
812
|
let content = await fsx.read(stylesPath);
|
|
813
|
+
let changed = false;
|
|
816
814
|
|
|
817
|
-
if (
|
|
818
|
-
|
|
815
|
+
if (content.includes('parent="Theme.SplashScreen"')) {
|
|
816
|
+
content = content.replace(
|
|
817
|
+
'parent="Theme.SplashScreen"',
|
|
818
|
+
'parent="Theme.AppCompat.DayNight.NoActionBar"',
|
|
819
|
+
);
|
|
820
|
+
changed = true;
|
|
819
821
|
}
|
|
820
822
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
823
|
+
if (content.includes('"android:background">@drawable/splash')) {
|
|
824
|
+
content = content.replace(
|
|
825
|
+
'"android:background">@drawable/splash',
|
|
826
|
+
'"android:windowBackground">@drawable/splash',
|
|
827
|
+
);
|
|
828
|
+
changed = true;
|
|
829
|
+
}
|
|
825
830
|
|
|
826
|
-
|
|
831
|
+
if (changed) {
|
|
832
|
+
await fsx.write(stylesPath, content);
|
|
833
|
+
}
|
|
827
834
|
}
|
|
828
835
|
|
|
829
836
|
//#endregion
|
|
@@ -966,8 +973,9 @@ export default config;
|
|
|
966
973
|
if (content.includes("signingConfigs")) return;
|
|
967
974
|
|
|
968
975
|
const storeType = sign.keystoreType ?? "jks";
|
|
969
|
-
const
|
|
970
|
-
const
|
|
976
|
+
const escapeGroovy = (s: string) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
977
|
+
const escapedStorePassword = escapeGroovy(sign.storePassword);
|
|
978
|
+
const escapedKeyPassword = escapeGroovy(sign.password);
|
|
971
979
|
|
|
972
980
|
const signingBlock = ` signingConfigs {
|
|
973
981
|
release {
|
|
@@ -981,7 +989,7 @@ export default config;
|
|
|
981
989
|
`;
|
|
982
990
|
|
|
983
991
|
// signingConfigs 블록을 buildTypes 앞에 삽입
|
|
984
|
-
content = content.replace(/(\s*buildTypes\s*\{)/,
|
|
992
|
+
content = content.replace(/(\s*buildTypes\s*\{)/, (match) => `\n${signingBlock}${match}`);
|
|
985
993
|
|
|
986
994
|
// buildTypes.release에 signingConfig 추가
|
|
987
995
|
content = content.replace(
|
|
@@ -1007,12 +1015,15 @@ export default config;
|
|
|
1007
1015
|
|
|
1008
1016
|
const androidPath = pathx.posixResolve(this._capPath, "android");
|
|
1009
1017
|
const isWindows = process.platform === "win32";
|
|
1010
|
-
const gradlew = isWindows
|
|
1011
|
-
? pathx.posixResolve(androidPath, "gradlew.bat")
|
|
1012
|
-
: pathx.posixResolve(androidPath, "gradlew");
|
|
1013
1018
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1019
|
+
if (isWindows) {
|
|
1020
|
+
Capacitor._logger.debug(`Gradle 실행: cmd /c gradlew.bat ${gradleTask}`);
|
|
1021
|
+
await this._exec("cmd", ["/c", "gradlew.bat", gradleTask, "--no-daemon"], androidPath);
|
|
1022
|
+
} else {
|
|
1023
|
+
const gradlew = pathx.posixResolve(androidPath, "gradlew");
|
|
1024
|
+
Capacitor._logger.debug(`Gradle 실행: ${gradlew} ${gradleTask}`);
|
|
1025
|
+
await this._exec(gradlew, [gradleTask, "--no-daemon"], androidPath);
|
|
1026
|
+
}
|
|
1016
1027
|
}
|
|
1017
1028
|
|
|
1018
1029
|
/**
|
|
@@ -20,6 +20,10 @@ export interface ViteEngineOptions {
|
|
|
20
20
|
rebuildManager?: RebuildManager;
|
|
21
21
|
/** sdScopeWatchPlugin용 replaceDeps 항목 */
|
|
22
22
|
replaceDeps?: Array<{ packageName: string; sourcePath: string }>;
|
|
23
|
+
/** 빌드 출력 경로 (미설정 시 pkgDir/dist) */
|
|
24
|
+
outDir?: string;
|
|
25
|
+
/** Vite base 경로 (미설정 시 /{pkgName}/) */
|
|
26
|
+
base?: string;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
/**
|
|
@@ -34,6 +38,8 @@ export class ViteEngine implements BuildEngine {
|
|
|
34
38
|
private readonly _resultCollector: ResultCollector | undefined;
|
|
35
39
|
private readonly _rebuildManager: RebuildManager | undefined;
|
|
36
40
|
private readonly _replaceDeps: Array<{ packageName: string; sourcePath: string }> | undefined;
|
|
41
|
+
private readonly _outDir: string | undefined;
|
|
42
|
+
private readonly _base: string | undefined;
|
|
37
43
|
|
|
38
44
|
private _worker: WorkerProxy<typeof ClientWorkerModule> | undefined;
|
|
39
45
|
private _isWatchMode = false;
|
|
@@ -47,6 +53,8 @@ export class ViteEngine implements BuildEngine {
|
|
|
47
53
|
this._resultCollector = options.resultCollector;
|
|
48
54
|
this._rebuildManager = options.rebuildManager;
|
|
49
55
|
this._replaceDeps = options.replaceDeps;
|
|
56
|
+
this._outDir = options.outDir;
|
|
57
|
+
this._base = options.base;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
/**
|
|
@@ -65,6 +73,8 @@ export class ViteEngine implements BuildEngine {
|
|
|
65
73
|
browserSupport: this._pkg.config.browserSupport,
|
|
66
74
|
enableLint: output.lint,
|
|
67
75
|
exclude: this._pkg.config.exclude,
|
|
76
|
+
outDir: this._outDir,
|
|
77
|
+
base: this._base,
|
|
68
78
|
});
|
|
69
79
|
|
|
70
80
|
logger.debug(`[${this._pkg.name}] ViteEngine.run 완료 (success: ${result.success})`);
|
package/src/engines/index.ts
CHANGED
|
@@ -38,6 +38,10 @@ export function createBuildEngine(
|
|
|
38
38
|
resolvedReplaceDeps?: Array<{ packageName: string; sourcePath: string }>;
|
|
39
39
|
resultCollector?: ResultCollector;
|
|
40
40
|
rebuildManager?: RebuildManager;
|
|
41
|
+
/** 클라이언트 빌드 출력 경로 (ViteEngine에만 적용) */
|
|
42
|
+
outDir?: string;
|
|
43
|
+
/** Vite base 경로 (ViteEngine에만 적용, 미설정 시 /{pkgName}/) */
|
|
44
|
+
base?: string;
|
|
41
45
|
},
|
|
42
46
|
): BuildEngine {
|
|
43
47
|
if (pkg.config.target === "client") {
|
|
@@ -48,6 +52,8 @@ export function createBuildEngine(
|
|
|
48
52
|
resultCollector: options.resultCollector,
|
|
49
53
|
rebuildManager: options.rebuildManager,
|
|
50
54
|
replaceDeps: options.resolvedReplaceDeps,
|
|
55
|
+
outDir: options.outDir,
|
|
56
|
+
base: options.base,
|
|
51
57
|
});
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -328,9 +328,13 @@ export class BuildOrchestrator {
|
|
|
328
328
|
|
|
329
329
|
buildTasks.push(async () => {
|
|
330
330
|
this._logger.debug(`[${name}] (client) 빌드 시작`);
|
|
331
|
+
const isNativeBuild = config.capacitor != null || config.electron != null;
|
|
332
|
+
const outDir = config.capacitor != null
|
|
333
|
+
? pathx.posixResolve(pkgDir, ".capacitor/www")
|
|
334
|
+
: undefined;
|
|
331
335
|
const engine = createBuildEngine(
|
|
332
336
|
{ name, dir: pkgDir, config: { ...config, env: { ...baseEnv, ...config.env } } },
|
|
333
|
-
{ cwd: this._cwd },
|
|
337
|
+
{ cwd: this._cwd, outDir, base: isNativeBuild ? "" : undefined },
|
|
334
338
|
);
|
|
335
339
|
|
|
336
340
|
try {
|
package/src/utils/vite-config.ts
CHANGED
|
@@ -55,6 +55,10 @@ export interface CreateClientViteConfigOptions {
|
|
|
55
55
|
exclude?: string[];
|
|
56
56
|
/** watch 모드 (build.watch 활성화, emptyOutDir: false) */
|
|
57
57
|
watch?: boolean;
|
|
58
|
+
/** 빌드 출력 경로 (미설정 시 pkgDir/dist) */
|
|
59
|
+
outDir?: string;
|
|
60
|
+
/** Vite base 경로 (미설정 시 /{pkgName}/) */
|
|
61
|
+
base?: string;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
|
@@ -99,6 +103,7 @@ export async function createClientViteConfig(
|
|
|
99
103
|
sdAngularPlugin({
|
|
100
104
|
tsconfig: options.tsconfigPath,
|
|
101
105
|
dev: options.mode === "dev",
|
|
106
|
+
sourcemap: options.mode === "dev" || options.watch === true,
|
|
102
107
|
onBuildStart: options.onBuildStart,
|
|
103
108
|
onBuild: options.onBuild,
|
|
104
109
|
enableLint: options.enableLint,
|
|
@@ -153,7 +158,7 @@ export async function createClientViteConfig(
|
|
|
153
158
|
|
|
154
159
|
const config: InlineConfig = {
|
|
155
160
|
root: options.pkgDir,
|
|
156
|
-
base: `/${name}/`,
|
|
161
|
+
base: options.base ?? `/${name}/`,
|
|
157
162
|
define: Object.keys(define).length > 0 ? define : undefined,
|
|
158
163
|
plugins,
|
|
159
164
|
server: serverConfig,
|
|
@@ -210,31 +215,57 @@ export async function createClientViteConfig(
|
|
|
210
215
|
);
|
|
211
216
|
}
|
|
212
217
|
|
|
213
|
-
// polyfills plugin
|
|
218
|
+
// polyfills plugin
|
|
214
219
|
if (options.polyfills != null && options.polyfills.length > 0) {
|
|
215
220
|
const polyfillImports = options.polyfills;
|
|
216
|
-
(
|
|
217
|
-
|
|
218
|
-
transformIndexHtml
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
if (options.legacyModule === true) {
|
|
222
|
+
// legacyModule: 메인 엔트리의 transform에서 polyfill import를 상단에 주입한다.
|
|
223
|
+
// transformIndexHtml의 인라인 <script type="module">은 Vite 빌드에서 번들링되지 않으므로,
|
|
224
|
+
// Rollup이 처리할 수 있도록 소스 코드 레벨에서 주입한다.
|
|
225
|
+
const mainEntryPath = path.resolve(options.pkgDir, "src/main.ts");
|
|
226
|
+
(config.plugins as PluginOption[]).push({
|
|
227
|
+
name: "sd-polyfills",
|
|
228
|
+
transform(code, id) {
|
|
229
|
+
if (path.normalize(id) !== path.normalize(mainEntryPath)) return null;
|
|
230
|
+
// polyfillImports는 pkgDir 기준 상대경로 (예: "./src/polyfills.ts")
|
|
231
|
+
// main.ts는 src/ 안에 있으므로, main.ts 기준 상대경로로 변환한다
|
|
232
|
+
const mainDir = path.dirname(mainEntryPath);
|
|
233
|
+
const polyfillCode = polyfillImports
|
|
234
|
+
.map((p) => {
|
|
235
|
+
const abs = path.resolve(options.pkgDir, p);
|
|
236
|
+
this.addWatchFile(abs);
|
|
237
|
+
const rel = path.relative(mainDir, abs);
|
|
238
|
+
const posixRel = rel.replace(/\\/g, "/");
|
|
239
|
+
return "import \"./" + posixRel + "\";";
|
|
240
|
+
})
|
|
241
|
+
.join("\n");
|
|
242
|
+
return { code: polyfillCode + "\n" + code, map: null };
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
// dev 모드: transformIndexHtml로 인라인 스크립트 주입 (Vite dev server가 처리)
|
|
247
|
+
(config.plugins as PluginOption[]).push({
|
|
248
|
+
name: "sd-polyfills",
|
|
249
|
+
transformIndexHtml() {
|
|
250
|
+
return [
|
|
251
|
+
{
|
|
252
|
+
tag: "script",
|
|
253
|
+
attrs: { type: "module" },
|
|
254
|
+
children: polyfillImports.map((p) => `import "${p}";`).join("\n"),
|
|
255
|
+
injectTo: "head-prepend" as const,
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
229
261
|
}
|
|
230
262
|
|
|
231
|
-
// legacyModule: true → 코드 스플리팅 비활성화 + esbuild import.meta
|
|
263
|
+
// legacyModule: true → 코드 스플리팅 비활성화 + esbuild import.meta 변환 + 잔여 import() 제거
|
|
232
264
|
if (options.legacyModule === true) {
|
|
233
265
|
config.esbuild = {
|
|
234
266
|
...config.esbuild,
|
|
235
267
|
supported: {
|
|
236
268
|
"import-meta": false,
|
|
237
|
-
"dynamic-import": false,
|
|
238
269
|
},
|
|
239
270
|
};
|
|
240
271
|
config.build = {
|
|
@@ -245,17 +276,38 @@ export async function createClientViteConfig(
|
|
|
245
276
|
},
|
|
246
277
|
},
|
|
247
278
|
};
|
|
279
|
+
|
|
280
|
+
// Rollup이 인라인하지 못한 잔여 dynamic import()를 제거한다.
|
|
281
|
+
// inlineDynamicImports가 정적 경로를 모두 인라인한 후에도,
|
|
282
|
+
// @vite-ignore나 런타임 계산 경로의 import()가 남을 수 있다.
|
|
283
|
+
// Chrome 61은 import() 구문을 파싱하지 못하므로 no-op 함수로 치환한다.
|
|
284
|
+
(config.plugins as PluginOption[]).push({
|
|
285
|
+
name: "sd-legacy-strip-dynamic-import",
|
|
286
|
+
enforce: "post",
|
|
287
|
+
renderChunk(code) {
|
|
288
|
+
if (!code.includes("import(")) return null;
|
|
289
|
+
return {
|
|
290
|
+
code: code.replace(
|
|
291
|
+
/\bimport\s*\(/g,
|
|
292
|
+
"(function(){return Promise.reject(new Error(\"Dynamic import not supported\"))})(",
|
|
293
|
+
),
|
|
294
|
+
map: null,
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
});
|
|
248
298
|
}
|
|
249
299
|
|
|
250
300
|
// build 모드 설정
|
|
251
301
|
if (options.mode === "build") {
|
|
252
302
|
config.build = {
|
|
253
303
|
...config.build,
|
|
254
|
-
outDir: path.join(options.pkgDir, "dist"),
|
|
304
|
+
outDir: options.outDir ?? path.join(options.pkgDir, "dist"),
|
|
255
305
|
};
|
|
256
306
|
if (options.watch === true) {
|
|
257
307
|
config.build.watch = {};
|
|
258
308
|
config.build.emptyOutDir = false;
|
|
309
|
+
config.build.minify = false;
|
|
310
|
+
config.build.sourcemap = true;
|
|
259
311
|
} else {
|
|
260
312
|
config.logLevel = "silent";
|
|
261
313
|
config.build.emptyOutDir = true;
|
|
@@ -37,6 +37,10 @@ export interface ClientBuildInfo {
|
|
|
37
37
|
enableLint?: boolean;
|
|
38
38
|
/** Vite optimizeDeps.exclude에 전달할 패키지 목록 */
|
|
39
39
|
exclude?: string[];
|
|
40
|
+
/** 빌드 출력 경로 (미설정 시 pkgDir/dist) */
|
|
41
|
+
outDir?: string;
|
|
42
|
+
/** Vite base 경로 (미설정 시 /{pkgName}/) */
|
|
43
|
+
base?: string;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/** Client 빌드 결과 */
|
|
@@ -416,12 +420,14 @@ async function build(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
416
420
|
polyfills,
|
|
417
421
|
pwa: info.pwa,
|
|
418
422
|
exclude: info.exclude,
|
|
423
|
+
outDir: info.outDir,
|
|
424
|
+
base: info.base,
|
|
419
425
|
});
|
|
420
426
|
|
|
421
427
|
await viteBuild(viteConfig);
|
|
422
428
|
|
|
423
|
-
// .config.json 생성
|
|
424
|
-
writeConfigJson(info.pkgDir, info.configs);
|
|
429
|
+
// .config.json 생성 (항상 dist/에 기록 — outDir과 무관)
|
|
430
|
+
writeConfigJson(path.join(info.pkgDir, "dist"), info.configs);
|
|
425
431
|
|
|
426
432
|
logger.debug(`[${info.name}] client worker build 완료`);
|
|
427
433
|
return { success: true, lint: lintResult };
|
|
@@ -432,12 +438,11 @@ async function build(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
432
438
|
}
|
|
433
439
|
}
|
|
434
440
|
|
|
435
|
-
/**
|
|
441
|
+
/** .config.json 생성 */
|
|
436
442
|
function writeConfigJson(
|
|
437
|
-
|
|
443
|
+
distDir: string,
|
|
438
444
|
configs?: Record<string, unknown>,
|
|
439
445
|
): void {
|
|
440
|
-
const distDir = path.join(pkgDir, "dist");
|
|
441
446
|
fs.mkdirSync(distDir, { recursive: true });
|
|
442
447
|
fs.writeFileSync(
|
|
443
448
|
path.join(distDir, ".config.json"),
|
|
@@ -93,6 +93,19 @@ vi.mock("consola", () => ({
|
|
|
93
93
|
|
|
94
94
|
const PKG_PATH = "/fake/pkg";
|
|
95
95
|
|
|
96
|
+
/** Gradle 실행 명령을 찾는다 (Windows: cmd /c gradlew.bat, 그 외: gradlew) */
|
|
97
|
+
function findGradleCall(calls: { command: string; args: string[] }[]) {
|
|
98
|
+
return calls.find(
|
|
99
|
+
(c) => c.command.includes("gradlew") || (c.command === "cmd" && c.args.includes("gradlew.bat")),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findGradleCallIndex(calls: { command: string; args: string[] }[]) {
|
|
104
|
+
return calls.findIndex(
|
|
105
|
+
(c) => c.command.includes("gradlew") || (c.command === "cmd" && c.args.includes("gradlew.bat")),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
function setupDefaultMocks() {
|
|
97
110
|
mockFsxExists.mockResolvedValue(true);
|
|
98
111
|
|
|
@@ -181,7 +194,7 @@ describe("Capacitor 빌드", () => {
|
|
|
181
194
|
|
|
182
195
|
await cap.build("/fake/out");
|
|
183
196
|
|
|
184
|
-
const gradleCmd = execaCalls
|
|
197
|
+
const gradleCmd = findGradleCall(execaCalls);
|
|
185
198
|
expect(gradleCmd).toBeDefined();
|
|
186
199
|
expect(gradleCmd!.args).toContain("bundleRelease");
|
|
187
200
|
});
|
|
@@ -197,7 +210,7 @@ describe("Capacitor 빌드", () => {
|
|
|
197
210
|
|
|
198
211
|
await cap.build("/fake/out");
|
|
199
212
|
|
|
200
|
-
const gradleCmd = execaCalls
|
|
213
|
+
const gradleCmd = findGradleCall(execaCalls);
|
|
201
214
|
expect(gradleCmd).toBeDefined();
|
|
202
215
|
expect(gradleCmd!.args).toContain("assembleRelease");
|
|
203
216
|
});
|
|
@@ -214,7 +227,7 @@ describe("Capacitor 빌드", () => {
|
|
|
214
227
|
|
|
215
228
|
await cap.build("/fake/out");
|
|
216
229
|
|
|
217
|
-
const gradleCmd = execaCalls
|
|
230
|
+
const gradleCmd = findGradleCall(execaCalls);
|
|
218
231
|
expect(gradleCmd).toBeDefined();
|
|
219
232
|
expect(gradleCmd!.args).toContain("assembleDebug");
|
|
220
233
|
});
|
|
@@ -274,12 +287,12 @@ describe("Capacitor 빌드", () => {
|
|
|
274
287
|
const capCopyIndex = execaCalls.findIndex(
|
|
275
288
|
(c) => c.command === "pnpm" && c.args.includes("cap") && c.args.includes("copy"),
|
|
276
289
|
);
|
|
277
|
-
const gradlewIndex = execaCalls
|
|
290
|
+
const gradlewIndex = findGradleCallIndex(execaCalls);
|
|
278
291
|
expect(capCopyIndex).toBeGreaterThanOrEqual(0);
|
|
279
292
|
expect(gradlewIndex).toBeGreaterThan(capCopyIndex);
|
|
280
293
|
});
|
|
281
294
|
|
|
282
|
-
it("Windows에서 gradlew.bat을
|
|
295
|
+
it("Windows에서 cmd /c gradlew.bat으로 Gradle을 실행한다", async () => {
|
|
283
296
|
const originalPlatform = process.platform;
|
|
284
297
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
285
298
|
|
|
@@ -294,8 +307,37 @@ describe("Capacitor 빌드", () => {
|
|
|
294
307
|
|
|
295
308
|
await cap.build("/fake/out");
|
|
296
309
|
|
|
297
|
-
const gradleCmd = execaCalls.find((c) => c.command
|
|
298
|
-
expect(gradleCmd
|
|
310
|
+
const gradleCmd = execaCalls.find((c) => c.command === "cmd");
|
|
311
|
+
expect(gradleCmd).toBeDefined();
|
|
312
|
+
expect(gradleCmd!.args[0]).toBe("/c");
|
|
313
|
+
expect(gradleCmd!.args[1]).toBe("gradlew.bat");
|
|
314
|
+
expect(gradleCmd!.args).toContain("assembleRelease");
|
|
315
|
+
expect(gradleCmd!.args).toContain("--no-daemon");
|
|
316
|
+
} finally {
|
|
317
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("Linux/macOS에서 gradlew를 직접 실행한다", async () => {
|
|
322
|
+
const originalPlatform = process.platform;
|
|
323
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
327
|
+
|
|
328
|
+
const cap = await Capacitor.create(PKG_PATH, {
|
|
329
|
+
appId: "com.test.app",
|
|
330
|
+
appName: "Test App",
|
|
331
|
+
platform: { android: {} },
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await cap.build("/fake/out");
|
|
335
|
+
|
|
336
|
+
const gradleCmd = findGradleCall(execaCalls);
|
|
337
|
+
expect(gradleCmd).toBeDefined();
|
|
338
|
+
expect(gradleCmd!.command).toContain("gradlew");
|
|
339
|
+
expect(gradleCmd!.command).not.toContain("gradlew.bat");
|
|
340
|
+
expect(gradleCmd!.args).toContain("assembleRelease");
|
|
299
341
|
} finally {
|
|
300
342
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
301
343
|
}
|
|
@@ -377,6 +419,44 @@ describe("Capacitor 빌드", () => {
|
|
|
377
419
|
await expect(cap.build("/fake/out")).rejects.toThrow("keystore");
|
|
378
420
|
});
|
|
379
421
|
|
|
422
|
+
it("비밀번호에 $, \\, ' 등 특수문자가 있으면 Groovy 이스케이프하여 build.gradle에 기록한다", async () => {
|
|
423
|
+
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
424
|
+
mockFsxExists.mockResolvedValue(true);
|
|
425
|
+
|
|
426
|
+
const cap = await Capacitor.create(PKG_PATH, {
|
|
427
|
+
appId: "com.test.app",
|
|
428
|
+
appName: "Test App",
|
|
429
|
+
platform: {
|
|
430
|
+
android: {
|
|
431
|
+
sign: {
|
|
432
|
+
keystore: "my-release.keystore",
|
|
433
|
+
storePassword: "12tlavmf#$",
|
|
434
|
+
alias: "my-key",
|
|
435
|
+
password: "pass\\'word",
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await cap.build("/fake/out");
|
|
442
|
+
|
|
443
|
+
const writeCalls = mockFsxWrite.mock.calls;
|
|
444
|
+
const gradleWrite = writeCalls.find(
|
|
445
|
+
(call) =>
|
|
446
|
+
typeof call[0] === "string" &&
|
|
447
|
+
call[0].includes("build.gradle") &&
|
|
448
|
+
typeof call[1] === "string" &&
|
|
449
|
+
call[1].includes("signingConfigs"),
|
|
450
|
+
);
|
|
451
|
+
expect(gradleWrite).toBeDefined();
|
|
452
|
+
|
|
453
|
+
const gradleContent = gradleWrite![1] as string;
|
|
454
|
+
// $ 는 Groovy single-quoted string에서 그대로 유지
|
|
455
|
+
expect(gradleContent).toContain("storePassword '12tlavmf#$'");
|
|
456
|
+
// \ → \\, ' → \' 이스케이프
|
|
457
|
+
expect(gradleContent).toContain("keyPassword 'pass\\\\\\'word'");
|
|
458
|
+
});
|
|
459
|
+
|
|
380
460
|
it("signed 빌드 산출물이 unsigned 접미사 없이 복사된다", async () => {
|
|
381
461
|
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
382
462
|
|
|
@@ -549,7 +549,7 @@ describe("Android 네이티브 설정", () => {
|
|
|
549
549
|
expect(manifestWrite).toBeDefined();
|
|
550
550
|
});
|
|
551
551
|
|
|
552
|
-
it("styles.xml의 Theme.SplashScreen parent를
|
|
552
|
+
it("styles.xml의 Theme.SplashScreen parent를 변경하고 android:background를 android:windowBackground로 변경한다", async () => {
|
|
553
553
|
const { Capacitor } = await import("../../src/capacitor/capacitor.js");
|
|
554
554
|
const cap = await Capacitor.create(PKG_PATH, {
|
|
555
555
|
appId: "com.test.app",
|
|
@@ -567,10 +567,14 @@ describe("Android 네이티브 설정", () => {
|
|
|
567
567
|
call[1].includes('parent="Theme.AppCompat.DayNight.NoActionBar"'),
|
|
568
568
|
);
|
|
569
569
|
expect(stylesWrite).toBeDefined();
|
|
570
|
-
|
|
571
|
-
|
|
570
|
+
const content = stylesWrite![1] as string;
|
|
571
|
+
// @drawable/splash는 유지
|
|
572
|
+
expect(content).toContain("@drawable/splash");
|
|
572
573
|
// Theme.SplashScreen은 제거됨
|
|
573
|
-
expect(
|
|
574
|
+
expect(content).not.toContain('parent="Theme.SplashScreen"');
|
|
575
|
+
// android:background → android:windowBackground로 변경됨
|
|
576
|
+
expect(content).toContain('"android:windowBackground">@drawable/splash');
|
|
577
|
+
expect(content).not.toContain('"android:background">@drawable/splash');
|
|
574
578
|
});
|
|
575
579
|
|
|
576
580
|
it("이미 변경된 styles.xml은 재변경하지 않는다", async () => {
|
|
@@ -579,7 +583,7 @@ describe("Android 네이티브 설정", () => {
|
|
|
579
583
|
return `<?xml version="1.0" encoding="utf-8"?>
|
|
580
584
|
<resources>
|
|
581
585
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.AppCompat.DayNight.NoActionBar">
|
|
582
|
-
<item name="android:
|
|
586
|
+
<item name="android:windowBackground">@drawable/splash</item>
|
|
583
587
|
</style>
|
|
584
588
|
</resources>`;
|
|
585
589
|
}
|
|
@@ -257,8 +257,8 @@ describe("createClientViteConfig", () => {
|
|
|
257
257
|
|
|
258
258
|
// --- legacyModule esbuild.supported override (Feature 1.4) ---
|
|
259
259
|
|
|
260
|
-
// Acceptance: Scenario "legacyModule: true일 때 esbuild.supported에 import-meta
|
|
261
|
-
it("sets esbuild.supported to disable import-meta
|
|
260
|
+
// Acceptance: Scenario "legacyModule: true일 때 esbuild.supported에 import-meta false 설정"
|
|
261
|
+
it("sets esbuild.supported to disable import-meta when legacyModule is true", async () => {
|
|
262
262
|
const config = await createClientViteConfig({
|
|
263
263
|
...createDefaultOptions(),
|
|
264
264
|
legacyModule: true,
|
|
@@ -268,7 +268,6 @@ describe("createClientViteConfig", () => {
|
|
|
268
268
|
expect(esbuildOpts?.["supported"]).toEqual(
|
|
269
269
|
expect.objectContaining({
|
|
270
270
|
"import-meta": false,
|
|
271
|
-
"dynamic-import": false,
|
|
272
271
|
}),
|
|
273
272
|
);
|
|
274
273
|
});
|
|
@@ -504,6 +503,25 @@ describe("createClientViteConfig", () => {
|
|
|
504
503
|
expect(config.build?.outDir).toMatch(/dist$/);
|
|
505
504
|
});
|
|
506
505
|
|
|
506
|
+
// --- outDir override ---
|
|
507
|
+
|
|
508
|
+
// Acceptance: Scenario "outDir 설정 시 해당 경로로 빌드 출력"
|
|
509
|
+
it("uses custom outDir when provided", async () => {
|
|
510
|
+
const config = await createClientViteConfig({
|
|
511
|
+
...createDefaultOptions(),
|
|
512
|
+
outDir: "/packages/my-client/.capacitor/www",
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(config.build?.outDir).toBe("/packages/my-client/.capacitor/www");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Acceptance: Scenario "outDir 미설정 시 pkgDir/dist 사용"
|
|
519
|
+
it("defaults outDir to pkgDir/dist when not provided", async () => {
|
|
520
|
+
const config = await createClientViteConfig(createDefaultOptions());
|
|
521
|
+
|
|
522
|
+
expect(config.build?.outDir).toMatch(/my-client[\\/]dist$/);
|
|
523
|
+
});
|
|
524
|
+
|
|
507
525
|
// --- exclude (Feature 1.1: vite-exclude-passthrough) ---
|
|
508
526
|
|
|
509
527
|
// Acceptance: Scenario "exclude에 패키지를 지정하면 pre-bundling에서 제외된다"
|