parent
0f182b592b
commit
ff34578d07
@ -1,2 +1,5 @@
|
|||||||
export * from '../../lib/src/module.ts'
|
export * from '../../lib/src/module.ts'
|
||||||
export * from '../../di/module.ts'
|
export * from '../../di/module.ts'
|
||||||
|
|
||||||
|
import * as std from '../../lib/src/external/std.ts'
|
||||||
|
export { std }
|
||||||
|
@ -1,5 +1,27 @@
|
|||||||
import { env } from '../../lib/src/unit/Scaffolding.ts';
|
import { env } from '../../lib/src/unit/Scaffolding.ts'
|
||||||
|
import {ViewEngine} from '../../lib/src/const/view_engines.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: env('APP_NAME', 'Daton'),
|
name: env('APP_NAME', 'Daton'),
|
||||||
|
|
||||||
|
views: {
|
||||||
|
/*
|
||||||
|
* View engine that should be used to render templates.
|
||||||
|
* Options are Handlebars, Ejs, or Denjuck.
|
||||||
|
*/
|
||||||
|
engine: ViewEngine.Handlebars,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Relative path from the app directory to the base directory where
|
||||||
|
* view files should be looked up.
|
||||||
|
*/
|
||||||
|
base_dir: 'http/views',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If using Handlebars, optionally, the path to the directory within the
|
||||||
|
* base_dir that contains the partials. They will be automatically registered
|
||||||
|
* with Handlebars.
|
||||||
|
*/
|
||||||
|
partials_dir: 'partials',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import Controller from '../../../lib/src/http/Controller.ts'
|
import Controller from '../../../lib/src/http/Controller.ts'
|
||||||
import {Request} from '../../../lib/src/http/Request.ts'
|
import {Request} from '../../../lib/src/http/Request.ts'
|
||||||
import {view} from '../../../lib/src/http/response/helpers.ts'
|
import {view} from '../../../lib/src/http/response/helpers.ts'
|
||||||
|
import {Injectable} from '../../../di/module.ts'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
export default class HomeController extends Controller {
|
export default class HomeController extends Controller {
|
||||||
|
|
||||||
get_home(request: Request) {
|
async get_home(request: Request) {
|
||||||
return view('home', { request })
|
return view('home', { greeting: 'Hello' })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1 +1,6 @@
|
|||||||
<h1>Welcome to Daton!</h1>
|
<html>
|
||||||
|
{{> header }}
|
||||||
|
<body>
|
||||||
|
<h1>{{ greeting }} from Daton!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
{{#if title}}
|
|
||||||
<title>{{ title }} | Daton</title>
|
|
||||||
{{else}}
|
|
||||||
<title>Daton</title>
|
|
||||||
{{/if}}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{{{ body }}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<title>Daton</title>
|
||||||
|
</head>
|
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
export enum ViewEngine {
|
||||||
|
Denjuck = 'Denjuck',
|
||||||
|
Ejs = 'Ejs',
|
||||||
|
Handlebars = 'Handlebars',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ViewFileExtension {
|
||||||
|
Denjuck = '.html',
|
||||||
|
Ejs = '.ejs',
|
||||||
|
Handlebars = '.hbs',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isViewEngine(what: any): what is ViewEngine {
|
||||||
|
return (['Denjuck', 'Ejs', 'Handlebars'].includes(String(what)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isViewFileExtension(what: any): what is ViewFileExtension {
|
||||||
|
return (['.html', '.ejs', '.hbs']).includes(String(what))
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* An error to be thrown when a configuration value is invalid.
|
||||||
|
*/
|
||||||
|
export class ConfigError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly key: string,
|
||||||
|
public readonly supplied_value: any,
|
||||||
|
message?: string
|
||||||
|
) {
|
||||||
|
super(message || `The value of the config key "${key}" is invalid. (${supplied_value} provided)`)
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,4 @@
|
|||||||
export * from 'https://deno.land/x/postgres/mod.ts'
|
// export * from 'https://deno.land/x/postgres@v0.4.3/mod.ts'
|
||||||
|
|
||||||
|
// FIXME: waiting on https://github.com/deno-postgres/deno-postgres/pull/166
|
||||||
|
export * from 'https://raw.githubusercontent.com/glmdev/deno-postgres/master/mod.ts'
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
export * from 'https://deno.land/std@0.53.0/http/server.ts'
|
export * from 'https://deno.land/std@0.67.0/http/server.ts'
|
||||||
export * from 'https://deno.land/std@0.53.0/http/cookie.ts'
|
export * from 'https://deno.land/std@0.67.0/http/cookie.ts'
|
||||||
export { Handlebars } from 'https://deno.land/x/handlebars/mod.ts'
|
|
||||||
|
import * as views from 'https://deno.land/x/view_engine@v1.3.0/mod.ts'
|
||||||
|
export { views }
|
||||||
|
|
||||||
|
import hb from 'https://dev.jspm.io/handlebars@4.7.6'
|
||||||
|
const handlebars = <any>hb
|
||||||
|
export { handlebars }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export * from 'https://deno.land/std@0.53.0/fmt/colors.ts'
|
export * from 'https://deno.land/std@0.67.0/fmt/colors.ts'
|
||||||
export { config as dotenv } from 'https://deno.land/x/dotenv/mod.ts'
|
export { config as dotenv } from 'https://deno.land/x/dotenv/mod.ts'
|
||||||
export * as path from 'https://deno.land/std@0.53.0/path/mod.ts'
|
export * as path from 'https://deno.land/std@0.67.0/path/mod.ts'
|
||||||
export * as fs from 'https://deno.land/std@0.53.0/fs/mod.ts'
|
export * as fs from 'https://deno.land/std@0.67.0/fs/mod.ts'
|
||||||
export { generate as uuid } from 'https://deno.land/std/uuid/v4.ts'
|
export { generate as uuid } from 'https://deno.land/std@0.67.0/uuid/v4.ts'
|
||||||
// export { moment } from 'https://deno.land/x/moment/moment.ts'
|
// export { moment } from 'https://deno.land/x/moment/moment.ts'
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import ResponseFactory from './ResponseFactory.ts'
|
|
||||||
import ViewEngine from '../../unit/ViewEngine.ts'
|
|
||||||
import {Request} from '../Request.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response factory that renders a partial view as HTML.
|
|
||||||
* @return ResponseFactory
|
|
||||||
*/
|
|
||||||
export default class PartialViewResponseFactory extends ResponseFactory {
|
|
||||||
constructor(
|
|
||||||
/**
|
|
||||||
* The view name.
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
public readonly view: string,
|
|
||||||
/**
|
|
||||||
* Optionally, the response context.
|
|
||||||
*/
|
|
||||||
public readonly context?: any,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
public async write(request: Request) {
|
|
||||||
const views: ViewEngine = this.make(ViewEngine)
|
|
||||||
request.response.body = await views.partial(this.view, this.context)
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,84 +1,197 @@
|
|||||||
import LifecycleUnit from '../lifecycle/Unit.ts'
|
import LifecycleUnit from '../lifecycle/Unit.ts'
|
||||||
|
import Config from './Config.ts'
|
||||||
|
import {isViewEngine, ViewEngine as Types, ViewFileExtension} from '../const/view_engines.ts'
|
||||||
|
import {views, handlebars} from '../external/http.ts'
|
||||||
import {Unit} from '../lifecycle/decorators.ts'
|
import {Unit} from '../lifecycle/decorators.ts'
|
||||||
import {Handlebars} from '../external/http.ts'
|
|
||||||
import {Logging} from '../service/logging/Logging.ts'
|
import {Logging} from '../service/logging/Logging.ts'
|
||||||
import {fs} from '../external/std.ts'
|
import {ConfigError} from '../error/ConfigError.ts'
|
||||||
|
import {path, fs} from '../external/std.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle unit which sets up and provides basic view engine services.
|
* Error thrown when an action requiring a view engine is attempted, but no view
|
||||||
|
* engine has been configured properly.
|
||||||
|
*/
|
||||||
|
export class MissingViewEngineError extends Error {
|
||||||
|
constructor(message = 'Missing or invalid view engine type config.') {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when an action requiring a template base directory is attempted,
|
||||||
|
* but no such directory has been configured.
|
||||||
|
*/
|
||||||
|
export class MissingTemplateDirectoryError extends Error {
|
||||||
|
constructor(message = 'Missing or invalid path to template base directory.') {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle unit which manages the initialization of the configured rendering
|
||||||
|
* engine, as well as utilities for rendering template files and strings.
|
||||||
* @extends LifecycleUnit
|
* @extends LifecycleUnit
|
||||||
*/
|
*/
|
||||||
@Unit()
|
@Unit()
|
||||||
export default class ViewEngine extends LifecycleUnit {
|
export default class ViewEngine extends LifecycleUnit {
|
||||||
/**
|
protected engine?: Types
|
||||||
* The Handlebars instance.
|
protected template_dir?: string
|
||||||
* @type Handlebars
|
|
||||||
*/
|
|
||||||
protected _handlebars!: Handlebars
|
|
||||||
|
|
||||||
// TODO include basic app info in view data
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
protected readonly config: Config,
|
||||||
protected readonly logger: Logging,
|
protected readonly logger: Logging,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
async up() {
|
public async up() {
|
||||||
this.logger.info(`Setting views base dir: ${this.app.app_path('http', 'views')}`)
|
const config: unknown = this.config.get('app.views.engine')
|
||||||
this._handlebars = new Handlebars({
|
const base_dir: unknown = this.config.get('app.views.base_dir')
|
||||||
baseDir: this.app.app_path('http', 'views'),
|
|
||||||
extname: '.hbs',
|
if ( !isViewEngine(config) ) {
|
||||||
layoutsDir: 'layouts',
|
throw new ConfigError('app.views.engine', config)
|
||||||
partialsDir: 'partials',
|
|
||||||
defaultLayout: 'main',
|
|
||||||
helpers: undefined,
|
|
||||||
compilerOptions: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const main_layout_path = this.app.app_path('http', 'views', 'layouts', 'main.hbs')
|
|
||||||
if ( !(await fs.exists(main_layout_path)) ) {
|
|
||||||
this.logger.warn(`Unable to open main view layout file: ${main_layout_path}`)
|
|
||||||
this.logger.warn(`Unless you are using a custom layout, this could cause errors.`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const partials_path = this.app.app_path('http', 'views', 'partials')
|
if ( !String(base_dir) ) {
|
||||||
if ( !(await fs.exists(partials_path)) ) {
|
throw new ConfigError('app.views.base_dir', base_dir)
|
||||||
this.logger.warn(`Unable to open view partials directory: ${partials_path}`)
|
|
||||||
this.logger.warn(`This directory must exist for the view engine to function, even if it is empty.`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.template_dir = this.app.app_path(base_dir)
|
||||||
|
this.engine = config
|
||||||
|
this.logger.info(`Determined view engine from config: ${config}`)
|
||||||
|
this.logger.info(`Determined base directory for templates: ${this.template_dir}`)
|
||||||
|
|
||||||
|
await this.init_engine()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The handlebars instance.
|
* Render a template file using the configured engine.
|
||||||
* @type Handlebars
|
* @param {string} template_path - the relative path of the template w/in the template dir.
|
||||||
|
* @param {object} [data = {}]
|
||||||
|
* @return Promise<string>
|
||||||
*/
|
*/
|
||||||
get handlebars(): Handlebars {
|
public async template(template_path: string, data: { [key: string]: any } = {}) {
|
||||||
return this._handlebars
|
const file_path = `${this.template_path(template_path.replace(/:/g, '/'))}${this.get_file_extension()}`
|
||||||
|
|
||||||
|
this.logger.debug(`Rendering template "${template_path}" from file: ${file_path}`)
|
||||||
|
|
||||||
|
// TODO cache this
|
||||||
|
// TODO replace with fs.readFileStr
|
||||||
|
const content = await Deno.readTextFile(file_path)
|
||||||
|
const engine: views.Engine = this.get_engine()
|
||||||
|
|
||||||
|
return engine(
|
||||||
|
content,
|
||||||
|
data,
|
||||||
|
this.get_render_context(),
|
||||||
|
`${template_path}${this.get_file_extension()}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a view with the given name, using the specified arguments and layout.
|
* Resolve a path within the template base directory.
|
||||||
* @param {string} view
|
* @param {...string} parts
|
||||||
* @param [args]
|
* @return string
|
||||||
* @param {string} [layout]
|
*/
|
||||||
|
public template_path(...parts: string[]): string {
|
||||||
|
if ( !this.template_dir ) throw new MissingTemplateDirectoryError()
|
||||||
|
return path.resolve(this.template_dir, ...parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a template string using the configured engine.
|
||||||
|
* @param {string} template
|
||||||
|
* @param {object} [data = {}]
|
||||||
* @return Promise<string>
|
* @return Promise<string>
|
||||||
*/
|
*/
|
||||||
async render(view: string, args?: any, layout?: string): Promise<string> {
|
public async render(template: string, data: { [key: string]: any } = {}) {
|
||||||
this.logger.debug(`Rendering view: ${view}`)
|
const engine: views.Engine = this.get_engine()
|
||||||
return this.handlebars.renderView(view, args, layout)
|
return engine(template, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a partial view with the given name, using the specified arguments.
|
* Get the rendering engine for the configured backend.
|
||||||
* @param {string} view
|
|
||||||
* @param [args]
|
|
||||||
*/
|
*/
|
||||||
async partial(view: string, args?: any) {
|
public get_engine(): views.Engine {
|
||||||
const parts = `${view}.hbs`.split(':')
|
if ( this.engine === Types.Denjuck ) {
|
||||||
const resolved = this.app.app_path('http', 'views', ...parts)
|
return views.engineFactory.getDenjuckEngine()
|
||||||
|
} else if ( this.engine === Types.Ejs ) {
|
||||||
|
return views.engineFactory.getEjsEngine()
|
||||||
|
} else if ( this.engine === Types.Handlebars ) {
|
||||||
|
return views.engineFactory.getHandlebarsEngine()
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.debug(`Rendering partial: ${view} from ${resolved}`)
|
throw new MissingViewEngineError()
|
||||||
return this.handlebars.render(resolved, args)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file extension expected by the currently configured render engine.
|
||||||
|
*/
|
||||||
|
public get_file_extension(): ViewFileExtension {
|
||||||
|
if ( this.engine === Types.Denjuck ) {
|
||||||
|
return ViewFileExtension.Denjuck
|
||||||
|
} else if ( this.engine === Types.Ejs ) {
|
||||||
|
return ViewFileExtension.Ejs
|
||||||
|
} else if ( this.engine === Types.Handlebars ) {
|
||||||
|
return ViewFileExtension.Handlebars
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MissingViewEngineError()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rendering context to pass to the render engine. This is here
|
||||||
|
* for compatibility with the view_engine library.
|
||||||
|
*/
|
||||||
|
public get_render_context() {
|
||||||
|
if ( !this.template_dir ) throw new MissingTemplateDirectoryError()
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewExt: this.get_file_extension(),
|
||||||
|
viewEngine: this.get_engine(),
|
||||||
|
viewRoot: this.template_dir,
|
||||||
|
useCache: false, // TODO better value here
|
||||||
|
cache: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the appropriate rendering engine.
|
||||||
|
*/
|
||||||
|
protected async init_engine() {
|
||||||
|
if ( this.engine === Types.Handlebars ) {
|
||||||
|
await this.init_engine_handlebars()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Handlebars rendering engine by loading and registering partial views
|
||||||
|
* from the configured partials directory, if one exists.
|
||||||
|
*/
|
||||||
|
protected async init_engine_handlebars() {
|
||||||
|
const partials_dir: unknown = this.config.get('app.views.partials_dir')
|
||||||
|
if ( String(partials_dir) ) {
|
||||||
|
const partials_path = this.template_path(String(partials_dir))
|
||||||
|
this.logger.info(`Registering Handlebars partials from: ${partials_path}`)
|
||||||
|
|
||||||
|
for await ( const entry of fs.walk(partials_path) ) {
|
||||||
|
if ( !entry.isFile || !entry.path.endsWith(this.get_file_extension()) ) {
|
||||||
|
if ( entry.isFile ) this.logger.debug(`Skipping file in Handlebars partials path with invalid suffix: ${entry.path}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await Deno.readTextFile(entry.path)
|
||||||
|
|
||||||
|
// Generate the partial name.
|
||||||
|
// This is done by taking the relative path from the partials dir, removing the suffix,
|
||||||
|
// and replacing directory separators with the standard : identifier.
|
||||||
|
let partial_name = entry.path.replace(partials_path, '').slice(0, -1 * (this.get_file_extension()).length)
|
||||||
|
if ( partial_name.startsWith('/') ) partial_name = partial_name.slice(1)
|
||||||
|
partial_name = partial_name.replace(/\//g, ':')
|
||||||
|
|
||||||
|
this.logger.debug(`Registering handlebars partial: ${partial_name}`)
|
||||||
|
handlebars.registerPartial(partial_name, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in new issue