├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmrc ├── .scss-lint.yml ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── dev.js └── prod.js ├── index.js ├── nodemon.json ├── package.json ├── src ├── actions │ ├── IntlActionCreators.js │ └── PhotoActionCreators.js ├── app.js ├── client.js ├── components │ ├── Footer.js │ ├── LocaleSwitcher.js │ ├── Logo.js │ ├── NavBar.js │ ├── Page.js │ ├── Photo.js │ ├── PhotoAttribution.js │ ├── PhotoCreatedAt.js │ ├── PhotoMeta.js │ ├── PhotoRating.js │ └── Thumbnail.js ├── config.js ├── constants │ ├── Actions.js │ └── features.js ├── containers │ ├── ErrorPage.js │ ├── FeaturedPage.js │ ├── HomePage.js │ ├── Html.js │ ├── InitActions.js │ ├── LoadingPage.js │ ├── NotFoundPage.js │ ├── PhotoPage.js │ └── Root.js ├── intl │ ├── en.js │ ├── fr.js │ ├── it.js │ └── pt.js ├── routes.js ├── server.js ├── server │ ├── ga.js │ ├── handleServerRendering.js │ ├── intl-polyfill.js │ └── setLocale.js ├── services │ ├── photo.js │ └── photos.js ├── stores │ ├── FeaturedStore.js │ ├── HtmlHeadStore.js │ ├── IntlStore.js │ └── PhotoStore.js ├── style │ ├── Animate.scss │ ├── Footer.scss │ ├── Loader.scss │ ├── LocaleSwitcher.scss │ ├── NavBar.scss │ ├── Page.scss │ ├── PhotoMeta.scss │ ├── Root.scss │ ├── Thumbnail.scss │ ├── ThumbnailCollection.scss │ ├── constants │ │ ├── animation.scss │ │ ├── breaks.scss │ │ ├── colors.scss │ │ └── typography.scss │ ├── mixins │ │ └── break.scss │ └── utils │ │ └── normalize.scss └── utils │ ├── APIUtils.js │ ├── CookieUtils.js │ ├── IntlComponents.js │ ├── IntlUtils.js │ ├── connectToIntlStore.js │ ├── getIntlMessage.js │ └── trackPageView.js ├── static └── assets │ ├── favicon.png │ └── grid-loader.svg └── webpack ├── .eslintrc ├── dev.config.js ├── prod.config.js └── server.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | 16 | [COMMIT_EDITMSG] 17 | max_line_length = 0 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["react"], 4 | "extends": "eslint:recommended", 5 | "rules": { 6 | "arrow-spacing": 2, 7 | "curly": 2, 8 | "eol-last": 2, 9 | "indent": [2, 2], 10 | "no-lonely-if": 2, 11 | "no-spaced-func": 2, 12 | "no-unused-vars": 2, 13 | "no-var": 2, 14 | "prefer-const": 2, 15 | "quotes": [2, "double"], 16 | "jsx-quotes": [1, "prefer-double"], 17 | "space-before-blocks": 2, 18 | 19 | "react/jsx-no-duplicate-props": 2, 20 | "react/jsx-no-undef": 2, 21 | "react/jsx-uses-react": 2, 22 | "react/no-danger": 2, 23 | "react/no-unknown-property": 2, 24 | "react/self-closing-comp": 2, 25 | "react/sort-comp": 2 26 | 27 | }, 28 | 29 | "env": { 30 | "node": true, 31 | "browser": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Dependency directory 5 | node_modules 6 | 7 | # Dist bundle generated by webpack 8 | static/dist 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix = '' 2 | -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | # Default application configuration that all configurations inherit from. 2 | 3 | scss_files: "**/*.scss" 4 | 5 | linters: 6 | BangFormat: 7 | enabled: true 8 | space_before_bang: true 9 | space_after_bang: false 10 | 11 | BorderZero: 12 | enabled: true 13 | convention: zero # or `none` 14 | 15 | ColorKeyword: 16 | enabled: true 17 | 18 | ColorVariable: 19 | enabled: true 20 | 21 | Comment: 22 | enabled: true 23 | 24 | DebugStatement: 25 | enabled: true 26 | 27 | DeclarationOrder: 28 | enabled: true 29 | 30 | DuplicateProperty: 31 | enabled: true 32 | 33 | ElsePlacement: 34 | enabled: true 35 | style: same_line # or 'new_line' 36 | 37 | EmptyLineBetweenBlocks: 38 | enabled: true 39 | ignore_single_line_blocks: true 40 | 41 | EmptyRule: 42 | enabled: true 43 | 44 | FinalNewline: 45 | enabled: true 46 | present: true 47 | 48 | HexLength: 49 | enabled: true 50 | style: short # or 'long' 51 | 52 | HexNotation: 53 | enabled: true 54 | style: uppercase # or 'lowercase' 55 | 56 | HexValidation: 57 | enabled: true 58 | 59 | IdSelector: 60 | enabled: false 61 | 62 | ImportantRule: 63 | enabled: true 64 | 65 | ImportPath: 66 | enabled: true 67 | leading_underscore: false 68 | filename_extension: false 69 | 70 | Indentation: 71 | enabled: true 72 | allow_non_nested_indentation: true 73 | character: space # or 'tab' 74 | width: 2 75 | 76 | LeadingZero: 77 | enabled: true 78 | style: exclude_zero # or 'include_zero' 79 | 80 | MergeableSelector: 81 | enabled: true 82 | force_nesting: true 83 | 84 | NameFormat: 85 | enabled: false 86 | allow_leading_underscore: true 87 | convention: BEM # or 'BEM', or a regex pattern 88 | 89 | NestingDepth: 90 | enabled: true 91 | max_depth: 3 92 | 93 | PlaceholderInExtend: 94 | enabled: true 95 | 96 | PropertyCount: 97 | enabled: false 98 | include_nested: false 99 | max_properties: 10 100 | 101 | PropertySortOrder: 102 | enabled: true 103 | order: ["position", "top", "right", "bottom", "left", "z-index", "display", "flex-direction", "flex-flow", "flex-wrap", "justify-content", "align-items", "align-content", "order", "flex", "flex-grow", "flex-shrink", "flex-basis", "align-self", "visibility", "overflow", "overflow-x", "overflow-y", "float", "clear", "table-layout", "border-collapse", "empty-cells", "box-sizing", "width", "min-width", "max-width", "height", "min-height", "max-height", "margin", "margin-top", "margin-right", "margin-bottom", "margin-left", "padding", "padding-top", "padding-right", "padding-bottom", "padding-left", "border", "border-width", "border-style", "border-color", "border-radius", "border-top", "border-top-width", "border-top-style", "border-top-color", "border-right", "border-right-width", "border-right-style", "border-right-color", "border-bottom", "border-bottom-width", "border-bottom-style", "border-bottom-color", "border-left", "border-left-width", "border-left-style", "border-left-color", "border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius", "white-space", "content", "color", "background", "background-color", "background-image", "background-repeat", "background-attachment", "background-position", "background-size", "opacity", "font", "font-weight", "font-style", "font-variant", "font-size", "font-family", "letter-spacing", "line-height", "list-style", "list-style-type", "list-style-position", "list-style-image", "outline", "outline-width", "outline-style", "outline-color", "text-align", "text-decoration", "text-indent", "text-transform", "text-shadow", "animation", "transform", "transition", "box-shadow"] 104 | ignore_unspecified: true 105 | 106 | PropertySpelling: 107 | enabled: true 108 | extra_properties: [] 109 | 110 | QualifyingElement: 111 | enabled: false 112 | allow_element_with_attribute: false 113 | allow_element_with_class: false 114 | allow_element_with_id: false 115 | 116 | SelectorDepth: 117 | enabled: true 118 | max_depth: 3 119 | 120 | SelectorFormat: 121 | enabled: false 122 | convention: BEM # or 'BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern 123 | 124 | Shorthand: 125 | enabled: true 126 | 127 | SingleLinePerProperty: 128 | enabled: true 129 | allow_single_line_rule_sets: true 130 | 131 | SingleLinePerSelector: 132 | enabled: true 133 | 134 | SpaceAfterComma: 135 | enabled: true 136 | 137 | SpaceAfterPropertyColon: 138 | enabled: true 139 | style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' 140 | 141 | SpaceAfterPropertyName: 142 | enabled: true 143 | 144 | SpaceBeforeBrace: 145 | enabled: true 146 | style: space # or 'new_line' 147 | allow_single_line_padding: false 148 | 149 | SpaceBetweenParens: 150 | enabled: true 151 | spaces: 0 152 | 153 | StringQuotes: 154 | enabled: true 155 | style: single_quotes # or double_quotes 156 | 157 | TrailingSemicolon: 158 | enabled: true 159 | 160 | TrailingZero: 161 | enabled: false 162 | 163 | UnnecessaryMantissa: 164 | enabled: true 165 | 166 | UnnecessaryParentReference: 167 | enabled: true 168 | 169 | UrlFormat: 170 | enabled: true 171 | 172 | UrlQuotes: 173 | enabled: true 174 | 175 | VariableForProperty: 176 | enabled: false 177 | properties: [] 178 | 179 | VendorPrefixes: 180 | enabled: false 181 | identifier_list: base 182 | additional_identifiers: [] 183 | excluded_identifiers: [] 184 | 185 | ZeroUnit: 186 | enabled: true 187 | 188 | Compass::*: 189 | enabled: false 190 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_script: 5 | - "npm run lint" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Giampaolo Bellavite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # isomorphic500 2 | 3 | [Isomorphic500](https://isomorphic500.herokuapp.com) is a small isomorphic ([universal](https://medium.com/@mjackson/universal-javascript-4761051b7ae9)) web application featuring photos from [500px](http://500px.com). 4 | 5 | It is built on [express](http://expressjs.com) using [React](https://facebook.github.io/react) and [Flux](https://facebook.github.io/flux) with [yahoo/fluxible](http://fluxible.io). It is developed with [webpack](http://webpack.github.io) and [react-hot-loader](http://gaearon.github.io/react-hot-loader/) and written with [babeljs](http://babeljs.io) with the help of [eslint](http://eslint.org). It supports multiple languages using [react-intl](http://formatjs.io/react/). 6 | 7 | 8 | 9 | [![Build Status](https://travis-ci.org/gpbl/isomorphic500.svg?branch=master)](https://travis-ci.org/gpbl/isomorphic500) 10 | 11 | The intent of this project is to solidify my experience with these technologies and perhaps to inspire other developers in their journey with React and Flux. It works also as example of a javascript development environment with all the cool recent stuff :-) 12 | 13 | - see the demo on [isomorphic500.herokuapp.com](https://isomorphic500.herokuapp.com) (with source maps!) 14 | - clone this repo and run the server to confirm it is actually working 15 | - edit a react component or a css style, and see the updated app as you save your changes! 16 | - read on for some technical details 17 | 18 | **Get help** 19 | Join the [gitter chat](https://gitter.im/gpbl/isomorphic500?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) or the [#isomorphic500](https://reactiflux.slack.com/archives/isomorphic500) on [reactiflux](http://www.reactiflux.com) :-) 20 | 21 | **Clone this repo** 22 | 23 | **Note** This app has been tested on node 4 24 | 25 | ``` 26 | git clone https://github.com/gpbl/isomorphic500.git 27 | cd isomorphic500 28 | npm install 29 | ``` 30 | 31 | **Start the app** 32 | 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | and open [localhost:3000](http://localhost:3000). 38 | 39 | You can also try the built app: 40 | 41 | ```bash 42 | npm run build # First, build for production 43 | npm run prod # then, run the production version 44 | ``` 45 | 46 | then open [localhost:8080](http://localhost:8080). 47 | 48 | > If you are starting the server on Windows, please read https://github.com/gpbl/isomorphic500/issues/58 49 | 50 | 51 | ## Table of Contents 52 | 53 | * [Application structure](#application-structure) 54 | * [The fluxible app](#the-fluxible-app) 55 | * [Async data](#async-data) 56 | * [Router](#router) 57 | * [Stores](#stores) 58 | * [Resource stores](#resource-stores) 59 | * [List stores](#list-stores) 60 | * [The HtmlHeadStore](#the-htmlheadstore) 61 | * [Internationalization (i18n)](#internationalization-i18n) 62 | * [How the user’s locale is detected](#how-the-user’s-locale-is-detected) 63 | * [Setting up react-intl](#setting-up-react-intl) 64 | * [Internationalization, the flux way](#internationalization-the-flux-way) 65 | * [Sending the locale to the API](#sending-the-locale-to-the-api) 66 | * [Development](#development) 67 | * [nodemon](#nodemon) 68 | * [Webpack](#webpack) 69 | * [Babeljs](#babeljs) 70 | * [.editorconfig](#editorconfig) 71 | * [Linting](#linting) 72 | * [Debugging](#debugging) 73 | 74 | ## Application structure 75 | 76 | ```bash 77 | . 78 | ├── index.js # Starts the express server and the webpack dev server 79 | ├── config # Contains the configuration for dev and prod environments 80 | ├── nodemon.json # Configure nodemon to watch some files 81 | ├── src 82 | │ ├── app.js # The fluxible app 83 | │ ├── client.js # Entry point for the client 84 | │ ├── config.js # Config loader (load the config files from /config) 85 | │ ├── routes.js # Routes used by fluxible-router 86 | │ ├── server.js # Start the express server and render the routes server-side 87 | │ │ 88 | │ ├── actions # Fluxible actions 89 | │ ├── components # React components 90 | │ ├── constants # Constants 91 | │ ├── containers # Contains React containers components 92 | │ │   ├── ... 93 | │ │   ├── Html.js # Used to render the document server-side 94 | │ │   └── Root.js # Root component 95 | 96 | │ ├── intl # Contains the messages for i18n 97 | │ ├── server # Server-side only code 98 | │ │   ├── ga.js # Google Analytics script 99 | │ │   ├── intl-polyfill.js # Patch node to support `Intl` and locale-data 100 | │ │   ├── render.js # Middleware to render server-side the fluxible app 101 | │ │   └── setLocale.js # Middleware to detect and set the request's locale 102 | │ ├── services # Fetchr services 103 | │ ├── stores # Fluxible stores 104 | │ ├── style # Contains the Sass files 105 | │ └── utils 106 | │ ├── APIUtils.js # Wrapper to superagent for communicating with 500px API 107 | │ ├── CookieUtils.js # Utility to write/read cookies 108 | │ ├── IntlComponents.js # Exports wrapped react-intl components 109 | │ ├── IntlUtils.js # Utilities to load `Intl` and locale-data 110 | │ ├── connectToIntlStore.js # Connects react-intl components with the IntlStore 111 | │ ├── getIntlMessage.js # Get react-intl messages 112 | │ └── trackPageView.js # Track a page view with google analitics 113 | ├── static 114 | │   ├── assets # Static files 115 | │   └── dist # Output files for webpack on production 116 | └── webpack 117 | ├── dev.config.js # Webpack config for development 118 | ├── prod.config.js # Webpack config for building the production files 119 | └── server.js # Used to starts the webpack dev server 120 | 121 | ``` 122 | 123 | ### The fluxible app 124 | 125 | The [src/app](src/app) file is the core of the Fluxible application: 126 | 127 | - it configures Fluxible with [src/containers/Root.js](src/containers/Root.js) as the root component. 128 | - it registers the stores so they can work on the same React context 129 | - it adds the [fetchr plugin]((https://github.com/yahoo/fluxible-plugin-fetchr)), to share the same API requests both client and server-side 130 | - it makes possible to dehydrate the stores [on the server](src/server/render.js) and rehydrate them [on the client](src/client.js) 131 | 132 | ### Async data 133 | 134 | I used [Fetchr](https://github.com/yahoo/fetchr) and [fluxible-plugin-fetchr](https://github.com/yahoo/fluxible-plugin-fetchr). 135 | [Fetchr services](src/services) run only on server and send [superagent](http://visionmedia.github.com/superagent) requests to 500px. 136 | 137 | ### Router 138 | 139 | This app uses [fluxible-router](https://github.com/yahoo/fluxible-router) for routing. Fluxible-router works pretty well in fluxible applications since it follows the flux paradigm. The [Application component](src/containers/Root.js) uses the `@handleHistory` decorator to bind the router to the app. 140 | 141 | ### Stores 142 | 143 | Instead of directly listening to stores, components use fluxible's `@connectToStores` decorator: a store state is passed to components as prop. See for example the [PhotoPage](src/containers/PhotoPage.js) or the [FeaturedPage](src/containers/FeaturedPage.js). 144 | 145 | `connectToStore` can also "consume" store data without actually listening to any store. This is the case of [NavBar](src/components/NavBar.js) or [LocaleSwitcher](src/components/LocaleSwitcher.js). 146 | 147 | #### Resource stores 148 | 149 | While REST APIs usually return collections as arrays, a resource store keeps items as big object – like the [PhotoStore](src/stores/PhotoStore.js). This simplifies the progressive resource updates that may happen during the app’s life. 150 | 151 | #### List stores 152 | 153 | A list store keeps references to a resource store, as the [FeaturedStore](src/stores/FeaturedStore.js) holds the ids of the photos in [PhotoStore](src/stores/PhotoStore.js). 154 | 155 | #### The HtmlHeadStore 156 | 157 | The [HtmlHeadStore](src/stores/HtmlHeadStore.js) is a special store used to set the `` meta-tags in the `Html` component, during server-side rendering. It is also listened by the `Application` component to change the browser's `document.title`. 158 | 159 | This store listens to route actions and set its content according to the current route. It also get data from other stores (e.g. the photo's title from the `PhotoStore`), or the localized messages from the `IntlStore`. 160 | 161 | ## Internationalization (i18n) 162 | 163 | To give an example on how to implement i18n in a React application, isomorphic500 supports English, [Italian](https://www.youtube.com/watch?v=9JhuOicPFZY), Portuguese and French. 164 | 165 | This app adopts [React Intl](http://formatjs.io/react/), which is a solid library for this purpose. 166 | 167 | ### How the user’s locale is detected 168 | 169 | The app sniffs the browser's `accept-language` request header. The [locale](https://github.com/jed/locale) npm module has a nice express middleware for that. Locales are restricted to those set in the app's [config](../config). 170 | 171 | The user may want to override the detected locale: the [LocaleSwitcher](src/components/LocaleSwitcher.js) component set a cookie when the user chooses a language. Also, we enable the `?hl` parameter in the query string to override it. Server-side, cookie and query string are detected by the [setLocale](src/server/setLocale.js) middleware. 172 | 173 | ### Setting up react-intl 174 | 175 | React-intl requires some boilerplate to work properly. Difficulties here arise mainly for two reasons: 176 | 177 | 1. React Intl relies on the [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) *global* API, not always available on node.js or some browsers (e.g. Safari). Luckly there's an [Intl polyfill](https://www.npmjs.com/package/intl): on the server we can just "require" it – however on the browser we want to download it *only* when `Intl` is not supported. 178 | 179 | 2. For each language, we need to load a set of *locale data* (used by `Intl` to format numbers and dates) and the translated strings, called *messages* (used by `react-intl`). While on node.js we can load them in memory, on the client they need to be downloaded first – and we want to download only the relevant data for the current locale. 180 | 181 | **On the server** the solution is easy: as said, the server [loads a polyfill](src/server/intl-polyfill) including both `Intl` and the locale data. For supporting the browser, we can instead rely on our technology stack, i.e. flux and webpack. 182 | 183 | **On the client**, we have to load the `Intl` polyfill and its locale data *before* rendering the app, i.e. in [client.js](src/client.js). 184 | 185 | For this purpose, I used webpack's `require.ensure()` to split `Intl` and localized data in multiple chunks. Only after they have been downloaded, the app can be mounted. See the `loadIntlPolyfill()` and `loadLocaleData()` functions in [IntlUtils](src/utils/IntlUtils.js): they return a promise that is resolved when the webpack chunks are downloaded and `require`d. 186 | 187 | They are used in [client.js](client.js) before mounting the app. 188 | 189 | > **Important**: since `react-intl` assumes `Intl` is already in the global scope, we can't import the fluxible app (which imports react-intl in some of its components) *before* polyfilling `Intl`. That's why you see in [client.js](src/client.js) `require("./app")` inside the in the `renderApp()` function, and not as `import` on the top of the file. 190 | 191 | ### Internationalization, the flux way 192 | 193 | Lets talk about the data that `react-intl` needs to deliver translated content. Translated messages are saved in the [intl](src/intl) directory and shared between client and server using the [IntlStore](stores/IntlStore). 194 | 195 | This store listens to a `LOAD_INTL_SERVER` action dispatched by [IntlActionCreator](src/actions/IntlActionCreators.js). We execute this action **only server side** before rendering the `Html` component together with the usual `navigateAction`. This allows to dehydrate/rehydrate the store content. 196 | 197 | React-intl components need to have access to the `IntlStore`. Plus, since I'm using ES6 classes, I can't adopt the react-intl `Mixin` in my components. To solve this, I wrap the `Formatted*` components and make them available from [IntlComponents](src/utils/IntlComponents.js). 198 | 199 | ### Sending the locale to the API 200 | 201 | While this is not required by the 500px API, we can send the current locale to the API so it can deliver localized content. This is made very easy by the Fetchr services, since they expose the `req` object: see for example the [photo service](src/services/photo.js). 202 | 203 | ## Development 204 | 205 | Run the development version with 206 | 207 | ``` 208 | npm run dev 209 | ``` 210 | 211 | ### nodemon 212 | 213 | This task runs the server with [nodemon](https://github.com/remy/nodemon). Nodemon will restart the server when some of the files specified in [its config](nodemon.json) change. 214 | 215 | ### Webpack 216 | 217 | Webpack is used as commonjs module bundler, css builder (using sass-loader) and assets loader (images and svg files). 218 | 219 | The [development config](./webpack/dev.config.js) enables source maps, the [Hot Module Replacement](http://webpack.github.io/docs/hot-module-replacement.html) and [react-hot-loader](http://gaearon.github.io/react-hot-loader/). It loads CSS styles with `