Skip to content

Migrate client from Create React App (CRA) to Vite #53

Description

@lupita-hom

Summary

Migrate the RunWatch client from Create React App (CRA) + CRACO to Vite as the build toolchain. CRA is abandoned (no react-scripts@6 will ever ship), and its frozen dependency tree carries 6 unfixable high-severity vulnerabilities (nth-check → css-select → svgo → @svgr → react-scripts) that cannot be patched without migrating off CRA entirely.


Why Migrate

Security (primary driver)

CRA pins transitive dependencies that have known vulnerabilities with no upgrade path:

  • nth-check@<2.0.1 — ReDoS (high severity)
  • svgo@1.xcss-select@2.x — locked chain, cannot bump without breaking

These are build-time only (not shipped to users), but they trigger Dependabot alerts and fail strict audit policies. PR #52 resolved all fixable high vulnerabilities; these 6 remain because react-scripts@5.0.1 locks the dependency tree.

CRA is dead

  • Last release: react-scripts@5.0.1 (April 2022)
  • Facebook officially recommends migrating to frameworks or Vite
  • No security patches, no bug fixes, no compatibility updates
  • The project uses CRACO (@craco/craco@7.1.0) to work around CRA limitations (Tailwind PostCSS integration) — a workaround for a tool that no longer needs working around

Developer experience

  • CRA dev server is slow (Webpack 5 cold starts)
  • Vite offers near-instant HMR via native ES modules
  • Vite builds are significantly faster (esbuild + Rollup vs Webpack)

