UNPKG

feature-redux

Version:
594 lines (497 loc) 23.8 kB
'use strict'; 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); }