wandres.dev
ELEGIR HERRAMIENTA · Turborepo vs Nx vs Moon

Migrar entre herramientas

Cambiar de orquestador —de Turborepo a Nx, de Nx a Moon o cualquier combinación— parece una reescritura y casi nunca lo es: lo que de verdad sostiene el monorepo vive en la capa de pnpm, que no se toca. Esta lección separa lo que sobrevive intacto a una migración —los workspaces, los scripts, la topología de tareas— de lo que hay que volver a declarar —el pipeline y la caché—, propone una transición incremental que corre los dos orquestadores en paralelo sin parar el desarrollo, y afila las señales que justifican migrar frente a las que aconsejan no hacerlo.

⏱ 16 min

Migrar entre orquestadores tiene fama de proyecto grande y arriesgado, y esa fama es casi siempre injusta. La razón está en la arquitectura de capas que estas lecciones insisten en separar: debajo vive pnpm con sus workspaces, y encima se sienta el orquestador —Turborepo, Nx o Moon— que se limita a leer ese sustrato para orquestar tareas. Migrar, entonces, no toca la capa que de verdad define tu monorepo; sustituye la capa superior, que es fina y reversible. El trabajo real de una migración no es mover código sino traducir una declaración: el mismo grafo de tareas que ya existe, expresado en otra sintaxis. Esta lección disecciona qué sobrevive intacto a ese cambio, qué hay que volver a escribir, cómo hacerlo sin detener el desarrollo ni un día, y —lo más importante— cómo distinguir las pocas señales que justifican migrar de las muchas que solo disfrazan una moda.

🎯 Al terminar esta lección sabrás
  • Separar lo que una migración conserva —pnpm workspaces, scripts, la topología del grafo— de lo que obliga a reescribir.
  • Entender que el pipeline y la caché no se trasladan: se vuelven a declarar y se recalculan desde cero.
  • Ejecutar una transición incremental que corra los dos orquestadores en paralelo sin parar el desarrollo.
  • Leer las señales que justifican migrar y, sobre todo, las que aconsejan quedarse donde estás.

Qué se conserva y qué cambia

Para migrar sin miedo hay que saber con exactitud qué se mueve y qué no. La respuesta es nítida porque sigue la frontera entre capas: todo lo que vive en la capa de pnpm se conserva intacto, y solo lo que vive en la capa de orquestación se reescribe.

Del lado de lo que se conserva está el sustrato entero. El pnpm-workspace.yaml que enumera tus paquetes no cambia una coma. Los scripts de cada package.json siguen ahí, porque son la definición real de qué hace cada tarea —compilar, testear, lintar— y ningún orquestador los reemplaza, solo los invoca. Y, sobre todo, se conserva lo más valioso y lo que más costó construir: la topología del grafo, es decir, qué paquete depende de cuál y en qué orden deben ejecutarse las tareas. Esa estructura es una propiedad de tu código, no del orquestador, y por eso sobrevive a cualquier cambio de herramienta.

A ese sustrato pertenece también toda la capa de instalación, que la migración ni roza: el pnpm-lock.yaml que fija las versiones exactas, el node_modules que pnpm enlaza desde su store, y el .npmrc con su política de hoisting siguen siendo asunto de pnpm y de nadie más. El orquestador nunca instala nada; se limita a ejecutar tareas sobre un árbol de dependencias que ya existe. Por eso una migración de orquestador y una migración de gestor de paquetes son operaciones de naturaleza distinta y de coste incomparable: la primera reescribe un archivo de pipeline; la segunda tocaría la capa que de verdad sostiene todo.

Del lado de lo que cambia hay, en realidad, una sola cosa expresada de dos formas: la declaración del pipeline y la configuración de la caché. Cada orquestador tiene su propio archivo y su propia sintaxis para decir lo mismo —qué tarea depende de qué, qué archivos son entrada, qué archivos son salida—. Migrar es traducir esa declaración de un dialecto a otro. La tabla siguiente muestra que la traducción es casi un isomorfismo: los conceptos son idénticos, solo cambian los nombres y el archivo donde viven.

Concepto Turborepo Nx Moon
archivo de configuración turbo.json nx.json y project.json .moon/workspace.yml y moon.yml
definición de tareas tasks targets tasks
dependencias entre tareas dependsOn dependsOn deps
entradas y salidas inputs y outputs inputs y outputs inputs y outputs
caché remota Vercel Remote Cache Nx Cloud remota compatible

