
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;
Moduleen vez deProgram, objetosspanen vez de posicionesstart/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

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 | Sí | Sí |
| Import attributes | No | Sí | Sí |
| 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 |

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.