@jamx-framework/renderer 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +560 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/html/escape.d.ts.map +1 -1
- package/dist/html/escape.js +8 -4
- package/dist/html/escape.js.map +1 -1
- package/dist/html/serializer.d.ts.map +1 -1
- package/dist/html/serializer.js +2 -0
- package/dist/html/serializer.js.map +1 -1
- package/dist/jsx/jsx-runtime.d.ts +2 -2
- package/dist/jsx/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx/jsx-runtime.js +5 -1
- package/dist/jsx/jsx-runtime.js.map +1 -1
- package/package.json +37 -12
- package/.turbo/turbo-build.log +0 -4
- package/src/error/boundary.ts +0 -80
- package/src/error/error-page.ts +0 -137
- package/src/error/types.ts +0 -6
- package/src/html/escape.ts +0 -90
- package/src/html/serializer.ts +0 -161
- package/src/index.ts +0 -33
- package/src/jsx/jsx-runtime.ts +0 -247
- package/src/pipeline/context.ts +0 -45
- package/src/pipeline/renderer.ts +0 -138
- package/test/unit/error/boundary.test.ts +0 -78
- package/test/unit/error/error-page.test.ts +0 -63
- package/test/unit/html/escape.test.ts +0 -34
- package/test/unit/html/renderer.test.ts +0 -93
- package/test/unit/html/serializer.test.ts +0 -141
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -4
package/README.md
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# @jamx-framework/renderer
|
|
2
|
+
|
|
3
|
+
## Descripción
|
|
4
|
+
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.
|
|
5
|
+
|
|
6
|
+
## Cómo funciona
|
|
7
|
+
El proceso de renderizado SSR consta de varias etapas:
|
|
8
|
+
|
|
9
|
+
1. **Recibir componente de página**: Un objeto que implementa `PageComponentLike` con método `render()` y opcionalmente `meta()` y `layout`
|
|
10
|
+
2. **Obtener metadatos**: Se llama a `page.meta()` para obtener tags del `<head>` (título, descripción, etc.)
|
|
11
|
+
3. **Renderizar contenido**: Se ejecuta `page.render()` para obtener el árbol JSX del cuerpo
|
|
12
|
+
4. **Aplicar layout**: Si existe `page.layout`, se envuelve el contenido en él
|
|
13
|
+
5. **Serializar a HTML**: `HtmlSerializer` convierte el árbol JSX a string HTML
|
|
14
|
+
6. **Construir documento**: Se envuelve todo en un documento HTML completo con `<!DOCTYPE>`, `<head>` y `<body>`
|
|
15
|
+
|
|
16
|
+
## Componentes principales
|
|
17
|
+
|
|
18
|
+
### SSRRenderer (`src/pipeline/renderer.ts`)
|
|
19
|
+
Clase principal que orquesta el proceso de renderizado:
|
|
20
|
+
- `render(page, ctx)`: Renderiza una página completa
|
|
21
|
+
- `buildDocument(bodyHtml, head, ctx)`: Construye el HTML final
|
|
22
|
+
- `buildMetaTags(head)`: Genera tags `<meta>` adicionales
|
|
23
|
+
|
|
24
|
+
### HtmlSerializer (`src/html/serializer.ts`)
|
|
25
|
+
Serializa árboles JSX a strings HTML:
|
|
26
|
+
- `serialize(node)`: Método principal de serialización
|
|
27
|
+
- `serializeElement(element)`: Serializa un elemento JSX
|
|
28
|
+
- `serializeComponent(component, props)`: Ejecuta y serializa componentes funcionales
|
|
29
|
+
- `serializeAttributes(props)`: Convierte props a atributos HTML
|
|
30
|
+
|
|
31
|
+
### JSX Runtime (`src/jsx/jsx-runtime.ts`)
|
|
32
|
+
Implementación personalizada del JSX runtime para JAMX:
|
|
33
|
+
- `jsx(type, props, key?)`: Función principal que crea `JamxElement`
|
|
34
|
+
- `jsxs`: Alias de `jsx` para múltiples hijos
|
|
35
|
+
- `Fragment`: Componente para grupos de elementos sin wrapper
|
|
36
|
+
- Tipos: `JamxNode`, `JamxElement`, `Props`, `ComponentFn`
|
|
37
|
+
|
|
38
|
+
### Error Boundary (`src/error/boundary.ts`)
|
|
39
|
+
Componente para capturar y mostrar errores de renderizado:
|
|
40
|
+
- `ErrorBoundary`: Clase que envuelve componentes
|
|
41
|
+
- `renderErrorPage(error, ctx)`: Genera página de error HTML
|
|
42
|
+
|
|
43
|
+
### HTML Utilities (`src/html/escape.ts`)
|
|
44
|
+
Funciones de escape para seguridad XSS:
|
|
45
|
+
- `escapeHtml(text)`: Escapa caracteres HTML
|
|
46
|
+
- `escapeAttr(value)`: Escapa atributos
|
|
47
|
+
- Constantes: `VOID_ELEMENTS`, `BOOLEAN_ATTRIBUTES`, `RAW_TEXT_ELEMENTS`
|
|
48
|
+
|
|
49
|
+
## Uso básico
|
|
50
|
+
|
|
51
|
+
### Definir una página
|
|
52
|
+
```typescript
|
|
53
|
+
import { jsx } from '@jamx-framework/renderer';
|
|
54
|
+
import type { PageComponentLike, RenderContext } from '@jamx-framework/renderer';
|
|
55
|
+
|
|
56
|
+
const HomePage: PageComponentLike = {
|
|
57
|
+
render(ctx: RenderContext) {
|
|
58
|
+
return jsx('div', { className: 'home' }, [
|
|
59
|
+
jsx('h1', {}, 'Bienvenido a JAMX'),
|
|
60
|
+
jsx('p', {}, 'Esta es una página renderizada en servidor'),
|
|
61
|
+
]);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
meta(ctx: RenderContext) {
|
|
65
|
+
return {
|
|
66
|
+
title: 'Inicio - Mi App JAMX',
|
|
67
|
+
description: 'Página de inicio de mi aplicación',
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Usar SSRRenderer
|
|
74
|
+
```typescript
|
|
75
|
+
import { SSRRenderer } from '@jamx-framework/renderer';
|
|
76
|
+
import type { RenderContext } from '@jamx-framework/renderer';
|
|
77
|
+
|
|
78
|
+
const renderer = new SSRRenderer();
|
|
79
|
+
|
|
80
|
+
const ctx: RenderContext = {
|
|
81
|
+
env: 'production',
|
|
82
|
+
path: '/',
|
|
83
|
+
url: 'https://example.com/',
|
|
84
|
+
// ... otras propiedades del contexto
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const result = await renderer.render(HomePage, ctx);
|
|
88
|
+
// result.html contiene el HTML completo
|
|
89
|
+
// result.statusCode = 200
|
|
90
|
+
// result.headers = { 'Content-Type': 'text/html; charset=utf-8' }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Definir un layout
|
|
94
|
+
```typescript
|
|
95
|
+
const MainLayout = {
|
|
96
|
+
render({ children, ctx }) {
|
|
97
|
+
return jsx('div', { id: 'layout' }, [
|
|
98
|
+
jsx('header', {}, jsx('nav', {}, 'Navegación')),
|
|
99
|
+
jsx('main', {}, children),
|
|
100
|
+
jsx('footer', {}, '© 2024'),
|
|
101
|
+
]);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const PageWithLayout: PageComponentLike = {
|
|
106
|
+
layout: MainLayout,
|
|
107
|
+
render(ctx) {
|
|
108
|
+
return jsx('article', {}, [
|
|
109
|
+
jsx('h1', {}, 'Contenido de la página'),
|
|
110
|
+
jsx('p', {}, 'Este contenido está dentro del layout'),
|
|
111
|
+
]);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Componentes funcionales
|
|
117
|
+
```typescript
|
|
118
|
+
function Button(props: { children: string; onClick?: string }) {
|
|
119
|
+
return jsx(
|
|
120
|
+
'button',
|
|
121
|
+
{
|
|
122
|
+
type: 'button',
|
|
123
|
+
class: 'btn',
|
|
124
|
+
...(props.onClick && { onclick: props.onClick }),
|
|
125
|
+
},
|
|
126
|
+
props.children
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Card(props: { title: string; children: string }) {
|
|
131
|
+
return jsx('div', { class: 'card' }, [
|
|
132
|
+
jsx('h2', { class: 'card-title' }, props.title),
|
|
133
|
+
jsx('div', { class: 'card-body' }, props.children),
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const MyPage: PageComponentLike = {
|
|
138
|
+
render() {
|
|
139
|
+
return jsx('div', {}, [
|
|
140
|
+
jsx('h1', {}, 'Página con componentes'),
|
|
141
|
+
Button({ children: 'Haz clic' }),
|
|
142
|
+
Card({
|
|
143
|
+
title: 'Tarjeta',
|
|
144
|
+
children: 'Contenido de la tarjeta',
|
|
145
|
+
}),
|
|
146
|
+
]);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Manejo de errores con ErrorBoundary
|
|
152
|
+
```typescript
|
|
153
|
+
import { ErrorBoundary, renderErrorPage } from '@jamx-framework/renderer';
|
|
154
|
+
|
|
155
|
+
const RiskyPage: PageComponentLike = {
|
|
156
|
+
render(ctx) {
|
|
157
|
+
// Código que puede fallar
|
|
158
|
+
if (Math.random() > 0.5) {
|
|
159
|
+
throw new Error('Algo salió mal');
|
|
160
|
+
}
|
|
161
|
+
return jsx('div', {}, 'Todo bien');
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Envolver con ErrorBoundary
|
|
166
|
+
const SafePage = ErrorBoundary.wrap(RiskyPage, {
|
|
167
|
+
fallback: (error) => renderErrorPage(error, { env: 'development' }),
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## API Reference
|
|
172
|
+
|
|
173
|
+
### Tipos
|
|
174
|
+
|
|
175
|
+
#### JamxNode
|
|
176
|
+
```typescript
|
|
177
|
+
type JamxNode =
|
|
178
|
+
| string
|
|
179
|
+
| number
|
|
180
|
+
| boolean
|
|
181
|
+
| null
|
|
182
|
+
| undefined
|
|
183
|
+
| JamxElement
|
|
184
|
+
| JamxNode[];
|
|
185
|
+
```
|
|
186
|
+
Nodo válido en el árbol JSX. Puede ser primitivos, elementos, o arrays de nodos.
|
|
187
|
+
|
|
188
|
+
#### JamxElement
|
|
189
|
+
```typescript
|
|
190
|
+
interface JamxElement {
|
|
191
|
+
type: string | ComponentFn;
|
|
192
|
+
props: Props;
|
|
193
|
+
key: string | null;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
Elemento JSX representado como objeto plano.
|
|
197
|
+
|
|
198
|
+
#### Props
|
|
199
|
+
```typescript
|
|
200
|
+
type Props = Record<string, unknown> & {
|
|
201
|
+
children?: JamxNode | JamxNode[];
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
Propiedades de un elemento, incluyendo `children` opcional.
|
|
205
|
+
|
|
206
|
+
#### ComponentFn
|
|
207
|
+
```typescript
|
|
208
|
+
type ComponentFn = (props: Props) => JamxNode;
|
|
209
|
+
```
|
|
210
|
+
Función que recibe props y retorna un JamxNode.
|
|
211
|
+
|
|
212
|
+
#### PageComponentLike
|
|
213
|
+
```typescript
|
|
214
|
+
interface PageComponentLike {
|
|
215
|
+
render: (ctx: RenderContext) => JamxNode;
|
|
216
|
+
meta?: (ctx: RenderContext) => PageHead;
|
|
217
|
+
layout?: LayoutComponentLike;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
Componente de página que puede ser renderizado.
|
|
221
|
+
|
|
222
|
+
#### LayoutComponentLike
|
|
223
|
+
```typescript
|
|
224
|
+
interface LayoutComponentLike {
|
|
225
|
+
render: (props: { children: JamxNode; ctx: RenderContext }) => JamxNode;
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
Componente layout que envuelve páginas.
|
|
229
|
+
|
|
230
|
+
#### RenderContext
|
|
231
|
+
```typescript
|
|
232
|
+
interface RenderContext {
|
|
233
|
+
env: 'development' | 'production' | 'test';
|
|
234
|
+
path: string;
|
|
235
|
+
url: string;
|
|
236
|
+
// ... propiedades adicionales definidas por el usuario
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
Contexto de renderizado pasado a todos los componentes.
|
|
240
|
+
|
|
241
|
+
#### PageHead
|
|
242
|
+
```typescript
|
|
243
|
+
interface PageHead {
|
|
244
|
+
title?: string;
|
|
245
|
+
description?: string;
|
|
246
|
+
[key: string]: unknown;
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
Metadatos para el `<head>` de la página.
|
|
250
|
+
|
|
251
|
+
### Clases
|
|
252
|
+
|
|
253
|
+
#### SSRRenderer
|
|
254
|
+
```typescript
|
|
255
|
+
class SSRRenderer {
|
|
256
|
+
constructor();
|
|
257
|
+
async render(page: PageComponentLike, ctx: RenderContext): Promise<RenderResult>;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
Renderizador principal de páginas SSR.
|
|
261
|
+
|
|
262
|
+
**RenderResult:**
|
|
263
|
+
```typescript
|
|
264
|
+
interface RenderResult {
|
|
265
|
+
html: string;
|
|
266
|
+
statusCode: number;
|
|
267
|
+
headers: Record<string, string>;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### HtmlSerializer
|
|
272
|
+
```typescript
|
|
273
|
+
class HtmlSerializer {
|
|
274
|
+
serialize(node: JamxNode): string;
|
|
275
|
+
private serializeElement(element: JamxElement): string;
|
|
276
|
+
private serializeComponent(component: ComponentFn, props: Props): string;
|
|
277
|
+
private serializeAttributes(props: Props): string;
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
Serializador de árboles JSX a HTML.
|
|
281
|
+
|
|
282
|
+
### Funciones
|
|
283
|
+
|
|
284
|
+
#### jsx
|
|
285
|
+
```typescript
|
|
286
|
+
function jsx(
|
|
287
|
+
type: string | ComponentFn,
|
|
288
|
+
props: Props,
|
|
289
|
+
key?: string
|
|
290
|
+
): JamxElement
|
|
291
|
+
```
|
|
292
|
+
Crea un elemento JSX. Usado por TypeScript cuando se escribe `<Tag />`.
|
|
293
|
+
|
|
294
|
+
#### jsxs
|
|
295
|
+
```typescript
|
|
296
|
+
const jsxs = jsx;
|
|
297
|
+
```
|
|
298
|
+
Alias de `jsx` para cuando hay múltiples hijos.
|
|
299
|
+
|
|
300
|
+
#### escapeHtml
|
|
301
|
+
```typescript
|
|
302
|
+
function escapeHtml(text: string): string;
|
|
303
|
+
```
|
|
304
|
+
Escapa caracteres HTML especiales para prevenir XSS.
|
|
305
|
+
|
|
306
|
+
#### escapeAttr
|
|
307
|
+
```typescript
|
|
308
|
+
function escapeAttr(value: unknown): string;
|
|
309
|
+
```
|
|
310
|
+
Escapa valores para usar en atributos HTML.
|
|
311
|
+
|
|
312
|
+
#### ErrorBoundary.wrap
|
|
313
|
+
```typescript
|
|
314
|
+
static wrap(
|
|
315
|
+
component: PageComponentLike,
|
|
316
|
+
options: { fallback: (error: Error, info: { componentStack: string }) => JamxNode }
|
|
317
|
+
): PageComponentLike
|
|
318
|
+
```
|
|
319
|
+
Envuelve un componente para capturar errores de renderizado.
|
|
320
|
+
|
|
321
|
+
#### renderErrorPage
|
|
322
|
+
```typescript
|
|
323
|
+
function renderErrorPage(
|
|
324
|
+
error: Error,
|
|
325
|
+
ctx: RenderContext
|
|
326
|
+
): JamxNode
|
|
327
|
+
```
|
|
328
|
+
Genera una página de error por defecto.
|
|
329
|
+
|
|
330
|
+
## Flujo interno detallado
|
|
331
|
+
|
|
332
|
+
### 1. SSRRenderer.render()
|
|
333
|
+
```typescript
|
|
334
|
+
async render(page, ctx) {
|
|
335
|
+
// Paso 1: Obtener metadatos del head
|
|
336
|
+
const head = page.meta?.(ctx) ?? {};
|
|
337
|
+
|
|
338
|
+
// Paso 2: Renderizar el contenido de la página
|
|
339
|
+
const contentNode = page.render(ctx);
|
|
340
|
+
|
|
341
|
+
// Paso 3: Aplicar layout si existe
|
|
342
|
+
const bodyNode = page.layout
|
|
343
|
+
? page.layout.render({ children: contentNode, ctx })
|
|
344
|
+
: contentNode;
|
|
345
|
+
|
|
346
|
+
// Paso 4: Serializar a HTML
|
|
347
|
+
const bodyHtml = this.serializer.serialize(bodyNode);
|
|
348
|
+
|
|
349
|
+
// Paso 5: Construir documento completo
|
|
350
|
+
const html = this.buildDocument(bodyHtml, head, ctx);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
html,
|
|
354
|
+
statusCode: 200,
|
|
355
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 2. HtmlSerializer.serialize()
|
|
361
|
+
```typescript
|
|
362
|
+
serialize(node) {
|
|
363
|
+
// Casos base
|
|
364
|
+
if (node === null || node === undefined || node === false) return "";
|
|
365
|
+
if (node === true) return "";
|
|
366
|
+
if (typeof node === "number") return String(node);
|
|
367
|
+
if (typeof node === "string") return escapeHtml(node);
|
|
368
|
+
|
|
369
|
+
// Arrays
|
|
370
|
+
if (Array.isArray(node)) {
|
|
371
|
+
return node.map(child => this.serialize(child)).join("");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Elemento JSX
|
|
375
|
+
return this.serializeElement(node);
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 3. Serialización de elementos
|
|
380
|
+
```typescript
|
|
381
|
+
serializeElement(element) {
|
|
382
|
+
const { type, props } = element;
|
|
383
|
+
|
|
384
|
+
// Componente funcional
|
|
385
|
+
if (typeof type === "function") {
|
|
386
|
+
return this.serializeComponent(type, props);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Fragment
|
|
390
|
+
if (type === Fragment) {
|
|
391
|
+
return this.serialize(props.children);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Elemento HTML
|
|
395
|
+
const tag = type as string;
|
|
396
|
+
const attrs = this.serializeAttributes(props);
|
|
397
|
+
const children = this.serialize(props.children);
|
|
398
|
+
|
|
399
|
+
if (VOID_ELEMENTS.has(tag)) {
|
|
400
|
+
return `<${tag}${attrs} />`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return `<${tag}${attrs}>${children}</${tag}>`;
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 4. Construcción del documento HTML
|
|
408
|
+
```typescript
|
|
409
|
+
buildDocument(bodyHtml, head, ctx) {
|
|
410
|
+
const title = head.title ?? "JAMX App";
|
|
411
|
+
const description = head.description ?? "";
|
|
412
|
+
const isDev = ctx.env === "development";
|
|
413
|
+
|
|
414
|
+
const metaTags = this.buildMetaTags(head);
|
|
415
|
+
|
|
416
|
+
return `<!DOCTYPE html>
|
|
417
|
+
<html lang="en">
|
|
418
|
+
<head>
|
|
419
|
+
<meta charset="UTF-8" />
|
|
420
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
421
|
+
<title>${escapeTitle(title)}</title>
|
|
422
|
+
${description ? `<meta name="description" content="${escapeAttr(description)}" />` : ""}
|
|
423
|
+
${metaTags}
|
|
424
|
+
<link rel="stylesheet" href="/__jamx/styles.css" />
|
|
425
|
+
</head>
|
|
426
|
+
<body>
|
|
427
|
+
<div id="__jamx_root__" data-route="${escapeAttr(ctx.path)}">${bodyHtml}</div>
|
|
428
|
+
<script type="module" src="/__jamx/client.js"${isDev ? ' data-dev="true"' : ""}></script>
|
|
429
|
+
</body>
|
|
430
|
+
</html>`;
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Consideraciones de rendimiento
|
|
435
|
+
- **Serialización en un solo paso**: El árbol JSX se serializa en un solo pase sin interrupciones
|
|
436
|
+
- **Escape automático**: Todos los strings se escapan por defecto para prevenir XSS
|
|
437
|
+
- **Componentes puros**: Los componentes deben ser puros (sin side effects) para SSR
|
|
438
|
+
- **Streaming**: No implementado; todo el HTML se genera como string completo
|
|
439
|
+
- **Caching**: Se puede cachear el resultado de `render()` para páginas estáticas
|
|
440
|
+
|
|
441
|
+
## Seguridad
|
|
442
|
+
- **XSS Prevention**: Todos los strings se escapan con `escapeHtml()` y `escapeAttr()`
|
|
443
|
+
- **Content Security Policy**: Se puede agregar CSP headers en `RenderContext`
|
|
444
|
+
- **Sanitización**: No se permite HTML raw en props (por defecto)
|
|
445
|
+
|
|
446
|
+
## Configuración
|
|
447
|
+
|
|
448
|
+
### tsconfig.json
|
|
449
|
+
```json
|
|
450
|
+
{
|
|
451
|
+
"extends": "../../tsconfig.base.json",
|
|
452
|
+
"compilerOptions": {
|
|
453
|
+
"rootDir": "src",
|
|
454
|
+
"outDir": "dist",
|
|
455
|
+
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
|
456
|
+
},
|
|
457
|
+
"include": ["src/**/*"],
|
|
458
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Scripts disponibles
|
|
463
|
+
- `pnpm build` - Compila TypeScript a JavaScript
|
|
464
|
+
- `pnpm dev` - Compilación en watch mode
|
|
465
|
+
- `pnpm test` - Ejecuta tests unitarios
|
|
466
|
+
- `pnpm test:watch` - Tests en watch mode
|
|
467
|
+
- `pnpm type-check` - Verifica tipos sin compilar
|
|
468
|
+
- `pnpm clean` - Limpia archivos compilados
|
|
469
|
+
|
|
470
|
+
## Testing
|
|
471
|
+
Tests en `packages/renderer/test/unit/`:
|
|
472
|
+
- `html/escape.test.ts`: Pruebas de escape de HTML
|
|
473
|
+
- `html/serializer.test.ts`: Pruebas de serialización
|
|
474
|
+
- `error/boundary.test.ts`: Pruebas de ErrorBoundary
|
|
475
|
+
- `error/error-page.test.ts`: Pruebas de página de error
|
|
476
|
+
|
|
477
|
+
Ejecutar tests:
|
|
478
|
+
```bash
|
|
479
|
+
pnpm test
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Dependencias
|
|
483
|
+
- `@jamx-framework/core` - Dependencia de trabajo (workspace)
|
|
484
|
+
- `@types/node` - Tipos de Node.js para desarrollo
|
|
485
|
+
- `vitest` - Framework de testing
|
|
486
|
+
- `rimraf` - Limpieza de directorios
|
|
487
|
+
|
|
488
|
+
## Ejemplo completo
|
|
489
|
+
```typescript
|
|
490
|
+
// app/pages/index.tsx
|
|
491
|
+
import { jsx } from '@jamx-framework/renderer';
|
|
492
|
+
import type { PageComponentLike, RenderContext } from '@jamx-framework/renderer';
|
|
493
|
+
|
|
494
|
+
export const HomePage: PageComponentLike = {
|
|
495
|
+
meta() {
|
|
496
|
+
return {
|
|
497
|
+
title: 'Inicio - Mi Tienda Online',
|
|
498
|
+
description: 'La mejor tienda online con los mejores productos',
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
render(ctx: RenderContext) {
|
|
503
|
+
const user = ctx.locals.user as { name: string } | null;
|
|
504
|
+
|
|
505
|
+
return jsx('div', { class: 'container' }, [
|
|
506
|
+
jsx('header', {}, [
|
|
507
|
+
jsx('h1', {}, 'Mi Tienda Online'),
|
|
508
|
+
user && jsx('p', {}, `Bienvenido, ${user.name}`),
|
|
509
|
+
]),
|
|
510
|
+
jsx('main', {}, [
|
|
511
|
+
jsx('section', { id: 'products' }, [
|
|
512
|
+
jsx('h2', {}, 'Productos destacados'),
|
|
513
|
+
jsx('ul', { class: 'product-list' }, [
|
|
514
|
+
jsx('li', { key: '1' }, 'Producto 1'),
|
|
515
|
+
jsx('li', { key: '2' }, 'Producto 2'),
|
|
516
|
+
jsx('li', { key: '3' }, 'Producto 3'),
|
|
517
|
+
]),
|
|
518
|
+
]),
|
|
519
|
+
]),
|
|
520
|
+
jsx('footer', {}, [
|
|
521
|
+
jsx('p', {}, '© 2024 Mi Tienda Online'),
|
|
522
|
+
]),
|
|
523
|
+
]);
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// server/render.ts
|
|
530
|
+
import { SSRRenderer } from '@jamx-framework/renderer';
|
|
531
|
+
import { HomePage } from '../app/pages/index.js';
|
|
532
|
+
|
|
533
|
+
const renderer = new SSRRenderer();
|
|
534
|
+
|
|
535
|
+
export async function renderPage(path: string, ctx: RenderContext) {
|
|
536
|
+
// Aquí se seleccionaría la página según la ruta
|
|
537
|
+
const page = HomePage;
|
|
538
|
+
|
|
539
|
+
const result = await renderer.render(page, ctx);
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
statusCode: result.statusCode,
|
|
543
|
+
headers: result.headers,
|
|
544
|
+
body: result.html,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Limitaciones
|
|
550
|
+
- No soporta streaming de HTML (todo se genera en memoria)
|
|
551
|
+
- No incluye hidratación automática (requiere cliente separado)
|
|
552
|
+
- No soporta Suspense o async components en SSR (por ahora)
|
|
553
|
+
- Los componentes deben ser puros (sin side effects en render)
|
|
554
|
+
|
|
555
|
+
## Futuras mejoras
|
|
556
|
+
- Streaming SSR con `ReadableStream`
|
|
557
|
+
- Soporte para Suspense y async components
|
|
558
|
+
- Optimizaciones de compilación (compile-time rendering)
|
|
559
|
+
- Soporte para más tags HTML y atributos especiales
|
|
560
|
+
- Integración con sistemas de caché (Redis, CDN)
|