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,42 @@
1
+ /**
2
+ * ModestBench Reporter Utilities
3
+ *
4
+ * Formatting functions for benchmark data, exported for use by third-party
5
+ * reporter plugins.
6
+ */
7
+ import type { ReporterUtils } from "../types/plugin.cjs";
8
+ /**
9
+ * Format bytes in human-readable format
10
+ *
11
+ * @param bytes - Number of bytes
12
+ * @returns Formatted string (e.g., "1.5 GB", "256 MB", "1.2 KB")
13
+ */
14
+ export declare const formatBytes: (bytes: number) => string;
15
+ /**
16
+ * Format duration in human-readable format
17
+ *
18
+ * @param nanoseconds - Duration in nanoseconds
19
+ * @returns Formatted string (e.g., "1.23ms", "456.78μs", "789.00ns")
20
+ */
21
+ export declare const formatDuration: (nanoseconds: number) => string;
22
+ /**
23
+ * Format operations per second in human-readable format
24
+ *
25
+ * @param opsPerSecond - Operations per second
26
+ * @returns Formatted string (e.g., "1.2M ops/sec", "456K ops/sec")
27
+ */
28
+ export declare const formatOpsPerSecond: (opsPerSecond: number) => string;
29
+ /**
30
+ * Format percentage value
31
+ *
32
+ * @param value - Percentage value (e.g., 12.345 for 12.345%)
33
+ * @returns Formatted string (e.g., "12.35%")
34
+ */
35
+ export declare const formatPercentage: (value: number) => string;
36
+ /**
37
+ * Reporter utilities object implementing the ReporterUtils interface
38
+ *
39
+ * This object is provided to reporter plugins via the ReporterContext.
40
+ */
41
+ export declare const reporterUtils: ReporterUtils;
42
+ //# sourceMappingURL=reporter-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter-utils.d.ts","sourceRoot":"","sources":["../../src/utils/reporter-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,4BAA2B;AAExD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAW3C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,aAAa,MAAM,KAAG,MAUpD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,cAAc,MAAM,KAAG,MAUzD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,MAEhD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,aAK3B,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ModestBench Reporter Utilities
3
+ *
4
+ * Formatting functions for benchmark data, exported for use by third-party
5
+ * reporter plugins.
6
+ */
7
+ import type { ReporterUtils } from "../types/plugin.js";
8
+ /**
9
+ * Format bytes in human-readable format
10
+ *
11
+ * @param bytes - Number of bytes
12
+ * @returns Formatted string (e.g., "1.5 GB", "256 MB", "1.2 KB")
13
+ */
14
+ export declare const formatBytes: (bytes: number) => string;
15
+ /**
16
+ * Format duration in human-readable format
17
+ *
18
+ * @param nanoseconds - Duration in nanoseconds
19
+ * @returns Formatted string (e.g., "1.23ms", "456.78μs", "789.00ns")
20
+ */
21
+ export declare const formatDuration: (nanoseconds: number) => string;
22
+ /**
23
+ * Format operations per second in human-readable format
24
+ *
25
+ * @param opsPerSecond - Operations per second
26
+ * @returns Formatted string (e.g., "1.2M ops/sec", "456K ops/sec")
27
+ */
28
+ export declare const formatOpsPerSecond: (opsPerSecond: number) => string;
29
+ /**
30
+ * Format percentage value
31
+ *
32
+ * @param value - Percentage value (e.g., 12.345 for 12.345%)
33
+ * @returns Formatted string (e.g., "12.35%")
34
+ */
35
+ export declare const formatPercentage: (value: number) => string;
36
+ /**
37
+ * Reporter utilities object implementing the ReporterUtils interface
38
+ *
39
+ * This object is provided to reporter plugins via the ReporterContext.
40
+ */
41
+ export declare const reporterUtils: ReporterUtils;
42
+ //# sourceMappingURL=reporter-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter-utils.d.ts","sourceRoot":"","sources":["../../src/utils/reporter-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,2BAA2B;AAExD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAW3C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,aAAa,MAAM,KAAG,MAUpD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,cAAc,MAAM,KAAG,MAUzD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,MAEhD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,aAK3B,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ModestBench Reporter Utilities
3
+ *
4
+ * Formatting functions for benchmark data, exported for use by third-party
5
+ * reporter plugins.
6
+ */
7
+ /**
8
+ * Format bytes in human-readable format
9
+ *
10
+ * @param bytes - Number of bytes
11
+ * @returns Formatted string (e.g., "1.5 GB", "256 MB", "1.2 KB")
12
+ */
13
+ export const formatBytes = (bytes) => {
14
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
15
+ let size = bytes;
16
+ let unitIndex = 0;
17
+ while (size >= 1024 && unitIndex < units.length - 1) {
18
+ size /= 1024;
19
+ unitIndex++;
20
+ }
21
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
22
+ };
23
+ /**
24
+ * Format duration in human-readable format
25
+ *
26
+ * @param nanoseconds - Duration in nanoseconds
27
+ * @returns Formatted string (e.g., "1.23ms", "456.78μs", "789.00ns")
28
+ */
29
+ export const formatDuration = (nanoseconds) => {
30
+ if (nanoseconds < 1000) {
31
+ return `${nanoseconds.toFixed(2)}ns`;
32
+ }
33
+ else if (nanoseconds < 1_000_000) {
34
+ return `${(nanoseconds / 1000).toFixed(2)}μs`;
35
+ }
36
+ else if (nanoseconds < 1_000_000_000) {
37
+ return `${(nanoseconds / 1_000_000).toFixed(2)}ms`;
38
+ }
39
+ else {
40
+ return `${(nanoseconds / 1_000_000_000).toFixed(2)}s`;
41
+ }
42
+ };
43
+ /**
44
+ * Format operations per second in human-readable format
45
+ *
46
+ * @param opsPerSecond - Operations per second
47
+ * @returns Formatted string (e.g., "1.2M ops/sec", "456K ops/sec")
48
+ */
49
+ export const formatOpsPerSecond = (opsPerSecond) => {
50
+ if (opsPerSecond < 1000) {
51
+ return `${opsPerSecond.toFixed(2)} ops/sec`;
52
+ }
53
+ else if (opsPerSecond < 1_000_000) {
54
+ return `${(opsPerSecond / 1000).toFixed(2)}K ops/sec`;
55
+ }
56
+ else if (opsPerSecond < 1_000_000_000) {
57
+ return `${(opsPerSecond / 1_000_000).toFixed(2)}M ops/sec`;
58
+ }
59
+ else {
60
+ return `${(opsPerSecond / 1_000_000_000).toFixed(2)}B ops/sec`;
61
+ }
62
+ };
63
+ /**
64
+ * Format percentage value
65
+ *
66
+ * @param value - Percentage value (e.g., 12.345 for 12.345%)
67
+ * @returns Formatted string (e.g., "12.35%")
68
+ */
69
+ export const formatPercentage = (value) => {
70
+ return `${value.toFixed(2)}%`;
71
+ };
72
+ /**
73
+ * Reporter utilities object implementing the ReporterUtils interface
74
+ *
75
+ * This object is provided to reporter plugins via the ReporterContext.
76
+ */
77
+ export const reporterUtils = {
78
+ formatBytes,
79
+ formatDuration,
80
+ formatOpsPerSecond,
81
+ formatPercentage,
82
+ };
83
+ //# sourceMappingURL=reporter-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter-utils.js","sourceRoot":"","sources":["../../src/utils/reporter-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAa,EAAU,EAAE;IACnD,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC5C,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,OAAO,IAAI,IAAI,IAAI,IAAI,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpD,IAAI,IAAI,IAAI,CAAC;QACb,SAAS,EAAE,CAAC;IACd,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;AAClD,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,WAAmB,EAAU,EAAE;IAC5D,IAAI,WAAW,GAAG,IAAI,EAAE,CAAC;QACvB,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACvC,CAAC;SAAM,IAAI,WAAW,GAAG,SAAS,EAAE,CAAC;QACnC,OAAO,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAChD,CAAC;SAAM,IAAI,WAAW,GAAG,aAAa,EAAE,CAAC;QACvC,OAAO,GAAG,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACrD,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,CAAC,WAAW,GAAG,aAAa,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IACxD,CAAC;AACH,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,YAAoB,EAAU,EAAE;IACjE,IAAI,YAAY,GAAG,IAAI,EAAE,CAAC;QACxB,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;IAC9C,CAAC;SAAM,IAAI,YAAY,GAAG,SAAS,EAAE,CAAC;QACpC,OAAO,GAAG,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC;IACxD,CAAC;SAAM,IAAI,YAAY,GAAG,aAAa,EAAE,CAAC;QACxC,OAAO,GAAG,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC;IAC7D,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,CAAC,YAAY,GAAG,aAAa,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC;IACjE,CAAC;AACH,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAa,EAAU,EAAE;IACxD,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;AAChC,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAkB;IAC1C,WAAW;IACX,cAAc;IACd,kBAAkB;IAClB,gBAAgB;CACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modestbench",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "A full-ass benchmarking framework for Node.js",
6
6
  "repository": {
@@ -94,7 +94,7 @@
94
94
  "clean": "rm -rf dist",
95
95
  "commitlint": "commitlint",
96
96
  "dev": "zshy --watch",
97
- "docs:build": "run-s docs:copy astro:build",
97
+ "docs:build": "run-s build:schema docs:copy astro:build",
98
98
  "docs:copy": "tsx scripts/copy-docs.ts",
99
99
  "docs:dev": "run-s docs:copy astro:dev",
100
100
  "docs:preview": "astro preview --config astro.config.js",
@@ -132,7 +132,7 @@
132
132
  "zod": "4.1.13"
133
133
  },
