hadars 0.1.32 → 0.1.34

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 CHANGED
@@ -387,17 +387,23 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
387
387
  "track",
388
388
  "wbr"
389
389
  ]);
390
+ var HTML_ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#x27;" };
390
391
  function escapeHtml(str) {
391
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#x27;");
392
+ return str.replace(/[&<>']/g, (c) => HTML_ESC[c]);
392
393
  }
394
+ var ATTR_ESC = { "&": "&amp;", '"': "&quot;", "<": "&lt;", ">": "&gt;" };
393
395
  function escapeAttr(str) {
394
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
396
+ return str.replace(/[&"<>]/g, (c) => ATTR_ESC[c]);
395
397
  }
396
398
  function styleObjectToString(style) {
397
- return Object.entries(style).map(([key, value]) => {
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
- return `${cssKey}:${value}`;
400
- }).join(";");
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 [key, value] of Object.entries(props)) {
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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
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
- let html = await renderToString(createElement(App, props));
1104
+ bodyHtml = await renderToString(createElement(App, props));
1058
1105
  if (getAfterRenderProps) {
1059
- props = await getAfterRenderProps(props, html);
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
- App,
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
  };
@@ -1137,7 +1181,8 @@ var getConfigBase = (mode, isServerBuild = false) => {
1137
1181
  // Transforms loadModule('./path') based on build target.
1138
1182
  // Runs before swc-loader (loaders execute right-to-left).
1139
1183
  {
1140
- loader: loaderPath
1184
+ loader: loaderPath,
1185
+ options: { server: isServerBuild }
1141
1186
  },
1142
1187
  {
1143
1188
  loader: "builtin:swc-loader",
@@ -1168,7 +1213,8 @@ var getConfigBase = (mode, isServerBuild = false) => {
1168
1213
  exclude: [loaderPath],
1169
1214
  use: [
1170
1215
  {
1171
- loader: loaderPath
1216
+ loader: loaderPath,
1217
+ options: { server: isServerBuild }
1172
1218
  },
1173
1219
  {
1174
1220
  loader: "builtin:swc-loader",
@@ -1631,47 +1677,6 @@ import { existsSync as existsSync2 } from "node:fs";
1631
1677
  import os from "node:os";
1632
1678
  import { spawn } from "node:child_process";
1633
1679
  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
1680
  var encoder = new TextEncoder();
1676
1681
  async function processHtmlTemplate(templatePath) {
1677
1682
  const html = await fs.readFile(templatePath, "utf-8");
@@ -1816,19 +1821,11 @@ var RenderWorkerPool = class {
1816
1821
  await Promise.all(this.workers.map((w) => w.terminate()));
1817
1822
  }
1818
1823
  };
1819
- async function buildSsrResponse(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspendForRender) {
1824
+ async function buildSsrResponse(bodyHtml, clientProps, headHtml, status, getPrecontentHtml) {
1820
1825
  const responseStream = new ReadableStream({
1821
1826
  async start(controller) {
1822
1827
  const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
1823
1828
  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
1829
  const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c");
1833
1830
  controller.enqueue(encoder.encode(
1834
1831
  `<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent
@@ -2202,7 +2199,7 @@ var dev = async (options) => {
2202
2199
  getAfterRenderProps,
2203
2200
  getFinalProps
2204
2201
  } = await import(importPath);
2205
- const { App, appProps, clientProps, unsuspend, status, headHtml } = await getReactResponse(request, {
2202
+ const { bodyHtml, clientProps, status, headHtml } = await getReactResponse(request, {
2206
2203
  document: {
2207
2204
  body: Component,
2208
2205
  lang: "en",
@@ -2211,7 +2208,7 @@ var dev = async (options) => {
2211
2208
  getFinalProps
2212
2209
  }
2213
2210
  });
2214
- return buildSsrResponse(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspend);
2211
+ return buildSsrResponse(bodyHtml, clientProps, headHtml, status, getPrecontentHtml);
2215
2212
  } catch (err) {
2216
2213
  console.error("[hadars] SSR render error:", err);
2217
2214
  const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, "&lt;");
@@ -2356,7 +2353,7 @@ var run = async (options) => {
2356
2353
  status: wStatus
2357
2354
  });
2358
2355
  }
2359
- const { App, appProps, clientProps, unsuspend, status, headHtml } = await getReactResponse(request, {
2356
+ const { bodyHtml, clientProps, status, headHtml } = await getReactResponse(request, {
2360
2357
  document: {
2361
2358
  body: Component,
2362
2359
  lang: "en",
@@ -2365,7 +2362,7 @@ var run = async (options) => {
2365
2362
  getFinalProps
2366
2363
  }
2367
2364
  });
2368
- return buildSsrResponse(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspend);
2365
+ return buildSsrResponse(bodyHtml, clientProps, headHtml, status, getPrecontentHtml);
2369
2366
  } catch (err) {
2370
2367
  console.error("[hadars] SSR render error:", err);
2371
2368
  return new Response("Internal Server Error", { status: 500 });
package/dist/loader.cjs CHANGED
@@ -22,7 +22,8 @@ __export(loader_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(loader_exports);
24
24
  function loader(source) {
25
- const isServer = this.target === "node" || this.target === "async-node";
25
+ const opts = this.getOptions?.() ?? {};
26
+ const isServer = typeof opts.server === "boolean" ? opts.server : this.target === "node" || this.target === "async-node";
26
27
  const resourcePath = this.resourcePath ?? this.resource ?? "(unknown)";
27
28
  let swc;
28
29
  try {
@@ -51,7 +52,22 @@ function swcTransform(swc, source, isServer, resourcePath) {
51
52
  if (node.type !== "CallExpression")
52
53
  return;
53
54
  const callee = node.callee;
54
- if (!callee || callee.type !== "Identifier" || callee.value !== "loadModule")
55
+ if (!callee || callee.type !== "Identifier")
56
+ return;
57
+ const name = callee.value;
58
+ if (!isServer && name === "useServerData") {
59
+ const args2 = node.arguments;
60
+ if (!args2 || args2.length < 2)
61
+ return;
62
+ const fnArg = args2[1].expression ?? args2[1];
63
+ replacements.push({
64
+ start: fnArg.span.start - fileOffset,
65
+ end: fnArg.span.end - fileOffset,
66
+ replacement: "()=>undefined"
67
+ });
68
+ return;
69
+ }
70
+ if (name !== "loadModule")
55
71
  return;
56
72
  const args = node.arguments;
57
73
  if (!args || args.length === 0)
@@ -136,11 +152,87 @@ function countLeadingNonCodeBytes(source) {
136
152
  }
137
153
  const LOAD_MODULE_RE = /\bloadModule\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(\s*(['"`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/gs;
138
154
  const DYNAMIC_LOAD_MODULE_RE = /\bloadModule\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
155
+ function scanExpressionEnd(source, pos) {
156
+ let depth = 0;
157
+ let i = pos;
158
+ while (i < source.length) {
159
+ const ch = source[i];
160
+ if (ch === "(" || ch === "[" || ch === "{") {
161
+ depth++;
162
+ i++;
163
+ continue;
164
+ }
165
+ if (ch === ")" || ch === "]" || ch === "}") {
166
+ if (depth === 0)
167
+ break;
168
+ depth--;
169
+ i++;
170
+ continue;
171
+ }
172
+ if (ch === "," && depth === 0)
173
+ break;
174
+ if (ch === '"' || ch === "'" || ch === "`") {
175
+ const q = ch;
176
+ i++;
177
+ while (i < source.length && source[i] !== q) {
178
+ if (source[i] === "\\")
179
+ i++;
180
+ i++;
181
+ }
182
+ i++;
183
+ continue;
184
+ }
185
+ if (ch === "/" && source[i + 1] === "/") {
186
+ while (i < source.length && source[i] !== "\n")
187
+ i++;
188
+ continue;
189
+ }
190
+ if (ch === "/" && source[i + 1] === "*") {
191
+ i += 2;
192
+ while (i + 1 < source.length && !(source[i] === "*" && source[i + 1] === "/"))
193
+ i++;
194
+ i += 2;
195
+ continue;
196
+ }
197
+ i++;
198
+ }
199
+ return i;
200
+ }
201
+ function stripUseServerDataFns(source) {
202
+ const CALL_RE = /\buseServerData\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
203
+ let result = "";
204
+ let lastIndex = 0;
205
+ let match;
206
+ CALL_RE.lastIndex = 0;
207
+ while ((match = CALL_RE.exec(source)) !== null) {
208
+ const callStart = match.index;
209
+ let i = match.index + match[0].length;
210
+ while (i < source.length && /\s/.test(source[i]))
211
+ i++;
212
+ i = scanExpressionEnd(source, i);
213
+ if (i >= source.length || source[i] !== ",")
214
+ continue;
215
+ i++;
216
+ while (i < source.length && /\s/.test(source[i]))
217
+ i++;
218
+ const fnStart = i;
219
+ const fnEnd = scanExpressionEnd(source, i);
220
+ if (fnEnd <= fnStart)
221
+ continue;
222
+ result += source.slice(lastIndex, fnStart) + "()=>undefined";
223
+ lastIndex = fnEnd;
224
+ CALL_RE.lastIndex = fnEnd;
225
+ }
226
+ return lastIndex === 0 ? source : result + source.slice(lastIndex);
227
+ }
139
228
  function regexTransform(source, isServer, resourcePath) {
140
- const transformed = source.replace(
229
+ let transformed = source.replace(
141
230
  LOAD_MODULE_RE,
142
231
  (_match, quote, modulePath) => isServer ? `Promise.resolve(require(${quote}${modulePath}${quote}))` : `import(${quote}${modulePath}${quote})`
143
232
  );
233
+ if (!isServer) {
234
+ transformed = stripUseServerDataFns(transformed);
235
+ }
144
236
  let match;
145
237
  DYNAMIC_LOAD_MODULE_RE.lastIndex = 0;
146
238
  while ((match = DYNAMIC_LOAD_MODULE_RE.exec(transformed)) !== null) {
@@ -370,17 +370,23 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
370
370
  "track",
371
371
  "wbr"
372
372
  ]);
373
+ var HTML_ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#x27;" };
373
374
  function escapeHtml(str) {
374
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#x27;");
375
+ return str.replace(/[&<>']/g, (c) => HTML_ESC[c]);
375
376
  }
377
+ var ATTR_ESC = { "&": "&amp;", '"': "&quot;", "<": "&lt;", ">": "&gt;" };
376
378
  function escapeAttr(str) {
377
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
379
+ return str.replace(/[&"<>]/g, (c) => ATTR_ESC[c]);
378
380
  }
379
381
  function styleObjectToString(style) {
380
- return Object.entries(style).map(([key, value]) => {
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
- return `${cssKey}:${value}`;
383
- }).join(";");
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 [key, value] of Object.entries(props)) {
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())
@@ -269,17 +269,23 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
269
269
  "track",
270
270
  "wbr"
271
271
  ]);
272
+ var HTML_ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#x27;" };
272
273
  function escapeHtml(str) {
273
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#x27;");
274
+ return str.replace(/[&<>']/g, (c) => HTML_ESC[c]);
274
275
  }
276
+ var ATTR_ESC = { "&": "&amp;", '"': "&quot;", "<": "&lt;", ">": "&gt;" };
275
277
  function escapeAttr(str) {
276
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
278
+ return str.replace(/[&"<>]/g, (c) => ATTR_ESC[c]);
277
279
  }
278
280
  function styleObjectToString(style) {
279
- return Object.entries(style).map(([key, value]) => {
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
- return `${cssKey}:${value}`;
282
- }).join(";");
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 [key, value] of Object.entries(props)) {
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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#x27;" };
297
298
  function escapeHtml(str) {
298
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#x27;");
299
+ return str.replace(/[&<>']/g, (c) => HTML_ESC[c]);
299
300
  }
301
+ var ATTR_ESC = { "&": "&amp;", '"': "&quot;", "<": "&lt;", ">": "&gt;" };
300
302
  function escapeAttr(str) {
301
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
303
+ return str.replace(/[&"<>]/g, (c) => ATTR_ESC[c]);
302
304
  }
303
305
  function styleObjectToString(style) {
304
- return Object.entries(style).map(([key, value]) => {
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
- return `${cssKey}:${value}`;
307
- }).join(";");
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 [key, value] of Object.entries(props)) {
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
- let html = await renderToString(createElement(Component, props));
985
+ prelimHtml = await renderToString(createElement(Component, props));
978
986
  if (getAfterRenderProps) {
979
- props = await getAfterRenderProps(props, html);
987
+ props = await getAfterRenderProps(props, prelimHtml);
980
988
  await renderToString(
981
989
  createElement(Component, { ...props, location: serialReq.location, context })
982
990
  );
983
991
  }
984
- } finally {
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
- return { finalAppProps, clientProps, unsuspend, headHtml: buildHeadHtml(context.head), status: context.head.status ?? 200 };
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 { finalAppProps, clientProps, unsuspend, headHtml, status } = await runFullLifecycle(request);
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/dist/ssr-watch.js CHANGED
@@ -52,7 +52,8 @@ var getConfigBase = (mode, isServerBuild = false) => {
52
52
  // Transforms loadModule('./path') based on build target.
53
53
  // Runs before swc-loader (loaders execute right-to-left).
54
54
  {
55
- loader: loaderPath
55
+ loader: loaderPath,
56
+ options: { server: isServerBuild }
56
57
  },
57
58
  {
58
59
  loader: "builtin:swc-loader",
@@ -83,7 +84,8 @@ var getConfigBase = (mode, isServerBuild = false) => {
83
84
  exclude: [loaderPath],
84
85
  use: [
85
86
  {
86
- loader: loaderPath
87
+ loader: loaderPath,
88
+ options: { server: isServerBuild }
87
89
  },
88
90
  {
89
91
  loader: "builtin:swc-loader",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
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
- import { renderToString as slimRenderToString, createElement } from './slim-react/index';
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
- App: any,
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 renderToString blocks the thread.
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 { App, appProps, clientProps, unsuspend, status, headHtml } = await getReactResponse(request, {
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(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspend);
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, '&lt;');
@@ -880,7 +869,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
880
869
  });
881
870
  }
882
871
 
883
- const { App, appProps, clientProps, unsuspend, status, headHtml } = await getReactResponse(request, {
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(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspend);
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 });
@@ -61,30 +61,25 @@ const VOID_ELEMENTS = new Set([
61
61
  "wbr",
62
62
  ]);
63
63
 
64
+ const HTML_ESC: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#x27;' };
64
65
  function escapeHtml(str: string): string {
65
- return str
66
- .replace(/&/g, "&amp;")
67
- .replace(/</g, "&lt;")
68
- .replace(/>/g, "&gt;")
69
- .replace(/'/g, "&#x27;");
66
+ return str.replace(/[&<>']/g, c => HTML_ESC[c]!);
70
67
  }
71
68
 
69
+ const ATTR_ESC: Record<string, string> = { '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
72
70
  function escapeAttr(str: string): string {
73
- return str
74
- .replace(/&/g, "&amp;")
75
- .replace(/"/g, "&quot;")
76
- .replace(/</g, "&lt;")
77
- .replace(/>/g, "&gt;");
71
+ return str.replace(/[&"<>]/g, c => ATTR_ESC[c]!);
78
72
  }
79
73
 
80
74
  function styleObjectToString(style: Record<string, any>): string {
81
- return Object.entries(style)
82
- .map(([key, value]) => {
83
- // camelCase kebab-case
84
- const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
85
- return `${cssKey}:${value}`;
86
- })
87
- .join(";");
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 [key, value] of Object.entries(props)) {
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" ||
@@ -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
- let html = await renderToString(createElement(Component, props));
107
+ prelimHtml = await renderToString(createElement(Component, props));
107
108
 
108
109
  if (getAfterRenderProps) {
109
- props = await getAfterRenderProps(props, html);
110
+ props = await getAfterRenderProps(props, prelimHtml);
110
111
  await renderToString(
111
112
  createElement(Component, { ...props, location: serialReq.location, context }),
112
113
  );
113
114
  }
114
- } finally {
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
- return { finalAppProps, clientProps, unsuspend, headHtml: buildHeadHtml(context.head), status: context.head.status ?? 200 };
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 { finalAppProps, clientProps, unsuspend, headHtml, status } =
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) {
@@ -1,34 +1,49 @@
1
1
  /**
2
- * Rspack/webpack loader that transforms `loadModule('path')` calls based on
3
- * the compilation target:
2
+ * Rspack/webpack loader that applies two source-level transforms based on the
3
+ * compilation target (web vs node):
4
4
  *
5
+ * ── loadModule('path') ────────────────────────────────────────────────────────
5
6
  * - web (browser): replaced with `import('./path')` — rspack treats this as
6
7
  * a true dynamic import and splits the module into a separate chunk.
7
- *
8
8
  * - node (SSR): replaced with `Promise.resolve(require('./path'))` —
9
- * rspack bundles the module statically so it is always available
10
- * synchronously on the server, wrapped in Promise.resolve to keep the
11
- * API shape identical to the client side.
9
+ * bundled statically, wrapped in Promise.resolve to keep the API shape.
10
+ *
11
+ * ── useServerData(key, fn) ───────────────────────────────────────────────────
12
+ * - web (browser): the second argument `fn` is replaced with `()=>undefined`.
13
+ * `fn` is a server-only callback that may reference internal endpoints,
14
+ * credentials, or other sensitive information. It is never called in the
15
+ * browser (the hook returns the SSR-cached value immediately), but without
16
+ * this transform it would still be compiled into the client bundle — exposing
17
+ * those details to anyone who inspects the JS. Stripping it at bundle time
18
+ * prevents the leak entirely.
19
+ * - node (SSR): kept as-is — the real fn is needed to fetch data.
12
20
  *
13
21
  * Transformation strategy:
14
22
  * Primary — SWC AST parsing via @swc/core. Handles any valid TS/JS syntax
15
23
  * including arbitrarily-nested generics, comments, and string
16
- * literals that contain the text "loadModule".
17
- * Fallback — Regex transform used when @swc/core is unavailable.
24
+ * literals that contain the function names.
25
+ * Fallback — Scanner-based transform used when @swc/core is unavailable.
18
26
  *
19
- * Example usage:
27
+ * Example:
20
28
  *
21
- * import { loadModule } from 'hadars';
29
+ * // Source (shared component):
30
+ * const user = useServerData('user', () => db.getUser(req.userId));
22
31
  *
23
- * // Code-split React component (wrap with React.lazy + Suspense):
24
- * const MyComp = React.lazy(() => loadModule('./MyComp'));
32
+ * // Client bundle after transform:
33
+ * const user = useServerData('user', ()=>undefined);
25
34
  *
26
- * // Dynamic module load:
27
- * const { default: fn } = await loadModule('./heavyUtil');
35
+ * // Server bundle (unchanged):
36
+ * const user = useServerData('user', () => db.getUser(req.userId));
28
37
  */
29
38
 
30
39
  export default function loader(this: any, source: string): string {
31
- const isServer = this.target === 'node' || this.target === 'async-node';
40
+ // Prefer the explicit `server` option injected by rspack.ts over the legacy
41
+ // `this.target` heuristic (which is unreliable when `target` is not set in
42
+ // the rspack config — rspack then reports 'web' for every build).
43
+ const opts = this.getOptions?.() ?? {};
44
+ const isServer: boolean = (typeof opts.server === 'boolean')
45
+ ? opts.server
46
+ : (this.target === 'node' || this.target === 'async-node');
32
47
  const resourcePath: string = this.resourcePath ?? this.resource ?? '(unknown)';
33
48
 
34
49
  let swc: any;
@@ -80,7 +95,26 @@ function swcTransform(this: any, swc: any, source: string, isServer: boolean, re
80
95
  if (node.type !== 'CallExpression') return;
81
96
 
82
97
  const callee = node.callee;
83
- if (!callee || callee.type !== 'Identifier' || callee.value !== 'loadModule') return;
98
+ if (!callee || callee.type !== 'Identifier') return;
99
+
100
+ const name: string = callee.value;
101
+
102
+ // ── useServerData(key, fn) — strip fn on client builds ────────────────
103
+ if (!isServer && name === 'useServerData') {
104
+ const args: any[] = node.arguments;
105
+ if (!args || args.length < 2) return;
106
+ const fnArg = args[1].expression ?? args[1];
107
+ // Normalise to 0-based local byte offsets and replace with stub.
108
+ replacements.push({
109
+ start: fnArg.span.start - fileOffset,
110
+ end: fnArg.span.end - fileOffset,
111
+ replacement: '()=>undefined',
112
+ });
113
+ return;
114
+ }
115
+
116
+ // ── loadModule(path) ─────────────────────────────────────────────────
117
+ if (name !== 'loadModule') return;
84
118
 
85
119
  const args: any[] = node.arguments;
86
120
  if (!args || args.length === 0) return;
@@ -199,13 +233,99 @@ const LOAD_MODULE_RE =
199
233
  // (i.e. a dynamic / non-literal path argument).
200
234
  const DYNAMIC_LOAD_MODULE_RE = /\bloadModule\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
201
235
 
236
+ /**
237
+ * Scan forward from `pos` in `source`, skipping over a balanced JS expression
238
+ * (handles nested parens/brackets/braces and string literals).
239
+ * Returns the index of the first character AFTER the expression
240
+ * (i.e. the position of the trailing `,` or `)` at depth 0).
241
+ */
242
+ function scanExpressionEnd(source: string, pos: number): number {
243
+ let depth = 0;
244
+ let i = pos;
245
+ while (i < source.length) {
246
+ const ch = source[i]!;
247
+ if (ch === '(' || ch === '[' || ch === '{') { depth++; i++; continue; }
248
+ if (ch === ')' || ch === ']' || ch === '}') {
249
+ if (depth === 0) break; // end of expression — closing delimiter of outer call
250
+ depth--; i++; continue;
251
+ }
252
+ if (ch === ',' && depth === 0) break; // end of expression — next argument
253
+ // String / template literals
254
+ if (ch === '"' || ch === "'" || ch === '`') {
255
+ const q = ch; i++;
256
+ while (i < source.length && source[i] !== q) {
257
+ if (source[i] === '\\') i++; // escape sequence
258
+ i++;
259
+ }
260
+ i++; // closing quote
261
+ continue;
262
+ }
263
+ // Line comment
264
+ if (ch === '/' && source[i + 1] === '/') {
265
+ while (i < source.length && source[i] !== '\n') i++;
266
+ continue;
267
+ }
268
+ // Block comment
269
+ if (ch === '/' && source[i + 1] === '*') {
270
+ i += 2;
271
+ while (i + 1 < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++;
272
+ i += 2;
273
+ continue;
274
+ }
275
+ i++;
276
+ }
277
+ return i;
278
+ }
279
+
280
+ /**
281
+ * Strip the `fn` argument from `useServerData(key, fn)` calls in client builds.
282
+ * Uses a character-level scanner to handle arbitrary fn expressions (arrow
283
+ * functions with nested calls, async functions, object literals, etc.).
284
+ */
285
+ function stripUseServerDataFns(source: string): string {
286
+ // Match `useServerData` + optional generic + opening paren
287
+ const CALL_RE = /\buseServerData\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
288
+ let result = '';
289
+ let lastIndex = 0;
290
+ let match: RegExpExecArray | null;
291
+ CALL_RE.lastIndex = 0;
292
+ while ((match = CALL_RE.exec(source)) !== null) {
293
+ const callStart = match.index;
294
+ let i = match.index + match[0].length;
295
+ // Skip whitespace before first arg
296
+ while (i < source.length && /\s/.test(source[i]!)) i++;
297
+ // Skip first argument (key: string or array)
298
+ i = scanExpressionEnd(source, i);
299
+ // Expect comma separator
300
+ if (i >= source.length || source[i] !== ',') continue;
301
+ i++; // skip comma
302
+ // Skip whitespace before fn
303
+ while (i < source.length && /\s/.test(source[i]!)) i++;
304
+ const fnStart = i;
305
+ // Scan to end of fn argument
306
+ const fnEnd = scanExpressionEnd(source, i);
307
+ if (fnEnd <= fnStart) continue;
308
+ // Emit everything up to fn, then the stub, skip the original fn
309
+ result += source.slice(lastIndex, fnStart) + '()=>undefined';
310
+ lastIndex = fnEnd;
311
+ // Advance regex past this call to avoid re-matching
312
+ CALL_RE.lastIndex = fnEnd;
313
+ }
314
+ return lastIndex === 0 ? source : result + source.slice(lastIndex);
315
+ }
316
+
202
317
  function regexTransform(this: any, source: string, isServer: boolean, resourcePath: string): string {
203
- const transformed = source.replace(LOAD_MODULE_RE, (_match, quote, modulePath) =>
318
+ let transformed = source.replace(LOAD_MODULE_RE, (_match, quote, modulePath) =>
204
319
  isServer
205
320
  ? `Promise.resolve(require(${quote}${modulePath}${quote}))`
206
321
  : `import(${quote}${modulePath}${quote})`
207
322
  );
208
323
 
324
+ // Strip server-only fn arguments from useServerData on client builds.
325
+ if (!isServer) {
326
+ transformed = stripUseServerDataFns(transformed);
327
+ }
328
+
209
329
  // Warn for any remaining dynamic calls
210
330
  let match: RegExpExecArray | null;
211
331
  DYNAMIC_LOAD_MODULE_RE.lastIndex = 0;
@@ -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
- App: React.FC<any>,
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
- let html = await renderToString(createElement(App as any, props as any));
82
+ bodyHtml = await renderToString(createElement(App as any, props as any));
83
83
  if (getAfterRenderProps) {
84
- props = await getAfterRenderProps(props, html);
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
- App: App as React.FC<any>,
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
  };
@@ -59,6 +59,7 @@ const getConfigBase = (mode: "development" | "production", isServerBuild = false
59
59
  // Runs before swc-loader (loaders execute right-to-left).
60
60
  {
61
61
  loader: loaderPath,
62
+ options: { server: isServerBuild },
62
63
  },
63
64
  {
64
65
  loader: 'builtin:swc-loader',
@@ -90,6 +91,7 @@ const getConfigBase = (mode: "development" | "production", isServerBuild = false
90
91
  use: [
91
92
  {
92
93
  loader: loaderPath,
94
+ options: { server: isServerBuild },
93
95
  },
94
96
  {
95
97
  loader: 'builtin:swc-loader',