├── .editorconfig ├── .gitattributes ├── .gitignore ├── .remarkignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json ├── test ├── fixtures │ └── views │ │ ├── home.pug │ │ └── other │ │ ├── nested │ │ └── deep.pug │ │ └── test.pug └── test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | .idea 4 | node_modules 5 | coverage 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | test/snapshots/**/*.md 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '12' 5 | script: 6 | npm run test-coverage 7 | after_success: 8 | npm run coverage 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nick Baugh (http://niftylettuce.com/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cache-pug-templates 2 | 3 | [![build status](https://img.shields.io/travis/ladjs/cache-pug-templates.svg)](https://travis-ci.org/ladjs/cache-pug-templates) 4 | [![code coverage](https://img.shields.io/codecov/c/github/ladjs/cache-pug-templates.svg)](https://codecov.io/gh/ladjs/cache-pug-templates) 5 | [![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 6 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 7 | [![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org) 8 | [![license](https://img.shields.io/github/license/ladjs/cache-pug-templates.svg)](<>) 9 | 10 | > Cache [Pug][] templates for [Lad][]/[Koa][]/[Express][]/[Connect][]. 11 | 12 | 13 | ## Table of Contents 14 | 15 | * [Install](#install) 16 | * [Usage](#usage) 17 | * [Basic](#basic) 18 | * [Koa](#koa) 19 | * [Express](#express) 20 | * [Options](#options) 21 | * [Debugging](#debugging) 22 | * [Contributors](#contributors) 23 | * [License](#license) 24 | 25 | 26 | ## Install 27 | 28 | [npm][]: 29 | 30 | ```sh 31 | npm install cache-pug-templates 32 | ``` 33 | 34 | [yarn][]: 35 | 36 | ```sh 37 | yarn add cache-pug-templates 38 | ``` 39 | 40 | 41 | ## Usage 42 | 43 | ### Basic 44 | 45 | ```js 46 | const path = require('path'); 47 | const CachePugTemplates = require('cache-pug-templates'); 48 | 49 | const views = path.join(__dirname, 'views'); 50 | 51 | const cache = new CachePugTemplates({ views }); 52 | cache.start(); 53 | ``` 54 | 55 | ### Koa 56 | 57 | ```js 58 | const path = require('path'); 59 | const Koa = require('koa'); 60 | const CachePugTemplates = require('cache-pug-templates'); 61 | 62 | const app = new Koa(); 63 | 64 | // optional (e.g. if you want to cache in non-production) 65 | // app.cache = true; 66 | 67 | // note that koa requires us to specify a 68 | // path name for the views directory 69 | const views = path.join(__dirname, 'views'); 70 | 71 | app.listen(3000, () => { 72 | const cache = new CachePugTemplates({ app, views }); 73 | cache.start(); 74 | }); 75 | ``` 76 | 77 | ### Express 78 | 79 | ```js 80 | const path = require('path'); 81 | const express = require('express'); 82 | const CachePugTemplates = require('cache-pug-templates'); 83 | 84 | const app = express(); 85 | 86 | // optional (by default express defaults to `./views`) 87 | // app.set('views', path.join(__dirname, 'views')); 88 | 89 | app.set('view engine', 'pug'); 90 | 91 | app.listen(3000, () => { 92 | const cache = new CachePugTemplates({ app, views }); 93 | cache.start(); 94 | }); 95 | ``` 96 | 97 | 98 | ## Options 99 | 100 | * `app` (Object) - an instance of Koa, Express, or Connect 101 | * `views` (String or Array) - a file directory path (or an Array of file directory paths) (if you pass an Express app instance as the `app` option, this will be automatically populated to your applications `views` configuration option via `app.get('views')`) 102 | * `logger` (Object) - a logger, defaults to `console` (we recommend using [Cabin][] for your logger) 103 | * `callback` (Function) - defaults to `false` (no operation), but if a function is provided then it will be invoked with two arguments, `file` (String) and `template` (Function) 104 | * `cache` (Boolean) - defaults to `true`, whether or not to cache templates automatically if `cache.start()` is called (useful if you are writing tests or have a custom approach using `callback` function) 105 | * `concurrency` (Number) - number of concurrent files that can be cached per `interval` in parallel (defaults to `1`) 106 | * `interval` (Number) - duration of time in (milliseconds) to limit concurrency for (e.g. `1 cached file every 1000ms` is the default), this value's default is `1000` 107 | 108 | 109 | ## Debugging 110 | 111 | If you want to check what the cache state is at anytime: 112 | 113 | ```js 114 | const pug = require('pug'); 115 | 116 | // ... 117 | 118 | // get everything: 119 | console.log('pug.cache', pug.cache); 120 | 121 | // just get the file names: 122 | console.log('pug cached files', Object.keys(pug.cache)); 123 | ``` 124 | 125 | 126 | ## Contributors 127 | 128 | | Name | Website | 129 | | -------------- | -------------------------- | 130 | | **Nick Baugh** | | 131 | 132 | 133 | ## License 134 | 135 | [MIT](LICENSE) © [Nick Baugh](http://niftylettuce.com/) 136 | 137 | 138 | ## 139 | 140 | [npm]: https://www.npmjs.com/ 141 | 142 | [yarn]: https://yarnpkg.com/ 143 | 144 | [pug]: https://pugjs.org 145 | 146 | [lad]: https://lad.js.org 147 | 148 | [koa]: http://koajs.com 149 | 150 | [express]: https://expressjs.com/ 151 | 152 | [connect]: https://github.com/senchalabs/connect 153 | 154 | [cabin]: https://cabinjs.com 155 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const pug = require('pug'); 4 | const debug = require('debug')('cache-pug-templates'); 5 | const _ = require('lodash'); 6 | const rateLimit = require('function-rate-limit'); 7 | 8 | // 9 | for (const [key, value] of Object.entries(pug.runtime)) { 10 | pug[key] = value; 11 | } 12 | 13 | class CachePugTemplates { 14 | constructor(config) { 15 | this.config = { 16 | app: false, 17 | views: false, 18 | logger: console, 19 | callback: false, 20 | cache: true, 21 | concurrency: 1, 22 | interval: 500, 23 | ...config 24 | }; 25 | 26 | this.queuedFiles = []; 27 | 28 | // legacy support for `views` being a String 29 | if (_.isString(this.config.views)) this.config.views = [this.config.views]; 30 | 31 | // 32 | // detect if we're using koa or express/connect 33 | // 34 | if (this.config.app) { 35 | // koa 36 | if (_.isObject(this.config.app.context)) { 37 | debug('detected koa'); 38 | 39 | // ensure that views is defined and is a string 40 | if (_.isArray(this.config.views) && _.isEmpty(this.config.views)) 41 | throw new Error( 42 | '`views` directory argument must be provided for koa' 43 | ); 44 | } else { 45 | // 46 | // express/connect 47 | // 48 | debug('detected express/connect'); 49 | 50 | // only continue if pug is the view engine 51 | if (this.config.app.get('view engine') !== 'pug') { 52 | const errorMessage = `view engine was "${this.config.app.get( 53 | 'view engine' 54 | )}" and needs to be set to "pug"`; 55 | throw new Error(errorMessage); 56 | } 57 | 58 | // views is always defined (defaults to `/views`) 59 | if (!_.isArray(this.config.views)) this.config.views = []; 60 | 61 | this.config.views.push(this.config.app.get('views')); 62 | } 63 | } 64 | 65 | this.config.views = _.uniq(this.config.views); 66 | 67 | debug(this.config); 68 | 69 | this.writeCache = this.writeCache.bind(this); 70 | this.cacheFile = this.cacheFile.bind(this); 71 | this.getFilename = this.getFilename.bind(this); 72 | this.cacheDirectory = this.cacheDirectory.bind(this); 73 | this.start = this.start.bind(this); 74 | 75 | this.rateLimitedCacheFile = rateLimit( 76 | this.config.concurrency, 77 | this.config.interval, 78 | this.cacheFile.bind(this) 79 | ); 80 | } 81 | 82 | writeCache(filename) { 83 | debug(`compiling template located at ${filename}`); 84 | fs.readFile(filename, 'utf8', (err, string) => { 85 | if (err) return this.config.logger.error(err); 86 | const options = { cache: true, filename }; 87 | if (pug.cache[filename]) 88 | return this.config.logger.warn( 89 | `${filename} was already cached in pug.cache` 90 | ); 91 | // 92 | setImmediate(() => { 93 | const template = pug.compile(string, options); 94 | if (this.config.cache) { 95 | debug(`caching ${filename}`); 96 | try { 97 | pug.cache[filename] = template; 98 | } catch (err) { 99 | this.config.logger.error(err); 100 | } 101 | } else { 102 | debug( 103 | `not caching ${filename} since \`cache\` option was set to \`false\`` 104 | ); 105 | } 106 | 107 | if (_.isFunction(this.config.callback)) 108 | this.config.callback(filename, template); 109 | }); 110 | }); 111 | } 112 | 113 | cacheFile(filename) { 114 | fs.stat(filename, (err, stat) => { 115 | if (err) return this.config.logger.error(err); 116 | 117 | if (stat.isDirectory()) { 118 | setImmediate(() => { 119 | this.cacheDirectory(filename); 120 | }); 121 | return; 122 | } 123 | 124 | debug(`checking ${filename}`); 125 | 126 | if (path.extname(filename) !== '.pug') { 127 | debug(`${filename} did not have ".pug" extension`); 128 | return; 129 | } 130 | 131 | if (pug.cache[filename]) { 132 | debug(`${filename} was already cached in pug.cache`); 133 | return; 134 | } 135 | 136 | setImmediate(() => { 137 | this.writeCache(filename); 138 | }); 139 | }); 140 | } 141 | 142 | getFilename(dir) { 143 | return (file) => path.join(dir, file); 144 | } 145 | 146 | cacheDirectory(dir) { 147 | fs.readdir(dir, (err, files) => { 148 | if (err) return this.config.logger.error(err); 149 | for (const file of files) { 150 | setImmediate(() => { 151 | const filename = this.getFilename(dir)(file); 152 | this.rateLimitedCacheFile(filename); 153 | }); 154 | } 155 | }); 156 | } 157 | 158 | start() { 159 | debug('pre-caching pug views'); 160 | 161 | if (this.config.app) { 162 | // koa 163 | if (_.isObject(this.config.app.context)) { 164 | // only continue if `env` is production 165 | // or if `app.cacheViews = true` is set 166 | if (this.config.app.env !== 'production' && !this.config.app.cache) { 167 | debug('koa env was not production and app.cache not set'); 168 | return; 169 | } 170 | } else if (!this.config.app.enabled('view cache')) { 171 | // only continue if caching is enabled 172 | debug('view cache was not enabled'); 173 | return; 174 | } 175 | } 176 | 177 | // cache directories 178 | for (let i = 0; i < this.config.views.length; i++) { 179 | setImmediate(() => { 180 | this.cacheDirectory(this.config.views[i]); 181 | }); 182 | } 183 | } 184 | } 185 | 186 | module.exports = CachePugTemplates; 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-pug-templates", 3 | "description": "Cache Pug templates for Lad/Koa/Express/Connect", 4 | "version": "2.0.3", 5 | "author": "Nick Baugh (http://niftylettuce.com/)", 6 | "ava": { 7 | "verbose": true, 8 | "timeout": "20s", 9 | "serial": true 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/ladjs/cache-pug-templates/issues", 13 | "email": "niftylettuce@gmail.com" 14 | }, 15 | "contributors": [ 16 | "Nick Baugh (http://niftylettuce.com/)" 17 | ], 18 | "dependencies": { 19 | "debug": "^4.1.1", 20 | "function-rate-limit": "^1.1.0", 21 | "lodash": "^4.17.19" 22 | }, 23 | "devDependencies": { 24 | "ava": "^3.10.1", 25 | "codecov": "^3.7.2", 26 | "cross-env": "^7.0.2", 27 | "ejs": "^3.1.3", 28 | "eslint": "^7.5.0", 29 | "eslint-config-xo-lass": "^1.0.3", 30 | "express": "^4.17.1", 31 | "fixpack": "^3.0.6", 32 | "husky": "^4.2.5", 33 | "koa": "^2.13.0", 34 | "lint-staged": "^10.2.11", 35 | "nyc": "^15.1.0", 36 | "pug": "^2.0.3", 37 | "remark-cli": "^8.0.1", 38 | "remark-preset-github": "2.0.2", 39 | "xo": "^0.32.1" 40 | }, 41 | "engines": { 42 | "node": ">=8.3" 43 | }, 44 | "homepage": "https://github.com/ladjs/cache-pug-templates", 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "lint-staged && npm test" 48 | } 49 | }, 50 | "keywords": [ 51 | "cache", 52 | "cache-pug-templates", 53 | "caching", 54 | "deploy", 55 | "deployment", 56 | "engine", 57 | "express", 58 | "helper", 59 | "jade", 60 | "koa", 61 | "lad", 62 | "lass", 63 | "middleware", 64 | "pre", 65 | "pre-cache", 66 | "pug", 67 | "template", 68 | "templating" 69 | ], 70 | "license": "MIT", 71 | "lint-staged": { 72 | "*.js": [ 73 | "xo --fix", 74 | "git add" 75 | ], 76 | "*.md": [ 77 | "remark . -qfo", 78 | "git add" 79 | ], 80 | "package.json": [ 81 | "fixpack", 82 | "git add" 83 | ] 84 | }, 85 | "main": "index.js", 86 | "peerDependencies": { 87 | "pug": "*" 88 | }, 89 | "prettier": { 90 | "singleQuote": true, 91 | "bracketSpacing": true, 92 | "trailingComma": "none" 93 | }, 94 | "remarkConfig": { 95 | "plugins": [ 96 | "preset-github" 97 | ] 98 | }, 99 | "repository": { 100 | "type": "git", 101 | "url": "https://github.com/ladjs/cache-pug-templates" 102 | }, 103 | "scripts": { 104 | "ava": "cross-env NODE_ENV=production ava", 105 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 106 | "lint": "xo && remark . -qfo", 107 | "nyc": "cross-env NODE_ENV=production nyc ava", 108 | "test": "npm run lint && npm run ava", 109 | "test-coverage": "npm run lint && npm run nyc" 110 | }, 111 | "xo": { 112 | "prettier": true, 113 | "space": true, 114 | "extends": [ 115 | "xo-lass" 116 | ] 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/fixtures/views/home.pug: -------------------------------------------------------------------------------- 1 | h1 Welcome 2 | p Here's some text 3 | -------------------------------------------------------------------------------- /test/fixtures/views/other/nested/deep.pug: -------------------------------------------------------------------------------- 1 | h2 Deep 2 | -------------------------------------------------------------------------------- /test/fixtures/views/other/test.pug: -------------------------------------------------------------------------------- 1 | h3 Make something awesome 2 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const test = require('ava'); 3 | const pug = require('pug'); 4 | const Koa = require('koa'); 5 | const express = require('express'); 6 | 7 | const CachePugTemplates = require('..'); 8 | 9 | test.beforeEach(() => { 10 | pug.cache = {}; 11 | }); 12 | 13 | test.cb('rendering works', (t) => { 14 | const views = path.join(__dirname, 'fixtures', 'views'); 15 | const cache = new CachePugTemplates({ 16 | views 17 | }); 18 | cache.start(); 19 | setTimeout(() => { 20 | t.is(Object.keys(pug.cache).length, 3); 21 | const home = path.join(views, 'home.pug'); 22 | t.log(`length 1 ${pug.cache[home]().length}`); 23 | t.log(`str 1 ${pug.cache[home]()}`); 24 | t.log(`length 2 ${pug.compileFile(home)().length}`); 25 | t.log(`str 2 ${pug.compileFile(home)()}`); 26 | t.is(pug.cache[home](), pug.compileFile(home)()); 27 | t.end(); 28 | }, 3000); 29 | }); 30 | 31 | test.cb('koa', (t) => { 32 | const app = new Koa(); 33 | 34 | // optional (e.g. if you want to cache in non-production) 35 | // app.cache = true; 36 | 37 | const views = path.join(__dirname, 'fixtures', 'views'); 38 | 39 | app.listen(() => { 40 | const cache = new CachePugTemplates({ app, views }); 41 | cache.start(); 42 | setTimeout(() => { 43 | t.is(Object.keys(pug.cache).length, 3); 44 | t.end(); 45 | }, 3000); 46 | }); 47 | }); 48 | 49 | test.cb('express', (t) => { 50 | const app = express(); 51 | app.set('views', path.join(__dirname, 'fixtures', 'views')); 52 | app.set('view engine', 'pug'); 53 | app.listen(() => { 54 | const cache = new CachePugTemplates({ app }); 55 | cache.start(); 56 | setTimeout(() => { 57 | t.is(Object.keys(pug.cache).length, 3); 58 | t.end(); 59 | }, 3000); 60 | }); 61 | }); 62 | 63 | test.cb('throws on unsupported view engine', (t) => { 64 | const app = express(); 65 | app.set('views', path.join(__dirname, 'fixtures', 'views')); 66 | app.set('view engine', 'ejs'); 67 | app.set('view cache', true); 68 | app.listen(() => { 69 | const error = t.throws(() => { 70 | const cache = new CachePugTemplates({ app }); 71 | cache.start(); 72 | }); 73 | 74 | t.is(error.message, `view engine was "ejs" and needs to be set to "pug"`); 75 | t.end(); 76 | }); 77 | }); 78 | --------------------------------------------------------------------------------