hadars 0.1.9 → 0.1.11
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/dist/cli.js +259 -122
- package/dist/index.cjs +2 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +2 -1
- package/dist/loader.cjs +126 -5
- package/dist/ssr-render-worker.js +3 -0
- package/dist/utils/Head.tsx +4 -2
- package/dist/utils/clientScript.tsx +6 -2
- package/package.json +16 -15
- package/src/build.ts +193 -68
- package/src/ssr-render-worker.ts +3 -0
- package/src/types/ninety.ts +18 -0
- package/src/utils/Head.tsx +4 -2
- package/src/utils/clientScript.tsx +6 -2
- package/src/utils/loader.ts +193 -11
- package/src/utils/response.tsx +51 -55
- package/src/utils/serve.ts +40 -0
- package/src/utils/loadModule.ts +0 -4
package/dist/cli.js
CHANGED
|
@@ -149,48 +149,48 @@ async function getStaticMarkupRenderer() {
|
|
|
149
149
|
}
|
|
150
150
|
return _renderToStaticMarkup;
|
|
151
151
|
}
|
|
152
|
-
var
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
152
|
+
var ESC = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
270
|
+
headHtml: getHeadHtml(context.head),
|
|
272
271
|
renderPayload: {
|
|
273
272
|
appProps: { ...props, location: req.location, context },
|
|
274
273
|
clientProps
|
|
@@ -619,7 +618,45 @@ function nodeReadableToWebStream(readable) {
|
|
|
619
618
|
});
|
|
620
619
|
}
|
|
621
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
|
+
}
|
|
622
657
|
async function serve(port, fetchHandler, websocket) {
|
|
658
|
+
fetchHandler = withCompression(fetchHandler);
|
|
659
|
+
fetchHandler = withRequestLogging(fetchHandler);
|
|
623
660
|
if (isBun) {
|
|
624
661
|
globalThis.Bun.serve({
|
|
625
662
|
port,
|
|
@@ -740,6 +777,7 @@ import { RspackDevServer } from "@rspack/dev-server";
|
|
|
740
777
|
import pathMod3 from "node:path";
|
|
741
778
|
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
742
779
|
import { createRequire as createRequire2 } from "node:module";
|
|
780
|
+
import crypto from "node:crypto";
|
|
743
781
|
import fs from "node:fs/promises";
|
|
744
782
|
import { existsSync as existsSync2 } from "node:fs";
|
|
745
783
|
import os from "node:os";
|
|
@@ -766,42 +804,52 @@ var RenderWorkerPool = class {
|
|
|
766
804
|
workerPending = /* @__PURE__ */ new Map();
|
|
767
805
|
nextId = 0;
|
|
768
806
|
rrIndex = 0;
|
|
807
|
+
_Worker = null;
|
|
808
|
+
_workerPath = "";
|
|
809
|
+
_ssrBundlePath = "";
|
|
769
810
|
constructor(workerPath, size, ssrBundlePath) {
|
|
770
811
|
this._init(workerPath, size, ssrBundlePath);
|
|
771
812
|
}
|
|
772
813
|
_init(workerPath, size, ssrBundlePath) {
|
|
814
|
+
this._workerPath = workerPath;
|
|
815
|
+
this._ssrBundlePath = ssrBundlePath;
|
|
773
816
|
import("node:worker_threads").then(({ Worker }) => {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
this.
|
|
777
|
-
w.on("message", (msg) => {
|
|
778
|
-
const { id, html, headHtml, status, error } = msg;
|
|
779
|
-
const p = this.pending.get(id);
|
|
780
|
-
if (!p)
|
|
781
|
-
return;
|
|
782
|
-
this.pending.delete(id);
|
|
783
|
-
this.workerPending.get(w)?.delete(id);
|
|
784
|
-
if (error)
|
|
785
|
-
p.reject(new Error(error));
|
|
786
|
-
else
|
|
787
|
-
p.resolve({ html, headHtml, status });
|
|
788
|
-
});
|
|
789
|
-
w.on("error", (err) => {
|
|
790
|
-
console.error("[hadars] Render worker error:", err);
|
|
791
|
-
this._handleWorkerDeath(w, err);
|
|
792
|
-
});
|
|
793
|
-
w.on("exit", (code) => {
|
|
794
|
-
if (code !== 0) {
|
|
795
|
-
console.error(`[hadars] Render worker exited with code ${code}`);
|
|
796
|
-
this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
this.workers.push(w);
|
|
800
|
-
}
|
|
817
|
+
this._Worker = Worker;
|
|
818
|
+
for (let i = 0; i < size; i++)
|
|
819
|
+
this._spawnWorker();
|
|
801
820
|
}).catch((err) => {
|
|
802
821
|
console.error("[hadars] Failed to initialise render worker pool:", err);
|
|
803
822
|
});
|
|
804
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
|
+
}
|
|
805
853
|
_handleWorkerDeath(w, err) {
|
|
806
854
|
const idx = this.workers.indexOf(w);
|
|
807
855
|
if (idx !== -1)
|
|
@@ -817,6 +865,8 @@ var RenderWorkerPool = class {
|
|
|
817
865
|
}
|
|
818
866
|
this.workerPending.delete(w);
|
|
819
867
|
}
|
|
868
|
+
console.log("[hadars] Spawning replacement render worker");
|
|
869
|
+
this._spawnWorker();
|
|
820
870
|
}
|
|
821
871
|
nextWorker() {
|
|
822
872
|
if (this.workers.length === 0)
|
|
@@ -906,6 +956,74 @@ var makePrecontentHtmlGetter = (htmlFilePromise) => {
|
|
|
906
956
|
return [preHead + headHtml + postHead, postContent];
|
|
907
957
|
};
|
|
908
958
|
};
|
|
959
|
+
async function transformStream(data, stream) {
|
|
960
|
+
const writer = stream.writable.getWriter();
|
|
961
|
+
writer.write(data);
|
|
962
|
+
writer.close();
|
|
963
|
+
const chunks = [];
|
|
964
|
+
const reader = stream.readable.getReader();
|
|
965
|
+
while (true) {
|
|
966
|
+
const { done, value } = await reader.read();
|
|
967
|
+
if (done)
|
|
968
|
+
break;
|
|
969
|
+
chunks.push(value);
|
|
970
|
+
}
|
|
971
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
972
|
+
const out = new Uint8Array(total);
|
|
973
|
+
let offset = 0;
|
|
974
|
+
for (const c of chunks) {
|
|
975
|
+
out.set(c, offset);
|
|
976
|
+
offset += c.length;
|
|
977
|
+
}
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
var gzipCompress = (d) => transformStream(d, new globalThis.CompressionStream("gzip"));
|
|
981
|
+
var gzipDecompress = (d) => transformStream(d, new globalThis.DecompressionStream("gzip"));
|
|
982
|
+
function createRenderCache(opts, handler) {
|
|
983
|
+
const store = /* @__PURE__ */ new Map();
|
|
984
|
+
return async (req, ctx) => {
|
|
985
|
+
const hadarsReq = parseRequest(req);
|
|
986
|
+
const cacheOpts = await opts(hadarsReq);
|
|
987
|
+
const key = cacheOpts?.key ?? null;
|
|
988
|
+
if (key != null) {
|
|
989
|
+
const entry = store.get(key);
|
|
990
|
+
if (entry) {
|
|
991
|
+
if (entry.expiresAt == null || Date.now() < entry.expiresAt) {
|
|
992
|
+
const accept = req.headers.get("Accept-Encoding") ?? "";
|
|
993
|
+
if (accept.includes("gzip")) {
|
|
994
|
+
return new Response(entry.body.buffer, { status: entry.status, headers: entry.headers });
|
|
995
|
+
}
|
|
996
|
+
const plain = await gzipDecompress(entry.body);
|
|
997
|
+
const headers = entry.headers.filter(([k]) => k.toLowerCase() !== "content-encoding");
|
|
998
|
+
return new Response(plain.buffer, { status: entry.status, headers });
|
|
999
|
+
}
|
|
1000
|
+
store.delete(key);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const res = await handler(req, ctx);
|
|
1004
|
+
if (key != null && res) {
|
|
1005
|
+
const ttl = cacheOpts?.ttl;
|
|
1006
|
+
res.clone().arrayBuffer().then(async (buf) => {
|
|
1007
|
+
const body = await gzipCompress(new Uint8Array(buf));
|
|
1008
|
+
const headers = [];
|
|
1009
|
+
res.headers.forEach((v, k) => {
|
|
1010
|
+
if (k.toLowerCase() !== "content-encoding" && k.toLowerCase() !== "content-length") {
|
|
1011
|
+
headers.push([k, v]);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
headers.push(["content-encoding", "gzip"]);
|
|
1015
|
+
store.set(key, {
|
|
1016
|
+
body,
|
|
1017
|
+
status: res.status,
|
|
1018
|
+
headers,
|
|
1019
|
+
expiresAt: ttl != null ? Date.now() + ttl : null
|
|
1020
|
+
});
|
|
1021
|
+
}).catch(() => {
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return res;
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
909
1027
|
var SSR_FILENAME = "index.ssr.js";
|
|
910
1028
|
var __dirname2 = process.cwd();
|
|
911
1029
|
var getSuffix = (mode) => mode === "development" ? `?v=${Date.now()}` : "";
|
|
@@ -968,7 +1086,7 @@ var dev = async (options) => {
|
|
|
968
1086
|
}
|
|
969
1087
|
const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
|
|
970
1088
|
await fs.writeFile(tmpFilePath, clientScript);
|
|
971
|
-
let ssrBuildId =
|
|
1089
|
+
let ssrBuildId = crypto.randomBytes(4).toString("hex");
|
|
972
1090
|
const clientCompiler = createClientCompiler(tmpFilePath, {
|
|
973
1091
|
target: "web",
|
|
974
1092
|
output: {
|
|
@@ -1083,7 +1201,7 @@ var dev = async (options) => {
|
|
|
1083
1201
|
} catch (e) {
|
|
1084
1202
|
}
|
|
1085
1203
|
if (chunk.includes(rebuildMarker)) {
|
|
1086
|
-
ssrBuildId =
|
|
1204
|
+
ssrBuildId = crypto.randomBytes(4).toString("hex");
|
|
1087
1205
|
console.log("[hadars] SSR bundle updated, build id:", ssrBuildId);
|
|
1088
1206
|
}
|
|
1089
1207
|
}
|
|
@@ -1137,23 +1255,32 @@ var dev = async (options) => {
|
|
|
1137
1255
|
return projectRes;
|
|
1138
1256
|
const ssrComponentPath = pathMod3.join(__dirname2, HadarsFolder, SSR_FILENAME);
|
|
1139
1257
|
const importPath = pathToFileURL2(ssrComponentPath).href + `?t=${ssrBuildId}`;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
getAfterRenderProps,
|
|
1144
|
-
getFinalProps
|
|
1145
|
-
} = await import(importPath);
|
|
1146
|
-
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1147
|
-
document: {
|
|
1148
|
-
body: Component,
|
|
1149
|
-
lang: "en",
|
|
1258
|
+
try {
|
|
1259
|
+
const {
|
|
1260
|
+
default: Component,
|
|
1150
1261
|
getInitProps,
|
|
1151
1262
|
getAfterRenderProps,
|
|
1152
1263
|
getFinalProps
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1264
|
+
} = await import(importPath);
|
|
1265
|
+
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1266
|
+
document: {
|
|
1267
|
+
body: Component,
|
|
1268
|
+
lang: "en",
|
|
1269
|
+
getInitProps,
|
|
1270
|
+
getAfterRenderProps,
|
|
1271
|
+
getFinalProps
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
|
|
1275
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
console.error("[hadars] SSR render error:", err);
|
|
1278
|
+
const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, "<");
|
|
1279
|
+
return new Response(`<!doctype html><pre style="white-space:pre-wrap">${msg}</pre>`, {
|
|
1280
|
+
status: 500,
|
|
1281
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1157
1284
|
}, options.websocket);
|
|
1158
1285
|
};
|
|
1159
1286
|
var build = async (options) => {
|
|
@@ -1236,7 +1363,7 @@ var run = async (options) => {
|
|
|
1236
1363
|
const getPrecontentHtml = makePrecontentHtmlGetter(
|
|
1237
1364
|
fs.readFile(pathMod3.join(__dirname2, StaticPath, "out.html"), "utf-8")
|
|
1238
1365
|
);
|
|
1239
|
-
|
|
1366
|
+
const runHandler = async (req, ctx) => {
|
|
1240
1367
|
const request = parseRequest(req);
|
|
1241
1368
|
if (handler) {
|
|
1242
1369
|
const res = await handler(request);
|
|
@@ -1273,33 +1400,43 @@ var run = async (options) => {
|
|
|
1273
1400
|
const componentPath = pathToFileURL2(
|
|
1274
1401
|
pathMod3.resolve(__dirname2, HadarsFolder, SSR_FILENAME)
|
|
1275
1402
|
).href;
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
getAfterRenderProps,
|
|
1280
|
-
getFinalProps
|
|
1281
|
-
} = await import(componentPath);
|
|
1282
|
-
if (renderPool) {
|
|
1283
|
-
const serialReq = await serializeRequest(request);
|
|
1284
|
-
const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
|
|
1285
|
-
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
1286
|
-
return new Response(precontentHtml + html + postContent, {
|
|
1287
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1288
|
-
status: wStatus
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1291
|
-
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1292
|
-
document: {
|
|
1293
|
-
body: Component,
|
|
1294
|
-
lang: "en",
|
|
1403
|
+
try {
|
|
1404
|
+
const {
|
|
1405
|
+
default: Component,
|
|
1295
1406
|
getInitProps,
|
|
1296
1407
|
getAfterRenderProps,
|
|
1297
1408
|
getFinalProps
|
|
1409
|
+
} = await import(componentPath);
|
|
1410
|
+
if (renderPool) {
|
|
1411
|
+
const serialReq = await serializeRequest(request);
|
|
1412
|
+
const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
|
|
1413
|
+
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
1414
|
+
return new Response(precontentHtml + html + postContent, {
|
|
1415
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1416
|
+
status: wStatus
|
|
1417
|
+
});
|
|
1298
1418
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1419
|
+
const { ReactPage, status, headHtml, renderPayload } = await getReactResponse(request, {
|
|
1420
|
+
document: {
|
|
1421
|
+
body: Component,
|
|
1422
|
+
lang: "en",
|
|
1423
|
+
getInitProps,
|
|
1424
|
+
getAfterRenderProps,
|
|
1425
|
+
getFinalProps
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
const unsuspend = renderPayload.appProps.context?._unsuspend ?? null;
|
|
1429
|
+
return buildSsrResponse(ReactPage, headHtml, status, getPrecontentHtml, unsuspend);
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
console.error("[hadars] SSR render error:", err);
|
|
1432
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
await serve(
|
|
1436
|
+
port,
|
|
1437
|
+
options.cache ? createRenderCache(options.cache, runHandler) : runHandler,
|
|
1438
|
+
options.websocket
|
|
1439
|
+
);
|
|
1303
1440
|
};
|
|
1304
1441
|
|
|
1305
1442
|
// cli-lib.ts
|
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) ?
|
|
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);
|
package/dist/index.d.ts
CHANGED
|
@@ -92,6 +92,29 @@ interface HadarsOptions {
|
|
|
92
92
|
* Has no effect on the SSR bundle or dev mode.
|
|
93
93
|
*/
|
|
94
94
|
optimization?: Record<string, unknown>;
|
|
95
|
+
/**
|
|
96
|
+
* SSR response cache for `run()` mode. Has no effect in `dev()` mode.
|
|
97
|
+
*
|
|
98
|
+
* Receives the incoming request and should return `{ key, ttl? }` to cache
|
|
99
|
+
* the response, or `null`/`undefined` to skip caching for that request.
|
|
100
|
+
* `ttl` is the time-to-live in milliseconds; omit for entries that never expire.
|
|
101
|
+
* The function may be async.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* // Cache every page by pathname (no per-user personalisation):
|
|
105
|
+
* cache: (req) => ({ key: req.pathname })
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Cache with a per-route TTL, skip authenticated requests:
|
|
109
|
+
* cache: (req) => req.cookies.session ? null : { key: req.pathname, ttl: 60_000 }
|
|
110
|
+
*/
|
|
111
|
+
cache?: (req: HadarsRequest) => {
|
|
112
|
+
key: string;
|
|
113
|
+
ttl?: number;
|
|
114
|
+
} | null | undefined | Promise<{
|
|
115
|
+
key: string;
|
|
116
|
+
ttl?: number;
|
|
117
|
+
} | null | undefined>;
|
|
95
118
|
}
|
|
96
119
|
type SwcPluginItem = string | [string, Record<string, unknown>] | {
|
|
97
120
|
path: string;
|
|
@@ -106,7 +129,8 @@ interface HadarsRequest extends Request {
|
|
|
106
129
|
}
|
|
107
130
|
|
|
108
131
|
/** Call this before hydrating to seed the client cache from the server's data.
|
|
109
|
-
* Invoked automatically by the hadars client bootstrap.
|
|
132
|
+
* Invoked automatically by the hadars client bootstrap.
|
|
133
|
+
* Always clears the existing cache before populating — call with `{}` to just clear. */
|
|
110
134
|
declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
111
135
|
/**
|
|
112
136
|
* Fetch async data on the server during SSR. Returns `undefined` on the first
|
package/dist/index.js
CHANGED
|
@@ -132,12 +132,13 @@ var AppProviderCSR = React.memo(({ children }) => {
|
|
|
132
132
|
var useApp = () => React.useContext(AppContext);
|
|
133
133
|
var clientServerDataCache = /* @__PURE__ */ new Map();
|
|
134
134
|
function initServerDataCache(data) {
|
|
135
|
+
clientServerDataCache.clear();
|
|
135
136
|
for (const [k, v] of Object.entries(data)) {
|
|
136
137
|
clientServerDataCache.set(k, v);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
function useServerData(key, fn) {
|
|
140
|
-
const cacheKey = Array.isArray(key) ?
|
|
141
|
+
const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
|
|
141
142
|
if (typeof window !== "undefined") {
|
|
142
143
|
if (clientServerDataCache.has(cacheKey)) {
|
|
143
144
|
return clientServerDataCache.get(cacheKey);
|