@simplysm/sd-cli 14.0.37 → 14.0.39

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 (107) hide show
  1. package/dist/angular/angular-build-pipeline.d.ts +1 -1
  2. package/dist/angular/angular-build-pipeline.js +1 -1
  3. package/dist/angular/client-transform-stylesheet.d.ts +1 -1
  4. package/dist/angular/client-transform-stylesheet.js +3 -3
  5. package/dist/angular/vite-angular-plugin.d.ts +2 -5
  6. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  7. package/dist/angular/vite-angular-plugin.js +19 -72
  8. package/dist/angular/vite-angular-plugin.js.map +1 -1
  9. package/dist/commands/publish/storage-publisher.js +1 -0
  10. package/dist/commands/publish/storage-publisher.js.map +1 -1
  11. package/dist/dev-server/hmr-service.d.ts +2 -0
  12. package/dist/dev-server/hmr-service.d.ts.map +1 -1
  13. package/dist/dev-server/hmr-service.js +24 -10
  14. package/dist/dev-server/hmr-service.js.map +1 -1
  15. package/dist/electron/electron.js +4 -4
  16. package/dist/electron/electron.js.map +1 -1
  17. package/dist/engines/BaseEngine.d.ts.map +1 -1
  18. package/dist/engines/BaseEngine.js +10 -1
  19. package/dist/engines/BaseEngine.js.map +1 -1
  20. package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
  21. package/dist/engines/EsbuildClientEngine.js +12 -1
  22. package/dist/engines/EsbuildClientEngine.js.map +1 -1
  23. package/dist/engines/engine-factory.d.ts +0 -4
  24. package/dist/engines/engine-factory.d.ts.map +1 -1
  25. package/dist/engines/engine-factory.js.map +1 -1
  26. package/dist/esbuild/esbuild-client-config.d.ts +0 -2
  27. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  28. package/dist/esbuild/esbuild-client-config.js +24 -14
  29. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  30. package/dist/esbuild/esbuild-postcss-plugin.d.ts +8 -0
  31. package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -0
  32. package/dist/esbuild/esbuild-postcss-plugin.js +105 -0
  33. package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -0
  34. package/dist/esbuild/esbuild-tsc-plugin.d.ts +23 -0
  35. package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -0
  36. package/dist/esbuild/esbuild-tsc-plugin.js +60 -0
  37. package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -0
  38. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  39. package/dist/orchestrators/DevOrchestrator.js +0 -5
  40. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  41. package/dist/orchestrators/TypecheckOrchestrator.js +2 -2
  42. package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -1
  43. package/dist/sd-cli.js +2 -1
  44. package/dist/sd-cli.js.map +1 -1
  45. package/dist/utils/output-utils.d.ts.map +1 -1
  46. package/dist/utils/output-utils.js +3 -2
  47. package/dist/utils/output-utils.js.map +1 -1
  48. package/dist/workers/client.worker.d.ts.map +1 -1
  49. package/dist/workers/client.worker.js +39 -3
  50. package/dist/workers/client.worker.js.map +1 -1
  51. package/dist/workers/server-build.worker.d.ts.map +1 -1
  52. package/dist/workers/server-build.worker.js +129 -90
  53. package/dist/workers/server-build.worker.js.map +1 -1
  54. package/dist/workers/server-esbuild-context.d.ts +27 -0
  55. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  56. package/dist/workers/server-esbuild-context.js +43 -3
  57. package/dist/workers/server-esbuild-context.js.map +1 -1
  58. package/package.json +6 -4
  59. package/src/angular/angular-build-pipeline.ts +2 -2
  60. package/src/angular/client-transform-stylesheet.ts +4 -4
  61. package/src/angular/vite-angular-plugin.ts +19 -82
  62. package/src/commands/publish/storage-publisher.ts +1 -0
  63. package/src/dev-server/hmr-service.ts +28 -13
  64. package/src/electron/electron.ts +5 -5
  65. package/src/engines/BaseEngine.ts +10 -1
  66. package/src/engines/EsbuildClientEngine.ts +13 -1
  67. package/src/engines/engine-factory.ts +0 -1
  68. package/src/esbuild/esbuild-client-config.ts +27 -18
  69. package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
  70. package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
  71. package/src/orchestrators/DevOrchestrator.ts +0 -6
  72. package/src/orchestrators/TypecheckOrchestrator.ts +2 -2
  73. package/src/sd-cli.ts +2 -1
  74. package/src/utils/output-utils.ts +3 -2
  75. package/src/workers/client.worker.ts +40 -3
  76. package/src/workers/server-build.worker.ts +136 -97
  77. package/src/workers/server-esbuild-context.ts +59 -3
  78. package/tests/angular/_vite-angular-plugin-test-setup.ts +3 -30
  79. package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
  80. package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +14 -31
  81. package/tests/angular/vite-angular-plugin-vitest.spec.ts +4 -34
  82. package/tests/angular/vite-angular-plugin.spec.ts +24 -60
  83. package/tests/commands/typecheck.spec.ts +1 -1
  84. package/tests/engines/base-engine.spec.ts +25 -0
  85. package/tests/engines/engine-adapter-isolation.spec.ts +3 -3
  86. package/tests/engines/esbuild-client-engine.acc.spec.ts +29 -0
  87. package/tests/engines/esbuild-client-engine.spec.ts +26 -0
  88. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
  89. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
  90. package/tests/orchestrators/build-orchestrator.spec.ts +1 -1
  91. package/tests/orchestrators/dev-orchestrator.spec.ts +1 -1
  92. package/tests/orchestrators/typecheck-orchestrator.spec.ts +1 -1
  93. package/tests/orchestrators/watch-orchestrator.spec.ts +1 -1
  94. package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
  95. package/tests/utils/esbuild-client-config.acc.spec.ts +30 -15
  96. package/tests/utils/esbuild-client-config.spec.ts +73 -11
  97. package/tests/utils/esbuild-postcss-plugin.acc.spec.ts +299 -0
  98. package/tests/utils/esbuild-postcss-plugin.spec.ts +290 -0
  99. package/tests/utils/esbuild-scss-plugin.acc.spec.ts +1 -0
  100. package/tests/utils/hmr-service-dispatcher.acc.spec.ts +70 -0
  101. package/tests/workers/client-worker-initial-build-error.verify.md +8 -0
  102. package/tests/workers/server-build-lint.spec.ts +43 -0
  103. package/tests/workers/server-build-worker-refactoring.verify.md +14 -0
  104. package/tests/workers/server-build-worker.spec.ts +122 -9
  105. package/tests/workers/server-esbuild-context-tsc.verify.md +7 -0
  106. package/tests/workers/server-esbuild-context.acc.spec.ts +156 -2
  107. package/tests/workers/server-esbuild-context.spec.ts +320 -2
