├── 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 |
20 | - Home
21 | - About
22 | - Topics
23 | - Legal
24 |
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 |
74 |
75 |
76 | {isProduction
77 | ?
78 |
Error
79 |
Sorry, a critical error occurred on this page.
80 | {sentry ?
Code: {sentry}
: null}
81 |
82 | :
83 |
{error.name}
84 |
{error.message}
85 |
{error.stack}
86 |
}
87 |
88 |
89 | );
90 | };
91 |
92 | Error.defaultProps = {
93 | sentry: null,
94 | lang: 'en'
95 | };
96 |
97 | Error.propTypes = {
98 | error: PropTypes.shape({
99 | message: PropTypes.string,
100 | name: PropTypes.string,
101 | stack: PropTypes.string
102 | }).isRequired,
103 | sentry: PropTypes.string,
104 | lang: PropTypes.string
105 | };
106 |
107 | export default Error;
108 |
--------------------------------------------------------------------------------
/scripts/client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export, no-console */
2 | const express = require('express');
3 | const debug = require('debug');
4 |
5 | const devMiddleware = require('webpack-dev-middleware');
6 | const hotMiddleware = require('webpack-hot-middleware');
7 | const webpack = require('webpack');
8 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
9 |
10 | const { PUBLIC } = require('../config/paths');
11 | const webpackConfig = require('../config/webpack.config.dev');
12 | const env = require('../config/environment');
13 |
14 | const logger = require('./logger');
15 | const buildUtils = require('../config/utils');
16 |
17 | const log = {
18 | general: console.log,
19 | pack: debug('react-playground:build:compiler'),
20 | hot: debug('react-playground:build:hot-reload')
21 | };
22 |
23 | let hasCompleteFirstCompilation = false;
24 |
25 | module.exports = () =>
26 | new Promise((resolve, reject) => {
27 | logger.start('Creating assets server');
28 | const app = express();
29 | const compiler = webpack(webpackConfig);
30 |
31 | app.use((req, res, next) => {
32 | res.setHeader('Access-Control-Allow-Origin', '*');
33 | next();
34 | });
35 |
36 | app.use('/', express.static(PUBLIC));
37 |
38 | app.use(
39 | devMiddleware(compiler, {
40 | noInfo: true,
41 | quiet: true,
42 | headers: { 'Access-Control-Allow-Origin': '*' },
43 | publicPath: webpackConfig.output.publicPath,
44 | stats: {
45 | colors: true,
46 | reasons: false
47 | }
48 | })
49 | );
50 |
51 | if (buildUtils.isHot()) {
52 | app.use(
53 | hotMiddleware(compiler, {
54 | log: log.hot,
55 | path: '/__webpack_hmr',
56 | heartbeat: 10 * 1000
57 | })
58 | );
59 | }
60 |
61 | let httpServer;
62 |
63 | const onExit = () => {
64 | console.log('exiting process');
65 | if (httpServer) {
66 | httpServer.close();
67 | }
68 | process.exit(0);
69 | };
70 |
71 | compiler.plugin('done', stats => {
72 | log.pack('Assets compiled');
73 | log.pack(
74 | stats.toString({
75 | chunks: false,
76 | colors: true
77 | })
78 | );
79 |
80 | const rawMessages = stats.toJson({}, true);
81 | const messages = formatWebpackMessages(rawMessages);
82 | if (!messages.errors.length && !messages.warnings.length) {
83 | logger.task('Assets compiled successfully!');
84 | if (!hasCompleteFirstCompilation) {
85 | logger.end('Assets server compiled');
86 | }
87 |
88 | hasCompleteFirstCompilation = true;
89 | resolve(httpServer);
90 | return;
91 | }
92 |
93 | if (messages.errors.length) {
94 | logger.error('Failed to compile.');
95 | messages.errors.forEach(log.general);
96 |
97 | if (!hasCompleteFirstCompilation) {
98 | onExit();
99 | reject(stats);
100 | }
101 |
102 | return;
103 | }
104 |
105 | if (messages.warnings.length) {
106 | logger.warn('Compiled with warnings.');
107 | messages.warnings.forEach(log.general);
108 | }
109 | });
110 |
111 | httpServer = app.listen(env.assetsPort);
112 | process.on('SIGTERM', onExit);
113 | process.on('SIGINT', onExit);
114 |
115 | logger.info(`Assets Server spawned at :${env.assetsPort}. Please wait for assets rebuild`);
116 | });
117 |
--------------------------------------------------------------------------------
/src/server/middleware/render-app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-filename-extension */
2 | import { Provider as Redux } from 'react-redux';
3 |
4 | import StaticRouter from 'react-router/StaticRouter';
5 | import { withAsyncComponents } from 'react-async-component';
6 | import ServerTiming from 'servertiming';
7 |
8 | import debug from 'debug';
9 | import React from 'react';
10 | import { renderToString, renderToStaticMarkup } from 'react-dom/server';
11 | import Helmet from 'react-helmet';
12 |
13 | import buildClientConfig from './buildClientConfig';
14 | import Root from '../../app/Root';
15 |
16 | import Html from '../templates/Html';
17 |
18 | import configureStore from '../../app/store/store';
19 | import { INITIAL_CONSTRUCT } from '../../app/core/constants';
20 |
21 | const log = debug('react-playground:server:render');
22 | const perfomanceLog = debug('react-playground:server:perfomance');
23 |
24 | const App = (store, req, routerContext) => (
25 |
26 |
27 |
28 |
29 |
30 | );
31 |
32 | const renderPage = (body, head, initialState, config, assets, { state, STATE_IDENTIFIER } = {}) => {
33 | perfomanceLog('rendering page result for ');
34 | const clientConfig = buildClientConfig(config);
35 |
36 | const html = renderToStaticMarkup(
37 |
42 |
43 |
50 | {asyncComponents && asyncComponents.state
51 | ?
57 | : null}
58 | {assets && assets.vendor && assets.vendor.js ? : null}
59 | {assets && assets.app && assets.app.js ? : null}
60 | {trackingId
61 | ?
66 | : null}
67 | {trackingId ? : null}
68 |
69 |
44 | );
45 |
46 | return `${html}`;
47 | };
48 |
49 | export default function renderAppWrapper(assets, config) {
50 | return async (req, res, next) => {
51 | const timing = new ServerTiming();
52 | timing.startTimer('Render');
53 |
54 | perfomanceLog(`request started for ${req.protocol}://${req.get('host')}${req.originalUrl}`);
55 | // creating store
56 | const store = configureStore({});
57 | // dispatch initial state construction to update dynamic values
58 | store.dispatch({ type: INITIAL_CONSTRUCT });
59 | // create router context
60 | const routerContext = {};
61 | // construct app component with async loaded chunks
62 | const asyncSplit = await withAsyncComponents(App(store, req, routerContext));
63 | // getting async component after code split loaded
64 | const { appWithAsyncComponents } = asyncSplit;
65 | // actual component to string
66 | const body = renderToString(appWithAsyncComponents);
67 | // getting head
68 | const head = Helmet.rewind();
69 | // and inital state
70 | const initialState = store.getState();
71 |
72 | if (routerContext.url) {
73 | timing.stopTimer('Render');
74 | res.setHeader('Server-Timing', timing.generateHeader());
75 | // we got URL - this is a signal that redirect happened
76 | res.status(301).setHeader('Location', routerContext.url);
77 |
78 | perfomanceLog(`request ended for ${req.protocol}://${req.get('host')}${req.originalUrl}`);
79 | res.end();
80 | next();
81 | return;
82 | }
83 | // checking is page is 404
84 | let status = 200;
85 | if (routerContext.status === '404') {
86 | log('sending 404 for ', req.url);
87 | status = 404;
88 | } else {
89 | log('router resolved to actual page');
90 | }
91 |
92 | // rendering result page
93 | const page = renderPage(body, head, initialState, config, assets, asyncSplit);
94 | timing.stopTimer('Render');
95 | res.setHeader('Server-Timing', timing.generateHeader());
96 | res.status(status).send(page);
97 |
98 | perfomanceLog(`request ended for ${req.protocol}://${req.get('host')}${req.originalUrl}`);
99 | next();
100 | };
101 | }
102 |
--------------------------------------------------------------------------------
/src/server/templates/Html.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-danger */
2 | /* eslint-disable react/no-unused-prop-types */
3 | /* test */
4 | import React, { PropTypes } from 'react';
5 | import serialize from 'serialize-javascript';
6 |
7 | const Html = (
8 | {
9 | head,
10 | style,
11 | assets,
12 | body,
13 | config,
14 | initialState,
15 | asyncComponents
16 | }
17 | ) => {
18 | const attrs = head.htmlAttributes.toComponent();
19 | const { lang, ...rest } = attrs || {};
20 | const trackingId = config && config.analytics && config.analytics.google && config.analytics.google.trackingId;
21 | return (
22 |
23 |
24 | {head.title.toComponent()}
25 |
26 |
27 |
28 |
29 |
30 | {head.meta.toComponent()}
31 |
32 | {assets && assets.vendor && assets.vendor.css
33 | ?
34 | : null}
35 | {assets && assets.app && assets.app.css
36 | ?
37 | : null}
38 | {head.link.toComponent()}
39 | {style ? : null}
40 |
41 |