Installation
Install React Doctor as a dependency:
Basic Usage
import { diagnose } from 'react-doctor/api';
const result = await diagnose('./my-project');
console.log(`Score: ${result.score?.score}`);
console.log(`Found ${result.diagnostics.length} issues`);
API Reference
diagnose()
The main function for scanning a React codebase.
diagnose(directory: string, options?: DiagnoseOptions): Promise<DiagnoseResult>
Absolute or relative path to the project directory to scan
Optional configuration for the scan
Returns: Promise<DiagnoseResult>
Types
DiagnoseOptions
interface DiagnoseOptions {
lint?: boolean; // Enable linting (default: true)
deadCode?: boolean; // Enable dead code detection (default: true)
includePaths?: string[]; // Specific files to scan (for diff mode)
}
Enable or disable linting checks using oxlint
Enable or disable dead code detection using Knip
Array of file paths to scan. When provided, enables diff mode and scans only the specified files. Dead code detection is automatically disabled in diff mode.
DiagnoseResult
interface DiagnoseResult {
diagnostics: Diagnostic[]; // Array of detected issues
score: ScoreResult | null; // Health score (null if offline)
project: ProjectInfo; // Project metadata
elapsedMilliseconds: number; // Scan duration
}
Array of all detected issues, each containing file path, rule, severity, message, and location information
Health score object with score (0-100) and label. null when telemetry is unavailable.
Project metadata including React version, framework, TypeScript status, and file count
Total time taken for the scan in milliseconds
Diagnostic
interface Diagnostic {
filePath: string; // Relative path to the file
plugin: string; // Plugin name (e.g., "react-doctor", "oxlint")
rule: string; // Rule identifier
severity: "error" | "warning";
message: string; // Issue description
help: string; // Suggestion for fixing
line: number; // Line number (0 if not available)
column: number; // Column number (0 if not available)
category: string; // Issue category
weight?: number; // Severity weight for scoring
}
ScoreResult
interface ScoreResult {
score: number; // Health score (0-100)
label: string; // Human-readable label (e.g., "Excellent", "Good")
}
ProjectInfo
interface ProjectInfo {
rootDirectory: string; // Absolute path to project root
projectName: string; // Project name from package.json
reactVersion: string | null;
framework: Framework; // Detected framework
hasTypeScript: boolean;
hasReactCompiler: boolean;
sourceFileCount: number; // Total source files in project
}
Framework
type Framework =
| "nextjs"
| "vite"
| "cra"
| "remix"
| "gatsby"
| "expo"
| "react-native"
| "unknown";
ReactDoctorConfig
interface ReactDoctorConfig {
ignore?: {
rules?: string[]; // Rules to ignore
files?: string[]; // File patterns to ignore
};
lint?: boolean;
deadCode?: boolean;
verbose?: boolean;
diff?: boolean | string; // Diff mode configuration
failOn?: "error" | "warning" | "none";
}
Examples
Basic Scan
import { diagnose } from 'react-doctor/api';
async function scanProject() {
const result = await diagnose('./src');
console.log(`Project: ${result.project.projectName}`);
console.log(`Framework: ${result.project.framework}`);
console.log(`React: ${result.project.reactVersion}`);
console.log(`Score: ${result.score?.score}/100`);
console.log(`Issues: ${result.diagnostics.length}`);
console.log(`Scan time: ${result.elapsedMilliseconds}ms`);
}
scanProject();
Lint Only
Disable dead code detection:
import { diagnose } from 'react-doctor/api';
const result = await diagnose('./src', {
lint: true,
deadCode: false
});
console.log(`Found ${result.diagnostics.length} linting issues`);
Diff Mode
Scan only specific files (useful for pre-commit hooks):
import { diagnose, getDiffInfo, filterSourceFiles } from 'react-doctor/api';
const diffInfo = getDiffInfo('./src', 'main');
if (diffInfo) {
const changedFiles = filterSourceFiles(diffInfo.changedFiles);
const result = await diagnose('./src', {
includePaths: changedFiles
});
console.log(`Scanned ${changedFiles.length} changed files`);
console.log(`Found ${result.diagnostics.length} issues`);
}
Process Diagnostics
Group and analyze diagnostics:
import { diagnose } from 'react-doctor/api';
const result = await diagnose('./src');
// Group by severity
const errors = result.diagnostics.filter(d => d.severity === 'error');
const warnings = result.diagnostics.filter(d => d.severity === 'warning');
console.log(`Errors: ${errors.length}`);
console.log(`Warnings: ${warnings.length}`);
// Group by category
const byCategory = result.diagnostics.reduce((acc, d) => {
acc[d.category] = (acc[d.category] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.log('Issues by category:', byCategory);
// Group by file
const byFile = result.diagnostics.reduce((acc, d) => {
if (!acc[d.filePath]) acc[d.filePath] = [];
acc[d.filePath].push(d);
return acc;
}, {} as Record<string, Diagnostic[]>);
for (const [file, diagnostics] of Object.entries(byFile)) {
console.log(`${file}: ${diagnostics.length} issues`);
}
Custom Reporting
Generate custom reports:
import { diagnose } from 'react-doctor/api';
import fs from 'fs';
const result = await diagnose('./src');
// Generate JSON report
fs.writeFileSync(
'react-doctor-report.json',
JSON.stringify(result, null, 2)
);
// Generate CSV report
const csv = [
'File,Rule,Severity,Line,Message',
...result.diagnostics.map(d =>
`"${d.filePath}","${d.rule}",${d.severity},${d.line},"${d.message}"`
)
].join('\n');
fs.writeFileSync('react-doctor-report.csv', csv);
console.log('Reports generated!');
CI Integration
Use in a Node.js CI script:
import { diagnose } from 'react-doctor/api';
async function ciCheck() {
try {
const result = await diagnose(process.cwd());
const errors = result.diagnostics.filter(d => d.severity === 'error');
if (errors.length > 0) {
console.error(`❌ Found ${errors.length} errors`);
for (const error of errors) {
console.error(
`${error.filePath}:${error.line} - ${error.message}`
);
}
process.exit(1);
}
console.log(`✅ No errors found. Score: ${result.score?.score}/100`);
} catch (error) {
console.error('Scan failed:', error);
process.exit(1);
}
}
ciCheck();
Pre-commit Hook
Create a pre-commit hook using Husky:
import { diagnose, getDiffInfo, filterSourceFiles } from 'react-doctor/api';
import { execSync } from 'child_process';
async function preCommitCheck() {
// Get staged files
const staged = execSync('git diff --cached --name-only --diff-filter=ACM')
.toString()
.trim()
.split('\n')
.filter(f => /\.(ts|tsx|js|jsx)$/.test(f));
if (staged.length === 0) {
console.log('No staged files to check');
return;
}
console.log(`Checking ${staged.length} staged files...`);
const result = await diagnose(process.cwd(), {
includePaths: staged
});
const errors = result.diagnostics.filter(d => d.severity === 'error');
if (errors.length > 0) {
console.error(`\n❌ Found ${errors.length} errors in staged files:\n`);
for (const error of errors) {
console.error(` ${error.filePath}:${error.line}`);
console.error(` ${error.message}`);
console.error(` ${error.help}\n`);
}
process.exit(1);
}
console.log('✅ All staged files passed React Doctor checks');
}
preCommitCheck();
Watch Mode
Implement file watching:
import { diagnose } from 'react-doctor/api';
import chokidar from 'chokidar';
import path from 'path';
const srcDir = path.resolve('./src');
let isScanning = false;
async function scanChangedFiles(changedFiles: string[]) {
if (isScanning) return;
isScanning = true;
try {
const result = await diagnose(srcDir, {
includePaths: changedFiles
});
console.clear();
console.log(`Scanned ${changedFiles.length} files`);
console.log(`Issues: ${result.diagnostics.length}`);
if (result.diagnostics.length > 0) {
for (const d of result.diagnostics) {
console.log(
`${d.severity === 'error' ? '✗' : '⚠'} ${d.filePath}:${d.line} - ${d.message}`
);
}
} else {
console.log('✅ No issues found');
}
} finally {
isScanning = false;
}
}
const watcher = chokidar.watch('src/**/*.{ts,tsx,js,jsx}', {
ignoreInitial: true
});
const changedFiles = new Set<string>();
let debounceTimer: NodeJS.Timeout;
watcher.on('all', (event, filePath) => {
changedFiles.add(path.relative(srcDir, filePath));
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
scanChangedFiles(Array.from(changedFiles));
changedFiles.clear();
}, 1000);
});
console.log('Watching for changes...');
Helper Functions
getDiffInfo()
Get git diff information:
import { getDiffInfo } from 'react-doctor/api';
const diffInfo = getDiffInfo('./src', 'main');
if (diffInfo) {
console.log(`Current branch: ${diffInfo.currentBranch}`);
console.log(`Base branch: ${diffInfo.baseBranch}`);
console.log(`Changed files: ${diffInfo.changedFiles.length}`);
}
filterSourceFiles()
Filter for React/TypeScript source files:
import { filterSourceFiles } from 'react-doctor/api';
const allFiles = ['src/App.tsx', 'README.md', 'src/utils.ts'];
const sourceFiles = filterSourceFiles(allFiles);
// Returns: ['src/App.tsx', 'src/utils.ts']
Error Handling
import { diagnose } from 'react-doctor/api';
try {
const result = await diagnose('./src');
console.log(`Scan completed: ${result.diagnostics.length} issues`);
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('No React dependency')) {
console.error('This is not a React project');
} else {
console.error(`Scan failed: ${error.message}`);
}
}
process.exit(1);
}