diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js
index ac9281c85f..548e0ba47a 100644
--- a/public/src/modules/helpers.js
+++ b/public/src/modules/helpers.js
@@ -1,16 +1,38 @@
 'use strict';
 
-(function (exports) {
-	/* globals define, utils, config */
-
-	// export the class if we are in a Node-like system.
-	if (typeof module === 'object' && module.exports === exports) {
-		exports = module.exports/* = SemVer*/;
+(function (factory) {
+	if (typeof module === 'object' && module.exports) {
+		var relative_path = require('nconf').get('relative_path');
+		module.exports = factory(require('../utils'), require('templates.js'), require('string'), relative_path);
+	} else if (typeof define === 'function' && define.amd) {
+		define('helpers', ['string'], function (string) {
+			return factory(utils, templates, string, config.relative_path);
+		});
+	} else {
+		window.helpers = factory(utils, templates, window.String, config.relative_path);
 	}
+}(function (utils, templates, S, relative_path) {
+	var helpers = {
+		displayMenuItem: displayMenuItem,
+		buildMetaTag: buildMetaTag,
+		buildLinkTag: buildLinkTag,
+		stringify: stringify,
+		escape: escape,
+		stripTags: stripTags,
+		generateCategoryBackground: generateCategoryBackground,
+		generateChildrenCategories: generateChildrenCategories,
+		generateTopicClass: generateTopicClass,
+		displayUserSearch: displayUserSearch,
+		membershipBtn: membershipBtn,
+		spawnPrivilegeStates: spawnPrivilegeStates,
+		localeToHTML: localeToHTML,
+		renderTopicImage: renderTopicImage,
+		renderDigestAvatar: renderDigestAvatar,
+		userAgentIcons: userAgentIcons,
+		register: register,
+	};
 
-	var helpers = exports;
-
-	helpers.displayMenuItem = function (data, index) {
+	function displayMenuItem(data, index) {
 		var item = data.navigation[index];
 		if (!item) {
 			return false;
@@ -35,17 +57,17 @@
 		}
 
 		return true;
-	};
+	}
 
-	helpers.buildMetaTag = function (tag) {
+	function buildMetaTag(tag) {
 		var name = tag.name ? 'name="' + tag.name + '" ' : '';
 		var property = tag.property ? 'property="' + tag.property + '" ' : '';
 		var content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : '';
 
 		return '<meta ' + name + property + content + '/>\n\t';
-	};
+	}
 
-	helpers.buildLinkTag = function (tag) {
+	function buildLinkTag(tag) {
 		var link = tag.link ? 'link="' + tag.link + '" ' : '';
 		var rel = tag.rel ? 'rel="' + tag.rel + '" ' : '';
 		var type = tag.type ? 'type="' + tag.type + '" ' : '';
@@ -53,29 +75,22 @@
 		var sizes = tag.sizes ? 'sizes="' + tag.sizes + '" ' : '';
 
 		return '<link ' + link + rel + type + sizes + href + '/>\n\t';
-	};
+	}
 
-	helpers.stringify = function (obj) {
+	function stringify(obj) {
 		// Turns the incoming object into a JSON string
 		return JSON.stringify(obj).replace(/&/gm, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;').replace(/"/g, '&quot;');
-	};
+	}
 
-	helpers.escape = function (str) {
-		if (typeof utils !== 'undefined') {
-			return utils.escapeHTML(str);
-		}
-		return require('../utils').escapeHTML(str);
-	};
+	function escape(str) {
+		return utils.escapeHTML(str);
+	}
 
-	helpers.stripTags = function (str) {
-		if (typeof window !== 'undefined' && window.S) {
-			return window.S(String(str)).stripTags().s;
-		}
-		var S = require('string');
+	function stripTags(str) {
 		return S(String(str)).stripTags().s;
-	};
+	}
 
-	helpers.generateCategoryBackground = function (category) {
+	function generateCategoryBackground(category) {
 		if (!category) {
 			return '';
 		}
@@ -97,11 +112,10 @@
 		}
 
 		return style.join('; ') + ';';
-	};
+	}
 
-	helpers.generateChildrenCategories = function (category) {
+	function generateChildrenCategories(category) {
 		var html = '';
-		var relative_path = (typeof config !== 'undefined' ? config.relative_path : require('nconf').get('relative_path'));
 		if (!category || !category.children || !category.children.length) {
 			return html;
 		}
@@ -117,9 +131,9 @@
 		});
 		html = html ? ('<span class="category-children">' + html + '</span>') : html;
 		return html;
-	};
+	}
 
-	helpers.generateTopicClass = function (topic) {
+	function generateTopicClass(topic) {
 		var style = [];
 
 		if (topic.locked) {
@@ -139,14 +153,14 @@
 		}
 
 		return style.join(' ');
-	};
+	}
 
-	helpers.displayUserSearch = function (data, allowGuestUserSearching) {
+	function displayUserSearch(data, allowGuestUserSearching) {
 		return data.loggedIn || allowGuestUserSearching === 'true';
-	};
+	}
 
 	// Groups helpers
-	helpers.membershipBtn = function (groupObj) {
+	function membershipBtn(groupObj) {
 		if (groupObj.isMember && groupObj.name !== 'administrators') {
 			return '<button class="btn btn-danger" data-action="leave" data-group="' + groupObj.displayName + '"><i class="fa fa-times"></i> [[groups:membership.leave-group]]</button>';
 		}
@@ -159,9 +173,9 @@
 			return '<button class="btn btn-success" data-action="join" data-group="' + groupObj.displayName + '"><i class="fa fa-plus"></i> [[groups:membership.join-group]]</button>';
 		}
 		return '';
-	};
+	}
 
-	helpers.spawnPrivilegeStates = function (member, privileges) {
+	function spawnPrivilegeStates(member, privileges) {
 		var states = [];
 		for (var priv in privileges) {
 			if (privileges.hasOwnProperty(priv)) {
@@ -174,21 +188,21 @@
 		return states.map(function (priv) {
 			return '<td class="text-center" data-privilege="' + priv.name + '"><input type="checkbox"' + (priv.state ? ' checked' : '') + (member === 'guests' && priv.name === 'groups:moderate' ? ' disabled="disabled"' : '') + ' /></td>';
 		}).join('');
-	};
+	}
 
-	helpers.localeToHTML = function (locale, fallback) {
+	function localeToHTML(locale, fallback) {
 		locale = locale || fallback || 'en-GB';
 		return locale.replace('_', '-');
-	};
+	}
 
-	helpers.renderTopicImage = function (topicObj) {
+	function renderTopicImage(topicObj) {
 		if (topicObj.thumb) {
 			return '<img src="' + topicObj.thumb + '" class="img-circle user-img" title="' + topicObj.user.username + '" />';
 		}
 		return '<img component="user/picture" data-uid="' + topicObj.user.uid + '" src="' + topicObj.user.picture + '" class="user-img" title="' + topicObj.user.username + '" />';
-	};
+	}
 
-	helpers.renderDigestAvatar = function (block) {
+	function renderDigestAvatar(block) {
 		if (block.teaser) {
 			if (block.teaser.user.picture) {
 				return '<img style="vertical-align: middle; width: 16px; height: 16px; padding-right: 1em;" src="' + block.teaser.user.picture + '" title="' + block.teaser.user.username + '" />';
@@ -199,9 +213,9 @@
 			return '<img style="vertical-align: middle; width: 16px; height: 16px; padding-right: 1em;" src="' + block.user.picture + '" title="' + block.user.username + '" />';
 		}
 		return '<div style="width: 16px; height: 16px; line-height: 16px; font-size: 10px; margin-right: 1em; background-color: ' + block.user['icon:bgColor'] + '; color: white; text-align: center; display: inline-block;">' + block.user['icon:text'] + '</div>';
-	};
+	}
 
-	helpers.userAgentIcons = function (data) {
+	function userAgentIcons(data) {
 		var icons = '';
 
 		switch (data.platform) {
@@ -251,29 +265,13 @@
 		}
 
 		return icons;
-	};
-
-	exports.register = function () {
-		var templates;
-
-		if (typeof module === 'object') {
-			templates = require('templates.js');
-		} else {
-			templates = window.templates;
-		}
+	}
 
+	function register() {
 		Object.keys(helpers).forEach(function (helperName) {
 			templates.registerHelper(helperName, helpers[helperName]);
 		});
-	};
-
-	// export the class if we are in a Node-like system.
-	if (typeof module === 'object' && module.exports === exports) {
-		exports = module.exports/* = SemVer*/;
-	} else if (typeof define === 'function' && define.amd) {
-		// Use the define() function if we're in AMD land
-		define('helpers', exports);
-	} else if (typeof window === 'object') {
-		window.helpers = exports;
 	}
-}(typeof exports === 'object' ? exports : {}));
+
+	return helpers;
+}));
diff --git a/src/meta.js b/src/meta.js
index 59c3447cf4..cb2a381d6d 100644
--- a/src/meta.js
+++ b/src/meta.js
@@ -12,16 +12,16 @@ var Meta = module.exports;
 
 Meta.reloadRequired = false;
 
-require('./meta/configs')(Meta);
-require('./meta/themes')(Meta);
-require('./meta/js')(Meta);
-require('./meta/css')(Meta);
-require('./meta/sounds')(Meta);
-require('./meta/settings')(Meta);
-require('./meta/logs')(Meta);
-require('./meta/errors')(Meta);
-require('./meta/tags')(Meta);
-require('./meta/dependencies')(Meta);
+Meta.configs = require('./meta/configs');
+Meta.themes = require('./meta/themes');
+Meta.js = require('./meta/js');
+Meta.css = require('./meta/css');
+Meta.sounds = require('./meta/sounds');
+Meta.settings = require('./meta/settings');
+Meta.logs = require('./meta/logs');
+Meta.errors = require('./meta/errors');
+Meta.tags = require('./meta/tags');
+Meta.dependencies = require('./meta/dependencies');
 Meta.templates = require('./meta/templates');
 Meta.blacklist = require('./meta/blacklist');
 Meta.languages = require('./meta/languages');
diff --git a/src/meta/configs.js b/src/meta/configs.js
index daaa807d19..925ff61255 100644
--- a/src/meta/configs.js
+++ b/src/meta/configs.js
@@ -6,142 +6,142 @@ var nconf = require('nconf');
 
 var db = require('../database');
 var pubsub = require('../pubsub');
+var Meta = require('../meta');
 var cacheBuster = require('./cacheBuster');
 
-module.exports = function (Meta) {
-	Meta.config = {};
-	Meta.configs = {};
-
-	Meta.configs.init = function (callback) {
-		delete Meta.config;
-
-		async.waterfall([
-			function (next) {
-				Meta.configs.list(next);
-			},
-			function (config, next) {
-				cacheBuster.read(function (err, buster) {
-					if (err) {
-						return next(err);
-					}
-
-					config['cache-buster'] = 'v=' + (buster || Date.now());
-
-					Meta.config = config;
-					next();
-				});
-			},
-		], callback);
-	};
-
-	Meta.configs.list = function (callback) {
-		db.getObject('config', function (err, config) {
-			config = config || {};
-			config.version = nconf.get('version');
-			config.registry = nconf.get('registry');
-			callback(err, config);
-		});
-	};
-
-	Meta.configs.get = function (field, callback) {
-		db.getObjectField('config', field, callback);
-	};
-
-	Meta.configs.getFields = function (fields, callback) {
-		db.getObjectFields('config', fields, callback);
-	};
-
-	Meta.configs.set = function (field, value, callback) {
-		callback = callback || function () {};
-		if (!field) {
-			return callback(new Error('[[error:invalid-data]]'));
-		}
+var Configs = module.exports;
 
-		var data = {};
-		data[field] = value;
-		Meta.configs.setMultiple(data, callback);
-	};
-
-
-	Meta.configs.setMultiple = function (data, callback) {
-		async.waterfall([
-			function (next) {
-				processConfig(data, next);
-			},
-			function (next) {
-				db.setObject('config', data, next);
-			},
-			function (next) {
-				updateConfig(data);
-				setImmediate(next);
-			},
-		], callback);
-	};
+Meta.config = {};
 
-	function processConfig(data, callback) {
-		if (data.customCSS) {
-			return saveRenderedCss(data, callback);
-		}
-		setImmediate(callback);
-	}
+Configs.init = function (callback) {
+	Meta.config = null;
 
-	function saveRenderedCss(data, callback) {
-		var less = require('less');
-		async.waterfall([
-			function (next) {
-				less.render(data.customCSS, {
-					compress: true,
-				}, next);
-			},
-			function (lessObject, next) {
-				data.renderedCustomCSS = lessObject.css;
-				setImmediate(next);
-			},
-		], callback);
-	}
+	async.waterfall([
+		function (next) {
+			Configs.list(next);
+		},
+		function (config, next) {
+			cacheBuster.read(function (err, buster) {
+				if (err) {
+					return next(err);
+				}
 
-	function updateConfig(config) {
-		updateLocalConfig(config);
-		pubsub.publish('config:update', config);
-	}
+				config['cache-buster'] = 'v=' + (buster || Date.now());
 
-	function updateLocalConfig(config) {
-		for (var field in config) {
-			if (config.hasOwnProperty(field)) {
-				Meta.config[field] = config[field];
-			}
-		}
+				Meta.config = config;
+				next();
+			});
+		},
+	], callback);
+};
+
+Configs.list = function (callback) {
+	db.getObject('config', function (err, config) {
+		config = config || {};
+		config.version = nconf.get('version');
+		config.registry = nconf.get('registry');
+		callback(err, config);
+	});
+};
+
+Configs.get = function (field, callback) {
+	db.getObjectField('config', field, callback);
+};
+
+Configs.getFields = function (fields, callback) {
+	db.getObjectFields('config', fields, callback);
+};
+
+Configs.set = function (field, value, callback) {
+	callback = callback || function () {};
+	if (!field) {
+		return callback(new Error('[[error:invalid-data]]'));
 	}
 
-	pubsub.on('config:update', function onConfigReceived(config) {
-		if (typeof config === 'object' && Meta.config) {
-			updateLocalConfig(config);
+	var data = {};
+	data[field] = value;
+	Configs.setMultiple(data, callback);
+};
+
+
+Configs.setMultiple = function (data, callback) {
+	async.waterfall([
+		function (next) {
+			processConfig(data, next);
+		},
+		function (next) {
+			db.setObject('config', data, next);
+		},
+		function (next) {
+			updateConfig(data);
+			setImmediate(next);
+		},
+	], callback);
+};
+
+function processConfig(data, callback) {
+	if (data.customCSS) {
+		return saveRenderedCss(data, callback);
+	}
+	setImmediate(callback);
+}
+
+function saveRenderedCss(data, callback) {
+	var less = require('less');
+	async.waterfall([
+		function (next) {
+			less.render(data.customCSS, {
+				compress: true,
+			}, next);
+		},
+		function (lessObject, next) {
+			data.renderedCustomCSS = lessObject.css;
+			setImmediate(next);
+		},
+	], callback);
+}
+
+function updateConfig(config) {
+	updateLocalConfig(config);
+	pubsub.publish('config:update', config);
+}
+
+function updateLocalConfig(config) {
+	for (var field in config) {
+		if (config.hasOwnProperty(field)) {
+			Meta.config[field] = config[field];
 		}
-	});
+	}
+}
 
-	Meta.configs.setOnEmpty = function (values, callback) {
-		async.waterfall([
-			function (next) {
-				db.getObject('config', next);
-			},
-			function (data, next) {
-				data = data || {};
-				var empty = {};
-				Object.keys(values).forEach(function (key) {
-					if (!data.hasOwnProperty(key)) {
-						empty[key] = values[key];
-					}
-				});
-				if (Object.keys(empty).length) {
-					db.setObject('config', empty, next);
-				} else {
-					setImmediate(next);
+pubsub.on('config:update', function onConfigReceived(config) {
+	if (typeof config === 'object' && Meta.config) {
+		updateLocalConfig(config);
+	}
+});
+
+Configs.setOnEmpty = function (values, callback) {
+	async.waterfall([
+		function (next) {
+			db.getObject('config', next);
+		},
+		function (data, next) {
+			data = data || {};
+			var empty = {};
+			Object.keys(values).forEach(function (key) {
+				if (!data.hasOwnProperty(key)) {
+					empty[key] = values[key];
 				}
-			},
-		], callback);
-	};
+			});
+			if (Object.keys(empty).length) {
+				db.setObject('config', empty, next);
+			} else {
+				setImmediate(next);
+			}
+		},
+	], callback);
+};
 
-	Meta.configs.remove = function (field, callback) {
-		db.deleteObjectField('config', field, callback);
-	};
+Configs.remove = function (field, callback) {
+	db.deleteObjectField('config', field, callback);
 };
diff --git a/src/meta/css.js b/src/meta/css.js
index f2abe608ac..3db85a50f3 100644
--- a/src/meta/css.js
+++ b/src/meta/css.js
@@ -11,158 +11,156 @@ var db = require('../database');
 var file = require('../file');
 var minifier = require('./minifier');
 
-module.exports = function (Meta) {
-	Meta.css = {};
-
-	var buildImports = {
-		client: function (source) {
-			return '@import "./theme";\n' + source + '\n' + [
-				'@import "font-awesome";',
-				'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
-				'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
-				'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
-				'@import (inline) "../node_modules/cropperjs/dist/cropper.css";',
-				'@import "../../public/less/flags.less";',
-				'@import "../../public/less/blacklist.less";',
-				'@import "../../public/less/generics.less";',
-				'@import "../../public/less/mixins.less";',
-				'@import "../../public/less/global.less";',
-			].map(function (str) {
-				return str.replace(/\//g, path.sep);
-			}).join('\n');
-		},
-		admin: function (source) {
-			return source + '\n' + [
-				'@import "font-awesome";',
-				'@import "../public/less/admin/admin";',
-				'@import "../public/less/generics.less";',
-				'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
-				'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
-				'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
-				'@import (inline) "../public/vendor/mdl/material.css";',
-			].map(function (str) {
-				return str.replace(/\//g, path.sep);
-			}).join('\n');
-		},
-	};
+var CSS = module.exports;
+
+var buildImports = {
+	client: function (source) {
+		return '@import "./theme";\n' + source + '\n' + [
+			'@import "font-awesome";',
+			'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
+			'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
+			'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
+			'@import (inline) "../node_modules/cropperjs/dist/cropper.css";',
+			'@import "../../public/less/flags.less";',
+			'@import "../../public/less/blacklist.less";',
+			'@import "../../public/less/generics.less";',
+			'@import "../../public/less/mixins.less";',
+			'@import "../../public/less/global.less";',
+		].map(function (str) {
+			return str.replace(/\//g, path.sep);
+		}).join('\n');
+	},
+	admin: function (source) {
+		return source + '\n' + [
+			'@import "font-awesome";',
+			'@import "../public/less/admin/admin";',
+			'@import "../public/less/generics.less";',
+			'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
+			'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
+			'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
+			'@import (inline) "../public/vendor/mdl/material.css";',
+		].map(function (str) {
+			return str.replace(/\//g, path.sep);
+		}).join('\n');
+	},
+};
 
-	function filterMissingFiles(filepaths, callback) {
-		async.filter(filepaths, function (filepath, next) {
-			file.exists(path.join(__dirname, '../../node_modules', filepath), function (err, exists) {
-				if (!exists) {
-					winston.warn('[meta/css] File not found! ' + filepath);
-				}
+function filterMissingFiles(filepaths, callback) {
+	async.filter(filepaths, function (filepath, next) {
+		file.exists(path.join(__dirname, '../../node_modules', filepath), function (err, exists) {
+			if (!exists) {
+				winston.warn('[meta/css] File not found! ' + filepath);
+			}
+
+			next(err, exists);
+		});
+	}, callback);
+}
+
+function getImports(files, prefix, extension, callback) {
+	var pluginDirectories = [];
+	var source = '';
+
+	files.forEach(function (styleFile) {
+		if (styleFile.endsWith(extension)) {
+			source += prefix + path.sep + styleFile + '";';
+		} else {
+			pluginDirectories.push(styleFile);
+		}
+	});
+
+	async.each(pluginDirectories, function (directory, next) {
+		file.walk(directory, function (err, styleFiles) {
+			if (err) {
+				return next(err);
+			}
 
-				next(err, exists);
+			styleFiles.forEach(function (styleFile) {
+				source += prefix + path.sep + styleFile + '";';
 			});
-		}, callback);
-	}
 
-	function getImports(files, prefix, extension, callback) {
-		var pluginDirectories = [];
-		var source = '';
+			next();
+		});
+	}, function (err) {
+		callback(err, source);
+	});
+}
+
+function getBundleMetadata(target, callback) {
+	var paths = [
+		path.join(__dirname, '../../node_modules'),
+		path.join(__dirname, '../../public/vendor/fontawesome/less'),
+	];
+
+	async.waterfall([
+		function (next) {
+			if (target !== 'client') {
+				return next(null, null);
+			}
 
-		files.forEach(function (styleFile) {
-			if (styleFile.endsWith(extension)) {
-				source += prefix + path.sep + styleFile + '";';
-			} else {
-				pluginDirectories.push(styleFile);
+			db.getObjectFields('config', ['theme:type', 'theme:id'], next);
+		},
+		function (themeData, next) {
+			if (target === 'client') {
+				var themeId = (themeData['theme:id'] || 'nodebb-theme-persona');
+				var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla'));
+				paths.unshift(baseThemePath);
 			}
-		});
 
-		async.each(pluginDirectories, function (directory, next) {
-			file.walk(directory, function (err, styleFiles) {
-				if (err) {
-					return next(err);
-				}
+			async.parallel({
+				less: function (cb) {
+					async.waterfall([
+						function (next) {
+							filterMissingFiles(plugins.lessFiles, next);
+						},
+						function (lessFiles, next) {
+							getImports(lessFiles, '\n@import ".', '.less', next);
+						},
+					], cb);
+				},
+				css: function (cb) {
+					async.waterfall([
+						function (next) {
+							filterMissingFiles(plugins.cssFiles, next);
+						},
+						function (cssFiles, next) {
+							getImports(cssFiles, '\n@import (inline) ".', '.css', next);
+						},
+					], cb);
+				},
+			}, next);
+		},
+		function (result, next) {
+			var cssImports = result.css;
+			var lessImports = result.less;
 
-				styleFiles.forEach(function (styleFile) {
-					source += prefix + path.sep + styleFile + '";';
-				});
+			var imports = cssImports + '\n' + lessImports;
+			imports = buildImports[target](imports);
 
-				next();
-			});
-		}, function (err) {
-			callback(err, source);
-		});
-	}
-
-	function getBundleMetadata(target, callback) {
-		var paths = [
-			path.join(__dirname, '../../node_modules'),
-			path.join(__dirname, '../../public/vendor/fontawesome/less'),
-		];
-
-		async.waterfall([
-			function (next) {
-				if (target !== 'client') {
-					return next(null, null);
-				}
-
-				db.getObjectFields('config', ['theme:type', 'theme:id'], next);
-			},
-			function (themeData, next) {
-				if (target === 'client') {
-					var themeId = (themeData['theme:id'] || 'nodebb-theme-persona');
-					var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla'));
-					paths.unshift(baseThemePath);
-				}
-
-				async.parallel({
-					less: function (cb) {
-						async.waterfall([
-							function (next) {
-								filterMissingFiles(plugins.lessFiles, next);
-							},
-							function (lessFiles, next) {
-								getImports(lessFiles, '\n@import ".', '.less', next);
-							},
-						], cb);
-					},
-					css: function (cb) {
-						async.waterfall([
-							function (next) {
-								filterMissingFiles(plugins.cssFiles, next);
-							},
-							function (cssFiles, next) {
-								getImports(cssFiles, '\n@import (inline) ".', '.css', next);
-							},
-						], cb);
-					},
-				}, next);
-			},
-			function (result, next) {
-				var cssImports = result.css;
-				var lessImports = result.less;
-
-				var imports = cssImports + '\n' + lessImports;
-				imports = buildImports[target](imports);
-
-				next(null, imports);
-			},
-		], function (err, imports) {
-			if (err) {
-				return callback(err);
-			}
+			next(null, imports);
+		},
+	], function (err, imports) {
+		if (err) {
+			return callback(err);
+		}
+
+		callback(null, { paths: paths, imports: imports });
+	});
+}
+
+CSS.buildBundle = function (target, fork, callback) {
+	async.waterfall([
+		function (next) {
+			getBundleMetadata(target, next);
+		},
+		function (data, next) {
+			var minify = global.env !== 'development';
+			minifier.css.bundle(data.imports, data.paths, minify, fork, next);
+		},
+		function (bundle, next) {
+			var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css';
 
-			callback(null, { paths: paths, imports: imports });
-		});
-	}
-
-	Meta.css.buildBundle = function (target, fork, callback) {
-		async.waterfall([
-			function (next) {
-				getBundleMetadata(target, next);
-			},
-			function (data, next) {
-				var minify = global.env !== 'development';
-				minifier.css.bundle(data.imports, data.paths, minify, fork, next);
-			},
-			function (bundle, next) {
-				var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css';
-
-				fs.writeFile(path.join(__dirname, '../../build/public', filename), bundle.code, next);
-			},
-		], callback);
-	};
+			fs.writeFile(path.join(__dirname, '../../build/public', filename), bundle.code, next);
+		},
+	], callback);
 };
diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js
index 3a16f0e9e5..db403732ec 100644
--- a/src/meta/dependencies.js
+++ b/src/meta/dependencies.js
@@ -9,73 +9,72 @@ require('colors');
 
 var pkg = require('../../package.json');
 
-module.exports = function (Meta) {
-	Meta.dependencies = {};
-	var depsMissing = false;
-	var depsOutdated = false;
+var Dependencies = module.exports;
 
-	Meta.dependencies.check = function (callback) {
-		var modules = Object.keys(pkg.dependencies);
+var depsMissing = false;
+var depsOutdated = false;
 
-		winston.verbose('Checking dependencies for outdated modules');
+Dependencies.check = function (callback) {
+	var modules = Object.keys(pkg.dependencies);
 
-		async.each(modules, Meta.dependencies.checkModule, function (err) {
-			if (err) {
-				return callback(err);
-			}
+	winston.verbose('Checking dependencies for outdated modules');
 
-			if (depsMissing) {
-				callback(new Error('dependencies-missing'));
-			} else if (depsOutdated) {
-				callback(global.env !== 'development' ? new Error('dependencies-out-of-date') : null);
-			} else {
-				callback(null);
-			}
-		});
-	};
+	async.each(modules, Dependencies.checkModule, function (err) {
+		if (err) {
+			return callback(err);
+		}
+
+		if (depsMissing) {
+			callback(new Error('dependencies-missing'));
+		} else if (depsOutdated) {
+			callback(global.env !== 'development' ? new Error('dependencies-out-of-date') : null);
+		} else {
+			callback(null);
+		}
+	});
+};
 
-	Meta.dependencies.checkModule = function (moduleName, callback) {
-		fs.readFile(path.join(__dirname, '../../node_modules/', moduleName, 'package.json'), {
-			encoding: 'utf-8',
-		}, function (err, pkgData) {
-			if (err) {
-				// If a bundled plugin/theme is not present, skip the dep check (#3384)
-				if (err.code === 'ENOENT' && (moduleName === 'nodebb-rewards-essentials' || moduleName.startsWith('nodebb-plugin') || moduleName.startsWith('nodebb-theme'))) {
-					winston.warn('[meta/dependencies] Bundled plugin ' + moduleName + ' not found, skipping dependency check.');
-					return callback(null, true);
-				}
-				return callback(err);
+Dependencies.checkModule = function (moduleName, callback) {
+	fs.readFile(path.join(__dirname, '../../node_modules/', moduleName, 'package.json'), {
+		encoding: 'utf-8',
+	}, function (err, pkgData) {
+		if (err) {
+			// If a bundled plugin/theme is not present, skip the dep check (#3384)
+			if (err.code === 'ENOENT' && (moduleName === 'nodebb-rewards-essentials' || moduleName.startsWith('nodebb-plugin') || moduleName.startsWith('nodebb-theme'))) {
+				winston.warn('[meta/dependencies] Bundled plugin ' + moduleName + ' not found, skipping dependency check.');
+				return callback(null, true);
 			}
+			return callback(err);
+		}
 
-			pkgData = Meta.dependencies.parseModuleData(moduleName, pkgData);
+		pkgData = Dependencies.parseModuleData(moduleName, pkgData);
 
-			var satisfies = Meta.dependencies.doesSatisfy(pkgData, pkg.dependencies[moduleName]);
-			callback(null, satisfies);
-		});
-	};
+		var satisfies = Dependencies.doesSatisfy(pkgData, pkg.dependencies[moduleName]);
+		callback(null, satisfies);
+	});
+};
 
-	Meta.dependencies.parseModuleData = function (moduleName, pkgData) {
-		try {
-			pkgData = JSON.parse(pkgData);
-		} catch (e) {
-			winston.warn('[' + 'missing'.red + '] ' + moduleName.bold + ' is a required dependency but could not be found\n');
-			depsMissing = true;
-			return null;
-		}
-		return pkgData;
-	};
+Dependencies.parseModuleData = function (moduleName, pkgData) {
+	try {
+		pkgData = JSON.parse(pkgData);
+	} catch (e) {
+		winston.warn('[' + 'missing'.red + '] ' + moduleName.bold + ' is a required dependency but could not be found\n');
+		depsMissing = true;
+		return null;
+	}
+	return pkgData;
+};
 
-	Meta.dependencies.doesSatisfy = function (moduleData, packageJSONVersion) {
-		if (!moduleData) {
-			return false;
-		}
-		var versionOk = !semver.validRange(packageJSONVersion) || semver.satisfies(moduleData.version, packageJSONVersion);
-		var githubRepo = moduleData._resolved && moduleData._resolved.indexOf('//github.com') !== -1;
-		var satisfies = versionOk || githubRepo;
-		if (!satisfies) {
-			winston.warn('[' + 'outdated'.yellow + '] ' + moduleData.name.bold + ' installed v' + moduleData.version + ', package.json requires ' + packageJSONVersion + '\n');
-			depsOutdated = true;
-		}
-		return satisfies;
-	};
+Dependencies.doesSatisfy = function (moduleData, packageJSONVersion) {
+	if (!moduleData) {
+		return false;
+	}
+	var versionOk = !semver.validRange(packageJSONVersion) || semver.satisfies(moduleData.version, packageJSONVersion);
+	var githubRepo = moduleData._resolved && moduleData._resolved.indexOf('//github.com') !== -1;
+	var satisfies = versionOk || githubRepo;
+	if (!satisfies) {
+		winston.warn('[' + 'outdated'.yellow + '] ' + moduleData.name.bold + ' installed v' + moduleData.version + ', package.json requires ' + packageJSONVersion + '\n');
+		depsOutdated = true;
+	}
+	return satisfies;
 };
diff --git a/src/meta/errors.js b/src/meta/errors.js
index f269490bbf..38e206e501 100644
--- a/src/meta/errors.js
+++ b/src/meta/errors.js
@@ -8,61 +8,59 @@ var cronJob = require('cron').CronJob;
 var db = require('../database');
 var analytics = require('../analytics');
 
-module.exports = function (Meta) {
-	Meta.errors = {};
+var Errors = module.exports;
 
-	var counters = {};
+var counters = {};
 
-	new cronJob('0 * * * * *', function () {
-		Meta.errors.writeData();
-	}, null, true);
+new cronJob('0 * * * * *', function () {
+	Errors.writeData();
+}, null, true);
 
-	Meta.errors.writeData = function () {
-		var dbQueue = [];
-		if (Object.keys(counters).length > 0) {
-			for (var key in counters) {
-				if (counters.hasOwnProperty(key)) {
-					dbQueue.push(async.apply(db.sortedSetIncrBy, 'errors:404', counters[key], key));
-				}
+Errors.writeData = function () {
+	var dbQueue = [];
+	if (Object.keys(counters).length > 0) {
+		for (var key in counters) {
+			if (counters.hasOwnProperty(key)) {
+				dbQueue.push(async.apply(db.sortedSetIncrBy, 'errors:404', counters[key], key));
 			}
-			counters = {};
-			async.series(dbQueue, function (err) {
-				if (err) {
-					winston.error(err);
-				}
-			});
 		}
-	};
+		counters = {};
+		async.series(dbQueue, function (err) {
+			if (err) {
+				winston.error(err);
+			}
+		});
+	}
+};
 
-	Meta.errors.log404 = function (route, callback) {
-		callback = callback || function () {};
-		if (!route) {
-			return setImmediate(callback);
-		}
-		route = route.replace(/\/$/, '');	// remove trailing slashes
-		analytics.increment('errors:404');
-		counters[route] = counters[route] || 0;
-		counters[route] += 1;
-		setImmediate(callback);
-	};
+Errors.log404 = function (route, callback) {
+	callback = callback || function () {};
+	if (!route) {
+		return setImmediate(callback);
+	}
+	route = route.replace(/\/$/, '');	// remove trailing slashes
+	analytics.increment('errors:404');
+	counters[route] = counters[route] || 0;
+	counters[route] += 1;
+	setImmediate(callback);
+};
 
-	Meta.errors.get = function (escape, callback) {
-		async.waterfall([
-			function (next) {
-				db.getSortedSetRevRangeWithScores('errors:404', 0, 199, next);
-			},
-			function (data, next) {
-				data = data.map(function (nfObject) {
-					nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value;
-					return nfObject;
-				});
+Errors.get = function (escape, callback) {
+	async.waterfall([
+		function (next) {
+			db.getSortedSetRevRangeWithScores('errors:404', 0, 199, next);
+		},
+		function (data, next) {
+			data = data.map(function (nfObject) {
+				nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value;
+				return nfObject;
+			});
 
-				next(null, data);
-			},
-		], callback);
-	};
+			next(null, data);
+		},
+	], callback);
+};
 
-	Meta.errors.clear = function (callback) {
-		db.delete('errors:404', callback);
-	};
+Errors.clear = function (callback) {
+	db.delete('errors:404', callback);
 };
diff --git a/src/meta/js.js b/src/meta/js.js
index b239ee21fe..8f483779da 100644
--- a/src/meta/js.js
+++ b/src/meta/js.js
@@ -10,341 +10,339 @@ var file = require('../file');
 var plugins = require('../plugins');
 var minifier = require('./minifier');
 
-module.exports = function (Meta) {
-	Meta.js = {};
-
-	Meta.js.scripts = {
-		base: [
-			'node_modules/jquery/dist/jquery.js',
-			'node_modules/socket.io-client/dist/socket.io.js',
-			'public/vendor/jquery/timeago/jquery.timeago.js',
-			'public/vendor/jquery/js/jquery.form.min.js',
-			'public/vendor/visibility/visibility.min.js',
-			'node_modules/bootstrap/dist/js/bootstrap.js',
-			'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js',
-			'public/vendor/jquery/textcomplete/jquery.textcomplete.js',
-			'public/vendor/requirejs/require.js',
-			'public/src/require-config.js',
-			'public/vendor/bootbox/bootbox.js',
-			'public/vendor/bootbox/wrapper.js',
-			'public/vendor/tinycon/tinycon.js',
-			'public/vendor/xregexp/xregexp.js',
-			'public/vendor/xregexp/unicode/unicode-base.js',
-			'node_modules/templates.js/lib/templates.js',
-			'public/src/utils.js',
-			'public/src/sockets.js',
-			'public/src/app.js',
-			'public/src/ajaxify.js',
-			'public/src/overrides.js',
-			'public/src/widgets.js',
-			'node_modules/promise-polyfill/promise.js',
-		],
-
-		// files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load
-		rjs: [
-			'public/src/client/footer.js',
-			'public/src/client/chats.js',
-			'public/src/client/infinitescroll.js',
-			'public/src/client/pagination.js',
-			'public/src/client/recent.js',
-			'public/src/client/unread.js',
-			'public/src/client/topic.js',
-			'public/src/client/topic/events.js',
-			'public/src/client/topic/fork.js',
-			'public/src/client/topic/move.js',
-			'public/src/client/topic/posts.js',
-			'public/src/client/topic/images.js',
-			'public/src/client/topic/postTools.js',
-			'public/src/client/topic/threadTools.js',
-			'public/src/client/categories.js',
-			'public/src/client/category.js',
-			'public/src/client/category/tools.js',
-
-			'public/src/modules/translator.js',
-			'public/src/modules/notifications.js',
-			'public/src/modules/chat.js',
-			'public/src/modules/components.js',
-			'public/src/modules/sort.js',
-			'public/src/modules/navigator.js',
-			'public/src/modules/topicSelect.js',
-			'public/src/modules/categorySelector.js',
-			'public/src/modules/share.js',
-			'public/src/modules/search.js',
-			'public/src/modules/alerts.js',
-			'public/src/modules/taskbar.js',
-			'public/src/modules/helpers.js',
-			'public/src/modules/string.js',
-			'public/src/modules/flags.js',
-			'public/src/modules/storage.js',
-		],
-
-		// modules listed below are built (/src/modules) so they can be defined anonymously
-		modules: {
-			'Chart.js': 'node_modules/chart.js/dist/Chart.min.js',
-			'mousetrap.js': 'node_modules/mousetrap/mousetrap.min.js',
-			'cropper.js': 'node_modules/cropperjs/dist/cropper.min.js',
-			'jqueryui.js': 'public/vendor/jquery/js/jquery-ui.js',
-			'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js',
-			ace: 'node_modules/ace-builds/src-min',
-		},
-	};
-
-	var basePath = path.resolve(__dirname, '../..');
-
-	function minifyModules(modules, fork, callback) {
-		var moduleDirs = modules.reduce(function (prev, mod) {
-			var dir = path.resolve(path.dirname(mod.destPath));
-			if (prev.indexOf(dir) === -1) {
-				prev.push(dir);
-			}
-			return prev;
-		}, []);
-
-		async.eachLimit(moduleDirs, 1000, mkdirp, function (err) {
-			if (err) {
-				return callback(err);
-			}
-
-			var filtered = modules.reduce(function (prev, mod) {
-				if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) {
-					prev.skip.push(mod);
-				} else {
-					prev.minify.push(mod);
-				}
-
-				return prev;
-			}, { minify: [], skip: [] });
-
-			async.parallel([
-				function (cb) {
-					minifier.js.minifyBatch(filtered.minify, fork, cb);
-				},
-				function (cb) {
-					async.eachLimit(filtered.skip, 500, function (mod, next) {
-						file.link(mod.srcPath, mod.destPath, next);
-					}, cb);
-				},
-			], callback);
-		});
-	}
+var JS = module.exports;
+
+JS.scripts = {
+	base: [
+		'node_modules/jquery/dist/jquery.js',
+		'node_modules/socket.io-client/dist/socket.io.js',
+		'public/vendor/jquery/timeago/jquery.timeago.js',
+		'public/vendor/jquery/js/jquery.form.min.js',
+		'public/vendor/visibility/visibility.min.js',
+		'node_modules/bootstrap/dist/js/bootstrap.js',
+		'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js',
+		'public/vendor/jquery/textcomplete/jquery.textcomplete.js',
+		'public/vendor/requirejs/require.js',
+		'public/src/require-config.js',
+		'public/vendor/bootbox/bootbox.js',
+		'public/vendor/bootbox/wrapper.js',
+		'public/vendor/tinycon/tinycon.js',
+		'public/vendor/xregexp/xregexp.js',
+		'public/vendor/xregexp/unicode/unicode-base.js',
+		'node_modules/templates.js/lib/templates.js',
+		'public/src/utils.js',
+		'public/src/sockets.js',
+		'public/src/app.js',
+		'public/src/ajaxify.js',
+		'public/src/overrides.js',
+		'public/src/widgets.js',
+		'node_modules/promise-polyfill/promise.js',
+	],
+
+	// files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load
+	rjs: [
+		'public/src/client/footer.js',
+		'public/src/client/chats.js',
+		'public/src/client/infinitescroll.js',
+		'public/src/client/pagination.js',
+		'public/src/client/recent.js',
+		'public/src/client/unread.js',
+		'public/src/client/topic.js',
+		'public/src/client/topic/events.js',
+		'public/src/client/topic/fork.js',
+		'public/src/client/topic/move.js',
+		'public/src/client/topic/posts.js',
+		'public/src/client/topic/images.js',
+		'public/src/client/topic/postTools.js',
+		'public/src/client/topic/threadTools.js',
+		'public/src/client/categories.js',
+		'public/src/client/category.js',
+		'public/src/client/category/tools.js',
+
+		'public/src/modules/translator.js',
+		'public/src/modules/notifications.js',
+		'public/src/modules/chat.js',
+		'public/src/modules/components.js',
+		'public/src/modules/sort.js',
+		'public/src/modules/navigator.js',
+		'public/src/modules/topicSelect.js',
+		'public/src/modules/categorySelector.js',
+		'public/src/modules/share.js',
+		'public/src/modules/search.js',
+		'public/src/modules/alerts.js',
+		'public/src/modules/taskbar.js',
+		'public/src/modules/helpers.js',
+		'public/src/modules/string.js',
+		'public/src/modules/flags.js',
+		'public/src/modules/storage.js',
+	],
+
+	// modules listed below are built (/src/modules) so they can be defined anonymously
+	modules: {
+		'Chart.js': 'node_modules/chart.js/dist/Chart.min.js',
+		'mousetrap.js': 'node_modules/mousetrap/mousetrap.min.js',
+		'cropper.js': 'node_modules/cropperjs/dist/cropper.min.js',
+		'jqueryui.js': 'public/vendor/jquery/js/jquery-ui.js',
+		'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js',
+		ace: 'node_modules/ace-builds/src-min',
+	},
+};
 
-	function linkModules(callback) {
-		var modules = Meta.js.scripts.modules;
-
-		async.eachLimit(Object.keys(modules), 1000, function (relPath, next) {
-			var srcPath = path.join(__dirname, '../../', modules[relPath]);
-			var destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
-
-			async.parallel({
-				dir: function (cb) {
-					mkdirp(path.dirname(destPath), function (err) {
-						cb(err);
-					});
-				},
-				stats: function (cb) {
-					fs.stat(srcPath, cb);
-				},
-			}, function (err, res) {
-				if (err) {
-					return next(err);
-				}
-				if (res.stats.isDirectory()) {
-					return file.linkDirs(srcPath, destPath, next);
-				}
+var basePath = path.resolve(__dirname, '../..');
 
-				if (process.platform === 'win32') {
-					fs.readFile(srcPath, function (err, file) {
-						if (err) {
-							return next(err);
-						}
+function minifyModules(modules, fork, callback) {
+	var moduleDirs = modules.reduce(function (prev, mod) {
+		var dir = path.resolve(path.dirname(mod.destPath));
+		if (prev.indexOf(dir) === -1) {
+			prev.push(dir);
+		}
+		return prev;
+	}, []);
 
-						fs.writeFile(destPath, file, next);
-					});
-				} else {
-					file.link(srcPath, destPath, next);
-				}
-			});
-		}, callback);
-	}
+	async.eachLimit(moduleDirs, 1000, mkdirp, function (err) {
+		if (err) {
+			return callback(err);
+		}
 
-	var moduleDirs = ['modules', 'admin', 'client'];
+		var filtered = modules.reduce(function (prev, mod) {
+			if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) {
+				prev.skip.push(mod);
+			} else {
+				prev.minify.push(mod);
+			}
 
-	function getModuleList(callback) {
-		var modules = Object.keys(Meta.js.scripts.modules).map(function (relPath) {
-			return {
-				srcPath: path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]),
-				destPath: path.join(__dirname, '../../build/public/src/modules', relPath),
-			};
-		});
+			return prev;
+		}, { minify: [], skip: [] });
 
-		var coreDirs = moduleDirs.map(function (dir) {
-			return {
-				srcPath: path.join(__dirname, '../../public/src', dir),
-				destPath: path.join(__dirname, '../../build/public/src', dir),
-			};
-		});
+		async.parallel([
+			function (cb) {
+				minifier.js.minifyBatch(filtered.minify, fork, cb);
+			},
+			function (cb) {
+				async.eachLimit(filtered.skip, 500, function (mod, next) {
+					file.link(mod.srcPath, mod.destPath, next);
+				}, cb);
+			},
+		], callback);
+	});
+}
 
-		modules = modules.concat(coreDirs);
+function linkModules(callback) {
+	var modules = JS.scripts.modules;
 
-		var moduleFiles = [];
-		async.eachLimit(modules, 1000, function (module, next) {
-			var srcPath = module.srcPath;
-			var destPath = module.destPath;
+	async.eachLimit(Object.keys(modules), 1000, function (relPath, next) {
+		var srcPath = path.join(__dirname, '../../', modules[relPath]);
+		var destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
 
-			fs.stat(srcPath, function (err, stats) {
-				if (err) {
-					return next(err);
-				}
-				if (!stats.isDirectory()) {
-					moduleFiles.push(module);
-					return next();
-				}
+		async.parallel({
+			dir: function (cb) {
+				mkdirp(path.dirname(destPath), function (err) {
+					cb(err);
+				});
+			},
+			stats: function (cb) {
+				fs.stat(srcPath, cb);
+			},
+		}, function (err, res) {
+			if (err) {
+				return next(err);
+			}
+			if (res.stats.isDirectory()) {
+				return file.linkDirs(srcPath, destPath, next);
+			}
 
-				file.walk(srcPath, function (err, files) {
+			if (process.platform === 'win32') {
+				fs.readFile(srcPath, function (err, file) {
 					if (err) {
 						return next(err);
 					}
 
-					var mods = files.filter(function (filePath) {
-						return path.extname(filePath) === '.js';
-					}).map(function (filePath) {
-						return {
-							srcPath: path.normalize(filePath),
-							destPath: path.join(destPath, path.relative(srcPath, filePath)),
-						};
-					});
-
-					moduleFiles = moduleFiles.concat(mods).map(function (mod) {
-						mod.filename = path.relative(basePath, mod.srcPath).replace(/\\/g, '/');
-						return mod;
-					});
-
-					next();
+					fs.writeFile(destPath, file, next);
 				});
-			});
-		}, function (err) {
-			callback(err, moduleFiles);
+			} else {
+				file.link(srcPath, destPath, next);
+			}
 		});
-	}
+	}, callback);
+}
 
-	function clearModules(callback) {
-		var builtPaths = moduleDirs.map(function (p) {
-			return path.join(__dirname, '../../build/public/src', p);
-		});
-		async.each(builtPaths, function (builtPath, next) {
-			rimraf(builtPath, next);
-		}, function (err) {
-			callback(err);
-		});
-	}
+var moduleDirs = ['modules', 'admin', 'client'];
 
-	Meta.js.buildModules = function (fork, callback) {
-		async.waterfall([
-			clearModules,
-			function (next) {
-				if (global.env === 'development') {
-					return linkModules(callback);
-				}
+function getModuleList(callback) {
+	var modules = Object.keys(JS.scripts.modules).map(function (relPath) {
+		return {
+			srcPath: path.join(__dirname, '../../', JS.scripts.modules[relPath]),
+			destPath: path.join(__dirname, '../../build/public/src/modules', relPath),
+		};
+	});
 
-				getModuleList(next);
-			},
-			function (modules, next) {
-				minifyModules(modules, fork, next);
-			},
-		], callback);
-	};
+	var coreDirs = moduleDirs.map(function (dir) {
+		return {
+			srcPath: path.join(__dirname, '../../public/src', dir),
+			destPath: path.join(__dirname, '../../build/public/src', dir),
+		};
+	});
+
+	modules = modules.concat(coreDirs);
+
+	var moduleFiles = [];
+	async.eachLimit(modules, 1000, function (module, next) {
+		var srcPath = module.srcPath;
+		var destPath = module.destPath;
 
-	Meta.js.linkStatics = function (callback) {
-		rimraf(path.join(__dirname, '../../build/public/plugins'), function (err) {
+		fs.stat(srcPath, function (err, stats) {
 			if (err) {
-				return callback(err);
+				return next(err);
+			}
+			if (!stats.isDirectory()) {
+				moduleFiles.push(module);
+				return next();
 			}
-			async.eachLimit(Object.keys(plugins.staticDirs), 1000, function (mappedPath, next) {
-				var sourceDir = plugins.staticDirs[mappedPath];
-				var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);
 
-				mkdirp(path.dirname(destDir), function (err) {
-					if (err) {
-						return next(err);
-					}
+			file.walk(srcPath, function (err, files) {
+				if (err) {
+					return next(err);
+				}
 
-					file.linkDirs(sourceDir, destDir, next);
+				var mods = files.filter(function (filePath) {
+					return path.extname(filePath) === '.js';
+				}).map(function (filePath) {
+					return {
+						srcPath: path.normalize(filePath),
+						destPath: path.join(destPath, path.relative(srcPath, filePath)),
+					};
 				});
-			}, callback);
-		});
-	};
 
-	function getBundleScriptList(target, callback) {
-		var pluginDirectories = [];
+				moduleFiles = moduleFiles.concat(mods).map(function (mod) {
+					mod.filename = path.relative(basePath, mod.srcPath).replace(/\\/g, '/');
+					return mod;
+				});
 
-		if (target === 'admin') {
-			target = 'acp';
-		}
-		var pluginScripts = plugins[target + 'Scripts'].filter(function (path) {
-			if (path.endsWith('.js')) {
-				return true;
+				next();
+			});
+		});
+	}, function (err) {
+		callback(err, moduleFiles);
+	});
+}
+
+function clearModules(callback) {
+	var builtPaths = moduleDirs.map(function (p) {
+		return path.join(__dirname, '../../build/public/src', p);
+	});
+	async.each(builtPaths, function (builtPath, next) {
+		rimraf(builtPath, next);
+	}, function (err) {
+		callback(err);
+	});
+}
+
+JS.buildModules = function (fork, callback) {
+	async.waterfall([
+		clearModules,
+		function (next) {
+			if (global.env === 'development') {
+				return linkModules(callback);
 			}
 
-			pluginDirectories.push(path);
-			return false;
-		});
+			getModuleList(next);
+		},
+		function (modules, next) {
+			minifyModules(modules, fork, next);
+		},
+	], callback);
+};
+
+JS.linkStatics = function (callback) {
+	rimraf(path.join(__dirname, '../../build/public/plugins'), function (err) {
+		if (err) {
+			return callback(err);
+		}
+		async.eachLimit(Object.keys(plugins.staticDirs), 1000, function (mappedPath, next) {
+			var sourceDir = plugins.staticDirs[mappedPath];
+			var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);
 
-		async.each(pluginDirectories, function (directory, next) {
-			file.walk(directory, function (err, scripts) {
+			mkdirp(path.dirname(destDir), function (err) {
 				if (err) {
 					return next(err);
 				}
 
-				pluginScripts = pluginScripts.concat(scripts);
-				next();
+				file.linkDirs(sourceDir, destDir, next);
 			});
-		}, function (err) {
+		}, callback);
+	});
+};
+
+function getBundleScriptList(target, callback) {
+	var pluginDirectories = [];
+
+	if (target === 'admin') {
+		target = 'acp';
+	}
+	var pluginScripts = plugins[target + 'Scripts'].filter(function (path) {
+		if (path.endsWith('.js')) {
+			return true;
+		}
+
+		pluginDirectories.push(path);
+		return false;
+	});
+
+	async.each(pluginDirectories, function (directory, next) {
+		file.walk(directory, function (err, scripts) {
 			if (err) {
-				return callback(err);
+				return next(err);
 			}
 
-			var scripts = Meta.js.scripts.base.concat(pluginScripts);
+			pluginScripts = pluginScripts.concat(scripts);
+			next();
+		});
+	}, function (err) {
+		if (err) {
+			return callback(err);
+		}
 
-			if (target === 'client' && global.env !== 'development') {
-				scripts = scripts.concat(Meta.js.scripts.rjs);
-			}
+		var scripts = JS.scripts.base.concat(pluginScripts);
 
-			scripts = scripts.map(function (script) {
-				var srcPath = path.resolve(basePath, script).replace(/\\/g, '/');
-				return {
-					srcPath: srcPath,
-					filename: path.relative(basePath, srcPath).replace(/\\/g, '/'),
-				};
-			});
+		if (target === 'client' && global.env !== 'development') {
+			scripts = scripts.concat(JS.scripts.rjs);
+		}
 
-			callback(null, scripts);
+		scripts = scripts.map(function (script) {
+			var srcPath = path.resolve(basePath, script).replace(/\\/g, '/');
+			return {
+				srcPath: srcPath,
+				filename: path.relative(basePath, srcPath).replace(/\\/g, '/'),
+			};
 		});
-	}
 
-	Meta.js.buildBundle = function (target, fork, callback) {
-		var fileNames = {
-			client: 'nodebb.min.js',
-			admin: 'acp.min.js',
-		};
+		callback(null, scripts);
+	});
+}
 
-		async.waterfall([
-			function (next) {
-				getBundleScriptList(target, next);
-			},
-			function (files, next) {
-				var minify = global.env !== 'development';
-				var filePath = path.join(__dirname, '../../build/public', fileNames[target]);
-
-				minifier.js.bundle({
-					files: files,
-					filename: fileNames[target],
-					destPath: filePath,
-				}, minify, fork, next);
-			},
-		], callback);
+JS.buildBundle = function (target, fork, callback) {
+	var fileNames = {
+		client: 'nodebb.min.js',
+		admin: 'acp.min.js',
 	};
 
-	Meta.js.killMinifier = function () {
-		minifier.killAll();
-	};
+	async.waterfall([
+		function (next) {
+			getBundleScriptList(target, next);
+		},
+		function (files, next) {
+			var minify = global.env !== 'development';
+			var filePath = path.join(__dirname, '../../build/public', fileNames[target]);
+
+			minifier.js.bundle({
+				files: files,
+				filename: fileNames[target],
+				destPath: filePath,
+			}, minify, fork, next);
+		},
+	], callback);
+};
+
+JS.killMinifier = function () {
+	minifier.killAll();
 };
diff --git a/src/meta/logs.js b/src/meta/logs.js
index 30dd983a36..1f950d6057 100644
--- a/src/meta/logs.js
+++ b/src/meta/logs.js
@@ -3,18 +3,16 @@
 var path = require('path');
 var fs = require('fs');
 
-module.exports = function (Meta) {
-	Meta.logs = {
-		path: path.join(__dirname, '..', '..', 'logs', 'output.log'),
-	};
+var Logs = module.exports;
 
-	Meta.logs.get = function (callback) {
-		fs.readFile(Meta.logs.path, {
-			encoding: 'utf-8',
-		}, callback);
-	};
+Logs.path = path.join(__dirname, '..', '..', 'logs', 'output.log');
 
-	Meta.logs.clear = function (callback) {
-		fs.truncate(Meta.logs.path, 0, callback);
-	};
+Logs.get = function (callback) {
+	fs.readFile(Logs.path, {
+		encoding: 'utf-8',
+	}, callback);
+};
+
+Logs.clear = function (callback) {
+	fs.truncate(Logs.path, 0, callback);
 };
diff --git a/src/meta/settings.js b/src/meta/settings.js
index a1d13b248d..18eae100c3 100644
--- a/src/meta/settings.js
+++ b/src/meta/settings.js
@@ -4,61 +4,60 @@ var async = require('async');
 
 var db = require('../database');
 var plugins = require('../plugins');
+var Meta = require('../meta');
 
-module.exports = function (Meta) {
-	Meta.settings = {};
+var Settings = module.exports;
 
-	Meta.settings.get = function (hash, callback) {
-		db.getObject('settings:' + hash, function (err, settings) {
-			callback(err, settings || {});
-		});
-	};
-
-	Meta.settings.getOne = function (hash, field, callback) {
-		db.getObjectField('settings:' + hash, field, callback);
-	};
-
-	Meta.settings.set = function (hash, values, callback) {
-		async.waterfall([
-			function (next) {
-				db.setObject('settings:' + hash, values, next);
-			},
-			function (next) {
-				plugins.fireHook('action:settings.set', {
-					plugin: hash,
-					settings: values,
-				});
+Settings.get = function (hash, callback) {
+	db.getObject('settings:' + hash, function (err, settings) {
+		callback(err, settings || {});
+	});
+};
 
-				Meta.reloadRequired = true;
-				next();
-			},
-		], callback);
-	};
+Settings.getOne = function (hash, field, callback) {
+	db.getObjectField('settings:' + hash, field, callback);
+};
 
-	Meta.settings.setOne = function (hash, field, value, callback) {
-		db.setObjectField('settings:' + hash, field, value, callback);
-	};
+Settings.set = function (hash, values, callback) {
+	async.waterfall([
+		function (next) {
+			db.setObject('settings:' + hash, values, next);
+		},
+		function (next) {
+			plugins.fireHook('action:settings.set', {
+				plugin: hash,
+				settings: values,
+			});
+
+			Meta.reloadRequired = true;
+			next();
+		},
+	], callback);
+};
 
-	Meta.settings.setOnEmpty = function (hash, values, callback) {
-		async.waterfall([
-			function (next) {
-				db.getObject('settings:' + hash, next);
-			},
-			function (settings, next) {
-				settings = settings || {};
-				var empty = {};
-				Object.keys(values).forEach(function (key) {
-					if (!settings.hasOwnProperty(key)) {
-						empty[key] = values[key];
-					}
-				});
+Settings.setOne = function (hash, field, value, callback) {
+	db.setObjectField('settings:' + hash, field, value, callback);
+};
 
-				if (Object.keys(empty).length) {
-					db.setObject('settings:' + hash, empty, next);
-				} else {
-					next();
+Settings.setOnEmpty = function (hash, values, callback) {
+	async.waterfall([
+		function (next) {
+			db.getObject('settings:' + hash, next);
+		},
+		function (settings, next) {
+			settings = settings || {};
+			var empty = {};
+			Object.keys(values).forEach(function (key) {
+				if (!settings.hasOwnProperty(key)) {
+					empty[key] = values[key];
 				}
-			},
-		], callback);
-	};
+			});
+
+			if (Object.keys(empty).length) {
+				db.setObject('settings:' + hash, empty, next);
+			} else {
+				next();
+			}
+		},
+	], callback);
 };
diff --git a/src/meta/sounds.js b/src/meta/sounds.js
index e7bb7c3599..ec89b471da 100644
--- a/src/meta/sounds.js
+++ b/src/meta/sounds.js
@@ -9,125 +9,124 @@ var async = require('async');
 var file = require('../file');
 var plugins = require('../plugins');
 var user = require('../user');
+var Meta = require('../meta');
 
 var soundsPath = path.join(__dirname, '../../build/public/sounds');
 var uploadsPath = path.join(__dirname, '../../public/uploads/sounds');
 
-module.exports = function (Meta) {
-	Meta.sounds = {};
+var Sounds = module.exports;
 
-	Meta.sounds.addUploads = function addUploads(callback) {
-		fs.readdir(uploadsPath, function (err, files) {
-			if (err) {
-				if (err.code !== 'ENOENT') {
-					return callback(err);
-				}
-
-				files = [];
+Sounds.addUploads = function addUploads(callback) {
+	fs.readdir(uploadsPath, function (err, files) {
+		if (err) {
+			if (err.code !== 'ENOENT') {
+				return callback(err);
 			}
 
-			var uploadSounds = files.reduce(function (prev, fileName) {
-				var name = fileName.split('.');
-				if (!name.length || !name[0].length) {
-					return prev;
-				}
-				name = name[0];
-				name = name[0].toUpperCase() + name.slice(1);
+			files = [];
+		}
 
-				prev[name] = fileName;
+		var uploadSounds = files.reduce(function (prev, fileName) {
+			var name = fileName.split('.');
+			if (!name.length || !name[0].length) {
 				return prev;
-			}, {});
-
-			plugins.soundpacks = plugins.soundpacks.filter(function (pack) {
-				return pack.name !== 'Uploads';
-			});
-			if (Object.keys(uploadSounds).length) {
-				plugins.soundpacks.push({
-					name: 'Uploads',
-					id: 'uploads',
-					dir: uploadsPath,
-					sounds: uploadSounds,
-				});
 			}
+			name = name[0];
+			name = name[0].toUpperCase() + name.slice(1);
+
+			prev[name] = fileName;
+			return prev;
+		}, {});
 
-			callback();
+		plugins.soundpacks = plugins.soundpacks.filter(function (pack) {
+			return pack.name !== 'Uploads';
 		});
-	};
+		if (Object.keys(uploadSounds).length) {
+			plugins.soundpacks.push({
+				name: 'Uploads',
+				id: 'uploads',
+				dir: uploadsPath,
+				sounds: uploadSounds,
+			});
+		}
 
-	Meta.sounds.build = function build(callback) {
-		Meta.sounds.addUploads(function (err) {
-			if (err) {
-				return callback(err);
-			}
+		callback();
+	});
+};
 
-			var map = plugins.soundpacks.map(function (pack) {
-				return Object.keys(pack.sounds).reduce(function (prev, soundName) {
-					var soundPath = pack.sounds[soundName];
-					prev[pack.name + ' | ' + soundName] = pack.id + '/' + soundPath;
-					return prev;
-				}, {});
-			});
-			map.unshift({});
-			map = Object.assign.apply(null, map);
-
-			async.series([
-				function (next) {
-					rimraf(soundsPath, next);
-				},
-				function (next) {
-					mkdirp(soundsPath, next);
-				},
-				function (cb) {
-					async.parallel([
-						function (next) {
-							fs.writeFile(path.join(soundsPath, 'fileMap.json'), JSON.stringify(map), next);
-						},
-						function (next) {
-							async.each(plugins.soundpacks, function (pack, next) {
-								file.linkDirs(pack.dir, path.join(soundsPath, pack.id), next);
-							}, next);
-						},
-					], cb);
-				},
-			], function (err) {
-				callback(err);
-			});
-		});
-	};
+Sounds.build = function build(callback) {
+	Sounds.addUploads(function (err) {
+		if (err) {
+			return callback(err);
+		}
 
-	var keys = ['chat-incoming', 'chat-outgoing', 'notification'];
+		var map = plugins.soundpacks.map(function (pack) {
+			return Object.keys(pack.sounds).reduce(function (prev, soundName) {
+				var soundPath = pack.sounds[soundName];
+				prev[pack.name + ' | ' + soundName] = pack.id + '/' + soundPath;
+				return prev;
+			}, {});
+		});
+		map.unshift({});
+		map = Object.assign.apply(null, map);
 
-	Meta.sounds.getUserSoundMap = function getUserSoundMap(uid, callback) {
-		async.parallel({
-			defaultMapping: function (next) {
-				Meta.configs.getFields(keys, next);
+		async.series([
+			function (next) {
+				rimraf(soundsPath, next);
 			},
-			userSettings: function (next) {
-				user.getSettings(uid, next);
+			function (next) {
+				mkdirp(soundsPath, next);
 			},
-		}, function (err, results) {
-			if (err) {
-				return callback(err);
-			}
-
-			var userSettings = results.userSettings;
-			userSettings = {
-				notification: userSettings.notificationSound,
-				'chat-incoming': userSettings.incomingChatSound,
-				'chat-outgoing': userSettings.outgoingChatSound,
-			};
-			var defaultMapping = results.defaultMapping || {};
-			var soundMapping = {};
-
-			keys.forEach(function (key) {
-				if (userSettings[key] || userSettings[key] === '') {
-					soundMapping[key] = userSettings[key] || '';
-				} else {
-					soundMapping[key] = defaultMapping[key] || '';
-				}
-			});
+			function (cb) {
+				async.parallel([
+					function (next) {
+						fs.writeFile(path.join(soundsPath, 'fileMap.json'), JSON.stringify(map), next);
+					},
+					function (next) {
+						async.each(plugins.soundpacks, function (pack, next) {
+							file.linkDirs(pack.dir, path.join(soundsPath, pack.id), next);
+						}, next);
+					},
+				], cb);
+			},
+		], function (err) {
+			callback(err);
+		});
+	});
+};
 
-			callback(null, soundMapping);
+var keys = ['chat-incoming', 'chat-outgoing', 'notification'];
+
+Sounds.getUserSoundMap = function getUserSoundMap(uid, callback) {
+	async.parallel({
+		defaultMapping: function (next) {
+			Meta.configs.getFields(keys, next);
+		},
+		userSettings: function (next) {
+			user.getSettings(uid, next);
+		},
+	}, function (err, results) {
+		if (err) {
+			return callback(err);
+		}
+
+		var userSettings = results.userSettings;
+		userSettings = {
+			notification: userSettings.notificationSound,
+			'chat-incoming': userSettings.incomingChatSound,
+			'chat-outgoing': userSettings.outgoingChatSound,
+		};
+		var defaultMapping = results.defaultMapping || {};
+		var soundMapping = {};
+
+		keys.forEach(function (key) {
+			if (userSettings[key] || userSettings[key] === '') {
+				soundMapping[key] = userSettings[key] || '';
+			} else {
+				soundMapping[key] = defaultMapping[key] || '';
+			}
 		});
-	};
+
+		callback(null, soundMapping);
+	});
 };
diff --git a/src/meta/tags.js b/src/meta/tags.js
index ac0b395a23..34ed3c43a9 100644
--- a/src/meta/tags.js
+++ b/src/meta/tags.js
@@ -4,163 +4,163 @@ var nconf = require('nconf');
 var validator = require('validator');
 var async = require('async');
 var winston = require('winston');
+
 var plugins = require('../plugins');
+var Meta = require('../meta');
+
+var Tags = module.exports;
+
+Tags.parse = function (req, meta, link, callback) {
+	async.parallel({
+		tags: function (next) {
+			var defaultTags = [{
+				name: 'viewport',
+				content: 'width=device-width, initial-scale=1.0',
+			}, {
+				name: 'content-type',
+				content: 'text/html; charset=UTF-8',
+				noEscape: true,
+			}, {
+				name: 'apple-mobile-web-app-capable',
+				content: 'yes',
+			}, {
+				name: 'mobile-web-app-capable',
+				content: 'yes',
+			}, {
+				property: 'og:site_name',
+				content: Meta.config.title || 'NodeBB',
+			}, {
+				name: 'msapplication-badge',
+				content: 'frequency=30; polling-uri=' + nconf.get('url') + '/sitemap.xml',
+				noEscape: true,
+			}];
+
+			if (Meta.config.keywords) {
+				defaultTags.push({
+					name: 'keywords',
+					content: Meta.config.keywords,
+				});
+			}
 
-module.exports = function (Meta) {
-	Meta.tags = {};
+			if (Meta.config['brand:logo']) {
+				defaultTags.push({
+					name: 'msapplication-square150x150logo',
+					content: Meta.config['brand:logo'],
+					noEscape: true,
+				});
+			}
 
-	Meta.tags.parse = function (req, meta, link, callback) {
-		async.parallel({
-			tags: function (next) {
-				var defaultTags = [{
-					name: 'viewport',
-					content: 'width=device-width, initial-scale=1.0',
+			plugins.fireHook('filter:meta.getMetaTags', defaultTags, next);
+		},
+		links: function (next) {
+			var defaultLinks = [{
+				rel: 'icon',
+				type: 'image/x-icon',
+				href: nconf.get('relative_path') + '/favicon.ico' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''),
+			}, {
+				rel: 'manifest',
+				href: nconf.get('relative_path') + '/manifest.json',
+			}];
+
+			if (plugins.hasListeners('filter:search.query')) {
+				defaultLinks.push({
+					rel: 'search',
+					type: 'application/opensearchdescription+xml',
+					href: nconf.get('relative_path') + '/osd.xml',
+				});
+			}
+
+			// Touch icons for mobile-devices
+			if (Meta.config['brand:touchIcon']) {
+				defaultLinks.push({
+					rel: 'apple-touch-icon',
+					href: nconf.get('relative_path') + '/apple-touch-icon',
 				}, {
-					name: 'content-type',
-					content: 'text/html; charset=UTF-8',
-					noEscape: true,
+					rel: 'icon',
+					sizes: '36x36',
+					href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-36.png',
 				}, {
-					name: 'apple-mobile-web-app-capable',
-					content: 'yes',
+					rel: 'icon',
+					sizes: '48x48',
+					href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-48.png',
 				}, {
-					name: 'mobile-web-app-capable',
-					content: 'yes',
+					rel: 'icon',
+					sizes: '72x72',
+					href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-72.png',
 				}, {
-					property: 'og:site_name',
-					content: Meta.config.title || 'NodeBB',
+					rel: 'icon',
+					sizes: '96x96',
+					href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-96.png',
 				}, {
-					name: 'msapplication-badge',
-					content: 'frequency=30; polling-uri=' + nconf.get('url') + '/sitemap.xml',
-					noEscape: true,
-				}];
-
-				if (Meta.config.keywords) {
-					defaultTags.push({
-						name: 'keywords',
-						content: Meta.config.keywords,
-					});
-				}
-
-				if (Meta.config['brand:logo']) {
-					defaultTags.push({
-						name: 'msapplication-square150x150logo',
-						content: Meta.config['brand:logo'],
-						noEscape: true,
-					});
-				}
-
-				plugins.fireHook('filter:meta.getMetaTags', defaultTags, next);
-			},
-			links: function (next) {
-				var defaultLinks = [{
 					rel: 'icon',
-					type: 'image/x-icon',
-					href: nconf.get('relative_path') + '/favicon.ico' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''),
+					sizes: '144x144',
+					href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-144.png',
 				}, {
-					rel: 'manifest',
-					href: nconf.get('relative_path') + '/manifest.json',
-				}];
-
-				if (plugins.hasListeners('filter:search.query')) {
-					defaultLinks.push({
-						rel: 'search',
-						type: 'application/opensearchdescription+xml',
-						href: nconf.get('relative_path') + '/osd.xml',
-					});
-				}
-
-				// Touch icons for mobile-devices
-				if (Meta.config['brand:touchIcon']) {
-					defaultLinks.push({
-						rel: 'apple-touch-icon',
-						href: nconf.get('relative_path') + '/apple-touch-icon',
-					}, {
-						rel: 'icon',
-						sizes: '36x36',
-						href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-36.png',
-					}, {
-						rel: 'icon',
-						sizes: '48x48',
-						href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-48.png',
-					}, {
-						rel: 'icon',
-						sizes: '72x72',
-						href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-72.png',
-					}, {
-						rel: 'icon',
-						sizes: '96x96',
-						href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-96.png',
-					}, {
-						rel: 'icon',
-						sizes: '144x144',
-						href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-144.png',
-					}, {
-						rel: 'icon',
-						sizes: '192x192',
-						href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-192.png',
-					});
-				}
-				plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next);
-			},
-		}, function (err, results) {
-			if (err) {
-				return callback(err);
+					rel: 'icon',
+					sizes: '192x192',
+					href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-192.png',
+				});
 			}
+			plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next);
+		},
+	}, function (err, results) {
+		if (err) {
+			return callback(err);
+		}
 
-			meta = results.tags.concat(meta || []).map(function (tag) {
-				if (!tag || typeof tag.content !== 'string') {
-					winston.warn('Invalid meta tag. ', tag);
-					return tag;
-				}
-
-				if (!tag.noEscape) {
-					tag.content = validator.escape(String(tag.content));
-				}
-
+		meta = results.tags.concat(meta || []).map(function (tag) {
+			if (!tag || typeof tag.content !== 'string') {
+				winston.warn('Invalid meta tag. ', tag);
 				return tag;
-			});
+			}
 
-			addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
+			if (!tag.noEscape) {
+				tag.content = validator.escape(String(tag.content));
+			}
 
-			var ogUrl = nconf.get('url') + req.path;
-			addIfNotExists(meta, 'property', 'og:url', ogUrl);
+			return tag;
+		});
 
-			addIfNotExists(meta, 'name', 'description', Meta.config.description);
-			addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
+		addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
 
-			var ogImage = Meta.config['og:image'] || Meta.config['brand:logo'] || '';
-			if (ogImage && !ogImage.startsWith('http')) {
-				ogImage = nconf.get('url') + ogImage;
-			}
-			addIfNotExists(meta, 'property', 'og:image', ogImage);
-			if (ogImage) {
-				addIfNotExists(meta, 'property', 'og:image:width', 200);
-				addIfNotExists(meta, 'property', 'og:image:height', 200);
-			}
+		var ogUrl = nconf.get('url') + req.path;
+		addIfNotExists(meta, 'property', 'og:url', ogUrl);
 
-			link = results.links.concat(link || []);
+		addIfNotExists(meta, 'name', 'description', Meta.config.description);
+		addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
 
-			callback(null, {
-				meta: meta,
-				link: link,
-			});
-		});
-	};
+		var ogImage = Meta.config['og:image'] || Meta.config['brand:logo'] || '';
+		if (ogImage && !ogImage.startsWith('http')) {
+			ogImage = nconf.get('url') + ogImage;
+		}
+		addIfNotExists(meta, 'property', 'og:image', ogImage);
+		if (ogImage) {
+			addIfNotExists(meta, 'property', 'og:image:width', 200);
+			addIfNotExists(meta, 'property', 'og:image:height', 200);
+		}
 
-	function addIfNotExists(meta, keyName, tagName, value) {
-		var exists = false;
-		meta.forEach(function (tag) {
-			if (tag[keyName] === tagName) {
-				exists = true;
-			}
+		link = results.links.concat(link || []);
+
+		callback(null, {
+			meta: meta,
+			link: link,
 		});
+	});
+};
 
-		if (!exists && value) {
-			var data = {
-				content: validator.escape(String(value)),
-			};
-			data[keyName] = tagName;
-			meta.push(data);
+function addIfNotExists(meta, keyName, tagName, value) {
+	var exists = false;
+	meta.forEach(function (tag) {
+		if (tag[keyName] === tagName) {
+			exists = true;
 		}
+	});
+
+	if (!exists && value) {
+		var data = {
+			content: validator.escape(String(value)),
+		};
+		data[keyName] = tagName;
+		meta.push(data);
 	}
-};
+}
diff --git a/src/meta/themes.js b/src/meta/themes.js
index 9bca68c431..d034bd8a96 100644
--- a/src/meta/themes.js
+++ b/src/meta/themes.js
@@ -9,168 +9,167 @@ var async = require('async');
 
 var file = require('../file');
 var db = require('../database');
-
-module.exports = function (Meta) {
-	Meta.themes = {};
-
-	Meta.themes.get = function (callback) {
-		var themePath = nconf.get('themes_path');
-		if (typeof themePath !== 'string') {
-			return callback(null, []);
-		}
-
-		async.waterfall([
-			function (next) {
-				fs.readdir(themePath, next);
-			},
-			function (files, next) {
-				async.filter(files, function (file, next) {
-					fs.stat(path.join(themePath, file), function (err, fileStat) {
-						if (err) {
-							if (err.code === 'ENOENT') {
-								return next(null, false);
-							}
-							return next(err);
-						}
-
-						next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
-					});
-				}, next);
-			},
-			function (themes, next) {
-				async.map(themes, function (theme, next) {
-					var config = path.join(themePath, theme, 'theme.json');
-
-					fs.readFile(config, function (err, file) {
-						if (err) {
-							if (err.code === 'ENOENT') {
-								return next(null, null);
-							}
-							return next(err);
+var Meta = require('../meta');
+
+var Themes = module.exports;
+
+Themes.get = function (callback) {
+	var themePath = nconf.get('themes_path');
+	if (typeof themePath !== 'string') {
+		return callback(null, []);
+	}
+
+	async.waterfall([
+		function (next) {
+			fs.readdir(themePath, next);
+		},
+		function (files, next) {
+			async.filter(files, function (file, next) {
+				fs.stat(path.join(themePath, file), function (err, fileStat) {
+					if (err) {
+						if (err.code === 'ENOENT') {
+							return next(null, false);
 						}
-						try {
-							var configObj = JSON.parse(file.toString());
-
-							// Minor adjustments for API output
-							configObj.type = 'local';
-							if (configObj.screenshot) {
-								configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id;
-							} else {
-								configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png';
-							}
-							next(null, configObj);
-						} catch (err) {
-							winston.error('[themes] Unable to parse theme.json ' + theme);
-							next(null, null);
+						return next(err);
+					}
+
+					next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
+				});
+			}, next);
+		},
+		function (themes, next) {
+			async.map(themes, function (theme, next) {
+				var config = path.join(themePath, theme, 'theme.json');
+
+				fs.readFile(config, function (err, file) {
+					if (err) {
+						if (err.code === 'ENOENT') {
+							return next(null, null);
 						}
-					});
-				}, next);
-			},
-			function (themes, next) {
-				themes = themes.filter(Boolean);
-				next(null, themes);
-			},
-		], callback);
-	};
-
-	Meta.themes.set = function (data, callback) {
-		var themeData = {
-			'theme:type': data.type,
-			'theme:id': data.id,
-			'theme:staticDir': '',
-			'theme:templates': '',
-			'theme:src': '',
-		};
-
-		switch (data.type) {
-		case 'local':
-			async.waterfall([
-				async.apply(Meta.configs.get, 'theme:id'),
-				function (current, next) {
-					async.series([
-						async.apply(db.sortedSetRemove, 'plugins:active', current),
-						async.apply(db.sortedSetAdd, 'plugins:active', 0, data.id),
-					], function (err) {
-						next(err);
-					});
-				},
-				function (next) {
-					fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function (err, config) {
-						if (!err) {
-							config = JSON.parse(config.toString());
-							next(null, config);
+						return next(err);
+					}
+					try {
+						var configObj = JSON.parse(file.toString());
+
+						// Minor adjustments for API output
+						configObj.type = 'local';
+						if (configObj.screenshot) {
+							configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id;
 						} else {
-							next(err);
+							configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png';
 						}
-					});
-				},
-				function (config, next) {
-					themeData['theme:staticDir'] = config.staticDir ? config.staticDir : '';
-					themeData['theme:templates'] = config.templates ? config.templates : '';
-					themeData['theme:src'] = '';
-
-					Meta.configs.setMultiple(themeData, next);
+						next(null, configObj);
+					} catch (err) {
+						winston.error('[themes] Unable to parse theme.json ' + theme);
+						next(null, null);
+					}
+				});
+			}, next);
+		},
+		function (themes, next) {
+			themes = themes.filter(Boolean);
+			next(null, themes);
+		},
+	], callback);
+};
 
-					// Re-set the themes path (for when NodeBB is reloaded)
-					Meta.themes.setPath(config);
-				},
-			], callback);
-
-			Meta.reloadRequired = true;
-			break;
-
-		case 'bootswatch':
-			Meta.configs.setMultiple({
-				'theme:src': data.src,
-				bootswatchSkin: data.id.toLowerCase(),
-			}, callback);
-			break;
-		}
+Themes.set = function (data, callback) {
+	var themeData = {
+		'theme:type': data.type,
+		'theme:id': data.id,
+		'theme:staticDir': '',
+		'theme:templates': '',
+		'theme:src': '',
 	};
 
-	Meta.themes.setupPaths = function (callback) {
+	switch (data.type) {
+	case 'local':
 		async.waterfall([
+			async.apply(Meta.configs.get, 'theme:id'),
+			function (current, next) {
+				async.series([
+					async.apply(db.sortedSetRemove, 'plugins:active', current),
+					async.apply(db.sortedSetAdd, 'plugins:active', 0, data.id),
+				], function (err) {
+					next(err);
+				});
+			},
 			function (next) {
-				async.parallel({
-					themesData: Meta.themes.get,
-					currentThemeId: function (next) {
-						db.getObjectField('config', 'theme:id', next);
-					},
-				}, next);
+				fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function (err, config) {
+					if (!err) {
+						config = JSON.parse(config.toString());
+						next(null, config);
+					} else {
+						next(err);
+					}
+				});
 			},
-			function (data, next) {
-				var themeId = data.currentThemeId || 'nodebb-theme-persona';
-
-				var themeObj = data.themesData.filter(function (themeObj) {
-					return themeObj.id === themeId;
-				})[0];
-
-				if (process.env.NODE_ENV === 'development') {
-					winston.info('[themes] Using theme ' + themeId);
-				}
+			function (config, next) {
+				themeData['theme:staticDir'] = config.staticDir ? config.staticDir : '';
+				themeData['theme:templates'] = config.templates ? config.templates : '';
+				themeData['theme:src'] = '';
 
-				if (!themeObj) {
-					return callback(new Error('[[error:theme-not-found]]'));
-				}
+				Meta.configs.setMultiple(themeData, next);
 
-				Meta.themes.setPath(themeObj);
-				next();
+				// Re-set the themes path (for when NodeBB is reloaded)
+				Themes.setPath(config);
 			},
 		], callback);
-	};
 
-	Meta.themes.setPath = function (themeObj) {
-		// Theme's templates path
-		var themePath = nconf.get('base_templates_path');
-		var fallback = path.join(nconf.get('themes_path'), themeObj.id, 'templates');
+		Meta.reloadRequired = true;
+		break;
 
-		if (themeObj.templates) {
-			themePath = path.join(nconf.get('themes_path'), themeObj.id, themeObj.templates);
-		} else if (file.existsSync(fallback)) {
-			themePath = fallback;
-		}
+	case 'bootswatch':
+		Meta.configs.setMultiple({
+			'theme:src': data.src,
+			bootswatchSkin: data.id.toLowerCase(),
+		}, callback);
+		break;
+	}
+};
 
-		nconf.set('theme_templates_path', themePath);
-		nconf.set('theme_config', path.join(nconf.get('themes_path'), themeObj.id, 'theme.json'));
-	};
+Themes.setupPaths = function (callback) {
+	async.waterfall([
+		function (next) {
+			async.parallel({
+				themesData: Themes.get,
+				currentThemeId: function (next) {
+					db.getObjectField('config', 'theme:id', next);
+				},
+			}, next);
+		},
+		function (data, next) {
+			var themeId = data.currentThemeId || 'nodebb-theme-persona';
+
+			var themeObj = data.themesData.filter(function (themeObj) {
+				return themeObj.id === themeId;
+			})[0];
+
+			if (process.env.NODE_ENV === 'development') {
+				winston.info('[themes] Using theme ' + themeId);
+			}
+
+			if (!themeObj) {
+				return callback(new Error('[[error:theme-not-found]]'));
+			}
+
+			Themes.setPath(themeObj);
+			next();
+		},
+	], callback);
+};
+
+Themes.setPath = function (themeObj) {
+	// Theme's templates path
+	var themePath = nconf.get('base_templates_path');
+	var fallback = path.join(nconf.get('themes_path'), themeObj.id, 'templates');
+
+	if (themeObj.templates) {
+		themePath = path.join(nconf.get('themes_path'), themeObj.id, themeObj.templates);
+	} else if (file.existsSync(fallback)) {
+		themePath = fallback;
+	}
+
+	nconf.set('theme_templates_path', themePath);
+	nconf.set('theme_config', path.join(nconf.get('themes_path'), themeObj.id, 'theme.json'));
 };