从一次 Babel 更新带来的故障说起

Created
May 3, 2021 01:03 PM
Tags
JavaScript
Babel

起因

2021 年 2 月 23 日早上,我们发现某两个前端项目所有线上线下的 build 和测试都挂掉了,报的是同一个错误: unknown polyfill "es6.array.slice"
>> /home/jenkins/ci/node_modules/_@babel_core@7.13.0@@babel/core/lib/transformation/index.js:45    throw e;
    ^
Error: /home/jenkins/ci/test/e2e/helper.js: Internal error in the corejs2 provider: unknown polyfill "es6.array.slice".
    at shouldInjectPolyfill (/home/jenkins/ci/node_modules/_@babel_helper-define-polyfill-provider@0.1.0@@babel/helper-define-polyfill-provider/lib/index.js:111:15)
    at inject (/home/jenkins/ci/node_modules/_babel-plugin-polyfill-corejs2@0.1.2@babel-plugin-polyfill-corejs2/lib/index.js:50:11)
    at /home/jenkins/ci/node_modules/_babel-plugin-polyfill-corejs2@0.1.2@babel-plugin-polyfill-corejs2/lib/index.js:58:26
    at Array.forEach (<anonymous>)
    at inject (/home/jenkins/ci/node_modules/_babel-plugin-polyfill-corejs2@0.1.2@babel-plugin-polyfill-corejs2/lib/index.js:58:10)
    at Object.usageGlobal (/home/jenkins/ci/node_modules/_babel-plugin-polyfill-corejs2@0.1.2@babel-plugin-polyfill-corejs2/lib/index.js:109:7)
    at callProvider (/home/jenkins/ci/node_modules/_@babel_helper-define-polyfill-provider@0.1.0@@babel/helper-define-polyfill-provider/lib/index.js:188:27)
    at property (/home/jenkins/ci/node_modules/_@babel_helper-define-polyfill-provider@0.1.0@@babel/helper-define-polyfill-provider/lib/visitors/usage.js:10:12)
    at PluginPass.MemberExpression (/home/jenkins/ci/node_modules/_@babel_helper-define-polyfill-provider@0.1.0@@babel/helper-define-polyfill-provider/lib/visitors/usage.js:41:14)

初步定位

看调用堆栈,很明显是 Babel 报错了。 再去看 Babel 最近的发布历史,babel-core 刚刚发了一个 7.13 版本,锅应该是来自 babel 了。
notion image
过了一会儿,有其他人在 GitHub 上发了一个同样报错的 issue,基本上排除了我们自己代码出错的可能: Internal error in the corejs2 provider: unknown polyfill “es6.array.slice”

止血

既然定位在最新版 babel,止血的办法就很自然了:降级到昨天的老 babel。

降级@babel/core

