assets-middleware

Abstract asset middleware for compilation, concatenation, and post-processing.

assets-middleware

Abstract Express/Connect middleware for dealing with static assets like JS and CSS

Higher-level middleware (like LESS, Stylus, Coffeescript compilation, minifying, etc) or combinations of them can be built on top of this.

npm install assets-middleware

Here is an example that takes .coffee and .js files from the ./public directory and the ./lib/jquery.js file, calls compile on any CoffeeScript files, and then calls minify on the concatenated content.

var app = require('express')();
var assets = require('assets-middleware');
 
app.get('/my-scripts.js', assets({
    src : [ './public', './lib/jquery.js' ],
    pipeline : {
        prefilter : [ 'coffee', 'js' ],
        // don't include CSS files. You could use [ 'less', 'css' ] for them 
 
        // compile any .coffee files 
        mapContent : function(contentfilenext) {
            file.extname === '.coffee' ?
                compile(content, next) :
                next(null, content);
        },
 
        // minify the batch 
        postReduceContent : function (concatenatedContentnext) {
            minify(concatenatedContent, next);
        }
    }
}));
 
app.listen(8080);
OptionDefaultAllowed valuesDescription
destidentitySyncpath, function(path) : pathThe file path to write the resulting asset to.
src'./public'path, [path], function(path, callback)The file paths and directories to read from. The passed in path is the destination file
prefix'/'stringA prefix to remove from the URL path when converting it to a local file path. This is called before dest
encoding'utf8'stringThe encoding to use for reading and writing files.
force'ifnewer'true, false, 'ifnewer'Where to force regeneration on every request (true), if the last generated file is older than any source file ('ifnewer') or only if the file doesn't exist (false)
servetruetrue, falseWhether to serve the generated file. If set to false, you'll have to use a static middleware to serve the file yourself.
loggernullfunction(string, level)An optional logger function that is called with a string output and string log level ('error', 'warn', 'info', 'debug')
pipelinestreamy pipelineObjectAn object where each key is a step and its value is the function to execute. See the pipeline tables below
StepDefaultAllowed valuesDescription
prefilterasyncTrueextension, [extension], function(file, callback)This step offers a way to filter out files that shouldn't be included before any heavy processing is done. It can be a function, or use the extension-filtering shorthand.
mapContentidentityfunction(content, file, callback)This step is where you transform the input content into output content.
filterasyncTruefunction(file, callback)This step offers you a last ditch spot to exclude files from the output. You will rarely need to specify this step.
postReduceContentidentityfunction(content, callback)This step is where you can act on the combined content of all your source files.
StepDefaultAllowed valuesDescription
prefilterasyncTrueextension, [extension], function(file, callback)This step offers a way to filter out files that shouldn't be included before any heavy processing is done. It can be a function, or use the extension-filtering shorthand.
mapidentityfunction(content, file, callback)This step is where you transform the input content into output content.
filterasyncTruefunction(file, callback)This step offers you a last ditch spot to exclude files from the output. You will rarely need to specify this step.
reduceSeedcreateWritableStreamfunction(path, callback)Generate a Writable stream from the output file path.
reducepipefunction(writable, readable, callback)Pipe your Readable stream into the Writable stream.
postReduceidentityfunction(content, callback)This step is where you can act on the combined content of all your source files.
ParameterShape
extensionstring
pathstring
contentstring
file{ path : string, stat : stat, extname : string, mapped : ?* }
seedWritable, { path : string, content : string }
callbackfunction(err, result)
  • Note that "mapped" is only present in a file after the map step. In the stringy pipeline, it's a content string. In the streamy pipeline, it's a Readable stream.

We're going to set up a basic compile + minify filter. Step one is to pick our source directories and files:

app.get('/my-scripts.js', assets({
    src : [ './public', './lib/jquery.js' ]
}));

We can choose a destination too. The default is to use the URL path, relative to the current working directory.

app.get('/my-scripts.js', assets({
    src : [ './public', './lib/jquery.js' ],
    dest : './new-location.js'
}));

The above example would take all the files in ./public and the single ./lib/jquery.js file and concatenate them into ./my-location.js. This is of somewhat questionable value since CSS and JS files that exist under ./public would all be included in the same output.

So let's work with this a bit more. assets-middleware gives you an optional pipeline for transforming your files. You can prefilter files based on the path, map to transform the content, filter them again if necessary, and reduce to combine them.

So let's filter out just the JS and skip CSS files. We can do this in our "prefilter" step:

app.get('/my-scripts.js', assets({
    pipeline : { prefilter : 'js' },
    src : [ './public', './lib/jquery.js' ], // specify the source files 
}));

But maybe you like CoffeeScript. Let's include those too, and compile them in the "mapContent" step of the pipeline:

var path = require('path');
var fs = require('fs');
var coffee = require('coffee-script');
function compile(codenext) {
    try {
        next(null, coffee.compile(code));
    } catch(e) { // compile error 
        next(e);
    }
}
 
app.get('/my-scripts.js', assets({
    pipeline : {
        prefilter : [ 'coffee', 'js' ],
        mapContent : function(contentfilenext) {
            file.extname === '.coffee' ?
                compile(content, next) :
                next(null, content);
        }
    },
    src : [ './public', './lib/jquery.js' ]
}));

And you want it minified? Well then we should do that in the postReduceContent step:

var uglify = require('uglify-js');
function minify(codenext) {
    try {
        next(null, uglify.minify(code, { fromString : true }).code);
    } catch (e) {
        next(e);
    }
}
 
app.get('/my-scripts.js', assets({
    pipeline : {
        prefilter : [ 'coffee', 'js' ],
        mapContent : function(contentfilenext) {
            file.extname === '.coffee' ?
                compile(content, next) :
                next(null, content);
        },
        postReduceContent : function (concatenatedContentnext) {
            minify(concatenatedContent, next);
        }
    },
    src : [ './public', './lib/jquery.js' ]
}));

And you're done!

VersionChanges
0.0.3Add serve option and ignore destination file when in a source directory.
0.0.2Respect falsy force values
0.0.1Initial npm publish