"use strict"; /* * Copyright (c) 2015, Yahoo Inc. All rights reserved. * Copyrights licensed under the New BSD License. * See the accompanying LICENSE file for terms. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const Handlebars = require("handlebars"); const fs = require("graceful-fs"); const path = require("node:path"); const node_util_1 = require("node:util"); const glob_1 = require("glob"); const readFile = (0, node_util_1.promisify)(fs.readFile); // ----------------------------------------------------------------------------- const defaultConfig = { handlebars: Handlebars, extname: ".handlebars", encoding: "utf8", layoutsDir: undefined, partialsDir: undefined, defaultLayout: "main", helpers: undefined, compilerOptions: undefined, runtimeOptions: undefined, }; class ExpressHandlebars { constructor(config = {}) { // Config properties with defaults. Object.assign(this, defaultConfig, config); // save given config to override other settings. this.config = config; // Express view engine integration point. this.engine = this.renderView.bind(this); // Normalize `extname`. if (this.extname.charAt(0) !== ".") { this.extname = "." + this.extname; } // Internal caches of compiled and precompiled templates. this.compiled = {}; this.precompiled = {}; // Private internal file system cache. this._fsCache = {}; } getPartials(options = {}) { return __awaiter(this, void 0, void 0, function* () { if (typeof this.partialsDir === "undefined") { return {}; } const partialsDirs = Array.isArray(this.partialsDir) ? this.partialsDir : [this.partialsDir]; const dirs = yield Promise.all(partialsDirs.map((dir) => __awaiter(this, void 0, void 0, function* () { let dirPath; let dirTemplates; let dirNamespace; let dirRename; // 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; dirRename = dir.rename; 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"); } const templates = dirTemplates || (yield this.getTemplates(dirPath, options)); return { templates: templates, namespace: dirNamespace, rename: dirRename, }; }))); const partials = {}; for (const dir of dirs) { const { templates, namespace, rename } = dir; const filePaths = Object.keys(templates); const getTemplateNameFn = typeof rename === "function" ? rename : this._getTemplateName.bind(this); for (const filePath of filePaths) { const partialName = getTemplateNameFn(filePath, namespace); partials[partialName] = templates[filePath]; } } return partials; }); } getTemplate(filePath, options = {}) { return __awaiter(this, void 0, void 0, function* () { filePath = path.resolve(filePath); const encoding = options.encoding || this.encoding; const cache = options.precompiled ? this.precompiled : this.compiled; const template = options.cache && cache[filePath]; if (template) { return template; } // Optimistically cache template promise to reduce file system I/O, but // remove from cache if there was a problem. try { cache[filePath] = this._getFile(filePath, { cache: options.cache, encoding }) .then((file) => { const compileTemplate = (options.precompiled ? this._precompileTemplate : this._compileTemplate).bind(this); return compileTemplate(file, this.compilerOptions); }); return yield cache[filePath]; } catch (err) { delete cache[filePath]; throw err; } }); } getTemplates(dirPath, options = {}) { return __awaiter(this, void 0, void 0, function* () { const cache = options.cache; const filePaths = yield this._getDir(dirPath, { cache }); const templates = yield Promise.all(filePaths.map(filePath => { return this.getTemplate(path.join(dirPath, filePath), options); })); const hash = {}; for (let i = 0; i < filePaths.length; i++) { hash[filePaths[i]] = templates[i]; } return hash; }); } render(filePath, context = {}, options = {}) { return __awaiter(this, void 0, void 0, function* () { const encoding = options.encoding || this.encoding; const [template, partials] = yield Promise.all([ this.getTemplate(filePath, { cache: options.cache, encoding }), (options.partials || this.getPartials({ cache: options.cache, encoding })), ]); const helpers = Object.assign(Object.assign({}, this.helpers), options.helpers); const runtimeOptions = Object.assign(Object.assign({}, this.runtimeOptions), options.runtimeOptions); // Add ExpressHandlebars metadata to the data channel so that it's // accessible within the templates and helpers, namespaced under: // `@exphbs.*` const data = Object.assign(Object.assign({}, options.data), { exphbs: Object.assign(Object.assign({}, options), { filePath, helpers, partials, runtimeOptions }) }); const html = this._renderTemplate(template, context, Object.assign(Object.assign({}, runtimeOptions), { data, helpers, partials })); return html; }); } renderView(viewPath, options = {}, callback = null) { return __awaiter(this, void 0, void 0, function* () { if (typeof options === "function") { callback = options; options = {}; } const context = options; let promise = null; if (!callback) { promise = new Promise((resolve, reject) => { callback = (err, value) => { err !== null ? reject(err) : resolve(value); }; }); } // 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 views = options.settings && options.settings.views; const viewsPath = this._resolveViewsPath(views, viewPath); 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/"); } const encoding = options.encoding || this.encoding; // Merge render-level and instance-level helpers together. const helpers = Object.assign(Object.assign({}, this.helpers), options.helpers); // Merge render-level and instance-level partials together. const partials = Object.assign(Object.assign({}, yield this.getPartials({ cache: options.cache, encoding })), (options.partials || {})); // Pluck-out ExpressHandlebars-specific options and Handlebars-specific // rendering options. const renderOptions = { cache: options.cache, encoding, view, layout: "layout" in options ? options.layout : this.defaultLayout, data: options.data, helpers, partials, runtimeOptions: options.runtimeOptions, }; try { let html = yield this.render(viewPath, context, renderOptions); const layoutPath = this._resolveLayoutPath(renderOptions.layout); if (layoutPath) { html = yield this.render(layoutPath, Object.assign(Object.assign({}, context), { body: html }), Object.assign(Object.assign({}, renderOptions), { layout: undefined })); } callback(null, html); } catch (err) { callback(err); } return promise; }); } resetCache(filePathsOrFilter) { let filePaths = []; if (typeof filePathsOrFilter === "undefined") { filePaths = Object.keys(this._fsCache); } else if (typeof filePathsOrFilter === "string") { filePaths = [filePathsOrFilter]; } else if (typeof filePathsOrFilter === "function") { filePaths = Object.keys(this._fsCache).filter(filePathsOrFilter); } else if (Array.isArray(filePathsOrFilter)) { filePaths = filePathsOrFilter; } for (const filePath of filePaths) { delete this._fsCache[filePath]; } } // -- Protected Hooks ---------------------------------------------------------- _compileTemplate(template, options = {}) { return this.handlebars.compile(template.trim(), options); } _precompileTemplate(template, options = {}) { return this.handlebars.precompile(template.trim(), options); } _renderTemplate(template, context = {}, options = {}) { return template(context, options).trim(); } // -- Private ------------------------------------------------------------------ _getDir(dirPath, options = {}) { return __awaiter(this, void 0, void 0, function* () { dirPath = path.resolve(dirPath); const cache = this._fsCache; let dir = options.cache && cache[dirPath]; if (dir) { return [...yield dir]; } const pattern = "**/*" + this.extname; // Optimistically cache dir promise to reduce file system I/O, but remove // from cache if there was a problem. try { dir = cache[dirPath] = (0, glob_1.glob)(pattern, { cwd: dirPath, follow: true, posix: true, }); // @ts-expect-error FIXME: not sure how to throw error in glob for test coverage if (options._throwTestError) { throw new Error("test"); } return [...yield dir]; } catch (err) { delete cache[dirPath]; throw err; } }); } _getFile(filePath, options = {}) { return __awaiter(this, void 0, void 0, function* () { filePath = path.resolve(filePath); const cache = this._fsCache; const encoding = options.encoding || this.encoding; const file = options.cache && cache[filePath]; if (file) { return file; } // Optimistically cache file promise to reduce file system I/O, but remove // from cache if there was a problem. try { cache[filePath] = readFile(filePath, { encoding: encoding || "utf8" }); return yield cache[filePath]; } catch (err) { delete cache[filePath]; throw err; } }); } _getTemplateName(filePath, namespace = null) { let name = filePath; if (name.endsWith(this.extname)) { name = name.substring(0, name.length - this.extname.length); } if (namespace) { name = namespace + "/" + name; } return name; } _resolveViewsPath(views, file) { if (!Array.isArray(views)) { return views; } let lastDir = path.resolve(file); let dir = path.dirname(lastDir); const absoluteViews = views.map(v => path.resolve(v)); // find the closest parent while (dir !== lastDir) { const index = absoluteViews.indexOf(dir); if (index >= 0) { return views[index]; } lastDir = dir; dir = path.dirname(lastDir); } // cannot resolve view return null; } _resolveLayoutPath(layoutPath) { if (!layoutPath) { return null; } if (!path.extname(layoutPath)) { layoutPath += this.extname; } return path.resolve(this.layoutsDir || "", layoutPath); } } exports.default = ExpressHandlebars; //# sourceMappingURL=express-handlebars.js.map