phy-css-dead-code

GitHub 作者 PHY041

Unused CSS detector and dead selector auditor. Scans your stylesheet files against all HTML, JSX, TSX, Vue, and template files to find selectors that are defined but never referenced anywhere in your codebase. Detects CSS framework (Tailwind, Bootstrap, CSS Modules, vanilla CSS, Sass/SCSS) and applies the right analysis strategy for each. Reports estimated byte savings per dead rule, finds !important overuse, duplicate selectors, and orphaned media-query blocks. Generates a safe-deletion plan with exact grep verification commands. Works with PostCSS, Sass, Less, and raw CSS. Zero external API — uses npx and grep only. Triggers on "unused CSS", "dead CSS", "CSS bloat", "remove unused styles", "CSS cleanup", "what CSS can I delete", "/css-dead-code".

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install github:LeoYeAI~openclaw-master-skills~phy-css-dead-code
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/github%3ALeoYeAI~openclaw-master-skills~phy-css-dead-code/file -o phy-css-dead-code.md
# CSS Dead Code Auditor

CSS only grows. Every sprint adds new rules; almost none get removed. A feature gets deleted — the styles stay. A component gets renamed — the old class selectors stay. After a year, 30-40% of your stylesheet is dead weight: downloaded by every user, parsed by every browser, doing nothing.

Point this skill at your project and get a complete inventory of dead selectors, their exact file locations, estimated byte savings, and safe-deletion commands.

**Works with any CSS methodology. No config. No build step. Zero external APIs.**

---

## Trigger Phrases

- "unused CSS", "dead CSS selectors", "CSS dead code"
- "CSS bloat", "remove unused styles", "CSS cleanup"
- "what CSS can I delete", "CSS coverage", "unused classes"
- "Tailwind purge", "Bootstrap unused", "CSS audit"
- "stylesheet cleanup", "orphaned selectors"
- "/css-dead-code"

---

## How to Provide Input

```bash
# Option 1: Audit the current directory
/css-dead-code

# Option 2: Specific CSS file or directory
/css-dead-code src/styles/
/css-dead-code assets/main.css

# Option 3: Specify content directory (where HTML/JSX lives)
/css-dead-code --content src/

# Option 4: Specific framework
/css-dead-code --framework tailwind
/css-dead-code --framework vanilla
/css-dead-code --framework bootstrap

# Option 5: Show only selectors that can be safely deleted (high confidence)
/css-dead-code --safe-only

# Option 6: Estimate size savings only (quick mode)
/css-dead-code --size-estimate

# Option 7: Check a single selector
/css-dead-code --check ".btn-legacy-primary"
```

---

## Step 1: Detect CSS Framework and Project Structure

```bash
# Detect CSS framework from project files
FRAMEWORK="unknown"

if [ -f "tailwind.config.js" ] || [ -f "tailwind.config.ts" ] || [ -f "tailwind.config.cjs" ]; then
  FRAMEWORK="tailwind"
elif grep -r "bootstrap" package.json 2>/dev/null | grep -q "\"bootstrap\""; then
  FRAMEWORK="bootstrap"
elif find . -name "*.module.css" -not -path "*/node_modules/*" 2>/dev/null | head -1 | grep -q "."; then
  FRAMEWORK="css-modules"
elif find . -name "*.scss" -o -name "*.sass" -not -path "*/node_modules/*" 2>/dev/null | head -3 | grep -q "."; then
  FRAMEWORK="sass"
else
  FRAMEWORK="vanilla"
fi

echo "Detected framework: $FRAMEWORK"

# Find all CSS/SCSS/SASS/Less files
CSS_FILES=$(find . \( -name "*.css" -o -name "*.scss" -o -name "*.sass" -o -name "*.less" \) \
  -not -path "*/node_modules/*" \
  -not -path "*/dist/*" \
  -not -path "*/.next/*" \
  -not -path "*/build/*" \
  2>/dev/null)

CSS_COUNT=$(echo "$CSS_FILES" | wc -l | tr -d ' ')
echo "Found $CSS_COUNT CSS/SCSS files"

# Find all template/component files
CONTENT_FILES=$(find . \( -name "*.html" -o -name "*.jsx" -o -name "*.tsx" \
  -o -name "*.vue" -o -name "*.svelte" -o -name "*.hbs" \
  -o -name "*.erb" -o -name "*.blade.php" -o -name "*.pug" \) \
  -not -path "*/node_modules/*" \
  -not -path "*/dist/*" \
  2>/dev/null)

CONTENT_COUNT=$(echo "$CONTENT_FILES" | wc -l | tr -d ' ')
echo "Found $CONTENT_COUNT template/component files to cross-reference"
```

---

## Step 2: Framework-Specific Analysis

### Tailwind CSS

