@simplysm/sd-cli 14.0.38 → 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 (50) 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/esbuild/esbuild-client-config.d.ts +0 -2
  6. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  7. package/dist/esbuild/esbuild-client-config.js +19 -9
  8. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  9. package/dist/esbuild/esbuild-postcss-plugin.d.ts +8 -0
  10. package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -0
  11. package/dist/esbuild/esbuild-postcss-plugin.js +105 -0
  12. package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -0
  13. package/dist/esbuild/esbuild-tsc-plugin.d.ts +23 -0
  14. package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -0
  15. package/dist/esbuild/esbuild-tsc-plugin.js +60 -0
  16. package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -0
  17. package/dist/workers/client.worker.d.ts.map +1 -1
  18. package/dist/workers/client.worker.js +32 -2
  19. package/dist/workers/client.worker.js.map +1 -1
  20. package/dist/workers/server-build.worker.d.ts.map +1 -1
  21. package/dist/workers/server-build.worker.js +129 -90
  22. package/dist/workers/server-build.worker.js.map +1 -1
  23. package/dist/workers/server-esbuild-context.d.ts +27 -0
  24. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  25. package/dist/workers/server-esbuild-context.js +43 -3
  26. package/dist/workers/server-esbuild-context.js.map +1 -1
  27. package/package.json +6 -4
  28. package/src/angular/angular-build-pipeline.ts +2 -2
  29. package/src/angular/client-transform-stylesheet.ts +4 -4
  30. package/src/esbuild/esbuild-client-config.ts +21 -12
  31. package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
  32. package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
  33. package/src/workers/client.worker.ts +32 -2
  34. package/src/workers/server-build.worker.ts +136 -97
  35. package/src/workers/server-esbuild-context.ts +59 -3
  36. package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
  37. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
  38. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
  39. package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
  40. package/tests/utils/esbuild-client-config.acc.spec.ts +26 -14
  41. package/tests/utils/esbuild-client-config.spec.ts +73 -11
  42. package/tests/utils/esbuild-postcss-plugin.acc.spec.ts +299 -0
  43. package/tests/utils/esbuild-postcss-plugin.spec.ts +290 -0
  44. package/tests/utils/esbuild-scss-plugin.acc.spec.ts +1 -0
  45. package/tests/workers/server-build-lint.spec.ts +43 -0
  46. package/tests/workers/server-build-worker-refactoring.verify.md +14 -0
  47. package/tests/workers/server-build-worker.spec.ts +122 -9
  48. package/tests/workers/server-esbuild-context-tsc.verify.md +7 -0
  49. package/tests/workers/server-esbuild-context.acc.spec.ts +156 -2
  50. package/tests/workers/server-esbuild-context.spec.ts +320 -2
@@ -1,8 +1,12 @@
1
+ import type ts from "typescript";
1
2
  import esbuild from "esbuild";
2
3
  import {
3
4
  createServerEsbuildOptions,
4
5
  writeChangedOutputFiles,
5
6
  } from "../esbuild/esbuild-config";
7
+ import { createTscPlugin, type TscPluginResult } from "../esbuild/esbuild-tsc-plugin";
8
+ import type { TypecheckEnv } from "../utils/tsconfig";
9
+ import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
6
10
 
7
11
  /**
8
12
  * esbuild watch context 생성 옵션
@@ -12,6 +16,13 @@ export interface EsbuildContextOptions {
12
16
  entryPoints: string[];
13
17
  env?: Record<string, string>;
14
18
  external: string[];
19
+ /** tsc 플러그인 옵션. 제공 시 createTscPlugin으로 플러그인을 생성하여 esbuild context에 포함한다. */
20
+ tsc?: {
21
+ cwd: string;
22
+ output: { dts: boolean };
23
+ env?: TypecheckEnv;
24
+ includeTests?: boolean;
25
+ };
15
26
  }
16
27
 
17
28
  /** esbuild watch context (모듈 스코프 상태) */
@@ -20,11 +31,24 @@ let context: esbuild.BuildContext | undefined;
20
31
  /** 마지막 빌드의 metafile (변경 필터링용) */
21
32
  let lastMetafile: esbuild.Metafile | undefined;
22
33
 
34
+ /** tsc 플러그인 인스턴스 (모듈 스코프 상태) */
35
+ let tscPlugin: TscPluginResult | undefined;
36
+
23
37
  /**
24
38
  * esbuild watch context를 생성한다.
25
39
  * dev 모드 전용 (metafile:true, write:false).
26
40
  */
