@simplysm/sd-cli 14.0.12 → 14.0.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/angular/vite-angular-plugin.d.ts +5 -1
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +19 -7
- 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 +39 -19
- 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 +8 -2
- package/dist/engines/ViteEngine.d.ts.map +1 -1
- package/dist/engines/ViteEngine.js +10 -2
- 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 +8 -40
- package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
- package/dist/sd-config.types.d.ts +2 -0
- package/dist/sd-config.types.d.ts.map +1 -1
- package/dist/utils/vite-config.d.ts +6 -0
- package/dist/utils/vite-config.d.ts.map +1 -1
- package/dist/utils/vite-config.js +81 -27
- package/dist/utils/vite-config.js.map +1 -1
- package/dist/workers/client.worker.d.ts +6 -0
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +12 -8
- package/dist/workers/client.worker.js.map +1 -1
- package/package.json +6 -5
- package/src/angular/vite-angular-plugin.ts +28 -8
- package/src/capacitor/capacitor.ts +47 -25
- package/src/engines/ViteEngine.ts +14 -2
- package/src/engines/index.ts +6 -0
- package/src/orchestrators/BuildOrchestrator.ts +8 -40
- package/src/sd-config.types.ts +2 -0
- package/src/utils/vite-config.ts +96 -33
- package/src/workers/client.worker.ts +18 -8
- package/tests/capacitor/capacitor-build.spec.ts +87 -7
- package/tests/capacitor/capacitor-init.spec.ts +9 -5
- package/tests/orchestrators/build-orchestrator.spec.ts +7 -145
- package/tests/utils/vite-config.spec.ts +120 -3
- package/tests/workers/client-worker.spec.ts +2 -1
package/src/utils/vite-config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from "path";
|
|
|
3
3
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
4
4
|
import browserslistToEsbuild from "browserslist-to-esbuild";
|
|
5
5
|
import { sdAngularPlugin } from "../angular/vite-angular-plugin.js";
|
|
6
|
+
import solidPlugin from "vite-plugin-solid";
|
|
6
7
|
import {
|
|
7
8
|
sdScopeWatchPlugin,
|
|
8
9
|
type ScopeWatchReplaceDep,
|
|
@@ -14,6 +15,8 @@ import { generatePwaIcons } from "./generate-pwa-icons.js";
|
|
|
14
15
|
|
|
15
16
|
/** createClientViteConfig 옵션 */
|
|
16
17
|
export interface CreateClientViteConfigOptions {
|
|
18
|
+
/** 클라이언트 프레임워크 선택. 미지정 시 "angular" */
|
|
19
|
+
framework?: "angular" | "solid";
|
|
17
20
|
/** 패키지 디렉토리 경로 */
|
|
18
21
|
pkgDir: string;
|
|
19
22
|
/** 패키지명 (예: "@scope/my-client") */
|
|
@@ -55,6 +58,10 @@ export interface CreateClientViteConfigOptions {
|
|
|
55
58
|
exclude?: string[];
|
|
56
59
|
/** watch 모드 (build.watch 활성화, emptyOutDir: false) */
|
|
57
60
|
watch?: boolean;
|
|
61
|
+
/** 빌드 출력 경로 (미설정 시 pkgDir/dist) */
|
|
62
|
+
outDir?: string;
|
|
63
|
+
/** Vite base 경로 (미설정 시 /{pkgName}/) */
|
|
64
|
+
base?: string;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
@@ -77,7 +84,7 @@ export async function createClientViteConfig(
|
|
|
77
84
|
esbuildTarget = browserslistToEsbuild(queries);
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
// browserslist 정규화 (Angular
|
|
87
|
+
// browserslist 정규화 (Angular 플러그인의 PostCSS 연동용)
|
|
81
88
|
const normalizedBrowserslist =
|
|
82
89
|
options.browserslist != null
|
|
83
90
|
? Array.isArray(options.browserslist)
|
|
@@ -96,22 +103,31 @@ export async function createClientViteConfig(
|
|
|
96
103
|
// plugins
|
|
97
104
|
const plugins: PluginOption[] = [
|
|
98
105
|
tsconfigPaths({ projects: [options.tsconfigPath] }),
|
|
99
|
-
sdAngularPlugin({
|
|
100
|
-
tsconfig: options.tsconfigPath,
|
|
101
|
-
dev: options.mode === "dev",
|
|
102
|
-
onBuildStart: options.onBuildStart,
|
|
103
|
-
onBuild: options.onBuild,
|
|
104
|
-
enableLint: options.enableLint,
|
|
105
|
-
browserslist: normalizedBrowserslist,
|
|
106
|
-
postCssPlugins: options.postCssPlugins,
|
|
107
|
-
}),
|
|
108
106
|
];
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
if (options.framework === "solid") {
|
|
109
|
+
plugins.push(solidPlugin());
|
|
110
|
+
} else {
|
|
112
111
|
plugins.push(
|
|
113
|
-
|
|
112
|
+
sdAngularPlugin({
|
|
113
|
+
tsconfig: options.tsconfigPath,
|
|
114
|
+
dev: options.mode === "dev",
|
|
115
|
+
legacyModule: options.legacyModule,
|
|
116
|
+
sourcemap: options.mode === "dev" || options.watch === true,
|
|
117
|
+
onBuildStart: options.onBuildStart,
|
|
118
|
+
onBuild: options.onBuild,
|
|
119
|
+
enableLint: options.enableLint,
|
|
120
|
+
browserslist: normalizedBrowserslist,
|
|
121
|
+
postCssPlugins: options.postCssPlugins,
|
|
122
|
+
}),
|
|
114
123
|
);
|
|
124
|
+
|
|
125
|
+
// PostCSS inline plugin (Angular @Component inline styles 전용)
|
|
126
|
+
if (options.postCssPlugins != null && options.postCssPlugins.length > 0) {
|
|
127
|
+
plugins.push(
|
|
128
|
+
sdPostCssInlinePlugin({ postCssPlugins: options.postCssPlugins }),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
// replaceDeps HMR (dev 모드 또는 watch 모드)
|
|
@@ -153,7 +169,7 @@ export async function createClientViteConfig(
|
|
|
153
169
|
|
|
154
170
|
const config: InlineConfig = {
|
|
155
171
|
root: options.pkgDir,
|
|
156
|
-
base: `/${name}/`,
|
|
172
|
+
base: options.base ?? `/${name}/`,
|
|
157
173
|
define: Object.keys(define).length > 0 ? define : undefined,
|
|
158
174
|
plugins,
|
|
159
175
|
server: serverConfig,
|
|
@@ -210,31 +226,57 @@ export async function createClientViteConfig(
|
|
|
210
226
|
);
|
|
211
227
|
}
|
|
212
228
|
|
|
213
|
-
// polyfills plugin
|
|
229
|
+
// polyfills plugin
|
|
214
230
|
if (options.polyfills != null && options.polyfills.length > 0) {
|
|
215
231
|
const polyfillImports = options.polyfills;
|
|
216
|
-
(
|
|
217
|
-
|
|
218
|
-
transformIndexHtml
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
if (options.legacyModule === true) {
|
|
233
|
+
// legacyModule: 메인 엔트리의 transform에서 polyfill import를 상단에 주입한다.
|
|
234
|
+
// transformIndexHtml의 인라인 <script type="module">은 Vite 빌드에서 번들링되지 않으므로,
|
|
235
|
+
// Rollup이 처리할 수 있도록 소스 코드 레벨에서 주입한다.
|
|
236
|
+
const mainEntryPath = path.resolve(options.pkgDir, "src/main.ts");
|
|
237
|
+
(config.plugins as PluginOption[]).push({
|
|
238
|
+
name: "sd-polyfills",
|
|
239
|
+
transform(code, id) {
|
|
240
|
+
if (path.normalize(id) !== path.normalize(mainEntryPath)) return null;
|
|
241
|
+
// polyfillImports는 pkgDir 기준 상대경로 (예: "./src/polyfills.ts")
|
|
242
|
+
// main.ts는 src/ 안에 있으므로, main.ts 기준 상대경로로 변환한다
|
|
243
|
+
const mainDir = path.dirname(mainEntryPath);
|
|
244
|
+
const polyfillCode = polyfillImports
|
|
245
|
+
.map((p) => {
|
|
246
|
+
const abs = path.resolve(options.pkgDir, p);
|
|
247
|
+
this.addWatchFile(abs);
|
|
248
|
+
const rel = path.relative(mainDir, abs);
|
|
249
|
+
const posixRel = rel.replace(/\\/g, "/");
|
|
250
|
+
return "import \"./" + posixRel + "\";";
|
|
251
|
+
})
|
|
252
|
+
.join("\n");
|
|
253
|
+
return { code: polyfillCode + "\n" + code, map: null };
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
} else {
|
|
257
|
+
// dev 모드: transformIndexHtml로 인라인 스크립트 주입 (Vite dev server가 처리)
|
|
258
|
+
(config.plugins as PluginOption[]).push({
|
|
259
|
+
name: "sd-polyfills",
|
|
260
|
+
transformIndexHtml() {
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
tag: "script",
|
|
264
|
+
attrs: { type: "module" },
|
|
265
|
+
children: polyfillImports.map((p) => `import "${p}";`).join("\n"),
|
|
266
|
+
injectTo: "head-prepend" as const,
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
229
272
|
}
|
|
230
273
|
|
|
231
|
-
// legacyModule: true → 코드 스플리팅 비활성화 + esbuild import.meta
|
|
274
|
+
// legacyModule: true → 코드 스플리팅 비활성화 + esbuild import.meta 변환 + 잔여 import() 제거
|
|
232
275
|
if (options.legacyModule === true) {
|
|
233
276
|
config.esbuild = {
|
|
234
277
|
...config.esbuild,
|
|
235
278
|
supported: {
|
|
236
279
|
"import-meta": false,
|
|
237
|
-
"dynamic-import": false,
|
|
238
280
|
},
|
|
239
281
|
};
|
|
240
282
|
config.build = {
|
|
@@ -245,17 +287,38 @@ export async function createClientViteConfig(
|
|
|
245
287
|
},
|
|
246
288
|
},
|
|
247
289
|
};
|
|
290
|
+
|
|
291
|
+
// Rollup이 인라인하지 못한 잔여 dynamic import()를 제거한다.
|
|
292
|
+
// inlineDynamicImports가 정적 경로를 모두 인라인한 후에도,
|
|
293
|
+
// @vite-ignore나 런타임 계산 경로의 import()가 남을 수 있다.
|
|
294
|
+
// Chrome 61은 import() 구문을 파싱하지 못하므로 no-op 함수로 치환한다.
|
|
295
|
+
(config.plugins as PluginOption[]).push({
|
|
296
|
+
name: "sd-legacy-strip-dynamic-import",
|
|
297
|
+
enforce: "post",
|
|
298
|
+
renderChunk(code) {
|
|
299
|
+
if (!code.includes("import(")) return null;
|
|
300
|
+
return {
|
|
301
|
+
code: code.replace(
|
|
302
|
+
/\bimport\s*\(/g,
|
|
303
|
+
"(function(){return Promise.reject(new Error(\"Dynamic import not supported\"))})(",
|
|
304
|
+
),
|
|
305
|
+
map: null,
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
});
|
|
248
309
|
}
|
|
249
310
|
|
|
250
|
-
// build 모드 설정
|
|
251
|
-
if (options.mode === "build") {
|
|
311
|
+
// build 모드 설정 (프로덕션 빌드 또는 legacyModule dev)
|
|
312
|
+
if (options.mode === "build" || options.legacyModule === true) {
|
|
252
313
|
config.build = {
|
|
253
314
|
...config.build,
|
|
254
|
-
outDir: path.join(options.pkgDir, "dist"),
|
|
315
|
+
outDir: options.outDir ?? path.join(options.pkgDir, "dist"),
|
|
255
316
|
};
|
|
256
317
|
if (options.watch === true) {
|
|
257
318
|
config.build.watch = {};
|
|
258
319
|
config.build.emptyOutDir = false;
|
|
320
|
+
config.build.minify = false;
|
|
321
|
+
config.build.sourcemap = true;
|
|
259
322
|
} else {
|
|
260
323
|
config.logLevel = "silent";
|
|
261
324
|
config.build.emptyOutDir = true;
|
|
@@ -21,6 +21,8 @@ export interface ClientBuildInfo {
|
|
|
21
21
|
name: string;
|
|
22
22
|
cwd: string;
|
|
23
23
|
pkgDir: string;
|
|
24
|
+
/** 클라이언트 프레임워크 선택 */
|
|
25
|
+
framework?: "angular" | "solid";
|
|
24
26
|
/** Vite dev server 포트 (standalone clients with server: number) */
|
|
25
27
|
port?: number;
|
|
26
28
|
/** 빌드 시 치환할 환경변수 */
|
|
@@ -37,6 +39,10 @@ export interface ClientBuildInfo {
|
|
|
37
39
|
enableLint?: boolean;
|
|
38
40
|
/** Vite optimizeDeps.exclude에 전달할 패키지 목록 */
|
|
39
41
|
exclude?: string[];
|
|
42
|
+
/** 빌드 출력 경로 (미설정 시 pkgDir/dist) */
|
|
43
|
+
outDir?: string;
|
|
44
|
+
/** Vite base 경로 (미설정 시 /{pkgName}/) */
|
|
45
|
+
base?: string;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/** Client 빌드 결과 */
|
|
@@ -199,6 +205,7 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
199
205
|
const polyfills = fs.existsSync(polyfillsPath) ? ["./src/polyfills.ts"] : undefined;
|
|
200
206
|
|
|
201
207
|
const viteConfig = await createClientViteConfig({
|
|
208
|
+
framework: info.framework,
|
|
202
209
|
pkgDir: info.pkgDir,
|
|
203
210
|
pkgName,
|
|
204
211
|
mode: "dev",
|
|
@@ -241,7 +248,7 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
241
248
|
sender.send("serverReady", { port: actualPort });
|
|
242
249
|
|
|
243
250
|
// .config.json 생성
|
|
244
|
-
writeConfigJson(info.pkgDir, info.configs);
|
|
251
|
+
writeConfigJson(path.join(info.pkgDir, "dist"), info.configs);
|
|
245
252
|
|
|
246
253
|
return { success: true };
|
|
247
254
|
} catch (err) {
|
|
@@ -269,9 +276,10 @@ async function startLegacyWatch(info: ClientBuildInfo): Promise<ClientBuildResul
|
|
|
269
276
|
const polyfills = fs.existsSync(polyfillsPath) ? ["./src/polyfills.ts"] : undefined;
|
|
270
277
|
|
|
271
278
|
const viteConfig = await createClientViteConfig({
|
|
279
|
+
framework: info.framework,
|
|
272
280
|
pkgDir: info.pkgDir,
|
|
273
281
|
pkgName,
|
|
274
|
-
mode: "
|
|
282
|
+
mode: "dev",
|
|
275
283
|
tsconfigPath,
|
|
276
284
|
serverPort: 0,
|
|
277
285
|
env: info.env,
|
|
@@ -293,7 +301,7 @@ async function startLegacyWatch(info: ClientBuildInfo): Promise<ClientBuildResul
|
|
|
293
301
|
rollupWatcher = watcher;
|
|
294
302
|
|
|
295
303
|
// .config.json 생성
|
|
296
|
-
writeConfigJson(info.pkgDir, info.configs);
|
|
304
|
+
writeConfigJson(path.join(info.pkgDir, "dist"), info.configs);
|
|
297
305
|
|
|
298
306
|
// HTTP 정적 파일 서버 시작
|
|
299
307
|
const name = pkgName.replace(/^@[^/]+\//, "");
|
|
@@ -398,6 +406,7 @@ async function build(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
398
406
|
let lintResult: LintWithProgramResult | undefined;
|
|
399
407
|
|
|
400
408
|
const viteConfig = await createClientViteConfig({
|
|
409
|
+
framework: info.framework,
|
|
401
410
|
pkgDir: info.pkgDir,
|
|
402
411
|
pkgName,
|
|
403
412
|
mode: "build",
|
|
@@ -416,12 +425,14 @@ async function build(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
416
425
|
polyfills,
|
|
417
426
|
pwa: info.pwa,
|
|
418
427
|
exclude: info.exclude,
|
|
428
|
+
outDir: info.outDir,
|
|
429
|
+
base: info.base,
|
|
419
430
|
});
|
|
420
431
|
|
|
421
432
|
await viteBuild(viteConfig);
|
|
422
433
|
|
|
423
|
-
// .config.json 생성
|
|
424
|
-
writeConfigJson(info.pkgDir, info.configs);
|
|
434
|
+
// .config.json 생성 (항상 dist/에 기록 — outDir과 무관)
|
|
435
|
+
writeConfigJson(path.join(info.pkgDir, "dist"), info.configs);
|
|
425
436
|
|
|
426
437
|
logger.debug(`[${info.name}] client worker build 완료`);
|
|
427
438
|
return { success: true, lint: lintResult };
|
|
@@ -432,12 +443,11 @@ async function build(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
432
443
|
}
|
|
433
444
|
}
|
|
434
445
|
|
|
435
|
-
/**
|
|
446
|
+
/** .config.json 생성 */
|
|
436
447
|
function writeConfigJson(
|
|
437
|
-
|
|
448
|
+
distDir: string,
|
|
438
449
|
configs?: Record<string, unknown>,
|
|
439
450
|
): void {
|
|
440
|
-
const distDir = path.join(pkgDir, "dist");
|
|
441
451
|
fs.mkdirSync(distDir, { recursive: true });
|
|
442
452
|
fs.writeFileSync(
|
|
443
453
|
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
|
}
|