一、故障现场:一个让人抓狂的报错
周六下午,我正美滋滋地在电脑前写一个企业内部工具。项目里既要安装公司私有 npm 包(这些包放在 Verdaccio 上),还要用到几个直接挂在私有 Git 仓库里的公共模块。我照着惯例先配好了 .npmrc,把 registry 指向公司的 Verdaccio 地址,然后执行 npm install。结果屏幕哗啦啦输出一堆红字,其中最重要的几句是:
# 执行 npm install 时,出现类似下面的错误
npm ERR! code E404
npm ERR! 404 Not Found - GET https://verdaccio.company.com/@company%2flogger
npm ERR! 404 Not Found - GET https://verdaccio.company.com/@company%2fui-kit
npm ERR! 404 Not Found - GET https://verdaccio.company.com/common-utils
npm ERR! 404 Not Found - GET https://verdaccio.company.com/@company%2fcommon-utils
npm ERR! ...
我看到这些立刻懵了:@company/logger 和 @company/ui-kit 明明已经发到 Verdaccio 上了,怎么会 404?更奇怪的是,common-utils 是从私有 Git 仓库拉取的,怎么也会跑到 Verdaccio 上去找?那感觉就像你明明把钥匙放在门口的花盆底下,结果你非要去敲邻居家的门问“钥匙在吗”,邻居当然一脸茫然。
为了把问题说清楚,先贴一下当时的 package.json,你们感受一下:
{
"name": "internal-tool",
"version": "1.0.0",
"private": true,
"dependencies": {
"@company/logger": "^1.2.0",
"@company/ui-kit": "^2.0.1",
"common-utils": "git+ssh://git@gitserver.internal/team/common-utils.git#v1.3.0"
}
}
注意看,common-utils 这个依赖的地址是 git+ssh://git@gitserver.internal/team/common-utils.git#v1.3.0,意思很明确:让 npm 直接从私有 Git 仓库里克隆这个模块,并且切到 v1.3.0 这个 tag 上。可是 npm 偏偏不这么干,非要把这个包名也当成 registry 上的普通包去 Verdaccio 查一遍,查到没有就直接抛 404。
这就是今天要聊的“包关联异常”的典型现场:Verdaccio 和私有 Git 仓库明明是两个独立的东西,但它们在同一个项目里待着的时候,会因为各种奇奇怪怪的原因互相搅和,最后让 npm install 彻底罢工。
二、先搞明白 Verdaccio 和私有 Git 仓库各自扮演什么角色
先说 Verdaccio。你可以把它理解成一个“公司内部的 npm 商店”。平时我们发布公司自己的 npm 包,就扔到这个商店里,同事安装的时候,只要把 registry 指到 Verdaccio,就能像从官方 npm 源下载一样,把这些内部包装到本地。它的存在让内部代码不会泄露到公网,同时也让包的安装速度快很多。
再说私有 Git 仓库。它是存放代码的地方,比如 GitLab、Gitea 或者自己搭的 Gogs。很多时候,有些模块还没有被正式做成 npm 包,或者为了改起来方便,大家就喜欢直接在 package.json 里写一个 git 地址,让 npm 去仓库里把代码拉下来。这本身是完全合法的用法,npm 支持 git+ssh、git+https、git+http 这些协议。
那么“集成”是什么意思?这里要说的不是把 Verdaccio 的存储目录塞进 Git 仓库,而是说一个项目里同时依赖两种来源的东西:来自 Verdaccio 的私有 npm 包,和来自私有 Git 仓库的模块。这两者在一起,本该是井水不犯河水,但现实里经常出现题目那种“包关联异常”。原因一般不是它们俩本身有仇,而是配置、权限、命名习惯这些细节出了问题。
理解了这一点,我们就能像侦探一样,按着线索一点点排查。
三、故障排查三板斧
遇到这种问题,千万别急着把锅甩给 Verdaccio 或者 Git 服务器。先按下面三个步骤来一遍,大部分情况都能水落石出。
3.1 第一板斧:检查 Verdaccio 的 uplinks 配置
Verdaccio 有一个功能叫 uplinks,它默认会连接官方的 npm 源。也就是说,当你在 Verdaccio 上找不到某个包的时候,它会去上游的 npmjs.org 帮你找,找到之后存在本地,下次就快了。这个功能本身很好,但如果你公司的网络不能访问外网,或者上游源配置错了,就会出现一种奇葩情况:本来该从 Git 仓库拉取的包,因为某些原因被当成普通包,Verdaccio 去上游找,找不到,就给前端返回一个 404 或者 500。
看一个常见的 config.yaml 片段:
# Verdaccio config.yaml 中的 uplinks 部分
uplinks:
npmjs:
url: https://registry.npmjs.org/
timeout: 10s
packages:
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs
注意这里 packages 规则里写了 proxy: npmjs。这意味着,所有没有被明确拒绝的包,Verdaccio 都会尝试去 npmjs 上找。如果你公司内网的 Git 仓库里的包名和公网上的某个包重名了,或者你的 npm 客户端因为某种原因把 git 依赖的包名也发给了 Verdaccio,那 Verdaccio 就会傻乎乎地往上游找,最后给你一个 404。排查方法很简单:先临时把 proxy: npmjs 去掉,重启 Verdaccio,再执行 npm install 看看。如果错误变了,那就说明问题出在这个代理关系上。
3.2 第二板斧:检查 Git 依赖的协议和权限
Git 依赖最常见的坑就在协议上。git+ssh 和 git+https 的差异很大。git+ssh 走的是 SSH 的 22 端口,需要你本地有对应的 SSH key,并且 key 已经加到了 Git 服务器上。git+https 走的是 443 端口,通常需要用户名密码,或者 token。
很多公司为了省事,会给开发者统一配置一个全局的 Git 配置,比如把 HTTPS 的凭证缓存下来。但 npm 在拉取 git 依赖的时候,用的不一定是你在终端里手动操作时的那些配置,它可能没有加载你的 SSH agent,或者没有读取全局的 .gitconfig。于是就会出现明明你手动 git clone 能成功,npm 却报权限错误。
看一个典型的错误输出:
# npm install 时出现的 Git 相关错误
npm ERR! fatal: Could not read from remote repository.
npm ERR!
npm ERR! Please make sure you have the correct access rights
npm ERR! and the repository exists.
npm ERR!
npm ERR! Error: Command failed: git clone --depth=1 -b v1.3.0 git+ssh://git@gitserver.internal/team/common-utils.git ...
这种问题一般和 Verdaccio 一点关系都没有,纯粹是 SSH key 或者 Git 服务器访问权限的问题。排查方法:在终端里手动执行同样的克隆命令,比如 git clone git@... 或 git clone ssh://...,看能不能成功。如果手动能成,npm 却不成,那就要检查环境变量 GIT_SSH_COMMAND 或者 .npmrc 里的相关配置。
3.3 第三板斧:检查 .npmrc 中的认证信息
很多人只知道在 .npmrc 里写 registry=...,但不知道里面还有其他讲究。尤其是当你使用了私有 Git 仓库 + Verdaccio 的组合,registry 这个配置会影响 npm 对所有依赖的解析方式。npm 在读取依赖的时候,会优先看 package.json 里写的是什么地址,如果写的是 git 地址,它理论上应该走 git;但如果你在 .npmrc 里设置了 always-auth=true,或者有其他的 @company:registry 之类的映射,就可能把 git 依赖也带偏。
我们来看一个错误的 .npmrc 示例:
# 错误的 .npmrc 示例
registry=https://verdaccio.company.com/
always-auth=true
@company:registry=https://verdaccio.company.com/
这个配置的意思很明确:所有包都去 Verdaccio;公司 @company 范围的包也去 Verdaccio;并且每次请求都要带认证信息。问题来了:如果你的 Git 仓库里的 package.json 里也写了一些依赖,比如它自身也依赖 common-utils,而 npm 在分析这个 git 依赖的 package.json 时,可能会尝试去 registry 上解析这些依赖。如果 always-auth=true,而你的 .npmrc 里又没有提供合法的 token,那就会触发一堆 401 认证错误。特别是当 Git 仓库的模块还没有发布到 Verdaccio 时,这个错误会非常误导人,看起来像是在说 Verdaccio 拒绝了你,其实只是 npm 在“多管闲事”。
四、常见故障类型与解决方法
排查三板斧只能帮你定位大概方向,但具体的问题五花八门。下面列几种最常见的故障类型,每一个都有对应的处理办法。
4.1 默认 registry 拦截了 git 依赖
这个就是开头我遇到的情况。明明 package.json 里写的是 git+ssh://...,但 npm 还是会先去 registry 上问一圈。尤其是当你用了某些 npm 版本,或者安装了第三方插件的时候,这种行为会被放大。
解决办法有两个:一是把 git 依赖的地址写得更加“显式”,在 package.json 里加上 "type": "git" 或者在协议前加上 git+,这能帮助 npm 识别;二是在项目根目录建一个 .npmrc,单独为某个包名指定 git 源。不过更靠谱的做法是使用 overrides 强制 npm 不要去看 registry。比如:
{
"name": "internal-tool",
"version": "1.0.0",
"private": true,
"dependencies": {
"common-utils": "git+ssh://git@gitserver.internal/team/common-utils.git#v1.3.0"
},
"overrides": {
"common-utils": {
"type": "git",
"source": "git+ssh://git@gitserver.internal/team/common-utils.git"
}
}
}
需要注意的是,overrides 字段在各 npm 版本中的行为略有差异,但整体上能帮你把依赖的来源“钉死”。
4.2 私有 Git 仓库中的 submodule 问题
有些同学喜欢在 Git 仓库里用 submodule 来管理公共代码。当 npm 从 Git 仓库拉取这个包时,它只会执行普通的 git clone,并不会自动初始化 submodule。如果那个模块里的代码引用了 submodule 里的文件,npm 就会报“文件不存在”或者“模块找不到”的错误。这种错误很容易被误认为是 Verdaccio 的锅,因为报错信息里可能会出现 Could not find module './submodule/x.js'。
处理方法很简单:在 package.json 里给 git 依赖加上 # 和分支/tag,然后让 npm 在安装后执行一个钩子,手动初始化 submodule。比如:
{
"scripts": {
"postinstall": "git submodule update --init --recursive"
}
}
这样在 npm install 完成后,会进入每个 git 依赖的目录,把 submodule 拉下来。
4.3 包名与仓库名不一致导致关联异常
这个坑特别隐蔽。你的私有 Git 仓库可能叫 team/awesome-utils,但里面 package.json 的 name 字段写的是 @team/utils。npm 在拉取 git 依赖时,会读取这个包名,然后和它的依赖树做关联。如果同一个项目里既有 @team/utils 来自 Verdaccio 的版本,又通过 git 引入了一个同名但不同版本的 @team/utils,npm 就会产生混乱,报出类似“Conflicting peer dependency”或者“ETARGET”的错误。
遇到这种情况,最推荐的做法是让 Git 仓库里的包名和你在 package.json 里引用的名字完全一致,并且明确指出 tag 或 commit。看一个修正后的依赖声明:
{
"dependencies": {
"@team/utils": "git+ssh://git@gitserver.internal/team/awesome-utils.git#v2.0.0"
}
}
当然,最好的办法是让 Git 仓库的 name 尽早改成和发布到 Verdaccio 时的名字一致,避免双份维护。
4.4 版本号错乱带来的迷惑
还有一个特别常见的情况:你在 package.json 里写的是 "common-utils": "^1.3.0",但你的 common-utils 是从 Git 仓库拉取的,Git 仓库里的 package.json 版本号是 2.0.0。此时 npm 会认为这个包不满足 ^1.3.0 的要求,于是尝试去 registry 上寻找 1.3.0 的版本,结果找不到,就把错误推到 Verdaccio 头上。
解决办法是:如果要从 Git 仓库拉取依赖,就不要在版本号前面加 ^ 或 ~,直接用 git+... 并带上 tag 或 commit。例如:
{
"dependencies": {
"common-utils": "git+ssh://git@gitserver.internal/team/common-utils.git#v1.3.0"
}
}
这样 npm 就不会去检查版本范围了,自然也不会去 registry 上碰壁。
五、完整的处置示例:从报错到成功
为了让大家看得更清楚,我把整个处理过程完整走一遍。下面的所有示例都基于 Node.js + npm 这一套技术栈,避免混入其他工具干扰视线。
假设项目的基本结构长这样:
internal-tool/
├── package.json
├── .npmrc
└── node_modules/ (安装后生成)
最初的 package.json 是这样的:
{
"name": "internal-tool",
"version": "1.0.0",
"private": true,
"dependencies": {
"@company/logger": "^1.2.0",
"@company/ui-kit": "^2.0.1",
"common-utils": "git+ssh://git@gitserver.internal/team/common-utils.git#v1.3.0"
}
}
最初的 .npmrc 是这样的:
registry=https://verdaccio.company.com/
@company:registry=https://verdaccio.company.com/
执行 npm install 后,报了 404 错误。我们按照三步骤排查。
第一步,检查 Verdaccio 的 uplinks。打开 config.yaml,发现 proxy: npmjs 存在,但追问历史,发现公司内网其实访问不了外网。于是暂时把 proxy 去掉,重启 Verdaccio,再执行 npm install。这时 404 变成了 403,错误信息变成了:
# 修改 uplinks 之后的错误
npm ERR! code E403
npm ERR! 403 Forbidden - GET https://verdaccio.company.com/common-utils
这说明 Verdaccio 确实收到了对 common-utils 的请求,而且因为去不了上游,或者没有这个包,就返回了 403。问题在 npm 客户端为什么会把 common-utils 这个 git 依赖发给 Verdaccio。
第二步,检查 Git 依赖的协议和权限。我在终端手动执行:
# 手动克隆测试
git clone --depth=1 -b v1.3.0 git@gitgitserver.internal:team/common-utils.git /tmp/test-common-utils
发现可以正常克隆,说明 SSH 权限没有问题。
第三步,检查 .npmrc。发现 @company:registry 这个配置可能会让 npm 把所有以 @company 开头的包都指向 Verdaccio,但 common-utils 并不是 @company 开头,理论上不受影响。那么为什么 npm 还会去请求 Verdaccio 呢?
后来我注意到,common-utils 的 Git 仓库里的 package.json 写了一个奇怪的 publishConfig,内容是这样的:
{
"name": "common-utils",
"version": "1.3.0",
"publishConfig": {
"registry": "https://verdaccio.company.com/"
}
}
这就好理解了——npm 在安装 git 依赖时,会读取这个依赖的 package.json,发现它指定了 publishConfig.registry,于是 npm 就尝试用这个 registry 去解析它的其他依赖或者元数据。一旦 Verdaccio 上没有这个包,就直接报错。这个问题不是 Verdaccio 的错,也不是 Git 仓库的错,而是包里的 publishConfig 把两边的角色搞混了。
我将 common-utils 里的 publishConfig 去掉(或者改成空对象),然后重新运行:
# 清理缓存后重新安装
npm cache clean --force
npm install
虽然 npm cache clean --force 这个命令已经不太推荐,但在这个场景下能起到从零开始的效果。这次安装成功,没有报错,node_modules 里有了 common-utils。
为了以后不再遇到这种问题,我还做了一个保险措施:在项目的 .npmrc 里显式地把 git 协议和 registry 的边界划清楚:
# 最终修正的 .npmrc
registry=https://verdaccio.company.com/
@company:registry=https://verdaccio.company.com/
# 让 git 依赖走本地的 Git,而不是 registry
legacy-peer-deps=true
legacy-peer-deps 虽然主要是解决 peerDependencies 问题的,但在某些版本中也能放宽 npm 对依赖源的检查。不过这个选项不是根本解决办法,真正的根本办法是保持依赖声明足够清晰,并且不要让 Git 仓库里的包包含任何 publishConfig。
六、如何避免再踩坑
经历了这一通折腾,我发现大部分“包关联异常”其实都是可以提前避免的。下面这几条经验,希望能帮你省下几个周六下午。
第一,统一依赖来源。同一个包,要么走 Verdaccio,要么走私有 Git 仓库,不要两个都占。如果你已经发布到了 Verdaccio,就安心用 "common-utils": "^1.3.0";还没有发布,就用 git+ssh://... 并固定到 tag。千万不要在 package.json 里写 "common-utils": "^1.3.0",然后又偷偷在 overrides 里改成 git 地址,这样会让所有维护者精神分裂。
第二,检查包名和版本号。Git 仓库里的 package.json 的 name 和 version 字段,要和你在依赖里写的一致。不写版本范围符(^、~),直接用 #v1.3.0 指定 tag。如果仓库名和包名不同,尽量给 Git 仓库加一个 package.json 的子目录,或者把仓库名改过来。
第三,让 Verdaccio 和 Git 仓库各司其职。Verdaccio 只管 npm 包的分发,Git 仓库只管源码管理。如果你的公司内网无法访问外网,务必把 Verdaccio 的 uplinks 关掉或者改成内部镜像,否则一个普通的 404 会浪费你半小时。同时,不要在私有 npm 包的 publishConfig 里写任何外部 registry 地址,除非你明确知道自己在干什么。
第四,处理好认证信息。对于 git+ssh,推荐在 ~/.ssh/config 里给内网 Git 服务器配置一个别名,比如:
# ~/.ssh/config 片段
Host gitserver
HostName gitserver.internal
User git
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
然后依赖地址写成 git+ssh://gitserver/team/common-utils.git,这样 npm 克隆时会走这个配置,不容易迷路。对于 git+https,把 token 写入全局 Git 凭证,或者使用 ~/.npmrc 中的 //gitserver.internal/:_authToken= 方式,但要注意 token 不要泄露。
第五,善用 npm ls 和 npm explain。当依赖关系混乱时,先用这两个命令看看到底是谁引用了谁。比如:
# 查看 common-utils 的依赖树
npm ls common-utils
# 查看为什么 common-utils 会被解析成某个版本
npm explain common-utils
这两条命令会输出非常清晰的关联链路,能直接帮你定位是哪一条依赖规则导致 npm 去向 Verdaccio 要 git 包。
七、文章总结
Verdaccio 和私有 Git 仓库本身都是很靠谱的工具,但把它们放在同一个项目里时,就像两个都很能干的人一起合作,如果不提前商量好各自的分工,就容易互相递错东西。今天我们通过一个 npm install 报 404 的场景,一步步排查出了三个常见的故障原因:uplinks 代理影响了包的查找、Git 依赖协议或权限不对、.npmrc 和 publishConfig 把 npm 带到了错误的源头。接着,我们列举了四种故障类型,并给出了对应的修正示例。最后,我用一个完整的处置示例展示了从报错到成功的全过程,也总结了避免踩坑的几条实用建议。
其实这类问题的核心逻辑很简单:npm 在安装依赖时,会严格执行 package.json 里的声明。你要么给它一个完整的 git 地址,要么给它一个 registry 上的版本。最怕的就是两边都沾一点,结果两边都找不到。下次再遇到 npm install 报错,别慌,按照“uplinks → Git 权限 → npmrc 认证”的顺序走一遍,多半能解决。要是还解决不了,就把 npm cache clean --force 和 node_modules 删除重装一遍——虽然不是治本的方法,但有时候“重启大法”在 Node 世界里一样管用。
希望这篇内容能帮你少踩几个包关联的坑。老老实实把依赖来源管清楚,Verdaccio 和私有 Git 仓库就能成为你工程化路上的左膀右臂,而不是让你周六下午抓耳挠腮的冤家。
评论
围绕“Verdaccio与私有Git仓库集成时包关联异常的故障处理”参与讨论