` also works. (e.g. `npm run generate container`)
19 |
20 | ### Learn more
21 |
22 | - [Redux](redux.md)
23 | - [ImmutableJS](immutablejs.md)
24 | - [reselect](reselect.md)
25 | - [redux-saga](redux-saga.md)
26 | - [react-intl](i18n.md)
27 | - [routing](routing.md)
28 | - [Asynchronously loaded components](async-components.md)
29 |
30 | ## Architecture: `components` and `containers`
31 |
32 | We adopted a split between stateless, reusable components called (wait for it...)
33 | `components` and stateful parent components called `containers`.
34 |
35 | ### Learn more
36 |
37 | See [this article](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
38 | by Dan Abramov for a great introduction to this approach.
39 |
--------------------------------------------------------------------------------
/docs/js/async-components.md:
--------------------------------------------------------------------------------
1 | # Loading components with react-loadable
2 |
3 | [`react-loadable`](https://github.com/thejameskyle/react-loadable) is integrated into
4 | `react-boilerplate` because of its rich functionality and good design (it does not
5 | conflate routes with asynchronous components).
6 |
7 | To load component asynchronously, create a `Loadable` file by hand or via component/container generators with
8 | 'Do you want an async loader?' option activated. This is how it can look like:
9 |
10 | ```JS
11 | import Loadable from 'react-loadable';
12 |
13 | import LoadingIndicator from 'components/LoadingIndicator';
14 |
15 | export default Loadable({
16 | loader: () => import('./index'),
17 | loading: LoadingIndicator,
18 | });
19 | ```
20 |
21 | You can find more information on how to use `react-loadable` in [their docs](https://github.com/thejameskyle/react-loadable).
22 |
--------------------------------------------------------------------------------
/docs/js/i18n.md:
--------------------------------------------------------------------------------
1 | # `i18n`
2 |
3 | `react-intl` is a library to manage internationalization and pluralization support
4 | for your react application. This involves multi-language support for both the static text but also things like variable numbers, words or names that change with application state. `react-intl` provides an incredible amount of mature facility to perform these very tasks.
5 |
6 | The complete `react-intl` docs can be found here:
7 |
8 | https://github.com/yahoo/react-intl/wiki
9 |
10 | ## Usage
11 |
12 | Below we see a `messages.js` file for the `Footer` component example. A `messages.js` file should be included in any simple or container component that wants to use internationalization. You can add this support when you scaffold your component using this boilerplates scaffolding `plop` system.
13 |
14 | All default English text for the component is contained here (e.g. `This project is licensed under the MIT license.`), and is tagged with an ID (e.g. `boilerplate.components.Footer.license.message`) in addition to it's object definition id (e.g. `licenseMessage`).
15 |
16 | This is set in `react-intl`'s `defineMessages` function which is then exported for use in the component. You can read more about `defineMessages` here:
17 |
18 | https://github.com/yahoo/react-intl/wiki/API#definemessages
19 |
20 | ```js
21 | /*
22 | * Footer Messages
23 | *
24 | * This contains all the text for the Footer component.
25 | */
26 | import { defineMessages } from 'react-intl';
27 |
28 | export default defineMessages({
29 | licenseMessage: {
30 | id: 'boilerplate.components.Footer.license.message',
31 | defaultMessage: 'This project is licensed under the MIT license.',
32 | },
33 | authorMessage: {
34 | id: 'boilerplate.components.Footer.author.message',
35 | defaultMessage: `
36 | Made with love by {author}.
37 | `,
38 | },
39 | });
40 | ```
41 |
42 | Below is the example `Footer` component. Here we see the component including the `messages.js` file, which contains all the default component text, organized with ids (and optionally descriptions). We are also importing the `FormattedMessage` component, which will display a given message from the `messages.js` file in the selected language.
43 |
44 | You will also notice a more complex use of `FormattedMessage` for the author message where alternate or variable values (i.e. `author: Max Stoiber,`) are being injected, in this case it's a react component.
45 |
46 | ```js
47 | import React from 'react';
48 |
49 | import messages from './messages';
50 | import A from 'components/A';
51 | import styles from './styles.css';
52 | import { FormattedMessage } from 'react-intl';
53 |
54 | function Footer() {
55 | return (
56 |
73 | );
74 | }
75 |
76 | export default Footer;
77 | ```
78 |
79 | ## Extracting i18n JSON files
80 |
81 | You can extract all i18n language within each component by running the following command:
82 |
83 | ```
84 | npm run extract-intl
85 | ```
86 |
87 | This will extract all language into i18n JSON files in `app/translations`.
88 |
89 | ## Adding A Language
90 |
91 | You can add a language by running the generate command:
92 |
93 | ```
94 | npm run generate language
95 | ```
96 |
97 | Then enter the two character i18n standard language specifier (e.g. "fr", "de", "es" - without quotes). This will add in the necessary JSON language file and import statements for the language. Note, it is up to you to fill in the translations for the language.
98 |
99 | ## Removing i18n and react-intl
100 |
101 | You can remove `react-intl` modules by first removing the `IntlProvider` object from the `app/app.js` file and by either removing or not selecting the i18n text option during component scaffolding.
102 |
103 | The packages associated with `react-intl` are:
104 | - react-intl
105 | - babel-plugin-react-intl
106 |
--------------------------------------------------------------------------------
/docs/js/immutablejs.md:
--------------------------------------------------------------------------------
1 | # ImmutableJS
2 |
3 | Immutable data structures can be deeply compared in no time. This allows us to
4 | efficiently determine if our components need to rerender since we know if the
5 | `props` changed or not!
6 |
7 | Check out the [official documentation](https://facebook.github.io/immutable-js/)
8 | for a good explanation of the more intricate benefits it has.
9 |
10 | ## Usage
11 |
12 | In our reducers, we make the initial state an immutable data structure with the
13 | `fromJS` function. We pass it an object or an array, and it takes care of
14 | converting it to a immutable data structure. (Note: the conversion is performed deeply so
15 | that even arbitrarily nested arrays/objects are immutable structures too!)
16 |
17 | ```JS
18 | import { fromJS } from 'immutable';
19 |
20 | const initialState = fromJS({
21 | myData: {
22 | message: 'Hello World!'
23 | },
24 | });
25 | ```
26 |
27 |
28 |
29 | When a reducer is subscribed to an action and needs to return the new state they can do so by using setter methods such as [`.set`](https://facebook.github.io/immutable-js/docs/#/Map/set) and [`.update`](https://facebook.github.io/immutable-js/docs/#/Map/update) and [`.merge`](https://facebook.github.io/immutable-js/docs/#/Map/merge).
30 | If the changing state data is nested, we can utilize the 'deep' versions of these setters: [`.setIn`](https://facebook.github.io/immutable-js/docs/#/Map/setIn) and [`.updateIn`](https://facebook.github.io/immutable-js/docs/#/Map/updateIn), [`.mergeIn`](https://facebook.github.io/immutable-js/docs/#/Map/mergeIn).
31 |
32 | ```JS
33 | import { SOME_ACTION, SOME_OTHER_ACTION } from './actions';
34 |
35 | // […]
36 |
37 | function myReducer(state = initialState, action) {
38 | switch (action.type) {
39 | case SOME_ACTION:
40 | return state.set('myData', action.payload);
41 | case SOME_OTHER_ACTION:
42 | return state.setIn(['myData', 'message'], action.payload);
43 | default:
44 | return state;
45 | }
46 | }
47 | ```
48 |
49 | We use [`reselect`](./reselect.md) to efficiently cache our computed application
50 | state. Since that state is now immutable, we need to use the [`.get`](https://facebook.github.io/immutable-js/docs/#/Iterable/get) and [`.getIn`](https://facebook.github.io/immutable-js/docs/#/Iterable/getIn)
51 | functions to select the part we want.
52 |
53 | ```JS
54 | const myDataSelector = (state) => state.get('myData');
55 | const messageSelector = (state) => state.getIn(['myData', 'message']);
56 |
57 | export default myDataSelector;
58 | ```
59 |
60 | To learn more, check out [`reselect.md`](reselect.md)!
61 |
62 | ## Immutable Records
63 |
64 | ImmutableJS provides a number of immutable structures such as [`Map`](https://facebook.github.io/immutable-js/docs/#/Map), [`Set`](https://facebook.github.io/immutable-js/docs/#/Set) and [`List`](https://facebook.github.io/immutable-js/docs/#/List).
65 | One drawback to these structures is that properties must be accessed via the getter methods (`.get` or `.getIn`) and cannot be accessed with dot notation as they would in a plain javascript object.
66 | For instance you'll write `map.get('property')` instead of `object.property`, and `list.get(0)` instead of `array[0]`.
67 | This can make your code a little harder to follow and requires you to be extra cautious when passing arguments or props to functions or components that try to access values with regular dot notation.
68 | ImmutableJS's [`Record`](https://facebook.github.io/immutable-js/docs/#/Record) structure offers a solution to this issue.
69 |
70 | A `Record` is similar to a `Map` but has a fixed shape, meaning it's property keys are predefined and you can't later add a new property after the record is created. Attempting to set new properties will cause an error.
71 | One benefit of `Record` is that you can now, along with other immutable read methods (.get, .set, .merge and so on), use the dot notation to access properties.
72 |
73 | The creation of a record is less simple than simply calling `.toJS()`.
74 | First, you have to define the `Record` shape. With the example above, to create your initial state, you'll write:
75 |
76 | ```JS
77 | // Defining the shape
78 | const StateRecord = Record({
79 | myData: {
80 | message: 'Hello World!'
81 | }
82 | });
83 |
84 | const initialState = new StateRecord({}); // initialState is now a new StateRecord instance
85 | // initialized with myData.message set by default as 'Hello World!'
86 | ```
87 |
88 | Now, if you want to access `myData`, you can just write `state.myData` in your reducer code and to access the `message` property you can write `state.myData.message` as you would in a plain javascript object.
89 |
90 | ### Gotchas of Using Records
91 |
92 | Although dot notation can now be used to read properties the same does not apply to setting properties. Any attempts to set a property on a `Record` using dot notation will result in errors.
93 | Instead setter methods ( `.set`, `.update`, `.merge`) should be used.
94 |
95 | Certain properties can not be set on a record as they would conflict with the API. Consider the below example:
96 | ```JS
97 | const ProductRecord = Record({
98 | type: 'tshirt',
99 | size: 'small'
100 | });
101 | ```
102 |
103 | Because record.size is used to return the records count (similar to array.length), the above definition would throw an error.
--------------------------------------------------------------------------------
/docs/js/redux-saga.md:
--------------------------------------------------------------------------------
1 | # `redux-saga`
2 |
3 | `redux-saga` is a library to manage side effects in your application. It works
4 | beautifully for data fetching, concurrent computations and a lot more.
5 | [Sebastien Lorber](https://twitter.com/sebastienlorber) put it best:
6 |
7 | > Imagine there is widget1 and widget2. When some button on widget1 is clicked,
8 | then it should have an effect on widget2. Instead of coupling the 2 widgets
9 | together (ie widget1 dispatch an action that targets widget2), widget1 only
10 | dispatch that its button was clicked. Then the saga listen for this button
11 | click and then update widget2 by dispatching a new event that widget2 is aware of.
12 | >
13 | > This adds a level of indirection that is unnecessary for simple apps, but make
14 | it more easy to scale complex applications. You can now publish widget1 and
15 | widget2 to different npm repositories so that they never have to know about
16 | each others, without having them to share a global registry of actions. The 2
17 | widgets are now bounded contexts that can live separately. They do not need
18 | each others to be consistent and can be reused in other apps as well. **The saga
19 | is the coupling point between the two widgets that coordinate them in a
20 | meaningful way for your business.**
21 |
22 | _Note: It is well worth reading the [source](https://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34623840#34623840)
23 | of this quote in its entirety!_
24 |
25 | To learn more about this amazing way to handle concurrent flows, start with the
26 | [official documentation](https://redux-saga.github.io/redux-saga) and explore
27 | some examples! (read [this comparison](https://stackoverflow.com/questions/34930735/pros-cons-of-using-redux-saga-with-es6-generators-vs-redux-thunk-with-es7-async/34933395) if you're used to `redux-thunk`)
28 |
29 | ## Usage
30 |
31 | Sagas are associated with a container, just like actions, constants, selectors
32 | and reducers. If your container already has a `saga.js` file, simply add your
33 | saga to that. If your container does not yet have a `saga.js` file, add one with
34 | this boilerplate structure:
35 |
36 | ```JS
37 | import { takeLatest, call, put, select } from 'redux-saga/effects';
38 |
39 | // Root saga
40 | export default function* rootSaga() {
41 | // if necessary, start multiple sagas at once with `all`
42 | yield [
43 | takeLatest(LOAD_REPOS, getRepos),
44 | takeLatest(LOAD_USERS, getUsers),
45 | ];
46 | }
47 | ```
48 |
49 | Then, in your `index.js`, use a decorator to inject the root saga:
50 |
51 | ```JS
52 | import injectSaga from 'utils/injectSaga';
53 | import { RESTART_ON_REMOUNT } from 'utils/constants';
54 | import saga from './saga';
55 |
56 | // ...
57 |
58 | // `mode` is an optional argument, default value is `RESTART_ON_REMOUNT`
59 | const withSaga = injectSaga({ key: 'yourcomponent', saga, mode: RESTART_ON_REMOUNT });
60 |
61 | export default compose(
62 | withSaga,
63 | )(YourComponent);
64 | ```
65 |
66 | A `mode` argument can be one of three constants (import them from `utils/constants`):
67 |
68 | - `RESTART_ON_REMOUNT` (default value)—starts a saga when a component is being mounted
69 | and cancels with `task.cancel()` on component un-mount for improved performance;
70 | - `DAEMON`—starts a saga on component mount and never cancels it or starts again;
71 | - `ONCE_TILL_UNMOUNT`—behaves like `RESTART_ON_REMOUNT` but never runs the saga again.
72 |
73 | Now add as many sagas to your `saga.js` file as you want!
74 |
75 | ---
76 |
77 | _Don't like this feature? [Click here](remove.md)_
78 |
--------------------------------------------------------------------------------
/docs/js/redux.md:
--------------------------------------------------------------------------------
1 | # Redux
2 |
3 | If you haven't worked with Redux, it's highly recommended (possibly indispensable!)
4 | to read through the (amazing) [official documentation](http://redux.js.org)
5 | and/or watch this [free video tutorial series](https://egghead.io/series/getting-started-with-redux).
6 |
7 | ## Usage
8 |
9 | See above! As minimal as Redux is, the challenge it addresses - app state
10 | management - is a complex topic that is too involved to properly discuss here.
11 |
12 | You can attach a dynamic reducer to a component whether it's a regular component
13 | or a component that will be loaded dynamically. Dynamic means that it will be
14 | injected when the component it attached to is mounted. In your component's `index.js`:
15 |
16 | ```JS
17 | import injectReducer from 'utils/injectReducer';
18 | import reducer from './reducer';
19 |
20 | // ...
21 |
22 | export function mapDispatchToProps(dispatch) {
23 | // ...
24 | }
25 |
26 | const mapStateToProps = createStructuredSelector({
27 | // ...
28 | });
29 |
30 | const withConnect = connect(mapStateToProps, mapDispatchToProps);
31 |
32 | const withReducer = injectReducer({ key: 'yourcomponent', reducer });
33 |
34 | export default compose(
35 | // Put `withReducer` before `withConnect`
36 | withReducer,
37 | withConnect,
38 | )(YourComponent);
39 | ```
40 | You don't need to do it by hand, a `container` generator will generate everything
41 | that's necessary.
42 |
43 | ## Removing redux
44 |
45 | There are a few reasons why we chose to bundle redux with React Boilerplate, the
46 | biggest being that it is widely regarded as the current best Flux implementation
47 | in terms of architecture, support and documentation.
48 |
49 | You may feel differently! This is completely OK :)
50 |
51 | Below are a few reasons you might want to remove it:
52 |
53 | ### I'm just getting started and Flux is hard
54 |
55 | You're under no obligation to use Redux or any other Flux library! The complexity
56 | of your application will determine the point at which you need to introduce it.
57 |
58 | Here are a couple of great resources for taking a minimal approach:
59 |
60 | - [Misconceptions of Tooling in JavaScript](http://javascriptplayground.com/blog/2016/02/the-react-webpack-tooling-problem)
61 | - [Learn Raw React — no JSX, no Flux, no ES6, no Webpack…](http://jamesknelson.com/learn-raw-react-no-jsx-flux-es6-webpack/)
62 |
63 | ### It's overkill for my project!
64 |
65 | See above.
66 |
67 | ### I prefer `(Alt|MobX|SomethingElse)`!
68 |
69 | React Boilerplate is a baseline for _your_ app: go for it!
70 |
71 | If you feel that we should take a closer look at supporting your preference
72 | out of the box, please let us know.
73 |
--------------------------------------------------------------------------------
/docs/js/remove.md:
--------------------------------------------------------------------------------
1 | ## Removing `redux-saga`
2 |
3 | **We don't recommend removing `redux-saga`**, as we strongly feel that it's the
4 | way to go for most redux based applications.
5 |
6 | If you really want to get rid of it, you will have to delete its traces from several places.
7 |
8 | **app/configureStore.js**
9 |
10 | 1. Remove statement `import createSagaMiddleware from 'redux-saga'`.
11 | 2. Remove statement `const sagaMiddleware = createSagaMiddleware()`.
12 | 3. Remove `sagaMiddleware` from `middlewares` array.
13 | 4. Remove statement `store.runSaga = sagaMiddleware.run`
14 | 5. Remove `store.injectedSagas = {}; // Saga registry`
15 |
16 | **app/utils**
17 |
18 | 1. Remove two files: `injectSaga.js` and `sagaInjectors.js`.
19 |
20 | **app/containers/\*/index.js**
21 |
22 | Clean up containers that inject a dynamic saga
23 |
24 | 1. Remove saga injections like: `const withSaga = injectSaga({ key: 'home', saga });`.
25 |
26 | **Finally, remove it from the `package.json`. Then you should be good to go with whatever
27 | side-effect management library you want to use!**
28 |
29 | ## Removing `reselect`
30 |
31 | To remove `reselect`, remove it from your dependencies in `package.json` and then write
32 | your `mapStateToProps` functions like you normally would!
33 |
34 | You'll also need to hook up the history directly to the store. Make changes to `app/app.js`.
35 |
36 | 1. Remove statement `import { makeSelectLocationState } from 'containers/App/selectors'`
37 | 2. Make necessary changes to `history` as follows:
38 |
39 | ```js
40 |
41 | const makeSelectLocationState = () => {
42 | let prevRoutingState;
43 | let prevRoutingStateJS;
44 |
45 | return (state) => {
46 | const routingState = state.get('route'); // or state.route
47 |
48 | if (!routingState.equals(prevRoutingState)) {
49 | prevRoutingState = routingState;
50 | prevRoutingStateJS = routingState.toJS();
51 | }
52 |
53 | return prevRoutingStateJS;
54 | };
55 | };
56 |
57 | const history = syncHistoryWithStore(browserHistory, store, {
58 | selectLocationState: makeSelectLocationState(),
59 | });
60 | ```
61 |
--------------------------------------------------------------------------------
/docs/js/reselect.md:
--------------------------------------------------------------------------------
1 | # `reselect`
2 |
3 | reselect memoizes ("caches") previous state trees and calculations based on said
4 | tree. This means repeated changes and calculations are fast and efficient,
5 | providing us with a performance boost over standard `mapStateToProps`
6 | implementations.
7 |
8 | The [official documentation](https://github.com/reactjs/reselect)
9 | offers a good starting point!
10 |
11 | ## Usage
12 |
13 | There are two different kinds of selectors, simple and complex ones.
14 |
15 | ### Simple selectors
16 |
17 | Simple selectors are just that: they take the application state and select a
18 | part of it.
19 |
20 | ```javascript
21 | const mySelector = (state) => state.get('someState');
22 |
23 | export {
24 | mySelector,
25 | };
26 | ```
27 |
28 | ### Complex selectors
29 |
30 | If we need to, we can combine simple selectors to build more complex ones which
31 | get nested state parts with reselect's `createSelector` function. We import other
32 | selectors and pass them to the `createSelector` call:
33 |
34 | ```javascript
35 | import { createSelector } from 'reselect';
36 | import mySelector from 'mySelector';
37 |
38 | const myComplexSelector = createSelector(
39 | mySelector,
40 | (myState) => myState.get('someNestedState')
41 | );
42 |
43 | export {
44 | myComplexSelector,
45 | };
46 | ```
47 |
48 | These selectors can then either be used directly in our containers as
49 | `mapStateToProps` functions or be nested with `createSelector` once again:
50 |
51 | ```javascript
52 | export default connect(createSelector(
53 | myComplexSelector,
54 | (myNestedState) => ({ data: myNestedState })
55 | ))(SomeComponent);
56 | ```
57 |
58 | ### Adding a new selector
59 |
60 | If you have a `selectors.js` file next to the reducer which's part of the state
61 | you want to select, add your selector to said file. If you don't have one yet,
62 | add a new one into your container folder and fill it with this boilerplate code:
63 |
64 | ```JS
65 | import { createSelector } from 'reselect';
66 |
67 | const selectMyState = () => createSelector(
68 |
69 | );
70 |
71 | export {
72 | selectMyState,
73 | };
74 | ```
75 |
76 | ---
77 |
78 | _Don't like this feature? [Click here](remove.md)_
79 |
--------------------------------------------------------------------------------
/docs/js/routing.md:
--------------------------------------------------------------------------------
1 | # Routing via `react-router` and `react-router-redux`
2 |
3 | `react-router` is the de-facto standard routing solution for react applications.
4 | The thing is that with redux and a single state tree, the URL is part of that
5 | state. `react-router-redux` takes care of synchronizing the location of our
6 | application with the application state.
7 |
8 | (See the [`react-router-redux` documentation](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux)
9 | for more information)
10 |
11 | ## Usage
12 |
13 | To add a new route, simply import the `Route` component and use it standalone or inside the `Switch` component (all part of [RR4 API](https://reacttraining.com/react-router/web/api)):
14 |
15 | ```JS
16 |
17 | ```
18 |
19 | Top level routes are located in `App.js`.
20 |
21 | If you want your route component (or any component for that matter) to be loaded asynchronously, use container or component generator with 'Do you want an async loader?' option activated.
22 |
23 | To go to a new page use the `push` function by `react-router-redux`:
24 |
25 | ```JS
26 | import { push } from 'react-router-redux';
27 |
28 | dispatch(push('/some/page'));
29 | ```
30 |
31 | ## Child Routes
32 | For example, if you have a route called `about` at `/about` and want to make a child route called `team` at `/about/our-team`, follow the example
33 | in `App.js` to create a `Switch` within the parent component. Also remove the `exact` property from the `about` parent route.
34 |
35 | ```JS
36 | // AboutPage/index.js
37 | import { Switch, Route } from 'react-router-dom';
38 |
39 | class AboutPage extends React.PureComponent {
40 | render() {
41 | return (
42 |
43 |
44 |
45 | );
46 | }
47 | }
48 | ```
49 |
50 | Note that with React Router v4, route re-rendering is handled by React's `setState`. This
51 | means that when wrapping route components in a redux connected container, or `PureComponent` or any other component with
52 | `shouldComponentUpdate`, you need to create a [ConnectedSwitch](https://github.com/ReactTraining/react-router/issues/5072#issuecomment-310184271)
53 | container that receives `location` directly from a redux store. Read more about this in
54 | [Dealing with Update Blocking](https://reacttraining.com/react-router/web/guides/dealing-with-update-blocking).
55 |
56 | You can read more in [`react-router`'s documentation](https://reacttraining.com/react-router/web/api).
57 |
--------------------------------------------------------------------------------
/docs/testing/README.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | - [Unit Testing](unit-testing.md)
4 | - [Component Testing](component-testing.md)
5 | - [Remote Testing](remote-testing.md)
6 |
7 | Testing your application is a vital part of serious development. There are a few
8 | things you should test. If you've never done this before start with [unit testing](unit-testing.md).
9 | Move on to [component testing](component-testing.md) when you feel like you
10 | understand that!
11 |
12 | We also support [remote testing](remote-testing.md) your local application,
13 | which is quite awesome, so definitely check that out!
14 |
15 | ## Usage with this boilerplate
16 |
17 | To test your application started with this boilerplate do the following:
18 |
19 | 1. Sprinkle `.test.js` files directly next to the parts of your application you
20 | want to test. (Or in `test/` subdirectories, it doesn't really matter as long
21 | as they are directly next to those parts and end in `.test.js`)
22 |
23 | 1. Write your unit and component tests in those files.
24 |
25 | 1. Run `npm run test` in your terminal and see all the tests pass! (hopefully)
26 |
27 | There are a few more commands related to testing, checkout the [commands documentation](../general/commands.md#testing)
28 | for the full list!
29 |
--------------------------------------------------------------------------------
/docs/testing/remote-testing.md:
--------------------------------------------------------------------------------
1 | # Remote testing
2 |
3 | ```Shell
4 | npm run start:tunnel
5 | ```
6 |
7 | This command will start a server and tunnel it with `ngrok`. You'll get a URL
8 | that looks a bit like this: `http://abcdef.ngrok.com`
9 |
10 | This URL will show the version of your application that's in the `build` folder,
11 | and it's accessible from the entire world! This is great for testing on different
12 | devices and from different locations!
13 |
--------------------------------------------------------------------------------
/internals/config.js:
--------------------------------------------------------------------------------
1 | const resolve = require('path').resolve;
2 | const pullAll = require('lodash/pullAll');
3 | const uniq = require('lodash/uniq');
4 |
5 | const ReactBoilerplate = {
6 | // This refers to the react-boilerplate version this project is based on.
7 | version: '3.4.0',
8 |
9 | /**
10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading
11 | * by caching the module metadata for all of our npm dependencies. We enable it by default
12 | * in development.
13 | *
14 | *
15 | * To disable the DLL Plugin, set this value to false.
16 | */
17 | dllPlugin: {
18 | defaults: {
19 | /**
20 | * we need to exclude dependencies which are not intended for the browser
21 | * by listing them here.
22 | */
23 | exclude: [
24 | 'chalk',
25 | 'compression',
26 | 'cross-env',
27 | 'express',
28 | 'ip',
29 | 'minimist',
30 | 'sanitize.css',
31 | ],
32 |
33 | /**
34 | * Specify any additional dependencies here. We include core-js and lodash
35 | * since a lot of our dependencies depend on them and they get picked up by webpack.
36 | */
37 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'],
38 |
39 | // The path where the DLL manifest and bundle will get built
40 | path: resolve('../node_modules/react-boilerplate-dlls'),
41 | },
42 |
43 | entry(pkg) {
44 | const dependencyNames = Object.keys(pkg.dependencies);
45 | const exclude = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude;
46 | const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include;
47 | const includeDependencies = uniq(dependencyNames.concat(include));
48 |
49 | return {
50 | reactBoilerplateDeps: pullAll(includeDependencies, exclude),
51 | };
52 | },
53 | },
54 | };
55 |
56 | module.exports = ReactBoilerplate;
57 |
--------------------------------------------------------------------------------
/internals/generators/component/class.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{ properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | // import styled from 'styled-components';
9 |
10 | {{#if wantMessages}}
11 | import { FormattedMessage } from 'react-intl';
12 | import messages from './messages';
13 | {{/if}}
14 |
15 | class {{ properCase name }} extends {{{ type }}} { // eslint-disable-line react/prefer-stateless-function
16 | render() {
17 | return (
18 |
19 | {{#if wantMessages}}
20 |
21 | {{/if}}
22 |
23 | );
24 | }
25 | }
26 |
27 | {{ properCase name }}.propTypes = {
28 |
29 | };
30 |
31 | export default {{ properCase name }};
32 |
--------------------------------------------------------------------------------
/internals/generators/component/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Component Generator
3 | */
4 |
5 | /* eslint strict: ["off"] */
6 |
7 | 'use strict';
8 |
9 | const componentExists = require('../utils/componentExists');
10 |
11 | module.exports = {
12 | description: 'Add an unconnected component',
13 | prompts: [{
14 | type: 'list',
15 | name: 'type',
16 | message: 'Select the type of component',
17 | default: 'Stateless Function',
18 | choices: () => ['Stateless Function', 'React.PureComponent', 'React.Component'],
19 | }, {
20 | type: 'input',
21 | name: 'name',
22 | message: 'What should it be called?',
23 | default: 'Button',
24 | validate: (value) => {
25 | if ((/.+/).test(value)) {
26 | return componentExists(value) ? 'A component or container with this name already exists' : true;
27 | }
28 |
29 | return 'The name is required';
30 | },
31 | }, {
32 | type: 'confirm',
33 | name: 'wantMessages',
34 | default: true,
35 | message: 'Do you want i18n messages (i.e. will this component use text)?',
36 | }, {
37 | type: 'confirm',
38 | name: 'wantLoadable',
39 | default: false,
40 | message: 'Do you want to load the component asynchronously?',
41 | }],
42 | actions: (data) => {
43 | // Generate index.js and index.test.js
44 | let componentTemplate;
45 |
46 | switch (data.type) {
47 | case 'Stateless Function': {
48 | componentTemplate = './component/stateless.js.hbs';
49 | break;
50 | }
51 | default: {
52 | componentTemplate = './component/class.js.hbs';
53 | }
54 | }
55 |
56 | const actions = [{
57 | type: 'add',
58 | path: '../../app/components/{{properCase name}}/index.js',
59 | templateFile: componentTemplate,
60 | abortOnFail: true,
61 | }, {
62 | type: 'add',
63 | path: '../../app/components/{{properCase name}}/tests/index.test.js',
64 | templateFile: './component/test.js.hbs',
65 | abortOnFail: true,
66 | }];
67 |
68 | // If they want a i18n messages file
69 | if (data.wantMessages) {
70 | actions.push({
71 | type: 'add',
72 | path: '../../app/components/{{properCase name}}/messages.js',
73 | templateFile: './component/messages.js.hbs',
74 | abortOnFail: true,
75 | });
76 | }
77 |
78 | // If want Loadable.js to load the component asynchronously
79 | if (data.wantLoadable) {
80 | actions.push({
81 | type: 'add',
82 | path: '../../app/components/{{properCase name}}/Loadable.js',
83 | templateFile: './component/loadable.js.hbs',
84 | abortOnFail: true,
85 | });
86 | }
87 |
88 | return actions;
89 | },
90 | };
91 |
--------------------------------------------------------------------------------
/internals/generators/component/loadable.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Asynchronously loads the component for {{ properCase name }}
4 | *
5 | */
6 |
7 | import Loadable from 'react-loadable';
8 |
9 | export default Loadable({
10 | loader: () => import('./index'),
11 | loading: () => null,
12 | });
13 |
--------------------------------------------------------------------------------
/internals/generators/component/messages.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | * {{ properCase name }} Messages
3 | *
4 | * This contains all the text for the {{ properCase name }} component.
5 | */
6 | import { defineMessages } from 'react-intl';
7 |
8 | export default defineMessages({
9 | header: {
10 | id: 'app.components.{{ properCase name }}.header',
11 | defaultMessage: 'This is the {{ properCase name}} component !',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/internals/generators/component/stateless.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{ properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | // import styled from 'styled-components';
9 |
10 | {{#if wantMessages}}
11 | import { FormattedMessage } from 'react-intl';
12 | import messages from './messages';
13 | {{/if}}
14 |
15 | function {{ properCase name }}() {
16 | return (
17 |
18 | {{#if wantMessages}}
19 |
20 | {{/if}}
21 |
22 | );
23 | }
24 |
25 | {{ properCase name }}.propTypes = {
26 |
27 | };
28 |
29 | export default {{ properCase name }};
30 |
--------------------------------------------------------------------------------
/internals/generators/component/test.js.hbs:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { shallow } from 'enzyme';
3 |
4 | // import {{ properCase name }} from '../index';
5 |
6 | describe('<{{ properCase name }} />', () => {
7 | it('Expect to have unit tests specified', () => {
8 | expect(true).toEqual(false);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/internals/generators/container/actions.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} actions
4 | *
5 | */
6 |
7 | import {
8 | DEFAULT_ACTION,
9 | } from './constants';
10 |
11 | export function defaultAction() {
12 | return {
13 | type: DEFAULT_ACTION,
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/internals/generators/container/actions.test.js.hbs:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | defaultAction,
4 | } from '../actions';
5 | import {
6 | DEFAULT_ACTION,
7 | } from '../constants';
8 |
9 | describe('{{ properCase name }} actions', () => {
10 | describe('Default Action', () => {
11 | it('has a type of DEFAULT_ACTION', () => {
12 | const expected = {
13 | type: DEFAULT_ACTION,
14 | };
15 | expect(defaultAction()).toEqual(expected);
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/internals/generators/container/class.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | {{#if wantHeaders}}
11 | import { Helmet } from 'react-helmet';
12 | {{/if}}
13 | {{#if wantMessages}}
14 | import { FormattedMessage } from 'react-intl';
15 | {{/if}}
16 | {{#if wantActionsAndReducer}}
17 | import { createStructuredSelector } from 'reselect';
18 | {{/if}}
19 | import { compose } from 'redux';
20 |
21 | {{#if wantSaga}}
22 | import injectSaga from 'utils/injectSaga';
23 | {{/if}}
24 | {{#if wantActionsAndReducer}}
25 | import injectReducer from 'utils/injectReducer';
26 | import makeSelect{{properCase name}} from './selectors';
27 | import reducer from './reducer';
28 | {{/if}}
29 | {{#if wantSaga}}
30 | import saga from './saga';
31 | {{/if}}
32 | {{#if wantMessages}}
33 | import messages from './messages';
34 | {{/if}}
35 |
36 | export class {{ properCase name }} extends {{{ type }}} { // eslint-disable-line react/prefer-stateless-function
37 | render() {
38 | return (
39 |
40 | {{#if wantHeaders}}
41 |
42 | {{properCase name}}
43 |
44 |
45 | {{/if}}
46 | {{#if wantMessages}}
47 |
48 | {{/if}}
49 |
50 | );
51 | }
52 | }
53 |
54 | {{ properCase name }}.propTypes = {
55 | dispatch: PropTypes.func.isRequired,
56 | };
57 |
58 | {{#if wantActionsAndReducer}}
59 | const mapStateToProps = createStructuredSelector({
60 | {{ lowerCase name }}: makeSelect{{properCase name}}(),
61 | });
62 | {{/if}}
63 |
64 | function mapDispatchToProps(dispatch) {
65 | return {
66 | dispatch,
67 | };
68 | }
69 |
70 | {{#if wantActionsAndReducer}}
71 | const withConnect = connect(mapStateToProps, mapDispatchToProps);
72 |
73 | const withReducer = injectReducer({ key: '{{ camelCase name }}', reducer });
74 | {{else}}
75 | const withConnect = connect(null, mapDispatchToProps);
76 | {{/if}}
77 | {{#if wantSaga}}
78 | const withSaga = injectSaga({ key: '{{ camelCase name }}', saga });
79 | {{/if}}
80 |
81 | export default compose(
82 | {{#if wantActionsAndReducer}}
83 | withReducer,
84 | {{/if}}
85 | {{#if wantSaga}}
86 | withSaga,
87 | {{/if}}
88 | withConnect,
89 | )({{ properCase name }});
90 |
--------------------------------------------------------------------------------
/internals/generators/container/constants.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} constants
4 | *
5 | */
6 |
7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION';
8 |
--------------------------------------------------------------------------------
/internals/generators/container/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Container Generator
3 | */
4 |
5 | const componentExists = require('../utils/componentExists');
6 |
7 | module.exports = {
8 | description: 'Add a container component',
9 | prompts: [{
10 | type: 'list',
11 | name: 'type',
12 | message: 'Select the base component type:',
13 | default: 'Stateless Function',
14 | choices: () => ['Stateless Function', 'React.PureComponent', 'React.Component'],
15 | }, {
16 | type: 'input',
17 | name: 'name',
18 | message: 'What should it be called?',
19 | default: 'Form',
20 | validate: (value) => {
21 | if ((/.+/).test(value)) {
22 | return componentExists(value) ? 'A component or container with this name already exists' : true;
23 | }
24 |
25 | return 'The name is required';
26 | },
27 | }, {
28 | type: 'confirm',
29 | name: 'wantHeaders',
30 | default: false,
31 | message: 'Do you want headers?',
32 | }, {
33 | type: 'confirm',
34 | name: 'wantActionsAndReducer',
35 | default: true,
36 | message: 'Do you want an actions/constants/selectors/reducer tuple for this container?',
37 | }, {
38 | type: 'confirm',
39 | name: 'wantSaga',
40 | default: true,
41 | message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)',
42 | }, {
43 | type: 'confirm',
44 | name: 'wantMessages',
45 | default: true,
46 | message: 'Do you want i18n messages (i.e. will this component use text)?',
47 | }, {
48 | type: 'confirm',
49 | name: 'wantLoadable',
50 | default: true,
51 | message: 'Do you want to load resources asynchronously?',
52 | }],
53 | actions: (data) => {
54 | // Generate index.js and index.test.js
55 | var componentTemplate; // eslint-disable-line no-var
56 |
57 | switch (data.type) {
58 | case 'Stateless Function': {
59 | componentTemplate = './container/stateless.js.hbs';
60 | break;
61 | }
62 | default: {
63 | componentTemplate = './container/class.js.hbs';
64 | }
65 | }
66 |
67 | const actions = [{
68 | type: 'add',
69 | path: '../../app/containers/{{properCase name}}/index.js',
70 | templateFile: componentTemplate,
71 | abortOnFail: true,
72 | }, {
73 | type: 'add',
74 | path: '../../app/containers/{{properCase name}}/tests/index.test.js',
75 | templateFile: './container/test.js.hbs',
76 | abortOnFail: true,
77 | }];
78 |
79 | // If component wants messages
80 | if (data.wantMessages) {
81 | actions.push({
82 | type: 'add',
83 | path: '../../app/containers/{{properCase name}}/messages.js',
84 | templateFile: './container/messages.js.hbs',
85 | abortOnFail: true,
86 | });
87 | }
88 |
89 | // If they want actions and a reducer, generate actions.js, constants.js,
90 | // reducer.js and the corresponding tests for actions and the reducer
91 | if (data.wantActionsAndReducer) {
92 | // Actions
93 | actions.push({
94 | type: 'add',
95 | path: '../../app/containers/{{properCase name}}/actions.js',
96 | templateFile: './container/actions.js.hbs',
97 | abortOnFail: true,
98 | });
99 | actions.push({
100 | type: 'add',
101 | path: '../../app/containers/{{properCase name}}/tests/actions.test.js',
102 | templateFile: './container/actions.test.js.hbs',
103 | abortOnFail: true,
104 | });
105 |
106 | // Constants
107 | actions.push({
108 | type: 'add',
109 | path: '../../app/containers/{{properCase name}}/constants.js',
110 | templateFile: './container/constants.js.hbs',
111 | abortOnFail: true,
112 | });
113 |
114 | // Selectors
115 | actions.push({
116 | type: 'add',
117 | path: '../../app/containers/{{properCase name}}/selectors.js',
118 | templateFile: './container/selectors.js.hbs',
119 | abortOnFail: true,
120 | });
121 | actions.push({
122 | type: 'add',
123 | path: '../../app/containers/{{properCase name}}/tests/selectors.test.js',
124 | templateFile: './container/selectors.test.js.hbs',
125 | abortOnFail: true,
126 | });
127 |
128 | // Reducer
129 | actions.push({
130 | type: 'add',
131 | path: '../../app/containers/{{properCase name}}/reducer.js',
132 | templateFile: './container/reducer.js.hbs',
133 | abortOnFail: true,
134 | });
135 | actions.push({
136 | type: 'add',
137 | path: '../../app/containers/{{properCase name}}/tests/reducer.test.js',
138 | templateFile: './container/reducer.test.js.hbs',
139 | abortOnFail: true,
140 | });
141 | }
142 |
143 | // Sagas
144 | if (data.wantSaga) {
145 | actions.push({
146 | type: 'add',
147 | path: '../../app/containers/{{properCase name}}/saga.js',
148 | templateFile: './container/saga.js.hbs',
149 | abortOnFail: true,
150 | });
151 | actions.push({
152 | type: 'add',
153 | path: '../../app/containers/{{properCase name}}/tests/saga.test.js',
154 | templateFile: './container/saga.test.js.hbs',
155 | abortOnFail: true,
156 | });
157 | }
158 |
159 | if (data.wantLoadable) {
160 | actions.push({
161 | type: 'add',
162 | path: '../../app/containers/{{properCase name}}/Loadable.js',
163 | templateFile: './component/loadable.js.hbs',
164 | abortOnFail: true,
165 | });
166 | }
167 |
168 | return actions;
169 | },
170 | };
171 |
--------------------------------------------------------------------------------
/internals/generators/container/index.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | {{#if wantHeaders}}
11 | import { Helmet } from 'react-helmet';
12 | {{/if}}
13 | {{#if wantMessages}}
14 | import { FormattedMessage } from 'react-intl';
15 | {{/if}}
16 | {{#if wantActionsAndReducer}}
17 | import { createStructuredSelector } from 'reselect';
18 | import makeSelect{{properCase name}} from './selectors';
19 | {{/if}}
20 | {{#if wantMessages}}
21 | import messages from './messages';
22 | {{/if}}
23 |
24 | export class {{ properCase name }} extends React.{{{ component }}} { // eslint-disable-line react/prefer-stateless-function
25 | render() {
26 | return (
27 |
28 | {{#if wantHeaders}}
29 |
30 | {{properCase name}}
31 |
32 |
33 | {{/if}}
34 | {{#if wantMessages}}
35 |
36 | {{/if}}
37 |
38 | );
39 | }
40 | }
41 |
42 | {{ properCase name }}.propTypes = {
43 | dispatch: PropTypes.func.isRequired,
44 | };
45 |
46 | {{#if wantActionsAndReducer}}
47 | const mapStateToProps = createStructuredSelector({
48 | {{name}}: makeSelect{{properCase name}}(),
49 | });
50 | {{/if}}
51 |
52 | function mapDispatchToProps(dispatch) {
53 | return {
54 | dispatch,
55 | };
56 | }
57 |
58 | {{#if wantActionsAndReducer}}
59 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }});
60 | {{else}}
61 | export default connect(null, mapDispatchToProps)({{ properCase name }});
62 | {{/if}}
63 |
--------------------------------------------------------------------------------
/internals/generators/container/messages.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | * {{properCase name }} Messages
3 | *
4 | * This contains all the text for the {{properCase name }} component.
5 | */
6 | import { defineMessages } from 'react-intl';
7 |
8 | export default defineMessages({
9 | header: {
10 | id: 'app.containers.{{properCase name }}.header',
11 | defaultMessage: 'This is {{properCase name}} container !',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/internals/generators/container/reducer.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} reducer
4 | *
5 | */
6 |
7 | import { fromJS } from 'immutable';
8 | import {
9 | DEFAULT_ACTION,
10 | } from './constants';
11 |
12 | const initialState = fromJS({});
13 |
14 | function {{ camelCase name }}Reducer(state = initialState, action) {
15 | switch (action.type) {
16 | case DEFAULT_ACTION:
17 | return state;
18 | default:
19 | return state;
20 | }
21 | }
22 |
23 | export default {{ camelCase name }}Reducer;
24 |
--------------------------------------------------------------------------------
/internals/generators/container/reducer.test.js.hbs:
--------------------------------------------------------------------------------
1 |
2 | import { fromJS } from 'immutable';
3 | import {{ camelCase name }}Reducer from '../reducer';
4 |
5 | describe('{{ camelCase name }}Reducer', () => {
6 | it('returns the initial state', () => {
7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({}));
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/internals/generators/container/saga.js.hbs:
--------------------------------------------------------------------------------
1 | // import { take, call, put, select } from 'redux-saga/effects';
2 |
3 | // Individual exports for testing
4 | export default function* defaultSaga() {
5 | // See example in containers/HomePage/saga.js
6 | }
7 |
--------------------------------------------------------------------------------
/internals/generators/container/saga.test.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | * Test sagas
3 | */
4 |
5 | /* eslint-disable redux-saga/yield-effects */
6 | // import { take, call, put, select } from 'redux-saga/effects';
7 | // import { defaultSaga } from '../saga';
8 |
9 | // const generator = defaultSaga();
10 |
11 | describe('defaultSaga Saga', () => {
12 | it('Expect to have unit tests specified', () => {
13 | expect(true).toEqual(false);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/internals/generators/container/selectors.js.hbs:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | /**
4 | * Direct selector to the {{ camelCase name }} state domain
5 | */
6 | const select{{ properCase name }}Domain = (state) => state.get('{{ camelCase name }}');
7 |
8 | /**
9 | * Other specific selectors
10 | */
11 |
12 |
13 | /**
14 | * Default selector used by {{ properCase name }}
15 | */
16 |
17 | const makeSelect{{ properCase name }} = () => createSelector(
18 | select{{ properCase name }}Domain,
19 | (substate) => substate.toJS()
20 | );
21 |
22 | export default makeSelect{{ properCase name }};
23 | export {
24 | select{{ properCase name }}Domain,
25 | };
26 |
--------------------------------------------------------------------------------
/internals/generators/container/selectors.test.js.hbs:
--------------------------------------------------------------------------------
1 | // import { fromJS } from 'immutable';
2 | // import { select{{ properCase name }}Domain } from '../selectors';
3 |
4 | describe('select{{ properCase name }}Domain', () => {
5 | it('Expect to have unit tests specified', () => {
6 | expect(true).toEqual(false);
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/internals/generators/container/stateless.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | {{#if wantHeaders}}
11 | import { Helmet } from 'react-helmet';
12 | {{/if}}
13 | {{#if wantMessages}}
14 | import { FormattedMessage } from 'react-intl';
15 | {{/if}}
16 | {{#if wantActionsAndReducer}}
17 | import { createStructuredSelector } from 'reselect';
18 | {{/if}}
19 | import { compose } from 'redux';
20 |
21 | {{#if wantSaga}}
22 | import injectSaga from 'utils/injectSaga';
23 | {{/if}}
24 | {{#if wantActionsAndReducer}}
25 | import injectReducer from 'utils/injectReducer';
26 | import makeSelect{{properCase name}} from './selectors';
27 | import reducer from './reducer';
28 | {{/if}}
29 | {{#if wantSaga}}
30 | import saga from './saga';
31 | {{/if}}
32 | {{#if wantMessages}}
33 | import messages from './messages';
34 | {{/if}}
35 |
36 | function {{ properCase name }}() {
37 | return (
38 |
39 | {{#if wantHeaders}}
40 |
41 | {{properCase name}}
42 |
43 |
44 | {{/if}}
45 | {{#if wantMessages}}
46 |
47 | {{/if}}
48 |
49 | );
50 | }
51 |
52 | {{ properCase name }}.propTypes = {
53 | dispatch: PropTypes.func.isRequired,
54 | };
55 |
56 | {{#if wantActionsAndReducer}}
57 | const mapStateToProps = createStructuredSelector({
58 | {{ lowerCase name }}: makeSelect{{properCase name}}(),
59 | });
60 | {{/if}}
61 |
62 | function mapDispatchToProps(dispatch) {
63 | return {
64 | dispatch,
65 | };
66 | }
67 |
68 | {{#if wantActionsAndReducer}}
69 | const withConnect = connect(mapStateToProps, mapDispatchToProps);
70 |
71 | const withReducer = injectReducer({ key: '{{ lowerCase name }}', reducer });
72 | {{else}}
73 | const withConnect = connect(null, mapDispatchToProps);
74 | {{/if}}
75 | {{#if wantSaga}}
76 | const withSaga = injectSaga({ key: '{{ lowerCase name }}', saga });
77 | {{/if}}
78 |
79 | export default compose(
80 | {{#if wantActionsAndReducer}}
81 | withReducer,
82 | {{/if}}
83 | {{#if wantSaga}}
84 | withSaga,
85 | {{/if}}
86 | withConnect,
87 | )({{ properCase name }});
88 |
--------------------------------------------------------------------------------
/internals/generators/container/test.js.hbs:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { shallow } from 'enzyme';
3 |
4 | // import { {{ properCase name }} } from '../index';
5 |
6 | describe('<{{ properCase name }} />', () => {
7 | it('Expect to have unit tests specified', () => {
8 | expect(true).toEqual(false);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/internals/generators/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * generator/index.js
3 | *
4 | * Exports the generators so plop knows them
5 | */
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const componentGenerator = require('./component/index.js');
10 | const containerGenerator = require('./container/index.js');
11 | const languageGenerator = require('./language/index.js');
12 |
13 | module.exports = (plop) => {
14 | plop.setGenerator('component', componentGenerator);
15 | plop.setGenerator('container', containerGenerator);
16 | plop.setGenerator('language', languageGenerator);
17 | plop.addHelper('directory', (comp) => {
18 | try {
19 | fs.accessSync(path.join(__dirname, `../../app/containers/${comp}`), fs.F_OK);
20 | return `containers/${comp}`;
21 | } catch (e) {
22 | return `components/${comp}`;
23 | }
24 | });
25 | plop.addHelper('curly', (object, open) => (open ? '{' : '}'));
26 | };
27 |
--------------------------------------------------------------------------------
/internals/generators/language/add-locale-data.hbs:
--------------------------------------------------------------------------------
1 | $1addLocaleData({{language}}LocaleData);
2 |
--------------------------------------------------------------------------------
/internals/generators/language/app-locale.hbs:
--------------------------------------------------------------------------------
1 | $1 '{{language}}',
2 |
--------------------------------------------------------------------------------
/internals/generators/language/format-translation-messages.hbs:
--------------------------------------------------------------------------------
1 | $1 {{language}}: formatTranslationMessages('{{language}}', {{language}}TranslationMessages),
2 |
--------------------------------------------------------------------------------
/internals/generators/language/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Language Generator
3 | */
4 | const fs = require('fs');
5 | const exec = require('child_process').exec;
6 |
7 | function languageIsSupported(language) {
8 | try {
9 | fs.accessSync(`app/translations/${language}.json`, fs.F_OK);
10 | return true;
11 | } catch (e) {
12 | return false;
13 | }
14 | }
15 |
16 | module.exports = {
17 | description: 'Add a language',
18 | prompts: [{
19 | type: 'input',
20 | name: 'language',
21 | message: 'What is the language you want to add i18n support for (e.g. "fr", "de")?',
22 | default: 'fr',
23 | validate: (value) => {
24 | if ((/.+/).test(value) && value.length === 2) {
25 | return languageIsSupported(value) ? `The language "${value}" is already supported.` : true;
26 | }
27 |
28 | return '2 character language specifier is required';
29 | },
30 | }],
31 |
32 | actions: () => {
33 | const actions = [];
34 | actions.push({
35 | type: 'modify',
36 | path: '../../app/i18n.js',
37 | pattern: /('react-intl\/locale-data\/[a-z]+';\n)(?!.*'react-intl\/locale-data\/[a-z]+';)/g,
38 | templateFile: './language/intl-locale-data.hbs',
39 | });
40 | actions.push({
41 | type: 'modify',
42 | path: '../../app/i18n.js',
43 | pattern: /(\s+'[a-z]+',\n)(?!.*\s+'[a-z]+',)/g,
44 | templateFile: './language/app-locale.hbs',
45 | });
46 | actions.push({
47 | type: 'modify',
48 | path: '../../app/i18n.js',
49 | pattern: /(from\s'.\/translations\/[a-z]+.json';\n)(?!.*from\s'.\/translations\/[a-z]+.json';)/g,
50 | templateFile: './language/translation-messages.hbs',
51 | });
52 | actions.push({
53 | type: 'modify',
54 | path: '../../app/i18n.js',
55 | pattern: /(addLocaleData\([a-z]+LocaleData\);\n)(?!.*addLocaleData\([a-z]+LocaleData\);)/g,
56 | templateFile: './language/add-locale-data.hbs',
57 | });
58 | actions.push({
59 | type: 'modify',
60 | path: '../../app/i18n.js',
61 | pattern: /([a-z]+:\sformatTranslationMessages\('[a-z]+',\s[a-z]+TranslationMessages\),\n)(?!.*[a-z]+:\sformatTranslationMessages\('[a-z]+',\s[a-z]+TranslationMessages\),)/g,
62 | templateFile: './language/format-translation-messages.hbs',
63 | });
64 | actions.push({
65 | type: 'add',
66 | path: '../../app/translations/{{language}}.json',
67 | templateFile: './language/translations-json.hbs',
68 | abortOnFail: true,
69 | });
70 | actions.push({
71 | type: 'modify',
72 | path: '../../app/app.js',
73 | pattern: /(import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),\n)(?!.*import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),)/g,
74 | templateFile: './language/polyfill-intl-locale.hbs',
75 | });
76 | actions.push(
77 | () => {
78 | const cmd = 'npm run extract-intl';
79 | exec(cmd, (err, result, stderr) => {
80 | if (err || stderr) {
81 | throw err || stderr;
82 | }
83 | process.stdout.write(result);
84 | });
85 | return 'modify translation messages';
86 | }
87 | );
88 |
89 | return actions;
90 | },
91 | };
92 |
--------------------------------------------------------------------------------
/internals/generators/language/intl-locale-data.hbs:
--------------------------------------------------------------------------------
1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}';
2 |
--------------------------------------------------------------------------------
/internals/generators/language/polyfill-intl-locale.hbs:
--------------------------------------------------------------------------------
1 | $1 import('intl/locale-data/jsonp/{{language}}.js'),
2 |
--------------------------------------------------------------------------------
/internals/generators/language/translation-messages.hbs:
--------------------------------------------------------------------------------
1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json';
2 |
--------------------------------------------------------------------------------
/internals/generators/language/translations-json.hbs:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/internals/generators/utils/componentExists.js:
--------------------------------------------------------------------------------
1 | /**
2 | * componentExists
3 | *
4 | * Check whether the given component exist in either the components or containers directory
5 | */
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const pageComponents = fs.readdirSync(path.join(__dirname, '../../../app/components'));
10 | const pageContainers = fs.readdirSync(path.join(__dirname, '../../../app/containers'));
11 | const components = pageComponents.concat(pageContainers);
12 |
13 | function componentExists(comp) {
14 | return components.indexOf(comp) >= 0;
15 | }
16 |
17 | module.exports = componentExists;
18 |
--------------------------------------------------------------------------------
/internals/mocks/cssModule.js:
--------------------------------------------------------------------------------
1 | module.exports = 'CSS_MODULE';
2 |
--------------------------------------------------------------------------------
/internals/mocks/image.js:
--------------------------------------------------------------------------------
1 | module.exports = 'IMAGE_MOCK';
2 |
--------------------------------------------------------------------------------
/internals/scripts/analyze.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const shelljs = require('shelljs');
4 | const animateProgress = require('./helpers/progress');
5 | const chalk = require('chalk');
6 | const addCheckMark = require('./helpers/checkmark');
7 |
8 | const progress = animateProgress('Generating stats');
9 |
10 | // Generate stats.json file with webpack
11 | shelljs.exec(
12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json',
13 | addCheckMark.bind(null, callback) // Output a checkmark on completion
14 | );
15 |
16 | // Called after webpack has finished generating the stats.json file
17 | function callback() {
18 | clearInterval(progress);
19 | process.stdout.write(
20 | '\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' +
21 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n')
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/internals/scripts/clean.js:
--------------------------------------------------------------------------------
1 | const shell = require('shelljs');
2 | const addCheckMark = require('./helpers/checkmark.js');
3 |
4 | if (!shell.which('git')) {
5 | shell.echo('Sorry, this script requires git');
6 | shell.exit(1);
7 | }
8 |
9 | if (!shell.test('-e', 'internals/templates')) {
10 | shell.echo('The example is deleted already.');
11 | shell.exit(1);
12 | }
13 |
14 | process.stdout.write('Cleanup started...');
15 |
16 | // Reuse existing LanguageProvider and i18n tests
17 | shell.mv('app/containers/LanguageProvider/tests', 'internals/templates/containers/LanguageProvider');
18 | shell.cp('app/tests/i18n.test.js', 'internals/templates/tests/i18n.test.js');
19 |
20 | // Cleanup components/
21 | shell.rm('-rf', 'app/components/*');
22 |
23 | // Handle containers/
24 | shell.rm('-rf', 'app/containers');
25 | shell.mv('internals/templates/containers', 'app');
26 |
27 | // Handle tests/
28 | shell.mv('internals/templates/tests', 'app');
29 |
30 | // Handle translations/
31 | shell.rm('-rf', 'app/translations')
32 | shell.mv('internals/templates/translations', 'app');
33 |
34 | // Handle utils/
35 | shell.rm('-rf', 'app/utils');
36 | shell.mv('internals/templates/utils', 'app')
37 |
38 | // Replace the files in the root app/ folder
39 | shell.cp('internals/templates/app.js', 'app/app.js');
40 | shell.cp('internals/templates/global-styles.js', 'app/global-styles.js');
41 | shell.cp('internals/templates/i18n.js', 'app/i18n.js');
42 | shell.cp('internals/templates/index.html', 'app/index.html');
43 | shell.cp('internals/templates/reducers.js', 'app/reducers.js');
44 | shell.cp('internals/templates/configureStore.js', 'app/configureStore.js');
45 |
46 | // Remove the templates folder
47 | shell.rm('-rf', 'internals/templates');
48 |
49 | addCheckMark();
50 |
51 | // Commit the changes
52 | if (shell.exec('git add . --all && git commit -qm "Remove default example"').code !== 0) {
53 | shell.echo('\nError: Git commit failed');
54 | shell.exit(1);
55 | }
56 |
57 | shell.echo('\nCleanup done. Happy Coding!!!');
58 |
--------------------------------------------------------------------------------
/internals/scripts/dependencies.js:
--------------------------------------------------------------------------------
1 | // No need to build the DLL in production
2 | if (process.env.NODE_ENV === 'production') {
3 | process.exit(0);
4 | }
5 |
6 | require('shelljs/global');
7 |
8 | const path = require('path');
9 | const fs = require('fs');
10 | const exists = fs.existsSync;
11 | const writeFile = fs.writeFileSync;
12 |
13 | const defaults = require('lodash/defaultsDeep');
14 | const pkg = require(path.join(process.cwd(), 'package.json'));
15 | const config = require('../config');
16 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults);
17 | const outputPath = path.join(process.cwd(), dllConfig.path);
18 | const dllManifestPath = path.join(outputPath, 'package.json');
19 |
20 | /**
21 | * I use node_modules/react-boilerplate-dlls by default just because
22 | * it isn't going to be version controlled and babel wont try to parse it.
23 | */
24 | mkdir('-p', outputPath);
25 |
26 | echo('Building the Webpack DLL...');
27 |
28 | /**
29 | * Create a manifest so npm install doesn't warn us
30 | */
31 | if (!exists(dllManifestPath)) {
32 | writeFile(
33 | dllManifestPath,
34 | JSON.stringify(defaults({
35 | name: 'react-boilerplate-dlls',
36 | private: true,
37 | author: pkg.author,
38 | repository: pkg.repository,
39 | version: pkg.version,
40 | }), null, 2),
41 | 'utf8'
42 | );
43 | }
44 |
45 | // the BUILDING_DLL env var is set to avoid confusing the development environment
46 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js --hide-modules');
47 |
--------------------------------------------------------------------------------
/internals/scripts/extract-intl.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * This script will extract the internationalization messages from all components
4 | and package them in the translation json files in the translations file.
5 | */
6 | const fs = require('fs');
7 | const nodeGlob = require('glob');
8 | const transform = require('babel-core').transform;
9 |
10 | const animateProgress = require('./helpers/progress');
11 | const addCheckmark = require('./helpers/checkmark');
12 |
13 | const pkg = require('../../package.json');
14 | const presets = pkg.babel.presets;
15 | const plugins = pkg.babel.plugins || [];
16 |
17 | const i18n = require('../../app/i18n');
18 | import { DEFAULT_LOCALE } from '../../app/containers/App/constants';
19 |
20 | require('shelljs/global');
21 |
22 | // Glob to match all js files except test files
23 | const FILES_TO_PARSE = 'app/**/!(*.test).js';
24 | const locales = i18n.appLocales;
25 |
26 | const newLine = () => process.stdout.write('\n');
27 |
28 | // Progress Logger
29 | let progress;
30 | const task = (message) => {
31 | progress = animateProgress(message);
32 | process.stdout.write(message);
33 |
34 | return (error) => {
35 | if (error) {
36 | process.stderr.write(error);
37 | }
38 | clearTimeout(progress);
39 | return addCheckmark(() => newLine());
40 | }
41 | }
42 |
43 | // Wrap async functions below into a promise
44 | const glob = (pattern) => new Promise((resolve, reject) => {
45 | nodeGlob(pattern, (error, value) => (error ? reject(error) : resolve(value)));
46 | });
47 |
48 | const readFile = (fileName) => new Promise((resolve, reject) => {
49 | fs.readFile(fileName, (error, value) => (error ? reject(error) : resolve(value)));
50 | });
51 |
52 | const writeFile = (fileName, data) => new Promise((resolve, reject) => {
53 | fs.writeFile(fileName, data, (error, value) => (error ? reject(error) : resolve(value)));
54 | });
55 |
56 | // Store existing translations into memory
57 | const oldLocaleMappings = [];
58 | const localeMappings = [];
59 | // Loop to run once per locale
60 | for (const locale of locales) {
61 | oldLocaleMappings[locale] = {};
62 | localeMappings[locale] = {};
63 | // File to store translation messages into
64 | const translationFileName = `app/translations/${locale}.json`;
65 | try {
66 | // Parse the old translation message JSON files
67 | const messages = JSON.parse(fs.readFileSync(translationFileName));
68 | const messageKeys = Object.keys(messages);
69 | for (const messageKey of messageKeys) {
70 | oldLocaleMappings[locale][messageKey] = messages[messageKey];
71 | }
72 | } catch (error) {
73 | if (error.code !== 'ENOENT') {
74 | process.stderr.write(
75 | `There was an error loading this translation file: ${translationFileName}
76 | \n${error}`
77 | );
78 | }
79 | }
80 | }
81 |
82 | /* push `react-intl` plugin to the existing plugins that are already configured in `package.json`
83 | Example:
84 | ```
85 | "babel": {
86 | "plugins": [
87 | ["transform-object-rest-spread", { "useBuiltIns": true }]
88 | ],
89 | "presets": [
90 | "env",
91 | "react"
92 | ]
93 | }
94 | ```
95 | */
96 | plugins.push(['react-intl'])
97 |
98 | const extractFromFile = async (fileName) => {
99 | try {
100 | const code = await readFile(fileName);
101 | // Use babel plugin to extract instances where react-intl is used
102 | const { metadata: result } = await transform(code, { presets, plugins }); // object-shorthand
103 | for (const message of result['react-intl'].messages) {
104 | for (const locale of locales) {
105 | const oldLocaleMapping = oldLocaleMappings[locale][message.id];
106 | // Merge old translations into the babel extracted instances where react-intl is used
107 | const newMsg = ( locale === DEFAULT_LOCALE) ? message.defaultMessage : '';
108 | localeMappings[locale][message.id] = (oldLocaleMapping)
109 | ? oldLocaleMapping
110 | : newMsg;
111 | }
112 | }
113 | } catch (error) {
114 | process.stderr.write(`Error transforming file: ${fileName}\n${error}`);
115 | }
116 | };
117 |
118 | (async function main() {
119 | const memoryTaskDone = task('Storing language files in memory');
120 | const files = await glob(FILES_TO_PARSE);
121 | memoryTaskDone()
122 |
123 | const extractTaskDone = task('Run extraction on all files');
124 | // Run extraction on all files that match the glob on line 16
125 | await Promise.all(files.map((fileName) => extractFromFile(fileName)));
126 | extractTaskDone()
127 |
128 | // Make the directory if it doesn't exist, especially for first run
129 | mkdir('-p', 'app/translations');
130 | for (const locale of locales) {
131 | const translationFileName = `app/translations/${locale}.json`;
132 |
133 | try {
134 | const localeTaskDone = task(
135 | `Writing translation messages for ${locale} to: ${translationFileName}`
136 | );
137 |
138 | // Sort the translation JSON file so that git diffing is easier
139 | // Otherwise the translation messages will jump around every time we extract
140 | let messages = {};
141 | Object.keys(localeMappings[locale]).sort().forEach(function(key) {
142 | messages[key] = localeMappings[locale][key];
143 | });
144 |
145 | // Write to file the JSON representation of the translation messages
146 | const prettified = `${JSON.stringify(messages, null, 2)}\n`;
147 |
148 | await writeFile(translationFileName, prettified);
149 |
150 | localeTaskDone();
151 | } catch (error) {
152 | localeTaskDone(
153 | `There was an error saving this translation file: ${translationFileName}
154 | \n${error}`
155 | );
156 | }
157 | }
158 |
159 | process.exit()
160 | }());
161 |
--------------------------------------------------------------------------------
/internals/scripts/generate-templates-for-linting.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This script is for internal `react-boilerplate`'s usage. The only purpose of generating all of these templates is
3 | * to be able to lint them and detect critical errors. Every generated component's name has to start with
4 | * 'RbGenerated' so it can be easily excluded from the test coverage reports.
5 | */
6 |
7 | const nodePlop = require('node-plop');
8 | const path = require('path');
9 | const chalk = require('chalk');
10 | const rimraf = require('rimraf');
11 |
12 | const xmark = require('./helpers/xmark');
13 |
14 | process.chdir(path.join(__dirname, '../generators'));
15 |
16 | const prettyStringify = (data) => JSON.stringify(data, null, 2);
17 |
18 | const checkForErrors = (result) => {
19 | if (Array.isArray(result.failures) && result.failures.length > 0) {
20 | throw result.failures;
21 | }
22 | };
23 |
24 | const reportErrorsFor = (title) => (err) => {
25 | // TODO Replace with our own helpers/log that is guaranteed to be blocking?
26 | xmark(() => console.error(chalk.red(` ERROR generating '${title}': `), prettyStringify(err)));
27 | process.exit(1);
28 | };
29 |
30 | // Generated tests are designed to fail, which would in turn fail CI builds
31 | const removeTestsDirFrom = (relativePath) => () => rimraf.sync(path.join(__dirname, '/../../app/', relativePath, '/tests'));
32 |
33 | const plop = nodePlop('./index');
34 |
35 | const componentGen = plop.getGenerator('component');
36 | componentGen.runActions({ name: 'RbGeneratedComponentEsclass', type: 'React.Component', wantMessages: true, wantLoadable: true, })
37 | .then(checkForErrors)
38 | .then(removeTestsDirFrom('components/RbGeneratedComponentEsclass'))
39 | .catch(reportErrorsFor('component/React.Component'));
40 |
41 | componentGen.runActions({ name: 'RbGeneratedComponentEsclasspure', type: 'React.PureComponent', wantMessages: true, wantLoadable: true })
42 | .then(checkForErrors)
43 | .then(removeTestsDirFrom('components/RbGeneratedComponentEsclasspure'))
44 | .catch(reportErrorsFor('component/React.PureComponent'));
45 |
46 | componentGen.runActions({ name: 'RbGeneratedComponentStatelessfunction', type: 'Stateless Function', wantMessages: true, wantLoadable: true })
47 | .then(checkForErrors)
48 | .then(removeTestsDirFrom('components/RbGeneratedComponentStatelessfunction'))
49 | .catch(reportErrorsFor('component/Stateless Function'));
50 |
51 | const containerGen = plop.getGenerator('container');
52 | containerGen.runActions({
53 | name: 'RbGeneratedContainerPureComponent',
54 | type: 'React.PureComponent',
55 | wantHeaders: true,
56 | wantActionsAndReducer: true,
57 | wantSagas: true,
58 | wantMessages: true,
59 | wantLoadable: true,
60 | })
61 | .then(checkForErrors)
62 | .then(removeTestsDirFrom('containers/RbGeneratedContainerPureComponent'))
63 | .catch(reportErrorsFor('container/React.PureComponent'));
64 |
65 | containerGen.runActions({
66 | name: 'RbGeneratedContainerComponent',
67 | type: 'React.Component',
68 | wantHeaders: true,
69 | wantActionsAndReducer: true,
70 | wantSagas: true,
71 | wantMessages: true,
72 | wantLoadable: true,
73 | })
74 | .then(checkForErrors)
75 | .then(removeTestsDirFrom('containers/RbGeneratedContainerComponent'))
76 | .catch(reportErrorsFor('container/React.Component'));
77 |
78 | containerGen.runActions({
79 | name: 'RbGeneratedContainerStateless',
80 | type: 'Stateless Function',
81 | wantHeaders: true,
82 | wantActionsAndReducer: true,
83 | wantSagas: true,
84 | wantMessages: true,
85 | wantLoadable: true,
86 | })
87 | .then(checkForErrors)
88 | .then(removeTestsDirFrom('containers/RbGeneratedContainerStateless'))
89 | .catch(reportErrorsFor('container/Stateless'));
90 |
91 | const languageGen = plop.getGenerator('language');
92 | languageGen.runActions({ language: 'fr' })
93 | .catch(reportErrorsFor('language'));
94 |
--------------------------------------------------------------------------------
/internals/scripts/helpers/checkmark.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 |
3 | /**
4 | * Adds mark check symbol
5 | */
6 | function addCheckMark(callback) {
7 | process.stdout.write(chalk.green(' ✓'));
8 | if (callback) callback();
9 | }
10 |
11 | module.exports = addCheckMark;
12 |
--------------------------------------------------------------------------------
/internals/scripts/helpers/progress.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const readline = require('readline');
4 |
5 | /**
6 | * Adds an animated progress indicator
7 | *
8 | * @param {string} message The message to write next to the indicator
9 | * @param {number} amountOfDots The amount of dots you want to animate
10 | */
11 | function animateProgress(message, amountOfDots) {
12 | if (typeof amountOfDots !== 'number') {
13 | amountOfDots = 3;
14 | }
15 |
16 | let i = 0;
17 | return setInterval(function() {
18 | readline.cursorTo(process.stdout, 0);
19 | i = (i + 1) % (amountOfDots + 1);
20 | const dots = new Array(i + 1).join('.');
21 | process.stdout.write(message + dots);
22 | }, 500);
23 | }
24 |
25 | module.exports = animateProgress;
26 |
--------------------------------------------------------------------------------
/internals/scripts/helpers/xmark.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 |
3 | /**
4 | * Adds mark cross symbol
5 | */
6 | function addXMark(callback) {
7 | process.stdout.write(chalk.red(' ✘'));
8 | if (callback) callback();
9 | }
10 |
11 | module.exports = addXMark;
12 |
--------------------------------------------------------------------------------
/internals/scripts/npmcheckversion.js:
--------------------------------------------------------------------------------
1 | const exec = require('child_process').exec;
2 | exec('npm -v', function (err, stdout, stderr) {
3 | if (err) throw err;
4 | if (parseFloat(stdout) < 3) {
5 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3');
6 | process.exit(1);
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/internals/testing/test-bundler.js:
--------------------------------------------------------------------------------
1 | // needed for regenerator-runtime
2 | // (ES7 generator support is required by redux-saga)
3 | import 'babel-polyfill';
4 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.base.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * COMMON WEBPACK CONFIGURATION
3 | */
4 |
5 | const path = require('path');
6 | const webpack = require('webpack');
7 |
8 | // Remove this line once the following warning goes away (it was meant for webpack loader authors not users):
9 | // 'DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic,
10 | // see https://github.com/webpack/loader-utils/issues/56 parseQuery() will be replaced with getOptions()
11 | // in the next major version of loader-utils.'
12 | process.noDeprecation = true;
13 |
14 | module.exports = (options) => ({
15 | entry: options.entry,
16 | output: Object.assign({ // Compile into js/build.js
17 | path: path.resolve(process.cwd(), 'build'),
18 | publicPath: '/',
19 | }, options.output), // Merge with env dependent settings
20 | module: {
21 | rules: [
22 | {
23 | test: /\.js$/, // Transform all .js files required somewhere with Babel
24 | exclude: /node_modules/,
25 | use: {
26 | loader: 'babel-loader',
27 | options: options.babelQuery,
28 | },
29 | },
30 | {
31 | // Preprocess our own .css files
32 | // This is the place to add your own loaders (e.g. sass/less etc.)
33 | // for a list of loaders, see https://webpack.js.org/loaders/#styling
34 | test: /\.css$/,
35 | exclude: /node_modules/,
36 | use: ['style-loader', 'css-loader'],
37 | },
38 | {
39 | // Preprocess 3rd party .css files located in node_modules
40 | test: /\.css$/,
41 | include: /node_modules/,
42 | use: ['style-loader', 'css-loader'],
43 | },
44 | {
45 | test: /\.(eot|svg|otf|ttf|woff|woff2)$/,
46 | use: 'file-loader',
47 | },
48 | {
49 | test: /\.(jpg|png|gif)$/,
50 | use: [
51 | 'file-loader',
52 | {
53 | loader: 'image-webpack-loader',
54 | options: {
55 | progressive: true,
56 | optimizationLevel: 7,
57 | interlaced: false,
58 | pngquant: {
59 | quality: '65-90',
60 | speed: 4,
61 | },
62 | },
63 | },
64 | ],
65 | },
66 | {
67 | test: /\.html$/,
68 | use: 'html-loader',
69 | },
70 | {
71 | test: /\.json$/,
72 | use: 'json-loader',
73 | },
74 | {
75 | test: /\.(mp4|webm)$/,
76 | use: {
77 | loader: 'url-loader',
78 | options: {
79 | limit: 10000,
80 | },
81 | },
82 | },
83 | ],
84 | },
85 | plugins: options.plugins.concat([
86 | new webpack.ProvidePlugin({
87 | // make fetch available
88 | fetch: 'exports-loader?self.fetch!whatwg-fetch',
89 | }),
90 |
91 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV`
92 | // inside your code for any environment checks; UglifyJS will automatically
93 | // drop any unreachable code.
94 | new webpack.DefinePlugin({
95 | 'process.env': {
96 | NODE_ENV: JSON.stringify(process.env.NODE_ENV),
97 | },
98 | }),
99 | new webpack.NamedModulesPlugin(),
100 | ]),
101 | resolve: {
102 | modules: ['app', 'node_modules'],
103 | extensions: [
104 | '.js',
105 | '.jsx',
106 | '.react.js',
107 | ],
108 | mainFields: [
109 | 'browser',
110 | 'jsnext:main',
111 | 'main',
112 | ],
113 | },
114 | devtool: options.devtool,
115 | target: 'web', // Make web variables accessible to webpack, e.g. window
116 | performance: options.performance || {},
117 | });
118 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.dev.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * DEVELOPMENT WEBPACK CONFIGURATION
3 | */
4 |
5 | const path = require('path');
6 | const fs = require('fs');
7 | const glob = require('glob');
8 | const webpack = require('webpack');
9 | const HtmlWebpackPlugin = require('html-webpack-plugin');
10 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
11 | const CircularDependencyPlugin = require('circular-dependency-plugin');
12 | const logger = require('../../server/logger');
13 | const pkg = require(path.resolve(process.cwd(), 'package.json'));
14 | const dllPlugin = pkg.dllPlugin;
15 |
16 | const plugins = [
17 | new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading
18 | new webpack.NoEmitOnErrorsPlugin(),
19 | new HtmlWebpackPlugin({
20 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js
21 | template: 'app/index.html',
22 | }),
23 | new CircularDependencyPlugin({
24 | exclude: /a\.js|node_modules/, // exclude node_modules
25 | failOnError: false, // show a warning when there is a circular dependency
26 | }),
27 | ];
28 |
29 | if (dllPlugin) {
30 | glob.sync(`${dllPlugin.path}/*.dll.js`).forEach((dllPath) => {
31 | plugins.push(
32 | new AddAssetHtmlPlugin({
33 | filepath: dllPath,
34 | includeSourcemap: false,
35 | })
36 | );
37 | });
38 | }
39 |
40 | module.exports = require('./webpack.base.babel')({
41 | // Add hot reloading in development
42 | entry: [
43 | 'eventsource-polyfill', // Necessary for hot reloading with IE
44 | 'webpack-hot-middleware/client?reload=true',
45 | path.join(process.cwd(), 'app/app.js'), // Start with js/app.js
46 | ],
47 |
48 | // Don't use hashes in dev mode for better performance
49 | output: {
50 | filename: '[name].js',
51 | chunkFilename: '[name].chunk.js',
52 | },
53 |
54 | // Add development plugins
55 | plugins: dependencyHandlers().concat(plugins), // eslint-disable-line no-use-before-define
56 |
57 | // Emit a source map for easier debugging
58 | // See https://webpack.js.org/configuration/devtool/#devtool
59 | devtool: 'eval-source-map',
60 |
61 | performance: {
62 | hints: false,
63 | },
64 | });
65 |
66 | /**
67 | * Select which plugins to use to optimize the bundle's handling of
68 | * third party dependencies.
69 | *
70 | * If there is a dllPlugin key on the project's package.json, the
71 | * Webpack DLL Plugin will be used. Otherwise the CommonsChunkPlugin
72 | * will be used.
73 | *
74 | */
75 | function dependencyHandlers() {
76 | // Don't do anything during the DLL Build step
77 | if (process.env.BUILDING_DLL) { return []; }
78 |
79 | // If the package.json does not have a dllPlugin property, use the CommonsChunkPlugin
80 | if (!dllPlugin) {
81 | return [
82 | new webpack.optimize.CommonsChunkPlugin({
83 | name: 'vendor',
84 | children: true,
85 | minChunks: 2,
86 | async: true,
87 | }),
88 | ];
89 | }
90 |
91 | const dllPath = path.resolve(process.cwd(), dllPlugin.path || 'node_modules/react-boilerplate-dlls');
92 |
93 | /**
94 | * If DLLs aren't explicitly defined, we assume all production dependencies listed in package.json
95 | * Reminder: You need to exclude any server side dependencies by listing them in dllConfig.exclude
96 | */
97 | if (!dllPlugin.dlls) {
98 | const manifestPath = path.resolve(dllPath, 'reactBoilerplateDeps.json');
99 |
100 | if (!fs.existsSync(manifestPath)) {
101 | logger.error('The DLL manifest is missing. Please run `npm run build:dll`');
102 | process.exit(0);
103 | }
104 |
105 | return [
106 | new webpack.DllReferencePlugin({
107 | context: process.cwd(),
108 | manifest: require(manifestPath), // eslint-disable-line global-require
109 | }),
110 | ];
111 | }
112 |
113 | // If DLLs are explicitly defined, we automatically create a DLLReferencePlugin for each of them.
114 | const dllManifests = Object.keys(dllPlugin.dlls).map((name) => path.join(dllPath, `/${name}.json`));
115 |
116 | return dllManifests.map((manifestPath) => {
117 | if (!fs.existsSync(path)) {
118 | if (!fs.existsSync(manifestPath)) {
119 | logger.error(`The following Webpack DLL manifest is missing: ${path.basename(manifestPath)}`);
120 | logger.error(`Expected to find it in ${dllPath}`);
121 | logger.error('Please run: npm run build:dll');
122 |
123 | process.exit(0);
124 | }
125 | }
126 |
127 | return new webpack.DllReferencePlugin({
128 | context: process.cwd(),
129 | manifest: require(manifestPath), // eslint-disable-line global-require
130 | });
131 | });
132 | }
133 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.dll.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WEBPACK DLL GENERATOR
3 | *
4 | * This profile is used to cache webpack's module
5 | * contexts for external library and framework type
6 | * dependencies which will usually not change often enough
7 | * to warrant building them from scratch every time we use
8 | * the webpack process.
9 | */
10 |
11 | const { join } = require('path');
12 | const defaults = require('lodash/defaultsDeep');
13 | const webpack = require('webpack');
14 | const pkg = require(join(process.cwd(), 'package.json'));
15 | const dllPlugin = require('../config').dllPlugin;
16 |
17 | if (!pkg.dllPlugin) { process.exit(0); }
18 |
19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults);
20 | const outputPath = join(process.cwd(), dllConfig.path);
21 |
22 | module.exports = require('./webpack.base.babel')({
23 | context: process.cwd(),
24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg),
25 | devtool: 'eval',
26 | output: {
27 | filename: '[name].dll.js',
28 | path: outputPath,
29 | library: '[name]',
30 | },
31 | plugins: [
32 | new webpack.DllPlugin({
33 | name: '[name]',
34 | path: join(outputPath, '[name].json'),
35 | }),
36 | ],
37 | performance: {
38 | hints: false,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.prod.babel.js:
--------------------------------------------------------------------------------
1 | // Important modules this config uses
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const OfflinePlugin = require('offline-plugin');
6 |
7 | module.exports = require('./webpack.base.babel')({
8 | // In production, we skip all hot-reloading stuff
9 | entry: [
10 | path.join(process.cwd(), 'app/app.js'),
11 | ],
12 |
13 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets
14 | output: {
15 | filename: '[name].[chunkhash].js',
16 | chunkFilename: '[name].[chunkhash].chunk.js',
17 | },
18 |
19 | plugins: [
20 | new webpack.optimize.ModuleConcatenationPlugin(),
21 | new webpack.optimize.CommonsChunkPlugin({
22 | name: 'vendor',
23 | children: true,
24 | minChunks: 2,
25 | async: true,
26 | }),
27 |
28 | // Minify and optimize the index.html
29 | new HtmlWebpackPlugin({
30 | template: 'app/index.html',
31 | minify: {
32 | removeComments: true,
33 | collapseWhitespace: true,
34 | removeRedundantAttributes: true,
35 | useShortDoctype: true,
36 | removeEmptyAttributes: true,
37 | removeStyleLinkTypeAttributes: true,
38 | keepClosingSlash: true,
39 | minifyJS: true,
40 | minifyCSS: true,
41 | minifyURLs: true,
42 | },
43 | inject: true,
44 | }),
45 |
46 | // Put it in the end to capture all the HtmlWebpackPlugin's
47 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin
48 | new OfflinePlugin({
49 | relativePaths: false,
50 | publicPath: '/',
51 |
52 | // No need to cache .htaccess. See http://mxs.is/googmp,
53 | // this is applied before any match in `caches` section
54 | excludes: ['.htaccess'],
55 |
56 | caches: {
57 | main: [':rest:'],
58 |
59 | // All chunks marked as `additional`, loaded after main section
60 | // and do not prevent SW to install. Change to `optional` if
61 | // do not want them to be preloaded at all (cached only when first loaded)
62 | additional: ['*.chunk.js'],
63 | },
64 |
65 | // Removes warning for about `additional` section usage
66 | safeToUseOptionalCaches: true,
67 |
68 | AppCache: false,
69 | }),
70 | ],
71 |
72 | performance: {
73 | assetFilter: (assetFilename) => !(/(\.map$)|(^(main\.|favicon\.))/.test(assetFilename)),
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/server/argv.js:
--------------------------------------------------------------------------------
1 | module.exports = require('minimist')(process.argv.slice(2));
2 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /* eslint consistent-return:0 */
2 |
3 | const express = require('express');
4 | const logger = require('./logger');
5 |
6 | const argv = require('./argv');
7 | const port = require('./port');
8 | const setup = require('./middlewares/frontendMiddleware');
9 | const isDev = process.env.NODE_ENV !== 'production';
10 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false;
11 | const resolve = require('path').resolve;
12 | const app = express();
13 |
14 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here
15 | // app.use('/api', myApi);
16 |
17 | // In production we need to pass these values in instead of relying on webpack
18 | setup(app, {
19 | outputPath: resolve(process.cwd(), 'build'),
20 | publicPath: '/',
21 | });
22 |
23 | // get the intended host and port number, use localhost and port 3000 if not provided
24 | const customHost = argv.host || process.env.HOST;
25 | const host = customHost || null; // Let http.Server use its default IPv6/4 host
26 | const prettyHost = customHost || 'localhost';
27 |
28 | // Start your app.
29 | app.listen(port, host, (err) => {
30 | if (err) {
31 | return logger.error(err.message);
32 | }
33 |
34 | // Connect to ngrok in dev mode
35 | if (ngrok) {
36 | ngrok.connect(port, (innerErr, url) => {
37 | if (innerErr) {
38 | return logger.error(innerErr);
39 | }
40 |
41 | logger.appStarted(port, prettyHost, url);
42 | });
43 | } else {
44 | logger.appStarted(port, prettyHost);
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/server/logger.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const chalk = require('chalk');
4 | const ip = require('ip');
5 |
6 | const divider = chalk.gray('\n-----------------------------------');
7 |
8 | /**
9 | * Logger middleware, you can customize it to make messages more personal
10 | */
11 | const logger = {
12 |
13 | // Called whenever there's an error on the server we want to print
14 | error: (err) => {
15 | console.error(chalk.red(err));
16 | },
17 |
18 | // Called when express.js app starts on given port w/o errors
19 | appStarted: (port, host, tunnelStarted) => {
20 | console.log(`Server started ! ${chalk.green('✓')}`);
21 |
22 | // If the tunnel started, log that and the URL it's available at
23 | if (tunnelStarted) {
24 | console.log(`Tunnel initialised ${chalk.green('✓')}`);
25 | }
26 |
27 | console.log(`
28 | ${chalk.bold('Access URLs:')}${divider}
29 | Localhost: ${chalk.magenta(`http://${host}:${port}`)}
30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) +
31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider}
32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)}
33 | `);
34 | },
35 | };
36 |
37 | module.exports = logger;
38 |
--------------------------------------------------------------------------------
/server/middlewares/addDevMiddlewares.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const webpackDevMiddleware = require('webpack-dev-middleware');
4 | const webpackHotMiddleware = require('webpack-hot-middleware');
5 |
6 | function createWebpackMiddleware(compiler, publicPath) {
7 | return webpackDevMiddleware(compiler, {
8 | noInfo: true,
9 | publicPath,
10 | silent: true,
11 | stats: 'errors-only',
12 | });
13 | }
14 |
15 | module.exports = function addDevMiddlewares(app, webpackConfig) {
16 | const compiler = webpack(webpackConfig);
17 | const middleware = createWebpackMiddleware(compiler, webpackConfig.output.publicPath);
18 |
19 | app.use(middleware);
20 | app.use(webpackHotMiddleware(compiler));
21 |
22 | // Since webpackDevMiddleware uses memory-fs internally to store build
23 | // artifacts, we use it instead
24 | const fs = middleware.fileSystem;
25 |
26 | app.get('*', (req, res) => {
27 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => {
28 | if (err) {
29 | res.sendStatus(404);
30 | } else {
31 | res.send(file.toString());
32 | }
33 | });
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/server/middlewares/addProdMiddlewares.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const compression = require('compression');
4 |
5 | module.exports = function addProdMiddlewares(app, options) {
6 | const publicPath = options.publicPath || '/';
7 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build');
8 |
9 | // compression middleware compresses your server responses which makes them
10 | // smaller (applies also to assets). You can read more about that technique
11 | // and other good practices on official Express.js docs http://mxs.is/googmy
12 | app.use(compression());
13 | app.use(publicPath, express.static(outputPath));
14 |
15 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html')));
16 | };
17 |
--------------------------------------------------------------------------------
/server/middlewares/frontendMiddleware.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 |
3 | /**
4 | * Front-end middleware
5 | */
6 | module.exports = (app, options) => {
7 | const isProd = process.env.NODE_ENV === 'production';
8 |
9 | if (isProd) {
10 | const addProdMiddlewares = require('./addProdMiddlewares');
11 | addProdMiddlewares(app, options);
12 | } else {
13 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel');
14 | const addDevMiddlewares = require('./addDevMiddlewares');
15 | addDevMiddlewares(app, webpackConfig);
16 | }
17 |
18 | return app;
19 | };
20 |
--------------------------------------------------------------------------------
/server/port.js:
--------------------------------------------------------------------------------
1 | const argv = require('./argv');
2 |
3 | module.exports = parseInt(argv.port || process.env.PORT || '3000', 10);
4 |
--------------------------------------------------------------------------------