134
134
  "devDependencies": {
135
- "@astrojs/mdx": "4.3.12",
135
+ "@astrojs/mdx": "4.3.13",
136
136
  "@astrojs/starlight": "0.37.1",
137
137
  "@commitlint/cli": "20.2.0",
138
138
  "@commitlint/config-conventional": "20.2.0",
@@ -144,8 +144,8 @@
144
144
  "@types/node": "24.10.3",
145
145
  "@types/wallabyjs": "0.0.15",
146
146
  "@types/yargs": "17.0.35",
147
- "asciinema-player": "3.12.1",
148
- "astro": "5.16.4",
147
+ "asciinema-player": "3.13.2",
148
+ "astro": "5.16.5",
149
149
  "astro-broken-link-checker": "file:./vendor/astro-broken-link-checker",
150
150
  "ava": "6.4.1",
151
151
  "bupkis": "0.14.0",
@@ -156,7 +156,7 @@
156
156
  "globals": "16.5.0",
157
157
  "husky": "9.1.7",
158
158
  "jest": "30.2.0",
159
- "knip": "5.73.1",
159
+ "knip": "5.73.4",
160
160
  "lint-staged": "16.2.7",
161
161
  "markdownlint-cli2": "0.20.0",
162
162
  "markdownlint-cli2-formatter-pretty": "0.0.9",
@@ -7,21 +7,32 @@
7
7
 
8
8
  import { resolve } from 'node:path';
9
9
 
10
- import type { BenchmarkRun, ModestBenchConfig } from '../../types/index.js';
10
+ import type {
11
+ BenchmarkRun,
12
+ ModestBenchConfig,
13
+ Reporter,
14
+ } from '../../types/index.js';
11
15
  import type { CliContext } from '../index.js';
12
16
 
13
- import { ErrorCodes } from '../../constants.js';
17
+ import { ErrorCodes, ExitCodes } from '../../constants.js';
14
18
  import { resolveOutputPath } from '../../core/output-path-resolver.js';
15
19
  import {
16
20
  type BudgetExceededError,
17
21
  InvalidArgumentError,
22
+ ReporterLoadError,
23
+ ReporterValidationError,
18
24
  UnknownReporterError,
19
25
  } from '../../errors/index.js';
20
26
  import { CsvReporter } from '../../reporters/csv.js';
21
27
  import { HumanReporter } from '../../reporters/human.js';
22
28
  import { JsonReporter } from '../../reporters/json.js';
29
+ import { NyanReporter } from '../../reporters/nyan.js';
23
30
  import { SimpleReporter } from '../../reporters/simple.js';
24
- import { ExitCodes } from '../../types/cli.js';
31
+ import {
32
+ isBuiltInReporter,
33
+ isFilePath,
34
+ loadReporter,
35
+ } from '../../services/reporter-loader.js';
25
36
  import { hasErrorCode, isError } from '../../utils/type-guards.js';
26
37
 
27
38
  /**
@@ -106,7 +117,7 @@ export const handleRunCommand = async (
106
117
  if (showCliMessages) {
107
118
  console.error('Setting up reporters...');
108
119
  }
109
- const reporters = setupReporters(
120
+ const reporters = await setupReporters(
110
121
  context,
111
122
  config,
112
123
  verbose,
@@ -370,19 +381,26 @@ const loadConfiguration = async (context: CliContext, options: RunOptions) => {
370
381
 
371
382
  /**
372
383
  * Setup and configure reporters based on configuration
384
+ *
385
+ * Supports built-in reporters, registry-based custom reporters, and external
386
+ * reporters loaded from file paths or npm packages.
373
387
  */
374
- const setupReporters = (
388
+ const setupReporters = async (
375
389
  context: CliContext,
376
- config: { outputDir?: string; reporters?: string[] },
390
+ config: {
391
+ outputDir?: string;
392
+ reporterConfig?: Record<string, unknown>;
393
+ reporters?: string[];
394
+ },
377
395
  isVerbose: boolean,
378
396
  showCliMessages: boolean,
379
397
  explicitQuiet: boolean,
380
398
  explicitOutputDir?: string,
381
399
  explicitOutputFile?: string,
382
400
  progressOption?: boolean,
383
- ) => {
401
+ ): Promise<Reporter[]> => {
384
402
  try {
385
- const reporters = [];
403
+ const reporters: Reporter[] = [];
386
404
  // Dedupe requested reporters
387
405
  const requestedReporters = [...new Set(config.reporters || ['human'])];
388
406
 
@@ -395,63 +413,106 @@ const setupReporters = (
395
413
  : undefined;
396
414
 
397
415
  // Built-in reporter names for error messages
398
- const builtInReporters = ['human', 'json', 'csv', 'simple'];
416
+ const builtInReporters = ['human', 'json', 'csv', 'nyan', 'simple'];
399
417
 
400
418
  for (const reporterName of requestedReporters) {
401
- let reporter;
402
-
403
- // Create reporter instances with output path configuration
404
- switch (reporterName) {
405
- case 'csv': {
406
- const outputPath = resolveOutputPath(
407
- outputDir,
408
- explicitOutputFile,
409
- 'results.csv',
410
- );
411
- reporter = new CsvReporter({
412
- includeHeaders: true,
413
- includeMetadata: true,
414
- ...(outputPath ? { outputPath } : {}),
415
- quiet: explicitQuiet, // Only applies explicit --quiet flag; CSV output can coexist with progress messages on different streams
416
- verbose: isVerbose,
417
- });
418
- break;
419
- }
419
+ let reporter: Reporter;
420
+
421
+ // Check if this is a built-in reporter
422
+ if (isBuiltInReporter(reporterName)) {
423
+ // Create reporter instances with output path configuration
424
+ switch (reporterName) {
425
+ case 'csv': {
426
+ const outputPath = resolveOutputPath(
427
+ outputDir,
428
+ explicitOutputFile,
429
+ 'results.csv',
430
+ );
431
+ reporter = new CsvReporter({
432
+ includeHeaders: true,
433
+ includeMetadata: true,
434
+ ...(outputPath ? { outputPath } : {}),
435
+ quiet: explicitQuiet,
436
+ verbose: isVerbose,
437
+ });
438
+ break;
439
+ }
420
440
 
421
- case 'human':
422
- reporter = new HumanReporter({
423
- color: true,
424
- progress: progressOption ?? true,
425
- quiet: explicitQuiet, // Only applies explicit --quiet flag; JSON reporter forcing quiet mode does not affect HumanReporter progress output
426
- verbose: isVerbose,
427
- });
428
- break;
429
-
430
- case 'json': {
431
- const outputPath = resolveOutputPath(
432
- outputDir,
433
- explicitOutputFile,
434
- 'results.json',
435
- );
436
- reporter = new JsonReporter({
437
- ...(outputPath ? { outputPath } : {}),
438
- prettyPrint: true,
439
- });
440
- break;
441
- }
441
+ case 'human':
442
+ reporter = new HumanReporter({
443
+ color: true,
444
+ progress: progressOption ?? true,
445
+ quiet: explicitQuiet,
446
+ verbose: isVerbose,
447
+ });
448
+ break;
449
+
450
+ case 'json': {
451
+ const outputPath = resolveOutputPath(
452
+ outputDir,
453
+ explicitOutputFile,
454
+ 'results.json',
455
+ );
456
+ reporter = new JsonReporter({
457
+ ...(outputPath ? { outputPath } : {}),
458
+ prettyPrint: true,
459
+ });
460
+ break;
461
+ }
442
462
 
443
- case 'simple':
444
- reporter = new SimpleReporter({
445
- quiet: explicitQuiet,
446
- verbose: isVerbose,
447
- });
448
- break;
449
-
450
- default:
451
- // Fall back to registry for custom reporters
452
- reporter = context.reporterRegistry.get(reporterName);
453
- if (!reporter) {
454
- // Combine built-in reporters with registered custom reporters
463
+ case 'nyan':
464
+ reporter = new NyanReporter({
465
+ color: true,
466
+ quiet: explicitQuiet,
467
+ });
468
+ break;
469
+
470
+ case 'simple':
471
+ reporter = new SimpleReporter({
472
+ quiet: explicitQuiet,
473
+ verbose: isVerbose,
474
+ });
475
+ break;
476
+
477
+ default:
478
+ // TypeScript exhaustiveness check - should never reach here
479
+ throw new Error(`Unhandled built-in reporter: ${reporterName}`);
480
+ }
481
+ } else if (isFilePath(reporterName)) {
482
+ // External reporter from file path
483
+ const reporterOptions =
484
+ (config.reporterConfig?.[reporterName] as Record<string, unknown>) ??
485
+ {};
486
+ if (showCliMessages) {
487
+ console.error(`Loading external reporter: ${reporterName}`);
488
+ }
489
+ reporter = await loadReporter(reporterName, reporterOptions);
490
+ } else {
491
+ // Try registry first, then npm package
492
+ const registryReporter = context.reporterRegistry.get(reporterName);
493
+ if (registryReporter) {
494
+ reporter = registryReporter;
495
+ } else {
496
+ // Try loading as npm package
497
+ const reporterOptions =
498
+ (config.reporterConfig?.[reporterName] as Record<
499
+ string,
500
+ unknown
501
+ >) ?? {};
502
+ if (showCliMessages) {
503
+ console.error(`Loading reporter package: ${reporterName}`);
504
+ }
505
+ try {
506
+ reporter = await loadReporter(reporterName, reporterOptions);
507
+ } catch (error) {
508
+ // If loading fails and it's not a file path, provide helpful error
509
+ if (
510
+ error instanceof ReporterLoadError ||
511
+ error instanceof ReporterValidationError
512
+ ) {
513
+ throw error;
514
+ }
515
+ // Combine built-in reporters with registered custom reporters for error message
455
516
  const registeredReporters = Object.keys(
456
517
  context.reporterRegistry.getAll(),
457
518
  );
@@ -460,10 +521,11 @@ const setupReporters = (
460
521
  ...registeredReporters,
461
522
  ];
462
523
  throw new UnknownReporterError(
463
- `Unknown reporter: ${reporterName}. Available: ${availableReporters.join(', ')}`,
524
+ `Unknown reporter: ${reporterName}. Available built-in reporters: ${availableReporters.join(', ')}. ` +
525
+ `For external reporters, use a file path (./my-reporter.js) or npm package name.`,
464
526
  );
465
527
  }
466
- break;
528
+ }
467
529
  }
468
530
 
469
531
  reporters.push(reporter);
@@ -477,7 +539,11 @@ const setupReporters = (
477
539
  } catch (error) {
478
540
  // Re-throw our custom errors
479
541
  const errorCode = hasErrorCode(error) ? error.code : undefined;
480
- if (errorCode === ErrorCodes.REPORTER_UNKNOWN) {
542
+ if (
543
+ errorCode === ErrorCodes.REPORTER_UNKNOWN ||
544
+ errorCode === ErrorCodes.REPORTER_LOAD_FAILED ||
545
+ errorCode === ErrorCodes.REPORTER_INVALID
546
+ ) {
481
547
  throw error;
482
548
  }
483
549
  throw new InvalidArgumentError(
@@ -5,8 +5,7 @@
5
5
  * executing them through a lightweight benchmark runner.
6
6
  */
7
7
 
8
- import { hostname } from 'node:os';
9
- import { cpus, freemem, totalmem } from 'node:os';
8
+ import { cpus, freemem, hostname, totalmem } from 'node:os';
10
9
  import { resolve } from 'node:path';
11
10
  import { performance } from 'node:perf_hooks';
12
11
 
@@ -31,7 +30,7 @@ import {
31
30
  type ConvertedBenchmarkSuite,
32
31
  type TestFramework,
33
32
  } from '../../adapters/types.js';
34
- import { ExitCodes } from '../../types/cli.js';
33
+ import { ExitCodes } from '../../constants.js';
35
34
  import { createRunId } from '../../types/core.js';
36
35
  import { isError } from '../../utils/type-guards.js';
37
36
 
package/src/cli/index.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  CsvReporter,
40
40
  HumanReporter,
41
41
  JsonReporter,
42
+ NyanReporter,
42
43
  SimpleReporter,
43
44
  } from '../reporters/index.js';
44
45
  // Import commands
@@ -1151,6 +1152,13 @@ const createCliContext = async (
1151
1152
  }),
1152
1153
  );
1153
1154
 
1155
+ engine.registerReporter(
1156
+ 'nyan',
1157
+ new NyanReporter({
1158
+ color: !options.noColor,
1159
+ }),
1160
+ );
1161
+
1154
1162
  return {
1155
1163
  abortController,
1156
1164
  configManager: engine.configManager,
package/src/constants.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Engine } from './types/cli.js';
1
+ import { type Engine } from './types/core.js';
2
2
 
3
3
  /**
4
4
  * Supported benchmark file extensions
@@ -60,6 +60,7 @@ export const Reporters = {
60
60
  CSV: 'csv',
61
61
  HUMAN: 'human',
62
62
  JSON: 'json',
63
+ NYAN: 'nyan',
63
64
  SIMPLE: 'simple',
64
65
  } as const;
65
66
 
@@ -109,6 +110,8 @@ export const ErrorCodes = {
109
110
 
110
111
  //#region reporter-errors
111
112
  REPORTER_ALREADY_REGISTERED: 'ERR_MB_REPORTER_ALREADY_REGISTERED',
113
+ REPORTER_INVALID: 'ERR_MB_REPORTER_INVALID',
114
+ REPORTER_LOAD_FAILED: 'ERR_MB_REPORTER_LOAD_FAILED',
112
115
  REPORTER_OUTPUT_FAILED: 'ERR_MB_REPORTER_OUTPUT_FAILED',
113
116
  REPORTER_UNKNOWN: 'ERR_MB_REPORTER_UNKNOWN',
114
117
  //#endregion
@@ -53,7 +53,9 @@ export {
53
53
  // Reporter errors
54
54
  export {
55
55
  ReporterAlreadyRegisteredError,
56
+ ReporterLoadError,
56
57
  ReporterOutputError,
58
+ ReporterValidationError,
57
59
  UnknownReporterError,
58
60
  } from './reporter.js';
59
61
 
@@ -16,6 +16,26 @@ export class ReporterAlreadyRegisteredError extends ModestBenchError {
16
16
  readonly code = 'ERR_MB_REPORTER_ALREADY_REGISTERED';
17
17
  }
18
18
 
19
+ /**
20
+ * Reporter load failed
21
+ *
22
+ * Thrown when a reporter module cannot be loaded (file not found, syntax error,
23
+ * invalid module format, etc.).
24
+ */
25
+ export class ReporterLoadError extends ModestBenchError {
26
+ readonly code = 'ERR_MB_REPORTER_LOAD_FAILED';
27
+
28
+ /**
29
+ * The specifier (file path or package name) that failed to load
30
+ */
31
+ readonly specifier: string;
32
+
33
+ constructor(message: string, specifier: string, options?: ErrorOptions) {
34
+ super(`Failed to load reporter "${specifier}": ${message}`, options);
35
+ this.specifier = specifier;
36
+ }
37
+ }
38
+
19
39
  /**
20
40
  * Reporter output failed
21
41
  *
@@ -25,6 +45,41 @@ export class ReporterOutputError extends ModestBenchError {
25
45
  readonly code = 'ERR_MB_REPORTER_OUTPUT_FAILED';
26
46
  }
27
47
 
48
+ /**
49
+ * Reporter validation failed
50
+ *
51
+ * Thrown when a loaded module does not implement the required Reporter
52
+ * interface methods.
53
+ */
54
+ export class ReporterValidationError extends ModestBenchError {
55
+ readonly code = 'ERR_MB_REPORTER_INVALID';
56
+
57
+ /**
58
+ * The methods that are missing from the reporter
59
+ */
60
+ readonly missingMethods: string[];
61
+
62
+ /**
63
+ * The specifier (file path or package name) of the invalid reporter
64
+ */
65
+ readonly specifier: string;
66
+
67
+ constructor(
68
+ message: string,
69
+ specifier: string,
70
+ missingMethods: string[] = [],
71
+ options?: ErrorOptions,
72
+ ) {
73
+ const methodsInfo =
74
+ missingMethods.length > 0
75
+ ? ` Missing required methods: ${missingMethods.join(', ')}.`
76
+ : '';
77
+ super(`Invalid reporter "${specifier}": ${message}${methodsInfo}`, options);
78
+ this.specifier = specifier;
79
+ this.missingMethods = missingMethods;
80
+ }
81
+ }
82
+
28
83
  /**
29
84
  * Unknown reporter
30
85
  *
package/src/index.ts CHANGED
@@ -8,6 +8,9 @@
8
8
 
9
9
  export { bootstrap as modestbench } from './bootstrap.js';
10
10
 
11
+ // Constants
12
+ export { ExitCodes } from './constants.js';
13
+
11
14
  // Core engine
12
15
  export { ModestBenchEngine } from './core/engine.js';
13
16
  export { AccurateEngine, TinybenchEngine } from './core/engines/index.js';
@@ -38,6 +41,15 @@ export { parseProfile } from './services/profiler/profile-parser.js';
38
41
 
39
42
  export { runWithProfiling } from './services/profiler/profile-runner.js';
40
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
+
41
53
  export {
42
54
  BaseReporter,
43
55
  CompositeReporter,
@@ -48,4 +60,13 @@ export {
48
60
  export * from './types/index.js';
49
61
 
50
62
  // Utilities
51
- 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';