Skip to content

Commit 14d9a7a

Browse files
boneskullclaude
andcommitted
feat(json-reporter): make compact output the default
- Change JSON reporter default from prettyPrint: true to false - Add --json-pretty CLI flag to opt-in to formatted output - Support reporterConfig.json.prettyPrint in config files - CLI flag takes precedence over config file setting - Derive ReporterConfig and JsonReporterConfig from Zod schemas - Add TODO for ThresholdConfig schema derivation - Add integration tests for CLI flag and config precedence BREAKING CHANGE: JSON reporter now outputs compact JSON by default. Use --json-pretty flag or set reporterConfig.json.prettyPrint: true in config file to get formatted output. Closes #74 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent e147847 commit 14d9a7a

File tree

8 files changed

+319
-113
lines changed

8 files changed

+319
-113
lines changed

src/cli/commands/run.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
BenchmarkRun,
1212
ModestBenchConfig,
1313
Reporter,
14+
ReporterConfig,
1415
} from '../../types/index.js';
1516
import type { CliContext } from '../index.js';
1617

@@ -61,6 +62,7 @@ interface RunOptions {
6162
excludeTags?: string[] | undefined;
6263
iterations?: number | undefined;
6364
json?: boolean | undefined;
65+
jsonPretty?: boolean | undefined;
6466
noColor?: boolean | undefined;
6567
outputDir?: string | undefined;
6668
outputFile?: string | undefined;
@@ -126,6 +128,7 @@ export const handleRunCommand = async (
126128
options.outputDir,
127129
options.outputFile,
128130
options.progress,
131+
options.jsonPretty,
129132
);
130133

131134
// Step 3: Discovery phase
@@ -389,7 +392,7 @@ const setupReporters = async (
389392
context: CliContext,
390393
config: {
391394
outputDir?: string;
392-
reporterConfig?: Record<string, unknown>;
395+
reporterConfig?: ReporterConfig;
393396
reporters?: string[];
394397
},
395398
isVerbose: boolean,
@@ -398,6 +401,7 @@ const setupReporters = async (
398401
explicitOutputDir?: string,
399402
explicitOutputFile?: string,
400403
progressOption?: boolean,
404+
explicitJsonPretty?: boolean,
401405
): Promise<Reporter[]> => {
402406
try {
403407
const reporters: Reporter[] = [];
@@ -453,9 +457,14 @@ const setupReporters = async (
453457
explicitOutputFile,
454458
'results.json',
455459
);
460+
// Precedence: CLI flag > config file > default (false)
461+
const prettyPrint =
462+
explicitJsonPretty ??
463+
config.reporterConfig?.json?.prettyPrint ??
464+
false;
456465
reporter = new JsonReporter({
457466
...(outputPath ? { outputPath } : {}),
458-
prettyPrint: true,
467+
prettyPrint,
459468
});
460469
break;
461470
}

src/cli/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ export const main = async (
276276
'Benchmark engine: tinybench (default) or accurate (requires --allow-natives-syntax)',
277277
type: 'string',
278278
})
279+
.option('json-pretty', {
280+
defaultDescription: 'false',
281+
description:
282+
'Pretty-print JSON output (only affects json reporter)',
283+
type: 'boolean',
284+
})
279285
.example([
280286
['$0 run', 'Run benchmarks in current directory and bench/'],
281287
['$0 run benchmarks/', 'Run all benchmarks in a directory'],
@@ -315,6 +321,7 @@ export const main = async (
315321
excludeTags: argv['exclude-tag'],
316322
iterations: argv.iterations,
317323
json: argv.json,
324+
jsonPretty: argv['json-pretty'],
318325
noColor: argv.noColor,
319326
outputDir: argv.output,
320327
outputFile: argv['output-file'],
@@ -1133,7 +1140,7 @@ const createCliContext = async (
11331140
engine.registerReporter(
11341141
'json',
11351142
new JsonReporter({
1136-
prettyPrint: true,
1143+
prettyPrint: false,
11371144
}),
11381145
);
11391146

src/config/schema.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,36 @@
88

99
import * as z from 'zod';
1010

11-
import type { ModestBenchConfig } from '../types/core.js';
12-
1311
import { BENCHMARK_FILE_PATTERN } from '../constants.js';
1412
import { parsePercentageString, parseTimeString } from './budget-schema.js';
1513

14+
/**
15+
* Schema for JSON reporter configuration options
16+
*/
17+
export const jsonReporterConfigSchema = z.object({
18+
prettyPrint: z
19+
.boolean()
20+
.optional()
21+
.describe('Whether to pretty-print JSON output (default: false)'),
22+
});
23+
24+
/**
25+
* Schema for reporter-specific configuration
26+
*
27+
* Allows typed configuration for known reporters while permitting unknown
28+
* reporter configs via catchall.
29+
*/
30+
export const reporterConfigSchema = z
31+
.object({
32+
json: jsonReporterConfigSchema
33+
.optional()
34+
.describe('Configuration options for the JSON reporter'),
35+
})
36+
.catchall(z.unknown())
37+
.describe(
38+
'Configuration options specific to individual reporters, keyed by reporter name',
39+
);
40+
1641
/**
1742
* Schema for threshold configuration
1843
*
@@ -226,11 +251,7 @@ const modestBenchConfigSchema = z
226251
.describe(
227252
'Run in quiet mode with minimal console output (only errors and final results)',
228253
),
229-
reporterConfig: z
230-
.record(z.string(), z.unknown())
231-
.describe(
232-
'Configuration options specific to individual reporters, keyed by reporter name',
233-
),
254+
reporterConfig: reporterConfigSchema,
234255
reporters: z
235256
.array(z.string())
236257
.min(1)
@@ -446,3 +467,5 @@ export const safeParseConfig = (config: unknown) => {
446467

447468
return result;
448469
};
470+
471+
export type ModestBenchConfig = z.infer<typeof modestBenchConfigSchema>;

src/reporters/json.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class JsonReporter extends BaseReporter {
9090
super('json', options);
9191

9292
this.outputPath = options.outputPath;
93-
this.prettyPrint = options.prettyPrint ?? true;
93+
this.prettyPrint = options.prettyPrint ?? false;
9494
this.includeStatistics = options.includeStatistics ?? true;
9595
this.includeMetadata = options.includeMetadata ?? true;
9696
}

src/services/config-manager.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,10 @@ export class ModestBenchConfigurationManager implements ConfigurationManager {
198198
);
199199
}
200200

201-
return validation.data;
201+
// TODO: safeParseConfig transforms budgets from nested to flat format,
202+
// but the schema type doesn't reflect this. Fix by defining a separate
203+
// output type for the transformed config. (see #15)
204+
return validation.data as ModestBenchConfig;
202205
} catch (error) {
203206
// Re-throw our custom errors
204207
if (

src/types/core.ts

Lines changed: 58 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import type { z } from 'zod';
2+
3+
import type {
4+
jsonReporterConfigSchema,
5+
ModestBenchConfig,
6+
reporterConfigSchema,
7+
} from '../config/schema.js';
18
// Budget-related types
29
import type {
310
AbsoluteBudget,
@@ -13,30 +20,6 @@ import type {
1320
TaskId,
1421
} from './budgets.js';
1522

16-
export type {
17-
AbsoluteBudget,
18-
BaselineReference,
19-
BaselineStorage,
20-
BaselineSummaryData,
21-
Budget,
22-
BudgetResult,
23-
BudgetSummary,
24-
BudgetViolation,
25-
RelativeBudget,
26-
RunId,
27-
TaskId,
28-
};
29-
30-
// Re-export schema-derived types
31-
export type {
32-
BenchmarkDefinition,
33-
BenchmarkDefinitionInput,
34-
BenchmarkSuite,
35-
BenchmarkSuiteInput,
36-
BenchmarkTask,
37-
BenchmarkTaskInput,
38-
} from '../core/benchmark-schema.js';
39-
4023
/**
4124
* Benchmark file structure after parsing
4225
*/
@@ -51,21 +34,6 @@ export interface BenchmarkFile {
5134
readonly metadata: FileMetadata;
5235
}
5336

54-
/**
55-
* ModestBench Core Types
56-
*
57-
* Defines the fundamental data structures used throughout the ModestBench
58-
* system. These types represent benchmark results, metadata, configuration, and
59-
* system state.
60-
*
61-
* Note: BenchmarkDefinition, BenchmarkSuite, and BenchmarkTask types are
62-
* derived from Zod schemas and re-exported from benchmark-schema.ts for type
63-
* safety and consistency.
64-
*/
65-
66-
// Re-export identifier helper functions
67-
export { createRunId, createTaskId } from '../utils/identifiers.js';
68-
6937
/**
7038
* Represents a complete benchmark run across multiple files
7139
*/
@@ -98,6 +66,22 @@ export interface BenchmarkRun {
9866
readonly tags?: string[];
9967
}
10068

69+
export type {
70+
AbsoluteBudget,
71+
BaselineReference,
72+
BaselineStorage,
73+
BaselineSummaryData,
74+
Budget,
75+
BudgetResult,
76+
BudgetSummary,
77+
BudgetViolation,
78+
RelativeBudget,
79+
RunId,
80+
TaskId,
81+
};
82+
83+
export type { ModestBenchConfig };
84+
10185
/**
10286
* CI/CD environment information
10387
*/
@@ -116,6 +100,28 @@ export interface CiInfo {
116100
readonly pullRequest?: string;
117101
}
118102

103+
/**
104+
* ModestBench Core Types
105+
*
106+
* Defines the fundamental data structures used throughout the ModestBench
107+
* system. These types represent benchmark results, metadata, configuration, and
108+
* system state.
109+
*
110+
* Note: BenchmarkDefinition, BenchmarkSuite, and BenchmarkTask types are
111+
* derived from Zod schemas and re-exported from benchmark-schema.ts for type
112+
* safety and consistency.
113+
*/
114+
115+
// Re-export schema-derived types
116+
export type {
117+
BenchmarkDefinition,
118+
BenchmarkDefinitionInput,
119+
BenchmarkSuite,
120+
BenchmarkSuiteInput,
121+
BenchmarkTask,
122+
BenchmarkTaskInput,
123+
} from '../core/benchmark-schema.js';
124+
119125
/**
120126
* CPU information
121127
*/
@@ -277,6 +283,11 @@ export interface GitInfo {
277283
readonly timestamp: Date;
278284
}
279285

286+
/**
287+
* Configuration options for the JSON reporter
288+
*/
289+
export type JsonReporterConfig = z.infer<typeof jsonReporterConfigSchema>;
290+
280291
/**
281292
* Memory information
282293
*/
@@ -289,69 +300,15 @@ export interface MemoryInfo {
289300
readonly used: number;
290301
}
291302

303+
// Re-export identifier helper functions
304+
export { createRunId, createTaskId } from '../utils/identifiers.js';
292305
/**
293-
* Benchmark configuration
294-
*
295-
* The JSON Schema for this configuration is available at
296-
* `dist/schema/modestbench-config.schema.json` after building the project.
306+
* Reporter-specific configuration
297307
*
298-
* Config files can optionally include a `$schema` property pointing to the
299-
* schema file for IDE autocomplete and validation support.
300-
*
301-
* @example
302-
*
303-
* ```json
304-
* {
305-
* "$schema": "./node_modules/modestbench/dist/schema/modestbench-config.schema.json",
306-
* "iterations": 1000,
307-
* "reporters": ["human", "json"],
308-
* "time": 5000
309-
* }
310-
* ```
308+
* Provides typed configuration for known reporters. Unknown reporter configs
309+
* are allowed via index signature.
311310
*/
312-
export interface ModestBenchConfig {
313-
readonly $schema?: string | undefined;
314-
/** Whether to stop on first failure */
315-
readonly bail: boolean;
316-
/** Name of baseline to use for relative budget comparisons */
317-
readonly baseline?: string | undefined;
318-
/** How to handle budget violations: 'fail', 'warn', or 'report' */
319-
readonly budgetMode?: 'fail' | 'report' | 'warn' | undefined;
320-
/** Performance budgets mapped by task identifier */
321-
readonly budgets?: Record<string, unknown> | undefined;
322-
/** Patterns to exclude from discovery */
323-
readonly exclude: string[];
324-
/** Tags to exclude from execution */
325-
readonly excludeTags: string[];
326-
/** Default number of iterations per task */
327-
readonly iterations: number;
328-
/** How to limit benchmark execution: 'time', 'iterations', 'any', or 'all' */
329-
readonly limitBy: 'all' | 'any' | 'iterations' | 'time';
330-
/** Custom metadata to attach to runs */
331-
readonly metadata: Record<string, unknown>;
332-
/** Output directory for reports (undefined means stdout for data reporters) */
333-
readonly outputDir?: string;
334-
/** Pattern(s) for discovering benchmark files */
335-
readonly pattern: string | string[];
336-
/** Whether to run in quiet mode */
337-
readonly quiet: boolean;
338-
/** Configuration for specific reporters */
339-
readonly reporterConfig: Record<string, unknown>;
340-
/** Reporters to use for output */
341-
readonly reporters: string[];
342-
/** Tags to include (if empty, include all) */
343-
readonly tags: string[];
344-
/** Threshold configuration for performance assertions */
345-
readonly thresholds: ThresholdConfig;
346-
/** Maximum time to spend on each task in milliseconds */
347-
readonly time: number;
348-
/** Timeout for individual tasks in milliseconds */
349-
readonly timeout: number;
350-
/** Whether to run in verbose mode */
351-
readonly verbose: boolean;
352-
/** Number of warmup iterations before measurement */
353-
readonly warmup: number;
354-
}
311+
export type ReporterConfig = z.infer<typeof reporterConfigSchema>;
355312

356313
/**
357314
* Summary statistics for a benchmark run
@@ -441,6 +398,8 @@ export interface TaskResult {
441398

442399
/**
443400
* Threshold configuration for performance assertions
401+
*
402+
* TODO: Derive from thresholdConfigSchema via z.infer (see #15)
444403
*/
445404
export interface ThresholdConfig {
446405
/** Maximum allowed margin of error percentage */

0 commit comments

Comments
 (0)