freeCodeCamp.org

How to Diagnose vite: not found After npm ci in a Container Build

8.5内容质量
How to Diagnose vite: not found After npm ci in a Container Build

TL;DR · AI 摘要

解决容器构建中vite缺失问题需检查npm ci安装、修正lockfile中的私有仓库URL并添加构建校验。

核心要点

  • npm ci失败会导致node_modules为空,后续构建步骤无法找到vite可执行文件
  • package-lock.json中resolved字段存储了内部代理服务器的私有URL
  • 修改lockfile中所有私有仓库URL前缀并添加构建前依赖校验可彻底修复问题

结构提纲

按章节快速跳转。

  1. 描述容器构建中vite缺失问题的典型场景和影响范围

  2. 解释npm ci在私有代理环境下生成的lockfile导致构建失败的机制

  3. 通过检查node_modules目录和构建日志确认npm ci执行状态

  4. 定位并修改lockfile中私有仓库URL前缀为公共仓库地址

  5. 在Dockerfile中添加依赖完整性校验步骤防止类似问题

  6. 通过依赖树分析和信任边界检查确认修复方案有效性

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • 诊断vite缺失问题
    • 失败原因
      • 私有代理URL残留
      • npm ci异常终止
    • 解决方案
      • 检查安装状态
      • 修正URL前缀
      • 添加构建校验

金句 / Highlights

值得收藏与分享的关键句。

#npm#容器构建#Vite#Docker
打开原文

如何诊断容器构建中 npm ci 后出现 vite: not found 的问题

2026年10月21日

/

#npm

Ahmed Tariq

我最近参与了一个项目,该项目中的 Node.js 应用使用 Vite 编译前端。在浏览器托管的开发环境中运行正常,但在迁移到 Azure 容器注册表后,镜像构建失败。构建过程中在依赖项安装失败后报告了缺失的 Vite 可执行文件。

容器构建最终出现了一个简单错误:

code
sh: 1: vite: not found

Vite 已经出现在 devDependencies 中。构建阶段在调用构建脚本前已运行了 npm ci。Dockerfile 看起来普通,因此最初的解释都指向依赖项选择或 NODE_ENV。

缺失的二进制文件来自更早的失败。npm ci 尝试从仅存在于浏览器开发环境中的包主机下载数百个 tarball 文件。它打印了 "Exit handler never called!",使 node_modules 变为空,且未向 Docker 提供明确的失败状态。后续层级到达 npm run build 时报告了第一个缺失的可执行文件。

锁文件将 npm 指向了一个不可达的主机。本教程将展示如何检查安装是否完成、定位受影响的 tarball URL、并在构建应用前检查依赖项层级。修复方案包括更改公共包的 URL 前缀并添加构建检查。包版本、完整性值、应用代码和 Vite 配置保持不变。

目录

  • 开始前的准备
  • 构建如何到达失败状态
  • 步骤 1:检查 npm ci 是否安装了任何内容
  • 步骤 2:查找锁文件将下载包的位置
  • 步骤 3:仅重写已知的公共包 URL 前缀
  • 步骤 4:在每次独立安装中应用防护措施
  • 步骤 5:验证依赖树和信任边界
  • 参考资料

开始前的准备

您需要一个包含 package.json 的 Node.js 项目、版本 2 或 3 的 package-lock.json、容器构建日志以及 Docker。示例使用了受支持的 Node 24 镜像和一个将构建输出写入 dist 的 Vite 项目。请检查您自己的依赖项兼容性和输出目录。

在更改任何 URL 之前,请确认受影响的包来自公共或私有注册表。下面的 .invalid 主机名代表原始内部主机。在审查它提供的包后,再调整该前缀。

构建如何到达失败状态

该应用最初来自浏览器托管的开发环境,后来迁移到 Azure 容器注册表构建。开发平台充当公共 npm 包的代理。当它重新生成 package-lock.json 时,会在 resolved 字段中存储自己的内部 tarball URL。

