UNPKG

10.4 kBTypeScriptView Raw
1// this must be imported to allow async-functions within an AWS lambda environment
2// see: https://github.com/babel/babel/issues/5085
3import "@babel/polyfill";
4
5import React, { ReactNode} from "react";
6import ReactDOMServer from "react-dom/server";
7import express from "express";
8import serverless from "serverless-http";
9
10// make the styled-components work with server-side rendering
11import { ServerStyleSheet } from 'styled-components'; // <-- importing ServerStyleSheet
12import { matchPath } from 'react-router';
13import helmet from 'react-helmet';
14import {createServerApp} from "./routed-app";
15
16import Types from '../src/types';
17import { extractObject, INFRASTRUCTURE_MODES, loadConfigurationFromModule } from '../src/libs/loader';
18
19//import { getClientFilename } from '../types/app-config';
20export const getClientFilename = (name: string): string => {
21 return name+".bundle.js";
22}
23
24//import {loadIsoConfigFromComponent, applyCustomComponents} from "../isolib";
25//import { applyAppClientModules } from '../types/client-app-config';
26
27const createServer = (assetsDir, resolvedAssetsPath, isomorphicId) => {
28
29 // express is the web-framework that lets us configure the endpoints
30 const app = express();
31
32
33 // in production, API-Gateway proxy redirects the request to S3
34 // serve static files - the async components of the server - only used of localhost
35 app.use('/'+assetsDir, express.static(resolvedAssetsPath));
36
37
38 // load the IsomorphicComponent
39 // we must load it directly from the module here, to enable the aliad of the config_file_path
40 const isoConfig = loadConfigurationFromModule(require('__CONFIG_FILE_PATH__'), INFRASTRUCTURE_MODES.RUNTIME);
41
42
43 // let's extract it from the root configuration
44 const isoApp = extractObject(
45 isoConfig,
46 Types.INFRASTRUCTURE_TYPE_CONFIGURATION,
47 isomorphicId
48 )
49 // connect the middlewares
50 isoApp.middlewares.map(mw => app.use(mw.callback));
51
52
53 // split the clientApps here and define a function for each of the clientApps, with the right middleware
54 isoApp.webApps
55 //.filter(clientApp => clientApp.middlewares !== undefined)
56 .map(clientApp => {
57
58 const serveMiddleware = (req, res, next) => serve(req, res, next, clientApp, assetsDir);
59 const routes = clientApp.routes.filter(route => route.middlewares !== undefined && route.middlewares.length > 0);
60
61 if (clientApp.method.toUpperCase() == "GET") {
62
63 app.get(clientApp.path, ...clientApp.middlewares.map(mw => mw.callback));
64 routes.forEach(route => app.get(route.path, ...route.middlewares.map(mw => mw.callback)));
65 app.get(clientApp.path, serveMiddleware);
66
67 } else if (clientApp.method.toUpperCase() == "POST") {
68
69 app.post(clientApp.path, ...clientApp.middlewares.map(mw => mw.callback));
70 routes.forEach(route => app.post(route.path, ...route.middlewares.map(mw => mw.callback)));
71 app.post(clientApp.path, serveMiddleware);
72
73 } else if (clientApp.method.toUpperCase() == "PUT") {
74
75 app.put(clientApp.path, ...clientApp.middlewares.map(mw => mw.callback));
76 routes.forEach(route => app.put(route.path, ...route.middlewares.map(mw => mw.callback)));
77 app.put(clientApp.path, serveMiddleware);
78
79 } else if (clientApp.method.toUpperCase() == "DELETE") {
80
81 app.delete(clientApp.path, ...clientApp.middlewares.map(mw => mw.callback));
82 routes.forEach(route => app.delete(route.path, ...route.middlewares.map(mw => mw.callback)));
83 app.delete(clientApp.path, serveMiddleware);
84
85 }
86
87 return clientApp;
88 });
89
90
91 return app;
92};
93
94
95async function serve (req, res, next, clientApp, assetsDir) {
96
97 //TODO use try catch depending on the environment
98 //try {
99
100
101 //context is used by react router, empty by default
102 let context: any = {};
103
104
105 const basename = getBasename();
106
107 // creating the stylesheet
108 const sheet = new ServerStyleSheet();
109
110
111 const parsedUrl = req.url.indexOf("?") >= 0 ? req.url.substring(0, req.url.indexOf("?")) : req.url;
112 console.log("parsedUrl: ", parsedUrl);
113
114
115 ////////// TODO refactor
116 var foundPath = undefined;
117
118
119 // match request url to our React Router paths and grab the path-params
120 let matchResult = clientApp.routes.find(
121 ({ path, exact }) => {
122 foundPath = matchPath(parsedUrl,
123 {
124 path,
125 exact,
126 strict: false
127 }
128 )
129 return foundPath
130 }) || {};
131 let { path } = matchResult;
132
133 console.log("found: ", foundPath);
134 console.log("params: ", foundPath ? foundPath.params : "---");
135
136 const routePath = foundPath ? (
137 foundPath.path.indexOf("/:") > 0 ?
138 foundPath.path.substring(0, foundPath.path.indexOf("/:")) :
139 foundPath.path
140 ) : "";
141
142 console.log("routePath: ", routePath);
143 ////////// END OF REFACTORING required
144
145 //console.log("app data layer: ", clientApp.dataLayer);
146
147 const connectWithDataLayer = clientApp.dataLayer !== undefined ?
148 clientApp.dataLayer.type({infrastructureMode: "component"}).connectWithDataLayer :
149 async function (app) {
150 console.log("default dummy data layer")
151 return {connectedApp: app, getState: () => ""};
152 };
153
154 // create the app and connect it with the DataAbstractionLayer
155 await connectWithDataLayer(
156 createServerApp(
157 clientApp.routes,
158 clientApp.redirects,
159 basename,
160 req.url,
161 context, req)
162 ).then(({connectedApp, getState}) => {
163
164 console.log("resolved...")
165
166 // collect the styles from the connected app
167 const htmlData = ReactDOMServer.renderToString(sheet.collectStyles(connectedApp));
168
169 // getting all the tags from the sheet
170 const styles = sheet.getStyleTags();
171
172 //render helmet data aka meta data in <head></head>
173 const helmetData = helmet.renderStatic();
174
175 // render a page with the state and return it in the response
176 res.status(200).send(
177 renderHtmlPage(htmlData, styles, getState(), helmetData, basename, routePath, clientApp, assetsDir)
178 ).end();
179 });
180
181
182}
183
184
185/**
186 *
187 * This functions puts together the whole Html
188 *
189 * - style as collected from the styled-components
190 * - head-meta data: from helmet
191 * - data state: from the DAL
192 * - basename: to let the client know
193 *
194 * The html loads the script from the path where we find the assets. This is part of the config, thus load it
195 * using `require('../config').pathToAssets(process.env.STAGE_PATH)`
196 *
197 * The script loading the app.bundle.js uses the window location in order to find out whether there is a slash
198 *
199 * //TODO the app.bundle.js depends on the name "app". Paramterize this!
200 *
201 * //TODO: apply the same base style as the client does
202 *
203 * when we are in a sub-route, e.g. when we have path parameters, we need to add ../ to the path to the assets
204 *
205 * Routing to the Assets
206 *
207 * entered url | basename==/ | basename==/dev
208 * ---------------------------------------------
209 * (none) / /dev
210 * / / /dev
211 * (dev)/ / /
212 * (dev)/route / /
213 * (dev)/route/ ../ ../
214 * (dev)/route/:var ../ ../
215 * TODO what happens with more than one path parameter?
216 *
217 *
218 * @param host the host of the request
219 * @param html
220 * @param styles
221 * @param preloadedState the state in form of a script
222 * @param helmet
223 */
224function renderHtmlPage(html, styles, preloadedState, helmet, basename, routePath, clientApp, assetsDir) {
225 //<link rel="icon" href="/assets/favicon.ico" type="image/ico" />
226 console.log(preloadedState);
227 const path = require('path');
228
229 const calcBase = () => {
230 return path.join(basename, routePath);
231 }
232
233
234
235 //For each"/" in the entered path after the basename, we need to add "../" to the assets-path
236 //when there is a basename, it must be added
237
238 console.log("calcBase: ", calcBase());
239
240 return `<!doctype html>
241 <html>
242 <head>
243 <meta charset="utf-8" />
244 ${helmet.title.toString()}
245 ${helmet.meta.toString()}
246 ${helmet.link.toString()}
247 ${styles}
248 <style>
249 body {
250 display: block;
251 margin: 0px;
252 }
253 </style>
254 </head>
255 <body>
256 <div id="root">${html.trim()}</div>
257 <script>
258 ${preloadedState}
259 window.__BASENAME__ = "${basename}";
260 </script>
261 <script>
262 var loadscript = document.createElement('script');
263 function getPath() {
264 console.log( window.location.pathname);
265 const basePath = ${basename !== "/" ? "window.location.pathname.startsWith(\""+basename+"\") ? \"\": \"/\" " : "\"\"" };
266 const routePath= "${routePath !== "/" ? routePath : ""}";
267 const pre = window.location.pathname.startsWith(basePath+routePath+"/") ? ".." : "";
268 return pre+"${path.join(basename, assetsDir, getClientFilename(clientApp.id))}";
269
270 }
271
272 loadscript.setAttribute('src',getPath());
273 document.body.appendChild(loadscript);
274 </script>
275
276 </body>
277 </html>`
278}
279
280const getBasename = () => {
281 return process.env.STAGE_PATH !== undefined && process.env.STAGE_PATH !== "undefined" ?
282 "/"+process.env.STAGE_PATH : "/";
283};
284
285// we're exporting the handler-function as default, must match the sls-config!
286//export default (assetsDir, resolvedAssetsPath) => serverless(createServer(assetsDir, resolvedAssetsPath));
287
288/*
289const serverIndexPath = path.join(serverPath, "index.js");
290fs.writeFileSync(serverIndexPath, `const lib = require ('./server');
291const server = lib.default('${ssrConfig.assetsPath}', '${resolveAssetsPath(ssrConfig)}');
292exports.default = server;*/
293
294// these variables are replaced during compilation
295export default serverless(createServer(__ASSETS_PATH__, __RESOLVED_ASSETS_PATH__, __ISOMORPHIC_ID__));
\No newline at end of file