Initial Code Commit

This commit is contained in:
Alan Bridgeman 2025-02-17 16:32:27 -06:00
parent fa1468245f
commit 28db152b87
24 changed files with 2691 additions and 2 deletions

38
src/App.ts Normal file
View file

@ -0,0 +1,38 @@
import 'reflect-metadata';
import { Application } from 'express';
import { Initializer } from './Initializer';
/**
* The top level container for running the web app
*/
export class App {
/** The default port to run the server on (if the environment variable isn't set) */
private readonly DEFAULT_PORT = 3000;
/**
* The main entry point for the web app
* This is mostly required because of async/await
*/
async run<T extends Initializer>(initializer?: T, callback?: (app: Application) => void | Promise<void>) {
// Do the initial setup of the app
let app: Application;
if(typeof initializer !== 'undefined') {
app = await initializer.init();
}
else {
app = await (new Initializer()).init();
}
// Start the server
const port = process.env.PORT || this.DEFAULT_PORT;
app.listen(port, async () => {
console.log(`Server is running on port ${port}`);
// Run the callback if one is provided
if(typeof callback !== 'undefined') {
await callback(app);
}
});
}
}

514
src/BaseTemplateCreator.ts Normal file
View file

@ -0,0 +1,514 @@
import path from 'path';
import fse from 'fs-extra';
import { JSDOM } from 'jsdom';
/** The inputs for setting up the meta tags */
type SetupMetaTagInputs = {
/** Description for the Meta description tag */
description?: string,
/** Keywords for the Meta keywords tag */
keywords?: string[],
/** author for the Meta author tag */
author?: string,
}
/** The inputs for the `setupHead` method */
type SetupHeadInputs = SetupMetaTagInputs & {
/** if to include Stripe script(s) */
includeStripe?: boolean,
/** If to include Foundation Framework script(s) */
includeFoundationFramework?: boolean,
/** The Font Awesome kit to include (if applicable) */
fontAwesomeKit?: string
}
/** The inputs for the base template */
export type BaseTemplateInputs = SetupHeadInputs;
/**
* A class to create a base template for an app
*/
export class BaseTemplateCreator {
/** The output of the template */
private output: string;
/** The DOM of the template */
private dom: JSDOM;
/** The document of the template */
private document: Document;
/**
* The constructor for the BaseTemplateCreator class
*
* @param language The human language of the document (default: English/`en`)
*/
constructor(language: string = 'en') {
this.output = '';
// Create the base document
const { dom, document } = this.createBaseDocument(language);
this.dom = dom;
this.document = document;
}
/**
* Create a base document
*
* @param language The human language of the document (default: English/`en`)
* @returns The base document's DOM and document
*/
private createBaseDocument(language = 'en') {
// Create a basic HTML document (as a string)
const docTypeTag = '<!DOCTYPE html>';
const emptyHeadTags = '<head></head>';
const emptyBodyTags = '<body></body>';
const htmlLanguageProperty = `lang="${language}"`;
const basicHtmlTags = `<html ${htmlLanguageProperty}>${emptyHeadTags}${emptyBodyTags}</html>`;
const domStr = `${docTypeTag}${basicHtmlTags}`;
// Create a DOM and document from the string
const dom = new JSDOM(domStr);
const document = dom.window.document;
return { dom, document };
}
/**
* Create the meta tag section of the head tag
*
* @param inputs The inputs to use for setting up the meta tag section of the head tag
* @returns The string representation of the meta tag section of the head tag
*/
private createHeadMetaTagSection(inputs?: SetupMetaTagInputs) {
let output = '';
// Create the meta charset tag
const metaCharsetTag = this.document.createElement('meta');
metaCharsetTag.setAttribute('charset', 'utf-8');
// Create the meta description tag
// Note, if no description is given then we specify a template string that allows specifying it later
const metaDescTag = this.document.createElement('meta');
metaDescTag.name = 'description';
metaDescTag.content = typeof inputs !== 'undefined' && typeof inputs.description !== 'undefined' ? inputs.description : '<% if(typeof description !== \'undefined\') { %><%= description %><% } %>';
// Create the meta keywords tag
// Note, if no keywords are given then we specify a template string that allows specifying it later
const metaKeywordsTag = this.document.createElement('meta');
metaKeywordsTag.name = 'keywords';
metaKeywordsTag.content = typeof inputs !== 'undefined' && typeof inputs.keywords !== 'undefined' ? inputs.keywords.join(', ') : '<% if(typeof keywords !== \'undefined\') { %><%= keywords.join(\', \') %><% } %>';
// Create the meta author tag
// Note, if no author is given then we specify a template string that allows specifying it later
const metaAuthorTag = this.document.createElement('meta');
metaAuthorTag.name = 'author';
metaAuthorTag.content = typeof inputs !== 'undefined' && typeof inputs.author !== 'undefined' ? inputs.author : '<% if(typeof author !== \'undefined\') { %><%= author %><% } %>';
// Create the meta viewport tag
const metaViewportTag = this.document.createElement('meta');
metaViewportTag.name = 'viewport';
const viewportWidth = 'device-width';
const viewportWidthParam = `width=${viewportWidth}`;
const viewportScale = '1.0';
const viewportScaleParam = `initial-scale=${viewportScale}`;
metaViewportTag.content = [viewportWidthParam, viewportScaleParam].join(', ');
output += '\t\t' + metaCharsetTag.outerHTML + '\n';
output += '\t\t' + metaDescTag.outerHTML + '\n';
output += '\t\t' + metaKeywordsTag.outerHTML + '\n';
output += '\t\t' + metaAuthorTag.outerHTML + '\n';
output += '\t\t' + metaViewportTag.outerHTML + '\n';
return output;
}
/**
* Create the title tag for the template
*
* @returns The string representation of the title tag
*/
private createTitleTag() {
// Create the title tag
const titleTag = this.document.createElement('title');
const titlePrefixPortion = '<% if (typeof titlePrefix !== \'undefined\') { %><%= titlePrefix %><% } %>';
const titlePortion = '<% if(typeof title !== \'undefined\') { %><%= title %><% } %>';
const titleSuffixPortion = '<% if (typeof titleSuffix !== \'undefined\') { %><%= titleSuffix %><% } %>';
titleTag.innerHTML = `${titlePrefixPortion}${titlePortion}${titleSuffixPortion}`;
return '\t\t' + titleTag.outerHTML + '\n';
}
/**
* Create the extra styles block for the template
*
* This is a portion of the `<head></head>` tag within the template that allows a individual controller to specify additional styles.
* In particular, it allows a controller to specify additional CSS stylesheets to include in the page.
*
* @returns The string representation of the extra styles block
*/
private createExtraStylesBlock() {
let output = '';
const extraStylesArrayLinkTag = this.document.createElement('link');
extraStylesArrayLinkTag.rel = 'stylesheet';
extraStylesArrayLinkTag.type = 'text/css';
extraStylesArrayLinkTag.href = '/css/<%= style %>.css';
const extraStylesLinkTag = this.document.createElement('link');
extraStylesLinkTag.rel = 'stylesheet';
extraStylesLinkTag.type = 'text/css';
extraStylesLinkTag.href = '/css/<%= extraStyles %>.css';
output += '<%# Add any additional stylesheets specified within a controller etc... %>' + '\n';
output += '<%# This can either be a singular string or a array of strings %>' + '\n';
output += '<%# Note, that the string should be the name of the stylesheet WITHOUT the `.css` extension and exist in the `css/` directory %>' + '\n';
output += '<% if (typeof extraStyles !== \'undefined\') { %>' + '\n';
output += '\t' + '<% if (Array.isArray(extraStyles)) { %>' + '\n';
output += '\t\t' + '<%# Because it\'s an array, we need to loop through each stylesheet and include it %>' + '\n';
output += '\t\t' + '<% for (let style of extraStyles) { %>' + '\n';
output += '\t\t\t' + extraStylesArrayLinkTag.outerHTML + '\n';
output += '\t\t' + '<% } %>' + '\n';
output += '\t' + '<% } else { %>' + '\n';
output += '\t\t' + '<%# Include the singular stylesheet %>' + '\n';
output += '\t\t' + extraStylesLinkTag.outerHTML +'\n';
output += '\t' + '<% } %>' + '\n';
output += '<% } %>';
return output;
}
/**
* Create the styles section of the head tag
*
* @param includeFoundationFramework If to include the Foundation Framework styles
* @returns The string representation of the styles section of the head tag
*/
private createHeadStylesSection(includeFoundationFramework: boolean = false) {
let output = '';
const stylesComment = this.document.createComment('Styles');
output += '\t\t' + `<!-- ${stylesComment.data} -->` + '\n';
const customStyles = this.document.createComment('Custom styling');
output += '\t\t' + `<!-- ${customStyles.data} -->` + '\n';
const baseStylesLinkTag = this.document.createElement('link');
baseStylesLinkTag.rel = 'stylesheet';
baseStylesLinkTag.type = 'text/css';
baseStylesLinkTag.href = '/css/style.css';
output += '\t\t' + baseStylesLinkTag.outerHTML + '\n';
const accessibilityStylesLinkTag = this.document.createElement('link');
accessibilityStylesLinkTag.rel = 'stylesheet';
accessibilityStylesLinkTag.type = 'text/css';
accessibilityStylesLinkTag.href = '/css/accessibility.css';
output += '\t\t' + accessibilityStylesLinkTag.outerHTML + '\n';
if(includeFoundationFramework) {
const foundationFrameworkComment = this.document.createComment('Foundation Framework');
const foundationFrameworkLinkTag = this.document.createElement('link');
foundationFrameworkLinkTag.rel = 'stylesheet';
foundationFrameworkLinkTag.type = 'text/css';
foundationFrameworkLinkTag.href = '/css/app.css';
output += '\t\t' + `<!-- ${foundationFrameworkComment.data} -->` + '\n';
output += '\t\t' + `${foundationFrameworkLinkTag.outerHTML}` + '\n';
}
//const sassComment = this.document.createComment('SASS components');
/*const systemThemedBackgroundLinkTag = this.document.createElement('link');
systemThemedBackgroundLinkTag.rel = 'stylesheet';
systemThemedBackgroundLinkTag.type = 'text/css';
systemThemedBackgroundLinkTag.href = '/css/components/system-themed-background.css';*/
//output += '\t\t' + `<!-- ${sassComment.data} -->` + '\n';
//output += '\t\t' + `${systemThemedBackgroundLinkTag.outerHTML}` + '\n';
const ejsExtraStylesBlock = this.createExtraStylesBlock();
output += '\t\t' + ejsExtraStylesBlock.replaceAll('\n', '\n\t\t') + '\n';
return output;
}
/**
* Create the extra scripts block for the template
*
* This is a portion of the `<head></head>` tag within the template that allows a individual controller to specify additional scripts
* These scripts can come in a few different forms which is one of the reason it requires a template block like this.
*
* @returns The string representation of the extra scripts block
*/
private createExtraScriptsBlock() {
let output = '';
const htmlBlockComment = this.document.createComment('Controller specific scripts');
const arrayItemStringExternalScriptTag = this.document.createElement('script');
arrayItemStringExternalScriptTag.type = 'application/javascript';
arrayItemStringExternalScriptTag.src = '<%= script %>';
const arrayItemStringLocalScriptTag = this.document.createElement('script');
arrayItemStringLocalScriptTag.type = 'application/javascript'
arrayItemStringLocalScriptTag.src = '/js/<%= script %>.js'
arrayItemStringLocalScriptTag.defer = true;
const singularStringExternalScriptTag = this.document.createElement('script');
singularStringExternalScriptTag.type = 'application/javascript'
singularStringExternalScriptTag.src = '<%= extraScripts %>';
const singularStringLocalScriptTag = this.document.createElement('script');
singularStringLocalScriptTag.type = 'application/javascript'
singularStringLocalScriptTag.src = '/js/<%= extraScripts %>.js'
singularStringLocalScriptTag.defer = true;
output += `<!-- ${htmlBlockComment.data} -->` + '\n';
output += '<%# Add any additional scripts specified within a controller etc... %>' + '\n';
output += '<%# %>' + '\n';
output += '<%# Note, that these can come in multiple formats as described in the table below: %>' + '\n';
output += '<%# | Type | Description | Format | Use Cases | %>' + '\n';
output += '<%# | ------ | --------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- | %>' + '\n';
output += '<%# | string | The name of the script to include | `<script name>` | Simple include of the script | %>' + '\n';
output += '<%# | object | An object about the script to include | `{ script: \'<script name>\', defer: <true/false> }` | Being more explicit about script\'s properties (ex. defer vs. async, etc...) | %>' + '\n';
output += '<%# | array | An array of strings or objects (as described above) | `[ \'<script name>\', { script: \'<script name>\', defer: <true/false> } ]` | Include multiple scripts | %>' + '\n';
output += '<%# %>' + '\n';
output += '<%# The string or `.script` property of the object should be the script name WITHOUT the `.js` extension and exist in the `js/` directory if it\'s a "local" script. %>' + '\n';
output += '<%# Or should be the full URL if it\'s a "external" script %>' + '\n';
output += '<% if (typeof extraScripts !== \'undefined\') { %>' + '\n';
output += '\t' + '<% if (Array.isArray(extraScripts)) { %>' + '\n';
output += '\t\t' + '<%# Because it\'s an array, we need to loop through each script and include it %>' + '\n';
output += '\t\t' + '<% for (let script of extraScripts) { %>' + '\n';
output += '\t\t\t' + '<% if(typeof script === \'object\') { %>' + '\n';
output += '\t\t\t\t' + '<%# Because the current array items is an object we use the `.script` and `.defer` properties to include it %>' + '\n';
output += '\t\t\t\t' + '<% if(script.script.startsWith(\'http\') || script.script.startsWith(\'https\')) { %>' + '\n';
output += '\t\t\t\t\t' + '<%# Because the `.script` property starts with `http` or `https` we assume it\'s an "external" script and include it as a straight URL %>' + '\n';
output += '\t\t\t\t\t' + '<script type="application/javascript" src="<%= script.script %>" <% if(script.defer) { %>defer<% } %>></script>' + '\n';
output += '\t\t\t\t' + '<% } else { %>' + '\n';
output += '\t\t\t\t\t' + '<%# Because the `.script` property doesn\'t start with `http` or `https` we assume it\'s a "local" script and include it as a local script (from the `js/` folder and with a `.js` extension) %>' + '\n';
output += '\t\t\t\t\t' + '<script type="application/javascript" src="/js/<%= script.script %>.js" <% if(script.defer) { %>defer<% } %>></script>' + '\n';
output += '\t\t\t\t' + '<% } %>' + '\n';
output += '\t\t\t' + '<% } else { %>' + '\n';
output += '\t\t\t\t' + '<% if(script.startsWith(\'http\') || script.startsWith(\'https\')) { %>' + '\n';
output += '\t\t\t\t\t' + '<%# Because the string starts with `http` or `https` we assume it\'s an "external" script and include it as a straight URL %>' + '\n';
output += '\t\t\t\t\t' + arrayItemStringExternalScriptTag.outerHTML + '\n';
output += '\t\t\t\t' + '<% } else { %>' + '\n';
output += '\t\t\t\t\t' + '<%# Because the string doesn\'t start with `http` or `https` we assume it\'s a "local" script and include it as a local script (from the `js/` folder and with a `.js` extension) %>' + '\n';
output += '\t\t\t\t\t' + arrayItemStringLocalScriptTag.outerHTML + '\n';
output += '\t\t\t\t' + '<% } %>' + '\n';
output += '\t\t\t' + '<% } %>' + '\n';
output += '\t\t' + '<% } %>' + '\n';
output += '\t' + '<% } else if (typeof extraScripts === \'object\') { %>' + '\n';
output += '\t\t' + '<% if(extraScripts.script.startsWith(\'http\') || extraScripts.script.startsWith(\'https\')) { %>' + '\n';
output += '\t\t\t' + '<%# Because the `.script` property of the singular object starts with `http` or `https` we assume it\'s an "external" script and include it as a straight URL %>' + '\n';
output += '\t\t\t' + '<script type="application/javascript" src="<%= extraScripts.script %>" <% if(extraScripts.defer) { %>defer<% } %>></script>' + '\n';
output += '\t\t' + '<% } else { %>' + '\n';
output += '\t\t\t' + '<%# Because the `.script` property of the singular object doesn\'t start with `http` or `https` we assume it\'s a "local" script and include it as a local script (from the `js/` folder and with a `.js` extension) %>' + '\n';
output += '\t\t\t' + '<script type="application/javascript" src="/js/<%= extraScripts.script %>.js" <% if(extraScripts.defer) { %>defer<% } %>></script>' + '\n';
output += '\t\t' + '<% } %>' + '\n';
output += '\t' + '<% } else { %>' + '\n';
output += '\t\t' + '<% if(extraScripts.startsWith(\'http\') || extraScripts.startsWith(\'https\')) { %>' + '\n';
output += '\t\t\t' + '<%# Because the singular string starts with `http` or `https` we assume it\'s an "external" script and include it as a straight URL %>' + '\n';
output += '\t\t\t' + singularStringExternalScriptTag.outerHTML + '\n';
output += '\t\t' + '<% } else { %>' + '\n';
output += '\t\t\t' + '<%# Because the singular string doesn\'t start with `http` or `https` we assume it\'s a "local" script and include it as a local script (from the `js/` folder and with a `.js` extension) %>' + '\n';
output += '\t\t\t' + singularStringLocalScriptTag.outerHTML + '\n';
output += '\t\t' + '<% } %>' + '\n';
output += '\t' + '<% } %>' + '\n';
output += '<% } %>' + '\n';
return output;
}
/**
* Create the scripts section of the head tag
*
* @param includeStripe If to include the Stripe script(s)
* @param includeFoundationFramework If to include the Foundation Framework script(s)
* @param fontAwesomeKit The Font Awesome kit to include (if applicable)
* @returns The string representation of the scripts section of the head tag
*/
private createHeadScriptsSection(includeStripe: boolean = false, includeFoundationFramework: boolean = false, fontAwesomeKit?: string) {
let output = '';
const scriptsComment = this.document.createComment('Scripts');
output += '\t\t' + `<!-- ${scriptsComment.data} -->` + '\n';
if (includeStripe) {
const stripeScriptTag = this.document.createElement('script');
stripeScriptTag.src = 'https://js.stripe.com/v3';
stripeScriptTag.async = true;
output += '\t\t' + stripeScriptTag.outerHTML + '\n';
}
if(typeof fontAwesomeKit !== 'undefined') {
const fontAwesomeScriptTag = this.document.createElement('script');
fontAwesomeScriptTag.src = `https://kit.fontawesome.com/${fontAwesomeKit}.js`;
fontAwesomeScriptTag.crossOrigin = 'anonymous';
output += '\t\t' + fontAwesomeScriptTag.outerHTML + '\n'
}
if(includeFoundationFramework) {
const foundationFrameworkComment = this.document.createComment('Foundation Framework');
const foundationFrameworkScriptTag = this.document.createElement('script');
foundationFrameworkScriptTag.type = 'application/javascript';
foundationFrameworkScriptTag.src = '/js/foundation/main.js';
foundationFrameworkScriptTag.defer = true;
output += '\t\t' + `<!-- ${foundationFrameworkComment.data} -->` + '\n';
output += '\t\t' + foundationFrameworkScriptTag.outerHTML + '\n';
}
const ejsExtraScriptsBlock = this.createExtraScriptsBlock();
output += '\t\t' + ejsExtraScriptsBlock.replaceAll('\n', '\n\t\t') + '\n';
return output;
}
/**
* Setup the head portion of the document (`<head></head>` tag)
*
* @param inputs The inputs to use for setting up the head portion of the document
* @returns The string representation of the head portion of the document
*/
private setupHead(inputs?: SetupHeadInputs) {
const headTag = this.document.querySelector('head');
if (!headTag) {
console.error('Head tag not found');
return;
}
const includeFoundationFramework = typeof inputs !== 'undefined' && typeof inputs.includeFoundationFramework !== 'undefined' ? inputs.includeFoundationFramework : false;
const includeStripe = typeof inputs !== 'undefined' && typeof inputs.includeStripe !== 'undefined' ? inputs.includeStripe : false;
let headTagContents = headTag.innerHTML + '\n';
headTagContents += this.createHeadMetaTagSection(inputs);
headTagContents += this.createTitleTag();
headTagContents += this.createHeadStylesSection(includeFoundationFramework);
headTagContents += this.createHeadScriptsSection(includeStripe, includeFoundationFramework, inputs?.fontAwesomeKit);
headTag.innerHTML = headTagContents;
}
/**
* Create the header block for the template
*
* @returns The string representation of the header block
*/
private createHeaderBlock() {
let output = '';
output += "<% if(typeof header !== 'undefined') { %>" + '\n';
output += '\t' + '<%- include(header) %>' + '\n';
output += '<% } else { %>' + '\n';
output += '\t' + "<%- include('includes/header.ejs') %>" + '\n';
output += '<% } %>';
return output;
}
/**
* Create the footer block for the template
*
* @return The string representation of the footer block
*/
private createFooterBlock() {
let output = '';
output += "<% if(typeof footer !== 'undefined') { %>" + '\n';
output += '\t' + '<%- include(footer) %>' + '\n';
output += '<% } else { %>' + '\n';
output += '\t' + "<%- include('includes/footer.ejs') %>" + '\n';
output += '<% } %>';
return output;
}
/**
* Setup the body portion of the document (`<body></body>` tag)
*
* @returns The string representation of the body portion of the document
*/
private setupBody() {
const skipLink = this.document.createElement('a');
skipLink.id = 'skip-link';
skipLink.href = '#main';
skipLink.innerText = 'Skip to main content';
this.document.body.appendChild(skipLink);
const contentsDiv = this.document.createElement('div');
contentsDiv.id = 'contents';
const headerElem = this.document.createElement('header');
let ejsHeaderIncludeBlock = this.createHeaderBlock();
headerElem.innerHTML = ejsHeaderIncludeBlock;
contentsDiv.appendChild(headerElem);
const mainElem = this.document.createElement('main');
mainElem.id = 'main';
const containerDiv = this.document.createElement('div');
containerDiv.classList.add('container');
const contentDiv = this.document.createElement('div');
contentDiv.classList.add('content');
contentDiv.innerHTML = '<%- include(page) %>';
containerDiv.appendChild(contentDiv);
mainElem.appendChild(containerDiv);
contentsDiv.appendChild(mainElem);
const footerElem = this.document.createElement('footer');
const ejsFooterIncludeBlock = this.createFooterBlock();
footerElem.innerHTML = ejsFooterIncludeBlock;
contentsDiv.appendChild(footerElem);
this.document.body.appendChild(contentsDiv);
}
/**
* Prepare the output of the template for writing to a file
*/
private prepare() {
this.output = this.dom.serialize()
// Fixing the EJS tags
.replaceAll('&lt;%', '<%')
.replaceAll('%&gt;', '%>')
// Logical operators (particularly used within EJS tags)
.replaceAll('&amp;&amp;', '&&')
.replaceAll(' &gt; ', ' > ')
.replaceAll(' &lt; ', ' < ');
}
/**
* Write the template out to a `base.ejs` file
*
* @param folder The folder where the file should be written to
*/
private write(folder: string) {
fse.writeFileSync(path.resolve(folder, 'base.ejs'), this.output);
}
/**
* The static method to create a base template
*
* @param folder The folder to write the base template to
* @param baseTemplateInputs The inputs to use for the base template
*/
static create(folder: string = 'pages', baseTemplateInputs?: BaseTemplateInputs) {
// Create the base template creator object (sets up an initial document to work with)
const templateCreator = new BaseTemplateCreator();
// Sets up the head portion of the document
templateCreator.setupHead(baseTemplateInputs);
templateCreator.setupBody();
templateCreator.prepare();
templateCreator.write(folder);
}
}

