@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.
- package/dist/angular/vite-angular-plugin.d.ts +2 -0
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +57 -28
- package/dist/angular/vite-angular-plugin.js.map +1 -1
- package/dist/capacitor/capacitor.d.ts +0 -1
- package/dist/capacitor/capacitor.d.ts.map +1 -1
- package/dist/capacitor/capacitor.js +12 -37
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/device.d.ts.map +1 -1
- package/dist/commands/device.js +3 -2
- package/dist/commands/device.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +9 -4
- package/dist/electron/electron.js.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.js +12 -0
- package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
- package/dist/utils/vite-config.d.ts +1 -1
- package/dist/utils/vite-config.d.ts.map +1 -1
- package/dist/utils/vite-config.js +76 -26
- package/dist/utils/vite-config.js.map +1 -1
- package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
- package/dist/utils/vite-scope-watch-plugin.js +7 -1
- package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
- package/dist/workers/server-runtime.worker.d.ts.map +1 -1
- package/dist/workers/server-runtime.worker.js +15 -0
- package/dist/workers/server-runtime.worker.js.map +1 -1
- package/package.json +9 -7
- package/src/angular/vite-angular-plugin.ts +88 -34
- package/src/capacitor/capacitor.ts +14 -46
- package/src/commands/device.ts +3 -2
- package/src/electron/electron.ts +11 -4
- package/src/orchestrators/DevWatchOrchestrator.ts +14 -0
- package/src/utils/vite-config.ts +83 -27
- package/src/utils/vite-scope-watch-plugin.ts +6 -1
- package/src/workers/server-runtime.worker.ts +15 -0
- package/tests/angular/linker-disk-cache.spec.ts +31 -25
- package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
- package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
- package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +108 -0
- package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
- package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +10 -15
- package/tests/angular/vite-angular-plugin.spec.ts +80 -15
- package/tests/capacitor/capacitor-workspace.spec.ts +22 -12
- package/tests/commands/device.spec.ts +12 -7
- package/tests/electron/electron.spec.ts +27 -2
- package/tests/utils/vite-config.spec.ts +255 -133
- package/tests/utils/vite-scope-watch-plugin.spec.ts +22 -0
- 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
|
|
21
|
+
type OnLoadHandler = (args: { path: string }) => Promise<{ contents: string; loader: string } | null>;
|
|
22
22
|
|
|
23
|
-
function
|
|
23
|
+
function getOnLoadHandler(plugin: ReturnType<typeof sdAngularPlugin>): OnLoadHandler {
|
|
24
24
|
const config = (plugin as any).config();
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const esbuildPlugin = config.optimizeDeps.esbuildOptions.plugins[0] as {
|
|
26
|
+
setup: (build: { onLoad: Function }) => void;
|
|
27
27
|
};
|
|
28
|
-
|
|
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
|
|
58
|
+
const onLoad = getOnLoadHandler(plugin);
|
|
53
59
|
|
|
54
60
|
// First call: cache miss
|
|
55
|
-
const result1 = await
|
|
61
|
+
const result1 = await onLoad({ path: jsFile });
|
|
56
62
|
expect(mockTransformFile).toHaveBeenCalledOnce();
|
|
57
|
-
expect(result1).
|
|
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
|
|
72
|
+
const result2 = await onLoad({ path: jsFile });
|
|
67
73
|
expect(mockTransformFile).not.toHaveBeenCalled();
|
|
68
|
-
expect(result2).
|
|
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
|
|
87
|
+
const onLoad = getOnLoadHandler(plugin);
|
|
82
88
|
|
|
83
|
-
await
|
|
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
|
|
97
|
+
const result = await onLoad({ path: jsFile });
|
|
92
98
|
expect(mockTransformFile).toHaveBeenCalledOnce();
|
|
93
|
-
expect(result).
|
|
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
|
|
112
|
+
const onLoad = getOnLoadHandler(plugin);
|
|
107
113
|
|
|
108
114
|
// First call to populate cache
|
|
109
|
-
await
|
|
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
|
|
122
|
+
const result = await onLoad({ path: jsFile });
|
|
117
123
|
expect(mockTransformFile).toHaveBeenCalledOnce();
|
|
118
|
-
expect(result).
|
|
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
|
|
139
|
+
const onLoad = getOnLoadHandler(plugin);
|
|
134
140
|
|
|
135
|
-
const result = await
|
|
136
|
-
expect(result).
|
|
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
|
|
146
|
+
const result2 = await onLoad({ path: jsFile });
|
|
141
147
|
expect(mockTransformFile).not.toHaveBeenCalled();
|
|
142
|
-
expect(result2).
|
|
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
|
|
158
|
+
const onLoad = getOnLoadHandler(plugin);
|
|
153
159
|
|
|
154
|
-
const result = await
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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
|
-
//
|
|
213
|
-
const hmrResult = await (plugin as any).
|
|
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
|
-
//
|
|
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)에서는
|
|
253
|
-
it("returns undefined from
|
|
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).
|
|
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개 이상일 때도
|
|
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
|
-
//
|
|
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).
|
|
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
|
-
//
|
|
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:
|
|
135
|
-
it("collects templateUpdates from
|
|
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
|
-
//
|
|
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).
|
|
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
|
|
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
|
-
// 첫 번째
|
|
199
|
-
await (plugin as any).
|
|
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
|
-
// 두 번째
|
|
208
|
-
await (plugin as any).
|
|
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
|
|
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
|
|
171
|
-
const
|
|
172
|
-
await
|
|
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
|
|
8
|
+
function mockServerWithModuleGraph() {
|
|
9
9
|
return {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
81
|
-
expect((plugin as any).
|
|
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).
|
|
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
|
|
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
|
-
//
|
|
136
|
-
const _hmrResult = await (plugin as any).
|
|
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 파일 수정 —
|
|
151
|
-
it("
|
|
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).
|
|
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
|
|
196
|
-
it("registers angular-vite-optimize-deps
|
|
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
|
|
200
|
+
const esbuildPlugins = config?.optimizeDeps?.esbuildOptions?.plugins as
|
|
201
201
|
| { name: string }[]
|
|
202
202
|
| undefined;
|
|
203
|
-
expect(
|
|
204
|
-
expect(
|
|
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)
|