UNPKG

23.4 kBTypeScriptView Raw
1declare var __ASSETS_PATH__: any;
2declare var __RESOLVED_ASSETS_PATH__: any;
3declare var __ISOMORPHIC_ID__: any;
4declare var __DATALAYER_ID__: any;
5declare var __ISOFFLINE__: any;
6
7// this must be imported to allow async-functions within an AWS lambda environment
8// see: https://github.com/babel/babel/issues/5085
9import "@babel/polyfill";
10
11import React, { ReactNode} from "react";
12import ReactDOMServer from "react-dom/server";
13import express from "express";
14import serverless from "serverless-http";
15import ConnectSequence from 'connect-sequence';
16
17// make the styled-components work with server-side rendering
18import { ServerStyleSheet } from 'styled-components'; // <-- importing ServerStyleSheet
19import { matchPath } from 'react-router';
20import helmet from 'react-helmet';
21import {createServerApp} from "./routed-app";
22import {getBasename} from '../libs/iso-libs';
23import {serviceAttachDataLayer} from "./attach-data-layer";
24import { serviceAttachStorage } from '../components/attach-storage';
25import { listFiles } from '../storage/storage-libs';
26import { findComponentRecursively } from '../libs';
27import {isStorage} from "../storage/storage-component";
28import { serviceAttachService } from '../components/attach-service';
29import { callService } from './service-libs';
30
31import Types from '../types';
32import { extractObject, INFRASTRUCTURE_MODES, loadConfigurationFromModule } from '../libs/loader';
33import { connectWithDataLayer } from './datalayer-integration';
34
35import {
36 graphql,
37 GraphQLSchema,
38 GraphQLObjectType,
39 GraphQLString,
40 GraphQLNonNull
41} from 'graphql';
42
43import { getClientFilename } from '../libs/server-libs';
44
45//import {loadIsoConfigFromComponent, applyCustomComponents} from "../isolib";
46//import { applyAppClientModules } from '../types/client-app-config';
47
48const createServer = (assetsDir, resolvedAssetsPath, isomorphicId, isOffline) => {
49
50
51 // express is the web-framework that lets us configure the endpoints
52 const app = express();
53
54 // in production, API-Gateway proxy redirects the request to S3
55 // serve static files - the async components of the server - only used of localhost
56 app.use('/'+assetsDir, express.static(resolvedAssetsPath));
57
58
59 // load the IsomorphicComponent
60 // we must load it directly from the module here, to enable the aliad of the config_file_path
61 const isoConfig = loadConfigurationFromModule(require('__CONFIG_FILE_PATH__'), INFRASTRUCTURE_MODES.RUNTIME);
62
63
64 // let's extract it from the root configuration
65 const isoApp = extractObject(
66 isoConfig,
67 Types.INFRASTRUCTURE_TYPE_CONFIGURATION,
68 isomorphicId
69 )
70 // connect the middlewares
71 isoApp.middlewares.map(mw => app.use(mw.callback));
72
73
74 // let's extract it from the root configuration
75 const dataLayer = extractObject(
76 isoConfig,
77 Types.INFRASTRUCTURE_TYPE_COMPONENT,
78 __DATALAYER_ID__
79 );
80
81 if (dataLayer) {
82
83 //console.log ("Datalayer Active: ", dataLayer.id)
84
85 if (isOffline) {
86 //console.log("setOffline!")
87 dataLayer.setOffline(true);
88
89
90 } else {
91 //console.log("NOT offline!")
92
93 const cors = require('cors');
94
95 const corsOptions = {
96 origin(origin, callback) {
97 callback(null, true);
98 },
99 credentials: true
100 };
101 app.use(cors(corsOptions));
102
103 // TODO only allow the domains of the app (S3, APIs)
104 var allowCrossDomain = function(req, res, next) {
105 res.header('Access-Control-Allow-Origin', '*');
106 res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
107 //res.header('Access-Control-Allow-Headers', 'Content-Type,token');
108 next();
109 }
110 app.use(allowCrossDomain);
111
112 }
113
114 app.use('/query', async (req, res, next) => {
115 //console.log("query-endpoint / offline: ", isOffline);
116 const parsedBody = JSON.parse(req.body);
117 //console.log(parsedBody)
118
119 await graphql(dataLayer.getSchema(false), parsedBody.query).then(
120 result_type => {
121 const entryQueryName = Object.keys(result_type.data)[0];
122
123 // when the query resolves, we get back
124 //console.log("pre-resolve | found entry: ", entryQueryName)
125
126 new ConnectSequence(req, res, next)
127 .append(...dataLayer.entries.filter(entry => entry.providesQuery(entryQueryName)).map(entry=> entry.middleware.callback))
128 .append(async (req, res, next) => {
129
130 //console.log("DL-mw: req: ");
131 //const parsedBody = JSON.parse(req.body);
132 //console.log("parsedBody: ", parsedBody);
133
134 // now we let the schema resolve with data
135 await graphql(dataLayer.getSchema(true), parsedBody.query).then(
136 result => res.status(200).set({
137 "Access-Control-Allow-Origin" : "*", // Required for CORS support to work
138 "Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS
139 }).send(JSON.stringify(result)),
140 err => res.set({
141 "Access-Control-Allow-Origin" : "*", // Required for CORS support to work
142 "Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS
143 }).status(500).send(err)
144 );
145 })
146 .run();
147 },
148 err => res.set({
149 "Access-Control-Allow-Origin" : "*", // Required for CORS support to work
150 "Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS
151 }).status(500).send(err)
152 );
153 });
154
155 };
156
157 // let's extract it from the root configuration
158 const storages = findComponentRecursively(isoConfig, c => isStorage(c));
159
160 const reqListFiles = (
161 storageId: string,
162 prefix: string,
163 listMode: string,
164 data: any,
165 onComplete: (data: any) => void,
166 onError: (err: string) => void
167 ) => {
168
169 //console.log("listFiles function: ", storageId);
170 //console.log("found storages: ", storages);
171
172 const storage = storages.find(storage => storage.id == storageId);
173 if (storage) {
174 listFiles(storageId, prefix, listMode, data, onComplete, onError, storage, isOffline);
175 } else {
176 onError("could not find storage with id "+ storageId);
177 }
178 };
179
180 const reqCallService = (
181 id: string,
182 args: any,
183 onResult: (result: any) => void,
184 onError: (error: any) => void,
185 config: any,
186 isOffline: Boolean = false
187 ) => callService(id, args,onResult,onError,isoConfig,isOffline);
188
189
190 // flattens the callbacks
191 const unpackMiddlewares = (middlewares) => {
192 // always returns the list of callbacks
193 const cbList = (mw) => Array.isArray(mw.callback) ? mw.callback : [mw.callback];
194 return middlewares.reduce(
195 (res,mw) => res.concat(...cbList(mw)),
196 // attach the callService-function
197 [serviceAttachService(reqCallService)].concat(
198 // when we have a dataLayer, let's attach it to the request
199 dataLayer ? [serviceAttachDataLayer(dataLayer)] : [],
200
201 //when we have a storage, attach the listFiles-function
202 storages ? [ serviceAttachStorage(reqListFiles)] : []
203 )
204 );
205 };
206
207
208 // split the clientApps here and define a function for each of the clientApps, with the right middleware
209 isoApp.services.map(service => {
210
211
212
213 //console.log("found service: ", service);
214
215 if (service.method.toUpperCase() == "GET") {
216 app.get(service.path, ...unpackMiddlewares(service.middlewares));
217
218 } else if (service.method.toUpperCase() == "POST") {
219 app.post(service.path, ...unpackMiddlewares(service.middlewares));
220
221 } else if (service.method.toUpperCase() == "PUT") {
222 app.put(service.path, ...unpackMiddlewares(service.middlewares));
223
224 } else if (service.method.toUpperCase() == "DELETE") {
225 app.delete(service.path, ...unpackMiddlewares(service.middlewares));
226
227 }
228
229 return service;
230 });
231
232 //console.log("webApps: ",isoApp.webApps.length, " -> ", isoApp.webApps);
233
234 // split the clientApps here and define a function for each of the clientApps, with the right middleware
235 isoApp.webApps
236 //.filter(clientApp => clientApp.middlewares !== undefined)
237 .map(clientApp => {
238
239 const serveMiddleware = (req, res, next) => serve(req, res, next, clientApp, assetsDir, isoConfig, isOffline);
240 const routes = clientApp.routes.filter(route => route.middlewares !== undefined && route.middlewares.length > 0);
241
242 if (clientApp.method.toUpperCase() == "GET") {
243
244 app.get(clientApp.path, ...unpackMiddlewares(clientApp.middlewares));
245 routes.forEach(route => app.get(route.path, ...unpackMiddlewares(route.middlewares)));
246 app.get(clientApp.path, serveMiddleware);
247
248 } else if (clientApp.method.toUpperCase() == "POST") {
249
250 app.post(clientApp.path, ...unpackMiddlewares(clientApp.middlewares));
251 routes.forEach(route => app.post(route.path, ...unpackMiddlewares(route.middlewares)));
252 app.post(clientApp.path, serveMiddleware);
253
254 } else if (clientApp.method.toUpperCase() == "PUT") {
255
256 app.put(clientApp.path, ...unpackMiddlewares(clientApp.middlewares));
257 routes.forEach(route => app.put(route.path, ...unpackMiddlewares(route.middlewares)));
258 app.put(clientApp.path, serveMiddleware);
259
260 } else if (clientApp.method.toUpperCase() == "DELETE") {
261
262 app.delete(clientApp.path, ...unpackMiddlewares(clientApp.middlewares));
263 routes.forEach(route => app.delete(route.path, ...unpackMiddlewares(route.middlewares)));
264 app.delete(clientApp.path, serveMiddleware);
265
266 }
267
268 return clientApp;
269 });
270
271
272 return app;
273};
274
275
276async function serve (req, res, next, clientApp, assetsDir, isoConfig, isOffline) {
277
278 //console.log("serve - isOffline: ", isOffline)
279
280 //TODO use try catch depending on the environment
281 //try {
282
283
284 //context is used by react router, empty by default
285 let context: any = {};
286
287
288 const basename = getBasename();
289
290 // creating the stylesheet
291 const sheet = new ServerStyleSheet();
292
293 const parsedUrl = req.url.indexOf("?") >= 0 ? req.url.substring(0, req.url.indexOf("?")) : req.url;
294 //console.log("parsedUrl: ", parsedUrl);
295
296
297 ////////// TODO refactor
298 var foundPath = undefined;
299
300
301 // match request url to our React Router paths and grab the path-params
302 let matchResult = clientApp.routes.find(
303 ({ path, exact }) => {
304 foundPath = matchPath(parsedUrl,
305 {
306 path: path,
307 exact: exact,
308 strict: false
309 }
310 )
311 return foundPath
312 }) || {};
313 let { path } = matchResult;
314
315 //console.log("found: ", foundPath);
316 //console.log("server: path params: ", foundPath ? foundPath.params : "---");
317 //console.log("url: ", req.url);
318
319 const routePath = foundPath ? (
320 foundPath.path.indexOf("/:") > 0 ?
321 foundPath.path.substring(0, foundPath.path.indexOf("/:")) :
322 foundPath.path
323 ) : "";
324
325 //console.log("routePath: ", routePath);
326 ////////// END OF REFACTORING required
327
328
329
330
331
332
333
334 //console.log("app data layer id: ", clientApp.dataLayerId);
335
336 const serverState = {};
337 const setServerValue = (id, value, isInitial=false) => {
338 if (!serverState[id] || !isInitial) {
339 serverState[id] = value;
340 //console.log("set >>", id, "<< -> ", value);
341 }
342
343
344 }
345
346 const getIsomorphicState = () => {
347 //console.log("serverState: ", serverState);
348 return `window.__ISOMORPHICSTATE__ = ${JSON.stringify(serverState)}`
349 };
350
351 const fConnectWithDataLayer = clientApp.dataLayerId !== undefined ?
352 connectWithDataLayer(clientApp.dataLayerId, req, isOffline) :
353 async function (app) {
354 //console.log("default dummy data layer")
355 return {connectedApp: app, getState: () => ""};
356 };
357
358 /*const serverApp = await new Promise((resolve, reject) => {
359
360 });*/
361
362 var renderList = [];
363 const addToRenderList = (fRenderSsr, hashValue) => {
364 //console.log("add to Render: ", hashValue);
365 renderList = renderList.concat([{fRenderSsr: fRenderSsr, hashValue: hashValue}]);
366 };
367
368 const completeSSR = (htmlData, getState, renderListResults) => {
369
370 // getting all the tags from the sheet
371 const styles = sheet.getStyleTags();
372
373 //render helmet data aka meta data in <head></head>
374 const helmetData = helmet.renderStatic();
375
376 const fRender = clientApp.renderHtmlPage ? clientApp.renderHtmlPage : renderHtmlPage;
377
378 // render a page with the state and return it in the response
379 res.status(200).send(
380 fRender(htmlData, styles, getState(), getIsomorphicState(), `window.__RENDERLISTSTATE__ = ${JSON.stringify(renderListResults)}`, helmetData, basename, req.url, clientApp, assetsDir)
381 ).end();
382 };
383
384 async function renderApp (apolloState, oldServerState, oldStorageState, htmlData, getApolloState, newStorageState) {
385
386 const tempApolloState = getApolloState();
387 const tempServerState = Object.assign({}, serverState);
388
389 /*console.log("APOLLO STATE BEFORE", apolloState, "\nAPOLLO STATE AFTER ", tempApolloState);
390 console.log("SERVER STATE BEFORE", oldServerState, "\nSERVER STATE AFTER ", serverState);
391 console.log("STORAGE STATE BEFORE", oldStorageState, "\nSTORAGE STATE AFTER ", newStorageState);
392 */
393
394 if (JSON.stringify(apolloState) !== JSON.stringify(tempApolloState) ||
395 JSON.stringify(oldServerState) !== JSON.stringify(serverState) ||
396 (oldStorageState && oldStorageState.length !== newStorageState.length)
397 ) {
398 //console.log("--------- need to render --------");
399
400 // create the app and connect it with the DataAbstractionLayer
401 await fConnectWithDataLayer(
402 createServerApp(
403 clientApp.routes,
404 clientApp.redirects,
405 basename,
406 req.url,
407 context,
408 req,
409 require('infrastructure-components').getAuthCallback(isoConfig, clientApp.authenticationId),
410 setServerValue,
411 addToRenderList,
412 isoConfig,
413 isOffline,
414 newStorageState,
415 serverState
416 )
417 ).then(async ({connectedApp, getState}) => {
418
419 //console.log("renderList before: ", renderList)
420 // collect the styles from the connected appsheet.collectStyles(
421 const newHtmlData = ReactDOMServer.renderToString(connectedApp);
422
423 const addToStorage = await Promise.all(
424 renderList.filter((item, index, arr)=> {
425 const c = arr.map(item=> item.hashValue);
426 return index === c.indexOf(item.hashValue)
427 }).map(({fRenderSsr, hashValue}) => new Promise((resolve, reject) => {
428 return fRenderSsr(
429 ({data, files})=>{
430 //console.log("resolved: ", files);
431 resolve({
432 hashValue: hashValue,
433 data: data,
434 files: files,
435 })
436 }, err=>{
437 //console.log("rejected: ", err);
438 reject(err);
439 }
440 )
441 }))
442 );
443
444 //console.log("renderList after: ", renderList)
445 renderList.length = 0;
446
447 //console.log("addToStorage: ", addToStorage);
448
449 //completeSSR(newHtmlData, getState, storageState);
450 await renderApp (tempApolloState, tempServerState, newStorageState,
451 newHtmlData, getState, newStorageState.concat(addToStorage));
452 });
453
454 } else {
455 //console.log("----- no difference, return ------");
456 completeSSR(htmlData, getApolloState, newStorageState);
457 };
458 };
459
460 await renderApp ({}, serverState, undefined, "", ()=> "", []);
461
462 /*
463 // create the app and connect it with the DataAbstractionLayer
464 await fConnectWithDataLayer(
465 createServerApp(
466 clientApp.routes,
467 clientApp.redirects,
468 basename,
469 req.url,
470 context,
471 req,
472 require('infrastructure-components').getAuthCallback(isoConfig, clientApp.authenticationId),
473 setServerValue,
474 addToRenderList,
475 isoConfig,
476 isOffline,
477 undefined,
478 undefined
479 )
480 ).then(async ({connectedApp, getState}) => {
481
482 //console.log("resolved... renderList: ", renderList);
483
484 // collect the styles from the connected appsheet.collectStyles(
485 const htmlData = ReactDOMServer.renderToString(connectedApp);
486 //console.log(htmlData);
487
488 const renderListResults = await Promise.all(
489 renderList.filter((item, index, arr)=> {
490 const c = arr.map(item=> item.hashValue);
491 return index === c.indexOf(item.hashValue)
492 }).map(({fRenderSsr, hashValue}) => new Promise((resolve, reject) => {
493 return fRenderSsr(
494 (data, files)=>{
495 //console.log("resolved: ", files);
496 resolve({
497 hashValue: hashValue,
498 data: data,
499 files: files,
500 })
501 }, err=>{
502 //console.log("rejected: ", err);
503 reject(err);
504 }
505 )
506 }))
507 );
508
509
510 if (renderListResults && renderListResults.length > 0) {
511 //console.log("need to rerender! got renderListResults: ", renderListResults);
512
513 await renderApp ({}, serverState, renderListResults, htmlData, getState);
514
515 } else {
516 completeSSR(htmlData, getState, renderListResults);
517 }
518
519 });*/
520
521}
522
523
524/**
525 *
526 * This functions puts together the whole Html
527 *
528 * - style as collected from the styled-components
529 * - head-meta data: from helmet
530 * - data state: from the DAL
531 * - basename: to let the client know
532 *
533 * The html loads the script from the path where we find the assets. This is part of the config, thus load it
534 * using `require('../config').pathToAssets(process.env.STAGE_PATH)`
535 *
536 * The script loading the app.bundle.js uses the window location in order to find out whether there is a slash
537 *
538 * //TODO the app.bundle.js depends on the name "app". Paramterize this!
539 *
540 * //TODO: apply the same base style as the client does
541 *
542 * when we are in a sub-route, e.g. when we have path parameters, we need to add ../ to the path to the assets
543 *
544 * Routing to the Assets
545 *
546 * entered url | basename==/ | basename==/dev
547 * ---------------------------------------------
548 * (none) / /dev
549 * / / /dev
550 * (dev)/ / /
551 * (dev)/route / /
552 * (dev)/route/ ../ ../
553 * (dev)/route/:var ../ ../
554 * TODO what happens with more than one path parameter?
555 *
556 *
557 * @param host the host of the request
558 * @param html
559 * @param styles
560 * @param preloadedState the state in form of a script
561 * @param helmet
562 */
563function renderHtmlPage(html, styles, preloadedState, isomorphicState, renderListResults, helmet, basename, routePath, clientApp, assetsDir) {
564 //<link rel="icon" href="/assets/favicon.ico" type="image/ico" />
565 //console.log(preloadedState);
566 const path = require('path');
567
568 const calcBase = () => {
569 return path.join(basename, routePath);
570 }
571
572
573
574 //For each"/" in the entered path after the basename, we need to add "../" to the assets-path
575 //when there is a basename, it must be added
576
577 //console.log("calcBase: ", calcBase());
578
579 return `<!doctype html>
580 <html>
581 <head>
582 <meta charset="utf-8" />
583 ${helmet.title.toString()}
584 ${helmet.meta.toString()}
585 ${helmet.link.toString()}
586 <meta name="viewport" content="width=device-width, initial-scale=1.0">
587 ${styles}
588 <style>
589 body {
590 display: block;
591 margin: 0px;
592 }
593 </style>
594 </head>
595 <body>
596 <div id="root">${html.trim()}</div>
597 <script>
598 ${preloadedState}
599 ${isomorphicState}
600 ${renderListResults}
601 window.__BASENAME__ = "${basename}";
602 </script>
603 <script>
604 var loadscript = document.createElement('script');
605 function getPath() {
606 const basePath = ${basename !== "/" ? "window.location.pathname.startsWith(\""+basename+"\") ? \"\": \"/\" " : "\"\"" };
607 const routePath= "${routePath !== "/" ? routePath : ""}";
608 const pre = window.location.pathname.startsWith(basePath+routePath+"/") ? ".." : "";
609 return pre+"${path.join(basename, assetsDir, getClientFilename(clientApp.id))}";
610
611 }
612
613 loadscript.setAttribute('src',getPath());
614 document.body.appendChild(loadscript);
615 </script>
616
617 </body>
618 </html>`
619}
620
621
622// we're exporting the handler-function as default, must match the sls-config!
623//export default (assetsDir, resolvedAssetsPath) => serverless(createServer(assetsDir, resolvedAssetsPath));
624
625/*
626const serverIndexPath = path.join(serverPath, "index.js");
627fs.writeFileSync(serverIndexPath, `const lib = require ('./server');
628const server = lib.default('${ssrConfig.assetsPath}', '${resolveAssetsPath(ssrConfig)}');
629exports.default = server;*/
630
631// these variables are replaced during compilation
632export default serverless(createServer(__ASSETS_PATH__, __RESOLVED_ASSETS_PATH__, __ISOMORPHIC_ID__, __ISOFFLINE__));
\No newline at end of file