Skip to content

Commit e68c491

Browse files
feat: add option for JSON output (#179)
* Add option for JSON output file * Moved back to JSON output
1 parent 4a93267 commit e68c491

4 files changed

Lines changed: 68 additions & 9 deletions

File tree

src/cli.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ const subCommands = new Map<string, LazyCommand<any>>([
2929
['migrate', lazy(migrateCommand, migrateMeta)]
3030
]);
3131

32-
cli(process.argv.slice(2), defaultCommand, {
32+
const args = process.argv.slice(2);
33+
const isJsonMode = args[0] === 'analyze' && args.includes('--json');
34+
35+
cli(args, defaultCommand, {
3336
name: 'cli',
3437
version,
3538
description: `${styleText('cyan', 'e18e')}`,
36-
subCommands
39+
subCommands,
40+
renderHeader: isJsonMode ? null : undefined
3741
});

src/commands/analyze.meta.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export const meta = {
1414
multiple: true,
1515
description:
1616
'Path(s) to custom manifest file(s) for module replacements analysis'
17+
},
18+
json: {
19+
type: 'boolean',
20+
default: false,
21+
description: 'Output results as JSON to stdout'
1722
}
1823
}
1924
} as const;

src/commands/analyze.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,17 @@ export async function run(ctx: CommandContext<typeof meta>) {
3737
const providedPath =
3838
ctx.positionals.length > 1 ? ctx.positionals[1] : undefined;
3939
const logLevel = ctx.values['log-level'];
40-
let root: string | undefined = undefined;
40+
const jsonOutput = ctx.values['json'];
41+
let root: string | undefined;
4142

4243
// Enable debug output based on log level
4344
if (logLevel === 'debug') {
4445
enableDebug('e18e:*');
4546
}
4647

47-
prompts.intro('Analyzing...');
48+
if (!jsonOutput) {
49+
prompts.intro('Analyzing...');
50+
}
4851

4952
// Path can be a directory (analyze project)
5053
if (providedPath) {
@@ -56,7 +59,11 @@ export async function run(ctx: CommandContext<typeof meta>) {
5659
}
5760

5861
if (!stat || !stat.isDirectory()) {
59-
prompts.cancel(`Path must be a directory: ${providedPath}`);
62+
if (jsonOutput) {
63+
process.stderr.write(`Path must be a directory: ${providedPath}\n`);
64+
} else {
65+
prompts.cancel(`Path must be a directory: ${providedPath}`);
66+
}
6067
process.exit(1);
6168
}
6269

@@ -71,6 +78,19 @@ export async function run(ctx: CommandContext<typeof meta>) {
7178
manifest: customManifests
7279
});
7380

81+
const thresholdRank = FAIL_THRESHOLD_RANK[logLevel] ?? 0;
82+
const hasFailingMessages =
83+
thresholdRank > 0 &&
84+
messages.some((m) => SEVERITY_RANK[m.severity] >= thresholdRank);
85+
86+
if (jsonOutput) {
87+
process.stdout.write(JSON.stringify({stats, messages}, null, 2) + '\n');
88+
if (hasFailingMessages) {
89+
process.exit(1);
90+
}
91+
return;
92+
}
93+
7494
prompts.log.info('Summary');
7595

7696
const totalDeps =
@@ -197,10 +217,6 @@ export async function run(ctx: CommandContext<typeof meta>) {
197217
prompts.outro('Done!');
198218

199219
// Exit with non-zero when messages meet the fail threshold (--log-level)
200-
const thresholdRank = FAIL_THRESHOLD_RANK[logLevel] ?? 0;
201-
const hasFailingMessages =
202-
thresholdRank > 0 &&
203-
messages.some((m) => SEVERITY_RANK[m.severity] >= thresholdRank);
204220
if (hasFailingMessages) {
205221
process.exit(1);
206222
}

src/test/cli.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,40 @@ describe('analyze exit codes', () => {
157157
});
158158
});
159159

160+
describe('analyze --json', () => {
161+
beforeAll(async () => {
162+
const nodeModules = path.join(basicChalkFixture, 'node_modules');
163+
if (!existsSync(nodeModules)) {
164+
execSync('npm install', {cwd: basicChalkFixture, stdio: 'pipe'});
165+
}
166+
});
167+
168+
it('outputs valid JSON to stdout', async () => {
169+
const {stdout, code} = await runCliProcess(
170+
['analyze', '--json', '--log-level=error'],
171+
tempDir
172+
);
173+
expect(code).toBe(0);
174+
const parsed = JSON.parse(stdout);
175+
expect(parsed).toHaveProperty('stats');
176+
expect(parsed).toHaveProperty('messages');
177+
expect(parsed.stats).toHaveProperty('name', 'mock-package');
178+
expect(parsed.stats).toHaveProperty('version', '1.0.0');
179+
expect(parsed.stats).toHaveProperty('dependencyCount');
180+
expect(Array.isArray(parsed.messages)).toBe(true);
181+
});
182+
183+
it('exits 1 with --json when messages meet fail threshold', async () => {
184+
const {stdout, code} = await runCliProcess(
185+
['analyze', '--json'],
186+
basicChalkFixture
187+
);
188+
expect(code).toBe(1);
189+
const parsed = JSON.parse(stdout);
190+
expect(parsed.messages.length).toBeGreaterThan(0);
191+
});
192+
});
193+
160194
describe('analyze fixable summary', () => {
161195
it('includes fixable-by-migrate summary when project has fixable replacement', async () => {
162196
const {stdout, stderr, code} = await runCliProcess(

0 commit comments

Comments
 (0)