@simplysm/sd-cli 14.0.17 → 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
@@ -18,14 +18,20 @@ const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin.
18
18
  const TMP_DIR = path.join(os.tmpdir(), "sd-cli-linker-cache-test");
19
19
  const CACHE_DIR = path.join(TMP_DIR, "cache");
20
20
 
21
- type LoadHandler = (id: string) => Promise<string | null>;
21
+ type OnLoadHandler = (args: { path: string }) => Promise<{ contents: string; loader: string } | null>;
22
22
 
23
- function getLoadHandler(plugin: ReturnType<typeof sdAngularPlugin>): LoadHandler {
23
+ function getOnLoadHandler(plugin: ReturnType<typeof sdAngularPlugin>): OnLoadHandler {
24
24
  const config = (plugin as any).config();
25
- const rolldownPlugin = config.optimizeDeps.rolldownOptions.plugins[0] as {
26
- load: (id: string) => Promise<string | null>;
25
+ const esbuildPlugin = config.optimizeDeps.esbuildOptions.plugins[0] as {
26
+ setup: (build: { onLoad: Function }) => void;
27
27
  };
28
- return (id: string) => rolldownPlugin.load(id);
28
+ let handler: OnLoadHandler;
29
+ esbuildPlugin.setup({
30
+ onLoad(_filter: unknown, fn: OnLoadHandler) {
31
+ handler = fn;
32
+ },
33
+ });
34
+ return handler!;
29
35
  }
30
36
 
31
37
  describe("Linker disk cache (optimizeDeps)", () => {
@@ -49,12 +55,12 @@ describe("Linker disk cache (optimizeDeps)", () => {
49
55
  dev: true,
50
56
  linkerCacheDir: CACHE_DIR,
51
57
  });
52
- const load = getLoadHandler(plugin);
58
+ const onLoad = getOnLoadHandler(plugin);
53
59
 
54
60
  // First call: cache miss
55
- const result1 = await load(jsFile);
61
+ const result1 = await onLoad({ path: jsFile });
56
62
  expect(mockTransformFile).toHaveBeenCalledOnce();
57
- expect(result1).toBe("transformed-code");
63
+ expect(result1).toEqual({ contents: "transformed-code", loader: "js" });
58
64
 
59
65
  // Cache file created
60
66
  const cacheFiles = fs.readdirSync(CACHE_DIR);
@@ -63,9 +69,9 @@ describe("Linker disk cache (optimizeDeps)", () => {
63
69
 
64
70
  // Second call: cache hit
65
71
  mockTransformFile.mockClear();
66
- const result2 = await load(jsFile);
72
+ const result2 = await onLoad({ path: jsFile });
67
73
  expect(mockTransformFile).not.toHaveBeenCalled();
68
- expect(result2).toBe("transformed-code");
74
+ expect(result2).toEqual({ contents: "transformed-code", loader: "js" });
69
75
  });
70
76
 
71
77
  // Acceptance: file content change → cache miss
@@ -78,9 +84,9 @@ describe("Linker disk cache (optimizeDeps)", () => {
78
84
  dev: true,
79
85
  linkerCacheDir: CACHE_DIR,
80
86
  });
81
- const load = getLoadHandler(plugin);
87
+ const onLoad = getOnLoadHandler(plugin);
82
88
 
83
- await load(jsFile);
89
+ await onLoad({ path: jsFile });
84
90
  expect(mockTransformFile).toHaveBeenCalledOnce();
85
91
 
86
92
  // Change content
@@ -88,9 +94,9 @@ describe("Linker disk cache (optimizeDeps)", () => {
88
94
  mockTransformFile.mockClear();
89
95
  mockTransformFile.mockResolvedValue("transformed-v2");
90
96
 
91
- const result = await load(jsFile);
97
+ const result = await onLoad({ path: jsFile });
92
98
  expect(mockTransformFile).toHaveBeenCalledOnce();
93
- expect(result).toBe("transformed-v2");
99
+ expect(result).toEqual({ contents: "transformed-v2", loader: "js" });
94
100
  });
95
101
 
96
102
  // Unit: corrupted cache file → graceful fallback
@@ -103,19 +109,19 @@ describe("Linker disk cache (optimizeDeps)", () => {
103
109
  dev: true,
104
110
  linkerCacheDir: CACHE_DIR,
105
111
  });
106
- const load = getLoadHandler(plugin);
112
+ const onLoad = getOnLoadHandler(plugin);
107
113
 
108
114
  // First call to populate cache
109
- await load(jsFile);
115
+ await onLoad({ path: jsFile });
110
116
 
111
117
  // Remove the cache file to simulate corruption/missing cache
112
118
  const cacheFiles = fs.readdirSync(CACHE_DIR);
113
119
  fs.rmSync(path.join(CACHE_DIR, cacheFiles[0]));
114
120
 
115
121
  mockTransformFile.mockClear();
116
- const result = await load(jsFile);
122
+ const result = await onLoad({ path: jsFile });
117
123
  expect(mockTransformFile).toHaveBeenCalledOnce();
118
- expect(result).toBe("transformed-code");
124
+ expect(result).toEqual({ contents: "transformed-code", loader: "js" });
119
125
  });
120
126
 
121
127
  // Unit: Uint8Array result from transformFile is handled
@@ -130,16 +136,16 @@ describe("Linker disk cache (optimizeDeps)", () => {
130
136
  dev: true,
131
137
  linkerCacheDir: CACHE_DIR,
132
138
  });
133
- const load = getLoadHandler(plugin);
139
+ const onLoad = getOnLoadHandler(plugin);
134
140
 
135
- const result = await load(jsFile);
136
- expect(result).toBe("uint8-transformed");
141
+ const result = await onLoad({ path: jsFile });
142
+ expect(result).toEqual({ contents: "uint8-transformed", loader: "js" });
137
143
 
138
144
  // Second call: cache hit should also return string
139
145
  mockTransformFile.mockClear();
140
- const result2 = await load(jsFile);
146
+ const result2 = await onLoad({ path: jsFile });
141
147
  expect(mockTransformFile).not.toHaveBeenCalled();
142
- expect(result2).toBe("uint8-transformed");
148
+ expect(result2).toEqual({ contents: "uint8-transformed", loader: "js" });
143
149
  });
