Skip to content

建材仓库的大扫除

背景:node_modules 积压 8 个高危漏洞,开发环境暗藏风险。

过程:升级 TypeScript/ESLint/Vite/Babylon.js。


联邦的城邦都建在地上。

渲染城的高塔、物理城的钟楼、议会城的阶梯、图书馆城的书架——每一座城都光鲜亮丽。但很少有人去想:这些城,是用什么建起来的?

答案在仓库里。

在联邦领地的角落,有一座巨大的仓库,名叫 node_modules。里面堆满了各种建材——砖头、水泥、钢筋、玻璃、电线、管道……每一块砖头上都刻着一个版本号。

城邦的一砖一瓦,都来自这座仓库。

仓库越大,越容易藏污纳垢。


第一张清单:八颗红痣

"体检不能只看城邦本身,"外交官站在仓库大门前,手里拿着一张纸,"建材也得检查。"

"建材?"AI 同行者看着面前如山的箱子,"这些……不都好好的吗?"

"看上去好好的,"外交官摇摇头,"但你不知道哪块砖头里面有裂纹,哪袋水泥已经过期了,哪根钢筋里面生了锈。"

他举起手里的纸——那是一份 npm audit 报告

"八个漏洞,"他说,"七个高危,一个中危。"

AI 凑过去看:

esbuild  <=0.24.2
Severity: moderate
esbuild enables any website to send any requests to the development server...

minimatch  9.0.0 - 9.0.6
Severity: high
minimatch has a ReDoS via repeated wildcards...

"等等,"AI 挠头,"这些不是我们直接用的吧?esbuild 是 Vite 带的,minimatch 是 ESLint 带的……"

"对,都是传递依赖,"外交官点头,"但传递依赖也是依赖。就像你建房子,砖头是从建材商那里买的——砖坏了,墙一样会塌。"

"可这些都是开发环境用的啊,"AI 辩解,"生产构建又不带进去。"

"确实,生产环境不受影响,"外交官说,"但开发环境也是环境。你每天 npm run dev,开发服务器就跑在你电脑上。如果有个漏洞能让恶意网站读取你本地的文件——你觉得危不危险?"

AI 打了个寒颤。

"而且,"外交官补充,"现在是 8 个,不升级的话,以后会变成 18 个、80 个。债是会利滚利的。"

他把纸折起来:"大扫除吧。从最容易的开始。"


第一块砖:Babylon.js 小版本升级

最简单的是 Babylon.js

同大版本内的小版本升级——就像换了同品牌同型号的新砖头,尺寸、重量、接口全都一样,只是修了些小裂纹。

bash
npm install @babylonjs/core@^9.14.0

命令敲下去,几秒钟就完事。

"搞定,"AI 拍了拍手,"这也太简单了。"

"简单的事情先做,"外交官说,"省得放在那里心烦。"

验证也很简单——tsc --noEmit 过,vite build 过,完事。

第一块砖,轻松换掉。


第二块砖:TypeScript 大版本升级

接下来是 TypeScript

从 4.9 升到 5.4——跨了一个大版本。这就像把砖头从「标准砖」换成「高精度砖」,尺寸更精准、强度更高,但你得确认所有墙的尺寸都还对得上。

"听说 TypeScript 5 有很多 breaking changes?"AI 有点担心。

"还好,"外交官说,"TypeScript 的大版本升级通常比较温和。主要是类型收严,很少有语法级别的 breaking change。"

他敲下命令:

bash
npm install --save-dev typescript@^5.4.0

然后跑 tsc --noEmit

屏幕上跳出来一行红:

src/motion/beat-detector.ts(99,44): error TS2345:
Argument of type 'Uint8Array<ArrayBufferLike>' is not assignable
to parameter of type 'Uint8Array<ArrayBuffer>'.

"看,"外交官指着屏幕,"类型收严了。Uint8Array 默认是 ArrayBufferLike(包含 SharedArrayBuffer),但 getByteFrequencyData 只接受普通 ArrayBuffer。"

"就这一个错误?"AI 有点意外。

"就一个,"外交官点头,"显式声明一下类型就好。"

typescript
private freqData: Uint8Array<ArrayBuffer> = new Uint8Array(0);

改完再跑——一片绿。

"就改了一行?"AI 不敢相信。

"就一行,"外交官笑了,"TypeScript 团队的向后兼容性做得很好。大版本升级也没那么可怕。"

第二块砖,有惊无险。


第三块砖:ESLint 大版本升级 + flat config

真正的硬骨头是 ESLint

从 8 升到 9——这不仅仅是换砖头,连砌墙的方式都变了。ESLint 9 默认使用 flat config(扁平配置),取代了用了多年的 .eslintrc 格式。

"这就像……"AI 想了想,"之前的配置文件是层层嵌套的积木塔,现在要全部推倒,平铺成一层?"

"差不多那个意思,"外交官说,"旧的配置是继承链——extends 来 extends 去,像族谱一样。flat config 就是个数组,每个元素是一个配置对象,按顺序应用,简单直接。"

他删掉旧的 .eslintrc.cjs,新建了 eslint.config.js

"好处是更灵活、更可预测,"他一边写一边说,"坏处是——所有插件都得升级到支持 flat config 的版本。"

