modestbench 0.2.0 → 0.3.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 (332) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +131 -34
  3. package/dist/cli/commands/analyze.cjs +60 -0
  4. package/dist/cli/commands/analyze.cjs.map +1 -0
  5. package/dist/cli/commands/analyze.d.cts +35 -0
  6. package/dist/cli/commands/analyze.d.cts.map +1 -0
  7. package/dist/cli/commands/analyze.d.ts +35 -0
  8. package/dist/cli/commands/analyze.d.ts.map +1 -0
  9. package/dist/cli/commands/analyze.js +56 -0
  10. package/dist/cli/commands/analyze.js.map +1 -0
  11. package/dist/cli/commands/baseline.cjs +404 -0
  12. package/dist/cli/commands/baseline.cjs.map +1 -0
  13. package/dist/cli/commands/baseline.d.cts +72 -0
  14. package/dist/cli/commands/baseline.d.cts.map +1 -0
  15. package/dist/cli/commands/baseline.d.ts +72 -0
  16. package/dist/cli/commands/baseline.d.ts.map +1 -0
  17. package/dist/cli/commands/baseline.js +396 -0
  18. package/dist/cli/commands/baseline.js.map +1 -0
  19. package/dist/cli/commands/history.d.cts +1 -1
  20. package/dist/cli/commands/history.d.cts.map +1 -1
  21. package/dist/cli/commands/history.d.ts +1 -1
  22. package/dist/cli/commands/history.d.ts.map +1 -1
  23. package/dist/cli/commands/init.cjs +88 -155
  24. package/dist/cli/commands/init.cjs.map +1 -1
  25. package/dist/cli/commands/init.d.cts +4 -4
  26. package/dist/cli/commands/init.d.cts.map +1 -1
  27. package/dist/cli/commands/init.d.ts +4 -4
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +88 -155
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/run.cjs +132 -114
  32. package/dist/cli/commands/run.cjs.map +1 -1
  33. package/dist/cli/commands/run.d.cts +16 -3
  34. package/dist/cli/commands/run.d.cts.map +1 -1
  35. package/dist/cli/commands/run.d.ts +16 -3
  36. package/dist/cli/commands/run.d.ts.map +1 -1
  37. package/dist/cli/commands/run.js +131 -80
  38. package/dist/cli/commands/run.js.map +1 -1
  39. package/dist/cli/index.cjs +583 -394
  40. package/dist/cli/index.cjs.map +1 -1
  41. package/dist/cli/index.d.cts +4 -16
  42. package/dist/cli/index.d.cts.map +1 -1
  43. package/dist/cli/index.d.ts +4 -16
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +575 -386
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/config/budget-schema.cjs +172 -0
  48. package/dist/config/budget-schema.cjs.map +1 -0
  49. package/dist/config/budget-schema.d.cts +59 -0
  50. package/dist/config/budget-schema.d.cts.map +1 -0
  51. package/dist/config/budget-schema.d.ts +59 -0
  52. package/dist/config/budget-schema.d.ts.map +1 -0
  53. package/dist/config/budget-schema.js +166 -0
  54. package/dist/config/budget-schema.js.map +1 -0
  55. package/dist/config/schema.cjs +182 -2
  56. package/dist/config/schema.cjs.map +1 -1
  57. package/dist/config/schema.d.cts +122 -3
  58. package/dist/config/schema.d.cts.map +1 -1
  59. package/dist/config/schema.d.ts +122 -3
  60. package/dist/config/schema.d.ts.map +1 -1
  61. package/dist/config/schema.js +180 -1
  62. package/dist/config/schema.js.map +1 -1
  63. package/dist/constants.cjs +45 -2
  64. package/dist/constants.cjs.map +1 -1
  65. package/dist/constants.d.cts +41 -0
  66. package/dist/constants.d.cts.map +1 -1
  67. package/dist/constants.d.ts +41 -0
  68. package/dist/constants.d.ts.map +1 -1
  69. package/dist/constants.js +44 -1
  70. package/dist/constants.js.map +1 -1
  71. package/dist/core/engine.cjs +103 -21
  72. package/dist/core/engine.cjs.map +1 -1
  73. package/dist/core/engine.d.cts +7 -7
  74. package/dist/core/engine.d.cts.map +1 -1
  75. package/dist/core/engine.d.ts +7 -7
  76. package/dist/core/engine.d.ts.map +1 -1
  77. package/dist/core/engine.js +104 -22
  78. package/dist/core/engine.js.map +1 -1
  79. package/dist/core/output-path-resolver.cjs +8 -1
  80. package/dist/core/output-path-resolver.cjs.map +1 -1
  81. package/dist/core/output-path-resolver.d.cts.map +1 -1
  82. package/dist/core/output-path-resolver.d.ts.map +1 -1
  83. package/dist/core/output-path-resolver.js +9 -2
  84. package/dist/core/output-path-resolver.js.map +1 -1
  85. package/dist/errors/base.cjs +12 -3
  86. package/dist/errors/base.cjs.map +1 -1
  87. package/dist/errors/base.d.cts +7 -0
  88. package/dist/errors/base.d.cts.map +1 -1
  89. package/dist/errors/base.d.ts +7 -0
  90. package/dist/errors/base.d.ts.map +1 -1
  91. package/dist/errors/base.js +10 -2
  92. package/dist/errors/base.js.map +1 -1
  93. package/dist/errors/budget.cjs +37 -0
  94. package/dist/errors/budget.cjs.map +1 -0
  95. package/dist/errors/budget.d.cts +31 -0
  96. package/dist/errors/budget.d.cts.map +1 -0
  97. package/dist/errors/budget.d.ts +31 -0
  98. package/dist/errors/budget.d.ts.map +1 -0
  99. package/dist/errors/budget.js +33 -0
  100. package/dist/errors/budget.js.map +1 -0
  101. package/dist/errors/index.cjs +4 -1
  102. package/dist/errors/index.cjs.map +1 -1
  103. package/dist/errors/index.d.cts +1 -0
  104. package/dist/errors/index.d.cts.map +1 -1
  105. package/dist/errors/index.d.ts +1 -0
  106. package/dist/errors/index.d.ts.map +1 -1
  107. package/dist/errors/index.js +2 -0
  108. package/dist/errors/index.js.map +1 -1
  109. package/dist/index.cjs +13 -1
  110. package/dist/index.cjs.map +1 -1
  111. package/dist/index.d.cts +5 -0
  112. package/dist/index.d.cts.map +1 -1
  113. package/dist/index.d.ts +5 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +7 -0
  116. package/dist/index.js.map +1 -1
  117. package/dist/reporters/csv.cjs +37 -17
  118. package/dist/reporters/csv.cjs.map +1 -1
  119. package/dist/reporters/csv.d.cts +3 -6
  120. package/dist/reporters/csv.d.cts.map +1 -1
  121. package/dist/reporters/csv.d.ts +3 -6
  122. package/dist/reporters/csv.d.ts.map +1 -1
  123. package/dist/reporters/csv.js +37 -17
  124. package/dist/reporters/csv.js.map +1 -1
  125. package/dist/reporters/human.cjs +66 -40
  126. package/dist/reporters/human.cjs.map +1 -1
  127. package/dist/reporters/human.d.cts +14 -13
  128. package/dist/reporters/human.d.cts.map +1 -1
  129. package/dist/reporters/human.d.ts +14 -13
  130. package/dist/reporters/human.d.ts.map +1 -1
  131. package/dist/reporters/human.js +66 -40
  132. package/dist/reporters/human.js.map +1 -1
  133. package/dist/reporters/json.cjs +23 -48
  134. package/dist/reporters/json.cjs.map +1 -1
  135. package/dist/reporters/json.d.cts +2 -28
  136. package/dist/reporters/json.d.cts.map +1 -1
  137. package/dist/reporters/json.d.ts +2 -28
  138. package/dist/reporters/json.d.ts.map +1 -1
  139. package/dist/reporters/json.js +25 -50
  140. package/dist/reporters/json.js.map +1 -1
  141. package/dist/reporters/profile-human.cjs +149 -0
  142. package/dist/reporters/profile-human.cjs.map +1 -0
  143. package/dist/reporters/profile-human.d.cts +44 -0
  144. package/dist/reporters/profile-human.d.cts.map +1 -0
  145. package/dist/reporters/profile-human.d.ts +44 -0
  146. package/dist/reporters/profile-human.d.ts.map +1 -0
  147. package/dist/reporters/profile-human.js +142 -0
  148. package/dist/reporters/profile-human.js.map +1 -0
  149. package/dist/reporters/simple.cjs +64 -44
  150. package/dist/reporters/simple.cjs.map +1 -1
  151. package/dist/reporters/simple.d.cts +14 -14
  152. package/dist/reporters/simple.d.cts.map +1 -1
  153. package/dist/reporters/simple.d.ts +14 -14
  154. package/dist/reporters/simple.d.ts.map +1 -1
  155. package/dist/reporters/simple.js +64 -44
  156. package/dist/reporters/simple.js.map +1 -1
  157. package/dist/schema/modestbench-config.schema.json +153 -0
  158. package/dist/services/baseline-storage.cjs +151 -0
  159. package/dist/services/baseline-storage.cjs.map +1 -0
  160. package/dist/services/baseline-storage.d.cts +55 -0
  161. package/dist/services/baseline-storage.d.cts.map +1 -0
  162. package/dist/services/baseline-storage.d.ts +55 -0
  163. package/dist/services/baseline-storage.d.ts.map +1 -0
  164. package/dist/services/baseline-storage.js +147 -0
  165. package/dist/services/baseline-storage.js.map +1 -0
  166. package/dist/services/budget-evaluator.cjs +146 -0
  167. package/dist/services/budget-evaluator.cjs.map +1 -0
  168. package/dist/services/budget-evaluator.d.cts +29 -0
  169. package/dist/services/budget-evaluator.d.cts.map +1 -0
  170. package/dist/services/budget-evaluator.d.ts +29 -0
  171. package/dist/services/budget-evaluator.d.ts.map +1 -0
  172. package/dist/services/budget-evaluator.js +142 -0
  173. package/dist/services/budget-evaluator.js.map +1 -0
  174. package/dist/services/config-manager.cjs +23 -9
  175. package/dist/services/config-manager.cjs.map +1 -1
  176. package/dist/services/config-manager.d.cts +6 -1
  177. package/dist/services/config-manager.d.cts.map +1 -1
  178. package/dist/services/config-manager.d.ts +6 -1
  179. package/dist/services/config-manager.d.ts.map +1 -1
  180. package/dist/services/config-manager.js +23 -9
  181. package/dist/services/config-manager.js.map +1 -1
  182. package/dist/services/file-loader.cjs +3 -6
  183. package/dist/services/file-loader.cjs.map +1 -1
  184. package/dist/services/file-loader.d.cts.map +1 -1
  185. package/dist/services/file-loader.d.ts.map +1 -1
  186. package/dist/services/file-loader.js +3 -6
  187. package/dist/services/file-loader.js.map +1 -1
  188. package/dist/services/profiler/profile-filter.cjs +113 -0
  189. package/dist/services/profiler/profile-filter.cjs.map +1 -0
  190. package/dist/services/profiler/profile-filter.d.cts +20 -0
  191. package/dist/services/profiler/profile-filter.d.cts.map +1 -0
  192. package/dist/services/profiler/profile-filter.d.ts +20 -0
  193. package/dist/services/profiler/profile-filter.d.ts.map +1 -0
  194. package/dist/services/profiler/profile-filter.js +109 -0
  195. package/dist/services/profiler/profile-filter.js.map +1 -0
  196. package/dist/services/profiler/profile-parser.cjs +139 -0
  197. package/dist/services/profiler/profile-parser.cjs.map +1 -0
  198. package/dist/services/profiler/profile-parser.d.cts +18 -0
  199. package/dist/services/profiler/profile-parser.d.cts.map +1 -0
  200. package/dist/services/profiler/profile-parser.d.ts +18 -0
  201. package/dist/services/profiler/profile-parser.d.ts.map +1 -0
  202. package/dist/services/profiler/profile-parser.js +132 -0
  203. package/dist/services/profiler/profile-parser.js.map +1 -0
  204. package/dist/services/profiler/profile-runner.cjs +90 -0
  205. package/dist/services/profiler/profile-runner.cjs.map +1 -0
  206. package/dist/services/profiler/profile-runner.d.cts +29 -0
  207. package/dist/services/profiler/profile-runner.d.cts.map +1 -0
  208. package/dist/services/profiler/profile-runner.d.ts +29 -0
  209. package/dist/services/profiler/profile-runner.d.ts.map +1 -0
  210. package/dist/services/profiler/profile-runner.js +86 -0
  211. package/dist/services/profiler/profile-runner.js.map +1 -0
  212. package/dist/services/reporter-registry.cjs +18 -24
  213. package/dist/services/reporter-registry.cjs.map +1 -1
  214. package/dist/services/reporter-registry.d.cts +18 -40
  215. package/dist/services/reporter-registry.d.cts.map +1 -1
  216. package/dist/services/reporter-registry.d.ts +18 -40
  217. package/dist/services/reporter-registry.d.ts.map +1 -1
  218. package/dist/services/reporter-registry.js +18 -24
  219. package/dist/services/reporter-registry.js.map +1 -1
  220. package/dist/types/budgets.cjs +8 -0
  221. package/dist/types/budgets.cjs.map +1 -0
  222. package/dist/types/budgets.d.cts +149 -0
  223. package/dist/types/budgets.d.cts.map +1 -0
  224. package/dist/types/budgets.d.ts +149 -0
  225. package/dist/types/budgets.d.ts.map +1 -0
  226. package/dist/types/budgets.js +7 -0
  227. package/dist/types/budgets.js.map +1 -0
  228. package/dist/types/cli.cjs +2 -11
  229. package/dist/types/cli.cjs.map +1 -1
  230. package/dist/types/cli.d.cts +3 -227
  231. package/dist/types/cli.d.cts.map +1 -1
  232. package/dist/types/cli.d.ts +3 -227
  233. package/dist/types/cli.d.ts.map +1 -1
  234. package/dist/types/cli.js +2 -11
  235. package/dist/types/cli.js.map +1 -1
  236. package/dist/types/core.cjs +6 -1
  237. package/dist/types/core.cjs.map +1 -1
  238. package/dist/types/core.d.cts +13 -2
  239. package/dist/types/core.d.cts.map +1 -1
  240. package/dist/types/core.d.ts +13 -2
  241. package/dist/types/core.d.ts.map +1 -1
  242. package/dist/types/core.js +2 -1
  243. package/dist/types/core.js.map +1 -1
  244. package/dist/types/index.cjs +5 -0
  245. package/dist/types/index.cjs.map +1 -1
  246. package/dist/types/index.d.cts +2 -0
  247. package/dist/types/index.d.cts.map +1 -1
  248. package/dist/types/index.d.ts +2 -0
  249. package/dist/types/index.d.ts.map +1 -1
  250. package/dist/types/index.js +2 -0
  251. package/dist/types/index.js.map +1 -1
  252. package/dist/types/interfaces.d.cts +15 -8
  253. package/dist/types/interfaces.d.cts.map +1 -1
  254. package/dist/types/interfaces.d.ts +15 -8
  255. package/dist/types/interfaces.d.ts.map +1 -1
  256. package/dist/types/profiler.cjs +11 -0
  257. package/dist/types/profiler.cjs.map +1 -0
  258. package/dist/types/profiler.d.cts +100 -0
  259. package/dist/types/profiler.d.cts.map +1 -0
  260. package/dist/types/profiler.d.ts +100 -0
  261. package/dist/types/profiler.d.ts.map +1 -0
  262. package/dist/types/profiler.js +10 -0
  263. package/dist/types/profiler.js.map +1 -0
  264. package/dist/types/utility.cjs.map +1 -1
  265. package/dist/types/utility.d.cts +0 -8
  266. package/dist/types/utility.d.cts.map +1 -1
  267. package/dist/types/utility.d.ts +0 -8
  268. package/dist/types/utility.d.ts.map +1 -1
  269. package/dist/types/utility.js.map +1 -1
  270. package/dist/utils/identifiers.cjs +32 -0
  271. package/dist/utils/identifiers.cjs.map +1 -0
  272. package/dist/utils/identifiers.d.cts +32 -0
  273. package/dist/utils/identifiers.d.cts.map +1 -0
  274. package/dist/utils/identifiers.d.ts +32 -0
  275. package/dist/utils/identifiers.d.ts.map +1 -0
  276. package/dist/utils/identifiers.js +27 -0
  277. package/dist/utils/identifiers.js.map +1 -0
  278. package/dist/utils/package.cjs +40 -0
  279. package/dist/utils/package.cjs.map +1 -0
  280. package/dist/utils/package.d.cts +15 -0
  281. package/dist/utils/package.d.cts.map +1 -0
  282. package/dist/utils/package.d.ts +15 -0
  283. package/dist/utils/package.d.ts.map +1 -0
  284. package/dist/utils/package.js +33 -0
  285. package/dist/utils/package.js.map +1 -0
  286. package/dist/utils/type-guards.cjs +48 -0
  287. package/dist/utils/type-guards.cjs.map +1 -0
  288. package/dist/utils/type-guards.d.cts +22 -0
  289. package/dist/utils/type-guards.d.cts.map +1 -0
  290. package/dist/utils/type-guards.d.ts +22 -0
  291. package/dist/utils/type-guards.d.ts.map +1 -0
  292. package/dist/utils/type-guards.js +43 -0
  293. package/dist/utils/type-guards.js.map +1 -0
  294. package/package.json +10 -10
  295. package/src/cli/commands/analyze.ts +101 -0
  296. package/src/cli/commands/baseline.ts +577 -0
  297. package/src/cli/commands/history.ts +1 -1
  298. package/src/cli/commands/init.ts +105 -183
  299. package/src/cli/commands/run.ts +167 -98
  300. package/src/cli/index.ts +425 -183
  301. package/src/config/budget-schema.ts +189 -0
  302. package/src/config/schema.ts +260 -1
  303. package/src/constants.ts +53 -1
  304. package/src/core/engine.ts +151 -20
  305. package/src/core/output-path-resolver.ts +10 -2
  306. package/src/errors/base.ts +11 -2
  307. package/src/errors/budget.ts +38 -0
  308. package/src/errors/index.ts +3 -0
  309. package/src/index.ts +9 -0
  310. package/src/reporters/csv.ts +54 -25
  311. package/src/reporters/human.ts +88 -47
  312. package/src/reporters/json.ts +26 -71
  313. package/src/reporters/profile-human.ts +204 -0
  314. package/src/reporters/simple.ts +84 -53
  315. package/src/services/baseline-storage.ts +199 -0
  316. package/src/services/budget-evaluator.ts +182 -0
  317. package/src/services/config-manager.ts +23 -8
  318. package/src/services/file-loader.ts +3 -6
  319. package/src/services/profiler/profile-filter.ts +143 -0
  320. package/src/services/profiler/profile-parser.ts +194 -0
  321. package/src/services/profiler/profile-runner.ts +121 -0
  322. package/src/services/reporter-registry.ts +46 -81
  323. package/src/types/budgets.ts +180 -0
  324. package/src/types/cli.ts +5 -238
  325. package/src/types/core.ts +50 -10
  326. package/src/types/index.ts +5 -0
  327. package/src/types/interfaces.ts +16 -6
  328. package/src/types/profiler.ts +132 -0
  329. package/src/types/utility.ts +0 -10
  330. package/src/utils/identifiers.ts +58 -0
  331. package/src/utils/package.ts +35 -0
  332. package/src/utils/type-guards.ts +51 -0
