起因
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](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F02884fed-6066-4204-9640-9bbd46169ed2%2Fnpm-babel-core.png%3Fid%3D9ccbd815-a06d-4a7c-b184-b3ca2141a889%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3Dzzl3FYzcn-TXFyXLzvUj3htM2R6WI_LutSX-rkvNMlM?table=block&id=9ccbd815-a06d-4a7c-b184-b3ca2141a889&cache=v2)
过了一会儿,有其他人在 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](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Fb6840ca8-7389-44bf-a586-f3a5f693fff2%2Fbabel-core-packagejson.png%3Fid%3D822dfe59-0b26-458a-9d9e-84b25ac1afe8%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DXCbFpP5OCTiO9EOwzYYjcWawdgV7ltIvrzXIxNOLkV8?table=block&id=822dfe59-0b26-458a-9d9e-84b25ac1afe8&cache=v2)
让我们来复习一下^是什么意思:
^: 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](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F1bed50ea-0aa3-4c3f-a025-cdcd69a37d9a%2Fnpmrc-package-lock.png%3Fid%3De26e1a9e-c8db-4bca-b26a-0e601193a934%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DtPli11uiTlnH__9qCxhFGB57rh0otDwmwS11LSY37Do?table=block&id=e26e1a9e-c8db-4bca-b26a-0e601193a934&cache=v2)
没有 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](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F9cdb39d9-07ac-4426-a503-a51d02afae3a%2Fstatic-analysis-1.png%3Fid%3Df43a1d78-0f56-479d-abfd-e5a1ec968cc9%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DNUgA5fA7ULDlI1T6QU7w9dvBzGFTlVPvkZCEkmm3w0k?table=block&id=f43a1d78-0f56-479d-abfd-e5a1ec968cc9&cache=v2)
简单分析一下,这里报错是因为
polyfillsNames
里面没有找到 "es6.array.slice"
,而 polyfillsNames
又是从 provider.polyfills
里来的,而它又来自一个运行时传入的 factory
函数:![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Fe785f8f2-0262-439f-b1b7-339163541a50%2Fstatic-analysis-2.png%3Fid%3Dfebb5e55-c7bc-403b-ba2e-18238c404a95%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DvbCaPXCvxxCeiqTeGiF0DW57ptF7VQCaVfLuQNOtWCM?table=block&id=febb5e55-c7bc-403b-ba2e-18238c404a95&cache=v2)
动态分析
遇事不决,上调试器。 在 vscode 的 debugger
launch.json
配置里加上一条配置,用来启动会报错的 npm run build
命令:![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Fae08b51e-68fa-43c3-9656-18d9f8121523%2Fdynamic-analysis-1.png%3Fid%3D4980166f-bba6-41ac-bdc8-efeedad2315f%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DI8MqhMKFo8_gyPE_qdGM2hM-hbZphPy-H_5vdu2vG9g?table=block&id=4980166f-bba6-41ac-bdc8-efeedad2315f&cache=v2)
在上面可疑的几行代码上打上断点,运行
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Faa7309b2-2d40-4f2b-9bb1-bf41d839255d%2Fdynamic-analysis-2.png%3Fid%3Dd1a53a72-b827-4456-bf69-5d29a7b3a929%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DoHp0Q0ntZs4pWWNRXER_Wcu2ZmuJGWHWAogNqQXz8MQ?table=block&id=d1a53a72-b827-4456-bf69-5d29a7b3a929&cache=v2)
这里我们可以看到,
provider
是 corejs2
而 provider.polyfills
里面确实少了 "es6.array.slice"
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F0d46ba6f-93af-43d7-8b03-11a48cb36573%2Fdynamic-analysis-3.png%3Fid%3Dfbfe5d55-2faf-42ef-b836-3312ba52f978%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DAU8g13GrOqoGv8gmM9MlvNzgbKo7mOIkJimqYDjxN6U?table=block&id=fbfe5d55-2faf-42ef-b836-3312ba52f978&cache=v2)
而
polyfillsNames
正是由provider.polyfills
的 key 组成的 Set
再运行到抛错这一行,也验证了这一点。![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F4e39de31-344b-4d4d-8eba-a26a0750b767%2Fdynamic-analysis-4.png%3Fid%3D0cdd3e02-9eaa-4bf3-bddf-efc03071894e%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DRwZWD7y-_CqX6sYDuyO-CFcrVhK6nDtrqsRJNaNR4PU?table=block&id=0cdd3e02-9eaa-4bf3-bddf-efc03071894e&cache=v2)
所以
provider.polyfills
是哪里来的呢,我们继续看: 找到生成 provider
的 factory
,点击 step into 看看里面是啥![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F32ea66b1-88de-4f3e-a88a-c33db465bb44%2Fdynamic-analysis-5.png%3Fid%3D90e8d879-a4fc-4761-af2c-9a4102372a05%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3D2rS43cr9nO9ecnar-AXcYHwWuQbfIlIbidGsH1AQGVA?table=block&id=90e8d879-a4fc-4761-af2c-9a4102372a05&cache=v2)
咻的一下我们到了
babel-plugin-polyfill-corejs2
里面:![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Fa3d31dcf-ba5e-47d7-abc7-58cb775dab1e%2Fdynamic-analysis-6.png%3Fid%3Dbd505315-9a93-4bb3-ad34-1011cf5c4d00%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3D0wXq3COkWgd1Z-NncUWEkKMskAJxbPIDNy6wW5Ci1IM?table=block&id=bd505315-9a93-4bb3-ad34-1011cf5c4d00&cache=v2)
polyfills 就在 45 行这里,又是个函数,我们再到这行 step into,到了一个
add-platform-specific-polyfills.js
文件:![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Fa448ff9e-9ee7-4d00-a71d-443680c50e8d%2Fdynamic-analysis-7.png%3Fid%3D43c3c370-7dc4-4a4a-bedd-7d9babb7d4aa%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DNex44gOOoUTcIjnukTfq3MUb6voQlj-wNfp6xKI8MdE?table=block&id=43c3c370-7dc4-4a4a-bedd-7d9babb7d4aa&cache=v2)
这里很明显只是把外面传入的第三个参数
polyfills
套进 object 里原样传出了而已 再回到外面看一下:![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Fa0133748-04d9-45da-ab3b-1760065334bd%2Fdynamic-analysis-8.png%3Fid%3D5f1f4f48-4925-4c7a-a890-4eec6aa4eec0%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3D81WUJlTq6ySGGv8p7blvPm8RIIglSZmTp0fxvsxOMUg?table=block&id=5f1f4f48-4925-4c7a-a890-4eec6aa4eec0&cache=v2)
这个
_corejs2BuiltIns.default
就是 corejs2 的一个兼容性列表,里面也没有"es6.array.slice"
再看它的来源,它引入自 @babel/compat-data/corejs2-built-ins
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F0b0e27c3-237a-4911-a8b6-0b6d2434214c%2Fdynamic-analysis-9.png%3Fid%3D2ab7d9d6-e46c-46eb-95b9-adaea4e0299e%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3Dfhc4V57VcES9bnnaP9fbbtTfa2N07g8TLRAty-z5GlE?table=block&id=2ab7d9d6-e46c-46eb-95b9-adaea4e0299e&cache=v2)
而它又是直接导出的
data/corejs2-built-ins.json
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Fd94d8932-7d4d-47e5-b000-e7b91c2bf312%2Fdynamic-analysis-10.png%3Fid%3Dd51f6732-2479-4e68-9d74-e6f80997cfbc%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DjCMkW6gG34yC3QdPf9zAxI1FwAqLoURvX1kPuvkdWkA?table=block&id=d51f6732-2479-4e68-9d74-e6f80997cfbc&cache=v2)
这个 json就是我们已经见过几次的 corejs2 polyfill 兼容性列表,而里面正缺少了这个
"es6.array.slice"
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F2b4f0e6c-5508-4810-af6b-5e44f7a07eb3%2Fdynamic-analysis-11.png%3Fid%3D32a49bb3-99bd-4449-a8cb-bafddf92c926%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3Ds1wo6XcrZLvPtURPBJ21-qMrnG2T58bE55qVX6lRKxQ?table=block&id=32a49bb3-99bd-4449-a8cb-bafddf92c926&cache=v2)
验证
现在试试把
"es6.array.slice"
加回去能否修复问题,直接去 node_modules
里找到这个 json 修改它,既然我们在这里只用到了 key,所以放个空 object 就可以了:![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Feaa727ae-3569-4194-a6c8-c5fd150ba4d5%2Fvalidation.png%3Fid%3Db655804f-621a-4687-98b6-fbaa41d8962d%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3D68Z5xxqpiVPfttbkMZlhF4QlEC3BZrQOC8Wh4Uncn-M?table=block&id=b655804f-621a-4687-98b6-fbaa41d8962d&cache=v2)
改完了重新运行
npm run build
,项目成功编译,问题解决。修复
我去 GitHub 上给 Babel 官方提了一个 MR,项目负责人很热心负责,这里给他们一个赞。 上面的
corejs2-built-ins.json
当然不是手动维护的,而是由另一个脚本自动生成的,而这个脚本因为历史原因禁用了 es6.array.slice
等几个 polyfill 而忘记加回去,单测又没有覆盖到这个场景,这才是出现这次问题的根本原因。![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2Fcc7adb26-3736-4dec-83d2-68b5b9657169%2Ffix-1.png%3Fid%3D4cd22612-ed45-4a87-bc93-29cb5530d55d%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3Dx0FULcfnPs-F426fhkAvDqWFZaex7vGqCAZAuEmMTUg?table=block&id=4cd22612-ed45-4a87-bc93-29cb5530d55d&cache=v2)
此修复已由 babel 官方合并并在 babel 7.13.5 中推送,其他碰见问题的小伙伴升级安装依赖即可解决。
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F015ab979-66d5-4234-aeba-57e02d43990d%2F9db1b123-47cd-4bcc-9d83-209d21a6e10c%2Ffix-2.png%3Fid%3Dc01c96f2-d544-4332-bb3f-1099bae61313%26table%3Dblock%26spaceId%3D015ab979-66d5-4234-aeba-57e02d43990d%26expirationTimestamp%3D1719446400000%26signature%3DDfxXSzVop8E3NSdleT225wKpEnlr3xH5eaGT80ry49M?table=block&id=c01c96f2-d544-4332-bb3f-1099bae61313&cache=v2)
预防
- 我们已经在 A,B 以及相关的前端项目中都启用了 package lock,以免上游的锅直接掉到我们头上,我们还无法回滚
- 当然,我们会在开发流程中定期更新 package lock,以免被三体星人永远锁死在旧版
Today I Learn
- JavaScript 的底层工具链比你想象中更脆弱
- Babel 的团队还是很及时处理问题的
- Package lock 非常重要,这是防止线上事故偷溜进来的一个重要防线
- (我们的 CI 上都是发布时候现安装 npm 包的,在无 lock 的情况下,从开发测试到预发生产环节都无法保证有问题的上游更新不会被引入)
- 不要迷信 semver,除非你相信所有依赖都不会出 bug。
Loading Comments...