babel-plugin-jsx-dom-expressions: el compilador que es Solid
El grueso de Solid no vive en tiempo de ejecución sino en un transformador de Babel. Cómo `babel-plugin-jsx-dom-expressions` —un motor genérico de compilación de JSX a DOM— parte cada plantilla en una porción estática clonable y unas ligaduras reactivas, y cómo Solid lo convierte en su compilador enchufándole su runtime vía `moduleName`. La cadena `vite-plugin-solid` → `babel-preset-solid` → plugin, la partición estático contra dinámico, el trato distinto a nativos y componentes, y los tres modos de generación dom, ssr y universal.
Una tubería de tres piezas
Cuando Vite procesa un .tsx, vite-plugin-solid lo intercepta y ejecuta Babel con babel-preset-solid. Ese preset hace una sola cosa esencial: configurar e invocar babel-plugin-jsx-dom-expressions, pasándole las coordenadas del runtime de Solid. El plugin, por sí mismo, no sabe nada de signals ni de solid-js: es un motor genérico —DOM Expressions, de Ryan Carniato— que sabe convertir JSX en plantillas y ligaduras, pero delega las funciones concretas —template, insert, effect, createComponent, memo— en quien lo configure. Solid le dice “impórtalas de solid-js/web”, y así un compilador neutro se convierte en el compilador de Solid.
// lo que babel-preset-solid entrega al plugin, en esencia{ moduleName: "solid-js/web", // de aqui salen template, insert, effect, createComponent generate: "dom", // "dom" | "ssr" | "universal" hydratable: true, // emite claves de hidratacion para SSR delegateEvents: true, // un solo listener global por tipo de evento wrapConditionals: true, // envuelve && y ternarios en memos cuando conviene builtIns: ["For", "Show", "Switch", "Match", "Suspense"],}Ese moduleName es la costura de toda la arquitectura: cámbialo y el mismo plugin genera código para otro sistema reactivo. DOM Expressions nació como un experimento de Ryan Carniato para demostrar que se podía compilar JSX a DOM reactivo sin runtime pesado; Solid es su encarnación de producción, pero el compilador conserva esa neutralidad y no menciona createSignal por ningún lado, solo template, insert, effect y compañía.
¿Y por qué un plugin de Babel y no una transformación de esbuild o SWC? Porque la partición estático/dinámico exige recorrer y reescribir el árbol de sintaxis con precisión quirúrgica, y Babel es la herramienta madura para manipular AST de JavaScript. Han existido intentos de portarlo a otros compiladores por velocidad de build, pero la implementación de referencia —la que define qué significa compilar JSX en Solid— sigue siendo esta.
Conviene ver el encabezado que el plugin inyecta en cada módulo compilado: una línea de imports desde solid-js/web, con alias _$, que reúne solo los helpers que ese módulo realmente usa.
import { template as _$template, insert as _$insert, effect as _$effect, createComponent as _$createComponent, delegateEvents as _$delegateEvents,} from "solid-js/web";Si un módulo no delega eventos, delegateEvents no aparece; si no contiene componentes, tampoco createComponent. El propio conjunto de imports es ya un resumen de qué clases de trabajo hace ese fichero, y leerlo primero te orienta antes de bajar al detalle.
La partición: estático contra dinámico
El acto central del compilador, repetido para cada árbol de JSX, es una partición: decidir qué es demostrablemente constante —horneable en una cadena de HTML— y qué depende del grafo reactivo. El criterio es un análisis estático llamado isDynamic: una expresión es dinámica si es una llamada como nombre(), un acceso a miembro que podría cambiar, o cualquier cosa no probadamente constante. Todo lo estático entra en la cadena que recibe template(); todo lo dinámico sale envuelto en insert() o effect().
const saludo = <p class="card">Hola, {nombre()}</p>;import { template as _$template, insert as _$insert } from "solid-js/web";const _tmpl$ = _$template(`<p class=card>Hola, `); // la parte fija, una vezconst saludo = (() => { const _el$ = _tmpl$(); // clona el nodo _$insert(_el$, nombre, null); // la parte viva se liga aparte return _el$;})();Dos hechos sobre esa salida. El class="card" era constante, así que se endureció dentro de la cadena —y el compilador incluso soltó las comillas innecesarias—. Y {nombre()}, al ser una llamada, salió de la cadena y se convirtió en un insert que recibe nombre —el accessor, no su valor— para que el runtime pueda releerlo. Lo estático se clona; lo dinámico se suscribe.
Nativos contra componentes
La partición solo aplica a elementos nativos —etiquetas en minúscula que mapean a nodos reales del DOM—. Un componente —una etiqueta con mayúscula inicial como App o Boton— no es un nodo que el compilador pueda clonar: es tu función. Así que compila a una llamada a createComponent con un objeto de getters reactivos.
const ui = <Boton color={color()}>Ir</Boton>;const ui = _$createComponent(Boton, { get color() { return color(); }, // getter: preserva la suscripcion children: "Ir",});Cada prop es un getter, no un valor copiado: por eso destructurar la firma de props —que lee el getter una vez— rompe la reactividad. Y createComponent ni clona ni compara: invoca tu función una sola vez, dentro del contexto reactivo del padre, y devuelve el DOM que produzca. Esto alcanza al control de flujo: Show, For y Switch compilan igual, a createComponent con getters; la lista builtIns solo le dice al compilador cuáles puede optimizar de forma especial.
El mismo trato reciben las demás piezas que no son un nodo clonable. Los children de un componente viajan dentro del objeto de props, como getter si son dinámicos; y un fragmento <>...</> no es más que un array de nodos que el compilador ensambla sin envoltorio ni coste. Nada de esto entra en una plantilla, porque nada de esto es HTML estático que se pueda clonar.
El spread lo confirma. Sobre un elemento nativo compila a spread(), que aplica las propiedades y las mantiene reactivas; sobre un componente compila a mergeProps, que combina las fuentes preservando sus getters.
const fila = <Fila {...datos()}>{titulo()}</Fila>;const fila = _$createComponent(Fila, _$mergeProps(datos, { get children() { return titulo(); }, // hijo dinamico como getter}));Ni una plantilla ni un cloneNode a la vista: un componente es una llamada que devuelve DOM, no un nodo que se copie.
Tres modos de generación
La opción generate decide a cuál de los tres runtimes de Solid apunta el plugin, sin tocar tu JSX.
El contraste se ve en una sola línea. El mismo <p>{x()}</p> produce, en dom, un template y un insert; en ssr, una concatenación de cadenas que escapa solo la parte viva y no clona nada.
// generate: "ssr"_$ssr(["<p>", "</p>"], _$escape(x())); // sin template, sin effect: solo HTMLQue un mismo compilador sirva a tres destinos es la razón profunda de que SolidStart haga SSR y streaming con los mismos componentes que escribes para el cliente: no es una segunda implementación, es el mismo JSX pasado por el plugin con otro generate. Y es también la prueba de que la neutralidad de DOM Expressions no era un lujo académico: gracias a ella el ecosistema puede reactivizar el DOM en el cliente, cadenas de HTML en el servidor y árboles de objetos arbitrarios en un renderer propio, todo con una sola gramática de entrada.