eslint-plugin-harlanzw

My opinionated ESLint rules for Vue apps

29
0
29
1
TypeScript
public

eslint-plugin-harlanzw

npm version
npm downloads
License

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 🐦

Playground

Try the rules in action with a Nuxt ESLint interactive playground:

Open in StackBlitz

Rules

Note: 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:

  • Link Rules: Ensure link URLs are clean, accessible, and SEO-friendly
  • Nuxt Rules: Best practices for Nuxt applications
  • Vue Rules: Vue composition API and reactivity best practices
  • AI Deslop Rules: Clean AI-generated slop from content markdown
Rule Description
Link
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 URL paths
Nuxt
nuxt-await-navigate-to enforce awaiting navigateTo() calls
nuxt-no-random disallow random values during SSR rendering
nuxt-no-redundant-component-imports disallow #components imports used only as Vue template tags
nuxt-no-redundant-import-meta disallow redundant import.meta.server or import.meta.client checks in scoped components
nuxt-no-self-layer-alias disallow #layers/<name> alias when importing from the same layer; prefer a relative path
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-no-unsafe-date disallow unstable current dates during SSR rendering
nuxt-prefer-layer-alias prefer #layers/<name> alias over ~~/layers/<name> paths
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
Vue
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
General
no-silent-catch disallow silently swallowing errors in .catch() or try/catch
no-test-file-reads warn when tests read files instead of exercising exported behaviour
prefer-node-style-text prefer Node.js styleText() over raw ANSI escape codes
prefer-satisfies prefer satisfies over a widening type annotation on object literals
AI Deslop
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-false-sincerity remove false-sincerity openers that pad sentences (“honestly”, “frankly”, “in all honesty”)
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-em-dash replace em dashes in content prose
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-code-lang require language hints on fenced code examples
ai-deslop-vue-ts-lang require lang="ts" on Vue <script> blocks in code examples
pnpm
pnpm-require-trust-policy require trustPolicyIgnoreAfter: 262800 in pnpm-workspace.yaml

The plugin also includes 20 prompt linting rules for .prompt.md and .skill.md files. See the prompt configs section below.

Installation

Install the plugin:

pnpm add -D eslint-plugin-harlanzw

Usage

// eslint.config.js
import { harlanzw } from 'eslint-plugin-harlanzw'

export default harlanzw({
  link: true,
  nuxt: true,
  vue: true,
})

The Nuxt and Vue presets cover .vue, .js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, and .cts files.

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,
})

Test Rules

Nuxt and Vue presets enable test rules automatically.
no-test-file-reads warns when tests call Node.js readFile() or readFileSync().

Enable test rules alone with tests: true. Disable them with tests: false.

Shared Base

base carries the override blocks that were copy-pasted into every repo: the shared ignore set, node globals, and relaxations for test files, markdown code fences, and example manifests. It is opt in.

import antfu from '@antfu/eslint-config'
import { harlanzw } from 'eslint-plugin-harlanzw'

export default antfu(
  { type: 'lib' },
  ...harlanzw({
    base: true, // or { type: 'app', ignores: ['docs/**'] }
    nuxt: true,
    vue: true,
  }),
)

type defaults to 'lib', which also turns off ts/explicit-function-return-type. ignores appends to the shared set. Every shared ignore glob is recursive, so nested playgrounds and fixtures in a monorepo are covered.

Pass ignores: false to drop the shared ignore block and declare your own. A global ignore cannot be undone by a later config, so this is the only way to keep linting something the shared set covers, such as a playground you lint on purpose. The rule blocks still apply.

agentFiles decides what happens to the prompts you wrote: CLAUDE.md, AGENTS.md, and .cursor. It defaults to 'ignore', keeping them out of the lint run. A global ignore beats any files-scoped config, so harlanzw() switches it to 'lint' whenever its prompt config is enabled, otherwise the prompt rules would never see those files. Set it explicitly to override that.

