hadars 0.1.24 → 0.1.26

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.
@@ -33,7 +33,8 @@ import {
33
33
  pushContextValue,
34
34
  popContextValue,
35
35
  getContextValue,
36
- runWithContextStore,
36
+ swapContextMap,
37
+ captureMap,
37
38
  } from "./renderContext";
38
39
 
39
40
  // ---------------------------------------------------------------------------
@@ -613,7 +614,11 @@ function renderComponent(
613
614
  };
614
615
  const r = renderChildren(props.children, writer, isSvg);
615
616
  if (r && typeof (r as any).then === "function") {
616
- return (r as Promise<void>).then(finish, (e) => { finish(); throw e; });
617
+ const m = captureMap();
618
+ return (r as Promise<void>).then(
619
+ () => { swapContextMap(m); finish(); },
620
+ (e) => { swapContextMap(m); finish(); throw e; },
621
+ );
617
622
  }
618
623
  finish();
619
624
  return;
@@ -645,19 +650,29 @@ function renderComponent(
645
650
 
646
651
  // Async component
647
652
  if (result instanceof Promise) {
653
+ const m = captureMap();
648
654
  return result.then((resolved) => {
655
+ swapContextMap(m);
649
656
  const r = renderNode(resolved, writer, isSvg);
650
657
  if (r && typeof (r as any).then === "function") {
651
- return (r as Promise<void>).then(finish, (e) => { finish(); throw e; });
658
+ const m2 = captureMap();
659
+ return (r as Promise<void>).then(
660
+ () => { swapContextMap(m2); finish(); },
661
+ (e) => { swapContextMap(m2); finish(); throw e; },
662
+ );
652
663
  }
653
664
  finish();
654
- }, (e) => { finish(); throw e; });
665
+ }, (e) => { swapContextMap(m); finish(); throw e; });
655
666
  }
656
667
 
657
668
  const r = renderNode(result, writer, isSvg);
658
669
 
659
670
  if (r && typeof (r as any).then === "function") {
660
- return (r as Promise<void>).then(finish, (e) => { finish(); throw e; });
671
+ const m = captureMap();
672
+ return (r as Promise<void>).then(
673
+ () => { swapContextMap(m); finish(); },
674
+ (e) => { swapContextMap(m); finish(); throw e; },
675
+ );
661
676
  }
662
677
  finish();
663
678
  }
