hadars 0.3.1 → 0.3.2

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.
@@ -38,7 +38,21 @@ import {
38
38
  captureMap,
39
39
  captureUnsuspend,
40
40
  restoreUnsuspend,
41
+ type ContextSnapshot,
41
42
  } from "./renderContext";
43
+
44
+ /**
45
+ * Capture all three concurrent-render globals in one call.
46
+ * Must be called immediately before every `await` and the returned token
47
+ * passed to restoreRenderCtx immediately after resuming — just like the
48
+ * individual captureMap / captureUnsuspend calls they replace.
49
+ */
50
+ function captureRenderCtx(): { m: ReturnType<typeof captureMap>; u: unknown; t: ContextSnapshot } {
51
+ return { m: captureMap(), u: captureUnsuspend(), t: snapshotContext() };
52
+ }
53
+ function restoreRenderCtx(ctx: ReturnType<typeof captureRenderCtx>): void {
54
+ swapContextMap(ctx.m); restoreUnsuspend(ctx.u); restoreContext(ctx.t);
55
+ }
42
56
  import { installDispatcher, restoreDispatcher } from "./dispatcher";
43
57
 
44
58
  // ---------------------------------------------------------------------------
@@ -627,9 +641,9 @@ function renderComponent(
627
641
  if (e && typeof (e as any).then === "function") {
628
642
  if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
629
643
  patchPromiseStatus(e as Promise<unknown>);
630
- const m = captureMap(); const u = captureUnsuspend();
644
+ const rctx = captureRenderCtx();
631
645
  return (e as Promise<unknown>).then(() => {
632
- swapContextMap(m); restoreUnsuspend(u);
646
+ restoreRenderCtx(rctx);
633
647
  return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
634
648
  });
635
649
  }
@@ -686,10 +700,10 @@ function renderComponent(
686
700
  };
687
701
  const r = renderChildren(props.children, writer, isSvg);
688
702
  if (r && typeof (r as any).then === "function") {
689
- const m = captureMap(); const u = captureUnsuspend();
703
+ const rctx = captureRenderCtx();
690
704
  return (r as Promise<void>).then(
691
- () => { swapContextMap(m); restoreUnsuspend(u); finish(); },
692
- (e) => { swapContextMap(m); restoreUnsuspend(u); finish(); throw e; },
705
+ () => { restoreRenderCtx(rctx); finish(); },
706
+ (e) => { restoreRenderCtx(rctx); finish(); throw e; },
693
707
  );
694
708
  }
695
709
  finish();
@@ -722,9 +736,9 @@ function renderComponent(
722
736
  if (e && typeof (e as any).then === "function") {
723
737
  if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
724
738
  patchPromiseStatus(e as Promise<unknown>);
725
- const m = captureMap(); const u = captureUnsuspend();
739
+ const rctx = captureRenderCtx();
726
740
  return (e as Promise<unknown>).then(() => {
727
- swapContextMap(m); restoreUnsuspend(u);
741
+ restoreRenderCtx(rctx);
728
742
  return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
729
743
  });
730
744
  }
@@ -744,9 +758,9 @@ function renderComponent(
744
758
 
745
759
  // Async component
746
760
  if (result instanceof Promise) {
747
- const m = captureMap(); const u = captureUnsuspend();
761
+ const rctx = captureRenderCtx();
748
762
  return result.then((resolved) => {
749
- swapContextMap(m); restoreUnsuspend(u);
763
+ restoreRenderCtx(rctx);
750
764
  // Check useId after the async body has finished executing.
751
765
  let asyncSavedIdTree: number | undefined;
752
766
  if (componentCalledUseId()) {
@@ -754,17 +768,17 @@ function renderComponent(
754
768
  }
755
769
  const r = renderNode(resolved, writer, isSvg);
756
770
  if (r && typeof (r as any).then === "function") {
757
- const m2 = captureMap(); const u2 = captureUnsuspend();
771
+ const rctx2 = captureRenderCtx();
758
772
  // Only allocate cleanup closures when actually going async.
759
773
  return (r as Promise<void>).then(
760
774
  () => {
761
- swapContextMap(m2); restoreUnsuspend(u2);
775
+ restoreRenderCtx(rctx2);
762
776
  if (asyncSavedIdTree !== undefined) popTreeContext(asyncSavedIdTree);
763
777
  popComponentScope(savedScope);
764
778
  if (isProvider) popContextValue(ctx, prevCtxValue);
765
779
  },
766
780
  (e) => {
767
- swapContextMap(m2); restoreUnsuspend(u2);
781
+ restoreRenderCtx(rctx2);
768
782
  if (asyncSavedIdTree !== undefined) popTreeContext(asyncSavedIdTree);
769
783
  popComponentScope(savedScope);
770
784
  if (isProvider) popContextValue(ctx, prevCtxValue);
@@ -777,7 +791,7 @@ function renderComponent(
777
791
  popComponentScope(savedScope);
778
792
  if (isProvider) popContextValue(ctx, prevCtxValue);
779
793
  }, (e) => {
780
- swapContextMap(m); restoreUnsuspend(u);
794
+ restoreRenderCtx(rctx);
781
795
  // savedIdTree is always undefined here (async component skips the push).
782
796
  popComponentScope(savedScope);
783
797
  if (isProvider) popContextValue(ctx, prevCtxValue);
@@ -788,17 +802,17 @@ function renderComponent(
788
802
  const r = renderNode(result, writer, isSvg);
789
803
 
790
804
  if (r && typeof (r as any).then === "function") {
791
- const m = captureMap(); const u = captureUnsuspend();
805
+ const rctx = captureRenderCtx();
792
806
  // Only allocate cleanup closures when actually going async.
793
807
  return (r as Promise<void>).then(
794
808
  () => {
795
- swapContextMap(m); restoreUnsuspend(u);
809
+ restoreRenderCtx(rctx);
796
810
  if (savedIdTree !== undefined) popTreeContext(savedIdTree);
797
811
  popComponentScope(savedScope);
798
812
  if (isProvider) popContextValue(ctx, prevCtxValue);
799
813
  },
800
814
  (e) => {
801
- swapContextMap(m); restoreUnsuspend(u);
815
+ restoreRenderCtx(rctx);
802
816
  if (savedIdTree !== undefined) popTreeContext(savedIdTree);
803
817
  popComponentScope(savedScope);
804
818
  if (isProvider) popContextValue(ctx, prevCtxValue);
@@ -839,9 +853,9 @@ function renderChildArrayFrom(
839
853
  const savedTree = pushTreeContext(totalChildren, i);
840
854
  const r = renderNode(child, writer, isSvg);
841
855
  if (r && typeof (r as any).then === "function") {
842
- const m = captureMap(); const u = captureUnsuspend();
856
+ const rctx = captureRenderCtx();
843
857
  return (r as Promise<void>).then(() => {
844
- swapContextMap(m); restoreUnsuspend(u);
858
+ restoreRenderCtx(rctx);
845
859
  popTreeContext(savedTree);
846
860
  return renderChildArrayFrom(children, i + 1, writer, isSvg);
847
861
  });
@@ -895,9 +909,9 @@ async function renderSuspense(
895
909
  try {
896
910
  const r = renderNode(children, buffer, isSvg);
897
911
  if (r && typeof (r as any).then === "function") {
898
- const m = captureMap(); const u = captureUnsuspend();
912
+ const rctx = captureRenderCtx();
899
913
  await r;
900
- swapContextMap(m); restoreUnsuspend(u);
914
+ restoreRenderCtx(rctx);
901
915
  }
902
916
  // Success – wrap with React's Suspense boundary markers so hydrateRoot
903
917
  // can locate the boundary in the DOM (<!--$--> … <!--/$-->).
@@ -918,9 +932,9 @@ async function renderSuspense(
918
932
  if (fallback) {
919
933
  const r = renderNode(fallback, writer, isSvg);
920
934
  if (r && typeof (r as any).then === "function") {
921
- const m = captureMap(); const u = captureUnsuspend();
935
+ const rctx = captureRenderCtx();
922
936
  await r;
923
- swapContextMap(m); restoreUnsuspend(u);
937
+ restoreRenderCtx(rctx);
924
938
  }
925
939
  }
926
940
  writer.write("<!--/$-->");
@@ -984,7 +998,7 @@ export function renderToStream(
984
998
  try {
985
999
  const r = renderNode(element, writer);
986
1000
  if (r && typeof (r as any).then === "function") {
987
- const m = captureMap(); await r; swapContextMap(m);
1001
+ const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
988
1002
  }
989
1003
  writer.flush!(); // encode everything accumulated (sync renders: the whole page)
990
1004
  controller.close();
@@ -1037,7 +1051,7 @@ export async function renderPreflight(
1037
1051
  // so a single pass is guaranteed to complete with all promises resolved.
1038
1052
  const r = renderNode(element, NULL_WRITER);
1039
1053
  if (r && typeof (r as any).then === "function") {
1040
- const m = captureMap(); await r; swapContextMap(m);
1054
+ const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
1041
1055
  }
1042
1056
  } finally {
1043
1057
  swapContextMap(prev);
@@ -1075,7 +1089,7 @@ export async function renderToString(
1075
1089
  // so a single pass is guaranteed to complete with all promises resolved.
1076
1090
  const r = renderNode(element, writer);
1077
1091
  if (r && typeof (r as any).then === "function") {
1078
- const m = captureMap(); await r; swapContextMap(m);
1092
+ const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
1079
1093
  }
1080
1094
  return output;
1081
1095
  } finally {
@@ -220,21 +220,44 @@ export function componentCalledUseId(): boolean {
220
220
  return s().localIdCounter > 0;
221
221
  }
222
222
 
223
- export function snapshotContext(): { tree: TreeContext; localId: number; treeDepth: number } {
224
- const st = s();
225
- const ctx = st.currentTreeContext;
226
- // Copy scalars directly — avoids a spread allocation.
227
- return { tree: { id: ctx.id, overflow: ctx.overflow }, localId: st.localIdCounter, treeDepth: _treeDepth };
223
+ export interface ContextSnapshot {
224
+ tree: TreeContext;
225
+ localId: number;
226
+ treeDepth: number;
227
+ /** Saved parent-context id values for the stack slots 0..treeDepth-1.
228
+ * Required so that concurrent renders cannot corrupt popTreeContext calls
229
+ * that run after an await continuation. */
230
+ idStack: number[];
231
+ ovStack: string[];
228
232
  }
229
233
 
230
- export function restoreContext(snap: { tree: TreeContext; localId: number; treeDepth: number }): void {
234
+ export function snapshotContext(): ContextSnapshot {
235
+ const st = s();
236
+ const ctx = st.currentTreeContext;
237
+ const depth = _treeDepth;
238
+ return {
239
+ tree: { id: ctx.id, overflow: ctx.overflow },
240
+ localId: st.localIdCounter,
241
+ treeDepth: depth,
242
+ // Snapshot the live stack so that popTreeContext reads correct saved values
243
+ // even if another concurrent render's resetRenderState stomped the arrays.
244
+ idStack: _treeIdStack.slice(0, depth),
245
+ ovStack: _treeOvStack.slice(0, depth),
246
+ };
247
+ }
248
+
249
+ export function restoreContext(snap: ContextSnapshot): void {
231
250
  const st = s();
232
251
  const ctx = st.currentTreeContext;
233
- // Mutate in place — avoids allocating a new TreeContext object.
234
- ctx.id = snap.tree.id;
235
- ctx.overflow = snap.tree.overflow;
236
- st.localIdCounter = snap.localId;
237
- _treeDepth = snap.treeDepth;
252
+ ctx.id = snap.tree.id;
253
+ ctx.overflow = snap.tree.overflow;
254
+ st.localIdCounter = snap.localId;
255
+ _treeDepth = snap.treeDepth;
256
+ // Restore the stack so subsequent popTreeContext calls see the right values.
257
+ for (let i = 0; i < snap.treeDepth; i++) {
258
+ _treeIdStack[i] = snap.idStack[i]!;
259
+ _treeOvStack[i] = snap.ovStack[i]!;
260
+ }
238
261
  }
239
262
 
240
263
  /**
@@ -338,6 +338,12 @@ const buildCompilerConfig = (
338
338
  // changed files, making repeat dev starts significantly faster.
339
339
  cache: true,
340
340
  externals,
341
+ // externalsPresets.node externalises ALL Node.js built-ins (bare names
342
+ // and the node: prefix) for both static and dynamic imports. This
343
+ // complements the explicit `externals` array: the preset handles the
344
+ // node: URI scheme that rspack cannot resolve as a file, while the
345
+ // array keeps '@emotion/server' as an explicit external.
346
+ ...(isServerBuild ? { externalsPresets: { node: true } } : {}),
341
347
  ...(optimization !== undefined ? { optimization } : {}),
342
348
  plugins: [
343
349
  !isServerBuild && new rspack.HtmlRspackPlugin({
@@ -393,7 +399,7 @@ const buildCompilerConfig = (
393
399
  // SSR watcher writing .hadars/index.ssr.js triggers the client compiler
394
400
  // and vice versa, causing an infinite rebuild loop.
395
401
  watchOptions: {
396
- ignored: ['**/node_modules/**', '**/.hadars/**'],
402
+ ignored: ['**/node_modules/**', '**/.hadars/**', '/tmp/**'],
397
403
  },
398
404
  };
399
405
  };
@@ -418,7 +424,7 @@ export const compileEntry = async (entry: string, opts: EntryOptions & { watch?:
418
424
  let first = true;
419
425
  // Pass ignored patterns directly — compiler.watch(watchOptions) replaces
420
426
  // the config-level watchOptions, so we must repeat them here.
421
- compiler.watch({ ignored: ['**/node_modules/**', '**/.hadars/**'] }, (err: any, stats: any) => {
427
+ compiler.watch({ ignored: ['**/node_modules/**', '**/.hadars/**', '/tmp/**'] }, (err: any, stats: any) => {
422
428
  if (err) {
423
429
  if (first) { first = false; reject(err); }
424
430
  else { console.error('rspack watch error', err); }
@@ -452,10 +458,7 @@ export const compileEntry = async (entry: string, opts: EntryOptions & { watch?:
452
458
 
453
459
  console.log(stats?.toString({
454
460
  colors: true,
455
- modules: true,
456
- children: true,
457
- chunks: true,
458
- chunkModules: true,
461
+ preset: 'minimal',
459
462
  }));
460
463
 
461
464
  resolve(stats);
@@ -89,6 +89,18 @@ export const makePrecontentHtmlGetter = (htmlFilePromise: Promise<string>) => {
89
89
  let preHead: string | null = null;
90
90
  let postHead: string | null = null;
91
91
  let postContent: string | null = null;
92
+
93
+ // Parse the template as soon as the file read completes — during the
94
+ // process's idle time before the first request arrives. This way the
95
+ // first real request finds preHead !== null and takes the sync path.
96
+ const primed = htmlFilePromise.then(html => {
97
+ const headEnd = html.indexOf(HEAD_MARKER);
98
+ const contentStart = html.indexOf(BODY_MARKER);
99
+ preHead = html.slice(0, headEnd);
100
+ postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
101
+ postContent = html.slice(contentStart + BODY_MARKER.length);
102
+ });
103
+
92
104
  // Returns synchronously once the template has been loaded and parsed
93
105
  // (every request after the first). Callers can check `instanceof Promise`
94
106
  // to take a zero-await hot path.
@@ -97,14 +109,7 @@ export const makePrecontentHtmlGetter = (htmlFilePromise: Promise<string>) => {
97
109
  // Hot path — sync return, no Promise allocation.
98
110
  return [preHead + headHtml + postHead!, postContent!];
99
111
  }
100
- return htmlFilePromise.then(html => {
101
- const headEnd = html.indexOf(HEAD_MARKER);
102
- const contentStart = html.indexOf(BODY_MARKER);
103
- preHead = html.slice(0, headEnd);
104
- postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
105
- postContent = html.slice(contentStart + BODY_MARKER.length);
106
- return [preHead + headHtml + postHead, postContent];
107
- });
112
+ return primed.then(() => [preHead! + headHtml + postHead!, postContent!]);
108
113
  };
109
114
  };
110
115