失败跨越了两个环境:

  • 在开发环境中,npm 可以访问包代理。锁文件保留了这些代理 URL。
  • Azure 容器注册表镜像构建器接收到相同的锁文件,但无法解析代理主机名。
  • 依赖项安装打印了 "Exit handler never called!" 并使 node_modules 变为空。其失败状态未阻止此次事件中的下一个 Docker 层。
  • 下一层数运行 npm run build,该操作调用了 Vite。由于未安装 Vite 可执行文件,构建报告了 "vite: not found"。

以下检查将从缺失的可执行文件反向追踪到包位置,然后修复 URL 并为每次独立安装添加防护。

图1。容器构建报告了链中的最后一次失败。

步骤 1:检查 npm ci 是否安装了任何内容

该项目使用了 npm ci,这是可重复容器构建的正确命令。npm 的文档指出,该命令在开始安装之前会删除现有的 node_modules 目录。它还会将锁定文件视为冻结状态,而不是更新它。

这些特性使这次失败异常明显。构建无法回退到早期本地安装的包。一旦 tarball 下载失败,依赖项目录中就没有有用的内容。

Dockerfile 将依赖项安装与源代码编译分离开来。步骤 3 将比较原始序列与修订后的构建。

稍后的 "vite: not found" 消息并未显示 npm 是否安装了该包。从在相同环境中失败的依赖项目录开始:

code
test -d node_modules && find node_modules -mindepth 1 -maxdepth 1 | head
test -x node_modules/.bin/vite

在此构建中,第一个检查未发现已安装的包。第二个检查确认 Vite 可执行文件缺失。可执行文件缺失也可能是因为省略了开发依赖项,因此在更改 NODE_ENV 或包清单之前检查目录。

安装成功后,目录列表应显示包名称。test -x 命令不会输出任何内容。立即之后,检查其退出状态:

code
echo $?

对于缺失的 Vite 可执行文件,预期结果为:

code
1

结果为 0 表示可执行文件存在且可执行。

检查点:空的依赖项目录指向安装步骤。如果存在其他包但缺少 Vite,也要检查安装是否省略了开发依赖项。这些命令在不更改目录的情况下检查依赖项目录。

步骤 2:确定锁定文件将从何处下载包

一个通用条目如下所示:

code
{
  "node_modules/example-package": {
    "version": "1.2.3",
    "resolved": "http://packages.internal.invalid/npm/example-package/-/example-package-1.2.3.tgz",
    "integrity": "sha512-<subresource-integrity-value>"
  }
}

上面的 .invalid 主机名替换了真实的内部主机名。结构与历史锁定文件匹配。

npm 将 resolved 文档为获取包的位置。对于注册表包,该值可以是 tarball URL。因此,构建运行器上配置的注册表并未解决此事件。锁定文件已经告诉 npm 从何处获取每个 tarball。

我统计了历史文件中受影响的条目。在 1,038 个依赖项条目中,528 个携带了内部主机名。每个都指向在外部构建环境中无法解析的主机。

npm 的调试输出指向了更早的失败。主机名失败出现在 "Exit handler never called!" 之前。这个顺序很重要。退出处理程序消息描述了 npm 如何结束,而下载失败解释了为什么依赖项树为空。

我将锁定文件作为数据进行检查,而不是搜索单个包名。这个小脚本按主机名对每个 HTTP resolved 值进行分组:

code
const fs = require('node:fs')
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'))
const hosts = {}

for (const entry of Object.values(lock.packages ?? {})) {
  if (!entry.resolved?.startsWith('http')) continue
  const host = new URL(entry.resolved).host
  hosts[host] = (hosts[host] ?? 0) + 1
}

console.table(hosts)

该清单显示问题影响了528个条目而非一个损坏的tar包。它也明确了修复边界。resolved中记录的包位置需要修改,而版本号和完整性值保持不变。如果主机列表包含私有仓库,请在此处停止并确认它提供哪些包,然后再进行任何重写操作。

历史清单包含如下行。表格使用了示例中的替换主机名:

主机

解析后的URL

code
packages.internal.invalid

528

您的主机名和数量取决于您的锁定文件。此清单统计HTTP tar包位置,因此总数可能与依赖项条目数量不同。

检查点:识别无法到达的主机并确认其哪些包来自公共npm仓库。在选择替换前缀之前记录受影响数量。仅凭主机名数量无法确定重写这些URL是安全的。

