hadars 0.1.40-rc.3 → 0.1.40
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 +77 -2
- package/dist/cli.js +14 -60
- package/dist/{hadars-BmgNX2zr.d.cts → hadars-Bh-V5YXg.d.cts} +1 -3
- package/dist/{hadars-BmgNX2zr.d.ts → hadars-Bh-V5YXg.d.ts} +1 -3
- package/dist/index.cjs +88 -114
- package/dist/index.d.cts +3 -42
- package/dist/index.d.ts +3 -42
- package/dist/index.js +88 -91
- package/dist/lambda.cjs +25 -70
- package/dist/lambda.d.cts +1 -1
- package/dist/lambda.d.ts +1 -1
- package/dist/lambda.js +25 -36
- package/dist/ssr-render-worker.js +4 -56
- package/dist/utils/Head.tsx +90 -79
- package/index.ts +1 -2
- package/package.json +1 -1
- package/src/build.ts +0 -4
- package/src/index.tsx +0 -3
- package/src/lambda.ts +19 -17
- package/src/ssr-render-worker.ts +7 -26
- package/src/types/hadars.ts +0 -2
- package/src/utils/Head.tsx +90 -79
- package/src/utils/response.tsx +13 -20
- package/dist/chunk-UNQSQIOO.js +0 -60
- package/src/components/CacheSegment.tsx +0 -67
- package/src/utils/segmentCache.ts +0 -87
package/README.md
CHANGED
|
@@ -18,8 +18,29 @@ Bring your own router (or none), keep your components as plain React, and get SS
|
|
|
18
18
|
|
|
19
19
|
## Benchmarks
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
<!-- BENCHMARK_START -->
|
|
22
|
+
> Last run: 2026-03-18 · 60s · 200 connections · Bun runtime
|
|
23
|
+
> hadars is **7.8x faster** in requests/sec
|
|
24
|
+
|
|
25
|
+
**Throughput** (autocannon, 60s)
|
|
26
|
+
|
|
27
|
+
| Metric | hadars | Next.js |
|
|
28
|
+
|---|---:|---:|
|
|
29
|
+
| Requests/sec | **132** | 17 |
|
|
30
|
+
| Latency median | **1490 ms** | 2757 ms |
|
|
31
|
+
| Latency p99 | **2289 ms** | 6742 ms |
|
|
32
|
+
| Throughput | **37.38** MB/s | 9.37 MB/s |
|
|
33
|
+
| Build time | 0.7 s | 6.3 s |
|
|
34
|
+
|
|
35
|
+
**Page load** (Playwright · Chromium headless · median)
|
|
36
|
+
|
|
37
|
+
| Metric | hadars | Next.js |
|
|
38
|
+
|---|---:|---:|
|
|
39
|
+
| TTFB | **22 ms** | 42 ms |
|
|
40
|
+
| FCP | **124 ms** | 136 ms |
|
|
41
|
+
| DOMContentLoaded | **88 ms** | 126 ms |
|
|
42
|
+
| Load | **155 ms** | 173 ms |
|
|
43
|
+
<!-- BENCHMARK_END -->
|
|
23
44
|
## Quick start
|
|
24
45
|
|
|
25
46
|
Scaffold a new project in seconds:
|
|
@@ -123,6 +144,60 @@ const UserCard = ({ userId }: { userId: string }) => {
|
|
|
123
144
|
- **Client (hydration)** - reads the pre-resolved value from the hydration cache serialised by the server; `fn()` is never called in the browser
|
|
124
145
|
- **Client (navigation)** - when a component mounts during client-side navigation and its key is not in the cache, hadars fires a single `GET <current-url>` with `Accept: application/json`; all `useServerData` calls within the same render are batched into one request and suspended via React Suspense until the server returns the JSON data map
|
|
125
146
|
|
|
147
|
+
## HadarsHead
|
|
148
|
+
|
|
149
|
+
`HadarsHead` (exported as `Head`) manages `<title>`, `<meta>`, `<link>`, `<script>`, and `<style>` tags across both SSR and client renders. On the server it collects tags into the HTML `<head>`; on the client it upserts them into `document.head`.
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { HadarsHead } from 'hadars';
|
|
153
|
+
|
|
154
|
+
const Page = ({ context }) => (
|
|
155
|
+
<HadarsContext context={context}>
|
|
156
|
+
<HadarsHead status={200}>
|
|
157
|
+
<title>My page</title>
|
|
158
|
+
<meta name="description" content="A great page" />
|
|
159
|
+
<meta property="og:title" content="My page" />
|
|
160
|
+
<link rel="canonical" href="https://example.com/page" />
|
|
161
|
+
<link rel="stylesheet" href="/styles/page.css" />
|
|
162
|
+
<style data-id="page-critical">{`body { margin: 0 }`}</style>
|
|
163
|
+
<script src="/vendor/analytics.js" async />
|
|
164
|
+
<script data-id="inline-config" dangerouslySetInnerHTML={{ __html: 'var X=1' }} />
|
|
165
|
+
</HadarsHead>
|
|
166
|
+
...
|
|
167
|
+
</HadarsContext>
|
|
168
|
+
);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Deduplication
|
|
172
|
+
|
|
173
|
+
Each element type has a natural dedup key derived from its identifying attributes. Rendering the same `<Head>` block on multiple components or re-renders will not produce duplicate tags.
|
|
174
|
+
|
|
175
|
+
| Element | Dedup key |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `<title>` | always singular — last write wins |
|
|
178
|
+
| `<meta name="…">` | `name` value |
|
|
179
|
+
| `<meta property="…">` | `property` value |
|
|
180
|
+
| `<meta http-equiv="…">` | `http-equiv` value |
|
|
181
|
+
| `<meta charSet>` | singular — one charset per page |
|
|
182
|
+
| `<link rel="…" href="…">` | `rel` + `href` (+ `as` when present) |
|
|
183
|
+
| `<link rel="…">` (no href) | `rel` alone (e.g. `rel="preconnect"`) |
|
|
184
|
+
| `<script src="…">` | `src` URL |
|
|
185
|
+
| `<script data-id="…">` | `data-id` value |
|
|
186
|
+
| `<style data-id="…">` | `data-id` value |
|
|
187
|
+
|
|
188
|
+
### `data-id` for inline tags
|
|
189
|
+
|
|
190
|
+
Inline `<script>` (no `src`) and `<style>` elements have no natural URL to key on. Provide a `data-id` prop so hadars can find and update the same element across re-renders rather than appending a duplicate:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
<HadarsHead>
|
|
194
|
+
<style data-id="critical-css">{criticalStyles}</style>
|
|
195
|
+
<script data-id="gtm-config" dangerouslySetInnerHTML={{ __html: gtmSnippet }} />
|
|
196
|
+
</HadarsHead>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Omitting `data-id` on these elements triggers a console warning at render time and falls back to append-only behaviour (safe for one-time static tags, not for anything that re-renders).
|
|
200
|
+
|
|
126
201
|
## Data lifecycle hooks
|
|
127
202
|
|
|
128
203
|
| Hook | Runs on | Purpose |
|
package/dist/cli.js
CHANGED
|
@@ -980,43 +980,6 @@ async function renderToString(element, options) {
|
|
|
980
980
|
}
|
|
981
981
|
}
|
|
982
982
|
|
|
983
|
-
// src/utils/segmentCache.ts
|
|
984
|
-
function getStore() {
|
|
985
|
-
const g = globalThis;
|
|
986
|
-
if (!g.__hadarsSegmentStore) {
|
|
987
|
-
g.__hadarsSegmentStore = /* @__PURE__ */ new Map();
|
|
988
|
-
}
|
|
989
|
-
return g.__hadarsSegmentStore;
|
|
990
|
-
}
|
|
991
|
-
function setSegment(key, html, ttl) {
|
|
992
|
-
getStore().set(key, {
|
|
993
|
-
html,
|
|
994
|
-
expiresAt: ttl != null ? Date.now() + ttl : null
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
function processSegmentCache(html) {
|
|
998
|
-
let prev;
|
|
999
|
-
do {
|
|
1000
|
-
prev = html;
|
|
1001
|
-
html = html.replace(
|
|
1002
|
-
/<hadars-c([^>]*)>([\s\S]*?)<\/hadars-c>/g,
|
|
1003
|
-
(match, attrs, content) => {
|
|
1004
|
-
const cacheM = /data-cache="([^"]+)"/.exec(attrs);
|
|
1005
|
-
const keyM = /data-key="([^"]+)"/.exec(attrs);
|
|
1006
|
-
const ttlM = /data-ttl="(\d+)"/.exec(attrs);
|
|
1007
|
-
if (!cacheM || !keyM) return match;
|
|
1008
|
-
if (cacheM[1] === "miss") {
|
|
1009
|
-
setSegment(keyM[1], content, ttlM ? Number(ttlM[1]) : void 0);
|
|
1010
|
-
return content;
|
|
1011
|
-
}
|
|
1012
|
-
if (cacheM[1] === "hit") return content;
|
|
1013
|
-
return match;
|
|
1014
|
-
}
|
|
1015
|
-
);
|
|
1016
|
-
} while (html !== prev);
|
|
1017
|
-
return html;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
983
|
// src/utils/response.tsx
|
|
1021
984
|
var ESC = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
1022
985
|
var escAttr = (s2) => s2.replace(/[&<>"]/g, (c) => ESC[c] ?? c);
|
|
@@ -1029,10 +992,11 @@ var ATTR = {
|
|
|
1029
992
|
crossOrigin: "crossorigin",
|
|
1030
993
|
noModule: "nomodule",
|
|
1031
994
|
referrerPolicy: "referrerpolicy",
|
|
1032
|
-
fetchPriority: "fetchpriority"
|
|
995
|
+
fetchPriority: "fetchpriority",
|
|
996
|
+
hrefLang: "hreflang"
|
|
1033
997
|
};
|
|
1034
|
-
function renderHeadTag(tag,
|
|
1035
|
-
let attrs =
|
|
998
|
+
function renderHeadTag(tag, opts, selfClose = false) {
|
|
999
|
+
let attrs = "";
|
|
1036
1000
|
let inner = "";
|
|
1037
1001
|
for (const [k, v] of Object.entries(opts)) {
|
|
1038
1002
|
if (k === "key" || k === "children") continue;
|
|
@@ -1048,19 +1012,19 @@ function renderHeadTag(tag, id, opts, selfClose = false) {
|
|
|
1048
1012
|
}
|
|
1049
1013
|
var getHeadHtml = (seoData) => {
|
|
1050
1014
|
let html = `<title>${escText(seoData.title ?? "")}</title>`;
|
|
1051
|
-
for (const
|
|
1052
|
-
html += renderHeadTag("meta",
|
|
1053
|
-
for (const
|
|
1054
|
-
html += renderHeadTag("link",
|
|
1055
|
-
for (const
|
|
1056
|
-
html += renderHeadTag("style",
|
|
1057
|
-
for (const
|
|
1058
|
-
html += renderHeadTag("script",
|
|
1015
|
+
for (const opts of Object.values(seoData.meta))
|
|
1016
|
+
html += renderHeadTag("meta", opts, true);
|
|
1017
|
+
for (const opts of Object.values(seoData.link))
|
|
1018
|
+
html += renderHeadTag("link", opts, true);
|
|
1019
|
+
for (const opts of Object.values(seoData.style))
|
|
1020
|
+
html += renderHeadTag("style", opts);
|
|
1021
|
+
for (const opts of Object.values(seoData.script))
|
|
1022
|
+
html += renderHeadTag("script", opts);
|
|
1059
1023
|
return html;
|
|
1060
1024
|
};
|
|
1061
1025
|
var getReactResponse = async (req, opts) => {
|
|
1062
1026
|
const App = opts.document.body;
|
|
1063
|
-
const { getInitProps,
|
|
1027
|
+
const { getInitProps, getFinalProps } = opts.document;
|
|
1064
1028
|
const context = {
|
|
1065
1029
|
head: { title: "Hadars App", meta: {}, link: {}, style: {}, script: {}, status: 200 }
|
|
1066
1030
|
};
|
|
@@ -1074,12 +1038,6 @@ var getReactResponse = async (req, opts) => {
|
|
|
1074
1038
|
let bodyHtml;
|
|
1075
1039
|
try {
|
|
1076
1040
|
bodyHtml = await renderToString(createElement(App, props));
|
|
1077
|
-
if (getAfterRenderProps) {
|
|
1078
|
-
props = await getAfterRenderProps(props, bodyHtml);
|
|
1079
|
-
bodyHtml = await renderToString(
|
|
1080
|
-
createElement(App, { ...props, location: req.location, context })
|
|
1081
|
-
);
|
|
1082
|
-
}
|
|
1083
1041
|
} finally {
|
|
1084
1042
|
globalThis.__hadarsUnsuspend = null;
|
|
1085
1043
|
}
|
|
@@ -1094,7 +1052,7 @@ var getReactResponse = async (req, opts) => {
|
|
|
1094
1052
|
...Object.keys(serverData).length > 0 ? { __serverData: serverData } : {}
|
|
1095
1053
|
};
|
|
1096
1054
|
return {
|
|
1097
|
-
bodyHtml
|
|
1055
|
+
bodyHtml,
|
|
1098
1056
|
clientProps,
|
|
1099
1057
|
status: context.head.status,
|
|
1100
1058
|
headHtml: getHeadHtml(context.head)
|
|
@@ -2157,7 +2115,6 @@ var dev = async (options) => {
|
|
|
2157
2115
|
const {
|
|
2158
2116
|
default: Component,
|
|
2159
2117
|
getInitProps,
|
|
2160
|
-
getAfterRenderProps,
|
|
2161
2118
|
getFinalProps
|
|
2162
2119
|
} = await import(importPath);
|
|
2163
2120
|
const { bodyHtml, clientProps, status, headHtml } = await getReactResponse(request, {
|
|
@@ -2165,7 +2122,6 @@ var dev = async (options) => {
|
|
|
2165
2122
|
body: Component,
|
|
2166
2123
|
lang: "en",
|
|
2167
2124
|
getInitProps,
|
|
2168
|
-
getAfterRenderProps,
|
|
2169
2125
|
getFinalProps
|
|
2170
2126
|
}
|
|
2171
2127
|
});
|
|
@@ -2307,7 +2263,6 @@ var run = async (options) => {
|
|
|
2307
2263
|
const {
|
|
2308
2264
|
default: Component,
|
|
2309
2265
|
getInitProps,
|
|
2310
|
-
getAfterRenderProps,
|
|
2311
2266
|
getFinalProps
|
|
2312
2267
|
} = await import(componentPath);
|
|
2313
2268
|
if (renderPool && request.headers.get("Accept") !== "application/json") {
|
|
@@ -2324,7 +2279,6 @@ var run = async (options) => {
|
|
|
2324
2279
|
body: Component,
|
|
2325
2280
|
lang: "en",
|
|
2326
2281
|
getInitProps,
|
|
2327
|
-
getAfterRenderProps,
|
|
2328
2282
|
getFinalProps
|
|
2329
2283
|
}
|
|
2330
2284
|
});
|
|
@@ -2,13 +2,11 @@ import { MetaHTMLAttributes, LinkHTMLAttributes, StyleHTMLAttributes, ScriptHTML
|
|
|
2
2
|
|
|
3
3
|
type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest) => Promise<T> | T;
|
|
4
4
|
type HadarsGetClientProps<T extends {}> = (props: T) => Promise<T> | T;
|
|
5
|
-
type HadarsGetAfterRenderProps<T extends {}> = (props: HadarsProps<T>, html: string) => Promise<HadarsProps<T>> | HadarsProps<T>;
|
|
6
5
|
type HadarsGetFinalProps<T extends {}> = (props: HadarsProps<T>) => Promise<T> | T;
|
|
7
6
|
type HadarsApp<T extends {}> = React.FC<HadarsProps<T>>;
|
|
8
7
|
type HadarsEntryModule<T extends {}> = {
|
|
9
8
|
default: HadarsApp<T>;
|
|
10
9
|
getInitProps?: HadarsGetInitialProps<T>;
|
|
11
|
-
getAfterRenderProps?: HadarsGetAfterRenderProps<T>;
|
|
12
10
|
getFinalProps?: HadarsGetFinalProps<T>;
|
|
13
11
|
getClientProps?: HadarsGetClientProps<T>;
|
|
14
12
|
};
|
|
@@ -198,4 +196,4 @@ interface HadarsRequest extends Request {
|
|
|
198
196
|
cookies: Record<string, string>;
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
export type { AppContext as A, HadarsEntryModule as H, HadarsOptions as a, HadarsApp as b,
|
|
199
|
+
export type { AppContext as A, HadarsEntryModule as H, HadarsOptions as a, HadarsApp as b, HadarsGetClientProps as c, HadarsGetFinalProps as d, HadarsGetInitialProps as e, HadarsProps as f, HadarsRequest as g };
|
|
@@ -2,13 +2,11 @@ import { MetaHTMLAttributes, LinkHTMLAttributes, StyleHTMLAttributes, ScriptHTML
|
|
|
2
2
|
|
|
3
3
|
type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest) => Promise<T> | T;
|
|
4
4
|
type HadarsGetClientProps<T extends {}> = (props: T) => Promise<T> | T;
|
|
5
|
-
type HadarsGetAfterRenderProps<T extends {}> = (props: HadarsProps<T>, html: string) => Promise<HadarsProps<T>> | HadarsProps<T>;
|
|
6
5
|
type HadarsGetFinalProps<T extends {}> = (props: HadarsProps<T>) => Promise<T> | T;
|
|
7
6
|
type HadarsApp<T extends {}> = React.FC<HadarsProps<T>>;
|
|
8
7
|
type HadarsEntryModule<T extends {}> = {
|
|
9
8
|
default: HadarsApp<T>;
|
|
10
9
|
getInitProps?: HadarsGetInitialProps<T>;
|
|
11
|
-
getAfterRenderProps?: HadarsGetAfterRenderProps<T>;
|
|
12
10
|
getFinalProps?: HadarsGetFinalProps<T>;
|
|
13
11
|
getClientProps?: HadarsGetClientProps<T>;
|
|
14
12
|
};
|
|
@@ -198,4 +196,4 @@ interface HadarsRequest extends Request {
|
|
|
198
196
|
cookies: Record<string, string>;
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
export type { AppContext as A, HadarsEntryModule as H, HadarsOptions as a, HadarsApp as b,
|
|
199
|
+
export type { AppContext as A, HadarsEntryModule as H, HadarsOptions as a, HadarsApp as b, HadarsGetClientProps as c, HadarsGetFinalProps as d, HadarsGetInitialProps as e, HadarsProps as f, HadarsRequest as g };
|
package/dist/index.cjs
CHANGED
|
@@ -30,11 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.tsx
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
CacheSegment: () => CacheSegment,
|
|
34
33
|
HadarsContext: () => HadarsContext,
|
|
35
34
|
HadarsHead: () => Head,
|
|
36
|
-
clearSegments: () => clearSegments,
|
|
37
|
-
deleteSegment: () => deleteSegment,
|
|
38
35
|
initServerDataCache: () => initServerDataCache,
|
|
39
36
|
loadModule: () => loadModule,
|
|
40
37
|
useServerData: () => useServerData
|
|
@@ -44,6 +41,35 @@ module.exports = __toCommonJS(index_exports);
|
|
|
44
41
|
// src/utils/Head.tsx
|
|
45
42
|
var import_react = __toESM(require("react"), 1);
|
|
46
43
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
44
|
+
function deriveKey(tag, props) {
|
|
45
|
+
switch (tag) {
|
|
46
|
+
case "meta": {
|
|
47
|
+
if (props.name) return `meta:name:${props.name}`;
|
|
48
|
+
if (props.property) return `meta:property:${props.property}`;
|
|
49
|
+
const httpEquiv = props.httpEquiv ?? props["http-equiv"];
|
|
50
|
+
if (httpEquiv) return `meta:http-equiv:${httpEquiv}`;
|
|
51
|
+
if ("charSet" in props || "charset" in props) return "meta:charset";
|
|
52
|
+
return `meta:${JSON.stringify(props)}`;
|
|
53
|
+
}
|
|
54
|
+
case "link": {
|
|
55
|
+
const rel = props.rel ?? "";
|
|
56
|
+
const href = props.href ?? "";
|
|
57
|
+
const as_ = props.as ? `:as:${props.as}` : "";
|
|
58
|
+
return href ? `link:${rel}:${href}${as_}` : `link:${rel}${as_}`;
|
|
59
|
+
}
|
|
60
|
+
case "script": {
|
|
61
|
+
if (props.src) return `script:src:${props.src}`;
|
|
62
|
+
if (props["data-id"]) return `script:id:${props["data-id"]}`;
|
|
63
|
+
return `script:${JSON.stringify(props)}`;
|
|
64
|
+
}
|
|
65
|
+
case "style": {
|
|
66
|
+
if (props["data-id"]) return `style:id:${props["data-id"]}`;
|
|
67
|
+
return `style:${JSON.stringify(props)}`;
|
|
68
|
+
}
|
|
69
|
+
default:
|
|
70
|
+
return `${tag}:${JSON.stringify(props)}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
47
73
|
var AppContext = import_react.default.createContext({
|
|
48
74
|
setTitle: () => {
|
|
49
75
|
console.warn("AppContext: setTitle called outside of provider");
|
|
@@ -68,17 +94,17 @@ var AppProviderSSR = import_react.default.memo(({ children, context }) => {
|
|
|
68
94
|
const setTitle = import_react.default.useCallback((title) => {
|
|
69
95
|
head.title = title;
|
|
70
96
|
}, [head]);
|
|
71
|
-
const addMeta = import_react.default.useCallback((
|
|
72
|
-
head.meta[
|
|
97
|
+
const addMeta = import_react.default.useCallback((props) => {
|
|
98
|
+
head.meta[deriveKey("meta", props)] = props;
|
|
73
99
|
}, [head]);
|
|
74
|
-
const addLink = import_react.default.useCallback((
|
|
75
|
-
head.link[
|
|
100
|
+
const addLink = import_react.default.useCallback((props) => {
|
|
101
|
+
head.link[deriveKey("link", props)] = props;
|
|
76
102
|
}, [head]);
|
|
77
|
-
const addStyle = import_react.default.useCallback((
|
|
78
|
-
head.style[
|
|
103
|
+
const addStyle = import_react.default.useCallback((props) => {
|
|
104
|
+
head.style[deriveKey("style", props)] = props;
|
|
79
105
|
}, [head]);
|
|
80
|
-
const addScript = import_react.default.useCallback((
|
|
81
|
-
head.script[
|
|
106
|
+
const addScript = import_react.default.useCallback((props) => {
|
|
107
|
+
head.script[deriveKey("script", props)] = props;
|
|
82
108
|
}, [head]);
|
|
83
109
|
const setStatus = import_react.default.useCallback((status) => {
|
|
84
110
|
head.status = status;
|
|
@@ -97,69 +123,68 @@ var AppProviderCSR = import_react.default.memo(({ children }) => {
|
|
|
97
123
|
const setTitle = import_react.default.useCallback((title) => {
|
|
98
124
|
document.title = title;
|
|
99
125
|
}, []);
|
|
100
|
-
const addMeta = import_react.default.useCallback((
|
|
101
|
-
|
|
126
|
+
const addMeta = import_react.default.useCallback((props) => {
|
|
127
|
+
const p = props;
|
|
128
|
+
let meta = null;
|
|
129
|
+
if (p.name) meta = document.querySelector(`meta[name="${CSS.escape(p.name)}"]`);
|
|
130
|
+
else if (p.property) meta = document.querySelector(`meta[property="${CSS.escape(p.property)}"]`);
|
|
131
|
+
else if (p.httpEquiv ?? p["http-equiv"]) meta = document.querySelector(`meta[http-equiv="${CSS.escape(p.httpEquiv ?? p["http-equiv"])}"]`);
|
|
132
|
+
else if ("charSet" in p || "charset" in p) meta = document.querySelector("meta[charset]");
|
|
102
133
|
if (!meta) {
|
|
103
134
|
meta = document.createElement("meta");
|
|
104
|
-
meta.setAttribute("id", id);
|
|
105
135
|
document.head.appendChild(meta);
|
|
106
136
|
}
|
|
107
|
-
Object.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
meta.setAttribute(key, value);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
137
|
+
for (const [k, v] of Object.entries(p)) {
|
|
138
|
+
if (v != null && v !== false) meta.setAttribute(k === "charSet" ? "charset" : k === "httpEquiv" ? "http-equiv" : k, String(v));
|
|
139
|
+
}
|
|
113
140
|
}, []);
|
|
114
|
-
const addLink = import_react.default.useCallback((
|
|
115
|
-
|
|
141
|
+
const addLink = import_react.default.useCallback((props) => {
|
|
142
|
+
const p = props;
|
|
143
|
+
let link = null;
|
|
144
|
+
const asSel = p.as ? `[as="${CSS.escape(p.as)}"]` : "";
|
|
145
|
+
if (p.rel && p.href) link = document.querySelector(`link[rel="${CSS.escape(p.rel)}"][href="${CSS.escape(p.href)}"]${asSel}`);
|
|
146
|
+
else if (p.rel) link = document.querySelector(`link[rel="${CSS.escape(p.rel)}"]${asSel}`);
|
|
116
147
|
if (!link) {
|
|
117
148
|
link = document.createElement("link");
|
|
118
|
-
link.setAttribute("id", id);
|
|
119
149
|
document.head.appendChild(link);
|
|
120
150
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
});
|
|
151
|
+
const LINK_ATTR = { crossOrigin: "crossorigin", referrerPolicy: "referrerpolicy", fetchPriority: "fetchpriority", hrefLang: "hreflang" };
|
|
152
|
+
for (const [k, v] of Object.entries(p)) {
|
|
153
|
+
if (v != null && v !== false) link.setAttribute(LINK_ATTR[k] ?? k, String(v));
|
|
154
|
+
}
|
|
127
155
|
}, []);
|
|
128
|
-
const addStyle = import_react.default.useCallback((
|
|
129
|
-
|
|
156
|
+
const addStyle = import_react.default.useCallback((props) => {
|
|
157
|
+
const p = props;
|
|
158
|
+
let style = null;
|
|
159
|
+
if (p["data-id"]) style = document.querySelector(`style[data-id="${CSS.escape(p["data-id"])}"]`);
|
|
130
160
|
if (!style) {
|
|
131
161
|
style = document.createElement("style");
|
|
132
|
-
style.setAttribute("id", id);
|
|
133
162
|
document.head.appendChild(style);
|
|
134
163
|
}
|
|
135
|
-
Object.
|
|
136
|
-
if (
|
|
137
|
-
style.innerHTML =
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
const value = props[key];
|
|
141
|
-
if (value) {
|
|
142
|
-
style[key] = value;
|
|
164
|
+
for (const [k, v] of Object.entries(p)) {
|
|
165
|
+
if (k === "dangerouslySetInnerHTML") {
|
|
166
|
+
style.innerHTML = v.__html ?? "";
|
|
167
|
+
continue;
|
|
143
168
|
}
|
|
144
|
-
|
|
169
|
+
if (v != null && v !== false) style.setAttribute(k, String(v));
|
|
170
|
+
}
|
|
145
171
|
}, []);
|
|
146
|
-
const addScript = import_react.default.useCallback((
|
|
147
|
-
|
|
172
|
+
const addScript = import_react.default.useCallback((props) => {
|
|
173
|
+
const p = props;
|
|
174
|
+
let script = null;
|
|
175
|
+
if (p.src) script = document.querySelector(`script[src="${CSS.escape(p.src)}"]`);
|
|
176
|
+
else if (p["data-id"]) script = document.querySelector(`script[data-id="${CSS.escape(p["data-id"])}"]`);
|
|
148
177
|
if (!script) {
|
|
149
178
|
script = document.createElement("script");
|
|
150
|
-
script.setAttribute("id", id);
|
|
151
179
|
document.body.appendChild(script);
|
|
152
180
|
}
|
|
153
|
-
Object.
|
|
154
|
-
if (
|
|
155
|
-
script.innerHTML =
|
|
156
|
-
|
|
181
|
+
for (const [k, v] of Object.entries(p)) {
|
|
182
|
+
if (k === "dangerouslySetInnerHTML") {
|
|
183
|
+
script.innerHTML = v.__html ?? "";
|
|
184
|
+
continue;
|
|
157
185
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
script[key] = value;
|
|
161
|
-
}
|
|
162
|
-
});
|
|
186
|
+
if (v != null && v !== false) script.setAttribute(k, String(v));
|
|
187
|
+
}
|
|
163
188
|
}, []);
|
|
164
189
|
const contextValue = import_react.default.useMemo(() => ({
|
|
165
190
|
setTitle,
|
|
@@ -288,9 +313,6 @@ function useServerData(key, fn) {
|
|
|
288
313
|
if (existing.status === "rejected") throw existing.reason;
|
|
289
314
|
return existing.value;
|
|
290
315
|
}
|
|
291
|
-
var genRandomId = () => {
|
|
292
|
-
return "head-" + Math.random().toString(36).substr(2, 9);
|
|
293
|
-
};
|
|
294
316
|
var Head = import_react.default.memo(({ children, status }) => {
|
|
295
317
|
const {
|
|
296
318
|
setStatus,
|
|
@@ -307,26 +329,31 @@ var Head = import_react.default.memo(({ children, status }) => {
|
|
|
307
329
|
if (!import_react.default.isValidElement(child)) return;
|
|
308
330
|
const childType = child.type;
|
|
309
331
|
const childProps = child.props;
|
|
310
|
-
const id = childProps["id"] || genRandomId();
|
|
311
332
|
switch (childType) {
|
|
312
333
|
case "title": {
|
|
313
334
|
setTitle(childProps["children"]);
|
|
314
335
|
return;
|
|
315
336
|
}
|
|
316
337
|
case "meta": {
|
|
317
|
-
addMeta(
|
|
338
|
+
addMeta(childProps);
|
|
318
339
|
return;
|
|
319
340
|
}
|
|
320
341
|
case "link": {
|
|
321
|
-
addLink(
|
|
342
|
+
addLink(childProps);
|
|
322
343
|
return;
|
|
323
344
|
}
|
|
324
345
|
case "script": {
|
|
325
|
-
|
|
346
|
+
if (!childProps["src"] && !childProps["data-id"]) {
|
|
347
|
+
console.warn('[hadars] <Head>: inline <script> is missing a "data-id" prop \u2014 deduplication is not guaranteed across re-renders. Add data-id="unique-key" to ensure it.');
|
|
348
|
+
}
|
|
349
|
+
addScript(childProps);
|
|
326
350
|
return;
|
|
327
351
|
}
|
|
328
352
|
case "style": {
|
|
329
|
-
|
|
353
|
+
if (!childProps["data-id"]) {
|
|
354
|
+
console.warn('[hadars] <Head>: inline <style> is missing a "data-id" prop \u2014 deduplication is not guaranteed across re-renders. Add data-id="unique-key" to ensure it.');
|
|
355
|
+
}
|
|
356
|
+
addStyle(childProps);
|
|
330
357
|
return;
|
|
331
358
|
}
|
|
332
359
|
default: {
|
|
@@ -338,56 +365,6 @@ var Head = import_react.default.memo(({ children, status }) => {
|
|
|
338
365
|
return null;
|
|
339
366
|
});
|
|
340
367
|
|
|
341
|
-
// src/components/CacheSegment.tsx
|
|
342
|
-
var import_react2 = __toESM(require("react"), 1);
|
|
343
|
-
|
|
344
|
-
// src/utils/segmentCache.ts
|
|
345
|
-
function getStore() {
|
|
346
|
-
const g = globalThis;
|
|
347
|
-
if (!g.__hadarsSegmentStore) {
|
|
348
|
-
g.__hadarsSegmentStore = /* @__PURE__ */ new Map();
|
|
349
|
-
}
|
|
350
|
-
return g.__hadarsSegmentStore;
|
|
351
|
-
}
|
|
352
|
-
function getSegment(key) {
|
|
353
|
-
const entry = getStore().get(key);
|
|
354
|
-
if (!entry) return null;
|
|
355
|
-
if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
|
|
356
|
-
getStore().delete(key);
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
return entry.html;
|
|
360
|
-
}
|
|
361
|
-
function deleteSegment(key) {
|
|
362
|
-
getStore().delete(key);
|
|
363
|
-
}
|
|
364
|
-
function clearSegments() {
|
|
365
|
-
getStore().clear();
|
|
366
|
-
}
|
|
367
|
-
var CACHE_TAG = "hadars-c";
|
|
368
|
-
|
|
369
|
-
// src/components/CacheSegment.tsx
|
|
370
|
-
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
371
|
-
function CacheSegment({ cacheKey, ttl, children }) {
|
|
372
|
-
if (typeof window !== "undefined") {
|
|
373
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
|
|
374
|
-
}
|
|
375
|
-
const cached = getSegment(cacheKey);
|
|
376
|
-
if (cached !== null) {
|
|
377
|
-
return import_react2.default.createElement(CACHE_TAG, {
|
|
378
|
-
"data-key": cacheKey,
|
|
379
|
-
"data-cache": "hit",
|
|
380
|
-
dangerouslySetInnerHTML: { __html: cached }
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
const props = {
|
|
384
|
-
"data-key": cacheKey,
|
|
385
|
-
"data-cache": "miss"
|
|
386
|
-
};
|
|
387
|
-
if (ttl != null) props["data-ttl"] = ttl;
|
|
388
|
-
return import_react2.default.createElement(CACHE_TAG, props, children);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
368
|
// src/index.tsx
|
|
392
369
|
var HadarsContext = typeof window === "undefined" ? AppProviderSSR : AppProviderCSR;
|
|
393
370
|
function loadModule(path) {
|
|
@@ -398,11 +375,8 @@ function loadModule(path) {
|
|
|
398
375
|
}
|
|
399
376
|
// Annotate the CommonJS export names for ESM import in node:
|
|
400
377
|
0 && (module.exports = {
|
|
401
|
-
CacheSegment,
|
|
402
378
|
HadarsContext,
|
|
403
379
|
HadarsHead,
|
|
404
|
-
clearSegments,
|
|
405
|
-
deleteSegment,
|
|
406
380
|
initServerDataCache,
|
|
407
381
|
loadModule,
|
|
408
382
|
useServerData
|
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as React$1 from 'react';
|
|
2
2
|
import React__default from 'react';
|
|
3
|
-
import { A as AppContext } from './hadars-
|
|
4
|
-
export { b as HadarsApp, H as HadarsEntryModule, c as
|
|
5
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
+
import { A as AppContext } from './hadars-Bh-V5YXg.cjs';
|
|
4
|
+
export { b as HadarsApp, H as HadarsEntryModule, c as HadarsGetClientProps, d as HadarsGetFinalProps, e as HadarsGetInitialProps, a as HadarsOptions, f as HadarsProps, g as HadarsRequest } from './hadars-Bh-V5YXg.cjs';
|
|
6
5
|
|
|
7
6
|
/** Call this before hydrating to seed the client cache from the server's data.
|
|
8
7
|
* Invoked automatically by the hadars client bootstrap.
|
|
@@ -40,44 +39,6 @@ declare const Head: React__default.FC<{
|
|
|
40
39
|
status?: number;
|
|
41
40
|
}>;
|
|
42
41
|
|
|
43
|
-
interface CacheSegmentProps {
|
|
44
|
-
/**
|
|
45
|
-
* Unique cache key for this segment. Use a key that encodes all values
|
|
46
|
-
* the output depends on, e.g. `"product-" + product.id`.
|
|
47
|
-
*/
|
|
48
|
-
cacheKey: string;
|
|
49
|
-
/**
|
|
50
|
-
* Time-to-live in milliseconds. Omit for entries that never expire.
|
|
51
|
-
*/
|
|
52
|
-
ttl?: number;
|
|
53
|
-
children: React__default.ReactNode;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Caches the server-rendered HTML of its children across requests.
|
|
57
|
-
*
|
|
58
|
-
* **Server (SSR):**
|
|
59
|
-
* - Cache miss — children are rendered normally as part of the main
|
|
60
|
-
* `renderToString` call, so React context propagates correctly. The output
|
|
61
|
-
* is wrapped in a `<hadars-c>` marker that `processSegmentCache` uses to
|
|
62
|
-
* extract and store the HTML. The marker is stripped before the response
|
|
63
|
-
* is sent; the browser never sees it.
|
|
64
|
-
* - Cache hit — children are **not** rendered at all. The cached HTML is
|
|
65
|
-
* injected directly, saving the entire subtree render cost.
|
|
66
|
-
*
|
|
67
|
-
* **Client:** renders children normally (no caching). Because the server
|
|
68
|
-
* strips the marker wrapper, the client output matches the server HTML and
|
|
69
|
-
* React hydration succeeds without warnings for deterministic components.
|
|
70
|
-
*
|
|
71
|
-
* **Note:** components that rely on request-specific data (cookies, auth,
|
|
72
|
-
* personalisation) must not be wrapped in `CacheSegment` unless the cache
|
|
73
|
-
* key encodes that data — otherwise a cached response for one user could be
|
|
74
|
-
* served to another.
|
|
75
|
-
*/
|
|
76
|
-
declare function CacheSegment({ cacheKey, ttl, children }: CacheSegmentProps): react_jsx_runtime.JSX.Element;
|
|
77
|
-
|
|
78
|
-
declare function deleteSegment(key: string): void;
|
|
79
|
-
declare function clearSegments(): void;
|
|
80
|
-
|
|
81
42
|
declare const HadarsContext: React$1.FC<{
|
|
82
43
|
children: React.ReactNode;
|
|
83
44
|
context: AppContext;
|
|
@@ -102,4 +63,4 @@ declare const HadarsContext: React$1.FC<{
|
|
|
102
63
|
*/
|
|
103
64
|
declare function loadModule<T = any>(path: string): Promise<T>;
|
|
104
65
|
|
|
105
|
-
export {
|
|
66
|
+
export { HadarsContext, Head as HadarsHead, initServerDataCache, loadModule, useServerData };
|