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.
- package/README.md +16 -14
- package/dist/{chunk-H72BZXOA.js → chunk-2J2L2H3H.js} +21 -11
- package/dist/{chunk-LY5MTHFV.js → chunk-TV37IMRB.js} +50 -51
- package/dist/cli.js +83 -89
- package/dist/cloudflare.cjs +68 -59
- package/dist/cloudflare.js +2 -2
- package/dist/lambda.cjs +70 -67
- package/dist/lambda.js +4 -10
- package/dist/slim-react/index.cjs +50 -51
- package/dist/slim-react/index.js +1 -1
- package/dist/ssr-render-worker.js +46 -47
- package/dist/ssr-watch.js +9 -6
- package/package.json +2 -2
- package/src/build.ts +9 -5
- package/src/lambda.ts +7 -14
- package/src/slim-react/render.ts +39 -25
- package/src/slim-react/renderContext.ts +34 -11
- package/src/utils/rspack.ts +9 -6
- package/src/utils/serve.ts +1 -28
- package/src/utils/ssrHandler.ts +35 -10
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal server-side rendering framework for React built on [rspack](https://rspack.dev). Runs on Bun, Node.js, and Deno.
|
|
4
4
|
|
|
5
|
+
**[hadars.xyz](https://hadars.xyz)** — docs & website
|
|
6
|
+
|
|
5
7
|
## Why hadars?
|
|
6
8
|
|
|
7
9
|
hadars is an alternative to Next.js for apps that just need SSR.
|
|
@@ -19,30 +21,30 @@ Bring your own router (or none), keep your components as plain React, and get SS
|
|
|
19
21
|
## Benchmarks
|
|
20
22
|
|
|
21
23
|
<!-- BENCHMARK_START -->
|
|
22
|
-
> Last run: 2026-03-
|
|
23
|
-
> hadars is **8.
|
|
24
|
+
> Last run: 2026-03-25 · 120s · 100 connections · Bun runtime
|
|
25
|
+
> hadars is **8.9x faster** in requests/sec
|
|
24
26
|
|
|
25
27
|
**Throughput** (autocannon, 120s)
|
|
26
28
|
|
|
27
29
|
| Metric | hadars | Next.js |
|
|
28
30
|
|---|---:|---:|
|
|
29
|
-
| Requests/sec | **
|
|
30
|
-
| Latency median | **
|
|
31
|
-
| Latency p99 | **
|
|
32
|
-
| Throughput | **
|
|
33
|
-
| Peak RSS |
|
|
34
|
-
| Avg RSS |
|
|
35
|
-
| Build time | 0.
|
|
31
|
+
| Requests/sec | **151** | 17 |
|
|
32
|
+
| Latency median | **642 ms** | 2747 ms |
|
|
33
|
+
| Latency p99 | **959 ms** | 4019 ms |
|
|
34
|
+
| Throughput | **43.05** MB/s | 9.5 MB/s |
|
|
35
|
+
| Peak RSS | 950.3 MB | **478.5 MB** |
|
|
36
|
+
| Avg RSS | 763.3 MB | **426.4 MB** |
|
|
37
|
+
| Build time | 0.7 s | 6.0 s |
|
|
36
38
|
|
|
37
39
|
**Page load** (Playwright · Chromium headless · median)
|
|
38
40
|
|
|
39
41
|
| Metric | hadars | Next.js |
|
|
40
42
|
|---|---:|---:|
|
|
41
|
-
| TTFB | **
|
|
42
|
-
| FCP | **96 ms** |
|
|
43
|
-
| DOMContentLoaded | **39 ms** |
|
|
44
|
-
| Load | **122 ms** |
|
|
45
|
-
| Peak RSS |
|
|
43
|
+
| TTFB | **19 ms** | 42 ms |
|
|
44
|
+
| FCP | **96 ms** | 136 ms |
|
|
45
|
+
| DOMContentLoaded | **39 ms** | 127 ms |
|
|
46
|
+
| Load | **122 ms** | 173 ms |
|
|
47
|
+
| Peak RSS | 476.8 MB | **289.5 MB** |
|
|
46
48
|
<!-- BENCHMARK_END -->
|
|
47
49
|
|
|
48
50
|
## Quick start
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
renderPreflight,
|
|
3
3
|
renderToString
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-TV37IMRB.js";
|
|
5
5
|
import {
|
|
6
6
|
createElement
|
|
7
7
|
} from "./chunk-OS3V4CPN.js";
|
|
@@ -230,18 +230,18 @@ var makePrecontentHtmlGetter = (htmlFilePromise) => {
|
|
|
230
230
|
let preHead = null;
|
|
231
231
|
let postHead = null;
|
|
232
232
|
let postContent = null;
|
|
233
|
+
const primed = htmlFilePromise.then((html) => {
|
|
234
|
+
const headEnd = html.indexOf(HEAD_MARKER);
|
|
235
|
+
const contentStart = html.indexOf(BODY_MARKER);
|
|
236
|
+
preHead = html.slice(0, headEnd);
|
|
237
|
+
postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
|
|
238
|
+
postContent = html.slice(contentStart + BODY_MARKER.length);
|
|
239
|
+
});
|
|
233
240
|
return (headHtml) => {
|
|
234
241
|
if (preHead !== null) {
|
|
235
242
|
return [preHead + headHtml + postHead, postContent];
|
|
236
243
|
}
|
|
237
|
-
return
|
|
238
|
-
const headEnd = html.indexOf(HEAD_MARKER);
|
|
239
|
-
const contentStart = html.indexOf(BODY_MARKER);
|
|
240
|
-
preHead = html.slice(0, headEnd);
|
|
241
|
-
postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
|
|
242
|
-
postContent = html.slice(contentStart + BODY_MARKER.length);
|
|
243
|
-
return [preHead + headHtml + postHead, postContent];
|
|
244
|
-
});
|
|
244
|
+
return primed.then(() => [preHead + headHtml + postHead, postContent]);
|
|
245
245
|
};
|
|
246
246
|
};
|
|
247
247
|
async function transformStream(data, stream) {
|
|
@@ -264,8 +264,18 @@ async function transformStream(data, stream) {
|
|
|
264
264
|
}
|
|
265
265
|
return out;
|
|
266
266
|
}
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
async function zlibGzip(d) {
|
|
268
|
+
const zlib = await import("node:zlib");
|
|
269
|
+
const { promisify } = await import("node:util");
|
|
270
|
+
return promisify(zlib.gzip)(d);
|
|
271
|
+
}
|
|
272
|
+
async function zlibGunzip(d) {
|
|
273
|
+
const zlib = await import("node:zlib");
|
|
274
|
+
const { promisify } = await import("node:util");
|
|
275
|
+
return promisify(zlib.gunzip)(d);
|
|
276
|
+
}
|
|
277
|
+
var gzipCompress = (d) => globalThis.CompressionStream ? transformStream(d, new globalThis.CompressionStream("gzip")) : zlibGzip(d);
|
|
278
|
+
var gzipDecompress = (d) => globalThis.DecompressionStream ? transformStream(d, new globalThis.DecompressionStream("gzip")) : zlibGunzip(d);
|
|
269
279
|
async function buildCacheEntry(res, ttl) {
|
|
270
280
|
const buf = await res.arrayBuffer();
|
|
271
281
|
const body = await gzipCompress(new Uint8Array(buf));
|
|
@@ -115,7 +115,16 @@ function componentCalledUseId() {
|
|
|
115
115
|
function snapshotContext() {
|
|
116
116
|
const st = s();
|
|
117
117
|
const ctx = st.currentTreeContext;
|
|
118
|
-
|
|
118
|
+
const depth = _treeDepth;
|
|
119
|
+
return {
|
|
120
|
+
tree: { id: ctx.id, overflow: ctx.overflow },
|
|
121
|
+
localId: st.localIdCounter,
|
|
122
|
+
treeDepth: depth,
|
|
123
|
+
// Snapshot the live stack so that popTreeContext reads correct saved values
|
|
124
|
+
// even if another concurrent render's resetRenderState stomped the arrays.
|
|
125
|
+
idStack: _treeIdStack.slice(0, depth),
|
|
126
|
+
ovStack: _treeOvStack.slice(0, depth)
|
|
127
|
+
};
|
|
119
128
|
}
|
|
120
129
|
function restoreContext(snap) {
|
|
121
130
|
const st = s();
|
|
@@ -124,6 +133,10 @@ function restoreContext(snap) {
|
|
|
124
133
|
ctx.overflow = snap.tree.overflow;
|
|
125
134
|
st.localIdCounter = snap.localId;
|
|
126
135
|
_treeDepth = snap.treeDepth;
|
|
136
|
+
for (let i = 0; i < snap.treeDepth; i++) {
|
|
137
|
+
_treeIdStack[i] = snap.idStack[i];
|
|
138
|
+
_treeOvStack[i] = snap.ovStack[i];
|
|
139
|
+
}
|
|
127
140
|
}
|
|
128
141
|
function getTreeId() {
|
|
129
142
|
const { id, overflow } = s().currentTreeContext;
|
|
@@ -269,6 +282,14 @@ function restoreDispatcher(prev) {
|
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
// src/slim-react/render.ts
|
|
285
|
+
function captureRenderCtx() {
|
|
286
|
+
return { m: captureMap(), u: captureUnsuspend(), t: snapshotContext() };
|
|
287
|
+
}
|
|
288
|
+
function restoreRenderCtx(ctx) {
|
|
289
|
+
swapContextMap(ctx.m);
|
|
290
|
+
restoreUnsuspend(ctx.u);
|
|
291
|
+
restoreContext(ctx.t);
|
|
292
|
+
}
|
|
272
293
|
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
273
294
|
"area",
|
|
274
295
|
"base",
|
|
@@ -712,11 +733,9 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
712
733
|
if (e && typeof e.then === "function") {
|
|
713
734
|
if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
|
|
714
735
|
patchPromiseStatus(e);
|
|
715
|
-
const
|
|
716
|
-
const u = captureUnsuspend();
|
|
736
|
+
const rctx = captureRenderCtx();
|
|
717
737
|
return e.then(() => {
|
|
718
|
-
|
|
719
|
-
restoreUnsuspend(u);
|
|
738
|
+
restoreRenderCtx(rctx);
|
|
720
739
|
return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
|
|
721
740
|
});
|
|
722
741
|
}
|
|
@@ -753,17 +772,14 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
753
772
|
};
|
|
754
773
|
const r2 = renderChildren(props.children, writer, isSvg);
|
|
755
774
|
if (r2 && typeof r2.then === "function") {
|
|
756
|
-
const
|
|
757
|
-
const u = captureUnsuspend();
|
|
775
|
+
const rctx = captureRenderCtx();
|
|
758
776
|
return r2.then(
|
|
759
777
|
() => {
|
|
760
|
-
|
|
761
|
-
restoreUnsuspend(u);
|
|
778
|
+
restoreRenderCtx(rctx);
|
|
762
779
|
finish();
|
|
763
780
|
},
|
|
764
781
|
(e) => {
|
|
765
|
-
|
|
766
|
-
restoreUnsuspend(u);
|
|
782
|
+
restoreRenderCtx(rctx);
|
|
767
783
|
finish();
|
|
768
784
|
throw e;
|
|
769
785
|
}
|
|
@@ -792,11 +808,9 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
792
808
|
if (e && typeof e.then === "function") {
|
|
793
809
|
if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
|
|
794
810
|
patchPromiseStatus(e);
|
|
795
|
-
const
|
|
796
|
-
const u = captureUnsuspend();
|
|
811
|
+
const rctx = captureRenderCtx();
|
|
797
812
|
return e.then(() => {
|
|
798
|
-
|
|
799
|
-
restoreUnsuspend(u);
|
|
813
|
+
restoreRenderCtx(rctx);
|
|
800
814
|
return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
|
|
801
815
|
});
|
|
802
816
|
}
|
|
@@ -808,30 +822,25 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
808
822
|
savedIdTree = pushTreeContext(1, 0);
|
|
809
823
|
}
|
|
810
824
|
if (result instanceof Promise) {
|
|
811
|
-
const
|
|
812
|
-
const u = captureUnsuspend();
|
|
825
|
+
const rctx = captureRenderCtx();
|
|
813
826
|
return result.then((resolved) => {
|
|
814
|
-
|
|
815
|
-
restoreUnsuspend(u);
|
|
827
|
+
restoreRenderCtx(rctx);
|
|
816
828
|
let asyncSavedIdTree;
|
|
817
829
|
if (componentCalledUseId()) {
|
|
818
830
|
asyncSavedIdTree = pushTreeContext(1, 0);
|
|
819
831
|
}
|
|
820
832
|
const r2 = renderNode(resolved, writer, isSvg);
|
|
821
833
|
if (r2 && typeof r2.then === "function") {
|
|
822
|
-
const
|
|
823
|
-
const u2 = captureUnsuspend();
|
|
834
|
+
const rctx2 = captureRenderCtx();
|
|
824
835
|
return r2.then(
|
|
825
836
|
() => {
|
|
826
|
-
|
|
827
|
-
restoreUnsuspend(u2);
|
|
837
|
+
restoreRenderCtx(rctx2);
|
|
828
838
|
if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
|
|
829
839
|
popComponentScope(savedScope);
|
|
830
840
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
831
841
|
},
|
|
832
842
|
(e) => {
|
|
833
|
-
|
|
834
|
-
restoreUnsuspend(u2);
|
|
843
|
+
restoreRenderCtx(rctx2);
|
|
835
844
|
if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
|
|
836
845
|
popComponentScope(savedScope);
|
|
837
846
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
@@ -843,8 +852,7 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
843
852
|
popComponentScope(savedScope);
|
|
844
853
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
845
854
|
}, (e) => {
|
|
846
|
-
|
|
847
|
-
restoreUnsuspend(u);
|
|
855
|
+
restoreRenderCtx(rctx);
|
|
848
856
|
popComponentScope(savedScope);
|
|
849
857
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
850
858
|
throw e;
|
|
@@ -852,19 +860,16 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
|
|
|
852
860
|
}
|
|
853
861
|
const r = renderNode(result, writer, isSvg);
|
|
854
862
|
if (r && typeof r.then === "function") {
|
|
855
|
-
const
|
|
856
|
-
const u = captureUnsuspend();
|
|
863
|
+
const rctx = captureRenderCtx();
|
|
857
864
|
return r.then(
|
|
858
865
|
() => {
|
|
859
|
-
|
|
860
|
-
restoreUnsuspend(u);
|
|
866
|
+
restoreRenderCtx(rctx);
|
|
861
867
|
if (savedIdTree !== void 0) popTreeContext(savedIdTree);
|
|
862
868
|
popComponentScope(savedScope);
|
|
863
869
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
864
870
|
},
|
|
865
871
|
(e) => {
|
|
866
|
-
|
|
867
|
-
restoreUnsuspend(u);
|
|
872
|
+
restoreRenderCtx(rctx);
|
|
868
873
|
if (savedIdTree !== void 0) popTreeContext(savedIdTree);
|
|
869
874
|
popComponentScope(savedScope);
|
|
870
875
|
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
@@ -889,11 +894,9 @@ function renderChildArrayFrom(children, startIndex, writer, isSvg) {
|
|
|
889
894
|
const savedTree = pushTreeContext(totalChildren, i);
|
|
890
895
|
const r = renderNode(child, writer, isSvg);
|
|
891
896
|
if (r && typeof r.then === "function") {
|
|
892
|
-
const
|
|
893
|
-
const u = captureUnsuspend();
|
|
897
|
+
const rctx = captureRenderCtx();
|
|
894
898
|
return r.then(() => {
|
|
895
|
-
|
|
896
|
-
restoreUnsuspend(u);
|
|
899
|
+
restoreRenderCtx(rctx);
|
|
897
900
|
popTreeContext(savedTree);
|
|
898
901
|
return renderChildArrayFrom(children, i + 1, writer, isSvg);
|
|
899
902
|
});
|
|
@@ -917,11 +920,9 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
917
920
|
try {
|
|
918
921
|
const r = renderNode(children, buffer, isSvg);
|
|
919
922
|
if (r && typeof r.then === "function") {
|
|
920
|
-
const
|
|
921
|
-
const u = captureUnsuspend();
|
|
923
|
+
const rctx = captureRenderCtx();
|
|
922
924
|
await r;
|
|
923
|
-
|
|
924
|
-
restoreUnsuspend(u);
|
|
925
|
+
restoreRenderCtx(rctx);
|
|
925
926
|
}
|
|
926
927
|
writer.write("<!--$-->");
|
|
927
928
|
buffer.flushTo(writer);
|
|
@@ -935,11 +936,9 @@ async function renderSuspense(props, writer, isSvg = false) {
|
|
|
935
936
|
if (fallback) {
|
|
936
937
|
const r = renderNode(fallback, writer, isSvg);
|
|
937
938
|
if (r && typeof r.then === "function") {
|
|
938
|
-
const
|
|
939
|
-
const u = captureUnsuspend();
|
|
939
|
+
const rctx = captureRenderCtx();
|
|
940
940
|
await r;
|
|
941
|
-
|
|
942
|
-
restoreUnsuspend(u);
|
|
941
|
+
restoreRenderCtx(rctx);
|
|
943
942
|
}
|
|
944
943
|
}
|
|
945
944
|
writer.write("<!--/$-->");
|
|
@@ -976,9 +975,9 @@ function renderToStream(element, options) {
|
|
|
976
975
|
try {
|
|
977
976
|
const r = renderNode(element, writer);
|
|
978
977
|
if (r && typeof r.then === "function") {
|
|
979
|
-
const
|
|
978
|
+
const rctx = captureRenderCtx();
|
|
980
979
|
await r;
|
|
981
|
-
|
|
980
|
+
restoreRenderCtx(rctx);
|
|
982
981
|
}
|
|
983
982
|
writer.flush();
|
|
984
983
|
controller.close();
|
|
@@ -1005,9 +1004,9 @@ async function renderPreflight(element, options) {
|
|
|
1005
1004
|
NULL_WRITER.lastWasText = false;
|
|
1006
1005
|
const r = renderNode(element, NULL_WRITER);
|
|
1007
1006
|
if (r && typeof r.then === "function") {
|
|
1008
|
-
const
|
|
1007
|
+
const rctx = captureRenderCtx();
|
|
1009
1008
|
await r;
|
|
1010
|
-
|
|
1009
|
+
restoreRenderCtx(rctx);
|
|
1011
1010
|
}
|
|
1012
1011
|
} finally {
|
|
1013
1012
|
swapContextMap(prev);
|
|
@@ -1032,9 +1031,9 @@ async function renderToString(element, options) {
|
|
|
1032
1031
|
resetRenderState(idPrefix);
|
|
1033
1032
|
const r = renderNode(element, writer);
|
|
1034
1033
|
if (r && typeof r.then === "function") {
|
|
1035
|
-
const
|
|
1034
|
+
const rctx = captureRenderCtx();
|
|
1036
1035
|
await r;
|
|
1037
|
-
|
|
1036
|
+
restoreRenderCtx(rctx);
|
|
1038
1037
|
}
|
|
1039
1038
|
return output;
|
|
1040
1039
|
} finally {
|