├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── build.js ├── build_es6.js ├── local-dev.sh ├── merge-babel-config.js ├── merge-configs.js ├── server.js ├── server_es6.js ├── transpile.js ├── user-config.js ├── webpack-dev-server.js └── webpack-dev-server_es6.js ├── config ├── universal-redux.config.js ├── webpack-isomorphic-tools.config.js └── webpack.config.js ├── karma.conf.js ├── package.json ├── src ├── client.js ├── client │ ├── devtools.js │ ├── providers │ │ ├── async-props.js │ │ ├── react-router-scroll.js │ │ ├── react-router.js │ │ └── redux-async-connect.js │ └── root.js ├── configure.js ├── helpers │ ├── empty.js │ ├── fetchAllData.js │ └── inspect.js ├── index.js ├── server.js ├── server │ ├── body.js │ ├── head.js │ ├── html.js │ ├── providers │ │ ├── async-props.js │ │ ├── react-router.js │ │ └── redux-async-connect.js │ ├── renderer.js │ ├── root.js │ └── tools.js ├── shared │ └── create.js ├── start.js └── vendor │ └── async-props.js └── tests.webpack.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | [ "transform-runtime" ], 9 | [ "transform-decorators-legacy" ] 10 | ], 11 | "env": { 12 | "development": { 13 | "plugins": [] 14 | }, 15 | "production": { 16 | "plugins": [] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | karma.conf.js 2 | tests.webpack.js 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "rules": { 9 | "react/forbid-prop-types": 0, 10 | "react/jsx-filename-extension": 0, 11 | "react/no-danger": 0, 12 | "react/no-multi-comp": 2, 13 | "react/prefer-stateless-function": 0, 14 | "react/require-extension": 0, 15 | "import/default": 0, 16 | "import/imports-first": 0, 17 | "import/named": 2, 18 | "import/namespace": 2, 19 | "import/newline-after-import": 0, 20 | "import/no-duplicates": 0, 21 | "import/no-extraneous-dependencies": 0, 22 | "import/no-unresolved": 0, 23 | "import/no-named-as-default": 2, 24 | "arrow-body-style": 0, 25 | "comma-dangle": 0, 26 | "global-require": 0, 27 | "indent": [2, 2, {"SwitchCase": 1}], 28 | "no-console": 0, 29 | "no-alert": 0, 30 | "no-nested-ternary": 0, 31 | "no-path-concat": 0, 32 | "no-underscore-dangle": 0, 33 | "max-len": 0, 34 | "object-curly-spacing": [2, "always"], 35 | "prefer-template": 0, 36 | "array-bracket-spacing": [2, "always"] 37 | }, 38 | "plugins": [ 39 | "react", 40 | "import" 41 | ], 42 | "parser": "babel-eslint", 43 | "settings": { 44 | "import/resolve": { 45 | moduleDirectory: ["node_modules", "src"] 46 | } 47 | }, 48 | "globals": { 49 | "__CLIENT__": true, 50 | "__DEVELOPMENT__": true, 51 | "__DEVTOOLS__": true, 52 | "__DEVTOOLS_IS_VISIBLE__": true, 53 | "__DISABLE_SSR__": true, 54 | "__LOGGER__": false, 55 | "__PROVIDERS__": [], 56 | "__SERVER__": true, 57 | "webpackIsomorphicTools": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.iml 5 | webpack-assets.json 6 | webpack-stats.json 7 | npm-debug.log 8 | .DS_Store 9 | lib/ 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "stable" 5 | 6 | sudo: false 7 | 8 | before_install: 9 | # https://github.com/npm/npm/issues/11283 10 | - npm set progress=false 11 | 12 | before_script: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | 16 | script: 17 | - npm run lint 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. 8 | 9 | * Non-trivial changes should be discussed in an issue first 10 | * Develop in a topic branch, not master 11 | * Squash your commits 12 | 13 | ### Linting 14 | 15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails. 16 | 17 | ### Commit Message Format 18 | 19 | Each commit message should include a **type**, a **scope** and a **subject**: 20 | 21 | ``` 22 | (): 23 | ``` 24 | 25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: 26 | 27 | ``` 28 | #459 refactor(utils): create url mapper utility function 29 | #463 chore(webpack): update to isomorphic tools v2 30 | #494 fix(babel): correct dependencies and polyfills 31 | #510 feat(app): add react-bootstrap responsive navbar 32 | ``` 33 | 34 | #### Type 35 | 36 | Must be one of the following: 37 | 38 | * **feat**: A new feature 39 | * **fix**: A bug fix 40 | * **docs**: Documentation only changes 41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 42 | semi-colons, etc) 43 | * **refactor**: A code change that neither fixes a bug or adds a feature 44 | * **test**: Adding missing tests 45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 46 | generation 47 | 48 | #### Scope 49 | 50 | The scope could be anything specifying place of the commit change. For example `webpack`, 51 | `helpers`, `api` etc... 52 | 53 | #### Subject 54 | 55 | The subject contains succinct description of the change: 56 | 57 | * use the imperative, present tense: "change" not "changed" nor "changes" 58 | * don't capitalize first letter 59 | * no dot (.) at the end 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Erik Rasmussen 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Redux 2 | 3 | [![npm version](https://badge.fury.io/js/universal-redux.svg)](https://badge.fury.io/js/universal-redux) 4 | [![build status](https://img.shields.io/travis/bdefore/universal-redux/master.svg?style=flat-square)](https://travis-ci.org/bdefore/universal-redux) 5 | [![Dependency Status](https://david-dm.org/bdefore/universal-redux.svg?style=flat-square)](https://david-dm.org/bdefore/universal-redux) 6 | [![devDependency Status](https://david-dm.org/bdefore/universal-redux/dev-status.svg?style=flat-square)](https://david-dm.org/bdefore/universal-redux#info=devDependencies) 7 | [![Demo on Heroku](https://img.shields.io/badge/demo-heroku-brightgreen.svg?style=flat-square)](https://universal-redux.herokuapp.com) 8 | [![Discord](https://img.shields.io/badge/Discord-join%20chat%20%E2%86%92-738bd7.svg?style=flat-square)](https://discord.gg/0ZcbPKXt5bXmzEb4) 9 | 10 | ### Deprecation Notice 11 | 12 | This tool had a nice good run of things, but it's no longer recommended for greenfield projects. Consider [Create React App](https://github.com/facebook/create-react-app) to be a currently maintained spiritual successor. 13 | 14 | ### What and Why 15 | 16 | Universal Redux is an npm package that when used as a dependency in your project provides a universal (isomorphic) rendering server. You can either use its defaults and begin coding your project, or configure it to your liking with custom Webpack options and Express or Redux middleware. It's intended as both an easy starting point for developers new to React and Redux, as well as an extensible base by which advanced developers can augment with their own middleware and keep up to date with the fast-moving React ecosystem. 17 | 18 | ### Getting Started 19 | 20 | The quickest way to get started is to clone the [starter project](https://github.com/bdefore/universal-redux-starter). This gives you a base project that is set up with default configurations of Webpack and Express. 21 | 22 | #### Other Examples 23 | 24 | - [An example with JWT authentication](https://github.com/bdefore/universal-redux-jwt) ([Heroku demo](https://universal-redux-jwt-example.herokuapp.com)) 25 | - [A refactor of react-redux-universal-hot-example with universal-redux and react-router-redux](https://github.com/bdefore/react-redux-universal-hot-example/tree/babel6) ([Heroku demo](https://universal-redux.herokuapp.com)) 26 | - [An example using Koa instead of Express](https://github.com/bartolkaruza/universal-redux-koa) 27 | 28 | ### Usage 29 | 30 | Your project must define a set of routes as specified by a [React Router](https://github.com/rackt/react-router) configuration, but other than that, your folder structure and development path is up to you. Depending on your other dependencies, you may want to use a version of Universal Redux that is not the latest, using the [section below](https://github.com/bdefore/universal-redux#what-version-to-use) to decide. 31 | 32 | #### Requirements 33 | 34 | Node.JS >= 4.1.1 35 | npm >= 3.3.12 (install via `npm install -g npm@3` if you are on Node 4) 36 | 37 | #### Install 38 | 39 | ``` 40 | npm install --save universal-redux 41 | ``` 42 | 43 | ### Customization 44 | 45 | The configuration file in your project at `config/universal-redux.config.js` defines what properties you want to customize. You can start by copying the [annotated example](https://github.com/bdefore/universal-redux/blob/master/config/universal-redux.config.js). The configuration file is optional and is only necessary if you wish to modify default behavior. 46 | 47 | #### Routes 48 | 49 | Generally kept in `src/routes.js`, this is where you define what routes map to what views. See [`routes.js`](https://github.com/bdefore/react-redux-universal-hot-example/blob/example-project/src/routes.js) in the example project. 50 | 51 | #### Webpack configuration 52 | 53 | Any items specified in the `webpack.config` of your configuration will be merged with the [default Webpack configuration](https://github.com/bdefore/universal-redux/blob/master/config/webpack.config.js). You may also turn on `verbose` mode to see the exact Webpack configuration you are running. 54 | 55 | #### Express middleware 56 | 57 | You can add Express middleware by creating your own server.js like so: 58 | 59 | ```javascript 60 | import { express, renderer, start } from 'universal-redux'; 61 | import config from '../config/universal-redux.config.js'; 62 | 63 | const app = express(config); 64 | 65 | // app.use(someMiddleware); 66 | // app.use(someOtherMiddleware); 67 | 68 | app.use(renderer(config)); 69 | start(app, config); 70 | ``` 71 | 72 | You will need to run this server.js instead of calling the default universal-redux-server. 73 | 74 | Alternatively, you may create your own Express instance, add middleware beforehand and pass that instance as parameter when calling `universal.app(app)`. 75 | 76 | #### Redux middleware 77 | 78 | You can activate your own Redux middleware by specifying the `middleware` property in the configuration file. This must be a path to a file which exports each middleware as a function. All properties specified in `globals` will be available to the middleware. 79 | 80 | #### Adding your own items to HTML head 81 | 82 | The `html.head` configuration allows you to define your own `` that will be merged with the necessary items for serverside rendering. You can see an example of this in the JWT project [here](https://github.com/bdefore/universal-redux-jwt/blob/master/src/containers/Head/Head.js). 83 | 84 | Alternatively, you can specify `html.root` in your configuration and this will be used instead of the default one. If you do take that approach, you'll want to be sure to include the items from `src/server/head.js` and `src/server/body.js`. 85 | 86 | #### Webpack Isomorphic Tools configuration 87 | 88 | You can add or override the default [webpack-isomorphic-tools](https://github.com/halt-hammerzeit/webpack-isomorphic-tools) configuration, by providing a `toolsConfigPath` value to your `config.js`. 89 | 90 | #### Scripts 91 | 92 | The following npm bin aliases have been defined: 93 | 94 | ``` 95 | universal-redux-watch 96 | universal-redux-server 97 | universal-redux-build 98 | ``` 99 | 100 | You'll generally call these from the corresponding section of your project's scripts. See [`package.json`](https://github.com/bdefore/react-redux-universal-hot-example/blob/example-project/package.json) in the example project. 101 | 102 | ### What version to use 103 | 104 | Peer dependencies for each version: 105 | 106 | #### 0.x 107 | 108 | [Babel](https://github.com/babel/babel) 5, [Redux Router](https://github.com/acdlite/redux-router) 109 | 110 | ``` 111 | "react": "^0.14.3", 112 | "react-dom": "^0.14.3", 113 | "react-router": "^1.0.0", 114 | "redux-router": "^1.0.0-beta4" 115 | ``` 116 | 117 | #### 1.x 118 | 119 | [Babel](https://github.com/babel/babel) 5, [Redux Simple Router](https://github.com/rackt/react-router-redux) 120 | 121 | ``` 122 | "react": "^0.14.3", 123 | "react-dom": "^0.14.3", 124 | "react-router": "^1.0.0", 125 | "redux-simple-router": "^1.0.1" 126 | ``` 127 | 128 | #### 2.x 129 | 130 | [Babel](https://github.com/babel/babel) 6, [Redux Simple Router](https://github.com/rackt/react-router-redux) 131 | 132 | ``` 133 | "react": "^0.14.3", 134 | "react-dom": "^0.14.3", 135 | "react-router": "^1.0.0", 136 | "redux-simple-router": "^1.0.1" 137 | ``` 138 | 139 | #### 3.x 140 | 141 | [Babel](https://github.com/babel/babel) 6, [React Router](https://github.com/rackt/react-router) 2, [React Router Redux](https://github.com/rackt/react-router-redux) 3 (Redux Simple Router renamed) is available but optional. 142 | 143 | ``` 144 | "react": "^0.14.3", 145 | "react-dom": "^0.14.3", 146 | "react-router": "^2.0.0-rc4", 147 | ``` 148 | 149 | #### 4.x 150 | 151 | [Babel](https://github.com/babel/babel) 6, [React Router](https://github.com/rackt/react-router) 2, [React Router Redux](https://github.com/rackt/react-router-redux) 3 (Redux Simple Router renamed) is available but optional. 152 | 153 | ``` 154 | "react": "^15.0.0", 155 | "react-dom": "^15.0.0", 156 | "react-router": "^2.0.0", 157 | ``` 158 | 159 | ### Local development 160 | 161 | If you'd like to develop on Universal Redux, clone the repo and while testing with a project that uses it, you can run `PROJECT_PATH=/path/to/project npm run dev` from the Universal Redux root, which will watch for changes and copy them over to your project's `node_modules/universal-redux/lib` directory. If any of your changes add dependencies, you will need to copy those over manually. 162 | 163 | ### Inspirations 164 | 165 | This project forked off of [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example). Please refer to the README there for more details and join the discussion at the [pull request](https://github.com/erikras/react-redux-universal-hot-example/pull/759). 166 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./transpile'); // babel registration (runtime transpilation for node) 3 | require('./build_es6.js'); 4 | -------------------------------------------------------------------------------- /bin/build_es6.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.env.NODE_ENV = 'production'; 4 | 5 | const fs = require('fs'); 6 | const webpack = require('webpack'); 7 | const userConfig = require('./user-config'); 8 | const config = require('./merge-configs')(userConfig); 9 | const buildStats = false; 10 | const outputStatsPath = './webpack-stats.json'; 11 | const webpackConfig = config.webpack.config; 12 | 13 | console.log('\nBuilding webpack bundle...'); 14 | webpack(webpackConfig, (err, stats) => { 15 | if (err) { 16 | console.log('Webpack build had fatal error:', err); 17 | return; 18 | } 19 | 20 | const options = { 21 | hash: true, 22 | version: true, 23 | timings: true, 24 | assets: true, 25 | chunks: false, 26 | colors: true 27 | }; 28 | 29 | console.log('Webpack compile was successful.'); 30 | 31 | const jsonStats = stats.toJson(); 32 | if (jsonStats.errors.length > 0) { 33 | console.log('Webpack had errors.'); 34 | options.errors = true; 35 | } 36 | if (jsonStats.warnings.length > 0) { 37 | console.log('Webpack had warnings.'); 38 | options.warnings = true; 39 | } 40 | 41 | console.log(stats.toString(options)); 42 | 43 | if (buildStats) { 44 | fs.writeFile(outputStatsPath, JSON.stringify(stats.toJson()), (writeError) => { 45 | if (writeError) { 46 | console.log(writeError); 47 | return; 48 | } 49 | 50 | console.log('Webpack output stats were saved to', outputStatsPath); 51 | }); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /bin/local-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # find script base dir even with symlinks (such as when in node_modules/.bin) http://stackoverflow.com/a/246128/583755 4 | SOURCE="${BASH_SOURCE[0]}" 5 | while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink 6 | ROOT_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 7 | SOURCE="$(readlink "$SOURCE")" 8 | [[ $SOURCE != /* ]] && SOURCE="$ROOT_DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located 9 | done 10 | ROOT_DIR="$( cd -P "$( dirname "$SOURCE" )" && cd .. && pwd )" 11 | 12 | if [ -z ${PROJECT_PATH+x} ] ; then 13 | PROJECT_PATH="$( cd -P "$( dirname "$ROOT_DIR" )" && cd .. && pwd )" 14 | 15 | echo Did not receive a PROJECT_PATH, defaulting to $PROJECT_PATH 16 | else 17 | echo "Project path is set to '$PROJECT_PATH'" 18 | fi 19 | 20 | # lifted from http://stackoverflow.com/a/13864829/583755 21 | compile() { 22 | echo 23 | echo Files have changed, updating universal-redux... 24 | echo 25 | echo Source: $ROOT_DIR 26 | echo Destination: $PROJECT_PATH/node_modules/universal-redux 27 | echo 28 | cp -a $ROOT_DIR/bin/* $PROJECT_PATH/node_modules/universal-redux/bin/ > /dev/null 29 | cp -a $ROOT_DIR/config/* $PROJECT_PATH/node_modules/universal-redux/config/ > /dev/null 30 | cp $ROOT_DIR/.babelrc $PROJECT_PATH/node_modules/universal-redux 31 | cp $ROOT_DIR/.eslintrc $PROJECT_PATH/node_modules/universal-redux 32 | babel $ROOT_DIR/src/ --presets es2015,stage-0,react --plugins transform-runtime --out-dir $PROJECT_PATH/node_modules/universal-redux/lib > /dev/null 33 | echo Update complete, continuing to watch... 34 | } 35 | 36 | # lifted from http://stackoverflow.com/a/9461685/583755 37 | chsum1="" 38 | 39 | while [[ true ]] 40 | do 41 | chsum2=`find $ROOT_DIR/src $ROOT_DIR/bin $ROOT_DIR/config -type f -exec md5 {} \;` 42 | if [[ $chsum1 != $chsum2 ]] ; then 43 | compile 44 | chsum1=$chsum2 45 | fi 46 | sleep 2 47 | done 48 | -------------------------------------------------------------------------------- /bin/merge-babel-config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const util = require('util'); 4 | 5 | function loadAndParse(filePath) { 6 | const file = fs.readFileSync(filePath); 7 | return JSON.parse(file); 8 | } 9 | 10 | module.exports = (userBabelConfig, verbose) => { 11 | const baseBabelConfig = loadAndParse(path.resolve(__dirname, '..', './.babelrc')); 12 | const babelConfig = userBabelConfig ? Object.assign(baseBabelConfig, loadAndParse(path.resolve(userBabelConfig))) : baseBabelConfig; 13 | 14 | const hmrConfig = [ 15 | 'react-transform', { 16 | transforms: [ 17 | { 18 | transform: 'react-transform-hmr', 19 | imports: [ 'react' ], 20 | locals: [ 'module' ] 21 | }, 22 | { 23 | transform: 'react-transform-catch-errors', 24 | imports: [ 'react', 'redbox-react' ] 25 | } 26 | ] 27 | } 28 | ]; 29 | 30 | babelConfig.env.development.plugins.unshift(hmrConfig); 31 | babelConfig.cacheDirectory = true; 32 | 33 | const babelLoader = 'babel-loader?' + JSON.stringify(babelConfig); 34 | const jsLoaders = [ babelLoader ]; 35 | 36 | // output configuration files if user wants verbosity 37 | if (verbose) { 38 | const utilOptions = { 39 | depth: 10, 40 | colors: true 41 | }; 42 | 43 | console.log('\nBabel config:'); 44 | console.log(util.inspect(babelConfig, utilOptions)); 45 | } 46 | 47 | return jsLoaders; 48 | }; 49 | -------------------------------------------------------------------------------- /bin/merge-configs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); 3 | const util = require('util'); 4 | const lodash = require('lodash'); 5 | const webpack = require('webpack'); 6 | const mergeWebpack = require('webpack-config-merger'); 7 | const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 8 | const WebpackErrorNotificationPlugin = require('webpack-error-notification'); 9 | 10 | const mergeBabel = require('./merge-babel-config'); 11 | const baseConfig = require('../config/universal-redux.config.js'); 12 | const webpackConfigs = require('../config/webpack.config.js'); 13 | const baseToolsConfig = require('../config/webpack-isomorphic-tools.config.js'); 14 | 15 | const isProduction = process.env.NODE_ENV === 'production'; 16 | 17 | function inspect(obj) { 18 | const utilOptions = { 19 | depth: 12, 20 | colors: true 21 | }; 22 | 23 | console.log(util.inspect(obj, utilOptions)); 24 | } 25 | 26 | module.exports = (userConfig) => { 27 | const projectRoot = process.cwd(); 28 | const sourceRoot = `${projectRoot}/src`; 29 | 30 | // merge with base config 31 | const universalReduxConfig = lodash.merge(baseConfig, userConfig); 32 | 33 | // merge with base webpack config 34 | const webpackSubset = isProduction ? webpackConfigs.production : webpackConfigs.development; 35 | const baseWebpackConfig = mergeWebpack(webpackConfigs.common, webpackSubset); 36 | const combinedWebpackConfig = mergeWebpack(baseWebpackConfig, universalReduxConfig.webpack.config); 37 | combinedWebpackConfig.context = projectRoot; 38 | combinedWebpackConfig.resolve.root = sourceRoot; 39 | 40 | // derive webpack output destination from staticPath 41 | combinedWebpackConfig.output.path = universalReduxConfig.server.staticPath + '/dist'; 42 | 43 | // add babel for js transpiling 44 | const babelConfig = mergeBabel(universalReduxConfig.babelConfig, universalReduxConfig.verbose); 45 | combinedWebpackConfig.module.loaders.unshift({ test: /\.jsx?$/, exclude: /node_modules/, loaders: babelConfig }); 46 | 47 | // gather tools config 48 | const userToolsConfig = require(path.resolve(universalReduxConfig.toolsConfigPath)); 49 | const combinedToolsConfig = lodash.merge(baseToolsConfig, userToolsConfig); 50 | 51 | // bury it here rather than pollute the project directory 52 | combinedToolsConfig.webpack_assets_file_path = 'node_modules/universal-redux/webpack-assets.json'; 53 | 54 | // add tools settings to combined weback config 55 | const toolsPlugin = new WebpackIsomorphicToolsPlugin(combinedToolsConfig); 56 | 57 | combinedWebpackConfig.module.loaders.push({ test: toolsPlugin.regular_expression('images'), loader: 'url-loader?limit=10240' }); 58 | combinedWebpackConfig.plugins.push(isProduction ? toolsPlugin : toolsPlugin.development()); 59 | 60 | // turn on linting per webpack build, unless directed not to 61 | if (universalReduxConfig.lint.enabled !== false && !isProduction) { 62 | combinedWebpackConfig.module.loaders[0].loaders.push('eslint-loader'); 63 | const lintConfigPath = universalReduxConfig.lint.config || path.resolve(__dirname, '../.eslintrc'); 64 | combinedWebpackConfig.eslint = { 65 | configFile: lintConfigPath 66 | }; 67 | } 68 | 69 | // turn on desktop notifications if user elects to 70 | if (universalReduxConfig.notifications === true && !isProduction) { 71 | combinedWebpackConfig.plugins.push(new WebpackErrorNotificationPlugin()); 72 | } 73 | 74 | // add routes, reducer and rootClientComponent aliases so that client has access to them 75 | combinedWebpackConfig.resolve.alias = combinedWebpackConfig.resolve.alias || {}; 76 | combinedWebpackConfig.resolve.alias['universal-redux/routes'] = universalReduxConfig.routes; 77 | combinedWebpackConfig.resolve.alias['universal-redux/middleware'] = universalReduxConfig.redux.middleware || path.resolve(__dirname, '../lib/helpers/empty.js'); 78 | const rootComponentPath = universalReduxConfig.rootClientComponent || universalReduxConfig.rootComponent || path.resolve(__dirname, '../lib/client/root.js'); 79 | combinedWebpackConfig.resolve.alias['universal-redux/rootClientComponent'] = rootComponentPath; 80 | 81 | // add project level vendor libs 82 | if (universalReduxConfig.webpack.vendorLibraries && isProduction) { 83 | lodash.each(universalReduxConfig.webpack.vendorLibraries, (lib) => { 84 | combinedWebpackConfig.entry.vendor.push(lib); 85 | }); 86 | } 87 | 88 | // add default settings that are used by server via process.env 89 | const definitions = { 90 | __DEVTOOLS__: !isProduction, 91 | __DEVELOPMENT__: !isProduction, 92 | __LOGGER__: false, 93 | __PROVIDERS__: JSON.stringify(universalReduxConfig.providers) 94 | }; 95 | 96 | // override with user settings 97 | lodash.each(universalReduxConfig.globals, (value, key) => { definitions[key] = JSON.stringify(value); }); 98 | combinedWebpackConfig.plugins.push(new webpack.DefinePlugin(definitions)); 99 | 100 | // output configuration files if user wants verbosity 101 | if (universalReduxConfig.verbose) { 102 | console.log('\nWebpack config:'); 103 | inspect(combinedWebpackConfig); 104 | console.log('\nIsomorphic tools config:'); 105 | inspect(combinedToolsConfig); 106 | } 107 | 108 | universalReduxConfig.webpack.config = combinedWebpackConfig; 109 | 110 | return universalReduxConfig; 111 | }; 112 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./transpile'); // babel registration (runtime transpilation for node) 3 | require('./server_es6.js'); 4 | -------------------------------------------------------------------------------- /bin/server_es6.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const express = require('../lib/server.js').default; 3 | const renderer = require('../lib/server/renderer').default; 4 | const start = require('../lib/start').default; 5 | const userConfig = require('./user-config'); 6 | 7 | // since typically the dev server is logging this out too 8 | userConfig.verbose = false; 9 | 10 | const config = require('./merge-configs')(userConfig); 11 | 12 | const app = express(config); 13 | app.use(renderer(config)); 14 | start(app, config); 15 | -------------------------------------------------------------------------------- /bin/transpile.js: -------------------------------------------------------------------------------- 1 | // enable runtime transpilation to use ES6/7 in node 2 | 3 | /* eslint-disable */ 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | var babelrc = fs.readFileSync(path.resolve(__dirname, '../.babelrc')); 8 | var config; 9 | 10 | try { 11 | config = JSON.parse(babelrc); 12 | } catch (err) { 13 | console.error('==> ERROR: Error parsing your babelrc'); 14 | console.error(err); 15 | } 16 | /* eslint-enable */ 17 | 18 | require('babel-core/register')(config); 19 | -------------------------------------------------------------------------------- /bin/user-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const userConfigPath = path.join(process.cwd(), './config/universal-redux.config.js'); 4 | 5 | function getConfig() { 6 | try { 7 | const config = require(path.resolve(userConfigPath)); 8 | if (config.verbose) { 9 | console.log(`Loaded project level config from ${userConfigPath}`); 10 | } 11 | return config; 12 | } catch (err) { 13 | console.warn('No configuration file found, using defaults.'); 14 | return {}; 15 | } 16 | } 17 | 18 | module.exports = getConfig(); 19 | -------------------------------------------------------------------------------- /bin/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./transpile'); // babel registration (runtime transpilation for node) 3 | require('./webpack-dev-server_es6.js'); 4 | -------------------------------------------------------------------------------- /bin/webpack-dev-server_es6.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const Express = require('express'); 3 | const webpack = require('webpack'); 4 | const userConfig = require('./user-config'); 5 | 6 | const config = require('./merge-configs')(userConfig); 7 | 8 | const webpackConfig = config.webpack.config; 9 | const compiler = webpack(webpackConfig); 10 | 11 | const host = config.server.host || 'localhost'; 12 | const port = parseInt(config.server.port, 10) + 1 || 3001; 13 | const serverOptions = { 14 | contentBase: 'http://' + host + ':' + port, 15 | quiet: true, 16 | noInfo: true, 17 | hot: true, 18 | inline: true, 19 | lazy: false, 20 | publicPath: webpackConfig.output.publicPath, 21 | headers: { 'Access-Control-Allow-Origin': '*' }, 22 | stats: { colors: true } 23 | }; 24 | 25 | const app = new Express(); 26 | 27 | app.use(require('webpack-dev-middleware')(compiler, serverOptions)); 28 | app.use(require('webpack-hot-middleware')(compiler)); 29 | 30 | app.listen(port, (err) => { 31 | if (err) { 32 | console.error(err); 33 | } else { 34 | console.info('==> 🚧 Webpack development server listening on port %s', port); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /config/universal-redux.config.js: -------------------------------------------------------------------------------- 1 | const projectRoot = process.cwd(); 2 | const sourceRoot = `${projectRoot}/src`; 3 | const isProduction = process.env.NODE_ENV === 'production'; 4 | 5 | module.exports = { 6 | /* 7 | // Express configuration 8 | */ 9 | server: { 10 | /* 11 | // The host to run the Express universal renderer. See src/server.js. 12 | // 13 | // Expects: String 14 | */ 15 | host: process.env.HOST || 'localhost', 16 | 17 | /* 18 | // The port to run Express universal renderer will run on. See src/server.js. 19 | // 20 | // Expects: Number 21 | */ 22 | port: process.env.PORT || 3000, 23 | 24 | /* 25 | // The path at which static assets are served from. If omitted, Express will 26 | // serve any static assets from your project root 'static' directory. 27 | // Optional. 28 | // 29 | // Expects: String 30 | */ 31 | staticPath: projectRoot + '/static', 32 | 33 | /* 34 | // The path at which a favicon image will be served from using the `serve-favicon` 35 | // library. If omitted, Express will not serve a favicon. Optional. 36 | // 37 | // Expects: String 38 | */ 39 | // favicon: projectRoot + '/static/favicon.ico', 40 | 41 | /* 42 | // The maximum age, in milliseconds, for which a static asset will be 43 | // considered fresh, per the Cache-Control max-age property. If 44 | // ommitted, defaults to 0. Optional. 45 | // 46 | // Expects: Number 47 | */ 48 | // maxAge: 0 49 | 50 | /* 51 | // The renderer returns middleware for universal rendering. This config option sets the framework to 52 | // which this middleware should be compatible. 53 | // Expects: String 54 | // options: 'express' || 'koa' 55 | */ 56 | webFramework: 'express' 57 | }, 58 | 59 | /* 60 | // Globals available to both serverside and clientside rendering. 61 | // You may also add your own here. 62 | */ 63 | globals: { 64 | 65 | /* 66 | // Whether or not to run redux-logger 67 | // 68 | // Expects: Boolean 69 | */ 70 | __LOGGER__: !isProduction, 71 | 72 | /* 73 | // Whether or not to run redux-devtools 74 | // 75 | // Expects: Boolean 76 | */ 77 | __DEVTOOLS__: !isProduction, 78 | 79 | /* 80 | // Whether or not to show redux-devtools when page loads. 81 | // 82 | // Expects: Boolean 83 | */ 84 | __DEVTOOLS_IS_VISIBLE__: true 85 | }, 86 | 87 | /* 88 | // Enable eslint checks per Webpack build. Will not be run 89 | // on production. 90 | // 91 | // Expects: Boolean 92 | */ 93 | lint: { 94 | enabled: false 95 | // config: projectRoot + '/.eslintrc' 96 | }, 97 | 98 | /* 99 | // Providers for the root component to assemble. Built in options 100 | // include 'react-router', 'react-router-redux', 'redux-async-connect', 101 | // 'async-props'. Custom root components may accept additional options. 102 | // Optional. If unspecified will use react-router, react-router-redux, 103 | // and redux-async-connect. Experimental. 104 | // 105 | // Expects: Array 106 | */ 107 | providers: [ 108 | 'react-router', 109 | 'react-router-redux', 110 | 'redux-async-connect' 111 | ], 112 | 113 | /* 114 | // The root component factory file. Optional. Will be added to Webpack aliases. 115 | */ 116 | // rootClientComponent: sourceRoot + '/rootClientComponent.js', 117 | 118 | /* 119 | // The root component factory file. Optional. 120 | */ 121 | // rootServerComponent: sourceRoot + '/rootServerComponent.js', 122 | 123 | /* 124 | // The root component factory file. Optional. Will be added to Webpack aliases. 125 | // Is overridden by either rootClientComponent or rootServerComponent. 126 | */ 127 | // rootComponent: sourceRoot + '/rootComponent.js', 128 | 129 | /* 130 | // Project level babelConfig to be merged with defaults. Optional. 131 | // 132 | // Expects: String 133 | */ 134 | // babelConfig: projectRoot + '/.babelrc', 135 | 136 | /* 137 | // Enable native desktop notifications for Webpack build events. 138 | // Will not be run on production. 139 | // 140 | // Expects: Boolean 141 | */ 142 | notifications: false, 143 | 144 | /* 145 | // Path to a file with customizations for the default 146 | // webpack-isomorphic-tools configuration. Optional. 147 | // 148 | // Expects: String 149 | */ 150 | toolsConfigPath: __dirname + '/webpack-isomorphic-tools.config.js', 151 | 152 | /* 153 | // When eneabled, will output Webpack and Webpack Isomorphic 154 | // Tools configurations at startup 155 | // 156 | // Expects: Boolean 157 | */ 158 | verbose: true, 159 | 160 | /* 161 | // The react-router Routes file. Required. Will be added to Webpack aliases. 162 | */ 163 | routes: sourceRoot + '/routes.js', 164 | 165 | html: { 166 | /* 167 | // A path to a component that provides additional DOM items to be appended 168 | // to the . Optional. 169 | // 170 | // Expects: String 171 | */ 172 | // head: sourceRoot + '/containers/Head/Head.js', 173 | 174 | /* 175 | // A path to a component that provides the root html shell. It is strongly 176 | // encouraged to instead use html.head to provide your own additions, as 177 | // with this parameter you are responsible for some of the internals of 178 | // Universal Redux. 179 | // 180 | // Be sure that the content includes all of the items inside of the default 181 | // Head and Body inside of Universal Redux's src/server directory. 182 | // 183 | // Expects: String 184 | */ 185 | // root: sourceRoot + '/containers/Root/Root.js' 186 | }, 187 | 188 | /* 189 | // Deprecated in favor of html.root (or html.root if 190 | // necessary) 191 | // 192 | // The path to your replacement for the default HTML shell. Optional. 193 | // If not provided, the default used will be that in src/server/html.js 194 | // 195 | // Expects: String 196 | */ 197 | // htmlShell: sourceRoot + '/containers/HtmlShell/HtmlShell.js', 198 | 199 | redux: { 200 | /* 201 | // A path to an index of middleware functions. Optional. 202 | // 203 | // Expects: String 204 | */ 205 | middleware: sourceRoot + '/redux/middleware/index.js', 206 | }, 207 | 208 | /* 209 | // Customizations for Webpack configuration. Optional. 210 | // 211 | // Expects: Object 212 | */ 213 | webpack: { 214 | 215 | /* 216 | // A list of libraries that do not change frequently between deploys 217 | // and are best served in the vendor bundle. Optional. 218 | // 219 | // Expects: Array 220 | */ 221 | // vendorLibraries: [], 222 | 223 | /* 224 | // Webpack configuration cusomtizations. There are more parameters 225 | // available than specified here. For the full list, see 226 | // https://webpack.github.io/docs/configuration.html. Optional. 227 | // 228 | // Expects: Object 229 | */ 230 | config: { 231 | 232 | /* 233 | // The Webpack devtool configuration. May affect build times. 234 | // See https://webpack.github.io/docs/configuration.html#devtool 235 | */ 236 | devtool: isProduction ? 'source-map' : 'inline-eval-cheap-source-map' 237 | } 238 | } 239 | }; 240 | -------------------------------------------------------------------------------- /config/webpack-isomorphic-tools.config.js: -------------------------------------------------------------------------------- 1 | const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 2 | 3 | // see this link for more info on what all of this means 4 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 5 | module.exports = { 6 | 7 | // when adding "js" extension to asset types 8 | // and then enabling debug mode, it may cause a weird error: 9 | // 10 | // [0] npm run start-prod exited with code 1 11 | // Sending SIGTERM to other processes.. 12 | // 13 | // debug: true, 14 | 15 | assets: { 16 | images: { 17 | extensions: [ 18 | 'jpeg', 19 | 'jpg', 20 | 'png', 21 | 'gif' 22 | ], 23 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser 24 | }, 25 | fonts: { 26 | extensions: [ 27 | 'woff', 28 | 'woff2', 29 | 'otf', 30 | 'ttf', 31 | 'eot' 32 | ], 33 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser 34 | }, 35 | videos: { 36 | extensions: [ 'webm', 'mp4' ], 37 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser 38 | }, 39 | svg: { 40 | extension: 'svg', 41 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser 42 | }, 43 | style_modules: { 44 | extensions: [ 'css', 'less', 'scss' ], 45 | filter: (module, regex, options, log) => { 46 | if (options.development) { 47 | // in development mode there's webpack "style-loader", 48 | // so the module.name is not equal to module.name 49 | return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log); 50 | } 51 | 52 | // in production mode there's no webpack "style-loader", 53 | // so the module.name will be equal to the asset path 54 | return regex.test(module.name); 55 | }, 56 | path: (module, options, log) => { 57 | if (options.development) { 58 | // in development mode there's webpack "style-loader", 59 | // so the module.name is not equal to module.name 60 | return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log); 61 | } 62 | 63 | // in production mode there's no webpack "style-loader", 64 | // so the module.name will be equal to the asset path 65 | return module.name; 66 | }, 67 | parser: (module, options, log) => { 68 | if (options.development) { 69 | return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log); 70 | } 71 | 72 | // in production mode there's Extract Text Loader which extracts CSS text away 73 | return module.source; 74 | } 75 | } 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | // require('babel/polyfill'); 2 | 3 | // begin shared setup 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | const autoprefixer = require('autoprefixer'); 7 | 8 | // begin dev setup 9 | const host = (process.env.HOST || 'localhost'); 10 | const port = parseInt(process.env.PORT, 10) + 1 || 3001; 11 | 12 | // begin prod setup 13 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 14 | 15 | const vendor = [ 16 | 'lodash', 17 | 'react', 18 | 'react-dom', 19 | 'react-router', 20 | 'react-redux', 21 | 'redux', 22 | 'redux-logger', 23 | 'react-router-redux' 24 | ]; 25 | 26 | module.exports = { 27 | common: { 28 | context: path.resolve(__dirname, '..'), 29 | entry: { 30 | main: [ 31 | path.resolve(__dirname, '..', 'lib/client.js') 32 | ] 33 | }, 34 | module: { 35 | loaders: [ 36 | // { test: /\.jsx?$/, exclude: /node_modules/, loaders: jsLoaders }, // now prepended in merge-configs and merge-babel-config 37 | { test: /\.json$/, loader: 'json-loader' }, 38 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' }, 39 | { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' }, 40 | { test: /\.otf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' }, 41 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream' }, 42 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' }, 43 | { test: /\.(webm|mp4)$/, loader: 'file' }, 44 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml' } 45 | ] 46 | }, 47 | progress: true, 48 | postcss: () => { 49 | return [ autoprefixer({ browsers: [ 'last 2 versions' ] }) ]; 50 | }, 51 | resolve: { 52 | modulesDirectories: [ 53 | 'src', 54 | 'node_modules' 55 | ], 56 | extensions: [ '', '.json', '.js', '.jsx' ] 57 | }, 58 | resolveLoader: { 59 | modulesDirectories: [ 60 | 'src', 61 | 'node_modules' 62 | ] 63 | } 64 | }, 65 | development: { 66 | devtool: 'inline-source-map', 67 | entry: { 68 | main: [ 69 | 'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr', 70 | ] 71 | }, 72 | output: { 73 | filename: '[name]-[hash].js', 74 | chunkFilename: '[name]-[chunkhash].js', 75 | publicPath: 'http://' + host + ':' + port + '/dist/' 76 | }, 77 | module: { 78 | loaders: [ 79 | { test: /\.css$/, loader: 'style!css' }, 80 | { test: /\.less$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!postcss!less?outputStyle=expanded&sourceMap' }, 81 | { test: /\.scss$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!postcss!sass?outputStyle=expanded&sourceMap' } 82 | ] 83 | }, 84 | plugins: [ 85 | // hot reload 86 | new webpack.HotModuleReplacementPlugin(), 87 | new webpack.IgnorePlugin(/webpack-assets\.json$/), 88 | new webpack.DefinePlugin({ 89 | __CLIENT__: true, 90 | __SERVER__: false 91 | }) 92 | ] 93 | }, 94 | production: { 95 | devtool: 'source-map', 96 | entry: { 97 | vendor 98 | }, 99 | output: { 100 | filename: '[name]-[chunkhash].js', 101 | chunkFilename: '[name]-[chunkhash].js', 102 | publicPath: '/dist/' 103 | }, 104 | module: { 105 | loaders: [ 106 | { test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css') }, 107 | { test: /\.less$/, loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2&sourceMap!postcss!less?outputStyle=expanded&sourceMap=true&sourceMapContents=true') }, 108 | { test: /\.scss$/, loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2&sourceMap!postcss!sass?outputStyle=expanded&sourceMap=true&sourceMapContents=true') } 109 | ] 110 | }, 111 | plugins: [ 112 | // css files from the extract-text-plugin loader 113 | new ExtractTextPlugin('[name]-[chunkhash].css', { allChunks: true }), 114 | new webpack.DefinePlugin({ 115 | __CLIENT__: true, 116 | __SERVER__: false 117 | }), 118 | 119 | // set global consts 120 | new webpack.DefinePlugin({ 121 | 'process.env': { 122 | // Useful to reduce the size of client-side libraries, e.g. react 123 | NODE_ENV: JSON.stringify('production') 124 | } 125 | }), 126 | 127 | // optimizations 128 | new webpack.optimize.DedupePlugin(), 129 | new webpack.optimize.OccurenceOrderPlugin(), 130 | new webpack.optimize.UglifyJsPlugin({ 131 | compress: { 132 | warnings: false 133 | } 134 | }), 135 | new webpack.optimize.CommonsChunkPlugin({ 136 | names: [ 'vendor' ], 137 | minChunks: Infinity 138 | }) 139 | ] 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | browsers: ['PhantomJS'], 7 | 8 | singleRun: !!process.env.CONTINUOUS_INTEGRATION, 9 | 10 | frameworks: [ 'mocha' ], 11 | 12 | files: [ 13 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 14 | 'tests.webpack.js' 15 | ], 16 | 17 | preprocessors: { 18 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] 19 | }, 20 | 21 | reporters: [ 'mocha' ], 22 | 23 | plugins: [ 24 | require("karma-webpack"), 25 | require("karma-mocha"), 26 | require("karma-mocha-reporter"), 27 | require("karma-phantomjs-launcher"), 28 | require("karma-sourcemap-loader") 29 | ], 30 | 31 | webpack: { 32 | devtool: 'inline-source-map', 33 | module: { 34 | loaders: [ 35 | { test: /\.(jpe?g|png|gif|svg)$/, loader: 'url', query: {limit: 10240} }, 36 | { test: /\.js$/, exclude: /node_modules/, loaders: ['babel']}, 37 | { test: /\.json$/, loader: 'json-loader' }, 38 | { test: /\.less$/, loader: 'style!css!less' }, 39 | { test: /\.scss$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap' } 40 | ] 41 | }, 42 | resolve: { 43 | modulesDirectories: [ 44 | 'src', 45 | 'node_modules' 46 | ], 47 | extensions: ['', '.json', '.js'] 48 | }, 49 | plugins: [ 50 | new webpack.IgnorePlugin(/\.json$/), 51 | new webpack.NoErrorsPlugin(), 52 | new webpack.DefinePlugin({ 53 | __CLIENT__: true, 54 | __SERVER__: false, 55 | __DEVELOPMENT__: true, 56 | __DEVTOOLS__: false // <-------- DISABLE redux-devtools HERE 57 | }) 58 | ] 59 | }, 60 | 61 | webpackServer: { 62 | noInfo: true 63 | } 64 | 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-redux", 3 | "description": "A universal redux renderer (ES6, Webpack, Express)", 4 | "author": "Buck DeFore (http://github.com/bdefore)", 5 | "license": "MIT", 6 | "version": "4.4.3", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/bdefore/universal-redux.git" 10 | }, 11 | "homepage": "https://github.com/bdefore/universal-redux", 12 | "keywords": [ 13 | "react", 14 | "isomorphic", 15 | "universal", 16 | "webpack", 17 | "express", 18 | "hot reloading", 19 | "react-hot-reloader", 20 | "redux", 21 | "renderer", 22 | "boilerplate", 23 | "babel", 24 | "npm" 25 | ], 26 | "main": "lib/index.js", 27 | "scripts": { 28 | "compile": "babel --presets es2015,stage-0,react --plugins transform-runtime src/ --out-dir lib/", 29 | "dev": "./bin/local-dev.sh", 30 | "lint": "eslint -c .eslintrc src bin config", 31 | "test": "karma start --single-run", 32 | "prepublish": "npm run compile" 33 | }, 34 | "dependencies": { 35 | "autoprefixer": "6.3.7", 36 | "babel-cli": "6.14.0", 37 | "babel-core": "6.14.0", 38 | "babel-eslint": "6.1.2", 39 | "babel-loader": "6.2.5", 40 | "babel-plugin-react-transform": "2.0.2", 41 | "babel-plugin-transform-decorators-legacy": "1.3.4", 42 | "babel-plugin-transform-runtime": "6.15.0", 43 | "babel-preset-es2015": "6.14.0", 44 | "babel-preset-react": "6.11.1", 45 | "babel-preset-stage-0": "6.5.0", 46 | "babel-runtime": "6.11.6", 47 | "compression": "1.6.2", 48 | "css-loader": "0.25.0", 49 | "eslint": "3.5.0", 50 | "eslint-config-airbnb": "11.1.0", 51 | "eslint-loader": "1.5.0", 52 | "eslint-plugin-import": "1.15.0", 53 | "eslint-plugin-jsx-a11y": "2.2.2", 54 | "eslint-plugin-react": "6.2.2", 55 | "express": "4.14.0", 56 | "extract-text-webpack-plugin": "1.0.1", 57 | "file-loader": "0.9.0", 58 | "fmp-redux-async-connect": "0.1.15", 59 | "json-loader": "0.5.4", 60 | "less": "2.7.1", 61 | "less-loader": "2.2.3", 62 | "lodash": "^4.11.1", 63 | "node-sass": "3.10.0", 64 | "postcss-loader": "0.13.0", 65 | "pretty-error": "2.0.0", 66 | "query-string": "3.0.1", 67 | "react": "^15.0.1", 68 | "react-addons-test-utils": "^15.0.1", 69 | "react-dom": "^15.0.1", 70 | "react-helmet": "^3.1.0", 71 | "react-redux": "4.4.5", 72 | "react-router": "2.8.1", 73 | "react-router-redux": "3.0.0", 74 | "react-router-scroll": "0.3.2", 75 | "react-transform-catch-errors": "1.0.2", 76 | "react-transform-hmr": "1.0.4", 77 | "redbox-react": "1.3.0", 78 | "redux": "3.6.0", 79 | "redux-devtools": "3.3.1", 80 | "redux-devtools-dock-monitor": "1.1.1", 81 | "redux-devtools-log-monitor": "1.0.11", 82 | "redux-logger": "2.6.1", 83 | "sass-loader": "4.0.0", 84 | "serialize-javascript": "1.3.0", 85 | "serve-favicon": "2.3.0", 86 | "style-loader": "0.13.1", 87 | "url-loader": "0.5.7", 88 | "webpack": "1.13.2", 89 | "webpack-config-merger": "0.0.5", 90 | "webpack-dev-middleware": "1.8.1", 91 | "webpack-error-notification": "0.1.6", 92 | "webpack-hot-middleware": "2.12.1", 93 | "webpack-isomorphic-tools": "2.5.8" 94 | }, 95 | "devDependencies": { 96 | "chai": "3.4.1", 97 | "karma": "0.13.19", 98 | "karma-cli": "0.1.2", 99 | "karma-mocha": "0.2.1", 100 | "karma-mocha-reporter": "1.1.5", 101 | "karma-phantomjs-launcher": "0.2.3", 102 | "karma-sourcemap-loader": "0.3.7", 103 | "karma-webpack": "1.7.0", 104 | "mocha": "2.3.4", 105 | "phantomjs": "1.9.19", 106 | "phantomjs-polyfill": "0.0.1" 107 | }, 108 | "peerDependencies": { 109 | "react": "^15.0.0", 110 | "react-dom": "^15.0.0", 111 | "react-router": "^2.0.0" 112 | }, 113 | "engines": { 114 | "node": ">=4.1.1", 115 | "npm": ">=3.3.12" 116 | }, 117 | "bin": { 118 | "universal-redux-dev": "./bin/local-dev.sh", 119 | "universal-redux-watch": "./bin/webpack-dev-server.js", 120 | "universal-redux-server": "./bin/server.js", 121 | "universal-redux-build": "./bin/build.js" 122 | }, 123 | "bugs": { 124 | "url": "https://github.com/bdefore/universal-redux/issues" 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import createStore from './shared/create'; 5 | import { render as renderDevtools } from './client/devtools'; 6 | 7 | // dependencies of external source. these resolve via webpack aliases 8 | // as assigned in merge-configs.js 9 | import middleware, { middlewareAppliedCallback } from 'universal-redux/middleware'; 10 | import createRootClientComponent from 'universal-redux/rootClientComponent'; 11 | 12 | const dest = document.getElementById('content'); 13 | 14 | const store = createStore(middleware, window.__data); 15 | const devComponent = renderDevtools(); 16 | 17 | if (middlewareAppliedCallback) { 18 | middlewareAppliedCallback(); 19 | } 20 | 21 | // There is probably no need to be asynchronous here 22 | createRootClientComponent(store, __PROVIDERS__, devComponent) 23 | .then((root) => { 24 | ReactDOM.render(root, dest); 25 | 26 | if (process.env.NODE_ENV !== 'production') { 27 | window.React = React; // enable debugger 28 | if (!dest || !dest.firstChild || !dest.firstChild.attributes || !dest.firstChild.attributes['data-react-checksum']) { 29 | console.warn('WARNING: Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.'); 30 | } 31 | } 32 | }) 33 | .catch((err) => { 34 | console.error(err, err.stack); 35 | }); 36 | -------------------------------------------------------------------------------- /src/client/devtools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose as _compose, applyMiddleware } from 'redux'; 3 | import { createDevTools, persistState } from 'redux-devtools'; 4 | 5 | import LogMonitor from 'redux-devtools-log-monitor'; 6 | import DockMonitor from 'redux-devtools-dock-monitor'; 7 | 8 | export const DevTools = createDevTools( 9 | 10 | 11 | 12 | ); 13 | 14 | export const InvisibleDevTools = createDevTools( 15 | 16 | 17 | 18 | ); 19 | 20 | export function compose(middleware) { 21 | const Tools = __DEVTOOLS_IS_VISIBLE__ ? DevTools : InvisibleDevTools; 22 | return _compose( 23 | applyMiddleware(...middleware), 24 | window.devToolsExtension ? window.devToolsExtension() : Tools.instrument(), 25 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) 26 | ); 27 | } 28 | 29 | // context: https://github.com/rackt/react-router-redux/compare/1.0.2...2.0.2 30 | // context: https://github.com/rackt/react-router-redux/pull/141#issuecomment-167587581 31 | export function listenToRouter(routerMiddleware, store) { 32 | routerMiddleware.listenForReplays(store); 33 | } 34 | 35 | export function render() { 36 | if (__DEVTOOLS__ && !window.devToolsExtension) { 37 | const Tools = __DEVTOOLS_IS_VISIBLE__ ? DevTools : InvisibleDevTools; 38 | return ; 39 | } 40 | 41 | return null; 42 | } 43 | -------------------------------------------------------------------------------- /src/client/providers/async-props.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Router } from 'react-router'; 4 | import getRoutes from 'universal-redux/routes'; 5 | import AsyncProps from '../../vendor/async-props'; 6 | 7 | export default function (store, devComponent) { 8 | const root = ( 9 | 10 |
11 | } history={history}> 12 | {getRoutes(store)} 13 | 14 | {devComponent} 15 |
16 |
17 | ); 18 | 19 | return Promise.resolve(root); 20 | } 21 | -------------------------------------------------------------------------------- /src/client/providers/react-router-scroll.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { applyRouterMiddleware, browserHistory, Router } from 'react-router'; 4 | import { useScroll } from 'react-router-scroll'; 5 | import getRoutes from 'universal-redux/routes'; 6 | 7 | export default function (store, devComponent) { 8 | const root = ( 9 | 10 |
11 | 12 | {getRoutes(store)} 13 | 14 | {devComponent} 15 |
16 |
17 | ); 18 | 19 | return Promise.resolve(root); 20 | } 21 | -------------------------------------------------------------------------------- /src/client/providers/react-router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { browserHistory, Router } from 'react-router'; 3 | import { ReduxAsyncConnect } from 'fmp-redux-async-connect'; 4 | import getRoutes from 'universal-redux/routes'; 5 | 6 | export default function (store) { 7 | const component = ( 8 | } history={browserHistory}> 9 | {getRoutes(store)} 10 | 11 | ); 12 | 13 | return component; 14 | } 15 | -------------------------------------------------------------------------------- /src/client/providers/redux-async-connect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import createRouter from './react-router'; 4 | 5 | export default function (store, devComponent) { 6 | const root = ( 7 | 8 |
9 | {createRouter(store)} 10 | {devComponent} 11 |
12 |
13 | ); 14 | 15 | return Promise.resolve(root); 16 | } 17 | -------------------------------------------------------------------------------- /src/client/root.js: -------------------------------------------------------------------------------- 1 | import { includes } from 'lodash'; 2 | import reduxAsyncConnectClient from './providers/redux-async-connect'; 3 | import reactRouterScroll from './providers/react-router-scroll'; 4 | import asyncPropsClient from './providers/async-props'; 5 | 6 | export default function (store, providers, devComponent) { 7 | let client = reduxAsyncConnectClient; 8 | if (includes(providers, 'react-router-scroll')) { 9 | client = reactRouterScroll; 10 | } 11 | if (includes(providers, 'async-props')) { 12 | client = asyncPropsClient; 13 | } 14 | if (includes(providers, 'fmp-redux-async-connect')) { 15 | client = reduxAsyncConnectClient; 16 | } 17 | 18 | return client(store, devComponent); 19 | } 20 | -------------------------------------------------------------------------------- /src/configure.js: -------------------------------------------------------------------------------- 1 | import { each } from 'lodash'; 2 | import mergeConfigs from '../bin/merge-configs'; 3 | 4 | function validateConfig(config) { 5 | const errors = []; 6 | if (!config) { 7 | errors.push('==> ERROR: No configuration supplied.'); 8 | } 9 | if (config.server) { 10 | if (!config.server.host) { 11 | errors.push('==> ERROR: No host parameter supplied.'); 12 | } 13 | if (!config.server.port) { 14 | errors.push('==> ERROR: No port parameter supplied.'); 15 | } 16 | } 17 | if (!config.routes) { 18 | errors.push('==> ERROR: Must supply routes.'); 19 | } 20 | // TODO: check for more 21 | return errors; 22 | } 23 | 24 | export default (projectConfig) => { 25 | // since typically the dev server is logging this out too 26 | projectConfig.verbose = false; // eslint-disable-line no-param-reassign 27 | 28 | const config = mergeConfigs(projectConfig); 29 | 30 | // add user defined globals for serverside access 31 | each(config.globals, (value, key) => { global[key] = value; }); 32 | 33 | const errors = validateConfig(config); 34 | 35 | if (errors.length > 0) { 36 | each(errors, (error) => { console.error(error); }); 37 | throw new Error('Configuration errors for universal-redux. Stopping.'); 38 | } else { 39 | console.log('universal-redux configuration is valid.'); 40 | } 41 | 42 | return config; 43 | }; 44 | -------------------------------------------------------------------------------- /src/helpers/empty.js: -------------------------------------------------------------------------------- 1 | // so that we can use webpack aliases for optional 2 | // features like specifying custom redux middleware 3 | // without blowing up when they are imported on the 4 | // client and the path cannot be resolved 5 | export default {}; 6 | -------------------------------------------------------------------------------- /src/helpers/fetchAllData.js: -------------------------------------------------------------------------------- 1 | function fetchAllData(components, getState, dispatch, location, params, deferred) { 2 | const methodName = deferred ? 'fetchDataDeferred' : 'fetchData'; 3 | return components 4 | .filter(component => !!component) // Weed out 'undefined' routes 5 | .filter(component => component[methodName]) // only look at ones with a static fetchData() 6 | .map(component => component[methodName]) // pull out fetch data methods 7 | .map(fetchData => fetchData(getState, dispatch, location, params)); // call fetch data methods and save promises 8 | } 9 | 10 | export default (components, getState, dispatch, location, params) => { 11 | return new Promise((resolve) => { 12 | const doTransition = () => { 13 | Promise.all(fetchAllData(components, getState, dispatch, location, params, true)) 14 | .then(resolve) 15 | .catch((error) => { 16 | // TODO: You may want to handle errors for fetchDataDeferred here 17 | console.warn('Warning: Error in fetchDataDeferred', error); 18 | return resolve(); 19 | }); 20 | }; 21 | 22 | Promise.all(fetchAllData(components, getState, dispatch, location, params)) 23 | .then(doTransition) 24 | .catch((error) => { 25 | // TODO: You may want to handle errors for fetchData here 26 | console.warn('Warning: Error in fetchData', error); 27 | return doTransition(); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/helpers/inspect.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | export default (obj) => { 4 | const utilOptions = { 5 | depth: 12, 6 | colors: true 7 | }; 8 | 9 | console.log(util.inspect(obj, utilOptions)); 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export express from './server'; 2 | export start from './start'; 3 | export renderer from './server/renderer'; 4 | export configure from './configure'; 5 | export asyncProps from './vendor/async-props'; 6 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Express from 'express'; 3 | import favicon from 'serve-favicon'; 4 | import compression from 'compression'; 5 | 6 | import configure from './configure'; 7 | 8 | export default (projectConfig) => { 9 | const config = configure(projectConfig); 10 | const server = new Express(); 11 | server.use(compression()); 12 | 13 | if (config.server.favicon) { 14 | server.use(favicon(path.resolve(config.server.favicon))); 15 | } 16 | const maxAge = config.server.maxAge || 0; 17 | server.use(Express.static(path.resolve(config.server.staticPath), { maxage: maxAge })); 18 | 19 | return server; 20 | }; 21 | -------------------------------------------------------------------------------- /src/server/body.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactDOM from 'react-dom/server'; 3 | import serialize from 'serialize-javascript'; 4 | 5 | export default class Body extends Component { 6 | static propTypes = { 7 | assets: PropTypes.object, 8 | component: PropTypes.node, 9 | store: PropTypes.object, 10 | }; 11 | 12 | render() { 13 | const { assets, component, store } = this.props; 14 | const content = component ? ReactDOM.renderToString(component) : ''; 15 | 16 | return ( 17 | 18 |
19 | ` 134 | cb(null, propsAndComponents, scriptString) 135 | } 136 | } 137 | ) 138 | } 139 | 140 | function hydrate(props) { 141 | if (typeof __ASYNC_PROPS__ !== 'undefined') 142 | return { 143 | propsArray: __ASYNC_PROPS__, 144 | componentsArray: filterAndFlattenComponents(props.components) 145 | } 146 | else 147 | return null 148 | } 149 | 150 | 151 | class AsyncPropsContainer extends React.Component { 152 | 153 | static propTypes = { 154 | Component: func.isRequired, 155 | routerProps: object.isRequired 156 | }; 157 | 158 | static contextTypes = { 159 | asyncProps: object.isRequired 160 | }; 161 | 162 | componentWillReceiveProps(nextProps) { 163 | const paramsChanged = !shallowEqual(nextProps.routerProps.routeParams, 164 | this.props.routerProps.routeParams) 165 | if (paramsChanged) { 166 | this.context.asyncProps.reloadComponent(nextProps.Component) 167 | } 168 | } 169 | 170 | render() { 171 | const { Component, routerProps, ...props } = this.props 172 | const { propsAndComponents, loading, reloadComponent } = this.context.asyncProps 173 | const asyncProps = lookupPropsForComponent(Component, propsAndComponents) 174 | const reload = () => reloadComponent(Component) 175 | return ( 176 | 183 | ) 184 | } 185 | 186 | } 187 | 188 | class AsyncProps extends React.Component { 189 | 190 | static childContextTypes = { 191 | asyncProps: object 192 | }; 193 | 194 | static propTypes = { 195 | components: array.isRequired, 196 | params: object.isRequired, 197 | location: object.isRequired, 198 | onError: func.isRequired, 199 | renderLoading: func.isRequired, 200 | 201 | // server rendering 202 | propsArray: array, 203 | componentsArray: array 204 | }; 205 | 206 | static defaultProps = { 207 | onError(err) { 208 | throw err 209 | }, 210 | 211 | renderLoading() { 212 | return null 213 | }, 214 | 215 | render(props) { 216 | return 217 | } 218 | }; 219 | 220 | constructor(props, context) { 221 | super(props, context) 222 | const { propsArray, componentsArray } = this.props 223 | const isServerRender = propsArray && componentsArray 224 | this.state = { 225 | loading: false, 226 | prevProps: null, 227 | propsAndComponents: isServerRender ? 228 | { propsArray, componentsArray } : 229 | hydrate(props) 230 | } 231 | } 232 | 233 | getChildContext() { 234 | const { loading, propsAndComponents } = this.state 235 | return { 236 | asyncProps: { 237 | loading, 238 | propsAndComponents, 239 | reloadComponent: (Component) => { 240 | this.reloadComponent(Component) 241 | } 242 | } 243 | } 244 | } 245 | 246 | componentDidMount() { 247 | const { components, params, location } = this.props 248 | this.loadAsyncProps(components, params, location) 249 | } 250 | 251 | componentWillReceiveProps(nextProps) { 252 | const routeChanged = nextProps.location !== this.props.location 253 | if (!routeChanged) 254 | return 255 | 256 | const oldComponents = filterAndFlattenComponents(this.props.components) 257 | const newComponents = filterAndFlattenComponents(nextProps.components) 258 | let components = arrayDiff(oldComponents, newComponents) 259 | 260 | if (components.length === 0) { 261 | const sameComponents = shallowEqual(oldComponents, newComponents) 262 | if (sameComponents) { 263 | const paramsChanged = !shallowEqual(nextProps.params, this.props.params) 264 | if (paramsChanged) 265 | components = [ last(newComponents) ] 266 | } 267 | } 268 | 269 | if (components.length > 0) 270 | this.loadAsyncProps(components, nextProps.params, nextProps.location) 271 | } 272 | 273 | handleError(cb) { 274 | return (err, ...args) => { 275 | if (err && this.props.onError) 276 | this.props.onError(err) 277 | else 278 | cb(null, ...args) 279 | } 280 | } 281 | 282 | componentWillUnmount() { 283 | this._unmounted = true 284 | } 285 | 286 | loadAsyncProps(components, params, location, options) { 287 | this.setState({ 288 | loading: true, 289 | prevProps: this.props 290 | }) 291 | loadAsyncProps( 292 | filterAndFlattenComponents(components), 293 | params, 294 | this.handleError((err, propsAndComponents) => { 295 | const force = options && options.force 296 | const sameLocation = this.props.location === location 297 | // FIXME: next line has potential (rare) race conditions I think. If 298 | // somebody calls reloadAsyncProps, changes location, then changes 299 | // location again before its done and state gets out of whack (Rx folks 300 | // are like "LOL FLAT MAP LATEST NEWB"). Will revisit later. 301 | if ((force || sameLocation) && !this._unmounted) { 302 | if (this.state.propsAndComponents) { 303 | propsAndComponents = mergePropsAndComponents( 304 | this.state.propsAndComponents, 305 | propsAndComponents 306 | ) 307 | } 308 | this.setState({ 309 | loading: false, 310 | propsAndComponents, 311 | prevProps: null 312 | }) 313 | } 314 | }) 315 | ) 316 | } 317 | 318 | reloadComponent(Component) { 319 | const { params } = this.props 320 | this.loadAsyncProps([ Component ], params, null, { force: true }) 321 | } 322 | 323 | render() { 324 | const { propsAndComponents } = this.state 325 | if (!propsAndComponents) { 326 | return this.props.renderLoading() 327 | } 328 | else { 329 | const props = this.state.loading ? this.state.prevProps : this.props 330 | return this.props.render(props) 331 | } 332 | } 333 | 334 | } 335 | 336 | export default AsyncProps 337 | 338 | /* eslint-enable */ 339 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | var context = require.context('./src', true, /-test\.js$/); 2 | context.keys().forEach(context); 3 | --------------------------------------------------------------------------------