- Published on
Lint on Commit
My project setup to lint and type check my TypeScript code on commits.
On a recent project, I decided to give
lint-staged a try. For those who
don't know, it's a tool that runs linters against staged git files, thus
preventing poor code from slipping into the project code base. As I followed two
example projects using lint-staged,
here and
here,
I began to stumble down yet another rabbit hole of tooling pain.
To begin, I installed lint-staged and setup my package.json .
{
"scripts": {
"format": "prettier -w .",
"lint": "eslint . --ext ts --ext tsx",
"type-check": "tsc --pretty --noEmit"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.@(ts|tsx)": ["yarn format", "yarn lint", "yarn type-check"]
}
}You'll notice I use husky - a tool that
runs configured git hooks.
When I tried to commit a change, the type-check script failed, reporting
errors in packages within the node_modules folder. That's odd, since
tsconfig.json ignores node_modules.
The issue resides in how lint-staged calls tsc. It passes git staged files
to tsc via the command line, which means tsc ignores tsconfig.json.
(reference).
Fix #1
My first fix moved the call to type-check to its own git hook and removed it
from the lint-staged call list.
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "yarn type-check"
}
},
"lint-staged": {
"*.@(ts|tsx)": ["yarn format", "yarn lint"]
}
}This setup works, but I saw two consequences to this change.
type-checkchecks the entire project on eachgit push.type-checkruns after a commit, potentially allowing errors into the code base.
I can live with #1, but #2 defeats the whole purpose of the automatic type checking to ensure no errors get committed..
Fix #2
An alternative approach follows this
example.
I removed the lint-staged block from package.json and created a
lint-staged-config.js file as follows:
module.exports = {
'**/*.ts?(x)': () => 'yarn type-check',
'**/*.(ts|js)?(x)': (filenames) => `yarn lint ${filenames.join(' ')}`,
'**/*.js?(x)': (filenames) =>
filenames.map((filename) => `prettier --write '${filename}'`),
}In this setup, lint-staged runs type-check without command-line arguments
and tsc will run using tsconfig.json. This solution is cool, but perhaps
unnecessarily complex.
Husky v5
While my setup above was technically complete, I saw
typicode released husky v5 and decided to try it.
I installed husky v5, removed lint-staged.config.js and setup my
package.json.
{
"scripts": {
"format": "prettier -w .",
"lint": "eslint .",
"type-check": "tsc --pretty --noEmit",
"postinstall": "is-ci || husky install"
},
"lint-staged": {
"*.@(js|ts|tsx)": ["yarn lint", "yarn format"]
}
}Next, I ran the following command.
$ npx husky add .husky/pre-commit "yarn lint-staged"Lastly, I modified .husky/pre-commit - created in the above step - to run the
type-check script. Here's my final pre-commit script.
#!/bin/sh
[ -n "$CI" ] && exit 0
. "$(dirname "$0")/_/husky.sh"
yarn type-check
yarn lint-stagedWrap-up
With this last setup, I can run all my scripts manually from the command-line. The linting and formatting scripts run on staged files - keeping their execution times low. Lastly, type checking is part of the pre-commit hook - preventing errors from sneaking into the code base.
I'm happy with this setup. It's simple, clear and concise. You can see this setup in action here.