diff --git a/.eslintignore b/.eslintignore index 0799652254..250640f954 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,3 +18,4 @@ logs/ *.ipr *.iws /coverage +/build diff --git a/app.js b/app.js index 8844d8af6f..77da25936d 100644 --- a/app.js +++ b/app.js @@ -99,7 +99,7 @@ function loadConfig(callback) { nconf.defaults({ base_dir: __dirname, themes_path: path.join(__dirname, 'node_modules'), - views_dir: path.join(__dirname, 'public/templates'), + views_dir: path.join(__dirname, 'build/public/templates'), version: pkg.version }); diff --git a/nodebb b/nodebb index dff44fc5af..ee87b24288 100755 --- a/nodebb +++ b/nodebb @@ -231,12 +231,12 @@ function upgradePlugins(callback) { if (['y', 'Y', 'yes', 'YES'].indexOf(result.upgrade) !== -1) { process.stdout.write('\nUpgrading packages...'); - var args = ['npm', 'i']; + var args = ['i']; found.forEach(function (suggestObj) { args.push(suggestObj.name + '@' + suggestObj.suggested); }); - require('child_process').execFile('/usr/bin/env', args, { stdio: 'ignore' }, function (err) { + cproc.execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', args, { stdio: 'ignore' }, function (err) { if (!err) { process.stdout.write(' OK\n'.green); } diff --git a/package.json b/package.json index c47921d71a..2aebb0e57b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "start": "node loader.js", "lint": "eslint --cache .", - "pretest": "npm run lint && node app --build", + "pretest": "npm run lint", "test": "istanbul cover node_modules/mocha/bin/_mocha -- -R dot", "coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index 02a3cddc1c..6b18e5dda0 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -333,7 +333,7 @@ $(document).ready(function () { callback(templates.cache[template]); } else { $.ajax({ - url: RELATIVE_PATH + '/templates/' + template + '.tpl' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), + url: config.relative_path + '/assets/templates/' + template + '.tpl' + '?' + config['cache-buster'], type: 'GET', success: function (data) { callback(data.toString()); diff --git a/public/src/app.js b/public/src/app.js index fe12a7adfc..00d5e7c017 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -558,7 +558,7 @@ app.cacheBuster = null; var scriptEl = document.createElement('script'); scriptEl.type = 'text/javascript'; - scriptEl.src = config.relative_path + '/vendor/jquery/js/jquery-ui.js' + (app.cacheBuster ? '?v=' + app.cacheBuster : ''); + scriptEl.src = config.relative_path + '/vendor/jquery/js/jquery-ui.js' + '?' + config['cache-buster']; scriptEl.onload = callback; document.head.appendChild(scriptEl); }; @@ -625,7 +625,7 @@ app.cacheBuster = null; app.loadProgressiveStylesheet = function () { var linkEl = document.createElement('link'); linkEl.rel = 'stylesheet'; - linkEl.href = config.relative_path + '/js-enabled.css'; + linkEl.href = config.relative_path + '/assets/js-enabled.css'; document.head.appendChild(linkEl); }; diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js index e99b7ee0a8..d0f84ade3a 100644 --- a/public/src/client/account/header.js +++ b/public/src/client/account/header.js @@ -84,7 +84,7 @@ define('forum/account/header', [ params: {uid: ajaxify.data.uid }, accept: '.png,.jpg,.bmp' }, function (imageUrlOnServer) { - components.get('account/cover').css('background-image', 'url(' + imageUrlOnServer + '?v=' + Date.now() + ')'); + components.get('account/cover').css('background-image', 'url(' + imageUrlOnServer + '?' + config['cache-buster'] + ')'); }); }, removeCover diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 215f8c88df..aa719ff1a2 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -515,12 +515,12 @@ break; } - jQuery.getScript(config.relative_path + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function () { + jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function () { jQuery('.timeago').timeago(); adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings); // Retrieve the shorthand timeago values as well - jQuery.getScript(config.relative_path + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () { + jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () { // Switch back to long-form adaptor.toggleTimeagoShorthand(); }); diff --git a/public/src/require-config.js b/public/src/require-config.js index 8618685052..0e021f6be0 100644 --- a/public/src/require-config.js +++ b/public/src/require-config.js @@ -1,5 +1,5 @@ require.config({ - baseUrl: config.relative_path + "/src/modules", + baseUrl: config.relative_path + '/assets/src/modules', waitSeconds: 7, urlArgs: config['cache-buster'], paths: { diff --git a/public/src/widgets.js b/public/src/widgets.js index 46023fad15..377ccb2c53 100644 --- a/public/src/widgets.js +++ b/public/src/widgets.js @@ -29,7 +29,7 @@ } }); - $.get(config.relative_path + '/api/widgets/render' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), { + $.get(config.relative_path + '/api/widgets/render' + '?' + config['cache-buster'], { locations: widgetLocations, template: template + '.tpl', url: url, diff --git a/src/admin/search.js b/src/admin/search.js index 860c527e6b..4c53815e48 100644 --- a/src/admin/search.js +++ b/src/admin/search.js @@ -4,6 +4,7 @@ var fs = require('fs'); var path = require('path'); var async = require('async'); var sanitizeHTML = require('sanitize-html'); +var nconf = require('nconf'); var utils = require('../../public/src/utils'); var Translator = require('../../public/src/modules/translator').Translator; @@ -23,7 +24,7 @@ function filterDirectories(directories) { } function getAdminNamespaces(callback) { - utils.walk(path.resolve(__dirname, '../../public/templates/admin'), function (err, directories) { + utils.walk(path.resolve(nconf.get('views_dir'), 'admin'), function (err, directories) { if (err) { return callback(err); } @@ -60,7 +61,7 @@ var fallbackCacheInProgress = {}; var fallbackCache = {}; function initFallback(namespace, callback) { - fs.readFile(path.resolve(__dirname, '../../public/templates/', namespace + '.tpl'), function (err, file) { + fs.readFile(path.resolve(nconf.get('views_dir'), namespace + '.tpl'), function (err, file) { if (err) { return callback(err); } diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index 639267f5f5..0d1f509631 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -2,6 +2,7 @@ var async = require('async'); +var nconf = require('nconf'); var meta = require('../../meta'); var settingsController = module.exports; @@ -25,7 +26,7 @@ function renderEmail(req, res, next) { var path = require('path'); var utils = require('../../../public/src/utils'); - var emailsPath = path.join(__dirname, '../../../public/templates/emails'); + var emailsPath = path.join(nconf.get('views_dir'), 'emails'); async.waterfall([ function (next) { diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index d1664cb8f1..25b2ae34fd 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -105,14 +105,10 @@ uploadsController.uploadSound = function (req, res, next) { return next(err); } - var soundsPath = path.join(__dirname, '../../../public/sounds'), + var soundsPath = path.join(__dirname, '../../../build/public/sounds'), filePath = path.join(__dirname, '../../../public/uploads/sounds', uploadedFile.name); - if (process.platform === 'win32') { - fs.link(filePath, path.join(soundsPath, path.basename(filePath))); - } else { - fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file'); - } + file.link(filePath, path.join(soundsPath, path.basename(filePath))); fs.unlink(uploadedFile.path, function (err) { if (err) { diff --git a/src/coverPhoto.js b/src/coverPhoto.js index 7965f310ea..6307110253 100644 --- a/src/coverPhoto.js +++ b/src/coverPhoto.js @@ -26,7 +26,7 @@ function getCover(type, id) { return covers[id]; } - return nconf.get('relative_path') + '/images/cover-default.png'; + return nconf.get('relative_path') + '/assets/images/cover-default.png'; } module.exports = coverPhoto; diff --git a/src/file.js b/src/file.js index df820cc47f..d8fa128e5e 100644 --- a/src/file.js +++ b/src/file.js @@ -102,4 +102,17 @@ file.existsSync = function (path) { return !!exists; }; +file.link = function link(filePath, destPath, cb) { + if (process.platform === 'win32') { + fs.link(filePath, destPath, cb); + } else { + fs.symlink(filePath, destPath, 'file', cb); + } +}; + +file.linkDirs = function linkDirs(sourceDir, destDir, callback) { + var type = (process.platform === 'win32') ? 'junction' : 'dir'; + fs.symlink(sourceDir, destDir, type, callback); +}; + module.exports = file; diff --git a/src/image.js b/src/image.js index 1cfbdccbe3..0ab68e5a23 100644 --- a/src/image.js +++ b/src/image.js @@ -65,6 +65,9 @@ image.resizeImage = function (data, callback) { } }, function (image, next) { + if (data.write === false) { + return next(); + } image.write(data.target || data.path, next); } ], function (err) { diff --git a/src/meta/build.js b/src/meta/build.js index 78212c1d25..2d89abaf5a 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -43,10 +43,15 @@ exports.build = function build(targets, callback) { }; exports.buildTargets = function (targets, callback) { + var cacheBuster = require('./cacheBuster'); var meta = require('../meta'); buildStart = buildStart || Date.now(); - var step = function (startTime, target, next) { + var step = function (startTime, target, next, err) { + if (err) { + winston.error('Build failed: ' + err.message); + process.exit(1); + } winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's'); next(); }; @@ -57,6 +62,8 @@ exports.buildTargets = function (targets, callback) { winston.info('[build] Building javascript'); var startTime = Date.now(); async.series([ + meta.js.linkModules, + meta.js.linkStatics, async.apply(meta.js.minify, 'nodebb.min.js'), async.apply(meta.js.minify, 'acp.min.js') ], step.bind(this, startTime, 'js', next)); @@ -74,13 +81,13 @@ exports.buildTargets = function (targets, callback) { case 'clientCSS': winston.info('[build] Building client-side CSS'); startTime = Date.now(); - meta.css.minify('stylesheet.css', step.bind(this, startTime, target, next)); + meta.css.minify('client', step.bind(this, startTime, target, next)); break; case 'acpCSS': winston.info('[build] Building admin control panel CSS'); startTime = Date.now(); - meta.css.minify('admin.css', step.bind(this, startTime, target, next)); + meta.css.minify('admin', step.bind(this, startTime, target, next)); break; case 'tpl': @@ -108,14 +115,21 @@ exports.buildTargets = function (targets, callback) { return process.exit(1); } - var time = (Date.now() - buildStart) / 1000; + cacheBuster.write(function (err) { + if (err) { + winston.error('[build] Failed to write `cache-buster.conf`: ' + err.message); + return process.exit(1); + } - winston.info('[build] Asset compilation successful. Completed in ' + time + 's.'); + var time = (Date.now() - buildStart) / 1000; - if (typeof callback === 'function') { - callback(); - } else { - process.exit(0); - } + winston.info('[build] Asset compilation successful. Completed in ' + time + 's.'); + + if (typeof callback === 'function') { + callback(); + } else { + process.exit(0); + } + }); }); }; \ No newline at end of file diff --git a/src/meta/cacheBuster.js b/src/meta/cacheBuster.js new file mode 100644 index 0000000000..ca00fba057 --- /dev/null +++ b/src/meta/cacheBuster.js @@ -0,0 +1,46 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var mkdirp = require('mkdirp'); +var winston = require('winston'); + +var filePath = path.join(__dirname, '../../build/cache-buster'); + +var cached; + +// cache buster is an 11-character, lowercase, alphanumeric string +function generate() { + return (Math.random() * 1e18).toString(32).slice(0, 11); +} + +exports.write = function write(callback) { + mkdirp(path.dirname(filePath), function (err) { + if (err) { + return callback(err); + } + + fs.writeFile(filePath, generate(), callback); + }); +}; + +exports.read = function read(callback) { + if (cached) { + return callback(null, cached); + } + + fs.readFile(filePath, function (err, buffer) { + if (err) { + winston.warn('[cache-buster] could not read cache buster: ' + err.message); + return callback(); + } + + buffer = buffer.toString(); + if (buffer) { + cached = buffer; + return callback(null, cached); + } + + callback(); + }); +}; diff --git a/src/meta/configs.js b/src/meta/configs.js index 75da0595d2..907e0ef477 100644 --- a/src/meta/configs.js +++ b/src/meta/configs.js @@ -6,7 +6,7 @@ var nconf = require('nconf'); var db = require('../database'); var pubsub = require('../pubsub'); -var utils = require('../../public/src/utils'); +var cacheBuster = require('./cacheBuster'); module.exports = function (Meta) { @@ -21,10 +21,16 @@ module.exports = function (Meta) { Meta.configs.list(next); }, function (config, next) { - config['cache-buster'] = 'v=' + utils.generateUUID(); + cacheBuster.read(function (err, buster) { + if (err) { + return next(err); + } + + config['cache-buster'] = 'v=' + (buster || Date.now()); - Meta.config = config; - setImmediate(next); + Meta.config = config; + next(); + }); } ], callback); }; diff --git a/src/meta/css.js b/src/meta/css.js index 4bfb19ba47..3627c6c41a 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -18,8 +18,33 @@ var utils = require('../../public/src/utils'); module.exports = function (Meta) { Meta.css = {}; - Meta.css.cache = undefined; - Meta.css.acpCache = undefined; + + 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";', + ].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n'); + }, + }; Meta.css.minify = function (target, callback) { callback = callback || function () {}; @@ -30,25 +55,25 @@ module.exports = function (Meta) { return callback(err); } - var themeId = (themeData['theme:id'] || 'nodebb-theme-persona'), - baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')), - paths = [ - baseThemePath, - path.join(__dirname, '../../node_modules'), - path.join(__dirname, '../../public/vendor/fontawesome/less') - ], - source = '@import "font-awesome";'; + 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')); + var paths = [ + baseThemePath, + path.join(__dirname, '../../node_modules'), + path.join(__dirname, '../../public/vendor/fontawesome/less') + ]; + var source = ''; - plugins.lessFiles = filterMissingFiles(plugins.lessFiles); - plugins.cssFiles = filterMissingFiles(plugins.cssFiles); + var lessFiles = filterMissingFiles(plugins.lessFiles); + var cssFiles = filterMissingFiles(plugins.cssFiles); async.waterfall([ function (next) { - getStyleSource(plugins.cssFiles, '\n@import (inline) ".', '.css', next); + getStyleSource(cssFiles, '\n@import (inline) ".', '.css', next); }, function (src, next) { source += src; - getStyleSource(plugins.lessFiles, '\n@import ".', '.less', next); + getStyleSource(lessFiles, '\n@import ".', '.less', next); }, function (src, next) { source += src; @@ -59,43 +84,11 @@ module.exports = function (Meta) { return callback(err); } - var acpSource = source; - - if (target !== 'admin.css') { - source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; - source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";'; - source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";'; - source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";'; - source += '\n@import (inline) "..' + path.sep + 'node_modules/cropperjs/dist/cropper.css";'; - source = '@import "./theme";\n' + source; - - minify(source, paths, 'cache', callback); - } else { - acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n'; - acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n'; - acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n'; - acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; - acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";'; - - minify(acpSource, paths, 'acpCache', callback); - } + minify(buildImports[target](source), paths, target, callback); }); }); }; - Meta.css.getFromFile = function (callback) { - async.series([ - async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/stylesheet.css'), 'cache'), - async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/admin.css'), 'acpCache') - ], function (err) { - callback(err); - }); - }; - function getStyleSource(files, prefix, extension, callback) { var pluginDirectories = [], source = ''; @@ -125,35 +118,22 @@ module.exports = function (Meta) { }); } - Meta.css.commitToFile = function (filename, callback) { - var file = (filename === 'acpCache' ? 'admin' : 'stylesheet') + '.css'; + Meta.css.commitToFile = function (target, source, callback) { + var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css'; - fs.writeFile(path.join(__dirname, '../../public/' + file), Meta.css[filename], function (err) { + fs.writeFile(path.join(__dirname, '../../build/public/' + filename), source, function (err) { if (!err) { - winston.verbose('[meta/css] ' + file + ' committed to disk.'); + winston.verbose('[meta/css] ' + target + ' CSS committed to disk.'); } else { winston.error('[meta/css] ' + err.message); - process.exit(0); - } - - callback(); - }); - }; - - Meta.css.loadFile = function (filePath, filename, callback) { - winston.verbose('[meta/css] Reading stylesheet ' + filePath.split('/').pop() + ' from file'); - - fs.readFile(filePath, function (err, file) { - if (err) { - return callback(err); + process.exit(1); } - Meta.css[filename] = file; callback(); }); }; - function minify(source, paths, destination, callback) { + function minify(source, paths, target, callback) { callback = callback || function () {}; less.render(source, { paths: paths @@ -163,20 +143,14 @@ module.exports = function (Meta) { return callback(err); } - postcss([ autoprefixer, clean() ]).process(lessOutput.css).then(function (result) { + postcss(global.env === 'development' ? [ autoprefixer ] : [ autoprefixer, clean() ]).process(lessOutput.css).then(function (result) { result.warnings().forEach(function (warn) { winston.verbose(warn.toString()); }); - Meta.css[destination] = result.css; - - // Save the compiled CSS in public/ so things like nginx can serve it - if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) { - return Meta.css.commitToFile(destination, function () { - callback(null, result.css); - }); - } - callback(null, result.css); + return Meta.css.commitToFile(target, result.css, function () { + callback(null, result.css); + }); }); }); } diff --git a/src/meta/js.js b/src/meta/js.js index d53a2c1789..b6b6e61ee6 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -4,8 +4,11 @@ var winston = require('winston'); var fork = require('child_process').fork; var path = require('path'); var async = require('async'); -var nconf = require('nconf'); var fs = require('fs'); +var mkdirp = require('mkdirp'); +var rimraf = require('rimraf'); + +var file = require('../file'); var plugins = require('../plugins'); var utils = require('../../public/src/utils'); @@ -86,30 +89,45 @@ module.exports = function (Meta) { } } }; + + Meta.js.linkModules = function (callback) { + rimraf(path.join(__dirname, '../../build/public/src/modules'), function (err) { + if (err) { + return callback(err); + } + async.eachLimit(Object.keys(Meta.js.scripts.modules), 1000, function (relPath, next) { + var filePath = path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]); + var destPath = path.join(__dirname, '../../build/public/src/modules', relPath); - Meta.js.bridgeModules = function (app, callback) { - // Add routes for AMD-type modules to serve those files - function addRoute(relPath) { - var relativePath = nconf.get('relative_path'); + mkdirp(path.dirname(destPath), function (err) { + if (err) { + return next(err); + } - app.get(relativePath + '/src/modules/' + relPath, function (req, res) { - return res.sendFile(path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]), { - maxAge: app.enabled('cache') ? 5184000000 : 0 + file.link(filePath, destPath, next); }); - }); - } - - var numBridged = 0; + }, callback); + }); + }; - for(var relPath in Meta.js.scripts.modules) { - if (Meta.js.scripts.modules.hasOwnProperty(relPath)) { - addRoute(relPath); - ++numBridged; + Meta.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); + + mkdirp(path.dirname(destDir), function (err) { + if (err) { + return next(err); + } - winston.verbose('[meta/js] ' + numBridged + ' of ' + Object.keys(Meta.js.scripts.modules).length + ' modules bridged'); - callback(); + file.linkDirs(sourceDir, destDir, next); + }); + }, callback); + }); }; Meta.js.minify = function (target, callback) { @@ -139,12 +157,7 @@ module.exports = function (Meta) { winston.verbose('[meta/js] ' + target + ' minification complete'); minifier.kill(); - if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) { - return Meta.js.commitToFile(target, callback); - } else { - return callback(); - } - + Meta.js.commitToFile(target, callback); break; case 'error': winston.error('[meta/js] Could not compile ' + target + ': ' + message.message); @@ -203,48 +216,11 @@ module.exports = function (Meta) { }; Meta.js.commitToFile = function (target, callback) { - fs.writeFile(path.join(__dirname, '../../public/' + target), Meta.js.target[target].cache, function (err) { + fs.writeFile(path.join(__dirname, '../../build/public/' + target), Meta.js.target[target].cache, function (err) { callback(err); }); }; - Meta.js.getFromFile = function (target, callback) { - function readFile(filePath, next) { - fs.readFile(filePath, function (err, contents) { - if (err) { - if (err.code === 'ENOENT') { - if (!filePath.endsWith('.map')) { - winston.warn('[meta/js] ' + filePath + ' not found on disk, did you run ./nodebb build?'); - } - return next(null, ''); - } - } - next(err, contents); - }); - } - - var scriptPath = path.join(nconf.get('base_dir'), 'public/' + target); - var mapPath = path.join(nconf.get('base_dir'), 'public/' + target + '.map'); - - async.parallel({ - script: function (next) { - readFile(scriptPath, next); - }, - map: function (next) { - readFile(mapPath, next); - } - }, function (err, results) { - if (err) { - return callback(err); - } - Meta.js.target[target] = { - cache: results.script, - map: results.map - }; - callback(); - }); - }; - function setupDebugging() { /** * Check if the parent process is running with the debug option --debug (or --debug-brk) diff --git a/src/meta/sounds.js b/src/meta/sounds.js index 6068f16f5f..e44f43cccc 100644 --- a/src/meta/sounds.js +++ b/src/meta/sounds.js @@ -8,6 +8,7 @@ var rimraf = require('rimraf'); var mkdirp = require('mkdirp'); var async = require('async'); +var file = require('../file'); var plugins = require('../plugins'); var db = require('../database'); @@ -28,7 +29,7 @@ module.exports = function (Meta) { Meta.sounds.getFiles = function (callback) { async.waterfall([ function (next) { - fs.readdir(path.join(__dirname, '../../public/sounds'), next); + fs.readdir(path.join(__dirname, '../../build/public/sounds'), next); }, function (sounds, next) { fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), function (err, uploaded) { @@ -88,7 +89,7 @@ module.exports = function (Meta) { }; function setupSounds(callback) { - var soundsPath = path.join(__dirname, '../../public/sounds'); + var soundsPath = path.join(__dirname, '../../build/public/sounds'); async.waterfall([ function (next) { @@ -107,21 +108,6 @@ module.exports = function (Meta) { return; } - if (nconf.get('local-assets') === false) { - // Don't regenerate the public/sounds/ directory. Instead, create a mapping for the router to use - Meta.sounds._filePathHash = filePaths.reduce(function (hash, filePath) { - hash[path.basename(filePath)] = filePath; - return hash; - }, {}); - - winston.verbose('[sounds] Sounds OK'); - if (typeof next === 'function') { - return next(); - } else { - return; - } - } - // Clear the sounds directory async.series([ function (next) { @@ -138,11 +124,7 @@ module.exports = function (Meta) { // Link paths async.each(filePaths, function (filePath, next) { - if (process.platform === 'win32') { - fs.link(filePath, path.join(soundsPath, path.basename(filePath)), next); - } else { - fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file', next); - } + file.link(filePath, path.join(soundsPath, path.basename(filePath)), next); }, function (err) { if (!err) { winston.verbose('[sounds] Sounds OK'); diff --git a/src/meta/themes.js b/src/meta/themes.js index be48d1e70d..e19dcdbf4c 100644 --- a/src/meta/themes.js +++ b/src/meta/themes.js @@ -48,7 +48,7 @@ module.exports = function (Meta) { if (configObj.screenshot) { configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id; } else { - configObj.screenshot_url = nconf.get('relative_path') + '/images/themes/default.png'; + configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png'; } next(null, configObj); } catch (err) { diff --git a/src/middleware/admin.js b/src/middleware/admin.js index 3240eaf0af..9d4b43bf25 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -101,7 +101,7 @@ module.exports = function (middleware) { plugins: results.custom_header.plugins, authentication: results.custom_header.authentication, scripts: results.scripts, - 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', + 'cache-buster': meta.config['cache-buster'] || '', env: process.env.NODE_ENV ? true : false, title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel', bodyClass: data.bodyClass diff --git a/src/middleware/header.js b/src/middleware/header.js index c1b4175b5b..5e78416a0a 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -46,7 +46,7 @@ module.exports = function (middleware) { bootswatchCSS: meta.config['theme:src'], title: meta.config.title || '', description: meta.config.description || '', - 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', + 'cache-buster': meta.config['cache-buster'] || '', 'brand:logo': meta.config['brand:logo'] || '', 'brand:logo:url': meta.config['brand:logo:url'] || '', 'brand:logo:alt': meta.config['brand:logo:alt'] || '', diff --git a/src/plugins/install.js b/src/plugins/install.js index a914a09ca6..840d46008a 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -102,7 +102,7 @@ module.exports = function (Plugins) { } function runNpmCommand(command, pkgName, version, callback) { - require('child_process').execFile('npm', [command, pkgName + (command === 'install' ? '@' + version : '')], function (err, stdout) { + require('child_process').execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', [command, pkgName + (command === 'install' ? '@' + version : '')], function (err, stdout) { if (err) { return callback(err); } diff --git a/src/plugins/load.js b/src/plugins/load.js index 3121f8457e..01dedc4e45 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -54,7 +54,9 @@ module.exports = function (Plugins) { async.parallel([ async.apply(mapFiles, pluginData, 'css', 'cssFiles'), async.apply(mapFiles, pluginData, 'less', 'lessFiles'), - async.apply(mapClientSideScripts, pluginData) + async.apply(mapClientSideScripts, pluginData), + async.apply(mapClientModules, pluginData), + async.apply(mapStaticDirectories, pluginData, pluginData.path), ], next); }, next); } @@ -298,6 +300,7 @@ module.exports = function (Plugins) { pluginData.version = packageData.version; pluginData.repository = packageData.repository; pluginData.nbbpm = packageData.nbbpm; + pluginData.path = pluginPath; } catch(err) { var pluginDir = pluginPath.split(path.sep); pluginDir = pluginDir[pluginDir.length - 1]; diff --git a/src/routes/index.js b/src/routes/index.js index 6e0371a59c..8054031db3 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -15,7 +15,6 @@ var metaRoutes = require('./meta'); var apiRoutes = require('./api'); var adminRoutes = require('./admin'); var feedRoutes = require('./feeds'); -var pluginRoutes = require('./plugins'); var authRoutes = require('./authentication'); var helpers = require('./helpers'); @@ -121,7 +120,6 @@ module.exports = function (app, middleware, hotswapIds) { metaRoutes(router, middleware, controllers); apiRoutes(router, middleware, controllers); feedRoutes(router, middleware, controllers); - pluginRoutes(router, middleware, controllers); mainRoutes(router, middleware, controllers); topicRoutes(router, middleware, controllers); @@ -143,10 +141,44 @@ module.exports = function (app, middleware, hotswapIds) { } app.use(middleware.privateUploads); - app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../', 'build/public'), { + + app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../build/public'), { + maxAge: app.enabled('cache') ? 5184000000 : 0 + })); + app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../public'), { + maxAge: app.enabled('cache') ? 5184000000 : 0 + })); + // TODO: deprecate? + app.use(relativePath + '/plugins', express.static(path.join(__dirname, '../../build/public/plugins'), { maxAge: app.enabled('cache') ? 5184000000 : 0 })); + // DEPRECATED + var deprecatedPaths = [ + '/nodebb.min.js', + '/acp.min.js', + '/stylesheet.css', + '/js-enabled.css', + '/admin.css', + '/logo.png', + '/favicon.ico', + '/vendor/', + '/uploads/', + '/templates/', + '/src/', + '/images/', + '/language/', + '/sounds/', + ]; + app.use(relativePath, function (req, res, next) { + if (deprecatedPaths.some(function (path) { return req.path.startsWith(path); })) { + winston.warn('[deprecated] Accessing `' + req.path.slice(1) + '` from `/` is deprecated. ' + + 'Use `/assets' + req.path + '` to access this file.'); + res.redirect(relativePath + '/assets' + req.path + '?' + meta.config['cache-buster']); + } else { + next(); + } + }); // DEPRECATED app.use(relativePath + '/api/language', function (req, res) { winston.warn('[deprecated] Accessing language files from `/api/language` is deprecated. ' + @@ -154,10 +186,7 @@ module.exports = function (app, middleware, hotswapIds) { res.redirect(relativePath + '/assets/language' + req.path + '.json?' + meta.config['cache-buster']); }); - app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), { - maxAge: app.enabled('cache') ? 5184000000 : 0 - })); - app.use(relativePath + '/vendor/jquery/timeago/locales', middleware.processTimeagoLocales); + app.use(relativePath + '/assets/vendor/jquery/timeago/locales', middleware.processTimeagoLocales); app.use(controllers.handle404); app.use(controllers.handleURIErrors); app.use(controllers.handleErrors); diff --git a/src/routes/meta.js b/src/routes/meta.js index c62465c8d0..cb089f1200 100644 --- a/src/routes/meta.js +++ b/src/routes/meta.js @@ -1,51 +1,6 @@ "use strict"; -var path = require('path'); -var nconf = require('nconf'); - -var meta = require('../meta'); - - -function sendMinifiedJS(req, res) { - var target = path.basename(req.path); - var cache = meta.js.target[target] ? meta.js.target[target].cache : ''; - res.type('text/javascript').send(cache); -} - -// The portions of code involving the source map are commented out as they're broken in UglifyJS2 -// Follow along here: https://github.com/mishoo/UglifyJS2/issues/700 -// function sendJSSourceMap(req, res) { -// if (meta.js.hasOwnProperty('map')) { -// res.type('application/json').send(meta.js.map); -// } else { -// res.redirect(404); -// } -// }; - -function sendStylesheet(req, res) { - res.type('text/css').status(200).send(meta.css.cache); -} - -function sendACPStylesheet(req, res) { - res.type('text/css').status(200).send(meta.css.acpCache); -} - -function sendSoundFile(req, res, next) { - var resolved = meta.sounds._filePathHash[path.basename(req.path)]; - - if (resolved) { - res.status(200).sendFile(resolved); - } else { - next(); - } -} - module.exports = function (app, middleware, controllers) { - app.get('/stylesheet.css', middleware.addExpiresHeaders, sendStylesheet); - app.get('/admin.css', middleware.addExpiresHeaders, sendACPStylesheet); - app.get('/nodebb.min.js', middleware.addExpiresHeaders, sendMinifiedJS); - app.get('/acp.min.js', middleware.addExpiresHeaders, sendMinifiedJS); - // app.get('/nodebb.min.js.map', middleware.addExpiresHeaders, sendJSSourceMap); app.get('/sitemap.xml', controllers.sitemap.render); app.get('/sitemap/pages.xml', controllers.sitemap.getPages); app.get('/sitemap/categories.xml', controllers.sitemap.getCategories); @@ -53,8 +8,4 @@ module.exports = function (app, middleware, controllers) { app.get('/robots.txt', controllers.robots); app.get('/manifest.json', controllers.manifest); app.get('/css/previews/:theme', controllers.admin.themes.get); - - if (nconf.get('local-assets') === false) { - app.get('/sounds/*', middleware.addExpiresHeaders, sendSoundFile); - } }; diff --git a/src/routes/plugins.js b/src/routes/plugins.js deleted file mode 100644 index 37e23c5ee6..0000000000 --- a/src/routes/plugins.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict"; - -var _ = require('underscore'); -var path = require('path'); - -var plugins = require('../plugins'); - -module.exports = function (app, middleware, controllers) { - // Static Assets - app.get('/plugins/:id/*', middleware.addExpiresHeaders, function (req, res, next) { - - var relPath = req._parsedUrl.pathname.replace('/plugins/', ''); - - var matches = _.map(plugins.staticDirs, function (realPath, mappedPath) { - if (relPath.match(mappedPath)) { - var pathToFile = path.join(plugins.staticDirs[mappedPath], decodeURIComponent(relPath.slice(mappedPath.length))); - if (pathToFile.startsWith(plugins.staticDirs[mappedPath])) { - return pathToFile; - } - } - - return null; - }).filter(Boolean); - - if (!matches || !matches.length) { - return next(); - } - - res.sendFile(matches[0], {}, function (err) { - if (err) { - if (err.code === 'ENOENT') { - // File doesn't exist, this isn't an error, to send to 404 handler - return next(); - } else { - return next(err); - } - } - }); - }); -}; \ No newline at end of file diff --git a/src/user/picture.js b/src/user/picture.js index b790ef77c2..bc0b8f597f 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -57,7 +57,8 @@ module.exports = function (User) { path: picture.path, extension: extension, width: imageDimension, - height: imageDimension + height: imageDimension, + write: false, }, next); }, function (next) { diff --git a/src/views/admin/header.tpl b/src/views/admin/header.tpl index e6e92490f9..23ce1e2c02 100644 --- a/src/views/admin/header.tpl +++ b/src/views/admin/header.tpl @@ -4,8 +4,8 @@
[[admin/settings/group:default-cover-help]]
-[[admin/settings/uploads:default-covers-help]]
- + diff --git a/src/webserver.js b/src/webserver.js index 4e103d4a11..ee2605cdb2 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -45,7 +45,7 @@ server.on('error', function (err) { winston.error(err); if (err.code === 'EADDRINUSE') { winston.error('NodeBB address in use, exiting...'); - process.exit(0); + process.exit(1); } else { throw err; } @@ -88,7 +88,6 @@ function initializeNodeBB(callback) { plugins.init(app, middleware, next); }, async.apply(plugins.fireHook, 'static:assets.prepare', {}), - async.apply(meta.js.bridgeModules, app), function (next) { plugins.fireHook('static:app.preload', { app: app, @@ -104,9 +103,6 @@ function initializeNodeBB(callback) { }, function (next) { async.series([ - async.apply(meta.js.getFromFile, 'nodebb.min.js'), - async.apply(meta.js.getFromFile, 'acp.min.js'), - async.apply(meta.css.getFromFile), async.apply(meta.sounds.init), async.apply(languages.init), async.apply(meta.blacklist.load) diff --git a/src/widgets/admin.js b/src/widgets/admin.js index fc380804c6..ecfd73b750 100644 --- a/src/widgets/admin.js +++ b/src/widgets/admin.js @@ -3,6 +3,7 @@ var fs = require('fs'); var path = require('path'); var async = require('async'); +var nconf = require('nconf'); var plugins = require('../plugins'); var admin = {}; @@ -25,7 +26,7 @@ admin.get = function (callback) { plugins.fireHook('filter:widgets.getWidgets', [], next); }, adminTemplate: function (next) { - fs.readFile(path.resolve(__dirname, '../../public/templates/admin/partials/widget-settings.tpl'), 'utf8', next); + fs.readFile(path.resolve(nconf.get('views_dir'), 'admin/partials/widget-settings.tpl'), 'utf8', next); } }, function (err, widgetData) { if (err) { diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 82c40116fd..fa17493ae2 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -20,7 +20,7 @@ base_dir: path.join(__dirname,'../..'), themes_path: path.join(__dirname, '../../node_modules'), upload_url: path.join(path.sep, '../../uploads', path.sep), - views_dir: path.join(__dirname, '../../public/templates'), + views_dir: path.join(__dirname, '../../build/public/templates'), relative_path: '' });