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 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
- Benchmarks against an equivalent Next.js app show significantly faster server throughput (requests/second) and meaningfully better page load metrics (TTFB, FCP, DOMContentLoaded). Build times are also much lower due to rspack.
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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
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, id, opts, selfClose = false) {
1035
- let attrs = ` id="${escAttr(id)}"`;
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 [id, opts] of Object.entries(seoData.meta))
1052
- html += renderHeadTag("meta", id, opts, true);
1053
- for (const [id, opts] of Object.entries(seoData.link))
1054
- html += renderHeadTag("link", id, opts, true);
1055
- for (const [id, opts] of Object.entries(seoData.style))
1056
- html += renderHeadTag("style", id, opts);
1057
- for (const [id, opts] of Object.entries(seoData.script))
1058
- html += renderHeadTag("script", id, opts);
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, getAfterRenderProps, getFinalProps } = opts.document;
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: processSegmentCache(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, HadarsGetAfterRenderProps as c, HadarsGetClientProps as d, HadarsGetFinalProps as e, HadarsGetInitialProps as f, HadarsProps as g, HadarsRequest as h };
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, HadarsGetAfterRenderProps as c, HadarsGetClientProps as d, HadarsGetFinalProps as e, HadarsGetInitialProps as f, HadarsProps as g, HadarsRequest as h };
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((id, props) => {
72
- head.meta[id] = props;
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((id, props) => {
75
- head.link[id] = props;
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((id, props) => {
78
- head.style[id] = props;
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((id, props) => {
81
- head.script[id] = props;
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((id, props) => {
101
- let meta = document.querySelector(`#${id}`);
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.keys(props).forEach((key) => {
108
- const value = props[key];
109
- if (value) {
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((id, props) => {
115
- let link = document.querySelector(`#${id}`);
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
- Object.keys(props).forEach((key) => {
122
- const value = props[key];
123
- if (value) {
124
- link.setAttribute(key, value);
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((id, props) => {
129
- let style = document.getElementById(id);
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.keys(props).forEach((key) => {
136
- if (key === "dangerouslySetInnerHTML" && props[key] && props[key].__html) {
137
- style.innerHTML = props[key].__html;
138
- return;
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((id, props) => {
147
- let script = document.getElementById(id);
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.keys(props).forEach((key) => {
154
- if (key === "dangerouslySetInnerHTML" && props[key] && props[key].__html) {
155
- script.innerHTML = props[key].__html;
156
- return;
181
+ for (const [k, v] of Object.entries(p)) {
182
+ if (k === "dangerouslySetInnerHTML") {
183
+ script.innerHTML = v.__html ?? "";
184
+ continue;
157
185
  }
158
- const value = props[key];
159
- if (value) {
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(id.toString(), childProps);
338
+ addMeta(childProps);
318
339
  return;
319
340
  }
320
341
  case "link": {
321
- addLink(id.toString(), childProps);
342
+ addLink(childProps);
322
343
  return;
323
344
  }
324
345
  case "script": {
325
- addScript(id.toString(), childProps);
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
- addStyle(id.toString(), childProps);
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-BmgNX2zr.cjs';
4
- export { b as HadarsApp, H as HadarsEntryModule, c as HadarsGetAfterRenderProps, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest } from './hadars-BmgNX2zr.cjs';
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 { CacheSegment, HadarsContext, Head as HadarsHead, clearSegments, deleteSegment, initServerDataCache, loadModule, useServerData };
66
+ export { HadarsContext, Head as HadarsHead, initServerDataCache, loadModule, useServerData };