El pipeline: de la fuente al módulo
Cómo el compilador convierte un .astro en JavaScript ejecutable en tres pasos: parseo a un AST, transformación de ese árbol y generación de un módulo. La forma real del módulo emitido —createComponent, la función de render, el objeto result— y el paquete @astrojs/compiler con sus dos funciones públicas, parse para obtener el árbol y transform para obtener el código y sus metadatos.
Un componente .astro no llega nunca al navegador; llega al motor de render de Astro convertido en un módulo de JavaScript que exporta una función. Entre tu archivo y ese módulo hay una tubería de tres tramos —parsear, transformar, generar— que el compilador recorre para cada componente del proyecto. Verla por dentro desmitifica todo lo demás: las islas, el scope de estilos y los source maps de las próximas lecciones son consecuencias de decisiones que se toman en estos tres pasos. Y como el compilador se publica en @astrojs/compiler, esa tubería no es una caja negra: se puede invocar a mano.
- Recorrer los tres tramos del pipeline: parseo, transformación y generación.
- Reconocer la forma del módulo emitido:
createComponent, render y$$result. - Usar
parsepara obtener el AST de un.astroy recorrerlo. - Usar
transformpara obtener el código generado y sus metadatos.
Tres tramos: parsear, transformar, generar
El compilador no salta de tu texto al código final de un brinco; encadena tres transformaciones bien separadas. Primero parsea: lee la fuente carácter a carácter y construye un árbol de sintaxis abstracta, un AST, donde cada nodo es una pieza del componente —el frontmatter, un elemento, un atributo, una expresión, un bloque de estilo—. Después transforma ese árbol: reescribe selectores para el scope, marca las islas, iza los estilos, resuelve las directivas. Por último genera: recorre el árbol ya transformado y emite texto de JavaScript, el módulo que Astro importará.
flowchart LR SRC[fuente punto astro] --> P[parsear] P --> AST[arbol de sintaxis] AST --> T[transformar] T --> AST2[arbol anotado] AST2 --> G[generar] G --> MOD[modulo javascript y metadatos]
Cada tramo tiene una entrada y una salida bien definidas, y nombrarlas ayuda a razonar sobre dónde ocurre cada cosa:
- Parsear toma texto y devuelve un árbol. La entrada es tu
.astrocomo cadena; la salida, un AST con un nodo por cada pieza del componente. - Transformar toma un árbol y devuelve otro árbol anotado. Reescribe selectores, marca islas, iza estilos; no toca todavía una sola letra de JavaScript.
- Generar toma el árbol anotado y devuelve texto. La salida es el módulo de JavaScript, más el informe de metadatos recolectado por el camino.
La separación importa por una razón práctica: como cada tramo tiene fronteras claras, se pueden estudiar y depurar por partes. Un fallo de sintaxis es un problema del parseo; un estilo que no se aísla es un problema de la transformación; un HTML raro en la salida es un problema de la generación. Pensar en estos tres compartimentos convierte cualquier rareza del compilador en una pregunta con una dirección concreta donde buscar.
La forma del módulo emitido
El texto que sale del último tramo tiene una estructura reconocible, y merece la pena leerla una vez para no temerla nunca más. El frontmatter se convierte en el cuerpo de una función de render asíncrona; el marcado se convierte en una plantilla de cadena etiquetada con una función render que interpola las expresiones; y todo se envuelve en una llamada a createComponent, el envoltorio del runtime de Astro que produce el componente final.
// forma simplificada de lo que emite el compilador
import {
createComponent, render, renderComponent,
maybeRenderHead, createAstro
} from 'astro/runtime/server/index.js';
const $$Astro = createAstro();
const $$Pagina = createComponent(async ($$result, $$props, $$slots) => {
const $$Astro = $$result.createAstro($$Astro, $$props, $$slots);
const usuario = await fetch('/api/yo').then((r) => r.json());
return render`${maybeRenderHead()}<h1>Hola, ${usuario.nombre}</h1>`;
});
export default $$Pagina;
Cada pieza tiene su papel. El parámetro $$result es el acumulador del render: lleva la cuenta de los estilos, los scripts y las islas que se van encontrando mientras se pinta la página, y por eso lo recibe cada componente. $$props y $$slots son las props y los slots recibidos. La función render etiquetada es la que convierte las interpolaciones en HTML seguro, y maybeRenderHead inyecta en el momento justo los estilos y scripts que el $$result ha ido recolectando. Los prefijos $$ no son casuales: marcan lo generado por la máquina para que no choque con tus nombres.
Observa además cómo el frontmatter aparece casi textual dentro de la función: el await fetch(...) que escribiste sigue ahí, con la misma forma, solo que ahora vive en el cuerpo de createComponent. Esa fidelidad no es accidental; es lo que permite que los source maps de la última lección puedan trazar cada línea generada de vuelta a la tuya. El compilador transforma la estructura del componente, pero preserva la letra de tu código de servidor tanto como puede.
createComponent
Envuelve tu frontmatter y tu plantilla en el componente que Astro sabe renderizar.
render
La plantilla etiquetada que interpola tus expresiones y las escapa como HTML seguro.
maybeRenderHead
Inyecta en el momento justo los estilos y scripts que el render fue recolectando.
result
El acumulador que cada componente recibe para llevar la cuenta de estilos e islas.
El código emitido intimida por sus prefijos $$ y sus nombres internos, pero cada símbolo cumple una función simple. $$result es un acumulador, render es un interpolador seguro, createComponent es un envoltorio. Si en vez de memorizar nombres te preguntas qué papel juega cada uno en el render, el módulo generado deja de ser jeroglífico y pasa a ser una traducción bastante literal de tu .astro.
parse: el árbol antes de tocarlo
El paquete @astrojs/compiler publica la tubería como dos funciones. La primera, parse, se detiene en el primer tramo: recibe la fuente y devuelve el AST sin transformarlo ni generar nada. Es la puerta para inspeccionar un componente —lo que hacen las herramientas de formato, los linters o los plugins que analizan tu código— sin producir salida.
import { parse } from '@astrojs/compiler';
const fuente = `---\nconst n = 1;\n---\n<h1>Hola {n}</h1>`;
const { ast, diagnostics } = await parse(fuente);
// ast.type === 'root'; sus children son frontmatter, elementos, texto...
console.log(ast.children.map((nodo) => nodo.type));
El resultado trae el ast —un árbol cuyo nodo raíz es de tipo root— y una lista de diagnostics con los avisos y errores que el parseo haya encontrado. Los tipos de nodo son pocos y describen todo lo que un .astro puede contener:
frontmatter: el bloque entre vallas, con el código de servidor como su valor.elementycomponent: una etiqueta HTML o un componente importado, con sus atributos.expression: una interpolación{...}de la plantilla.text: el texto literal entre nodos.
Recorrer ese árbol es la forma de razonar sobre la estructura de un componente sin importar cómo se compile: dos archivos con el mismo AST significan lo mismo, aunque difieran en espacios o comentarios. Esa es la propiedad que explotan los formateadores —normalizan el texto sin cambiar el árbol— y los linters —inspeccionan el árbol sin generar código—.
transform: el código y sus metadatos
La segunda función, transform, recorre la tubería entera y devuelve el resultado final: el módulo de JavaScript ya generado y, con él, todo el informe de metadatos que el compilador extrajo por el camino.
import { transform } from '@astrojs/compiler';
const { code, map, styles, scripts, hydratedComponents, diagnostics } =
await transform(fuente, {
filename: 'src/pages/index.astro',
sourcemap: 'external',
});
console.log(code); // el modulo JS emitido
console.log(hydratedComponents); // islas detectadas para hidratar
En ese objeto está condensado todo lo que el resto del framework necesita. Vale la pena verlo como el contrato de salida del compilador, campo por campo:
code: el módulo de JavaScript, el único trozo destinado a ejecutarse.map: el source map que devuelve cada línea generada a tu.astro, tema de la última lección.stylesyscripts: los bloques<style>y<script>izados del componente para procesarse aparte.hydratedComponentsyclientOnlyComponents: las islas detectadas, con y sin render de servidor.diagnostics: los avisos y errores que la compilación encontró.
Astro no usa nada más para montar la página: llama a transform por cada .astro, guarda el code como módulo y reparte los metadatos entre sus subsistemas —el empaquetado de estilos, el registro de islas, el canal de errores—. Entender esa única llamada, con su entrada de texto y su salida de código más informe, es entender el corazón del build.
parse te da el árbol aunque el componente tenga errores semánticos que solo aparecen al generar; sus diagnostics cubren la sintaxis, no todo. transform puede descubrir problemas más tarde, al reescribir o generar. Si construyes herramientas sobre el compilador, no asumas que un parse limpio garantiza un transform sin diagnósticos: son dos profundidades distintas de análisis.
Hay una tentación de ver el compilador como una cinta transportadora que empuja texto de un extremo a otro, pero lo que ocurre en estos tres tramos es más interesante y más antiguo que Astro: es el patrón canónico de todo compilador desde hace medio siglo. El parseo convierte una secuencia plana de caracteres —que para la máquina no significa nada— en una estructura de árbol que sí tiene forma y jerarquía; es el momento en que el texto se vuelve entendible. La transformación trabaja sobre esa estructura, no sobre el texto, y ahí está la clave: reescribir selectores, marcar islas o izar estilos son operaciones sobre nodos, imposibles de hacer con fiabilidad sobre cadenas de caracteres, porque un árbol sabe qué es cada cosa y una cadena no. Y la generación hace el viaje inverso, del árbol de vuelta al texto, pero a un texto de otro mundo: ya no .astro, sino JavaScript que una máquina distinta —el motor de render— sabe ejecutar. Lo profundo es que el AST del centro no pertenece a ninguno de los dos idiomas; es una representación intermedia, un espacio neutral donde el significado del componente vive despojado de la sintaxis con que lo escribiste y aún sin la sintaxis con que se ejecutará. Esa idea —que entre dos lenguajes hay siempre una estructura intermedia donde de verdad ocurre el trabajo— es la que separa un compilador de un simple buscar y reemplazar. Cuando Astro publica parse y transform como funciones distintas, no te está dando dos utilidades: te está dando acceso a los dos costados de ese espacio intermedio, el que entra al árbol y el que sale de él. Quien entiende que el AST es el verdadero protagonista deja de ver el compilador como magia y empieza a verlo como lo que es: una traducción disciplinada entre dos mundos que no comparten ni una regla.
- Instala
@astrojs/compileren un proyecto de prueba y llama aparsesobre un.astrosencillo; imprime lostypede los hijos del nodo raíz. - Llama a
transformsobre el mismo archivo y lee elcodeemitido: localiza lacreateComponent, la función de render y la llamada arender. - En el resultado de
transform, inspeccionastyles,scriptsyhydratedComponentsy relaciona cada uno con lo que escribiste en el componente. - Introduce a propósito un error de sintaxis y compara qué aparece en los
diagnosticsdeparsefrente a los detransform.