Format and fix code: VS Code config and git pre-commit hook with husky and lint-staged

James ZhangJames Zhang
2 min read

In a typical web code repository, we use linters like eslint to highlight warnings or syntax errors and use tools like prettier to format the code.

There are a few ways to trigger the action:

VS Code

At editor level, e.g. VS Code, we can use command palette to run it or add keyboard shortcuts. We can also format on save. E.g. for VS Code settings.json

We can do

{
    "editor.codeActionsOnSave": ["source.formatDocument", "source.fixAll.eslint"]
}

Another way is to enforce this at pre-commit or pre-push git lifecycles. This will ensure that committed code are all complied to a same code format. And at this repo level, we can set up the standard across the team collaborating on this.

Husky

We can use husky to config arbitrary shell cmds to be run pre-commit.

pnpx husky-init && pnpm install

Then we can edit shell cmds in .husky/pre-commit

E.g. we can create custom linting scripts in package.json like

{
  // ...
  "scripts": {
    "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
    "lintfix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
    "prepare": "husky && husky install"
  }
}

Then add npm run lintfix to .husky/pre-commit

Lint-staged

We may not want to format the whole code repo on each commit which is not performant

Alternatively we can only format staged files and format incrementally with caching

pnpx mrm lint-staged

Then add to package.json

{
  "lint-staged": {
    "*.{json,js,jsx,ts,tsx,css,scss,md}": "prettier . --config .prettierrc --write",
    "*.{ts,tsx,js,jsx}": "eslint --cache --max-warnings 0"
  }
}

Now modify .husky/pre-commit to

npx lint-staged

Now on each commit it will only run eslint and prettier for staged files.

One more follow up on error with lint-staged

The above setup results in a error when I have generated files that shouldn't be linted but picked up by lint-staged:

/path/to/file/name.min.js
  0:0  warning  File ignored because of a matching ignore pattern. Use "--no-ignore" to override

The solution is introduced in https://stackoverflow.com/questions/37927772/how-to-silence-warnings-about-ignored-files-in-eslint

Create a new file lint-staged.config.js under root

const { ESLint } = require('eslint');

const removeIgnoredFiles = async (files) => {
  const eslint = new ESLint();
  const ignoredFiles = await Promise.all(
    files.map((file) => eslint.isPathIgnored(file))
  );
  const filteredFiles = files.filter((_, i) => !ignoredFiles[i]);
  return filteredFiles.join(' ');
};

module.exports = {
  '*.{json,js,jsx,ts,tsx,css,scss,md}': () =>
    'prettier . --config .prettierrc --write',
  '*.{js,ts,jsx,tsx}': async (files) => {
    const filesToLint = await removeIgnoredFiles(files);
    return [`eslint --max-warnings=0 ${filesToLint}`];
  },
};

This will ignore the files

If we adopt this then we can remove the lines about lint-staged in package.json

0
Subscribe to my newsletter

Read articles from James Zhang directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

James Zhang
James Zhang

Founding Engineer at Bazar (General Catalyst backed), Ex Meta Ads MLE, Ex Activision Blizzard Intern