modestbench 0.0.1

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 (275) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE.md +55 -0
  3. package/README.md +699 -0
  4. package/dist/bootstrap.cjs +37 -0
  5. package/dist/bootstrap.cjs.map +1 -0
  6. package/dist/bootstrap.d.cts +17 -0
  7. package/dist/bootstrap.d.cts.map +1 -0
  8. package/dist/bootstrap.d.ts +17 -0
  9. package/dist/bootstrap.d.ts.map +1 -0
  10. package/dist/bootstrap.js +33 -0
  11. package/dist/bootstrap.js.map +1 -0
  12. package/dist/cli/commands/history.cjs +459 -0
  13. package/dist/cli/commands/history.cjs.map +1 -0
  14. package/dist/cli/commands/history.d.cts +34 -0
  15. package/dist/cli/commands/history.d.cts.map +1 -0
  16. package/dist/cli/commands/history.d.ts +34 -0
  17. package/dist/cli/commands/history.d.ts.map +1 -0
  18. package/dist/cli/commands/history.js +422 -0
  19. package/dist/cli/commands/history.js.map +1 -0
  20. package/dist/cli/commands/init.cjs +566 -0
  21. package/dist/cli/commands/init.cjs.map +1 -0
  22. package/dist/cli/commands/init.d.cts +26 -0
  23. package/dist/cli/commands/init.d.cts.map +1 -0
  24. package/dist/cli/commands/init.d.ts +26 -0
  25. package/dist/cli/commands/init.d.ts.map +1 -0
  26. package/dist/cli/commands/init.js +562 -0
  27. package/dist/cli/commands/init.js.map +1 -0
  28. package/dist/cli/commands/run.cjs +285 -0
  29. package/dist/cli/commands/run.cjs.map +1 -0
  30. package/dist/cli/commands/run.d.cts +37 -0
  31. package/dist/cli/commands/run.d.cts.map +1 -0
  32. package/dist/cli/commands/run.d.ts +37 -0
  33. package/dist/cli/commands/run.d.ts.map +1 -0
  34. package/dist/cli/commands/run.js +248 -0
  35. package/dist/cli/commands/run.js.map +1 -0
  36. package/dist/cli/index.cjs +523 -0
  37. package/dist/cli/index.cjs.map +1 -0
  38. package/dist/cli/index.d.cts +58 -0
  39. package/dist/cli/index.d.cts.map +1 -0
  40. package/dist/cli/index.d.ts +58 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +515 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/config/manager.cjs +370 -0
  45. package/dist/config/manager.cjs.map +1 -0
  46. package/dist/config/manager.d.cts +46 -0
  47. package/dist/config/manager.d.cts.map +1 -0
  48. package/dist/config/manager.d.ts +46 -0
  49. package/dist/config/manager.d.ts.map +1 -0
  50. package/dist/config/manager.js +333 -0
  51. package/dist/config/manager.js.map +1 -0
  52. package/dist/config/schema.cjs +182 -0
  53. package/dist/config/schema.cjs.map +1 -0
  54. package/dist/config/schema.d.cts +51 -0
  55. package/dist/config/schema.d.cts.map +1 -0
  56. package/dist/config/schema.d.ts +51 -0
  57. package/dist/config/schema.d.ts.map +1 -0
  58. package/dist/config/schema.js +145 -0
  59. package/dist/config/schema.js.map +1 -0
  60. package/dist/constants.cjs +22 -0
  61. package/dist/constants.cjs.map +1 -0
  62. package/dist/constants.d.cts +10 -0
  63. package/dist/constants.d.cts.map +1 -0
  64. package/dist/constants.d.ts +10 -0
  65. package/dist/constants.d.ts.map +1 -0
  66. package/dist/constants.js +19 -0
  67. package/dist/constants.js.map +1 -0
  68. package/dist/core/benchmark-schema.cjs +135 -0
  69. package/dist/core/benchmark-schema.cjs.map +1 -0
  70. package/dist/core/benchmark-schema.d.cts +139 -0
  71. package/dist/core/benchmark-schema.d.cts.map +1 -0
  72. package/dist/core/benchmark-schema.d.ts +139 -0
  73. package/dist/core/benchmark-schema.d.ts.map +1 -0
  74. package/dist/core/benchmark-schema.js +132 -0
  75. package/dist/core/benchmark-schema.js.map +1 -0
  76. package/dist/core/engine.cjs +669 -0
  77. package/dist/core/engine.cjs.map +1 -0
  78. package/dist/core/engine.d.cts +128 -0
  79. package/dist/core/engine.d.cts.map +1 -0
  80. package/dist/core/engine.d.ts +128 -0
  81. package/dist/core/engine.d.ts.map +1 -0
  82. package/dist/core/engine.js +632 -0
  83. package/dist/core/engine.js.map +1 -0
  84. package/dist/core/engines/accurate-engine.cjs +292 -0
  85. package/dist/core/engines/accurate-engine.cjs.map +1 -0
  86. package/dist/core/engines/accurate-engine.d.cts +63 -0
  87. package/dist/core/engines/accurate-engine.d.cts.map +1 -0
  88. package/dist/core/engines/accurate-engine.d.ts +63 -0
  89. package/dist/core/engines/accurate-engine.d.ts.map +1 -0
  90. package/dist/core/engines/accurate-engine.js +288 -0
  91. package/dist/core/engines/accurate-engine.js.map +1 -0
  92. package/dist/core/engines/index.cjs +21 -0
  93. package/dist/core/engines/index.cjs.map +1 -0
  94. package/dist/core/engines/index.d.cts +16 -0
  95. package/dist/core/engines/index.d.cts.map +1 -0
  96. package/dist/core/engines/index.d.ts +16 -0
  97. package/dist/core/engines/index.d.ts.map +1 -0
  98. package/dist/core/engines/index.js +16 -0
  99. package/dist/core/engines/index.js.map +1 -0
  100. package/dist/core/engines/tinybench-engine.cjs +286 -0
  101. package/dist/core/engines/tinybench-engine.cjs.map +1 -0
  102. package/dist/core/engines/tinybench-engine.d.cts +18 -0
  103. package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
  104. package/dist/core/engines/tinybench-engine.d.ts +18 -0
  105. package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
  106. package/dist/core/engines/tinybench-engine.js +282 -0
  107. package/dist/core/engines/tinybench-engine.js.map +1 -0
  108. package/dist/core/error-manager.cjs +303 -0
  109. package/dist/core/error-manager.cjs.map +1 -0
  110. package/dist/core/error-manager.d.cts +77 -0
  111. package/dist/core/error-manager.d.cts.map +1 -0
  112. package/dist/core/error-manager.d.ts +77 -0
  113. package/dist/core/error-manager.d.ts.map +1 -0
  114. package/dist/core/error-manager.js +299 -0
  115. package/dist/core/error-manager.js.map +1 -0
  116. package/dist/core/loader.cjs +287 -0
  117. package/dist/core/loader.cjs.map +1 -0
  118. package/dist/core/loader.d.cts +55 -0
  119. package/dist/core/loader.d.cts.map +1 -0
  120. package/dist/core/loader.d.ts +55 -0
  121. package/dist/core/loader.d.ts.map +1 -0
  122. package/dist/core/loader.js +250 -0
  123. package/dist/core/loader.js.map +1 -0
  124. package/dist/core/stats-utils.cjs +99 -0
  125. package/dist/core/stats-utils.cjs.map +1 -0
  126. package/dist/core/stats-utils.d.cts +50 -0
  127. package/dist/core/stats-utils.d.cts.map +1 -0
  128. package/dist/core/stats-utils.d.ts +50 -0
  129. package/dist/core/stats-utils.d.ts.map +1 -0
  130. package/dist/core/stats-utils.js +94 -0
  131. package/dist/core/stats-utils.js.map +1 -0
  132. package/dist/index.cjs +64 -0
  133. package/dist/index.cjs.map +1 -0
  134. package/dist/index.d.cts +22 -0
  135. package/dist/index.d.cts.map +1 -0
  136. package/dist/index.d.ts +22 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +30 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/progress/manager.cjs +325 -0
  141. package/dist/progress/manager.cjs.map +1 -0
  142. package/dist/progress/manager.d.cts +125 -0
  143. package/dist/progress/manager.d.cts.map +1 -0
  144. package/dist/progress/manager.d.ts +125 -0
  145. package/dist/progress/manager.d.ts.map +1 -0
  146. package/dist/progress/manager.js +321 -0
  147. package/dist/progress/manager.js.map +1 -0
  148. package/dist/reporters/csv.cjs +250 -0
  149. package/dist/reporters/csv.cjs.map +1 -0
  150. package/dist/reporters/csv.d.cts +92 -0
  151. package/dist/reporters/csv.d.cts.map +1 -0
  152. package/dist/reporters/csv.d.ts +92 -0
  153. package/dist/reporters/csv.d.ts.map +1 -0
  154. package/dist/reporters/csv.js +246 -0
  155. package/dist/reporters/csv.js.map +1 -0
  156. package/dist/reporters/human.cjs +516 -0
  157. package/dist/reporters/human.cjs.map +1 -0
  158. package/dist/reporters/human.d.cts +86 -0
  159. package/dist/reporters/human.d.cts.map +1 -0
  160. package/dist/reporters/human.d.ts +86 -0
  161. package/dist/reporters/human.d.ts.map +1 -0
  162. package/dist/reporters/human.js +509 -0
  163. package/dist/reporters/human.js.map +1 -0
  164. package/dist/reporters/index.cjs +17 -0
  165. package/dist/reporters/index.cjs.map +1 -0
  166. package/dist/reporters/index.d.cts +10 -0
  167. package/dist/reporters/index.d.cts.map +1 -0
  168. package/dist/reporters/index.d.ts +10 -0
  169. package/dist/reporters/index.d.ts.map +1 -0
  170. package/dist/reporters/index.js +10 -0
  171. package/dist/reporters/index.js.map +1 -0
  172. package/dist/reporters/json.cjs +215 -0
  173. package/dist/reporters/json.cjs.map +1 -0
  174. package/dist/reporters/json.d.cts +79 -0
  175. package/dist/reporters/json.d.cts.map +1 -0
  176. package/dist/reporters/json.d.ts +79 -0
  177. package/dist/reporters/json.d.ts.map +1 -0
  178. package/dist/reporters/json.js +211 -0
  179. package/dist/reporters/json.js.map +1 -0
  180. package/dist/reporters/registry.cjs +255 -0
  181. package/dist/reporters/registry.cjs.map +1 -0
  182. package/dist/reporters/registry.d.cts +155 -0
  183. package/dist/reporters/registry.d.cts.map +1 -0
  184. package/dist/reporters/registry.d.ts +155 -0
  185. package/dist/reporters/registry.d.ts.map +1 -0
  186. package/dist/reporters/registry.js +249 -0
  187. package/dist/reporters/registry.js.map +1 -0
  188. package/dist/reporters/simple.cjs +328 -0
  189. package/dist/reporters/simple.cjs.map +1 -0
  190. package/dist/reporters/simple.d.cts +51 -0
  191. package/dist/reporters/simple.d.cts.map +1 -0
  192. package/dist/reporters/simple.d.ts +51 -0
  193. package/dist/reporters/simple.d.ts.map +1 -0
  194. package/dist/reporters/simple.js +321 -0
  195. package/dist/reporters/simple.js.map +1 -0
  196. package/dist/schema/modestbench-config.schema.json +162 -0
  197. package/dist/storage/history.cjs +456 -0
  198. package/dist/storage/history.cjs.map +1 -0
  199. package/dist/storage/history.d.cts +99 -0
  200. package/dist/storage/history.d.cts.map +1 -0
  201. package/dist/storage/history.d.ts +99 -0
  202. package/dist/storage/history.d.ts.map +1 -0
  203. package/dist/storage/history.js +452 -0
  204. package/dist/storage/history.js.map +1 -0
  205. package/dist/types/cli.cjs +21 -0
  206. package/dist/types/cli.cjs.map +1 -0
  207. package/dist/types/cli.d.cts +296 -0
  208. package/dist/types/cli.d.cts.map +1 -0
  209. package/dist/types/cli.d.ts +296 -0
  210. package/dist/types/cli.d.ts.map +1 -0
  211. package/dist/types/cli.js +18 -0
  212. package/dist/types/cli.js.map +1 -0
  213. package/dist/types/core.cjs +14 -0
  214. package/dist/types/core.cjs.map +1 -0
  215. package/dist/types/core.d.cts +380 -0
  216. package/dist/types/core.d.cts.map +1 -0
  217. package/dist/types/core.d.ts +380 -0
  218. package/dist/types/core.d.ts.map +1 -0
  219. package/dist/types/core.js +13 -0
  220. package/dist/types/core.js.map +1 -0
  221. package/dist/types/index.cjs +27 -0
  222. package/dist/types/index.cjs.map +1 -0
  223. package/dist/types/index.d.cts +11 -0
  224. package/dist/types/index.d.cts.map +1 -0
  225. package/dist/types/index.d.ts +11 -0
  226. package/dist/types/index.d.ts.map +1 -0
  227. package/dist/types/index.js +11 -0
  228. package/dist/types/index.js.map +1 -0
  229. package/dist/types/interfaces.cjs +10 -0
  230. package/dist/types/interfaces.cjs.map +1 -0
  231. package/dist/types/interfaces.d.cts +381 -0
  232. package/dist/types/interfaces.d.cts.map +1 -0
  233. package/dist/types/interfaces.d.ts +381 -0
  234. package/dist/types/interfaces.d.ts.map +1 -0
  235. package/dist/types/interfaces.js +9 -0
  236. package/dist/types/interfaces.js.map +1 -0
  237. package/dist/types/utility.cjs +92 -0
  238. package/dist/types/utility.cjs.map +1 -0
  239. package/dist/types/utility.d.cts +330 -0
  240. package/dist/types/utility.d.cts.map +1 -0
  241. package/dist/types/utility.d.ts +330 -0
  242. package/dist/types/utility.d.ts.map +1 -0
  243. package/dist/types/utility.js +78 -0
  244. package/dist/types/utility.js.map +1 -0
  245. package/package.json +211 -0
  246. package/src/bootstrap.ts +35 -0
  247. package/src/cli/commands/history.ts +569 -0
  248. package/src/cli/commands/init.ts +658 -0
  249. package/src/cli/commands/run.ts +346 -0
  250. package/src/cli/index.ts +642 -0
  251. package/src/config/manager.ts +387 -0
  252. package/src/config/schema.ts +188 -0
  253. package/src/constants.ts +21 -0
  254. package/src/core/benchmark-schema.ts +185 -0
  255. package/src/core/engine.ts +888 -0
  256. package/src/core/engines/accurate-engine.ts +408 -0
  257. package/src/core/engines/index.ts +16 -0
  258. package/src/core/engines/tinybench-engine.ts +335 -0
  259. package/src/core/error-manager.ts +372 -0
  260. package/src/core/loader.ts +324 -0
  261. package/src/core/stats-utils.ts +135 -0
  262. package/src/index.ts +46 -0
  263. package/src/progress/manager.ts +415 -0
  264. package/src/reporters/csv.ts +368 -0
  265. package/src/reporters/human.ts +707 -0
  266. package/src/reporters/index.ts +10 -0
  267. package/src/reporters/json.ts +302 -0
  268. package/src/reporters/registry.ts +349 -0
  269. package/src/reporters/simple.ts +459 -0
  270. package/src/storage/history.ts +600 -0
  271. package/src/types/cli.ts +312 -0
  272. package/src/types/core.ts +414 -0
  273. package/src/types/index.ts +18 -0
  274. package/src/types/interfaces.ts +451 -0
  275. package/src/types/utility.ts +446 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * ModestBench Configuration Manager
