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 +66 -69
- package/dist/loader.cjs +95 -3
- 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/dist/ssr-watch.js +4 -2
- 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/loader.ts +137 -17
- package/src/utils/response.tsx +7 -11
- package/src/utils/rspack.ts +2 -0
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
|
};
|
|
@@ -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(
|
|
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 {
|
|
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(
|
|
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, "<");
|
|
@@ -2356,7 +2353,7 @@ var run = async (options) => {
|
|
|
2356
2353
|
status: wStatus
|
|
2357
2354
|
});
|
|
2358
2355
|
}
|
|
2359
|
-
const {
|
|
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(
|
|
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
|
|
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"
|
|
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
|
-
|
|
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 = { "&": "&", "<": "<", ">": ">", "'": "'" };
|
|
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/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
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/loader.ts
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rspack/webpack loader that
|
|
3
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
17
|
-
* Fallback —
|
|
24
|
+
* literals that contain the function names.
|
|
25
|
+
* Fallback — Scanner-based transform used when @swc/core is unavailable.
|
|
18
26
|
*
|
|
19
|
-
* Example
|
|
27
|
+
* Example:
|
|
20
28
|
*
|
|
21
|
-
*
|
|
29
|
+
* // Source (shared component):
|
|
30
|
+
* const user = useServerData('user', () => db.getUser(req.userId));
|
|
22
31
|
*
|
|
23
|
-
* //
|
|
24
|
-
* const
|
|
32
|
+
* // Client bundle after transform:
|
|
33
|
+
* const user = useServerData('user', ()=>undefined);
|
|
25
34
|
*
|
|
26
|
-
* //
|
|
27
|
-
* const
|
|
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
|
-
|
|
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'
|
|
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
|
-
|
|
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;
|
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
|
};
|
package/src/utils/rspack.ts
CHANGED
|
@@ -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',
|