diff --git a/minifier.js b/minifier.js index 16ba513df9..0d4f2b7966 100644 --- a/minifier.js +++ b/minifier.js @@ -5,6 +5,7 @@ var uglifyjs = require('uglify-js'), async = require('async'), fs = require('fs'), path = require('path'), + crypto = require('crypto'), Minifier = { js: {}, @@ -28,13 +29,27 @@ Minifier.js.minify = function (scripts, minify, callback) { } try { - var minified = uglifyjs.minify(scripts, options); + var minified = uglifyjs.minify(scripts, options), + hasher = crypto.createHash('md5'), + hash; + + // Calculate js hash + hasher.update(minified.code, 'utf-8'); + hash = hasher.digest('hex'); + process.send({ + type: 'hash', + payload: hash.slice(0, 8) + }); + callback({ js: minified.code, map: minified.map }); } catch(err) { - process.send(err.message); + process.send({ + type: 'error', + payload: err + }); } }; @@ -43,9 +58,16 @@ process.on('message', function(payload) { case 'js': Minifier.js.minify(payload.scripts, payload.minify, function(data) { process.stdout.write(data.js); - process.send('end.script'); + process.send({ + type: 'end', + payload: 'script' + }); + process.stderr.write(data.map); - process.send('end.mapping'); + process.send({ + type: 'end', + payload: 'mapping' + }); }); break; } diff --git a/src/controllers/api.js b/src/controllers/api.js index 57a0cda154..43103eab19 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -44,6 +44,8 @@ apiController.getConfig = function(req, res, next) { config.environment = process.env.NODE_ENV; config.loggedIn = !!req.user; config['cache-buster'] = meta.config['cache-buster'] || ''; + config['script-buster'] = meta.js.hash; + config['css-buster'] = meta.css.hash; config.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1; config.topicPostSort = meta.config.topicPostSort || 'oldest_to_newest'; diff --git a/src/meta/css.js b/src/meta/css.js index a62b4aa1ef..f60e320703 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -5,6 +5,7 @@ var winston = require('winston'), fs = require('fs'), path = require('path'), less = require('less'), + crypto = require('crypto'), plugins = require('../plugins'), emitter = require('../emitter'), @@ -12,7 +13,9 @@ var winston = require('winston'), module.exports = function(Meta) { - Meta.css = {}; + Meta.css = { + 'css-buster': +new Date() + }; Meta.css.cache = undefined; Meta.css.branding = {}; Meta.css.defaultBranding = {}; @@ -57,15 +60,22 @@ module.exports = function(Meta) { try { var css = tree.toCSS({ cleancss: true - }); + }); } catch (err) { winston.error('[meta/css] Syntax Error: ' + err.message + ' - ' + path.basename(err.filename) + ' on line ' + err.line); - return; + return; } - + Meta.css.cache = css; + // Calculate css buster + var hasher = crypto.createHash('md5'), + hash; + hasher.update(css, 'utf-8'); + hash = hasher.digest('hex').slice(0, 8); + Meta.css.hash = hash; + var re = /.brand-([\S]*?)[ ]*?{[\s\S]*?color:([\S\s]*?)}/gi, match = re.exec(css); diff --git a/src/meta/js.js b/src/meta/js.js index 08cad37484..29c6f77880 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -16,6 +16,7 @@ module.exports = function(Meta) { Meta.js = { cache: '', map: '', + hash: +new Date(), prepared: false, minFile: 'nodebb.min.js', scripts: [ @@ -160,19 +161,22 @@ module.exports = function(Meta) { Meta.js.map += buffer.toString(); }); - minifier.on('message', function(payload) { - switch(payload) { - case 'end.script': - winston.info('[meta/js] Successfully minified.'); - onComplete(); + minifier.on('message', function(message) { + switch(message.type) { + case 'end': + if (message.payload === 'script') { + winston.info('[meta/js] Successfully minified.'); + onComplete(); + } else if (message.payload === 'mapping') { + winston.info('[meta/js] Retrieved Mapping.'); + onComplete(); + } break; - case 'end.mapping': - winston.info('[meta/js] Retrieved Mapping.'); - onComplete(); + case 'hash': + Meta.js.hash = message.payload; break; - - default: - winston.error('[meta/js] Could not compile client-side scripts! ' + payload); + case 'error': + winston.error('[meta/js] Could not compile client-side scripts! ' + message.payload.message); minifier.kill(); process.exit(); break; diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 738ada5cd2..470b85fed8 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -425,6 +425,15 @@ middleware.routeTouchIcon = function(req, res) { } }; +middleware.addExpiresHeaders = function(req, res, next) { + if (app.enabled('cache')) { + res.setHeader("Cache-Control", "public, max-age=5184000"); + res.setHeader("Expires", new Date(Date.now() + 5184000000).toUTCString()); + } + + next(); +}; + module.exports = function(webserver) { app = webserver; middleware.admin = require('./admin')(webserver); diff --git a/src/routes/meta.js b/src/routes/meta.js index cce309b089..f0d9aa85b2 100644 --- a/src/routes/meta.js +++ b/src/routes/meta.js @@ -6,6 +6,7 @@ var path = require('path'), meta = require('../meta'), db = require('../database'), plugins = require('../plugins'), + middleware = require('../middleware'), minificationEnabled = false; @@ -46,13 +47,13 @@ function setupPluginSourceMapping(app) { } module.exports = function(app, middleware, controllers) { - app.get('/stylesheet.css', sendStylesheet); - app.get('/nodebb.min.js', sendMinifiedJS); + app.get('/stylesheet.css', middleware.addExpiresHeaders, sendStylesheet); + app.get('/nodebb.min.js', middleware.addExpiresHeaders, sendMinifiedJS); app.get('/sitemap.xml', controllers.sitemap); app.get('/robots.txt', controllers.robots); if (!minificationEnabled) { - app.get('/nodebb.min.js.map', sendSourceMap); + app.get('/nodebb.min.js.map', middleware.addExpiresHeaders, sendSourceMap); setupPluginSourceMapping(app); } }; diff --git a/src/routes/plugins.js b/src/routes/plugins.js index bcff6bf352..fd195ca3f2 100644 --- a/src/routes/plugins.js +++ b/src/routes/plugins.js @@ -14,7 +14,7 @@ var _ = require('underscore'), module.exports = function(app, middleware, controllers) { // Static Assets - app.get('/plugins/:id/*', function(req, res) { + app.get('/plugins/:id/*', middleware.addExpiresHeaders, function(req, res) { var relPath = req._parsedUrl.pathname.replace('/plugins/', ''), matches = _.map(plugins.staticDirs, function(realPath, mappedPath) { if (relPath.match(mappedPath)) {