Volver al Blog
engineering||9 min de lectura

Un PR a un parser desbloqueó el prerendering en Brisa

AR
Aral Roca

Creador de Kitmul

Brisa Framework; el framework JavaScript cuyo pipeline de build parsea cada archivo fuente a través de Meriyah
Brisa Framework; el framework JavaScript cuyo pipeline de build parsea cada archivo fuente a través de Meriyah

Construí un framework de JavaScript llamado Brisa. El tipo de framework que necesita parsear cada archivo fuente de tu aplicación; analizar imports, detectar componentes de servidor vs. cliente, inyectar macros, transformar JSX. Todo eso ocurre a nivel de AST.

Antes de Brisa, ya mantenía next-translate, una librería de i18n para Next.js. Para el plugin que auto-inyecta cargadores de locale en las páginas, usé la API del compilador de TypeScript. Funcionaba. Pero era dolorosamente lento; ts.createProgram() por cada archivo de página en tiempo de build, instanciación completa del type-checker, resolución de librerías. Tuvimos que añadir noResolve: true y noLib: true solo para hacerlo soportable. El parser estaba haciendo diez veces más trabajo del necesario porque lo único que queríamos era el AST, no los tipos.

Cuando empecé a construir Brisa, sabía que necesitaba algo más rápido. Algo que me diera un AST compatible con ESTree sin la sobrecarga de un compilador completo. Así encontré Meriyah.

Por qué elegí Meriyah sobre todo lo demás

Meriyah está escrito completamente en JavaScript. Sin bindings nativos. Sin paso de carga WASM. Sin compilación. Solo parseScript(code, { jsx: true, module: true, next: true }) y obtienes un AST ESTree en microsegundos.

Para el pipeline de build de Brisa, esa diferencia de velocidad se acumula. Cada archivo fuente en un proyecto Brisa pasa por Meriyah. El parser se ejecuta dentro de AST().parseCodeToAST(), que primero transpila vía el transpiler de Bun y luego alimenta el resultado a Meriyah. La salida es un nodo Program ESTree estándar que puedo recorrer, modificar y regenerar con astring.

Pero aquí es donde se puso interesante. Brisa tiene una funcionalidad llamada renderOn que permite prerenderizar componentes en tiempo de build. Escribes esto en tu página:

<SomeComponent renderOn="build" foo="bar" />

Y en tiempo de build, la transformación AST detecta renderOn="build", reemplaza el JSX con una llamada a __prerender__macro(), e inyecta este import al principio del archivo:

import { __prerender__macro } from 'brisa/macros' with { type: 'macro' };

Ese with { type: 'macro' } es un import attribute que le dice al bundler de Bun que resuelva el import en tiempo de compilación. El componente se renderiza durante el build, y el resultado se inyecta como HTML estático. El usuario escribe renderOn="build", pero internamente el framework construye nodos ImportDeclaration e ImportAttribute del AST a mano y regenera el código.

El problema: Meriyah no soportaba import attributes cuando empecé a usarlo. Así que contribuí un PR para añadir la funcionalidad. Ese PR se aceptó, y todo el pipeline de prerender de Brisa pudo funcionar de principio a fin.

Pasar de "el parser no entiende mi sintaxis" a "voy a arreglar el parser directamente" es el tipo de cosa que solo ocurre cuando entiendes profundamente cómo funcionan los ASTs.

La inspiración

AST Explorer existe, y es genial. Lo uso regularmente. Es la herramienta de referencia para explorar ASTs. Quería construir algo similar como parte de Kitmul; mi propia versión de un visualizador AST con selección de parsers, vista de árbol interactiva y soporte para los parsers que uso día a día.

El Visualizador AST hace exactamente esto. Pega JavaScript, elige tu parser (Acorn, Meriyah o SWC), y obtén un árbol interactivo o JSON crudo. Todo se ejecuta localmente en tu navegador.