113
src/Initializer.ts Normal file
View file

@ -0,0 +1,113 @@
import express, { Application, RequestHandler } from 'express';
import { Router } from './Router';
import { StaticFileResolver } from './StaticFileResolver';
import { Renderer } from './Renderer';
/**
* Object to encapsulate the setup of the app
*
* This is passed to the App object's `run` method.
* This approach allows easy injection of customizations to the app setup with minimal code.
* This is done by creating a customized version of the Initializer class (setting the constructor parameters).
* Or by creating a child class of the Initializer class and overriding the `init` method.
*/
export class Initializer {
/** The path to the controllers */
private controllersPath?: string;
/** The path to the static files (css, js, etc...) */
private staticFilesPath?: string;
/** The view engine and path to the view files */
private view?: { filesPath: string, engine?: string };
/** The middlewares to use */
private middlewares: ((...args: any[]) => RequestHandler)[];
/**
* Create a new Initializer
*
* Note, that the parameter is an object of inputs that can be used to customize the setup.
* It's done this way because most of the customizations are optional and order doesn't matter.
*
* @param inputs The inputs for the initializer
* @param inputs.controllersPath The path to the controllers
* @param inputs.staticFilesPath The path to the static files (css, js, etc...)
* @param inputs.view.engine The view engine to use (ex. 'ejs')
* @param inputs.view.filesPath The path to the view files
* @param middlewares Th middlewares to use
*/
constructor(inputs?: { controllersPath?: string, staticFilesPath?: string, view?: { filesPath: string, engine?: string } }, ...middlewares: ((...args: any[]) => RequestHandler)[]) {
this.controllersPath = typeof inputs !== 'undefined' && inputs.controllersPath !== 'undefined' ? inputs.controllersPath : undefined;
this.staticFilesPath = typeof inputs !== 'undefined' && inputs.staticFilesPath !== 'undefined' ? inputs.staticFilesPath : undefined;
this.view = typeof inputs !== 'undefined' && typeof inputs.view !== 'undefined' ? inputs.view : undefined;
this.middlewares = middlewares;
}
/**
* Create the Express app
*
* @returns The newly created Express app
*/
private async createExpressApp(): Promise<Application> {
// Create the Express app
const app = express();
return app;
}
/**
* Setup the global middleware for the server
*
* @param app The Express app to setup the middleware on
*/
private async setupMiddleware(app: Application) {
this.middlewares.forEach(middleware => app.use(middleware()));
}
/**
* Do the initial setup for the application
*
* @returns The Express app that gets created and setup
*/
async init(): Promise<Application> {
// Create the Express app
const app = await this.createExpressApp();
// Setup global middleware
await this.setupMiddleware(app);
// Setup the router (how the app handles requests)
if(typeof this.controllersPath !== 'undefined') {
await (new Router(this.controllersPath)).setup(app);
}
else {
await (new Router()).setup(app);
}
// Setup the static file resolver (how the app serves static files)
if(typeof this.staticFilesPath !== 'undefined') {
await (new StaticFileResolver(this.staticFilesPath)).setup(app);
}
else {
await (new StaticFileResolver()).setup(app);
}
// Setup the renderer (how the app renders templates - templates can use any Express supported view engine)
if(typeof this.view !== 'undefined') {
if(typeof this.view.engine !== 'undefined') {
await (new Renderer(this.view.filesPath, this.view.engine)).setup(app);
}
else {
await (new Renderer(this.view.filesPath)).setup(app);
}
}
else {
await (new Renderer()).setup(app);
}
return app;
}
}

