@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,349 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import type esbuild from "esbuild";
3
+ import type { TscPackageBuildResult } from "../../src/utils/tsc-build";
4
+
5
+ //#region Mocks
6
+
7
+ const mockRunTscPackageBuild = vi.fn<(...args: unknown[]) => TscPackageBuildResult>();
8
+
9
+ vi.mock("../../src/utils/tsc-build", () => ({
10
+ runTscPackageBuild: (...args: unknown[]) => mockRunTscPackageBuild(...args),
11
+ }));
12
+
13
+ const mockParseTsconfig = vi.fn();
14
+
15
+ vi.mock("../../src/utils/tsconfig", async (importOriginal) => {
16
+ const actual = await importOriginal<typeof import("../../src/utils/tsconfig")>();
17
+ return {
18
+ ...actual,
19
+ parseTsconfig: (...args: unknown[]) => mockParseTsconfig(...args),
20
+ };
21
+ });
22
+
23
+ //#endregion
24
+
25
+ const { createTscPlugin } = await import("../../src/esbuild/esbuild-tsc-plugin");
26
+
27
+ /** esbuild 플러그인 lifecycle을 시뮬레이션하는 헬퍼 */
28
+ function setupPlugin(plugin: esbuild.Plugin) {
29
+ let onStartCb: (() => esbuild.OnStartResult | null | void | Promise<esbuild.OnStartResult | null | void>) | undefined;
30
+ let onEndCb: ((result: esbuild.BuildResult) => esbuild.OnEndResult | null | void | Promise<esbuild.OnEndResult | null | void>) | undefined;
31
+
32
+ const mockBuild = {
33
+ onStart(cb: typeof onStartCb) { onStartCb = cb; },
34
+ onEnd(cb: typeof onEndCb) { onEndCb = cb; },
35
+ } as unknown as esbuild.PluginBuild;
36
+
37
+ void plugin.setup(mockBuild);
38
+
39
+ return {
40
+ async invokeOnStart() {
41
+ return (await onStartCb?.()) ?? null;
42
+ },
43
+ async invokeOnEnd(result?: Partial<esbuild.BuildResult>) {
44
+ return (await onEndCb?.({
45
+ errors: [],
46
+ warnings: [],
47
+ mangleCache: {},
48
+ outputFiles: [],
49
+ metafile: { inputs: {}, outputs: {} },
50
+ ...result,
51
+ } as esbuild.BuildResult)) ?? null;
52
+ },
53
+ };
54
+ }
55
+
56
+ const baseOptions = {
57
+ pkgDir: "/workspace/packages/my-server",
58
+ cwd: "/workspace",
59
+ output: { dts: true },
60
+ };
61
+
62
+ const mockParsedConfig = {
63
+ options: { target: 99 },
64
+ fileNames: [],
65
+ errors: [],
66
+ } as any;
67
+
68
+ function createSuccessTscResult(): TscPackageBuildResult {
69
+ return {
70
+ success: true,
71
+ diagnostics: [],
72
+ errorCount: 0,
73
+ warningCount: 0,
74
+ program: { getSourceFiles: () => [] } as any,
75
+ affectedFiles: new Set(["/workspace/packages/my-server/src/main.ts"]),
76
+ builderProgram: {} as any,
77
+ };
78
+ }
79
+
80
+ function createErrorTscResult(): TscPackageBuildResult {
81
+ return {
82
+ success: false,
83
+ errors: ["TS2322: Type 'string' is not assignable to type 'number'"],
84
+ diagnostics: [{ category: 1, code: 2322, messageText: "Type mismatch" }],
85
+ errorCount: 1,
86
+ warningCount: 0,
87
+ program: { getSourceFiles: () => [] } as any,
88
+ affectedFiles: new Set(["/workspace/packages/my-server/src/main.ts"]),
89
+ builderProgram: {} as any,
90
+ };
91
+ }
92
+
93
+ describe("createTscPlugin — Acceptance Tests", () => {
94
+ beforeEach(() => {
95
+ vi.clearAllMocks();
96
+ mockParseTsconfig.mockReturnValue(mockParsedConfig);
97
+ mockRunTscPackageBuild.mockReturnValue(createSuccessTscResult());
98
+ });
99
+
100
+ // Rule: createTscPlugin은 esbuild.Plugin과 getter 객체를 반환한다
101
+ describe("Scenario: 필수 옵션으로 플러그인 생성", () => {
102
+ it('plugin.name이 "sd-tsc"이고 getter 함수들을 반환한다', () => {
103
+ const result = createTscPlugin(baseOptions);
104
+
105
+ expect(result.plugin.name).toBe("sd-tsc");
106
+ expect(typeof result.plugin.setup).toBe("function");
107
+ expect(typeof result.getProgram).toBe("function");
108
+ expect(typeof result.getAffectedFiles).toBe("function");
109
+ expect(typeof result.getDiagnostics).toBe("function");
110
+ expect(typeof result.getErrors).toBe("function");
111
+ expect(typeof result.resetBuilderProgram).toBe("function");
112
+ });
113
+ });
114
+
115
+ describe("Scenario: 선택 옵션 포함 플러그인 생성", () => {
116
+ it("env, includeTests 옵션이 runTscPackageBuild에 전달된다", async () => {
117
+ const result = createTscPlugin({
118
+ ...baseOptions,
119
+ env: "node",
120
+ includeTests: true,
121
+ });
122
+ const lifecycle = setupPlugin(result.plugin);
123
+
124
+ await lifecycle.invokeOnStart();
125
+ await lifecycle.invokeOnEnd();
126
+
127
+ expect(mockRunTscPackageBuild).toHaveBeenCalledWith(
128
+ expect.objectContaining({
129
+ env: "node",
130
+ includeTests: true,
131
+ }),
132
+ );
133
+ });
134
+ });
135
+
136
+ // Rule: onStart에서 tsc를 microtask로 스케줄링하고 await하지 않는다
137
+ describe("Scenario: tsc microtask 스케줄링", () => {
138
+ it("onStart는 tsc 완료를 기다리지 않고 즉시 반환한다", async () => {
139
+ const result = createTscPlugin(baseOptions);
140
+
141
+ // onStart 콜백을 직접 캡처하여 동기적으로 호출
142
+ let onStartCb!: () => void;
143
+ const mockBuild = {
144
+ onStart(cb: () => void) { onStartCb = cb; },
145
+ onEnd() { /* noop */ },
146
+ } as unknown as esbuild.PluginBuild;
147
+ void result.plugin.setup(mockBuild);
148
+
149
+ // onStart 동기 호출 — 반환값이 undefined (await하지 않음)
150
+ const onStartResult = onStartCb();
151
+ expect(onStartResult).toBeUndefined();
152
+
153
+ // onStart 반환 직후, microtask 전이므로 tsc 미호출
154
+ expect(mockRunTscPackageBuild).not.toHaveBeenCalled();
155
+
156
+ // microtask flush 후 tsc 호출됨
157
+ await Promise.resolve();
158
+ expect(mockRunTscPackageBuild).toHaveBeenCalledOnce();
159
+ });
160
+ });
161
+
162
+ describe("Scenario: parsedConfig 갱신", () => {
163
+ it("매 onStart마다 parseTsconfig를 호출하여 최신 tsconfig를 반영한다", async () => {
164
+ const result = createTscPlugin(baseOptions);
165
+ const lifecycle = setupPlugin(result.plugin);
166
+
167
+ // 첫 번째 빌드
168
+ await lifecycle.invokeOnStart();
169
+ await lifecycle.invokeOnEnd();
170
+
171
+ // 두 번째 빌드
172
+ const updatedConfig = { ...mockParsedConfig, fileNames: ["/new-file.ts"] };
173
+ mockParseTsconfig.mockReturnValue(updatedConfig);
174
+
175
+ await lifecycle.invokeOnStart();
176
+ await lifecycle.invokeOnEnd();
177
+
178
+ expect(mockParseTsconfig).toHaveBeenCalledTimes(2);
179
+ expect(mockParseTsconfig).toHaveBeenCalledWith(baseOptions.pkgDir);
180
+ expect(mockRunTscPackageBuild).toHaveBeenLastCalledWith(
181
+ expect.objectContaining({ parsedConfig: updatedConfig }),
182
+ );
183
+ });
184
+ });
185
+
186
+ // Rule: onEnd에서 tsc Promise를 await하여 결과를 내부 상태에 저장한다
187
+ describe("Scenario: tsc 정상 완료", () => {
188
+ it("program, affectedFiles, diagnostics를 저장하고 errors는 undefined", async () => {
189
+ const tscResult = createSuccessTscResult();
190
+ mockRunTscPackageBuild.mockReturnValue(tscResult);
191
+
192
+ const result = createTscPlugin(baseOptions);
193
+ const lifecycle = setupPlugin(result.plugin);
194
+
195
+ await lifecycle.invokeOnStart();
196
+ await lifecycle.invokeOnEnd();
197
+
198
+ expect(result.getProgram()).toBe(tscResult.program);
199
+ expect(result.getAffectedFiles()).toBe(tscResult.affectedFiles);
200
+ expect(result.getDiagnostics()).toEqual([]);
201
+ expect(result.getErrors()).toBeUndefined();
202
+ });
203
+ });
204
+
205
+ describe("Scenario: tsc 타입 에러 발생", () => {
206
+ it("errors를 string[]로, diagnostics를 SerializedDiagnostic[]로 저장한다", async () => {
207
+ const tscResult = createErrorTscResult();
208
+ mockRunTscPackageBuild.mockReturnValue(tscResult);
209
+
210
+ const result = createTscPlugin(baseOptions);
211
+ const lifecycle = setupPlugin(result.plugin);
212
+
213
+ await lifecycle.invokeOnStart();
214
+ await lifecycle.invokeOnEnd();
215
+
216
+ expect(result.getErrors()).toEqual(["TS2322: Type 'string' is not assignable to type 'number'"]);
217
+ expect(result.getDiagnostics()).toEqual([{ category: 1, code: 2322, messageText: "Type mismatch" }]);
218
+ expect(result.getProgram()).toBe(tscResult.program);
219
+ expect(result.getAffectedFiles()).toBe(tscResult.affectedFiles);
220
+ });
221
+ });
222
+
223
+ describe("Scenario: tsc 예외 발생", () => {
224
+ it("try-catch로 포착하여 errors에 메시지 저장, program/affectedFiles는 undefined", async () => {
225
+ mockRunTscPackageBuild.mockImplementation(() => {
226
+ throw new Error("tsconfig parse failed");
227
+ });
228
+
229
+ const result = createTscPlugin(baseOptions);
230
+ const lifecycle = setupPlugin(result.plugin);
231
+
232
+ await lifecycle.invokeOnStart();
233
+ await lifecycle.invokeOnEnd();
234
+
235
+ expect(result.getErrors()).toEqual(["tsconfig parse failed"]);
236
+ expect(result.getDiagnostics()).toEqual([]);
237
+ expect(result.getProgram()).toBeUndefined();
238
+ expect(result.getAffectedFiles()).toBeUndefined();
239
+ });
240
+ });
241
+
242
+ // Rule: result.errors에 push하지 않는다
243
+ describe("Scenario: tsc 에러가 있어도 result.errors는 변경하지 않음", () => {
244
+ it("onEnd의 result.errors에 tsc 에러를 push하지 않는다", async () => {
245
+ mockRunTscPackageBuild.mockReturnValue(createErrorTscResult());
246
+
247
+ const result = createTscPlugin(baseOptions);
248
+ const lifecycle = setupPlugin(result.plugin);
249
+
250
+ await lifecycle.invokeOnStart();
251
+
252
+ const buildResult = {
253
+ errors: [] as esbuild.Message[],
254
+ warnings: [] as esbuild.Message[],
255
+ };
256
+ await lifecycle.invokeOnEnd(buildResult);
257
+
258
+ // result.errors는 비어있어야 함 (tsc 에러가 push되지 않음)
259
+ expect(buildResult.errors).toEqual([]);
260
+ // tsc 에러는 getErrors()로만 조회 가능
261
+ expect(result.getErrors()).toBeDefined();
262
+ });
263
+ });
264
+
265
+ // Rule: getter는 마지막 빌드 결과를 반환한다
266
+ describe("Scenario: 빌드 전 getter 호출", () => {
267
+ it("빌드 실행 전 기본값을 반환한다", () => {
268
+ const result = createTscPlugin(baseOptions);
269
+
270
+ expect(result.getProgram()).toBeUndefined();
271
+ expect(result.getAffectedFiles()).toBeUndefined();
272
+ expect(result.getDiagnostics()).toEqual([]);
273
+ expect(result.getErrors()).toBeUndefined();
274
+ });
275
+ });
276
+
277
+ describe("Scenario: 빌드 후 getter 호출", () => {
278
+ it("tsc 빌드 성공 후 program과 affectedFiles를 반환한다", async () => {
279
+ const tscResult = createSuccessTscResult();
280
+ mockRunTscPackageBuild.mockReturnValue(tscResult);
281
+
282
+ const result = createTscPlugin(baseOptions);
283
+ const lifecycle = setupPlugin(result.plugin);
284
+
285
+ await lifecycle.invokeOnStart();
286
+ await lifecycle.invokeOnEnd();
287
+
288
+ expect(result.getProgram()).toBe(tscResult.program);
289
+ expect(result.getAffectedFiles()).toEqual(new Set(["/workspace/packages/my-server/src/main.ts"]));
290
+ expect(result.getDiagnostics()).toEqual([]);
291
+ });
292
+ });
293
+
294
+ // Rule: lastBuilderProgram을 캐싱하여 watch 모드 증분 빌드를 지원한다
295
+ describe("Scenario: 증분 빌드 — builderProgram 재사용", () => {
296
+ it("첫 번째 빌드 후 캐싱된 builderProgram을 두 번째 빌드에 oldBuilderProgram으로 전달한다", async () => {
297
+ const firstBuilderProgram = { kind: "first" } as any;
298
+ mockRunTscPackageBuild.mockReturnValue({
299
+ ...createSuccessTscResult(),
300
+ builderProgram: firstBuilderProgram,
301
+ });
302
+
303
+ const result = createTscPlugin(baseOptions);
304
+ const lifecycle = setupPlugin(result.plugin);
305
+
306
+ // 첫 번째 빌드
307
+ await lifecycle.invokeOnStart();
308
+ await lifecycle.invokeOnEnd();
309
+
310
+ // 두 번째 빌드
311
+ await lifecycle.invokeOnStart();
312
+ await lifecycle.invokeOnEnd();
313
+
314
+ // 두 번째 호출에서 oldBuilderProgram이 첫 번째 결과의 builderProgram
315
+ expect(mockRunTscPackageBuild).toHaveBeenCalledTimes(2);
316
+ expect(mockRunTscPackageBuild).toHaveBeenLastCalledWith(
317
+ expect.objectContaining({ oldBuilderProgram: firstBuilderProgram }),
318
+ );
319
+ });
320
+ });
321
+
322
+ describe("Scenario: builderProgram 리셋", () => {
323
+ it("resetBuilderProgram 호출 후 다음 빌드는 fresh build로 실행된다", async () => {
324
+ const cachedBuilderProgram = { kind: "cached" } as any;
325
+ mockRunTscPackageBuild.mockReturnValue({
326
+ ...createSuccessTscResult(),
327
+ builderProgram: cachedBuilderProgram,
328
+ });
329
+
330
+ const result = createTscPlugin(baseOptions);
331
+ const lifecycle = setupPlugin(result.plugin);
332
+
333
+ // 첫 번째 빌드 — builderProgram 캐싱
334
+ await lifecycle.invokeOnStart();
335
+ await lifecycle.invokeOnEnd();
336
+
337
+ // 리셋
338
+ result.resetBuilderProgram();
339
+
340
+ // 다음 빌드 — oldBuilderProgram이 undefined (fresh build)
341
+ await lifecycle.invokeOnStart();
342
+ await lifecycle.invokeOnEnd();
343
+
344
+ expect(mockRunTscPackageBuild).toHaveBeenLastCalledWith(
345
+ expect.objectContaining({ oldBuilderProgram: undefined }),
346
+ );
347
+ });
348
+ });
349
+ });
@@ -0,0 +1,230 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import type esbuild from "esbuild";
3
+ import type { TscPackageBuildResult } from "../../src/utils/tsc-build";
4
+
5
+ //#region Mocks
6
+
7
+ const mockRunTscPackageBuild = vi.fn<(...args: unknown[]) => TscPackageBuildResult>();
8
+
9
+ vi.mock("../../src/utils/tsc-build", () => ({
10
+ runTscPackageBuild: (...args: unknown[]) => mockRunTscPackageBuild(...args),
11
+ }));
12
+
13
+ const mockParseTsconfig = vi.fn();
14
+
15
+ vi.mock("../../src/utils/tsconfig", async (importOriginal) => {
16
+ const actual = await importOriginal<typeof import("../../src/utils/tsconfig")>();
17
+ return {
18
+ ...actual,
19
+ parseTsconfig: (...args: unknown[]) => mockParseTsconfig(...args),
20
+ };
21
+ });
22
+
23
+ //#endregion
24
+
25
+ const { createTscPlugin } = await import("../../src/esbuild/esbuild-tsc-plugin");
26
+
27
+ /** esbuild 플러그인 lifecycle을 시뮬레이션하는 헬퍼 */
28
+ function setupPlugin(plugin: esbuild.Plugin) {
29
+ let onStartCb: (() => esbuild.OnStartResult | null | void | Promise<esbuild.OnStartResult | null | void>) | undefined;
30
+ let onEndCb: ((result: esbuild.BuildResult) => esbuild.OnEndResult | null | void | Promise<esbuild.OnEndResult | null | void>) | undefined;
31
+
32
+ const mockBuild = {
33
+ onStart(cb: typeof onStartCb) { onStartCb = cb; },
34
+ onEnd(cb: typeof onEndCb) { onEndCb = cb; },
35
+ } as unknown as esbuild.PluginBuild;
36
+
37
+ void plugin.setup(mockBuild);
38
+
39
+ return {
40
+ async invokeOnStart() {
41
+ return (await onStartCb?.()) ?? null;
42
+ },
43
+ async invokeOnEnd(result?: Partial<esbuild.BuildResult>) {
44
+ return (await onEndCb?.({
45
+ errors: [],
46
+ warnings: [],
47
+ mangleCache: {},
48
+ outputFiles: [],
49
+ metafile: { inputs: {}, outputs: {} },
50
+ ...result,
51
+ } as esbuild.BuildResult)) ?? null;
52
+ },
53
+ };
54
+ }
55
+
56
+ const baseOptions = {
57
+ pkgDir: "/workspace/packages/my-server",
58
+ cwd: "/workspace",
59
+ output: { dts: true },
60
+ };
61
+
62
+ const mockParsedConfig = {
63
+ options: { target: 99 },
64
+ fileNames: [],
65
+ errors: [],
66
+ } as any;
67
+
68
+ function createSuccessTscResult(): TscPackageBuildResult {
69
+ return {
70
+ success: true,
71
+ diagnostics: [],
72
+ errorCount: 0,
73
+ warningCount: 0,
74
+ program: { getSourceFiles: () => [] } as any,
75
+ affectedFiles: new Set(["/workspace/packages/my-server/src/main.ts"]),
76
+ builderProgram: {} as any,
77
+ };
78
+ }
79
+
80
+ describe("createTscPlugin — Unit Tests", () => {
81
+ beforeEach(() => {
82
+ vi.clearAllMocks();
83
+ mockParseTsconfig.mockReturnValue(mockParsedConfig);
84
+ mockRunTscPackageBuild.mockReturnValue(createSuccessTscResult());
85
+ });
86
+
87
+ describe("plugin 구조", () => {
88
+ it("setup 함수가 onStart와 onEnd를 등록한다", () => {
89
+ const result = createTscPlugin(baseOptions);
90
+ const onStartSpy = vi.fn();
91
+ const onEndSpy = vi.fn();
92
+
93
+ const mockBuild = {
94
+ onStart: onStartSpy,
95
+ onEnd: onEndSpy,
96
+ } as unknown as esbuild.PluginBuild;
97
+
98
+ void result.plugin.setup(mockBuild);
99
+
100
+ expect(onStartSpy).toHaveBeenCalledOnce();
101
+ expect(onEndSpy).toHaveBeenCalledOnce();
102
+ });
103
+ });
104
+
105
+ describe("onStart — runTscPackageBuild 옵션 전달", () => {
106
+ it("pkgDir, cwd, output.dts를 올바르게 전달한다", async () => {
107
+ const result = createTscPlugin(baseOptions);
108
+ const lifecycle = setupPlugin(result.plugin);
109
+
110
+ await lifecycle.invokeOnStart();
111
+ await lifecycle.invokeOnEnd();
112
+
113
+ expect(mockRunTscPackageBuild).toHaveBeenCalledWith(
114
+ expect.objectContaining({
115
+ pkgDir: "/workspace/packages/my-server",
116
+ cwd: "/workspace",
117
+ output: { js: false, dts: true },
118
+ parsedConfig: mockParsedConfig,
119
+ }),
120
+ );
121
+ });
122
+
123
+ it("output.dts가 false일 때도 정확하게 전달한다", async () => {
124
+ const result = createTscPlugin({
125
+ ...baseOptions,
126
+ output: { dts: false },
127
+ });
128
+ const lifecycle = setupPlugin(result.plugin);
129
+
130
+ await lifecycle.invokeOnStart();
131
+ await lifecycle.invokeOnEnd();
132
+
133
+ expect(mockRunTscPackageBuild).toHaveBeenCalledWith(
134
+ expect.objectContaining({
135
+ output: { js: false, dts: false },
136
+ }),
137
+ );
138
+ });
139
+ });
140
+
141
+ describe("onEnd — 상태 저장", () => {
142
+ it("tsc 결과의 diagnostics를 정확히 저장한다", async () => {
143
+ const diagnostics = [
144
+ { category: 0, code: 6031, messageText: "Watching for changes" },
145
+ { category: 1, code: 2322, messageText: "Type error" },
146
+ ];
147
+ mockRunTscPackageBuild.mockReturnValue({
148
+ ...createSuccessTscResult(),
149
+ diagnostics,
150
+ });
151
+
152
+ const result = createTscPlugin(baseOptions);
153
+ const lifecycle = setupPlugin(result.plugin);
154
+
155
+ await lifecycle.invokeOnStart();
156
+ await lifecycle.invokeOnEnd();
157
+
158
+ expect(result.getDiagnostics()).toEqual(diagnostics);
159
+ });
160
+
161
+ it("parseTsconfig 예외 시에도 에러를 저장한다", async () => {
162
+ mockParseTsconfig.mockImplementation(() => {
163
+ throw new Error("Invalid tsconfig.json");
164
+ });
165
+
166
+ const result = createTscPlugin(baseOptions);
167
+ const lifecycle = setupPlugin(result.plugin);
168
+
169
+ await lifecycle.invokeOnStart();
170
+ await lifecycle.invokeOnEnd();
171
+
172
+ expect(result.getErrors()).toEqual(["Invalid tsconfig.json"]);
173
+ expect(result.getDiagnostics()).toEqual([]);
174
+ expect(result.getProgram()).toBeUndefined();
175
+ });
176
+ });
177
+
178
+ describe("getter — 연속 빌드", () => {
179
+ it("두 번째 빌드 결과가 첫 번째 결과를 덮어쓴다", async () => {
180
+ const result = createTscPlugin(baseOptions);
181
+ const lifecycle = setupPlugin(result.plugin);
182
+
183
+ // 첫 번째 빌드 — 에러
184
+ const errorResult: TscPackageBuildResult = {
185
+ success: false,
186
+ errors: ["first error"],
187
+ diagnostics: [{ category: 1, code: 1, messageText: "err" }],
188
+ errorCount: 1,
189
+ warningCount: 0,
190
+ };
191
+ mockRunTscPackageBuild.mockReturnValue(errorResult);
192
+ await lifecycle.invokeOnStart();
193
+ await lifecycle.invokeOnEnd();
194
+ expect(result.getErrors()).toEqual(["first error"]);
195
+
196
+ // 두 번째 빌드 — 성공
197
+ mockRunTscPackageBuild.mockReturnValue(createSuccessTscResult());
198
+ await lifecycle.invokeOnStart();
199
+ await lifecycle.invokeOnEnd();
200
+ expect(result.getErrors()).toBeUndefined();
201
+ expect(result.getProgram()).toBeDefined();
202
+ });
203
+ });
204
+
205
+ describe("resetBuilderProgram", () => {
206
+ it("빌드 전 호출해도 에러가 발생하지 않는다", () => {
207
+ const result = createTscPlugin(baseOptions);
208
+ expect(() => result.resetBuilderProgram()).not.toThrow();
209
+ });
210
+
211
+ it("여러 번 호출해도 안전하다", async () => {
212
+ const result = createTscPlugin(baseOptions);
213
+ const lifecycle = setupPlugin(result.plugin);
214
+
215
+ await lifecycle.invokeOnStart();
216
+ await lifecycle.invokeOnEnd();
217
+
218
+ result.resetBuilderProgram();
219
+ result.resetBuilderProgram();
220
+
221
+ // 리셋 후 빌드 가능
222
+ await lifecycle.invokeOnStart();
223
+ await lifecycle.invokeOnEnd();
224
+
225
+ expect(mockRunTscPackageBuild).toHaveBeenLastCalledWith(
226
+ expect.objectContaining({ oldBuilderProgram: undefined }),
227
+ );
228
+ });
229
+ });
230
+ });
@@ -0,0 +1,6 @@
1
+ # Feature 1.2 client 빌드 PostCSS 설정 통합 — LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] postcssConfigPath가 CreateClientEsbuildOptions 인터페이스에서 제거됨: `esbuild-client-config.ts`에서 `postcssConfigPath` 검색 결과 없음 (No matches found). `client.worker.ts`에서도 2곳 모두 제거 확인.
6
+ - [x] createScssPlugin에 로딩된 PostCSS 인스턴스가 전달됨: `esbuild-client-config.ts:118`에서 `postcssPlugins: loadedPostcssPlugins`로 `createScssPlugin`에 전달 확인.