UNPKG

@dr.pogodin/react-utils

Version:

Collection of generic ReactJS components and utils

205 lines (197 loc) 9.66 kB
// eslint-disable-next-line import/no-unassigned-import import 'source-map-support/register.js'; import http from 'node:http'; import https from 'node:https'; import { cloneDeep, defaults } from 'lodash-es'; // Polyfill required by ReactJS. // TODO: Double-check, if it is still required by React v19? // eslint-disable-next-line import/no-unassigned-import import 'raf/polyfill.js'; import serverFactory, { getDefaultCspSettings } from "./server"; import { SCRIPT_LOCATIONS, newDefaultLogger } from "./renderer"; import { errors } from "./utils"; export { errors, getDefaultCspSettings }; /** * Normalizes a port into a number, string, or false. * TODO: Drop this function? * @param value Port name or number. * @return Port number (Number), name (String). */ function normalizePort(value) { const port = typeof value === 'string' ? parseInt(value) : value; if (Number.isFinite(port)) return port; /* port number */ return value; /* named pipe */ } /** * Creates and launches web-server for ReactJS application. Allows zero * or detailed configuration, supports server-side rendering, * and development tools, including Hot Module Reloading (HMR). * * NOTE: Many of options defined below are passed down to the server and * renderer factories, and their declared default values are set in those * factories, rather than here. * * @param {object} webpackConfig Webpack configuration used to build * the frontend bundle. In production mode the server will read out of it * `context`, `publicPath`, and a few other parameters, necessary to locate * and serve the app bundle. In development mode the server will use entire * provided config to build the app bundle in memory, and further watch and * update it via HMR. * @param {object} [options] Additional parameters. * @param {Component} [options.Application] The root ReactJS component of * the app to use for the server-side rendering. When not provided * the server-side rendering is disabled. * @param {function} [options.beforeExpressJsError] Asynchronous callback * (`(server) => Promise<boolean>`) to be executed just before the default error * handler is added to ExpressJS server. If the callback is provided and its * result resolves to a truthy value, `react-utils` won't attach the default * error handler. * @param {function} [options.beforeExpressJsSetup] Asynchronous callback * (`(server) => Promise) to be executed right after ExpressJS server creation, * before any configuration is performed. * @param {BeforeRenderHook} [options.beforeRender] The hook to run just before * the server-side rendering. For each incoming request, it will be executed * just before the HTML markup is generated at the server. It allows to load * and provide the data necessary for server-side rendering, and also to inject * additional configuration and scripts into the generated HTML code. * @param {boolean} [options.noCsp] Set `true` to disable * Content-Security-Policy (CSP) headers altogether. * @param {function} [options.cspSettingsHook] A hook allowing * to customize [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) * settings for [helmet](https://github.com/helmetjs/helmet)'s * `contentSecurityPolicy` middleware on per-request basis. * * If provided it should be a with signature: \ * `(defaultSettings: object, req: object)` &rArr; `object` \ * which gets the default settings (also used without the hook), * and the incoming request object. The hook response will be passed * as options to the helmet `contentSecurityPolicy` middleware. * * Currently, the default settings is the following object in production * environment: * ```js * { * directives: { * defaultSrc: ["'self'"], * baseUri: ["'self'"], * blockAllMixedContent: [], * fontSrc: ["'self'", 'https:', 'data:'], * frameAncestors: ["'self'"], * frameSrc: ["'self'", 'https://*.youtube.com'], * imgSrc: ["'self'", 'data:'], * objectSrc: ["'none'"], * scriptSrc: ["'self'", "'unsafe-eval'", `'nonce-UNIQUE_NONCE_VALUE'`], * scriptSrcAttr: ["'none'"], * styleSrc: ["'self'", 'https:', "'unsafe-inline'"], * upgradeInsecureRequests: [] // Removed in dev mode. * } * } * ``` * It matches the default value used by Helmet with a few updates: * - YouTube host is whitelisted in the `frameSrc` directive to ensure * the {@link YouTubeVideo} component works. * - An unique per-request nonce is added to `scriptSrc` directive to * whitelist auxiliary scripts injected by react-utils. The actual nonce * value can be fetched by host code via `.nonce` field of `req` argument * of `.beforeRender` hook. * - `upgradeInsecureRequests` directive is removed in development mode, * to simplify local testing with http requests. * @param {string} [options.defaultLoggerLogLevel='info'] Log level for * the default logger, which is created if no `logger` option provided. * @param {boolean} [options.devMode] Pass in `true` to start the server in * development mode. * @param {string} [options.favicon] Path to the favicon to use by the server. * By default no favicon is used. * @param {object} [options.https] If provided, HTTPS server will be started, * instead of HTTP otherwise. The object should provide SSL certificate and key * via two string fields: `cert`, and `key`. * @param {string} [options.https.cert] SSL Certificate. * @param {string} [options.https.key] SSL key. * @param {boolean} [options.httpsRedirect=true] Pass in `true` to enable * automatic redirection of all incoming HTTP requests to HTTPS. * * To smoothly use it at `localhost` you need to run the server in HTTPS mode, * and also properly create and install a self-signed SSL sertificate on your * system. This article is helpful: * [How to get HTTPS working on your local development environment in 5 minutes](https://medium.freecodecamp.org/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec) * @param {Logger} [options.logger] The logger to use at server side. * By default [`winston`](https://www.npmjs.com/package/winston) logger * with console transport is used. The logger you provide, or the default * `winston` logger otherwise, will be attached to the created ExpressJS server * object. * @param {function} [options.onExpressJsSetup] An async callback * (`(server) => Promise`) to be triggered when most of the server * configuration is completed, just before the server-side renderer, * and the default error handler are attached. You can use it to mount * custom API routes. The server-side logger can be accessed as `server.logger`. * @param {number|string} [options.port=3000] The port to start the server on. * @param {number} [options.staticCacheSize=1.e7] The maximum * static cache size in bytes. Defaults to ~10 MB. * @param {function} [options.staticCacheController] When given, it activates, * and controls the static caching of generated HTML markup. When this function * is provided, on each incoming request it is triggered with the request * passed in as the argument. To attempt to serve the response from the cache * it should return the object with the following fields: * - `key: string` &ndash; the cache key for the response; * - `maxage?: number` &ndash; the maximum age of cached result in ms. * If undefined - infinite age is assumed. * @param {number} [options.maxSsrRounds=10] Maximum number of SSR rounds. * @param {number} [options.ssrTimeout=1000] SSR timeout in milliseconds, * defaults to 1 second. * @return Resolves to an object with created Express and HTTP servers. */ export default async function launchServer(webpackConfig, options = {}) { /* Options normalization. */ const ops = cloneDeep(options); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ops.port = normalizePort(ops.port || process.env.PORT || 3000); defaults(ops, { httpsRedirect: true }); // TODO: Need a separate type for normalized options, which guarantees // the logger is set! ops.logger ??= newDefaultLogger({ defaultLogLevel: ops.defaultLoggerLogLevel }); /* Creates servers, resolves and sets the port. */ const expressServer = await serverFactory(webpackConfig, ops); let httpServer; if (ops.https) { httpServer = https.createServer({ cert: ops.https.cert, key: ops.https.key }, expressServer); } else httpServer = http.createServer(expressServer); /* Sets error handler for HTTP(S) server. */ httpServer.on('error', error => { if (error.syscall !== 'listen') throw error; const bind = typeof ops.port === 'string' ? `Pipe ${ops.port}` : `Port ${ops.port}`; /* Human-readable message for some specific listen errors. */ switch (error.code) { case 'EACCES': ops.logger.error(`${bind} requires elevated privileges`); return process.exit(1); case 'EADDRINUSE': ops.logger.error(`${bind} is already in use`); return process.exit(1); case undefined: default: throw error; } }); /* Listening event handler for HTTP(S) server. */ httpServer.on('listening', () => { const addr = httpServer.address(); const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`; ops.logger.info(`Server listening on ${bind} in ${process.env.NODE_ENV} mode`); }); httpServer.listen(ops.port); return { expressServer, httpServer }; } launchServer.SCRIPT_LOCATIONS = SCRIPT_LOCATIONS; launchServer.getDefaultCspSettings = getDefaultCspSettings; launchServer.errors = errors; //# sourceMappingURL=index.js.map