@simplysm/sd-cli 13.0.68 → 13.0.70

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 (201) hide show
  1. package/README.md +10 -957
  2. package/dist/builders/BaseBuilder.d.ts +23 -23
  3. package/dist/builders/BaseBuilder.d.ts.map +1 -1
  4. package/dist/builders/BaseBuilder.js +15 -15
  5. package/dist/builders/DtsBuilder.d.ts +4 -4
  6. package/dist/builders/DtsBuilder.js +1 -1
  7. package/dist/builders/LibraryBuilder.d.ts +3 -3
  8. package/dist/builders/types.d.ts +10 -10
  9. package/dist/capacitor/capacitor.d.ts +36 -36
  10. package/dist/capacitor/capacitor.js +63 -63
  11. package/dist/capacitor/capacitor.js.map +1 -1
  12. package/dist/commands/add-client.d.ts +8 -8
  13. package/dist/commands/add-client.js +15 -15
  14. package/dist/commands/add-client.js.map +1 -1
  15. package/dist/commands/add-server.d.ts +9 -9
  16. package/dist/commands/add-server.js +13 -13
  17. package/dist/commands/add-server.js.map +1 -1
  18. package/dist/commands/build.d.ts +9 -9
  19. package/dist/commands/check.js +3 -3
  20. package/dist/commands/check.js.map +1 -1
  21. package/dist/commands/dev.d.ts +9 -9
  22. package/dist/commands/device.d.ts +9 -9
  23. package/dist/commands/device.d.ts.map +1 -1
  24. package/dist/commands/device.js +17 -17
  25. package/dist/commands/device.js.map +1 -1
  26. package/dist/commands/init.d.ts +6 -6
  27. package/dist/commands/init.js +12 -12
  28. package/dist/commands/init.js.map +1 -1
  29. package/dist/commands/lint.d.ts +23 -23
  30. package/dist/commands/lint.d.ts.map +1 -1
  31. package/dist/commands/lint.js +25 -25
  32. package/dist/commands/lint.js.map +1 -1
  33. package/dist/commands/publish.d.ts +13 -13
  34. package/dist/commands/publish.d.ts.map +1 -1
  35. package/dist/commands/publish.js +61 -61
  36. package/dist/commands/publish.js.map +1 -1
  37. package/dist/commands/replace-deps.d.ts +3 -3
  38. package/dist/commands/replace-deps.d.ts.map +1 -1
  39. package/dist/commands/replace-deps.js +1 -1
  40. package/dist/commands/replace-deps.js.map +1 -1
  41. package/dist/commands/typecheck.d.ts +20 -20
  42. package/dist/commands/typecheck.d.ts.map +1 -1
  43. package/dist/commands/typecheck.js +20 -20
  44. package/dist/commands/typecheck.js.map +1 -1
  45. package/dist/commands/watch.d.ts +7 -7
  46. package/dist/electron/electron.d.ts +27 -27
  47. package/dist/electron/electron.js +32 -32
  48. package/dist/electron/electron.js.map +1 -1
  49. package/dist/infra/ResultCollector.d.ts +9 -9
  50. package/dist/infra/ResultCollector.js +5 -5
  51. package/dist/infra/SignalHandler.d.ts +7 -7
  52. package/dist/infra/SignalHandler.js +4 -4
  53. package/dist/infra/WorkerManager.d.ts +14 -14
  54. package/dist/infra/WorkerManager.js +11 -11
  55. package/dist/orchestrators/BuildOrchestrator.d.ts +19 -19
  56. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  57. package/dist/orchestrators/BuildOrchestrator.js +26 -26
  58. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  59. package/dist/orchestrators/DevOrchestrator.d.ts +25 -25
  60. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  61. package/dist/orchestrators/DevOrchestrator.js +30 -30
  62. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  63. package/dist/orchestrators/WatchOrchestrator.d.ts +13 -13
  64. package/dist/orchestrators/WatchOrchestrator.js +17 -17
  65. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  66. package/dist/sd-cli-entry.d.ts +2 -2
  67. package/dist/sd-cli-entry.js +38 -38
  68. package/dist/sd-cli-entry.js.map +1 -1
  69. package/dist/sd-cli.d.ts +2 -2
  70. package/dist/sd-cli.js +1 -1
  71. package/dist/sd-cli.js.map +1 -1
  72. package/dist/sd-config.types.d.ts +84 -84
  73. package/dist/sd-config.types.d.ts.map +1 -1
  74. package/dist/utils/build-env.d.ts +1 -1
  75. package/dist/utils/config-editor.d.ts +5 -5
  76. package/dist/utils/config-editor.js +2 -2
  77. package/dist/utils/config-editor.js.map +1 -1
  78. package/dist/utils/copy-public.d.ts +9 -9
  79. package/dist/utils/copy-src.d.ts +9 -9
  80. package/dist/utils/esbuild-config.d.ts +30 -30
  81. package/dist/utils/esbuild-config.d.ts.map +1 -1
  82. package/dist/utils/output-utils.d.ts +6 -6
  83. package/dist/utils/package-utils.d.ts +6 -6
  84. package/dist/utils/package-utils.js +1 -1
  85. package/dist/utils/package-utils.js.map +1 -1
  86. package/dist/utils/rebuild-manager.js +3 -3
  87. package/dist/utils/rebuild-manager.js.map +1 -1
  88. package/dist/utils/replace-deps.d.ts +25 -25
  89. package/dist/utils/replace-deps.js +3 -3
  90. package/dist/utils/replace-deps.js.map +1 -1
  91. package/dist/utils/sd-config.d.ts +3 -3
  92. package/dist/utils/sd-config.js +3 -3
  93. package/dist/utils/sd-config.js.map +1 -1
  94. package/dist/utils/tailwind-config-deps.d.ts +3 -3
  95. package/dist/utils/template.d.ts +8 -8
  96. package/dist/utils/tsconfig.d.ts +16 -16
  97. package/dist/utils/tsconfig.js +2 -2
  98. package/dist/utils/tsconfig.js.map +1 -1
  99. package/dist/utils/typecheck-serialization.d.ts +8 -8
  100. package/dist/utils/vite-config.d.ts +8 -8
  101. package/dist/utils/vite-config.d.ts.map +1 -1
  102. package/dist/utils/vite-config.js +3 -3
  103. package/dist/utils/worker-events.d.ts +12 -12
  104. package/dist/utils/worker-events.d.ts.map +1 -1
  105. package/dist/utils/worker-utils.d.ts +3 -3
  106. package/dist/utils/worker-utils.js +2 -2
  107. package/dist/utils/worker-utils.js.map +1 -1
  108. package/dist/workers/client.worker.d.ts +14 -14
  109. package/dist/workers/client.worker.d.ts.map +1 -1
  110. package/dist/workers/client.worker.js +1 -1
  111. package/dist/workers/client.worker.js.map +1 -1
  112. package/dist/workers/dts.worker.d.ts +13 -13
  113. package/dist/workers/dts.worker.d.ts.map +1 -1
  114. package/dist/workers/dts.worker.js +3 -3
  115. package/dist/workers/dts.worker.js.map +1 -1
  116. package/dist/workers/library.worker.d.ts +12 -12
  117. package/dist/workers/library.worker.js +1 -1
  118. package/dist/workers/library.worker.js.map +1 -1
  119. package/dist/workers/lint.worker.d.ts +1 -1
  120. package/dist/workers/server-runtime.worker.d.ts +6 -6
  121. package/dist/workers/server-runtime.worker.js +6 -6
  122. package/dist/workers/server-runtime.worker.js.map +1 -1
  123. package/dist/workers/server.worker.d.ts +20 -20
  124. package/dist/workers/server.worker.d.ts.map +1 -1
  125. package/dist/workers/server.worker.js +6 -6
  126. package/dist/workers/server.worker.js.map +1 -1
  127. package/package.json +8 -7
  128. package/src/builders/BaseBuilder.ts +33 -33
  129. package/src/builders/DtsBuilder.ts +5 -5
  130. package/src/builders/LibraryBuilder.ts +9 -9
  131. package/src/builders/types.ts +10 -10
  132. package/src/capacitor/capacitor.ts +119 -119
  133. package/src/commands/add-client.ts +31 -31
  134. package/src/commands/add-server.ts +34 -34
  135. package/src/commands/build.ts +9 -9
  136. package/src/commands/check.ts +5 -5
  137. package/src/commands/dev.ts +9 -9
  138. package/src/commands/device.ts +30 -30
  139. package/src/commands/init.ts +25 -25
  140. package/src/commands/lint.ts +64 -64
  141. package/src/commands/publish.ts +139 -139
  142. package/src/commands/replace-deps.ts +4 -4
  143. package/src/commands/typecheck.ts +74 -74
  144. package/src/commands/watch.ts +7 -7
  145. package/src/electron/electron.ts +51 -51
  146. package/src/infra/ResultCollector.ts +9 -9
  147. package/src/infra/SignalHandler.ts +7 -7
  148. package/src/infra/WorkerManager.ts +14 -14
  149. package/src/orchestrators/BuildOrchestrator.ts +76 -76
  150. package/src/orchestrators/DevOrchestrator.ts +88 -88
  151. package/src/orchestrators/WatchOrchestrator.ts +39 -39
  152. package/src/sd-cli-entry.ts +43 -43
  153. package/src/sd-cli.ts +15 -15
  154. package/src/sd-config.types.ts +85 -85
  155. package/src/utils/build-env.ts +1 -1
  156. package/src/utils/config-editor.ts +19 -19
  157. package/src/utils/copy-public.ts +17 -17
  158. package/src/utils/copy-src.ts +11 -11
  159. package/src/utils/esbuild-config.ts +33 -33
  160. package/src/utils/output-utils.ts +11 -11
  161. package/src/utils/package-utils.ts +12 -12
  162. package/src/utils/rebuild-manager.ts +3 -3
  163. package/src/utils/replace-deps.ts +361 -361
  164. package/src/utils/sd-config.ts +44 -44
  165. package/src/utils/tailwind-config-deps.ts +98 -98
  166. package/src/utils/template.ts +56 -56
  167. package/src/utils/tsconfig.ts +127 -127
  168. package/src/utils/typecheck-serialization.ts +86 -86
  169. package/src/utils/vite-config.ts +341 -341
  170. package/src/utils/worker-events.ts +16 -16
  171. package/src/utils/worker-utils.ts +45 -45
  172. package/src/workers/client.worker.ts +34 -34
  173. package/src/workers/dts.worker.ts +467 -467
  174. package/src/workers/library.worker.ts +314 -314
  175. package/src/workers/lint.worker.ts +16 -16
  176. package/src/workers/server-runtime.worker.ts +157 -157
  177. package/src/workers/server.worker.ts +572 -572
  178. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  179. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  180. package/templates/init/package.json.hbs +3 -3
  181. package/tests/config-editor.spec.ts +160 -0
  182. package/tests/copy-src.spec.ts +50 -0
  183. package/tests/get-compiler-options-for-package.spec.ts +139 -0
  184. package/tests/get-package-source-files.spec.ts +181 -0
  185. package/tests/get-types-from-package-json.spec.ts +107 -0
  186. package/tests/infra/ResultCollector.spec.ts +39 -0
  187. package/tests/infra/SignalHandler.spec.ts +38 -0
  188. package/tests/infra/WorkerManager.spec.ts +97 -0
  189. package/tests/load-ignore-patterns.spec.ts +188 -0
  190. package/tests/load-sd-config.spec.ts +137 -0
  191. package/tests/package-utils.spec.ts +188 -0
  192. package/tests/parse-root-tsconfig.spec.ts +89 -0
  193. package/tests/replace-deps.spec.ts +308 -0
  194. package/tests/run-lint.spec.ts +415 -0
  195. package/tests/run-typecheck.spec.ts +653 -0
  196. package/tests/run-watch.spec.ts +75 -0
  197. package/tests/sd-cli.spec.ts +330 -0
  198. package/tests/tailwind-config-deps.spec.ts +30 -0
  199. package/tests/template.spec.ts +70 -0
  200. package/tests/utils/rebuild-manager.spec.ts +43 -0
  201. package/tests/write-changed-output-files.spec.ts +97 -0
