AI agents: read this page as markdown at /docs/frameworks/react/vite.md, or start from the full AI-readable index at /llms.txt.

React with Vite - Complete Setup Guide

Complete guide for setting up Meticulous with React applications built with Vite.


Overview

Vite is a fast build tool for modern web applications. This guide covers:

  • Recorder installation in index.html
  • CI/CD configuration with static asset upload
  • Authentication handling
  • Common patterns and troubleshooting

Prerequisites:


Quick Start

Step 1: Install Recorder in index.html

Add the Meticulous recorder script to your index.html before any other scripts.

File: index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Your App Name</title>

    <!-- Meticulous recorder - MUST be first script -->
    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
    <script
      data-project-id="YOUR_PROJECT_ID"
      src="https://snippet.meticulous.ai/v1/meticulous.js"
    ></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Important: The recorder must load before your application code to capture all events.


Step 2: Configure GitHub Actions Workflow

Vite builds to static files, so we use the upload-assets action instead of cloud-compute.

File: .github/workflows/meticulous.yml

name: Meticulous

# Important: The workflow needs to run both on pushes to your main branch and on
# pull requests. It needs to run on your main branch because it'll use the results
# from the base commit of the PR on the main branch to compare against.
on:
  push:
    branches:
      - main
  pull_request: {}
  # Important: We need the workflow to be triggered on workflow_dispatch events,
  # so that Meticulous can run the workflow on the base commit to compare
  # against if an existing workflow hasn't run. The meticulous-commit-sha input
  # lets Meticulous ask for a specific commit (e.g. stacked PRs); without it,
  # a dispatched run can only build whatever the branch currently points at.
  workflow_dispatch:
    inputs:
      meticulous-commit-sha:
        description: Commit Meticulous has asked this run to build. Defaults to the branch head.
        required: false

# Important: The workflow needs all the permissions below.
# These permissions are mainly needed to post and update the status check and
# feedback comment on your PR. Meticulous won't work without them.
permissions:
  actions: write
  contents: read
  issues: write
  pull-requests: write
  statuses: read