The .claude directory is always ignored, whatever agentFiles says. Nothing in it is a prompt this repo wrote: its skills are upstream package docs installed by skilld, its context is generated notes and job state, and agents drop whole repo checkouts in there. It also sits in its own block, so ignores: false does not hand it back.

Every rule in these blocks is set to off, and flat config ignores an off entry for a rule whose plugin is absent. So the blocks are safe with any preset, and need no dependency on @antfu/eslint-config.

Spread them without the rule presets when you only want the shared overrides:

import { base } from 'eslint-plugin-harlanzw'

export default antfu({ type: 'lib' }, ...base())

The base blocks come first in the returned array, so put harlanzw() after the preset whose rules it relaxes.

Contents:

Block Applies to Turns off
harlanzw/base/agent-ignores global .claude always, plus CLAUDE.md, AGENTS.md, .cursor when agentFiles is 'ignore'
harlanzw/base/ignores global .data, fixtures, playground, worker-configuration.d.ts, plus your ignores. Dropped by ignores: false
harlanzw/base/rules all files no-use-before-define, node/prefer-global/process, node/prefer-global/buffer (+ ts/explicit-function-return-type for libs)
harlanzw/base/tests test files no-console, ts/no-unsafe-function-type, antfu/no-top-level-await, e18e/prefer-static-regex
harlanzw/base/markdown **/*.md/** no-console, tabs, style/max-statements-per-line, e18e/prefer-static-regex, unused imports
harlanzw/base/examples examples/**/package.json pnpm catalog rules

Extra Configs

Pass additional flat configs as extra arguments:

export default harlanzw(
  { link: true, nuxt: true, vue: true },
  {
    rules: {
      'harlanzw/link-lowercase': ['error', { ignoreExternal: true }],
    },
  },
)

With @antfu/eslint-config

import antfu from '@antfu/eslint-config'
import { harlanzw } from 'eslint-plugin-harlanzw'

export default antfu(
  { vue: true },
  ...harlanzw({ link: true, nuxt: true, vue: true }),
)

With Nuxt ESLint

import { harlanzw } from 'eslint-plugin-harlanzw'
import withNuxt from './.nuxt/eslint.config.mjs'

export default withNuxt(
  ...harlanzw({ link: true, nuxt: true, vue: true }),
)

Public API

The factory exposes the raw plugin and framework detection. The package also exports typed rule maps for custom configs.

import type { RuleOptions, Rules } from 'eslint-plugin-harlanzw'
import { harlanzw, plugin } from 'eslint-plugin-harlanzw'

const detected = harlanzw.detectFramework()
const rawPlugin = harlanzw.plugin
const linkOptions: RuleOptions['link-lowercase'] = [{ ignoreExternal: true }]
const linkRule: Rules['link-lowercase'] = ['warn', ...linkOptions]

export default [
  ...plugin.configs.recommended,
  { rules: { 'harlanzw/link-lowercase': linkRule } },
]

You can also select individual presets:

import { plugin } from 'eslint-plugin-harlanzw'

export default [
  ...plugin.configs.link,
  ...plugin.configs.nuxt,
  ...plugin.configs.vue,
  ...plugin.configs.tests,
]

AI Deslop Rules

15 rules for cleaning AI-generated slop from your content markdown files (content/**/*.md). Most rules are auto-fixable. Prose checks skip fenced code blocks opened with backticks or tildes, including fences longer than three characters.

// 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-false-sincerity Strips false-sincerity openers (“honestly”, “frankly”, “in all honesty”, “let’s be real”)
ai-deslop-hedging Strips hedging words that weaken copy (“very”, “really”, “quite”, “just”, “somewhat”)
ai-deslop-no-em-dash Replaces em dashes in content prose
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-code-lang Adds language hints to fenced code blocks
ai-deslop-vue-ts-lang Adds lang="ts" to Vue <script> blocks in code examples

Prompt Rules

20 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'],
]

pnpm Rules

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)

Sponsors

Sponsors

Credits

This plugin is based on eslint-plugin-antfu by Anthony Fu.

License

Licensed under the MIT license.

v0.3.3[beta]