La elección del parser importa porque cada uno produce un AST ligeramente diferente:

  • Acorn sigue la especificación ESTree estrictamente. Es el parser que ESLint usa internamente. Si escribes reglas de ESLint, este es el árbol que tu regla recorrerá.
  • Meriyah también sigue ESTree, pero añade soporte JSX y funcionalidades de última generación vía el flag next: true. Es el parser que elegí para Brisa porque es rápido, ligero y está escrito en JS puro.
  • SWC es un compilador basado en Rust que se ejecuta vía WASM en el navegador. Su AST usa una estructura diferente; Module en vez de Program, objetos span en vez de posiciones start/end. Si trabajas con internos de Next.js o Turbopack, este es el AST con el que tratas.

Cambiar entre parsers y ver cómo el mismo código produce árboles diferentes es una de las formas más rápidas de entender las diferencias entre parsers.

Tres cosas que el árbol te enseña y la documentación no

next-translate comparación de tamaño de bundle; la librería i18n para Next.js donde primero lidié con parsing de ASTs vía la API del compilador TypeScript
next-translate comparación de tamaño de bundle; la librería i18n para Next.js donde primero lidié con parsing de ASTs vía la API del compilador TypeScript

1. Expresiones vs. sentencias se hacen visibles.

Todo desarrollador JavaScript escucha "expresión vs. sentencia" en algún momento. Pocos pueden articular la diferencia hasta que la ven en un árbol. Considera:

x = 5;

El AST muestra un ExpressionStatement envolviendo un AssignmentExpression. La expresión es la parte x = 5. La sentencia es el envoltorio terminado en punto y coma que la convierte en una línea independiente. Esta distinción es la razón por la que if (x = 5) es JavaScript válido.

2. La precedencia de operadores se vuelve estructural.

Parsea 2 + 3 * 4 y verás:

BinaryExpression (operator: "+")
  ├─ left: Literal (2)
  └─ right: BinaryExpression (operator: "*")
           ├─ left: Literal (3)
           └─ right: Literal (4)

La multiplicación está anidada dentro del operando derecho de la suma. El nodo más profundo se evalúa primero. Los paréntesis cambian la estructura del árbol, no alguna bandera de prioridad invisible.

3. Los import attributes revelan cómo funciona renderOn="build".

Parsea esto con Meriyah:

import { __prerender__macro } from 'brisa/macros' with { type: 'macro' };

El nodo ImportDeclaration obtiene un array attributes conteniendo nodos ImportAttribute. Cada atributo tiene un key y un value, ambos nodos Literal. Este es el import que el pipeline de build de Brisa inyecta cuando encuentra renderOn="build" en un componente. El with { type: 'macro' } le dice a Bun que resuelva la función en tiempo de compilación. Sin ver el árbol, nunca adivinarías que with { type: 'macro' } se convierte en un array anidado de objetos atributo.

Casos de uso reales construyendo frameworks

Pipelines de build de frameworks. En Brisa, cada archivo fuente se parsea a un AST, se analiza para imports, se transforma (inyección de macros, separación servidor/cliente, procesamiento i18n), y se regenera como código. La función central es AST('tsx').parseCodeToAST(code).

Inyección de macros de prerender vía renderOn="build". Cuando Brisa encuentra <Foo renderOn="build" />, la transformación AST construye nodos ImportAttribute a mano para inyectar import {__prerender__macro} from 'brisa/macros' with { type: "macro" }. Hay una peculiaridad: Meriyah usa value en nodos Literal donde astring espera name. Eso es un comentario real en el código fuente de Brisa: // This astring is looking for "name", but meriyah "value". Solo descubres ese tipo de cosas mirando árboles.

Inyección de cargadores i18n. En next-translate-plugin, el loader de Webpack usa ts.createProgram() para parsear cada página y detectar sus exports. Necesita saber si la página tiene getStaticProps, getServerSideProps, o un export por defecto. El AST de TypeScript usa enums SyntaxKind en vez de tipos basados en strings, lo cual es un modelo mental diferente de ESTree.

