npm package: @jamx-framework/renderer
Description: JAMX Framework â€” SSR Renderer
Author: Stiven-21
Latest version: 1.0.1

# @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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
interface JamxElement {
  type: string | ComponentFn;
  props: Props;
  key: string | null;
}
```
Elemento JSX representado como objeto plano.

#### Props
```typescript
type Props = Record<string, unknown> & {
  children?: JamxNode | JamxNode[];
};
```
Propiedades de un elemento, incluyendo `children` opcional.

#### ComponentFn
```typescript
type ComponentFn = (props: Props) => JamxNode;
```
Función que recibe props y retorna un JamxNode.

#### PageComponentLike
```typescript
interface PageComponentLike {
  render: (ctx: RenderContext) => JamxNode;
  meta?: (ctx: RenderContext) => PageHead;
  layout?: LayoutComponentLike;
}
```
Componente de página que puede ser renderizado.

#### LayoutComponentLike
```typescript
interface LayoutComponentLike {
  render: (props: { children: JamxNode; ctx: RenderContext }) => JamxNode;
}
```
Componente layout que envuelve páginas.

#### RenderContext
```typescript
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
```typescript
interface PageHead {
  title?: string;
  description?: string;
  [key: string]: unknown;
}
```
Metadatos para el `<head>` de la página.

### Clases

#### SSRRenderer
```typescript
class SSRRenderer {
  constructor();
  async render(page: PageComponentLike, ctx: RenderContext): Promise<RenderResult>;
}
```
Renderizador principal de páginas SSR.

**RenderResult:**
```typescript
interface RenderResult {
  html: string;
  statusCode: number;
  headers: Record<string, string>;
}
```

#### HtmlSerializer
```typescript
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
```typescript
function jsx(
  type: string | ComponentFn,
  props: Props,
  key?: string
): JamxElement
```
Crea un elemento JSX. Usado por TypeScript cuando se escribe `<Tag />`.

#### jsxs
```typescript
const jsxs = jsx;
```
Alias de `jsx` para cuando hay múltiples hijos.

#### escapeHtml
```typescript
function escapeHtml(text: string): string;
```
Escapa caracteres HTML especiales para prevenir XSS.

#### escapeAttr
```typescript
function escapeAttr(value: unknown): string;
```
Escapa valores para usar en atributos HTML.

#### ErrorBoundary.wrap
```typescript
static wrap(
  component: PageComponentLike,
  options: { fallback: (error: Error, info: { componentStack: string }) => JamxNode }
): PageComponentLike
```
Envuelve un componente para capturar errores de renderizado.

#### renderErrorPage
```typescript
function renderErrorPage(
  error: Error,
  ctx: RenderContext
): JamxNode
```
Genera una página de error por defecto.

## Flujo interno detallado

### 1. SSRRenderer.render()
```typescript
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()
```typescript
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
```typescript
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
```typescript
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
```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:
```bash
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
```typescript
// 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'),
      ]),
    ]);
  },
};
```

```typescript
// 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)