3
+ *
4
+ * Handles loading, merging, and validation of configuration from multiple
5
+ * sources. Supports CLI arguments, config files (JSON/YAML/JS/TS), and
6
+ * defaults.
7
+ */
8
+
9
+ import { cosmiconfig } from 'cosmiconfig';
10
+ import { resolve } from 'node:path';
11
+
12
+ import type {
13
+ ConfigurationManager,
14
+ ModestBenchConfig,
15
+ ValidationError,
16
+ ValidationResult,
17
+ ValidationWarning,
18
+ } from '../types/index.js';
19
+
20
+ import { safeParseConfig } from './schema.js';
21
+
22
+ /**
23
+ * Get the default reporter based on TTY status and environment
24
+ */
25
+ const getDefaultReporter = (): string => {
26
+ // Use simple reporter when stdout is not a TTY and color is not forced
27
+ if (!process.stdout.isTTY && !isColorForced()) {
28
+ return 'simple';
29
+ }
30
+ return 'human';
31
+ };
32
+
33
+ /**
34
+ * Check if color output has been forced via environment variables
35
+ */
36
+ const isColorForced = (): boolean => {
37
+ return (
38
+ process.env.FORCE_COLOR !== undefined &&
39
+ process.env.FORCE_COLOR !== '0' &&
40
+ process.env.NO_COLOR === undefined
41
+ );
42
+ };
43
+
44
+ /**
45
+ * Default configuration values Using minimal values to reduce test overhead
46
+ * while maintaining functionality
47
+ */
48
+ const DEFAULT_CONFIG: ModestBenchConfig = {
49
+ bail: false,
50
+ exclude: ['node_modules/**', '.git/**'],
51
+ excludeTags: [],
52
+ iterations: 100, // Sufficient iterations for reliable statistics
53
+ limitBy: 'iterations', // Default to limiting by iteration count
54
+ metadata: {},
55
+ outputDir: './benchmark-results',
56
+ pattern: '**/*.bench.{js,ts,mjs,cjs,mts,cts}',
57
+ quiet: false,
58
+ reporterConfig: {},
59
+ reporters: [getDefaultReporter()],
60
+ tags: [],
61
+ thresholds: {},
62
+ time: 1000, // 1 second minimum for tinybench to gather samples
63
+ timeout: 30000, // 30 seconds
64
+ verbose: false, // No verbose output by default
65
+ warmup: 0, // No warmup by default for test speed
66
+ };
67
+
68
+ /**
69
+ * Configuration precedence order (highest to lowest):
70
+ *
71
+ * 1. CLI arguments
72
+ * 2. Config file
73
+ * 3. Default values
74
+ */
75
+ export class ModestBenchConfigurationManager implements ConfigurationManager {
76
+ /**
77
+ * Apply smart defaults for limitBy based on which flags were provided
78
+ */
79
+ public static applySmartDefaults(
80
+ merged: ModestBenchConfig,
81
+ cliArgs: Record<string, unknown>,
82
+ fileConfig: Partial<ModestBenchConfig>,
83
+ ): ModestBenchConfig {
84
+ // If limitBy was explicitly provided in CLI or file, use it
85
+ if (cliArgs['limit-by'] || cliArgs.limitBy || fileConfig.limitBy) {
86
+ return merged;
87
+ }
88
+
89
+ // Determine if user explicitly provided time or iterations
90
+ const userProvidedTime = 'time' in cliArgs || 't' in cliArgs;
91
+ const userProvidedIterations = 'iterations' in cliArgs || 'i' in cliArgs;
92
+
93
+ let smartDefault: 'any' | 'iterations' | 'time';
94
+
95
+ if (userProvidedTime && userProvidedIterations) {
96
+ // Both provided → stop at whichever comes first
97
+ smartDefault = 'any';
98
+ } else if (userProvidedTime) {
99
+ // Only time → limit by time
100
+ smartDefault = 'time';
101
+ } else {
102
+ // Only iterations (or neither) → limit by iterations
103
+ smartDefault = 'iterations';
104
+ }
105
+
106
+ return {
107
+ ...merged,
108
+ limitBy: smartDefault,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Get default configuration values
114
+ */
115
+ getDefaults(): ModestBenchConfig {
116
+ return { ...DEFAULT_CONFIG };
117
+ }
118
+
119
+ /**
120
+ * Load configuration from various sources with precedence
121
+ */
122
+ async load(
123
+ configPath?: string,
124
+ cliArgs?: Record<string, unknown>,
125
+ ): Promise<ModestBenchConfig> {
126
+ try {
127
+ // Create a fresh explorer for each load to avoid module caching issues
128
+ const explorer = this.createExplorer();
129
+
130
+ // 1. Load config file using cosmiconfig
131
+ let result;
132
+ if (configPath) {
133
+ const resolvedPath = resolve(configPath);
134
+ // For .js/.mjs/.cjs files, add cache busting to the import to avoid Node's module cache
135
+ if (
136
+ resolvedPath.endsWith('.js') ||
137
+ resolvedPath.endsWith('.mjs') ||
138
+ resolvedPath.endsWith('.cjs')
139
+ ) {
140
+ // Clear Node's module cache for this file to ensure fresh load
141
+ const moduleUrl = `${resolvedPath}?t=${Date.now()}`;
142
+ try {
143
+ const module = (await import(moduleUrl)) as {
144
+ [key: string]: unknown;
145
+ default?: unknown;
146
+ };
147
+ result = {
148
+ config: module.default || module,
149
+ filepath: resolvedPath,
150
+ };
151
+ } catch {
152
+ // Fall back to explorer.load if cache busting fails
153
+ result = await explorer.load(resolvedPath);
154
+ }
155
+ } else {
156
+ result = await explorer.load(resolvedPath);
157
+ }
158
+ } else {
159
+ result = await explorer.search();
160
+ }
161
+
162
+ const fileConfig = (result?.config || {}) as Partial<ModestBenchConfig>;
163
+
164
+ // 2. Merge: defaults <- file <- CLI args
165
+ const normalizedCliArgs = cliArgs ? this.normalizeCliArgs(cliArgs) : {};
166
+ const merged = this.merge(DEFAULT_CONFIG, fileConfig, normalizedCliArgs);
167
+
168
+ // 2.5. Apply smart defaults for limitBy if not explicitly provided
169
+ const finalConfig = ModestBenchConfigurationManager.applySmartDefaults(
170
+ merged,
171
+ cliArgs || {},
172
+ fileConfig,
173
+ );
174
+
175
+ // 3. Validate final configuration
176
+ const validation = this.validate(finalConfig);
177
+ if (!validation.valid) {
178
+ throw new Error(
179
+ `Configuration validation failed: ${validation.errors.map((e) => e.message).join(', ')}`,
180
+ );
181
+ }
182
+
183
+ return finalConfig;
184
+ } catch (error) {
185
+ throw new Error(
186
+ `Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`,
187
+ );
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Merge multiple configuration objects with precedence
193
+ */
194
+ merge(...configs: Partial<ModestBenchConfig>[]): ModestBenchConfig {
195
+ let result: Partial<ModestBenchConfig> = {};
196
+
197
+ for (const config of configs) {
198
+ result = {
199
+ ...result,
200
+ ...config,
201
+ // Special handling for arrays - replace rather than merge
202
+ // Allow empty arrays to override defaults (for pattern defaulting in loader)
203
+ ...(config.pattern !== undefined && {
204
+ pattern: Array.isArray(config.pattern)
205
+ ? [...config.pattern]
206
+ : config.pattern,
207
+ }),
208
+ ...(config.exclude && { exclude: [...config.exclude] }),
209
+ ...(config.excludeTags && { excludeTags: [...config.excludeTags] }),
210
+ ...(config.reporters && { reporters: [...config.reporters] }),
211
+ ...(config.tags && { tags: [...config.tags] }),
212
+ // Deep merge for objects
213
+ ...(config.reporterConfig && {
214
+ reporterConfig: {
215
+ ...result.reporterConfig,
216
+ ...config.reporterConfig,
217
+ },
218
+ }),
219
+ ...(config.metadata && {
220
+ metadata: { ...result.metadata, ...config.metadata },
221
+ }),
222
+ ...(config.thresholds && {
223
+ thresholds: { ...result.thresholds, ...config.thresholds },
224
+ }),
225
+ };
226
+ }
227
+
228
+ return { ...DEFAULT_CONFIG, ...result };
229
+ }
230
+
231
+ /**
232
+ * Validate configuration object using Zod schema
233
+ */
234
+ validate(config: ModestBenchConfig): ValidationResult {
235
+ const errors: ValidationError[] = [];
236
+ const warnings: ValidationWarning[] = [];
237
+
238
+ // Use Zod schema validation
239
+ const result = safeParseConfig(config);
240
+
241
+ if (!result.success) {
242
+ // Convert Zod errors to ValidationError format
243
+ for (const issue of result.error.issues) {
244
+ const path = issue.path.join('.');
245
+ errors.push({
246
+ code: `INVALID_${path.toUpperCase().replace(/\./g, '_') || 'CONFIG'}`,
247
+ file: 'configuration',
248
+ message: `${path ? `${path}: ` : ''}${issue.message}`,
249
+ severity: 'error',
250
+ });
251
+ }
252
+ }
253
+
254
+ // Additional logical validations and warnings
255
+ if (result.success) {
256
+ const validConfig = result.data;
257
+
258
+ // Warn about empty reporters
259
+ if (validConfig.reporters.length === 0) {
260
+ warnings.push({
261
+ code: 'NO_REPORTERS',
262
+ file: 'configuration',
263
+ message: 'no reporters specified, using default human reporter',
264
+ severity: 'warning',
265
+ });
266
+ }
267
+
268
+ // Warn about potentially long runtime
269
+ if (validConfig.iterations > 1000 && validConfig.time > 60000) {
270
+ warnings.push({
271
+ code: 'LONG_RUNTIME_WARNING',
272
+ file: 'configuration',
273
+ message:
274
+ 'high iterations and time values may result in very long benchmark runs',
275
+ severity: 'warning',
276
+ });
277
+ }
278
+ }
279
+
280
+ return {
281
+ errors,
282
+ files: ['configuration'],
283
+ valid: errors.length === 0,
284
+ warnings,
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Create a cosmiconfig explorer for loading configuration files
290
+ */
291
+ private createExplorer() {
292
+ return cosmiconfig('modestbench', {
293
+ cache: false, // Disable caching to prevent cross-contamination between different config files
294
+ loaders: {
295
+ '.ts': async (filepath: string): Promise<unknown> => {
296
+ // Use cosmiconfig-typescript-loader to load TypeScript files
297
+ // This works without tsx in the import chain
298
+ const { TypeScriptLoader: createTypeScriptLoader } = await import(
299
+ 'cosmiconfig-typescript-loader'
300
+ );
301
+ const loader = createTypeScriptLoader();
302
+ const { readFile } = await import('node:fs/promises');
303
+ const content = await readFile(filepath, 'utf-8');
304
+ return (await loader(filepath, content)) as unknown;
305
+ },
306
+ },
307
+ searchPlaces: [
308
+ 'package.json',
309
+ '.modestbenchrc',
310
+ '.modestbenchrc.json',
311
+ '.modestbenchrc.yaml',
312
+ '.modestbenchrc.yml',
313
+ '.modestbenchrc.js',
314
+ '.modestbenchrc.mjs',
315
+ '.modestbenchrc.cjs',
316
+ 'modestbench.config.json',
317
+ 'modestbench.config.yaml',
318
+ 'modestbench.config.yml',
319
+ 'modestbench.config.js',
320
+ 'modestbench.config.mjs',
321
+ 'modestbench.config.cjs',
322
+ 'modestbench.config.ts',
323
+ ],
324
+ });
325
+ }
326
+
327
+ /**
328
+ * Normalize CLI arguments to configuration format
329
+ */
330
+ private normalizeCliArgs(
331
+ cliArgs: Record<string, unknown>,
332
+ ): Partial<ModestBenchConfig> {
333
+ const normalized: Record<string, unknown> = {};
334
+
335
+ // Map CLI argument names to config property names
336
+ const argMap: Record<string, keyof ModestBenchConfig> = {
337
+ bail: 'bail',
338
+ exclude: 'exclude',
339
+ 'exclude-tags': 'excludeTags',
340
+ excludeTags: 'excludeTags',
341
+ i: 'iterations',
342
+ iterations: 'iterations',
343
+ 'limit-by': 'limitBy',
344
+ limitBy: 'limitBy',
345
+ o: 'outputDir',
346
+ output: 'outputDir',
347
+ 'output-dir': 'outputDir',
348
+ pattern: 'pattern',
349
+ q: 'quiet',
350
+ quiet: 'quiet',
351
+ r: 'reporters',
352
+ reporters: 'reporters',
353
+ t: 'time',
354
+ tags: 'tags',
355
+ time: 'time',
356
+ timeout: 'timeout',
357
+ v: 'verbose',
358
+ verbose: 'verbose',
359
+ w: 'warmup',
360
+ warmup: 'warmup',
361
+ };
362
+
363
+ for (const [cliKey, configKey] of Object.entries(argMap)) {
364
+ if (cliKey in cliArgs && cliArgs[cliKey] !== undefined) {
365
+ const value = cliArgs[cliKey];
366
+
367
+ // Handle array arguments that might come as strings
368
+ if (
369
+ configKey === 'exclude' ||
370
+ configKey === 'excludeTags' ||
371
+ configKey === 'reporters' ||
372
+ configKey === 'tags'
373
+ ) {
374
+ if (typeof value === 'string') {
375
+ normalized[configKey] = value.split(',').map((s) => s.trim());
376
+ } else if (Array.isArray(value)) {
377
+ normalized[configKey] = value;
378
+ }
379
+ } else {
380
+ normalized[configKey] = value;
381
+ }
382
+ }
383
+ }
384
+
385
+ return normalized as Partial<ModestBenchConfig>;
386
+ }
387
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * ModestBench Configuration Schemas
3
+ *
4
+ * Zod schemas for validating configuration. These schemas are constrained to
5
+ * match the TypeScript types defined in types/core.ts, ensuring type safety and
6
+ * enabling JSON Schema generation.
7
+ */
8
+
9
+ import * as z from 'zod';
10
+
11
+ import type { ModestBenchConfig } from '../types/core.js';
12
+
13
+ import { BENCHMARK_FILE_PATTERN } from '../constants.js';
14
+
15
+ /**
16
+ * Schema for threshold configuration
17
+ *
18
+ * Defines performance assertion thresholds for benchmark validation.
19
+ */
20
+ const thresholdConfigSchema = z
21
+ .object({
22
+ maxMarginOfError: z
23
+ .number()
24
+ .positive()
25
+ .describe('Maximum allowed margin of error as a percentage')
26
+ .optional(),
27
+ maxMean: z
28
+ .number()
29
+ .positive()
30
+ .describe('Maximum allowed mean execution time in nanoseconds')
31
+ .optional(),
32
+ maxP95: z
33
+ .number()
34
+ .positive()
35
+ .describe('Maximum allowed 95th percentile execution time in nanoseconds')
36
+ .optional(),
37
+ maxP99: z
38
+ .number()
39
+ .positive()
40
+ .describe('Maximum allowed 99th percentile execution time in nanoseconds')
41
+ .optional(),
42
+ maxStdDev: z
43
+ .number()
44
+ .positive()
45
+ .describe('Maximum allowed standard deviation in nanoseconds')
46
+ .optional(),
47
+ minOpsPerSecond: z
48
+ .number()
49
+ .positive()
50
+ .describe('Minimum required operations per second')
51
+ .optional(),
52
+ })
53
+ .strict()
54
+ .describe('Performance assertion thresholds for benchmark validation')
55
+ .meta({
56
+ title: 'Threshold Configuration',
57
+ });
58
+
59
+ /**
60
+ * Schema for the main ModestBench configuration
61
+ *
62
+ * This is the complete configuration schema used for validating benchmark
63
+ * configuration from all sources (files, CLI args, defaults).
64
+ */
65
+ const modestBenchConfigSchema = z
66
+ .object({
67
+ $schema: z
68
+ .string()
69
+ .optional()
70
+ .describe(
71
+ 'JSON Schema reference for IDE support (not used by ModestBench)',
72
+ ),
73
+ bail: z.boolean().describe('Stop benchmark execution on first failure'),
74
+ exclude: z
75
+ .array(z.string())
76
+ .describe(
77
+ 'Glob patterns to exclude from benchmark file discovery (e.g., "node_modules/**", ".git/**")',
78
+ ),
79
+ excludeTags: z
80
+ .array(z.string())
81
+ .describe(
82
+ 'Tags to exclude from benchmark execution. Benchmarks matching any of these tags will be skipped.',
83
+ ),
84
+ iterations: z
85
+ .number()
86
+ .int()
87
+ .positive()
88
+ .describe(
89
+ 'Default number of iterations to run for each benchmark task. Higher values provide more accurate statistics but take longer to execute.',
90
+ ),
91
+ limitBy: z
92
+ .enum(['time', 'iterations', 'any', 'all'])
93
+ .describe(
94
+ 'How to limit benchmark execution: "time" stops after time limit, "iterations" stops after iteration count, "any" stops at whichever comes first, "all" runs until both limits are reached',
95
+ ),
96
+ metadata: z
97
+ .record(z.string(), z.unknown())
98
+ .describe(
99
+ 'Custom metadata to attach to benchmark runs. Can include project name, version, environment details, etc.',
100
+ ),
101
+ outputDir: z
102
+ .string()
103
+ .min(1)
104
+ .describe(
105
+ 'Directory path where benchmark results and reports will be written',
106
+ ),
107
+ pattern: z
108
+ .union([z.string().min(1), z.array(z.string().min(1))])
109
+ .describe(
110
+ `Glob pattern(s) for discovering benchmark files. Can be a single pattern string or array of patterns (e.g., "**/*${BENCHMARK_FILE_PATTERN}")`,
111
+ ),
112
+ quiet: z
113
+ .boolean()
114
+ .describe(
115
+ 'Run in quiet mode with minimal console output (only errors and final results)',
116
+ ),
117
+ reporterConfig: z
118
+ .record(z.string(), z.unknown())
119
+ .describe(
120
+ 'Configuration options specific to individual reporters, keyed by reporter name',
121
+ ),
122
+ reporters: z
123
+ .array(z.string())
124
+ .min(1)
125
+ .describe(
126
+ 'List of reporter names to use for output. Available reporters: "human", "json", "csv"',
127
+ ),
128
+ tags: z
129
+ .array(z.string())
130
+ .describe(
131
+ 'Tags to filter which benchmarks to run. If empty, all benchmarks are included. Only benchmarks with matching tags will execute.',
132
+ ),
133
+ thresholds: thresholdConfigSchema,
134
+ time: z
135
+ .number()
136
+ .int()
137
+ .positive()
138
+ .describe(
139
+ 'Maximum time to spend on each benchmark task in milliseconds. Tasks will run at least until this duration or iteration count is reached, depending on limitBy setting.',
140
+ ),
141
+ timeout: z
142
+ .number()
143
+ .int()
144
+ .positive()
145
+ .describe(
146
+ 'Timeout for individual benchmark tasks in milliseconds. Tasks exceeding this duration will be terminated and marked as failed.',
147
+ ),
148
+ verbose: z
149
+ .boolean()
150
+ .describe(
151
+ 'Enable verbose output. Provides more detailed console output including progress, intermediate results, and diagnostic information',
152
+ ),
153
+ warmup: z
154
+ .number()
155
+ .int()
156
+ .nonnegative()
157
+ .describe(
158
+ 'Number of warmup iterations to run before measurement begins. Warmup helps stabilize performance by allowing JIT compilation and caching to occur.',
159
+ ),
160
+ })
161
+ .strict()
162
+ .describe(
163
+ 'ModestBench configuration for controlling benchmark discovery, execution, and reporting',
164
+ )
165
+ .meta({
166
+ title: 'ModestBench Configuration',
167
+ });
168
+
169
+ /**
170
+ * Validate a partial configuration object
171
+ *
172
+ * This is used for validating configuration from files or CLI args before
173
+ * merging with defaults.
174
+ */
175
+ export const partialModestBenchConfigSchema: z.ZodType<
176
+ Partial<ModestBenchConfig>
177
+ > = modestBenchConfigSchema.partial();
178
+
179
+ /**
180
+ * Safely parse and validate a configuration object
181
+ *
182
+ * @param config - The configuration object to validate
183
+ * @returns A result object with either success: true and data, or success:
184
+ * false and error
185
+ */
186
+ export const safeParseConfig = (config: unknown) => {
187
+ return modestBenchConfigSchema.safeParse(config);
188
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Supported benchmark file extensions
3
+ */
4
+ export const BENCHMARK_FILE_EXTENSIONS = new Set([
5
+ '.cjs',
6
+ '.cts',
7
+ '.js',
8
+ '.mjs',
9
+ '.mts',
10
+ '.ts',
11
+ ]);
12
+
13
+ /**
14
+ * Glob pattern fragment for benchmark file extensions. Example:
15
+ * ".bench.{js,mjs,cjs,ts,mts,cts}"
16
+ */
17
+ export const BENCHMARK_FILE_PATTERN = `.bench.{${Array.from(
18
+ BENCHMARK_FILE_EXTENSIONS,
19
+ )
20
+ .map((ext) => ext.slice(1))
21
+ .join(',')}}`;