@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.
Files changed (46) hide show
  1. package/dist/angular/vite-angular-plugin.d.ts +5 -1
  2. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  3. package/dist/angular/vite-angular-plugin.js +19 -7
  4. package/dist/angular/vite-angular-plugin.js.map +1 -1
  5. package/dist/capacitor/capacitor.d.ts +10 -4
  6. package/dist/capacitor/capacitor.d.ts.map +1 -1
  7. package/dist/capacitor/capacitor.js +39 -19
  8. package/dist/capacitor/capacitor.js.map +1 -1
  9. package/dist/commands/publish.d.ts.map +1 -1
  10. package/dist/commands/publish.js +7 -1
  11. package/dist/commands/publish.js.map +1 -1
  12. package/dist/engines/ViteEngine.d.ts +8 -2
  13. package/dist/engines/ViteEngine.d.ts.map +1 -1
  14. package/dist/engines/ViteEngine.js +10 -2
  15. package/dist/engines/ViteEngine.js.map +1 -1
  16. package/dist/engines/index.d.ts +4 -0
  17. package/dist/engines/index.d.ts.map +1 -1
  18. package/dist/engines/index.js +2 -0
  19. package/dist/engines/index.js.map +1 -1
  20. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  21. package/dist/orchestrators/BuildOrchestrator.js +8 -40
  22. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  23. package/dist/sd-config.types.d.ts +2 -0
  24. package/dist/sd-config.types.d.ts.map +1 -1
  25. package/dist/utils/vite-config.d.ts +6 -0
  26. package/dist/utils/vite-config.d.ts.map +1 -1
  27. package/dist/utils/vite-config.js +81 -27
  28. package/dist/utils/vite-config.js.map +1 -1
  29. package/dist/workers/client.worker.d.ts +6 -0
  30. package/dist/workers/client.worker.d.ts.map +1 -1
  31. package/dist/workers/client.worker.js +12 -8
  32. package/dist/workers/client.worker.js.map +1 -1
  33. package/package.json +6 -5
  34. package/src/angular/vite-angular-plugin.ts +28 -8
  35. package/src/capacitor/capacitor.ts +47 -25
  36. package/src/engines/ViteEngine.ts +14 -2
  37. package/src/engines/index.ts +6 -0
  38. package/src/orchestrators/BuildOrchestrator.ts +8 -40
  39. package/src/sd-config.types.ts +2 -0
  40. package/src/utils/vite-config.ts +96 -33
  41. package/src/workers/client.worker.ts +18 -8
  42. package/tests/capacitor/capacitor-build.spec.ts +87 -7
  43. package/tests/capacitor/capacitor-init.spec.ts +9 -5
  44. package/tests/orchestrators/build-orchestrator.spec.ts +7 -145
  45. package/tests/utils/vite-config.spec.ts +120 -3
  46. package/tests/workers/client-worker.spec.ts +2 -1
@@ -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
- // PostCSS inline plugin (라이브러리 JS Angular @Component styles)
111
- if (options.postCssPlugins != null && options.postCssPlugins.length > 0) {
108
+ if (options.framework === "solid") {
109
+ plugins.push(solidPlugin());
110
+ } else {
112
111
  plugins.push(
113
- sdPostCssInlinePlugin({ postCssPlugins: options.postCssPlugins }),
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 (transformIndexHtml)
229
+ // polyfills plugin
214
230
  if (options.polyfills != null && options.polyfills.length > 0) {
215
231
  const polyfillImports = options.polyfills;
216
- (config.plugins as PluginOption[]).push({
217
- name: "sd-polyfills",
218
- transformIndexHtml() {
219
- return [
220
- {
221
- tag: "script",
222
- attrs: { type: "module" },
223
- children: polyfillImports.map((p) => `import "${p}";`).join("\n"),
224
- injectTo: "head-prepend" as const,
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/import() 변환 활성화
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: "build",
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
- /** dist/.config.json 생성 */
446
+ /** .config.json 생성 */
436
447
  function writeConfigJson(
437
- pkgDir: string,
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.find((c) => c.command.includes("gradlew"));
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.find((c) => c.command.includes("gradlew"));
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.find((c) => c.command.includes("gradlew"));
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.findIndex((c) => c.command.includes("gradlew"));
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을 사용한다", async () => {
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.includes("gradlew"));
298
- expect(gradleCmd!.command).toContain("gradlew.bat");
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를 Theme.AppCompat.DayNight.NoActionBar로 변경한다", async () => {
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
- // android:background는 유지
571
- expect((stylesWrite![1] as string)).toContain("@drawable/splash");
570
+ const content = stylesWrite![1] as string;
571
+ // @drawable/splash는 유지
572
+ expect(content).toContain("@drawable/splash");
572
573
  // Theme.SplashScreen은 제거됨
573
- expect((stylesWrite![1] as string)).not.toContain('parent="Theme.SplashScreen"');
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:background">@drawable/splash</item>
586
+ <item name="android:windowBackground">@drawable/splash</item>
583
587
  </style>
584
588
  </resources>`;
585
589
  }