Vue Doctor
Diagnose and fix performance, security, and correctness issues in your Vue/Nuxt app.
npx vue-doctor@latest .
Vue Doctor scans your codebase and produces a 0–100 health score with actionable diagnostics across 7 categories and 47+ rules.
What it checks
Composition API
| Rule |
Description |
no-watch-as-computed |
watch() that only sets derived state → use computed() |
no-async-watcheffect |
async watchEffect() without cleanup → race conditions |
prefer-ref-over-reactive-primitive |
reactive({ count: 0 }) → use ref(0) |
no-mutation-in-computed |
Side effects in computed() getter |
no-watch-immediate-fetch |
watch(..., fetch, { immediate: true }) → use useFetch() |
no-reactive-destructure |
Destructuring reactive() loses reactivity |
no-ref-in-computed |
Creating ref() inside computed() (memory leak) |
no-async-computed |
computed(async () => ...) creates Promise-based reactive state |
no-conditional-composable-call |
Calling useXxx() conditionally can break stable setup behavior |
Performance
| Rule |
Description |
no-index-as-key |
v-for with index as :key breaks reordering |
require-key-for-v-for |
v-for without :key |
no-expensive-inline-expression |
{{ items.filter().map() }} → use computed |
no-deep-watch |
watch(..., { deep: true }) traverses entire object tree |
no-template-method-call |
Method calls inside v-for re-run on every render |
no-giant-component |
Components > 300 lines |
Security
| Rule |
Description |
no-v-html |
v-html enables XSS attacks |
no-eval |
eval() / new Function() execute arbitrary code |
no-secrets-in-client |
API keys / tokens hardcoded in client code |
Architecture
| Rule |
Description |
no-prop-mutation |
Direct prop mutation breaks one-way data flow |
require-emits-declaration |
emit() without defineEmits() |
require-component-key |
Component in v-for without :key |
Correctness
| Rule |
Description |
no-direct-dom-manipulation |
document.querySelector() → use template refs |
no-this-in-setup |
this is undefined in <script setup> |
no-v-if-with-v-for |
v-if + v-for on same element (priority issue) |
require-defineprops-types |
defineProps() without TypeScript types |
Nuxt
| Rule |
Description |
use-usefetch-over-fetch |
Raw fetch() in setup → use useFetch() |
require-server-route-error-handling |
Server routes without try/catch |
no-window-in-ssr |
window/document access during SSR |
require-seo-meta |
Pages without useSeoMeta() or useHead() |
no-process-env-in-client |
process.env in client code → use useRuntimeConfig() |
require-define-page-meta |
Pages should define route/page metadata via definePageMeta() |
no-server-only-import-in-client |
Prevent server-only imports in client-rendered files |
no-client-composable-in-server-route |
Prevent client-only composables in Nuxt server routes |
Bundle Size
| Rule |
Description |
no-barrel-import |
Barrel imports prevent tree-shaking |
no-full-lodash-import |
Full lodash adds ~70kb |
no-moment-import |
moment.js adds ~230kb |
prefer-async-component |
Heavy components that could be async-loaded |
no-heavy-library |
Importing jQuery, Underscore, Ramda |
Accessibility
| Rule |
Description |
no-autofocus |
autofocus can disrupt keyboard and screen-reader users |
no-positive-tabindex |
Positive tabindex breaks natural keyboard navigation |
require-button-type |
<button> without type can trigger unintended form submits |
require-img-alt |
<img> must include an alt attribute |
require-accessible-form-control-name |
Form controls need an accessible name (aria-label/aria-labelledby/label) |
no-click-without-keyboard-handler |
Clickable non-interactive elements must support keyboard interaction |
require-media-captions |
<video> should include captions/subtitles tracks |
no-aria-hidden-on-focusable |
Prevent aria-hidden=\"true\" on focusable controls |
CLI Usage
# Scan current directory
npx vue-doctor@latest .
# Scan specific directory
npx vue-doctor@latest ./my-app
# Show verbose file details per rule
npx vue-doctor@latest . --verbose
# Output only the health score (0-100)
npx vue-doctor@latest . --score
# Scan only files changed vs main branch
npx vue-doctor@latest . --diff
# Scan only files changed vs a specific branch
npx vue-doctor@latest . --diff main
# Skip prompts (CI mode)
npx vue-doctor@latest . -y
# Disable dead code detection
npx vue-doctor@latest . --no-dead-code
# Select a specific workspace project
npx vue-doctor@latest . --project my-app
Configuration
Create vue-doctor.config.json in your project root, or add a vueDoctor key to package.json:
{
"ignore": {
"rules": ["vue-doctor/no-v-html"],
"files": ["src/generated/**", "src/legacy/**"]
},
"lint": true,
"deadCode": true,
"verbose": false,
"diff": false
}
Options
| Option |
Type |
Default |
Description |
ignore.rules |
string[] |
[] |
Rule IDs to ignore |
ignore.files |
string[] |
[] |
Glob patterns for files to ignore |
lint |
boolean |
true |
Run lint checks |
deadCode |
boolean |
true |
Run dead code detection |
verbose |
boolean |
false |
Show file paths per diagnostic |
diff |
boolean|string |
false |
Scan only changed files |
Programmatic API
import { diagnose } from 'vue-doctor/api'
const result = await diagnose('./my-vue-app', {
lint: true,
deadCode: true,
})
console.log(result.score) // { score: 84, label: 'Good' }
console.log(result.diagnostics) // Diagnostic[]
console.log(result.project) // { framework: 'nuxt', vueVersion: '^3.4.0', ... }
console.log(result.elapsedMilliseconds)
ESLint Plugin
Use the rules standalone in your ESLint config:
// eslint.config.js
import vueDoctorPlugin from 'vue-doctor/eslint-plugin'
import vueParser from 'vue-eslint-parser'
export default [
{
files: ['**/*.vue'],
languageOptions: { parser: vueParser },
plugins: { 'vue-doctor': vueDoctorPlugin },
rules: {
'vue-doctor/no-v-html': 'error',
'vue-doctor/no-prop-mutation': 'error',
'vue-doctor/no-index-as-key': 'warn',
'vue-doctor/no-barrel-import': 'warn',
},
},
]
Score
The health score (0–100) is calculated based on the number and severity of diagnostics found:
| Score |
Label |
Meaning |
| 75–100 |
Good |
Healthy codebase |
| 50–74 |
OK |
Some issues to address |
| 0–49 |
Critical |
Significant problems |
Monorepo Support
Vue Doctor automatically detects monorepos (pnpm workspaces, npm workspaces) and lets you scan individual packages:
# Scan all packages
npx vue-doctor@latest . -y
# Scan a specific package
npx vue-doctor@latest . --project my-nuxt-app
Inspired by
MIT License