Commit 60abc8f5 by 小明

初始化 ik_invoicing 项目

parents
{
"presets": [
[
"latest",
{
"es2015": {
"modules": false
}
}
],
"react",
"stage-0"
],
"env": {
"production": {
"only": [
"app"
],
"plugins": [
"transform-react-remove-prop-types",
"transform-react-constant-elements",
"transform-react-inline-elements"
]
},
"test": {
"plugins": [
"istanbul"
]
}
}
}
\ No newline at end of file
root = true
[*]
end_of_line = lf
insert_final_newline = false
indent_style = space
indent_size = 2
{
"parser": "babel-eslint",
"extends": "airbnb",
"env": {
"browser": true,
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"redux-saga",
"react",
"jsx-a11y"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"arrow-parens": [
"error",
"always"
],
"arrow-body-style": [
2,
"as-needed"
],
"comma-dangle": [
2,
"always-multiline"
],
"import/imports-first": 0,
"import/newline-after-import": 0,
"import/no-dynamic-require": 0,
"import/no-extraneous-dependencies": 0,
"import/no-named-as-default": 0,
"import/no-unresolved": 2,
"import/prefer-default-export": 0,
"indent": [
2,
2,
{
"SwitchCase": 1
}
],
"jsx-a11y/aria-props": 2,
"jsx-a11y/heading-has-content": 0,
"jsx-a11y/href-no-hash": 2,
"jsx-a11y/label-has-for": 2,
"jsx-a11y/mouse-events-have-key-events": 2,
"jsx-a11y/role-has-required-aria-props": 2,
"jsx-a11y/role-supports-aria-props": 2,
"max-len": 0,
"newline-per-chained-call": 0,
"no-console": 1,
"no-use-before-define": 0,
"prefer-template": 2,
"class-methods-use-this": 0,
"react/forbid-prop-types": 0,
"react/jsx-first-prop-new-line": [
2,
"multiline"
],
"react/jsx-filename-extension": 0,
"react/jsx-no-target-blank": 0,
"react/require-extension": 0,
"react/self-closing-comp": 0,
"redux-saga/no-yield-in-race": 2,
"redux-saga/yield-effects": 2,
"require-yield": 0
},
"settings": {
"import/resolver": {
"webpack": {
"config": "./internals/webpack/webpack.test.babel.js"
}
}
}
}
\ No newline at end of file
# From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto
#
# The above will handle all files NOT found below
#
#
## These files are text and should be normalized (Convert crlf => lf)
#
# source code
*.php text
*.css text
*.sass text
*.scss text
*.less text
*.styl text
*.js text eol=lf
*.coffee text
*.json text
*.htm text
*.html text
*.xml text
*.svg text
*.txt text
*.ini text
*.inc text
*.pl text
*.rb text
*.py text
*.scm text
*.sql text
*.sh text
*.bat text
# templates
*.ejs text
*.hbt text
*.jade text
*.haml text
*.hbs text
*.dot text
*.tmpl text
*.phtml text
# server config
.htaccess text
# git config
.gitattributes text
.gitignore text
.gitconfig text
# code analysis config
.jshintrc text
.jscsrc text
.jshintignore text
.csslintrc text
# misc config
*.yaml text
*.yml text
.editorconfig text
# build config
*.npmignore text
*.bowerrc text
# Heroku
Procfile text
.slugignore text
# Documentation
*.md text
LICENSE text
AUTHORS text
#
## These files are binary and should be left untouched
#
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.pdf binary
# Don't check auto-generated stuff into git
coverage
build
node_modules
stats.json
# Cruft
.DS_Store
npm-debug.log
.idea
approvals = 2
pattern = "(?i):shipit:|LGTM"
self_approval_off = true
v7.2.0
\ No newline at end of file
language: node_js
node_js:
- 6
- 5
- 4
script: npm run build
install:
- npm i -g npm@latest
- npm install
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
notifications:
email:
on_failure: change
after_success: 'npm run coveralls'
cache:
directories:
- node_modules
# ik invoicing for dingtalk
`react` + `react-router` + `redux` + `react-redux` + `reselect` + `react-saga` + `webpack`
```
项目初始化原型来源于[react-boilerplate](https://github.com/mxstbr/react-boilerplate)
```
## Installation
```
npm run setup
```
## build
```
构建生产环境
npm run build
```
## server
```
开发
npm run start
```
## 部署
```
npm run deploy-dev # 开发环境
npm run deploy-test # 测试环境
npm run deploy # 生产环境
npm run deploy-kingdee-dev # 云之家开发环境
```
## test
```
npm run test
```
## 代码规范:
代码组织:代码目录组织结构按功能模块组织,目录名称使用复数。例如客户模块目录组织结构如下:
```
src/customers
├── customers.config.js
├── customers.controller.js
├── customers.service.js
└── customers.html
```
目录和文件命名风格: `underscore_separated_feature.type.js`
`service``controller` 的命名请使用 `FeatureFunctionType`的命名方式, 例如转移客户给他人的 `service` 可以这样命名: `CustomerTransferService`
angular通用书写指导手册[angular-styleguide](https://github.com/johnpapa/angular-styleguide/blob/master/i18n/zh-CN.md)
css规范指导[css-styleguide](http://cssguidelin.es/)
代码缩进:代码缩进统一使用2个空格,各个编辑器的EditorConfig配置请见:[EditorConfig](http://editorconfig.org/)
## API接口
API文档地址: http://dev.api.ikcrm.com/api_doc/index.html
测试数据:
```
access_token: 9ad4a66396ac9fdd8161f77a4b9e0b7cc38c192e4ceccfcbc40fb2fccc85e544066f743e33726c51b2223b
user_token: c6347b0158da16c36af274ca58755f0a
device: dingtalk
version_code: 1.0
```
### 注意:
所有的功能模块都在src目录下开发,比如开发“商机”模块时,在src目录下创建opportunities目录,将相关的文件全部放到该目录下。
所有调试日志统一用 `$log`, 方便全局启用或禁用
# http://www.appveyor.com/docs/appveyor-yml
# Set build version format here instead of in the admin panel
version: "{build}"
# Do not build on gh tags
skip_tags: true
# Test against these versions of Node.js
environment:
matrix:
# Node versions to run
- nodejs_version: 6
- nodejs_version: 5
- nodejs_version: 4
# Fix line endings in Windows. (runs before repo cloning)
init:
- git config --global core.autocrlf input
# Install scripts--runs after repo cloning
install:
# Install chrome
- choco install -y googlechrome
# Install the latest stable version of Node
- ps: Install-Product node $env:nodejs_version
- npm -g install npm
- set PATH=%APPDATA%\npm;%PATH%
- npm install
# Disable automatic builds
build: off
# Post-install test scripts
test_script:
# Output debugging info
- node --version
- npm --version
# run build and run tests
- npm run build
# Cache node_modules for faster builds
cache:
- node_modules -> package.json
# remove, as appveyor doesn't support secure variables on pr builds
# so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file
#on_success:
#- npm run coveralls
# Documentation
## Table of Contents
- [General](general)
- [**CLI Commands**](general/commands.md)
- [Tool Configuration](general/files.md)
- [Server Configurations](general/server-configs.md)
- [Deployment](general/deployment.md) *(currently Heroku specific)*
- [FAQ](general/faq.md)
- [Gotchas](general/gotchas.md)
- [Remove](general/remove.md)
- [Testing](testing)
- [Unit Testing](testing/unit-testing.md)
- [Component Testing](testing/component-testing.md)
- [Remote Testing](testing/remote-testing.md)
- [CSS](css)
- [`styled-components`](css/styled-componets.md)
- [sanitize.css](css/sanitize.md)
- [JS](js)
- [Redux](js/redux.md)
- [ImmutableJS](js/immutablejs.md)
- [reselect](js/reselect.md)
- [redux-saga](js/redux-saga.md)
- [i18n](js/i18n.md)
- [routing](js/routing.md)
## Overview
### Quickstart
1. First, let's kick the tyres by launching the sample _Repospective_ app
bundled with this project to demo some of its best features:
```Shell
npm run setup && npm start
```
1. Open [localhost:3000](http://localhost:3000) to see it in action.
- Add a Github username to see Redux and Redux Sagas in action: effortless
async state updates and side effects are now yours :)
- Edit the file at `./app/containers/HomePage/index.js` so that the text of
the `<Button>` component reads "Features!!!"... Hot Module Reloading gives
you a feedback loop with your UI so smooth it's almost conversational!
- Click your (newly emphatic) Features button to see React Router in action...
Now you can share a direct link to that content privately over your LAN or
globally addressable to any device, anywhere. Not bad for a locally-running
Single Page App.
1. Time to build your own app: run
```shell
npm run clean
```
...and use the built-in generators to start your first feature.
### Development
Run `npm start` to see your app at `localhost:3000`
### Building & Deploying
1. Run `npm run build`, which will compile all the necessary files to the
`build` folder.
2. Upload the contents of the `build` folder to your web server's root folder.
### Structure
The [`app/`](../../../tree/master/app) directory contains your entire application code, including CSS,
JavaScript, HTML and tests.
The rest of the folders and files only exist to make your life easier, and
should not need to be touched.
*(If they do have to be changed, please [submit an issue](https://github.com/mxstbr/react-boilerplate/issues)!)*
### CSS
Utilising [tagged template literals](./docs/tagged-template-literals.md)
(a recent addition to JavaScript) and the [power of CSS](./docs/css-we-support.md),
`styled-components` allows you to write actual CSS code to style your components.
It also removes the mapping between components and styles – using components as a
low-level styling construct could not be easier!
See the [CSS documentation](./css/README.md) for more information.
### JS
We bundle all your clientside scripts and chunk them into several files using
code splitting where possible. We then automatically optimize your code when
building for production so you don't have to worry about that.
See the [JS documentation](./js/README.md) for more information about the
JavaScript side of things.
### SEO
We use [react-helmet](https://github.com/nfl/react-helmet) for managing document head tags. Examples on how to
write head tags can be found [here](https://github.com/nfl/react-helmet#examples).
### Testing
For a thorough explanation of the testing procedure, see the
[testing documentation](./testing/README.md)!
#### Performance testing
With the production server running (i.e. while `npm run start:production` is running in
another tab), enter `npm run pagespeed` to run Google PageSpeed Insights and
get a performance check right in your terminal!
#### Browser testing
`npm run start:tunnel` makes your locally-running app globally available on the web
via a temporary URL: great for testing on different devices, client demos, etc!
#### Unit testing
Unit tests live in `test/` directories right next to the components being tested
and are run with `npm run test`.
# CSS
This boilerplate uses [`styled-components`](https://github.com/styled-components/styled-components)
allowing you to write your CSS in your JavaScript,
removing the mapping between styles and components.
`styled-components` let's us embrace component encapsulation while sanitize.css gives us
data-driven cross-browser normalisation.
Learn more:
- [`syled-components`](styled-componets.md)
- [sanitize.css](sanitize.md)
- [Using Sass](sass.md)
## Removing `sanitize.css`
Delete [lines 31 and 32 in `app.js`](../../app/app.js#L31-L32) and remove it
from the `dependencies` in [`package.json`](../../package.json)!
# `sanitize.css`
Sanitize.css makes browsers render elements more in
line with developer expectations (e.g. having the box model set to a cascading
`box-sizing: border-box`) and preferences (its defaults can be individually
overridden).
It was selected over older projects like `normalize.css` and `reset.css` due
to its greater flexibility and better alignment with CSSNext features like CSS
variables.
See the [official documentation](https://github.com/10up/sanitize.css) for more
information.
---
_Don't like this feature? [Click here](remove.md)_
# Can I use Sass with this boilerplate?
Yes, although we advise against it and **do not support this**. We selected
[`styled-components`](https://github.com/styled-components/styled-components)
over Sass because its approach is more powerful: instead of trying to
give a styling language programmatic abilities, it pulls logic and configuration
out into JS where we believe those features belong.
If you _really_ still want (or need) to use Sass then...
1. You will need to add a [sass-loader](https://github.com/jtangelder/sass-loader)
to the loaders section in `internals/webpack/webpack.base.babel.js` so it reads something like
```javascript
{
test: /\.scss$/,
exclude: /node_modules/,
loaders: ['style', 'css', 'sass']
}
```
Then run `npm i -D sass-loader node-sass`
...and you should be good to go!
# `styled-components`
`styled-components` allow you to write actual CSS code in your JavaScript to style your components,
removing the mapping between components and styles.
See the
[official documentation](https://github.com/styled-components/styled-components)
for more information!
## Usage
This creates two react components, `<Title>` and `<Wrapper>`:
```JSX
import React from 'react';
import styled from 'styled-components';
// Create a <Title> react component that renders an <h1> which is
// centered, palevioletred and sized at 1.5em
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
// Create a <Wrapper> react component that renders a <section> with
// some padding and a papayawhip background
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
```
*(The CSS rules are automatically vendor prefixed, so you don't have to think about it!)*
You render them like so:
```JSX
// Use them like any other React component – except they're styled!
<Wrapper>
<Title>Hello World, this is my first styled component!</Title>
</Wrapper>
```
For further examples see the
[official documentation](https://github.com/styled-components/styled-components).
# Introduction
The JavaScript ecosystem evolves at incredible speed: staying current can feel
overwhelming. So, instead of you having to stay on top of every new tool,
feature and technique to hit the headlines, this project aims to lighten the
load by providing a curated baseline of the most valuable ones.
Using React Boilerplate, you get to start your app with our community's current
ideas on what represents optimal developer experience, best practice, most
efficient tooling and cleanest project structure.
- [**CLI Commands**](commands.md)
- [Tool Configuration](files.md)
- [Server Configurations](server-configs.md)
- [Deployment](deployment.md) *(currently Heroku specific)*
- [FAQ](faq.md)
- [Gotchas](gotchas.md)
# Feature overview
## Quick scaffolding
Automate the creation of components, containers, routes, selectors and sagas -
and their tests - right from the CLI!
Run `npm run generate` in your terminal and choose one of the parts you want
to generate. They'll automatically be imported in the correct places and have
everything set up correctly.
> We use [plop] to generate new components, you can find all the logic and
templates for the generation in `internals/generators`.
[plop]: https://github.com/amwmedia/plop
## Instant feedback
Enjoy the best DX and code your app at the speed of thought! Your saved changes
to the CSS and JS are reflected instantaneously without refreshing the page.
Preserve application state even when you update something in the underlying code!
## Predictable state management
We use Redux to manage our applications state. We have also added optional
support for the [Chrome Redux DevTools Extension] – if you have it installed,
you can see, play back and change your action history!
[Chrome Redux DevTools Extension]: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd
## Next generation JavaScript
Use ESNext template strings, object destructuring, arrow functions, JSX syntax
and more, today. This is possible thanks to Babel with the `latest`, `stage-0`
and `react` presets!
## Next generation CSS
Write composable CSS that's co-located with your components using [`styled-components`]
for complete modularity. Unique generated class names keep the specificity low
while eliminating style clashes. Ship only the styles that are used on the
visible page for the best performance.
[`styled-components`]: ../css/styled-components.md
## Industry-standard routing
It's natural to want to add pages (e.g. `/about`) to your application, and
routing makes this possible. Thanks to [react-router] with [react-router-redux],
that's as easy as pie and the url is auto-synced to your application state!
[react-router]: https://github.com/reactjs/react-router
[react-router-redux]: https://github.com/reactjs/react-router-redux
# Optional extras
_Don't like any of these features? [Click here](remove.md)_
## Offline-first
The next frontier in performant web apps: availability without a network
connection from the instant your users load the app. This is done with a
ServiceWorker and a fallback to AppCache, so this feature even works on older
browsers!
> All your files are included automatically. No manual intervention needed
thanks to Webpack's [`offline-plugin`](https://github.com/NekR/offline-plugin)
### Add To Homescreen
After repeat visits to your site, users will get a prompt to add your application
to their homescreen. Combined with offline caching, this means your web app can
be used exactly like a native application (without the limitations of an app store).
The name and icon to be displayed are set in the `app/manifest.json` file.
Change them to your project name and icon, and try it!
## Performant Web Font Loading
If you simply use web fonts in your project, the page will stay blank until
these fonts are downloaded. That means a lot of waiting time in which users
could already read the content.
[FontFaceObserver](https://github.com/bramstein/fontfaceobserver) adds a class
to the `body` when the fonts have loaded. (see [`app.js`](../../app/app.js#L26-L36)
and [`App/styles.css`](../../app/containers/App/styles.css))
### Adding a new font
1. Either add the `@font-face` declaration to `App/styles.css` or add a `<link>`
tag to the [`index.html`](../../app/index.html). (Don't forget to remove the `<link>`
for Open Sans from the [`index.html`](../../app/index.html)!)
2. In `App/styles.css`, specify your initial `font-family` in the `body` tag
with only web-save fonts. In the `body.jsFontLoaded` tag, specify your
`font-family` stack with your web font.
3. In `app.js` add a `<fontName>Observer` for your font.
## Image optimization
Images often represent the majority of bytes downloaded on a web page, so image
optimization can often be a notable performance improvement. Thanks to Webpack's
[`image-loader`](https://github.com/tcoopman/image-webpack-loader), every PNG, JPEG, GIF and SVG images
is optimized.
See [`image-loader`](https://github.com/tcoopman/image-webpack-loader) to customize optimizations options.
# Command Line Commands
## Initialization
```Shell
npm run setup
```
Initializes a new project with this boilerplate. Deletes the `react-boilerplate`
git history, installs the dependencies and initializes a new repository.
> Note: This command is self-destructive, once you've run it the init script is
gone forever. This is for your own safety, so you can't delete your project's
history irreversibly by accident.
## Development
```Shell
npm run start
```
Starts the development server running on `http://localhost:3000`
## Cleaning
```Shell
npm run clean
```
Deletes the example app, replacing it with the smallest amount of boilerplate
code necessary to start writing your app!
> Note: This command is self-destructive, once you've run it you cannot run it
again. This is for your own safety, so you can't delete portions of your project
irreversibly by accident.
## Generators
```Shell
npm run generate
```
Allows you to auto-generate boilerplate code for common parts of your
application, specifically `component`s, `container`s, and `route`s. You can
also run `npm run generate <part>` to skip the first selection. (e.g. `npm run
generate container`)
## Server
### Development
```Shell
npm start
```
Starts the development server and makes your application accessible at
`localhost:3000`. Tunnels that server with `ngrok`, which means the website
accessible anywhere! Changes in the application code will be hot-reloaded.
### Production
```Shell
npm run start:prod
```
Starts the production server, configured for optimal performance: assets are
minified and served gzipped.
### Port
To change the port the app is accessible at pass the `--port` option to the command
with `--`. E.g. to make the app visible at `localhost:5000`, run the following:
`npm start -- --port 5000`
## Building
```Shell
npm run build
```
Preps your app for deployment. Optimizes and minifies all files, piping them to
a folder called `build`. Upload the contents of `build` to your web server to
see your work live!
## Testing
See the [testing documentation](../testing/README.md) for detailed information
about our testing setup!
## Unit testing
```Shell
npm run test
```
Tests your application with the unit tests specified in the `*test.js` files
throughout the application.
All the `test` commands allow an optional `-- --grep string` argument to filter
the tests ran by Karma. Useful if you need to run a specific test only.
```Shell
# Run only the Button component tests
npm run test:watch -- --grep Button
```
### Browsers
To choose the browser to run your unit tests in (Chrome by default), run one of
the following commands:
#### Firefox
```Shell
npm run test:firefox
```
#### Safari
```Shell
npm run test:safari
```
#### Internet Explorer
*Windows only!*
```Shell
npm run test:ie
```
### Watching
```Shell
npm run test:watch
```
Watches changes to your application and reruns tests whenever a file changes.
### Remote testing
```Shell
npm run start:tunnel
```
Starts the development server and tunnels it with `ngrok`, making the website
available on the entire world. Useful for testing on different devices in different locations!
### Performance testing
```Shell
npm run pagespeed
```
With the remote server running (i.e. while `npm run start:prod` is running in
another terminal session), enter this command to run Google PageSpeed Insights
and get a performance check right in your terminal!
### Dependency size test
```Shell
npm run analyze
```
This command will generate a `stats.json` file from your production build, which
you can upload to the [webpack analyzer](https://webpack.github.io/analyse/). This
analyzer will visualize your dependencies and chunks with detailed statistics
about the bundle size.
## Linting
```Shell
npm run lint
```
Lints your JavaScript and CSS.
### JavaScript
```Shell
npm run lint:js
```
Only lints your JavaScript.
### CSS
```Shell
npm run lint:css
```
Only lints your CSS.
# Deployment
## Heroku
### Easy 5-Step Deployment Process
*Step 1:* Create a _Procfile_ with the following line: `web: npm run start:prod`. We do this because Heroku runs `npm run start` by default, so we need this setting to override the default run command.
*Step 2:* Install the Node.js buildpack for your Heroku app by running the following command: `heroku buildpacks:set https://github.com/heroku/heroku-buildpack-nodejs#v91 -a [your app name]`. Make sure to replace `#v91` with whatever the latest buildpack is, which you can [find here](https://github.com/heroku/heroku-buildpack-nodejs/releases).
*Step 3:* Add this line to your `package.json` file in the scripts area: `"heroku-postbuild": "npm run build",`. This is so Heroku can build your production assets when deploying (more of which you can [read about here](https://devcenter.heroku.com/articles/nodejs-support#heroku-specific-build-steps)). Then, adjust the _prebuild_ script in your `package.json` file so it looks like this: `"prebuild": "npm run build:clean",` to avoid having Heroku attempt to run Karma tests (which are unsupported with this buildpack).
*Step 4:* Run `heroku config:set NPM_CONFIG_PRODUCTION=false` so that Heroku can compile the NPM modules included in your _devDependencies_ (since many of these packages are required for the build process).
*Step 5:* Follow the standard Heroku deploy process:
1. `git add .`
2. `git commit -m 'Made some epic changes as per usual'`
3. `git push heroku master`
# Frequently Asked Questions
## Where are Babel and ESLint configured?
In package.json
## Where are the files coming from when I run `npm start`?
In development Webpack compiles your application runs it in-memory. Only when
you run `npm run build` will it write to disk and preserve your bundled
application across computer restarts.
## How do I fix `Error: listen EADDRINUSE 127.0.0.1:3000`?
This simply means that there's another process already listening on port 3000.
The fix is to kill the process and rerun `npm start`.
### OS X / Linux:
1. Find the process id (PID):
```Shell
ps aux | grep node
```
> This will return the PID as the value following your username:
> ```Shell
> janedoe 29811 49.1 2.1 3394936 356956 s004 S+ 4:45pm 2:40.07 node server
> ```
> Note: If nothing is listed, you can try `lsof -i tcp:3000`
1. Then run
```Shell
kill -9 YOUR_PID
```
> e.g. given the output from the example above, `YOUR_PID` is `29811`, hence
that would mean you would run `kill -9 29811`
### Windows
1. Find the process id (PID):
```Shell
netstat -a -o -n
```
> This will return a list of running processes and the ports they're
listening on:
> ```
> Proto Local Address Foreign Address State PID
> TCP 0.0.0.0:25 0.0.0.0:0 Listening 4196
> ...
> TCP 0.0.0.0:3000 0.0.0.0:0 Listening 28344
```
1. Then run
```Shell
taskkill /F /PID YOUR_PID
```
> e.g. given the output from the example above, `YOUR_PID` is `28344`, hence
that would mean you would run `taskkill /F /PID 28344`
## Issue with local caching when running in production mode (F5 / ctrl+F5 / cmd+r weird behavior)
Your production site isn't working? You update the code and nothing changes? It drives you insane?
#### Quick fix on your local browser:
To fix it on your local browser, just do the following. (Suited when you're testing the production mode locally)
`Chrome dev tools > Application > Clear Storage > Clear site data` *(Chrome)*
#### Full in-depth explanation
Read more at https://github.com/NekR/offline-plugin/blob/master/docs/updates.md
## Local webfonts not working for development
In development mode CSS sourcemaps require that styling is loaded by blob://,
resulting in browsers resolving font files relative to the main document.
A way to use local webfonts in development mode is to add an absolute
output.publicPath in webpack.dev.babel.js, with protocol.
```javascript
// webpack.dev.babel.js
output: {
publicPath: 'http://127.0.0.1:3000/',
/* … */
},
```
## Non-route containers
> Note: Container will always be nested somewhere below a route. Even if there's dozens of components
in between, somewhere up the tree will be route. (maybe only "/", but still a route)
### Where do I put the reducer?
While you can include the reducer statically in `reducers.js`, we don't recommend this as you lose
the benefits of code splitting. Instead, add it as a _composed reducer_. This means that you
pass actions onward to a second reducer from a lower-level route reducer like so:
```JS
// Main route reducer
function myReducerOfRoute(state, action) {
switch (action.type) {
case SOME_OTHER_ACTION:
return someOtherReducer(state, action);
}
}
```
That way, you still get the code splitting at route level, but avoid having a static `combineReducers`
call that includes all of them by default.
*See [this and the following lesson](https://egghead.io/lessons/javascript-redux-reducer-composition-with-arrays?course=getting-started-with-redux) of the egghead.io Redux course for more information about reducer composition!*
### How do I run the saga?
Since a container will always be within a route, one we can simply add it to the exported array in
`sagas.js` of the route container somewhere up the tree:
```JS
// /containers/SomeContainer/sagas.js
import { someOtherSagaFromNestedContainer } from './containers/SomeNestedContainer/sagas';
function* someSaga() { /**/ }
export default [
someSaga,
someOtherSagaFromNestedContainer,
];
```
Or, if you have multiple sagas in the nested container:
```JS
// /containers/SomeContainer/sagas.js
import nestedContainerSagas from './containers/SomeNestedContainer/sagas';
function* someSaga() { /**/ }
export default [
someSaga,
...nestedContainerSagas,
];
```
## Using this boilerplate with WebStorm
WebStorm is a powerful IDE, and why not also use it as debugger tool? Here is the steps
1. [Install JetBrain Chrome Extension](https://chrome.google.com/webstore/detail/jetbrains-ide-support/hmhgeddbohgjknpmjagkdomcpobmllji)
2. [Setting up the PORT](https://www.jetbrains.com/help/webstorm/2016.1/using-jetbrains-chrome-extension.html)
3. Change WebPack devtool config to `source-map` [(This line)](https://github.com/mxstbr/react-boilerplate/blob/56eb5a0ec4aa691169ef427f3a0122fde5a5aa24/internals/webpack/webpack.dev.babel.js#L65)
4. Run web server (`npm run start`)
5. Create Run Configuration (Run > Edit Configurations)
6. Add new `JavaScript Debug`
7. Setting up URL
8. Start Debug (Click the green bug button)
9. Edit Run Configuration Again
10. Mapping Url as below picture
* Map your `root` directory with `webpack://.` (please note the last dot)
* Map your `build` directory with your root path (e.g. `http://localhost:3000`)
11. Hit OK and restart debugging session
![How to debug using WebStorm](webstorm-debug.png)
### Troubleshooting
1. You miss the last `.` (dot) in `webpack://.`
2. The port debugger is listening tool and the JetBrain extension is mismatch.
### Enable ESLint
ESLint help making all developer follow the same coding format. Please also setting up in your IDE, otherwise, you will fail ESLint test.
1. Go to WebStorm Preference
2. Search for `ESLint`
3. Click `Enable`
![Setting up ESLint](webstorm-eslint.png)
## Use CI with bitbucket pipelines
Your project is on bitbucket? Take advantage of the pipelines feature (Continuous Integration) by creating a 'bitbucket-pipelines.yml' file at the root of the project and use the following code to automatically test your app at each commit:
```YAML
image: gwhansscheuren/bitbucket-pipelines-node-chrome-firefox
pipelines:
default:
- step:
script:
- node --version
- npm --version
- npm install
- npm test
```
## I'm using Node v0.12 and the server doesn't work?
We settled on supporting the last three major Node.js versions for the boilerplate – at the moment
of this writing those are v4, v5 and v6. We **highly recommend upgrading to a newer Node.js version**!
If you _have_ to use Node.js 0.12, you can hack around the server not running by using `babel-cli` to
run the server: `npm install babel-cli`, and then replace all instances of `node server` in the `"scripts"`
in the `package.json` with `babel server`!
## Have another question?
Submit an [issue](https://github.com/mxstbr/react-boilerplate/issues),
hop onto the [Gitter channel](https://gitter.im/mxstbr/react-boilerplate)
or contact Max direct on [twitter](https://twitter.com/mxstbr)!
# Configuration: A Glossary
A guide to the configuration files for this project: where they live and what
they do.
## The root folder
* `.editorconfig`: Sets the default configuration for certain files across editors. (e.g. indentation)
* `.gitattributes`: Normalizes how `git`, the version control system this boilerplate uses, handles certain files.
* `.gitignore`: Tells `git` to ignore certain files and folders which don't need to be version controlled, like the build folder.
* `.travis.yml` and `appveyor.yml`: Continuous Integration configuration<br/>
This boilerplate uses [Travis CI](https://travis-ci.com) for Linux environments
and [AppVeyor](https://www.appveyor.com/) for Windows platforms, but feel free
to swap either out for your own choice of CI.
* `package.json`: Our `npm` configuration file has three functions:
1. It's where Babel and ESLint are configured
1. It's the API for the project: a consistent interface for all its controls
1. It lists the project's package dependencies
Baking the config in is a slightly unusual set-up, but it allows us to keep
the project root as uncluttered and grokkable-at-a-glance as possible.
## The `./internals` folder
This is where the bulk of the tooling configuration lives, broken out into
recognisable units of work.
Feel free to change anything you like but don't be afraid to [ask upfront](https://gitter.im/mxstbr/react-boilerplate)
whether you should: build systems are easy to break!
# Gotchas
These are some things to be aware of when using this boilerplate.
## Special images in HTML files
If you specify your images in the `.html` files using the `<img>` tag, everything
will work fine. The problem comes up if you try to include images using anything
except that tag, like meta tags:
```HTML
<meta property="og:image" content="img/yourimg.png" />
```
The webpack `html-loader` does not recognise this as an image file and will not
transfer the image to the build folder. To get webpack to transfer them, you
have to import them with the file loader in your JavaScript somewhere, e.g.:
```JavaScript
import 'file?name=[name].[ext]!../img/yourimg.png';
```
Then webpack will correctly transfer the image to the build folder.
### Removing offline access
**Careful** about removing this, as there is no real downside to having your
application available when the users network connection isn't perfect.
To remove offline capability, delete the `offline-plugin` from the
[`package.json`](../../package.json), remove the import of the plugin in
[`app.js`](../../app/app.js) and remove the plugin from the
[`webpack.prod.babel.js`](../../internals/webpack/webpack.prod.babel.js).
### Removing add to homescreen functionality
Delete [`manifest.json`](../../app/manifest.json) and remove the
`<link rel="manifest" href="manifest.json">` tag from the
[`index.html`](../../app/index.html).
### Removing performant web font loading
**Careful** about removing this, as perceived performance might be highly impacted.
To remove `FontFaceObserver`, don't import it in [`app.js`](../../app/app.js) and
remove it from the [`package.json`](../../package.json).
### Removing image optimization
To remove image optimization, delete the `image-webpack-loader` from the
[`package.json`](../../package.json), and remove the `image-loader` from [`webpack.base.babel.js`](../../internals/webpack/webpack.base.babel.js):
```
{
test: /\.(jpg|png|gif)$/,
loaders: [
'file-loader',
'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}',
],
}
```
Then replace it with classic `file-loader`:
```
{
test: /\.(jpg|png|gif)$/,
loader: 'file-loader',
}
```
# Server Configurations
## Apache
This boilerplate includes a `.htaccess` file that does two things:
1. Redirect all traffic to HTTPS because ServiceWorker only works for encrypted
traffic.
1. Rewrite all pages (e.g. `yourdomain.com/subpage`) to `yourdomain.com/index.html`
to let `react-router` take care of presenting the correct page.
> Note: For performance reasons you should probably adapt it to run as a static
`.conf` file (typically under `/etc/apache2/sites-enabled` or similar) so that
your server doesn't have to apply its rules dynamically per request)
## Nginx
Also it includes a `.nginx.conf` file that does the same on Nginx server.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
# JavaScript
## State management
This boilerplate manages application state using [Redux](redux.md), makes it
immutable with [`ImmutableJS`](immutablejs.md) and keeps access performant
via [`reselect`](reselect.md).
For managing asynchronous flows (e.g. logging in) we use [`redux-saga`](redux-saga.md).
For routing, we use [`react-router` in combination with `react-router-redux`](routing.md).
We include a generator for components, containers, sagas, routes and selectors.
Run `npm run generate` to choose from the available generators, and automatically
add new parts of your application!
> Note: If you want to skip the generator selection process,
`npm run generate <generator>` also works. (e.g. `npm run generate route`)
### Learn more
- [Redux](redux.md)
- [ImmutableJS](immutablejs.md)
- [reselect](reselect.md)
- [redux-saga](redux-saga.md)
- [react-intl](i18n.md)
- [routing](routing.md)
## Architecture: `components` and `containers`
We adopted a split between stateless, reusable components called (wait for it...)
`components` and stateful parent components called `containers`.
### Learn more
See [this article](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
by Dan Abramov for a great introduction to this approach.
# `i18n`
`react-intl` is a library to manage internationalization and pluralization support
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 preform these very tasks.
The complete `react-intl` docs can be found here:
https://github.com/yahoo/react-intl/wiki
## Usage
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.
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`).
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:
https://github.com/yahoo/react-intl/wiki/API#definemessages
```js
/*
* Footer Messages
*
* This contains all the text for the Footer component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
licenseMessage: {
id: 'boilerplate.components.Footer.license.message',
defaultMessage: 'This project is licensed under the MIT license.',
},
authorMessage: {
id: 'boilerplate.components.Footer.author.message',
defaultMessage: `
Made with love by {author}.
`,
},
});
```
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.
You will also notice a more complex use of `FormattedMessage` for the author message where alternate or variable values (i.e. `author: <A href="https://twitter.com/mxstbr">Max Stoiber</A>,`) are being injected, in this case it's a react component.
```js
import React from 'react';
import messages from './messages';
import A from 'components/A';
import styles from './styles.css';
import { FormattedMessage } from 'react-intl';
function Footer() {
return (
<footer className={styles.footer}>
<section>
<p>
<FormattedMessage {...messages.licenseMessage} />
</p>
</section>
<section>
<p>
<FormattedMessage
{...messages.authorMessage}
values={{
author: <A href="https://twitter.com/mxstbr">Max Stoiber</A>,
}}
/>
</p>
</section>
</footer>
);
}
export default Footer;
```
## Extracting i18n JSON files
You can extract all i18n language within each component by running the following command:
```
npm run extract-intl
```
This will extract all language into i18n JSON files in `app/translations`.
## Adding A Language
You can add a language by running the generate command:
```
npm run generate language
```
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.
## Removing i18n and react-intl
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.
The packages associated with `react-intl` are:
- react-intl
- babel-plugin-react-intl
# ImmutableJS
Immutable data structures can be deeply compared in no time. This allows us to
efficiently determine if our components need to rerender since we know if the
`props` changed or not!
Check out the [official documentation](https://facebook.github.io/immutable-js/)
for a good explanation of the more intricate benefits it has.
## Usage
In our reducers, we make the initial state an immutable data structure with the
`fromJS` function. We pass it an object or an array, and it takes care of
converting it to a immutable data structure. (Note: the conversion is performed deeply so
that even arbitrarily nested arrays/objects are immutable structures too!)
```JS
import { fromJS } from 'immutable';
const initialState = fromJS({
myData: {
message: 'Hello World!'
},
});
```
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).
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).
```JS
import { SOME_ACTION, SOME_OTHER_ACTION } from './actions';
// […]
function myReducer(state = initialState, action) {
switch (action.type) {
case SOME_ACTION:
return state.set('myData', action.payload);
case SOME_OTHER_ACTION:
return state.setIn(['myData', 'message'], action.payload);
default:
return state;
}
}
```
We use [`reselect`](./reselect.md) to efficiently cache our computed application
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)
functions to select the part we want.
```JS
const myDataSelector = (state) => state.get('myData');
const messageSelector = (state) => state.getIn(['myData', 'message']);
export default myDataSelector;
```
To learn more, check out [`reselect.md`](reselect.md)!
## Immutable Records
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).
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.
For instance you'll write `map.get('property')` instead of `object.property`, and `list.get(0)` instead of `array[0]`.
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.
ImmutableJS's [`Record`](https://facebook.github.io/immutable-js/docs/#/Record) structure offers a solution to this issue.
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.
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.
The creation of a record is less simple than simply calling `.toJS()`.
First, you have to define the `Record` shape. With the example above, to create your initial state, you'll write:
```JS
// Defining the shape
const StateRecord = Record({
myData: {
message: 'Hello World!'
}
});
const initialState = new StateRecord({}); // initialState is now a new StateRecord instance
// initialized with myData.message set by default as 'Hello World!'
```
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.
### Gotchas of Using Records
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.
Instead setter methods ( `.set`, `.update`, `.merge`) should be used.
Certain properties can not be set on a record as they would conflict with the API. Consider the below example:
```JS
const ProductRecord = Record({
type: 'tshirt',
size: 'small'
});
```
Because record.size is used to return the records count (similar to array.length), the above definition would throw an error.
\ No newline at end of file
# `redux-saga`
`redux-saga` is a library to manage side effects in your application. It works
beautifully for data fetching, concurrent computations and a lot more.
[Sebastien Lorber](https://twitter.com/sebastienlorber) put it best:
> Imagine there is widget1 and widget2. When some button on widget1 is clicked,
then it should have an effect on widget2. Instead of coupling the 2 widgets
together (ie widget1 dispatch an action that targets widget2), widget1 only
dispatch that its button was clicked. Then the saga listen for this button
click and then update widget2 by dispatching a new event that widget2 is aware of.
>
> This adds a level of indirection that is unnecessary for simple apps, but make
it more easy to scale complex applications. You can now publish widget1 and
widget2 to different npm repositories so that they never have to know about
each others, without having them to share a global registry of actions. The 2
widgets are now bounded contexts that can live separately. They do not need
each others to be consistent and can be reused in other apps as well. **The saga
is the coupling point between the two widgets that coordinate them in a
meaningful way for your business.**
_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)
of this quote in its entirety!_
To learn more about this amazing way to handle concurrent flows, start with the
[official documentation](https://github.com/yelouafi/redux-saga) and explore
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`)
## Usage
Sagas are associated with a container, just like actions, constants, selectors
and reducers. If your container already has a `sagas.js` file, simply add your
saga to that. If your container does not yet have a `sagas.js` file, add one with
this boilerplate structure:
```JS
import { take, call, put, select } from 'redux-saga/effects';
// Your sagas for this container
export default [
sagaName,
];
// Individual exports for testing
export function* sagaName() {
}
```
Then, in your `routes.js`, add injection for the newly added saga:
```JS
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/YourComponent/reducer'),
System.import('containers/YourComponent/sagas'),
System.import('containers/YourComponent'),
]);
const renderRoute = loadModule(cb);
importModules.then(([reducer, sagas, component]) => {
injectReducer('home', reducer.default);
injectSagas(sagas.default); // Inject the saga
renderRoute(component);
});
importModules.catch(errorLoading);
},
```
Now add as many sagas to your `sagas.js` file as you want!
---
_Don't like this feature? [Click here](remove.md)_
# Redux
If you haven't worked with Redux, it's highly recommended (possibly indispensable!)
to read through the (amazing) [official documentation](http://redux.js.org)
and/or watch this [free video tutorial series](https://egghead.io/series/getting-started-with-redux).
## Usage
See above! As minimal as Redux is, the challenge it addresses - app state
management - is a complex topic that is too involved to properly discuss here.
## Removing redux
There are a few reasons why we chose to bundle redux with React Boilerplate, the
biggest being that it is widely regarded as the current best Flux implementation
in terms of architecture, support and documentation.
You may feel differently! This is completely OK :)
Below are a few reasons you might want to remove it:
### I'm just getting started and Flux is hard
You're under no obligation to use Redux or any other Flux library! The complexity
of your application will determine the point at which you need to introduce it.
Here are a couple of great resources for taking a minimal approach:
- [Misconceptions of Tooling in JavaScript](http://javascriptplayground.com/blog/2016/02/the-react-webpack-tooling-problem)
- [Learn Raw React — no JSX, no Flux, no ES6, no Webpack…](http://jamesknelson.com/learn-raw-react-no-jsx-flux-es6-webpack/)
### It's overkill for my project!
See above.
### I prefer `(Alt|MobX|SomethingElse)`!
React Boilerplate is a baseline for _your_ app: go for it!
If you feel that we should take a closer look at supporting your preference
out of the box, please let us know.
## Removing `redux-saga`
**We don't recommend removing `redux-saga`**, as we strongly feel that it's the
way to go for most redux based applications.
If you really want to get rid of it, you will have to delete its traces from several places.
**app/store.js**
1. Remove statement `import createSagaMiddleware from 'redux-saga'`.
2. Remove statement `const sagaMiddleware = createSagaMiddleware()`.
3. Remove `sagaMiddleware` from `middlewares` array.
4. Remove statement `store.runSaga = sagaMiddleware.run`
**app/utils/asyncInjectors.js**
1. Remove `runSaga: isFunction` from `shape`.
2. Remove function `injectAsyncSagas`.
3. Do not export `injectSagas: injectAsyncSagas(store, true)`.
**app/routes.js**
1. Do not pull out `injectSagas` from `getAsyncInjectors()`.
2. Remove `sagas` from `importModules.then()`.
3. Remove `injectSagas(sagas.default)` from every route that uses Saga.
**Finally, remove it from the `package.json`. Then you should be good to go with whatever
side-effect management library you want to use!**
## Removing `reselect`
To remove `reselect`, remove it from your dependencies in `package.json` and then write
your `mapStateToProps` functions like you normally would!
You'll also need to hook up the history directly to the store. Make changes to `app/app.js`.
1. Remove statement `import { selectLocationState } from 'containers/App/selectors'`
2. Make necessary changes to `history` as follows:
```js
const selectLocationState = () => {
let prevRoutingState;
let prevRoutingStateJS;
return (state) => {
const routingState = state.get('route'); // or state.route
if (!routingState.equals(prevRoutingState)) {
prevRoutingState = routingState;
prevRoutingStateJS = routingState.toJS();
}
return prevRoutingStateJS;
};
};
const history = syncHistoryWithStore(browserHistory, store, {
selectLocationState: selectLocationState(),
});
```
# `reselect`
reselect memoizes ("caches") previous state trees and calculations based on said
tree. This means repeated changes and calculations are fast and efficient,
providing us with a performance boost over standard `mapStateToProps`
implementations.
The [official documentation](https://github.com/reactjs/reselect)
offers a good starting point!
## Usage
There are two different kinds of selectors, simple and complex ones.
### Simple selectors
Simple selectors are just that: they take the application state and select a
part of it.
```javascript
const mySelector = (state) => state.get('someState');
export {
mySelector,
};
```
### Complex selectors
If we need to, we can combine simple selectors to build more complex ones which
get nested state parts with reselect's `createSelector` function. We import other
selectors and pass them to the `createSelector` call:
```javascript
import { createSelector } from 'reselect';
import mySelector from 'mySelector';
const myComplexSelector = createSelector(
mySelector,
(myState) => myState.get('someNestedState')
);
export {
myComplexSelector,
};
```
These selectors can then either be used directly in our containers as
`mapStateToProps` functions or be nested with `createSelector` once again:
```javascript
export default connect(createSelector(
myComplexSelector,
(myNestedState) => ({ data: myNestedState })
))(SomeComponent);
```
### Adding a new selector
If you have a `selectors.js` file next to the reducer which's part of the state
you want to select, add your selector to said file. If you don't have one yet,
add a new one into your container folder and fill it with this boilerplate code:
```JS
import { createSelector } from 'reselect';
const selectMyState = () => createSelector(
);
export {
selectMyState,
};
```
---
_Don't like this feature? [Click here](remove.md)_
# Routing via `react-router` and `react-router-redux`
`react-router` is the de-facto standard routing solution for react applications.
The thing is that with redux and a single state tree, the URL is part of that
state. `react-router-redux` takes care of synchronizing the location of our
application with the application state.
(See the [`react-router-redux` documentation](https://github.com/reactjs/react-router-redux)
for more information)
## Usage
To add a new route, use the generator with `npm run generate route`.
This is what a standard (generated) route looks like for a container:
```JS
{
path: '/',
name: 'home',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/HomePage')
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
},
}
```
To go to a new page use the `push` function by `react-router-redux`:
```JS
import { push } from 'react-router-redux';
dispatch(push('/some/page'));
```
## Child Routes
`npm run generate route` does not currently support automatically generating child routes if you need them, but they can be easily created manually.
For example, if you have a route called `about` at `/about` and want to make a child route called `team` at `/about/our-team` you can just add that child page to the parent page's `childRoutes` array like so:
```JS
/* your app's other routes would already be in this array */
{
path: '/about',
name: 'about',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/AboutPage'),
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
},
childRoutes: [
{
path: '/about/our-team',
name: 'team',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/TeamPage'),
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
},
},
]
}
```
## Index routes
To add an index route, use the following pattern:
```JS
{
path: '/',
name: 'home',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/HomePage')
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
},
indexRoute: {
getComponent(partialNextState, cb) {
const importModules = Promise.all([
System.import('containers/HomeView')
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
},
},
}
```
## Dynamic routes
To go to a dynamic route such as 'post/:slug' eg 'post/cool-new-post', firstly add the route to your `routes.js`, as per documentation:
```JS
path: '/posts/:slug',
name: 'post',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/Post/reducer'),
System.import('containers/Post/sagas'),
System.import('containers/Post'),
]);
const renderRoute = loadModule(cb);
importModules.then(([reducer, sagas, component]) => {
injectReducer('post', reducer.default);
injectSagas(sagas.default);
renderRoute(component);
});
importModules.catch(errorLoading);
},
```
###Container:
```JSX
<Link to={`/posts/${post.slug}`} key={post._id}>
```
Clickable link with payload (you could use push if needed).
###Action:
```JS
export function getPost(slug) {
return {
type: LOAD_POST,
slug,
};
}
export function postLoaded(post) {
return {
type: LOAD_POST_SUCCESS,
podcast,
};
}
```
###Saga:
```JS
const { slug } = yield take(LOAD_POST);
yield call(getXhrPodcast, slug);
export function* getXhrPodcast(slug) {
const requestURL = `http://your.api.com/api/posts/${slug}`;
const post = yield call(request, requestURL);
if (!post.err) {
yield put(postLoaded(post));
} else {
yield put(postLoadingError(post.err));
}
}
```
Wait (`take`) for the LOAD_POST constant, which contains the slug payload from the `getPost()` function in actions.js.
When the action is fired then dispatch the `getXhrPodcast()` function to get the response from your api. On success dispatch the `postLoaded()` action (`yield put`) which sends back the response and can be added into the reducer state.
You can read more on [`react-router`'s documentation](https://github.com/reactjs/react-router/blob/master/docs/API.md#props-3).
# Testing
- [Unit Testing](unit-testing.md)
- [Component Testing](component-testing.md)
- [Remote Testing](remote-testing.md)
Testing your application is a vital part of serious development. There are a few
things you should test. If you've never done this before start with [unit testing](unit-testing.md).
Move on to [component testing](component-testing.md) when you feel like you
understand that!
We also support [remote testing](remote-testing.md) your local application,
which is quite awesome, so definitely check that out!
## Usage with this boilerplate
To test your application started with this boilerplate do the following:
1. Sprinkle `.test.js` files directly next to the parts of your application you
want to test. (Or in `test/` subdirectories, it doesn't really matter as long
as they are directly next to those parts and end in `.test.js`)
1. Write your unit and component tests in those files.
1. Run `npm run test` in your terminal and see all the tests pass! (hopefully)
There are a few more commands related to testing, checkout the [commands documentation](../general/commands.md#testing)
for the full list!
# Component testing
[Unit testing your Redux actions and reducers](unit-testing.md) is nice, but you
can do even more to make sure nothing breaks your application. Since React is
the _view_ layer of your app, let's see how to test Components too!
<!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
- [Shallow rendering](#shallow-rendering)
- [Enzyme](#enzyme)
<!-- /TOC -->
## Shallow rendering
React provides us with a nice add-on called the Shallow Renderer. This renderer
will render a React component **one level deep**. Lets take a look at what that
means with a simple `<Button>` component...
This component renders a `<button>` element containing a checkmark icon and some
text:
```javascript
// Button.react.js
import CheckmarkIcon from './CheckmarkIcon.react';
function Button(props) {
return (
<button className="btn" onClick={props.onClick}>
<CheckmarkIcon />
{ React.Children.only(props.children) }
</button>
);
}
export default Button;
```
_Note: This is a [state**less** ("dumb") component](../js/README.md#architecture-components-and-containers)_
It might be used in another component like this:
```javascript
// HomePage.react.js
import Button from './Button.react';
class HomePage extends React.Component {
render() {
return(
<Button onClick={this.doSomething}>Click me!</Button>
);
}
}
```
_Note: This is a [state**ful** ("smart") component](../js/README.md#architecture-components-and-containers)!_
When rendered normally with the standard `ReactDOM.render` function, this will
be the HTML output
(*Comments added in parallel to compare structures in HTML from JSX source*):
```html
<button> <!-- <Button> -->
<i class="fa fa-checkmark"></i> <!-- <CheckmarkIcon /> -->
Click Me! <!-- { props.children } -->
</button> <!-- </Button> -->
```
Conversely, when rendered with the shallow renderer, we'll get a String
containing this "HTML":
```html
<button> <!-- <Button> -->
<CheckmarkIcon /> <!-- NOT RENDERED! -->
Click Me! <!-- { props.children } -->
</button> <!-- </Button> -->
```
If we test our `Button` with the normal renderer and there's a problem
with the `CheckmarkIcon` then the test for the `Button` will fail as well...
but finding the culprit will be hard. Using the _shallow_ renderer, we isolate
the problem's cause since we don't render any other components other than the
one we're testing!
The problem with the shallow renderer is that all assertions have to be done
manually, and you cannot do anything that needs the DOM.
Thankfully, [AirBnB](https://twitter.com/AirbnbEng) has open sourced their
wrapper around the React shallow renderer and jsdom, called `enzyme`. `enzyme`
is a testing utility that gives us a nice assertion/traversal/manipulation API.
## Enzyme
Lets test our `<Button>` component! We're going to assess three things: First,
that it renders a HTML `<button>` tag, second that it renders its children we
pass it and third that handles clicks!
This is our Mocha setup:
```javascript
describe('<Button />', () => {
it('renders a <button>', () => {});
it('renders its children', () => {});
it('handles clicks', () => {});
});
```
Lets start with testing that it renders a `<button>`. To do that we first
`shallow` render it, and then `expect` that a `<button>` node exists.
```javascript
it('renders a <button>', () => {
const renderedComponent = shallow(
<Button></Button>
);
expect(
renderedComponent.find("button").node
).toExist();
});
```
Nice! If somebody breaks our button component by having it render an `<a>` tag
or something else we'll immediately know! Let's do something a bit more advanced
now, and check that our `<Button>` renders its children.
We render our button component with some text, and then verify that our text
exists:
```javascript
it('renders its children', () => {
const text = "Click me!";
const renderedComponent = shallow(
<Button>{ text }</Button>
);
expect(
renderedComponent.contains(text)
).toEqual(true);
});
```
Great! Onwards to our last and most advanced test: checking that our `<Button>` handles clicks correctly. We'll use a Spy for that. A Spy is a
function that knows if, and how often, it has been called. We create the Spy
(thoughtfully provided by `expect`), pass _it_ as the `onClick` handler to our
component, simulate a click on the rendered `<button>` element and, lastly,
see that our Spy was called:
```javascript
it('handles clicks', () => {
const onClickSpy = expect.createSpy();
const renderedComponent = shallow(<Button onClick={onClickSpy} />);
renderedComponent.find('button').simulate('click');
expect(onClickSpy).toHaveBeenCalled();
});
```
And that's how you unit test your components and make sure they work correctly!
# Remote testing
```Shell
npm run start:tunnel
```
This command will start a server and tunnel it with `ngrok`. You'll get a URL
that looks a bit like this: `http://abcdef.ngrok.com`
This URL will show the version of your application that's in the `build` folder,
and it's accessible from the entire world! This is great for testing on different
devices and from different locations!
# Unit testing
Unit testing is the practice of testing the smallest possible *units* of our
code, functions. We run our tests and automatically verify that our functions
do the thing we expect them to do. We assert that, given a set of inputs, our
functions return the proper values and handle problems.
This boilerplate uses the [Mocha](https://github.com/mochajs/mocha) test
framework to run the tests and [expect](http://github.com/mjackson/expect) for
assertions. These libraries make writing tests as easy as speaking - you
`describe` a unit of your code and `expect` `it` to do the correct thing.
<!-- TOC depthFrom:2 depthTo:4 withLinks:1 updateOnSave:1 orderedList:0 -->
- [Basics](#basics)
- [Mocha](#mocha)
- [expect](#expect)
- [Testing Redux Applications](#testing-redux-applications)
- [Reducers](#reducers)
- [rewire](#rewire)
- [Actions](#actions)
<!-- /TOC -->
We use this glob pattern to find unit tests `app/**/*.test.js` - this tells
mocha to run all files that end with `.test.js` anywhere within the `app`
folder. Use this to your advantage, and put unit tests next to the files you
want to test so relevant files stay together!
Imagine a navigation bar, this is what its folder might look like:
```
NavBar # Wrapping folder
├── NavBar.css # Styles
├── NavBar.react.js # Actual component
├── NavBar.actions.js # Actions
├── NavBar.constants.js # Constants
├── NavBar.reducer.js # Reducer
└── test # Folder of tests
├── NavBar.actions.test.js # Actions tests
└── NavBar.reducer.test.js # Reducer tests
```
## Basics
For the sake of this guide, lets pretend we're testing this function. It's
situated in the `add.js` file:
```javascript
// add.js
export function add(x, y) {
return x + y;
}
```
> Note: The `export` here is ES6 syntax, and you will need an ES6 transpiler
(e.g. babel.js) to run this JavaScript.
> The `export` exports our function as a module, which we can `import` and use
in other files. Continue below to see what that looks like.
### Mocha
Mocha is our unit testing framework. Its API, which we write tests with, is
speech like and easy to use.
> Note: This is the [official documentation](http://mochajs.org) of Mocha.
We're going to add a second file called `add.test.js` with our unit tests
inside. Running said unit tests requires us to enter `mocha add.test.js` into
the command line.
First, we `import` the function in our `add.test.js` file:
```javascript
// add.test.js
import { add } from './add.js';
```
Second, we `describe` our function:
```javascript
describe('add()', () => {
});
```
> Note: `(arg1, arg2) => { }` is ES6 notation for anonymous functions, i.e. is
the same thing as `function(arg1, arg2) { }`
Third, we tell Mocha what `it` (our function) should do:
```javascript
describe('add()', () => {
it('adds two numbers', () => {
});
it('doesnt add the third number', () => {
});
});
```
That's the entire Mocha part! Onwards to the actual tests.
### expect
Using expect, we `expect` our little function to return the same thing every
time given the same input.
> Note: This is the [official documentation](https://github.com/mjackson/expect) for expect.
First, we have to import `expect` at the top of our file, before the tests:
```javascript
import expect from 'expect';
describe('add()', () => {
// [...]
});
```
We're going to test that our little function correctly adds two numbers first.
We are going to take some chosen inputs, and `expect` the result `toEqual` the
corresponding output:
```javascript
// [...]
it('adds two numbers', () => {
expect(add(2, 3)).toEqual(5);
});
// [...]
```
Lets add the second test, which determines that our function doesn't add the
third number if one is present:
```javascript
// [...]
it('doesnt add the third number', () => {
expect(add(2, 3, 5)).toEqual(add(2, 3));
});
// [...]
```
> Note: Notice that we call `add` in `toEqual`. I won't tell you why, but just
think about what would happen if we rewrote the expect as `expect(add(2, 3, 5)).toEqual(5)`
and somebody broke something in the add function. What would this test
actually... test?
Should our function work, Mocha will show this output when running the tests:
```
add()
✓ adds two numbers
✓ doesnt add the third number
```
Lets say an unnamed colleague of ours breaks our function:
```javascript
// add.js
export function add(x, y) {
return x * y;
}
```
Oh no, now our function doesn't add the numbers anymore, it multiplies them!
Imagine the consequences to our code that uses the function!
Thankfully, we have unit tests in place. Because we run the unit tests before we
deploy our application, we see this output:
```
add()
1) adds two numbers
✓ doesnt add the third number
1) add adds two numbers:
Error: Expected 6 to equal 5
```
This tells us that something is broken in the add function before any users get
the code! Congratulations, you just saved time and money!
## Testing Redux Applications
This boilerplate uses Redux, partially because it turns our data flow into
testable (pure) functions. Let's go back to our `NavBar` component from above,
and see what testing the actions and the reducer of it would look like.
This is what our `NavBar` actions look like:
```javascript
// NavBar.actions.js
import { TOGGLE_NAV } from './NavBar.constants.js';
export function toggleNav() {
return { type: TOGGLE_NAV };
}
```
with this reducer:
```javascript
// NavBar.reducer.js
import { TOGGLE_NAV } from './NavBar.constants.js';
const initialState = {
open: false
};
function NavBarReducer(state = initialState, action) {
switch (action.type) {
case TOGGLE_NAV:
return Object.assign({}, state, {
open: !state.open
});
default:
return state;
}
}
export default NavBarReducer;
```
Lets test the reducer first!
### Reducers
First, we have to import `expect`, the reducer and the constant.
```javascript
// NavBar.reducer.test.js
import expect from 'expect';
import NavBarReducer from '../NavBar.reducer';
import { TOGGLE_NAV } from '../NavBar.constants';
```
Then we `describe` the reducer, and add two tests: we check that it returns the
initial state and that it handles the `toggleNav` action.
```javascript
describe('NavBarReducer', () => {
it('returns the initial state', () => {
});
it('handles the toggleNav action', () => {
});
});
```
Lets write the tests themselves! Since the reducer is just a function, we can
call it like any other function and `expect` the output to equal something.
To test that it returns the initial state, we call it with a state of `undefined`
(the first argument), and an empty action (second argument). The reducer should
return the initial state of the `NavBar`, which is
```javascript
{
open: false
}
```
Lets put that into practice:
```javascript
describe('NavBarReducer', () => {
it('returns the initial state', () => {
expect(NavBarReducer(undefined, {})).toEqual({
open: false
});
});
it('handles the toggleNav action', () => {
});
});
```
This works, but we have one problem: We also test the initial state itself. When
somebody changes the initial state, this test will fail, even though the reducer
correctly returns the initial state.
To fix that, we have to `import` the initial state from the reducer file and
check that the reducer returns that. This has one problem: Our initial state
isn't `export`ed.
Now, you might be thinking "Ha! easy: simply add an `export` before the
`const initialState` in the reducer and boom!"... But in fact we _don't_ want
to do that because it's an internal (or "private") property of that module
alone and shouldn't really be accessible from the outside at all.
This is where the `rewire` module comes in handy.
#### rewire
Rewire allows us to access properties we normally couldn't via special
`__get__` and `__set__` methods it injects into modules.
Start by `import`ing rewire **at the top** of your test file:
```javascript
// `NavBar.reducer.test.js`
import expect from 'expect';
import rewire from 'rewire';
import NavBarReducer from '../NavBar.reducer';
import { TOGGLE_NAV } from '../NavBar.constants';
const initialState = NavBarReducer.__get__('initialState');
```
> Note: You might be wondering why we still `import` the `NavBarReducer` above.
The `NavBarReducer` imported with `rewire` isn't the _actual_ reducer, it's a
`rewire`d version.
Now we can really see whether the `NavBarReducer` returns the initial state if
no action is passed!
```javascript
it('returns the initial state', () => {
expect(NavBarReducer(undefined, {})).toEqual(initialState);
});
```
w00t, we fixed the test!
> For more information on Rewire, see the [official documentation](https://github.com/jhnns/rewire)
Lets see how we can test actions next.
### Actions
We have one action `toggleNav` that changes the `NavBar` open state.
A Redux action is a pure function, so testing it isn't more difficult than
testing our `add` function from the first part of this guide!
The first step is to import the action to be tested, the constant it should
return and `expect`:
```javascript
// NavBar.actions.test.js
import { toggleNav } from '../NavBar.actions';
import { TOGGLE_NAV } from '../NavBar.constants';
import expect from 'expect';
```
Then we `describe` the actions:
```javascript
describe('NavBar actions', () => {
describe('toggleNav', () => {
it('should return the correct constant', () => {
});
});
});
```
> Note: `describe`s can be nested, which gives us nice output, as we'll see later.
And the last step is to add the assertion:
```javascript
it('should return the correct constant', () => {
expect(toggleNav()).toEqual({
type: TOGGLE_NAV
});
});
```
If our `toggleNav` action works correctly, this is the output Mocha will show us:
```
NavBar actions
toggleNav
✓ should return the correct constant
```
And that's it, we now know when somebody breaks the `toggleNav` action!
*Continue to learn how to test your application with [Component Testing](component-testing.md)!*
const resolve = require('path').resolve;
const pullAll = require('lodash/pullAll');
const uniq = require('lodash/uniq');
const ReactBoilerplate = {
// This refers to the react-boilerplate version this project is based on.
version: '3.3.3',
/**
* The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading
* by caching the module metadata for all of our npm dependencies. We enable it by default
* in development.
*
*
* To disable the DLL Plugin, set this value to false.
*/
dllPlugin: {
defaults: {
/**
* we need to exclude dependencies which are not intended for the browser
* by listing them here.
*/
exclude: [
'chalk',
'compression',
'cross-env',
'express',
'ip',
'minimist',
'sanitize.css',
],
/**
* Specify any additional dependencies here. We include core-js and lodash
* since a lot of our dependencies depend on them and they get picked up by webpack.
*/
include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'],
// The path where the DLL manifest and bundle will get built
path: resolve('../node_modules/react-boilerplate-dlls'),
},
entry(pkg) {
const dependencyNames = Object.keys(pkg.dependencies);
const exclude = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude;
const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include;
const includeDependencies = uniq(dependencyNames.concat(include));
return {
reactBoilerplateDeps: pullAll(includeDependencies, exclude),
};
},
},
};
module.exports = ReactBoilerplate;
/**
*
* {{ properCase name }}
*
*/
import React, { Component } from 'react';
{{#if wantCSS}}
import styles from './styles.css';
{{/if}}
class {{ properCase name }} extends Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
{{#if wantCSS}}
<div className={{curly true}}styles.{{ camelCase name }}{{curly}}>
{{else}}
<div>
{{/if}}
{{#if wantMessages}}
<FormattedMessage {...messages.header} />
{{/if}}
</div>
);
}
}
export default {{ properCase name }};
/**
*
* {{ properCase name }}
*
*/
import React, { PureComponent } from 'react';
{{#if wantCSS}}
import styles from './styles.css';
{{/if}}
class {{ properCase name }} extends PureComponent { // eslint-disable-line react/prefer-stateless-function
render() {
return (
{{#if wantCSS}}
<div className={{curly true}}styles.{{ camelCase name }}{{curly}}>
{{else}}
<div>
{{/if}}
{{#if wantMessages}}
<FormattedMessage {...messages.header} />
{{/if}}
</div>
);
}
}
export default {{ properCase name }};
/**
* Component Generator
*/
const componentExists = require('../utils/componentExists');
module.exports = {
description: 'Add an unconnected component',
prompts: [{
type: 'list',
name: 'type',
message: 'Select the type of component',
default: 'Stateless Function',
choices: () => ['Stateless Function', 'ES6 Class (Pure)', 'ES6 Class'],
}, {
type: 'input',
name: 'name',
message: 'What should it be called?',
default: 'Button',
validate: (value) => {
if ((/.+/).test(value)) {
return componentExists(value) ? 'A component or container with this name already exists' : true;
}
return 'The name is required';
},
}],
actions: (data) => {
// Generate index.js and index.test.js
let componentTemplate;
switch (data.type) {
case 'ES6 Class': {
componentTemplate = './component/es6.js.hbs';
break;
}
case 'ES6 Class (Pure)': {
componentTemplate = './component/es6.pure.js.hbs';
break;
}
case 'Stateless Function': {
componentTemplate = './component/stateless.js.hbs';
break;
}
default: {
componentTemplate = './component/es6.js.hbs';
}
}
const actions = [{
type: 'add',
path: '../../src/components/{{properCase name}}/index.js',
templateFile: componentTemplate,
abortOnFail: true,
}, {
type: 'add',
path: '../../src/components/{{properCase name}}/tests/index.test.js',
templateFile: './component/test.js.hbs',
abortOnFail: true,
}];
return actions;
},
};
/**
*
* {{ properCase name }}
*
*/
import React, { Component } from 'react';
{{#if wantCSS}}
import styles from './styles.css';
{{/if}}
function {{ properCase name }}() {
return (
{{#if wantCSS}}
<div className={{curly true}}styles.{{ camelCase name }}{{curly}}>
{{else}}
<div>
{{/if}}
{{#if wantMessages}}
<FormattedMessage {...messages.header} />
{{/if}}
</div>
);
}
export default {{ properCase name }};
// import {{ properCase name }} from '../index';
import expect from 'expect';
// import { shallow } from 'enzyme';
// import React from 'react';
describe('<{{ properCase name }} />', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});
/*
*
* {{ properCase name }} actions
*
*/
import {
DEFAULT_ACTION,
} from './constants';
export function defaultAction() {
return {
type: DEFAULT_ACTION,
};
}
import expect from 'expect';
import {
defaultAction,
} from '../actions';
import {
DEFAULT_ACTION,
} from '../constants';
describe('{{ properCase name }} actions', () => {
describe('Default Action', () => {
it('has a type of DEFAULT_ACTION', () => {
const expected = {
type: DEFAULT_ACTION,
};
expect(defaultAction()).toEqual(expected);
});
});
});
/*
*
* {{ properCase name }} constants
*
*/
export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION';
/**
* Container Generator
*/
const componentExists = require('../utils/componentExists');
module.exports = {
description: 'Add a container component',
prompts: [{
type: 'input',
name: 'name',
message: 'What should it be called?',
default: 'Form',
validate: (value) => {
if ((/.+/).test(value)) {
return componentExists(value) ? 'A component or container with this name already exists' : true;
}
return 'The name is required';
},
}, {
type: 'list',
name: 'component',
message: 'Select a base component:',
default: 'PureComponent',
choices: () => ['PureComponent', 'Component'],
}, {
type: 'confirm',
name: 'wantHeaders',
default: false,
message: 'Do you want headers?',
}, {
type: 'confirm',
name: 'wantActionsAndReducer',
default: true,
message: 'Do you want an actions/constants/selectors/reducer tupel for this container?',
}, {
type: 'confirm',
name: 'wantSagas',
default: true,
message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)',
}],
actions: (data) => {
// Generate index.js and index.test.js
const actions = [{
type: 'add',
path: '../../src/containers/{{properCase name}}/index.js',
templateFile: './container/index.js.hbs',
abortOnFail: true,
}, {
type: 'add',
path: '../../src/containers/{{properCase name}}/tests/index.test.js',
templateFile: './container/test.js.hbs',
abortOnFail: true,
}];
// If they want actions and a reducer, generate actions.js, constants.js,
// reducer.js and the corresponding tests for actions and the reducer
if (data.wantActionsAndReducer) {
// Actions
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/actions.js',
templateFile: './container/actions.js.hbs',
abortOnFail: true,
});
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/tests/actions.test.js',
templateFile: './container/actions.test.js.hbs',
abortOnFail: true,
});
// Constants
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/constants.js',
templateFile: './container/constants.js.hbs',
abortOnFail: true,
});
// Selectors
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/selectors.js',
templateFile: './container/selectors.js.hbs',
abortOnFail: true,
});
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/tests/selectors.test.js',
templateFile: './container/selectors.test.js.hbs',
abortOnFail: true,
});
// Reducer
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/reducer.js',
templateFile: './container/reducer.js.hbs',
abortOnFail: true,
});
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/tests/reducer.test.js',
templateFile: './container/reducer.test.js.hbs',
abortOnFail: true,
});
}
// Sagas
if (data.wantSagas) {
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/sagas.js',
templateFile: './container/sagas.js.hbs',
abortOnFail: true,
});
actions.push({
type: 'add',
path: '../../src/containers/{{properCase name}}/tests/sagas.test.js',
templateFile: './container/sagas.test.js.hbs',
abortOnFail: true,
});
}
return actions;
},
};
/*
*
* {{properCase name }}
*
*/
import React, { Component } from 'react';
import { connect } from 'react-redux';
{{#if wantHeaders}}
import Helmet from 'react-helmet';
{{/if}}
{{#if wantActionsAndReducer}}
import select{{properCase name}} from './selectors';
{{/if}}
{{#if wantCSS}}
import styles from './styles.css';
{{/if}}
export class {{ properCase name }} extends {{{ component }}} { // eslint-disable-line react/prefer-stateless-function
render() {
return (
{{#if wantCSS}}
<div className={{curly true}}styles.{{ camelCase name }}{{curly}}>
{{else}}
<div>
{{/if}}
{{#if wantHeaders}}
<Helmet
title="{{properCase name}}"
meta={{curly true}}[
{{curly true}} name: 'description', content: 'Description of {{properCase name}}' {{curly}},
]{{curly}}
/>
{{/if}}
</div>
);
}
}
{{#if wantActionsAndReducer}}
const mapStateToProps = select{{properCase name}}();
{{/if}}
function mapDispatchToProps(dispatch) {
return {
dispatch,
};
}
{{#if wantActionsAndReducer}}
export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }});
{{else}}
export default connect(null, mapDispatchToProps)({{ properCase name }});
{{/if}}
/*
*
* {{ properCase name }} reducer
*
*/
import { fromJS } from 'immutable';
import {
DEFAULT_ACTION,
} from './constants';
const initialState = fromJS({});
function {{ camelCase name }}Reducer(state = initialState, action) {
switch (action.type) {
case DEFAULT_ACTION:
return state;
default:
return state;
}
}
export default {{ camelCase name }}Reducer;
import expect from 'expect';
import {{ camelCase name }}Reducer from '../reducer';
import { fromJS } from 'immutable';
describe('{{ camelCase name }}Reducer', () => {
it('returns the initial state', () => {
expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({}));
});
});
// import { take, call, put, select } from 'redux-saga/effects';
// Individual exports for testing
export function* defaultSaga() {
return;
}
// All sagas to be loaded
export default [
defaultSaga,
];
/**
* Test sagas
*/
import expect from 'expect';
// import { take, call, put, select } from 'redux-saga/effects';
// import { defaultSaga } from '../sagas';
// const generator = defaultSaga();
describe('defaultSaga Saga', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});
import { createSelector } from 'reselect';
/**
* Direct selector to the {{ camelCase name }} state domain
*/
const select{{ properCase name }}Domain = () => (state) => state.get('{{ camelCase name }}');
/**
* Other specific selectors
*/
/**
* Default selector used by {{ properCase name }}
*/
const select{{ properCase name }} = () => createSelector(
select{{ properCase name }}Domain(),
(substate) => substate.toJS()
);
export default select{{ properCase name }};
export {
select{{ properCase name }}Domain,
};
// import { select{{ properCase name }}Domain } from '../selectors';
// import { fromJS } from 'immutable';
import expect from 'expect';
// const selector = select{{ properCase name}}Domain();
describe('select{{ properCase name }}Domain', () => {
it('Expect to have unit tests specified', () => {
expect('Test case').toEqual(false);
});
});
// import { {{ properCase name }} } from '../index';
import expect from 'expect';
// import { shallow } from 'enzyme';
// import React from 'react';
describe('<{{ properCase name }} />', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
});
});
/**
* generator/index.js
*
* Exports the generators so plop knows them
*/
const fs = require('fs');
const componentGenerator = require('./component/index.js');
const containerGenerator = require('./container/index.js');
const routeGenerator = require('./route/index.js');
module.exports = (plop) => {
plop.setGenerator('component', componentGenerator);
plop.setGenerator('container', containerGenerator);
plop.setGenerator('route', routeGenerator);
plop.addHelper('directory', (comp) => {
try {
fs.accessSync(`src/containers/${comp}`, fs.F_OK);
return `containers/${comp}`;
} catch (e) {
return `components/${comp}`;
}
});
plop.addHelper('curly', (object, open) => (open ? '{' : '}'));
};
/**
* Route Generator
*/
const fs = require('fs');
const componentExists = require('../utils/componentExists');
function reducerExists(comp) {
try {
fs.accessSync(`src/containers/${comp}/reducer.js`, fs.F_OK);
return true;
} catch (e) {
return false;
}
}
function sagasExists(comp) {
try {
fs.accessSync(`src/containers/${comp}/sagas.js`, fs.F_OK);
return true;
} catch (e) {
return false;
}
}
function trimTemplateFile(template) {
// Loads the template file and trims the whitespace and then returns the content as a string.
return fs.readFileSync(`internals/generators/route/${template}`, 'utf8').replace(/\s*$/, '');
}
module.exports = {
description: 'Add a route',
prompts: [{
type: 'input',
name: 'component',
message: 'Which component should the route show?',
validate: (value) => {
if ((/.+/).test(value)) {
return componentExists(value) ? true : `"${value}" doesn't exist.`;
}
return 'The path is required';
},
}, {
type: 'input',
name: 'path',
message: 'Enter the path of the route.',
default: '/about',
validate: (value) => {
if ((/.+/).test(value)) {
return true;
}
return 'path is required';
},
}],
// Add the route to the routes.js file above the error route
// TODO smarter route adding
actions: (data) => {
const actions = [];
if (reducerExists(data.component)) {
data.useSagas = sagasExists(data.component); // eslint-disable-line no-param-reassign
actions.push({
type: 'modify',
path: '../../src/routes.js',
pattern: /(\s{\n\s{0,}path: '\*',)/g,
template: trimTemplateFile('routeWithReducer.hbs'),
});
} else {
actions.push({
type: 'modify',
path: '../../src/routes.js',
pattern: /(\s{\n\s{0,}path: '\*',)/g,
template: trimTemplateFile('route.hbs'),
});
}
return actions;
},
};
{
path: '{{ path }}',
name: '{{ camelCase component }}',
getComponent(location, cb) {
System.import('{{{directory (properCase component)}}}')
.then(loadModule(cb))
.catch(errorLoading);
},
},$1
{
path: '{{ path }}',
name: '{{ camelCase component }}',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/{{ properCase component }}/reducer'),
{{#if useSagas}}
System.import('containers/{{ properCase component }}/sagas'),
{{/if}}
System.import('containers/{{ properCase component }}'),
]);
const renderRoute = loadModule(cb);
importModules.then(([reducer,{{#if useSagas}} sagas,{{/if}} component]) => {
injectReducer('{{ camelCase component }}', reducer.default);
{{#if useSagas}}
injectSagas(sagas.default);
{{/if}}
renderRoute(component);
});
importModules.catch(errorLoading);
},
},$1
/**
* componentExists
*
* Check whether the given component exist in either the components or containers directory
*/
const fs = require('fs');
const pageComponents = fs.readdirSync('src/components');
const pageContainers = fs.readdirSync('src/containers');
const components = pageComponents.concat(pageContainers);
function componentExists(comp) {
return components.indexOf(comp) >= 0;
}
module.exports = componentExists;
#!/usr/bin/env node
const shelljs = require('shelljs');
const animateProgress = require('./helpers/progress');
const chalk = require('chalk');
const addCheckMark = require('./helpers/checkmark');
const progress = animateProgress('Generating stats');
// Generate stats.json file with webpack
shelljs.exec(
'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json',
addCheckMark.bind(null, callback) // Output a checkmark on completion
);
// Called after webpack has finished generating the stats.json file
function callback() {
clearInterval(progress);
process.stdout.write(
'\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' +
chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n')
);
}
require('shelljs/global');
const addCheckMark = require('./helpers/checkmark.js');
if (!which('git')) {
echo('Sorry, this script requires git');
exit(1);
}
if (!test('-e', 'internals/templates')) {
echo('The example is deleted already.');
exit(1);
}
process.stdout.write('Cleanup started...');
// Cleanup components folder
rm('-rf', 'app/components/*');
// Cleanup containers folder
rm('-rf', 'app/containers/*');
mkdir('-p', 'app/containers/App');
mkdir('-p', 'app/containers/NotFoundPage');
mkdir('-p', 'app/containers/HomePage');
cp('internals/templates/appContainer.js', 'app/containers/App/index.js');
cp('internals/templates/constants.js', 'app/containers/App/constants.js');
cp('internals/templates/notFoundPage/notFoundPage.js', 'app/containers/NotFoundPage/index.js');
cp('internals/templates/notFoundPage/messages.js', 'app/containers/NotFoundPage/messages.js');
cp('internals/templates/homePage/homePage.js', 'app/containers/HomePage/index.js');
cp('internals/templates/homePage/messages.js', 'app/containers/HomePage/messages.js');
// Handle Translations
rm('-rf', 'app/translations/*')
mkdir('-p', 'app/translations');
cp('internals/templates/translations/en.json',
'app/translations/en.json');
// move i18n file
cp('internals/templates/i18n.js',
'app/i18n.js');
// Copy LanguageProvider
mkdir('-p', 'app/containers/LanguageProvider');
mkdir('-p', 'app/containers/LanguageProvider/tests');
cp('internals/templates/languageProvider/actions.js',
'app/containers/LanguageProvider/actions.js');
cp('internals/templates/languageProvider/constants.js',
'app/containers/LanguageProvider/constants.js');
cp('internals/templates/languageProvider/languageProvider.js',
'app/containers/LanguageProvider/index.js');
cp('internals/templates/languageProvider/reducer.js',
'app/containers/LanguageProvider/reducer.js');
cp('internals/templates/languageProvider/selectors.js',
'app/containers/LanguageProvider/selectors.js');
// Copy selectors
mkdir('app/containers/App/tests');
cp('internals/templates/selectors.js',
'app/containers/App/selectors.js');
cp('internals/templates/selectors.test.js',
'app/containers/App/tests/selectors.test.js');
// Utils
rm('-rf', 'app/utils');
mkdir('app/utils');
mkdir('app/utils/tests');
cp('internals/templates/asyncInjectors.js',
'app/utils/asyncInjectors.js');
cp('internals/templates/asyncInjectors.test.js',
'app/utils/tests/asyncInjectors.test.js');
// Replace the files in the root app/ folder
cp('internals/templates/app.js', 'app/app.js');
cp('internals/templates/index.html', 'app/index.html');
cp('internals/templates/reducers.js', 'app/reducers.js');
cp('internals/templates/routes.js', 'app/routes.js');
cp('internals/templates/store.js', 'app/store.js');
cp('internals/templates/store.test.js', 'app/tests/store.test.js');
// Remove the templates folder
rm('-rf', 'internals/templates');
addCheckMark();
// Commit the changes
if (exec('git add . --all && git commit -qm "Remove default example"').code !== 0) {
echo('\nError: Git commit failed');
exit(1);
}
echo('\nCleanup done. Happy Coding!!!');
// No need to build the DLL in production
if (process.env.NODE_ENV === 'production') {
process.exit(0);
}
require('shelljs/global');
const path = require('path');
const fs = require('fs');
const exists = fs.existsSync;
const writeFile = fs.writeFileSync;
const defaults = require('lodash/defaultsDeep');
const pkg = require(path.join(process.cwd(), 'package.json'));
const config = require('../config');
const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults);
const outputPath = path.join(process.cwd(), dllConfig.path);
const dllManifestPath = path.join(outputPath, 'package.json');
/**
* I use node_modules/react-boilerplate-dlls by default just because
* it isn't going to be version controlled and babel wont try to parse it.
*/
mkdir('-p', outputPath);
echo('Building the Webpack DLL...');
/**
* Create a manifest so npm install doesn't warn us
*/
if (!exists(dllManifestPath)) {
writeFile(
dllManifestPath,
JSON.stringify(defaults({
name: 'react-boilerplate-dlls',
private: true,
author: pkg.author,
repository: pkg.repository,
version: pkg.version,
}), null, 2),
'utf8'
);
}
// the BUILDING_DLL env var is set to avoid confusing the development environment
exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js');
/* eslint-disable */
/**
* This script will extract the internationalization messages from all components
and package them in the translation json files in the translations file.
*/
const fs = require('fs');
const nodeGlob = require('glob');
const transform = require('babel-core').transform;
const animateProgress = require('./helpers/progress');
const addCheckmark = require('./helpers/checkmark');
const pkg = require('../../package.json');
const i18n = require('../../app/i18n');
import { DEFAULT_LOCALE } from '../../app/containers/App/constants';
require('shelljs/global');
// Glob to match all js files except test files
const FILES_TO_PARSE = 'app/**/!(*.test).js';
const locales = i18n.appLocales;
const newLine = () => process.stdout.write('\n');
// Progress Logger
let progress;
const task = (message) => {
progress = animateProgress(message);
process.stdout.write(message);
return (error) => {
if (error) {
process.stderr.write(error);
}
clearTimeout(progress);
return addCheckmark(() => newLine());
}
}
// Wrap async functions below into a promise
const glob = (pattern) => new Promise((resolve, reject) => {
nodeGlob(pattern, (error, value) => (error ? reject(error) : resolve(value)));
});
const readFile = (fileName) => new Promise((resolve, reject) => {
fs.readFile(fileName, (error, value) => (error ? reject(error) : resolve(value)));
});
const writeFile = (fileName, data) => new Promise((resolve, reject) => {
fs.writeFile(fileName, data, (error, value) => (error ? reject(error) : resolve(value)));
});
// Store existing translations into memory
const oldLocaleMappings = [];
const localeMappings = [];
// Loop to run once per locale
for (const locale of locales) {
oldLocaleMappings[locale] = {};
localeMappings[locale] = {};
// File to store translation messages into
const translationFileName = `app/translations/${locale}.json`;
try {
// Parse the old translation message JSON files
const messages = JSON.parse(fs.readFileSync(translationFileName));
const messageKeys = Object.keys(messages);
for (const messageKey of messageKeys) {
oldLocaleMappings[locale][messageKey] = messages[messageKey];
}
} catch (error) {
if (error.code !== 'ENOENT') {
process.stderr.write(
`There was an error loading this translation file: ${translationFileName}
\n${error}`
);
}
}
}
const extractFromFile = async (fileName) => {
try {
const code = await readFile(fileName);
// Use babel plugin to extract instances where react-intl is used
const { metadata: result } = await transform(code, {
presets: pkg.babel.presets,
plugins: [
['react-intl'],
],
});
for (const message of result['react-intl'].messages) {
for (const locale of locales) {
const oldLocaleMapping = oldLocaleMappings[locale][message.id];
// Merge old translations into the babel extracted instances where react-intl is used
const newMsg = ( locale === DEFAULT_LOCALE) ? message.defaultMessage : '';
localeMappings[locale][message.id] = (oldLocaleMapping)
? oldLocaleMapping
: newMsg;
}
}
} catch (error) {
process.stderr.write(`Error transforming file: ${fileName}\n${error}`);
}
};
(async function main() {
const memoryTaskDone = task('Storing language files in memory');
const files = await glob(FILES_TO_PARSE);
memoryTaskDone()
const extractTaskDone = task('Run extraction on all files');
// Run extraction on all files that match the glob on line 16
await Promise.all(files.map((fileName) => extractFromFile(fileName)));
extractTaskDone()
// Make the directory if it doesn't exist, especially for first run
mkdir('-p', 'app/translations');
for (const locale of locales) {
const translationFileName = `app/translations/${locale}.json`;
try {
const localeTaskDone = task(
`Writing translation messages for ${locale} to: ${translationFileName}`
);
// Sort the translation JSON file so that git diffing is easier
// Otherwise the translation messages will jump around every time we extract
let messages = {};
Object.keys(localeMappings[locale]).sort().forEach(function(key) {
messages[key] = localeMappings[locale][key];
});
// Write to file the JSON representation of the translation messages
const prettified = `${JSON.stringify(messages, null, 2)}\n`;
await writeFile(translationFileName, prettified);
localeTaskDone();
} catch (error) {
localeTaskDone(
`There was an error saving this translation file: ${translationFileName}
\n${error}`
);
}
}
process.exit()
}());
const chalk = require('chalk');
/**
* Adds mark check symbol
*/
function addCheckMark(callback) {
process.stdout.write(chalk.green(' ✓'));
if (callback) callback();
}
module.exports = addCheckMark;
'use strict';
const readline = require('readline');
/**
* Adds an animated progress indicator
*
* @param {string} message The message to write next to the indicator
* @param {number} amountOfDots The amount of dots you want to animate
*/
function animateProgress(message, amountOfDots) {
if (typeof amountOfDots !== 'number') {
amountOfDots = 3;
}
let i = 0;
return setInterval(function() {
readline.cursorTo(process.stdout, 0);
i = (i + 1) % (amountOfDots + 1);
const dots = new Array(i + 1).join('.');
process.stdout.write(message + dots);
}, 500);
}
module.exports = animateProgress;
const exec = require('child_process').exec;
exec('npm -v', function (err, stdout, stderr) {
if (err) throw err;
if (parseFloat(stdout) < 3) {
throw new Error('[ERROR: React Boilerplate] You need npm version @>=3');
process.exit(1);
}
});
#!/usr/bin/env node
process.stdin.resume();
process.stdin.setEncoding('utf8');
const ngrok = require('ngrok');
const psi = require('psi');
const chalk = require('chalk');
log('\nStarting ngrok tunnel');
startTunnel(runPsi);
function runPsi(url) {
log('\nStarting PageSpeed Insights');
psi.output(url).then(function (err) {
process.exit(0);
});
}
function startTunnel(cb) {
ngrok.connect(3000, function (err, url) {
if (err) {
log(chalk.red('\nERROR\n' + err));
process.exit(0);
}
log('\nServing tunnel from: ' + chalk.magenta(url));
cb(url);
});
}
function log(string) {
process.stdout.write(string);
}
const webpackConfig = require('../webpack/webpack.test.babel');
const argv = require('minimist')(process.argv.slice(2));
const path = require('path');
module.exports = (config) => {
config.set({
frameworks: ['mocha'],
reporters: ['coverage', 'mocha'],
browsers: process.env.TRAVIS // eslint-disable-line no-nested-ternary
? ['ChromeTravis']
: process.env.APPVEYOR
? ['IE'] : ['Chrome'],
autoWatch: false,
singleRun: true,
client: {
mocha: {
grep: argv.grep,
},
},
files: [
{
pattern: './test-bundler.js',
watched: false,
served: true,
included: true,
},
],
preprocessors: {
['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key
},
webpack: webpackConfig,
// make Webpack bundle generation quiet
webpackMiddleware: {
noInfo: true,
stats: 'errors-only',
},
customLaunchers: {
ChromeTravis: {
base: 'Chrome',
flags: ['--no-sandbox'],
},
},
coverageReporter: {
dir: path.join(process.cwd(), 'coverage'),
reporters: [
{ type: 'lcov', subdir: 'lcov' },
{ type: 'html', subdir: 'html' },
{ type: 'text-summary' },
],
},
});
};
// needed for regenerator-runtime
// (ES7 generator support is required by redux-saga)
import 'babel-polyfill';
// If we need to use Chai, we'll have already chaiEnzyme loaded
import chai from 'chai';
import chaiEnzyme from 'chai-enzyme';
chai.use(chaiEnzyme());
// Include all .js files under `app`, except app.js, reducers.js, and routes.js.
// This is for code coverage
const context = require.context('../../app', true, /^^((?!(app|reducers|routes)).)*\.js$/);
context.keys().forEach(context);
/**
* COMMON WEBPACK CONFIGURATION
*/
const path = require('path');
const webpack = require('webpack');
module.exports = (options) => ({
entry: options.entry,
output: Object.assign({ // Compile into js/build.js
path: path.resolve(process.cwd(), 'build'),
publicPath: '/',
}, options.output), // Merge with env dependent settings
module: {
loaders: [{
test: /\.js$/, // Transform all .js files required somewhere with Babel
loader: 'babel',
exclude: /node_modules/,
query: options.babelQuery,
}, {
// Do not transform vendor's CSS with CSS-modules
// The point is that they remain in global scope.
// Since we require these CSS files in our JS or CSS files,
// they will be a part of our compilation either way.
// So, no need for ExtractTextPlugin here.
test: /\.css$/,
include: /node_modules/,
loaders: ['style-loader', 'css-loader'],
}, {
test: /\.(eot|svg|ttf|woff|woff2)$/,
loader: 'file-loader',
}, {
test: /\.(jpg|png|gif)$/,
loaders: [
'file-loader',
'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}',
],
}, {
test: /\.html$/,
loader: 'html-loader',
}, {
test: /\.json$/,
loader: 'json-loader',
}, {
test: /\.(mp4|webm)$/,
loader: 'url-loader?limit=10000',
}],
},
plugins: options.plugins.concat([
new webpack.ProvidePlugin({
// make fetch available
fetch: 'exports?self.fetch!whatwg-fetch',
}),
// Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV`
// inside your code for any environment checks; UglifyJS will automatically
// drop any unreachable code.
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
}),
new webpack.NamedModulesPlugin(),
]),
resolve: {
modules: ['src', 'node_modules'],
extensions: [
'.js',
'.jsx',
'.react.js',
'.scss',
'.css'
],
mainFields: [
'browser',
'jsnext:main',
'main',
],
},
devtool: options.devtool,
target: 'web', // Make web variables accessible to webpack, e.g. window
});
/**
* DEVELOPMENT WEBPACK CONFIGURATION
*/
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const logger = require('../../server/logger');
const cheerio = require('cheerio');
const pkg = require(path.resolve(process.cwd(), 'package.json'));
const dllPlugin = pkg.dllPlugin;
const plugins = [
new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading
new webpack.NoErrorsPlugin(),
new HtmlWebpackPlugin({
inject: true, // Inject all files that are generated by webpack, e.g. bundle.js
templateContent: templateContent(), // eslint-disable-line no-use-before-define
}),
];
module.exports = require('./webpack.base.babel')({
// Add hot reloading in development
entry: [
'eventsource-polyfill', // Necessary for hot reloading with IE
'webpack-hot-middleware/client',
path.join(process.cwd(), 'src/app.js'), // Start with js/app.js
],
// Don't use hashes in dev mode for better performance
output: {
filename: '[name].js',
chunkFilename: '[name].chunk.js',
},
// Add development plugins
plugins: dependencyHandlers().concat(plugins), // eslint-disable-line no-use-before-define
// Tell babel that we want to hot-reload
babelQuery: {
presets: ['react-hmre'],
},
// Emit a source map for easier debugging
devtool: 'cheap-module-eval-source-map',
});
/**
* Select which plugins to use to optimize the bundle's handling of
* third party dependencies.
*
* If there is a dllPlugin key on the project's package.json, the
* Webpack DLL Plugin will be used. Otherwise the CommonsChunkPlugin
* will be used.
*
*/
function dependencyHandlers() {
// Don't do anything during the DLL Build step
if (process.env.BUILDING_DLL) { return []; }
// If the package.json does not have a dllPlugin property, use the CommonsChunkPlugin
if (!dllPlugin) {
return [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
children: true,
minChunks: 2,
async: true,
}),
];
}
const dllPath = path.resolve(process.cwd(), dllPlugin.path || 'node_modules/react-boilerplate-dlls');
/**
* If DLLs aren't explicitly defined, we assume all production dependencies listed in package.json
* Reminder: You need to exclude any server side dependencies by listing them in dllConfig.exclude
*
* @see https://github.com/mxstbr/react-boilerplate/tree/master/docs/general/webpack.md
*/
if (!dllPlugin.dlls) {
const manifestPath = path.resolve(dllPath, 'reactBoilerplateDeps.json');
if (!fs.existsSync(manifestPath)) {
logger.error('The DLL manifest is missing. Please run `npm run build:dll`');
process.exit(0);
}
return [
new webpack.DllReferencePlugin({
context: process.cwd(),
manifest: require(manifestPath), // eslint-disable-line global-require
}),
];
}
// If DLLs are explicitly defined, we automatically create a DLLReferencePlugin for each of them.
const dllManifests = Object.keys(dllPlugin.dlls).map((name) => path.join(dllPath, `/${name}.json`));
return dllManifests.map((manifestPath) => {
if (!fs.existsSync(path)) {
if (!fs.existsSync(manifestPath)) {
logger.error(`The following Webpack DLL manifest is missing: ${path.basename(manifestPath)}`);
logger.error(`Expected to find it in ${dllPath}`);
logger.error('Please run: npm run build:dll');
process.exit(0);
}
}
return new webpack.DllReferencePlugin({
context: process.cwd(),
manifest: require(manifestPath), // eslint-disable-line global-require
});
});
}
/**
* We dynamically generate the HTML content in development so that the different
* DLL Javascript files are loaded in script tags and available to our application.
*/
function templateContent() {
const html = fs.readFileSync(
path.resolve(process.cwd(), 'src/index.html')
).toString();
if (!dllPlugin) { return html; }
const doc = cheerio(html);
const body = doc.find('body');
const dllNames = !dllPlugin.dlls ? ['reactBoilerplateDeps'] : Object.keys(dllPlugin.dlls);
dllNames.forEach((dllName) => body.append(`<script data-dll='true' src='/${dllName}.dll.js'></script>`));
return doc.toString();
}
/**
* WEBPACK DLL GENERATOR
*
* This profile is used to cache webpack's module
* contexts for external library and framework type
* dependencies which will usually not change often enough
* to warrant building them from scratch every time we use
* the webpack process.
*/
const { join } = require('path');
const defaults = require('lodash/defaultsDeep');
const webpack = require('webpack');
const pkg = require(join(process.cwd(), 'package.json'));
const dllPlugin = require('../config').dllPlugin;
if (!pkg.dllPlugin) { process.exit(0); }
const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults);
const outputPath = join(process.cwd(), dllConfig.path);
module.exports = require('./webpack.base.babel')({
context: process.cwd(),
entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg),
devtool: 'eval',
output: {
filename: '[name].dll.js',
path: outputPath,
library: '[name]',
},
plugins: [
new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }), // eslint-disable-line no-new
],
});
// Important modules this config uses
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const OfflinePlugin = require('offline-plugin');
module.exports = require('./webpack.base.babel')({
// In production, we skip all hot-reloading stuff
entry: [
path.join(process.cwd(), 'src/app.js'),
],
// Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets
output: {
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].chunk.js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
children: true,
minChunks: 2,
async: true,
}),
// Merge all duplicate modules
new webpack.optimize.DedupePlugin(),
// Minify and optimize the index.html
new HtmlWebpackPlugin({
template: 'app/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
inject: true,
}),
// Put it in the end to capture all the HtmlWebpackPlugin's
// assets manipulations and do leak its manipulations to HtmlWebpackPlugin
new OfflinePlugin({
relativePaths: false,
publicPath: '/',
// No need to cache .htaccess. See http://mxs.is/googmp,
// this is applied before any match in `caches` section
excludes: ['.htaccess'],
caches: {
main: [':rest:'],
// All chunks marked as `additional`, loaded after main section
// and do not prevent SW to install. Change to `optional` if
// do not want them to be preloaded at all (cached only when first loaded)
additional: ['*.chunk.js'],
},
// Removes warning for about `additional` section usage
safeToUseOptionalCaches: true,
AppCache: false,
}),
],
});
/**
* TEST WEBPACK CONFIGURATION
*/
const webpack = require('webpack');
const modules = [
'app',
'node_modules',
];
module.exports = {
devtool: 'inline-source-map',
module: {
// Some libraries don't like being run through babel.
// If they gripe, put them here.
noParse: [
/node_modules(\\|\/)sinon/,
/node_modules(\\|\/)acorn/,
],
loaders: [
{ test: /\.json$/, loader: 'json-loader' },
{ test: /\.css$/, loader: 'null-loader' },
// sinon.js--aliased for enzyme--expects/requires global vars.
// imports-loader allows for global vars to be injected into the module.
// See https://github.com/webpack/webpack/issues/304
{ test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/,
loader: 'imports?define=>false,require=>false',
},
{ test: /\.js$/,
loader: 'babel',
exclude: [/node_modules/],
},
{ test: /\.jpe?g$|\.gif$|\.png$|\.svg$/i,
loader: 'null-loader',
},
],
},
plugins: [
// Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV`
// inside your code for any environment checks; UglifyJS will automatically
// drop any unreachable code.
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
})],
// Some node_modules pull in Node-specific dependencies.
// Since we're running in a browser we have to stub them out. See:
// https://webpack.github.io/docs/configuration.html#node
// https://github.com/webpack/node-libs-browser/tree/master/mock
// https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520
node: {
fs: 'empty',
child_process: 'empty',
net: 'empty',
tls: 'empty',
},
// required for enzyme to work properly
externals: {
jsdom: 'window',
'react/addons': true,
'react/lib/ExecutionEnvironment': true,
'react/lib/ReactContext': 'window',
},
resolve: {
modules,
alias: {
// required for enzyme to work properly
sinon: 'sinon/pkg/sinon',
},
},
};
{
"name": "ik_invoicing",
"version": "1.0.0",
"description": "ik invoicing for dingtalk",
"repository": {
"type": "git",
"url": "http://gitlab.ikcrm.com/ikcrm_frontend/ik_invoicing.git"
},
"engines": {
"npm": ">=3"
},
"author": "ik developer",
"license": "MIT",
"scripts": {
"analyze:clean": "rimraf stats.json",
"preanalyze": "npm run analyze:clean",
"analyze": "node ./internals/scripts/analyze.js",
"extract-intl": "babel-node --presets latest,stage-0 -- ./internals/scripts/extract-intl.js",
"npmcheckversion": "node ./internals/scripts/npmcheckversion.js",
"preinstall": "npm run npmcheckversion",
"postinstall": "npm run build:dll",
"prebuild": "npm run build:clean && npm run test",
"build": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.babel.js --color -p --progress",
"build:clean": "npm run test:clean && rimraf ./build",
"build:dll": "node ./internals/scripts/dependencies.js",
"start": "cross-env NODE_ENV=development node server",
"start:tunnel": "cross-env NODE_ENV=development ENABLE_TUNNEL=true node server",
"start:production": "npm run build && npm run start:prod",
"start:prod": "cross-env NODE_ENV=production node server",
"pagespeed": "node ./internals/scripts/pagespeed.js",
"presetup": "npm i chalk shelljs",
"setup": "node ./internals/scripts/setup.js",
"postsetup": "npm run build:dll",
"clean": "shjs ./internals/scripts/clean.js",
"clean:all": "npm run analyze:clean && npm run test:clean && npm run build:clean",
"generate": "plop --plopfile internals/generators/index.js",
"lint": "npm run lint:js",
"lint:eslint": "eslint --ignore-path .gitignore --ignore-pattern internals/scripts",
"lint:js": "npm run lint:eslint -- . ",
"lint:staged": "lint-staged",
"pretest": "npm run test:clean && npm run lint",
"test:clean": "rimraf ./coverage",
"test": "cross-env NODE_ENV=test karma start internals/testing/karma.conf.js --single-run",
"test:watch": "npm run test -- --auto-watch --no-single-run",
"test:firefox": "npm run test -- --browsers Firefox",
"test:safari": "npm run test -- --browsers Safari",
"test:ie": "npm run test -- --browsers IE",
"coveralls": "cat ./coverage/lcov/lcov.info | coveralls"
},
"lint-staged": {
"*.js": "lint:eslint"
},
"pre-commit": "lint:staged",
"dllPlugin": {
"path": "node_modules/react-boilerplate-dlls",
"exclude": [
"chalk",
"compression",
"cross-env",
"express",
"ip",
"minimist",
"sanitize.css"
],
"include": [
"core-js",
"lodash",
"eventsource-polyfill"
]
},
"dependencies": {
"babel-polyfill": "6.16.0",
"chalk": "1.1.3",
"compression": "1.6.2",
"cross-env": "3.1.3",
"express": "4.14.0",
"fontfaceobserver": "2.0.5",
"immutable": "3.8.1",
"intl": "1.2.5",
"invariant": "2.2.1",
"ip": "1.1.3",
"lodash": "4.16.4",
"minimist": "1.2.0",
"react": "15.3.2",
"react-dom": "15.3.2",
"react-helmet": "3.1.0",
"react-redux": "4.4.5",
"react-router": "3.0.0",
"react-router-redux": "4.0.6",
"react-router-scroll": "0.3.3",
"redux": "3.6.0",
"redux-immutable": "3.0.8",
"redux-saga": "0.12.0",
"reselect": "2.5.4",
"sanitize.css": "4.1.0",
"styled-components": "1.0.3",
"warning": "3.0.0",
"whatwg-fetch": "1.0.0"
},
"devDependencies": {
"babel-cli": "6.18.0",
"babel-core": "6.18.0",
"babel-eslint": "7.1.0",
"babel-loader": "6.2.7",
"babel-plugin-istanbul": "2.0.3",
"babel-plugin-react-intl": "2.2.0",
"babel-plugin-react-transform": "2.0.2",
"babel-plugin-transform-react-constant-elements": "6.9.1",
"babel-plugin-transform-react-inline-elements": "6.8.0",
"babel-plugin-transform-react-remove-prop-types": "0.2.10",
"babel-preset-latest": "6.16.0",
"babel-preset-react": "6.16.0",
"babel-preset-react-hmre": "1.1.1",
"babel-preset-stage-0": "6.16.0",
"chai": "3.5.0",
"chai-enzyme": "0.5.2",
"cheerio": "0.22.0",
"coveralls": "2.11.14",
"css-loader": "0.25.0",
"enzyme": "2.5.1",
"eslint": "3.9.0",
"eslint-config-airbnb": "12.0.0",
"eslint-config-airbnb-base": "9.0.0",
"eslint-import-resolver-webpack": "0.6.0",
"eslint-plugin-import": "2.0.1",
"eslint-plugin-jsx-a11y": "2.2.3",
"eslint-plugin-react": "6.4.1",
"eslint-plugin-redux-saga": "0.1.5",
"eventsource-polyfill": "0.9.6",
"expect": "1.20.2",
"expect-jsx": "2.6.0",
"exports-loader": "0.6.3",
"file-loader": "0.9.0",
"html-loader": "0.4.4",
"html-webpack-plugin": "2.24.0",
"image-webpack-loader": "2.0.0",
"imports-loader": "0.6.5",
"json-loader": "0.5.4",
"karma": "1.3.0",
"karma-chrome-launcher": "2.0.0",
"karma-coverage": "1.1.1",
"karma-firefox-launcher": "1.0.0",
"karma-ie-launcher": "1.0.0",
"karma-mocha": "1.2.0",
"karma-mocha-reporter": "2.2.0",
"karma-safari-launcher": "1.0.0",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "1.8.0",
"lint-staged": "3.2.0",
"mocha": "3.1.2",
"ngrok": "2.2.3",
"null-loader": "0.1.1",
"offline-plugin": "3.4.2",
"plop": "1.5.0",
"pre-commit": "1.1.3",
"psi": "2.0.4",
"react-addons-test-utils": "15.3.2",
"rimraf": "2.5.4",
"shelljs": "0.7.5",
"sinon": "2.0.0-pre",
"style-loader": "0.13.1",
"url-loader": "0.5.7",
"webpack": "2.1.0-beta.25",
"webpack-dev-middleware": "1.8.4",
"webpack-hot-middleware": "2.13.1"
}
}
/* eslint consistent-return:0 */
const express = require('express');
const logger = require('./logger');
const argv = require('minimist')(process.argv.slice(2));
const setup = require('./middlewares/frontendMiddleware');
const isDev = process.env.NODE_ENV !== 'production';
const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false;
const resolve = require('path').resolve;
const app = express();
// If you need a backend, e.g. an API, add your custom backend-specific middleware here
// app.use('/api', myApi);
// In production we need to pass these values in instead of relying on webpack
setup(app, {
outputPath: resolve(process.cwd(), 'build'),
publicPath: '/',
});
// get the intended port number, use port 3000 if not provided
const port = argv.port || process.env.PORT || 3000;
// Start your app.
app.listen(port, (err) => {
if (err) {
return logger.error(err.message);
}
// Connect to ngrok in dev mode
if (ngrok) {
ngrok.connect(port, (innerErr, url) => {
if (innerErr) {
return logger.error(innerErr);
}
logger.appStarted(port, url);
});
} else {
logger.appStarted(port);
}
});
/* eslint-disable no-console */
const chalk = require('chalk');
const ip = require('ip');
const divider = chalk.gray('\n-----------------------------------');
/**
* Logger middleware, you can customize it to make messages more personal
*/
const logger = {
// Called whenever there's an error on the server we want to print
error: (err) => {
console.error(chalk.red(err));
},
// Called when express.js app starts on given port w/o errors
appStarted: (port, tunnelStarted) => {
console.log(`Server started ${chalk.green('✓')}`);
// If the tunnel started, log that and the URL it's available at
if (tunnelStarted) {
console.log(`Tunnel initialised ${chalk.green('✓')}`);
}
console.log(`
${chalk.bold('Access URLs:')}${divider}
Localhost: ${chalk.magenta(`http://localhost:${port}`)}
LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) +
(tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider}
${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)}
`);
},
};
module.exports = logger;
/* eslint-disable global-require */
const express = require('express');
const path = require('path');
const compression = require('compression');
const pkg = require(path.resolve(process.cwd(), 'package.json'));
// Dev middleware
const addDevMiddlewares = (app, webpackConfig) => {
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const compiler = webpack(webpackConfig);
const middleware = webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: webpackConfig.output.publicPath,
silent: true,
stats: 'errors-only',
});
app.use(middleware);
app.use(webpackHotMiddleware(compiler));
// Since webpackDevMiddleware uses memory-fs internally to store build
// artifacts, we use it instead
const fs = middleware.fileSystem;
if (pkg.dllPlugin) {
app.get(/\.dll\.js$/, (req, res) => {
const filename = req.path.replace(/^\//, '');
res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename));
});
}
app.get('*', (req, res) => {
fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => {
if (err) {
res.sendStatus(404);
} else {
res.send(file.toString());
}
});
});
};
// Production middlewares
const addProdMiddlewares = (app, options) => {
const publicPath = options.publicPath || '/';
const outputPath = options.outputPath || path.resolve(process.cwd(), 'build');
// compression middleware compresses your server responses which makes them
// smaller (applies also to assets). You can read more about that technique
// and other good practices on official Express.js docs http://mxs.is/googmy
app.use(compression());
app.use(publicPath, express.static(outputPath));
app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html')));
};
/**
* Front-end middleware
*/
module.exports = (app, options) => {
const isProd = process.env.NODE_ENV === 'production';
if (isProd) {
addProdMiddlewares(app, options);
} else {
const webpackConfig = require('../../internals/webpack/webpack.dev.babel');
addDevMiddlewares(app, webpackConfig);
}
return app;
};
<ifModule mod_rewrite.c>
#######################################################################
# GENERAL #
#######################################################################
# Make apache follow sym links to files
Options +FollowSymLinks
# If somebody opens a folder, hide all files from the resulting folder list
IndexIgnore */*
#######################################################################
# REWRITING #
#######################################################################
# Enable rewriting
RewriteEngine On
# If its not HTTPS
RewriteCond %{HTTPS} off
# Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL
# RewriteCond %{HTTP:X-Forwarded-Proto} !https
# Redirect to the same URL with https://, ignoring all further rules if this one is in effect
RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L]
# If we get to here, it means we are on https://
# If the file with the specified name in the browser doesn't exist
RewriteCond %{REQUEST_FILENAME} !-f
# and the directory with the specified name in the browser doesn't exist
RewriteCond %{REQUEST_FILENAME} !-d
# and we are not opening the root already (otherwise we get a redirect loop)
RewriteCond %{REQUEST_FILENAME} !\/$
# Rewrite all requests to the root
RewriteRule ^(.*) /
</ifModule>
##
# Put this file in /etc/nginx/conf.d folder and make sure
# you have line 'include /etc/nginx/conf.d/*.conf;'
# in your main nginx configuration file
##
##
# Redirect to the same URL with https://
##
server {
listen 80;
# Type your domain name below
server_name example.com;
return 301 https://$server_name$request_uri;
}
##
# HTTPS configurations
##
server {
listen 443;
# Type your domain name below
server_name example.com;
ssl on;
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/server.key;
# Use only TSL protocols for more secure
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# Always serve index.html for any request
location / {
# Set path
root /var/www/;
try_files $uri /index.html;
}
##
# If you want to use Node/Rails/etc. API server
# on the same port (443) config Nginx as a reverse proxy.
# For security reasons use a firewall like ufw in Ubuntu
# and deny port 3000/tcp.
##
# location /api/ {
#
# proxy_pass http://localhost:3000;
# proxy_http_version 1.1;
# proxy_set_header X-Forwarded-Proto https;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_cache_bypass $http_upgrade;
#
# }
}
/**
* app.js
*
* This is the entry file for the application, only setup and boilerplate
* code.
*/
import 'babel-polyfill';
/* eslint-disable import/no-unresolved, import/extensions */
// Load the manifest.json file and the .htaccess file
import '!file?name=[name].[ext]!./manifest.json';
import 'file?name=[name].[ext]!./.htaccess';
/* eslint-enable import/no-unresolved, import/extensions */
// Import all the third party stuff
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { applyRouterMiddleware, Router, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import { useScroll } from 'react-router-scroll';
import LanguageProvider from 'containers/LanguageProvider';
import configureStore from './store';
// Import i18n messages
import { translationMessages } from './i18n';
// Import the CSS reset, which HtmlWebpackPlugin transfers to the build folder
import 'sanitize.css/sanitize.css';
// Create redux store with history
// this uses the singleton browserHistory provided by react-router
// Optionally, this could be changed to leverage a created history
// e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();`
const initialState = {};
const store = configureStore(initialState, browserHistory);
// Sync history and store, as the react-router-redux reducer
// is under the non-default key ("routing"), selectLocationState
// must be provided for resolving how to retrieve the "route" in the state
import { selectLocationState } from 'containers/App/selectors';
const history = syncHistoryWithStore(browserHistory, store, {
selectLocationState: selectLocationState(),
});
// Set up the router, wrapping all Routes in the App component
import App from 'containers/App';
import createRoutes from './routes';
const rootRoute = {
component: App,
childRoutes: createRoutes(store),
};
const render = (translatedMessages) => {
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={translatedMessages}>
<Router
history={history}
routes={rootRoute}
render={
// Scroll to top when going to a new page, imitating default browser
// behaviour
applyRouterMiddleware(useScroll())
}
/>
</LanguageProvider>
</Provider>,
document.getElementById('app')
);
};
// Hot reloadable translation json files
if (module.hot) {
// modules.hot.accept does not accept dynamic dependencies,
// have to be constants at compile-time
module.hot.accept('./i18n', () => {
render(translationMessages);
});
}
// Chunked polyfill for browsers without Intl support
if (!window.Intl) {
(new Promise((resolve) => {
resolve(System.import('intl'));
}))
.then(() => Promise.all([
System.import('intl/locale-data/jsonp/de.js'),
]))
.then(() => render(translationMessages))
.catch((err) => {
throw err;
});
} else {
render(translationMessages);
}
// Install ServiceWorker and AppCache in the end since
// it's not most important operation and if main code fails,
// we do not want it installed
import { install } from 'offline-plugin/runtime';
install();
/*
* AppConstants
* Each action has a corresponding type, which the reducer knows and picks up on.
* To avoid weird typos between the reducer and the actions, we save them as
* constants here. We prefix them with 'yourproject/YourComponent' so we avoid
* reducers accidentally picking up actions they shouldn't.
*
* Follow this format:
* export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT';
*/
export const DEFAULT_LOCALE = 'en';
/**
*
* App.react.js
*
* This component is the skeleton around the actual pages, and should only
* contain code that should be seen on all pages. (e.g. navigation bar)
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a necessity for you then you can refactor it and remove
* the linting exception.
*/
import React from 'react';
export default class App extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function
static propTypes = {
children: React.PropTypes.node,
};
render() {
return (
<div>
{React.Children.toArray(this.props.children)}
</div>
);
}
}
// selectLocationState expects a plain JS object for the routing state
const selectLocationState = () => {
let prevRoutingState;
let prevRoutingStateJS;
return (state) => {
const routingState = state.get('route'); // or state.route
if (!routingState.equals(prevRoutingState)) {
prevRoutingState = routingState;
prevRoutingStateJS = routingState.toJS();
}
return prevRoutingStateJS;
};
};
export {
selectLocationState,
};
import { fromJS } from 'immutable';
import expect from 'expect';
import { selectLocationState } from 'containers/App/selectors';
describe('selectLocationState', () => {
it('should select the route as a plain JS object', () => {
const route = fromJS({
locationBeforeTransitions: null,
});
const mockedState = fromJS({
route,
});
expect(selectLocationState()(mockedState)).toEqual(route.toJS());
});
});
/*
* HomePage
*
* This is the first thing users see of our App, at the '/' route
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a necessity for you then you can refactor it and remove
* the linting exception.
*/
import React from 'react';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
export default class HomePage extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<h1>
<FormattedMessage {...messages.header} />
</h1>
);
}
}
/*
* HomePage Messages
*
* This contains all the text for the HomePage component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
header: {
id: 'app.components.HomePage.header',
defaultMessage: 'This is HomePage components !',
},
});
/*
*
* LanguageProvider actions
*
*/
import {
CHANGE_LOCALE,
} from './constants';
export function changeLocale(languageLocale) {
return {
type: CHANGE_LOCALE,
locale: languageLocale,
};
}
/*
*
* LanguageProvider constants
*
*/
export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE';
export const DEFAULT_LOCALE = 'en';
/*
*
* LanguageProvider
*
* this component connects the redux state language locale to the
* IntlProvider component and i18n messages (loaded from `app/translations`)
*/
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { IntlProvider } from 'react-intl';
import { selectLocale } from './selectors';
export class LanguageProvider extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<IntlProvider locale={this.props.locale} key={this.props.locale} messages={this.props.messages[this.props.locale]}>
{React.Children.only(this.props.children)}
</IntlProvider>
);
}
}
LanguageProvider.propTypes = {
locale: React.PropTypes.string,
messages: React.PropTypes.object,
children: React.PropTypes.element.isRequired,
};
const mapStateToProps = createSelector(
selectLocale(),
(locale) => ({ locale })
);
function mapDispatchToProps(dispatch) {
return {
dispatch,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider);
/*
*
* LanguageProvider reducer
*
*/
import { fromJS } from 'immutable';
import {
CHANGE_LOCALE,
} from './constants';
import {
DEFAULT_LOCALE,
} from '../App/constants'; // eslint-disable-line
const initialState = fromJS({
locale: DEFAULT_LOCALE,
});
function languageProviderReducer(state = initialState, action) {
switch (action.type) {
case CHANGE_LOCALE:
return state
.set('locale', action.locale);
default:
return state;
}
}
export default languageProviderReducer;
import { createSelector } from 'reselect';
/**
* Direct selector to the languageToggle state domain
*/
const selectLanguage = () => (state) => state.get('language');
/**
* Select the language locale
*/
const selectLocale = () => createSelector(
selectLanguage(),
(languageState) => languageState.get('locale')
);
export {
selectLanguage,
selectLocale,
};
/**
* NotFoundPage
*
* This is the page we show when the user visits a url that doesn't have a route
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a necessity for you then you can refactor it and remove
* the linting exception.
*/
import React from 'react';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
export default class NotFound extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<h1>
<FormattedMessage {...messages.header} />
</h1>
);
}
}
/*
* NotFoundPage Messages
*
* This contains all the text for the NotFoundPage component.
*/
import { defineMessages } from 'react-intl';
export default defineMessages({
header: {
id: 'app.components.NotFoundPage.header',
defaultMessage: 'This is NotFoundPage component !',
},
});
This diff was suppressed by a .gitattributes entry.
import { injectGlobal } from 'styled-components';
/* eslint no-unused-expressions: 0 */
injectGlobal`
html,
body {
height: 100%;
width: 100%;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
body.fontLoaded {
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
#app {
background-color: #fafafa;
min-height: 100%;
min-width: 100%;
}
p,
label {
font-family: Georgia, Times, 'Times New Roman', serif;
line-height: 1.5em;
}
`;
/**
* i18n.js
*
* This will setup the i18n language files and locale data for your app.
*
*/
import { addLocaleData } from 'react-intl';
import { DEFAULT_LOCALE } from './containers/App/constants'; // eslint-disable-line
import enLocaleData from 'react-intl/locale-data/en';
export const appLocales = [
'en',
];
import enTranslationMessages from './translations/en.json';
addLocaleData(enLocaleData);
export const formatTranslationMessages = (locale, messages) => {
const defaultFormattedMessages = locale !== DEFAULT_LOCALE ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) : {};
const formattedMessages = {};
const messageKeys = Object.keys(messages);
for (const messageKey of messageKeys) {
if (locale === DEFAULT_LOCALE) {
formattedMessages[messageKey] = messages[messageKey];
} else {
formattedMessages[messageKey] = messages[messageKey] || defaultFormattedMessages[messageKey];
}
}
return formattedMessages;
};
export const translationMessages = {
en: formatTranslationMessages('en', enTranslationMessages),
};
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Make the page mobile compatible -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Allow installing the app to the homescreen -->
<link rel="manifest" href="manifest.json">
<meta name="mobile-web-app-capable" content="yes">
<title>爱客进销存</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
{
"name": "爱客进销存",
"icons": [
{
"src": "favicon.png",
"sizes": "48x48",
"type": "image/png",
"density": 1.0
},
{
"src": "favicon.png",
"sizes": "96x96",
"type": "image/png",
"density": 2.0
},
{
"src": "favicon.png",
"sizes": "144x144",
"type": "image/png",
"density": 3.0
},
{
"src": "favicon.png",
"sizes": "192x192",
"type": "image/png",
"density": 4.0
}
],
"start_url": "index.html",
"display": "standalone",
"orientation": "portrait",
"background_color": "#FFFFFF"
}
\ No newline at end of file
/**
* Combine all reducers in this file and export the combined reducers.
* If we were to do this in store.js, reducers wouldn't be hot reloadable.
*/
import { combineReducers } from 'redux-immutable';
import { fromJS } from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
import languageProviderReducer from 'containers/LanguageProvider/reducer';
/*
* routeReducer
*
* The reducer merges route location changes into our immutable state.
* The change is necessitated by moving to react-router-redux@4
*
*/
// Initial routing state
const routeInitialState = fromJS({
locationBeforeTransitions: null,
});
/**
* Merge route into the global application state
*/
function routeReducer(state = routeInitialState, action) {
switch (action.type) {
/* istanbul ignore next */
case LOCATION_CHANGE:
return state.merge({
locationBeforeTransitions: action.payload,
});
default:
return state;
}
}
/**
* Creates the main reducer with the asynchronously loaded ones
*/
export default function createReducer(asyncReducers) {
return combineReducers({
route: routeReducer,
language: languageProviderReducer,
...asyncReducers,
});
}
// These are the pages you can go to.
// They are all wrapped in the App component, which should contain the navbar etc
// See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information
// about the code splitting business
import { getAsyncInjectors } from 'utils/asyncInjectors';
const errorLoading = (err) => {
console.error('Dynamic page loading failed', err); // eslint-disable-line no-console
};
const loadModule = (cb) => (componentModule) => {
cb(null, componentModule.default);
};
export default function createRoutes(store) {
// Create reusable async injectors using getAsyncInjectors factory
const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars
return [
{
path: '/',
name: 'home',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/HomePage'),
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
},
}, {
path: '*',
name: 'notfound',
getComponent(nextState, cb) {
System.import('containers/NotFoundPage')
.then(loadModule(cb))
.catch(errorLoading);
},
},
];
}
/**
* Create the store with asynchronously loaded reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';
const sagaMiddleware = createSagaMiddleware();
export default function configureStore(initialState = {}, history) {
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [
sagaMiddleware,
routerMiddleware(history),
];
const enhancers = [
applyMiddleware(...middlewares),
];
// If Redux DevTools Extension is installed use it, otherwise use Redux compose
/* eslint-disable no-underscore-dangle */
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;
/* eslint-enable */
const store = createStore(
createReducer(),
fromJS(initialState),
composeEnhancers(...enhancers)
);
// Extensions
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {}; // Async reducer registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
System.import('./reducers').then((reducerModule) => {
const createReducers = reducerModule.default;
const nextReducers = createReducers(store.asyncReducers);
store.replaceReducer(nextReducers);
});
});
}
return store;
}
/**
* Test store addons
*/
import expect from 'expect';
import configureStore from '../store'; // eslint-disable-line
import { browserHistory } from 'react-router';
describe('configureStore', () => {
let store;
before(() => {
store = configureStore({}, browserHistory);
});
describe('asyncReducers', () => {
it('should contain an object for async reducers', () => {
expect(typeof store.asyncReducers).toEqual('object');
});
});
describe('runSaga', () => {
it('should contain a hook for `sagaMiddleware.run`', () => {
expect(typeof store.runSaga).toEqual('function');
});
});
});
import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash';
import invariant from 'invariant';
import warning from 'warning';
import createReducer from 'reducers';
/**
* Validate the shape of redux store
*/
export function checkStore(store) {
const shape = {
dispatch: isFunction,
subscribe: isFunction,
getState: isFunction,
replaceReducer: isFunction,
runSaga: isFunction,
asyncReducers: isObject,
};
invariant(
conformsTo(store, shape),
'(app/utils...) asyncInjectors: Expected a valid redux store'
);
}
/**
* Inject an asynchronously loaded reducer
*/
export function injectAsyncReducer(store, isValid) {
return function injectReducer(name, asyncReducer) {
if (!isValid) checkStore(store);
invariant(
isString(name) && !isEmpty(name) && isFunction(asyncReducer),
'(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function'
);
if (Reflect.has(store.asyncReducers, name)) return;
store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.asyncReducers));
};
}
/**
* Inject an asynchronously loaded saga
*/
export function injectAsyncSagas(store, isValid) {
return function injectSagas(sagas) {
if (!isValid) checkStore(store);
invariant(
Array.isArray(sagas),
'(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions'
);
warning(
!isEmpty(sagas),
'(app/utils...) injectAsyncSagas: Received an empty `sagas` array'
);
sagas.map(store.runSaga);
};
}
/**
* Helper for creating injectors
*/
export function getAsyncInjectors(store) {
checkStore(store);
return {
injectReducer: injectAsyncReducer(store, true),
injectSagas: injectAsyncSagas(store, true),
};
}
/**
* Test async injectors
*/
import expect from 'expect';
import configureStore from 'store';
import { memoryHistory } from 'react-router';
import { put } from 'redux-saga/effects';
import { fromJS } from 'immutable';
import {
injectAsyncReducer,
injectAsyncSagas,
getAsyncInjectors,
} from 'utils/asyncInjectors';
// Fixtures
const initialState = fromJS({ reduced: 'soon' });
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'TEST':
return state.set('reduced', action.payload);
default:
return state;
}
};
function* testSaga() {
yield put({ type: 'TEST', payload: 'yup' });
}
const sagas = [
testSaga,
];
describe('asyncInjectors', () => {
let store;
describe('getAsyncInjectors', () => {
before(() => {
store = configureStore({}, memoryHistory);
});
it('given a store, should return all async injectors', () => {
const { injectReducer, injectSagas } = getAsyncInjectors(store);
injectReducer('test', reducer);
injectSagas(sagas);
const actual = store.getState().get('test');
const expected = initialState.merge({ reduced: 'yup' });
expect(actual.toJS()).toEqual(expected.toJS());
});
it('should throw if passed invalid store shape', () => {
let result = false;
Reflect.deleteProperty(store, 'dispatch');
try {
getAsyncInjectors(store);
} catch (err) {
result = err.name === 'Invariant Violation';
}
expect(result).toEqual(true);
});
});
describe('helpers', () => {
before(() => {
store = configureStore({}, memoryHistory);
});
describe('injectAsyncReducer', () => {
it('given a store, it should provide a function to inject a reducer', () => {
const injectReducer = injectAsyncReducer(store);
injectReducer('test', reducer);
const actual = store.getState().get('test');
const expected = initialState;
expect(actual.toJS()).toEqual(expected.toJS());
});
it('should throw if passed invalid name', () => {
let result = false;
const injectReducer = injectAsyncReducer(store);
try {
injectReducer('', reducer);
} catch (err) {
result = err.name === 'Invariant Violation';
}
try {
injectReducer(999, reducer);
} catch (err) {
result = err.name === 'Invariant Violation';
}
expect(result).toEqual(true);
});
it('should throw if passed invalid reducer', () => {
let result = false;
const injectReducer = injectAsyncReducer(store);
try {
injectReducer('bad', 'nope');
} catch (err) {
result = err.name === 'Invariant Violation';
}
try {
injectReducer('coolio', 12345);
} catch (err) {
result = err.name === 'Invariant Violation';
}
expect(result).toEqual(true);
});
});
describe('injectAsyncSagas', () => {
it('given a store, it should provide a function to inject a saga', () => {
const injectSagas = injectAsyncSagas(store);
injectSagas(sagas);
const actual = store.getState().get('test');
const expected = initialState.merge({ reduced: 'yup' });
expect(actual.toJS()).toEqual(expected.toJS());
});
it('should throw if passed invalid saga', () => {
let result = false;
const injectSagas = injectAsyncSagas(store);
try {
injectSagas({ testSaga });
} catch (err) {
result = err.name === 'Invariant Violation';
}
try {
injectSagas(testSaga);
} catch (err) {
result = err.name === 'Invariant Violation';
}
expect(result).toEqual(true);
});
});
});
});
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment