@simplysm/sd-cli 14.0.36 → 14.0.38
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 -5
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +19 -72
- package/dist/angular/vite-angular-plugin.js.map +1 -1
- package/dist/commands/publish/storage-publisher.js +1 -0
- package/dist/commands/publish/storage-publisher.js.map +1 -1
- package/dist/dev-server/hmr-service.d.ts +2 -0
- package/dist/dev-server/hmr-service.d.ts.map +1 -1
- package/dist/dev-server/hmr-service.js +24 -10
- package/dist/dev-server/hmr-service.js.map +1 -1
- package/dist/electron/electron.js +4 -4
- package/dist/electron/electron.js.map +1 -1
- package/dist/engines/BaseEngine.d.ts.map +1 -1
- package/dist/engines/BaseEngine.js +10 -1
- package/dist/engines/BaseEngine.js.map +1 -1
- package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
- package/dist/engines/EsbuildClientEngine.js +12 -1
- package/dist/engines/EsbuildClientEngine.js.map +1 -1
- package/dist/engines/engine-factory.d.ts +0 -4
- package/dist/engines/engine-factory.d.ts.map +1 -1
- package/dist/engines/engine-factory.js.map +1 -1
- package/dist/esbuild/esbuild-client-config.js +5 -5
- package/dist/esbuild/esbuild-client-config.js.map +1 -1
- package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevOrchestrator.js +0 -5
- package/dist/orchestrators/DevOrchestrator.js.map +1 -1
- package/dist/orchestrators/TypecheckOrchestrator.js +2 -2
- package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -1
- package/dist/sd-cli.js +2 -1
- package/dist/sd-cli.js.map +1 -1
- package/dist/utils/output-utils.d.ts.map +1 -1
- package/dist/utils/output-utils.js +3 -2
- package/dist/utils/output-utils.js.map +1 -1
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +7 -1
- package/dist/workers/client.worker.js.map +1 -1
- package/package.json +4 -4
- package/src/angular/vite-angular-plugin.ts +19 -82
- package/src/commands/publish/storage-publisher.ts +1 -0
- package/src/dev-server/hmr-service.ts +28 -13
- package/src/electron/electron.ts +5 -5
- package/src/engines/BaseEngine.ts +10 -1
- package/src/engines/EsbuildClientEngine.ts +13 -1
- package/src/engines/engine-factory.ts +0 -1
- package/src/esbuild/esbuild-client-config.ts +6 -6
- package/src/orchestrators/DevOrchestrator.ts +0 -6
- package/src/orchestrators/TypecheckOrchestrator.ts +2 -2
- package/src/sd-cli.ts +2 -1
- package/src/utils/output-utils.ts +3 -2
- package/src/workers/client.worker.ts +8 -1
- package/tests/angular/_vite-angular-plugin-test-setup.ts +3 -30
- package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +14 -31
- package/tests/angular/vite-angular-plugin-vitest.spec.ts +4 -34
- package/tests/angular/vite-angular-plugin.spec.ts +24 -60
- package/tests/commands/typecheck.spec.ts +1 -1
- package/tests/engines/base-engine.spec.ts +25 -0
- package/tests/engines/engine-adapter-isolation.spec.ts +3 -3
- package/tests/engines/esbuild-client-engine.acc.spec.ts +29 -0
- package/tests/engines/esbuild-client-engine.spec.ts +26 -0
- package/tests/orchestrators/build-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/dev-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/typecheck-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/watch-orchestrator.spec.ts +1 -1
- package/tests/utils/esbuild-client-config.acc.spec.ts +4 -1
- package/tests/utils/hmr-service-dispatcher.acc.spec.ts +70 -0
- package/tests/workers/client-worker-initial-build-error.verify.md +8 -0
|
@@ -1,114 +1,95 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import type { SdConfig } from "../../src/sd-config.types";
|
|
4
3
|
import {
|
|
5
4
|
FIXTURE_DIR,
|
|
6
5
|
PKG_DIR,
|
|
7
|
-
createTestSdConfig,
|
|
8
6
|
initPlugin,
|
|
9
7
|
} from "./_vite-angular-plugin-test-setup";
|
|
10
8
|
|
|
11
|
-
const mockLoadSdConfig = vi.fn<(...args: unknown[]) => Promise<SdConfig>>();
|
|
12
|
-
|
|
13
|
-
vi.mock("../../src/utils/sd-config", () => ({
|
|
14
|
-
loadSdConfig: (...args: unknown[]) => mockLoadSdConfig(...args),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
9
|
const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin.js");
|
|
18
10
|
|
|
19
11
|
beforeEach(() => {
|
|
20
12
|
vi.clearAllMocks();
|
|
21
13
|
vi.spyOn(process, "cwd").mockReturnValue(FIXTURE_DIR);
|
|
22
|
-
mockLoadSdConfig.mockResolvedValue(createTestSdConfig());
|
|
23
14
|
});
|
|
24
15
|
|
|
25
16
|
describe("sdAngularPlugin", () => {
|
|
26
|
-
// config() define 반환은 Vitest 축소로 제거됨 (esbuild-client-config에서 처리)
|
|
27
|
-
|
|
28
17
|
// Scenario: non-Angular .ts 파일은 기본 처리
|
|
29
18
|
it("returns undefined for non-emitted .ts files", async () => {
|
|
30
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app"});
|
|
19
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
31
20
|
await initPlugin(plugin);
|
|
32
21
|
|
|
33
|
-
const result =
|
|
22
|
+
const result = (plugin as any).transform?.call(
|
|
34
23
|
{},
|
|
35
24
|
"export const x = 1;",
|
|
36
25
|
"/some/unknown/file.ts",
|
|
37
26
|
);
|
|
38
27
|
|
|
39
28
|
expect(result).toBeUndefined();
|
|
40
|
-
|
|
29
|
+
(plugin as any).buildEnd?.call({});
|
|
41
30
|
});
|
|
42
31
|
|
|
43
32
|
// Scenario: buildStart에서 초기화 및 emit + buildEnd에서 리소스 정리
|
|
44
33
|
it("initializes facade in buildStart and disposes in buildEnd", async () => {
|
|
45
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app"});
|
|
34
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
46
35
|
await initPlugin(plugin);
|
|
47
|
-
|
|
36
|
+
(plugin as any).buildEnd?.call({});
|
|
48
37
|
});
|
|
49
38
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Scenario: .mjs 파일이 JavaScriptTransformer를 통과한다
|
|
54
|
-
it("transforms .mjs files through JavaScriptTransformer", async () => {
|
|
55
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app"});
|
|
39
|
+
// Scenario: .mjs/.js 파일은 처리하지 않는다
|
|
40
|
+
it("returns undefined for .mjs files", async () => {
|
|
41
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
56
42
|
await initPlugin(plugin);
|
|
57
43
|
|
|
58
|
-
const result =
|
|
44
|
+
const result = (plugin as any).transform?.call(
|
|
59
45
|
{},
|
|
60
46
|
"export const x = 1;",
|
|
61
47
|
"/some/library/module.mjs",
|
|
62
48
|
);
|
|
63
49
|
|
|
64
|
-
expect(result).
|
|
65
|
-
|
|
66
|
-
expect(typeof result.code).toBe("string");
|
|
67
|
-
await (plugin as any).buildEnd?.call({});
|
|
50
|
+
expect(result).toBeUndefined();
|
|
51
|
+
(plugin as any).buildEnd?.call({});
|
|
68
52
|
});
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app"});
|
|
54
|
+
it("returns undefined for .js files", async () => {
|
|
55
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
73
56
|
await initPlugin(plugin);
|
|
74
57
|
|
|
75
|
-
const result =
|
|
58
|
+
const result = (plugin as any).transform?.call(
|
|
76
59
|
{},
|
|
77
60
|
"export const y = 2;",
|
|
78
61
|
"/some/library/module.js",
|
|
79
62
|
);
|
|
80
63
|
|
|
81
|
-
expect(result).
|
|
82
|
-
|
|
83
|
-
expect(typeof result.code).toBe("string");
|
|
84
|
-
await (plugin as any).buildEnd?.call({});
|
|
64
|
+
expect(result).toBeUndefined();
|
|
65
|
+
(plugin as any).buildEnd?.call({});
|
|
85
66
|
});
|
|
86
67
|
|
|
87
68
|
// Scenario: 비대상 파일(.css 등)은 transform하지 않는다
|
|
88
69
|
it("returns undefined for non-JS files like .css", async () => {
|
|
89
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app"});
|
|
70
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
90
71
|
await initPlugin(plugin);
|
|
91
72
|
|
|
92
|
-
const result =
|
|
73
|
+
const result = (plugin as any).transform?.call(
|
|
93
74
|
{},
|
|
94
75
|
"body { color: red; }",
|
|
95
76
|
"/some/styles.css",
|
|
96
77
|
);
|
|
97
78
|
|
|
98
79
|
expect(result).toBeUndefined();
|
|
99
|
-
|
|
80
|
+
(plugin as any).buildEnd?.call({});
|
|
100
81
|
});
|
|
101
82
|
|
|
102
|
-
// Scenario: Angular .ts 파일 transform
|
|
83
|
+
// Scenario: Angular .ts 파일 transform
|
|
103
84
|
it("transforms emitted .ts files with compiled JS", async () => {
|
|
104
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app"});
|
|
105
|
-
await initPlugin(plugin
|
|
85
|
+
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
86
|
+
await initPlugin(plugin);
|
|
106
87
|
|
|
107
88
|
const appComponentPath = path
|
|
108
89
|
.join(PKG_DIR, "src/app.component.ts")
|
|
109
90
|
.replace(/\\/g, "/");
|
|
110
91
|
|
|
111
|
-
const result =
|
|
92
|
+
const result = (plugin as any).transform?.call({}, "", appComponentPath);
|
|
112
93
|
|
|
113
94
|
if (result != null) {
|
|
114
95
|
expect(result).toHaveProperty("code");
|
|
@@ -116,23 +97,6 @@ describe("sdAngularPlugin", () => {
|
|
|
116
97
|
expect(result.code.length).toBeGreaterThan(0);
|
|
117
98
|
}
|
|
118
99
|
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// config() define 반환값은 Vitest 축소로 제거됨 (Feature 3.3에서 별도 테스트)
|
|
123
|
-
|
|
124
|
-
// Scenario: browserSupport 미설정 시 기본값 동작
|
|
125
|
-
it("works when browserSupport is not set in sd.config.ts", async () => {
|
|
126
|
-
mockLoadSdConfig.mockResolvedValue(createTestSdConfig());
|
|
127
|
-
|
|
128
|
-
const plugin = sdAngularPlugin({ pkg: "basic-app" });
|
|
129
|
-
|
|
130
|
-
await initPlugin(plugin);
|
|
131
|
-
|
|
132
|
-
const appComponentPath = PKG_DIR.replace(/\\/g, "/") + "/src/app.component.ts";
|
|
133
|
-
const result = await (plugin as any).transform?.call({}, "", appComponentPath);
|
|
134
|
-
expect(result).toBeDefined();
|
|
135
|
-
|
|
136
|
-
await (plugin as any).buildEnd?.call({});
|
|
100
|
+
(plugin as any).buildEnd?.call({});
|
|
137
101
|
});
|
|
138
102
|
});
|
|
@@ -27,7 +27,7 @@ vi.mock("../../src/typecheck/typecheck-non-package", () => ({
|
|
|
27
27
|
typecheckNonPackageFiles: mocks.typecheckNonPackageFiles,
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
-
vi.mock("../../src/engines/
|
|
30
|
+
vi.mock("../../src/engines/engine-factory", () => ({
|
|
31
31
|
createTypecheckEngine: mocks.createTypecheckEngine,
|
|
32
32
|
}));
|
|
33
33
|
|
|
@@ -254,6 +254,31 @@ describe("BaseEngine", () => {
|
|
|
254
254
|
await engine.stop();
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
it("_callStartWatch 실패 시 에러를 ResultCollector에 보고하고 resolveInitialBuild를 호출한다", async () => {
|
|
258
|
+
const mockResultCollector = { add: vi.fn() };
|
|
259
|
+
|
|
260
|
+
mockWorker.startWatch.mockRejectedValue(new Error("Worker RPC failed"));
|
|
261
|
+
|
|
262
|
+
const engine = new TscEngine({
|
|
263
|
+
cwd: "/root",
|
|
264
|
+
pkg: createMockPkg(),
|
|
265
|
+
resultCollector: mockResultCollector as any,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// startWatch()가 hang하지 않고 resolve되어야 한다
|
|
269
|
+
await engine.startWatch({ js: true, dts: true });
|
|
270
|
+
|
|
271
|
+
// ResultCollector에 에러가 보고되어야 한다
|
|
272
|
+
const errorReport = mockResultCollector.add.mock.calls.find(
|
|
273
|
+
(c: any[]) => c[0].type === "build" && c[0].status === "error",
|
|
274
|
+
);
|
|
275
|
+
expect(errorReport).toBeDefined();
|
|
276
|
+
expect(errorReport![0].name).toBe("test-pkg");
|
|
277
|
+
expect(errorReport![0].message).toContain("Worker RPC failed");
|
|
278
|
+
|
|
279
|
+
await engine.stop();
|
|
280
|
+
});
|
|
281
|
+
|
|
257
282
|
it("calls resolver on error event to release RebuildManager batch", async () => {
|
|
258
283
|
const mockResolver = vi.fn();
|
|
259
284
|
const mockRebuildManager = { registerBuild: vi.fn(() => mockResolver) };
|
|
@@ -25,7 +25,7 @@ describe("EsbuildClientEngine adapter isolation", () => {
|
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it("vite-angular-plugin.ts
|
|
28
|
+
it("vite-angular-plugin.ts does not import @angular/* directly", () => {
|
|
29
29
|
|
|
30
30
|
const pluginFile = path.resolve(
|
|
31
31
|
import.meta.dirname,
|
|
@@ -33,8 +33,8 @@ describe("EsbuildClientEngine adapter isolation", () => {
|
|
|
33
33
|
);
|
|
34
34
|
const content = fs.readFileSync(pluginFile, "utf-8");
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
expect(content).
|
|
36
|
+
const angularImportPattern = /from\s+["']@angular\/(build|compiler-cli)/;
|
|
37
|
+
expect(angularImportPattern.test(content)).toBe(false);
|
|
38
38
|
expect(content).not.toContain("createAngularCompilation");
|
|
39
39
|
expect(content).not.toMatch(/\bSourceFileCache\b(?<!AngularSourceFileCache)/);
|
|
40
40
|
expect(content).not.toContain("ComponentStylesheetBundler");
|
|
@@ -134,6 +134,35 @@ describe("EsbuildClientEngine Acceptance", () => {
|
|
|
134
134
|
await engine.stop();
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
// Scenario: 초기 빌드 실패 시 ResultCollector에 에러 보고
|
|
138
|
+
it("초기 빌드 실패 시 ResultCollector에 에러가 보고된다", async () => {
|
|
139
|
+
// Given: client 패키지가 정의되어 있다
|
|
140
|
+
const mockResultCollector = { add: vi.fn() };
|
|
141
|
+
|
|
142
|
+
mockWorker.startWatch.mockResolvedValue({
|
|
143
|
+
success: false,
|
|
144
|
+
errors: ["esbuild compilation failed"],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const engine = new EsbuildClientEngine({
|
|
148
|
+
cwd: "/root",
|
|
149
|
+
pkg: createMockPkg(),
|
|
150
|
+
resultCollector: mockResultCollector as any,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// When: startWatch()를 호출한다
|
|
154
|
+
await engine.startWatch({ js: true, dts: false });
|
|
155
|
+
|
|
156
|
+
// Then: ResultCollector에 에러가 보고된다
|
|
157
|
+
const errorReport = mockResultCollector.add.mock.calls.find(
|
|
158
|
+
(c: any[]) => c[0].type === "build" && c[0].status === "error",
|
|
159
|
+
);
|
|
160
|
+
expect(errorReport).toBeDefined();
|
|
161
|
+
expect(errorReport![0].name).toBe("my-client");
|
|
162
|
+
|
|
163
|
+
await engine.stop();
|
|
164
|
+
});
|
|
165
|
+
|
|
137
166
|
// Scenario: 엔진 중지
|
|
138
167
|
it("stop()으로 worker를 종료하고 .dev-port를 삭제한다", async () => {
|
|
139
168
|
// Given: dev watch 모드가 실행 중이다
|
|
@@ -271,6 +271,32 @@ describe("EsbuildClientEngine", () => {
|
|
|
271
271
|
await engine.stop();
|
|
272
272
|
});
|
|
273
273
|
|
|
274
|
+
it("초기 빌드 실패 시 에러를 logger.error로 출력한다", async () => {
|
|
275
|
+
mockWorker.startWatch.mockResolvedValue({
|
|
276
|
+
success: false,
|
|
277
|
+
errors: ["Module not found: @angular/core", "Syntax error in app.ts"],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const engine = new EsbuildClientEngine({ cwd: "/root", pkg: createMockPkg() });
|
|
281
|
+
await engine.startWatch({ js: true, dts: false });
|
|
282
|
+
|
|
283
|
+
// startWatch가 reject하지 않고 정상 완료되는지 확인 (기존 동작 유지)
|
|
284
|
+
// 에러 로깅은 verify.md에서 검증
|
|
285
|
+
expect(mockWorker.startWatch).toHaveBeenCalled();
|
|
286
|
+
|
|
287
|
+
await engine.stop();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("초기 빌드 성공 시 에러 로깅을 하지 않는다", async () => {
|
|
291
|
+
mockWorker.startWatch.mockResolvedValue({ success: true });
|
|
292
|
+
|
|
293
|
+
const engine = new EsbuildClientEngine({ cwd: "/root", pkg: createMockPkg() });
|
|
294
|
+
// 예외 없이 완료
|
|
295
|
+
await expect(engine.startWatch({ js: true, dts: false })).resolves.toBeUndefined();
|
|
296
|
+
|
|
297
|
+
await engine.stop();
|
|
298
|
+
});
|
|
299
|
+
|
|
274
300
|
it("scopeRebuild 이벤트를 구독하지 않는다", async () => {
|
|
275
301
|
mockWorker.startWatch.mockResolvedValue({ success: true });
|
|
276
302
|
|
|
@@ -59,7 +59,7 @@ const mockEngines: Array<{
|
|
|
59
59
|
stop: ReturnType<typeof vi.fn>;
|
|
60
60
|
}> = [];
|
|
61
61
|
|
|
62
|
-
vi.mock("../../src/engines/
|
|
62
|
+
vi.mock("../../src/engines/engine-factory", () => ({
|
|
63
63
|
createBuildEngine: vi.fn(() => {
|
|
64
64
|
const engine = {
|
|
65
65
|
run: vi.fn().mockResolvedValue({
|
|
@@ -40,7 +40,7 @@ const mockBuildEngines: Array<{
|
|
|
40
40
|
port?: number;
|
|
41
41
|
}> = [];
|
|
42
42
|
|
|
43
|
-
vi.mock("../../src/engines/
|
|
43
|
+
vi.mock("../../src/engines/engine-factory", () => ({
|
|
44
44
|
createBuildEngine: vi.fn((pkg: any, options: any) => {
|
|
45
45
|
const engine = {
|
|
46
46
|
run: vi.fn().mockResolvedValue({
|
|
@@ -28,7 +28,7 @@ vi.mock("../../src/typecheck/typecheck-non-package", () => ({
|
|
|
28
28
|
typecheckNonPackageFiles: mocks.typecheckNonPackageFiles,
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
|
-
vi.mock("../../src/engines/
|
|
31
|
+
vi.mock("../../src/engines/engine-factory", () => ({
|
|
32
32
|
createTypecheckEngine: mocks.createTypecheckEngine,
|
|
33
33
|
}));
|
|
34
34
|
|
|
@@ -45,7 +45,7 @@ const mockBuildEngines: Array<{
|
|
|
45
45
|
_pkgName: string;
|
|
46
46
|
}> = [];
|
|
47
47
|
|
|
48
|
-
vi.mock("../../src/engines/
|
|
48
|
+
vi.mock("../../src/engines/engine-factory", () => ({
|
|
49
49
|
createBuildEngine: vi.fn((pkg: any, options: any) => {
|
|
50
50
|
const engine = {
|
|
51
51
|
run: vi.fn().mockResolvedValue({
|
|
@@ -108,7 +108,7 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
108
108
|
expect(pluginOpts.incremental).toBe(true);
|
|
109
109
|
expect(pluginOpts.sourceFileCache).toBe(mockSourceFileCache);
|
|
110
110
|
expect(pluginOpts.loadResultCache).toBe(mockSourceFileCache.loadResultCache);
|
|
111
|
-
expect(
|
|
111
|
+
expect(pluginOpts.includeTestMetadata).toBe(true);
|
|
112
112
|
|
|
113
113
|
// BundleStylesheetOptions
|
|
114
114
|
expect(styleOpts.workspaceRoot).toBe("/workspace");
|
|
@@ -161,6 +161,9 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
161
161
|
expect(pluginOpts.sourcemap).toBe(false);
|
|
162
162
|
expect(pluginOpts.advancedOptimizations).toBe(true);
|
|
163
163
|
expect(pluginOpts.thirdPartySourcemaps).toBe(false);
|
|
164
|
+
expect(pluginOpts.incremental).toBe(false);
|
|
165
|
+
expect(pluginOpts.includeTestMetadata).toBe(false);
|
|
166
|
+
expect((pluginOpts as unknown as Record<string, unknown>)["browserOnlyBuild"]).toBeUndefined();
|
|
164
167
|
|
|
165
168
|
// BundleStylesheetOptions
|
|
166
169
|
expect(styleOpts.optimization).toBe(true);
|
|
@@ -155,6 +155,76 @@ describe("HMR 디스패처 통합", () => {
|
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
describe("Scenario: 파일 크기 동일하지만 내용 변경 → 변경 감지", () => {
|
|
159
|
+
it("CSS 내용이 변경되었지만 크기가 동일한 경우 css-update를 전송한다", async () => {
|
|
160
|
+
// 임시 dist 디렉토리에 CSS 파일 생성
|
|
161
|
+
const distDir = path.join(tmpDir, "dist-hash-test");
|
|
162
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
163
|
+
fs.writeFileSync(path.join(distDir, "main.css"), "body { color: red; }");
|
|
164
|
+
fs.writeFileSync(path.join(distDir, "main.js"), "console.log('hello');");
|
|
165
|
+
|
|
166
|
+
// outDir을 전달하는 HMR 서비스 생성
|
|
167
|
+
hmrService.close();
|
|
168
|
+
await new Promise<void>((resolve, reject) => {
|
|
169
|
+
httpServer.close((err) => {
|
|
170
|
+
if (err != null) reject(err);
|
|
171
|
+
else resolve();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
httpServer = http.createServer((_req, res) => { res.writeHead(404); res.end(); });
|
|
176
|
+
hmrService = createHmrService({
|
|
177
|
+
httpServer,
|
|
178
|
+
basePath: "/app/",
|
|
179
|
+
templateUpdates,
|
|
180
|
+
outDir: distDir,
|
|
181
|
+
});
|
|
182
|
+
port = await new Promise<number>((resolve, reject) => {
|
|
183
|
+
httpServer.listen(0, "127.0.0.1", () => {
|
|
184
|
+
const addr = httpServer.address();
|
|
185
|
+
if (typeof addr === "object" && addr != null) resolve(addr.port);
|
|
186
|
+
else reject(new Error("포트 감지 실패"));
|
|
187
|
+
});
|
|
188
|
+
httpServer.on("error", reject);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const cssContent = "body { color: red; }";
|
|
192
|
+
const metafile1: esbuild.Metafile = {
|
|
193
|
+
inputs: {},
|
|
194
|
+
outputs: {
|
|
195
|
+
"main.js": { bytes: 22, inputs: {}, imports: [], exports: [] },
|
|
196
|
+
"main.css": { bytes: cssContent.length, inputs: {}, imports: [], exports: [] },
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// 첫 빌드 (baseline)
|
|
201
|
+
hmrService.onBuildEnd(metafile1);
|
|
202
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
203
|
+
|
|
204
|
+
// CSS 내용 변경 (같은 크기)
|
|
205
|
+
const newCssContent = "body { color: blu; }"; // 같은 길이
|
|
206
|
+
fs.writeFileSync(path.join(distDir, "main.css"), newCssContent);
|
|
207
|
+
|
|
208
|
+
const ws = await connectWs();
|
|
209
|
+
const msgPromise = waitForMessage(ws);
|
|
210
|
+
|
|
211
|
+
const metafile2: esbuild.Metafile = {
|
|
212
|
+
inputs: {},
|
|
213
|
+
outputs: {
|
|
214
|
+
"main.js": { bytes: 22, inputs: {}, imports: [], exports: [] },
|
|
215
|
+
"main.css": { bytes: newCssContent.length, inputs: {}, imports: [], exports: [] },
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
hmrService.onBuildEnd(metafile2);
|
|
219
|
+
|
|
220
|
+
const msg = await msgPromise;
|
|
221
|
+
expect(msg["type"]).toBe("css-update");
|
|
222
|
+
|
|
223
|
+
ws.close();
|
|
224
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
158
228
|
describe("Scenario: CSS-only 변경 → css-update 메시지", () => {
|
|
159
229
|
it("JS 출력은 동일하고 CSS만 변경되면 css-update를 전송한다", async () => {
|
|
160
230
|
// 초기 빌드
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# 초기 빌드 에러 보고 — LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] client.worker.ts onEnd에서 초기 빌드 시 errors 필드가 포함되는지: `client.worker.ts:285-291` — `initialBuildResolve`에 `errors: result.errors.map((e) => e.text)` 포함 확인
|
|
6
|
+
- [x] EsbuildClientEngine.startWatch에서 반환값의 success 확인 후 에러 로깅하는지: `EsbuildClientEngine.ts:135-137` — `result != null && !result.success` 조건으로 `logger.error` 호출 확인. 테스트 실행 시 `[my-client] 초기 빌드 실패: Module not found: @angular/core; Syntax error in app.ts` 출력 확인
|
|
7
|
+
- [x] esbuild-client-config.ts에서 dev 모드 logLevel이 "warning"인지: `esbuild-client-config.ts:169` — `logLevel: isDev ? "warning" : "silent"` 확인. `isDev = options.mode === "dev"` (line 52)
|
|
8
|
+
- [x] esbuild-client-config.ts에서 build 모드 logLevel이 "silent"인지: 동일 라인 — 삼항 연산자의 false 분기가 `"silent"` 확인
|