谁动了我的 .npmrc?一次由“淘宝源”引发的诡异事件排查
前言
2022 年 5 月,淘宝源发布了一个重要公告: 淘宝镜像源地址由 registry.npm.taobao.org
正式替换为 registry.npmmirror.com
。
2024 年 1 月 22 日,registry.npm.taobao.org
的 SSL 证书正式过期,使用该源的方法彻底失效。
按理说,前端开发同学只需要执行下面的命令替换更新源地址即可:
npm config set registry https://registry.npmmirror.com
或者全局替换:
npm config set registry https://registry.npmmirror.com --global
看起来很美好,起初我也是这么以为的... 然而现实总是比想象复杂一些 🤔
场景重现
某一天,有个老项目的客户找过来要做功能迭代。走完前置流程,进入开发阶段:
- 拉取git代码:
git clone xxxxx.git
- 安装依赖:
yarn install
然后控制台无情地报错了:
info No lockfile found.
[1/4] Resolving packages...
error Error: certificate has expired
at TLSSocket.onConnectSecure (node:_tls_wrap:1539:34)
at TLSSocket.emit (node:events:513:28)
at TLSSocket._finishInit (node:_tls_wrap:953:8)
at TLSWrap.ssl.onhandshakedone (node:_tls_wrap:734:12)
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
嗯?🤨 检查一下项目配置:
- .npmrc配置正常 ✅
- yarn.lock 文件仓库映射已经更新 ✅
再执行,还是报错?!这就奇怪了...
根.npmrc 异常
排除掉项目层面的问题后,我查看了用户主目录的 .npmrc 文件,发现有这么一堆定义:
这是什么时候写的?删掉!
重新执行 yarn install
,依旧报错?!
再次查看 用户目录下的.npmrc
,上面的一坨定义又回来了?!! 这是什么神奇的操作... 😱
问题排查
借助 AI 给出的思路,出现这个问题的情况有以下几种:
1. 公司或团队的统一配置脚本
如果你是在公司环境下工作,有可能是公司的 IT 部门或团队的初始化脚本自动执行了这些配置。
❌ 这一点排除,目前架构部针对框架没有相关配置。
2. cnpm 或淘宝定制的 Node.js 安装包
你可能在过去某个时间点安装了 cnpm(npm 的一个中国镜像版本)或者使用了淘宝团队维护的 Node.js 安装程序。这些工具在安装时,为了方便用户,会全局性地配置 .npmrc 文件,将各种包的下载源都指向淘宝镜像。
❌ 这一点也排除,实际上我电脑上并没有安装过 cnpm。
3. 某些脚手架或开发工具
一些面向国内用户的脚手架工具或集成开发环境,在初始化项目或环境时,可能会有自动检测网络环境并配置最佳镜像源的功能。
所以排查的重点放在了第三点,应该是某些脚手架工具在安装依赖时写入了异常的源配置。
那么问题来了,怎么佐证呢?🕵️♂️
查找文件写入凶手
使用 Process Monitor (ProcMon) 进行实时监控
信息
Process Monitor 是微软官方提供的一款免费、强大的高级监视工具,可以实时显示文件系统、注册表和进程/线程的活动。简单来说,就是一个超级强大的「监控神器」!
操作步骤:
1. 下载 ProcMon
从微软官网下载 Process Monitor (Sysinternals Suite)。它是一个绿色软件,无需安装,解压后直接运行 procmon.exe
即可。
2. 设置过滤器 (Filter)
这是最关键的一步,我们只关心对 .npmrc 文件的写入操作。
- 打开 ProcMon 后,它会立即开始捕获大量事件。先点击菜单栏的「捕获」按钮(放大镜图标)停止捕获,或者按
Ctrl+E
。 - 点击菜单栏的 "Filter" -> "Filter..."(或者按
Ctrl+L
)打开过滤器设置窗口。 - 设置以下两个过滤条件,然后点击 "Add" 添加:
第一个条件: Path | is | C:\Users\你的用户名\.npmrc (请务必替换成你的实际路径) -> "Add"
第二个条件: Operation | is | WriteFile -> "Add"
设置好后点击 "OK" 保存。
3. 开始捕获和复现问题
- 清空当前的捕获列表(橡皮擦图标或按
Ctrl+X
)。 - 重新开始捕获(再次点击放大镜图标或按
Ctrl+E
)。 - 现在,执行那个你怀疑会修改文件的操作,比如在你的项目里运行
yarn
或npm install
。
4. 分析结果
执行 yarn install
,回到 ProcMon 窗口查看。 窗口中会列出所有对 .npmrc 文件的写入操作。Process Name
和 PID
这两列会明确告诉你,是哪个程序修改了文件。
日志结果:
"Time of Day","Process Name","PID","Operation","Path","Result","Detail"
"9:12:55.1136582","node.exe","17032","WriteFile","C:\Users\Administrator\.npmrc","SUCCESS","Offset: 0, Length: 2,310, Priority: Normal"
"9:12:56.1525239","System","4","WriteFile","C:\Users\Administrator\.npmrc","SUCCESS","Offset: 0, Length: 4,096, I/O Flags: Non-cached, Paging I/O, Synchronous Paging I/O, Priority: Normal"
通过日志发现,确实是 yarn install 时执行了脚本,写入了异常的源配置。真相大白!🎯
查看项目配置文件
接下来查看 package.json
,看看到底是哪个依赖包在作怪:
{
...省略其他
"scripts": {
"build": "taro build --type weapp",
"build:weapp": "taro build --type weapp",
"build:swan": "taro build --type swan",
"build:alipay": "taro build --type alipay",
"build:tt": "taro build --type tt",
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"dev": "npm run build:weapp -- --watch",
"dev:weapp": "npm run build:weapp -- --watch",
"dev:swan": "npm run build:swan -- --watch",
"dev:alipay": "npm run build:alipay -- --watch",
"dev:tt": "npm run build:tt -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch"
},
"author": "",
"license": "MIT",
"dependencies": {
"@tarojs/async-await": "1.3.38",
"@tarojs/cli": "1.3.38",
"@tarojs/components": "1.3.38",
"@tarojs/mobx": "1.3.38",
"@tarojs/mobx-h5": "1.3.38",
"@tarojs/mobx-rn": "1.3.38",
"@tarojs/rn-runner": "1.3.38",
"@tarojs/router": "1.3.38",
"@tarojs/taro": "1.3.38",
"@tarojs/taro-alipay": "1.3.38",
"@tarojs/taro-h5": "1.3.38",
"@tarojs/taro-swan": "1.3.38",
"@tarojs/taro-tt": "1.3.38",
"@tarojs/taro-weapp": "1.3.38",
"@types/classnames": "^2.2.10",
"classnames": "^2.2.6",
"core-decorators": "^0.20.0",
"dayjs": "^1.8.26",
"eslint": "^6.5.1",
"global": "^4.4.0",
"mirror-config-china": "^2.5.1",
"mobx": "4.8.0",
"nerv-devtools": "^1.4.0",
"nervjs": "^1.4.0",
"number-precision": "^1.3.2",
"url-search-params": "^1.1.0"
},
"devDependencies": {
"@tarojs/plugin-babel": "1.3.38",
"@tarojs/plugin-csso": "1.3.38",
"@tarojs/plugin-less": "1.3.38",
"@tarojs/plugin-uglifyjs": "1.3.38",
"@tarojs/webpack-runner": "1.3.38",
"@types/react": "16.3.14",
"@types/webpack-env": "^1.15.2",
"@typescript-eslint/eslint-plugin": "2.3.3",
"@typescript-eslint/parser": "2.3.3",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-jsx-stylesheet": "^0.6.11",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.6.1",
"eslint-config-alloy": "3.0.0",
"eslint-plugin-react": "7.16.0",
"gulp": "^4.0.2",
"stylelint": "9.3.0",
"stylelint-config-taro-rn": "1.3.38",
"stylelint-taro-rn": "1.3.38",
"typescript": "^3.8.3",
"yarn": "^1.22.4"
},
}
经过仔细排查,发现项目中有这么一个依赖包 mirror-config-china
。去 npm 仓库查看,发现它的作用是:
查看项目仓库源码,发现有这么一段配置:
// lib/config.js
const npmrc = {
'registry': 'https://registry.npmmirror.com',
'disturl': '{bin-mirrors}/node',
'chromedriver-cdnurl': '{bin-mirrors}/chromedriver',
'couchbase-binary-host-mirror': '{bin-mirrors}/couchbase/v{version}',
'debug-binary-host-mirror': '{bin-mirrors}/node-inspector',
'electron-mirror': '{bin-mirrors}/electron/',
'flow-bin-binary-host-mirror': '{bin-mirrors}/flow/v',
'fse-binary-host-mirror': '{bin-mirrors}/fsevents',
'fuse-bindings-binary-host-mirror': '{bin-mirrors}/fuse-bindings/v{version}',
'git4win-mirror': '{bin-mirrors}/git-for-windows',
'gl-binary-host-mirror': '{bin-mirrors}/gl/v{version}',
'grpc-node-binary-host-mirror': '{bin-mirrors}',
'hackrf-binary-host-mirror': '{bin-mirrors}/hackrf/v{version}',
'leveldown-binary-host-mirror': '{bin-mirrors}/leveldown/v{version}',
'leveldown-hyper-binary-host-mirror': '{bin-mirrors}/leveldown-hyper/v{version}',
'mknod-binary-host-mirror': '{bin-mirrors}/mknod/v{version}',
'node-sqlite3-binary-host-mirror': '{bin-mirrors}',
'node-tk5-binary-host-mirror': '{bin-mirrors}/node-tk5/v{version}',
'nodegit-binary-host-mirror': '{bin-mirrors}/nodegit/v{version}/',
'operadriver-cdnurl': '{bin-mirrors}/operadriver',
'phantomjs-cdnurl': '{bin-mirrors}/phantomjs',
'profiler-binary-host-mirror': '{bin-mirrors}/node-inspector/',
'puppeteer-download-host': '{bin-mirrors}',
'python-mirror': '{bin-mirrors}/python',
'rabin-binary-host-mirror': '{bin-mirrors}/rabin/v{version}',
'sass-binary-site': '{bin-mirrors}/node-sass',
'sodium-prebuilt-binary-host-mirror': '{bin-mirrors}/sodium-prebuilt/v{version}',
'sqlite3-binary-site': '{bin-mirrors}/sqlite3',
'utf-8-validate-binary-host-mirror': '{bin-mirrors}/utf-8-validate/v{version}',
'utp-native-binary-host-mirror': '{bin-mirrors}/utp-native/v{version}',
'zmq-prebuilt-binary-host-mirror': '{bin-mirrors}/zmq-prebuilt/v{version}',
'canvas-binary-host-mirror': '{bin-mirrors}/node-canvas-prebuilt/v{version}',
'canvas-prebuilt-binary-host-mirror': '{bin-mirrors}/node-canvas-prebuilt/v{version}',
'swc_binary_site': '{bin-mirrors}/node-swc'
};
这不就对上了!😅 并且可以发现,这个库已经于多年前就停止了更新,妥妥的「古董级」依赖包。
问题解决
找到问题原因后,我们就可以针对性地解决了。以下提供三种解决方案:
方案一:移除依赖包(推荐)
既然项目中使用的是 mirror-config-china
这个库,那我们直接移除它就行了。简单粗暴!
执行以下命令:
yarn remove mirror-config-china
方案二:在项目内创建 .npmrc 文件进行覆盖
如果你因为某些原因不能或不想移除那个包,可以在项目根目录下(和 package.json 同级)创建一个 .npmrc 文件。
利用「项目级配置文件优先级高于用户全局配置文件」这个特性,我们可以进行覆盖。
在项目根目录创建一个名为 .npmrc
的文件,在文件中写入你希望使用的官方源:
registry=https://registry.npmjs.org/
这样,即使 mirror-config-china
修改了你全局的 .npmrc,但只要你在这个项目里运行 yarn 或 npm,它们会优先使用项目内的配置。这是一种「隔离」策略。
警告
这种方式依然会使得全局的.npmrc被写入
方案三:设置全局 .npmrc 只读权限
将文件设置为只读权限,防止被修改。这是一种「防御性」策略。
macOS: chmod 400 ~/.npmrc
Windows: 找到 .npmrc 文件所在目录(一般为 C:\Users\你的用户名\.npmrc
),右键属性,将只读属性勾选。
警告
需要注意的是,设置为只读权限虽然拒绝了写入,但安装时会报错:
china: Command failed.
Exit code: 1
Command: node lib/install
Arguments:
Directory: D:\ruisi\node_modules\mirror-config-china
Output:
[Error: EPERM: operation not permitted, open 'C:\Users\Administrator\.npmrc'] {
errno: -4048,
code: 'EPERM',
此时你可以选择运行 yarn install --ignore-scripts
来跳过脚本执行。
总结
经过这次「侦探式」排查,我们成功找到了问题的根源。最佳的解决策略是:
- 移除项目中的
mirror-config-china
- 从根源解决问题 - 设置全局
.npmrc
只读 - 防止类似问题再次发生
这个案例告诉我们,有时候看似简单的问题背后可能隐藏着复杂的原因。当遇到奇怪的问题时,不要慌张,善用工具进行排查,总能找到真相!🔍
希望这篇文章能帮助到遇到类似问题的同学们。记住:工具在手,bug 不愁! 💪