前端專案通常都會有 Webpack、ESLint、Prettier、Browserslist 等設定,當專案開始大量增加時,這些重複的設定會散落在各個專案,導致後續維護和修改的困難。這時我們可以開始把共用的專案設定抽至一個 common library 並發布成 npm package。有了統一的設定,其他專案只需要安裝這些 npm package 並且在需要時更新 package.json
即可。
.eslintrc
, .prettier
, .browserslistrc
都有提供 extends 的使用方式,簡單來說就是可以直套用別人寫好的 rules,缺少的、不符合需求的 rules 再自己補上或是 override。以下用 ESLint 舉例:
{
// extends 官方推薦的規則,讓你不必每條規則逐一設定
"extends": ["eslint:recommended"],
"rules": {
// 假設我認為 debug 用的 console 忘記拿掉的人都該死,我就加這條規則。
"no-console": "error",
// 假設我認為官方推薦的規則太嚴格了,那我就可以 override 這條規則把檢查關掉。
"no-sparse-arrays": "off"
}
}
我們知道了 extends 的使用方法後,接著我們就要建造自己的 shareable config 並且發布至 npm。首先必須至 npm 註冊用戶,並在本機 command line 執行 npm adduser
進行登入,這樣才能確保後續能夠透過 npm publish
發布套件。接著我們在 npm 上建立自己的 organization,這麼做的好處很明顯:可以利用 scope 來避免套件名稱已經被使用的問題。以下假設我們成功建立了名為 my-org
的 organization。
建立一個名叫 eslint-config
的資料夾,並在資料夾內執行 npm init --scope=@my-org
初始化 npm 專案來產生 package.json
。接著我們安裝相關的 dependencies:
npm install eslint eslint-plugin-import eslint-plugin-react eslint-config-prettier --save
由於可能不同的專案類型可能會需要不同的 ESLint 設定,所以我們在創建 lib/
並在其中建立兩份設定:vanilla.js
、react.js
,一份是純 js 專案的設定,一份 React 專案的設定。以下為 vanilla.js
的範例:
module.exports = {
env: { node: true, es2021: true },
extends: ['eslint:recommended', 'prettier'],
plugins: ['import'],
rules: {
'import/order': 'error',
'no-var': 'error',
quotes: ['error', 'single', { avoidEscape: true }]
}
}
接著在 package.json
中設定 main
作為套件的入口,files
為被打包進套件,可供外界使用的檔案:
{
"main": "lib/vanilla.js",
"files": [
"/lib"
]
}
都設定好了之後,我們就可以開始體驗一下如何使用這個套件。在專案根目錄執行 node
進入交互模式:
> const vanillaEslintConfig = require('@my-org/eslint-config')
> console.log(vanillaEslintConfig) // show my vanilla eslint config
> const reactEslintConfig = require('@my-org/eslint-config/lib/react')
> console.log(reactEslintConfig) // show my react eslint config
暸解使用方式後,在專案的根目錄加上 .eslintrc
設定:
{
// 自己吃自己,自己訂定的規則自己也必須遵守。
"extends": "@my-org/eslint-config"
}
到這一步驟,我們就可以執行 npx eslint lib/**/*.js
,'用自己訂下的 ESLint 規則來 lint 專案本身。
各套件 shareable config 詳細設定方式參考以下連結,不再贅述。
接著我們要 publish 至 npm,讓其他前端專案也能夠使用這份設定。npm 的套件建議遵守 Semantic Version。如果套件在初期開發不穩定的情況下,可以從 0.1.0
版本開始,和過去 API 不相容的修改跳 Minor 版號,其餘跳 Patch 版號。確定好版本後就可以發布至 NPM:
npm publish --access public
等到成功發布後就可以在其他專案中使用:
npm install @my-org/eslint-config --save-dev
設定 .eslintrc
{
"extends": "@my-org/eslint-config"
}
在開發初期,如果需要在其他專案下引入這份設定一邊同時進行調整的話,那麼每次修改完 @my-org/eslint-config
都需要發布至 npm 後再 install 下來也太沒效率了。這個時候就會需要借助 npm link 的力量了。
在 @my-org/eslint-config
專案底下執行 npm link
產生 global link (其實就是 symlink),接著執行 npm ls -g --depth=0 --link=true
來查看所有 global link:
/Users/galtz/.nvm/versions/node/v16.4.2/lib
└── @my-org/eslint-config@0.1.0 -> ./../../../../../Developer/eslint-config
在我們需要引入 eslint-config
進行測試的專案執行 npm link @my-org/eslint-config
建立連結,設定完後我們就能直接修改 @my-org/eslint-config
的內容,會即時反應修改的結果,以測試的角度來說相當方便。
除了上述的 ESLint,Browserslist、Prettier 也需要抽出 sharing config。如果全部都各自建一個 repo 的會又會讓這些設定分散各處,一切又變得雜亂起來,根本自己搞自己。這個時候我們可以使用 monorepo 的方式,即是在單一 repo 中,管理多個模組。
Lerna 就是用來管理這些小王八蛋的,透過指令協助開發者對多個模組進行管理。使用方式很簡單,在一個新資料夾底下執行 lerna init --independent
。會產生 package.json
, lerna,json
以及 packages/
:就是這些小王八蛋的住所。接下來就是把各模組放到裏面,架構會長得像是下面這樣:
lerna.json
package.json
packages/
├── eslint-config/
│ ├── lib/
│ └── package.json
├── prettier-config/
│ ├── index.js
│ └── package.json
└── browserslist-config/
├── index.js
└── package.json
需要特別在各模組下的 package.json
中加入以下設定,確保 scope 下的 package 為公開 publish。
{
"publishConfig": {
"access": "public"
}
}
這裡簡單紀錄幾個常用指令:
lerna init
: 初始 monorepo。lerna bootstrap
: 為所有子模組執行 npm install
。lerna clean
: 刪除所有子模組中的 node_modules
。lerna run <command>
: 執行所有子模組的 npm run <command>
。lerna version
: 為有修改過的子模組進版,並且自動 commit、為 commit 打上 git tag,最後 push 到 git remote。lerna publish
: 發布所有子模組至 npm。能自動化的事情就盡量自動化,開發者會希望能夠專注在開發上,publish 就交給自動化的流程去處理。Github Actions 可以協助我們在每次 push 程式碼到 Github 上後,自動幫我執行 build -> test -> lint -> publish 一系列動作。
在本機上必須先用帳號密碼登入 npm 才有權限 publish,那如何賦予 Github Actions 權限呢?方法是在 npm 上面產生一組 automation token ,接著在 Github repo 上的 settings -> secrets 設定 NPM_TOKEN
的值。有了這把 token 就能夠任意發布套件至你個人或 organization 底下,請務必小心保管。
註:npm access token 有三種,但只有 publish 權限且可繞過 2FA 的只有 automation token。
以下為 package.json
中的設定:
{
"ci:bootstrap": "npx lerna bootstrap --ci",
"ci:publish": "npx lerna publish from-package --yes --no-verify-access"
}
npm install
來安裝套件;但是發布出去的套件會用 npm ci 來確保 dependencies 完全依照 package-lock.json
中指定的版本進行安裝,所以明確指定 --ci
。--no-verify-access
繞過。詳細可以參考這串 issue。接著新增資料夾 .github/workflows
,並在其中新增 main.yml
:
name: Publish
on:
# 當有新的 change 進到 master 時觸發
push:
branches:
- master
# 讓你能夠手動觸發,方便測試
workflow_dispatch:
jobs:
publish:
# 運行在 ubuntu 上
runs-on: ubuntu-latest
steps:
# checkout 至 repo 的 master branch
- uses: actions/checkout@v2
# 設定 node 環境
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 16
registry-url: 'https://registry.npmjs.org'
# install & lint
- run: npm run ci:bootstrap
- run: npm run lint
# 利用 automation token publish 至 npm
- run: npm run ci:publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
到這裡就全部設定完畢了。接下來只需要:
npx lerna version
, Lerna 會為修改過的模組進版,接著 commit、上 git tag 後 push 至 Github remote。推薦閱讀:PJCHENder - 建立公司內部使用的 eslint-config package
針對 ESLint 中 extends、plugin、overrides 之間的愛恨糾葛描述的清晰明暸。