├── .babelrc
├── .editorconfig
├── .env.example
├── .eslintignore
├── .eslintrc
├── .gitignore
├── README.md
├── client
├── index.js
└── styles.js
├── common
├── css
│ ├── base
│ │ ├── _fonts.scss
│ │ ├── _overrides.scss
│ │ ├── _styles.scss
│ │ └── index.scss
│ └── resources
│ │ ├── _colors.scss
│ │ ├── _mixins.scss
│ │ ├── _variables.scss
│ │ └── _vendors.scss
├── fonts
│ └── .gitkeep
├── images
│ └── favicon.png
└── js
│ ├── actions
│ └── todos.js
│ ├── components
│ ├── common
│ │ ├── Footer
│ │ │ └── index.js
│ │ ├── Header
│ │ │ └── index.js
│ │ ├── Loading
│ │ │ └── index.js
│ │ ├── RouteWithSubRoutes
│ │ │ └── index.js
│ │ └── index.js
│ └── todos
│ │ ├── TodoForm
│ │ ├── index.js
│ │ ├── index.scss
│ │ └── spec
│ │ │ ├── TodoForm.test.js
│ │ │ └── __snapshots__
│ │ │ └── TodoForm.test.js.snap
│ │ ├── TodoItem
│ │ ├── index.js
│ │ ├── index.scss
│ │ └── spec
│ │ │ ├── TodoItem.test.js
│ │ │ └── __snapshots__
│ │ │ └── TodoItem.test.js.snap
│ │ ├── TodoList
│ │ ├── index.js
│ │ ├── index.scss
│ │ └── spec
│ │ │ ├── TodoList.test.js
│ │ │ └── __snapshots__
│ │ │ └── TodoList.test.js.snap
│ │ └── index.js
│ ├── constants
│ └── index.js
│ ├── containers
│ ├── App
│ │ └── index.js
│ └── Todos
│ │ ├── index.js
│ │ └── index.scss
│ ├── lib
│ ├── api.js
│ └── generateActionCreator.js
│ ├── middleware
│ └── .gitkeep
│ ├── pages
│ ├── Error
│ │ └── index.js
│ ├── Home
│ │ ├── index.js
│ │ └── index.scss
│ └── Todos
│ │ └── index.js
│ ├── reducers
│ ├── index.js
│ └── todos.js
│ ├── routes
│ └── index.js
│ └── store
│ ├── index.dev.js
│ ├── index.js
│ └── index.prod.js
├── config
└── index.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── postinstall.js
├── server
├── .node-dev.json
├── api
│ ├── index.js
│ └── todos
│ │ ├── index.js
│ │ ├── spec
│ │ └── todos.controller.test.js
│ │ ├── todos.controller.js
│ │ └── todos.fixture.js
├── index.js
├── lib
│ └── .gitkeep
├── middleware
│ ├── httpsRedirect.js
│ └── index.js
├── registerAliases.js
├── renderer
│ ├── handler.js
│ ├── index.js
│ └── render.js
├── server.js
└── templates
│ └── layouts
│ └── application.html
├── test
└── support
│ ├── jest.config.js
│ ├── jest.globalSetup.js
│ └── jest.setup.js
└── webpack
├── babel.config.client.js
├── babel.config.ssr.js
├── base.js
├── development.client.babel.js
├── production.client.babel.js
└── production.ssr.babel.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "targets": {
7 | "node": "current",
8 | "uglify": false
9 | }
10 | }
11 | ]
12 | ],
13 | "plugins": [
14 | "transform-es2015-modules-commonjs",
15 | "transform-class-properties",
16 | "transform-export-extensions",
17 | "transform-object-rest-spread"
18 | ],
19 | "env": {
20 | "test": {
21 | "presets": [
22 | "react"
23 | ]
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Server-side application settings
2 | # The main port to run the server on
3 | APPLICATION_PORT=3000
4 |
5 | # The absolute URL to the application.
6 | APPLICATION_BASE_URL=http://localhost:3000
7 |
8 | # The output path of server and client files built by webpack and babel.
9 | OUTPUT_PATH=dist
10 | PUBLIC_OUTPUT_PATH=dist/public
11 |
12 | # Settings for webpack-dev-server.
13 | DEV_SERVER_PORT=3001
14 | DEV_SERVER_HOSTNAME=localhost
15 | DEV_SERVER_HOST_URL=http://localhost:3001
16 |
17 | # The primary asset path. Can be changed to be a CDN URL.
18 | PUBLIC_ASSET_PATH=http://localhost:3001/assets/
19 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules/*
2 | **/dist/*
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "max-len": [
5 | "error",
6 | { "code": 80, "tabWidth": 2 }
7 | ],
8 | "indent": [
9 | 1,
10 | 2,
11 | { "SwitchCase": 1 }
12 | ],
13 | "quotes": [
14 | 2,
15 | "single"
16 | ],
17 | "react/jsx-uses-react": 1,
18 | "react/jsx-uses-vars": 1,
19 | "linebreak-style": [
20 | 2,
21 | "unix"
22 | ],
23 | "no-console": 0,
24 | "no-unused-vars": [1],
25 | "semi": [
26 | 2,
27 | "always"
28 | ]
29 | },
30 | "env": {
31 | "browser": true,
32 | "es6": true,
33 | "jest": true,
34 | "node": true
35 | },
36 | "globals": {
37 | "expect": true,
38 | "__non_webpack_require__": true
39 | },
40 | "extends": [
41 | "eslint:recommended",
42 | "plugin:react/recommended"
43 | ],
44 | "parserOptions": {
45 | "sourceType": "module",
46 | "ecmaFeatures": {
47 | "experimentalObjectRestSpread": true,
48 | "jsx": true
49 | },
50 | "ecmaVersion": 6
51 | },
52 | "plugins": [
53 | "babel",
54 | "react"
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-error.log
2 | .env
3 | node_modules
4 | npm-debug.log*
5 | .DS_Store
6 |
7 | # ignore built static files
8 | /dist
9 | /webpack-assets.json
10 | /webpack-stats.json
11 | /react-loadable.json
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Universal React Redux Boilerplate
2 |
3 | A universal React/Redux boilerplate with sensible defaults. Out of the box, this
4 | boilerplate comes with:
5 |
6 | - Server-side rendering with Express
7 | - Code splitting with [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) and [react-loadable](https://github.com/thejameskyle/react-loadable)
8 | - Sane [webpack configurations](webpack/)
9 | - JS hot reloading with [react-hot-loader (@next)](https://github.com/gaearon/react-hot-loader) and [webpack-dev-server](https://github.com/webpack/webpack-dev-server)
10 | - CSS, SASS and [css-modules](https://github.com/css-modules/css-modules) support with hot reloading and no [flash of unstyled content](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) ([css-hot-loader](https://github.com/shepherdwind/css-hot-loader))
11 | - Routing with [react-router-v4](https://github.com/ReactTraining/react-router)
12 | - Full production builds that do not rely on `babel-node`.
13 | - Pre-configured testing tools with `jest` and `enzyme` to work with css modules, static files, and aliased module paths.
14 |
15 | ## Philosophy
16 |
17 | The JavaScript ecosystem is brimming with open source libraries. With advances
18 | in ES6 and commitments by the big tech companies to invest in JavaScript, the
19 | last several years have arguably turned web development into what was once a
20 | huge pain in the ass, to a pretty decently enjoyable experience.
21 |
22 | With so many different packages now available, we now have the freedom and the
23 | choice to craft applications to our exact specifications, reducing bloat and
24 | minimizing the number of code we need to support cross-platform apps. It really
25 | is a new world.
26 |
27 | However, with so many different developers working on different libraries,
28 | things are constantly in flux, and breaking changes are often introduced. It can
29 | be hard to keep up with the latest and greatest since they're always changing.
30 |
31 | To help alleviate this, we've collected some of the best practices and features
32 | from the React ecosystem and put them in one place. Although this boilerplate is
33 | fully production-capable as is, its main goal is to serve as an example of how
34 | to bring an application together using the latest tools in the ecosystem.
35 |
36 | ## Development Mode
37 |
38 | Copy environment variables and edit them if necessary:
39 |
40 | ```
41 | cp .env.example .env
42 | ```
43 |
44 | Then:
45 |
46 | ```
47 | npm install
48 | npm start
49 | ```
50 |
51 | Direct your browser to `http://localhost:3000`.
52 |
53 | ## Production Builds
54 |
55 | Add environment variables the way you normally would on your production system.
56 |
57 | ```
58 | npm run prod:build
59 | npm run serve
60 | ```
61 |
62 | Or simply:
63 |
64 | ```
65 | npm run prod
66 | ```
67 |
68 | If using Heroku, simply add a `Procfile` in the root directory. The
69 | [postinstall](postinstall.js) script will do the rest.
70 |
71 | ```
72 | web: npm run serve
73 | ```
74 |
75 | ## Path Aliases
76 |
77 | In `package.json`, there is a property named `_moduleAliases`. This object
78 | defines the require() aliases used by both webpack and node.
79 |
80 | Aliased paths are prefixed with one of two symbols, which denote different
81 | things:
82 |
83 | `@` - component and template paths, e.g. `@components`
84 |
85 | `$` - server paths that are built by babel, e.g. `server/api`
86 |
87 | Aliases are nice to use for convenience, and lets us avoid using relative paths
88 | in our components:
89 |
90 | ```
91 | // This sucks
92 | import SomeComponent from '../../../components/SomeComponent';
93 |
94 | // This is way better
95 | import SomeComponent from '@components/SomeComponent';
96 | ```
97 |
98 | You can add additional aliases in `package.json` to your own liking.
99 |
100 | ## Environment Variables
101 |
102 | In development mode, environment variables are loaded by `dotenv` off the `.env`
103 | file in your root directory. In production, you'll have to manage these
104 | yourself.
105 |
106 | An example with Heroku:
107 |
108 | ```
109 | heroku config:set FOO=bar
110 | ```
111 |
112 | ## CSS Modules
113 |
114 | This project uses [CSS Modules](https://github.com/css-modules/css-modules).
115 | Class names should be in `camelCase`. Simply import the .scss file into your
116 | component, for example:
117 |
118 | ```
119 | ├── components
120 | │ ├── Header.js
121 | │ ├── Header.scss
122 | ```
123 |
124 | ```
125 | // Header.scss
126 | .headerContainer {
127 | height: 100px;
128 | width: 100%;
129 | }
130 | ```
131 |
132 | ```
133 | // Header.js
134 | import css from './Header.scss';
135 |
136 | const Header = (props) => {
137 | return (
138 |
139 | {...}
140 |
141 | );
142 | }
143 |
144 | ```
145 |
146 | ## Redux Devtools
147 |
148 | This project supports the awesome [Redux Devtools Extension](https://github.com/zalmoxisus/redux-devtools-extension).
149 | Install the Chrome or Firefox extension and it should just work.
150 |
151 | ## Pre-fetching Data for Server Side Rendering (SSR)
152 |
153 | When rendering components on the server, you'll find that you may need to fetch
154 | some data before it can be rendered. The [component renderer](server/renderer/handler.js)
155 | looks for a `fetchData` method on the container component and its child
156 | components, then executes all of them and only renders after the promises have
157 | all been resolved.
158 |
159 | ```
160 | // As an ES6 class
161 |
162 | class TodosContainer extends React.Component {
163 | static fetchData = ({ store }) => {
164 | return store.dispatch(fetchTodos());
165 | };
166 | }
167 |
168 | // As a functional stateless component
169 |
170 | const TodosContainer = (props) => {
171 | const { todos } = props;
172 | return (
173 | // ...component code
174 | );
175 | }
176 |
177 | TodosContainer.fetchData = ({ store }) => {
178 | return store.dispatch(fetchTodos());
179 | }
180 | ```
181 |
182 | ## Async / Await
183 |
184 | This project uses `async/await`, available by default in Node.js v8.x.x or
185 | higher. If you experience errors, please upgrade your version of Node.js.
186 |
187 | ## Testing
188 |
189 | The default testing framework is Jest, though you can use whatever you want.
190 |
191 | Tests and their corresponding files such as Jest snapshots, should be co-located
192 | alongside the modules they are testing, in a `spec/` folder. For example:
193 |
194 | ```
195 | ├── components
196 | │ ├── todos
197 | │ │ ├── TodoForm
198 | │ │ │ ├── spec
199 | │ │ │ │ ├── TodoForm.test.js
200 | │ │ │ ├── index.js
201 | │ │ │ ├── index.scss
202 | ```
203 |
204 | Tests can be written with ES2015, since it passes through `babel-register`.
205 |
206 | ## Running Tests
207 |
208 | To run a single test:
209 |
210 | ```
211 | npm test /path/to/single.test.js
212 |
213 | // Or, to watch for changes
214 | npm run test:watch /path/to/single.test.js
215 | ```
216 |
217 | To run all tests:
218 |
219 | ```
220 | npm run test:all
221 |
222 | // Or, to watch for changes
223 | npm run test:all:watch
224 | ```
225 |
226 | ## Running ESLint
227 |
228 | ```
229 | npm run lint
230 | ```
231 |
232 | Check the `.eslintignore` file for directories excluded from linting.
233 |
234 | ## Changing the public asset path
235 |
236 | By default, assets are built into `dist/public`. This path is then served by
237 | express under the path `assets`. This is the public asset path. In a production
238 | scenario, you may want your assets to be hosted on a CDN. To do so, just change
239 | the `PUBLIC_ASSET_PATH` environment variant.
240 |
241 | Example using Heroku, if serving via CDN:
242 |
243 | ```
244 | heroku config:set PUBLIC_ASSET_PATH=https://my.cdn.com
245 | ```
246 |
247 | Example using Heroku, if serving locally:
248 |
249 | ```
250 | heroku config:set PUBLIC_ASSET_PATH=/assets
251 | ```
252 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import './styles';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { Provider } from 'react-redux';
5 | import { ConnectedRouter } from 'react-router-redux';
6 | import createHistory from 'history/createBrowserHistory';
7 | import configureStore from '@store';
8 | import App from '@containers/App';
9 | import Loadable from 'react-loadable';
10 |
11 | // Hydrate the redux store from server state.
12 | const initialState = window.__INITIAL_STATE__;
13 | const history = createHistory();
14 | const store = configureStore(initialState, history);
15 |
16 | // Render the application
17 | window.main = () => {
18 | Loadable.preloadReady().then(() => {
19 | ReactDOM.hydrate(
20 |
21 |
22 |
23 |
24 | ,
25 | document.getElementById('app')
26 | );
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/client/styles.js:
--------------------------------------------------------------------------------
1 | /* Style Loader
2 | *
3 | * Anything imported in here will either be added to the vendor CSS chunk, or
4 | * the main app CSS chunk. Where they will go depends on its location or its
5 | * extension.
6 | *
7 | * Files will be added to the vendor.css chunk if:
8 | * - they are located inside `node_modules`, or
9 | * - they are plain .css files.
10 | * Otherwise, files will be added to the main app.css chunk.
11 | */
12 |
13 | // Pre-built Semantic-UI css. If you want to customize this, you can build your
14 | // own distribution of it and include it here.
15 | // See https: *semantic-ui.com/introduction/build-tools.html
16 | import 'semantic-ui-css/semantic.min.css';
17 |
18 | // Include initial base styles.
19 | import '@css/base/index.scss';
20 |
--------------------------------------------------------------------------------
/common/css/base/_fonts.scss:
--------------------------------------------------------------------------------
1 | // Import fonts here, from google, or whereever.
2 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600');
3 |
--------------------------------------------------------------------------------
/common/css/base/_overrides.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/css/base/_overrides.scss
--------------------------------------------------------------------------------
/common/css/base/_styles.scss:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | font-family: 'Open Sans', sans-serif;
4 |
5 | p {
6 | font-size: 16px;
7 | line-height: 24px;
8 | }
9 | }
10 |
11 | body {
12 | background-color: $white;
13 | }
14 |
15 | #app {
16 | height: 100%;
17 | width: 100%;
18 | margin: 0 auto;
19 | }
20 |
--------------------------------------------------------------------------------
/common/css/base/index.scss:
--------------------------------------------------------------------------------
1 | // Resource files (.e.g. variables, mixins, etc)
2 | @import '../resources/variables';
3 | @import '../resources/mixins';
4 |
5 | // Add custom fonts, overrides, and base styles.
6 | @import 'fonts';
7 | @import 'overrides';
8 | @import 'styles';
9 |
--------------------------------------------------------------------------------
/common/css/resources/_colors.scss:
--------------------------------------------------------------------------------
1 | $white: #fff;
2 | $dark-gray: #333;
3 |
--------------------------------------------------------------------------------
/common/css/resources/_mixins.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/css/resources/_mixins.scss
--------------------------------------------------------------------------------
/common/css/resources/_variables.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/css/resources/_variables.scss
--------------------------------------------------------------------------------
/common/css/resources/_vendors.scss:
--------------------------------------------------------------------------------
1 | /* Import or define any sass mixins, functions, and vendor mixins below */
2 |
3 | @import "~include-media/dist/include-media";
4 |
--------------------------------------------------------------------------------
/common/fonts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/fonts/.gitkeep
--------------------------------------------------------------------------------
/common/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/images/favicon.png
--------------------------------------------------------------------------------
/common/js/actions/todos.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_TODO,
3 | REMOVE_TODO,
4 | TOGGLE_TODO,
5 | FETCH_TODOS_REQUEST,
6 | FETCH_TODOS_SUCCESS,
7 | FETCH_TODOS_FAILURE
8 | } from '@constants/index';
9 | import api from '@lib/api';
10 | import generateActionCreator from '@lib/generateActionCreator';
11 |
12 | export const addTodo = generateActionCreator(ADD_TODO, 'text');
13 | export const removeTodo = generateActionCreator(REMOVE_TODO, 'id');
14 | export const toggleTodo = generateActionCreator(TOGGLE_TODO, 'id');
15 |
16 | export const fetchTodosRequest = generateActionCreator(FETCH_TODOS_REQUEST);
17 | export const fetchTodosSuccess = generateActionCreator(
18 | FETCH_TODOS_SUCCESS,
19 | 'todos'
20 | );
21 | export const fetchTodosFailure = generateActionCreator(
22 | FETCH_TODOS_FAILURE,
23 | 'error'
24 | );
25 |
26 | export const fetchTodos = () => {
27 | return dispatch => {
28 | dispatch(fetchTodosRequest());
29 |
30 | return api
31 | .get('/api/todos')
32 | .then(todos => {
33 | dispatch(fetchTodosSuccess(todos));
34 |
35 | return Promise.resolve(todos);
36 | })
37 | .catch(error => {
38 | dispatch(fetchTodosFailure(error));
39 |
40 | return Promise.reject(error);
41 | });
42 | };
43 | };
44 |
--------------------------------------------------------------------------------
/common/js/components/common/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class Footer extends Component {
4 | render() {
5 | return (
6 |
7 | );
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/common/js/components/common/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { Header, Menu } from 'semantic-ui-react';
4 |
5 | const menuItems = [
6 | { name: 'Home', to: '/', exact: true },
7 | { name: 'Todos', to: '/todos' }
8 | ];
9 |
10 | class HeaderView extends Component {
11 | render() {
12 | return (
13 |
14 |
15 | {menuItems.map(item => (
16 |
17 | {item.name}
18 |
19 | ))}
20 |
21 |
22 | );
23 | }
24 | }
25 |
26 | export default HeaderView;
27 |
--------------------------------------------------------------------------------
/common/js/components/common/Loading/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Loading = (props) => {
5 | const { error, timedOut, pastDelay } = props;
6 |
7 | if (error) {
8 | // When the loader has errored
9 | return (
10 | Error!
11 | );
12 | } else if (timedOut) {
13 | // When the loader has taken longer than the timeout
14 | return (
15 | Taking a long time...
16 | );
17 | } else if (pastDelay) {
18 | // When the loader has taken longer than the delay
19 | return (
20 | Loading...
21 | );
22 | }
23 |
24 | return null;
25 | };
26 |
27 | Loading.propTypes = {
28 | error: PropTypes.bool,
29 | timedOut: PropTypes.bool,
30 | pastDelay: PropTypes.bool
31 | };
32 |
33 | export default Loading;
34 |
--------------------------------------------------------------------------------
/common/js/components/common/RouteWithSubRoutes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route } from 'react-router';
3 |
4 | const RouteWithSubRoutes = props => {
5 | const {
6 | path,
7 | computedMatch,
8 | component: Component,
9 | routes,
10 | restProps
11 | } = props;
12 |
13 | return (
14 | {
17 | // pass the sub-routes down to keep nesting
18 | return (
19 |
25 | );
26 | }}
27 | />
28 | );
29 | };
30 |
31 | export default RouteWithSubRoutes;
32 |
--------------------------------------------------------------------------------
/common/js/components/common/index.js:
--------------------------------------------------------------------------------
1 | export { default as Footer } from './Footer';
2 | export { default as Header } from './Header';
3 | export { default as Loading } from './Loading';
4 | export { default as RouteWithSubRoutes } from './RouteWithSubRoutes';
5 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Form } from 'semantic-ui-react';
4 | import classnames from 'classnames/bind';
5 | import css from './index.scss';
6 |
7 | class TodoForm extends Component {
8 | static propTypes = {
9 | onSubmit: PropTypes.func,
10 | className: PropTypes.string
11 | };
12 |
13 | state = { todoText: '' };
14 |
15 | submitTodo = ev => {
16 | ev.preventDefault();
17 |
18 | const { onSubmit } = this.props;
19 | const { todoText } = this.state;
20 |
21 | if (todoText && todoText !== '' && typeof onSubmit === 'function') {
22 | onSubmit(todoText);
23 | this.setState({ todoText: '' });
24 | }
25 | };
26 |
27 | onTodoChange = ev => {
28 | this.setState({ todoText: ev.target.value });
29 | };
30 |
31 | render() {
32 | const { className } = this.props;
33 | const { todoText } = this.state;
34 |
35 | return (
36 |
41 |
47 |
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | export default TodoForm;
55 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoForm/index.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/js/components/todos/TodoForm/index.scss
--------------------------------------------------------------------------------
/common/js/components/todos/TodoForm/spec/TodoForm.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TodoForm from '../index';
3 | import { mount } from 'enzyme';
4 |
5 | describe('TodoForm', () => {
6 | it('renders correctly', () => {
7 | const component = mount( );
8 | expect(component).toMatchSnapshot();
9 | });
10 |
11 | describe('clicking on submit button', () => {
12 | test('calls the onSubmit prop', () => {
13 | const mockSubmit = jest.fn();
14 | const component = mount( );
15 |
16 | // test form submission
17 | component.setState({ todoText: 'Foobar' });
18 | component.find('form').simulate('submit');
19 |
20 | expect(mockSubmit.mock.calls.length).toEqual(1);
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoForm/spec/__snapshots__/TodoForm.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TodoForm renders correctly 1`] = `
4 |
5 |
100 |
101 |
102 | `;
103 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoItem/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { List, Checkbox, Button } from 'semantic-ui-react';
4 | import classnames from 'classnames/bind';
5 | import css from './index.scss';
6 |
7 | const cx = classnames.bind(css);
8 |
9 | const TodoItem = props => {
10 | const { onRemove, onChange, todo: { id, completed, text } } = props;
11 |
12 | return (
13 |
14 |
15 | onRemove(id)} icon="remove" size="mini" />
16 |
17 |
18 | onChange(id)}
22 | />
23 |
24 |
25 | {text}
26 |
27 |
28 | );
29 | };
30 |
31 | TodoItem.propTypes = {
32 | todo: PropTypes.object.isRequired,
33 | onRemove: PropTypes.func,
34 | onChange: PropTypes.func
35 | };
36 |
37 | TodoItem.defaultProps = {
38 | onRemove: () => {},
39 | onChange: () => {}
40 | };
41 |
42 | export default TodoItem;
43 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoItem/index.scss:
--------------------------------------------------------------------------------
1 | .todo {
2 | &.extra {
3 | padding: 10px;
4 | }
5 |
6 | .completeInput {}
7 |
8 | .text {
9 | &.completed {
10 | text-decoration: line-through;
11 | color: #ccc;
12 | }
13 | }
14 |
15 | .delete {
16 | cursor: pointer;
17 | color: blue;
18 | text-decoration: underline
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoItem/spec/TodoItem.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TodoItem from '../index';
3 | import { shallow } from 'enzyme';
4 |
5 | describe('TodoItem', () => {
6 | it('renders correctly', () => {
7 | const component = shallow( );
8 | expect(component).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoItem/spec/__snapshots__/TodoItem.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TodoItem renders correctly 1`] = `
4 |
7 |
10 |
16 |
17 |
20 |
24 |
25 |
28 | Work
29 |
30 |
31 | `;
32 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoList/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { List } from 'semantic-ui-react';
4 | import { TodoItem } from '@components/todos';
5 | import classnames from 'classnames';
6 | import css from './index.scss';
7 |
8 | const TodoList = props => {
9 | const { className, onChange, onRemove, todos: { todos } } = props;
10 |
11 | return (
12 |
13 | {todos.map((todo, idx) => (
14 |
20 | ))}
21 |
22 | );
23 | };
24 |
25 | TodoList.propTypes = {
26 | todos: PropTypes.object.isRequired,
27 | className: PropTypes.string,
28 | onChange: PropTypes.func,
29 | onRemove: PropTypes.func
30 | };
31 |
32 | export default TodoList;
33 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoList/index.scss:
--------------------------------------------------------------------------------
1 | .todos {
2 | width: 300px;
3 | margin: 0;
4 | padding: 0;
5 | list-style: none;
6 | }
7 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoList/spec/TodoList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TodoList from '../index';
3 | import { shallow } from 'enzyme';
4 |
5 | describe('TodoList', () => {
6 | let todos = {
7 | isFetched: true,
8 | todos: [
9 | {
10 | id: 1,
11 | text: 'Learn React',
12 | completed: true
13 | },
14 | {
15 | id: 2,
16 | text: 'Learn Redux',
17 | completed: true
18 | }
19 | ]
20 | };
21 |
22 |
23 | it('renders correctly', () => {
24 | const component = shallow( );
25 | expect(component).toMatchSnapshot();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/common/js/components/todos/TodoList/spec/__snapshots__/TodoList.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TodoList renders correctly 1`] = `
4 |
8 |
20 |
32 |
33 | `;
34 |
--------------------------------------------------------------------------------
/common/js/components/todos/index.js:
--------------------------------------------------------------------------------
1 | export { default as TodoForm } from './TodoForm';
2 | export { default as TodoItem } from './TodoItem';
3 | export { default as TodoList } from './TodoList';
4 |
--------------------------------------------------------------------------------
/common/js/constants/index.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO';
2 | export const REMOVE_TODO = 'REMOVE_TODO';
3 | export const TOGGLE_TODO = 'TOGGLE_TODO';
4 |
5 | export const FETCH_TODOS_REQUEST = 'FETCH_TODOS_REQUEST';
6 | export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
7 | export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE';
8 |
--------------------------------------------------------------------------------
/common/js/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch } from 'react-router-dom';
3 | import { Container } from 'semantic-ui-react';
4 | import { Header, Footer, RouteWithSubRoutes } from '@components/common';
5 | import { hot } from 'react-hot-loader';
6 | import routes from '@routes';
7 |
8 | const App = () => (
9 |
10 |
11 |
12 | {routes.map(route => (
13 |
14 | ))}
15 |
16 |
17 |
18 | );
19 |
20 | export default hot(module)(App);
21 |
--------------------------------------------------------------------------------
/common/js/containers/Todos/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { Helmet } from 'react-helmet';
5 | import { fetchTodos, addTodo, toggleTodo, removeTodo } from '@actions/todos';
6 | import { Container } from 'semantic-ui-react';
7 | import { TodoList, TodoForm } from '@components/todos';
8 |
9 | class TodosContainer extends Component {
10 | static propTypes = {
11 | todos: PropTypes.object.isRequired,
12 | dispatch: PropTypes.func.isRequired
13 | };
14 |
15 | componentDidMount() {
16 | const { dispatch, todos: { isFetched } } = this.props;
17 |
18 | if (!isFetched) {
19 | dispatch(fetchTodos());
20 | }
21 | }
22 |
23 | submitTodo = text => {
24 | const { dispatch } = this.props;
25 |
26 | if (text) {
27 | dispatch(addTodo(text));
28 | }
29 | };
30 |
31 | checkTodo = id => {
32 | const { dispatch } = this.props;
33 |
34 | dispatch(toggleTodo(id));
35 | };
36 |
37 | removeTodo = id => {
38 | const { dispatch } = this.props;
39 |
40 | dispatch(removeTodo(id));
41 | };
42 |
43 | render() {
44 | const { todos } = this.props;
45 | const title = 'Todo List';
46 |
47 | return (
48 |
49 |
50 | {title}
51 |
52 | {title}
53 |
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | const mapStateToProps = state => ({
65 | todos: state.todos
66 | });
67 |
68 | export default connect(mapStateToProps)(TodosContainer);
69 |
--------------------------------------------------------------------------------
/common/js/containers/Todos/index.scss:
--------------------------------------------------------------------------------
1 | .todoForm {
2 | width: 300px;
3 | margin: 10px 0 0;
4 | display: flex;
5 |
6 | input, button {
7 | padding: 5px 3px;
8 | }
9 |
10 | input { flex: 1; }
11 | }
12 |
--------------------------------------------------------------------------------
/common/js/lib/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const headers = { 'Content-Type': 'application/json' };
4 | const baseURL = process.env.APPLICATION_BASE_URL || '';
5 | const api = axios.create({ baseURL, headers, timeout: 200000 });
6 |
7 | api.interceptors.response.use(
8 | (response) => Promise.resolve(response.data) ,
9 | (err) => Promise.reject(err.response.data)
10 | );
11 |
12 | module.exports = api;
13 |
--------------------------------------------------------------------------------
/common/js/lib/generateActionCreator.js:
--------------------------------------------------------------------------------
1 | /* Generates an action creator.
2 | * @param type [String] The action type as a constant, e.g. ADD_TODO
3 | * @param [...propNames] [String] The string name of each action property as arguments
4 | * @return [Object] The action creator.
5 | * @example
6 | */
7 |
8 | export default function generateActionCreator(type, ...propNames) {
9 | return function actionCreator(...args) {
10 | const actionProps = {};
11 |
12 | propNames.forEach((prop, idx) => {
13 | actionProps[prop] = args[idx];
14 | });
15 |
16 | return { type, ...actionProps };
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/common/js/middleware/.gitkeep:
--------------------------------------------------------------------------------
1 | // Add custom middleware here.
2 |
--------------------------------------------------------------------------------
/common/js/pages/Error/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const ERROR_MESSAGES = {
5 | 404: 'The page you requested was not found.',
6 | 500: 'The server encountered an error.'
7 | };
8 |
9 | const ErrorPage = (props) => {
10 | const { message } = props;
11 |
12 | return (
13 |
14 |
Sorry!
15 |
{message}
16 |
17 | );
18 | };
19 |
20 | ErrorPage.propTypes = {
21 | code: PropTypes.number,
22 | message: PropTypes.string
23 | };
24 |
25 | ErrorPage.defaultProps = {
26 | code: 404,
27 | message: ERROR_MESSAGES[404]
28 | };
29 |
30 | export default ErrorPage;
31 |
--------------------------------------------------------------------------------
/common/js/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Helmet } from 'react-helmet';
4 | import css from './index.scss';
5 |
6 | class HomePage extends Component {
7 | render() {
8 | return (
9 |
10 |
11 | Home
12 |
13 |
It Works!
14 |
15 | You've successfully started up your first universally rendered react
16 | and redux app.
17 | Hint: Try View Source on this page to see that it was rendered on the
18 | server as well.
19 |
20 |
21 | Check out the todos list.
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default HomePage;
29 |
--------------------------------------------------------------------------------
/common/js/pages/Home/index.scss:
--------------------------------------------------------------------------------
1 | .home {
2 | h1 {
3 | font-size: 30px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/common/js/pages/Todos/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { fetchTodos } from '@actions/todos';
3 | import TodosContainer from '@containers/Todos';
4 | // NOTE: To turn on dynamic imports, uncomment this
5 | // import Loadable from 'react-loadable';
6 | // import { Loading } from '@components/common';
7 | // const TodosContainer = Loadable({
8 | // loader: () => import('../../containers/Todos'),
9 | // loading: Loading
10 | // });
11 |
12 | class TodosPage extends Component {
13 | static fetchData = ({ store }) => {
14 | return store.dispatch(fetchTodos());
15 | };
16 |
17 | render() {
18 | return ;
19 | }
20 | }
21 |
22 | export default TodosPage;
23 |
--------------------------------------------------------------------------------
/common/js/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerReducer } from 'react-router-redux';
3 |
4 | // Import your reducers here
5 | import todos from './todos';
6 |
7 | const rootReducer = combineReducers({
8 | routing: routerReducer,
9 | todos
10 | });
11 |
12 | export default rootReducer;
13 |
--------------------------------------------------------------------------------
/common/js/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_TODO, REMOVE_TODO, TOGGLE_TODO,
3 | FETCH_TODOS_REQUEST, FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE
4 | } from '@constants/index';
5 |
6 | const defaultState = {
7 | todos: [],
8 | isFetching: false,
9 | isFetched: false,
10 | error: null
11 | };
12 |
13 | const todos = (state = defaultState, action) => {
14 | switch (action.type) {
15 | case ADD_TODO:
16 | return {
17 | ...state,
18 | todos: [
19 | ...state.todos, { id: Date.now(), text: action.text, completed: false }
20 | ]
21 | };
22 |
23 | case REMOVE_TODO:
24 | return {
25 | ...state,
26 | todos: state.todos.filter(todo => todo.id !== action.id)
27 | };
28 |
29 | case TOGGLE_TODO:
30 | return {
31 | ...state,
32 | todos: state.todos.map(todo => {
33 | if (todo.id === action.id) {
34 | return { ...todo, completed: !todo.completed };
35 | }
36 | return todo;
37 | })
38 | };
39 |
40 | case FETCH_TODOS_REQUEST:
41 | return { ...state, isFetching: true, isFetched: false };
42 |
43 | case FETCH_TODOS_SUCCESS:
44 | return { ...state, todos: action.todos, isFetching: false, isFetched: true };
45 |
46 | case FETCH_TODOS_FAILURE:
47 | return { ...state, isFetching: false, isFetched: false, error: action.error };
48 |
49 | default:
50 | return state;
51 | }
52 | };
53 |
54 | export default todos;
55 |
--------------------------------------------------------------------------------
/common/js/routes/index.js:
--------------------------------------------------------------------------------
1 | import Error from '@pages/Error';
2 | import Home from '@pages/Home';
3 | import Todos from '@pages/Todos';
4 |
5 | export default [
6 | { path: '/', exact: true, component: Home },
7 | { path: '/todos', exact: true, component: Todos },
8 | { path: '*', component: Error }
9 | ];
10 |
--------------------------------------------------------------------------------
/common/js/store/index.dev.js:
--------------------------------------------------------------------------------
1 | import { compose, createStore, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from '@reducers';
4 | import { createLogger } from 'redux-logger';
5 | import { routerMiddleware } from 'react-router-redux';
6 |
7 | export default function configureStore(initialState, history = null) {
8 | /* Middleware
9 | * Configure this array with the middleware that you want included
10 | */
11 | let middleware = [
12 | thunk,
13 | createLogger()
14 | ];
15 |
16 | if (history) {
17 | middleware.push(routerMiddleware(history));
18 | }
19 |
20 | // Add universal enhancers here
21 | let enhancers = [];
22 |
23 | const composeEnhancers = (
24 | typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
25 | ) || compose;
26 | const enhancer = composeEnhancers(...[
27 | applyMiddleware(...middleware),
28 | ...enhancers
29 | ]);
30 |
31 | // create store with enhancers, middleware, reducers, and initialState
32 | const store = createStore(rootReducer, initialState, enhancer);
33 |
34 | if (module.hot) {
35 | // Enable Webpack hot module replacement for reducers
36 | module.hot.accept('../reducers', () => {
37 | const nextRootReducer = require('../reducers').default;
38 | store.replaceReducer(nextRootReducer);
39 | });
40 | }
41 |
42 | return store;
43 | }
44 |
--------------------------------------------------------------------------------
/common/js/store/index.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./index.prod');
3 | } else {
4 | module.exports = require('./index.dev');
5 | }
6 |
--------------------------------------------------------------------------------
/common/js/store/index.prod.js:
--------------------------------------------------------------------------------
1 | import { compose, createStore, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from '@reducers';
4 | import { routerMiddleware } from 'react-router-redux';
5 |
6 | export default function configureStore(initialState, history = null) {
7 | /* Middleware
8 | * Configure this array with the middleware that you want included.
9 | */
10 | let middleware = [ thunk ];
11 |
12 | if (history) {
13 | middleware.push(routerMiddleware(history));
14 | }
15 |
16 | // Add universal enhancers here
17 | let enhancers = [];
18 |
19 | const enhancer = compose(...[
20 | applyMiddleware(...middleware),
21 | ...enhancers
22 | ]);
23 |
24 | // create store with enhancers, middleware, reducers, and initialState
25 | const store = createStore(rootReducer, initialState, enhancer);
26 |
27 | return store;
28 | }
29 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | // Enable or disable server-side rendering
4 | enableSSR: true,
5 |
6 | // Enable or disable dynamic imports (code splitting)
7 | enableDynamicImports: true,
8 |
9 | // The env vars to expose on the client side. If you add them here, they will
10 | // be available on the client as process.env[VAR_NAME], same as they would be
11 | // in node.js.
12 | //
13 | // **WARNING**: Be careful not to expose any secrets here!
14 | clientEnv: [
15 | 'NODE_ENV',
16 | 'APPLICATION_BASE_URL'
17 | ],
18 |
19 | /* The identifier to use for css-modules.
20 | */
21 | cssModulesIdentifier: '[name]__[local]__[hash:base64:5]',
22 |
23 | // Isomorphic configuration
24 | isomorphicConfig: {
25 | assets: {
26 | images: {
27 | extensions: [
28 | 'png',
29 | 'jpg',
30 | 'jpeg',
31 | 'gif',
32 | 'ico',
33 | 'svg'
34 | ]
35 | }
36 | }
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "universal-react-redux",
3 | "description": "Universal React/Redux boilerplate with sensible defaults.",
4 | "version": "7.0.0",
5 | "license": "MIT",
6 | "main": "client/index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/combine/universal-react-redux.git"
10 | },
11 | "engines": {
12 | "node": "8.x.x",
13 | "npm": "5.2.x"
14 | },
15 | "_moduleAliases": {
16 | "@css": "common/css",
17 | "@fonts": "common/fonts",
18 | "@images": "common/images",
19 | "@actions": "common/js/actions",
20 | "@components": "common/js/components",
21 | "@constants": "common/js/constants",
22 | "@containers": "common/js/containers",
23 | "@lib": "common/js/lib",
24 | "@middleware": "common/js/middleware",
25 | "@pages": "common/js/pages",
26 | "@reducers": "common/js/reducers",
27 | "@routes": "common/js/routes",
28 | "@store": "common/js/store",
29 | "@templates": "server/templates",
30 | "@config": "config",
31 | "$api": "server/api",
32 | "$lib": "server/lib",
33 | "$middleware": "server/middleware"
34 | },
35 | "scripts": {
36 | "start": "better-npm-run dev:start",
37 | "prod": "better-npm-run prod:build && better-npm-run serve",
38 | "serve": "better-npm-run serve",
39 | "dev:start": "better-npm-run dev:start",
40 | "dev:start:server": "better-npm-run dev:start:server",
41 | "dev:start:client": "better-npm-run dev:start:client",
42 | "prod:build": "better-npm-run prod:build",
43 | "prod:build:client": "better-npm-run prod:build:client",
44 | "prod:build:ssr": "better-npm-run prod:build:ssr",
45 | "prod:build:server": "better-npm-run prod:build:server",
46 | "test": "better-npm-run test",
47 | "test:all": "better-npm-run test:all",
48 | "test:watch": "better-npm-run test:watch",
49 | "test:all:watch": "better-npm-run test:all:watch",
50 | "lint": "better-npm-run lint",
51 | "heroku-postbuild": "better-npm-run prod:build"
52 | },
53 | "betterScripts": {
54 | "serve": {
55 | "command": "node ./dist/index.js",
56 | "env": {
57 | "NODE_ENV": "production"
58 | }
59 | },
60 | "dev:start": "npm run dev:start:client & npm run dev:start:server",
61 | "dev:start:server": {
62 | "command": "$(npm bin)/node-dev server",
63 | "env": {
64 | "NODE_ENV": "development"
65 | }
66 | },
67 | "dev:start:client": {
68 | "command": "npx babel-node webpack/development.client.babel",
69 | "env": {
70 | "NODE_ENV": "development"
71 | }
72 | },
73 | "prod:build": {
74 | "command": "npm run prod:build:client && npm run prod:build:ssr && npm run prod:build:server",
75 | "env": {
76 | "NODE_ENV": "production"
77 | }
78 | },
79 | "prod:build:client": {
80 | "command": "$(npm bin)/webpack --config webpack/production.client.babel.js --progress",
81 | "env": {
82 | "NODE_ENV": "production",
83 | "PUBLIC_ASSET_PATH": "/assets/"
84 | }
85 | },
86 | "prod:build:ssr": {
87 | "command": "$(npm bin)/webpack --config webpack/production.ssr.babel.js --progress",
88 | "env": {
89 | "NODE_ENV": "production",
90 | "SSR": true
91 | }
92 | },
93 | "prod:build:server": {
94 | "command": "npx babel ./server -d ./dist --ignore '**/*.test.js,renderer/handler.js'",
95 | "env": {
96 | "NODE_ENV": "production"
97 | }
98 | },
99 | "test": {
100 | "command": "$(npm bin)/jest -c ./test/support/jest.config.js",
101 | "env": {
102 | "NODE_ENV": "test"
103 | }
104 | },
105 | "test:watch": {
106 | "command": "npm run test -- --watch",
107 | "env": {
108 | "NODE_ENV": "test"
109 | }
110 | },
111 | "lint": {
112 | "command": "$(npm bin)/eslint --ext .js,.jsx .",
113 | "env": {
114 | "NODE_ENV": "test"
115 | }
116 | }
117 | },
118 | "devDependencies": {
119 | "autoprefixer": "^8.2.0",
120 | "babel-cli": "^6.26.0",
121 | "babel-core": "^6.26.0",
122 | "babel-eslint": "^8.2.2",
123 | "babel-loader": "^7.1.4",
124 | "babel-plugin-dynamic-import-node": "^1.2.0",
125 | "babel-plugin-lodash": "^3.3.2",
126 | "babel-plugin-react-transform": "^3.0.0",
127 | "babel-plugin-syntax-class-properties": "^6.13.0",
128 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
129 | "babel-plugin-syntax-object-rest-spread": "^6.13.0",
130 | "babel-plugin-transform-class-properties": "^6.24.1",
131 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
132 | "babel-plugin-transform-export-extensions": "^6.22.0",
133 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
134 | "babel-plugin-transform-react-remove-prop-types": "^0.4.13",
135 | "babel-polyfill": "^6.26.0",
136 | "babel-preset-env": "^1.6.1",
137 | "babel-preset-react": "^6.24.1",
138 | "better-npm-run": "^0.1.0",
139 | "compression-webpack-plugin": "^1.1.11",
140 | "css-hot-loader": "^1.3.9",
141 | "css-loader": "^0.28.11",
142 | "debug": "^3.1.0",
143 | "dotenv": "^5.0.1",
144 | "dotenv-safe": "^5.0.1",
145 | "dotenv-webpack": "^1.5.5",
146 | "enzyme": "^3.3.0",
147 | "enzyme-adapter-react-16": "^1.1.1",
148 | "enzyme-to-json": "^3.3.3",
149 | "eslint": "^4.19.1",
150 | "eslint-loader": "^2.0.0",
151 | "eslint-plugin-babel": "^5.0.0",
152 | "eslint-plugin-react": "^7.7.0",
153 | "expose-loader": "^0.7.5",
154 | "extract-text-webpack-plugin": "^4.0.0-alpha.0",
155 | "file-loader": "^1.1.11",
156 | "identity-obj-proxy": "^3.0.0",
157 | "jest": "^22.4.3",
158 | "mini-css-extract-plugin": "^0.4.0",
159 | "node-dev": "^3.1.3",
160 | "node-sass": "^4.8.3",
161 | "postcss-csso": "^3.0.0",
162 | "postcss-loader": "^2.1.3",
163 | "prettier-eslint": "^8.8.1",
164 | "react-hot-loader": "^4.0.1",
165 | "react-transform-catch-errors": "^1.0.2",
166 | "redbox-react": "^1.5.0",
167 | "redux-logger": "^3.0.6",
168 | "resolve-url-loader": "^2.3.0",
169 | "sass-loader": "^6.0.7",
170 | "sass-resources-loader": "^1.3.3",
171 | "style-loader": "^0.20.3",
172 | "supertest": "^3.0.0",
173 | "url-loader": "^1.0.1",
174 | "webpack": "^4.5.0",
175 | "webpack-bundle-analyzer": "^2.11.1",
176 | "webpack-cli": "^2.0.14",
177 | "webpack-dev-server": "^3.1.3",
178 | "webpack-isomorphic-tools": "^3.0.5",
179 | "webpack-merge": "^4.1.2",
180 | "webpack-node-externals": "^1.7.2",
181 | "webpack-sources": "^1.1.0",
182 | "yn": "^2.0.0"
183 | },
184 | "dependencies": {
185 | "axios": "^0.18.0",
186 | "body-parser": "^1.18.2",
187 | "chokidar": "^2.0.3",
188 | "classnames": "^2.2.5",
189 | "compression": "^1.7.2",
190 | "cookie-parser": "^1.4.3",
191 | "css-modules-require-hook": "^4.2.3",
192 | "express": "^4.16.3",
193 | "font-awesome": "^4.7.0",
194 | "helmet": "^3.12.0",
195 | "history": "^4.7.2",
196 | "include-media": "^1.4.9",
197 | "lodash": "^4.17.5",
198 | "module-alias": "^2.0.6",
199 | "react": "^16.3.1",
200 | "react-dom": "^16.3.1",
201 | "react-helmet": "^5.2.0",
202 | "react-loadable": "^5.3.1",
203 | "react-redux": "^5.0.7",
204 | "react-responsive-redux": "^0.5.0",
205 | "react-router-dom": "^4.2.2",
206 | "react-router-redux": "^5.0.0-alpha.9",
207 | "redux": "^3.7.2",
208 | "redux-thunk": "^2.2.0",
209 | "semantic-ui-css": "^2.3.1",
210 | "semantic-ui-react": "^0.79.1",
211 | "serve-static": "^1.13.2"
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const autoprefixer = require('autoprefixer');
2 | const csso = require('postcss-csso')({ restructure: true, comments: false });
3 |
4 | const pluginsList = [autoprefixer];
5 | if (process.env.NODE_ENV === 'production') {
6 | pluginsList.push(csso);
7 | }
8 | module.exports = {
9 | plugins: pluginsList
10 | };
--------------------------------------------------------------------------------
/postinstall.js:
--------------------------------------------------------------------------------
1 | const exec = require('child_process').exec;
2 |
3 | // For cross-platform postinstalls, use a node script to execute any
4 | // post-install tasks rather than using a bash script since that wouldn't work
5 | // on Windows.
6 | if (process.env.NODE_ENV === 'production') {
7 | exec('npm run prod:build', (error, stdout, stderr) => {
8 | if (error) {
9 | console.error(`exec error: ${error}`);
10 | return;
11 | }
12 | console.log(`stdout: ${stdout}`);
13 | console.log(`stderr: ${stderr}`);
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/server/.node-dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "notify": false,
3 | "extensions": {
4 | "js": {
5 | "name": "babel-core/register",
6 | "options": {
7 | "presets": [
8 | "react"
9 | ],
10 | "plugins": [
11 | "transform-class-properties",
12 | "transform-object-rest-spread",
13 | "dynamic-import-node"
14 | ]
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/server/api/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import bodyParser from 'body-parser';
3 | import todos from './todos';
4 |
5 | const Api = express();
6 |
7 | // always send JSON headers
8 | Api.use((req, res, next) => {
9 | res.contentType('application/json');
10 | next();
11 | });
12 |
13 | // parse JSON body
14 | Api.use(bodyParser.json());
15 |
16 | // Add all API endpoints here
17 | Api.use('/todos', todos);
18 |
19 | export default Api;
20 |
--------------------------------------------------------------------------------
/server/api/todos/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import controller from './todos.controller';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/', controller.index);
7 | router.get('/:id', controller.show);
8 |
9 | export default router;
10 |
--------------------------------------------------------------------------------
/server/api/todos/spec/todos.controller.test.js:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import express from 'express';
3 | import todos from '../index';
4 |
5 | let app, agent;
6 |
7 | beforeAll(async () => {
8 | app = express();
9 | app.use('/api/todos', todos);
10 | agent = request.agent(app.listen());
11 | });
12 |
13 | describe('GET /api/todos', function() {
14 | test('endpoint returns a list of todos', function() {
15 | return agent
16 | .get('/api/todos')
17 | .expect(200)
18 | .then(({ body }) => {
19 | expect(body.map(t => t.id)).toEqual([1, 2, 3, 4, 5, 6]);
20 | });
21 | });
22 | });
23 |
24 | describe('GET /api/todos/:id', function() {
25 | test('endpoint returns a specific todos', function() {
26 | return agent
27 | .get('/api/todos/1')
28 | .expect(200)
29 | .then(({ body }) => {
30 | expect(body.id).toEqual(1);
31 | });
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/server/api/todos/todos.controller.js:
--------------------------------------------------------------------------------
1 | import { find } from 'lodash';
2 |
3 | // Exported controller methods
4 | export default {
5 | index,
6 | show
7 | };
8 |
9 | // This is an example to mock an async fetch from a database or service.
10 | const getTodos = async () => {
11 | return new Promise((resolve) => {
12 | setTimeout(() => {
13 | resolve(require('./todos.fixture.js'));
14 | });
15 | });
16 | };
17 |
18 | export function show(req, res) {
19 | const id = parseInt(req.params.id);
20 | const todos = require('./todos.fixture.js');
21 | const todo = find(todos, { id });
22 |
23 | return res.json(todo);
24 | }
25 |
26 | export async function index(req, res) {
27 | const todos = await getTodos();
28 | return res.json(todos);
29 | }
30 |
--------------------------------------------------------------------------------
/server/api/todos/todos.fixture.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | id: 1,
4 | text: 'Learn React',
5 | completed: true
6 | },
7 | {
8 | id: 2,
9 | text: 'Learn Redux',
10 | completed: true
11 | },
12 | {
13 | id: 3,
14 | text: 'Start an app',
15 | completed: true
16 | },
17 | {
18 | id: 4,
19 | text: 'Make it universally rendered',
20 | completed: true
21 | },
22 | {
23 | id: 5,
24 | text: 'Enable code splitting',
25 | completed: true
26 | },
27 | {
28 | id: 6,
29 | text: 'Build a kick ass app',
30 | completed: false
31 | }
32 | ];
33 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import './registerAliases';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import config from '@config';
5 | import Loadable from 'react-loadable';
6 | import chokidar from 'chokidar';
7 |
8 | const env = process.env.NODE_ENV || 'development';
9 |
10 | // HTML files are read as pure strings
11 | require.extensions['.html'] = (module, filename) => {
12 | module.exports = fs.readFileSync(filename, 'utf8');
13 | };
14 |
15 | if (env === 'development') {
16 | require('dotenv').load();
17 |
18 | // In development, we compile css-modules on the fly on the server. This is
19 | // not necessary in production since we build renderer and server files with
20 | // webpack and babel.
21 | require('css-modules-require-hook')({
22 | extensions: ['.scss'],
23 | generateScopedName: config.cssModulesIdentifier,
24 | devMode: true
25 | });
26 |
27 | // Add better stack tracing for promises in dev mode
28 | process.on('unhandledRejection', r => console.log(r));
29 | }
30 |
31 | const configureIsomorphicTools = function(server) {
32 | // configure isomorphic tools
33 | // this must be equal to the Webpack configuration's "context" parameter
34 | const basePath = path.resolve(__dirname, '..');
35 | const ISOTools = require('webpack-isomorphic-tools');
36 |
37 | // this global variable will be used later in express middleware
38 | global.ISOTools = new ISOTools(config.isomorphicConfig).server(
39 | basePath,
40 | () => server
41 | );
42 | };
43 |
44 | const startServer = () => {
45 | const server = require('./server');
46 | const port = process.env.PORT || process.env.APPLICATION_PORT || 3000;
47 |
48 | if (!global.ISOTools) {
49 | configureIsomorphicTools(server);
50 | }
51 |
52 | return Loadable.preloadAll().then(() => {
53 | return server.listen(port, error => {
54 | if (error) {
55 | console.error(error);
56 | } else {
57 | console.info(`Application server mounted on http://localhost:${port}.`);
58 | }
59 | });
60 | });
61 | };
62 |
63 | // This check is required to ensure we're not in a test environment.
64 | if (!module.parent) {
65 | let server;
66 |
67 | if (!config.enableDynamicImports) {
68 | startServer();
69 | } else {
70 | // Ensure react-loadable stats file exists before starting the server
71 | const statsPath = path.join(__dirname, '..', 'react-loadable.json');
72 | const watcher = chokidar.watch(statsPath, { persistent: true });
73 |
74 | console.info(`Checking/waiting for ${statsPath}...`);
75 |
76 | watcher.on('all', (event, path) => {
77 | if (event === 'add') {
78 | if (env === 'production') {
79 | watcher.close();
80 | }
81 |
82 | console.info(`Stats file found at ${path}, starting server...`);
83 |
84 | startServer().then(s => server = s);
85 |
86 | } else if (event === 'change') {
87 | if (env === 'production') {
88 | watcher.close();
89 | }
90 |
91 | console.info('Stats file changed, restarting server...');
92 |
93 | if (server) {
94 | // if the server is already started, restart it.
95 | server.close(() => {
96 | startServer().then(s => server = s);
97 | });
98 | } else {
99 | // otherwise, just start the server.
100 | startServer().then(s => server = s);
101 | }
102 | }
103 | });
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/server/lib/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/server/lib/.gitkeep
--------------------------------------------------------------------------------
/server/middleware/httpsRedirect.js:
--------------------------------------------------------------------------------
1 | import url from 'url';
2 |
3 | export default function ({ enabled = false }) {
4 | return function(req, res, next) {
5 | if (enabled && !req.secure) {
6 | const secureUrl = url.resolve(`https://${req.headers.host}`, req.url);
7 | return res.redirect(secureUrl);
8 | }
9 |
10 | return next();
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/server/middleware/index.js:
--------------------------------------------------------------------------------
1 | export { default as httpsRedirect } from './httpsRedirect';
2 |
--------------------------------------------------------------------------------
/server/registerAliases.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import moduleAlias from 'module-alias';
3 | import { _moduleAliases } from '../package.json';
4 | import { mapValues } from 'lodash';
5 |
6 | // Add module aliases, but for server aliases (prefix by the `$` symbol), change
7 | // the directory to match the current build directory (e.g. /dist)
8 | // This ensures that all alises are properly referencing the babel-built
9 | // server files
10 | moduleAlias.addAliases(mapValues(_moduleAliases, (aliasPath, aliasName) => {
11 | const sym = aliasName[0];
12 |
13 | if (sym === '$') {
14 | return path.join(__dirname, aliasPath.split('/').slice(1).join('/'));
15 | }
16 |
17 | return path.join(__dirname, '..', aliasPath);
18 | }));
19 |
--------------------------------------------------------------------------------
/server/renderer/handler.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { StaticRouter, matchPath } from 'react-router';
4 | import { setMobileDetect, mobileParser } from 'react-responsive-redux';
5 | import { renderToString } from 'react-dom/server';
6 | import { getBundles } from 'react-loadable/webpack';
7 | import Loadable from 'react-loadable';
8 | import render from './render';
9 | import routes from '@routes';
10 | import configureStore from '@store';
11 | import App from '@containers/App';
12 | import config from '@config';
13 |
14 | let stats = null;
15 |
16 | // This is a small 'hack' to tell webpack to avoid resolving the below file
17 | // during compilation, since react-loadable.json may or may not exist.
18 | const requireFunc = typeof __webpack_require__ === 'function'
19 | ? __non_webpack_require__
20 | : require;
21 |
22 | if (config.enableDynamicImports) {
23 | stats = requireFunc('../../react-loadable.json');
24 | }
25 |
26 | export default function handleRender(req, res) {
27 | let context = {}, modules = [], initialState = {};
28 |
29 | // Create a new Redux store instance
30 | const store = configureStore(initialState);
31 |
32 | // Server side responsive detection
33 | const mobileDetect = mobileParser(req);
34 |
35 | // set mobile detection for our responsive store
36 | store.dispatch(setMobileDetect(mobileDetect));
37 |
38 | // Grab the initial state from our Redux store
39 | const finalState = store.getState();
40 |
41 | // If SSR is disabled, just render the skeleton HTML.
42 | if (!config.enableSSR) {
43 | const markup = render(null, finalState, []);
44 | return res.send(markup);
45 | }
46 |
47 | // See react-router's Server Rendering section:
48 | // https://reacttraining.com/react-router/web/guides/server-rendering
49 | const matchRoutes = routes => {
50 | return routes.reduce((matches, route) => {
51 | const { path } = route;
52 | const match = matchPath(req.baseUrl, {
53 | path,
54 | exact: true,
55 | strict: false
56 | });
57 |
58 | if (match) {
59 | const wc =
60 | (route.component && route.component.WrappedComponent) ||
61 | route.component;
62 |
63 | matches.push({
64 | route,
65 | match,
66 | fetchData: (wc && wc.fetchData) || (() => Promise.resolve())
67 | });
68 | }
69 |
70 | if (!match && route.routes) {
71 | // recursively try to match nested routes
72 | const nested = matchRoutes(route.routes);
73 |
74 | if (nested.length) {
75 | matches = matches.concat(nested);
76 | }
77 | }
78 |
79 | return matches;
80 | }, []);
81 | };
82 |
83 | const matches = matchRoutes(routes);
84 |
85 | // No matched route, send an error.
86 | if (!matches.length) {
87 | return res.status(500).send('Server Error');
88 | }
89 |
90 | const getComponent = () => {
91 | let component = (
92 |
93 |
94 |
95 |
96 |
97 | );
98 |
99 | if (config.enableDynamicImports) {
100 | return (
101 | modules.push(moduleName)}>
102 | {component}
103 |
104 | );
105 | }
106 |
107 | return component;
108 | };
109 |
110 | // There's a match, render the component with the matched route, firing off
111 | // any fetchData methods that are statically defined on the server.
112 | const fetchData = matches.map(match => {
113 | const { fetchData, ...rest } = match; // eslint-disable-line no-unused-vars
114 |
115 | // return fetch data Promise, excluding unnecessary fetchData method
116 | return match.fetchData({ store, ...rest });
117 | });
118 |
119 | // Execute the render only after all promises have been resolved.
120 | Promise.all(fetchData).then(() => {
121 | const state = store.getState();
122 | const html = renderToString(getComponent());
123 | const bundles = stats && getBundles(stats, modules) || [];
124 | const markup = render(html, state, bundles);
125 | const status = matches.length && matches[0].match.path === '*' ? 404 : 200;
126 |
127 | // A 301 redirect was rendered somewhere if context.url exists after
128 | // rendering has happened.
129 | if (context.url) {
130 | return res.redirect(302, context.url);
131 | }
132 |
133 | res.contentType('text/html');
134 | return res.status(status).send(markup);
135 | });
136 | }
137 |
--------------------------------------------------------------------------------
/server/renderer/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 |
3 | const env = process.env.NODE_ENV || 'development';
4 | const router = express.Router();
5 | const handleRender = require(
6 | env === 'production' ? './handler.built' : './handler'
7 | ).default;
8 |
9 | router.use(handleRender);
10 |
11 | module.exports = router;
12 |
--------------------------------------------------------------------------------
/server/renderer/render.js:
--------------------------------------------------------------------------------
1 | // cache the main layout template with lodash
2 | import { template } from 'lodash';
3 | import { Helmet } from 'react-helmet';
4 |
5 | const { NODE_ENV } = process.env;
6 | const compile = template(require('@templates/layouts/application.html'));
7 | const env = NODE_ENV || 'development';
8 |
9 | export default function render(html, initialState = {}, bundles = []) {
10 | if (env === 'development') {
11 | global.ISOTools.refresh();
12 | }
13 |
14 | const assets = global.ISOTools.assets();
15 | const appJs = assets.javascript.app;
16 | const vendorJs = assets.javascript.vendor;
17 | const helmet = Helmet.renderStatic();
18 | const appCss = assets.styles.app;
19 | const vendorCss = assets.styles.vendor;
20 | const chunkCss = bundles.filter(bundle => bundle.file.match(/.css/));
21 | const chunkJs = bundles.filter(bundle => bundle.file.match(/.js/));
22 |
23 | return compile({
24 | html,
25 | helmet,
26 | appCss,
27 | appJs,
28 | vendorJs,
29 | vendorCss,
30 | chunkCss,
31 | chunkJs,
32 | initialState
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import express from 'express';
3 | import helmet from 'helmet';
4 | import compression from 'compression';
5 | import Api from './api';
6 | import cookieParser from 'cookie-parser';
7 | import ReactRenderer from './renderer';
8 | import { httpsRedirect } from '$middleware';
9 |
10 | const env = process.env.NODE_ENV || 'development';
11 | const app = new express();
12 |
13 | // Secure with helmet
14 | app.use(helmet());
15 |
16 | // Ensures SSL in used in production.
17 | app.use(httpsRedirect({ enabled: env === 'production' }));
18 |
19 | // parse cookies!
20 | app.use(cookieParser());
21 |
22 | // gzip
23 | app.use(compression());
24 |
25 | // Add middleware to serve up all static files
26 | app.use(
27 | '/assets',
28 | express.static(path.join(__dirname, '../' + process.env.PUBLIC_OUTPUT_PATH)),
29 | express.static(path.join(__dirname, '../common/images')),
30 | express.static(path.join(__dirname, '../common/fonts'))
31 | );
32 |
33 | // handle browsers requesting favicon
34 | app.use(
35 | '/favicon.ico',
36 | express.static(path.join(__dirname, '../common/images/favicon/favicon.ico'))
37 | );
38 |
39 | // Mount the REST API
40 | app.use('/api', Api);
41 |
42 | // Mount the react render handler
43 |
44 | app.use('*', ReactRenderer);
45 |
46 | module.exports = app;
47 |
--------------------------------------------------------------------------------
/server/templates/layouts/application.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= helmet.title %>
5 |
6 |
7 |
8 | <%= vendorCss ? ` ` : '' %>
9 |
10 | <%= chunkCss.map(bundle => {
11 | return ` `
12 | }).join('\n') %>
13 |
14 |
15 | <%= html %>
16 |
17 | <%= vendorJs ? `` : '' %>
18 |
19 | <%= chunkJs.map(bundle => {
20 | return ``
21 | }).join('\n') %>
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/support/jest.config.js:
--------------------------------------------------------------------------------
1 | const { mapValues, mapKeys } = require('lodash');
2 | const { _moduleAliases } = require('../../package.json');
3 | const escapeStringRegexp = require('escape-string-regexp');
4 |
5 | const toRegex = (alias) => `^${escapeStringRegexp(alias)}/(.*)$`;
6 |
7 | // Maps _moduleAliases in package.json to Jest's regex format that it can read
8 | const moduleAliasesMap = mapValues(
9 | mapKeys(_moduleAliases, (_, alias) => toRegex(alias)),
10 | path => `/${path}/$1`
11 | );
12 |
13 | const cssFiles = '\\.(css|scss|less)$';
14 | const staticFiles =
15 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|' +
16 | 'webm|wav|mp3|m4a|aac|oga)$';
17 |
18 | module.exports = {
19 | verbose: true,
20 | moduleFileExtensions: ['js', 'jsx'],
21 | rootDir: process.cwd(),
22 | snapshotSerializers: ['enzyme-to-json/serializer'],
23 | setupTestFrameworkScriptFile: '/test/support/jest.setup.js',
24 | globalSetup: '/test/support/jest.globalSetup.js',
25 | moduleNameMapper: {
26 | ...moduleAliasesMap,
27 | [staticFiles]: '/__mocks__/fileMock.js',
28 | [cssFiles]: 'identity-obj-proxy'
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/test/support/jest.globalSetup.js:
--------------------------------------------------------------------------------
1 | module.exports = async () => {
2 | // This module can be used to do any sort of global setup before tests are
3 | // run, such as connecting to or seeding a database.
4 | };
5 |
--------------------------------------------------------------------------------
/test/support/jest.setup.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | // configure an adapter for enzyme
5 | configure({ adapter: new Adapter() });
6 |
--------------------------------------------------------------------------------
/webpack/babel.config.client.js:
--------------------------------------------------------------------------------
1 | export default {
2 | babelrc: false,
3 | presets: [
4 | [
5 | 'env',
6 | {
7 | targets: {
8 | browser: 'last 2 versions',
9 | uglify: false
10 | }
11 | }
12 | ],
13 | 'react'
14 | ],
15 | plugins: [
16 | 'react-loadable/babel',
17 | 'transform-es2015-modules-commonjs',
18 | 'transform-class-properties',
19 | 'transform-export-extensions',
20 | 'transform-object-rest-spread',
21 | 'syntax-dynamic-import',
22 | [
23 | 'lodash',
24 | {
25 | id: ['lodash', 'semantic-ui-react']
26 | }
27 | ]
28 | ],
29 | env: {
30 | development: {
31 | plugins: ['react-hot-loader/babel']
32 | },
33 | production: {
34 | plugins: ['transform-react-remove-prop-types']
35 | }
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/webpack/babel.config.ssr.js:
--------------------------------------------------------------------------------
1 | export default {
2 | babelrc: false,
3 | presets: [
4 | [
5 | 'env',
6 | {
7 | targets: {
8 | node: 'current',
9 | uglify: true
10 | }
11 | }
12 | ],
13 | 'react'
14 | ],
15 | plugins: [
16 | 'react-loadable/babel',
17 | 'transform-es2015-modules-commonjs',
18 | 'transform-class-properties',
19 | 'transform-export-extensions',
20 | 'transform-object-rest-spread',
21 | 'syntax-dynamic-import'
22 | ],
23 | env: {
24 | development: {
25 | plugins: []
26 | },
27 | production: {
28 | plugins: ['transform-react-remove-prop-types']
29 | }
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/webpack/base.js:
--------------------------------------------------------------------------------
1 | import yn from 'yn';
2 | import path from 'path';
3 | import webpack from 'webpack';
4 | import IsoPlugin from 'webpack-isomorphic-tools/plugin';
5 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
6 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
7 | import { ReactLoadablePlugin } from 'react-loadable/webpack';
8 | import { mapValues, keyBy, filter } from 'lodash';
9 | import { _moduleAliases } from '../package.json';
10 | import babelOpts from './babel.config.client';
11 | import {
12 | enableDynamicImports,
13 | isomorphicConfig,
14 | clientEnv,
15 | cssModulesIdentifier
16 | } from '../config';
17 |
18 | const isDev = process.env.NODE_ENV === 'development';
19 | const cwd = process.cwd();
20 |
21 | if (isDev) {
22 | require('dotenv').load();
23 | }
24 |
25 | export const isSSR = yn(process.env.SSR) || false;
26 | export const analyzeBundle = yn(process.env.ANALYZE) || false;
27 | export const basePlugins = {
28 | reactLoadablePlugin: new ReactLoadablePlugin({
29 | filename: path.join(__dirname, '..', 'react-loadable.json')
30 | }),
31 | isomorphicPlugin: new IsoPlugin(isomorphicConfig).development(isDev),
32 | miniExtractPlugin: new MiniCssExtractPlugin({
33 | filename: '[name].[chunkhash].css'
34 | }),
35 | definePlugin: new webpack.DefinePlugin({
36 | 'process.env': mapValues(keyBy(clientEnv), env => {
37 | return JSON.stringify(process.env[env]);
38 | })
39 | }),
40 | bundleAnalyzerPlugin: new BundleAnalyzerPlugin()
41 | };
42 |
43 | const allowedPlugin = (plugin, key) => {
44 | switch (key) {
45 | case 'reactLoadablePlugin':
46 | return enableDynamicImports;
47 | case 'miniExtractPlugin':
48 | return !isSSR;
49 | case 'bundleAnalyzerPlugin':
50 | return analyzeBundle;
51 | default:
52 | return true;
53 | }
54 | };
55 |
56 | export default {
57 | context: path.resolve(__dirname, '..'),
58 | mode: isDev ? 'development' : 'production',
59 | entry: {
60 | app: ['./client/index']
61 | },
62 | optimization: {
63 | splitChunks: {
64 | cacheGroups: {
65 | vendor: {
66 | name: 'vendor',
67 | chunks: 'all',
68 | reuseExistingChunk: true,
69 | priority: 1,
70 | enforce: true,
71 | // extract to vendor chunk if it's in /node_modules
72 | test: module => /node_modules/.test(module.context)
73 | }
74 | }
75 | }
76 | },
77 | output: {
78 | path: path.join(__dirname, '..', process.env.PUBLIC_OUTPUT_PATH),
79 | filename: '[name].bundle.js',
80 | publicPath: process.env.PUBLIC_ASSET_PATH || '/',
81 | chunkFilename: enableDynamicImports ? '[name].bundle.js' : undefined
82 | },
83 | resolve: {
84 | extensions: ['.js', '.jsx', '.scss'],
85 | alias: mapValues(_moduleAliases, aliasPath =>
86 | path.join(cwd, ...aliasPath.split('/'))
87 | )
88 | },
89 | plugins: filter(basePlugins, allowedPlugin),
90 | module: {
91 | rules: [
92 | {
93 | test: /\.jsx$|\.js$/,
94 | exclude: /node_modules/,
95 | loader: 'babel-loader',
96 | options: babelOpts
97 | },
98 | {
99 | // For all .scss files that should be modularized. This should exclude
100 | // anything inside node_modules and everything inside common/css/base
101 | // since they should be globally scoped.
102 | test: /\.scss$/,
103 | exclude: [
104 | path.resolve(__dirname, '../node_modules'),
105 | path.resolve(__dirname, '../common/css/base')
106 | ],
107 | use: [
108 | 'css-hot-loader',
109 | MiniCssExtractPlugin.loader,
110 | {
111 | loader: 'css-loader',
112 | options: {
113 | modules: true,
114 | minimize: false,
115 | importLoaders: 1,
116 | localIdentName: cssModulesIdentifier
117 | }
118 | },
119 | { loader: 'postcss-loader' },
120 | { loader: 'sass-loader' },
121 | {
122 | loader: 'sass-resources-loader',
123 | options: {
124 | resources: './common/css/resources/*.scss'
125 | }
126 | }
127 | ]
128 | },
129 | {
130 | // for .scss modules that need to be available globally, we don't pass
131 | // the files through css-loader to be modularized.
132 | test: /\.scss$/,
133 | include: [
134 | path.resolve(__dirname, '../node_modules'),
135 | path.resolve(__dirname, '../common/css/base')
136 | ],
137 | use: [
138 | 'css-hot-loader',
139 | MiniCssExtractPlugin.loader,
140 | { loader: 'css-loader' },
141 | { loader: 'postcss-loader' },
142 | { loader: 'sass-loader' },
143 | {
144 | loader: 'sass-resources-loader',
145 | options: {
146 | resources: './common/css/resources/*.scss'
147 | }
148 | }
149 | ]
150 | },
151 | {
152 | test: /\.css$/,
153 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
154 | },
155 | {
156 | test: basePlugins.isomorphicPlugin.regular_expression('images'),
157 | use: [
158 | {
159 | loader: 'url-loader',
160 | options: {
161 | limit: 10240
162 | }
163 | }
164 | ]
165 | },
166 | {
167 | // Load fonts using file-loader
168 | test: /\.(ttf|eot|woff2?)$/,
169 | loader: 'file-loader'
170 | }
171 | ]
172 | }
173 | };
174 |
--------------------------------------------------------------------------------
/webpack/development.client.babel.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import WebpackDevServer from 'webpack-dev-server';
3 | import webpack from 'webpack';
4 | import merge from 'webpack-merge';
5 | import baseConfig from './base';
6 |
7 | const {
8 | DEV_SERVER_PORT,
9 | DEV_SERVER_HOSTNAME,
10 | DEV_SERVER_HOST_URL
11 | } = process.env;
12 |
13 | const webpackConfig = merge(baseConfig, {
14 | devtool: 'eval',
15 | entry: {
16 | app: [
17 | 'webpack-dev-server/client?' + DEV_SERVER_HOST_URL,
18 | 'webpack/hot/only-dev-server'
19 | ]
20 | },
21 | plugins: [
22 | new webpack.HotModuleReplacementPlugin()
23 | ]
24 | });
25 |
26 | console.info('Firing up Webpack dev server...\n');
27 |
28 | new WebpackDevServer(webpack(webpackConfig), {
29 | port: process.env.DEV_SERVER_PORT,
30 | publicPath: webpackConfig.output.publicPath,
31 | hot: true,
32 | historyApiFallback: true,
33 | noInfo: false,
34 | quiet: false,
35 | headers: { 'Access-Control-Allow-Origin': '*' },
36 | stats: {
37 | colors: true,
38 | hash: false,
39 | version: false,
40 | chunks: false,
41 | children: false
42 | },
43 | disableHostCheck: true
44 | }).listen(DEV_SERVER_PORT, DEV_SERVER_HOSTNAME, e => {
45 | if (e) {
46 | console.error(e);
47 | } else {
48 | console.info(
49 | `Webpack dev server mounted at ${DEV_SERVER_HOST_URL}.`
50 | );
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/webpack/production.client.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import baseConfig from './base';
3 | import CompressionPlugin from 'compression-webpack-plugin';
4 | import UglifyJSPlugin from 'uglifyjs-webpack-plugin';
5 | import merge from 'webpack-merge';
6 | import config from '../config';
7 |
8 | export default merge(baseConfig, {
9 | output: {
10 | filename: '[name].[hash].js',
11 | chunkFilename: config.enableDynamicImports ? '[name].[hash].js' : undefined
12 | },
13 | optimization: {
14 | splitChunks: {
15 | chunks: 'all',
16 | name: true
17 | }
18 | },
19 | plugins: [
20 | new webpack.BannerPlugin({
21 | banner:
22 | 'hash:[hash], chunkhash:[chunkhash], name:[name], ' +
23 | 'filebase:[filebase], query:[query], file:[file]'
24 | }),
25 | new UglifyJSPlugin({
26 | uglifyOptions: {
27 | parallel: 4,
28 | compress: {
29 | warnings: false
30 | },
31 | mangle: true,
32 | output: {
33 | comments: false
34 | }
35 | }
36 | }),
37 | new CompressionPlugin({
38 | asset: '[file].gz',
39 | algorithm: 'gzip',
40 | test: /\.css$|\.js$|\.html$/,
41 | threshold: 10240,
42 | minRatio: 0.8
43 | })
44 | ]
45 | });
46 |
--------------------------------------------------------------------------------
/webpack/production.ssr.babel.js:
--------------------------------------------------------------------------------
1 | import nodeExternals from 'webpack-node-externals';
2 | import merge from 'webpack-merge';
3 | import path from 'path';
4 | import config from '../config';
5 | import babelOpts from './babel.config.ssr';
6 | import { enableDynamicImports } from '../config';
7 | import baseConfig, { basePlugins, analyzeBundle } from './base';
8 | import { set, filter } from 'lodash';
9 |
10 | const allowedPlugin = (plugin, key) => {
11 | switch (key) {
12 | case 'reactLoadablePlugin':
13 | return enableDynamicImports;
14 | case 'isomorphicPlugin':
15 | return false;
16 | case 'bundleAnalyzerPlugin':
17 | return analyzeBundle;
18 | default:
19 | return true;
20 | }
21 | };
22 |
23 | export default merge.strategy({
24 | entry: 'replace',
25 | plugins: 'replace',
26 | module: 'replace',
27 | optimization: 'replace'
28 | })(baseConfig, {
29 | context: null,
30 | mode: 'development',
31 | optimization: {
32 | splitChunks: {
33 | chunks: 'all',
34 | name: true
35 | }
36 | },
37 | target: 'node',
38 | entry: ['./server/renderer/handler.js'],
39 | externals: [
40 | // images are handled by isomorphic webpack.
41 | // html files are required directly
42 | /\.(html|png|gif|jpg)$/,
43 | // treat all node modules as external to keep this bundle small
44 | nodeExternals()
45 | ],
46 | output: {
47 | path: path.join(__dirname, '..', process.env.OUTPUT_PATH, 'renderer'),
48 | filename: 'handler.built.js',
49 | libraryTarget: 'commonjs'
50 | },
51 | plugins: [...filter(basePlugins, allowedPlugin)],
52 | module: {
53 | rules: [
54 | {
55 | test: /\.js|jsx$/,
56 | exclude: /node_modules/,
57 | use: {
58 | loader: 'babel-loader',
59 | // tell babel to uglify production server code for SSR rendering
60 | options: set(babelOpts, 'presets[0][1].targets.uglify', true)
61 | }
62 | },
63 | {
64 | test: /\.scss$/,
65 | exclude: [
66 | path.resolve(__dirname, '../node_modules'),
67 | path.resolve(__dirname, '../common/css/base')
68 | ],
69 | use: [
70 | {
71 | loader: 'css-loader/locals',
72 | options: {
73 | modules: true,
74 | minimize: false,
75 | importLoaders: 0,
76 | localIdentName: config.cssModulesIdentifier
77 | }
78 | },
79 | { loader: 'postcss-loader' },
80 | { loader: 'sass-loader' },
81 | {
82 | loader: 'sass-resources-loader',
83 | options: {
84 | resources: './common/css/resources/*.scss'
85 | }
86 | }
87 | ]
88 | }
89 | ]
90 | }
91 | });
92 |
--------------------------------------------------------------------------------