Windows 上 Next.js standalone + pnpm:一次「构建成功却把 next 删空」的踩坑记录
前言
最近本地开发时经常出现这种循环:
pnpm build成功- 再跑
pnpm dev直接炸 - 删掉
node_modules重装才能恢复 - 下次再 build,又重来一遍
搞得我每次把node_modules删除重装然后又删了重装,最后忍受不不了的查一下原因解决根本问题
报错通常是:
text
Error: Cannot find module 'next/dist/pages/_app'
或更狠一点:
text
Error: Cannot find module '.../node_modules/next/dist/bin/next'
第一反应会以为依赖没装好、pnpm 坏了、磁盘坏了。 真正原因更离谱:Next.js 在清理 standalone 产物时,顺着 symlink 把真实的 node_modules 删空了。
现象复现
环境大致如下:
- OS:Windows
- 包管理器:pnpm
- Next.js:14.x(例如
14.1.4) next.config中开启了:
js
output: "standalone"
复现步骤:
bash
pnpm install
pnpm build # 成功
pnpm dev # 失败:找不到 next/dist/pages/_app
或者:
bash
pnpm build
pnpm build # 第二次构建也可能直接把依赖删坏
检查现场会发现一个很反直觉的状态:
node_modules/next这个目录/junction 还在- 但里面的文件几乎被删光(文件数可能变成
0)
所以不是「模块解析路径错了」,而是 包内容真的没了 。
这也解释了为什么只能重装 node_modules。
standalone 是干什么的?
output: "standalone" 是 Next.js 的生产部署模式。
开启后,next build 会额外生成:
text
.next/standalone/
这个目录是一份精简可运行产物 :带上运行所需的最小依赖,适合 Docker 等场景,不必把完整 node_modules 打进镜像。
典型启动方式:
bash
node .next/standalone/server.js
对本项目来说:
- 本地
pnpm dev不需要 standalone - Docker 生产镜像 依赖 standalone(例如用
proxy-server.js拉起.next/standalone/server.js)
问题就出在:为了生产部署开了 standalone,本地 Windows 上每次 build 也会生成它,随后触发清理逻辑的副作用。
根因分析
1. pnpm 的依赖结构依赖链接
pnpm 不会像 npm 那样把包完整复制到扁平 node_modules,而是通过:
- content-addressable store
- hardlink / symlink / junction
把包链接到项目里。
2. standalone 会把依赖「链接」进产物目录
生成 .next/standalone/node_modules 时,Windows + pnpm 下经常会出现:
text
.next/standalone/node_modules/next → 指向真实的 node_modules/.../next
也就是说,standalone 里的很多目录,并不是独立拷贝,而是指回真实依赖。
3. 清理时错误地「递归删除」了链接目标
下一次执行:
next build(清理旧.next/ standalone)- 或某些情况下的
next dev
Next 会尝试删掉旧的 .next/standalone。
关键问题是:递归删除时没有正确处理目录 symlink/junction,于是:
text
删 .next/standalone/node_modules/next
↓ 跟随链接
把真实 node_modules 里的 next(甚至 react 等)内容删空
于是你看到:
- junction 还在
- 文件没了
next自己把自己删了
Linux / macOS / CI(通常在 Linux 容器)往往不容易踩到,所以会出现「同事都没事,只有我 Windows 本机天天重装依赖」。
官方有没有修?
有,但不是你现在这个老版本能直接吃到的。
修复思路就是:删除时避免顺着目录 symlink 把真实包内容清掉。
但如果你还在 next@14.1.4 这类版本,这个修复并不在其中。
要官方修复生效,需要升级到包含该 PR 的较新 Next(大致在 15.4+ / 含修复的 canary)。
对存量业务来说,仅仅为了这个 bug 立刻大版本升级,往往成本偏高。
所以短期用 workaround 更务实。
实用解决方案
方案 A:启动前先删掉危险目录(推荐短期方案)
在 dev / build 前主动删除 .next/standalone,避免 Next 自己「跟链路删除」。
核心脚本示例:
js
// scripts/clean-standalone.mjs
import { existsSync, rmSync } from "node:fs";
import { join } from "node:path";
const standaloneDir = join(process.cwd(), ".next", "standalone");
if (existsSync(standaloneDir)) {
rmSync(standaloneDir, { recursive: true, force: true });
console.log("[clean-standalone] removed .next/standalone");
}
package.json:
json
{
"scripts": {
"clean:standalone": "node ./scripts/clean-standalone.mjs",
"dev": "node ./scripts/clean-standalone.mjs && next dev",
"build": "node ./scripts/clean-standalone.mjs && next build"
}
}
注意:
- 清理发生在
next build之前 next build成功后仍会重新生成 standalone- 因此对 Docker / CI 打包通常没有破坏性影响
唯一要注意:如果你本机「刚 build 完 → 立刻 pnpm dev」,dev 会先清掉 standalone。
需要产物时,先打包/拷贝,再开 dev。
方案 B:长期升级 Next
升级到包含官方修复的版本后,再验证:
bash
pnpm build
pnpm build
pnpm dev
确认不再误删依赖,再考虑去掉清理脚本。
方案 C:本地关闭 standalone(架构更干净,但要拆配置)
如果本地开发不需要 standalone,可以:
- 本地
next.config不开启output: "standalone" - 只在 Docker / 生产配置中开启
这样从根上减少本机触发面。
代价是要维护「本地配置 / 生产配置」的差异,并确保 CI/Docker 仍能正确产出 standalone。
排查清单(下次再遇到时)
- 看报错是不是
next/dist/pages/_app或next/dist/bin/next - 检查
node_modules/next是否「目录在、文件空」 - 确认
next.config是否开了output: "standalone" - 确认是否 Windows + pnpm
- 先删
.next/standalone,再决定是否需要重装依赖 - 若已被删空,只能
pnpm install(必要时强制重建被清空的包)恢复
一句话总结
这不是你依赖没装好,也不是 pnpm 的锅,更不是 Windows 故意的------ 这是 Next.js 在 Windows + pnpm 组合下的清理逻辑太"耿直"了,耿直到把自家的包都给端了。
官方在较新版本已修复;旧版本项目用「build/dev 前清理 .next/standalone」即可稳住日常开
参考链接
- Next.js #72888 --- build 后 dev 报
next/dist/pages/_app - Next.js #75560 --- Windows 上第二次 build 失败
- Next.js #82157 --- symlink 包被清空
- PR #82191 --- 官方修复:避免递归删除跟随目录 symlink