hadars 0.2.0 → 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.
- package/README.md +84 -15
- package/cli-lib.ts +89 -12
- package/dist/chunk-HWOLYLPF.js +332 -0
- package/dist/cli.js +82 -12
- package/dist/cloudflare.cjs +1394 -0
- package/dist/cloudflare.d.cts +64 -0
- package/dist/cloudflare.d.ts +64 -0
- package/dist/cloudflare.js +68 -0
- package/dist/{hadars-Bh-V5YXg.d.cts → hadars-DEBSYAQl.d.cts} +1 -36
- package/dist/{hadars-Bh-V5YXg.d.ts → hadars-DEBSYAQl.d.ts} +1 -36
- package/dist/index.cjs +129 -156
- package/dist/index.d.cts +5 -11
- package/dist/index.d.ts +5 -11
- package/dist/index.js +129 -155
- package/dist/lambda.cjs +4 -0
- package/dist/lambda.d.cts +1 -2
- package/dist/lambda.d.ts +1 -2
- package/dist/lambda.js +10 -317
- package/dist/ssr-render-worker.js +3 -2
- package/dist/utils/Head.tsx +132 -187
- package/package.json +7 -2
- package/src/cloudflare.ts +139 -0
- package/src/index.tsx +0 -3
- package/src/ssr-render-worker.ts +4 -1
- package/src/types/hadars.ts +0 -1
- package/src/utils/Head.tsx +132 -187
- package/src/utils/response.tsx +6 -0
package/README.md
CHANGED
|
@@ -19,27 +19,30 @@ 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-
|
|
23
|
-
> hadars is **
|
|
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 | **
|
|
30
|
-
| Latency median | **
|
|
31
|
-
| Latency p99 | **
|
|
32
|
-
| Throughput | **
|
|
33
|
-
|
|
|
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 | **
|
|
40
|
-
| FCP | **
|
|
41
|
-
| DOMContentLoaded | **
|
|
42
|
-
| Load | **
|
|
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 -->
|
|
44
47
|
|
|
45
48
|
## Quick start
|
|
@@ -76,17 +79,17 @@ export default config;
|
|
|
76
79
|
**src/App.tsx**
|
|
77
80
|
```tsx
|
|
78
81
|
import React from 'react';
|
|
79
|
-
import {
|
|
82
|
+
import { HadarsHead, type HadarsApp, type HadarsRequest } from 'hadars';
|
|
80
83
|
|
|
81
84
|
interface Props { user: { name: string } }
|
|
82
85
|
|
|
83
|
-
const App: HadarsApp<Props> = ({ user
|
|
84
|
-
|
|
86
|
+
const App: HadarsApp<Props> = ({ user }) => (
|
|
87
|
+
<>
|
|
85
88
|
<HadarsHead status={200}>
|
|
86
89
|
<title>Hello {user.name}</title>
|
|
87
90
|
</HadarsHead>
|
|
88
91
|
<h1>Hello, {user.name}!</h1>
|
|
89
|
-
|
|
92
|
+
</>
|
|
90
93
|
);
|
|
91
94
|
|
|
92
95
|
export const getInitProps = async (req: HadarsRequest): Promise<Props> => ({
|
|
@@ -113,6 +116,9 @@ hadars run
|
|
|
113
116
|
|
|
114
117
|
# Bundle the app into a single self-contained Lambda .mjs file
|
|
115
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]
|
|
116
122
|
```
|
|
117
123
|
|
|
118
124
|
## Features
|
|
@@ -282,6 +288,69 @@ Client-side code (anything that runs in the browser) is an exception: `process.e
|
|
|
282
288
|
|
|
283
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.
|
|
284
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
|
+
|
|
285
354
|
## slim-react
|
|
286
355
|
|
|
287
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 {
|
|
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<{}> = (
|
|
359
|
+
const App: HadarsApp<{}> = () => {
|
|
288
360
|
const [count, setCount] = React.useState(0);
|
|
289
361
|
|
|
290
362
|
return (
|
|
291
|
-
|
|
363
|
+
<>
|
|
292
364
|
<HadarsHead status={200}>
|
|
293
365
|
<title>My App</title>
|
|
294
|
-
<meta
|
|
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
|
-
|
|
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
|
|
440
|
-
|
|
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 = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
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
|
+
};
|