modestbench 0.6.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.
Files changed (139) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +6 -2
  3. package/dist/cli/commands/run.cjs +100 -54
  4. package/dist/cli/commands/run.cjs.map +1 -1
  5. package/dist/cli/commands/run.d.cts.map +1 -1
  6. package/dist/cli/commands/run.d.ts.map +1 -1
  7. package/dist/cli/commands/run.js +93 -47
  8. package/dist/cli/commands/run.js.map +1 -1
  9. package/dist/cli/commands/test.cjs +14 -15
  10. package/dist/cli/commands/test.cjs.map +1 -1
  11. package/dist/cli/commands/test.d.cts.map +1 -1
  12. package/dist/cli/commands/test.d.ts.map +1 -1
  13. package/dist/cli/commands/test.js +2 -3
  14. package/dist/cli/commands/test.js.map +1 -1
  15. package/dist/cli/index.cjs +3 -0
  16. package/dist/cli/index.cjs.map +1 -1
  17. package/dist/cli/index.d.cts.map +1 -1
  18. package/dist/cli/index.d.ts.map +1 -1
  19. package/dist/cli/index.js +4 -1
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/constants.cjs +3 -0
  22. package/dist/constants.cjs.map +1 -1
  23. package/dist/constants.d.cts +3 -0
  24. package/dist/constants.d.cts.map +1 -1
  25. package/dist/constants.d.ts +3 -0
  26. package/dist/constants.d.ts.map +1 -1
  27. package/dist/constants.js +3 -0
  28. package/dist/constants.js.map +1 -1
  29. package/dist/errors/index.cjs +3 -1
  30. package/dist/errors/index.cjs.map +1 -1
  31. package/dist/errors/index.d.cts +1 -1
  32. package/dist/errors/index.d.cts.map +1 -1
  33. package/dist/errors/index.d.ts +1 -1
  34. package/dist/errors/index.d.ts.map +1 -1
  35. package/dist/errors/index.js +1 -1
  36. package/dist/errors/index.js.map +1 -1
  37. package/dist/errors/reporter.cjs +45 -1
  38. package/dist/errors/reporter.cjs.map +1 -1
  39. package/dist/errors/reporter.d.cts +32 -0
  40. package/dist/errors/reporter.d.cts.map +1 -1
  41. package/dist/errors/reporter.d.ts +32 -0
  42. package/dist/errors/reporter.d.ts.map +1 -1
  43. package/dist/errors/reporter.js +42 -0
  44. package/dist/errors/reporter.js.map +1 -1
  45. package/dist/index.cjs +19 -1
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +4 -1
  48. package/dist/index.d.cts.map +1 -1
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +7 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/reporters/index.cjs +3 -1
  54. package/dist/reporters/index.cjs.map +1 -1
  55. package/dist/reporters/index.d.cts +1 -0
  56. package/dist/reporters/index.d.cts.map +1 -1
  57. package/dist/reporters/index.d.ts +1 -0
  58. package/dist/reporters/index.d.ts.map +1 -1
  59. package/dist/reporters/index.js +1 -0
  60. package/dist/reporters/index.js.map +1 -1
  61. package/dist/reporters/nyan.cjs +318 -0
  62. package/dist/reporters/nyan.cjs.map +1 -0
  63. package/dist/reporters/nyan.d.cts +118 -0
  64. package/dist/reporters/nyan.d.cts.map +1 -0
  65. package/dist/reporters/nyan.d.ts +118 -0
  66. package/dist/reporters/nyan.d.ts.map +1 -0
  67. package/dist/reporters/nyan.js +314 -0
  68. package/dist/reporters/nyan.js.map +1 -0
  69. package/dist/services/reporter-loader.cjs +281 -0
  70. package/dist/services/reporter-loader.cjs.map +1 -0
  71. package/dist/services/reporter-loader.d.cts +67 -0
  72. package/dist/services/reporter-loader.d.cts.map +1 -0
  73. package/dist/services/reporter-loader.d.ts +67 -0
  74. package/dist/services/reporter-loader.d.ts.map +1 -0
  75. package/dist/services/reporter-loader.js +241 -0
  76. package/dist/services/reporter-loader.js.map +1 -0
  77. package/dist/types/core.cjs.map +1 -1
  78. package/dist/types/core.d.cts +13 -12
  79. package/dist/types/core.d.cts.map +1 -1
  80. package/dist/types/core.d.ts +13 -12
  81. package/dist/types/core.d.ts.map +1 -1
  82. package/dist/types/core.js.map +1 -1
  83. package/dist/types/index.cjs +0 -2
  84. package/dist/types/index.cjs.map +1 -1
  85. package/dist/types/index.d.cts +1 -1
  86. package/dist/types/index.d.cts.map +1 -1
  87. package/dist/types/index.d.ts +1 -1
  88. package/dist/types/index.d.ts.map +1 -1
  89. package/dist/types/index.js +0 -2
  90. package/dist/types/index.js.map +1 -1
  91. package/dist/types/plugin.cjs +9 -0
  92. package/dist/types/plugin.cjs.map +1 -0
  93. package/dist/types/plugin.d.cts +179 -0
  94. package/dist/types/plugin.d.cts.map +1 -0
  95. package/dist/types/plugin.d.ts +179 -0
  96. package/dist/types/plugin.d.ts.map +1 -0
  97. package/dist/types/plugin.js +8 -0
  98. package/dist/types/plugin.js.map +1 -0
  99. package/dist/utils/package.cjs +66 -5
  100. package/dist/utils/package.cjs.map +1 -1
  101. package/dist/utils/package.d.cts +6 -0
  102. package/dist/utils/package.d.cts.map +1 -1
  103. package/dist/utils/package.d.ts +6 -0
  104. package/dist/utils/package.d.ts.map +1 -1
  105. package/dist/utils/package.js +31 -1
  106. package/dist/utils/package.js.map +1 -1
  107. package/dist/utils/reporter-utils.cjs +90 -0
  108. package/dist/utils/reporter-utils.cjs.map +1 -0
  109. package/dist/utils/reporter-utils.d.cts +42 -0
  110. package/dist/utils/reporter-utils.d.cts.map +1 -0
  111. package/dist/utils/reporter-utils.d.ts +42 -0
  112. package/dist/utils/reporter-utils.d.ts.map +1 -0
  113. package/dist/utils/reporter-utils.js +83 -0
  114. package/dist/utils/reporter-utils.js.map +1 -0
  115. package/package.json +6 -6
  116. package/src/cli/commands/run.ts +130 -64
  117. package/src/cli/commands/test.ts +2 -3
  118. package/src/cli/index.ts +8 -0
  119. package/src/constants.ts +4 -1
  120. package/src/errors/index.ts +2 -0
  121. package/src/errors/reporter.ts +55 -0
  122. package/src/index.ts +22 -1
  123. package/src/reporters/index.ts +1 -0
  124. package/src/reporters/nyan.ts +409 -0
  125. package/src/services/reporter-loader.ts +323 -0
  126. package/src/types/core.ts +16 -14
  127. package/src/types/index.ts +3 -3
  128. package/src/types/plugin.ts +197 -0
  129. package/src/utils/package.ts +32 -1
  130. package/src/utils/reporter-utils.ts +85 -0
  131. package/dist/types/cli.cjs +0 -12
  132. package/dist/types/cli.cjs.map +0 -1
  133. package/dist/types/cli.d.cts +0 -75
  134. package/dist/types/cli.d.cts.map +0 -1
  135. package/dist/types/cli.d.ts +0 -75
  136. package/dist/types/cli.d.ts.map +0 -1
  137. package/dist/types/cli.js +0 -9
  138. package/dist/types/cli.js.map +0 -1
  139. package/src/types/cli.ts +0 -82
