Dead Code Finder
A script that detects unused exports, unreferenced files, and dead code in TypeScript projects using the TypeScript compiler API.
Description
Uses ts-prune and custom analysis to find dead code in TypeScript projects — unused exports, orphan files, and unreferenced modules. Outputs a prioritized cleanup list.
Features
- Unused exports detection via
ts-prune - Orphan file detection — files not imported anywhere
- Dead dependency check — packages in
package.jsonnot imported in source - JSON output for CI integration
The Script
#!/bin/bash
# dead-code-finder.sh — Find unused exports and orphan files
set -euo pipefail
SRC_DIR="${1:-src}"
REPORT_FILE="dead-code-report.json"
VIOLATIONS=0
echo "🔍 Dead Code Finder"
echo " Source: $SRC_DIR"
echo ""
# ── 1. Unused Exports (ts-prune) ──
echo "📦 Checking unused exports..."
if command -v npx &> /dev/null && npx ts-prune --help &> /dev/null 2>&1; then
UNUSED_EXPORTS=$(npx ts-prune --project tsconfig.json 2>/dev/null | grep -v '(used in module)' || true)
UNUSED_COUNT=$(echo "$UNUSED_EXPORTS" | grep -c "." || echo "0")
if [ "$UNUSED_COUNT" -gt 0 ]; then
echo " ⚠️ $UNUSED_COUNT unused export(s) found:"
echo "$UNUSED_EXPORTS" | head -20 | while IFS= read -r line; do
echo " $line"
done
if [ "$UNUSED_COUNT" -gt 20 ]; then
echo " ... and $((UNUSED_COUNT - 20)) more"
fi
VIOLATIONS=$((VIOLATIONS + UNUSED_COUNT))
else
echo " ✅ No unused exports"
fi
else
echo " ⏭️ ts-prune not available, skipping export check"
echo " Install: npm install -D ts-prune"
fi
echo ""
# ── 2. Orphan Files ──
echo "📄 Checking for orphan files..."
# Get all TS/JS source files
ALL_FILES=$(find "$SRC_DIR" -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) \
! -path "*/node_modules/*" \
! -path "*/__tests__/*" \
! -path "*/test/*" \
! -name "*.test.*" \
! -name "*.spec.*" \
! -name "*.d.ts" \
! -name "index.ts" \
! -name "index.js" \
! -name "main.ts" \
! -name "app.ts" \
| sort)
ORPHANS=""
ORPHAN_COUNT=0
for file in $ALL_FILES; do
# Get the module name without extension
base=$(basename "$file" | sed 's/\.\(ts\|tsx\|js\|jsx\)$//')
dir=$(dirname "$file")
# Check if this file is imported anywhere
IMPORT_CHECK=$(grep -rl --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
-E "(from|import).*['\"].*${base}['\"]" "$SRC_DIR" 2>/dev/null | grep -v "$file" | head -1 || true)
if [ -z "$IMPORT_CHECK" ]; then
ORPHANS="$ORPHANS\n $file"
ORPHAN_COUNT=$((ORPHAN_COUNT + 1))
fi
done
if [ "$ORPHAN_COUNT" -gt 0 ]; then
echo " ⚠️ $ORPHAN_COUNT potentially orphaned file(s):"
echo -e "$ORPHANS"
VIOLATIONS=$((VIOLATIONS + ORPHAN_COUNT))
else
echo " ✅ No orphan files detected"
fi
echo ""
# ── 3. Unused Dependencies ──
echo "📦 Checking for unused dependencies..."
if [ -f "package.json" ]; then
DEPS=$(node -e "
const pkg = require('./package.json');
const deps = Object.keys(pkg.dependencies || {});
deps.forEach(d => console.log(d));
" 2>/dev/null || true)
UNUSED_DEPS=""
UNUSED_DEP_COUNT=0
for dep in $DEPS; do
# Skip framework deps that might be used implicitly
if [[ "$dep" == @types/* ]] || [[ "$dep" == typescript ]] || [[ "$dep" == eslint* ]]; then
continue
fi
# Check if the dep is imported in source code
IMPORT_FOUND=$(grep -rl --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
-E "(from|require|import).*['\"]${dep}['\"/]" "$SRC_DIR" 2>/dev/null | head -1 || true)
if [ -z "$IMPORT_FOUND" ]; then
UNUSED_DEPS="$UNUSED_DEPS\n $dep"
UNUSED_DEP_COUNT=$((UNUSED_DEP_COUNT + 1))
fi
done
if [ "$UNUSED_DEP_COUNT" -gt 0 ]; then
echo " ⚠️ $UNUSED_DEP_COUNT potentially unused dependencie(s):"
echo -e "$UNUSED_DEPS"
VIOLATIONS=$((VIOLATIONS + UNUSED_DEP_COUNT))
else
echo " ✅ No unused dependencies detected"
fi
else
echo " ⏭️ No package.json found"
fi
echo ""
# ── Summary ──
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ "$VIOLATIONS" -gt 0 ]; then
echo "❌ $VIOLATIONS potential dead code item(s) found"
echo " Review and remove what's truly unused."
exit 1
else
echo "✅ No dead code detected — codebase is clean!"
exit 0
fi
Usage
# Make executable
chmod +x dead-code-finder.sh
# Run against src/
./dead-code-finder.sh src
# Run in CI (fails on violations)
./dead-code-finder.sh src || echo "Dead code found — please clean up"