@karta.io/eslint-config
v2.3.0
Published
A Eslint config we use for our projects
Readme
Eslint config
Flat ESLint config for JavaScript, TypeScript, Vue 3, Prettier. Thanks to sxzz for example.
Features
- Format with Prettier.
- Designed to work with TypeScript and Vue 3 out-of-box.
- Support JSON(5), YAML, Markdown...
- Enforces the Karta import group order and sorts keys in
package.json,tsconfig.json... - ESLint Flat config, compose easily!
- Reasonable defaults, best practices, only one-line of config
Usage
Install
npm install @karta.io/eslint-config eslint --save-dev
# or with pnpm:
pnpm add -D @karta.io/eslint-config eslintRequires Node.js ^20.19.0 || ^22.13.0 || >=24 and ESLint ^9.38.0 || ^10.0.0.
Both floors come from the plugins, not from ESLint itself. The Node range is declared by
eslint-plugin-jsonc@3, eslint-plugin-yml@3 and eslint-plugin-n@18; the 9.38.0
floor comes from the first two, which both require eslint >=9.38.0. Staying on ESLint 9
does not lower either.
And create eslint.config.mjs in your project root:
// eslint.config.mjs
import { createConfig } from '@karta.io/eslint-config';
export default createConfig();Add script for package.json
For example:
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}Customization
We use ESLint Flat config. It provides much better organization and composition.
Normally you only need to import the createConfig function:
// eslint.config.mjs
import { createConfig } from '@karta.io/eslint-config';
export default createConfig();And that's it! Or you can enable/disable each integration individually, for example:
// eslint.config.mjs
import { createConfig } from '@karta.io/eslint-config';
export default createConfig({
settings: {
// Vue is auto-detected, you can also explicitly enable or disable it:
vue: true,
// Disable yml and markdown support
yml: false,
markdown: false,
// Disable prettier support
prettier: false,
// Disable jsonc support and sorting keys for package.json and tsconfig.json
jsonc: false,
sortKeys: false,
},
});The keys are exactly vue, prettier, sortKeys, markdown, jsonc and yml. In a
.mjs config nothing type-checks them, so an unknown key is silently ignored — earlier
revisions of this file documented yaml, which never disabled anything.
Vue detection is package-based: the Vue config turns on when vue, nuxt, vitepress
or @slidev/cli resolves from the working directory. In a directory where Vue is not
installed — a scratch dir, a script that lints another project's sources — .vue files
are silently left unlinted. Pass vue: true explicitly when that matters.
And also you can override any rule:
// eslint.config.mjs
import { createConfig } from '@karta.io/eslint-config';
export default createConfig({
// https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects
config: [
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-redeclare': 'off',
},
},
{
files: ['**/*.vue'],
rules: {
'vue/no-dupe-keys': 'error',
},
},
],
});Upgrading to 2.0.0
Four breaking changes. The first three are mechanical, the fourth is the one that produces a large diff.
ESLint 10 support, ESLint 9 kept. The peer range widens to ^9.38.0 || ^10.0.0, so
you can take this version without jumping to ESLint 10 in the same step. CI here runs
both ends of that range.
Node floor. See the install section above — repositories still on Node 20.12 need the runtime bump first.
Rule namespace of eslint-comments. The abandoned eslint-plugin-eslint-comments
is replaced by @eslint-community/eslint-plugin-eslint-comments, so rule ids change
from eslint-comments/* to @eslint-community/eslint-comments/*. This only matters if
you reference those ids yourself — in an override, or in an inline eslint-disable
comment.
Style-guide conventions are now enforced as rules. They used to live as prose in
CLAUDE.md and were checked by eye during review:
import/order— full Karta group order (external →@karta.io→ stores → query-client → queries → sdk → services → api → router → composables → helpers → data → mocks → seeds → enums → interfaces → assets → components), plus a blank line between rank groups;vue/attributes-order— the style-guide order, which differs from the plugin default in one place:v-html/v-textcome before events;vue/component-api-style—<script setup>only;vue/match-component-file-name— the name indefineOptionsmust match the file name. Note it only checks a name that exists: a component withoutdefineOptionsis not reported. That gap is covered bykarta/require-component-name(warn), the package's own rule: an SFC with a<script>block must setdefineOptions({ name }). Template-only files are out of scope, a non-literal name counts as set, and there is no autofix;karta/match-class-name-file-name(warn) — the block passed touseClassName('…')must be the kebab-case file name (ItemCard.vue→item-card,UIButton.vue→ui-button). Only the first call in a file is checked, since later calls legitimately name child blocks; a non-literal first argument skips the file, and there is no autofix;vue/padding-line-between-tags— a blank line between adjacentdivblocks;no-magic-numbers(warn) — a bare number inside an expression must be a named constant: a comparison, an argument, arithmetic.0,1,-1and array indexes are allowed, and so is a default value (searchDelay = 300), because its name already is the constant. Three places stay out of scope by design, so do not read a clean run as "no magic numbers left": a value assigned to a variable (const timeout = 45000— the name is the point), a property value ({ limit: 50 }, sincedetectObjectsis off), and a numeric enum member. The property case is the one that bites: turningdetectObjectson costs +152 findings in the admin panel alone, and the largest single file is a plan-price table where the numbers are the data. There is no autofix.
Three of them auto-fix: import/order, vue/attributes-order and
vue/padding-line-between-tags. Expect eslint . --fix to touch a large number of files
on first run, and land that run as its own commit so it stays reviewable. The rest need
editing by hand — vue/component-api-style, no-magic-numbers and both karta/* rules have no
fixer, and vue/match-component-file-name only offers an editor suggestion, which --fix does
not apply.
Versioning
The public surface of this package is not only what it exports — it is the verdict it gives on someone else's code. So the bump is chosen by what happens in a consumer, not by how large the diff here is:
- major — a rule turns from
warnintoerror, or a config entry point / option is renamed or removed. Code that used to pass CI now fails it, and there is no autofix; - minor — a new rule, a new group, or any change to what
--fixproduces. Consumer code that was clean yesterday is reported today, but the fixer resolves it. Both 2.1.0 (three new import groups) and 2.2.0 (assetsmoved, relative*.mockdropped) are this case: they reordered imports in dozens of consumer files. A new rule with no fixer belongs here too — as long as it lands aswarn, which is why every rule of ours without a fixer does. A new rule that lands aserrorwith no fixer is major by the first bullet's own logic: it fails a consumer's CI on the bump itself, so it breaks the delivery and not just the code; - patch — no verdict changes anywhere: docs, internal refactor of the build, a fix to the package's own types or tests.
Consumers pin the exact version ("@karta.io/eslint-config": "2.2.0", no caret), so a bump never
reaches anyone by itself — which is exactly why the number has to carry the meaning: it is the only
signal of how much churn the upgrade brings before you run it.
