@ereo/server 0.1.23 → 0.1.25
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/bun-server.d.ts +30 -0
- package/dist/bun-server.d.ts.map +1 -1
- package/dist/index.js +296 -75
- package/dist/streaming.d.ts +3 -1
- package/dist/streaming.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/bun-server.d.ts
CHANGED
|
@@ -95,6 +95,14 @@ export declare class BunServer {
|
|
|
95
95
|
* Handle a matched route.
|
|
96
96
|
*/
|
|
97
97
|
private handleRoute;
|
|
98
|
+
/**
|
|
99
|
+
* Execute inline middleware exported from the route module.
|
|
100
|
+
*/
|
|
101
|
+
private executeInlineMiddleware;
|
|
102
|
+
/**
|
|
103
|
+
* Inner route handler (after middleware).
|
|
104
|
+
*/
|
|
105
|
+
private handleRouteInner;
|
|
98
106
|
/**
|
|
99
107
|
* Render a full HTML page with the route component and layouts.
|
|
100
108
|
*/
|
|
@@ -102,6 +110,10 @@ export declare class BunServer {
|
|
|
102
110
|
/**
|
|
103
111
|
* Render page using React 18 streaming SSR.
|
|
104
112
|
* Uses renderToReadableStream for Bun environments with native Web Streams API.
|
|
113
|
+
*
|
|
114
|
+
* Bytes flow progressively: shell head → React chunks as they render → tail.
|
|
115
|
+
* The browser can parse the head (CSS, meta) and start rendering immediately
|
|
116
|
+
* while React continues resolving Suspense boundaries on the server.
|
|
105
117
|
*/
|
|
106
118
|
private renderStreamingPage;
|
|
107
119
|
/**
|
|
@@ -111,6 +123,9 @@ export declare class BunServer {
|
|
|
111
123
|
/**
|
|
112
124
|
* Render page directly using streaming when layout provides HTML structure.
|
|
113
125
|
* The layout component is expected to render the full HTML document.
|
|
126
|
+
*
|
|
127
|
+
* React content streams progressively. Hydration scripts are appended
|
|
128
|
+
* after the stream completes (browsers tolerate post-body scripts).
|
|
114
129
|
*/
|
|
115
130
|
private renderStreamingPageDirect;
|
|
116
131
|
/**
|
|
@@ -137,6 +152,21 @@ export declare class BunServer {
|
|
|
137
152
|
* Extract meta tags from meta descriptors (excluding title).
|
|
138
153
|
*/
|
|
139
154
|
private extractMetaTags;
|
|
155
|
+
/**
|
|
156
|
+
* Build matches data for useMatches hook.
|
|
157
|
+
* Returns array of matched routes from outermost layout to page.
|
|
158
|
+
*/
|
|
159
|
+
private buildMatchesData;
|
|
160
|
+
/**
|
|
161
|
+
* Build merged response headers from route and layout headers functions.
|
|
162
|
+
* Cascades from outermost layout → innermost layout → route.
|
|
163
|
+
* Each headers function receives the parent headers from the layout above it.
|
|
164
|
+
*/
|
|
165
|
+
private buildRouteHeaders;
|
|
166
|
+
/**
|
|
167
|
+
* Apply custom route headers to a Response, preserving required headers.
|
|
168
|
+
*/
|
|
169
|
+
private applyRouteHeaders;
|
|
140
170
|
/**
|
|
141
171
|
* Escape HTML special characters.
|
|
142
172
|
*/
|
package/dist/bun-server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun-server.d.ts","sourceRoot":"","sources":["../src/bun-server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,KAAK,EAAmE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"bun-server.d.ts","sourceRoot":"","sources":["../src/bun-server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,KAAK,EAAmE,iBAAiB,EAA0C,MAAM,YAAY,CAAC;AAC7J,OAAO,EAAiC,OAAO,EAAiB,MAAM,YAAY,CAAC;AACnF,OAAO,EAAE,UAAU,EAAwD,MAAM,cAAc,CAAC;AAChG,OAAO,EAIL,IAAI,EACJ,eAAe,EAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAA+C,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAwD9F;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEtD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,wBAAwB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB;IAClB,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,6BAA6B;IAC7B,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7D,wBAAwB;IACxB,SAAS,CAAC,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACzD,kBAAkB;IAClB,GAAG,CAAC,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,wFAAwF;IACxF,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,GAAG,CAAwB;IACnC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,aAAa,CAAiE;IACtF,OAAO,CAAC,OAAO,CAAgB;gBAEnB,OAAO,GAAE,aAAkB;IAoBvC;;OAEG;IACH,OAAO,CAAC,eAAe;IAmBvB;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAI1B;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAOnC;;OAEG;IACH,GAAG,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IACrC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI;IASnD;;OAEG;YACW,aAAa;IA0F3B;;OAEG;YACW,sBAAsB;IA2BpC;;OAEG;YACW,WAAW;IAuBzB;;OAEG;YACW,uBAAuB;IAoBrC;;OAEG;YACW,gBAAgB;IAuJ9B;;OAEG;YACW,UAAU;IAyFxB;;;;;;;OAOG;YACW,mBAAmB;IA2FjC;;OAEG;YACW,gBAAgB;IAmC9B;;;;;;OAMG;YACW,yBAAyB;IA4EvC;;OAEG;YACW,sBAAsB;IAqCpC;;OAEG;YACW,iBAAiB;IAqC/B;;OAEG;IACH,OAAO,CAAC,eAAe;IA+BvB;;OAEG;IACH,OAAO,CAAC,SAAS;IA0BjB;;OAEG;IACH,OAAO,CAAC,YAAY;IASpB;;OAEG;IACH,OAAO,CAAC,eAAe;IAcvB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA8BxB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA4CzB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAqBzB;;OAEG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,WAAW;IA0CnB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAmCvC;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ7B;;OAEG;IACH,SAAS,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI;IAInC;;OAEG;IACH,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,OAAO,CAAA;KAAE;CAOpE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAE/D;AAED;;GAEG;AACH,wBAAsB,KAAK,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,SAAS,CAAC,CAIvE"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var __require = import.meta.require;
|
|
3
3
|
|
|
4
4
|
// src/bun-server.ts
|
|
5
|
-
import { createContext } from "@ereo/core";
|
|
5
|
+
import { createContext, NotFoundError } from "@ereo/core";
|
|
6
6
|
import { matchWithLayouts } from "@ereo/router";
|
|
7
7
|
|
|
8
8
|
// src/middleware.ts
|
|
@@ -422,7 +422,7 @@ function staticMiddleware(options) {
|
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
// src/streaming.ts
|
|
425
|
-
import { serializeLoaderData } from "@ereo/data";
|
|
425
|
+
import { serializeLoaderData, hasDeferredData, resolveAllDeferred } from "@ereo/data";
|
|
426
426
|
function createShell(options) {
|
|
427
427
|
const { shell = {}, scripts = [], styles = [], loaderData } = options;
|
|
428
428
|
const htmlAttrs = Object.entries(shell.htmlAttrs || {}).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
@@ -435,6 +435,11 @@ function createShell(options) {
|
|
|
435
435
|
`);
|
|
436
436
|
const styleLinks = styles.map((href) => `<link rel="stylesheet" href="${href}">`).join(`
|
|
437
437
|
`);
|
|
438
|
+
const linkTags = (shell.links || []).map((link) => {
|
|
439
|
+
const attrs = Object.entries(link).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}="${escapeAttr(String(v))}"`).join(" ");
|
|
440
|
+
return `<link ${attrs}>`;
|
|
441
|
+
}).join(`
|
|
442
|
+
`);
|
|
438
443
|
const scriptTags = scripts.map((src) => `<script type="module" src="${src}"></script>`).join(`
|
|
439
444
|
`);
|
|
440
445
|
const loaderScript = loaderData ? `<script>window.__EREO_DATA__=${serializeLoaderData(loaderData)}</script>` : "";
|
|
@@ -445,6 +450,7 @@ function createShell(options) {
|
|
|
445
450
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
446
451
|
${shell.title ? `<title>${shell.title}</title>` : ""}
|
|
447
452
|
${metaTags}
|
|
453
|
+
${linkTags}
|
|
448
454
|
${styleLinks}
|
|
449
455
|
${shell.head || ""}
|
|
450
456
|
</head>
|
|
@@ -468,6 +474,9 @@ async function renderToStream(element, options) {
|
|
|
468
474
|
context: options.context
|
|
469
475
|
});
|
|
470
476
|
}
|
|
477
|
+
if (hasDeferredData(loaderData)) {
|
|
478
|
+
loaderData = await resolveAllDeferred(loaderData);
|
|
479
|
+
}
|
|
471
480
|
const { head, tail } = createShell({ shell, scripts, styles, loaderData });
|
|
472
481
|
return new Promise((resolve2, reject) => {
|
|
473
482
|
const { PassThrough } = __require("stream");
|
|
@@ -522,6 +531,9 @@ async function renderToString(element, options) {
|
|
|
522
531
|
context: options.context
|
|
523
532
|
});
|
|
524
533
|
}
|
|
534
|
+
if (hasDeferredData(loaderData)) {
|
|
535
|
+
loaderData = await resolveAllDeferred(loaderData);
|
|
536
|
+
}
|
|
525
537
|
const { head, tail } = createShell({ shell, scripts, styles, loaderData });
|
|
526
538
|
const content = reactRenderToString(element);
|
|
527
539
|
const html = head + content + tail;
|
|
@@ -560,8 +572,9 @@ function createSuspenseStream() {
|
|
|
560
572
|
}
|
|
561
573
|
|
|
562
574
|
// src/bun-server.ts
|
|
563
|
-
import { serializeLoaderData as serializeLoaderData2 } from "@ereo/data";
|
|
575
|
+
import { serializeLoaderData as serializeLoaderData2, hasDeferredData as hasDeferredData2, resolveAllDeferred as resolveAllDeferred2 } from "@ereo/data";
|
|
564
576
|
import { createElement } from "react";
|
|
577
|
+
import { OutletProvider } from "@ereo/client";
|
|
565
578
|
async function getStreamingRenderer() {
|
|
566
579
|
try {
|
|
567
580
|
const browserServer = await import("react-dom/server.browser");
|
|
@@ -679,7 +692,7 @@ class BunServer {
|
|
|
679
692
|
if (typeof this.router.loadModule === "function") {
|
|
680
693
|
await this.router.loadModule(match.route);
|
|
681
694
|
}
|
|
682
|
-
return this.handleRoute(request, { ...match, layouts: [] }, context);
|
|
695
|
+
return this.handleRoute(request, { ...match, layouts: match.layouts || [] }, context);
|
|
683
696
|
}
|
|
684
697
|
}
|
|
685
698
|
if (this.app) {
|
|
@@ -713,6 +726,50 @@ class BunServer {
|
|
|
713
726
|
if (!module) {
|
|
714
727
|
return new Response("Route module not loaded", { status: 500 });
|
|
715
728
|
}
|
|
729
|
+
if (module.middleware && module.middleware.length > 0) {
|
|
730
|
+
return this.executeInlineMiddleware(request, context, module.middleware, () => this.handleRouteInner(request, match, context));
|
|
731
|
+
}
|
|
732
|
+
return this.handleRouteInner(request, match, context);
|
|
733
|
+
}
|
|
734
|
+
async executeInlineMiddleware(request, context, middleware, handler) {
|
|
735
|
+
let index = 0;
|
|
736
|
+
const next = async () => {
|
|
737
|
+
if (index >= middleware.length) {
|
|
738
|
+
return handler();
|
|
739
|
+
}
|
|
740
|
+
const mw = middleware[index++];
|
|
741
|
+
return mw(request, context, next);
|
|
742
|
+
};
|
|
743
|
+
return next();
|
|
744
|
+
}
|
|
745
|
+
async handleRouteInner(request, match, context) {
|
|
746
|
+
const module = match.route.module;
|
|
747
|
+
const httpMethod = request.method.toUpperCase();
|
|
748
|
+
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
|
|
749
|
+
if (HTTP_METHODS.includes(httpMethod)) {
|
|
750
|
+
const methodHandler = module[httpMethod];
|
|
751
|
+
if (typeof methodHandler === "function") {
|
|
752
|
+
const result = await methodHandler({ request, params: match.params, context });
|
|
753
|
+
if (result instanceof Response) {
|
|
754
|
+
const routeHeaders3 = this.buildRouteHeaders(match);
|
|
755
|
+
return this.applyRouteHeaders(result, routeHeaders3);
|
|
756
|
+
}
|
|
757
|
+
const jsonResponse = new Response(JSON.stringify(result), {
|
|
758
|
+
headers: { "Content-Type": "application/json" }
|
|
759
|
+
});
|
|
760
|
+
const routeHeaders2 = this.buildRouteHeaders(match);
|
|
761
|
+
return this.applyRouteHeaders(jsonResponse, routeHeaders2);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const layouts = match.layouts || [];
|
|
765
|
+
for (const layout of layouts) {
|
|
766
|
+
if (layout.module?.beforeLoad) {
|
|
767
|
+
await layout.module.beforeLoad({ request, params: match.params, context });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (module.beforeLoad) {
|
|
771
|
+
await module.beforeLoad({ request, params: match.params, context });
|
|
772
|
+
}
|
|
716
773
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
717
774
|
if (module.action) {
|
|
718
775
|
const result = await module.action({
|
|
@@ -721,43 +778,85 @@ class BunServer {
|
|
|
721
778
|
context
|
|
722
779
|
});
|
|
723
780
|
if (result instanceof Response) {
|
|
724
|
-
|
|
781
|
+
const actionHeaders = new Headers(result.headers);
|
|
782
|
+
const routeHeaders3 = this.buildRouteHeaders(match, actionHeaders);
|
|
783
|
+
return this.applyRouteHeaders(result, routeHeaders3);
|
|
725
784
|
}
|
|
726
|
-
|
|
785
|
+
const actionResponse = new Response(JSON.stringify(result), {
|
|
727
786
|
headers: { "Content-Type": "application/json" }
|
|
728
787
|
});
|
|
788
|
+
const routeHeaders2 = this.buildRouteHeaders(match);
|
|
789
|
+
return this.applyRouteHeaders(actionResponse, routeHeaders2);
|
|
729
790
|
}
|
|
730
791
|
return new Response("Method Not Allowed", { status: 405 });
|
|
731
792
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
793
|
+
const loaderArgs = { request, params: match.params, context };
|
|
794
|
+
const loaderPromises = [];
|
|
795
|
+
loaderPromises.push(module.loader ? Promise.resolve(module.loader(loaderArgs)) : Promise.resolve(null));
|
|
796
|
+
for (const layout of layouts) {
|
|
797
|
+
loaderPromises.push(layout.module?.loader ? Promise.resolve(layout.module.loader(loaderArgs)) : Promise.resolve(null));
|
|
798
|
+
}
|
|
799
|
+
const loaderResults = await Promise.all(loaderPromises);
|
|
800
|
+
const loaderData = loaderResults[0];
|
|
801
|
+
if (loaderData instanceof Response) {
|
|
802
|
+
return loaderData;
|
|
803
|
+
}
|
|
804
|
+
const layoutLoaderData = new Map;
|
|
805
|
+
for (let i = 0;i < layouts.length; i++) {
|
|
806
|
+
const layoutData = loaderResults[i + 1];
|
|
807
|
+
if (layoutData instanceof Response) {
|
|
808
|
+
return layoutData;
|
|
809
|
+
}
|
|
810
|
+
if (layoutData !== null) {
|
|
811
|
+
layoutLoaderData.set(layouts[i].id, layoutData);
|
|
741
812
|
}
|
|
742
813
|
}
|
|
814
|
+
const routeHeaders = this.buildRouteHeaders(match);
|
|
743
815
|
if (request.headers.get("Accept")?.includes("application/json")) {
|
|
744
|
-
|
|
816
|
+
const resolvedLoaderData = hasDeferredData2(loaderData) ? await resolveAllDeferred2(loaderData) : loaderData;
|
|
817
|
+
const jsonPayload = {
|
|
818
|
+
data: resolvedLoaderData,
|
|
819
|
+
params: match.params
|
|
820
|
+
};
|
|
821
|
+
if (layoutLoaderData.size > 0) {
|
|
822
|
+
const layoutDataObj = {};
|
|
823
|
+
for (const [id, data] of layoutLoaderData) {
|
|
824
|
+
layoutDataObj[id] = hasDeferredData2(data) ? await resolveAllDeferred2(data) : data;
|
|
825
|
+
}
|
|
826
|
+
jsonPayload.layoutData = layoutDataObj;
|
|
827
|
+
}
|
|
828
|
+
const module2 = match.route.module;
|
|
829
|
+
const routeLinks = module2?.links ? module2.links() : [];
|
|
830
|
+
const layoutLinksList = (match.layouts || []).flatMap((layout) => layout.module?.links ? layout.module.links() : []);
|
|
831
|
+
const allLinks = [...layoutLinksList, ...routeLinks];
|
|
832
|
+
if (allLinks.length > 0) {
|
|
833
|
+
jsonPayload.links = allLinks;
|
|
834
|
+
}
|
|
835
|
+
const matchesData = this.buildMatchesData(match, loaderData, layoutLoaderData);
|
|
836
|
+
jsonPayload.matches = matchesData;
|
|
837
|
+
const jsonResponse = new Response(JSON.stringify(jsonPayload), {
|
|
745
838
|
headers: { "Content-Type": "application/json" }
|
|
746
839
|
});
|
|
840
|
+
return this.applyRouteHeaders(jsonResponse, routeHeaders);
|
|
747
841
|
}
|
|
748
|
-
|
|
842
|
+
const htmlResponse = await this.renderPage(request, match, context, loaderData, layoutLoaderData);
|
|
843
|
+
return this.applyRouteHeaders(htmlResponse, routeHeaders);
|
|
749
844
|
}
|
|
750
|
-
async renderPage(request, match, context, loaderData) {
|
|
845
|
+
async renderPage(request, match, context, loaderData, layoutLoaderData = new Map) {
|
|
751
846
|
const module = match.route.module;
|
|
752
847
|
if (!module?.default) {
|
|
753
848
|
return this.renderMinimalPage(match, loaderData);
|
|
754
849
|
}
|
|
755
850
|
const url = new URL(request.url);
|
|
756
851
|
const metaDescriptors = this.buildMeta(module, loaderData, match.params, url);
|
|
852
|
+
const routeLinks = module.links ? module.links() : [];
|
|
853
|
+
const layoutLinks = (match.layouts || []).flatMap((layout) => layout.module?.links ? layout.module.links() : []);
|
|
854
|
+
const allLinks = [...layoutLinks, ...routeLinks];
|
|
757
855
|
const shell = {
|
|
758
856
|
...this.options.shell,
|
|
759
857
|
title: this.extractTitle(metaDescriptors) || this.options.shell?.title,
|
|
760
|
-
meta: this.extractMetaTags(metaDescriptors)
|
|
858
|
+
meta: this.extractMetaTags(metaDescriptors),
|
|
859
|
+
links: allLinks.length > 0 ? allLinks : undefined
|
|
761
860
|
};
|
|
762
861
|
const PageComponent = module.default;
|
|
763
862
|
let element = createElement(PageComponent, {
|
|
@@ -769,25 +868,27 @@ class BunServer {
|
|
|
769
868
|
const layout = layouts[i];
|
|
770
869
|
if (layout.module?.default) {
|
|
771
870
|
const LayoutComponent = layout.module.default;
|
|
772
|
-
|
|
773
|
-
|
|
871
|
+
const childElement = element;
|
|
872
|
+
element = createElement(OutletProvider, { element: childElement }, createElement(LayoutComponent, {
|
|
873
|
+
loaderData: layoutLoaderData.get(layout.id) ?? null,
|
|
774
874
|
params: match.params,
|
|
775
|
-
children:
|
|
776
|
-
});
|
|
875
|
+
children: childElement
|
|
876
|
+
}));
|
|
777
877
|
}
|
|
778
878
|
}
|
|
879
|
+
const allLoaderData = layoutLoaderData.size > 0 ? { __routeData: loaderData, __layoutData: Object.fromEntries(layoutLoaderData) } : loaderData;
|
|
779
880
|
const hasRootLayout = layouts.length > 0 && layouts[0].module?.default;
|
|
780
881
|
if (hasRootLayout) {
|
|
781
882
|
if (this.options.renderMode === "streaming") {
|
|
782
|
-
return this.renderStreamingPageDirect(element,
|
|
883
|
+
return this.renderStreamingPageDirect(element, allLoaderData);
|
|
783
884
|
} else {
|
|
784
|
-
return this.renderStringPageDirect(element,
|
|
885
|
+
return this.renderStringPageDirect(element, allLoaderData);
|
|
785
886
|
}
|
|
786
887
|
}
|
|
787
888
|
if (this.options.renderMode === "streaming") {
|
|
788
|
-
return this.renderStreamingPage(element, shell,
|
|
889
|
+
return this.renderStreamingPage(element, shell, allLoaderData);
|
|
789
890
|
} else {
|
|
790
|
-
return this.renderStringPage(element, shell,
|
|
891
|
+
return this.renderStringPage(element, shell, allLoaderData);
|
|
791
892
|
}
|
|
792
893
|
}
|
|
793
894
|
async renderStreamingPage(element, shell, loaderData) {
|
|
@@ -796,38 +897,64 @@ class BunServer {
|
|
|
796
897
|
if (!renderToReadableStream) {
|
|
797
898
|
return this.renderStringPage(element, shell, loaderData);
|
|
798
899
|
}
|
|
900
|
+
const hasDeferred = hasDeferredData2(loaderData);
|
|
799
901
|
const scripts = [this.options.clientEntry];
|
|
800
|
-
const { head, tail } = createShell({
|
|
902
|
+
const { head, tail } = createShell({
|
|
903
|
+
shell,
|
|
904
|
+
scripts,
|
|
905
|
+
loaderData: hasDeferred ? null : loaderData
|
|
906
|
+
});
|
|
801
907
|
const encoder = new TextEncoder;
|
|
802
908
|
const headBytes = encoder.encode(head);
|
|
803
|
-
const tailBytes = encoder.encode(tail);
|
|
909
|
+
const tailBytes = hasDeferred ? null : encoder.encode(tail);
|
|
804
910
|
const reactStream = await renderToReadableStream(element, {
|
|
805
911
|
onError(error) {
|
|
806
912
|
console.error("Streaming render error:", error);
|
|
807
913
|
}
|
|
808
914
|
});
|
|
809
|
-
|
|
915
|
+
const clientEntry = this.options.clientEntry;
|
|
810
916
|
const reader = reactStream.getReader();
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
917
|
+
let phase = "head";
|
|
918
|
+
const stream = new ReadableStream({
|
|
919
|
+
async pull(controller) {
|
|
920
|
+
switch (phase) {
|
|
921
|
+
case "head":
|
|
922
|
+
controller.enqueue(headBytes);
|
|
923
|
+
phase = "body";
|
|
924
|
+
break;
|
|
925
|
+
case "body": {
|
|
926
|
+
const { done, value } = await reader.read();
|
|
927
|
+
if (done) {
|
|
928
|
+
if (hasDeferred) {
|
|
929
|
+
const resolved = await resolveAllDeferred2(loaderData);
|
|
930
|
+
const loaderScript = `<script>window.__EREO_DATA__=${serializeLoaderData2(resolved)}</script>`;
|
|
931
|
+
const scriptTag = `<script type="module" src="${clientEntry}"></script>`;
|
|
932
|
+
const resolvedTail = `</div>
|
|
933
|
+
${loaderScript}
|
|
934
|
+
${scriptTag}
|
|
935
|
+
</body>
|
|
936
|
+
</html>`;
|
|
937
|
+
controller.enqueue(encoder.encode(resolvedTail));
|
|
938
|
+
} else {
|
|
939
|
+
controller.enqueue(tailBytes);
|
|
940
|
+
}
|
|
941
|
+
controller.close();
|
|
942
|
+
phase = "done";
|
|
943
|
+
} else {
|
|
944
|
+
controller.enqueue(value);
|
|
945
|
+
}
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
cancel() {
|
|
951
|
+
reader.cancel();
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
return new Response(stream, {
|
|
827
955
|
status: 200,
|
|
828
956
|
headers: {
|
|
829
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
830
|
-
"Content-Length": totalLength.toString()
|
|
957
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
831
958
|
}
|
|
832
959
|
});
|
|
833
960
|
} catch (error) {
|
|
@@ -838,8 +965,9 @@ class BunServer {
|
|
|
838
965
|
async renderStringPage(element, shell, loaderData) {
|
|
839
966
|
try {
|
|
840
967
|
const { renderToString: reactRenderToString } = await import("react-dom/server");
|
|
968
|
+
const resolvedData = hasDeferredData2(loaderData) ? await resolveAllDeferred2(loaderData) : loaderData;
|
|
841
969
|
const scripts = [this.options.clientEntry];
|
|
842
|
-
const { head, tail } = createShell({ shell, scripts, loaderData });
|
|
970
|
+
const { head, tail } = createShell({ shell, scripts, loaderData: resolvedData });
|
|
843
971
|
const content = reactRenderToString(element);
|
|
844
972
|
const html = head + content + tail;
|
|
845
973
|
const encoder = new TextEncoder;
|
|
@@ -867,32 +995,40 @@ class BunServer {
|
|
|
867
995
|
console.error("Streaming render error:", error);
|
|
868
996
|
}
|
|
869
997
|
});
|
|
870
|
-
await reactStream.allReady;
|
|
871
|
-
const reader = reactStream.getReader();
|
|
872
|
-
const chunks = [];
|
|
873
|
-
while (true) {
|
|
874
|
-
const { done, value } = await reader.read();
|
|
875
|
-
if (done)
|
|
876
|
-
break;
|
|
877
|
-
chunks.push(value);
|
|
878
|
-
}
|
|
879
998
|
const encoder = new TextEncoder;
|
|
880
|
-
const
|
|
881
|
-
const
|
|
882
|
-
const injectedScripts = encoder.encode(
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
999
|
+
const hasDeferred = hasDeferredData2(loaderData);
|
|
1000
|
+
const clientEntry = this.options.clientEntry;
|
|
1001
|
+
const injectedScripts = hasDeferred ? null : encoder.encode((loaderData ? `<script>window.__EREO_DATA__=${serializeLoaderData2(loaderData)}</script>` : "") + `<script type="module" src="${clientEntry}"></script>`);
|
|
1002
|
+
const reader = reactStream.getReader();
|
|
1003
|
+
let done = false;
|
|
1004
|
+
const stream = new ReadableStream({
|
|
1005
|
+
async pull(controller) {
|
|
1006
|
+
if (done)
|
|
1007
|
+
return;
|
|
1008
|
+
const result = await reader.read();
|
|
1009
|
+
if (result.done) {
|
|
1010
|
+
if (hasDeferred) {
|
|
1011
|
+
const resolved = await resolveAllDeferred2(loaderData);
|
|
1012
|
+
const loaderScript = resolved ? `<script>window.__EREO_DATA__=${serializeLoaderData2(resolved)}</script>` : "";
|
|
1013
|
+
const clientScript = `<script type="module" src="${clientEntry}"></script>`;
|
|
1014
|
+
controller.enqueue(encoder.encode(loaderScript + clientScript));
|
|
1015
|
+
} else {
|
|
1016
|
+
controller.enqueue(injectedScripts);
|
|
1017
|
+
}
|
|
1018
|
+
controller.close();
|
|
1019
|
+
done = true;
|
|
1020
|
+
} else {
|
|
1021
|
+
controller.enqueue(result.value);
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
cancel() {
|
|
1025
|
+
reader.cancel();
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
return new Response(stream, {
|
|
892
1029
|
status: 200,
|
|
893
1030
|
headers: {
|
|
894
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
895
|
-
"Content-Length": totalLength.toString()
|
|
1031
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
896
1032
|
}
|
|
897
1033
|
});
|
|
898
1034
|
} catch (error) {
|
|
@@ -903,8 +1039,9 @@ class BunServer {
|
|
|
903
1039
|
async renderStringPageDirect(element, loaderData) {
|
|
904
1040
|
try {
|
|
905
1041
|
const { renderToString: reactRenderToString } = await import("react-dom/server");
|
|
1042
|
+
const resolvedData = hasDeferredData2(loaderData) ? await resolveAllDeferred2(loaderData) : loaderData;
|
|
906
1043
|
let html = reactRenderToString(element);
|
|
907
|
-
const loaderScript =
|
|
1044
|
+
const loaderScript = resolvedData ? `<script>window.__EREO_DATA__=${serializeLoaderData2(resolvedData)}</script>` : "";
|
|
908
1045
|
const clientScript = `<script type="module" src="${this.options.clientEntry}"></script>`;
|
|
909
1046
|
html = html.replace("</body>", `${loaderScript}${clientScript}</body>`);
|
|
910
1047
|
const encoder = new TextEncoder;
|
|
@@ -921,9 +1058,10 @@ class BunServer {
|
|
|
921
1058
|
return this.renderErrorPage(error);
|
|
922
1059
|
}
|
|
923
1060
|
}
|
|
924
|
-
renderMinimalPage(match, loaderData) {
|
|
1061
|
+
async renderMinimalPage(match, loaderData) {
|
|
1062
|
+
const resolvedData = hasDeferredData2(loaderData) ? await resolveAllDeferred2(loaderData) : loaderData;
|
|
925
1063
|
const serializedData = serializeLoaderData2({
|
|
926
|
-
loaderData,
|
|
1064
|
+
loaderData: resolvedData,
|
|
927
1065
|
params: match.params
|
|
928
1066
|
});
|
|
929
1067
|
const html = `<!DOCTYPE html>
|
|
@@ -1015,6 +1153,79 @@ class BunServer {
|
|
|
1015
1153
|
}
|
|
1016
1154
|
return tags;
|
|
1017
1155
|
}
|
|
1156
|
+
buildMatchesData(match, loaderData, layoutLoaderData) {
|
|
1157
|
+
const matches = [];
|
|
1158
|
+
for (const layout of match.layouts || []) {
|
|
1159
|
+
matches.push({
|
|
1160
|
+
id: layout.id,
|
|
1161
|
+
pathname: match.pathname,
|
|
1162
|
+
params: match.params,
|
|
1163
|
+
data: layoutLoaderData.get(layout.id) ?? null,
|
|
1164
|
+
handle: layout.module?.handle ?? undefined
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
matches.push({
|
|
1168
|
+
id: match.route.id,
|
|
1169
|
+
pathname: match.pathname,
|
|
1170
|
+
params: match.params,
|
|
1171
|
+
data: loaderData,
|
|
1172
|
+
handle: match.route.module?.handle ?? undefined
|
|
1173
|
+
});
|
|
1174
|
+
return matches;
|
|
1175
|
+
}
|
|
1176
|
+
buildRouteHeaders(match, actionHeaders = new Headers) {
|
|
1177
|
+
const layouts = match.layouts || [];
|
|
1178
|
+
const module = match.route.module;
|
|
1179
|
+
let parentHeaders = new Headers;
|
|
1180
|
+
for (const layout of layouts) {
|
|
1181
|
+
const headersFn = layout.module?.headers;
|
|
1182
|
+
if (headersFn) {
|
|
1183
|
+
try {
|
|
1184
|
+
const result = headersFn({
|
|
1185
|
+
loaderHeaders: new Headers,
|
|
1186
|
+
actionHeaders,
|
|
1187
|
+
parentHeaders
|
|
1188
|
+
});
|
|
1189
|
+
parentHeaders = result instanceof Headers ? result : new Headers(result);
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
console.error(`Error in layout headers function (${layout.id}):`, error);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (module?.headers) {
|
|
1196
|
+
try {
|
|
1197
|
+
const result = module.headers({
|
|
1198
|
+
loaderHeaders: new Headers,
|
|
1199
|
+
actionHeaders,
|
|
1200
|
+
parentHeaders
|
|
1201
|
+
});
|
|
1202
|
+
return result instanceof Headers ? result : new Headers(result);
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
console.error("Error in route headers function:", error);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return parentHeaders;
|
|
1208
|
+
}
|
|
1209
|
+
applyRouteHeaders(response, routeHeaders) {
|
|
1210
|
+
let hasHeaders = false;
|
|
1211
|
+
routeHeaders.forEach(() => {
|
|
1212
|
+
hasHeaders = true;
|
|
1213
|
+
});
|
|
1214
|
+
if (!hasHeaders)
|
|
1215
|
+
return response;
|
|
1216
|
+
const newHeaders = new Headers(response.headers);
|
|
1217
|
+
routeHeaders.forEach((value, key) => {
|
|
1218
|
+
const lower = key.toLowerCase();
|
|
1219
|
+
if (lower === "content-type" || lower === "content-length")
|
|
1220
|
+
return;
|
|
1221
|
+
newHeaders.set(key, value);
|
|
1222
|
+
});
|
|
1223
|
+
return new Response(response.body, {
|
|
1224
|
+
status: response.status,
|
|
1225
|
+
statusText: response.statusText,
|
|
1226
|
+
headers: newHeaders
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1018
1229
|
escapeHtml(str) {
|
|
1019
1230
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1020
1231
|
}
|
|
@@ -1022,6 +1233,16 @@ class BunServer {
|
|
|
1022
1233
|
if (error instanceof Response) {
|
|
1023
1234
|
return error;
|
|
1024
1235
|
}
|
|
1236
|
+
if (error instanceof NotFoundError) {
|
|
1237
|
+
return new Response(JSON.stringify({
|
|
1238
|
+
error: "Not Found",
|
|
1239
|
+
status: 404,
|
|
1240
|
+
data: error.data
|
|
1241
|
+
}), {
|
|
1242
|
+
status: 404,
|
|
1243
|
+
headers: { "Content-Type": "application/json" }
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1025
1246
|
const message = error instanceof Error ? error.message : "Internal Server Error";
|
|
1026
1247
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
1027
1248
|
console.error("Server error:", error);
|
package/dist/streaming.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Server-side rendering with streaming support for React 18+.
|
|
5
5
|
*/
|
|
6
6
|
import type { ReactElement } from 'react';
|
|
7
|
-
import type { RouteMatch, AppContext } from '@ereo/core';
|
|
7
|
+
import type { RouteMatch, AppContext, LinkDescriptor } from '@ereo/core';
|
|
8
8
|
/**
|
|
9
9
|
* Render options.
|
|
10
10
|
*/
|
|
@@ -34,6 +34,8 @@ export interface ShellTemplate {
|
|
|
34
34
|
property?: string;
|
|
35
35
|
content: string;
|
|
36
36
|
}>;
|
|
37
|
+
/** Link descriptors for stylesheets, preloads, etc. */
|
|
38
|
+
links?: LinkDescriptor[];
|
|
37
39
|
/** Head content */
|
|
38
40
|
head?: string;
|
|
39
41
|
/** Body attributes */
|
package/dist/streaming.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EAAS,UAAU,EAAE,UAAU,EAAkB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EAAS,UAAU,EAAE,UAAU,EAAkB,cAAc,EAAE,MAAM,YAAY,CAAC;AAGhG;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,kBAAkB;IAClB,KAAK,EAAE,UAAU,CAAC;IAClB,sBAAsB;IACtB,OAAO,EAAE,UAAU,CAAC;IACpB,qBAAqB;IACrB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,uBAAuB;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wBAAwB;IACxB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,qBAAqB;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB;IAChB,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,uDAAuD;IACvD,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,sBAAsB;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IAC1C,uBAAuB;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACnC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA8DjC;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,YAAY,CAAC,CAwEvB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,YAAY,CAAC,CAgCvB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,QAAQ,CAK7D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI;IACtC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAmBA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ereo/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Ereo Team",
|
|
6
6
|
"homepage": "https://ereo.dev",
|
|
@@ -25,16 +25,17 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "bun build ./src/index.ts --outdir ./dist --target bun --external @ereo/core --external @ereo/router --external @ereo/data --external react --external react-dom && bun run build:types",
|
|
28
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun --external @ereo/core --external @ereo/client --external @ereo/router --external @ereo/data --external react --external react-dom && bun run build:types",
|
|
29
29
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
|
30
30
|
"dev": "bun build ./src/index.ts --outdir ./dist --target bun --watch",
|
|
31
31
|
"test": "bun test",
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ereo/core": "^0.1.
|
|
36
|
-
"@ereo/
|
|
37
|
-
"@ereo/
|
|
35
|
+
"@ereo/core": "^0.1.25",
|
|
36
|
+
"@ereo/client": "^0.1.25",
|
|
37
|
+
"@ereo/router": "^0.1.25",
|
|
38
|
+
"@ereo/data": "^0.1.25"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/bun": "^1.1.0",
|