UNPKG

@tdb/web

Version:

Common condiguration for serving a web-site and testing web-based UI components.

213 lines (187 loc) 5.86 kB
import { Express, RequestHandler, Router, static as staticMiddleware, } from 'express'; import { parse as parseUrl } from 'url'; import { bodyParser, constants, cookieParser, cors, express, fsPath, IWebApp, log, next, React, } from './common'; import { fileHandler } from './handler.file'; import { redirectHandler } from './handler.redirect'; import { IWebAppOptions, RedirectPath } from './types'; /** * A web-application configuration. */ export class WebApp implements IWebApp { public static init(options: IWebAppOptions = {}) { return new WebApp(options); } public isStarted = false; public readonly dev: boolean; public readonly port: number; public readonly silent: boolean; public readonly staticPath: string; public readonly dir: string; public readonly router = Router(); private _server: { express: Express; next: next.Server; }; private _render: Array<{ urlPath: string; pagePath?: string }> = []; private constructor(options: IWebAppOptions) { // Store default values. this.dev = optionValue<boolean>('dev', constants.IS_DEV, options); this.port = optionValue<number>('port', 3000, options); this.silent = optionValue<boolean>('silent', false, options); this.staticPath = optionValue<string>('static', './static', options); this.dir = optionValue<string>('dir', './lib', options); } private get server() { if (!this._server) { const nextApp = next({ dev: this.dev, dir: this.dir }); const expressApp = express .app() .use(bodyParser.json({})) .use(cookieParser()) .use(staticMiddleware(fsPath.resolve(this.staticPath))) .use(cors({})) .use(this.router); this._server = { express: expressApp, next: nextApp, }; } return this._server; } private restHandler(verb: 'get' | 'put' | 'post' | 'delete') { return (url: string, handler: RequestHandler) => { this.router[verb](url, handler); return this; }; } public get = this.restHandler('get'); public put = this.restHandler('put'); public post = this.restHandler('post'); public delete = this.restHandler('delete'); public use(...handlers: Array<RequestHandler | IWebApp>) { // Merge in express-handlers. handlers .filter(item => !(item instanceof WebApp)) .map(item => item as RequestHandler) .forEach(handler => this.router.use(handler)); // Merge in another web-app's router and render mappings. handlers .filter(item => item instanceof WebApp) .map(app => app as WebApp) .forEach(app => { this.router.use(app.router); this._render = [...this._render, ...app._render]; }); return this; } public redirect(fromPath: string, toPath: RedirectPath | string) { return this.use(redirectHandler(fromPath, toPath)); } public file(urlPath: string, filePath: string) { return this.use(fileHandler(urlPath, filePath)); } public static(urlPath: string, dirPath: string) { const middleware = staticMiddleware(fsPath.resolve(dirPath)); this.router.use(urlPath, middleware); return this; } public render(urlPath: string, pagePath?: string) { this._render = [...this._render, { urlPath, pagePath }]; return this; } public async start(port?: number) { // Setup initial conditions. if (this.isStarted) { return this; } this.isStarted = true; // Setup custom URL routes that render NextJS pages. this._render.forEach(({ urlPath, pagePath }) => { this.router.get(urlPath, (req, res) => { const url = parseUrl(req.url, true); const pathname = pagePath || urlPath; const query = { ...req.params, ...req.query }; this.server.next.render(req, res, pathname, query, url); }); }); // Prepare the NextJS app. const handle = this.server.next.getRequestHandler(); this.router.get('*', (req, res) => handle(req, res)); await this.server.next.prepare(); // Start the express server. port = port === undefined ? this.port : port; await listen(this, this.server.express, port); return this; } } /** * INTERNAL */ function optionValue<T>( key: keyof IWebAppOptions, defaultValue: T, options: IWebAppOptions, ): T { return options && options[key] !== undefined ? (options[key] as any) : defaultValue; } const listen = (app: WebApp, express: Express, port: number) => { return new Promise((resolve, reject) => { express.listen(port, (err: Error) => { if (err) { reject(err); } else { logStarted(app, port); resolve(); } }); }); }; const logStarted = (app: WebApp, port: number) => { if (app.silent) { return; } // Log application details. const PACKAGE = require(fsPath.resolve('./package.json')); const LIB_PACKAGE = require(fsPath.join(__dirname, '../../package.json')); log.info(`> Ready on ${log.cyan('localhost')}:${log.magenta(port)}`); log.info(); log.info.gray(` name: ${log.white(PACKAGE.name)}@${PACKAGE.version}`); log.info.gray(` dev: ${app.dev}`); log.info.gray(` dir: ${app.dir}`); log.info.gray(` static: ${app.staticPath}`); log.info.gray(` react: ${React.version}`); log.info.gray(` next: ${LIB_PACKAGE.dependencies.next}`); // Log routes. const routes = express.routes(app.router); if (routes.length > 0) { log.info(); log.info.cyan(` Routes:`); routes .map(route => route.methods.map(method => ({ method, path: route.path }))) .reduce((acc, next) => [...acc, ...next], []) .forEach(route => { const method = `${route.method} `.substr(0, 8); log.info(` ${log.magenta(method)} ${route.path}`); }); } // Finish up. log.info(); };