hadars 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -66,12 +66,32 @@ hadars run # multi-core when workers > 1
66
66
 
67
67
  - **React Fast Refresh** — full HMR via rspack-dev-server, module-level patches
68
68
  - **True SSR** — components render on the server with your data, then hydrate on the client
69
+ - **Shell streaming** — HTML shell is flushed immediately so browsers can start loading assets before the body arrives
69
70
  - **Code splitting** — `loadModule('./Comp')` splits on the browser, bundles statically on the server
70
71
  - **Head management** — `HadarsHead` controls `<title>`, `<meta>`, `<link>` on server and client
71
72
  - **Cross-runtime** — Bun, Node.js, Deno; uses the standard Fetch API throughout
72
73
  - **Multi-core** — `workers: os.cpus().length` forks a process per CPU core via `node:cluster`
73
74
  - **TypeScript-first** — full types for props, lifecycle hooks, config, and the request object
74
75
 
76
+ ## useServerData
77
+
78
+ Fetch async data inside a component during SSR. The framework's render loop awaits the promise and re-renders until all values are resolved, then serialises them into the page for zero-cost client hydration.
79
+
80
+ ```tsx
81
+ import { useServerData } from 'hadars';
82
+
83
+ const UserCard = ({ userId }: { userId: string }) => {
84
+ const user = useServerData(['user', userId], () => db.getUser(userId));
85
+ if (!user) return null; // undefined while pending on the first SSR pass
86
+ return <p>{user.name}</p>;
87
+ };
88
+ ```
89
+
90
+ - **`key`** — string or string array; must be stable and unique within the page
91
+ - **Server** — calls `fn()`, awaits the result across render iterations, returns `undefined` until resolved
92
+ - **Client** — reads the pre-resolved value from the hydration cache serialised by the server; `fn()` is never called in the browser
93
+ - **Suspense libraries** — also works when `fn()` throws a thenable (e.g. Relay `useLazyLoadQuery` with `suspense: true`); the thrown promise is awaited and the next render re-calls `fn()` synchronously
94
+
75
95
  ## Data lifecycle hooks
76
96
 
77
97
  | Hook | Runs on | Purpose |
@@ -97,20 +117,7 @@ hadars run # multi-core when workers > 1
97
117
  | `fetch` | `function` | — | Custom fetch handler; return a `Response` to short-circuit SSR |
98
118
  | `websocket` | `object` | — | WebSocket handler (Bun only) |
99
119
  | `wsPath` | `string` | `"/ws"` | Path that triggers WebSocket upgrade |
100
- | `streaming` | `boolean` | `false` | Set to `true` to use `renderToReadableStream` (streaming SSR) instead of the default `renderToString` |
101
-
102
- ## Local build
103
-
104
- ```bash
105
- npm install
106
- npm run build:all
107
- ```
108
-
109
- ## Publishing
110
-
111
- 1. Update `version`, `repository`, `license` in `package.json`
112
- 2. `npm login`
113
- 3. `npm publish`
120
+ | `optimization` | `object` | | Override rspack `optimization` for production client builds (merged on top of defaults) |
114
121
 
115
122
  ## License
116
123
 
package/dist/cli.js CHANGED
@@ -149,48 +149,48 @@ async function getStaticMarkupRenderer() {
149
149
  }
150
150
  return _renderToStaticMarkup;
151
151
  }
