hadars 0.4.1 → 0.4.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/dist/{chunk-TV37IMRB.js → chunk-2TMQUXFL.js} +10 -10
- package/dist/{chunk-2J2L2H3H.js → chunk-NYLXE7T7.js} +6 -6
- package/dist/{chunk-OS3V4CPN.js → chunk-OZUZS2PD.js} +4 -4
- package/dist/cli.js +462 -496
- package/dist/cloudflare.cjs +11 -11
- package/dist/cloudflare.js +3 -3
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/lambda.cjs +11 -11
- package/dist/lambda.js +7 -7
- package/dist/loader.cjs +90 -54
- package/dist/slim-react/index.cjs +13 -13
- package/dist/slim-react/index.js +2 -2
- package/dist/slim-react/jsx-runtime.cjs +2 -4
- package/dist/slim-react/jsx-runtime.js +1 -1
- package/dist/ssr-render-worker.js +174 -161
- package/dist/ssr-watch.js +40 -74
- package/package.json +8 -10
- package/cli-lib.ts +0 -676
- package/cli.ts +0 -36
- package/index.ts +0 -17
- package/src/build.ts +0 -805
- package/src/cloudflare.ts +0 -140
- package/src/index.tsx +0 -41
- package/src/lambda.ts +0 -287
- package/src/slim-react/context.ts +0 -55
- package/src/slim-react/dispatcher.ts +0 -87
- package/src/slim-react/hooks.ts +0 -137
- package/src/slim-react/index.ts +0 -232
- package/src/slim-react/jsx-runtime.ts +0 -7
- package/src/slim-react/jsx.ts +0 -53
- package/src/slim-react/render.ts +0 -1101
- package/src/slim-react/renderContext.ts +0 -294
- package/src/slim-react/types.ts +0 -33
- package/src/source/context.ts +0 -113
- package/src/source/graphiql.ts +0 -101
- package/src/source/inference.ts +0 -260
- package/src/source/runner.ts +0 -138
- package/src/source/store.ts +0 -50
- package/src/ssr-render-worker.ts +0 -116
- package/src/ssr-watch.ts +0 -62
- package/src/static.ts +0 -109
- package/src/types/global.d.ts +0 -5
- package/src/types/hadars.ts +0 -350
- package/src/utils/Head.tsx +0 -462
- package/src/utils/clientScript.tsx +0 -71
- package/src/utils/cookies.ts +0 -16
- package/src/utils/loader.ts +0 -335
- package/src/utils/proxyHandler.tsx +0 -104
- package/src/utils/request.tsx +0 -9
- package/src/utils/response.tsx +0 -141
- package/src/utils/rspack.ts +0 -467
- package/src/utils/runtime.ts +0 -19
- package/src/utils/serve.ts +0 -155
- package/src/utils/ssrHandler.ts +0 -239
- package/src/utils/staticFile.ts +0 -43
- package/src/utils/template.html +0 -11
- package/src/utils/upgradeRequest.tsx +0 -19
package/src/utils/Head.tsx
DELETED
|
@@ -1,462 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { AppHead, AppUnsuspend, HadarsDocumentNode, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/hadars'
|
|
3
|
-
|
|
4
|
-
interface InnerContext {
|
|
5
|
-
setTitle: (title: string) => void;
|
|
6
|
-
addMeta: (props: MetaProps) => void;
|
|
7
|
-
addLink: (props: LinkProps) => void;
|
|
8
|
-
addStyle: (props: StyleProps) => void;
|
|
9
|
-
addScript: (props: ScriptProps) => void;
|
|
10
|
-
setStatus: (status: number) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Derive a stable dedup key from an element's natural identifying attributes.
|
|
14
|
-
function deriveKey(tag: string, props: Record<string, any>): string {
|
|
15
|
-
switch (tag) {
|
|
16
|
-
case 'meta': {
|
|
17
|
-
if (props.name) return `meta:name:${props.name}`;
|
|
18
|
-
if (props.property) return `meta:property:${props.property}`;
|
|
19
|
-
const httpEquiv = props.httpEquiv ?? props['http-equiv'];
|
|
20
|
-
if (httpEquiv) return `meta:http-equiv:${httpEquiv}`;
|
|
21
|
-
if ('charSet' in props || 'charset' in props) return 'meta:charset';
|
|
22
|
-
return `meta:${JSON.stringify(props)}`;
|
|
23
|
-
}
|
|
24
|
-
case 'link': {
|
|
25
|
-
const rel = props.rel ?? '';
|
|
26
|
-
const href = props.href ?? '';
|
|
27
|
-
const as_ = props.as ? `:as:${props.as}` : '';
|
|
28
|
-
return href ? `link:${rel}:${href}${as_}` : `link:${rel}${as_}`;
|
|
29
|
-
}
|
|
30
|
-
case 'script': {
|
|
31
|
-
if (props.src) return `script:src:${props.src}`;
|
|
32
|
-
if (props['data-id']) return `script:id:${props['data-id']}`;
|
|
33
|
-
return `script:${JSON.stringify(props)}`;
|
|
34
|
-
}
|
|
35
|
-
case 'style': {
|
|
36
|
-
if (props['data-id']) return `style:id:${props['data-id']}`;
|
|
37
|
-
return `style:${JSON.stringify(props)}`;
|
|
38
|
-
}
|
|
39
|
-
default:
|
|
40
|
-
return `${tag}:${JSON.stringify(props)}`;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── Head context resolution ──────────────────────────────────────────────────
|
|
45
|
-
//
|
|
46
|
-
// On the server, HadarsHead reads from globalThis.__hadarsContext.head which
|
|
47
|
-
// the SSR render worker populates before every renderToString call.
|
|
48
|
-
// On the client, HadarsHead directly manipulates the DOM.
|
|
49
|
-
// This approach means users do NOT need to wrap their App with HadarsContext.
|
|
50
|
-
|
|
51
|
-
const LINK_ATTR: Record<string, string> = {
|
|
52
|
-
crossOrigin: 'crossorigin',
|
|
53
|
-
referrerPolicy: 'referrerpolicy',
|
|
54
|
-
fetchPriority: 'fetchpriority',
|
|
55
|
-
hrefLang: 'hreflang',
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
function makeServerCtx(head: AppHead): InnerContext {
|
|
59
|
-
return {
|
|
60
|
-
setTitle: (t) => { head.title = t; },
|
|
61
|
-
addMeta: (p) => { head.meta[deriveKey('meta', p as any)] = p; },
|
|
62
|
-
addLink: (p) => { head.link[deriveKey('link', p as any)] = p; },
|
|
63
|
-
addStyle: (p) => { head.style[deriveKey('style', p as any)] = p; },
|
|
64
|
-
addScript: (p) => { head.script[deriveKey('script', p as any)] = p; },
|
|
65
|
-
setStatus: (s) => { head.status = s; },
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Lazy singleton for the client-side DOM context.
|
|
70
|
-
let _cliCtx: InnerContext | null = null;
|
|
71
|
-
|
|
72
|
-
function makeClientCtx(): InnerContext {
|
|
73
|
-
if (_cliCtx) return _cliCtx;
|
|
74
|
-
_cliCtx = {
|
|
75
|
-
setTitle: (title) => { document.title = title; },
|
|
76
|
-
setStatus: () => { /* no-op on client */ },
|
|
77
|
-
addMeta: (props) => {
|
|
78
|
-
const p = props as Record<string, any>;
|
|
79
|
-
let meta: HTMLMetaElement | null = null;
|
|
80
|
-
if (p.name) meta = document.querySelector(`meta[name="${CSS.escape(p.name)}"]`);
|
|
81
|
-
else if (p.property) meta = document.querySelector(`meta[property="${CSS.escape(p.property)}"]`);
|
|
82
|
-
else if (p.httpEquiv ?? p['http-equiv']) meta = document.querySelector(`meta[http-equiv="${CSS.escape(p.httpEquiv ?? p['http-equiv'])}"]`);
|
|
83
|
-
else if ('charSet' in p || 'charset' in p) meta = document.querySelector('meta[charset]');
|
|
84
|
-
if (!meta) { meta = document.createElement('meta'); document.head.appendChild(meta); }
|
|
85
|
-
for (const [k, v] of Object.entries(p)) {
|
|
86
|
-
if (v != null && v !== false) meta.setAttribute(k === 'charSet' ? 'charset' : k === 'httpEquiv' ? 'http-equiv' : k, String(v));
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
addLink: (props) => {
|
|
90
|
-
const p = props as Record<string, any>;
|
|
91
|
-
let link: HTMLLinkElement | null = null;
|
|
92
|
-
const asSel = p.as ? `[as="${CSS.escape(p.as)}"]` : '';
|
|
93
|
-
if (p.rel && p.href) link = document.querySelector(`link[rel="${CSS.escape(p.rel)}"][href="${CSS.escape(p.href)}"]${asSel}`);
|
|
94
|
-
else if (p.rel) link = document.querySelector(`link[rel="${CSS.escape(p.rel)}"]${asSel}`);
|
|
95
|
-
if (!link) { link = document.createElement('link'); document.head.appendChild(link); }
|
|
96
|
-
for (const [k, v] of Object.entries(p)) {
|
|
97
|
-
if (v != null && v !== false) link.setAttribute(LINK_ATTR[k] ?? k, String(v));
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
addStyle: (props) => {
|
|
101
|
-
const p = props as Record<string, any>;
|
|
102
|
-
let style: HTMLStyleElement | null = null;
|
|
103
|
-
if (p['data-id']) style = document.querySelector(`style[data-id="${CSS.escape(p['data-id'])}"]`);
|
|
104
|
-
if (!style) { style = document.createElement('style'); document.head.appendChild(style); }
|
|
105
|
-
for (const [k, v] of Object.entries(p)) {
|
|
106
|
-
if (k === 'dangerouslySetInnerHTML') { style.innerHTML = (v as any).__html ?? ''; continue; }
|
|
107
|
-
if (v != null && v !== false) style.setAttribute(k, String(v));
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
addScript: (props) => {
|
|
111
|
-
const p = props as Record<string, any>;
|
|
112
|
-
let script: HTMLScriptElement | null = null;
|
|
113
|
-
if (p.src) script = document.querySelector(`script[src="${CSS.escape(p.src)}"]`);
|
|
114
|
-
else if (p['data-id']) script = document.querySelector(`script[data-id="${CSS.escape(p['data-id'])}"]`);
|
|
115
|
-
if (!script) { script = document.createElement('script'); document.body.appendChild(script); }
|
|
116
|
-
for (const [k, v] of Object.entries(p)) {
|
|
117
|
-
if (k === 'dangerouslySetInnerHTML') { script.innerHTML = (v as any).__html ?? ''; continue; }
|
|
118
|
-
if (v != null && v !== false) script.setAttribute(k, String(v));
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
return _cliCtx;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function getCtx(): InnerContext | null {
|
|
126
|
-
if (typeof window === 'undefined') {
|
|
127
|
-
const head: AppHead | undefined = (globalThis as any).__hadarsContext?.head;
|
|
128
|
-
if (!head) return null;
|
|
129
|
-
return makeServerCtx(head);
|
|
130
|
-
}
|
|
131
|
-
return makeClientCtx();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── useServerData ─────────────────────────────────────────────────────────────
|
|
135
|
-
//
|
|
136
|
-
// Client-side cache pre-populated from the server's resolved data before
|
|
137
|
-
// hydration. During the initial SSR load, keyed by the server's useId() values
|
|
138
|
-
// (which match React's hydrateRoot IDs). During client-side navigation the
|
|
139
|
-
// server keys are _R_..._ (slim-react format) but the client is NOT in
|
|
140
|
-
// hydration mode so useId() returns _r_..._ — a completely different format.
|
|
141
|
-
// We bridge this by storing navigation results as an ordered array and consuming
|
|
142
|
-
// them sequentially on the retry, then caching by the client's own useId() key.
|
|
143
|
-
const clientServerDataCache = new Map<string, unknown>();
|
|
144
|
-
|
|
145
|
-
// Tracks in-flight data-only requests keyed by pathname+search so that all
|
|
146
|
-
// useServerData calls within a single Suspense pass share one network request.
|
|
147
|
-
const pendingDataFetch = new Map<string, Promise<void>>();
|
|
148
|
-
// Paths for which a client data fetch has already completed. Prevents re-fetching
|
|
149
|
-
// when a key is genuinely absent from the server response for this path.
|
|
150
|
-
const fetchedPaths = new Set<string>();
|
|
151
|
-
|
|
152
|
-
// Ordered values from the most recent navigation fetch.
|
|
153
|
-
// React retries suspended components in tree order (same order as SSR), so
|
|
154
|
-
// consuming these positionally is safe and avoids the server/client key mismatch.
|
|
155
|
-
let _navValues: unknown[] = [];
|
|
156
|
-
let _navIdx = 0;
|
|
157
|
-
|
|
158
|
-
/** Call this before hydrating to seed the client cache from the server's data.
|
|
159
|
-
* Invoked automatically by the hadars client bootstrap.
|
|
160
|
-
* Always clears the existing cache before populating — call with `{}` to just clear. */
|
|
161
|
-
export function initServerDataCache(data: Record<string, unknown>) {
|
|
162
|
-
clientServerDataCache.clear();
|
|
163
|
-
_navValues = [];
|
|
164
|
-
_navIdx = 0;
|
|
165
|
-
for (const [k, v] of Object.entries(data)) {
|
|
166
|
-
clientServerDataCache.set(k, v);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Fetch async data on the server during SSR. Returns `undefined` on the first
|
|
172
|
-
* render pass(es) while the promise is in flight; returns the resolved value
|
|
173
|
-
* once the framework's render loop has awaited it.
|
|
174
|
-
*
|
|
175
|
-
* On the client the pre-resolved value is read from the hydration cache
|
|
176
|
-
* serialised into the page by the server, so no fetch is issued in the browser.
|
|
177
|
-
*
|
|
178
|
-
* The cache key is derived automatically from the call-site's position in the
|
|
179
|
-
* component tree via `useId()` — no manual key is required.
|
|
180
|
-
*
|
|
181
|
-
* `fn` may return a `Promise<T>` (async usage) or return `T` synchronously.
|
|
182
|
-
* The resolved value is serialised into `__serverData` and returned from cache
|
|
183
|
-
* during hydration.
|
|
184
|
-
*
|
|
185
|
-
* `fn` is **server-only**: it is never called in the browser. On client-side
|
|
186
|
-
* navigation (after the initial SSR load), hadars automatically fires a
|
|
187
|
-
* data-only request to the current URL (`X-Hadars-Data: 1`) and suspends via
|
|
188
|
-
* React Suspense until the server returns the JSON map of resolved values.
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
* const user = useServerData(() => db.getUser(id));
|
|
192
|
-
* const post = useServerData(() => db.getPost(postId));
|
|
193
|
-
* if (!user) return null; // undefined while pending on the first SSR pass
|
|
194
|
-
*
|
|
195
|
-
* // cache: false — evicts the entry on unmount so the next mount fetches fresh data
|
|
196
|
-
* const stats = useServerData(() => getServerStats(), { cache: false });
|
|
197
|
-
*/
|
|
198
|
-
export function useServerData<T>(fn: () => Promise<T> | T, options?: { cache?: boolean }): T | undefined {
|
|
199
|
-
const cacheKey = React.useId();
|
|
200
|
-
|
|
201
|
-
// When cache: false, evict the entry on unmount so the next mount fetches
|
|
202
|
-
// fresh data from the server. The eviction is deferred via setTimeout so
|
|
203
|
-
// that React Strict Mode's synchronous fake-unmount/remount cycle can cancel
|
|
204
|
-
// it before it fires — a real unmount has no follow-up effect to cancel it.
|
|
205
|
-
const evictTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
206
|
-
React.useEffect(() => {
|
|
207
|
-
if (options?.cache !== false) return;
|
|
208
|
-
// Cancel any timer left over from a Strict Mode fake unmount.
|
|
209
|
-
if (evictTimerRef.current !== null) {
|
|
210
|
-
clearTimeout(evictTimerRef.current);
|
|
211
|
-
evictTimerRef.current = null;
|
|
212
|
-
}
|
|
213
|
-
return () => {
|
|
214
|
-
evictTimerRef.current = setTimeout(() => {
|
|
215
|
-
clientServerDataCache.delete(cacheKey);
|
|
216
|
-
evictTimerRef.current = null;
|
|
217
|
-
}, 0);
|
|
218
|
-
};
|
|
219
|
-
}, []);
|
|
220
|
-
|
|
221
|
-
if (typeof window !== 'undefined') {
|
|
222
|
-
// Cache hit — return the server-resolved value directly.
|
|
223
|
-
if (clientServerDataCache.has(cacheKey)) {
|
|
224
|
-
return clientServerDataCache.get(cacheKey) as T;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Cache miss — this component is mounting during client-side navigation
|
|
228
|
-
// (the server hasn't sent data for this path yet). Fire a data-only
|
|
229
|
-
// request to the server at the current URL and suspend via React Suspense
|
|
230
|
-
// until it completes. All useServerData calls within the same React render
|
|
231
|
-
// share one Promise so only one network request is made per navigation.
|
|
232
|
-
const pathKey = window.location.pathname + window.location.search;
|
|
233
|
-
|
|
234
|
-
// After a navigation fetch has completed, consume values positionally.
|
|
235
|
-
// The server returns keys in slim-react (_R_..._) format, but the client
|
|
236
|
-
// is not in hydration mode so useId() returns _r_..._ — they never match.
|
|
237
|
-
// We store the ordered values from the fetch and hand them out in tree
|
|
238
|
-
// order (which is identical on server and client for a given route).
|
|
239
|
-
if (fetchedPaths.has(pathKey)) {
|
|
240
|
-
if (_navIdx < _navValues.length) {
|
|
241
|
-
const value = _navValues[_navIdx++] as T;
|
|
242
|
-
clientServerDataCache.set(cacheKey, value);
|
|
243
|
-
return value;
|
|
244
|
-
}
|
|
245
|
-
// Positional data exhausted — cache:false eviction or remount with new
|
|
246
|
-
// useId() keys. Remove the path so a fresh fetch fires below.
|
|
247
|
-
fetchedPaths.delete(pathKey);
|
|
248
|
-
_navValues = [];
|
|
249
|
-
_navIdx = 0;
|
|
250
|
-
// fall through to trigger a new fetch
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (!pendingDataFetch.has(pathKey)) {
|
|
254
|
-
let resolve!: () => void;
|
|
255
|
-
const p = new Promise<void>(res => { resolve = res; });
|
|
256
|
-
pendingDataFetch.set(pathKey, p);
|
|
257
|
-
// Fire in a microtask so that every useServerData call in this React
|
|
258
|
-
// render is registered against the same deferred before the fetch starts.
|
|
259
|
-
queueMicrotask(async () => {
|
|
260
|
-
try {
|
|
261
|
-
let json: { serverData: Record<string, unknown> } | null = null;
|
|
262
|
-
|
|
263
|
-
if ((globalThis as any).__hadarsStatic) {
|
|
264
|
-
// Static export: the __hadarsStatic flag was embedded in the
|
|
265
|
-
// page by `hadars export static`. Fetch the pre-generated
|
|
266
|
-
// index.json sidecar directly — no live SSR server exists.
|
|
267
|
-
const sidecarUrl = pathKey.replace(/\/$/, '') + '/index.json';
|
|
268
|
-
const res = await fetch(sidecarUrl).catch(() => null);
|
|
269
|
-
if (res?.ok) json = await res.json().catch(() => null);
|
|
270
|
-
} else {
|
|
271
|
-
// Live server: request the current URL with Accept: application/json.
|
|
272
|
-
const res = await fetch(pathKey, { headers: { 'Accept': 'application/json' } });
|
|
273
|
-
if (res.ok) json = await res.json();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Store as ordered array — consumed positionally on retry to
|
|
277
|
-
// avoid the server (_R_..._) vs client (_r_..._) key mismatch.
|
|
278
|
-
_navValues = Object.values(json?.serverData ?? {});
|
|
279
|
-
_navIdx = 0;
|
|
280
|
-
// Only keep the freshly-fetched path in fetchedPaths — clear
|
|
281
|
-
// others so stale positional data from a previous page cannot
|
|
282
|
-
// be served if the user navigates back to it.
|
|
283
|
-
fetchedPaths.clear();
|
|
284
|
-
} finally {
|
|
285
|
-
fetchedPaths.add(pathKey);
|
|
286
|
-
pendingDataFetch.delete(pathKey);
|
|
287
|
-
resolve();
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
throw pendingDataFetch.get(pathKey)!;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Server: communicate via globalThis.__hadarsUnsuspend which is set by the
|
|
295
|
-
// framework around every renderToStaticMarkup / renderToString call. Using
|
|
296
|
-
// globalThis bypasses React context identity issues that arise when the user's
|
|
297
|
-
// SSR bundle and the hadars source each have their own React context instance.
|
|
298
|
-
const unsuspend: AppUnsuspend | undefined = (globalThis as any).__hadarsUnsuspend;
|
|
299
|
-
if (!unsuspend) return undefined;
|
|
300
|
-
|
|
301
|
-
const existing = unsuspend.cache.get(cacheKey);
|
|
302
|
-
|
|
303
|
-
if (!existing) {
|
|
304
|
-
// First encounter — call fn(), which may:
|
|
305
|
-
// (a) return a Promise<T> — async usage (serialised for the client)
|
|
306
|
-
// (b) return T synchronously — e.g. a sync data source
|
|
307
|
-
const result = fn();
|
|
308
|
-
|
|
309
|
-
const isThenable = result !== null && typeof result === 'object' && typeof (result as any).then === 'function';
|
|
310
|
-
|
|
311
|
-
// (b) Synchronous return — store as fulfilled and return immediately.
|
|
312
|
-
if (!isThenable) {
|
|
313
|
-
const value = result as T;
|
|
314
|
-
unsuspend.cache.set(cacheKey, { status: 'fulfilled', value });
|
|
315
|
-
return value;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// (a) Async Promise — standard useServerData usage.
|
|
319
|
-
const promise = (result as Promise<T>).then(
|
|
320
|
-
value => { unsuspend.cache.set(cacheKey, { status: 'fulfilled', value }); },
|
|
321
|
-
reason => { unsuspend.cache.set(cacheKey, { status: 'rejected', reason }); },
|
|
322
|
-
);
|
|
323
|
-
unsuspend.cache.set(cacheKey, { status: 'pending', promise });
|
|
324
|
-
throw promise; // slim-react will await and retry
|
|
325
|
-
}
|
|
326
|
-
if (existing.status === 'pending') {
|
|
327
|
-
throw existing.promise; // slim-react will await and retry
|
|
328
|
-
}
|
|
329
|
-
if (existing.status === 'rejected') throw existing.reason;
|
|
330
|
-
return existing.value as T;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// ── useGraphQL ────────────────────────────────────────────────────────────────
|
|
335
|
-
//
|
|
336
|
-
// Execute a GraphQL query during SSR via the hadars data layer. The executor
|
|
337
|
-
// is stored in globalThis.__hadarsGraphQL by the framework before each render.
|
|
338
|
-
// On the client, useServerData handles hydration + client-side navigation.
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Execute a GraphQL query server-side and return the result.
|
|
342
|
-
*
|
|
343
|
-
* Wraps `useServerData` — on the client the pre-resolved value is read from
|
|
344
|
-
* the hydration cache. During client-side navigation hadars automatically
|
|
345
|
-
* fires a data-only request to the server so the query re-executes there.
|
|
346
|
-
*
|
|
347
|
-
* Throws if the executor returns GraphQL errors, so the page is correctly
|
|
348
|
-
* marked as failed during `hadars export static`.
|
|
349
|
-
*
|
|
350
|
-
* ```tsx
|
|
351
|
-
* // Typed via codegen document — TData and TVariables are inferred:
|
|
352
|
-
* const result = useGraphQL(GetPostDocument, { slug });
|
|
353
|
-
* const post = result?.data?.blogPost; // fully typed
|
|
354
|
-
*
|
|
355
|
-
* // Plain string query — untyped:
|
|
356
|
-
* const result = useGraphQL(`{ allBlogPost { slug title } }`);
|
|
357
|
-
* if (!result) return null; // undefined while pending on first SSR pass
|
|
358
|
-
* ```
|
|
359
|
-
*/
|
|
360
|
-
// Overload 1: TypedDocumentNode — TData and TVariables are inferred from the document.
|
|
361
|
-
export function useGraphQL<
|
|
362
|
-
TData,
|
|
363
|
-
TVariables extends Record<string, unknown>,
|
|
364
|
-
>(
|
|
365
|
-
query: HadarsDocumentNode<TData, TVariables>,
|
|
366
|
-
variables?: TVariables,
|
|
367
|
-
): { data?: TData } | undefined;
|
|
368
|
-
// Overload 2: plain string query — untyped result.
|
|
369
|
-
export function useGraphQL(
|
|
370
|
-
query: string,
|
|
371
|
-
variables?: Record<string, unknown>,
|
|
372
|
-
): { data?: Record<string, unknown> } | undefined;
|
|
373
|
-
// Implementation
|
|
374
|
-
export function useGraphQL(
|
|
375
|
-
query: string | HadarsDocumentNode<unknown, Record<string, unknown>>,
|
|
376
|
-
variables?: Record<string, unknown>,
|
|
377
|
-
): { data?: unknown } | undefined {
|
|
378
|
-
return useServerData(async () => {
|
|
379
|
-
const executor: ((q: any, v?: Record<string, unknown>) => Promise<any>) | undefined =
|
|
380
|
-
(globalThis as any).__hadarsGraphQL;
|
|
381
|
-
|
|
382
|
-
if (!executor) {
|
|
383
|
-
throw new Error(
|
|
384
|
-
'[hadars] useGraphQL: no GraphQL executor is available for this request. ' +
|
|
385
|
-
'Make sure you have `sources` or a `graphql` executor configured in hadars.config.ts.',
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Pass the original query (string or document object) — the executor
|
|
390
|
-
// calls print() itself so codegen documents without loc.source.body work.
|
|
391
|
-
const result = await executor(query, variables);
|
|
392
|
-
|
|
393
|
-
if (result.errors?.length) {
|
|
394
|
-
const messages = result.errors.map((e: { message: string }) => e.message).join(', ');
|
|
395
|
-
throw new Error(`[hadars] GraphQL error: ${messages}`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return result;
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// ── HadarsHead ────────────────────────────────────────────────────────────────
|
|
403
|
-
|
|
404
|
-
export const Head: React.FC<{
|
|
405
|
-
children?: React.ReactNode;
|
|
406
|
-
status?: number;
|
|
407
|
-
}> = React.memo( ({ children, status }) => {
|
|
408
|
-
|
|
409
|
-
const ctx = getCtx();
|
|
410
|
-
if (!ctx) return null;
|
|
411
|
-
|
|
412
|
-
const { setStatus, setTitle, addMeta, addLink, addStyle, addScript } = ctx;
|
|
413
|
-
|
|
414
|
-
if ( status ) {
|
|
415
|
-
setStatus(status);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
React.Children.forEach(children, ( child ) => {
|
|
419
|
-
if ( !React.isValidElement(child) ) return;
|
|
420
|
-
|
|
421
|
-
const childType = child.type;
|
|
422
|
-
// React 19 types element.props as unknown; cast here since we
|
|
423
|
-
// inspect props dynamically based on the element type below.
|
|
424
|
-
const childProps = child.props as Record<string, any>;
|
|
425
|
-
|
|
426
|
-
switch ( childType ) {
|
|
427
|
-
case 'title': {
|
|
428
|
-
const raw = childProps['children'];
|
|
429
|
-
setTitle(Array.isArray(raw) ? raw.join('') : String(raw ?? ''));
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
case 'meta': {
|
|
433
|
-
addMeta(childProps as MetaProps);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
case 'link': {
|
|
437
|
-
addLink(childProps as LinkProps);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
case 'script': {
|
|
441
|
-
if (!childProps['src'] && !childProps['data-id']) {
|
|
442
|
-
console.warn('[hadars] <Head>: inline <script> is missing a "data-id" prop — deduplication is not guaranteed across re-renders. Add data-id="unique-key" to ensure it.');
|
|
443
|
-
}
|
|
444
|
-
addScript(childProps as ScriptProps);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
case 'style': {
|
|
448
|
-
if (!childProps['data-id']) {
|
|
449
|
-
console.warn('[hadars] <Head>: inline <style> is missing a "data-id" prop — deduplication is not guaranteed across re-renders. Add data-id="unique-key" to ensure it.');
|
|
450
|
-
}
|
|
451
|
-
addStyle(childProps as StyleProps);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
default: {
|
|
455
|
-
console.warn(`HadarsHead: Unsupported child type: ${childType}`);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
return null;
|
|
462
|
-
} );
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
3
|
-
import type { HadarsEntryModule } from '../types/hadars';
|
|
4
|
-
import { initServerDataCache } from 'hadars';
|
|
5
|
-
import * as _appMod from '$_MOD_PATH$';
|
|
6
|
-
|
|
7
|
-
const appMod = _appMod as HadarsEntryModule<{}>;
|
|
8
|
-
|
|
9
|
-
const getProps = () => {
|
|
10
|
-
const script = document.getElementById('hadars');
|
|
11
|
-
if (script) {
|
|
12
|
-
try {
|
|
13
|
-
const data = JSON.parse(script.textContent || '{}');
|
|
14
|
-
return data.hadars?.props || {};
|
|
15
|
-
} catch (e) {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const main = async () => {
|
|
23
|
-
let props = getProps();
|
|
24
|
-
|
|
25
|
-
// Extract the static-export flag before it reaches user code. When set,
|
|
26
|
-
// useServerData fetches index.json sidecars directly on client navigation
|
|
27
|
-
// instead of requesting the live SSR server with Accept: application/json.
|
|
28
|
-
if ((props as any).__hadarsStatic) {
|
|
29
|
-
(globalThis as any).__hadarsStatic = true;
|
|
30
|
-
const { __hadarsStatic: _, ...rest } = props as any;
|
|
31
|
-
props = rest;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Seed the useServerData client cache from server-resolved values before
|
|
35
|
-
// hydration so that hooks return the same data on the first render.
|
|
36
|
-
if (props.__serverData && typeof props.__serverData === 'object') {
|
|
37
|
-
initServerDataCache(props.__serverData as Record<string, unknown>);
|
|
38
|
-
const { __serverData: _, ...rest } = props;
|
|
39
|
-
props = rest;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const { location } = props;
|
|
43
|
-
|
|
44
|
-
if (appMod.getClientProps) {
|
|
45
|
-
try {
|
|
46
|
-
props = await appMod.getClientProps(props);
|
|
47
|
-
} catch (err) {
|
|
48
|
-
console.error('[hadars] getClientProps threw an error:', err);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
props = {
|
|
53
|
-
...props,
|
|
54
|
-
location,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const Component = appMod.default;
|
|
58
|
-
|
|
59
|
-
const appEl = document.getElementById("app");
|
|
60
|
-
if (appEl) {
|
|
61
|
-
// In HMR mode the client component may have already changed since SSR,
|
|
62
|
-
// so skip hydration to avoid mismatch warnings and do a fresh render.
|
|
63
|
-
if ((module as any).hot) {
|
|
64
|
-
createRoot(appEl).render(<Component {...props} />);
|
|
65
|
-
} else {
|
|
66
|
-
hydrateRoot(appEl, <Component {...props} />);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
main();
|
package/src/utils/cookies.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export const parseCookies = (cookieString: string): Record<string, string> => {
|
|
2
|
-
const cookies: Record<string, string> = {};
|
|
3
|
-
if (!cookieString) {
|
|
4
|
-
return cookies;
|
|
5
|
-
}
|
|
6
|
-
const pairs = cookieString.split(';');
|
|
7
|
-
for (const pair of pairs) {
|
|
8
|
-
const index = pair.indexOf('=');
|
|
9
|
-
if (index > -1) {
|
|
10
|
-
const key = pair.slice(0, index).trim();
|
|
11
|
-
const value = pair.slice(index + 1).trim();
|
|
12
|
-
try { cookies[key] = decodeURIComponent(value); } catch { cookies[key] = value; }
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return cookies;
|
|
16
|
-
};
|