上京エンジニアの葛藤

都会に染まる日々

husky コードリーディングメモ

今まで雰囲気で TypeScript (node.js) を書き続けていたので OSS のコードを読みながら少し真面目に勉強しようと思いました。

早速 husky のコードリーディングをしたので、記録としてメモを残します。 github.com

husky は Git Hooks を簡単に設定できるようなツールで、CLI ツールなのでボリュームも少なくて読みやすかったです。

コードリーディング

エントリーポイント

try {
  // Run command or show usage for unknown command
  cmds[cmd] ? cmds[cmd]() : help(0)
} catch (e) {
  console.error(e instanceof Error ? `husky - ${e.message}` : e)
  process.exit(1)
}

https://github.com/typicode/husky/blob/840c4164d06d3c412caf63a9c9d7ca1af15f2165/src/bin.ts#L36-L42

cmds には実行可能なコマンドが定義されている
引数の cmd が存在していれば対象の関数が実行されるような仕組み
存在しなければ help コマンドが実行され、引数の 0 は process.exit(0) で正常終了させるために渡される


function help(code: number) {
  console.log(`Usage:
  husky install [dir] (default: .husky)
  husky uninstall
  husky set|add <file> [cmd]`)
  process.exit(code)
}

https://github.com/typicode/husky/blob/840c4164d06d3c412caf63a9c9d7ca1af15f2165/src/bin.ts#L5-L12

cmds には install, uninstall, set, add, -v というコマンドが用意されている
cmd は process.argv で受け取れる
nodejs.org


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),
}

https://github.com/typicode/husky/blob/840c4164d06d3c412caf63a9c9d7ca1af15f2165/src/bin.ts#L25-L33

-v の場合 package.json に書かれている version を参照するような工夫がされている

husky install

const git = (args: string[]): cp.SpawnSyncReturns<Buffer> =>
  cp.spawnSync('git', args, { stdio: 'inherit' })

https://github.com/typicode/husky/blob/840c4164d06d3c412caf63a9c9d7ca1af15f2165/src/index.ts#L9-L10

git が入っているかチェック
子プロセスを生成して git rev-parse を実行する
rev-parse なのは特に意味はなさそう

nodejs.org


  if (!p.resolve(process.cwd(), dir).startsWith(process.cwd())) {
    throw new Error(`.. not allowed (see ${url})`)
  }

https://github.com/typicode/husky/blob/840c4164d06d3c412caf63a9c9d7ca1af15f2165/src/index.ts#L29-L31

husky は原則 .git と同じ階層に入れる必要がある
現在の dir に入れれるかチェック

別の project に husky を入れる場合はこんな感じでできる https://typicode.github.io/husky/#/?id=custom-directory


  if (!fs.existsSync('.git')) {
    throw new Error(`.git can't be found (see ${url})`)
  }

https://github.com/typicode/husky/blob/840c4164d06d3c412caf63a9c9d7ca1af15f2165/src/index.ts#L34-L36

.git がカレントディレクトリに存在しているかチェック


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

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

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

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

https://github.com/typicode/husky/blob/840c4164d06d3c412caf63a9c9d7ca1af15f2165/src/index.ts#L34-L36

最後に .husky/ に shell をコピーして、git の core.hooksPath に .husky を設定
.husky/ 以下には commit-msg, pre-commit が hook と用意されて実行されるようになる

以上です。