File: lib/server.js

Recommend this page to a friend!
  Classes of Gamaiel Zavala  >  cmbn  >  lib/server.js  >  Download  
File: lib/server.js
Role: Class source
Content type: text/plain
Description: Class source
Class: cmbn
Generate combos of JavaScript code served via CDNs
Author: By
Last change: Update of lib/server.js
Date: 2 months ago
Size: 6,625 bytes
 

Contents

Class file image Download
/* Copyright (c) 2012 Yahoo! Inc.  All rights reserved.
   Copyrights licensed under the MIT License. See the accompanying LICENSE file for terms. */

var liburl = require('url'),
    md5 = require('MD5'),
    lru = require('lru-cache'),
    async = require('async'),
    request = require('request'),
    express = require('express'),
    cdns = require('./cdns'),
    gzippo;

// fallback to connect for compress() if gzippo isn't available
try
{
    gzippo = require('gzippo');
}
catch (e)
{
    console.log('Notice: gzippo not found, using connect instead.');
    gzippo = require('connect');
}

var middleware = function (options)
{
    var opts = options || {},
        cache = lru(opts.cacheSize || 5000000, opts.cacheFunc || function (item){ return item.length; });
    
    return function combine (req, res, next)
    {
        var assets = [],
            reqs = [],
            contentType = '',
            maxAge = null,
            regex = /\/(~.+)/,
            incoming = liburl.parse(req.url),
            matches = regex.exec(incoming.pathname),
            segments;
        
        if(matches)
        {
            segments = matches[1].split('/');
        }
        else
        {
            return next();
        }
        
        segments.forEach(function (segment)
        {
            var paths = segment.split(';'),
                cdnSem = paths.shift().substr(1).split('@'),
                cdnCode = cdnSem[0],
                ver = cdnSem[1] || null,
                cdn = cdns.getCdn(cdnCode),
                root;
            
            if(cdn)
            {
                root = (cdn.protocol || 'http') + '://' + cdn.host + '/' + (cdn.prefix || '');
                paths.forEach(function (path)
                {
                    var path = path.replace(/,/g, '/'),
                        url = root + (cdns.isFunc(cdn.formatter) ? cdn.formatter(path, ver) : path),
                        hash = md5(url),
                        asset = cache.get(hash) || {url: url, hash: hash, content: null};
                    
                    assets.push(asset);
                });
            }
            else
            {
                respond({msg: 'Invalid CDN', code: 400});
            }
        });
        
        assets.forEach(function (asset)
        {
            if(asset.content === null)
            {
                reqs.push(function(cb)
                {
                    request(asset.url, function (err, resp, body)
                    {
                        handleResp (err, resp, body, cb);
                    });
                });
            }
        });
        
        if(reqs.length)
        {
            async.parallel(reqs, respond);
        }
        else
        {
            respond();
        }
        
        function handleResp (err, resp, body, cb)
        {
            if (err || resp.statusCode !== 200)
            {
                respond({msg: 'Error fetching ' + resp.request.href, code: resp.statusCode});
            }
            else
            {
                var url =  resp.request.href,
                    type = resp.headers['content-type'],
                    cc = resp.headers['cache-control'],
                    maxAge = cc ? cc.match(/max\-age=([0-9]+)/) : null;
                
                // normalize js type
                if(type.match('javascript'))
                {
                    type = 'application/javascript';
                }
                // fix relative paths for css
                else if(type.match('css'))
                {
                    resp.body = fixCssPaths(url, resp.body);
                }
                
                // find asset by url and set content
                assets.forEach(function (asset, index, assets)
                {
                    if(asset.url === url)
                    {
                        asset.type = type;
                        asset.age = maxAge ? maxAge[1] : null;
                        asset.content = resp.body;
                        cache.set(asset.hash, asset);
                    }
                });
                
                cb();
            }
        }
        
        function respond (err, results)
        {
            if (err)
            {
                res.send(err.msg || "", err.code || 404);
                res.end();
            }
            else
            {
                var combined = '';
                
                assets.forEach(function (asset)
                {
                    combined += "\n" + asset.content;
                    
                    // set content type or use default if diff types were returned
                    if(asset.type !== contentType)
                    {
                        contentType = contentType ? 'text/plain' : asset.type;
                    }
                    
                    // set max age to a minimum of all assets
                    if(asset.age !== null)
                    {
                        maxAge = maxAge === null ? asset.age : Math.min(maxAge, asset.age);
                    }
                });
                
                res.setHeader('Content-Type', contentType);
                res.setHeader('Cache-Control', 'public,max-age=' + maxAge);
                
                res.write(combined);
                res.end();
            }
        }
    };
}

var createServer = function (options)
{
    var opts = options || {},
        app = express.createServer();
    
    // enable compression if available
    if(gzippo.compress)
    {
        app.use(gzippo.compress());
    }
    
    // add cmbn middleware
    app.use(middleware(opts));
    
    // optional dir for local assets that can be comboed
    if(opts.staticDir)
    {
        app.use(express.static(opts.staticDir, {maxAge: opts.staticMaxAge * 1000 || 1e15}));
    }
    
    app.listen(opts.port || 80);
}

var fixCssPaths = function (assetUrl, assetContent)
{
    var regex = /url\(\s*(?:['"])?([^'")]+)(?:['"])?\s*\)/g,
        matches;
    
    while(matches = regex.exec(assetContent))
    {
        var match = matches[0],
            path = matches[1];
        
        // skip canonical and data uris
        if('http' === path.substr(0, 4) || 'data:' === path.substr(0, 5))
        {
            continue;
        }
        
        // replace path w/ canonical
        assetContent = assetContent.replace(match, 'url(' + liburl.resolve(assetUrl, path) + ')');
    };
    
    return assetContent;
}

module.exports =
{
    middleware: middleware,
    createServer: createServer
}