├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .projections.json ├── Procfile ├── README.md ├── package.json ├── setup-tests.js ├── src ├── applications │ └── index │ │ ├── index.html │ │ └── index.jsx ├── components │ ├── application │ │ ├── index.jsx │ │ └── index.test.jsx │ └── header │ │ └── index.jsx └── services │ ├── environments │ ├── development.js │ ├── index.js │ └── production.js │ ├── index.js │ └── rendering.jsx └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "retainLines": true, 3 | "presets": [ 4 | ["env", { 5 | "targets": { 6 | "browsers": ["last 2 versions"] 7 | } 8 | }], 9 | "stage-0", 10 | "react", 11 | "flow" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | 5 | "plugins": [ 6 | "react", 7 | "jsx-a11y", 8 | "prettier", 9 | "flowtype", 10 | "import" 11 | ], 12 | 13 | "globals": { 14 | "__dirname": true, 15 | "console": true, 16 | "document": true, 17 | "expect": true, 18 | "module": true, 19 | "process": true, 20 | "require": true, 21 | "test": true 22 | }, 23 | 24 | "parserOptions": { 25 | "sourceType": "module", 26 | "ecmaFeatures": { 27 | "jsx": true 28 | } 29 | }, 30 | 31 | "env": { 32 | "es6": true, 33 | "browser": true, 34 | "node": true 35 | }, 36 | 37 | "rules": { 38 | "strict": [2, "never"], 39 | "react/jsx-uses-react": 2, 40 | "react/jsx-uses-vars": 2, 41 | "react/react-in-jsx-scope": 2, 42 | "semi": ["error", "never"], 43 | "max-len": ["error", 79], 44 | 45 | "prettier/prettier": ["error", { 46 | "bracketSpacing": false, 47 | "jsxBracketSameLine": true, 48 | "parser": "flow", 49 | "printWidth": 79, 50 | "semi": false, 51 | "singleQuote": true, 52 | "tabWidth": 2, 53 | "trailingComma": "all", 54 | "useTabs": false 55 | }], 56 | 57 | "space-before-function-paren": ["error", { 58 | "anonymous": "never", 59 | "named": "never", 60 | "asyncArrow": "always" 61 | }] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | \/node_modules\/.* 3 | \/dist\/.* 4 | 5 | [include] 6 | /src 7 | 8 | [libs] 9 | 10 | 11 | [options] 12 | all=true 13 | emoji=true 14 | esproposal.class_instance_fields=ignore 15 | esproposal.class_static_fields=ignore 16 | esproposal.decorators=ignore 17 | experimental.const_params=true 18 | module.file_ext=.js 19 | module.file_ext=.jsx 20 | module.system.node.resolve_dirname=node_modules 21 | module.system.node.resolve_dirname=src 22 | module.system=node 23 | module.use_strict=true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Log files (usually from NPM) 2 | *.log 3 | 4 | # Server-side rendering output from `npm run build` 5 | /lib/ 6 | 7 | # Build output 8 | /dist/* 9 | 10 | 11 | # Package locking 12 | *-lock.json 13 | 14 | 15 | # Node dependencies 16 | /node_modules/* 17 | 18 | 19 | # Typings 20 | /flow-typed/ 21 | 22 | 23 | # Non-git hidden file patterns 24 | .* 25 | !.git* 26 | 27 | # Configuration files 28 | !.babelrc 29 | !.flowconfig 30 | !.eslintrc 31 | !.projections.json 32 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/applications/*/index.jsx": { 3 | "type": "App", 4 | "alternate": "src/applications/{snakecase}/index.test.jsx" 5 | }, 6 | 7 | "src/applications/index/index.jsx": { 8 | "type": "App", 9 | "alternate": "src/applications/index/index.test.jsx" 10 | }, 11 | 12 | "src/components/*/index.jsx": { 13 | "type": "Component", 14 | "alternate": "src/components/{snakecase}/index.test.jsx", 15 | "template": [ 16 | "import React from 'react'", 17 | "", 18 | "export default () =>

{camelcase|capitalize}

" 19 | ] 20 | }, 21 | 22 | "src/services/*.js": { 23 | "type": "Service", 24 | "alternate": "src/services/{snakecase}.test.js" 25 | }, 26 | 27 | "README.md": { 28 | "type": "Readme" 29 | }, 30 | 31 | "package.json": { 32 | "type": "package" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run production 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Webpack Starter 2 | ===================== 3 | 4 | 5 | This is a project providing a minimal boilerplate for getting started with 6 | server-side rendering as well as static rendering for React projects. 7 | 8 | To create a new React application with it, you can follow the following steps: 9 | 10 | > git clone https://github.com/monokrome/webpack-react-starter myproject 11 | 12 | > cd myproject 13 | 14 | > npm install 15 | 16 | To run a development (not production-ready yet) server for server-side 17 | rendering: 18 | 19 | > npm start 20 | 21 | You can also run without server-side rendering using: 22 | 23 | > npm run develop 24 | 25 | You can reformat, lint, and type-check your code all at once with: 26 | 27 | > npm run lint 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-react-starter", 3 | "version": "0.1.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "cross-env NODE_ENV=production webpack -p && babel src -d lib", 7 | "dev": "npm run development", 8 | "develop": "npm run development", 9 | "development": "cross-env NODE_ENV=development npm run services", 10 | "frontend": "cross-env NODE_ENV=development webpack-dev-server --hot --inline", 11 | "lint": "flow-typed install && eslint --fix \"src/**/*.js\" \"src/**/*.jsx\"", 12 | "precommit": "npm run lint", 13 | "production": "cross-env NODE_PATH=./lib NODE_ENV=production node lib/services", 14 | "services": "cross-env NODE_PATH=./src nodemon -d 3 -w package.json -w .babelrc -w webpack.config.js -e js -e jsx -x babel-node src/services", 15 | "start": "cross-env NODE_ENV=development npm run services", 16 | "test": "jest", 17 | "tests": "jest --watch --notify" 18 | }, 19 | "author": "Bailey Stoner ", 20 | "license": "MIT", 21 | "dependencies": { 22 | "express": "^4.15.3", 23 | "isomorphic-fetch": "^2.2.1", 24 | "jsdom": "^11.0.0", 25 | "nodemon": "^1.11.0", 26 | "react": "^16.1.1", 27 | "react-dom": "^16.1.1", 28 | "styled-components": "^2.2.3", 29 | "cross-env": "^5.1.1", 30 | "url-loader": "^0.6.2" 31 | }, 32 | "devDependencies": { 33 | "autoprefixer": "^7.1.1", 34 | "babel-cli": "^6.24.1", 35 | "babel-core": "^6.24.1", 36 | "babel-eslint": "^8.0.2", 37 | "babel-loader": "^7.0.0", 38 | "babel-preset-env": "^1.5.1", 39 | "babel-preset-flow": "^6.23.0", 40 | "babel-preset-react": "^6.24.1", 41 | "babel-preset-stage-0": "^6.24.1", 42 | "enzyme": "^3.2.0", 43 | "enzyme-adapter-react-16": "^1.1.0", 44 | "eslint": "^4.11.0", 45 | "eslint-plugin-flowtype": "^2.34.0", 46 | "eslint-plugin-import": "^2.3.0", 47 | "eslint-plugin-jsx-a11y": "^6.0.2", 48 | "eslint-plugin-prettier": "^2.3.1", 49 | "eslint-plugin-react": "^7.1.0", 50 | "extract-loader": "^1.0.1", 51 | "file-loader": "^1.1.5", 52 | "flow": "^0.2.3", 53 | "flow-bin": "^0.59.0", 54 | "flow-typed": "^2.2.3", 55 | "glob": "^7.1.2", 56 | "html-loader": "^0.5.1", 57 | "jest": "^21.2.1", 58 | "jest-enzyme": "^4.0.1", 59 | "pleeease-filters": "^4.0.0", 60 | "prettier": "^1.8.2", 61 | "prettier-eslint-cli": "^4.4.0", 62 | "prop-types": "^15.5.10", 63 | "raf": "^3.4.0", 64 | "webpack": "^3.8.1", 65 | "webpack-dev-server": "^2.4.5" 66 | }, 67 | "jest": { 68 | "setupTestFrameworkScriptFile": "/setup-tests.js", 69 | "moduleFileExtensions": [ 70 | "js", 71 | "jsx" 72 | ], 73 | "moduleDirectories": [ 74 | "node_modules", 75 | "src" 76 | ], 77 | "moduleNameMapper": { 78 | "\\.(css|less)$": "/fixtures/static/style.js", 79 | "\\.(gif|ttf|eot|svg)$": "/fixtures/static/style.js" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /setup-tests.js: -------------------------------------------------------------------------------- 1 | import 'raf/polyfill' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | import {configure} from 'enzyme' 4 | configure({adapter: new Adapter()}) 5 | -------------------------------------------------------------------------------- /src/applications/index/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome to React 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/applications/index/index.jsx: -------------------------------------------------------------------------------- 1 | import './index.html' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | 6 | import Application from 'components/application' 7 | 8 | ReactDOM.render(, document.getElementById('root')) 9 | 10 | if (module.hot) module.hot.accept() 11 | -------------------------------------------------------------------------------- /src/components/application/index.jsx: -------------------------------------------------------------------------------- 1 | import Header from 'components/header' 2 | import React from 'react' 3 | 4 | export default () =>
5 | -------------------------------------------------------------------------------- /src/components/application/index.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {shallow} from 'enzyme' 3 | 4 | import Application from './index' 5 | 6 | describe('Application', function() { 7 | it('should render', () => expect(true).toBe(true)) 8 | }) 9 | -------------------------------------------------------------------------------- /src/components/header/index.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import React from 'react' 3 | 4 | import styled from 'styled-components' 5 | 6 | class Header extends React.Component { 7 | static propTypes = { 8 | title: PropTypes.string.isRequired, 9 | className: PropTypes.string.isRequired, 10 | } 11 | 12 | render() { 13 | const {className, title} = this.props 14 | return

{title}

15 | } 16 | } 17 | 18 | export default styled(Header)` 19 | margin: calc(50vh - 0.8em) auto 0; 20 | width: 2.5ex; 21 | height: 0.6em; 22 | line-height: 0.9em; 23 | background-color: #090939; 24 | color: #fff; 25 | overflow: hidden; 26 | font-size: 8rem; 27 | text-align: center; 28 | border-radius: 1rem 0 1rem 0; 29 | ` 30 | -------------------------------------------------------------------------------- /src/services/environments/development.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import WebpackDevServer from 'webpack-dev-server' 3 | 4 | import ReactRenderingService from './index' 5 | import configuration from '../../../webpack.config' 6 | 7 | const DEV_CLIENT_PATH = 'webpack-dev-server/client?http://localhost:3030' 8 | 9 | configuration.plugins.push(new webpack.HotModuleReplacementPlugin()) 10 | configuration.plugins.push(new webpack.NamedModulesPlugin()) 11 | 12 | // Prepends code for the websocket client and HMR support 13 | for (const key in configuration.entry) { 14 | configuration.entry[key].unshift(DEV_CLIENT_PATH) 15 | configuration.entry[key].unshift('webpack/hot/dev-server') 16 | } 17 | 18 | export default class extends ReactRenderingService { 19 | createServer() { 20 | this.setService( 21 | new WebpackDevServer(webpack(configuration), { 22 | hot: true, 23 | lazy: false, 24 | }), 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/services/environments/index.js: -------------------------------------------------------------------------------- 1 | export default class ReactRenderingService { 2 | options: Object 3 | render: Function 4 | service: Object 5 | 6 | constructor(render: Function, options: Object = {}) { 7 | this.options = options 8 | this.render = render 9 | 10 | this.createServer() 11 | } 12 | 13 | createServer() { 14 | throw new Error( 15 | 'createServer must be implemented on ReactRenderingService subclasses.', 16 | ) 17 | } 18 | 19 | setService(service: Object) { 20 | this.service = service 21 | this.listen(this.options.port) 22 | return this.service 23 | } 24 | 25 | handleRequest(request: Object, response: Object) { 26 | response.writeHead(200, {'Content-Type': 'text/html'}) 27 | return response.end(this.render(request)) 28 | } 29 | 30 | listen(port: number) { 31 | this.service.listen(process.env.PORT || port || 3030) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/services/environments/production.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import render from '../rendering' 4 | import ReactRenderingService from './index' 5 | 6 | const STATIC_PATH = process.env.STATIC_PATH || 'dist' 7 | 8 | function renderToResponse(request: Object, response: Object) { 9 | response.writeHead(200, {'Content-Type': 'text/html'}) 10 | response.end(render(request)) 11 | } 12 | 13 | export default class extends ReactRenderingService { 14 | createServer() { 15 | const service = express() 16 | service.use(express.static(STATIC_PATH, {index: false})) 17 | service.get('/', renderToResponse) 18 | this.setService(service) 19 | return service 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | import render from './rendering' 2 | 3 | function getServiceClass() { 4 | switch (process.env.NODE_ENV || 'development') { 5 | case 'production': 6 | return require('./environments/production').default 7 | default: 8 | return require('./environments/development').default 9 | } 10 | } 11 | 12 | const Service = getServiceClass() 13 | export default new Service(render) 14 | -------------------------------------------------------------------------------- /src/services/rendering.jsx: -------------------------------------------------------------------------------- 1 | import DOM from 'react-dom/server' 2 | import React from 'react' 3 | import fs from 'fs' 4 | 5 | import {ServerStyleSheet} from 'styled-components' 6 | 7 | import Application from '../components/application' 8 | 9 | export default function render(request: Object) { 10 | const sheet = new ServerStyleSheet() 11 | const document = DOM.renderToString(sheet.collectStyles()) 12 | const stylesheet = sheet.getStyleTags() 13 | 14 | return ` 15 | 16 | 17 | 18 | 19 | 20 | Welcome to React 21 | ${stylesheet} 22 | 23 | 24 | 25 |
${document}
26 | 27 | 28 | 29 | ` 30 | } 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | const APPLICATION_ENTRIES = {} 5 | const SCRIPT_LOADERS = ['babel-loader?retainLines=true'] 6 | const APPLICATIONS_PATH = path.join(__dirname, 'src', 'applications') 7 | 8 | for (const moduleName of fs.readdirSync(APPLICATIONS_PATH)) 9 | APPLICATION_ENTRIES[moduleName] = [path.join(APPLICATIONS_PATH, moduleName)] 10 | 11 | module.exports = { 12 | context: __dirname, 13 | devtool: 'sourcemap', 14 | entry: APPLICATION_ENTRIES, 15 | 16 | output: { 17 | path: path.resolve('dist'), 18 | filename: 'index.js', 19 | }, 20 | 21 | resolve: { 22 | alias: {}, 23 | extensions: ['.js', '.jsx'], 24 | modules: [path.resolve('node_modules'), path.resolve('src')], 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | { 30 | use: SCRIPT_LOADERS, 31 | test: /\.jsx?$/, 32 | 33 | exclude: [path.resolve('node_modules')], 34 | }, 35 | 36 | { 37 | test: /\.html$/, 38 | use: [ 39 | 'file-loader?name=[name].[ext]', 40 | 'extract-loader', 41 | 'html-loader', 42 | ], 43 | }, 44 | 45 | {loader: 'url-loader', test: /\.png$/}, 46 | {loader: 'file-loader', test: /\.(ttf|eot|svg)$/}, 47 | ], 48 | }, 49 | 50 | watchOptions: { 51 | ignored: /node_modules/, 52 | }, 53 | 54 | devServer: { 55 | port: 3030, 56 | contentBase: path.resolve('src'), 57 | 58 | stats: 'errors-only', 59 | 60 | overlay: { 61 | warnings: true, 62 | errors: true, 63 | }, 64 | }, 65 | 66 | plugins: [], 67 | } 68 | --------------------------------------------------------------------------------