Boltdocs 3.2.0 — Warm Builds 5x Más Rápidos, Syntax Highlighting con WASM y Timing del Pipeline

3.2.0 hace los warm builds 5x más rápidos al arreglar el skip del server build, agrega syntax highlighting potenciado por WASM, paraleliza el procesamiento del CSS crítico y te muestra exactamente dónde se va el tiempo del build.
Esta se trata de hacer que la caché realmente funcione
3.1.0 trajo los cimientos — modo turbo, Sätteri, zig-critters. 3.2.0 es sobre lo que pasa cuando construyes de nuevo. Y otra vez. Y otra vez.
Los warm builds (sin cambios de código) bajaron de ~50s a ~10s — una aceleración de 5x. El server Vite build ahora se salta correctamente cuando nada cambia, el hash del cliente usa un pre-check ligero, y el procesamiento del CSS crítico corre en paralelo entre los cores del CPU.
El problema de la caché
Antes de 3.2.0, cada build ejecutaba el server Vite build completo desde cero — incluso cuando nada había cambiado. La causa raíz era una sola línea al final de cada build:
await fs.remove(join(root, '.vite-react-ssg-temp'))
Esto eliminaba todo el directorio de output del build SSR. El check serverBuildSkipped del siguiente build necesitaba que ese directorio existiera — pero siempre estaba eliminado. El resultado: el paso más costoso del pipeline (bundling Vite SSR) se ejecutaba en cada build, haciendo la caché inútil.
La corrección: No eliminar el directorio temporal del SSR cuando el client build fue saltado. Si el hash del código del cliente no cambió, el bundle SSR tampoco cambió — no es necesario reconstruirlo.
Resultados del benchmark
| Tipo de build | Antes de 3.2.0 | Después de 3.2.0 | Aceleración |
|---|---|---|---|
| Cold (primer build) | ~82s | ~78s | ~1.05x |
| Warm (sin cambios) | ~50s | ~10s | 5x |
| Incremental (1 archivo editado) | ~50s | ~15s | 3.3x |
El cold build apenas cambia porque el server Vite build debe ejecutarse. Pero los warm e incrementales ahora lo saltan por completo — el bundle SSR se reutiliza del build anterior.
Caché mtime: llamadas stat 5.9x más rápidas
Cada FileCache.get() ejecutaba fs.statSync() para verificar si un archivo había cambiado — incluso cuando el mismo archivo se verificaba milisegundos después. Para 500 archivos en múltiples capas de caché, esto significaba miles de llamadas síncronas bloqueantes por build.
La nueva caché mtime en memoria almacena { mtime, ts } por archivo con un TTL de 2 segundos. Dentro del TTL, la llamada a stat se salta por completo:
const MTIME_TTL_MS = 2000
const mtimeCache = new Map<string, { mtime: number; ts: number }>()
export function getFileMtime(filePath: string): number {
const now = Date.now()
const cached = mtimeCache.get(filePath)
if (cached && now - cached.ts < MTIME_TTL_MS) return cached.mtime
const mtime = fs.statSync(filePath).mtimeMs
mtimeCache.set(filePath, { mtime, ts: now })
return mtime
}
| Métrica | Antes | Después | Aceleración |
|---|---|---|---|
| 5 archivos × 500 rondas | 18.6ms | 3.2ms | 5.9x |
Hash del cliente: stat único por archivo
computeClientCodeHash() ejecutaba fs.statSync() hasta 3 veces por archivo — una para el pre-check Math.max(), otra para el hash, y otra para persistir el meta. La nueva implementación hace un solo pase, recolectando todos los stats y reutilizándolos para cada propósito:
// Pase único: stat de cada archivo una sola vez
const fileStats = files.map(file => {
const stat = fs.statSync(file)
return { file, mtime: stat.mtimeMs, size: stat.size }
})
// Pre-check usa fileStats — sin llamadas stat extra
const lastMtime = Math.max(...fileStats.map(s => s.mtime))
// Hash usa los mismos fileStats
for (const { file, mtime, size } of fileStats) {
hasher.update(relative(root, file)).update(mtime.toString()).update(size.toString())
}
Para un proyecto con 500 archivos, esto reduce las llamadas stat de ~1500 a 500 — una reducción del 66%.
Caché MDX: ruta + mtime en dev
La clave del caché MDX anteriormente incluía un hash de contenido (crypto.createHash('md5').update(code)), lo que significaba que el caché se invalidaba en cada pulsación de tecla. En modo dev, el caché ahora usa ruta del archivo + mtime en su lugar:
// Antes: invalidado en cada cambio de contenido
const cacheKey = `${cleanId}:${contentHash}:${isProd}:${MDX_PLUGIN_VERSION}`
// Después: sobrevive entre reinicios de dev cuando no hay cambios
const cacheKey = isDev
? `${cleanId}:${getFileMtime(cleanId)}:${isProd}:${MDX_PLUGIN_VERSION}`
? `${cleanId}:${contentHash}:${isProd}:${MDX_PLUGIN_VERSION}`
Esto significa que el caché de transformación MDX se reutiliza cuando reiniciás el servidor de desarrollo sin cambiar ningún archivo — sin recompilar 176 archivos MDX desde cero.
Concurrencia acotada para parsing de rutas
El parsing de rutas usaba Promise.all(files.map(...)) con concurrencia ilimitada — lanzando todos los parsers simultáneamente. Para proyectos con cientos de archivos, esto causaba presión de memoria y contención de I/O.
Ahora limitado a 32 workers concurrentes:
parsed = await runWithConcurrency(files, 32, async (file) => {
const cached = docCache.get(file)
if (cached) return cached
const result = await parseDocFile(file, docsDir, finalBasePath, config)
docCache.set(file, result)
return result
})
El patrón de worker pool toma elementos de una cola compartida, manteniendo exactamente 32 parsers ejecutándose en cualquier momento — suficiente para saturar I/O sin sobrecargar la memoria.
HMR: lookup O(1) del module graph
Cuando un archivo cambiaba, el handler HMR buscaba en fileToModulesMap de Vite con un scan bruto O(N) — iterando cada entrada y comparando keys decodificadas en minúsculas. Para proyectos grandes con miles de módulos, esto añadía latencia a cada edición de contenido.
La corrección construye un índice pre-computado en minúsculas para uso inicial:
let lowerModuleIndex: Map<string, any> | null = null
function getLowerModuleIndex(): Map<string, any> {
if (lowerModuleIndex) return lowerModuleIndex
lowerModuleIndex = new Map()
for (const [key, value] of server.moduleGraph.fileToModulesMap.entries()) {
lowerModuleIndex.set(decodeURIComponent(key).toLowerCase(), value)
}
return lowerModuleIndex
}
El índice se invalida cuando el module graph cambia (onFileChange), manteniendo los lookups en O(1) y la consistencia.
docCache: sin lecturas de disco redundantes
docCache.load() se ejecutaba en cada invocación de generateRoutes() — leyendo todo el cache del disco (potencialmente JSON comprimido) incluso cuando ya estaba cargado en memoria. Ahora un flag loaded evita la re-lectura:
async load(): Promise<void> {
if (this.loaded) return // Saltar si ya está en memoria
// ... leer del disco ...
this.loaded = true
}
invalidateAll(): void {
this.entries.clear()
this.loaded = false // Resetear en invalidación
}
Prewarming con priorización de rutas
Antes, todas las rutas se prewarmeaban en orden arbitrario. Páginas como /docs/ y /docs/getting-started son visitadas primero por la mayoría de usuarios, pero no se les daba prioridad.
Ahora las rutas se ordenan por prioridad antes de agruparlas:
const PRIORITY_PATTERNS = [/\/index\./i, /\/getting-started/i, /\/intro/i, /\/readme/i]
const files = routes
.filter(r => r.filePath)
.map(r => r.filePath)
.sort((a, b) => getRoutePriority(a) - getRoutePriority(b))
El delay también se incrementó de 0ms a 150ms, dando a la primera petición de página una ventaja antes de que el prewarming consuma CPU.
Motor Shiki WASM
El resaltado de sintaxis ahora usa el motor Oniguruma basado en WASM en lugar del motor de regex JavaScript:
// Antes
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
engine: createJavaScriptRegexEngine()
// Después
import { createOnigurumaEngine } from '@shikijs/engine-oniguruma'
engine: createOnigurumaEngine(import('shiki/wasm'))
| Métrica | JS Regex | WASM Oniguruma | Diferencia |
|---|---|---|---|
| 3 langs × 50 rondas | 1101ms | 959ms | 13% más rápido |
| Por iteración | 22.0ms | 17.7ms | 4.3ms ahorrados |
Concurrencia del CSS crítico
El procesador de CSS crítico Beasties corría con concurrency: 1 — procesando 174 páginas una a la vez. Ahora corre con concurrency: min(cpus, 4):
// Antes
const crittersQueue = new PQueue({ concurrency: 1 })
// Después
const crittersQueue = new PQueue({ concurrency: Math.min(os.cpus().length, 4) })
Paralelismo del pipeline
Dos pasos independientes del pipeline — validación SEO y generación de tipos — ahora corren en paralelo:
// Antes
.addStep(new SEOValidateStep())
.addStep(new TypeGenerateStep())
// Después
.addParallelSteps([new SEOValidateStep(), new TypeGenerateStep()])
Timing del pipeline
El build ahora reporta el tiempo por pasos:
[pipeline] Build steps:
ConfigResolve 52ms
RouteGenerate 11ms
SEOValidate 7ms
TypeGenerate 7ms
SSGBuild 5.6s
SEOWrite 4ms
Total 6.7s
Modo dev: sin gzip
TransformCache estaba comprimiendo con gzip cada shard del caché al escribir — incluso en modo dev. Ahora la compresión se salta cuando NODE_ENV !== 'production'.
Un bundle de Mermaid que ya no viaja al navegador
Si una página tiene diagramas, esperás que tarde. Mermaid.js son ~800 KB de d3, DOMPurify, motores de layout y parsers por diagrama — todo code-split on-demand. En páginas sin diagramas, 2.8.0 ya te ahorraba ~27 KB con un import dinámico. Ayudaba, pero seguía dejando casi todo el costo en las páginas que sí usaban Mermaid.
3.2.0 lo elimina por completo para builds de producción.
La arquitectura:
- En build time, el plugin Remark recorre cada bloque
```mermaidy lo envía a un worker persistente de Node.js (render-worker.mjs) que carga el motor headless de Mermaid (vía Playwright + jsdom + DOMPurify) y renderiza ambas variantes de tema (claro y oscuro) a SVG. - Esos dos SVGs se adjuntan al componente
<Mermaid />como propssvgLightysvgDark— viajan en el output SSG como strings planos, sin fetch extra en runtime. - El plugin registra un alias de Vite que cambia
@bdocs/plugin-mermaid/clientpor@bdocs/plugin-mermaid/client/staticcada vez que correvite build. El componente estático (mermaid-static.tsx, servido vía el pipeline derender-worker) nunca referenciamermaid— solo hacedangerouslySetInnerHTMLdel SVG pre-renderizado. - En dev el alias no se aplica, así el HMR conserva el renderer del cliente. O podés optar explícitamente por no participar con
mermaidPlugin({ preRender: false }).
~800 KB de chunks code-split de Mermaid — architectureDiagram, sequenceDiagram, d3, DOMPurify, motores de layout — salen del bundle del cliente por completo. Las páginas renderizan el diagrama como SVG inline: cero round-trip de JS, sin FOUC, sin flash de fallback.
Antes vs. después
| Tipo de página | Antes de 3.2.0 | Después de 3.2.0 |
|---|---|---|
| Página sin diagramas | ~0 KB runtime mermaid (import dinámico en useEffect) | ~0 KB — el componente estático nunca referencia mermaid |
| Página con diagramas | ~800 KB de chunks mermaid cargados on-demand, luego mermaid.initialize + render | ~0 KB — el SVG ya está en el HTML inicial |
| Primer paint del diagrama | Después de que el bundle del cliente y el motor inicializan | Instantáneo — el SVG viaja con la página |
| Cambio de tema | Re-ejecuta mermaid.initialize y re-renderiza en cada cambio | Instantáneo — intercambia svgLight ↔ svgDark |
Llamadas a mermaid.initialize() en el cliente por página | 1 (una por mount) | 0 — los SVGs light/dark van serializados en el HTML |
Trade-offs
Esto es opt-out, no opt-in — usá el flag preRender: false. Tres cosas que saber:
- Tiempo de build: cada bloque
```mermaidse renderiza una sola vez a través del worker de Playwright. Para la mayoría de proyectos añade solo unos segundos; para proyectos con miles de diagramas puede costar más. - Fallos de pre-render: si Mermaid falla en un diagrama (por ejemplo, error de sintaxis), el componente estático cae al código fuente crudo. Verás un
warnen build time. - Diagramas verdaderamente dinámicos: este camino es para SVGs estáticos. Si necesitás diagramas runtime, usá
mermaidPlugin({ preRender: false }).
Animaciones del fullscreen de Mermaid
El overlay de fullscreen recibió animaciones de entrada/salida — un detalle pequeño que hace que el feel coincida con el resto del sitio.
Otras mejoras
addParallelSteps()del Pipeline — nueva API para ejecutar pasos independientes concurrentemente- Skip del server build preservado entre builds — el output SSR ahora vive bajo
.boltdocs/build/ssr/y ya no se elimina cuando el código del cliente no ha cambiado - Persistencia del meta del hash —
hash-meta.jsonpara validación rápida de caché - Corrección del scroll de páginas externas — removido
overflow: hiddendehtml, body - Clases Tailwind inválidas corregidas —
from-bg-main→from-main,-z-1→-z-[1]
Reducción del peso del paquete
3.2.0 pasa las herramientas más pesadas del framework por la misma triaje que el pipeline de build: inclui en el bundle lo que se ejecuta en cada página, diferí lo que se ejecuta solo a veces, subí a peer lo que el consumidor ya tiene. El resultado es un install notablemente más chico, un bundle del cliente code-split, y un node_modules más liviano para los sitios que no usan toda la functionality.
Por qué importa
El install de 3.1.x cargaba react-aria-components como dependencia dura junto al toolchain completo de pre-render de Mermaid, todos los iconos de lenguajes, y los binarios nativos del image optimizer — incluso para usuarios que nunca renderizaron un diagrama Mermaid, nunca tuvieron un code block y nunca necesitaron sharp/svgo. 3.2.0 separa el qué del cuándo.
Antes vs. después
Dos categorías de "tamaño" — bundle del cliente (bytes que viajan al navegador) y footprint de node_modules (bytes desempaquetados en disco por install del consumidor) — medidas por separado para que puedas leer la tabla de arriba a abajo sin confusión.
Bundle del cliente — bytes que viajan al navegador (por cold load)
| Superficie | Antes de 3.2.0 | Después de 3.2.0 | Notas |
|---|---|---|---|
client/index.js (puerta de entrada) | 108 KB | 108 KB | Sin cambios en la entrada; el ahorro vive en los chunks diferidos. |
icons-dev.tsx (blob de iconos de lenguajes) en el chunk inicial | 44 KB en cada página | 0 KB en todas las páginas | El módulo completo lang-icons.tsx (doce iconos SVG + mapa del registry) fue eliminado del core. Los títulos de los code blocks renderizan solo el texto del nombre del archivo — no se envía ningún ícono de lenguaje al cliente. Las páginas con code blocks ahorran ~17 KB versus el bundle eager de 3.1.x; las páginas sin code blocks ya estaban en cero bytes para estos íconos. |
| Iconos sociales/nav en el bundle del navbar | mezclados con todo | ~1 KB aislados en icons-prod.tsx | Github, Discord, XSocial, Bluesky están namespaced aparte para que layouts que no los necesiten puedan sobreescribirlos sin arrastrar el set de lenguajes. |
Footprint de node_modules — bytes desempaquetados (por pnpm install)
| Superficie | Antes de 3.2.0 | Después de 3.2.0 | Notas |
|---|---|---|---|
react-aria-components (desempaquetado) | ~1 MB transitivo en node_modules | 0 KB en core | Ahora un peer requerido — el consumidor lo instala una vez junto a React. Sin doble-bundling. |
Binarios nativos sharp + svgo | ~35 MB desempaquetados en cada install del sitio | 0 KB en core; instala solo si usás @bdocs/plugin-image-optimizer | Movidos a peerDependenciesMeta.optional del plugin de imágenes. Usuarios de Alpine ARM, musl libc o glibc viejo ya no rompen el postinstall de sharp. |
shiki + @shikijs/engine-oniguruma + @mdx-js/rollup | Igual que 3.1.x | Igual que 3.1.x | Quedan en dependencies — el CLI los importa incondicionalmente cuando corre npx boltdocs build. Nunca llegan al bundle del cliente. |
Sección optionalDependencies | presente (vacía) | removida por completo | Se elimina la "trampa del silent-install" de optionalDependencies. |
| Pregunta: "¿mi sitio instala 35 MB de binarios nativos para renderizar docs?" | Sí | No — solo si optás por el image optimizer | La mayoría de los sitios dicen "no, gracias" y se ahorran el costo nativo. |
Qué cambió (semver-wise: minor)
Esta release cae cómodamente en el bin semver-minor porque los contratos de la API pública siguen estrictos. Esta es la auditoría:
-
react-aria-components— promovida dedependenciesa peer requerido. Si ya usás Boltdocs, instalá una vez junto a React. Si no, el próximopnpm installmostrará un peer-warning que podés ignorar si tu wrapper de framework provee React Aria, o satisfacer conpnpm add react-aria-components. El peer no está marcado comooptional, así que un peer faltante es un install-time warning duro — pero el runtime ya lo importaba antes, así que el comportamiento binario no cambió. -
sharpysvgo— se movieron fuera de core por completo. Antes se instalaban transitivamente a través de boltdocs; ahora son peer de@bdocs/plugin-image-optimizerconpeerDependenciesMeta.optional: true. Si tu sitio no usa el image optimizer, ahorrás ~35 MB de binarios nativos desempaquetados. Si lo usás, obtenés los mismos binarios — pnpm los hoistea vía las peer declarations del plugin. -
Iconos de lenguajes en code blocks MDX — los doce iconos de lenguajes (TypeScript, JavaScript, React, JSON, CSS, HTML, Markdown, Shell, YAML, Rust, TOML, CSV) estuvieron bundleados eager en 3.1.x, luego lazy-loaded en
lang-icons.tsx. El módulo completo fue ahora eliminado del core. Los títulos de los code blocks renderizan el texto del nombre del archivo con un íconoFilegenérico — no se envía al cliente ningún ícono por idioma. Las páginas con code blocks ahorran ~17 KB versus 3.1.x; las páginas sin code blocks ya estaban en cero bytes. -
Iconos sociales/nav —
Github,Discord,XSocial,Blueskyse movieron a un nuevo archivoicons-prod.tsxque se bundlea eager con el navbar. Están tipados aparte (IconProps) y se exportan de su propio entry para que layouts custom puedan sobreescribirlos sin tocar el set de lenguajes. -
Shape test — un nuevo
packages/core/tests/package-shape.test.tsfija el contrato de dependencias: aserta quereact-aria-componentses peer duro,shiki/@shikijs/engine-oniguruma/@mdx-js/rolluppermanecen endependencies,sharp/svgono están en core, ypeerDependenciesMetaestá ausente por completo. Esto significa que cualquier PR futuro que re-infle el bundle falla CI antes de review. -
Subpath exports del cliente preservados —
'boltdocs/client','boltdocs/primitives','boltdocs/mdx','boltdocs/server'siguen separados para que las apps puedan importar selectivamente sin sobre-bundlear.
Migración en un paste
// package.json
{
"dependencies": {
// ...existentes...
"boltdocs": "^3.2.0",
// AGREGÁ esta línea (o aceptá el peer warning):
"react-aria-components": "^1.16.0" // antes transitivo; ahora explícito
}
}
Sin cambios de código. Sin cambios de configuración. El manejo de peer de plugin/image-optimizer no cambia si ya lo tenías instalado.
Setups de CI / lockfile estrictos
Los equipos que rompen builds con peer warnings (Husky pre-commit, políticas de Renovate, monorepos con pnpm install --frozen-lockfile --strict-peer-dependencies) necesitan una sola línea en .npmrc para silenciar solo este peer warning sin ocultar roturas reales:
Elegí uno, no ambos. Usá .npmrc O .pnpmrc, no los dos. Elegir los dos puede duplicar el hoist o caer en una quirk de parsing de pnpm (public-hoist-pattern[] es sintaxis de array estilo npm; pnpm lee .pnpmrc como una línea por setting). La mayoría de los equipos solo necesita uno de los dos.
# .npmrc
# Permite solo el peer advisory documentado de boltdocs@3.2 → react-aria-components.
# NO desactives globalmente los peer checks con legacy-peer-deps.
public-hoist-pattern[]=*react-aria-components*
O, equivalentemente en .pnpmrc (sintaxis pnpm-nativa — sin corchetes):
# .pnpmrc
# Hacés que react-aria-components sea un peer explícito y transparente en el lockfile.
# `.pnpmrc` NO usa la sintaxis `[]` de array — la forma nativa de pnpm es sin corchetes por línea.
public-hoist-pattern=*react-aria-components*
peerDependencyRules.allowedVersions.react-aria-components=^1.16.0
No uses legacy-peer-deps=true. Eso silencia cada peer warning en todo el árbol y va a enmascarar futuras roturas reales.
Leé la auditoría completa en Actualizar a 3.2.0 → Cambios semi-breaking.
Nuevo paquete @bdocs/unist-utils
Boltdocs 3.2.0 extrae las utilidades compartidas de AST en un paquete npm independiente:
@bdocs/unist-utils. Antes de esta release los mismos helpers (visitNodes,
createMdxElement, setNodeProperty, parseMetaString, etc.) vivían en dos
lugares dentro del monorepo con tipados sutilmente diferentes; ahora tienen una
única fuente de verdad.
pnpm add @bdocs/unist-utils
Por qué esto importa para autores de plugins:
- 100% del surface tipado. Sin
any, sinunknownen los límites.Node,Parent,ElementNode,MdxJsxElementestán todos declarados en el paquete para que los autores de plugins puedan escribir código completamente tipado. - Desacoplado de los internos de boltdocs. Los plugins externos ya no necesitan meterse en el barrel interno de Boltdocs o escribir sus propios helpers de AST con tipos más laxos.
- Re-exportado por
boltdocspara back-compat. El código viejo que importa desde'boltdocs'sigue funcionando. Los proyectos nuevos deberían preferirimport ... from '@bdocs/unist-utils'.
El paquete incluye: visitors (visitNodes, visitRehypeElements, visitMdxElements), builders (createMdxAttribute, createMdxElement, createRehypeElement), helpers de h-properties, mutación de class-list (addNodeClass, removeNodeClass, hasNodeClass), parseo de meta strings (parseMetaString), y type guards.
Consultá la doc completa de @bdocs/unist-utils para la referencia de API, guía de migración y ejemplos.
Plugin API enriquecida
Cada hook de lifecycle de plugin ahora recibe un PluginContext más rico con siete namespaces enriquecidos. El subsistema de slots (ctx.slots / BoltdocsPlugin.slots / virtual:boltdocs-layout-slots / diagnósticos con prefijo slots-) se removió por completo. Los namespaces restantes — caches, diagnostics, paths, virtual modules, middleware, server, hmr — funcionan exactamente como antes:
ctx.caches — PluginCachesAPI
Tres tipos de caché, cada uno con un scope namespaced para que los plugins nunca colisionen:
// Transform cache (sharded, hash-keyed, async)
const cache = ctx.caches.transform('my-plugin')
await cache.get('key') // → string | null
cache.set('key', value)
// Routes cache (read/write/invalidate por file path)
const route = ctx.caches.routes.get('/abs/path/page.mdx')
ctx.caches.routes.invalidate('/abs/path/page.mdx')
// In-memory FIFO cache (Map-backed, sin dependencias externas)
const mem = ctx.caches.memory<MyType>('my-plugin', { max: 50, ttl: 60_000 })
mem.set('key', value)
mem.get('key')
ctx.diagnostics — PluginDiagnosticsAPI
Canal estructurado de diagnósticos en lugar de spam al logger. Los registros se acumulan en una cola FIFO capada (256 entradas) que herramientas downstream pueden drenar:
ctx.diagnostics.report(
'warn',
'MY_PLUGIN_SLOW',
'Transformation took 3.2s',
{ filePath: '/abs/path/page.mdx' },
)
const allRecords = ctx.diagnostics.list()
ctx.diagnostics.clear()
ctx.paths — PluginPathsAPI
Resolución segura de paths con validación anti-traversal. Cada resultado está garantizado a quedar dentro del límite del workspace, rechazando escapes .. e inyección de paths absolutos:
const mdxFile = ctx.paths.resolveDocs('guides', 'start.mdx')
const asset = ctx.paths.resolveAsset('public', 'logo.webp')
const url = ctx.paths.safeFileURL('/abs/path/diagram.svg')
// Throws — el path escapa del workspace
ctx.paths.resolveDocs('../../etc/passwd')
ctx.virtualModules — PluginVirtualModulesAPI
Declará módulos virtuales que Vite resuelve y carga sin tocar el sistema de archivos. Las registrations ocurren dentro de beforeBuild / beforeDev y se flushean ante cambios de configuración:
ctx.virtualModules.add(
'virtual:my-plugin/theme.css',
() => `:root { --primary: #6366f1; }`,
)
Reglas: ids duplicados lanzan error, el prefijo virtual:boltdocs-* está reservado, y el flag eager existe para futura auto-inyección.
Consultá la Plugin API Reference completa. La API se separó en páginas dedicadas — Caches, Diagnostics, Paths, Virtual Modules, Middleware, Server, y HMR.
API de Transform Middleware
Los plugins ahora pueden registrar middleware de transform standalone — funciones más pequeñas y enfocadas que transforman source, MDX, o HTML en un pipeline.
A diferencia de los lifecycle hooks, el middleware puede declararse estáticamente vía BoltdocsPlugin.middleware o registrarse programáticamente desde cualquier hook via ctx.middleware.add(). Cada middleware soporta orden por enforce y control de cadena con señales (__signal: 'skip' / __signal: 'break'):
const plugin = {
name: 'my-plugin',
middleware: [{
name: 'inject-copyright',
transformHtml: async (_ctx, { html, path }) => ({
html: html.replace('</body>', '<footer>© 2026</footer></body>'),
}),
}],
}
Consultá la Middleware API docs para la referencia completa.
Nuevo componente MDX <Timeline>
3.2.0 trae un nuevo componente MDX de timeline vertical pensado para changelogs, notas de release, actualizaciones de estado y bitácoras de auditoría. Cada entrada tiene un punto de color enganchado a la línea conectora, una fecha y etiqueta opcionales, un título y un cuerpo en Markdown.
<Timeline>
<Timeline.Item
date="2026-07-20"
title="Boltdocs 3.2.0"
badge="Major"
icon={<Sparkles />}
>
Lanzamiento de la **Plugin v3.2 API** — caches, diagnostics, paths, virtual
modules, middleware, server y hooks HMR. Consultá la [guía de migración](/es/blog/boltdocs-3.2.0).
</Timeline.Item>
<Timeline.Item
date="2026-06-01"
title="Soporte de i18n"
badge={{ text: 'Minor', variant: 'success' }}
>
Convención de internacionalización por filesystem, rutas conscientes de versión y prefijos SSR-safe.
</Timeline.Item>
</Timeline>
Se renderiza como:
● 20 jul 2026 [MAJOR]
│ Boltdocs 3.2.0
│ Lanzamiento de la Plugin v3.2 API — caches, diagnostics, paths, virtual
│ modules, middleware, server y hooks HMR. Consultá la guía de migración.
│
● 1 jun 2026 [MINOR]
│ Soporte de i18n
│ Convención de internacionalización por filesystem, rutas conscientes de versión y prefijos SSR-safe.
Qué trae
- Auto-registrado — escribí
<Timeline>en cualquier archivo.mdx. Sin import. - Dos familias de variantes — semánticas (
primary,success,info,warning,danger) más aliases de ciclo de vida (major,minor,patch,new,deprecated,breaking). - Fechas localizadas con hidratación segura — la prop
datese renderiza viatoLocaleDateStringcon una locale fija ('en-US'por defecto). - El cuerpo acepta Markdown completo — párrafos, código, enlaces,
<Callout>inline, listas. - Modo compact —
<Timeline compact>para listas densas de cambios chicos. - Generación desde datos — cada
Timeline.Itemes un elemento React normal, así podés hacer.map()sobre un array JSON de releases.
Consultá la doc completa de Timeline para ver la API completa, notas de accesibilidad y el patrón de changelog guiado por datos.
Actualización
Para la mayoría de teams, es un install de una línea:
pnpm add boltdocs@latest
Un subset pequeño de installs verá un peerDependencies warning de npm/pnpm:
boltdocs@3.2.0 requiere react-aria-components@^1.16.0 como peer
Satísfacélo con pnpm add react-aria-components (o fijá una versión menor si tu vendor de React Aria ya lo provee). Los sitios que usan @bdocs/plugin-image-optimizer no se ven afectados — las peer declarations del optimizer ya funcionan, y sharp/svgo se instalan via peerDependenciesMeta.optional: true del optimizer.
Consultá Actualizar a 3.2.0 → Cambios semi-breaking para la auditoría completa, snippets de package.json antes/después y notas por plataforma.
Revisá la documentación completa para explorar todo lo nuevo.