diff --git a/lib/middleware/webpack.js b/lib/middleware/webpack.js index 30dda3bc..af3216c2 100644 --- a/lib/middleware/webpack.js +++ b/lib/middleware/webpack.js @@ -1,22 +1,112 @@ -var webpackMiddleware = require('webpack-dev-middleware') +var path = require('path') +var url = require('url') + var webpack = require('webpack') +var mime = require('mime') +var Promise = require('bluebird') var _ = require('lodash') +var MemoryOutputFileSystem = require('webpack/lib/MemoryOutputFileSystem') +var MemoryInputFileSystem = + require('webpack/node_modules/enhanced-resolve/lib/MemoryInputFileSystem') -var webpackOptions = require('../../webpack.config.js') +var logger = require('../util/logger') +var lifecycle = require('../util/lifecycle') +var globalOptions = require('../../webpack.config') -var overrideOptions = { - debug: true, - devtool: 'eval' -} +// Similar to webpack-dev-middleware, but integrates with our custom +// lifecycle, behaves more like normal express middleware, and removes +// all unnecessary features. +module.exports = function(options) { + var log = logger.createLogger('middleware:webpack') + options = _.defaults(options || {}, globalOptions) -var finalOptions = _.assign(webpackOptions, overrideOptions) + var storage = Object.create(null) + var fs = new MemoryInputFileSystem(storage) -module.exports = webpackMiddleware(webpack(finalOptions), { - noInfo: false, - quiet: false, - lazy: false, - publicPath: '/static/build/', - stats: { - colors: true + var compiler = webpack(options) + compiler.outputFileSystem = new MemoryOutputFileSystem(storage) + + var valid = false + var queue = [] + + log.info('Creating bundle') + var watching = compiler.watch(options.watchDelay, function(err) { + if (err) { + log.fatal('Webpack had an error', err.stack) + lifecycle.fatal() + } + }) + + lifecycle.observe(function() { + watching.watcher.close() + }) + + function doneListener(stats) { + process.nextTick(function() { + if (valid) { + log.info(stats.toString(options.stats)) + + if (stats.hasErrors()) { + log.error('Bundle has errors') + } + else if (stats.hasWarnings()) { + log.warn('Bundle has warnings') + } + else { + log.info('Bundle is now valid') + } + + queue.forEach(function(resolver) { + resolver.resolve() + }) + } + }) + + valid = true } -}) + + function invalidate() { + if (valid) { + log.info('Bundle is now invalid') + valid = false + } + } + + compiler.plugin('done', doneListener) + compiler.plugin('invalid', invalidate) + compiler.plugin('compile', invalidate) + + function bundle() { + if (valid) { + return Promise.resolve() + } + else { + log.info('Waiting for bundle to finish') + var resolver = Promise.defer() + queue.push(resolver) + return resolver.promise + } + } + + return function(req, res, next) { + var parsedUrl = url.parse(req.url) + + var target = path.join( + compiler.outputPath + , parsedUrl.pathname + ) + + bundle() + .then(function() { + try { + var body = fs.readFileSync(target) + res.set('Content-Type', mime.lookup(target)) + res.end(body) + } + catch (err) { + return next() + } + }) + .catch(next) + } +} diff --git a/lib/roles/app.js b/lib/roles/app.js index 1010e07c..72f623c2 100644 --- a/lib/roles/app.js +++ b/lib/roles/app.js @@ -45,6 +45,13 @@ module.exports = function(options) { io.set('browser client', false) io.set('transports', ['websocket']) + if (!options.disableWatch) { + app.use('/static/build', webpack({ + debug: true + , devtool: 'eval' + })) + } + app.use('/static/bower_components', express.static(pathutil.resource('bower_components'))) app.use('/static/data', express.static(pathutil.resource('data'))) @@ -54,10 +61,6 @@ module.exports = function(options) { app.use(express.favicon(pathutil.resource( 'bower_components/stf-graphics/logo/exports/STF-128.png'))) - if (!options.disableWatch) { - app.use(webpack) - } - app.use(express.cookieParser(options.secret)) app.use(express.cookieSession({ key: options.ssid diff --git a/webpack.config.js b/webpack.config.js index 1c63e094..683cc0ca 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,9 @@ module.exports = { , publicPath: '/static/build/' , filename: 'bundle.js' } +, stats: { + colors: true + } , resolve: { modulesDirectories: [ pathutil.resource('bower_components')