UNPKG

@tsed/platform-http

Version:
311 lines (310 loc) 11.1 kB
import { isClass, isFunction, isString, nameOf } from "@tsed/core"; import { colors, configuration, constant, createContainer, destroyInjector, injector, logger, ProviderScope, setLoggerConfiguration } from "@tsed/di"; import { $asyncAlter, $asyncEmit } from "@tsed/hooks"; import { getMiddlewaresForHook } from "@tsed/platform-middlewares"; import { application } from "../fn/application.js"; import { Platform } from "../services/Platform.js"; import { PlatformAdapter } from "../services/PlatformAdapter.js"; import { PlatformApplication } from "../services/PlatformApplication.js"; import { closeServer } from "../utils/closeServer.js"; import { createInjector } from "../utils/createInjector.js"; import { getConfiguration } from "../utils/getConfiguration.js"; import { getStaticsOptions } from "../utils/getStaticsOptions.js"; import { printRoutes } from "../utils/printRoutes.js"; import { resolveControllers } from "../utils/resolveControllers.js"; /** * @platform */ export class PlatformBuilder { #rootModule; #promise; #servers; #listeners; constructor(module, settings) { this.startedAt = new Date(); this.current = new Date(); this.#listeners = []; this.#rootModule = module; const configuration = getConfiguration(settings, module); createInjector(configuration); this.log(`Loading ${this.name.toUpperCase()} platform adapter...`); this.createHttpServers(); this.log("Injector created..."); } get name() { return this.adapter.NAME; } get rootModule() { return injector().get(this.#rootModule); } get app() { return injector().get(PlatformApplication); } get platform() { return injector().get(Platform); } get adapter() { return injector().get(PlatformAdapter); } /** * Return the settings configured by the decorator @@Configuration@@. * * ```typescript * @Configuration({ * port: 8000, * httpsPort: 8080, * mount: { * "/rest": "${rootDir}/controllers/**\/*.js" * } * }) * export class Server { * $onInit(){ * console.log(this.settings); // {rootDir, port, httpsPort,...} * } * } * ``` * * @returns {PlatformConfiguration} */ get settings() { return configuration(); } get logger() { return logger(); } get disableBootstrapLog() { return constant("logger.disableBootstrapLog"); } /** * @deprecated use injector() instead of this method. */ get injector() { return injector(); } static create(module, settings) { return this.build(module, { httpsPort: false, httpPort: false, ...settings }); } static build(module, settings) { return new PlatformBuilder(module, settings); } /** * Bootstrap a server application * @param module * @param settings */ static bootstrap(module, settings) { return this.build(module, settings).bootstrap(); } callback(...args) { return this.adapter.app.callback(...args); } log(...data) { return !this.disableBootstrapLog && logger().info(...data, this.diff()); } /** * Add classes decorated by @@Controller@@ to components container. * * ### Example * * ```typescript * @Controller('/ctrl') * class MyController{ * } * * platform.addControllers('/rest', [MyController]) * ``` * * ::: tip * If the MyController class isn't decorated, the class will be ignored. * ::: * * @param {string} endpoint * @param {any[]} controllers */ addControllers(endpoint, controllers) { [].concat(controllers).forEach((token) => { configuration().routes.push({ token, route: endpoint }); }); } async runLifecycle() { // init adapter (Express, Koa, etc...) await this.adapter.onInit(); setLoggerConfiguration(); // create the middleware mapping to be executed to the expected hook await this.mapTokenMiddlewares(); await this.loadInjector(); // add the context middleware to the application this.log("Mount app context"); await this.adapter.useContext(); // init routes (controllers, middlewares, etc...) this.log("Load routes"); await this.adapter.beforeLoadRoutes(); if (this.rootModule.$beforeRoutesInit) { await this.rootModule.$beforeRoutesInit(); // remove method to avoid multiple call and preserve hook order this.rootModule.$beforeRoutesInit = () => { }; } // Hooks execution (adding middlewares, controllers, services, etc...) await this.loadStatics("$beforeRoutesInit"); await this.callHook("$beforeRoutesInit"); const routes = configuration().get("routes"); this.platform.addRoutes(routes); await this.callHook("$onRoutesInit"); await this.loadStatics("$afterRoutesInit"); await this.callHook("$afterRoutesInit"); await this.adapter.afterLoadRoutes(); // map routers are loaded after all hooks because it contains all added middlewares/controllers in the virtual Ts.ED layers // This step will convert all Ts.ED layers to the platform layer (Express or Koa) await this.mapRouters(); // Server is bootstrapped and ready to listen return this; } async loadInjector() { this.log("Build providers"); const settings = configuration(); settings.set("routes", settings.get("routes").concat(resolveControllers(settings))); const container = createContainer(); container.delete(this.#rootModule); container.addProvider(this.#rootModule, { type: "server:module", scope: ProviderScope.SINGLETON }); await injector().load(container); this.log("Settings and injector loaded..."); await this.callHook("$afterInit"); } async listen(network = true) { if (!this.#promise) { await this.bootstrap(); } await this.callHook("$beforeListen"); if (network) { await this.listenServers(); } await this.callHook("$afterListen"); await this.ready(); } async stop() { await destroyInjector(); this.#listeners.map(closeServer); } async ready() { const { startedAt } = this; await this.callHook("$onReady"); this.log(`Started in ${new Date().getTime() - startedAt.getTime()} ms`); } async callHook(hook, ...args) { if (!this.disableBootstrapLog) { logger().debug(`\x1B[1mCall hook ${hook}\x1B[22m`); } // Load middlewares for the given hook this.loadMiddlewaresFor(hook); // call hooks added by providers await $asyncEmit(hook, args); } loadStatics(hook) { const statics = constant("statics"); const app = application(); getStaticsOptions(statics).forEach(({ path, options }) => { if (options.hook === hook) { app.statics(path, options); } }); } useProvider(token, settings) { injector().addProvider(token, settings); return this; } bootstrap() { this.#promise = this.#promise || this.runLifecycle(); return this.#promise; } mapRouters() { const layers = this.platform.getLayers(); this.adapter.mapLayers(layers); const rawBody = constant("rawBody") || layers.some(({ handlers }) => { return handlers.some((handler) => handler.opts?.paramsTypes?.RAW_BODY); }); this.settings.set("rawBody", rawBody); return this.logRoutes(layers.filter((layer) => layer.isProvider())); } diff() { const ms = colors.yellow(`+${new Date().getTime() - this.current.getTime()}ms`); this.current = new Date(); return ms; } /** * Load middlewares from configuration for the given hook * @param hook * @protected */ loadMiddlewaresFor(hook) { return getMiddlewaresForHook(hook, this.settings, "$beforeRoutesInit").forEach(({ use }) => { this.app.use(use); }); } createHttpServers() { this.#servers = this.adapter.getServers(); } async listenServers() { this.#listeners = await Promise.all(this.#servers.map((cb) => cb && cb())); } async logRoutes(layers) { this.log("Routes mounted..."); if (!this.settings.get("logger.disableRoutesSummary") && !this.disableBootstrapLog) { const routes = layers.map((layer) => { return { url: layer.path, method: layer.method, name: layer.opts.name || `${layer.provider.className}.constructor()`, className: layer.opts.className || layer.provider.className, methodClassName: layer.opts.methodClassName || "" }; }); logger().info(printRoutes(await $asyncAlter("$logRoutes", routes))); } } async mapTokenMiddlewares() { let middlewares = constant("middlewares", []); const env = constant("env"); const defaultHook = "$beforeRoutesInit"; const promises = middlewares.map(async (middleware) => { if (isFunction(middleware)) { return { env, hook: defaultHook, use: middleware }; } if (isString(middleware)) { middleware = { env, use: middleware, hook: defaultHook }; } let { use, options } = middleware; if (isString(use)) { if (["text-parser", "raw-parser", "json-parser", "urlencoded-parser"].includes(use)) { use = this.adapter.bodyParser(use.replace("-parser", ""), options); } else { const mod = await import(use); use = (mod.default || mod)(options); } } if (isClass(use) && ["$beforeInit", "$onInit", "$afterInit"].includes(middleware.hook)) { throw new Error(`Ts.ED Middleware "${nameOf(use)}" middleware cannot be added on ${middleware.hook} hook. Use one of this hooks instead: $beforeRoutesInit, $onRoutesInit, $afterRoutesInit, $beforeListen, $afterListen, $onReady`); } return { env, hook: defaultHook, ...middleware, use }; }); middlewares = await Promise.all(promises); configuration().set("middlewares", middlewares.filter((middleware) => middleware.use)); } }