你在本地跑 npm run build 一切正常,push 完发现 Vercel / Netlify / GitHub Actions 红了,错误是 Cannot find module 'some-lib' 或 SyntaxError: Unexpected token 或 error TS2307。这是 AI 辅助编码里最高频的一类”环境漂移”问题——Claude Code 或 Cursor 在你本地装了个新依赖、用了你本地有但 CI 没有的 Node 特性、或改动了某个被你的全局 git 配置忽略掉的文件。这篇拆 6 个最常见原因,给你一套 5 分钟内能定位并修复的流程。
常见原因
按命中率从高到低排序。
1. Lockfile 没 commit(最常见)
Agent 跑了 npm install some-lib 装了新依赖,package.json 进了 commit,但 package-lock.json / pnpm-lock.yaml / yarn.lock 没进。CI 上跑 npm ci 时报 “lock file out of sync”,或者跑 npm install 装了不同版本导致 API 不一致。
npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json or npm-shrinkwrap.json are in sync.
npm error Missing: some-lib@2.3.0 from lock file
如何判断:git log --name-only -1 看最近 commit,如果 package.json 变了但 lockfile 没变,就是这个。
2. 本地 Node 版本和 CI 不同
你本地是 Node 22,CI 默认跑 Node 18。Agent 用了 Array.prototype.toSorted()(Node 20+)或 ESM 顶层 await(Node 14.8+),本地能跑,CI 上 SyntaxError。
SyntaxError: Unexpected token 'await'
at wrapSafe (node:internal/modules/cjs/loader:1378:18)
如何判断:本地跑 node -v 和 CI log 里第一行的 Node 版本对比。差一个大版本就高度可疑。
3. AI 用了本地有但 CI 没装的全局工具
Agent 在 build 脚本里加了 tsx、esbuild、vite,假定全局可用。你本地有,CI 没装到 devDependencies 里,CI 报 command not found: tsx。
如何判断:在干净的 docker 里跑一遍 docker run --rm -v $(pwd):/app -w /app node:18 npm ci && npm run build,最快能复现 CI 失败。
4. 文件名大小写差异
macOS 和 Windows 文件系统默认不区分大小写,Linux(CI)严格区分。Agent 在 macOS 上写了 import Button from './button',但文件实际叫 Button.tsx,本地能解析,CI 报 Cannot find module './button'。
如何判断:git ls-files | grep -i 文件名 看实际文件名大小写,和 import 对比。
5. 环境变量本地有、CI 没有
Agent 加了 process.env.SOME_API_KEY,你本地 .env 里有这个值,CI 没设。build 时为 undefined,注入到 client 代码后运行时报 undefined is not a function。Astro / Next.js 在 build 期就会读 env,CI 直接挂。
如何判断:grep 最近 diff 里所有 process.env.XXX,对比 CI / Vercel / Netlify 的 env 设置面板。
6. AI 残留了 console.log 或开发期 mock
Agent 调试时在 next.config.js 或 astro.config.mjs 里塞了 mock 数据 / fixture 路径,本地有这些文件,CI 上没有,build 报 ENOENT: no such file or directory。
如何判断:git diff main -- '*.config.*' 看所有 config 文件改动。
最短修复路径
按收益排序。先做 step 1-2 通常能修 70%。
Step 1:本地用和 CI 一样的 Node 版本 + npm ci 复现
最快的第一步:
# 看 CI log 第一行确认 CI 用的 Node 版本,比如 18.19.0
nvm install 18.19.0
nvm use 18.19.0
rm -rf node_modules
npm ci # 注意:是 ci 不是 install
npm run build
如果本地能复现,下一步就好定位了。如果本地依然过,说明问题在 env vars 或 CI 镜像差异,跳到 Step 4。
Step 2:检查 lockfile 是否在最近 commit 里
git log --name-only -5 | grep -E '(package-lock|pnpm-lock|yarn.lock)'
如果最近 5 个 commit 都没动 lockfile 但 package.json 动了,立刻补提:
npm install # 重新生成 lockfile
git add package.json package-lock.json
git commit -m "chore: sync lockfile after AI dependency add"
Step 3:在 package.json 和 CI 里固定 Node 版本
package.json:
{
"engines": {
"node": ">=18.19.0 <19.0.0",
"npm": ">=10.0.0"
}
}
GitHub Actions:
- uses: actions/setup-node@v4
with:
node-version: '18.19.0'
cache: 'npm'
Vercel:项目设置 → General → Node.js Version 选具体版本。Netlify:在 netlify.toml 里加 [build.environment] NODE_VERSION = "18.19.0"。
加一个 .nvmrc 文件让本地自动切:
echo "18.19.0" > .nvmrc
Step 4:环境变量对账
把代码里所有 process.env.X 列出来,和部署平台对账:
grep -rEo 'process\.env\.[A-Z_]+' src/ | sort -u
对比 Vercel/Netlify env 面板。差一个就在面板里加上,然后触发 redeploy。注意:客户端可见的变量在 Next.js 里是 NEXT_PUBLIC_*,Astro 里是 PUBLIC_*,Vite 里是 VITE_*,前缀写错也会编译时为 undefined。
Step 5:在干净的 docker 里跑一遍
终极复现:
docker run --rm -it -v "$(pwd):/app" -w /app node:18.19.0 sh -c "
npm ci &&
npm run build
"
这模拟 CI 完全干净的 node_modules + 严格 Node 版本。如果这里都过,剩下的差异基本就是 CI 平台的 env vars 或缓存。
预防建议
- 在
package.json的engines、.nvmrc、CI workflow、部署平台四处都固定相同的 Node 版本 - 本地始终用
npm ci(而不是npm install)跑,这样 lockfile 漂移会立刻报错 - 在 CLAUDE.md /
.cursorrules里写:“装新依赖必须同时 commit lockfile,不要单 commit package.json” - pre-commit hook 加一条:
package.json改了就检查 lockfile 是否也改了 - import 路径强制小写或和文件名严格一致——ESLint 加
import/no-unresolved规则配合大小写检查 - 把
.env.example提交进 repo,CI 启动时校验所有必需 env vars 都已设置