```bash
# Check tailwind.config.js content paths
echo "=== Tailwind Config Check ==="
python3 -c "
import re, json

try:
    with open('tailwind.config.js') as f:
        content = f.read()
    # Extract content array
    content_match = re.search(r'content\s*:\s*\[(.*?)\]', content, re.DOTALL)
    if content_match:
        print('Content paths configured:')
        for line in content_match.group(1).split(','):
            line = line.strip().strip('\"\'')
            if line:
                print(f'  {line}')
    else:
        print('⚠️  No content array found in tailwind.config.js — all classes may be purged!')
except FileNotFoundError:
    print('tailwind.config.js not found')
"

# Find classes actually used in content files
echo ""
echo "=== Tailwind Classes In Use ==="
# Extract all class="" values from content files
grep -rh "class[Name]*=\"[^\"]*\"\|class[Name]*={[^}]*}" . \
  --include="*.jsx" --include="*.tsx" --include="*.html" --include="*.vue" \
  --exclude-dir="node_modules" --exclude-dir="dist" 2>/dev/null | \
  grep -oE '[a-z][a-z0-9:-]+' | sort -u | head -50

# Run Tailwind CLI to see what would be purged
if command -v npx &>/dev/null; then
  echo ""
  echo "Running Tailwind dry-run to check purged classes..."
  npx --yes tailwindcss -i ./input.css -o /tmp/tw-output.css --minify 2>/dev/null
  echo "Output size: $(du -h /tmp/tw-output.css 2>/dev/null | cut -f1)"
fi
```

### Vanilla CSS / SCSS

```python
# Extract all selectors from CSS files and cross-reference with content
import re, os, sys
from pathlib import Path
from collections import defaultdict

def extract_selectors(css_content):
    """Extract all CSS selectors from a stylesheet."""
    # Remove comments
    css = re.sub(r'/\*.*?\*/', '', css_content, flags=re.DOTALL)
    # Remove @-rules (media queries, keyframes) content for now
    css = re.sub(r'@[^{]+\{[^}]+\}', '', css)
    # Extract selectors (before the { block)
    selectors = re.findall(r'([^{}@][^{}]*)\s*\{', css)
    result = []
    for sel_group in selectors:
        # Split on commas for multi-selector rules
        for sel in sel_group.split(','):
            sel = sel.strip()
            if sel and not sel.startswith('@'):
                result.append(sel)
    return result

def extract_class_refs(content):
    """Extract all class and ID references from template/component files."""
    classes = set()
    ids = set()
    # class="foo bar baz"
    for match in re.finditer(r'class(?:Name)?\s*=\s*["\']([^"\']+)["\']', content):
        classes.update(match.group(1).split())
    # className={`...`} dynamic classes
    for match in re.finditer(r'["\']([a-z][a-z0-9-_]+)["\']', content):
        classes.add(match.group(1))
    # id="foo"
    for match in re.finditer(r'\bid\s*=\s*["\']([^"\']+)["\']', content):
        ids.add(match.group(1))
    return classes, ids

# Collect all selectors from CSS files
all_selectors = defaultdict(list)  # selector -> [(file, line_num)]

css_dirs = ['.']
for css_dir in css_dirs:
    for fpath in Path(css_dir).rglob('*.css'):
        if 'node_modules' in str(fpath) or 'dist' in str(fpath):
            continue
        content = fpath.read_text(errors='ignore')
        for sel in extract_selectors(content):
            all_selectors[sel].append(str(fpath))

print(f"Total unique selectors found: {len(all_selectors)}")

# Collect all class/id references from content files
all_classes = set()
all_ids = set()
content_exts = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.erb', '.hbs']
for root, dirs, files in os.walk('.'):
    dirs[:] = [d for d in dirs if d not in {'node_modules', 'dist', '.next', 'build', '.git'}]
    for fname in files:
        if any(fname.endswith(ext) for ext in content_exts):
            fpath = Path(root) / fname
            content = fpath.read_text(errors='ignore')
            c, i = extract_class_refs(content)
            all_classes.update(c)
            all_ids.update(i)

print(f"Unique class references found in templates: {len(all_classes)}")

# Find unused selectors
unused = []
for selector, locations in all_selectors.items():
    # Extract the class name from the selector
    class_matches = re.findall(r'\.([a-zA-Z][a-zA-Z0-9_-]*)', selector)
    id_matches = re.findall(r'#([a-zA-Z][a-zA-Z0-9_-]*)', selector)

    is_used = False
    for cls in class_matches:
        if cls in all_classes:
            is_used = True
            break
    for id_name in id_matches:
        if id_name in all_ids:
            is_used = True
            break

    # Skip element-only selectors (body, h1, p, etc.) — too risky to flag
    if not class_matches and not id_matches:
        continue  # element selector, skip

    if not is_used:
        unused.append((selector, locations))

print(f"\nUnused selectors: {len(unused)}")
for sel, locs in sorted(unused, key=lambda x: len(x[1]), reverse=True)[:3