hadars 0.3.0 → 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.
@@ -134,7 +134,16 @@ function componentCalledUseId() {
134
134
  function snapshotContext() {
135
135
  const st = s();
136
136
  const ctx = st.currentTreeContext;
137
- return { tree: { id: ctx.id, overflow: ctx.overflow }, localId: st.localIdCounter, treeDepth: _treeDepth };
137
+ const depth = _treeDepth;
138
+ return {
139
+ tree: { id: ctx.id, overflow: ctx.overflow },
140
+ localId: st.localIdCounter,
141
+ treeDepth: depth,
142
+ // Snapshot the live stack so that popTreeContext reads correct saved values
143
+ // even if another concurrent render's resetRenderState stomped the arrays.
144
+ idStack: _treeIdStack.slice(0, depth),
145
+ ovStack: _treeOvStack.slice(0, depth)
146
+ };
138
147
  }
139
148
  function restoreContext(snap) {
140
149
  const st = s();
@@ -143,6 +152,10 @@ function restoreContext(snap) {
143
152
  ctx.overflow = snap.tree.overflow;
144
153
  st.localIdCounter = snap.localId;
145
154
  _treeDepth = snap.treeDepth;
155
+ for (let i = 0; i < snap.treeDepth; i++) {
156
+ _treeIdStack[i] = snap.idStack[i];
157
+ _treeOvStack[i] = snap.ovStack[i];
158
+ }
146
159
  }
147
160
  function getTreeId() {
148
161
  const { id, overflow } = s().currentTreeContext;
@@ -256,6 +269,14 @@ function restoreDispatcher(prev) {
256
269
  }
257
270
 
258
271
  // src/slim-react/render.ts
272
+ function captureRenderCtx() {
273
+ return { m: captureMap(), u: captureUnsuspend(), t: snapshotContext() };
274
+ }
275
+ function restoreRenderCtx(ctx) {
276
+ swapContextMap(ctx.m);
277
+ restoreUnsuspend(ctx.u);
278
+ restoreContext(ctx.t);
279
+ }
259
280
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
260
281
  "area",
261
282
  "base",
@@ -699,11 +720,9 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
699
720
  if (e && typeof e.then === "function") {
700
721
  if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
701
722
  patchPromiseStatus(e);
702
- const m = captureMap();
703
- const u = captureUnsuspend();
723
+ const rctx = captureRenderCtx();
704
724
  return e.then(() => {
705
- swapContextMap(m);
706
- restoreUnsuspend(u);
725
+ restoreRenderCtx(rctx);
707
726
  return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
708
727
  });
709
728
  }
@@ -740,17 +759,14 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
740
759
  };
741
760
  const r2 = renderChildren(props.children, writer, isSvg);
742
761
  if (r2 && typeof r2.then === "function") {
743
- const m = captureMap();
744
- const u = captureUnsuspend();
762
+ const rctx = captureRenderCtx();
745
763
  return r2.then(
746
764
  () => {
747
- swapContextMap(m);
748
- restoreUnsuspend(u);
765
+ restoreRenderCtx(rctx);
749
766
  finish();
750
767
  },
751
768
  (e) => {
752
- swapContextMap(m);
753
- restoreUnsuspend(u);
769
+ restoreRenderCtx(rctx);
754
770
  finish();
755
771
  throw e;
756
772
  }
@@ -779,11 +795,9 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
779
795
  if (e && typeof e.then === "function") {
780
796
  if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
781
797
  patchPromiseStatus(e);
782
- const m = captureMap();
783
- const u = captureUnsuspend();
798
+ const rctx = captureRenderCtx();
784
799
  return e.then(() => {
785
- swapContextMap(m);
786
- restoreUnsuspend(u);
800
+ restoreRenderCtx(rctx);
787
801
  return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
788
802
  });
789
803
  }
@@ -795,30 +809,25 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
795
809
  savedIdTree = pushTreeContext(1, 0);
796
810
  }
797
811
  if (result instanceof Promise) {
798
- const m = captureMap();
799
- const u = captureUnsuspend();
812
+ const rctx = captureRenderCtx();
800
813
  return result.then((resolved) => {
801
- swapContextMap(m);
802
- restoreUnsuspend(u);
814
+ restoreRenderCtx(rctx);
803
815
  let asyncSavedIdTree;
804
816
  if (componentCalledUseId()) {
805
817
  asyncSavedIdTree = pushTreeContext(1, 0);
806
818
  }
807
819
  const r2 = renderNode(resolved, writer, isSvg);
808
820
  if (r2 && typeof r2.then === "function") {
809
- const m2 = captureMap();
810
- const u2 = captureUnsuspend();
821
+ const rctx2 = captureRenderCtx();
811
822
  return r2.then(
812
823
  () => {
813
- swapContextMap(m2);
814
- restoreUnsuspend(u2);
824
+ restoreRenderCtx(rctx2);
815
825
  if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
816
826
  popComponentScope(savedScope);
817
827
  if (isProvider) popContextValue(ctx, prevCtxValue);
818
828
  },
819
829
  (e) => {
820
- swapContextMap(m2);
821
- restoreUnsuspend(u2);
830
+ restoreRenderCtx(rctx2);
822
831
  if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
823
832
  popComponentScope(savedScope);
824
833
  if (isProvider) popContextValue(ctx, prevCtxValue);
@@ -830,8 +839,7 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
830
839
  popComponentScope(savedScope);
831
840
  if (isProvider) popContextValue(ctx, prevCtxValue);
832
841
  }, (e) => {
833
- swapContextMap(m);
834
- restoreUnsuspend(u);
842
+ restoreRenderCtx(rctx);
835
843
  popComponentScope(savedScope);
836
844
  if (isProvider) popContextValue(ctx, prevCtxValue);
837
845
  throw e;
@@ -839,19 +847,16 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
839
847
  }
840
848
  const r = renderNode(result, writer, isSvg);
841
849
  if (r && typeof r.then === "function") {
842
- const m = captureMap();
843
- const u = captureUnsuspend();
850
+ const rctx = captureRenderCtx();
844
851
  return r.then(
845
852
  () => {
846
- swapContextMap(m);
847
- restoreUnsuspend(u);
853
+ restoreRenderCtx(rctx);
848
854
  if (savedIdTree !== void 0) popTreeContext(savedIdTree);
849
855
  popComponentScope(savedScope);
850
856
  if (isProvider) popContextValue(ctx, prevCtxValue);
851
857
  },
852
858
  (e) => {
853
- swapContextMap(m);
854
- restoreUnsuspend(u);
859
+ restoreRenderCtx(rctx);
855
860
  if (savedIdTree !== void 0) popTreeContext(savedIdTree);
856
861
  popComponentScope(savedScope);
857
862
  if (isProvider) popContextValue(ctx, prevCtxValue);
@@ -876,11 +881,9 @@ function renderChildArrayFrom(children, startIndex, writer, isSvg) {
876
881
  const savedTree = pushTreeContext(totalChildren, i);
877
882
  const r = renderNode(child, writer, isSvg);
878
883
  if (r && typeof r.then === "function") {
879
- const m = captureMap();
880
- const u = captureUnsuspend();
884
+ const rctx = captureRenderCtx();
881
885
  return r.then(() => {
882
- swapContextMap(m);
883
- restoreUnsuspend(u);
886
+ restoreRenderCtx(rctx);
884
887
  popTreeContext(savedTree);
885
888
  return renderChildArrayFrom(children, i + 1, writer, isSvg);
886
889
  });
@@ -904,11 +907,9 @@ async function renderSuspense(props, writer, isSvg = false) {
904
907
  try {
905
908
  const r = renderNode(children, buffer, isSvg);
906
909
  if (r && typeof r.then === "function") {
907
- const m = captureMap();
908
- const u = captureUnsuspend();
910
+ const rctx = captureRenderCtx();
909
911
  await r;
910
- swapContextMap(m);
911
- restoreUnsuspend(u);
912
+ restoreRenderCtx(rctx);
912
913
  }
913
914
  writer.write("<!--$-->");
914
915
  buffer.flushTo(writer);
@@ -922,11 +923,9 @@ async function renderSuspense(props, writer, isSvg = false) {
922
923
  if (fallback) {
923
924
  const r = renderNode(fallback, writer, isSvg);
924
925
  if (r && typeof r.then === "function") {
925
- const m = captureMap();
926
- const u = captureUnsuspend();
926
+ const rctx = captureRenderCtx();
927
927
  await r;
928
- swapContextMap(m);
929
- restoreUnsuspend(u);
928
+ restoreRenderCtx(rctx);
930
929
  }
931
930
  }
932
931
  writer.write("<!--/$-->");
@@ -955,9 +954,9 @@ async function renderToString(element, options) {
955
954
  resetRenderState(idPrefix);
956
955
  const r = renderNode(element, writer);
957
956
  if (r && typeof r.then === "function") {
958
- const m = captureMap();
957
+ const rctx = captureRenderCtx();
959
958
  await r;
960
- swapContextMap(m);
959
+ restoreRenderCtx(rctx);
961
960
  }
962
961
  return output;
963
962
  } finally {
package/dist/ssr-watch.js CHANGED
@@ -265,6 +265,12 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
265
265
  // changed files, making repeat dev starts significantly faster.
266
266
  cache: true,
267
267
  externals,
268
+ // externalsPresets.node externalises ALL Node.js built-ins (bare names
269
+ // and the node: prefix) for both static and dynamic imports. This
270
+ // complements the explicit `externals` array: the preset handles the
271
+ // node: URI scheme that rspack cannot resolve as a file, while the
272
+ // array keeps '@emotion/server' as an explicit external.
273
+ ...isServerBuild ? { externalsPresets: { node: true } } : {},
268
274
  ...optimization !== void 0 ? { optimization } : {},
269
275
  plugins: [
270
276
  !isServerBuild && new rspack.HtmlRspackPlugin({
@@ -317,7 +323,7 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
317
323
  // SSR watcher writing .hadars/index.ssr.js triggers the client compiler
318
324
  // and vice versa, causing an infinite rebuild loop.
319
325
  watchOptions: {
320
- ignored: ["**/node_modules/**", "**/.hadars/**"]
326
+ ignored: ["**/node_modules/**", "**/.hadars/**", "/tmp/**"]
321
327
  }
322
328
  };
323
329
  };
@@ -326,7 +332,7 @@ var compileEntry = async (entry2, opts) => {
326
332
  if (opts.watch) {
327
333
  await new Promise((resolve, reject) => {
328
334
  let first = true;
329
- compiler.watch({ ignored: ["**/node_modules/**", "**/.hadars/**"] }, (err, stats) => {
335
+ compiler.watch({ ignored: ["**/node_modules/**", "**/.hadars/**", "/tmp/**"] }, (err, stats) => {
330
336
  if (err) {
331
337
  if (first) {
332
338
  first = false;
@@ -359,10 +365,7 @@ var compileEntry = async (entry2, opts) => {
359
365
  }
360
366
  console.log(stats?.toString({
361
367
  colors: true,
362
- modules: true,
363
- children: true,
364
- chunks: true,
365
- chunkModules: true
368
+ preset: "minimal"
366
369
  }));
367
370
  resolve(stats);
368
371
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "scripts": {
42
42
  "build:lib": "tsup src/index.tsx src/lambda.ts src/cloudflare.ts src/slim-react/index.ts src/slim-react/jsx-runtime.ts --format esm,cjs --dts --out-dir dist --clean --external '@rspack/*' --external '@rspack/binding'",
43
43
  "build:cli": "node build-scripts/build-cli.mjs",
44
- "build:all": "npm run build:lib && npm run build:cli",
44
+ "build:all": "node build-scripts/build-all.mjs",
45
45
  "test": "bun test test/render-compare.test.tsx && bun test test/ssr.test.ts",
46
46
  "prepare": "npm run build:all",
47
47
  "prepublishOnly": "npm run build:all"
package/src/build.ts CHANGED
@@ -701,6 +701,14 @@ export const run = async (options: HadarsRuntimeOptions) => {
701
701
  fs.readFile(pathMod.join(__dirname, StaticPath, 'out.html'), 'utf-8')
702
702
  );
703
703
 
704
+ // Hoist and pre-import the SSR module at startup so the first request does
705
+ // not pay the module parse/eval cost. The file: URL is stable for the life
706
+ // of the process (no cache-busting needed in run mode).
707
+ const componentPath = pathToFileURL(
708
+ pathMod.resolve(__dirname, HadarsFolder, SSR_FILENAME)
709
+ ).href;
710
+ const ssrModulePromise = import(componentPath) as Promise<HadarsEntryModule<any>>;
711
+
704
712
  const runHandler: CacheFetchHandler = async (req, ctx) => {
705
713
  const request = parseRequest(req);
706
714
  if (handler) {
@@ -733,16 +741,12 @@ export const run = async (options: HadarsRuntimeOptions) => {
733
741
  if (routeRes) return routeRes;
734
742
  }
735
743
 
736
- const componentPath = pathToFileURL(
737
- pathMod.resolve(__dirname, HadarsFolder, SSR_FILENAME)
738
- ).href;
739
-
740
744
  try {
741
745
  const {
742
746
  default: Component,
743
747
  getInitProps,
744
748
  getFinalProps,
745
- } = (await import(componentPath)) as HadarsEntryModule<any>;
749
+ } = await ssrModulePromise;
746
750
 
747
751
  if (renderPool && request.headers.get('Accept') !== 'application/json') {
748
752
  // Worker runs the full lifecycle — no non-serializable objects cross the thread boundary.
package/src/lambda.ts CHANGED
@@ -193,20 +193,13 @@ export function createLambdaHandler(options: HadarsOptions, bundled?: LambdaBund
193
193
  ? makePrecontentHtmlGetter(Promise.resolve(bundled.outHtml))
194
194
  : makePrecontentHtmlGetter(fs.readFile(pathMod.join(cwd, StaticPath, 'out.html'), 'utf-8'));
195
195
 
196
- // Hoist the SSR module reference so it is resolved once, not on every
197
- // request. In bundled mode the module is already in-memory; in file-based
198
- // mode we lazily import it and cache the promise so Node's module cache is
199
- // only consulted once.
200
- let ssrModulePromise: Promise<HadarsEntryModule<any>> | null = null;
201
- const getSsrModule = (): Promise<HadarsEntryModule<any>> => {
202
- if (bundled) return Promise.resolve(bundled.ssrModule);
203
- if (!ssrModulePromise) {
204
- ssrModulePromise = import(
205
- pathToFileURL(pathMod.resolve(cwd, HadarsFolder, SSR_FILENAME)).href
206
- ) as Promise<HadarsEntryModule<any>>;
207
- }
208
- return ssrModulePromise;
209
- };
196
+ // Start loading the SSR module immediately during Lambda's init phase this
197
+ // is "free" time not billed against request latency. Lazy loading would
198
+ // push the module parse cost onto the first request.
199
+ const ssrModulePromise: Promise<HadarsEntryModule<any>> = bundled
200
+ ? Promise.resolve(bundled.ssrModule)
201
+ : import(pathToFileURL(pathMod.resolve(cwd, HadarsFolder, SSR_FILENAME)).href) as Promise<HadarsEntryModule<any>>;
202
+ const getSsrModule = () => ssrModulePromise;
210
203
 
211
204
  const runHandler = async (req: Request): Promise<Response> => {
212
205
  const request = parseRequest(req);
@@ -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);
@@ -58,33 +58,6 @@ function withRequestLogging(handler: FetchHandler): FetchHandler {
58
58
  };
59
59
  }
60
60
 
61
- const COMPRESSIBLE_RE = /^text\/|\/json|\/javascript|\/xml|\/wasm/;
62
-
63
- /**
64
- * Wraps a handler to apply on-the-fly gzip compression via CompressionStream.
65
- * Unlike a buffering approach, this pipes the response ReadableStream through
66
- * a CompressionStream so bytes are compressed and flushed as they arrive —
67
- * streaming responses remain streaming.
68
- */
69
- function withCompression(handler: FetchHandler): FetchHandler {
70
- return async (req, ctx) => {
71
- const res = await handler(req, ctx);
72
- if (!res || !res.body) return res;
73
- const accept = req.headers.get('Accept-Encoding') ?? '';
74
- if (!accept.includes('gzip')) return res;
75
- // Skip if already encoded (e.g. pre-compressed cache entry).
76
- if (res.headers.has('content-encoding')) return res;
77
- const ct = res.headers.get('content-type') ?? '';
78
- if (!COMPRESSIBLE_RE.test(ct)) return res;
79
- const compressed = res.body.pipeThrough(
80
- new (globalThis as any).CompressionStream('gzip'),
81
- );
82
- const headers = new Headers(res.headers);
83
- headers.set('content-encoding', 'gzip');
84
- headers.delete('content-length'); // length is unknown after compression
85
- return new Response(compressed, { status: res.status, headers });
86
- };
87
- }
88
61
 
89
62
  export async function serve(
90
63
  port: number,
@@ -92,7 +65,7 @@ export async function serve(
92
65
  /** Bun WebSocketHandler — ignored on Deno and Node.js. */
93
66
  websocket?: unknown,
94
67
  ): Promise<void> {
95
- fetchHandler = withCompression(withRequestLogging(fetchHandler));
68
+ fetchHandler = withRequestLogging(fetchHandler);
96
69
 
97
70
  // ── Bun ────────────────────────────────────────────────────────────────
98
71
  if (isBun) {