hadars 0.1.29 → 0.1.30
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 +25 -38
- package/dist/index.cjs +1 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -4
- package/dist/slim-react/index.cjs +13 -10
- package/dist/slim-react/index.d.ts +11 -3
- package/dist/slim-react/index.js +13 -10
- package/dist/ssr-render-worker.js +15 -24
- package/dist/utils/Head.tsx +6 -8
- package/package.json +5 -3
- package/src/build.ts +13 -8
- package/src/slim-react/dispatcher.ts +17 -2
- package/src/slim-react/index.ts +1 -1
- package/src/slim-react/render.ts +22 -5
- package/src/slim-react/renderContext.ts +8 -8
- package/src/ssr-render-worker.ts +11 -16
- package/src/utils/Head.tsx +6 -8
- package/src/utils/response.tsx +8 -23
package/dist/cli.js
CHANGED
|
@@ -141,7 +141,6 @@ var FRAGMENT_TYPE = Symbol.for("react.fragment");
|
|
|
141
141
|
var SUSPENSE_TYPE = Symbol.for("react.suspense");
|
|
142
142
|
|
|
143
143
|
// src/slim-react/jsx.ts
|
|
144
|
-
var Fragment = FRAGMENT_TYPE;
|
|
145
144
|
function createElement(type, props, ...children) {
|
|
146
145
|
const normalizedProps = { ...props || {} };
|
|
147
146
|
if (children.length === 1) {
|
|
@@ -198,10 +197,11 @@ function s() {
|
|
|
198
197
|
}
|
|
199
198
|
return g[GLOBAL_KEY];
|
|
200
199
|
}
|
|
201
|
-
function resetRenderState() {
|
|
200
|
+
function resetRenderState(idPrefix = "") {
|
|
202
201
|
const st = s();
|
|
203
202
|
st.currentTreeContext = { ...EMPTY };
|
|
204
203
|
st.localIdCounter = 0;
|
|
204
|
+
st.idPrefix = idPrefix;
|
|
205
205
|
}
|
|
206
206
|
function pushTreeContext(totalChildren, index) {
|
|
207
207
|
const st = s();
|
|
@@ -265,10 +265,10 @@ function makeId() {
|
|
|
265
265
|
const st = s();
|
|
266
266
|
const treeId = getTreeId();
|
|
267
267
|
const n = st.localIdCounter++;
|
|
268
|
-
let id = "
|
|
268
|
+
let id = "_R_" + st.idPrefix + treeId;
|
|
269
269
|
if (n > 0)
|
|
270
270
|
id += "H" + n.toString(32);
|
|
271
|
-
return id + "
|
|
271
|
+
return id + "_";
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
// src/slim-react/hooks.ts
|
|
@@ -330,8 +330,8 @@ function use(usable) {
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
// src/slim-react/dispatcher.ts
|
|
333
|
-
import
|
|
334
|
-
var _internals =
|
|
333
|
+
import * as ReactNS from "react";
|
|
334
|
+
var _internals = ReactNS.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
335
335
|
var slimDispatcher = {
|
|
336
336
|
useId: makeId,
|
|
337
337
|
readContext: (ctx) => getContextValue(ctx),
|
|
@@ -915,8 +915,8 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
915
915
|
const snap = snapshotContext();
|
|
916
916
|
while (attempts < MAX_SUSPENSE_RETRIES) {
|
|
917
917
|
restoreContext(snap);
|
|
918
|
+
let buffer = new BufferWriter();
|
|
918
919
|
try {
|
|
919
|
-
const buffer = new BufferWriter();
|
|
920
920
|
const r = renderNode(children, buffer, isSvg);
|
|
921
921
|
if (r && typeof r.then === "function") {
|
|
922
922
|
const m = captureMap();
|
|
@@ -950,12 +950,13 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
950
950
|
}
|
|
951
951
|
writer.write("<!--/$-->");
|
|
952
952
|
}
|
|
953
|
-
async function renderToString(element) {
|
|
953
|
+
async function renderToString(element, options) {
|
|
954
|
+
const idPrefix = options?.identifierPrefix ?? "";
|
|
954
955
|
const contextMap = /* @__PURE__ */ new Map();
|
|
955
956
|
const prev = swapContextMap(contextMap);
|
|
956
957
|
try {
|
|
957
958
|
for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
|
|
958
|
-
resetRenderState();
|
|
959
|
+
resetRenderState(idPrefix);
|
|
959
960
|
swapContextMap(contextMap);
|
|
960
961
|
const chunks = [];
|
|
961
962
|
const writer = {
|
|
@@ -1072,31 +1073,14 @@ var getReactResponse = async (req, opts) => {
|
|
|
1072
1073
|
location: req.location,
|
|
1073
1074
|
...Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}
|
|
1074
1075
|
};
|
|
1075
|
-
const
|
|
1076
|
-
Fragment,
|
|
1077
|
-
null,
|
|
1078
|
-
createElement(
|
|
1079
|
-
"div",
|
|
1080
|
-
{ id: "app" },
|
|
1081
|
-
createElement(App, { ...props, location: req.location, context })
|
|
1082
|
-
),
|
|
1083
|
-
createElement("script", {
|
|
1084
|
-
id: "hadars",
|
|
1085
|
-
type: "application/json",
|
|
1086
|
-
dangerouslySetInnerHTML: {
|
|
1087
|
-
__html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c")
|
|
1088
|
-
}
|
|
1089
|
-
})
|
|
1090
|
-
);
|
|
1076
|
+
const appProps = { ...props, location: req.location, context };
|
|
1091
1077
|
return {
|
|
1092
|
-
|
|
1078
|
+
App,
|
|
1079
|
+
appProps,
|
|
1080
|
+
clientProps,
|
|
1093
1081
|
unsuspend,
|
|
1094
1082
|
status: context.head.status,
|
|
1095
|
-
headHtml: getHeadHtml(context.head)
|
|
1096
|
-
renderPayload: {
|
|
1097
|
-
appProps: { ...props, location: req.location, context },
|
|
1098
|
-
clientProps
|
|
1099
|
-
}
|
|
1083
|
+
headHtml: getHeadHtml(context.head)
|
|
1100
1084
|
};
|
|
1101
1085
|
};
|
|
1102
1086
|
|
|
@@ -1830,7 +1814,7 @@ var RenderWorkerPool = class {
|
|
|
1830
1814
|
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
1831
1815
|
}
|
|
1832
1816
|
};
|
|
1833
|
-
async function buildSsrResponse(
|
|
1817
|
+
async function buildSsrResponse(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspendForRender) {
|
|
1834
1818
|
const responseStream = new ReadableStream({
|
|
1835
1819
|
async start(controller) {
|
|
1836
1820
|
const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
|
|
@@ -1838,12 +1822,15 @@ async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml,
|
|
|
1838
1822
|
let bodyHtml;
|
|
1839
1823
|
try {
|
|
1840
1824
|
globalThis.__hadarsUnsuspend = unsuspendForRender;
|
|
1841
|
-
bodyHtml = await renderToString(
|
|
1825
|
+
bodyHtml = await renderToString(createElement(App, appProps));
|
|
1842
1826
|
} finally {
|
|
1843
1827
|
globalThis.__hadarsUnsuspend = null;
|
|
1844
1828
|
}
|
|
1845
1829
|
bodyHtml = processSegmentCache(bodyHtml);
|
|
1846
|
-
|
|
1830
|
+
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c");
|
|
1831
|
+
controller.enqueue(encoder.encode(
|
|
1832
|
+
`<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent
|
|
1833
|
+
));
|
|
1847
1834
|
controller.close();
|
|
1848
1835
|
}
|
|
1849
1836
|
});
|
|
@@ -2213,7 +2200,7 @@ var dev = async (options) => {
|
|
|
2213
2200
|
getAfterRenderProps,
|
|
2214
2201
|
getFinalProps
|
|
2215
2202
|
} = await import(importPath);
|
|
2216
|
-
const {
|
|
2203
|
+
const { App, appProps, clientProps, unsuspend, status, headHtml } = await getReactResponse(request, {
|
|
2217
2204
|
document: {
|
|
2218
2205
|
body: Component,
|
|
2219
2206
|
lang: "en",
|
|
@@ -2222,7 +2209,7 @@ var dev = async (options) => {
|
|
|
2222
2209
|
getFinalProps
|
|
2223
2210
|
}
|
|
2224
2211
|
});
|
|
2225
|
-
return buildSsrResponse(
|
|
2212
|
+
return buildSsrResponse(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspend);
|
|
2226
2213
|
} catch (err) {
|
|
2227
2214
|
console.error("[hadars] SSR render error:", err);
|
|
2228
2215
|
const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, "<");
|
|
@@ -2367,7 +2354,7 @@ var run = async (options) => {
|
|
|
2367
2354
|
status: wStatus
|
|
2368
2355
|
});
|
|
2369
2356
|
}
|
|
2370
|
-
const {
|
|
2357
|
+
const { App, appProps, clientProps, unsuspend, status, headHtml } = await getReactResponse(request, {
|
|
2371
2358
|
document: {
|
|
2372
2359
|
body: Component,
|
|
2373
2360
|
lang: "en",
|
|
@@ -2376,7 +2363,7 @@ var run = async (options) => {
|
|
|
2376
2363
|
getFinalProps
|
|
2377
2364
|
}
|
|
2378
2365
|
});
|
|
2379
|
-
return buildSsrResponse(
|
|
2366
|
+
return buildSsrResponse(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspend);
|
|
2380
2367
|
} catch (err) {
|
|
2381
2368
|
console.error("[hadars] SSR render error:", err);
|
|
2382
2369
|
return new Response("Internal Server Error", { status: 500 });
|
package/dist/index.cjs
CHANGED
|
@@ -183,10 +183,7 @@ function initServerDataCache(data) {
|
|
|
183
183
|
function useServerData(key, fn) {
|
|
184
184
|
const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
|
|
185
185
|
if (typeof window !== "undefined") {
|
|
186
|
-
|
|
187
|
-
return clientServerDataCache.get(cacheKey);
|
|
188
|
-
}
|
|
189
|
-
return fn();
|
|
186
|
+
return clientServerDataCache.get(cacheKey);
|
|
190
187
|
}
|
|
191
188
|
const unsuspend = globalThis.__hadarsUnsuspend;
|
|
192
189
|
if (!unsuspend)
|
package/dist/index.d.ts
CHANGED
|
@@ -205,6 +205,9 @@ declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
|
205
205
|
* awaited, the cache entry is then cleared so that the next render re-calls
|
|
206
206
|
* `fn()` — at that point the Suspense hook returns synchronously.
|
|
207
207
|
*
|
|
208
|
+
* `fn` is **server-only**: it is never called in the browser. The resolved value
|
|
209
|
+
* is serialised into `__serverData` and returned from cache during hydration.
|
|
210
|
+
*
|
|
208
211
|
* @example
|
|
209
212
|
* const user = useServerData('current_user', () => db.getUser(id));
|
|
210
213
|
* const post = useServerData(['post', postId], () => db.getPost(postId));
|
package/dist/index.js
CHANGED
|
@@ -140,10 +140,7 @@ function initServerDataCache(data) {
|
|
|
140
140
|
function useServerData(key, fn) {
|
|
141
141
|
const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
|
|
142
142
|
if (typeof window !== "undefined") {
|
|
143
|
-
|
|
144
|
-
return clientServerDataCache.get(cacheKey);
|
|
145
|
-
}
|
|
146
|
-
return fn();
|
|
143
|
+
return clientServerDataCache.get(cacheKey);
|
|
147
144
|
}
|
|
148
145
|
const unsuspend = globalThis.__hadarsUnsuspend;
|
|
149
146
|
if (!unsuspend)
|
|
@@ -148,10 +148,11 @@ function s() {
|
|
|
148
148
|
}
|
|
149
149
|
return g[GLOBAL_KEY];
|
|
150
150
|
}
|
|
151
|
-
function resetRenderState() {
|
|
151
|
+
function resetRenderState(idPrefix = "") {
|
|
152
152
|
const st = s();
|
|
153
153
|
st.currentTreeContext = { ...EMPTY };
|
|
154
154
|
st.localIdCounter = 0;
|
|
155
|
+
st.idPrefix = idPrefix;
|
|
155
156
|
}
|
|
156
157
|
function pushTreeContext(totalChildren, index) {
|
|
157
158
|
const st = s();
|
|
@@ -215,10 +216,10 @@ function makeId() {
|
|
|
215
216
|
const st = s();
|
|
216
217
|
const treeId = getTreeId();
|
|
217
218
|
const n = st.localIdCounter++;
|
|
218
|
-
let id = "
|
|
219
|
+
let id = "_R_" + st.idPrefix + treeId;
|
|
219
220
|
if (n > 0)
|
|
220
221
|
id += "H" + n.toString(32);
|
|
221
|
-
return id + "
|
|
222
|
+
return id + "_";
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// src/slim-react/hooks.ts
|
|
@@ -312,8 +313,8 @@ function createContext(defaultValue) {
|
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
// src/slim-react/dispatcher.ts
|
|
315
|
-
var
|
|
316
|
-
var _internals =
|
|
316
|
+
var ReactNS = __toESM(require("react"), 1);
|
|
317
|
+
var _internals = ReactNS.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
317
318
|
var slimDispatcher = {
|
|
318
319
|
useId: makeId,
|
|
319
320
|
readContext: (ctx) => getContextValue(ctx),
|
|
@@ -897,8 +898,8 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
897
898
|
const snap = snapshotContext();
|
|
898
899
|
while (attempts < MAX_SUSPENSE_RETRIES) {
|
|
899
900
|
restoreContext(snap);
|
|
901
|
+
let buffer = new BufferWriter();
|
|
900
902
|
try {
|
|
901
|
-
const buffer = new BufferWriter();
|
|
902
903
|
const r = renderNode(children, buffer, isSvg);
|
|
903
904
|
if (r && typeof r.then === "function") {
|
|
904
905
|
const m = captureMap();
|
|
@@ -932,12 +933,13 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
932
933
|
}
|
|
933
934
|
writer.write("<!--/$-->");
|
|
934
935
|
}
|
|
935
|
-
function renderToStream(element) {
|
|
936
|
+
function renderToStream(element, options) {
|
|
936
937
|
const encoder = new TextEncoder();
|
|
938
|
+
const idPrefix = options?.identifierPrefix ?? "";
|
|
937
939
|
const contextMap = /* @__PURE__ */ new Map();
|
|
938
940
|
return new ReadableStream({
|
|
939
941
|
async start(controller) {
|
|
940
|
-
resetRenderState();
|
|
942
|
+
resetRenderState(idPrefix);
|
|
941
943
|
const prev = swapContextMap(contextMap);
|
|
942
944
|
const writer = {
|
|
943
945
|
lastWasText: false,
|
|
@@ -966,12 +968,13 @@ function renderToStream(element) {
|
|
|
966
968
|
}
|
|
967
969
|
});
|
|
968
970
|
}
|
|
969
|
-
async function renderToString(element) {
|
|
971
|
+
async function renderToString(element, options) {
|
|
972
|
+
const idPrefix = options?.identifierPrefix ?? "";
|
|
970
973
|
const contextMap = /* @__PURE__ */ new Map();
|
|
971
974
|
const prev = swapContextMap(contextMap);
|
|
972
975
|
try {
|
|
973
976
|
for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
|
|
974
|
-
resetRenderState();
|
|
977
|
+
resetRenderState(idPrefix);
|
|
975
978
|
swapContextMap(contextMap);
|
|
976
979
|
const chunks = [];
|
|
977
980
|
const writer = {
|
|
@@ -80,19 +80,27 @@ declare function createContext<T>(defaultValue: T): Context<T>;
|
|
|
80
80
|
* until the async data is ready, then continues – exactly as requested.
|
|
81
81
|
*/
|
|
82
82
|
|
|
83
|
+
interface RenderOptions {
|
|
84
|
+
/**
|
|
85
|
+
* Must match the `identifierPrefix` option passed to `hydrateRoot` on the
|
|
86
|
+
* client so that `useId()` generates identical IDs on server and client.
|
|
87
|
+
* Defaults to `""` (React's default).
|
|
88
|
+
*/
|
|
89
|
+
identifierPrefix?: string;
|
|
90
|
+
}
|
|
83
91
|
/**
|
|
84
92
|
* Render a component tree to a `ReadableStream<Uint8Array>`.
|
|
85
93
|
*
|
|
86
94
|
* The stream pauses at `<Suspense>` boundaries until the suspended
|
|
87
95
|
* promise resolves, then continues writing HTML.
|
|
88
96
|
*/
|
|
89
|
-
declare function renderToStream(element: SlimNode): ReadableStream<Uint8Array>;
|
|
97
|
+
declare function renderToStream(element: SlimNode, options?: RenderOptions): ReadableStream<Uint8Array>;
|
|
90
98
|
/**
|
|
91
99
|
* Convenience: render to a complete HTML string.
|
|
92
100
|
* Retries the full tree when a component throws a Promise (Suspense protocol),
|
|
93
101
|
* so useServerData and similar hooks work without requiring explicit <Suspense>.
|
|
94
102
|
*/
|
|
95
|
-
declare function renderToString(element: SlimNode): Promise<string>;
|
|
103
|
+
declare function renderToString(element: SlimNode, options?: RenderOptions): Promise<string>;
|
|
96
104
|
|
|
97
105
|
/**
|
|
98
106
|
* slim-react – a lightweight, SSR-only React-compatible runtime.
|
|
@@ -179,4 +187,4 @@ declare const React: {
|
|
|
179
187
|
version: string;
|
|
180
188
|
};
|
|
181
189
|
|
|
182
|
-
export { Children, Component, Context, PureComponent, SlimElement, SlimNode, Suspense, cloneElement, createContext, createElement, React as default, forwardRef, isValidElement, lazy, memo, renderToStream as renderToReadableStream, renderToStream, renderToString, startTransition, use, useActionState, useCallback, useContext, useDebugValue, useDeferredValue, useEffect, useFormStatus, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useOptimistic, useReducer, useRef, useState, useSyncExternalStore, useTransition, version };
|
|
190
|
+
export { Children, Component, Context, PureComponent, RenderOptions, SlimElement, SlimNode, Suspense, cloneElement, createContext, createElement, React as default, forwardRef, isValidElement, lazy, memo, renderToStream as renderToReadableStream, renderToStream, renderToString, startTransition, use, useActionState, useCallback, useContext, useDebugValue, useDeferredValue, useEffect, useFormStatus, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useOptimistic, useReducer, useRef, useState, useSyncExternalStore, useTransition, version };
|
package/dist/slim-react/index.js
CHANGED
|
@@ -47,10 +47,11 @@ function s() {
|
|
|
47
47
|
}
|
|
48
48
|
return g[GLOBAL_KEY];
|
|
49
49
|
}
|
|
50
|
-
function resetRenderState() {
|
|
50
|
+
function resetRenderState(idPrefix = "") {
|
|
51
51
|
const st = s();
|
|
52
52
|
st.currentTreeContext = { ...EMPTY };
|
|
53
53
|
st.localIdCounter = 0;
|
|
54
|
+
st.idPrefix = idPrefix;
|
|
54
55
|
}
|
|
55
56
|
function pushTreeContext(totalChildren, index) {
|
|
56
57
|
const st = s();
|
|
@@ -114,10 +115,10 @@ function makeId() {
|
|
|
114
115
|
const st = s();
|
|
115
116
|
const treeId = getTreeId();
|
|
116
117
|
const n = st.localIdCounter++;
|
|
117
|
-
let id = "
|
|
118
|
+
let id = "_R_" + st.idPrefix + treeId;
|
|
118
119
|
if (n > 0)
|
|
119
120
|
id += "H" + n.toString(32);
|
|
120
|
-
return id + "
|
|
121
|
+
return id + "_";
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
// src/slim-react/hooks.ts
|
|
@@ -211,8 +212,8 @@ function createContext(defaultValue) {
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
// src/slim-react/dispatcher.ts
|
|
214
|
-
import
|
|
215
|
-
var _internals =
|
|
215
|
+
import * as ReactNS from "react";
|
|
216
|
+
var _internals = ReactNS.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
216
217
|
var slimDispatcher = {
|
|
217
218
|
useId: makeId,
|
|
218
219
|
readContext: (ctx) => getContextValue(ctx),
|
|
@@ -796,8 +797,8 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
796
797
|
const snap = snapshotContext();
|
|
797
798
|
while (attempts < MAX_SUSPENSE_RETRIES) {
|
|
798
799
|
restoreContext(snap);
|
|
800
|
+
let buffer = new BufferWriter();
|
|
799
801
|
try {
|
|
800
|
-
const buffer = new BufferWriter();
|
|
801
802
|
const r = renderNode(children, buffer, isSvg);
|
|
802
803
|
if (r && typeof r.then === "function") {
|
|
803
804
|
const m = captureMap();
|
|
@@ -831,12 +832,13 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
831
832
|
}
|
|
832
833
|
writer.write("<!--/$-->");
|
|
833
834
|
}
|
|
834
|
-
function renderToStream(element) {
|
|
835
|
+
function renderToStream(element, options) {
|
|
835
836
|
const encoder = new TextEncoder();
|
|
837
|
+
const idPrefix = options?.identifierPrefix ?? "";
|
|
836
838
|
const contextMap = /* @__PURE__ */ new Map();
|
|
837
839
|
return new ReadableStream({
|
|
838
840
|
async start(controller) {
|
|
839
|
-
resetRenderState();
|
|
841
|
+
resetRenderState(idPrefix);
|
|
840
842
|
const prev = swapContextMap(contextMap);
|
|
841
843
|
const writer = {
|
|
842
844
|
lastWasText: false,
|
|
@@ -865,12 +867,13 @@ function renderToStream(element) {
|
|
|
865
867
|
}
|
|
866
868
|
});
|
|
867
869
|
}
|
|
868
|
-
async function renderToString(element) {
|
|
870
|
+
async function renderToString(element, options) {
|
|
871
|
+
const idPrefix = options?.identifierPrefix ?? "";
|
|
869
872
|
const contextMap = /* @__PURE__ */ new Map();
|
|
870
873
|
const prev = swapContextMap(contextMap);
|
|
871
874
|
try {
|
|
872
875
|
for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
|
|
873
|
-
resetRenderState();
|
|
876
|
+
resetRenderState(idPrefix);
|
|
874
877
|
swapContextMap(contextMap);
|
|
875
878
|
const chunks = [];
|
|
876
879
|
const writer = {
|
|
@@ -48,7 +48,6 @@ var FRAGMENT_TYPE = Symbol.for("react.fragment");
|
|
|
48
48
|
var SUSPENSE_TYPE = Symbol.for("react.suspense");
|
|
49
49
|
|
|
50
50
|
// src/slim-react/jsx.ts
|
|
51
|
-
var Fragment = FRAGMENT_TYPE;
|
|
52
51
|
function createElement(type, props, ...children) {
|
|
53
52
|
const normalizedProps = { ...props || {} };
|
|
54
53
|
if (children.length === 1) {
|
|
@@ -105,10 +104,11 @@ function s() {
|
|
|
105
104
|
}
|
|
106
105
|
return g[GLOBAL_KEY];
|
|
107
106
|
}
|
|
108
|
-
function resetRenderState() {
|
|
107
|
+
function resetRenderState(idPrefix = "") {
|
|
109
108
|
const st = s();
|
|
110
109
|
st.currentTreeContext = { ...EMPTY };
|
|
111
110
|
st.localIdCounter = 0;
|
|
111
|
+
st.idPrefix = idPrefix;
|
|
112
112
|
}
|
|
113
113
|
function pushTreeContext(totalChildren, index) {
|
|
114
114
|
const st = s();
|
|
@@ -172,10 +172,10 @@ function makeId() {
|
|
|
172
172
|
const st = s();
|
|
173
173
|
const treeId = getTreeId();
|
|
174
174
|
const n = st.localIdCounter++;
|
|
175
|
-
let id = "
|
|
175
|
+
let id = "_R_" + st.idPrefix + treeId;
|
|
176
176
|
if (n > 0)
|
|
177
177
|
id += "H" + n.toString(32);
|
|
178
|
-
return id + "
|
|
178
|
+
return id + "_";
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// src/slim-react/hooks.ts
|
|
@@ -237,8 +237,8 @@ function use(usable) {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// src/slim-react/dispatcher.ts
|
|
240
|
-
import
|
|
241
|
-
var _internals =
|
|
240
|
+
import * as ReactNS from "react";
|
|
241
|
+
var _internals = ReactNS.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
242
242
|
var slimDispatcher = {
|
|
243
243
|
useId: makeId,
|
|
244
244
|
readContext: (ctx) => getContextValue(ctx),
|
|
@@ -822,8 +822,8 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
822
822
|
const snap = snapshotContext();
|
|
823
823
|
while (attempts < MAX_SUSPENSE_RETRIES) {
|
|
824
824
|
restoreContext(snap);
|
|
825
|
+
let buffer = new BufferWriter();
|
|
825
826
|
try {
|
|
826
|
-
const buffer = new BufferWriter();
|
|
827
827
|
const r = renderNode(children, buffer, isSvg);
|
|
828
828
|
if (r && typeof r.then === "function") {
|
|
829
829
|
const m = captureMap();
|
|
@@ -857,12 +857,13 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
857
857
|
}
|
|
858
858
|
writer.write("<!--/$-->");
|
|
859
859
|
}
|
|
860
|
-
async function renderToString(element) {
|
|
860
|
+
async function renderToString(element, options) {
|
|
861
|
+
const idPrefix = options?.identifierPrefix ?? "";
|
|
861
862
|
const contextMap = /* @__PURE__ */ new Map();
|
|
862
863
|
const prev = swapContextMap(contextMap);
|
|
863
864
|
try {
|
|
864
865
|
for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
|
|
865
|
-
resetRenderState();
|
|
866
|
+
resetRenderState(idPrefix);
|
|
866
867
|
swapContextMap(contextMap);
|
|
867
868
|
const chunks = [];
|
|
868
869
|
const writer = {
|
|
@@ -1003,26 +1004,16 @@ parentPort.on("message", async (msg) => {
|
|
|
1003
1004
|
return;
|
|
1004
1005
|
const { finalAppProps, clientProps, unsuspend, headHtml, status } = await runFullLifecycle(request);
|
|
1005
1006
|
const Component = _ssrMod.default;
|
|
1006
|
-
const page = createElement(
|
|
1007
|
-
Fragment,
|
|
1008
|
-
null,
|
|
1009
|
-
createElement("div", { id: "app" }, createElement(Component, finalAppProps)),
|
|
1010
|
-
createElement("script", {
|
|
1011
|
-
id: "hadars",
|
|
1012
|
-
type: "application/json",
|
|
1013
|
-
dangerouslySetInnerHTML: {
|
|
1014
|
-
__html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c")
|
|
1015
|
-
}
|
|
1016
|
-
})
|
|
1017
|
-
);
|
|
1018
1007
|
globalThis.__hadarsUnsuspend = unsuspend;
|
|
1019
|
-
let
|
|
1008
|
+
let appHtml;
|
|
1020
1009
|
try {
|
|
1021
|
-
|
|
1010
|
+
appHtml = await renderToString(createElement(Component, finalAppProps));
|
|
1022
1011
|
} finally {
|
|
1023
1012
|
globalThis.__hadarsUnsuspend = null;
|
|
1024
1013
|
}
|
|
1025
|
-
|
|
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>`;
|
|
1026
1017
|
parentPort.postMessage({ id, html, headHtml, status });
|
|
1027
1018
|
} catch (err) {
|
|
1028
1019
|
parentPort.postMessage({ id, error: err?.message ?? String(err) });
|
package/dist/utils/Head.tsx
CHANGED
|
@@ -204,6 +204,9 @@ export function initServerDataCache(data: Record<string, unknown>) {
|
|
|
204
204
|
* awaited, the cache entry is then cleared so that the next render re-calls
|
|
205
205
|
* `fn()` — at that point the Suspense hook returns synchronously.
|
|
206
206
|
*
|
|
207
|
+
* `fn` is **server-only**: it is never called in the browser. The resolved value
|
|
208
|
+
* is serialised into `__serverData` and returned from cache during hydration.
|
|
209
|
+
*
|
|
207
210
|
* @example
|
|
208
211
|
* const user = useServerData('current_user', () => db.getUser(id));
|
|
209
212
|
* const post = useServerData(['post', postId], () => db.getPost(postId));
|
|
@@ -213,14 +216,9 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
|
|
|
213
216
|
const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
|
|
214
217
|
|
|
215
218
|
if (typeof window !== 'undefined') {
|
|
216
|
-
// Client: if the server serialised a value for this key, return it directly
|
|
217
|
-
// (
|
|
218
|
-
|
|
219
|
-
return clientServerDataCache.get(cacheKey) as T | undefined;
|
|
220
|
-
}
|
|
221
|
-
// Key not serialised → Suspense hook case (e.g. useSuspenseQuery).
|
|
222
|
-
// Call fn() directly so the hook runs with its own hydrated cache.
|
|
223
|
-
return fn() as T | undefined;
|
|
219
|
+
// Client: if the server serialised a value for this key, return it directly.
|
|
220
|
+
// fn() is a server-only operation and must never run in the browser.
|
|
221
|
+
return clientServerDataCache.get(cacheKey) as T | undefined;
|
|
224
222
|
}
|
|
225
223
|
|
|
226
224
|
// Server: communicate via globalThis.__hadarsUnsuspend which is set by the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hadars",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"node": ">=18.0.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"react": "^19.
|
|
56
|
-
"react-dom": "^19.
|
|
55
|
+
"react": "^19.2.0",
|
|
56
|
+
"react-dom": "^19.2.0",
|
|
57
57
|
"typescript": "^5"
|
|
58
58
|
},
|
|
59
59
|
"peerDependenciesMeta": {
|
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
"@types/react": "^19.2.14",
|
|
74
74
|
"@types/react-dom": "^19.2.3",
|
|
75
75
|
"esbuild": "^0.19.12",
|
|
76
|
+
"react": "^19.2.4",
|
|
77
|
+
"react-dom": "^19.2.4",
|
|
76
78
|
"tsup": "^6.7.0",
|
|
77
79
|
"typescript": "^5.9.3"
|
|
78
80
|
},
|
package/src/build.ts
CHANGED
|
@@ -73,7 +73,7 @@ async function processHtmlTemplate(templatePath: string): Promise<string> {
|
|
|
73
73
|
const HEAD_MARKER = '<meta name="HADARS_HEAD">';
|
|
74
74
|
const BODY_MARKER = '<meta name="HADARS_BODY">';
|
|
75
75
|
|
|
76
|
-
import { renderToString as slimRenderToString } from './slim-react/index';
|
|
76
|
+
import { renderToString as slimRenderToString, createElement } from './slim-react/index';
|
|
77
77
|
|
|
78
78
|
// Round-robin thread pool for SSR rendering — used on Bun/Deno where
|
|
79
79
|
// node:cluster is not available but node:worker_threads is.
|
|
@@ -194,7 +194,9 @@ class RenderWorkerPool {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
async function buildSsrResponse(
|
|
197
|
-
|
|
197
|
+
App: any,
|
|
198
|
+
appProps: Record<string, unknown>,
|
|
199
|
+
clientProps: Record<string, unknown>,
|
|
198
200
|
headHtml: string,
|
|
199
201
|
status: number,
|
|
200
202
|
getPrecontentHtml: (headHtml: string) => Promise<[string, string]>,
|
|
@@ -210,12 +212,15 @@ async function buildSsrResponse(
|
|
|
210
212
|
let bodyHtml: string;
|
|
211
213
|
try {
|
|
212
214
|
(globalThis as any).__hadarsUnsuspend = unsuspendForRender;
|
|
213
|
-
bodyHtml = await slimRenderToString(
|
|
215
|
+
bodyHtml = await slimRenderToString(createElement(App, appProps));
|
|
214
216
|
} finally {
|
|
215
217
|
(globalThis as any).__hadarsUnsuspend = null;
|
|
216
218
|
}
|
|
217
219
|
bodyHtml = processSegmentCache(bodyHtml);
|
|
218
|
-
|
|
220
|
+
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
|
|
221
|
+
controller.enqueue(encoder.encode(
|
|
222
|
+
`<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent
|
|
223
|
+
));
|
|
219
224
|
controller.close();
|
|
220
225
|
},
|
|
221
226
|
});
|
|
@@ -685,7 +690,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
685
690
|
getFinalProps,
|
|
686
691
|
} = (await import(importPath)) as HadarsEntryModule<any>;
|
|
687
692
|
|
|
688
|
-
const {
|
|
693
|
+
const { App, appProps, clientProps, unsuspend, status, headHtml } = await getReactResponse(request, {
|
|
689
694
|
document: {
|
|
690
695
|
body: Component as React.FC<HadarsProps<object>>,
|
|
691
696
|
lang: 'en',
|
|
@@ -695,7 +700,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
695
700
|
},
|
|
696
701
|
});
|
|
697
702
|
|
|
698
|
-
return buildSsrResponse(
|
|
703
|
+
return buildSsrResponse(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspend);
|
|
699
704
|
} catch (err: any) {
|
|
700
705
|
console.error('[hadars] SSR render error:', err);
|
|
701
706
|
const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, '<');
|
|
@@ -875,7 +880,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
875
880
|
});
|
|
876
881
|
}
|
|
877
882
|
|
|
878
|
-
const {
|
|
883
|
+
const { App, appProps, clientProps, unsuspend, status, headHtml } = await getReactResponse(request, {
|
|
879
884
|
document: {
|
|
880
885
|
body: Component as React.FC<HadarsProps<object>>,
|
|
881
886
|
lang: 'en',
|
|
@@ -885,7 +890,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
885
890
|
},
|
|
886
891
|
});
|
|
887
892
|
|
|
888
|
-
return buildSsrResponse(
|
|
893
|
+
return buildSsrResponse(App, appProps, clientProps, headHtml, status, getPrecontentHtml, unsuspend);
|
|
889
894
|
} catch (err: any) {
|
|
890
895
|
console.error('[hadars] SSR render error:', err);
|
|
891
896
|
return new Response('Internal Server Error', { status: 500 });
|
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
* call so that `React.useId()` routes through slim-react's tree-aware
|
|
10
10
|
* `makeId()`. All other hooks already have working SSR stubs in hooks.ts;
|
|
11
11
|
* they are forwarded here so libraries that call them via `React.*` also work.
|
|
12
|
+
*
|
|
13
|
+
* In the Rspack SSR bundle `react` is aliased to `slim-react`, which does NOT
|
|
14
|
+
* export `__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE`.
|
|
15
|
+
* Accessing that property via a namespace import (`import * as`) always returns
|
|
16
|
+
* `undefined` safely — `import default from "react"` would crash via interop
|
|
17
|
+
* because Rspack compiles it as `require("react").default`, and slim-react has
|
|
18
|
+
* no `default` export, so `.default` itself is `undefined` and the subsequent
|
|
19
|
+
* property access throws.
|
|
20
|
+
*
|
|
21
|
+
* With the namespace import, `_internals` is `undefined` in the SSR bundle and
|
|
22
|
+
* the install/restore functions become no-ops, which is correct: the SSR bundle
|
|
23
|
+
* already routes all `React.*` hook calls to slim-react's own implementations.
|
|
12
24
|
*/
|
|
13
25
|
|
|
14
26
|
import { makeId, getContextValue } from "./renderContext";
|
|
@@ -19,11 +31,14 @@ import {
|
|
|
19
31
|
useOptimistic, useActionState, use,
|
|
20
32
|
} from "./hooks";
|
|
21
33
|
|
|
22
|
-
import
|
|
34
|
+
// Use namespace import so that when `react` is aliased to slim-react in the
|
|
35
|
+
// Rspack SSR bundle, `ReactNS` is always an object (never undefined), and
|
|
36
|
+
// accessing a missing property returns `undefined` rather than throwing.
|
|
37
|
+
import * as ReactNS from "react";
|
|
23
38
|
|
|
24
39
|
// React 19 exposes its shared internals under this key.
|
|
25
40
|
const _internals: { H: object | null } | undefined =
|
|
26
|
-
(
|
|
41
|
+
(ReactNS as any).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
27
42
|
|
|
28
43
|
// The dispatcher object we install. We keep a stable reference so the same
|
|
29
44
|
// object is reused across every component call.
|
package/src/slim-react/index.ts
CHANGED
|
@@ -75,7 +75,7 @@ export function useContext<T>(context: Context<T>): T {
|
|
|
75
75
|
|
|
76
76
|
// ---- Rendering ----
|
|
77
77
|
import { renderToStream, renderToString, renderToReadableStream } from "./render";
|
|
78
|
-
export { renderToStream, renderToString, renderToReadableStream };
|
|
78
|
+
export { renderToStream, renderToString, renderToReadableStream, type RenderOptions } from "./render";
|
|
79
79
|
|
|
80
80
|
// ---- Suspense (as a JSX tag) ----
|
|
81
81
|
import { SUSPENSE_TYPE } from "./types";
|
package/src/slim-react/render.ts
CHANGED
|
@@ -812,8 +812,8 @@ async function renderSuspense(
|
|
|
812
812
|
while (attempts < MAX_SUSPENSE_RETRIES) {
|
|
813
813
|
// Restore context to the state it was in when we entered <Suspense>.
|
|
814
814
|
restoreContext(snap);
|
|
815
|
+
let buffer = new BufferWriter();
|
|
815
816
|
try {
|
|
816
|
-
const buffer = new BufferWriter();
|
|
817
817
|
const r = renderNode(children, buffer, isSvg);
|
|
818
818
|
if (r && typeof (r as any).then === "function") {
|
|
819
819
|
const m = captureMap(); await r; swapContextMap(m);
|
|
@@ -850,19 +850,32 @@ async function renderSuspense(
|
|
|
850
850
|
// Public API
|
|
851
851
|
// ---------------------------------------------------------------------------
|
|
852
852
|
|
|
853
|
+
export interface RenderOptions {
|
|
854
|
+
/**
|
|
855
|
+
* Must match the `identifierPrefix` option passed to `hydrateRoot` on the
|
|
856
|
+
* client so that `useId()` generates identical IDs on server and client.
|
|
857
|
+
* Defaults to `""` (React's default).
|
|
858
|
+
*/
|
|
859
|
+
identifierPrefix?: string;
|
|
860
|
+
}
|
|
861
|
+
|
|
853
862
|
/**
|
|
854
863
|
* Render a component tree to a `ReadableStream<Uint8Array>`.
|
|
855
864
|
*
|
|
856
865
|
* The stream pauses at `<Suspense>` boundaries until the suspended
|
|
857
866
|
* promise resolves, then continues writing HTML.
|
|
858
867
|
*/
|
|
859
|
-
export function renderToStream(
|
|
868
|
+
export function renderToStream(
|
|
869
|
+
element: SlimNode,
|
|
870
|
+
options?: RenderOptions,
|
|
871
|
+
): ReadableStream<Uint8Array> {
|
|
860
872
|
const encoder = new TextEncoder();
|
|
873
|
+
const idPrefix = options?.identifierPrefix ?? "";
|
|
861
874
|
|
|
862
875
|
const contextMap = new Map<object, unknown>();
|
|
863
876
|
return new ReadableStream({
|
|
864
877
|
async start(controller) {
|
|
865
|
-
resetRenderState();
|
|
878
|
+
resetRenderState(idPrefix);
|
|
866
879
|
const prev = swapContextMap(contextMap);
|
|
867
880
|
|
|
868
881
|
const writer: Writer = {
|
|
@@ -897,12 +910,16 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
|
897
910
|
* Retries the full tree when a component throws a Promise (Suspense protocol),
|
|
898
911
|
* so useServerData and similar hooks work without requiring explicit <Suspense>.
|
|
899
912
|
*/
|
|
900
|
-
export async function renderToString(
|
|
913
|
+
export async function renderToString(
|
|
914
|
+
element: SlimNode,
|
|
915
|
+
options?: RenderOptions,
|
|
916
|
+
): Promise<string> {
|
|
917
|
+
const idPrefix = options?.identifierPrefix ?? "";
|
|
901
918
|
const contextMap = new Map<object, unknown>();
|
|
902
919
|
const prev = swapContextMap(contextMap);
|
|
903
920
|
try {
|
|
904
921
|
for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
|
|
905
|
-
resetRenderState();
|
|
922
|
+
resetRenderState(idPrefix);
|
|
906
923
|
swapContextMap(contextMap); // re-activate our map on each retry
|
|
907
924
|
const chunks: string[] = [];
|
|
908
925
|
const writer: Writer = {
|
|
@@ -92,10 +92,11 @@ function s(): RenderState {
|
|
|
92
92
|
return g[GLOBAL_KEY] as RenderState;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export function resetRenderState() {
|
|
95
|
+
export function resetRenderState(idPrefix = "") {
|
|
96
96
|
const st = s();
|
|
97
97
|
st.currentTreeContext = { ...EMPTY };
|
|
98
98
|
st.localIdCounter = 0;
|
|
99
|
+
st.idPrefix = idPrefix;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
export function setIdPrefix(prefix: string) {
|
|
@@ -192,20 +193,19 @@ function getTreeId(): string {
|
|
|
192
193
|
/**
|
|
193
194
|
* Generate a `useId`-compatible ID for the current call site.
|
|
194
195
|
*
|
|
195
|
-
* Format:
|
|
196
|
+
* Format: `_R_<idPrefix><treeId>_` (React 19.2+)
|
|
196
197
|
* with an optional `H<n>` suffix for the n-th useId call in the same
|
|
197
198
|
* component (matching React 19's `localIdCounter` behaviour).
|
|
198
199
|
*
|
|
199
|
-
* React 19.
|
|
200
|
-
* This matches React 19.
|
|
201
|
-
*
|
|
202
|
-
* real React runtime during `hydrateRoot`.
|
|
200
|
+
* React 19.2 uses `_R_<id>_` (underscore-delimited).
|
|
201
|
+
* This matches React 19.2's output from both renderToString (Fizz) and
|
|
202
|
+
* hydrateRoot, so SSR-generated IDs agree with client React during hydration.
|
|
203
203
|
*/
|
|
204
204
|
export function makeId(): string {
|
|
205
205
|
const st = s();
|
|
206
206
|
const treeId = getTreeId();
|
|
207
207
|
const n = st.localIdCounter++;
|
|
208
|
-
let id = "
|
|
208
|
+
let id = "_R_" + st.idPrefix + treeId;
|
|
209
209
|
if (n > 0) id += "H" + n.toString(32);
|
|
210
|
-
return id + "
|
|
210
|
+
return id + "_";
|
|
211
211
|
}
|
package/src/ssr-render-worker.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { workerData, parentPort } from 'node:worker_threads';
|
|
14
14
|
import { pathToFileURL } from 'node:url';
|
|
15
15
|
import { processSegmentCache } from './utils/segmentCache';
|
|
16
|
-
import { renderToString, createElement
|
|
16
|
+
import { renderToString, createElement } from './slim-react/index';
|
|
17
17
|
|
|
18
18
|
const { ssrBundlePath } = workerData as { ssrBundlePath: string };
|
|
19
19
|
|
|
@@ -145,26 +145,21 @@ parentPort!.on('message', async (msg: any) => {
|
|
|
145
145
|
|
|
146
146
|
const Component = _ssrMod.default;
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
type: 'application/json',
|
|
153
|
-
dangerouslySetInnerHTML: {
|
|
154
|
-
__html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c'),
|
|
155
|
-
},
|
|
156
|
-
}),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
// Re-use the same cache so useServerData returns immediately (no re-fetch).
|
|
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.
|
|
160
152
|
(globalThis as any).__hadarsUnsuspend = unsuspend;
|
|
161
|
-
let
|
|
153
|
+
let appHtml: string;
|
|
162
154
|
try {
|
|
163
|
-
|
|
155
|
+
appHtml = await renderToString(createElement(Component, finalAppProps));
|
|
164
156
|
} finally {
|
|
165
157
|
(globalThis as any).__hadarsUnsuspend = null;
|
|
166
158
|
}
|
|
167
|
-
|
|
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>`;
|
|
168
163
|
|
|
169
164
|
parentPort!.postMessage({ id, html, headHtml, status });
|
|
170
165
|
|
package/src/utils/Head.tsx
CHANGED
|
@@ -204,6 +204,9 @@ export function initServerDataCache(data: Record<string, unknown>) {
|
|
|
204
204
|
* awaited, the cache entry is then cleared so that the next render re-calls
|
|
205
205
|
* `fn()` — at that point the Suspense hook returns synchronously.
|
|
206
206
|
*
|
|
207
|
+
* `fn` is **server-only**: it is never called in the browser. The resolved value
|
|
208
|
+
* is serialised into `__serverData` and returned from cache during hydration.
|
|
209
|
+
*
|
|
207
210
|
* @example
|
|
208
211
|
* const user = useServerData('current_user', () => db.getUser(id));
|
|
209
212
|
* const post = useServerData(['post', postId], () => db.getPost(postId));
|
|
@@ -213,14 +216,9 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
|
|
|
213
216
|
const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
|
|
214
217
|
|
|
215
218
|
if (typeof window !== 'undefined') {
|
|
216
|
-
// Client: if the server serialised a value for this key, return it directly
|
|
217
|
-
// (
|
|
218
|
-
|
|
219
|
-
return clientServerDataCache.get(cacheKey) as T | undefined;
|
|
220
|
-
}
|
|
221
|
-
// Key not serialised → Suspense hook case (e.g. useSuspenseQuery).
|
|
222
|
-
// Call fn() directly so the hook runs with its own hydrated cache.
|
|
223
|
-
return fn() as T | undefined;
|
|
219
|
+
// Client: if the server serialised a value for this key, return it directly.
|
|
220
|
+
// fn() is a server-only operation and must never run in the browser.
|
|
221
|
+
return clientServerDataCache.get(cacheKey) as T | undefined;
|
|
224
222
|
}
|
|
225
223
|
|
|
226
224
|
// Server: communicate via globalThis.__hadarsUnsuspend which is set by the
|
package/src/utils/response.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import type { AppHead, HadarsRequest, HadarsEntryBase, HadarsEntryModule, HadarsProps, AppContext } from "../types/hadars";
|
|
3
|
-
import { renderToString, createElement
|
|
3
|
+
import { renderToString, createElement } from '../slim-react/index';
|
|
4
4
|
|
|
5
5
|
interface ReactResponseOptions {
|
|
6
6
|
document: {
|
|
@@ -55,14 +55,12 @@ export const getReactResponse = async (
|
|
|
55
55
|
req: HadarsRequest,
|
|
56
56
|
opts: ReactResponseOptions,
|
|
57
57
|
): Promise<{
|
|
58
|
-
|
|
58
|
+
App: React.FC<any>,
|
|
59
|
+
appProps: Record<string, unknown>,
|
|
60
|
+
clientProps: Record<string, unknown>,
|
|
59
61
|
unsuspend: { cache: Map<string, any> },
|
|
60
62
|
status: number,
|
|
61
63
|
headHtml: string,
|
|
62
|
-
renderPayload: {
|
|
63
|
-
appProps: Record<string, unknown>;
|
|
64
|
-
clientProps: Record<string, unknown>;
|
|
65
|
-
};
|
|
66
64
|
}> => {
|
|
67
65
|
const App = opts.document.body;
|
|
68
66
|
const { getInitProps, getAfterRenderProps, getFinalProps } = opts.document;
|
|
@@ -105,27 +103,14 @@ export const getReactResponse = async (
|
|
|
105
103
|
...(Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}),
|
|
106
104
|
};
|
|
107
105
|
|
|
108
|
-
const
|
|
109
|
-
createElement('div', { id: 'app' },
|
|
110
|
-
createElement(App as any, { ...props, location: req.location, context } as any),
|
|
111
|
-
),
|
|
112
|
-
createElement('script', {
|
|
113
|
-
id: 'hadars',
|
|
114
|
-
type: 'application/json',
|
|
115
|
-
dangerouslySetInnerHTML: {
|
|
116
|
-
__html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c'),
|
|
117
|
-
},
|
|
118
|
-
}),
|
|
119
|
-
);
|
|
106
|
+
const appProps = { ...props, location: req.location, context } as unknown as Record<string, unknown>;
|
|
120
107
|
|
|
121
108
|
return {
|
|
122
|
-
|
|
109
|
+
App: App as React.FC<any>,
|
|
110
|
+
appProps,
|
|
111
|
+
clientProps: clientProps as Record<string, unknown>,
|
|
123
112
|
unsuspend,
|
|
124
113
|
status: context.head.status,
|
|
125
114
|
headHtml: getHeadHtml(context.head),
|
|
126
|
-
renderPayload: {
|
|
127
|
-
appProps: { ...props, location: req.location, context } as Record<string, unknown>,
|
|
128
|
-
clientProps: clientProps as Record<string, unknown>,
|
|
129
|
-
},
|
|
130
115
|
};
|
|
131
116
|
};
|