├── .nvmrc ├── .eslintignore ├── app-home.js ├── source ├── test │ ├── index.js │ ├── functional │ │ ├── index.js │ │ ├── routes │ │ │ ├── index │ │ │ │ └── index.js │ │ │ └── test-data │ │ │ │ └── index.js │ │ └── browser │ │ │ └── smoketest.js │ ├── e2e.js │ ├── helpers │ │ └── test-route │ │ │ └── index.js │ └── title │ │ └── index.js ├── shared │ ├── components │ │ ├── title │ │ │ └── index.js │ │ ├── app │ │ │ └── index.js │ │ └── test-data │ │ │ └── index.js │ ├── routes │ │ └── index.js │ ├── configure-store.js │ └── reducers │ │ └── index.js ├── server │ ├── log.js │ ├── render.js │ ├── settings │ │ └── index.js │ ├── server.js │ ├── render-layout.js │ ├── app.js │ └── routes │ │ └── main │ │ └── index.js └── client │ └── index.js ├── .gitignore ├── .editorconfig ├── static └── index.html ├── .babelrc ├── circle.yml ├── webpack.config.dev.js ├── devServer.js ├── webpack.config.prod.js ├── nightwatch.json ├── LICENSE ├── package.json ├── .eslintrc └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | stable 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | app/node_modules/public/js/ 3 | build/ 4 | -------------------------------------------------------------------------------- /app-home.js: -------------------------------------------------------------------------------- 1 | export default { 2 | APP_HOME: __dirname 3 | }; 4 | -------------------------------------------------------------------------------- /source/test/index.js: -------------------------------------------------------------------------------- 1 | import 'test/title'; 2 | import 'test/functional'; 3 | -------------------------------------------------------------------------------- /source/test/functional/index.js: -------------------------------------------------------------------------------- 1 | import './routes/index'; 2 | import './routes/test-data'; 3 | -------------------------------------------------------------------------------- /source/shared/components/title/index.js: -------------------------------------------------------------------------------- 1 | export default React => (props) =>

{ props.title }