Dependency bloat

  • react-scripts pulls in hundreds of transitive dependencies (Webpack, Babel, ESLint configs, Jest configs, all bundled)
  • Vite has a fraction of the dependency footprint
  • The --legacy-peer-deps flag is already required for npm install due to peer dependency conflicts (typescript@5 vs CRA's typescript@^3 || ^4)

Current Client Architecture (Audit)

Stack

Component Current Notes
Build tool react-scripts@5.0.1 + @craco/craco@7.1.0 CRA with CRACO override
Framework React 19 + React DOM 19 Standard SPA
Routing react-router-dom@7.2.0 BrowserRouter, 7 routes
Styling Tailwind CSS 3.4 + PostCSS + Autoprefixer Custom dark theme (Material Design 3 tokens)
Charts chart.js@4 + react-chartjs-2@5 Dashboard metrics
HTTP axios@1.8.1 API service
WebSocket socket.io-client@4.8.1 Real-time updates
Notifications react-toastify@11 Toast messages
TypeScript typescript@5.7.3 tsconfig present, but all source files are .js/.jsx
Testing @testing-library/react@16 + Jest (via react-scripts) No test files currently exist
Language JavaScript (JSX) 18 source files, ~5,400 lines total

File structure

client/
├── src/
│   ├── api/            # apiService.js, socketService.js
│   ├── common/
│   │   ├── components/ # Layout, StatusChip, AdminTokenDialog
│   │   ├── context/    # AdminTokenContext
│   │   └── utils/      # statusHelpers
│   ├── features/
│   │   ├── dashboard/  # Dashboard.jsx
│   │   ├── repository/ # RepositoryView.jsx
│   │   ├── runners/    # RunnersView.jsx
│   │   ├── settings/   # Settings.jsx, SyncHistoryDetails.jsx
│   │   ├── stats/      # RepositoryStats.jsx
│   │   └── workflows/  # WorkflowDetails.jsx, WorkflowHistory.jsx
│   ├── App.js          # Router + Layout
│   ├── App.css         # Global styles
│   ├── index.js        # Entry point (ReactDOM.createRoot)
│   └── index.css       # Tailwind directives
├── public/             # index.html, favicon, manifest
├── craco.config.js     # Tailwind PostCSS integration
├── tailwind.config.js  # Custom theme (Material Design 3 tokens)
├── tsconfig.json       # TypeScript config (unused — all files are JS)
├── Dockerfile          # Multi-stage: node:20-alpine → nginx:alpine
├── nginx.conf          # SPA fallback + API/WS proxy
└── docker-entrypoint.sh # Runtime env var replacement in built JS

CRA-specific patterns in use

  1. REACT_APP_* env varsREACT_APP_API_URL, REACT_APP_WEBSOCKET_URL (baked at build time, replaced at runtime via docker-entrypoint.sh sed)
  2. %PUBLIC_URL% — used in public/index.html for favicon/manifest paths
  3. CRACO config — custom PostCSS/Tailwind integration (replaces CRA's locked PostCSS config)
  4. CI=false npm run build — suppresses CRA's "treat warnings as errors" behavior in CI
  5. npx react-scripts test — Jest runner (no actual test files exist)
  6. baseUrl: "src" in tsconfig — absolute imports from src/

Docker build pattern

node:20-alpine → npm ci --legacy-peer-deps → craco build → nginx:alpine

The docker-entrypoint.sh uses sed to replace hardcoded API URLs in the built JS bundles at container startup — this pattern works with any bundler's output.


Migration Plan

Recommended target: Vite 6 + React plugin

Vite is the natural successor for CRA React SPAs: same developer model (index.html entry, dev server, build command), but faster and actively maintained.

Phase 1: Scaffolding & Config (Low risk)

  1. Install Vite and plugins

    npm install --save-dev vite @vitejs/plugin-react
    
  2. Create vite.config.js

    import { defineConfig } from "vite";
    import react from "@vitejs/plugin-react";
    
    export default defineConfig({
      plugins: [react()],
      resolve: {
        alias: { src: "/src" }, // replaces tsconfig baseUrl
      },
      server: {
        port: 3000,
        proxy: {
          "/api": "http://localhost:5001",
          "/socket.io": { target: "http://localhost:5001", ws: true },
        },
      },
    });
  3. Move public/index.htmlindex.html (project root)

    • Replace %PUBLIC_URL% with empty string or relative paths
    • Add <script type="module" src="/src/index.jsx"></script> to <body>
  4. Update tailwind.config.js and postcss.config.js

    • Vite has native PostCSS support — no CRACO needed
    • Create a standard postcss.config.js with tailwindcss + autoprefixer

Phase 2: Source Changes (Low risk)

  1. Rename entry point: src/index.jssrc/index.jsx (Vite requires explicit JSX extensions, or configure esbuild.loader in vite config)

  2. Replace REACT_APP_* env vars with VITE_*

    • process.env.REACT_APP_API_URLimport.meta.env.VITE_API_URL
    • process.env.REACT_APP_WEBSOCKET_URLimport.meta.env.VITE_WEBSOCKET_URL
    • Only 3 occurrences across 2 files (apiService.js, socketService.js)
  3. Remove reportWebVitals.js — CRA boilerplate, not used meaningfully

  4. Update absolute imports — if using src/ prefix imports, configure Vite alias (see step 2)

Phase 3: Package.json & Cleanup (Medium risk)

  1. Update scripts

    {
      "scripts": {
        "dev": "vite",
        "build": "vite build",
        "preview": "vite preview",
        "test": "vitest"
      }
    }
  2. Remove CRA dependencies

    npm uninstall react-scripts @craco/craco
    

    This single step eliminates the entire frozen dependency tree including all 6 unfixable high vulns.

  3. Delete CRA configs: craco.config.js (replaced by vite.config.js + postcss.config.js)

  4. Clean up tsconfig.json — remove CRA-specific settings, update for Vite (or remove entirely since the project is pure JS)

  5. Remove --legacy-peer-deps requirement — with CRA gone, peer dep conflicts should resolve

Phase 4: Docker & CI (Medium risk)

  1. Update Dockerfile

    • Build output moves from build/ to dist/
    • COPY --from=build /app/dist /usr/share/nginx/html
    • Remove --legacy-peer-deps from npm ci if no longer needed
  2. Update docker-entrypoint.sh

    • Same sed replacement logic works — just ensure the VITE_* defaults match
    • Vite builds output to dist/assets/*.js instead of build/static/js/*.js
  3. Update CI workflow (.github/workflows/ci.yml)

    • Replace craco build / npm run build — already uses npm run build so just works
    • Replace npx react-scripts test with npx vitest run
    • Remove CI=false hack (Vite doesn't treat warnings as errors by default)
    • Remove --legacy-peer-deps if possible
  4. Update deploy workflow (.github/workflows/deploy-demo.yml)

    • Same build output path change (build/dist/)

Phase 5: Testing (Low risk)

  1. Migrate test runner from Jest (via react-scripts) to Vitest
    • Vitest uses the same API as Jest (describe, it, expect)
    • Since no test files currently exist, this is just config
    • Install: npm install --save-dev vitest @testing-library/jest-dom

Risk Assessment

Area Risk Mitigation
Build output Low Vite produces standard static assets; nginx serves them identically
Runtime env replacement Low docker-entrypoint.sh sed logic works on any JS bundle output
Tailwind CSS None Vite has first-class PostCSS support; simpler than CRA+CRACO
React Router None Framework-agnostic, works identically with Vite
Chart.js / socket.io None Standard npm packages, no bundler coupling
TypeScript None tsconfig exists but no .ts files — can clean up or keep for IDE support
Tests None No test files exist; migrating from Jest to Vitest is config-only
%PUBLIC_URL% Low Replace with relative paths or Vite's base config
Absolute imports (src/) Low Vite alias config handles this

Overall risk: LOW — This is a straightforward migration. The client is a standard React SPA with no exotic CRA features (no service workers, no custom Webpack loaders, no eject, no CSS modules). The CRACO override is only for Tailwind, which Vite handles natively.


Estimated Effort

  • ~2-4 hours for an experienced developer
  • 18 source files, ~5,400 lines — small codebase
  • 3 env var replacements — minimal search-and-replace
  • Only CRA-specific code: index.html template, reportWebVitals.js, craco.config.js

Success Criteria

  • npm run dev starts Vite dev server with HMR
  • npm run build produces production bundle in dist/
  • Docker build succeeds with updated Dockerfile
  • Runtime env var replacement works in Docker
  • All CI checks pass (build, lint, TypeScript, Docker)
  • npm audit shows 0 high/critical vulnerabilities from build tooling
  • react-scripts and @craco/craco fully removed from dependencies
  • --legacy-peer-deps no longer required for npm install
  • GitHub Pages demo deploy works with updated output path

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions