[note] 建立公司內部使用的 eslint-config package
在現今的 JavaScript 專案中,為了確保程式碼的品質和撰寫風格,ESLint 的設定和使用幾乎可以算是標配。
透過 ESLint 除了可以避免掉許多不必要的程式語法錯誤,還能確保開發者彼此之間有一致的程式撰寫風格(coding style),才不會 A 開發者寫出來的程式碼和 B 開發者寫出來的,在撰寫風格上有太大的落差。
為什麼要建立 OneDegree 自己的 eslint-config package
eslint-config package 指的就是把 ESLint 設定檔,打包成一個 npm 套件,這裡面包含了要套用那些規則、套用這些規則的邏輯等等,目的則是讓不同專案能夠直接套用。幾個大家可能聽過的像是 eslint-config-standard、eslint-config-airbnb、eslint-config-prettier、eslint-config-google、...等等,都是包好的 ESLint 設定檔,讓想要套用的開發者可以直接下載使用。
一般來說,每個專案都會有獨立的一隻 ESLint 設定檔,可以針對不同專案進行設定就好,不需要額外抽成一個套件。然而,在 OneDegree 中有許多不同的專案同時在進行,包含 B2B 和多個 B2C 專案,如果每次都要在各個專案中複製貼上相同的設定,會是相當麻煩的一件事;另外,如果在專案中有針對部分規則進行個別的微調,久了之後可能會忘了原始套用的設定是長什麼樣子,甚至發生不同的專案套用的設定差異過大的情況。
這時候如果可以把 ESLint 中的設定打包成一個套件,未來新開專案時只需要使用 npm 安裝這個套件後,就可以套用到公司內部一致的設定,將會省下非常多不必要的麻煩。
認識 ESLint 中的幾個設定欄位
實際上要建立 eslint-config 的套件並沒有太大的難度,因為本質上就是把寫好的 ESLint 設定進行匯出而已。比較需要釐清的反而是先了解和釐清 ESLint 設定檔中的幾個欄位。下面是一隻 ESLint 設定檔常見的欄位:
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['plugin:react/recommended', 'airbnb'],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: ['react'],
rules: {},
};
其中包含了:
-
env
:讓 ESLint 知道那些是原本就存在專案中的全域變數(global variable),例如,alert
、window
等等,否則當你在專案中直接使用了這些全域變數卻又沒有定義的話,ESLint 就會認為這個變數不存在而報錯。 -
parserOptions
:告訴 ESLint 專案中所使用的 JavaScript 語法版本(例如,ECMAScript6、ECMAScript7)。 -
rules
:透過這個欄位的設定,可以讓 ESLint 知道當不同的規則觸發時,ESLint 要用什麼類型的方式給予提示,是要當成是 error、warning 、或是不用理會。例如,在這裏可以設定,假如開發者在程式中定義了變數卻沒有使用到這個變數時,要當成是嚴重的錯誤(error),出現警告的提示(warning)就好,或是可以完全不用理會這個情況(off)。
除了這幾個欄位之外,比較容易混淆的則是 plugins 和 extends 這兩個欄位,因此會花多一些的篇幅來描述。
plugins:對應到的是一系列定義好的規則,但不包含如何使用這些規則
plugins
是一系列由其他開發者撰寫好的規則,讓使用的人可以把這些規則載入到 ESLint 中使用,但要留意的是:「它只是定義好的規則,並沒有說明要如何使用這些規則」,也就是說,plugin 只會載入規則,但不會說明這些規則發生時到底要被 ESLint 判斷成是 error、warning、還是可以不用理會,這個部分一樣是要在 rules
欄位中設定。
常見 eslint plugin 的套件像是 eslint-plugin-react、eslint-plugin-import、eslint-plugin-vue、eslint-plugin-prettier、eslint-plugin-jest、...等等。
以 eslint-plugin-react 為例,可以看到這裡定義好了一系列可以被 ESLint 使用的規則:
在這些 plugin 中都有定義了許多不同的規則可以載入到 ESLint 中,再由開發者自行針對這些規則,透過 rules
欄位來決定嚴重程度。
從這些 eslint plugin 的套件中可以看到,每個套件的前綴都是一樣的,都是以 eslint-plugin-*
當作開頭,因此當我們在 ESLint 的設定檔中,想要啟用這個 plugin 的規則時,可以省略掉這個前綴。
例如,在上面範例的 .eslintrc.js
中,可以看到 plugins 欄位只寫了 react
,而實際上它完整的套件名稱是 eslint-plugin-react
,eslint-plugin-* 的部分可以省略:
{
plugins: [
// eslint-plugin-react 的縮寫
'react',
],
}
當我們掛入 plugins 時,其實就只是把許多定義好的規則載入到 ESLint 中,但這些規則要怎麼被使用並不包括在 plugins 的範疇內,需要透過 rules 欄位,設定每個規則要如何被使用後才算真的有效。
extends:對應到的是 ESLint 的設定檔(eslint-config)
最後來說明 extends 這個欄位,extends 的功能其實很單純,就是把其他開發者寫好的 eslint-config(ESLint 設定檔)給載入進來。
舉例來說,上面範例的 .eslintrc.js
就是一個 ESLint 的設定檔,如果其他開發者想要使用同樣 的設定,除了用複製貼上的方式,把同樣的設定貼到自己的 ESLint 設定檔之外,也可以透過用 extends
這個欄位,直接套用別人寫好的設定,ESLint 就會把對應的設定也都載入到你專案的 ESlint 設定檔中。
也就是說,我們可以先建立了公司內部要共用的 ESLint 設定檔後,在不同專案的 extends
欄位都去載入這個共用的 ESLint 設定檔後,這些設定以及建立好的規則判斷(哪些規則要顯示為嚴重、哪些是警告、哪些可以不用理會)就會自動套用到專案當中,如此就可以避免需要重複複製貼上的問題。
常見的 ESLint 設定檔像是文章開頭提到的 eslint-config-standard、eslint-config-airbnb、eslint-config-prettier、eslint-config-google,同樣的,你會發現它們也都有一樣的前綴,都是以 eslint-config-*
開頭,因此在使用時可以省略掉 eslint-config- 的前綴,像是這樣:
{
extends: [
'plugin:react/recommended',
'airbnb',
],
}
這裡 extends 了 airbnb
,其實指的就是載入 eslint-config-airbnb
的 ESLint 設定檔。
在這裏我們還看到另外也載入了一個名為 plugin:react/recommend
的 ESLint 設定檔,之所以前面是以 plugin:react
開頭,是因為這隻設定檔實際上是放在 eslint-plugin-react
中(注意,是 eslint-plugin-react
而不是 eslint-config-react
喔!)。
為什麼要把 React 的 ESLint 設定檔放在 eslint-plugin-react 中,而不是獨立成 eslint-config-react 呢?
這呼應到前面有提到的,plugins 本質上只是用來定義一系列的規則,但這些規則怎麼被使用並不在 plugin 的範疇內。可是,既然都把規則定義好了,何不乾脆幫大家把怎麼使用這些規則的建議設定也一併提供在 plugin 的專案中 ,方便大家下載 plugin 後就可以直接使用呢?這也就是為什麼在 eslint-plugin-react 中,還有 recommended 這個 eslint-config 可以使用。
eslint-plugin-react
本身是 ESLint 的 plugin,但套件裡同時提供了plugin:react/recommended
這個 ESLint 設定檔(eslint-config)讓開發者使用。
當你在 extends 欄位中載入了 plugin:react/recommended
後,就會連帶的把使用這些規則的建議設定也載入到你的專案當中:
extends 和 plugin 的作用不同,但很容易搞混的原因也在這裡。
簡單來說,plugin 定義的是一系列可以被使用的規則,雖然不是必須,但它經常也會順便提供許多建議可以怎麼使用這些規則的設定,讓開發者可以直接透過 extends 來套用。像是大家常聽到的 prettier 也是透過這樣的方式,它先定義好一系列的規則後放在 eslint-plugin-prettier
中,而在這個 plugin 的套件裡也順便提供了建議的 ESLint 設定讓開發者能夠套用,所以你可能經常會看到這樣的 ESLint 設定檔:
// .eslintrc.js
module.exports = {
plugins: ['react', 'jest', 'prettier'],
extends: [
'plugin:react/recommended',
'plugin:jest/recommended',
'plugin:prettier/recommended',
],
};
都是類似的道理。
plugins 提供許多的規則,讓開發者可以自行設定要如何使用這些規則,而 plugin 的作者也可能一併提供了他認為合理的設定檔讓開發者可以放在
extends
中直接使用,但這並不是強制的。
有些時候你可能會看到下面這種只有 extends 卻沒有載入 plugins 的情況而感到困惑:
// .eslintrc.js
module.exports = {
plugins: [],
extends: [
'plugin:react/recommended',
'plugin:jest/recommended',
'plugin:prettier/recommended',
],
};
之所以可以這樣寫,是因為通常在這些 extends 的 ESLint 設定檔中,都已經幫你把使用到的 plugin 給定義好,所以你只要確保有安裝了這些 plugin,即使沒有在專案的 ESLint 設定檔中明確透過 plugins
定義這些 plugin,它們還是會被載入。
例如,你可以在 eslint-plugin-react 的 recommended config 中看到,它已經幫你把 plugins: ['react']
寫好了:
因此一旦你有使用 extends: ['plugin:react/recommended']
載入這個設定檔,它就已經幫你在 plugins
的地方載入 eslint-plugin-react。
實作 OneDegree 內部的 ESLint config
在了解 ESLint 中 plugin、extends 和 rules 的概念後,就可以知道,我們只需要先建立好一隻可以被共用的 ESLint 設定檔,在這裡面定義好各專案都希望遵循的規則及套用規則的邏輯後,接著只要在各個專案中,各自透過 extends 的方式,載入這個共用的設定檔,就可以達到預期的效果。
建立共用的 eslint 設定檔
以程式碼的概念會像是這樣子。
先建立一支準備被共用的 ESLint 設定檔,例如這裡取名叫 base.eslint.config.js
:
// base.eslint.config.js
module.exports = {
env: {
es6: true,
},
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: [],
rules: {
// new JSX transform
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
// customized rules
'no-console': 'off',
},
};
接著在專案本身的 ESLint 設定檔中,透過 extends
的方式去讀取到這支共用的設定檔就可以了。
假設這支共用的設定檔和專案自身的 ESLint 設定檔放在相同的路徑時,可以直接這樣載入:
// .eslintrc.js
// 專案本身的 ESLint 設定檔
module.exports = {
extends: ['./base.eslint.config'],
};
如此這個專案就會載入並套用這支共用的設定。
如果要確定專案有沒有實際吃到 extends 的內容,可以透過下列指令,複製專案中 ESLint 最終實際會套用到的設定來瞧瞧:
# 檢視並複製當前套用到的 eslint config
$ npx eslint --print-config path::String | pbcopy
可以看到,雖然專案本身的 ESLint config 只有短短一行 extends: ['./base.eslint.config']
,但透過上述的指令,可以看到實際套用的設定非常多:
除了可以看到有套用了幾個 plugins 之外,我們寫在 base.eslint.config.js
中的設定(例如:no-console
的規則邏輯)也有出現在最終實際會被 ESLint 使用到的設定中。
根據需要建立不同類型的設定檔
由於共用的設定檔本身也只是 JavaScript,因此不需要把所有的設定都放在同一支 base.eslint.config.js
中,而是可以透過 JavaScript 模組的方式來進行管理。
例如,可以有一支 base 的設定檔:
// base.eslint.config.js
module.exports = {
env: {
es6: true,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
rules: {
// ...
},
};
接著在建立一隻專門給 React 專案使用的,在這支 react.eslint.config.js
中,會先去 extends base
的設定,把剛剛寫的設定檔載入後,再加上針對 React 專案要套用的規則:
// react.eslint.config.js
module.exports = {
extends: [
// 載入 base 的設定
'./base.eslint.config',
// 套用針對 react 想要使用的設定
'plugin:react/recommended',
'plugin:react-hooks/recommended',
],
rules: {
// new JSX transform
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
},
};
針對 TypeScript 的專案,可以同樣 extends base
的設定後,針對 TypeScript 的部分進行設定:
// typescript.eslint.config.js
module.exports = {
extends: ['./base.eslint.config'],
plugins: ['@typescript-eslint'],
parser: '@typescript-eslint/parser',
rules: {
'no-param-reassign': [
'error',
{
props: true,
ignorePropertyModificationsFor: ['state'],
},
],
},
};
如此,未來如果新開的是 react 的專案,該專案可以直接 extends react.eslint.config.js
;如果是 TypeScript 的專案,則可以 extends typescript.eslint.config.js
即可。
包成 npm 套件
實際上,我們不只是會建立共用的 ESLint 設定檔,還會把這個共用的 eslint 設定檔發佈到 npm 上,就像是前面提到的 eslint-config-standard 或 eslint-config-airbnb 一樣,讓想要套用這個設定檔的專案只需要透過 npm 就可以安裝對應的設定檔,並且透過 extends 的方式來載入。
以 OneDegree 為例,我們把共用的 eslint 設定檔包成一個名為 eslint-config-onedegree
的套件,未來開新專案的時候,只需透過 npm install eslint-config-onedegree
下載設定檔後,接著在專案本身的 ESLint 設定檔中去 extends 它就可以了:
// .eslintrc.js
module.exports = {
extends: ['onedegree'], // 套件名稱開頭的 "eslint-config-*"" 可以省略
};
如果是針對特定類型的專案一樣可以像這樣進行 extends:
// .eslintrc.js
module.exports = {
extends: ['onedegree/react'], // 如果是針對 react 的專案
};
或是針對 TypeScript 的專案:
module.exports = {
extends: ['onedegree/typescript'], // 如果是針對 typescript 的專案
};
註:在 OneDegree 中,公司內部有自己的 npm registry,因此佈署時,是發佈到公司內部 host 的 npm registry。