27
41
  export async function createContext(options: EsbuildContextOptions): Promise<void> {
42
+ if (options.tsc != null) {
43
+ tscPlugin = createTscPlugin({
44
+ pkgDir: options.pkgDir,
45
+ cwd: options.tsc.cwd,
46
+ output: options.tsc.output,
47
+ env: options.tsc.env,
48
+ includeTests: options.tsc.includeTests,
49
+ });
50
+ }
51
+
28
52
  const baseOptions = createServerEsbuildOptions({
29
53
  pkgDir: options.pkgDir,
30
54
  entryPoints: options.entryPoints,
@@ -35,6 +59,7 @@ export async function createContext(options: EsbuildContextOptions): Promise<voi
35
59
 
36
60
  context = await esbuild.context({
37
61
  ...baseOptions,
62
+ plugins: tscPlugin != null ? [tscPlugin.plugin] : [],
38
63
  metafile: true,
39
64
  write: false,
40
65
  });
@@ -61,12 +86,14 @@ export async function rebuild(): Promise<{
61
86
  await writeChangedOutputFiles(result.outputFiles);
62
87
  }
63
88
 
64
- const errors = result.errors.map((e) => e.text);
89
+ const esbuildErrors = result.errors.map((e) => e.text);
90
+ const tscErrors = tscPlugin?.getErrors() ?? [];
91
+ const allErrors = [...esbuildErrors, ...tscErrors];
65
92
  const warnings = result.warnings.map((w) => w.text);
66
93
 
67
94
  return {
68
- success: result.errors.length === 0,
69
- errors: errors.length > 0 ? errors : undefined,
95
+ success: allErrors.length === 0,
96
+ errors: allErrors.length > 0 ? allErrors : undefined,
70
97
  warnings: warnings.length > 0 ? warnings : undefined,
71
98
  };
72
99
  }
@@ -85,6 +112,10 @@ export async function recreateContext(options: EsbuildContextOptions): Promise<v
85
112
  context = undefined;
86
113
  lastMetafile = undefined;
87
114
 
115
+ if (tscPlugin != null) {
116
+ tscPlugin.resetBuilderProgram();
117
+ }
118
+
88
119
  try {
89
120
  await createContext(options);
90
121
  } finally {
@@ -101,6 +132,7 @@ export async function dispose(): Promise<void> {
101
132
  const contextToDispose = context;
102
133
  context = undefined;
103
134
  lastMetafile = undefined;
135
+ tscPlugin = undefined;
104
136
 
105
137
  if (contextToDispose != null) {
106
138
  await contextToDispose.dispose();
@@ -120,3 +152,27 @@ export function getMetafile(): esbuild.Metafile | undefined {
120
152
  export function hasContext(): boolean {
121
153
  return context != null;
122
154
  }
155
+
156
+ /**
157
+ * tsc 플러그인의 ts.Program을 반환한다.
158
+ * 플러그인이 없으면 undefined를 반환한다.
159
+ */
160
+ export function getTscProgram(): ts.Program | undefined {
161
+ return tscPlugin?.getProgram();
162
+ }
163
+
164
+ /**
165
+ * tsc 플러그인의 affected files를 반환한다.
166
+ * 플러그인이 없으면 undefined를 반환한다.
167
+ */
168
+ export function getTscAffectedFiles(): ReadonlySet<string> | undefined {
169
+ return tscPlugin?.getAffectedFiles();
170
+ }
171
+
172
+ /**
173
+ * tsc 플러그인의 diagnostics를 반환한다.
174
+ * 플러그인이 없으면 빈 배열을 반환한다.
175
+ */
176
+ export function getTscDiagnostics(): SerializedDiagnostic[] {
177
+ return tscPlugin?.getDiagnostics() ?? [];
178
+ }
@@ -118,7 +118,7 @@ describe("createClientTransformStylesheet", () => {
118
118
  const deps = new Map<string, Set<string>>();
119
119
  const transform = createClientTransformStylesheet({
120
120
  loadPaths: [],
121
- postCssPlugins: [testPlugin],
121
+ postcssPlugins: [testPlugin],
122
122
  scssErrors: errors,
123
123
  scssDependencies: deps,
124
124
  });
@@ -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
+ });