@@ -0,0 +1,290 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import postcss from "postcss";
6
+ import type esbuild from "esbuild";
7
+ import { createPostcssPlugin } from "../../src/esbuild/esbuild-postcss-plugin";
8
+
9
+ // --- Helpers ---
10
+
11
+ function markerPlugin(): postcss.Plugin {
12
+ return {
13
+ postcssPlugin: "test-marker",
14
+ Once(root) {
15
+ root.prepend(new postcss.Comment({ text: "marker" }));
16
+ },
17
+ };
18
+ }
19
+
20
+ function captureOnEnd(
21
+ plugin: esbuild.Plugin,
22
+ ): (result: esbuild.BuildResult) => Promise<void> | void {
23
+ let cb!: (result: esbuild.BuildResult) => Promise<void> | void;
24
+ void plugin.setup({
25
+ onEnd(fn: (result: esbuild.BuildResult) => Promise<void> | void) {
26
+ cb = fn;
27
+ },
28
+ } as unknown as esbuild.PluginBuild);
29
+ return cb;
30
+ }
31
+
32
+ function buildResult(
33
+ outputs: Record<string, unknown>,
34
+ ): esbuild.BuildResult {
35
+ return {
36
+ errors: [] as esbuild.Message[],
37
+ warnings: [] as esbuild.Message[],
38
+ metafile: { inputs: {}, outputs } as esbuild.Metafile,
39
+ outputFiles: [],
40
+ mangleCache: {},
41
+ };
42
+ }
43
+
44
+ // --- Tests ---
45
+
46
+ describe("createPostcssPlugin — 플러그인 구조", () => {
47
+ it("플러그인 이름이 sd-postcss이다", () => {
48
+ const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
49
+ expect(plugin.name).toBe("sd-postcss");
50
+ });
51
+
52
+ it("onEnd 콜백이 등록된다", () => {
53
+ const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
54
+ let registered = false;
55
+ void plugin.setup({
56
+ onEnd() {
57
+ registered = true;
58
+ },
59
+ } as unknown as esbuild.PluginBuild);
60
+ expect(registered).toBe(true);
61
+ });
62
+ });
63
+
64
+ describe("createPostcssPlugin — metafile 가드", () => {
65
+ let tmpDir: string;
66
+
67
+ beforeEach(() => {
68
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
69
+ });
70
+
71
+ afterEach(() => {
72
+ fs.rmSync(tmpDir, { recursive: true, force: true });
73
+ });
74
+
75
+ it("metafile이 null이면 아무 처리도 하지 않는다", async () => {
76
+ const cssFile = path.join(tmpDir, "main.css");
77
+ const original = ".host { display: flex; }";
78
+ fs.writeFileSync(cssFile, original);
79
+
80
+ const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
81
+ const onEnd = captureOnEnd(plugin);
82
+
83
+ await onEnd({
84
+ errors: [],
85
+ warnings: [],
86
+ metafile: undefined,
87
+ outputFiles: [],
88
+ mangleCache: {},
89
+ } as unknown as esbuild.BuildResult);
90
+
91
+ expect(fs.readFileSync(cssFile, "utf-8")).toBe(original);
92
+ });
93
+ });
94
+
95
+ describe("createPostcssPlugin — .css 파일 처리", () => {
96
+ let tmpDir: string;
97
+
98
+ beforeEach(() => {
99
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
100
+ });
101
+
102
+ afterEach(() => {
103
+ fs.rmSync(tmpDir, { recursive: true, force: true });
104
+ });
105
+
106
+ it("여러 .css 파일이 있으면 모두 처리된다", async () => {
107
+ const css1 = path.join(tmpDir, "a.css");
108
+ const css2 = path.join(tmpDir, "b.css");
109
+ fs.writeFileSync(css1, ".a { color: red; }");
110
+ fs.writeFileSync(css2, ".b { color: blue; }");
111
+
112
+ const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
113
+ const onEnd = captureOnEnd(plugin);
114
+
115
+ await onEnd(
116
+ buildResult({
117
+ [css1]: { bytes: 10, inputs: {}, imports: [], exports: [] },
118
+ [css2]: { bytes: 10, inputs: {}, imports: [], exports: [] },
119
+ }),
120
+ );
121
+
122
+ expect(fs.readFileSync(css1, "utf-8")).toContain("/* marker */");
123
+ expect(fs.readFileSync(css2, "utf-8")).toContain("/* marker */");
124
+ });
125
+
126
+ it("outputs에 .css 파일이 없으면 파일을 읽지 않는다", async () => {
127
+ const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
128
+ const onEnd = captureOnEnd(plugin);
129
+
130
+ // .js와 .map만 있는 outputs — 에러 없이 정상 완료
131
+ await onEnd(
132
+ buildResult({
133
+ "dist/main.js": { bytes: 10, inputs: {}, imports: [], exports: [] },
134
+ "dist/main.js.map": { bytes: 10, inputs: {}, imports: [], exports: [] },
135
+ }),
136
+ );
137
+
138
+ // 에러 없이 완료되면 성공
139
+ });
140
+
141
+ it("PostCSS 에러 메시지에 파일 경로가 포함된다", async () => {
142
+ const cssFile = path.join(tmpDir, "err.css");
143
+ fs.writeFileSync(cssFile, ".host { color: red; }");
144
+
145
+ const throwing: postcss.Plugin = {
146
+ postcssPlugin: "throw-with-detail",
147
+ Once() {
148
+ throw new Error("detailed-error-message");
149
+ },
150
+ };
151
+
152
+ const plugin = createPostcssPlugin({ plugins: [throwing] });
153
+ const onEnd = captureOnEnd(plugin);
154
+
155
+ const result = buildResult({
156
+ [cssFile]: { bytes: 10, inputs: {}, imports: [], exports: [] },
157
+ });
158
+ await onEnd(result);
159
+
160
+ expect(result.errors[0].text).toContain(cssFile);
161
+ expect(result.errors[0].text).toContain("detailed-error-message");
162
+ });
163
+ });
164
+
165
+ describe("createPostcssPlugin — .js 파일 사전 필터링", () => {
166
+ let tmpDir: string;
167
+
168
+ beforeEach(() => {
169
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
170
+ });
171
+
172
+ afterEach(() => {
173
+ fs.rmSync(tmpDir, { recursive: true, force: true });
174
+ });
175
+
176
+ it("styles 문자열이 없는 .js 파일은 원본이 유지된다", async () => {
177
+ const jsFile = path.join(tmpDir, "chunk.js");
178
+ const original = 'export const x = "no relevant content";';
179
+ fs.writeFileSync(jsFile, original);
180
+
181
+ const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
182
+ const onEnd = captureOnEnd(plugin);
183
+
184
+ await onEnd(
185
+ buildResult({
186
+ [jsFile]: { bytes: 50, inputs: {}, imports: [], exports: [] },
187
+ }),
188
+ );
189
+
190
+ expect(fs.readFileSync(jsFile, "utf-8")).toBe(original);
191
+ });
192
+ });
193
+
194
+ describe("createPostcssPlugin — .js AST 기반 styles 추출", () => {
195
+ let tmpDir: string;
196
+
197
+ beforeEach(() => {
198
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
199
+ });
200
+
201
+ afterEach(() => {
202
+ fs.rmSync(tmpDir, { recursive: true, force: true });
203
+ });
204
+
205
+ it("styles 배열에 Identifier(변수 참조)만 있으면 파일이 수정되지 않는다", async () => {
206
+ const jsFile = path.join(tmpDir, "varref.js");
207
+ const code = [
208
+ 'import * as i0 from "@angular/core";',
209
+ "const _c0 = [];",
210
+ "class MyComp {}",
211
+ "MyComp.ɵcmp = i0.ɵɵdefineComponent({",
212
+ " styles: _c0",
213
+ "});",
214
+ ].join("\n");
215
+ fs.writeFileSync(jsFile, code);
216
+
217
+ const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
218
+ const onEnd = captureOnEnd(plugin);
219
+
220
+ await onEnd(
221
+ buildResult({
222
+ [jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
223
+ }),
224
+ );
225
+
226
+ expect(fs.readFileSync(jsFile, "utf-8")).toBe(code);
227
+ });
228
+
229
+ it("styles 배열에 문자열과 변수 참조가 혼재하면 문자열만 처리된다", async () => {
230
+ const jsFile = path.join(tmpDir, "mixed.js");
231
+ const code = [
232
+ 'import * as i0 from "@angular/core";',
233
+ "const _c0 = '.external { margin: 0 }';",
234
+ "class MyComp {}",
235
+ "MyComp.ɵcmp = i0.ɵɵdefineComponent({",
236
+ ' styles: [_c0, ".inline { color: red; }"]',
237
+ "});",
238
+ ].join("\n");
239
+ fs.writeFileSync(jsFile, code);
240
+
241
+ const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
242
+ const onEnd = captureOnEnd(plugin);
243
+
244
+ await onEnd(
245
+ buildResult({
246
+ [jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
247
+ }),
248
+ );
249
+
250
+ const output = fs.readFileSync(jsFile, "utf-8");
251
+ // 문자열 리터럴만 처리됨 (마커 1개)
252
+ const markerCount = (output.match(/\/\* marker \*\//g) ?? []).length;
253
+ expect(markerCount).toBe(1);
254
+ // 변수 참조는 원본 유지
255
+ expect(output).toContain("_c0");
256
+ });
257
+
258
+ it(".js PostCSS 실패 시에도 다른 .css 파일은 정상 처리된다", async () => {
259
+ const jsFile = path.join(tmpDir, "fail.js");
260
+ const cssFile = path.join(tmpDir, "ok.css");
261
+ const jsCode = [
262
+ 'import * as i0 from "@angular/core";',
263
+ "class MyComp {}",
264
+ "MyComp.ɵcmp = i0.ɵɵdefineComponent({",
265
+ ' styles: [".host { display: flex; }"]',
266
+ "});",
267
+ ].join("\n");
268
+ fs.writeFileSync(jsFile, jsCode);
269
+ fs.writeFileSync(cssFile, ".ok { color: green; }");
270
+
271
+ const throwing: postcss.Plugin = {
272
+ postcssPlugin: "test-throwing",
273
+ Once() {
274
+ throw new Error("js-postcss-error");
275
+ },
276
+ };
277
+
278
+ const plugin = createPostcssPlugin({ plugins: [throwing] });
279
+ const onEnd = captureOnEnd(plugin);
280
+
281
+ const result = buildResult({
282
+ [cssFile]: { bytes: 20, inputs: {}, imports: [], exports: [] },
283
+ [jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
284
+ });
285
+ await onEnd(result);
286
+
287
+ // 에러가 보고됨
288
+ expect(result.errors.length).toBeGreaterThanOrEqual(1);
289
+ });
290
+ });
@@ -109,3 +109,4 @@ describe("createScssPlugin — Acceptance", () => {
109
109
  }
110
110
  });
111
111
  });
112
+
@@ -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"` 확인
@@ -32,6 +32,20 @@ vi.mock("../../src/utils/tsc-build", () => ({
32
32
  runTscPackageBuild: vi.fn(() => mockTscResult),
33
33
  }));
34
34
 
35
+ // tsc plugin mock (build() js=true path)
36
+ const mockTscPlugin = {
37
+ plugin: { name: "sd-tsc", setup: vi.fn() },
38
+ getProgram: vi.fn(),
39
+ getAffectedFiles: vi.fn(),
40
+ getDiagnostics: vi.fn((): unknown[] => []),
41
+ getErrors: vi.fn((): string[] | undefined => undefined),
42
+ resetBuilderProgram: vi.fn(),
43
+ };
44
+
45
+ vi.mock("../../src/esbuild/esbuild-tsc-plugin", () => ({
46
+ createTscPlugin: vi.fn(() => mockTscPlugin),
47
+ }));
48
+
35
49
  vi.mock("../../src/utils/tsconfig", () => ({
36
50
  parseTsconfig: vi.fn(() => ({ options: {}, fileNames: [] })),
37
51
  getPackageSourceFiles: vi.fn(() => []),
@@ -107,6 +121,13 @@ beforeEach(() => {
107
121
  formattedOutput: "",
108
122
  });
109
123
  mockTscResult.program = { getSourceFiles: () => [] } as any;
124
+
125
+ // Reset tsc plugin mock
126
+ mockTscPlugin.getProgram.mockReset();
127
+ mockTscPlugin.getAffectedFiles.mockReset();
128
+ mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
129
+ mockTscPlugin.getErrors.mockReset().mockReturnValue(undefined);
130
+ mockTscPlugin.resetBuilderProgram.mockReset();
110
131
  });
111
132
 
112
133
  describe("server-build.worker lint integration (Slice 3)", () => {
@@ -129,6 +150,28 @@ describe("server-build.worker lint integration (Slice 3)", () => {
129
150
  });
130
151
  });
131
152
 
153
+ describe("Scenario: build js=true runs lint using tscPlugin.getProgram()", () => {
154
+ it("returns lint result using plugin program when js=true and lint enabled", async () => {
155
+ const fakeProgram = { getSourceFiles: () => [] };
156
+ mockTscPlugin.getProgram.mockReturnValue(fakeProgram);
157
+
158
+ const result = await workerMethods["build"]({
159
+ name: "my-server",
160
+ cwd: "/workspace",
161
+ pkgDir: "/workspace/packages/my-server",
162
+ output: { js: true, dts: false, lint: true },
163
+ });
164
+
165
+ expect(result).toHaveProperty("lint");
166
+ expect(result.lint).toEqual({
167
+ success: true,
168
+ errorCount: 0,
169
+ warningCount: 0,
170
+ formattedOutput: "",
171
+ });
172
+ });
173
+ });
174
+
132
175
  describe("Scenario: lint disabled", () => {
133
176
  it("does not run lint when output.lint is not set", async () => {
134
177
  const result = await workerMethods["build"]({
@@ -0,0 +1,14 @@
1
+ # server-build.worker.ts 리팩토링 — LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] rebuildAll()이 startWatch() 내부 클로저로 정의: `server-build.worker.ts:272` — startWatch (`:259`) 내부에 `async function rebuildAll()` 정의 확인
6
+ - [x] lastBuilderProgram이 startWatch 로컬 변수: `server-build.worker.ts:264` — `let lastBuilderProgram` startWatch 함수 스코프 내 정의 확인
7
+ - [x] watchLintRunner가 startWatch 로컬 변수: `server-build.worker.ts:265` — `let watchLintRunner` startWatch 함수 스코프 내 정의 확인
8
+ - [x] watchInfo 모듈 스코프 변수 제거: 모듈 스코프에 `watchInfo` 선언 없음 확인, startWatch의 `info` 파라미터를 직접 사용
9
+ - [x] cleanup()에서 lastBuilderProgram 참조 제거: `server-build.worker.ts:108-123` — cleanup 함수에 lastBuilderProgram 참조 없음 확인
10
+ - [x] startWatch에서 createContext에 tsc 옵션 전달: `server-build.worker.ts:348-359` — `tsc: { cwd, output: { dts }, env, includeTests }` 옵션 포함 확인
11
+ - [x] build() js=true에서 createTscPlugin 로컬 생성: `server-build.worker.ts:153-159` — build 함수 내 `const tscPlugin = createTscPlugin({...})` 확인
12
+ - [x] build() js=false에서 runTscPackageBuild 직접 호출: `server-build.worker.ts:192-199` — `runTscPackageBuild({...})` 직접 호출 확인
13
+ - [x] 외부 인터페이스(ServerBuildInfo/WatchInfo/Result/WorkerEvents) 변경 없음: 타입 정의 `:33-96` 변경 없음 확인
14
+ - [x] runTscPackageBuild import 유지: `server-build.worker.ts:19` — import 존재 확인 (js=false fallback용)
@@ -29,8 +29,8 @@ let mockMetafileInputs: Record<string, unknown> = {};
29
29
  // tsc build mock
30
30
  const mockRunTscPackageBuild = vi.fn(() => ({
31
31
  success: true,
32
- errors: undefined,
33
- diagnostics: [],
32
+ errors: undefined as string[] | undefined,
33
+ diagnostics: [] as unknown[],
34
34
  errorCount: 0,
35
35
  warningCount: 0,
36
36
  }));
@@ -116,6 +116,20 @@ vi.mock("../../src/utils/tsc-build", () => ({
116
116
  runTscPackageBuild: mockRunTscPackageBuild,
117
117
  }));
118
118
 
119
+ // tsc plugin mock (build() js=true path uses createTscPlugin)
120
+ const mockTscPlugin = {
121
+ plugin: { name: "sd-tsc", setup: vi.fn() },
122
+ getProgram: vi.fn(),
123
+ getAffectedFiles: vi.fn(),
124
+ getDiagnostics: vi.fn((): unknown[] => []),
125
+ getErrors: vi.fn((): string[] | undefined => undefined),
126
+ resetBuilderProgram: vi.fn(),
127
+ };
128
+
129
+ vi.mock("../../src/esbuild/esbuild-tsc-plugin", () => ({
130
+ createTscPlugin: vi.fn(() => mockTscPlugin),
131
+ }));
132
+
119
133
  vi.mock("../../src/workers/shared-worker-lifecycle", () => {
120
134
  let guardCalled = false;
121
135
  resetGuard = () => { guardCalled = false; };
@@ -175,7 +189,14 @@ describe("server-build.worker build()", () => {
175
189
  diagnostics: [],
176
190
  errorCount: 0,
177
191
  warningCount: 0,
178
- });
192
+ });
193
+
194
+ // Reset tsc plugin mock (used for js=true path)
195
+ mockTscPlugin.getProgram.mockReset();
196
+ mockTscPlugin.getAffectedFiles.mockReset();
197
+ mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
198
+ mockTscPlugin.getErrors.mockReset().mockReturnValue(undefined);
199
+ mockTscPlugin.resetBuilderProgram.mockReset();
179
200
 
180
201
  // Reset lockfile content and cache
181
202
  mockLockfileContent = "";
@@ -212,21 +233,65 @@ describe("server-build.worker build()", () => {
212
233
  expect(result.mainJsPath).toBe(path.resolve(baseBuildInfo.pkgDir, "dist", "main.js").replace(/\\/g, "/"));
213
234
  });
214
235
 
215
- // Acceptance: type error detected
236
+ // Acceptance: type error detected via tsc plugin (js=true)
216
237
  it("reports typecheck error in build field", async () => {
238
+ mockTscPlugin.getErrors.mockReturnValue(["TS2345: type error"]);
239
+ mockTscPlugin.getDiagnostics.mockReturnValue([{ code: 2345, category: 1 }]);
240
+
241
+ const result = await workerFns["build"](baseBuildInfo);
242
+
243
+ expect(result.build.success).toBe(false);
244
+ expect(result.build.errors).toContain("TS2345: type error");
245
+ expect(result.build.diagnostics).toHaveLength(1);
246
+ });
247
+
248
+ // Acceptance: esbuild + tsc both error — merged
249
+ it("merges esbuild and tsc errors when both fail", async () => {
250
+ vi.mocked(esbuild.build).mockResolvedValueOnce({
251
+ errors: [{ text: "esbuild syntax error" }],
252
+ warnings: [],
253
+ outputFiles: [],
254
+ } as any);
255
+ mockTscPlugin.getErrors.mockReturnValue(["TS2322: type mismatch"]);
256
+
257
+ const result = await workerFns["build"](baseBuildInfo);
258
+
259
+ expect(result.build.success).toBe(false);
260
+ expect(result.build.errors).toContain("esbuild syntax error");
261
+ expect(result.build.errors).toContain("TS2322: type mismatch");
262
+ });
263
+
264
+ // Acceptance: diagnostics from tsc plugin (js=true)
265
+ it("includes diagnostics from tsc plugin in build result", async () => {
266
+ mockTscPlugin.getDiagnostics.mockReturnValue([
267
+ { code: 2322, category: 1, messageText: "Type mismatch" },
268
+ ]);
269
+
270
+ const result = await workerFns["build"](baseBuildInfo);
271
+
272
+ expect(result.build.diagnostics).toHaveLength(1);
273
+ expect(result.build.diagnostics[0]).toEqual(
274
+ expect.objectContaining({ code: 2322 }),
275
+ );
276
+ });
277
+
278
+ // Acceptance: js=false uses runTscPackageBuild directly
279
+ it("uses runTscPackageBuild directly when output.js=false", async () => {
217
280
  mockRunTscPackageBuild.mockReturnValueOnce({
218
281
  success: false,
219
- errors: ["TS2345: type error"] as any,
282
+ errors: ["TS2345: type error"],
220
283
  diagnostics: [{ code: 2345, category: 1 }] as any,
221
284
  errorCount: 1,
222
285
  warningCount: 0,
223
- });
286
+ });
224
287
 
225
- const result = await workerFns["build"](baseBuildInfo);
288
+ const result = await workerFns["build"]({
289
+ ...baseBuildInfo,
290
+ output: { js: false, dts: true },
291
+ });
226
292
 
227
293
  expect(result.build.success).toBe(false);
228
294
  expect(result.build.errors).toContain("TS2345: type error");
229
- expect(result.build.diagnostics).toHaveLength(1);
230
295
  });
231
296
 
232
297
  // Acceptance: esbuild error detected
@@ -466,7 +531,14 @@ describe("server-build.worker startWatch()", () => {
466
531
  diagnostics: [],
467
532
  errorCount: 0,
468
533
  warningCount: 0,
469
- });
534
+ });
535
+
536
+ // Reset tsc plugin mock (used for watch mode rebuild)
537
+ mockTscPlugin.getProgram.mockReset();
538
+ mockTscPlugin.getAffectedFiles.mockReset();
539
+ mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
540
+ mockTscPlugin.getErrors.mockReset().mockReturnValue(undefined);
541
+ mockTscPlugin.resetBuilderProgram.mockReset();
470
542
 
471
543
  mockReadFileSync.mockImplementation((filePath: string) => {
472
544
  if (String(filePath).endsWith("package.json")) {
@@ -567,6 +639,47 @@ describe("server-build.worker startWatch()", () => {
567
639
  const buildCalls = mockSend.mock.calls.filter((c) => c[0] === "build");
568
640
  expect(buildCalls.length).toBeGreaterThanOrEqual(1);
569
641
  });
642
+
643
+ // Acceptance: rebuildAll js=true — single esbuildCtx.rebuild() call, tsc not called directly
644
+ it("uses esbuildCtx.rebuild() without direct tsc call in watch mode rebuild", async () => {
645
+ mockMetafileInputs = { "packages/my-server/src/main.ts": {} };
646
+
647
+ await workerFns["startWatch"](watchInfo);
648
+
649
+ const onChangeHandler = mockOnChange.mock.calls[0][1] as (
650
+ changes: Array<{ event: string; path: string }>,
651
+ ) => Promise<void>;
652
+
653
+ mockRebuild.mockClear();
654
+ mockRunTscPackageBuild.mockClear();
655
+ mockSend.mockClear();
656
+
657
+ const absPath = path.resolve("/workspace", "packages/my-server/src/main.ts").replace(/\\/g, "/");
658
+ await onChangeHandler([{ event: "change", path: absPath }]);
659
+
660
+ // esbuild rebuild should have been called (tsc triggered by plugin inside)
661
+ expect(mockRebuild).toHaveBeenCalled();
662
+ // runTscPackageBuild should NOT be called directly for js=true
663
+ expect(mockRunTscPackageBuild).not.toHaveBeenCalled();
664
+ // Build event should be sent
665
+ expect(mockSend).toHaveBeenCalledWith("build", expect.objectContaining({
666
+ build: expect.objectContaining({ success: true }),
667
+ }));
668
+ });
669
+
670
+ // Acceptance: startWatch passes tsc options to createContext
671
+ it("passes tsc options to esbuildCtx.createContext", async () => {
672
+ await workerFns["startWatch"]({
673
+ ...watchInfo,
674
+ output: { js: true, dts: true, env: "node" as any, includeTests: true },
675
+ });
676
+
677
+ expect(esbuild.context).toHaveBeenCalledWith(
678
+ expect.objectContaining({
679
+ plugins: [mockTscPlugin.plugin],
680
+ }),
681
+ );
682
+ });
570
683
  });
571
684
 
572
685
  describe("server-build.worker stopWatch()", () => {
@@ -0,0 +1,7 @@
1
+ # server-esbuild-context tsc 통합 — LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] `EsbuildContextOptions.tsc`가 optional이므로 `server-watch-manager.ts:66-71`의 기존 `recreateContext()` 호출이 타입 에러 없이 동작: 확인 — `tsc` 미전달 시 기존 플러그인 재사용 로직 정상
6
+ - [x] `server-watch-manager.spec.ts` 8개 테스트 전부 통과: 확인 — 회귀 없음
7
+ - [x] `server-watch-manager.ts`에 코드 변경 없음: 확인 — 파일 미수정