hadars 0.1.40 → 0.2.1

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.
Files changed (42) hide show
  1. package/README.md +85 -70
  2. package/cli-lib.ts +89 -12
  3. package/dist/chunk-HWOLYLPF.js +332 -0
  4. package/dist/{chunk-2ENP7IAW.js → chunk-LY5MTHFV.js} +360 -203
  5. package/dist/cli.js +506 -274
  6. package/dist/cloudflare.cjs +1394 -0
  7. package/dist/cloudflare.d.cts +64 -0
  8. package/dist/cloudflare.d.ts +64 -0
  9. package/dist/cloudflare.js +68 -0
  10. package/dist/{hadars-Bh-V5YXg.d.cts → hadars-DEBSYAQl.d.cts} +1 -36
  11. package/dist/{hadars-Bh-V5YXg.d.ts → hadars-DEBSYAQl.d.ts} +1 -36
  12. package/dist/index.cjs +129 -156
  13. package/dist/index.d.cts +5 -11
  14. package/dist/index.d.ts +5 -11
  15. package/dist/index.js +129 -155
  16. package/dist/lambda.cjs +391 -229
  17. package/dist/lambda.d.cts +1 -2
  18. package/dist/lambda.d.ts +1 -2
  19. package/dist/lambda.js +18 -307
  20. package/dist/slim-react/index.cjs +361 -203
  21. package/dist/slim-react/index.d.cts +24 -8
  22. package/dist/slim-react/index.d.ts +24 -8
  23. package/dist/slim-react/index.js +3 -1
  24. package/dist/ssr-render-worker.js +352 -221
  25. package/dist/utils/Head.tsx +132 -187
  26. package/package.json +7 -2
  27. package/src/build.ts +7 -6
  28. package/src/cloudflare.ts +139 -0
  29. package/src/index.tsx +0 -3
  30. package/src/lambda.ts +6 -2
  31. package/src/slim-react/context.ts +2 -1
  32. package/src/slim-react/index.ts +21 -18
  33. package/src/slim-react/render.ts +379 -240
  34. package/src/slim-react/renderContext.ts +105 -45
  35. package/src/ssr-render-worker.ts +14 -44
  36. package/src/types/hadars.ts +0 -1
  37. package/src/utils/Head.tsx +132 -187
  38. package/src/utils/cookies.ts +1 -1
  39. package/src/utils/response.tsx +68 -33
  40. package/src/utils/serve.ts +29 -27
  41. package/src/utils/ssrHandler.ts +54 -25
  42. package/src/utils/staticFile.ts +2 -7
package/README.md CHANGED
@@ -19,28 +19,32 @@ Bring your own router (or none), keep your components as plain React, and get SS
19
19
  ## Benchmarks
20
20
 
21
21
  <!-- BENCHMARK_START -->
22
- > Last run: 2026-03-18 · 60s · 200 connections · Bun runtime
23
- > hadars is **7.8x faster** in requests/sec
22
+ > Last run: 2026-03-20 · 60s · 100 connections · Bun runtime
23
+ > hadars is **8.8x faster** in requests/sec
24
24
 
25
25
  **Throughput** (autocannon, 60s)
26
26
 
27
27
  | Metric | hadars | Next.js |
28
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 |
29
+ | Requests/sec | **159** | 18 |
30
+ | Latency median | **598 ms** | 2644 ms |
31
+ | Latency p99 | **953 ms** | 6436 ms |
32
+ | Throughput | **45.24** MB/s | 10.28 MB/s |
33
+ | Peak RSS | 1027.8 MB | **469.8 MB** |
34
+ | Avg RSS | 794.1 MB | **417.9 MB** |
35
+ | Build time | 0.8 s | 5.8 s |
34
36
 
35
37
  **Page load** (Playwright · Chromium headless · median)
36
38
 
37
39
  | Metric | hadars | Next.js |
38
40
  |---|---:|---:|
