Next standalone Turbopack trace 源码复制修复记录
背景
apps/blog 使用 Next.js output: "standalone" 部署,运行时需要读取仓库根目录的 content/ 文章源。
构建后入口是:
apps/blog/.next/standalone/apps/blog/server.js部署包需要包含:
apps/blog/.next/standalone/
apps/blog/public -> apps/blog/.next/standalone/apps/blog/public
apps/blog/.next/static -> apps/blog/.next/standalone/apps/blog/.next/static
content -> apps/blog/.next/standalone/content问题是反复构建后,apps/blog/.next/standalone/apps/blog 里会出现 blog 项目的 TypeScript 源码,例如:
apps/blog/.next/standalone/apps/blog/app/page.tsx
apps/blog/.next/standalone/apps/blog/components/navigation.tsx
apps/blog/.next/standalone/apps/blog/lib/blog.ts
apps/blog/.next/standalone/apps/blog/instrumentation-node.ts
apps/blog/.next/standalone/apps/blog/next.config.ts这不是期望的 standalone 输出。standalone 应该复制运行时需要的 JS、manifest、依赖和显式包含的内容源,不应该把应用源码目录原样放进发布包。
现象确认
先确认 standalone 中是否含有源码:
find apps/blog/.next/standalone \
-path '*/node_modules' -prune -o \
-type f \( -name '*.ts' -o -name '*.tsx' \) -print再看 NFT trace 清单:
find apps/blog/.next -type f -name '*.nft.json' | sort其中页面路由和 instrumentation 都会生成 trace 文件,例如:
apps/blog/.next/server/app/page.js.nft.json
apps/blog/.next/server/instrumentation.js.nft.json用 Node 快速检查 trace 是否列入源码:
node -e "const fs=require('fs'); for (const f of ['apps/blog/.next/server/instrumentation.js.nft.json','apps/blog/.next/server/app/page.js.nft.json']) { const j=JSON.parse(fs.readFileSync(f,'utf8')); const hits=j.files.filter(x=>/\.tsx?$/.test(x)||x.includes('next.config')); console.log(f, hits.length); console.log(hits.slice(0,40).join('\n')||'(none)'); }"排查中发现一个关键差异:
- webpack 构建下,大量源码复制可能来自旧
standalone目录残留,重新构建后只剩少量 trace 问题。 - Turbopack 构建下,
instrumentation.js.nft.json会把app/、components/、lib/、next.config.ts等源码列进 trace,从而复制到 standalone。
错误方向
一开始尝试过两个方向,但它们都不是根因修复。
第一种是构建前删除旧的 apps/blog/.next/standalone。这能避免旧构建残留误导判断,但不能解释为什么新的 NFT trace 会把源码列进去。
第二种是构建后删除 standalone 中的 .ts、.tsx 文件。这个只能清理结果,属于掩盖问题。如果 trace 本身仍然错误,未来换构建环境、换部署流程或升级 Next.js 后还会重新出现。
还尝试过 outputFileTracingExcludes:
outputFileTracingExcludes: {
"/*": [
"app/**/*.ts",
"app/**/*.tsx",
"components/**/*.tsx",
"hooks/**/*.ts",
"instrumentation-node.ts",
"instrumentation.ts",
"lib/**/*.ts",
"next.config.ts",
],
},这个配置可以影响页面路由 trace,但不能完整解决 instrumentation trace。原因是 Next.js standalone 复制阶段会单独处理:
.next/server/instrumentation.js.nft.json这条 trace 不按普通页面路由的 /* 匹配逻辑处理,所以不能靠 broad exclude 作为根治方案。
根因
根因在 apps/blog/instrumentation-node.ts。
blog 站点启动时会初始化内容索引,代码会从 ../../content 动态扫描 Markdown 文件和附件:
const CONTENT_DIR = path.join(process.cwd(), "../../content");后续还有大量运行时文件系统访问:
fs.existsSync(dir)
fs.readdirSync(dir, { withFileTypes: true })
fs.readFileSync(filePath, "utf8")
fs.statSync(resolved).isFile()
path.join(contentDir, "posts")
path.resolve(path.dirname(sourceFile), value)这些调用的目标在运行时确实只应该是 content/。但 Turbopack 的 NFT trace 无法从这些动态路径中稳定判断边界,于是保守地认为可能需要项目里的更多文件,最终把整个 blog 应用源码目录列入 instrumentation.js.nft.json。
这也是为什么警告里会出现类似信息:
Encountered unexpected file in NFT list
A file was traced that indicates that the whole project was traced unintentionally.Import trace 指向:
Instrumentation:
./apps/blog/next.config.ts
./apps/blog/instrumentation-node.ts
./apps/blog/instrumentation.ts关键不是 next.config.ts 被 import 了,而是 instrumentation 链路中的动态 fs/path 操作让 NFT trace 失去精确边界。
修复
修复方式是在运行时 content 扫描相关的 fs/path 参数上加 /* turbopackIgnore: true */,告诉 Turbopack 不要追踪这些动态文件系统路径。
示例:
const CONTENT_DIR = path.join(/* turbopackIgnore: true */ process.cwd(), "../../content");目录扫描:
const postsDir = path.join(/* turbopackIgnore: true */ contentDir, "posts");
const pagesDir = path.join(/* turbopackIgnore: true */ contentDir, "pages");
if (!fs.existsSync(/* turbopackIgnore: true */ dir)) return;
for (const entry of fs.readdirSync(/* turbopackIgnore: true */ dir, { withFileTypes: true })) {
const filePath = path.join(/* turbopackIgnore: true */ dir, entry.name);
}附件解析:
const resolved = path.resolve(/* turbopackIgnore: true */ path.dirname(sourceFile), value);
if (
isWithin(contentDir, resolved) &&
fs.existsSync(/* turbopackIgnore: true */ resolved) &&
fs.statSync(/* turbopackIgnore: true */ resolved).isFile()
) {
return resolved;
}文件读取:
const source = fs.readFileSync(/* turbopackIgnore: true */ filePath, "utf8");这不是让部署包丢掉 content/。content/ 仍然通过 Next 配置显式包含:
const nextConfig: NextConfig = {
output: "standalone",
outputFileTracingRoot: path.resolve(process.cwd(), "../.."),
outputFileTracingIncludes: {
"/*": ["../../content/**/*"],
},
};也就是说:
turbopackIgnore负责阻止 NFT 因动态 fs 扫描误追踪整个 app 源码。outputFileTracingIncludes负责明确把运行时需要的content/放进 standalone。
验证
修复后重新构建:
npm run blog:build当前 build 脚本可直接使用 Turbopack:
{
"scripts": {
"build": "next build"
}
}如果在受限沙箱中看到 Turbopack/PostCSS 绑定本地端口失败,例如:
creating new process
binding to a port
Operation not permitted (os error 1)这是运行环境限制,不是本次 trace 问题。换成允许本地端口/子进程的环境后可正常构建。
构建完成后检查 standalone:
find apps/blog/.next/standalone \
-path '*/node_modules' -prune -o \
-type f \( -name '*.ts' -o -name '*.tsx' \) -print期望输出为空。
再检查 instrumentation trace:
node -e "const fs=require('fs'); const f='apps/blog/.next/server/instrumentation.js.nft.json'; const j=JSON.parse(fs.readFileSync(f,'utf8')); const hits=j.files.filter(x=>/\.tsx?$/.test(x)||x.includes('next.config')); console.log('files', j.files.length); console.log(hits.join('\n') || '(no blog source hits)');"期望看到:
(no blog source hits)同时确认 content/ 仍然被复制:
find apps/blog/.next/standalone/content -maxdepth 2 -type d | sort
find apps/blog/.next/standalone/content -type f -name '*.md' | wc -l这说明修复没有靠删除源码文件,也没有牺牲运行时内容目录。
结论
本次问题的根因不是 standalone 本身,也不是 outputFileTracingIncludes 写错,而是 instrumentation 中的动态运行时文件扫描让 Turbopack NFT trace 误判边界。
正确修复是对这些运行时 fs/path 参数使用 /* turbopackIgnore: true */,让 trace 停止追踪动态扫描路径;再用 outputFileTracingIncludes 明确声明真正需要进入部署包的 content/。
后续如果再添加类似运行时扫描逻辑,应遵守同一原则:
- 动态读取的运行时数据目录,不要让 Turbopack 通过 fs/path 调用自行猜。
- 用
turbopackIgnore阻止误 trace。 - 用
outputFileTracingIncludes明确声明部署包必须包含的数据目录。 - 验证 NFT 清单和 standalone 产物,不要只看构建是否成功。