ๆƒฏๆ€ง่šๅˆ ้ซ˜ๆ•ˆ่ฟฝ่ธชๅ’Œ้˜…่ฏปไฝ ๆ„Ÿๅ…ด่ถฃ็š„ๅšๅฎขใ€ๆ–ฐ้—ปใ€็ง‘ๆŠ€่ต„่ฎฏ
้˜…่ฏปๅŽŸๆ–‡ ๅœจๆƒฏๆ€ง่šๅˆไธญๆ‰“ๅผ€

ๆŽจ่่ฎข้˜…ๆบ

ๅš
ๅšๅฎขๅ›ญ - ๅถๅฐ้’—
MyScale Blog
MyScale Blog
ๅš
ๅšๅฎขๅ›ญ - ใ€ๅฝ“่€็‰นใ€‘
I
InfoQ
่…พ
่…พ่ฎฏCDC
aimingoo็š„ไธ“ๆ 
aimingoo็š„ไธ“ๆ 
L
LangChain Blog
ไบบไบบ้ƒฝๆ˜ฏไบงๅ“็ป็†
ไบบไบบ้ƒฝๆ˜ฏไบงๅ“็ป็†
D
DataBreaches.Net
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Engineering at Meta
Engineering at Meta
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
Vercel News
Vercel News
C
Check Point Blog
B
Blog RSS Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
็พŽ
็พŽๅ›ขๆŠ€ๆœฏๅ›ข้˜Ÿ
Stack Overflow Blog
Stack Overflow Blog
Y
Y Combinator Blog
D
Docker
MongoDB | Blog
MongoDB | Blog
้‡
้‡ๅญไฝ
ๅš
ๅšๅฎขๅ›ญ_้ฆ–้กต

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Donโ€™t Fail โ€” They Drift Spilling beans for how i learn for exam๐Ÿ˜"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" โ€” What Actually Happened Comfy Cloudโ€™s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions โ€” here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components โ€” Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cรณmo construรญ un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 ๐Ÿš€ I Built an Ethical Hacking Scanner Tool โ€“ Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points โ€” Here's What I Found About How Markets Really Move EcoTrack AI โ€” Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Automating React Router v6 to v7 Migration with AST Codemods
Ankit raj ยท 2026-05-02 ยท via DEV Community

๐Ÿ”ฌ Case Study: Engineering a Zero-Fault React Router v7 Codemod

React Router v6 to v7 Migration Engine

How we built an AST-powered migration engine that transforms entire codebases
with zero false positives โ€” and what we learned along the way.


Watch Demo

 
Try It


Table of Contents


๐Ÿ’ฅ The Challenge

React Router v7 is one of the most impactful major releases in the React ecosystem. It introduced four simultaneous breaking changes that affect virtually every React application:

mindmap
  root((React Router v7<br/>Breaking Changes))
    ๐Ÿ“ฆ Module Consolidation
      react-router-dom deprecated
      Merged into react-router
      Every import must change
    ๐Ÿšฉ Future Flags
      6 mandatory flags
      Must inject into every Router
      v7_startTransition
      v7_relativeSplatPath
      v7_fetcherPersist
      v7_normalizeFormMethod
      v7_partialHydration
      v7_skipActionErrorRevalidation
    ๐Ÿงน API Deprecation
      json() removed
      defer() removed
      Must unwrap to plain objects
    ๐Ÿ“„ Package.json
      Dependency swap required
      Version bump needed

Enter fullscreen mode Exit fullscreen mode

The Scale of the Problem

For a typical mid-size React application:

Metric Typical Count Manual Time
Files with react-router-dom imports 20โ€“60 ~2 min each
Router components needing flags 1โ€“5 ~5 min each
json() calls in loaders 5โ€“20 ~1 min each
defer() calls in loaders 2โ€“10 ~1 min each
Total estimated manual effort 1.5 โ€“ 3 hours

Our codemod completes this in under 3 seconds.


โŒ Why Regex Fails

The obvious first approach โ€” regex find-and-replace โ€” is fundamentally flawed for code transformation. Here's why:

Problem 1: String and Comment Pollution

// This regex: /react-router-dom/g would incorrectly match:

const docs = "See react-router-dom docs for more info";  // โ† string literal
// TODO: Migrate react-router-dom to react-router         // โ† comment
const url = "https://npm.im/react-router-dom";            // โ† URL in string

Enter fullscreen mode Exit fullscreen mode

A regex cannot distinguish between an import statement and a string containing the same text. AST parsing can.