@@ -691,7 +706,9 @@ function renderChildArray(
691
706
  const r = renderNode(children[i], writer, isSvg);
692
707
  if (r && typeof (r as any).then === "function") {
693
708
  // One child went async – continue the rest asynchronously
709
+ const m = captureMap();
694
710
  return (r as Promise<void>).then(() => {
711
+ swapContextMap(m);
695
712
  popTreeContext(savedTree);
696
713
  // Continue with remaining children
697
714
  return renderChildArrayFrom(children, i + 1, writer, isSvg);
@@ -716,7 +733,9 @@ function renderChildArrayFrom(
716
733
  const savedTree = pushTreeContext(totalChildren, i);
717
734
  const r = renderNode(children[i], writer, isSvg);
718
735
  if (r && typeof (r as any).then === "function") {
736
+ const m = captureMap();
719
737
  return (r as Promise<void>).then(() => {
738
+ swapContextMap(m);
720
739
  popTreeContext(savedTree);
721
740
  return renderChildArrayFrom(children, i + 1, writer, isSvg);
722
741
  });
@@ -765,7 +784,7 @@ async function renderSuspense(
765
784
  const buffer = new BufferWriter();
766
785
  const r = renderNode(children, buffer, isSvg);
767
786
  if (r && typeof (r as any).then === "function") {
768
- await r;
787
+ const m = captureMap(); await r; swapContextMap(m);
769
788
  }
770
789
  // Success – wrap with React's Suspense boundary markers so hydrateRoot
771
790
  // can locate the boundary in the DOM (<!--$--> … <!--/$-->).
@@ -775,7 +794,7 @@ async function renderSuspense(
775
794
  return;
776
795
  } catch (error: unknown) {
777
796
  if (error && typeof (error as any).then === "function") {
778
- await (error as Promise<unknown>);
797
+ const m = captureMap(); await (error as Promise<unknown>); swapContextMap(m);
779
798
  attempts++;
780
799
  } else {
781
800
  throw error;
@@ -788,7 +807,9 @@ async function renderSuspense(
788
807
  writer.write("<!--$?-->");
789
808
  if (fallback) {
790
809
  const r = renderNode(fallback, writer, isSvg);
791
- 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
+ }
792
813
  }
793
814
  writer.write("<!--/$-->");
794
815
  }
@@ -806,9 +827,11 @@ async function renderSuspense(
806
827
  export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
807
828
  const encoder = new TextEncoder();
808
829
 
809
- return runWithContextStore(() => new ReadableStream({
830
+ const contextMap = new Map<object, unknown>();
831
+ return new ReadableStream({
810
832
  async start(controller) {
811
833
  resetRenderState();
834
+ const prev = swapContextMap(contextMap);
812
835
 
813
836
  const writer: Writer = {
814
837
  lastWasText: false,
@@ -824,13 +847,17 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
824
847
 
825
848
  try {
826
849
  const r = renderNode(element, writer);
827
- 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
+ }
828
853
  controller.close();
829
854
  } catch (error) {
830
855
  controller.error(error);
856
+ } finally {
857
+ swapContextMap(prev);
831
858
  }
832
859
  },
833
- }));
860
+ });
834
861
  }
835
862
 
836
863
  /**
@@ -838,10 +865,13 @@ export function renderToStream(element: SlimNode): ReadableStream<Uint8Array> {
838
865
  * Retries the full tree when a component throws a Promise (Suspense protocol),
839
866
  * so useServerData and similar hooks work without requiring explicit <Suspense>.
840
867
  */
841
- export function renderToString(element: SlimNode): Promise<string> {
842
- return runWithContextStore(async () => {
868
+ export async function renderToString(element: SlimNode): Promise<string> {
869
+ const contextMap = new Map<object, unknown>();
870
+ const prev = swapContextMap(contextMap);
871
+ try {
843
872
  for (let attempt = 0; attempt < MAX_SUSPENSE_RETRIES; attempt++) {
844
873
  resetRenderState();
874
+ swapContextMap(contextMap); // re-activate our map on each retry
845
875
  const chunks: string[] = [];
846
876
  const writer: Writer = {
847
877
  lastWasText: false,
@@ -850,18 +880,22 @@ export function renderToString(element: SlimNode): Promise<string> {
850
880
  };
851
881
  try {
852
882
  const r = renderNode(element, writer);
853
- if (r && typeof (r as any).then === "function") await r;
883
+ if (r && typeof (r as any).then === "function") {
884
+ const m = captureMap(); await r; swapContextMap(m);
885
+ }
854
886
  return chunks.join("");
855
887
  } catch (error) {
856
888
  if (error && typeof (error as any).then === "function") {
857
- await (error as Promise<unknown>);
889
+ const m = captureMap(); await (error as Promise<unknown>); swapContextMap(m);
858
890
  continue;
859
891
  }
860
892
  throw error;
861
893
  }
862
894
  }
863
895
  throw new Error("[slim-react] renderToString exceeded maximum retries");
864
- });
896
+ } finally {
897
+ swapContextMap(prev);
898
+ }
865
899
  }
866
900
 
867
901
  /** Alias matching React 18+ server API naming. */
@@ -5,66 +5,62 @@
5
5
  * multiple slim-react instances (the render worker's direct import and the
6
6
  * SSR bundle's bundled copy) share the same singletons without coordination.
7
7
  *
8
- * Context values are stored in an AsyncLocalStorage<Map> so each concurrent
9
- * SSR request gets its own isolated scope that propagates through all awaits.
10
- * Call `runWithContextStore` at the start of every render to establish the scope.
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.
11
13
  */
12
14
 
13
- // Shared AsyncLocalStorage instance kept on globalThis so both copies of
14
- // slim-react (direct import + SSR bundle) use the same store.
15
- // The import is done with require() inside a try/catch so that bundlers that
16
- // cannot resolve node:async_hooks (e.g. rspack without target:node set) do
17
- // not fail at build time — the SSR render process always runs in Node.js and
18
- // will find the module at runtime regardless.
19
- const CONTEXT_STORE_KEY = "__slimReactContextStore";
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";
20
18
  const _g = globalThis as any;
21
- if (!_g[CONTEXT_STORE_KEY]) {
22
- try {
23
- // eslint-disable-next-line @typescript-eslint/no-require-imports
24
- const { AsyncLocalStorage } = require("node:async_hooks") as typeof import("node:async_hooks");
25
- _g[CONTEXT_STORE_KEY] = new AsyncLocalStorage<Map<object, unknown>>();
26
- } catch {
27
- // Fallback: no-op store — context values fall back to _defaultValue.
28
- // This should never happen in a real SSR environment.
29
- _g[CONTEXT_STORE_KEY] = null;
30
- }
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;
31
32
  }
32
- const _contextStore: { run<T>(store: Map<object,unknown>, fn: () => T): T; getStore(): Map<object,unknown> | undefined } | null = _g[CONTEXT_STORE_KEY];
33
33
 
34
- /** Wrap a render entry-point so it gets its own isolated context-value scope. */
35
- export function runWithContextStore<T>(fn: () => T): T {
36
- return _contextStore ? _contextStore.run(new Map(), fn) : fn();
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
37
  }
38
38
 
39
- /**
40
- * Read the current value for a context within the active render.
41
- * Falls back to `_defaultValue` (or `_currentValue` for external contexts).
42
- */
39
+ /** Read the current value for a context within the active render. */
43
40
  export function getContextValue<T>(context: object): T {
44
- const store = _contextStore?.getStore();
45
- if (store && store.has(context)) return store.get(context) as T;
41
+ const map: Map<object, unknown> | null = _g[MAP_KEY];
42
+ if (map && map.has(context)) return map.get(context) as T;
46
43
  const c = context as any;
47
44
  return ("_defaultValue" in c ? c._defaultValue : c._currentValue) as T;
48
45
  }
49
46
 
50
47
  /**
51
- * Push a new value for a context Provider onto the per-request store.
52
- * Returns the previous value so the caller can restore it later.
48
+ * Push a new Provider value into the active map.
49
+ * Returns the previous value so the caller can restore it on exit.
53
50
  */
54
51
  export function pushContextValue(context: object, value: unknown): unknown {
55
- const store = _contextStore?.getStore();
52
+ const map: Map<object, unknown> | null = _g[MAP_KEY];
56
53
  const c = context as any;
57
- const prev = store && store.has(context)
58
- ? store.get(context)
54
+ const prev = map && map.has(context)
55
+ ? map.get(context)
59
56
  : ("_defaultValue" in c ? c._defaultValue : c._currentValue);
60
- if (store) store.set(context, value);
57
+ map?.set(context, value);
61
58
  return prev;
62
59
  }
63
60
 
64
61
  /** Restore a previously saved context value (called by Provider on exit). */
65
62
  export function popContextValue(context: object, prev: unknown): void {
66
- const store = _contextStore?.getStore();
67
- if (store) store.set(context, prev);
63
+ (_g[MAP_KEY] as Map<object, unknown> | null)?.set(context, prev);
68
64
  }
69
65
 
70
66
  export interface TreeContext {
@@ -218,25 +218,18 @@ const buildCompilerConfig = (
218
218
  react: slimReactIndex,
219
219
  'react/jsx-runtime': slimReactJsx,
220
220
  'react/jsx-dev-runtime': slimReactJsx,
221
- // Keep emotion on the project's node_modules (server-safe entry).
222
- '@emotion/react': path.resolve(process.cwd(), 'node_modules', '@emotion', 'react'),
223
- '@emotion/server': path.resolve(process.cwd(), 'node_modules', '@emotion', 'server'),
224
- '@emotion/cache': path.resolve(process.cwd(), 'node_modules', '@emotion', 'cache'),
225
- '@emotion/styled': path.resolve(process.cwd(), 'node_modules', '@emotion', 'styled'),
221
+ // @emotion/* is bundled (not external) so that its `react` imports are
222
+ // resolved through the alias above to slim-react. If left external,
223
+ // emotion loads real React from node_modules and calls
224
+ // ReactSharedInternals.H.useContext which requires React's dispatcher.
226
225
  } : undefined;
227
226
 
228
227
  const externals = isServerBuild ? [
229
228
  // Node.js built-ins — must not be bundled; resolved by the runtime.
230
- // Both the bare name and the node: prefix are listed because rspack
231
- // may encounter either form depending on how the import is written.
232
- 'node:async_hooks', 'async_hooks',
233
229
  'node:fs', 'node:path', 'node:os', 'node:stream', 'node:util',
234
- // react / react-dom are replaced by slim-react via alias above — not external.
235
- // emotion should be external on server builds to avoid client/browser code
236
- '@emotion/react',
230
+ // @emotion/server is only used outside component rendering (CSS extraction)
231
+ // and does not call React hooks, so it is safe to leave as external.
237
232
  '@emotion/server',
238
- '@emotion/cache',
239
- '@emotion/styled',
240
233
  ] : undefined;
241
234
 
242
235
  const extraPlugins: any[] = [];
@@ -256,6 +249,9 @@ const buildCompilerConfig = (
256
249
  alias: resolveAliases,
257
250
  // for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
258
251
  mainFields: isServerBuild ? ['main', 'module'] : ['browser', 'module', 'main'],
252
+ // for server builds exclude the "browser" condition so packages with package.json
253
+ // "exports" conditions (e.g. @emotion/*) resolve their Node/CJS entry, not the browser build
254
+ ...(isServerBuild ? { conditionNames: ['node', 'require', 'default'] } : {}),
259
255
  };
260
256
 
261
257
  // Production client builds get vendor splitting and deterministic module IDs.
@@ -1,11 +0,0 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined")
5
- return require.apply(this, arguments);
6
- throw new Error('Dynamic require of "' + x + '" is not supported');
7
- });
8
-
9
- export {
10
- __require
11
- };