@simplysm/sd-cli 14.0.38 → 14.0.40

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 (57) 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/dev-server/hmr-client-script.d.ts +2 -2
  6. package/dist/dev-server/hmr-client-script.d.ts.map +1 -1
  7. package/dist/dev-server/hmr-client-script.js +4 -4
  8. package/dist/dev-server/hmr-client-script.js.map +1 -1
  9. package/dist/esbuild/esbuild-client-config.d.ts +0 -2
  10. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  11. package/dist/esbuild/esbuild-client-config.js +20 -10
  12. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  13. package/dist/esbuild/esbuild-postcss-plugin.d.ts +8 -0
  14. package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -0
  15. package/dist/esbuild/esbuild-postcss-plugin.js +105 -0
  16. package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -0
  17. package/dist/esbuild/esbuild-tsc-plugin.d.ts +23 -0
  18. package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -0
  19. package/dist/esbuild/esbuild-tsc-plugin.js +60 -0
  20. package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -0
  21. package/dist/workers/client.worker.d.ts.map +1 -1
  22. package/dist/workers/client.worker.js +94 -26
  23. package/dist/workers/client.worker.js.map +1 -1
  24. package/dist/workers/server-build.worker.d.ts.map +1 -1
  25. package/dist/workers/server-build.worker.js +129 -90
  26. package/dist/workers/server-build.worker.js.map +1 -1
  27. package/dist/workers/server-esbuild-context.d.ts +27 -0
  28. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  29. package/dist/workers/server-esbuild-context.js +57 -4
  30. package/dist/workers/server-esbuild-context.js.map +1 -1
  31. package/package.json +6 -4
  32. package/src/angular/angular-build-pipeline.ts +2 -2
  33. package/src/angular/client-transform-stylesheet.ts +4 -4
  34. package/src/dev-server/hmr-client-script.ts +4 -4
  35. package/src/esbuild/esbuild-client-config.ts +22 -13
  36. package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
  37. package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
  38. package/src/workers/client.worker.ts +96 -29
  39. package/src/workers/server-build.worker.ts +136 -97
  40. package/src/workers/server-esbuild-context.ts +72 -4
  41. package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
  42. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
  43. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
  44. package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
  45. package/tests/utils/esbuild-client-config.acc.spec.ts +34 -20
  46. package/tests/utils/esbuild-client-config.spec.ts +79 -16
  47. package/tests/utils/esbuild-postcss-plugin.acc.spec.ts +299 -0
  48. package/tests/utils/esbuild-postcss-plugin.spec.ts +290 -0
  49. package/tests/utils/esbuild-scss-plugin.acc.spec.ts +1 -0
  50. package/tests/utils/hmr-client-script.acc.spec.ts +8 -8
  51. package/tests/utils/hmr-client-script.spec.ts +5 -5
  52. package/tests/workers/server-build-lint.spec.ts +43 -0
  53. package/tests/workers/server-build-worker-refactoring.verify.md +14 -0
  54. package/tests/workers/server-build-worker.spec.ts +122 -9
  55. package/tests/workers/server-esbuild-context-tsc.verify.md +7 -0
  56. package/tests/workers/server-esbuild-context.acc.spec.ts +188 -2
  57. package/tests/workers/server-esbuild-context.spec.ts +401 -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
