From 4e12c4505759fa782b4bb65f5a8b999ea84ad382 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Mar 2016 17:22:56 -0700 Subject: [PATCH] Add webpack cold reloading On changes to the react bundle webpack will store the current redux state in localStorage, waits (to allow the server to restart) then refreshes the page. On page load, it checks if it has state stored and loads it into the app. --- client/cold-reload.js | 16 +++++++ client/index.js | 66 +++++++++++++------------- gulpfile.js | 74 +++++++++++++----------------- package.json | 1 + server/boot/a-react.js | 13 +++--- server/middlewares/csp.js | 6 ++- server/middlewares/jade-helpers.js | 9 ++++ server/server.js | 2 + server/views/layout-react.jade | 2 +- webpack.config.js | 8 ++-- 10 files changed, 114 insertions(+), 83 deletions(-) create mode 100644 client/cold-reload.js diff --git a/client/cold-reload.js b/client/cold-reload.js new file mode 100644 index 0000000000..4f3fc5e63b --- /dev/null +++ b/client/cold-reload.js @@ -0,0 +1,16 @@ +import store from 'store'; + +const key = '__cold-storage__'; +export function isColdStored() { + return store.has(key); +} + +export function getColdStorage() { + const coldReloadData = store.get(key); + store.remove(key); + return coldReloadData; +} + +export function saveToColdStorage(data) { + store.set(key, data); +} diff --git a/client/index.js b/client/index.js index bb31086834..4fce290f7f 100644 --- a/client/index.js +++ b/client/index.js @@ -6,7 +6,7 @@ import { Router } from 'react-router'; import { routeReducer as routing, syncHistory } from 'react-router-redux'; import { createHistory } from 'history'; -import app$ from '../common/app'; +import createApp from '../common/app'; import provideStore from '../common/app/provide-store'; // client specific sagas @@ -14,20 +14,26 @@ import sagas from './sagas'; // render to observable import render from '../common/app/utils/render'; +import { + isColdStored, + getColdStorage, + saveToColdStorage +} from './cold-reload'; + +Rx.config.longStackSupport = !!debug.enabled; const log = debug('fcc:client'); const DOMContainer = document.getElementById('fcc'); -const initialState = window.__fcc__.data; +const hotReloadTimeout = 5000; const csrfToken = window.__fcc__.csrf.token; +const initialState = isColdStored() ? + getColdStorage() : + window.__fcc__.data; initialState.app.csrfToken = csrfToken; const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } }; -Rx.config.longStackSupport = !!debug.enabled; const history = createHistory(); -const appLocation = history.createLocation( - location.pathname + location.search -); const routingMiddleware = syncHistory(history); const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f; @@ -35,37 +41,35 @@ const shouldRouterListenForReplays = !!window.devToolsExtension; const clientSagaOptions = { doc: document }; -// returns an observable -app$({ - location: appLocation, - history, - serviceOptions, - initialState, - middlewares: [ - routingMiddleware, - ...sagas.map(saga => saga(clientSagaOptions)) - ], - reducers: { routing }, - enhancers: [ devTools ] -}) - .flatMap(({ props, store }) => { - - // because of weirdness in react-routers match function - // we replace the wrapped returned in props with the first one - // we passed in. This might be fixed in react-router 2.0 - props.history = history; +createApp({ + history, + serviceOptions, + initialState, + middlewares: [ + routingMiddleware, + ...sagas.map(saga => saga(clientSagaOptions)) + ], + reducers: { routing }, + enhancers: [ devTools ] + }) + .doOnNext(({ store }) => { if (shouldRouterListenForReplays && store) { log('routing middleware listening for replays'); routingMiddleware.listenForReplays(store); } - - log('rendering'); - return render( - provideStore(React.createElement(Router, props), store), - DOMContainer - ); + if (module.hot && typeof module.hot.accept === 'function') { + module.hot.accept('../common/app', function() { + saveToColdStorage(store.getState()); + setTimeout(() => window.location.reload(), hotReloadTimeout); + }); + } }) + .doOnNext(() => log('rendering')) + .flatMap(({ props, store }) => render( + provideStore(React.createElement(Router, props), store), + DOMContainer + )) .subscribe( () => debug('react rendered'), err => { throw err; }, diff --git a/gulpfile.js b/gulpfile.js index 2426e34a88..f2a2989b4a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,7 +21,9 @@ var Rx = require('rx'), sourcemaps = require('gulp-sourcemaps'), // react app - webpack = require('webpack-stream'), + webpack = require('webpack'), + webpackStream = require('webpack-stream'), + WebpackDevServer = require('webpack-dev-server'), webpackConfig = require('./webpack.config.js'), webpackConfigNode = require('./webpack.config.node.js'), @@ -55,7 +57,6 @@ var paths = { serverIgnore: [ 'gulpfile.js', 'public/', - '!public/js/bundle*', 'node_modules/', 'client/', 'seed', @@ -225,7 +226,6 @@ var syncDepenedents = [ 'js', 'less', 'dependents', - 'pack-watch', 'build-manifest' ]; @@ -269,7 +269,7 @@ gulp.task('pack-client', function() { return gulp.src(webpackConfig.entry) .pipe(plumber({ errorHandler: errorHandler })) - .pipe(webpack(Object.assign( + .pipe(webpackStream(Object.assign( {}, webpackConfig, webpackOptions @@ -289,51 +289,47 @@ gulp.task('pack-client', function() { .pipe(gulp.dest(paths.manifest)); }); -var defaultStatsOptions = { - colors: gutil.colors.supportsColor, - hash: false, - timings: false, - chunks: false, - chunkModules: false, - modules: false, - children: true, - version: true, - cached: false, - cachedAssets: false, - reasons: false, - source: false, - errorDetails: false -}; - var webpackCalled = false; -gulp.task('pack-watch', function(cb) { +gulp.task('webpack-dev-server', function(cb) { if (webpackCalled) { - console.log('webpack watching already runnning'); + console.log('webpack dev server already runnning'); return cb(); } - gulp.src(webpackConfig.entry) - .pipe(plumber({ errorHandler: errorHandler })) - .pipe(webpack(Object.assign( - {}, - webpackConfig, - webpackOptions, - { watch: true } - ), null, function(notUsed, stats) { - if (stats) { - gutil.log(stats.toString(defaultStatsOptions)); + var devServerOptions = { + headers: { + 'Access-Control-Allow-Credentials': 'true' + }, + hot: true, + noInfo: true, + contentBase: false, + publicPath: '/js' + }; + webpackConfig.entry = [ + 'webpack-dev-server/client?http://localhost:2999/', + 'webpack/hot/dev-server' + ].concat(webpackConfig.entry); + + var compiler = webpack(webpackConfig); + var devServer = new WebpackDevServer(compiler, devServerOptions); + devServer.use(function(req, res, next) { + res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*'); + next(); + }); + return devServer.listen('2999', 'localhost', function(err) { + if (err) { + throw new gutil.PluginError('webpack-dev-server', err); } if (!webpackCalled) { - debug('webpack init completed'); + gutil.log('[webpack-dev-server]', 'webpack init completed'); webpackCalled = true; cb(); } - })) - .pipe(gulp.dest(webpackConfig.output.path)); + }); }); -gulp.task('pack-watch-manifest', ['pack-watch'], function() { +gulp.task('pack-watch-manifest', function() { var manifestName = 'react-manifest.json'; var dest = webpackConfig.output.path; return gulp.src(dest + '/bundle.js') @@ -520,8 +516,6 @@ var watchDependents = [ 'dependents', 'serve', 'sync', - 'pack-watch', - 'pack-watch-manifest', 'build-manifest' ]; @@ -540,14 +534,12 @@ gulp.task('watch', watchDependents, function() { ['dependents'] ); gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']); - gulp.watch(webpackConfig.output.path + '/bundle.js', ['pack-watch-manifest']); }); gulp.task('default', [ 'less', 'serve', - 'pack-watch', - 'pack-watch-manifest', + 'webpack-dev-server', 'build-manifest-watch', 'watch', 'sync' diff --git a/package.json b/package.json index 7b6f4a0c60..ca604a5acd 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "url-regex": "^3.0.0", "validator": "^5.0.0", "webpack": "^1.9.12", + "webpack-dev-server": "^1.14.1", "webpack-stream": "^3.1.0", "xss-filters": "^1.2.6", "yargs": "^4.1.0", diff --git a/server/boot/a-react.js b/server/boot/a-react.js index ff7411a09c..9f9dc6e4f6 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -5,7 +5,7 @@ import debug from 'debug'; import renderToString from '../../common/app/utils/render-to-string'; import provideStore from '../../common/app/provide-store'; -import app$ from '../../common/app'; +import createApp from '../../common/app'; const log = debug('fcc:react-server'); @@ -14,7 +14,8 @@ const log = debug('fcc:react-server'); const routes = [ '/videos', '/videos/*', - '/challenges' + '/challenges', + '/map' ]; const devRoutes = []; @@ -37,9 +38,9 @@ export default function reactSubRouter(app) { function serveReactApp(req, res, next) { const serviceOptions = { req }; - app$({ - location: req.path, - serviceOptions + createApp({ + serviceOptions, + location: req.path }) // if react-router does not find a route send down the chain .filter(({ redirect, props }) => { @@ -47,7 +48,7 @@ export default function reactSubRouter(app) { res.redirect(redirect.pathname + redirect.search); } if (!props) { - log(`react tried to find ${location.pathname} but got 404`); + log(`react tried to find ${req.path} but got 404`); return next(); } return !!props; diff --git a/server/middlewares/csp.js b/server/middlewares/csp.js index da71631824..42a327f32a 100644 --- a/server/middlewares/csp.js +++ b/server/middlewares/csp.js @@ -5,7 +5,11 @@ let trusted = [ ]; if (process.env.NODE_ENV !== 'production') { - trusted.push('ws://localhost:3001'); + trusted = trusted.concat([ + 'ws://localhost:3001', + 'http://localhost:2999', + 'ws://localhost:2999' + ]); } export default function csp() { diff --git a/server/middlewares/jade-helpers.js b/server/middlewares/jade-helpers.js index 5bfbaf55fa..c8625c34be 100644 --- a/server/middlewares/jade-helpers.js +++ b/server/middlewares/jade-helpers.js @@ -1,4 +1,7 @@ const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint):\s/i; +import config from '../../webpack.config'; + +const __DEV__ = process.env.NODE_ENV !== 'production'; export default function jadeHelpers() { return function jadeHelpersMiddleware(req, res, next) { @@ -6,6 +9,12 @@ export default function jadeHelpers() { return str.replace(challengesRegex, ''); }; + res.locals.getBundleLocation = function getBundleLocation() { + return __DEV__ ? + config.output.publicPath + '/bundle.js' : + 'js/bundle.js'; + }; + next(); }; } diff --git a/server/server.js b/server/server.js index 1d5f9aefda..7958853ecb 100755 --- a/server/server.js +++ b/server/server.js @@ -3,12 +3,14 @@ var pmx = require('pmx'); pmx.init(); var _ = require('lodash'), + Rx = require('rx'), loopback = require('loopback'), boot = require('loopback-boot'), expressState = require('express-state'), path = require('path'), setupPassport = require('./component-passport'); +Rx.config.longStackSupport = process.env.NODE_DEBUG !== 'production'; var app = loopback(); var isBeta = !!process.env.BETA; diff --git a/server/views/layout-react.jade b/server/views/layout-react.jade index f31f102c9a..cd766c8a5a 100644 --- a/server/views/layout-react.jade +++ b/server/views/layout-react.jade @@ -10,4 +10,4 @@ html(lang='en') #fcc!= markup script!= state script(src=rev('/js', 'vendor-challenges.js')) - script(src=rev('/js', 'bundle.js')) + script(src=getBundleLocation()) diff --git a/webpack.config.js b/webpack.config.js index 718e7b26d9..8839a1492f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,15 +1,15 @@ var webpack = require('webpack'); var path = require('path'); -var webpack = require('webpack'); var __DEV__ = process.env.NODE_ENV !== 'production'; module.exports = { entry: './client', + devtool: 'inline-source-map', output: { filename: 'bundle.js', path: path.join(__dirname, '/public/js'), - publicPath: 'public/' + publicPath: __DEV__ ? 'http://localhost:2999/js' : '/js' }, module: { loaders: [ @@ -42,6 +42,8 @@ module.exports = { 'NODE_ENV': JSON.stringify(__DEV__ ? 'development' : 'production') }, '__DEVTOOLS__': !__DEV__ - }) + }), + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin() ] };