@getforma/core 0.3.1 → 0.8.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 +279 -20
- package/dist/chunk-3U57L2TY.cjs +38 -0
- package/dist/chunk-3U57L2TY.cjs.map +1 -0
- package/dist/{chunk-5H52PKGF.js → chunk-N522P3G6.js} +37 -19
- package/dist/chunk-N522P3G6.js.map +1 -0
- package/dist/chunk-OZCHIVAZ.js +35 -0
- package/dist/chunk-OZCHIVAZ.js.map +1 -0
- package/dist/{chunk-TSQ7AKFT.cjs → chunk-YMIMKO4W.cjs} +66 -25
- package/dist/chunk-YMIMKO4W.cjs.map +1 -0
- package/dist/forma-runtime-csp.js +1 -1
- package/dist/forma-runtime.js +1 -1
- package/dist/formajs-runtime-hardened.global.js +1 -1
- package/dist/formajs-runtime-hardened.global.js.map +1 -1
- package/dist/formajs-runtime.global.js +1 -1
- package/dist/formajs-runtime.global.js.map +1 -1
- package/dist/formajs.global.js +1 -1
- package/dist/formajs.global.js.map +1 -1
- package/dist/index.cjs +142 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -29
- package/dist/index.d.ts +70 -29
- package/dist/index.js +64 -60
- package/dist/index.js.map +1 -1
- package/dist/runtime-hardened.cjs +166 -39
- package/dist/runtime-hardened.cjs.map +1 -1
- package/dist/runtime-hardened.js +167 -40
- package/dist/runtime-hardened.js.map +1 -1
- package/dist/runtime.cjs +184 -57
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +162 -35
- package/dist/runtime.js.map +1 -1
- package/dist/signal-B4_wQJHs.d.cts +53 -0
- package/dist/signal-B4_wQJHs.d.ts +53 -0
- package/dist/ssr/index.cjs +37 -12
- package/dist/ssr/index.cjs.map +1 -1
- package/dist/ssr/index.js +37 -12
- package/dist/ssr/index.js.map +1 -1
- package/dist/tc39-compat.cjs +4 -12
- package/dist/tc39-compat.cjs.map +1 -1
- package/dist/tc39-compat.d.cts +1 -1
- package/dist/tc39-compat.d.ts +1 -1
- package/dist/tc39-compat.js +3 -11
- package/dist/tc39-compat.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-5H52PKGF.js.map +0 -1
- package/dist/chunk-FPSLC62A.cjs +0 -31
- package/dist/chunk-FPSLC62A.cjs.map +0 -1
- package/dist/chunk-KX5WRZH7.js +0 -27
- package/dist/chunk-KX5WRZH7.js.map +0 -1
- package/dist/chunk-TSQ7AKFT.cjs.map +0 -1
- package/dist/signal-CfLDwMyg.d.cts +0 -29
- package/dist/signal-CfLDwMyg.d.ts +0 -29
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forma Reactive - Signal
|
|
3
|
+
*
|
|
4
|
+
* Fine-grained reactive primitive backed by alien-signals.
|
|
5
|
+
* API: createSignal returns [getter, setter] tuple following SolidJS conventions.
|
|
6
|
+
*
|
|
7
|
+
* TC39 Signals equivalent: Signal.State
|
|
8
|
+
*/
|
|
9
|
+
type SignalGetter<T> = () => T;
|
|
10
|
+
type SignalSetter<T> = (v: T | ((prev: T) => T)) => void;
|
|
11
|
+
interface SignalOptions<T> {
|
|
12
|
+
/** Debug name — attached to getter in dev mode for devtools inspection. */
|
|
13
|
+
name?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Custom equality check. When provided, the setter will read the current
|
|
16
|
+
* value and only update the signal if `equals(prev, next)` returns `false`.
|
|
17
|
+
*
|
|
18
|
+
* Default: none (alien-signals uses strict inequality `!==` internally).
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const [pos, setPos] = createSignal(
|
|
22
|
+
* { x: 0, y: 0 },
|
|
23
|
+
* { equals: (a, b) => a.x === b.x && a.y === b.y },
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* setPos({ x: 0, y: 0 }); // skipped — equals returns true
|
|
27
|
+
* setPos({ x: 1, y: 0 }); // applied — equals returns false
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
equals?: (prev: T, next: T) => boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a reactive signal.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* const [count, setCount] = createSignal(0);
|
|
37
|
+
* console.log(count()); // 0
|
|
38
|
+
* setCount(1);
|
|
39
|
+
* setCount(prev => prev + 1);
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* With custom equality:
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const [pos, setPos] = createSignal(
|
|
46
|
+
* { x: 0, y: 0 },
|
|
47
|
+
* { equals: (a, b) => a.x === b.x && a.y === b.y },
|
|
48
|
+
* );
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function createSignal<T>(initialValue: T, options?: SignalOptions<T>): [get: SignalGetter<T>, set: SignalSetter<T>];
|
|
52
|
+
|
|
53
|
+
export { type SignalGetter as S, type SignalOptions as a, createSignal as c };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forma Reactive - Signal
|
|
3
|
+
*
|
|
4
|
+
* Fine-grained reactive primitive backed by alien-signals.
|
|
5
|
+
* API: createSignal returns [getter, setter] tuple following SolidJS conventions.
|
|
6
|
+
*
|
|
7
|
+
* TC39 Signals equivalent: Signal.State
|
|
8
|
+
*/
|
|
9
|
+
type SignalGetter<T> = () => T;
|
|
10
|
+
type SignalSetter<T> = (v: T | ((prev: T) => T)) => void;
|
|
11
|
+
interface SignalOptions<T> {
|
|
12
|
+
/** Debug name — attached to getter in dev mode for devtools inspection. */
|
|
13
|
+
name?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Custom equality check. When provided, the setter will read the current
|
|
16
|
+
* value and only update the signal if `equals(prev, next)` returns `false`.
|
|
17
|
+
*
|
|
18
|
+
* Default: none (alien-signals uses strict inequality `!==` internally).
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const [pos, setPos] = createSignal(
|
|
22
|
+
* { x: 0, y: 0 },
|
|
23
|
+
* { equals: (a, b) => a.x === b.x && a.y === b.y },
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* setPos({ x: 0, y: 0 }); // skipped — equals returns true
|
|
27
|
+
* setPos({ x: 1, y: 0 }); // applied — equals returns false
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
equals?: (prev: T, next: T) => boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a reactive signal.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* const [count, setCount] = createSignal(0);
|
|
37
|
+
* console.log(count()); // 0
|
|
38
|
+
* setCount(1);
|
|
39
|
+
* setCount(prev => prev + 1);
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* With custom equality:
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const [pos, setPos] = createSignal(
|
|
46
|
+
* { x: 0, y: 0 },
|
|
47
|
+
* { equals: (a, b) => a.x === b.x && a.y === b.y },
|
|
48
|
+
* );
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function createSignal<T>(initialValue: T, options?: SignalOptions<T>): [get: SignalGetter<T>, set: SignalSetter<T>];
|
|
52
|
+
|
|
53
|
+
export { type SignalGetter as S, type SignalOptions as a, createSignal as c };
|
package/dist/ssr/index.cjs
CHANGED
|
@@ -26,8 +26,10 @@ function escapeHtml(str) {
|
|
|
26
26
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
27
27
|
}
|
|
28
28
|
function escapeAttr(str) {
|
|
29
|
-
return str.replace(/&/g, "&").replace(/"/g, """);
|
|
29
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'");
|
|
30
30
|
}
|
|
31
|
+
var DANGEROUS_URI_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction"]);
|
|
32
|
+
var DANGEROUS_URI_RE = /^\s*(javascript|vbscript|data\s*:\s*text\/html)/i;
|
|
31
33
|
function sh(tag, props, ...children) {
|
|
32
34
|
return { tag, props: props ?? null, children };
|
|
33
35
|
}
|
|
@@ -65,6 +67,9 @@ function renderToBuffer(node, parts) {
|
|
|
65
67
|
if (resolved === true) {
|
|
66
68
|
parts.push(" ", attrName);
|
|
67
69
|
} else if (resolved !== false && resolved != null) {
|
|
70
|
+
if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === "string" && DANGEROUS_URI_RE.test(resolved)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
68
73
|
parts.push(" ", attrName, '="', escapeAttr(String(resolved)), '"');
|
|
69
74
|
}
|
|
70
75
|
}
|
|
@@ -80,11 +85,11 @@ function renderToBuffer(node, parts) {
|
|
|
80
85
|
const html = raw.__html;
|
|
81
86
|
if (typeof html === "string") {
|
|
82
87
|
parts.push(html);
|
|
83
|
-
} else
|
|
84
|
-
|
|
88
|
+
} else {
|
|
89
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
85
90
|
}
|
|
86
|
-
} else
|
|
87
|
-
|
|
91
|
+
} else {
|
|
92
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
88
93
|
}
|
|
89
94
|
} else {
|
|
90
95
|
for (const child of children) {
|
|
@@ -141,6 +146,9 @@ function renderToBufferHydrated(node, parts, ctx) {
|
|
|
141
146
|
if (resolved === true) {
|
|
142
147
|
parts.push(" ", attrName);
|
|
143
148
|
} else if (resolved !== false && resolved != null) {
|
|
149
|
+
if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === "string" && DANGEROUS_URI_RE.test(resolved)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
144
152
|
parts.push(" ", attrName, '="', escapeAttr(String(resolved)), '"');
|
|
145
153
|
}
|
|
146
154
|
}
|
|
@@ -156,11 +164,11 @@ function renderToBufferHydrated(node, parts, ctx) {
|
|
|
156
164
|
const html = raw.__html;
|
|
157
165
|
if (typeof html === "string") {
|
|
158
166
|
parts.push(html);
|
|
159
|
-
} else
|
|
160
|
-
|
|
167
|
+
} else {
|
|
168
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
161
169
|
}
|
|
162
|
-
} else
|
|
163
|
-
|
|
170
|
+
} else {
|
|
171
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
164
172
|
}
|
|
165
173
|
} else {
|
|
166
174
|
for (const child of children) {
|
|
@@ -197,8 +205,11 @@ function $FORMA_SWAP(id,html){
|
|
|
197
205
|
}
|
|
198
206
|
</script>`;
|
|
199
207
|
}
|
|
208
|
+
function safeJsonStringify(val) {
|
|
209
|
+
return JSON.stringify(val).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
210
|
+
}
|
|
200
211
|
function getSwapTag(id, html) {
|
|
201
|
-
return `<script>$FORMA_SWAP(${
|
|
212
|
+
return `<script>$FORMA_SWAP(${safeJsonStringify(id)},${safeJsonStringify(html)})</script>`;
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
// src/ssr/stream.ts
|
|
@@ -231,6 +242,9 @@ function renderSync(node, parts) {
|
|
|
231
242
|
if (resolved === true) {
|
|
232
243
|
parts.push(" ", attrName);
|
|
233
244
|
} else if (resolved !== false && resolved != null) {
|
|
245
|
+
if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === "string" && DANGEROUS_URI_RE.test(resolved)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
234
248
|
parts.push(" ", attrName, '="', escapeAttr(String(resolved)), '"');
|
|
235
249
|
}
|
|
236
250
|
}
|
|
@@ -241,7 +255,11 @@ function renderSync(node, parts) {
|
|
|
241
255
|
}
|
|
242
256
|
parts.push(">");
|
|
243
257
|
if (props?.["dangerouslySetInnerHTML"]) {
|
|
244
|
-
|
|
258
|
+
const raw = props["dangerouslySetInnerHTML"];
|
|
259
|
+
if (typeof raw !== "object" || raw == null || !("__html" in raw) || typeof raw.__html !== "string") {
|
|
260
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
261
|
+
}
|
|
262
|
+
parts.push(raw.__html);
|
|
245
263
|
} else {
|
|
246
264
|
for (const child of children) renderSync(child, parts);
|
|
247
265
|
}
|
|
@@ -336,6 +354,9 @@ function renderStreamNode(node, parts, state) {
|
|
|
336
354
|
if (resolved === true) {
|
|
337
355
|
parts.push(" ", attrName);
|
|
338
356
|
} else if (resolved !== false && resolved != null) {
|
|
357
|
+
if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === "string" && DANGEROUS_URI_RE.test(resolved)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
339
360
|
parts.push(" ", attrName, '="', escapeAttr(String(resolved)), '"');
|
|
340
361
|
}
|
|
341
362
|
}
|
|
@@ -346,7 +367,11 @@ function renderStreamNode(node, parts, state) {
|
|
|
346
367
|
}
|
|
347
368
|
parts.push(">");
|
|
348
369
|
if (props?.["dangerouslySetInnerHTML"]) {
|
|
349
|
-
|
|
370
|
+
const raw = props["dangerouslySetInnerHTML"];
|
|
371
|
+
if (typeof raw !== "object" || raw == null || !("__html" in raw) || typeof raw.__html !== "string") {
|
|
372
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
373
|
+
}
|
|
374
|
+
parts.push(raw.__html);
|
|
350
375
|
} else {
|
|
351
376
|
for (const child of children) renderStreamNode(child, parts, state);
|
|
352
377
|
}
|
package/dist/ssr/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ssr/render.ts","../../src/ssr/ssr-reactive.ts","../../src/ssr/client-script.ts","../../src/ssr/stream.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// Void elements that don't have closing tags\nexport const VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr',\n]);\n\n// Map of prop names to HTML attribute names\nexport const PROP_TO_ATTR: Record<string, string> = {\n className: 'class',\n htmlFor: 'for',\n tabIndex: 'tabindex',\n};\n\n// Escape HTML entities\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Escape attribute values\nexport function escapeAttr(str: string): string {\n return str.replace(/&/g, '&').replace(/\"/g, '"');\n}\n\nexport interface VNode {\n tag: string;\n props: Record<string, unknown> | null;\n children: unknown[];\n}\n\n/**\n * Server-side hyperscript — creates a virtual node instead of a DOM element.\n */\nexport function sh(tag: string, props?: Record<string, unknown> | null, ...children: unknown[]): VNode {\n return { tag, props: props ?? null, children };\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string.\n *\n * Uses an array buffer internally to avoid O(n^2) string concatenation\n * for large trees, then joins once at the end.\n *\n * Usage:\n * ```ts\n * import { renderToString, sh } from '@getforma/core/ssr';\n *\n * const html = renderToString(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()), // signal getters resolved synchronously\n * )\n * );\n * ```\n */\nexport function renderToString(node: unknown): string {\n const parts: string[] = [];\n renderToBuffer(node, parts);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer.\n * Avoids O(n^2) string concatenation for large trees.\n */\nfunction renderToBuffer(node: unknown, parts: string[]): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter) → resolve and render\n if (typeof node === 'function') { renderToBuffer(node(), parts); return; }\n\n // Array → render each\n if (Array.isArray(node)) {\n for (const child of node) renderToBuffer(child, parts);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip event handlers, refs, and internal props\n if (key.startsWith('on') || key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n const attrName = PROP_TO_ATTR[key] ?? key;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n if (resolved === true) {\n parts.push(' ', attrName);\n } else if (resolved !== false && resolved != null) {\n parts.push(' ', attrName, '=\"', escapeAttr(String(resolved)), '\"');\n }\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else if (typeof console !== 'undefined') {\n console.warn('[FormaJS SSR] dangerouslySetInnerHTML.__html must be a string, got ' + typeof html);\n }\n } else if (typeof console !== 'undefined') {\n console.warn('[FormaJS SSR] dangerouslySetInnerHTML: expected { __html: string }, got ' + typeof raw);\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBuffer(child, parts);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\nexport function isVNode(v: unknown): v is VNode {\n return v != null && typeof v === 'object' && 'tag' in v && 'children' in v;\n}\n\n// ---------------------------------------------------------------------------\n// Hydration-aware rendering\n// ---------------------------------------------------------------------------\n\n/** Per-render hydration context — avoids shared module-level counter. */\ninterface HydrationContext {\n id: number;\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string with hydration markers.\n *\n * Like `renderToString`, but injects comment markers and `data-forma-h`\n * attributes so the client-side `hydrate()` function can adopt existing\n * DOM nodes without re-creating them.\n *\n * Each call creates its own hydration counter, so concurrent calls\n * (e.g. multiple SSR requests in the same process) produce independent,\n * non-overlapping hydration IDs.\n *\n * Marker types:\n * - `data-forma-h=\"N\"` — element boundary (attribute on the element)\n * - `<!--forma-t:N-->` / `<!--/forma-t:N-->` — reactive text boundary\n * - `<!--forma-l:N-->` / `<!--/forma-l:N-->` — list boundary\n *\n * Usage:\n * ```ts\n * import { renderToStringWithHydration, sh } from '@getforma/core/ssr';\n *\n * const html = renderToStringWithHydration(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()),\n * )\n * );\n * ```\n */\nexport function renderToStringWithHydration(node: unknown): string {\n const ctx: HydrationContext = { id: 0 };\n const parts: string[] = [];\n renderToBufferHydrated(node, parts, ctx);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer with hydration markers.\n * The `ctx` object carries the hydration counter so concurrent renders are isolated.\n */\nfunction renderToBufferHydrated(node: unknown, parts: string[], ctx: HydrationContext): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter / reactive text) → wrap with text markers\n if (typeof node === 'function') {\n const id = ctx.id++;\n parts.push(`<!--forma-t:${id}-->`);\n renderToBufferHydrated(node(), parts, ctx);\n parts.push(`<!--/forma-t:${id}-->`);\n return;\n }\n\n // Array → wrap with list markers\n if (Array.isArray(node)) {\n const id = ctx.id++;\n parts.push(`<!--forma-l:${id}-->`);\n for (const child of node) renderToBufferHydrated(child, parts, ctx);\n parts.push(`<!--/forma-l:${id}-->`);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const id = ctx.id++;\n const { tag, props, children } = node;\n\n // Add hydration data-attribute to element\n parts.push('<', tag, ` data-forma-h=\"${id}\"`);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip event handlers, refs, and internal props\n if (key.startsWith('on') || key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n const attrName = PROP_TO_ATTR[key] ?? key;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n if (resolved === true) {\n parts.push(' ', attrName);\n } else if (resolved !== false && resolved != null) {\n parts.push(' ', attrName, '=\"', escapeAttr(String(resolved)), '\"');\n }\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else if (typeof console !== 'undefined') {\n console.warn('[FormaJS SSR] dangerouslySetInnerHTML.__html must be a string, got ' + typeof html);\n }\n } else if (typeof console !== 'undefined') {\n console.warn('[FormaJS SSR] dangerouslySetInnerHTML: expected { __html: string }, got ' + typeof raw);\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBufferHydrated(child, parts, ctx);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\n","/**\n * SSR-only signal: just a value holder, no reactivity needed on server.\n */\nexport function ssrSignal<T>(initial: T): [get: () => T, set: (v: T) => void] {\n let value = initial;\n return [() => value, (v) => { value = v; }];\n}\n\nexport function ssrComputed<T>(fn: () => T): () => T {\n return fn; // On server, just call the function directly\n}\n","/**\n * FormaJS SSR - Client Swap Script\n *\n * Tiny inline script injected into the HTML stream that handles\n * out-of-order Suspense boundary resolution.\n *\n * When the server resolves a Suspense boundary, it sends:\n * <script>$FORMA_SWAP(\"forma-s:0\",\"<resolved html>\")</script>\n *\n * This script replaces the fallback placeholder with the resolved content.\n */\n\n/**\n * Returns the client-side swap script as a string, ready to be injected\n * into the HTML stream (typically right after <body> or before first Suspense).\n */\nexport function getSwapScript(): string {\n return `<script>\nfunction $FORMA_SWAP(id,html){\n var el=document.getElementById(id);\n if(el){\n var tpl=document.createElement('template');\n tpl.innerHTML=html;\n el.replaceWith(tpl.content);\n }\n}\n</script>`;\n}\n\n/**\n * Returns a script tag that swaps a specific Suspense boundary's content.\n * Called when an async resource resolves during streaming.\n */\nexport function getSwapTag(id: string, html: string): string {\n // Use JSON.stringify for both arguments to handle all edge cases:\n // backslashes, quotes, newlines, null bytes, </script>, unicode escapes\n return `<script>$FORMA_SWAP(${JSON.stringify(id)},${JSON.stringify(html)})</script>`;\n}\n","/**\n * FormaJS SSR - Streaming Renderer\n *\n * Implements true chunked HTML streaming with out-of-order Suspense resolution.\n *\n * How it works:\n * 1. Yields shell HTML immediately (opening tags, head, static content)\n * 2. When a Suspense boundary is encountered:\n * - Yields the fallback HTML with a placeholder: <div id=\"forma-s:N\" style=\"display:contents\">\n * - Continues rendering siblings without blocking\n * - When the async resource resolves, yields a swap script\n * 3. Suspense boundaries resolve independently in any order (out-of-order streaming)\n */\n\nimport { getSwapScript, getSwapTag } from './client-script.js';\nimport { escapeHtml, escapeAttr, isVNode, VOID_ELEMENTS, PROP_TO_ATTR, type VNode } from './render.js';\n\n// ---------------------------------------------------------------------------\n// Suspense boundary tracking\n// ---------------------------------------------------------------------------\n\ninterface PendingBoundary {\n id: string;\n promise: Promise<string>;\n}\n\n/** Per-render mutable state, scoped to a single renderToStreamNew call. */\ninterface StreamState {\n suspenseCounter: number;\n pendingBoundaries: PendingBoundary[];\n}\n\n// ---------------------------------------------------------------------------\n// Synchronous render to buffer (for non-async content)\n// ---------------------------------------------------------------------------\n\nfunction renderSync(node: unknown, parts: string[]): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderSync(node(), parts); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderSync(child, parts);\n return;\n }\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key.startsWith('on') || key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const attrName = PROP_TO_ATTR[key] ?? key;\n const resolved = typeof value === 'function' ? value() : value;\n if (resolved === true) {\n parts.push(' ', attrName);\n } else if (resolved !== false && resolved != null) {\n parts.push(' ', attrName, '=\"', escapeAttr(String(resolved)), '\"');\n }\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n parts.push((props['dangerouslySetInnerHTML'] as { __html: string }).__html);\n } else {\n for (const child of children) renderSync(child, parts);\n }\n parts.push('</', tag, '>');\n return;\n }\n parts.push(escapeHtml(String(node)));\n}\n\n// ---------------------------------------------------------------------------\n// Suspense VNode detection\n// ---------------------------------------------------------------------------\n\n/**\n * A Suspense boundary VNode has a special shape:\n * { tag: 'forma-suspense', props: { fallback: VNode }, children: [asyncChildren] }\n *\n * Or it can be created via:\n * shSuspense(fallbackVNode, () => asyncContent)\n */\nexport interface SuspenseVNode {\n tag: 'forma-suspense';\n props: { fallback: unknown };\n children: [() => Promise<unknown>];\n}\n\nfunction isSuspenseVNode(v: unknown): v is SuspenseVNode {\n return isVNode(v) && v.tag === 'forma-suspense';\n}\n\n/**\n * Create a server-side Suspense VNode for streaming SSR.\n */\nexport function shSuspense(fallback: unknown, asyncChildren: () => Promise<unknown>): SuspenseVNode {\n return {\n tag: 'forma-suspense',\n props: { fallback },\n children: [asyncChildren],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Streaming renderer\n// ---------------------------------------------------------------------------\n\nexport interface StreamOptions {\n /** Script to bootstrap the client-side app (e.g. <script src=\"/app.js\"></script>) */\n bootstrapScript?: string;\n /** Whether to inject the $FORMA_SWAP client script (default: true) */\n injectSwapScript?: boolean;\n}\n\n/**\n * Render a FormaJS virtual tree to a stream of HTML chunks.\n *\n * Yields HTML progressively:\n * - Static content is yielded immediately\n * - Suspense boundaries yield their fallback, then resolve later\n * - Resolved Suspense boundaries yield swap scripts\n *\n * Usage:\n * ```ts\n * import { renderToStream, shSuspense, sh } from '@getforma/core/ssr';\n *\n * const stream = renderToStream(\n * sh('html', null,\n * sh('body', null,\n * sh('h1', null, 'Hello'),\n * shSuspense(\n * sh('div', null, 'Loading...'),\n * async () => {\n * const data = await fetchData();\n * return sh('div', null, data.name);\n * },\n * ),\n * ),\n * ),\n * );\n *\n * for await (const chunk of stream) {\n * response.write(chunk);\n * }\n * ```\n */\nexport async function* renderToStreamNew(\n node: unknown,\n options?: StreamOptions,\n): AsyncGenerator<string> {\n // Per-render state — scoped to this generator invocation\n const state: StreamState = {\n suspenseCounter: 0,\n pendingBoundaries: [],\n };\n\n const injectSwap = options?.injectSwapScript !== false;\n\n // Inject the swap script first (if there could be Suspense boundaries)\n if (injectSwap) {\n yield getSwapScript();\n }\n\n // Render the main tree synchronously, collecting Suspense boundaries\n const mainParts: string[] = [];\n renderStreamNode(node, mainParts, state);\n yield mainParts.join('');\n\n // Yield bootstrap script if provided\n if (options?.bootstrapScript) {\n yield options.bootstrapScript;\n }\n\n // Wait for and yield all pending Suspense boundaries\n // Each resolves independently (out-of-order)\n while (state.pendingBoundaries.length > 0) {\n const pending = [...state.pendingBoundaries];\n state.pendingBoundaries = [];\n\n // Race all pending boundaries — yield each as it resolves\n const results = await Promise.allSettled(\n pending.map(async (boundary) => {\n const html = await boundary.promise;\n return { id: boundary.id, html };\n }),\n );\n\n for (const result of results) {\n if (result.status === 'fulfilled') {\n yield getSwapTag(result.value.id, result.value.html);\n }\n // On rejection, the fallback stays visible (no swap)\n }\n }\n}\n\n/**\n * Render a node synchronously. If a Suspense boundary is encountered,\n * render the fallback inline and queue the async resolution.\n */\nfunction renderStreamNode(node: unknown, parts: string[], state: StreamState): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderStreamNode(node(), parts, state); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderStreamNode(child, parts, state);\n return;\n }\n\n // Suspense boundary: render fallback, queue async resolution\n if (isSuspenseVNode(node)) {\n const id = `forma-s:${state.suspenseCounter++}`;\n const fallback = node.props.fallback;\n const asyncFn = node.children[0]!;\n\n // Render fallback inside a replaceable container\n parts.push(`<div id=\"${id}\" style=\"display:contents\">`);\n renderSync(fallback, parts);\n parts.push('</div>');\n\n // Queue the async resolution\n const promise = asyncFn().then((resolved: unknown) => {\n const resolvedParts: string[] = [];\n renderSync(resolved, resolvedParts);\n return resolvedParts.join('');\n });\n state.pendingBoundaries.push({ id, promise });\n return;\n }\n\n // Regular VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'fallback') continue; // skip internal suspense props\n if (key.startsWith('on') || key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const attrName = PROP_TO_ATTR[key] ?? key;\n const resolved = typeof value === 'function' ? value() : value;\n if (resolved === true) {\n parts.push(' ', attrName);\n } else if (resolved !== false && resolved != null) {\n parts.push(' ', attrName, '=\"', escapeAttr(String(resolved)), '\"');\n }\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n parts.push((props['dangerouslySetInnerHTML'] as { __html: string }).__html);\n } else {\n for (const child of children) renderStreamNode(child, parts, state);\n }\n parts.push('</', tag, '>');\n return;\n }\n\n parts.push(escapeHtml(String(node)));\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/ssr/render.ts","../../src/ssr/ssr-reactive.ts","../../src/ssr/client-script.ts","../../src/ssr/stream.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// Void elements that don't have closing tags\nexport const VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr',\n]);\n\n// Map of prop names to HTML attribute names\nexport const PROP_TO_ATTR: Record<string, string> = {\n className: 'class',\n htmlFor: 'for',\n tabIndex: 'tabindex',\n};\n\n// Escape HTML entities\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Escape attribute values\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/'/g, ''');\n}\n\n// Dangerous URI detection for href, src, action, formaction attributes\nexport const DANGEROUS_URI_ATTRS = new Set(['href', 'src', 'action', 'formaction']);\nexport const DANGEROUS_URI_RE = /^\\s*(javascript|vbscript|data\\s*:\\s*text\\/html)/i;\n\nexport interface VNode {\n tag: string;\n props: Record<string, unknown> | null;\n children: unknown[];\n}\n\n/**\n * Server-side hyperscript — creates a virtual node instead of a DOM element.\n */\nexport function sh(tag: string, props?: Record<string, unknown> | null, ...children: unknown[]): VNode {\n return { tag, props: props ?? null, children };\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string.\n *\n * Uses an array buffer internally to avoid O(n^2) string concatenation\n * for large trees, then joins once at the end.\n *\n * Usage:\n * ```ts\n * import { renderToString, sh } from '@getforma/core/ssr';\n *\n * const html = renderToString(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()), // signal getters resolved synchronously\n * )\n * );\n * ```\n */\nexport function renderToString(node: unknown): string {\n const parts: string[] = [];\n renderToBuffer(node, parts);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer.\n * Avoids O(n^2) string concatenation for large trees.\n */\nfunction renderToBuffer(node: unknown, parts: string[]): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter) → resolve and render\n if (typeof node === 'function') { renderToBuffer(node(), parts); return; }\n\n // Array → render each\n if (Array.isArray(node)) {\n for (const child of node) renderToBuffer(child, parts);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip event handlers, refs, and internal props\n if (key.startsWith('on') || key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n const attrName = PROP_TO_ATTR[key] ?? key;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n if (resolved === true) {\n parts.push(' ', attrName);\n } else if (resolved !== false && resolved != null) {\n if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === 'string' && DANGEROUS_URI_RE.test(resolved)) {\n continue; // skip dangerous URI\n }\n parts.push(' ', attrName, '=\"', escapeAttr(String(resolved)), '\"');\n }\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBuffer(child, parts);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\nexport function isVNode(v: unknown): v is VNode {\n return v != null && typeof v === 'object' && 'tag' in v && 'children' in v;\n}\n\n// ---------------------------------------------------------------------------\n// Hydration-aware rendering\n// ---------------------------------------------------------------------------\n\n/** Per-render hydration context — avoids shared module-level counter. */\ninterface HydrationContext {\n id: number;\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string with hydration markers.\n *\n * Like `renderToString`, but injects comment markers and `data-forma-h`\n * attributes so the client-side `hydrate()` function can adopt existing\n * DOM nodes without re-creating them.\n *\n * Each call creates its own hydration counter, so concurrent calls\n * (e.g. multiple SSR requests in the same process) produce independent,\n * non-overlapping hydration IDs.\n *\n * Marker types:\n * - `data-forma-h=\"N\"` — element boundary (attribute on the element)\n * - `<!--forma-t:N-->` / `<!--/forma-t:N-->` — reactive text boundary\n * - `<!--forma-l:N-->` / `<!--/forma-l:N-->` — list boundary\n *\n * Usage:\n * ```ts\n * import { renderToStringWithHydration, sh } from '@getforma/core/ssr';\n *\n * const html = renderToStringWithHydration(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()),\n * )\n * );\n * ```\n */\nexport function renderToStringWithHydration(node: unknown): string {\n const ctx: HydrationContext = { id: 0 };\n const parts: string[] = [];\n renderToBufferHydrated(node, parts, ctx);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer with hydration markers.\n * The `ctx` object carries the hydration counter so concurrent renders are isolated.\n */\nfunction renderToBufferHydrated(node: unknown, parts: string[], ctx: HydrationContext): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter / reactive text) → wrap with text markers\n if (typeof node === 'function') {\n const id = ctx.id++;\n parts.push(`<!--forma-t:${id}-->`);\n renderToBufferHydrated(node(), parts, ctx);\n parts.push(`<!--/forma-t:${id}-->`);\n return;\n }\n\n // Array → wrap with list markers\n if (Array.isArray(node)) {\n const id = ctx.id++;\n parts.push(`<!--forma-l:${id}-->`);\n for (const child of node) renderToBufferHydrated(child, parts, ctx);\n parts.push(`<!--/forma-l:${id}-->`);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const id = ctx.id++;\n const { tag, props, children } = node;\n\n // Add hydration data-attribute to element\n parts.push('<', tag, ` data-forma-h=\"${id}\"`);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip event handlers, refs, and internal props\n if (key.startsWith('on') || key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n const attrName = PROP_TO_ATTR[key] ?? key;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n if (resolved === true) {\n parts.push(' ', attrName);\n } else if (resolved !== false && resolved != null) {\n if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === 'string' && DANGEROUS_URI_RE.test(resolved)) {\n continue; // skip dangerous URI\n }\n parts.push(' ', attrName, '=\"', escapeAttr(String(resolved)), '\"');\n }\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBufferHydrated(child, parts, ctx);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\n","/**\n * SSR-only signal: just a value holder, no reactivity needed on server.\n */\nexport function ssrSignal<T>(initial: T): [get: () => T, set: (v: T) => void] {\n let value = initial;\n return [() => value, (v) => { value = v; }];\n}\n\nexport function ssrComputed<T>(fn: () => T): () => T {\n return fn; // On server, just call the function directly\n}\n","/**\n * FormaJS SSR - Client Swap Script\n *\n * Tiny inline script injected into the HTML stream that handles\n * out-of-order Suspense boundary resolution.\n *\n * When the server resolves a Suspense boundary, it sends:\n * <script>$FORMA_SWAP(\"forma-s:0\",\"<resolved html>\")</script>\n *\n * This script replaces the fallback placeholder with the resolved content.\n */\n\n/**\n * Returns the client-side swap script as a string, ready to be injected\n * into the HTML stream (typically right after <body> or before first Suspense).\n */\nexport function getSwapScript(): string {\n return `<script>\nfunction $FORMA_SWAP(id,html){\n var el=document.getElementById(id);\n if(el){\n var tpl=document.createElement('template');\n tpl.innerHTML=html;\n el.replaceWith(tpl.content);\n }\n}\n</script>`;\n}\n\n/**\n * Serializes a value to a JSON string that is safe to embed inside a\n * `<script>` block. Standard `JSON.stringify` does not escape `<` or `>`,\n * so a payload containing `</script>` would prematurely close the script\n * tag, enabling an injection attack. We also escape U+2028 and U+2029\n * which are valid JSON but treated as line terminators in JavaScript source.\n */\nfunction safeJsonStringify(val: unknown): string {\n return JSON.stringify(val)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/**\n * Returns a script tag that swaps a specific Suspense boundary's content.\n * Called when an async resource resolves during streaming.\n */\nexport function getSwapTag(id: string, html: string): string {\n // safeJsonStringify escapes <, >, U+2028, and U+2029 so the serialized\n // content cannot break out of the surrounding <script> block.\n return `<script>$FORMA_SWAP(${safeJsonStringify(id)},${safeJsonStringify(html)})</script>`;\n}\n","/**\n * FormaJS SSR - Streaming Renderer\n *\n * Implements true chunked HTML streaming with out-of-order Suspense resolution.\n *\n * How it works:\n * 1. Yields shell HTML immediately (opening tags, head, static content)\n * 2. When a Suspense boundary is encountered:\n * - Yields the fallback HTML with a placeholder: <div id=\"forma-s:N\" style=\"display:contents\">\n * - Continues rendering siblings without blocking\n * - When the async resource resolves, yields a swap script\n * 3. Suspense boundaries resolve independently in any order (out-of-order streaming)\n */\n\nimport { getSwapScript, getSwapTag } from './client-script.js';\nimport { escapeHtml, escapeAttr, isVNode, VOID_ELEMENTS, PROP_TO_ATTR, DANGEROUS_URI_ATTRS, DANGEROUS_URI_RE, type VNode } from './render.js';\n\n// ---------------------------------------------------------------------------\n// Suspense boundary tracking\n// ---------------------------------------------------------------------------\n\ninterface PendingBoundary {\n id: string;\n promise: Promise<string>;\n}\n\n/** Per-render mutable state, scoped to a single renderToStreamNew call. */\ninterface StreamState {\n suspenseCounter: number;\n pendingBoundaries: PendingBoundary[];\n}\n\n// ---------------------------------------------------------------------------\n// Synchronous render to buffer (for non-async content)\n// ---------------------------------------------------------------------------\n\nfunction renderSync(node: unknown, parts: string[]): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderSync(node(), parts); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderSync(child, parts);\n return;\n }\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key.startsWith('on') || key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const attrName = PROP_TO_ATTR[key] ?? key;\n const resolved = typeof value === 'function' ? value() : value;\n if (resolved === true) {\n parts.push(' ', attrName);\n } else if (resolved !== false && resolved != null) {\n if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === 'string' && DANGEROUS_URI_RE.test(resolved)) {\n continue; // skip dangerous URI\n }\n parts.push(' ', attrName, '=\"', escapeAttr(String(resolved)), '\"');\n }\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderSync(child, parts);\n }\n parts.push('</', tag, '>');\n return;\n }\n parts.push(escapeHtml(String(node)));\n}\n\n// ---------------------------------------------------------------------------\n// Suspense VNode detection\n// ---------------------------------------------------------------------------\n\n/**\n * A Suspense boundary VNode has a special shape:\n * { tag: 'forma-suspense', props: { fallback: VNode }, children: [asyncChildren] }\n *\n * Or it can be created via:\n * shSuspense(fallbackVNode, () => asyncContent)\n */\nexport interface SuspenseVNode {\n tag: 'forma-suspense';\n props: { fallback: unknown };\n children: [() => Promise<unknown>];\n}\n\nfunction isSuspenseVNode(v: unknown): v is SuspenseVNode {\n return isVNode(v) && v.tag === 'forma-suspense';\n}\n\n/**\n * Create a server-side Suspense VNode for streaming SSR.\n */\nexport function shSuspense(fallback: unknown, asyncChildren: () => Promise<unknown>): SuspenseVNode {\n return {\n tag: 'forma-suspense',\n props: { fallback },\n children: [asyncChildren],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Streaming renderer\n// ---------------------------------------------------------------------------\n\nexport interface StreamOptions {\n /** Script to bootstrap the client-side app (e.g. <script src=\"/app.js\"></script>) */\n bootstrapScript?: string;\n /** Whether to inject the $FORMA_SWAP client script (default: true) */\n injectSwapScript?: boolean;\n}\n\n/**\n * Render a FormaJS virtual tree to a stream of HTML chunks.\n *\n * Yields HTML progressively:\n * - Static content is yielded immediately\n * - Suspense boundaries yield their fallback, then resolve later\n * - Resolved Suspense boundaries yield swap scripts\n *\n * Usage:\n * ```ts\n * import { renderToStream, shSuspense, sh } from '@getforma/core/ssr';\n *\n * const stream = renderToStream(\n * sh('html', null,\n * sh('body', null,\n * sh('h1', null, 'Hello'),\n * shSuspense(\n * sh('div', null, 'Loading...'),\n * async () => {\n * const data = await fetchData();\n * return sh('div', null, data.name);\n * },\n * ),\n * ),\n * ),\n * );\n *\n * for await (const chunk of stream) {\n * response.write(chunk);\n * }\n * ```\n */\nexport async function* renderToStreamNew(\n node: unknown,\n options?: StreamOptions,\n): AsyncGenerator<string> {\n // Per-render state — scoped to this generator invocation\n const state: StreamState = {\n suspenseCounter: 0,\n pendingBoundaries: [],\n };\n\n const injectSwap = options?.injectSwapScript !== false;\n\n // Inject the swap script first (if there could be Suspense boundaries)\n if (injectSwap) {\n yield getSwapScript();\n }\n\n // Render the main tree synchronously, collecting Suspense boundaries\n const mainParts: string[] = [];\n renderStreamNode(node, mainParts, state);\n yield mainParts.join('');\n\n // Yield bootstrap script if provided\n if (options?.bootstrapScript) {\n yield options.bootstrapScript;\n }\n\n // Wait for and yield all pending Suspense boundaries\n // Each resolves independently (out-of-order)\n while (state.pendingBoundaries.length > 0) {\n const pending = [...state.pendingBoundaries];\n state.pendingBoundaries = [];\n\n // Race all pending boundaries — yield each as it resolves\n const results = await Promise.allSettled(\n pending.map(async (boundary) => {\n const html = await boundary.promise;\n return { id: boundary.id, html };\n }),\n );\n\n for (const result of results) {\n if (result.status === 'fulfilled') {\n yield getSwapTag(result.value.id, result.value.html);\n }\n // On rejection, the fallback stays visible (no swap)\n }\n }\n}\n\n/**\n * Render a node synchronously. If a Suspense boundary is encountered,\n * render the fallback inline and queue the async resolution.\n */\nfunction renderStreamNode(node: unknown, parts: string[], state: StreamState): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderStreamNode(node(), parts, state); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderStreamNode(child, parts, state);\n return;\n }\n\n // Suspense boundary: render fallback, queue async resolution\n if (isSuspenseVNode(node)) {\n const id = `forma-s:${state.suspenseCounter++}`;\n const fallback = node.props.fallback;\n const asyncFn = node.children[0]!;\n\n // Render fallback inside a replaceable container\n parts.push(`<div id=\"${id}\" style=\"display:contents\">`);\n renderSync(fallback, parts);\n parts.push('</div>');\n\n // Queue the async resolution\n const promise = asyncFn().then((resolved: unknown) => {\n const resolvedParts: string[] = [];\n renderSync(resolved, resolvedParts);\n return resolvedParts.join('');\n });\n state.pendingBoundaries.push({ id, promise });\n return;\n }\n\n // Regular VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'fallback') continue; // skip internal suspense props\n if (key.startsWith('on') || key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const attrName = PROP_TO_ATTR[key] ?? key;\n const resolved = typeof value === 'function' ? value() : value;\n if (resolved === true) {\n parts.push(' ', attrName);\n } else if (resolved !== false && resolved != null) {\n if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === 'string' && DANGEROUS_URI_RE.test(resolved)) {\n continue; // skip dangerous URI\n }\n parts.push(' ', attrName, '=\"', escapeAttr(String(resolved)), '\"');\n }\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderStreamNode(child, parts, state);\n }\n parts.push('</', tag, '>');\n return;\n }\n\n parts.push(escapeHtml(String(node)));\n}\n"]}
|
package/dist/ssr/index.js
CHANGED
|
@@ -24,8 +24,10 @@ function escapeHtml(str) {
|
|
|
24
24
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
25
25
|
}
|
|
26
26
|
function escapeAttr(str) {
|
|
27
|
-
return str.replace(/&/g, "&").replace(/"/g, """);
|
|
27
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'");
|
|
28
28
|
}
|
|
29
|
+
var DANGEROUS_URI_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction"]);
|
|
30
|
+
var DANGEROUS_URI_RE = /^\s*(javascript|vbscript|data\s*:\s*text\/html)/i;
|
|
29
31
|
function sh(tag, props, ...children) {
|
|
30
32
|
return { tag, props: props ?? null, children };
|
|
31
33
|
}
|
|
@@ -63,6 +65,9 @@ function renderToBuffer(node, parts) {
|
|
|
63
65
|
if (resolved === true) {
|
|
64
66
|
parts.push(" ", attrName);
|
|
65
67
|
} else if (resolved !== false && resolved != null) {
|
|
68
|
+
if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === "string" && DANGEROUS_URI_RE.test(resolved)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
66
71
|
parts.push(" ", attrName, '="', escapeAttr(String(resolved)), '"');
|
|
67
72
|
}
|
|
68
73
|
}
|
|
@@ -78,11 +83,11 @@ function renderToBuffer(node, parts) {
|
|
|
78
83
|
const html = raw.__html;
|
|
79
84
|
if (typeof html === "string") {
|
|
80
85
|
parts.push(html);
|
|
81
|
-
} else
|
|
82
|
-
|
|
86
|
+
} else {
|
|
87
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
83
88
|
}
|
|
84
|
-
} else
|
|
85
|
-
|
|
89
|
+
} else {
|
|
90
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
86
91
|
}
|
|
87
92
|
} else {
|
|
88
93
|
for (const child of children) {
|
|
@@ -139,6 +144,9 @@ function renderToBufferHydrated(node, parts, ctx) {
|
|
|
139
144
|
if (resolved === true) {
|
|
140
145
|
parts.push(" ", attrName);
|
|
141
146
|
} else if (resolved !== false && resolved != null) {
|
|
147
|
+
if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === "string" && DANGEROUS_URI_RE.test(resolved)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
142
150
|
parts.push(" ", attrName, '="', escapeAttr(String(resolved)), '"');
|
|
143
151
|
}
|
|
144
152
|
}
|
|
@@ -154,11 +162,11 @@ function renderToBufferHydrated(node, parts, ctx) {
|
|
|
154
162
|
const html = raw.__html;
|
|
155
163
|
if (typeof html === "string") {
|
|
156
164
|
parts.push(html);
|
|
157
|
-
} else
|
|
158
|
-
|
|
165
|
+
} else {
|
|
166
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
159
167
|
}
|
|
160
|
-
} else
|
|
161
|
-
|
|
168
|
+
} else {
|
|
169
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
162
170
|
}
|
|
163
171
|
} else {
|
|
164
172
|
for (const child of children) {
|
|
@@ -195,8 +203,11 @@ function $FORMA_SWAP(id,html){
|
|
|
195
203
|
}
|
|
196
204
|
</script>`;
|
|
197
205
|
}
|
|
206
|
+
function safeJsonStringify(val) {
|
|
207
|
+
return JSON.stringify(val).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
208
|
+
}
|
|
198
209
|
function getSwapTag(id, html) {
|
|
199
|
-
return `<script>$FORMA_SWAP(${
|
|
210
|
+
return `<script>$FORMA_SWAP(${safeJsonStringify(id)},${safeJsonStringify(html)})</script>`;
|
|
200
211
|
}
|
|
201
212
|
|
|
202
213
|
// src/ssr/stream.ts
|
|
@@ -229,6 +240,9 @@ function renderSync(node, parts) {
|
|
|
229
240
|
if (resolved === true) {
|
|
230
241
|
parts.push(" ", attrName);
|
|
231
242
|
} else if (resolved !== false && resolved != null) {
|
|
243
|
+
if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === "string" && DANGEROUS_URI_RE.test(resolved)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
232
246
|
parts.push(" ", attrName, '="', escapeAttr(String(resolved)), '"');
|
|
233
247
|
}
|
|
234
248
|
}
|
|
@@ -239,7 +253,11 @@ function renderSync(node, parts) {
|
|
|
239
253
|
}
|
|
240
254
|
parts.push(">");
|
|
241
255
|
if (props?.["dangerouslySetInnerHTML"]) {
|
|
242
|
-
|
|
256
|
+
const raw = props["dangerouslySetInnerHTML"];
|
|
257
|
+
if (typeof raw !== "object" || raw == null || !("__html" in raw) || typeof raw.__html !== "string") {
|
|
258
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
259
|
+
}
|
|
260
|
+
parts.push(raw.__html);
|
|
243
261
|
} else {
|
|
244
262
|
for (const child of children) renderSync(child, parts);
|
|
245
263
|
}
|
|
@@ -334,6 +352,9 @@ function renderStreamNode(node, parts, state) {
|
|
|
334
352
|
if (resolved === true) {
|
|
335
353
|
parts.push(" ", attrName);
|
|
336
354
|
} else if (resolved !== false && resolved != null) {
|
|
355
|
+
if (DANGEROUS_URI_ATTRS.has(attrName) && typeof resolved === "string" && DANGEROUS_URI_RE.test(resolved)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
337
358
|
parts.push(" ", attrName, '="', escapeAttr(String(resolved)), '"');
|
|
338
359
|
}
|
|
339
360
|
}
|
|
@@ -344,7 +365,11 @@ function renderStreamNode(node, parts, state) {
|
|
|
344
365
|
}
|
|
345
366
|
parts.push(">");
|
|
346
367
|
if (props?.["dangerouslySetInnerHTML"]) {
|
|
347
|
-
|
|
368
|
+
const raw = props["dangerouslySetInnerHTML"];
|
|
369
|
+
if (typeof raw !== "object" || raw == null || !("__html" in raw) || typeof raw.__html !== "string") {
|
|
370
|
+
throw new TypeError("dangerouslySetInnerHTML must be { __html: string }");
|
|
371
|
+
}
|
|
372
|
+
parts.push(raw.__html);
|
|
348
373
|
} else {
|
|
349
374
|
for (const child of children) renderStreamNode(child, parts, state);
|
|
350
375
|
}
|