Closed36

Next.js v13.5.5 でビルドが失敗する件

snakasnaka

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 プロセスではビルドが成功している
  • Next.js のバージョンは v13.5.6

対処方法

以下のいずれか

  • config.next.js で config.experimental.cpus の値を 1, 2, 4 などに設定する
  • config.next.js で config.outputFileTracingfalse に設定する
  • Next.js 14 にアップデートする
snakasnaka

バックトレースを含むエラーメッセージ

(実際の内容を多少改変している)

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 は存在していた。

snakasnaka

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
            }
          },
snakasnaka

nodeFileTrace

@vercel/nft は Next.js のリポジトリにコンパイル結果のコード自体が含まれているっぽい ( どういう理由だろう? )

https://github.com/vercel/next.js/blob/v13.5.5/packages/next/src/compiled/%40vercel/nft/index.js

もしかしたら Vendoring 目的だろうか?

https://qiita.com/acevif/items/fd7c6512387a0c566b2b

それっぽい PR

https://github.com/vercel/next.js/pull/51083

@vercel/nft のどのバージョンが使われているかは package.json を見たら良いのかな?

packages/next/package.json#L193

    "@vercel/nft": "0.22.6",
snakasnaka

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) かな?

snakasnaka

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);
    }
snakasnaka

おそらく this.readFile(path) でファイルが読み込めなかったと推測する

path はどこから来たか?

  async emitDependency (path: string, parent?: string) {

emitDependency の引数として受け取っている

その pathnodeFileTrace で渡している

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);
  }));
snakasnaka

(つづき) 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'
        ),
      ]

serverEntriessharedEntriesSet はフレームワーク側が吐くファイルっぽいな

snakasnaka

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,
}: {
snakasnaka

ここで出てくる Trace が意味するものが分からなくなってきた

OTEL などの文脈での Trace であるようにも見える ( telemetry とか span とか出てくる ) し、依存関係の追跡(Trace) というニュアンスにも受け取れる。
最初は後者だと思ってコードを読み始めたが、コードを読むにつれて前者のようにも見えてくる....

どっちなんだ?あるいはどっちでもないのか?

snakasnaka

(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)
              })
snakasnaka

buildTraceContextwebpackBuild() の結果から 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)
              })
          })
snakasnaka

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 情報を出してみると、どちらで動いているか判別できそう

snakasnaka

debug オプションは --debug または環境変数 NEXT_DEBUG_BUILD

packages/next/src/cli/next-build.ts#L61

    args['--debug'] || process.env.NEXT_DEBUG_BUILD,

やってみたが、コンソールに何か出力されるわけでは無いのか...

snakasnaka

config.experimental.webpackBuildWorker の設定の違いによる debug 出力の違い

trueusing 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) 

falsebuilding 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) 
snakasnaka

エラーになってるの、Generating static pages の処理だから、ビルド(compile)の後か...

snakasnaka

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,
        }),
snakasnaka

plugins の中身を getBaseWebpackConfig() から探る

packages/next/src/build/webpack-config.ts#L424

export default async function getBaseWebpackConfig(

compilerTypeCOMPILER_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 || [],
          }
        ),
snakasnaka

TraceEntryPointsPluginbuildTraceContext がどのようにセットされているかを見ていく

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])
      }
snakasnaka

config.outputFileTracingfalse にするとビルド成功した

packages/next/src/server/config-schema.ts#L479

    outputFileTracing: z.boolean().optional(),

ということは、これまで想定していた collectBuildTraces() を呼び出している箇所が間違っている可能性出てきた

snakasnaka

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'
snakasnaka

buildcli/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' 
snakasnaka

webpackBuild() を呼び出して、buildTraceContext を受け取っている箇所

packages/next/src/build/index.ts#L1106-L1110

          const { duration: webpackBuildDuration, ...rest } = turboNextBuild
            ? await turbopackBuild()
            : await webpackBuild()

          buildTraceContext = rest.buildTraceContext
snakasnaka

webpackBuild() 以降の流れは前述のものと同じなので省略

snakasnaka

dist dir のエラー時と正常時の違い

エラー時

├── index.js
├── index.js.nft.json

正常時

├── index.html
├── index.js.nft.json

どこかで js から html に変換してる?

snakasnaka

ビルドが失敗するプロジェクトのコードを削って行って、どの時点でエラーが解消するか探る

snakasnaka

再現コードなしでとりあえず Issue だけでも立てようと思ったが... テンプレートに再現コードは必須とあったので、なんとかして再現させる必要があるな ....

プライベートなコードなのでそのまま公開するわけにもいかないし、再現する条件がよくわからんので辛いな...

snakasnaka

next@14.0.0 に上げたら普通にビルド通った

このスクラップは2023/12/06にクローズされました