La misma tarea de build, en dos dialectos, deja ver el isomorfismo con toda claridad. Turborepo envuelve el script que ya vive en el package.json; Moon prefiere declarar el comando explícito. Cambia la piel, no el esqueleto:

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**"],
      "outputs": ["dist/**"]
    }
  }
}
tasks:
  build:
    command: tsc --build
    deps: ["^:build"]
    inputs: ["src/**"]
    outputs: ["dist"]
flowchart TD
sustrato[pnpm workspaces scripts y grafo] --> conserva[se conserva intacto]
orquestador[capa de orquestacion] --> cambia[se vuelve a declarar]
cambia --> pipe[pipeline de tareas]
cambia --> cache[claves e inputs de cache]
style conserva fill:#a6e3a1,color:#11111b
style cambia fill:#eba0ac,color:#11111b

Hay una excepción crucial a la simetría, y es la caché. Aunque los conceptos de inputs y outputs se traducen uno a uno, la caché acumulada no se traslada: cada herramienta calcula el hash de una tarea con su propio algoritmo, así que un acierto de Turborepo no significa nada para Nx. El primer día tras migrar, tu caché remota vale cero y todo se reconstruye una vez. No es un fallo, es la naturaleza de la caché por contenido: el hash es un contrato interno de cada herramienta, y cambiar de herramienta lo invalida por completo.

📝
Los scripts son la verdad; el pipeline solo los ordena

Conviene tener clarísima la división de trabajo entre las dos capas. Los scripts de tus package.json contienen el qué: el comando concreto que compila o testea un paquete. El pipeline del orquestador contiene el cuándo y en qué orden: la relación de dependencia entre esas tareas. Migrar reescribe lo segundo y no toca lo primero, y por eso un tsc --build o un vitest run sobreviven idénticos a cualquier cambio de orquestador. Si al migrar te descubres reescribiendo comandos, algo va mal: esos comandos son de pnpm y de tus herramientas de base, no del orquestador que estás cambiando.

Una transición incremental sin parar el desarrollo

La forma ingenua de migrar es el big bang: arrancar el orquestador viejo, enchufar el nuevo y rezar, todo en un único PR gigante. Es tentador porque parece rápido, pero paraliza el desarrollo mientras se estabiliza y no deja vía de vuelta si algo falla en producción. La forma profesional es la contraria: operación en paralelo, donde ambos orquestadores conviven hasta que el nuevo se gana la confianza.

La estrategia se apoya en cuatro pasos, y su columna vertebral es que los scripts siguen siendo la fuente de verdad durante todo el proceso:

  1. Instala el nuevo junto al viejo, sin borrar nada. Ambos leen el mismo pnpm-workspace.yaml y los mismos scripts, así que coexisten sin conflicto: son dos lectores del mismo sustrato, no dos dueños que se disputan el repo.
  2. Traduce el pipeline tarea por tarea, de la más simple a la más compleja. Empieza por lint o typecheck, que no tienen dependencias, y sube hacia build y test. Cada tarea traducida se verifica antes de pasar a la siguiente.
  3. Corre los dos en el CI en paralelo un tiempo. El viejo sigue siendo la fuente de verdad que bloquea el merge; el nuevo corre como sombra que reporta pero no bloquea. Cuando el nuevo lleve semanas coincidiendo, invierte los papeles.
  4. Retira el viejo en un PR pequeño. Cuando el nuevo orquestador es la fuente de verdad y el equipo confía en él, un único commit que solo borra la configuración vieja cierra la migración.

Instalar los dos a la vez es literalmente un add en la raíz del workspace, y a partir de ahí cada uno se invoca con su propia sintaxis sobre el mismo grafo:

# ambos orquestadores instalados, leyendo el mismo workspace
pnpm add -Dw turbo nx
# el viejo manda en el merge; el nuevo corre en sombra
turbo run build
nx run-many -t build
flowchart LR
A[scripts como fuente de verdad] --> B[instala el nuevo en paralelo]
B --> C[traduce y compara una tarea]
C --> D[ci en sombra sin bloquear]
D --> E[invierte y retira el viejo]
style A fill:#89b4fa,color:#11111b
style E fill:#a6e3a1,color:#11111b

