@simplysm/sd-cli 14.0.18 → 14.0.21

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 (124) hide show
  1. package/dist/angular/vite-angular-plugin.d.ts +2 -0
  2. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  3. package/dist/angular/vite-angular-plugin.js +57 -28
  4. package/dist/angular/vite-angular-plugin.js.map +1 -1
  5. package/dist/angular/vite-postcss-inline-plugin.d.ts.map +1 -1
  6. package/dist/angular/vite-postcss-inline-plugin.js +4 -1
  7. package/dist/angular/vite-postcss-inline-plugin.js.map +1 -1
  8. package/dist/capacitor/capacitor-android.d.ts +16 -0
  9. package/dist/capacitor/capacitor-android.d.ts.map +1 -0
  10. package/dist/capacitor/capacitor-android.js +289 -0
  11. package/dist/capacitor/capacitor-android.js.map +1 -0
  12. package/dist/capacitor/capacitor.d.ts +0 -50
  13. package/dist/capacitor/capacitor.d.ts.map +1 -1
  14. package/dist/capacitor/capacitor.js +16 -281
  15. package/dist/capacitor/capacitor.js.map +1 -1
  16. package/dist/commands/check.js +2 -2
  17. package/dist/commands/check.js.map +1 -1
  18. package/dist/commands/device.d.ts.map +1 -1
  19. package/dist/commands/device.js +3 -2
  20. package/dist/commands/device.js.map +1 -1
  21. package/dist/commands/lint.d.ts +1 -42
  22. package/dist/commands/lint.d.ts.map +1 -1
  23. package/dist/commands/lint.js +1 -151
  24. package/dist/commands/lint.js.map +1 -1
  25. package/dist/commands/publish.d.ts.map +1 -1
  26. package/dist/commands/publish.js +2 -1
  27. package/dist/commands/publish.js.map +1 -1
  28. package/dist/commands/typecheck.d.ts +3 -40
  29. package/dist/commands/typecheck.d.ts.map +1 -1
  30. package/dist/commands/typecheck.js +3 -232
  31. package/dist/commands/typecheck.js.map +1 -1
  32. package/dist/electron/electron.d.ts.map +1 -1
  33. package/dist/electron/electron.js +20 -8
  34. package/dist/electron/electron.js.map +1 -1
  35. package/dist/orchestrators/DevWatchOrchestrator.d.ts +1 -0
  36. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  37. package/dist/orchestrators/DevWatchOrchestrator.js +16 -0
  38. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  39. package/dist/orchestrators/TypecheckOrchestrator.d.ts +74 -0
  40. package/dist/orchestrators/TypecheckOrchestrator.d.ts.map +1 -0
  41. package/dist/orchestrators/TypecheckOrchestrator.js +285 -0
  42. package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -0
  43. package/dist/sd-cli.js +6 -1
  44. package/dist/sd-cli.js.map +1 -1
  45. package/dist/utils/lint-core.d.ts +43 -0
  46. package/dist/utils/lint-core.d.ts.map +1 -0
  47. package/dist/utils/lint-core.js +154 -0
  48. package/dist/utils/lint-core.js.map +1 -0
  49. package/dist/utils/lint-utils.d.ts +1 -1
  50. package/dist/utils/lint-utils.d.ts.map +1 -1
  51. package/dist/utils/server-production-files.d.ts +22 -0
  52. package/dist/utils/server-production-files.d.ts.map +1 -0
  53. package/dist/utils/server-production-files.js +162 -0
  54. package/dist/utils/server-production-files.js.map +1 -0
  55. package/dist/utils/vite-config.d.ts +1 -1
  56. package/dist/utils/vite-config.d.ts.map +1 -1
  57. package/dist/utils/vite-config.js +76 -26
  58. package/dist/utils/vite-config.js.map +1 -1
  59. package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
  60. package/dist/utils/vite-scope-watch-plugin.js +7 -1
  61. package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
  62. package/dist/workers/lint.worker.d.ts +1 -1
  63. package/dist/workers/lint.worker.d.ts.map +1 -1
  64. package/dist/workers/lint.worker.js +1 -1
  65. package/dist/workers/lint.worker.js.map +1 -1
  66. package/dist/workers/server-build.worker.d.ts.map +1 -1
  67. package/dist/workers/server-build.worker.js +11 -161
  68. package/dist/workers/server-build.worker.js.map +1 -1
  69. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  70. package/dist/workers/server-runtime.worker.js +15 -0
  71. package/dist/workers/server-runtime.worker.js.map +1 -1
  72. package/package.json +9 -7
  73. package/src/angular/vite-angular-plugin.ts +88 -34
  74. package/src/angular/vite-postcss-inline-plugin.ts +5 -1
  75. package/src/capacitor/capacitor-android.ts +368 -0
  76. package/src/capacitor/capacitor.ts +18 -363
  77. package/src/commands/check.ts +2 -2
  78. package/src/commands/device.ts +3 -2
  79. package/src/commands/lint.ts +1 -201
  80. package/src/commands/publish.ts +2 -1
  81. package/src/commands/typecheck.ts +7 -292
  82. package/src/electron/electron.ts +15 -8
  83. package/src/orchestrators/DevWatchOrchestrator.ts +18 -0
  84. package/src/orchestrators/TypecheckOrchestrator.ts +364 -0
  85. package/src/sd-cli.ts +6 -1
  86. package/src/utils/lint-core.ts +205 -0
  87. package/src/utils/lint-utils.ts +1 -1
  88. package/src/utils/server-production-files.ts +186 -0
  89. package/src/utils/vite-config.ts +83 -27
  90. package/src/utils/vite-scope-watch-plugin.ts +6 -1
  91. package/src/workers/lint.worker.ts +1 -1
  92. package/src/workers/server-build.worker.ts +10 -185
  93. package/src/workers/server-runtime.worker.ts +15 -0
  94. package/tests/angular/linker-disk-cache.spec.ts +31 -25
  95. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
  96. package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
  97. package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +108 -0
  98. package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
  99. package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +10 -15
  100. package/tests/angular/vite-angular-plugin.spec.ts +80 -15
  101. package/tests/angular/vite-postcss-inline-plugin.spec.ts +10 -0
  102. package/tests/capacitor/capacitor-android-exports.verify.md +11 -0
  103. package/tests/capacitor/capacitor-android.spec.ts +219 -0
  104. package/tests/capacitor/capacitor-build.spec.ts +17 -21
  105. package/tests/capacitor/capacitor-icon.spec.ts +17 -19
  106. package/tests/capacitor/capacitor-init.spec.ts +18 -14
  107. package/tests/capacitor/capacitor-run.spec.ts +10 -24
  108. package/tests/capacitor/capacitor-workspace.spec.ts +30 -25
  109. package/tests/commands/check.spec.ts +2 -2
  110. package/tests/commands/device.spec.ts +12 -7
  111. package/tests/commands/lint.spec.ts +33 -194
  112. package/tests/commands/publish-set.verify.md +7 -0
  113. package/tests/electron/electron-symlink-cleanup.verify.md +8 -0
  114. package/tests/electron/electron.spec.ts +27 -2
  115. package/tests/orchestrators/dist-delete-watcher.verify.md +10 -0
  116. package/tests/orchestrators/typecheck-orchestrator.spec.ts +180 -0
  117. package/tests/sd-cli-catch-all.verify.md +7 -0
  118. package/tests/utils/lint-core-import-paths.verify.md +10 -0
  119. package/tests/utils/lint-core.spec.ts +188 -0
  120. package/tests/utils/server-production-files-import-paths.verify.md +14 -0
  121. package/tests/utils/vite-config.spec.ts +255 -133
  122. package/tests/utils/vite-scope-watch-plugin.spec.ts +22 -0
  123. package/tests/workers/server-build-context-dispose.verify.md +8 -0
  124. package/tests/workers/server-runtime-worker.spec.ts +48 -4
