├── src ├── styles │ └── app.css ├── server │ ├── public │ │ ├── tile.png │ │ ├── favicon.ico │ │ ├── robots.txt │ │ ├── tile-wide.png │ │ └── crossdomain.xml │ ├── content │ │ └── readme.md │ ├── middleware │ │ ├── buildClientConfig.js │ │ ├── error.js │ │ └── render-app.js │ ├── router.js │ ├── server.js │ └── templates │ │ ├── Error.jsx │ │ └── Html.jsx ├── app │ ├── about │ │ ├── constants.js │ │ ├── reducers │ │ │ └── about.js │ │ ├── index.jsx │ │ ├── attach.js │ │ └── containers │ │ │ └── About.jsx │ ├── core │ │ ├── constants.js │ │ ├── reducers │ │ │ ├── index.js │ │ │ └── core.js │ │ ├── env.js │ │ └── components │ │ │ └── Status.jsx │ ├── home │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers │ │ │ ├── home.spec.js │ │ │ └── home.js │ │ └── containers │ │ │ └── Home.jsx │ ├── store │ │ ├── dummyReducer.js │ │ ├── createReducer.js │ │ ├── withAsyncReducers.js │ │ └── store.js │ ├── ReactHotLoader.jsx │ └── Root.jsx ├── server-entry.js └── client-entry.js ├── config ├── README.md ├── vendor.js ├── babel.js ├── utils.js ├── assets.js ├── webpack.config.dev.js ├── webpack.config.server.js ├── paths.js ├── webpack.config.prod.js ├── environment.js └── webpack.common.js ├── .eslintingore ├── .env ├── tests └── setup.js ├── .editorconfig ├── .babelrc ├── postcss.config.js ├── .client.babelrc ├── .gitignore ├── LICENSE ├── .flowconfig ├── scripts ├── dev.js ├── logger.js ├── server.js └── client.js ├── .eslintrc.json ├── package.json └── README.md /src/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 16px; 3 | } 4 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | > App Config 2 | 3 | App config goes in here. 4 | -------------------------------------------------------------------------------- /config/vendor.js: -------------------------------------------------------------------------------- 1 | module.exports = ['react', 'redux', 'react-redux']; 2 | -------------------------------------------------------------------------------- /.eslintingore: -------------------------------------------------------------------------------- 1 | src/server/public/* 2 | build/* 3 | docker/* 4 | flow-typed/* 5 | -------------------------------------------------------------------------------- /src/server/public/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlebedynskyi/react-playground/HEAD/src/server/public/tile.png -------------------------------------------------------------------------------- /src/server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlebedynskyi/react-playground/HEAD/src/server/public/favicon.ico -------------------------------------------------------------------------------- /src/app/about/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export const ABOUT_SWITCH = 'about/SWITCH'; 3 | -------------------------------------------------------------------------------- /src/server/content/readme.md: -------------------------------------------------------------------------------- 1 | This folder should have static content that server should have access but not served to client. 2 | -------------------------------------------------------------------------------- /src/server/public/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /src/server/public/tile-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlebedynskyi/react-playground/HEAD/src/server/public/tile-wide.png -------------------------------------------------------------------------------- /src/app/core/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export const INITIAL_CONSTRUCT = 'INITIAL_CONSTRUCT'; 3 | -------------------------------------------------------------------------------- /src/app/home/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable import/prefer-default-export */ 3 | export const HOME_SWITCH = 'home/SWITCH'; 4 | -------------------------------------------------------------------------------- /src/app/store/dummyReducer.js: -------------------------------------------------------------------------------- 1 | import createReducer from './createReducer'; 2 | 3 | const dummy = createReducer({}); 4 | export default dummy; 5 | -------------------------------------------------------------------------------- /src/app/core/reducers/index.js: -------------------------------------------------------------------------------- 1 | import core from './core'; 2 | 3 | export default { 4 | core 5 | // rest of shared reducers like forms etc 6 | }; 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=5001 2 | DEV_ASSETS_PORT=5000 3 | PROTOCOL=http 4 | HOSTNAME=localhost 5 | 6 | STATIC_CACHE_DURATION=2592000000 7 | DYNAMIC_CACHE_DURATION=5 8 | -------------------------------------------------------------------------------- /src/app/core/reducers/core.js: -------------------------------------------------------------------------------- 1 | import createReducer from '../../store/createReducer'; 2 | 3 | const initialState = {}; 4 | export default createReducer(initialState, {}); 5 | -------------------------------------------------------------------------------- /config/babel.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = path => { 4 | const babelrc = fs.readFileSync(path); 5 | const config = JSON.parse(babelrc); 6 | return config; 7 | }; 8 | -------------------------------------------------------------------------------- /src/server/middleware/buildClientConfig.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // ditch whatever should not go to client 3 | export default ({ contentDir, ...rest }) => ({ 4 | ...rest 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/core/env.js: -------------------------------------------------------------------------------- 1 | let ENV = {}; 2 | 3 | export function configureENV(config) { 4 | ENV = config; 5 | } 6 | 7 | export function getENV() { 8 | return ENV; 9 | } 10 | 11 | export default getENV; 12 | -------------------------------------------------------------------------------- /src/app/home/index.js: -------------------------------------------------------------------------------- 1 | import Home from './containers/Home'; 2 | import reducer from './reducers/home'; 3 | import withAsyncReducers from '../store/withAsyncReducers'; 4 | 5 | export default withAsyncReducers('home', reducer)(Home); 6 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('ignore-styles'); 3 | 4 | global.document = require('jsdom').jsdom(''); 5 | 6 | global.window = document.defaultView; 7 | global.navigator = window.navigator; 8 | -------------------------------------------------------------------------------- /src/app/home/reducers/home.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import reducer from './home'; 3 | 4 | const initialState = { 5 | counter: 0 6 | }; 7 | 8 | test('initialState', t => { 9 | const state = reducer(); 10 | t.deepEqual(state, initialState, 'must have initialState'); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/about/reducers/about.js: -------------------------------------------------------------------------------- 1 | import createReducer from '../../store/createReducer'; 2 | import { ABOUT_SWITCH } from '../constants'; 3 | 4 | const initialState = { 5 | counter: 1 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [ABOUT_SWITCH]: state => ({ ...state, counter: state.counter + 1 }) 10 | }); 11 | -------------------------------------------------------------------------------- /config/utils.js: -------------------------------------------------------------------------------- 1 | const isDevelopment = () => process.env.NODE_ENV === 'development'; 2 | const isProduction = () => process.env.NODE_ENV === 'production'; 3 | const isNode = config => config.target === 'node'; 4 | 5 | const isHot = () => process.env.HOT_RELOAD !== 'false'; 6 | 7 | module.exports = { isDevelopment, isProduction, isNode, isHot }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 2 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /src/app/ReactHotLoader.jsx: -------------------------------------------------------------------------------- 1 | import { Children } from 'react'; 2 | 3 | // We create this wrapper so that we only import react-hot-laoder for a 4 | // development build. Small savings. :) 5 | const ReactHotLoader = process.env.NODE_ENV === 'development' 6 | ? require('react-hot-loader').AppContainer 7 | : ({ children }) => Children.only(children); 8 | 9 | export default ReactHotLoader; 10 | -------------------------------------------------------------------------------- /src/app/about/index.jsx: -------------------------------------------------------------------------------- 1 | import { createAsyncComponent } from 'react-async-component'; 2 | 3 | const AsyncAbout = createAsyncComponent({ 4 | name: 'about', 5 | resolve: () => 6 | new Promise(resolve => 7 | require.ensure( 8 | [], 9 | require => { 10 | resolve(require('./attach')); 11 | }, 12 | 'about' 13 | )) 14 | }); 15 | 16 | export default AsyncAbout; 17 | -------------------------------------------------------------------------------- /src/app/home/reducers/home.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { HOME_SWITCH } from '../constants'; 3 | 4 | const initialState: Object = { 5 | counter: 0 6 | }; 7 | 8 | const home = (state: Object = initialState, action: {type: string}) => { 9 | if (!action) { 10 | return state; 11 | } 12 | if (action.type === HOME_SWITCH) { 13 | return { ...state, counter: state.counter + 1 }; 14 | } 15 | 16 | return state; 17 | }; 18 | 19 | export default home; 20 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": [ 4 | "react", 5 | ["env", { 6 | "targets": { 7 | "node": true 8 | }, 9 | "useBuiltIns": true 10 | }] 11 | ], 12 | "plugins": [ 13 | "transform-class-properties", 14 | "transform-export-extensions", 15 | ["transform-react-jsx", { 16 | "useBuiltIns": true 17 | }], 18 | ["transform-object-rest-spread", { 19 | "useBuiltIns": true 20 | }] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('precss')(), 4 | require('autoprefixer')({ 5 | browsers: [ 6 | 'safari 9', 7 | 'ie 10-11', 8 | 'last 2 Chrome versions', 9 | 'last 2 Firefox versions', 10 | 'edge 13', 11 | 'ios_saf 9.0-9.2', 12 | 'ie_mob 11', 13 | 'Android >= 4' 14 | ], 15 | cascade: false, 16 | add: true, 17 | remove: true 18 | }) 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/home/containers/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { HOME_SWITCH } from '../constants'; 4 | 5 | const hoc = connect( 6 | state => ({ 7 | text: state && state.home && state.home.counter 8 | }), 9 | dispatch => ({ 10 | change: () => dispatch({ type: HOME_SWITCH }) 11 | }) 12 | ); 13 | 14 | export default hoc(({ text, change }) => ( 15 |
16 |

Home saying: {text}

17 | 18 |
19 | )); 20 | -------------------------------------------------------------------------------- /config/assets.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug'); 2 | const { WEBPACK_ASSET_FILE_PATH } = require('./paths'); 3 | 4 | const fs = require('fs'); 5 | 6 | const log = debug('react-playground:server:assets'); 7 | 8 | module.exports = () => { 9 | const file = fs.readFileSync(WEBPACK_ASSET_FILE_PATH, 'utf8'); 10 | const assets = file ? JSON.parse(file) : null; 11 | if (!file || !assets) { 12 | log('Assets file was not found. Expected ', file); 13 | return null; 14 | } 15 | 16 | log('assets file', assets); 17 | return assets; 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/about/attach.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | import { attachReducer } from '../store/store'; 3 | import withAsyncReducers from '../store/withAsyncReducers'; 4 | import About from './containers/About'; 5 | 6 | const REDUCER_NAME = 'about'; 7 | 8 | if (module.hot) { 9 | module.hot.accept(() => { 10 | const newReducer = require('./reducers/about').default; 11 | attachReducer(REDUCER_NAME, newReducer, true); 12 | }); 13 | } 14 | 15 | const reducer = require('./reducers/about').default; 16 | 17 | export default withAsyncReducers(REDUCER_NAME, reducer)(About); 18 | -------------------------------------------------------------------------------- /.client.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": [ 4 | "react", 5 | ["env", { 6 | "targets": { 7 | "browsers": ["last 2 versions", "safari >= 9"] 8 | }, 9 | "modules": false, 10 | "loose": true, 11 | "useBuiltIns": true 12 | }] 13 | ], 14 | "plugins": [ 15 | "transform-class-properties", 16 | "transform-export-extensions", 17 | ["transform-react-jsx", { 18 | "useBuiltIns": true 19 | }], 20 | ["transform-object-rest-spread", { 21 | "useBuiltIns": true 22 | }], 23 | "react-hot-loader/babel" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/app/core/components/Status.jsx: -------------------------------------------------------------------------------- 1 | import { PropTypes, Component } from 'react'; 2 | 3 | class Status extends Component { 4 | static contextTypes = { 5 | router: PropTypes.shape({ 6 | staticContext: PropTypes.object 7 | }).isRequired 8 | }; 9 | static defaultProps = { 10 | code: '200' 11 | }; 12 | 13 | static propTypes = { 14 | code: PropTypes.string 15 | }; 16 | 17 | componentWillMount() { 18 | const { staticContext } = this.context.router; 19 | if (staticContext) { 20 | staticContext.status = this.props.code; 21 | } 22 | } 23 | 24 | render() { 25 | return null; 26 | } 27 | } 28 | 29 | export default Status; 30 | -------------------------------------------------------------------------------- /src/server/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/store/createReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * create reducer - helper funciton to create reducer as an object based on RSA 3 | * http://redux.js.org/docs/recipes/ReducingBoilerplate.html#generating-reducers 4 | * @param initialState - initial state of reducer 5 | * @param {object} handlers object with keys as action.type and value as reduce function 6 | * @return {function} - create reducer function 7 | **/ 8 | export default function createReducer(initialState, handlers = {}) { 9 | return function reducer(state = initialState, action) { 10 | if (action && {}.hasOwnProperty.call(handlers, action.type)) { 11 | return handlers[action.type](state, action); 12 | } 13 | return state; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/about/containers/About.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { ABOUT_SWITCH } from '../constants'; 4 | 5 | const hoc = connect( 6 | state => ({ 7 | text: state && state.about && state.about.counter 8 | }), 9 | dispatch => ({ 10 | change: () => dispatch({ type: ABOUT_SWITCH }) 11 | }) 12 | ); 13 | 14 | const About = ({ text, change }) => ( 15 |
16 |

