eslint-plugin-harlanzw

Harlan's opinionated ESLint rules
60,587 29
Install
npm install eslint-plugin-harlanzw
Latest Version:0.23.0
Vue:3
License:MIT
Last Updated:Oct 14, 2026
Links: GitHub  ·  npm
Maintainer: harlan_zw

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
  • Prompt Rules: Lint agent prompt files for dangling paths, dilution, and structure
  • Docs Rules: Enforce the root-docs contract across a repository's documentation
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
Prompt
prompt-dangling-path flag a backticked repository path in an instruction file that does not exist
Docs
docs-work-brief-contract require every open brief in docs/work to carry the fields that make it auditable
docs-reference-no-status forbid a Status line in a reference document, where location already carries status
docs-root-allowlist forbid new Markdown at the repository root outside the root-docs allowlist
docs-retired-pointer flag a pointer to a document that has been retired, naming its replacement
pnpm
pnpm-require-trust-policy require trustPolicyIgnoreAfter: 262800 in pnpm-workspace.yaml
Nuxt UI design, opt in
nuxt-ui-no-restyle use component props and site-approved classes for Nuxt UI appearance
vue-no-dynamic-tailwind-classes use complete Tailwind class names in Vue bindings

The plugin also includes 21 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.

Link Options

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 read source files or files with unknown paths. Known non-source paths, such as generated .json, .md, and .txt files, are allowed.

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

Nuxt UI Design Rules

Enable our design rules through your site's existing Nuxt ESLint config:

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

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

nuxtUi enables automatically when the current package declares @nuxt/ui in dependencies or devDependencies. Use nuxtUi: false to disable it. Use nuxtUi: true when another workspace package owns the dependency. It adds two Vue template checks:

  • Warn when Nuxt UI controls or configured wrappers override component appearance.
  • Report partial Tailwind class construction in :class and :ui bindings.

Messages guide agents toward component props and shared styling. The rules never change appearance automatically. No runtime Nuxt module or shadcn dependency is needed.

Pass an object to configure component names, allowed classes, slots, sizes, and repair messages. See site configuration for a complete example and coverage limits. Size and variant guidance comes from these explicit options. App configuration is not executed or automatically discovered.

Theme-aware checks

Load the site's CSS to validate utility names, colors, and spacing:

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

export default withNuxt(
  ...harlanzw(),
  await tailwind({ stylesheet: './app/assets/css/main.css' }),
)

Install @tailwindcss/node@~4.3.3 for this optional helper. It uses your CSS imports, theme tokens, and component styles.

  • Valid classes: compiler-backed checks, custom hooks, and layer styles.
  • Theme tokens: known token equivalents and optional color and spacing policies.
  • Component props: size, color, variant, and configured wrapper values.

OG-image templates are excluded from theme checks by default because their renderer has separate styling rules.

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

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

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)

Docs Rules

Opt-in rules for the root-docs contract: an allowlist for Markdown at the repository root and a lifecycle for docs/. Enable them per repository, since the contract is a convention rather than a fact about Markdown.

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

export default [
  ...plugin.configs.docs,
]

This config is opinionated and opt-in. It encodes one convention about where Markdown lives and what a brief must say. Nothing in it is enabled by recommended, content, or any prompt:* config, and a test pins that. Every default below is an option, because a repository adopting the config will not share every opinion in it.

docs-root-allowlist and prompt-dangling-path apply to root *.md files only. docs-work-brief-contract and docs-reference-no-status apply to docs/**/*.md. docs-retired-pointer applies to both root *.md and docs/**/*.md:

Rule What it does Key options
docs-work-brief-contract Requires every open brief in docs/work/ to carry title, Status:, **Next move:**, Done means:, ## Ledger, and ## Log fields dir, buckets, require
docs-reference-no-status Forbids a Status: line in reference documents, where the folder already carries status dirs
docs-root-allowlist Forbids new Markdown at the repository root outside the root-docs allowlist allow, additionalAllow, root
docs-retired-pointer Flags a pointer to a retired document and names its replacement retired
prompt-dangling-path Flags a backticked repository path that does not exist root, ignore

The defaults are Harlan's. **Next move:** accepts Harlan, Blocked or Ready, and a bucket named after a person is the clearest sign to set your own:

export default [
  {
    rules: {
      'harlanzw/docs-work-brief-contract': ['error', {
        dir: 'docs/initiatives',
        buckets: ['Waiting', 'Go'],
        require: ['title', 'doneMeans'],
      }],
    },
  },
]

Sponsors

Sponsors

Credits

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

License

Licensed under the MIT license.

Automatic detection skips known Nuxt UI versions below v4. Explicit nuxtUi configuration overrides detection.

Related Packages

eslint-plugin-vue-data-testid

An ESLint plugin to enforce consistent usage of 'data-testid' attributes in Vue....

147,849 3
eslint-plugin-vue-storefront

ESLint rules for Vue Storefront

39,292
eslint-plugin-vue-composable

ESLint plugin providing Vue composable related rules

551,160
@scayle/eslint-plugin-vue-composable

Rules to avoid unpleasant composable problems

207,394 4
eslint-plugin-vue-kuzzle

Kuzzle Vue Coding Standard

83,068 0

Version History

Version Released Vue License
0.23.0 ^3.5.42 MIT
0.22.0 ^3.5.42 MIT
0.21.1 ^3.5.42 MIT
0.21.0 ^3.5.42 MIT
0.20.0 ^3.5.41 MIT