Next standalone Turbopack trace 源码复制修复记录
太阳作者太阳
原创内容采用 CC-4.0 协议发布,转载请注明出处
Next.jsTurbopackstandalone构建部署故障排查

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/

后续如果再添加类似运行时扫描逻辑,应遵守同一原则:

  1. 动态读取的运行时数据目录,不要让 Turbopack 通过 fs/path 调用自行猜。
  2. turbopackIgnore 阻止误 trace。
  3. outputFileTracingIncludes 明确声明部署包必须包含的数据目录。
  4. 验证 NFT 清单和 standalone 产物,不要只看构建是否成功。