39
- | TTFB | **22 ms** | 42 ms |
40
- | FCP | **124 ms** | 136 ms |
41
- | DOMContentLoaded | **88 ms** | 126 ms |
42
- | Load | **155 ms** | 173 ms |
41
+ | TTFB | **17 ms** | 45 ms |
42
+ | FCP | **88 ms** | 128 ms |
43
+ | DOMContentLoaded | **36 ms** | 114 ms |
44
+ | Load | **115 ms** | 157 ms |
45
+ | Peak RSS | 502.8 MB | **282.8 MB** |
43
46
  <!-- BENCHMARK_END -->
47
+
44
48
  ## Quick start
45
49
 
46
50
  Scaffold a new project in seconds:
@@ -75,17 +79,17 @@ export default config;
75
79
  **src/App.tsx**
76
80
  ```tsx
77
81
  import React from 'react';
78
- import { HadarsContext, HadarsHead, type HadarsApp, type HadarsRequest } from 'hadars';
82
+ import { HadarsHead, type HadarsApp, type HadarsRequest } from 'hadars';
79
83
 
80
84
  interface Props { user: { name: string } }
81
85
 
82
- const App: HadarsApp<Props> = ({ user, context }) => (
83
- <HadarsContext context={context}>
86
+ const App: HadarsApp<Props> = ({ user }) => (
87
+ <>
84
88
  <HadarsHead status={200}>
85
89
  <title>Hello {user.name}</title>
86
90
  </HadarsHead>
87
91
  <h1>Hello, {user.name}!</h1>
88
- </HadarsContext>
92
+ </>
89
93
  );
90
94
 
91
95
  export const getInitProps = async (req: HadarsRequest): Promise<Props> => ({
@@ -112,6 +116,9 @@ hadars run
112
116
 
113
117
  # Bundle the app into a single self-contained Lambda .mjs file
114
118
  hadars export lambda [output.mjs]
119
+
120
+ # Bundle the app into a single self-contained Cloudflare Worker .mjs file
121
+ hadars export cloudflare [output.mjs]
115
122
  ```
116
123
 
117
124
  ## Features
