@simplysm/sd-cli 14.0.16 → 14.0.18

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 (202) hide show
  1. package/README.md +2 -1
  2. package/dist/angular/client-transform-stylesheet.d.ts +2 -0
  3. package/dist/angular/client-transform-stylesheet.d.ts.map +1 -1
  4. package/dist/angular/client-transform-stylesheet.js +88 -2
  5. package/dist/angular/client-transform-stylesheet.js.map +1 -1
  6. package/dist/angular/vite-angular-plugin.d.ts +7 -0
  7. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  8. package/dist/angular/vite-angular-plugin.js +78 -16
  9. package/dist/angular/vite-angular-plugin.js.map +1 -1
  10. package/dist/capacitor/capacitor.d.ts.map +1 -1
  11. package/dist/capacitor/capacitor.js +9 -13
  12. package/dist/capacitor/capacitor.js.map +1 -1
  13. package/dist/commands/check.d.ts.map +1 -1
  14. package/dist/commands/check.js +8 -9
  15. package/dist/commands/check.js.map +1 -1
  16. package/dist/commands/device.d.ts.map +1 -1
  17. package/dist/commands/device.js +33 -1
  18. package/dist/commands/device.js.map +1 -1
  19. package/dist/commands/lint.d.ts +0 -1
  20. package/dist/commands/lint.d.ts.map +1 -1
  21. package/dist/commands/lint.js +2 -3
  22. package/dist/commands/lint.js.map +1 -1
  23. package/dist/commands/publish.js +2 -2
  24. package/dist/commands/publish.js.map +1 -1
  25. package/dist/commands/typecheck.d.ts.map +1 -1
  26. package/dist/commands/typecheck.js +0 -1
  27. package/dist/commands/typecheck.js.map +1 -1
  28. package/dist/electron/electron.d.ts +3 -2
  29. package/dist/electron/electron.d.ts.map +1 -1
  30. package/dist/electron/electron.js +54 -31
  31. package/dist/electron/electron.js.map +1 -1
  32. package/dist/engines/BaseEngine.js +1 -1
  33. package/dist/engines/BaseEngine.js.map +1 -1
  34. package/dist/engines/NgtscEngine.d.ts.map +1 -1
  35. package/dist/engines/NgtscEngine.js +0 -1
  36. package/dist/engines/NgtscEngine.js.map +1 -1
  37. package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
  38. package/dist/engines/ServerEsbuildEngine.js +0 -1
  39. package/dist/engines/ServerEsbuildEngine.js.map +1 -1
  40. package/dist/engines/TscEngine.d.ts.map +1 -1
  41. package/dist/engines/TscEngine.js +0 -1
  42. package/dist/engines/TscEngine.js.map +1 -1
  43. package/dist/engines/ViteEngine.d.ts.map +1 -1
  44. package/dist/engines/ViteEngine.js +8 -1
  45. package/dist/engines/ViteEngine.js.map +1 -1
  46. package/dist/engines/index.d.ts +0 -10
  47. package/dist/engines/index.d.ts.map +1 -1
  48. package/dist/engines/index.js +0 -5
  49. package/dist/engines/index.js.map +1 -1
  50. package/dist/engines/types.d.ts +0 -1
  51. package/dist/engines/types.d.ts.map +1 -1
  52. package/dist/infra/SignalHandler.d.ts +1 -6
  53. package/dist/infra/SignalHandler.d.ts.map +1 -1
  54. package/dist/infra/SignalHandler.js +4 -13
  55. package/dist/infra/SignalHandler.js.map +1 -1
  56. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  57. package/dist/orchestrators/BuildOrchestrator.js +7 -12
  58. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  59. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  60. package/dist/orchestrators/DevWatchOrchestrator.js +17 -10
  61. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  62. package/dist/sd-cli-entry.d.ts +0 -1
  63. package/dist/sd-cli-entry.d.ts.map +1 -1
  64. package/dist/sd-cli-entry.js +7 -8
  65. package/dist/sd-cli-entry.js.map +1 -1
  66. package/dist/sd-config.types.d.ts +11 -1
  67. package/dist/sd-config.types.d.ts.map +1 -1
  68. package/dist/utils/angular-compiler.d.ts.map +1 -1
  69. package/dist/utils/angular-compiler.js +20 -13
  70. package/dist/utils/angular-compiler.js.map +1 -1
  71. package/dist/utils/esbuild-config.d.ts +1 -1
  72. package/dist/utils/esbuild-config.d.ts.map +1 -1
  73. package/dist/utils/esbuild-config.js +1 -4
  74. package/dist/utils/esbuild-config.js.map +1 -1
  75. package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
  76. package/dist/utils/ngtsc-build-core.js +3 -0
  77. package/dist/utils/ngtsc-build-core.js.map +1 -1
  78. package/dist/utils/tsc-build.d.ts +5 -0
  79. package/dist/utils/tsc-build.d.ts.map +1 -1
  80. package/dist/utils/tsc-build.js +2 -1
  81. package/dist/utils/tsc-build.js.map +1 -1
  82. package/dist/utils/vite-config.d.ts +1 -1
  83. package/dist/utils/vite-config.d.ts.map +1 -1
  84. package/dist/utils/vite-config.js +22 -53
  85. package/dist/utils/vite-config.js.map +1 -1
  86. package/dist/utils/vite-pwa-plugin.d.ts +9 -0
  87. package/dist/utils/vite-pwa-plugin.d.ts.map +1 -0
  88. package/dist/utils/vite-pwa-plugin.js +139 -0
  89. package/dist/utils/vite-pwa-plugin.js.map +1 -0
  90. package/dist/utils/worker-utils.d.ts +2 -5
  91. package/dist/utils/worker-utils.d.ts.map +1 -1
  92. package/dist/utils/worker-utils.js +5 -11
  93. package/dist/utils/worker-utils.js.map +1 -1
  94. package/dist/workers/client.worker.d.ts.map +1 -1
  95. package/dist/workers/client.worker.js +9 -3
  96. package/dist/workers/client.worker.js.map +1 -1
  97. package/dist/workers/library-build.worker.d.ts.map +1 -1
  98. package/dist/workers/library-build.worker.js +6 -2
  99. package/dist/workers/library-build.worker.js.map +1 -1
  100. package/dist/workers/ngtsc-build.worker.js +2 -2
  101. package/dist/workers/ngtsc-build.worker.js.map +1 -1
  102. package/dist/workers/server-build.worker.d.ts.map +1 -1
  103. package/dist/workers/server-build.worker.js +6 -2
  104. package/dist/workers/server-build.worker.js.map +1 -1
  105. package/dist/workers/server-runtime.worker.js +4 -4
  106. package/dist/workers/server-runtime.worker.js.map +1 -1
  107. package/docs/config.md +26 -0
  108. package/docs/pwa-configuration-types.md +1 -1
  109. package/package.json +8 -10
  110. package/src/angular/client-transform-stylesheet.ts +104 -2
  111. package/src/angular/vite-angular-plugin.ts +92 -31
  112. package/src/capacitor/capacitor.ts +10 -26
  113. package/src/commands/check.ts +8 -11
  114. package/src/commands/device.ts +38 -3
  115. package/src/commands/lint.ts +2 -3
  116. package/src/commands/publish.ts +2 -2
  117. package/src/commands/typecheck.ts +0 -1
  118. package/src/electron/electron.ts +62 -43
  119. package/src/engines/BaseEngine.ts +1 -1
  120. package/src/engines/NgtscEngine.ts +0 -1
  121. package/src/engines/ServerEsbuildEngine.ts +0 -1
  122. package/src/engines/TscEngine.ts +0 -1
  123. package/src/engines/ViteEngine.ts +7 -1
  124. package/src/engines/index.ts +0 -10
  125. package/src/engines/types.ts +0 -1
  126. package/src/infra/SignalHandler.ts +4 -14
  127. package/src/orchestrators/BuildOrchestrator.ts +7 -9
  128. package/src/orchestrators/DevWatchOrchestrator.ts +21 -9
  129. package/src/sd-cli-entry.ts +11 -16
  130. package/src/sd-config.types.ts +12 -1
  131. package/src/utils/angular-compiler.ts +21 -21
  132. package/src/utils/esbuild-config.ts +2 -5
  133. package/src/utils/ngtsc-build-core.ts +7 -0
  134. package/src/utils/tsc-build.ts +7 -0
  135. package/src/utils/vite-config.ts +23 -55
  136. package/src/utils/vite-pwa-plugin.ts +168 -0
  137. package/src/utils/worker-utils.ts +5 -11
  138. package/src/workers/client.worker.ts +11 -3
  139. package/src/workers/library-build.worker.ts +6 -2
  140. package/src/workers/ngtsc-build.worker.ts +2 -2
  141. package/src/workers/server-build.worker.ts +7 -2
  142. package/src/workers/server-runtime.worker.ts +4 -4
  143. package/tests/angular/client-transform-stylesheet.spec.ts +43 -0
  144. package/tests/angular/find-affected-by-scss.spec.ts +37 -0
  145. package/tests/angular/fixtures/basic-app/scss/_colors.scss +1 -0
  146. package/tests/angular/fixtures/basic-app/scss/_variables.scss +3 -0
  147. package/tests/angular/fixtures/basic-app/src/styled.component.ts +14 -0
  148. package/tests/angular/linker-disk-cache.spec.ts +158 -0
  149. package/tests/angular/scss-disk-cache.spec.ts +162 -0
  150. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
  151. package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
  152. package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
  153. package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +87 -0
  154. package/tests/angular/vite-angular-plugin.spec.ts +15 -15
  155. package/tests/capacitor/capacitor-icon.spec.ts +2 -4
  156. package/tests/capacitor/capacitor-init.spec.ts +2 -4
  157. package/tests/capacitor/capacitor-workspace.spec.ts +2 -4
  158. package/tests/commands/device.spec.ts +100 -0
  159. package/tests/electron/electron.spec.ts +24 -17
  160. package/tests/engines/ngtsc-engine.spec.ts +0 -3
  161. package/tests/engines/server-esbuild-engine.spec.ts +0 -3
  162. package/tests/engines/tsc-engine.spec.ts +1 -2
  163. package/tests/engines/vite-engine.spec.ts +0 -2
  164. package/tests/infra/signal-handler.spec.ts +1 -12
  165. package/tests/orchestrators/build-orchestrator.spec.ts +0 -6
  166. package/tests/orchestrators/dev-watch-orchestrator.spec.ts +24 -66
  167. package/tests/utils/angular-compiler.spec.ts +1396 -32
  168. package/tests/utils/esbuild-config.spec.ts +4 -7
  169. package/tests/utils/{ngtsc-build-core-angular-compiler.spec.ts → ngtsc-build-core.spec.ts} +142 -11
  170. package/tests/utils/tsc-build.spec.ts +4 -1
  171. package/tests/utils/vite-config.spec.ts +130 -261
  172. package/tests/utils/vite-pwa-plugin.acc.spec.ts +143 -0
  173. package/tests/utils/vite-pwa-plugin.spec.ts +350 -0
  174. package/tests/utils/worker-utils.spec.ts +8 -7
  175. package/tests/workers/client-worker.spec.ts +50 -1
  176. package/tests/workers/dev-port-file.verify.md +6 -0
  177. package/tests/workers/library-build-lint.spec.ts +1 -1
  178. package/tests/workers/library-build-worker.spec.ts +1 -1
  179. package/tests/workers/ngtsc-build-lint.spec.ts +1 -1
  180. package/tests/workers/server-build-lint.spec.ts +1 -1
  181. package/tests/workers/server-build-worker.spec.ts +1 -1
  182. package/tests/workers/server-runtime-worker.spec.ts +8 -1
  183. package/dist/infra/WorkerManager.d.ts +0 -40
  184. package/dist/infra/WorkerManager.d.ts.map +0 -1
  185. package/dist/infra/WorkerManager.js +0 -59
  186. package/dist/infra/WorkerManager.js.map +0 -1
  187. package/dist/utils/SdCliReporter.d.ts +0 -18
  188. package/dist/utils/SdCliReporter.d.ts.map +0 -1
  189. package/dist/utils/SdCliReporter.js +0 -144
  190. package/dist/utils/SdCliReporter.js.map +0 -1
  191. package/src/infra/WorkerManager.ts +0 -65
  192. package/src/utils/SdCliReporter.ts +0 -177
  193. package/tests/angular/scss-compiler-async.spec.ts +0 -54
  194. package/tests/commands/dev.spec.ts +0 -53
  195. package/tests/commands/watch.spec.ts +0 -53
  196. package/tests/infra/worker-manager.spec.ts +0 -63
  197. package/tests/utils/angular-compiler-emit.spec.ts +0 -570
  198. package/tests/utils/angular-compiler-init.spec.ts +0 -705
  199. package/tests/utils/angular-compiler-update.spec.ts +0 -293
  200. package/tests/utils/build-env.spec.ts +0 -33
  201. package/tests/utils/ngtsc-build-core-transform-stylesheet.spec.ts +0 -124
  202. package/tests/utils/ngtsc-scss-refactor.spec.ts +0 -47
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import type { Plugin } from "vite";
3
+ import fs from "node:fs";
4
+
5
+ // --- Mock factories ---
6
+
7
+ const mockGeneratePwaIcons = vi.fn();
8
+ vi.mock("../../src/utils/generate-pwa-icons.js", () => ({
9
+ generatePwaIcons: mockGeneratePwaIcons,
10
+ }));
11
+
12
+ const mockGlob = vi.fn();
13
+ vi.mock("glob", () => ({
14
+ glob: mockGlob,
15
+ }));
16
+
17
+ // --- Dynamic import ---
18
+
19
+ const { sdPwaPlugin } = await import("../../src/utils/vite-pwa-plugin");
20
+
21
+ // --- Helpers ---
22
+
23
+ function createPlugin(
24
+ overrides?: Partial<Parameters<typeof sdPwaPlugin>[0]>,
25
+ ): Plugin {
26
+ return sdPwaPlugin({
27
+ pkgDir: "/packages/test-app",
28
+ pkgName: "test-app",
29
+ ...overrides,
30
+ }) as Plugin;
31
+ }
32
+
33
+ function initPlugin(plugin: Plugin): void {
34
+ (plugin.configResolved as Function)({
35
+ base: "/test-app/",
36
+ build: { outDir: "/packages/test-app/dist" },
37
+ });
38
+ }
39
+
40
+ function getManifestWriteCall(): [string, string] | undefined {
41
+ return vi.mocked(fs.writeFileSync).mock.calls.find((c) =>
42
+ String(c[0]).includes("manifest.webmanifest"),
43
+ ) as [string, string] | undefined;
44
+ }
45
+
46
+ function parseWrittenManifest(): Record<string, unknown> {
47
+ const call = getManifestWriteCall();
48
+ if (call == null) throw new Error("manifest.webmanifest not written");
49
+ return JSON.parse(call[1]);
50
+ }
51
+
52
+ // --- Tests ---
53
+
54
+ beforeEach(() => {
55
+ vi.clearAllMocks();
56
+ mockGeneratePwaIcons.mockResolvedValue([]);
57
+ mockGlob.mockResolvedValue([]);
58
+ vi.spyOn(fs, "readFileSync").mockReturnValue(
59
+ JSON.stringify({ version: "1.0.0" }),
60
+ );
61
+ vi.spyOn(fs, "writeFileSync").mockReturnValue(undefined);
62
+ });
63
+
64
+ afterEach(() => {
65
+ vi.restoreAllMocks();
66
+ });
67
+
68
+ describe("sdPwaPlugin — Acceptance: Slice 1 manifest 생성", () => {
69
+ // Scenario: 기본 manifest 생성
70
+ it("generates manifest.webmanifest with default fields when pwa is empty", async () => {
71
+ const plugin = createPlugin({ pwa: {} });
72
+ initPlugin(plugin);
73
+ await (plugin.closeBundle as Function)();
74
+
75
+ const manifest = parseWrittenManifest();
76
+ expect(manifest["name"]).toBe("test-app");
77
+ expect(manifest["short_name"]).toBe("test-app");
78
+ expect(manifest["display"]).toBe("standalone");
79
+ expect(manifest["theme_color"]).toBe("#ffffff");
80
+ expect(manifest["background_color"]).toBe("#ffffff");
81
+ expect(manifest["start_url"]).toBe(".");
82
+ });
83
+
84
+ // Scenario: manifest 필드 커스텀
85
+ it("applies custom manifest fields from SdPwaConfig", async () => {
86
+ const plugin = createPlugin({
87
+ pwa: { manifest: { name: "My App", theme_color: "#000000" } },
88
+ });
89
+ initPlugin(plugin);
90
+ await (plugin.closeBundle as Function)();
91
+
92
+ const manifest = parseWrittenManifest();
93
+ expect(manifest["name"]).toBe("My App");
94
+ expect(manifest["theme_color"]).toBe("#000000");
95
+ expect(manifest["short_name"]).toBe("test-app");
96
+ expect(manifest["display"]).toBe("standalone");
97
+ });
98
+
99
+ // Scenario: 기본 아이콘 자동 생성
100
+ it("includes generated icons in manifest when no custom icons", async () => {
101
+ mockGeneratePwaIcons.mockResolvedValue([
102
+ { src: "icons/icon-192x192.png", sizes: "192x192", type: "image/png" },
103
+ ]);
104
+
105
+ const plugin = createPlugin();
106
+ initPlugin(plugin);
107
+ await (plugin.closeBundle as Function)();
108
+
109
+ expect(mockGeneratePwaIcons).toHaveBeenCalledWith("/packages/test-app");
110
+ const manifest = parseWrittenManifest();
111
+ expect(manifest["icons"]).toEqual([
112
+ { src: "icons/icon-192x192.png", sizes: "192x192", type: "image/png" },
113
+ ]);
114
+ });
115
+
116
+ // Scenario: 커스텀 아이콘 설정 시 자동 생성 건너뜀
117
+ it("uses custom icons and skips generatePwaIcons", async () => {
118
+ const icons = [
119
+ { src: "/custom.png", sizes: "512x512", type: "image/png" },
120
+ ];
121
+ const plugin = createPlugin({
122
+ pwa: { manifest: { icons } },
123
+ });
124
+ initPlugin(plugin);
125
+ await (plugin.closeBundle as Function)();
126
+
127
+ expect(mockGeneratePwaIcons).not.toHaveBeenCalled();
128
+ const manifest = parseWrittenManifest();
129
+ expect(manifest["icons"]).toEqual(icons);
130
+ });
131
+
132
+ // Scenario: 원본 아이콘 없음
133
+ it("omits icons field when generatePwaIcons returns empty array", async () => {
134
+ mockGeneratePwaIcons.mockResolvedValue([]);
135
+
136
+ const plugin = createPlugin();
137
+ initPlugin(plugin);
138
+ await (plugin.closeBundle as Function)();
139
+
140
+ const manifest = parseWrittenManifest();
141
+ expect(manifest["icons"]).toBeUndefined();
142
+ });
143
+ });
@@ -0,0 +1,350 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import type { Plugin } from "vite";
3
+ import fs from "node:fs";
4
+
5
+ // --- Mock factories ---
6
+
7
+ const mockGeneratePwaIcons = vi.fn();
8
+ vi.mock("../../src/utils/generate-pwa-icons.js", () => ({
9
+ generatePwaIcons: mockGeneratePwaIcons,
10
+ }));
11
+
12
+ const mockGlob = vi.fn();
13
+ vi.mock("glob", () => ({
14
+ glob: mockGlob,
15
+ }));
16
+
17
+ // --- Dynamic import ---
18
+
19
+ const { sdPwaPlugin } = await import("../../src/utils/vite-pwa-plugin");
20
+
21
+ // --- Helpers ---
22
+
23
+ function createPlugin(
24
+ overrides?: Partial<Parameters<typeof sdPwaPlugin>[0]>,
25
+ ): Plugin {
26
+ return sdPwaPlugin({
27
+ pkgDir: "/packages/test-app",
28
+ pkgName: "test-app",
29
+ ...overrides,
30
+ }) as Plugin;
31
+ }
32
+
33
+ function initPlugin(plugin: Plugin): void {
34
+ (plugin.configResolved as Function)({
35
+ base: "/test-app/",
36
+ build: { outDir: "/packages/test-app/dist" },
37
+ });
38
+ }
39
+
40
+ function getWriteCall(filename: string): [string, string] | undefined {
41
+ return vi.mocked(fs.writeFileSync).mock.calls.find((c) =>
42
+ String(c[0]).endsWith(filename),
43
+ ) as [string, string] | undefined;
44
+ }
45
+
46
+ // --- Tests ---
47
+
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ mockGeneratePwaIcons.mockResolvedValue([]);
51
+ mockGlob.mockResolvedValue([]);
52
+ vi.spyOn(fs, "readFileSync").mockReturnValue(
53
+ JSON.stringify({ version: "1.0.0" }),
54
+ );
55
+ vi.spyOn(fs, "writeFileSync").mockReturnValue(undefined);
56
+ });
57
+
58
+ afterEach(() => {
59
+ vi.restoreAllMocks();
60
+ });
61
+
62
+ describe("sdPwaPlugin — manifest generation", () => {
63
+ // Unit: pwa undefined uses same defaults as empty object
64
+ it("uses defaults when pwa is undefined", async () => {
65
+ const plugin = createPlugin({ pwa: undefined });
66
+ initPlugin(plugin);
67
+ await (plugin.closeBundle as Function)();
68
+
69
+ const call = getWriteCall("manifest.webmanifest");
70
+ expect(call).toBeDefined();
71
+ const manifest = JSON.parse(call![1]) as Record<string, unknown>;
72
+ expect(manifest["name"]).toBe("test-app");
73
+ expect(manifest["scope"]).toBe(".");
74
+ });
75
+
76
+ // Unit: manifest written to correct outDir path
77
+ it("writes manifest to resolvedOutDir", async () => {
78
+ const plugin = createPlugin();
79
+ initPlugin(plugin);
80
+ await (plugin.closeBundle as Function)();
81
+
82
+ const call = getWriteCall("manifest.webmanifest");
83
+ const writtenPath = String(call![0]).replace(/\\/g, "/");
84
+ expect(writtenPath).toContain("packages/test-app/dist");
85
+ });
86
+
87
+ // Unit: all manifest fields can be overridden
88
+ it("overrides all customizable manifest fields", async () => {
89
+ const plugin = createPlugin({
90
+ pwa: {
91
+ manifest: {
92
+ name: "A",
93
+ short_name: "B",
94
+ display: "fullscreen",
95
+ theme_color: "#111",
96
+ background_color: "#222",
97
+ },
98
+ },
99
+ });
100
+ initPlugin(plugin);
101
+ await (plugin.closeBundle as Function)();
102
+
103
+ const manifest = JSON.parse(getWriteCall("manifest.webmanifest")![1]) as Record<
104
+ string,
105
+ unknown
106
+ >;
107
+ expect(manifest["name"]).toBe("A");
108
+ expect(manifest["short_name"]).toBe("B");
109
+ expect(manifest["display"]).toBe("fullscreen");
110
+ expect(manifest["theme_color"]).toBe("#111");
111
+ expect(manifest["background_color"]).toBe("#222");
112
+ });
113
+ });
114
+
115
+ describe("sdPwaPlugin — precache file collection", () => {
116
+ // Unit: default globPatterns
117
+ it("uses default globPatterns when workbox is not configured", async () => {
118
+ mockGlob.mockResolvedValue(["index.html", "assets/main.js"]);
119
+
120
+ const plugin = createPlugin();
121
+ initPlugin(plugin);
122
+ await (plugin.closeBundle as Function)();
123
+
124
+ expect(mockGlob).toHaveBeenCalledWith(
125
+ "**/*.{js,css,html,ico,png,svg,woff2}",
126
+ { cwd: "/packages/test-app/dist" },
127
+ );
128
+ });
129
+
130
+ // Unit: custom globPatterns
131
+ it("uses custom globPatterns from workbox config", async () => {
132
+ mockGlob.mockResolvedValue(["index.html"]);
133
+
134
+ const plugin = createPlugin({
135
+ pwa: { workbox: { globPatterns: ["**/*.{js,html}"] } },
136
+ });
137
+ initPlugin(plugin);
138
+ await (plugin.closeBundle as Function)();
139
+
140
+ expect(mockGlob).toHaveBeenCalledWith("**/*.{js,html}", expect.anything());
141
+ });
142
+
143
+ // Unit: excludes sw.js and manifest.webmanifest from precache
144
+ it("excludes sw.js and manifest.webmanifest from precache list", async () => {
145
+ mockGlob.mockResolvedValue([
146
+ "index.html",
147
+ "main.js",
148
+ "sw.js",
149
+ "manifest.webmanifest",
150
+ ]);
151
+
152
+ const plugin = createPlugin();
153
+ initPlugin(plugin);
154
+ await (plugin.closeBundle as Function)();
155
+
156
+ const swCall = getWriteCall("sw.js");
157
+ const swContent = swCall![1];
158
+ expect(swContent).toContain('"index.html"');
159
+ expect(swContent).toContain('"main.js"');
160
+ expect(swContent).not.toMatch(/"sw\.js"/);
161
+ expect(swContent).not.toMatch(/"manifest\.webmanifest"/);
162
+ });
163
+
164
+ // Unit: deduplicates file list
165
+ it("deduplicates files matched by multiple patterns", async () => {
166
+ mockGlob
167
+ .mockResolvedValueOnce(["index.html", "main.js"])
168
+ .mockResolvedValueOnce(["index.html", "styles.css"]);
169
+
170
+ const plugin = createPlugin({
171
+ pwa: {
172
+ workbox: { globPatterns: ["**/*.{html,js}", "**/*.{html,css}"] },
173
+ },
174
+ });
175
+ initPlugin(plugin);
176
+ await (plugin.closeBundle as Function)();
177
+
178
+ const swContent = getWriteCall("sw.js")![1];
179
+ // Extract only the PRECACHE_URLS array declaration
180
+ const precacheMatch = swContent.match(
181
+ /const PRECACHE_URLS = \[([\s\S]*?)\];/,
182
+ );
183
+ expect(precacheMatch).toBeDefined();
184
+ const precacheBlock = precacheMatch![1];
185
+ const htmlMatches = precacheBlock.match(/"index\.html"/g);
186
+ expect(htmlMatches).toHaveLength(1);
187
+ });
188
+ });
189
+
190
+ describe("sdPwaPlugin — sw.js generation", () => {
191
+ // Unit: version injected from package.json
192
+ it("injects APP_VERSION from package.json", async () => {
193
+ vi.mocked(fs.readFileSync).mockReturnValue(
194
+ JSON.stringify({ version: "14.0.16" }),
195
+ );
196
+
197
+ const plugin = createPlugin();
198
+ initPlugin(plugin);
199
+ await (plugin.closeBundle as Function)();
200
+
201
+ const swContent = getWriteCall("sw.js")![1];
202
+ expect(swContent).toContain('const APP_VERSION = "14.0.16"');
203
+ expect(swContent).toContain('"precache-" + APP_VERSION');
204
+ });
205
+
206
+ // Unit: base URL injected
207
+ it("injects BASE_URL from resolved config", async () => {
208
+ const plugin = createPlugin();
209
+ initPlugin(plugin);
210
+ await (plugin.closeBundle as Function)();
211
+
212
+ const swContent = getWriteCall("sw.js")![1];
213
+ expect(swContent).toContain('const BASE_URL = "/test-app/"');
214
+ });
215
+
216
+ // Unit: sw.js contains all 4 event listeners
217
+ it("contains install, activate, fetch, and message event listeners", async () => {
218
+ const plugin = createPlugin();
219
+ initPlugin(plugin);
220
+ await (plugin.closeBundle as Function)();
221
+
222
+ const swContent = getWriteCall("sw.js")![1];
223
+ expect(swContent).toContain('self.addEventListener("install"');
224
+ expect(swContent).toContain('self.addEventListener("activate"');
225
+ expect(swContent).toContain('self.addEventListener("fetch"');
226
+ expect(swContent).toContain('self.addEventListener("message"');
227
+ });
228
+
229
+ // Unit: install handler uses cache.addAll
230
+ it("install handler caches all precache URLs", async () => {
231
+ const plugin = createPlugin();
232
+ initPlugin(plugin);
233
+ await (plugin.closeBundle as Function)();
234
+
235
+ const swContent = getWriteCall("sw.js")![1];
236
+ expect(swContent).toContain("caches.open(CACHE_NAME)");
237
+ expect(swContent).toContain("cache.addAll(PRECACHE_URLS)");
238
+ });
239
+
240
+ // Unit: activate handler deletes old caches with precache- prefix only
241
+ it("activate handler filters by precache- prefix", async () => {
242
+ const plugin = createPlugin();
243
+ initPlugin(plugin);
244
+ await (plugin.closeBundle as Function)();
245
+
246
+ const swContent = getWriteCall("sw.js")![1];
247
+ expect(swContent).toContain('name.startsWith("precache-")');
248
+ expect(swContent).toContain("name !== CACHE_NAME");
249
+ expect(swContent).toContain("self.clients.claim()");
250
+ });
251
+
252
+ // Unit: fetch handler has navigate fallback to index.html with network fallback
253
+ it("fetch handler falls back to index.html for navigate requests with network fallback", async () => {
254
+ const plugin = createPlugin();
255
+ initPlugin(plugin);
256
+ await (plugin.closeBundle as Function)();
257
+
258
+ const swContent = getWriteCall("sw.js")![1];
259
+ expect(swContent).toContain('event.request.mode === "navigate"');
260
+ expect(swContent).toContain('BASE_URL + "index.html"');
261
+ // Network fallback when index.html is not in cache (storage pressure)
262
+ expect(swContent).toContain("resp || fetch(event.request)");
263
+ });
264
+
265
+ // Unit: message handler responds to SKIP_WAITING
266
+ it("message handler calls self.skipWaiting on SKIP_WAITING", async () => {
267
+ const plugin = createPlugin();
268
+ initPlugin(plugin);
269
+ await (plugin.closeBundle as Function)();
270
+
271
+ const swContent = getWriteCall("sw.js")![1];
272
+ expect(swContent).toContain('"SKIP_WAITING"');
273
+ expect(swContent).toContain("self.skipWaiting()");
274
+ });
275
+
276
+ // Unit: backslash normalization in file paths
277
+ it("normalizes backslashes in precache URLs", async () => {
278
+ mockGlob.mockResolvedValue(["assets\\main.js"]);
279
+
280
+ const plugin = createPlugin();
281
+ initPlugin(plugin);
282
+ await (plugin.closeBundle as Function)();
283
+
284
+ const swContent = getWriteCall("sw.js")![1];
285
+ expect(swContent).toContain('"assets/main.js"');
286
+ expect(swContent).not.toContain("\\\\");
287
+ });
288
+ });
289
+
290
+ describe("sdPwaPlugin — transformIndexHtml", () => {
291
+ // Unit: manifest link tag
292
+ it("injects manifest link tag", () => {
293
+ const plugin = createPlugin();
294
+ initPlugin(plugin);
295
+ const tags = (plugin.transformIndexHtml as Function)() as Array<{
296
+ tag: string;
297
+ attrs?: Record<string, string>;
298
+ injectTo?: string;
299
+ }>;
300
+
301
+ const linkTag = tags.find((t) => t.tag === "link");
302
+ expect(linkTag).toBeDefined();
303
+ expect(linkTag!.attrs!["rel"]).toBe("manifest");
304
+ expect(linkTag!.attrs!["href"]).toBe("manifest.webmanifest");
305
+ });
306
+
307
+ // Unit: SW registration script tag
308
+ it("injects SW registration script", () => {
309
+ const plugin = createPlugin();
310
+ initPlugin(plugin);
311
+ const tags = (plugin.transformIndexHtml as Function)() as Array<{
312
+ tag: string;
313
+ children?: string;
314
+ injectTo?: string;
315
+ }>;
316
+
317
+ const scriptTag = tags.find((t) => t.tag === "script");
318
+ expect(scriptTag).toBeDefined();
319
+ expect(scriptTag!.children).toContain("serviceWorker");
320
+ expect(scriptTag!.children).toContain('register("sw.js")');
321
+ });
322
+
323
+ // Unit: registration script dispatches sd-pwa-update-ready event
324
+ it("dispatches sd-pwa-update-ready CustomEvent", () => {
325
+ const plugin = createPlugin();
326
+ initPlugin(plugin);
327
+ const tags = (plugin.transformIndexHtml as Function)() as Array<{
328
+ tag: string;
329
+ children?: string;
330
+ }>;
331
+
332
+ const scriptTag = tags.find((t) => t.tag === "script");
333
+ expect(scriptTag!.children).toContain("sd-pwa-update-ready");
334
+ expect(scriptTag!.children).toContain("SKIP_WAITING");
335
+ });
336
+
337
+ // Unit: registration script reloads on controllerchange
338
+ it("reloads page on controllerchange", () => {
339
+ const plugin = createPlugin();
340
+ initPlugin(plugin);
341
+ const tags = (plugin.transformIndexHtml as Function)() as Array<{
342
+ tag: string;
343
+ children?: string;
344
+ }>;
345
+
346
+ const scriptTag = tags.find((t) => t.tag === "script");
347
+ expect(scriptTag!.children).toContain("controllerchange");
348
+ expect(scriptTag!.children).toContain("location.reload");
349
+ });
350
+ });
@@ -1,13 +1,15 @@
1
1
  import { describe, it, expect, vi, afterEach } from "vitest";