Problem 2: Multi-line JSX Complexity

// How do you regex-inject 6 props into a component 
// that may or may not already have props, 
// may span multiple lines, and may have children?

<BrowserRouter
  basename="/app"
  // some comment
>
  <App />
</BrowserRouter>

Enter fullscreen mode Exit fullscreen mode

Regex would need to handle every possible formatting variant. AST parsing treats this as a single node with child nodes.

Problem 3: Idempotency

Running a regex twice could produce react-routerr from react-router-dom โ†’ react-router โ†’ (accidental re-match). Or duplicate future flags. AST matching is inherently idempotent โ€” it matches structural patterns, not character sequences.

graph LR
    subgraph Regex["โŒ Regex Approach"]
        R1["Match text<br/>patterns"] --> R2["Risk: strings,<br/>comments, URLs"]
        R2 --> R3["Risk: double<br/>replacement"]
        R3 --> R4["โŒ False positives<br/>likely"]
    end

    subgraph AST["โœ… AST Approach"]
        A1["Parse syntax<br/>tree"] --> A2["Match node<br/>types exactly"]
        A2 --> A3["Mutate only<br/>target nodes"]
        A3 --> A4["โœ… Zero false<br/>positives"]
    end

    style Regex fill:#fef2f2,stroke:#ef4444
    style AST fill:#f0fdf4,stroke:#10b981
    style R4 fill:#ef4444,stroke:#dc2626,color:#fff
    style A4 fill:#10b981,stroke:#059669,color:#fff

Enter fullscreen mode Exit fullscreen mode


๐Ÿง  Our Solution: AST-Based Transforms

We chose @ast-grep/napi โ€” a Rust-based AST tool exposed via Node.js N-API bindings. It's ~100ร— faster than Babel-based alternatives and supports structural pattern matching out of the box.

How AST Matching Works

Instead of matching character sequences, we match tree structures:

Source Code:   import { Link, Route } from 'react-router-dom';

AST Tree:      import_statement
               โ”œโ”€โ”€ import_clause
               โ”‚   โ””โ”€โ”€ named_imports
               โ”‚       โ”œโ”€โ”€ import_specifier ("Link")
               โ”‚       โ””โ”€โ”€ import_specifier ("Route")
               โ””โ”€โ”€ string ("react-router-dom")    โ† We match THIS node

Enter fullscreen mode Exit fullscreen mode

Our pattern import { $$$IMPORTS } from 'react-router-dom' matches the structural shape of the AST, not the text. This means:

  • โœ… It matches regardless of whitespace or formatting
  • โœ… It never matches inside strings or comments
  • โœ… It preserves all import specifiers exactly as written
  • โœ… It preserves inline comments and type annotations

๐Ÿ—๏ธ Architecture Deep-Dive

System Architecture

sequenceDiagram
    actor User
    participant CLI as apply-codemod.js
    participant Backup as Rollback Manager
    participant FS as File System
    participant AST as AST Engine<br/>(ast-grep/Rust)
    participant Report as Report Generator

    User->>CLI: node apply-codemod.js ./my-app

    Note over CLI,FS: Phase 1 โ€” Safety Net
    CLI->>FS: Scan target directory
    FS-->>CLI: File list (34 files)
    CLI->>Backup: Create backup with SHA-256 hashes
    Backup->>FS: Snapshot all source files
    Backup-->>CLI: โœ… Backup complete

    Note over CLI,FS: Phase 2 โ€” Package Migration
    CLI->>FS: Read package.json
    CLI->>FS: Replace react-router-dom โ†’ react-router@7

    Note over CLI,AST: Phase 3 โ€” AST Transforms
    loop Every .ts/.tsx/.js/.jsx file
        CLI->>FS: Read source file
        CLI->>AST: update-imports.ts
        AST-->>CLI: Rewritten imports
        CLI->>AST: add-future-flags.ts
        AST-->>CLI: Flags injected/merged
        CLI->>AST: remove-json-defer.ts
        AST-->>CLI: APIs unwrapped
        CLI->>FS: Write transformed file
    end

    Note over CLI,Report: Phase 4 โ€” Verification
    CLI->>Report: Generate HTML/JSON report
    Report-->>User: ๐Ÿ“Š migration-report.html
    CLI-->>User: โœ… Migration complete!

Enter fullscreen mode Exit fullscreen mode

Transform Pipeline Detail

Each transform is a pure function: (fileInfo) โ†’ string