一顿操作,在降级 @babel/core 到 7.12 甚至 7.1 之后,问题依旧,这是为什么呢? 打开[@babel/core 的 package.json](https://github.com/babel/babel/blob/v7.12.18/packages/babel-core/package.json#L47) ,我们可以看到,@babel/core本身的 dependencies,都是^开头:
notion image
让我们来复习一下^是什么意思:
^: include everything that does not increment the first non-zero portion of semver – https://semver.npmjs.com/
意思是,只保证版本号第一个数字(Major version)不变。就算你装的是 7.12 版本的@babel/core,它的间接依赖 ^7.12.13  也已经是指向最新的 ^7.13.x  了,而@babel/core的间接依赖多如牛毛。 换句话说,在这个时候,你的项目间接依赖版本是不可控的。
那怎么办?

回到过去

其实有一个大家都听说过的东西,就是专门用来解决这个问题的,他就是package-lock.json 或者 yarn.lock (只不过我们内部默认情况给禁用了)。 Package lock 的本质是,给你当前的 node_modules  文件夹打一个快照,保证你今天装的直接依赖和间接依赖和明天装的还是一样的,这样你打出来的包也是可重现的(reproducible build)。

利用 package-lock.json

在 A 项目中,我们恰巧找到了一个之前保留下来的package-lock.json文件,把它放到项目目录,在.npmrc中启用package-lock,重新npm install,本地验证问题解决。
notion image

没有 lock 就用 shrinkwrap

在另一个项目 B 中,我们并没有留下 lock 文件,只有一个还能用的旧 node_modules  文件夹。
这怎么办呢?
这时,我们找到 npm 的shrinkwrap 命令,恰巧就可以从 node_modules  快照生成一个与 package-lock.json 格式相同的 npm-shrinkwrap.json ,可以直接把它当 lock 文件用。
npm shrinkwrap
mv npm-shrinkwrap.json package-lock.json
实际上 shrinkwrap 就是 lock 的前身,所以兼容也是自然的。 重新 npm install 后,本地验证问题解决。

根本修复 babel

到这里虽然止血成功了,但是根本问题并没有解决,babel 到底是哪里出了问题呢?

定位

静态分析

很明显,报错信息中有一个很特别的字符串: unknown polyfill  我们在 node_modules  中全文搜索unknown polyfill ,就找到一个结果,在 @babel/helper-define-polyfill-provider  中,就它了。
notion image
 
简单分析一下,这里报错是因为 polyfillsNames  里面没有找到 "es6.array.slice" ,而 polyfillsNames  又是从 provider.polyfills  里来的,而它又来自一个运行时传入的 factory  函数:
notion image
 

动态分析

遇事不决,上调试器。 在 vscode 的 debugger launch.json  配置里加上一条配置,用来启动会报错的 npm run build  命令:
 
notion image
在上面可疑的几行代码上打上断点,运行
notion image
 
这里我们可以看到, provider  是 corejs2  而 provider.polyfills  里面确实少了 "es6.array.slice"
notion image
 
polyfillsNames  正是由provider.polyfills 的 key 组成的 Set  再运行到抛错这一行,也验证了这一点。
notion image
所以provider.polyfills是哪里来的呢,我们继续看: 找到生成 provider  的 factory ,点击 step into 看看里面是啥
notion image
 
咻的一下我们到了 babel-plugin-polyfill-corejs2  里面:
notion image
polyfills 就在 45 行这里,又是个函数,我们再到这行 step into,到了一个 add-platform-specific-polyfills.js  文件:
notion image
这里很明显只是把外面传入的第三个参数 polyfills  套进 object 里原样传出了而已 再回到外面看一下:
notion image
这个 _corejs2BuiltIns.default  就是 corejs2 的一个兼容性列表,里面也没有"es6.array.slice" 再看它的来源,它引入自 @babel/compat-data/corejs2-built-ins
notion image
而它又是直接导出data/corejs2-built-ins.json
notion image
这个 json就是我们已经见过几次的 corejs2 polyfill 兼容性列表,而里面正缺少了这个"es6.array.slice"
notion image

验证

现在试试把"es6.array.slice" 加回去能否修复问题,直接去 node_modules  里找到这个 json 修改它,既然我们在这里只用到了 key,所以放个空 object 就可以了:
notion image
改完了重新运行 npm run build ,项目成功编译,问题解决。

修复

我去 GitHub 上给 Babel 官方提了一个 MR,项目负责人很热心负责,这里给他们一个赞。 上面的 corejs2-built-ins.json  当然不是手动维护的,而是由另一个脚本自动生成的,而这个脚本因为历史原因禁用了 es6.array.slice  等几个 polyfill 而忘记加回去,单测又没有覆盖到这个场景,这才是出现这次问题的根本原因。
notion image
此修复已由 babel 官方合并并在 babel 7.13.5 中推送,其他碰见问题的小伙伴升级安装依赖即可解决。
notion image

预防

  • 我们已经在 A,B 以及相关的前端项目中都启用了 package lock,以免上游的锅直接掉到我们头上,我们还无法回滚
    • 当然,我们会在开发流程中定期更新 package lock,以免被三体星人永远锁死在旧版

Today I Learn

  • JavaScript 的底层工具链比你想象中更脆弱
  • Babel 的团队还是很及时处理问题的
  • Package lock 非常重要,这是防止线上事故偷溜进来的一个重要防线
    • (我们的 CI 上都是发布时候现安装 npm 包的,在无 lock 的情况下,从开发测试到预发生产环节都无法保证有问题的上游更新不会被引入)
  • 不要迷信 semver,除非你相信所有依赖都不会出 bug。

Loading Comments...