@jamx-framework/renderer
v1.0.1
Published
JAMX Framework — SSR Renderer
Downloads
251
Maintainers
Readme
@jamx-framework/renderer
Descripción
Motor de renderizado SSR (Server-Side Rendering) para JAMX Framework. Convierte componentes de página escritos en JSX/TSX en strings HTML completos, listos para ser enviados al cliente. Incluye un serializador HTML, un runtime JSX personalizado, y componentes de manejo de errores.
Cómo funciona
El proceso de renderizado SSR consta de varias etapas:
- Recibir componente de página: Un objeto que implementa
PageComponentLikecon métodorender()y opcionalmentemeta()ylayout - Obtener metadatos: Se llama a
page.meta()para obtener tags del<head>(título, descripción, etc.) - Renderizar contenido: Se ejecuta
page.render()para obtener el árbol JSX del cuerpo - Aplicar layout: Si existe
page.layout, se envuelve el contenido en él - Serializar a HTML:
HtmlSerializerconvierte el árbol JSX a string HTML - Construir documento: Se envuelve todo en un documento HTML completo con
<!DOCTYPE>,<head>y<body>
Componentes principales
SSRRenderer (src/pipeline/renderer.ts)
Clase principal que orquesta el proceso de renderizado:
render(page, ctx): Renderiza una página completabuildDocument(bodyHtml, head, ctx): Construye el HTML finalbuildMetaTags(head): Genera tags<meta>adicionales
HtmlSerializer (src/html/serializer.ts)
Serializa árboles JSX a strings HTML:
serialize(node): Método principal de serializaciónserializeElement(element): Serializa un elemento JSXserializeComponent(component, props): Ejecuta y serializa componentes funcionalesserializeAttributes(props): Convierte props a atributos HTML
JSX Runtime (src/jsx/jsx-runtime.ts)
Implementación personalizada del JSX runtime para JAMX:
jsx(type, props, key?): Función principal que creaJamxElementjsxs: Alias dejsxpara múltiples hijosFragment: Componente para grupos de elementos sin wrapper- Tipos:
JamxNode,JamxElement,Props,ComponentFn
Error Boundary (src/error/boundary.ts)
Componente para capturar y mostrar errores de renderizado:
ErrorBoundary: Clase que envuelve componentesrenderErrorPage(error, ctx): Genera página de error HTML
HTML Utilities (src/html/escape.ts)
Funciones de escape para seguridad XSS:
escapeHtml(text): Escapa caracteres HTMLescapeAttr(value): Escapa atributos- Constantes:
VOID_ELEMENTS,BOOLEAN_ATTRIBUTES,RAW_TEXT_ELEMENTS
Uso básico
Definir una página
import { jsx } from '@jamx-framework/renderer';
import type { PageComponentLike, RenderContext } from '@jamx-framework/renderer';
const HomePage: PageComponentLike = {
render(ctx: RenderContext) {
return jsx('div', { className: 'home' }, [
jsx('h1', {}, 'Bienvenido a JAMX'),
jsx('p', {}, 'Esta es una página renderizada en servidor'),
]);
},
meta(ctx: RenderContext) {
return {
title: 'Inicio - Mi App JAMX',
description: 'Página de inicio de mi aplicación',
};
},
};Usar SSRRenderer
import { SSRRenderer } from '@jamx-framework/renderer';
import type { RenderContext } from '@jamx-framework/renderer';
const renderer = new SSRRenderer();
const ctx: RenderContext = {
env: 'production',
path: '/',
url: 'https://example.com/',
// ... otras propiedades del contexto
};
const result = await renderer.render(HomePage, ctx);
// result.html contiene el HTML completo
// result.statusCode = 200
// result.headers = { 'Content-Type': 'text/html; charset=utf-8' }Definir un layout
const MainLayout = {
render({ children, ctx }) {
return jsx('div', { id: 'layout' }, [
jsx('header', {}, jsx('nav', {}, 'Navegación')),
jsx('main', {}, children),
jsx('footer', {}, '© 2024'),
]);
},
};
const PageWithLayout: PageComponentLike = {
layout: MainLayout,
render(ctx) {
return jsx('article', {}, [
jsx('h1', {}, 'Contenido de la página'),
jsx('p', {}, 'Este contenido está dentro del layout'),
]);
},
};Componentes funcionales
function Button(props: { children: string; onClick?: string }) {
return jsx(
'button',
{
type: 'button',
class: 'btn',
...(props.onClick && { onclick: props.onClick }),
},
props.children
);
}
function Card(props: { title: string; children: string }) {
return jsx('div', { class: 'card' }, [
jsx('h2', { class: 'card-title' }, props.title),
jsx('div', { class: 'card-body' }, props.children),
]);
}
const MyPage: PageComponentLike = {
render() {
return jsx('div', {}, [
jsx('h1', {}, 'Página con componentes'),
Button({ children: 'Haz clic' }),
Card({
title: 'Tarjeta',
children: 'Contenido de la tarjeta',
}),
]);
},
};Manejo de errores con ErrorBoundary
import { ErrorBoundary, renderErrorPage } from '@jamx-framework/renderer';
const RiskyPage: PageComponentLike = {
render(ctx) {
// Código que puede fallar
if (Math.random() > 0.5) {
throw new Error('Algo salió mal');
}
return jsx('div', {}, 'Todo bien');
},
};
// Envolver con ErrorBoundary
const SafePage = ErrorBoundary.wrap(RiskyPage, {
fallback: (error) => renderErrorPage(error, { env: 'development' }),
});API Reference
Tipos
JamxNode
type JamxNode =
| string
| number
| boolean
| null
| undefined
| JamxElement
| JamxNode[];Nodo válido en el árbol JSX. Puede ser primitivos, elementos, o arrays de nodos.
JamxElement
interface JamxElement {
type: string | ComponentFn;
props: Props;
key: string | null;
}Elemento JSX representado como objeto plano.
Props
type Props = Record<string, unknown> & {
children?: JamxNode | JamxNode[];
};Propiedades de un elemento, incluyendo children opcional.
ComponentFn
type ComponentFn = (props: Props) => JamxNode;Función que recibe props y retorna un JamxNode.
PageComponentLike
interface PageComponentLike {
render: (ctx: RenderContext) => JamxNode;
meta?: (ctx: RenderContext) => PageHead;
layout?: LayoutComponentLike;
}Componente de página que puede ser renderizado.
LayoutComponentLike
interface LayoutComponentLike {
render: (props: { children: JamxNode; ctx: RenderContext }) => JamxNode;
}Componente layout que envuelve páginas.
RenderContext
interface RenderContext {
env: 'development' | 'production' | 'test';
path: string;
url: string;
// ... propiedades adicionales definidas por el usuario
}Contexto de renderizado pasado a todos los componentes.
PageHead
interface PageHead {
title?: string;
description?: string;
[key: string]: unknown;
}Metadatos para el <head> de la página.
Clases
SSRRenderer
class SSRRenderer {
constructor();
async render(page: PageComponentLike, ctx: RenderContext): Promise<RenderResult>;
}Renderizador principal de páginas SSR.
RenderResult:
interface RenderResult {
html: string;
statusCode: number;
headers: Record<string, string>;
}HtmlSerializer
class HtmlSerializer {
serialize(node: JamxNode): string;
private serializeElement(element: JamxElement): string;
private serializeComponent(component: ComponentFn, props: Props): string;
private serializeAttributes(props: Props): string;
}Serializador de árboles JSX a HTML.
Funciones
jsx
function jsx(
type: string | ComponentFn,
props: Props,
key?: string
): JamxElementCrea un elemento JSX. Usado por TypeScript cuando se escribe <Tag />.
jsxs
const jsxs = jsx;Alias de jsx para cuando hay múltiples hijos.
escapeHtml
function escapeHtml(text: string): string;Escapa caracteres HTML especiales para prevenir XSS.
escapeAttr
function escapeAttr(value: unknown): string;Escapa valores para usar en atributos HTML.
ErrorBoundary.wrap
static wrap(
component: PageComponentLike,
options: { fallback: (error: Error, info: { componentStack: string }) => JamxNode }
): PageComponentLikeEnvuelve un componente para capturar errores de renderizado.
renderErrorPage
function renderErrorPage(
error: Error,
ctx: RenderContext
): JamxNodeGenera una página de error por defecto.
Flujo interno detallado
1. SSRRenderer.render()
async render(page, ctx) {
// Paso 1: Obtener metadatos del head
const head = page.meta?.(ctx) ?? {};
// Paso 2: Renderizar el contenido de la página
const contentNode = page.render(ctx);
// Paso 3: Aplicar layout si existe
const bodyNode = page.layout
? page.layout.render({ children: contentNode, ctx })
: contentNode;
// Paso 4: Serializar a HTML
const bodyHtml = this.serializer.serialize(bodyNode);
// Paso 5: Construir documento completo
const html = this.buildDocument(bodyHtml, head, ctx);
return {
html,
statusCode: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
};
}2. HtmlSerializer.serialize()
serialize(node) {
// Casos base
if (node === null || node === undefined || node === false) return "";
if (node === true) return "";
if (typeof node === "number") return String(node);
if (typeof node === "string") return escapeHtml(node);
// Arrays
if (Array.isArray(node)) {
return node.map(child => this.serialize(child)).join("");
}
// Elemento JSX
return this.serializeElement(node);
}3. Serialización de elementos
serializeElement(element) {
const { type, props } = element;
// Componente funcional
if (typeof type === "function") {
return this.serializeComponent(type, props);
}
// Fragment
if (type === Fragment) {
return this.serialize(props.children);
}
// Elemento HTML
const tag = type as string;
const attrs = this.serializeAttributes(props);
const children = this.serialize(props.children);
if (VOID_ELEMENTS.has(tag)) {
return `<${tag}${attrs} />`;
}
return `<${tag}${attrs}>${children}</${tag}>`;
}4. Construcción del documento HTML
buildDocument(bodyHtml, head, ctx) {
const title = head.title ?? "JAMX App";
const description = head.description ?? "";
const isDev = ctx.env === "development";
const metaTags = this.buildMetaTags(head);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${escapeTitle(title)}</title>
${description ? `<meta name="description" content="${escapeAttr(description)}" />` : ""}
${metaTags}
<link rel="stylesheet" href="/__jamx/styles.css" />
</head>
<body>
<div id="__jamx_root__" data-route="${escapeAttr(ctx.path)}">${bodyHtml}</div>
<script type="module" src="/__jamx/client.js"${isDev ? ' data-dev="true"' : ""}></script>
</body>
</html>`;
}Consideraciones de rendimiento
- Serialización en un solo paso: El árbol JSX se serializa en un solo pase sin interrupciones
- Escape automático: Todos los strings se escapan por defecto para prevenir XSS
- Componentes puros: Los componentes deben ser puros (sin side effects) para SSR
- Streaming: No implementado; todo el HTML se genera como string completo
- Caching: Se puede cachear el resultado de
render()para páginas estáticas
Seguridad
- XSS Prevention: Todos los strings se escapan con
escapeHtml()yescapeAttr() - Content Security Policy: Se puede agregar CSP headers en
RenderContext - Sanitización: No se permite HTML raw en props (por defecto)
Configuración
tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}Scripts disponibles
pnpm build- Compila TypeScript a JavaScriptpnpm dev- Compilación en watch modepnpm test- Ejecuta tests unitariospnpm test:watch- Tests en watch modepnpm type-check- Verifica tipos sin compilarpnpm clean- Limpia archivos compilados
Testing
Tests en packages/renderer/test/unit/:
html/escape.test.ts: Pruebas de escape de HTMLhtml/serializer.test.ts: Pruebas de serializaciónerror/boundary.test.ts: Pruebas de ErrorBoundaryerror/error-page.test.ts: Pruebas de página de error
Ejecutar tests:
pnpm testDependencias
@jamx-framework/core- Dependencia de trabajo (workspace)@types/node- Tipos de Node.js para desarrollovitest- Framework de testingrimraf- Limpieza de directorios
Ejemplo completo
// app/pages/index.tsx
import { jsx } from '@jamx-framework/renderer';
import type { PageComponentLike, RenderContext } from '@jamx-framework/renderer';
export const HomePage: PageComponentLike = {
meta() {
return {
title: 'Inicio - Mi Tienda Online',
description: 'La mejor tienda online con los mejores productos',
};
},
render(ctx: RenderContext) {
const user = ctx.locals.user as { name: string } | null;
return jsx('div', { class: 'container' }, [
jsx('header', {}, [
jsx('h1', {}, 'Mi Tienda Online'),
user && jsx('p', {}, `Bienvenido, ${user.name}`),
]),
jsx('main', {}, [
jsx('section', { id: 'products' }, [
jsx('h2', {}, 'Productos destacados'),
jsx('ul', { class: 'product-list' }, [
jsx('li', { key: '1' }, 'Producto 1'),
jsx('li', { key: '2' }, 'Producto 2'),
jsx('li', { key: '3' }, 'Producto 3'),
]),
]),
]),
jsx('footer', {}, [
jsx('p', {}, '© 2024 Mi Tienda Online'),
]),
]);
},
};// server/render.ts
import { SSRRenderer } from '@jamx-framework/renderer';
import { HomePage } from '../app/pages/index.js';
const renderer = new SSRRenderer();
export async function renderPage(path: string, ctx: RenderContext) {
// Aquí se seleccionaría la página según la ruta
const page = HomePage;
const result = await renderer.render(page, ctx);
return {
statusCode: result.statusCode,
headers: result.headers,
body: result.html,
};
}Limitaciones
- No soporta streaming de HTML (todo se genera en memoria)
- No incluye hidratación automática (requiere cliente separado)
- No soporta Suspense o async components en SSR (por ahora)
- Los componentes deben ser puros (sin side effects en render)
Futuras mejoras
- Streaming SSR con
ReadableStream - Soporte para Suspense y async components
- Optimizaciones de compilación (compile-time rendering)
- Soporte para más tags HTML y atributos especiales
- Integración con sistemas de caché (Redis, CDN)