@@ -144,66 +151,11 @@ const UserCard = ({ userId }: { userId: string }) => {
144
151
  - **Client (hydration)** - reads the pre-resolved value from the hydration cache serialised by the server; `fn()` is never called in the browser
145
152
  - **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
146
153
 
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
-
201
154
  ## Data lifecycle hooks
202
155
 
203
156
  | Hook | Runs on | Purpose |
204
157
  |---|---|---|
205
158
  | `getInitProps` | server | Fetch server-side data from the `HadarsRequest` |
206
- | `getAfterRenderProps` | server | Inspect the rendered HTML (e.g. extract critical CSS) |
207
159
  | `getFinalProps` | server | Strip server-only fields before props are serialised to the client |
208
160
  | `getClientProps` | client | Enrich props with browser-only data (localStorage, device APIs) |
209
161
 
@@ -336,6 +288,69 @@ Client-side code (anything that runs in the browser) is an exception: `process.e
336
288
 
337
289
  Client-side navigation sends a `GET <url>` request with `Accept: application/json` to refetch server data. The Lambda handler returns a JSON `{ serverData }` map for these requests — the same as the regular server does — so `useServerData` works identically in both deployment modes.
338
290
 
291
+ ## Cloudflare Workers
292
+
293
+ hadars apps can be deployed to Cloudflare Workers. The Worker handles SSR; static assets (JS, CSS, fonts) are served from R2 or another CDN.
294
+
295
+ ### Single-file bundle
296
+
297
+ `hadars export cloudflare` produces a self-contained `.mjs` Worker script. Unlike the Lambda adapter, no event format conversion is needed — Cloudflare Workers natively use the Web `Request`/`Response` API.
298
+
299
+ ```bash
300
+ # Outputs cloudflare.mjs in the current directory
301
+ hadars export cloudflare
302
+
303
+ # Custom output path
304
+ hadars export cloudflare dist/worker.mjs
305
+ ```
306
+
307
+ The command:
308
+ 1. Runs `hadars build`
309
+ 2. Generates an entry shim with static imports of the SSR module and `out.html`
310
+ 3. Bundles everything into a single ESM `.mjs` with esbuild (`platform: browser`, `target: es2022`)
311
+ 4. Prints wrangler deploy instructions
312
+
313
+ ### Deploy steps
314
+
315
+ 1. Add a `wrangler.toml` pointing at the output file:
316
+
317
+ ```toml
318
+ name = "my-app"
319
+ main = "cloudflare.mjs"
320
+ compatibility_date = "2024-09-23"
321
+ compatibility_flags = ["nodejs_compat"]
322
+ ```
323
+
324
+ 2. Upload `.hadars/static/` assets to R2 and configure routing rules so static file extensions (`*.js`, `*.css`, etc.) are served from R2 and all other requests go to the Worker.
325
+
326
+ 3. Deploy:
327
+
328
+ ```bash
329
+ wrangler deploy
330
+ ```
331
+
332
+ ### `createCloudflareHandler` API
333
+
334
+ ```ts
335
+ import { createCloudflareHandler, type CloudflareBundled } from 'hadars/cloudflare';
336
+ import * as ssrModule from './.hadars/index.ssr.js';
337
+ import outHtml from './.hadars/static/out.html';
338
+ import config from './hadars.config';
339
+
340
+ export default createCloudflareHandler(config, { ssrModule, outHtml });
341
+ ```
342
+
343
+ | Parameter | Type | Description |
344
+ |---|---|---|
345
+ | `options` | `HadarsOptions` | Same config object used for `dev`/`run` |
346
+ | `bundled` | `CloudflareBundled` | Pre-loaded SSR module + HTML template |
347
+
348
+ The returned object is a standard Cloudflare Workers export (`{ fetch(req, env, ctx) }`).
349
+
350
+ ### CPU time
351
+
352
+ hadars uses [slim-react](#slim-react) for SSR which is synchronous and typically renders a page in under 3 ms — well within Cloudflare's 10 ms free-plan CPU budget. Paid plans (Workers Paid) have no CPU time limit.
353
+
339
354
  ## slim-react
340
355
 
341
356
  hadars ships its own lightweight React-compatible SSR renderer called **slim-react** (`src/slim-react/`). It replaces `react-dom/server` on the server side entirely.
package/cli-lib.ts CHANGED
@@ -43,6 +43,78 @@ async function loadConfig(configPath: string): Promise<HadarsOptions> {
43
43
  return (mod && (mod.default ?? mod)) as HadarsOptions
44
44
  }
45
45
 
46
+ // ── hadars export cloudflare ─────────────────────────────────────────────────
47
+
48
+ async function bundleCloudflare(
49
+ config: HadarsOptions,
50
+ configPath: string,
51
+ outputFile: string,
52
+ cwd: string,
53
+ ): Promise<void> {
54
+ console.log('Building hadars project...')
55
+ await Hadars.build({ ...config, mode: 'production' })
56
+
57
+ const ssrBundle = resolve(cwd, '.hadars', 'index.ssr.js')
58
+ const outHtml = resolve(cwd, '.hadars', 'static', 'out.html')
59
+
60
+ if (!existsSync(ssrBundle)) {
61
+ console.error(`SSR bundle not found: ${ssrBundle}`)
62
+ process.exit(1)
63
+ }
64
+ if (!existsSync(outHtml)) {
65
+ console.error(`HTML template not found: ${outHtml}`)
66
+ process.exit(1)
67
+ }
68
+
69
+ // Resolve cloudflare.js from the dist/ directory (sibling of cli.js).
70
+ const cloudflareModule = resolve(dirname(fileURLToPath(import.meta.url)), 'cloudflare.js')
71
+ const shimPath = join(cwd, `.hadars-cloudflare-shim-${Date.now()}.ts`)
72
+ const shim = [
73
+ `import * as ssrModule from ${JSON.stringify(ssrBundle)};`,
74
+ `import outHtml from ${JSON.stringify(outHtml)};`,
75
+ `import { createCloudflareHandler } from ${JSON.stringify(cloudflareModule)};`,
76
+ `import config from ${JSON.stringify(configPath)};`,
77
+ `export default createCloudflareHandler(config as any, { ssrModule: ssrModule as any, outHtml });`,
78
+ ].join('\n') + '\n'
79
+ await writeFile(shimPath, shim, 'utf-8')
80
+
81
+ try {
82
+ const { build: esbuild } = await import('esbuild')
83
+ console.log(`Bundling Cloudflare Worker → ${outputFile}`)
84
+ await esbuild({
85
+ entryPoints: [shimPath],
86
+ bundle: true,
87
+ // 'browser' avoids Node.js built-in shims; CF Workers uses Web APIs.
88
+ // If you use node:* APIs in your app code, add nodejs_compat to wrangler.toml.
89
+ platform: 'browser',
90
+ format: 'esm',
91
+ target: ['es2022'],
92
+ outfile: outputFile,
93
+ sourcemap: false,
94
+ loader: { '.html': 'text', '.tsx': 'tsx', '.ts': 'ts' },
95
+ // @rspack/* is build-time only — never imported at Worker runtime.
96
+ external: ['@rspack/*'],
97
+ // Cloudflare Workers supports the Web Crypto API natively; suppress
98
+ // esbuild's attempt to polyfill node:crypto.
99
+ define: { 'global': 'globalThis' },
100
+ })
101
+ console.log(`Cloudflare Worker bundle written to ${outputFile}`)
102
+ console.log(`\nDeploy instructions:`)
103
+ console.log(` 1. Ensure wrangler.toml points to the output file:`)
104
+ console.log(` name = "my-app"`)
105
+ console.log(` main = "${outputFile}"`)
106
+ console.log(` compatibility_date = "2024-09-23"`)
107
+ console.log(` compatibility_flags = ["nodejs_compat"]`)
108
+ console.log(` 2. Upload .hadars/static/ assets to R2 (or another CDN):`)
109
+ console.log(` wrangler r2 object put my-bucket/assets/ --file .hadars/static/ --recursive`)
110
+ console.log(` 3. Add a route rule in wrangler.toml to send *.js / *.css to R2`)
111
+ console.log(` and all other requests to the Worker.`)
112
+ console.log(` 4. Deploy: wrangler deploy`)
113
+ } finally {
114
+ await unlink(shimPath).catch(() => {})
115
+ }
116
+ }
117
+
46
118
  // ── hadars export lambda ────────────────────────────────────────────────────
47
119
 
48
120
  async function bundleLambda(
@@ -172,7 +244,7 @@ dist/
172
244
 
173
245
  'src/App.tsx': () =>
174
246
  `import React from 'react';
175
- import { HadarsContext, HadarsHead, type HadarsApp } from 'hadars';
247
+ import { HadarsHead, type HadarsApp } from 'hadars';
176
248
 
177
249
  const css = \`
178
250
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -284,15 +356,15 @@ const css = \`
284
356
 
285
357
  \`;
286
358
 
287
- const App: HadarsApp<{}> = ({ context }) => {
359
+ const App: HadarsApp<{}> = () => {
288
360
  const [count, setCount] = React.useState(0);
289
361
 
290
362
  return (
291
- <HadarsContext context={context}>
363
+ <>
292
364
  <HadarsHead status={200}>
293
365
  <title>My App</title>
294
- <meta id="viewport" name="viewport" content="width=device-width, initial-scale=1" />
295
- <style id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
366
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
367
+ <style data-id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
296
368
  </HadarsHead>
297
369
 
298
370
  <nav className="nav">
@@ -350,7 +422,7 @@ const App: HadarsApp<{}> = ({ context }) => {
350
422
  </div>
351
423
  </div>
352
424
 
353
- </HadarsContext>
425
+ </>
354
426
  );
355
427
  };
356
428
 
@@ -388,7 +460,7 @@ Done! Next steps:
388
460
  // ── CLI entry ─────────────────────────────────────────────────────────────────
389
461
 
390
462
  function usage(): void {
391
- console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs]>')
463
+ console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs]>')
392
464
  }
393
465
 
394
466
  export async function runCli(argv: string[], cwd = process.cwd()): Promise<void> {
@@ -436,13 +508,18 @@ export async function runCli(argv: string[], cwd = process.cwd()): Promise<void>
436
508
  process.exit(0)
437
509
  case 'export': {
438
510
  const subCmd = argv[3]
439
- if (subCmd !== 'lambda') {
440
- console.error(`Unknown export target: ${subCmd ?? '(none)'}. Did you mean: hadars export lambda`)
511
+ if (subCmd === 'lambda') {
512
+ const outputFile = resolve(cwd, argv[4] ?? 'lambda.mjs')
513
+ await bundleLambda(cfg, configPath, outputFile, cwd)
514
+ process.exit(0)
515
+ } else if (subCmd === 'cloudflare') {
516
+ const outputFile = resolve(cwd, argv[4] ?? 'cloudflare.mjs')
517
+ await bundleCloudflare(cfg, configPath, outputFile, cwd)
518
+ process.exit(0)
519
+ } else {
520
+ console.error(`Unknown export target: ${subCmd ?? '(none)'}. Supported: lambda, cloudflare`)
441
521
  process.exit(1)
442
522
  }
443
- const outputFile = resolve(cwd, argv[4] ?? 'lambda.mjs')
444
- await bundleLambda(cfg, configPath, outputFile, cwd)
445
- process.exit(0)
446
523
  }
447
524
  case 'run':
448
525
  console.log('Running project...')
@@ -0,0 +1,332 @@
1
+ import {
2
+ renderPreflight,
3
+ renderToString
4
+ } from "./chunk-LY5MTHFV.js";
5
+ import {
6
+ createElement
7
+ } from "./chunk-OS3V4CPN.js";
8
+
9
+ // src/utils/cookies.ts
10
+ var parseCookies = (cookieString) => {
11
+ const cookies = {};
12
+ if (!cookieString) {
13
+ return cookies;
14
+ }
15
+ const pairs = cookieString.split(";");
16
+ for (const pair of pairs) {
17
+ const index = pair.indexOf("=");
18
+ if (index > -1) {
19
+ const key = pair.slice(0, index).trim();
20
+ const value = pair.slice(index + 1).trim();
21
+ try {
22
+ cookies[key] = decodeURIComponent(value);
23
+ } catch {
24
+ cookies[key] = value;
25
+ }
26
+ }
27
+ }
28
+ return cookies;
29
+ };
30
+
31
+ // src/utils/request.tsx
32
+ var parseRequest = (request) => {
33
+ const url = new URL(request.url);
34
+ const cookies = request.headers.get("Cookie") || "";
35
+ const cookieRecord = parseCookies(cookies);
36
+ return Object.assign(request, { pathname: url.pathname, search: url.search, location: url.pathname + url.search, cookies: cookieRecord });
37
+ };
38
+
39
+ // src/utils/proxyHandler.tsx
40
+ var cloneHeaders = (headers) => {
41
+ return new Headers(headers);
42
+ };
43
+ var getCORSHeaders = (req) => {
44
+ const origin = req.headers.get("Origin") || "*";
45
+ return {
46
+ "Access-Control-Allow-Origin": origin,
47
+ "Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
48
+ "Access-Control-Allow-Headers": req.headers.get("Access-Control-Request-Headers") || "*",
49
+ "Access-Control-Allow-Credentials": "true"
50
+ };
51
+ };
52
+ var createProxyHandler = (options) => {
53
+ const { proxy, proxyCORS } = options;
54
+ if (!proxy) {
55
+ return () => void 0;
56
+ }
57
+ if (typeof proxy === "function") {
58
+ return async (req) => {
59
+ if (req.method === "OPTIONS" && options.proxyCORS) {
60
+ return new Response(null, {
61
+ status: 204,
62
+ headers: getCORSHeaders(req)
63
+ });
64
+ }
65
+ const res = await proxy(req);
66
+ if (res && proxyCORS) {
67
+ const modifiedHeaders = new Headers(res.headers);
68
+ Object.entries(getCORSHeaders(req)).forEach(([key, value]) => {
69
+ modifiedHeaders.set(key, value);
70
+ });
71
+ return new Response(res.body, {
72
+ status: res.status,
73
+ statusText: res.statusText,
74
+ headers: modifiedHeaders
75
+ });
76
+ }
77
+ return res || void 0;
78
+ };
79
+ }
80
+ const proxyRules = Object.entries(proxy).sort((a, b) => b[0].length - a[0].length);
81
+ return async (req) => {
82
+ for (const [path, target] of proxyRules) {
83
+ if (req.pathname.startsWith(path)) {
84
+ if (req.method === "OPTIONS" && proxyCORS) {
85
+ return new Response(null, {
86
+ status: 204,
87
+ headers: getCORSHeaders(req)
88
+ });
89
+ }
90
+ const targetURL = new URL(target);
91
+ targetURL.pathname = targetURL.pathname.replace(/\/$/, "") + req.pathname.slice(path.length);
92
+ targetURL.search = req.search;
93
+ const sendHeaders = cloneHeaders(req.headers);
94
+ sendHeaders.set("Host", targetURL.host);
95
+ const hasBody = !["GET", "HEAD"].includes(req.method);
96
+ const proxyReq = new Request(targetURL.toString(), {
97
+ method: req.method,
98
+ headers: sendHeaders,
99
+ body: hasBody ? req.body : void 0,
100
+ redirect: "follow",
101
+ // Node.js (undici) requires duplex:'half' when body is a ReadableStream
102
+ ...hasBody ? { duplex: "half" } : {}
103
+ });
104
+ const res = await fetch(proxyReq);
105
+ const body = await res.arrayBuffer();
106
+ const clonedRes = new Headers(res.headers);
107
+ clonedRes.delete("content-length");
108
+ clonedRes.delete("content-encoding");
109
+ if (proxyCORS) {
110
+ Object.entries(getCORSHeaders(req)).forEach(([key, value]) => {
111
+ clonedRes.set(key, value);
112
+ });
113
+ }
114
+ return new Response(body, {
115
+ status: res.status,
116
+ statusText: res.statusText,
117
+ headers: clonedRes
118
+ });
119
+ }
120
+ }
121
+ return void 0;
122
+ };
123
+ };
124
+
125
+ // src/utils/response.tsx
126
+ var ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
127
+ var escAttr = (s) => s.replace(/[&<>"]/g, (c) => ESC[c] ?? c);
128
+ var escText = (s) => s.replace(/[&<>]/g, (c) => ESC[c] ?? c);
129
+ var ATTR = {
130
+ className: "class",
131
+ htmlFor: "for",
132
+ httpEquiv: "http-equiv",
133
+ charSet: "charset",
134
+ crossOrigin: "crossorigin",
135
+ noModule: "nomodule",
136
+ referrerPolicy: "referrerpolicy",
137
+ fetchPriority: "fetchpriority",
138
+ hrefLang: "hreflang"
139
+ };
140
+ function renderHeadTag(tag, id, opts, selfClose = false) {
141
+ let attrs = ` id="${escAttr(id)}"`;
142
+ let inner = "";
143
+ for (const [k, v] of Object.entries(opts)) {
144
+ if (k === "key" || k === "children") continue;
145
+ if (k === "dangerouslySetInnerHTML") {
146
+ inner = v.__html ?? "";
147
+ continue;
148
+ }
149
+ const attr = ATTR[k] ?? k;
150
+ if (v === true) attrs += ` ${attr}`;
151
+ else if (v !== false && v != null) attrs += ` ${attr}="${escAttr(String(v))}"`;
152
+ }
153
+ return selfClose ? `<${tag}${attrs}>` : `<${tag}${attrs}>${inner}</${tag}>`;
154
+ }
155
+ function buildHeadHtml(seoData) {
156
+ let html = `<title>${escText(seoData.title ?? "")}</title>`;
157
+ for (const [id, opts] of Object.entries(seoData.meta))
158
+ html += renderHeadTag("meta", id, opts, true);
159
+ for (const [id, opts] of Object.entries(seoData.link))
160
+ html += renderHeadTag("link", id, opts, true);
161
+ for (const [id, opts] of Object.entries(seoData.style))
162
+ html += renderHeadTag("style", id, opts);
163
+ for (const [id, opts] of Object.entries(seoData.script))
164
+ html += renderHeadTag("script", id, opts);
165
+ return html;
166
+ }
167
+ var getReactResponse = async (req, opts) => {
168
+ const App = opts.document.body;
169
+ const { getInitProps, getFinalProps } = opts.document;
170
+ const context = {
171
+ head: { title: "Hadars App", meta: {}, link: {}, style: {}, script: {}, status: 200 }
172
+ };
173
+ let props = {
174
+ ...getInitProps ? await getInitProps(req) : {},
175
+ location: req.location,
176
+ context
177
+ };
178
+ const unsuspend = { cache: /* @__PURE__ */ new Map() };
179
+ globalThis.__hadarsUnsuspend = unsuspend;
180
+ globalThis.__hadarsContext = context;
181
+ const element = createElement(App, props);
182
+ try {
183
+ await renderPreflight(element);
184
+ } finally {
185
+ globalThis.__hadarsUnsuspend = null;
186
+ globalThis.__hadarsContext = null;
187
+ }
188
+ const status = context.head.status;
189
+ const getAppBody = async () => {
190
+ globalThis.__hadarsUnsuspend = unsuspend;
191
+ globalThis.__hadarsContext = context;
192
+ try {
193
+ return await renderToString(element);
194
+ } finally {
195
+ globalThis.__hadarsUnsuspend = null;
196
+ globalThis.__hadarsContext = null;
197
+ }
198
+ };
199
+ const finalize = async () => {
200
+ const { context: _, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
201
+ const serverData = {};
202
+ let hasServerData = false;
203
+ for (const [key, entry] of unsuspend.cache) {
204
+ if (entry.status === "fulfilled") {
205
+ serverData[key] = entry.value;
206
+ hasServerData = true;
207
+ }
208
+ }
209
+ return {
210
+ clientProps: {
211
+ ...restProps,
212
+ location: req.location,
213
+ ...hasServerData ? { __serverData: serverData } : {}
214
+ }
215
+ };
216
+ };
217
+ return { head: context.head, status, getAppBody, finalize };
218
+ };
219
+
220
+ // src/utils/ssrHandler.ts
221
+ var HEAD_MARKER = '<meta name="HADARS_HEAD">';
222
+ var BODY_MARKER = '<meta name="HADARS_BODY">';
223
+ var encoder = new TextEncoder();
224
+ async function buildSsrHtml(bodyHtml, clientProps, headHtml, getPrecontentHtml) {
225
+ const [precontentHtml, postContent] = await Promise.resolve(getPrecontentHtml(headHtml));
226
+ const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c");
227
+ return precontentHtml + `<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent;
228
+ }
229
+ var makePrecontentHtmlGetter = (htmlFilePromise) => {
230
+ let preHead = null;
231
+ let postHead = null;
232
+ let postContent = null;
233
+ return (headHtml) => {
234
+ if (preHead !== null) {
235
+ return [preHead + headHtml + postHead, postContent];
236
+ }
237
+ return htmlFilePromise.then((html) => {
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
+ });
245
+ };
246
+ };
247
+ async function transformStream(data, stream) {
248
+ const writer = stream.writable.getWriter();
249
+ writer.write(data);
250
+ writer.close();
251
+ const chunks = [];
252
+ const reader = stream.readable.getReader();
253
+ while (true) {
254
+ const { done, value } = await reader.read();
255
+ if (done) break;
256
+ chunks.push(value);
257
+ }
258
+ const total = chunks.reduce((n, c) => n + c.length, 0);
259
+ const out = new Uint8Array(total);
260
+ let offset = 0;
261
+ for (const c of chunks) {
262
+ out.set(c, offset);
263
+ offset += c.length;
264
+ }
265
+ return out;
266
+ }
267
+ var gzipCompress = (d) => transformStream(d, new globalThis.CompressionStream("gzip"));
268
+ var gzipDecompress = (d) => transformStream(d, new globalThis.DecompressionStream("gzip"));
269
+ async function buildCacheEntry(res, ttl) {
270
+ const buf = await res.arrayBuffer();
271
+ const body = await gzipCompress(new Uint8Array(buf));
272
+ const headers = [];
273
+ res.headers.forEach((v, k) => {
274
+ if (k.toLowerCase() !== "content-encoding" && k.toLowerCase() !== "content-length") {
275
+ headers.push([k, v]);
276
+ }
277
+ });
278
+ headers.push(["content-encoding", "gzip"]);
279
+ return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
280
+ }
281
+ async function serveFromEntry(entry, req) {
282
+ const accept = req.headers.get("Accept-Encoding") ?? "";
283
+ if (accept.includes("gzip")) {
284
+ return new Response(entry.body.buffer, { status: entry.status, headers: entry.headers });
285
+ }
286
+ const plain = await gzipDecompress(entry.body);
287
+ const headers = entry.headers.filter(([k]) => k.toLowerCase() !== "content-encoding");
288
+ return new Response(plain.buffer, { status: entry.status, headers });
289
+ }
290
+ function createRenderCache(opts, handler) {
291
+ const store = /* @__PURE__ */ new Map();
292
+ const inFlight = /* @__PURE__ */ new Map();
293
+ return async (req, ctx) => {
294
+ const hadarsReq = parseRequest(req);
295
+ const cacheOpts = await opts(hadarsReq);
296
+ const key = cacheOpts?.key ?? null;
297
+ if (key != null) {
298
+ const entry = store.get(key);
299
+ if (entry) {
300
+ const expired = entry.expiresAt != null && Date.now() >= entry.expiresAt;
301
+ if (!expired) return serveFromEntry(entry, req);
302
+ store.delete(key);
303
+ }
304
+ let flight = inFlight.get(key);
305
+ if (!flight) {
306
+ const ttl = cacheOpts?.ttl;
307
+ flight = handler(new Request(req), ctx).then(async (res) => {
308
+ if (!res || res.status < 200 || res.status >= 300 || res.headers.has("set-cookie")) {
309
+ return null;
310
+ }
311
+ const newEntry2 = await buildCacheEntry(res, ttl);
312
+ store.set(key, newEntry2);
313
+ return newEntry2;
314
+ }).catch(() => null).finally(() => inFlight.delete(key));
315
+ inFlight.set(key, flight);
316
+ }
317
+ const newEntry = await flight;
318
+ if (newEntry) return serveFromEntry(newEntry, req);
319
+ }
320
+ return handler(req, ctx);
321
+ };
322
+ }
323
+
324
+ export {
325
+ parseRequest,
326
+ createProxyHandler,
327
+ buildHeadHtml,
328
+ getReactResponse,
329
+ buildSsrHtml,
330
+ makePrecontentHtmlGetter,
331
+ createRenderCache
332
+ };