53
src/Renderer.ts Normal file
View file

@ -0,0 +1,53 @@
import { existsSync, statSync } from 'fs';
import path from 'path';
import { Application } from 'express';
import { BaseTemplateInputs, BaseTemplateCreator } from './BaseTemplateCreator';
export class Renderer {
/** The default folder name for the views */
private readonly DEFAULT_VIEWS_FOLDER = 'pages';
/** The view engine to use (Ex. EJS, Pug, etc...) */
private engine: string;
/** The path to the folder/directory that contains the view files (Ex. EJS files) */
private viewsDir: string;
/**
* Creates a new instance of the Renderer class
*
* Note, that if the base template doesn't exist and the engine is `ejs`, then a base template will be created automatically.
*
* @param viewsDir The path to the folder/directory that contains the view files
* @param engine The view engine to use (Ex. EJS, Pug, etc...)
* @param baseTemplateInputs The inputs to use for the base template (if the template is generated)
* @throws Error if the pages path is not a valid directory
*/
constructor(viewsDir: string = path.join(process.cwd(), this.DEFAULT_VIEWS_FOLDER), engine: string = 'ejs', baseTemplateInputs?: BaseTemplateInputs) {
// Verify the views directory exists and is a directory
if(!existsSync(viewsDir) || !statSync(viewsDir).isDirectory()) {
throw new Error('The views directory must be a valid directory');
}
this.viewsDir = path.resolve(viewsDir);
this.engine = engine;
// We can only automatically generate a base template if the engine is `ejs`
// We also don't want to generate a base template if one already exists
if(this.engine === 'ejs' && !existsSync(path.resolve(this.viewsDir, 'base.ejs'))) {
BaseTemplateCreator.create(this.viewsDir, baseTemplateInputs);
}
}
/**
* Sets up the views for the Express app
*
* @param app The Express app to setup the views for
*/
setup(app: Application) {
// Set the view engine to the appropriate value (ex. `ejs`) and to serve views from the view files directory
app.set('view engine', this.engine);
app.set('views', this.viewsDir);
}
}

