From e5a08eed844f177b0f365f882a20c7b229715bdd Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Mon, 6 Jul 2020 11:19:27 -0500 Subject: [PATCH 1/5] fix: update code to es2015+ --- .eslintrc.js | 3 + examples/advanced/package.json | 2 +- examples/advanced/public/echo.js | 8 +- examples/advanced/server.js | 18 +- examples/basic/package.json | 2 +- examples/basic/server.js | 6 +- index.js | 2 +- lib/express-handlebars.js | 529 +++++++++++++++---------------- 8 files changed, 271 insertions(+), 299 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c5bfe385..3c957c7f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,9 @@ module.exports = { "comma-dangle": ["error", "always-multiline"], indent: ["error", "tab", { SwitchCase: 1 }], "no-tabs": "off", + "no-var": "error", + "prefer-const": "error", + "object-shorthand": "error", "no-restricted-globals": [ "error", { diff --git a/examples/advanced/package.json b/examples/advanced/package.json index cf21a87e..b594db44 100644 --- a/examples/advanced/package.json +++ b/examples/advanced/package.json @@ -11,6 +11,6 @@ "author": "Eric Ferraiuolo ", "license": "BSD", "dependencies": { - "express": "^4.7.2" + "express": "^4.17.1" } } diff --git a/examples/advanced/public/echo.js b/examples/advanced/public/echo.js index dda3bfa1..4d5b2c15 100644 --- a/examples/advanced/public/echo.js +++ b/examples/advanced/public/echo.js @@ -5,13 +5,13 @@ // This will prompt the user for a message, then echo it out by rendering the // message using the shared template which was exposed by the server. (function () { - var button = document.getElementById("say"); + const button = document.getElementById("say"); button.addEventListener("click", function (e) { - var message = prompt("Say Something:", "Yo yo"); - var echo = document.createElement("div"); + const message = prompt("Say Something:", "Yo yo"); + const echo = document.createElement("div"); - echo.innerHTML = Handlebars.templates.echo({ message: message }); + echo.innerHTML = Handlebars.templates.echo({ message }); document.body.appendChild(echo); }, false); }()); diff --git a/examples/advanced/server.js b/examples/advanced/server.js index c2064fc3..a7ebc7b2 100644 --- a/examples/advanced/server.js +++ b/examples/advanced/server.js @@ -1,16 +1,16 @@ "use strict"; -var Promise = global.Promise || require("promise"); +const Promise = global.Promise || require("promise"); -var express = require("express"); -var exphbs = require("../../"); // "express-handlebars" -var helpers = require("./lib/helpers"); +const express = require("express"); +const exphbs = require("../../"); // "express-handlebars" +const helpers = require("./lib/helpers"); -var app = express(); +const app = express(); // Create `ExpressHandlebars` instance with a default layout. -var hbs = exphbs.create({ - helpers: helpers, +const hbs = exphbs.create({ + helpers, // Uses multiple partials dirs, templates in "shared/templates/" are shared // with the client-side of the app (see below). @@ -34,7 +34,7 @@ function exposeTemplates (req, res, next) { precompiled: true, }).then(function (templates) { // RegExp to remove the ".handlebars" extension from the template names. - var extRegex = new RegExp(hbs.extname + "$"); + const extRegex = new RegExp(hbs.extname + "$"); // Creates an array of templates which are exposed via // `res.locals.templates`. @@ -77,7 +77,7 @@ app.get("/exclaim", function (req, res) { // This overrides _only_ the default `yell()` helper. helpers: { - yell: function (msg) { + yell (msg) { return (msg + "!!!"); }, }, diff --git a/examples/basic/package.json b/examples/basic/package.json index 05140418..9229e5c8 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -11,6 +11,6 @@ "author": "Eric Ferraiuolo ", "license": "BSD", "dependencies": { - "express": "^4.7.2" + "express": "^4.17.1" } } diff --git a/examples/basic/server.js b/examples/basic/server.js index 2100a131..2d41228e 100644 --- a/examples/basic/server.js +++ b/examples/basic/server.js @@ -1,9 +1,9 @@ "use strict"; -var express = require("express"); -var exphbs = require("../../"); // "express-handlebars" +const express = require("express"); +const exphbs = require("../../"); // "express-handlebars" -var app = express(); +const app = express(); app.engine("handlebars", exphbs()); app.set("view engine", "handlebars"); diff --git a/index.js b/index.js index 8f87865b..7dfd97bd 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ "use strict"; -var ExpressHandlebars = require("./lib/express-handlebars"); +const ExpressHandlebars = require("./lib/express-handlebars"); exports = module.exports = exphbs; exports.create = create; diff --git a/lib/express-handlebars.js b/lib/express-handlebars.js index 9bd7e8ad..5206c18e 100644 --- a/lib/express-handlebars.js +++ b/lib/express-handlebars.js @@ -4,355 +4,324 @@ * See the accompanying LICENSE file for terms. */ -"use strict"; +const util = require("util"); +const glob = util.promisify(require("glob")); +const Handlebars = require("handlebars"); +const fs = require("graceful-fs"); +const readFile = util.promisify(fs.readFile); +const path = require("path"); -var glob = require("glob"); -var Handlebars = require("handlebars"); -var fs = require("graceful-fs"); -var path = require("path"); +// ----------------------------------------------------------------------------- -module.exports = ExpressHandlebars; +const defaultConfig = { + handlebars: Handlebars, + extname: ".handlebars", + layoutsDir: undefined, // Default layouts directory is relative to `express settings.view` + `layouts/` + partialsDir: undefined, // Default partials directory is relative to `express settings.view` + `partials/` + defaultLayout: "main", + helpers: undefined, + compilerOptions: undefined, + runtimeOptions: undefined, +}; -// ----------------------------------------------------------------------------- +class ExpressHandlebars { + constructor (config = {}) { + // Config properties with defaults. + Object.assign(this, defaultConfig, config); -function ExpressHandlebars (config) { - config || (config = {}); - // Config properties with defaults. - Object.assign(this, { - handlebars: Handlebars, - extname: ".handlebars", - layoutsDir: undefined, // Default layouts directory is relative to `express settings.view` + `layouts/` - partialsDir: undefined, // Default partials directory is relative to `express settings.view` + `partials/` - defaultLayout: "main", - helpers: undefined, - compilerOptions: undefined, - runtimeOptions: undefined, - }, config, { config }); - - // Express view engine integration point. - this.engine = this.renderView.bind(this); - - // Normalize `extname`. - if (this.extname.charAt(0) !== ".") { - this.extname = "." + this.extname; - } + // save given config to override other settings. + this.config = config; - // Internal caches of compiled and precompiled templates. - this.compiled = Object.create(null); - this.precompiled = Object.create(null); + // Express view engine integration point. + this.engine = this.renderView.bind(this); - // Private internal file system cache. - this._fsCache = Object.create(null); -} + // Normalize `extname`. + if (this.extname.charAt(0) !== ".") { + this.extname = "." + this.extname; + } + + // Internal caches of compiled and precompiled templates. + this.compiled = {}; + this.precompiled = {}; -ExpressHandlebars.prototype.getPartials = function (options) { - if (typeof this.partialsDir === "undefined") { - return Promise.resolve({}); + // Private internal file system cache. + this._fsCache = {}; } - var partialsDirs = Array.isArray(this.partialsDir) - ? this.partialsDir : [this.partialsDir]; - - partialsDirs = partialsDirs.map(function (dir) { - var dirPath; - var dirTemplates; - var dirNamespace; - - // Support `partialsDir` collection with object entries that contain a - // templates promise and a namespace. - if (typeof dir === "string") { - dirPath = dir; - } else if (typeof dir === "object") { - dirTemplates = dir.templates; - dirNamespace = dir.namespace; - dirPath = dir.dir; - } - // We must have some path to templates, or templates themselves. - if (!(dirPath || dirTemplates)) { - throw new Error("A partials dir must be a string or config object"); + async getPartials (options) { + if (typeof this.partialsDir === "undefined") { + return {}; } + const partialsDirs = Array.isArray(this.partialsDir) ? this.partialsDir : [this.partialsDir]; + + const dirs = await Promise.all(partialsDirs.map(async dir => { + let dirPath; + let dirTemplates; + let dirNamespace; + + // Support `partialsDir` collection with object entries that contain a + // templates promise and a namespace. + if (typeof dir === "string") { + dirPath = dir; + } else if (typeof dir === "object") { + dirTemplates = dir.templates; + dirNamespace = dir.namespace; + dirPath = dir.dir; + } - // Make sure we're have a promise for the templates. - var templatesPromise = dirTemplates ? Promise.resolve(dirTemplates) - : this.getTemplates(dirPath, options); + // We must have some path to templates, or templates themselves. + if (!dirPath && !dirTemplates) { + throw new Error("A partials dir must be a string or config object"); + } + + const templates = dirTemplates || await this.getTemplates(dirPath, options); - return templatesPromise.then(function (templates) { return { - templates: templates, + templates, namespace: dirNamespace, }; - }); - }, this); + })); - return Promise.all(partialsDirs).then(function (dirs) { - var getTemplateName = this._getTemplateName.bind(this); + const partials = {}; - return dirs.reduce(function (partials, dir) { - var templates = dir.templates; - var namespace = dir.namespace; - var filePaths = Object.keys(templates); + for (const dir of dirs) { + const { templates, namespace } = dir; + const filePaths = Object.keys(templates); - filePaths.forEach(function (filePath) { - var partialName = getTemplateName(filePath, namespace); + for (const filePath of filePaths) { + const partialName = this._getTemplateName(filePath, namespace); partials[partialName] = templates[filePath]; - }); - - return partials; - }, {}); - }.bind(this)); -}; - -ExpressHandlebars.prototype.getTemplate = function (filePath, options) { - filePath = path.resolve(filePath); - options || (options = {}); - - var precompiled = options.precompiled; - var cache = precompiled ? this.precompiled : this.compiled; - var template = options.cache && cache[filePath]; + } + } - if (template) { - return template; + return partials; } - // Optimistically cache template promise to reduce file system I/O, but - // remove from cache if there was a problem. - template = cache[filePath] = this._getFile(filePath, { cache: options.cache }) - .then(function (file) { - if (precompiled) { - return this._precompileTemplate(file, this.compilerOptions); - } + async getTemplate (filePath, options = {}) { + filePath = path.resolve(filePath); - return this._compileTemplate(file, this.compilerOptions); - }.bind(this)); + const cache = options.precompiled ? this.precompiled : this.compiled; + let template = options.cache && cache[filePath]; - return template.catch(function (err) { - delete cache[filePath]; - throw err; - }); -}; + if (template) { + return template; + } -ExpressHandlebars.prototype.getTemplates = function (dirPath, options) { - options || (options = {}); - var cache = options.cache; + // Optimistically cache template promise to reduce file system I/O, but + // remove from cache if there was a problem. + try { + template = cache[filePath] = this._getFile(filePath, { cache: options.cache }) + .then(file => { + const compileTemplate = (options.precompiled ? this._precompileTemplate : this._compileTemplate).bind(this); + return compileTemplate(file, this.compilerOptions); + }); + + return template; + } catch (err) { + delete cache[filePath]; + throw err; + } + } - return this._getDir(dirPath, { cache: cache }).then(function (filePaths) { - var templates = filePaths.map(function (filePath) { - return this.getTemplate(path.join(dirPath, filePath), options); - }, this); + async getTemplates (dirPath, options = {}) { + const cache = options.cache; - return Promise.all(templates).then(function (templates) { - return filePaths.reduce(function (hash, filePath, i) { - hash[filePath] = templates[i]; - return hash; - }, {}); - }); - }.bind(this)); -}; + const filePaths = await this._getDir(dirPath, { cache }); + const templates = await Promise.all(filePaths.map(filePath => { + return this.getTemplate(path.join(dirPath, filePath), options); + })); -ExpressHandlebars.prototype.render = function (filePath, context, options) { - options || (options = {}); + const hash = {}; + for (let i = 0; i < filePaths.length; i++) { + hash[filePaths[i]] = templates[i]; + } + return hash; + } - return Promise.all([ - this.getTemplate(filePath, { cache: options.cache }), - options.partials || this.getPartials({ cache: options.cache }), - ]).then(function (templates) { - var template = templates[0]; - var partials = templates[1]; - var helpers = Object.assign({}, this.helpers, options.helpers); - var runtimeOptions = Object.assign({}, this.runtimeOptions, options.runtimeOptions); + async render (filePath, context, options = {}) { + const [template, partials] = await Promise.all([ + this.getTemplate(filePath, { cache: options.cache }), + options.partials || this.getPartials({ cache: options.cache }), + ]); + const helpers = { ...this.helpers, ...options.helpers }; + const runtimeOptions = { ...this.runtimeOptions, ...options.runtimeOptions }; // Add ExpressHandlebars metadata to the data channel so that it's // accessible within the templates and helpers, namespaced under: // `@exphbs.*` - var data = Object.assign({}, options.data, { - exphbs: Object.assign({}, options, { - filePath: filePath, - helpers: helpers, - partials: partials, - runtimeOptions: runtimeOptions, - }), + const data = { + ...options.data, + exphbs: { + ...options, + filePath, + helpers, + partials, + runtimeOptions, + }, + }; + + const html = this._renderTemplate(template, context, { + ...runtimeOptions, + data, + helpers, + partials, }); - var templateOptions = Object.assign({}, runtimeOptions, { - data: data, - helpers: helpers, - partials: partials, - }); + return html; + } - return this._renderTemplate(template, context, templateOptions); - }.bind(this)); -}; + async renderView (viewPath, options = {}, callback = null) { + const context = options; -ExpressHandlebars.prototype.renderView = function (viewPath, options, callback) { - options || (options = {}); + let promise; + if (!callback) { + promise = new Promise((resolve, reject) => { + callback = (err, value) => { err !== null ? reject(err) : resolve(value); }; + }); + } - var context = options; + // Express provides `settings.views` which is the path to the views dir that + // the developer set on the Express app. When this value exists, it's used + // to compute the view's name. Layouts and Partials directories are relative + // to `settings.view` path + let view; + const viewsPath = options.settings && options.settings.views; + if (viewsPath) { + view = this._getTemplateName(path.relative(viewsPath, viewPath)); + this.partialsDir = this.config.partialsDir || path.join(viewsPath, "partials/"); + this.layoutsDir = this.config.layoutsDir || path.join(viewsPath, "layouts/"); + } - var promise; - if (!callback) { - promise = new Promise((resolve, reject) => { - callback = (err, value) => { err !== null ? reject(err) : resolve(value); }; - }); - } + // Merge render-level and instance-level helpers together. + const helpers = { ...this.helpers, ...options.helpers }; - // Express provides `settings.views` which is the path to the views dir that - // the developer set on the Express app. When this value exists, it's used - // to compute the view's name. Layouts and Partials directories are relative - // to `settings.view` path - var view; - var viewsPath = options.settings && options.settings.views; - if (viewsPath) { - view = this._getTemplateName(path.relative(viewsPath, viewPath)); - this.partialsDir = this.config.partialsDir || path.join(viewsPath, "partials/"); - this.layoutsDir = this.config.layoutsDir || path.join(viewsPath, "layouts/"); - } + // Merge render-level and instance-level partials together. + const partials = { + ...await this.getPartials({ cache: options.cache }), + ...await (options.partials || {}), + }; + + // Pluck-out ExpressHandlebars-specific options and Handlebars-specific + // rendering options. + options = { + cache: options.cache, + view, + layout: "layout" in options ? options.layout : this.defaultLayout, + + data: options.data, + helpers, + partials, + runtimeOptions: options.runtimeOptions, + }; - // Merge render-level and instance-level helpers together. - var helpers = Object.assign({}, this.helpers, options.helpers); - - // Merge render-level and instance-level partials together. - var partials = Promise.all([ - this.getPartials({ cache: options.cache }), - Promise.resolve(options.partials), - ]).then(function (partials) { - return Object.assign.apply(null, [{}].concat(partials)); - }); - - // Pluck-out ExpressHandlebars-specific options and Handlebars-specific - // rendering options. - options = { - cache: options.cache, - view: view, - layout: "layout" in options ? options.layout : this.defaultLayout, - - data: options.data, - helpers: helpers, - partials: partials, - runtimeOptions: options.runtimeOptions, - }; - - this.render(viewPath, context, options) - .then(function (body) { - var layoutPath = this._resolveLayoutPath(options.layout); + try { + let html = await this.render(viewPath, context, options); + const layoutPath = this._resolveLayoutPath(options.layout); if (layoutPath) { - return this.render( + html = await this.render( layoutPath, - Object.assign({}, context, { body: body }), - Object.assign({}, options, { layout: undefined }), + { ...context, body: html }, + { ...options, layout: undefined }, ); } + callback(null, html); + } catch (err) { + callback(err); + } + + return promise; + } - return body; - }.bind(this)) - .then((value) => { callback(null, value); }) - .catch((err) => { callback(err); }); + // -- Protected Hooks ---------------------------------------------------------- - return promise; -}; + _compileTemplate (template, options) { + return this.handlebars.compile(template.trim(), options); + } -// -- Protected Hooks ---------------------------------------------------------- + _precompileTemplate (template, options) { + return this.handlebars.precompile(template, options); + } -ExpressHandlebars.prototype._compileTemplate = function (template, options) { - return this.handlebars.compile(template.trim(), options); -}; + _renderTemplate (template, context, options) { + return template(context, options).trim(); + } -ExpressHandlebars.prototype._precompileTemplate = function (template, options) { - return this.handlebars.precompile(template, options); -}; + // -- Private ------------------------------------------------------------------ -ExpressHandlebars.prototype._renderTemplate = function (template, context, options) { - return template(context, options).trim(); -}; + async _getDir (dirPath, options = {}) { + dirPath = path.resolve(dirPath); + + const cache = this._fsCache; + let dir = options.cache && cache[dirPath]; -// -- Private ------------------------------------------------------------------ + if (dir) { + dir = await dir; + return dir.concat(); + } -ExpressHandlebars.prototype._getDir = function (dirPath, options) { - dirPath = path.resolve(dirPath); - options || (options = {}); + const pattern = "**/*" + this.extname; - var cache = this._fsCache; - var dir = options.cache && cache[dirPath]; + // Optimistically cache dir promise to reduce file system I/O, but remove + // from cache if there was a problem. - if (dir) { - return dir.then(function (dir) { + try { + dir = cache[dirPath] = glob(pattern, { + cwd: dirPath, + follow: true, + }); + dir = await dir; return dir.concat(); - }); + } catch (err) { + delete cache[dirPath]; + throw err; + }; } - var pattern = "**/*" + this.extname; - - // Optimistically cache dir promise to reduce file system I/O, but remove - // from cache if there was a problem. - dir = cache[dirPath] = new Promise(function (resolve, reject) { - glob(pattern, { - cwd: dirPath, - follow: true, - }, function (err, dir) { - if (err) { - reject(err); - } else { - resolve(dir); - } - }); - }); - - return dir.then(function (dir) { - return dir.concat(); - }).catch(function (err) { - delete cache[dirPath]; - throw err; - }); -}; + async _getFile (filePath, options = {}) { + filePath = path.resolve(filePath); -ExpressHandlebars.prototype._getFile = function (filePath, options) { - filePath = path.resolve(filePath); - options || (options = {}); + const cache = this._fsCache; + let file = options.cache && cache[filePath]; - var cache = this._fsCache; - var file = options.cache && cache[filePath]; + if (file) { + return file; + } - if (file) { - return file; + // Optimistically cache file promise to reduce file system I/O, but remove + // from cache if there was a problem. + try { + file = cache[filePath] = readFile(filePath, "utf8"); + file = await file; + return file; + } catch (err) { + delete cache[filePath]; + throw err; + }; } - // Optimistically cache file promise to reduce file system I/O, but remove - // from cache if there was a problem. - file = cache[filePath] = new Promise(function (resolve, reject) { - fs.readFile(filePath, "utf8", function (err, file) { - if (err) { - reject(err); - } else { - resolve(file); - } - }); - }); - - return file.catch(function (err) { - delete cache[filePath]; - throw err; - }); -}; + _getTemplateName (filePath, namespace) { + const extRegex = new RegExp(this.extname + "$"); + let name = filePath.replace(extRegex, ""); -ExpressHandlebars.prototype._getTemplateName = function (filePath, namespace) { - var extRegex = new RegExp(this.extname + "$"); - var name = filePath.replace(extRegex, ""); + if (namespace) { + name = namespace + "/" + name; + } - if (namespace) { - name = namespace + "/" + name; + return name; } - return name; -}; + _resolveLayoutPath (layoutPath) { + if (!layoutPath) { + return null; + } -ExpressHandlebars.prototype._resolveLayoutPath = function (layoutPath) { - if (!layoutPath) { - return null; - } + if (!path.extname(layoutPath)) { + layoutPath += this.extname; + } - if (!path.extname(layoutPath)) { - layoutPath += this.extname; + return path.resolve(this.layoutsDir || "", layoutPath); } +} - return path.resolve(this.layoutsDir || "", layoutPath); -}; +module.exports = ExpressHandlebars; From baccfcb5ea605aa437c195496249c6aa5113134e Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Mon, 6 Jul 2020 12:14:09 -0500 Subject: [PATCH 2/5] test: use create function --- spec/express-handlebars.test.js | 94 +++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/spec/express-handlebars.test.js b/spec/express-handlebars.test.js index 1c2ab8d6..cd578c0d 100644 --- a/spec/express-handlebars.test.js +++ b/spec/express-handlebars.test.js @@ -1,5 +1,5 @@ const path = require("path"); -const ExpressHandlebars = require("../lib/express-handlebars.js"); +const expressHandlebars = require("../"); function fixturePath (filePath = "") { return path.resolve(__dirname, "./fixtures", filePath); @@ -7,15 +7,15 @@ function fixturePath (filePath = "") { describe("express-handlebars", () => { test("should nomalize extname", () => { - const exphbs1 = new ExpressHandlebars({ extname: "ext" }); - const exphbs2 = new ExpressHandlebars({ extname: ".ext" }); + const exphbs1 = expressHandlebars.create({ extname: "ext" }); + const exphbs2 = expressHandlebars.create({ extname: ".ext" }); expect(exphbs1.extname).toBe(".ext"); expect(exphbs2.extname).toBe(".ext"); }); describe("getPartials", () => { test("should throw if partialsDir is not correct type", async () => { - const exphbs = new ExpressHandlebars({ partialsDir: 1 }); + const exphbs = expressHandlebars.create({ partialsDir: 1 }); let error; try { await exphbs.getPartials(); @@ -27,19 +27,19 @@ describe("express-handlebars", () => { }); test("should return empty object if no partialsDir is defined", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const partials = await exphbs.getPartials(); expect(partials).toEqual({}); }); test("should return empty object partialsDir does not exist", async () => { - const exphbs = new ExpressHandlebars({ partialsDir: "does-not-exist" }); + const exphbs = expressHandlebars.create({ partialsDir: "does-not-exist" }); const partials = await exphbs.getPartials(); expect(partials).toEqual({}); }); test("should return partials on string", async () => { - const exphbs = new ExpressHandlebars({ partialsDir: fixturePath("partials") }); + const exphbs = expressHandlebars.create({ partialsDir: fixturePath("partials") }); const partials = await exphbs.getPartials(); expect(partials).toEqual({ partial: expect.any(Function), @@ -47,7 +47,7 @@ describe("express-handlebars", () => { }); test("should return partials on array", async () => { - const exphbs = new ExpressHandlebars({ partialsDir: [fixturePath("partials")] }); + const exphbs = expressHandlebars.create({ partialsDir: [fixturePath("partials")] }); const partials = await exphbs.getPartials(); expect(partials).toEqual({ partial: expect.any(Function), @@ -55,7 +55,7 @@ describe("express-handlebars", () => { }); test("should return partials on path relative to cwd", async () => { - const exphbs = new ExpressHandlebars({ partialsDir: "spec/fixtures/partials" }); + const exphbs = expressHandlebars.create({ partialsDir: "spec/fixtures/partials" }); const partials = await exphbs.getPartials(); expect(partials).toEqual({ partial: expect.any(Function), @@ -63,7 +63,7 @@ describe("express-handlebars", () => { }); test("should return template function", async () => { - const exphbs = new ExpressHandlebars({ partialsDir: "spec/fixtures/partials" }); + const exphbs = expressHandlebars.create({ partialsDir: "spec/fixtures/partials" }); const partials = await exphbs.getPartials(); const html = partials.partial({ text: "test text" }); expect(html).toBe("partial test text"); @@ -72,7 +72,7 @@ describe("express-handlebars", () => { describe("getTemplate", () => { test("should return cached template", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const filePath = fixturePath("templates/template.handlebars"); const compiledCachedFunction = Symbol("compiledCachedFunction"); exphbs.compiled[filePath] = compiledCachedFunction; @@ -83,7 +83,7 @@ describe("express-handlebars", () => { }); test("should return precompiled cached template", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const filePath = fixturePath("templates/template.handlebars"); const compiledCachedFunction = Symbol("compiledCachedFunction"); exphbs.compiled[filePath] = compiledCachedFunction; @@ -94,7 +94,7 @@ describe("express-handlebars", () => { }); test("should store in precompiled cache", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const filePath = fixturePath("templates/template.handlebars"); expect(exphbs.compiled[filePath]).toBeUndefined(); expect(exphbs.precompiled[filePath]).toBeUndefined(); @@ -104,7 +104,7 @@ describe("express-handlebars", () => { }); test("should store in compiled cache", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const filePath = fixturePath("templates/template.handlebars"); expect(exphbs.compiled[filePath]).toBeUndefined(); expect(exphbs.precompiled[filePath]).toBeUndefined(); @@ -114,7 +114,7 @@ describe("express-handlebars", () => { }); test("should return a template", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const filePath = fixturePath("templates/template.handlebars"); const template = await exphbs.getTemplate(filePath); const html = template({ text: "test text" }); @@ -122,7 +122,7 @@ describe("express-handlebars", () => { }); test("should not store in cache on error", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const filePath = "does-not-exist"; expect(exphbs.compiled[filePath]).toBeUndefined(); let error; @@ -138,7 +138,7 @@ describe("express-handlebars", () => { describe("getTemplates", () => { test("should return cached templates", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const dirPath = fixturePath("templates"); const fsCache = Promise.resolve([]); exphbs._fsCache[dirPath] = fsCache; @@ -147,7 +147,7 @@ describe("express-handlebars", () => { }); test("should return templates", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const dirPath = fixturePath("templates"); const templates = await exphbs.getTemplates(dirPath); const html = templates["template.handlebars"]({ text: "test text" }); @@ -155,7 +155,7 @@ describe("express-handlebars", () => { }); test("should get templates in sub directories", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const dirPath = fixturePath("templates"); const templates = await exphbs.getTemplates(dirPath); expect(Object.keys(templates)).toEqual([ @@ -167,7 +167,7 @@ describe("express-handlebars", () => { describe("render", () => { test("should return cached templates", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const filePath = fixturePath("render-cached.handlebars"); exphbs.compiled[filePath] = () => "cached"; const html = await exphbs.render(filePath, null, { cache: true }); @@ -175,7 +175,7 @@ describe("express-handlebars", () => { }); test("should use helpers", async () => { - const exphbs = new ExpressHandlebars({ + const exphbs = expressHandlebars.create({ helpers: { help: () => "help", }, @@ -186,7 +186,7 @@ describe("express-handlebars", () => { }); test("should override helpers", async () => { - const exphbs = new ExpressHandlebars({ + const exphbs = expressHandlebars.create({ helpers: { help: () => "help", }, @@ -201,14 +201,14 @@ describe("express-handlebars", () => { }); test("should return html", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const filePath = fixturePath("render-text.handlebars"); const html = await exphbs.render(filePath, { text: "test text" }); expect(html).toBe("

test text

"); }); test("should render with partial", async () => { - const exphbs = new ExpressHandlebars({ + const exphbs = expressHandlebars.create({ partialsDir: fixturePath("partials"), }); const filePath = fixturePath("render-partial.handlebars"); @@ -217,7 +217,7 @@ describe("express-handlebars", () => { }); test("should render with runtimeOptions", async () => { - const exphbs = new ExpressHandlebars({ + const exphbs = expressHandlebars.create({ runtimeOptions: { runtimeOptionTest: "test" }, }); const filePath = fixturePath("test"); @@ -228,7 +228,7 @@ describe("express-handlebars", () => { }); test("should override runtimeOptions", async () => { - const exphbs = new ExpressHandlebars({ + const exphbs = expressHandlebars.create({ runtimeOptions: { runtimeOptionTest: "test" }, }); const filePath = fixturePath("test"); @@ -242,9 +242,33 @@ describe("express-handlebars", () => { }); }); + describe("engine", () => { + test("should call renderView", async () => { + const arr = Array(100).fill(0).map((_, i) => i); + jest.spyOn(expressHandlebars.ExpressHandlebars.prototype, "renderView").mockImplementation(() => {}); + const exphbs = expressHandlebars.create(); + exphbs.engine(...arr); + expect(expressHandlebars.ExpressHandlebars.prototype.renderView).toHaveBeenCalledWith(...arr); + }); + + test("should call engine", async () => { + const arr = Array(100).fill(0).map((_, i) => i); + jest.spyOn(expressHandlebars.ExpressHandlebars.prototype, "renderView").mockImplementation(() => {}); + expressHandlebars()(...arr); + expect(expressHandlebars.ExpressHandlebars.prototype.renderView).toHaveBeenCalledWith(...arr); + }); + + test("should render html", async () => { + const renderView = expressHandlebars({ defaultLayout: null }); + const viewPath = fixturePath("render-text.handlebars"); + const html = await renderView(viewPath, { text: "test text" }); + expect(html).toBe("

test text

"); + }); + }); + describe("renderView", () => { test("should use settings.views", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const viewPath = fixturePath("render-partial.handlebars"); const viewsPath = fixturePath(); const html = await exphbs.renderView(viewPath, { @@ -255,7 +279,7 @@ describe("express-handlebars", () => { }); test("should use settings.views when it changes", async () => { - const exphbs = new ExpressHandlebars(); + const exphbs = expressHandlebars.create(); const viewPath = fixturePath("render-partial.handlebars"); const viewsPath = fixturePath(); const html = await exphbs.renderView(viewPath, { @@ -272,7 +296,7 @@ describe("express-handlebars", () => { }); test("should not overwrite config with settings.views", async () => { - const exphbs = new ExpressHandlebars({ + const exphbs = expressHandlebars.create({ layoutsDir: fixturePath("layouts"), partialsDir: fixturePath("partials"), }); @@ -286,7 +310,7 @@ describe("express-handlebars", () => { }); test("should merge helpers", async () => { - const exphbs = new ExpressHandlebars({ + const exphbs = expressHandlebars.create({ defaultLayout: null, helpers: { help: () => "help", @@ -303,7 +327,7 @@ describe("express-handlebars", () => { }); test("should use layout option", async () => { - const exphbs = new ExpressHandlebars({ defaultLayout: null }); + const exphbs = expressHandlebars.create({ defaultLayout: null }); const layoutPath = fixturePath("layouts/main.handlebars"); const viewPath = fixturePath("render-text.handlebars"); const html = await exphbs.renderView(viewPath, { @@ -314,14 +338,14 @@ describe("express-handlebars", () => { }); test("should render html", async () => { - const exphbs = new ExpressHandlebars({ defaultLayout: null }); + const exphbs = expressHandlebars.create({ defaultLayout: null }); const viewPath = fixturePath("render-text.handlebars"); const html = await exphbs.renderView(viewPath, { text: "test text" }); expect(html).toBe("

test text

"); }); test("should call callback with html", (done) => { - const exphbs = new ExpressHandlebars({ defaultLayout: null }); + const exphbs = expressHandlebars.create({ defaultLayout: null }); const viewPath = fixturePath("render-text.handlebars"); exphbs.renderView(viewPath, { text: "test text" }, (err, html) => { expect(err).toBe(null); @@ -331,7 +355,7 @@ describe("express-handlebars", () => { }); test("should call callback with error", (done) => { - const exphbs = new ExpressHandlebars({ defaultLayout: null }); + const exphbs = expressHandlebars.create({ defaultLayout: null }); const viewPath = "does-not-exist"; exphbs.renderView(viewPath, {}, (err, html) => { expect(err.message).toEqual(expect.stringContaining("no such file or directory")); @@ -341,7 +365,7 @@ describe("express-handlebars", () => { }); test("should use runtimeOptions", async () => { - const exphbs = new ExpressHandlebars({ defaultLayout: null }); + const exphbs = expressHandlebars.create({ defaultLayout: null }); const filePath = fixturePath("test"); const spy = jest.fn(() => { return "test"; }); exphbs.compiled[filePath] = spy; From ea30d531b2f458c37f65b50bddc504180e774f8f Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Mon, 6 Jul 2020 12:15:56 -0500 Subject: [PATCH 3/5] fix: update node support BREAKING CHANGE: Drop support for node versions below v10 --- .github/workflows/main.yml | 39 +++++++++++++++++++++++++++++++------- package.json | 2 +- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 25432557..fef053ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,19 +9,40 @@ env: CI: true jobs: - Test: + TestOS: + name: Test if: "!contains(github.event.head_commit.message, '[skip ci]')" strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [macos-latest, windows-latest] + node_version: ['lts/*'] runs-on: ${{ matrix.os }} steps: - name: Checkout Code uses: actions/checkout@v2 - name: Install Node - uses: actions/setup-node@v1 + uses: dcodeIO/setup-node-nvm@master with: - node-version: '12.x' + node-version: ${{ matrix.node_version }} + - name: Install dependencies + run: npm ci + - name: Run tests 👩🏾‍💻 + run: npm run test + TestNode: + name: Test + if: "!contains(github.event.head_commit.message, '[skip ci]')" + strategy: + matrix: + os: [ubuntu-latest] + node_version: [10, 'lts/*', 'node'] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Install Node + uses: dcodeIO/setup-node-nvm@master + with: + node-version: ${{ matrix.node_version }} - name: Install dependencies run: npm ci - name: Run tests 👩🏾‍💻 @@ -33,9 +54,9 @@ jobs: - name: Checkout Code uses: actions/checkout@v2 - name: Install Node - uses: actions/setup-node@v1 + uses: dcodeIO/setup-node-nvm@master with: - node-version: '13.x' + node-version: 'lts/*' - name: Install dependencies run: npm ci - name: Run tests 👩🏾‍💻 @@ -46,13 +67,17 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v2 + - name: Install Node + uses: dcodeIO/setup-node-nvm@master + with: + node-version: 'lts/*' - name: NPM install run: npm ci - name: Lint ✨ run: npm run lint Release: - needs: [Test, Lint] + needs: [TestOS, TestNode, Coverage, Lint] if: | github.ref == 'refs/heads/master' && github.event.repository.fork == false diff --git a/package.json b/package.json index 3ebe600a..452af895 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "url": "https://github.com/express-handlebars/express-handlebars/issues" }, "engines": { - "node": ">=0.10" + "node": ">=10" }, "dependencies": { "glob": "^7.1.6", From 0f1aeeb1b35ae0c571767117bf273d592c8971f8 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Mon, 6 Jul 2020 16:44:53 -0500 Subject: [PATCH 4/5] test: 100% coverage --- jest.config.js | 4 +- lib/express-handlebars.js | 15 ++- spec/express-handlebars.test.js | 193 ++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 7 deletions(-) diff --git a/jest.config.js b/jest.config.js index 20360513..99a952ca 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,13 +1,12 @@ module.exports = { restoreMocks: true, clearMocks: true, - // collectCoverage: true, collectCoverageFrom: [ "lib/**/*.js", + "index.js", "!**/node_modules/**", ], coverageDirectory: "coverage", - /* coverageThreshold: { global: { branches: 100, @@ -16,6 +15,5 @@ module.exports = { statements: 100, }, }, - */ testRegex: /\.test\.jsx?/.source, }; diff --git a/lib/express-handlebars.js b/lib/express-handlebars.js index 5206c18e..b5bc50cd 100644 --- a/lib/express-handlebars.js +++ b/lib/express-handlebars.js @@ -115,7 +115,7 @@ class ExpressHandlebars { const compileTemplate = (options.precompiled ? this._precompileTemplate : this._compileTemplate).bind(this); return compileTemplate(file, this.compilerOptions); }); - + template = await template; return template; } catch (err) { delete cache[filePath]; @@ -240,7 +240,7 @@ class ExpressHandlebars { } _precompileTemplate (template, options) { - return this.handlebars.precompile(template, options); + return this.handlebars.precompile(template.trim(), options); } _renderTemplate (template, context, options) { @@ -270,6 +270,10 @@ class ExpressHandlebars { cwd: dirPath, follow: true, }); + if (options._throwTestError) { + // FIXME: not sure how to throw error in glob for test coverage + throw new Error("test"); + } dir = await dir; return dir.concat(); } catch (err) { @@ -301,8 +305,11 @@ class ExpressHandlebars { } _getTemplateName (filePath, namespace) { - const extRegex = new RegExp(this.extname + "$"); - let name = filePath.replace(extRegex, ""); + let name = filePath; + + if (name.endsWith(this.extname)) { + name = name.substring(0, name.length - this.extname.length); + } if (namespace) { name = namespace + "/" + name; diff --git a/spec/express-handlebars.test.js b/spec/express-handlebars.test.js index cd578c0d..7eda8d61 100644 --- a/spec/express-handlebars.test.js +++ b/spec/express-handlebars.test.js @@ -54,6 +54,21 @@ describe("express-handlebars", () => { }); }); + test("should return partials on object", async () => { + const fn = jest.fn(); + const exphbs = expressHandlebars.create({ + partialsDir: { + templates: { "partial template": fn }, + namespace: "partial namespace", + dir: fixturePath("partials"), + }, + }); + const partials = await exphbs.getPartials(); + expect(partials).toEqual({ + "partial namespace/partial template": fn, + }); + }); + test("should return partials on path relative to cwd", async () => { const exphbs = expressHandlebars.create({ partialsDir: "spec/fixtures/partials" }); const partials = await exphbs.getPartials(); @@ -364,6 +379,18 @@ describe("express-handlebars", () => { }); }); + test("should reject with error", async () => { + const exphbs = expressHandlebars.create({ defaultLayout: null }); + const viewPath = "does-not-exist"; + let error; + try { + await exphbs.renderView(viewPath); + } catch (e) { + error = e; + } + expect(error.message).toEqual(expect.stringContaining("no such file or directory")); + }); + test("should use runtimeOptions", async () => { const exphbs = expressHandlebars.create({ defaultLayout: null }); const filePath = fixturePath("test"); @@ -376,4 +403,170 @@ describe("express-handlebars", () => { expect(spy).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ runtimeOptionTest: "test" })); }); }); + + describe("hooks", () => { + describe("_compileTemplate", () => { + test("should call template with context and options", () => { + const exphbs = expressHandlebars.create(); + jest.spyOn(exphbs.handlebars, "compile").mockImplementation(() => {}); + const template = "template"; + const options = {}; + exphbs._compileTemplate(template, options); + expect(exphbs.handlebars.compile).toHaveBeenCalledWith(template, options); + }); + + test("should trim template", () => { + const exphbs = expressHandlebars.create(); + jest.spyOn(exphbs.handlebars, "compile").mockImplementation(() => {}); + const template = " template\n"; + const options = {}; + exphbs._compileTemplate(template, options); + expect(exphbs.handlebars.compile).toHaveBeenCalledWith("template", options); + }); + }); + + describe("_precompileTemplate", () => { + test("should call template with context and options", () => { + const exphbs = expressHandlebars.create(); + jest.spyOn(exphbs.handlebars, "precompile").mockImplementation(() => {}); + const template = "template"; + const options = {}; + exphbs._precompileTemplate(template, options); + expect(exphbs.handlebars.precompile).toHaveBeenCalledWith(template, options); + }); + + test("should trim template", () => { + const exphbs = expressHandlebars.create(); + jest.spyOn(exphbs.handlebars, "precompile").mockImplementation(() => {}); + const template = " template\n"; + const options = {}; + exphbs._precompileTemplate(template, options); + expect(exphbs.handlebars.precompile).toHaveBeenCalledWith("template", options); + }); + }); + + describe("_renderTemplate", () => { + test("should call template with context and options", () => { + const exphbs = expressHandlebars.create(); + const template = jest.fn(() => ""); + const context = {}; + const options = {}; + exphbs._renderTemplate(template, context, options); + expect(template).toHaveBeenCalledWith(context, options); + }); + + test("should trim html", () => { + const exphbs = expressHandlebars.create(); + const template = () => " \n"; + const html = exphbs._renderTemplate(template); + expect(html).toBe(""); + }); + }); + + describe("_getDir", () => { + test("should get from cache", async () => { + const exphbs = expressHandlebars.create(); + const filePath = fixturePath("test"); + exphbs._fsCache[filePath] = "test"; + const file = await exphbs._getDir(filePath, { cache: true }); + expect(file).toBe("test"); + }); + + test("should store in cache", async () => { + const exphbs = expressHandlebars.create(); + const filePath = fixturePath("templates"); + expect(exphbs._fsCache[filePath]).toBeUndefined(); + await exphbs._getDir(filePath); + expect(exphbs._fsCache[filePath]).toBeDefined(); + }); + + test("should not store in cache on error", async () => { + const exphbs = expressHandlebars.create(); + const filePath = "test"; + expect(exphbs._fsCache[filePath]).toBeUndefined(); + let error; + try { + await exphbs._getDir(filePath, { _throwTestError: true }); + } catch (e) { + error = e; + } + expect(error).toBeTruthy(); + expect(exphbs._fsCache[filePath]).toBeUndefined(); + }); + }); + + describe("_getFile", () => { + test("should get from cache", async () => { + const exphbs = expressHandlebars.create(); + const filePath = fixturePath("test"); + exphbs._fsCache[filePath] = "test"; + const file = await exphbs._getFile(filePath, { cache: true }); + expect(file).toBe("test"); + }); + + test("should store in cache", async () => { + const exphbs = expressHandlebars.create(); + const filePath = fixturePath("render-text.handlebars"); + expect(exphbs._fsCache[filePath]).toBeUndefined(); + await exphbs._getFile(filePath); + expect(exphbs._fsCache[filePath]).toBeDefined(); + }); + + test("should not store in cache on error", async () => { + const exphbs = expressHandlebars.create(); + const filePath = "does-not-exist"; + expect(exphbs._fsCache[filePath]).toBeUndefined(); + let error; + try { + await exphbs._getFile(filePath); + } catch (e) { + error = e; + } + expect(error.message).toEqual(expect.stringContaining("no such file or directory")); + expect(exphbs._fsCache[filePath]).toBeUndefined(); + }); + }); + + describe("_getTemplateName", () => { + test("should remove extension", () => { + const exphbs = expressHandlebars.create(); + const name = exphbs._getTemplateName("filePath.handlebars"); + expect(name).toBe("filePath"); + }); + + test("should leave if no extension", () => { + const exphbs = expressHandlebars.create(); + const name = exphbs._getTemplateName("filePath"); + expect(name).toBe("filePath"); + }); + + test("should add namespace", () => { + const exphbs = expressHandlebars.create(); + const name = exphbs._getTemplateName("filePath.handlebars", "namespace"); + expect(name).toBe("namespace/filePath"); + }); + }); + + describe("_resolveLayoutPath", () => { + test("should add extension", () => { + const exphbs = expressHandlebars.create(); + const layoutPath = exphbs._resolveLayoutPath("filePath"); + expect(layoutPath).toEqual(expect.stringMatching(/filePath\.handlebars$/)); + }); + + test("should use layoutsDir", () => { + const layoutsDir = fixturePath("layouts"); + const filePath = "filePath.handlebars"; + const exphbs = expressHandlebars.create({ layoutsDir }); + const layoutPath = exphbs._resolveLayoutPath(filePath); + expect(layoutPath).toBe(path.resolve(layoutsDir, filePath)); + }); + + test("should return null", () => { + const exphbs = expressHandlebars.create(); + const layoutPath = exphbs._resolveLayoutPath(null); + expect(layoutPath).toBe(null); + }); + }); + }); }); From 80ec8281b7a568f3e7f42065d43e7b891a273f3c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 6 Jul 2020 22:08:36 +0000 Subject: [PATCH 5/5] chore(release): 5.0.0 [skip ci] # [5.0.0](https://github.com/express-handlebars/express-handlebars/compare/v4.0.6...v5.0.0) (2020-07-06) ### Bug Fixes * update code to es2015+ ([e5a08ee](https://github.com/express-handlebars/express-handlebars/commit/e5a08eed844f177b0f365f882a20c7b229715bdd)) * update node support ([ea30d53](https://github.com/express-handlebars/express-handlebars/commit/ea30d531b2f458c37f65b50bddc504180e774f8f)) ### BREAKING CHANGES * Drop support for node versions below v10 --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dfe73e1..6338110c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [5.0.0](https://github.com/express-handlebars/express-handlebars/compare/v4.0.6...v5.0.0) (2020-07-06) + + +### Bug Fixes + +* update code to es2015+ ([e5a08ee](https://github.com/express-handlebars/express-handlebars/commit/e5a08eed844f177b0f365f882a20c7b229715bdd)) +* update node support ([ea30d53](https://github.com/express-handlebars/express-handlebars/commit/ea30d531b2f458c37f65b50bddc504180e774f8f)) + + +### BREAKING CHANGES + +* Drop support for node versions below v10 + ## [4.0.6](https://github.com/express-handlebars/express-handlebars/compare/v4.0.5...v4.0.6) (2020-07-06) diff --git a/package-lock.json b/package-lock.json index 46e65395..ea5a6f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "express-handlebars", - "version": "4.0.6", + "version": "5.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 452af895..cd37f46d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express-handlebars", "description": "A Handlebars view engine for Express which doesn't suck.", - "version": "4.0.6", + "version": "5.0.0", "homepage": "https://github.com/express-handlebars/express-handlebars", "keywords": [ "express",