2
- import { applyDebugLevel, createOnceGuard, registerCleanupHandlers } from "../../src/utils/worker-utils";
2
+ import { setupWorkerConsola, createOnceGuard, registerCleanupHandlers } from "../../src/utils/worker-utils";
3
3
  import consola, { LogLevels } from "consola";
4
4
 
5
- describe("applyDebugLevel", () => {
5
+ describe("setupWorkerConsola", () => {
6
6
  const originalLevel = consola.level;
7
+ const originalReporters = [...consola.options.reporters];
7
8
  const originalEnv = process.env["SD_DEBUG"];
8
9
 
9
10
  afterEach(() => {
10
11
  consola.level = originalLevel;
12
+ consola.options.reporters = originalReporters;
11
13
  if (originalEnv === undefined) {
12
14
  delete process.env["SD_DEBUG"];
13
15
  } else {
@@ -17,15 +19,14 @@ describe("applyDebugLevel", () => {
17
19
 
18
20
  it("sets consola level to debug when SD_DEBUG is 'true'", () => {
19
21
  process.env["SD_DEBUG"] = "true";
20
- applyDebugLevel();
22
+ setupWorkerConsola();
21
23
  expect(consola.level).toBe(LogLevels.debug);
22
24
  });
23
25
 
24
- it("does not change level when SD_DEBUG is not set", () => {
26
+ it("sets consola level to debug even when SD_DEBUG is not set", () => {
25
27
  delete process.env["SD_DEBUG"];
26
- const before = consola.level;
27
- applyDebugLevel();
28
- expect(consola.level).toBe(before);
28
+ setupWorkerConsola();
29
+ expect(consola.level).toBe(LogLevels.debug);
29
30
  });
30
31
  });
31
32
 
@@ -75,7 +75,7 @@ vi.mock("consola", () => ({
75
75
 
76
76
  vi.mock("../../src/utils/worker-utils.js", () => ({
77
77
  registerCleanupHandlers: vi.fn(),
78
- applyDebugLevel: vi.fn(),
78
+ setupWorkerConsola: vi.fn(),
79
79
  }));
80
80
 
81
81
  //#endregion
@@ -522,6 +522,55 @@ describe("client.worker", () => {
522
522
  });
523
523
  });
524
524
 
525
+ // Acceptance: Scenario "Vite dev server 포트 확정 시 포트 파일 기록"
526
+ describe("dev port file", () => {
527
+ it("writes .dev-port after serverReady in dev mode", async () => {
528
+ const mockServer = {
529
+ listen: vi.fn().mockResolvedValue(undefined),
530
+ close: vi.fn().mockResolvedValue(undefined),
531
+ httpServer: { address: () => ({ port: 5173 }) },
532
+ };
533
+ mockCreateServer.mockResolvedValue(mockServer);
534
+
535
+ await workerFns["startWatch"]({
536
+ ...createBaseInfo(),
537
+ port: 5173,
538
+ });
539
+
540
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
541
+ expect.stringContaining(".dev-port"),
542
+ "5173",
543
+ );
544
+
545
+ await workerFns["stopWatch"]();
546
+ });
547
+
548
+ it("writes .dev-port after serverReady in legacy mode", async () => {
549
+ const mockWatcher = createMockWatcher();
550
+ mockViteBuild.mockImplementation(() => {
551
+ setTimeout(() => mockWatcher.emit("event", { code: "END" }), 0);
552
+ return Promise.resolve(mockWatcher);
553
+ });
554
+
555
+ mockExistsSync.mockReturnValue(false);
556
+
557
+ await workerFns["startWatch"]({
558
+ ...createBaseInfo(),
559
+ browserSupport: { legacyModule: true },
560
+ port: 0,
561
+ });
562
+
563
+ const port = mockSend.mock.calls.find((c: any[]) => c[0] === "serverReady")?.[1]?.port;
564
+
565
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
566
+ expect.stringContaining(".dev-port"),
567
+ String(port),
568
+ );
569
+
570
+ await workerFns["stopWatch"]();
571
+ });
572
+ });
573
+
525
574
  describe("startWatch — legacy live reload (Feature 1.3)", () => {
526
575
  // Acceptance: Scenario "index.html 서빙 시 reload 스크립트 자동 삽입"
527
576
  it("injects live reload script into HTML responses", async () => {
@@ -0,0 +1,6 @@
1
+ # 포트 파일 기록/삭제 — LLM 검증
2
+
3
+ ## 검증 항목
4
+ - [x] client.worker에서 dev 모드 serverReady 후 writeDevPort 호출: `client.worker.ts:250-253` — serverReady(248) 직후 `fs.writeFileSync(path.join(distDir, ".dev-port"), String(actualPort))` 확인
5
+ - [x] client.worker에서 legacy 모드 serverReady 후 writeDevPort 호출: `client.worker.ts:331-332` — serverReady(329) 직후 `fs.writeFileSync(path.join(info.pkgDir, "dist", ".dev-port"), String(serverPort))` 확인
6
+ - [x] ViteEngine.stop()에서 deleteDevPort 호출: `ViteEngine.ts:202-204` — `fs.unlinkSync(portFile)` with try-catch 확인
@@ -35,7 +35,7 @@ vi.mock("../../src/utils/tsc-build", () => ({
35
35
  vi.mock("../../src/utils/worker-utils", () => ({
36
36
  registerCleanupHandlers: vi.fn(),
37
37
  createOnceGuard: vi.fn(() => vi.fn()),
38
- applyDebugLevel: vi.fn(),
38
+ setupWorkerConsola: vi.fn(),
39
39
  }));
40
40
 
41
41
  const mockConsolaLogger = {
@@ -59,7 +59,7 @@ vi.mock("../../src/utils/tsc-build", () => ({
59
59
  }));
60
60
 
61
61
  vi.mock("../../src/utils/worker-utils", () => ({
62
- applyDebugLevel: vi.fn(),
62
+ setupWorkerConsola: vi.fn(),
63
63
  registerCleanupHandlers: vi.fn(),
64
64
  createOnceGuard: vi.fn(() => vi.fn()),
65
65
  }));
@@ -72,7 +72,7 @@ vi.mock("typescript", async (importOriginal) => {
72
72
  vi.mock("../../src/utils/worker-utils", () => ({
73
73
  registerCleanupHandlers: vi.fn(),
74
74
  createOnceGuard: vi.fn(() => vi.fn()),
75
- applyDebugLevel: vi.fn(),
75
+ setupWorkerConsola: vi.fn(),
76
76
  }));
77
77
 
78
78
  vi.mock("../../src/utils/package-utils", () => ({
@@ -53,7 +53,7 @@ vi.mock("esbuild", () => ({
53
53
  vi.mock("../../src/utils/worker-utils", () => ({
54
54
  registerCleanupHandlers: vi.fn(),
55
55
  createOnceGuard: vi.fn(() => vi.fn()),
56
- applyDebugLevel: vi.fn(),
56
+ setupWorkerConsola: vi.fn(),
57
57
  }));
58
58
 
59
59
  vi.mock("../../src/utils/copy-public", () => ({
@@ -132,7 +132,7 @@ vi.mock("../../src/utils/worker-utils", () => ({
132
132
  if (guardCalled) throw new Error("startWatch has already been called");
133
133
  guardCalled = true;
134
134
  }),
135
- applyDebugLevel: vi.fn(),
135
+ setupWorkerConsola: vi.fn(),
136
136
  }));
137
137
 
138
138
  vi.mock("../../src/utils/package-utils", () => ({
@@ -16,6 +16,13 @@ vi.mock("@simplysm/core-node", () => ({
16
16
  }));
17
17
 
18
18
  vi.mock("@simplysm/core-common", () => ({
19
+ env: vi.fn((key: string, value?: string) => {
20
+ if (value !== undefined) {
21
+ process.env[key] = value;
22
+ return;
23
+ }
24
+ return process.env[key];
25
+ }),
19
26
  err: { message: (e: any) => e?.message ?? String(e) },
20
27
  }));
21
28
 
@@ -32,7 +39,7 @@ vi.mock("consola", () => ({
32
39
 
33
40
  vi.mock("../../src/utils/worker-utils", () => ({
34
41
  registerCleanupHandlers: vi.fn(),
35
- applyDebugLevel: vi.fn(),
42
+ setupWorkerConsola: vi.fn(),
36
43
  }));
37
44
 
38
45
  // @fastify/http-proxy mock