About with counter

17 |

Counter is {text}

18 | 19 |
20 | ); 21 | 22 | About.propTypes = { 23 | text: PropTypes.number.isRequired, 24 | change: PropTypes.func.isRequired 25 | }; 26 | 27 | export default hoc(About); 28 | -------------------------------------------------------------------------------- /src/server-entry.js: -------------------------------------------------------------------------------- 1 | require('core-js'); 2 | require('ignore-styles'); 3 | 4 | const getAssets = require('../config/assets'); 5 | const configureENV = require('./app/core/env').configureENV; 6 | const config = require('../config/environment'); 7 | 8 | configureENV(config); 9 | 10 | const createServer = require('./server/server').createServer; 11 | 12 | // Tell any CSS tooling to use all vendor prefixes if the 13 | // user agent is not known. 14 | global.navigator = global.navigator || {}; 15 | global.navigator.userAgent = global.navigator.userAgent || 'all'; 16 | 17 | const server = createServer(config, getAssets); 18 | 19 | server.listen(config.port, () => { 20 | console.log(`listening at http://localhost:${config.port}`); // eslint-disable-line 21 | }); 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # build 40 | build 41 | dist 42 | temp 43 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | const utils = require('./utils'); 4 | 5 | const defaultConfig = require('./webpack.common'); 6 | const { publicAssets } = require('./environment'); 7 | 8 | const hot = `webpack-hot-middleware/client?path=${publicAssets}/__webpack_hmr`; 9 | 10 | const devConfig = Object.assign({}, defaultConfig); 11 | 12 | // enable hot server 13 | const app = utils.isHot() ? [hot, 'react-hot-loader/patch', ...devConfig.entry.app] : [...devConfig.entry.app]; 14 | 15 | devConfig.entry.app = app; 16 | // enable Hot module replacement 17 | if (utils.isHot()) { 18 | devConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); 19 | } 20 | 21 | // extract all styles in one chunk 22 | devConfig.plugins.push(new ExtractTextPlugin({ filename: '[name].css', allChunks: true })); 23 | 24 | module.exports = devConfig; 25 | -------------------------------------------------------------------------------- /src/app/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Redirect from 'react-router/Redirect'; 3 | import Route from 'react-router-dom/Route'; 4 | import Link from 'react-router-dom/Link'; 5 | import Switch from 'react-router-dom/Switch'; 6 | import Status from './core/components/Status'; 7 | import Home from './home'; 8 | import About from './about'; 9 | 10 | const NotFound = () => ( 11 |
12 | 13 | Not Found 14 |
15 | ); 16 | 17 | export default () => ( 18 |
19 | 25 |
26 | 27 | 28 | } /> 29 | 30 | 31 | 32 |
33 | ); 34 | -------------------------------------------------------------------------------- /src/server/middleware/error.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export, react/jsx-filename-extension */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/server'; 4 | import Error from '../templates/Error'; 5 | 6 | /** 7 | * If this code path is reached, one of two things occurred: 8 | * 1.) An error route was not suppied in the defined routes 9 | * so a not found was piped to this middleware 10 | * 2.) a non get request was made and was unsuccessfully routed 11 | * falling into this 500 code block 12 | */ 13 | /* eslint-disable no-unused-vars */ 14 | export const onError = (err, req, res, next) => { 15 | /* eslint-enable no-unused-vars */ 16 | if (process.env.NODE_ENV === 'development') { 17 | console.error(err); // eslint-disable-line no-console 18 | renderError(res, err); 19 | } else { 20 | renderError(res, null, res.sentry); 21 | } 22 | }; 23 | 24 | function renderError(res, err, sentryError) { 25 | const html = ReactDOM.renderToStaticMarkup(); 26 | res.status((err && err.status) || 500); 27 | res.send(`${html}`); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dima Lebedynskyi 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 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [include] 2 | 3 | # Including these files causes issues. 4 | 5 | [ignore] 6 | .*/infrastructure/* 7 | */build/* 8 | # node modules that don't work well with flow. fbjs -lol 9 | .*/node_modules/fbjs/.* 10 | .*/node_modules/stylelint/.* 11 | .*/node_modules/flow-remove-types/.* 12 | .*/node_modules/flow-coverage-report/.* 13 | 14 | [libs] 15 | # Official "flow-typed" repository definitions. 16 | flow-typed/npm 17 | 18 | 19 | # Note: the following definitions come bundled with flow. It can be handy 20 | # to reference them. 21 | # React: https://github.com/facebook/flow/blob/master/lib/react.js 22 | # Javascript: https://github.com/facebook/flow/blob/master/lib/core.js 23 | # Node: https://github.com/facebook/flow/blob/master/lib/node.js 24 | # DOM: https://github.com/facebook/flow/blob/master/lib/dom.js 25 | # BOM: https://github.com/facebook/flow/blob/master/lib/bom.js 26 | # CSSOM: https://github.com/facebook/flow/blob/master/lib/cssom.js 27 | # IndexDB: https://github.com/facebook/flow/blob/master/lib/indexeddb.js 28 | 29 | [options] 30 | esproposal.class_static_fields=enable 31 | esproposal.class_instance_fields=enable 32 | esproposal.export_star_as=enable 33 | 34 | [version] 35 | ^0.42.0 36 | -------------------------------------------------------------------------------- /config/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | require('./environment'); 5 | 6 | const { SRC, COMPILED } = require('./paths'); 7 | 8 | module.exports = { 9 | target: 'node', 10 | externals: [nodeExternals()], 11 | entry: { server: [`${SRC}/server-entry.js`] }, 12 | devtool: 'source-map', 13 | output: { 14 | path: COMPILED, 15 | filename: '[name].js', 16 | chunkFilename: '[name].chunk.js', 17 | publicPath: '/' 18 | }, 19 | performance: false, 20 | plugins: [ 21 | new webpack.NoEmitOnErrorsPlugin(), 22 | new webpack.DefinePlugin({ 23 | 'process.env.PORT': JSON.stringify(process.env.PORT), 24 | 'process.env.DEBUG': JSON.stringify(process.env.DEBUG), 25 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 26 | }) 27 | ], 28 | resolve: { 29 | modules: ['node_modules', SRC], 30 | extensions: ['.js', '.jsx', '.json', '.scss'] 31 | }, 32 | module: { 33 | loaders: [ 34 | { 35 | test: /\.json$/, 36 | loader: 'json-loader' 37 | }, 38 | { 39 | test: /\.jsx?$/, 40 | include: [/src/], 41 | loader: 'babel-loader' 42 | } 43 | ] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/app/store/withAsyncReducers.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { injectReducer } from './store'; 3 | 4 | const storeKey = 'store'; 5 | /** 6 | * withAsyncReducers - HOC component to register async reducer 7 | * @param {string} name reducer name 8 | * @param {function} reducer reducer function 9 | * @param {Boolean} [force=false] should replace be forced 10 | * @return {Component} React Component 11 | */ 12 | const withAsyncReducers = (name, reducer, force = false) => 13 | BaseComponent => 14 | class WithAsyncComponent extends Component { 15 | static contextTypes = { 16 | [storeKey]: PropTypes.object 17 | }; 18 | 19 | constructor(props, context) { 20 | super(props, context); 21 | this.store = this.props[storeKey] || this.context[storeKey]; 22 | } 23 | 24 | componentWillMount() { 25 | this.attachReducers(); 26 | } 27 | 28 | attachReducers() { 29 | if (!reducer || !name) { 30 | return; 31 | } 32 | injectReducer(this.store, `${name}`, reducer, force); 33 | } 34 | 35 | render() { 36 | return React.createElement(BaseComponent, this.props); 37 | } 38 | }; 39 | 40 | export default withAsyncReducers; 41 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const ROOT = path.join(process.cwd()); 4 | const BABEL_CLIENT = path.join(ROOT, '.client.babelrc'); 5 | 6 | const SRC = path.join(ROOT, 'src'); 7 | const COMPILED = path.join(ROOT, 'build'); 8 | const SERVER_PATH = path.join(COMPILED, 'server.js'); 9 | 10 | const DIST = path.join(COMPILED, 'dist'); 11 | const COMPILED_ASSETS_PUBLIC_PATH = 'assets/'; 12 | const DIST_COMPILED_ASSETS_PUBLIC_PATH = path.join(DIST, COMPILED_ASSETS_PUBLIC_PATH); 13 | 14 | const APP = path.join(SRC, 'app'); 15 | const SERVER = path.join(SRC, 'server'); 16 | const PUBLIC = path.join(SERVER, 'public'); 17 | const CONTENT = path.join(SERVER, 'content'); 18 | const DIST_CONTENT_PATH = path.join(COMPILED, 'content'); 19 | const ICONS = path.join(SRC, 'icons'); 20 | const STYLES = path.join(SRC, 'styles'); 21 | 22 | const WEBPACK_ASSET_FILE_NAME = 'webpack-assets.json'; 23 | const WEBPACK_ASSET_FILE_FOLDER = COMPILED; 24 | const WEBPACK_ASSET_FILE_PATH = path.join(WEBPACK_ASSET_FILE_FOLDER, WEBPACK_ASSET_FILE_NAME); 25 | 26 | module.exports = { 27 | ROOT, 28 | BABEL_CLIENT, 29 | SRC, 30 | DIST, 31 | COMPILED_ASSETS_PUBLIC_PATH, 32 | DIST_COMPILED_ASSETS_PUBLIC_PATH, 33 | DIST_CONTENT_PATH, 34 | COMPILED, 35 | SERVER_PATH, 36 | APP, 37 | ICONS, 38 | PUBLIC, 39 | CONTENT, 40 | STYLES, 41 | WEBPACK_ASSET_FILE_NAME, 42 | WEBPACK_ASSET_FILE_FOLDER, 43 | WEBPACK_ASSET_FILE_PATH 44 | }; 45 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const WebpackStrip = require('strip-loader'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | const { 6 | DIST, 7 | PUBLIC, 8 | COMPILED_ASSETS_PUBLIC_PATH, 9 | CONTENT, 10 | DIST_CONTENT_PATH 11 | } = require('./paths'); 12 | const defaultConfig = require('./webpack.common'); 13 | const { publicAssets } = require('./environment'); 14 | 15 | const prodConfig = Object.assign({}, defaultConfig, { 16 | output: { 17 | path: `${DIST}/${COMPILED_ASSETS_PUBLIC_PATH}`, 18 | filename: '[name].[chunkhash].js', 19 | chunkFilename: '[name].[chunkhash].chunk.js', 20 | publicPath: `${publicAssets}/${COMPILED_ASSETS_PUBLIC_PATH}` 21 | } 22 | }); 23 | 24 | prodConfig.module.loaders.push({ 25 | test: /\.jsx?$/, 26 | loader: WebpackStrip.loader('console.log', 'console.debug') 27 | }); 28 | 29 | // extract styles as single file 30 | prodConfig.plugins.push( 31 | new ExtractTextPlugin({ 32 | filename: '[name].[contenthash].css', 33 | allChunks: true 34 | }) 35 | ); 36 | // copy content of PUBLIC folder to dist. 37 | // it is expectec to have only static assets 38 | prodConfig.plugins.push( 39 | new CopyWebpackPlugin([ 40 | { from: PUBLIC, to: DIST, ignore: '**/.*' }, 41 | { from: CONTENT, to: DIST_CONTENT_PATH, ignore: '**/.*' } 42 | ]) 43 | ); 44 | 45 | module.exports = prodConfig; 46 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | const nodemon = require('nodemon'); 2 | const debug = require('debug'); 3 | const clearConsole = require('react-dev-utils/clearConsole'); 4 | const openBrowser = require('react-dev-utils/openBrowser'); 5 | 6 | const webpackClient = require('./client'); 7 | const webpackServer = require('./server'); 8 | 9 | const { SERVER_PATH, COMPILED } = require('../config/paths'); 10 | const env = require('../config/environment'); 11 | 12 | const logger = require('./logger'); 13 | 14 | const log = debug('react-playground:build:nodemon'); 15 | // Define 16 | const monitorServer = () => { 17 | logger.start('Starting to monitor build'); 18 | log('execute path', SERVER_PATH); 19 | log('watching for ', COMPILED); 20 | 21 | return nodemon({ 22 | script: SERVER_PATH, 23 | watch: [COMPILED] 24 | }) 25 | .once('start', () => { 26 | logger.end('Server started'); 27 | if (process.env.NO_BROWSER !== 'true') { 28 | const url = `${env.protocol}://${env.hostname}${env.port ? `:${env.port}` : ''}`; 29 | log(`setting timer to open browser at ${url}`); 30 | setTimeout( 31 | () => { 32 | openBrowser(url); 33 | }, 34 | 1000 35 | ); 36 | } 37 | }) 38 | .on('restart', () => { 39 | logger.info('restarting monitor'); 40 | }) 41 | .on('crash', err => { 42 | logger.error('monitor failed', err); 43 | }) 44 | .on('quit', process.exit); 45 | }; 46 | 47 | const onExit = code => { 48 | process.exit(code); 49 | }; 50 | 51 | process.on('EADDRINUSE', onExit); 52 | process.on('SIGINT', onExit); 53 | process.on('SIGTERM', onExit); 54 | 55 | clearConsole(); 56 | logger.start('Starting build'); 57 | 58 | webpackClient().then(() => webpackServer()).then(() => monitorServer()); 59 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | // Load environment variables from .env file. Surpress warnings using silent 2 | // if this file is missing. dotenv will never modify any environment variables 3 | // that have already been set. 4 | // https://github.com/motdotla/dotenv 5 | require('dotenv').config({ silent: true }); 6 | const debug = require('debug'); 7 | 8 | const { CONTENT, DIST_CONTENT_PATH } = require('./paths'); 9 | 10 | const configValue = {}; 11 | 12 | configValue.environment = getEnvOrDefault('NODE_ENV', 'development'); 13 | 14 | if (configValue.environment === 'development') { 15 | configValue.assetsPort = getEnvOrDefault('DEV_ASSETS_PORT', '5000'); 16 | configValue.publicAssets = `http://localhost:${configValue.assetsPort}`; 17 | } else { 18 | configValue.publicAssets = getEnvOrDefault('CDN_URL', ''); 19 | } 20 | 21 | configValue.port = getEnvOrDefault('PORT', 5001); 22 | configValue.protocol = getEnvOrDefault('PROTOCOL', 'http'); 23 | configValue.hostname = getEnvOrDefault('HOSTNAME', 'localhost'); 24 | 25 | configValue.staticAssetsCache = getEnvOrDefault('STATIC_CACHE_DURATION', 1000 * 60 * 60 * 24 * 30); // 30 days 26 | configValue.dynamicCache = getEnvOrDefault('DYNAMIC_CACHE_DURATION', 0); // 0 ms 27 | 28 | configValue.contentDir = configValue.environment === 'development' ? CONTENT : DIST_CONTENT_PATH; 29 | 30 | debug.enable(process.env.DEBUG); 31 | 32 | module.exports = configValue; 33 | 34 | function getEnvOrDefault(key, defaultValue) { 35 | if (!process.env[key]) { 36 | if (typeof defaultValue === 'undefined' && process.env.NODE_ENV !== 'test') { 37 | console.warn('WARNING: Missing ENV var ', key); //eslint-disable-line 38 | debug(`WARNING: Missing ENV var ${key}`); 39 | } else { 40 | process.env[key] = defaultValue; 41 | } 42 | } 43 | return process.env[key]; 44 | } 45 | -------------------------------------------------------------------------------- /src/client-entry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require, no-underscore-dangle, react/jsx-filename-extension, */ 2 | import 'core-js'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import debug from 'debug'; 6 | 7 | import Router from 'react-router-dom/BrowserRouter'; 8 | import { Provider } from 'react-redux'; 9 | import { withAsyncComponents } from 'react-async-component'; 10 | 11 | import configureStore from './app/store/store'; 12 | import ReactHotLoader from './app/ReactHotLoader'; 13 | import { configureENV } from './app/core/env'; 14 | import { INITIAL_CONSTRUCT } from './app/core/constants'; 15 | import './styles/app.css'; 16 | 17 | const log = debug('react-playground:client-entry'); 18 | log('Client environment %s', process.env); 19 | 20 | const rootEl = document.getElementById('app'); 21 | 22 | const config = window.__CONFIG__ || {}; 23 | log('configuring client env with %j', config); 24 | configureENV(config); 25 | 26 | log('recived initial state', window.__INITIAL_STATE__); 27 | // creating store with registry 28 | const store = configureStore(window.__INITIAL_STATE__ || {}); 29 | // dispatch initial state construction to update dynamic values 30 | store.dispatch({ type: INITIAL_CONSTRUCT }); 31 | 32 | // create render function 33 | const render = RootEl => { 34 | const app = ( 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | 42 | withAsyncComponents(app).then(({ appWithAsyncComponents }) => { 43 | ReactDOM.render(appWithAsyncComponents, rootEl); 44 | }); 45 | }; 46 | 47 | // set up hot reloading on the client 48 | if (process.env.NODE_ENV === 'development' && module.hot) { 49 | module.hot.accept('./app/Root', () => { 50 | const Root = require('./app/Root').default; // eslint-disable-line no-shadow 51 | 52 | render(Root); 53 | }); 54 | } 55 | 56 | // now ready for first render 57 | const Root = require('./app/Root').default; // eslint-disable-line no-shadow 58 | 59 | render(Root); 60 | -------------------------------------------------------------------------------- /src/app/store/store.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* Based on https://gist.github.com/gaearon/0a2213881b5d53973514 */ 3 | 4 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; 5 | import thunkMiddleware from 'redux-thunk'; 6 | 7 | import reducers from '../core/reducers'; 8 | import dummyReducer from './dummyReducer'; 9 | 10 | const debugEnhancer = typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' 11 | ? window.devToolsExtension() 12 | : f => f; 13 | 14 | export default function configureStore(initialState = {}) { 15 | const initialReducers = createAsyncReducers({}, Object.keys(initialState)); 16 | 17 | const enhancer = compose( 18 | applyMiddleware(thunkMiddleware), // middlewares 19 | debugEnhancer 20 | ); 21 | 22 | const store = createStore(initialReducers, initialState, enhancer); 23 | 24 | // registry for async reducers 25 | store.asyncReducers = {}; 26 | 27 | if (module.hot) { 28 | module.hot.accept('../core/reducers', () => { 29 | const nextReducers = require('../core/reducers').default; // eslint-disable-line global-require 30 | 31 | const replace = { ...nextReducers, ...store.asyncReducers }; 32 | store.replaceReducer(replace); 33 | }); 34 | } 35 | 36 | return store; 37 | } 38 | 39 | export function createAsyncReducers(asyncReducers, persist = []) { 40 | const allReducers = { 41 | ...reducers, 42 | ...asyncReducers 43 | }; 44 | 45 | persist.forEach(key => { 46 | if (!{}.hasOwnProperty.call(allReducers, key)) { 47 | allReducers[key] = dummyReducer; 48 | } 49 | }); 50 | 51 | return combineReducers(allReducers); 52 | } 53 | 54 | export function injectReducer(store, name, asyncReducer, force = false) { 55 | if (!force && {}.hasOwnProperty.call(store.asyncReducers, name)) { 56 | const r = store.asyncReducers[name]; 57 | if (r === dummyReducer) { 58 | return; 59 | } 60 | } 61 | 62 | store.asyncReducers[name] = asyncReducer; 63 | store.replaceReducer(createAsyncReducers(store.asyncReducers)); 64 | } 65 | -------------------------------------------------------------------------------- /src/server/router.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import debug from 'debug'; 3 | import compression from 'compression'; 4 | 5 | import renderApp from './middleware/render-app'; 6 | 7 | import { DIST, PUBLIC } from '../../config/paths'; 8 | import { onError } from './middleware/error'; 9 | 10 | const log = debug('react-playground:router'); 11 | 12 | export const routingApp = express(); 13 | 14 | /** 15 | * function to server static assets for dev from server/public (PUBLIC) without cache 16 | * or from build/dist (DIST) on production with cache in place 17 | * @param {object} config 18 | * @return static middleware 19 | */ 20 | function getStaticAssets(config) { 21 | if (config.environment === 'development') { 22 | log('serving external files from', PUBLIC); 23 | return express.static(PUBLIC); 24 | } 25 | 26 | log('serving production assets from ', DIST); 27 | return express.static(DIST, { 28 | maxAge: +config.staticAssetsCache 29 | }); 30 | } 31 | 32 | export function setRoutes(config, buildAssets) { 33 | const assets = buildAssets(); 34 | log('adding react routes'); 35 | log('recived assets', assets); 36 | log('public path maps to', PUBLIC); 37 | routingApp.disable('x-powered-by'); 38 | 39 | // setting logging via Raven 40 | if (config.environment === 'production') { 41 | log('configure production logging'); 42 | // TODO: production loging set up now 43 | } else { 44 | log('development loging'); 45 | } 46 | // setting headers and static assets 47 | routingApp 48 | .use((req, res, next) => { 49 | res.header('X-Powered-By', 'Unicorn Poops'); 50 | next(); 51 | }) 52 | .use(getStaticAssets(config)) 53 | .use(compression()); 54 | 55 | routingApp.get('*', renderApp(assets, config)); 56 | 57 | // setting dynamicCache for html page 58 | if (config.environment === 'production') { 59 | routingApp.use((req, res, next) => { 60 | res.set('Cache-Control', `private, max-age=${config.dynamicCache}`); 61 | next(); 62 | }); 63 | } 64 | // custom error responce to client 65 | routingApp.use(onError); 66 | } 67 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "plugin:flowtype/recommended", 5 | "plugin:ava/recommended" 6 | ], 7 | "plugins": [ 8 | "react", 9 | "jsx-a11y", 10 | "import", 11 | "ava", 12 | "flowtype" 13 | ], 14 | "settings": { 15 | "import/external-module-folders": ["node_modules", "src"], 16 | "flowtype": { 17 | "onlyFilesWithFlowAnnotation": true 18 | } 19 | }, 20 | "parser": "babel-eslint", 21 | "parserOptions": { 22 | "ecmaVersion": 6, 23 | "sourceType": "module", 24 | "ecmaFeatures": { 25 | "jsx": true 26 | } 27 | }, 28 | "env": { 29 | "browser": true, 30 | "node": true, 31 | "es6": true 32 | }, 33 | "globals": { 34 | "__DEV__": true 35 | }, 36 | "prettierOptions": { 37 | "bracketSpacing": true, 38 | "trailingComma": "none", 39 | "jsxBracketSameLine": false, 40 | "singleQuote": true, 41 | "printWidth": 120 42 | }, 43 | "rules": { 44 | "max-len": ["warn", {"code": 120, "ignoreUrls": true}], 45 | "generator-star-spacing": 0, 46 | "comma-dangle": [ "error", "never" ], 47 | "quotes": ["error", "single", {"allowTemplateLiterals": true, "avoidEscape": true }], 48 | "object-curly-spacing": 0, 49 | "no-confusing-arrow": 0, 50 | "no-mixed-operators": 0, 51 | "no-console": 1, 52 | "no-trailing-spaces": 0, 53 | "no-cond-assign": 0, 54 | "dot-notation": 1, 55 | "arrow-parens": ["warn", "as-needed"], 56 | "class-methods-use-this": 0, 57 | "no-use-before-define": ["error", {"functions": false}], 58 | "react/jsx-sort-props": 0, 59 | "react/forbid-prop-types": 0, 60 | "react/jsx-tag-spacing": 1, 61 | "react/no-array-index-key": 1, 62 | "jsx-a11y/no-static-element-interactions": 0, 63 | "react/jsx-closing-bracket-location": [ 2, "after-props" ], 64 | "react/jsx-space-before-closing": 0, 65 | "react/jsx-indent": 0, 66 | "react/no-unused-prop-types": 1, 67 | "import/no-extraneous-dependencies": 0, 68 | "import/prefer-default-export": "warn", 69 | "import/no-named-as-default": 0 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | @source https://github.com/NYTimes/kyt/blob/master/cli/logger.js 3 | */ 4 | 5 | const cl = require('chalkline'); 6 | 7 | const logger = console; 8 | const write = (status, text, verbose) => { 9 | let textToLog = ''; 10 | let logObject = false; 11 | let line = () => {}; 12 | 13 | if (status === 'task') textToLog = '👍 '; 14 | else if (status === 'start') textToLog = '\n🔥 '; 15 | else if (status === 'end') textToLog = '\n✅ '; 16 | else if (status === 'info') textToLog = 'ℹ️ '; 17 | else if (status === 'warn') textToLog = '💩 '; 18 | else if (status === 'error') textToLog = '\n❌ '; 19 | else if (status === 'debug') textToLog = '🐞 '; 20 | 21 | if (status === 'error') line = cl.red; 22 | if (status === 'start') line = cl.white; 23 | if (status === 'warn') line = cl.yellow; 24 | if (status === 'task') line = cl.green; 25 | 26 | textToLog += text; 27 | 28 | // Adds optional verbose output 29 | if (verbose) { 30 | if (typeof verbose === 'object') { 31 | logObject = true; 32 | } else { 33 | textToLog += `\n${verbose}`; 34 | } 35 | } 36 | 37 | line(); 38 | logger.log(textToLog); 39 | if (['start', 'end', 'error'].indexOf(status) > -1) { 40 | logger.log(); 41 | } 42 | if (logObject) logger.dir(verbose, { depth: 15 }); 43 | }; 44 | // Printing any statements 45 | const log = text => logger.log(text); 46 | 47 | // Starting a process 48 | const start = text => write('start', text); 49 | 50 | // Ending a process 51 | const end = text => write('end', text); 52 | 53 | // Tasks within a process 54 | const task = text => write('task', text); 55 | 56 | // Info about a process task 57 | const info = text => write('info', text); 58 | 59 | // Verbose output 60 | // takes optional data 61 | const debug = (text, data) => write('debug', text, data); 62 | 63 | // Warn output 64 | const warn = (text, data) => write('warn', text, data); 65 | 66 | // Error output 67 | // takes an optional error 68 | const error = (text, err) => write('error', text, err); 69 | 70 | module.exports = { 71 | log, 72 | task, 73 | info, 74 | debug, 75 | warn, 76 | error, 77 | start, 78 | end 79 | }; 80 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import express from 'express'; 3 | import debug from 'debug'; 4 | import hpp from 'hpp'; 5 | import helmet from 'helmet'; 6 | import compression from 'compression'; 7 | 8 | import { routingApp, setRoutes } from './router'; 9 | 10 | export const createServer = (config, getAssets) => { 11 | const server = express(); 12 | 13 | const log = debug('react-playground:server'); 14 | log('starting with config: '); 15 | log('%O', config); 16 | 17 | server.set('etag', true); 18 | 19 | // Prevent HTTP Parameter pollution. 20 | // @see http://bit.ly/2f8q7Td 21 | server.use(hpp()); 22 | 23 | if (config.environment !== 'production') { 24 | server.use((req, res, next) => { 25 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); 26 | res.header('Pragma', 'no-cache'); 27 | res.header('Expires', 0); 28 | next(); 29 | }); 30 | } 31 | 32 | // The xssFilter middleware sets the X-XSS-Protection header to prevent 33 | // reflected XSS attacks. 34 | // @see https://helmetjs.github.io/docs/xss-filter/ 35 | server.use(helmet.xssFilter()); 36 | 37 | // Frameguard mitigates clickjacking attacks by setting the X-Frame-Options header. 38 | // @see https://helmetjs.github.io/docs/frameguard/ 39 | server.use(helmet.frameguard('deny')); 40 | 41 | // Sets the X-Download-Options to prevent Internet Explorer from executing 42 | // downloads in your site’s context. 43 | // @see https://helmetjs.github.io/docs/ienoopen/ 44 | server.use(helmet.ieNoOpen()); 45 | 46 | // Don’t Sniff Mimetype middleware, noSniff, helps prevent browsers from trying 47 | // to guess (“sniff”) the MIME type, which can have security implications. It 48 | // does this by setting the X-Content-Type-Options header to nosniff. 49 | // @see https://helmetjs.github.io/docs/dont-sniff-mimetype/ 50 | server.use(helmet.noSniff()); 51 | 52 | server.use(compression()); 53 | server.enable('view cache'); 54 | server.enable('strict routing'); 55 | 56 | setRoutes(config, getAssets); 57 | server.use('/', routingApp); 58 | // Don't expose any software information to potential hackers. 59 | server.disable('X-Powered-By'); 60 | 61 | return server; 62 | }; 63 | -------------------------------------------------------------------------------- /scripts/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export, no-console */ 2 | const webpack = require('webpack'); 3 | const debug = require('debug'); 4 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 5 | const webpackConfig = require('../config/webpack.config.server.js'); 6 | 7 | const logger = require('./logger'); 8 | 9 | const log = { 10 | warn: console.warn || console.log, 11 | error: console.error, 12 | pack: debug('react-playground:build:server') 13 | }; 14 | let hasCompleteFirstCompilation = false; 15 | 16 | module.exports = () => 17 | new Promise((resolve, reject) => { 18 | logger.start('Building server'); 19 | const compiler = webpack(webpackConfig); 20 | let watcher; 21 | const onExit = () => { 22 | if (watcher) { 23 | watcher.close(); 24 | } 25 | }; 26 | 27 | watcher = compiler.watch({}, (err, stats) => { 28 | log.pack('Server compiled'); 29 | log.pack( 30 | stats.toString({ 31 | chunks: false, 32 | colors: true 33 | }) 34 | ); 35 | if (err) { 36 | console.error('got webpack error', err); 37 | reject(err); 38 | return; 39 | } 40 | 41 | const rawMessages = stats.toJson({}, true); 42 | const messages = formatWebpackMessages(rawMessages); 43 | // resolving promise on first compilation complete 44 | if (!messages.errors.length) { 45 | logger.task('Server compiled successfully!'); 46 | hasCompleteFirstCompilation = true; 47 | resolve(watcher); 48 | return; 49 | } 50 | 51 | // some errors happened 52 | if (messages.errors.length) { 53 | logger.error('Failed to compile.'); 54 | // report errors to console 55 | messages.errors.forEach(log.error); 56 | // first compile failed. rejecting promise 57 | if (!hasCompleteFirstCompilation) { 58 | onExit(); 59 | reject(stats); 60 | } 61 | return; 62 | } 63 | 64 | if (messages.warnings.length) { 65 | logger.warn('Compiled with warnings.'); 66 | messages.warnings.forEach(log.warn); 67 | } 68 | }); 69 | 70 | process.on('SIGTERM', onExit); 71 | process.on('SIGINT', onExit); 72 | }); 73 | -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | const AssetsPlugin = require('assets-webpack-plugin'); 4 | const utils = require('./utils'); 5 | const loadBabel = require('./babel'); 6 | 7 | const { publicAssets } = require('./environment'); 8 | 9 | const vendor = require('./vendor'); 10 | 11 | const { 12 | SRC, 13 | DIST, 14 | COMPILED_ASSETS_PUBLIC_PATH, 15 | WEBPACK_ASSET_FILE_NAME, 16 | WEBPACK_ASSET_FILE_FOLDER, 17 | BABEL_CLIENT 18 | } = require('./paths'); 19 | 20 | const babelrc = loadBabel(BABEL_CLIENT); 21 | const babelPlugins = babelrc.plugins; 22 | 23 | if (utils.isProduction()) { 24 | babelPlugins.push('transform-react-remove-prop-types'); 25 | } 26 | 27 | Object.assign(babelrc, { 28 | plugins: babelPlugins 29 | }); 30 | 31 | module.exports = { 32 | entry: { 33 | vendor, 34 | app: [`${SRC}/client-entry.js`] 35 | }, 36 | performance: false, 37 | devtool: 'source-map', 38 | output: { 39 | path: `${DIST}/${COMPILED_ASSETS_PUBLIC_PATH}`, 40 | filename: '[name].js', 41 | chunkFilename: '[name].chunk.js', 42 | publicPath: `${publicAssets}/${COMPILED_ASSETS_PUBLIC_PATH}` 43 | }, 44 | plugins: [ 45 | new webpack.NoEmitOnErrorsPlugin(), 46 | new webpack.DefinePlugin({ 47 | 'process.env.PORT': JSON.stringify(process.env.PORT), 48 | 'process.env.DEBUG': JSON.stringify(process.env.DEBUG), 49 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 50 | }), 51 | new AssetsPlugin({ 52 | filename: WEBPACK_ASSET_FILE_NAME, 53 | path: WEBPACK_ASSET_FILE_FOLDER, 54 | includeManifest: 'manifest', 55 | prettyPrint: true 56 | }) 57 | ], 58 | resolve: { 59 | modules: ['node_modules', SRC], 60 | extensions: ['.js', '.jsx', '.json', '.scss'] 61 | }, 62 | module: { 63 | loaders: [ 64 | { 65 | test: /\.json$/, 66 | loader: 'json-loader' 67 | }, 68 | { 69 | test: /\.jsx?$/, 70 | include: [/src/], 71 | loader: 'babel-loader', 72 | query: babelrc 73 | }, 74 | { 75 | test: /\.css$/, 76 | include: [/src/], 77 | loader: ExtractTextPlugin.extract({ 78 | fallbackLoader: 'style-loader', 79 | loader: ['css-loader', 'postcss-loader'] 80 | }) 81 | }, 82 | { 83 | test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|eot)$/, 84 | loader: 'url-loader', 85 | query: { 86 | name: '[hash].[ext]', 87 | limit: 10000 88 | } 89 | }, 90 | { 91 | test: /\.(eot|svg|ttf|woff(2)?)(\?v=\d+\.\d+\.\d+)?/, 92 | loader: 'url-loader' 93 | }, 94 | { 95 | test: /\.(eot|ttf|wav|mp3)$/, 96 | loader: 'file-loader', 97 | query: { 98 | name: '[hash].[ext]' 99 | } 100 | } 101 | ] 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/server/templates/Error.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-danger */ 2 | import React, { PropTypes } from 'react'; 3 | 4 | const Error = ( 5 | { 6 | error, 7 | sentry, 8 | lang 9 | } 10 | ) => { 11 | const isProduction = process.env.NODE_ENV === 'production'; 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | {isProduction 20 | ? 21 | : } 22 |