@ereo/server 0.1.6

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/index.js ADDED
@@ -0,0 +1,1080 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/bun-server.ts
5
+ import { createContext } from "@ereo/core";
6
+ import { matchWithLayouts } from "@ereo/router";
7
+
8
+ // src/middleware.ts
9
+ class MiddlewareChain {
10
+ middlewares = [];
11
+ use(pathOrHandler, maybeHandler) {
12
+ if (typeof pathOrHandler === "function") {
13
+ this.middlewares.push({ handler: pathOrHandler });
14
+ } else {
15
+ this.middlewares.push({ path: pathOrHandler, handler: maybeHandler });
16
+ }
17
+ return this;
18
+ }
19
+ async execute(request, context, final) {
20
+ const url = new URL(request.url);
21
+ const pathname = url.pathname;
22
+ const applicable = this.middlewares.filter((m) => {
23
+ if (!m.path)
24
+ return true;
25
+ const paths = Array.isArray(m.path) ? m.path : [m.path];
26
+ return paths.some((p) => this.matchPath(pathname, p));
27
+ });
28
+ let index = 0;
29
+ const next = async () => {
30
+ if (index < applicable.length) {
31
+ const middleware = applicable[index++];
32
+ return middleware.handler(request, context, next);
33
+ }
34
+ return final();
35
+ };
36
+ return next();
37
+ }
38
+ matchPath(pathname, pattern) {
39
+ if (pattern === "*")
40
+ return true;
41
+ if (pattern.endsWith("/*")) {
42
+ const prefix = pattern.slice(0, -2);
43
+ return pathname.startsWith(prefix);
44
+ }
45
+ return pathname === pattern;
46
+ }
47
+ }
48
+ function createMiddlewareChain() {
49
+ return new MiddlewareChain;
50
+ }
51
+ function logger() {
52
+ return async (request, context, next) => {
53
+ const start = performance.now();
54
+ const url = new URL(request.url);
55
+ console.log(`\u2192 ${request.method} ${url.pathname}`);
56
+ const response = await next();
57
+ const duration = (performance.now() - start).toFixed(2);
58
+ console.log(`\u2190 ${request.method} ${url.pathname} ${response.status} ${duration}ms`);
59
+ return response;
60
+ };
61
+ }
62
+ function cors(options = {}) {
63
+ const {
64
+ origin = "*",
65
+ methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
66
+ allowedHeaders = ["Content-Type", "Authorization"],
67
+ exposedHeaders = [],
68
+ credentials = false,
69
+ maxAge = 86400
70
+ } = options;
71
+ return async (request, context, next) => {
72
+ const requestOrigin = request.headers.get("Origin");
73
+ let allowedOrigin = null;
74
+ if (typeof origin === "string") {
75
+ allowedOrigin = origin;
76
+ } else if (Array.isArray(origin)) {
77
+ allowedOrigin = requestOrigin && origin.includes(requestOrigin) ? requestOrigin : null;
78
+ } else if (typeof origin === "function") {
79
+ allowedOrigin = requestOrigin && origin(requestOrigin) ? requestOrigin : null;
80
+ }
81
+ if (request.method === "OPTIONS") {
82
+ return new Response(null, {
83
+ status: 204,
84
+ headers: {
85
+ "Access-Control-Allow-Origin": allowedOrigin || "*",
86
+ "Access-Control-Allow-Methods": methods.join(", "),
87
+ "Access-Control-Allow-Headers": allowedHeaders.join(", "),
88
+ "Access-Control-Max-Age": maxAge.toString(),
89
+ ...credentials && { "Access-Control-Allow-Credentials": "true" }
90
+ }
91
+ });
92
+ }
93
+ const response = await next();
94
+ const headers = new Headers(response.headers);
95
+ if (allowedOrigin) {
96
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
97
+ }
98
+ if (exposedHeaders.length > 0) {
99
+ headers.set("Access-Control-Expose-Headers", exposedHeaders.join(", "));
100
+ }
101
+ if (credentials) {
102
+ headers.set("Access-Control-Allow-Credentials", "true");
103
+ }
104
+ return new Response(response.body, {
105
+ status: response.status,
106
+ statusText: response.statusText,
107
+ headers
108
+ });
109
+ };
110
+ }
111
+ function securityHeaders(options = {}) {
112
+ const {
113
+ contentSecurityPolicy = "default-src 'self'",
114
+ xFrameOptions = "SAMEORIGIN",
115
+ xContentTypeOptions = true,
116
+ referrerPolicy = "strict-origin-when-cross-origin",
117
+ permissionsPolicy = false
118
+ } = options;
119
+ return async (request, context, next) => {
120
+ const response = await next();
121
+ const headers = new Headers(response.headers);
122
+ if (contentSecurityPolicy) {
123
+ headers.set("Content-Security-Policy", contentSecurityPolicy);
124
+ }
125
+ if (xFrameOptions) {
126
+ headers.set("X-Frame-Options", xFrameOptions);
127
+ }
128
+ if (xContentTypeOptions) {
129
+ headers.set("X-Content-Type-Options", "nosniff");
130
+ }
131
+ if (referrerPolicy) {
132
+ headers.set("Referrer-Policy", referrerPolicy);
133
+ }
134
+ if (permissionsPolicy) {
135
+ headers.set("Permissions-Policy", permissionsPolicy);
136
+ }
137
+ return new Response(response.body, {
138
+ status: response.status,
139
+ statusText: response.statusText,
140
+ headers
141
+ });
142
+ };
143
+ }
144
+ function compress() {
145
+ return async (request, context, next) => {
146
+ const response = await next();
147
+ const acceptEncoding = request.headers.get("Accept-Encoding") || "";
148
+ const contentType = response.headers.get("Content-Type") || "";
149
+ const shouldCompress = (contentType.includes("text/") || contentType.includes("application/json") || contentType.includes("application/javascript")) && acceptEncoding.includes("gzip");
150
+ if (!shouldCompress) {
151
+ return response;
152
+ }
153
+ const headers = new Headers(response.headers);
154
+ headers.set("Content-Encoding", "gzip");
155
+ return new Response(Bun.gzipSync(await response.arrayBuffer()), {
156
+ status: response.status,
157
+ statusText: response.statusText,
158
+ headers
159
+ });
160
+ };
161
+ }
162
+ function rateLimit(options = {}) {
163
+ const { windowMs = 60000, max = 100, keyGenerator } = options;
164
+ const requests = new Map;
165
+ const getKey = keyGenerator || ((request) => {
166
+ const forwarded = request.headers.get("X-Forwarded-For");
167
+ return forwarded?.split(",")[0].trim() || "unknown";
168
+ });
169
+ return async (request, context, next) => {
170
+ const key = getKey(request);
171
+ const now = Date.now();
172
+ let record = requests.get(key);
173
+ if (!record || now > record.resetTime) {
174
+ record = { count: 0, resetTime: now + windowMs };
175
+ requests.set(key, record);
176
+ }
177
+ record.count++;
178
+ if (Math.random() < 0.01) {
179
+ for (const [k, v] of requests) {
180
+ if (now > v.resetTime) {
181
+ requests.delete(k);
182
+ }
183
+ }
184
+ }
185
+ if (record.count > max) {
186
+ return new Response(JSON.stringify({ error: "Too many requests" }), {
187
+ status: 429,
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ "Retry-After": Math.ceil((record.resetTime - now) / 1000).toString()
191
+ }
192
+ });
193
+ }
194
+ const response = await next();
195
+ const headers = new Headers(response.headers);
196
+ headers.set("X-RateLimit-Limit", max.toString());
197
+ headers.set("X-RateLimit-Remaining", Math.max(0, max - record.count).toString());
198
+ headers.set("X-RateLimit-Reset", Math.ceil(record.resetTime / 1000).toString());
199
+ return new Response(response.body, {
200
+ status: response.status,
201
+ statusText: response.statusText,
202
+ headers
203
+ });
204
+ };
205
+ }
206
+
207
+ // src/static.ts
208
+ import { stat } from "fs/promises";
209
+ import { join, extname } from "path";
210
+ var MIME_TYPES = {
211
+ ".html": "text/html; charset=utf-8",
212
+ ".css": "text/css; charset=utf-8",
213
+ ".js": "text/javascript; charset=utf-8",
214
+ ".mjs": "text/javascript; charset=utf-8",
215
+ ".json": "application/json",
216
+ ".png": "image/png",
217
+ ".jpg": "image/jpeg",
218
+ ".jpeg": "image/jpeg",
219
+ ".gif": "image/gif",
220
+ ".svg": "image/svg+xml",
221
+ ".ico": "image/x-icon",
222
+ ".webp": "image/webp",
223
+ ".avif": "image/avif",
224
+ ".woff": "font/woff",
225
+ ".woff2": "font/woff2",
226
+ ".ttf": "font/ttf",
227
+ ".otf": "font/otf",
228
+ ".eot": "application/vnd.ms-fontobject",
229
+ ".mp4": "video/mp4",
230
+ ".webm": "video/webm",
231
+ ".mp3": "audio/mpeg",
232
+ ".wav": "audio/wav",
233
+ ".pdf": "application/pdf",
234
+ ".zip": "application/zip",
235
+ ".wasm": "application/wasm",
236
+ ".map": "application/json",
237
+ ".txt": "text/plain; charset=utf-8",
238
+ ".xml": "application/xml"
239
+ };
240
+ function getMimeType(filepath) {
241
+ const ext = extname(filepath).toLowerCase();
242
+ return MIME_TYPES[ext] || "application/octet-stream";
243
+ }
244
+ var NEGOTIABLE_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png"];
245
+ function supportsFormat(accept, format) {
246
+ if (!accept)
247
+ return false;
248
+ return accept.includes(`image/${format}`);
249
+ }
250
+ async function findOptimizedVariant(filepath, accept) {
251
+ const ext = extname(filepath).toLowerCase();
252
+ if (!NEGOTIABLE_IMAGE_EXTENSIONS.includes(ext)) {
253
+ return null;
254
+ }
255
+ const basePath = filepath.slice(0, -ext.length);
256
+ if (supportsFormat(accept, "avif")) {
257
+ const avifPath = `${basePath}.avif`;
258
+ try {
259
+ await stat(avifPath);
260
+ return { path: avifPath, mime: "image/avif" };
261
+ } catch {}
262
+ }
263
+ if (supportsFormat(accept, "webp")) {
264
+ const webpPath = `${basePath}.webp`;
265
+ try {
266
+ await stat(webpPath);
267
+ return { path: webpPath, mime: "image/webp" };
268
+ } catch {}
269
+ }
270
+ return null;
271
+ }
272
+ function serveStatic(options) {
273
+ const {
274
+ root,
275
+ prefix = "/",
276
+ maxAge = 0,
277
+ immutable = true,
278
+ index = "index.html",
279
+ listing = false,
280
+ fallback,
281
+ negotiateImageFormat = true
282
+ } = options;
283
+ return async (request) => {
284
+ if (request.method !== "GET" && request.method !== "HEAD") {
285
+ return null;
286
+ }
287
+ const url = new URL(request.url);
288
+ let pathname = url.pathname;
289
+ if (prefix !== "/" && !pathname.startsWith(prefix)) {
290
+ return null;
291
+ }
292
+ if (prefix !== "/") {
293
+ pathname = pathname.slice(prefix.length) || "/";
294
+ }
295
+ if (pathname.includes("..")) {
296
+ return new Response("Forbidden", { status: 403 });
297
+ }
298
+ let filepath = join(root, pathname);
299
+ try {
300
+ let stats = await stat(filepath);
301
+ if (negotiateImageFormat && stats.isFile()) {
302
+ const accept = request.headers.get("Accept");
303
+ const optimized = await findOptimizedVariant(filepath, accept);
304
+ if (optimized) {
305
+ filepath = optimized.path;
306
+ stats = await stat(filepath);
307
+ }
308
+ }
309
+ if (stats.isDirectory()) {
310
+ const indexPath = join(filepath, index);
311
+ try {
312
+ stats = await stat(indexPath);
313
+ filepath = indexPath;
314
+ } catch {
315
+ if (listing) {
316
+ return createDirectoryListing(filepath, pathname);
317
+ }
318
+ return null;
319
+ }
320
+ }
321
+ const file = Bun.file(filepath);
322
+ const headers = new Headers;
323
+ headers.set("Content-Type", getMimeType(filepath));
324
+ headers.set("Content-Length", stats.size.toString());
325
+ const etag = `"${stats.mtimeMs.toString(16)}-${stats.size.toString(16)}"`;
326
+ headers.set("ETag", etag);
327
+ headers.set("Last-Modified", new Date(stats.mtimeMs).toUTCString());
328
+ const isFingerprinted = /\.[a-f0-9]{8,}\./.test(filepath);
329
+ if (isFingerprinted && immutable) {
330
+ headers.set("Cache-Control", `public, max-age=${maxAge}, immutable`);
331
+ } else if (maxAge > 0) {
332
+ headers.set("Cache-Control", `public, max-age=${maxAge}`);
333
+ }
334
+ const ext = extname(filepath).toLowerCase();
335
+ if (negotiateImageFormat && NEGOTIABLE_IMAGE_EXTENSIONS.includes(ext)) {
336
+ headers.set("Vary", "Accept");
337
+ }
338
+ const ifNoneMatch = request.headers.get("If-None-Match");
339
+ if (ifNoneMatch === etag) {
340
+ return new Response(null, { status: 304, headers });
341
+ }
342
+ const ifModifiedSince = request.headers.get("If-Modified-Since");
343
+ if (ifModifiedSince) {
344
+ const ifModifiedDate = new Date(ifModifiedSince);
345
+ if (stats.mtimeMs <= ifModifiedDate.getTime()) {
346
+ return new Response(null, { status: 304, headers });
347
+ }
348
+ }
349
+ if (request.method === "HEAD") {
350
+ return new Response(null, { status: 200, headers });
351
+ }
352
+ return new Response(file, { status: 200, headers });
353
+ } catch (error) {
354
+ if (error.code === "ENOENT") {
355
+ if (fallback) {
356
+ const fallbackPath = join(root, fallback);
357
+ try {
358
+ const file = Bun.file(fallbackPath);
359
+ return new Response(file, {
360
+ status: 200,
361
+ headers: {
362
+ "Content-Type": "text/html; charset=utf-8"
363
+ }
364
+ });
365
+ } catch {}
366
+ }
367
+ return null;
368
+ }
369
+ throw error;
370
+ }
371
+ };
372
+ }
373
+ async function createDirectoryListing(dirpath, pathname) {
374
+ const { readdir } = await import("fs/promises");
375
+ const entries = await readdir(dirpath, { withFileTypes: true });
376
+ const items = entries.map((entry) => {
377
+ const name = entry.isDirectory() ? `${entry.name}/` : entry.name;
378
+ const href = join(pathname, entry.name);
379
+ return `<li><a href="${href}">${name}</a></li>`;
380
+ });
381
+ const html = `
382
+ <!DOCTYPE html>
383
+ <html>
384
+ <head>
385
+ <meta charset="utf-8">
386
+ <title>Index of ${pathname}</title>
387
+ <style>
388
+ body { font-family: system-ui, sans-serif; padding: 2rem; }
389
+ ul { list-style: none; padding: 0; }
390
+ li { padding: 0.25rem 0; }
391
+ a { color: #0066cc; text-decoration: none; }
392
+ a:hover { text-decoration: underline; }
393
+ </style>
394
+ </head>
395
+ <body>
396
+ <h1>Index of ${pathname}</h1>
397
+ <ul>
398
+ ${pathname !== "/" ? '<li><a href="..">..</a></li>' : ""}
399
+ ${items.join(`
400
+ `)}
401
+ </ul>
402
+ </body>
403
+ </html>
404
+ `.trim();
405
+ return new Response(html, {
406
+ status: 200,
407
+ headers: { "Content-Type": "text/html; charset=utf-8" }
408
+ });
409
+ }
410
+ function staticMiddleware(options) {
411
+ const handler = serveStatic(options);
412
+ return async (request, context, next) => {
413
+ const response = await handler(request);
414
+ if (response)
415
+ return response;
416
+ return next();
417
+ };
418
+ }
419
+
420
+ // src/streaming.ts
421
+ import { serializeLoaderData } from "@ereo/data";
422
+ function createShell(options) {
423
+ const { shell = {}, scripts = [], styles = [], loaderData } = options;
424
+ const htmlAttrs = Object.entries(shell.htmlAttrs || {}).map(([k, v]) => `${k}="${v}"`).join(" ");
425
+ const bodyAttrs = Object.entries(shell.bodyAttrs || {}).map(([k, v]) => `${k}="${v}"`).join(" ");
426
+ const metaTags = (shell.meta || []).map((m) => {
427
+ const key = m.name ? `name="${m.name}"` : `property="${m.property}"`;
428
+ return `<meta ${key} content="${m.content}">`;
429
+ }).join(`
430
+ `);
431
+ const styleLinks = styles.map((href) => `<link rel="stylesheet" href="${href}">`).join(`
432
+ `);
433
+ const scriptTags = scripts.map((src) => `<script type="module" src="${src}"></script>`).join(`
434
+ `);
435
+ const loaderScript = loaderData ? `<script>window.__EREO_DATA__=${serializeLoaderData(loaderData)}</script>` : "";
436
+ const head = `<!DOCTYPE html>
437
+ <html ${htmlAttrs}>
438
+ <head>
439
+ <meta charset="utf-8">
440
+ <meta name="viewport" content="width=device-width, initial-scale=1">
441
+ ${shell.title ? `<title>${shell.title}</title>` : ""}
442
+ ${metaTags}
443
+ ${styleLinks}
444
+ ${shell.head || ""}
445
+ </head>
446
+ <body ${bodyAttrs}>
447
+ <div id="root">`;
448
+ const tail = `</div>
449
+ ${loaderScript}
450
+ ${scriptTags}
451
+ </body>
452
+ </html>`;
453
+ return { head, tail };
454
+ }
455
+ async function renderToStream(element, options) {
456
+ const { renderToPipeableStream } = await import("react-dom/server");
457
+ const { shell, scripts = [], styles = [] } = options;
458
+ let loaderData = null;
459
+ if (options.match.route.module?.loader) {
460
+ loaderData = await options.match.route.module.loader({
461
+ request: new Request("http://localhost"),
462
+ params: options.match.params,
463
+ context: options.context
464
+ });
465
+ }
466
+ const { head, tail } = createShell({ shell, scripts, styles, loaderData });
467
+ return new Promise((resolve, reject) => {
468
+ const { PassThrough } = __require("stream");
469
+ const { pipe, abort } = renderToPipeableStream(element, {
470
+ bootstrapScripts: scripts,
471
+ onShellReady() {
472
+ const passThrough = new PassThrough;
473
+ const chunks = [];
474
+ chunks.push(Buffer.from(head));
475
+ passThrough.on("data", (chunk) => {
476
+ chunks.push(chunk);
477
+ });
478
+ passThrough.on("end", () => {
479
+ chunks.push(Buffer.from(tail));
480
+ const fullHtml = Buffer.concat(chunks).toString("utf-8");
481
+ resolve({
482
+ body: fullHtml,
483
+ headers: new Headers({
484
+ "Content-Type": "text/html; charset=utf-8",
485
+ "Content-Length": Buffer.byteLength(fullHtml).toString()
486
+ }),
487
+ status: 200
488
+ });
489
+ });
490
+ passThrough.on("error", (error) => {
491
+ console.error("Stream error:", error);
492
+ reject(error);
493
+ });
494
+ pipe(passThrough);
495
+ },
496
+ onShellError(error) {
497
+ console.error("Shell render error:", error);
498
+ reject(error);
499
+ },
500
+ onError(error) {
501
+ console.error("Render error:", error);
502
+ }
503
+ });
504
+ setTimeout(() => {
505
+ abort();
506
+ }, 1e4);
507
+ });
508
+ }
509
+ async function renderToString(element, options) {
510
+ const { renderToString: reactRenderToString } = await import("react-dom/server");
511
+ const { shell, scripts = [], styles = [] } = options;
512
+ let loaderData = null;
513
+ if (options.match.route.module?.loader) {
514
+ loaderData = await options.match.route.module.loader({
515
+ request: new Request("http://localhost"),
516
+ params: options.match.params,
517
+ context: options.context
518
+ });
519
+ }
520
+ const { head, tail } = createShell({ shell, scripts, styles, loaderData });
521
+ const content = reactRenderToString(element);
522
+ const html = head + content + tail;
523
+ return {
524
+ body: html,
525
+ headers: new Headers({
526
+ "Content-Type": "text/html; charset=utf-8",
527
+ "Content-Length": Buffer.byteLength(html).toString()
528
+ }),
529
+ status: 200
530
+ };
531
+ }
532
+ function createResponse(result) {
533
+ return new Response(result.body, {
534
+ status: result.status,
535
+ headers: result.headers
536
+ });
537
+ }
538
+ function createSuspenseStream() {
539
+ const encoder = new TextEncoder;
540
+ let controller;
541
+ const stream = new ReadableStream({
542
+ start(c) {
543
+ controller = c;
544
+ }
545
+ });
546
+ return {
547
+ stream,
548
+ push: (chunk) => {
549
+ controller.enqueue(encoder.encode(chunk));
550
+ },
551
+ close: () => {
552
+ controller.close();
553
+ }
554
+ };
555
+ }
556
+
557
+ // src/bun-server.ts
558
+ import { serializeLoaderData as serializeLoaderData2 } from "@ereo/data";
559
+ import { createElement } from "react";
560
+ async function getStreamingRenderer() {
561
+ try {
562
+ const browserServer = await import("react-dom/server.browser");
563
+ if (typeof browserServer.renderToReadableStream === "function") {
564
+ return {
565
+ renderToReadableStream: browserServer.renderToReadableStream,
566
+ renderToString: browserServer.renderToString
567
+ };
568
+ }
569
+ } catch {}
570
+ try {
571
+ const server = await import("react-dom/server");
572
+ return {
573
+ renderToReadableStream: server.renderToReadableStream,
574
+ renderToString: server.renderToString
575
+ };
576
+ } catch {
577
+ return {
578
+ renderToReadableStream: undefined,
579
+ renderToString: (await import("react-dom/server")).renderToString
580
+ };
581
+ }
582
+ }
583
+
584
+ class BunServer {
585
+ server = null;
586
+ app = null;
587
+ router = null;
588
+ middleware;
589
+ staticHandler = null;
590
+ options;
591
+ constructor(options = {}) {
592
+ this.options = {
593
+ port: 3000,
594
+ hostname: "localhost",
595
+ development: true,
596
+ logging: true,
597
+ renderMode: "streaming",
598
+ assetsPath: "/_ereo",
599
+ clientEntry: "/_ereo/client.js",
600
+ ...options
601
+ };
602
+ this.middleware = createMiddlewareChain();
603
+ this.setupMiddleware();
604
+ if (options.static) {
605
+ this.staticHandler = serveStatic(options.static);
606
+ }
607
+ }
608
+ setupMiddleware() {
609
+ if (this.options.logging) {
610
+ this.middleware.use(logger());
611
+ }
612
+ if (this.options.cors) {
613
+ const corsOptions = typeof this.options.cors === "object" ? this.options.cors : {};
614
+ this.middleware.use(cors(corsOptions));
615
+ }
616
+ if (this.options.security !== false) {
617
+ const securityOptions = typeof this.options.security === "object" ? this.options.security : {};
618
+ this.middleware.use(securityHeaders(securityOptions));
619
+ }
620
+ }
621
+ setApp(app) {
622
+ this.app = app;
623
+ }
624
+ setRouter(router) {
625
+ this.router = router;
626
+ if (this.app) {
627
+ this.app.setRouteMatcher((pathname) => router.match(pathname));
628
+ }
629
+ }
630
+ use(pathOrHandler, maybeHandler) {
631
+ if (typeof pathOrHandler === "function") {
632
+ this.middleware.use(pathOrHandler);
633
+ } else if (maybeHandler) {
634
+ this.middleware.use(pathOrHandler, maybeHandler);
635
+ }
636
+ }
637
+ async handleRequest(request) {
638
+ const context = createContext(request);
639
+ try {
640
+ if (this.staticHandler) {
641
+ const staticResponse = await this.staticHandler(request);
642
+ if (staticResponse) {
643
+ return staticResponse;
644
+ }
645
+ }
646
+ const response = await this.middleware.execute(request, context, async () => {
647
+ if (this.options.handler) {
648
+ return this.options.handler(request);
649
+ }
650
+ if (this.router) {
651
+ const pathname = new URL(request.url).pathname;
652
+ if (typeof this.router.getRoutes === "function") {
653
+ const routes = this.router.getRoutes();
654
+ const matchResult = matchWithLayouts(pathname, routes);
655
+ if (!matchResult) {
656
+ return new Response("Not Found", { status: 404 });
657
+ }
658
+ await this.router.loadModule(matchResult.route);
659
+ for (const layout of matchResult.layouts) {
660
+ await this.router.loadModule(layout);
661
+ }
662
+ return this.handleRoute(request, matchResult, context);
663
+ } else {
664
+ const match = this.router.match(pathname);
665
+ if (!match) {
666
+ return new Response("Not Found", { status: 404 });
667
+ }
668
+ if (typeof this.router.loadModule === "function") {
669
+ await this.router.loadModule(match.route);
670
+ }
671
+ return this.handleRoute(request, { ...match, layouts: [] }, context);
672
+ }
673
+ }
674
+ if (this.app) {
675
+ return this.app.handle(request);
676
+ }
677
+ return new Response("Not Found", { status: 404 });
678
+ });
679
+ return context.applyToResponse(response);
680
+ } catch (error) {
681
+ return this.handleError(error, context);
682
+ }
683
+ }
684
+ async handleRoute(request, match, context) {
685
+ const module = match.route.module;
686
+ if (!module) {
687
+ return new Response("Route module not loaded", { status: 500 });
688
+ }
689
+ if (request.method !== "GET" && request.method !== "HEAD") {
690
+ if (module.action) {
691
+ const result = await module.action({
692
+ request,
693
+ params: match.params,
694
+ context
695
+ });
696
+ if (result instanceof Response) {
697
+ return result;
698
+ }
699
+ return new Response(JSON.stringify(result), {
700
+ headers: { "Content-Type": "application/json" }
701
+ });
702
+ }
703
+ return new Response("Method Not Allowed", { status: 405 });
704
+ }
705
+ let loaderData = null;
706
+ if (module.loader) {
707
+ loaderData = await module.loader({
708
+ request,
709
+ params: match.params,
710
+ context
711
+ });
712
+ if (loaderData instanceof Response) {
713
+ return loaderData;
714
+ }
715
+ }
716
+ if (request.headers.get("Accept")?.includes("application/json")) {
717
+ return new Response(JSON.stringify({ data: loaderData, params: match.params }), {
718
+ headers: { "Content-Type": "application/json" }
719
+ });
720
+ }
721
+ return this.renderPage(request, match, context, loaderData);
722
+ }
723
+ async renderPage(request, match, context, loaderData) {
724
+ const module = match.route.module;
725
+ if (!module?.default) {
726
+ return this.renderMinimalPage(match, loaderData);
727
+ }
728
+ const url = new URL(request.url);
729
+ const metaDescriptors = this.buildMeta(module, loaderData, match.params, url);
730
+ const shell = {
731
+ ...this.options.shell,
732
+ title: this.extractTitle(metaDescriptors) || this.options.shell?.title,
733
+ meta: this.extractMetaTags(metaDescriptors)
734
+ };
735
+ const PageComponent = module.default;
736
+ let element = createElement(PageComponent, {
737
+ loaderData,
738
+ params: match.params
739
+ });
740
+ const layouts = match.layouts || [];
741
+ for (let i = layouts.length - 1;i >= 0; i--) {
742
+ const layout = layouts[i];
743
+ if (layout.module?.default) {
744
+ const LayoutComponent = layout.module.default;
745
+ element = createElement(LayoutComponent, {
746
+ loaderData: null,
747
+ params: match.params,
748
+ children: element
749
+ });
750
+ }
751
+ }
752
+ const hasRootLayout = layouts.length > 0 && layouts[0].module?.default;
753
+ if (hasRootLayout) {
754
+ if (this.options.renderMode === "streaming") {
755
+ return this.renderStreamingPageDirect(element, loaderData);
756
+ } else {
757
+ return this.renderStringPageDirect(element, loaderData);
758
+ }
759
+ }
760
+ if (this.options.renderMode === "streaming") {
761
+ return this.renderStreamingPage(element, shell, loaderData);
762
+ } else {
763
+ return this.renderStringPage(element, shell, loaderData);
764
+ }
765
+ }
766
+ async renderStreamingPage(element, shell, loaderData) {
767
+ try {
768
+ const { renderToReadableStream } = await getStreamingRenderer();
769
+ if (!renderToReadableStream) {
770
+ return this.renderStringPage(element, shell, loaderData);
771
+ }
772
+ const scripts = [this.options.clientEntry];
773
+ const { head, tail } = createShell({ shell, scripts, loaderData });
774
+ const encoder = new TextEncoder;
775
+ const headBytes = encoder.encode(head);
776
+ const tailBytes = encoder.encode(tail);
777
+ const reactStream = await renderToReadableStream(element, {
778
+ onError(error) {
779
+ console.error("Streaming render error:", error);
780
+ }
781
+ });
782
+ await reactStream.allReady;
783
+ const reader = reactStream.getReader();
784
+ const chunks = [headBytes];
785
+ while (true) {
786
+ const { done, value } = await reader.read();
787
+ if (done)
788
+ break;
789
+ chunks.push(value);
790
+ }
791
+ chunks.push(tailBytes);
792
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
793
+ const fullHtml = new Uint8Array(totalLength);
794
+ let offset = 0;
795
+ for (const chunk of chunks) {
796
+ fullHtml.set(chunk, offset);
797
+ offset += chunk.length;
798
+ }
799
+ return new Response(fullHtml, {
800
+ status: 200,
801
+ headers: {
802
+ "Content-Type": "text/html; charset=utf-8",
803
+ "Content-Length": totalLength.toString()
804
+ }
805
+ });
806
+ } catch (error) {
807
+ console.error("Streaming render failed:", error);
808
+ return this.renderStringPage(element, shell, loaderData);
809
+ }
810
+ }
811
+ async renderStringPage(element, shell, loaderData) {
812
+ try {
813
+ const { renderToString: reactRenderToString } = await import("react-dom/server");
814
+ const scripts = [this.options.clientEntry];
815
+ const { head, tail } = createShell({ shell, scripts, loaderData });
816
+ const content = reactRenderToString(element);
817
+ const html = head + content + tail;
818
+ const encoder = new TextEncoder;
819
+ const htmlBytes = encoder.encode(html);
820
+ return new Response(htmlBytes, {
821
+ status: 200,
822
+ headers: {
823
+ "Content-Type": "text/html; charset=utf-8",
824
+ "Content-Length": htmlBytes.length.toString()
825
+ }
826
+ });
827
+ } catch (error) {
828
+ console.error("String render failed:", error);
829
+ return this.renderErrorPage(error);
830
+ }
831
+ }
832
+ async renderStreamingPageDirect(element, loaderData) {
833
+ try {
834
+ const { renderToReadableStream } = await getStreamingRenderer();
835
+ if (!renderToReadableStream) {
836
+ return this.renderStringPageDirect(element, loaderData);
837
+ }
838
+ const reactStream = await renderToReadableStream(element, {
839
+ onError(error) {
840
+ console.error("Streaming render error:", error);
841
+ }
842
+ });
843
+ await reactStream.allReady;
844
+ const reader = reactStream.getReader();
845
+ const chunks = [];
846
+ while (true) {
847
+ const { done, value } = await reader.read();
848
+ if (done)
849
+ break;
850
+ chunks.push(value);
851
+ }
852
+ const encoder = new TextEncoder;
853
+ const loaderScript = loaderData ? `<script>window.__EREO_DATA__=${serializeLoaderData2(loaderData)}</script>` : "";
854
+ const clientScript = `<script type="module" src="${this.options.clientEntry}"></script>`;
855
+ const injectedScripts = encoder.encode(loaderScript + clientScript);
856
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0) + injectedScripts.length;
857
+ const fullHtml = new Uint8Array(totalLength);
858
+ let offset = 0;
859
+ for (const chunk of chunks) {
860
+ fullHtml.set(chunk, offset);
861
+ offset += chunk.length;
862
+ }
863
+ fullHtml.set(injectedScripts, offset);
864
+ return new Response(fullHtml, {
865
+ status: 200,
866
+ headers: {
867
+ "Content-Type": "text/html; charset=utf-8",
868
+ "Content-Length": totalLength.toString()
869
+ }
870
+ });
871
+ } catch (error) {
872
+ console.error("Streaming render failed:", error);
873
+ return this.renderStringPageDirect(element, loaderData);
874
+ }
875
+ }
876
+ async renderStringPageDirect(element, loaderData) {
877
+ try {
878
+ const { renderToString: reactRenderToString } = await import("react-dom/server");
879
+ let html = reactRenderToString(element);
880
+ const loaderScript = loaderData ? `<script>window.__EREO_DATA__=${serializeLoaderData2(loaderData)}</script>` : "";
881
+ const clientScript = `<script type="module" src="${this.options.clientEntry}"></script>`;
882
+ html = html.replace("</body>", `${loaderScript}${clientScript}</body>`);
883
+ const encoder = new TextEncoder;
884
+ const htmlBytes = encoder.encode(html);
885
+ return new Response(htmlBytes, {
886
+ status: 200,
887
+ headers: {
888
+ "Content-Type": "text/html; charset=utf-8",
889
+ "Content-Length": htmlBytes.length.toString()
890
+ }
891
+ });
892
+ } catch (error) {
893
+ console.error("String render failed:", error);
894
+ return this.renderErrorPage(error);
895
+ }
896
+ }
897
+ renderMinimalPage(match, loaderData) {
898
+ const serializedData = serializeLoaderData2({
899
+ loaderData,
900
+ params: match.params
901
+ });
902
+ const html = `<!DOCTYPE html>
903
+ <html lang="en">
904
+ <head>
905
+ <meta charset="utf-8">
906
+ <meta name="viewport" content="width=device-width, initial-scale=1">
907
+ <title>EreoJS App</title>
908
+ </head>
909
+ <body>
910
+ <div id="root"></div>
911
+ <script>window.__EREO_DATA__=${serializedData}</script>
912
+ <script type="module" src="${this.options.clientEntry}"></script>
913
+ </body>
914
+ </html>`;
915
+ const encoder = new TextEncoder;
916
+ const htmlBytes = encoder.encode(html);
917
+ return new Response(htmlBytes, {
918
+ status: 200,
919
+ headers: {
920
+ "Content-Type": "text/html; charset=utf-8",
921
+ "Content-Length": htmlBytes.length.toString()
922
+ }
923
+ });
924
+ }
925
+ renderErrorPage(error) {
926
+ const message = error instanceof Error ? error.message : "An error occurred";
927
+ const stack = this.options.development && error instanceof Error ? error.stack : undefined;
928
+ const html = `<!DOCTYPE html>
929
+ <html lang="en">
930
+ <head>
931
+ <meta charset="utf-8">
932
+ <meta name="viewport" content="width=device-width, initial-scale=1">
933
+ <title>Error - EreoJS</title>
934
+ <style>
935
+ body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
936
+ h1 { color: #dc2626; }
937
+ pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; overflow-x: auto; border-radius: 4px; }
938
+ </style>
939
+ </head>
940
+ <body>
941
+ <h1>Render Error</h1>
942
+ <p>${this.escapeHtml(message)}</p>
943
+ ${stack ? `<pre>${this.escapeHtml(stack)}</pre>` : ""}
944
+ </body>
945
+ </html>`;
946
+ return new Response(html, {
947
+ status: 500,
948
+ headers: {
949
+ "Content-Type": "text/html; charset=utf-8"
950
+ }
951
+ });
952
+ }
953
+ buildMeta(module, loaderData, params, url) {
954
+ if (!module.meta) {
955
+ return [];
956
+ }
957
+ try {
958
+ return module.meta({
959
+ data: loaderData,
960
+ params,
961
+ location: {
962
+ pathname: url.pathname,
963
+ search: url.search,
964
+ hash: url.hash
965
+ }
966
+ });
967
+ } catch (error) {
968
+ console.error("Error building meta:", error);
969
+ return [];
970
+ }
971
+ }
972
+ extractTitle(meta) {
973
+ for (const descriptor of meta) {
974
+ if (descriptor.title) {
975
+ return descriptor.title;
976
+ }
977
+ }
978
+ return;
979
+ }
980
+ extractMetaTags(meta) {
981
+ const tags = [];
982
+ for (const descriptor of meta) {
983
+ if (descriptor.name && descriptor.content) {
984
+ tags.push({ name: descriptor.name, content: descriptor.content });
985
+ } else if (descriptor.property && descriptor.content) {
986
+ tags.push({ property: descriptor.property, content: descriptor.content });
987
+ }
988
+ }
989
+ return tags;
990
+ }
991
+ escapeHtml(str) {
992
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
993
+ }
994
+ handleError(error, context) {
995
+ const message = error instanceof Error ? error.message : "Internal Server Error";
996
+ const stack = error instanceof Error ? error.stack : undefined;
997
+ console.error("Server error:", error);
998
+ if (this.options.development) {
999
+ return new Response(JSON.stringify({
1000
+ error: message,
1001
+ stack: stack?.split(`
1002
+ `)
1003
+ }), {
1004
+ status: 500,
1005
+ headers: { "Content-Type": "application/json" }
1006
+ });
1007
+ }
1008
+ return new Response("Internal Server Error", { status: 500 });
1009
+ }
1010
+ async start() {
1011
+ const { port, hostname, tls, websocket } = this.options;
1012
+ const serverOptions = {
1013
+ port,
1014
+ hostname,
1015
+ fetch: (request) => this.handleRequest(request),
1016
+ error: (error) => this.handleError(error, {})
1017
+ };
1018
+ if (tls) {
1019
+ serverOptions.tls = tls;
1020
+ }
1021
+ if (websocket) {
1022
+ serverOptions.websocket = websocket;
1023
+ }
1024
+ this.server = Bun.serve(serverOptions);
1025
+ const protocol = tls ? "https" : "http";
1026
+ console.log(`Server running at ${protocol}://${hostname}:${port}`);
1027
+ return this.server;
1028
+ }
1029
+ stop() {
1030
+ if (this.server) {
1031
+ this.server.stop();
1032
+ this.server = null;
1033
+ }
1034
+ }
1035
+ async reload() {
1036
+ if (this.server) {
1037
+ this.server.reload({
1038
+ fetch: (request) => this.handleRequest(request)
1039
+ });
1040
+ }
1041
+ }
1042
+ getServer() {
1043
+ return this.server;
1044
+ }
1045
+ getInfo() {
1046
+ return {
1047
+ port: this.options.port,
1048
+ hostname: this.options.hostname,
1049
+ development: this.options.development
1050
+ };
1051
+ }
1052
+ }
1053
+ function createServer(options) {
1054
+ return new BunServer(options);
1055
+ }
1056
+ async function serve(options) {
1057
+ const server = createServer(options);
1058
+ await server.start();
1059
+ return server;
1060
+ }
1061
+ export {
1062
+ staticMiddleware,
1063
+ serveStatic,
1064
+ serve,
1065
+ securityHeaders,
1066
+ renderToString,
1067
+ renderToStream,
1068
+ rateLimit,
1069
+ logger,
1070
+ getMimeType,
1071
+ createSuspenseStream,
1072
+ createShell,
1073
+ createServer,
1074
+ createResponse,
1075
+ createMiddlewareChain,
1076
+ cors,
1077
+ compress,
1078
+ MiddlewareChain,
1079
+ BunServer
1080
+ };