244
src/Router.ts Normal file
View file

@ -0,0 +1,244 @@
import path from 'path';
import { Application } from 'express';
import fse from 'fs-extra';
import { BaseController } from './controllers/BaseController';
/**
* The Router sets up the routes/endpoints for the App.
*
* More specifically, it does automatic detection of controllers (and their routes) within the specified folder.
*
* A controller is a class that extends the `BaseController` class and uses the `@Controller` decorator.
* Routes within a controller are defined by methods that use the `@GET` or `@POST` decorators.
*/
export class Router {
/** The default folder name for controllers */
private readonly DEFAULT_CONTROLLERS_FOLDER = 'routes';
/** The path to the controllers folder */
private controllersPath: string;
/**
* Create a new Router
*
* @param controllersPath The path to the controllers folder (default is 'routes' in the current working directory)
* @throws Error if the controllers path is not a valid directory
*/
constructor(controllersPath: string = path.join(process.cwd(), this.DEFAULT_CONTROLLERS_FOLDER)) {
if(!fse.existsSync(controllersPath) || !fse.statSync(controllersPath).isDirectory()) {
throw new Error('The controllers path must be a valid directory');
}
this.controllersPath = path.resolve(controllersPath);
}
/**
* Check if a file is a controller
*
* Note, if the specified "file" is a directory, this method will recurse into the directory and return all controllers found in the directory.
*
* @param file The file to check if it is a controller
* @param folder The folder that the file is in
* @returns The controller class (or list of controller classes) in the "file" or null if the file is not a controller
*/
private async checkIfControllerFile(file: string, folder: string): Promise<any /*BaseController*/ | any[]/*BaseController[]*/ | null> {
// Get the path to the specific file
const controllerPath = path.join(folder, file);
// Check if the path is a directory/folder
const stats = await fse.stat(controllerPath);
if(stats.isDirectory()) {
// Recurse into the directory
const controllersFoundInDir = await Promise.all((await fse.readdir(controllerPath)).map(file => this.checkIfControllerFile(file, controllerPath)));
const controllersInDir = controllersFoundInDir.filter(controller => controller !== null);
return controllersInDir.length === 0 ? null : controllersInDir.flat();
}
// We only want to load JavaScript files (Note, Typescript files get compiled to JavaScript files)
if(!controllerPath.endsWith('.js')) {
return null;
}
// Load the file as a module
const controllerModule = require(controllerPath);
console.log(`${controllerPath} loaded successfully: ${JSON.stringify(Object.keys(controllerModule))} exports found.`)
// Get all the classes in the module that are controllers
//
// Note: We filter by if the class extends the `BaseController` class.
// This is because native JavaScript doesn't have decorators (which are used to mark a class as a controller).
// Consequently, we enforce that all controllers in addition to using the decorator must extend the `BaseController` class so that we can identify them.
// Further, note we can't do this as part of the decorator because of the way decorators get transpiled to JavaScript.
const controllers = Object.keys(controllerModule)
.filter(key => typeof controllerModule[key] === 'function' && /^\s*class\s+/.test(controllerModule[key].toString()))
.filter(exportedClassName => controllerModule[exportedClassName].prototype instanceof BaseController)
.map(controllerClassName => {
return controllerModule[controllerClassName];
});
switch(controllers.length) {
case 0:
return null;
case 1:
return controllers[0];
default:
return controllers.flat();
}
}
/**
* Get the routes in a controller
*
* A route is defined by a method in the controller that has the `@GET` or `@POST` decorator.
*
* @param controller The controller to get the routes from
* @returns The routes in the controller
*/
private getRoutesInController(controller: any) {
// Loop over all the methods in the provided class looking for methods that use the GET decorator
const pathsGET = Object.getOwnPropertyNames(controller.prototype)
// Find all methods that have a Get metadata key (GET decorator)
.filter((method) => Reflect.getMetadata('Get', controller.prototype, method))
.map((method) => {
// Get the method
const fn = controller.prototype[method];
// Get the path
const path = Reflect.getMetadata('Get', controller.prototype, method) as string;
return path;
});
// Loop over all the methods in the provided class looking for methods that use the POST decorator
const pathsPOST = Object.getOwnPropertyNames(controller.prototype)
// Find all methods that have a Post metadata key (POST decorator)
.filter((method) => Reflect.getMetadata('Post', controller.prototype, method))
.map((method) => {
// Get the method
const fn = controller.prototype[method];
// Get the metadata object (which contains a path and middleware)
const postRoute = Reflect.getMetadata('Post', controller.prototype, method);
const path = postRoute.path;
return path;
});
return {
GET: pathsGET,
POST: pathsPOST
}
}
/**
* Setup the controllers for the application
*
* This loops over all the "files" in the directory (this includes subdirectories) and finds any controllers.
* A controller being a class that extends the BaseController class and uses the `@Controller` decorator.
* Once it knows the controllers it adds the routes they contain to the Express app.
* Routes are defined by methods in the controller that have the `@GET` or `@POST` decorators.
*
* @param app The Express app to add the routes to
*/
private async setupControllers(app: Application) {
// Get the list of files in the controllers folder
const files = await fse.readdir(this.controllersPath);
// Get the controller classes from the files
const loadedControllers = (await Promise.all(
files.map(file => this.checkIfControllerFile(file, this.controllersPath/*, app*/))
)).filter(controller => controller !== null);
const addedRoutes: string[] = [];
// Get all controllers that have the child controller decorator
const controllersWithChildControllers = loadedControllers.filter(controller => Reflect.getMetadata('ChildController', controller) !== undefined);
// Get all those controllers that are designated as children controllers by parent controllers
const childrenControllers = controllersWithChildControllers.map(controller => Reflect.getMetadata('ChildController', controller));
// Get the exclusive set of controllers that don't fit in the group of controllers with child controllers OR a child controller themselves
const controllersWithoutChildren = loadedControllers.filter(controller => !controllersWithChildControllers.includes(controller) && !childrenControllers.includes(controller));
// Take the list of controllers with child controllers and the list without children (that excludes children themselves)
// And forma a list that has all child controllers remove
const topLevelControllers = [...controllersWithChildControllers, ...controllersWithoutChildren];
// Add the routes for the child controllers to the list of added routes WITHOUT calling the setup method (as this is called in the parent controller's setup method)
childrenControllers
.forEach(decoratedController => {
if(Array.isArray(decoratedController)) {
decoratedController.forEach(decoratedControllerCls => {
const controller = Reflect.getMetadata('originalClass', decoratedControllerCls);
const routes = this.getRoutesInController(controller);
routes.GET.forEach((path: string) => {
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
});
routes.POST.forEach((path: string) => {
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
});
});
}
else {
const controller = Reflect.getMetadata('originalClass', decoratedController);
const routes = this.getRoutesInController(controller);
routes.GET.forEach((path: string) => {
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
});
routes.POST.forEach((path: string) => {
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
});
}
});
// Add the routes for the top level controllers to the list of added routes and call the setup method
topLevelControllers
.forEach(decoratedController => {
if(Array.isArray(decoratedController)) {
decoratedController.forEach(decoratedControllerCls => {
const controller = Reflect.getMetadata('originalClass', decoratedControllerCls);
const routes = this.getRoutesInController(controller);
routes.GET.forEach((path: string) => {
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
});
routes.POST.forEach((path: string) => {
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
});
decoratedControllerCls.setup(app);
});
}
else {
const controller = Reflect.getMetadata('originalClass', decoratedController);
const routes = this.getRoutesInController(controller);
routes.GET.forEach((path: string) => {
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
});
routes.POST.forEach((path: string) => {
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
});
(decoratedController as any).setup(app);
}
});
console.log('Routes added:');
addedRoutes.forEach(route => console.log('\t' + route));
}
/**
* Setup the routes for the application
*
* @param app The Express app to add the routes to
*/
async setup(app: Application) {
console.log('Setting up routes...')
await this.setupControllers(app);
}
}

