@jamx-framework/renderer 0.1.0 → 1.0.0

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 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)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jamx-framework/renderer",
3
- "version": "0.1.0",
4
- "description": "JAMX Framework SSR Renderer",
3
+ "version": "1.0.0",
4
+ "description": "JAMX Framework — SSR Renderer",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -19,14 +19,6 @@
19
19
  "types": "./dist/jsx/jsx-runtime.d.ts"
20
20
  }
21
21
  },
22
- "dependencies": {
23
- "@jamx-framework/core": "0.1.0"
24
- },
25
- "devDependencies": {
26
- "@types/node": "^22.0.0",
27
- "rimraf": "^6.0.0",
28
- "vitest": "^2.1.0"
29
- },
30
22
  "scripts": {
31
23
  "build": "tsc --project tsconfig.json",
32
24
  "dev": "tsc --project tsconfig.json --watch",
@@ -34,5 +26,38 @@
34
26
  "test:watch": "vitest",
35
27
  "type-check": "tsc --noEmit",
36
28
  "clean": "rimraf dist *.tsbuildinfo"
37
- }
38
- }
29
+ },
30
+ "dependencies": {
31
+ "@jamx-framework/core": "1.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "rimraf": "^6.0.0",
36
+ "vitest": "^2.1.0"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "LICENSE",
41
+ "README.md"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "license": "MIT",
47
+ "author": "Stiven-21 <https://github.com/Stiven-21>",
48
+ "homepage": "https://jamx-framework.dev",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/Stiven-21/jamx",
52
+ "directory": "packages/renderer"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/Stiven-21/jamx/issues"
56
+ },
57
+ "keywords": [
58
+ "jamx-framework",
59
+ "framework",
60
+ "typescript",
61
+ "fullstack"
62
+ ]
63
+ }
@@ -1,4 +0,0 @@
1
-
2
- > @jamx-framework/renderer@0.1.0 build /home/runner/work/jamx/jamx/packages/renderer
3
- > tsc --project tsconfig.json
4
-
@@ -1,80 +0,0 @@
1
- import { HttpException } from "@jamx-framework/core";
2
- import { renderErrorPage } from "./error-page.js";
3
- import type { RenderError } from "./types.js";
4
-
5
- export interface BoundaryOptions {
6
- isDev: boolean;
7
- path?: string;
8
- }
9
-
10
- export interface BoundaryResult {
11
- html: string;
12
- statusCode: number;
13
- error: RenderError;
14
- }
15
-
16
- /**
17
- * Captura errores durante el render SSR y genera una página de error.
18
- *
19
- * Uso:
20
- * const result = await ErrorBoundary.wrap(
21
- * () => renderer.render(page, props),
22
- * { isDev: true, path: '/about' }
23
- * )
24
- *
25
- * if (result.caught) {
26
- * res.html(result.html, result.statusCode)
27
- * }
28
- */
29
- export class ErrorBoundary {
30
- static async wrap<T>(
31
- fn: () => Promise<T>,
32
- options: BoundaryOptions,
33
- ): Promise<
34
- { caught: false; value: T } | ({ caught: true } & BoundaryResult)
35
- > {
36
- try {
37
- const value = await fn();
38
- return { caught: false, value };
39
- } catch (err) {
40
- const renderError = ErrorBoundary.toRenderError(err, options.path);
41
- const html = renderErrorPage(renderError, options.isDev);
42
-
43
- return {
44
- caught: true,
45
- html,
46
- statusCode: renderError.statusCode,
47
- error: renderError,
48
- };
49
- }
50
- }
51
-
52
- /**
53
- * Convierte cualquier error a RenderError.
54
- */
55
- static toRenderError(err: unknown, path?: string): RenderError {
56
- if (err instanceof HttpException) {
57
- return {
58
- statusCode: err.statusCode,
59
- message: err.message,
60
- stack: err.stack,
61
- path,
62
- };
63
- }
64
-
65
- if (err instanceof Error) {
66
- return {
67
- statusCode: 500,
68
- message: err.message,
69
- stack: err.stack,
70
- path,
71
- };
72
- }
73
-
74
- return {
75
- statusCode: 500,
76
- message: "An unexpected error occurred",
77
- path,
78
- };
79
- }
80
- }