├── .gitignore ├── LICENCE ├── Makefile ├── README.md ├── examples ├── connect │ ├── README.md │ ├── app.js │ ├── assets │ │ └── css │ │ │ ├── all.less │ │ │ └── reset.less │ └── bin │ │ └── precompile_assets.js ├── express │ ├── README.md │ ├── app.js │ ├── assets │ │ ├── css │ │ │ ├── all.css.less │ │ │ └── reset.css │ │ └── js │ │ │ ├── application.js.ejs │ │ │ ├── main.coffee.ejs │ │ │ └── models │ │ │ ├── post.coffee │ │ │ └── user.coffee │ ├── bin │ │ └── precompile_assets.js │ ├── vendor │ │ └── js │ │ │ ├── jquery.js │ │ │ └── modernizer.js │ └── views │ │ └── home.ejs └── nib │ ├── README.md │ ├── app.js │ ├── assets │ └── css │ │ └── main.css.styl │ └── views │ └── index.ejs ├── index.js ├── lib └── connect_mincer.js ├── package.json └── test ├── apps └── basic │ ├── assets │ ├── css │ │ └── layout.css.less.ejs │ ├── img │ │ └── background.png │ └── js │ │ ├── app.js │ │ └── libs.js │ ├── index.js │ ├── vendor │ └── js │ │ ├── jquery.js │ │ └── underscore.js │ └── views │ └── index.ejs ├── mocha.opts ├── requests └── basic.js └── unit └── connect_mincer.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .tern-port -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Clark 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | NODE_ENV=test ./node_modules/.bin/mocha test/unit test/requests 3 | 4 | .PHONY: test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connect-mincer 2 | 3 | This is an Express-compatible, Connect middleware for [Mincer](https://github.com/nodeca/mincer). 4 | 5 | ## What is Mincer and why do I want this? 6 | 7 | Mincer is an excellent port of Sprockets, which means it is a robust and comprehensive asset manager for your Node app. However, Mincer makes no assumptions about your application so by default it requires some work to get going with a typical Express app. 8 | 9 | Using connect-mincer, you can skip that work and simply: 10 | 11 | * Write and serve CoffeeScript, LESS, Stylus, Sass, etc 12 | * Have everything recompiled on each request (in development) 13 | * Serve files with an MD5 digest (for caching) 14 | * Use whatever directory structure you want 15 | * Precompile all your assets and have your Connect app read from the compile manifest 16 | 17 | If you're used to the Rails asset pipeline, using this will give your Connect/Express application almost all the same capability - the only thing missing is a built-in precompile, but that's easily added (see the [example app](https://github.com/clarkdave/connect-mincer/tree/master/examples/express) for an example precompile script). 18 | 19 | ## Let's go! 20 | 21 | npm install connect-mincer 22 | 23 | Now, in your connect app: 24 | 25 | ``` javascript 26 | var ConnectMincer = require('connect-mincer'); 27 | 28 | var connectMincer = new ConnectMincer({ 29 | root: __dirname, 30 | production: process.env.NODE_ENV === 'production', 31 | mountPoint: '/assets', 32 | manifestFile: __dirname + '/public/assets/manifest.json', 33 | paths: [ 34 | 'assets/css', 35 | 'assets/js', 36 | 'vendor/js' 37 | ] 38 | }); 39 | 40 | // access the internal Mincer object if you want to do anything extra to it, e.g. 41 | connectMincer.Mincer.CoffeeEngine.setOptions({ bare: false }); 42 | 43 | app.use(connectMincer.assets()); 44 | 45 | if (process.env.NODE_ENV !== 'production') 46 | app.use('/assets', connectMincer.createServer()); 47 | ``` 48 | 49 | The connectMincer.assets() middleware will: 50 | 51 | * Provide js(), css() and asset_path() helpers for your views 52 | * In development, ensure that assets are recompiled on every request 53 | 54 | Now, in your views, you can do this: 55 | 56 | ``` html 57 | 58 | <%- css('main.css') %> 59 | <%- js('application.js') %> 60 | 61 | ``` 62 | 63 | These helpers will output something like: ``. 64 | 65 | The second piece of middleware, `connectMincer.createServer()`, sets up a Mincer server which will send the compiled version of the asset. This is great for development, though in production you'll probably want to have these files served by nginx or from a cloud host (see more about 'in production' below). 66 | 67 | ## In more detail 68 | 69 | Mincer and this middleware are unopinionated about where your keep your assets. When you initialise connect-mincer you pass in several options: 70 | 71 | - **root** 72 | - This is usually the root of your app. Asset paths are relative to this. 73 | - **mincer** 74 | - (Optional) Use this to pass in your own Mincer object. 75 | - If not provided, ConnectMincer will use its own bundled version of Mincer, which may be out of date. The Mincer version provided MUST be >= 0.5.0, as older versions have an unsupported API 76 | - **production** 77 | - Set to true if the app is running in production mode. 78 | - **paths** 79 | - A list of directories where your assets are located. 80 | - **mountPoint** *(optional)* 81 | - This is what the js, css and asset_path helpers use to create the URL for each asset. Defaults to `/assets`. 82 | - **assetHost** *(optional)* 83 | - If specified, the view helpers will generate urls of the form `assetHost + mountPoint + asset`. E.g. `//j2938fj.cloudfront.net/assets/layout.css` 84 | - You should specify the protocol, i.e. `http://`, `https://` or `//` 85 | - This can be used to serve assets from a CDN like Cloudfront 86 | 87 | A typical app folder structure might be this: 88 | 89 | app/ 90 | assets/ 91 | js/ 92 | application.js 93 | css/ 94 | main.less 95 | blog/ 96 | more.css 97 | images/ 98 | logo.png 99 | lib/ 100 | public/ 101 | vendor/ 102 | js/ 103 | jquery.js 104 | app.js 105 | 106 | With this, a suitable path list would be: 107 | 108 | ['assets/js', 'assets/css', 'assets/images', 'vendor/js'] 109 | 110 | Now anything within these paths can be referenced in your views with the helpers like so: 111 | 112 | css('main.css') 113 | css('print.css', { media: 'print' }) 114 | js('jquery.js') 115 | 116 | Which would become: 117 | 118 | 119 | 120 | 121 | 122 | ### View helpers 123 | 124 | connect-mincer provides several helpers which are available in all views. All of them take an asset filename as their first argument, which is used to search for a matching asset across all asset directories (i.e. the path option provided to connect-mincer). If the asset is actually a bundle (it includes or requires other assets), all of these 125 | will be returned in development mode. In production, only the bundle itself will be returned (which will include all its required dependencies concatenated within). 126 | 127 | In all cases, the urls returned here will contain their MD5 digest if the app is running in production. 128 | 129 | #### js(path, attributes) -> One or more script tags 130 | 131 | * filename (String) The filename of the asset relative to its asset directory 132 | * attributes (Object) An object containing attributes for the ` 160 | 161 | which will correspond to the file `/public/assets/application-4b02e3a0746a47886505c9acf5f8c655.js`. Now you can set nginx up to intercept requests to `/assets` and serve the static file in `/public/assets` instead. Thanks to the MD5 digest, you can set the cache headers to maximum. The next time you deploy and precompile the digests will change, and your app will adjust its ` 17 | <%- js('application.js') %> 18 | 19 | -------------------------------------------------------------------------------- /examples/nib/README.md: -------------------------------------------------------------------------------- 1 | # connect-mincer example with Stylus and nib 2 | 3 | Nothing much to see here: it's just a tiny example app which uses nib and Stylus. -------------------------------------------------------------------------------- /examples/nib/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'), 4 | env = process.env.NODE_ENV, 5 | ConnectMincer = require('../../') 6 | ; 7 | 8 | var app = express(); 9 | 10 | var mincer = new ConnectMincer({ 11 | root: __dirname, 12 | production: env === 'production' || env === 'staging', 13 | mountPoint: '/assets', 14 | manifestFile: __dirname + '/public/assets/manifest.json', 15 | paths: [ 16 | 'assets/images', 17 | 'assets/css', 18 | 'assets/js', 19 | 'vendor/css', 20 | 'vendor/js' 21 | ] 22 | }); 23 | 24 | mincer.Mincer.StylusEngine.configure(function(style) { 25 | style.use(require('nib')()); 26 | }); 27 | 28 | app.use(mincer.assets()); 29 | 30 | if (env === 'production' || env === 'staging') { 31 | app.use(express.static(__dirname + '/public')); 32 | } else { 33 | // in dev, just use the normal server which recompiles assets as needed 34 | app.use('/assets', mincer.createServer()); 35 | } 36 | 37 | app.set('port', process.env.PORT || 9000); 38 | app.set('view engine', 'ejs'); 39 | 40 | app.get('/', function(req, res) { 41 | res.render('index.ejs'); 42 | }); 43 | 44 | app.listen(app.get('port'), function() { 45 | console.info('Express app started on ' + app.get('port')); 46 | }); -------------------------------------------------------------------------------- /examples/nib/assets/css/main.css.styl: -------------------------------------------------------------------------------- 1 | @import 'nib'; 2 | 3 | global-reset(); 4 | 5 | body { 6 | background-color: #222; 7 | color: #fff; 8 | } -------------------------------------------------------------------------------- /examples/nib/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- css('main.css') %> 6 | 7 | 8 |

This an app

9 |

Hello

10 | 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clarkdave/connect-mincer/e6788f1d7388a13f6f5dfbca9f035ca639446e7f/index.js -------------------------------------------------------------------------------- /lib/connect_mincer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'), 4 | _ = require('underscore') 5 | ; 6 | 7 | var ConnectMincer = (function() { 8 | 9 | function ConnectMincer(opts) { 10 | opts = opts || {}; 11 | 12 | this._setMincer(opts.mincer); 13 | 14 | this.production = opts.production === true ? true : false; 15 | this.options = opts; 16 | 17 | this._setAssetHost(opts.assetHost); 18 | this._setMountPoint(opts.mountPoint); 19 | this._createEnvironment(opts.root, opts.paths); 20 | 21 | if (this.options.precompile !== false) { 22 | // if precompile has not explicitly been set to false, default it to true 23 | this.options.precompile = true; 24 | } 25 | 26 | if (this.production) { 27 | // in production, we look up assets in the supplied manifest, so let's load that now and error 28 | // if it's not present, because in production mode we never compile assets on the fly 29 | if (fs.existsSync(opts.manifestFile)) { 30 | this.manifest = require(opts.manifestFile); 31 | if (!this.manifest || !this.manifest.assets) { 32 | throw new Error("Running in production but manifest file [" + opts.manifestFile + "] is not a valid manifest file"); 33 | } 34 | } else { 35 | throw new Error("Running in production but manifest file [" + opts.manifestFile + "] not found"); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Set the Mincer class we'll use. 42 | * 43 | * If not provided, the bundled Mincer object will be used. It should be safe to pass in any version of Mincer 44 | * unless there has been a major update and the public API has changed. 45 | * 46 | * Versions < 0.5.x of Mincer are not supported and will generate an error 47 | * 48 | * @param {[type]} Mincer [description] 49 | */ 50 | ConnectMincer.prototype._setMincer = function(Mincer) { 51 | 52 | // if no Mincer was provided, we'll require our own bundled one 53 | this.Mincer = Mincer || require('mincer'); 54 | 55 | // check the version of the passed in Mincer. We no longer support versions < 0.5 because of API changes 56 | var version = this.Mincer.VERSION; 57 | 58 | if (!version) throw new Error("Mincer class should be a valid Mincer object, e.g. require('mincer')"); 59 | 60 | var major = parseInt(version.split('.')[0], 10), 61 | minor = parseInt(version.split('.')[1], 10); 62 | 63 | if (major === 0 && minor < 5) throw new Error("provided Mincer class must be >= 0.5.x (you provided " + version + ")"); 64 | }; 65 | 66 | /** 67 | * Sanitize and set the mount point. If no mount point was provided, it will default 68 | * to /assets. 69 | * 70 | * @acccess private 71 | * @param {[type]} mountPoint [description] 72 | */ 73 | ConnectMincer.prototype._setMountPoint = function(mountPoint) { 74 | 75 | if (!mountPoint) { 76 | mountPoint = '/assets'; 77 | } else { 78 | if (mountPoint.substr(0, 1) !== '/') { 79 | mountPoint = '/' + mountPoint; 80 | } 81 | if (mountPoint.substr(-1) === '/') { 82 | mountPoint = mountPoint.substr(0, mountPoint.length - 1); 83 | } 84 | } 85 | 86 | this.mountPoint = mountPoint; 87 | }; 88 | 89 | /** 90 | * Set the asset host if the provided assetHost is not null or an empty string. 91 | * 92 | * @param {[type]} assetHost [description] 93 | */ 94 | ConnectMincer.prototype._setAssetHost = function(assetHost) { 95 | 96 | this.assetHost = null; 97 | 98 | if (assetHost && assetHost.length > 0) { 99 | this.assetHost = assetHost; 100 | 101 | // if (assetHost.substr(0, 2) !== '//' && assetHost.substr(0, 7) !== 'http://' && assetHost.substr(0, 8) !== 'https://') { 102 | // throw new Error("Asset host must start with '//', 'http://' or 'https://"); 103 | // } 104 | } 105 | }; 106 | 107 | /** 108 | * Create a new Mincer Environment with the provided root and paths. If the root does 109 | * not exist an error will be thrown. In production, this will create a cached version 110 | * of the environment. 111 | * 112 | * This also attaches various helpers to the environment which can be used in assets. 113 | * 114 | * @access private 115 | * @param {[type]} root [description] 116 | * @param {[type]} paths [description] 117 | * @return {[type]} [description] 118 | */ 119 | ConnectMincer.prototype._createEnvironment = function(root, paths) { 120 | var self = this; 121 | 122 | if (!fs.existsSync(root)) { 123 | throw new Error("Asset root [" + root + "] does not exist"); 124 | } 125 | 126 | if (!Array.isArray(paths) || !paths.length) { 127 | throw new Error("Asset paths are missing, e.g. ['assets/css', 'assets/js']"); 128 | } 129 | 130 | var environment = new this.Mincer.Environment(root); 131 | paths.forEach(function(path) { 132 | environment.appendPath(path); 133 | }); 134 | 135 | // implement the `asset_path` helper for assets. this is not set by default because Mincer doesn't 136 | // know what the final path should be, but we do so we're implementing it here 137 | // 138 | // this can then be used in assets thusly: <%= asset_path('logo.png') %> 139 | 140 | environment.registerHelper('asset_path', function(name, opts) { 141 | var asset = environment.findAsset(name, opts); 142 | if (!asset) throw new Error("File [" + name + "] not found"); 143 | if (self.production) { 144 | return self._toAssetUrl(asset.digestPath); 145 | } else { 146 | return self._toAssetUrl(asset.logicalPath); 147 | } 148 | }); 149 | 150 | this.environment = environment; 151 | }; 152 | 153 | /** 154 | * Find all the asset paths for the provided logicalPath and returns them. 155 | * 156 | * If the logicalPath is a bundle, and the app is not in production mode, multiple paths 157 | * will be returned (one for each asset referenced in the bundle). 158 | * 159 | * @access private 160 | * @param {[type]} logicalPath [description] 161 | * @param {[type]} ext [description] 162 | * @return {[type]} [description] 163 | */ 164 | ConnectMincer.prototype._findAssetPaths = function(logicalPath, ext) { 165 | var self = this; 166 | 167 | if (this.production) { 168 | // we're in production, and have a valid manifest, so instead of using the environment 169 | // we will get the digestPath directly from the manifest 170 | 171 | var digestPath = this.manifest.assets[logicalPath]; 172 | 173 | if (!digestPath) { 174 | // this is bad, and probably means someone forgot to do compile assets before running 175 | // in production. 176 | throw new Error("Asset [" + logicalPath + "] has not been compiled"); 177 | } 178 | 179 | return [this._toAssetUrl(digestPath)]; 180 | } 181 | 182 | // if we're in normal development mode, we should look for the asset in our current environment 183 | // and return it. It should already be compiled because we force a precompile in dev mode on 184 | // every request. 185 | 186 | var asset = this.environment.findAsset(logicalPath), 187 | paths = [] 188 | ; 189 | 190 | if (!asset) return null; 191 | 192 | // precompiling/compiling is no longer supported in Mincer 5.x which simplifies things for us - the 193 | // asset itself will be compiled on the fly when a request for it comes in (except for in prod, but 194 | // that's handled above and requires a manifest and compiled assets anyway) 195 | 196 | asset.toArray().forEach(function(a) { 197 | // iterate this asset (there could be multiple if it's a bundle) and add 198 | // each as a logical path (no digest), with ?body=1 as per sprockets 199 | paths.push(self._toAssetUrl(a.logicalPath) + '?body=1'); 200 | }); 201 | 202 | return paths; 203 | }; 204 | 205 | ConnectMincer.prototype._toAssetUrl = function(source) { 206 | return (this.assetHost ? this.assetHost : '') + this.mountPoint + '/' + source; 207 | }; 208 | 209 | /** 210 | * Get the helper function matching the given name, which should be one of: 211 | * - js 212 | * - css 213 | * - asset_path 214 | * 215 | * @param {[type]} name Helper name 216 | * @return {[type]} The helper function 217 | */ 218 | ConnectMincer.prototype.getHelper = function(name) { 219 | var self = this; 220 | 221 | var createTag = function(type, path, attributes) { 222 | var tag, base_attributes = {}; 223 | 224 | switch (type) { 225 | case 'js': 226 | tag = "'; 245 | case 'css': 246 | return tag + '>'; 247 | } 248 | }; 249 | 250 | switch (name) { 251 | 252 | case 'js': 253 | return function(path, attributes) { 254 | var paths = self._findAssetPaths(path); 255 | if (!paths) throw new Error('Javascript asset [' + path + '] not found'); 256 | 257 | var tags = paths.map(function(path) { 258 | return createTag('js', path, attributes); 259 | }); 260 | 261 | return tags.join("\n"); 262 | }; 263 | 264 | case 'css': 265 | return function(path, attributes) { 266 | var paths = self._findAssetPaths(path); 267 | if (!paths) throw new Error('CSS asset [' + path + '] not found'); 268 | 269 | var tags = paths.map(function(path) { 270 | return createTag('css', path, attributes); 271 | }); 272 | 273 | return tags.join("\n"); 274 | }; 275 | 276 | case 'asset_path': 277 | return function(path) { 278 | var paths = self._findAssetPaths(path); 279 | 280 | if (!paths) return ''; 281 | 282 | if (paths.length === 1) { 283 | return paths[0]; 284 | } else { 285 | return paths; 286 | } 287 | }; 288 | } 289 | 290 | return null; 291 | }; 292 | 293 | /** 294 | * Get an object containing all helper functions 295 | * 296 | * @return {[type]} Object with all helper functions 297 | */ 298 | ConnectMincer.prototype.getHelpers = function() { 299 | return { 300 | js: this.getHelper('js'), 301 | css: this.getHelper('css'), 302 | asset_path: this.getHelper('asset_path') 303 | }; 304 | }; 305 | 306 | /** 307 | * The asset middleware. 308 | * 309 | * The asset middleware adds view helpers for loading js and css, and if in development mode 310 | * will also ensure assets are precompiled and route the mincer assetServer to /assetUrl 311 | * 312 | * The following helpers will be available in all views, if Express is used: 313 | * 314 | * - js() 315 | * Accepts an asset name (e.g. 'app.js') and if found, outputs an appropriate