47
src/StaticFileResolver.ts Normal file
View file

@ -0,0 +1,47 @@
import { existsSync, statSync } from 'fs';
import path from 'path';
import express, { Application } from 'express';
/**
* A class that wraps around Express's static file serving functionality.
*
* This is mostly future proofing if additional logic/functionality around serving static files becomes necessary.
*/
export class StaticFileResolver {
/** The default folder name for the static files */
private readonly DEFAULT_STATIC_FOLDER = 'static';
/**
* The path to the folder/directory that contains the static files
*
* The folder should contain the following sub-folders at minimum:
* - css
* - js
* - img
*/
private staticFilesDir: string;
/**
* Create a new instance of the StaticFileResolver
*
* @param staticFilesDir The path to the folder/directory that contains the static files. Defaults to the 'static' folder in the current working directory.
* @throws Error if the staticFilesDir is not a valid directory
*/
constructor(staticFilesDir: string = path.join(process.cwd(), this.DEFAULT_STATIC_FOLDER)) {
if(!existsSync(staticFilesDir) || !statSync(staticFilesDir).isDirectory()) {
throw new Error('The static files path must be a valid directory');
}
this.staticFilesDir = path.resolve(staticFilesDir);
}
/**
* Setup the Express app to serve static files from the static directory
*
* @param app The Express application to setup the static file serving on.
*/
setup(app: Application) {
// Serve static files from the static directory
app.use(express.static(this.staticFilesDir));
}
}

