-
Notifications
You must be signed in to change notification settings - Fork 0
/
app-render.middleware.ts
82 lines (69 loc) · 3.1 KB
/
app-render.middleware.ts
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
import fse from 'fs-extra';
import { Writable } from 'node:stream';
import { renderToPipeableStream } from 'react-dom/server';
import { DIST_APP_TEMPLATE } from '@config/environment';
import { type Request as IRequest, type Response as IResponse, type NextFunction as INext } from 'express';
import { type ICreateAppFunction } from '@client/application.types';
import { type ICreateAppStore } from '@client/store/store.types';
const getAppStateStr = (state: object, CSPNonce: string) => {
const stringifiedAppState = `
<script type="text/javascript" nonce="${CSPNonce}">
// WARNING: See the following for security issues around embedding JSON in HTML:
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(state).replace(/</g, '\\u003c')}
</script>
`;
return stringifiedAppState;
};
// Get rendering template
const getProcessedTemplateParts = async (appState: object, CSPNonce: string) => {
const templateSource = await fse.readFile(DIST_APP_TEMPLATE, 'utf-8');
const stringifiedAppState = getAppStateStr(appState, CSPNonce);
const processedTemplate = templateSource
.replace('SSR_TEMPLATE_SCRIPT_NONCE', CSPNonce)
.replace('<!--SSR_TEMPLATE_APP_STATE-->', stringifiedAppState);
// [0] - HTML response part before rendered App, [1] - HTML response part after rendered App
return processedTemplate.split('<!--SSR_TEMPLATE_APP-->');
};
const createRenderMiddleware =
(options: { createApp: ICreateAppFunction; createAppStore: ICreateAppStore; CSPNonce: string }) =>
async (req: IRequest, res: IResponse, next: INext) => {
const { createApp, createAppStore, CSPNonce } = options;
const requestPath = req.path || req.url;
const { services } = res.locals;
const store = await createAppStore({ isServer: true, services });
const helmetContext: { helmet?: { htmlAttributes: object } } = {};
const app = createApp({
isServer: true,
store,
path: requestPath,
services,
helmetContext,
});
const [templateBefore, templateAfter] = await getProcessedTemplateParts(store.getState(), CSPNonce);
const writableStream = new Writable({
write(chunk, _encoding, cb) {
res.write(chunk, cb);
},
final() {
res.end(templateAfter);
},
});
const { pipe } = renderToPipeableStream(app, {
onShellReady() {
res.setHeader('Content-type', 'text/html');
// Helmet Context is only populated after rendered App (Shell) ready
const resultTemplateBefore = templateBefore.replace(
/<html(.*)>/,
`<html ${helmetContext?.helmet?.htmlAttributes.toString()}>`
);
res.write(resultTemplateBefore);
pipe(writableStream);
},
onError(x) {
console.error(x);
},
});
next();
};
export default createRenderMiddleware;