Next.js v13.5.5 でビルドが失敗する件
引っ越し元:
Next.js v13.5.5 でビルドが失敗する件
#snaka_oss
症状
-
next build
を実行すると、コンパイル後の依存関係の trace の段階でエラーが出る - エラーの内容的には
{output path}/.next/server/pages/path/to/xxxx.js
が見つからないというもの - トレース情報(?)
{output path}/.next/server/pages/path/to/xxxx.js.nft.json
は存在しているが、それに対応する.js
が出力されていないように見える - OSには依存していなさそう(WSL2, Mac 両方で再現した)
- ただしビルド環境によってエラーになるファイルが異なる
- ローカル環境(CPU:16論理コア)だとエラーが出るが、GitHub Actions 上で実行している CI プロセスではビルドが成功している
- 試しにローカルのCPUコア数を2に下げたらビルド成功した
- Next.js のバージョンは v13.5.6
対処方法
以下のいずれか
- config.next.js で
config.experimental.cpus
の値を1
,2
,4
などに設定する - config.next.js で
config.outputFileTracing
をfalse
に設定する - Next.js 14 にアップデートする
バックトレースを含むエラーメッセージ
(実際の内容を多少改変している)
Error: File /home/snaka/projects/hoge-fuga/dist/src/.next/server/pages/xxxx/edit.js does not exist.
at Job.emitDependency (/home/snaka/projects/hoge-fuga/node_modules/next/dist/compiled/@vercel/nft/index.js:1:39473)
at async Promise.all (index 30)
at async nodeFileTrace (/home/snaka/projects/hoge-fuga/node_modules/next/dist/compiled/@vercel/nft/index.js:1:35430)
at async /home/snaka/projects/hoge-fuga/node_modules/next/dist/build/collect-build-traces.js:260:28
at async Span.traceAsyncFn (/home/snaka/projects/hoge-fuga/node_modules/next/dist/trace/trace.js:105:20)
at async collectBuildTraces (/home/snaka/projects/hoge-fuga/node_modules/next/dist/build/collect-build-traces.js:131:5)
エラーメッセージにあるとおり dist/src/.next/server/pages/xxxx/edit.js
は存在しなかったが、 dist/src/.next/server/pages/xxxx/edit.js.nft.json
は存在していた。
collectBuildTraces
nodeFileTrace
は @vercel/nft
が提供する関数
packages/next/src/build/collect-build-traces.ts#L339
const result = await nodeFileTrace(chunksToTrace, {
base: outputFileTracingRoot,
processCwd: dir,
mixedModules: true,
async readFile(p) {
try {
return await fs.readFile(p, 'utf8')
} catch (e) {
if (isError(e) && (e.code === 'ENOENT' || e.code === 'EISDIR')) {
// handle temporary internal webpack files
if (p.match(/static[/\\]media/)) {
return ''
}
return null
}
throw e
}
},
nodeFileTrace
@vercel/nft
は Next.js のリポジトリにコンパイル結果のコード自体が含まれているっぽい ( どういう理由だろう? )
もしかしたら Vendoring 目的だろうか?
それっぽい PR
@vercel/nft
のどのバージョンが使われているかは package.json を見たら良いのかな?
packages/next/package.json#L193
"@vercel/nft": "0.22.6",
src/node-file-trace.ts#L15-L42
export async function nodeFileTrace(files: string[], opts: NodeFileTraceOptions = {}): Promise<NodeFileTraceResult> {
const job = new Job(opts);
if (opts.readFile)
job.readFile = opts.readFile
if (opts.stat)
job.stat = opts.stat
if (opts.readlink)
job.readlink = opts.readlink
if (opts.resolve)
job.resolve = opts.resolve
job.ts = true;
await Promise.all(files.map(async file => {
const path = resolve(file);
await job.emitFile(path, 'initial');
return job.emitDependency(path);
}));
const result: NodeFileTraceResult = {
fileList: job.fileList,
esmFileList: job.esmFileList,
reasons: job.reasons,
warnings: job.warnings
};
return result;
};
エラー吐いたのは Promise.all( ... )
の中の job.emitDependency(path)
かな?
Job#emitDependency
エラーを吐いたと思われる場所
src/node-file-trace.ts#L302-L314
const cachedAnalysis = this.analysisCache.get(path);
if (cachedAnalysis) {
analyzeResult = cachedAnalysis;
}
else {
const source = await this.readFile(path);
if (source === null) throw new Error('File ' + path + ' does not exist.');
// analyze should not have any side-effects e.g. calling `job.emitFile`
// directly as this will not be included in the cachedAnalysis and won't
// be emit for successive runs that leverage the cache
analyzeResult = await analyze(path, source.toString(), this);
this.analysisCache.set(path, analyzeResult);
}
おそらく this.readFile(path)
でファイルが読み込めなかったと推測する
path
はどこから来たか?
async emitDependency (path: string, parent?: string) {
emitDependency
の引数として受け取っている
その path
は nodeFileTrace
で渡している
src/node-file-trace.ts#L29-L33
await Promise.all(files.map(async file => {
const path = resolve(file);
await job.emitFile(path, 'initial');
return job.emitDependency(path);
}));
path.resolve()
って何だっけ?
The path.resolve() method resolves a sequence of paths or path segments into an absolute path.
相対 path 与えたら絶対 path が得られる感じか
(つづき) files
はどこから?
nodeFileTrace()
の引数として受け取っている
export async function nodeFileTrace(files: string[], opts: NodeFileTraceOptions = {}): Promise<NodeFileTraceResult> {
collect-build-traces.ts の collectBuildTraces()
の中から nodeFileTrace()
を呼び出していて、 files
には chunksToTrace
を渡している
chunksToTrace
とは?
packages/next/src/build/collect-build-traces.ts#L333-L337
const chunksToTrace: string[] = [
...(buildTraceContext?.chunksTrace?.action.input || []),
...serverEntries,
...minimalServerEntries,
]
serverEntries
は
packages/next/src/build/collect-build-traces.ts#L211-L221
const serverEntries = [
...sharedEntriesSet,
...(isStandalone
? [
require.resolve('next/dist/server/lib/start-server'),
require.resolve('next/dist/server/next'),
require.resolve('next/dist/server/require-hook'),
]
: []),
require.resolve('next/dist/server/next-server'),
].filter(Boolean) as string[]
minimalServerEntries
packages/next/src/build/collect-build-traces.ts#L223-L226
const minimalServerEntries = [
...sharedEntriesSet,
require.resolve('next/dist/compiled/next-server/server.runtime.prod'),
].filter(Boolean)
sharedEntriesSet
const sharedEntriesSet = [
...(config.experimental.turbotrace
? []
: Object.keys(defaultOverrides).map((value) =>
require.resolve(value, {
paths: [require.resolve('next/dist/server/require-hook')],
})
)),
require.resolve('next/dist/compiled/next-server/app-page.runtime.prod'),
require.resolve(
'next/dist/compiled/next-server/app-route.runtime.prod'
),
require.resolve('next/dist/compiled/next-server/pages.runtime.prod'),
require.resolve(
'next/dist/compiled/next-server/pages-api.runtime.prod'
),
]
serverEntries
と sharedEntriesSet
はフレームワーク側が吐くファイルっぽいな
buildTraceContext?.chunksTrace?.action.input
について見ていく
packages/next/src/build/collect-build-traces.ts#L31-L42
export async function collectBuildTraces({
dir,
config,
distDir,
pageKeys,
pageInfos,
staticPages,
nextBuildSpan = new Span({ name: 'build' }),
hasSsrAmpPages,
buildTraceContext,
outputFileTracingRoot,
}: {
ここで出てくる Trace が意味するものが分からなくなってきた
OTEL などの文脈での Trace であるようにも見える ( telemetry とか span とか出てくる ) し、依存関係の追跡(Trace) というニュアンスにも受け取れる。
最初は後者だと思ってコードを読み始めたが、コードを読むにつれて前者のようにも見えてくる....
どっちなんだ?あるいはどっちでもないのか?
(2つ前からのつづき) trace のために別プロセス(?)を立ち上げているらしき箇所
packages/next/src/build/index.ts#L1062-L1085
const buildTraceWorker = new Worker(
require.resolve('./collect-build-traces'),
{
numWorkers: 1,
exposedMethods: ['collectBuildTraces'],
}
) as Worker & typeof import('./collect-build-traces')
buildTracesPromise = buildTraceWorker
.collectBuildTraces({
dir,
config,
distDir,
pageKeys,
pageInfos: [],
staticPages: [],
hasSsrAmpPages: false,
buildTraceContext,
outputFileTracingRoot,
})
.catch((err) => {
console.error(err)
process.exit(1)
})
buildTraceContext
は webpackBuild()
の結果から res.buildTraceContext
packages/next/src/build/index.ts#L1059-L1086
await webpackBuild(['server']).then((res) => {
buildTraceContext = res.buildTraceContext
durationInSeconds += res.duration
const buildTraceWorker = new Worker(
require.resolve('./collect-build-traces'),
{
numWorkers: 1,
exposedMethods: ['collectBuildTraces'],
}
) as Worker & typeof import('./collect-build-traces')
buildTracesPromise = buildTraceWorker
.collectBuildTraces({
dir,
config,
distDir,
pageKeys,
pageInfos: [],
staticPages: [],
hasSsrAmpPages: false,
buildTraceContext,
outputFileTracingRoot,
})
.catch((err) => {
console.error(err)
process.exit(1)
})
})
webpackBuild()
の実装はConfigオプションによって webpackBuildWithWorker()
と webpackBuildImpl()
を切り替えている
packages/next/src/build/webpack-build/index.ts#L128-L141
export async function webpackBuild(
compilerNames?: typeof ORDERED_COMPILER_NAMES
) {
const config = NextBuildContext.config!
if (config.experimental.webpackBuildWorker) {
debug('using separate compiler workers')
return await webpackBuildWithWorker(compilerNames)
} else {
debug('building all compilers in same process')
const webpackBuildImpl = require('./impl').webpackBuildImpl
return await webpackBuildImpl()
}
}
debug 情報を出してみると、どちらで動いているか判別できそう
debug オプションは --debug
または環境変数 NEXT_DEBUG_BUILD
packages/next/src/cli/next-build.ts#L61
args['--debug'] || process.env.NEXT_DEBUG_BUILD,
やってみたが、コンソールに何か出力されるわけでは無いのか...
config.experimental.cpus
の値を 1
, 2
, 4
などの値で実行するとビルドが通った
packages/next/src/server/config-schema.ts#L244
cpus: z.number().optional(),
8
はダメだった
6
はOK
7
はOK
debug
パッケージの使い方わかってなかった...
以下のように環境変数 DEBUG
を設定することで出力された
DEBUG=* yarn next build --debug
config.experimental.webpackBuildWorker
の設定の違いによる debug 出力の違い
true → using separate compiler workers
Linting and checking validity of types ...
⚠ The Next.js plugin was not detected in your ESLint configuration. See https://nextjs.org/docs/basic-features/eslint#migrating-existing-config
✓ Linting and checking validity of types
Creating an optimized production build .
next:build:webpack-build using separate compiler workers +0ms
Creating an optimized production build ...
2023-10-21T14:43:29.919Z next:build:webpack-build starting compiler server
2023-10-21T14:43:29.919Z next:build:webpack-build starting server compiler
Creating an optimized production build ..
2023-10-21T14:43:32.675Z next:build:webpack-build server compiler finished 2756ms
Creating an optimized production build .
2023-10-21T14:43:33.144Z next:build:webpack-build starting compiler edge-server
2023-10-21T14:43:33.145Z next:build:webpack-build starting edge-server compiler
2023-10-21T14:43:33.196Z next:build:webpack-build edge-server compiler finished 51ms
Creating an optimized production build ...
2023-10-21T14:43:33.643Z next:build:webpack-build starting compiler client
2023-10-21T14:43:33.643Z next:build:webpack-build starting client compiler
Creating an optimized production build ..
2023-10-21T14:43:35.197Z next:build:webpack-build client compiler finished 1554ms
✓ Creating an optimized production build
✓ Compiled successfully
✓ Collecting page data
next:build:build-traces starting build traces +0ms
✓ Generating static pages (38/38)
false → building all compilers in same process
Linting and checking validity of types ...
⚠ The Next.js plugin was not detected in your ESLint configuration. See https://nextjs.org/docs/basic-features/eslint#migrating-existing-config
✓ Linting and checking validity of types
Creating an optimized production build .
next:build:webpack-build building all compilers in same process +0ms
next:build:webpack-build starting compiler undefined +0ms
next:build:webpack-build starting server compiler +0ms
Creating an optimized production build ..
next:build:webpack-build server compiler finished 5162ms +5s
next:build:webpack-build starting edge-server compiler +0ms
next:build:webpack-build edge-server compiler finished 9ms +9ms
next:build:webpack-build starting client compiler +0ms
Creating an optimized production build ...
next:build:webpack-build client compiler finished 6476ms +6s
✓ Creating an optimized production build
✓ Compiled successfully
✓ Collecting page data
next:build:build-traces starting build traces +0ms
✓ Generating static pages (38/38)
エラーになってるの、Generating static pages
の処理だから、ビルド(compile)の後か...
buildTraceContext.chunksTrace
に何が入ってくるか見ていく
packages/next/src/build/webpack-build/impl.ts#L56-L58
export async function webpackBuildImpl(
compilerName?: keyof typeof COMPILER_INDEXES
): Promise<{
この関数が返すオブジェクトの buildTraceContext
プロパティ
packages/next/src/build/webpack-build/impl.ts#L324
buildTraceContext: traceEntryPointsPlugin?.buildTraceContext,
traceEntryPointsPlugin
の中身
const traceEntryPointsPlugin = (
serverConfig as webpack.Configuration
).plugins?.find(isTraceEntryPointsPlugin)
serverConfig
const serverConfig = configs[1]
config[1]
getBaseWebpackConfig(dir, {
...commonWebpackOptions,
runWebpackSpan,
middlewareMatchers: entrypoints.middlewareMatchers,
compilerType: COMPILER_NAMES.server,
entrypoints: entrypoints.server,
...info,
}),
plugins
の中身を getBaseWebpackConfig()
から探る
packages/next/src/build/webpack-config.ts#L424
export default async function getBaseWebpackConfig(
compilerType
は COMPILER_NAMES.server
const isNodeServer = compilerType === COMPILER_NAMES.server
// If the current compilation is aimed at server-side code instead of client-side code.
const isNodeOrEdgeCompilation = isNodeServer || isEdgeServer
plugins
配列に TraceEntryPointsPlugin
をセットしている箇所
packages/next/src/build/webpack-config.ts#L1974-L1989
config.outputFileTracing &&
isNodeServer &&
!dev &&
new (require('./webpack/plugins/next-trace-entrypoints-plugin')
.TraceEntryPointsPlugin as typeof import('./webpack/plugins/next-trace-entrypoints-plugin').TraceEntryPointsPlugin)(
{
rootDir: dir,
appDir: appDir,
pagesDir: pagesDir,
esmExternals: config.experimental.esmExternals,
outputFileTracingRoot: config.experimental.outputFileTracingRoot,
appDirEnabled: hasAppDir,
turbotrace: config.experimental.turbotrace,
traceIgnores: config.experimental.outputFileTracingIgnores || [],
}
),
TraceEntryPointsPlugin
の buildTraceContext
がどのようにセットされているかを見ていく
packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts#L130-L131
export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
public buildTraceContext: BuildTraceContext = {}
async createTraceAssets(compilation: any, assets: any, span: Span)
の以下の行
this.buildTraceContext.chunksTrace = {
action: {
action: 'annotate',
input: [...chunksToTrace],
chunksToTrace
for (const entrypoint of compilation.entrypoints.values()) {
const entryFiles = new Set<string>()
for (const chunk of entrypoint
.getEntrypointChunk()
.getAllReferencedChunks()) {
for (const file of chunk.files) {
if (isTraceable(file)) {
const filePath = nodePath.join(outputPath, file)
chunksToTrace.add(filePath)
entryFiles.add(filePath)
}
}
for (const file of chunk.auxiliaryFiles) {
if (isTraceable(file)) {
const filePath = nodePath.join(outputPath, file)
chunksToTrace.add(filePath)
entryFiles.add(filePath)
}
}
}
entryFilesMap.set(entrypoint, entryFiles)
entryNameFilesMap.set(entrypoint.name, [...entryFiles])
}
Webpack の plugin について見ておく
ここまでをまとめ
config.outputFileTracing
を false
にするとビルド成功した
packages/next/src/server/config-schema.ts#L479
outputFileTracing: z.boolean().optional(),
ということは、これまで想定していた collectBuildTraces()
を呼び出している箇所が間違っている可能性出てきた
build
のパラメタを確認する
packages/next/src/build/index.ts#L335-L347
export default async function build(
dir: string,
reactProductionProfiling = false,
debugOutput = false,
runLint = true,
noMangling = false,
appDirOnly = false,
turboNextBuild = false,
turboNextBuildRoot = null,
buildMode: 'default' | 'experimental-compile' | 'experimental-generate'
): Promise<void> {
const isCompile = buildMode === 'experimental-compile'
const isGenerate = buildMode === 'experimental-generate'
build
は cli/next-build.ts
から呼び出される
packages/next/src/cli/next-build.ts#L54-L68
if (args['--experimental-turbo']) {
process.env.TURBOPACK = '1'
}
return build(
dir,
args['--profile'],
args['--debug'] || process.env.NEXT_DEBUG_BUILD,
!args['--no-lint'],
args['--no-mangling'],
args['--experimental-app-only'],
!!process.env.TURBOPACK,
args['--experimental-turbo-root'],
args['--build-mode'] || 'default'
).catch((err) => {
特にオプションの指定が無い場合は build()
では以下の引数を受け取っているはず
reactProductionProfiling = false,
debugOutput = false,
runLint = true,
noMangling = false,
appDirOnly = false,
turboNextBuild = false,
turboNextBuildRoot = null,
buildMode: 'default'
webpackBuild()
を呼び出して、buildTraceContext
を受け取っている箇所
packages/next/src/build/index.ts#L1106-L1110
const { duration: webpackBuildDuration, ...rest } = turboNextBuild
? await turbopackBuild()
: await webpackBuild()
buildTraceContext = rest.buildTraceContext
webpackBuild()
以降の流れは前述のものと同じなので省略
dist
dir のエラー時と正常時の違い
エラー時
├── index.js
├── index.js.nft.json
正常時
├── index.html
├── index.js.nft.json
どこかで js
から html
に変換してる?
再現コードを用意する方向にチェンジ
問題が混入したバージョンを特定する
- ❌
next@13.5.5
- ❌
next@13.5.5-canary.18
- ❌
next@13.5.5-canary.17
- ❌
next@13.5.5-canary.16
- Fix build traces case by ijjk · Pull Request #56817 · vercel/next.js ← これ?
- ❌ node.js
20.8.1
- ❌ node.js
18.16.1
- ✅
next@13.5.5-canary.15
- ✅
next@13.5.5-canary.10
- ✅
next@13.5.4
- barrel_optimize のメッセージが大量に出る
ビルドが失敗するプロジェクトのコードを削って行って、どの時点でエラーが解消するか探る
再現コードなしでとりあえず Issue だけでも立てようと思ったが... テンプレートに再現コードは必須とあったので、なんとかして再現させる必要があるな ....
プライベートなコードなのでそのまま公開するわけにもいかないし、再現する条件がよくわからんので辛いな...
next@14.0.0
に上げたら普通にビルド通った