hadars 0.1.28 → 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.
@@ -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();
@@ -91,6 +92,9 @@ function pushComponentScope() {
91
92
  function popComponentScope(saved) {
92
93
  s().localIdCounter = saved;
93
94
  }
95
+ function componentCalledUseId() {
96
+ return s().localIdCounter > 0;
97
+ }
94
98
  function snapshotContext() {
95
99
  const st = s();
96
100
  return { tree: { ...st.currentTreeContext }, localId: st.localIdCounter };
@@ -111,7 +115,7 @@ function makeId() {
111
115
  const st = s();
112
116
  const treeId = getTreeId();
113
117
  const n = st.localIdCounter++;
114
- let id = "_" + st.idPrefix + "R_" + treeId;
118
+ let id = "_R_" + st.idPrefix + treeId;
115
119
  if (n > 0)
116
120
  id += "H" + n.toString(32);
117
121
  return id + "_";
@@ -207,6 +211,47 @@ function createContext(defaultValue) {
207
211
  return context;
208
212
  }
209
213
 
214
+ // src/slim-react/dispatcher.ts
215
+ import * as ReactNS from "react";
216
+ var _internals = ReactNS.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
217
+ var slimDispatcher = {
218
+ useId: makeId,
219
+ readContext: (ctx) => getContextValue(ctx),
220
+ useContext: (ctx) => getContextValue(ctx),
221
+ useState,
222
+ useReducer,
223
+ useEffect,
224
+ useLayoutEffect,
225
+ useInsertionEffect,
226
+ useRef,
227
+ useMemo,
228
+ useCallback,
229
+ useDebugValue,
230
+ useImperativeHandle,
231
+ useSyncExternalStore,
232
+ useTransition,
233
+ useDeferredValue,
234
+ useOptimistic,
235
+ useActionState,
236
+ use,
237
+ // React internals that compiled output may call
238
+ useMemoCache: (size) => new Array(size).fill(void 0),
239
+ useCacheRefresh: () => () => {
240
+ },
241
+ useHostTransitionStatus: () => false
242
+ };
243
+ function installDispatcher() {
244
+ if (!_internals)
245
+ return null;
246
+ const prev = _internals.H;
247
+ _internals.H = slimDispatcher;
248
+ return prev;
249
+ }
250
+ function restoreDispatcher(prev) {
251
+ if (_internals)
252
+ _internals.H = prev;
253
+ }
254
+
210
255
  // src/slim-react/render.ts
211
256
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
212
257
  "area",
@@ -610,6 +655,7 @@ function renderComponent(type, props, writer, isSvg) {
610
655
  return;
611
656
  }
612
657
  let result;
658
+ const prevDispatcher = installDispatcher();
613
659
  try {
614
660
  if (type.prototype && typeof type.prototype.render === "function") {
615
661
  const instance = new type(props);
@@ -623,12 +669,20 @@ function renderComponent(type, props, writer, isSvg) {
623
669
  result = type(props);
624
670
  }
625
671
  } catch (e) {
672
+ restoreDispatcher(prevDispatcher);
626
673
  popComponentScope(savedScope);
627
674
  if (isProvider)
628
675
  popContextValue(ctx, prevCtxValue);
629
676
  throw e;
630
677
  }
678
+ restoreDispatcher(prevDispatcher);
679
+ let savedIdTree;
680
+ if (!(result instanceof Promise) && componentCalledUseId()) {
681
+ savedIdTree = pushTreeContext(1, 0);
682
+ }
631
683
  const finish = () => {
684
+ if (savedIdTree !== void 0)
685
+ popTreeContext(savedIdTree);
632
686
  popComponentScope(savedScope);
633
687
  if (isProvider)
634
688
  popContextValue(ctx, prevCtxValue);
@@ -637,22 +691,33 @@ function renderComponent(type, props, writer, isSvg) {
637
691
  const m = captureMap();
638
692
  return result.then((resolved) => {
639
693
  swapContextMap(m);
694
+ let asyncSavedIdTree;
695
+ if (componentCalledUseId()) {
696
+ asyncSavedIdTree = pushTreeContext(1, 0);
697
+ }
698
+ const asyncFinish = () => {
699
+ if (asyncSavedIdTree !== void 0)
700
+ popTreeContext(asyncSavedIdTree);
701
+ popComponentScope(savedScope);
702
+ if (isProvider)
703
+ popContextValue(ctx, prevCtxValue);
704
+ };
640
705
  const r2 = renderNode(resolved, writer, isSvg);
641
706
  if (r2 && typeof r2.then === "function") {
642
707
  const m2 = captureMap();
643
708
  return r2.then(
644
709
  () => {
645
710
  swapContextMap(m2);
646
- finish();
711
+ asyncFinish();
647
712
  },
648
713
  (e) => {
649
714
  swapContextMap(m2);
650
- finish();
715
+ asyncFinish();
651
716
  throw e;
652
717
  }
653
718
  );
654
719
  }
655
- finish();
720
+ asyncFinish();
656
721
  }, (e) => {
657
722
  swapContextMap(m);
658
723
  finish();
@@ -732,8 +797,8 @@ async function renderSuspense(props, writer, isSvg = false) {
732
797
  const snap = snapshotContext();
733
798
  while (attempts < MAX_SUSPENSE_RETRIES) {
734
799
  restoreContext(snap);
800
+ let buffer = new BufferWriter();
735
801
  try {
736
- const buffer = new BufferWriter();
737
802
  const r = renderNode(children, buffer, isSvg);
738
803
  if (r && typeof r.then === "function") {
739
804
  const m = captureMap();
@@ -767,12 +832,13 @@ async function renderSuspense(props, writer, isSvg = false) {
767
832
  }
768
833
  writer.write("<!--/$-->");
769
834
  }
770
- function renderToStream(element) {
835
+ function renderToStream(element, options) {
771
836
  const encoder = new TextEncoder();
837
+ const idPrefix = options?.identifierPrefix ?? "";
772
838
  const contextMap = /* @__PURE__ */ new Map();
773
839
  return new ReadableStream({
774
840
  async start(controller) {
775
- resetRenderState();
841
+ resetRenderState(idPrefix);
776
842
  const prev = swapContextMap(contextMap);
777
843
  const writer = {
778
844
  lastWasText: false,
@@ -801,12 +867,13 @@ function renderToStream(element) {
801
867
  }
802
868
  });
803
869
  }
804
- async function renderToString(element) {
870
+ async function renderToString(element, options) {
871
+ const idPrefix = options?.identifierPrefix ?? "";
805
872
  const contextMap = /* @__PURE__ */ new Map();
806
873
  const prev = swapContextMap(contextMap);
807
874
  try {
808
875
  for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
809
- resetRenderState();
876
+ resetRenderState(idPrefix);
810
877
  swapContextMap(contextMap);
811
878
  const chunks = [];
812
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();
@@ -149,6 +149,9 @@ function pushComponentScope() {
149
149
  function popComponentScope(saved) {
150
150
  s().localIdCounter = saved;
151
151
  }
152
+ function componentCalledUseId() {
153
+ return s().localIdCounter > 0;
154
+ }
152
155
  function snapshotContext() {
153
156
  const st = s();
154
157
  return { tree: { ...st.currentTreeContext }, localId: st.localIdCounter };
@@ -158,6 +161,121 @@ function restoreContext(snap) {
158
161
  st.currentTreeContext = { ...snap.tree };
159
162
  st.localIdCounter = snap.localId;
160
163
  }
164
+ function getTreeId() {
165
+ const { id, overflow } = s().currentTreeContext;
166
+ if (id === 1)
167
+ return overflow;
168
+ const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
169
+ return stripped + overflow;
170
+ }
171
+ function makeId() {
172
+ const st = s();
173
+ const treeId = getTreeId();
174
+ const n = st.localIdCounter++;
175
+ let id = "_R_" + st.idPrefix + treeId;
176
+ if (n > 0)
177
+ id += "H" + n.toString(32);
178
+ return id + "_";
179
+ }
180
+
181
+ // src/slim-react/hooks.ts
182
+ function useState(initialState) {
183
+ const value = typeof initialState === "function" ? initialState() : initialState;
184
+ return [value, () => {
185
+ }];
186
+ }
187
+ function useReducer(_reducer, initialState) {
188
+ return [initialState, () => {
189
+ }];
190
+ }
191
+ function useEffect(_effect, _deps) {
192
+ }
193
+ function useLayoutEffect(_effect, _deps) {
194
+ }
195
+ function useInsertionEffect(_effect, _deps) {
196
+ }
197
+ function useRef(initialValue) {
198
+ return { current: initialValue };
199
+ }
200
+ function useMemo(factory, _deps) {
201
+ return factory();
202
+ }
203
+ function useCallback(callback, _deps) {
204
+ return callback;
205
+ }
206
+ function useDebugValue(_value, _format) {
207
+ }
208
+ function useImperativeHandle(_ref, _createHandle, _deps) {
209
+ }
210
+ function useSyncExternalStore(_subscribe, getSnapshot, getServerSnapshot) {
211
+ return (getServerSnapshot || getSnapshot)();
212
+ }
213
+ function useTransition() {
214
+ return [false, (cb) => cb()];
215
+ }
216
+ function useDeferredValue(value) {
217
+ return value;
218
+ }
219
+ function useOptimistic(passthrough) {
220
+ return [passthrough, () => {
221
+ }];
222
+ }
223
+ function useActionState(_action, initialState, _permalink) {
224
+ return [initialState, () => {
225
+ }, false];
226
+ }
227
+ function use(usable) {
228
+ if (typeof usable === "object" && usable !== null && ("_currentValue" in usable || "_defaultValue" in usable)) {
229
+ return getContextValue(usable);
230
+ }
231
+ const promise = usable;
232
+ if (promise.status === "fulfilled")
233
+ return promise.value;
234
+ if (promise.status === "rejected")
235
+ throw promise.reason;
236
+ throw promise;
237
+ }
238
+
239
+ // src/slim-react/dispatcher.ts
240
+ import * as ReactNS from "react";
241
+ var _internals = ReactNS.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
242
+ var slimDispatcher = {
243
+ useId: makeId,
244
+ readContext: (ctx) => getContextValue(ctx),
245
+ useContext: (ctx) => getContextValue(ctx),
246
+ useState,
247
+ useReducer,
248
+ useEffect,
249
+ useLayoutEffect,
250
+ useInsertionEffect,
251
+ useRef,
252
+ useMemo,
253
+ useCallback,
254
+ useDebugValue,
255
+ useImperativeHandle,
256
+ useSyncExternalStore,
257
+ useTransition,
258
+ useDeferredValue,
259
+ useOptimistic,
260
+ useActionState,
261
+ use,
262
+ // React internals that compiled output may call
263
+ useMemoCache: (size) => new Array(size).fill(void 0),
264
+ useCacheRefresh: () => () => {
265
+ },
266
+ useHostTransitionStatus: () => false
267
+ };
268
+ function installDispatcher() {
269
+ if (!_internals)
270
+ return null;
271
+ const prev = _internals.H;
272
+ _internals.H = slimDispatcher;
273
+ return prev;
274
+ }
275
+ function restoreDispatcher(prev) {
276
+ if (_internals)
277
+ _internals.H = prev;
278
+ }
161
279
 
162
280
  // src/slim-react/render.ts
163
281
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
@@ -562,6 +680,7 @@ function renderComponent(type, props, writer, isSvg) {
562
680
  return;
563
681
  }
564
682
  let result;
683
+ const prevDispatcher = installDispatcher();
565
684
  try {
566
685
  if (type.prototype && typeof type.prototype.render === "function") {
567
686
  const instance = new type(props);
@@ -575,12 +694,20 @@ function renderComponent(type, props, writer, isSvg) {
575
694
  result = type(props);
576
695
  }
577
696
  } catch (e) {
697
+ restoreDispatcher(prevDispatcher);
578
698
  popComponentScope(savedScope);
579
699
  if (isProvider)
580
700
  popContextValue(ctx, prevCtxValue);
581
701
  throw e;
582
702
  }
703
+ restoreDispatcher(prevDispatcher);
704
+ let savedIdTree;
705
+ if (!(result instanceof Promise) && componentCalledUseId()) {
706
+ savedIdTree = pushTreeContext(1, 0);
707
+ }
583
708
  const finish = () => {
709
+ if (savedIdTree !== void 0)
710
+ popTreeContext(savedIdTree);
584
711
  popComponentScope(savedScope);
585
712
  if (isProvider)
586
713
  popContextValue(ctx, prevCtxValue);
@@ -589,22 +716,33 @@ function renderComponent(type, props, writer, isSvg) {
589
716
  const m = captureMap();
590
717
  return result.then((resolved) => {
591
718
  swapContextMap(m);
719
+ let asyncSavedIdTree;
720
+ if (componentCalledUseId()) {
721
+ asyncSavedIdTree = pushTreeContext(1, 0);
722
+ }
723
+ const asyncFinish = () => {
724
+ if (asyncSavedIdTree !== void 0)
725
+ popTreeContext(asyncSavedIdTree);
726
+ popComponentScope(savedScope);
727
+ if (isProvider)
728
+ popContextValue(ctx, prevCtxValue);
729
+ };
592
730
  const r2 = renderNode(resolved, writer, isSvg);
593
731
  if (r2 && typeof r2.then === "function") {
594
732
  const m2 = captureMap();
595
733
  return r2.then(
596
734
  () => {
597
735
  swapContextMap(m2);
598
- finish();
736
+ asyncFinish();
599
737
  },
600
738
  (e) => {
601
739
  swapContextMap(m2);
602
- finish();
740
+ asyncFinish();
603
741
  throw e;
604
742
  }
605
743
  );
606
744
  }
607
- finish();
745
+ asyncFinish();
608
746
  }, (e) => {
609
747
  swapContextMap(m);
610
748
  finish();
@@ -684,8 +822,8 @@ async function renderSuspense(props, writer, isSvg = false) {
684
822
  const snap = snapshotContext();
685
823
  while (attempts < MAX_SUSPENSE_RETRIES) {
686
824
  restoreContext(snap);
825
+ let buffer = new BufferWriter();
687
826
  try {
688
- const buffer = new BufferWriter();
689
827
  const r = renderNode(children, buffer, isSvg);
690
828
  if (r && typeof r.then === "function") {
691
829
  const m = captureMap();
@@ -719,12 +857,13 @@ async function renderSuspense(props, writer, isSvg = false) {
719
857
  }
720
858
  writer.write("<!--/$-->");
721
859
  }
722
- async function renderToString(element) {
860
+ async function renderToString(element, options) {
861
+ const idPrefix = options?.identifierPrefix ?? "";
723
862
  const contextMap = /* @__PURE__ */ new Map();
724
863
  const prev = swapContextMap(contextMap);
725
864
  try {
726
865
  for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
727
- resetRenderState();
866
+ resetRenderState(idPrefix);
728
867
  swapContextMap(contextMap);
729
868
  const chunks = [];
730
869
  const writer = {
@@ -865,26 +1004,16 @@ parentPort.on("message", async (msg) => {
865
1004
  return;
866
1005
  const { finalAppProps, clientProps, unsuspend, headHtml, status } = await runFullLifecycle(request);
867
1006
  const Component = _ssrMod.default;
868
- const page = createElement(
869
- Fragment,
870
- null,
871
- createElement("div", { id: "app" }, createElement(Component, finalAppProps)),
872
- createElement("script", {
873
- id: "hadars",
874
- type: "application/json",
875
- dangerouslySetInnerHTML: {
876
- __html: JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c")
877
- }
878
- })
879
- );
880
1007
  globalThis.__hadarsUnsuspend = unsuspend;
881
- let html;
1008
+ let appHtml;
882
1009
  try {
883
- html = await renderToString(page);
1010
+ appHtml = await renderToString(createElement(Component, finalAppProps));
884
1011
  } finally {
885
1012
  globalThis.__hadarsUnsuspend = null;
886
1013
  }
887
- html = processSegmentCache(html);
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>`;
888
1017
  parentPort.postMessage({ id, html, headHtml, status });
889
1018
  } catch (err) {
890
1019
  parentPort.postMessage({ id, error: err?.message ?? String(err) });
@@ -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
- // (normal async case no re-fetch in the browser).
218
- if (clientServerDataCache.has(cacheKey)) {
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.28",
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.1.1",
56
- "react-dom": "^19.1.1",
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
- ReactPage: any,
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(ReactPage);
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
- controller.enqueue(encoder.encode(bodyHtml + postContent));
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 { ReactPage, unsuspend, status, headHtml } = await getReactResponse(request, {
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(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
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, '&lt;');
@@ -875,7 +880,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
875
880
  });
876
881
  }
877
882
 
878
- const { ReactPage, unsuspend, status, headHtml } = await getReactResponse(request, {
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(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
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 });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * React dispatcher shim for slim-react SSR.
3
+ *
4
+ * During a slim-react render, `React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H`
5
+ * is null, so any component that calls `React.useId()` (or another hook) via
6
+ * React's own package will hit `resolveDispatcher()` → null → error.
7
+ *
8
+ * We install a minimal dispatcher object for the duration of each component
9
+ * call so that `React.useId()` routes through slim-react's tree-aware
10
+ * `makeId()`. All other hooks already have working SSR stubs in hooks.ts;
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.
24
+ */
25
+
26
+ import { makeId, getContextValue } from "./renderContext";
27
+ import {
28
+ useState, useReducer, useEffect, useLayoutEffect, useInsertionEffect,
29
+ useRef, useMemo, useCallback, useDebugValue, useImperativeHandle,
30
+ useSyncExternalStore, useTransition, useDeferredValue,
31
+ useOptimistic, useActionState, use,
32
+ } from "./hooks";
33
+
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";
38
+
39
+ // React 19 exposes its shared internals under this key.
40
+ const _internals: { H: object | null } | undefined =
41
+ (ReactNS as any).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
42
+
43
+ // The dispatcher object we install. We keep a stable reference so the same
44
+ // object is reused across every component call.
45
+ const slimDispatcher: Record<string, unknown> = {
46
+ useId: makeId,
47
+ readContext: (ctx: any) => getContextValue(ctx),
48
+ useContext: (ctx: any) => getContextValue(ctx),
49
+ useState,
50
+ useReducer,
51
+ useEffect,
52
+ useLayoutEffect,
53
+ useInsertionEffect,
54
+ useRef,
55
+ useMemo,
56
+ useCallback,
57
+ useDebugValue,
58
+ useImperativeHandle,
59
+ useSyncExternalStore,
60
+ useTransition,
61
+ useDeferredValue,
62
+ useOptimistic,
63
+ useActionState,
64
+ use,
65
+ // React internals that compiled output may call
66
+ useMemoCache: (size: number) => new Array(size).fill(undefined),
67
+ useCacheRefresh: () => () => {},
68
+ useHostTransitionStatus: () => false,
69
+ };
70
+
71
+ /**
72
+ * Install the slim dispatcher and return the previous value.
73
+ * Call `restoreDispatcher(prev)` when the component finishes.
74
+ */
75
+ export function installDispatcher(): object | null {
76
+ if (!_internals) return null;
77
+ const prev = _internals.H;
78
+ _internals.H = slimDispatcher;
79
+ return prev;
80
+ }
81
+
82
+ export function restoreDispatcher(prev: object | null): void {
83
+ if (_internals) _internals.H = prev;
84
+ }
@@ -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";