El punto de control de cada paso es la equivalencia de salidas. Antes de confiar en la traducción de una tarea, corre esa tarea con las dos herramientas sobre el mismo commit y compara sus artefactos: si el dist que produce Nx es byte a byte el que producía Turborepo, la traducción fue fiel. Esa comparación objetiva sustituye a la fe y convierte la migración en una serie de pequeñas verificaciones en vez de un salto ciego.

Hay un único trozo de fontanería que sí es nuevo y merece atención aparte: las credenciales de la caché remota. El pipeline se traduce, pero el token que autentica contra Vercel Remote Cache, Nx Cloud o el backend que uses es una pieza de infraestructura de CI que hay que aprovisionar de cero para el orquestador nuevo. Es poca cosa —una variable de entorno secreta y su conexión en el runner— pero es lo que separa un CI que reutiliza la caché de uno que reconstruye todo en cada corrida, así que conviene tenerlo listo antes de invertir los papeles.

💡
Coexistir es posible porque ambos leen el mismo sustrato

La clave técnica que hace viable la transición incremental es que los orquestadores no se pisan: Turborepo, Nx y Moon son todos lectores pasivos del mismo pnpm-workspace.yaml y de los mismos scripts. Instalar el segundo no desinstala ni altera al primero. Esto convierte lo que parecía una operación a corazón abierto en un cambio con red: mientras traduces y verificas, el orquestador viejo sigue construyendo y desplegando como siempre. El desarrollo no se detiene ni un commit, porque en todo momento hay una fuente de verdad que funciona.

⚠️
La caché arranca vacía: no confundas eso con una regresión

El primer CI tras invertir los papeles será lento, porque la caché remota del orquestador nuevo está vacía y reconstruye todo una vez. Es esperado y transitorio, pero si nadie lo anticipa se lee como una regresión de rendimiento y dispara una reversión innecesaria. Comunica de antemano que habrá un build frío, mide la segunda ejecución —ya con caché caliente— y compárala contra la línea base del orquestador viejo. La comparación honesta es caliente contra caliente, nunca frío contra caliente.

Señales para migrar y señales para no hacerlo

La parte técnica de una migración es la fácil; la difícil es decidir si debe ocurrir. Como la capa de orquestación es reversible y barata de cambiar, la tentación de migrar por cualquier motivo es alta, y ahí está el error. La regla es simple de enunciar y difícil de obedecer: migra por un dolor concreto y presente que la herramienta actual no resuelve; no migres por nada más.

🎯

Migra: un dolor concreto sin solución

Tu herramienta actual no expresa algo que necesitas y la nueva sí: divergencia de runtime entre máquinas que solo el toolchain de Moon fija, un repo que se volvió poliglota y pide el grafo y los generadores de Nx, o una escala que exige convenciones que ya no sostienes a mano.

🛟

Migra: la herramienta deja su nicho

El proyecto que usas cambia de rumbo, deja de mantenerse, o el modelo de precios de su caché remota deja de encajar. Migrar ante un riesgo de sostenibilidad real del proveedor es una decisión defensiva y legítima.

🚫

No migres: moda o envidia de pila

Migrar porque algo se reescribió en Rust, porque una empresa famosa anunció su stack, o porque el orquestador nuevo simplemente es más nuevo. Ninguna de esas es un dolor tuyo: son ruido que paga coste sin cobrar beneficio.

🪤

No migres: para huir de tu propia config

Si tu CI es lento porque tus tareas declaran mal sus inputs y outputs, cambiar de orquestador no lo arregla: el problema es tu configuración, no la herramienta, y se mudará intacto a la nueva.

La señal para no migrar más importante y más ignorada es la más aburrida: que la herramienta actual todavía encaja. Un orquestador que resuelve tu dolor de hoy no necesita ser reemplazado porque exista uno más admirado; la ausencia de dolor es una respuesta completa, no un vacío que haya que llenar con novedad. Antes de traducir una sola línea de pipeline, exige que exista un dolor que puedas escribir en una frase; si esa frase no aparece, la migración correcta es la que no haces.

