husky 源碼分析——這個庫到底做了什么?

husky 源碼分析

前言

Github:https://github.com/typicode/husky 使用 Git 鉤子變得簡單

在做前端工程化時 husky 可以說是一個必不可少的工具。husky 可以讓我們在項目中方便添加 git hooks。

這個庫的名字指的是 “哈士奇”,結合庫主要用在提交前發現問題、規范代碼的作用,應該是這個意思: ‘不好好規范你的代碼,你就像一個哈士奇一樣,會用代碼拆家的’ 。這個寓意跟另一個經常和 husky 搭配使用的庫 lint-staged 很像

lint-staged官網

Run linters against staged git files and don't let ?? slip into your code base!(大概意思:不要讓粑粑溜進你的代碼庫!)

這大概就是 ‘屎山’ 一詞的由來吧(我猜的)有個性!同時也說明了代碼規范的重要性!

今天我從源碼來探究下,這個 npm 庫到底做了什么!

該項目為 ts 項目,通過 tsconfig.json、package.json 可知,編譯入口為 src 出口為 lib 。項目執行主文件為 lib/index.js

主要文件為:src/index.ts、src/bin.ts、husky.sh

主文件 src/index.ts

import cp = require('child_process') // 子進程
import fs = require('fs') // 文件處理
import p = require('path') // 路徑方法

// 日志輸出
const l = (msg: string): void => console.log(`husky - ${msg}`)

// 通過 node 子進程拼接 git 參數并返回執行結果
const git = (args: string[]): cp.SpawnSyncReturns<Buffer> =>
  cp.spawnSync('git', args, { stdio: 'inherit' })

/**
 * install 方法可以看作初始化設置,在此方法中實現了以下功能
 * 1、輔助 Plumbing 底層命令,主要用于操作
 * 2、判斷安裝目錄是否為項目根目錄并創建 .husky 文件夾 
 * 3、判斷項目中是否存在 .git 文件。如果不存在 則不存在 githook 文件,也就 不存在替換 githook 的文件路徑的可能了
 * 4、
 *   [1] 在 .husky 文件下創建文件夾 '_'
 *   [2] 在 '_' 文件下寫入文件 .gitignore, 文件內容為 ‘*’, 忽略該目錄下所有文件的 git 提交
 *   [3] 復制 husky 項目根目錄下的 husky.sh 文件 到 目標項目的 '_' 目錄下,名稱不變
 *   [4] 執行 git 操作,修改 githook 的執行路徑為目標項目的 .husky 文件下
 *   [5] 執行成功 or 失敗后的 log 提示
*/
export function install(dir = '.husky'): void {
  // Ensure that we're inside a git repository
  // If git command is not found, status is null and we should return.
  // That's why status value needs to be checked explicitly.
  // ----1----
  if (git(['rev-parse']).status !== 0) {
    return
  }

  // Custom dir help
  const url = 'https://git.io/Jc3F9'

  // ----2----
  // Ensure that we're not trying to install outside of cwd
  if (!p.resolve(process.cwd(), dir).startsWith(process.cwd())) {
    throw new Error(`.. not allowed (see ${url})`)
  }

  // ----3----
  // Ensure that cwd is git top level
  if (!fs.existsSync('.git')) {
    throw new Error(`.git can't be found (see ${url})`)
  }

  // ----4----
  try {
    // Create .husky/_   ----4.1----
    fs.mkdirSync(p.join(dir, '_'), { recursive: true })

    // Create .husky/_/.gitignore   ----4.2----
    fs.writeFileSync(p.join(dir, '_/.gitignore'), '*')

    // Copy husky.sh to .husky/_/husky.sh  ----4.3----
    fs.copyFileSync(p.join(__dirname, '../husky.sh'), p.join(dir, '_/husky.sh')) 

    // Configure repo    ----4.4----
    const { error } = git(['config', 'core.hooksPath', dir])
    if (error) {
      throw error
    }
    // ----4.5----
  } catch (e) {
    l('Git hooks failed to install') 
    throw e
  }

  l('Git hooks installed')
}

/**
 * set: 創建指定的 githook 文件,并寫入文件內容
 * 1、如果文件目錄不存在, 中斷并提示執行 husky install 初始化配置
 * 2、寫入文件, 指定解釋器為 sh 執行 shell 腳本, cmd 動態參數,為開發者想要在這個 githook 階段執行的操作,一般為腳本 
 *    例:npm run lint
 * 3、創建成功的 log
*/
export function set(file: string, cmd: string): void {
  const dir = p.dirname(file)
  // ----1----
  if (!fs.existsSync(dir)) {
    throw new Error(
      `can't create hook, ${dir} directory doesn't exist (try running husky install)`,
    )
  }

  // ----2----
  fs.writeFileSync(
    file,
    `#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

${cmd}
`,
    { mode: 0o0755 },
  )

  // ----3----
  l(`created ${file}`)
}

