齊雲雷,微醫雲服務團隊前端工程師。專一於 Node.js 基礎生態建設以及在 Web 應用中的方案沉澱。javascript
4 月 25 日,NPM 社區又一次因更新事故引燃技術圈的討論,導火索便來自名爲 is-promise 的包。前端
網上盛傳一個單行代碼的包影響到了谷歌、FaceBook、亞馬遜等衆多大咖的知名項目,也有人揚言它使幾乎整個 JavaScript 生態陷入了混亂。java
不過「雪崩」之時,我和身邊人都沒有體會到震感,不由疑惑,平時不多有場景須要判斷某個值是否爲 Promise,如此名聲不顯、功能又不重要的 NPM 包,真的有這麼大的影響和破壞力嗎?node
既是好奇心的驅使,也是不認同部分誇張的言辭,我決定向前一探究竟。react
先解讀一下事故發生以前,is-promise 2.1.0 版本的完整代碼。git
module.exports = isPromise;
function isPromise(obj) { return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; } 複製代碼
這是一個比較寬鬆的 Promise Like 檢查函數,雖然包名叫 is-promise,其實更像 is-thenable。別看只有一行的邏輯,須要不淺的功力才能準確寫出。github
例如,前置的 typeof
能有效過濾 String.prototype.then = function () {}
這樣不合規範的 thenable 字符串。web
咱們能夠不使用,但不應貶低這個包的價值。Promise/A+ 是一個自由的規範,而非語言特性,長久以來有着衆多版本實現,採起這種具備包容性的判斷方式是合情合理的。npm
相似的 NPM 包還有 Sindre Sorhus 的 p-is-promise,它增長了 catch 方法的檢查。json
讓咱們一塊兒回到那個週末,從新審視整個事件的始末。
is-promise 做者 Forbes Lindesay 回顧了當時的主要歷程:
可見,做者收到告警信息後的反應是很是迅速的,但撤銷操做滯後的問題仍須要指責。
接下來,咱們逐個分析 2.2.x 版本的更迭。
站在上帝視角,咱們明確知道問題出在這裏,做者在 package.json 中新增了兩個字段
{
"type": "module", "exports": { "import": "index.mjs", "require": "index.js" } } 複製代碼
很快,就有人反饋 BUG,一共有兩類報錯
錯誤一:exports 的文件路徑遺漏了 './',在 Node.js 中
Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config /xxx/node_modules/is-promise/package.json; targets must start with "./"
複製代碼
錯誤二:添加了 type: module
,致使 require 被禁用,必須使用 import 才能引入。
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /xxx/node_modules/is-promise/index.js
複製代碼
以及被隱藏的錯誤三:沒有更新 package.json 中的 files 字段,致使 index.mjs、index.d.ts 沒有一塊兒打包發佈。
改動後的 package.json 包含以下
{
"exports": { "import": "./index.mjs", "require": "./index.js" } } 複製代碼
然而,若是使用 require('is-promise/package.json') 引入模塊下其餘文件,則會拋出
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './package.json' is not defined by "exports" in /Users/claude/Workspace/test/is-p/node_modules/is-promise/package.json
複製代碼
甚至不容許引用 'is-promise/index' 和 'is-promise/index.js'。
爲了完全解決 2.2.0 帶來的 Breaking Change,終於在 2.2.2 刪掉了 exports 字段。
本次事故源於兩個少見的 package.json 字段,咱們已經見識到了其反作用,但還沒搞明白爲何會被做者引入,不妨進一步明確它們的概念。
官網文檔在 12.x 及以上版本都包含這些字段的描述,可是並不表明 12.x 用戶必定享受到了這個特性。
它決定當前 package.json 層級目錄內文件遵循哪一種規範,包含兩種值,默認爲 commonjs。
要正常使用這個特性,在 Node.js v12.x 的早期版本,必須主動開啓 --experimental-modules。可是從 v12.16.0 之後就有些混亂,不開啓選項的狀況下錯誤使用該字段會當即拋出異常。直到了 v13.2.0 正式引入,取消了實驗特性的標識,纔算恢復正常。
is-promise 將 type 顯式指定爲 module,顯然會影響到特定版本的 CommonJS 用戶。
type
是相對較老的特性,exports
則是鮮有人知。
功能來自 proposal-pkg-exports 提案,以實驗特性 --experimental-exports 加入 v12.7.0,於 v12.16.0 正式引入。具體時間線能夠經過這個 PR 追溯。
下面看它的具體做用。
一般,咱們用 main 字段指定包的入口文件,但也僅限於指定惟一的入口文件。
exports 字段是 main 的補充,支持定製不一樣運行環境、不一樣引入方式下的入口文件,也支持導出其餘文件,看下面的例子便知。
{
"main": "./main.js", "exports": { ".": "./main.js", "./feature": { "browser": "./feature-browser.js", "default": "./feature.js" } } } 複製代碼
但值得注意的是,在支持 exports 的 Node.js 版本中,exports 會覆蓋 main.js。
exports 一旦被指定,只能引用 exports 中顯示導出的文件。
用下面這種特殊寫法,才能容許項目內全部文件被導出(未通過充分測試)。 但缺點是沒法使用 import isPromise from 'is-promise/index’
,而必須帶上文件後綴 import isPromise from 'is-promise/index.mjs'
。
{
"exports": { ".": ".", "./": "./", } } 複製代碼
此外,做者想固然覺得 exports 和 main 字段同樣,支持省略 "./",這在文檔中並無交代。
過後,做者發佈了一篇 《is-promise post mortem》,他公開說明了上述的一部分錯誤,還總結了導致犯錯的幾個因素
總結下來就兩點,測試不充分,流程不規範。
我翻找了相關 ISSUES,發現 create-react-app、@angular/cli、firebase-tools 等項目的確受到影響,具體表現則爲安裝、構建失敗。
再回看 NPM 生態,is-promise 周下載量在千萬級,存在直接引用關係的就有 766 個包(現只剩 561,受事故影響,許多包取消了引用),GitHub 顯示依賴它的項目更是有 3.5m 之衆。
從問題版本 2.2.0 發佈,到 2.2.2 修復,歷時約 3 個小時,考慮到 NPM 的緩存機制,實際影響時間會被拉長。
所以,它的影響範圍的確很廣,但實際沒有那麼誇張。
一方面,Node.js 12.16.0 之前的 LST 和更早版本纔是主流,這些運行時可被認定爲安全。
另外一方面,遭到輻射的項目(大多爲 CLI 工具)並不具有整個生態的表明性,也不會危及生產環境。
看過了問題,也藉此反思一下如何避免悲劇發生在本身身上吧。
加鎖能夠 100% 避免本次意外,尤爲面向應用開發者,這是一直在呼籲的工做,卻不多真正落地。
不要吐槽 package-lock.json 會本身變,由於只有一個 lock 文件是不成氣候的,若是 package.json 沒有鎖定版本,NPM 會使用浮動的版本覆蓋 package-lock.json。
但對於 NPM 包的開發者,除非是對穩定性有所要求的工具鏈、產品,仍是不建議濫用版本鎖定。若是全部的 NPM 包都這麼作,必定會加大 node_modules 的混亂程度,也不利於及時享受到相關依賴的修復補丁,反而提升了維護難度。
測試的重要性無須多言。
is-promise 的新增更改根本沒有獲得測試覆蓋,甚至連 require 引入都會報錯。除了開發者要完善 CI,NPM 是否也有提供內置檢測服務的義務呢?
小型庫背後是衆多開源人士的努力貢獻,優質的文檔、測試用例遠超代碼的原始價值。
is-promise 的問題不在於它有幾行代碼,而且代碼邏輯沒有變動。
我的認爲,NPM 包開發者有必要減小依賴數量,應用開發者則能夠自由決定。引用也好,套用也罷,但至少請給這些代碼的做者和協議應有的尊重。
2.2.0 這個版本號的使用是否得當,若是隻從功能上看,它是向下兼容 2.1.0 的一次更新嗎?
看過上面 exports 字段的介紹能夠得知,它固然屬於 Breaking Change,但 Node.js 文檔的描寫是模糊的,讓 is-promise 的做者認爲 exports 是無害的。
官網通篇沒有一個警告字樣,若是沒有此次事故後才提交的 PR,恐怕會有更多的人掉入坑中。
曾經有很多人傾向於 Yarn 的機制,時至今日,Yarn 和 NPM 的差距已經大大收縮,二者都是不錯的選擇,我惟一建議是不要混合使用。
Yarn 的速度已經沒有特別大的優點
還有像 PNPM 這類致力於改進 NPM 生態的努力,值得咱們持續關注。
當前仍在批判 NPM 生態的人羣,大部分不會參與 JS 社區的建設,願改善現狀而貢獻的更是百裏挑一。
各位 NPM 用戶無須危言聳聽,人有失手,馬有失蹄,只要規範流程,可以有效下降負面影響。
逆耳未必是忠言,但願更多有價值的聲音能被髮出。