├── src ├── static │ ├── robots.txt │ ├── favicon.ico │ └── humans.txt ├── layouts │ └── CoreLayout │ │ ├── CoreLayout.scss │ │ ├── index.js │ │ ├── CoreLayout.js │ │ └── CoreLayout.spec.js ├── app │ ├── Header.scss │ ├── sagas.js │ ├── entities │ │ ├── createRequestActionTypes.js │ │ ├── sagas.js │ │ └── fetchEntities.js │ ├── reducers.js │ ├── Header.js │ ├── App.js │ ├── Header.spec.js │ ├── routes.js │ └── configureStore.js ├── modules │ ├── home │ │ ├── Home.scss │ │ ├── assets │ │ │ └── Duck.jpg │ │ ├── homeRoutes.js │ │ ├── Home.js │ │ ├── homeRoutes.spec.js │ │ └── Home.spec.js │ ├── user │ │ ├── userPropTypes.js │ │ ├── userApi.js │ │ ├── userActions.js │ │ ├── userSagas.js │ │ ├── userRoutes.js │ │ ├── userReducer.js │ │ └── User.js │ └── counter │ │ ├── Counter.scss │ │ ├── counterRoutes.spec.js │ │ ├── counterReducer.js │ │ ├── counterActions.js │ │ ├── counterRoutes.js │ │ ├── Counter.spec.js │ │ ├── Counter.js │ │ └── counterReducer.spec.js ├── styles │ ├── _base.scss │ └── core.scss ├── index.html └── main.js ├── blueprints ├── blueprint │ ├── files │ │ └── blueprints │ │ │ └── __name__ │ │ │ ├── files │ │ │ └── .gitkeep │ │ │ └── index.js │ └── index.js ├── duck │ ├── index.js │ └── files │ │ ├── __test__ │ │ └── redux │ │ │ └── modules │ │ │ └── __name__.spec.js │ │ └── __root__ │ │ └── redux │ │ └── modules │ │ └── __name__.js ├── view │ ├── index.js │ └── files │ │ ├── __root__ │ │ └── views │ │ │ └── __name__View │ │ │ ├── index.js │ │ │ └── __name__View.js │ │ └── __test__ │ │ └── views │ │ └── __name__View.spec.js ├── dumb │ ├── index.js │ └── files │ │ ├── __root__ │ │ └── components │ │ │ └── __name__ │ │ │ ├── index.js │ │ │ └── __name__.js │ │ └── __test__ │ │ └── components │ │ └── __name__.spec.js ├── layout │ ├── index.js │ └── files │ │ ├── __root__ │ │ └── layouts │ │ │ └── __name__Layout │ │ │ ├── index.js │ │ │ └── __name__Layout.js │ │ └── __test__ │ │ └── layouts │ │ └── __name__Layout.spec.js ├── form │ ├── index.js │ └── files │ │ ├── __test__ │ │ └── forms │ │ │ └── __name__Form.spec.js │ │ └── __root__ │ │ └── forms │ │ └── __name__Form │ │ ├── index.js │ │ └── __name__Form.js ├── smart │ ├── files │ │ ├── __test__ │ │ │ └── containers │ │ │ │ └── __name__.spec.js │ │ └── __root__ │ │ │ └── containers │ │ │ └── __name__ │ │ │ ├── index.js │ │ │ └── __name__.js │ └── index.js └── .eslintrc ├── .gitignore ├── nodemon.json ├── .eslintignore ├── .reduxrc ├── tests ├── .eslintrc ├── framework.spec.js └── test-bundler.js ├── .travis.yml ├── bin ├── server.js └── compile.js ├── .babelrc ├── server ├── lib │ └── apply-express-middleware.js ├── middleware │ ├── webpack-hmr.js │ └── webpack-dev.js └── main.js ├── .eslintrc ├── .editorconfig ├── LICENSE ├── config ├── environments.js └── index.js ├── dd1ac21a8e4eb7562d3e1a3d39437aff29674162.patch ├── CONTRIBUTING.md ├── package.json ├── README.md └── CHANGELOG.md /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /blueprints/blueprint/files/blueprints/__name__/files/.gitkeep: -------------------------------------------------------------------------------- 1 | put your files here 2 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.scss: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | padding-top:20px; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | 4 | node_modules 5 | 6 | dist 7 | coverage 8 | .publish 9 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": ["dist", "coverage", "tests", "src"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app/Header.scss: -------------------------------------------------------------------------------- 1 | .activeRoute { 2 | font-weight: bold; 3 | text-decoration: underline; 4 | } 5 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from './CoreLayout' 2 | 3 | export default CoreLayout 4 | -------------------------------------------------------------------------------- /src/modules/home/Home.scss: -------------------------------------------------------------------------------- 1 | .duck { 2 | display: block; 3 | width: 120px; 4 | margin: 1.5rem auto; 5 | } 6 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koshuang/react-redux-starter-kit/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | blueprints/**/files/** 2 | coverage/** 3 | node_modules/** 4 | dist/** 5 | *.spec.js 6 | src/index.html 7 | -------------------------------------------------------------------------------- /blueprints/duck/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'generates a redux duck' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /blueprints/view/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'generates a view component' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/home/assets/Duck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koshuang/react-redux-starter-kit/HEAD/src/modules/home/assets/Duck.jpg -------------------------------------------------------------------------------- /blueprints/dumb/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'generates a dumb (pure) component' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /blueprints/layout/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'generates a functional layout component' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/home/homeRoutes.js: -------------------------------------------------------------------------------- 1 | import Home from './Home' 2 | 3 | // Sync route definition 4 | export default { 5 | component: Home 6 | } 7 | -------------------------------------------------------------------------------- /blueprints/form/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'generates a connected redux-form form component' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /blueprints/form/files/__test__/forms/__name__Form.spec.js: -------------------------------------------------------------------------------- 1 | describe('(Form) <%= pascalEntityName %>', () => { 2 | it('exists', () => { 3 | 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /.reduxrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceBase":"src", 3 | "testBase":"tests", 4 | "smartPath":"containers", 5 | "dumbPath":"components", 6 | "fileCasing":"pascal" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /blueprints/smart/files/__test__/containers/__name__.spec.js: -------------------------------------------------------------------------------- 1 | describe('(Component) <%= pascalEntityName %>', () => { 2 | it('exists', () => { 3 | 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /blueprints/dumb/files/__root__/components/__name__/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %> from './<%= pascalEntityName %>' 2 | export default <%= pascalEntityName %> 3 | -------------------------------------------------------------------------------- /blueprints/smart/files/__root__/containers/__name__/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %> from './<%= pascalEntityName %>' 2 | export default <%= pascalEntityName %> 3 | -------------------------------------------------------------------------------- /src/static/humans.txt: -------------------------------------------------------------------------------- 1 | # Check it out: http://humanstxt.org/ 2 | 3 | # TEAM 4 | 5 | -- -- 6 | 7 | # THANKS 8 | 9 | 10 | -------------------------------------------------------------------------------- /blueprints/form/files/__root__/forms/__name__Form/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %>Form from './<%= pascalEntityName %>Form' 2 | export default <%= pascalEntityName %>Form 3 | -------------------------------------------------------------------------------- /blueprints/view/files/__root__/views/__name__View/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %>View from './<%= pascalEntityName %>View' 2 | export default <%= pascalEntityName %>View 3 | -------------------------------------------------------------------------------- /blueprints/layout/files/__root__/layouts/__name__Layout/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %>Layout from './<%= pascalEntityName %>Layout' 2 | export default <%= pascalEntityName %>Layout 3 | -------------------------------------------------------------------------------- /blueprints/view/files/__test__/views/__name__View.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | describe('(View) <%= pascalEntityName %>', () => { 4 | it('should exist', () => { 5 | 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /src/app/sagas.js: -------------------------------------------------------------------------------- 1 | import { fork } from 'redux-saga/effects' 2 | import userSagas from 'modules/user/userSagas' 3 | 4 | export default function * (getState) { 5 | yield fork(userSagas, getState) 6 | } 7 | -------------------------------------------------------------------------------- /blueprints/layout/files/__test__/layouts/__name__Layout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | describe('(Layout) <%= pascalEntityName %>', () => { 4 | it('should exist', () => { 5 | 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /src/modules/user/userPropTypes.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react' 2 | 3 | const UserPropType = { 4 | id: PropTypes.number.isRequired, 5 | login: PropTypes.string.isRequired 6 | } 7 | 8 | export default UserPropType 9 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /blueprints/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/counter/Counter.scss: -------------------------------------------------------------------------------- 1 | .counter { 2 | font-weight: bold; 3 | } 4 | 5 | .counter--green { 6 | composes: counter; 7 | color: rgb(25,200,25); 8 | } 9 | 10 | .counterContainer { 11 | margin: 1em auto; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/user/userApi.js: -------------------------------------------------------------------------------- 1 | import { fetchEntitiesFactory, fetchEntityFactory } from 'app/entities/fetchEntities' 2 | 3 | export const fetchUsers = fetchEntitiesFactory('users') 4 | export const fetchUser = fetchEntityFactory('users') 5 | -------------------------------------------------------------------------------- /blueprints/dumb/files/__test__/components/__name__.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import <%= pascalEntityName %> from 'components/<%= pascalEntityName %>/<%= pascalEntityName %>' 3 | 4 | describe('(Component) <%= pascalEntityName %>', () => { 5 | it('should exist', () => { 6 | 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /blueprints/blueprint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'generates a blueprint and definition' 4 | }, 5 | 6 | beforeInstall() { 7 | console.log('Before installation hook!') 8 | }, 9 | 10 | afterInstall() { 11 | console.log('After installation hook!') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /blueprints/smart/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'generates a smart (container) component' 4 | }, 5 | 6 | fileMapTokens() { 7 | return { 8 | __smart__: (options) => { 9 | return options.settings.getSetting('smartPath') 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/entities/createRequestActionTypes.js: -------------------------------------------------------------------------------- 1 | const REQUEST = 'REQUEST' 2 | const SUCCESS = 'SUCCESS' 3 | const FAILURE = 'FAILURE' 4 | 5 | export default (base) => { 6 | return [REQUEST, SUCCESS, FAILURE].reduce((requestTypes, type) => { 7 | requestTypes[type] = `${base}_${type}` 8 | return requestTypes 9 | }, {}) 10 | } 11 | -------------------------------------------------------------------------------- /blueprints/dumb/files/__root__/components/__name__/__name__.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | 5 | }; 6 | export class <%= pascalEntityName %> extends React.Component { 7 | props: Props; 8 | 9 | render () { 10 | return ( 11 |
12 | ) 13 | } 14 | } 15 | 16 | export default <%= pascalEntityName %> 17 | 18 | -------------------------------------------------------------------------------- /blueprints/view/files/__root__/views/__name__View/__name__View.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Props = { 4 | 5 | }; 6 | export class <%= pascalEntityName %> extends React.Component { 7 | props: Props; 8 | 9 | render () { 10 | return ( 11 |
12 | ) 13 | } 14 | } 15 | 16 | export default <%= pascalEntityName %> 17 | -------------------------------------------------------------------------------- /blueprints/duck/files/__test__/redux/modules/__name__.spec.js: -------------------------------------------------------------------------------- 1 | import reducer, { initialState } from 'redux/modules/<%= pascalEntityName %>' 2 | 3 | describe('(Redux) <%= pascalEntityName %>', () => { 4 | describe('(Reducer)', () => { 5 | it('sets up initial state', () => { 6 | expect(reducer(undefined, {})).to.eql(initialState) 7 | }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /blueprints/duck/files/__root__/redux/modules/__name__.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | // export const constants = { } 3 | 4 | // Action Creators 5 | // export const actions = { } 6 | 7 | // Reducer 8 | export const initialState = {} 9 | export default function (state = initialState, action) { 10 | switch (action.type) { 11 | default: 12 | return state 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Application Settings Go Here 3 | ------------------------------------ 4 | This file acts as a bundler for all variables/mixins/themes, so they 5 | can easily be swapped out without `core.scss` ever having to know. 6 | 7 | For example: 8 | 9 | @import './variables/colors'; 10 | @import './variables/components'; 11 | @import './themes/default'; 12 | */ 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "5.0" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm run lint 15 | - npm run test 16 | - NODE_ENV=development npm run deploy 17 | - NODE_ENV=staging npm run deploy 18 | - NODE_ENV=production npm run deploy 19 | 20 | after_success: 21 | - npm run codecov 22 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Redux Starter Kit 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | import config from '../config' 2 | import server from '../server/main' 3 | import _debug from 'debug' 4 | 5 | const debug = _debug('app:bin:server') 6 | const port = config.server_port 7 | const host = config.server_host 8 | 9 | server.listen(port) 10 | debug(`Server is now running at http://${host}:${port}.`) 11 | debug(`Server accessible via localhost:${port} if you are using the project defaults.`) 12 | -------------------------------------------------------------------------------- /blueprints/layout/files/__root__/layouts/__name__Layout/__name__Layout.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | function <%= pascalEntityName %> ({ children }) { 4 | return ( 5 |
6 | {children} 7 |
8 | ) 9 | } 10 | 11 | <%= pascalEntityName %>.propTypes = { 12 | children: PropTypes.element 13 | } 14 | 15 | export default <%= pascalEntityName %> 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | // NOTE: These options are overriden by the babel-loader configuration 2 | // for webpack, which can be found in ~/build/webpack.config. 3 | // 4 | // Why? The react-transform-hmr plugin depends on HMR (and throws if 5 | // module.hot is disabled), so keeping it and related plugins contained 6 | // within webpack helps prevent unexpected errors. 7 | { 8 | "presets": ["es2015", "react", "stage-0"], 9 | "plugins": ["transform-runtime"] 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | @import 'base'; 3 | @import '~normalize.css/normalize'; 4 | 5 | // Some best-practice CSS that's useful for most apps 6 | // Just remove them if they're not what you want 7 | html { 8 | box-sizing: border-box; 9 | } 10 | 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | height: 100%; 16 | } 17 | 18 | *, 19 | *:before, 20 | *:after { 21 | box-sizing: inherit; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/lib/apply-express-middleware.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/dayAlone/koa-webpack-hot-middleware/blob/master/index.js 2 | export default function applyExpressMiddleware(fn, req, res) { 3 | const originalEnd = res.end 4 | 5 | return new Promise((resolve) => { 6 | res.end = function() { 7 | originalEnd.apply(this, arguments) 8 | resolve(false) 9 | } 10 | fn(req, res, function() { 11 | resolve(true) 12 | }) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import DuckImage from './assets/Duck.jpg' 3 | import classes from './Home.scss' 4 | 5 | class Home extends Component { 6 | render() { 7 | return ( 8 |
9 |

Welcome!

10 | This is a duck, because Redux! 14 |
15 | ) 16 | } 17 | } 18 | 19 | export default Home 20 | -------------------------------------------------------------------------------- /src/modules/counter/counterRoutes.spec.js: -------------------------------------------------------------------------------- 1 | import CounterRoutes from './counterRoutes' 2 | 3 | describe('(Route) Counter', () => { 4 | let _route 5 | 6 | beforeEach(() => { 7 | _route = CounterRoutes({}) 8 | }) 9 | 10 | it('Should return a route configuration object', () => { 11 | expect(typeof(_route)).to.equal('object') 12 | }) 13 | 14 | it('Configuration should contain path `counter`', () => { 15 | expect(_route.path).to.equal('counter') 16 | }) 17 | 18 | }) 19 | -------------------------------------------------------------------------------- /src/modules/home/homeRoutes.spec.js: -------------------------------------------------------------------------------- 1 | import HomeRoute from './homeRoutes' 2 | 3 | describe('(Route) Home', () => { 4 | let _component 5 | 6 | beforeEach(() => { 7 | _component = new HomeRoute.component().render() 8 | console.log(_component); 9 | }) 10 | 11 | it('Should return a route configuration object', () => { 12 | expect(typeof(HomeRoute)).to.equal('object') 13 | }) 14 | 15 | it('Should define a route component', () => { 16 | expect(_component.type).to.equal('div') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/app/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as router } from 'react-router-redux' 3 | 4 | export const makeRootReducer = (asyncReducers) => { 5 | return combineReducers({ 6 | // Add sync reducers here 7 | router, 8 | ...asyncReducers 9 | }) 10 | } 11 | 12 | export const injectReducer = (store, { key, reducer }) => { 13 | store.asyncReducers[key] = reducer 14 | store.replaceReducer(makeRootReducer(store.asyncReducers)) 15 | } 16 | 17 | export default makeRootReducer 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "extends" : [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "env" : { 8 | "browser" : true 9 | }, 10 | "globals" : { 11 | "__DEV__" : false, 12 | "__PROD__" : false, 13 | "__DEBUG__" : false, 14 | "__COVERAGE__" : false, 15 | "__BASENAME__" : false 16 | }, 17 | "rules": { 18 | semi: [2, "never"], 19 | "jsx-quotes": ["error", "prefer-double"], 20 | "space-before-function-paren": ["error", "never"], 21 | "max-len": [2, 120, 2] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/counter/counterReducer.js: -------------------------------------------------------------------------------- 1 | import { COUNTER_INCREMENT } from './counterActions' 2 | 3 | // ------------------------------------ 4 | // Action Handlers 5 | // ------------------------------------ 6 | const ACTION_HANDLERS = { 7 | [COUNTER_INCREMENT]: (state, action) => state + action.payload 8 | } 9 | 10 | // ------------------------------------ 11 | // Reducer 12 | // ------------------------------------ 13 | const initialState = 0 14 | export default function counterReducer(state = initialState, action) { 15 | const handler = ACTION_HANDLERS[action.type] 16 | 17 | return handler ? handler(state, action) : state 18 | } 19 | -------------------------------------------------------------------------------- /blueprints/smart/files/__root__/containers/__name__/__name__.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | 5 | type Props = { 6 | 7 | } 8 | export class <%= pascalEntityName %> extends React.Component { 9 | props: Props; 10 | 11 | render () { 12 | return ( 13 |
14 | ) 15 | } 16 | } 17 | 18 | const mapStateToProps = (state) => { 19 | return {} 20 | } 21 | const mapDispatchToProps = (dispatch) => { 22 | return {} 23 | } 24 | 25 | export default connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(<%= pascalEntityName %>) 29 | -------------------------------------------------------------------------------- /server/middleware/webpack-hmr.js: -------------------------------------------------------------------------------- 1 | import WebpackHotMiddleware from 'webpack-hot-middleware' 2 | import applyExpressMiddleware from '../lib/apply-express-middleware' 3 | import _debug from 'debug' 4 | 5 | const debug = _debug('app:server:webpack-hmr') 6 | 7 | export default function(compiler, opts) { 8 | debug('Enable Webpack Hot Module Replacement (HMR).') 9 | 10 | const middleware = WebpackHotMiddleware(compiler, opts) 11 | return async function koaWebpackHMR(ctx, next) { 12 | let hasNext = await applyExpressMiddleware(middleware, ctx.req, ctx.res) 13 | 14 | if (hasNext && next) { 15 | await next() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Header from '../../app/Header' 3 | import classes from './CoreLayout.scss' 4 | import '../../styles/core.scss' 5 | 6 | class CoreLayout extends Component { 7 | static propTypes = { 8 | children: React.PropTypes.element.isRequired 9 | } 10 | 11 | render() { 12 | const { children } = this.props 13 | 14 | return ( 15 |
16 |
17 |
18 | {children} 19 |
20 |
21 | ) 22 | } 23 | } 24 | 25 | export default CoreLayout 26 | -------------------------------------------------------------------------------- /src/modules/home/Home.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Home from './Home' 3 | import { render } from 'enzyme' 4 | 5 | describe('(View) Home', () => { 6 | let _component 7 | 8 | beforeEach(() => { 9 | _component = render() 10 | }) 11 | 12 | it('Renders a welcome message', () => { 13 | const welcome = _component.find('h4') 14 | expect(welcome).to.exist 15 | expect(welcome.text()).to.match(/Welcome!/) 16 | }) 17 | 18 | it('Renders an awesome duck image', () => { 19 | const duck = _component.find('img') 20 | expect(duck).to.exist 21 | expect(duck.attr('alt')).to.match(/This is a duck, because Redux!/) 22 | }) 23 | 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { IndexLink, Link } from 'react-router' 3 | import classes from './Header.scss' 4 | 5 | class Header extends Component { 6 | render() { 7 | return ( 8 |
9 |

React Redux Starter Kit

10 | 11 | Home 12 | 13 | {' · '} 14 | 15 | Counter 16 | 17 | {' · '} 18 | 19 | User 20 | 21 |
22 | ) 23 | } 24 | } 25 | 26 | export default Header 27 | -------------------------------------------------------------------------------- /src/modules/user/userActions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | import createRequestActionTypes from 'app/entities/createRequestActionTypes' 3 | 4 | export const userActionTypes = { 5 | item: createRequestActionTypes('USER'), 6 | list: createRequestActionTypes('USERS') 7 | } 8 | 9 | export default { 10 | item: { 11 | request: createAction(userActionTypes.item.REQUEST), 12 | success: createAction(userActionTypes.item.SUCCESS), 13 | failure: createAction(userActionTypes.item.FAILURE) 14 | }, 15 | list: { 16 | request: createAction(userActionTypes.list.REQUEST), 17 | success: createAction(userActionTypes.list.SUCCESS), 18 | failure: createAction(userActionTypes.list.FAILURE) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /blueprints/blueprint/files/blueprints/__name__/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | locals: function (options) { 5 | // Return custom template variables here. 6 | return {} 7 | }, 8 | 9 | fileMapTokens: function (options) { 10 | // Return custom tokens to be replaced in your files 11 | return { 12 | __token__: function (options) { 13 | // logic to determine value goes here 14 | return 'value' 15 | } 16 | } 17 | }, 18 | 19 | // Should probably never need to be overriden 20 | 21 | filesPath: function () { 22 | return path.join(this.path, 'files') 23 | }, 24 | 25 | beforeInstall: function (options) {}, 26 | afterInstall: function (options) {} 27 | } 28 | -------------------------------------------------------------------------------- /src/app/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Router } from 'react-router' 3 | import { Provider } from 'react-redux' 4 | 5 | class App extends Component { 6 | static propTypes = { 7 | history: PropTypes.object.isRequired, 8 | routes: PropTypes.object.isRequired, 9 | routerKey: PropTypes.number, 10 | store: PropTypes.object.isRequired 11 | } 12 | 13 | render() { 14 | const { history, routes, routerKey, store } = this.props 15 | 16 | return ( 17 | 18 |
19 | 20 |
21 |
22 | ) 23 | } 24 | } 25 | 26 | export default App 27 | -------------------------------------------------------------------------------- /src/modules/user/userSagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest } from 'redux-saga' 2 | import userActions, { userActionTypes } from './userActions' 3 | import { fetchUser as fetchUserApi, fetchUsers as fetchUsersApi } from './userApi' 4 | import { loadListFactory, loadItemFactory } from 'app/entities/sagas' 5 | 6 | export const loadUsers = fetchUsers => 7 | loadListFactory(userActionTypes, userActions, fetchUsers) 8 | 9 | export const loadUser = fetchUser => 10 | loadItemFactory(userActionTypes, userActions, fetchUser) 11 | 12 | const sagas = function * sagas() { 13 | yield [ 14 | takeLatest(userActionTypes.item.REQUEST, loadUser(fetchUserApi)), 15 | takeLatest(userActionTypes.list.REQUEST, loadUsers(fetchUsersApi)) 16 | ] 17 | } 18 | 19 | export default sagas 20 | -------------------------------------------------------------------------------- /bin/compile.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import _debug from 'debug' 3 | import webpackCompiler from '../build/webpack-compiler' 4 | import webpackConfig from '../build/webpack.config' 5 | import config from '../config' 6 | 7 | const debug = _debug('app:bin:compile') 8 | const paths = config.utils_paths 9 | 10 | ;(async function () { 11 | try { 12 | debug('Run compiler') 13 | const stats = await webpackCompiler(webpackConfig) 14 | if (stats.warnings.length && config.compiler_fail_on_warning) { 15 | debug('Config set to fail on warning, exiting with status code "1".') 16 | process.exit(1) 17 | } 18 | debug('Copy static assets to dist folder.') 19 | fs.copySync(paths.client('static'), paths.dist()) 20 | } catch (e) { 21 | debug('Compiler encountered an error.', e) 22 | process.exit(1) 23 | } 24 | })() 25 | -------------------------------------------------------------------------------- /blueprints/form/files/__root__/forms/__name__Form/__name__Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { reduxForm } from 'redux-form' 3 | 4 | export const fields = [] 5 | 6 | const validate = (values) => { 7 | const errors = {} 8 | return errors 9 | } 10 | 11 | type Props = { 12 | handleSubmit: Function, 13 | fields: Object, 14 | } 15 | export class <%= pascalEntityName %> extends React.Component { 16 | props: Props; 17 | 18 | defaultProps = { 19 | fields: {}, 20 | } 21 | 22 | render() { 23 | const { fields, handleSubmit } = this.props 24 | 25 | return ( 26 |
27 |
28 | ) 29 | } 30 | } 31 | 32 | <%= pascalEntityName %> = reduxForm({ 33 | form: '<%= pascalEntityName %>', 34 | fields, 35 | validate 36 | })(<%= pascalEntityName %>) 37 | 38 | export default <%= pascalEntityName %> 39 | -------------------------------------------------------------------------------- /src/app/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from './Header' 3 | import { IndexLink, Link } from 'react-router' 4 | import { shallow } from 'enzyme' 5 | 6 | describe('(Component) Header', () => { 7 | let _wrapper 8 | 9 | beforeEach(() => { 10 | _wrapper = shallow(
) 11 | }) 12 | 13 | it('Renders a welcome message', () => { 14 | const welcome = _wrapper.find('h1') 15 | expect(welcome).to.exist 16 | expect(welcome.text()).to.match(/React Redux Starter Kit/) 17 | }) 18 | 19 | describe('Navigation links...', () => { 20 | 21 | it('Should render an IndexLink to Home route', () => { 22 | expect(_wrapper.contains()).to.equal.true 23 | }) 24 | 25 | it('Should render an Link to Counter route)', () => { 26 | expect(_wrapper.contains()).to.equal.true 27 | }) 28 | 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestUtils from 'react-addons-test-utils' 3 | import CoreLayout from './CoreLayout' 4 | 5 | function shallowRender (component) { 6 | const renderer = TestUtils.createRenderer() 7 | 8 | renderer.render(component) 9 | return renderer.getRenderOutput() 10 | } 11 | 12 | function shallowRenderWithProps (props = {}) { 13 | return shallowRender() 14 | } 15 | 16 | describe('(Layout) Core', function () { 17 | let _component 18 | let _props 19 | let _child 20 | 21 | beforeEach(function () { 22 | _child =

Child

23 | _props = { 24 | children: _child 25 | } 26 | 27 | _component = shallowRenderWithProps(_props) 28 | }) 29 | 30 | it('Should render as a
.', function () { 31 | expect(_component.type).to.equal('div') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/modules/user/userRoutes.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from 'app/reducers' 2 | 3 | export default (store) => ({ 4 | path: 'users', 5 | /* Async getComponent is only invoked when route matches */ 6 | getComponent(nextState, cb) { 7 | /* Webpack - use 'require.ensure' to create a split point 8 | and embed an async module loader (jsonp) when bundling */ 9 | require.ensure([], (require) => { 10 | /* Webpack - use require callback to define 11 | dependencies for bundling */ 12 | const User = require('./User').default 13 | const reducer = require('./userReducer').default 14 | 15 | /* Add the reducer to the store on key 'user' */ 16 | injectReducer(store, { key: 'user', reducer }) 17 | 18 | /* Return getComponent */ 19 | cb(null, User) 20 | 21 | /* Webpack named bundle */ 22 | }, 'user') 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /src/modules/counter/counterActions.js: -------------------------------------------------------------------------------- 1 | export const COUNTER_INCREMENT = 'COUNTER_INCREMENT' 2 | 3 | export const increment = (value = 1) => { 4 | return { 5 | type: COUNTER_INCREMENT, 6 | payload: value 7 | } 8 | } 9 | 10 | /* This is a thunk, meaning it is a function that immediately 11 | returns a function for lazy evaluation. It is incredibly useful for 12 | creating async actions, especially when combined with redux-thunk! 13 | 14 | NOTE: This is solely for demonstration purposes. In a real application, 15 | you'd probably want to dispatch an action of COUNTER_DOUBLE and let the 16 | reducer take care of this logic. */ 17 | export const doubleAsync = () => { 18 | return (dispatch, getState) => { 19 | return new Promise((resolve) => { 20 | setTimeout(() => { 21 | dispatch(increment(getState().counter)) 22 | resolve() 23 | }, 200) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/counter/counterRoutes.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from 'app/reducers' 2 | 3 | export default (store) => ({ 4 | path: 'counter', 5 | /* Async getComponent is only invoked when route matches */ 6 | getComponent(nextState, cb) { 7 | /* Webpack - use 'require.ensure' to create a split point 8 | and embed an async module loader (jsonp) when bundling */ 9 | require.ensure([], (require) => { 10 | /* Webpack - use require callback to define 11 | dependencies for bundling */ 12 | const Counter = require('./Counter').default 13 | const reducer = require('./counterReducer').default 14 | 15 | /* Add the reducer to the store on key 'counter' */ 16 | injectReducer(store, { key: 'counter', reducer }) 17 | 18 | /* Return getComponent */ 19 | cb(null, Counter) 20 | 21 | /* Webpack named bundle */ 22 | }, 'counter') 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/app/entities/sagas.js: -------------------------------------------------------------------------------- 1 | import { call, put } from 'redux-saga/effects' 2 | 3 | export const loadListFactory = (actionTypes, actions, fetchList, jwtAccessor) => 4 | function * loadList() { 5 | const jwt = typeof jwtAccessor === 'function' ? jwtAccessor() : undefined 6 | const { error, list } = yield call(fetchList, jwt) 7 | 8 | if (error) { 9 | console.error(error.message) 10 | yield put(actions.list.failure(error)) 11 | } else { 12 | yield put(actions.list.success(list)) 13 | } 14 | } 15 | 16 | export const loadItemFactory = (actionTypes, actions, fetchItem, jwtAccessor) => 17 | function * loadItem({ payload }) { 18 | const jwt = typeof jwtAccessor === 'function' ? jwtAccessor() : undefined 19 | const { error, item } = yield call(fetchItem, payload, jwt) 20 | 21 | if (error) { 22 | console.error(error.message) 23 | yield put(actions.item.failure(error)) 24 | } else { 25 | yield put(actions.item.success(item)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/middleware/webpack-dev.js: -------------------------------------------------------------------------------- 1 | import WebpackDevMiddleware from 'webpack-dev-middleware' 2 | import applyExpressMiddleware from '../lib/apply-express-middleware' 3 | import _debug from 'debug' 4 | import config from '../../config' 5 | 6 | const paths = config.utils_paths 7 | const debug = _debug('app:server:webpack-dev') 8 | 9 | export default function(compiler, publicPath) { 10 | debug('Enable webpack dev middleware.') 11 | 12 | const middleware = WebpackDevMiddleware(compiler, { 13 | publicPath, 14 | contentBase: paths.client(), 15 | hot: true, 16 | quiet: config.compiler_quiet, 17 | noInfo: config.compiler_quiet, 18 | lazy: false, 19 | stats: config.compiler_stats 20 | }) 21 | 22 | return async function koaWebpackDevMiddleware(ctx, next) { 23 | let hasNext = await applyExpressMiddleware(middleware, ctx.req, { 24 | end: (content) => (ctx.body = content), 25 | setHeader: function() { 26 | ctx.set.apply(ctx, arguments) 27 | } 28 | }) 29 | 30 | if (hasNext) { 31 | await next() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Zukowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/modules/user/userReducer.js: -------------------------------------------------------------------------------- 1 | import { userActionTypes } from './userActions' 2 | 3 | const initialState = { 4 | list: [], 5 | item: null, 6 | error: null, 7 | loading: false 8 | } 9 | 10 | export default (state = initialState, { type, payload }) => { 11 | switch (type) { 12 | case userActionTypes.list.REQUEST: 13 | case userActionTypes.item.REQUEST: 14 | return { 15 | ...state, 16 | loading: true 17 | } 18 | 19 | case userActionTypes.list.SUCCESS: 20 | return { 21 | ...state, 22 | list: payload, 23 | error: null, 24 | loading: false 25 | } 26 | 27 | case userActionTypes.list.FAILURE: 28 | return { 29 | ...state, 30 | list: [], 31 | error: payload, 32 | loading: false 33 | } 34 | 35 | case userActionTypes.item.SUCCESS: 36 | return { 37 | ...state, 38 | item: payload, 39 | error: null, 40 | loading: false 41 | } 42 | 43 | case userActionTypes.item.FAILURE: 44 | return { 45 | ...state, 46 | item: null, 47 | error: payload, 48 | loading: false 49 | } 50 | 51 | default: 52 | return state 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/framework.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import React from 'react' 3 | import {mount, render, shallow} from 'enzyme' 4 | 5 | class Fixture extends React.Component { 6 | render () { 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } 14 | } 15 | 16 | describe('(Framework) Karma Plugins', function () { 17 | it('Should expose "expect" globally.', function () { 18 | assert.ok(expect) 19 | }) 20 | 21 | it('Should expose "should" globally.', function () { 22 | assert.ok(should) 23 | }) 24 | 25 | it('Should have chai-as-promised helpers.', function () { 26 | const pass = new Promise(res => res('test')) 27 | const fail = new Promise((res, rej) => rej()) 28 | 29 | return Promise.all([ 30 | expect(pass).to.be.fulfilled, 31 | expect(fail).to.not.be.fulfilled 32 | ]) 33 | }) 34 | 35 | it('should have chai-enzyme working', function() { 36 | let wrapper = shallow() 37 | expect(wrapper.find('#checked')).to.be.checked() 38 | 39 | wrapper = mount() 40 | expect(wrapper.find('#checked')).to.be.checked() 41 | 42 | wrapper = render() 43 | expect(wrapper.find('#checked')).to.be.checked() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/app/routes.js: -------------------------------------------------------------------------------- 1 | // We only need to import the modules necessary for initial render 2 | import CoreLayout from 'layouts/CoreLayout/CoreLayout' 3 | import HomeRoutes from 'modules/home/homeRoutes' 4 | import CounterRoutes from 'modules/counter/counterRoutes' 5 | import UserRoutes from 'modules/user/userRoutes' 6 | 7 | /* Note: Instead of using JSX, we recommend using react-router 8 | PlainRoute objects to build route definitions. */ 9 | 10 | export const createRoutes = (store) => ({ 11 | path: '/', 12 | component: CoreLayout, 13 | indexRoute: HomeRoutes, 14 | childRoutes: [ 15 | CounterRoutes(store), 16 | UserRoutes(store) 17 | ] 18 | }) 19 | 20 | /* Note: childRoutes can be chunked or otherwise loaded programmatically 21 | using getChildRoutes with the following signature: 22 | 23 | getChildRoutes (location, cb) { 24 | require.ensure([], (require) => { 25 | cb(null, [ 26 | // Remove imports! 27 | require('./Counter').default(store) 28 | ]) 29 | }) 30 | } 31 | 32 | However, this is not necessary for code-splitting! It simply provides 33 | an API for async route definitions. Your code splitting should occur 34 | inside the route `getComponent` function, since it is only invoked 35 | when the route exists and matches. 36 | */ 37 | 38 | export default createRoutes 39 | -------------------------------------------------------------------------------- /config/environments.js: -------------------------------------------------------------------------------- 1 | // Here is where you can define configuration overrides based on the execution environment. 2 | // Supply a key to the default export matching the NODE_ENV that you wish to target, and 3 | // the base configuration will apply your overrides before exporting itself. 4 | export default { 5 | // ====================================================== 6 | // Overrides when NODE_ENV === 'development' 7 | // ====================================================== 8 | // NOTE: In development, we use an explicit public path when the assets 9 | // are served webpack by to fix this issue: 10 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 11 | development: (config) => ({ 12 | compiler_public_path: `http://${config.server_host}:${config.server_port}/`, 13 | proxy: { 14 | enabled: true, 15 | options: { 16 | host: 'https://api.github.com', 17 | map: (path) => path.replace('/api', ''), 18 | match: /^\/api\/.*/ 19 | } 20 | } 21 | }), 22 | 23 | // ====================================================== 24 | // Overrides when NODE_ENV === 'production' 25 | // ====================================================== 26 | production: (config) => ({ 27 | compiler_public_path: '/', 28 | compiler_fail_on_warning: false, 29 | compiler_hash_type: 'chunkhash', 30 | compiler_devtool: null, 31 | compiler_stats: { 32 | chunks: true, 33 | chunkModules: true, 34 | colors: true 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /tests/test-bundler.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Test Environment Setup 3 | // --------------------------------------- 4 | import 'babel-polyfill' 5 | import sinon from 'sinon' 6 | import chai from 'chai' 7 | import sinonChai from 'sinon-chai' 8 | import chaiAsPromised from 'chai-as-promised' 9 | import chaiEnzyme from 'chai-enzyme' 10 | 11 | chai.use(sinonChai) 12 | chai.use(chaiAsPromised) 13 | chai.use(chaiEnzyme()) 14 | 15 | global.chai = chai 16 | global.sinon = sinon 17 | global.expect = chai.expect 18 | global.should = chai.should() 19 | 20 | // --------------------------------------- 21 | // Require Tests 22 | // --------------------------------------- 23 | // for use with karma-webpack-with-fast-source-maps 24 | // NOTE: `new Array()` is used rather than an array literal since 25 | // for some reason an array literal without a trailing `;` causes 26 | // some build environments to fail. 27 | const __karmaWebpackManifest__ = new Array() // eslint-disable-line 28 | const inManifest = (path) => ~__karmaWebpackManifest__.indexOf(path) 29 | 30 | // require all `tests/**/*.spec.js` 31 | const testsContext = require.context('./', true, /\.spec\.js$/) 32 | 33 | // only run tests that have changed after the first pass. 34 | const testsToRun = testsContext.keys().filter(inManifest) 35 | ;(testsToRun.length ? testsToRun : testsContext.keys()).forEach(testsContext) 36 | 37 | // require all `src/**/*.js` except for `main.js` (for isparta coverage reporting) 38 | if (__COVERAGE__) { 39 | const componentsContext = require.context('../src/', true, /^((?!main).)*\.js$/) 40 | componentsContext.keys().forEach(componentsContext) 41 | } 42 | -------------------------------------------------------------------------------- /src/app/entities/fetchEntities.js: -------------------------------------------------------------------------------- 1 | /* globals API_URL */ 2 | export const fetchEntitiesFactory = path => jwt => { 3 | const headers = { 4 | 'Accept': 'application/json', 5 | 'Content-Type': 'application/json charset=utf-8' 6 | } 7 | 8 | if (jwt) { 9 | headers['Authorization'] = jwt 10 | } 11 | 12 | return fetch(`${API_URL}/${path}`, { 13 | headers, 14 | // Allows API to set http-only cookies with AJAX calls 15 | // @see http://www.redotheweb.com/2015/11/09/api-security.html 16 | credentials: 'include' 17 | }) 18 | .then(response => { 19 | if (!response.ok) { 20 | return response.text().then(result => Promise.reject(new Error(result))) 21 | } 22 | 23 | return response.json() 24 | }) 25 | .then(json => { 26 | return { list: json } 27 | }, error => ({ 28 | error 29 | })) 30 | } 31 | 32 | export const fetchEntityFactory = path => (id, jwt) => { 33 | const headers = { 34 | 'Accept': 'application/json', 35 | 'Content-Type': 'application/json charset=utf-8' 36 | } 37 | 38 | if (jwt) { 39 | headers['Authorization'] = jwt 40 | } 41 | 42 | return fetch(`${API_URL}/${path}/${id}`, { 43 | headers, 44 | // Allows API to set http-only cookies with AJAX calls 45 | // @see http://www.redotheweb.com/2015/11/09/api-security.html 46 | credentials: 'include' 47 | }) 48 | .then(response => { 49 | if (!response.ok) { 50 | return response.text().then(result => Promise.reject(new Error(result))) 51 | } 52 | 53 | return response.json() 54 | }) 55 | .then(json => { 56 | return { item: json } 57 | }, error => ({ 58 | error 59 | })) 60 | } 61 | -------------------------------------------------------------------------------- /src/app/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux' 2 | import { routerMiddleware } from 'react-router-redux' 3 | import thunk from 'redux-thunk' 4 | import createSagaMiddleware from 'redux-saga' 5 | import makeRootReducer from './reducers' 6 | import sagas from './sagas' 7 | 8 | export default (initialState = {}, history) => { 9 | // ====================================================== 10 | // Middleware Configuration 11 | // ====================================================== 12 | const sagaMiddleware = createSagaMiddleware() 13 | const middleware = [ 14 | sagaMiddleware, 15 | thunk, 16 | routerMiddleware(history) 17 | ] 18 | 19 | // ====================================================== 20 | // Store Enhancers 21 | // ====================================================== 22 | const enhancers = [] 23 | if (__DEBUG__) { 24 | const devToolsExtension = window.devToolsExtension 25 | 26 | if (typeof devToolsExtension === 'function') { 27 | enhancers.push(devToolsExtension()) 28 | } 29 | } 30 | 31 | // ====================================================== 32 | // Store Instantiation and HMR Setup 33 | // ====================================================== 34 | const store = createStore( 35 | makeRootReducer(), 36 | initialState, 37 | compose( 38 | applyMiddleware(...middleware), 39 | ...enhancers 40 | ) 41 | ) 42 | sagaMiddleware.run(sagas) 43 | store.asyncReducers = {} 44 | 45 | if (module.hot) { 46 | module.hot.accept('./reducers', () => { 47 | const reducers = require('./reducers').default 48 | store.replaceReducer(reducers) 49 | }) 50 | } 51 | 52 | return store 53 | } 54 | -------------------------------------------------------------------------------- /dd1ac21a8e4eb7562d3e1a3d39437aff29674162.patch: -------------------------------------------------------------------------------- 1 | From dd1ac21a8e4eb7562d3e1a3d39437aff29674162 Mon Sep 17 00:00:00 2001 2 | From: marshallford 3 | Date: Wed, 27 Apr 2016 22:12:17 -0500 4 | Subject: [PATCH] feat(webpack): allow user to serve app over local network 5 | 6 | --- 7 | config/index.js | 4 +++- 8 | package.json | 3 ++- 9 | 2 files changed, 5 insertions(+), 2 deletions(-) 10 | 11 | diff --git a/config/index.js b/config/index.js 12 | index 79a551b..70fe44d 100644 13 | --- a/config/index.js 14 | +++ b/config/index.js 15 | @@ -2,7 +2,9 @@ 16 | import path from 'path' 17 | import _debug from 'debug' 18 | import { argv } from 'yargs' 19 | +import ip from 'ip' 20 | 21 | +const localip = ip.address() 22 | const debug = _debug('app:config') 23 | debug('Creating default configuration.') 24 | 25 | @@ -24,7 +26,7 @@ const config = { 26 | // ---------------------------------- 27 | // Server Configuration 28 | // ---------------------------------- 29 | - server_host : 'localhost', 30 | + server_host : localip, // use string 'localhost' to prevent exposure on local network 31 | server_port : process.env.PORT || 3000, 32 | 33 | // ---------------------------------- 34 | diff --git a/package.json b/package.json 35 | index 6cc5823..a6f8c13 100644 36 | --- a/package.json 37 | +++ b/package.json 38 | @@ -83,8 +83,8 @@ 39 | "babel-plugin-transform-runtime": "^6.3.13", 40 | "babel-preset-es2015": "^6.3.13", 41 | "babel-preset-react": "^6.3.13", 42 | - "babel-preset-stage-0": "^6.3.13", 43 | "babel-preset-react-optimize": "^1.0.1", 44 | + "babel-preset-stage-0": "^6.3.13", 45 | "babel-register": "^6.3.13", 46 | "babel-runtime": "^6.3.19", 47 | "better-npm-run": "0.0.8", 48 | @@ -97,6 +97,7 @@ 49 | "history": "^2.0.0", 50 | "html-webpack-plugin": "^2.7.1", 51 | "imports-loader": "^0.6.5", 52 | + "ip": "^1.1.2", 53 | "json-loader": "^0.5.4", 54 | "koa": "^2.0.0-alpha.3", 55 | "koa-connect-history-api-fallback": "^0.3.0", 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. 8 | 9 | * Non-trivial changes should be discussed in an issue first 10 | * Develop in a topic branch, not master 11 | * Squash your commits 12 | 13 | ### Linting 14 | 15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails. 16 | 17 | ### Commit Message Format 18 | 19 | Each commit message should include a **type**, a **scope** and a **subject**: 20 | 21 | ``` 22 | (): 23 | ``` 24 | 25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: 26 | 27 | ``` 28 | #271 feat(standard): add style config and refactor to match 29 | #270 fix(config): only override publicPath when served by webpack 30 | #269 feat(eslint-config-defaults): replace eslint-config-airbnb 31 | #268 feat(config): allow user to configure webpack stats output 32 | ``` 33 | 34 | #### Type 35 | 36 | Must be one of the following: 37 | 38 | * **feat**: A new feature 39 | * **fix**: A bug fix 40 | * **docs**: Documentation only changes 41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 42 | semi-colons, etc) 43 | * **refactor**: A code change that neither fixes a bug or adds a feature 44 | * **test**: Adding missing tests 45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 46 | generation 47 | 48 | #### Scope 49 | 50 | The scope could be anything specifying place of the commit change. For example `webpack`, 51 | `babel`, `redux` etc... 52 | 53 | #### Subject 54 | 55 | The subject contains succinct description of the change: 56 | 57 | * use the imperative, present tense: "change" not "changed" nor "changes" 58 | * don't capitalize first letter 59 | * no dot (.) at the end 60 | -------------------------------------------------------------------------------- /src/modules/counter/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { Counter } from './Counter' 4 | import { shallow } from 'enzyme' 5 | 6 | describe('(Component) Counter', () => { 7 | let _props, _spies, _wrapper 8 | 9 | beforeEach(() => { 10 | _spies = {} 11 | _props = { 12 | counter: 5, 13 | ...bindActionCreators({ 14 | doubleAsync: (_spies.doubleAsync = sinon.spy()), 15 | increment: (_spies.increment = sinon.spy()) 16 | }, _spies.dispatch = sinon.spy()) 17 | } 18 | _wrapper = shallow() 19 | }) 20 | 21 | it('Should render as a
.', () => { 22 | expect(_wrapper.is('div')).to.equal(true) 23 | }) 24 | 25 | it('Should render with an

that includes Sample Counter text.', () => { 26 | expect(_wrapper.find('h2').text()).to.match(/Counter:/) 27 | }) 28 | 29 | it('Should render props.counter at the end of the sample counter

.', () => { 30 | expect(_wrapper.find('h2').text()).to.match(/5$/) 31 | _wrapper.setProps({ counter: 8 }) 32 | expect(_wrapper.find('h2').text()).to.match(/8$/) 33 | }) 34 | 35 | it('Should render exactly two buttons.', () => { 36 | expect(_wrapper).to.have.descendants('.btn') 37 | }) 38 | // 39 | describe('An increment button...', () => { 40 | let _button 41 | 42 | beforeEach(() => { 43 | _button = _wrapper.find('button').filterWhere(a => a.text() === 'Increment') 44 | }) 45 | 46 | it('has bootstrap classes', () => { 47 | expect(_button.hasClass('btn btn-default')).to.be.true 48 | }) 49 | 50 | it('Should dispatch a `increment` action when clicked', () => { 51 | _spies.dispatch.should.have.not.been.called 52 | 53 | _button.simulate('click') 54 | 55 | _spies.dispatch.should.have.been.called 56 | _spies.increment.should.have.been.called 57 | }); 58 | }) 59 | 60 | describe('A Double (Async) button...', () => { 61 | let _button 62 | 63 | beforeEach(() => { 64 | _button = _wrapper.find('button').filterWhere(a => a.text() === 'Double (Async)') 65 | }) 66 | 67 | it('has bootstrap classes', () => { 68 | expect(_button.hasClass('btn btn-default')).to.be.true 69 | }) 70 | 71 | it('Should dispatch a `doubleAsync` action when clicked', () => { 72 | _spies.dispatch.should.have.not.been.called 73 | 74 | _button.simulate('click') 75 | 76 | _spies.dispatch.should.have.been.called 77 | _spies.doubleAsync.should.have.been.called 78 | }); 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/modules/counter/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import classes from './Counter.scss' 3 | import { connect } from 'react-redux' 4 | import { increment, doubleAsync } from './counterActions' 5 | 6 | export class Counter extends Component { 7 | static propTypes = { 8 | counter: PropTypes.number.isRequired, 9 | doubleAsync: PropTypes.func.isRequired, 10 | increment: PropTypes.func.isRequired 11 | } 12 | 13 | render() { 14 | const { counter, doubleAsync, increment } = this.props 15 | 16 | return ( 17 |
18 |

19 | Counter: 20 | {' '} 21 | 22 | {counter} 23 | 24 |

25 | 28 | {' '} 29 | 32 |
33 | ) 34 | } 35 | } 36 | 37 | /* This is a container component. Notice it does not contain any JSX, 38 | nor does it import React. This component is **only** responsible for 39 | wiring in the actions and state necessary to render a presentational 40 | component - in this case, the counter: */ 41 | 42 | /* Object of action creators (can also be function that returns object). 43 | Keys will be passed as props to presentational components. Here we are 44 | implementing our wrapper around increment; the component doesn't care */ 45 | 46 | const mapActionCreators = { 47 | increment: () => increment(1), 48 | doubleAsync 49 | } 50 | 51 | const mapStateToProps = (state) => ({ 52 | counter: state.counter 53 | }) 54 | 55 | /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: 56 | 57 | import { createSelector } from 'reselect' 58 | const counter = (state) => state.counter 59 | const tripleCount = createSelector(counter, (count) => count * 3) 60 | const mapStateToProps = (state) => ({ 61 | counter: tripleCount(state) 62 | }) 63 | 64 | Selectors can compute derived data, allowing Redux to store the minimal possible state. 65 | Selectors are efficient. A selector is not recomputed unless one of its arguments change. 66 | Selectors are composable. They can be used as input to other selectors. 67 | https://github.com/reactjs/reselect */ 68 | 69 | export default connect(mapStateToProps, mapActionCreators)(Counter) 70 | -------------------------------------------------------------------------------- /src/modules/user/User.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import userActions from './userActions' 5 | import { UserPropType } from './userPropTypes' 6 | 7 | class User extends Component { 8 | componentDidMount() { 9 | this.props.loadUsers() 10 | } 11 | 12 | render() { 13 | const { loading, users } = this.props 14 | 15 | return ( 16 |
17 | {loading && 18 |
19 | Loading 20 |
21 | } 22 | {!loading && 23 | users.map(user => ( 24 |
25 | {user.login} 26 |
27 | )) 28 | } 29 |
30 | ) 31 | } 32 | } 33 | 34 | User.propTypes = { 35 | users: PropTypes.arrayOf(PropTypes.shape(UserPropType)), 36 | loading: PropTypes.bool.isRequired, 37 | loadUsers: PropTypes.func.isRequired 38 | } 39 | 40 | /* This is a container component. Notice it does not contain any JSX, 41 | nor does it import React. This component is **only** responsible for 42 | wiring in the actions and state necessary to render a presentational 43 | component - in this case, the user: */ 44 | 45 | /* Object of action creators (can also be function that returns object). 46 | Keys will be passed as props to presentational components. Here we are 47 | implementing our wrapper around increment; the component doesn't care */ 48 | 49 | const mapStateToProps = (state) => ({ 50 | loading: state.user.loading, 51 | users: state.user.list 52 | }) 53 | 54 | const mapDispatchToProps = (dispatch) => bindActionCreators({ 55 | loadUsers: userActions.list.request 56 | }, dispatch) 57 | 58 | /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: 59 | 60 | import { createSelector } from 'reselect' 61 | const user = (state) => state.user 62 | const tripleCount = createSelector(user, (count) => count * 3) 63 | const mapStateToProps = (state) => ({ 64 | user: tripleCount(state) 65 | }) 66 | 67 | Selectors can compute derived data, allowing Redux to store the minimal possible state. 68 | Selectors are efficient. A selector is not recomputed unless one of its arguments change. 69 | Selectors are composable. They can be used as input to other selectors. 70 | https://github.com/reactjs/reselect */ 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(User) 73 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import convert from 'koa-convert' 3 | import webpack from 'webpack' 4 | import webpackConfig from '../build/webpack.config' 5 | import historyApiFallback from 'koa-connect-history-api-fallback' 6 | import serve from 'koa-static' 7 | import proxy from 'koa-proxy' 8 | import _debug from 'debug' 9 | import config from '../config' 10 | import webpackDevMiddleware from './middleware/webpack-dev' 11 | import webpackHMRMiddleware from './middleware/webpack-hmr' 12 | import open from 'open' 13 | 14 | const debug = _debug('app:server') 15 | const paths = config.utils_paths 16 | const app = new Koa() 17 | 18 | // Enable koa-proxy if it has been enabled in the config. 19 | if (config.proxy && config.proxy.enabled) { 20 | app.use(convert(proxy(config.proxy.options))) 21 | } 22 | 23 | // This rewrites all routes requests to the root /index.html file 24 | // (ignoring file requests). If you want to implement isomorphic 25 | // rendering, you'll want to remove this middleware. 26 | app.use(convert(historyApiFallback({ 27 | verbose: false 28 | }))) 29 | 30 | // ------------------------------------ 31 | // Apply Webpack HMR Middleware 32 | // ------------------------------------ 33 | if (config.env === 'development') { 34 | const compiler = webpack(webpackConfig) 35 | 36 | // Enable webpack-dev and webpack-hot middleware 37 | const { publicPath } = webpackConfig.output 38 | 39 | app.use(webpackDevMiddleware(compiler, publicPath)) 40 | app.use(webpackHMRMiddleware(compiler)) 41 | 42 | // Serve static assets from ~/src/static since Webpack is unaware of 43 | // these files. This middleware doesn't need to be enabled outside 44 | // of development since this directory will be copied into ~/dist 45 | // when the application is compiled. 46 | app.use(convert(serve(paths.client('static')))) 47 | 48 | open(`http://${config.server_host}:${config.server_port}`) 49 | } else { 50 | debug( 51 | 'Server is being run outside of live development mode, meaning it will ' + 52 | 'only serve the compiled application bundle in ~/dist. Generally you ' + 53 | 'do not need an application server for this and can instead use a web ' + 54 | 'server such as nginx to serve your static files. See the "deployment" ' + 55 | 'section in the README for more information on deployment strategies.' 56 | ) 57 | 58 | // Serving ~/dist by default. Ideally these files should be served by 59 | // the web server and not the app server, but this helps to demo the 60 | // server in production. 61 | app.use(convert(serve(paths.dist()))) 62 | } 63 | 64 | export default app 65 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import createBrowserHistory from 'history/lib/createBrowserHistory' 4 | import { useRouterHistory } from 'react-router' 5 | import { syncHistoryWithStore } from 'react-router-redux' 6 | import configureStore from './app/configureStore' 7 | import App from './app/App' 8 | 9 | // ======================================================== 10 | // Browser History Setup 11 | // ======================================================== 12 | const browserHistory = useRouterHistory(createBrowserHistory)({ 13 | basename: __BASENAME__ 14 | }) 15 | 16 | // ======================================================== 17 | // Store and History Instantiation 18 | // ======================================================== 19 | // Create redux store and sync with react-router-redux. We have installed the 20 | // react-router-redux reducer under the key "router" in src/routes/index.js, 21 | // so we need to provide a custom `selectLocationState` to inform 22 | // react-router-redux of its location. 23 | const initialState = window.___INITIAL_STATE__ 24 | const store = configureStore(initialState, browserHistory) 25 | const history = syncHistoryWithStore(browserHistory, store, { 26 | selectLocationState: (state) => state.router 27 | }) 28 | 29 | // ======================================================== 30 | // Developer Tools Setup 31 | // ======================================================== 32 | if (__DEBUG__) { 33 | if (window.devToolsExtension) { 34 | window.devToolsExtension.open() 35 | } 36 | } 37 | 38 | // ======================================================== 39 | // Render Setup 40 | // ======================================================== 41 | const MOUNT_NODE = document.getElementById('root') 42 | let render = (routerKey = null) => { 43 | const routes = require('./app/routes').default(store) 44 | 45 | ReactDOM.render( 46 | , 52 | MOUNT_NODE 53 | ) 54 | } 55 | 56 | // ======================================================== 57 | // Developer Tools Setup 58 | // ======================================================== 59 | if (__DEV__ && module.hot) { 60 | const renderApp = render 61 | const renderError = (error) => { 62 | const RedBox = require('redbox-react') 63 | 64 | ReactDOM.render(, MOUNT_NODE) 65 | } 66 | render = () => { 67 | try { 68 | renderApp(Math.random()) 69 | } catch (error) { 70 | renderError(error) 71 | } 72 | } 73 | module.hot.accept(['./app/routes'], () => render()) 74 | } 75 | 76 | // ======================================================== 77 | // Go! 78 | // ======================================================== 79 | render() 80 | -------------------------------------------------------------------------------- /src/modules/counter/counterReducer.spec.js: -------------------------------------------------------------------------------- 1 | import counterReducer from './counterReducer' 2 | 3 | import { 4 | COUNTER_INCREMENT, 5 | increment, 6 | doubleAsync 7 | } from './counterActions' 8 | 9 | describe('(Redux Module) Counter', () => { 10 | it('Should export a constant COUNTER_INCREMENT.', () => { 11 | expect(COUNTER_INCREMENT).to.equal('COUNTER_INCREMENT') 12 | }) 13 | 14 | describe('(Reducer)', () => { 15 | it('Should be a function.', () => { 16 | expect(counterReducer).to.be.a('function') 17 | }) 18 | 19 | it('Should initialize with a state of 0 (Number).', () => { 20 | expect(counterReducer(undefined, {})).to.equal(0) 21 | }) 22 | 23 | it('Should return the previous state if an action was not matched.', () => { 24 | let state = counterReducer(undefined, {}) 25 | expect(state).to.equal(0) 26 | state = counterReducer(state, {type: '@@@@@@@'}) 27 | expect(state).to.equal(0) 28 | state = counterReducer(state, increment(5)) 29 | expect(state).to.equal(5) 30 | state = counterReducer(state, {type: '@@@@@@@'}) 31 | expect(state).to.equal(5) 32 | }) 33 | }) 34 | 35 | describe('(Action Creator) increment', () => { 36 | it('Should be exported as a function.', () => { 37 | expect(increment).to.be.a('function') 38 | }) 39 | 40 | it('Should return an action with type "COUNTER_INCREMENT".', () => { 41 | expect(increment()).to.have.property('type', COUNTER_INCREMENT) 42 | }) 43 | 44 | it('Should assign the first argument to the "payload" property.', () => { 45 | expect(increment(5)).to.have.property('payload', 5) 46 | }) 47 | 48 | it('Should default the "payload" property to 1 if not provided.', () => { 49 | expect(increment()).to.have.property('payload', 1) 50 | }) 51 | }) 52 | 53 | describe('(Action Creator) doubleAsync', () => { 54 | let _globalState 55 | let _dispatchSpy 56 | let _getStateSpy 57 | 58 | beforeEach(() => { 59 | _globalState = { 60 | counter: counterReducer(undefined, {}) 61 | } 62 | _dispatchSpy = sinon.spy((action) => { 63 | _globalState = { 64 | ..._globalState, 65 | counter: counterReducer(_globalState.counter, action) 66 | } 67 | }) 68 | _getStateSpy = sinon.spy(() => { 69 | return _globalState 70 | }) 71 | }) 72 | 73 | it('Should be exported as a function.', () => { 74 | expect(doubleAsync).to.be.a('function') 75 | }) 76 | 77 | it('Should return a function (is a thunk).', () => { 78 | expect(doubleAsync()).to.be.a('function') 79 | }) 80 | 81 | it('Should return a promise from that thunk that gets fulfilled.', () => { 82 | return doubleAsync()(_dispatchSpy, _getStateSpy).should.eventually.be.fulfilled 83 | }) 84 | 85 | it('Should call dispatch and getState exactly once.', () => { 86 | return doubleAsync()(_dispatchSpy, _getStateSpy) 87 | .then(() => { 88 | _dispatchSpy.should.have.been.calledOnce 89 | _getStateSpy.should.have.been.calledOnce 90 | }) 91 | }) 92 | 93 | it('Should produce a state that is double the previous state.', () => { 94 | _globalState = { counter: 2 } 95 | 96 | return doubleAsync()(_dispatchSpy, _getStateSpy) 97 | .then(() => { 98 | _dispatchSpy.should.have.been.calledOnce 99 | _getStateSpy.should.have.been.calledOnce 100 | expect(_globalState.counter).to.equal(4) 101 | return doubleAsync()(_dispatchSpy, _getStateSpy) 102 | }) 103 | .then(() => { 104 | _dispatchSpy.should.have.been.calledTwice 105 | _getStateSpy.should.have.been.calledTwice 106 | expect(_globalState.counter).to.equal(8) 107 | }) 108 | }) 109 | }) 110 | 111 | // NOTE: if you have a more complex state, you will probably want to verify 112 | // that you did not mutate the state. In this case our state is just a number 113 | // (which cannot be mutated). 114 | describe('(Action Handler) COUNTER_INCREMENT', () => { 115 | it('Should increment the state by the action payload\'s "value" property.', () => { 116 | let state = counterReducer(undefined, {}) 117 | expect(state).to.equal(0) 118 | state = counterReducer(state, increment(1)) 119 | expect(state).to.equal(1) 120 | state = counterReducer(state, increment(2)) 121 | expect(state).to.equal(3) 122 | state = counterReducer(state, increment(-3)) 123 | expect(state).to.equal(0) 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | /* eslint key-spacing:0 spaced-comment:0 */ 2 | import path from 'path' 3 | import _debug from 'debug' 4 | import { argv } from 'yargs' 5 | import ip from 'ip' 6 | 7 | const localip = ip.address() 8 | const debug = _debug('app:config') 9 | debug('Creating default configuration.') 10 | 11 | // ======================================================== 12 | // Default Configuration 13 | // ======================================================== 14 | const config = { 15 | env : process.env.NODE_ENV || 'development', 16 | 17 | // ---------------------------------- 18 | // Project Structure 19 | // ---------------------------------- 20 | path_base : path.resolve(__dirname, '..'), 21 | dir_client : 'src', 22 | dir_dist : 'dist', 23 | dir_server : 'server', 24 | dir_test : 'tests', 25 | 26 | // ---------------------------------- 27 | // Server Configuration 28 | // ---------------------------------- 29 | server_host : localip, // use string 'localhost' to prevent exposure on local network 30 | server_port : process.env.PORT || 3000, 31 | 32 | // ---------------------------------- 33 | // Compiler Configuration 34 | // ---------------------------------- 35 | compiler_css_modules : true, 36 | compiler_devtool : 'source-map', 37 | compiler_hash_type : 'hash', 38 | compiler_fail_on_warning : false, 39 | compiler_quiet : false, 40 | compiler_public_path : '/', 41 | compiler_stats : { 42 | chunks : false, 43 | chunkModules : false, 44 | colors : true 45 | }, 46 | compiler_vendor : [ 47 | 'history', 48 | 'react', 49 | 'react-redux', 50 | 'react-router', 51 | 'react-router-redux', 52 | 'redux' 53 | ], 54 | 55 | // ---------------------------------- 56 | // Test Configuration 57 | // ---------------------------------- 58 | coverage_reporters : [ 59 | { type : 'text-summary' }, 60 | { type : 'lcov', dir : 'coverage' } 61 | ] 62 | } 63 | 64 | /************************************************ 65 | ------------------------------------------------- 66 | 67 | All Internal Configuration Below 68 | Edit at Your Own Risk 69 | 70 | ------------------------------------------------- 71 | ************************************************/ 72 | 73 | // ------------------------------------ 74 | // Environment 75 | // ------------------------------------ 76 | // N.B.: globals added here must _also_ be added to .eslintrc 77 | config.globals = { 78 | 'process.env' : { 79 | 'NODE_ENV' : JSON.stringify(config.env) 80 | }, 81 | 'NODE_ENV' : config.env, 82 | '__DEV__' : config.env === 'development', 83 | '__PROD__' : config.env === 'production', 84 | '__TEST__' : config.env === 'test', 85 | '__DEBUG__' : config.env === 'development' && !argv.no_debug, 86 | '__COVERAGE__' : !argv.watch && config.env === 'test', 87 | '__BASENAME__' : JSON.stringify(process.env.BASENAME || ''), 88 | 'API_URL' : JSON.stringify('/api') 89 | } 90 | 91 | // ------------------------------------ 92 | // Validate Vendor Dependencies 93 | // ------------------------------------ 94 | const pkg = require('../package.json') 95 | 96 | config.compiler_vendor = config.compiler_vendor 97 | .filter((dep) => { 98 | if (pkg.dependencies[dep]) return true 99 | 100 | debug( 101 | `Package "${dep}" was not found as an npm dependency in package.json; ` + 102 | `it won't be included in the webpack vendor bundle. 103 | Consider removing it from vendor_dependencies in ~/config/index.js` 104 | ) 105 | }) 106 | 107 | // ------------------------------------ 108 | // Utilities 109 | // ------------------------------------ 110 | const resolve = path.resolve 111 | const base = (...args) => 112 | Reflect.apply(resolve, null, [config.path_base, ...args]) 113 | 114 | config.utils_paths = { 115 | base : base, 116 | client : base.bind(null, config.dir_client), 117 | dist : base.bind(null, config.dir_dist) 118 | } 119 | 120 | // ======================================================== 121 | // Environment Configuration 122 | // ======================================================== 123 | debug(`Looking for environment overrides for NODE_ENV "${config.env}".`) 124 | const environments = require('./environments').default 125 | const overrides = environments[config.env] 126 | if (overrides) { 127 | debug('Found overrides, applying to default configuration.') 128 | Object.assign(config, overrides(config)) 129 | } else { 130 | debug('No environment overrides found, defaults will be used.') 131 | } 132 | 133 | export default config 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-starter-kit", 3 | "version": "3.0.0-alpha.0", 4 | "description": "Get started with React, Redux, and React-Router!", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=4.2.0", 8 | "npm": "^3.0.0" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "compile": "better-npm-run compile", 13 | "lint": "eslint .", 14 | "lint:fix": "npm run lint -- --fix", 15 | "start": "better-npm-run start", 16 | "dev": "better-npm-run dev", 17 | "dev:no-debug": "npm run dev -- --no_debug", 18 | "test": "better-npm-run test", 19 | "test:dev": "npm run test -- --watch", 20 | "deploy": "better-npm-run deploy", 21 | "deploy:dev": "better-npm-run deploy:dev", 22 | "deploy:prod": "better-npm-run deploy:prod", 23 | "codecov": "cat coverage/*/lcov.info | codecov" 24 | }, 25 | "betterScripts": { 26 | "compile": { 27 | "command": "babel-node bin/compile", 28 | "env": { 29 | "DEBUG": "app:*" 30 | } 31 | }, 32 | "dev": { 33 | "command": "nodemon --exec babel-node bin/server", 34 | "env": { 35 | "NODE_ENV": "development", 36 | "DEBUG": "app:*" 37 | } 38 | }, 39 | "deploy": { 40 | "command": "npm run clean && npm run compile", 41 | "env": { 42 | "DEBUG": "app:*" 43 | } 44 | }, 45 | "deploy:dev": { 46 | "command": "npm run deploy", 47 | "env": { 48 | "NODE_ENV": "development", 49 | "DEBUG": "app:*" 50 | } 51 | }, 52 | "deploy:prod": { 53 | "command": "npm run deploy", 54 | "env": { 55 | "NODE_ENV": "production", 56 | "DEBUG": "app:*" 57 | } 58 | }, 59 | "start": { 60 | "command": "babel-node bin/server", 61 | "env": { 62 | "DEBUG": "app:*" 63 | } 64 | }, 65 | "test": { 66 | "command": "babel-node ./node_modules/karma/bin/karma start build/karma.conf", 67 | "env": { 68 | "NODE_ENV": "test", 69 | "DEBUG": "app:*" 70 | } 71 | } 72 | }, 73 | "repository": { 74 | "type": "git", 75 | "url": "git+https://github.com/davezuko/react-redux-starter-kit.git" 76 | }, 77 | "author": "David Zukowski (http://zuko.me)", 78 | "license": "MIT", 79 | "dependencies": { 80 | "babel-cli": "^6.5.1", 81 | "babel-core": "^6.3.17", 82 | "babel-loader": "^6.2.0", 83 | "babel-plugin-transform-runtime": "^6.3.13", 84 | "babel-preset-es2015": "^6.3.13", 85 | "babel-preset-react": "^6.3.13", 86 | "babel-preset-react-optimize": "^1.0.1", 87 | "babel-preset-stage-0": "^6.3.13", 88 | "babel-register": "^6.3.13", 89 | "babel-runtime": "^6.3.19", 90 | "better-npm-run": "0.0.8", 91 | "css-loader": "^0.23.0", 92 | "cssnano": "^3.3.2", 93 | "debug": "^2.2.0", 94 | "extract-text-webpack-plugin": "^1.0.0", 95 | "file-loader": "^0.8.4", 96 | "fs-extra": "^0.30.0", 97 | "history": "^2.0.0", 98 | "html-webpack-plugin": "^2.7.1", 99 | "immutable": "^3.8.1", 100 | "imports-loader": "^0.6.5", 101 | "ip": "^1.1.2", 102 | "json-loader": "^0.5.4", 103 | "koa": "^2.0.0-alpha.3", 104 | "koa-connect-history-api-fallback": "^0.3.0", 105 | "koa-convert": "^1.2.0", 106 | "koa-proxy": "^0.6.0", 107 | "koa-static": "^2.0.0", 108 | "node-sass": "^3.7.0", 109 | "normalize.css": "^4.1.1", 110 | "postcss-loader": "^0.9.0", 111 | "react": "^15.1.0", 112 | "react-dom": "^15.1.0", 113 | "react-redux": "^4.0.0", 114 | "react-router": "^2.2.0", 115 | "react-router-redux": "^4.0.0", 116 | "redux": "^3.0.0", 117 | "redux-actions": "0.9.1", 118 | "redux-saga": "~0.10.4", 119 | "redux-thunk": "^2.0.0", 120 | "rimraf": "^2.5.1", 121 | "sass-loader": "^3.0.0", 122 | "style-loader": "^0.13.0", 123 | "url-loader": "^0.5.6", 124 | "webpack": "^1.12.14", 125 | "yargs": "^4.0.0" 126 | }, 127 | "devDependencies": { 128 | "babel-eslint": "^6.0.0-beta.6", 129 | "babel-polyfill": "^6.9.0", 130 | "chai": "^3.4.1", 131 | "chai-as-promised": "^5.1.0", 132 | "chai-enzyme": "^0.4.0", 133 | "chai-immutable": "^1.5.4", 134 | "cheerio": "^0.20.0", 135 | "codecov": "^1.0.1", 136 | "enzyme": "^2.3.0", 137 | "eslint": "^2.4.0", 138 | "eslint-config-standard": "^5.1.0", 139 | "eslint-config-standard-react": "^2.2.0", 140 | "eslint-plugin-babel": "^3.0.0", 141 | "eslint-plugin-promise": "^1.0.8", 142 | "eslint-plugin-react": "^5.0.0", 143 | "eslint-plugin-standard": "^1.3.1", 144 | "isparta-loader": "^2.0.0", 145 | "karma": "^0.13.21", 146 | "karma-coverage": "^1.0.0", 147 | "karma-mocha": "^1.0.1", 148 | "karma-mocha-reporter": "^2.0.0", 149 | "karma-phantomjs-launcher": "^1.0.0", 150 | "karma-webpack-with-fast-source-maps": "^1.9.2", 151 | "mocha": "^2.2.5", 152 | "nodemon": "^1.8.1", 153 | "open": "0.0.5", 154 | "phantomjs-prebuilt": "^2.1.3", 155 | "react-addons-test-utils": "^15.1.0", 156 | "redbox-react": "^1.2.2", 157 | "sinon": "^1.17.3", 158 | "sinon-chai": "^2.8.0", 159 | "webpack-dev-middleware": "^1.6.1", 160 | "webpack-hot-middleware": "^2.6.0" 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Redux Starter Kit 2 | 3 | [![Join the chat at https://gitter.im/davezuko/react-redux-starter-kit](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/davezuko/react-redux-starter-kit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://travis-ci.org/davezuko/react-redux-starter-kit.svg?branch=master)](https://travis-ci.org/davezuko/react-redux-starter-kit?branch=master) 5 | [![dependencies](https://david-dm.org/davezuko/react-redux-starter-kit.svg)](https://david-dm.org/davezuko/react-redux-starter-kit) 6 | [![devDependency Status](https://david-dm.org/davezuko/react-redux-starter-kit/dev-status.svg)](https://david-dm.org/davezuko/react-redux-starter-kit#info=devDependencies) 7 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 8 | 9 | This repository is based on https://github.com/davezuko/react-redux-starter-kit. The difference is its structure is modular which is inspired by the post http://marmelab.com/blog/2015/12/17/react-directory-structure.html of François Zaninotto. 10 | 11 | Also, there is a example for calling github API to fetching user by using redux-saga which is also inspired by https://github.com/marmelab/javascript-boilerplate. 12 | 13 | === 14 | 15 | This starter kit is designed to get you up and running with a bunch of awesome new front-end technologies, all on top of a configurable, feature-rich webpack build system that's already setup to provide hot reloading, CSS modules with Sass support, unit testing, code coverage reports, bundle splitting, and a whole lot more. 16 | 17 | The primary goal of this project is to remain as **unopinionated** as possible. Its purpose is not to dictate your project structure or to demonstrate a complete sample application, but to provide a set of tools intended to make front-end development robust, easy, and, most importantly, fun. Check out the full feature list below! 18 | 19 | Finally, This project wouldn't be possible without the help of our many contributors, so [thank you](#thank-you) for all of your help. 20 | 21 | ## Table of Contents 22 | 1. [Features](#features) 23 | 1. [Requirements](#requirements) 24 | 1. [Getting Started](#getting-started) 25 | 1. [Application Structure](#application-structure) 26 | 1. [Development](#development) 27 | 1. [Developer Tools](#developer-tools) 28 | 1. [Routing](#routing) 29 | 1. [Testing](#testing) 30 | 1. [Deployment](#deployment) 31 | 1. [Build System](#build-system) 32 | 1. [Configuration](#configuration) 33 | 1. [Root Resolve](#root-resolve) 34 | 1. [Globals](#globals) 35 | 1. [Styles](#styles) 36 | 1. [Server](#server) 37 | 1. [Production Optimization](#production-optimization) 38 | 1. [Learning Resources](#learning-resources) 39 | 1. [FAQ](#troubleshooting) 40 | 1. [Thank You](#thank-you) 41 | 42 | ## Features 43 | * [react](https://github.com/facebook/react) 44 | * [redux](https://github.com/rackt/redux) 45 | * [react-router](https://github.com/rackt/react-router) 46 | * [react-router-redux](https://github.com/rackt/react-router-redux) 47 | * [webpack](https://github.com/webpack/webpack) 48 | * [babel](https://github.com/babel/babel) 49 | * [koa](https://github.com/koajs/koa) 50 | * [karma](https://github.com/karma-runner/karma) 51 | * [eslint](http://eslint.org) 52 | 53 | ## Requirements 54 | * node `^4.2.0` 55 | * npm `^3.0.0` 56 | 57 | ## Getting Started 58 | 59 | After confirming that your development environment meets the specified [requirements](#requirements), you can follow these steps to get the project up and running: 60 | 61 | ```bash 62 | $ git clone https://github.com/koshuang/react-redux-starter-kit.git 63 | $ cd react-redux-starter-kit 64 | $ npm install # Install project dependencies 65 | $ npm start # Compile and launch 66 | ``` 67 | 68 | If everything works, you should see the following: 69 | 70 | 71 | 72 | While developing, you will probably rely mostly on `npm start`; however, there are additional scripts at your disposal: 73 | 74 | |`npm run