env:
  # Prefer the dispatched commit when set; otherwise the PR head. On pull_request github.sha is the merge commit,
  # not the PR head SHA that Meticulous looks up.
  METICULOUS_COMMIT_SHA: ${{ github.event.inputs['meticulous-commit-sha'] || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}

jobs:
  test:
    name: Meticulous
    runs-on: ubuntu-latest

    steps:
      # Same workflow file as the upload step — ensure-base dispatches *this*
      # workflow on the base branch. Run it before checkout/build so the base
      # can start while this job continues. Needs no checkout. Pass the same
      # ref as checkout so we pre-warm the base the upload step will ask for.
      - name: Ensure base tests exist
        uses: alwaysmeticulous/report-diffs-action/ensure-base@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          ref: ${{ env.METICULOUS_COMMIT_SHA }}

      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          ref: ${{ env.METICULOUS_COMMIT_SHA }}

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build app
        run: npm run build
        env:
          NODE_ENV: production

      - name: Upload and test
        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-directory: "dist"
          rewrites: |
            [
              { "source": "/(.*)", "destination": "/index.html" }
            ]

Step 3: Add API Token Secret

  1. Get your API token from Meticulous dashboard → Project Settings
  2. Go to GitHub repo → Settings → Secrets and variables → Actions
  3. Create secret named METICULOUS_API_TOKEN with your token

How It Works

upload-assets Action

The upload-assets action:

  1. Uploads your built static files to Meticulous
  2. Serves them on a temporary URL
  3. Runs tests against that URL
  4. Reports diffs back to your PR

Key differences from cloud-compute:

  • Simpler setup (no server needed)
  • Faster for static sites
  • Can't test server-side logic
  • No backend API calls (unless mocked)

Rewrites Configuration

The rewrites parameter handles client-side routing:

[
  { "source": "/(.*)", "destination": "/index.html" }
]

This ensures all routes (/about, /dashboard, etc.) serve index.html, allowing React Router to handle routing.


Common Patterns

Pattern 1: Detect Test Mode

Use window.Meticulous.isRunningAsTest to detect when running as a test:

// In any component
function MyComponent() {
  const isTest = window.Meticulous?.isRunningAsTest

  if (isTest) {
    // Skip animations, use test data, etc.
  }

  return <div>...</div>
}

Pattern 2: Bypass Authentication

// In App.tsx or auth provider
import { useEffect } from 'react'

function App() {
  useEffect(() => {
    if (window.Meticulous?.isRunningAsTest) {
      // Mock authentication for tests
      localStorage.setItem('auth-token', 'test-token')
      localStorage.setItem('user', JSON.stringify({
        id: 'test-user',
        name: 'Test User',
        email: 'test@example.com'
      }))
    }
  }, [])

  return <YourApp />
}

Pattern 3: Mock API Responses

Since there's no backend in upload-assets mode, API calls need to be mocked:

Option 1: Use MSW (Mock Service Worker)

// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)

// src/main.tsx
if (window.Meticulous?.isRunningAsTest && 'serviceWorker' in navigator) {
  const { worker } = await import('./mocks/browser')
  await worker.start()
}

Option 2: Use recorded custom values

// During recording
window.Meticulous?.recordCustomValues?.({
  apiData: await fetchFromAPI()
})

// During replay
const data = window.Meticulous?.isRunningAsTest
  ? window.Meticulous.getCustomValues()?.apiData
  : await fetchFromAPI()

Pattern 4: Handle Environment Variables

Vite exposes environment variables prefixed with VITE_:

const apiUrl = import.meta.env.VITE_API_URL

// Use different URL for tests
const effectiveUrl = window.Meticulous?.isRunningAsTest
  ? 'https://api.test.example.com'
  : apiUrl

Complete Example

File Structure

your-app/
├── src/
│   ├── main.tsx              # Entry point
│   ├── App.tsx               # Main app component
│   ├── components/
│   ├── lib/
│   │   └── auth.ts           # Auth utilities
│   └── mocks/                # MSW mocks (optional)
├── index.html                # Recorder installation
├── vite.config.ts            # Vite configuration
├── .github/
│   └── workflows/
│       └── meticulous.yml    # CI/CD
└── package.json

Example: Protected Route

File: src/App.tsx

import { useEffect, useState } from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'

function App() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Mock auth for tests
    if (window.Meticulous?.isRunningAsTest) {
      setUser({
        id: 'test-user-123',
        name: 'Test User',
        email: 'test@example.com'
      })
      setLoading(false)
      return
    }

    // Normal auth flow
    checkAuth().then(user => {
      setUser(user)
      setLoading(false)
    })
  }, [])

  if (loading) {
    return <div>Loading...</div>
  }

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route
          path="/dashboard"
          element={user ? <Dashboard user={user} /> : <Navigate to="/login" />}
        />
        <Route path="/login" element={<LoginPage />} />
      </Routes>
    </BrowserRouter>
  )
}

export default App

CI/CD Configuration Details

Environment Variables

Add build-time environment variables:

- name: Build app
  run: npm run build
  env:
    VITE_API_URL: "https://api.example.com"
    VITE_APP_NAME: "My App"
    NODE_ENV: production

In code:

const apiUrl = import.meta.env.VITE_API_URL

Custom Build Directory

If Vite outputs to a different directory:

- name: Upload and test
  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-directory: "build" # Change from default "dist"
    rewrites: |
      [
        { "source": "/(.*)", "destination": "/index.html" }
      ]

Multiple Rewrites

For complex routing:

rewrites: |
  [
    { "source": "/api/(.*)", "destination": "/api/index.html" },
    { "source": "/(.*)", "destination": "/index.html" }
  ]

Troubleshooting

Issue: Recorder Not Loading

Symptom: window.Meticulous is undefined

Checks:

  1. Verify recorder script is in index.html <head>
  2. Check project ID is correct
  3. Check for CSP blocking (console errors)
  4. Verify script loads before src/main.tsx

Fix: Ensure correct order in index.html:

<head>
  <!-- Recorder FIRST -->
  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>

  <!-- Then other scripts -->
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>

Issue: Routes Return 404

