利用husky實現前端專案自定義規範校驗

LnEoi發表於2021-11-02

提交Bug修復時容易忘記同步調整版本號,如果遺漏了很容易造成漏更新等問題,所以打算將版本號修改做成硬性的檢測規範。

實現效果跟ESlint的檢測差不多,Git提交後自動執行程式碼檢測,不符合規範就報錯撤回提交,符合規範就通過提交。

分析

主要要解決以下幾個問題:

  • 觸發檢測的方式

    既然想到ESlint,那第一個念頭是給ESlint增加自定義外掛。但仔細又想了想,因為檢測的是非JavaScript檔案,而且也不是程式碼那種邏輯檢測,只是在提交前做一下相應的檔案是否有修改,實際上並不是很適合的場景。

    最適合的還是直接用Git的鉤子,ESlint就是利用husky在相關鉤子中呼叫檢測。

    之前寫了篇husky7 + commitlint + lint-staged 記錄,所以流程比較熟悉,只需要在.husky資料夾下新增相關鉤子名稱的檔案,然後直接寫shell指令碼就能出發。

    單純的指令碼並不利於維護,所以打算跟ESlint一樣,寫成命令列式的,也方便後期增加功能。

  • 怎樣知曉檔案變動

    這個就是Git自帶的功能了,只需要利用git diff,對比package.json檔案,就會輸出對比資訊。

    但文字的不好分析,一下子又沒找到相關可用的外掛,於是用peggy自己寫了個相關的解析器,將文字轉換成方便解析的json資料。

  • 提交失敗後終止提交

    shell指令碼成功返回0、失敗返回非0Node.js提供了相關方法process.exit(1)

    如果有不懂的方式實際上翻一翻lint-staged都能找到所需的答案,畢竟現成的例子在那裡。

  • 提供可以主動繞過提交的方式

    既然是加入鉤子中,不需要的時候肯定不能刪了鉤子再提交。那隻能在提交邏輯中做跳過檢測。

    最合適的是在提交訊息的body中攜帶指定的關鍵字,當程式識別到關鍵字就跳過檢測邏輯即可。

關鍵邏輯

提供命令列呼叫命令

建立一個空專案,新增bin欄位,為我們的程式增加相關的呼叫命令

"bin":{
    "check": "./dist/index.js"
}

對應的check是命令列的呼叫名稱,對應的值是要執行的JavaScript檔案:

#!/usr/bin/env node
console.log('hello')

這樣就能列印出hello了。

當然這樣還不夠,命令列程式還可能攜帶引數,或是其他功能,所以可以配合Commander.js輔助處理命令列命令。

由於和業務程式碼寫在一起,只能提供大概的示例:

import { Command } from "commander";

const program = new Command();
program
    .command("change")
    .option(
      "--commitMsgPath <filePath>",
      "當 commit message 中包含指定字串時跳過當前命令檢測"
    )
    .description("檢測版本號是否有變動")
    .action(async ({ commitMsgPath }: { commitMsgPath: string }) => {
      try {
        let msg: string[];
        if (commitMsgPath) msg = readGitMessage(commitMsgPath);

        const flag = await checkVersion(msg);
        if (!flag) throw new Error("請修改版本號再提交");
      } catch (e) {
        console.error(`${DATA.NAME}: ${e.name} ${e.message}`);
        process.exit(1);
      }
    });
    
program.parse(process.argv);

呼叫git diff判斷資料

執行git命令可以使用simple-git這個庫,功能很豐富,但可惜diff的返回資料是純文字,並不方便處理。利用之前寫好的簡單的diff格式的解析器,處理成可讀的json資料。

剩下的就很簡單了,將返回的diff資料傳入解析器,然後判斷解析器中是否有相應的關鍵字version,有即代表版號是有修改過的,有需要可以做更細緻的版本號校驗。

import simpleGit, { SimpleGit } from "simple-git";
const GitDiffParser = require("../lib/gitDiffParser");

/**
 * 檢測版本號是否變動
 * @param msg
 */
export const checkVersion = async (msg?: string[]) => {
  let flag = false;

  const git: SimpleGit = simpleGit({
    baseDir: process.cwd(),
    binary: "git",
  });

  // 檢測 git message 中是否有指定文字 有則跳過版本檢測
  if (msg && msg.some((item) => item === DATA.SKIP_MESSAGE_KEY)) return true;

  // 判斷版本號是否有變化
  const diff = await git.diff(["--cached", "package.json"]);
  if (diff) {
    const result = <GitDiffParserResult>(
      GitDiffParser.parse(diff, { REMOVE_INDENT: true })
    );

    result.change.forEach((item) => {
      item.content.forEach((content) => {
        if (content.type === "+" && content.text.includes(`"version"`))
          flag = true;
      });
    });
    return flag;
  }
  return flag;
};

檢測message提供跳過檢測的方式

本來是寫在pre-commit鉤子中的,但查了幾個獲取訊息的git命令,都沒辦法獲取到當前的message

所以只能換到commit-msg鉤子中,通過$1變數來獲取到訊息。而且傳回的並不是訊息的文字,而是訊息的檔案路徑,所以需要自行讀取檔案內容。

/**
 * 讀取git message訊息檔案
 */
export const readGitMessage = (filePath: string) => {
  try {
    const msg: string = fs.readFileSync(
      path.join(process.cwd(), filePath),
      "utf-8"
    );
    return msg.split(/\s/).filter((item) => item);
  } catch (e) {
    return undefined;
  }
};

這樣在checkVersion流程中呼叫,跟預定的key對比,如果相同就直接返回true,這樣就跳過了版本檢測邏輯了。

commit-msg指令碼

新增進專案,就能直接通過npx調起命令,執行檢測邏輯。而訊息地址作為引數傳入命令中(之前的.option("--commitMsgPath <filePath>", "當 commit message 中包含指定字串時跳過當前命令檢測")配置的引數)。

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx commitlint --edit "$1"
npx check change --commitMsgPath "$1"

相關文章