├── .babelrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── app ├── components │ ├── App.jsx │ ├── Layout.jsx │ ├── Logo.jsx │ └── Piece.jsx ├── images │ ├── Babel.png │ ├── React.svg │ ├── Sass.svg │ └── Webpack.gif ├── index.html ├── index.jsx ├── lib.js ├── routes.jsx └── styles │ └── site.sass ├── firebase.json ├── package.json ├── server ├── entry.jsx └── nginx.conf ├── webpack.config.js ├── webpack.server.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }], 4 | "react", 5 | "stage-1" 6 | ], 7 | "env": { 8 | "development": { 9 | "presets": ["react-hmre"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "max-len": "off", 6 | "comma-dangle": "off", 7 | "no-else-return": "off", 8 | "no-mixed-operators": "off", 9 | "prefer-const": "warn", 10 | "prefer-template": "warn", 11 | "no-extend-native": "warn", 12 | "no-script-url": "warn", 13 | "no-use-before-define": "warn", 14 | "object-curly-spacing": ["error", "never"], 15 | "global-require": "warn", 16 | "jsx-a11y/img-has-alt": "warn", 17 | "react/prefer-stateless-function": "warn" 18 | }, 19 | "plugins": [ 20 | "react" 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | temp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Junfeng Liu 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webpack+React+Babel+Sass 2 | It took me several days to learn and assemble these pieces, so this boilerplate can save others time. 3 | 4 | DEPRECATED: Use [parcel-react-boilerplate](https://github.com/J-F-Liu/parcel-react-boilerplate.git) instead. 5 | 6 | # Usage 7 | 8 | 1. Install Node.js and Git 9 | 10 | 2. Download the repository 11 | ``` 12 | git clone https://github.com/J-F-Liu/webpack-react-boilerplate.git 13 | cd webpack-react-boilerplate 14 | ``` 15 | 3. Checkout `react-router` branch if you want to use react-router 16 | 17 | `git checkout react-router` 18 | 19 | 4. Install npm packages 20 | 21 | `npm install` 22 | 23 | 5. Start dev server 24 | 25 | `npm start` 26 | 27 | 6. Build website for production enviroment 28 | 29 | `npm run build` 30 | 31 | View [online demo](http://j-f-liu.github.io/webpack-react-boilerplate/).
32 | View [online demo with react-router](https://webpack-react-boiler.firebaseapp.com/). 33 | 34 | # Trouble shooting 35 | ``` 36 | ERROR in ./~/css-loader!./~/sass-loader?indentedSyntax=true!./app/styles/site.sass 37 | Module build failed: Error: Node Sass does not yet support your current environment: Linux 64-bit with Unsupported runtime (51) 38 | ``` 39 | or 40 | ``` 41 | ERROR in ./~/css-loader!./~/sass-loader?indentedSyntax=true!./app/styles/site.sass 42 | Module build failed: Error: ENOENT: no such file or directory, scandir 43 | ``` 44 | Run `npm rebuild node-sass`. 45 | 46 | When using Microsoft Windows, install [Python 2.7](https://www.python.org/downloads/windows/) and [Visual C++ Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools), then run 47 | ``` 48 | npm install node-sass --msvs_version=2015 49 | npm rebuild node-sass 50 | ``` 51 | -------------------------------------------------------------------------------- /app/components/App.jsx: -------------------------------------------------------------------------------- 1 | export default class App { 2 | static pieces = [ 3 | {name: 'webpack', link: 'http://webpack.github.io/', logo: 'Webpack.gif'}, 4 | {name: 'React', link: 'http://facebook.github.io/react/', logo: 'React.svg'}, 5 | {name: 'Babel', link: 'http://babeljs.io/', logo: 'Babel.png'}, 6 | {name: 'Sass', link: 'http://sass-lang.com/', logo: 'Sass.svg'}, 7 | ]; 8 | } 9 | -------------------------------------------------------------------------------- /app/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Piece from './Piece'; 4 | import App from './App'; 5 | 6 | export default function Layout(props) { 7 | return ( 8 |
9 |

10 | {App.pieces.map((piece, i) => ).insertSeparator('+')} 11 |

12 |

13 | {props.children} 14 |

15 |
16 | ); 17 | } 18 | 19 | Layout.propTypes = { 20 | children: PropTypes.node 21 | }; 22 | -------------------------------------------------------------------------------- /app/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import App from './App'; 4 | 5 | export default class Logo extends Component { 6 | static propTypes = { 7 | params: PropTypes.shape({ 8 | name: PropTypes.string 9 | }) 10 | }; 11 | 12 | state = {link: '', image: ''}; 13 | 14 | componentDidMount() { 15 | this.loadLogo(this.props.params.name); 16 | } 17 | 18 | componentWillReceiveProps(nextProps) { 19 | const name = nextProps.params.name; 20 | if (name !== this.props.params.name) { 21 | this.loadLogo(name); 22 | } 23 | } 24 | 25 | loadLogo(name) { 26 | const piece = App.pieces.find(p => p.name === name); 27 | const imageUrl = require(`../images/${piece.logo}`); 28 | this.setState({link: piece.link, image: imageUrl}); 29 | } 30 | 31 | render() { 32 | return ; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/components/Piece.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Link} from 'react-router'; 4 | 5 | export default class Piece extends Component { 6 | static propTypes = { 7 | name: PropTypes.string.isRequired, 8 | }; 9 | 10 | state = {active: false}; 11 | 12 | handleMouseOver = () => { 13 | this.setState({active: true}); 14 | }; 15 | 16 | handleMouseOut = () => { 17 | this.setState({active: false}); 18 | }; 19 | 20 | render() { 21 | if (this.state.active) { 22 | return {this.props.name}; 23 | } else { 24 | return {this.props.name}; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/images/Babel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J-F-Liu/webpack-react-boilerplate/8aca5c53997fbabd67bcf68560410a0f606e7724/app/images/Babel.png -------------------------------------------------------------------------------- /app/images/React.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /app/images/Sass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/images/Webpack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J-F-Liu/webpack-react-boilerplate/8aca5c53997fbabd67bcf68560410a0f606e7724/app/images/Webpack.gif -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Web App 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Router, browserHistory} from 'react-router'; 4 | import routes from './routes'; 5 | import './styles/site.sass'; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('app') 10 | ); 11 | -------------------------------------------------------------------------------- /app/lib.js: -------------------------------------------------------------------------------- 1 | Array.prototype.insertSeparator = function (sep) { 2 | if (this === null || this.length < 2) return this; 3 | let result = new Array(this.length * 2 - 1); 4 | for (let i = 0; i < this.length; i++) { 5 | result[i * 2] = this[i]; 6 | if (i < this.length - 1) { 7 | result[i * 2 + 1] = sep; 8 | } 9 | } 10 | return result; 11 | }; 12 | 13 | if (!Array.prototype.find) { 14 | Array.prototype.find = function (predicate) { 15 | if (this === null) { 16 | throw new TypeError('Array.prototype.find called on null or undefined'); 17 | } 18 | if (typeof predicate !== 'function') { 19 | throw new TypeError('predicate must be a function'); 20 | } 21 | const list = Object(this); 22 | const length = list.length >>> 0; 23 | let value; 24 | 25 | for (let i = 0; i < length; i++) { 26 | value = list[i]; 27 | if (predicate.call(null, value, i, list)) { 28 | return value; 29 | } 30 | } 31 | return undefined; 32 | }; 33 | } 34 | 35 | /** 36 | * @param {string} string 37 | * @param {number} index 38 | * @param {string} substring 39 | * @returns {string} New string 40 | */ 41 | export function insertString(string, index, substring) { 42 | return [string.slice(0, index), substring, string.slice(index)].join(""); 43 | } 44 | -------------------------------------------------------------------------------- /app/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route} from 'react-router'; 3 | import './lib'; 4 | 5 | import Layout from './components/Layout'; 6 | import Logo from './components/Logo'; 7 | 8 | export default ( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /app/styles/site.sass: -------------------------------------------------------------------------------- 1 | body 2 | font-family: "source-serif-pro",Georgia,"Times New Roman",Times,serif 3 | color: #3a87ad 4 | h1, p 5 | text-align: center 6 | img 7 | max-height: 480px 8 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": "webpack-react-boiler", 3 | "public": "build", 4 | "rewrites": [ { 5 | "source": "**", 6 | "destination": "/index.html" 7 | } ], 8 | "ignore": [ 9 | "firebase.json", 10 | "**/.*", 11 | "**/node_modules/**" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-react-boilerplate", 3 | "version": "2.0.0", 4 | "description": "webpack-react-boilerplate", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "prebuild": "rimraf dist/client", 8 | "build": "cross-env NODE_ENV=production webpack", 9 | "start": "cross-env NODE_ENV=development webpack-dev-server", 10 | "lint": "eslint --ext .js,.jsx app", 11 | "build:server": "cross-env NODE_ENV=production webpack --config webpack.server.js", 12 | "serve": "npm run build:server && node dist/server/entry.js" 13 | }, 14 | "keywords": [], 15 | "author": "Junfeng Liu", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "babel-core": "^6.18.0", 19 | "babel-eslint": "^7.1.0", 20 | "babel-loader": "^7.0.0", 21 | "babel-polyfill": "^6.9.1", 22 | "babel-preset-es2015": "^6.3.13", 23 | "babel-preset-react": "^6.3.13", 24 | "babel-preset-react-hmre": "^1.0.1", 25 | "babel-preset-stage-1": "^6.5.0", 26 | "babel-register": "^6.9.0", 27 | "command-line-args": "^4.0.4", 28 | "cross-env": "^5.0.0", 29 | "css-loader": "^0.28.0", 30 | "eslint": "^4.18.2", 31 | "eslint-config-airbnb": "^15.0.0", 32 | "eslint-plugin-import": "^2.0.1", 33 | "eslint-plugin-jsx-a11y": "^5.0.1", 34 | "eslint-plugin-react": "^7.0.1", 35 | "extract-text-webpack-plugin": "^2.1.0", 36 | "file-loader": "^0.11.1", 37 | "git-revision": "0.0.2", 38 | "html-webpack-plugin": "^2.28.0", 39 | "img-loader": "^2.0.0", 40 | "koa": "^2.2.0", 41 | "koa-compress": "^2.0.0", 42 | "koa-static": "^3.0.0", 43 | "node-sass": "^4.5.2", 44 | "pem": "^1.8.3", 45 | "rimraf": "^2.5.2", 46 | "sass-loader": "^6.0.5", 47 | "spdy": "^3.3.3", 48 | "style-loader": "^0.17.0", 49 | "webpack": "^2.5.1", 50 | "webpack-dev-server": ">=3.1.11", 51 | "webpack-merge": "^4.1.0" 52 | }, 53 | "dependencies": { 54 | "react": "^15.5.4", 55 | "react-dom": "^15.5.4", 56 | "react-router": "^3.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/entry.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-register'; 2 | import 'babel-polyfill'; 3 | 4 | import Koa from 'koa'; 5 | import serve from 'koa-static'; 6 | import compress from 'koa-compress'; 7 | import commandLineArgs from 'command-line-args'; 8 | import React from 'react'; 9 | import ReactDOMServer from 'react-dom/server'; 10 | import { match, RouterContext } from 'react-router'; 11 | 12 | import fs from 'fs'; 13 | import pem from 'pem'; 14 | import spdy from 'spdy'; 15 | import {insertString} from '../app/lib'; 16 | import routes from '../app/routes'; 17 | 18 | function generatePage(content) { 19 | let html = fs.readFileSync('dist/client/index.html', 'utf8'); 20 | let mountPoint = '
'; 21 | let insertPoint = html.indexOf(mountPoint) + mountPoint.length; 22 | return insertString(html, insertPoint, content); 23 | } 24 | 25 | const app = new Koa(); 26 | 27 | app.use(async (ctx, next) => { 28 | try { 29 | await next(); 30 | } catch (err) { 31 | ctx.body = `

${err.message}

${err.stack}
`; 32 | ctx.status = err.status || 500; 33 | } 34 | }); 35 | 36 | app.use(compress({ 37 | threshold: 2048, 38 | flush: require('zlib').Z_SYNC_FLUSH 39 | })) 40 | 41 | app.use(async (ctx, next) => { 42 | let matched = true; 43 | match({routes, location: ctx.url}, (error, redirectLocation, renderProps) => { 44 | if (error) { 45 | ctx.status = 500; 46 | ctx.message = error.message; 47 | } else if (redirectLocation) { 48 | ctx.redirect(302, redirectLocation.pathname + redirectLocation.search) 49 | } else if (renderProps) { 50 | ctx.status = 200; 51 | ctx.body = generatePage(ReactDOMServer.renderToString()); 52 | } else { 53 | matched = false; 54 | } 55 | }); 56 | if(!matched) { 57 | await next(); 58 | } 59 | }); 60 | 61 | app.use(serve('dist/client')); 62 | 63 | const optionDefinitions = [ 64 | { name: 'port', alias: 'p', type: Number, defaultValue: 3000 }, 65 | { name: 'https', alias: 's', type: Boolean, defaultValue: false }, 66 | ]; 67 | const options = commandLineArgs(optionDefinitions); 68 | 69 | if (!options.https) { 70 | app.listen(options.port); 71 | console.log(`Listening on http://localhost:${options.port}`); 72 | } else { 73 | pem.createCertificate({ 74 | days: 1, 75 | selfSigned: true 76 | }, function(err, keys){ 77 | const credentials = { 78 | key: keys.serviceKey, 79 | cert: keys.certificate 80 | }; 81 | 82 | const server = spdy.createServer(credentials, app.callback()); 83 | server.listen(options.port, function() { 84 | console.log(`Listening on https://localhost:${options.port}`); 85 | }); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /server/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name your_domain.com; 4 | 5 | location / { 6 | root /var/web/your_project/dist/client; 7 | try_files $uri /index.html; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const HtmlwebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const gitRevision = require('git-revision'); 7 | const packages = require('./package.json'); 8 | 9 | const BUILD = process.env.BABEL_ENV = process.env.NODE_ENV; 10 | 11 | const PATHS = { 12 | app: path.join(__dirname, 'app'), 13 | dist: path.join(__dirname, 'dist/client'), 14 | }; 15 | 16 | const common = { 17 | output: { 18 | path: PATHS.dist, 19 | publicPath: '/' 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.jsx'] 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.jsx?$/, 28 | use: ['babel-loader'], 29 | include: PATHS.app 30 | }, 31 | { 32 | test: /\.(jpe?g|png|gif|svg)$/i, 33 | use: ['file-loader?name=img/[hash].[ext]', 'img-loader?-minimize'] 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new HtmlwebpackPlugin({ 39 | template: 'app/index.html', 40 | inject: 'body' 41 | }) 42 | ] 43 | }; 44 | 45 | const different = function(build) { 46 | switch(build) { 47 | case "development": 48 | return { 49 | entry: PATHS.app, 50 | output: { 51 | filename: 'bundle.js' 52 | }, 53 | module: { 54 | rules: [ 55 | { 56 | test: /\.sass$/, 57 | use: ['style-loader', 'css-loader', 'sass-loader?indentedSyntax=true'], 58 | include: PATHS.app 59 | } 60 | ] 61 | }, 62 | // devtool: 'eval-source-map', 63 | devServer: { 64 | historyApiFallback: true, 65 | hot: true, 66 | inline: true, 67 | stats: 'errors-only', 68 | host: process.env.HOST, 69 | port: parseInt(process.env.PORT) 70 | }, 71 | plugins: [ 72 | new webpack.HotModuleReplacementPlugin() 73 | ] 74 | }; 75 | case "production": 76 | return { 77 | entry: { 78 | app: PATHS.app, 79 | vendor: Object.keys(packages.dependencies) 80 | }, 81 | output: { 82 | filename: 'js/[name].[chunkhash].js', 83 | chunkFilename: '[chunkhash].js' 84 | }, 85 | module: { 86 | rules: [ 87 | { 88 | test: /\.sass$/, 89 | use: ExtractTextPlugin.extract({ 90 | fallback: "style-loader", 91 | use: ['css-loader', 'sass-loader?indentedSyntax=true'] 92 | }), 93 | include: PATHS.app 94 | } 95 | ] 96 | }, 97 | plugins: [ 98 | new webpack.DefinePlugin({ 99 | "process.env": { 100 | VERSION: JSON.stringify(gitRevision("short")), 101 | NODE_ENV: JSON.stringify("production"), 102 | } 103 | }), 104 | new ExtractTextPlugin("css/[name].[chunkhash].css"), 105 | new webpack.optimize.CommonsChunkPlugin({ 106 | names: ['vendor', 'manifest'] 107 | }), 108 | new webpack.optimize.UglifyJsPlugin({ 109 | compress: {warnings: false} 110 | }) 111 | ] 112 | }; 113 | default: 114 | return {}; 115 | } 116 | }(BUILD); 117 | 118 | module.exports = merge(common, different); 119 | -------------------------------------------------------------------------------- /webpack.server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const merge = require('webpack-merge'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const packages = require('./package.json'); 7 | 8 | const BUILD = process.env.BABEL_ENV = process.env.NODE_ENV; 9 | 10 | const PATHS = { 11 | app: path.join(__dirname, 'app'), 12 | server: path.join(__dirname, 'server/entry.jsx'), 13 | dist: path.join(__dirname, 'dist/server'), 14 | }; 15 | 16 | const common = { 17 | output: { 18 | path: PATHS.dist, 19 | publicPath: '/' 20 | }, 21 | target: 'node', 22 | externals: fs.readdirSync('node_modules'), 23 | resolve: { 24 | extensions: ['.js', '.jsx'] 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.jsx?$/, 30 | use: ['babel-loader'], 31 | include: [PATHS.app, PATHS.server] 32 | }, 33 | { 34 | test: /\.(jpe?g|png|gif|svg)$/i, 35 | use: ['file-loader?name=img/[hash].[ext]', 'img-loader?-minimize'] 36 | } 37 | ] 38 | } 39 | }; 40 | 41 | const different = function(build) { 42 | switch(build) { 43 | case "development": 44 | return { 45 | }; 46 | case "production": 47 | return { 48 | entry: PATHS.server, 49 | output: { 50 | filename: 'entry.js', 51 | libraryTarget: 'commonjs2' 52 | }, 53 | module: { 54 | rules: [ 55 | { 56 | test: /\.sass$/, 57 | use: ExtractTextPlugin.extract({ 58 | fallback: "style-loader", 59 | use: ['css-loader', 'sass-loader?indentedSyntax=true'] 60 | }), 61 | include: PATHS.app 62 | } 63 | ] 64 | }, 65 | plugins: [ 66 | new webpack.DefinePlugin({ 67 | "process.env": { 68 | NODE_ENV: JSON.stringify("production") 69 | } 70 | }), 71 | new ExtractTextPlugin("css/[chunkhash].css"), 72 | // new webpack.optimize.UglifyJsPlugin({ 73 | // compress: {warnings: false} 74 | // }) 75 | ] 76 | }; 77 | default: 78 | return {}; 79 | } 80 | }(BUILD); 81 | 82 | module.exports = merge(common, different); 83 | --------------------------------------------------------------------------------