Skip to content

谁动了我的 .npmrc?一次由“淘宝源”引发的诡异事件排查

前言

2022 年 5 月,淘宝源发布了一个重要公告: 淘宝镜像源地址由 registry.npm.taobao.org 正式替换为 registry.npmmirror.com
2024 年 1 月 22 日,registry.npm.taobao.org 的 SSL 证书正式过期,使用该源的方法彻底失效。

按理说,前端开发同学只需要执行下面的命令替换更新源地址即可:

bash
npm config set registry https://registry.npmmirror.com

或者全局替换:

bash
npm config set registry https://registry.npmmirror.com --global

看起来很美好,起初我也是这么以为的... 然而现实总是比想象复杂一些 🤔

场景重现

某一天,有个老项目的客户找过来要做功能迭代。走完前置流程,进入开发阶段:

  • 拉取git代码:git clone xxxxx.git
  • 安装依赖:yarn install

然后控制台无情地报错了:

bash
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 文件,发现有这么一堆定义:

KooTyQm99htd

这是什么时候写的?删掉!

重新执行 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" 添加:
plain
第一个条件: Path | is | C:\Users\你的用户名\.npmrc (请务必替换成你的实际路径) -> "Add"
第二个条件: Operation | is | WriteFile -> "Add"

设置好后点击 "OK" 保存。

3. 开始捕获和复现问题

  • 清空当前的捕获列表(橡皮擦图标或按 Ctrl+X)。
  • 重新开始捕获(再次点击放大镜图标或按 Ctrl+E)。
  • 现在,执行那个你怀疑会修改文件的操作,比如在你的项目里运行 yarnnpm install

4. 分析结果

执行 yarn install,回到 ProcMon 窗口查看。 窗口中会列出所有对 .npmrc 文件的写入操作。Process NamePID 这两列会明确告诉你,是哪个程序修改了文件。

日志结果:

log
"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,看看到底是哪个依赖包在作怪:

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 仓库查看,发现它的作用是:

sY5IQXtswAKp

查看项目仓库源码,发现有这么一段配置:

javascript
// 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 这个库,那我们直接移除它就行了。简单粗暴!

执行以下命令:

bash
yarn remove mirror-config-china

方案二:在项目内创建 .npmrc 文件进行覆盖

如果你因为某些原因不能或不想移除那个包,可以在项目根目录下(和 package.json 同级)创建一个 .npmrc 文件。

利用「项目级配置文件优先级高于用户全局配置文件」这个特性,我们可以进行覆盖。

在项目根目录创建一个名为 .npmrc 的文件,在文件中写入你希望使用的官方源:

bash
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 来跳过脚本执行。

总结

经过这次「侦探式」排查,我们成功找到了问题的根源。最佳的解决策略是:

  1. 移除项目中的 mirror-config-china - 从根源解决问题
  2. 设置全局 .npmrc 只读 - 防止类似问题再次发生

这个案例告诉我们,有时候看似简单的问题背后可能隐藏着复杂的原因。当遇到奇怪的问题时,不要慌张,善用工具进行排查,总能找到真相!🔍

希望这篇文章能帮助到遇到类似问题的同学们。记住:工具在手,bug 不愁! 💪