今まで雰囲気で 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 なのは特に意味はなさそう
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 と用意されて実行されるようになる
以上です。