From 7506d6567d73793084ebe46fd517ec7d1c6dfc23 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Thu, 24 Jun 2021 00:14:04 -0500 Subject: [PATCH] Support registering namespaced view directories; add lib() universal path --- package.json | 2 + pnpm-lock.yaml | 122 +++++++++++++++++++++++++++-- src/di/ContainerBlueprint.ts | 13 ++- src/index.ts | 1 + src/lib.ts | 8 ++ src/resources/views/auth/login.pug | 3 + src/service/Routing.ts | 5 ++ src/views/PugViewEngine.ts | 11 +-- src/views/ViewEngine.ts | 53 ++++++++++++- 9 files changed, 201 insertions(+), 17 deletions(-) create mode 100644 src/lib.ts create mode 100644 src/resources/views/auth/login.pug diff --git a/package.json b/package.json index da4b097..9434ca2 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,14 @@ "typedoc-plugin-pages-fork": "^0.0.1", "typedoc-plugin-sourcefile-url": "^1.0.6", "typescript": "^4.2.3", + "copyfiles": "^2.4.1", "uuid": "^8.3.2" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "prebuild": "pnpm run lint", "build": "tsc", + "postbuild": "copyfiles -u 1 \"src/resources/**/*\" lib", "app": "tsc && node lib/index.js", "prepare": "pnpm run build", "docs:build": "typedoc --options typedoc.json", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6080b65..4c8c7a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,7 @@ dependencies: busboy: 0.3.1 cli-table: 0.3.6 colors: 1.4.0 + copyfiles: 2.4.1 dotenv: 8.2.0 mkdirp: 1.0.4 negotiator: 0.6.2 @@ -383,7 +384,6 @@ packages: resolution: integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8= /ansi-regex/5.0.0: - dev: true engines: node: '>=8' resolution: @@ -399,7 +399,6 @@ packages: /ansi-styles/4.3.0: dependencies: color-convert: 2.0.1 - dev: true engines: node: '>=8' resolution: @@ -568,6 +567,14 @@ packages: node: '>= 0.2.0' resolution: integrity: sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ== + /cliui/7.0.4: + dependencies: + string-width: 4.2.2 + strip-ansi: 6.0.0 + wrap-ansi: 7.0.0 + dev: false + resolution: + integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== /code-point-at/1.1.0: dev: false engines: @@ -583,7 +590,6 @@ packages: /color-convert/2.0.1: dependencies: color-name: 1.1.4 - dev: true engines: node: '>=7.0.0' resolution: @@ -593,7 +599,6 @@ packages: resolution: integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= /color-name/1.1.4: - dev: true resolution: integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== /colors/1.0.3: @@ -626,6 +631,19 @@ packages: dev: false resolution: integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw== + /copyfiles/2.4.1: + dependencies: + glob: 7.1.7 + minimatch: 3.0.4 + mkdirp: 1.0.4 + noms: 0.0.0 + through2: 2.0.5 + untildify: 4.0.0 + yargs: 16.2.0 + dev: false + hasBin: true + resolution: + integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg== /core-util-is/1.0.2: dev: false resolution: @@ -722,7 +740,6 @@ packages: resolution: integrity: sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== /emoji-regex/8.0.0: - dev: true resolution: integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== /enquirer/2.3.6: @@ -733,6 +750,12 @@ packages: node: '>=8.6' resolution: integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + /escalade/3.1.1: + dev: false + engines: + node: '>=6' + resolution: + integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== /escape-string-regexp/1.0.5: dev: true engines: @@ -986,6 +1009,12 @@ packages: dev: false resolution: integrity: sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + /get-caller-file/2.0.5: + dev: false + engines: + node: 6.* || 8.* || >= 10.* + resolution: + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== /get-intrinsic/1.1.1: dependencies: function-bind: 1.1.1 @@ -1168,7 +1197,6 @@ packages: resolution: integrity: sha1-754xOG8DGn8NZDr4L95QxFfvAMs= /is-fullwidth-code-point/3.0.0: - dev: true engines: node: '>=8' resolution: @@ -1200,6 +1228,10 @@ packages: node: '>= 0.4' resolution: integrity: sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== + /isarray/0.0.1: + dev: false + resolution: + integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= /isarray/1.0.0: dev: false resolution: @@ -1391,6 +1423,13 @@ packages: node: 4.x || >=6.0.0 resolution: integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + /noms/0.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 1.0.34 + dev: false + resolution: + integrity: sha1-2o69nzr51nYJGbJ9nNyAkqczKFk= /nopt/5.0.0: dependencies: abbrev: 1.1.1 @@ -1697,6 +1736,15 @@ packages: dev: true resolution: integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + /readable-stream/1.0.34: + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + dev: false + resolution: + integrity: sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= /readable-stream/2.3.7: dependencies: core-util-is: 1.0.2 @@ -1737,6 +1785,12 @@ packages: node: '>=8' resolution: integrity: sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + /require-directory/2.1.1: + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I= /require-from-string/2.0.2: dev: true engines: @@ -1913,11 +1967,14 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.0 - dev: true engines: node: '>=8' resolution: integrity: sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + /string_decoder/0.10.31: + dev: false + resolution: + integrity: sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= /string_decoder/1.1.1: dependencies: safe-buffer: 5.1.2 @@ -1941,7 +1998,6 @@ packages: /strip-ansi/6.0.0: dependencies: ansi-regex: 5.0.0 - dev: true engines: node: '>=8' resolution: @@ -1998,6 +2054,13 @@ packages: dev: true resolution: integrity: sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + /through2/2.0.5: + dependencies: + readable-stream: 2.3.7 + xtend: 4.0.2 + dev: false + resolution: + integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== /to-fast-properties/2.0.0: dev: false engines: @@ -2144,6 +2207,12 @@ packages: node: '>= 10.0.0' resolution: integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + /untildify/4.0.0: + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== /uri-js/4.4.1: dependencies: punycode: 2.1.1 @@ -2209,6 +2278,16 @@ packages: dev: false resolution: integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + /wrap-ansi/7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.2 + strip-ansi: 6.0.0 + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== /wrappy/1.0.2: resolution: integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= @@ -2218,6 +2297,12 @@ packages: node: '>=0.4' resolution: integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + /y18n/5.0.8: + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== /yallist/3.1.1: dev: false resolution: @@ -2225,6 +2310,26 @@ packages: /yallist/4.0.0: resolution: integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + /yargs-parser/20.2.9: + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + /yargs/16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.2 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== /yn/3.1.1: dev: false engines: @@ -2250,6 +2355,7 @@ specifiers: busboy: ^0.3.1 cli-table: ^0.3.6 colors: ^1.4.0 + copyfiles: ^2.4.1 dotenv: ^8.2.0 eslint: ^7.27.0 mkdirp: ^1.0.4 diff --git a/src/di/ContainerBlueprint.ts b/src/di/ContainerBlueprint.ts index 3b78d75..ab902c8 100644 --- a/src/di/ContainerBlueprint.ts +++ b/src/di/ContainerBlueprint.ts @@ -1,7 +1,8 @@ -import {Instantiable} from './types' +import {DependencyKey, Instantiable} from './types' import NamedFactory from './factory/NamedFactory' import {AbstractFactory} from './factory/AbstractFactory' import {Factory} from './factory/Factory' +import {ClosureFactory} from './factory/ClosureFactory' export class ContainerBlueprint { private static instance?: ContainerBlueprint @@ -36,6 +37,16 @@ export class ContainerBlueprint { return this } + /** + * Register a producer function as a ClosureFactory with this container. + * @param key + * @param producer + */ + registerProducer(key: DependencyKey, producer: () => any): this { + this.factories.push(() => new ClosureFactory(key, producer)) + return this + } + resolve(): AbstractFactory[] { return this.factories.map(x => x()) } diff --git a/src/index.ts b/src/index.ts index c51b45d..9cef892 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './util' +export * from './lib' export * from './di' export * from './event/types' diff --git a/src/lib.ts b/src/lib.ts new file mode 100644 index 0000000..27f90a8 --- /dev/null +++ b/src/lib.ts @@ -0,0 +1,8 @@ +import {UniversalPath} from './util' + +/** + * Get the path to the root of the @extollo/lib package. + */ +export function lib(): UniversalPath { + return new UniversalPath(__dirname) +} diff --git a/src/resources/views/auth/login.pug b/src/resources/views/auth/login.pug new file mode 100644 index 0000000..b428040 --- /dev/null +++ b/src/resources/views/auth/login.pug @@ -0,0 +1,3 @@ +html + body + h1 Extollo Login Page diff --git a/src/service/Routing.ts b/src/service/Routing.ts index d43a28c..b79a724 100644 --- a/src/service/Routing.ts +++ b/src/service/Routing.ts @@ -5,6 +5,8 @@ import {Logging} from './Logging' import {Route} from '../http/routing/Route' import {HTTPMethod} from '../http/lifecycle/Request' import {ViewEngineFactory} from '../views/ViewEngineFactory' +import {ViewEngine} from '../views/ViewEngine' +import {lib} from '../lib' /** * Application unit that loads the various route files from `app/http/routes` and pre-compiles the route handlers. @@ -18,6 +20,9 @@ export class Routing extends Unit { public async up(): Promise { this.app().registerFactory(new ViewEngineFactory()) + const engine = this.make(ViewEngine) + this.logging.verbose('Registering @extollo view engine namespace.') + engine.registerNamespace('extollo', lib().concat('resources', 'views')) for await ( const entry of this.path.walk() ) { if ( !entry.endsWith('.routes.js') ) { diff --git a/src/views/PugViewEngine.ts b/src/views/PugViewEngine.ts index b7b6e0b..37b87c5 100644 --- a/src/views/PugViewEngine.ts +++ b/src/views/PugViewEngine.ts @@ -20,11 +20,8 @@ export class PugViewEngine extends ViewEngine { return compiled(locals) } - if ( !templateName.endsWith('.pug') ) { - templateName += '.pug' - } - const filePath = this.path.concat(...templateName.split(':')) - compiled = pug.compileFile(filePath.toLocal, this.getOptions()) + const filePath = this.resolveName(templateName) + compiled = pug.compileFile(filePath.toLocal, this.getOptions(templateName)) this.compileCache[templateName] = compiled return compiled(locals) @@ -34,9 +31,9 @@ export class PugViewEngine extends ViewEngine { * Get the object of options passed to Pug's compile methods. * @protected */ - protected getOptions(): pug.Options { + protected getOptions(templateName?: string): pug.Options { return { - basedir: this.path.toLocal, + basedir: templateName ? this.resolveBasePath(templateName).toLocal : this.path.toLocal, debug: this.debug, compileDebug: this.debug, globals: [], diff --git a/src/views/ViewEngine.ts b/src/views/ViewEngine.ts index 51ceea1..f8adef5 100644 --- a/src/views/ViewEngine.ts +++ b/src/views/ViewEngine.ts @@ -1,7 +1,7 @@ import {AppClass} from '../lifecycle/AppClass' import {Config} from '../service/Config' import {Container} from '../di' -import {UniversalPath} from '../util' +import {ErrorWithContext, UniversalPath} from '../util' /** * Abstract base class for rendering views via different view engines. @@ -11,6 +11,8 @@ export abstract class ViewEngine extends AppClass { protected readonly debug: boolean + protected readonly namespaces: {[key: string]: UniversalPath} = {} + constructor() { super() this.config = Container.getContainer().make(Config) @@ -38,4 +40,53 @@ export abstract class ViewEngine extends AppClass { * @param locals */ public abstract renderByName(templateName: string, locals: {[key: string]: any}): string | Promise + + public registerNamespace(namespace: string, basePath: UniversalPath): this { + if ( namespace.startsWith('@') ) { + namespace = namespace.substr(1) + } + + this.namespaces[namespace] = basePath + return this + } + + public resolveName(templateName: string): UniversalPath { + let path = this.path + if ( templateName.startsWith('@') ) { + const [namespace, ...parts] = templateName.split(':') + path = this.namespaces[namespace.substr(1)] + + if ( !path ) { + throw new ErrorWithContext('Invalid template namespace: ' + namespace, { + namespace, + templateName, + }) + } + + templateName = parts.join(':') + } + + if ( !templateName.endsWith('.pug') ) { + templateName += '.pug' + } + + return path.concat(...templateName.split(':')) + } + + public resolveBasePath(templateName: string): UniversalPath { + let path = this.path + if ( templateName.startsWith('@') ) { + const [namespace] = templateName.split(':') + path = this.namespaces[namespace.substr(1)] + + if ( !path ) { + throw new ErrorWithContext('Invalid template namespace: ' + namespace, { + namespace, + templateName, + }) + } + } + + return path + } }