diff --git a/.babelrc b/.babelrc index 7ea24c1..741aa4d 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,8 @@ { "ignore": [], + "plugins": [ + "dev-expression" + ], "env": { "test": { "presets": [ diff --git a/README.md b/README.md index bf5fdd8..83abfec 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,16 @@ Helpers for scaling and abstracting redux by co-locating actions, reducers and selectors. -[![Build Status](https://travis-ci.org/thomasdashney/redux-modular.svg?branch=master)](https://travis-ci.org/thomasdashney/redux-modular) [![Test Coverage](https://codeclimate.com/github/thomasdashney/redux-modular/badges/coverage.svg)](https://codeclimate.com/github/thomasdashney/redux-modular/coverage) [![Code Climate](https://codeclimate.com/github/thomasdashney/redux-modular/badges/gpa.svg)](https://codeclimate.com/github/thomasdashney/redux-modular) +[![Build Status](https://travis-ci.org/thomasdashney/redux-modular.svg?branch=master)](https://travis-ci.org/thomasdashney/redux-modular) [![Test Coverage](https://co`dec`limate.com/github/thomasdashney/redux-modular/badges/coverage.svg)](https://codeclimate.com/github/thomasdashney/redux-modular/coverage) [![Code Climate](https://codeclimate.com/github/thomasdashney/redux-modular/badges/gpa.svg)](https://codeclimate.com/github/thomasdashney/redux-modular) + +* [Install](#install) +* [Usage Guide](#usage-guide) + * [Defining actions](#defining-actions) + * [Defining reducers](#defining-reducers) + * [Defining selectors](#defining-selectors) + * [Defining reusable redux logic](#defining-reusable-redux-logic) + * [Writing tests](#writing-tests) +* [API](#api) ## Install @@ -16,96 +25,299 @@ or $ yarn add redux-modular ``` -## Usage +## Usage Guide + +This guide uses a counter as an example, which starts at 0 and ends at 10. If you try to `increment` past the 10, it will stay at 10. + +Two selectors are provided: `value` which gets the current value of the counter, and `isComplete`, which return `true` if the counter has reached 10 (the maximum). Finally, there is a `reset` action for resetting back to the initial state of `0`. -

- -

+Here is how one might implement this using plain redux: ```js -import { combineReducers, createStore } from 'redux' -import { mount, createReducer } from 'redux-modular' - -/* Create an object containing the logic (actions, reducer, selectors) */ - -const counter = { - // mapping of action names to optional payload creators - actions: { - increment: null, - decrement: null, - set: (value) => ({ value }) - }, - - // function mapping actions to reducers - reducer: actions => createReducer(0, { - [actions.increment]: state => state + 1, - [actions.decrement]: state => state - 1, - [actions.set]: (state, payload) => payload.value - }), - - // function mapping local state selector to your selectors - selectors: localStateSelector => ({ - counterValue: state => localStateSelector(state) - }) +import { createStore, combineReducers } from 'redux' + +const INITIAL_STATE = 0 +const COUNTER_MAX = 10 + +const COUNTER_TYPES = { + INCREMENT: 'increment (counter)', + RESET: 'reset (counter)' } -/* Instantiate the counter logic by mounting to redux paths */ +const counterActions = { + increment: (amount = 1) => ({ type: COUNTER_TYPES.INCREMENT, payload: { amount } }), + reset: () => ({ type: COUNTER_TYPES.RESET }) +} + +const counterReducer = (state = INITIAL_STATE, action) => { + switch (action.type) { + case COUNTER_TYPES.INCREMENT: + return Math.min(state + action.payload.amount, COUNTER_MAX) + case COUNTER_TYPES.RESET: + return INITIAL_STATE + default: + return state + } +} -const counter1 = mount('counter1', counter) -const counter2 = mount('counter2', counter) -const counter3 = mount(['nested', 'counter3'], counter) +const counterSelectors = { + value: state => state.counter, + isComplete: state => state.counter === COUNTER_MAX +} -/* Add the reducers to your root reducer */ +// then the reducer would be mounted to the store at `state.counter`: -const rootReducer = combineReducers({ - counter1: counter1.reducer, - counter2: counter2.reducer, - nested: combineReducers({ - counter3: counter3.reducer +const store = createStore( + combineReducers({ + counter: counterReducer }) +) + +counterSelectors.value(store.getState()) // 0 +counterSelectors.isComplete(store.getState()) // true + +store.dispatch(counterActions.increment(10)) +counterSelectors.value(store.getState()) // 10 +counterSelectors.isComplete(store.getState()) // true + +store.dispatch(counterActions.reset()) +counterSelectors.value(store.getState()) // 0 +counterSelectors.isComplete(store.getState()) // false +``` + +Each section in this guide shows how each `redux-modular` helper function can be used to reduce code repetition and boilerplate. + +### Defining actions + +Using `createAction`, we can easily define `FSA-Compliant` action creator: + +```js +import { createAction } from 'redux-modular' + +const counterActions = { + increment: createAction(COUNTER_TYPES.INCREMENT, (amount = 1) => ({ amount })), + reset: createAction(COUNTER_TYPES.RESET) +} +``` + +`createAction` also does some extra work for us. If you call `toString()` on any of these action creators, it will return the action's type. This allows us to remove our `COUNTER_TYPES` constant declaration: + +```js +import { createAction } from 'redux-modular' + +const counterActions = { + increment: createAction('increment (counter)', (amount = 1) => ({ amount })), + reset: createAction('reset (counter)') +} + +counterActions.increment.toString() // 'increment (counter)' +``` + +Next, we can remove the repetition of namespacing our actions with the `(counter)` suffix. The `mountAction` helper will modify an action creator's action type using a given namespace: + +```js +import { createAction, mountAction } from 'redux-modular' + +const namespace = 'counter' + +const counterActions = { + increment: mountAction(namespace, createAction('increment', (amount = 1) => ({ amount }))) + reset: mountAction(createAction('reset') +}) + +counterActions.increment.toString() // 'increment (counter)' +``` + +Finally, we can use the `createActions` and `mountActions` helpers to reduce all of the above boilerplate: + +```js +import { createActions, mountActions } from 'redux-modular' + +const counterActions = mountActions('counter', createActions({ + increment: (amount = 1) => ({ amount }), + reset +})) +``` + +### Defining reducers + +`createReducer` creates a reducer which switches based on action type, and passes the action `payload` directly to your sub-reducer function: + +```js +import { createReducer } from 'redux-modular' + +const INITIAL_STATE = 0 +const COUNTER_MAX = 10 + +const counterReducer = createReducer(INITIAL_STATE, { + [counterActions.increment]: (state, payload) => Math.min(state + payload.amount, COUNTER_MAX), + [counterActions.reset]: () => INITIAL_STATE }) +``` + +Note that, because we passed the action creators directly as the object keys, the `toString()` function will be called on them automatically. + +### Defining selectors -const store = createStore(rootReducer) +`mountSelector` will wrap a selector with a selector which first selects the state at a provided path: -/* Use actions and selectors for each counter instance in your app */ +```js +const isCompleteSelector = mountSelector('counter', counterState => counterState === COUNTER_MAX) + +isCompleteSelector({ counter: 5 }) // 5 +``` -const { actions, selectors } = counter1 +`mountSelectors` allows you to mount multiple selectors at once: -console.log(selectors.counterValue(store.getState())) // prints `0` +```js +const counterSelectors = mountSelectors('counter', { + value: counterState => counterState, + isComplete: counterState => counterState === COUNTER_MAX +}) +``` -store.dispatch(actions.increment()) -console.log(selectors.counterValue(store.getState())) // prints `1` +If our logic lives multiple levels deep in the redux state tree, you can use [lodash.get](https://lodash.com/docs/4.17.10#get) syntax to perform a deep select: -store.dispatch(actions.decrement()) -console.log(selectors.counterValue(store.getState())) // prints `0` +```js +const counterSelectors = mountSelectors('path.counter', { + value: counterState => counterState, + isComplete: counterState => counterState === COUNTER_MAX +}) -store.dispatch(actions.set(5)) -console.log(selectors.counterValue(store.getState())) // prints `5` +counterSelectors.value({ nested: { counter: 5 } }) // 5 ``` -## Writing Tests +### Defining reusable redux logic -If you `mount` your logic to a path of `null`, you can test your state logic without any assumption of where it sits in your redux state. +Putting the above examples together, we have reduced much boilerplate & repetition: ```js -/* eslint-env jest */ +import { createActions, mountActions, createReducer, mountSelectors } from 'redux-modular' -const counter = require('./counter') +const REDUX_PATH = 'counter' +const INITIAL_STATE = 0 +const COUNTER_MAX = 10 -const { actions, reducer, selectors } = mount(null, counter) +const counterActions = mountActions(REDUX_PATH, createActions({ + increment: (amount = 1) => ({ amount }), +})) -it('can increment', () => { - const state = reducer(0, actions.increment()) - expect(selectors.counterValue(state)).toEqual(1) +const counterReducer = createReducer(INITIAL_STATE, { + [counterActions.increment]: (state, payload) => Math.min(state + payload.amount, COUNTER_MAX), + [counterActions.reset]: () => INITIAL_STATE }) -it('can decrement', () => { - const state = reducer(0, actions.decrement()) - expect(selectors.counterValue(state)).toEqual(-1) +const counterSelector = mountSelectors(REDUX_PATH, { + value: counterState => counterState, + isComplete: counterState => counterState === COUNTER_MAX }) +``` + +However, what if we wanted to reuse this logic in multiple places in the redux state tree? We can easily wrap these definitions in a factory function, with the path and counter max as parameters: + +```js +// create-counter-logic.js +import { createActions, mountActions } from 'redux-modular' + +const INITIAL_STATE = 0 + +export default function createCounterLogic (path, counterMax) { + const actions = mountActions(path, createActions({ + increment: (amount = 1) => ({ amount }), + })) + + const reducer = createReducer(INITIAL_STATE, { + [counterActions.increment]: (state, payload) => Math.min(state + payload.amount, counterMax), + [counterActions.reset]: () => INITIAL_STATE + }) + + const selectors = mountSelectors(path, { + value: counterState => counterState, + isComplete: counterState => counterState === counterMax + }) + + return { + actions, + reducer, + selectors + } +} +``` -it('can be set to a number', () => { - const state = reducer(0, actions.set(5)) - expect(selectors.counterValue(state)).toEqual(5) +Now, we can quickly and easily instantiate our logic and mount it multiple places in our redux state tree: + +```js +import { createStore,combineReducers } from 'redux' +import createCounterLogic from './create-counter-logic.js' + +const counterTo5 = createCounterLogic('counterTo5', 0, 5) +const counterTo10 = createCounterLogic('counterTo10', 0, 10) + +const store = createStore( + combineReducers({ + counterTo5: counterTo5.reducer + nested: { + counterTo10: counterTo10.reducer + } + }) +) + +store.dispatch(counterTo5.actions.increment(5)) +counterTo5.selectors.value(store.getState()) // 5 +counterTo5.selectors.isComplete(store.getState()) // true + +counterTo10.selectors.isComplete(store.getState()) // false +store.dispatch(counterTo5.actions.increment(10)) +counterTo10.selectors.value(store.getState()) // 10 +counterTo10.selectors.isComplete(store.getState()) // true +``` + +### Writing Tests + +An easy, minimal way to test your logic is by running `actions` through the `reducer`, and making assertions about the return value of `selectors`. Here is an example using our +"counter" logic: + +```js +import createCounterLogic from './create-counter-logic' + +const COUNTER_MAX = 5 + +const { + counterActions, + counterSelectors, + counterReducer +} = createCounterLogic(null, COUNTER_MAX) + +test('counter logic', () => { + let state = counterReducer(undefined, { type: '@@INIT' }) + expect(counterSelectors.value(state)).toEqual(0) + expect(counterSelectors.isComplete(state)).toEqual(false) + + state = counterReducer(state, counterActions.increment()) + expect(counterSelectors.value(state)).toEqual(1) + expect(counterSelectors.isComplete(state)).toEqual(false) + + state = counterReducer(state, counterActions.increment(4)) + expect(counterSelectors.value(state)).toEqual(5) + expect(counterSelectors.isComplete(state)).toEqual(true) + + state = counterReducer(state, counterActions.increment()) + expect(counterSelectors.value(state)).toEqual(5) // shouldn't be able to increment past 5 }) ``` + +## API + +`createAction(String type, [Function payloadCreator]) : ActionCreator` + +`createActions(Object payloadCreatorMap) : Object` + +`mountAction(String|Array path, ActionCreator actionCreator) : ActionCreator` + +`mountActions(Object actionCreatorMap) : Object` + +`createReducer(Any initialState, Object) : Function reducer` + +`mountSelector(String|Array path, Function selector) : Function selector` + +`mountSelectors(String|Array path, Object selectorMap) : Object` diff --git a/package-lock.json b/package-lock.json index fb05d37..ac6c188 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "redux-modular", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -57,7 +57,7 @@ "ansi-escapes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", - "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", + "integrity": "sha1-7D6LTp+AZPwCw6ybZfHCdb2o75I=", "dev": true }, "ansi-regex": { @@ -78,7 +78,7 @@ "anymatch": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "integrity": "sha1-VT3Lj5HjyImEXf26NMd3IbkLnXo=", "dev": true, "requires": { "micromatch": "2.3.11", @@ -115,7 +115,7 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", "dev": true }, "array-equal": { @@ -151,13 +151,13 @@ "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "integrity": "sha1-bIw/uCfdQ+45GPJ7gngqt2WKb9k=", "dev": true }, "async": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", - "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=", "dev": true, "requires": { "lodash": "4.17.4" @@ -411,7 +411,7 @@ "babel-jest": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-21.2.0.tgz", - "integrity": "sha512-O0W2qLoWu1QOoOGgxiR2JID4O6WSpxPiQanrkyi9SSlM0PJ60Ptzlck47lhtnr9YZO3zYOsxHwnyeWJ6AffoBQ==", + "integrity": "sha1-LOBZUZqTdKLEbyRVtvvvWtddhj4=", "dev": true, "requires": { "babel-plugin-istanbul": "4.1.5", @@ -436,6 +436,11 @@ "babel-runtime": "6.26.0" } }, + "babel-plugin-dev-expression": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/babel-plugin-dev-expression/-/babel-plugin-dev-expression-0.2.1.tgz", + "integrity": "sha1-1Ke+7++7UOPyc0mQqCokhs+eue4=" + }, "babel-plugin-istanbul": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz", @@ -450,7 +455,7 @@ "babel-plugin-jest-hoist": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.2.0.tgz", - "integrity": "sha512-yi5QuiVyyvhBUDLP4ButAnhYzkdrUwWDtvUJv71hjH3fclhnZg4HkDeqaitcR2dZZx/E67kGkRcPVjtVu+SJfQ==", + "integrity": "sha1-LO9jclm9S2KKbKzgOd5fzRTbsAY=", "dev": true }, "babel-plugin-syntax-async-functions": { @@ -793,7 +798,7 @@ "babel-preset-jest": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-21.2.0.tgz", - "integrity": "sha512-hm9cBnr2h3J7yXoTtAVV0zg+3vg0Q/gT2GYuzlreTU0EPkJRtlNgKJJ3tBKEn0+VjAi3JykV6xCJkuUYttEEfA==", + "integrity": "sha1-/50rzgir2Y6KNtmopRibkXO4Vjg=", "dev": true, "requires": { "babel-plugin-jest-hoist": "21.2.0", @@ -870,7 +875,7 @@ "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=", "dev": true }, "balanced-match": { @@ -1003,7 +1008,7 @@ "ci-info": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.1.tgz", - "integrity": "sha512-vHDDF/bP9RYpTWtUhpJRhCFdvvp3iDWvEbuDbWgvjUrNGV1MXJrE0MPcwGtEled04m61iwdBLUIHZtDgzWS4ZQ==", + "integrity": "sha1-R7RN8RjEjSWXtW00Ln4leRBgFxo=", "dev": true }, "cliui": { @@ -1116,7 +1121,7 @@ "boom": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", "dev": true, "requires": { "hoek": "4.2.0" @@ -1151,7 +1156,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", "dev": true, "requires": { "ms": "2.0.0" @@ -1242,7 +1247,7 @@ "escodegen": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.0.tgz", - "integrity": "sha512-v0MYvNQ32bzwoG2OSFzWAkuahDQHK92JBN0pTAALJ4RIxEZe766QJPDR8Hqy7XNUy5K3fnVL76OqYAdc4TZEIw==", + "integrity": "sha1-mBGi8mXcHNOJRCDuNxcGS2MriFI=", "dev": true, "requires": { "esprima": "3.1.3", @@ -1263,7 +1268,7 @@ "esprima": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=", "dev": true }, "estraverse": { @@ -1287,7 +1292,7 @@ "exec-sh": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", - "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", + "integrity": "sha1-FjuYpuiea2W0fCoo0hW8H2OYnDg=", "dev": true, "requires": { "merge": "1.2.0" @@ -1329,7 +1334,7 @@ "expect": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/expect/-/expect-21.2.1.tgz", - "integrity": "sha512-orfQQqFRTX0jH7znRIGi8ZMR8kTNpXklTTz8+HGTpmTKZo3Occ6JNB5FXMb8cRuiiC/GyDqsr30zUa66ACYlYw==", + "integrity": "sha1-ADrCrHAFw8Kec7OKJy1K+t1tHXs=", "dev": true, "requires": { "ansi-styles": "3.2.0", @@ -1461,7 +1466,7 @@ "fsevents": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", - "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "integrity": "sha1-MoK3E/s62A7eDp/PRhG1qm/AM/Q=", "dev": true, "optional": true, "requires": { @@ -2381,7 +2386,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "dev": true, "requires": { "fs.realpath": "1.0.0", @@ -2414,7 +2419,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", "dev": true }, "graceful-fs": { @@ -2492,7 +2497,7 @@ "hawk": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", "dev": true, "requires": { "boom": "4.3.1", @@ -2504,7 +2509,7 @@ "hoek": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==", + "integrity": "sha1-ctnQdU9/4lyi0BrY+PmpRJqJUm0=", "dev": true }, "home-or-tmp": { @@ -2520,7 +2525,7 @@ "hosted-git-info": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=", "dev": true }, "html-encoding-sniffer": { @@ -2758,7 +2763,7 @@ "istanbul-lib-coverage": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz", - "integrity": "sha512-0+1vDkmzxqJIn5rcoEqapSB4DmPxE31EtI2dF2aCkV5esN9EWHxZ0dwgDClivMXJqE7zaYQxq30hj5L0nlTN5Q==", + "integrity": "sha1-c7+5mIhSmUFck9OKPprfeEp3qdo=", "dev": true }, "istanbul-lib-hook": { @@ -2839,7 +2844,7 @@ "jest": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest/-/jest-21.2.1.tgz", - "integrity": "sha512-mXN0ppPvWYoIcC+R+ctKxAJ28xkt/Z5Js875padm4GbgUn6baeR5N4Ng6LjatIRpUQDZVJABT7Y4gucFjPryfw==", + "integrity": "sha1-yWTgtHODdooUOOPM88PUcDJ2BOE=", "dev": true, "requires": { "jest-cli": "21.2.1" @@ -2848,7 +2853,7 @@ "jest-cli": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-21.2.1.tgz", - "integrity": "sha512-T1BzrbFxDIW/LLYQqVfo94y/hhaj1NzVQkZgBumAC+sxbjMROI7VkihOdxNR758iYbQykL2ZOWUBurFgkQrzdg==", + "integrity": "sha1-nFKLZinWUZERONIovbAzwVfsjAA=", "dev": true, "requires": { "ansi-escapes": "3.0.0", @@ -2887,7 +2892,7 @@ "jest-changed-files": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-21.2.0.tgz", - "integrity": "sha512-+lCNP1IZLwN1NOIvBcV5zEL6GENK6TXrDj4UxWIeLvIsIDa+gf6J7hkqsW2qVVt/wvH65rVvcPwqXdps5eclTQ==", + "integrity": "sha1-Xb7srUL12ItIIzSQLOHLptl5jSk=", "dev": true, "requires": { "throat": "4.1.0" @@ -2896,7 +2901,7 @@ "jest-config": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-21.2.1.tgz", - "integrity": "sha512-fJru5HtlD/5l2o25eY9xT0doK3t2dlglrqoGpbktduyoI0T5CwuB++2YfoNZCrgZipTwPuAGonYv0q7+8yDc/A==", + "integrity": "sha1-x1hseerQvMHzjEAeVflk8TvypIA=", "dev": true, "requires": { "chalk": "2.1.0", @@ -2915,7 +2920,7 @@ "jest-diff": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-21.2.1.tgz", - "integrity": "sha512-E5fu6r7PvvPr5qAWE1RaUwIh/k6Zx/3OOkZ4rk5dBJkEWRrUuSgbMt2EO8IUTPTd6DOqU3LW6uTIwX5FRvXoFA==", + "integrity": "sha1-RszLbKstAs6YvDFAEXZLuVsGW08=", "dev": true, "requires": { "chalk": "2.1.0", @@ -2927,13 +2932,13 @@ "jest-docblock": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", - "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", + "integrity": "sha1-UVKcOzDV/RWdpgwnzu3Blfr41BQ=", "dev": true }, "jest-environment-jsdom": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-21.2.1.tgz", - "integrity": "sha512-mecaeNh0eWmzNrUNMWARysc0E9R96UPBamNiOCYL28k7mksb1d0q6DD38WKP7ABffjnXyUWJPVaWRgUOivwXwg==", + "integrity": "sha1-ONmYDIJZsqYI7CMt7uYommDZ1bQ=", "dev": true, "requires": { "jest-mock": "21.2.0", @@ -2944,7 +2949,7 @@ "jest-environment-node": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-21.2.1.tgz", - "integrity": "sha512-R211867wx9mVBVHzrjGRGTy5cd05K7eqzQl/WyZixR/VkJ4FayS8qkKXZyYnwZi6Rxo6WEV81cDbiUx/GfuLNw==", + "integrity": "sha1-mMZ99WY8f74g9ueSrCJyx0DTuMg=", "dev": true, "requires": { "jest-mock": "21.2.0", @@ -2954,13 +2959,13 @@ "jest-get-type": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-21.2.0.tgz", - "integrity": "sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q==", + "integrity": "sha1-9jdqudtLYNgeOfMHScbEZvQNSiM=", "dev": true }, "jest-haste-map": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-21.2.0.tgz", - "integrity": "sha512-5LhsY/loPH7wwOFRMs+PT4aIAORJ2qwgbpMFlbWbxfN0bk3ZCwxJ530vrbSiTstMkYLao6JwBkLhCJ5XbY7ZHw==", + "integrity": "sha1-E2PwqLtDOPJPABgGVx7/eksv89g=", "dev": true, "requires": { "fb-watchman": "2.0.0", @@ -2974,7 +2979,7 @@ "jest-jasmine2": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-21.2.1.tgz", - "integrity": "sha512-lw8FXXIEekD+jYNlStfgNsUHpfMWhWWCgHV7n0B7mA/vendH7vBFs8xybjQsDzJSduptBZJHqQX9SMssya9+3A==", + "integrity": "sha1-nMb8EIrM+pfv684QxDCFSKTqdZI=", "dev": true, "requires": { "chalk": "2.1.0", @@ -2990,7 +2995,7 @@ "jest-matcher-utils": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz", - "integrity": "sha512-kn56My+sekD43dwQPrXBl9Zn9tAqwoy25xxe7/iY4u+mG8P3ALj5IK7MLHZ4Mi3xW7uWVCjGY8cm4PqgbsqMCg==", + "integrity": "sha1-csgm6rpBoJOsK0Vl+GXrhHXeD2Q=", "dev": true, "requires": { "chalk": "2.1.0", @@ -3001,7 +3006,7 @@ "jest-message-util": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-21.2.1.tgz", - "integrity": "sha512-EbC1X2n0t9IdeMECJn2BOg7buOGivCvVNjqKMXTzQOu7uIfLml+keUfCALDh8o4rbtndIeyGU8/BKfoTr/LVDQ==", + "integrity": "sha1-v+XUaSyEyCfR3PQYI3lVWPChrL4=", "dev": true, "requires": { "chalk": "2.1.0", @@ -3012,19 +3017,19 @@ "jest-mock": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-21.2.0.tgz", - "integrity": "sha512-aZDfyVf0LEoABWiY6N0d+O963dUQSyUa4qgzurHR3TBDPen0YxKCJ6l2i7lQGh1tVdsuvdrCZ4qPj+A7PievCw==", + "integrity": "sha1-frB3DnMXloFl9h6ipygRMVNLPA8=", "dev": true }, "jest-regex-util": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-21.2.0.tgz", - "integrity": "sha512-BKQ1F83EQy0d9Jen/mcVX7D+lUt2tthhK/2gDWRgLDJRNOdRgSp1iVqFxP8EN1ARuypvDflRfPzYT8fQnoBQFQ==", + "integrity": "sha1-Gx4z5jFDurw+Dy5sm1uh6zSy1TA=", "dev": true }, "jest-resolve": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-21.2.0.tgz", - "integrity": "sha512-vefQ/Lr+VdNvHUZFQXWtOqHX3HEdOc2MtSahBO89qXywEbUxGPB9ZLP9+BHinkxb60UT2Q/tTDOS6rYc6Mwigw==", + "integrity": "sha1-BokTrSumogIY5f0yRx84dABd46Y=", "dev": true, "requires": { "browser-resolve": "1.11.2", @@ -3035,7 +3040,7 @@ "jest-resolve-dependencies": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-21.2.0.tgz", - "integrity": "sha512-ok8ybRFU5ScaAcfufIQrCbdNJSRZ85mkxJ1EhUp8Bhav1W1/jv/rl1Q6QoVQHObNxmKnbHVKrfLZbCbOsXQ+bQ==", + "integrity": "sha1-niMeNx4ac2oa1OS5qEO8cr/gPQk=", "dev": true, "requires": { "jest-regex-util": "21.2.0" @@ -3044,7 +3049,7 @@ "jest-runner": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-21.2.1.tgz", - "integrity": "sha512-Anb72BOQlHqF/zETqZ2K20dbYsnqW/nZO7jV8BYENl+3c44JhMrA8zd1lt52+N7ErnsQMd2HHKiVwN9GYSXmrg==", + "integrity": "sha1-GUcy4+UYv7PXy/wP1YcSRsfhpGc=", "dev": true, "requires": { "jest-config": "21.2.1", @@ -3062,7 +3067,7 @@ "jest-runtime": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-21.2.1.tgz", - "integrity": "sha512-6omlpA3+NSE+rHwD0PQjNEjZeb2z+oRmuehMfM1tWQVum+E0WV3pFt26Am0DUfQkkPyTABvxITRjCUclYgSOsA==", + "integrity": "sha1-mdzhUwnGcEQu7i6+H/U6PL27tz4=", "dev": true, "requires": { "babel-core": "6.26.0", @@ -3095,7 +3100,7 @@ "jest-snapshot": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-21.2.1.tgz", - "integrity": "sha512-bpaeBnDpdqaRTzN8tWg0DqOTo2DvD3StOemxn67CUd1p1Po+BUpvePAp44jdJ7Pxcjfg+42o4NHw1SxdCA2rvg==", + "integrity": "sha1-KeSfFiAkFuRzQ+dX5e/5SMB/17A=", "dev": true, "requires": { "chalk": "2.1.0", @@ -3109,7 +3114,7 @@ "jest-util": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-21.2.1.tgz", - "integrity": "sha512-r20W91rmHY3fnCoO7aOAlyfC51x2yeV3xF+prGsJAUsYhKeV670ZB8NO88Lwm7ASu8SdH0S+U+eFf498kjhA4g==", + "integrity": "sha1-onSy9yawiXSU1pSmw9amGrgZu3g=", "dev": true, "requires": { "callsites": "2.0.0", @@ -3124,7 +3129,7 @@ "jest-validate": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-21.2.1.tgz", - "integrity": "sha512-k4HLI1rZQjlU+EC682RlQ6oZvLrE5SCh3brseQc24vbZTxzT/k/3urar5QMCVgjadmSO7lECeGdc6YxnM3yEGg==", + "integrity": "sha1-zAy8plPNVJN7pPKhEXlndFMN08c=", "dev": true, "requires": { "chalk": "2.1.0", @@ -3136,13 +3141,12 @@ "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, "js-yaml": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "integrity": "sha1-LnhEFka9RoLpY/IrbpKCPDCcYtw=", "dev": true, "requires": { "argparse": "1.0.9", @@ -3339,7 +3343,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "dev": true, "requires": { "js-tokens": "3.0.2" } @@ -3347,7 +3350,7 @@ "lru-cache": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=", "dev": true, "requires": { "pseudomap": "1.0.2", @@ -3423,7 +3426,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "dev": true, "requires": { "brace-expansion": "1.1.8" @@ -3484,7 +3487,7 @@ "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", "dev": true, "requires": { "hosted-git-info": "2.5.0", @@ -3595,7 +3598,7 @@ "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", "dev": true, "requires": { "execa": "0.7.0", @@ -3612,7 +3615,7 @@ "p-cancelable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", - "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==", + "integrity": "sha1-ueEjgAvOu3rBOkeb4ZW1B7mNMPo=", "dev": true }, "p-finally": { @@ -3748,7 +3751,7 @@ "pretty-format": { "version": "21.2.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-21.2.1.tgz", - "integrity": "sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A==", + "integrity": "sha1-rlQH888hBmzQEaobpfzntqLt2zY=", "dev": true, "requires": { "ansi-regex": "3.0.0", @@ -3790,13 +3793,13 @@ "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=", "dev": true }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", "dev": true, "requires": { "is-number": "3.0.0", @@ -3879,7 +3882,7 @@ "redux": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "integrity": "sha1-BrcxIyFZAdJdBlvjQusCa8HIU3s=", "dev": true, "requires": { "lodash": "4.17.4", @@ -3891,19 +3894,19 @@ "regenerate": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", - "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==", + "integrity": "sha1-DDNtOYBVPXVcObWGrjsgqknIK38=", "dev": true }, "regenerator-runtime": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", - "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==", + "integrity": "sha1-flT+W1zNXWYk6mJVw0c74JC4AuE=", "dev": true }, "regenerator-transform": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "integrity": "sha1-HkmWg3Ix2ot/PPQRTXG1aRoGgN0=", "dev": true, "requires": { "babel-runtime": "6.26.0", @@ -3914,7 +3917,7 @@ "regex-cache": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=", "dev": true, "requires": { "is-equal-shallow": "0.1.3" @@ -3984,7 +3987,7 @@ "request": { "version": "2.83.0", "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", "dev": true, "requires": { "aws-sign2": "0.7.0", @@ -4042,7 +4045,7 @@ "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", "dev": true, "requires": { "glob": "7.1.2" @@ -4051,13 +4054,13 @@ "rollup": { "version": "0.50.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.50.0.tgz", - "integrity": "sha512-7RqCBQ9iwsOBPkjYgoIaeUij606mSkDMExP0NT7QDI3bqkHYQHrQ83uoNIXwPcQm/vP2VbsUz3kiyZZ1qPlLTQ==", + "integrity": "sha1-TBWPTngObLM/8Nv8GEpSzFjNXzs=", "dev": true }, "rollup-plugin-babel": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-3.0.2.tgz", - "integrity": "sha512-ALGPBFtwJZcYHsNPM6RGJlEncTzAARPvZOGjNPZgDe5hS5t6sJGjiOWibEFVEz5LQN7S7spvCBILaS4N1Cql2w==", + "integrity": "sha1-onZd6g6qiuzjUcmDVzMA0XSXSVs=", "dev": true, "requires": { "rollup-pluginutils": "1.5.2" @@ -4076,13 +4079,13 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=", "dev": true }, "sane": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/sane/-/sane-2.2.0.tgz", - "integrity": "sha512-OSJxhHO0CgPUw3lUm3GhfREAfza45smvEI9ozuFrxKG10GHVo0ryW9FK5VYlLvxj0SV7HVKHW0voYJIRu27GWg==", + "integrity": "sha1-1tLi/KsA49KDyTuRK3w6IIRvHVY=", "dev": true, "requires": { "anymatch": "1.3.2", @@ -4106,13 +4109,13 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=", "dev": true }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=", "dev": true }, "set-blocking": { @@ -4139,7 +4142,7 @@ "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "integrity": "sha1-1rkYHBpI05cyTISHHvvPxz/AZUs=", "dev": true }, "signal-exit": { @@ -4172,7 +4175,7 @@ "source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=", "dev": true, "requires": { "source-map": "0.5.7" @@ -4234,7 +4237,7 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "dev": true, "requires": { "is-fullwidth-code-point": "2.0.0", @@ -4311,7 +4314,7 @@ "test-exclude": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.1.1.tgz", - "integrity": "sha512-35+Asrsk3XHJDBgf/VRFexPgh3UyETv8IAn/LRTiZjVy6rjPVqdEk8dJcJYBzl1w0XCJM48lvTy8SfEsCWS4nA==", + "integrity": "sha1-TYSWSwlmsAh+zDNKLOAC09k0HiY=", "dev": true, "requires": { "arrify": "1.0.1", @@ -4422,7 +4425,7 @@ "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=", "dev": true }, "validate-npm-package-license": { @@ -4455,6 +4458,14 @@ "makeerror": "1.0.11" } }, + "warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.1.tgz", + "integrity": "sha512-rAVtTNZw+cQPjvGp1ox0XC5Q2IBFyqoqh+QII4J/oguyu83Bax1apbo2eqB8bHRS+fqYUBagys6lqUoVwKSmXQ==", + "requires": { + "loose-envify": "1.3.1" + } + }, "watch": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", @@ -4476,7 +4487,7 @@ "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "integrity": "sha1-qFWYCx8LazWbodXZ+zmulB+qY60=", "dev": true }, "whatwg-encoding": { @@ -4509,7 +4520,7 @@ "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", "dev": true, "requires": { "isexe": "2.0.0" @@ -4537,7 +4548,7 @@ "worker-farm": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.5.0.tgz", - "integrity": "sha512-DHRiUggxtbruaTwnLDm2/BRDKZIoOYvrgYUj5Bam4fU6Gtvc0FaEyoswFPBjMXAweGW2H4BDNIpy//1yXXuaqQ==", + "integrity": "sha1-rf3wzUBYFGXtCh9kj5c1cir9XI0=", "dev": true, "requires": { "errno": "0.1.4", @@ -4585,7 +4596,7 @@ "write-file-atomic": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "integrity": "sha1-H/YVdcLipOjlENb6TiQ8zhg5mas=", "dev": true, "requires": { "graceful-fs": "4.1.11", diff --git a/package.json b/package.json index 6ede855..c343043 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ }, "homepage": "https://github.com/thomasdashney/redux-modular#readme", "dependencies": { - "lodash.get": "^4.4.2" + "babel-plugin-dev-expression": "^0.2.1", + "lodash.get": "^4.4.2", + "warning": "^4.0.1" }, "devDependencies": { "babel-jest": "^21.2.0", diff --git a/rollup.config.js b/rollup.config.js index ca7daef..ce46866 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -12,5 +12,5 @@ export default { exclude: 'node_modules/**' // only transpile our source code }) ], - external: ['lodash.get'] + external: ['lodash.get', 'warning'] } diff --git a/src/create-action.js b/src/create-action.js index 39ef6cd..5fcb6b9 100644 --- a/src/create-action.js +++ b/src/create-action.js @@ -15,6 +15,7 @@ export default function createAction (type, payloadCreator) { } actionCreator.toString = () => type + actionCreator.payloadCreator = payloadCreator return actionCreator } diff --git a/src/create-actions.js b/src/create-actions.js new file mode 100644 index 0000000..625cf84 --- /dev/null +++ b/src/create-actions.js @@ -0,0 +1,8 @@ +import createAction from './create-action' +import mapValues from './util/map-values' + +export default function createActions (payloadCreators) { + return mapValues(payloadCreators, (payloadCreator, key) => { + return createAction(key, payloadCreator) + }) +} diff --git a/src/globalize-actions.js b/src/globalize-actions.js deleted file mode 100644 index 3da3836..0000000 --- a/src/globalize-actions.js +++ /dev/null @@ -1,24 +0,0 @@ -import createAction from './create-action' - -const isArray = value => { - return typeof value === 'object' && - value !== null && - value.constructor === Array -} -const isString = value => typeof value === 'string' - -export default function globalizeActions (pathToState, actions) { - if (isArray(pathToState)) { - pathToState = pathToState.join('.') - } else if (pathToState !== null && !isString(pathToState)) { - throw new Error('path must be a string or array') - } - - return Object.keys(actions).reduce((prev, key) => { - const type = pathToState ? `${key} (${pathToState})` : key - - return Object.assign({}, prev, { - [key]: createAction(type, actions[key]) - }) - }, {}) -} diff --git a/src/index.js b/src/index.js index a2a084e..f09bef0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,10 @@ -export { default as mount } from './mount' -export { default as createReducer } from './create-reducer' export { default as createAction } from './create-action' +export { default as createActions } from './create-actions' +export { default as mountAction } from './mount-action' +export { default as mountActions } from './mount-actions' +export { default as createReducer } from './create-reducer' +export { default as mountSelector } from './mount-selector' +export { default as mountSelectors } from './mount-selectors' + +// deprecated +export { default as mount } from './mount' diff --git a/src/mount-action.js b/src/mount-action.js new file mode 100644 index 0000000..2388898 --- /dev/null +++ b/src/mount-action.js @@ -0,0 +1,19 @@ +import createAction from './create-action' +import { validatePath } from './util/arg-validators' +import { isArray } from './util/type-utils' + +export default function mountAction (path, action) { + if (path === null) { + return action + } + + if (!validatePath(path)) { + throw new Error('path must be a string or array of strings') + } + + if (isArray(path)) { + path = path.join('.') + } + + return createAction(`${action.toString()} (${path})`, action.payloadCreator) +} diff --git a/src/mount-actions.js b/src/mount-actions.js new file mode 100644 index 0000000..b8bab35 --- /dev/null +++ b/src/mount-actions.js @@ -0,0 +1,6 @@ +import mountAction from './mount-action' +import mapValues from './util/map-values' + +export default function mountActions (path, actions) { + return mapValues(actions, action => mountAction(path, action)) +} diff --git a/src/mount-selector.js b/src/mount-selector.js new file mode 100644 index 0000000..dfadfb3 --- /dev/null +++ b/src/mount-selector.js @@ -0,0 +1,15 @@ +import get from 'lodash.get' + +export default function mountSelector (path, selector) { + if (!path) return selector + + return state => { + const nestedState = get(state, path) + + if (nestedState === undefined) { + throw new Error(`nested state ${path} does not exist`) + } + + return selector(nestedState) + } +} diff --git a/src/mount-selectors.js b/src/mount-selectors.js new file mode 100644 index 0000000..d12d020 --- /dev/null +++ b/src/mount-selectors.js @@ -0,0 +1,6 @@ +import mountSelector from './mount-selector' +import mapValues from './util/map-values' + +export default function mountSelectors (path, selectors) { + return mapValues(selectors, (selector) => mountSelector(path, selector)) +} diff --git a/src/mount.js b/src/mount.js index 6d91c61..db1d97f 100644 --- a/src/mount.js +++ b/src/mount.js @@ -1,32 +1,46 @@ import get from 'lodash.get' -import globalizeActions from './globalize-actions' +import warning from 'warning' +import createActions from './create-actions' +import mountActions from './mount-actions' -export default function (pathToState, logic) { +export default function mount (pathToState, logic) { + warning(true, 'redux-modular mount() is deprecated') + validateArgs(...arguments) + return mountLogic(pathToState, logic) +} + +function validateArgs (pathToState, logic) { if (!logic) { throw new Error('logic must be passed to mount') } +} - let { actions, reducer, selectors } = logic +function mountLogic (pathToState, logic) { + const result = {} - if (actions) { - actions = globalizeActions(pathToState, actions) + if (logic.actions) { + result.actions = createAndMountActions(pathToState, logic.actions) } - if (actions && reducer) { - reducer = reducer(actions) + if (logic.actions && logic.reducer) { + result.reducer = logic.reducer(result.actions) } - if (selectors) { - const localStateSelector = pathToState + if (logic.selectors) { + result.selectors = createSelectors(pathToState, logic.selectors) + } + + return result +} + +function createAndMountActions (pathToState, actions) { + return mountActions(pathToState, createActions(actions)) +} + +function createSelectors (pathToState, selectors) { + const localStateSelector = pathToState ? state => get(state, pathToState) : state => state - selectors = selectors(localStateSelector) - } - - return { - actions, - reducer, - selectors - } + return selectors(localStateSelector) } diff --git a/src/util/arg-validators.js b/src/util/arg-validators.js new file mode 100644 index 0000000..9964ca7 --- /dev/null +++ b/src/util/arg-validators.js @@ -0,0 +1,5 @@ +import { isArray, isString } from './type-utils' + +export const validatePath = path => { + return isString(path) || (isArray(path) && path.every(isString)) +} diff --git a/src/util/map-values.js b/src/util/map-values.js new file mode 100644 index 0000000..aa6cbf9 --- /dev/null +++ b/src/util/map-values.js @@ -0,0 +1,7 @@ +export default function mapValues (object, map) { + return Object.keys(object).reduce((prev, key) => { + return Object.assign(prev, { + [key]: map(object[key], key) + }) + }, {}) +} diff --git a/src/util/type-utils.js b/src/util/type-utils.js new file mode 100644 index 0000000..2a82c2e --- /dev/null +++ b/src/util/type-utils.js @@ -0,0 +1,7 @@ +export const isArray = value => { + return typeof value === 'object' && + value !== null && + value.constructor === Array +} + +export const isString = value => typeof value === 'string' diff --git a/test/create-actions.test.js b/test/create-actions.test.js new file mode 100644 index 0000000..97ac295 --- /dev/null +++ b/test/create-actions.test.js @@ -0,0 +1,17 @@ +/* eslint-env jest */ + +import createActions from '../src/create-actions' + +it('creates an object of action creators given an object of payload creators', () => { + let actions = createActions({ + testAction: null + }) + + expect(actions).toHaveProperty('testAction') + expect(actions.testAction.toString()).toEqual('testAction') + + actions = createActions({ + testAction: value => value + }) + expect(actions.testAction('testValue').payload).toEqual('testValue') +}) diff --git a/test/globalize-actions.test.js b/test/globalize-actions.test.js deleted file mode 100644 index a15a7f7..0000000 --- a/test/globalize-actions.test.js +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-env jest */ - -import globalizeActions from '../src/globalize-actions' - -it('creates actions with types including the state path', () => { - let actions = globalizeActions('path.to.state', { - increment: null - }) - expect(actions).toHaveProperty('increment') - expect(actions.increment.toString()).toEqual('increment (path.to.state)') - - actions = globalizeActions(['path', 'to', 'state'], { - increment: null - }) - expect(actions.increment.toString()).toEqual('increment (path.to.state)') -}) - -it('creates actions with payload creators', () => { - const actions = globalizeActions('path.to.state', { - increment: (param1, param2) => ({ param1, param2 }) - }) - - const action = actions.increment('test1', 'test2') - expect(action).toHaveProperty('payload') - expect(action.payload).toEqual({ - param1: 'test1', - param2: 'test2' - }) -}) - -it('does not include the state path if pathToState is null', () => { - let actions = globalizeActions(null, { - increment: null - }) - expect(actions).toHaveProperty('increment') - expect(actions.increment.toString()).toEqual('increment') -}) - -it('throws an error if pathToState is invalid', () => { - const actions = { increment: () => null } - - expect(() => globalizeActions(5, actions)).toThrow() - expect(() => globalizeActions({}, actions)).toThrow() -}) diff --git a/test/index.test.js b/test/index.test.js index 3a960b4..f6c02a8 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -2,8 +2,13 @@ import * as reduxModular from '../src/index' -it('exports modularize and createReducer', () => { +test('exported functions', () => { expect(reduxModular).toHaveProperty('mount') - expect(reduxModular).toHaveProperty('createReducer') expect(reduxModular).toHaveProperty('createAction') + expect(reduxModular).toHaveProperty('createActions') + expect(reduxModular).toHaveProperty('mountAction') + expect(reduxModular).toHaveProperty('mountActions') + expect(reduxModular).toHaveProperty('createReducer') + expect(reduxModular).toHaveProperty('mountSelector') + expect(reduxModular).toHaveProperty('mountSelectors') }) diff --git a/test/mount-action.test.js b/test/mount-action.test.js new file mode 100644 index 0000000..77a77ce --- /dev/null +++ b/test/mount-action.test.js @@ -0,0 +1,26 @@ +/* eslint-env jest */ + +import createAction from '../src/create-action' +import mountAction from '../src/mount-action' + +const action = createAction('test', () => 'payloadValue') + +it('mounts an action to a given path', () => { + const mountedAction = mountAction('path', action) + expect(mountedAction.toString()).toEqual('test (path)') + expect(mountedAction().payload).toEqual('payloadValue') +}) + +it('can mount to an array of strings', () => { + const mountedAction = mountAction(['nested', 'path'], action) + expect(mountedAction.toString()).toEqual('test (nested.path)') +}) + +it('can mount to a path of null', () => { + const mountedAction = mountAction(null, action) + expect(mountedAction).toEqual(action) +}) + +it('throws an error if the path is invalid', () => { + expect(() => mountAction(5, action)).toThrow() +}) diff --git a/test/mount-actions.test.js b/test/mount-actions.test.js new file mode 100644 index 0000000..78cd09a --- /dev/null +++ b/test/mount-actions.test.js @@ -0,0 +1,21 @@ +/* eslint-env jest */ + +import createActions from '../src/create-actions' +import mountActions from '../src/mount-actions' + +const actions = createActions({ + action1: null, + action2: value => value +}) + +it('mounts an object of actions to a given path', () => { + const mountedActions = mountActions('path', actions) + expect(mountedActions).toHaveProperty('action1') + expect(mountedActions).toHaveProperty('action2') + expect(mountedActions.action1.toString()).toEqual('action1 (path)') + expect(mountedActions.action2.toString()).toEqual('action2 (path)') +}) + +it('throws an error if the path is invalid', () => { + expect(() => mountActions(5, actions)).toThrow() +}) diff --git a/test/mount-selector.test.js b/test/mount-selector.test.js new file mode 100644 index 0000000..90cf92b --- /dev/null +++ b/test/mount-selector.test.js @@ -0,0 +1,24 @@ +/* eslint-env jest */ + +import mountSelector from '../src/mount-selector' + +it('wraps a given selector to a given path', () => { + const selector1 = mountSelector('path', state => state.key) + expect(selector1({ path: { key: 'value' } })).toEqual('value') + + const selector2 = mountSelector('nested.path', state => state.key) + expect(selector2({ nested: { path: { key: 'value' } } })).toEqual('value') + + const selector3 = mountSelector(['nested', 'path'], state => state.key) + expect(selector3({ nested: { path: { key: 'value' } } })).toEqual('value') +}) + +it('return the original selector if no path is provided', () => { + const selector = mountSelector(null, state => state.key) + expect(selector({ key: 'value' })).toEqual('value') +}) + +it('throws an error if the path to state does not exist', () => { + const selector = mountSelector('path', state => state) + expect(() => selector({})).toThrow() +}) diff --git a/test/mount-selectors.test.js b/test/mount-selectors.test.js new file mode 100644 index 0000000..d023614 --- /dev/null +++ b/test/mount-selectors.test.js @@ -0,0 +1,14 @@ +/* eslint-env jest */ + +import mountSelectors from '../src/mount-selectors' + +const selectors = { + selector1: state => state.key1, + selector2: state => state.key2 +} + +it('mounts an object of selectors to a given path', () => { + const mountedSelectors = mountSelectors('path', selectors) + expect(mountedSelectors.selector1({ path: { key1: 'value' } })).toEqual('value') + expect(mountedSelectors.selector2({ path: { key2: 'value' } })).toEqual('value') +})