Skip to content

Commit 746591f

Browse files
frankieyanclaude
andauthored
feat: add --show-errors flag to show detailed compiler errors (#41)
* refactor: extract argument parsing into separate module Moves CLI argument parsing logic into src/args.mts with unit tests. Prepares for adding new flags without complicating index.mts. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add --show-errors flag to show detailed compiler errors Adds a --show-errors flag that works with --check-files to display exact violation reasons and line numbers instead of just counts. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: pass --show-errors to all commands The --show-errors flag was parsed but only used with --check-files. Now it works with all commands: default check-all, --overwrite, and --stage-record-file. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: organize integration tests by CLI flag Group tests into describe blocks by flag category for better readability: - no flag (default check-all) - --check-files - --overwrite - --stage-record-file Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: dedupe verbose error output and show occurrence counts Consolidate duplicate errors by line and reason, displaying a count suffix (e.g., "x3") instead of repeating the same error multiple times. Extract formatErrorDetails() helper to reduce code duplication. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: add --show-errors flag to README Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: make showErrors parameter required in checkErrorChanges All callers pass this parameter explicitly, so making it required improves type consistency. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c143407 commit 746591f

5 files changed

Lines changed: 299 additions & 73 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ Checks all source files matching `sourceGlob` and reports the total error count.
9898
npx @doist/react-compiler-tracker
9999
```
100100

101+
### `--show-errors`
102+
103+
Shows error information from the compiler including file path, line number, and error reason. Can be combined with any command.
104+
105+
```bash
106+
npx @doist/react-compiler-tracker --check-files --show-errors src/components/Button.tsx
107+
```
108+
109+
Example output:
110+
```
111+
React Compiler errors have increased in:
112+
• src/components/Button.tsx: +3
113+
114+
Detailed errors:
115+
- src/components/Button.tsx: Line 15: Cannot access refs during render (x2)
116+
- src/components/Button.tsx: Line 28: Invalid hook usage
117+
```
118+
101119
## Integration Examples
102120

103121
### lint-staged

src/args.mts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
type Command = 'check-files' | 'overwrite' | 'stage-record-file' | 'check-all'
2+
3+
type ParsedArgs = {
4+
command: Command
5+
filePaths: string[]
6+
showErrors: boolean
7+
}
8+
9+
const FLAG_TO_COMMAND: Record<string, Command> = {
10+
'--check-files': 'check-files',
11+
'--overwrite': 'overwrite',
12+
'--stage-record-file': 'stage-record-file',
13+
}
14+
15+
function parseArgs(argv: string[]): ParsedArgs {
16+
const showErrors = argv.includes('--show-errors')
17+
const argsWithoutShowErrors = argv.filter((arg) => arg !== '--show-errors')
18+
19+
const flag = argsWithoutShowErrors[0] ?? ''
20+
const filePaths = argsWithoutShowErrors.slice(1)
21+
const command = FLAG_TO_COMMAND[flag] ?? 'check-all'
22+
23+
return { command, filePaths, showErrors }
24+
}
25+
26+
export { parseArgs, type ParsedArgs }

src/args.test.mts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { parseArgs } from './args.mjs'
3+
4+
describe('parseArgs', () => {
5+
it('parses --check-files with files', () => {
6+
const result = parseArgs(['--check-files', 'file.ts'])
7+
expect(result).toEqual({
8+
command: 'check-files',
9+
filePaths: ['file.ts'],
10+
showErrors: false,
11+
})
12+
})
13+
14+
it('parses --show-errors before command', () => {
15+
const result = parseArgs(['--show-errors', '--check-files', 'file.ts'])
16+
expect(result).toEqual({
17+
command: 'check-files',
18+
filePaths: ['file.ts'],
19+
showErrors: true,
20+
})
21+
})
22+
23+
it('parses --show-errors after command', () => {
24+
const result = parseArgs(['--check-files', '--show-errors', 'file.ts'])
25+
expect(result.showErrors).toBe(true)
26+
expect(result.filePaths).toEqual(['file.ts'])
27+
})
28+
29+
it('parses --show-errors at end', () => {
30+
const result = parseArgs(['--check-files', 'file.ts', '--show-errors'])
31+
expect(result.showErrors).toBe(true)
32+
expect(result.filePaths).toEqual(['file.ts'])
33+
})
34+
35+
it('parses --overwrite with --show-errors', () => {
36+
const result = parseArgs(['--show-errors', '--overwrite'])
37+
expect(result.command).toBe('overwrite')
38+
expect(result.showErrors).toBe(true)
39+
})
40+
41+
it('defaults to check-all when no command', () => {
42+
const result = parseArgs([])
43+
expect(result.command).toBe('check-all')
44+
})
45+
46+
it('parses --stage-record-file with files', () => {
47+
const result = parseArgs(['--stage-record-file', 'file1.ts', 'file2.ts'])
48+
expect(result).toEqual({
49+
command: 'stage-record-file',
50+
filePaths: ['file1.ts', 'file2.ts'],
51+
showErrors: false,
52+
})
53+
})
54+
55+
it('parses --overwrite without files', () => {
56+
const result = parseArgs(['--overwrite'])
57+
expect(result).toEqual({
58+
command: 'overwrite',
59+
filePaths: [],
60+
showErrors: false,
61+
})
62+
})
63+
64+
it('treats unknown flags as check-all command', () => {
65+
const result = parseArgs(['--unknown-flag'])
66+
expect(result.command).toBe('check-all')
67+
})
68+
})

src/index.integration.test.mts

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -33,65 +33,97 @@ describe('CLI', () => {
3333
}
3434
})
3535

36-
it('runs check on all files when no flag provided', () => {
37-
const output = runCLI()
36+
describe('no flag (default check-all)', () => {
37+
it('runs check on all files when no flag provided', () => {
38+
const output = runCLI()
3839

39-
expect(output).toContain('🔍 Checking all 5 source files for React Compiler errors…')
40-
expect(output).toContain('⚠️ Found 4 React Compiler issues across 2 files')
41-
expect(() => JSON.parse(readFileSync(recordsPath, 'utf8'))).toThrow()
42-
})
40+
expect(output).toContain('🔍 Checking all 5 source files for React Compiler errors…')
41+
expect(output).toContain('⚠️ Found 4 React Compiler issues across 2 files')
42+
expect(() => JSON.parse(readFileSync(recordsPath, 'utf8'))).toThrow()
43+
})
44+
45+
it('--show-errors works with default check-all', () => {
46+
const output = runCLI(['--show-errors'])
4347

44-
it('accepts --check-files flag with file arguments', () => {
45-
const output = runCLI(['--check-files', 'src/bad-component.tsx', 'src/bad-hook.ts'])
46-
47-
expect(output).toContain('🔍 Checking 2 files for React Compiler errors…')
48-
expect(output).toContain('React Compiler errors have increased in:')
49-
expect(output).toContain('• src/bad-component.tsx: +1')
50-
expect(output).toContain('• src/bad-hook.ts: +3')
51-
expect(output).toContain('Please fix the errors and run the command again.')
52-
expect(output).not.toContain('src/good-component.tsx')
53-
expect(output).not.toContain('src/good-hook.ts')
54-
expect(() => JSON.parse(readFileSync(recordsPath, 'utf8'))).toThrow()
48+
expect(output).toContain('Found 4 React Compiler issues')
49+
expect(output).toContain('Detailed errors:')
50+
expect(output).toMatch(/Line \d+:/)
51+
})
5552
})
5653

57-
it('errors when file does not exist', () => {
58-
const output = runCLI(['--check-files', 'src/nonexistent-file.tsx'])
54+
describe('--check-files', () => {
55+
it('accepts --check-files flag with file arguments', () => {
56+
const output = runCLI(['--check-files', 'src/bad-component.tsx', 'src/bad-hook.ts'])
5957

60-
expect(output).toContain('File not found: src/nonexistent-file.tsx')
61-
})
58+
expect(output).toContain('🔍 Checking 2 files for React Compiler errors…')
59+
expect(output).toContain('React Compiler errors have increased in:')
60+
expect(output).toContain('• src/bad-component.tsx: +1')
61+
expect(output).toContain('• src/bad-hook.ts: +3')
62+
expect(output).toContain('Please fix the errors and run the command again.')
63+
expect(output).not.toContain('src/good-component.tsx')
64+
expect(output).not.toContain('src/good-hook.ts')
65+
expect(() => JSON.parse(readFileSync(recordsPath, 'utf8'))).toThrow()
66+
})
67+
68+
it('errors when file does not exist', () => {
69+
const output = runCLI(['--check-files', 'src/nonexistent-file.tsx'])
70+
71+
expect(output).toContain('File not found: src/nonexistent-file.tsx')
72+
})
73+
74+
it('handles shell-escaped file paths with $ character', () => {
75+
// Simulate what CI tools do when passing filenames with $ through shell variables
76+
const output = runCLI(['--check-files', 'src/route.\\$param.tsx'])
77+
78+
expect(output).toContain('🔍 Checking 1 file for React Compiler errors…')
79+
expect(output).not.toContain('File not found')
80+
})
6281

63-
it('handles shell-escaped file paths with $ character', () => {
64-
// Simulate what CI tools do when passing filenames with $ through shell variables
65-
const output = runCLI(['--check-files', 'src/route.\\$param.tsx'])
82+
it('--show-errors shows detailed error info', () => {
83+
const output = runCLI(['--check-files', '--show-errors', 'src/bad-hook.ts'])
6684

67-
expect(output).toContain('🔍 Checking 1 file for React Compiler errors…')
68-
expect(output).not.toContain('File not found')
85+
expect(output).toContain('React Compiler errors have increased')
86+
expect(output).toContain('Detailed errors:')
87+
expect(output).toContain(
88+
'src/bad-hook.ts: Line 6: Cannot access refs during render (x3)',
89+
)
90+
})
6991
})
7092

71-
it('accepts --overwrite flag', () => {
72-
const output = runCLI(['--overwrite'])
73-
74-
expect(output).toContain(
75-
'🔍 Checking all 5 source files for React Compiler errors and recreating records…',
76-
)
77-
expect(output).toContain(
78-
'✅ Records saved to .react-compiler.rec.json. Found 4 total React Compiler issues across 2 files',
79-
)
80-
81-
const records = JSON.parse(readFileSync(recordsPath, 'utf8'))
82-
expect(records.recordVersion).toBe(1)
83-
expect(records['react-compiler-version']).toBe('1.0.0')
84-
expect(records.files).toEqual({
85-
'src/bad-component.tsx': {
86-
CompileError: 1,
87-
},
88-
'src/bad-hook.ts': {
89-
CompileError: 3,
90-
},
93+
describe('--overwrite', () => {
94+
it('accepts --overwrite flag', () => {
95+
const output = runCLI(['--overwrite'])
96+
97+
expect(output).toContain(
98+
'🔍 Checking all 5 source files for React Compiler errors and recreating records…',
99+
)
100+
expect(output).toContain(
101+
'✅ Records saved to .react-compiler.rec.json. Found 4 total React Compiler issues across 2 files',
102+
)
103+
104+
const records = JSON.parse(readFileSync(recordsPath, 'utf8'))
105+
expect(records.recordVersion).toBe(1)
106+
expect(records['react-compiler-version']).toBe('1.0.0')
107+
expect(records.files).toEqual({
108+
'src/bad-component.tsx': {
109+
CompileError: 1,
110+
},
111+
'src/bad-hook.ts': {
112+
CompileError: 3,
113+
},
114+
})
115+
})
116+
117+
it('--show-errors shows detailed errors while saving', () => {
118+
const output = runCLI(['--overwrite', '--show-errors'])
119+
120+
expect(output).toContain('Records saved to')
121+
expect(output).toContain('Detailed errors:')
122+
expect(output).toMatch(/Line \d+:/)
91123
})
92124
})
93125

94-
describe('--stage-record-file flag', () => {
126+
describe('--stage-record-file', () => {
95127
it('exits cleanly when no files provided', () => {
96128
const output = runCLI(['--stage-record-file'])
97129

@@ -113,6 +145,14 @@ describe('CLI', () => {
113145
expect(output).toContain('• src/bad-hook.ts: +3')
114146
})
115147

148+
it('--stage-record-file --show-errors shows detailed errors', () => {
149+
const output = runCLI(['--stage-record-file', '--show-errors', 'src/bad-hook.ts'])
150+
151+
expect(output).toContain('React Compiler errors have increased')
152+
expect(output).toContain('Detailed errors:')
153+
expect(output).toMatch(/Line \d+:/)
154+
})
155+
116156
it('checks provided files with existing records', () => {
117157
// First create records
118158
runCLI(['--overwrite'])

0 commit comments

Comments
 (0)