Skip to main content

Installation

Install React Doctor as a dependency:
npm install react-doctor

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>
directory
string
required
Absolute or relative path to the project directory to scan
options
DiagnoseOptions
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)
}
lint
boolean
default:"true"
Enable or disable linting checks using oxlint
deadCode
boolean
default:"true"
Enable or disable dead code detection using Knip
includePaths
string[]
default:"[]"
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
}
diagnostics
Diagnostic[]
Array of all detected issues, each containing file path, rule, severity, message, and location information
score
ScoreResult | null
Health score object with score (0-100) and label. null when telemetry is unavailable.
project
ProjectInfo
Project metadata including React version, framework, TypeScript status, and file count
elapsedMilliseconds
number
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:
pre-commit.ts
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);
}

Build docs developers (and LLMs) love