75
src/bin/create-project.ts Normal file
View file

@ -0,0 +1,75 @@
import fs from 'fs';
import { exec } from 'child_process';
function writePackageJson() {
// Run `npm init` and appropriate `npm install` commands
exec('npm init -y', (error, stdout, stderr) => { console.log(stdout); });
exec('npm install --save express', (error, stdout, stderr) => { console.log(stdout); });
exec('npm install --save-dev @types/express @types/node gulp gulp-typescript nodemon ts-node typescript', (error, stdout, stderr) => { console.log(stdout); });
// Add build script section to package.json
const packageJson = JSON.parse(fs.readFileSync('package.json').toString());
if(typeof packageJson.scripts === 'undefined') {
packageJson.scripts = {};
}
if(typeof packageJson.scripts.start === 'undefined') {
packageJson.scripts.start = 'node dist/server.js';
}
if(typeof packageJson.scripts.build === 'undefined') {
packageJson.scripts.dev = 'gulp';
}
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
}
function createControllersDir() {
// Create a routes folder to keep all controller files and copy in the example HomeRoutes file
fs.mkdirSync('src/routes');
fs.copyFileSync('examples/HomeRoutes.ts', 'src/routes/HomeRoutes.ts');
}
function createStaticFilesDir() {
// Create a static folder to keep all static files (css, js, images, etc...) and copy in the example stylesheet
fs.mkdirSync('static');
fs.mkdirSync('static/css');
fs.copyFileSync('exmples/styles.css', 'src/static/css/styles.css')
fs.mkdirSync('static/js');
fs.mkdirSync('static/img');
}
function createViewsDir() {
fs.mkdirSync('pages');
fs.copyFileSync('examples/base.ejs', 'src/pages/base.ejs');
}
function createSrcDir() {
// Create a source folder to keep all source files
fs.mkdirSync('src');
// Copy the example server file to the source folder (starts the app/server)
fs.copyFileSync('examples/server.ts', 'src/server.ts')
createControllersDir();
createStaticFilesDir();
createViewsDir();
}
// Create a package.json file with the necessary dependencies
writePackageJson();
// Create a source folder to keep all source files in
createSrcDir();
// Copy the example gulpfile to the root of the project
fs.copyFileSync('examples/gulpfile.mjs', 'gulpfile.mjs');
// Run the build script to compile the project
exec('npm run build', (error, stdout, stderr) => { console.log(stdout); });
// Start the server
exec('npm run start', (error, stdout, stderr) => { console.log(stdout); });

