diff --git a/.gitignore b/.gitignore
index e29155f..195849c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
.idea
.DS_Store
tmp
-spm_modules
node_modules
examples/**/dist
boilerplate/dist
+*.log
diff --git a/.npmignore b/.npmignore
index 8869ccb..fb23fce 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,6 +1,5 @@
.idea
.DS_Store
tmp
-spm_modules
node_modules
examples
diff --git a/README.md b/README.md
index 9fccc5b..95cf4b6 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,9 @@ Generate boilerplate.
```bash
$ mkdir foo && cd foo
$ antd-init
+// or
+$ antd-init --type plain-react
+$ antd-init --type redux
```
Start development server.
diff --git a/bin/antd-init b/bin/antd-init
index 792231d..941b4cb 100755
--- a/bin/antd-init
+++ b/bin/antd-init
@@ -4,6 +4,7 @@ var vfs = require('vinyl-fs');
var fs = require('fs');
var through = require('through2');
var path = require('path');
+var inquirer = require('inquirer');
var join = path.join;
var basename = path.basename;
@@ -13,17 +14,43 @@ if (process.argv.length === 3 &&
return;
}
-var cwd = join(__dirname, '../boilerplate');
-var dest = process.cwd();
+if (process.argv.length === 4 && process.argv[2] === '--type') {
+ init(process.argv[3]);
+ return;
+}
-vfs.src('**/*', {cwd: cwd, cwdbase: true, dot: true})
- .pipe(template(dest))
- .pipe(vfs.dest(dest))
- .on('end', function() {
- fs.renameSync(path.join(dest,'gitignore'),path.join(dest,'.gitignore'));
- require('../lib/install');
+inquirer.prompt({
+ name: 'type',
+ type: 'list',
+ message: 'Please select boilerplate type',
+ choices: [
+ {
+ name: 'plain react - for simple project',
+ value: 'plain-react',
+ },
+ {
+ name: 'redux - for complex project',
+ value: 'redux',
+ },
+ ],
})
- .resume();
+ .then(function(answers) {
+ init(answers.type);
+ });
+
+function init(type) {
+ var cwd = join(__dirname, '../boilerplates', type);
+ var dest = process.cwd();
+
+ vfs.src('**/*', {cwd: cwd, cwdbase: true, dot: true})
+ .pipe(template(dest))
+ .pipe(vfs.dest(dest))
+ .on('end', function() {
+ fs.renameSync(path.join(dest,'gitignore'),path.join(dest,'.gitignore'));
+ require('../lib/install');
+ })
+ .resume();
+}
function template(dest) {
return through.obj(function (file, enc, cb) {
diff --git a/boilerplate/.editorconfig b/boilerplates/plain-react/.editorconfig
similarity index 100%
rename from boilerplate/.editorconfig
rename to boilerplates/plain-react/.editorconfig
diff --git a/boilerplate/.eslintrc b/boilerplates/plain-react/.eslintrc
similarity index 100%
rename from boilerplate/.eslintrc
rename to boilerplates/plain-react/.eslintrc
diff --git a/boilerplate/README.md b/boilerplates/plain-react/README.md
similarity index 100%
rename from boilerplate/README.md
rename to boilerplates/plain-react/README.md
diff --git a/boilerplate/gitignore b/boilerplates/plain-react/gitignore
similarity index 100%
rename from boilerplate/gitignore
rename to boilerplates/plain-react/gitignore
diff --git a/boilerplate/index.html b/boilerplates/plain-react/index.html
similarity index 100%
rename from boilerplate/index.html
rename to boilerplates/plain-react/index.html
diff --git a/boilerplate/package.json b/boilerplates/plain-react/package.json
similarity index 100%
rename from boilerplate/package.json
rename to boilerplates/plain-react/package.json
diff --git a/boilerplate/proxy.config.js b/boilerplates/plain-react/proxy.config.js
similarity index 100%
rename from boilerplate/proxy.config.js
rename to boilerplates/plain-react/proxy.config.js
diff --git a/boilerplate/src/common/lib.js b/boilerplates/plain-react/src/common/lib.js
similarity index 100%
rename from boilerplate/src/common/lib.js
rename to boilerplates/plain-react/src/common/lib.js
diff --git a/boilerplate/src/component/App.jsx b/boilerplates/plain-react/src/component/App.jsx
similarity index 100%
rename from boilerplate/src/component/App.jsx
rename to boilerplates/plain-react/src/component/App.jsx
diff --git a/boilerplate/src/component/App.less b/boilerplates/plain-react/src/component/App.less
similarity index 100%
rename from boilerplate/src/component/App.less
rename to boilerplates/plain-react/src/component/App.less
diff --git a/boilerplate/src/entry/index.jsx b/boilerplates/plain-react/src/entry/index.jsx
similarity index 100%
rename from boilerplate/src/entry/index.jsx
rename to boilerplates/plain-react/src/entry/index.jsx
diff --git a/boilerplate/webpack.config.js b/boilerplates/plain-react/webpack.config.js
similarity index 100%
rename from boilerplate/webpack.config.js
rename to boilerplates/plain-react/webpack.config.js
diff --git a/boilerplates/redux/.eslintrc b/boilerplates/redux/.eslintrc
new file mode 100644
index 0000000..4f1e575
--- /dev/null
+++ b/boilerplates/redux/.eslintrc
@@ -0,0 +1,20 @@
+{
+ "parser": "babel-eslint",
+ "extends": "eslint-config-airbnb",
+ "rules": {
+ "spaced-comment": [0],
+ "no-unused-vars": [0],
+ "no-empty": [0],
+ "react/wrap-multilines": [0],
+ "react/no-multi-comp": [0],
+ "no-constant-condition": [0],
+ "react/jsx-no-bind": [0],
+ "react/prop-types": [0],
+ "arrow-body-style": [0],
+ "react/prefer-stateless-function": [0],
+ "semi": [0]
+ },
+ "ecmaFeatures": {
+ "experimentalObjectRestSpread": true
+ }
+}
diff --git a/boilerplates/redux/README.md b/boilerplates/redux/README.md
new file mode 100644
index 0000000..9c74844
--- /dev/null
+++ b/boilerplates/redux/README.md
@@ -0,0 +1,103 @@
+# react-redux-boilerplate
+
+A boilerplate with react, redux, redux-saga, react-router, webpack, babel, css-modules ...
+
+## 环境准备
+
+先安装依赖
+
+```bash
+$ npm install
+```
+
+想要更好的开发体验,还需安装两个 Chrome 插件:[Redux DevTools](https://chrome.google.com/webstore/detail/lmhkpmbekcpmknklioeibfkpmmfibljd) 和 [LiveReload](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei) 。
+
+## 启动调试
+
+```bash
+$ npm start
+$ open http://localhost:8989/
+```
+
+## 构建代码
+
+```bash
+$ npm run build
+
+// 构建但不压缩
+$ npm run build -- --no-compress
+```
+
+## 目录结构
+
+```
+.
+├── /dist/ # 构建输出的文件会在这里
+├── /node_modules/ # 第三方类库和工具
+├── /src/ # 应用源码
+│ ├── /components/ # React components
+│ ├── /entries/ # 应用入口
+│ ├── /layouts/ # 布局信息
+│ ├── /reducers/ # reducers
+│ ├── /routes/ # 路由信息
+│ ├── /sagas/ # redux-sagas
+│ └── /services/ # 处理和服务器的交互
+├── proxy.config.js # 配置 dora-plugin-proxy,用于 mock 和在线调试
+├── webpack.config.js # 扩展 webpack 配置
+└── package.json # 配置入口文件、依赖和 scripts
+```
+
+## 系统组织
+
+![](https://camo.githubusercontent.com/068c4ff126977b861cff3338428bdde6927f7dad/68747470733a2f2f6f732e616c697061796f626a656374732e636f6d2f726d73706f7274616c2f43684d775a42755a6c614c725377652e706e67)
+
+详见:[React + Redux 最佳实践](https://github.com/sorrycc/blog/issues/1)
+
+## 内置类库
+
+- [react](https://github.com/facebook/react)
+- [redux](https://github.com/reactjs/redux)
+- [redux-saga](https://github.com/yelouafi/redux-saga)
+- [redux-actions](https://github.com/acdlite/redux-actions)
+- [react-router](https://github.com/reactjs/react-router)
+- [classnames](https://github.com/JedWatson/classnames)
+- [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch)
+- [react-router](https://github.com/reactjs/react-router)
+- [react-router-redux](https://github.com/reactjs/react-router-redux)
+
+## 工具特性
+
+热替换和 LiveReload
+
+> 基于 [Webpack Vanilla HMR](https://webpack.github.io/docs/hot-module-replacement-with-webpack.html),支持 `components`, `reducers`, `routers`, `sagas`, `layouts` 目录的模块热替换,其余目录的修改则会自动刷新页面。
+
+> CSS 的自动刷新需通过 [LiveReload](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei) Chrome 插件配合使用。
+
+> - [Why Vanilla HMR](https://github.com/reactjs/redux/pull/1455)
+
+支持 css-modules
+
+> 所有 less 文件会被解析为 css-modules
+
+运行错误和语法错误的提醒
+
+> 通过 [redbox-react](https://github.com/KeywordBrain/redbox-react) 和 webpack hmr overlay 提示运行错误和语法错误
+
+自动引入 `reducer` 和 `saga`
+
+> 通过 webpack 的 `require.context` 黑魔法批量引入 `reducer` 和 `saga`,新增、删除和重命名时会更方便
+
+自动安装 npm 依赖
+
+> ![](https://camo.githubusercontent.com/898e02d6539900efe65fadbfd15e2a1d7ce4dccf/68747470733a2f2f6f732e616c697061796f626a656374732e636f6d2f726d73706f7274616c2f4b6541474f776a70746a6152684d6d2e676966)
+
+数据 mock 和线上调试
+
+> 通过 dora-plugin-proxy 实现,详见:https://github.com/dora-js/dora-plugin-proxy#规则定义
+
+...
+
+## License
+
+MIT
+
diff --git a/boilerplates/redux/gitignore b/boilerplates/redux/gitignore
new file mode 100644
index 0000000..a7cf257
--- /dev/null
+++ b/boilerplates/redux/gitignore
@@ -0,0 +1,4 @@
+dist
+node_modules
+.DS_Store
+npm-debug.log
diff --git a/boilerplates/redux/package.json b/boilerplates/redux/package.json
new file mode 100644
index 0000000..01092d7
--- /dev/null
+++ b/boilerplates/redux/package.json
@@ -0,0 +1,51 @@
+{
+ "private": true,
+ "entry": {},
+ "dependencies": {
+ "antd": "^1.0.1",
+ "atool-build": "^0.7.1",
+ "babel-plugin-antd": "^0.4.0",
+ "babel-plugin-transform-runtime": "^6.8.0",
+ "babel-runtime": "^6.6.1",
+ "classnames": "^2.2.3",
+ "history": "^2.0.1",
+ "isomorphic-fetch": "^2.2.1",
+ "js-cookie": "^2.1.1",
+ "react": "^15.0.2",
+ "react-dom": "^15.0.2",
+ "react-redux": "4.4.x",
+ "react-router": "^2.0.1",
+ "react-router-redux": "^4.0.1",
+ "redux": "^3.5.2",
+ "redux-actions": "0.9.x",
+ "redux-saga": "^0.10.4"
+ },
+ "devDependencies": {
+ "atool-test-mocha": "^0.1.4",
+ "babel-eslint": "^6.0.2",
+ "dora": "0.3.x",
+ "dora-plugin-browser-history": "^0.1.1",
+ "dora-plugin-livereload": "^0.4.0",
+ "dora-plugin-proxy": "^0.6.1",
+ "dora-plugin-webpack": "0.6.x",
+ "dora-plugin-webpack-hmr": "^0.1.0",
+ "eslint": "^2.10.1",
+ "eslint-config-airbnb": "^9.0.1",
+ "eslint-plugin-import": "^1.8.0",
+ "eslint-plugin-jsx-a11y": "^1.2.0",
+ "eslint-plugin-react": "^5.1.1",
+ "expect": "^1.20.1",
+ "glob": "^7.0.3",
+ "pre-commit": "^1.1.3",
+ "redbox-react": "^1.2.2"
+ },
+ "pre-commit": [
+ "lint"
+ ],
+ "scripts": {
+ "start": "dora --plugins 'proxy,webpack,webpack-hmr,livereload?enableJs=false&injectHost=localhost,browser-history?index=/src/entries/index.html'",
+ "build": "atool-build",
+ "lint": "eslint --ext .js,.jsx src/",
+ "test": "atool-test-mocha ./src/**/__tests__/*-test.js"
+ }
+}
\ No newline at end of file
diff --git a/boilerplates/redux/proxy.config.js b/boilerplates/redux/proxy.config.js
new file mode 100644
index 0000000..1762116
--- /dev/null
+++ b/boilerplates/redux/proxy.config.js
@@ -0,0 +1,27 @@
+// Learn more on how to config.
+// - https://github.com/dora-js/dora-plugin-proxy#规则定义
+
+module.exports = {
+ '/api/todos': function(req, res) {
+ setTimeout(function() {
+ res.json({
+ success: true,
+ data: [
+ {
+ id: 1,
+ text: 'Learn antd',
+ isComplete: true,
+ },
+ {
+ id: 2,
+ text: 'Learn ant-tool',
+ },
+ {
+ id: 3,
+ text: 'Learn dora',
+ },
+ ],
+ });
+ }, 500);
+ },
+};
diff --git a/boilerplates/redux/src/components/App.jsx b/boilerplates/redux/src/components/App.jsx
new file mode 100644
index 0000000..d6f9f73
--- /dev/null
+++ b/boilerplates/redux/src/components/App.jsx
@@ -0,0 +1,17 @@
+import React, { Component, PropTypes } from 'react';
+import { connect } from 'react-redux';
+import Todos from './Todos/Todos';
+import MainLayout from '../layouts/MainLayout/MainLayout';
+
+const App = ({ location }) => {
+ return (
+
+
+
+ );
+};
+
+App.propTypes = {
+};
+
+export default connect()(App);
diff --git a/boilerplates/redux/src/components/NotFound.jsx b/boilerplates/redux/src/components/NotFound.jsx
new file mode 100644
index 0000000..a17c355
--- /dev/null
+++ b/boilerplates/redux/src/components/NotFound.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Button } from 'antd';
+import styles from './NotFound.less';
+
+const NotFound = () => {
+ return (
+
+ );
+};
+
+export default NotFound;
diff --git a/boilerplates/redux/src/components/NotFound.less b/boilerplates/redux/src/components/NotFound.less
new file mode 100644
index 0000000..fd8d23b
--- /dev/null
+++ b/boilerplates/redux/src/components/NotFound.less
@@ -0,0 +1,25 @@
+.normal {
+ width: 100%;
+ height: 100%;
+ min-height: 100vh;
+ padding-top: 120px;
+}
+
+.container {
+ padding: 0;
+ margin: 0 auto;
+ width: 620px;
+ height: 300px;
+ text-align: center;
+}
+
+.title {
+ font-size: 80px;
+ color: #666;
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+
+.desc {
+ font-size: 14px;
+}
diff --git a/boilerplates/redux/src/components/Todos/Todo.jsx b/boilerplates/redux/src/components/Todos/Todo.jsx
new file mode 100644
index 0000000..41a02bd
--- /dev/null
+++ b/boilerplates/redux/src/components/Todos/Todo.jsx
@@ -0,0 +1,35 @@
+import React, { Component, PropTypes } from 'react';
+import classnames from 'classnames';
+import styles from './Todo.less';
+
+const Todo = ({ data, onToggleComplete }) => {
+ const { text, isComplete } = data;
+ const todoCls = classnames({
+ [styles.normal]: true,
+ [styles.isComplete]: isComplete,
+ });
+
+ return (
+
+ );
+};
+
+Todo.propTypes = {
+ data: PropTypes.object.isRequired,
+ onToggleComplete: PropTypes.func.isRequired,
+};
+
+export default Todo;
+
diff --git a/boilerplates/redux/src/components/Todos/Todo.less b/boilerplates/redux/src/components/Todos/Todo.less
new file mode 100644
index 0000000..b527fbc
--- /dev/null
+++ b/boilerplates/redux/src/components/Todos/Todo.less
@@ -0,0 +1,10 @@
+.normal {
+ display: flex;
+}
+.checkbox {
+ margin-right: 6px;
+}
+.isComplete {
+ color: #ccc;
+ text-decoration: line-through;
+}
diff --git a/boilerplates/redux/src/components/Todos/Todos.jsx b/boilerplates/redux/src/components/Todos/Todos.jsx
new file mode 100644
index 0000000..e68d917
--- /dev/null
+++ b/boilerplates/redux/src/components/Todos/Todos.jsx
@@ -0,0 +1,61 @@
+import React, { Component, PropTypes } from 'react';
+import { connect } from 'react-redux';
+import { Spin } from 'antd';
+import Todo from './Todo';
+import styles from './Todos.less';
+
+const Todos = ({ todos, dispatch }) => {
+ const handleToggleComplete = (id) => {
+ dispatch({
+ type: 'todos/toggleComplete',
+ payload: id,
+ });
+ };
+
+ const renderList = () => {
+ const { list, loading } = todos;
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+ {list.map(item =>
+ )}
+
+ );
+ };
+
+ return (
+
+ {renderList()}
+
+ );
+};
+
+Todos.propTypes = {};
+
+function filter(todos, pathname) {
+ const newList = todos.list.filter(todo => {
+ if (pathname === '/actived') {
+ return !todo.isComplete;
+ }
+ if (pathname === '/completed') {
+ return todo.isComplete;
+ }
+ return true;
+ });
+ return { ...todos, list: newList };
+}
+
+function mapStateToProps({ todos }, { location }) {
+ return {
+ todos: filter(todos, location.pathname),
+ };
+}
+
+export default connect(mapStateToProps)(Todos);
diff --git a/boilerplates/redux/src/components/Todos/Todos.less b/boilerplates/redux/src/components/Todos/Todos.less
new file mode 100644
index 0000000..8af4569
--- /dev/null
+++ b/boilerplates/redux/src/components/Todos/Todos.less
@@ -0,0 +1,3 @@
+.normal {
+
+}
diff --git a/boilerplates/redux/src/entries/index.html b/boilerplates/redux/src/entries/index.html
new file mode 100644
index 0000000..f65f403
--- /dev/null
+++ b/boilerplates/redux/src/entries/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Demo
+
+
+
+
+
+
+
+
+
+
+
diff --git a/boilerplates/redux/src/entries/index.js b/boilerplates/redux/src/entries/index.js
new file mode 100644
index 0000000..4e2da00
--- /dev/null
+++ b/boilerplates/redux/src/entries/index.js
@@ -0,0 +1,72 @@
+import './index.html';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
+import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
+import createSagaMiddleware from 'redux-saga';
+import { browserHistory } from 'react-router';
+import { syncHistoryWithStore, routerReducer as routing } from 'react-router-redux';
+import reducers from '../reducers/index';
+import SagaManager from '../sagas/SagaManager';
+import './index.less';
+
+//////////////////////
+// Store
+
+const sagaMiddleware = createSagaMiddleware();
+const initialState = {};
+const enhancer = compose(
+ applyMiddleware(sagaMiddleware),
+ window.devToolsExtension ? window.devToolsExtension() : f => f
+);
+const store = createStore(combineReducers({
+ ...reducers, routing,
+}), initialState, enhancer);
+SagaManager.startSagas(sagaMiddleware);
+
+if (module.hot) {
+ module.hot.accept('../reducers', () => {
+ const reducers = require('../reducers');
+ const combinedReducers = combineReducers({ ...reducers, routing });
+ store.replaceReducer(combinedReducers);
+ });
+ module.hot.accept('../sagas/SagaManager', () => {
+ SagaManager.cancelSagas(store);
+ require('../sagas/SagaManager').default.startSagas(sagaMiddleware);
+ });
+}
+
+//////////////////////
+// Render
+
+const history = syncHistoryWithStore(browserHistory, store);
+
+let render = () => {
+ const Routes = require('../routes/index');
+ ReactDOM.render(
+
+
+
+ , document.getElementById('root'));
+};
+
+if (module.hot) {
+ const renderNormally = render;
+ const renderException = (error) => {
+ const RedBox = require('redbox-react');
+ ReactDOM.render(, document.getElementById('root'));
+ };
+ render = () => {
+ try {
+ renderNormally();
+ } catch (error) {
+ console.error('error', error);
+ renderException(error);
+ }
+ };
+ module.hot.accept('../routes/index', () => {
+ render();
+ });
+}
+
+render();
diff --git a/boilerplates/redux/src/entries/index.less b/boilerplates/redux/src/entries/index.less
new file mode 100644
index 0000000..2e6520a
--- /dev/null
+++ b/boilerplates/redux/src/entries/index.less
@@ -0,0 +1,6 @@
+
+:global {
+ html, body, #root {
+ height: 100%;
+ }
+}
diff --git a/boilerplates/redux/src/layouts/MainLayout/MainLayout.jsx b/boilerplates/redux/src/layouts/MainLayout/MainLayout.jsx
new file mode 100644
index 0000000..9a36cfa
--- /dev/null
+++ b/boilerplates/redux/src/layouts/MainLayout/MainLayout.jsx
@@ -0,0 +1,34 @@
+import React, { Component, PropTypes } from 'react';
+import { Router, Route, IndexRoute, Link } from 'react-router';
+import styles from './MainLayout.less';
+
+const MainLayout = ({ children }) => {
+ return (
+
+
+
Todo App
+
+
+
+
Filters:
+ All
+ Actived
+ Completed
+ 404
+
+
+ {children}
+
+
+
+ Built with redux, react and antd.
+
+
+ );
+};
+
+MainLayout.propTypes = {
+ children: PropTypes.element.isRequired,
+};
+
+export default MainLayout;
diff --git a/boilerplates/redux/src/layouts/MainLayout/MainLayout.less b/boilerplates/redux/src/layouts/MainLayout/MainLayout.less
new file mode 100644
index 0000000..f367da8
--- /dev/null
+++ b/boilerplates/redux/src/layouts/MainLayout/MainLayout.less
@@ -0,0 +1,38 @@
+
+.normal {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.head {
+ margin-bottom: 20px;
+ background: cadetblue;
+ height: 80px;
+ padding: 8px;
+ color: #fff;
+}
+
+.content {
+ flex: 1;
+ display: flex;
+}
+
+.side {
+ padding: 8px;
+ width: 20%;
+ min-width: 200px;
+ background: #fafafa;
+ margin-right: 20px;
+}
+
+.main {
+ padding: 8px;
+ flex: 1 0 auto;
+}
+
+.foot {
+ margin-top: 20px;
+ background: greenyellow;
+ padding: 8px;
+}
diff --git a/boilerplates/redux/src/reducers/__tests__/todos-test.js b/boilerplates/redux/src/reducers/__tests__/todos-test.js
new file mode 100644
index 0000000..ad4638e
--- /dev/null
+++ b/boilerplates/redux/src/reducers/__tests__/todos-test.js
@@ -0,0 +1,18 @@
+import expect from 'expect';
+import todos from '../todos';
+
+describe('todos', () => {
+
+ it('todos/get', () => {
+ expect(todos({}, { type: 'todos/get' })).toEqual({ loading: true });
+ });
+
+ it('todos/get/success', () => {
+ const newState = todos({ list: 1, loading: true }, { type: 'todos/get/success', payload:2 });
+ expect(newState).toEqual({
+ loading: false,
+ list: 2,
+ });
+ });
+
+});
diff --git a/boilerplates/redux/src/reducers/index.js b/boilerplates/redux/src/reducers/index.js
new file mode 100644
index 0000000..9a4e66f
--- /dev/null
+++ b/boilerplates/redux/src/reducers/index.js
@@ -0,0 +1,11 @@
+// Use require.context to require reducers automatically
+// Ref: https://webpack.github.io/docs/context.html
+const context = require.context('./', false, /\.js$/);
+const keys = context.keys().filter(item => item !== './index.js');
+
+const reducers = keys.reduce((memo, key) => {
+ memo[key.match(/([^\/]+)\.js$/)[1]] = context(key);
+ return memo;
+}, {});
+
+export default reducers;
diff --git a/boilerplates/redux/src/reducers/todos.js b/boilerplates/redux/src/reducers/todos.js
new file mode 100644
index 0000000..baea8de
--- /dev/null
+++ b/boilerplates/redux/src/reducers/todos.js
@@ -0,0 +1,45 @@
+import { handleActions } from 'redux-actions';
+import { combineReducer } from 'redux';
+
+const todos = handleActions({
+ ['todos/get'](state) {
+ return { ...state, loading: true, };
+ },
+ ['todos/get/success'](state, action) {
+ return { ...state, list: action.payload, loading: false, };
+ },
+ ['todos/get/failed'](state, action) {
+ return { ...state, err: action.err, loading: false, };
+ },
+ ['todos/delete'](state, action) {
+ const id = action.payload;
+ const newList = state.list.filter(todo => todo.id !== id);
+ return { ...state, list: newList, };
+ },
+ ['todos/create'](state, action) {
+ const text = action.payload;
+ const newTodo = { text, };
+ return { ...state, list: [newTodo, ...state.list], };
+ },
+ ['todos/toggleComplete'](state, action) {
+ const id = action.payload;
+ const newList = state.list.map(todo => {
+ if (id === todo.id) {
+ return { ...todo, isComplete: !todo.isComplete };
+ } else {
+ return todo;
+ }
+ });
+ return { ...state, list: newList, };
+ },
+ ['todos/toggleCompleteAll'](state, action) {
+ const isComplete = action.payload;
+ const newList = state.list.map(todo => ({ ...todo, isComplete }));
+ return { ...state, list: newList, };
+ },
+}, {
+ list: [],
+ loading: false,
+});
+
+export default todos;
diff --git a/boilerplates/redux/src/routes/index.js b/boilerplates/redux/src/routes/index.js
new file mode 100644
index 0000000..1db19c9
--- /dev/null
+++ b/boilerplates/redux/src/routes/index.js
@@ -0,0 +1,18 @@
+import React, { PropTypes } from 'react';
+import { Router, Route, IndexRoute, Link } from 'react-router';
+import App from '../components/App';
+import NotFound from '../components/NotFound';
+
+const Routes = ({ history }) =>
+
+
+
+
+
+ ;
+
+Routes.propTypes = {
+ history: PropTypes.any,
+};
+
+export default Routes;
diff --git a/boilerplates/redux/src/sagas/SagaManager.js b/boilerplates/redux/src/sagas/SagaManager.js
new file mode 100644
index 0000000..026c96a
--- /dev/null
+++ b/boilerplates/redux/src/sagas/SagaManager.js
@@ -0,0 +1,31 @@
+import rootSaga from './index';
+import { take, fork, cancel } from 'redux-saga/effects';
+
+const sagas = [rootSaga];
+
+export const CANCEL_SAGAS_HMR = 'CANCEL_SAGAS_HMR';
+
+function createAbortableSaga (saga) {
+ if (process.env.NODE_ENV === 'development') {
+ return function* main () {
+ const sagaTask = yield fork(saga);
+ yield take(CANCEL_SAGAS_HMR);
+ yield cancel(sagaTask);
+ };
+ } else {
+ return saga;
+ }
+}
+
+const SagaManager = {
+ startSagas(sagaMiddleware) {
+ sagas.map(createAbortableSaga).forEach(saga => sagaMiddleware.run(saga));
+ },
+ cancelSagas(store) {
+ store.dispatch({
+ type: CANCEL_SAGAS_HMR
+ });
+ }
+};
+
+export default SagaManager;
diff --git a/boilerplates/redux/src/sagas/index.js b/boilerplates/redux/src/sagas/index.js
new file mode 100644
index 0000000..3b507fc
--- /dev/null
+++ b/boilerplates/redux/src/sagas/index.js
@@ -0,0 +1,12 @@
+import { fork } from 'redux-saga/effects';
+
+// Use require.context to require sagas automatically
+// Ref: https://webpack.github.io/docs/context.html
+const context = require.context('./', false, /\.js$/);
+const keys = context.keys().filter(item => item !== './index.js' && item !== './SagaManager.js');
+
+export default function* root() {
+ for (let i = 0; i < keys.length; i ++) {
+ yield fork(context(keys[i]));
+ }
+}
diff --git a/boilerplates/redux/src/sagas/todos.js b/boilerplates/redux/src/sagas/todos.js
new file mode 100644
index 0000000..13674cb
--- /dev/null
+++ b/boilerplates/redux/src/sagas/todos.js
@@ -0,0 +1,35 @@
+import { takeLatest } from 'redux-saga';
+import { take, call, put, fork, cancel } from 'redux-saga/effects';
+import { getAll } from '../services/todos';
+import { message } from 'antd';
+
+function* getTodos() {
+ try {
+ const { jsonResult } = yield call(getAll);
+ if (jsonResult.data) {
+ yield put({
+ type: 'todos/get/success',
+ payload: jsonResult.data,
+ });
+ }
+ } catch (err) {
+ message.error(err);
+ //yield put({
+ // type: 'todos/get/failed',
+ // err,
+ //});
+ }
+}
+
+function* watchTodosGet() {
+ yield takeLatest('todos/get', getTodos)
+}
+
+export default function* () {
+ yield fork(watchTodosGet);
+
+ // Load todos.
+ yield put({
+ type: 'todos/get',
+ });
+}
diff --git a/boilerplates/redux/src/services/todos.js b/boilerplates/redux/src/services/todos.js
new file mode 100644
index 0000000..cf7e699
--- /dev/null
+++ b/boilerplates/redux/src/services/todos.js
@@ -0,0 +1,5 @@
+import xFetch from './xFetch';
+
+export async function getAll() {
+ return xFetch('/api/todos');
+}
diff --git a/boilerplates/redux/src/services/xFetch.js b/boilerplates/redux/src/services/xFetch.js
new file mode 100644
index 0000000..8be02ee
--- /dev/null
+++ b/boilerplates/redux/src/services/xFetch.js
@@ -0,0 +1,46 @@
+import fetch from 'isomorphic-fetch';
+import cookie from 'js-cookie';
+
+const errorMessages = (res) => `${res.status} ${res.statusText}`;
+
+function check401(res) {
+ if (res.status === 401) {
+ location.href = '/401';
+ }
+ return res;
+}
+
+function check404(res) {
+ if (res.status === 404) {
+ return Promise.reject(errorMessages(res));
+ }
+ return res;
+}
+
+function jsonParse(res) {
+ return res.json().then(jsonResult => ({ ...res, jsonResult }));
+}
+
+function errorMessageParse(res) {
+ const { success, message } = res.jsonResult;
+ if (!success) {
+ return Promise.reject(message);
+ }
+ return res;
+}
+
+function xFetch(url, options) {
+ const opts = { ...options };
+ opts.headers = {
+ ...opts.headers,
+ authorization: cookie.get('authorization') || '',
+ };
+
+ return fetch(url, opts)
+ .then(check401)
+ .then(check404)
+ .then(jsonParse)
+ .then(errorMessageParse);
+}
+
+export default xFetch;
diff --git a/boilerplates/redux/webpack.config.js b/boilerplates/redux/webpack.config.js
new file mode 100644
index 0000000..bc58cd5
--- /dev/null
+++ b/boilerplates/redux/webpack.config.js
@@ -0,0 +1,39 @@
+// Learn more on how to config.
+// - https://github.com/ant-tool/atool-build#配置扩展
+
+const webpack = require('atool-build/lib/webpack');
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+
+module.exports = function(webpackConfig) {
+ webpackConfig.babel.plugins.push('transform-runtime');
+ webpackConfig.babel.plugins.push(['antd', {style: 'css'}]);
+
+ // Enable this if you have to support IE8.
+ // webpackConfig.module.loaders.unshift({
+ // test: /\.jsx?$/,
+ // loader: 'es3ify-loader',
+ // });
+
+ // Parse all less files as css module.
+ webpackConfig.module.loaders.forEach(function(loader, index) {
+ if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.less$') > -1) {
+ loader.test = /\.dont\.exist\.file/;
+ }
+ if (loader.test.toString() === '/\\.module\\.less$/') {
+ loader.test = /\.less$/;
+ }
+ });
+
+ // Load src/entries/*.js as entry automatically.
+ const files = glob.sync('./src/entries/*.js');
+ const newEntries = files.reduce(function(memo, file) {
+ const name = path.basename(file, '.js');
+ memo[name] = file;
+ return memo;
+ }, {});
+ webpackConfig.entry = Object.assign({}, webpackConfig.entry, newEntries);
+
+ return webpackConfig;
+};
diff --git a/package.json b/package.json
index 47c5759..d9f3fb5 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"test": "mocha --no-timeouts"
},
"dependencies": {
+ "inquirer": "^1.0.0",
"through2": "2.0.x",
"vinyl-fs": "^2.4.3",
"which": "1.2.x"