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.
@@ -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?._currentValue;
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._currentValue;
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._currentValue = prevCtxValue;
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
- return (r as Promise<void>).then(finish);
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._currentValue = prevCtxValue;
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._currentValue = prevCtxValue;
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
- return (r as Promise<void>).then(finish);
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
- return (r as Promise<void>).then(finish);
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") await r;
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") await r;
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
- for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
840
- resetRenderState();
841
- const chunks: string[] = [];
842
- const writer: Writer = {
843
- lastWasText: false,
844
- write(c) { chunks.push(c); this.lastWasText = false; },
845
- text(s) { chunks.push(s); this.lastWasText = true; },
846
- };
847
- try {
848
- const r = renderNode(element, writer);
849
- if (r && typeof (r as any).then === "function") await r;
850
- return chunks.join("");
851
- } catch (error) {
852
- if (error && typeof (error as any).then === "function") {
853
- await (error as Promise<unknown>);
854
- continue;
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 context without any coordination.
7
- * Safe because each worker processes one render at a time; `resetRenderState`
8
- * is always called at the top of every `renderToString` / `renderToStream`.
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;
@@ -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',