@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
@@ -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
+ }
@@ -1,37 +1,10 @@
1
- import { vi } from "vitest";
2
1
  import path from "path";
3
- import type { SdConfig, SdClientPackageConfig } from "../../src/sd-config.types";
4
2
 
5
3
  export const FIXTURE_DIR = path.resolve(import.meta.dirname, "fixtures");
6
4
  export const PKG_DIR = path.resolve(FIXTURE_DIR, "packages/basic-app");
7
5
 
8
- export function createTestSdConfig(
9
- overrides?: Partial<SdClientPackageConfig>,
10
- ): SdConfig {
11
- return {
12
- packages: {
13
- "basic-app": { target: "client", server: 3000, ...overrides },
14
- },
15
- };
16
- }
17
-
18
- /** Vite lifecycle 시뮬레이션: config → configResolved → [configureServer] → buildStart */
19
- export async function initPlugin(
20
- plugin: any,
21
- options?: { mode?: string; command?: string; sourcemap?: boolean; withServer?: boolean },
22
- ): Promise<void> {
23
- const mode = options?.mode ?? "development";
24
- const command = options?.command ?? "serve";
25
- const sourcemap = options?.sourcemap ?? false;
26
- await plugin.config?.({}, { mode, command });
27
- plugin.configResolved?.({ build: { sourcemap } });
28
- if (options?.withServer === true) {
29
- plugin.configureServer?.({
30
- middlewares: { use: vi.fn() },
31
- httpServer: { on: vi.fn() },
32
- config: { base: "/" },
33
- hot: { send: vi.fn() },
34
- });
35
- }
6
+ /** Vite lifecycle 시뮬레이션: config → buildStart */
7
+ export async function initPlugin(plugin: any): Promise<void> {
8
+ await plugin.config?.({}, { mode: "development", command: "serve" });
36
9
  await plugin.buildStart?.call({});
37
10
  }
@@ -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
  });
@@ -1,35 +1,22 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import path from "path";
3
- import type { SdConfig } from "../../src/sd-config.types";
4
3
  import {
5
4
  FIXTURE_DIR,
6
5
  PKG_DIR,
7
- createTestSdConfig,
8
6
  initPlugin,
9
7
  } from "./_vite-angular-plugin-test-setup";
10
8
 
11
- const mockLoadSdConfig = vi.fn<(...args: unknown[]) => Promise<SdConfig>>();
12
-
13
- vi.mock("../../src/utils/sd-config", () => ({
14
- loadSdConfig: (...args: unknown[]) => mockLoadSdConfig(...args),
15
- }));
16
-
17
9
  const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin.js");
18
10
 
19
- describe("sdAngularPlugin legacy watch rebuild", () => {
11
+ describe("sdAngularPlugin watch rebuild", () => {
20
12
  beforeEach(() => {
21
13
  vi.clearAllMocks();
22
14
  vi.spyOn(process, "cwd").mockReturnValue(FIXTURE_DIR);
23
- mockLoadSdConfig.mockResolvedValue(
24
- createTestSdConfig({ browserSupport: { legacyModule: true } }),
25
- );
26
15
  });
27
16
 
28
17
  // Acceptance: 재빌드 시 변경 파일의 캐시가 무효화되고 증분 컴파일된다
29
18
  it("invalidates cache and produces updated output on watch rebuild", async () => {
30
- const plugin = sdAngularPlugin({
31
- pkg: "basic-app",
32
- });
19
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
33
20
 
34
21
  // 초기 빌드
35
22
  await initPlugin(plugin);
@@ -39,31 +26,29 @@ describe("sdAngularPlugin legacy watch rebuild", () => {
39
26
  .replace(/\\/g, "/");
40
27
 
41
28
  // 초기 transform 결과 캡처
42
- const initialResult = await (plugin as any).transform?.call({}, "", appComponentPath);
29
+ const initialResult = (plugin as any).transform?.call({}, "", appComponentPath);
43
30
  expect(initialResult).toBeDefined();
44
31
  expect(initialResult.code.length).toBeGreaterThan(0);
45
32
 
46
33
  // watchChange 호출 (파일 변경 알림)
47
34
  expect((plugin as any).watchChange).toBeDefined();
48
- await (plugin as any).watchChange?.call({}, appComponentPath, { event: "update" });
35
+ (plugin as any).watchChange?.call({}, appComponentPath, { event: "update" });
49
36
 
50
37
  // 재빌드 (buildStart 재호출)
51
38
  await (plugin as any).buildStart?.call({});
52
39
 
53
40
  // 재빌드 후 transform — 컴파일러가 재실행되어 결과를 반환해야 한다
54
- const rebuiltResult = await (plugin as any).transform?.call({}, "", appComponentPath);
41
+ const rebuiltResult = (plugin as any).transform?.call({}, "", appComponentPath);
55
42
  expect(rebuiltResult).toBeDefined();
56
43
  expect(rebuiltResult.code).toBeDefined();
57
44
  expect(rebuiltResult.code.length).toBeGreaterThan(0);
58
45
 
59
- await (plugin as any).buildEnd?.call({});
46
+ (plugin as any).buildEnd?.call({});
60
47
  });
61
48
 
62
- // Acceptance: 첫 buildStart는 기존 로직으로 전체 컴파일한다
49
+ // Acceptance: 첫 buildStart는 전체 컴파일한다
63
50
  it("performs full compilation on first buildStart", async () => {
64
- const plugin = sdAngularPlugin({
65
- pkg: "basic-app",
66
- });
51
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
67
52
 
68
53
  // watchChange 없이 첫 buildStart
69
54
  await initPlugin(plugin);
@@ -72,33 +57,31 @@ describe("sdAngularPlugin legacy watch rebuild", () => {
72
57
  .join(PKG_DIR, "src/app.component.ts")
73
58
  .replace(/\\/g, "/");
74
59
 
75
- const result = await (plugin as any).transform?.call({}, "", appComponentPath);
60
+ const result = (plugin as any).transform?.call({}, "", appComponentPath);
76
61
  expect(result).toBeDefined();
77
62
  expect(result.code.length).toBeGreaterThan(0);
78
63
 
79
- await (plugin as any).buildEnd?.call({});
64
+ (plugin as any).buildEnd?.call({});
80
65
  });
81
66
 
82
67
  // Acceptance: watchChange 없이 buildStart 재호출 시에도 정상 동작
83
68
  it("handles buildStart re-invocation without watchChange gracefully", async () => {
84
- const plugin = sdAngularPlugin({
85
- pkg: "basic-app",
86
- });
69
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
87
70
 
88
71
  // 초기 빌드
89
72
  await initPlugin(plugin);
90
73
 
91
- // watchChange 없이 재빌드 (예: Rolldown이 변경 없이 재빌드 트리거)
74
+ // watchChange 없이 재빌드
92
75
  await (plugin as any).buildStart?.call({});
93
76
 
94
77
  const appComponentPath = path
95
78
  .join(PKG_DIR, "src/app.component.ts")
96
79
  .replace(/\\/g, "/");
97
80
 
98
- const result = await (plugin as any).transform?.call({}, "", appComponentPath);
81
+ const result = (plugin as any).transform?.call({}, "", appComponentPath);
99
82
  expect(result).toBeDefined();
100
83
  expect(result.code.length).toBeGreaterThan(0);
101
84
 
102
- await (plugin as any).buildEnd?.call({});
85
+ (plugin as any).buildEnd?.call({});
103
86
  });
104
87
  });
@@ -1,19 +1,11 @@
1
1
  import { describe, it, expect, vi, beforeAll } from "vitest";
2
2
  import type { Plugin } from "vite";
3
3
  import { resolve } from "node:path";
4
- import type { SdConfig } from "../../src/sd-config.types";
5
4
  import {
6
5
  FIXTURE_DIR,
7
6
  PKG_DIR,
8
- createTestSdConfig,
9
7
  } from "./_vite-angular-plugin-test-setup";
10
8
 
11
- const mockLoadSdConfig = vi.fn<(...args: unknown[]) => Promise<SdConfig>>();
12
-
13
- vi.mock("../../src/utils/sd-config", () => ({
14
- loadSdConfig: (...args: unknown[]) => mockLoadSdConfig(...args),
15
- }));
16
-
17
9
  const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin");
18
10
 
19
11
  describe("sdAngularPlugin Vitest 지원", () => {
@@ -21,18 +13,16 @@ describe("sdAngularPlugin Vitest 지원", () => {
21
13
 
22
14
  beforeAll(async () => {
23
15
  vi.spyOn(process, "cwd").mockReturnValue(FIXTURE_DIR);
24
- mockLoadSdConfig.mockResolvedValue(createTestSdConfig());
25
16
 
26
17
  plugin = sdAngularPlugin({ pkg: "basic-app" });
27
18
  await (plugin as any).config?.({}, { mode: "development", command: "serve" });
28
- (plugin as any).configResolved?.({ build: { sourcemap: false } });
29
19
  await (plugin as any).buildStart?.call({});
30
20
  }, 60_000);
31
21
 
32
22
  // Scenario: @Component 소스 컴파일 및 ɵcmp 런타임 코드 서빙
33
- it("compiles @Component source and serves ɵcmp runtime code", async () => {
23
+ it("compiles @Component source and serves ɵcmp runtime code", () => {
34
24
  const filePath = resolve(PKG_DIR, "src/app.component.ts");
35
- const result = await (plugin as any).transform?.call({}, "", filePath);
25
+ const result = (plugin as any).transform?.call({}, "", filePath);
36
26
 
37
27
  expect(result).toBeDefined();
38
28
  expect(result!.code).toContain("ɵcmp");
@@ -40,32 +30,12 @@ describe("sdAngularPlugin Vitest 지원", () => {
40
30
  });
41
31
 
42
32
  // Scenario: node_modules .ts 파일은 AOT 대상이 아니므로 undefined 반환
43
- it("returns undefined for node_modules .ts path", async () => {
44
- const result = await (plugin as any).transform?.call(
33
+ it("returns undefined for node_modules .ts path", () => {
34
+ const result = (plugin as any).transform?.call(
45
35
  {},
46
36
  "",
47
37
  resolve(FIXTURE_DIR, "node_modules/@angular/core/index.ts"),
48
38
  );
49
39
  expect(result).toBeUndefined();
50
40
  });
51
-
52
- // Scenario: browser target 패키지에서 플러그인 초기화
53
- it("initializes successfully with non-client (browser target) package", async () => {
54
- mockLoadSdConfig.mockResolvedValue(
55
- createTestSdConfig({ target: "browser" } as any),
56
- );
57
-
58
- const browserPlugin = sdAngularPlugin({ pkg: "basic-app" });
59
-
60
- await (browserPlugin as any).config?.({}, { mode: "development", command: "serve" });
61
- (browserPlugin as any).configResolved?.({ build: { sourcemap: false } });
62
- await (browserPlugin as any).buildStart?.call({});
63
-
64
- const appComponentPath = resolve(PKG_DIR, "src/app.component.ts");
65
- const result = await (browserPlugin as any).transform?.call({}, "", appComponentPath);
66
- expect(result).toBeDefined();
67
- expect(result.code.length).toBeGreaterThan(0);
68
-
69
- await (browserPlugin as any).buildEnd?.call({});
70
- });
71
41
  });
@@ -1,114 +1,95 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import path from "path";
3
- import type { SdConfig } from "../../src/sd-config.types";
4
3
  import {
5
4
  FIXTURE_DIR,
6
5
  PKG_DIR,
7
- createTestSdConfig,
8
6
  initPlugin,
9
7
  } from "./_vite-angular-plugin-test-setup";
10
8
 
11
- const mockLoadSdConfig = vi.fn<(...args: unknown[]) => Promise<SdConfig>>();
12
-
13
- vi.mock("../../src/utils/sd-config", () => ({
14
- loadSdConfig: (...args: unknown[]) => mockLoadSdConfig(...args),
15
- }));
16
-
17
9
  const { sdAngularPlugin } = await import("../../src/angular/vite-angular-plugin.js");
18
10
 
19
11
  beforeEach(() => {
20
12
  vi.clearAllMocks();
21
13
  vi.spyOn(process, "cwd").mockReturnValue(FIXTURE_DIR);
22
- mockLoadSdConfig.mockResolvedValue(createTestSdConfig());
23
14
  });
24
15
 
25
16
  describe("sdAngularPlugin", () => {
26
- // config() define 반환은 Vitest 축소로 제거됨 (esbuild-client-config에서 처리)
27
-
28
17
  // Scenario: non-Angular .ts 파일은 기본 처리
29
18
  it("returns undefined for non-emitted .ts files", async () => {
30
- const plugin = sdAngularPlugin({ pkg: "basic-app"});
19
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
31
20
  await initPlugin(plugin);
32
21
 
33
- const result = await (plugin as any).transform?.call(
22
+ const result = (plugin as any).transform?.call(
34
23
  {},
35
24
  "export const x = 1;",
36
25
  "/some/unknown/file.ts",
37
26
  );
38
27
 
39
28
  expect(result).toBeUndefined();
40
- await (plugin as any).buildEnd?.call({});
29
+ (plugin as any).buildEnd?.call({});
41
30
  });
42
31
 
43
32
  // Scenario: buildStart에서 초기화 및 emit + buildEnd에서 리소스 정리
44
33
  it("initializes facade in buildStart and disposes in buildEnd", async () => {
45
- const plugin = sdAngularPlugin({ pkg: "basic-app"});
34
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
46
35
  await initPlugin(plugin);
47
- await (plugin as any).buildEnd?.call({});
36
+ (plugin as any).buildEnd?.call({});
48
37
  });
49
38
 
50
- // handleHotUpdate, configureServer, optimizeDeps는 Vitest 축소로 제거됨
51
- // Feature 3.3에서 esbuild 기반 테스트로 교체 예정
52
-
53
- // Scenario: .mjs 파일이 JavaScriptTransformer를 통과한다
54
- it("transforms .mjs files through JavaScriptTransformer", async () => {
55
- const plugin = sdAngularPlugin({ pkg: "basic-app"});
39
+ // Scenario: .mjs/.js 파일은 처리하지 않는다
40
+ it("returns undefined for .mjs files", async () => {
41
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
56
42
  await initPlugin(plugin);
57
43
 
58
- const result = await (plugin as any).transform?.call(
44
+ const result = (plugin as any).transform?.call(
59
45
  {},
60
46
  "export const x = 1;",
61
47
  "/some/library/module.mjs",
62
48
  );
63
49
 
64
- expect(result).toBeDefined();
65
- expect(result.code).toBeDefined();
66
- expect(typeof result.code).toBe("string");
67
- await (plugin as any).buildEnd?.call({});
50
+ expect(result).toBeUndefined();
51
+ (plugin as any).buildEnd?.call({});
68
52
  });
69
53
 
70
- // Scenario: .js 파일이 JavaScriptTransformer를 통과한다
71
- it("transforms .js files through JavaScriptTransformer", async () => {
72
- const plugin = sdAngularPlugin({ pkg: "basic-app"});
54
+ it("returns undefined for .js files", async () => {
55
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
73
56
  await initPlugin(plugin);
74
57
 
75
- const result = await (plugin as any).transform?.call(
58
+ const result = (plugin as any).transform?.call(
76
59
  {},
77
60
  "export const y = 2;",
78
61
  "/some/library/module.js",
79
62
  );
80
63
 
81
- expect(result).toBeDefined();
82
- expect(result.code).toBeDefined();
83
- expect(typeof result.code).toBe("string");
84
- await (plugin as any).buildEnd?.call({});
64
+ expect(result).toBeUndefined();
65
+ (plugin as any).buildEnd?.call({});
85
66
  });
86
67
 
87
68
  // Scenario: 비대상 파일(.css 등)은 transform하지 않는다
88
69
  it("returns undefined for non-JS files like .css", async () => {
89
- const plugin = sdAngularPlugin({ pkg: "basic-app"});
70
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
90
71
  await initPlugin(plugin);
91
72
 
92
- const result = await (plugin as any).transform?.call(
73
+ const result = (plugin as any).transform?.call(
93
74
  {},
94
75
  "body { color: red; }",
95
76
  "/some/styles.css",
96
77
  );
97
78
 
98
79
  expect(result).toBeUndefined();
99
- await (plugin as any).buildEnd?.call({});
80
+ (plugin as any).buildEnd?.call({});
100
81
  });
101
82
 
102
- // Scenario: Angular .ts 파일 transform (prod 모드)
83
+ // Scenario: Angular .ts 파일 transform
103
84
  it("transforms emitted .ts files with compiled JS", async () => {
104
- const plugin = sdAngularPlugin({ pkg: "basic-app"});
105
- await initPlugin(plugin, { mode: "production", command: "build" });
85
+ const plugin = sdAngularPlugin({ pkg: "basic-app" });
86
+ await initPlugin(plugin);
106
87
 
107
88
  const appComponentPath = path
108
89
  .join(PKG_DIR, "src/app.component.ts")
109
90
  .replace(/\\/g, "/");
110
91
 
111
- const result = await (plugin as any).transform?.call({}, "", appComponentPath);
92
+ const result = (plugin as any).transform?.call({}, "", appComponentPath);
112
93
 
113
94
  if (result != null) {
114
95
  expect(result).toHaveProperty("code");
@@ -116,23 +97,6 @@ describe("sdAngularPlugin", () => {
116
97
  expect(result.code.length).toBeGreaterThan(0);
117
98
  }
118
99
 
119
- await (plugin as any).buildEnd?.call({});
120
- });
121
-
122
- // config() define 반환값은 Vitest 축소로 제거됨 (Feature 3.3에서 별도 테스트)
123
-
124
- // Scenario: browserSupport 미설정 시 기본값 동작
125
- it("works when browserSupport is not set in sd.config.ts", async () => {
126
- mockLoadSdConfig.mockResolvedValue(createTestSdConfig());
127
-
128
- const plugin = sdAngularPlugin({ pkg: "basic-app" });
129
-
130
- await initPlugin(plugin);
131
-
132
- const appComponentPath = PKG_DIR.replace(/\\/g, "/") + "/src/app.component.ts";
133
- const result = await (plugin as any).transform?.call({}, "", appComponentPath);
134
- expect(result).toBeDefined();
135
-
136
- await (plugin as any).buildEnd?.call({});
100
+ (plugin as any).buildEnd?.call({});
137
101
  });
138
102
  });
@@ -27,7 +27,7 @@ vi.mock("../../src/typecheck/typecheck-non-package", () => ({
27
27
  typecheckNonPackageFiles: mocks.typecheckNonPackageFiles,
28
28
  }));
29
29
 
30
- vi.mock("../../src/engines/index", () => ({
30
+ vi.mock("../../src/engines/engine-factory", () => ({
31
31
  createTypecheckEngine: mocks.createTypecheckEngine,
32
32
  }));
33
33
 
@@ -254,6 +254,31 @@ describe("BaseEngine", () => {
254
254
  await engine.stop();
255
255
  });
256
256
 
257
+ it("_callStartWatch 실패 시 에러를 ResultCollector에 보고하고 resolveInitialBuild를 호출한다", async () => {
258
+ const mockResultCollector = { add: vi.fn() };
259
+
260
+ mockWorker.startWatch.mockRejectedValue(new Error("Worker RPC failed"));
261
+
262
+ const engine = new TscEngine({
263
+ cwd: "/root",
264
+ pkg: createMockPkg(),
265
+ resultCollector: mockResultCollector as any,
266
+ });
267
+
268
+ // startWatch()가 hang하지 않고 resolve되어야 한다
269
+ await engine.startWatch({ js: true, dts: true });
270
+
271
+ // ResultCollector에 에러가 보고되어야 한다
272
+ const errorReport = mockResultCollector.add.mock.calls.find(
273
+ (c: any[]) => c[0].type === "build" && c[0].status === "error",
274
+ );
275
+ expect(errorReport).toBeDefined();
276
+ expect(errorReport![0].name).toBe("test-pkg");
277
+ expect(errorReport![0].message).toContain("Worker RPC failed");
278
+
279
+ await engine.stop();
280
+ });
281
+
257
282
  it("calls resolver on error event to release RebuildManager batch", async () => {
258
283
  const mockResolver = vi.fn();
259
284
  const mockRebuildManager = { registerBuild: vi.fn(() => mockResolver) };
@@ -25,7 +25,7 @@ describe("EsbuildClientEngine adapter isolation", () => {
25
25
  }
26
26
  });
27
27
 
28
- it("vite-angular-plugin.ts imports only JavaScriptTransformer from @angular/build/private", () => {
28
+ it("vite-angular-plugin.ts does not import @angular/* directly", () => {
29
29
 
30
30
  const pluginFile = path.resolve(
31
31
  import.meta.dirname,
@@ -33,8 +33,8 @@ describe("EsbuildClientEngine adapter isolation", () => {
33
33
  );
34
34
  const content = fs.readFileSync(pluginFile, "utf-8");
35
35
 
36
- expect(content).toContain("@angular/build/private");
37
- expect(content).toContain("JavaScriptTransformer");
36
+ const angularImportPattern = /from\s+["']@angular\/(build|compiler-cli)/;
37
+ expect(angularImportPattern.test(content)).toBe(false);
38
38
  expect(content).not.toContain("createAngularCompilation");
39
39
  expect(content).not.toMatch(/\bSourceFileCache\b(?<!AngularSourceFileCache)/);
40
40
  expect(content).not.toContain("ComponentStylesheetBundler");
@@ -134,6 +134,35 @@ describe("EsbuildClientEngine Acceptance", () => {
134
134
  await engine.stop();
135
135
  });
136
136
 
137
+ // Scenario: 초기 빌드 실패 시 ResultCollector에 에러 보고
138
+ it("초기 빌드 실패 시 ResultCollector에 에러가 보고된다", async () => {
139
+ // Given: client 패키지가 정의되어 있다
140
+ const mockResultCollector = { add: vi.fn() };
141
+
142
+ mockWorker.startWatch.mockResolvedValue({
143
+ success: false,
144
+ errors: ["esbuild compilation failed"],
145
+ });
146
+
147
+ const engine = new EsbuildClientEngine({
148
+ cwd: "/root",
149
+ pkg: createMockPkg(),
150
+ resultCollector: mockResultCollector as any,
151
+ });
152
+
153
+ // When: startWatch()를 호출한다
154
+ await engine.startWatch({ js: true, dts: false });
155
+
156
+ // Then: ResultCollector에 에러가 보고된다
157
+ const errorReport = mockResultCollector.add.mock.calls.find(
158
+ (c: any[]) => c[0].type === "build" && c[0].status === "error",
159
+ );
160
+ expect(errorReport).toBeDefined();
161
+ expect(errorReport![0].name).toBe("my-client");
162
+
163
+ await engine.stop();
164
+ });
165
+
137
166
  // Scenario: 엔진 중지
138
167
  it("stop()으로 worker를 종료하고 .dev-port를 삭제한다", async () => {
139
168
  // Given: dev watch 모드가 실행 중이다
@@ -271,6 +271,32 @@ describe("EsbuildClientEngine", () => {
271
271
  await engine.stop();
272
272
  });
273
273
 
274
+ it("초기 빌드 실패 시 에러를 logger.error로 출력한다", async () => {
275
+ mockWorker.startWatch.mockResolvedValue({
276
+ success: false,
277
+ errors: ["Module not found: @angular/core", "Syntax error in app.ts"],
278
+ });
279
+
280
+ const engine = new EsbuildClientEngine({ cwd: "/root", pkg: createMockPkg() });
281
+ await engine.startWatch({ js: true, dts: false });
282
+
283
+ // startWatch가 reject하지 않고 정상 완료되는지 확인 (기존 동작 유지)
284
+ // 에러 로깅은 verify.md에서 검증
285
+ expect(mockWorker.startWatch).toHaveBeenCalled();
286
+
287
+ await engine.stop();
288
+ });
289
+
290
+ it("초기 빌드 성공 시 에러 로깅을 하지 않는다", async () => {
291
+ mockWorker.startWatch.mockResolvedValue({ success: true });
292
+
293
+ const engine = new EsbuildClientEngine({ cwd: "/root", pkg: createMockPkg() });
294
+ // 예외 없이 완료
295
+ await expect(engine.startWatch({ js: true, dts: false })).resolves.toBeUndefined();
296
+
297
+ await engine.stop();
298
+ });
299
+
274
300
  it("scopeRebuild 이벤트를 구독하지 않는다", async () => {
275
301
  mockWorker.startWatch.mockResolvedValue({ success: true });
276
302