@@ -0,0 +1,415 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
+ import path from "path";
3
+
4
+ // State management hoisted via vi.hoisted
5
+ const { mockState, mockJitiImportFn } = vi.hoisted(() => ({
6
+ mockState: {
7
+ lintResults: [] as Array<{ errorCount: number; warningCount: number }>,
8
+ lintedFiles: [] as string[],
9
+ outputFixesCalled: false,
10
+ },
11
+ mockJitiImportFn: vi.fn(),
12
+ }));
13
+
14
+ // 외부 의존성 모킹
15
+
16
+ vi.mock("eslint", () => {
17
+ class MockESLint {
18
+ lintFiles(files: string[]) {
19
+ mockState.lintedFiles = files;
20
+ return Promise.resolve(mockState.lintResults);
21
+ }
22
+ loadFormatter() {
23
+ return Promise.resolve({
24
+ format: () => Promise.resolve(""),
25
+ });
26
+ }
27
+ static outputFixes(_results: unknown) {
28
+ mockState.outputFixesCalled = true;
29
+ }
30
+ }
31
+
32
+ return { ESLint: MockESLint };
33
+ });
34
+
35
+ vi.mock("@simplysm/core-node", () => {
36
+ const posix = (p: string) => p.replace(/\\/g, "/");
37
+ const isChildPath = (child: string, parent: string) => {
38
+ if (child === parent) return false;
39
+ const parentWithSlash = parent.endsWith("/") ? parent : parent + "/";
40
+ return child.startsWith(parentWithSlash);
41
+ };
42
+ return {
43
+ fsExists: vi.fn(),
44
+ fsGlob: vi.fn(),
45
+ pathPosix: vi.fn(posix),
46
+ pathIsChildPath: vi.fn(isChildPath),
47
+ pathFilterByTargets: vi.fn((files: string[], targets: string[], cwd: string) => {
48
+ if (targets.length === 0) return files;
49
+ return files.filter((file) => {
50
+ const relativePath = posix(file.replace(cwd + "/", ""));
51
+ return targets.some(
52
+ (target) => relativePath === target || isChildPath(relativePath, target),
53
+ );
54
+ });
55
+ }),
56
+ };
57
+ });
58
+
59
+ vi.mock("jiti", () => ({
60
+ createJiti: vi.fn(() => ({
61
+ import: (configPath: string) => mockJitiImportFn(configPath),
62
+ })),
63
+ }));
64
+
65
+ vi.mock("consola", () => {
66
+ const mockLogger = {
67
+ debug: vi.fn(),
68
+ info: vi.fn(),
69
+ error: vi.fn(),
70
+ warn: vi.fn(),
71
+ start: vi.fn(),
72
+ success: vi.fn(),
73
+ fail: vi.fn(),
74
+ withTag: vi.fn(() => mockLogger),
75
+ level: 3, // info level
76
+ };
77
+ return {
78
+ consola: mockLogger,
79
+ default: mockLogger,
80
+ LogLevels: { debug: 4, info: 3, warn: 2, error: 1 },
81
+ };
82
+ });
83
+
84
+ import { fsExists, fsGlob } from "@simplysm/core-node";
85
+ import { runLint, executeLint } from "../src/commands/lint";
86
+
87
+ describe("runLint", () => {
88
+ let originalExitCode: typeof process.exitCode;
89
+ let originalCwd: () => string;
90
+
91
+ beforeEach(() => {
92
+ vi.clearAllMocks();
93
+ originalExitCode = process.exitCode;
94
+ originalCwd = process.cwd;
95
+ process.cwd = vi.fn().mockReturnValue("/project");
96
+
97
+ // 상태 초기화
98
+ mockState.lintResults = [];
99
+ mockState.lintedFiles = [];
100
+ mockState.outputFixesCalled = false;
101
+ });
102
+
103
+ afterEach(() => {
104
+ vi.restoreAllMocks();
105
+ process.exitCode = originalExitCode;
106
+ process.cwd = originalCwd;
107
+ });
108
+
109
+ it("sets exitCode to 1 when lint errors occur", async () => {
110
+ const cwd = "/project";
111
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
112
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
113
+ });
114
+
115
+ mockJitiImportFn.mockResolvedValue({
116
+ default: [{ ignores: ["node_modules/**"] }],
117
+ });
118
+
119
+ vi.mocked(fsGlob).mockResolvedValue(["/project/src/index.ts"]);
120
+
121
+ mockState.lintResults = [{ errorCount: 2, warningCount: 0 }];
122
+
123
+ await runLint({ targets: [], fix: false, timing: false });
124
+
125
+ expect(process.exitCode).toBe(1);
126
+ });
127
+
128
+ it("does not set exitCode when no lint errors", async () => {
129
+ const cwd = "/project";
130
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
131
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
132
+ });
133
+
134
+ mockJitiImportFn.mockResolvedValue({
135
+ default: [{ ignores: ["node_modules/**"] }],
136
+ });
137
+
138
+ vi.mocked(fsGlob).mockResolvedValue(["/project/src/index.ts"]);
139
+
140
+ mockState.lintResults = [{ errorCount: 0, warningCount: 0 }];
141
+
142
+ await runLint({ targets: [], fix: false, timing: false });
143
+
144
+ expect(process.exitCode).toBeUndefined();
145
+ });
146
+
147
+ it("filters files using targets option", async () => {
148
+ const cwd = "/project";
149
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
150
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
151
+ });
152
+
153
+ mockJitiImportFn.mockResolvedValue({
154
+ default: [{ ignores: ["node_modules/**"] }],
155
+ });
156
+
157
+ vi.mocked(fsGlob).mockResolvedValue([
158
+ "/project/packages/core-common/src/index.ts",
159
+ "/project/packages/core-node/src/index.ts",
160
+ "/project/packages/cli/src/index.ts",
161
+ ]);
162
+
163
+ mockState.lintResults = [{ errorCount: 0, warningCount: 0 }];
164
+
165
+ await runLint({
166
+ targets: ["packages/core-common"],
167
+ fix: false,
168
+ timing: false,
169
+ });
170
+
171
+ expect(mockState.lintedFiles).toHaveLength(1);
172
+ expect(mockState.lintedFiles[0]).toContain("core-common");
173
+ });
174
+
175
+ it("filters files from all paths when multiple targets specified", async () => {
176
+ const cwd = "/project";
177
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
178
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
179
+ });
180
+
181
+ mockJitiImportFn.mockResolvedValue({
182
+ default: [{ ignores: ["node_modules/**"] }],
183
+ });
184
+
185
+ vi.mocked(fsGlob).mockResolvedValue([
186
+ "/project/packages/core-common/src/index.ts",
187
+ "/project/packages/core-node/src/index.ts",
188
+ "/project/packages/cli/src/index.ts",
189
+ "/project/tests/orm/src/test.spec.ts",
190
+ ]);
191
+
192
+ mockState.lintResults = [{ errorCount: 0, warningCount: 0 }];
193
+
194
+ await runLint({
195
+ targets: ["packages/core-common", "packages/cli"],
196
+ fix: false,
197
+ timing: false,
198
+ });
199
+
200
+ expect(mockState.lintedFiles).toHaveLength(2);
201
+ expect(mockState.lintedFiles.some((f) => f.includes("core-common"))).toBe(true);
202
+ expect(mockState.lintedFiles.some((f) => f.includes("cli"))).toBe(true);
203
+ expect(mockState.lintedFiles.some((f) => f.includes("core-node"))).toBe(false);
204
+ expect(mockState.lintedFiles.some((f) => f.includes("tests/orm"))).toBe(false);
205
+ });
206
+
207
+ it("calls ESLint.outputFixes when fix option is enabled", async () => {
208
+ const cwd = "/project";
209
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
210
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
211
+ });
212
+
213
+ mockJitiImportFn.mockResolvedValue({
214
+ default: [{ ignores: ["node_modules/**"] }],
215
+ });
216
+
217
+ vi.mocked(fsGlob).mockResolvedValue(["/project/src/index.ts"]);
218
+
219
+ mockState.lintResults = [{ errorCount: 0, warningCount: 0 }];
220
+
221
+ await runLint({ targets: [], fix: true, timing: false });
222
+
223
+ expect(mockState.outputFixesCalled).toBe(true);
224
+ });
225
+
226
+ it("exits early when no files to lint", async () => {
227
+ const cwd = "/project";
228
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
229
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
230
+ });
231
+
232
+ mockJitiImportFn.mockResolvedValue({
233
+ default: [{ ignores: ["node_modules/**"] }],
234
+ });
235
+
236
+ vi.mocked(fsGlob).mockResolvedValue([]);
237
+
238
+ await runLint({ targets: [], fix: false, timing: false });
239
+
240
+ // ESLint가 호출되지 않으므로 lintedFiles는 빈 배열 유지
241
+ expect(mockState.lintedFiles).toHaveLength(0);
242
+ expect(process.exitCode).toBeUndefined();
243
+ });
244
+
245
+ it("throws error when ESLint config file is not found", async () => {
246
+ // When all config files are missing
247
+ vi.mocked(fsExists).mockResolvedValue(false);
248
+
249
+ await expect(runLint({ targets: [], fix: false, timing: false })).rejects.toThrow(
250
+ "Cannot find ESLint config file",
251
+ );
252
+
253
+ // Since error is thrown, exitCode must be set by caller (not set inside runLint)
254
+ // ESLint is not called
255
+ expect(mockState.lintedFiles).toHaveLength(0);
256
+ });
257
+
258
+ it("does not set exitCode when only warnings exist", async () => {
259
+ const cwd = "/project";
260
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
261
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
262
+ });
263
+
264
+ mockJitiImportFn.mockResolvedValue({
265
+ default: [{ ignores: ["node_modules/**"] }],
266
+ });
267
+
268
+ vi.mocked(fsGlob).mockResolvedValue(["/project/src/index.ts"]);
269
+
270
+ mockState.lintResults = [{ errorCount: 0, warningCount: 3 }];
271
+
272
+ await runLint({ targets: [], fix: false, timing: false });
273
+
274
+ expect(process.exitCode).toBeUndefined();
275
+ });
276
+
277
+ it("sets TIMING environment variable when timing option is enabled", async () => {
278
+ const cwd = "/project";
279
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
280
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
281
+ });
282
+
283
+ mockJitiImportFn.mockResolvedValue({
284
+ default: [{ ignores: ["node_modules/**"] }],
285
+ });
286
+
287
+ vi.mocked(fsGlob).mockResolvedValue(["/project/src/index.ts"]);
288
+
289
+ mockState.lintResults = [{ errorCount: 0, warningCount: 0 }];
290
+
291
+ const originalTiming = process.env["TIMING"];
292
+ delete process.env["TIMING"];
293
+
294
+ await runLint({ targets: [], fix: false, timing: true });
295
+
296
+ // TIMING이 설정되었는지 확인 (함수 내에서 설정됨)
297
+ expect(process.env["TIMING"]).toBe("1");
298
+
299
+ // cleanup
300
+ if (originalTiming !== undefined) {
301
+ process.env["TIMING"] = originalTiming;
302
+ } else {
303
+ delete process.env["TIMING"];
304
+ }
305
+ });
306
+
307
+ it("uses eslint.config.mts when eslint.config.ts is not found", async () => {
308
+ const cwd = "/project";
309
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
310
+ // eslint.config.ts does not exist, only eslint.config.mts
311
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.mts"));
312
+ });
313
+
314
+ mockJitiImportFn.mockResolvedValue({
315
+ default: [{ ignores: ["dist/**"] }],
316
+ });
317
+
318
+ vi.mocked(fsGlob).mockResolvedValue(["/project/src/index.ts"]);
319
+
320
+ mockState.lintResults = [{ errorCount: 0, warningCount: 0 }];
321
+
322
+ await runLint({ targets: [], fix: false, timing: false });
323
+
324
+ // Verify that mts file was loaded
325
+ expect(mockJitiImportFn).toHaveBeenCalledWith(expect.stringContaining("eslint.config.mts"));
326
+ expect(process.exitCode).toBeUndefined();
327
+ });
328
+
329
+ it("propagates error when glob fails", async () => {
330
+ const cwd = "/project";
331
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
332
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
333
+ });
334
+
335
+ mockJitiImportFn.mockResolvedValue({
336
+ default: [{ ignores: ["node_modules/**"] }],
337
+ });
338
+
339
+ vi.mocked(fsGlob).mockRejectedValue(new Error("Glob error"));
340
+
341
+ await expect(runLint({ targets: [], fix: false, timing: false })).rejects.toThrow("Glob error");
342
+ });
343
+ });
344
+
345
+ describe("executeLint", () => {
346
+ let originalCwd: () => string;
347
+
348
+ beforeEach(() => {
349
+ vi.clearAllMocks();
350
+ originalCwd = process.cwd;
351
+ process.cwd = vi.fn().mockReturnValue("/project");
352
+
353
+ // 상태 초기화
354
+ mockState.lintResults = [];
355
+ mockState.lintedFiles = [];
356
+ mockState.outputFixesCalled = false;
357
+
358
+ // 기본 ESLint 설정 mock
359
+ const cwd = "/project";
360
+ vi.mocked(fsExists).mockImplementation((filePath: string) => {
361
+ return Promise.resolve(filePath === path.join(cwd, "eslint.config.ts"));
362
+ });
363
+ mockJitiImportFn.mockResolvedValue({
364
+ default: [{ ignores: ["node_modules/**"] }],
365
+ });
366
+ });
367
+
368
+ afterEach(() => {
369
+ vi.restoreAllMocks();
370
+ process.cwd = originalCwd;
371
+ });
372
+
373
+ it("returns success result when no errors", async () => {
374
+ mockState.lintResults = [{ errorCount: 0, warningCount: 0 }];
375
+ vi.mocked(fsGlob).mockResolvedValue(["/project/packages/core-common/src/index.ts"]);
376
+
377
+ const result = await executeLint({ targets: [], fix: false, timing: false });
378
+
379
+ expect(result.success).toBe(true);
380
+ expect(result.errorCount).toBe(0);
381
+ expect(result.warningCount).toBe(0);
382
+ });
383
+
384
+ it("returns failure result when errors exist", async () => {
385
+ mockState.lintResults = [{ errorCount: 2, warningCount: 1 }];
386
+ vi.mocked(fsGlob).mockResolvedValue(["/project/packages/core-common/src/index.ts"]);
387
+
388
+ const result = await executeLint({ targets: [], fix: false, timing: false });
389
+
390
+ expect(result.success).toBe(false);
391
+ expect(result.errorCount).toBe(2);
392
+ expect(result.warningCount).toBe(1);
393
+ });
394
+
395
+ it("includes formatter output in formattedOutput", async () => {
396
+ mockState.lintResults = [{ errorCount: 1, warningCount: 0 }];
397
+ vi.mocked(fsGlob).mockResolvedValue(["/project/packages/core-common/src/index.ts"]);
398
+
399
+ const result = await executeLint({ targets: [], fix: false, timing: false });
400
+
401
+ // MockESLint's formatter returns empty string, so formattedOutput is also empty string
402
+ expect(result.formattedOutput).toBeDefined();
403
+ expect(typeof result.formattedOutput).toBe("string");
404
+ });
405
+
406
+ it("returns success result when no files exist", async () => {
407
+ vi.mocked(fsGlob).mockResolvedValue([]);
408
+
409
+ const result = await executeLint({ targets: [], fix: false, timing: false });
410
+
411
+ expect(result.success).toBe(true);
412
+ expect(result.errorCount).toBe(0);
413
+ expect(result.warningCount).toBe(0);
414
+ });
415
+ });