@cretadoc/server
Version:
1 lines • 17.7 kB
Source Map (JSON)
{"version":3,"file":"server.cjs","sources":["../src/middleware/error-handler/error-handler.ts","../src/middleware/load-api/load-api.ts","../src/middleware/load-common-middleware/load-common-middleware.ts","../src/utils/constants.ts","../src/utils/exceptions/cretadoc-server-error.ts","../src/utils/exceptions/config-error.ts","../src/utils/helpers/config.ts","../src/utils/helpers/paths.ts","../src/middleware/load-static-dir/load-static-dir.ts","../src/middleware/render-contents/render-contents.ts","../src/server.ts"],"sourcesContent":["/* eslint-disable max-params */\nimport { HTTP_STATUS_CODE } from '@cretadoc/utils';\nimport type { ErrorRequestHandler } from 'express';\n\n/**\n * Error handler middleware.\n *\n * @param err - The error.\n * @param _req - The Express request.\n * @param res - The Express response.\n * @param next - The Express next function.\n */\nexport const errorHandler: ErrorRequestHandler = (err, _req, res, next) => {\n if (err instanceof Error) {\n console.error(err.stack);\n res\n .status(HTTP_STATUS_CODE.INTERNAL_SERVER_ERROR)\n .send(`${err.name}: ${err.message}`);\n } else next(err);\n};\n","import type { APIInstance } from '@cretadoc/api';\nimport type { RequestHandler } from 'express';\n\n// We need to be explicit to avoid TS2742 error.\ntype LoadAPI = (instance: APIInstance) => RequestHandler;\n\nexport const loadAPI: LoadAPI =\n (instance: APIInstance): RequestHandler =>\n (req, res, next) => {\n instance(req, res).catch(next);\n };\n","import cors from 'cors';\nimport type { RequestHandler } from 'express';\nimport helmet from 'helmet';\nimport type { ServerMode } from '../../types';\n\nexport const loadCommonMiddleware =\n (mode: ServerMode): RequestHandler =>\n (_req, _res, next) => {\n try {\n if (mode === 'production') {\n cors({ origin: false });\n helmet();\n } else cors({ origin: '*' });\n\n next();\n } catch (error) {\n next(error);\n }\n };\n","import type { ServerConfig } from '../types';\n\nexport const ENVIRONMENT = {\n DEVELOPMENT: 'development',\n PRODUCTION: 'production',\n TEST: 'test',\n} as const;\n\nexport const DEFAULT_HOSTNAME = 'localhost';\nexport const DEFAULT_MODE = ENVIRONMENT.DEVELOPMENT;\nexport const DEFAULT_PORT = 3000;\n\nexport const DEFAULT_ENTRYPOINT_FILE = 'index.html';\nexport const DEFAULT_SSR_ROUTE = '/';\nexport const DEFAULT_STATIC_ROUTE = '/static';\n\nexport const DEFAULT_CONFIG = {\n api: undefined,\n hmr: undefined,\n hostname: DEFAULT_HOSTNAME,\n mode: DEFAULT_MODE,\n port: DEFAULT_PORT,\n ssr: undefined,\n staticDir: undefined,\n} as const satisfies ServerConfig;\n\nexport const SERVER_ERROR_CODE = {\n /**\n * The server is misconfigured.\n */\n BAD_CONFIGURATION: 'BAD_CONFIGURATION',\n /**\n * An unspecified error occurred.\n */\n INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',\n} as const;\n","export class CretadocServerError extends Error {\n constructor(context: string, message: string) {\n super(`${context}: ${message}`);\n this.name = 'CretadocServerError';\n }\n}\n","import { SERVER_ERROR_CODE } from '../constants';\nimport { CretadocServerError } from './cretadoc-server-error';\n\nexport class ConfigError extends CretadocServerError {\n constructor(message: string) {\n super(SERVER_ERROR_CODE.BAD_CONFIGURATION, message);\n }\n}\n","import { existsSync } from 'fs';\nimport {\n deepFreeze,\n type Maybe,\n type PartialDeep,\n type ReadonlyDeep,\n} from '@cretadoc/utils';\nimport type {\n HMRConfig,\n ServerConfig,\n ServerMode,\n SSRConfig,\n StaticDirConfig,\n} from '../../types';\nimport {\n DEFAULT_CONFIG,\n DEFAULT_ENTRYPOINT_FILE,\n DEFAULT_SSR_ROUTE,\n DEFAULT_STATIC_ROUTE,\n} from '../constants';\nimport { ConfigError } from '../exceptions';\n\n/**\n * Merge the user static directory config with some default values if needed.\n *\n * @param {PartialDeep<StaticDirConfig>} [userConfig] - The user config.\n * @returns {Maybe<StaticDirConfig>} The merged config.\n */\nexport const mergeStaticDirConfig = (\n userConfig?: PartialDeep<StaticDirConfig>\n): Maybe<StaticDirConfig> => {\n if (!userConfig) return undefined;\n\n if (!userConfig.path)\n throw new ConfigError('The static directory path is mandatory.');\n\n if (!existsSync(userConfig.path))\n throw new ConfigError(\n `The static directory path does not exist. Received: ${userConfig.path}`\n );\n\n return {\n entrypoint: userConfig.entrypoint ?? DEFAULT_ENTRYPOINT_FILE,\n path: userConfig.path,\n route: userConfig.route ?? DEFAULT_STATIC_ROUTE,\n };\n};\n\n/**\n * Merge the user HMR config with some default values if needed.\n *\n * @param {PartialDeep<HMRConfig>} [userConfig] - The user config.\n * @param {Maybe<ServerMode>} [mode] - The server mode.\n * @returns {Maybe<HMRConfig>} The merged config.\n */\nexport const mergeHMRConfig = (\n userConfig?: PartialDeep<HMRConfig>,\n mode?: Maybe<ServerMode>\n): Maybe<HMRConfig> => {\n if (userConfig === false) return userConfig;\n if (userConfig) return { port: userConfig.port };\n\n return mode === 'production' ? false : undefined;\n};\n\n/**\n * Merge the user SSR config with some default values if needed.\n *\n * @param {PartialDeep<SSRConfig>} [userConfig] - The user config.\n * @returns {Maybe<SSRConfig>} The merged config.\n */\nexport const mergeSSRConfig = (\n userConfig?: PartialDeep<SSRConfig>\n): Maybe<SSRConfig> => {\n if (!userConfig) return undefined;\n\n if (!userConfig.entrypoint)\n throw new ConfigError('In SSR mode, the server entrypoint is mandatory.');\n\n return {\n entrypoint: userConfig.entrypoint,\n route: userConfig.route ?? DEFAULT_SSR_ROUTE,\n };\n};\n\n/**\n * Merge the user config with default server configuration.\n *\n * @param {Maybe<PartialDeep<ServerConfig>>} userConfig - A config object.\n * @returns {ReadonlyDeep<ServerConfig>} The server configuration.\n */\nexport const mergeDefaultConfigWith = (\n userConfig: Maybe<PartialDeep<ServerConfig>>\n): ReadonlyDeep<ServerConfig> => {\n const mode = userConfig?.mode ?? DEFAULT_CONFIG.mode;\n\n if (!userConfig) return deepFreeze(DEFAULT_CONFIG);\n\n const newConfig: ServerConfig = {\n api: userConfig.api,\n hmr: mergeHMRConfig(userConfig.hmr, mode),\n hostname: userConfig.hostname ?? DEFAULT_CONFIG.hostname,\n mode,\n port: userConfig.port ?? DEFAULT_CONFIG.port,\n ssr: mergeSSRConfig(userConfig.ssr),\n staticDir: mergeStaticDirConfig(userConfig.staticDir),\n };\n\n return deepFreeze(newConfig);\n};\n","import { join } from 'path';\n\n/**\n * Retrieve a file path from a given entrypoint and a path.\n *\n * It concatenates the base path with entrypoint if needed.\n *\n * @param {string} entrypoint - An entrypoint path or a filename.\n * @param {string} path - A base path.\n * @returns {string} The absolute file path.\n */\nexport const getFilePathFrom = (entrypoint: string, path: string): string => {\n if (entrypoint.startsWith(path)) return entrypoint;\n return join(path, entrypoint);\n};\n","import express, { type RequestHandler } from 'express';\nimport type { StaticDirConfig } from '../../types';\nimport { getFilePathFrom } from '../../utils/helpers';\n\n/**\n * Express middleware to serve a static directory.\n *\n * @param {Omit<StaticDirConfig, 'route'>} config - The static directory config.\n * @returns {RequestHandler} The request handler.\n */\nexport const loadStaticDir =\n ({ entrypoint, path }: Omit<StaticDirConfig, 'route'>): RequestHandler =>\n (_req, res, next) => {\n try {\n express.static(path, { index: false });\n res.sendFile(getFilePathFrom(entrypoint, path));\n } catch (error) {\n next(error);\n }\n };\n","import { isObject, isObjKeyExist, type Maybe } from '@cretadoc/utils';\nimport type { RequestHandler } from 'express';\nimport type { ViteDevServer } from 'vite';\nimport type { RenderFn, SSRConfig } from '../../types';\nimport { SERVER_ERROR_CODE } from '../../utils/constants';\nimport { CretadocServerError } from '../../utils/exceptions';\n\ntype ValidExports = {\n /**\n * The render method.\n */\n render: RenderFn;\n};\n\n/**\n * Check if the given value matches the `RenderFn` type.\n *\n * @param {unknown} value - A value to validate.\n * @returns {boolean} True if it is a function.\n */\nconst isFunction = (value: unknown): value is RenderFn => {\n if (typeof value !== 'function') return false;\n return true;\n};\n\n/**\n * Check if an imported value match the `ValidExports` type.\n *\n * @param {unknown} value - A value to validate.\n * @returns {boolean} True if the export is valid.\n */\nexport const isValidExports = (value: unknown): value is ValidExports => {\n if (!isObject(value)) return false;\n if (!isObjKeyExist(value, 'render')) return false;\n if (!isFunction(value.render)) return false;\n return true;\n};\n\n/**\n * Import the render method from the given entrypoint.\n *\n * @param {string} entrypoint - The path of the server entrypoint.\n * @param {Maybe<ViteDevServer>} viteServer - The Vite server in dev mode.\n * @returns {Promise<RenderFn>} The render method.\n */\nconst importRenderMethod = async (\n entrypoint: string,\n viteServer: Maybe<ViteDevServer>\n): Promise<RenderFn> => {\n const allExports = viteServer\n ? await viteServer.ssrLoadModule(entrypoint)\n : ((await import(entrypoint)) as unknown);\n\n if (!isValidExports(allExports))\n throw new CretadocServerError(\n SERVER_ERROR_CODE.INTERNAL_SERVER_ERROR,\n 'The server entrypoint must export a render function.'\n );\n\n return allExports.render;\n};\n\n/**\n * Express middleware to render contents using SSR.\n *\n * @param {Omit<SSRConfig, 'route'>} config - The SSR configuration.\n * @param {ViteDevServer} [viteServer] - The Vite server.\n * @returns {RequestHandler}\n */\nexport const renderContents =\n (\n config: Omit<SSRConfig, 'route'>,\n viteServer?: ViteDevServer\n ): RequestHandler =>\n async (req, res, next) => {\n try {\n const render = await importRenderMethod(config.entrypoint, viteServer);\n await render({ req, res, viteServer });\n } catch (error) {\n if (viteServer && error instanceof Error)\n viteServer.ssrFixStacktrace(error);\n\n next(error);\n }\n };\n","import type { Server } from 'http';\nimport type { Maybe, PartialDeep } from '@cretadoc/utils';\nimport express, { type Express } from 'express';\nimport { createServer as createViteServer, type ViteDevServer } from 'vite';\nimport {\n errorHandler,\n loadAPI,\n loadCommonMiddleware,\n loadStaticDir,\n renderContents,\n} from './middleware';\nimport type { CretadocServer, HMRConfig, ServerConfig } from './types';\nimport { ENVIRONMENT } from './utils/constants';\nimport { mergeDefaultConfigWith } from './utils/helpers';\n\n/**\n * Create a Vite server in middleware mode.\n *\n * @param {HMRConfig} [hmr] - The HMR configuration.\n * @returns {Promise<ViteDevServer>} The Vite server.\n */\nconst createDevServer = async (hmr?: HMRConfig): Promise<ViteDevServer> =>\n createViteServer({\n appType: 'custom',\n server: { hmr, middlewareMode: true },\n });\n\ntype ExpressAppConfig = Pick<ServerConfig, 'mode'> &\n Maybe<Pick<ServerConfig, 'api' | 'hmr' | 'ssr' | 'staticDir'>>;\n\n/**\n * Create an Express application.\n *\n * @param {ExpressAppConfig} config - The configuration.\n * @returns {Express} The express app.\n */\nconst createExpressApp = async ({\n api,\n hmr,\n mode,\n ssr,\n staticDir,\n}: ExpressAppConfig): Promise<Express> => {\n const app = express();\n app.disable('x-powered-by');\n\n const viteServer =\n mode === ENVIRONMENT.DEVELOPMENT ? await createDevServer(hmr) : undefined;\n\n app.use(loadCommonMiddleware(mode));\n // cSpell:ignore-word middlewares\n if (api) app.use(api.graphqlEndpoint, loadAPI(api));\n if (viteServer) app.use(viteServer.middlewares);\n if (staticDir) app.use(staticDir.route, loadStaticDir(staticDir));\n if (ssr) app.use(ssr.route, renderContents(ssr, viteServer));\n app.use(errorHandler);\n\n return app;\n};\n\n// Without this additional type, the config type becomes `any` on build.\ntype CreateServer = (\n config?: PartialDeep<ServerConfig>\n) => Promise<CretadocServer>;\n\n/**\n * Create a new server.\n *\n * @param {PartialDeep<ServerConfig>} [config] - The server configuration.\n * @returns {Promise<CretadocServer>} The methods to start/stop the server.\n */\nexport const createServer: CreateServer = async (\n config?: PartialDeep<ServerConfig>\n): Promise<CretadocServer> => {\n const mergedConfig = mergeDefaultConfigWith(config);\n const { hostname, port, ...appConfig } = mergedConfig;\n const app = await createExpressApp(appConfig);\n let server: Maybe<Server> = undefined;\n\n const start = () => {\n server = app.listen(port, () => {\n console.log(`[server]: Server is running at http://${hostname}:${port}`);\n });\n };\n\n const stop = () => {\n server?.close((error) => {\n if (error) console.error(error);\n else console.log('[server]: Server is stopped.');\n });\n };\n\n return {\n config: mergedConfig,\n start,\n stop,\n };\n};\n"],"names":["errorHandler","err","_req","res","next","HTTP_STATUS_CODE","loadAPI","instance","req","loadCommonMiddleware","mode","_res","cors","helmet","error","ENVIRONMENT","DEFAULT_HOSTNAME","DEFAULT_MODE","DEFAULT_PORT","DEFAULT_ENTRYPOINT_FILE","DEFAULT_SSR_ROUTE","DEFAULT_STATIC_ROUTE","DEFAULT_CONFIG","SERVER_ERROR_CODE","CretadocServerError","context","message","ConfigError","mergeStaticDirConfig","userConfig","existsSync","mergeHMRConfig","mergeSSRConfig","mergeDefaultConfigWith","deepFreeze","newConfig","getFilePathFrom","entrypoint","path","join","loadStaticDir","express","isFunction","value","isValidExports","isObject","isObjKeyExist","importRenderMethod","viteServer","allExports","renderContents","config","createDevServer","hmr","createViteServer","createExpressApp","api","ssr","staticDir","app","createServer","mergedConfig","hostname","port","appConfig","server"],"mappings":"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"}