import {createLogger} from "@gongt/ts-stl-library/debug/create-logger"; import {isDebugMode} from "@gongt/ts-stl-library/debug/is-debug-mode"; import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels"; import {RequestError} from "@gongt/ts-stl-library/request/request-error"; import {escapeRegExp} from "@gongt/ts-stl-library/strings/escape-regexp"; import {EventEmitter} from "events"; import * as express from "express"; import {Router} from "express"; import { Application, ErrorRequestHandler, NextFunction, PathParams, Request, RequestHandler, Response, } from "express-serve-static-core"; import {lstatSync, readdirSync} from "fs"; import * as logger from "morgan"; import {resolve} from "path"; import {APP_ROOT_PATH} from "../boot/detect-root"; import {CrossDomainMiddleware} from "../communication/crossdomain/middleware"; import {ExpressHandler} from "../express/base/handler"; import {createServeStatic, ServeStaticEx} from "../express/inject/serve-static"; import {serverRequestOnly} from "../safe/server-request-only"; const debug = createLogger(LOG_LEVEL.INFO, 'express'); const debug_err = createLogger(LOG_LEVEL.ERROR, 'express'); export function createExpressApp() { return new ExpressAppBuilder('/'); } export function createRouterOn(router: ExpressBuilder, path: string) { return new ExpressRouterBuilder(path, router); } export type StaticLocalPath = { localPath: string; // for debug } export type RequestHandlerWithPath = { path: string; handler: RequestHandler; } export abstract class ExpressBuilder extends EventEmitter { protected path: string; protected middlewares: { prepend: RequestHandlerWithPath[], publicDirs: (RequestHandlerWithPath&StaticLocalPath)[], express_handlers: ExpressHandler[], prependAfterPublic: (PathParams|ErrorRequestHandler|RequestHandler)[][], prependBeforePublic: (PathParams|ErrorRequestHandler|RequestHandler)[][], }; protected router: express.Router&any; protected selfIsRoot: boolean; protected rootBuilderObject: ExpressAppBuilder; protected finished: boolean; protected childrens: ExpressBuilder[]; protected routerInitCallback: (router: Application) => void; protected errorHandler: ErrorRequestHandler; protected commonInit() { this.middlewares = { prepend: [], publicDirs: [], express_handlers: [], prependAfterPublic: [], prependBeforePublic: [], }; this.childrens = []; } constructor(path: string, parent: ExpressBuilder) { super(); if (parent && parent.finished) { throw new TypeError(`you can't edit app after generate.`) } this.commonInit(); if (parent) { this.path = resolve(parent.path, path); this.selfIsRoot = false; this.router = Router(); parent.childrens.push(this); this.rootBuilderObject = parent.rootBuilderObject; } else { this.path = resolve('/', path); this.selfIsRoot = true; } } mountPublic(path: string, fsPath: string, serveStaticOptions: ServeStaticEx = {}) { const localPath = resolve(this.rootBuilderObject.serverRootPath, fsPath); debug('serve-static:\n\t\tpath=%s\n\t\tlocal=%s\n\t\toptions=%j', path, localPath, serveStaticOptions); const serveStatic = createServeStatic(localPath, serveStaticOptions); this.middlewares.publicDirs.push({ path: path, handler: serveStatic, localPath: localPath.replace(this.rootBuilderObject.serverRootPath, '.'), }); } prependMiddleware(...handlers: (ErrorRequestHandler|RequestHandler)[]); prependMiddleware(path: PathParams, ...handlers: (ErrorRequestHandler|RequestHandler)[]); prependMiddleware(...handlers: (PathParams|ErrorRequestHandler|RequestHandler)[]) { if (typeof handlers[0] !== 'function') { handlers['_debug_path'] = handlers[0]; } this.middlewares.prependBeforePublic.push(handlers); } registerMiddleware(...handlers: (ErrorRequestHandler|RequestHandler)[]); registerMiddleware(path: PathParams, ...handlers: (ErrorRequestHandler|RequestHandler)[]); registerMiddleware(...handlers: (PathParams|ErrorRequestHandler|RequestHandler)[]) { if (typeof handlers[0] !== 'function') { handlers['_debug_path'] = handlers[0]; } this.middlewares.prependAfterPublic.push(handlers); } registerHandler(handler: ExpressHandler) { if (handler['__mounted']) { throw new Error('mount same handler multiple times: ' + handler.fileName); } handler['__mounted'] = true; handler.debugBaseUrl = this.path; this.middlewares.express_handlers.push(handler); } registerHandlersFromDir(dir: string, decorator?: (e: ExpressHandler) => void) { const dirPath = resolve(this.rootBuilderObject.serverRootPath, dir); readdirSync(dirPath).forEach((file) => { const absFile = resolve(dirPath, file); if (/^\./.test(absFile)) { return; } else if (/\.js$/.test(absFile)) { debug('register from file: %s', absFile); const exhandler: ExpressHandler = require(absFile).default; if (exhandler instanceof ExpressHandler) { if (decorator) { decorator(exhandler); } this.registerHandler(exhandler); } else { const exhandler: ExpressHandler = require(absFile).handler; if (exhandler instanceof ExpressHandler) { if (decorator) { decorator(exhandler); } this.registerHandler(exhandler); } else { debug_err(`file "${absFile}" not export ExpressHandler instance as DEFAULT or {handler}.`); } } } else if (lstatSync(absFile).isDirectory()) { this.registerHandlersFromDir(absFile); } }) } protected finish(): void { if (this.finished) { return; } if (this.routerInitCallback) { this.routerInitCallback(this.router); } const router = this.router; const selfPath = this.path === '/'? '' : this.path; if (this.middlewares.prepend.length) { debug('mounting prepend routers:'); this.middlewares.prepend.forEach(({path, handler}) => { if (path) { debug('\t%s at %s', selfPath, path); router.use(path, handler); } else { debug('\t* at %s', selfPath); router.use(handler); } }); } this.prePublic(); if (this.middlewares.publicDirs.length) { debug('mounting public dirs:'); this.middlewares.publicDirs.forEach(({path, handler, localPath}) => { if (path) { debug('\t%s%s [from] %s', selfPath, path, localPath); router.use(path, handler); } else { debug('\t%s\x1B[2m/\x1B[0m* [from] %s', selfPath, localPath); router.use(handler); } }); } this.preMount(); if (this.middlewares.express_handlers.length) { debug('mounting express controllers:'); this.middlewares.express_handlers.forEach((e) => { debug('\t[%s] on %s', e.constructor['name'], e.url); e.registerRouter(this.router); }); } this.childrens.forEach((e) => { e.finish(); // fixme: not usable - wrong order debug('mount sub router %s', e.path); this.router.use(e.path, e.router) }); if (this.errorHandler) { debug('mount error handler: %s', this.errorHandler.name); this.router.use(this.errorHandler); } this.postMount(); } protected prePublic(): void { debug('prepend before handlers (length=%s)', this.middlewares.prependBeforePublic.length); const selfPath = this.path; this.middlewares.prependBeforePublic.forEach((middlewareList) => { if (middlewareList['_debug_path']) { debug('\t%s\x1B[2m/\x1B[0m%s <-funcCnt:%s', selfPath, middlewareList['_debug_path'], middlewareList.length); } else { debug('\t%s\x1B[2m/\x1B[0m%s <-funcCnt:%s', selfPath, '{*}', middlewareList.length); } this.router.use(...middlewareList); }); } protected preMount(): void { debug('prepend extra handlers (length=%s)', this.middlewares.prependAfterPublic.length); const selfPath = this.path === '/'? '' : this.path; this.middlewares.prependAfterPublic.forEach((middlewareList) => { if (middlewareList['_debug_path']) { debug('\t%s\x1B[2m/\x1B[0m%s <-funcCnt:%s', selfPath, middlewareList['_debug_path'], middlewareList.length); } else { debug('\t%s\x1B[2m/\x1B[0m%s <-funcCnt:%s', selfPath, '{*}', middlewareList.length); } this.router.use(...middlewareList); }); } protected postMount(): void { } } export type CustomLogger = (req: Request, res: Response) => string; export type ViewFunc = (path: string, options: any, callback: (err: Error, html: string) => void) => void; export class ExpressAppBuilder extends ExpressBuilder { protected _rootPath: string; protected logger: RequestHandler; protected errorHandler: ErrorRequestHandler = defaultErrorHandler; private viewEngineSettings: { views?: string; defaultEngine?: string; engines: {name: string; func: ViewFunc;}[]; }; constructor(path: string) { super(path, null); this.logger = logger(':method :url :status - :response-time ms'); this._rootPath = process.cwd(); this.rootBuilderObject = this; this.viewEngineSettings = { engines: [], }; } protected preMount(): void { if (isDebugMode()) { debug('mounting header debugger'); try { const onHeaders = require('on-headers'); this.router.use((req, res, next) => { onHeaders(res, function (this: any) { if (this.__debug_header_trace) { debug_err(DUPLICATE_HEADER_DEBUG_STRING, tryFindLocalFiles(skipLines(res['__debug_header_trace'], 3)), tryFindLocalFiles(skipLines((new Error).stack, 2)), ); } this.__debug_header_trace = (new Error).stack; }); next(); }); } catch (e) { debug('can not mount, no `on-headers\' module'); } } super.preMount(); debug('mounting logger'); this.router.use(this.logger); } protected postMount(): void { debug('root app mounted'); this.commonInit(); } setDefaultLogging(format: string|CustomLogger) { this.logger = logger(format); } setServerRootPath(path) { this._rootPath = path; } setView(viewFolder: string, defaultEngine: string) { this.viewEngineSettings.views = viewFolder; this.viewEngineSettings.defaultEngine = defaultEngine; } addView(name: string, func?: ViewFunc) { this.viewEngineSettings.engines.push({ name: name, func: func, }); if (!this.viewEngineSettings.defaultEngine) { this.viewEngineSettings.defaultEngine = name; } } setCors(path: string = '/public', obj: CrossDomainMiddleware) { this.middlewares.prepend.push({ path: path, handler: obj.getMiddleware(), }); } setFaviconPath(path: string, ttlSecond: number) { const absPath = resolve(this._rootPath, path); const serveFavicon = require("serve-favicon")( absPath, {maxAge: ttlSecond * 1000}, ); this.middlewares.prepend.push({ path: null, handler: serveFavicon, }); } get serverRootPath() { return this._rootPath; } private mountAllPending() { if (this.finished) { return; } this.finish(); } generateApplication(router?: express.Application&any): express.Application { this.router = router || express(); this.emit('pre-app', this.router); this.viewEngineSettings.engines.forEach(({name, func}) => { if (func) { this.router.engine(name, func); } else { this.router.engine(name); } }); this.router.set('view engine', this.viewEngineSettings.defaultEngine || 'ejs'); this.router.set('views', this.viewEngineSettings.views || process.cwd() + '/views'); this.mountAllPending(); this.emit('post-app', this.router); return this.router; } } export class ExpressRouterBuilder extends ExpressBuilder { constructor(path: string, parent: ExpressBuilder) { super(path, parent); } internalProtect(path: string = null) { const ref = { path: path, handler: serverRequestOnly(), }; this.middlewares.prepend.push(ref); return { withKey(newKey: string) { ref.handler = serverRequestOnly(newKey); }, }; } public routerOptions(cb: (router: Application) => void) { this.routerInitCallback = cb; } protected postMount(): void { this.finished = true; delete this.middlewares; delete this.childrens; Object.freeze(this); } } const defErr = createLogger(LOG_LEVEL.SILLY, 'default error'); export function defaultErrorHandler(error: Error|RequestError, req: Request, res: Response, next: NextFunction) { // console.log('-----------default error-------------'); if (error instanceof Error) { defErr('\x1B[2m%s\n%s\x1B[0m', error.stack.split(/\n/g, 4).join('\n'), req.xhr? '(ajax, immediately returned)' : '(not ajax, won\'t handle)'); } else { defErr('response error [%s: %s]', error.code, error.message, req.xhr? '(ajax, immediately returned)' : '(not ajax, won\'t handle)'); } if (res.headersSent) { debug_err('header already sent before.'); if (res['__debug_header_trace']) { debug_err(DUPLICATE_HEADER_DEBUG_STRING, tryFindLocalFiles(skipLines(res['__debug_header_trace'], 3)), tryFindLocalFiles(skipLines((new Error).stack, 2)), ); } else { debug_err('to debug this error, you must :\n\t* install "on-headers" module\n\t* set environment variable "LOG_SILLY=yes"'); } process.exit(1); } if (req.xhr) { return res.send(RequestError.internal(error).response()); } else { next(error); } } const DUPLICATE_HEADER_DEBUG_STRING = `\x1B[38;5;9mduplicate header set:\x1B[0m \x1B[38;5;14m- first set:\x1B[0m \x1B[2m%s\x1B[0m \x1B[38;5;14m- this time:\x1B[0m \x1B[2m%s\x1B[0m`; function skipLines(string: string, lines: number) { return string.split(/\n/g) .slice(lines + 1) .map(l => '\t' + l.trim()) .join('\n'); } function tryFindLocalFiles(str: string) { const root = resolve(APP_ROOT_PATH) + '/'; const reg = new RegExp('^.+' + escapeRegExp(root) + '(?!node_modules).+$', 'gm'); str = str.replace(reg, (m0) => { return `\x1B[0;38;5;6m${m0}\x1B[0;2m`; }); str = str.replace(/\.ts\b/g, (m0) => { return `\x1B[0;38;5;2m${m0}\x1B[0;2m`; }); str = str.replace(/^.+\([^\/)]+\)$/mg, (m0) => { return `\x1B[0;38;5;236m${m0}\x1B[0;2m`; }); str = str.replace(/^.+node_modules\/express.+$/mg, (m0) => { return `\x1B[0;38;5;236m${m0}\x1B[0;2m`; }); return str; }