npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jamx-framework/renderer

v1.0.1

Published

JAMX Framework — SSR Renderer

Downloads

251

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.

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