Harlan’s ESLint rules for Vue projects with focus on link hygiene, Nuxt best practices, and Vue reactivity patterns.
|
Made possible by my Sponsor Program 💖 Follow me @harlan_zw 🐦 |
Try the rules in action with a Nuxt ESLint interactive playground:
undefinedNote: These rules are experimental and may change. They will be submitted to the official Vue ESLint plugin for consideration.
The rules are organized into the following categories:
| Rule | Description |
|---|---|
| undefinedLinkundefined | |
link-ascii-only |
ensure link URLs contain only ASCII characters |
link-lowercase |
ensure link URLs do not contain uppercase characters |
link-no-double-slashes |
ensure link URLs do not contain consecutive slashes |
link-no-underscores |
ensure link URLs do not contain underscores |
link-no-whitespace |
ensure link URLs do not contain whitespace characters |
link-require-descriptive-text |
require descriptive link text |
link-require-href |
require href/to attribute on link elements |
link-trailing-slash |
enforce trailing slash consistency on link URLs |
| undefinedNuxtundefined | |
nuxt-await-navigate-to |
enforce awaiting navigateTo() calls |
nuxt-no-redundant-import-meta |
disallow redundant import.meta.server or import.meta.client checks in scoped components |
nuxt-no-side-effects-in-async-data-handler |
disallow side effects in async data handlers |
nuxt-no-side-effects-in-setup |
disallow side effects in setup functions |
nuxt-prefer-navigate-to-over-router-push-replace |
prefer navigateTo() over router.push() or router.replace() |
nuxt-prefer-nuxt-link-over-router-link |
prefer NuxtLink over RouterLink |
nuxt-ui-prefer-shorthand-css |
prefer Nuxt UI shorthand CSS classes over verbose var() syntax |
| undefinedVueundefined | |
vue-no-faux-composables |
stop fake composables that don’t use Vue reactivity |
vue-no-nested-reactivity |
don’t mix ref() and reactive() together |
vue-no-passing-refs-as-props |
don’t pass refs as props - unwrap them first |
vue-no-reactive-destructuring |
avoid destructuring reactive objects |
vue-no-ref-access-in-templates |
don’t use .value in Vue templates |
vue-no-torefs-on-props |
don’t use toRefs() on the props object |
vue-no-reactivity-after-await |
disallow subscription APIs (watch, computed, etc.) after await in async functions |
vue-no-async-lifecycle-hook |
disallow async callbacks in Vue lifecycle hooks |
vue-no-resolve-component-in-composables |
disallow resolveComponent()/resolveDirective() outside top-level <script setup> |
vue-no-unresolvable-define-emits |
disallow unresolvable types in defineEmits type parameters |
vue-prefer-define-emits-object-syntax |
prefer Vue 3.3+ object syntax for defineEmits over call signatures |
vue-require-composable-prefix |
enforce use* prefix for functions using Vue reactivity |
| undefinedGeneralundefined | |
no-silent-catch |
disallow silently swallowing errors in .catch() or try/catch |
| undefinedAI Deslopundefined | |
ai-deslop-adverbs |
remove unnecessary adverbs that add no meaning (e.g. “significantly”, “fundamentally”) |
ai-deslop-autolink |
auto-link first mention of known tech terms to their canonical URLs |
ai-deslop-buzzwords |
replace AI-generated buzzword phrases with simpler alternatives (e.g. “leverage” → “use”) |
ai-deslop-casing |
enforce correct casing for tech terms, brands, and abbreviations (e.g. “github” → “GitHub”) |
ai-deslop-false-dichotomy |
flag “it’s not X, it’s Y” contrast patterns common in AI writing |
ai-deslop-filler |
remove AI-generated filler sentences and phrases (e.g. “it’s worth noting that”) |
ai-deslop-hedging |
remove hedging/qualifying words that weaken copy (e.g. “very”, “really”, “quite”, “just”) |
ai-deslop-no-exclamation |
remove exclamation marks from content prose |
ai-deslop-passive-voice |
flag passive voice constructions (e.g. “is generated” → rewrite in active voice) |
ai-deslop-weak-opener |
flag weak sentence openers like “There is” and “It is possible to” |
ai-deslop-frontmatter-spacing |
remove empty lines inside YAML frontmatter |
ai-deslop-vue-ts-lang |
require lang="ts" on Vue <script> blocks in code examples |
| undefinedpnpmundefined | |
pnpm-require-trust-policy |
require trustPolicyIgnoreAfter: 262800 in pnpm-workspace.yaml |
The plugin also includes 21 prompt linting rules for .prompt.md and .skill.md files. See the prompt configs section below.
Install the plugin:
pnpm add -D eslint-plugin-harlanzw
// eslint.config.js
import harlanzw from 'eslint-plugin-harlanzw'
export default harlanzw({
link: true,
nuxt: true,
vue: true,
})
All link rules share ignoreExternal and exclude options. Configure them once:
export default harlanzw({
link: {
ignoreExternal: true, // skip http(s):// URLs and elements with `external` attr
exclude: ['^/api/', '/OAuth/'], // skip URLs matching any regex pattern
requireTrailingSlash: true, // passed to link-trailing-slash
},
nuxt: true,
vue: true,
})
Pass additional flat configs as extra arguments:
export default harlanzw(
{ link: true, nuxt: true, vue: true },
{
rules: {
'harlanzw/link-lowercase': ['error', { ignoreExternal: true }],
},
},
)
import antfu from '@antfu/eslint-config'
import harlanzw from 'eslint-plugin-harlanzw'
export default antfu(
{ vue: true },
...harlanzw({ link: true, nuxt: true, vue: true }),
)
import harlanzw from 'eslint-plugin-harlanzw'
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
...harlanzw({ link: true, nuxt: true, vue: true }),
)
For custom configs or if you need the raw ESLint plugin object:
import { plugin } from 'eslint-plugin-harlanzw'
export default [
...plugin.configs.recommended, // link + nuxt + vue
// or pick:
...plugin.configs.link,
...plugin.configs.nuxt,
...plugin.configs.vue,
]
12 rules for cleaning AI-generated slop from your content markdown files (content/**/*.md). Most rules are auto-fixable.
// eslint.config.js
export default harlanzw({
content: true,
})
Or use the raw config:
import { plugin } from 'eslint-plugin-harlanzw'
export default [
...plugin.configs.content,
]
| Rule | What it does |
|---|---|
ai-deslop-buzzwords |
Replaces overused AI phrases with plain alternatives (“leverage” → “use”, “delve into” → “explore”) |
ai-deslop-filler |
Removes filler phrases that add nothing (“it’s worth noting that”, “at the end of the day”) |
ai-deslop-adverbs |
Strips unnecessary adverbs (“significantly”, “fundamentally”, “essentially”) |
ai-deslop-casing |
Fixes tech term casing using a 300+ term dictionary (“github” → “GitHub”, “typescript” → “TypeScript”) |
ai-deslop-autolink |
Links first mention of tech terms to their canonical URLs (“Nuxt” → [Nuxt](https://nuxt.com)) |
ai-deslop-false-dichotomy |
Flags “it’s not X, it’s Y” false contrast patterns |
ai-deslop-hedging |
Strips hedging words that weaken copy (“very”, “really”, “quite”, “just”, “somewhat”) |
ai-deslop-no-exclamation |
Replaces exclamation marks with periods in content prose |
ai-deslop-passive-voice |
Flags passive voice (“is generated”, “was created”) for active rewriting |
ai-deslop-weak-opener |
Flags weak expletive openers (“There is”, “It is possible to”) |
ai-deslop-frontmatter-spacing |
Removes empty lines inside YAML frontmatter blocks |
ai-deslop-vue-ts-lang |
Adds lang="ts" to Vue <script> blocks in code examples |
21 rules for linting .prompt.md and .skill.md files using a custom prompt language:
import { plugin } from 'eslint-plugin-harlanzw'
export default [
...plugin.configs['prompt:recommended'],
// or stricter:
// ...plugin.configs['prompt:strict'],
// for skill files:
// ...plugin.configs['prompt:skill'],
]
Enforces required fields in pnpm-workspace.yaml. Auto-enabled when the file exists.
export default harlanzw({
pnpm: true,
})
Or use the raw config:
import { plugin } from 'eslint-plugin-harlanzw'
export default [
...plugin.configs.pnpm,
]
| Rule | What it does |
|---|---|
pnpm-require-trust-policy |
Ensures trustPolicyIgnoreAfter: 262800 is present in pnpm-workspace.yaml (auto-fixable) |
This plugin is based on eslint-plugin-antfu by Anthony Fu.
Licensed under the MIT license.
We use cookies
We use cookies to analyze traffic and improve your experience. You can accept or reject analytics cookies.