規則手冊
撰寫可攜式套件非常重要,因為它能確保您的使用者無論使用哪種套件管理員,都能享有最佳體驗。
為此,本頁詳述了您應遵循的最新良好實務集合,以便讓您的套件在所有三個主要的套件管理員(Yarn、pnpm 和 npm)上都能順利運作,如果您想進一步了解,我們也提供了說明。
套件應該只要求其相依性中正式列出的內容
為什麼?因為否則您的套件將容易受到無法預測的 提升,這將導致某些使用者會遇到偽隨機崩潰,具體取決於他們碰巧使用的其他套件。
假設愛麗絲使用 Babel。Babel 依賴於一個實用程式套件,而實用程式套件本身又依賴於舊版本的 Lodash。由於實用程式套件已經依賴於 Lodash,因此 Babel 維護者鮑伯決定使用 Lodash,而沒有在 Babel 本身中正式宣告它。
由於提升,Lodash 將被放在最上面,樹狀結構會變成像這樣
到目前為止,一切都很好:工具套件仍然需要 Lodash,但我們不再需要在 Babel 中建立子目錄。現在,想像 Alice 也將 Gatsby 加入其中,我們假裝它也依賴 Lodash,但這次是較新的版本;樹狀圖會像這樣
提升變得更有趣了 - 由於 Babel 沒有正式宣告依賴關係,因此可能會發生兩種不同的提升配置。第一個與我們之前所見的幾乎相同,只不過我們現在有兩個 Lodash 副本,只有一個提升到停止點,這樣就不會造成衝突
但第二個配置也很有可能發生!而這時事情就變得棘手了
首先,讓我們檢查這個配置是否有效:Gatsby 仍然取得其 Lodash 4 依賴關係,Babel 工具套件仍然取得 Lodash 1,而 Babel 本身仍然取得工具套件,就像之前一樣。但其他事情改變了!Babel 將不再存取 Lodash 1!它會改為擷取 Gatsby 提供的 Lodash 4 副本,這可能與 Babel 原先預期的不符。最好的情況是應用程式會崩潰,最糟的情況是它會靜默通過並產生錯誤的結果。
如果 Babel 反而將 Lodash 1 定義為其自己的依賴關係,套件管理員就能夠編碼這個限制,並確保無論提升如何,需求都能獲得滿足。
解決方案:在大部分情況下(當遺失的依賴關係是工具套件時),修正方法其實就是將遺失的項目加入 dependencies
欄位。雖然通常這樣就夠了,但有時也會出現一些更複雜的情況
-
如果你的套件是外掛程式(例如
babel-plugin-transform-commonjs
),而遺失的依賴關係是核心(例如babel-core
),你需要在peerDependencies
欄位 中註冊依賴關係。 -
如果你的套件是自動載入外掛程式的東西(例如
eslint
),對等依賴關係顯然不是選項,因為你不可能合理地列出所有外掛程式。相反地,你應該使用createRequire
函式(或其 polyfill)來載入外掛程式,代表列出要載入外掛程式的設定檔 - 無論是 package.json 或自訂的,例如.eslintrc.js
檔案。 -
如果你的套件僅在使用者控制的特定情況下需要相依性(例如,僅當使用者實際使用 SQLite3 資料庫時才依賴於
sqlite3
的mikro-orm
),請使用peerDependenciesMeta
欄位 將同儕相依性宣告為「可選」,並在未滿足時取消任何警告。 -
如果你的套件是公用程式元套件(例如,本身依賴於 Webpack 的 Next.js,如此一來,使用者就不必這麼做),情況會有點複雜,你有兩種不同的選項
-
建議的做法是將相依性(在 Next.js 的情況下為
webpack
)列為「一般相依性和同儕相依性」。Yarn 會將此模式解譯為「具有預設值的同儕相依性」,表示你的使用者可以在需要時取得 Webpack 套件的所有權,同時仍讓套件管理員有能力在提供的版本與你的套件預期不相容時發出警告。 -
另一種做法是重新匯出相依性作為公開 API 的一部分。例如,Next 可以公開一個僅包含
module.exports = require('webpack')
的next/webpack
檔案,而使用者會需要它,而不是典型的webpack
模組。不過,這並非建議的做法,因為它無法與預期 Webpack 成為同儕相依性的外掛程式順利運作(它們不會知道需要使用這個next/webpack
模組)。
-
模組不應對 node_modules
路徑硬編碼,以存取其他模組
原因:提升會讓無法確定 node_modules
資料夾的配置是否始終相同。事實上,根據確切的安裝策略,node_modules
資料夾甚至可能不存在。
解決方案:如果你需要透過 fs
API 存取其中一個相依性的檔案(例如,讀取相依性的 package.json
),請使用 require.resolve
取得路徑,而無需對相依性位置做出假設
const fs = require(`fs`);
const data = fs.readFileSync(require.resolve(`my-dep/package.json`));
如果您需要存取相依項的相依項(我們真的不建議這麼做,但在某些特殊情況下可能會發生),請使用 createRequire
函式,而不是硬編碼 node_modules
路徑
const {createRequire} = require(`module`);
const firstDepReq = createRequire(require.resolve(`my-dep/package.json`));
const secondDep = firstDepReq(`transitive-dep`);
請注意,雖然 createRequire
是 Node 12+,但有一個名為 create-require
的多載元件。
使用者腳本不應硬編碼 node_modules/.bin
資料夾
原因: .bin
資料夾是實作細節,根據安裝策略可能根本不存在。
解決方案:如果您正在撰寫 腳本,您可以直接透過名稱參照二進位檔!因此,請改寫成 jest -w
,而不是 node_modules/.bin/jest -w
,這樣就可以了。如果因為某些原因而無法使用 jest
,請檢查目前的套件是否已正確 將其定義為相依項。
有時您可能會發現自己有更複雜的需求,例如如果您希望使用特定的 Node 旗標產生腳本。根據不同的情況,我們建議透過 NODE_OPTIONS
環境變數,而不是 CLI 來傳遞選項,但如果這不是一個選項,您可以使用 yarn bin name
來取得指定的二進位檔路徑
yarn node --inspect $(yarn bin jest)
請注意,在這個特定情況下,yarn run
也支援 --inspect
旗標,因此您可以直接撰寫
已發佈的套件應避免在腳本中使用 npm run
為什麼?這是一個棘手的問題...基本上,歸結為:封裝管理員不可互換。在由另一個封裝管理員安裝的專案上使用一個封裝管理員是造成問題的根源,因為它們遵循不同的組態設定和規則。例如,Yarn 提供一個掛鉤系統,允許其使用者追蹤執行哪些腳本以及它們花費多少時間。因為 npm run
不知道如何呼叫這些掛鉤,所以它們會被忽略,導致你的消費者感到沮喪。
解決方案:雖然不是最美觀的選項,但目前最可攜帶的選項是簡單地取代 npm run name
(或 yarn run name
)在你的 postinstall 腳本中,並由下列取代
$npm_execpath run <name>
$npm_execpath
環境變數將根據你的消費者將使用的封裝管理員替換為正確的二進位檔。Yarn 也支援僅呼叫 run <name>
而無需提及封裝管理員,但到目前為止,沒有其他封裝管理員支援。
封裝不應寫入其自己的資料夾內,postinstall 除外
為什麼?根據安裝策略,封裝可能會儲存在拒絕寫入存取的唯讀資料儲存中。當使用「系統全域」儲存時,這尤其正確,因為修改一個封裝的來源會冒著損壞從同一台機器依賴它的所有專案的風險。
解決方案:只要寫入另一個目錄,而不是你自己的封裝。任何方法都可行,但一個非常常見的慣用語是使用 node_modules/.cache
資料夾來儲存快取資料 - 例如 Babel、Webpack 等就是這樣做的。
如果你絕對需要寫入你的封裝來源資料夾(但實際上,我們以前從未遇到過這種使用案例),你仍然可以使用 preferUnplugged
指示 Yarn 對你的封裝停用最佳化,並將其儲存在其自己的專案本機副本中,你可以在其中隨意變更它。
封裝應使用 prepack
腳本在發布前產生 dist 檔案
為何?原本的 npm 支援 許多不同的腳本。事實上,它支援的腳本數量如此之多,以至於很難知道在什麼情況下應該使用哪個腳本。特別是 prepack
、prepare
、prepublish
和 prepublish-only
腳本之間非常細微的差異,導致許多人在錯誤的情況下使用了錯誤的腳本。因此,Yarn 2 已將大部分腳本標示為已棄用,並將它們整合到一組受限的攜帶式腳本中。
解決方案:如果您希望在發布套件之前產生 dist 工件,請務必使用 prepack
腳本。它會在呼叫 yarn pack
(它本身會在呼叫 yarn npm publish
之前呼叫)之前呼叫,當將您的 git 儲存庫複製為 git 相依項時,以及任何時候您執行 yarn prepack
時。至於 prepublish
,請勿將它與副作用一起使用 - 它唯一的用途應該是執行發布步驟之前的測試。