144
150
 
145
151
  // Unit: non-.js file returns null (filter)
@@ -149,9 +155,9 @@ describe("Linker disk cache (optimizeDeps)", () => {
149
155
  dev: true,
150
156
  linkerCacheDir: CACHE_DIR,
151
157
  });
152
- const load = getLoadHandler(plugin);
158
+ const onLoad = getOnLoadHandler(plugin);
153
159
 
154
- const result = await load(path.join(TMP_DIR, "data.json"));
160
+ const result = await onLoad({ path: path.join(TMP_DIR, "data.json") });
155
161
  expect(result).toBeNull();
156
162
  expect(mockTransformFile).not.toHaveBeenCalled();
157
163
  });
@@ -28,7 +28,7 @@ describe("sdAngularPlugin CSS HMR compatibility", () => {
28
28
  .join(FIXTURE_DIR, "node_modules/@scope/lib/dist/style.css")
29
29
  .replace(/\\/g, "/");
30
30
 
31
- const hmrResult = await (plugin as any).hotUpdate?.({
31
+ const hmrResult = await (plugin as any).handleHotUpdate?.({
32
32
  file: cssFilePath,
33
33
  modules: [{ file: cssFilePath, id: cssFilePath }],
34
34
  server: { watcher: { emit: vi.fn() } },
@@ -68,7 +68,7 @@ describe("sdAngularPlugin CSS HMR compatibility", () => {
68
68
  .join(FIXTURE_DIR, "src/app.component.ts")
69
69
  .replace(/\\/g, "/");
70
70
 
71
- await (plugin as any).hotUpdate?.({
71
+ await (plugin as any).handleHotUpdate?.({
72
72
  file: tsFilePath,
73
73
  modules: [{ file: tsFilePath, id: tsFilePath }],
74
74
  server: { watcher: { emit: vi.fn() } },
@@ -88,7 +88,7 @@ describe("sdAngularPlugin CSS HMR compatibility", () => {
88
88
  .join(FIXTURE_DIR, "node_modules/@scope/lib/dist/style.css")
89
89
  .replace(/\\/g, "/");
90
90
 
91
- await (plugin as any).hotUpdate?.({
91
+ await (plugin as any).handleHotUpdate?.({
92
92
  file: cssFilePath,
93
93
  modules: [{ file: cssFilePath, id: cssFilePath }],
94
94
  server: { watcher: { emit: vi.fn() } },
@@ -121,7 +121,7 @@ describe("sdAngularPlugin CSS HMR compatibility", () => {
121
121
  ];
122
122
 
123
123
  for (const cssPath of cssVariants) {
124
- const result = await (plugin as any).hotUpdate?.({
124
+ const result = await (plugin as any).handleHotUpdate?.({
125
125
  file: cssPath,
126
126
  modules: [{ file: cssPath, id: cssPath }],
127
127
  server: { watcher: { emit: vi.fn() } },
@@ -159,7 +159,7 @@ describe("sdAngularPlugin CSS HMR compatibility", () => {
159
159
  .join(FIXTURE_DIR, "src/styles.scss")
160
160
  .replace(/\\/g, "/");
161
161
 
162
- const result = await (plugin as any).hotUpdate?.({
162
+ const result = await (plugin as any).handleHotUpdate?.({
163
163
  file: scssFilePath,
164
164
  modules: [{ file: scssFilePath, id: scssFilePath }],
165
165
  server: { watcher: { emit: vi.fn() } },
@@ -209,8 +209,8 @@ describe("sdAngularPlugin HMR fallback", () => {
209
209
  .join(FIXTURE_DIR, "src/app.component.ts")
210
210
  .replace(/\\/g, "/");
211
211
 
212
- // hotUpdate 호출
213
- const hmrResult = await (plugin as any).hotUpdate?.({
212
+ // handleHotUpdate 호출
213
+ const hmrResult = await (plugin as any).handleHotUpdate?.({
214
214
  file: appComponentPath,
215
215
  modules: [{ file: appComponentPath, id: appComponentPath }],
216
216
  server: { watcher: { emit: vi.fn() } },
@@ -218,7 +218,7 @@ describe("sdAngularPlugin HMR fallback", () => {
218
218
  read: () => Promise.resolve(""),
219
219
  });
220
220
 
221
- // hotUpdate는 항상 affected modules 배열을 반환해야 한다
221
+ // handleHotUpdate는 항상 affected modules 배열을 반환해야 한다
222
222
  // (templateUpdates가 undefined이든 아니든)
223
223
  expect(Array.isArray(hmrResult)).toBe(true);
224
224
 
@@ -249,8 +249,8 @@ describe("sdAngularPlugin HMR fallback", () => {
249
249
  await (plugin as any).buildEnd?.call({});
250
250
  });
251
251
 
252
- // Unit: prod 모드(dev: false)에서는 hotUpdate가 void 반환 (HMR 비활성)
253
- it("returns undefined from hotUpdate in prod mode", async () => {
252
+ // Unit: prod 모드(dev: false)에서는 handleHotUpdate가 void 반환 (HMR 비활성)
253
+ it("returns undefined from handleHotUpdate in prod mode", async () => {
254
254
  const plugin = sdAngularPlugin({
255
255
  tsconfig: TSCONFIG_PATH,
256
256
  dev: false,
@@ -262,7 +262,7 @@ describe("sdAngularPlugin HMR fallback", () => {
262
262
  .join(FIXTURE_DIR, "src/app.component.ts")
263
263
  .replace(/\\/g, "/");
264
264
 
265
- const hmrResult = await (plugin as any).hotUpdate?.({
265
+ const hmrResult = await (plugin as any).handleHotUpdate?.({
266
266
  file: appComponentPath,
267
267
  modules: [{ file: appComponentPath }],
268
268
  server: { watcher: { emit: vi.fn() } },
@@ -276,7 +276,7 @@ describe("sdAngularPlugin HMR fallback", () => {
276
276
  await (plugin as any).buildEnd?.call({});
277
277
  });
278
278
 
279
- // Acceptance: 수정 파일 33개 이상일 때도 hotUpdate가 정상 동작
279
+ // Acceptance: 수정 파일 33개 이상일 때도 handleHotUpdate가 정상 동작
280
280
  // (Angular 컴파일러 내부에서 HMR 분석을 생략하고 templateUpdates=undefined 반환)
281
281
  it("handles update with many modified files gracefully (HMR skipped by compiler)", async () => {
282
282
  const plugin = sdAngularPlugin({
@@ -290,11 +290,11 @@ describe("sdAngularPlugin HMR fallback", () => {
290
290
  .join(FIXTURE_DIR, "src/app.component.ts")
291
291
  .replace(/\\/g, "/");
292
292
 
293
- // hotUpdate는 단일 파일에 대해 호출됨 (Vite 설계)
293
+ // handleHotUpdate는 단일 파일에 대해 호출됨 (Vite 설계)
294
294
  // 33개 이상 수정 파일 제한은 Angular 컴파일러 내부에서 처리
295
295
  // AngularFacade.update()에서 modifiedFiles가 SourceFileCache.modifiedFiles로 전달되므로
296
296
  // 실제 33개 이상 파일 변경은 SourceFileCache를 통해 추적됨
297
- const hmrResult = await (plugin as any).hotUpdate?.({
297
+ const hmrResult = await (plugin as any).handleHotUpdate?.({
298
298
  file: appComponentPath,
299
299
  modules: [{ file: appComponentPath }],
300
300
  server: { watcher: { emit: vi.fn() } },
@@ -302,7 +302,7 @@ describe("sdAngularPlugin HMR fallback", () => {
302
302
  read: () => Promise.resolve(""),
303
303
  });
304
304
 
305
- // hotUpdate가 에러 없이 동작해야 한다
305
+ // handleHotUpdate가 에러 없이 동작해야 한다
306
306
  expect(Array.isArray(hmrResult)).toBe(true);
307
307
 
308
308
  await (plugin as any).buildEnd?.call({});
@@ -131,8 +131,8 @@ describe("sdAngularPlugin HMR + component-middleware", () => {
131
131
  await (plugin as any).buildEnd?.call({});
132
132
  });
133
133
 
134
- // Acceptance: hotUpdate에서 templateUpdates를 수집하고 middleware에서 서빙
135
- it("collects templateUpdates from hotUpdate and serves via middleware", async () => {
134
+ // Acceptance: handleHotUpdate에서 templateUpdates를 수집하고 middleware에서 서빙
135
+ it("collects templateUpdates from handleHotUpdate and serves via middleware", async () => {
136
136
  const plugin = sdAngularPlugin({ tsconfig: TSCONFIG_PATH, dev: true });
137
137
 
138
138
  await (plugin as any).buildStart?.call({});
@@ -151,12 +151,12 @@ describe("sdAngularPlugin HMR + component-middleware", () => {
151
151
  };
152
152
  (plugin as any).configureServer?.(mockServer);
153
153
 
154
- // hotUpdate 호출 (인라인 템플릿 변경)
154
+ // handleHotUpdate 호출 (인라인 템플릿 변경)
155
155
  const appComponentPath = path
156
156
  .join(FIXTURE_DIR, "src/app.component.ts")
157
157
  .replace(/\\/g, "/");
158
158
 
159
- await (plugin as any).hotUpdate?.({
159
+ await (plugin as any).handleHotUpdate?.({
160
160
  file: appComponentPath,
161
161
  modules: [{ file: appComponentPath, id: appComponentPath }],
162
162
  server: { watcher: { emit: vi.fn() } },
@@ -174,7 +174,7 @@ describe("sdAngularPlugin HMR + component-middleware", () => {
174
174
  });
175
175
 
176
176
  // Acceptance: rebuild 시작 시 이전 templateUpdates 정리
177
- it("clears templateUpdates at the start of hotUpdate", async () => {
177
+ it("clears templateUpdates at the start of handleHotUpdate", async () => {
178
178
  const plugin = sdAngularPlugin({ tsconfig: TSCONFIG_PATH, dev: true });
179
179
  await (plugin as any).buildStart?.call({});
180
180
 
@@ -195,8 +195,8 @@ describe("sdAngularPlugin HMR + component-middleware", () => {
195
195
  .join(FIXTURE_DIR, "src/app.component.ts")
196
196
  .replace(/\\/g, "/");
197
197
 
198
- // 첫 번째 hotUpdate
199
- await (plugin as any).hotUpdate?.({
198
+ // 첫 번째 handleHotUpdate
199
+ await (plugin as any).handleHotUpdate?.({
200
200
  file: appComponentPath,
201
201
  modules: [{ file: appComponentPath }],
202
202
  server: { watcher: { emit: vi.fn() } },
@@ -204,8 +204,8 @@ describe("sdAngularPlugin HMR + component-middleware", () => {
204
204
  read: () => Promise.resolve(""),
205
205
  });
206
206
 
207
- // 두 번째 hotUpdate — 이전 templateUpdates가 정리되어야 한다
208
- await (plugin as any).hotUpdate?.({
207
+ // 두 번째 handleHotUpdate — 이전 templateUpdates가 정리되어야 한다
208
+ await (plugin as any).handleHotUpdate?.({
209
209
  file: appComponentPath,
210
210
  modules: [{ file: appComponentPath }],
211
211
  server: { watcher: { emit: vi.fn() } },
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import path from "path";
3
+ import { sdAngularPlugin } from "../../src/angular/vite-angular-plugin.js";
4
+
5
+ const FIXTURE_DIR = path.resolve(import.meta.dirname, "fixtures/basic-app");
6
+ const TSCONFIG_PATH = path.join(FIXTURE_DIR, "tsconfig.json");
7
+
8
+ describe("sdAngularPlugin legacy watch rebuild", () => {
9
+ // Acceptance: 재빌드 시 변경 파일의 캐시가 무효화되고 증분 컴파일된다
10
+ it("invalidates cache and produces updated output on watch rebuild", async () => {
11
+ const onBuild = vi.fn();
12
+ const plugin = sdAngularPlugin({
13
+ tsconfig: TSCONFIG_PATH,
14
+ dev: true,
15
+ legacyModule: true,
16
+ onBuild,
17
+ });
18
+
19
+ // 초기 빌드
20
+ await (plugin as any).buildStart?.call({});
21
+
22
+ const appComponentPath = path
23
+ .join(FIXTURE_DIR, "src/app.component.ts")
24
+ .replace(/\\/g, "/");
25
+
26
+ // 초기 transform 결과 캡처
27
+ const initialResult = await (plugin as any).transform?.call({}, "", appComponentPath);
28
+ expect(initialResult).toBeDefined();
29
+ expect(initialResult.code.length).toBeGreaterThan(0);
30
+
31
+ // watchChange 호출 (파일 변경 알림)
32
+ expect((plugin as any).watchChange).toBeDefined();
33
+ await (plugin as any).watchChange?.call({}, appComponentPath, { event: "update" });
34
+
35
+ // 재빌드 (buildStart 재호출)
36
+ await (plugin as any).buildStart?.call({});
37
+
38
+ // 재빌드 후 transform — 컴파일러가 재실행되어 결과를 반환해야 한다
39
+ const rebuiltResult = await (plugin as any).transform?.call({}, "", appComponentPath);
40
+ expect(rebuiltResult).toBeDefined();
41
+ expect(rebuiltResult.code).toBeDefined();
42
+ expect(rebuiltResult.code.length).toBeGreaterThan(0);
43
+
44
+ // onBuild이 초기 빌드와 재빌드 모두에서 호출되어야 한다
45
+ expect(onBuild).toHaveBeenCalledTimes(2);
46
+ expect(onBuild).toHaveBeenCalledWith(
47
+ expect.objectContaining({ success: true }),
48
+ );
49
+
50
+ await (plugin as any).buildEnd?.call({});
51
+ });
52
+
53
+ // Acceptance: 첫 buildStart는 기존 로직으로 전체 컴파일한다
54
+ it("performs full compilation on first buildStart", async () => {
55
+ const onBuild = vi.fn();
56
+ const plugin = sdAngularPlugin({
57
+ tsconfig: TSCONFIG_PATH,
58
+ dev: true,
59
+ legacyModule: true,
60
+ onBuild,
61
+ });
62
+
63
+ // watchChange 없이 첫 buildStart
64
+ await (plugin as any).buildStart?.call({});
65
+
66
+ const appComponentPath = path
67
+ .join(FIXTURE_DIR, "src/app.component.ts")
68
+ .replace(/\\/g, "/");
69
+
70
+ const result = await (plugin as any).transform?.call({}, "", appComponentPath);
71
+ expect(result).toBeDefined();
72
+ expect(result.code.length).toBeGreaterThan(0);
73
+
74
+ // onBuild 정상 호출
75
+ expect(onBuild).toHaveBeenCalledWith(
76
+ expect.objectContaining({ success: true }),
77
+ );
78
+
79
+ await (plugin as any).buildEnd?.call({});
80
+ });
81
+
82
+ // Acceptance: watchChange 없이 buildStart 재호출 시에도 정상 동작
83
+ it("handles buildStart re-invocation without watchChange gracefully", async () => {
84
+ const onBuild = vi.fn();
85
+ const plugin = sdAngularPlugin({
86
+ tsconfig: TSCONFIG_PATH,
87
+ dev: true,
88
+ legacyModule: true,
89
+ onBuild,
90
+ });
91
+
92
+ // 초기 빌드
93
+ await (plugin as any).buildStart?.call({});
94
+
95
+ // watchChange 없이 재빌드 (예: Rolldown이 변경 없이 재빌드 트리거)
96
+ await (plugin as any).buildStart?.call({});
97
+
98
+ const appComponentPath = path
99
+ .join(FIXTURE_DIR, "src/app.component.ts")
100
+ .replace(/\\/g, "/");
101
+
102
+ const result = await (plugin as any).transform?.call({}, "", appComponentPath);
103
+ expect(result).toBeDefined();
104
+ expect(result.code.length).toBeGreaterThan(0);
105
+
106
+ await (plugin as any).buildEnd?.call({});
107
+ });
108
+ });
@@ -145,7 +145,7 @@ describe("vite-angular-plugin lint integration (Slice 5)", () => {
145
145
  });
146
146
  });
147
147
 
148
- describe("Scenario: lint runs in hotUpdate", () => {
148
+ describe("Scenario: lint runs in handleHotUpdate", () => {
149
149
  it("runs lint after incremental compilation and passes result to onBuild", async () => {
150
150
  const onBuildResults: any[] = [];
151
151
 
@@ -167,9 +167,9 @@ describe("vite-angular-plugin lint integration (Slice 5)", () => {
167
167
  formattedOutput: "lint errors found",
168
168
  });
169
169
 
170
- // Call hotUpdate
171
- const hotUpdate = (plugin as any).hotUpdate;
172
- await hotUpdate.call({}, {
170
+ // Call handleHotUpdate
171
+ const handleHotUpdate = (plugin as any).handleHotUpdate;
172
+ await handleHotUpdate.call({}, {
173
173
  file: "/workspace/packages/client/src/app.ts",
174
174
  modules: [],
175
175
  server: {},
@@ -5,13 +5,11 @@ import { sdAngularPlugin } from "../../src/angular/vite-angular-plugin.js";
5
5
  const FIXTURE_DIR = path.resolve(import.meta.dirname, "fixtures/basic-app");
6
6
  const TSCONFIG_PATH = path.join(FIXTURE_DIR, "tsconfig.json");
7
7
 
8
- function mockEnvironmentContext() {
8
+ function mockServerWithModuleGraph() {
9
9
  return {
10
- environment: {
11
- moduleGraph: {
12
- getModulesByFile: (file: string) => {
13
- return new Set([{ file, id: file }]);
14
- },
10
+ moduleGraph: {
11
+ getModulesByFile: (file: string) => {
12
+ return new Set([{ file, id: file }]);
15
13
  },
16
14
  },
17
15
  };
@@ -35,11 +33,10 @@ describe("sdAngularPlugin SCSS @use HMR", () => {
35
33
  .join(FIXTURE_DIR, "scss/_variables.scss")
36
34
  .replace(/\\/g, "/");
37
35
 
38
- const ctx = mockEnvironmentContext();
39
- const result = await (plugin as any).hotUpdate?.call(ctx, {
36
+ const result = await (plugin as any).handleHotUpdate?.({
40
37
  file: variablesPath,
41
38
  modules: [],
42
- server: {},
39
+ server: mockServerWithModuleGraph(),
43
40
  timestamp: Date.now(),
44
41
  read: () => Promise.resolve(""),
45
42
  });
@@ -54,11 +51,10 @@ describe("sdAngularPlugin SCSS @use HMR", () => {
54
51
  .join(FIXTURE_DIR, "scss/_colors.scss")
55
52
  .replace(/\\/g, "/");
56
53
 
57
- const ctx = mockEnvironmentContext();
58
- const result = await (plugin as any).hotUpdate?.call(ctx, {
54
+ const result = await (plugin as any).handleHotUpdate?.({
59
55
  file: colorsPath,
60
56
  modules: [],
61
- server: {},
57
+ server: mockServerWithModuleGraph(),
62
58
  timestamp: Date.now(),
63
59
  read: () => Promise.resolve(""),
64
60
  });
@@ -73,11 +69,10 @@ describe("sdAngularPlugin SCSS @use HMR", () => {
73
69
  .join(FIXTURE_DIR, "scss/_unrelated.scss")
74
70
  .replace(/\\/g, "/");
75
71
 
76
- const ctx = mockEnvironmentContext();
77
- const result = await (plugin as any).hotUpdate?.call(ctx, {
72
+ const result = await (plugin as any).handleHotUpdate?.({
78
73
  file: unrelatedPath,
79
74
  modules: [],
80
- server: {},
75
+ server: mockServerWithModuleGraph(),
81
76
  timestamp: Date.now(),
82
77
  read: () => Promise.resolve(""),
83
78
  });
@@ -54,7 +54,7 @@ describe("sdAngularPlugin", () => {
54
54
  });
55
55
 
56
56
  // Scenario: Angular 컴포넌트 .ts 파일 수정 시 컴포넌트 HMR (Acceptance — Feature 3.3)
57
- it("updates emit cache and returns affected modules when hotUpdate is called", async () => {
57
+ it("updates emit cache and returns affected modules when handleHotUpdate is called", async () => {
58
58
  const onBuildStart = vi.fn();
59
59
  const onBuild = vi.fn();
60
60
 
@@ -77,12 +77,12 @@ describe("sdAngularPlugin", () => {
77
77
  expect(initialResult).toBeDefined();
78
78
  expect(initialResult.code.length).toBeGreaterThan(0);
79
79
 
80
- // hotUpdate must exist
81
- expect((plugin as any).hotUpdate).toBeDefined();
80
+ // handleHotUpdate must exist
81
+ expect((plugin as any).handleHotUpdate).toBeDefined();
82
82
 
83
83
  // Simulate file change
84
84
  const mockModule = { file: appComponentPath, id: appComponentPath };
85
- const hmrResult = await (plugin as any).hotUpdate?.({
85
+ const hmrResult = await (plugin as any).handleHotUpdate?.({
86
86
  file: appComponentPath,
87
87
  modules: [mockModule],
88
88
  server: { watcher: { emit: vi.fn() } },
@@ -122,7 +122,7 @@ describe("sdAngularPlugin", () => {
122
122
  });
123
123
 
124
124
  // Scenario: 컴파일 에러 발생 및 복구 (Acceptance — Feature 3.3)
125
- it("calls onBuild with success=false when hotUpdate encounters compile error", async () => {
125
+ it("calls onBuild with success=false when handleHotUpdate encounters compile error", async () => {
126
126
  const onBuild = vi.fn();
127
127
  const plugin = sdAngularPlugin({
128
128
  tsconfig: TSCONFIG_PATH,
@@ -132,8 +132,8 @@ describe("sdAngularPlugin", () => {
132
132
 
133
133
  await (plugin as any).buildStart?.call({});
134
134
 
135
- // hotUpdate with a non-existent file — facade.update() should handle gracefully
136
- const _hmrResult = await (plugin as any).hotUpdate?.({
135
+ // handleHotUpdate with a non-existent file — facade.update() should handle gracefully
136
+ const _hmrResult = await (plugin as any).handleHotUpdate?.({
137
137
  file: path.join(FIXTURE_DIR, "src/nonexistent-file.ts").replace(/\\/g, "/"),
138
138
  modules: [],
139
139
  server: { watcher: { emit: vi.fn() } },
@@ -147,8 +147,8 @@ describe("sdAngularPlugin", () => {
147
147
  await (plugin as any).buildEnd?.call({});
148
148
  });
149
149
 
150
- // Scenario: non-Angular .ts 파일 수정 — hotUpdate passes through
151
- it("hotUpdate skips non-ts/html/scss files", async () => {
150
+ // Scenario: non-Angular .ts 파일 수정 — handleHotUpdate passes through
151
+ it("handleHotUpdate skips non-ts/html/scss files", async () => {
152
152
  const onBuildStart = vi.fn();
153
153
  const plugin = sdAngularPlugin({
154
154
  tsconfig: TSCONFIG_PATH,
@@ -159,7 +159,7 @@ describe("sdAngularPlugin", () => {
159
159
  await (plugin as any).buildStart?.call({});
160
160
 
161
161
  // .json file should be ignored
162
- const result = await (plugin as any).hotUpdate?.({
162
+ const result = await (plugin as any).handleHotUpdate?.({
163
163
  file: "/some/file.json",
164
164
  modules: [],
165
165
  server: {},
@@ -192,16 +192,81 @@ describe("sdAngularPlugin", () => {
192
192
  // (in real use, Vite server close triggers this)
193
193
  });
194
194
 
195
- // Scenario: optimizeDeps에 Angular Linker Rolldown 플러그인이 등록된다
196
- it("registers angular-vite-optimize-deps rolldown plugin in optimizeDeps config", () => {
195
+ // Scenario: optimizeDeps에 Angular Linker esbuild 플러그인이 등록된다
196
+ it("registers angular-vite-optimize-deps esbuild plugin in optimizeDeps config", () => {
197
197
  const plugin = sdAngularPlugin({ tsconfig: TSCONFIG_PATH, dev: true });
198
198
  const config = (plugin as any).config?.();
199
199
 
200
- const rolldownPlugins = config?.optimizeDeps?.rolldownOptions?.plugins as
200
+ const esbuildPlugins = config?.optimizeDeps?.esbuildOptions?.plugins as
201
201
  | { name: string }[]
202
202
  | undefined;
203
- expect(rolldownPlugins).toBeDefined();
204
- expect(rolldownPlugins!.some((p) => p.name === "angular-vite-optimize-deps")).toBe(true);
203
+ expect(esbuildPlugins).toBeDefined();
204
+ expect(esbuildPlugins!.some((p) => p.name === "angular-vite-optimize-deps")).toBe(true);
205
+ });
206
+
207
+ // Scenario: Linker 캐시 skip — replaceDeps 파일은 사전 번들링에서 건너뛴다
208
+ it("angular-vite-optimize-deps returns null for replaceDeps dist files", async () => {
209
+ const plugin = sdAngularPlugin({
210
+ tsconfig: TSCONFIG_PATH,
211
+ dev: true,
212
+ replaceDepDistPaths: ["/packages/my-client/node_modules/@scope/core/dist"],
213
+ });
214
+
215
+ const config = (plugin as any).config?.();
216
+ const esbuildPlugins = config?.optimizeDeps?.esbuildOptions?.plugins as
217
+ | { name: string; setup: (build: { onLoad: Function }) => void }[];
218
+ const optimizePlugin = esbuildPlugins.find(
219
+ (p) => p.name === "angular-vite-optimize-deps",
220
+ )!;
221
+
222
+ // esbuild onLoad 핸들러 추출
223
+ let onLoadHandler: (args: { path: string }) => Promise<unknown>;
224
+ optimizePlugin.setup({
225
+ onLoad(_filter: unknown, fn: (args: { path: string }) => Promise<unknown>) {
226
+ onLoadHandler = fn;
227
+ },
228
+ });
229
+
230
+ // replaceDeps dist 경로 내 .js 파일 → null 반환 (skip)
231
+ const result = await onLoadHandler!(
232
+ { path: "/packages/my-client/node_modules/@scope/core/dist/index.js" },
233
+ );
234
+ expect(result).toBeNull();
235
+ });
236
+
237
+ // Scenario: handleHotUpdate에서 replaceDeps .js 변경 시 full-reload 강제
238
+ it("handleHotUpdate triggers full-reload for replaceDeps .js files", async () => {
239
+ const plugin = sdAngularPlugin({
240
+ tsconfig: TSCONFIG_PATH,
241
+ dev: true,
242
+ replaceDepDistPaths: ["/packages/my-client/node_modules/@scope/core/dist"],
243
+ });
244
+
245
+ await (plugin as any).buildStart?.call({});
246
+
247
+ // configureServer로 server 참조 저장
248
+ const mockHotSend = vi.fn();
249
+ const mockServer = {
250
+ middlewares: { use: vi.fn() },
251
+ httpServer: { on: vi.fn() },
252
+ config: { base: "/" },
253
+ hot: { send: mockHotSend },
254
+ };
255
+ (plugin as any).configureServer?.(mockServer);
256
+
257
+ // replaceDeps .js 파일 변경 → full-reload
258
+ const hmrResult = await (plugin as any).handleHotUpdate?.({
259
+ file: "/packages/my-client/node_modules/@scope/core/dist/component.js",
260
+ modules: [],
261
+ server: mockServer,
262
+ timestamp: Date.now(),
263
+ read: () => Promise.resolve(""),
264
+ });
265
+
266
+ expect(mockHotSend).toHaveBeenCalledWith({ type: "full-reload" });
267
+ expect(hmrResult).toEqual([]);
268
+
269
+ await (plugin as any).buildEnd?.call({});
205
270
  });
206
271
 
207
272
  // Scenario: .mjs 파일이 JavaScriptTransformer를 통과한다 (Feature 1.1 Angular Linker)