graph TB
    Input["๐Ÿ“„ Source File"] --> T1

    subgraph Pipeline["Transform Pipeline (per file)"]
        T1["update-imports.ts<br/><i>react-router-dom โ†’ react-router</i>"] --> T2
        T2["add-future-flags.ts<br/><i>Inject/merge 6 v7 flags</i>"] --> T3
        T3["remove-json-defer.ts<br/><i>Unwrap deprecated APIs</i>"]
    end

    T3 --> Output["๐Ÿ“„ Transformed File"]

    T1 -.- N1["AST: import_statement<br/>with string 'react-router-dom'"]
    T2 -.- N2["AST: jsx_element<br/>BrowserRouter / HashRouter / etc."]
    T3 -.- N3["AST: call_expression<br/>json(...) / defer(...)"]

    style Input fill:#fef3c7,stroke:#f59e0b
    style Output fill:#d1fae5,stroke:#10b981
    style Pipeline fill:#f8fafc,stroke:#94a3b8
    style N1 fill:#dbeafe,stroke:#3b82f6,color:#1e40af
    style N2 fill:#dbeafe,stroke:#3b82f6,color:#1e40af
    style N3 fill:#dbeafe,stroke:#3b82f6,color:#1e40af

Enter fullscreen mode Exit fullscreen mode


โญ Engineering Highlights

1. The Smart-Merge Algorithm

The hardest transform isn't import rewriting โ€” it's future flag injection. The challenge: a developer may have already added some flags manually. Blindly injecting all 6 would create duplicates.

Our solution queries the AST for the future prop's object literal, extracts existing flag names, and only appends the missing ones:

// Simplified smart-merge logic
const existingFlags = ["v7_startTransition", "v7_relativeSplatPath"];
const allRequiredFlags = [
  "v7_relativeSplatPath", "v7_startTransition", 
  "v7_fetcherPersist", "v7_normalizeFormMethod",
  "v7_partialHydration", "v7_skipActionErrorRevalidation"
];

// Only inject what's missing
const missingFlags = allRequiredFlags.filter(f => !existingFlags.includes(f));
// โ†’ ["v7_fetcherPersist", "v7_normalizeFormMethod", 
//    "v7_partialHydration", "v7_skipActionErrorRevalidation"]

Enter fullscreen mode Exit fullscreen mode

This guarantees idempotent execution โ€” running the codemod 10 times produces the exact same output as running it once.

graph LR
    A["<BrowserRouter<br/>future={{ v7_startTransition: true }}/>"] 
    --> B["Smart-Merge<br/>Engine"]
    --> C["<BrowserRouter<br/>future={{<br/>  v7_startTransition: true,<br/>  v7_relativeSplatPath: true,<br/>  v7_fetcherPersist: true,<br/>  v7_normalizeFormMethod: true,<br/>  v7_partialHydration: true,<br/>  v7_skipActionErrorRevalidation: true<br/>}}/>"]

    B -.- D["Only 5 flags added<br/>(1 already existed)"]

    style A fill:#fef3c7,stroke:#f59e0b
    style C fill:#d1fae5,stroke:#10b981
    style D fill:#f0f9ff,stroke:#3b82f6,color:#1e40af

Enter fullscreen mode Exit fullscreen mode

2. Bypassing Infrastructure Failures

During development, the official npx codemod workflow CLI consistently failed with unresolvable schema validation errors:

Error: no variant of enum StepAction found in flattened data
Error: missing field `schema_version`
Error: Package too large: 1087194363 bytes

Enter fullscreen mode Exit fullscreen mode

Rather than abandoning the project, we took a dual approach:

  1. Custom Node.js Orchestrator (apply-codemod.js) โ€” A robust, zero-dependency runner that dynamically compiles TypeScript transforms via ts-node and applies them directly. This is the primary way users run the codemod locally.

  2. Fixed Workflow for Registry โ€” We reverse-engineered the correct codemod.yaml + workflow.yaml schema by scaffolding a reference project with codemod init, then adapted our transforms to fit. The result is a published registry package that works with npx codemod react-router-v6-to-v7.

Key insight: A resilient engine that works is worth more than a perfect integration that doesn't.

3. The Rollback System

Every migration creates a .codemod-backup/ directory containing:

