Turbopack: Symlink node_modules is invalid (solución)

Creas un worktree de Git para trabajar en otra rama, enlazas su carpeta node_modules a la del repositorio principal para no reinstalar dependencias y ejecutas next dev o next build. Next.js 16 se detiene con un error parecido a este:

FATAL: An unexpected Turbopack error occurred.
Error [TurbopackInternalError]: Symlink [project]/node_modules is invalid, it points out of the filesystem root

El texto exacto cambia un poco entre versiones; por ejemplo, puede aparecer sin el prefijo [project]/. Este artículo explica por qué Turbopack rechaza ese enlace y presenta cuatro formas de resolverlo, con lo que cuesta cada una.

Por qué pasa

Desde Next.js 16, Turbopack es el empaquetador por defecto tanto en next dev como en next build. Turbopack resuelve los módulos dentro de una carpeta raíz, y la documentación de Next.js es explícita: los archivos que quedan fuera de la raíz del proyecto no se resuelven. Next.js detecta esa raíz buscando uno de estos archivos: pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lock o bun.lockb.

Un node_modules que es un enlace simbólico hacia otro checkout apunta a una carpeta situada fuera de esa raíz. La sección de la documentación de Turbopack sobre las diferencias con webpack se titula "Filesystem Root" y pone como ejemplo los paquetes enlazados fuera del proyecto con npm link, yarn link o pnpm link, que no se resuelven por defecto. El mensaje de error usa esa misma expresión. Si tu enlace es una junction de Windows, el planteamiento es igual: el destino sigue estando fuera de la raíz.

El truco del enlace es tentador con los worktrees porque git worktree add crea una carpeta de trabajo nueva con los archivos versionados, y node_modules suele estar ignorada por Git, así que no viaja con ella. En un reporte público del mismo error, su autor comenta que ese esquema funcionaba con builds basados en webpack; con Turbopack ya no.

Solución

Las opciones van de más a menos recomendable. Después de aplicar cualquiera de ellas, haz el último paso (borrar .next).

  1. Instala las dependencias en cada worktree. Primero elimina el enlace sin tocar la carpeta a la que apunta. Si borras contenido a través del enlace, dañas el checkout principal. Estos comandos quitan solo el enlace y fallan si node_modules fuera una carpeta real con contenido:

    # Linux, macOS o WSL (sin -r y sin barra final)
    rm node_modules
    :: Windows, en cmd (sin /s)
    rmdir node_modules

    Después instala:

    pnpm install

    pnpm no copia cada paquete en cada proyecto. Según su documentación, guarda los archivos una sola vez en un almacén global direccionable por contenido (content-addressable store) y crea enlaces duros desde el node_modules de cada proyecto hacia ese almacén. Por eso instalar en un worktree nuevo ocupa poco disco y descarga poco si las versiones ya están en el almacén. Un detalle importante: los enlaces duros solo existen dentro de un mismo sistema de archivos. Si el almacén y el proyecto están en discos distintos, pnpm copia los archivos y la instalación pesa más y tarda más.

  2. Copia node_modules con enlaces duros. En Linux o WSL, con GNU cp, y con origen y destino en el mismo sistema de archivos:

    cp -al ../mi-app/node_modules ./node_modules

    La opción -a conserva la estructura y copia los enlaces simbólicos como enlaces; -l crea enlaces duros en lugar de duplicar el contenido. Obtienes una carpeta real dentro del proyecto, así que Turbopack ya no ve un enlace que sale de la raíz, y casi no gastas disco. Limitaciones:

    • Si origen y destino están en sistemas de archivos distintos, el comando falla.
    • Los archivos enlazados son los mismos en ambas carpetas. Si una herramienta modifica un archivo de node_modules en su sitio (un parche, un generador de código), el cambio también aparece en el otro checkout.
    • Los enlaces simbólicos internos se copian tal cual. Si alguno apunta a una ruta absoluta del checkout original, seguirá apuntando allí. Revísalo con ls -l o readlink.
  3. Compila con webpack, como solución provisional. La CLI de Next.js documenta la opción --webpack para ambos comandos:

    next dev --webpack
    next build --webpack

    El costo es que dejas de probar con el empaquetador que usa tu producción, si ahí usas Turbopack. La documentación enumera diferencias entre ambos (por ejemplo, el orden de los módulos CSS o la precisión decimal) y funciones que solo existen con Turbopack, como import.meta.glob. Úsala para destrabarte, no para validar una entrega.

  4. Amplía la raíz de Turbopack. La documentación indica que, para resolver archivos de dependencias enlazadas fuera del proyecto, puedes apuntar turbopack.root a la carpeta padre que contiene ambos lugares:

    // next.config.js
    const path = require('path')
    
    module.exports = {
      turbopack: {
        root: path.join(__dirname, '..'),
      },
    }

    No amplíes la raíz más de lo necesario (por ejemplo, hasta tu carpeta de usuario): la documentación advierte que Turbopack puede vigilar todos los archivos dentro de las raíces y que una raíz demasiado grande causa problemas de rendimiento. Además, la página general de Turbopack reserva turbopack.root para aplicaciones que importan archivos de fuera de la raíz detectada de forma directa, y recomienda otra opción para las dependencias alcanzadas mediante enlaces simbólicos: la opción experimental experimental.turbopackAdditionalRoots, descrita en la documentación de Next.js 16.4, que permite seguir enlaces cuyo destino está fuera de turbopack.root:

    // next.config.js
    const path = require('path')
    
    module.exports = {
      experimental: {
        turbopackAdditionalRoots: {
          checkoutPrincipal: { path: path.join(__dirname, '../mi-app') },
        },
      },
    }

    Esta segunda opción es experimental y no existe en versiones anteriores a 16.4.0. No se verificó esta configuración con una estructura concreta de worktrees, así que pruébala en la tuya antes de depender de ella. Tampoco la confundas con outputFileTracingRoot: según la documentación, esa opción controla qué archivos se incluyen al trazar dependencias para el despliegue en monorepos, no la resolución de módulos de Turbopack.

  5. Borra la salida anterior y vuelve a arrancar. Turbopack guarda caché de compilación dentro de .next, y esa carpeta puede conservar resultados de intentos anteriores con la configuración vieja:

    rm -rf .next
    Remove-Item -Recurse -Force .next

