commit 9f490661270886c025f18ff1cd2fb6c1941b0059
parent 10a0e82dc72677ed22637688622d6a2dc7c652be
Author: Konstantin Tarkus <hello@tarkus.me>
Date: Mon, 9 Feb 2015 01:08:23 +0300
Update server-side rendering logic and other misc stuff
- Refactor webpack config
- Generate source maps in debug mode
- Create two bundles during a build - `./build/app.js` (client-side) and `./build/server.js` (server-side)
- Replace the original JSX transpiler with 6to5
- Register core-js polyfills
- Clean up the top-level React component (App)
- Load page content asynchronously
- Remove `./src/images` folder and `images` Gulp task in favor of images-per-component
Closes #57, #55, #53, #4
Diffstat:
16 files changed, 177 insertions(+), 187 deletions(-)
diff --git a/config/webpack.js b/config/webpack.js
@@ -6,96 +6,122 @@
'use strict';
var webpack = require('webpack');
+var update = require('react/lib/update');
+var argv = require('minimist')(process.argv.slice(2));
-/**
- * Get configuration for Webpack
- *
- * @see http://webpack.github.io/docs/configuration
- * https://github.com/petehunt/webpack-howto
- *
- * @param {boolean} release True if configuration is intended to be used in
- * a release mode, false otherwise
- * @return {object} Webpack configuration
- */
-module.exports = function(release) {
- return {
- entry: './src/app.js',
+var DEBUG = !argv.release;
- output: {
- filename: 'app.js',
- path: './build/',
- publicPath: './build/'
- },
+// Common configuration for both
+// client-side and server-side bundles
+var config = {
+ output: {
+ path: './build/',
+ publicPath: './build/',
+ sourcePrefix: ' '
+ },
- cache: !release,
- debug: !release,
- devtool: false,
+ cache: DEBUG,
+ debug: DEBUG,
+ devtool: DEBUG ? '#inline-source-map' : false,
- stats: {
- colors: true,
- reasons: !release
- },
+ stats: {
+ colors: true,
+ reasons: DEBUG
+ },
- plugins: release ? [
- new webpack.DefinePlugin({
- 'process.env.NODE_ENV': '"production"',
- '__DEV__': false,
- '__SERVER__': false
- }),
- new webpack.optimize.DedupePlugin(),
- new webpack.optimize.UglifyJsPlugin(),
- new webpack.optimize.OccurenceOrderPlugin(),
- new webpack.optimize.AggressiveMergingPlugin()
- ] : [
- new webpack.DefinePlugin({
- '__DEV__': true,
- '__SERVER__': false
- })
- ],
+ plugins: DEBUG ? [
+ new webpack.DefinePlugin({
+ '__DEV__': true,
+ '__SERVER__': false
+ })
+ ] : [
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': '"production"',
+ '__DEV__': false,
+ '__SERVER__': false
+ }),
+ new webpack.optimize.DedupePlugin(),
+ new webpack.optimize.UglifyJsPlugin(),
+ new webpack.optimize.OccurenceOrderPlugin(),
+ new webpack.optimize.AggressiveMergingPlugin()
+ ],
- resolve: {
- extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx']
- },
+ resolve: {
+ extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx']
+ },
- module: {
- preLoaders: [
- {
- test: /\.js$/,
- exclude: /node_modules/,
- loader: 'jshint'
- }
- ],
+ module: {
+ preLoaders: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: 'jshint'
+ }
+ ],
- loaders: [
- {
- test: /\.css$/,
- loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:[' +
- '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' +
- '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}'
- },
- {
- test: /\.less$/,
- loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:[' +
- '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' +
- '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}!less-loader'
- },
- {
- test: /\.gif/,
- loader: 'url-loader?limit=10000&mimetype=image/gif'
- },
- {
- test: /\.jpg/,
- loader: 'url-loader?limit=10000&mimetype=image/jpg'
- },
- {
- test: /\.png/,
- loader: 'url-loader?limit=10000&mimetype=image/png'
- },
- {
- test: /\.jsx?$/,
- loader: 'jsx-loader?harmony&stripTypes'
- }
- ]
- }
- };
+ loaders: [
+ {
+ test: /\.css$/,
+ loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:[' +
+ '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' +
+ '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}'
+ },
+ {
+ test: /\.less$/,
+ loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:[' +
+ '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' +
+ '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}!less-loader'
+ },
+ {
+ test: /\.gif/,
+ loader: 'url-loader?limit=10000&mimetype=image/gif'
+ },
+ {
+ test: /\.jpg/,
+ loader: 'url-loader?limit=10000&mimetype=image/jpg'
+ },
+ {
+ test: /\.png/,
+ loader: 'url-loader?limit=10000&mimetype=image/png'
+ },
+ {
+ test: /\.jsx?$/,
+ exclude: /node_modules/,
+ loader: '6to5-loader'
+ }
+ ]
+ }
};
+
+// Configuration for the client-side bundle
+var appConfig = update(config, {
+ entry: {$set: './src/app.js'},
+ output: {filename: {$set: 'app.js'}}
+});
+
+// Configuration for the server-side bundle
+var serverConfig = update(config, {
+ entry: {$set: './src/server.js'},
+ output: {
+ filename: {$set: 'server.js'},
+ libraryTarget: {$set: 'commonjs2'}
+ },
+ target: {$set: 'node'},
+ externals: {$set: /^[a-z\-0-9]+$/},
+ node: {$set: {
+ console: true,
+ global: false,
+ process: false,
+ Buffer: false,
+ __filename: false,
+ __dirname: false
+ }},
+ module: {loaders: {$apply: function(loaders) {
+ loaders.forEach(function(loader) {
+ loader.loader = loader.loader.replace('style-loader!', '');
+ });
+ return loaders;
+ }}}
+});
+
+module.exports = [appConfig, serverConfig];
diff --git a/gulpfile.js b/gulpfile.js
@@ -52,26 +52,17 @@ gulp.task('vendor', function() {
// Static files
gulp.task('assets', function() {
- src.assets = 'src/assets/**';
+ src.assets = [
+ 'src/assets/**',
+ 'src/content*/**/*.*',
+ 'src/templates*/**/*.*'
+ ];
return gulp.src(src.assets)
.pipe($.changed(DEST))
.pipe(gulp.dest(DEST))
.pipe($.size({title: 'assets'}));
});
-// Images
-gulp.task('images', function() {
- src.images = 'src/images/**';
- return gulp.src(src.images)
- .pipe($.changed(DEST + '/images'))
- .pipe($.imagemin({
- progressive: true,
- interlaced: true
- }))
- .pipe(gulp.dest(DEST + '/images'))
- .pipe($.size({title: 'images'}));
-});
-
// CSS style sheets
gulp.task('styles', function() {
src.styles = 'src/styles/**/*.{css,less}';
@@ -92,7 +83,7 @@ gulp.task('styles', function() {
// Bundle
gulp.task('bundle', function(cb) {
var started = false;
- var config = require('./config/webpack.js')(RELEASE);
+ var config = require('./config/webpack.js');
var bundler = webpack(config);
function bundle(err, stats) {
@@ -119,7 +110,7 @@ gulp.task('bundle', function(cb) {
// Build the app from source code
gulp.task('build', ['clean'], function(cb) {
- runSequence(['vendor', 'assets', 'images', 'styles', 'bundle'], cb);
+ runSequence(['vendor', 'assets', 'styles', 'bundle'], cb);
});
// Launch a lightweight HTTP Server
@@ -133,8 +124,8 @@ gulp.task('serve', function(cb) {
runSequence('build', function() {
var server = require('nodemon')({
- script: 'src/server.js',
- watch: [path.join(__dirname, 'src/**/*.js')],
+ script: 'build/server.js',
+ watch: [path.join(__dirname, 'build/server.js')],
env: {NODE_ENV: 'development'}
}).on('log', function(log) {
$.util.log('nodemon', $.util.colors.green(log.message));
@@ -160,7 +151,6 @@ gulp.task('serve', function(cb) {
});
gulp.watch(src.assets, ['assets']);
- gulp.watch(src.images, ['images']);
gulp.watch(src.styles, ['styles']);
gulp.watch(DEST + '/**/*.*', function (file) {
browserSync.reload(path.relative(__dirname, file.path));
diff --git a/package.json b/package.json
@@ -6,6 +6,7 @@
"repository": "https://github.com/kriasoft/react-starter-kit",
"license": "MIT",
"dependencies": {
+ "6to5": "3.5.3",
"bootstrap": "3.3.1",
"director": "1.2.7",
"eventemitter3": "0.1.6",
@@ -18,6 +19,8 @@
"superagent": "0.21.0"
},
"devDependencies": {
+ "6to5-core": "^3.5.3",
+ "6to5-loader": "^3.0.0",
"autoprefixer-loader": "^1.1.0",
"browser-sync": "^1.9.0",
"css-loader": "^0.9.1",
@@ -47,7 +50,6 @@
"jshint": "^2.5.11",
"jshint-loader": "^0.8.0",
"jshint-stylish": "^1.0.0",
- "jsx-loader": "^0.12.2",
"less": "^2.2.0",
"less-loader": "^2.0.0",
"minimist": "^1.1.0",
diff --git a/src/actions/AppActions.js b/src/actions/AppActions.js
@@ -34,7 +34,7 @@ module.exports = {
.accept('application/json')
.end((err, res) => {
Dispatcher.handleServerAction({
- actionType: ActionTypes.LOAD_PAGE, path: path, err: err, page: res
+ actionType: ActionTypes.LOAD_PAGE, path: path, err: err, page: res.body
});
if (cb) {
cb();
diff --git a/src/app.js b/src/app.js
@@ -8,7 +8,10 @@
'use strict';
+require('6to5/polyfill');
+
var React = require('react');
+var emptyFunction = require('react/lib/emptyFunction');
var App = require('./components/App');
var Dispatcher = require('./core/Dispatcher');
var AppActions = require('./actions/AppActions');
@@ -17,36 +20,34 @@ var ActionTypes = require('./constants/ActionTypes');
// Export React so the dev tools can find it
(window !== window.top ? window.top : window).React = React;
-// Initial properties and callbacks
-// which should be passed into the top-level React component (App)
-var props = {
- path: decodeURI(window.location.pathname),
- onSetTitle: (title) => {
- document.title = title;
- },
- onSetMeta: (name, content) => {
- // Remove and create a new <meta /> tag in order to make it work
- // with bookmarks in Safari
- var elements = document.getElementsByTagName('meta');
- [].slice.call(elements).forEach((element) => {
- if (element.getAttribute('name') === name) {
- element.parentNode.removeChild(element);
- }
- });
- var meta = document.createElement('meta');
- meta.setAttribute('name', name);
- meta.setAttribute('content', content);
- document.getElementsByTagName('head')[0].appendChild(meta);
- },
- onPageNotFound: () => { /* do nothing */ }
+var path = decodeURI(window.location.pathname);
+var setMetaTag = (name, content) => {
+ // Remove and create a new <meta /> tag in order to make it work
+ // with bookmarks in Safari
+ var elements = document.getElementsByTagName('meta');
+ [].slice.call(elements).forEach((element) => {
+ if (element.getAttribute('name') === name) {
+ element.parentNode.removeChild(element);
+ }
+ });
+ var meta = document.createElement('meta');
+ meta.setAttribute('name', name);
+ meta.setAttribute('content', content);
+ document.getElementsByTagName('head')[0].appendChild(meta);
};
-// Render application when DOM is ready
-function startup() {
- // Render the top-level React component and mount it to the `document.body`
- var app = React.render(React.createElement(App, props), document.body);
+function run() {
+ // Render the top-level React component
+ var props = {
+ path: path,
+ onSetTitle: (title) => document.title = title,
+ onSetMeta: setMetaTag,
+ onPageNotFound: emptyFunction
+ };
+ var component = React.createElement(App, props);
+ var app = React.render(component, document.body);
- // Set Application.path property when `window.location` is changed
+ // Update `Application.path` prop when `window.location` is changed
Dispatcher.register((payload) => {
if (payload.action.actionType === ActionTypes.CHANGE_LOCATION) {
app.setProps({path: decodeURI(payload.action.path)});
@@ -54,11 +55,17 @@ function startup() {
});
}
-// Load page content
-AppActions.loadPage(props.path, () => {
- if (window.addEventListener) {
- window.addEventListener('DOMContentLoaded', startup, false);
- } else {
- window.attachEvent('onload', startup);
- }
-});
+// Run the application when both DOM is ready
+// and page content is loaded
+Promise.all([
+ new Promise((resolve) => {
+ if (window.addEventListener) {
+ window.addEventListener('DOMContentLoaded', resolve);
+ } else {
+ window.attachEvent('onload', resolve);
+ }
+ }),
+ new Promise((resolve) => {
+ AppActions.loadPage(path, resolve);
+ })
+]).then(run);
diff --git a/src/components/App/App.js b/src/components/App/App.js
@@ -11,7 +11,7 @@
require('./App.less');
var React = require('react');
-var ExecutionEnvironment = require('react/lib/ExecutionEnvironment');
+var invariant = require('react/lib/invariant');
var AppActions = require('../../actions/AppActions');
var NavigationMixin = require('./NavigationMixin');
var AppStore = require('../../stores/AppStore');
@@ -30,26 +30,9 @@ var Application = React.createClass({
onPageNotFound: React.PropTypes.func.isRequired
},
- getInitialState() {
- return {loading: false};
- },
-
- componentWillMount() {
- if (ExecutionEnvironment.canUseDOM) {
- this.setState({loading: true});
- AppActions.loadPage(this.props.path, () => {
- this.setState({loading: false});
- });
- }
- },
-
render() {
var page = AppStore.getPage(this.props.path);
-
- if (page === undefined) {
- return false;
- }
-
+ invariant(page !== undefined, 'Failed to load page content.');
this.props.onSetTitle(page.title);
if (page.type === 'notfound') {
diff --git a/src/components/App/NavigationMixin.js b/src/components/App/NavigationMixin.js
@@ -27,7 +27,6 @@ var NavigationMixin = {
},
handlePopState(event) {
- console.log('Application.handlePopState(' + (event.state ? event.state.path : '') + ')');
if (event.state) {
var path = event.state.path;
// TODO: Replace current location
@@ -85,7 +84,9 @@ var NavigationMixin = {
var path = el.pathname + el.search + (el.hash || '');
event.preventDefault();
- AppActions.navigateTo(path);
+ AppActions.loadPage(path, () => {
+ AppActions.navigateTo(path);
+ });
}
};
diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js
@@ -10,6 +10,8 @@
var React = require('react');
+var logo = require('./logo-small.png');
+
var Navbar = React.createClass({
render() {
@@ -18,7 +20,7 @@ var Navbar = React.createClass({
<div className="navbar-top" role="navigation">
<div className="container">
<a className="navbar-brand row" href="/">
- <img src="/images/logo-small.png" width="38" height="38" alt="React" />
+ <img src={require('./logo-small.png')} width="38" height="38" alt="React" />
<span>React.js Starter Kit</span>
</a>
</div>
diff --git a/src/images/logo-small.png b/src/components/Navbar/logo-small.png
Binary files differ.
diff --git a/src/images/logo-small@2x.png b/src/components/Navbar/logo-small@2x.png
Binary files differ.
diff --git a/src/pages/about.jade b/src/content/about.jade
diff --git a/src/pages/index.jade b/src/content/index.jade
diff --git a/src/pages/privacy.jade b/src/content/privacy.jade
diff --git a/src/server.js b/src/server.js
@@ -13,35 +13,15 @@ var fs = require('fs');
var path = require('path');
var express = require('express');
var React = require('react');
-var ReactTools = require('react-tools');
// Set global variables
global.__DEV__ = process.env.NODE_ENV == 'development';
global.__SERVER__ = true;
-// Configure JSX Harmony transform in order to be able
-// require .js files with JSX
-var jsExt = require.extensions['.js'];
-var ignoreExt = function(module, file) { module._compile('', file); };
-require.extensions['.less'] = ignoreExt;
-require.extensions['.svg'] = ignoreExt;
-require.extensions['.js'] = function(module, file) {
- if (file.indexOf('node_modules') === -1) {
- var src = fs.readFileSync(file, 'utf8');
- try {
- src = ReactTools.transform(src, {harmony: true, stripTypes: true});
- } catch (e) {
- throw new Error('Error transforming ' + file + '. ' + e.toString());
- }
- module._compile(src, file);
- } else {
- jsExt(module, file);
- }
-};
-
// The top-level React component + HTML template for it
var App = React.createFactory(require('./components/App'));
-var template = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
+var templateFile = path.join(__dirname, 'templates/index.html');
+var template = fs.readFileSync(templateFile, 'utf8');
var Dispatcher = require('./core/Dispatcher');
var ActionTypes = require('./constants/ActionTypes');
var AppStore = require('./stores/AppStore');
@@ -49,7 +29,7 @@ var AppStore = require('./stores/AppStore');
var server = express();
server.set('port', (process.env.PORT || 5000));
-server.use(express.static(path.join(__dirname, '../build')));
+server.use(express.static(path.join(__dirname)));
// Page API
server.get('/api/page/*', function(req, res) {
@@ -77,7 +57,7 @@ server.get('*', function(req, res) {
var assign = require('react/lib/Object.assign');
var fm = require('front-matter');
var jade = require('jade');
- var sourceDir = path.join(__dirname, './pages');
+ var sourceDir = path.join(__dirname, './content');
var getFiles = function(dir) {
var pages = [];
fs.readdirSync(dir).forEach(function(file) {
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
@@ -20,7 +20,6 @@ var _pages = {};
var _loading = false;
if (__SERVER__) {
- console.log('Fill the AppStore with data');
_pages['/'] = {title: 'Home Page'};
_pages['/privacy'] = {title: 'Privacy Policy'};
}
diff --git a/src/index.html b/src/templates/index.html