152
- var getHeadHtml = (seoData, renderToStaticMarkup) => {
153
- const metaEntries = Object.entries(seoData.meta);
154
- const linkEntries = Object.entries(seoData.link);
155
- const styleEntries = Object.entries(seoData.style);
156
- const scriptEntries = Object.entries(seoData.script);
157
- return renderToStaticMarkup(
158
- /* @__PURE__ */ jsxs(Fragment, { children: [
159
- /* @__PURE__ */ jsx("title", { children: seoData.title }),
160
- metaEntries.map(([id, options]) => /* @__PURE__ */ jsx(
161
- "meta",
162
- {
163
- id,
164
- ...options
165
- },
166
- id
167
- )),
168
- linkEntries.map(([id, options]) => /* @__PURE__ */ jsx(
169
- "link",
170
- {
171
- id,
172
- ...options
173
- },
174
- id
175
- )),
176
- styleEntries.map(([id, options]) => /* @__PURE__ */ jsx(
177
- "style",
178
- {
179
- id,
180
- ...options
181
- },
182
- id
183
- )),
184
- scriptEntries.map(([id, options]) => /* @__PURE__ */ jsx(
185
- "script",
186
- {
187
- id,
188
- ...options
189
- },
190
- id
191
- ))
192
- ] })
193
- );
152
+ var ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
153
+ var escAttr = (s) => s.replace(/[&<>"]/g, (c) => ESC[c]);
154
+ var escText = (s) => s.replace(/[&<>]/g, (c) => ESC[c]);
155
+ var ATTR = {
156
+ className: "class",
157
+ htmlFor: "for",
158
+ httpEquiv: "http-equiv",
159
+ charSet: "charset",
160
+ crossOrigin: "crossorigin",
161
+ noModule: "nomodule",
162
+ referrerPolicy: "referrerpolicy",
163
+ fetchPriority: "fetchpriority"
164
+ };
165
+ function renderHeadTag(tag, id, opts, selfClose = false) {
166
+ let attrs = ` id="${escAttr(id)}"`;
167
+ let inner = "";
168
+ for (const [k, v] of Object.entries(opts)) {
169
+ if (k === "key" || k === "children")
170
+ continue;
171
+ if (k === "dangerouslySetInnerHTML") {
172
+ inner = v.__html ?? "";
173
+ continue;
174
+ }
175
+ const attr = ATTR[k] ?? k;
176
+ if (v === true)
177
+ attrs += ` ${attr}`;
178
+ else if (v !== false && v != null)
179
+ attrs += ` ${attr}="${escAttr(String(v))}"`;
180
+ }
181
+ return selfClose ? `<${tag}${attrs}>` : `<${tag}${attrs}>${inner}</${tag}>`;
182
+ }
183
+ var getHeadHtml = (seoData) => {
184
+ let html = `<title>${escText(seoData.title ?? "")}</title>`;
185
+ for (const [id, opts] of Object.entries(seoData.meta))
186
+ html += renderHeadTag("meta", id, opts, true);
187
+ for (const [id, opts] of Object.entries(seoData.link))
188
+ html += renderHeadTag("link", id, opts, true);
189
+ for (const [id, opts] of Object.entries(seoData.style))
190
+ html += renderHeadTag("style", id, opts);
191
+ for (const [id, opts] of Object.entries(seoData.script))
192
+ html += renderHeadTag("script", id, opts);
193
+ return html;
194
194
  };
195
195
  var getReactResponse = async (req, opts) => {
196
196
  const App = opts.document.body;
@@ -233,18 +233,17 @@ var getReactResponse = async (req, opts) => {
233
233
  if (unsuspend.hasPending)
234
234
  await processUnsuspend();
235
235
  } while (unsuspend.hasPending && ++iters < 25);
236
- props = getAfterRenderProps ? await getAfterRenderProps(props, html) : props;
237
- try {
238
- globalThis.__hadarsUnsuspend = unsuspend;
239
- renderToStaticMarkup(
240
- /* @__PURE__ */ jsx(App, { ...{
241
- ...props,
242
- location: req.location,
243
- context
244
- } })
245
- );
246
- } finally {
247
- globalThis.__hadarsUnsuspend = null;
236
+ if (unsuspend.hasPending) {
237
+ console.warn("[hadars] SSR render loop hit the 25-iteration cap \u2014 some useServerData values may not be resolved. Check for data dependencies that are never fulfilled.");
238
+ }
239
+ if (getAfterRenderProps) {
240
+ props = await getAfterRenderProps(props, html);
241
+ try {
242
+ globalThis.__hadarsUnsuspend = unsuspend;
243
+ renderToStaticMarkup(/* @__PURE__ */ jsx(App, { ...{ ...props, location: req.location, context } }));
244
+ } finally {
245
+ globalThis.__hadarsUnsuspend = null;
246
+ }
248
247
  }
249
248
  const serverData = {};
250
249
  for (const [k, v] of unsuspend.cache) {
@@ -268,7 +267,7 @@ var getReactResponse = async (req, opts) => {
268
267
  return {
269
268
  ReactPage,
270
269
  status: context.head.status,
271
- headHtml: getHeadHtml(context.head, renderToStaticMarkup),
270
+ headHtml: getHeadHtml(context.head),
272
271
  renderPayload: {
273
272
  appProps: { ...props, location: req.location, context },
274
273
  clientProps
@@ -486,6 +485,21 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
486
485
  // for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
487
486
  mainFields: isServerBuild ? ["main", "module"] : ["browser", "module", "main"]
488
487
  };
488
+ const optimization = !isServerBuild && !isDev ? {
489
+ moduleIds: "deterministic",
490
+ splitChunks: {
491
+ chunks: "all",
492
+ cacheGroups: {
493
+ react: {
494
+ test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
495
+ name: "vendor-react",
496
+ chunks: "all",
497
+ priority: 20
498
+ }
499
+ }
500
+ },
501
+ ...opts.optimization ?? {}
502
+ } : opts.optimization ? { ...opts.optimization } : void 0;
489
503
  return {
490
504
  entry,
491
505
  output: {
@@ -494,6 +508,7 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
494
508
  },
495
509
  mode: opts.mode,
496
510
  externals,
511
+ ...optimization !== void 0 ? { optimization } : {},
497
512
  plugins: [
498
513
  new rspack.HtmlRspackPlugin({
499
514
  publicPath: base || "/",
@@ -603,7 +618,45 @@ function nodeReadableToWebStream(readable) {
603
618
  });
604
619
  }
605
620
  var noopCtx = { upgrade: () => false };
621
+ var COMPRESSIBLE_RE = /\b(?:text\/|application\/(?:json|javascript|xml)|image\/svg\+xml)/;
622
+ function withCompression(handler) {
623
+ return async (req, ctx) => {
624
+ const res = await handler(req, ctx);
625
+ if (!res?.body)
626
+ return res;
627
+ if (!COMPRESSIBLE_RE.test(res.headers.get("Content-Type") ?? ""))
628
+ return res;
629
+ if (res.headers.has("Content-Encoding"))
630
+ return res;
631
+ const accept = req.headers.get("Accept-Encoding") ?? "";
632
+ const encoding = accept.includes("br") ? "br" : accept.includes("gzip") ? "gzip" : null;
633
+ if (!encoding)
634
+ return res;
635
+ try {
636
+ const compressed = res.body.pipeThrough(new globalThis.CompressionStream(encoding));
637
+ const headers = new Headers(res.headers);
638
+ headers.set("Content-Encoding", encoding);
639
+ headers.delete("Content-Length");
640
+ return new Response(compressed, { status: res.status, statusText: res.statusText, headers });
641
+ } catch {
642
+ return res;
643
+ }
644
+ };
645
+ }
646
+ function withRequestLogging(handler) {
647
+ return async (req, ctx) => {
648
+ const start = performance.now();
649
+ const res = await handler(req, ctx);
650
+ const ms = Math.round(performance.now() - start);
651
+ const status = res?.status ?? 404;
652
+ const path2 = new URL(req.url).pathname;
653
+ console.log(`[hadars] ${req.method} ${path2} ${status} ${ms}ms`);
654
+ return res;
655
+ };
656
+ }
606
657
  async function serve(port, fetchHandler, websocket) {
658
+ fetchHandler = withCompression(fetchHandler);
659
+ fetchHandler = withRequestLogging(fetchHandler);
607
660
  if (isBun) {
608
661
  globalThis.Bun.serve({
609
662
  port,
@@ -733,16 +786,6 @@ import cluster from "node:cluster";
733
786
  var encoder = new TextEncoder();
734
787
  var HEAD_MARKER = '<meta name="NINETY_HEAD">';
735
788
  var BODY_MARKER = '<meta name="NINETY_BODY">';
736
- var _renderToReadableStream = null;
737
- async function getReadableStreamRenderer() {
738
- if (!_renderToReadableStream) {
739
- const req = createRequire2(pathMod3.resolve(process.cwd(), "__hadars_fake__.js"));
740
- const resolved = req.resolve("react-dom/server.browser");
741
- const mod = await import(pathToFileURL2(resolved).href);
742
- _renderToReadableStream = mod.renderToReadableStream;
743
- }
744
- return _renderToReadableStream;
745
- }
746
789
  var _renderToString = null;
747
790
  async function getRenderToString() {
748
791
  if (!_renderToString) {
@@ -761,64 +804,52 @@ var RenderWorkerPool = class {
761
804
  workerPending = /* @__PURE__ */ new Map();
762
805
  nextId = 0;
763
806
  rrIndex = 0;
807
+ _Worker = null;
808
+ _workerPath = "";
809
+ _ssrBundlePath = "";
764
810
  constructor(workerPath, size, ssrBundlePath) {
765
811
  this._init(workerPath, size, ssrBundlePath);
766
812
  }
767
813
  _init(workerPath, size, ssrBundlePath) {
814
+ this._workerPath = workerPath;
815
+ this._ssrBundlePath = ssrBundlePath;
768
816
  import("node:worker_threads").then(({ Worker }) => {
769
- for (let i = 0; i < size; i++) {
770
- const w = new Worker(workerPath, { workerData: { ssrBundlePath } });
771
- this.workerPending.set(w, /* @__PURE__ */ new Set());
772
- w.on("message", (msg) => {
773
- const { id, type, html, headHtml, status, error, chunk } = msg;
774
- const p = this.pending.get(id);
775
- if (!p)
776
- return;
777
- if (p.kind === "renderFullStream") {
778
- if (type === "head") {
779
- p.headSettled = true;
780
- p.headResolve({ headHtml, status });
781
- return;
782
- }
783
- if (type === "chunk") {
784
- p.controller.enqueue(chunk);
785
- return;
786
- }
787
- this.pending.delete(id);
788
- this.workerPending.get(w)?.delete(id);
789
- if (type === "done")
790
- p.controller.close();
791
- else {
792
- const err = new Error(error ?? "Stream error");
793
- if (!p.headSettled)
794
- p.headReject(err);
795
- p.controller.error(err);
796
- }
797
- return;
798
- }
799
- this.pending.delete(id);
800
- this.workerPending.get(w)?.delete(id);
801
- if (error)
802
- p.reject(new Error(error));
803
- else
804
- p.resolve({ html, headHtml, status });
805
- });
806
- w.on("error", (err) => {
807
- console.error("[hadars] Render worker error:", err);
808
- this._handleWorkerDeath(w, err);
809
- });
810
- w.on("exit", (code) => {
811
- if (code !== 0) {
812
- console.error(`[hadars] Render worker exited with code ${code}`);
813
- this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
814
- }
815
- });
816
- this.workers.push(w);
817
- }
817
+ this._Worker = Worker;
818
+ for (let i = 0; i < size; i++)
819
+ this._spawnWorker();
818
820
  }).catch((err) => {
819
821
  console.error("[hadars] Failed to initialise render worker pool:", err);
820
822
  });
821
823
  }
824
+ _spawnWorker() {
825
+ if (!this._Worker)
826
+ return;
827
+ const w = new this._Worker(this._workerPath, { workerData: { ssrBundlePath: this._ssrBundlePath } });
828
+ this.workerPending.set(w, /* @__PURE__ */ new Set());
829
+ w.on("message", (msg) => {
830
+ const { id, html, headHtml, status, error } = msg;
831
+ const p = this.pending.get(id);
832
+ if (!p)
833
+ return;
834
+ this.pending.delete(id);
835
+ this.workerPending.get(w)?.delete(id);
836
+ if (error)
837
+ p.reject(new Error(error));
838
+ else
839
+ p.resolve({ html, headHtml, status });
840
+ });
841
+ w.on("error", (err) => {
842
+ console.error("[hadars] Render worker error:", err);
843
+ this._handleWorkerDeath(w, err);
844
+ });
845
+ w.on("exit", (code) => {
846
+ if (code !== 0) {
847
+ console.error(`[hadars] Render worker exited with code ${code}`);
848
+ this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
849
+ }
850
+ });
851
+ this.workers.push(w);
852
+ }
822
853
  _handleWorkerDeath(w, err) {
823
854
  const idx = this.workers.indexOf(w);
824
855
  if (idx !== -1)
@@ -829,17 +860,13 @@ var RenderWorkerPool = class {
829
860
  const p = this.pending.get(id);
830
861
  if (p) {
831
862
  this.pending.delete(id);
832
- if (p.kind === "renderFull")
833
- p.reject(err);
834
- else {
835
- if (!p.headSettled)
836
- p.headReject(err);
837
- p.controller.error(err);
838
- }
863
+ p.reject(err);
839
864
  }
840
865
  }
841
866
  this.workerPending.delete(w);
842
867
  }
868
+ console.log("[hadars] Spawning replacement render worker");
869
+ this._spawnWorker();
843
870
  }
844
871
  nextWorker() {
845
872
  if (this.workers.length === 0)
@@ -868,82 +895,25 @@ var RenderWorkerPool = class {
868
895
  }
869
896
  });
870
897
  }
871
- /** Run the full SSR lifecycle in a worker thread, streaming the response. */
872
- renderFullStream(req) {
873
- let headResolve;
874
- let headReject;
875
- const head = new Promise((res, rej) => {
876
- headResolve = res;
877
- headReject = rej;
878
- });
879
- let controller;
880
- const stream = new ReadableStream({ start: (ctrl) => {
881
- controller = ctrl;
882
- } });
883
- const w = this.nextWorker();
884
- if (!w) {
885
- queueMicrotask(() => {
886
- headReject(new Error("[hadars] No render workers available"));
887
- controller.error(new Error("[hadars] No render workers available"));
888
- });
889
- return { head, stream };
890
- }
891
- const id = this.nextId++;
892
- this.pending.set(id, { kind: "renderFullStream", headSettled: false, headResolve, headReject, controller });
893
- this.workerPending.get(w)?.add(id);
894
- try {
895
- w.postMessage({ id, type: "renderFull", streaming: true, request: req });
896
- } catch (err) {
897
- this.pending.delete(id);
898
- this.workerPending.get(w)?.delete(id);
899
- queueMicrotask(() => {
900
- headReject(err);
901
- controller.error(err);
902
- });
903
- }
904
- return { head, stream };
905
- }
906
898
  async terminate() {
907
899
  await Promise.all(this.workers.map((w) => w.terminate()));
908
900
  }
909
901
  };
910
- async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, streaming, unsuspendForRender) {
911
- const renderToString = !streaming ? await getRenderToString() : null;
912
- const renderReadableStream = streaming ? await getReadableStreamRenderer() : null;
913
- if (!streaming) {
914
- const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
915
- let bodyHtml;
916
- try {
917
- globalThis.__hadarsUnsuspend = unsuspendForRender;
918
- bodyHtml = renderToString(ReactPage);
919
- } finally {
920
- globalThis.__hadarsUnsuspend = null;
921
- }
922
- return new Response(precontentHtml + bodyHtml + postContent, {
923
- headers: { "Content-Type": "text/html; charset=utf-8" },
924
- status
925
- });
926
- }
902
+ async function buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspendForRender) {
903
+ const renderToString = await getRenderToString();
927
904
  const responseStream = new ReadableStream({
928
905
  async start(controller) {
929
906
  const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
930
907
  controller.enqueue(encoder.encode(precontentHtml));
931
- let streamPromise;
908
+ await Promise.resolve();
909
+ let bodyHtml;
932
910
  try {
933
911
  globalThis.__hadarsUnsuspend = unsuspendForRender;
934
- streamPromise = renderReadableStream(ReactPage);
912
+ bodyHtml = renderToString(ReactPage);
935
913
  } finally {
936
914
  globalThis.__hadarsUnsuspend = null;
937
915
  }
938
- const bodyStream = await streamPromise;
939
- const reader = bodyStream.getReader();
940
- while (true) {
941
- const { done, value } = await reader.read();
942
- if (done)
943
- break;
944
- controller.enqueue(value);
945
- }
946
- controller.enqueue(encoder.encode(postContent));
916
+ controller.enqueue(encoder.encode(bodyHtml + postContent));
947
917
  controller.close();
948
918
  }
949
919
  });
@@ -1048,7 +1018,7 @@ var dev = async (options) => {
1048
1018
  }
1049
1019
  const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
1050
1020
  await fs.writeFile(tmpFilePath, clientScript);
1051
- let ssrBuildId = Date.now();
1021
+ let ssrBuildId = crypto.randomBytes(4).toString("hex");
1052
1022
  const clientCompiler = createClientCompiler(tmpFilePath, {
1053
1023
  target: "web",
1054
1024
  output: {
@@ -1163,7 +1133,7 @@ var dev = async (options) => {
1163
1133
  } catch (e) {
1164
1134
  }
1165
1135
  if (chunk.includes(rebuildMarker)) {
1166
- ssrBuildId = Date.now();
1136
+ ssrBuildId = crypto.randomBytes(4).toString("hex");
1167
1137
  console.log("[hadars] SSR bundle updated, build id:", ssrBuildId);
1168
1138
  }
1169
1139
  }
@@ -1217,23 +1187,32 @@ var dev = async (options) => {
1217
1187
  return projectRes;
1218
1188
  const ssrComponentPath = pathMod3.join(__dirname2, HadarsFolder, SSR_FILENAME);
1219
1189
  const importPath = pathToFileURL2(ssrComponentPath).href + `?t=${ssrBuildId}`;
1220
- const {
1221
- default: Component,
1222
- getInitProps,
1223
- getAfterRenderProps,
1224
- getFinalProps
1225
- } = await import(importPath);
1226
- const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
1227
- document: {
1228
- body: Component,
1229
- lang: "en",
1190
+ try {
1191
+ const {
1192
+ default: Component,
1230
1193
  getInitProps,
1231
1194
  getAfterRenderProps,
1232
1195
  getFinalProps
1233
- }
1234
- });
1235
- const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
1236
- return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true, unsuspend);
1196
+ } = await import(importPath);
1197
+ const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
1198
+ document: {
1199
+ body: Component,
1200
+ lang: "en",
1201
+ getInitProps,
1202
+ getAfterRenderProps,
1203
+ getFinalProps
1204
+ }
1205
+ });
1206
+ const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
1207
+ return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
1208
+ } catch (err) {
1209
+ console.error("[hadars] SSR render error:", err);
1210
+ const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, "&lt;");
1211
+ return new Response(`<!doctype html><pre style="white-space:pre-wrap">${msg}</pre>`, {
1212
+ status: 500,
1213
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1214
+ });
1215
+ }
1237
1216
  }, options.websocket);
1238
1217
  };
1239
1218
  var build = async (options) => {
@@ -1250,19 +1229,20 @@ var build = async (options) => {
1250
1229
  }
1251
1230
  const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
1252
1231
  await fs.writeFile(tmpFilePath, clientScript);
1253
- const randomStr = crypto.randomBytes(6).toString("hex");
1254
1232
  console.log("Building client and server bundles in parallel...");
1255
1233
  await Promise.all([
1256
1234
  compileEntry(tmpFilePath, {
1257
1235
  target: "web",
1258
1236
  output: {
1259
- filename: `index-${randomStr}.js`,
1237
+ // Content hash: filename is stable when code is unchanged → better browser/CDN cache.
1238
+ filename: "index.[contenthash:8].js",
1260
1239
  path: pathMod3.resolve(__dirname2, StaticPath)
1261
1240
  },
1262
1241
  base: options.baseURL,
1263
1242
  mode: "production",
1264
1243
  swcPlugins: options.swcPlugins,
1265
- define: options.define
1244
+ define: options.define,
1245
+ optimization: options.optimization
1266
1246
  }),
1267
1247
  compileEntry(pathMod3.resolve(__dirname2, options.entry), {
1268
1248
  output: {
@@ -1280,10 +1260,6 @@ var build = async (options) => {
1280
1260
  })
1281
1261
  ]);
1282
1262
  await fs.rm(tmpFilePath);
1283
- await fs.writeFile(
1284
- pathMod3.join(__dirname2, HadarsFolder, "hadars.json"),
1285
- JSON.stringify({ buildId: randomStr })
1286
- );
1287
1263
  console.log("Build complete.");
1288
1264
  };
1289
1265
  var run = async (options) => {
@@ -1356,37 +1332,15 @@ var run = async (options) => {
1356
1332
  const componentPath = pathToFileURL2(
1357
1333
  pathMod3.resolve(__dirname2, HadarsFolder, SSR_FILENAME)
1358
1334
  ).href;
1359
- const {
1360
- default: Component,
1361
- getInitProps,
1362
- getAfterRenderProps,
1363
- getFinalProps
1364
- } = await import(componentPath);
1365
- if (renderPool) {
1366
- const serialReq = await serializeRequest(request);
1367
- if (options.streaming) {
1368
- const { head, stream } = renderPool.renderFullStream(serialReq);
1369
- const { headHtml: wHead, status: wStatus } = await head;
1370
- const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
1371
- const responseStream = new ReadableStream({
1372
- async start(controller) {
1373
- controller.enqueue(encoder.encode(precontentHtml));
1374
- const reader = stream.getReader();
1375
- while (true) {
1376
- const { done, value } = await reader.read();
1377
- if (done)
1378
- break;
1379
- controller.enqueue(value);
1380
- }
1381
- controller.enqueue(encoder.encode(postContent));
1382
- controller.close();
1383
- }
1384
- });
1385
- return new Response(responseStream, {
1386
- headers: { "Content-Type": "text/html; charset=utf-8" },
1387
- status: wStatus
1388
- });
1389
- } else {
1335
+ try {
1336
+ const {
1337
+ default: Component,
1338
+ getInitProps,
1339
+ getAfterRenderProps,
1340
+ getFinalProps
1341
+ } = await import(componentPath);
1342
+ if (renderPool) {
1343
+ const serialReq = await serializeRequest(request);
1390
1344
  const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
1391
1345
  const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
1392
1346
  return new Response(precontentHtml + html + postContent, {
@@ -1394,18 +1348,21 @@ var run = async (options) => {
1394
1348
  status: wStatus
1395
1349
  });
1396
1350
  }
1351
+ const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
1352
+ document: {
1353
+ body: Component,
1354
+ lang: "en",
1355
+ getInitProps,
1356
+ getAfterRenderProps,
1357
+ getFinalProps
1358
+ }
1359
+ });
1360
+ const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
1361
+ return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
1362
+ } catch (err) {
1363
+ console.error("[hadars] SSR render error:", err);
1364
+ return new Response("Internal Server Error", { status: 500 });
1397
1365
  }
1398
- const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
1399
- document: {
1400
- body: Component,
1401
- lang: "en",
1402
- getInitProps,
1403
- getAfterRenderProps,
1404
- getFinalProps
1405
- }
1406
- });
1407
- const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
1408
- return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, options.streaming === true, unsuspend);
1409
1366
  }, options.websocket);
1410
1367
  };
1411
1368
 
package/dist/index.cjs CHANGED
@@ -172,12 +172,13 @@ var AppProviderCSR = import_react.default.memo(({ children }) => {
172
172
  var useApp = () => import_react.default.useContext(AppContext);
173
173
  var clientServerDataCache = /* @__PURE__ */ new Map();
174
174
  function initServerDataCache(data) {
175
+ clientServerDataCache.clear();
175
176
  for (const [k, v] of Object.entries(data)) {
176
177
  clientServerDataCache.set(k, v);
177
178
  }
178
179
  }
179
180
  function useServerData(key, fn) {
180
- const cacheKey = Array.isArray(key) ? key.join("\0") : key;
181
+ const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
181
182
  if (typeof window !== "undefined") {
182
183
  if (clientServerDataCache.has(cacheKey)) {
183
184
  return clientServerDataCache.get(cacheKey);