Add support for view engines, PNPM

This commit is contained in:
2021-03-08 18:07:55 -06:00
parent 4ecada6be8
commit b820b35b70
12 changed files with 814 additions and 3 deletions

View File

@@ -4,7 +4,7 @@ import {HTTPKernelModule} from "./HTTPKernelModule";
import {Logging} from "../../service/Logging";
import {AppClass} from "../../lifecycle/AppClass";
import {Request} from "../lifecycle/Request";
import {http} from "../response/HTTPErrorResponseFactory";
import {error} from "../response/ErrorResponseFactory";
/**
* Interface for fluently registering kernel modules into the kernel.
@@ -73,7 +73,7 @@ export class HTTPKernel extends AppClass {
}
} catch (e: any) {
this.logging.error(e)
await http(HTTPStatus.REQUEST_TIMEOUT).write(request)
await error(e).status(HTTPStatus.INTERNAL_SERVER_ERROR).write(request)
}
this.logging.verbose('Finished kernel lifecycle')

View File

@@ -0,0 +1,22 @@
import {Container} from "@extollo/di";
import {ResponseFactory} from "./ResponseFactory";
import {Request} from "../lifecycle/Request";
import {ViewEngine} from "../../views/ViewEngine";
export function view(name: string, data?: {[key: string]: any}): ViewResponseFactory {
return new ViewResponseFactory(name, data)
}
export class ViewResponseFactory extends ResponseFactory {
constructor(
public readonly viewName: string,
public readonly data?: {[key: string]: any}
) { super() }
public async write(request: Request) {
const viewEngine = <ViewEngine> Container.getContainer().make(ViewEngine)
request.response.body = await viewEngine.renderByName(this.viewName, this.data || {})
request.response.setHeader('Content-Type', 'text/html; charset=utf-8')
return request
}
}

View File

@@ -29,6 +29,7 @@ export * from './http/response/JSONResponseFactory'
export * from './http/response/ResponseFactory'
export * from './http/response/StringResponseFactory'
export * from './http/response/TemporaryRedirectResponseFactory'
export * from './http/response/ViewResponseFactory'
export * from './http/routing/ActivatedRoute'
export * from './http/routing/Route'
@@ -51,3 +52,7 @@ export * from './service/Config'
export * from './service/Controllers'
export * from './service/HTTPServer'
export * from './service/Routing'
export * from './views/ViewEngine'
export * from './views/ViewEngineFactory'
export * from './views/PugViewEngine'

View File

@@ -6,4 +6,12 @@ export class Config extends CanonicalRecursive {
protected appPath: string[] = ['configs']
protected suffix: string = '.config.js'
protected canonicalItem: string = 'config'
public async up() {
await super.up()
if ( this.get('server.debug', false) ) {
Error.stackTraceLimit = Infinity
}
}
}

View File

@@ -11,6 +11,7 @@ import {InjectSessionHTTPModule} from "../http/kernel/module/InjectSessionHTTPMo
import {PersistSessionHTTPModule} from "../http/kernel/module/PersistSessionHTTPModule";
import {MountActivatedRouteHTTPModule} from "../http/kernel/module/MountActivatedRouteHTTPModule";
import {ExecuteResolvedRouteHandlerHTTPModule} from "../http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule";
import {error} from "../http/response/ErrorResponseFactory";
@Singleton()
export class HTTPServer extends Unit {
@@ -76,7 +77,12 @@ export class HTTPServer extends Unit {
.run()
.catch(e => this.logging.error(e))
await this.kernel.handle(extolloReq)
try {
await this.kernel.handle(extolloReq)
} catch (e) {
await error(e).write(extolloReq)
}
await extolloReq.response.send()
}
}

View File

@@ -4,6 +4,7 @@ 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";
@Singleton()
export class Routing extends Unit {
@@ -13,6 +14,8 @@ export class Routing extends Unit {
protected compiledRoutes: Collection<Route> = new Collection<Route>()
public async up() {
this.app().registerFactory(new ViewEngineFactory());
for await ( const entry of this.path.walk() ) {
if ( !entry.endsWith('.routes.js') ) {
this.logging.debug(`Skipping routes file with invalid suffix: ${entry}`)

View File

@@ -0,0 +1,33 @@
import {ViewEngine} from "./ViewEngine"
import {Injectable} from "@extollo/di"
import * as pug from "pug"
@Injectable()
export class PugViewEngine extends ViewEngine {
protected compileCache: {[key: string]: ((locals?: pug.LocalsObject) => string)} = {}
public renderString(templateString: string, locals: { [p: string]: any }): string | Promise<string> {
return pug.compile(templateString, this.getOptions())(locals)
}
public renderByName(templateName: string, locals: { [p: string]: any }): string | Promise<string> {
let compiled = this.compileCache[templateName]
if ( compiled ) return compiled(locals)
if ( !templateName.endsWith('.pug') ) templateName += '.pug'
const filePath = this.path.concat(...templateName.split(':'))
compiled = pug.compileFile(filePath.toLocal, this.getOptions())
this.compileCache[templateName] = compiled
return compiled(locals)
}
protected getOptions() {
return {
basedir: this.path.toLocal,
debug: this.debug,
compileDebug: this.debug,
globals: [],
}
}
}

23
src/views/ViewEngine.ts Normal file
View File

@@ -0,0 +1,23 @@
import {AppClass} from "../lifecycle/AppClass"
import {Config} from "../service/Config"
import {Container} from "@extollo/di"
import {UniversalPath} from "@extollo/util"
export abstract class ViewEngine extends AppClass {
protected readonly config: Config
protected readonly debug: boolean
constructor() {
super()
this.config = Container.getContainer().make(Config)
this.debug = (this.config.get('server.mode', 'production') === 'development'
|| this.config.get('server.debug', false))
}
public get path(): UniversalPath {
return this.app().appPath(...['resources', 'views']) // FIXME allow configuring
}
public abstract renderString(templateString: string, locals: {[key: string]: any}): string | Promise<string>
public abstract renderByName(templateName: string, locals: {[key: string]: any}): string | Promise<string>
}

View File

@@ -0,0 +1,66 @@
import {
AbstractFactory,
Container,
DependencyRequirement,
PropertyDependency,
isInstantiable,
DEPENDENCY_KEYS_METADATA_KEY,
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY
} from "@extollo/di"
import {Collection, ErrorWithContext} from "@extollo/util"
import {Logging} from "../service/Logging";
import {Config} from "../service/Config";
import {ViewEngine} from "./ViewEngine";
import {PugViewEngine} from "./PugViewEngine";
export class ViewEngineFactory extends AbstractFactory {
protected readonly logging: Logging
protected readonly config: Config
constructor() {
super({})
this.logging = Container.getContainer().make<Logging>(Logging)
this.config = Container.getContainer().make<Config>(Config)
}
produce(dependencies: any[], parameters: any[]): ViewEngine {
return new (this.getViewEngineClass())
}
match(something: any) {
return something === ViewEngine
}
getDependencyKeys(): Collection<DependencyRequirement> {
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.getViewEngineClass())
if ( meta ) return meta
return new Collection<DependencyRequirement>()
}
getInjectedProperties(): Collection<PropertyDependency> {
const meta = new Collection<PropertyDependency>()
let currentToken = this.getViewEngineClass()
do {
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken)
if ( loadedMeta ) meta.concat(loadedMeta)
currentToken = Object.getPrototypeOf(currentToken)
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype)
return meta
}
protected getViewEngineClass() {
const ViewEngineClass = this.config.get('server.view_engine.driver', PugViewEngine)
if ( !isInstantiable(ViewEngineClass) || !(ViewEngineClass.prototype instanceof ViewEngine) ) {
const e = new ErrorWithContext('Provided session class does not extend from @extollo/lib.ViewEngine');
e.context = {
config_key: 'server.view_engine.driver',
class: ViewEngineClass.toString(),
}
}
return ViewEngineClass
}
}