2021-07-08 03:50:48 +00:00
|
|
|
import {Inject, Singleton} from '../di'
|
2022-01-17 21:57:40 +00:00
|
|
|
import {Awaitable, Collection, ErrorWithContext, Maybe, Pipeline, universalPath, UniversalPath} from '../util'
|
2021-07-08 03:50:48 +00:00
|
|
|
import {Unit, UnitStatus} from '../lifecycle/Unit'
|
2021-06-03 03:36:25 +00:00
|
|
|
import {Logging} from './Logging'
|
|
|
|
import {Route} from '../http/routing/Route'
|
|
|
|
import {HTTPMethod} from '../http/lifecycle/Request'
|
|
|
|
import {ViewEngineFactory} from '../views/ViewEngineFactory'
|
2021-06-24 05:14:04 +00:00
|
|
|
import {ViewEngine} from '../views/ViewEngine'
|
|
|
|
import {lib} from '../lib'
|
2021-06-29 06:44:07 +00:00
|
|
|
import {Config} from './Config'
|
2021-07-08 03:50:48 +00:00
|
|
|
import {PackageDiscovered} from '../support/PackageDiscovered'
|
2021-07-17 17:49:07 +00:00
|
|
|
import {staticServer} from '../http/servers/static'
|
2022-01-27 01:37:54 +00:00
|
|
|
import {Bus} from '../support/bus'
|
2022-06-08 02:56:56 +00:00
|
|
|
import {RequestLocalStorage} from '../http/RequestLocalStorage'
|
2021-03-08 15:00:43 +00:00
|
|
|
|
2021-03-25 13:50:13 +00:00
|
|
|
/**
|
|
|
|
* Application unit that loads the various route files from `app/http/routes` and pre-compiles the route handlers.
|
|
|
|
*/
|
2021-03-08 15:00:43 +00:00
|
|
|
@Singleton()
|
|
|
|
export class Routing extends Unit {
|
|
|
|
@Inject()
|
|
|
|
protected readonly logging!: Logging
|
|
|
|
|
2021-06-29 06:44:07 +00:00
|
|
|
@Inject()
|
|
|
|
protected readonly config!: Config
|
|
|
|
|
2021-07-08 03:50:48 +00:00
|
|
|
@Inject()
|
2022-01-27 01:37:54 +00:00
|
|
|
protected readonly bus!: Bus
|
2021-07-08 03:50:48 +00:00
|
|
|
|
2022-06-08 02:56:56 +00:00
|
|
|
@Inject()
|
|
|
|
protected readonly request!: RequestLocalStorage
|
|
|
|
|
2022-01-19 19:24:59 +00:00
|
|
|
protected compiledRoutes: Collection<Route<unknown, unknown[]>> = new Collection<Route<unknown, unknown[]>>()
|
2021-03-08 15:00:43 +00:00
|
|
|
|
2021-06-03 03:36:25 +00:00
|
|
|
public async up(): Promise<void> {
|
|
|
|
this.app().registerFactory(new ViewEngineFactory())
|
2021-06-24 05:14:04 +00:00
|
|
|
const engine = <ViewEngine> this.make(ViewEngine)
|
|
|
|
this.logging.verbose('Registering @extollo view engine namespace.')
|
|
|
|
engine.registerNamespace('extollo', lib().concat('resources', 'views'))
|
2021-03-09 00:07:55 +00:00
|
|
|
|
2022-07-09 16:01:34 +00:00
|
|
|
const jsSuffix = `.routes.js`
|
|
|
|
const tsSuffix = `.routes.ts`
|
2021-03-08 15:00:43 +00:00
|
|
|
for await ( const entry of this.path.walk() ) {
|
2022-07-09 16:01:34 +00:00
|
|
|
if ( !entry.endsWith(jsSuffix) && !entry.endsWith(tsSuffix) ) {
|
2021-03-08 15:00:43 +00:00
|
|
|
this.logging.debug(`Skipping routes file with invalid suffix: ${entry}`)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
this.logging.info(`Importing routes from: ${entry}`)
|
|
|
|
await import(entry)
|
|
|
|
}
|
|
|
|
|
2021-07-17 17:49:07 +00:00
|
|
|
await this.registerBuiltIns()
|
|
|
|
|
2021-03-08 15:00:43 +00:00
|
|
|
this.logging.info('Compiling routes...')
|
2022-01-19 19:24:59 +00:00
|
|
|
this.compiledRoutes = new Collection<Route<unknown, unknown[]>>(await Route.compile())
|
2021-03-08 15:00:43 +00:00
|
|
|
|
|
|
|
this.logging.info(`Compiled ${this.compiledRoutes.length} route(s).`)
|
|
|
|
this.compiledRoutes.each(route => {
|
|
|
|
this.logging.verbose(`${route}`)
|
|
|
|
})
|
2021-07-08 03:50:48 +00:00
|
|
|
|
2022-01-27 01:37:54 +00:00
|
|
|
await this.bus.subscribe(PackageDiscovered, async (event: PackageDiscovered) => {
|
2021-07-08 03:50:48 +00:00
|
|
|
const loadFrom = event.packageConfig?.extollo?.routes?.loadFrom
|
|
|
|
if ( Array.isArray(loadFrom) ) {
|
|
|
|
for ( const path of loadFrom ) {
|
|
|
|
const loadFile = event.packageJson.concat('..', path)
|
|
|
|
this.logging.debug(`Loading routes for package ${event.packageConfig.name} from ${loadFile}...`)
|
|
|
|
await import(loadFile.toLocal)
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.recompile()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Execute a callback that registers routes and recompiles the routing stack.
|
|
|
|
* @param callback
|
|
|
|
*/
|
|
|
|
public async registerRoutes(callback: () => Awaitable<void>): Promise<void> {
|
|
|
|
await callback()
|
|
|
|
|
|
|
|
if ( this.status === UnitStatus.Started ) {
|
|
|
|
await this.recompile()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recompile registered routes into resolved handler stacks.
|
|
|
|
*/
|
|
|
|
public async recompile(): Promise<void> {
|
|
|
|
this.logging.debug('Recompiling routes...')
|
2022-01-19 19:24:59 +00:00
|
|
|
this.compiledRoutes = this.compiledRoutes.concat(new Collection<Route<unknown, unknown[]>>(await Route.compile()))
|
2021-07-08 03:50:48 +00:00
|
|
|
|
|
|
|
this.logging.debug(`Re-compiled ${this.compiledRoutes.length} route(s).`)
|
|
|
|
this.compiledRoutes.each(route => {
|
|
|
|
this.logging.verbose(`${route}`)
|
|
|
|
})
|
2021-03-08 15:00:43 +00:00
|
|
|
}
|
|
|
|
|
2021-03-25 13:50:13 +00:00
|
|
|
/**
|
|
|
|
* Given an HTTPMethod and route path, return the Route instance that matches them,
|
|
|
|
* if one exists.
|
|
|
|
* @param method
|
|
|
|
* @param path
|
|
|
|
*/
|
2022-07-14 02:35:18 +00:00
|
|
|
public match(method: 'ws' | HTTPMethod, path: string): Route<unknown, unknown[]> | undefined {
|
2021-03-08 15:00:43 +00:00
|
|
|
return this.compiledRoutes.firstWhere(route => {
|
|
|
|
return route.match(method, path)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-03-25 13:50:13 +00:00
|
|
|
/**
|
|
|
|
* Get the universal path to the root directory of the route definitions.
|
|
|
|
*/
|
2021-03-08 15:00:43 +00:00
|
|
|
public get path(): UniversalPath {
|
|
|
|
return this.app().appPath('http', 'routes')
|
|
|
|
}
|
2021-06-18 00:35:31 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the collection of compiled routes.
|
|
|
|
*/
|
2022-01-19 19:24:59 +00:00
|
|
|
public getCompiled(): Collection<Route<unknown, unknown[]>> {
|
2021-06-18 00:35:31 +00:00
|
|
|
return this.compiledRoutes
|
|
|
|
}
|
2021-06-29 06:44:07 +00:00
|
|
|
|
2021-07-03 02:44:34 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-06-29 06:44:07 +00:00
|
|
|
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 {
|
2021-07-08 03:50:48 +00:00
|
|
|
return this.getVendorBase().concat(namespace, ...parts)
|
2021-06-29 06:44:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public getVendorBase(): UniversalPath {
|
|
|
|
return this.getAppUrl().concat(this.config.get('server.builtIns.vendor.prefix', '/vendor'))
|
|
|
|
}
|
|
|
|
|
2021-07-17 17:49:07 +00:00
|
|
|
public getNamedPath(name: string): UniversalPath {
|
|
|
|
const route = this.getByName(name)
|
|
|
|
if ( route ) {
|
|
|
|
return this.getAppUrl().concat(route.getRoute())
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new ErrorWithContext(`Route does not exist with name: ${name}`, {
|
|
|
|
routeName: name,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
public hasNamedRoute(name: string): boolean {
|
|
|
|
return Boolean(this.getByName(name))
|
|
|
|
}
|
|
|
|
|
2022-01-19 19:24:59 +00:00
|
|
|
public getByName(name: string): Maybe<Route<unknown, unknown[]>> {
|
2021-07-17 17:49:07 +00:00
|
|
|
return this.compiledRoutes
|
2022-01-19 19:24:59 +00:00
|
|
|
.firstWhere(route => route.hasAlias(name))
|
2021-07-17 17:49:07 +00:00
|
|
|
}
|
|
|
|
|
2021-06-29 06:44:07 +00:00
|
|
|
public getAppUrl(): UniversalPath {
|
2022-06-08 03:45:16 +00:00
|
|
|
const forceSsl = Boolean(this.config.get('server.forceSsl', false))
|
|
|
|
|
2022-06-08 02:56:56 +00:00
|
|
|
if ( this.request.has() ) {
|
2022-06-08 03:03:05 +00:00
|
|
|
// If we have a request context, use that URL, as it is the most reliable
|
2022-06-08 02:56:56 +00:00
|
|
|
const request = this.request.get()
|
2022-06-08 03:03:05 +00:00
|
|
|
const url = new URL(request.fullUrl)
|
2022-06-08 03:45:16 +00:00
|
|
|
if ( forceSsl ) {
|
|
|
|
url.protocol = 'https:'
|
|
|
|
}
|
2022-06-08 03:03:05 +00:00
|
|
|
return universalPath(url.origin)
|
2022-06-08 02:56:56 +00:00
|
|
|
}
|
|
|
|
|
2022-06-08 03:03:05 +00:00
|
|
|
const urlOverride = String(this.config.get('server.url', '')).toLowerCase()
|
|
|
|
if ( urlOverride ) {
|
|
|
|
// If the config has a URL override explicitly set, use that
|
|
|
|
return universalPath(urlOverride)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, try to build one from the host and port
|
|
|
|
const rawHost = String(this.config.get('server.host', 'http://localhost')).toLowerCase()
|
2021-06-29 06:44:07 +00:00
|
|
|
const isSSL = rawHost.startsWith('https://')
|
|
|
|
const port = this.config.get('server.port', 8000)
|
|
|
|
|
2022-01-17 21:57:40 +00:00
|
|
|
return Pipeline.id<string>()
|
2021-06-29 06:44:07 +00:00
|
|
|
.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<UniversalPath>(host => universalPath(host))
|
2022-01-17 21:57:40 +00:00
|
|
|
.apply(rawHost)
|
2021-06-29 06:44:07 +00:00
|
|
|
}
|
2021-07-17 17:49:07 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Register an asset directory for the given package.
|
|
|
|
* This creates a static server route for the package with the
|
|
|
|
* configured vendor prefix.
|
|
|
|
* @param packageName
|
|
|
|
* @param basePath
|
|
|
|
*/
|
|
|
|
public async registerVendorAssets(packageName: string, basePath: UniversalPath): Promise<void> {
|
|
|
|
if ( this.config.get('server.builtIns.vendor.enabled', true) ) {
|
|
|
|
this.logging.debug(`Registering vendor assets route for package ${packageName} on ${basePath}...`)
|
|
|
|
await this.registerRoutes(() => {
|
|
|
|
const prefix = this.config.get('server.builtIns.vendor.prefix', '/vendor')
|
|
|
|
Route.group(prefix, () => {
|
|
|
|
Route.group(packageName, () => {
|
2022-01-19 19:24:59 +00:00
|
|
|
Route.get('/**')
|
|
|
|
.passingRequest()
|
|
|
|
.handledBy(staticServer({
|
|
|
|
basePath,
|
|
|
|
directoryListing: false,
|
|
|
|
}))
|
2021-07-17 17:49:07 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Register built-in servers and routes. */
|
|
|
|
protected async registerBuiltIns(): Promise<void> {
|
|
|
|
const extolloAssets = universalPath(__dirname, '..', 'resources', 'assets')
|
|
|
|
await this.registerVendorAssets('@extollo/lib', extolloAssets)
|
|
|
|
|
|
|
|
this.bus.subscribe(PackageDiscovered, async (event: PackageDiscovered) => {
|
|
|
|
if ( event.packageConfig?.extollo?.assets?.discover && event.packageConfig.name ) {
|
|
|
|
this.logging.debug(`Registering vendor assets for discovered package: ${event.packageConfig.name}`)
|
|
|
|
const basePath = event.packageConfig?.extollo?.assets?.basePath
|
|
|
|
if ( basePath && Array.isArray(basePath) ) {
|
|
|
|
const assetPath = event.packageJson.concat('..', ...basePath)
|
|
|
|
await this.registerVendorAssets(event.packageConfig.name, assetPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if ( this.config.get('server.builtIns.assets.enabled', true) ) {
|
|
|
|
const prefix = this.config.get('server.builtIns.assets.prefix', '/assets')
|
|
|
|
this.logging.debug(`Registering built-in assets server with prefix: ${prefix}`)
|
|
|
|
await this.registerRoutes(() => {
|
|
|
|
Route.group(prefix, () => {
|
2022-01-19 19:24:59 +00:00
|
|
|
Route.get('/**')
|
|
|
|
.passingRequest()
|
|
|
|
.handledBy(staticServer({
|
|
|
|
directoryListing: false,
|
|
|
|
basePath: ['resources', 'assets'],
|
|
|
|
}))
|
2021-07-17 17:49:07 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-03-08 15:00:43 +00:00
|
|
|
}
|