View file

@ -0,0 +1,5 @@
import { Application } from 'express';
export abstract class BaseController {
static setup(app: Application) {}
}

3
src/controllers/index.ts Normal file
View file

@ -0,0 +1,3 @@
import { BaseController } from './BaseController';
export { BaseController };

View file

@ -0,0 +1,8 @@
import { BaseController } from '../controllers/BaseController';
export function ChildController<T extends { new(...args: any[]): {} }>(childController: BaseController | BaseController[]) {
return function (target: T) {
// Define a metadata key for the child controller
Reflect.defineMetadata('ChildController', childController, target);
};
}

View file

@ -0,0 +1,95 @@
import { Application, NextFunction } from 'express';
import { BaseController } from '../controllers/BaseController';
/**
* Class decorator to "fill in" the setup method which is called on server startup and connects the methods/functions and their desired paths in the express app.
*
* That is, this is largely for the convenience of controller writers/developers so that they don't have to manually create a `setup` method in their controller classes.
*
* @example
* The following example show how to use the Controller decorator to setup a path `/path` with a GET and POST method.
* ```ts
* import { Request, Response } from 'express';
*
* import { Controller } from '../decorators/Controller';
* import { GET } from '../decorators/GET';
* import { POST } from '../decorators/POST';
*
* import { BaseController } from './BaseController';
*
* @Controller()
* export class MyController extends BaseController {
* @GET('/path')
* private myGetMethod(req: Request, res: Response) {}
*
* @POST('/path', express.json())
* private myPostMethod(req: Request, res: Response) {}
* }
* ```
*/
export function Controller<T extends { new (...args: any[]): BaseController }>() {
// Technically the function we return here with the target parameter is the actual decorator itself but we wrap it in case we ever want to add parameters to the decorator
return function(target: T){
Reflect.defineMetadata('originalClass', target, target);
// We extend the class that is decorated and override the setup method to automatically setup the routes
return class extends target {
/**
* Setup the routes for the controller.
*
* @param app The express application to setup the routes on.
*/
static setup(app: Application) {
// If the decorated class is also decorated with the `@ChildController` decorator,
// then we call the child controller's setup method as well.
//
// Currently, there is very little linkage between the decorated controller class and the child controller class(es).
// Though in the future this may look more like forcing these to be on sub-paths of the parent controller etc...
const childControllers = Reflect.getMetadata('ChildController', target);
if(typeof childControllers !== 'undefined') {
if(Array.isArray(childControllers)) {
childControllers.forEach((childController) => {
childController.setup(app);
});
} else {
childControllers.setup(app);
}
}
const controller = new (Reflect.getMetadata('originalClass', target))();
// Loop over all the methods in the decorated class looking for methods that use the GET decorator
Object.getOwnPropertyNames(target.prototype)
// Find all methods that have a Get metadata key (GET decorator)
.filter((method) => Reflect.getMetadata('Get', target.prototype, method))
.map((method) => {
// Get the method
const fn = target.prototype[method];
// Get the path
const path = Reflect.getMetadata('Get', target.prototype, method);
// Bind the method to the class instance
app.get(path, fn.bind(controller));
});
// Loop over all the methods in the decorated class looking for methods that use the POST decorator
Object.getOwnPropertyNames(target.prototype)
// Find all methods that have a Post metadata key (POST decorator)
.filter((method) => Reflect.getMetadata('Post', target.prototype, method))
.map((method) => {
// Get the method
const fn = target.prototype[method];
// Get the metadata object (which contains a path and middleware)
const postRoute = Reflect.getMetadata('Post', target.prototype, method);
const path = postRoute.path;
const middleware: NextFunction[] = postRoute.middleware;
// Bind the method to the class instance
app.post(path, ...middleware, fn.bind(controller));
});
}
}
}
}

14
src/decorators/GET.ts Normal file
View file

