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() ] };