background

In recent research into lint-staged code, Husky has often worked with Lint-staged code to prevent misbehaving code from being delivered remotely.

I am very interested in the working principle of Husky, so I spent some time studying it. I hope this article will be of some help to my friends who are learning this content.

Lint Best Practices

“Best practices” may be a misnomer, but most front-end projects I’ve seen use this combination

Using Javascript as an example, to code Lint, there are the following steps:

  1. Install the appropriate packages, including:eslint,huskyandlint-staged, how to install is not the focus of this article, please learn by yourself
  2. Add the corresponding configuration:
    1. increase.eslintrc.jsFile, configurationeslintFor details, see the ESLint documentation
    2. inpackage.jsonor.huskyrcAdd to filehuskyConfiguration items
    "husky": {
      "hooks": {
        "pre-commit": "lint-staged"
      }
    },
    Copy the code
    1. Add lint-staged configuration items to package.json
    "lint-staged": {
      "**/*.js": "eslint"
    },
    Copy the code

At this point, code validation is performed at Git COMMIT, and only staged files are validated.

ESlint and Lint-staged episodes are not the focus of this article, learn about them yourself; this article focuses on Husky principles

Husky principle

Git hooks to introduce

How does Husky trigger code validation when code is submitted? Git hooks before we explore how they work, we need to introduce another concept: git hooks.

Like other version control systems, Git can trigger custom scripts when certain important actions occur. There are two sets of hooks: client-side and server-side. Client-side hooks are invoked by operations such as commit and merge, while server-side hooks are invoked by networked operations such as receiving pushed commits. You can use the hooks however you like.

Currently, Git supports 17 hooks, which are stored as separate scripts in the. Git /hooks folder:

In the case of a commit, pre-commit, prepare-commit-msg, commit-msg, and post-commit are automatically triggered. We can do something interesting with these hooks. For example, you can use pre-commit to verify your code, and commit- MSG to verify your commit message. If you know shell syntax, you can also use Perl, Ruby, or Python.

Also, we don’t need to create all the hook scripts, just on demand.

Git hooks/Husky hooks

Husky official description:

Git hooks make Easy

Imagine a scenario, for example, on a multi-player team, where you create hooks in.git/hooks that you want to share with your teammates, but the.git/hooks folder doesn’t commit to the remote end, so you have to copy it.

Husky was designed to solve this problem. The hook works with a simple configuration described in the Husky Usage documentation, using package.json as an example:

// package.json
"husky": {
    "hooks": {
      "pre-commit": "eslint"}},Copy the code

When NPM installs Husky, Husky creates all supported hooks in the.git/hooks folder of the project, as well as the husky.local.sh and husky.sh files. Every hook script has the same content:

# pre-commit
#! /bin/sh
# husky

# Created by Husky v4.2.5 (https://github.com/typicode/husky#readme)
#At: 2020/8/3 11:25:21 am
#From: ... (https://github.com/typicode/husky#readme)

. "$(dirname "$0")/husky.sh"
Copy the code

We can see just executing the husky.sh script, with emphasis on the husky.sh script.

Principle of Husky install hooks

How does Husky create files in hooks after installation? In fact, NPM scripts uses the install command. When the NPM package is installed, the scripts under this command will be automatically executed. For details, please refer to the documentation. By looking at Husky’s package.json, you can see that the instructions are:

node husky install

/lib/installer/bin is the script for which the hooks are created./lib/installer/hooks.

Husky core source code interpretation

Husky’s core code is in the husky.sh file:

# # Created by Husky v4.2.5 (https://github.com/typicode/husky#readme) At: 2020/8/3 11:25:21 # From: in the morning... (https://github.com/typicode/husky#readme) debug () { if [ "$HUSKY_DEBUG" = "true" ] || [ "$HUSKY_DEBUG" = "1" ]; then echo "husky:debug $1" fi } command_exists () { command -v "$1" >/dev/null 2>&1 } run_command () { if command_exists "$1". then "$@" husky-run $hookName "$gitParams" exitCode="$?" debug "$* husky-run exited with $exitCode exit code" if [ $exitCode -eq 127 ]; then echo "Can't find Husky, skipping $hookName hook" echo "You can reinstall it using 'npm install husky --save-dev' or delete this hook" else exit $exitCode fi else echo "Can't find $1 in PATH: $PATH" echo "Skipping $hookName hook" exit 0 fi } hookIsDefined () { grep -qs $hookName \ package.json \ .huskyrc \ .huskyrc.json \.huskyrc.yaml \.huskyrc.yml} huskyVersion="4.2.5" gitParams="$*" hookName="$(basename "$0)" debug "husky v$huskyVersion - $hookName" # Skip if HUSKY_SKIP_HOOKS is set if [ "$HUSKY_SKIP_HOOKS" = "true" ] || [ "$HUSKY_SKIP_HOOKS" = "1" ]; then debug "HUSKY_SKIP_HOOKS is set to $HUSKY_SKIP_HOOKS, skipping hook" exit 0 fi # Source user var and change directory . "$(dirname "$0")/husky.local.sh" debug "Current working directory is $(pwd)" # Skip fast if hookName is not defined # Don't skip if .huskyrc.js or .huskyrc.config.js are used as the heuristic could # fail due to the dynamic aspect of JS. For example: # `"pre-" + "commit"` or `require('./config/hooks')`) if [ ! -f .huskyrc.js ] && [ ! -f husky.config.js ] && ! hookIsDefined; then debug "$hookName config not found, skipping hook" exit 0 fi # Source user ~/.huskyrc if [ -f ~/.huskyrc ]; then debug "source ~/.huskyrc" . ~/.huskyrc fi # Set HUSKY_GIT_STDIN from stdin case $hookName in "pre-push"|"post-rewrite") export HUSKY_GIT_STDIN="$(cat)";; esac # Windows 10, Git Bash and Yarn 1 installer if command_exists winpty && test -t 1; then exec < /dev/tty fi # Run husky-run with the package manager used to install Husky case $packageManager in "npm") run_command npx --no-install;; "npminstall") run_command npx --no-install;; "pnpm") run_command pnpx --no-install;; "yarn") run_command yarn run --silent;; *) echo "Unknown package manager: $packageManager"; exit 0;; esacCopy the code

Let’s extract a few key points for analysis:

The first key is to get the name of the current script by basename “$0”, for example: pre-commit. This is important because all instructions match around this name and hookName is pre-commit

hookName="$(basename "$0")"
Copy the code

HookIsDefined function, which uses grep command to determine whether there is pre-commit in each configuration file.

hookIsDefined () {
  grep -qs $hookName \
    package.json \
    .huskyrc \
    .huskyrc.json \
    .huskyrc.yaml \
    .huskyrc.yml
}
Copy the code

The third key is the run_command function, which is used to execute hooks using the local husky-run directive, as configured at the beginning of this article, to execute ESLint.

npx --no-install husky-run pre-commit "$gitParams"
Copy the code

Husky – is corresponding executing scripts run. Node_modules/husky/bin/run. The js, script content is also very simple, is the call. Node_modules/husky/lib/runner/bin. Js, And eventually is called the node_modules/husky/lib/runner/index of js runCommand interface, the interface of the sub process execution in the pre – commit the corresponding script.

Summary of the Husky principle

With Husky’s principles behind us, let’s conclude:

  1. Create hooks at install time
  2. Commit from the configuration file (Package. json,.huskyrc,.huskyrc.json...) read the correspondinghookconfiguration
  3. Execute instructions/scripts in the configuration