惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News | PayPal Newsroom
IT之家
IT之家
Jina AI
Jina AI
博客园 - 司徒正美
GbyAI
GbyAI
WordPress大学
WordPress大学
B
Blog
大猫的无限游戏
大猫的无限游戏
Y
Y Combinator Blog
阮一峰的网络日志
阮一峰的网络日志
Blog — PlanetScale
Blog — PlanetScale
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recorded Future
Recorded Future
T
Threat Research - Cisco Blogs
AWS News Blog
AWS News Blog
Latest news
Latest news
宝玉的分享
宝玉的分享
小众软件
小众软件
NISL@THU
NISL@THU
C
CERT Recently Published Vulnerability Notes
The GitHub Blog
The GitHub Blog
P
Privacy & Cybersecurity Law Blog
P
Palo Alto Networks Blog
Spread Privacy
Spread Privacy
Last Week in AI
Last Week in AI
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Proofpoint News Feed
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
量子位
博客园_首页
T
The Exploit Database - CXSecurity.com
The Cloudflare Blog
M
MIT News - Artificial intelligence
H
Help Net Security
Security Archives - TechRepublic
Security Archives - TechRepublic
V2EX - 技术
V2EX - 技术
I
InfoQ
D
Darknet – Hacking Tools, Hacker News & Cyber Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
O
OpenAI News
MongoDB | Blog
MongoDB | Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Privacy International News Feed
Microsoft Security Blog
Microsoft Security Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Google DeepMind News
Google DeepMind News
H
Hacker News: Front Page
W
WeLiveSecurity
N
News and Events Feed by Topic

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
pnpm workspaces en monorepo: el setup que sobrevivió CI en Railway y los problemas que los docs no anticipan
Juan Torchia · 2026-06-19 · via DEV Community

pnpm workspaces en monorepo: el setup que sobrevivió CI en Railway y los problemas que los docs no anticipan

La solución correcta para acelerar installs en un monorepo TypeScript es agregar más restricciones a la resolución de paquetes. Sé que suena raro — la intuición dice "si algo falla, aflojá la configuración". Pero con pnpm workspaces, aflojar el hoisting es exactamente lo que convierte un CI estable en un CI que falla de maneras distintas cada vez.

Mi tesis es esta: pnpm workspaces es la mejor opción para monorepos TypeScript en 2026, pero el path de felicidad de los docs esconde tres trampas que solo aparecen en CI con deployment real. No son edge cases raros. Son exactamente las cosas que pasan cuando el tutorial de 5 pasos funciona en local y el primer deploy en Railway devuelve un error que no aparece en ningún README.

Este post no es una guía de setup inicial. Es el análisis de lo que viene después del setup — cuando ya tenés el pnpm-workspace.yaml, el monorepo levanta localmente y CI empieza a romperse de formas que no tienen documentación directa.


El estado real de pnpm workspaces: qué dicen los docs y qué omiten

La documentación oficial de pnpm workspaces explica bien la mecánica base: un archivo pnpm-workspace.yaml en la raíz define los paquetes, workspace:* como protocolo para dependencias internas y pnpm install desde la raíz resuelve todo el grafo. Hasta ahí todo claro.

Lo que los docs no dicen explícitamente es qué pasa cuando ese grafo se reconstruye en un entorno CI sin el store local de pnpm. En una máquina de desarrollo, el content-addressable store de pnpm actúa como caché global y muchos errores de resolución se enmascaran. En Railway, cada build arranca desde cero — y ahí aparecen las trampas.

El setup mínimo que funciona como base:

# pnpm-workspace.yaml — en la raíz del repo
packages:
  - 'apps/*'      # Next.js, APIs, servicios
  - 'packages/*'  # UI components, utils, config compartida

// package.json raíz  scripts de orquestación
{
  "private": true,
  "scripts": {
    "build": "pnpm --filter='./apps/*' build",
    "dev": "pnpm --filter='./apps/*' dev --parallel",
    "typecheck": "pnpm -r typecheck"
  },
  "engines": {
    "node": ">=20",
    "pnpm": ">=9"
  }
}

Esto funciona. El problema viene cuando empezás a agregar complejidad real — un paquete compartido que usa una dependencia que otra app también usa, pero desde otra versión.