Y cuando la decisión sí es legítima, presupuesta el periodo de paralelo como lo que es: la fase donde de verdad se paga la migración. Correr dos orquestadores en el CI cuesta minutos de runner y atención del equipo, y ese doble gasto dura semanas, no horas. Es dinero bien invertido —compra la certeza de que el nuevo es equivalente antes de depender de él— pero hay que nombrarlo por adelantado, porque una migración que nadie presupuestó se abandona a medias en cuanto el primer sprint aprieta, y un repo con dos orquestadores instalados y ninguno retirado es peor que cualquiera de los dos por separado.

ℹ️
Reversible no es gratis

Que la capa sea reversible rebaja el riesgo de equivocarse, no el coste de moverse. Cada migración tiene un precio real: la caché fría del primer build, las semanas de doble CI, el reaprendizaje del equipo, la superficie nueva que mantener. La reversibilidad significa que si te equivocas puedes volver, no que moverte sea indoloro. Por eso el listón para migrar no es puedo revertir si sale mal, sino tengo un dolor presente que esta migración resuelve y el actual no. Confundir la baratura de la reversión con la gratuidad del movimiento es cómo un equipo acaba migrando cada seis meses sin construir nada encima.

Lo que se conserva es el activo; lo que cambia es la piel

Que migrar entre orquestadores sea tan barato no es un accidente afortunado: es la recompensa diferida de una decisión de arquitectura tomada mucho antes, la de no dejar que la herramienta de orquestación absorbiera la capa de workspace. Cada vez que estas lecciones insistieron en que pnpm es el sustrato y el orquestador solo se sienta encima, estaban comprando exactamente esta opción: la de cambiar de opinión mañana sin reescribir nada esencial. Una migración, vista así, es una prueba de fuego de tu arquitectura: si duele, es que en algún momento dejaste que las capas se enredaran —un comando que en realidad terminó viviendo en la config del orquestador, una convención de la plataforma que se filtró en tu código de producto—; si es casi trivial, es que mantuviste la frontera limpia. El diff de una buena migración lo demuestra sin palabras: borra un archivo de configuración, añade otro, y no toca ni una línea de tu código de aplicación ni de tu pnpm-workspace.yaml. Pero la lección más honda no es técnica sino de criterio. La facilidad de migrar es una espada de doble filo: la misma reversibilidad que te protege de un error te tienta a moverte sin motivo, a coleccionar orquestadores como quien colecciona modas, a confundir actividad con progreso. El ingeniero maduro invierte esa lógica: precisamente porque migrar es barato, se permite ser exigente con el porqué, no laxo. Migra cuando un dolor concreto y presente lo pide, y solo entonces; se queda quieto ante la novedad, la reescritura en Rust de turno, el anuncio de la empresa famosa, porque sabe que ninguna de esas cosas es un problema suyo. Y entiende la asimetría final: lo que se conserva en una migración —los workspaces, los scripts, la topología del grafo— es el activo real, el que costó años construir y encierra el conocimiento de cómo encaja tu sistema; lo que cambia —la sintaxis de un pipeline— es la parte barata y desechable. Confundir las dos, tratar el orquestador como si fuera la arquitectura y no su capa más fina y sustituible, es el error de fondo del que nacen todos los demás. Quien lo tiene claro no migra herramientas por deporte: cuida el sustrato, y cambia de orquestador cuando le conviene, sin ceremonia y sin miedo.

⚔️ Diseña tu migración o justifica no hacerla
  1. Escribe en dos columnas qué conservaría y qué reescribiría una migración de tu orquestador actual a otro: pnpm-workspace.yaml y scripts en la primera, pipeline y caché en la segunda.
  2. Traduce a mano una sola tarea —la de lint o typecheck— del formato de tu orquestador actual al de otro, y confirma que el comando subyacente no cambia ni una letra.
  3. Instala el segundo orquestador junto al actual sin borrar nada, y comprueba que ambos leen el mismo workspace sin conflicto.
  4. Corre esa única tarea con las dos herramientas sobre el mismo commit y verifica que producen la misma salida; esa equivalencia es tu prueba de traducción fiel.
  5. Antes de seguir, escribe el dolor concreto y presente que justifica la migración; si no encuentras ninguno y solo hallas curiosidad o moda, cierra el experimento y quédate donde estás: acabas de ahorrarte una migración innecesaria.