parent
0f182b592b
commit
ff34578d07
@ -1,2 +1,5 @@
|
||||
export * from '../../lib/src/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 {
|
||||
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 {Request} from '../../../lib/src/http/Request.ts'
|
||||
import {view} from '../../../lib/src/http/response/helpers.ts'
|
||||
import {Injectable} from '../../../di/module.ts'
|
||||
|
||||
@Injectable()
|
||||
export default class HomeController extends Controller {
|
||||
|
||||
get_home(request: Request) {
|
||||
return view('home', { request })
|
||||
async get_home(request: 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.53.0/http/cookie.ts'
|
||||
export { Handlebars } from 'https://deno.land/x/handlebars/mod.ts'
|
||||
export * from 'https://deno.land/std@0.67.0/http/server.ts'
|
||||
export * from 'https://deno.land/std@0.67.0/http/cookie.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 * as path from 'https://deno.land/std@0.53.0/path/mod.ts'
|
||||
export * as fs from 'https://deno.land/std@0.53.0/fs/mod.ts'
|
||||
export { generate as uuid } from 'https://deno.land/std/uuid/v4.ts'
|
||||
export * as path from 'https://deno.land/std@0.67.0/path/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@0.67.0/uuid/v4.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 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 {Handlebars} from '../external/http.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.
|
||||
* @extends LifecycleUnit
|
||||
* Error thrown when an action requiring a view engine is attempted, but no view
|
||||
* engine has been configured properly.
|
||||
*/
|
||||
@Unit()
|
||||
export default class ViewEngine extends LifecycleUnit {
|
||||
export class MissingViewEngineError extends Error {
|
||||
constructor(message = 'Missing or invalid view engine type config.') {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Handlebars instance.
|
||||
* @type Handlebars
|
||||
* Error thrown when an action requiring a template base directory is attempted,
|
||||
* but no such directory has been configured.
|
||||
*/
|
||||
protected _handlebars!: Handlebars
|
||||
export class MissingTemplateDirectoryError extends Error {
|
||||
constructor(message = 'Missing or invalid path to template base directory.') {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO include basic app info in view data
|
||||
/**
|
||||
* Lifecycle unit which manages the initialization of the configured rendering
|
||||
* engine, as well as utilities for rendering template files and strings.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
@Unit()
|
||||
export default class ViewEngine extends LifecycleUnit {
|
||||
protected engine?: Types
|
||||
protected template_dir?: string
|
||||
|
||||
constructor(
|
||||
protected readonly config: Config,
|
||||
protected readonly logger: Logging,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async up() {
|
||||
this.logger.info(`Setting views base dir: ${this.app.app_path('http', 'views')}`)
|
||||
this._handlebars = new Handlebars({
|
||||
baseDir: this.app.app_path('http', 'views'),
|
||||
extname: '.hbs',
|
||||
layoutsDir: 'layouts',
|
||||
partialsDir: 'partials',
|
||||
defaultLayout: 'main',
|
||||
helpers: undefined,
|
||||
compilerOptions: undefined,
|
||||
})
|
||||
public async up() {
|
||||
const config: unknown = this.config.get('app.views.engine')
|
||||
const base_dir: unknown = this.config.get('app.views.base_dir')
|
||||
|
||||
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.`)
|
||||
if ( !isViewEngine(config) ) {
|
||||
throw new ConfigError('app.views.engine', config)
|
||||
}
|
||||
|
||||
const partials_path = this.app.app_path('http', 'views', 'partials')
|
||||
if ( !(await fs.exists(partials_path)) ) {
|
||||
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.`)
|
||||
if ( !String(base_dir) ) {
|
||||
throw new ConfigError('app.views.base_dir', base_dir)
|
||||
}
|
||||
|
||||
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.
|
||||
* @type Handlebars
|
||||
* Render a template file using the configured engine.
|
||||
* @param {string} template_path - the relative path of the template w/in the template dir.
|
||||
* @param {object} [data = {}]
|
||||
* @return Promise<string>
|
||||
*/
|
||||
get handlebars(): Handlebars {
|
||||
return this._handlebars
|
||||
public async template(template_path: string, data: { [key: string]: any } = {}) {
|
||||
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.
|
||||
* @param {string} view
|
||||
* @param [args]
|
||||
* @param {string} [layout]
|
||||
* Resolve a path within the template base directory.
|
||||
* @param {...string} parts
|
||||
* @return string
|
||||
*/
|
||||
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>
|
||||
*/
|
||||
async render(view: string, args?: any, layout?: string): Promise<string> {
|
||||
this.logger.debug(`Rendering view: ${view}`)
|
||||
return this.handlebars.renderView(view, args, layout)
|
||||
public async render(template: string, data: { [key: string]: any } = {}) {
|
||||
const engine: views.Engine = this.get_engine()
|
||||
return engine(template, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rendering engine for the configured backend.
|
||||
*/
|
||||
public get_engine(): views.Engine {
|
||||
if ( this.engine === Types.Denjuck ) {
|
||||
return views.engineFactory.getDenjuckEngine()
|
||||
} else if ( this.engine === Types.Ejs ) {
|
||||
return views.engineFactory.getEjsEngine()
|
||||
} else if ( this.engine === Types.Handlebars ) {
|
||||
return views.engineFactory.getHandlebarsEngine()
|
||||
}
|
||||
|
||||
throw new MissingViewEngineError()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a partial view with the given name, using the specified arguments.
|
||||
* @param {string} view
|
||||
* @param [args]
|
||||
* Initialize the Handlebars rendering engine by loading and registering partial views
|
||||
* from the configured partials directory, if one exists.
|
||||
*/
|
||||
async partial(view: string, args?: any) {
|
||||
const parts = `${view}.hbs`.split(':')
|
||||
const resolved = this.app.app_path('http', 'views', ...parts)
|
||||
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}`)
|
||||
|
||||
this.logger.debug(`Rendering partial: ${view} from ${resolved}`)
|
||||
return this.handlebars.render(resolved, args)
|
||||
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