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:
- Vue 3 application using Vite
- Basic familiarity with Meticulous concepts
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
- Get your API token from Meticulous dashboard → Project Settings
- Go to GitHub repo → Settings → Secrets and variables → Actions
- Create secret named
METICULOUS_API_TOKENwith 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:
- Verify recorder script in
index.html<head> - Check project ID is correct
- 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:
- Animations not completing
- Random data
- 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
- Onboarding Guide - General Meticulous setup
- Troubleshoot Authentication - Auth patterns and solutions
- Fix False Positives - Handle non-deterministic content