Comparing version
@@ -1,5 +0,16 @@ | ||
## 1.1.0 | ||
## 2.0.0 | ||
- ActionProvider now accepts (and defaults to) an option `passDispatchProp: | ||
Boolean` that will provide the decorated component with `dispatch` as a prop | ||
like ActionEmitters would. | ||
- ActionProvider now provides the decorated component with `dispatch` as a prop | ||
like ActionEmitters would | ||
- **BREAKING** ActionProvider now requires every action handler to yield a | ||
Promise value. As a result, the option `serviceWrapper` has been removed. | ||
- **BREAKING** ActionProvider option `reducer` has been renamed to `reduce` | ||
- **BREAKING** ActionProvider option `verbose` has been removed | ||
- **BREAKING** Action payload can now have an arbitrary arity (up from 1) | ||
- **BREAKING** Action aliasing has been removed, as a result the `ActionProxy` | ||
symbol is no longer exported | ||
- ActionEmitter will now throw an error if initialized with missing `actions` | ||
argument or one that is not an array | ||
- ActionEmitter's warning for unknown actions is removed in the production | ||
build now | ||
- No longer using React.createClass, React 15.4.1+ is now required |
@@ -10,6 +10,31 @@ const path = require('path'); | ||
'karma-webpack', | ||
'karma-coverage', | ||
], | ||
browsers: [ 'Chrome' ], | ||
browsers: [ 'ChromeWithoutSandbox' ], | ||
customLaunchers: { | ||
ChromeWithoutSandbox: { | ||
base: 'ChromeHeadless', | ||
flags: [ | ||
'--no-default-browser-check', | ||
'--no-first-run', | ||
'--disable-default-apps', | ||
'--disable-popup-blocking', | ||
'--disable-translate', | ||
'--disable-web-security', | ||
] | ||
} | ||
}, | ||
coverageReporter: { | ||
dir: path.join(root, 'coverage'), | ||
subdir: '.', | ||
reporters: [ | ||
{ type: 'json', file: 'report.json' }, | ||
{ type: 'html' }, | ||
{ type: 'text-summary' } | ||
] | ||
}, | ||
frameworks: [ | ||
@@ -27,12 +52,23 @@ 'mocha' | ||
reporters: [ 'dots' ], | ||
reporters: [ 'dots', 'coverage' ], | ||
webpack: { | ||
externals: { 'Promise': true }, | ||
mode: 'development', | ||
module: { | ||
loaders: [ | ||
rules: [ | ||
{ | ||
test: /\.js$/, | ||
include: path.join(root, 'src'), | ||
loader: 'babel' | ||
loader: 'babel-loader', | ||
options: { | ||
babelrc: false, | ||
presets: [ 'env', 'react' ], | ||
plugins: [ | ||
['istanbul', { | ||
exclude: [ | ||
"**/__tests__" | ||
] | ||
}] | ||
] | ||
} | ||
} | ||
@@ -54,5 +90,7 @@ ] | ||
webpackServer: { | ||
noInfo: true | ||
noInfo: true, | ||
quiet: true, | ||
stats: false | ||
} | ||
}); | ||
}; | ||
}; |
{ | ||
"name": "cornflux", | ||
"version": "1.1.0", | ||
"version": "2.0.0-beta.1", | ||
"description": "A dispatching library for React applications promoting data encapsulation.", | ||
"main": "src/index.js", | ||
"main": "dist/cornflux.js", | ||
"module": "src/index.js", | ||
"scripts": { | ||
"build": "babel src -d lib", | ||
"build": "NODE_ENV=production webpack", | ||
"lint": "eslint src", | ||
"prepublish": "npm run lint && npm run test -- --single-run && npm run build", | ||
"test": "karma start" | ||
@@ -29,11 +32,15 @@ }, | ||
"babel-cli": "6.18.0", | ||
"babel-core": "6.21.0", | ||
"babel-loader": "6.2.10", | ||
"babel-preset-es2015": "6.18.0", | ||
"babel-preset-react": "6.16.0", | ||
"babel-core": "6.26.3", | ||
"babel-loader": "7.1.4", | ||
"babel-plugin-istanbul": "4.1.6", | ||
"babel-preset-env": "1.7.0", | ||
"babel-preset-react": "6.24.1", | ||
"chai": "3.5.0", | ||
"karma": "1.3.0", | ||
"karma-chrome-launcher": "2.0.0", | ||
"eslint": "5.0.1", | ||
"eslint-plugin-react": "7.10.0", | ||
"karma": "2.0.4", | ||
"karma-chrome-launcher": "2.2.0", | ||
"karma-coverage": "1.1.2", | ||
"karma-mocha": "1.3.0", | ||
"karma-webpack": "1.8.1", | ||
"karma-webpack": "3.0.0", | ||
"mocha": "3.2.0", | ||
@@ -44,6 +51,9 @@ "react": "15.4.1", | ||
"react-schema": "2.0.0", | ||
"webpack": "1.14.0" | ||
"sinon": "6.0.1", | ||
"webpack": "4.14.0", | ||
"webpack-bundle-analyzer": "2.13.1", | ||
"webpack-cli": "3.0.8" | ||
}, | ||
"peerDependencies": { | ||
"react": "0.14 || 15", | ||
"react": ">= 15.4.1", | ||
"react-dom": "0.14 || 15", | ||
@@ -53,4 +63,5 @@ "react-schema": "2" | ||
"dependencies": { | ||
"invariant": "2.2.4", | ||
"warning": "3.0.0" | ||
} | ||
} |
@@ -1,11 +0,11 @@ | ||
# cornflux (beta) | ||
# cornflux | ||
A library for dispatching events in a React application using the react [context](https://facebook.github.io/react/docs/context.html) as a data bus. | ||
## Motivation | ||
The goal of this library is to achieve a layer of _data isolation_ where | ||
application state is confined to "data components" that can only be interfaced | ||
with by other components via action functions. | ||
The TL;DR version: data isolation. Protect your application data and state | ||
storage in components that can only be interfaced with via _actions_. | ||
The longer version can be found in [this post on medium](https://medium.com/@amireh/on-privacy-with-react-context-aa77ffd08509#.qz4awmpol) | ||
The longer version can be found in [this post on | ||
medium](https://medium.com/@amireh/on-privacy-with-react-context-aa77ffd08509#.qz4awmpol) | ||
if you have the patience. | ||
@@ -17,6 +17,11 @@ | ||
# make sure you have the dependencies first: | ||
npm install --save react react-dom react-schema | ||
npm install --save react react-dom | ||
npm install --save cornflux | ||
``` | ||
The source code is not transpiled; you will need a transpiler like | ||
[Babel.js](http://babeljs.io) to consume it. However, a built-version is | ||
provided under `dist/` but it expects `React` and `ReactDOM` to be available on | ||
`window`. | ||
## Usage | ||
@@ -32,5 +37,4 @@ | ||
Construct an "acting" version of the component that should be performing | ||
side-effects with the `ActionProvider` decorator and define the action | ||
handlers: | ||
Use the `ActionProvider` decorator to construct a version of the component that | ||
is able to receive action requests and carry them out. | ||
@@ -64,5 +68,5 @@ ```javascript | ||
Construct an "emitting" version of the component that needs to request an | ||
action be performed with the `ActionEmitter` decorator. You must explicitly | ||
specify the list of action names that it will trigger. | ||
Use the `ActionEmitter` decorator to construct a version of a component that | ||
needs to dispatch action requests. You must explicitly specify which actions | ||
the component is allowed to dispatch. | ||
@@ -114,58 +118,32 @@ ```javascript | ||
state: Object, | ||
payload: Object, | ||
Object.<{ | ||
payload: ...Any, | ||
delegate: { | ||
dispatch: (String, Object) -> Any, | ||
propagate: () -> Any | ||
}> | ||
) -> Any | ||
} | ||
) -> Promise | ||
``` | ||
The first argument, `state`, is either the reduced state of the container | ||
if a `reducer` was defined, otherwise it's the component instance itself. | ||
The first argument, `state`, is either the reduced state of the container if | ||
`reduce` was defined, otherwise it's the component instance itself. | ||
The `payload` argument is the action payload that was provided when the action | ||
was emitted. | ||
was emitted. It is "spread out" to as many arguments the dispatch call was | ||
provided by an emitter (vararg). | ||
The third argument is an object containing two functions: | ||
The last argument is an object containing two functions: | ||
- `dispatch` which lets your handler dispatch other events. The signature is | ||
is similar to emitter's `dispatch`. | ||
- `propagate` is a callback for yielding (or "bubbling") the action to a | ||
provider higher in the chain. Note that it accepts NO parameters, the action | ||
is yielded as-is. | ||
- `dispatch` to dispatch other events to the same provider. The signature is is | ||
similar to emitter's `dispatch`. | ||
- `propagate` to yield (or "bubble") the action to a provider higher in the | ||
tree. Note that it accepts NO parameters; the action is yielded as-is. | ||
#### ?displayName: `String` | ||
#### ?reduce: `(component) -> Object` | ||
A custom display name for the decorated component. | ||
Given the rendered component instance, generate the state to pass to action | ||
handlers. | ||
Defaults to: `ActionProvider($ORIGINAL_COMPONENT_DISPLAY_NAME)` | ||
Defaults to: the identity function where the component instance itself is | ||
passed through (which exposes `props`, `state`, `setState`, etc.) | ||
#### ?reducer: `(component) -> Object` | ||
A funcion for "reducing" the component instance into some state that the action | ||
handlers need. | ||
This option gives you a greater degree of what the action handlers may end up | ||
touching, if you're really paranoid. | ||
Defaults to: `(x) -> x`. The identity function where the component instance | ||
itself is passed through. | ||
#### ?serviceWrapper: `(Any) -> Any` | ||
Compatibility option for applications that use promises or relied on earlier | ||
dispatchers always generating promises (or something alike.) | ||
The function will receive the return value of the action handler and can | ||
augment it in any way it sees fit. | ||
Defaults to: `(x) -> x` | ||
#### ?verbose: `Boolean` | ||
Turn this on if you want diagnostic messages be output to the console for | ||
debugging. | ||
Defaults to: `false` | ||
### ActionEmitter: `(Component, options: Object) -> Component` | ||
@@ -172,0 +150,0 @@ |
import React, { PropTypes } from 'react'; | ||
import warning from 'warning'; | ||
import invariant from 'invariant'; | ||
const ActionEmitter = (Component, { actions, propName = 'dispatch' }) => React.createClass({ | ||
displayName: `ActionEmitter(${Component.displayName})`, | ||
const ActionEmitter = (Component, { actions, propName = 'dispatch' }) => { | ||
invariant(Array.isArray(actions), | ||
`Missing required argument "actions" by ActionEmitter for component "${Component.displayName}}".` | ||
) | ||
contextTypes: { | ||
availableActions: PropTypes.arrayOf(PropTypes.string), | ||
dispatch: PropTypes.func.isRequired, | ||
}, | ||
class WithActions extends React.Component { | ||
componentWillMount() { | ||
// istanbul ignore else | ||
if (process.env.NODE_ENV !== 'production') { | ||
const availableActions = this.context.availableActions; | ||
const unknownActions = actions.filter(action => { | ||
return availableActions.indexOf(action) === -1; | ||
}); | ||
componentWillMount() { | ||
const availableActions = this.context.availableActions || []; | ||
const unknownActions = actions.filter(action => { | ||
return availableActions.indexOf(action) === -1; | ||
}); | ||
warning(unknownActions.length === 0, | ||
`The component "${Component.displayName}" may emit actions that are not ` + | ||
`known to be supported by any provider:\n` + | ||
stringifyList(unknownActions, " ") | ||
); | ||
} | ||
} | ||
warning(unknownActions.length === 0, | ||
`The component "${Component.displayName}" may emit actions that are not ` + | ||
`known to be supported by any provider.\n` + | ||
`These actions are:\n${stringifyList(unknownActions, " ")}.` | ||
); | ||
}, | ||
render() { | ||
return ( | ||
<Component | ||
{...this.props} | ||
{...{ [propName]: this.context.dispatch }} | ||
/> | ||
) | ||
} | ||
}; | ||
render() { | ||
const dispatchingProps = {}; | ||
WithActions.displayName = `ActionEmitter(${Component.displayName})`; | ||
WithActions.contextTypes = { | ||
availableActions: PropTypes.arrayOf(PropTypes.string).isRequired, | ||
dispatch: PropTypes.func.isRequired, | ||
}; | ||
dispatchingProps[propName] = this.dispatch; | ||
return WithActions; | ||
} | ||
return <Component {...this.props} {...dispatchingProps} /> | ||
}, | ||
dispatch(type, payload) { | ||
return this.context.dispatch(type, payload); | ||
} | ||
}); | ||
function stringifyList(list, indent = "") { | ||
@@ -39,0 +47,0 @@ return list.map(x => `${indent}- ${x}`).join('\n') |
import React, { PropTypes } from 'react'; | ||
import Promise from 'Promise'; | ||
import ActionProxy from './ActionProxy'; | ||
import invariant from 'invariant'; | ||
const Identity = x => x; | ||
const createActionProxy = alias => new ActionProxy(alias); | ||
const ActionProvider = function(Component, { | ||
actions, | ||
displayName, | ||
reducer = Identity, | ||
serviceWrapper = Identity, | ||
verbose = false, | ||
passDispatchProp = true, | ||
}) { | ||
const debugLog = verbose ? console.debug.bind(console) : Function.prototype; | ||
return React.createClass({ | ||
displayName: displayName || `ActionProvider(${Component.displayName})`, | ||
contextTypes: { | ||
availableActions: PropTypes.arrayOf(PropTypes.string), | ||
dispatch: PropTypes.func, | ||
}, | ||
childContextTypes: { | ||
availableActions: PropTypes.arrayOf(PropTypes.string), | ||
dispatch: PropTypes.func, | ||
}, | ||
const ActionProvider = function(Component, { actions, reduce = Identity }) { | ||
const actionNames = Object.keys(actions); | ||
class WithActions extends React.Component { | ||
getChildContext() { | ||
return { | ||
availableActions: (this.context.availableActions || []).concat(Object.keys(actions)), | ||
dispatch: this.dispatchAction, | ||
availableActions: (this.context.availableActions || []).concat(actionNames), | ||
dispatch: this.bindings.dispatchAction, | ||
} | ||
}, | ||
} | ||
componentWillMount() { | ||
constructor() { | ||
super() | ||
this.unmounted = false; | ||
this.actionBuffer = []; | ||
}, | ||
this.bindings = { | ||
dispatchAction: this.dispatchAction.bind(this) | ||
}; | ||
} | ||
componentDidMount() { | ||
this.flushQueuedActions(); | ||
}, | ||
} | ||
componentDidUpdate() { | ||
this.flushQueuedActions(); | ||
}, | ||
componentWillUnmount() { | ||
this.bindings = null; | ||
this.unmounted = true; | ||
// TODO: possible leak here? what about pending promises? | ||
this.actionBuffer = null; | ||
}, | ||
} | ||
render() { | ||
const decoratorProps = {}; | ||
return ( | ||
<Component | ||
ref="component" | ||
dispatch={this.bindings.dispatchAction} | ||
{...this.props} | ||
/> | ||
) | ||
} | ||
if (passDispatchProp) { | ||
decoratorProps.dispatch = this.dispatchAction; | ||
} | ||
return <Component ref="container" {...decoratorProps} {...this.props} /> | ||
}, | ||
dispatchAction(type, payload) { | ||
dispatchAction(type, ...payload) { | ||
// Not an action we provide? Propagate | ||
if (!actions.hasOwnProperty(type)) { | ||
return this.propagateAction(type, payload); | ||
return this.propagateAction(type, ...payload); | ||
} | ||
// An alias? resolve it and re-dispatch | ||
else if (actions[type] instanceof ActionProxy) { | ||
return this.dispatchAction(actions[type].type, payload); | ||
} | ||
// Not ready yet? Queue it until we are. This happens if a child emitter | ||
@@ -78,3 +55,3 @@ // dispatches an action during the componentWillMount or componentDidMount | ||
// ready at that point yet. | ||
else if (!this.refs.container) { | ||
else if (!this.refs.component) { | ||
return this.dispatchActionWhenReady(type, payload); | ||
@@ -84,22 +61,24 @@ } | ||
const actionHandler = actions[type]; | ||
const state = reducer(this.refs.container); | ||
const state = reduce(this.refs.component); | ||
debugLog(`Dispatching action "${type}":`, payload); | ||
// TODO: validate payload | ||
return serviceWrapper( | ||
actionHandler(state, payload, { | ||
propagate: this.propagateAction.bind(null, type, payload), | ||
dispatch: this.dispatchAction, | ||
}) | ||
); | ||
const returnValue = actionHandler(state, ...payload, { | ||
propagate: this.propagateAction.bind(this, type, ...payload), | ||
dispatch: this.bindings.dispatchAction, | ||
}) | ||
invariant(returnValue && typeof returnValue.then === 'function', | ||
`Handler for action "${type}" yielded a non-Promise value` | ||
) | ||
return returnValue; | ||
} | ||
}, | ||
} | ||
dispatchActionWhenReady(type, payload) { | ||
debugLog(`Deferring action "${type}" until container is ready...`); | ||
return new Promise((resolve, reject) => { | ||
if (!this.isMounted()) { | ||
// possible race condition; child component dispatches an action while | ||
// the provider is being unmounted | ||
if (this.unmounted) { | ||
reject(); | ||
@@ -111,7 +90,7 @@ } | ||
}); | ||
}, | ||
} | ||
propagateAction(type, payload) { | ||
propagateAction(type, ...payload) { | ||
if (this.context.dispatch) { | ||
return this.context.dispatch(type, payload); | ||
return this.context.dispatch(type, ...payload); | ||
} | ||
@@ -121,17 +100,24 @@ else { | ||
} | ||
}, | ||
} | ||
flushQueuedActions() { | ||
this.actionBuffer.splice(0).forEach(({ type, payload, promise }) => { | ||
debugLog(`Dispatching deferred action "${type}".`); | ||
this.dispatchAction(type, payload).then(promise.resolve, promise.reject); | ||
this.dispatchAction(type, ...payload).then(promise.resolve, promise.reject); | ||
}); | ||
} | ||
}) | ||
}; | ||
WithActions.displayName = `ActionProvider(${Component.displayName})`; | ||
WithActions.contextTypes = { | ||
availableActions: PropTypes.arrayOf(PropTypes.string), | ||
dispatch: PropTypes.func, | ||
}; | ||
WithActions.childContextTypes = { | ||
availableActions: PropTypes.arrayOf(PropTypes.string), | ||
dispatch: PropTypes.func, | ||
}; | ||
return WithActions; | ||
} | ||
ActionProvider.ActionProxy = createActionProxy; | ||
export default ActionProvider; | ||
export { createActionProxy as ActionProxy }; |
@@ -1,10 +0,7 @@ | ||
import ActionProvider, { ActionProxy } from './ActionProvider'; | ||
import ActionProvider from './ActionProvider'; | ||
import ActionEmitter from './ActionEmitter'; | ||
import { PropTypes as Types } from 'react-schema'; | ||
export { | ||
ActionProvider, | ||
ActionProxy, | ||
ActionEmitter, | ||
Types, | ||
}; |
const webpack = require('webpack'); | ||
const common = require('./common'); | ||
const path = require('path'); | ||
const root = path.resolve(__dirname); | ||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; | ||
module.exports = { | ||
devtool: 'eval', | ||
mode: 'production', | ||
resolve: common.resolve, | ||
output: { | ||
filename: 'cornflux.js', | ||
path: path.join(root, 'dist'), | ||
}, | ||
externals: { | ||
'react': 'React', | ||
'react-dom': 'ReactDOM', | ||
}, | ||
module: { | ||
rules: [ | ||
{ | ||
test: /\.js$/, | ||
include: [ path.join(root, 'src') ], | ||
exclude: [ /\.test.js$/ ], | ||
loader: 'babel-loader', | ||
options: { | ||
presets: [ 'env', 'react' ] | ||
} | ||
} | ||
] | ||
}, | ||
plugins: [ | ||
new webpack.DefinePlugin({ | ||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), | ||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), | ||
}), | ||
// new BundleAnalyzerPlugin(), | ||
], | ||
module: common.getModuleConfigWithCustomLoaders(x => { | ||
if (x.id === 'js') { | ||
return Object.assign({}, x, { | ||
loaders: undefined, | ||
loader: 'babel', | ||
}); | ||
} | ||
else { | ||
return x; | ||
} | ||
}), | ||
}; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
624720
12.26%26
23.81%764
34.98%5
25%23
43.75%1
Infinity%162
-11.96%2
100%1
Infinity%+ Added
+ Added
+ Added