@pyreon/runtime-server 0.5.6 → 0.6.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/lib/types/index.d.ts +30 -303
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +3 -3
package/lib/types/index.d.ts
CHANGED
|
@@ -1,310 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ForSymbol, Fragment, Suspense, normalizeStyleValue, runWithHooks, setContextStackProvider } from "@pyreon/core";
|
|
1
|
+
import { VNode } from "@pyreon/core";
|
|
3
2
|
|
|
4
|
-
//#region src/index.ts
|
|
3
|
+
//#region src/index.d.ts
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
* API:
|
|
16
|
-
* renderToString(vnode) → Promise<string>
|
|
17
|
-
* renderToStream(vnode) → ReadableStream<string>
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Wire up per-request store isolation.
|
|
22
|
-
* Call once at server startup, passing a `setStoreRegistryProvider` function.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* import { configureStoreIsolation } from "@pyreon/runtime-server"
|
|
26
|
-
* configureStoreIsolation(setStoreRegistryProvider)
|
|
27
|
-
*/
|
|
28
|
-
function configureStoreIsolation(setStoreRegistryProvider) {
|
|
29
|
-
setStoreRegistryProvider(() => _storeAls.getStore() ?? /* @__PURE__ */new Map());
|
|
30
|
-
_storeIsolationActive = true;
|
|
31
|
-
}
|
|
32
|
-
/** Wrap a function call in a fresh store registry (no-op if isolation not configured). */
|
|
33
|
-
function withStoreContext(fn) {
|
|
34
|
-
if (!_storeIsolationActive) return fn();
|
|
35
|
-
return _storeAls.run(/* @__PURE__ */new Map(), fn);
|
|
36
|
-
}
|
|
5
|
+
* Wire up per-request store isolation.
|
|
6
|
+
* Call once at server startup, passing a `setStoreRegistryProvider` function.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { configureStoreIsolation } from "@pyreon/runtime-server"
|
|
10
|
+
* configureStoreIsolation(setStoreRegistryProvider)
|
|
11
|
+
*/
|
|
12
|
+
declare function configureStoreIsolation(setStoreRegistryProvider: (fn: () => Map<string, unknown>) => void): void;
|
|
37
13
|
/** Render a VNode tree to an HTML string. Supports async component functions. */
|
|
38
|
-
|
|
39
|
-
if (root === null) return "";
|
|
40
|
-
return withStoreContext(() => _contextAls.run([], () => renderNode(root)));
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Run an async function with a fresh, isolated context stack and store registry.
|
|
44
|
-
* Useful when you need to call Pyreon APIs (e.g. useHead, prefetchLoaderData)
|
|
45
|
-
* outside of renderToString but still want per-request isolation.
|
|
46
|
-
*/
|
|
47
|
-
function runWithRequestContext(fn) {
|
|
48
|
-
return withStoreContext(() => _contextAls.run([], fn));
|
|
49
|
-
}
|
|
14
|
+
declare function renderToString(root: VNode | null): Promise<string>;
|
|
50
15
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
* Suspense boundaries are streamed out-of-order: the fallback is emitted
|
|
58
|
-
* immediately, and the resolved children are sent as a `<template>` + inline
|
|
59
|
-
* swap script once ready — without blocking the rest of the page.
|
|
60
|
-
*
|
|
61
|
-
* Each renderToStream call gets its own isolated ALS context stack.
|
|
62
|
-
*/
|
|
63
|
-
function renderToStream(root) {
|
|
64
|
-
return new ReadableStream({
|
|
65
|
-
start(controller) {
|
|
66
|
-
const enqueue = chunk => controller.enqueue(chunk);
|
|
67
|
-
let bid = 0;
|
|
68
|
-
const ctx = {
|
|
69
|
-
pending: [],
|
|
70
|
-
nextId: () => bid++,
|
|
71
|
-
mainEnqueue: enqueue
|
|
72
|
-
};
|
|
73
|
-
return withStoreContext(() => _contextAls.run([], () => _streamCtxAls.run(ctx, async () => {
|
|
74
|
-
await streamNode(root, enqueue);
|
|
75
|
-
while (ctx.pending.length > 0) await Promise.all(ctx.pending.splice(0));
|
|
76
|
-
controller.close();
|
|
77
|
-
}).catch(err => controller.error(err))));
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
async function streamVNode(vnode, enqueue) {
|
|
82
|
-
if (vnode.type === Fragment) {
|
|
83
|
-
for (const child of vnode.children) await streamNode(child, enqueue);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (vnode.type === ForSymbol) {
|
|
87
|
-
const {
|
|
88
|
-
each,
|
|
89
|
-
children
|
|
90
|
-
} = vnode.props;
|
|
91
|
-
enqueue("<!--pyreon-for-->");
|
|
92
|
-
for (const item of each()) await streamNode(children(item), enqueue);
|
|
93
|
-
enqueue("<!--/pyreon-for-->");
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (typeof vnode.type === "function") {
|
|
97
|
-
await streamComponentNode(vnode, enqueue);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
await streamElementNode(vnode, enqueue);
|
|
101
|
-
}
|
|
102
|
-
async function streamComponentNode(vnode, enqueue) {
|
|
103
|
-
if (vnode.type === Suspense) {
|
|
104
|
-
await streamSuspenseBoundary(vnode, enqueue);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const {
|
|
108
|
-
vnode: output
|
|
109
|
-
} = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode));
|
|
110
|
-
const resolved = output instanceof Promise ? await output : output;
|
|
111
|
-
if (resolved !== null) await streamNode(resolved, enqueue);
|
|
112
|
-
}
|
|
113
|
-
async function streamElementNode(vnode, enqueue) {
|
|
114
|
-
const tag = vnode.type;
|
|
115
|
-
let open = `<${tag}`;
|
|
116
|
-
const props = vnode.props;
|
|
117
|
-
for (const key in props) {
|
|
118
|
-
const attr = renderProp(key, props[key]);
|
|
119
|
-
if (attr) open += ` ${attr}`;
|
|
120
|
-
}
|
|
121
|
-
if (isVoidElement(tag)) {
|
|
122
|
-
enqueue(`${open} />`);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
enqueue(`${open}>`);
|
|
126
|
-
for (const child of vnode.children) await streamNode(child, enqueue);
|
|
127
|
-
enqueue(`</${tag}>`);
|
|
128
|
-
}
|
|
129
|
-
async function streamNode(node, enqueue) {
|
|
130
|
-
if (typeof node === "function") return streamNode(node(), enqueue);
|
|
131
|
-
if (node == null || node === false) return;
|
|
132
|
-
if (typeof node === "string") {
|
|
133
|
-
enqueue(escapeHtml(node));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (typeof node === "number" || typeof node === "boolean") {
|
|
137
|
-
enqueue(String(node));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (Array.isArray(node)) {
|
|
141
|
-
for (const child of node) await streamNode(child, enqueue);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
await streamVNode(node, enqueue);
|
|
145
|
-
}
|
|
16
|
+
* Run an async function with a fresh, isolated context stack and store registry.
|
|
17
|
+
* Useful when you need to call Pyreon APIs (e.g. useHead, prefetchLoaderData)
|
|
18
|
+
* outside of renderToString but still want per-request isolation.
|
|
19
|
+
*/
|
|
20
|
+
declare function runWithRequestContext<T>(fn: () => Promise<T>): Promise<T>;
|
|
146
21
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const {
|
|
161
|
-
vnode: output
|
|
162
|
-
} = runWithHooks(Suspense, vnode.props);
|
|
163
|
-
if (output !== null) await streamNode(output, enqueue);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const id = ctx.nextId();
|
|
167
|
-
const {
|
|
168
|
-
mainEnqueue
|
|
169
|
-
} = ctx;
|
|
170
|
-
if (id === 0) mainEnqueue(SUSPENSE_SWAP_FN);
|
|
171
|
-
mainEnqueue(`<div id="pyreon-s-${id}">`);
|
|
172
|
-
await streamNode(fallback ?? null, enqueue);
|
|
173
|
-
mainEnqueue("</div>");
|
|
174
|
-
const ctxStore = _contextAls.getStore() ?? [];
|
|
175
|
-
ctx.pending.push(_contextAls.run(ctxStore, async () => {
|
|
176
|
-
try {
|
|
177
|
-
const buf = [];
|
|
178
|
-
await streamNode(children ?? null, s => buf.push(s));
|
|
179
|
-
mainEnqueue(`<template id="pyreon-t-${id}">${buf.join("")}</template>`);
|
|
180
|
-
mainEnqueue(`<script>__NS("pyreon-s-${id}","pyreon-t-${id}")<\/script>`);
|
|
181
|
-
} catch (_err) {}
|
|
182
|
-
}));
|
|
183
|
-
}
|
|
184
|
-
async function renderNode(node) {
|
|
185
|
-
if (typeof node === "function") return renderNode(node());
|
|
186
|
-
if (node == null || node === false) return "";
|
|
187
|
-
if (typeof node === "string") return escapeHtml(node);
|
|
188
|
-
if (typeof node === "number" || typeof node === "boolean") return String(node);
|
|
189
|
-
if (Array.isArray(node)) {
|
|
190
|
-
let html = "";
|
|
191
|
-
for (const child of node) html += await renderNode(child);
|
|
192
|
-
return html;
|
|
193
|
-
}
|
|
194
|
-
const vnode = node;
|
|
195
|
-
if (vnode.type === Fragment) return renderChildren(vnode.children);
|
|
196
|
-
if (vnode.type === ForSymbol) {
|
|
197
|
-
const {
|
|
198
|
-
each,
|
|
199
|
-
children
|
|
200
|
-
} = vnode.props;
|
|
201
|
-
let forHtml = "<!--pyreon-for-->";
|
|
202
|
-
for (const item of each()) forHtml += await renderNode(children(item));
|
|
203
|
-
forHtml += "<!--/pyreon-for-->";
|
|
204
|
-
return forHtml;
|
|
205
|
-
}
|
|
206
|
-
if (typeof vnode.type === "function") return renderComponent(vnode);
|
|
207
|
-
return renderElement(vnode);
|
|
208
|
-
}
|
|
209
|
-
async function renderChildren(children) {
|
|
210
|
-
let html = "";
|
|
211
|
-
for (const child of children) html += await renderNode(child);
|
|
212
|
-
return html;
|
|
213
|
-
}
|
|
214
|
-
async function renderComponent(vnode) {
|
|
215
|
-
const {
|
|
216
|
-
vnode: output
|
|
217
|
-
} = runWithHooks(vnode.type, mergeChildrenIntoProps(vnode));
|
|
218
|
-
if (output instanceof Promise) {
|
|
219
|
-
const resolved = await output;
|
|
220
|
-
if (resolved === null) return "";
|
|
221
|
-
return renderNode(resolved);
|
|
222
|
-
}
|
|
223
|
-
if (output === null) return "";
|
|
224
|
-
return renderNode(output);
|
|
225
|
-
}
|
|
226
|
-
async function renderElement(vnode) {
|
|
227
|
-
const tag = vnode.type;
|
|
228
|
-
let html = `<${tag}`;
|
|
229
|
-
const props = vnode.props;
|
|
230
|
-
for (const key in props) {
|
|
231
|
-
const attr = renderProp(key, props[key]);
|
|
232
|
-
if (attr) html += ` ${attr}`;
|
|
233
|
-
}
|
|
234
|
-
if (isVoidElement(tag)) {
|
|
235
|
-
html += " />";
|
|
236
|
-
return html;
|
|
237
|
-
}
|
|
238
|
-
html += ">";
|
|
239
|
-
for (const child of vnode.children) html += await renderNode(child);
|
|
240
|
-
html += `</${tag}>`;
|
|
241
|
-
return html;
|
|
242
|
-
}
|
|
243
|
-
function renderPropSkipped(key) {
|
|
244
|
-
if (key === "key" || key === "ref" || key === "n-show") return true;
|
|
245
|
-
if (key.startsWith("n-")) return true;
|
|
246
|
-
if (/^on[A-Z]/.test(key)) return true;
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
function renderPropValue(key, value) {
|
|
250
|
-
if (value === null || value === void 0 || value === false) return null;
|
|
251
|
-
if (value === true) return escapeHtml(toAttrName(key));
|
|
252
|
-
if (key === "class") {
|
|
253
|
-
const cls = normalizeClass(value);
|
|
254
|
-
return cls ? `class="${escapeHtml(cls)}"` : null;
|
|
255
|
-
}
|
|
256
|
-
if (key === "style") {
|
|
257
|
-
const style = normalizeStyle(value);
|
|
258
|
-
return style ? `style="${escapeHtml(style)}"` : null;
|
|
259
|
-
}
|
|
260
|
-
return `${escapeHtml(toAttrName(key))}="${escapeHtml(String(value))}"`;
|
|
261
|
-
}
|
|
262
|
-
function renderProp(key, value) {
|
|
263
|
-
if (renderPropSkipped(key)) return null;
|
|
264
|
-
if (typeof value === "function") return renderProp(key, value());
|
|
265
|
-
if (SSR_URL_ATTRS.has(key) && typeof value === "string" && SSR_UNSAFE_URL_RE.test(value)) return null;
|
|
266
|
-
return renderPropValue(key, value);
|
|
267
|
-
}
|
|
268
|
-
function isVoidElement(tag) {
|
|
269
|
-
return VOID_ELEMENTS.has(tag.toLowerCase());
|
|
270
|
-
}
|
|
271
|
-
/** camelCase prop → kebab-case HTML attribute (e.g. className → class, htmlFor → for) */
|
|
272
|
-
function toAttrName(key) {
|
|
273
|
-
if (key === "className") return "class";
|
|
274
|
-
if (key === "htmlFor") return "for";
|
|
275
|
-
return key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`);
|
|
276
|
-
}
|
|
277
|
-
function normalizeClass(value) {
|
|
278
|
-
if (typeof value === "string") return value;
|
|
279
|
-
if (Array.isArray(value)) return value.filter(Boolean).join(" ");
|
|
280
|
-
if (typeof value === "object" && value !== null) return Object.entries(value).filter(([, v]) => v).map(([k]) => k).join(" ");
|
|
281
|
-
return "";
|
|
282
|
-
}
|
|
283
|
-
function normalizeStyle(value) {
|
|
284
|
-
if (typeof value === "string") return value;
|
|
285
|
-
if (typeof value === "object" && value !== null) return Object.entries(value).map(([k, v]) => `${toKebab(k)}: ${normalizeStyleValue(k, v)}`).join("; ");
|
|
286
|
-
return "";
|
|
287
|
-
}
|
|
288
|
-
function toKebab(str) {
|
|
289
|
-
return str.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`);
|
|
290
|
-
}
|
|
291
|
-
function escapeHtml(str) {
|
|
292
|
-
if (!NEEDS_ESCAPE_RE.test(str)) return str;
|
|
293
|
-
return str.replace(/[&<>"']/g, c => ESCAPE_MAP[c] ?? c);
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Merge vnode.children into props.children for component rendering.
|
|
297
|
-
* Matches the behavior of mount.ts and hydrate.ts so components can
|
|
298
|
-
* access children passed via h(Comp, props, child1, child2).
|
|
299
|
-
*/
|
|
300
|
-
function mergeChildrenIntoProps(vnode) {
|
|
301
|
-
if (vnode.children.length > 0 && vnode.props.children === void 0) return {
|
|
302
|
-
...vnode.props,
|
|
303
|
-
children: vnode.children.length === 1 ? vnode.children[0] : vnode.children
|
|
304
|
-
};
|
|
305
|
-
return vnode.props;
|
|
306
|
-
}
|
|
307
|
-
|
|
22
|
+
* Render a VNode tree to a Web-standard ReadableStream of HTML chunks.
|
|
23
|
+
*
|
|
24
|
+
* True progressive streaming: HTML is flushed to the client as soon as each
|
|
25
|
+
* node is ready. Synchronous subtrees are enqueued immediately; async component
|
|
26
|
+
* boundaries are awaited in-order and their output is enqueued as it resolves.
|
|
27
|
+
*
|
|
28
|
+
* Suspense boundaries are streamed out-of-order: the fallback is emitted
|
|
29
|
+
* immediately, and the resolved children are sent as a `<template>` + inline
|
|
30
|
+
* swap script once ready — without blocking the rest of the page.
|
|
31
|
+
*
|
|
32
|
+
* Each renderToStream call gets its own isolated ALS context stack.
|
|
33
|
+
*/
|
|
34
|
+
declare function renderToStream(root: VNode | null): ReadableStream<string>;
|
|
308
35
|
//#endregion
|
|
309
36
|
export { configureStoreIsolation, renderToStream, renderToString, runWithRequestContext };
|
|
310
|
-
//# sourceMappingURL=
|
|
37
|
+
//# sourceMappingURL=index2.d.ts.map
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;;;;;;;AAwFA;;iBA3BgB,uBAAA,CACd,wBAAA,GAA2B,EAAA,QAAU,GAAA;;iBAejB,cAAA,CAAe,IAAA,EAAM,KAAA,UAAe,OAAA;;;;;;iBAW1C,qBAAA,GAAA,CAAyB,EAAA,QAAU,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;;;;;;;AAiBxE;;;;;;;iBAAgB,cAAA,CAAe,IAAA,EAAM,KAAA,UAAe,cAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/runtime-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "SSR/SSG renderer for Pyreon — streaming HTML + static generation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"prepublishOnly": "bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@pyreon/core": "^0.
|
|
43
|
-
"@pyreon/reactivity": "^0.
|
|
42
|
+
"@pyreon/core": "^0.6.0",
|
|
43
|
+
"@pyreon/reactivity": "^0.6.0"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|