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.
- package/dist/cli.js +162 -37
- 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 +87 -10
- package/dist/slim-react/index.d.ts +11 -3
- package/dist/slim-react/index.js +77 -10
- package/dist/ssr-render-worker.js +152 -23
- package/dist/utils/Head.tsx +6 -8
- package/package.json +5 -3
- package/src/build.ts +13 -8
- package/src/slim-react/dispatcher.ts +84 -0
- package/src/slim-react/index.ts +1 -1
- package/src/slim-react/render.ts +52 -8
- package/src/slim-react/renderContext.ts +14 -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/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();
|
|
@@ -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 = "
|
|
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
|
-
|
|
711
|
+
asyncFinish();
|
|
647
712
|
},
|
|
648
713
|
(e) => {
|
|
649
714
|
swapContextMap(m2);
|
|
650
|
-
|
|
715
|
+
asyncFinish();
|
|
651
716
|
throw e;
|
|
652
717
|
}
|
|
653
718
|
);
|
|
654
719
|
}
|
|
655
|
-
|
|
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
|
-
|
|
736
|
+
asyncFinish();
|
|
599
737
|
},
|
|
600
738
|
(e) => {
|
|
601
739
|
swapContextMap(m2);
|
|
602
|
-
|
|
740
|
+
asyncFinish();
|
|
603
741
|
throw e;
|
|
604
742
|
}
|
|
605
743
|
);
|
|
606
744
|
}
|
|
607
|
-
|
|
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
|
|
1008
|
+
let appHtml;
|
|
882
1009
|
try {
|
|
883
|
-
|
|
1010
|
+
appHtml = await renderToString(createElement(Component, finalAppProps));
|
|
884
1011
|
} finally {
|
|
885
1012
|
globalThis.__hadarsUnsuspend = null;
|
|
886
1013
|
}
|
|
887
|
-
|
|
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) });
|
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 });
|
|
@@ -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
|
+
}
|
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";
|