@@ -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/core.ts CHANGED
@@ -37,6 +37,20 @@ export type {
37
37
  BenchmarkTaskInput,
38
38
  } from '../core/benchmark-schema.js';
39
39
 
40
+ /**
41
+ * Benchmark file structure after parsing
42
+ */
43
+ export interface BenchmarkFile {
44
+ /** Raw file content */
45
+ readonly content: string;
46
+ /** Parsed exports from the file */
47
+ readonly exports: unknown;
48
+ /** Absolute path to the file */
49
+ readonly filePath: string;
50
+ /** File metadata */
51
+ readonly metadata: FileMetadata;
52
+ }
53
+
40
54
  /**
41
55
  * ModestBench Core Types
42
56
  *
@@ -52,20 +66,6 @@ export type {
52
66
  // Re-export identifier helper functions
53
67
  export { createRunId, createTaskId } from '../utils/identifiers.js';
54
68
 
55
- /**
56
- * Benchmark file structure after parsing
57
- */
58
- export interface BenchmarkFile {
59
- /** Raw file content */
60
- readonly content: string;
61
- /** Parsed exports from the file */
62
- readonly exports: unknown;
63
- /** Absolute path to the file */
64
- readonly filePath: string;
65
- /** File metadata */
66
- readonly metadata: FileMetadata;
67
- }
68
-
69
69
  /**
70
70
  * Represents a complete benchmark run across multiple files
71
71
  */
@@ -128,6 +128,8 @@ export interface CpuInfo {
128
128
  readonly speed: number;
129
129
  }
130
130
 
131
+ export type Engine = 'accurate' | 'tinybench';
132
+
131
133
  /**
132
134
  * Environment information captured during benchmark execution
133
135
  */
@@ -5,9 +5,6 @@
5
5
  * This file re-exports all types from the individual type modules.
6
6
  */
7
7
 
8
- // CLI-specific types
9
- export * from './cli.js';
10
-
11
8
  // Core data types
12
9
  export type * from './core.js';
13
10
  // Helper functions from core (value exports)
@@ -16,6 +13,9 @@ export { createRunId, createTaskId } from './core.js';
16
13
  // Interface contracts
17
14
  export type * from './interfaces.js';
18
15
 
16
+ // Plugin types (for third-party reporter authors)
17
+ export type * from './plugin.js';
18
+
19
19
  // Profiler types
20
20
  export type * from './profiler.js';
21
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
+ }
@@ -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
+ };