Skip to content

Commit

Permalink
Minify and compress JS & CSS before sending it
Browse files Browse the repository at this point in the history
  • Loading branch information
Pita committed May 28, 2011
1 parent d81b539 commit 0c3f0e9
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 39 deletions.
233 changes: 233 additions & 0 deletions node/minify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
var settings = require('./settings');
var async = require('async');
var fs = require('fs');
var cleanCSS = require('clean-css');
var jsp = require("uglify-js").parser;
var pro = require("uglify-js").uglify;
var compress=require("compress");
var path = require('path');
var Buffer = require('buffer').Buffer;
var gzip = require('gzip');

/**
* Answers a http request for the pad javascript
*/
exports.padJS = function(req, res)
{
res.header("Content-Type","text/javascript");

var jsFiles = ["plugins.js", "undo-xpopup.js", "json2.js", "pad_utils.js", "pad_cookie.js", "pad_editor.js", "pad_editbar.js", "pad_docbar.js", "pad_modals.js", "ace.js", "collab_client.js", "pad_userlist.js", "pad_impexp.js", "pad_savedrevs.js", "pad_connectionstatus.js", "pad2.js"];

//minifying is enabled
if(settings.minify)
{
var fileValues = {};
var embeds = {};
var latestModification = 0;

async.series([
//find out the highest modification date
function(callback)
{
var folders2check = ["../static/css","../static/js"];

//go trough this two folders
async.forEach(folders2check, function(path, callback)
{
//read the files in the folder
fs.readdir(path, function(err, files)
{
if(err) { callback(err); return; }

//we wanna check the directory itself for changes too
files.push(".");

//go trough all files in this folder
async.forEach(files, function(filename, callback)
{
//get the stat data of this file
fs.stat(path + "/" + filename, function(err, stats)
{
if(err) { callback(err); return; }

//get the modification time
var modificationTime = stats.mtime.getTime();

//compare the modification time to the highest found
if(modificationTime > latestModification)
{
latestModification = modificationTime;
}

callback();
});
}, callback);
});
}, callback);
},
function(callback)
{
//check the modification time of the minified js
fs.stat("../var/minified_pad.js", function(err, stats)
{
if(err && err.code != "ENOENT") callback(err);

//there is no minfied file or there new changes since this file was generated, so continue generating this file
if((err && err.code == "ENOENT") || stats.mtime.getTime() < latestModification)
{
callback();
}
//the minified file is still up to date, stop minifying
else
{
callback("stop");
}
});
},
//load all js files
function (callback)
{
async.forEach(jsFiles, function (item, callback)
{
fs.readFile("../static/js/" + item, "utf-8", function(err, data)
{
fileValues[item] = data;
callback(err);
});
}, callback);
},
//find all includes in ace.js and embed them
function(callback)
{
var founds = fileValues["ace.js"].match(/\$\$INCLUDE_[a-zA-Z_]+\([a-zA-Z0-9.\/_"]+\)/gi);

//go trough all includes
async.forEach(founds, function (item, callback)
{
var filename = item.match(/"[^"]*"/g)[0].substr(1);
filename = filename.substr(0,filename.length-1);

var type = item.match(/INCLUDE_[A-Z]+/g)[0].substr("INCLUDE_".length);

var quote = item.search("_Q") != -1;

//read the included file
fs.readFile(".." + filename, "utf-8", function(err, data)
{
//compress the file
if(type == "JS")
{
embeds[item] = "<script>\n" + compressJS([data])+ "\n\\x3c/script>";
}
else
{
embeds[item] = "<style>" + compressCSS([data])+ "</style>";
}

//do the first escape
embeds[item] = JSON.stringify(embeds[item]).replace(/'/g, "\\'").replace(/\\"/g, "\"");
embeds[item] = embeds[item].substr(1);
embeds[item] = embeds[item].substr(0, embeds[item].length-1);

//add quotes, if wished
if(quote)
{
embeds[item] = "'" + embeds[item] + "'";
}

//do the second escape
embeds[item] = JSON.stringify(embeds[item]).replace(/'/g, "\\'").replace(/\"/g, "\"");
embeds[item] = embeds[item].substr(1);
embeds[item] = embeds[item].substr(0, embeds[item].length-1);
embeds[item] = "'" + embeds[item] + "'";

callback(err);
});
}, function(err)
{
//replace the include command with the include
for(var i in embeds)
{
fileValues["ace.js"]=fileValues["ace.js"].replace(i, embeds[i]);
}

callback(err);
});
},
//put all together and write it into a file
function(callback)
{
//put all javascript files in an array
var values = [];
for(var i in fileValues)
{
values.push(fileValues[i]);
}

//minify all javascript files to one
var result = compressJS(values);

async.parallel([
//write the results plain in a file
function(callback)
{
fs.writeFile("../var/minified_pad.js", result, "utf8", callback);
},
//write the results compressed in a file
function(callback)
{
gzip(result, 9, function(err, compressedResult){
if(err) {callback(err); return}

fs.writeFile("../var/minified_pad.js.gz", compressedResult, callback);
});
}
],callback);
}
], function(err)
{
if(err && err != "stop") throw err;

//check if gzip is supported by this browser
var gzipSupport = req.header('Accept-Encoding', '').indexOf('gzip') != -1;

var pathStr;
if(gzipSupport)
{
pathStr = path.normalize(__dirname + "/../var/minified_pad.js.gz");
res.header('Content-Encoding', 'gzip');
}
else
{
pathStr = path.normalize(__dirname + "/../var/minified_pad.js");
}

res.sendfile(pathStr);
})
}
//minifying is disabled, so load the files with jquery
else
{
for(var i in jsFiles)
{
res.write("$.getScript('/static/js/" + jsFiles[i]+ "');\n");
}

res.end();
}
}

function compressJS(values)
{
var complete = values.join("\n");
var ast = jsp.parse(complete); // parse code and get the initial AST
ast = pro.ast_mangle(ast); // get a new AST with mangled names
ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
return pro.gen_code(ast); // compressed code here
}

function compressCSS(values)
{
var complete = values.join("\n");
return cleanCSS.process(complete);
}
26 changes: 22 additions & 4 deletions node/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@

require('joose');

var socketio = require('socket.io')
var settings = require('./settings')
var db = require('./db')
var socketio = require('socket.io');
var settings = require('./settings');
var db = require('./db');
var async = require('async');
var express = require('express');
var path = require('path');
var minify = require('./minify');

var serverName = "Etherpad-Lite ( http://j.mp/ep-lite )";

Expand All @@ -45,10 +46,27 @@ async.waterfall([
app.get('/static/*', function(req, res)
{
res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/.." + req.url);
var filePath = path.normalize(__dirname + "/.." + req.url.split("?")[0]);
res.sendfile(filePath, { maxAge: 1000*60*60 });
});

//serve minified files
app.get('/minified/:id', function(req, res)
{
res.header("Server", serverName);

var id = req.params.id;

if(id == "pad.js")
{
minify.padJS(req,res);
}
else
{
res.send('404 - Not Found', 404);
}
});

//serve pad.html under /p
app.get('/p/:pad', function(req, res)
{
Expand Down
1 change: 1 addition & 0 deletions node/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ exports.dbType = "sqlite";
exports.dbSettings = { "filename" : "../var/sqlite.db" };
exports.logHTTP = true;
exports.defaultPadText = "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n";
exports.minify = true;

//read the settings sync
var settingsStr = fs.readFileSync("../settings.json").toString();
Expand Down
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
{
"name" : "ep-lite",
"description" : "A Etherpad based on node.js",
"url" : "https://github.com/Pita/etherpad-lite",
"homepage" : "https://github.com/Pita/etherpad-lite",
"keywords" : ["etherpad", "realtime", "collaborative", "editor"],
"author" : "Peter 'Pita' Martischka <[email protected]>",
"dependencies" : {
"socket.io" : ">=0.6.18",
"ueberDB" : ">=0.0.3",
"async" : ">=0.1.9",
"joose" : ">=3.18.0",
"express" : ">=2.3.6"
"socket.io" : "0.6.18",
"ueberDB" : "0.0.3",
"async" : "0.1.9",
"joose" : "3.18.0",
"express" : "2.3.6",
"clean-css" : "0.2.3",
"uglify-js" : "1.0.2",
"gzip" : "0.1.0"
},
"version" : "0.0.3",
"bin" : {
Expand Down
8 changes: 6 additions & 2 deletions settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ This file must be valid JSON. But comments are allowed
"host" : "localhost",
"password": "",
"database": "store"
}
},
*/

//if true, every http request will be loged to stdout
"logHTTP" : true,

//the default text of a pad
"defaultPadText" : "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n"
"defaultPadText" : "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n",

/* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly,
but makes it impossible to debug the javascript/css */
"minify" : true
}
23 changes: 6 additions & 17 deletions static/js/ace.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,27 +164,17 @@ function Ace2Editor() {
};

(function() {
var doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '+
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';

var doctype = "<!doctype html>";

var iframeHTML = ["'"+doctype+"<html><head>'"];

plugins.callHook(
"aceInitInnerdocbodyHead", {iframeHTML:iframeHTML});

// these lines must conform to a specific format because they are passed by the build script:
//iframeHTML.push($$INCLUDE_CSS_Q("editor.css syntax.css inner.css"));

// these lines must conform to a specific format because they are passed by the build script:
iframeHTML.push($$INCLUDE_CSS_Q("/static/css/editor.css"));
iframeHTML.push($$INCLUDE_CSS_Q("/static/css/syntax.css"));
iframeHTML.push($$INCLUDE_CSS_Q("/static/css/inner.css"));

//iframeHTML.push(INCLUDE_JS_Q_DEV("ace2_common_dev.js"));
//iframeHTML.push(INCLUDE_JS_Q_DEV("profiler.js"));

//iframeHTML.push($$INCLUDE_JS_Q("ace2_common.js skiplist.js virtual_lines.js easysync2.js cssmanager.js colorutils.js undomodule.js contentcollector.js changesettracker.js linestylefilter.js domline.js"));
//iframeHTML.push($$INCLUDE_JS_Q("ace2_inner.js"));

iframeHTML.push($$INCLUDE_JS_Q("/static/js/ace2_common.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/skiplist.js"));
iframeHTML.push($$INCLUDE_JS_Q("/static/js/virtual_lines.js"));
Expand Down Expand Up @@ -212,19 +202,18 @@ function Ace2Editor() {
'outerdocbody.insertBefore(iframe, outerdocbody.firstChild); '+
'iframe.ace_outerWin = window; '+
'readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; '+
'var doc = iframe.contentWindow.document; doc.open(); doc.write('+
iframeHTML.join('+')+'); doc.close(); '+
'var doc = iframe.contentWindow.document; doc.open(); var text = ('+
iframeHTML.join('+')+').replace(/\\\\x3c/g, \'<\');doc.write(text); doc.close(); '+
'}, 0); }';

var outerHTML = [doctype, '<html><head>',
$$INCLUDE_CSS("/static/css/editor.css"),
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
// (throbs busy while typing)
'<link rel="stylesheet" type="text/css" href="data:text/css,"/>',
'\x3cscript>', outerScript, '\x3c/script>',
'\x3cscript>\n', outerScript, '\n\x3c/script>',
'</head><body id="outerdocbody"><div id="sidediv"><!-- --></div><div id="linemetricsdiv">x</div><div id="overlaysdiv"><!-- --></div></body></html>'];


if (!Array.prototype.map) Array.prototype.map = function(fun) { //needed for IE
if (typeof fun != "function") throw new TypeError();
var len = this.length;
Expand Down
Loading

0 comments on commit 0c3f0e9

Please sign in to comment.