ESLint → oxlint Migration
Move lint rules from ESLint to oxlint whenever oxlint gains coverage, prioritized by what actually costs time in the ESLint pass. Oxlint runs the whole repo in well under a minute; the ESLint pass takes several minutes, and a handful of type-aware rules account for most of its rule time.
What works today
- One root
.oxlintrc.jsonruns oxlint repo-wide withtypeAware: true—oxlint-tsgolintexecutes the type-awaretypescript/*rules (no-floating-promises,await-thenable,no-duplicate-type-constituents, …) natively. eslint-plugin-oxlint(packages/configuration/eslint/oxlint.js) reads the same.oxlintrc.jsonviabuildFromOxlintConfigFileand appends"off"entries for every ESLint rule oxlint already covers. It is appended last in both flat configs, so its disables win.- The
correctnesscategory is listed explicitly in.oxlintrc.json. This matters: oxlint itself keepscorrectnessenabled by default even when other categories are configured, buteslint-plugin-oxlintreplaces its default categories with the configured ones. Beforecorrectnesswas explicit, the plugin assumed the category was off and left the ESLint twins of every correctness rule enabled — so ESLint re-ran the four most expensive type-aware rules (roughly half its rule time) that oxlint was already checking. - The whole
typescript-eslintstrictTypeChecked+stylisticTypeCheckedrule set is now covered by oxlint's 110typescript/*rules.packages/configuration/eslint/typescriptRules.jsno longer spreads those configs (and thetypescript-eslintpackage has been removed) — it holds onlyno-restricted-syntax, the one rule oxlint cannot express yet (no AST-selector rule).prefer-optional-chain,no-restricted-imports(therandomUUIDban),no-restricted-types(theOmit→Exceptban), andno-unused-expressionswere moved into.oxlintrc.json. - A migrated ban must be configured in oxlint, not merely un-deleted from ESLint.
eslint-plugin-oxlintdisables the ESLint twin of any rule oxlint has (e.g.no-restricted-imports,no-restricted-types,no-unused-expressions) regardless of whether oxlint's copy is configured. So an ESLint-side ban for such a rule is silently dead the moment oxlint ships the rule name — the ban only lives if it is written into.oxlintrc.json. Verify witheslint --print-config <file>(the rule should read[0]/off) plus a planted violation run through oxlint.
flowchart LR config[".oxlintrc.json (single source of truth)"] oxlint["oxlint + oxlint-tsgolint (type-aware)"] plugin["eslint-plugin-oxlint (buildFromOxlintConfigFile)"] eslint["ESLint (only rules oxlint lacks)"] config -->|"categories + rules + typeAware"| oxlint config -->|"same file"| plugin plugin -->|"appends off for every covered rule"| eslint
What remains in ESLint and why
The remaining expensive rules, in descending cost order, and the trigger for migrating each:
| Rule | Share of rule time | Blocker | Migrate when |
|---|---|---|---|
vue/no-child-content | ~a tenth | Not implemented in oxlint's vue plugin | upstream implements it (check eslint-plugin-oxlint's generated rule maps after upgrades) |
perfectionist/sort-imports | small | oxlint has no import-sorting rule | upstream implements sorting |
no-restricted-syntax | small | oxlint has no AST-selector rule | oxlint ships a selector-based rule (the custom bans move into .oxlintrc.json) |
neverthrow/must-use-result was dropped, not migrated. It was the single most expensive rule (~a third of rule time) because it is the only one that needed parserOptions.projectService, and type-aware parsing dominates the whole ESLint pass rather than just that rule's share. Deleting it — plugin file, config entry and @ninoseki/eslint-plugin-neverthrow dependency — is what makes pnpm lint fast enough to run on every change. The cost is real and accepted: an unterminated Result chain is now caught only by review, and it fails silently (the call still runs, but the Err it returns is never read, so no error surfaces). Re-adding any type-aware ESLint plugin gives back the same multiplier, so the answer to "this convention needs types" is a review rule or a runtime assertion, never a rule here.
Everything else in the ESLint pass is either Nuxt/Vue-specific (vue/* SFC rules, nuxt/*) or a plugin oxlint has not implemented (perfectionist/*, pinia/*, unocss/*); none of them individually costs meaningful time. typescript/naming-convention is parked as a commented-out block in typescriptRules.js — it was too expensive under typescript-eslint to ever ship, and is waiting on oxlint to support it.
Ongoing process
On every oxlint / eslint-plugin-oxlint catalog bump:
- Re-measure with
TIMING=10on the app ESLint run to see which rules now dominate. - Check whether the top ESLint rules appear in
eslint-plugin-oxlint's generated rule maps (dist/generated/rules-by-category.*— including the*TypeAwareRulessets). If a rule is newly covered, it disappears from ESLint automatically via the appended config — verify witheslint --print-configon a sample file rather than editing anything. - For custom rules (
no-restricted-syntaxselectors), evaluate oxlint's JS-plugin support as it matures — non-type-aware custom rules can move as soon as oxlint's plugin API supports the needed AST surface for.vueand.tsfiles. - Remove ESLint-side manual
"off"entries that only existed to duplicate oxlint coverage (they are dead weight onceeslint-plugin-oxlintdisables the rule).
Key files
| File | Role |
|---|---|
.oxlintrc.json | Single source of truth: categories (list correctness explicitly), per-rule overrides, typeAware |
packages/configuration/eslint/oxlint.js | Builds the ESLint disable config from .oxlintrc.json |
packages/configuration/eslint/index.typescript.js, index.vue.js | Append the oxlint disables last so they win |
packages/configuration/eslint/typescriptRules.js | ESLint-only rules oxlint cannot express — just no-restricted-syntax (plus the parked naming-convention) |
Notes
- Never remove
correctnessfrom the explicit category list — oxlint would still run those rules (its own default), buteslint-plugin-oxlintwould silently re-enable their ESLint twins and double-lint them. - Coverage parity is verified, not assumed: before relying on oxlint for a migrated rule, reproduce a violation in a scratch file and confirm oxlint reports it (tsgolint rules included).
Previous
Next