第3步:仅重写已知的公共包URL前缀

重新生成锁定文件可能会将仓库修复与依赖项更新混合。保持包版本和完整性值不变可以使修复更容易审查。

该应用中受影响的包均来自公共npm仓库。开发平台对其进行了代理并保留了完整性值。历史修复在容器安装之前替换了其URL前缀。

对于本操作指南,请将以下内容保存为rewrite-lockfile.cjs。它仅修改具有批准前缀的resolved字段。在写入文件前,它会检查旧主机上缺失的完整性值和意外路径:

code
const fs = require('node:fs')

const oldPrefix = 'http://packages.internal.invalid/npm/'
const newPrefix = 'https://registry.npmjs.org/'
const oldHostname = new URL(oldPrefix).hostname
const filename = 'package-lock.json'
const lock = JSON.parse(fs.readFileSync(filename, 'utf8'))

if (!lock.packages || typeof lock.packages !== 'object') {
  throw new Error('Expected a lockfile with a packages object (version 2 or 3)')
}

let rewritten = 0
for (const entry of Object.values(lock.packages)) {
  if (typeof entry.resolved !== 'string') continue
  if (!entry.resolved.startsWith(oldPrefix)) continue
  if (!entry.integrity) throw new Error('An affected package has no integrity value')
  entry.resolved = newPrefix + entry.resolved.slice(oldPrefix.length)
  rewritten += 1
}

for (const entry of Object.values(lock.packages)) {
  if (typeof entry.resolved !== 'string') continue
  if (!entry.resolved.startsWith('http')) continue
  if (new URL(entry.resolved).hostname === oldHostname) {
    throw new Error('The old hostname remains under an unexpected URL prefix')
  }
}

fs.writeFileSync(filename, JSON.stringify(lock, null, 2) + '\n')
console.log(`Rewrote ${rewritten} resolved URLs`)

当依赖项阶段运行此脚本时,其输出应与使用批准前缀的URL数量匹配。用于检查本教程的小型测试项目输出:

code
Rewrote 40 resolved URLs

该测试项目与历史应用是独立的,历史应用有528个受影响条目。请使用您自己的清单作为预期数量。已修复文件的计数为零是正常的。如果未修复文件的计数为零,请检查前缀。

该脚本不会确认包是否为公共包。需要单独确认。npm的完整性检查随后会验证下载的字节是否与锁定文件匹配。

使用 .dockerignore 文件将本地依赖项和配置文件排除在构建上下文之外:

code
node_modules
dist
.git
.env*
.npmrc

此示例安装公共包。私有仓库需要自己的可访问端点和构建时的安全认证。

下方第一部分展示原始 Dockerfile 中受影响的指令,第二部分是完整的教程构建。它使用 Node 24 和受保护安装,因此需要检查与您应用程序的兼容性。

之前:原始安装和构建流程

code
COPY package.json package-lock.json ./
RUN npm ci

COPY . .
RUN npm run build

之后:URL 重写和依赖项检查

使用两个构建阶段,使源代码变更不会使依赖项安装阶段失效:

code
FROM node:24-slim AS dependencies
WORKDIR /app

COPY package.json package-lock.json rewrite-lockfile.cjs ./
RUN node rewrite-lockfile.cjs \
 && npm ci --include=dev \
 && test -x node_modules/.bin/vite

FROM node:24-slim AS build
WORKDIR /app

COPY . .
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=dependencies /app/package-lock.json ./package-lock.json
RUN npm run build

修订后的 Dockerfile 明确划分了安装的每个环节:

  • URL 重写在 npm ci 下载包之前替换已批准的公共包前缀。
  • --include=dev 明确选择安装开发依赖项。
  • Vite 可执行文件检查会在安装遗漏 Vite 时停止依赖项阶段。
  • 独立的依赖项阶段在应用程序构建阶段复制文件和编译源代码之前先安装包。

第二阶段在应用程序源代码之后复制修复后的锁文件。否则 COPY . . 会将原始锁文件重新放入构建镜像。.dockerignore 还能防止本地 node_modules 覆盖第一阶段的依赖项。

