import React from 'react'; import express, { Application, Request, Response } from 'express'; import cookieParser from 'cookie-parser'; import { v1 as uuidv1 } from 'uuid'; import ReactDOMServer from 'react-dom/server'; import api from './api'; import path from 'path'; import fs from 'fs'; import { buildOrisonDirectory } from './build'; import { Configuration } from 'webpack'; import * as devServer from './dev-server'; import WebSocketService from './websocket'; import expressWs from 'express-ws'; import OrisonStyleContext, { IOrisonStyleContext } from './styles'; export interface SessionState { sessionToken: string; [key: string]: any; } export type SessionRequest = Request & { session: SessionState; } export interface OrisonSettings { /** * returns the default session state created for new sessions */ sessionStateGenerator: (req: Request) => Omit; /** * returns the webpack configuration for compilation, given the default one */ rewebpack?: (options: Configuration, transformer?: 'server' | 'client') => Configuration; /** * true if the server is in production mode */ production?: boolean; /** * a list of routes which will be rendered server-side only and will not be hydrated client-side */ skipHydration?: string[]; /** * whether or not to statically render the master page and its child page before passing it to the `App` on the server-side. * If this is `true`, the body passed to the `App` is a `div` containing the static HTML string. * If this is `false,` the body passed is a `React.ReactElement` containing the React component for the body. * Note that if this is `false` the body will be statically rendered twice: one phase for the server-side CSS generation, and another phase for the App. * * @default true */ preRenderBody?: boolean; } export class OrisonServer { private server: expressWs.Instance; private sessions: SessionState[] = []; private settings: OrisonSettings | null = null; private modules: Record = {}; private pageDependencies: Record = {}; private webSocketService: WebSocketService = new WebSocketService(); public constructor() { this.server = expressWs(express()); } public setModuleDefinition(path: string, module: any) { this.modules[path] = module; } public setModuleDependencies(path: string, deps: string[]) { this.pageDependencies[path] = deps; } public getSettings(): OrisonSettings | null { return this.settings; } public getSessions(): SessionState[] { return this.sessions; } public getWebSocketService(): WebSocketService { return this.webSocketService; } /** * Initializes the Orison server instance using the given settings, builds the server and client files with Webpack, initializes all API routes, and creates all Express request listeners. * * @param settings The settings for the Orison server. */ public async configure(settings: OrisonSettings) { if (settings.production) { process.env.NODE_ENV = 'production'; } else { process.env.NODE_ENV = 'development'; } if (settings.preRenderBody === undefined) { settings.preRenderBody = true; } const buildResult = await buildOrisonDirectory(settings); api.initialize(this); this.server.app.use('*', cookieParser()); this.server.app.use('*', (req, res, next) => { try { const toSessionRequest = (session: SessionState) => { const sr = req as any as SessionRequest; sr.session = session; } const addSession = () => { const token = uuidv1(); let expires = new Date(); expires.setMonth(expires.getMonth() + 1); res.cookie('__ORISON_TOKEN', token, { httpOnly: true, expires }); const session: SessionState = { sessionToken: token, ...this.settings!.sessionStateGenerator(req) }; this.sessions.push(session); toSessionRequest(session); next(); }; if (typeof req.cookies.__ORISON_TOKEN !== 'string') { addSession(); } else { const session = this.sessions.find(session => session.sessionToken === req.cookies.__ORISON_TOKEN); if (session === undefined) { addSession(); } else { toSessionRequest(session); next(); } } } catch (e) { console.log(e); if (!res.headersSent) { res.sendStatus(500); } } }); this.webSocketService.initialize(); this.server.app.ws('/websocket', (ws, req) => { this.webSocketService.handleConnection(this, ws, req as SessionRequest); }); const appFunction: OrisonApp = require(path.relative(__dirname, path.join(process.cwd(), '.orison', 'pages', '_app_server'))).default; let masterFunction: OrisonPage | null = null; const mfp = path.join(process.cwd(), '.orison', 'pages', '_master_server'); if (fs.existsSync(mfp + '.js')) { masterFunction = require(path.relative(__dirname, mfp)).default; } this.pageDependencies = buildResult.pageDependencies; for (const page of buildResult.pages) { if (page.path === '/_master' || page.path === '/_app') { continue; } const module = require(path.relative(__dirname, path.join(process.cwd(), '.orison', 'pages', page.serverScriptPath + '_server'))); const loaded = module.default; this.setModuleDefinition(page.path, loaded); } for (const page of buildResult.pages) { this.server.app.get(page.path, async (req, res) => { const loaded = this.modules[page.path]; const isStatic = page.clientScriptPath === null; let props = {}; if (typeof loaded.getServerSideProps === 'function') { props = await loaded.getServerSideProps(req as any as SessionRequest, res); if (res.headersSent) { return; } if (props === undefined) { throw new Error('No props were returned, but the connection was not closed'); } } let masterProps = {}; if (masterFunction !== null && typeof masterFunction.getServerSideProps === 'function') { masterProps = await masterFunction.getServerSideProps(req as any as SessionRequest, res); if (res.headersSent) { return; } if (masterProps === undefined) { throw new Error('No props were returned, but the connection was not closed'); } } let subElement: React.ReactElement = React.createElement(loaded, props); if (masterFunction !== null) { subElement = React.createElement(masterFunction, masterProps, subElement); } const body = ( <>
{subElement}
{!isStatic ? ( <> {this.pageDependencies[page.clientScriptPath!]?.map(vendor => (