hadars 0.1.9 → 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/dist/cli.js +184 -120
- package/dist/index.cjs +2 -1
- package/dist/index.d.ts +2 -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 +4 -3
- package/src/build.ts +95 -66
- package/src/ssr-render-worker.ts +3 -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)
|
|
@@ -968,7 +1018,7 @@ var dev = async (options) => {
|
|
|
968
1018
|
}
|
|
969
1019
|
const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
|
|
970
1020
|
await fs.writeFile(tmpFilePath, clientScript);
|
|
971
|
-
let ssrBuildId =
|
|
1021
|
+
let ssrBuildId = crypto.randomBytes(4).toString("hex");
|
|
972
1022
|
const clientCompiler = createClientCompiler(tmpFilePath, {
|
|
973
1023
|
target: "web",
|
|
974
1024
|
output: {
|
|
@@ -1083,7 +1133,7 @@ var dev = async (options) => {
|
|
|
1083
1133
|
} catch (e) {
|
|
1084
1134
|
}
|
|
1085
1135
|
if (chunk.includes(rebuildMarker)) {
|
|
1086
|
-
ssrBuildId =
|
|
1136
|
+
ssrBuildId = crypto.randomBytes(4).toString("hex");
|
|
1087
1137
|
console.log("[hadars] SSR bundle updated, build id:", ssrBuildId);
|
|
1088
1138
|
}
|
|
1089
1139
|
}
|
|
@@ -1137,23 +1187,32 @@ var dev = async (options) => {
|
|
|
1137
1187
|
return projectRes;
|
|
1138
1188
|
const ssrComponentPath = pathMod3.join(__dirname2, HadarsFolder, SSR_FILENAME);
|
|
1139
1189
|
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",
|
|
1190
|
+
try {
|
|
1191
|
+
const {
|
|
1192
|
+
default: Component,
|
|
1150
1193
|
getInitProps,
|
|
1151
1194
|
getAfterRenderProps,
|
|
1152
1195
|
getFinalProps
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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, "<");
|
|
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
|
+
}
|
|
1157
1216
|
}, options.websocket);
|
|
1158
1217
|
};
|
|
1159
1218
|
var build = async (options) => {
|
|
@@ -1273,32 +1332,37 @@ var run = async (options) => {
|
|
|
1273
1332
|
const componentPath = pathToFileURL2(
|
|
1274
1333
|
pathMod3.resolve(__dirname2, HadarsFolder, SSR_FILENAME)
|
|
1275
1334
|
).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",
|
|
1335
|
+
try {
|
|
1336
|
+
const {
|
|
1337
|
+
default: Component,
|
|
1295
1338
|
getInitProps,
|
|
1296
1339
|
getAfterRenderProps,
|
|
1297
1340
|
getFinalProps
|
|
1341
|
+
} = await import(componentPath);
|
|
1342
|
+
if (renderPool) {
|
|
1343
|
+
const serialReq = await serializeRequest(request);
|
|
1344
|
+
const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
|
|
1345
|
+
const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
|
|
1346
|
+
return new Response(precontentHtml + html + postContent, {
|
|
1347
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1348
|
+
status: wStatus
|
|
1349
|
+
});
|
|
1298
1350
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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 });
|
|
1365
|
+
}
|
|
1302
1366
|
}, options.websocket);
|
|
1303
1367
|
};
|
|
1304
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) ?
|
|
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
|
@@ -106,7 +106,8 @@ interface HadarsRequest extends Request {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/** Call this before hydrating to seed the client cache from the server's data.
|
|
109
|
-
* Invoked automatically by the hadars client bootstrap.
|
|
109
|
+
* Invoked automatically by the hadars client bootstrap.
|
|
110
|
+
* Always clears the existing cache before populating — call with `{}` to just clear. */
|
|
110
111
|
declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
111
112
|
/**
|
|
112
113
|
* 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);
|
package/dist/loader.cjs
CHANGED
|
@@ -21,14 +21,135 @@ __export(loader_exports, {
|
|
|
21
21
|
default: () => loader
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(loader_exports);
|
|
24
|
-
const LOAD_MODULE_RE = /\bloadModule\s*(?:<.*?>\s*)?\(\s*(['"`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/gs;
|
|
25
24
|
function loader(source) {
|
|
26
25
|
const isServer = this.target === "node" || this.target === "async-node";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const resourcePath = this.resourcePath ?? this.resource ?? "(unknown)";
|
|
27
|
+
let swc;
|
|
28
|
+
try {
|
|
29
|
+
swc = require("@swc/core");
|
|
30
|
+
} catch {
|
|
31
|
+
return regexTransform.call(this, source, isServer, resourcePath);
|
|
32
|
+
}
|
|
33
|
+
return swcTransform.call(this, swc, source, isServer, resourcePath);
|
|
34
|
+
}
|
|
35
|
+
function swcTransform(swc, source, isServer, resourcePath) {
|
|
36
|
+
const isTs = /\.[mc]?tsx?$/.test(resourcePath);
|
|
37
|
+
const isTsx = /\.(tsx|jsx)$/.test(resourcePath);
|
|
38
|
+
let ast;
|
|
39
|
+
try {
|
|
40
|
+
ast = swc.parseSync(source, {
|
|
41
|
+
syntax: isTs ? "typescript" : "ecmascript",
|
|
42
|
+
tsx: isTsx
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
return regexTransform.call(this, source, isServer, resourcePath);
|
|
46
|
+
}
|
|
47
|
+
const srcBytes = Buffer.from(source, "utf8");
|
|
48
|
+
const fileOffset = ast.span.start - countLeadingNonCodeBytes(source);
|
|
49
|
+
const replacements = [];
|
|
50
|
+
walkAst(ast, (node) => {
|
|
51
|
+
if (node.type !== "CallExpression")
|
|
52
|
+
return;
|
|
53
|
+
const callee = node.callee;
|
|
54
|
+
if (!callee || callee.type !== "Identifier" || callee.value !== "loadModule")
|
|
55
|
+
return;
|
|
56
|
+
const args = node.arguments;
|
|
57
|
+
if (!args || args.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
const firstArg = args[0].expression ?? args[0];
|
|
60
|
+
let modulePath;
|
|
61
|
+
let quoteChar;
|
|
62
|
+
if (firstArg.type === "StringLiteral") {
|
|
63
|
+
modulePath = firstArg.value;
|
|
64
|
+
const quoteByteIdx = firstArg.span.start - fileOffset;
|
|
65
|
+
quoteChar = String.fromCharCode(srcBytes[quoteByteIdx]);
|
|
66
|
+
} else if (firstArg.type === "TemplateLiteral" && firstArg.expressions.length === 0 && firstArg.quasis.length === 1) {
|
|
67
|
+
modulePath = firstArg.quasis[0].raw;
|
|
68
|
+
quoteChar = "`";
|
|
30
69
|
} else {
|
|
31
|
-
|
|
70
|
+
const start0 = node.span.start - fileOffset;
|
|
71
|
+
const bytesBefore = srcBytes.slice(0, start0);
|
|
72
|
+
const line = bytesBefore.toString("utf8").split("\n").length;
|
|
73
|
+
this.emitWarning(
|
|
74
|
+
new Error(
|
|
75
|
+
`[hadars] loadModule() called with a dynamic (non-literal) path at ${resourcePath}:${line}. Only string-literal paths are transformed by the loader; dynamic calls fall back to runtime import().`
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
32
79
|
}
|
|
80
|
+
const replacement = isServer ? `Promise.resolve(require(${quoteChar}${modulePath}${quoteChar}))` : `import(${quoteChar}${modulePath}${quoteChar})`;
|
|
81
|
+
replacements.push({ start: node.span.start - fileOffset, end: node.span.end - fileOffset, replacement });
|
|
33
82
|
});
|
|
83
|
+
if (replacements.length === 0)
|
|
84
|
+
return source;
|
|
85
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
86
|
+
let result = srcBytes;
|
|
87
|
+
for (const { start, end, replacement } of replacements) {
|
|
88
|
+
result = Buffer.concat([result.slice(0, start), Buffer.from(replacement, "utf8"), result.slice(end)]);
|
|
89
|
+
}
|
|
90
|
+
return result.toString("utf8");
|
|
91
|
+
}
|
|
92
|
+
function walkAst(node, visit) {
|
|
93
|
+
if (!node || typeof node !== "object")
|
|
94
|
+
return;
|
|
95
|
+
visit(node);
|
|
96
|
+
for (const key of Object.keys(node)) {
|
|
97
|
+
if (key === "span" || key === "type" || key === "ctxt")
|
|
98
|
+
continue;
|
|
99
|
+
const val = node[key];
|
|
100
|
+
if (Array.isArray(val)) {
|
|
101
|
+
for (const child of val)
|
|
102
|
+
walkAst(child, visit);
|
|
103
|
+
} else if (val && typeof val === "object") {
|
|
104
|
+
walkAst(val, visit);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function countLeadingNonCodeBytes(source) {
|
|
109
|
+
let i = 0;
|
|
110
|
+
while (i < source.length) {
|
|
111
|
+
if (source[i] === " " || source[i] === " " || source[i] === "\r" || source[i] === "\n") {
|
|
112
|
+
i++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (source[i] === "/" && source[i + 1] === "/") {
|
|
116
|
+
while (i < source.length && source[i] !== "\n")
|
|
117
|
+
i++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (source[i] === "/" && source[i + 1] === "*") {
|
|
121
|
+
i += 2;
|
|
122
|
+
while (i + 1 < source.length && !(source[i] === "*" && source[i + 1] === "/"))
|
|
123
|
+
i++;
|
|
124
|
+
if (i + 1 < source.length)
|
|
125
|
+
i += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (i === 0 && source[i] === "#" && source[i + 1] === "!") {
|
|
129
|
+
while (i < source.length && source[i] !== "\n")
|
|
130
|
+
i++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
return Buffer.byteLength(source.slice(0, i), "utf8");
|
|
136
|
+
}
|
|
137
|
+
const LOAD_MODULE_RE = /\bloadModule\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(\s*(['"`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/gs;
|
|
138
|
+
const DYNAMIC_LOAD_MODULE_RE = /\bloadModule\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
|
|
139
|
+
function regexTransform(source, isServer, resourcePath) {
|
|
140
|
+
const transformed = source.replace(
|
|
141
|
+
LOAD_MODULE_RE,
|
|
142
|
+
(_match, quote, modulePath) => isServer ? `Promise.resolve(require(${quote}${modulePath}${quote}))` : `import(${quote}${modulePath}${quote})`
|
|
143
|
+
);
|
|
144
|
+
let match;
|
|
145
|
+
DYNAMIC_LOAD_MODULE_RE.lastIndex = 0;
|
|
146
|
+
while ((match = DYNAMIC_LOAD_MODULE_RE.exec(transformed)) !== null) {
|
|
147
|
+
const line = transformed.slice(0, match.index).split("\n").length;
|
|
148
|
+
this.emitWarning(
|
|
149
|
+
new Error(
|
|
150
|
+
`[hadars] loadModule() called with a dynamic (non-literal) path at ${resourcePath}:${line}. Only string-literal paths are transformed by the loader; dynamic calls fall back to runtime import().`
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return transformed;
|
|
34
155
|
}
|
|
@@ -108,6 +108,9 @@ async function runFullLifecycle(serialReq) {
|
|
|
108
108
|
await Promise.all(pending);
|
|
109
109
|
}
|
|
110
110
|
} while (unsuspend.hasPending && ++iters < 25);
|
|
111
|
+
if (unsuspend.hasPending) {
|
|
112
|
+
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.");
|
|
113
|
+
}
|
|
111
114
|
props = getAfterRenderProps ? await getAfterRenderProps(props, html) : props;
|
|
112
115
|
try {
|
|
113
116
|
globalThis.__hadarsUnsuspend = unsuspend;
|
package/dist/utils/Head.tsx
CHANGED
|
@@ -175,8 +175,10 @@ export const useApp = () => React.useContext(AppContext);
|
|
|
175
175
|
const clientServerDataCache = new Map<string, unknown>();
|
|
176
176
|
|
|
177
177
|
/** Call this before hydrating to seed the client cache from the server's data.
|
|
178
|
-
* Invoked automatically by the hadars client bootstrap.
|
|
178
|
+
* Invoked automatically by the hadars client bootstrap.
|
|
179
|
+
* Always clears the existing cache before populating — call with `{}` to just clear. */
|
|
179
180
|
export function initServerDataCache(data: Record<string, unknown>) {
|
|
181
|
+
clientServerDataCache.clear();
|
|
180
182
|
for (const [k, v] of Object.entries(data)) {
|
|
181
183
|
clientServerDataCache.set(k, v);
|
|
182
184
|
}
|
|
@@ -208,7 +210,7 @@ export function initServerDataCache(data: Record<string, unknown>) {
|
|
|
208
210
|
* if (!user) return null; // undefined while pending on the first SSR pass
|
|
209
211
|
*/
|
|
210
212
|
export function useServerData<T>(key: string | string[], fn: () => Promise<T> | T): T | undefined {
|
|
211
|
-
const cacheKey = Array.isArray(key) ?
|
|
213
|
+
const cacheKey = Array.isArray(key) ? JSON.stringify(key) : key;
|
|
212
214
|
|
|
213
215
|
if (typeof window !== 'undefined') {
|
|
214
216
|
// Client: if the server serialised a value for this key, return it directly
|
|
@@ -32,8 +32,12 @@ const main = async () => {
|
|
|
32
32
|
|
|
33
33
|
const { location } = props;
|
|
34
34
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
35
|
+
if (appMod.getClientProps) {
|
|
36
|
+
try {
|
|
37
|
+
props = await appMod.getClientProps(props);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error('[hadars] getClientProps threw an error:', err);
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
props = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hadars",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"build:lib": "tsup src/index.tsx --format esm,cjs --dts --out-dir dist --clean --external '@rspack/*' --external '@rspack/binding'",
|
|
33
33
|
"build:cli": "node build-scripts/build-cli.mjs",
|
|
34
34
|
"build:all": "npm run build:lib && npm run build:cli",
|
|
35
|
+
"test": "bun test test/ssr.test.ts",
|
|
35
36
|
"prepare": "npm run build:all",
|
|
36
37
|
"prepublishOnly": "npm run build:all"
|
|
37
38
|
},
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"@rspack/core": "1.4.9",
|
|
65
66
|
"@rspack/dev-server": "^1.1.4",
|
|
66
67
|
"@rspack/plugin-react-refresh": "^1.5.0",
|
|
68
|
+
"@swc/core": "^1.4.0",
|
|
67
69
|
"@types/bun": "latest",
|
|
68
70
|
"@types/react-dom": "^19.1.9",
|
|
69
71
|
"react-refresh": "^0.17.0",
|
|
@@ -73,8 +75,7 @@
|
|
|
73
75
|
"@types/react": "^19.0.0",
|
|
74
76
|
"@types/react-dom": "^19.0.0",
|
|
75
77
|
"esbuild": "^0.19.0",
|
|
76
|
-
"tsup": "^6.6.0"
|
|
77
|
-
"@swc/core": "^1.4.0"
|
|
78
|
+
"tsup": "^6.6.0"
|
|
78
79
|
},
|
|
79
80
|
"dependencies": {
|
|
80
81
|
"@mdx-js/loader": "^3.1.1",
|