hadars 0.1.23 → 0.1.25
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 +131 -35
- package/dist/slim-react/index.cjs +137 -39
- package/dist/slim-react/index.d.ts +1 -0
- package/dist/slim-react/index.js +137 -39
- package/dist/ssr-render-worker.js +125 -35
- package/dist/ssr-watch.js +6 -0
- package/package.json +1 -1
- package/src/slim-react/context.ts +3 -1
- package/src/slim-react/hooks.ts +3 -3
- package/src/slim-react/index.ts +2 -4
- package/src/slim-react/render.ts +71 -32
- package/src/slim-react/renderContext.ts +59 -4
- package/src/utils/rspack.ts +2 -0
package/src/slim-react/render.ts
CHANGED
|
@@ -30,6 +30,11 @@ import {
|
|
|
30
30
|
popComponentScope,
|
|
31
31
|
snapshotContext,
|
|
32
32
|
restoreContext,
|
|
33
|
+
pushContextValue,
|
|
34
|
+
popContextValue,
|
|
35
|
+
getContextValue,
|
|
36
|
+
swapContextMap,
|
|
37
|
+
captureMap,
|
|
33
38
|
} from "./renderContext";
|
|
34
39
|
|
|
35
40
|
// ---------------------------------------------------------------------------
|
|
@@ -566,7 +571,7 @@ function renderComponent(
|
|
|
566
571
|
// React.Consumer (React 19) — call the children render prop with the current value
|
|
567
572
|
if (typeOf === REACT_CONSUMER) {
|
|
568
573
|
const ctx = (type as any)._context;
|
|
569
|
-
const value = ctx
|
|
574
|
+
const value = ctx ? getContextValue(ctx) : undefined;
|
|
570
575
|
const result: SlimNode =
|
|
571
576
|
typeof props.children === "function" ? props.children(value) : null;
|
|
572
577
|
const savedScope = pushComponentScope();
|
|
@@ -594,8 +599,7 @@ function renderComponent(
|
|
|
594
599
|
if (isProvider) {
|
|
595
600
|
// Resolve the actual context object from any provider variant
|
|
596
601
|
ctx = (type as any)._context ?? type;
|
|
597
|
-
prevCtxValue = ctx.
|
|
598
|
-
ctx._currentValue = props.value;
|
|
602
|
+
prevCtxValue = pushContextValue(ctx, props.value);
|
|
599
603
|
}
|
|
600
604
|
|
|
601
605
|
// Each component gets a fresh local-ID counter (for multiple useId calls).
|
|
@@ -606,11 +610,15 @@ function renderComponent(
|
|
|
606
610
|
if (isProvider && typeof type !== "function") {
|
|
607
611
|
const finish = () => {
|
|
608
612
|
popComponentScope(savedScope);
|
|
609
|
-
ctx
|
|
613
|
+
popContextValue(ctx, prevCtxValue);
|
|
610
614
|
};
|
|
611
615
|
const r = renderChildren(props.children, writer, isSvg);
|
|
612
616
|
if (r && typeof (r as any).then === "function") {
|
|
613
|
-
|
|
617
|
+
const m = captureMap();
|
|
618
|
+
return (r as Promise<void>).then(
|
|
619
|
+
() => { swapContextMap(m); finish(); },
|
|
620
|
+
(e) => { swapContextMap(m); finish(); throw e; },
|
|
621
|
+
);
|
|
614
622
|
}
|
|
615
623
|
finish();
|
|
616
624
|
return;
|
|
@@ -631,30 +639,40 @@ function renderComponent(
|
|
|
631
639
|
}
|
|
632
640
|
} catch (e) {
|
|
633
641
|
popComponentScope(savedScope);
|
|
634
|
-
if (isProvider) ctx
|
|
642
|
+
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
635
643
|
throw e;
|
|
636
644
|
}
|
|
637
645
|
|
|
638
646
|
const finish = () => {
|
|
639
647
|
popComponentScope(savedScope);
|
|
640
|
-
if (isProvider) ctx
|
|
648
|
+
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
641
649
|
};
|
|
642
650
|
|
|
643
651
|
// Async component
|
|
644
652
|
if (result instanceof Promise) {
|
|
653
|
+
const m = captureMap();
|
|
645
654
|
return result.then((resolved) => {
|
|
655
|
+
swapContextMap(m);
|
|
646
656
|
const r = renderNode(resolved, writer, isSvg);
|
|
647
657
|
if (r && typeof (r as any).then === "function") {
|
|
648
|
-
|
|
658
|
+
const m2 = captureMap();
|
|
659
|
+
return (r as Promise<void>).then(
|
|
660
|
+
() => { swapContextMap(m2); finish(); },
|
|
661
|
+
(e) => { swapContextMap(m2); finish(); throw e; },
|
|
662
|
+
);
|
|
649
663
|
}
|
|
650
664
|
finish();
|
|
651
|
-
});
|
|
665
|
+
}, (e) => { swapContextMap(m); finish(); throw e; });
|
|
652
666
|
}
|
|
653
667
|
|
|
654
668
|
const r = renderNode(result, writer, isSvg);
|
|
655
669
|
|
|
656
670
|
if (r && typeof (r as any).then === "function") {
|
|
657
|
-
|
|
671
|
+
const m = captureMap();
|
|
672
|
+
return (r as Promise<void>).then(
|
|
673
|
+
() => { swapContextMap(m); finish(); },
|
|
674
|
+
(e) => { swapContextMap(m); finish(); throw e; },
|
|
675
|
+
);
|
|
658
676
|
}
|
|
659
677
|
finish();
|
|
660
678
|
}
|
|
@@ -688,7 +706,9 @@ function renderChildArray(
|
|
|
688
706
|
const r = renderNode(children[i], writer, isSvg);
|
|
689
707
|
if (r && typeof (r as any).then === "function") {
|
|
690
708
|
// One child went async – continue the rest asynchronously
|
|
709
|
+
const m = captureMap();
|
|
691
710
|
return (r as Promise<void>).then(() => {
|
|
711
|
+
swapContextMap(m);
|
|
692
712
|
popTreeContext(savedTree);
|
|
693
713
|
// Continue with remaining children
|
|
694
714
|
return renderChildArrayFrom(children, i + 1, writer, isSvg);
|
|
@@ -713,7 +733,9 @@ function renderChildArrayFrom(
|
|
|
713
733
|
const savedTree = pushTreeContext(totalChildren, i);
|
|
714
734
|
const r = renderNode(children[i], writer, isSvg);
|
|
715
735
|
if (r && typeof (r as any).then === "function") {
|
|
736
|
+
const m = captureMap();
|
|
716
737
|
return (r as Promise<void>).then(() => {
|
|
738
|
+
swapContextMap(m);
|
|
717
739
|
popTreeContext(savedTree);
|
|
718
740
|
return renderChildArrayFrom(children, i + 1, writer, isSvg);
|
|
719
741
|
});
|
|
@@ -762,7 +784,7 @@ async function renderSuspense(
|
|
|
762
784
|
const buffer = new BufferWriter();
|
|
763
785
|
const r = renderNode(children, buffer, isSvg);
|
|
764
786
|
if (r && typeof (r as any).then === "function") {
|
|
765
|
-
await r;
|
|
787
|
+
const m = captureMap(); await r; swapContextMap(m);
|
|
766
788
|
}
|
|
767
789
|
// Success – wrap with React's Suspense boundary markers so hydrateRoot
|
|
768
790
|
// can locate the boundary in the DOM (<!--$--> … <!--/$-->).
|
|
@@ -772,7 +794,7 @@ async function renderSuspense(
|
|
|
772
794
|
return;
|
|
773
795
|
} catch (error: unknown) {
|
|
774
796
|
if (error && typeof (error as any).then === "function") {
|
|
775
|
-
await (error as Promise<unknown>);
|
|
797
|
+
const m = captureMap(); await (error as Promise<unknown>); swapContextMap(m);
|
|
776
798
|
attempts++;
|
|
777
799
|
} else {
|
|
778
800
|
throw error;
|
|
@@ -785,7 +807,9 @@ async function renderSuspense(
|
|
|
785
807
|
writer.write("<!--$?-->");
|
|
786
808
|
if (fallback) {
|
|
787
809
|
const r = renderNode(fallback, writer, isSvg);
|
|
788
|
-
if (r && typeof (r as any).then === "function")
|
|
810
|
+
if (r && typeof (r as any).then === "function") {
|
|
811
|
+
const m = captureMap(); await r; swapContextMap(m);
|
|
812
|
+
}
|
|
789
813
|
}
|
|
790
814
|
writer.write("<!--/$-->");
|
|
791
815
|
}
|
|
@@ -803,9 +827,11 @@ async function renderSuspense(
|
|
|
803
827
|
export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
804
828
|
const encoder = new TextEncoder();
|
|
805
829
|
|
|
830
|
+
const contextMap = new Map<object, unknown>();
|
|
806
831
|
return new ReadableStream({
|
|
807
832
|
async start(controller) {
|
|
808
833
|
resetRenderState();
|
|
834
|
+
const prev = swapContextMap(contextMap);
|
|
809
835
|
|
|
810
836
|
const writer: Writer = {
|
|
811
837
|
lastWasText: false,
|
|
@@ -821,10 +847,14 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
|
821
847
|
|
|
822
848
|
try {
|
|
823
849
|
const r = renderNode(element, writer);
|
|
824
|
-
if (r && typeof (r as any).then === "function")
|
|
850
|
+
if (r && typeof (r as any).then === "function") {
|
|
851
|
+
const m = captureMap(); await r; swapContextMap(m);
|
|
852
|
+
}
|
|
825
853
|
controller.close();
|
|
826
854
|
} catch (error) {
|
|
827
855
|
controller.error(error);
|
|
856
|
+
} finally {
|
|
857
|
+
swapContextMap(prev);
|
|
828
858
|
}
|
|
829
859
|
},
|
|
830
860
|
});
|
|
@@ -836,27 +866,36 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
|
|
|
836
866
|
* so useServerData and similar hooks work without requiring explicit <Suspense>.
|
|
837
867
|
*/
|
|
838
868
|
export async function renderToString(element: SlimNode): Promise<string> {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
869
|
+
const contextMap = new Map<object, unknown>();
|
|
870
|
+
const prev = swapContextMap(contextMap);
|
|
871
|
+
try {
|
|
872
|
+
for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
|
|
873
|
+
resetRenderState();
|
|
874
|
+
swapContextMap(contextMap); // re-activate our map on each retry
|
|
875
|
+
const chunks: string[] = [];
|
|
876
|
+
const writer: Writer = {
|
|
877
|
+
lastWasText: false,
|
|
878
|
+
write(c) { chunks.push(c); this.lastWasText = false; },
|
|
879
|
+
text(s) { chunks.push(s); this.lastWasText = true; },
|
|
880
|
+
};
|
|
881
|
+
try {
|
|
882
|
+
const r = renderNode(element, writer);
|
|
883
|
+
if (r && typeof (r as any).then === "function") {
|
|
884
|
+
const m = captureMap(); await r; swapContextMap(m);
|
|
885
|
+
}
|
|
886
|
+
return chunks.join("");
|
|
887
|
+
} catch (error) {
|
|
888
|
+
if (error && typeof (error as any).then === "function") {
|
|
889
|
+
const m = captureMap(); await (error as Promise<unknown>); swapContextMap(m);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
throw error;
|
|
855
893
|
}
|
|
856
|
-
throw error;
|
|
857
894
|
}
|
|
895
|
+
throw new Error("[slim-react] renderToString exceeded maximum retries");
|
|
896
|
+
} finally {
|
|
897
|
+
swapContextMap(prev);
|
|
858
898
|
}
|
|
859
|
-
throw new Error("[slim-react] renderToString exceeded maximum retries");
|
|
860
899
|
}
|
|
861
900
|
|
|
862
901
|
/** Alias matching React 18+ server API naming. */
|
|
@@ -1,12 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render-time context for tree-position-based `useId
|
|
2
|
+
* Render-time context for tree-position-based `useId` and React Context values.
|
|
3
3
|
*
|
|
4
4
|
* State lives on `globalThis` rather than module-level variables so that
|
|
5
5
|
* multiple slim-react instances (the render worker's direct import and the
|
|
6
|
-
* SSR bundle's bundled copy) share the same
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* SSR bundle's bundled copy) share the same singletons without coordination.
|
|
7
|
+
*
|
|
8
|
+
* Context values are stored in a plain Map that is created per render call.
|
|
9
|
+
* Node.js is single-threaded: only one render is executing between any two
|
|
10
|
+
* await points. The renderer captures the map reference before every await
|
|
11
|
+
* and restores it in the continuation, so concurrent renders stay isolated
|
|
12
|
+
* without any external dependencies.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// The context map for the render that is currently executing (between awaits).
|
|
16
|
+
// Kept on globalThis so both slim-react instances share the same slot.
|
|
17
|
+
const MAP_KEY = "__slimReactContextMap";
|
|
18
|
+
const _g = globalThis as any;
|
|
19
|
+
if (!("__slimReactContextMap" in _g)) _g[MAP_KEY] = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Swap in a new context map and return the previous one.
|
|
23
|
+
* Called at render entry-points and at every await/then continuation to
|
|
24
|
+
* restore the correct map for the resuming render.
|
|
25
|
+
*/
|
|
26
|
+
export function swapContextMap(
|
|
27
|
+
map: Map<object, unknown> | null,
|
|
28
|
+
): Map<object, unknown> | null {
|
|
29
|
+
const prev: Map<object, unknown> | null = _g[MAP_KEY];
|
|
30
|
+
_g[MAP_KEY] = map;
|
|
31
|
+
return prev;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Return the active map without changing it (used to capture before an await). */
|
|
35
|
+
export function captureMap(): Map<object, unknown> | null {
|
|
36
|
+
return _g[MAP_KEY];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read the current value for a context within the active render. */
|
|
40
|
+
export function getContextValue<T>(context: object): T {
|
|
41
|
+
const map: Map<object, unknown> | null = _g[MAP_KEY];
|
|
42
|
+
if (map && map.has(context)) return map.get(context) as T;
|
|
43
|
+
const c = context as any;
|
|
44
|
+
return ("_defaultValue" in c ? c._defaultValue : c._currentValue) as T;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Push a new Provider value into the active map.
|
|
49
|
+
* Returns the previous value so the caller can restore it on exit.
|
|
9
50
|
*/
|
|
51
|
+
export function pushContextValue(context: object, value: unknown): unknown {
|
|
52
|
+
const map: Map<object, unknown> | null = _g[MAP_KEY];
|
|
53
|
+
const c = context as any;
|
|
54
|
+
const prev = map && map.has(context)
|
|
55
|
+
? map.get(context)
|
|
56
|
+
: ("_defaultValue" in c ? c._defaultValue : c._currentValue);
|
|
57
|
+
map?.set(context, value);
|
|
58
|
+
return prev;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Restore a previously saved context value (called by Provider on exit). */
|
|
62
|
+
export function popContextValue(context: object, prev: unknown): void {
|
|
63
|
+
(_g[MAP_KEY] as Map<object, unknown> | null)?.set(context, prev);
|
|
64
|
+
}
|
|
10
65
|
|
|
11
66
|
export interface TreeContext {
|
|
12
67
|
id: number;
|
package/src/utils/rspack.ts
CHANGED
|
@@ -226,6 +226,8 @@ const buildCompilerConfig = (
|
|
|
226
226
|
} : undefined;
|
|
227
227
|
|
|
228
228
|
const externals = isServerBuild ? [
|
|
229
|
+
// Node.js built-ins — must not be bundled; resolved by the runtime.
|
|
230
|
+
'node:fs', 'node:path', 'node:os', 'node:stream', 'node:util',
|
|
229
231
|
// react / react-dom are replaced by slim-react via alias above — not external.
|
|
230
232
|
// emotion should be external on server builds to avoid client/browser code
|
|
231
233
|
'@emotion/react',
|