这意味着一堆东西都要跟着升:

  • @typescript-eslint/eslint-plugin 6 → 8
  • @typescript-eslint/parser 6 → 8
  • eslint-config-prettier 8 → 10
  • eslint-plugin-vitest —— 等等,这个还没 1.0

"vitest 插件最新才 0.5.4,"AI 查了一下,"不支持 ESLint 9?"

"对,还没跟上,"外交官说,"那就先去掉吧。反正它主要是给测试文件加规则,影响不大。以后出了 1.0 再加回来。"

卸掉一个包袱,剩下的一口气升上去。

然后是写 flat config。

旧配置里有 ban-types 规则,到了 v8 改名成 ban 了。一开始报错找不到规则,查了 changelog 才知道改名了。

"这就是大版本升级的麻烦,"外交官一边改一边说,"规则会改名、会移除、会合并。你得一个个对照着改。"

globals 也变了。旧配置里 env: { browser: true, es2022: true } 一行搞定,flat config 里得自己引入 globals 包,然后展开。

"多了一步,"AI 说,"但也更清晰了。"

"对,"外交官点头,"什么都明明白白写在那里,没有黑魔法。"

折腾了半个钟头,配置文件终于写好了。

跑一下 eslint src——

✖ 2331 problems (0 errors, 2331 warnings)

"0 errors!"AI 松了口气,"warnings 和之前差不多。"

"那就好,"外交官也松了口气,"warnings 是正常的,我们的规则本来就比较松。只要没新增 error 就行。"

第三块砖,啃下来了。


第四块砖:Vite 大版本升级

最后一块是 Vite

从 5 升到 6——构建工具的大版本升级,通常是最让人担心的。毕竟构建工具是整个项目的地基,地基动了,上面的一切都可能受影响。

"要不要先做个备份?"AI 有点紧张。

"不用,"外交官很淡定,"有 git 呢。大不了回退。"

他敲下命令:

bash
npm install --save-dev vite@^6.0.0

然后屏息凝神,敲下:

bash
npx vite build

屏幕上开始滚动构建日志。

  dist/index.html                       4.41 kB
  dist/assets/index_bg-y0ZN0FV8.wasm  546.19 kB
  dist/assets/index-C-B0-siL.js       575.63 kB

  (!) Some chunks are larger than 500 kB after minification...

  ✓ built in 1.25s

"成了?"AI 不敢相信。

"成了,"外交官也有点意外,"这么顺利?"

"Vite 6 的 breaking changes 主要是 SSR 和插件 API 那边,"AI 查了一下,"对纯客户端应用影响不大。"

"那正好,"外交官笑了,"省了不少事。"

验证完构建,再验证开发服务器——vite 能正常启动,热更新正常,一切正常。

第四块砖,意外地顺利。


归零

四块砖都换完了。

外交官再次拿起 npm audit 报告,看了一眼——

found 0 vulnerabilities

"零,"他把纸递给 AI,"八个漏洞,全没了。"

AI 接过纸,盯着那个大大的 0 看了好一会儿。

"就这么……搞定了?"他说,"感觉也没多难啊。"

"是不难,"外交官说,"难的是开始。"

"什么意思?"

"很多项目拖着不升级依赖,"外交官把纸折好,"总觉得'以后再说',总觉得'现在能用就行'。结果越拖越多,最后变成不敢碰的屎山——一碰就碎。"

"我们这不是碰了吗?"AI 说,"也没碎啊。"

"因为我们升得勤,"外交官说,"小步快跑,每次只升一两个,出了问题也好定位。攒一年再升,那才叫酸爽。"

他拍了拍 AI 的肩膀:

"记住——技术债的利息是复利的。 越早还,越省力。"


仓库的新规矩

大扫除结束后,外交官在仓库门口贴了一张新规矩:

【建材维护须知】

1. 每月跑一次 `npm run audit`
   ——发现高危漏洞,及时处理

2. 每季度跑一次 `npm run outdated`
   ——看看哪些依赖过期了,择机升级

3. 小版本随时升
   ——同大版本内,风险低,收益稳

4. 大版本攒够了再升
   ——一次升一堆,比分散升省事

5. 升完跑三件事
   ——tsc 检查、build 构建、lint 过一遍

"就这些?"AI 看着纸条。

"就这些,"外交官说,"简单的规矩,坚持执行,比什么都强。"

仓库里,阳光透过天窗照进来。

一排排货架整齐排列,每个箱子上都刻着最新的版本号。

没有裂纹,没有锈迹,没有过期。

干净,清爽,随时待命。

这就是大扫除的意义——

不是为了完美,

而是为了心里踏实


番外附记:升级成本 vs 收益

升级项版本跨度改动量风险收益
@babylonjs/core9.13 → 9.140 行代码🟢 极低bug 修复、小特性
TypeScript4.9 → 5.41 行代码🟢 低新语法、性能改进、类型更准
ESLint + @typescript-eslint8/6 → 9/8新配置文件🟡 中flat config、修复 7 个 high 漏洞
Vite5 → 60 行代码🟢 低修复 esbuild 漏洞、构建更快

总耗时: 约 1 小时 总改动: 约 150 行(主要是新配置文件) 漏洞数: 8 → 0


番外二完。地下管道检修了,建材仓库也打扫了。联邦从里到外,干干净净。