Skip to content

Commit

Permalink
feat(fullStack): add ability to get reducer/middleware creation stack…
Browse files Browse the repository at this point in the history
… traces in dev mode
  • Loading branch information
jedwards1211 committed Jan 15, 2017
1 parent c8d8a49 commit 1d8268f
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 14 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,54 @@ setConfigEntry('hello', 'world').meta // {key: 'hello', domain: 'config'}
forConfigDomain(setEntry('hello', 'world')).meta // {key: 'hello', domain: 'config'}
```

## fullStack(error: Error, wrapped?: (error: Error) => string): string

Errors thrown from the sub-reducers you pass to `createReducer`, `composeReducers`, 'prefixReducer', or sub-middleware
you pass to `createMiddleware` or `composeMiddleware` normally don't include any information about where the associated
call to `createReducer` etc. occurred, making debugging difficult. However, in dev mode, `mindfront-redux-utils` adds
this info to the resulting reducers and middleware, and you can get it by calling `fullStack`, like so:

```js
import {createReducer, fullStack} from './src'

function hello() {
throw new Error("TEST")
}
const r = createReducer({hello})

try {
r({}, {type: 'hello'})
} catch (e) {
console.error(fullStack(e))
}
```

Output:
```
Error: TEST
at hello (/Users/andy/redux-utils/temp.js:4:9)
at result (/Users/andy/redux-utils/src/createReducer.js:19:24)
at withCause (/Users/andy/redux-utils/src/addCreationStack.js:5:14)
at Object.<anonymous> (/Users/andy/redux-utils/temp.js:9:3)
at Module._compile (module.js:556:32)
at loader (/Users/andy/redux-utils/node_modules/babel-register/lib/node.js:144:5)
at Object.require.extensions.(anonymous function) [as .js] (/Users/andy/redux-utils/node_modules/babel-register/lib/node.js:154:7)
at Module.load (module.js:473:32)
at tryModuleLoad (module.js:432:12)
at Function.Module._load (module.js:424:3)
Caused by reducer created at:
at addCreationStack (/Users/andy/redux-utils/src/addCreationStack.js:2:21)
at createReducer (/Users/andy/redux-utils/src/createReducer.js:25:55)
at Object.<anonymous> (/Users/andy/redux-utils/temp.js:6:11)
at Module._compile (module.js:556:32)
at loader (/Users/andy/redux-utils/node_modules/babel-register/lib/node.js:144:5)
at Object.require.extensions.(anonymous function) [as .js] (/Users/andy/redux-utils/node_modules/babel-register/lib/node.js:154:7)
at Module.load (module.js:473:32)
at tryModuleLoad (module.js:432:12)
at Function.Module._load (module.js:424:3)
at Function.Module.runMain (module.js:590:10)
```

If you are using [VError](https://github.com/joyent/node-verror), you may pass VError's `fullStack` function as the
second argument to also include the cause chain from `VError`.

12 changes: 12 additions & 0 deletions src/addCreationStack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function addCreationStack(fn, what) {
const createdAt = new Error(what + ' created at:')
return function withCause(...args) {
try {
return fn(...args)
} catch (error) {
if (!error.creationStack) error.creationStack = () => createdAt.stack
throw error
}
}
}

12 changes: 9 additions & 3 deletions src/composeMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import forEach from 'lodash.foreach'
import mapValues from 'lodash.mapvalues'
import createMiddleware from './createMiddleware'
import checkForNonFunctions from './checkForNonFunctions'
import addCreationStack from './addCreationStack'

export default function composeMiddleware(...middlewares) {
if (process.env.NODE_ENV !== 'production') checkForNonFunctions(middlewares, 'middlewares')

if (middlewares.length === 0) return store => dispatch => dispatch
if (middlewares.length === 1) return middlewares[0]

if (process.env.NODE_ENV !== 'production') checkForNonFunctions(middlewares, 'middlewares')

if (every(middlewares, middleware => middleware.actionHandlers)) {
// regroup all the action handlers in the middlewares by action type.
let actionHandlers = {}
Expand All @@ -20,5 +21,10 @@ export default function composeMiddleware(...middlewares) {
})
return createMiddleware(mapValues(actionHandlers, typeHandlers => composeMiddleware(...typeHandlers)))
}
return store => next => middlewares.reduceRight((next, handler) => handler(store)(next), next)

return store => next => {
let handleAction = middlewares.reduceRight((next, handler) => handler(store)(next), next)
if (process.env.NODE_ENV !== 'production') handleAction = addCreationStack(handleAction, 'middleware')
return handleAction
}
}
15 changes: 10 additions & 5 deletions src/composeReducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import forEach from 'lodash.foreach'
import mapValues from 'lodash.mapvalues'
import createReducer from './createReducer'
import checkForNonFunctions from './checkForNonFunctions'
import addCreationStack from './addCreationStack'

export default function composeReducers(...reducers) {
if (reducers.length === 0) return state => state
if (reducers.length === 1) return reducers[0]

if (process.env.NODE_ENV !== 'production') checkForNonFunctions(reducers, 'reducers')

let result
if (reducers.length === 0) result = state => state
if (reducers.length === 1) result = reducers[0]

// if all reducers have actionHandlers maps, merge the maps using composeReducers
if (every(reducers, reducer => reducer.actionHandlers instanceof Object)) {
else if (every(reducers, reducer => reducer.actionHandlers instanceof Object)) {
let actionHandlers = {}
let initialState
reducers.forEach(reducer => {
Expand All @@ -23,7 +25,10 @@ export default function composeReducers(...reducers) {
})
return createReducer(initialState, mapValues(actionHandlers,
typeHandlers => composeReducers(...typeHandlers)))
} else {
result = (state, action) => reduce(reducers, (state, reducer) => reducer(state, action), state)
}
if (process.env.NODE_ENV !== 'production') result = addCreationStack(result, 'reducer')

return (state, action) => reduce(reducers, (state, reducer) => reducer(state, action), state)
return result
}
13 changes: 9 additions & 4 deletions src/createMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import checkForNonFunctions from './checkForNonFunctions'
import addCreationStack from './addCreationStack'

export default function createMiddleware(actionHandlers) {
if (process.env.NODE_ENV !== 'production') checkForNonFunctions(actionHandlers, 'actionHandlers')

const result = store => next => action => {
const handler = actionHandlers[action.type]
if (!handler) return next(action)
return handler(store)(next)(action)
let result = store => next => {
let handleAction = action => {
const handler = actionHandlers[action.type]
if (!handler) return next(action)
return handler(store)(next)(action)
}
if (process.env.NODE_ENV !== 'production') handleAction = addCreationStack(handleAction, 'middleware')
return handleAction
}
result.actionHandlers = actionHandlers
return result
Expand Down
6 changes: 5 additions & 1 deletion src/createReducer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import size from 'lodash.size'
import checkForNonFunctions from './checkForNonFunctions'
import addCreationStack from './addCreationStack'

export default function createReducer(initialState, actionHandlers) {
if (arguments.length === 1) {
actionHandlers = initialState
initialState = undefined
}

if (process.env.NODE_ENV !== 'production') checkForNonFunctions(actionHandlers, 'actionHandlers')
if (process.env.NODE_ENV !== 'production') {
checkForNonFunctions(actionHandlers, 'actionHandlers')
}

let result
if (size(actionHandlers)) {
Expand All @@ -19,6 +22,7 @@ export default function createReducer(initialState, actionHandlers) {
else {
result = (state = initialState) => state
}
if (process.env.NODE_ENV !== 'production') result = addCreationStack(result, 'reducer')
result.initialState = initialState
result.actionHandlers = actionHandlers
return result
Expand Down
6 changes: 6 additions & 0 deletions src/fullStack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function fullStack(error, wrapped = error => error.stack) {
let result = wrapped(error)
if (error.creationStack) result += '\nCaused by ' + error.creationStack().substring('Error: '.length)
return result
}

2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import createPluggableMiddleware from './createPluggableMiddleware'
import prefixReducer from './prefixReducer'
import prefixActionCreator from './prefixActionCreator'
import addMeta from './addMeta'
import fullStack from './fullStack'

export {
createReducer,
Expand All @@ -18,5 +19,6 @@ export {
prefixReducer,
prefixActionCreator,
addMeta,
fullStack,
}

6 changes: 5 additions & 1 deletion src/prefixReducer.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import mapKeys from 'lodash.mapkeys'
import createReducer from './createReducer'
import addCreationStack from './addCreationStack'

export default function prefixReducer(prefix) {
return reducer => {
if (reducer.actionHandlers instanceof Object) {
return createReducer(reducer.initialState, mapKeys(reducer.actionHandlers, (handler, key) => prefix + key))
}
return (state, action) => typeof action.type === 'string' && action.type.startsWith(prefix)
let result = (state, action) => typeof action.type === 'string' && action.type.startsWith(prefix)
? reducer(state, {...action, type: action.type.substring(prefix.length)})
: state

if (process.env.NODE_ENV !== 'production') result = addCreationStack(result, 'reducer')
return result
}
}

71 changes: 71 additions & 0 deletions test/creationStackTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {assert, expect} from 'chai'
import {createReducer, composeReducers, createMiddleware, composeMiddleware, prefixReducer, fullStack} from '../src'

describe('addCreationStack', () => {
let origNodeEnv
before(() => {
origNodeEnv = process.env.NODE_ENV
process.env.NODE_ENV = ''
})
after(() => process.env.NODE_ENV = origNodeEnv)

describe('createReducer', () => {
it('adds creation stack to errors', () => {
const r = createReducer({hello: () => { throw new Error('test') }})
try {
r({}, {type: 'hello'})
assert.fail('expected error to be thrown')
} catch (error) {
expect(fullStack(error)).to.match(/caused by reducer created at/i)
}
})
})
describe('composeReducers', () => {
it('adds creation stack to errors', () => {
const r = composeReducers(() => { throw new Error('test') })
try {
r({}, {type: 'hello'})
assert.fail('expected error to be thrown')
} catch (error) {
expect(fullStack(error)).to.match(/caused by reducer created at/i)
}
})
})
describe('createMiddleware', () => {
it('adds creation stack to errors', () => {
const r = createMiddleware({hello: store => next => action => { throw new Error('test') }})
try {
r(null)(null)({type: 'hello'})
assert.fail('expected error to be thrown')
} catch (error) {
expect(fullStack(error)).to.match(/caused by middleware created at/i)
}
})
})
describe('composeMiddleware', () => {
it('adds creation stack to errors', () => {
const r = composeMiddleware(
store => dispatch => dispatch,
store => next => action => { throw new Error('test') }
)
try {
r(null)(null)({type: 'hello'})
assert.fail('expected error to be thrown')
} catch (error) {
expect(fullStack(error)).to.match(/caused by middleware created at/i)
}
})
})
describe('prefixReducer', () => {
it('adds creation stack to errors', () => {
const r = prefixReducer('hello')(() => { throw new Error('test') })
try {
r({}, {type: 'hello'})
assert.fail('expected error to be thrown')
} catch (error) {
expect(fullStack(error)).to.match(/caused by reducer created at/i)
}
})
})
})

0 comments on commit 1d8268f

Please sign in to comment.