graph TD
    subgraph Backup[".codemod-backup/"]
        M["manifest.json<br/><i>File list + SHA-256 hashes</i>"]
        F["files/<br/><i>Complete file snapshots</i>"]
    end

    subgraph Rollback["--rollback"]
        R1["Read manifest"] --> R2["Verify hash integrity"]
        R2 --> R3["Restore original files"]
        R3 --> R4["Clean up backup"]
    end

    Backup --> Rollback

    style Backup fill:#dbeafe,stroke:#3b82f6
    style Rollback fill:#d1fae5,stroke:#10b981

Enter fullscreen mode Exit fullscreen mode

  • Integrity verification: Each file is hash-checked before restore
  • Atomic restore: All-or-nothing โ€” if any file fails integrity, the rollback aborts
  • Clean exit: Backup directory is removed after successful rollback (unless --keep-backup)

4. Post-Migration Reporting

The HTML report generator produces a professional, dark-mode-aware dashboard:

Metric What It Shows
Files Scanned Total source files found in target
Files Modified How many were actually changed
False Positives Always 0 โ€” verified post-migration
TypeScript Status tsc --noEmit compilation result
Per-File Detail Lines added/removed for each file

๐Ÿ† Real-World Validation

We deployed the codemod against multiple real-world open-source repositories to prove it works beyond synthetic fixtures.

react-admin (TypeScript, Large)

  • 45 TypeScript files scanned in milliseconds
  • Handled legacy duplicate react-router-dom entries in package.json
  • Correctly rewrote isolated react-router-dom imports without touching adjacent react-admin or react-dom imports

react-petstore (JavaScript, Medium)

  • 34 source files processed
  • 15 files modified โ€” imports rewritten, future flags injected
  • All component formatting preserved exactly
  • Test files with MemoryRouter correctly updated

Validation Matrix

graph LR
    subgraph Tested["โœ… Fully Tested"]
        RA["react-admin<br/>45 files, 3 modified"]
        RP["react-petstore<br/>34 files, 15 modified"]
    end

    subgraph Pending["โš ๏ธ Open Issues"]
        MC["medicine-cabinet"]
        EE["etp-express"]
    end

    subgraph Skipped["โญ๏ธ Already v7"]
        CT["Cashtab"]
    end

    style Tested fill:#d1fae5,stroke:#10b981
    style Pending fill:#fef3c7,stroke:#f59e0b
    style Skipped fill:#f1f5f9,stroke:#94a3b8

Enter fullscreen mode Exit fullscreen mode

Repository Stack Result False Positives
react-admin TypeScript + v6 โœ… All transforms applied cleanly 0
react-petstore JavaScript + v6 โœ… All transforms applied cleanly 0
medicine-cabinet JavaScript + v6 โš ๏ธ Open issue, Dependabot PR closed โ€”
etp-express TypeScript + v6 โš ๏ธ Open migration issue โ€”
Cashtab Already v7 โญ๏ธ Skipped (no changes needed) โ€”

๐Ÿ“š Lessons Learned

1. AST > Regex, Always

For any code transformation that needs to be reliable at scale, AST-based approaches are the only viable path. The upfront complexity pays for itself immediately in zero false positives and zero edge-case debugging.

2. Build the Bypass First

When infrastructure fails (and it will), having a direct execution path saves the project. Our custom CLI (apply-codemod.js) was built in response to CLI failures and ended up being the most robust part of the system.

3. Idempotency is Non-Negotiable

The smart-merge pattern โ€” check what exists, only add what's missing โ€” should be the default for any code transformation tool. Developers will run your tool multiple times. It must be safe every time.

4. Test with Real Code, Not Just Fixtures

Synthetic test fixtures caught structural correctness. Real-world repos caught edge cases we never imagined โ€” duplicate dependency entries, mixed import styles, unusual formatting patterns.


๐Ÿ Conclusion

This project proves that AST-based codemods are the only viable path for enterprise-scale React migrations. By combining structural pattern matching with a resilient custom orchestrator, we built a tool that:

  • โœ… Transforms codebases in seconds, not hours
  • โœ… Guarantees zero false positives via AST node matching
  • โœ… Runs idempotently with smart-merge logic
  • โœ… Provides full backup, rollback, and reporting
  • โœ… Is published and available as a one-liner on the Codemod Registry
graph LR
    A["๐Ÿ• 2+ hours<br/>manual migration"] -->|"Replaced by"| B["โšก 3 seconds<br/>automated migration"]

    style A fill:#fef2f2,stroke:#ef4444,color:#991b1b
    style B fill:#f0fdf4,stroke:#10b981,color:#065f46

Enter fullscreen mode Exit fullscreen mode