├── .npmignore ├── .prettierrc ├── tests ├── test-broken.ejs ├── test-broken.pug ├── test-broken.nunjucks ├── test-broken.handlebars ├── test.output.html ├── test.pug ├── test.output.raw.pug.html ├── test.ejs ├── test.handlebars ├── test.nunjucks ├── test.output.raw.html ├── test.output.raw.handlebars.html └── index.js ├── examples └── handlebars │ ├── main.handlebars │ ├── package.json │ └── handlebars-example.js ├── LICENSE ├── package.json ├── .gitignore ├── minifier.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | tests 3 | package-lock.json 4 | .prettierrc 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "endOfLine": "auto", 7 | "useTabs": false, 8 | "tabWidth": 2 9 | } 10 | -------------------------------------------------------------------------------- /tests/test-broken.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
10 | -------------------------------------------------------------------------------- /tests/test-broken.pug: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
10 | -------------------------------------------------------------------------------- /tests/test-broken.nunjucks: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
10 | -------------------------------------------------------------------------------- /tests/test-broken.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
10 | -------------------------------------------------------------------------------- /tests/test.output.html: -------------------------------------------------------------------------------- 1 | Express minify HTML testHello world -------------------------------------------------------------------------------- /tests/test.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | // This unnecessary HTML comment should probably be removed! 4 | head.head 5 | meta(charset='UTF-8') 6 | title Express minify HTML test 7 | body#body 8 | | Hello #{hello} 9 | script(type='text/javascript'). 10 | function reallyNiceFunction () { 11 | return "Hello!"; 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /tests/test.output.raw.pug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Express minify HTML test 7 | 8 | Hello world 9 | 14 | 15 | -------------------------------------------------------------------------------- /tests/test.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Express minify HTML test 7 | 8 | 9 | Hello <%= hello %> 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /tests/test.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Express minify HTML test 7 | 8 | 9 | Hello {{hello}} 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /tests/test.nunjucks: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Express minify HTML test 7 | 8 | 9 | Hello {{ hello }} 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /tests/test.output.raw.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Express minify HTML test 7 | 8 | 9 | Hello world 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /tests/test.output.raw.handlebars.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Express minify HTML test 7 | 8 | 9 | Hello world 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /examples/handlebars/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Express minify HTML handlebars example 7 | 8 | 9 | Hello {{hello}}! 10 | 11 | 20 | -------------------------------------------------------------------------------- /examples/handlebars/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-minify-html-handlebars-example", 3 | "version": "0.0.1", 4 | "description": "Handlebars.js example for this middleware", 5 | "main": "handlebars-example.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node handlebars-example.js" 9 | }, 10 | "author": "Matti Jokitulppo ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "express": "^4.13.3", 14 | "express-handlebars": "^2.0.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/handlebars/handlebars-example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var minifyHTML = require('../../minifier.js'); 5 | var exhbs = require('express-handlebars'); 6 | 7 | var app = express(); 8 | 9 | var hbs = exhbs.create({ 10 | defaultLayout: 'main', 11 | layoutsDir: './' 12 | }); 13 | 14 | app.engine('handlebars', hbs.engine); 15 | app.set('view engine', 'handlebars'); 16 | app.set('views', './'); 17 | 18 | app.use(minifyHTML({ 19 | override: true, 20 | htmlMinifier: { 21 | removeComments: true, 22 | collapseWhitespace: true, 23 | collapseBooleanAttributes: true, 24 | removeAttributeQuotes: true, 25 | removeEmptyAttributes: true, 26 | minifyJS: true 27 | } 28 | })); 29 | 30 | app.get('/', function(req, res) { 31 | res.render('main', { hello: 'world' }); 32 | }); 33 | 34 | var server = app.listen(3000, function() { 35 | var host = server.address().address; 36 | var port = server.address().port; 37 | 38 | console.log('Handlebars example app listening at http://%s:%s', host, port); 39 | }); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matti Jokitulppo (http://mattij.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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-minify-html-2", 3 | "version": "2.0.0", 4 | "description": "Express.js middleware wrapper around html-minifier-terser", 5 | "main": "minifier.js", 6 | "scripts": { 7 | "test": "node tests" 8 | }, 9 | "engines": { 10 | "npm": ">=6.4.0", 11 | "node": ">=14.0.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/meszaros-lajos-gyorgy/express-minify-html-2.git" 16 | }, 17 | "keywords": [ 18 | "express", 19 | "html", 20 | "minifier", 21 | "middleware", 22 | "minify" 23 | ], 24 | "author": "Matti Jokitulppo ", 25 | "maintainers": [ 26 | "Lajos Mészáros " 27 | ], 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/meszaros-lajos-gyorgy/express-minify-html-2/issues" 31 | }, 32 | "homepage": "https://github.com/meszaros-lajos-gyorgy/express-minify-html-2#readme", 33 | "dependencies": { 34 | "html-minifier-terser": "^7.2.0" 35 | }, 36 | "devDependencies": { 37 | "ejs": "3.1.9", 38 | "express": "4.18.2", 39 | "express-handlebars": "7.0.7", 40 | "handlebars": "4.7.7", 41 | "nunjucks": "3.2.4", 42 | "pug": "3.0.2", 43 | "supertest": "6.3.3", 44 | "tape": "5.6.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # source: https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # vuepress v2.x temp and cache directory 106 | .temp 107 | .cache 108 | 109 | # Docusaurus cache and generated files 110 | .docusaurus 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # yarn v2 128 | .yarn/cache 129 | .yarn/unplugged 130 | .yarn/build-state.yml 131 | .yarn/install-state.gz 132 | .pnp.* 133 | -------------------------------------------------------------------------------- /minifier.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { minify } = require('html-minifier-terser') 4 | 5 | const toArrayIfNotAlreadyAnArray = (value) => { 6 | return Array.isArray(value) ? value : [value] 7 | } 8 | 9 | const any = (fn, values) => { 10 | for (let value of values) { 11 | if (fn(value)) { 12 | return true 13 | } 14 | } 15 | 16 | return false 17 | } 18 | 19 | const none = (fn, values) => { 20 | return !any(fn, values) 21 | } 22 | 23 | const propOr = (key, fallbackValue, obj) => { 24 | if (typeof obj === 'object' && typeof obj[key] !== 'undefined') { 25 | return obj[key] 26 | } 27 | 28 | return fallbackValue 29 | } 30 | 31 | const parseOptions = (options) => ({ 32 | override: propOr('override', false, options), 33 | exceptionUrls: toArrayIfNotAlreadyAnArray(propOr('exceptionUrls', propOr('exception_url', false, options), options)), 34 | htmlMinifier: Object.assign( 35 | { 36 | removeComments: true, 37 | collapseWhitespace: true, 38 | collapseBooleanAttributes: true, 39 | removeAttributeQuotes: true, 40 | removeEmptyAttributes: true, 41 | }, 42 | propOr('htmlMinifier', {}, options), 43 | ), 44 | }) 45 | 46 | const matchesException = (req, res) => { 47 | return (exception) => { 48 | if (exception instanceof RegExp) { 49 | return exception.test(req.url) 50 | } 51 | 52 | if (typeof exception === 'function') { 53 | return exception(req, res) || false 54 | } 55 | 56 | if (typeof exception === 'string') { 57 | return req.url.match(exception) !== null 58 | } 59 | 60 | return false 61 | } 62 | } 63 | 64 | const sendMinified = (res, next, htmlMinifierOptions, callback) => { 65 | if (typeof callback === 'undefined') { 66 | // No callback specified, just minify and send to client. 67 | return (err, html) => { 68 | if (err) { 69 | return next(err) 70 | } 71 | 72 | minify(html, htmlMinifierOptions) 73 | .then((html) => { 74 | res.send(html) 75 | }) 76 | .catch((err) => { 77 | res.status(500).send(err) 78 | }) 79 | } 80 | } 81 | 82 | // Custom callback specified by user, use that one 83 | return (err, html) => { 84 | if (!html) { 85 | callback(err, html) 86 | return 87 | } 88 | 89 | minify(html, htmlMinifierOptions) 90 | .then((html) => { 91 | callback(err, html) 92 | }) 93 | .catch((err) => { 94 | callback(err) 95 | }) 96 | } 97 | } 98 | 99 | module.exports = (options = {}) => { 100 | const { override, exceptionUrls, htmlMinifier } = parseOptions(options) 101 | 102 | return (req, res, next) => { 103 | res.renderMin = function (view, renderOpts, callback) { 104 | this.render(view, renderOpts, sendMinified(res, next, htmlMinifier, callback)) 105 | } 106 | 107 | if (override && none(matchesException(req, res), exceptionUrls)) { 108 | res.oldRender = res.render 109 | res.render = function (view, renderOpts, callback) { 110 | this.oldRender(view, renderOpts, sendMinified(res, next, htmlMinifier, callback)) 111 | } 112 | } 113 | 114 | return next() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-minify-html-2 2 | 3 | Express middleware wrapper around [html-minifier-terser](https://github.com/terser/html-minifier-terser) 4 | 5 | ## Note that this is a fork of express-minify-html 6 | 7 | This fork is made in order to keep the [express-minify-html](https://github.com/melonmanchan/express-minify-html) 8 | project alive. All credits go to the contributors in the original repo. 9 | 10 | ## Description 11 | 12 | This express middleware simply enchances the regular 'render' method of the response object for minifying HTML. 13 | 14 | ## Usage 15 | 16 | ```sh 17 | npm install --save --production express-minify-html-2 express 18 | ``` 19 | 20 | ```js 21 | var express = require('express') 22 | var minifyHTML = require('express-minify-html-2') 23 | 24 | var app = express() 25 | 26 | app.use( 27 | minifyHTML({ 28 | override: true, 29 | /** 30 | * exceptionUrls can also be spelled as exception_url for backwards compatibility 31 | */ 32 | exceptionUrls: false, 33 | htmlMinifier: { 34 | removeComments: true, 35 | collapseWhitespace: true, 36 | collapseBooleanAttributes: true, 37 | removeAttributeQuotes: true, 38 | removeEmptyAttributes: true, 39 | }, 40 | }), 41 | ) 42 | 43 | app.get('hello', function (req, res, next) { 44 | res.render('helloTemplate', { hello: 'world' }, function (err, html) { 45 | // The output is minified, huzzah! 46 | console.log(html) 47 | res.send(html) 48 | }) 49 | }) 50 | ``` 51 | 52 | Set 'override' to false if you don't want to hijack the ordinary res.render function. This adds an additional 53 | res.renderMin function to the response object to render minimized HTML. 54 | 55 | The 'htmlMinifier' opts are simply passed on to the `html-minifier-terser` plugin. For all the available configuration 56 | options, see [the original repo](https://github.com/terser/html-minifier-terser#options-quick-reference)! 57 | 58 | If no callback is provided, res.render/res.renderMin sends the minified HTML to the client just as the regular 59 | express res.render does. Otherwise, the callback is called with the error object and the minified HTML content, as 60 | demonstrated above. 61 | 62 | the `exceptionUrls` (or it's alias `exception_url`) optional parameter is a single value, or an array of strings, regexes and functions 63 | that can be used to check whether minifying should be skipped entirely. 64 | 65 | ```js 66 | exceptionUrls: [ 67 | 'url_to_avoid_minify_html', // String. 68 | /regex_to_analyze_req_to_avoid_minify/i, // Regex. 69 | function (req, res) { 70 | // Function. 71 | // Code to analyze req and decide if skips or not minify. 72 | // Needs to return a boolean value. 73 | return true 74 | }, 75 | ] 76 | ``` 77 | 78 | Full examples can naturally be found under the 'examples'-folder of this repository! 79 | 80 | ## Other infos 81 | 82 | The code inside minifier.js should be compatible with nodejs 6.0.0, 83 | but since `html-minifier-terser` requires "^14.13.1" the package also requires at least nodejs 14.0.0 84 | 85 | ## License 86 | 87 | MIT © [Matti Jokitulppo](http://mattij.com) 88 | 89 | [![npm version](https://badge.fury.io/js/express-minify-html-2.svg)](https://badge.fury.io/js/express-minify-html-2) 90 | [![npm downloads](https://img.shields.io/npm/dm/express-minify-html-2.svg)](https://img.shields.io/npm/dm/express-minify-html-2.svg) 91 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const express = require('express') 5 | const expressHandlebars = require('express-handlebars') 6 | const minifyHTML = require('../minifier.js') 7 | const request = require('supertest') 8 | const test = require('tape') 9 | const nunjucks = require('nunjucks') 10 | 11 | const handlebars = expressHandlebars.create({ 12 | defaultLayout: 'test', 13 | layoutsDir: __dirname, 14 | }) 15 | 16 | let app 17 | 18 | nunjucks.configure(__dirname, { 19 | autoescape: true, 20 | express: app, 21 | }) 22 | 23 | // ----------------------------- 24 | 25 | app = express() 26 | 27 | app.engine('handlebars', handlebars.engine) 28 | app.engine('nunjucks', nunjucks.render) 29 | 30 | app.locals.pretty = true 31 | 32 | app.set('views', __dirname) 33 | 34 | app.use( 35 | minifyHTML({ 36 | override: true, 37 | exception_url: ['skip-minify'], 38 | htmlMinifier: { 39 | removeComments: true, 40 | collapseWhitespace: true, 41 | collapseBooleanAttributes: true, 42 | removeAttributeQuotes: true, 43 | removeEmptyAttributes: true, 44 | minifyJS: true, 45 | }, 46 | }), 47 | ) 48 | 49 | app 50 | .get('/test', function (req, res, next) { 51 | res.render('test', { hello: 'world' }) 52 | }) 53 | .get('/test-broken', function (req, res, next) { 54 | res.render('test-broken', { hello: 'world' }) 55 | }) 56 | .get('/skip-minify', function (req, res, next) { 57 | res.render('test', { hello: 'world' }) 58 | }) 59 | 60 | // ----------------------------- 61 | 62 | const expectedHTML = fs.readFileSync(__dirname + '/test.output.html', 'utf8') 63 | const expectedRawHTML = { 64 | default: fs.readFileSync(__dirname + '/test.output.raw.html', 'utf8'), 65 | pug: fs.readFileSync(__dirname + '/test.output.raw.pug.html', 'utf8'), 66 | handlebars: fs.readFileSync(__dirname + '/test.output.raw.handlebars.html', 'utf8'), 67 | } 68 | 69 | function checkMinified(t) { 70 | request(app) 71 | .get('/test') 72 | .expect(200) 73 | .end((err, res) => { 74 | t.equal(res.text, expectedHTML) 75 | t.end() 76 | }) 77 | } 78 | 79 | function checkSkipMinified(t, engine) { 80 | request(app) 81 | .get('/skip-minify') 82 | .expect(200) 83 | .end((err, res) => { 84 | var raw = expectedRawHTML[engine] || expectedRawHTML.default 85 | t.equal(res.text.trim(), raw.trim()) 86 | t.end() 87 | }) 88 | } 89 | 90 | // ----------------------------- 91 | 92 | test('Should minify EJS templates', (t) => { 93 | app.set('view engine', 'ejs') 94 | 95 | checkMinified(t) 96 | }) 97 | 98 | test('Should minify Pug templates', (t) => { 99 | app.set('view engine', 'pug') 100 | 101 | checkMinified(t) 102 | }) 103 | 104 | test('Should minify Handlebars templates', (t) => { 105 | app.set('view engine', 'handlebars') 106 | 107 | checkMinified(t) 108 | }) 109 | 110 | test('Should minify Nunjucks templates', (t) => { 111 | app.set('view engine', 'nunjucks') 112 | 113 | checkMinified(t) 114 | }) 115 | 116 | test('Should skip minify EJS templates', (t) => { 117 | app.set('view engine', 'ejs') 118 | 119 | checkSkipMinified(t, 'ejs') 120 | }) 121 | 122 | test('Should skip minify Pug templates', (t) => { 123 | app.set('view engine', 'pug') 124 | 125 | checkSkipMinified(t, 'pug') 126 | }) 127 | 128 | test('Should skip minify Handlebars templates', (t) => { 129 | app.set('view engine', 'handlebars') 130 | 131 | checkSkipMinified(t, 'handlebars') 132 | }) 133 | 134 | test('Should skip minify Nunjucks templates', (t) => { 135 | app.set('view engine', 'nunjucks') 136 | 137 | checkSkipMinified(t) 138 | }) 139 | 140 | test('Should pass error to express on broken html', (t) => { 141 | request(app) 142 | .get('/test-broken') 143 | .expect(500) 144 | .end(function (err, res) { 145 | t.end() 146 | }) 147 | }) 148 | --------------------------------------------------------------------------------