diff --git a/app/bundle/daton.ts b/app/bundle/daton.ts
index 9b07d55..a7dbf5b 100644
--- a/app/bundle/daton.ts
+++ b/app/bundle/daton.ts
@@ -1,2 +1,5 @@
export * from '../../lib/src/module.ts'
-export * from '../../di/module.ts'
\ No newline at end of file
+export * from '../../di/module.ts'
+
+import * as std from '../../lib/src/external/std.ts'
+export { std }
diff --git a/app/configs/app.config.ts b/app/configs/app.config.ts
index a56cc2a..8a1b5f8 100644
--- a/app/configs/app.config.ts
+++ b/app/configs/app.config.ts
@@ -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',
+ },
}
diff --git a/app/http/controllers/Home.controller.ts b/app/http/controllers/Home.controller.ts
index 6ceeaa7..ae1ddf8 100644
--- a/app/http/controllers/Home.controller.ts
+++ b/app/http/controllers/Home.controller.ts
@@ -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' })
}
}
diff --git a/app/http/views/home.hbs b/app/http/views/home.hbs
index 9eb7a75..ca7dadb 100644
--- a/app/http/views/home.hbs
+++ b/app/http/views/home.hbs
@@ -1 +1,6 @@
-
Welcome to Daton!
+
+ {{> header }}
+
+ {{ greeting }} from Daton!
+
+
\ No newline at end of file
diff --git a/app/http/views/layouts/.gitkeep b/app/http/views/layouts/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/app/http/views/layouts/main.hbs b/app/http/views/layouts/main.hbs
deleted file mode 100644
index 2129e45..0000000
--- a/app/http/views/layouts/main.hbs
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- {{#if title}}
- {{ title }} | Daton
- {{else}}
- Daton
- {{/if}}
-
-
- {{{ body }}}
-
-
\ No newline at end of file
diff --git a/app/http/views/partials/.gitkeep b/app/http/views/partials/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/app/http/views/partials/header.hbs b/app/http/views/partials/header.hbs
new file mode 100644
index 0000000..444e9b8
--- /dev/null
+++ b/app/http/views/partials/header.hbs
@@ -0,0 +1,3 @@
+
+ Daton
+
diff --git a/app/index.ts b/app/index.ts
index 2ed0d40..adfc2ae 100755
--- a/app/index.ts
+++ b/app/index.ts
@@ -8,8 +8,11 @@ import units from './units.ts'
* Let's get up and running. The scaffolding provides the bare minimum
* amount of support required to get Daton up and running. The app handles
* the rest.
+ *
+ * Daton will automatically load and process application resources, which is
+ * why we need to pass in the base path of this script.
*/
-const scaffolding = make(Scaffolding)
+const scaffolding = make(Scaffolding, import.meta.url)
await scaffolding.up()
/*
diff --git a/app/units.ts b/app/units.ts
index acbcb16..d751bf2 100644
--- a/app/units.ts
+++ b/app/units.ts
@@ -17,11 +17,11 @@ export default [
ConfigUnit,
DatabaseUnit,
ModelsUnit,
+ ViewEngineUnit,
HttpKernelUnit,
MiddlewareUnit,
ControllerUnit,
- ViewEngineUnit,
RoutesUnit,
RoutingUnit,
- // HttpServerUnit,
+ HttpServerUnit,
]
diff --git a/lib/src/const/view_engines.ts b/lib/src/const/view_engines.ts
new file mode 100644
index 0000000..383ece1
--- /dev/null
+++ b/lib/src/const/view_engines.ts
@@ -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))
+}
diff --git a/lib/src/error/ConfigError.ts b/lib/src/error/ConfigError.ts
new file mode 100644
index 0000000..a11b166
--- /dev/null
+++ b/lib/src/error/ConfigError.ts
@@ -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)`)
+ }
+}
diff --git a/lib/src/external/db.ts b/lib/src/external/db.ts
index 14bb523..f3b7f82 100644
--- a/lib/src/external/db.ts
+++ b/lib/src/external/db.ts
@@ -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'
diff --git a/lib/src/external/http.ts b/lib/src/external/http.ts
index 6b6409b..71c371d 100644
--- a/lib/src/external/http.ts
+++ b/lib/src/external/http.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'
\ No newline at end of file
+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 = hb
+export { handlebars }
diff --git a/lib/src/external/std.ts b/lib/src/external/std.ts
index 3882bdb..51f2b72 100644
--- a/lib/src/external/std.ts
+++ b/lib/src/external/std.ts
@@ -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'
diff --git a/lib/src/http/CookieJar.ts b/lib/src/http/CookieJar.ts
index 3f93ada..b29129f 100644
--- a/lib/src/http/CookieJar.ts
+++ b/lib/src/http/CookieJar.ts
@@ -1,5 +1,5 @@
import { Injectable } from '../../../di/src/decorator/Injection.ts'
-import { getCookies, setCookie, delCookie, ServerRequest } from '../external/http.ts'
+import { getCookies, setCookie, deleteCookie, ServerRequest } from '../external/http.ts'
import { InMemCache } from '../support/InMemCache.ts'
import { HTTPRequest } from './type/HTTPRequest.ts'
@@ -117,6 +117,6 @@ export class CookieJar {
*/
public async delete(key: string): Promise {
await this._cache.drop(key)
- delCookie(this.request.response, key)
+ deleteCookie(this.request.response, key)
}
}
diff --git a/lib/src/http/response/HTMLResponseFactory.ts b/lib/src/http/response/HTMLResponseFactory.ts
index 1b017c8..ca5efef 100644
--- a/lib/src/http/response/HTMLResponseFactory.ts
+++ b/lib/src/http/response/HTMLResponseFactory.ts
@@ -14,7 +14,7 @@ export default class HTMLResponseFactory extends ResponseFactory {
public async write(request: Request): Promise {
request = await super.write(request)
- request.response.headers.set('Content-Type', 'text/html')
+ request.response.headers.set('Content-Type', 'text/html; charset=utf-8')
request.response.body = this.value
return request
}
diff --git a/lib/src/http/response/PartialViewResponseFactory.ts b/lib/src/http/response/PartialViewResponseFactory.ts
deleted file mode 100644
index 7b738a7..0000000
--- a/lib/src/http/response/PartialViewResponseFactory.ts
+++ /dev/null
@@ -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
- }
-}
diff --git a/lib/src/http/response/ViewResponseFactory.ts b/lib/src/http/response/ViewResponseFactory.ts
index ca57df9..1cf4203 100644
--- a/lib/src/http/response/ViewResponseFactory.ts
+++ b/lib/src/http/response/ViewResponseFactory.ts
@@ -13,22 +13,19 @@ export default class ViewResponseFactory extends ResponseFactory {
* @type string
*/
public readonly view: string,
+
/**
- * Optionally, the view context.
- */
- public readonly context?: any,
- /**
- * Optionally, the layout name.
- * @type string
+ * Optionally, the view data.
*/
- public readonly layout?: string,
+ public readonly data?: any,
) {
super()
}
public async write(request: Request) {
const views: ViewEngine = this.make(ViewEngine)
- request.response.body = await views.render(this.view, this.context, this.layout)
+ request.response.body = await views.template(this.view, this.data)
+ request.response.headers.set('Content-Type', 'text/html; charset=utf-8')
return request
}
}
diff --git a/lib/src/http/response/helpers.ts b/lib/src/http/response/helpers.ts
index e2dd320..19fab60 100644
--- a/lib/src/http/response/helpers.ts
+++ b/lib/src/http/response/helpers.ts
@@ -9,7 +9,6 @@ import {HTTPStatus} from '../../const/http.ts'
import HTTPErrorResponseFactory from './HTTPErrorResponseFactory.ts'
import HTTPError from '../../error/HTTPError.ts'
import ViewResponseFactory from './ViewResponseFactory.ts'
-import PartialViewResponseFactory from './PartialViewResponseFactory.ts'
/**
* Get a new JSON response factory that writes the given object as JSON.
@@ -69,22 +68,11 @@ export function http(status: HTTPStatus, message?: string): HTTPErrorResponseFac
}
/**
- * Get a new view response factory for the given view name, passing along context and layout.
+ * Get a new view response factory for the given view name, passing along any view data.
* @param {string} view
- * @param [context]
- * @param {string} [layout]
+ * @param [data]
* @return ViewResponseFactory
*/
-export function view(view: string, context?: any, layout?: string): ViewResponseFactory {
- return make(ViewResponseFactory, view, context, layout)
-}
-
-/**
- * Get a new partial view response factory for the given view name, passing along context.
- * @param {string} view
- * @param [context]
- * @return PartialViewResponseFactory
- */
-export function partial(view: string, context?: any): PartialViewResponseFactory {
- return make(PartialViewResponseFactory, view, context)
+export function view(view: string, data?: any): ViewResponseFactory {
+ return make(ViewResponseFactory, view, data)
}
diff --git a/lib/src/lifecycle/Application.ts b/lib/src/lifecycle/Application.ts
index 6720959..b8fd493 100644
--- a/lib/src/lifecycle/Application.ts
+++ b/lib/src/lifecycle/Application.ts
@@ -8,6 +8,8 @@ import {Status} from '../const/status.ts'
import Instantiable from '../../../di/src/type/Instantiable.ts'
import {Collection} from '../collection/Collection.ts'
import {path} from '../external/std.ts'
+import Scaffolding from '../unit/Scaffolding.ts'
+import {Container} from '../../../di/src/Container.ts'
/**
* Central class for Daton applications.
@@ -22,6 +24,7 @@ export default class Application {
constructor(
protected logger: Logging,
+ protected injector: Container,
protected rleh: RunLevelErrorHandler,
/**
* Array of unit classes to run for this application.
@@ -112,7 +115,7 @@ export default class Application {
* @type string
*/
get root() {
- return path.resolve('.')
+ return this.injector.make(Scaffolding).base_dir
}
/**
@@ -120,7 +123,7 @@ export default class Application {
* @type string
*/
get app_root() {
- return path.resolve('./app')
+ return this.injector.make(Scaffolding).base_dir
}
/**
diff --git a/lib/src/unit/Scaffolding.ts b/lib/src/unit/Scaffolding.ts
index f6b7603..4132e85 100644
--- a/lib/src/unit/Scaffolding.ts
+++ b/lib/src/unit/Scaffolding.ts
@@ -9,6 +9,7 @@ import 'https://deno.land/x/dotenv/load.ts'
import { Container } from '../../../di/src/Container.ts'
import { Inject } from '../../../di/src/decorator/Injection.ts'
import CacheFactory from '../support/CacheFactory.ts'
+import { path } from '../external/std.ts'
/**
* Simple helper for loading ENV values with fallback.
@@ -29,11 +30,17 @@ export { env }
*/
@Unit()
export default class Scaffolding extends LifecycleUnit {
+ public base_dir!: string
+
constructor(
protected logger: Logging,
protected utility: Utility,
@Inject('injector') protected injector: Container,
- ) { super() }
+ base_script: string
+ ) {
+ super()
+ this.base_dir = path.dirname(base_script).replace('file:///', '/')
+ }
/**
* Helper method for fetching environment variables.
@@ -47,6 +54,8 @@ export default class Scaffolding extends LifecycleUnit {
public async up() {
this.setup_logging()
this.register_factories()
+
+ this.logger.info(`Base directory: ${this.base_dir}`)
}
/**
diff --git a/lib/src/unit/ViewEngine.ts b/lib/src/unit/ViewEngine.ts
index 63ae94f..d6dc6bd 100644
--- a/lib/src/unit/ViewEngine.ts
+++ b/lib/src/unit/ViewEngine.ts
@@ -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.
+ * 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
*/
@Unit()
export default class ViewEngine extends LifecycleUnit {
- /**
- * The Handlebars instance.
- * @type Handlebars
- */
- protected _handlebars!: Handlebars
-
- // TODO include basic app info in view data
+ 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,
- })
-
- 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.`)
+ public async up() {
+ const config: unknown = this.config.get('app.views.engine')
+ const base_dir: unknown = this.config.get('app.views.base_dir')
+
+ 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
*/
- 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
*/
- async render(view: string, args?: any, layout?: string): Promise {
- 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)
}
/**
- * Render a partial view with the given name, using the specified arguments.
- * @param {string} view
- * @param [args]
+ * Get the rendering engine for the configured backend.
*/
- async partial(view: string, args?: any) {
- const parts = `${view}.hbs`.split(':')
- const resolved = this.app.app_path('http', 'views', ...parts)
+ 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()
+ }
- this.logger.debug(`Rendering partial: ${view} from ${resolved}`)
- return this.handlebars.render(resolved, args)
+ 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()
+ }
+ }
+
+ /**
+ * 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)
+ }
+ }
}
}