├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── app │ ├── components │ │ ├── App │ │ │ ├── App.css │ │ │ ├── App.js │ │ │ └── index.js │ │ └── Navigation │ │ │ ├── Navigation.css │ │ │ ├── Navigation.js │ │ │ └── index.js │ └── routes │ │ ├── About │ │ ├── About.js │ │ └── index.js │ │ ├── Home │ │ ├── Home.js │ │ └── index.js │ │ └── NotFound │ │ ├── NotFound.js │ │ └── index.js ├── client │ └── index.js └── server │ ├── index.js │ └── middleware │ ├── render.js │ └── webpack.js └── webpack ├── common.js ├── webpack.client.babel.js └── webpack.server.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-object-rest-spread" 4 | ], 5 | "presets": [ 6 | "env" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "react/jsx-filename-extension": "off", 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | stats.json 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | - 6 5 | - 5 6 | - 4 7 | script: npm run lint && npm run build 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Richard Käll (richardkall.se) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Starter [![Build Status](https://travis-ci.org/richardkall/react-starter.svg?branch=master)](https://travis-ci.org/richardkall/react-starter) 2 | 3 | > Starter kit for creating universal React applications. 4 | 5 | ## Features 6 | 7 | - [x] [Babel](https://babeljs.io/) 8 | - [x] [CSS Modules](https://github.com/css-modules/css-modules) + [cssnext](http://cssnext.io/) 9 | - [x] [ESLint](http://eslint.org/) 10 | - [x] [Express](http://expressjs.com/) 11 | - [x] [React](http://facebook.github.io/react/) 12 | - [x] [React Router v4](https://github.com/reactjs/react-router) 13 | - [x] [Webpack v2](https://webpack.github.io) 14 | 15 | ### Extras 16 | - [x] [Apollo Client](http://dev.apollodata.com/) (separate branch: [feature/apollo](https://github.com/richardkall/react-starter/tree/feature/apollo)) 17 | - [x] [Redux](http://redux.js.org/) (separate branch: [feature/redux](https://github.com/richardkall/react-starter/tree/feature/redux)) 18 | 19 | ## Setup 20 | 21 | ```bash 22 | $ npm install 23 | ``` 24 | 25 | ## Usage 26 | 27 | Start development server: 28 | 29 | ```bash 30 | $ npm run dev 31 | ``` 32 | 33 | Start production server: 34 | 35 | ```bash 36 | $ NODE_ENV=production npm run build && npm start 37 | ``` 38 | 39 | ## License 40 | 41 | MIT © [Richard Käll](https://richardkall.se) 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-starter", 3 | "version": "0.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=4" 7 | }, 8 | "scripts": { 9 | "analyze": "webpack --config webpack/webpack.client.babel.js --profile --json > stats.json", 10 | "build": "npm run build:client && npm run build:server", 11 | "build:client": "webpack --progress --config webpack/webpack.client.babel.js", 12 | "build:server": "webpack --progress --config webpack/webpack.server.babel.js", 13 | "dev": "npm run build:server && node build/server", 14 | "lint": "eslint --ignore-path .gitignore .", 15 | "prebuild": "rimraf build/*", 16 | "start": "node build/server" 17 | }, 18 | "devDependencies": { 19 | "eslint": "3.18.0", 20 | "eslint-config-airbnb": "14.1.0", 21 | "eslint-plugin-import": "2.2.0", 22 | "eslint-plugin-jsx-a11y": "4.0.0", 23 | "eslint-plugin-react": "6.10.0" 24 | }, 25 | "dependencies": { 26 | "assets-webpack-plugin": "3.5.1", 27 | "babel-cli": "6.24.0", 28 | "babel-loader": "6.4.1", 29 | "babel-plugin-transform-object-rest-spread": "6.23.0", 30 | "babel-plugin-transform-react-remove-prop-types": "0.3.2", 31 | "babel-preset-env": "1.2.2", 32 | "babel-preset-react": "6.23.0", 33 | "compression": "1.6.2", 34 | "css-loader": "0.27.3", 35 | "eslint-plugin-react": "6.9.0", 36 | "express": "4.15.2", 37 | "extract-text-webpack-plugin": "2.1.0", 38 | "file-loader": "0.10.1", 39 | "morgan": "1.8.1", 40 | "postcss-cssnext": "2.10.0", 41 | "postcss-loader": "1.3.3", 42 | "react": "15.4.2", 43 | "react-dom": "15.4.2", 44 | "react-router-dom": "4.0.0", 45 | "rimraf": "2.6.1", 46 | "url-loader": "0.5.8", 47 | "webpack": "2.2.1", 48 | "webpack-dev-middleware": "1.10.1", 49 | "webpack-hot-middleware": "2.17.1", 50 | "webpack-md5-hash": "0.0.5", 51 | "webpack-node-externals": "1.5.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/components/App/App.css: -------------------------------------------------------------------------------- 1 | :global html, 2 | :global body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | :global html { 8 | box-sizing: border-box; 9 | } 10 | 11 | :global *:after, 12 | :global *:before { 13 | box-sizing: inherit; 14 | } 15 | 16 | .root { 17 | font-family: system-ui; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import Home from '../../routes/Home'; 5 | import About from '../../routes/About'; 6 | import NotFound from '../../routes/NotFound'; 7 | import Navigation from '../Navigation'; 8 | 9 | import style from './App.css'; 10 | 11 | function App() { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/app/components/App/index.js: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | 3 | export default App; 4 | -------------------------------------------------------------------------------- /src/app/components/Navigation/Navigation.css: -------------------------------------------------------------------------------- 1 | .list { 2 | display: flex; 3 | list-style: none; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .item { 9 | display: inline; 10 | padding: 14px; 11 | } 12 | 13 | .link { 14 | display: inline-block; 15 | } 16 | 17 | .activeLink { 18 | font-weight: bold; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/components/Navigation/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import style from './Navigation.css'; 5 | 6 | function Navigation() { 7 | return ( 8 | 32 | ); 33 | } 34 | 35 | export default Navigation; 36 | -------------------------------------------------------------------------------- /src/app/components/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import Navigation from './Navigation'; 2 | 3 | export default Navigation; 4 | -------------------------------------------------------------------------------- /src/app/routes/About/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function About() { 4 | return ( 5 |
6 |

About

7 |
8 | ); 9 | } 10 | 11 | export default About; 12 | -------------------------------------------------------------------------------- /src/app/routes/About/index.js: -------------------------------------------------------------------------------- 1 | import About from './About'; 2 | 3 | export default About; 4 | -------------------------------------------------------------------------------- /src/app/routes/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Home() { 4 | return ( 5 |
6 |

Home

7 |
8 | ); 9 | } 10 | 11 | export default Home; 12 | -------------------------------------------------------------------------------- /src/app/routes/Home/index.js: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | 3 | export default Home; 4 | -------------------------------------------------------------------------------- /src/app/routes/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class NotFound extends Component { 4 | componentWillMount() { 5 | if (this.props.staticContext) { 6 | this.props.staticContext.status = 404; 7 | } 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

Page not found

14 |
15 | ); 16 | } 17 | } 18 | 19 | NotFound.propTypes = { 20 | staticContext: PropTypes.shape(), // eslint-disable-line react/require-default-props 21 | }; 22 | 23 | export default NotFound; 24 | -------------------------------------------------------------------------------- /src/app/routes/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import NotFound from './NotFound'; 2 | 3 | export default NotFound; 4 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import { render } from 'react-dom'; 4 | 5 | import App from '../app/components/App'; 6 | 7 | render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | 14 | if (module.hot) { 15 | module.hot.accept(); 16 | } 17 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require, no-console */ 2 | import path from 'path'; 3 | 4 | import compression from 'compression'; 5 | import express from 'express'; 6 | import morgan from 'morgan'; 7 | 8 | import renderMiddleware from './middleware/render'; 9 | 10 | const isProduction = process.env.NODE_ENV === 'production'; 11 | const port = process.env.PORT || 3000; 12 | const app = express(); 13 | 14 | if (isProduction) { 15 | app.use(compression()); 16 | } else { 17 | const { 18 | webpackDevMiddleware, 19 | webpackHotMiddleware, 20 | } = require('./middleware/webpack'); 21 | 22 | app.use(webpackDevMiddleware); 23 | app.use(webpackHotMiddleware); 24 | } 25 | 26 | app.use(morgan(isProduction ? 'combined' : 'dev')); 27 | app.use(express.static(path.resolve(__dirname, '../build'))); 28 | app.use(renderMiddleware); 29 | 30 | app.listen(port, console.log(`Server running on port ${port}`)); 31 | -------------------------------------------------------------------------------- /src/server/middleware/render.js: -------------------------------------------------------------------------------- 1 | /* global CSS_BUNDLE: true, VENDOR_BUNDLE: true, CLIENT_BUNDLE: true */ 2 | import React from 'react'; 3 | import { StaticRouter } from 'react-router-dom'; 4 | import { renderToString } from 'react-dom/server'; 5 | 6 | import App from '../../app/components/App'; 7 | 8 | function render(req, res) { 9 | const context = {}; 10 | 11 | const html = renderToString( 12 | 13 | 14 | , 15 | ); 16 | 17 | if (context.url) { 18 | return res.redirect(302, context.url); 19 | } 20 | 21 | return res 22 | .status(context.status || 200) 23 | .send(` 24 | 25 | 26 | 27 | 28 | 29 | React Starter 30 | 31 | 32 | 33 |
${html}
34 | 35 | 36 | 37 | 38 | `); 39 | } 40 | 41 | export default render; 42 | -------------------------------------------------------------------------------- /src/server/middleware/webpack.js: -------------------------------------------------------------------------------- 1 | import devMiddleware from 'webpack-dev-middleware'; 2 | import hotMiddleware from 'webpack-hot-middleware'; 3 | import webpack from 'webpack'; 4 | 5 | import config from '../../../webpack/webpack.client.babel'; 6 | 7 | const compiler = webpack(config); 8 | 9 | export const webpackDevMiddleware = devMiddleware(compiler, { stats: 'minimal' }); 10 | export const webpackHotMiddleware = hotMiddleware(compiler); 11 | -------------------------------------------------------------------------------- /webpack/common.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | 4 | const isProduction = process.env.NODE_ENV === 'production'; 5 | 6 | export const babelLoaderOptions = { 7 | cacheDirectory: !isProduction, 8 | plugins: [ 9 | 'transform-object-rest-spread', 10 | ...(isProduction && ['transform-react-remove-prop-types']), 11 | ], 12 | }; 13 | 14 | export const cssLoaderOptions = { 15 | minimize: isProduction, 16 | modules: true, 17 | importLoaders: 1, 18 | localIdentName: '[name]__[local]___[hash:base64:5]', 19 | }; 20 | 21 | export const urlLoaderOptions = { 22 | limit: 10000, 23 | name: `media/[name]${isProduction ? '.[hash:8]' : ''}.[ext]`, 24 | }; 25 | 26 | export default { 27 | output: { 28 | path: path.resolve(__dirname, '../build'), 29 | publicPath: '/', 30 | }, 31 | plugins: [ 32 | new webpack.DefinePlugin({ 33 | 'process.env': { 34 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 35 | }, 36 | }), 37 | ...(isProduction && [ 38 | new webpack.optimize.UglifyJsPlugin({ comments: false }), 39 | ]), 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /webpack/webpack.client.babel.js: -------------------------------------------------------------------------------- 1 | import AssetsPlugin from 'assets-webpack-plugin'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import WebpackMd5Hash from 'webpack-md5-hash'; 4 | import webpack from 'webpack'; 5 | 6 | import common, { 7 | babelLoaderOptions, 8 | cssLoaderOptions, 9 | urlLoaderOptions, 10 | } from './common'; 11 | 12 | const isProduction = process.env.NODE_ENV === 'production'; 13 | 14 | export default { 15 | name: 'client', 16 | entry: { 17 | client: [ 18 | ...(!isProduction && ['webpack-hot-middleware/client']), 19 | './src/client', 20 | ], 21 | }, 22 | output: { 23 | ...common.output, 24 | filename: `js/[name]${isProduction ? '.[chunkhash:8]' : ''}.js`, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.css$/, 30 | use: ExtractTextPlugin.extract({ 31 | use: [ 32 | { 33 | loader: 'css-loader', 34 | options: cssLoaderOptions, 35 | }, 36 | { 37 | loader: 'postcss-loader', 38 | options: { 39 | plugins() { 40 | return [ 41 | require('postcss-cssnext'), // eslint-disable-line global-require 42 | ]; 43 | }, 44 | }, 45 | }, 46 | ], 47 | }), 48 | }, 49 | { 50 | test: /\.js$/, 51 | exclude: /node_modules/, 52 | use: [ 53 | { 54 | loader: 'babel-loader', 55 | options: { 56 | ...babelLoaderOptions, 57 | presets: [ 58 | [ 59 | 'env', 60 | { 61 | targets: { 62 | browsers: '> 1%, Last 2 versions', 63 | }, 64 | modules: false, 65 | }, 66 | ], 67 | 'react', 68 | ], 69 | }, 70 | }, 71 | ], 72 | }, 73 | { 74 | exclude: /\.(css|js|json)$/, 75 | use: [ 76 | { 77 | loader: 'url-loader', 78 | options: urlLoaderOptions, 79 | }, 80 | ], 81 | }, 82 | ], 83 | }, 84 | plugins: [ 85 | ...common.plugins, 86 | ...(isProduction 87 | ? [ 88 | new AssetsPlugin({ 89 | filename: 'assets.json', 90 | path: common.output.path, 91 | }), 92 | ] 93 | : [new webpack.HotModuleReplacementPlugin()] 94 | ), 95 | new ExtractTextPlugin({ 96 | allChunks: true, 97 | filename: `css/style${isProduction ? '.[contenthash:8]' : ''}.css`, 98 | }), 99 | new webpack.optimize.CommonsChunkPlugin({ 100 | name: 'vendor', 101 | minChunks: ({ resource }) => /node_modules/.test(resource), 102 | }), 103 | new WebpackMd5Hash(), 104 | ], 105 | bail: isProduction, 106 | }; 107 | -------------------------------------------------------------------------------- /webpack/webpack.server.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved, global-require */ 2 | import nodeExternals from 'webpack-node-externals'; 3 | import webpack from 'webpack'; 4 | 5 | import common, { 6 | babelLoaderOptions, 7 | cssLoaderOptions, 8 | urlLoaderOptions, 9 | } from './common'; 10 | 11 | const isProduction = process.env.NODE_ENV === 'production'; 12 | 13 | export default { 14 | name: 'server', 15 | entry: './src/server', 16 | target: 'node', 17 | node: { 18 | __dirname: false, 19 | }, 20 | output: { 21 | ...common.output, 22 | filename: 'server.js', 23 | libraryTarget: 'commonjs2', 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.css$/, 29 | use: { 30 | loader: 'css-loader/locals', 31 | options: cssLoaderOptions, 32 | }, 33 | }, 34 | { 35 | test: /\.js$/, 36 | exclude: /node_modules/, 37 | use: [ 38 | { 39 | loader: 'babel-loader', 40 | options: { 41 | ...babelLoaderOptions, 42 | presets: [ 43 | [ 44 | 'env', 45 | { 46 | targets: { 47 | node: true, 48 | }, 49 | modules: false, 50 | }, 51 | ], 52 | 'react', 53 | ], 54 | }, 55 | }, 56 | ], 57 | }, 58 | { 59 | exclude: /\.(css|js|json)$/, 60 | use: [ 61 | { 62 | loader: 'url-loader', 63 | options: { 64 | ...urlLoaderOptions, 65 | emitFile: false, 66 | }, 67 | }, 68 | ], 69 | }, 70 | ], 71 | }, 72 | plugins: [ 73 | ...common.plugins, 74 | new webpack.DefinePlugin({ 75 | CSS_BUNDLE: JSON.stringify( 76 | isProduction 77 | ? require('../build/assets.json').client.css 78 | : '/css/style.css', 79 | ), 80 | CLIENT_BUNDLE: JSON.stringify( 81 | isProduction 82 | ? require('../build/assets.json').client.js 83 | : '/js/client.js', 84 | ), 85 | VENDOR_BUNDLE: JSON.stringify( 86 | isProduction 87 | ? require('../build/assets.json').vendor.js 88 | : '/js/vendor.js', 89 | ), 90 | 'process.env': { 91 | PORT: JSON.stringify(process.env.PORT), 92 | }, 93 | }), 94 | ], 95 | externals: [nodeExternals()], 96 | bail: isProduction, 97 | }; 98 | --------------------------------------------------------------------------------