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

Vue 3 with Vite - Complete Setup Guide

Complete guide for setting up Meticulous with Vue 3 applications built with Vite.


Overview

Vue 3 with Vite provides a fast development experience. 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" href="/favicon.ico" />
    <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="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Important: The recorder must load before your application code.


Step 2: Configure GitHub Actions Workflow

Vue/Vite builds to static files, so we use the upload-assets action.

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 uploads your built files and runs tests against them.

Build output: Vite builds to dist/ directory by default.

Rewrites Configuration

Handles client-side routing (Vue Router):

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

Common Patterns

Pattern 1: Detect Test Mode

Use window.Meticulous.isRunningAsTest in your components:

Options API:

<template>
  <div>
    <p v-if="isTest">Running as test</p>
    <p v-else>Running normally</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isTest: window.Meticulous?.isRunningAsTest || false
    }
  }
}
</script>

Composition API:

<template>
  <div>
    <p v-if="isTest">Running as test</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const isTest = ref(window.Meticulous?.isRunningAsTest || false)
</script>

Pattern 2: Bypass Authentication

File: src/main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)

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

app.use(createPinia())
app.use(router)
app.mount('#app')

Pattern 3: Router Navigation Guards

Handle authentication in router:

File: src/router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
import type { RouteLocationNormalized } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('../views/HomeView.vue')
    },
    {
      path: '/dashboard',
      name: 'dashboard',
      component: () => import('../views/DashboardView.vue'),
      meta: { requiresAuth: true }
    }
  ]
})

router.beforeEach((to: RouteLocationNormalized) => {
  // Skip auth check during tests
  if (window.Meticulous?.isRunningAsTest) {
    return true
  }

  // Normal auth check
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }

  return true
})

export default router

Pattern 4: Composable for Test Detection

Create a reusable composable:

File: src/composables/useMeticulous.ts

import { ref, readonly } from 'vue'

export function useMeticulous() {
  const isRunningAsTest = ref(
    window.Meticulous?.isRunningAsTest || false
  )

  const recordCustomValues = (values: Record<string, any>) => {
    window.Meticulous?.recordCustomValues?.(values)
  }

  const getCustomValues = () => {
    return window.Meticulous?.getCustomValues?.()
  }

  return {
    isRunningAsTest: readonly(isRunningAsTest),
    recordCustomValues,
    getCustomValues
  }
}

Usage:

<script setup>
import { useMeticulous } from '@/composables/useMeticulous'

const { isRunningAsTest } = useMeticulous()
</script>

Complete Example

File Structure

your-app/
├── src/
│   ├── main.ts               # Entry point
│   ├── App.vue               # Root component
│   ├── router/
│   │   └── index.ts          # Router configuration
│   ├── stores/               # Pinia stores
│   ├── views/                # Page components
│   ├── components/           # Reusable components
│   └── composables/
│       └── useMeticulous.ts  # Test detection composable
├── index.html                # Recorder installation
├── vite.config.ts            # Vite configuration
├── .github/
│   └── workflows/
│       └── meticulous.yml    # CI/CD
└── package.json

Example: Protected View

File: src/views/DashboardView.vue

<template>
  <div class="dashboard">
    <h1>Welcome, {{ user?.name }}!</h1>
    <p>Email: {{ user?.email }}</p>

    <div v-if="loading">
      <p>Loading dashboard...</p>
    </div>
    <div v-else-if="error">
      <p class="error">{{ error }}</p>
    </div>
    <div v-else>
      <!-- Dashboard content -->
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'

interface User {
  id: string
  name: string
  email: string
}

const router = useRouter()
const user = ref<User | null>(null)
const loading = ref(true)
const error = ref('')

onMounted(async () => {
  // Mock data for tests
  if (window.Meticulous?.isRunningAsTest) {
    user.value = {
      id: 'test-user-123',
      name: 'Test User',
      email: 'test@example.com'
    }
    loading.value = false
    return
  }

  // Normal data fetching
  try {
    const response = await fetch('/api/user')
    if (!response.ok) throw new Error('Failed to fetch user')
    user.value = await response.json()
  } catch (err) {
    error.value = err instanceof Error ? err.message : 'Unknown error'
    router.push('/login')
  } finally {
    loading.value = false
  }
})
</script>

CI/CD Configuration Details

Environment Variables

Vite exposes environment variables prefixed with VITE_:

- 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

TypeScript Configuration

Ensure TypeScript recognizes Vite env variables:

File: src/env.d.ts

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string
  readonly VITE_APP_NAME: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

Troubleshooting

Issue: Recorder Not Loading

Symptom: window.Meticulous is undefined

Checks:

  1. Verify recorder script in index.html <head>
  2. Check project ID is correct
  3. Check for CSP blocking

Fix: Ensure correct placement:

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

  <!-- Then other elements -->
  <title>Your App</title>
</head>

Issue: Routes Return 404

Symptom: Direct navigation to routes returns 404

Cause: Missing rewrite configuration

Fix:

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

Issue: TypeScript Errors

Symptom: Property 'Meticulous' does not exist on type 'Window'.

Fix: Add type declarations

File: src/types/meticulous.d.ts

interface Meticulous {
  isRunningAsTest?: boolean
  recordCustomValues?: (values: Record<string, any>) => void
  getCustomValues?: () => Record<string, any> | undefined
  pause?: () => void
  resume?: () => void
}

declare global {
  interface Window {
    Meticulous?: Meticulous
  }
}

export {}

Issue: False Positive Diffs

Common causes:

  1. Animations not completing
  2. Random data
  3. Timestamps

Fixes:

Disable animations in tests:

<template>
  <Transition :duration="transitionDuration">
    <div>Content</div>
  </Transition>
</template>

<script setup>
import { computed } from 'vue'

const transitionDuration = computed(() =>
  window.Meticulous?.isRunningAsTest ? 0 : 300
)
</script>

Deterministic IDs:

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

Ignore timestamps:

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

Learn more: Fix False Positive Diffs


Testing Best Practices

1. Handle Loading States

Always show loading states:

<template>
  <div v-if="loading">
    <SkeletonLoader />
  </div>
  <div v-else-if="error">
    <ErrorMessage :error="error" />
  </div>
  <div v-else>
    <!-- Content -->
  </div>
</template>

2. Use Suspense for Async Components

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

3. Test Locally First

# Build your app
npm run build

# Serve built files
npx serve dist

# Verify at http://localhost:3000

Advanced Configuration

Monorepo Setup

- 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

File: vite.config.ts

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import CssSourcemapPlugin from '@alwaysmeticulous/recorder-plugin/css-sourcemap'

export default defineConfig({
  plugins: [vue(), CssSourcemapPlugin()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  build: {
    outDir: 'dist',
    sourcemap: true
  }
})

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