JSPM

@jamx-framework/renderer

1.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 8
  • Score
    100M100P100Q72994F
  • License MIT

JAMX Framework — SSR Renderer

Package Exports

  • @jamx-framework/renderer
  • @jamx-framework/renderer/jsx-dev-runtime
  • @jamx-framework/renderer/jsx-runtime

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:

  1. Recibir componente de página: Un objeto que implementa PageComponentLike con método render() y opcionalmente meta() y layout
  2. Obtener metadatos: Se llama a page.meta() para obtener tags del <head> (título, descripción, etc.)
  3. Renderizar contenido: Se ejecuta page.render() para obtener el árbol JSX del cuerpo
  4. Aplicar layout: Si existe page.layout, se envuelve el contenido en él
  5. Serializar a HTML: HtmlSerializer convierte el árbol JSX a string HTML
  6. 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 completa
  • buildDocument(bodyHtml, head, ctx): Construye el HTML final
  • buildMetaTags(head): Genera tags <meta> adicionales

HtmlSerializer (src/html/serializer.ts)

Serializa árboles JSX a strings HTML:

  • serialize(node): Método principal de serialización
  • serializeElement(element): Serializa un elemento JSX
  • serializeComponent(component, props): Ejecuta y serializa componentes funcionales
  • serializeAttributes(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 crea JamxElement
  • jsxs: Alias de jsx para múltiples hijos
  • Fragment: 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 componentes
  • renderErrorPage(error, ctx): Genera página de error HTML

HTML Utilities (src/html/escape.ts)

Funciones de escape para seguridad XSS:

  • escapeHtml(text): Escapa caracteres HTML
  • escapeAttr(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.

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
): JamxElement

Crea 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 }
): PageComponentLike

Envuelve un componente para capturar errores de renderizado.

renderErrorPage

function renderErrorPage(
  error: Error,
  ctx: RenderContext
): JamxNode

Genera 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() y escapeAttr()
  • 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 JavaScript
  • pnpm dev - Compilación en watch mode
  • pnpm test - Ejecuta tests unitarios
  • pnpm test:watch - Tests en watch mode
  • pnpm type-check - Verifica tipos sin compilar
  • pnpm clean - Limpia archivos compilados

Testing

Tests en packages/renderer/test/unit/:

  • html/escape.test.ts: Pruebas de escape de HTML
  • html/serializer.test.ts: Pruebas de serialización
  • error/boundary.test.ts: Pruebas de ErrorBoundary
  • error/error-page.test.ts: Pruebas de página de error

Ejecutar tests:

pnpm test

Dependencias

  • @jamx-framework/core - Dependencia de trabajo (workspace)
  • @types/node - Tipos de Node.js para desarrollo
  • vitest - Framework de testing
  • rimraf - 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)