hadars 0.1.32 → 0.1.33
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/dist/cli.js +62 -67
- package/dist/slim-react/index.cjs +13 -6
- package/dist/slim-react/index.js +13 -6
- package/dist/ssr-render-worker.js +29 -22
- package/package.json +1 -1
- package/src/build.ts +7 -18
- package/src/slim-react/render.ts +14 -18
- package/src/ssr-render-worker.ts +21 -25
- package/src/utils/response.tsx +7 -11
package/dist/cli.js
CHANGED
|
@@ -387,17 +387,23 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
387
387
|
"track",
|
|
388
388
|
"wbr"
|
|
389
389
|
]);
|
|
390
|
+
var HTML_ESC = { "&": "&", "<": "<", ">": ">", "'": "'" };
|
|
390
391
|
function escapeHtml(str) {
|
|
391
|
-
return str.replace(
|
|
392
|
+
return str.replace(/[&<>']/g, (c) => HTML_ESC[c]);
|
|
392
393
|
}
|
|
394
|
+
var ATTR_ESC = { "&": "&", '"': """, "<": "<", ">": ">" };
|
|
393
395
|
function escapeAttr(str) {
|
|
394
|
-
return str.replace(
|
|
396
|
+
return str.replace(/[&"<>]/g, (c) => ATTR_ESC[c]);
|
|
395
397
|
}
|
|
396
398
|
function styleObjectToString(style) {
|
|
397
|
-
|
|
399
|
+
let result = "";
|
|
400
|
+
for (const key in style) {
|
|
401
|
+
if (result)
|
|
402
|
+
result += ";";
|
|
398
403
|
const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
399
|
-
|
|
400
|
-
}
|
|
404
|
+
result += cssKey + ":" + style[key];
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
401
407
|
}
|
|
402
408
|
var SVG_ATTR_MAP = {
|
|
403
409
|
// Presentation / geometry
|
|
@@ -532,7 +538,8 @@ var SVG_ATTR_MAP = {
|
|
|
532
538
|
};
|
|
533
539
|
function renderAttributes(props, isSvg) {
|
|
534
540
|
let attrs = "";
|
|
535
|
-
for (const
|
|
541
|
+
for (const key in props) {
|
|
542
|
+
const value = props[key];
|
|
536
543
|
if (key === "children" || key === "key" || key === "ref" || key === "dangerouslySetInnerHTML" || key === "suppressHydrationWarning" || key === "suppressContentEditableWarning")
|
|
537
544
|
continue;
|
|
538
545
|
if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
|
|
@@ -996,6 +1003,45 @@ async function renderToString(element, options) {
|
|
|
996
1003
|
}
|
|
997
1004
|
}
|
|
998
1005
|
|
|
1006
|
+
// src/utils/segmentCache.ts
|
|
1007
|
+
function getStore() {
|
|
1008
|
+
const g = globalThis;
|
|
1009
|
+
if (!g.__hadarsSegmentStore) {
|
|
1010
|
+
g.__hadarsSegmentStore = /* @__PURE__ */ new Map();
|
|
1011
|
+
}
|
|
1012
|
+
return g.__hadarsSegmentStore;
|
|
1013
|
+
}
|
|
1014
|
+
function setSegment(key, html, ttl) {
|
|
1015
|
+
getStore().set(key, {
|
|
1016
|
+
html,
|
|
1017
|
+
expiresAt: ttl != null ? Date.now() + ttl : null
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
function processSegmentCache(html) {
|
|
1021
|
+
let prev;
|
|
1022
|
+
do {
|
|
1023
|
+
prev = html;
|
|
1024
|
+
html = html.replace(
|
|
1025
|
+
/<hadars-c([^>]*)>([\s\S]*?)<\/hadars-c>/g,
|
|
1026
|
+
(match, attrs, content) => {
|
|
1027
|
+
const cacheM = /data-cache="([^"]+)"/.exec(attrs);
|
|
1028
|
+
const keyM = /data-key="([^"]+)"/.exec(attrs);
|
|
1029
|
+
const ttlM = /data-ttl="(\d+)"/.exec(attrs);
|
|
1030
|
+
if (!cacheM || !keyM)
|
|
1031
|
+
return match;
|
|
1032
|
+
if (cacheM[1] === "miss") {
|
|
1033
|
+
setSegment(keyM[1], content, ttlM ? Number(ttlM[1]) : void 0);
|
|
1034
|
+
return content;
|
|
1035
|
+
}
|
|
1036
|
+
if (cacheM[1] === "hit")
|
|
1037
|
+
return content;
|
|
1038
|
+
return match;
|
|
1039
|
+
}
|
|
1040
|
+
);
|
|
1041
|
+
} while (html !== prev);
|
|
1042
|
+
return html;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
999
1045
|
// src/utils/response.tsx
|
|
1000
1046
|
var ESC = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
1001
1047
|
var escAttr = (s2) => s2.replace(/[&<>"]/g, (c) => ESC[c] ?? c);
|
|
@@ -1053,11 +1099,12 @@ var getReactResponse = async (req, opts) => {
|
|
|
1053
1099
|
};
|
|
1054
1100
|
const unsuspend = { cache: /* @__PURE__ */ new Map() };
|
|
1055
1101
|
globalThis.__hadarsUnsuspend = unsuspend;
|
|
1102
|
+
let bodyHtml;
|
|
1056
1103
|
try {
|
|
1057
|
-
|
|
1104
|
+
bodyHtml = await renderToString(createElement(App, props));
|
|
1058
1105
|
if (getAfterRenderProps) {
|
|
1059
|
-
props = await getAfterRenderProps(props,
|
|
1060
|
-
await renderToString(
|
|
1106
|
+
props = await getAfterRenderProps(props, bodyHtml);
|
|
1107
|
+
bodyHtml = await renderToString(
|
|
1061
1108
|
createElement(App, { ...props, location: req.location, context })
|
|
1062
1109
|
);
|
|
1063
1110
|
}
|
|
@@ -1075,12 +1122,9 @@ var getReactResponse = async (req, opts) => {
|
|
|
1075
1122
|
location: req.location,
|
|
1076
1123
|
...Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}
|
|
1077
1124
|
};
|
|
1078
|
-
const appProps = { ...props, location: req.location, context };
|
|
1079
1125
|
return {
|
|
1080
|
-
|
|
1081
|
-
appProps,
|
|
1126
|
+
bodyHtml: processSegmentCache(bodyHtml),
|
|
1082
1127
|
clientProps,
|
|
1083
|
-
unsuspend,
|
|
1084
1128
|
status: context.head.status,
|
|
1085
1129
|
headHtml: getHeadHtml(context.head)
|
|
1086
1130
|
};
|
|
@@ -1631,47 +1675,6 @@ import { existsSync as existsSync2 } from "node:fs";
|
|
|
1631
1675
|
import os from "node:os";
|
|
1632
1676
|
import { spawn } from "node:child_process";
|
|
1633
1677
|
import cluster from "node:cluster";
|
|
1634
|
-
|
|
1635
|
-
// src/utils/segmentCache.ts
|
|
1636
|
-
function getStore() {
|
|
1637
|
-
const g = globalThis;
|
|
1638
|
-
if (!g.__hadarsSegmentStore) {
|
|
1639
|
-
g.__hadarsSegmentStore = /* @__PURE__ */ new Map();
|
|
1640
|
-
}
|
|
1641
|
-
return g.__hadarsSegmentStore;
|
|
1642
|
-
}
|
|
1643
|
-
function setSegment(key, html, ttl) {
|
|
1644
|
-
getStore().set(key, {
|
|
1645
|
-
html,
|
|
1646
|
-
expiresAt: ttl != null ? Date.now() + ttl : null
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
function processSegmentCache(html) {
|
|
1650
|
-
let prev;
|
|
1651
|
-
do {
|
|
1652
|
-
prev = html;
|
|
1653
|
-
html = html.replace(
|
|
1654
|
-
/<hadars-c([^>]*)>([\s\S]*?)<\/hadars-c>/g,
|
|
1655
|
-
(match, attrs, content) => {
|
|
1656
|
-
const cacheM = /data-cache="([^"]+)"/.exec(attrs);
|
|
1657
|
-
const keyM = /data-key="([^"]+)"/.exec(attrs);
|
|
1658
|
-
const ttlM = /data-ttl="(\d+)"/.exec(attrs);
|
|
1659
|
-
if (!cacheM || !keyM)
|
|
1660
|
-
return match;
|
|
1661
|
-
if (cacheM[1] === "miss") {
|
|
1662
|
-
setSegment(keyM[1], content, ttlM ? Number(ttlM[1]) : void 0);
|
|
1663
|
-
return content;
|
|
1664
|
-
}
|
|
1665
|
-
if (cacheM[1] === "hit")
|
|
1666
|
-
return content;
|
|
1667
|
-
return match;
|
|
1668
|
-
}
|
|
1669
|
-
);
|
|
1670
|
-
} while (html !== prev);
|
|
1671
|
-
return html;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
// src/build.ts
|
|
1675
1678
|
var encoder = new TextEncoder();
|
|
1676
1679
|
async function processHtmlTemplate(templatePath) {
|
|
1677
1680
|
const html = await fs.readFile(templatePath, "utf-8");
|
|
@@ -1816,19 +1819,11 @@ var RenderWorkerPool = class {
|
|
|
1816
1819
|
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
1817
1820
|
}
|
|
1818
1821
|
};
|
|
1819
|
-
async function buildSsrResponse(
|
|
1822
|
+
async function buildSsrResponse(bodyHtml, clientProps, headHtml, status, getPrecontentHtml) {
|
|
1820
1823
|
const responseStream = new ReadableStream({
|
|
1821
1824
|
async start(controller) {
|
|
1822
1825
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
1823
1826
|
controller.enqueue(encoder.encode(precontentHtml));
|
|
1824
|
-
let bodyHtml;
|
|
1825
|
-
try {
|
|
1826
|
-
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
1827
|
-
bodyHtml = await renderToString(createElement(App, appProps));
|
|
1828
|
-
} finally {
|
|
1829
|
-
globalThis.__hadarsUnsuspend = null;
|
|
1830
|
-
}
|
|
1831
|
-
bodyHtml = processSegmentCache(bodyHtml);
|
|
1832
1827
|
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c");
|
|
1833
1828
|
controller.enqueue(encoder.encode(
|
|
1834
1829
|
`<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent
|
|
@@ -2202,7 +2197,7 @@ var dev = async (options) => {
|
|
|
2202
2197
|
getAfterRenderProps,
|
|
2203
2198
|
getFinalProps
|
|
2204
2199
|
} = await import(importPath);
|
|
2205
|
-
const {
|
|
2200
|
+
const { bodyHtml, clientProps, status, headHtml } = await getReactResponse(request, {
|
|
2206
2201
|
document: {
|
|
2207
2202
|
body: Component,
|
|
2208
2203
|
lang: "en",
|
|
@@ -2211,7 +2206,7 @@ var dev = async (options) => {
|
|
|
2211
2206
|
getFinalProps
|
|
2212
2207
|
}
|
|
2213
2208
|
});
|
|
2214
|
-
return buildSsrResponse(
|
|
2209
|
+
return buildSsrResponse(bodyHtml, clientProps, headHtml, status, getPrecontentHtml);
|
|
2215
2210
|
} catch (err) {
|
|
2216
2211
|
console.error("[hadars] SSR render error:", err);
|
|
2217
2212
|
const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, "<");
|
|
@@ -2356,7 +2351,7 @@ var run = async (options) => {
|
|
|
2356
2351
|
status: wStatus
|
|
2357
2352
|
});
|
|
2358
2353
|
}
|
|
2359
|
-
const {
|
|
2354
|
+
const { bodyHtml, clientProps, status, headHtml } = await getReactResponse(request, {
|
|
2360
2355
|
document: {
|
|
2361
2356
|
body: Component,
|
|
2362
2357
|
lang: "en",
|
|
@@ -2365,7 +2360,7 @@ var run = async (options) => {
|
|
|
2365
2360
|
getFinalProps
|
|
2366
2361
|
}
|
|
2367
2362
|
});
|
|
2368
|
-
return buildSsrResponse(
|
|
2363
|
+
return buildSsrResponse(bodyHtml, clientProps, headHtml, status, getPrecontentHtml);
|
|
2369
2364
|
} catch (err) {
|
|
2370
2365
|
console.error("[hadars] SSR render error:", err);
|
|
2371
2366
|
return new Response("Internal Server Error", { status: 500 });
|
|
@@ -370,17 +370,23 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
370
370
|
"track",
|
|
371
371
|
"wbr"
|
|
372
372
|
]);
|
|
373
|
+
var HTML_ESC = { "&": "&", "<": "<", ">": ">", "'": "'" };
|
|
373
374
|
function escapeHtml(str) {
|
|
374
|
-
return str.replace(
|
|
375
|
+
return str.replace(/[&<>']/g, (c) => HTML_ESC[c]);
|
|
375
376
|
}
|
|
377
|
+
var ATTR_ESC = { "&": "&", '"': """, "<": "<", ">": ">" };
|
|
376
378
|
function escapeAttr(str) {
|
|
377
|
-
return str.replace(
|
|
379
|
+
return str.replace(/[&"<>]/g, (c) => ATTR_ESC[c]);
|
|
378
380
|
}
|
|
379
381
|
function styleObjectToString(style) {
|
|
380
|
-
|
|
382
|
+
let result = "";
|
|
383
|
+
for (const key in style) {
|
|
384
|
+
if (result)
|
|
385
|
+
result += ";";
|
|
381
386
|
const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
382
|
-
|
|
383
|
-
}
|
|
387
|
+
result += cssKey + ":" + style[key];
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
384
390
|
}
|
|
385
391
|
var SVG_ATTR_MAP = {
|
|
386
392
|
// Presentation / geometry
|
|
@@ -515,7 +521,8 @@ var SVG_ATTR_MAP = {
|
|
|
515
521
|
};
|
|
516
522
|
function renderAttributes(props, isSvg) {
|
|
517
523
|
let attrs = "";
|
|
518
|
-
for (const
|
|
524
|
+
for (const key in props) {
|
|
525
|
+
const value = props[key];
|
|
519
526
|
if (key === "children" || key === "key" || key === "ref" || key === "dangerouslySetInnerHTML" || key === "suppressHydrationWarning" || key === "suppressContentEditableWarning")
|
|
520
527
|
continue;
|
|
521
528
|
if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
|
package/dist/slim-react/index.js
CHANGED
|
@@ -269,17 +269,23 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
269
269
|
"track",
|
|
270
270
|
"wbr"
|
|
271
271
|
]);
|
|
272
|
+
var HTML_ESC = { "&": "&", "<": "<", ">": ">", "'": "'" };
|
|
272
273
|
function escapeHtml(str) {
|
|
273
|
-
return str.replace(
|
|
274
|
+
return str.replace(/[&<>']/g, (c) => HTML_ESC[c]);
|
|
274
275
|
}
|
|
276
|
+
var ATTR_ESC = { "&": "&", '"': """, "<": "<", ">": ">" };
|
|
275
277
|
function escapeAttr(str) {
|
|
276
|
-
return str.replace(
|
|
278
|
+
return str.replace(/[&"<>]/g, (c) => ATTR_ESC[c]);
|
|
277
279
|
}
|
|
278
280
|
function styleObjectToString(style) {
|
|
279
|
-
|
|
281
|
+
let result = "";
|
|
282
|
+
for (const key in style) {
|
|
283
|
+
if (result)
|
|
284
|
+
result += ";";
|
|
280
285
|
const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
281
|
-
|
|
282
|
-
}
|
|
286
|
+
result += cssKey + ":" + style[key];
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
283
289
|
}
|
|
284
290
|
var SVG_ATTR_MAP = {
|
|
285
291
|
// Presentation / geometry
|
|
@@ -414,7 +420,8 @@ var SVG_ATTR_MAP = {
|
|
|
414
420
|
};
|
|
415
421
|
function renderAttributes(props, isSvg) {
|
|
416
422
|
let attrs = "";
|
|
417
|
-
for (const
|
|
423
|
+
for (const key in props) {
|
|
424
|
+
const value = props[key];
|
|
418
425
|
if (key === "children" || key === "key" || key === "ref" || key === "dangerouslySetInnerHTML" || key === "suppressHydrationWarning" || key === "suppressContentEditableWarning")
|
|
419
426
|
continue;
|
|
420
427
|
if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
|
|
@@ -294,17 +294,23 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
294
294
|
"track",
|
|
295
295
|
"wbr"
|
|
296
296
|
]);
|
|
297
|
+
var HTML_ESC = { "&": "&", "<": "<", ">": ">", "'": "'" };
|
|
297
298
|
function escapeHtml(str) {
|
|
298
|
-
return str.replace(
|
|
299
|
+
return str.replace(/[&<>']/g, (c) => HTML_ESC[c]);
|
|
299
300
|
}
|
|
301
|
+
var ATTR_ESC = { "&": "&", '"': """, "<": "<", ">": ">" };
|
|
300
302
|
function escapeAttr(str) {
|
|
301
|
-
return str.replace(
|
|
303
|
+
return str.replace(/[&"<>]/g, (c) => ATTR_ESC[c]);
|
|
302
304
|
}
|
|
303
305
|
function styleObjectToString(style) {
|
|
304
|
-
|
|
306
|
+
let result = "";
|
|
307
|
+
for (const key in style) {
|
|
308
|
+
if (result)
|
|
309
|
+
result += ";";
|
|
305
310
|
const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
306
|
-
|
|
307
|
-
}
|
|
311
|
+
result += cssKey + ":" + style[key];
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
308
314
|
}
|
|
309
315
|
var SVG_ATTR_MAP = {
|
|
310
316
|
// Presentation / geometry
|
|
@@ -439,7 +445,8 @@ var SVG_ATTR_MAP = {
|
|
|
439
445
|
};
|
|
440
446
|
function renderAttributes(props, isSvg) {
|
|
441
447
|
let attrs = "";
|
|
442
|
-
for (const
|
|
448
|
+
for (const key in props) {
|
|
449
|
+
const value = props[key];
|
|
443
450
|
if (key === "children" || key === "key" || key === "ref" || key === "dangerouslySetInnerHTML" || key === "suppressHydrationWarning" || key === "suppressContentEditableWarning")
|
|
444
451
|
continue;
|
|
445
452
|
if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
|
|
@@ -973,16 +980,18 @@ async function runFullLifecycle(serialReq) {
|
|
|
973
980
|
};
|
|
974
981
|
const unsuspend = { cache: /* @__PURE__ */ new Map() };
|
|
975
982
|
globalThis.__hadarsUnsuspend = unsuspend;
|
|
983
|
+
let prelimHtml;
|
|
976
984
|
try {
|
|
977
|
-
|
|
985
|
+
prelimHtml = await renderToString(createElement(Component, props));
|
|
978
986
|
if (getAfterRenderProps) {
|
|
979
|
-
props = await getAfterRenderProps(props,
|
|
987
|
+
props = await getAfterRenderProps(props, prelimHtml);
|
|
980
988
|
await renderToString(
|
|
981
989
|
createElement(Component, { ...props, location: serialReq.location, context })
|
|
982
990
|
);
|
|
983
991
|
}
|
|
984
|
-
}
|
|
992
|
+
} catch (e) {
|
|
985
993
|
globalThis.__hadarsUnsuspend = null;
|
|
994
|
+
throw e;
|
|
986
995
|
}
|
|
987
996
|
const { context: _ctx, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
|
|
988
997
|
const serverData = {};
|
|
@@ -996,7 +1005,16 @@ async function runFullLifecycle(serialReq) {
|
|
|
996
1005
|
...Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}
|
|
997
1006
|
};
|
|
998
1007
|
const finalAppProps = { ...props, location: serialReq.location, context };
|
|
999
|
-
|
|
1008
|
+
let appHtml;
|
|
1009
|
+
try {
|
|
1010
|
+
appHtml = await renderToString(createElement(Component, finalAppProps));
|
|
1011
|
+
} finally {
|
|
1012
|
+
globalThis.__hadarsUnsuspend = null;
|
|
1013
|
+
}
|
|
1014
|
+
appHtml = processSegmentCache(appHtml);
|
|
1015
|
+
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c");
|
|
1016
|
+
const html = `<div id="app">${appHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>`;
|
|
1017
|
+
return { html, headHtml: buildHeadHtml(context.head), status: context.head.status ?? 200 };
|
|
1000
1018
|
}
|
|
1001
1019
|
parentPort.on("message", async (msg) => {
|
|
1002
1020
|
const { id, type, request } = msg;
|
|
@@ -1004,18 +1022,7 @@ parentPort.on("message", async (msg) => {
|
|
|
1004
1022
|
await init();
|
|
1005
1023
|
if (type !== "renderFull")
|
|
1006
1024
|
return;
|
|
1007
|
-
const {
|
|
1008
|
-
const Component = _ssrMod.default;
|
|
1009
|
-
globalThis.__hadarsUnsuspend = unsuspend;
|
|
1010
|
-
let appHtml;
|
|
1011
|
-
try {
|
|
1012
|
-
appHtml = await renderToString(createElement(Component, finalAppProps));
|
|
1013
|
-
} finally {
|
|
1014
|
-
globalThis.__hadarsUnsuspend = null;
|
|
1015
|
-
}
|
|
1016
|
-
appHtml = processSegmentCache(appHtml);
|
|
1017
|
-
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c");
|
|
1018
|
-
const html = `<div id="app">${appHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>`;
|
|
1025
|
+
const { html, headHtml, status } = await runFullLifecycle(request);
|
|
1019
1026
|
parentPort.postMessage({ id, html, headHtml, status });
|
|
1020
1027
|
} catch (err) {
|
|
1021
1028
|
parentPort.postMessage({ id, error: err?.message ?? String(err) });
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -16,7 +16,6 @@ import os from 'node:os';
|
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
17
|
import cluster from 'node:cluster';
|
|
18
18
|
import type { HadarsEntryModule, HadarsOptions, HadarsProps } from "./types/hadars";
|
|
19
|
-
import { processSegmentCache } from "./utils/segmentCache";
|
|
20
19
|
const encoder = new TextEncoder();
|
|
21
20
|
|
|
22
21
|
/**
|
|
@@ -73,7 +72,7 @@ async function processHtmlTemplate(templatePath: string): Promise<string> {
|
|
|
73
72
|
const HEAD_MARKER = '<meta name="HADARS_HEAD">';
|
|
74
73
|
const BODY_MARKER = '<meta name="HADARS_BODY">';
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
|
|
77
76
|
|
|
78
77
|
// Round-robin thread pool for SSR rendering — used on Bun/Deno where
|
|
79
78
|
// node:cluster is not available but node:worker_threads is.
|
|
@@ -194,29 +193,19 @@ class RenderWorkerPool {
|
|
|
194
193
|
}
|
|
195
194
|
|
|
196
195
|
async function buildSsrResponse(
|
|
197
|
-
|
|
198
|
-
appProps: Record<string, unknown>,
|
|
196
|
+
bodyHtml: string,
|
|
199
197
|
clientProps: Record<string, unknown>,
|
|
200
198
|
headHtml: string,
|
|
201
199
|
status: number,
|
|
202
200
|
getPrecontentHtml: (headHtml: string) => Promise<[string, string]>,
|
|
203
|
-
unsuspendForRender: any,
|
|
204
201
|
): Promise<Response> {
|
|
205
202
|
const responseStream = new ReadableStream({
|
|
206
203
|
async start(controller) {
|
|
207
204
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
208
205
|
// Flush the shell (precontentHtml) immediately so the browser can
|
|
209
|
-
// start loading CSS/fonts before
|
|
206
|
+
// start loading CSS/fonts before the body is assembled.
|
|
210
207
|
controller.enqueue(encoder.encode(precontentHtml));
|
|
211
208
|
|
|
212
|
-
let bodyHtml: string;
|
|
213
|
-
try {
|
|
214
|
-
(globalThis as any).__hadarsUnsuspend = unsuspendForRender;
|
|
215
|
-
bodyHtml = await slimRenderToString(createElement(App, appProps));
|
|
216
|
-
} finally {
|
|
217
|
-
(globalThis as any).__hadarsUnsuspend = null;
|
|
218
|
-
}
|
|
219
|
-
bodyHtml = processSegmentCache(bodyHtml);
|
|
220
209
|
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
|
|
221
210
|
controller.enqueue(encoder.encode(
|
|
222
211
|
`<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent
|
|
@@ -690,7 +679,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
690
679
|
getFinalProps,
|
|
691
680
|
} = (await import(importPath)) as HadarsEntryModule<any>;
|
|
692
681
|
|
|
693
|
-
const {
|
|
682
|
+
const { bodyHtml, clientProps, status, headHtml } = await getReactResponse(request, {
|
|
694
683
|
document: {
|
|
695
684
|
body: Component as React.FC<HadarsProps<object>>,
|
|
696
685
|
lang: 'en',
|
|
@@ -700,7 +689,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
700
689
|
},
|
|
701
690
|
});
|
|
702
691
|
|
|
703
|
-
return buildSsrResponse(
|
|
692
|
+
return buildSsrResponse(bodyHtml, clientProps, headHtml, status, getPrecontentHtml);
|
|
704
693
|
} catch (err: any) {
|
|
705
694
|
console.error('[hadars] SSR render error:', err);
|
|
706
695
|
const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, '<');
|
|
@@ -880,7 +869,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
880
869
|
});
|
|
881
870
|
}
|
|
882
871
|
|
|
883
|
-
const {
|
|
872
|
+
const { bodyHtml, clientProps, status, headHtml } = await getReactResponse(request, {
|
|
884
873
|
document: {
|
|
885
874
|
body: Component as React.FC<HadarsProps<object>>,
|
|
886
875
|
lang: 'en',
|
|
@@ -890,7 +879,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
890
879
|
},
|
|
891
880
|
});
|
|
892
881
|
|
|
893
|
-
return buildSsrResponse(
|
|
882
|
+
return buildSsrResponse(bodyHtml, clientProps, headHtml, status, getPrecontentHtml);
|
|
894
883
|
} catch (err: any) {
|
|
895
884
|
console.error('[hadars] SSR render error:', err);
|
|
896
885
|
return new Response('Internal Server Error', { status: 500 });
|
package/src/slim-react/render.ts
CHANGED
|
@@ -61,30 +61,25 @@ const VOID_ELEMENTS = new Set([
|
|
|
61
61
|
"wbr",
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
|
+
const HTML_ESC: Record<string, string> = { '&': '&', '<': '<', '>': '>', "'": ''' };
|
|
64
65
|
function escapeHtml(str: string): string {
|
|
65
|
-
return str
|
|
66
|
-
.replace(/&/g, "&")
|
|
67
|
-
.replace(/</g, "<")
|
|
68
|
-
.replace(/>/g, ">")
|
|
69
|
-
.replace(/'/g, "'");
|
|
66
|
+
return str.replace(/[&<>']/g, c => HTML_ESC[c]!);
|
|
70
67
|
}
|
|
71
68
|
|
|
69
|
+
const ATTR_ESC: Record<string, string> = { '&': '&', '"': '"', '<': '<', '>': '>' };
|
|
72
70
|
function escapeAttr(str: string): string {
|
|
73
|
-
return str
|
|
74
|
-
.replace(/&/g, "&")
|
|
75
|
-
.replace(/"/g, """)
|
|
76
|
-
.replace(/</g, "<")
|
|
77
|
-
.replace(/>/g, ">");
|
|
71
|
+
return str.replace(/[&"<>]/g, c => ATTR_ESC[c]!);
|
|
78
72
|
}
|
|
79
73
|
|
|
80
74
|
function styleObjectToString(style: Record<string, any>): string {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
75
|
+
let result = '';
|
|
76
|
+
for (const key in style) {
|
|
77
|
+
if (result) result += ';';
|
|
78
|
+
// camelCase → kebab-case
|
|
79
|
+
const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
80
|
+
result += cssKey + ':' + style[key];
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
88
83
|
}
|
|
89
84
|
|
|
90
85
|
// ---------------------------------------------------------------------------
|
|
@@ -247,7 +242,8 @@ const SVG_ELEMENTS = new Set([
|
|
|
247
242
|
|
|
248
243
|
function renderAttributes(props: Record<string, any>, isSvg: boolean): string {
|
|
249
244
|
let attrs = "";
|
|
250
|
-
for (const
|
|
245
|
+
for (const key in props) {
|
|
246
|
+
const value = props[key];
|
|
251
247
|
// Skip internal / non-attribute props
|
|
252
248
|
if (
|
|
253
249
|
key === "children" ||
|
package/src/ssr-render-worker.ts
CHANGED
|
@@ -102,17 +102,19 @@ async function runFullLifecycle(serialReq: SerializableRequest) {
|
|
|
102
102
|
const unsuspend = { cache: new Map<string, any>() };
|
|
103
103
|
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
104
104
|
|
|
105
|
+
let prelimHtml: string;
|
|
105
106
|
try {
|
|
106
|
-
|
|
107
|
+
prelimHtml = await renderToString(createElement(Component, props));
|
|
107
108
|
|
|
108
109
|
if (getAfterRenderProps) {
|
|
109
|
-
props = await getAfterRenderProps(props,
|
|
110
|
+
props = await getAfterRenderProps(props, prelimHtml);
|
|
110
111
|
await renderToString(
|
|
111
112
|
createElement(Component, { ...props, location: serialReq.location, context }),
|
|
112
113
|
);
|
|
113
114
|
}
|
|
114
|
-
}
|
|
115
|
+
} catch (e) {
|
|
115
116
|
(globalThis as any).__hadarsUnsuspend = null;
|
|
117
|
+
throw e;
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
const { context: _ctx, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
|
|
@@ -129,7 +131,21 @@ async function runFullLifecycle(serialReq: SerializableRequest) {
|
|
|
129
131
|
};
|
|
130
132
|
|
|
131
133
|
const finalAppProps = { ...props, location: serialReq.location, context };
|
|
132
|
-
|
|
134
|
+
|
|
135
|
+
// Final render — __hadarsUnsuspend is still set; cache is fully populated so
|
|
136
|
+
// useServerData calls return cached values without any async work.
|
|
137
|
+
let appHtml: string;
|
|
138
|
+
try {
|
|
139
|
+
appHtml = await renderToString(createElement(Component, finalAppProps));
|
|
140
|
+
} finally {
|
|
141
|
+
(globalThis as any).__hadarsUnsuspend = null;
|
|
142
|
+
}
|
|
143
|
+
appHtml = processSegmentCache(appHtml);
|
|
144
|
+
|
|
145
|
+
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
|
|
146
|
+
const html = `<div id="app">${appHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>`;
|
|
147
|
+
|
|
148
|
+
return { html, headHtml: buildHeadHtml(context.head), status: context.head.status ?? 200 };
|
|
133
149
|
}
|
|
134
150
|
|
|
135
151
|
// ── Message handler ────────────────────────────────────────────────────────
|
|
@@ -140,27 +156,7 @@ parentPort!.on('message', async (msg: any) => {
|
|
|
140
156
|
await init();
|
|
141
157
|
if (type !== 'renderFull') return;
|
|
142
158
|
|
|
143
|
-
const {
|
|
144
|
-
await runFullLifecycle(request as SerializableRequest);
|
|
145
|
-
|
|
146
|
-
const Component = _ssrMod.default;
|
|
147
|
-
|
|
148
|
-
// Render the Component as the direct root — matching hydrateRoot(div#app, <Component>)
|
|
149
|
-
// on the client. Wrapping in Fragment(div#app(...), script) would add an extra
|
|
150
|
-
// pushTreeContext(2,0) from the Fragment's child array, shifting all tree-position
|
|
151
|
-
// useId values by 2 bits and causing a hydration mismatch.
|
|
152
|
-
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
153
|
-
let appHtml: string;
|
|
154
|
-
try {
|
|
155
|
-
appHtml = await renderToString(createElement(Component, finalAppProps));
|
|
156
|
-
} finally {
|
|
157
|
-
(globalThis as any).__hadarsUnsuspend = null;
|
|
158
|
-
}
|
|
159
|
-
appHtml = processSegmentCache(appHtml);
|
|
160
|
-
|
|
161
|
-
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
|
|
162
|
-
const html = `<div id="app">${appHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>`;
|
|
163
|
-
|
|
159
|
+
const { html, headHtml, status } = await runFullLifecycle(request as SerializableRequest);
|
|
164
160
|
parentPort!.postMessage({ id, html, headHtml, status });
|
|
165
161
|
|
|
166
162
|
} catch (err: any) {
|
package/src/utils/response.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import type { AppHead, HadarsRequest, HadarsEntryBase, HadarsEntryModule, HadarsProps, AppContext } from "../types/hadars";
|
|
3
3
|
import { renderToString, createElement } from '../slim-react/index';
|
|
4
|
+
import { processSegmentCache } from './segmentCache';
|
|
4
5
|
|
|
5
6
|
interface ReactResponseOptions {
|
|
6
7
|
document: {
|
|
@@ -55,10 +56,8 @@ export const getReactResponse = async (
|
|
|
55
56
|
req: HadarsRequest,
|
|
56
57
|
opts: ReactResponseOptions,
|
|
57
58
|
): Promise<{
|
|
58
|
-
|
|
59
|
-
appProps: Record<string, unknown>,
|
|
59
|
+
bodyHtml: string,
|
|
60
60
|
clientProps: Record<string, unknown>,
|
|
61
|
-
unsuspend: { cache: Map<string, any> },
|
|
62
61
|
status: number,
|
|
63
62
|
headHtml: string,
|
|
64
63
|
}> => {
|
|
@@ -78,11 +77,12 @@ export const getReactResponse = async (
|
|
|
78
77
|
// Create per-request cache for useServerData, active for all renders.
|
|
79
78
|
const unsuspend = { cache: new Map<string, any>() };
|
|
80
79
|
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
80
|
+
let bodyHtml: string;
|
|
81
81
|
try {
|
|
82
|
-
|
|
82
|
+
bodyHtml = await renderToString(createElement(App as any, props as any));
|
|
83
83
|
if (getAfterRenderProps) {
|
|
84
|
-
props = await getAfterRenderProps(props,
|
|
85
|
-
await renderToString(
|
|
84
|
+
props = await getAfterRenderProps(props, bodyHtml);
|
|
85
|
+
bodyHtml = await renderToString(
|
|
86
86
|
createElement(App as any, { ...props, location: req.location, context } as any),
|
|
87
87
|
);
|
|
88
88
|
}
|
|
@@ -103,13 +103,9 @@ export const getReactResponse = async (
|
|
|
103
103
|
...(Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}),
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
const appProps = { ...props, location: req.location, context } as unknown as Record<string, unknown>;
|
|
107
|
-
|
|
108
106
|
return {
|
|
109
|
-
|
|
110
|
-
appProps,
|
|
107
|
+
bodyHtml: processSegmentCache(bodyHtml),
|
|
111
108
|
clientProps: clientProps as Record<string, unknown>,
|
|
112
|
-
unsuspend,
|
|
113
109
|
status: context.head.status,
|
|
114
110
|
headHtml: getHeadHtml(context.head),
|
|
115
111
|
};
|