+
@@ -5,7 +5,7 @@ import { getHmrClientScript, createHmrPostTransform } from "../../src/dev-server
5
5
  describe("HMR 클라이언트 스크립트 통합", () => {
6
6
  describe("Scenario: HMR 클라이언트 문법 호환성", () => {
7
7
  it("Chrome 61 비호환 문법을 사용하지 않는다", () => {
8
- const script = getHmrClientScript("/app/");
8
+ const script = getHmrClientScript("/app/", 4200);
9
9
 
10
10
  // optional chaining (?.) 미사용
11
11
  expect(script).not.toMatch(/\?\./);
@@ -16,13 +16,13 @@ describe("HMR 클라이언트 스크립트 통합", () => {
16
16
  });
17
17
 
18
18
  it("WebSocket 연결 코드를 포함한다", () => {
19
- const script = getHmrClientScript("/app/");
19
+ const script = getHmrClientScript("/app/", 4200);
20
20
  expect(script).toContain("WebSocket");
21
21
  expect(script).toContain("ws://");
22
22
  });
23
23
 
24
24
  it("component-update, css-update, full-reload 메시지 핸들러를 포함한다", () => {
25
- const script = getHmrClientScript("/app/");
25
+ const script = getHmrClientScript("/app/", 4200);
26
26
  expect(script).toContain("component-update");
27
27
  expect(script).toContain("css-update");
28
28
  expect(script).toContain("full-reload");
@@ -31,7 +31,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
31
31
  });
32
32
 
33
33
  it("자동 재연결 로직을 포함한다", () => {
34
- const script = getHmrClientScript("/app/");
34
+ const script = getHmrClientScript("/app/", 4200);
35
35
  expect(script).toContain("setTimeout");
36
36
  expect(script).toContain("connect");
37
37
  });
@@ -77,7 +77,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
77
77
  }
78
78
 
79
79
  it("css-update 시 msg.files와 매칭되는 link만 cache-busting 적용", () => {
80
- const script = getHmrClientScript("/app/");
80
+ const script = getHmrClientScript("/app/", 4200);
81
81
  const { sandbox, triggerMessage, mainLink, vendorLink } = createScriptEnv();
82
82
 
83
83
  runInNewContext(script, sandbox);
@@ -89,7 +89,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
89
89
  });
90
90
 
91
91
  it("css-update 시 files에 여러 파일이 있으면 매칭되는 모든 link를 업데이트", () => {
92
- const script = getHmrClientScript("/app/");
92
+ const script = getHmrClientScript("/app/", 4200);
93
93
  const { sandbox, triggerMessage, mainLink, vendorLink } = createScriptEnv();
94
94
 
95
95
  runInNewContext(script, sandbox);
@@ -107,7 +107,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
107
107
 
108
108
  describe("Scenario: 스크립트 주입", () => {
109
109
  it("postTransform이 </body> 직전에 script 태그를 삽입한다", async () => {
110
- const transform = createHmrPostTransform("/app/");
110
+ const transform = createHmrPostTransform("/app/", 4200);
111
111
  const html = "<html><body><div>content</div></body></html>";
112
112
  const result = await transform(html);
113
113
 
@@ -117,7 +117,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
117
117
  });
118
118
 
119
119
  it("</body>가 없는 HTML에서도 스크립트를 추가한다", async () => {
120
- const transform = createHmrPostTransform("/app/");
120
+ const transform = createHmrPostTransform("/app/", 4200);
121
121
  const html = "<html><body><div>content</div>";
122
122
  const result = await transform(html);
123
123
 
@@ -3,19 +3,19 @@ import { getHmrClientScript, createHmrPostTransform } from "../../src/dev-server
3
3
 
4
4
  describe("getHmrClientScript", () => {
5
5
  it("유효한 JavaScript를 생성한다", () => {
6
- const script = getHmrClientScript("/app/");
6
+ const script = getHmrClientScript("/app/", 4200);
7
7
  // new Function으로 구문 오류 확인
8
8
  expect(() => new Function(script)).not.toThrow();
9
9
  });
10
10
 
11
11
  it("IIFE로 감싸져 있다", () => {
12
- const script = getHmrClientScript("/app/");
12
+ const script = getHmrClientScript("/app/", 4200);
13
13
  expect(script.trimStart()).toMatch(/^\(function\(\)/);
14
14
  expect(script.trimEnd()).toMatch(/\}\)\(\);$/);
15
15
  });
16
16
 
17
17
  it("const/let 대신 var를 사용한다", () => {
18
- const script = getHmrClientScript("/app/");
18
+ const script = getHmrClientScript("/app/", 4200);
19
19
  // var 사용 확인
20
20
  expect(script).toContain("var ws");
21
21
  // const/let 미사용 확인
@@ -26,7 +26,7 @@ describe("getHmrClientScript", () => {
26
26
 
27
27
  describe("createHmrPostTransform", () => {
28
28
  it("여러 </body> 태그가 있으면 마지막 것 앞에 주입한다", async () => {
29
- const transform = createHmrPostTransform("/app/");
29
+ const transform = createHmrPostTransform("/app/", 4200);
30
30
  const html = "<body>first</body><body>second</body>";
31
31
  const result = await transform(html);
32
32
 
@@ -37,7 +37,7 @@ describe("createHmrPostTransform", () => {
37
37
  });
38
38
 
39
39
  it("빈 HTML에도 스크립트를 추가한다", async () => {
40
- const transform = createHmrPostTransform("/");
40
+ const transform = createHmrPostTransform("/", 4200);
41
41
  const result = await transform("");
42
42
  expect(result).toContain("<script>");
43
43
  });
@@ -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`에 코드 변경 없음: 확인 — 파일 미수정