Symptom: Direct navigation to /about returns 404

Cause: Missing rewrite configuration

Fix: Add rewrites to workflow:

rewrites: |
  [
    { "source": "/(.*)", "destination": "/index.html" }
  ]

Issue: API Calls Fail

Symptom: API requests fail during tests

Cause: No backend in upload-assets mode

Solutions:

Option 1: Mock APIs with MSW (recommended)

See Pattern 3 above. Keeps you on upload-assets, which is the simplest and most reliable workflow.

Option 2: Switch to upload-container (if you have a backend you want to actually run)

Build a Docker image that runs both your frontend and backend (or just your backend, if your built frontend is what upload-assets was uploading), and switch the workflow to upload-container:

- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
  with:
    context: .
    tags: my-app:${{ env.METICULOUS_COMMIT_SHA }}
    platforms: linux/amd64
    push: false
    load: true

- name: Run Meticulous tests
  uses: alwaysmeticulous/report-diffs-action/upload-container@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    image-tag: my-app:${{ env.METICULOUS_COMMIT_SHA }}
    container-port: 5173

Issue: Build Fails

Symptom: npm run build fails in CI

Common causes:

  1. TypeScript errors
  2. Missing environment variables
  3. Linting errors treated as build errors

Debug:

- name: Build app
  run: npm run build
  env:
    CI: false # Treats warnings as non-blocking
    NODE_ENV: production

Issue: False Positive Diffs

Symptom: Tests show diffs for content that hasn't changed

Common causes:

  1. Animations not completing
  2. Random IDs or keys
  3. Timestamps

Fixes:

Animations: Disable in tests

const duration = window.Meticulous?.isRunningAsTest ? 0 : 300

Random IDs: Use deterministic values

const generateId = () => {
  if (window.Meticulous?.isRunningAsTest) {
    return 'test-id-12345'
  }
  return crypto.randomUUID()
}

Timestamps: Add meticulous-ignore class

<span className="meticulous-ignore">
  {new Date().toLocaleString()}
</span>

Learn more: Fix False Positive Diffs


Testing Best Practices

1. Test Locally First

Run tests locally before CI:

# Build your app
npm run build

# Serve built files
npx serve dist

# In another terminal, run Meticulous
npx @alwaysmeticulous/cli simulate \
  --sessionId="YOUR_SESSION_ID" \
  --appUrl="http://localhost:3000"

2. Handle Loading States

Ensure loading states complete:

useEffect(() => {
  const fetchData = async () => {
    setLoading(true)
    const data = await getData()
    setData(data)
    setLoading(false)
  }

  fetchData()
}, [])

if (loading) {
  return <div>Loading...</div>
}

3. Use Skeleton Screens

Instead of spinners:

if (loading) {
  return <SkeletonCard /> // Consistent placeholder
}

Advanced Configuration

Monorepo Setup

If your Vite app is in a subdirectory:

- name: Install dependencies
  working-directory: ./apps/frontend
  run: npm ci

- name: Build app
  working-directory: ./apps/frontend
  run: npm run build

- name: Upload and test
  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-directory: "./apps/frontend/dist"
    rewrites: |
      [
        { "source": "/(.*)", "destination": "/index.html" }
      ]

Custom Vite Config

If you have a custom Vite config:

File: vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import CssSourcemapPlugin from '@alwaysmeticulous/recorder-plugin/css-sourcemap'

export default defineConfig({
  plugins: [react(), CssSourcemapPlugin()],
  build: {
    outDir: 'dist',
    sourcemap: true,
  },
  server: {
    port: 5173,
  }
})

Important: build.sourcemap covers JavaScript only — it does nothing for CSS. Vite emits no CSS source maps for production builds at all, so without extra help Meticulous cannot attribute stylesheet coverage back to the files in your repo. @alwaysmeticulous/recorder-plugin/css-sourcemap emits a .css.map for each CSS asset to close that gap.

The plugin disables Vite's CSS minification, which is what makes the maps accurate, so enable it on the build whose coverage Meticulous collects rather than on every production build. See Viewing source coverage information for the accuracy details and the full set of options.


See Also