﻿# 設定 Angular 專案使用 husky 簡化 Git hooks 設定並用 Prettier 統一風格

我們在團隊中開發 Angular 應用程式，經常需要同步每個成員的程式碼格式，與其訂定 Coding Style (代碼風格)，倒不如直接用工具**強制**所有成員用一致的風格進行程式碼排版。本篇文章我將示範用 [husky](https://github.com/typicode/husky) 搭配 [prettier](https://prettier.io/) 來設定 Git 的 `pre-commit` hook，讓每個人在 `git commit` 之前就自動將變更的程式碼進行排版。

### 介紹 `npm set-script` 命令

我在安裝 [husky](https://github.com/typicode/husky) 的時候，看到一個很方便的 `npm set-script` 命令，他可以透過 `npm` 命令，使用命令列執行的方式幫你自動設定 `package.json` 的 `scripts` 區段內容，自動設定執行腳本內容。

不過我卻發現在我的電腦不能執行 `npm set-script` 命令，查詢 [npm-set-script](https://docs.npmjs.com/cli/v7/commands/npm-set-script) 官網文件後發現這是 npm 7.x 才有的全新功能。由於我的電腦安裝的是 [Node.js 14.17.5 LTS](https://nodejs.org/en/) 版本，內建的 npm 是 `6.14.14` 版，所以不支援這個好用命令。

在 Windows 要升級 npm 必須**以管理者身份執行**以下 Windows PowerShell 命令才能順利升級到最新版：

```powershell
Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force
npm install -g npm-windows-upgrade
npm-windows-upgrade -p -v latest
```

```
C:\>npm --version
7.21.0
```

如果是 Linux 或 macOS 的話，很簡單的透過以下命令就可以升級完畢：

```bash
npm install –g npm@latest
```

> 注意: 只有在第一次設定 husky 的時候才有可能需要升級 npm 版本，就算你不升級版本，手動調整 `package.json` 也是可以的，不一定要用 `npm set-script` 來調整 `package.json` 的內容。對於團隊成員來說，其實 npm 用什麼版本都沒差！

這個段落純粹想記錄一下在 Windows 環境如何升級 npm 版本，但 Angular CLI v12 目前還不支援 npm v7 的版本，所以要降版請用以下命令：

```powershell
npm-windows-upgrade -p -v 6.14.15
```

### 為團隊設定 Coding Style (代碼風格) 自動化設定

這邊我直接以 Angular 12 專案為範例進行實作，講述完整的安裝與設定過程。

1.  先透過 Angular CLI 12.2.2 建立一個全新專案：
    
    ```bash
    npm i -g @angular/cli
    ```
    
    ```bash
    ng new demo1 --routing --style=css
    cd demo1
    ```
    
2.  安裝 [husky](https://github.com/typicode/husky) 與 [pretty-quick](https://github.com/azz/pretty-quick) 套件
    
    ```bash
    npm install --save-dev husky prettier pretty-quick
    ```
    
    > 注意: 雖然 [pretty-quick](https://github.com/azz/pretty-quick) 套件本身就包含了 [prettier](https://prettier.io/) 套件在內，但是 [prettier](https://prettier.io/) 被註冊在 `peerDependencies` 裡面，使用 `npm` 會全自動一併安裝起來，但使用 `yarn` 的話會跳出警告訊息，必須手動額外安裝。所以我上述命令才連同 `prettier` 一併安裝！
    
    ```log
    [3/4] Linking dependencies...
    warning " > pretty-quick@3.1.1" has unmet peer dependency "prettier@>=2.0.0".
    ```
    
3.  設定 npm 的 `prepare` 生命週期腳本並執行 husky 安裝
    
    你可以透過 `npm pkg set scripts.prepare` 命令方便的將 `husky install` 自動註冊進 npm 的 `prepare` 生命週期階段([Life Cycle Scripts](https://docs.npmjs.com/cli/v7/using-npm/scripts#life-cycle-operation-order))。然後 `npm run prepare` 可以直接執行 `husky install` 命令，這個動作會自動幫你建立一個 `.husky` 資料夾：
    
    ```bash
    npm pkg set scripts.prepare="husky install"
    npm run prepare
    ```
    
    > 預設 `husky install` 產生的所有檔案都不會加入 Git 版控，你額外新增的 Hooks 檔案才需要加入版控。
    
    特別值得一提的地方，是這個 `prepare` 會在執行 `npm install` 之後自動執行 (必須在沒有附帶任何其他參數下執行 `npm install` 才行)。如果你寫的專案是會發布到 npm registry 的話，`prepare` 會在 `npm publish` 與 `npm pack` 之前自動執行。詳細的執行生命週期請見 [Life Cycle Operation Order](https://docs.npmjs.com/cli/v7/using-npm/scripts#life-cycle-operation-order) 文件。
    
    > 請不要將 `husky install` 註冊在 `postinstall` 生命週期階段！([Typicode's blog - Why husky doesn't autoinstall anymore](https://blog.typicode.com/posts/husky-git-hooks-autoinstall/))
    
4.  設定 husky 的 `pre-commit` Hook
    
    這裡我們設定 `pre-commit` Hook，讓團隊成員在執行 `git commit` 之前，就會自動透過 [pretty-quick](https://github.com/azz/pretty-quick) 的 `pretty-quick --staged` 命令對 **Git 暫存的檔案** (Staged files) 進行程式碼編排，而且是參照 `.prettierrc.json`, `.prettierignore` 與 `.editorconfig` 的設定進行，確保團隊的程式撰寫風格一致。以下命令會產生一個 `.husky/pre-commit` 檔案，這個檔案需要加入到 Git 版控之中。
    
    ```bash
    npx husky set .husky/pre-commit "npx pretty-quick --staged"
    ```
    
    這個 `npx pretty-quick --staged` 命令，主要做兩件事：
    
    1.  找出目前 Git 版控中，已經被加入到 Staged 狀態的檔案清單
    2.  將這些工作目錄中的檔案透過 Prettier 排版後，直接加入到 Git 的 Staged 狀態中
    
    簡單來說，他會在無形之中，默默的將你的程式碼排版成 Prettier 想要排版的樣子。你想要用自己的方式排版怎麼辦呢？沒錯！Prettier 就是這麼霸道，也沒什麼好商量的，整個團隊都用同一套規則集排版程式碼就對了，如此一來，你就不要再花時間想程式碼風格這件事情了！
    
5.  加入 `.prettierignore` 檔案 (不要透過 Prettier 排版的檔案清單)
    
    我基本上習慣將專案根目錄下的 `package.json` 與 `package-lock.json` 加入到 `.prettierignore` 檔案中，畢竟這些檔案都是透過 `npm` 工具在更新的居多，沒必要特別排版過。除此之外，我也會把 Visual Studio Code 的設定檔 (`.vscode/*`) 也都自動排除在 Prettier 掃描的清單中。最後，Angular 建置後的檔案通常也不用自動格式化。
    
    ```
    # IDE/Editor
    .vscode
    .idea
    
    # Package Manager
    package.json
    package-lock.json
    yarn.lock
    
    # TypeScript-related
    tsconfig.json
    tsconfig.app.json
    tsconfig.spec.json
    
    # Angular-related
    **/assets/**/*
    
    # Compiled output
    /dist
    /tmp
    /out-tsc
    /bazel-out
    
    # Miscellaneous
    /.angular/cache
    .sass-cache/
    /connect.lock
    /coverage
    /libpeerconnection.log
    testem.log
    /typings
    ```
    
6.  加入 `.prettierrc.json` 檔案
    
    其實 Prettier 可以設定的[選項](https://prettier.io/docs/en/options.html)並不多，但有些地雷。其中初學者最容易遇到的問題，就是 `.prettierrc.json` 與 `.editorconfig` 設定不一致的情況。在編輯器中有兩種不同的風格，絕對會遇到許多問題，所以請務必設定正確！
    
    你要是從網路上找的許多的 `.prettierrc` 範例，都會叫你設定 [`tabWidth`](https://prettier.io/docs/en/options.html#tab-width) 與 [`useTabs`](https://prettier.io/docs/en/options.html#tabs) 這兩項。但是這兩個設定會跟 `.editorconfig` 中的 `indent_size` 與 `indent_style` 完全重複，如果你剛好兩邊的設定不相同，就可能會出現非常詭異的情況，有時候甚至覺得會不知所措，也就是俗稱「鬼打牆」的情況。所以我一般都會特別看過兩邊的設定是否一致，這點真的非常重要！
    
    ```json
    {
      "tabWidth": 2,
      "useTabs": false,
      "printWidth": 100,
      "bracketSpacing": true,
      "singleQuote": true,
      "trailingComma": "es5",
      "semi": true,
      "overrides": [
        {
          "files": [
            "*.json",
            ".babelrc"
          ],
          "options": {
            "parser": "json-stringify"
          }
        },
        {
          "files": [
            "*.jsonc",
            "tsconfig*.json"
          ],
          "options": {
            "parser": "json"
          }
        },
        {
          "files": [
            "*.js",
            "*.cjs",
            "*.mjs"
          ],
          "options": {
            "parser": "babel"
          }
        },
        {
          "files": [
            "*.ts"
          ],
          "options": {
            "parser": "typescript"
          }
        }
      ]
    }
    ```
    
    > Angular CLI 透過 `ng new` 產生的專案範本，其 `.editorconfig` 檔案中的 `indent_size` 設定值預設為 `2`！
    
    > 請注意: 修改 `.editorconfig` 檔案設定之後，編輯器不一定會立刻生效 (通常會)，當你遇到靈異事件的時候，請記得「重開治百病」這句至理名言，將編輯器重開即可！
    
7.  調整 Visual Studio Code 的工作區設定
    
    因為 Visual Studio Code 除了會參考 `.editorconfig` 編輯器設定外，還有不同程式語言就會有不同的 Formatter 擴充套件，而且不同 Formatter 的格式化差異可能很大，VSCode 本來就內建許多語言的 Formatter，但使用者還可以自行加裝不同的 Formatter 擴充套件，所以開發者的 Formatter 可能會非常混亂！
    
    為了要讓團隊的開發風格趨於一致，也希望團隊成員不要花任何心思在排版上，所以 Visual Studio Code 的設定其實是非常重要的。
    
    我們可以在專案目錄下建立 `.vscode/settings.json` 檔案，讓所有人在「儲存」的時候就自動用我們指定的 Prettier Formatter 排版好。不過，就算使用者沒有設定，由於我們也設定了 `pre-commit` hook，所以最終在 `git commit` 的時候，程式碼還是會自動格式化的！但我們真正希望的是，不要有太多的意外，不要有太多的靈異事件，最好我們在編輯器中看到的程式碼，就是最終 Prettier 幫我格式化過後的格式！
    
    請設定以下 `.vscode/settings.json` 檔案內容：
    
    ```json
    {
      "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[html]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[json]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[jsonc]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "editor.formatOnSave": true
    }
    ```
    
    再建立一個 `.vscode/extensions.json` 檔案，建議成員一定要安裝 [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 與 [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 擴充套件：
    
    ```json
    {
        "recommendations": [
            "angular.ng-template",
            "EditorConfig.EditorConfig",
            "esbenp.prettier-vscode"
        ]
    }
    ```
    
8.  將修改加入 Git 版控
    
    走到這一步已經全部設定完成，所以從現在開始 `git commit` 就會開始套用 Prettier 的設定，並自動將**已加入 Git Staged 的原始碼**進行格式化並加入版控。
    
    ```bash
    git commit
    ```
    
    不過，若你想讓特定版本跳過 Git 的 `pre-commit` hook 的話，可以考慮用以下命令直接將現有版本加入版控：
    
    ```bash
    git commit --no-verify
    ```
    

### 檢查所有原始碼的排版後風格

在正式執行 `git commit` 自動排版之前，我們可以透過以下命令，先手動執行看看感覺，驗證一下排版過的格式是否滿意。

1.  使用 Prettier 對**整個專案所有原始碼**進行檢查
    
    只要有出現 `[warn]` 字樣，就代表該檔案會被格式化，但不會真的寫入：
    
    ```bash
    npx prettier --check .
    ```
    
2.  使用 Prettier 對**整個專案所有原始碼**進行格式化
    
    底下命令則會將原始碼格式化之後寫入檔案，你就可以看出這些檔案的排版風格：
    
    ```bash
    npx prettier --write .
    ```
    
    > 我會建議啟用 husky + pretty-quick 之後的第一個 Git 版本，可以用 `npx prettier --write .` 先進行一次格式排版，讓團隊都基於相同的基礎下進行版控，如此一來可以減少日後版本追蹤上的困擾。
    
    > 但是如果你的程式碼很多，團隊也不小，專案也尚未進入穩定發展的狀態，這代表團隊成員還在積極開發中，這樣我就會建議可以跳過這個步驟，否則突然的變動可能會造成數千支檔案同時調整過，這樣不但會增加衝突發生的機率，同時也難以透過 `git diff` 比對**排版之前**與**排版之後**的版本變更。
    
3.  使用 pretty-quick 對**已加入 Git Staged 的原始碼**進行檢查
    
    如果透過以下命令檢查出有檔案尚未符合格式要求，該命令的 Exit Code 為 `1`，這很適合在 CI 階段的時候檢查格式是否符合標準，也可以查出有成員沒有正確設定 husky + pretty-quick 等工具。
    
    ```bash
    npx pretty-quick --check
    ```
    
4.  使用 pretty-quick 對**已加入 Git Staged 的原始碼**進行格式化
    
    以下命令則會直接對檔案進行格式化，並自動寫入格式化後的版本，但不會自動幫你 `git add` 加入暫存的變更 (Staged)
    
    ```bash
    npx pretty-quick --bail
    ```
    

### 後記

要搞清楚 [Visual Studio Code](https://code.visualstudio.com/) 的 Formatter, [EditorConfig](https://editorconfig.org/), [Prettier](https://prettier.io/), [pretty-quick](https://github.com/azz/pretty-quick), [husky](https://github.com/typicode/husky), [Git Hooks](https://git-scm.com/book/zh-tw/v2/Customizing-Git-Git-Hooks) 彼此之間的連動關係，除了要經年累月的背景知識加持，還要對每套工具的正確理解，才有辦法真正釐清整個來龍去脈。另一方面，這些不同工具之間都有各種不同的使用情境，如果再加上工具新舊版本的差異，可以想見網路上可以找到的文章非常的混亂，可能以前適用的設定，到今日就不適合了。所以看別人的部落格文章時，絕對不能照抄設定，一定要理解消化之後才能正確使用。

我之前有很長一段時間覺得 Prettier 很難用，而且跟 Visual Studio Code 內建的格式化風格差別很大，常常在 VSCode 格式化之後，透過 Prettier 又格式化成另一種格式，覺得非常麻煩。今天整理這篇文章，算是完整解決了我多年來的困擾，非常開心啊！ 😄

現在我們公司成長到快 50 人了，團隊的開發效率變成一件非常重要的事情，你很難想像我花多少時間整理這些資訊，但也唯有這樣的研究，才能讓團隊成員更輕鬆無腦的參與團隊協作，所以花這些時間我覺得蠻值得的！🙂

### 相關連結

-   npm
    -   [node.js - How can I update npm on Windows? - Stack Overflow](https://stackoverflow.com/a/31520672/910074)
    -   [scripts | npm Docs](https://docs.npmjs.com/cli/v7/using-npm/scripts)
-   Tools
    -   [husky - Modern native Git hooks made easy 🐶 woof!](https://github.com/typicode/husky)
    -   [Prettier · Opinionated Code Formatter](https://prettier.io/)
        -   [Options · Prettier](https://prettier.io/docs/en/options.html#parser)
        -   [Pre-commit Hook · Prettier](https://prettier.io/docs/en/precommit.html)
        -   [Ignoring Code · Prettier](https://prettier.io/docs/en/ignore.html)
    -   [azz/pretty-quick: ⚡ Get Pretty Quick](https://github.com/azz/pretty-quick)
-   Blogs
    -   [Ultimate Angular + Prettier Cheatsheet | by Aaron Frost | ngconf | Medium](https://medium.com/ngconf/ultimate-prettier-angular-cheatsheet-777c9515f4fb)
    -   [How To Format Code with Prettier in Visual Studio Code | DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-format-code-with-prettier-in-visual-studio-code)
    -   [How To Automate Code Tasks With Git Hooks 🔥 - DEV Community](https://dev.to/nikosanif/awesome-dev-tools-to-automate-code-tasks-47ko)
    -   [Typicode's blog - Why husky doesn't autoinstall anymore](https://blog.typicode.com/posts/husky-git-hooks-autoinstall/)