Cómo verificar que quedó resuelto

Primero comprueba que node_modules ya no es un enlace que sale del proyecto (salvo que hayas elegido la opción 4):

ls -ld node_modules
(Get-Item node_modules).LinkType

En Linux, la primera columna debe empezar con d (carpeta) y no con l (enlace). En PowerShell, el resultado debe estar vacío si es una carpeta real. Después ejecuta:

next dev
next build

El servidor de desarrollo debe arrancar y la compilación debe terminar sin el error FATAL. En un caso real, con la copia por enlaces duros el servidor de desarrollo arrancó y las pruebas funcionales completas del worktree se ejecutaron sin problemas; con --webpack, la compilación terminó correctamente.

Variantes y errores relacionados

El mismo mensaje con otros archivos

El error no es exclusivo de node_modules. Se ha reportado también con otros archivos enlazados, como un package.json. La causa es la misma: un enlace cuyo destino queda fuera de la raíz que usa Turbopack.

Almacén virtual global de pnpm

La documentación de turbopackAdditionalRoots menciona como caso de uso los paquetes enlazados que viven fuera del proyecto, con npm link o con el almacén virtual global de pnpm. Si usas esa función de pnpm y aparece el mismo error, es coherente con esta causa: los enlaces de node_modules apuntan fuera del proyecto.

Copias con enlaces duros y herramientas que escriben en node_modules

Si usas la opción 2 y ves cambios inesperados en el otro checkout después de instalar o generar código, es probable que ambas carpetas compartan archivos. En ese caso vuelve a la opción 1: una instalación por worktree es más predecible.

Resumen

  • Turbopack, el empaquetador por defecto de Next.js 16, no resuelve archivos fuera de su raíz; un node_modules enlazado hacia otro checkout cae en ese caso.
  • La solución más robusta es instalar dependencias en cada worktree; el almacén de pnpm usa enlaces duros, así que es barato si está en el mismo disco.
  • Alternativas: cp -al en el mismo sistema de archivos, --webpack como parche temporal o ampliar la raíz con turbopack.root.
  • Al borrar un enlace, quita solo el enlace y no el contenido de su destino.
  • Tras cualquier cambio, borra .next y verifica con next dev y next build.

Referencias

Comentarios