@@ -166,19 +166,19 @@ describe("runDevice", () => {
166
166
  await expect(runDevice({ target: "my-server", options: [] })).rejects.toThrow();
167
167
  });
168
168
 
169
- // Acceptance: Scenario "dev 서버 실행 device 실행 URL 자동 생성"
170
- it("auto-detects URL from .dev-port when server is a string", async () => {
169
+ // Acceptance: Scenario "server가 string일 서버 패키지의 .dev-port에서 포트 읽기"
170
+ it("reads .dev-port from server package directory when server is a string", async () => {
171
171
  vi.mocked(loadSdConfig).mockResolvedValue({
172
172
  packages: {
173
173
  "client-devtool": {
174
174
  target: "client",
175
- server: "server",
175
+ server: "my-server",
176
176
  electron: { appId: "com.test.electron" },
177
177
  },
178
178
  },
179
179
  });
180
180
 
181
- mockReadFileSync.mockReturnValue("5173");
181
+ mockReadFileSync.mockReturnValue("3000");
182
182
 
183
183
  // HTTP 헬스체크 성공 mock
184
184
  mockHttpGet.mockImplementation((_url: string, cb: Function) => {
@@ -189,8 +189,13 @@ describe("runDevice", () => {
189
189
 
190
190
  await runDevice({ target: "client-devtool", options: [] });
191
191
 
192
+ // 서버 패키지 경로의 .dev-port를 읽어야 함
193
+ expect(mockReadFileSync).toHaveBeenCalledWith(
194
+ expect.stringContaining("my-server"),
195
+ "utf-8",
196
+ );
192
197
  expect(mockElectronInstance.run).toHaveBeenCalledWith(
193
- "http://localhost:5173/client-devtool/",
198
+ "http://localhost:3000/client-devtool/",
194
199
  );
195
200
  });
196
201
 
@@ -200,7 +205,7 @@ describe("runDevice", () => {
200
205
  packages: {
201
206
  "client-devtool": {
202
207
  target: "client",
203
- server: "server",
208
+ server: "my-server",
204
209
  electron: { appId: "com.test.electron" },
205
210
  },
206
211
  },
@@ -221,7 +226,7 @@ describe("runDevice", () => {
221
226
  packages: {
222
227
  "client-devtool": {
223
228
  target: "client",
224
- server: "server",
229
+ server: "my-server",
225
230
  electron: { appId: "com.test.electron" },
226
231
  },
227
232
  },
@@ -1,189 +1,16 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
 
3
- // Hoisted mock references — available inside vi.mock factories
4
- const mocks = vi.hoisted(() => ({
5
- fsxExists: vi.fn<(path: string) => Promise<boolean>>(),
6
- fsxGlob: vi.fn<(...args: unknown[]) => Promise<string[]>>(),
7
- filterByTargets: vi.fn(
8
- (files: string[], _targets: string[], _cwd: string) => files,
9
- ),
10
- lintFiles: vi.fn<() => Promise<Array<{ errorCount: number; warningCount: number }>>>(),
11
- loadFormatter: vi.fn(),
12
- outputFixes: vi.fn(),
13
- jitiImport: vi.fn(),
14
- eslintCtor: vi.fn(),
15
- }));
3
+ //#region Mocks
16
4
 
17
- vi.mock("@simplysm/core-node", () => ({
18
- fsx: { exists: mocks.fsxExists, glob: mocks.fsxGlob },
19
- pathx: { filterByTargets: mocks.filterByTargets },
20
- }));
21
-
22
- vi.mock("eslint", () => ({
23
- ESLint: class MockESLint {
24
- constructor(options: unknown) { mocks.eslintCtor(options); }
25
- lintFiles = mocks.lintFiles;
26
- loadFormatter = mocks.loadFormatter;
27
- static outputFixes = mocks.outputFixes;
28
- },
5
+ const mocks = vi.hoisted(() => ({
6
+ executeLint: vi.fn(),
29
7
  }));
30
8
 
31
- vi.mock("jiti", () => ({
32
- createJiti: vi.fn(() => ({ import: mocks.jitiImport })),
9
+ vi.mock("../../src/utils/lint-core", () => ({
10
+ executeLint: mocks.executeLint,
33
11
  }));
34
12
 
35
- vi.mock("consola", () => {
36
- const fns = (): Record<string, unknown> => ({
37
- debug: vi.fn(), start: vi.fn(), success: vi.fn(),
38
- info: vi.fn(), error: vi.fn(), warn: vi.fn(), log: vi.fn(),
39
- withTag: vi.fn(() => fns()),
40
- level: 0,
41
- });
42
- const c = fns();
43
- return { consola: c, default: c, LogLevels: {} };
44
- });
45
-
46
- const { loadIgnorePatterns, executeLint, runLint } = await import("../../src/commands/lint");
47
-
48
- //#region loadIgnorePatterns
49
-
50
- describe("loadIgnorePatterns", () => {
51
- beforeEach(() => vi.clearAllMocks());
52
-
53
- it("extracts globalIgnores patterns from eslint config", async () => {
54
- mocks.fsxExists.mockImplementation((p) =>
55
- Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
56
- );
57
- mocks.jitiImport.mockResolvedValue({
58
- default: [
59
- { ignores: ["dist/**", "node_modules/**"] },
60
- { files: ["*.ts"], rules: {} }, // not globalIgnores — has files key
61
- { ignores: [".cache/**"] },
62
- ],
63
- });
64
-
65
- const result = await loadIgnorePatterns("/project");
66
- expect(result).toEqual(["dist/**", "node_modules/**", ".cache/**"]);
67
- });
68
-
69
- it("ignores config objects that have files key", async () => {
70
- mocks.fsxExists.mockImplementation((p) =>
71
- Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
72
- );
73
- mocks.jitiImport.mockResolvedValue({
74
- default: [{ files: ["*.ts"], ignores: ["dist/**"] }],
75
- });
76
-
77
- const result = await loadIgnorePatterns("/project");
78
- expect(result).toEqual([]);
79
- });
80
-
81
- it("throws when no eslint config file found", async () => {
82
- mocks.fsxExists.mockResolvedValue(false);
83
-
84
- await expect(loadIgnorePatterns("/project")).rejects.toThrow(
85
- "ESLint 설정 파일",
86
- );
87
- });
88
- });
89
-
90
- //#endregion
91
-
92
- //#region executeLint
93
-
94
- describe("executeLint", () => {
95
- beforeEach(() => {
96
- vi.clearAllMocks();
97
- // Default: eslint config exists with no ignores
98
- mocks.fsxExists.mockImplementation((p) =>
99
- Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
100
- );
101
- mocks.jitiImport.mockResolvedValue({ default: [] });
102
- mocks.fsxGlob.mockResolvedValue(["/project/src/a.ts", "/project/src/b.ts"]);
103
- mocks.filterByTargets.mockImplementation((files) => files);
104
- mocks.lintFiles.mockResolvedValue([{ errorCount: 0, warningCount: 0 }]);
105
- mocks.loadFormatter.mockResolvedValue({
106
- format: vi.fn().mockResolvedValue(""),
107
- });
108
- });
109
-
110
- it("lints all collected files and returns success when no errors", async () => {
111
- const result = await executeLint({ targets: [], fix: false, timing: false });
112
-
113
- expect(result.success).toBe(true);
114
- expect(result.errorCount).toBe(0);
115
- expect(result.warningCount).toBe(0);
116
- expect(mocks.lintFiles).toHaveBeenCalled();
117
- });
118
-
119
- it("filters files by targets via pathx.filterByTargets", async () => {
120
- const filteredFiles = ["/project/packages/core-common/src/a.ts"];
121
- mocks.filterByTargets.mockReturnValue(filteredFiles);
122
-
123
- await executeLint({ targets: ["packages/core-common"], fix: false, timing: false });
124
-
125
- expect(mocks.filterByTargets).toHaveBeenCalledWith(
126
- expect.any(Array),
127
- ["packages/core-common"],
128
- expect.any(String),
129
- );
130
- });
131
-
132
- it("applies auto-fix when fix option is true", async () => {
133
- await executeLint({ targets: [], fix: true, timing: false });
134
-
135
- expect(mocks.outputFixes).toHaveBeenCalled();
136
- });
137
-
138
- it("sets TIMING env variable when timing option is true", async () => {
139
- const origTiming = process.env["TIMING"];
140
-
141
- await executeLint({ targets: [], fix: false, timing: true });
142
-
143
- expect(process.env["TIMING"]).toBe("1");
144
-
145
- // Cleanup
146
- if (origTiming == null) delete process.env["TIMING"];
147
- else process.env["TIMING"] = origTiming;
148
- });
149
-
150
- it("creates ESLint with cache enabled and correct cache location", async () => {
151
- await executeLint({ targets: [], fix: false, timing: false });
152
-
153
- expect(mocks.eslintCtor).toHaveBeenCalledWith(
154
- expect.objectContaining({
155
- cache: true,
156
- cacheLocation: expect.stringContaining("eslint.cache"),
157
- }),
158
- );
159
- });
160
-
161
- it("returns error count and formatted output when lint errors found", async () => {
162
- mocks.lintFiles.mockResolvedValue([
163
- { errorCount: 3, warningCount: 1 },
164
- { errorCount: 1, warningCount: 2 },
165
- ]);
166
- mocks.loadFormatter.mockResolvedValue({
167
- format: vi.fn().mockResolvedValue("error details"),
168
- });
169
-
170
- const result = await executeLint({ targets: [], fix: false, timing: false });
171
-
172
- expect(result.success).toBe(false);
173
- expect(result.errorCount).toBe(4);
174
- expect(result.warningCount).toBe(3);
175
- expect(result.formattedOutput).toBe("error details");
176
- });
177
-
178
- it("returns success when no files to lint", async () => {
179
- mocks.fsxGlob.mockResolvedValue([]);
180
-
181
- const result = await executeLint({ targets: [], fix: false, timing: false });
182
-
183
- expect(result.success).toBe(true);
184
- expect(mocks.lintFiles).not.toHaveBeenCalled();
185
- });
186
- });
13
+ const { runLint } = await import("../../src/commands/lint");
187
14
 
188
15
  //#endregion
189
16
 
@@ -200,15 +27,11 @@ describe("runLint", () => {
200
27
  writeSpy = vi.spyOn(process.stdout, "write").mockReturnValue(true);
201
28
 
202
29
  // Default: successful lint
203
- mocks.fsxExists.mockImplementation((p) =>
204
- Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
205
- );
206
- mocks.jitiImport.mockResolvedValue({ default: [] });
207
- mocks.fsxGlob.mockResolvedValue(["/project/src/a.ts"]);
208
- mocks.filterByTargets.mockImplementation((files) => files);
209
- mocks.lintFiles.mockResolvedValue([{ errorCount: 0, warningCount: 0 }]);
210
- mocks.loadFormatter.mockResolvedValue({
211
- format: vi.fn().mockResolvedValue(""),
30
+ mocks.executeLint.mockResolvedValue({
31
+ success: true,
32
+ errorCount: 0,
33
+ warningCount: 0,
34
+ formattedOutput: "",
212
35
  });
213
36
  });
214
37
 
@@ -218,9 +41,11 @@ describe("runLint", () => {
218
41
  });
219
42
 
220
43
  it("writes formatted output to stdout when there are results", async () => {
221
- mocks.lintFiles.mockResolvedValue([{ errorCount: 1, warningCount: 0 }]);
222
- mocks.loadFormatter.mockResolvedValue({
223
- format: vi.fn().mockResolvedValue("lint output here"),
44
+ mocks.executeLint.mockResolvedValue({
45
+ success: false,
46
+ errorCount: 1,
47
+ warningCount: 0,
48
+ formattedOutput: "lint output here",
224
49
  });
225
50
 
226
51
  await runLint({ targets: [], fix: false, timing: false });
@@ -229,15 +54,29 @@ describe("runLint", () => {
229
54
  });
230
55
 
231
56
  it("sets exitCode to 1 when lint errors are found", async () => {
232
- mocks.lintFiles.mockResolvedValue([{ errorCount: 1, warningCount: 0 }]);
233
- mocks.loadFormatter.mockResolvedValue({
234
- format: vi.fn().mockResolvedValue("errors"),
57
+ mocks.executeLint.mockResolvedValue({
58
+ success: false,
59
+ errorCount: 1,
60
+ warningCount: 0,
61
+ formattedOutput: "errors",
235
62
  });
236
63
 
237
64
  await runLint({ targets: [], fix: false, timing: false });
238
65
 
239
66
  expect(process.exitCode).toBe(1);
240
67
  });
68
+
69
+ it("does not set exitCode when lint passes", async () => {
70
+ await runLint({ targets: [], fix: false, timing: false });
71
+
72
+ expect(process.exitCode).toBeUndefined();
73
+ });
74
+
75
+ it("does not write to stdout when formattedOutput is empty", async () => {
76
+ await runLint({ targets: [], fix: false, timing: false });
77
+
78
+ expect(writeSpy).not.toHaveBeenCalled();
79
+ });
241
80
  });
242
81
 
243
82
  //#endregion
@@ -0,0 +1,7 @@
1
+ # 배포 실패 패키지 ���색 Set 사용 -- LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] Set 생성: `publish.ts:773` — `const publishedSet = new Set(publishedPackages)` 확인
6
+ - [x] Set.has() 사용: `publish.ts:774` — `allPkgNames.filter(n => !publishedSet.has(n))` 확인
7
+ - [x] 동작 동등성: `Array.includes()` → `Set.has()` 변환은 동일한 boolean 결과를 반환하���로 기능 동등
@@ -0,0 +1,8 @@
1
+ # symlink 테스트 임시 파일 정리 -- LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] try-catch-finally 구조 적용: `electron.ts:348-358` — try 블록에서 writeFile/symlink/lstat 수행, finally에서 정리
6
+ - [x] finally에서 testLink, testTarget 각각 unlink: `finally { try { fs.unlinkSync(testLink); } catch {} try { fs.unlinkSync(testTarget); } catch {} }` 확인
7
+ - [x] 성공 시에도 파일 정리: try 블록 return 후 finally가 실행되므로 정리 보장
8
+ - [x] 실패 시에도 파일 정리: catch 블록 return 후 finally가 실행되므로 정리 보장
@@ -245,6 +245,15 @@ describe("Electron", () => {
245
245
  expect(deps["sharp"]).toBe("^0.34.0");
246
246
  });
247
247
 
248
+ it("package.json에 type: module이 설정된다", async () => {
249
+ const { Electron } = await import("../../src/electron/electron.js");
250
+
251
+ const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
252
+ await electron.initialize();
253
+
254
+ expect(findElectronPackageJson()!["type"]).toBe("module");
255
+ });
256
+
248
257
  it("postInstallScript가 설정되면 scripts.postinstall에 포함한다", async () => {
249
258
  const { Electron } = await import("../../src/electron/electron.js");
250
259
 
@@ -277,7 +286,7 @@ describe("Electron", () => {
277
286
  expect.objectContaining({
278
287
  platform: "node",
279
288
  target: "node20",
280
- format: "cjs",
289
+ format: "esm",
281
290
  bundle: true,
282
291
  external: expect.arrayContaining(["electron", "better-sqlite3"]),
283
292
  }),
@@ -314,6 +323,18 @@ describe("Electron", () => {
314
323
  expect(banner).not.toContain("ELECTRON_DEV_URL");
315
324
  expect(callArgs.define).toBeUndefined();
316
325
  });
326
+
327
+ it("ESM 배너에 createRequire shim이 포함된다", async () => {
328
+ const { Electron } = await import("../../src/electron/electron.js");
329
+
330
+ const electron = await Electron.create(PKG_PATH, { appId: "com.test.app" });
331
+ await electron.build("/fake/out");
332
+
333
+ const callArgs = mockEsbuildBuild.mock.calls[0][0];
334
+ const banner = callArgs.banner?.js as string;
335
+ expect(banner).toContain("createRequire");
336
+ expect(banner).toContain("import.meta.url");
337
+ });
317
338
  });
318
339
 
319
340
  //#endregion
@@ -503,9 +524,13 @@ describe("Electron", () => {
503
524
 
504
525
  expect(callArgs.platform).toBe("node");
505
526
  expect(callArgs.target).toBe("node20");
506
- expect(callArgs.format).toBe("cjs");
527
+ expect(callArgs.format).toBe("esm");
507
528
  expect(callArgs.bundle).toBe(true);
508
529
  expect(callArgs.external).toContain("electron");
530
+
531
+ // ESM 배너에 createRequire shim 포함
532
+ expect(banner).toContain("createRequire");
533
+ expect(banner).toContain("import.meta.url");
509
534
  }, 10_000);
510
535
 
511
536
  it("throws when electron-main.ts entry point is missing", async () => {
@@ -0,0 +1,10 @@
1
+ # dist 삭제 감지 watcher 일반화 -- LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] 디버그 하드코딩 제거: `DevWatchOrchestrator.ts:281` — `angular` 하드코딩 블록이 제거됨
6
+ - [x] 모든 라이브러리 패키지 감시: `_startWatchMode()`에서 `this._libraryPackages`를 순회하며 각 `pkg.dir/dist`를 감시
7
+ - [x] 클래스 필드 저장: `_distDeleteWatchers: FsWatcher[]` 필드에 push
8
+ - [x] shutdown() 정리: `shutdown()`에서 `this._distDeleteWatchers.map(w => w.close())` 호출 확인
9
+ - [x] 정리 후 초기화: `this._distDeleteWatchers = []`로 참조 해제 확인
10
+ - [x] 로그 형식: `[dist-delete:{패키지명}]` 형식으로 어떤 패키지의 dist가 삭제되었는지 식별 가능
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ //#region Mocks
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ loadSdConfig: vi.fn(),
7
+ deserializeDiagnostic: vi.fn((d: any) => d),
8
+ typecheckNonPackageFiles: vi.fn(),
9
+ createBuildEngine: vi.fn(),
10
+ discoverWorkspacePackages: vi.fn(),
11
+ mergeTestsPackagesIntoConfig: vi.fn(),
12
+ }));
13
+
14
+ const mockEngines: Array<{
15
+ run: ReturnType<typeof vi.fn>;
16
+ stop: ReturnType<typeof vi.fn>;
17
+ }> = [];
18
+
19
+ vi.mock("../../src/utils/sd-config", () => ({
20
+ loadSdConfig: mocks.loadSdConfig,
21
+ }));
22
+
23
+ vi.mock("../../src/utils/typecheck-serialization", () => ({
24
+ deserializeDiagnostic: mocks.deserializeDiagnostic,
25
+ }));
26
+
27
+ vi.mock("../../src/utils/typecheck-non-package", () => ({
28
+ typecheckNonPackageFiles: mocks.typecheckNonPackageFiles,
29
+ }));
30
+
31
+ vi.mock("../../src/engines/index", () => ({
32
+ createBuildEngine: mocks.createBuildEngine,
33
+ }));
34
+
35
+ vi.mock("../../src/utils/package-utils", async (importOriginal) => {
36
+ const actual = await importOriginal<typeof import("../../src/utils/package-utils")>();
37
+ return {
38
+ ...actual,
39
+ discoverWorkspacePackages: mocks.discoverWorkspacePackages,
40
+ mergeTestsPackagesIntoConfig: mocks.mergeTestsPackagesIntoConfig,
41
+ };
42
+ });
43
+
44
+ vi.mock("typescript", async (importOriginal) => {
45
+ const orig = await importOriginal<Record<string, unknown>>();
46
+ const origDefault = (orig["default"] ?? orig) as Record<string, unknown>;
47
+ return {
48
+ ...orig,
49
+ default: {
50
+ ...origDefault,
51
+ sortAndDeduplicateDiagnostics: vi.fn((d: unknown[]) => d),
52
+ formatDiagnosticsWithColorAndContext: vi.fn((diags: Array<{ messageText: string }>) =>
53
+ diags.map((d) => `formatted: ${d.messageText}`).join("\n"),
54
+ ),
55
+ },
56
+ };
57
+ });
58
+
59
+ vi.mock("consola", () => {
60
+ const fns = (): Record<string, unknown> => ({
61
+ debug: vi.fn(), start: vi.fn(), success: vi.fn(),
62
+ info: vi.fn(), error: vi.fn(), warn: vi.fn(), log: vi.fn(),
63
+ withTag: vi.fn(() => fns()),
64
+ level: 0,
65
+ });
66
+ const c = fns();
67
+ return { consola: c, default: c, LogLevels: {} };
68
+ });
69
+
70
+ const { TypecheckOrchestrator } = await import(
71
+ "../../src/orchestrators/TypecheckOrchestrator"
72
+ );
73
+
74
+ //#endregion
75
+
76
+ //#region Helpers
77
+
78
+ function createMockEngine() {
79
+ const engine = {
80
+ run: vi.fn().mockResolvedValue({
81
+ build: { success: true, errors: [], warnings: [], diagnostics: [] },
82
+ }),
83
+ startWatch: vi.fn(),
84
+ stop: vi.fn().mockResolvedValue(undefined),
85
+ };
86
+ mockEngines.push(engine);
87
+ return engine;
88
+ }
89
+
90
+ function setupDefaults(packages: Record<string, any> = {}) {
91
+ mocks.loadSdConfig.mockResolvedValue({ packages });
92
+ mocks.createBuildEngine.mockImplementation(() => createMockEngine() as any);
93
+ mocks.typecheckNonPackageFiles.mockReturnValue({
94
+ success: true, errorCount: 0, warningCount: 0, diagnostics: [],
95
+ });
96
+
97
+ mocks.discoverWorkspacePackages.mockReturnValue(new Map<string, string>());
98
+ const pathMap = new Map<string, string>();
99
+ for (const name of Object.keys(packages)) {
100
+ pathMap.set(name, `packages/${name}`);
101
+ }
102
+ mocks.mergeTestsPackagesIntoConfig.mockReturnValue({
103
+ merged: packages,
104
+ pathMap,
105
+ });
106
+ }
107
+
108
+ //#endregion
109
+
110
+ //#region Tests
111
+
112
+ beforeEach(() => {
113
+ vi.clearAllMocks();
114
+ mockEngines.length = 0;
115
+ });
116
+
117
+ describe("TypecheckOrchestrator", () => {
118
+ // Acceptance: Scenario "TypecheckOrchestrator 정상 실행"
119
+ it("produces correct TypecheckResult through init → start → shutdown lifecycle", async () => {
120
+ setupDefaults({
121
+ "core-common": { target: "neutral" },
122
+ "core-node": { target: "node" },
123
+ });
124
+
125
+ const orchestrator = new TypecheckOrchestrator({ targets: [], options: [] });
126
+ await orchestrator.initialize();
127
+ const result = await orchestrator.start();
128
+ await orchestrator.shutdown();
129
+
130
+ expect(result.success).toBe(true);
131
+ expect(result.errorCount).toBe(0);
132
+ expect(result.warningCount).toBe(0);
133
+ // neutral → 2 tasks (node + browser), node → 1 task = 3 engines
134
+ expect(mocks.createBuildEngine).toHaveBeenCalledTimes(3);
135
+ for (const engine of mockEngines) {
136
+ expect(engine.run).toHaveBeenCalledWith(
137
+ expect.objectContaining({ js: false, dts: false }),
138
+ );
139
+ }
140
+ });
141
+
142
+ // Acceptance: Scenario "엔진 성공 시 리소스 정리"
143
+ it("calls engine.stop() after successful run", async () => {
144
+ setupDefaults({ "core-node": { target: "node" } });
145
+
146
+ const orchestrator = new TypecheckOrchestrator({ targets: [], options: [] });
147
+ await orchestrator.initialize();
148
+ await orchestrator.start();
149
+ await orchestrator.shutdown();
150
+
151
+ expect(mockEngines[0].stop).toHaveBeenCalled();
152
+ });
153
+
154
+ // Acceptance: Scenario "엔진 실패 시 리소스 정리"
155
+ it("calls engine.stop() even when run fails", async () => {
156
+ setupDefaults({ "core-node": { target: "node" } });
157
+ mocks.createBuildEngine.mockImplementation(() => {
158
+ const engine = {
159
+ run: vi.fn().mockRejectedValue(new Error("build error")),
160
+ startWatch: vi.fn(),
161
+ stop: vi.fn().mockResolvedValue(undefined),
162
+ };
163
+ mockEngines.push(engine);
164
+ return engine as any;
165
+ });
166
+
167
+ const orchestrator = new TypecheckOrchestrator({ targets: [], options: [] });
168
+ await orchestrator.initialize();
169
+ const result = await orchestrator.start();
170
+ await orchestrator.shutdown();
171
+
172
+ expect(result.success).toBe(false);
173
+ expect(mockEngines[0].stop).toHaveBeenCalled();
174
+ });
175
+ });
176
+
177
+ // executeTypecheck 편의 함수 테스트는 tests/commands/typecheck.spec.ts에서
178
+ // 동일한 코드 경로를 통해 검증되므로 중복 삭제함
179
+
180
+ //#endregion
@@ -0,0 +1,7 @@
1
+ # replaceDeps catch-all 에러 필터링 -- LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] catch 블록이 에러 코드를 확인하여 MODULE_NOT_FOUND/ERR_MODULE_NOT_FOUND만 무시: `sd-cli.ts:37-41` 확인 완료. `code` 프로퍼티를 체크하고 해당 코드가 아닌 경우 `console.warn`으로 경고 출력
6
+ - [x] 예상 외 에러(SyntaxError 등)에 경고 로그 출력: `console.warn("[sd-cli] replaceDeps 사전 설정 실패:", ...)` 확인
7
+ - [x] Phase 2로 정상 진행: catch 후 코드 흐름이 Phase 2(line 44~)로 계속됨 확인
@@ -0,0 +1,10 @@
1
+ # lint.worker/lint-utils/check.ts import 경로 검증 -- LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] lint.worker.ts가 `../utils/lint-core`에서 import: `import { executeLint, type LintOptions, type LintResult } from "../utils/lint-core";` (line 2) 확인
6
+ - [x] lint-utils.ts가 `./lint-core`에서 import: `import type { LintOptions, LintResult } from "./lint-core";` (line 2) 확인
7
+ - [x] check.ts가 `../utils/lint-core`에서 import: `import { executeLint, type LintResult } from "../utils/lint-core";` (line 4) 확인
8
+ - [x] commands/lint.ts�� `../commands/lint` import가 없음: `import { executeLint, type LintOptions } from "../utils/lint-core";` (line 1)만 존재
9
+ - [x] lint-core.ts에 runLint가 없음: `executeLint`, `loadIgnorePatterns`, `LintOptions`, `LintResult`만 export
10
+ - [x] commands/lint.ts에 runLint만 존재: 13줄의 thin wrapper