Add support for registering vendor asset routes
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
39d97d6e14
commit
e33d8dee8f
@ -1,4 +1,4 @@
|
|||||||
import {Singleton, StaticClass} from '../di'
|
import {Instantiable, Singleton, StaticClass} from '../di'
|
||||||
import {Bus, Dispatchable, EventSubscriber, EventSubscriberEntry, EventSubscription} from './types'
|
import {Bus, Dispatchable, EventSubscriber, EventSubscriberEntry, EventSubscription} from './types'
|
||||||
import {Awaitable, Collection, uuid4} from '../util'
|
import {Awaitable, Collection, uuid4} from '../util'
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export class EventBus implements Bus {
|
|||||||
*/
|
*/
|
||||||
protected subscribers: Collection<EventSubscriberEntry<any>> = new Collection<EventSubscriberEntry<any>>()
|
protected subscribers: Collection<EventSubscriberEntry<any>> = new Collection<EventSubscriberEntry<any>>()
|
||||||
|
|
||||||
subscribe<T extends Dispatchable>(event: StaticClass<T, T>, subscriber: EventSubscriber<T>): Awaitable<EventSubscription> {
|
subscribe<T extends Dispatchable>(event: StaticClass<T, Instantiable<T>>, subscriber: EventSubscriber<T>): Awaitable<EventSubscription> {
|
||||||
const entry: EventSubscriberEntry<T> = {
|
const entry: EventSubscriberEntry<T> = {
|
||||||
id: uuid4(),
|
id: uuid4(),
|
||||||
event,
|
event,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Awaitable, Rehydratable} from '../util'
|
import {Awaitable, Rehydratable} from '../util'
|
||||||
import {StaticClass} from '../di'
|
import {Instantiable, StaticClass} from '../di'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A closure that should be executed with the given event is fired.
|
* A closure that should be executed with the given event is fired.
|
||||||
@ -14,7 +14,7 @@ export interface EventSubscriberEntry<T extends Dispatchable> {
|
|||||||
id: string
|
id: string
|
||||||
|
|
||||||
/** The event class subscribed to. */
|
/** The event class subscribed to. */
|
||||||
event: StaticClass<T, T>
|
event: StaticClass<T, Instantiable<T>>
|
||||||
|
|
||||||
/** The closure to execute when the event is fired. */
|
/** The closure to execute when the event is fired. */
|
||||||
subscriber: EventSubscriber<T>
|
subscriber: EventSubscriber<T>
|
||||||
@ -41,7 +41,7 @@ export interface Dispatchable extends Rehydratable {
|
|||||||
* An event-driven bus that manages subscribers and dispatched items.
|
* An event-driven bus that manages subscribers and dispatched items.
|
||||||
*/
|
*/
|
||||||
export interface Bus {
|
export interface Bus {
|
||||||
subscribe<T extends Dispatchable>(eventClass: StaticClass<T, T>, subscriber: EventSubscriber<T>): Awaitable<EventSubscription>
|
subscribe<T extends Dispatchable>(eventClass: StaticClass<T, Instantiable<T>>, subscriber: EventSubscriber<T>): Awaitable<EventSubscription>
|
||||||
unsubscribe<T extends Dispatchable>(subscriber: EventSubscriber<T>): Awaitable<void>
|
unsubscribe<T extends Dispatchable>(subscriber: EventSubscriber<T>): Awaitable<void>
|
||||||
dispatch(event: Dispatchable): Awaitable<void>
|
dispatch(event: Dispatchable): Awaitable<void>
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {ResponseFactory} from './ResponseFactory'
|
import {ResponseFactory} from './ResponseFactory'
|
||||||
import {Request} from '../lifecycle/Request'
|
import {Request} from '../lifecycle/Request'
|
||||||
import {ErrorWithContext, UniversalPath} from '../../util'
|
import {ErrorWithContext, UniversalPath} from '../../util'
|
||||||
|
import {Logging} from '../../service/Logging'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function that creates a FileResponseFactory for the given path.
|
* Helper function that creates a FileResponseFactory for the given path.
|
||||||
@ -28,6 +29,7 @@ export class FileResponseFactory extends ResponseFactory {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request.make<Logging>(Logging).debug(`Setting Content-Type of ${this.path} to ${this.path.contentType}...`)
|
||||||
request.response.setHeader('Content-Type', this.path.contentType || 'application/octet-stream')
|
request.response.setHeader('Content-Type', this.path.contentType || 'application/octet-stream')
|
||||||
request.response.setHeader('Content-Length', String(await this.path.sizeInBytes()))
|
request.response.setHeader('Content-Length', String(await this.path.sizeInBytes()))
|
||||||
request.response.body = await this.path.readStream()
|
request.response.body = await this.path.readStream()
|
||||||
|
@ -156,6 +156,15 @@ export function staticServer(options: StaticServerOptions = {}): RouteHandler {
|
|||||||
|
|
||||||
// If the resolved path is a directory, send the directory listing response
|
// If the resolved path is a directory, send the directory listing response
|
||||||
if ( await filePath.isDirectory() ) {
|
if ( await filePath.isDirectory() ) {
|
||||||
|
if ( !options.directoryListing ) {
|
||||||
|
throw new StaticServerHTTPError(HTTPStatus.NOT_FOUND, 'File not found', {
|
||||||
|
basePath: basePath.toString(),
|
||||||
|
filePath: filePath.toString(),
|
||||||
|
route: route.path,
|
||||||
|
reason: 'Path is a directory, and directory listing is disabled',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if ( !route.path.endsWith('/') ) {
|
if ( !route.path.endsWith('/') ) {
|
||||||
return redirect(`${route.path}/`)
|
return redirect(`${route.path}/`)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {ModelKey, QueryRow, QuerySource} from '../types'
|
import {ModelKey, QueryRow, QuerySource} from '../types'
|
||||||
import {Container, Inject, StaticClass} from '../../di'
|
import {Container, Inject, Instantiable, StaticClass} from '../../di'
|
||||||
import {DatabaseService} from '../DatabaseService'
|
import {DatabaseService} from '../DatabaseService'
|
||||||
import {ModelBuilder} from './ModelBuilder'
|
import {ModelBuilder} from './ModelBuilder'
|
||||||
import {getFieldsMeta, ModelField} from './Field'
|
import {getFieldsMeta, ModelField} from './Field'
|
||||||
@ -804,7 +804,7 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
|||||||
(this as any)[thisFieldName] = object[objectFieldName]
|
(this as any)[thisFieldName] = object[objectFieldName]
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe<EventT extends Dispatchable>(event: StaticClass<EventT, EventT>, subscriber: EventSubscriber<EventT>): Awaitable<EventSubscription> {
|
subscribe<EventT extends Dispatchable>(event: StaticClass<EventT, Instantiable<EventT>>, subscriber: EventSubscriber<EventT>): Awaitable<EventSubscription> {
|
||||||
const entry: EventSubscriberEntry<EventT> = {
|
const entry: EventSubscriberEntry<EventT> = {
|
||||||
id: uuid4(),
|
id: uuid4(),
|
||||||
event,
|
event,
|
||||||
|
0
src/resources/assets/.gitkeep
Normal file
0
src/resources/assets/.gitkeep
Normal file
@ -1,5 +1,5 @@
|
|||||||
import {Inject, Singleton} from '../di'
|
import {Inject, Singleton} from '../di'
|
||||||
import {HTTPStatus, withTimeout} from '../util'
|
import {HTTPStatus, universalPath, UniversalPath, withTimeout} from '../util'
|
||||||
import {Unit} from '../lifecycle/Unit'
|
import {Unit} from '../lifecycle/Unit'
|
||||||
import {createServer, IncomingMessage, RequestListener, Server, ServerResponse} from 'http'
|
import {createServer, IncomingMessage, RequestListener, Server, ServerResponse} from 'http'
|
||||||
import {Logging} from './Logging'
|
import {Logging} from './Logging'
|
||||||
@ -17,6 +17,11 @@ import {ExecuteResolvedRoutePostflightHTTPModule} from '../http/kernel/module/Ex
|
|||||||
import {ParseIncomingBodyHTTPModule} from '../http/kernel/module/ParseIncomingBodyHTTPModule'
|
import {ParseIncomingBodyHTTPModule} from '../http/kernel/module/ParseIncomingBodyHTTPModule'
|
||||||
import {Config} from './Config'
|
import {Config} from './Config'
|
||||||
import {InjectRequestEventBusHTTPModule} from '../http/kernel/module/InjectRequestEventBusHTTPModule'
|
import {InjectRequestEventBusHTTPModule} from '../http/kernel/module/InjectRequestEventBusHTTPModule'
|
||||||
|
import {Routing} from './Routing'
|
||||||
|
import {Route} from '../http/routing/Route'
|
||||||
|
import {staticServer} from '../http/servers/static'
|
||||||
|
import {EventBus} from '../event/EventBus'
|
||||||
|
import {PackageDiscovered} from '../support/PackageDiscovered'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application unit that starts the HTTP/S server, creates Request and Response objects
|
* Application unit that starts the HTTP/S server, creates Request and Response objects
|
||||||
@ -33,9 +38,39 @@ export class HTTPServer extends Unit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly kernel!: HTTPKernel
|
protected readonly kernel!: HTTPKernel
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly routing!: Routing
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly bus!: EventBus
|
||||||
|
|
||||||
/** The underlying native Node.js server. */
|
/** The underlying native Node.js server. */
|
||||||
protected server?: Server
|
protected server?: Server
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.routing.registerRoutes(() => {
|
||||||
|
const prefix = this.config.get('server.builtIns.vendor.prefix', '/vendor')
|
||||||
|
Route.group(prefix, () => {
|
||||||
|
Route.group(packageName, () => {
|
||||||
|
Route.get('/**', staticServer({
|
||||||
|
basePath,
|
||||||
|
directoryListing: false,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async up(): Promise<void> {
|
public async up(): Promise<void> {
|
||||||
const port = this.config.get('server.port', 8000)
|
const port = this.config.get('server.port', 8000)
|
||||||
|
|
||||||
@ -51,6 +86,8 @@ export class HTTPServer extends Unit {
|
|||||||
ParseIncomingBodyHTTPModule.register(this.kernel)
|
ParseIncomingBodyHTTPModule.register(this.kernel)
|
||||||
InjectRequestEventBusHTTPModule.register(this.kernel)
|
InjectRequestEventBusHTTPModule.register(this.kernel)
|
||||||
|
|
||||||
|
await this.registerBuiltIns()
|
||||||
|
|
||||||
await new Promise<void>(res => {
|
await new Promise<void>(res => {
|
||||||
this.server = createServer(this.handler)
|
this.server = createServer(this.handler)
|
||||||
|
|
||||||
@ -111,4 +148,34 @@ export class HTTPServer extends Unit {
|
|||||||
await extolloReq.response.send()
|
await extolloReq.response.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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.routing.registerRoutes(() => {
|
||||||
|
Route.group(prefix, () => {
|
||||||
|
Route.get('/**', staticServer({
|
||||||
|
directoryListing: false,
|
||||||
|
basePath: ['resources', 'assets'],
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {Singleton, Inject} from '../di'
|
import {Inject, Singleton} from '../di'
|
||||||
import {UniversalPath, Collection, Pipe, universalPath} from '../util'
|
import {Awaitable, Collection, Pipe, universalPath, UniversalPath} from '../util'
|
||||||
import {Unit} from '../lifecycle/Unit'
|
import {Unit, UnitStatus} from '../lifecycle/Unit'
|
||||||
import {Logging} from './Logging'
|
import {Logging} from './Logging'
|
||||||
import {Route} from '../http/routing/Route'
|
import {Route} from '../http/routing/Route'
|
||||||
import {HTTPMethod} from '../http/lifecycle/Request'
|
import {HTTPMethod} from '../http/lifecycle/Request'
|
||||||
@ -8,6 +8,8 @@ import {ViewEngineFactory} from '../views/ViewEngineFactory'
|
|||||||
import {ViewEngine} from '../views/ViewEngine'
|
import {ViewEngine} from '../views/ViewEngine'
|
||||||
import {lib} from '../lib'
|
import {lib} from '../lib'
|
||||||
import {Config} from './Config'
|
import {Config} from './Config'
|
||||||
|
import {EventBus} from '../event/EventBus'
|
||||||
|
import {PackageDiscovered} from '../support/PackageDiscovered'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application unit that loads the various route files from `app/http/routes` and pre-compiles the route handlers.
|
* Application unit that loads the various route files from `app/http/routes` and pre-compiles the route handlers.
|
||||||
@ -20,6 +22,9 @@ export class Routing extends Unit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly config!: Config
|
protected readonly config!: Config
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly bus!: EventBus
|
||||||
|
|
||||||
protected compiledRoutes: Collection<Route> = new Collection<Route>()
|
protected compiledRoutes: Collection<Route> = new Collection<Route>()
|
||||||
|
|
||||||
public async up(): Promise<void> {
|
public async up(): Promise<void> {
|
||||||
@ -45,6 +50,44 @@ export class Routing extends Unit {
|
|||||||
this.compiledRoutes.each(route => {
|
this.compiledRoutes.each(route => {
|
||||||
this.logging.verbose(`${route}`)
|
this.logging.verbose(`${route}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.bus.subscribe(PackageDiscovered, async (event: PackageDiscovered) => {
|
||||||
|
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...')
|
||||||
|
this.compiledRoutes = this.compiledRoutes.concat(new Collection<Route>(await Route.compile()))
|
||||||
|
|
||||||
|
this.logging.debug(`Re-compiled ${this.compiledRoutes.length} route(s).`)
|
||||||
|
this.compiledRoutes.each(route => {
|
||||||
|
this.logging.verbose(`${route}`)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,7 +133,7 @@ export class Routing extends Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getVendorPath(namespace: string, ...parts: string[]): UniversalPath {
|
public getVendorPath(namespace: string, ...parts: string[]): UniversalPath {
|
||||||
return this.getVendorBase().concat(encodeURIComponent(namespace), ...parts)
|
return this.getVendorBase().concat(namespace, ...parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
public getVendorBase(): UniversalPath {
|
public getVendorBase(): UniversalPath {
|
||||||
|
@ -11,6 +11,13 @@ export interface ExtolloPackageDiscoveryConfig {
|
|||||||
recursiveDependencies?: {
|
recursiveDependencies?: {
|
||||||
discover?: boolean,
|
discover?: boolean,
|
||||||
},
|
},
|
||||||
|
routes?: {
|
||||||
|
loadFrom?: string[],
|
||||||
|
},
|
||||||
|
assets?: {
|
||||||
|
discover?: boolean,
|
||||||
|
basePath?: string[],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,14 +497,14 @@ export class UniversalPath {
|
|||||||
* Get the mime-type of this resource.
|
* Get the mime-type of this resource.
|
||||||
*/
|
*/
|
||||||
get mimeType(): string | false {
|
get mimeType(): string | false {
|
||||||
return mime.lookup(this.resourceLocalPath)
|
return mime.lookup(this.toBase)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content-type header of this resource.
|
* Get the content-type header of this resource.
|
||||||
*/
|
*/
|
||||||
get contentType(): string | false {
|
get contentType(): string | false {
|
||||||
return mime.contentType(this.resourceLocalPath)
|
return mime.contentType(this.toBase)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user