@ -0,0 +1,14 @@
/**
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
*
* This in conjunction with the `@Controller` decorator will automatically setup a GET route that executes the decorated method.
* The specific path for the route is defined by the path parameter.
*
* @param path The path for the GET route.
*/
export function GET(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Define a `Get` metadata key with the path as the value on the target's propertyKey
Reflect.defineMetadata('Get', path, target, propertyKey);
};
}

19
src/decorators/POST.ts Normal file
View file

@ -0,0 +1,19 @@
import { NextFunction } from 'express';
import { NextHandleFunction } from 'connect';
/**
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
*
* This in conjunction with the `@Controller` decorator will automatically setup a POST route that executes the decorated method.
* The specific path for the route is defined by the path parameter.
* Controller authors can also specify middleware to be used with the POST route (because a middleware is commonly required to parse the body of a POST request).
*
* @param path The path for the GET route.
* @param middleware The middleware to use with the POST route.
*/
export function POST(path: string, ...middleware: (NextHandleFunction | NextFunction)[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Define a `Get` metadata key with the path as the value on the target's propertyKey
Reflect.defineMetadata('Post', { path, middleware }, target, propertyKey);
};
}

45
src/decorators/Page.ts Normal file
View file

@ -0,0 +1,45 @@
import { Request, Response } from 'express';
export function Page(title: string, page: string, extraScripts: (string | { script: string, defer: boolean })[] = [], extraStyles: string[] = [], ...otherParams: any[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (req: Request, res: Response) {
// We run the original here so that if the decorated method has specific checks it needs to make (ex. if the ID of whatever actually exists) it can make them before rendering the page
//
// !!!IMPORTANT!!!
// This currently is kind of a hacky solution, because we bind to the target, which is the prototype rather than the instance.
// This means instance data is not properly available/accessible within the decorated method.
// However, you can use the `this` keyword to access other methods on the class, etc...
//
// The above (this approach) is necessary at time of writing because the `this` (which should be bound to for the instance) seems to be undefined within the context of the decorator function here for some reason
// Ideally, would dive into this and do it properly but this seems to be good enough for now.
const output = await original.bind(target)(req, res);
// If the output is false, we don't want to render the page
//
// This allows the decorated method to handle the response itself
// Ex. Logging in, if the user is already logged in we want to redirect them, not render the login page again
if(typeof output === 'boolean' && !output) {
return;
}
const renderParams: { [key: string]: any } = {
title: title,
page: page,
extraStyles: extraStyles,
extraScripts: extraScripts,
...otherParams
};
// If the decorated method's output is an object, we want to merge it with the renderParams
if(typeof output === 'object') {
Object.entries(output).forEach((entry) => {
renderParams[entry[0]] = entry[1];
});
}
res.render('base', renderParams);
}
}
}

13
src/decorators/index.ts Normal file
View file

@ -0,0 +1,13 @@
import { Controller } from './Controller';
import { ChildController } from './ChildController';
import { Page } from './Page';
import { GET } from './GET';
import { POST } from './POST';
export {
Controller,
ChildController,
Page,
GET,
POST
}

16
src/index.ts Normal file
View file

@ -0,0 +1,16 @@
import { App } from './App';
import { Initializer } from './Initializer';
import { Router } from './Router';
import { Renderer } from './Renderer';
import { StaticFileResolver } from './StaticFileResolver';
export {
App,
Initializer,
Router,
Renderer,
StaticFileResolver
};
export * from './controllers';
export * from './decorators';
export * from './middlewares';

View file

@ -0,0 +1,97 @@
import { Request, Response, NextFunction, RequestHandler } from 'express';
/**
* Class that creates middleware to set global template values for all pages.
*/
class GlobalTemplateValuesMiddleware {
/** The description of the website. */
private description?: string;
/** The keywords for the website. */
private keywords?: string;
/** The author of the website. */
private author?: string;
/** The prefix for the title of the website. */
private titlePrefix?: string;
/** The suffix for the title of the website. */
private titleSuffix?: string;
/** Other values to set. */
private otherValues: { [key: string]: string } = {};
/**
* Constructor for the GlobalTemplateValuesMiddleware class.
*
* @param options Options for the middleware.
* @param options.description The description of the website.
* @param options.keywords The keywords for the website.
* @param options.author The author of the website.
* @param options.titlePrefix The prefix for the title of the website.
* @param options.titleSuffix The suffix for the title of the website.
* @param options[key] Any other values to set.
*/
constructor(options: { description?: string, keywords?: string, author?: string, titlePrefix?: string, titleSuffix?: string, [key: string]: string | undefined }) {
this.description = options.description;
this.keywords = options.keywords;
this.author = options.author;
this.titlePrefix = options.titlePrefix;
this.titleSuffix = options.titleSuffix;
Object.keys(options).forEach(key => {
if(!['company', 'description', 'keywords', 'author', 'titlePrefix', 'titleSuffix'].includes(key)) {
this.otherValues[key] = options[key] as string;
}
})
}
/**
* Creates the middleware function.
*
* @returns The middleware function.
*/
middleware(): RequestHandler {
return async (req: Request, res: Response, next: NextFunction) => {
if(typeof this.description !== 'undefined') {
res.locals.description = this.description;
}
if(typeof this.keywords !== 'undefined') {
res.locals.keywords = this.keywords;
}
if(typeof this.author !== 'undefined') {
res.locals.author = this.author;
}
if(typeof this.titlePrefix !== 'undefined') {
res.locals.titlePrefix = this.titlePrefix;
}
if(typeof this.titleSuffix !== 'undefined') {
res.locals.titleSuffix = this.titleSuffix;
}
Object.entries(this.otherValues).forEach(([key, value]: [string, string]) => {
res.locals[key] = value;
});
// Continue to the next middleware
next();
}
}
}
/**
* Middleware wrapper function to set global template values for all pages.
*
* @param options Options for the middleware.
* @param options.description The description of the website.
* @param options.keywords The keywords for the website.
* @param options.author The author of the website.
* @param options.titlePrefix The prefix for the title of the website.
* @param options.titleSuffix The suffix for the title of the website.
* @returns The middleware function.
*/
export function globalTemplateValues(options: { description?: string, keywords?: string, author?: string, titlePrefix?: string, titleSuffix?: string, [key: string]: string | undefined }) {
const instance = new GlobalTemplateValuesMiddleware({ ...options });
return instance.middleware.bind(instance);
}

5
src/middlewares/index.ts Normal file
View file

@ -0,0 +1,5 @@
import { globalTemplateValues } from './GlobalTemplateValuesMiddleware';
export {
globalTemplateValues
}