From 541a3cfcbd9a824292fd9df8aff0249bab29458b Mon Sep 17 00:00:00 2001 From: Vishwanath Arondekar Date: Mon, 9 Feb 2015 01:08:23 +0300 Subject: [PATCH] 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 --- config/webpack.js | 194 ++++++++++-------- gulpfile.js | 28 +-- package.json | 4 +- src/actions/AppActions.js | 2 +- src/app.js | 77 +++---- src/components/App/App.js | 21 +- src/components/App/NavigationMixin.js | 5 +- src/components/Navbar/Navbar.js | 4 +- .../Navbar}/logo-small.png | Bin .../Navbar}/logo-small@2x.png | Bin src/{pages => content}/about.jade | 0 src/{pages => content}/index.jade | 0 src/{pages => content}/privacy.jade | 0 src/server.js | 28 +-- src/stores/AppStore.js | 1 - src/{ => templates}/index.html | 0 16 files changed, 177 insertions(+), 187 deletions(-) rename src/{images => components/Navbar}/logo-small.png (100%) rename src/{images => components/Navbar}/logo-small@2x.png (100%) rename src/{pages => content}/about.jade (100%) rename src/{pages => content}/index.jade (100%) rename src/{pages => content}/privacy.jade (100%) rename src/{ => templates}/index.html (100%) diff --git a/config/webpack.js b/config/webpack.js index 9c0f89222..8fd1d6bfc 100644 --- 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 index 1437828d3..665be86dd 100644 --- 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 index abaeba5aa..1e2a50f76 100644 --- 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 index 9737cda37..0f443c241 100644 --- 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 index 6989b3a57..494c5829e 100644 --- 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 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 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 index 4ddfee4b6..7590660ce 100644 --- 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 index f52f1b816..4570ce8a5 100644 --- 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 index 40866a937..e710d747d 100644 --- 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({
- React + React React.js Starter Kit
diff --git a/src/images/logo-small.png b/src/components/Navbar/logo-small.png similarity index 100% rename from src/images/logo-small.png rename to src/components/Navbar/logo-small.png diff --git a/src/images/logo-small@2x.png b/src/components/Navbar/logo-small@2x.png similarity index 100% rename from src/images/logo-small@2x.png rename to src/components/Navbar/logo-small@2x.png diff --git a/src/pages/about.jade b/src/content/about.jade similarity index 100% rename from src/pages/about.jade rename to src/content/about.jade diff --git a/src/pages/index.jade b/src/content/index.jade similarity index 100% rename from src/pages/index.jade rename to src/content/index.jade diff --git a/src/pages/privacy.jade b/src/content/privacy.jade similarity index 100% rename from src/pages/privacy.jade rename to src/content/privacy.jade diff --git a/src/server.js b/src/server.js index 844af289b..4fc2e1815 100644 --- 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 index ae687818a..314a150d2 100644 --- 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 similarity index 100% rename from src/index.html rename to src/templates/index.html