@simplysm/sd-cli 14.0.18 → 14.0.19

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 (49) hide show
  1. package/dist/angular/vite-angular-plugin.d.ts +2 -0
  2. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  3. package/dist/angular/vite-angular-plugin.js +57 -28
  4. package/dist/angular/vite-angular-plugin.js.map +1 -1
  5. package/dist/capacitor/capacitor.d.ts +0 -1
  6. package/dist/capacitor/capacitor.d.ts.map +1 -1
  7. package/dist/capacitor/capacitor.js +12 -37
  8. package/dist/capacitor/capacitor.js.map +1 -1
  9. package/dist/commands/device.d.ts.map +1 -1
  10. package/dist/commands/device.js +3 -2
  11. package/dist/commands/device.js.map +1 -1
  12. package/dist/electron/electron.d.ts.map +1 -1
  13. package/dist/electron/electron.js +9 -4
  14. package/dist/electron/electron.js.map +1 -1
  15. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  16. package/dist/orchestrators/DevWatchOrchestrator.js +12 -0
  17. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  18. package/dist/utils/vite-config.d.ts +1 -1
  19. package/dist/utils/vite-config.d.ts.map +1 -1
  20. package/dist/utils/vite-config.js +76 -26
  21. package/dist/utils/vite-config.js.map +1 -1
  22. package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
  23. package/dist/utils/vite-scope-watch-plugin.js +7 -1
  24. package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
  25. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  26. package/dist/workers/server-runtime.worker.js +15 -0
  27. package/dist/workers/server-runtime.worker.js.map +1 -1
  28. package/package.json +9 -7
  29. package/src/angular/vite-angular-plugin.ts +88 -34
  30. package/src/capacitor/capacitor.ts +14 -46
  31. package/src/commands/device.ts +3 -2
  32. package/src/electron/electron.ts +11 -4
  33. package/src/orchestrators/DevWatchOrchestrator.ts +14 -0
  34. package/src/utils/vite-config.ts +83 -27
  35. package/src/utils/vite-scope-watch-plugin.ts +6 -1
  36. package/src/workers/server-runtime.worker.ts +15 -0
  37. package/tests/angular/linker-disk-cache.spec.ts +31 -25
  38. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
  39. package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
  40. package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +108 -0
  41. package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
  42. package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +10 -15
  43. package/tests/angular/vite-angular-plugin.spec.ts +80 -15
  44. package/tests/capacitor/capacitor-workspace.spec.ts +22 -12
  45. package/tests/commands/device.spec.ts +12 -7
  46. package/tests/electron/electron.spec.ts +27 -2
  47. package/tests/utils/vite-config.spec.ts +255 -133
  48. package/tests/utils/vite-scope-watch-plugin.spec.ts +22 -0
  49. package/tests/workers/server-runtime-worker.spec.ts +48 -4
@@ -2,6 +2,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
 
3
3
  // --- Mock factories ---
4
4
 
5
+ vi.mock("vite-tsconfig-paths", () => ({
6
+ default: vi.fn(() => ({ name: "vite-tsconfig-paths" })),
7
+ }));
8
+
5
9
  const mockSdAngularPlugin = vi.fn(() => ({ name: "sd-angular-plugin" }));
