Skip to content

Next.js 16.1 build fails with EEXIST error: Turbopack creates symlinks in .next/node_modules that Amplify bundler cannot handle #4074

@yamaaaaaa31

Description

@yamaaaaaa31

Environment

  • Hosting: AWS Amplify
  • Framework: Next.js 16.1.0
  • Node.js version: 22.x
  • Package manager: pnpm 10.26.1
  • Region: ap-northeast-1

Describe the bug

When deploying a Next.js 16.1.0 application to AWS Amplify, the build succeeds but the bundling phase fails with an EEXIST error related to symlinks in .next/node_modules/.

Root Cause

Next.js 16.1 introduced changes to Turbopack's module resolution that creates hashed symlinks in the .next/node_modules/ directory during the build process.

For example:

.next/node_modules/pino-3de069a0e16ae0ec -> ../../node_modules/pino

This behavior change was introduced between Next.js 16.0.10 and 16.1.0 (see vercel/next.js#87402 for similar Turbopack symlink issues).

AWS Amplify's bundler cannot handle these symlinks properly, causing EEXIST errors when it attempts to create directories that conflict with existing symlinks during the compute bundle process.

Note: This may be related to the Improved Handling of serverExternalPackages feature in Next.js 16.1, which enhanced Turbopack's handling of transitive dependencies.

Build Logs

2025-12-19T16:24:11.137Z [INFO]: # Starting caching...
2025-12-19T16:24:11.142Z [INFO]: # Creating cache artifact...
2025-12-19T16:24:11.146Z [INFO]: # Created cache artifact
2025-12-19T16:24:11.147Z [INFO]: # Uploading cache artifact...
2025-12-19T16:24:11.253Z [INFO]: # Uploaded cache artifact
2025-12-19T16:24:11.253Z [INFO]: # Caching completed
2025-12-19T16:24:31.471Z [ERROR]: !!! We failed to bundle your application due to an unexpected error. Please try again. If the issue persists, please contact AWS support.
2025-12-19T16:24:31.591Z [ERROR]: !!! Error: EEXIST: file already exists, mkdir '/codebuild/output/src210259080/src/****/frontend/amplify-compute-bundle-output/compute/default/.next/node_modules/pino-3de069a0e16ae0ec'
2025-12-19T16:24:31.592Z [INFO]: # Starting environment caching...
2025-12-19T16:24:31.592Z [INFO]: # Environment caching completed

Expected behavior

Amplify's bundler should correctly handle symlinks created by Next.js 16.1+ Turbopack, or dereference them during the bundling process.

Steps to reproduce

  1. Create a Next.js 16.1.0 application (Turbopack is enabled by default)
  2. Include a package that Turbopack externalizes (e.g., pino, jsdom, or other native modules)
  3. Deploy to AWS Amplify
  4. Build succeeds, but bundling phase fails with EEXIST error

Workaround

We resolved this by adding a post-build script that resolves symlinks to actual directories before Amplify's bundler runs.

amplify.yml:

version: 1
applications:
  - appRoot: frontend
    frontend:
      phases:
        preBuild:
          commands:
            - corepack enable
            - pnpm install --frozen-lockfile
        build:
          commands:
            - pnpm build
            # Next.js 16.1 Turbopack creates hashed symlinks that Amplify can't handle
            - node scripts/resolve-next-symlinks.js
      artifacts:
        baseDirectory: .next
        files:
          - "**/*"
      cache:
        paths:
          - node_modules/**/*
          - .next/cache/**/*

scripts/resolve-next-symlinks.js:

/**
 * Next.js 16.1 Turbopack Symlink Resolver for AWS Amplify
 *
 * Next.js 16.1's Turbopack creates hashed symlinks for externalized packages
 * (e.g., pino-3de069a0e16ae0ec -> ../../node_modules/pino)
 *
 * AWS Amplify's bundler cannot handle these symlinks properly, causing
 * "EEXIST: file already exists" errors during deployment.
 *
 * This script resolves symlinks to real directories and copies their dependencies.
 */

const fs = require('fs');
const path = require('path');

const nextModules = path.join(__dirname, '..', '.next', 'node_modules');
const rootModules = path.join(__dirname, '..', 'node_modules');

function resolveDependencyPath(depName, parentPkgPath) {
  const directPath = path.join(rootModules, depName);
  try {
    const stat = fs.lstatSync(directPath);
    if (stat.isSymbolicLink()) {
      return fs.realpathSync(directPath);
    }
    if (stat.isDirectory()) {
      return directPath;
    }
  } catch {
    // Not found at root level
  }

  // For pnpm's isolated mode, check the parent package's node_modules
  if (parentPkgPath) {
    const parentNodeModules = path.dirname(parentPkgPath);
    const pnpmDepPath = path.join(parentNodeModules, depName);
    try {
      const stat = fs.lstatSync(pnpmDepPath);
      if (stat.isSymbolicLink()) {
        return fs.realpathSync(pnpmDepPath);
      }
      if (stat.isDirectory()) {
        return pnpmDepPath;
      }
    } catch {
      // Not found in parent's node_modules
    }
  }

  return null;
}

function copyPackageWithDeps(pkgPath, destPath, copiedSet, originalPkgPath) {
  const pkgName = path.basename(destPath);

  if (copiedSet.has(pkgName)) {
    return 0;
  }

  copiedSet.add(pkgName);

  console.log(`  Copying: ${pkgName}`);
  fs.cpSync(pkgPath, destPath, { recursive: true, dereference: true });
  let count = 1;

  const pkgJsonPath = path.join(destPath, 'package.json');
  if (fs.existsSync(pkgJsonPath)) {
    const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
    const deps = Object.keys(pkg.dependencies || {});

    for (const dep of deps) {
      const depDest = path.join(nextModules, dep);

      if (!fs.existsSync(depDest) && !copiedSet.has(dep)) {
        const depSrc = resolveDependencyPath(dep, originalPkgPath || pkgPath);
        if (depSrc) {
          count += copyPackageWithDeps(depSrc, depDest, copiedSet, depSrc);
        } else {
          console.log(`  Warning: Could not find dependency ${dep}`);
        }
      }
    }
  }

  return count;
}

function main() {
  if (!fs.existsSync(nextModules)) {
    console.log('No .next/node_modules directory found, skipping.');
    return;
  }

  const entries = fs.readdirSync(nextModules);
  let resolved = 0;
  const copiedSet = new Set();

  for (const name of entries) {
    const linkPath = path.join(nextModules, name);
    const stat = fs.lstatSync(linkPath);

    if (stat.isSymbolicLink()) {
      const target = fs.realpathSync(linkPath);
      console.log(`Resolving: ${name} -> ${target}`);

      fs.rmSync(linkPath);
      copyPackageWithDeps(target, linkPath, copiedSet, target);
      resolved++;
    }
  }

  console.log(`\nResolved ${resolved} symlinks, copied ${copiedSet.size} packages total.`);
}

main();

Suggested Fix

AWS Amplify should either:

  1. Dereference symlinks during bundling - Use --dereference or equivalent when copying/bundling the .next directory
  2. Update the bundler to properly handle symlinks created by modern build tools like Turbopack
  3. Document this limitation for Next.js 16.1+ users until a fix is available

Related Issues

Additional Context

  • This affects any Next.js 16.1+ project where Turbopack externalizes packages (e.g., pino, jsdom, native modules)
  • The issue is specific to AWS Amplify's bundler, not the Next.js build itself
  • The Next.js build completes successfully; the failure occurs during Amplify's compute bundle phase
  • The workaround works for npm, yarn, and pnpm package managers
  • Turbopack is enabled by default in Next.js 16, so this will affect most Next.js 16.1+ deployments

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions