-
-
Notifications
You must be signed in to change notification settings - Fork 31
/
Copy pathserver.tsx
121 lines (99 loc) · 3.28 KB
/
server.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import type { Request, Response } from "express";
import crypto from "crypto";
import type { Transform } from "stream";
import { Readable } from "stream";
import type { ComponentType } from "react";
import React from "react";
import type { PipeableStream } from "react-dom/server";
import { renderToPipeableStream } from "react-dom/server";
export const path = process.env.REACT_ESI_PATH || "/_fragment";
const secret =
process.env.REACT_ESI_SECRET || crypto.randomBytes(64).toString("hex");
/**
* Signs the ESI URL with a secret key using the HMAC-SHA256 algorithm.
*/
function sign(url: URL) {
const hmac = crypto.createHmac("sha256", secret);
hmac.update(url.pathname + url.search);
return hmac.digest("hex");
}
interface IEsiAttrs {
src?: string;
alt?: string;
onerror?: string;
}
interface IEsiProps {
attrs?: IEsiAttrs;
}
/**
* Creates the <esi:include> tag.
*/
export const createIncludeElement = (
fragmentID: string,
props: object,
esi: IEsiProps
) => {
const esiAt = esi.attrs || {};
const url = new URL(path, "http://example.com");
url.searchParams.append("fragment", fragmentID);
url.searchParams.append("props", JSON.stringify(props));
url.searchParams.append("sign", sign(url));
esiAt.src = url.pathname + url.search;
return React.createElement("esi:include", esiAt);
};
interface IServeFragmentOptions {
pipeStream?: (stream: PipeableStream) => InstanceType<typeof Transform>;
}
type Resolver<
TProps =
| Record<string, unknown>
| Promise<unknown>
| Promise<Record<string, unknown>>,
> = (
fragmentID: string,
props: object,
req: Request,
res: Response
) => ComponentType<TProps>;
/**
* Checks the signature, renders the given fragment as HTML
* and injects the initial props in a <script> tag.
*/
export async function serveFragment<TProps>(
req: Request,
res: Response,
resolve: Resolver<TProps>,
options: IServeFragmentOptions = {}
) {
const url = new URL(req.url, "http://example.com");
const expectedSign = url.searchParams.get("sign");
url.searchParams.delete("sign");
if (sign(url) !== expectedSign) {
res.status(400);
res.send("Bad signature");
return;
}
const rawProps = url.searchParams.get("props");
const props = rawProps ? JSON.parse(rawProps) : {};
const fragmentID = url.searchParams.get("fragment") || "";
const Component = resolve(fragmentID, props, req, res);
const { ...baseChildProps } = props;
const childProps =
"getInitialProps" in Component &&
typeof Component.getInitialProps === "function"
? await Component.getInitialProps({
props: baseChildProps,
req,
res,
})
: baseChildProps;
// Inject the initial props
const encodedProps = JSON.stringify(childProps).replace(/</g, "\\u003c");
// Remove the <script> class from the DOM to prevent breaking the React reconciliation algorithm
const script = `<script>window.__REACT_ESI__ = window.__REACT_ESI__ || {}; window.__REACT_ESI__['${fragmentID}'] = ${encodedProps};document.currentScript.remove();</script>`;
const scriptStream = Readable.from(script);
scriptStream.pipe(res, { end: false });
const stream = renderToPipeableStream(<Component {...childProps} />);
const lastStream = options.pipeStream ? options.pipeStream(stream) : stream;
lastStream.pipe(res);
}