; 2 | -------------------------------------------------------------------------------- /source/test/functional/routes/index/index.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import testRoute from 'test/helpers/test-route'; 3 | 4 | testRoute({ test, route: '/' }); 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | !app/node_modules 3 | .DS_Store 4 | config/BUILD 5 | npm-debug.log 6 | app/node_modules/public/js/bundle.* 7 | build/ 8 | .idea/ 9 | .eslintcache 10 | /reports 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Your App 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /source/test/e2e.js: -------------------------------------------------------------------------------- 1 | import settings from 'server/settings'; 2 | import runner from 'nightwatch-autorun'; 3 | import server from 'server/app.js'; 4 | 5 | const NODE_PORT = process.env.NODE_PORT || settings.NODE_PORT; 6 | 7 | runner({port: NODE_PORT, server}); 8 | -------------------------------------------------------------------------------- /source/server/log.js: -------------------------------------------------------------------------------- 1 | import logger from 'bunyan-request-logger'; 2 | import settings from 'server/settings'; 3 | 4 | const loggerSettings = { 5 | name: settings.APP_NAME, 6 | logParams: settings.LOG_PARAMS || undefined // so we don't pollute logs. 7 | }; 8 | 9 | export default logger(loggerSettings); 10 | -------------------------------------------------------------------------------- /source/test/functional/browser/smoketest.js: -------------------------------------------------------------------------------- 1 | var WAIT = 1000; 2 | 3 | module.exports = { 4 | 'Smoketest' (browser) { 5 | browser 6 | .url(`${browser.launchUrl}/test-data`) 7 | .waitForElementVisible('body', WAIT) 8 | .assert.containsText('body', 'Books') 9 | .end(); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /source/server/render.js: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'react-dom/server'; 2 | import { RouterContext } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | 5 | export default React => (renderProps, store) => { 6 | return renderToString( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /source/server/settings/index.js: -------------------------------------------------------------------------------- 1 | import appHome from '../../../app-home.js'; 2 | 3 | export default Object.assign({}, appHome, { 4 | APP_HOST: '0.0.0.0', 5 | APP_PORT: 3000, 6 | APP_NAME: 'Universal React Boilerplate', 7 | NODE_HOST: '0.0.0.0', 8 | NODE_PORT: 3000, 9 | TITLE: 'Your Cool React App!', 10 | LOG_PARAMS: false // use `/gif.log/:message` to track client logs. 11 | }); 12 | -------------------------------------------------------------------------------- /source/server/server.js: -------------------------------------------------------------------------------- 1 | import settings from 'server/settings'; 2 | import app from 'server/app.js'; 3 | 4 | const host = process.env.APP_HOST || settings.APP_HOST; 5 | const port = process.env.APP_PORT || settings.APP_PORT; 6 | 7 | app.listen(port, (err) => { 8 | if (err) { 9 | console.log(err); 10 | return; 11 | } 12 | 13 | console.log(`Listening at http://${ host }:${ port }`); 14 | }); 15 | -------------------------------------------------------------------------------- /source/shared/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router, Route } from 'react-router'; 2 | import createApp from 'shared/components/app'; 3 | import createTestData from 'shared/components/test-data'; 4 | 5 | export default (React, browserHistory) => { 6 | 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2", 5 | "react" 6 | ], 7 | "env": { 8 | "development": { 9 | "plugins": [ 10 | [ 11 | "react-transform", 12 | { 13 | "transforms": [ 14 | { 15 | "transform": "react-transform-catch-errors", 16 | "imports": ["react", "redbox-react"] 17 | } 18 | ] 19 | } 20 | ] 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/server/render-layout.js: -------------------------------------------------------------------------------- 1 | export default ({ settings, rootMarkup, initialState }) => { 2 | return ` 3 | 4 | 5 | 6 | ${ settings.TITLE } 7 | 8 | 9 |
${ rootMarkup }
10 | 13 | 14 | 15 | 16 | `; 17 | }; 18 | -------------------------------------------------------------------------------- /source/test/functional/routes/test-data/index.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import request from 'supertest'; 3 | import app from 'server/app.js'; 4 | 5 | const route = '/test-data'; 6 | 7 | test(`GET ${ route }`, nest => { 8 | nest.test(route, assert => { 9 | const msg = `${ route } should not return an error`; 10 | 11 | request(app) 12 | .get(route) 13 | .expect(200) 14 | .end(err => { 15 | assert.error(err, msg); 16 | 17 | assert.end(); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /source/test/helpers/test-route/index.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import app from 'server/app.js'; 3 | 4 | export default ({ route, test, message }) => { 5 | test(`GET ${ route }`, nest => { 6 | nest.test(route, assert => { 7 | const msg = message || `${ route } should not return an error`; 8 | 9 | request(app) 10 | .get(route) 11 | .expect(200) 12 | .end(err => { 13 | assert.error(err, msg); 14 | assert.end(); 15 | }); 16 | }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /source/shared/components/app/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import createTitle from 'shared/components/title'; 3 | 4 | const createApp = React => ({ title }) => { 5 | const Title = createTitle(React); 6 | 7 | return ( 8 |
9 | 10 | </div> 11 | ); 12 | }; 13 | 14 | const mapStateToProps = (state) => { 15 | const { title } = state; 16 | return { title }; 17 | }; 18 | 19 | // Connect props to component 20 | export default React => { 21 | const App = createApp(React); 22 | return connect(mapStateToProps)(App); 23 | }; 24 | -------------------------------------------------------------------------------- /source/server/app.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import express from 'express'; 3 | import noCache from 'connect-cache-control'; 4 | import log from 'server/log'; 5 | import settings from 'server/settings'; 6 | 7 | import mainRoute from './routes/main'; 8 | 9 | const app = express(); 10 | 11 | app.use(log.requestLogger()); 12 | 13 | app.get('/log.gif/:message', noCache, log.route()); 14 | 15 | const buildDir = '/build'; 16 | const staticDir = path.join(settings.APP_HOME, buildDir); 17 | 18 | app.use('/static', express.static(staticDir)); 19 | 20 | app.use('/', mainRoute); 21 | 22 | export default app; 23 | -------------------------------------------------------------------------------- /source/shared/configure-store.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import createLogger from 'redux-logger'; 4 | import { routerReducer } from 'react-router-redux'; 5 | import reducers from 'shared/reducers'; 6 | 7 | const logger = createLogger(); 8 | const rootReducer = combineReducers(Object.assign({}, reducers, { 9 | routing: routerReducer 10 | })); 11 | 12 | const configureStore = (initialState = {}) => { 13 | return compose( 14 | applyMiddleware(thunkMiddleware, logger) 15 | )(createStore)(rootReducer, initialState); 16 | }; 17 | 18 | export default configureStore; 19 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: v4.2.6 4 | 5 | 6 | dependencies: 7 | pre: 8 | - npm install -g npm@3 9 | - npm set progress=false 10 | 11 | test: 12 | override: 13 | - ./node_modules/.bin/eslint --debug . --format tap | ./node_modules/.bin/tap-xunit > ${CIRCLE_TEST_REPORTS}/lint.xml && test ${PIPESTATUS[0]} -eq 0 14 | - NODE_PATH=source ./node_modules/.bin/babel-node source/test | ./node_modules/.bin/tap-xunit > ${CIRCLE_TEST_REPORTS}/test.xml && test ${PIPESTATUS[0]} -eq 0 15 | - npm outdated --depth=0 16 | - npm run build -s 17 | - REPORT_DIR=${CIRCLE_TEST_REPORTS} LOG_DIR=${CIRCLE_ARTIFACTS} npm run test:e2e -s 18 | 19 | general: 20 | artifacts: 21 | - reports 22 | -------------------------------------------------------------------------------- /source/shared/reducers/index.js: -------------------------------------------------------------------------------- 1 | const initialState = [ 2 | { id: 1, text: 'Book 1', count: 2 }, 3 | { id: 2, text: 'Book 2', count: 3 }, 4 | { id: 3, text: 'Book 3', count: 4 }, 5 | ]; 6 | 7 | const books = (state = { 8 | items: initialState, 9 | }, action) => { 10 | switch (action.type) { 11 | case 'ADD_COUNT': 12 | const newItems = state.items.map(item => { 13 | if (item.id === action.item.id) { 14 | item.count++; 15 | } 16 | 17 | return item; 18 | }); 19 | 20 | return Object.assign({}, state.items, { 21 | items: newItems, 22 | }); 23 | default: 24 | return state; 25 | } 26 | }; 27 | 28 | const reducers = { 29 | books 30 | }; 31 | 32 | export default reducers; 33 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; // eslint-disable-line strict 2 | 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | 6 | module.exports = { 7 | devtool: 'eval', 8 | resolve: { 9 | root: path.join(__dirname, 'source') 10 | }, 11 | entry: [ 12 | './source/client/index' 13 | ], 14 | output: { 15 | path: path.join(__dirname, 'build'), 16 | filename: 'index.js', 17 | publicPath: '/static/' 18 | }, 19 | plugins: [ 20 | new webpack.NoErrorsPlugin() 21 | ], 22 | module: { 23 | loaders: [{ 24 | test: /\.js$/, 25 | loaders: ['babel'], 26 | include: [ 27 | path.join(__dirname, 'source'), 28 | path.join(__dirname, 'app-home.js') 29 | ] 30 | }] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /devServer.js: -------------------------------------------------------------------------------- 1 | import open from 'open'; 2 | import log from 'server/log'; 3 | import settings from 'server/settings'; 4 | import app from 'server/app'; 5 | import webpack from 'webpack'; 6 | import config from './webpack.config.dev'; 7 | 8 | const compiler = webpack(config); 9 | const NODE_PORT = process.env.NODE_PORT || settings.NODE_PORT; 10 | const NODE_HOST = process.env.NODE_HOST || settings.NODE_HOST; 11 | 12 | app.use(require('webpack-dev-middleware')(compiler, { 13 | noInfo: true, 14 | publicPath: config.output.publicPath 15 | })); 16 | 17 | const serverURL = `http://${NODE_HOST}:${NODE_PORT}`; 18 | const logAndOpen = () => { 19 | log.info(`Listening at ${serverURL}`); 20 | open(serverURL); 21 | }; 22 | 23 | app.listen(NODE_PORT, NODE_HOST, (err) => err ? console.error(err) : logAndOpen()); 24 | -------------------------------------------------------------------------------- /source/test/title/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import reactDom from 'react-dom/server'; 3 | import test from 'tape'; 4 | import dom from 'cheerio'; 5 | 6 | import createTitle from 'shared/components/title'; 7 | 8 | const Title = createTitle(React); 9 | const render = reactDom.renderToStaticMarkup; 10 | 11 | test('Title', assert => { 12 | const titleText = 'Hello!'; 13 | const props = { title: titleText, className: 'title' }; 14 | const re = new RegExp(titleText, 'g'); 15 | 16 | const el = <Title { ...props } />; 17 | const $ = dom.load(render(el)); 18 | const output = $('.title').html(); 19 | 20 | const actual = re.test(output); 21 | const expected = true; 22 | 23 | assert.equal(actual, expected, 24 | 'should output the correct title text'); 25 | 26 | assert.end(); 27 | }); 28 | -------------------------------------------------------------------------------- /source/shared/components/test-data/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import createTitle from 'shared/components/title'; 3 | 4 | const createApp = React => ({ dispatch, books }) => { 5 | const Title = createTitle(React); 6 | 7 | const bookNodes = books.items.map(book => { 8 | return ( 9 | <div key={ book.id }> 10 | { book.text } - Read by { book.count } people. 11 | <button onClick={ () => dispatch({ type: 'ADD_COUNT', item: book }) }>Add reader</button> 12 | </div> 13 | ); 14 | }); 15 | 16 | return ( 17 | <div> 18 | <Title title="Books" /> 19 | { bookNodes } 20 | </div> 21 | ); 22 | }; 23 | 24 | const mapStateToProps = (state) => { 25 | const { books } = state; 26 | return { books }; 27 | }; 28 | 29 | // Connect props to component 30 | export default React => { 31 | const App = createApp(React); 32 | return connect(mapStateToProps)(App); 33 | }; 34 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; // eslint-disable-line strict 2 | 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | resolve: { 9 | root: path.join(__dirname, 'source') 10 | }, 11 | entry: [ 12 | './source/client/index' 13 | ], 14 | output: { 15 | path: path.join(__dirname, 'build'), 16 | filename: 'index.js', 17 | publicPath: '/static/' 18 | }, 19 | plugins: [ 20 | new webpack.optimize.OccurenceOrderPlugin(), 21 | new webpack.DefinePlugin({ 22 | 'process.env': { 23 | 'NODE_ENV': JSON.stringify('production') 24 | } 25 | }), 26 | new webpack.optimize.UglifyJsPlugin({ 27 | compressor: { 28 | warnings: false 29 | } 30 | }) 31 | ], 32 | module: { 33 | loaders: [{ 34 | test: /\.js$/, 35 | loaders: ['babel'], 36 | include: path.join(__dirname, 'source') 37 | }] 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /nightwatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_folders" : ["source/test/functional/browser"], 3 | "output_folder" : "reports/test-e2e", 4 | 5 | "selenium" : { 6 | "start_process" : false 7 | }, 8 | 9 | "test_workers": { 10 | "enabled": true, 11 | "workers": 8 12 | }, 13 | 14 | "test_settings" : { 15 | "default" : { 16 | "launch_url" : "http://localhost:3000", 17 | "selenium_port" : 4444, 18 | "selenium_host" : "localhost", 19 | "silent": true, 20 | "screenshots" : { 21 | "enabled" : true, 22 | "on_failure" : true, 23 | "on_error" : false, 24 | "path" : "reports/test-e2e" 25 | }, 26 | "desiredCapabilities": { 27 | "browserName": "chrome", 28 | "javascriptEnabled": true, 29 | "acceptSslCerts": true 30 | } 31 | }, 32 | 33 | "chrome" : { 34 | "desiredCapabilities": { 35 | "browserName": "chrome", 36 | "javascriptEnabled": true, 37 | "acceptSslCerts": true 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /source/server/routes/main/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { match } from 'react-router'; 3 | 4 | import renderLayout from 'server/render-layout'; 5 | import render from 'server/render'; 6 | import settings from 'server/settings'; 7 | 8 | import configureStore from 'shared/configure-store'; 9 | import createRoutes from 'shared/routes'; 10 | 11 | const store = configureStore(); 12 | const routes = createRoutes(React); 13 | const initialState = store.getState(); 14 | 15 | export default (req, res) => { 16 | match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { 17 | if (error) { 18 | res.status(500).send(error.message); 19 | } else if (redirectLocation) { 20 | res.redirect(302, redirectLocation.pathname + redirectLocation.search); 21 | } else if (renderProps) { 22 | const rootMarkup = render(React)(renderProps, store); 23 | res.status(200).send(renderLayout({ settings, rootMarkup, initialState })); 24 | } else { 25 | res.status(404).send('Not found'); 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Eric Elliott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /source/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, combineReducers } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { Router, Route, browserHistory } from 'react-router'; 6 | import { syncHistoryWithStore, routerReducer } from 'react-router-redux'; 7 | import reducers from 'shared/reducers'; 8 | 9 | import createApp from 'shared/components/app'; 10 | import createTestData from 'shared/components/test-data'; 11 | 12 | // Add the reducer to your store on the `routing` key 13 | const store = createStore( 14 | combineReducers({ 15 | ...reducers, 16 | routing: routerReducer 17 | }), 18 | // hydrating server. 19 | window.BOOTSTRAP_CLIENT_STATE 20 | ); 21 | 22 | // Create an enhanced history that syncs navigation events with the store 23 | const history = syncHistoryWithStore(browserHistory, store); 24 | 25 | 26 | // Required for replaying actions from devtools to work 27 | // reduxRouterMiddleware.listenForReplays(store) 28 | 29 | ReactDOM.render( 30 | <Provider store={store}> 31 | <Router history={ history }> 32 | <Route path="/" component={ createApp(React) } /> 33 | <Route path="/test-data" component={ createTestData(React) } /> 34 | </Router> 35 | </Provider>, 36 | document.getElementById('root') 37 | ); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-react-boilerplate", 3 | "version": "3.0.0", 4 | "description": "A new Webpack boilerplate with React components and error handling on module and component level.", 5 | "scripts": { 6 | "clean": "rimraf build", 7 | "build:webpack": "webpack --config webpack.config.prod.js", 8 | "build:webpack:dev": "webpack --config webpack.config.dev.js", 9 | "build": "npm run clean && npm run build:webpack", 10 | "build:dev": "npm run clean && npm run build:webpack:dev", 11 | "start": "cross-env NODE_PATH=source babel-node devServer.js | bunyan", 12 | "server": "cross-env NODE_PATH=source babel-node source/server/server.js | bunyan", 13 | "test": "cross-env NODE_PATH=source babel-node source/test/index.js", 14 | "test:e2e": "cross-env NODE_PORT=3000 NODE_PATH=source babel-node source/test/e2e.js", 15 | "lint": "eslint --cache .", 16 | "watch": "watch \"clear && npm run lint -s && npm run test -s\" source", 17 | "check": "npm run lint && npm run test && npm outdated --depth=0", 18 | "update": "updtr" 19 | }, 20 | "engines": { 21 | "node": ">=4" 22 | }, 23 | "os": [ 24 | "darwin", 25 | "linux" 26 | ], 27 | "keywords": [ 28 | "react", 29 | "reactjs", 30 | "boilerplate", 31 | "webpack", 32 | "babel", 33 | "react-transform" 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "git@github.com:cloverfield-tools/universal-react-boilerplate.git" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "6.6.5", 41 | "babel-core": "6.7.7", 42 | "babel-eslint": "5.0.0", 43 | "babel-loader": "6.2.4", 44 | "babel-plugin-react-transform": "2.0.2", 45 | "babel-plugin-transform-runtime": "6.6.0", 46 | "babel-preset-es2015": "6.6.0", 47 | "babel-preset-react": "6.5.0", 48 | "babel-preset-stage-2": "6.5.0", 49 | "cheerio": "0.20.0", 50 | "cross-env": "1.0.7", 51 | "eslint": "2.4.0", 52 | "eslint-plugin-react": "4.2.2", 53 | "estraverse-fb": "1.3.1", 54 | "nightwatch-autorun": "2.3.1", 55 | "open": "0.0.5", 56 | "react-transform-catch-errors": "1.0.2", 57 | "redbox-react": "1.2.2", 58 | "rimraf": "2.5.2", 59 | "supertest": "1.2.0", 60 | "tap-xunit": "1.3.1", 61 | "tape": "4.5.1", 62 | "updtr": "0.1.7", 63 | "watch": "0.17.1", 64 | "webpack": "1.12.14", 65 | "webpack-dev-middleware": "1.5.1" 66 | }, 67 | "dependencies": { 68 | "bunyan-request-logger": "1.1.0", 69 | "connect-cache-control": "1.0.0", 70 | "express": "4.13.4", 71 | "react": "0.14.7", 72 | "react-dom": "0.14.7", 73 | "react-redux": "4.4.1", 74 | "react-router": "2.0.1", 75 | "react-router-redux": "4.0.0", 76 | "redux": "3.3.1", 77 | "redux-logger": "2.6.1", 78 | "redux-thunk": "2.0.1" 79 | }, 80 | "license": "MIT", 81 | "bugs": { 82 | "url": "https://github.com/cloverfield-tools/universal-react-boilerplate/issues" 83 | }, 84 | "homepage": "https://github.com/cloverfield-tools/universal-react-boilerplate" 85 | } 86 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true 8 | }, 9 | 10 | "plugins": [ 11 | "react" 12 | ], 13 | 14 | "ecmaFeatures": { 15 | "arrowFunctions": true, 16 | "binaryLiterals": true, 17 | "blockBindings": true, 18 | "classes": false, 19 | "defaultParams": true, 20 | "destructuring": true, 21 | "forOf": true, 22 | "generators": true, 23 | "modules": true, 24 | "objectLiteralComputedProperties": true, 25 | "objectLiteralDuplicateProperties": true, 26 | "objectLiteralShorthandMethods": true, 27 | "objectLiteralShorthandProperties": true, 28 | "octalLiterals": true, 29 | "regexUFlag": true, 30 | "regexYFlag": true, 31 | "spread": true, 32 | "superInFunctions": false, 33 | "templateStrings": true, 34 | "unicodeCodePointEscapes": true, 35 | "globalReturn": true, 36 | "jsx": true 37 | }, 38 | 39 | "rules": { 40 | "react/jsx-uses-react": 2, 41 | "react/jsx-uses-vars": 2, 42 | "react/react-in-jsx-scope": 2, 43 | "block-scoped-var": [0], 44 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 45 | "camelcase": [0], 46 | "comma-dangle": [0], 47 | "comma-spacing": [2], 48 | "comma-style": [2, "last"], 49 | "complexity": [0, 11], 50 | "consistent-return": [2], 51 | "consistent-this": [0, "that"], 52 | "curly": [2, "multi-line"], 53 | "default-case": [2], 54 | "dot-notation": [2, {"allowKeywords": true}], 55 | "eol-last": [2], 56 | "eqeqeq": [2], 57 | "func-names": [0], 58 | "func-style": [0, "declaration"], 59 | "generator-star-spacing": [2, "after"], 60 | "guard-for-in": [0], 61 | "handle-callback-err": [0], 62 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 63 | "keyword-spacing": [2], 64 | "quotes": [2, "single", "avoid-escape"], 65 | "max-depth": [0, 4], 66 | "max-len": [0, 80, 4], 67 | "max-nested-callbacks": [0, 2], 68 | "max-params": [0, 3], 69 | "max-statements": [0, 10], 70 | "new-parens": [2], 71 | "new-cap": [0], 72 | "newline-after-var": [0], 73 | "no-alert": [2], 74 | "no-array-constructor": [2], 75 | "no-bitwise": [0], 76 | "no-caller": [2], 77 | "no-catch-shadow": [2], 78 | "no-cond-assign": [2], 79 | "no-console": [0], 80 | "no-constant-condition": [1], 81 | "no-continue": [2], 82 | "no-control-regex": [2], 83 | "no-debugger": [2], 84 | "no-delete-var": [2], 85 | "no-div-regex": [0], 86 | "no-dupe-args": [2], 87 | "no-dupe-keys": [2], 88 | "no-duplicate-case": [2], 89 | "no-else-return": [0], 90 | "no-empty": [2], 91 | "no-empty-character-class": [2], 92 | "no-eq-null": [0], 93 | "no-eval": [2], 94 | "no-ex-assign": [2], 95 | "no-extend-native": [1], 96 | "no-extra-bind": [2], 97 | "no-extra-boolean-cast": [2], 98 | "no-extra-semi": [1], 99 | "no-fallthrough": [2], 100 | "no-floating-decimal": [2], 101 | "no-func-assign": [2], 102 | "no-implied-eval": [2], 103 | "no-inline-comments": [0], 104 | "no-inner-declarations": [2, "functions"], 105 | "no-invalid-regexp": [2], 106 | "no-irregular-whitespace": [2], 107 | "no-iterator": [2], 108 | "no-label-var": [2], 109 | "no-labels": [2], 110 | "no-lone-blocks": [2], 111 | "no-lonely-if": [2], 112 | "no-loop-func": [2], 113 | "no-mixed-requires": [0, false], 114 | "no-mixed-spaces-and-tabs": [2, false], 115 | "no-multi-spaces": [2], 116 | "no-multi-str": [2], 117 | "no-multiple-empty-lines": [2, {"max": 2}], 118 | "no-native-reassign": [1], 119 | "no-negated-in-lhs": [2], 120 | "no-nested-ternary": [0], 121 | "no-new": [2], 122 | "no-new-func": [2], 123 | "no-new-object": [2], 124 | "no-new-require": [0], 125 | "no-new-wrappers": [2], 126 | "no-obj-calls": [2], 127 | "no-octal": [2], 128 | "no-octal-escape": [2], 129 | "no-param-reassign": [2], 130 | "no-path-concat": [0], 131 | "no-plusplus": [0], 132 | "no-process-env": [0], 133 | "no-process-exit": [2], 134 | "no-proto": [2], 135 | "no-redeclare": [2], 136 | "no-regex-spaces": [2], 137 | "no-reserved-keys": [0], 138 | "no-restricted-modules": [0], 139 | "no-return-assign": [2], 140 | "no-script-url": [2], 141 | "no-self-compare": [0], 142 | "no-sequences": [2], 143 | "no-shadow": [2], 144 | "no-shadow-restricted-names": [2], 145 | "no-spaced-func": [2], 146 | "no-sparse-arrays": [2], 147 | "no-sync": [0], 148 | "no-ternary": [0], 149 | "no-throw-literal": [2], 150 | "no-trailing-spaces": [2], 151 | "no-undef": [2], 152 | "no-undef-init": [2], 153 | "no-undefined": [0], 154 | "no-underscore-dangle": [2], 155 | "no-unreachable": [2], 156 | "no-unused-expressions": [2], 157 | "no-unused-vars": [1, {"vars": "all", "args": "after-used"}], 158 | "no-use-before-define": [2], 159 | "no-void": [0], 160 | "no-warning-comments": [0, {"terms": ["todo", "fixme", "xxx"], "location": "start"}], 161 | "no-with": [2], 162 | "no-extra-parens": [0], 163 | "one-var": [0], 164 | "operator-assignment": [0, "always"], 165 | "operator-linebreak": [2, "after"], 166 | "padded-blocks": [0], 167 | "quote-props": [0], 168 | "radix": [0], 169 | "semi": [2], 170 | "semi-spacing": [2, {"before": false, "after": true}], 171 | "sort-vars": [0], 172 | "space-before-function-paren": [2, {"anonymous": "always", "named": "always"}], 173 | "space-before-blocks": [0, "always"], 174 | "space-in-brackets": [ 175 | 0, "never", { 176 | "singleValue": true, 177 | "arraysInArrays": false, 178 | "arraysInObjects": false, 179 | "objectsInArrays": true, 180 | "objectsInObjects": true, 181 | "propertyName": false 182 | } 183 | ], 184 | "space-in-parens": [0], 185 | "space-infix-ops": [2], 186 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 187 | "spaced-line-comment": [0, "always"], 188 | "strict": [2, "never"], 189 | "use-isnan": [2], 190 | "valid-jsdoc": [0], 191 | "valid-typeof": [2], 192 | "vars-on-top": [0], 193 | "wrap-iife": [2], 194 | "wrap-regex": [2], 195 | "yoda": [2, "never", {"exceptRange": true}] 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal React Boilerplate 2 | 3 | [![Dependency Status](https://david-dm.org/cloverfield-tools/universal-react-boilerplate.svg)](https://david-dm.org/cloverfield-tools/universal-react-boilerplate) 4 | [![devDependency Status](https://david-dm.org/cloverfield-tools/universal-react-boilerplate/dev-status.svg)](https://david-dm.org/cloverfield-tools/universal-react-boilerplate#info=devDependencies) 5 | [![Travis-CI](https://travis-ci.org/cloverfield-tools/universal-react-boilerplate.svg?branch=master)](https://travis-ci.org/cloverfield-tools/universal-react-boilerplate) 6 | 7 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/learn-javascript-courses/javascript-questions?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 8 | 9 | # STATUS -- DEPRECATED 10 | 11 | **Please check out [Next.js](https://zeit.co/blog/next) instead.** 12 | 13 | 14 | # -- DEPRECATED docs (see above): -- 15 | 16 | A simple boilerplate Node app featuring: 17 | 18 | * Universal JavaScript. *Routing & Rendering with shared components, shared store, & shared routes.* 19 | * State managed by Redux. 20 | * Standard ES6 modules using Babel + webpack. 21 | * React + JSX + ES7 object spread via Babel. 22 | * Express 4.x. 23 | * Useful scripts and conventions for app development. 24 | 25 | 26 | ## Learn JavaScript with Eric Elliott 27 | 28 | The Universal React Boilerplate was written for the ["Learn JavaScript with Eric Elliott" courses](https://ericelliottjs.com/). A series of courses to teach people how to build great JavaScript apps for production. Don't just learn JavaScript. Learn how to build amazing things. 29 | 30 | ### ES6 updates 31 | 32 | Rewritten from the ground up for ES6 + React with Babel and webpack. 33 | 34 | ### React 35 | 36 | Useful to get a working starting point. Still exploratory and evolving. Needs production hardening. If you use this in a production app, please contribute your production tweaks back so we can all benefit. 37 | 38 | Our next big challenge is to encapsulate universal route, render, and store config into its own library module, which would radically simplify the process of building your apps using this boilerplate. 39 | 40 | 41 | ## Getting Started 42 | 43 | We're using an ES6 template string for the page skeleton + React to render the actual UI into the `root` div. 44 | 45 | The React render happens on both the server and the client using shared code. React components are written in class-free style using [pure components](https://github.com/ericelliott/react-pure-component-starter) wherever possible. 46 | 47 | 48 | ``` 49 | npm install 50 | npm run build:dev 51 | npm start 52 | ``` 53 | 54 | Now the app should be running at http://0.0.0.0:3000/ 55 | 56 | ## Universal JavaScript 57 | 58 | Universal JavaScript (aka *"isomorphic JavaScript"*) means that it's designed to run a lot of the same code on both the client and the server. Typically that includes a lot of rendering and domain logic. 59 | 60 | There are many advantages to building apps this way, but the primary advantages are: 61 | 62 | * **Cross-functional teams.** Since everything is written in JavaScript, it's easier to build teams who know how to work on both the client and server sides of the app. 63 | * **Write once, run everywhere.** With the exception of a few library substitutions and browser polyfills, the code is shared, which means you have to write about half the code you'd write working on a non-universal app. 64 | * **More productive developers.** Since the app is more consistent across the stack, there's no context switching when you need to maintain application behavior on both sides of the stack. Write the behavior once, and you're done. Context switching slows developers down significantly. 65 | 66 | 67 | ## Tech stack 68 | 69 | Take a look at the [Roadmap](https://github.com/cloverfield-tools/universal-react-boilerplate/issues/4) for an idea of where this is going. Help is welcome and encouraged! =) 70 | 71 | The universal boilerplate uses standard JavaScript modules to author all of the code. All open-source modules are sourced from `npm`. 72 | 73 | 74 | **Why not use Bower and AMD?** Lots of reasons: 75 | 76 | * `npm` has 5x more modules than Bower, 60% of which are browser compatible, and `npm` is growing faster than Bower. In fact, `npm` is the largest package repository available for any programming language. 77 | * Webpack and browserify let you bundle standard ES6 style modules for the browser. 78 | * Typical Node applications are not written using AMD modules or Bower, so sharing code becomes more complicated when you author with AMD. 79 | * Bower modules frequently assume they're running in a browser environment and do things that don't make any sense in the server environment. 80 | * Typical AMD apps default to asynchronously loading all the modules. That's bad for performance. See below. 81 | * 2010 called. They want you to know that AMD was always intended to be a temporary hack until something better came along. Something better has come along. Welcome to the universal future. ;) 82 | 83 | 84 | ### The problem with AMD's async loading default 85 | 86 | Asynchronously loading all your modules by default is really bad for application start up performance because all those requests create latency queues which can be really painful on mobile devices. HTTP2 / SPDY are changing that in modern browsers, but people often use their mobile devices for years without upgrading the browsers on them. I recommend bundling all your commonly used behavior (including templates and CSS) into a single, compressed JavaScript file in order to minimize request latency. 87 | 88 | I know that AMD supports bundling with tools like `r.js`, but many apps start out without bundling and *never bother to fix it later.* I've personally been on three different app projects where bundling was postponed for a year or more, all the while making customers wait. I knew this was bad, and I tried to get it fixed, but on a large enterprise app project, getting something like that changed mid-project takes klout, political maneuvering, and buy-in from teams who may never have met you and could be wondering why you're creating work for them when they're already behind schedule. 89 | 90 | In my experience, every team is always behind schedule if you ask them to do work they weren't planning well ahead of time. ;) 91 | 92 | 93 | ## What's inside? 94 | 95 | There are some concerns that legitimately belong only on the server, or only on the client, so there are `client/` and `server/` directories for code that is specific to one or the other. Shared code goes in `shared/`: 96 | 97 | * `source/shared` - Shared code. 98 | * `source/client` - For browser-only code. 99 | * `source/server` - For server-only code. 100 | 101 | 102 | ## Index 103 | 104 | The `server/index` route serves dynamic content. Static assets are served from the `build` folder using `express.static`. 105 | 106 | 107 | ## Scripts 108 | 109 | Some of these scripts may require a Unix/Linux environment. OS X and Linux come with appropriate terminals ready to roll. On Windows, you'll need git installed, which comes with Git Bash. That should work. If you have any trouble, please [report the issue](https://github.com/cloverfield-tools/universal-react-boilerplate/issues/new). 110 | 111 | The `package.json` file comes with the following scripts that you may find useful: 112 | 113 | * `npm start` runs a client-only devserver 114 | * `npm run build` rebuilds the client 115 | * `npm run watch` runs a dev console that reports lint and unit test errors on save 116 | * `npm run server` runs the actual server process 117 | 118 | To run a script, open the terminal, navigate to the boilerplate directory, and type: 119 | 120 | ``` 121 | npm run <name of script> 122 | ``` 123 | 124 | 125 | ### Start 126 | 127 | Start the dev server. 128 | 129 | You can optionally leave `run` out of the `start` and `test` script invocations, so these are equivalent: 130 | 131 | ``` 132 | npm run start 133 | npm start 134 | ``` 135 | 136 | ## 137 | Log messages will be written to the console (stdout) in JSON format for convenient queries using tools like [Splunk](http://www.splunk.com/). You should be able to pipe the output to a third party logging service for aggregation without including that log aggregation logic in the app itself. 138 | 139 | 140 | ### Developer feedback console: 141 | 142 | ``` 143 | npm run watch 144 | ``` 145 | 146 | The dev console does the following: 147 | 148 | * Checks for syntax errors with `eslint` using idiomatic settings from `.eslintrc` 149 | * Runs the unit tests and reports any test failures. 150 | * Watches for file changes and re-runs the whole process. 151 | 152 | ## Requiring modules 153 | 154 | To require modules relative to the app root, just put them in `source` and require them just like you would require a module installed by npm. For example, if you had a file called `source/routes/index.js` you can require it with: 155 | 156 | ``` 157 | import routes from 'routes'; 158 | ``` 159 | 160 | This is a lot cleaner than using relative paths and littering your code with stuff like `../../../module/path/module.js`. 161 | 162 | This requires the `NODE_PATH` environment variable to be set to `source`. For example from the `package.json`: 163 | 164 | ```js 165 | scripts: { 166 | "server": "NODE_PATH=source babel-node source/server/index.js", 167 | "test": "NODE_PATH=source babel-node source/test/index.js", 168 | } 169 | ``` 170 | 171 | We also need to tell webpack configs (located in the project root) about the source path: 172 | 173 | ```js 174 | resolve: { 175 | root: __dirname + '/source' 176 | } 177 | ``` 178 | 179 | ### Why? 180 | 181 | * You can move things around more easily. 182 | * Every file documents your app's directory structure for you. You'll know exactly where to look for things. 183 | * Dazzle your coworkers. 184 | 185 | If you find yourself using the same file in a lot of modules, it's probably a better idea to split it out into its own module -- preferably open source. Then you can just install it like any other module so it can live in `node_modules`. 186 | 187 | 188 | <a href="https://ericelliottjs.com"><img width="1200" alt="Learn JavaScript with Eric Elliott" src="https://cloud.githubusercontent.com/assets/364727/8640836/76d86618-28c3-11e5-8b6e-27d9cd72180e.png"></a> 189 | 190 | Created for & Sponsored by "Learn JavaScript with Eric Elliott", an online course series for application builders. Ready to jump in? [Learn more](https://ericelliottjs.com/). 191 | --------------------------------------------------------------------------------