--include=dev 即使构建环境设置 NODE_ENV=production 也会包含 Vite 作为开发依赖项。此镜像是构建环境,因此需要编译器和构建工具。选择与项目兼容的受支持 Node 版本。

历史锁文件对比发现 528 个已更改字段,均为 resolved 字段。未发现版本或完整性字段变更,也没有剩余的内部主机名。上述示例脚本也保留了无关字段,尽管 JSON 格式可能会变化。

图 2. 可移植性修复在保留依赖项身份的同时更改了包位置。

可执行文件检查会在 Vite 仍然缺失时使依赖项阶段失败。Docker 会在该阶段停止,而不是继续执行应用程序构建。

检查点:将重写次数与库存进行核对,确认已移除已批准的旧主机名,并比较修复前后版本和完整性值。依赖项阶段必须完成 npm ci --include=dev 和 Vite 可执行文件检查。第 5 步构建镜像并验证应用程序输出。

第 4 步:在每次独立安装中应用保护机制

持续集成作业和容器镜像构建器执行了独立安装。修复工作流运行器并未改变 Dockerfile 内部的 npm ci。镜像构建接收了原始仓库文件,并在另一个环境中创建了自己的依赖项层。

重写操作应紧随依赖文件进入安装环境之后。Docker 的缓存指南建议在复制频繁更改的源文件之前先安装依赖项。在示例中,依赖项阶段设置了这一边界。仅源代码更改时,会重新构建应用程序阶段。

在每个独立安装之前应用相同的保护机制:

  • 持续集成类型检查安装
  • 容器构建安装
  • 多阶段 Dockerfile 中的任何运行时阶段安装

图 3。容器构建内部的安装不会自动修复工作流问题。

这种放置方式也避免了误导性的部分修复。否则类型检查可能通过,而镜像构建失败,因为两个任务在独立安装中使用了相同的锁文件。

第 5 步:验证依赖树和信任边界

此次修复是安全的,因为受影响条目代表的是通过透明开发平台代理的公共 npm 包。完整性字符串仍然存在,且重写操作未更改任何包版本。

对于私有包、修补过的包或在相同包版本下提供不同字节的注册表,这种替换将是不安全的。可移植构建应将这些包保留在可访问的私有注册表中,并为该注册表配置身份验证。

在将此模式应用到其他地方之前,我会清点所有已解析的主机名,并将公共包与私有包区分开。然后我会比较更改前后包版本和完整性值。如果存在未经批准的主机名,构建应失败。

一个 npm CLI 问题在从内部注册表下载包超时后记录了相同的“Exit handler never called!”消息。该消息本身无法识别包位置。在此事件中,tarball URL 和失败的主机名确定了原因。

构建镜像并验证输出:

code
docker build --progress=plain -t vite-install-check .
docker run --rm vite-install-check sh -c \
  'test -x node_modules/.bin/vite && test -s dist/index.html'

构建必须通过 npm ci、Vite 可执行文件检查和 npm run build。最后的命令检查生成镜像中的预期 Vite 输出。如果项目使用不同目录,请根据需要调整输出检查。本示例中的镜像会编译应用程序。如果需要生产网络服务器,请选择独立的服务阶段。

成功的最小示例不会验证现有应用程序中的每个依赖项。针对您计划发布的项目运行相同的构建。当缺少工具时,在更改构建工具之前检查安装结果及其早期错误。

参考资料

  • npm 文档:package-lock.json 字段
  • npm 文档:npm ci
  • npm CLI 问题:注册表超时后未调用退出处理程序
  • Docker 文档:优化构建缓存
  • Node.js 发布状态
  • 用于创建图表的图表库

合作伙伴推荐课程

(在新标签页中打开)

广告

  • Udemy 上的《Understanding NPM - Node.js Package Manager》Bogdan Stashchuk 4.6(1,808 条评分) 2小时20分钟 初学者 证书 查看课程(在新标签页中打开)
  • Skillshare 上的《Introduction to NodeJS: Back-End JavaScript》Keegan Francis 3.7(32 条评分) 36分钟 初学者 查看课程(在新标签页中打开)

更多 npm 课程请访问 Class Central

→

阅读更多文章。

如果这篇文章对你有帮助,请分享它。 免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发者。立即开始