Resolución de rutas de import. Brisa resuelve imports relativos a rutas absolutas en tiempo de build. La transformación recorre nodos ImportDeclaration, lee el string source.value, lo resuelve contra el sistema de archivos, y lo reemplaza.

Comparando parsers lado a lado

Característica Acorn Meriyah SWC
Lenguaje JavaScript JavaScript Rust (WASM en navegador)
Especificación ESTree ESTree SWC AST
Soporte JSX No
Import attributes No
Velocidad Rápido Muy rápido Rápido (tras carga WASM)
Tamaño del bundle ~120KB ~320KB ~14MB (WASM)
Usado por ESLint Brisa Next.js, Turbopack

El Visualizador AST mostrando selección de parser entre Acorn, Meriyah y SWC con modos de vista árbol y JSON
El Visualizador AST mostrando selección de parser entre Acorn, Meriyah y SWC con modos de vista árbol y JSON

Cinco fragmentos de código que vale la pena explorar

Pega estos en el Visualizador AST y prueba cada parser:

1. Arrow function con retorno implícito:

const add = (a, b) => a + b;

Observa cómo el ArrowFunctionExpression tiene expression: true y el body es un BinaryExpression, no un BlockStatement.

2. Import attributes (usa Meriyah o SWC):

import { __prerender__macro } from 'brisa/macros' with { type: 'macro' };

Con Meriyah, el ImportDeclaration obtiene un array attributes con nodos ImportAttribute. Con Acorn, esta sintaxis lanzará un error de parseo. Exactamente el tipo de diferencia entre parsers que importa en la práctica; el pipeline de build de Brisa depende de ello.

3. Optional chaining:

const value = obj?.nested?.deep?.property;

Cada ?. crea un ChainExpression envolviendo nodos MemberExpression con optional: true.

4. Async/await:

async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

El FunctionDeclaration tiene async: true. await crea un AwaitExpression envolviendo el CallExpression.

5. Destructuración con valores por defecto:

const { a = 1, b: { c = 2 } = {} } = config;

Los valores por defecto crean nodos AssignmentPattern. La destructuración anidada pone un ObjectPattern dentro del valor de un Property.

Privacidad

Los tres parsers se ejecutan completamente en tu navegador. Acorn y Meriyah son librerías JavaScript que se ejecutan del lado del cliente. SWC carga un binario WASM desde un archivo local. Ningún código se transmite a ningún servidor. Si estás parseando código propietario, nada sale de tu dispositivo.

La colección de Visualizadores y Herramientas Lógicas incluye visualizadores de grafos, generadores de tablas de verdad y herramientas de regex que complementan bien el trabajo con ASTs. Para trackear tus sesiones de estudio, el Temporizador Pomodoro con música de concentración integrada funciona sorprendentemente bien.

La conclusión real

Pasé de pelear con la API del compilador de TypeScript en next-translate a contribuir funcionalidades al parser de Meriyah para Brisa. El punto de inflexión no fue leer más documentación. Fue ver suficientes ASTs hasta que los tipos de nodos se volvieron algo natural.

El Visualizador AST no te va a enseñar teoría de compiladores. Te va a enseñar lo que el parser ve cuando lee tu código. Para escribir internos de frameworks, herramientas de build, codemods y reglas de ESLint, eso es lo único que importa.


El Visualizador AST es gratuito, privado y se ejecuta completamente en tu navegador. Sin registro, sin instalación, sin datos que abandonen tu dispositivo. Parte de la colección de Visualizadores y Herramientas Lógicas en Kitmul.

Comparte este artículo

Boletín

Recibe Consejos de Productividad y Nuevas Herramientas Primero

Únete a creadores y desarrolladores que valoran la privacidad. En cada edición: nuevas herramientas, trucos de productividad y novedades — sin spam.

Acceso prioritario a nuevas herramientas
Cancela en cualquier momento, sin preguntas