@useinsider/eslint-config
v1.14.0
Published
A collection of ESLint configurations provided by @useinsider, offering support for various environments including Vanilla JS, TypeScript, and framework-specific setups such as Vue 3.
Maintainers
Readme
@useinsider/eslint-config
Shared ESLint 9 flat-config for Insider projects. useInsider() takes a single
scope-keyed config object and returns a defineConfig([...]) array — one
entry per file scope you describe. There is no opaque preset list, no
freeform config escape hatch, and no expectation that you spread the result
into your own array.
The default export is the resolved Promise returned by useInsider(...);
ESLint 9 awaits it automatically. Do not await it. Do not spread it. Do
not wrap it in an array. If you find yourself wanting to append a block,
that is a sign your scope coverage is incomplete — describe the extra files
inside useInsider({...}) instead.
Table of Contents
- Prerequisites
- Quick start
- Manual setup
- API reference
- Migrating from v1.x
- First lint after install
- Troubleshooting
- Contributing
Prerequisites
- Node.js 20+
- ESLint 9+
- pnpm, npm, or yarn
Quick start
npx @useinsider/eslint-configThe CLI walks an interactive prompt (project type, language, framework, test
runner, jQuery globals, tsconfig layout), installs every required ESLint
plugin pinned to the versions this package was built against, and writes a
starter eslint.config.mjs at the project root.
To migrate an existing v1 eslint.config.{js,mjs,cjs} or a legacy
.eslintrc.{js,cjs}:
npx @useinsider/eslint-config migrate [path]The migrate flow auto-detects the source format. For flat configs it
parses the v1 file, prints a summary plus any dropped custom rules, asks
for confirmation, then rewrites the file in place. For legacy .eslintrc
files it writes a fresh eslint.config.mjs next to the original (custom
rules and non-@useinsider/eslint-config/* extends are dropped with a
loud warning so you can review what changed). Legacy .eslintrc.{json,yml,yaml}
are not supported in this release — convert them to .eslintrc.{js,cjs} first.
Example output for a Node + TypeScript project:
import { useInsider } from '@useinsider/eslint-config';
export default useInsider({
typescriptNode: [
{
files: ['**/*.{ts,tsx}'],
tsconfigPaths: ['tsconfig.json'],
},
],
});What the CLI installs for that answer set (versions reflect the publish-time pins):
@useinsider/eslint-configeslintglobalstypescripttypescript-eslint
Vue, Jest, or Vitest answers add the matching plugin set (e.g. eslint-plugin-vue,
vue-eslint-parser, eslint-plugin-vue-scoped-css, eslint-plugin-jest,
or @vitest/eslint-plugin).
Manual setup
Install the package first:
pnpm add -E -D @useinsider/eslint-configThen create eslint.config.mjs and call useInsider(...) with the scopes
your project needs. Each scope holds an array of { files, ... } entries;
each entry becomes its own flat-config block.
Node + TypeScript
import { useInsider } from '@useinsider/eslint-config';
export default useInsider({
typescriptNode: [
{
files: ['**/*.{ts,tsx}'],
tsconfigPaths: ['tsconfig.json'],
},
],
vitest: [
{ files: ['**/*.{test,spec}.{ts,tsx}'] },
],
});Browser + Vue 3 + TypeScript with split tsconfigs
A project using tsconfig.app.json for source and tsconfig.test.json for
tests, with RouterLink and RouterView as globally-registered components.
import { useInsider } from '@useinsider/eslint-config';
export default useInsider({
vue3Typescript: [
{
files: ['src/**/*.{ts,tsx,vue}'],
tsconfigPaths: ['tsconfig.app.json'],
globalComponents: ['RouterLink', 'RouterView'],
},
],
vitest: [
{
files: ['**/*.{test,spec}.{ts,tsx}'],
},
],
typescriptNode: [
{
files: ['*.config.{ts,mts,cts}'],
tsconfigPaths: ['tsconfig.node.json'],
},
],
});Multiple tsconfigPaths entries are supported per block — pass an array of
paths if a single scope needs more than one project reference.
Vue 2 + JavaScript with jQuery globals
import { useInsider } from '@useinsider/eslint-config';
export default useInsider({
vue2: [
{ files: ['src/**/*.{js,vue}'] },
],
javascriptNode: [
{ files: ['*.{js,cjs,mjs}'] },
],
jquery: [
{ files: ['src/**/*.{js,vue}'] },
],
});jquery is a top-level scope. Pass an array of { files } entries describing
the globs that should see the jQuery globals ($, jQuery). The block is
emitted after the scope blocks and before strict/spellChecker, so the
matching language scope still applies its rule set and jQuery globals are
layered on top of it.
Strict and spell-checker knobs
strict and spellChecker are flat cross-cutting options. They sit at the
top level of the config, not inside a scope entry.
import { useInsider } from '@useinsider/eslint-config';
export default useInsider({
typescriptNode: [
{
files: ['**/*.{ts,tsx}'],
tsconfigPaths: ['tsconfig.json'],
},
],
strict: ['src/**/*.ts'],
spellChecker: {
ignoredPaths: ['vendor/**', 'dist/**'],
customWordListFile: './cspell.custom.json',
},
silenceRules: ['no-console'],
});strict: trueapplies strict rules to every file.strict: string[]applies strict rules only to the matching paths.spellChecker.customWordListFilere-points@cspell/spellcheckerto a project-local word list.spellChecker.ignoredPathsturns the spell-checker off for the matching globs (the off block is emitted after the custom-list block, so ignore wins on overlap).silenceRulesrewrites the severity of the listed rule IDs to'warn'across every emitted block while preserving each rule's options.
API reference
useInsider(config) accepts the following top-level properties. Each scope
holds an array of entries; each entry produces its own flat-config block.
Every scope entry shape also accepts two common fields:
ignores?: string[]— applies as the block'signoreskey, scoping the rule set away from the listed globs.globals?: Record<string, GlobalConf>— merged on top of the globals the preset already provides; pass'readonly','writable', or'off'per ESLint'sLinter.Globals. Use this to declare project-specific globals likeAPP_URL,axios,atatus.
| Property | Entry shape | Notes |
| ---------------- | -------------------------------------------------------------------------------------------------------- | ----- |
| ignores | string[] (top-level) | Emits a leading { ignores } block applied to every other block. |
| javascript | { files: string[]; ignores?: string[]; globals?: Linter.Globals } | Environment-agnostic JS (workers, isomorphic). |
| javascriptDom | { files: string[]; ignores?: string[]; globals?: Linter.Globals } | Browser JS. |
| javascriptNode | { files: string[]; ignores?: string[]; globals?: Linter.Globals } | Node JS. |
| typescript | { files: string[]; tsconfigPaths: string[]; ignores?: string[]; globals?: Linter.Globals } | Environment-agnostic TS. |
| typescriptDom | { files: string[]; tsconfigPaths: string[]; ignores?: string[]; globals?: Linter.Globals } | Browser TS. |
| typescriptNode | { files: string[]; tsconfigPaths: string[]; ignores?: string[]; globals?: Linter.Globals } | Node TS. |
| vue2 | { files: string[]; globalComponents?: string[]; ignores?: string[]; globals?: Linter.Globals } | Vue 2 single-file components in JS. |
| vue3 | { files: string[]; globalComponents?: string[]; ignores?: string[]; globals?: Linter.Globals } | Vue 3 single-file components in JS. |
| vue2Typescript | { files: string[]; globalComponents?: string[]; tsconfigPaths: string[]; ignores?: string[]; globals?: Linter.Globals } | Vue 2 + TS. |
| vue3Typescript | { files: string[]; globalComponents?: string[]; tsconfigPaths: string[]; ignores?: string[]; globals?: Linter.Globals } | Vue 3 + TS. |
| jest | { files: string[]; ignores?: string[]; globals?: Linter.Globals } | Jest test files. |
| vitest | { files: string[]; ignores?: string[]; globals?: Linter.Globals } | Vitest test files. |
| jsdocBasedTs | { files: string[]; tsconfigPaths: string[]; ignores?: string[]; globals?: Linter.Globals } | JS files type-checked via JSDoc. |
| jquery | { files: string[]; ignores?: string[]; globals?: Linter.Globals } | Layers jQuery globals ($, jQuery) on top of the matching globs. Per-entry globals merge with the jQuery ones rather than replacing them. |
| strict | boolean \| string[] | Applies the strict preset globally (true) or scoped to globs. |
| spellChecker | { ignoredPaths?: string[]; customWordListFile?: string } | Cross-cutting cspell overrides. |
| silenceRules | string[] | Rule IDs to demote to 'warn' across every block. |
| configFiles | false \| string[] | Emits a dedicated block for root-level configuration files (*.config.*, eslint.config.*) that disables import-x/no-extraneous-dependencies and import-x/prefer-default-export. Defaults are applied automatically — omit the key (or pass []) to use the built-in defaults only, pass a string[] to layer your own globs on top, or pass false to opt out entirely. |
A scope entry with per-entry globals — declare project-specific bindings
without spreading the useInsider return into your own array:
import { useInsider } from '@useinsider/eslint-config';
export default useInsider({
ignores: ['dist', 'coverage'],
javascriptDom: [
{
files: ['src/**/*.js'],
ignores: ['src/vendor/**'],
globals: {
APP_URL: 'readonly',
axios: 'readonly',
atatus: 'readonly',
},
},
],
});Block-emission order — scope blocks first (in the order their keys were
inserted into the config object), then jquery blocks, then any strict
blocks, then spellChecker blocks. Later blocks win on overlap, matching
ESLint's flat-config precedence model.
useInsider returns Promise<Linter.Config[]>. Export it directly — ESLint
9 resolves the Promise at load time. useInsider's return type carries
through, so no JSDoc annotation is needed; the UseInsiderResult type alias
is exported for TypeScript callers that wrap useInsider in a helper of
their own.
silenceRules() (named export) is also available as a global registration
hook. Call it at module top level to register rule IDs to demote before
useInsider is invoked; the per-call silenceRules: [...] option layers
on top.
import { useInsider, silenceRules } from '@useinsider/eslint-config';
silenceRules(['no-magic-numbers']);
export default useInsider({ /* ... */ });Migrating from v1.x
v2 is a hard break. The summary:
preset: string[]→ top-level scope properties. The order in the v1presetarray no longer matters — the new shape has one property per scope and overrides are explicit.- await useInsider({ - preset: ['typescript-node'], - config: { files: ['src/**/*.ts'], languageOptions: { parserOptions: { project: ['./tsconfig.json'] } } }, - }) + useInsider({ + typescriptNode: [ + { files: ['src/**/*.ts'], tsconfigPaths: ['tsconfig.json'] }, + ], + })preset: ['vue3-typescript']+config→vue3Typescriptentry with a dedicatedglobalComponentsfield. No more hand-rolledvue/no-undef-componentsoverrides.- await useInsider({ - preset: ['vue3-typescript'], - config: { - files: ['src/**/*.{ts,vue}'], - languageOptions: { parserOptions: { project: ['./tsconfig.app.json'] } }, - rules: { 'vue/no-undef-components': ['error', { ignorePatterns: ['^RouterLink$', '^RouterView$'] }] }, - }, - }) + useInsider({ + vue3Typescript: [ + { + files: ['src/**/*.{ts,vue}'], + tsconfigPaths: ['tsconfig.app.json'], + globalComponents: ['RouterLink', 'RouterView'], + }, + ], + })preset: ['strict']→ top-levelstrict: true | string[].- await useInsider({ preset: ['typescript-node', 'strict'], config: { files: ['src/**/*.ts'] } }) + useInsider({ + typescriptNode: [{ files: ['src/**/*.ts'], tsconfigPaths: ['tsconfig.json'] }], + strict: true, + })Behavior change: in v1 the
strictpreset was always global when listed. In v2,strict: truekeeps that global behavior;strict: string[]scopes the strict rule set to the matching globs only. If you have paths-targeted strict checks, setstrict: [...]instead ofstrict: true.Shape change:
useInsider(...)is now exported directly as the default export, withoutawait, without spread, and without an array wrap. ESLint 9 resolves the Promise on load. The old patternexport default [...await useInsider({...})]still parses, but the preferred shape isexport default useInsider({...}). There is no supported override path — describe every glob you want linted inside the call, including jQuery files via the dedicatedjqueryscope.Removed: the
presetarray, theconfig: Linter.Configescape hatch, and theconfigpreset itself. Configuration-file linting is now an ordinarytypescriptNode(orjavascriptNode) entry pointed at the right tsconfig.Kept:
silenceRulesaccepts the samestring[]shape both as a top-level option and via the namedsilenceRules()export.silenceDependencyWarning()works unchanged. Side-effect lines such assilenceDependencyWarning(true)continue to sit above theexport default useInsider({...})call.
First lint after install
The first time you run lint after installing v2, ensureDependencies may
detect that your project's eslint or globals version is older than the
pin and auto-install them, exiting with code 1. Run lint again and it
will complete normally.
Troubleshooting
Missing dependencies detected:
If you see this error, your project is missing a peer plugin this config needs. The error message lists the install command. To silence the warning permanently:
import { useInsider, silenceDependencyWarning } from '@useinsider/eslint-config';
silenceDependencyWarning(['@cspell/eslint-plugin', '@stylistic/eslint-plugin']);
export default useInsider({ /* ... */ });Passing true silences every warning; that is not recommended because the
warning is the only signal that your installed plugin versions drifted
from what the config expects.
silenceDependencyWarning(true);Contributing
See CONTRIBUTING.md at the repo root.
