UNPKG

infrastructure-components

Version:

Infrastructure-Components configure the infrastructure of your React-App as part of your React-Components.

466 lines (454 loc) • 24.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); // this must be imported to allow async-functions within an AWS lambda environment // see: https://github.com/babel/babel/issues/5085 require("@babel/polyfill"); const server_1 = __importDefault(require("react-dom/server")); const express_1 = __importDefault(require("express")); const serverless_http_1 = __importDefault(require("serverless-http")); const connect_sequence_1 = __importDefault(require("connect-sequence")); // make the styled-components work with server-side rendering const styled_components_1 = require("styled-components"); // <-- importing ServerStyleSheet const react_router_1 = require("react-router"); const react_helmet_1 = __importDefault(require("react-helmet")); const routed_app_1 = require("./routed-app"); const iso_libs_1 = require("../libs/iso-libs"); const attach_data_layer_1 = require("./attach-data-layer"); const attach_storage_1 = require("../components/attach-storage"); const storage_libs_1 = require("../storage/storage-libs"); const libs_1 = require("../libs"); const storage_component_1 = require("../storage/storage-component"); const attach_service_1 = require("../components/attach-service"); const service_libs_1 = require("./service-libs"); const types_1 = __importDefault(require("../types")); const loader_1 = require("../libs/loader"); const datalayer_integration_1 = require("./datalayer-integration"); const graphql_1 = require("graphql"); const server_libs_1 = require("../libs/server-libs"); //import {loadIsoConfigFromComponent, applyCustomComponents} from "../isolib"; //import { applyAppClientModules } from '../types/client-app-config'; const createServer = (assetsDir, resolvedAssetsPath, isomorphicId, isOffline) => { // express is the web-framework that lets us configure the endpoints const app = express_1.default(); // in production, API-Gateway proxy redirects the request to S3 // serve static files - the async components of the server - only used of localhost app.use('/' + assetsDir, express_1.default.static(resolvedAssetsPath)); // load the IsomorphicComponent // we must load it directly from the module here, to enable the aliad of the config_file_path const isoConfig = loader_1.loadConfigurationFromModule(require('__CONFIG_FILE_PATH__'), loader_1.INFRASTRUCTURE_MODES.RUNTIME); // let's extract it from the root configuration const isoApp = loader_1.extractObject(isoConfig, types_1.default.INFRASTRUCTURE_TYPE_CONFIGURATION, isomorphicId); // connect the middlewares isoApp.middlewares.map(mw => app.use(mw.callback)); // let's extract it from the root configuration const dataLayer = loader_1.extractObject(isoConfig, types_1.default.INFRASTRUCTURE_TYPE_COMPONENT, __DATALAYER_ID__); if (dataLayer) { //console.log ("Datalayer Active: ", dataLayer.id) if (isOffline) { //console.log("setOffline!") dataLayer.setOffline(true); } else { //console.log("NOT offline!") const cors = require('cors'); const corsOptions = { origin(origin, callback) { callback(null, true); }, credentials: true }; app.use(cors(corsOptions)); // TODO only allow the domains of the app (S3, APIs) var allowCrossDomain = function (req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); //res.header('Access-Control-Allow-Headers', 'Content-Type,token'); next(); }; app.use(allowCrossDomain); } app.use('/query', (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { //console.log("query-endpoint / offline: ", isOffline); const parsedBody = JSON.parse(req.body); //console.log(parsedBody) yield graphql_1.graphql(dataLayer.getSchema(false), parsedBody.query).then(result_type => { const entryQueryName = Object.keys(result_type.data)[0]; // when the query resolves, we get back //console.log("pre-resolve | found entry: ", entryQueryName) new connect_sequence_1.default(req, res, next) .append(...dataLayer.entries.filter(entry => entry.providesQuery(entryQueryName)).map(entry => entry.middleware.callback)) .append((req, res, next) => __awaiter(void 0, void 0, void 0, function* () { //console.log("DL-mw: req: "); //const parsedBody = JSON.parse(req.body); //console.log("parsedBody: ", parsedBody); // now we let the schema resolve with data yield graphql_1.graphql(dataLayer.getSchema(true), parsedBody.query).then(result => res.status(200).set({ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS }).send(JSON.stringify(result)), err => res.set({ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS }).status(500).send(err)); })) .run(); }, err => res.set({ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS }).status(500).send(err)); })); } ; // let's extract it from the root configuration const storages = libs_1.findComponentRecursively(isoConfig, c => storage_component_1.isStorage(c)); const reqListFiles = (storageId, prefix, listMode, data, onComplete, onError) => { //console.log("listFiles function: ", storageId); //console.log("found storages: ", storages); const storage = storages.find(storage => storage.id == storageId); if (storage) { storage_libs_1.listFiles(storageId, prefix, listMode, data, onComplete, onError, storage, isOffline); } else { onError("could not find storage with id " + storageId); } }; const reqCallService = (id, args, onResult, onError, config, isOffline = false) => service_libs_1.callService(id, args, onResult, onError, isoConfig, isOffline); // flattens the callbacks const unpackMiddlewares = (middlewares) => { // always returns the list of callbacks const cbList = (mw) => Array.isArray(mw.callback) ? mw.callback : [mw.callback]; return middlewares.reduce((res, mw) => res.concat(...cbList(mw)), // attach the callService-function [attach_service_1.serviceAttachService(reqCallService)].concat( // when we have a dataLayer, let's attach it to the request dataLayer ? [attach_data_layer_1.serviceAttachDataLayer(dataLayer)] : [], //when we have a storage, attach the listFiles-function storages ? [attach_storage_1.serviceAttachStorage(reqListFiles)] : [])); }; // split the clientApps here and define a function for each of the clientApps, with the right middleware isoApp.services.map(service => { //console.log("found service: ", service); if (service.method.toUpperCase() == "GET") { app.get(service.path, ...unpackMiddlewares(service.middlewares)); } else if (service.method.toUpperCase() == "POST") { app.post(service.path, ...unpackMiddlewares(service.middlewares)); } else if (service.method.toUpperCase() == "PUT") { app.put(service.path, ...unpackMiddlewares(service.middlewares)); } else if (service.method.toUpperCase() == "DELETE") { app.delete(service.path, ...unpackMiddlewares(service.middlewares)); } return service; }); //console.log("webApps: ",isoApp.webApps.length, " -> ", isoApp.webApps); // split the clientApps here and define a function for each of the clientApps, with the right middleware isoApp.webApps //.filter(clientApp => clientApp.middlewares !== undefined) .map(clientApp => { const serveMiddleware = (req, res, next) => serve(req, res, next, clientApp, assetsDir, isoConfig, isOffline); const routes = clientApp.routes.filter(route => route.middlewares !== undefined && route.middlewares.length > 0); if (clientApp.method.toUpperCase() == "GET") { app.get(clientApp.path, ...unpackMiddlewares(clientApp.middlewares)); routes.forEach(route => app.get(route.path, ...unpackMiddlewares(route.middlewares))); app.get(clientApp.path, serveMiddleware); } else if (clientApp.method.toUpperCase() == "POST") { app.post(clientApp.path, ...unpackMiddlewares(clientApp.middlewares)); routes.forEach(route => app.post(route.path, ...unpackMiddlewares(route.middlewares))); app.post(clientApp.path, serveMiddleware); } else if (clientApp.method.toUpperCase() == "PUT") { app.put(clientApp.path, ...unpackMiddlewares(clientApp.middlewares)); routes.forEach(route => app.put(route.path, ...unpackMiddlewares(route.middlewares))); app.put(clientApp.path, serveMiddleware); } else if (clientApp.method.toUpperCase() == "DELETE") { app.delete(clientApp.path, ...unpackMiddlewares(clientApp.middlewares)); routes.forEach(route => app.delete(route.path, ...unpackMiddlewares(route.middlewares))); app.delete(clientApp.path, serveMiddleware); } return clientApp; }); return app; }; function serve(req, res, next, clientApp, assetsDir, isoConfig, isOffline) { return __awaiter(this, void 0, void 0, function* () { //console.log("serve - isOffline: ", isOffline) //TODO use try catch depending on the environment //try { //context is used by react router, empty by default let context = {}; const basename = iso_libs_1.getBasename(); // creating the stylesheet const sheet = new styled_components_1.ServerStyleSheet(); const parsedUrl = req.url.indexOf("?") >= 0 ? req.url.substring(0, req.url.indexOf("?")) : req.url; //console.log("parsedUrl: ", parsedUrl); ////////// TODO refactor var foundPath = undefined; // match request url to our React Router paths and grab the path-params let matchResult = clientApp.routes.find(({ path, exact }) => { //console.log("match path: ", new RegExp(path)) foundPath = react_router_1.matchPath(parsedUrl, { path: new RegExp(path), exact: exact, strict: false }); //console.log("foundPath: ", foundPath) return foundPath; }) || {}; let { path } = matchResult; //console.log("found: ", foundPath); //console.log("server: path params: ", foundPath ? foundPath.params : "---"); //console.log("url: ", req.url); const routePath = foundPath ? ((typeof foundPath.path === 'string' || foundPath.path instanceof String) ? (foundPath.path.indexOf("/:") > 0 ? foundPath.path.substring(0, foundPath.path.indexOf("/:")) : foundPath.path) : req.url) : ""; //console.log("routePath: ", routePath); ////////// END OF REFACTORING required //console.log("app data layer id: ", clientApp.dataLayerId); const serverState = {}; const setServerValue = (id, value, isInitial = false) => { if (!serverState[id] || !isInitial) { serverState[id] = value; //console.log("set >>", id, "<< -> ", value); } }; const getIsomorphicState = () => { //console.log("serverState: ", serverState); return `window.__ISOMORPHICSTATE__ = ${JSON.stringify(serverState)}`; }; const fConnectWithDataLayer = clientApp.dataLayerId !== undefined ? datalayer_integration_1.connectWithDataLayer(clientApp.dataLayerId, req, isOffline) : function (app) { return __awaiter(this, void 0, void 0, function* () { //console.log("default dummy data layer") return { connectedApp: app, getState: () => "" }; }); }; /*const serverApp = await new Promise((resolve, reject) => { });*/ var renderList = []; const addToRenderList = (fRenderSsr, hashValue) => { //console.log("add to Render: ", hashValue); renderList = renderList.concat([{ fRenderSsr: fRenderSsr, hashValue: hashValue }]); }; const completeSSR = (htmlData, getState, renderListResults) => { // getting all the tags from the sheet const styles = sheet.getStyleTags(); //render helmet data aka meta data in <head></head> const helmetData = react_helmet_1.default.renderStatic(); const fRender = clientApp.renderHtmlPage ? clientApp.renderHtmlPage : renderHtmlPage; // render a page with the state and return it in the response res.status(200).send(fRender(htmlData, styles, getState(), getIsomorphicState(), `window.__RENDERLISTSTATE__ = ${JSON.stringify(renderListResults)}`, helmetData, basename, req.url, clientApp, assetsDir)).end(); }; function renderApp(apolloState, oldServerState, oldStorageState, htmlData, getApolloState, newStorageState) { return __awaiter(this, void 0, void 0, function* () { const tempApolloState = getApolloState(); const tempServerState = Object.assign({}, serverState); /*console.log("APOLLO STATE BEFORE", apolloState, "\nAPOLLO STATE AFTER ", tempApolloState); console.log("SERVER STATE BEFORE", oldServerState, "\nSERVER STATE AFTER ", serverState); console.log("STORAGE STATE BEFORE", oldStorageState, "\nSTORAGE STATE AFTER ", newStorageState); */ if (JSON.stringify(apolloState) !== JSON.stringify(tempApolloState) || JSON.stringify(oldServerState) !== JSON.stringify(serverState) || (oldStorageState && oldStorageState.length !== newStorageState.length)) { //console.log("--------- need to render --------"); // create the app and connect it with the DataAbstractionLayer yield fConnectWithDataLayer(routed_app_1.createServerApp(clientApp.routes, clientApp.redirects, basename, req.url, context, req, res, require('infrastructure-components').getAuthCallback(isoConfig, clientApp.authenticationId), setServerValue, addToRenderList, isoConfig, isOffline, newStorageState, serverState)).then(({ connectedApp, getState }) => __awaiter(this, void 0, void 0, function* () { //console.log("renderList before: ", renderList) // collect the styles from the connected appsheet.collectStyles( const newHtmlData = server_1.default.renderToString(connectedApp); const addToStorage = yield Promise.all(renderList.filter((item, index, arr) => { const c = arr.map(item => item.hashValue); return index === c.indexOf(item.hashValue); }).map(({ fRenderSsr, hashValue }) => new Promise((resolve, reject) => { return fRenderSsr(({ data, files }) => { //console.log("resolved: ", files); resolve({ hashValue: hashValue, data: data, files: files, }); }, err => { //console.log("rejected: ", err); reject(err); }); }))); //console.log("renderList after: ", renderList) renderList.length = 0; //console.log("addToStorage: ", addToStorage); //completeSSR(newHtmlData, getState, storageState); yield renderApp(tempApolloState, tempServerState, newStorageState, newHtmlData, getState, newStorageState.concat(addToStorage)); })); } else { //console.log("----- no difference, return ------"); completeSSR(htmlData, getApolloState, newStorageState); } ; }); } ; yield renderApp({}, serverState, undefined, "", () => "", []); /* // create the app and connect it with the DataAbstractionLayer await fConnectWithDataLayer( createServerApp( clientApp.routes, clientApp.redirects, basename, req.url, context, req, require('infrastructure-components').getAuthCallback(isoConfig, clientApp.authenticationId), setServerValue, addToRenderList, isoConfig, isOffline, undefined, undefined ) ).then(async ({connectedApp, getState}) => { //console.log("resolved... renderList: ", renderList); // collect the styles from the connected appsheet.collectStyles( const htmlData = ReactDOMServer.renderToString(connectedApp); //console.log(htmlData); const renderListResults = await Promise.all( renderList.filter((item, index, arr)=> { const c = arr.map(item=> item.hashValue); return index === c.indexOf(item.hashValue) }).map(({fRenderSsr, hashValue}) => new Promise((resolve, reject) => { return fRenderSsr( (data, files)=>{ //console.log("resolved: ", files); resolve({ hashValue: hashValue, data: data, files: files, }) }, err=>{ //console.log("rejected: ", err); reject(err); } ) })) ); if (renderListResults && renderListResults.length > 0) { //console.log("need to rerender! got renderListResults: ", renderListResults); await renderApp ({}, serverState, renderListResults, htmlData, getState); } else { completeSSR(htmlData, getState, renderListResults); } });*/ }); } /** * * This functions puts together the whole Html * * - style as collected from the styled-components * - head-meta data: from helmet * - data state: from the DAL * - basename: to let the client know * * The html loads the script from the path where we find the assets. This is part of the config, thus load it * using `require('../config').pathToAssets(process.env.STAGE_PATH)` * * The script loading the app.bundle.js uses the window location in order to find out whether there is a slash * * //TODO the app.bundle.js depends on the name "app". Paramterize this! * * //TODO: apply the same base style as the client does * * when we are in a sub-route, e.g. when we have path parameters, we need to add ../ to the path to the assets * * Routing to the Assets * * entered url | basename==/ | basename==/dev * --------------------------------------------- * (none) / /dev * / / /dev * (dev)/ / / * (dev)/route / / * (dev)/route/ ../ ../ * (dev)/route/:var ../ ../ * TODO what happens with more than one path parameter? * * * @param host the host of the request * @param html * @param styles * @param preloadedState the state in form of a script * @param helmet */ function renderHtmlPage(html, styles, preloadedState, isomorphicState, renderListResults, helmet, basename, routePath, clientApp, assetsDir) { //<link rel="icon" href="/assets/favicon.ico" type="image/ico" /> //console.log(preloadedState); const path = require('path'); const calcBase = () => { return path.join(basename, routePath); }; //For each"/" in the entered path after the basename, we need to add "../" to the assets-path //when there is a basename, it must be added //console.log("calcBase: ", calcBase()); return `<!doctype html> <html> <head> <meta charset="utf-8" /> ${helmet.title.toString()} ${helmet.meta.toString()} ${helmet.link.toString()} <meta name="viewport" content="width=device-width, initial-scale=1.0"> ${styles} <style> html, body, #root { display: block; margin: 0px; } </style> </head> <body> <div id="root">${html.trim()}</div> <script> ${preloadedState} ${isomorphicState} ${renderListResults} window.__BASENAME__ = "${basename}"; </script> <script> var loadscript = document.createElement('script'); function getPath() { const basePath = ${basename !== "/" ? "window.location.pathname.startsWith(\"" + basename + "\") ? \"\": \"/\" " : "\"\""}; const routePath= "${routePath !== "/" ? routePath : ""}"; const pre = window.location.pathname.startsWith(basePath+routePath+"/") ? ".." : ""; return pre+"${path.join(basename, assetsDir, server_libs_1.getClientFilename(clientApp.id))}"; } loadscript.setAttribute('src',getPath()); document.body.appendChild(loadscript); </script> </body> </html>`; } // we're exporting the handler-function as default, must match the sls-config! //export default (assetsDir, resolvedAssetsPath) => serverless(createServer(assetsDir, resolvedAssetsPath)); /* const serverIndexPath = path.join(serverPath, "index.js"); fs.writeFileSync(serverIndexPath, `const lib = require ('./server'); const server = lib.default('${ssrConfig.assetsPath}', '${resolveAssetsPath(ssrConfig)}'); exports.default = server;*/ // these variables are replaced during compilation exports.default = serverless_http_1.default(createServer(__ASSETS_PATH__, __RESOLVED_ASSETS_PATH__, __ISOMORPHIC_ID__, __ISOFFLINE__)); //# sourceMappingURL=server.js.map