modestbench 0.7.0 → 0.8.0
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 +12 -0
- package/dist/cli/commands/run.cjs +89 -49
- package/dist/cli/commands/run.cjs.map +1 -1
- package/dist/cli/commands/run.d.cts.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +90 -50
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/constants.cjs +2 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +2 -0
- package/dist/constants.d.cts.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/errors/index.cjs +3 -1
- package/dist/errors/index.cjs.map +1 -1
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.cts.map +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/reporter.cjs +45 -1
- package/dist/errors/reporter.cjs.map +1 -1
- package/dist/errors/reporter.d.cts +32 -0
- package/dist/errors/reporter.d.cts.map +1 -1
- package/dist/errors/reporter.d.ts +32 -0
- package/dist/errors/reporter.d.ts.map +1 -1
- package/dist/errors/reporter.js +42 -0
- package/dist/errors/reporter.js.map +1 -1
- package/dist/index.cjs +16 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/services/reporter-loader.cjs +281 -0
- package/dist/services/reporter-loader.cjs.map +1 -0
- package/dist/services/reporter-loader.d.cts +67 -0
- package/dist/services/reporter-loader.d.cts.map +1 -0
- package/dist/services/reporter-loader.d.ts +67 -0
- package/dist/services/reporter-loader.d.ts.map +1 -0
- package/dist/services/reporter-loader.js +241 -0
- package/dist/services/reporter-loader.js.map +1 -0
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +1 -0
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/plugin.cjs +9 -0
- package/dist/types/plugin.cjs.map +1 -0
- package/dist/types/plugin.d.cts +179 -0
- package/dist/types/plugin.d.cts.map +1 -0
- package/dist/types/plugin.d.ts +179 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +8 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/utils/package.cjs +66 -5
- package/dist/utils/package.cjs.map +1 -1
- package/dist/utils/package.d.cts +6 -0
- package/dist/utils/package.d.cts.map +1 -1
- package/dist/utils/package.d.ts +6 -0
- package/dist/utils/package.d.ts.map +1 -1
- package/dist/utils/package.js +31 -1
- package/dist/utils/package.js.map +1 -1
- package/dist/utils/reporter-utils.cjs +90 -0
- package/dist/utils/reporter-utils.cjs.map +1 -0
- package/dist/utils/reporter-utils.d.cts +42 -0
- package/dist/utils/reporter-utils.d.cts.map +1 -0
- package/dist/utils/reporter-utils.d.ts +42 -0
- package/dist/utils/reporter-utils.d.ts.map +1 -0
- package/dist/utils/reporter-utils.js +83 -0
- package/dist/utils/reporter-utils.js.map +1 -0
- package/package.json +4 -4
- package/src/cli/commands/run.ts +127 -68
- package/src/constants.ts +2 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/reporter.ts +55 -0
- package/src/index.ts +19 -1
- package/src/services/reporter-loader.ts +323 -0
- package/src/types/index.ts +3 -0
- package/src/types/plugin.ts +197 -0
- package/src/utils/package.ts +32 -1
- package/src/utils/reporter-utils.ts +85 -0
package/src/index.ts
CHANGED
|
@@ -41,6 +41,15 @@ export { parseProfile } from './services/profiler/profile-parser.js';
|
|
|
41
41
|
|
|
42
42
|
export { runWithProfiling } from './services/profiler/profile-runner.js';
|
|
43
43
|
export { ModestBenchProgressManager } from './services/progress-manager.js';
|
|
44
|
+
// Reporter plugin loader
|
|
45
|
+
export {
|
|
46
|
+
createReporterContext,
|
|
47
|
+
isBuiltInReporter,
|
|
48
|
+
isFilePath,
|
|
49
|
+
loadReporter,
|
|
50
|
+
PLUGIN_API_VERSION,
|
|
51
|
+
} from './services/reporter-loader.js';
|
|
52
|
+
|
|
44
53
|
export {
|
|
45
54
|
BaseReporter,
|
|
46
55
|
CompositeReporter,
|
|
@@ -51,4 +60,13 @@ export {
|
|
|
51
60
|
export * from './types/index.js';
|
|
52
61
|
|
|
53
62
|
// Utilities
|
|
54
|
-
export { findPackageRoot } from './utils/package.js';
|
|
63
|
+
export { findPackageRoot, getPackageVersion } from './utils/package.js';
|
|
64
|
+
|
|
65
|
+
// Reporter utilities (for plugin authors)
|
|
66
|
+
export {
|
|
67
|
+
formatBytes,
|
|
68
|
+
formatDuration,
|
|
69
|
+
formatOpsPerSecond,
|
|
70
|
+
formatPercentage,
|
|
71
|
+
reporterUtils,
|
|
72
|
+
} from './utils/reporter-utils.js';
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Reporter Loader
|
|
3
|
+
*
|
|
4
|
+
* Service for loading third-party reporter plugins from file paths or npm
|
|
5
|
+
* packages. Supports multiple export patterns: plain objects, classes, and
|
|
6
|
+
* factory functions (sync or async).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
|
|
12
|
+
import type { Logger, Reporter, ReporterContext } from '../types/index.js';
|
|
13
|
+
|
|
14
|
+
import { Reporters } from '../constants.js';
|
|
15
|
+
import {
|
|
16
|
+
ReporterLoadError,
|
|
17
|
+
ReporterValidationError,
|
|
18
|
+
} from '../errors/reporter.js';
|
|
19
|
+
import { getPackageVersion } from '../utils/package.js';
|
|
20
|
+
import { reporterUtils } from '../utils/reporter-utils.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Current plugin API version
|
|
24
|
+
*
|
|
25
|
+
* Increment this when making breaking changes to the plugin API.
|
|
26
|
+
*/
|
|
27
|
+
export const PLUGIN_API_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Set of built-in reporter names
|
|
31
|
+
*/
|
|
32
|
+
const BUILT_IN_REPORTERS = new Set(Object.values(Reporters));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Required methods that all reporters must implement
|
|
36
|
+
*/
|
|
37
|
+
const REQUIRED_REPORTER_METHODS = [
|
|
38
|
+
'onStart',
|
|
39
|
+
'onEnd',
|
|
40
|
+
'onError',
|
|
41
|
+
'onTaskResult',
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default logger implementation using console
|
|
46
|
+
*
|
|
47
|
+
* This provides a simple console-based logger for reporter plugins.
|
|
48
|
+
*/
|
|
49
|
+
const defaultLogger: Logger = {
|
|
50
|
+
debug: (message, ...args) => console.debug(message, ...args),
|
|
51
|
+
error: (message, ...args) => console.error(message, ...args),
|
|
52
|
+
info: (message, ...args) => console.info(message, ...args),
|
|
53
|
+
trace: (message, ...args) => console.trace(message, ...args),
|
|
54
|
+
warn: (message, ...args) => console.warn(message, ...args),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a ReporterContext for passing to plugins
|
|
59
|
+
*
|
|
60
|
+
* @param logger - Optional logger to use (defaults to console-based logger)
|
|
61
|
+
* @returns ReporterContext with version info and utilities
|
|
62
|
+
*/
|
|
63
|
+
export const createReporterContext = (logger?: Logger): ReporterContext => {
|
|
64
|
+
return {
|
|
65
|
+
logger: logger ?? defaultLogger,
|
|
66
|
+
pluginApiVersion: PLUGIN_API_VERSION,
|
|
67
|
+
utils: reporterUtils,
|
|
68
|
+
version: getPackageVersion(),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the list of missing required methods from a reporter object
|
|
74
|
+
*
|
|
75
|
+
* @param obj - Object to check
|
|
76
|
+
* @returns Array of missing method names
|
|
77
|
+
*/
|
|
78
|
+
const getMissingMethods = (obj: unknown): string[] => {
|
|
79
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
80
|
+
return [...REQUIRED_REPORTER_METHODS];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return REQUIRED_REPORTER_METHODS.filter(
|
|
84
|
+
(method) => typeof (obj as Record<string, unknown>)[method] !== 'function',
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a specifier refers to a built-in reporter
|
|
90
|
+
*
|
|
91
|
+
* @param specifier - Reporter name or path
|
|
92
|
+
* @returns True if the specifier is a built-in reporter name
|
|
93
|
+
*/
|
|
94
|
+
export const isBuiltInReporter = (specifier: string): boolean => {
|
|
95
|
+
return BUILT_IN_REPORTERS.has(
|
|
96
|
+
specifier as (typeof Reporters)[keyof typeof Reporters],
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a function is a class constructor
|
|
102
|
+
*
|
|
103
|
+
* Uses heuristics to distinguish classes from regular functions:
|
|
104
|
+
*
|
|
105
|
+
* - Classes have a non-writable prototype property
|
|
106
|
+
* - Class syntax produces different toString() output
|
|
107
|
+
*
|
|
108
|
+
* @param func - Function to check
|
|
109
|
+
* @returns True if the function appears to be a class constructor
|
|
110
|
+
*/
|
|
111
|
+
const isClass = (
|
|
112
|
+
func: unknown,
|
|
113
|
+
): func is new (...args: unknown[]) => unknown => {
|
|
114
|
+
if (typeof func !== 'function') {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Classes have a non-writable prototype
|
|
119
|
+
const protoDescriptor = Object.getOwnPropertyDescriptor(func, 'prototype');
|
|
120
|
+
if (!protoDescriptor || protoDescriptor.writable) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check if it uses class syntax (handles both 'class Foo' and 'class{')
|
|
125
|
+
const funcStr = func.toString();
|
|
126
|
+
return /^class\b/.test(funcStr);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a specifier looks like a file path
|
|
131
|
+
*
|
|
132
|
+
* @param specifier - Reporter name or path
|
|
133
|
+
* @returns True if the specifier appears to be a file path
|
|
134
|
+
*/
|
|
135
|
+
export const isFilePath = (specifier: string): boolean => {
|
|
136
|
+
return (
|
|
137
|
+
specifier.startsWith('.') ||
|
|
138
|
+
specifier.startsWith('/') ||
|
|
139
|
+
// isAbsolute handles Windows paths like 'C:\path\to\file.js'
|
|
140
|
+
isAbsolute(specifier)
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if an object implements the Reporter interface
|
|
146
|
+
*
|
|
147
|
+
* Validates that all required methods are present and are functions.
|
|
148
|
+
*
|
|
149
|
+
* @param obj - Object to validate
|
|
150
|
+
* @returns True if the object has all required reporter methods
|
|
151
|
+
*/
|
|
152
|
+
const isReporterObject = (obj: unknown): obj is Reporter => {
|
|
153
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return REQUIRED_REPORTER_METHODS.every(
|
|
158
|
+
(method) => typeof (obj as Record<string, unknown>)[method] === 'function',
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Load a reporter from a file path or npm package name
|
|
164
|
+
*
|
|
165
|
+
* Supports multiple export patterns:
|
|
166
|
+
*
|
|
167
|
+
* 1. Plain Reporter object (simplest, no options support)
|
|
168
|
+
* 2. Class constructor (instantiated with options and context)
|
|
169
|
+
* 3. Factory function (called with options and context, can be async)
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
*
|
|
173
|
+
* ```typescript
|
|
174
|
+
* // Load from file path
|
|
175
|
+
* const reporter = await loadReporter('./my-reporter.js', {
|
|
176
|
+
* verbose: true,
|
|
177
|
+
* });
|
|
178
|
+
*
|
|
179
|
+
* // Load from npm package
|
|
180
|
+
* const reporter = await loadReporter('@company/custom-reporter', {
|
|
181
|
+
* apiKey: 'xxx',
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @param specifier - File path (relative or absolute) or npm package name
|
|
186
|
+
* @param options - Options to pass to the reporter factory/constructor
|
|
187
|
+
* @param cwd - Current working directory for resolving relative paths
|
|
188
|
+
* @returns Loaded reporter instance
|
|
189
|
+
* @throws ReporterLoadError if the module cannot be loaded
|
|
190
|
+
* @throws ReporterValidationError if the module doesn't implement Reporter
|
|
191
|
+
*/
|
|
192
|
+
export const loadReporter = async (
|
|
193
|
+
specifier: string,
|
|
194
|
+
options: Record<string, unknown> = {},
|
|
195
|
+
cwd: string = process.cwd(),
|
|
196
|
+
): Promise<Reporter> => {
|
|
197
|
+
const context = createReporterContext();
|
|
198
|
+
const resolvedSpecifier = resolveSpecifier(specifier, cwd);
|
|
199
|
+
|
|
200
|
+
let module: unknown;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
module = await import(resolvedSpecifier);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
206
|
+
throw new ReporterLoadError(message, specifier, { cause: error });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Handle ESM/CJS interop - get default export if present
|
|
210
|
+
const exported = (module as { default?: unknown }).default ?? module;
|
|
211
|
+
|
|
212
|
+
// Case 1: Already a Reporter object (plain object export)
|
|
213
|
+
if (isReporterObject(exported)) {
|
|
214
|
+
return exported;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Case 2: Class constructor
|
|
218
|
+
if (isClass(exported)) {
|
|
219
|
+
let instance: unknown;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
instance = new (exported as new (
|
|
223
|
+
options: Record<string, unknown>,
|
|
224
|
+
context: ReporterContext,
|
|
225
|
+
) => unknown)(options, context);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
228
|
+
throw new ReporterLoadError(
|
|
229
|
+
`Constructor threw error: ${message}`,
|
|
230
|
+
specifier,
|
|
231
|
+
{ cause: error },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
validateReporter(instance, specifier);
|
|
236
|
+
return instance;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Case 3: Factory function (sync or async)
|
|
240
|
+
if (typeof exported === 'function') {
|
|
241
|
+
let result: unknown;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
result = await (
|
|
245
|
+
exported as (
|
|
246
|
+
options: Record<string, unknown>,
|
|
247
|
+
context: ReporterContext,
|
|
248
|
+
) => Promise<unknown>
|
|
249
|
+
)(options, context);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
252
|
+
throw new ReporterLoadError(
|
|
253
|
+
`Factory function threw error: ${message}`,
|
|
254
|
+
specifier,
|
|
255
|
+
{ cause: error },
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
validateReporter(result, specifier);
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// None of the above - could be an object with missing methods or invalid type
|
|
264
|
+
if (typeof exported === 'object' && exported !== null) {
|
|
265
|
+
// It's an object but missing required methods
|
|
266
|
+
const missing = getMissingMethods(exported);
|
|
267
|
+
throw new ReporterValidationError(
|
|
268
|
+
'Module does not implement Reporter interface.',
|
|
269
|
+
specifier,
|
|
270
|
+
missing,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Completely invalid export type
|
|
275
|
+
throw new ReporterValidationError(
|
|
276
|
+
'Module must export a Reporter object, class, or factory function.',
|
|
277
|
+
specifier,
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Resolve a specifier to an importable URL or module name
|
|
283
|
+
*
|
|
284
|
+
* @param specifier - File path or npm package name
|
|
285
|
+
* @param cwd - Current working directory for resolving relative paths
|
|
286
|
+
* @returns Resolved module specifier
|
|
287
|
+
*/
|
|
288
|
+
const resolveSpecifier = (specifier: string, cwd: string): string => {
|
|
289
|
+
if (isFilePath(specifier)) {
|
|
290
|
+
const absolutePath = resolve(cwd, specifier);
|
|
291
|
+
return pathToFileURL(absolutePath).href;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// npm package name - return as-is for dynamic import
|
|
295
|
+
return specifier;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Validate that an object implements the Reporter interface
|
|
300
|
+
*
|
|
301
|
+
* @param obj - Object to validate
|
|
302
|
+
* @param specifier - Original specifier for error messages
|
|
303
|
+
* @throws ReporterValidationError if validation fails
|
|
304
|
+
*/
|
|
305
|
+
/**
|
|
306
|
+
* Type signature for the validateReporter assertion function
|
|
307
|
+
*/
|
|
308
|
+
type ValidateReporterFn = (
|
|
309
|
+
obj: unknown,
|
|
310
|
+
specifier: string,
|
|
311
|
+
) => asserts obj is Reporter;
|
|
312
|
+
|
|
313
|
+
const validateReporter: ValidateReporterFn = (obj, specifier) => {
|
|
314
|
+
const missing = getMissingMethods(obj);
|
|
315
|
+
|
|
316
|
+
if (missing.length > 0) {
|
|
317
|
+
throw new ReporterValidationError(
|
|
318
|
+
'Module does not implement Reporter interface.',
|
|
319
|
+
specifier,
|
|
320
|
+
missing,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
};
|
package/src/types/index.ts
CHANGED
|
@@ -13,6 +13,9 @@ export { createRunId, createTaskId } from './core.js';
|
|
|
13
13
|
// Interface contracts
|
|
14
14
|
export type * from './interfaces.js';
|
|
15
15
|
|
|
16
|
+
// Plugin types (for third-party reporter authors)
|
|
17
|
+
export type * from './plugin.js';
|
|
18
|
+
|
|
16
19
|
// Profiler types
|
|
17
20
|
export type * from './profiler.js';
|
|
18
21
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Plugin Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for third-party reporter plugins. These types are exported
|
|
5
|
+
* from the main package for use by plugin authors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Reporter } from './interfaces.js';
|
|
9
|
+
import type { Logger } from './utility.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Class constructor for reporter plugins
|
|
13
|
+
*
|
|
14
|
+
* Plugin authors can export a default class matching this signature. The class
|
|
15
|
+
* constructor receives options from the config file and a context object with
|
|
16
|
+
* utilities.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
*
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import type { Reporter, ReporterContext } from 'modestbench';
|
|
22
|
+
*
|
|
23
|
+
* interface MyReporterOptions {
|
|
24
|
+
* verbose?: boolean;
|
|
25
|
+
* outputFormat?: 'text' | 'markdown';
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* class MyReporter implements Reporter {
|
|
29
|
+
* constructor(
|
|
30
|
+
* private options: MyReporterOptions,
|
|
31
|
+
* private context: ReporterContext,
|
|
32
|
+
* ) {}
|
|
33
|
+
*
|
|
34
|
+
* onStart(run) {
|
|
35
|
+
* if (this.options.verbose) console.log('Starting');
|
|
36
|
+
* }
|
|
37
|
+
* onEnd(run) {
|
|
38
|
+
* console.log('Done');
|
|
39
|
+
* }
|
|
40
|
+
* onError(error) {
|
|
41
|
+
* console.error(error);
|
|
42
|
+
* }
|
|
43
|
+
* onTaskResult(result) {
|
|
44
|
+
* console.log(
|
|
45
|
+
* `${result.name}: ${this.context.utils.formatDuration(result.mean)}`,
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* export default MyReporter;
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @typeParam TOptions - The shape of the options object (defaults to
|
|
54
|
+
* Record<string, unknown>)
|
|
55
|
+
*/
|
|
56
|
+
export interface ReporterClass<
|
|
57
|
+
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
58
|
+
> {
|
|
59
|
+
new (options?: TOptions, context?: ReporterContext): Reporter;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Context provided to reporter plugins
|
|
64
|
+
*
|
|
65
|
+
* Contains version information and utility functions that plugins can use.
|
|
66
|
+
*/
|
|
67
|
+
export interface ReporterContext {
|
|
68
|
+
/**
|
|
69
|
+
* Logger for reporter output
|
|
70
|
+
*
|
|
71
|
+
* Use this instead of console.log/console.error to ensure output respects the
|
|
72
|
+
* user's verbosity settings and uses the correct output streams.
|
|
73
|
+
*/
|
|
74
|
+
readonly logger: Logger;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Plugin API version
|
|
78
|
+
*
|
|
79
|
+
* Incremented when breaking changes are made to the plugin API. Currently
|
|
80
|
+
* version 1.
|
|
81
|
+
*/
|
|
82
|
+
readonly pluginApiVersion: number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Utility functions for formatting benchmark data
|
|
86
|
+
*/
|
|
87
|
+
readonly utils: ReporterUtils;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* ModestBench version
|
|
91
|
+
*
|
|
92
|
+
* Plugins can use this to check compatibility.
|
|
93
|
+
*/
|
|
94
|
+
readonly version: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Factory function for creating reporter instances
|
|
99
|
+
*
|
|
100
|
+
* Plugin authors can export a default function matching this signature. The
|
|
101
|
+
* function receives options from the config file and a context object with
|
|
102
|
+
* utilities. Use the generic parameter to define the shape of your options.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
*
|
|
106
|
+
* ```typescript
|
|
107
|
+
* import type { ReporterFactory } from 'modestbench';
|
|
108
|
+
*
|
|
109
|
+
* interface MyReporterOptions {
|
|
110
|
+
* verbose?: boolean;
|
|
111
|
+
* outputFormat?: 'text' | 'markdown';
|
|
112
|
+
* }
|
|
113
|
+
*
|
|
114
|
+
* const createReporter: ReporterFactory<MyReporterOptions> = (
|
|
115
|
+
* options,
|
|
116
|
+
* context,
|
|
117
|
+
* ) => {
|
|
118
|
+
* return {
|
|
119
|
+
* onStart(run) {
|
|
120
|
+
* if (options.verbose) console.log('Starting');
|
|
121
|
+
* },
|
|
122
|
+
* onEnd(run) {
|
|
123
|
+
* console.log('Done');
|
|
124
|
+
* },
|
|
125
|
+
* onError(error) {
|
|
126
|
+
* console.error(error);
|
|
127
|
+
* },
|
|
128
|
+
* onTaskResult(result) {
|
|
129
|
+
* console.log(
|
|
130
|
+
* `${result.name}: ${context.utils.formatDuration(result.mean)}`,
|
|
131
|
+
* );
|
|
132
|
+
* },
|
|
133
|
+
* };
|
|
134
|
+
* };
|
|
135
|
+
*
|
|
136
|
+
* export default createReporter;
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* @typeParam TOptions - The shape of the options object (defaults to
|
|
140
|
+
* Record<string, unknown>)
|
|
141
|
+
*/
|
|
142
|
+
export type ReporterFactory<
|
|
143
|
+
TOptions extends Record<string, unknown> = Record<string, unknown>,
|
|
144
|
+
> = (
|
|
145
|
+
options: TOptions,
|
|
146
|
+
context: ReporterContext,
|
|
147
|
+
) => Promise<Reporter> | Reporter;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Union type representing all valid reporter plugin exports
|
|
151
|
+
*
|
|
152
|
+
* A reporter plugin module can export:
|
|
153
|
+
*
|
|
154
|
+
* - A plain Reporter object (simplest form, no options)
|
|
155
|
+
* - A ReporterClass constructor (instantiated with options)
|
|
156
|
+
* - A ReporterFactory function (most flexible, supports async)
|
|
157
|
+
*/
|
|
158
|
+
export type ReporterPlugin = Reporter | ReporterClass | ReporterFactory;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Utility functions available to reporter plugins
|
|
162
|
+
*
|
|
163
|
+
* These functions help format benchmark data consistently.
|
|
164
|
+
*/
|
|
165
|
+
export interface ReporterUtils {
|
|
166
|
+
/**
|
|
167
|
+
* Format bytes in human-readable format
|
|
168
|
+
*
|
|
169
|
+
* @param bytes - Number of bytes
|
|
170
|
+
* @returns Formatted string (e.g., "1.5 GB", "256 MB")
|
|
171
|
+
*/
|
|
172
|
+
formatBytes(bytes: number): string;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Format duration in human-readable format
|
|
176
|
+
*
|
|
177
|
+
* @param nanoseconds - Duration in nanoseconds
|
|
178
|
+
* @returns Formatted string (e.g., "1.23ms", "456.78μs")
|
|
179
|
+
*/
|
|
180
|
+
formatDuration(nanoseconds: number): string;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Format operations per second
|
|
184
|
+
*
|
|
185
|
+
* @param opsPerSecond - Operations per second
|
|
186
|
+
* @returns Formatted string (e.g., "1.2M ops/sec", "456K ops/sec")
|
|
187
|
+
*/
|
|
188
|
+
formatOpsPerSecond(opsPerSecond: number): string;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Format percentage value
|
|
192
|
+
*
|
|
193
|
+
* @param value - Percentage value
|
|
194
|
+
* @returns Formatted string (e.g., "12.34%")
|
|
195
|
+
*/
|
|
196
|
+
formatPercentage(value: number): string;
|
|
197
|
+
}
|
package/src/utils/package.ts
CHANGED
|
@@ -6,8 +6,39 @@
|
|
|
6
6
|
* @packageDocumentation
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
9
10
|
import { readFile } from 'node:fs/promises';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
+
import path, { dirname, join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Cached package version, loaded at module initialization
|
|
16
|
+
*
|
|
17
|
+
* NOTE: This relies on package.json being at the same relative path from both
|
|
18
|
+
* src/ and dist/ directories (../../package.json). If the build output
|
|
19
|
+
* structure changes, this will break.
|
|
20
|
+
*/
|
|
21
|
+
const cachedPackageVersion = (() => {
|
|
22
|
+
try {
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
25
|
+
const pkgContent = readFileSync(pkgPath, 'utf8');
|
|
26
|
+
const pkg = JSON.parse(pkgContent) as { version: string };
|
|
27
|
+
return pkg.version;
|
|
28
|
+
} catch {
|
|
29
|
+
// Fallback if package.json cannot be read (shouldn't happen in normal use)
|
|
30
|
+
return 'unknown';
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the ModestBench package version
|
|
36
|
+
*
|
|
37
|
+
* @returns The version string from package.json
|
|
38
|
+
*/
|
|
39
|
+
export const getPackageVersion = (): string => {
|
|
40
|
+
return cachedPackageVersion;
|
|
41
|
+
};
|
|
11
42
|
|
|
12
43
|
/**
|
|
13
44
|
* Find the nearest package.json and return its directory
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Reporter Utilities
|
|
3
|
+
*
|
|
4
|
+
* Formatting functions for benchmark data, exported for use by third-party
|
|
5
|
+
* reporter plugins.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ReporterUtils } from '../types/plugin.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format bytes in human-readable format
|
|
12
|
+
*
|
|
13
|
+
* @param bytes - Number of bytes
|
|
14
|
+
* @returns Formatted string (e.g., "1.5 GB", "256 MB", "1.2 KB")
|
|
15
|
+
*/
|
|
16
|
+
export const formatBytes = (bytes: number): string => {
|
|
17
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
18
|
+
let size = bytes;
|
|
19
|
+
let unitIndex = 0;
|
|
20
|
+
|
|
21
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
22
|
+
size /= 1024;
|
|
23
|
+
unitIndex++;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format duration in human-readable format
|
|
31
|
+
*
|
|
32
|
+
* @param nanoseconds - Duration in nanoseconds
|
|
33
|
+
* @returns Formatted string (e.g., "1.23ms", "456.78μs", "789.00ns")
|
|
34
|
+
*/
|
|
35
|
+
export const formatDuration = (nanoseconds: number): string => {
|
|
36
|
+
if (nanoseconds < 1000) {
|
|
37
|
+
return `${nanoseconds.toFixed(2)}ns`;
|
|
38
|
+
} else if (nanoseconds < 1_000_000) {
|
|
39
|
+
return `${(nanoseconds / 1000).toFixed(2)}μs`;
|
|
40
|
+
} else if (nanoseconds < 1_000_000_000) {
|
|
41
|
+
return `${(nanoseconds / 1_000_000).toFixed(2)}ms`;
|
|
42
|
+
} else {
|
|
43
|
+
return `${(nanoseconds / 1_000_000_000).toFixed(2)}s`;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format operations per second in human-readable format
|
|
49
|
+
*
|
|
50
|
+
* @param opsPerSecond - Operations per second
|
|
51
|
+
* @returns Formatted string (e.g., "1.2M ops/sec", "456K ops/sec")
|
|
52
|
+
*/
|
|
53
|
+
export const formatOpsPerSecond = (opsPerSecond: number): string => {
|
|
54
|
+
if (opsPerSecond < 1000) {
|
|
55
|
+
return `${opsPerSecond.toFixed(2)} ops/sec`;
|
|
56
|
+
} else if (opsPerSecond < 1_000_000) {
|
|
57
|
+
return `${(opsPerSecond / 1000).toFixed(2)}K ops/sec`;
|
|
58
|
+
} else if (opsPerSecond < 1_000_000_000) {
|
|
59
|
+
return `${(opsPerSecond / 1_000_000).toFixed(2)}M ops/sec`;
|
|
60
|
+
} else {
|
|
61
|
+
return `${(opsPerSecond / 1_000_000_000).toFixed(2)}B ops/sec`;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format percentage value
|
|
67
|
+
*
|
|
68
|
+
* @param value - Percentage value (e.g., 12.345 for 12.345%)
|
|
69
|
+
* @returns Formatted string (e.g., "12.35%")
|
|
70
|
+
*/
|
|
71
|
+
export const formatPercentage = (value: number): string => {
|
|
72
|
+
return `${value.toFixed(2)}%`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reporter utilities object implementing the ReporterUtils interface
|
|
77
|
+
*
|
|
78
|
+
* This object is provided to reporter plugins via the ReporterContext.
|
|
79
|
+
*/
|
|
80
|
+
export const reporterUtils: ReporterUtils = {
|
|
81
|
+
formatBytes,
|
|
82
|
+
formatDuration,
|
|
83
|
+
formatOpsPerSecond,
|
|
84
|
+
formatPercentage,
|
|
85
|
+
};
|