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.
- package/CHANGELOG.md +45 -0
- package/LICENSE.md +55 -0
- package/README.md +699 -0
- package/dist/bootstrap.cjs +37 -0
- package/dist/bootstrap.cjs.map +1 -0
- package/dist/bootstrap.d.cts +17 -0
- package/dist/bootstrap.d.cts.map +1 -0
- package/dist/bootstrap.d.ts +17 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +33 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli/commands/history.cjs +459 -0
- package/dist/cli/commands/history.cjs.map +1 -0
- package/dist/cli/commands/history.d.cts +34 -0
- package/dist/cli/commands/history.d.cts.map +1 -0
- package/dist/cli/commands/history.d.ts +34 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +422 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/init.cjs +566 -0
- package/dist/cli/commands/init.cjs.map +1 -0
- package/dist/cli/commands/init.d.cts +26 -0
- package/dist/cli/commands/init.d.cts.map +1 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +562 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/run.cjs +285 -0
- package/dist/cli/commands/run.cjs.map +1 -0
- package/dist/cli/commands/run.d.cts +37 -0
- package/dist/cli/commands/run.d.cts.map +1 -0
- package/dist/cli/commands/run.d.ts +37 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +248 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/index.cjs +523 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +58 -0
- package/dist/cli/index.d.cts.map +1 -0
- package/dist/cli/index.d.ts +58 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +515 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/manager.cjs +370 -0
- package/dist/config/manager.cjs.map +1 -0
- package/dist/config/manager.d.cts +46 -0
- package/dist/config/manager.d.cts.map +1 -0
- package/dist/config/manager.d.ts +46 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/config/manager.js +333 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/schema.cjs +182 -0
- package/dist/config/schema.cjs.map +1 -0
- package/dist/config/schema.d.cts +51 -0
- package/dist/config/schema.d.cts.map +1 -0
- package/dist/config/schema.d.ts +51 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +145 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/constants.cjs +22 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +10 -0
- package/dist/constants.d.cts.map +1 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +19 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/benchmark-schema.cjs +135 -0
- package/dist/core/benchmark-schema.cjs.map +1 -0
- package/dist/core/benchmark-schema.d.cts +139 -0
- package/dist/core/benchmark-schema.d.cts.map +1 -0
- package/dist/core/benchmark-schema.d.ts +139 -0
- package/dist/core/benchmark-schema.d.ts.map +1 -0
- package/dist/core/benchmark-schema.js +132 -0
- package/dist/core/benchmark-schema.js.map +1 -0
- package/dist/core/engine.cjs +669 -0
- package/dist/core/engine.cjs.map +1 -0
- package/dist/core/engine.d.cts +128 -0
- package/dist/core/engine.d.cts.map +1 -0
- package/dist/core/engine.d.ts +128 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +632 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/engines/accurate-engine.cjs +292 -0
- package/dist/core/engines/accurate-engine.cjs.map +1 -0
- package/dist/core/engines/accurate-engine.d.cts +63 -0
- package/dist/core/engines/accurate-engine.d.cts.map +1 -0
- package/dist/core/engines/accurate-engine.d.ts +63 -0
- package/dist/core/engines/accurate-engine.d.ts.map +1 -0
- package/dist/core/engines/accurate-engine.js +288 -0
- package/dist/core/engines/accurate-engine.js.map +1 -0
- package/dist/core/engines/index.cjs +21 -0
- package/dist/core/engines/index.cjs.map +1 -0
- package/dist/core/engines/index.d.cts +16 -0
- package/dist/core/engines/index.d.cts.map +1 -0
- package/dist/core/engines/index.d.ts +16 -0
- package/dist/core/engines/index.d.ts.map +1 -0
- package/dist/core/engines/index.js +16 -0
- package/dist/core/engines/index.js.map +1 -0
- package/dist/core/engines/tinybench-engine.cjs +286 -0
- package/dist/core/engines/tinybench-engine.cjs.map +1 -0
- package/dist/core/engines/tinybench-engine.d.cts +18 -0
- package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
- package/dist/core/engines/tinybench-engine.d.ts +18 -0
- package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
- package/dist/core/engines/tinybench-engine.js +282 -0
- package/dist/core/engines/tinybench-engine.js.map +1 -0
- package/dist/core/error-manager.cjs +303 -0
- package/dist/core/error-manager.cjs.map +1 -0
- package/dist/core/error-manager.d.cts +77 -0
- package/dist/core/error-manager.d.cts.map +1 -0
- package/dist/core/error-manager.d.ts +77 -0
- package/dist/core/error-manager.d.ts.map +1 -0
- package/dist/core/error-manager.js +299 -0
- package/dist/core/error-manager.js.map +1 -0
- package/dist/core/loader.cjs +287 -0
- package/dist/core/loader.cjs.map +1 -0
- package/dist/core/loader.d.cts +55 -0
- package/dist/core/loader.d.cts.map +1 -0
- package/dist/core/loader.d.ts +55 -0
- package/dist/core/loader.d.ts.map +1 -0
- package/dist/core/loader.js +250 -0
- package/dist/core/loader.js.map +1 -0
- package/dist/core/stats-utils.cjs +99 -0
- package/dist/core/stats-utils.cjs.map +1 -0
- package/dist/core/stats-utils.d.cts +50 -0
- package/dist/core/stats-utils.d.cts.map +1 -0
- package/dist/core/stats-utils.d.ts +50 -0
- package/dist/core/stats-utils.d.ts.map +1 -0
- package/dist/core/stats-utils.js +94 -0
- package/dist/core/stats-utils.js.map +1 -0
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/progress/manager.cjs +325 -0
- package/dist/progress/manager.cjs.map +1 -0
- package/dist/progress/manager.d.cts +125 -0
- package/dist/progress/manager.d.cts.map +1 -0
- package/dist/progress/manager.d.ts +125 -0
- package/dist/progress/manager.d.ts.map +1 -0
- package/dist/progress/manager.js +321 -0
- package/dist/progress/manager.js.map +1 -0
- package/dist/reporters/csv.cjs +250 -0
- package/dist/reporters/csv.cjs.map +1 -0
- package/dist/reporters/csv.d.cts +92 -0
- package/dist/reporters/csv.d.cts.map +1 -0
- package/dist/reporters/csv.d.ts +92 -0
- package/dist/reporters/csv.d.ts.map +1 -0
- package/dist/reporters/csv.js +246 -0
- package/dist/reporters/csv.js.map +1 -0
- package/dist/reporters/human.cjs +516 -0
- package/dist/reporters/human.cjs.map +1 -0
- package/dist/reporters/human.d.cts +86 -0
- package/dist/reporters/human.d.cts.map +1 -0
- package/dist/reporters/human.d.ts +86 -0
- package/dist/reporters/human.d.ts.map +1 -0
- package/dist/reporters/human.js +509 -0
- package/dist/reporters/human.js.map +1 -0
- package/dist/reporters/index.cjs +17 -0
- package/dist/reporters/index.cjs.map +1 -0
- package/dist/reporters/index.d.cts +10 -0
- package/dist/reporters/index.d.cts.map +1 -0
- package/dist/reporters/index.d.ts +10 -0
- package/dist/reporters/index.d.ts.map +1 -0
- package/dist/reporters/index.js +10 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/json.cjs +215 -0
- package/dist/reporters/json.cjs.map +1 -0
- package/dist/reporters/json.d.cts +79 -0
- package/dist/reporters/json.d.cts.map +1 -0
- package/dist/reporters/json.d.ts +79 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +211 -0
- package/dist/reporters/json.js.map +1 -0
- package/dist/reporters/registry.cjs +255 -0
- package/dist/reporters/registry.cjs.map +1 -0
- package/dist/reporters/registry.d.cts +155 -0
- package/dist/reporters/registry.d.cts.map +1 -0
- package/dist/reporters/registry.d.ts +155 -0
- package/dist/reporters/registry.d.ts.map +1 -0
- package/dist/reporters/registry.js +249 -0
- package/dist/reporters/registry.js.map +1 -0
- package/dist/reporters/simple.cjs +328 -0
- package/dist/reporters/simple.cjs.map +1 -0
- package/dist/reporters/simple.d.cts +51 -0
- package/dist/reporters/simple.d.cts.map +1 -0
- package/dist/reporters/simple.d.ts +51 -0
- package/dist/reporters/simple.d.ts.map +1 -0
- package/dist/reporters/simple.js +321 -0
- package/dist/reporters/simple.js.map +1 -0
- package/dist/schema/modestbench-config.schema.json +162 -0
- package/dist/storage/history.cjs +456 -0
- package/dist/storage/history.cjs.map +1 -0
- package/dist/storage/history.d.cts +99 -0
- package/dist/storage/history.d.cts.map +1 -0
- package/dist/storage/history.d.ts +99 -0
- package/dist/storage/history.d.ts.map +1 -0
- package/dist/storage/history.js +452 -0
- package/dist/storage/history.js.map +1 -0
- package/dist/types/cli.cjs +21 -0
- package/dist/types/cli.cjs.map +1 -0
- package/dist/types/cli.d.cts +296 -0
- package/dist/types/cli.d.cts.map +1 -0
- package/dist/types/cli.d.ts +296 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +18 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/core.cjs +14 -0
- package/dist/types/core.cjs.map +1 -0
- package/dist/types/core.d.cts +380 -0
- package/dist/types/core.d.cts.map +1 -0
- package/dist/types/core.d.ts +380 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/core.js +13 -0
- package/dist/types/core.js.map +1 -0
- package/dist/types/index.cjs +27 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +11 -0
- package/dist/types/index.d.cts.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +11 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/interfaces.cjs +10 -0
- package/dist/types/interfaces.cjs.map +1 -0
- package/dist/types/interfaces.d.cts +381 -0
- package/dist/types/interfaces.d.cts.map +1 -0
- package/dist/types/interfaces.d.ts +381 -0
- package/dist/types/interfaces.d.ts.map +1 -0
- package/dist/types/interfaces.js +9 -0
- package/dist/types/interfaces.js.map +1 -0
- package/dist/types/utility.cjs +92 -0
- package/dist/types/utility.cjs.map +1 -0
- package/dist/types/utility.d.cts +330 -0
- package/dist/types/utility.d.cts.map +1 -0
- package/dist/types/utility.d.ts +330 -0
- package/dist/types/utility.d.ts.map +1 -0
- package/dist/types/utility.js +78 -0
- package/dist/types/utility.js.map +1 -0
- package/package.json +211 -0
- package/src/bootstrap.ts +35 -0
- package/src/cli/commands/history.ts +569 -0
- package/src/cli/commands/init.ts +658 -0
- package/src/cli/commands/run.ts +346 -0
- package/src/cli/index.ts +642 -0
- package/src/config/manager.ts +387 -0
- package/src/config/schema.ts +188 -0
- package/src/constants.ts +21 -0
- package/src/core/benchmark-schema.ts +185 -0
- package/src/core/engine.ts +888 -0
- package/src/core/engines/accurate-engine.ts +408 -0
- package/src/core/engines/index.ts +16 -0
- package/src/core/engines/tinybench-engine.ts +335 -0
- package/src/core/error-manager.ts +372 -0
- package/src/core/loader.ts +324 -0
- package/src/core/stats-utils.ts +135 -0
- package/src/index.ts +46 -0
- package/src/progress/manager.ts +415 -0
- package/src/reporters/csv.ts +368 -0
- package/src/reporters/human.ts +707 -0
- package/src/reporters/index.ts +10 -0
- package/src/reporters/json.ts +302 -0
- package/src/reporters/registry.ts +349 -0
- package/src/reporters/simple.ts +459 -0
- package/src/storage/history.ts +600 -0
- package/src/types/cli.ts +312 -0
- package/src/types/core.ts +414 -0
- package/src/types/index.ts +18 -0
- package/src/types/interfaces.ts +451 -0
- 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
|
+
};
|
package/src/constants.ts
ADDED
|
@@ -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(',')}}`;
|