Las tres trampas que los docs no anticipan

Trampa 1: Phantom dependencies en CI

Las phantom dependencies son el problema más silencioso de pnpm workspaces. En npm y Yarn Classic, el node_modules flat permite que cualquier paquete importe cualquier otro que esté instalado en el árbol — aunque no lo declare como dependencia. pnpm, por diseño, rompe eso: cada paquete solo puede acceder a lo que declara explícitamente.

El problema es que en local, si alguna dependencia directa tiene a lodash como dependencia propia, puede que lo estés usando sin declararlo y funcione. En CI desde cero, la resolución puede variar y ese import explota.

// ❌ Esto puede funcionar en local y fallar en CI
// apps/dashboard/src/utils.ts
import { debounce } from 'lodash' // lodash no está en apps/dashboard/package.json

// ✅ La solución es declarar la dependencia explícitamente
// apps/dashboard/package.json
{
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

La manera de diagnosticar esto antes de que CI lo encuentre:

# Corré esto desde la raíz — lista dependencias usadas pero no declaradas
pnpm --filter='./apps/dashboard' ls --depth 0

# Alternativa: forzá la resolución estricta en local
# .npmrc en la raíz
node-linker=isolated

Con node-linker=isolated, pnpm crea node_modules con symlinks reales en lugar del modo por defecto. Hace que las phantom dependencies fallen en local antes de llegar a CI.

Trampa 2: shamefully-hoist en Railway — el trade-off que nadie te cuenta

La documentación de shamefully-hoist es honesta: el nombre es intencional, es una concesión de compatibilidad que pnpm considera un mal necesario. Lo que no explica es el patrón de falla específico en Railway.

Railway ejecuta el build desde el directorio del servicio que desplegás — no desde la raíz del monorepo. Si configurás shamefully-hoist=true en el .npmrc raíz, ese setting aplica en un pnpm install desde la raíz. Pero Railway, según cómo esté configurado el service, puede correr pnpm install desde apps/api y el .npmrc raíz no siempre se propaga como esperás.

# .npmrc en la raíz — esto NO garantiza que Railway lo use si instala desde un subdirectorio
shamefully-hoist=true

La solución más robusta no es shamefully-hoist. Es identificar qué paquete necesita el hoist y declararlo correctamente:

# .npmrc en la raíz — más granular y predecible en CI
# En lugar de hoist global, especificá qué paquetes necesitan ser hoisted
hoist-pattern[]=*eslint*
hoist-pattern[]=*prettier*
hoist-pattern[]=*typescript*

Esto hoistea solo las herramientas de desarrollo que realmente necesitan estar en el root node_modules — el caso más común son linters y el compilador de TypeScript cuando los configs están en la raíz. El resto de las dependencias mantiene la resolución estricta.

Para Railway específicamente, la configuración que tiende a ser más estable es deployar desde la raíz y configurar el build command del servicio para que filtre:

# Build command en Railway para el servicio apps/api
pnpm --filter=api build

# Install command en Railway — instalá desde la raíz siempre
pnpm install --frozen-lockfile

--frozen-lockfile es crítico en CI. Sin él, pnpm puede intentar actualizar el lockfile si encuentra inconsistencias — y eso puede enmascarar problemas reales o generar builds no reproducibles.

Trampa 3: Script filtering que no filtra lo que creés

pnpm --filter es poderoso pero tiene un comportamiento específico con las dependencias entre workspaces que confunde a casi todo el mundo la primera vez.

# Esto NO hace lo que parece en un monorepo con dependencias internas
pnpm --filter=dashboard build

# Si dashboard depende de packages/ui, este comando puede fallar
# porque packages/ui no está buildeado todavía

El flag --filter selecciona el paquete pero no resuelve el orden de build del grafo de dependencias internas automáticamente — a menos que uses el flag correcto:

# ✅ Esto sí buildea en el orden correcto del grafo
pnpm --filter=dashboard... build
# Los tres puntos significan: "dashboard y todo lo que dashboard depende"

# ✅ O más explícito todavía: build recursivo en orden topológico
pnpm -r --filter=dashboard... build

La documentación menciona esto, pero la diferencia entre --filter=dashboard y --filter=dashboard... está en una nota al pie que es fácil de saltear.

El otro gotcha con filtering: --parallel y el orden topológico son mutuamente excluyentes. Si usás --parallel, pnpm ejecuta los scripts en paralelo sin respetar el grafo de dependencias. Útil para dev (donde querés todos los watchers levantados), peligroso para build.

# ✅ dev en paralelo — todos los watchers al mismo tiempo
pnpm --filter='./apps/*' --parallel dev

# ❌ build en paralelo — puede fallar si apps/dashboard depende de packages/ui
pnpm --filter='./apps/*' --parallel build

# ✅ build respetando el grafo — más lento pero correcto
pnpm -r build


Errores comunes de configuración y cómo diagnosticarlos

Más allá de las tres trampas principales, hay un conjunto de errores de configuración que aparecen repetidamente en setups de monorepos con pnpm:

Lockfile desincronizado entre branches: Si dos branches modifican dependencias de paquetes distintos del monorepo y se mergean sin resolver el lockfile correctamente, CI puede pasar en ambas branches y fallar después del merge. --frozen-lockfile en CI convierte esto en un fallo ruidoso en lugar de un build silenciosamente inconsistente.

workspace:* vs versiones fijas: El protocolo workspace:* resuelve a la versión actual del paquete en el workspace. Esto es lo correcto para desarrollo. Pero si algún script de build o publicación no reemplaza workspace:* por la versión real antes de empaquetar, el paquete publicado no funciona fuera del monorepo. pnpm tiene pnpm publish --recursive que hace este reemplazo, pero si usás un builder custom en Railway, verificá que esto esté contemplado.

TypeScript paths y aliases que no atraviesan el build: Un patrón común es definir @ui/* como alias de TypeScript en el tsconfig.json raíz, tener packages/ui como workspace y que todo funcione en local con el language server. En CI, si el builder de apps/dashboard no hereda los path aliases correctamente, el build falla con errores de módulo no encontrado.

// tsconfig.base.json en la raíz
{
  "compilerOptions": {
    "paths": {
      "@ui/*": ["./packages/ui/src/*"]
    }
  }
}

// tsconfig.json en apps/dashboard  debe extender la base
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": "."
  }
}


Checklist: antes de hacer deploy a Railway con pnpm workspaces

Esto no es una garantía — es el conjunto de verificaciones que reduce la probabilidad de sorpresas en CI. Cada ítem es reproducible localmente:

  • [ ] pnpm install --frozen-lockfile pasa sin modificar el lockfile — si falla, hay una inconsistencia que hay que resolver antes de CI
  • [ ] Cada app buildea limpia desde la raíz con pnpm --filter=<app>... build — con los tres puntos para incluir dependencias internas
  • [ ] No hay phantom dependencies: pnpm --filter=<app> ls --depth 0 no muestra dependencias que no estén declaradas en el package.json del paquete
  • [ ] El .npmrc no usa shamefully-hoist=true sin motivo concreto — si lo necesitás, usá hoist-pattern[] con los paquetes específicos
  • [ ] Railway está configurado para instalar desde la raíz, no desde el subdirectorio del servicio — esto es configurable en el dashboard de Railway bajo "Root Directory"
  • [ ] El build command en Railway usa --filter con el nombre exacto del paquete según el campo name en su package.json, no el nombre del directorio
  • [ ] workspace:* se reemplaza correctamente si algún paquete se publica o se empaqueta fuera del monorepo

FAQ: pnpm workspaces en CI con Railway

¿Cuál es la diferencia entre pnpm -r build y pnpm --filter='./apps/*' build?

pnpm -r build ejecuta el script build en todos los paquetes del workspace que lo tengan definido, respetando el orden topológico del grafo de dependencias. pnpm --filter='./apps/*' build ejecuta build solo en los directorios bajo apps/, pero si esos paquetes dependen de algo en packages/, ese algo tiene que estar ya buildeado. Para CI, -r es más seguro. Para builds selectivos, usá --filter=<app>... con los tres puntos.

¿Por qué --frozen-lockfile es obligatorio en CI y no en local?

En local, pnpm puede actualizar el lockfile si encuentra que una dependencia cambió o si el lockfile no está completamente sincronizado. En CI, eso significa builds no reproducibles: dos corridas del mismo commit pueden instalar versiones distintas si el lockfile se actualiza entre medio. --frozen-lockfile hace que pnpm falle inmediatamente si el lockfile no coincide exactamente con el estado del package.json — lo que convertís en ruido audible en lugar de fallo silencioso.

¿Cuándo tiene sentido usar shamefully-hoist=true y cuándo no?

Tiene sentido como solución temporal cuando migrás un repo que venía de npm o Yarn Classic y tenés phantom dependencies masivas que no podés resolver de a una. Como estado permanente, no. El nombre refleja la postura de pnpm al respecto. La alternativa granular con hoist-pattern[] te da compatibilidad donde la necesitás (herramientas de CLI que buscan módulos en el root) sin comprometer el resto.

¿workspace:* o workspace:^ para dependencias internas?

workspace:* es la convención más usada y la que recomiendan los docs. Significa "la versión exacta que está en el workspace". workspace:^ permite compatibilidad semántica. Para paquetes internos de un monorepo que evolucionan juntos, workspace:* es más predecible — si rompés la API de packages/ui, querés que apps/dashboard falle explícitamente, no que intente resolver una versión compatible que ya no existe.

¿Cómo configuro Railway para que instale desde la raíz del monorepo?

En el dashboard de Railway, en la configuración del servicio, el campo "Root Directory" debería estar vacío o apuntar a la raíz del repo — no al subdirectorio del app. El "Build Command" debería ser algo como pnpm --filter=<nombre-del-app> build. Si dejás "Root Directory" apuntando al subdirectorio, Railway no va a encontrar el pnpm-workspace.yaml ni el lockfile raíz y el install va a fallar o generar un node_modules inconsistente.

¿Vale la pena pnpm workspaces sobre Turborepo o Nx para un monorepo TypeScript pequeño?

pnpm workspaces resuelve la instalación y la resolución de dependencias. Turborepo y Nx agregan una capa de orquestación de tasks con caché de outputs. Para un monorepo pequeño (dos o tres apps, uno o dos paquetes compartidos), pnpm workspaces solo es suficiente y es menos configuración. El salto a Turborepo empieza a justificarse cuando el pnpm -r build tarda más de lo que podés tolerar y necesitás caché de outputs — que es un problema diferente al de la resolución de dependencias.


Mi postura y el límite honesto de este análisis

pnpm workspaces es la herramienta correcta para monorepos TypeScript en 2026. El modelo de resolución estricta con el content-addressable store es mejor que el hoisting flat de npm o Yarn Classic — no por dogma, sino porque hace explícitas las dependencias que realmente necesitás declarar. Ese rigor es el que hace que las phantom dependencies exploten en local en lugar de en producción.

Lo que no compro es la narrativa de que "con pnpm todo funciona solo". El gap entre el tutorial de 5 pasos y un monorepo con tres apps, dos paquetes compartidos y deploy en Railway tiene fricción real. Phantom dependencies, hoisting config y script filtering son exactamente esa fricción — y vale la pena conocerla antes de encontrarla en un deploy fallido.

Lo que este análisis no puede garantizarte: las tres trampas que describí son patrones comunes documentados y reproducibles, pero el comportamiento exacto depende de las versiones específicas de pnpm (≥9 tiene algunos cambios de comportamiento respecto a v8), de cómo esté configurado el runtime de Railway en el momento en que leas esto y de la topología específica de tu monorepo. Los comandos y configs de este post son reproducibles — los resultados exactos en CI son función de variables que no controlo.

El próximo paso concreto: si tenés un monorepo con pnpm workspaces y querés validar que no tenés phantom dependencies antes de que CI las encuentre, empezá con node-linker=isolated en el .npmrc de desarrollo y corrés un pnpm install limpio. Si algo se rompe localmente, mejor ahora.


Para más contexto sobre decisiones de arquitectura en el stack TypeScript — cómo pienso el diseño de tokens de autenticación, el problema de caching en Next.js App Router o por qué Zod se rompe de tres maneras distintas en runtime — están en el blog.


Fuentes originales:


Este artículo fue publicado originalmente en juanchi.dev