import {Singleton, Inject} from '../di' import {UniversalPath, Collection, Pipe, universalPath} from '../util' import {Unit} from '../lifecycle/Unit' import {Logging} from './Logging' import {Route} from '../http/routing/Route' import {HTTPMethod} from '../http/lifecycle/Request' import {ViewEngineFactory} from '../views/ViewEngineFactory' import {ViewEngine} from '../views/ViewEngine' import {lib} from '../lib' import {Config} from './Config' /** * Application unit that loads the various route files from `app/http/routes` and pre-compiles the route handlers. */ @Singleton() export class Routing extends Unit { @Inject() protected readonly logging!: Logging @Inject() protected readonly config!: Config protected compiledRoutes: Collection = new Collection() public async up(): Promise { this.app().registerFactory(new ViewEngineFactory()) const engine = this.make(ViewEngine) this.logging.verbose('Registering @extollo view engine namespace.') engine.registerNamespace('extollo', lib().concat('resources', 'views')) for await ( const entry of this.path.walk() ) { if ( !entry.endsWith('.routes.js') ) { this.logging.debug(`Skipping routes file with invalid suffix: ${entry}`) continue } this.logging.info(`Importing routes from: ${entry}`) await import(entry) } this.logging.info('Compiling routes...') this.compiledRoutes = new Collection(await Route.compile()) this.logging.info(`Compiled ${this.compiledRoutes.length} route(s).`) this.compiledRoutes.each(route => { this.logging.verbose(`${route}`) }) } /** * Given an HTTPMethod and route path, return the Route instance that matches them, * if one exists. * @param method * @param path */ public match(method: HTTPMethod, path: string): Route | undefined { return this.compiledRoutes.firstWhere(route => { return route.match(method, path) }) } /** * Get the universal path to the root directory of the route definitions. */ public get path(): UniversalPath { return this.app().appPath('http', 'routes') } /** * Get the collection of compiled routes. */ public getCompiled(): Collection { return this.compiledRoutes } /** * Resolve a UniversalPath to a file served as an asset. * @example * ```ts * this.getAssetPath('images', '123.jpg').toRemote // => http://localhost:8000/assets/images/123.jpg * ``` * @param parts */ public getAssetPath(...parts: string[]): UniversalPath { return this.getAssetBase().concat(...parts) } public getAssetBase(): UniversalPath { return this.getAppUrl().concat(this.config.get('server.builtIns.assets.prefix', '/assets')) } public getVendorPath(namespace: string, ...parts: string[]): UniversalPath { return this.getVendorBase().concat(encodeURIComponent(namespace), ...parts) } public getVendorBase(): UniversalPath { return this.getAppUrl().concat(this.config.get('server.builtIns.vendor.prefix', '/vendor')) } public getAppUrl(): UniversalPath { const rawHost = String(this.config.get('server.url', 'http://localhost')).toLowerCase() const isSSL = rawHost.startsWith('https://') const port = this.config.get('server.port', 8000) return Pipe.wrap(rawHost) .unless( host => host.startsWith('http://') || host.startsWith('https'), host => `http://${host}`, ) .when( host => { const hasPort = host.split(':').length > 2 const defaultRaw = !isSSL && port === 80 const defaultSSL = isSSL && port === 443 return !hasPort && !defaultRaw && !defaultSSL }, host => { const parts = host.split('/') parts[2] += `:${port}` return parts.join('/') }, ) .tap(host => universalPath(host)) .get() } }