6
10
  vi.mock("../../src/angular/vite-angular-plugin.js", () => ({
7
11
  sdAngularPlugin: mockSdAngularPlugin,
@@ -31,9 +35,14 @@ vi.mock("browserslist-to-esbuild", () => ({
31
35
  }),
32
36
  }));
33
37
 
34
- const mockSdPwaPlugin = vi.fn(() => ({ name: "sd-pwa" }));
35
- vi.mock("../../src/utils/vite-pwa-plugin.js", () => ({
36
- sdPwaPlugin: mockSdPwaPlugin,
38
+ const mockVitePWA = vi.fn(() => ({ name: "vite-plugin-pwa" }));
39
+ vi.mock("vite-plugin-pwa", () => ({
40
+ VitePWA: mockVitePWA,
41
+ }));
42
+
43
+ const mockGeneratePwaIcons = vi.fn().mockResolvedValue([]);
44
+ vi.mock("../../src/utils/generate-pwa-icons.js", () => ({
45
+ generatePwaIcons: mockGeneratePwaIcons,
37
46
  }));
38
47
 
39
48
  // --- Dynamic import ---
@@ -64,8 +73,8 @@ afterEach(() => {
64
73
 
65
74
  describe("createClientViteConfig", () => {
66
75
  // Acceptance: Scenario "define['process.env'] 제거"
67
- it("does not include process.env in define, only import.meta.env keys", () => {
68
- const config = createClientViteConfig({
76
+ it("does not include process.env in define, only import.meta.env keys", async () => {
77
+ const config = await createClientViteConfig({
69
78
  ...createDefaultOptions(),
70
79
  env: { DEV: "true", VER: "1.0.0" },
71
80
  });
@@ -76,64 +85,63 @@ describe("createClientViteConfig", () => {
76
85
  expect(define["import.meta.env.VER"]).toBe('"1.0.0"');
77
86
  });
78
87
 
79
- // Acceptance: Scenario "기본 target 설정"
80
- it("uses es2022 build target when no browserslist is provided", () => {
81
- const config = createClientViteConfig(createDefaultOptions());
88
+ // Acceptance: Scenario "browserslist 미설정 시 최신 브라우저 유지"
89
+ it("uses es2022 esbuild target when no browserslist is provided", async () => {
90
+ const config = await createClientViteConfig(createDefaultOptions());
82
91
 
83
- expect(config.build?.target).toBe("es2022");
84
- expect(config.esbuild).toBeUndefined();
92
+ expect(config.esbuild).toEqual({ target: "es2022" });
85
93
  });
86
94
 
87
95
  // Acceptance: Scenario "postCss 미설정 시 처리 없음"
88
- it("does not set css.postcss when no postCssPlugins are provided", () => {
89
- const config = createClientViteConfig(createDefaultOptions());
96
+ it("does not set css.postcss when no postCssPlugins are provided", async () => {
97
+ const config = await createClientViteConfig(createDefaultOptions());
90
98
 
91
99
  expect(config.css).toBeUndefined();
92
100
  });
93
101
 
94
102
  // Acceptance: Scenario "polyfills.ts 파일 미존재 시 주입 없음"
95
- it("does not add polyfills plugin when no polyfills are provided", () => {
96
- const config = createClientViteConfig(createDefaultOptions());
103
+ it("does not add polyfills plugin when no polyfills are provided", async () => {
104
+ const config = await createClientViteConfig(createDefaultOptions());
97
105
 
98
106
  const plugins = config.plugins as Array<{ name: string }>;
99
107
  const polyfillPlugin = plugins.find((p) => p.name === "sd-polyfills");
100
108
  expect(polyfillPlugin).toBeUndefined();
101
109
  });
102
110
 
103
- // Acceptance: Scenario "browserslist target 설정"
104
- it("converts browserslist string to build target", () => {
105
- const config = createClientViteConfig({
111
+ // Acceptance: Scenario "browserslist 문자열 설정 시 해당 타겟으로 변환"
112
+ it("converts browserslist string to esbuild target", async () => {
113
+ const config = await createClientViteConfig({
106
114
  ...createDefaultOptions(),
107
115
  browserslist: "last 2 Chrome versions",
108
116
  });
109
117
 
110
- expect(config.build?.target).toEqual(["chrome120", "chrome119"]);
118
+ expect((config.esbuild as { target: string | string[] }).target).toEqual(["chrome120", "chrome119"]);
111
119
  });
112
120
 
113
121
  // Acceptance: Scenario "browserslist 배열 설정"
114
- it("converts browserslist array to build target", () => {
115
- const config = createClientViteConfig({
122
+ it("converts browserslist array to esbuild target", async () => {
123
+ const config = await createClientViteConfig({
116
124
  ...createDefaultOptions(),
117
125
  browserslist: ["ie 11", "last 2 versions"],
118
126
  });
119
127
 
120
- expect(config.build?.target).toEqual(["es2015"]);
128
+ expect((config.esbuild as { target: string | string[] }).target).toEqual(["es2015"]);
121
129
  });
122
130
 
123
- // Acceptance: Scenario "dev 모드에서도 target 적용"
124
- it("applies browserslist build target in dev mode too", () => {
125
- const config = createClientViteConfig({
131
+ // Acceptance: Scenario "dev server에서도 browserslist 적용"
132
+ it("applies browserslist in dev mode too", async () => {
133
+ const config = await createClientViteConfig({
126
134
  ...createDefaultOptions(),
127
135
  mode: "dev",
128
136
  browserslist: "last 2 Chrome versions",
129
137
  });
130
138
 
131
- expect(config.build?.target).toEqual(["chrome120", "chrome119"]);
139
+ expect((config.esbuild as { target: string | string[] }).target).toEqual(["chrome120", "chrome119"]);
132
140
  });
133
141
 
134
142
  // Unit: browserslist is passed to sdAngularPlugin
135
- it("passes browserslist to sdAngularPlugin as normalized array", () => {
136
- createClientViteConfig({
143
+ it("passes browserslist to sdAngularPlugin as normalized array", async () => {
144
+ await createClientViteConfig({
137
145
  ...createDefaultOptions(),
138
146
  browserslist: "last 2 Chrome versions",
139
147
  });
@@ -146,9 +154,9 @@ describe("createClientViteConfig", () => {
146
154
  });
147
155
 
148
156
  // Acceptance: Scenario ".scss 파일에 PostCSS 적용"
149
- it("sets css.postcss when postCssPlugins are provided", () => {
157
+ it("sets css.postcss when postCssPlugins are provided", async () => {
150
158
  const fakePlugin = { postcssPlugin: "autoprefixer" };
151
- const config = createClientViteConfig({
159
+ const config = await createClientViteConfig({
152
160
  ...createDefaultOptions(),
153
161
  postCssPlugins: [fakePlugin],
154
162
  });
@@ -157,9 +165,9 @@ describe("createClientViteConfig", () => {
157
165
  });
158
166
 
159
167
  // Unit: postCssPlugins is passed to sdAngularPlugin
160
- it("passes postCssPlugins to sdAngularPlugin", () => {
168
+ it("passes postCssPlugins to sdAngularPlugin", async () => {
161
169
  const fakePlugin = { postcssPlugin: "autoprefixer" };
162
- createClientViteConfig({
170
+ await createClientViteConfig({
163
171
  ...createDefaultOptions(),
164
172
  postCssPlugins: [fakePlugin],
165
173
  });
@@ -172,9 +180,9 @@ describe("createClientViteConfig", () => {
172
180
  });
173
181
 
174
182
  // Acceptance: Scenario "Angular 라이브러리 번들 JS 내 인라인 CSS에 PostCSS 적용"
175
- it("adds sdPostCssInlinePlugin when postCssPlugins are provided", () => {
183
+ it("adds sdPostCssInlinePlugin when postCssPlugins are provided", async () => {
176
184
  const fakePlugin = { postcssPlugin: "autoprefixer" };
177
- const config = createClientViteConfig({
185
+ const config = await createClientViteConfig({
178
186
  ...createDefaultOptions(),
179
187
  postCssPlugins: [fakePlugin],
180
188
  });
@@ -185,8 +193,8 @@ describe("createClientViteConfig", () => {
185
193
  });
186
194
 
187
195
  // Unit: no sdPostCssInlinePlugin when empty postCssPlugins
188
- it("does not add sdPostCssInlinePlugin when postCssPlugins is empty", () => {
189
- const config = createClientViteConfig({
196
+ it("does not add sdPostCssInlinePlugin when postCssPlugins is empty", async () => {
197
+ const config = await createClientViteConfig({
190
198
  ...createDefaultOptions(),
191
199
  postCssPlugins: [],
192
200
  });
@@ -197,8 +205,8 @@ describe("createClientViteConfig", () => {
197
205
  });
198
206
 
199
207
  // Acceptance: Scenario "polyfills.ts 파일 존재 시 자동 주입"
200
- it("adds sd-polyfills plugin when polyfills are provided", () => {
201
- const config = createClientViteConfig({
208
+ it("adds sd-polyfills plugin when polyfills are provided", async () => {
209
+ const config = await createClientViteConfig({
202
210
  ...createDefaultOptions(),
203
211
  polyfills: ["./src/polyfills.ts"],
204
212
  });
@@ -210,131 +218,245 @@ describe("createClientViteConfig", () => {
210
218
 
211
219
  // --- legacyModule (Feature 1.1) ---
212
220
 
213
- // Acceptance: Scenario "legacyModule에서 inlineDynamicImports"
214
- it("enables inlineDynamicImports and import.meta strip plugin when legacyModule is true", () => {
215
- const config = createClientViteConfig({
221
+ // Acceptance: Scenario "legacyModule 활성화 시 inlineDynamicImports만 설정한다"
222
+ it("enables inlineDynamicImports without import.meta plugin when legacyModule is true", async () => {
223
+ const config = await createClientViteConfig({
216
224
  ...createDefaultOptions(),
217
225
  legacyModule: true,
218
226
  });
219
227
 
220
- // inlineDynamicImports가 활성화된다 (rolldownOptions)
221
- expect((config.build as any)?.rolldownOptions?.output?.inlineDynamicImports).toBe(true);
222
- // import.meta 치환 플러그인이 존재한다
228
+ // inlineDynamicImports가 활성화된다
229
+ expect((config.build as any)?.rollupOptions?.output?.inlineDynamicImports).toBe(true);
230
+ // import.meta 치환 플러그인이 없다 (esbuild target이 자동 치환)
223
231
  const plugins = config.plugins as Array<{ name: string }>;
224
- const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-strip-import-meta");
225
- expect(legacyPlugin).toBeDefined();
232
+ const legacyPlugin = plugins.find((p) => p.name === "sd-legacy-import-meta");
233
+ expect(legacyPlugin).toBeUndefined();
226
234
  });
227
235
 
228
- // Acceptance: Scenario "legacyModule 미사용rolldownOptions 없음"
229
- it("does not set inlineDynamicImports when legacyModule is not set", () => {
230
- const config = createClientViteConfig(createDefaultOptions());
236
+ // Acceptance: Scenario "legacyModule 미설정코드 분할이 기본 동작한다"
237
+ it("does not set inlineDynamicImports when legacyModule is not set", async () => {
238
+ const config = await createClientViteConfig(createDefaultOptions());
231
239
 
232
240
  // 코드 분할이 기본 동작한다
233
- expect(config.build?.rolldownOptions).toBeUndefined();
241
+ expect(config.build?.rollupOptions).toBeUndefined();
242
+ });
243
+
244
+ // Acceptance: Scenario "legacyModule: true는 inlineDynamicImports를 활성화한다"
245
+ it("legacyModule: true provides inlineDynamicImports (splitting replacement)", async () => {
246
+ const config = await createClientViteConfig({
247
+ ...createDefaultOptions(),
248
+ legacyModule: true,
249
+ });
250
+
251
+ // 기존 splitting: false와 동일한 inlineDynamicImports 동작
252
+ expect((config.build as any)?.rollupOptions?.output?.inlineDynamicImports).toBe(true);
234
253
  });
235
254
 
236
- // --- legacyModule import.meta strip (Vite 8 migration) ---
255
+ // --- legacyModule esbuild.supported override (Feature 1.4) ---
237
256
 
238
- // Acceptance: Scenario "legacyModule import.meta 치환 플러그인 존재"
239
- it("adds sd-legacy-strip-import-meta plugin when legacyModule is true", () => {
240
- const config = createClientViteConfig({
257
+ // Acceptance: Scenario "legacyModule: true일 때 esbuild.supported에 import-meta false 설정"
258
+ it("sets esbuild.supported to disable import-meta when legacyModule is true", async () => {
259
+ const config = await createClientViteConfig({
241
260
  ...createDefaultOptions(),
242
261
  legacyModule: true,
243
262
  });
244
263
 
245
- const plugins = config.plugins as Array<{ name: string }>;
246
- expect(plugins.find((p) => p.name === "sd-legacy-strip-import-meta")).toBeDefined();
264
+ const esbuildOpts = config.esbuild as Record<string, unknown> | undefined;
265
+ expect(esbuildOpts?.["supported"]).toEqual(
266
+ expect.objectContaining({
267
+ "import-meta": false,
268
+ }),
269
+ );
247
270
  });
248
271
 
249
- // Acceptance: Scenario "legacyModule 미사용치환 플러그인 없음"
250
- it("does not add sd-legacy-strip-import-meta plugin when legacyModule is not specified", () => {
251
- const config = createClientViteConfig(createDefaultOptions());
272
+ // Acceptance: Scenario "legacyModule 미설정esbuild.supported 변경 없음"
273
+ it("does not set esbuild.supported when legacyModule is not specified", async () => {
274
+ const config = await createClientViteConfig(createDefaultOptions());
252
275
 
253
- const plugins = config.plugins as Array<{ name: string }>;
254
- expect(plugins.find((p) => p.name === "sd-legacy-strip-import-meta")).toBeUndefined();
276
+ const esbuildOpts = config.esbuild as Record<string, unknown> | undefined;
277
+ expect(esbuildOpts?.["supported"]).toBeUndefined();
255
278
  });
256
279
 
257
280
  // --- PWA (Feature 5.2) ---
258
281
 
259
- // Acceptance: Scenario "build 모드에서 기본 활성화"
260
- it("adds sdPwaPlugin in build mode when pwa is not specified", () => {
261
- const config = createClientViteConfig(createDefaultOptions());
282
+ // Acceptance: Scenario "기본 PWA 활성화"
283
+ it("adds VitePWA plugin in build mode when pwa is not specified (default enabled)", async () => {
284
+ const config = await createClientViteConfig(createDefaultOptions());
262
285
 
263
286
  const plugins = config.plugins as Array<{ name: string }>;
264
- const pwaPlugin = plugins.find((p) => p.name === "sd-pwa");
287
+ const pwaPlugin = plugins.find((p) => p.name === "vite-plugin-pwa");
265
288
  expect(pwaPlugin).toBeDefined();
266
289
  });
267
290
 
268
- // Acceptance: Scenario "pwa false로 비활성화"
269
- it("does not add sdPwaPlugin when pwa is false", () => {
270
- const config = createClientViteConfig({
291
+ // Acceptance: Scenario "PWA 명시적 비활성화"
292
+ it("does not add VitePWA plugin when pwa is false", async () => {
293
+ const config = await createClientViteConfig({
271
294
  ...createDefaultOptions(),
272
295
  pwa: false,
273
296
  });
274
297
 
275
298
  const plugins = config.plugins as Array<{ name: string }>;
276
- const pwaPlugin = plugins.find((p) => p.name === "sd-pwa");
299
+ const pwaPlugin = plugins.find((p) => p.name === "vite-plugin-pwa");
277
300
  expect(pwaPlugin).toBeUndefined();
278
301
  });
279
302
 
280
- // Acceptance: Scenario "dev 모드에서 비활성화"
281
- it("does not add sdPwaPlugin in dev mode", () => {
282
- const config = createClientViteConfig({
303
+ // Acceptance: Scenario "dev 모드에서 service worker 미등록"
304
+ it("does not add VitePWA plugin in dev mode", async () => {
305
+ const config = await createClientViteConfig({
283
306
  ...createDefaultOptions(),
284
307
  mode: "dev",
285
308
  });
286
309
 
287
310
  const plugins = config.plugins as Array<{ name: string }>;
288
- const pwaPlugin = plugins.find((p) => p.name === "sd-pwa");
311
+ const pwaPlugin = plugins.find((p) => p.name === "vite-plugin-pwa");
289
312
  expect(pwaPlugin).toBeUndefined();
290
313
  });
291
314
 
292
- // Acceptance: Scenario "pwa 객체 전달"
293
- it("passes pwa config object to sdPwaPlugin", () => {
294
- createClientViteConfig({
315
+ // Acceptance: Scenario "manifest 필드 커스텀"
316
+ it("passes custom manifest fields to VitePWA plugin", async () => {
317
+ await createClientViteConfig({
295
318
  ...createDefaultOptions(),
296
319
  pwa: {
297
320
  manifest: { name: "My App", theme_color: "#000000" },
298
321
  },
299
322
  });
300
323
 
301
- expect(mockSdPwaPlugin).toHaveBeenCalledWith({
302
- pkgDir: "/packages/my-client",
303
- pkgName: "my-client",
304
- pwa: { manifest: { name: "My App", theme_color: "#000000" } },
305
- });
324
+ expect(mockVitePWA).toHaveBeenCalledWith(
325
+ expect.objectContaining({
326
+ manifest: expect.objectContaining({
327
+ name: "My App",
328
+ theme_color: "#000000",
329
+ short_name: "my-client",
330
+ display: "standalone",
331
+ background_color: "#ffffff",
332
+ }),
333
+ }),
334
+ );
306
335
  });
307
336
 
308
- // Unit: pwa undefined passes undefined to sdPwaPlugin
309
- it("passes undefined pwa to sdPwaPlugin when pwa is not specified", () => {
310
- createClientViteConfig(createDefaultOptions());
337
+ // Acceptance: Scenario "기본 Workbox 캐싱"
338
+ it("uses default workbox globPatterns when pwa.workbox is not specified", async () => {
339
+ await createClientViteConfig(createDefaultOptions());
340
+
341
+ expect(mockVitePWA).toHaveBeenCalledWith(
342
+ expect.objectContaining({
343
+ workbox: {
344
+ globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2}"],
345
+ },
346
+ }),
347
+ );
348
+ });
311
349
 
312
- expect(mockSdPwaPlugin).toHaveBeenCalledWith({
313
- pkgDir: "/packages/my-client",
314
- pkgName: "my-client",
315
- pwa: undefined,
350
+ // Acceptance: Scenario "Workbox globPatterns 커스텀"
351
+ it("uses custom workbox globPatterns when specified", async () => {
352
+ await createClientViteConfig({
353
+ ...createDefaultOptions(),
354
+ pwa: {
355
+ workbox: { globPatterns: ["**/*.{js,css,html,json}"] },
356
+ },
316
357
  });
358
+
359
+ expect(mockVitePWA).toHaveBeenCalledWith(
360
+ expect.objectContaining({
361
+ workbox: {
362
+ globPatterns: ["**/*.{js,css,html,json}"],
363
+ },
364
+ }),
365
+ );
317
366
  });
318
367
 
319
- // Unit: pwa empty object passes empty object to sdPwaPlugin
320
- it("passes empty pwa object to sdPwaPlugin when pwa is empty", () => {
321
- createClientViteConfig({
368
+ // Unit: pwa empty object uses all defaults
369
+ it("uses all defaults when pwa is empty object", async () => {
370
+ await createClientViteConfig({
322
371
  ...createDefaultOptions(),
323
372
  pwa: {},
324
373
  });
325
374
 
326
- expect(mockSdPwaPlugin).toHaveBeenCalledWith({
327
- pkgDir: "/packages/my-client",
328
- pkgName: "my-client",
329
- pwa: {},
375
+ expect(mockVitePWA).toHaveBeenCalledWith(
376
+ expect.objectContaining({
377
+ manifest: expect.objectContaining({
378
+ name: "my-client",
379
+ display: "standalone",
380
+ }),
381
+ }),
382
+ );
383
+ });
384
+
385
+ // Unit: pwa manifest custom icons overrides default
386
+ it("includes custom icons in manifest and skips auto-generation", async () => {
387
+ const icons = [{ src: "/icon-192.png", sizes: "192x192", type: "image/png" }];
388
+ await createClientViteConfig({
389
+ ...createDefaultOptions(),
390
+ pwa: { manifest: { icons } },
330
391
  });
392
+
393
+ expect(mockVitePWA).toHaveBeenCalledWith(
394
+ expect.objectContaining({
395
+ manifest: expect.objectContaining({ icons }),
396
+ }),
397
+ );
398
+ expect(mockGeneratePwaIcons).not.toHaveBeenCalled();
399
+ });
400
+
401
+ // Acceptance: Scenario "기본 아이콘 자동 생성"
402
+ it("calls generatePwaIcons and includes result in manifest", async () => {
403
+ mockGeneratePwaIcons.mockResolvedValue([
404
+ { src: "icons/icon-192x192.png", sizes: "192x192", type: "image/png" },
405
+ { src: "icons/icon-512x512.png", sizes: "512x512", type: "image/png" },
406
+ ]);
407
+
408
+ await createClientViteConfig(createDefaultOptions());
409
+
410
+ expect(mockGeneratePwaIcons).toHaveBeenCalledWith("/packages/my-client");
411
+ expect(mockVitePWA).toHaveBeenCalledWith(
412
+ expect.objectContaining({
413
+ manifest: expect.objectContaining({
414
+ icons: [
415
+ { src: "icons/icon-192x192.png", sizes: "192x192", type: "image/png" },
416
+ { src: "icons/icon-512x512.png", sizes: "512x512", type: "image/png" },
417
+ ],
418
+ }),
419
+ }),
420
+ );
421
+ });
422
+
423
+ // Acceptance: Scenario "원본 아이콘 파일이 없을 때"
424
+ it("does not include icons in manifest when no source icon exists", async () => {
425
+ mockGeneratePwaIcons.mockResolvedValue([]);
426
+
427
+ await createClientViteConfig(createDefaultOptions());
428
+
429
+ expect(mockVitePWA).toHaveBeenCalledWith(
430
+ expect.objectContaining({
431
+ manifest: expect.not.objectContaining({ icons: expect.anything() }),
432
+ }),
433
+ );
434
+ });
435
+
436
+ // Acceptance: Scenario "pwa 필드 미설정 시 기본값"
437
+ it("uses default manifest values from pkgName when pwa is undefined", async () => {
438
+ await createClientViteConfig(createDefaultOptions());
439
+
440
+ expect(mockVitePWA).toHaveBeenCalledWith(
441
+ expect.objectContaining({
442
+ registerType: "prompt",
443
+ injectRegister: "script",
444
+ manifest: expect.objectContaining({
445
+ name: "my-client",
446
+ short_name: "my-client",
447
+ display: "standalone",
448
+ theme_color: "#ffffff",
449
+ background_color: "#ffffff",
450
+ }),
451
+ }),
452
+ );
331
453
  });
332
454
 
333
455
  // --- watch option (Feature 1.2: legacy dev mode) ---
334
456
 
335
457
  // Acceptance: Scenario "watch: true 시 build.watch 설정 및 emptyOutDir: false"
336
- it("sets build.watch and emptyOutDir: false when watch is true in build mode", () => {
337
- const config = createClientViteConfig({
458
+ it("sets build.watch and emptyOutDir: false when watch is true in build mode", async () => {
459
+ const config = await createClientViteConfig({
338
460
  ...createDefaultOptions(),
339
461
  mode: "build",
340
462
  watch: true,
@@ -349,7 +471,7 @@ describe("createClientViteConfig", () => {
349
471
  it("includes sdScopeWatchPlugin when watch is true with replaceDeps in build mode", async () => {
350
472
  const { sdScopeWatchPlugin } = await import("../../src/utils/vite-scope-watch-plugin");
351
473
 
352
- const config = createClientViteConfig({
474
+ const config = await createClientViteConfig({
353
475
  ...createDefaultOptions(),
354
476
  mode: "build",
355
477
  watch: true,
@@ -363,8 +485,8 @@ describe("createClientViteConfig", () => {
363
485
  });
364
486
 
365
487
  // Acceptance: Scenario "watch 미설정 시 기존 build 동작 유지"
366
- it("sets emptyOutDir: true and logLevel: silent when watch is not set in build mode", () => {
367
- const config = createClientViteConfig(createDefaultOptions());
488
+ it("sets emptyOutDir: true and logLevel: silent when watch is not set in build mode", async () => {
489
+ const config = await createClientViteConfig(createDefaultOptions());
368
490
 
369
491
  expect(config.build?.emptyOutDir).toBe(true);
370
492
  expect(config.logLevel).toBe("silent");
@@ -372,8 +494,8 @@ describe("createClientViteConfig", () => {
372
494
  });
373
495
 
374
496
  // Unit: watch: true without replaceDeps does not add sdScopeWatchPlugin
375
- it("does not add sdScopeWatchPlugin in watch mode without replaceDeps", () => {
376
- const config = createClientViteConfig({
497
+ it("does not add sdScopeWatchPlugin in watch mode without replaceDeps", async () => {
498
+ const config = await createClientViteConfig({
377
499
  ...createDefaultOptions(),
378
500
  mode: "build",
379
501
  watch: true,
@@ -385,8 +507,8 @@ describe("createClientViteConfig", () => {
385
507
  });
386
508
 
387
509
  // Unit: watch: true still sets outDir
388
- it("sets outDir in watch mode", () => {
389
- const config = createClientViteConfig({
510
+ it("sets outDir in watch mode", async () => {
511
+ const config = await createClientViteConfig({
390
512
  ...createDefaultOptions(),
391
513
  mode: "build",
392
514
  watch: true,
@@ -399,8 +521,8 @@ describe("createClientViteConfig", () => {
399
521
  // --- outDir override ---
400
522
 
401
523
  // Acceptance: Scenario "outDir 설정 시 해당 경로로 빌드 출력"
402
- it("uses custom outDir when provided", () => {
403
- const config = createClientViteConfig({
524
+ it("uses custom outDir when provided", async () => {
525
+ const config = await createClientViteConfig({
404
526
  ...createDefaultOptions(),
405
527
  outDir: "/packages/my-client/.capacitor/www",
406
528
  });
@@ -409,8 +531,8 @@ describe("createClientViteConfig", () => {
409
531
  });
410
532
 
411
533
  // Acceptance: Scenario "outDir 미설정 시 pkgDir/dist 사용"
412
- it("defaults outDir to pkgDir/dist when not provided", () => {
413
- const config = createClientViteConfig(createDefaultOptions());
534
+ it("defaults outDir to pkgDir/dist when not provided", async () => {
535
+ const config = await createClientViteConfig(createDefaultOptions());
414
536
 
415
537
  expect(config.build?.outDir).toMatch(/my-client[\\/]dist$/);
416
538
  });
@@ -418,8 +540,8 @@ describe("createClientViteConfig", () => {
418
540
  // --- exclude (Feature 1.1: vite-exclude-passthrough) ---
419
541
 
420
542
  // Acceptance: Scenario "exclude에 패키지를 지정하면 pre-bundling에서 제외된다"
421
- it("sets optimizeDeps.exclude when exclude is provided", () => {
422
- const config = createClientViteConfig({
543
+ it("sets optimizeDeps.exclude when exclude is provided", async () => {
544
+ const config = await createClientViteConfig({
423
545
  ...createDefaultOptions(),
424
546
  mode: "dev",
425
547
  exclude: ["jeep-sqlite"],
@@ -429,8 +551,8 @@ describe("createClientViteConfig", () => {
429
551
  });
430
552
 
431
553
  // Acceptance: Scenario "exclude 미설정 시 기존 동작과 동일하다"
432
- it("does not set optimizeDeps.exclude when exclude is not provided", () => {
433
- const config = createClientViteConfig({
554
+ it("does not set optimizeDeps.exclude when exclude is not provided", async () => {
555
+ const config = await createClientViteConfig({
434
556
  ...createDefaultOptions(),
435
557
  mode: "dev",
436
558
  });
@@ -442,22 +564,22 @@ describe("createClientViteConfig", () => {
442
564
  it("sets optimizeDeps.exclude from exclude while sdScopeWatchPlugin handles replaceDeps", async () => {
443
565
  const { sdScopeWatchPlugin } = await import("../../src/utils/vite-scope-watch-plugin");
444
566
 
445
- const config = createClientViteConfig({
567
+ const config = await createClientViteConfig({
446
568
  ...createDefaultOptions(),
447
569
  mode: "dev",
448
570
  exclude: ["jeep-sqlite"],
449
571
  replaceDeps: [{ packageName: "@scope/core", sourcePath: "/packages/core" }],
450
572
  });
451
573
 
452
- // Base config에 exclude 설정
453
- expect(config.optimizeDeps?.exclude).toEqual(["jeep-sqlite"]);
454
- // sdScopeWatchPlugin도 호출됨 (replaceDeps용 exclude는 plugin이 처리)
574
+ // Base config에 사용자 exclude + replaceDeps 패키지 모두 포함
575
+ expect(config.optimizeDeps?.exclude).toEqual(["jeep-sqlite", "@scope/core"]);
576
+ // sdScopeWatchPlugin도 호출됨
455
577
  expect(sdScopeWatchPlugin).toHaveBeenCalled();
456
578
  });
457
579
 
458
580
  // Acceptance: Scenario "exclude만 있고 replaceDeps가 없으면 exclude만 제외된다"
459
- it("sets optimizeDeps.exclude from exclude when no replaceDeps", () => {
460
- const config = createClientViteConfig({
581
+ it("sets optimizeDeps.exclude from exclude when no replaceDeps", async () => {
582
+ const config = await createClientViteConfig({
461
583
  ...createDefaultOptions(),
462
584
  mode: "dev",
463
585
  exclude: ["jeep-sqlite"],
@@ -473,8 +595,8 @@ describe("createClientViteConfig", () => {
473
595
  // --- framework selection (Feature 1.1: client-framework-selection) ---
474
596
 
475
597
  // Acceptance: Scenario "Solid 프레임워크 선택"
476
- it("uses solidPlugin when framework is 'solid'", () => {
477
- const config = createClientViteConfig({
598
+ it("uses solidPlugin when framework is 'solid'", async () => {
599
+ const config = await createClientViteConfig({
478
600
  ...createDefaultOptions(),
479
601
  framework: "solid",
480
602
  });
@@ -486,16 +608,16 @@ describe("createClientViteConfig", () => {
486
608
  });
487
609
 
488
610
  // Acceptance: Scenario "framework 미지정 시 기본값"
489
- it("uses sdAngularPlugin when framework is not specified", () => {
490
- createClientViteConfig(createDefaultOptions());
611
+ it("uses sdAngularPlugin when framework is not specified", async () => {
612
+ await createClientViteConfig(createDefaultOptions());
491
613
 
492
614
  expect(mockSdAngularPlugin).toHaveBeenCalled();
493
615
  expect(mockSolidPlugin).not.toHaveBeenCalled();
494
616
  });
495
617
 
496
618
  // Acceptance: Scenario "Angular 프레임워크 명시 선택"
497
- it("uses sdAngularPlugin when framework is 'angular'", () => {
498
- createClientViteConfig({
619
+ it("uses sdAngularPlugin when framework is 'angular'", async () => {
620
+ await createClientViteConfig({
499
621
  ...createDefaultOptions(),
500
622
  framework: "angular",
501
623
  });
@@ -505,9 +627,9 @@ describe("createClientViteConfig", () => {
505
627
  });
506
628
 
507
629
  // Acceptance: Scenario "Solid 빌드에서 PostCSS inline 플러그인 미적용"
508
- it("does not add sdPostCssInlinePlugin when framework is 'solid' even with postCssPlugins", () => {
630
+ it("does not add sdPostCssInlinePlugin when framework is 'solid' even with postCssPlugins", async () => {
509
631
  const fakePlugin = { postcssPlugin: "autoprefixer" };
510
- const config = createClientViteConfig({
632
+ const config = await createClientViteConfig({
511
633
  ...createDefaultOptions(),
512
634
  framework: "solid",
513
635
  postCssPlugins: [fakePlugin],
@@ -522,8 +644,8 @@ describe("createClientViteConfig", () => {
522
644
  // --- legacyModule dev mode (Feature: fix-legacy-ngdevmode) ---
523
645
 
524
646
  // Acceptance: Scenario "legacyModule: true + dev 명령 실행 시 sdAngularPlugin에 dev: true 전달"
525
- it("passes dev: true to sdAngularPlugin when mode is dev with legacyModule", () => {
526
- createClientViteConfig({
647
+ it("passes dev: true to sdAngularPlugin when mode is dev with legacyModule", async () => {
648
+ await createClientViteConfig({
527
649
  ...createDefaultOptions(),
528
650
  mode: "dev",
529
651
  legacyModule: true,
@@ -536,8 +658,8 @@ describe("createClientViteConfig", () => {
536
658
  });
537
659
 
538
660
  // Acceptance: Scenario "legacyModule dev에서 build output 설정이 적용된다"
539
- it("applies build output settings when mode is dev with legacyModule", () => {
540
- const config = createClientViteConfig({
661
+ it("applies build output settings when mode is dev with legacyModule", async () => {
662
+ const config = await createClientViteConfig({
541
663
  ...createDefaultOptions(),
542
664
  mode: "dev",
543
665
  legacyModule: true,
@@ -550,9 +672,9 @@ describe("createClientViteConfig", () => {
550
672
  expect(config.build?.minify).toBe(false);
551
673
  });
552
674
 
553
- // Acceptance: Scenario "dev + legacyModule에서 비활성화"
554
- it("does not add sdPwaPlugin when mode is dev with legacyModule", () => {
555
- const config = createClientViteConfig({
675
+ // Unit: legacyModule dev에서 PWA가 추가되지 않는다
676
+ it("does not add VitePWA plugin when mode is dev with legacyModule", async () => {
677
+ const config = await createClientViteConfig({
556
678
  ...createDefaultOptions(),
557
679
  mode: "dev",
558
680
  legacyModule: true,
@@ -560,7 +682,7 @@ describe("createClientViteConfig", () => {
560
682
  });
561
683
 
562
684
  const plugins = config.plugins as Array<{ name: string }>;
563
- const pwaPlugin = plugins.find((p) => p.name === "sd-pwa");
685
+ const pwaPlugin = plugins.find((p) => p.name === "vite-plugin-pwa");
564
686
  expect(pwaPlugin).toBeUndefined();
565
687
  });
566
688
  });