@@ -9,6 +9,7 @@ import path from 'node:path';
9
9
 
10
10
  import type {
11
11
  BenchmarkRun,
12
+ BudgetSummary,
12
13
  FileResult,
13
14
  ProgressState,
14
15
  SuiteResult,
@@ -71,6 +72,84 @@ export class HumanReporter extends BaseReporter {
71
72
  this.showProgress = options.progress ?? true;
72
73
  }
73
74
 
75
+ /**
76
+ * Format bytes in human-readable format
77
+ */
78
+ private static formatBytes(this: void, bytes: number): string {
79
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
80
+ let size = bytes;
81
+ let unitIndex = 0;
82
+
83
+ while (size >= 1024 && unitIndex < units.length - 1) {
84
+ size /= 1024;
85
+ unitIndex++;
86
+ }
87
+
88
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
89
+ }
90
+
91
+ /**
92
+ * Format file path - show relative path if within CWD, otherwise absolute
93
+ */
94
+ private static formatPath(this: void, filePath: string): string {
95
+ const cwd = process.cwd();
96
+ const absolutePath = path.resolve(filePath);
97
+
98
+ // Check if the file is within the current working directory
99
+ if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
100
+ return path.relative(cwd, absolutePath);
101
+ }
102
+
103
+ return absolutePath;
104
+ }
105
+
106
+ /**
107
+ * Simple pluralization helper
108
+ */
109
+ private static pluralize(this: void, str: string, count: number): string {
110
+ return count === 1 ? str : `${str}s`;
111
+ }
112
+
113
+ onBudgetResult(summary: BudgetSummary): void {
114
+ if (summary.total === 0 || this.quiet) {
115
+ return;
116
+ }
117
+
118
+ this.clearProgress();
119
+
120
+ this.printLine();
121
+ const budgetHeader = `${this.colorize('magenta', ansiChars.block.full.repeat(2))} ${this.colorize('brightWhite', this.colorize('bold', 'Performance Budgets'))}`;
122
+ this.printLine(budgetHeader);
123
+ this.printLine();
124
+
125
+ for (const result of summary.results) {
126
+ const icon = result.passed ? ansiChars.checkmark : ansiChars.cross;
127
+ const iconColor = result.passed ? 'brightCyan' : 'brightRed';
128
+
129
+ this.printLine(
130
+ ` ${this.colorize(iconColor, icon)} ${this.colorize('white', result.taskId)}`,
131
+ );
132
+
133
+ if (!result.passed && result.violations.length > 0) {
134
+ for (const violation of result.violations) {
135
+ this.printLine(
136
+ ` ${this.colorize('brightRed', violation.message)}`,
137
+ );
138
+ }
139
+ }
140
+ }
141
+
142
+ this.printLine();
143
+
144
+ const statusText =
145
+ summary.failed === 0
146
+ ? `${this.colorize('brightCyan', ansiChars.checkmark)} All ${summary.total} budget(s) passed`
147
+ : `${this.colorize('brightRed', ansiChars.cross)} ${summary.failed} of ${summary.total} budget(s) failed`;
148
+
149
+ this.printLine(` ${statusText}`);
150
+ this.printLine();
151
+ }
152
+
74
153
  onEnd(run: BenchmarkRun): void {
75
154
  if (this.quiet) {
76
155
  return;
@@ -121,7 +200,7 @@ export class HumanReporter extends BaseReporter {
121
200
  );
122
201
  }
123
202
  this.printLine(
124
- `${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', this.formatDuration(duration * 1000000))}`,
203
+ `${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', BaseReporter.formatDuration(duration * 1000000))}`,
125
204
  );
126
205
  this.printLine();
127
206
 
@@ -135,7 +214,7 @@ export class HumanReporter extends BaseReporter {
135
214
  this.printLine();
136
215
 
137
216
  for (const failure of this.failures) {
138
- const displayPath = this.formatPath(failure.file);
217
+ const displayPath = HumanReporter.formatPath(failure.file);
139
218
  this.printLine(
140
219
  ` ${this.colorize('dim', displayPath)} ${this.colorize('dim', '›')} ${this.colorize('white', failure.suite)} ${this.colorize('dim', '›')} ${this.colorize('brightWhite', failure.task)}`,
141
220
  );
@@ -189,7 +268,7 @@ export class HumanReporter extends BaseReporter {
189
268
  );
190
269
  } else {
191
270
  this.printLine(
192
- ` ${this.colorize('magenta', ansiChars.checkmark)} ${totalPassed > 1 ? this.colorize('brightMagenta', 'All ') : ''}${this.colorize('bold', this.colorize('brightMagenta', `${totalPassed}`))} ${this.colorize('brightMagenta', `${this.pluralize('task', totalPassed)} passed`)}`,
271
+ ` ${this.colorize('magenta', ansiChars.checkmark)} ${totalPassed > 1 ? this.colorize('brightMagenta', 'All ') : ''}${this.colorize('bold', this.colorize('brightMagenta', `${totalPassed}`))} ${this.colorize('brightMagenta', `${HumanReporter.pluralize('task', totalPassed)} passed`)}`,
193
272
  );
194
273
  }
195
274
 
@@ -203,7 +282,7 @@ export class HumanReporter extends BaseReporter {
203
282
  return;
204
283
  }
205
284
 
206
- const displayPath = this.formatPath(file);
285
+ const displayPath = HumanReporter.formatPath(file);
207
286
  const fileMarker = `${colors.magenta}${ansiChars.block.dark}${ansiChars.block.dark}${colors.reset}`;
208
287
  this.printLine(
209
288
  `${fileMarker} ${colors.underline}${this.colorize('brightMagenta', this.colorize('bold', displayPath))}${colors.reset}`,
@@ -278,7 +357,7 @@ export class HumanReporter extends BaseReporter {
278
357
  \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;30;48;5;38m▄\x1b[38;5;14;48;5;14m▄\x1b[48;5;14m \x1b[38;5;45;48;5;14m▄\x1b[38;5;89;48;5;14m▄\x1b[38;5;89;48;5;89m▄\x1b[38;5;14;48;5;31m▄\x1b[48;5;14m \x1b[38;5;37;48;5;89m▄\x1b[48;5;198m \x1b[38;5;198;48;5;198m▄\x1b[38;5;31;48;5;14m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m \x1b[2mnode.js:\x1b[m \x1b[36m${run.environment.nodeVersion} \x1b[m
279
358
  \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;44;48;5;31m▄\x1b[48;5;14m \x1b[38;5;126;48;5;38m▄\x1b[38;5;198;48;5;237m▄\x1b[38;5;237;48;5;37m▄\x1b[48;5;14m \x1b[38;5;14;48;5;14m▄\x1b[38;5;162;48;5;198m▄▄\x1b[38;5;53;48;5;240m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m \x1b[2mplatform:\x1b[m \x1b[36m${run.environment.platform} ${run.environment.arch} \x1b[m
280
359
  \x1b[48;5;0m \x1b[38;5;45;48;5;14m▄\x1b[48;5;14m \x1b[38;5;14;48;5;37m▄\x1b[38;5;14;48;5;5m▄\x1b[38;5;14;48;5;44m▄\x1b[48;5;14m \x1b[38;5;45;48;5;14m▄\x1b[48;5;0m \x1b[m \x1b[2mcpu:\x1b[m \x1b[36m${run.environment.cpu.model} \x1b[2m(\x1b[m\x1b[36m${run.environment.cpu.cores} cores\x1b[2m) \x1b[m
281
- \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;6m▄\x1b[38;5;232;48;5;14m▄\x1b[38;5;38;48;5;14m▄\x1b[48;5;14m \x1b[38;5;30;48;5;14m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;23m▄\x1b[49;38;5;0m▀▀\x1b[m \x1b[2mmem:\x1b[m \x1b[36m${this.formatBytes(run.environment.memory.total)} \x1b[m
360
+ \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;6m▄\x1b[38;5;232;48;5;14m▄\x1b[38;5;38;48;5;14m▄\x1b[48;5;14m \x1b[38;5;30;48;5;14m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;23m▄\x1b[49;38;5;0m▀▀\x1b[m \x1b[2mmem:\x1b[m \x1b[36m${HumanReporter.formatBytes(run.environment.memory.total)} \x1b[m
282
361
  \x1b[49m \x1b[49;38;5;0m▀\x1b[38;5;0;48;5;236m▄\x1b[38;5;0;48;5;45m▄\x1b[38;5;23;48;5;14m▄\x1b[48;5;14m \x1b[38;5;236;48;5;14m▄\x1b[38;5;0;48;5;44m▄\x1b[38;5;0;48;5;232m▄\x1b[49;38;5;0m▀\x1b[49m \x1b[m
283
362
  \x1b[49m \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;37m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;30m▄\x1b[49;38;5;0m▀▀\x1b[49m \x1b[m
284
363
  `;
@@ -333,7 +412,7 @@ export class HumanReporter extends BaseReporter {
333
412
  );
334
413
  } else {
335
414
  this.printLine(
336
- ` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${this.pluralize('task', passed)} passed`)}`,
415
+ ` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${HumanReporter.pluralize('task', passed)} passed`)}`,
337
416
  );
338
417
  }
339
418
  this.printLine();
@@ -414,37 +493,6 @@ export class HumanReporter extends BaseReporter {
414
493
  return `${colors[color]}${text}${colors.reset}`;
415
494
  }
416
495
 
417
- /**
418
- * Format bytes in human-readable format
419
- */
420
- private formatBytes(bytes: number): string {
421
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
422
- let size = bytes;
423
- let unitIndex = 0;
424
-
425
- while (size >= 1024 && unitIndex < units.length - 1) {
426
- size /= 1024;
427
- unitIndex++;
428
- }
429
-
430
- return `${size.toFixed(1)} ${units[unitIndex]}`;
431
- }
432
-
433
- /**
434
- * Format file path - show relative path if within CWD, otherwise absolute
435
- */
436
- private formatPath(filePath: string): string {
437
- const cwd = process.cwd();
438
- const absolutePath = path.resolve(filePath);
439
-
440
- // Check if the file is within the current working directory
441
- if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
442
- return path.relative(cwd, absolutePath);
443
- }
444
-
445
- return absolutePath;
446
- }
447
-
448
496
  /**
449
497
  * Format duration in human-readable format for progress display
450
498
  */
@@ -471,13 +519,6 @@ export class HumanReporter extends BaseReporter {
471
519
  return str.replace(/\x1b\[[0-9;]*m/g, '').length;
472
520
  }
473
521
 
474
- /**
475
- * Simple pluralization helper
476
- */
477
- private pluralize(str: string, count: number): string {
478
- return count === 1 ? str : `${str}s`;
479
- }
480
-
481
522
  /**
482
523
  * Print all task results in a suite with aligned columns
483
524
  */
@@ -534,9 +575,9 @@ export class HumanReporter extends BaseReporter {
534
575
  };
535
576
  }
536
577
 
537
- const duration = this.formatDuration(result.mean); // already in nanoseconds
538
- const opsPerSec = this.formatOpsPerSecond(result.opsPerSecond);
539
- const rme = this.formatPercentage(result.marginOfError * 100);
578
+ const duration = BaseReporter.formatDuration(result.mean); // already in nanoseconds
579
+ const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
580
+ const rme = BaseReporter.formatPercentage(result.marginOfError * 100);
540
581
 
541
582
  return {
542
583
  durationLen: this.getVisibleLength(duration),
@@ -5,16 +5,11 @@
5
5
  * processing, CI/CD integration, and data analysis.
6
6
  */
7
7
 
8
- import { mkdirSync, writeFileSync } from 'node:fs';
9
- import { dirname } from 'node:path';
8
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
10
11
 
11
- import type {
12
- BenchmarkRun,
13
- FileResult,
14
- ProgressState,
15
- SuiteResult,
16
- TaskResult,
17
- } from '../types/index.js';
12
+ import type { BenchmarkRun, TaskResult } from '../types/index.js';
18
13
 
19
14
  import { ReporterOutputError } from '../errors/index.js';
20
15
  import { BaseReporter } from '../services/reporter-registry.js';
@@ -40,12 +35,30 @@ interface JsonOutput {
40
35
  };
41
36
  }
42
37
 
38
+ /**
39
+ * Cache the package version at module load time
40
+ *
41
+ * NOTE: This relies on package.json being at the same relative path from both
42
+ * src/ and dist/ directories (../../package.json). If the build output
43
+ * structure changes, this will break.
44
+ */
45
+ const cachedPackageVersion = (() => {
46
+ try {
47
+ const __dirname = dirname(fileURLToPath(import.meta.url));
48
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
49
+ const pkgContent = readFileSync(pkgPath, 'utf8');
50
+ const pkg = JSON.parse(pkgContent) as { version: string };
51
+ return pkg.version;
52
+ } catch {
53
+ // Fallback if package.json cannot be read (shouldn't happen in normal use)
54
+ return 'unknown';
55
+ }
56
+ })();
57
+
43
58
  /**
44
59
  * JSON reporter for structured output
45
60
  */
46
61
  export class JsonReporter extends BaseReporter {
47
- private currentRun?: BenchmarkRun;
48
-
49
62
  private readonly includeMetadata: boolean;
50
63
 
51
64
  private readonly includeStatistics: boolean;
@@ -54,8 +67,6 @@ export class JsonReporter extends BaseReporter {
54
67
 
55
68
  private readonly prettyPrint: boolean;
56
69
 
57
- private readonly quiet: boolean;
58
-
59
70
  private statistics: {
60
71
  fastestTask?: TaskResult;
61
72
  slowestTask?: TaskResult;
@@ -74,8 +85,6 @@ export class JsonReporter extends BaseReporter {
74
85
  includeStatistics?: boolean;
75
86
  outputPath?: string;
76
87
  prettyPrint?: boolean;
77
- quiet?: boolean;
78
- verbose?: boolean;
79
88
  } = {},
80
89
  ) {
81
90
  super('json', options);
@@ -84,35 +93,6 @@ export class JsonReporter extends BaseReporter {
84
93
  this.prettyPrint = options.prettyPrint ?? true;
85
94
  this.includeStatistics = options.includeStatistics ?? true;
86
95
  this.includeMetadata = options.includeMetadata ?? true;
87
- this.quiet = options.quiet ?? false;
88
- }
89
-
90
- /**
91
- * Check if statistics are included
92
- */
93
- areStatisticsIncluded(): boolean {
94
- return this.includeStatistics;
95
- }
96
-
97
- /**
98
- * Get the output path (if configured)
99
- */
100
- getOutputPath(): string | undefined {
101
- return this.outputPath;
102
- }
103
-
104
- /**
105
- * Check if metadata is included
106
- */
107
- isMetadataIncluded(): boolean {
108
- return this.includeMetadata;
109
- }
110
-
111
- /**
112
- * Check if pretty printing is enabled
113
- */
114
- isPrettyPrintEnabled(): boolean {
115
- return this.prettyPrint;
116
96
  }
117
97
 
118
98
  async onEnd(run: BenchmarkRun): Promise<void> {
@@ -131,41 +111,16 @@ export class JsonReporter extends BaseReporter {
131
111
  console.error('JSON Reporter Error:', error.message);
132
112
  }
133
113
 
134
- onFileEnd(_result: FileResult): void {
135
- // No-op for JSON reporter
136
- }
137
-
138
- onFileStart(_file: string): void {
139
- // No-op for JSON reporter
140
- }
141
-
142
- onProgress(_state: ProgressState): void {
143
- // No-op for JSON reporter - we don't output progress in JSON format
144
- }
145
-
146
- onStart(run: BenchmarkRun): void {
147
- this.currentRun = run;
114
+ onStart(_run: BenchmarkRun): void {
148
115
  this.resetStatistics();
149
116
  }
150
117
 
151
- onSuiteEnd(_result: SuiteResult): void {
152
- // No-op for JSON reporter
153
- }
154
-
155
- onSuiteStart(_suite: string): void {
156
- // No-op for JSON reporter
157
- }
158
-
159
118
  onTaskResult(result: TaskResult): void {
160
119
  if (!result.error) {
161
120
  this.updateStatistics(result);
162
121
  }
163
122
  }
164
123
 
165
- onTaskStart(_task: string): void {
166
- // No-op for JSON reporter
167
- }
168
-
169
124
  /**
170
125
  * Build the complete JSON output structure
171
126
  */
@@ -174,7 +129,7 @@ export class JsonReporter extends BaseReporter {
174
129
  meta: {
175
130
  format: 'modestbench-json',
176
131
  timestamp: new Date().toISOString(),
177
- version: '0.1.0', // TODO: Get from package.json
132
+ version: cachedPackageVersion,
178
133
  },
179
134
  run: this.includeMetadata ? run : this.sanitizeRun(run),
180
135
  };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Profile Human Reporter
3
+ *
4
+ * Human-readable reporter for profile command. Uses modestbench's synthwave
5
+ * ANSI theme to display profiled functions in an attractive, color-coded
6
+ * format.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import path from 'node:path';
12
+
13
+ import type { FilteredProfileData } from '../types/profiler.js';
14
+
15
+ import { ansiChars, colors } from '../utils/ansi.js';
16
+
17
+ /**
18
+ * Reporter options
19
+ */
20
+ interface ProfileReporterOptions {
21
+ /** Enable color output */
22
+ color?: boolean;
23
+
24
+ /** Group by file */
25
+ groupByFile?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Human-readable profile reporter
30
+ */
31
+ export class ProfileHumanReporter {
32
+ private readonly groupByFile: boolean;
33
+
34
+ private readonly useColor: boolean;
35
+
36
+ constructor(options: ProfileReporterOptions = {}) {
37
+ this.useColor =
38
+ options.color ??
39
+ (process.stdout.isTTY &&
40
+ process.env.FORCE_COLOR !== '0' &&
41
+ process.env.NO_COLOR == null);
42
+
43
+ this.groupByFile = options.groupByFile ?? false;
44
+ }
45
+
46
+ /**
47
+ * Generate and print profile report
48
+ */
49
+ report(data: FilteredProfileData): void {
50
+ this.printHeader(data);
51
+ this.printLine();
52
+
53
+ if (this.groupByFile && data.groupedByFile) {
54
+ this.printGroupedResults(data);
55
+ } else {
56
+ this.printFlatResults(data);
57
+ }
58
+ }
59
+
60
+ private colorize(color: keyof typeof colors, text: string): string {
61
+ if (!this.useColor) {
62
+ return text;
63
+ }
64
+ return `${colors[color]}${text}${colors.reset}`;
65
+ }
66
+
67
+ /**
68
+ * Format file path - show relative path if within CWD, otherwise absolute
69
+ */
70
+ private formatPath(filePath: string): string {
71
+ const cwd = process.cwd();
72
+ const absolutePath = path.resolve(filePath);
73
+
74
+ // Check if the file is within the current working directory
75
+ if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
76
+ return path.relative(cwd, absolutePath);
77
+ }
78
+
79
+ return absolutePath;
80
+ }
81
+
82
+ private getPercentColor(percent: number): keyof typeof colors {
83
+ if (percent >= 10) {
84
+ return 'brightRed';
85
+ }
86
+ if (percent >= 5) {
87
+ return 'brightYellow';
88
+ }
89
+ if (percent >= 2) {
90
+ return 'brightCyan';
91
+ }
92
+ return 'white';
93
+ }
94
+
95
+ private printFlatResults(data: FilteredProfileData): void {
96
+ const header = `${this.colorize('magenta', ansiChars.block.full.repeat(2))} ${this.colorize('brightWhite', this.colorize('bold', 'Benchmark Candidates'))}`;
97
+ this.printLine(header);
98
+ this.printLine();
99
+ this.printLine('Top functions by execution time:');
100
+ this.printLine();
101
+
102
+ for (const fn of data.functions) {
103
+ // Function name and percentage
104
+ const percentColor = this.getPercentColor(fn.percentage);
105
+ const percent = `${fn.percentage.toFixed(1)}%`;
106
+ const ticks = `(${fn.ticks.toLocaleString()} ticks)`;
107
+
108
+ this.printLine(
109
+ ` ${this.colorize('brightWhite', fn.name).padEnd(60)} ${this.colorize(percentColor, percent.padStart(6))} ${this.colorize('dim', ticks)}`,
110
+ );
111
+
112
+ // File and line
113
+ const displayPath = this.formatPath(fn.file);
114
+ const lineInfo = fn.line ? `:${fn.line}` : '';
115
+ this.printLine(
116
+ ` ${this.colorize('brightMagenta', this.colorize('bold', displayPath + lineInfo))}`,
117
+ );
118
+ this.printLine();
119
+ }
120
+
121
+ this.printSummary(data);
122
+ }
123
+
124
+ private printGroupedResults(data: FilteredProfileData): void {
125
+ if (!data.groupedByFile) {
126
+ return;
127
+ }
128
+
129
+ const header = `${this.colorize('magenta', ansiChars.block.full.repeat(2))} ${this.colorize('brightWhite', this.colorize('bold', 'Grouped by File'))}`;
130
+ this.printLine(header);
131
+ this.printLine();
132
+
133
+ // Sort files by total percentage
134
+ const sortedFiles = Array.from(data.groupedByFile.entries()).sort(
135
+ (a, b) => {
136
+ const aTotal = a[1].reduce((sum, fn) => sum + fn.percentage, 0);
137
+ const bTotal = b[1].reduce((sum, fn) => sum + fn.percentage, 0);
138
+ return bTotal - aTotal;
139
+ },
140
+ );
141
+
142
+ for (const [file, functions] of sortedFiles) {
143
+ const totalPercent = functions.reduce(
144
+ (sum, fn) => sum + fn.percentage,
145
+ 0,
146
+ );
147
+ const totalTicks = functions.reduce((sum, fn) => sum + fn.ticks, 0);
148
+
149
+ const percentColor = this.getPercentColor(totalPercent);
150
+ const percent = `${totalPercent.toFixed(1)}%`;
151
+ const ticks = `(${totalTicks.toLocaleString()} ticks)`;
152
+
153
+ // File header
154
+ const displayPath = this.formatPath(file);
155
+ this.printLine(
156
+ `${this.colorize('magenta', ansiChars.block.dark)} ${this.colorize('brightMagenta', this.colorize('bold', displayPath)).padEnd(60)} ${this.colorize(percentColor, percent.padStart(6))} ${this.colorize('dim', ticks)}`,
157
+ );
158
+
159
+ // Functions in this file
160
+ for (const fn of functions) {
161
+ const fnPercent = `${fn.percentage.toFixed(1)}%`;
162
+ const fnTicks = `(${fn.ticks.toLocaleString()} ticks)`;
163
+ const lineInfo = fn.line ? `:${fn.line}` : '';
164
+
165
+ this.printLine(
166
+ ` ${this.colorize('magenta', ansiChars.smallSquare)} ${this.colorize('brightWhite', fn.name).padEnd(58)} ${this.colorize(this.getPercentColor(fn.percentage), fnPercent.padStart(6))} ${this.colorize('dim', fnTicks.padEnd(15))} ${this.colorize('dim', lineInfo)}`,
167
+ );
168
+ }
169
+
170
+ this.printLine();
171
+ }
172
+
173
+ this.printSummary(data);
174
+ }
175
+
176
+ private printHeader(data: FilteredProfileData): void {
177
+ const header = `${this.colorize('magenta', ansiChars.block.full.repeat(2))} ${this.colorize('brightWhite', this.colorize('bold', 'Profile Analysis'))}`;
178
+ this.printLine(header);
179
+ this.printLine();
180
+
181
+ if (data.command) {
182
+ this.printLine(`Command: ${this.colorize('cyan', data.command)}`);
183
+ }
184
+
185
+ if (data.duration) {
186
+ const durationSec = (data.duration / 1000).toFixed(1);
187
+ this.printLine(`Duration: ${this.colorize('cyan', `${durationSec}s`)}`);
188
+ }
189
+
190
+ this.printLine(
191
+ `Total Ticks: ${this.colorize('cyan', data.totalTicks.toLocaleString())}`,
192
+ );
193
+ }
194
+
195
+ private printLine(text = ''): void {
196
+ console.log(text);
197
+ }
198
+
199
+ private printSummary(data: FilteredProfileData): void {
200
+ this.printLine(
201
+ `${this.colorize('dim', `... (showing top ${data.totalShown} of ${data.totalFiltered} user functions)`)}`,
202
+ );
203
+ }
204
+ }