oxlint-plugin-effector
v0.1.1
Published
Enforcing best practices for Effector — a full type-aware oxlint plugin (port of eslint-plugin-effector)
Maintainers
Readme
oxlint-plugin-effector
Enforce Effector best practices with oxlint.
A full, type-aware port of eslint-plugin-effector to oxlint's
custom JS plugin API. All 23 rules (22 ported + no-multiple-useUnit) are the original rules,
unchanged — this package runs the upstream plugin's entire test suite (349 cases) against the real TypeScript type
checker and passes it.
How it stays type-aware on oxlint
The Effector rules need TypeScript types (to tell a Store from an Event, follow units across files, etc.).
oxlint does not hand type information to JS plugins. So instead of relying on it, the plugin builds its own
ts.Program using the typescript package directly and calls getTypeAtLocation itself, mapping oxlint's AST
nodes to TS nodes by source range. This is an oxlint plugin — it does not target or support ESLint; under
oxlint the plugin always constructs its own program. There is no @typescript-eslint in the runtime — the
only runtime dependencies are typescript (peer) and esquery. (The test suite happens to run the rules through
@typescript-eslint/rule-tester to validate against the real type checker, but that's a testing detail, not a
runtime target.)
Program discovery (src/shared/services.ts) is on plain typescript and handles the awkward setups:
- the nearest
tsconfig.jsonis used; project references are followed, so a solution-style root tsconfig (onlyreferences, nofiles) resolves the project that actually owns the file; - if no tsconfig is found, a node-resolution fallback is used so
effector/reacttypes still resolve; - set
OXLINT_EFFECTOR_TSCONFIGto point at a specific tsconfig (custom names, monorepo roots).
Performance
Several rules are type-aware, so the plugin builds its own TypeScript program and resolves types — the same
fixed cost any type-aware linter pays (comparable to typescript-eslint with parserOptions.project). oxlint does
not expose its native type information to JS plugins, so this cost cannot be shared with oxlint's own type checker.
On a real ~350-file React project expect a few seconds over oxlint's native rules, dominated by the one-time
program build + type resolution.
Import gate (automatic). Effector units originate from effector / effector-react / patronum, so a file
that imports none of them has no units for the type-aware rules to find. Such files skip type analysis entirely
(the purely syntactic rules still run). This avoids type-checking the (usually large) majority of files that don't
touch Effector. Edge case: a unit imported into a file that itself imports none of those packages is not
type-checked there — rare in practice. (Syntactic rules and the full 349-case test suite are unaffected.)
Further levers if it's still too slow:
OXLINT_EFFECTOR_TSCONFIG→ point at a lean tsconfig thatincludes only your source (skip tests/build output) so the program is smaller;- enable fewer type-aware rules — the syntactic rules (
no-forward,no-guard,keep-options-order,no-duplicate-on, …) cost nothing.
Installation
npm install -D oxlint oxlint-plugin-effector typescriptoxlint JS plugins require an oxlint version that supports jsPlugins. typescript is a peer dependency (the
plugin uses it for type analysis).
Usage
Register the plugin under jsPlugins; rules are namespaced effector/<rule>.
// .oxlintrc.json
{
"jsPlugins": ["oxlint-plugin-effector"],
"rules": {
"effector/enforce-store-naming-convention": ["error", { "mode": "prefix" }],
"effector/no-watch": "warn",
"effector/no-getState": "error"
}
}Severity presets are exported on effector.configs (recommended, react, scope, future, patronum) for use
from a JS/TS config:
// oxlint.config.js
import effector from "oxlint-plugin-effector"
export default {
jsPlugins: ["oxlint-plugin-effector"],
rules: { ...effector.configs.recommended, ...effector.configs.react },
}Rules
recommended
| Rule | Description |
| --- | --- |
| enforce-effect-naming-convention | Enforce Fx suffix for Effects |
| enforce-store-naming-convention | Enforce $ prefix/postfix for Stores |
| keep-options-order | Enforce options order for Effector methods |
| no-ambiguity-target | Forbid ambiguous target in sample/guard |
| no-duplicate-on | Forbid duplicate .on calls on Stores |
| no-forward | Prefer sample over forward |
| no-getState | Forbid .getState on Stores |
| no-guard | Prefer sample over guard |
| no-unnecessary-combination | Forbid unnecessary combine/merge in clock/source |
| no-unnecessary-duplication | Forbid duplicate source and clock |
| no-useless-methods | Forbid useless sample/guard calls |
| no-watch | Restrict .watch on units |
react
| Rule | Description |
| --- | --- |
| enforce-gate-naming-convention | Enforce capitalized Gate names |
| enforce-exhaustive-useUnit-destructuring | Ensure units passed to useUnit are destructured |
| mandatory-scope-binding | Forbid Event/Effect usage without useUnit |
| no-multiple-useUnit | Forbid more than one useUnit call per component (fixable) |
| no-units-spawn-in-render | Forbid creating units / calling operators in render |
| prefer-useUnit | Prefer useUnit over useStore/useEvent |
scope
| Rule | Description |
| --- | --- |
| require-pickup-in-persist | Require pickup in effector-storage persist |
| strict-effect-handlers | Forbid mixing regular async fns and Effects |
future
| Rule | Description |
| --- | --- |
| no-domain-unit-creators | Disallow Domain methods to create units |
patronum
| Rule | Description |
| --- | --- |
| no-patronum-debug | Disallow patronum's debug |
See examples/vite-comparison for a project linted by both this plugin and
eslint-plugin-effector, producing identical results.
License
MIT. Rules ported from eslint-plugin-effector (MIT).
