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.
This commit is contained in:
16
client/cold-reload.js
Normal file
16
client/cold-reload.js
Normal file
@ -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);
|
||||||
|
}
|
@ -6,7 +6,7 @@ import { Router } from 'react-router';
|
|||||||
import { routeReducer as routing, syncHistory } from 'react-router-redux';
|
import { routeReducer as routing, syncHistory } from 'react-router-redux';
|
||||||
import { createHistory } from 'history';
|
import { createHistory } from 'history';
|
||||||
|
|
||||||
import app$ from '../common/app';
|
import createApp from '../common/app';
|
||||||
import provideStore from '../common/app/provide-store';
|
import provideStore from '../common/app/provide-store';
|
||||||
|
|
||||||
// client specific sagas
|
// client specific sagas
|
||||||
@ -14,20 +14,26 @@ import sagas from './sagas';
|
|||||||
|
|
||||||
// render to observable
|
// render to observable
|
||||||
import render from '../common/app/utils/render';
|
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 log = debug('fcc:client');
|
||||||
const DOMContainer = document.getElementById('fcc');
|
const DOMContainer = document.getElementById('fcc');
|
||||||
const initialState = window.__fcc__.data;
|
const hotReloadTimeout = 5000;
|
||||||
const csrfToken = window.__fcc__.csrf.token;
|
const csrfToken = window.__fcc__.csrf.token;
|
||||||
|
const initialState = isColdStored() ?
|
||||||
|
getColdStorage() :
|
||||||
|
window.__fcc__.data;
|
||||||
initialState.app.csrfToken = csrfToken;
|
initialState.app.csrfToken = csrfToken;
|
||||||
|
|
||||||
const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } };
|
const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } };
|
||||||
|
|
||||||
Rx.config.longStackSupport = !!debug.enabled;
|
|
||||||
const history = createHistory();
|
const history = createHistory();
|
||||||
const appLocation = history.createLocation(
|
|
||||||
location.pathname + location.search
|
|
||||||
);
|
|
||||||
const routingMiddleware = syncHistory(history);
|
const routingMiddleware = syncHistory(history);
|
||||||
|
|
||||||
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
||||||
@ -35,37 +41,35 @@ const shouldRouterListenForReplays = !!window.devToolsExtension;
|
|||||||
|
|
||||||
const clientSagaOptions = { doc: document };
|
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) {
|
if (shouldRouterListenForReplays && store) {
|
||||||
log('routing middleware listening for replays');
|
log('routing middleware listening for replays');
|
||||||
routingMiddleware.listenForReplays(store);
|
routingMiddleware.listenForReplays(store);
|
||||||
}
|
}
|
||||||
|
if (module.hot && typeof module.hot.accept === 'function') {
|
||||||
log('rendering');
|
module.hot.accept('../common/app', function() {
|
||||||
return render(
|
saveToColdStorage(store.getState());
|
||||||
provideStore(React.createElement(Router, props), store),
|
setTimeout(() => window.location.reload(), hotReloadTimeout);
|
||||||
DOMContainer
|
});
|
||||||
);
|
}
|
||||||
})
|
})
|
||||||
|
.doOnNext(() => log('rendering'))
|
||||||
|
.flatMap(({ props, store }) => render(
|
||||||
|
provideStore(React.createElement(Router, props), store),
|
||||||
|
DOMContainer
|
||||||
|
))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => debug('react rendered'),
|
() => debug('react rendered'),
|
||||||
err => { throw err; },
|
err => { throw err; },
|
||||||
|
74
gulpfile.js
74
gulpfile.js
@ -21,7 +21,9 @@ var Rx = require('rx'),
|
|||||||
sourcemaps = require('gulp-sourcemaps'),
|
sourcemaps = require('gulp-sourcemaps'),
|
||||||
|
|
||||||
// react app
|
// react app
|
||||||
webpack = require('webpack-stream'),
|
webpack = require('webpack'),
|
||||||
|
webpackStream = require('webpack-stream'),
|
||||||
|
WebpackDevServer = require('webpack-dev-server'),
|
||||||
webpackConfig = require('./webpack.config.js'),
|
webpackConfig = require('./webpack.config.js'),
|
||||||
webpackConfigNode = require('./webpack.config.node.js'),
|
webpackConfigNode = require('./webpack.config.node.js'),
|
||||||
|
|
||||||
@ -55,7 +57,6 @@ var paths = {
|
|||||||
serverIgnore: [
|
serverIgnore: [
|
||||||
'gulpfile.js',
|
'gulpfile.js',
|
||||||
'public/',
|
'public/',
|
||||||
'!public/js/bundle*',
|
|
||||||
'node_modules/',
|
'node_modules/',
|
||||||
'client/',
|
'client/',
|
||||||
'seed',
|
'seed',
|
||||||
@ -225,7 +226,6 @@ var syncDepenedents = [
|
|||||||
'js',
|
'js',
|
||||||
'less',
|
'less',
|
||||||
'dependents',
|
'dependents',
|
||||||
'pack-watch',
|
|
||||||
'build-manifest'
|
'build-manifest'
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -269,7 +269,7 @@ gulp.task('pack-client', function() {
|
|||||||
|
|
||||||
return gulp.src(webpackConfig.entry)
|
return gulp.src(webpackConfig.entry)
|
||||||
.pipe(plumber({ errorHandler: errorHandler }))
|
.pipe(plumber({ errorHandler: errorHandler }))
|
||||||
.pipe(webpack(Object.assign(
|
.pipe(webpackStream(Object.assign(
|
||||||
{},
|
{},
|
||||||
webpackConfig,
|
webpackConfig,
|
||||||
webpackOptions
|
webpackOptions
|
||||||
@ -289,51 +289,47 @@ gulp.task('pack-client', function() {
|
|||||||
.pipe(gulp.dest(paths.manifest));
|
.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;
|
var webpackCalled = false;
|
||||||
gulp.task('pack-watch', function(cb) {
|
gulp.task('webpack-dev-server', function(cb) {
|
||||||
if (webpackCalled) {
|
if (webpackCalled) {
|
||||||
console.log('webpack watching already runnning');
|
console.log('webpack dev server already runnning');
|
||||||
return cb();
|
return cb();
|
||||||
}
|
}
|
||||||
gulp.src(webpackConfig.entry)
|
var devServerOptions = {
|
||||||
.pipe(plumber({ errorHandler: errorHandler }))
|
headers: {
|
||||||
.pipe(webpack(Object.assign(
|
'Access-Control-Allow-Credentials': 'true'
|
||||||
{},
|
},
|
||||||
webpackConfig,
|
hot: true,
|
||||||
webpackOptions,
|
noInfo: true,
|
||||||
{ watch: true }
|
contentBase: false,
|
||||||
), null, function(notUsed, stats) {
|
publicPath: '/js'
|
||||||
if (stats) {
|
};
|
||||||
gutil.log(stats.toString(defaultStatsOptions));
|
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) {
|
if (!webpackCalled) {
|
||||||
debug('webpack init completed');
|
gutil.log('[webpack-dev-server]', 'webpack init completed');
|
||||||
webpackCalled = true;
|
webpackCalled = true;
|
||||||
cb();
|
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 manifestName = 'react-manifest.json';
|
||||||
var dest = webpackConfig.output.path;
|
var dest = webpackConfig.output.path;
|
||||||
return gulp.src(dest + '/bundle.js')
|
return gulp.src(dest + '/bundle.js')
|
||||||
@ -520,8 +516,6 @@ var watchDependents = [
|
|||||||
'dependents',
|
'dependents',
|
||||||
'serve',
|
'serve',
|
||||||
'sync',
|
'sync',
|
||||||
'pack-watch',
|
|
||||||
'pack-watch-manifest',
|
|
||||||
'build-manifest'
|
'build-manifest'
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -540,14 +534,12 @@ gulp.task('watch', watchDependents, function() {
|
|||||||
['dependents']
|
['dependents']
|
||||||
);
|
);
|
||||||
gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']);
|
gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']);
|
||||||
gulp.watch(webpackConfig.output.path + '/bundle.js', ['pack-watch-manifest']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('default', [
|
gulp.task('default', [
|
||||||
'less',
|
'less',
|
||||||
'serve',
|
'serve',
|
||||||
'pack-watch',
|
'webpack-dev-server',
|
||||||
'pack-watch-manifest',
|
|
||||||
'build-manifest-watch',
|
'build-manifest-watch',
|
||||||
'watch',
|
'watch',
|
||||||
'sync'
|
'sync'
|
||||||
|
@ -135,6 +135,7 @@
|
|||||||
"url-regex": "^3.0.0",
|
"url-regex": "^3.0.0",
|
||||||
"validator": "^5.0.0",
|
"validator": "^5.0.0",
|
||||||
"webpack": "^1.9.12",
|
"webpack": "^1.9.12",
|
||||||
|
"webpack-dev-server": "^1.14.1",
|
||||||
"webpack-stream": "^3.1.0",
|
"webpack-stream": "^3.1.0",
|
||||||
"xss-filters": "^1.2.6",
|
"xss-filters": "^1.2.6",
|
||||||
"yargs": "^4.1.0",
|
"yargs": "^4.1.0",
|
||||||
|
@ -5,7 +5,7 @@ import debug from 'debug';
|
|||||||
import renderToString from '../../common/app/utils/render-to-string';
|
import renderToString from '../../common/app/utils/render-to-string';
|
||||||
import provideStore from '../../common/app/provide-store';
|
import provideStore from '../../common/app/provide-store';
|
||||||
|
|
||||||
import app$ from '../../common/app';
|
import createApp from '../../common/app';
|
||||||
|
|
||||||
const log = debug('fcc:react-server');
|
const log = debug('fcc:react-server');
|
||||||
|
|
||||||
@ -14,7 +14,8 @@ const log = debug('fcc:react-server');
|
|||||||
const routes = [
|
const routes = [
|
||||||
'/videos',
|
'/videos',
|
||||||
'/videos/*',
|
'/videos/*',
|
||||||
'/challenges'
|
'/challenges',
|
||||||
|
'/map'
|
||||||
];
|
];
|
||||||
|
|
||||||
const devRoutes = [];
|
const devRoutes = [];
|
||||||
@ -37,9 +38,9 @@ export default function reactSubRouter(app) {
|
|||||||
|
|
||||||
function serveReactApp(req, res, next) {
|
function serveReactApp(req, res, next) {
|
||||||
const serviceOptions = { req };
|
const serviceOptions = { req };
|
||||||
app$({
|
createApp({
|
||||||
location: req.path,
|
serviceOptions,
|
||||||
serviceOptions
|
location: req.path
|
||||||
})
|
})
|
||||||
// if react-router does not find a route send down the chain
|
// if react-router does not find a route send down the chain
|
||||||
.filter(({ redirect, props }) => {
|
.filter(({ redirect, props }) => {
|
||||||
@ -47,7 +48,7 @@ export default function reactSubRouter(app) {
|
|||||||
res.redirect(redirect.pathname + redirect.search);
|
res.redirect(redirect.pathname + redirect.search);
|
||||||
}
|
}
|
||||||
if (!props) {
|
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 next();
|
||||||
}
|
}
|
||||||
return !!props;
|
return !!props;
|
||||||
|
@ -5,7 +5,11 @@ let trusted = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
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() {
|
export default function csp() {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint):\s/i;
|
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() {
|
export default function jadeHelpers() {
|
||||||
return function jadeHelpersMiddleware(req, res, next) {
|
return function jadeHelpersMiddleware(req, res, next) {
|
||||||
@ -6,6 +9,12 @@ export default function jadeHelpers() {
|
|||||||
return str.replace(challengesRegex, '');
|
return str.replace(challengesRegex, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
res.locals.getBundleLocation = function getBundleLocation() {
|
||||||
|
return __DEV__ ?
|
||||||
|
config.output.publicPath + '/bundle.js' :
|
||||||
|
'js/bundle.js';
|
||||||
|
};
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,14 @@ var pmx = require('pmx');
|
|||||||
pmx.init();
|
pmx.init();
|
||||||
|
|
||||||
var _ = require('lodash'),
|
var _ = require('lodash'),
|
||||||
|
Rx = require('rx'),
|
||||||
loopback = require('loopback'),
|
loopback = require('loopback'),
|
||||||
boot = require('loopback-boot'),
|
boot = require('loopback-boot'),
|
||||||
expressState = require('express-state'),
|
expressState = require('express-state'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
setupPassport = require('./component-passport');
|
setupPassport = require('./component-passport');
|
||||||
|
|
||||||
|
Rx.config.longStackSupport = process.env.NODE_DEBUG !== 'production';
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
var isBeta = !!process.env.BETA;
|
var isBeta = !!process.env.BETA;
|
||||||
|
|
||||||
|
@ -10,4 +10,4 @@ html(lang='en')
|
|||||||
#fcc!= markup
|
#fcc!= markup
|
||||||
script!= state
|
script!= state
|
||||||
script(src=rev('/js', 'vendor-challenges.js'))
|
script(src=rev('/js', 'vendor-challenges.js'))
|
||||||
script(src=rev('/js', 'bundle.js'))
|
script(src=getBundleLocation())
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
var webpack = require('webpack');
|
var webpack = require('webpack');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var webpack = require('webpack');
|
|
||||||
|
|
||||||
var __DEV__ = process.env.NODE_ENV !== 'production';
|
var __DEV__ = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './client',
|
entry: './client',
|
||||||
|
devtool: 'inline-source-map',
|
||||||
output: {
|
output: {
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
path: path.join(__dirname, '/public/js'),
|
path: path.join(__dirname, '/public/js'),
|
||||||
publicPath: 'public/'
|
publicPath: __DEV__ ? 'http://localhost:2999/js' : '/js'
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
loaders: [
|
loaders: [
|
||||||
@ -42,6 +42,8 @@ module.exports = {
|
|||||||
'NODE_ENV': JSON.stringify(__DEV__ ? 'development' : 'production')
|
'NODE_ENV': JSON.stringify(__DEV__ ? 'development' : 'production')
|
||||||
},
|
},
|
||||||
'__DEVTOOLS__': !__DEV__
|
'__DEVTOOLS__': !__DEV__
|
||||||
})
|
}),
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new webpack.NoErrorsPlugin()
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user