/**
 * add: 在已有的 githook 文件中追加命令
 * 1、githook 文件是否存在
 * 2、存在:在原有文件基礎上追加新的內容
 * 3、不存在:執行 set 添加該 githook 文件
*/
export function add(file: string, cmd: string): void {
  // ----1----
  if (fs.existsSync(file)) {
    // ----2----
    fs.appendFileSync(file, `${cmd}\n`)
    l(`updated ${file}`)
  } else {
    // ----3----
    set(file, cmd)
  }
}

/**
 * uninstall: 卸載 install 中指定的 hooksPath 的路徑,恢復為 git 默認的 githook 路徑
*/
export function uninstall(): void {
  git(['config', '--unset', 'core.hooksPath'])
}

入口文件 src/bin.ts

用于接受命令行參數,觸發 src/index.ts 中的執行文件

#!/usr/bin/env node        // 指定使用 node 解析運行文件
import p = require('path') // 路徑方法
import h = require('./')   // src/index.ts 主文件方法

// 打印幫助命令,這里沒有使用 command 包,而是使用進程方法獲取參數,所以自己打印了幫助 log
// Show usage and exit with code
function help(code: number) {
  console.log(`Usage:
  husky install [dir] (default: .husky)
  husky uninstall
  husky set|add <file> [cmd]`)
  process.exit(code)
}

// Get CLI arguments     作者說了:獲取命令行參數
const [, , cmd, ...args] = process.argv
const ln = args.length
const [x, y] = args

// Set or add command in hook
// 處理需要參數的主文件的函數并執行, 對錯誤的參數進行了長度判斷
const hook = (fn: (a1: string, a2: string) => void) => (): void =>
  // Show usage if no arguments are provided or more than 2
  !ln || ln > 2 ? help(2) : fn(x, y)

// CLI commands
// 執行命令相對應的 src/index.ts 文件中的函數, 沒有參數的直接調用,需要參數的,套了一層 hook 函數,用于參數處理
const cmds: { [key: string]: () => void } = {
  install: (): void => (ln > 1 ? help(2) : h.install(x)),
  uninstall: h.uninstall, // 沒有參數直接調用
  set: hook(h.set),
  add: hook(h.add),
  ['-v']: () =>
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires
    console.log(require(p.join(__dirname, '../package.json')).version),
}

// Run CLI
try {
  // Run command or show usage for unknown command
  // 作者說了:命令存在則運行指定函數,不存在則打印幫助 log
  cmds[cmd] ? cmds[cmd]() : help(0)
} catch (e) {
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  console.error(e.message) // 打印 error 信息
  process.exit(1) // 報錯退出
}


shell 腳本文件 husky.sh

還記得在主文件的 set 方法中,寫入自定義 githook 方法時會默認寫入的兩行執行命令嘛?

#!/bin/sh   // 指定該文件使用 shell 解析執行

// 執行當前當前目錄下指定的文件,并執行該路徑下的 _/husky.sh 文件,也就是 主文件中 install 方法中復制到項目里的腳本文件
// $0 為 當前腳本名稱 調用 husky.sh 腳本
. "$(dirname "$0")/_/husky.sh" 
#!/bin/sh
// 判斷變量 husky_skip_init 的長度是否為 0
if [ -z "$husky_skip_init" ]; then
// 為 0 時, 創建 debug 函數, 用來打印報錯日志
  debug () {
    // HUSKY_DEBUG 為 “1” 時打印
    if [ "$HUSKY_DEBUG" = "1" ]; then
      // $1 表示參數
      echo "husky (debug) - $1"
    fi
  }

  // 聲明一個只讀參數, 內容為 basename + 文件名稱
  readonly hook_name="$(basename "$0")"
  debug "starting $hook_name..."

  // 判斷變量 HUSKY 是否 = “0”
  if [ "$HUSKY" = "0" ]; then
    debug "HUSKY env variable is set to 0, skipping hook"
    exit 0
  fi

  // 判斷 ~/.huskyrc 是否為普通文件
  if [ -f ~/.huskyrc ]; then
    debug "sourcing ~/.huskyrc"
    . ~/.huskyrc
  fi

  // 聲明只讀變量 
  export readonly husky_skip_init=1
  // 當前文件名 是否在 傳進來的參數中 存在則執行
  sh -e "$0" "$@"
  exitCode="$?"

  // 當 exitCode 不等于 0 時,打印當前執行的 hook 名稱 以及 退出碼
  if [ $exitCode != 0 ]; then
    echo "husky - $hook_name hook exited with code $exitCode (error)"
  fi

  exit $exitCode
fi

總結

該工具的最核心的代碼為

git config core.hooksPath 路徑   // 指定 githook觸發的路徑
git config --unset core.hooksPath // 卸載指定的路徑,恢復默認路徑

更改了 hook 執行的路徑,在該路徑下新建對應的 hook 的鉤子文件,該文件就會在指定的階段被觸發執行。

例:src/pre-commit 會在 git commit 時執行該文件的代碼

 #!/bin/sh

 npm run lint

該庫主要是將 git 命令進行了封裝、完善異常報錯機制,方便開發者使用。

作者自己打印了 cli 的幫助函數,可能是因為命令不多的原因吧,不過在寫 cli 的時候還是建議使用 command 庫,可以幫你處理很多事情,當然也會增加包的體積。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容