@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
@@ -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`에 코드 변경 없음: 확인 — 파일 미수정
@@ -5,6 +5,15 @@ import { describe, it, expect, vi, afterEach } from "vitest";
5
5
  const mockRebuild = vi.fn();
6
6
  const mockDispose = vi.fn();
7
7
 
8
+ const mockTscPlugin = {
9
+ plugin: { name: "sd-tsc", setup: vi.fn() },
10
+ getProgram: vi.fn(),
11
+ getAffectedFiles: vi.fn(),
12
+ getDiagnostics: vi.fn((): unknown[] => []),
13
+ getErrors: vi.fn(),
14
+ resetBuilderProgram: vi.fn(),
15
+ };
16
+
8
17
  vi.mock("esbuild", () => ({
9
18
  default: {
10
19
  context: vi.fn(() =>
@@ -21,11 +30,18 @@ vi.mock("../../src/esbuild/esbuild-config", async (importOriginal) => {
21
30
  };
22
31
  });
23
32
 
33
+ vi.mock("../../src/esbuild/esbuild-tsc-plugin", () => ({
34
+ createTscPlugin: vi.fn(() => mockTscPlugin),
35
+ }));
36
+
24
37
  //#endregion
25
38
 
26
39
  const esbuild = (await import("esbuild")).default;
27
- const { createContext, rebuild, recreateContext, dispose, getMetafile, hasContext } =
28
- await import("../../src/workers/server-esbuild-context");
40
+ const {
41
+ createContext, rebuild, recreateContext, dispose, getMetafile, hasContext,
42
+ getTscProgram, getTscAffectedFiles, getTscDiagnostics,
43
+ } = await import("../../src/workers/server-esbuild-context");
44
+ const { createTscPlugin } = await import("../../src/esbuild/esbuild-tsc-plugin");
29
45
 
30
46
  const baseOptions = {
31
47
  pkgDir: "/workspace/packages/my-server",
@@ -33,11 +49,22 @@ const baseOptions = {
33
49
  external: [],
34
50
  };
35
51
 
52
+ const baseTscOptions = {
53
+ cwd: "/workspace",
54
+ output: { dts: true },
55
+ };
56
+
36
57
  describe("server-esbuild-context lifecycle", () => {
37
58
  afterEach(async () => {
38
59
  mockRebuild.mockReset();
39
60
  mockDispose.mockReset();
61
+ mockTscPlugin.getProgram.mockReset();
62
+ mockTscPlugin.getAffectedFiles.mockReset();
63
+ mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
64
+ mockTscPlugin.getErrors.mockReset();
65
+ mockTscPlugin.resetBuilderProgram.mockReset();
40
66
  vi.mocked(esbuild.context).mockClear();
67
+ vi.mocked(createTscPlugin).mockClear();
41
68
  await dispose();
42
69
  });
43
70
 
@@ -96,3 +123,130 @@ describe("server-esbuild-context lifecycle", () => {
96
123
  expect(result).toBeNull();
97
124
  });
98
125
  });
126
+
127
+ describe("server-esbuild-context tsc integration lifecycle", () => {
128
+ afterEach(async () => {
129
+ mockRebuild.mockReset();
130
+ mockDispose.mockReset();
131
+ mockTscPlugin.getProgram.mockReset();
132
+ mockTscPlugin.getAffectedFiles.mockReset();
133
+ mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
134
+ mockTscPlugin.getErrors.mockReset();
135
+ mockTscPlugin.resetBuilderProgram.mockReset();
136
+ vi.mocked(esbuild.context).mockClear();
137
+ vi.mocked(createTscPlugin).mockClear();
138
+ await dispose();
139
+ });
140
+
141
+ // Acceptance: create with tsc → delegation works → dispose clears all
142
+ it("manages tsc plugin lifecycle: create with tsc → delegation returns plugin values → dispose clears", async () => {
143
+ const fakeProgram = { id: "fake-program" };
144
+ const fakeAffectedFiles = new Set(["/src/main.ts"]);
145
+ const fakeDiagnostics = [{ category: 1, code: 2322, messageText: "Type error" }];
146
+ mockTscPlugin.getProgram.mockReturnValue(fakeProgram);
147
+ mockTscPlugin.getAffectedFiles.mockReturnValue(fakeAffectedFiles);
148
+ mockTscPlugin.getDiagnostics.mockReturnValue(fakeDiagnostics);
149
+
150
+ // Create with tsc options
151
+ await createContext({ ...baseOptions, tsc: baseTscOptions });
152
+ expect(hasContext()).toBe(true);
153
+
154
+ // Delegation methods return plugin values
155
+ expect(getTscProgram()).toBe(fakeProgram);
156
+ expect(getTscAffectedFiles()).toBe(fakeAffectedFiles);
157
+ expect(getTscDiagnostics()).toEqual(fakeDiagnostics);
158
+
159
+ // Dispose clears everything
160
+ await dispose();
161
+ expect(hasContext()).toBe(false);
162
+ expect(getTscProgram()).toBeUndefined();
163
+ expect(getTscAffectedFiles()).toBeUndefined();
164
+ expect(getTscDiagnostics()).toEqual([]);
165
+ });
166
+
167
+ // Acceptance: create with tsc → recreate without tsc → plugin persists
168
+ it("persists tsc plugin across context recreation when tsc options omitted", async () => {
169
+ const fakeProgram = { id: "persisted-program" };
170
+ mockTscPlugin.getProgram.mockReturnValue(fakeProgram);
171
+
172
+ // Initial create with tsc
173
+ await createContext({ ...baseOptions, tsc: baseTscOptions });
174
+ expect(getTscProgram()).toBe(fakeProgram);
175
+
176
+ // Recreate without tsc options — plugin persists
177
+ await recreateContext(baseOptions);
178
+ expect(hasContext()).toBe(true);
179
+ expect(getTscProgram()).toBe(fakeProgram);
180
+ });
181
+
182
+ // Acceptance: create without tsc → no plugin → delegation returns defaults
183
+ it("returns default values from delegation methods when no tsc plugin exists", async () => {
184
+ await createContext(baseOptions);
185
+
186
+ expect(hasContext()).toBe(true);
187
+ expect(getTscProgram()).toBeUndefined();
188
+ expect(getTscAffectedFiles()).toBeUndefined();
189
+ expect(getTscDiagnostics()).toEqual([]);
190
+ });
191
+
192
+ // Acceptance: rebuild merges esbuild + tsc errors
193
+ it("merges esbuild and tsc errors in rebuild result", async () => {
194
+ mockRebuild.mockResolvedValue({
195
+ errors: [{ text: "esbuild syntax error" }],
196
+ warnings: [{ text: "deprecation" }],
197
+ outputFiles: [],
198
+ metafile: undefined,
199
+ });
200
+ mockTscPlugin.getErrors.mockReturnValue(["TS2322: type mismatch"]);
201
+
202
+ await createContext({ ...baseOptions, tsc: baseTscOptions });
203
+ const result = await rebuild();
204
+
205
+ expect(result).toEqual({
206
+ success: false,
207
+ errors: ["esbuild syntax error", "TS2322: type mismatch"],
208
+ warnings: ["deprecation"],
209
+ });
210
+ });
211
+
212
+ // Acceptance: recreateContext resets tsc and persists plugin
213
+ it("resets tsc builderProgram on recreateContext and persists plugin for next rebuild", async () => {
214
+ mockRebuild.mockResolvedValue({
215
+ errors: [],
216
+ warnings: [],
217
+ outputFiles: [],
218
+ metafile: { inputs: {}, outputs: {} },
219
+ });
220
+ mockTscPlugin.getErrors.mockReturnValue(undefined);
221
+
222
+ await createContext({ ...baseOptions, tsc: baseTscOptions });
223
+
224
+ // recreateContext — resets tsc + creates new esbuild context
225
+ await recreateContext(baseOptions);
226
+
227
+ expect(mockTscPlugin.resetBuilderProgram).toHaveBeenCalled();
228
+ expect(hasContext()).toBe(true);
229
+
230
+ // rebuild still works with merged result
231
+ const result = await rebuild();
232
+ expect(result).toEqual({
233
+ success: true,
234
+ errors: undefined,
235
+ warnings: undefined,
236
+ });
237
+ });
238
+
239
+ // Acceptance: LOGIC-001 — recreateContext failure preserves tsc plugin reset
240
+ it("resets tsc and disposes old context even when recreateContext fails (LOGIC-001)", async () => {
241
+ await createContext({ ...baseOptions, tsc: baseTscOptions });
242
+
243
+ // New context creation fails
244
+ vi.mocked(esbuild.context).mockRejectedValueOnce(new Error("creation failed"));
245
+
246
+ await expect(recreateContext(baseOptions)).rejects.toThrow("creation failed");
247
+
248
+ expect(mockTscPlugin.resetBuilderProgram).toHaveBeenCalled();
249
+ expect(mockDispose).toHaveBeenCalled();
250
+ expect(hasContext()).toBe(false);
251
+ });
252
+ });