@tdb/web
Version:
Common condiguration for serving a web-site and testing web-based UI components.
213 lines (187 loc) • 5.86 kB
text/typescript
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();
};