UNPKG

@underpostnet/underpost

Version:

Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc

125 lines (110 loc) 4.81 kB
/** * Module for managing server side rendering * @module src/client-builder/ssr.js * @namespace ServerSideRendering */ import fs from 'fs-extra'; import vm from 'node:vm'; import Underpost from '../index.js'; import { srcFormatted, JSONweb } from './client-formatted.js'; import { loggerFactory } from '../server/logger.js'; import { getRootDirectory } from '../server/process.js'; const logger = loggerFactory(import.meta); /** * Creates a server-side rendering component function from a given file path. * It reads the component file, formats it, and executes it in a sandboxed Node.js VM context to extract the component. * @param {string} [componentPath='./src/client/ssr/RootDocument.js'] - The path to the SSR component file. * @returns {Promise<Function>} A promise that resolves to the SSR component function. * @memberof ServerSideRendering */ const ssrFactory = async (componentPath = `./src/client/ssr/RootDocument.js`) => { const context = { SrrComponent: () => {}, npm_package_version: Underpost.version }; vm.createContext(context); vm.runInContext(await srcFormatted(fs.readFileSync(componentPath, 'utf8')), context); return context.SrrComponent; }; /** * Sanitizes an HTML string by adding a nonce to all script and style tags for Content Security Policy (CSP). * The nonce is retrieved from `res.locals.nonce`. * @param {object} res - The Express response object. * @param {object} req - The Express request object. * @param {string} html - The HTML string to sanitize. * @returns {string} The sanitized HTML string with nonces. * @memberof ServerSideRendering */ const sanitizeHtml = (res, req, html) => { const nonce = res.locals.nonce; return html .replace(/<script(?=\s|>)/gi, `<script nonce="${nonce}"`) .replace(/<style(?=\s|>)/gi, `<style nonce="${nonce}"`); }; /** * Factory function to create Express middleware for handling 404 and 500 errors. * It generates server-side rendered HTML for these error pages. If static error pages exist, it redirects to them. * @param {object} options - The options for creating the middleware. * @param {object} options.app - The Express app instance. * @param {string} options.directory - The directory for the instance's static files. * @param {string} options.rootHostPath - The root path for the host's public files. * @param {string} options.path - The base path for the instance. * @returns {Promise<{error500: Function, error400: Function}>} A promise that resolves to an object containing the 500 and 404 error handling middleware. * @memberof ServerSideRendering */ const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => { const Render = await ssrFactory(); const ssrPath = path === '/' ? path : `${path}/`; // Build default html src for 404 and 500 const defaultHtmlSrc404 = Render({ title: '404 Not Found', ssrPath, ssrHeadComponents: '', ssrBodyComponents: (await ssrFactory(`./src/client/ssr/body/404.js`))(), renderPayload: { apiBasePath: process.env.BASE_API, version: Underpost.version, }, renderApi: { JSONweb, }, }); const path404 = `${directory ? directory : `${getRootDirectory()}${rootHostPath}`}/404/index.html`; const page404 = fs.existsSync(path404) ? `${path === '/' ? '' : path}/404` : undefined; const defaultHtmlSrc500 = Render({ title: '500 Server Error', ssrPath, ssrHeadComponents: '', ssrBodyComponents: (await ssrFactory(`./src/client/ssr/body/500.js`))(), renderPayload: { apiBasePath: process.env.BASE_API, version: Underpost.version, }, renderApi: { JSONweb, }, }); const path500 = `${directory ? directory : `${getRootDirectory()}${rootHostPath}`}/500/index.html`; const page500 = fs.existsSync(path500) ? `${path === '/' ? '' : path}/500` : undefined; return { error500: function (err, req, res, next) { logger.error(err, err.stack); if (page500) return res.status(500).redirect(page500); else { res.set('Content-Type', 'text/html'); return res.status(500).send(sanitizeHtml(res, req, defaultHtmlSrc500)); } }, error400: function (req, res, next) { // if /<path>/home redirect to /<path> const homeRedirectPath = `${path === '/' ? '' : path}/home`; if (req.url.startsWith(homeRedirectPath)) { const redirectUrl = req.url.replace('/home', ''); return res.redirect(redirectUrl.startsWith('/') ? redirectUrl : `/${redirectUrl}`); } if (page404) return res.status(404).redirect(page404); else { res.set('Content-Type', 'text/html'); return res.status(404).send(sanitizeHtml(res, req, defaultHtmlSrc404)); } }, }; }; export { ssrMiddlewareFactory, ssrFactory, sanitizeHtml };