@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.
@@ -1,137 +0,0 @@
1
- import type { RenderError } from "./types.js";
2
-
3
- /**
4
- * Renderiza una página de error HTML.
5
- * En producción no muestra el stack trace.
6
- */
7
- export function renderErrorPage(error: RenderError, isDev: boolean): string {
8
- const title = errorTitle(error.statusCode);
9
- const stack =
10
- isDev && error.stack
11
- ? `<pre class="stack">${escapeHtml(error.stack)}</pre>`
12
- : "";
13
-
14
- return `<!DOCTYPE html>
15
- <html lang="en">
16
- <head>
17
- <meta charset="UTF-8">
18
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
- <title>${error.statusCode} — ${title}</title>
20
- <style>
21
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
22
-
23
- body {
24
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
25
- background: #0f0f0f;
26
- color: #e5e5e5;
27
- min-height: 100vh;
28
- display: flex;
29
- align-items: center;
30
- justify-content: center;
31
- }
32
-
33
- .container {
34
- max-width: 640px;
35
- width: 100%;
36
- padding: 2rem;
37
- }
38
-
39
- .code {
40
- font-size: 6rem;
41
- font-weight: 800;
42
- color: #3b82f6;
43
- line-height: 1;
44
- letter-spacing: -4px;
45
- }
46
-
47
- .title {
48
- font-size: 1.5rem;
49
- font-weight: 600;
50
- margin-top: 0.5rem;
51
- color: #f5f5f5;
52
- }
53
-
54
- .message {
55
- margin-top: 1rem;
56
- color: #a3a3a3;
57
- font-size: 0.95rem;
58
- line-height: 1.6;
59
- }
60
-
61
- .path {
62
- margin-top: 0.5rem;
63
- font-size: 0.8rem;
64
- color: #525252;
65
- font-family: monospace;
66
- }
67
-
68
- .stack {
69
- margin-top: 1.5rem;
70
- padding: 1rem;
71
- background: #171717;
72
- border: 1px solid #262626;
73
- border-radius: 8px;
74
- font-size: 0.75rem;
75
- color: #f87171;
76
- overflow-x: auto;
77
- white-space: pre-wrap;
78
- word-break: break-word;
79
- line-height: 1.6;
80
- }
81
-
82
- .divider {
83
- margin-top: 2rem;
84
- border: none;
85
- border-top: 1px solid #262626;
86
- }
87
-
88
- .footer {
89
- margin-top: 1rem;
90
- font-size: 0.75rem;
91
- color: #404040;
92
- }
93
- </style>
94
- </head>
95
- <body>
96
- <div class="container">
97
- <div class="code">${error.statusCode}</div>
98
- <div class="title">${escapeHtml(title)}</div>
99
- <div class="message">${escapeHtml(error.message)}</div>
100
- ${error.path ? `<div class="path">${escapeHtml(error.path)}</div>` : ""}
101
- ${stack}
102
- <hr class="divider">
103
- <div class="footer">JAMX Framework</div>
104
- </div>
105
- </body>
106
- </html>`;
107
- }
108
-
109
- function errorTitle(statusCode: number): string {
110
- switch (statusCode) {
111
- case 400:
112
- return "Bad Request";
113
- case 401:
114
- return "Unauthorized";
115
- case 403:
116
- return "Forbidden";
117
- case 404:
118
- return "Not Found";
119
- case 422:
120
- return "Unprocessable Entity";
121
- case 500:
122
- return "Internal Server Error";
123
- case 503:
124
- return "Service Unavailable";
125
- default:
126
- return statusCode >= 500 ? "Server Error" : "Client Error";
127
- }
128
- }
129
-
130
- function escapeHtml(str: string): string {
131
- return str
132
- .replace(/&/g, "&amp;")
133
- .replace(/</g, "&lt;")
134
- .replace(/>/g, "&gt;")
135
- .replace(/"/g, "&quot;")
136
- .replace(/'/g, "&#039;");
137
- }
@@ -1,6 +0,0 @@
1
- export interface RenderError {
2
- statusCode: number;
3
- message: string;
4
- stack?: string | undefined;
5
- path?: string | undefined;
6
- }
@@ -1,90 +0,0 @@
1
- // packages/renderer/src/html/escape.ts
2
-
3
- /**
4
- * Escapa caracteres especiales HTML para prevenir XSS.
5
- * Siempre usar esto al renderizar contenido de usuario.
6
- */
7
- export function escapeHtml(value: unknown): string {
8
- const str = String(value);
9
- let result = "";
10
-
11
- for (let i = 0; i < str.length; i++) {
12
- const char = str[i];
13
- switch (char) {
14
- case "&":
15
- result += "&amp;";
16
- break;
17
- case "<":
18
- result += "&lt;";
19
- break;
20
- case ">":
21
- result += "&gt;";
22
- break;
23
- case '"':
24
- result += "&quot;";
25
- break;
26
- case "'":
27
- result += "&#39;";
28
- break;
29
- default:
30
- result += char;
31
- }
32
- }
33
-
34
- return result;
35
- }
36
-
37
- /**
38
- * Tags HTML que no tienen closing tag.
39
- */
40
- export const VOID_ELEMENTS = new Set([
41
- "area",
42
- "base",
43
- "br",
44
- "col",
45
- "embed",
46
- "hr",
47
- "img",
48
- "input",
49
- "link",
50
- "meta",
51
- "param",
52
- "source",
53
- "track",
54
- "wbr",
55
- ]);
56
-
57
- /**
58
- * Tags cuyo contenido se renderiza tal cual (sin escapar).
59
- */
60
- export const RAW_TEXT_ELEMENTS = new Set(["script", "style"]);
61
-
62
- /**
63
- * Atributos booleanos — se renderizan sin valor si son true.
64
- * Ej: <input disabled /> en lugar de <input disabled="true" />
65
- */
66
- export const BOOLEAN_ATTRIBUTES = new Set([
67
- "allowfullscreen",
68
- "async",
69
- "autofocus",
70
- "autoplay",
71
- "checked",
72
- "controls",
73
- "default",
74
- "defer",
75
- "disabled",
76
- "formnovalidate",
77
- "hidden",
78
- "ismap",
79
- "loop",
80
- "multiple",
81
- "muted",
82
- "nomodule",
83
- "novalidate",
84
- "open",
85
- "readonly",
86
- "required",
87
- "reversed",
88
- "selected",
89
- "typemustmatch",
90
- ]);
@@ -1,161 +0,0 @@
1
- import {
2
- escapeHtml,
3
- VOID_ELEMENTS,
4
- RAW_TEXT_ELEMENTS,
5
- BOOLEAN_ATTRIBUTES,
6
- } from "./escape.js";
7
- import { Fragment } from "../jsx/jsx-runtime.js";
8
- import type {
9
- JamxNode,
10
- JamxElement,
11
- ComponentFn,
12
- Props,
13
- } from "../jsx/jsx-runtime.js";
14
-
15
- /**
16
- * Serializa un árbol JSX a una string HTML.
17
- *
18
- * Proceso:
19
- * 1. Si el nodo es un string/number → escapar y retornar
20
- * 2. Si el nodo es un componente (función) → llamarlo y serializar el resultado
21
- * 3. Si el nodo es un elemento HTML → serializar tag + props + children
22
- * 4. Si el nodo es un Fragment → serializar solo los children
23
- */
24
- export class HtmlSerializer {
25
- serialize(node: JamxNode): string {
26
- // Nodos primitivos
27
- if (node === null || node === undefined || node === false) return "";
28
- if (node === true) return "";
29
- if (typeof node === "number") return String(node);
30
- if (typeof node === "string") return escapeHtml(node);
31
-
32
- // Arrays
33
- if (Array.isArray(node)) {
34
- return node.map((child) => this.serialize(child)).join("");
35
- }
36
-
37
- // Elemento JSX
38
- return this.serializeElement(node);
39
- }
40
-
41
- private serializeElement(element: JamxElement): string {
42
- const { type, props } = element;
43
-
44
- // Componente funcional — ejecutar y serializar el resultado
45
- if (typeof type === "function") {
46
- return this.serializeComponent(type, props);
47
- }
48
-
49
- // Fragment — solo los hijos
50
- if (type === Fragment) {
51
- return this.serializeChildren(props.children);
52
- }
53
-
54
- // Elemento HTML
55
- return this.serializeHtmlElement(type, props);
56
- }
57
-
58
- private serializeComponent(fn: ComponentFn, props: Props): string {
59
- try {
60
- const result = fn(props);
61
- return this.serialize(result);
62
- } catch (err) {
63
- throw new Error(
64
- `Error rendering component '${fn.name || "anonymous"}': ${String(err)}`,
65
- );
66
- }
67
- }
68
-
69
- private serializeHtmlElement(tag: string, props: Props): string {
70
- const { children, ...attrs } = props;
71
-
72
- const attrString = this.serializeAttributes(attrs);
73
- const openTag = attrString ? `<${tag} ${attrString}>` : `<${tag}>`;
74
-
75
- // Elementos void — no tienen closing tag ni children
76
- if (VOID_ELEMENTS.has(tag)) {
77
- return openTag;
78
- }
79
-
80
- // Elementos con contenido raw (script, style)
81
- if (RAW_TEXT_ELEMENTS.has(tag)) {
82
- const rawContent = typeof children === "string" ? children : "";
83
- return `${openTag}${rawContent}</${tag}>`;
84
- }
85
-
86
- const childrenHtml = this.serializeChildren(children);
87
- return `${openTag}${childrenHtml}</${tag}>`;
88
- }
89
-
90
- private serializeAttributes(attrs: Record<string, unknown>): string {
91
- const parts: string[] = [];
92
-
93
- for (const [key, value] of Object.entries(attrs)) {
94
- // Ignorar atributos internos y undefined/null
95
- if (key === "key" || value === undefined || value === null) continue;
96
-
97
- // className → class
98
- const attrName =
99
- key === "className"
100
- ? "class"
101
- : key === "htmlFor"
102
- ? "for"
103
- : key === "tabIndex"
104
- ? "tabindex"
105
- : camelToKebab(key);
106
-
107
- // Atributos booleanos
108
- if (BOOLEAN_ATTRIBUTES.has(attrName)) {
109
- if (value === true) parts.push(attrName);
110
- continue;
111
- }
112
-
113
- // style como objeto
114
- if (key === "style" && typeof value === "object" && value !== null) {
115
- const styleStr = this.serializeStyle(value as Record<string, string>);
116
- if (styleStr) parts.push(`style="${escapeHtml(styleStr)}"`);
117
- continue;
118
- }
119
-
120
- // false → omitir el atributo
121
- if (value === false) continue;
122
-
123
- // true → atributo sin valor (no booleano, ej: data-active)
124
- if (value === true) {
125
- parts.push(attrName);
126
- continue;
127
- }
128
-
129
- parts.push(`${attrName}="${escapeHtml(value)}"`);
130
- }
131
-
132
- return parts.join(" ");
133
- }
134
-
135
- private serializeStyle(style: Record<string, string>): string {
136
- return Object.entries(style)
137
- .map(([prop, value]) => `${camelToKebab(prop)}: ${value}`)
138
- .join("; ");
139
- }
140
-
141
- private serializeChildren(children: unknown): string {
142
- if (children === undefined || children === null) return "";
143
- if (Array.isArray(children)) {
144
- return children
145
- .map((child) => this.serialize(child as JamxNode))
146
- .join("");
147
- }
148
- return this.serialize(children as JamxNode);
149
- }
150
- }
151
-
152
- // ── HELPERS ───────────────────────────────────────────────────────────────
153
-
154
- /**
155
- * Convierte camelCase a kebab-case para atributos HTML.
156
- * backgroundColor → background-color
157
- * dataTestId → data-test-id (los data- ya vienen bien)
158
- */
159
- function camelToKebab(str: string): string {
160
- return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
161
- }
package/src/index.ts DELETED
@@ -1,33 +0,0 @@
1
- export { SSRRenderer } from "./pipeline/renderer.js";
2
- export { HtmlSerializer } from "./html/serializer.js";
3
- export {
4
- escapeHtml,
5
- VOID_ELEMENTS,
6
- BOOLEAN_ATTRIBUTES,
7
- } from "./html/escape.js";
8
- export { jsx, jsxs, jsxDEV, Fragment } from "./jsx/jsx-runtime.js";
9
-
10
- export type {
11
- JamxNode,
12
- JamxElement,
13
- ComponentFn,
14
- Props,
15
- JSX,
16
- HtmlAttributes,
17
- } from "./jsx/jsx-runtime.js";
18
-
19
- export type {
20
- RenderContext,
21
- RenderResult,
22
- PageHead,
23
- } from "./pipeline/context.js";
24
-
25
- export type {
26
- PageComponentLike,
27
- LayoutComponentLike,
28
- } from "./pipeline/renderer.js";
29
-
30
- export { ErrorBoundary } from "./error/boundary.js";
31
- export { renderErrorPage } from "./error/error-page.js";
32
- export type { RenderError } from "./error/types.js";
33
- export type { BoundaryOptions, BoundaryResult } from "./error/boundary.js";
@@ -1,247 +0,0 @@
1
- // ── TIPOS DEL ÁRBOL JSX ───────────────────────────────────────────────────
2
-
3
- export type JamxNode =
4
- | string
5
- | number
6
- | boolean
7
- | null
8
- | undefined
9
- | JamxElement
10
- | JamxNode[];
11
-
12
- export interface JamxElement {
13
- type: string | ComponentFn;
14
- props: Props;
15
- key: string | null;
16
- }
17
-
18
- export type Props = Record<string, unknown> & {
19
- children?: JamxNode | JamxNode[];
20
- };
21
-
22
- export type ComponentFn = (props: Props) => JamxNode;
23
-
24
- // ── JSX RUNTIME ───────────────────────────────────────────────────────────
25
-
26
- /**
27
- * Función principal del JSX runtime.
28
- * TypeScript llama esto cuando encuentra <Tag prop="value" />
29
- *
30
- * En lugar de React.createElement, JAMX usa esta función
31
- * para construir el árbol de nodos que luego serializa a HTML.
32
- */
33
- export function jsx(
34
- type: string | ComponentFn,
35
- props: Props,
36
- key?: string,
37
- ): JamxElement {
38
- return {
39
- type,
40
- props: props ?? {},
41
- key: key ?? null,
42
- };
43
- }
44
-
45
- /**
46
- * Versión para elementos con múltiples hijos.
47
- * TypeScript la usa cuando hay más de un hijo.
48
- */
49
- export const jsxs = jsx;
50
-
51
- /**
52
- * Versión dev con información adicional para debugging.
53
- * En producción es igual a jsx.
54
- */
55
- export const jsxDEV = jsx;
56
-
57
- /**
58
- * Fragment — permite retornar múltiples elementos sin wrapper.
59
- * Uso: <> <div/> <div/> </>
60
- */
61
- export const Fragment = "jamx.fragment" as const;
62
-
63
- // ── TIPOS GLOBALES JSX ────────────────────────────────────────────────────
64
-
65
- /**
66
- * Declaraciones de tipos para que TypeScript entienda el JSX de JAMX.
67
- * Estas interfaces definen qué props acepta cada elemento HTML.
68
- */
69
- export namespace JSX {
70
- export type Element = JamxElement;
71
-
72
- export interface ElementChildrenAttribute {
73
- children: JamxNode | JamxNode[];
74
- }
75
-
76
- export interface IntrinsicElements {
77
- // Document
78
- html: HtmlAttributes & { lang?: string };
79
- head: HtmlAttributes;
80
- body: HtmlAttributes;
81
- title: HtmlAttributes;
82
- meta: MetaAttributes;
83
- link: LinkAttributes;
84
- script: ScriptAttributes;
85
- style: HtmlAttributes;
86
- // Sections
87
- main: HtmlAttributes;
88
- header: HtmlAttributes;
89
- footer: HtmlAttributes;
90
- nav: HtmlAttributes;
91
- section: HtmlAttributes;
92
- article: HtmlAttributes;
93
- aside: HtmlAttributes;
94
- // Headings
95
- h1: HtmlAttributes;
96
- h2: HtmlAttributes;
97
- h3: HtmlAttributes;
98
- h4: HtmlAttributes;
99
- h5: HtmlAttributes;
100
- h6: HtmlAttributes;
101
- // Text
102
- p: HtmlAttributes;
103
- span: HtmlAttributes;
104
- strong: HtmlAttributes;
105
- em: HtmlAttributes;
106
- small: HtmlAttributes;
107
- code: HtmlAttributes;
108
- pre: HtmlAttributes;
109
- // Lists
110
- ul: HtmlAttributes;
111
- ol: HtmlAttributes;
112
- li: HtmlAttributes;
113
- // Interactive
114
- a: AnchorAttributes;
115
- button: ButtonAttributes;
116
- input: InputAttributes;
117
- textarea: TextareaAttributes;
118
- select: HtmlAttributes;
119
- option: HtmlAttributes & { value?: string; selected?: boolean };
120
- form: FormAttributes;
121
- label: HtmlAttributes & { for?: string; htmlFor?: string };
122
- // Media
123
- img: ImgAttributes;
124
- video: HtmlAttributes & {
125
- src?: string;
126
- controls?: boolean;
127
- autoplay?: boolean;
128
- };
129
- audio: HtmlAttributes & { src?: string; controls?: boolean };
130
- canvas: HtmlAttributes & { width?: number; height?: number };
131
- // Layout
132
- div: HtmlAttributes;
133
- table: HtmlAttributes;
134
- thead: HtmlAttributes;
135
- tbody: HtmlAttributes;
136
- tr: HtmlAttributes;
137
- th: HtmlAttributes & { colSpan?: number; rowSpan?: number };
138
- td: HtmlAttributes & { colSpan?: number; rowSpan?: number };
139
- // Misc
140
- br: HtmlAttributes;
141
- hr: HtmlAttributes;
142
- slot: HtmlAttributes & { name?: string };
143
- // Generic fallback
144
- [tag: string]: HtmlAttributes;
145
- }
146
- }
147
-
148
- // ── TIPOS DE ATRIBUTOS ────────────────────────────────────────────────────
149
-
150
- export interface HtmlAttributes {
151
- id?: string;
152
- class?: string;
153
- className?: string;
154
- style?: string | Record<string, string>;
155
- children?: JamxNode | JamxNode[];
156
- // Data attributes
157
- [key: `data-${string}`]: string | undefined;
158
- // ARIA
159
- role?: string;
160
- "aria-label"?: string;
161
- "aria-hidden"?: boolean;
162
- "aria-expanded"?: boolean;
163
- "aria-disabled"?: boolean;
164
- "aria-selected"?: boolean;
165
- "aria-controls"?: string;
166
- "aria-describedby"?: string;
167
- tabIndex?: number;
168
- }
169
-
170
- export interface AnchorAttributes extends HtmlAttributes {
171
- href?: string;
172
- target?: "_blank" | "_self" | "_parent" | "_top";
173
- rel?: string;
174
- download?: boolean | string;
175
- }
176
-
177
- export interface ButtonAttributes extends HtmlAttributes {
178
- type?: "button" | "submit" | "reset";
179
- disabled?: boolean;
180
- name?: string;
181
- value?: string;
182
- }
183
-
184
- export interface InputAttributes extends HtmlAttributes {
185
- type?: string;
186
- name?: string;
187
- value?: string | number;
188
- placeholder?: string;
189
- required?: boolean;
190
- disabled?: boolean;
191
- readonly?: boolean;
192
- checked?: boolean;
193
- min?: string | number;
194
- max?: string | number;
195
- step?: string | number;
196
- pattern?: string;
197
- autocomplete?: string;
198
- }
199
-
200
- export interface TextareaAttributes extends HtmlAttributes {
201
- name?: string;
202
- rows?: number;
203
- cols?: number;
204
- placeholder?: string;
205
- required?: boolean;
206
- disabled?: boolean;
207
- readonly?: boolean;
208
- }
209
-
210
- export interface FormAttributes extends HtmlAttributes {
211
- action?: string;
212
- method?: "get" | "post";
213
- enctype?: string;
214
- }
215
-
216
- export interface ImgAttributes extends HtmlAttributes {
217
- src?: string;
218
- alt?: string;
219
- width?: number | string;
220
- height?: number | string;
221
- loading?: "lazy" | "eager";
222
- }
223
-
224
- export interface MetaAttributes extends HtmlAttributes {
225
- name?: string;
226
- content?: string;
227
- charset?: string;
228
- httpEquiv?: string;
229
- property?: string;
230
- "http-equiv"?: string;
231
- }
232
-
233
- export interface LinkAttributes extends HtmlAttributes {
234
- rel?: string;
235
- href?: string;
236
- type?: string;
237
- media?: string;
238
- as?: string;
239
- }
240
-
241
- export interface ScriptAttributes extends HtmlAttributes {
242
- src?: string;
243
- type?: string;
244
- async?: boolean;
245
- defer?: boolean;
246
- module?: boolean;
247
- }