feature-redux
Version:
feature-u redux integration
594 lines (497 loc) • 23.8 kB
JavaScript
;
exports.__esModule = true;
exports.default = createReducerAspect;
exports.accumAppReducer = accumAppReducer;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _redux = require('redux');
var _reactRedux = require('react-redux');
var _featureU = require('feature-u');
var _slicedReducer = require('./slicedReducer');
var _slicedReducer2 = _interopRequireDefault(_slicedReducer);
var _verify = require('./util/verify');
var _verify2 = _interopRequireDefault(_verify);
var _lodash = require('lodash.isstring');
var _lodash2 = _interopRequireDefault(_lodash);
var _lodash3 = require('lodash.isfunction');
var _lodash4 = _interopRequireDefault(_lodash3);
var _lodash5 = require('lodash.isplainobject');
var _lodash6 = _interopRequireDefault(_lodash5);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } // peerDependency
// peerDependency
// peerDependency
// peerDependency:
// our logger (integrated/activated via feature-u)
var logf = _featureU.launchApp.diag.logf.newLogger('- ***feature-redux*** reducerAspect: ');
// NOTE: See README for complete description
function createReducerAspect() {
var namedParams = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
// ***
// *** validate parameters
// ***
var check = _verify2.default.prefix('createReducerAspect() parameter violation: ');
// ... namedParams
check((0, _lodash6.default)(namedParams), 'only named parameters may be supplied');
// descturcture our individual namedParams
// ... NOTE: We do this here (rather in the function signature) to have access
// to the overall namedParams variable - for validation purposes!
// Access via the JavaScript implicit `arguments[0]` variable is
// NOT reliable (in this context) exhibiting a number of quirks :-(
var _namedParams$name = namedParams.name,
name = _namedParams$name === undefined ? 'reducer' : _namedParams$name,
_namedParams$initialS = namedParams.initialState,
initialState = _namedParams$initialS === undefined ? undefined : _namedParams$initialS,
_namedParams$allowNoR = namedParams.allowNoReducers,
allowNoReducers = _namedParams$allowNoR === undefined ? false : _namedParams$allowNoR,
unknownNamedArgs = _objectWithoutProperties(namedParams, ['name', 'initialState', 'allowNoReducers']);
// ... name (NOTE: name check takes precedence to facilitate `Aspect.name` identity in subsequent errors :-)
check(name, 'name is required');
check((0, _lodash2.default)(name), 'name must be a string');
// ... unrecognized positional parameter
// NOTE: when defaulting entire struct, arguments.length is 0
check(arguments.length <= 1, 'name:' + name + ' ... unrecognized positional parameters (only named parameters can be specified) ... ' + arguments.length + ' positional parameters were found');
// ... unrecognized named parameter
var unknownArgKeys = Object.keys(unknownNamedArgs);
check(unknownArgKeys.length === 0, 'name:' + name + ' ... unrecognized named parameter(s): ' + unknownArgKeys);
// ... allowNoReducers
check(allowNoReducers === true || allowNoReducers === false || (0, _lodash4.default)(allowNoReducers), 'name:' + name + ' ... allowNoReducers must be a boolean OR an app-wide reducer function');
// ... initialState ... validated by redux directly (simply a pass through)
// ***
// *** Initialization: register feature-redux proprietary Aspect APIs
// ***
logf('createReducerAspect(): registering feature-redux proprietary Aspect APIs: getReduxStore(), getReduxMiddleware(), and getReduxEnhancer()');
(0, _featureU.extendAspectProperty)('getReduxStore', 'feature-redux'); // Aspect.getReduxStore(): store
(0, _featureU.extendAspectProperty)('getReduxMiddleware', 'feature-redux'); // Aspect.getReduxMiddleware(): reduxMiddleware
(0, _featureU.extendAspectProperty)('getReduxEnhancer', 'feature-redux'); // Aspect.getReduxEnhancer(): StoreEnhancer
// ***
// *** create/promote our new aspect
// ***
var reducerAspect = (0, _featureU.createAspect)({
name: name,
validateFeatureContent: validateFeatureContent,
expandFeatureContent: expandFeatureContent,
assembleFeatureContent: assembleFeatureContent,
assembleAspectResources: assembleAspectResources,
getReduxStore: getReduxStore,
injectRootAppElm: injectRootAppElm,
injectParamsInHooks: injectParamsInHooks,
config: {
allowNoReducers$: allowNoReducers, // PUBLIC: client override to: true || [{reducerFn}]
initialState$: initialState, // PUBLIC: client pass-through to redux
createReduxStore$: createReduxStore$, // HIDDEN: createReduxStore$(appReducer, middlewareArr, enhancerArr): appStore
reduxDevToolHook$: reduxDevToolHook$ // HIDDEN: reduxDevToolHook$(): {enhancer$, compose$}
}
});
return reducerAspect;
}
/**
* Validate self's aspect content on supplied feature.
*
* NOTE: To better understand the context in which any returned
* validation messages are used, **feature-u** will prefix them
* with: 'createFeature() parameter violation: '
*
* @param {Feature} feature - the feature to validate, which is known
* to contain this aspect.
*
* @return {string} an error message when the supplied feature
* contains invalid content for this aspect (null when valid).
*
* @private
*/
function validateFeatureContent(feature) {
var content = feature[this.name];
return (0, _lodash4.default)(content) ? content.slice ? null : this.name + ' (when supplied) must be embellished with slicedReducer(). SideBar: slicedReducer() should always wrap the the outer function passed to createFeature() (even when expandWithFassets() is used).' : this.name + ' (when supplied) must be a function';
}
/**
* Expand the reducer content in the supplied feature -AND- transfer
* the slice property from the expansion function to the expanded
* reducer.
*
* @param {Fassets} fassets the Fassets object used in
* cross-feature-communication.
*
* @param {Feature} feature - the feature which is known to contain
* this aspect **and** is in need of expansion (as defined by
* expandWithFassets()).
*
* @return {string} an optional error message when the supplied
* feature contains invalid content for this aspect (falsy when
* valid). This is a specialized validation of the expansion
* function, over-and-above what is checked in the standard
* validateFeatureContent() hook.
*
* @private
*/
function expandFeatureContent(fassets, feature) {
// hold on to our reducer slice
// ... so as to apply it to our final resolved reducer (below)
var slice = feature[this.name].slice;
// insure the slice is defined
if (!slice) {
return this.name + ' (when supplied) must be embellished with slicedReducer(). SideBar: slicedReducer() should always wrap the the outer function passed to createFeature() (even when expandWithFassets() is used).';
}
// expand self's content in the supplied feature
// ... by invoking the expandWithFassetsCB(fassets) embellished by expandWithFassets(expandWithFassetsCB)
feature[this.name] = feature[this.name](fassets);
// apply same slice to our final resolved reducer
// ... so it is accessible to our internals (i.e. launchApp)
(0, _slicedReducer2.default)(slice, feature[this.name]);
logf('expandFeatureContent() successfully expanded Feature.name:' + feature.name + '\'s Feature.' + this.name + ' and applied slicedReducer() from outer expandWithFassets()');
}
/**
* Interpret the supplied features, generating our top-level app
* reducer function.
*
* @param {Fassets} fassets the Fassets object used in
* cross-feature-communication.
*
* @param {Feature[]} activeFeatures - The set of active (enabled)
* features that comprise this application.
*
* @private
*/
function assembleFeatureContent(fassets, activeFeatures) {
// interpret the supplied features, generating our top-level app reducer function
// ... our logf() is in the accumAppReducer() surrogate
var appReducer = accumAppReducer(this.name, activeFeatures, this.config.allowNoReducers$);
// retain for subsequent usage
this.appReducer = appReducer;
}
/**
* Collect any redux middleware from other aspects through OUR
* documented Aspect.getReduxMiddleware() API (an"aspect
* cross-communication" mechanism).
*
* @param {Fassets} fassets the Fassets object used in
* cross-feature-communication.
*
* @param {Aspect[]} aspects - The set of **feature-u** Aspect objects
* used in this this application.
*
* @private
*/
function assembleAspectResources(fassets, aspects) {
// collect any redux middleware from other aspects through OUR Aspect.getReduxMiddleware() API
var middlewareSummaryLog = [];
var middleware = aspects.reduce(function (accum, aspect) {
if (aspect.getReduxMiddleware) {
var reduxMiddleware = aspect.getReduxMiddleware();
if (reduxMiddleware) {
middlewareSummaryLog.push('\n Aspect.name:' + aspect.name + ' <-- defines: getReduxMiddleware()');
accum.push(reduxMiddleware);
} else {
middlewareSummaryLog.push('\n Aspect.name:' + aspect.name + ' <-- defines: getReduxMiddleware() ... HOWEVER returned null');
}
} else {
middlewareSummaryLog.push('\n Aspect.name:' + aspect.name);
}
return accum;
}, []);
logf('assembleAspectResources() gathered ReduxMiddleware from the following Aspects: ' + middlewareSummaryLog);
// collect any redux store enhancers from other aspects through OUR Aspect.getReduxEnhancer() API
var enhancerSummaryLog = [];
var enhancer = aspects.reduce(function (accum, aspect) {
if (aspect.getReduxEnhancer) {
var reduxEnhancer = aspect.getReduxEnhancer();
if (reduxEnhancer) {
enhancerSummaryLog.push('\n Aspect.name:' + aspect.name + ' <-- defines: getReduxEnhancer()');
accum.push(reduxEnhancer);
} else {
enhancerSummaryLog.push('\n Aspect.name:' + aspect.name + ' <-- defines: getReduxEnhancer() ... HOWEVER returned null');
}
} else {
enhancerSummaryLog.push('\n Aspect.name:' + aspect.name);
}
return accum;
}, []);
logf('assembleAspectResources() gathered ReduxEnhancer from the following Aspects: ' + enhancerSummaryLog);
// create our redux store (retained in self for subsequent usage)
// ... accomplished in internal config micro function (a defensive measure to allow easier overriding by client)
logf('assembleAspectResources() defining our Redux store WITH optional middleware and enhancer registration');
this.appStore = this.config.createReduxStore$(this.appReducer, middleware, enhancer);
}
/**
* An internal config micro function that creates/returns the redux
* app store WITH optional middleware registration.
*
* This logic is broken out in this internal method as a defensive
* measure to make it easier for a client to override (if needed for
* some unknown reason).
*
* @param {reducerFn} the top-level app reducer function.
*
* @param {reduxMiddleware[]} middlewareArr - the optional set of
* reduxMiddleware items to register to redux (zero length array if
* none).
*
* @param {reduxEnhancer[]} enhancerArr - the optional set of
* reduxEnhancer items to register to redux (zero length array if
* none).
*
* @return {reduxAppStore} the newly created redux app store.
*
* @private
*/
function createReduxStore$(appReducer, middlewareArr, enhancerArr) {
// collect all redux enhancers (if any)
var enhancers = [];
// ... middlewareArr: apply enhancer for middleware supplied by other Aspect Plugins
if (middlewareArr.length > 0) {
enhancers.push(_redux.applyMiddleware.apply(undefined, _toConsumableArray(middlewareArr)));
}
// ... enhancerArr: apply enhancers supplied by other Aspect Plugins
if (enhancerArr.length > 0) {
enhancers.push.apply(enhancers, _toConsumableArray(enhancerArr));
}
// define artifacts that automatically embellish Redux DevTools (when detected)
// ... used mutually exclusively (see JavaDocs)
var _reduxDevToolHook$ = this.reduxDevToolHook$(),
enhancer$ = _reduxDevToolHook$.enhancer$,
compose$ = _reduxDevToolHook$.compose$;
// finally - the complete enhancer that encompasses any/all middleware/enhancers
var enhancer = enhancers.length === 0 ? enhancer$ : compose$.apply(undefined, enhancers);
// define our Redux app-wide store
// NOTE: passing enhancer as last argument requires redux@>=3.1.0
return (0, _redux.createStore)(appReducer, this.initialState$, enhancer);
}
/**
* Auto detect Redux Dev Tools when installed/enabled in browser.
*
* NOTE: It's OK to use Redux Dev Tools in production: http://bit.ly/rdtOkForProd
*
* @return {enhancer$, compose$} to be used in creating redux store.
*
* These items are to be used mutually exclusively!
*
* - enhancer$: a pre-defined enhancer representing ReduxDevTools
* - to be used when NO OTHER enhancers are present
* - will be `undefined` when ReduxDevTools is undetected
* ... which is ALSO a VALID param to createStore()
*
* - compose$: an enhancer compose function that AUTO INCLUDES ReduxDevTools
* - to be used when ADDITIONAL enhancers are present
* - reverts to the "standard" enhancer compose function
* when ReduxDevTools NOT installed/enabled
*
* @private
*/
function reduxDevToolHook$() {
// apply empty object (that no-ops) when in non-browser env
// ... react-native, or SSR (Server Side Rendering), etc.
var win = window || {};
// a function returning a pre-defined enhancer which IS the ReduxDevTools
// ... undefined when ReduxDevTools not installed/enabled
var extension = win.__REDUX_DEVTOOLS_EXTENSION__;
// a pre-defined enhancer representing ReduxDevTools
// ... undefined when ReduxDevTools not installed/enabled
// NOTE: undefined can be passed into createStore
// representing NO enhancers
var enhancer$ = extension && extension();
// an enhancer compose function that AUTO INCLUDES ReduxDevTools
// ... OR the or the "standard" enhancer compose function
// when ReduxDevTools NOT installed/enabled
var compose$ = win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || _redux.compose;
// report when ReduxDevTools is active
if (extension) {
logf.force('createReduxStore$() hooking into Redux DevTools (installed in your browser)');
}
// expose ReduxDevTools adornments (see comments above)
// ... NOTE: these resources are used mutually exclusively
return { enhancer$: enhancer$, compose$: compose$ };
}
/**
* Promote our redux store (for good measure), just in case some
* external process needs it.
*
*@throws {Error} when getReduxStore() called before launchApp()
*/
function getReduxStore() {
if (!this.appStore) {
throw new Error('***ERROR*** feature-redux reducerAspect.getReduxStore() can only be called after a successful launchApp() execution');
}
return this.appStore;
}
/**
* Introduce the standard Redux Provider component in the app root
* element, providing standard access to the redux store (both state
* and dispatch) through redux connect().
*
* @param {Fassets} fassets the Fassets object used in
* cross-feature-communication.
*
* @param {reactElm} curRootAppElm - the current react app element root.
*
* @return {reactElm} a new react app element root (which in turn must
* contain the supplied curRootAppElm), or simply the supplied
* curRootAppElm (if no change).
*
* @private
*/
function injectRootAppElm(fassets, curRootAppElm) {
logf('injectRootAppElm() introducing redux <Provider> component into rootAppElm');
return _react2.default.createElement(
_reactRedux.Provider,
{ store: this.appStore },
curRootAppElm
);
// TODO: if an external "feature" changes curRootAppElm to an array, <Provider> can't handle multiple children
}
/**
* Promote our redux state/dispatch functions in the namedParams
* feature-u's Application Life Cycle Hooks
*
* @param {Fassets} fassets the Fassets object used in
* cross-feature-communication.
*
* @return {plainObject} our redux state/dispatch functions: {getState, dispatch}
*
* @private
*/
function injectParamsInHooks(fassets) {
return {
getState: this.appStore.getState,
dispatch: this.appStore.dispatch
};
}
/**
* @private
*
* Interpret the supplied features, generating our top-level app
* reducer function.
*
* @param {string} aspectName self's aspect name, used to "key"
* aspects of this type in the Feature object: `Feature.{name}: xyz`.
* @param {Feature[]} activeFeatures the "active" features that
* comprise this application.
*
* @param {boolean/function} allowNoReducers$ the
* reducerAspect.config.allowNoReducers$ in effect.
*
* @return {appReducerFn} a top-level app reducer function.
*/
function accumAppReducer(aspectName, activeFeatures, allowNoReducers$) {
// ... named export ONLY used in testing
// iterated over all activeFeatures,
// ... generating the "shaped" genesis structure
// used in combining all reducers into a top-level app reducer
// EXAMPLE:
// - given following reducers (each from a separate Feature):
// Feature.reducer: slicedReducer('device', deviceReducerFn)
// Feature.reducer: slicedReducer('auth', authReducerFn)
// Feature.reducer: slicedReducer('view.currentView', currentViewReducerFn)
// Feature.reducer: slicedReducer('view.discovery', discoveryReducerFn)
// Feature.reducer: slicedReducer('view.eateries', eateriesReducerFn)
// - the following shapedGenesis will result:
// shapedGenesis: {
// device: deviceReducerFn,
// auth: authReducerFn,
// view: {
// currentView: currentViewReducerFn,
// discovery: discoveryReducerFn,
// eateries: eateriesReducerFn,
// }
// }
var shapedGenesis = {};
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = activeFeatures[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var feature = _step.value;
// only interpret features that define our aspect
if (feature[aspectName]) {
var reducer = feature[aspectName]; // our feature content is a reducer!
var slice = reducer.slice; // our validation ensures embellishment via slicedReducer()
// interpret the slice's federated namespace into a structure with depth
var nodeNames = slice.split('.');
var runningNode = shapedGenesis;
var runningShape = '';
for (var i = 0; i < nodeNames.length; i++) {
// use old-styled for loop to interpret index (see: leafNode variable)
var nodeName = nodeNames[i];
var leafNode = i === nodeNames.length - 1;
// utilize existing subNode (from other features), or create new (on first occurrence)
var subNodeExisted = runningNode[nodeName] ? true : false;
var subNode = runningNode[nodeName] || {};
// maintain human readable shape (for error reporting)
runningShape += (runningShape ? '.' : '') + nodeName;
// apply validation constraints of our shapedGenesis
// 1: intermediate node cannot be a reducer, because we can't intermix feature reducer with combineReducer (of launchApp)
// 2: all leafs MUST be reducer functions (this is actually FORCED by our code below)
if ((0, _lodash4.default)(subNode) || subNodeExisted && leafNode) {
// TO BE ORDER INDEPENDENT, added: or condition
throw new Error('***ERROR*** feature-redux constraint violation: reducer slice: \'' + runningShape + '\' cannot be specified by multiple features (either as an intermediate node, or an outright duplicate) because we can\'t intermix feature reducers and combineReducer() from launchApp()');
}
// inject our new sub-node -or- the reducer for leaf nodes
runningNode[nodeName] = leafNode ? reducer : subNode;
// continue process into next level
runningNode = subNode;
}
}
}
// handle scenario where NO reducers were specified in our set of Features
// ... when shapedGenesis == {}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
var appHasNoState = Object.keys(shapedGenesis).length === 0;
if (appHasNoState) {
// by default, this is an error condition (when NOT overridden by client)
if (!allowNoReducers$) {
throw new Error('***ERROR*** feature-redux found NO reducers within your features ' + ('... did you forget to register Feature.' + aspectName + ' aspects in your features? ') + '(please refer to the feature-redux docs to see how to override this behavior).');
}
// when client override is a function, interpret it as an app-wide reducer
else if ((0, _lodash4.default)(allowNoReducers$)) {
logf.force('WARNING: NO reducers were found in your features (i.e. Feature.' + aspectName + '), ' + 'but client override (reducerAspect.config.allowNoReducers$=reducerFn;) ' + 'directed a continuation WITH the specified reducer.');
return allowNoReducers$; // use supplied reducer
}
// otherwise we simply use an identity reducer
else {
logf.force('WARNING: NO reducers were found in your features (i.e. Feature.' + aspectName + '), ' + 'but client override (reducerAspect.config.allowNoReducers$=truthy;) ' + 'directed a continuation WITH the identity reducer.');
return function (state) {
return state;
}; // use identity reducer
}
}
// convert our "shaped" genesis structure into a single top-level app reducer function
logf('assembleFeatureContent() the overall appState shape is: ', shapedGenesis);
var appReducer = accumReducer(shapedGenesis);
return appReducer;
}
/**
* A recursive function that accumulates all reducers in the supplied
* genesisNode into a single reducer function.
*
* @param {GenesisStruct} genesisNode a "shaped" genesis structure
* used in combining all reducers.
*
* @return {reducerFn} a reducer function that recursively
* accumulates all reducers found in the supplied genesisNode.
*
* @private
*/
function accumReducer(genesisNode) {
if ((0, _lodash4.default)(genesisNode)) {
return genesisNode;
}
var subReducers = {};
for (var subGenesisNodeName in genesisNode) {
var subGenesisNode = genesisNode[subGenesisNodeName];
subReducers[subGenesisNodeName] = accumReducer(subGenesisNode);
}
return (0, _redux.combineReducers)(subReducers);
}