@voxpelli/eslint-config
April 21, 2026 · View on GitHub
My personal ESLint config – a superset of the neostandard base config that I co-created and co-maintain.
This config contains a couple of more opinionated checks that I find helpful in my projects.
This is also the reference ESLint config for the types-in-JS workflow — JavaScript with JSDoc type annotations validated by tsc. The JSDoc rules are specifically tuned for this approach: type-checking rules are deactivated (handled by tsc), while documentation-oriented JSDoc rules remain active.
Install
To easily install correct peer dependencies, you can use install-peerdeps:
install-peerdeps --dev @voxpelli/eslint-config
Usage
Add an eslint.config.js (or eslint.config.mjs if your project is CJS) that exports this config:
export { default } from '@voxpelli/eslint-config';
If you need to configure something, instead do:
import { voxpelli } from '@voxpelli/eslint-config';
export default voxpelli({
cjs: true, // Ensures the config has rules fit for a CJS context rather than an ESM context
noMocha: true, // By standard this config expects tests to be of the Mocha kind, but one can opt out
noStyle: true, // Disables all stylistic rules (@stylistic + perfectionist sorting) — also passed to neostandard
browserFiles: ['client/**/*.js'], // Scopes browser globals and disables Node rules for matched files
cliFiles: ['bin/**/*.js', 'scripts/**/*.js'], // Relaxes rules for CLI scripts (process.exit, console, sync I/O, etc.)
});
Passing an unknown option key throws a TypeError with a message pointing to the composable pattern — this catches common mistakes like placing custom rules inside the options object.
Custom rules, plugins, and other ESLint config go in separate objects after the spread — not inside the voxpelli() options (which only accepts the keys shown above plus neostandard options):
import { voxpelli } from '@voxpelli/eslint-config';
export default [
...voxpelli({ /* config options */ }),
{
rules: {
'no-console': 'off',
'func-style': ['warn', 'declaration'],
},
},
];
Re-exported utilities
Bundled plugins and parsers are re-exported so consumers can reference them in custom overrides without installing them as separate dependencies:
plugins— from neostandard; gives access to its bundled plugin defaults (e.g., the TypeScript parser for Vue/Svelte/Astro setups).globals— from theglobalspackage.packageJsonPlugin— fromeslint-plugin-package-json.jsoncParser— fromjsonc-eslint-parser.
import { voxpelli, plugins, globals } from '@voxpelli/eslint-config';
export default [
...voxpelli({ ts: true }),
{
files: ['**/*.vue'],
languageOptions: {
parser: plugins['typescript-eslint'].parser,
},
},
];
Note: voxpelli() returns a flat config array. With ESLint 9.37+ you can also use defineConfig() from eslint/config which auto-flattens arrays — no spread needed:
import { defineConfig } from 'eslint/config';
import { voxpelli } from '@voxpelli/eslint-config';
export default defineConfig([
voxpelli({ /* options */ }),
{ /* custom rules */ },
]);
defineConfig() also supports an extends key inside config objects, which is useful when you need to scope the inherited config to specific files:
import { defineConfig } from 'eslint/config';
import { voxpelli } from '@voxpelli/eslint-config';
export default defineConfig([
{
files: ['src/**/*.js'],
extends: [voxpelli()],
},
]);
Composable sub-configs
browserFiles and cliFiles are also exported as standalone config factories for cases where you want to apply their rule sets without using voxpelli() as the base:
import { browserFiles, cliFiles } from '@voxpelli/eslint-config';
export default [
// your own base config …
...browserFiles(['client/**/*.js']),
...cliFiles(['bin/**/*.js']),
];
browserFiles(globs) — scopes browser globals and disables Node.js rules for matched files.
cliFiles(globs) — relaxes rules appropriate for CLI scripts: allows process.exit(), console, sync I/O, and top-level non-await patterns.
How does this differ from pure neostandard?
- :stop_sign: = changed to
errorlevel - :warning: = changed to
warnlevel - :mute: = deactivated
- :wrench: = changed config
Markers can combine when a rule has both a severity change and a separate config change — e.g. a bullet may lead with :warning: (severity) and carry an inline :wrench: (other config tweak).
:wrench: Changed neostandard rules
- :wrench:
@stylistic/comma-dangle– changed – set to enforce dangling commas in arrays, objects, imports and exports (disabled bynoStyle) - :warning:
@stylistic/object-curly-newline– :wrench: changed – softened towarn; enforces multiline imports and exports when 4+ specifiers are present (disabled bynoStyle) - :wrench:
no-unused-vars– changed – sets"args": "all", "argsIgnorePattern": "^_",because I personally don't feel limited by Express error handlers + wants to stay in sync with TypeScriptnoUnusedParameters
:heavy_plus_sign: Added ESLint core rules
- :warning:
func-style– disallows function declarations, good to be consistent with how functions are declared - :warning:
no-console– warns on existence ofconsole.logand similar, as they are mostly used for debugging and should not be committed - :stop_sign:
no-constant-binary-expression– errors when binary expressions are detected to constantly evaluate a specific way - :stop_sign:
no-nonoctal-decimal-escape– there's no reason not to ban it - :stop_sign:
no-unsafe-optional-chaining– enforces one to be careful with.?and not use it in ways that can inadvertently cause errors orNaNresults - :warning:
no-warning-comments– warns of the existence ofFIXMEcomments, as they should always be fixed before pushing - :stop_sign:
object-shorthand– requires the use of object shorthands for properties, more tidy
:package: Added ESLint rule packages
plugin:jsdoc/recommendedplugin:mocha/recommendedeslint-plugin-package-json(applied to**/package.json, with fixture paths ignored by default:**/test/**,**/tests/**,**/__tests__/**,**/test-*/**,**/fixtures/**,**/__fixtures__/**)eslint-plugin-perfectionist(4 sorting rules, replaceseslint-plugin-sort-destructure-keys)plugin:n/recommendedplugin:promise/recommendedplugin:security/recommendedplugin:unicorn/recommended
:wrench: Overrides of added ESLint rule packages
-
:wrench:
jsdoc/check-tag-names– changed – allows@plannedtag for knip unused-export suppression -
:mute:
jsdoc/check-types– deactivated – to improve use with types in js. -
:mute:
jsdoc/no-undefined-types– deactivated – to improve use with types in js. -
:mute:
jsdoc/require-jsdoc– deactivated – to improve use with types in js. -
:mute:
jsdoc/require-param-description– deactivated – to improve use with types in js. -
:mute:
jsdoc/require-property-description– deactivated – to improve use with types in js. -
:mute:
jsdoc/require-returns-description– deactivated – to improve use with types in js. -
:mute:
jsdoc/require-yields– deactivated – to improve use with types in js. -
:wrench:
jsdoc/tag-lines– changed – to enforce an empty line between description and tags, but disallow them elsewhere. -
:mute:
jsdoc/valid-types– deactivated – to improve use with types in js. -
:mute:
jsdoc/reject-any-type– deactivated – too strict for types in js workflow -
:mute:
jsdoc/reject-function-type– deactivated – too strict for types in js workflow -
:mute:
jsdoc/require-next-description– deactivated – to improve use with types in js. -
:mute:
jsdoc/require-throws-description– deactivated – to improve use with types in js. -
:mute:
jsdoc/require-yields-description– deactivated – to improve use with types in js. -
:mute:
mocha/no-mocha-arrows– deactivated – while Mocha discourages arrow functions I find it more readable to use them + I find it safe when combined with type checking as then the type checking will notify one when one tries to do athis.setTimeout()or similar in an arrow function where there is no such local context -
:mute:
n/no-extraneous-import– deactivated – superseded by knip, which validates imports more accurately without false positives in monorepos -
:mute:
n/no-process-exit– deactivated – added byplugin:n/recommended, but deactivated in favor ofunicorn/no-process-exit -
:wrench:
package-json/no-empty-fields– changed – addskeywordstoignoreProperties(alongside the defaultfiles); emptykeywordsarrays are common in WIP or internal packages -
:warning:
package-json/sort-collections– :wrench: changed – softened towarnin this major as a grace window; will promote toerrorin a future major -
:mute:
package-json/require-exports– deactivated – bin-only CLI packages legitimately ship withoutexports; too noisy across the canary fleet to surface useful signal -
:mute:
package-json/no-redundant-files– deactivated – conflicts withn/no-unpublished-bin: npm auto-includes files listed inbin/main/manregardless of thefilesfield, but eslint-plugin-n reports a false positive when those entries are removed fromfiles, so fixing one triggers the other. -
:mute:
package-json/require-sideEffects– deactivated – bundler-only signal; declaring it wrong is worse than declaring nothing. Browser/bundler-consumable packages can opt in locally. -
:warning:
package-json/specify-peers-locally– changed – softened towarn; optional peer-dep patterns are a legitimate exception -
:mute:
security/detect-object-injection– deactivated – causes too many false errors -
:mute:
security/detect-unsafe-regex– deactivated – at least early on wasn't very stable -
:wrench:
unicorn/catch-error-name– changed – I prefererrovererroras I finderrorto be a far too similar name to the built inErrorclass -
:mute:
unicorn/explicit-length-check– deactivated – I don't see an issue withif (string.length)instead ofif (string.length !== 0) -
:warning:
unicorn/unicorn/no-await-expression-member– changed – eg. useful in chai tests -
:warning:
unicorn/unicorn/no-negated-condition– deactivated – turned off, there are valid cases for this, so it simply gets noisy -
:mute:
unicorn/numeric-separators-style– deactivated – currently not enough good support for this in engines -
:warning:
unicorn/prefer-add-event-listener– changed – set towarninstead oferror -
:warning:
unicorn/prefer-event-target– changed – set towarninstead oferror -
:mute:
unicorn/prefer-module– deactivated – only useful when you know you're targetting ESM -
:warning:
unicorn/prefer-spread– changed – set towarninstead oferror -
:warning:
unicorn/prefer-string-replace-all– changed – set towarninstead oferror -
:mute:
unicorn/prevent-abbreviations– deactivated – same asunicorn/catch-error-name, I prefer an abbreviatederrover a non-abbreviatederrorbecause the latter is too similar toErrorfor my taste -
:mute:
unicorn/prefer-string-raw– deactivated -
:wrench:
unicorn/switch-case-braces– changed – I prefer to avoid braces incasestatements rather than enforcing them -
:mute:
unicorn/consistent-assert– deactivated – opinionated assert style preference that doesn't fit these projects -
:warning:
unicorn/consistent-template-literal-escape– changed – set towarninstead oferror -
:warning:
unicorn/isolated-functions– changed – set towarn, contentious scoping preference -
:warning:
unicorn/no-array-reverse– changed – set towarn, mutating.reverse()is common enough -
:warning:
unicorn/no-array-sort– changed – set towarn, mutating.sort()is widely used -
:warning:
unicorn/no-immediate-mutation– changed – set towarn, can be noisy -
:warning:
unicorn/prefer-class-fields– changed – set towarnfor gradual adoption -
:warning:
unicorn/prefer-classlist-toggle– changed – set towarn, DOM-specific -
:mute:
unicorn/prefer-import-meta-properties– deactivated – requires Node.js 20.11+, consistent withprefer-modulebeing off by default -
:warning:
unicorn/prefer-response-static-json– changed – set towarn, API-specific -
:warning:
unicorn/prefer-simple-condition-first– changed – set towarninstead oferror -
:warning:
unicorn/switch-case-break-position– changed – set towarninstead oferror
:heavy_plus_sign: Additional standalone ESLint rules
-
:stop_sign:
@stylistic/quote-props– requires properties to be quoted when needed but otherwise disallows it (disabled bynoStyle) -
:warning:
es-x/no-exponential-operators– disallows the use of the**operator, as that's in most cases a mistake and one really meant to write* -
:warning:
perfectionist/sort-imports– natural-order sort of import statements, with customgroupsordering: type imports first, then value imports; within each, singleline and multiline are paired tier by tier — builtin → external → [parent/sibling/index];newlinesBetween: 'ignore'so downstream formatting isn't disturbed (disabled bynoStyle) -
:warning:
perfectionist/sort-named-imports– natural-order sort of named import specifiers (disabled bynoStyle) -
:warning:
perfectionist/sort-named-exports– natural-order sort of named export specifiers (disabled bynoStyle) -
:warning:
perfectionist/sort-objects– natural-order sort of destructured object keys (replaceseslint-plugin-sort-destructure-keys) (disabled bynoStyle) -
:warning:
n/prefer-global/console -
:warning:
n/prefer-promises/fs -
:warning:
n/no-process-env -
:stop_sign:
n/no-sync -
:warning:
package-json/scripts-name-casing– added (not inrecommended), softened towarn– not auto-fixable (hasSuggestionsonly); enforces kebab-casescriptsnames; catches theclean:hashed→clean-hashedrename pattern -
:stop_sign:
promise/prefer-await-to-then -
:stop_sign:
unicorn/consistent-destructuring– while unicorn dropped it from their recommended config I still like it, see #283
ESM specific rules
Unless one configures cjs: true these additional rules will be applied:
:wrench: Overrides of rules
- :warning:
func-style– enforces function declarations whenever an arrow function isn't used. Better to doexport function foo () {thanexport const foo = function () { - :stop_sign:
unicorn/prefer-module– changed – restored to itsplugin:unicorn/recommendedvalue oferror
Can I use this in my own project?
You may want to use neostandard instead, it's the general base config that I help maintain for the community. If you follow the types-in-JS approach, this config provides battle-tested JSDoc rule tuning validated against 50+ downstream repositories.
I do maintain this project though as if multiple people are using it, so sure, you can use it, but its ultimate purpose is to support my projects.
I do follow semantic versioning, so the addition or tightening of any checks will trigger major releases whereas minor and patch releases should only ever have relaxation of rules and bug fixes.
Alternatives
See also
voxpelli/ghatemplates– the templates I use withghatto update GitHub Actions in my projectsvoxpelli/renovate-config-voxpelli– the shareable renovate setup I use in my projectsvoxpelli/tsconfig– the shareabletsconfig.jsonsetup I use in my projects