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.x → css-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
REACT_APP_* env vars — REACT_APP_API_URL, REACT_APP_WEBSOCKET_URL (baked at build time, replaced at runtime via docker-entrypoint.sh sed)
%PUBLIC_URL% — used in public/index.html for favicon/manifest paths
- CRACO config — custom PostCSS/Tailwind integration (replaces CRA's locked PostCSS config)
CI=false npm run build — suppresses CRA's "treat warnings as errors" behavior in CI
npx react-scripts test — Jest runner (no actual test files exist)
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)
-
Install Vite and plugins
npm install --save-dev vite @vitejs/plugin-react
-
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 },
},
},
});
-
Move public/index.html → index.html (project root)
- Replace
%PUBLIC_URL% with empty string or relative paths
- Add
<script type="module" src="/src/index.jsx"></script> to <body>
-
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)
-
Rename entry point: src/index.js → src/index.jsx (Vite requires explicit JSX extensions, or configure esbuild.loader in vite config)
-
Replace REACT_APP_* env vars with VITE_*
process.env.REACT_APP_API_URL → import.meta.env.VITE_API_URL
process.env.REACT_APP_WEBSOCKET_URL → import.meta.env.VITE_WEBSOCKET_URL
- Only 3 occurrences across 2 files (
apiService.js, socketService.js)
-
Remove reportWebVitals.js — CRA boilerplate, not used meaningfully
-
Update absolute imports — if using src/ prefix imports, configure Vite alias (see step 2)
Phase 3: Package.json & Cleanup (Medium risk)
-
Update scripts
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest"
}
}
-
Remove CRA dependencies
npm uninstall react-scripts @craco/craco
This single step eliminates the entire frozen dependency tree including all 6 unfixable high vulns.
-
Delete CRA configs: craco.config.js (replaced by vite.config.js + postcss.config.js)
-
Clean up tsconfig.json — remove CRA-specific settings, update for Vite (or remove entirely since the project is pure JS)
-
Remove --legacy-peer-deps requirement — with CRA gone, peer dep conflicts should resolve
Phase 4: Docker & CI (Medium risk)
-
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
-
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
-
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
-
Update deploy workflow (.github/workflows/deploy-demo.yml)
- Same build output path change (
build/ → dist/)
Phase 5: Testing (Low risk)
- 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
Summary
Migrate the RunWatch client from Create React App (CRA) + CRACO to Vite as the build toolchain. CRA is abandoned (no
react-scripts@6will 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.x→css-select@2.x— locked chain, cannot bump without breakingThese 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.1locks the dependency tree.CRA is dead
react-scripts@5.0.1(April 2022)@craco/craco@7.1.0) to work around CRA limitations (Tailwind PostCSS integration) — a workaround for a tool that no longer needs working aroundDeveloper experience
Dependency bloat
react-scriptspulls in hundreds of transitive dependencies (Webpack, Babel, ESLint configs, Jest configs, all bundled)--legacy-peer-depsflag is already required fornpm installdue to peer dependency conflicts (typescript@5vs CRA'stypescript@^3 || ^4)Current Client Architecture (Audit)
Stack
react-scripts@5.0.1+@craco/craco@7.1.0react-router-dom@7.2.0BrowserRouter, 7 routeschart.js@4+react-chartjs-2@5axios@1.8.1socket.io-client@4.8.1react-toastify@11typescript@5.7.3.js/.jsx@testing-library/react@16+ Jest (via react-scripts)File structure
CRA-specific patterns in use
REACT_APP_*env vars —REACT_APP_API_URL,REACT_APP_WEBSOCKET_URL(baked at build time, replaced at runtime viadocker-entrypoint.shsed)%PUBLIC_URL%— used inpublic/index.htmlfor favicon/manifest pathsCI=false npm run build— suppresses CRA's "treat warnings as errors" behavior in CInpx react-scripts test— Jest runner (no actual test files exist)baseUrl: "src"in tsconfig — absolute imports fromsrc/Docker build pattern
The
docker-entrypoint.shusessedto 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)
Install Vite and plugins
Create
vite.config.jsMove
public/index.html→index.html(project root)%PUBLIC_URL%with empty string or relative paths<script type="module" src="/src/index.jsx"></script>to<body>Update
tailwind.config.jsandpostcss.config.jspostcss.config.jswith tailwindcss + autoprefixerPhase 2: Source Changes (Low risk)
Rename entry point:
src/index.js→src/index.jsx(Vite requires explicit JSX extensions, or configureesbuild.loaderin vite config)Replace
REACT_APP_*env vars withVITE_*process.env.REACT_APP_API_URL→import.meta.env.VITE_API_URLprocess.env.REACT_APP_WEBSOCKET_URL→import.meta.env.VITE_WEBSOCKET_URLapiService.js,socketService.js)Remove
reportWebVitals.js— CRA boilerplate, not used meaningfullyUpdate absolute imports — if using
src/prefix imports, configure Vite alias (see step 2)Phase 3: Package.json & Cleanup (Medium risk)
Update scripts
{ "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "test": "vitest" } }Remove CRA dependencies
This single step eliminates the entire frozen dependency tree including all 6 unfixable high vulns.
Delete CRA configs:
craco.config.js(replaced by vite.config.js + postcss.config.js)Clean up
tsconfig.json— remove CRA-specific settings, update for Vite (or remove entirely since the project is pure JS)Remove
--legacy-peer-depsrequirement — with CRA gone, peer dep conflicts should resolvePhase 4: Docker & CI (Medium risk)
Update Dockerfile
build/todist/COPY --from=build /app/dist /usr/share/nginx/html--legacy-peer-depsfromnpm ciif no longer neededUpdate
docker-entrypoint.shdist/assets/*.jsinstead ofbuild/static/js/*.jsUpdate CI workflow (
.github/workflows/ci.yml)craco build/npm run build— already usesnpm run buildso just worksnpx react-scripts testwithnpx vitest runCI=falsehack (Vite doesn't treat warnings as errors by default)--legacy-peer-depsif possibleUpdate deploy workflow (
.github/workflows/deploy-demo.yml)build/→dist/)Phase 5: Testing (Low risk)
describe,it,expect)npm install --save-dev vitest @testing-library/jest-domRisk Assessment
docker-entrypoint.shsed logic works on any JS bundle output%PUBLIC_URL%baseconfigsrc/)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
index.htmltemplate,reportWebVitals.js,craco.config.jsSuccess Criteria
npm run devstarts Vite dev server with HMRnpm run buildproduces production bundle indist/npm auditshows 0 high/critical vulnerabilities from build toolingreact-scriptsand@craco/cracofully removed from dependencies--legacy-peer-depsno longer required fornpm install