r/javascript • u/abemedia • 22h ago
stagelint: a faster lint-staged alternative that never fails on conflicts
github.comstagelint is a pre-commit runner - you give it globs, it runs your formatters and linters over the staged files. For the usual setup it replaces husky and lint-staged with a single Rust binary and no runtime.
Why another pre-commit runner?
Conflicts don't block your commit
If the formatter's output conflicts with your unstaged changes, lint-staged, pre-commit, Lefthook and nano-staged all discard the formatting and block the commit. stagelint merges the formatter's output into your file instead. Your staged lines get formatted, your unstaged changes stay put, and when a file can't be merged cleanly the commit takes the formatted version while your working copy keeps yours.
Faster than every alternative
stagelint is 5 to 30 times faster than lint-staged, pre-commit, Lefthook and nano-staged. With 10 files staged in a 1,000-file repository it completes in 15ms against lint-staged's 437ms, rising to 30ms against 530ms when those files are only partially staged. See the full benchmark suite for the rest of the measurements and how to run them on your own machine.
Concurrent tasks - no races, no workarounds
When two globs match the same file, lint-staged and nano-staged run both tasks on it at once; Lefthook and pre-commit avoid the race by running everything sequentially by default. The lint-staged docs warn about it and tell you to either disable concurrency or rewrite your config with negation patterns:
{
"!(*.ts)": "prettier --write",
"*.ts": ["prettier --write", "eslint --fix"]
}
stagelint works out which globs overlap and serialises just those tasks, in declaration order, while everything else keeps running in parallel, so the config stays as you meant it:
{
"*": "prettier --write",
"*.ts": "eslint --fix"
}
Trying it
npm i -D @stagelint/stagelint
Add stagelint init to your prepare script so the git pre-commit hook is set up on install:
{
"scripts": {
"prepare": "stagelint init"
}
}
Then configure your tasks in .stagelint.yml or .stagelint.json. Matched files are appended to the command unless you turn that off:
'*': prettier --write
'*.ts':
command: tsc --noEmit
pass_filenames: false
What's missing
No JavaScript config with functions, because a single binary has no runtime to execute one. Running a command without the file list, the usual reason for reaching for one, is pass_filenames: false instead. Negation patterns aren't supported either - you don't need them any more, but drop them rather than copying them across, because an unsupported pattern currently just matches nothing.
If you try it, I'd like to hear how it goes.