UNPKG

feature-u

Version:

Feature Based Project Organization for React

1,019 lines (814 loc) 44.5 kB
'use strict'; exports.__esModule = true; exports.op = undefined; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; exports.default = launchApp; var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _lodash = require('lodash.isfunction'); var _lodash2 = _interopRequireDefault(_lodash); var _lodash3 = require('lodash.isplainobject'); var _lodash4 = _interopRequireDefault(_lodash3); var _verify = require('../util/verify'); var _verify2 = _interopRequireDefault(_verify); var _createAspect = require('../extend/createAspect'); var _createFeature = require('./createFeature'); var _createFassets = require('./createFassets'); var _createFassets2 = _interopRequireDefault(_createFassets); var _withFassets = require('./withFassets'); var _logf = require('../util/logf'); var _logf2 = _interopRequireDefault(_logf); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 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; } var executionOrder = 1; // running counter of execution order of life-cycle-hooks (unit-test related) /** * Launch an application by assembling the supplied features, driving * the configuration of the frameworks in use _(as orchestrated by the * supplied set of plugable Aspects)_. * * For more information _(with examples)_, please refer to * {{book.guide.detail_launchingApp}}. * * **Please Note** this function uses named parameters. * * @param {Feature[]} features the features that comprise this * application. * * @param {Aspect[]} [aspects] the set of plugable Aspects that extend * **feature-u**, integrating other frameworks to match your specific * run-time stack.<br/><br/> * * When NO Aspects are supplied _(an atypical case)_, only the very * basic **feature-u** characteristics are in effect (like fassets * and life-cycle hooks). * * @param {registerRootAppElmCB} registerRootAppElm the callback hook * that registers the supplied root application element to the specific * React framework used in the app.<br/><br/> * * Because this registration is accomplished by app-specific code, * **feature-u** can operate in any of the react platforms, such as: * {{book.ext.react}} web, {{book.ext.reactNative}}, * {{book.ext.expo}}, etc.<br/><br/> * * Please refer to {{book.guide.detail_reactRegistration}} for more * details and complete examples. * * @param {showStatusCB} [showStatus] an optional callback hook that * communicates a blocking "persistent" status message to the end * user. * * Please refer to {{book.api.showStatusCB}} for more information. * * @return {Fassets} the Fassets object used in * cross-feature-communication. * * @function launchApp */ function launchApp() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var features = _ref.features, _ref$aspects = _ref.aspects, aspects = _ref$aspects === undefined ? [] : _ref$aspects, registerRootAppElm = _ref.registerRootAppElm, _ref$showStatus = _ref.showStatus, showStatus = _ref$showStatus === undefined ? showStatusFallback : _ref$showStatus, unknownArgs = _objectWithoutProperties(_ref, ['features', 'aspects', 'registerRootAppElm', 'showStatus']); (0, _logf2.default)('STARTING: - your application is now starting up'); // reset: running counter of execution order of life-cycle-hooks (unit-test related) executionOrder = 1; // validate launchApp() parameters var check = _verify2.default.prefix('launchApp() parameter violation: '); // ... aspects var aspectMap = op.alch.genesis(aspects); // ... features check(features, 'features is required'); check(Array.isArray(features), 'features must be a Feature[] array'); // ... registerRootAppElm check(registerRootAppElm, 'registerRootAppElm is required'); check((0, _lodash2.default)(registerRootAppElm), 'registerRootAppElm must be a function'); // ... showStatus check(showStatus, 'showStatus is required'); check((0, _lodash2.default)(showStatus), 'showStatus must be a function'); // ... unrecognized named parameter var unknownArgKeys = Object.keys(unknownArgs); check(unknownArgKeys.length === 0, 'unrecognized named parameter(s): ' + unknownArgKeys); // ... unrecognized positional parameter check(arguments.length === 1, 'unrecognized positional parameters (only named parameters can be specified)'); // perform "Aspect" property validation op.helper.validateAspectProperties(aspects); // perform "Feature" property validation op.alch.validateFeatureContent(features, aspectMap); // prune to activeFeatures, insuring all feature.names are unique var activeFeatures = op.helper.pruneActiveFeatures(features); // accumulate all feature assets in our Fassets object (used in cross-feature-communication) var fassets = (0, _createFassets2.default)(activeFeatures); // expand the feature content of any aspect that relies on expandWithFassets() // ... AND perform a delayed validation, once expansion has occurred op.alch.expandFeatureContent(fassets, activeFeatures, aspects); // assemble content of each aspect across all features op.alch.assembleFeatureContent(fassets, activeFeatures, aspects); // assemble resources for each aspect across all other aspects, ONCE ALL aspects have assembled their feature content op.alch.assembleAspectResources(fassets, aspects); // define our rootAppElm via DOM injections from a combination of Aspects/Features // ... also apply Feature.appWillStart() life-cycle hook var rootAppElm = op.helper.defineRootAppElm(fassets, activeFeatures, aspects); // start our app by registering our rootAppElm to the appropriate react framework // NOTE 1: Because this is accomplished by app-specific code, // feature-u can operate in any number of containing react frameworks, // like React Web, React Native, Expo, etc. // NOTE 2: We delay this process (via timeout) making "import fassets" feasible // to UI rendering functions ... such as redux connect(). // This technique allows launchApp() to complete, and the fassets object // to have definition (via application code export) // for use in these UI rendering functions. // HOWEVER: We subsequently discovered that this timeout is // NOT compatible with Expo (or react-native - not sure which) // ERROR: Module AppRegistry is not a registered callable module (calling runApplication) // EVIDENTLY the designated expo mainline cannot run to completion // without first registering a component in some way // ... ex: Expo.registerRootComponent(...); //setTimeout(() => { // remove timeout (see "NOTE 2" above) registerRootAppElm(rootAppElm, fassets); //}, 0); // wrap the showStatus() function to prune duplicate status // ... this happens INTERNALLY when monitoring the next unresolved asynchronous appInit() process var priorStatus = { msg: undefined, err: undefined }; function showStatusNoDupes(msg, err) { // no-op on duplicate back-to-back status if (msg === priorStatus.msg && err === priorStatus.err) { return; } // retain the status info NOW reported priorStatus.msg = msg; priorStatus.err = err; // pass-through to supplied function showStatus(msg, err); } // gather the set of additional "Aspect Injected" named parameters // to pass into our remaining Application Life Cycle Hooks var additionalHooksParams = op.alch.injectParamsInHooks(fassets, aspects); // apply Feature.appInit() life-cycle hook op.flch.appInit(fassets, activeFeatures, additionalHooksParams, showStatusNoDupes).then(function () { // >>> once all async processes of feature.appInit() have completed, // continue on with our launchApp() processes // because of the "covert" async nature of launchApp(), // we report any errors via the showStatus() mechanism. try { // apply Feature.appDidStart() life-cycle hook op.flch.appDidStart(fassets, activeFeatures, additionalHooksParams); (0, _logf2.default)('COMPLETE: Your application has now started'); } catch (err) { var errMsg = 'A problem was encountered in a appDidStart() life-cycle hook'; (0, _logf2.default)('INCOMPLETE: Your application did NOT start ... ' + errMsg, err); showStatusNoDupes(errMsg, err); } }); // expose our new App object (used in feature cross-communication) return fassets; } //*** //*** A secret diagnostic hook attached to the launchApp() function. //*** launchApp.diag = { logf: _logf2.default // allow client to enable feature-u logs via: launchApp.diag.logf.enable(); }; //*** //*** Specification: registerRootAppElmCB //*** /** * The {{book.api.launchApp}} callback hook that registers the * supplied root application element to the specific React framework * used in the app. * * Because this registration is accomplished by app-specific code, * **feature-u** can operate in any of the React platforms, such as: * {{book.ext.react}} web, {{book.ext.reactNative}}, * {{book.ext.expo}}, etc. * * Please refer to {{book.guide.detail_reactRegistration}} for more * details and complete examples. * * @callback registerRootAppElmCB * * @param {reactElm} rootAppElm - the root application element to be * registered. * * @param {Fassets} fassets the Fassets object used in cross-feature-communication * (rarely needed except to allow client to inject their own * FassetsContext.Provider for a null rootAppElm). * * @return void */ //*** //*** Specification: showStatusCB //*** /** * The optional {{book.api.launchApp}} callback hook that communicates * a blocking "persistent" status message to the end user. * * These status messages originate from the blocking that occurs in * the asynchronous processes managed by the {{book.guide.appInitCB}} * life-cycle-hook. * * By design **feature-u** has no ability to manifest messages to the * end user, because this is very app-specific in styling and other * heuristics. By default (when **NO** `showStatus` parameter is * supplied, **feature-u** will simply **console log** these messages. * * A typical manifestation of this callback is to display a running * persistent SplashScreen, seeded with the supplied message. The * SplashScreen should be taken down when NO message is supplied * (i.e. `''`). * * Please refer to {{book.guide.appInitCB}} for more details and * examples. * * @callback showStatusCB * * @param {string} [msg] - the "persistent" message to display. When * NO message is supplied (i.e. `''`), **all** user notifications * should be cleared _(for example, take the SplashScreen down)_. * * @param {Error} [err] - an optional error to communicate to the * user. * * @return void */ //*** //*** Operations Bundle (broken out for testability) //*** /* * A bundle of launchApp() operations (i.e. functions) * - exported internally * - supporting isolated unit testing * * Because we use a bundled container, it provides rudimentary * testing hooks for things like: * - mocking * - monkey patching * - etc. */ var op = exports.op = { alch: {}, // aspect-life-cycle-hook ... see definitions (below) flch: {}, // feature-life-cycle-hook ... see definitions (below) helper: {} // general helpers ... see definitions (below) }; //*-------------------------------------------------------------- //* aspect-life-cycle-hook: genesis(aspects): aspectMap //*-------------------------------------------------------------- op.alch.genesis = function (aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.genesis.executionOrder = executionOrder++; var check = _verify2.default.prefix('launchApp() parameter violation: '); check(Array.isArray(aspects), 'aspects (when supplied) must be an Aspect[] array'); (0, _logf2.default)('the following Aspects are in effect: ' + aspects.map(function (aspect) { return '\n Aspect.name:' + aspect.name; })); // log summary var hookCount = aspects.reduce(function (count, aspect) { return aspect.genesis ? count + 1 : count; }, 0); var hookSummary = aspects.map(function (aspect) { return '\n Aspect.name:' + aspect.name + (aspect.genesis ? ' <-- defines: genesis()' : ''); }); (0, _logf2.default)('aspect-life-cycle-hook ... PROCESSING: Aspect.genesis() ... ' + hookCount + ' hooks:' + hookSummary); // our convenient hash of aspects // ... keyed by aspectName // aspectMap[aspectName]: aspect var aspectMap = aspects.reduce(function (accum, aspect) { // the set of aspects in use MUST have unique names if (accum[aspect.name]) { check(false, 'supplied aspects contain a NON-unique name: \'' + aspect.name + '\''); } // allow each aspect to perform Aspect related initialization and validation if (aspect.genesis) { (0, _logf2.default)('aspect-life-cycle-hook ... Aspect.name:' + aspect.name + ' ... invoking it\'s defined Aspect.genesis()'); var errMsg = aspect.genesis(); check(!errMsg, errMsg); // non-null is considered a validation error } // maintain our aspect hash accum[aspect.name] = aspect; return accum; }, {}); return aspectMap; }; //*--------------------------------------------------- //* helper: validateAspectProperties(aspects): void //*--------------------------------------------------- op.helper.validateAspectProperties = function (aspects) { // perform "Aspect" property validation // NOTE 1: This is done here rather than createAspect(), because // all Aspects need to be expanded (i.e. imported) for any extendAspectProperty() to be executed) // ... this will have been done by the time launchApp() is executed!! // NOTE 2: The original source of this error is in createAspect(), // so we prefix any errors as such! var check = _verify2.default.prefix('createAspect() parameter violation: '); aspects.forEach(function (aspect) { // for each aspect for (var propName in aspect) { // iterate over the aspects props // handle unrecognized Aspect.property // ... NOTE: Aspect extended properties have already been added to this isAspectProperty() list // via extendAspectProperty() // executed early within the extending Aspect (i.e. in genesis()) // SO it is in the list at this time!! check((0, _createAspect.isAspectProperty)(propName), 'Aspect.name: \'' + aspect.name + '\' contains unrecognized property: ' + propName + ' ... no Aspect is registered to handle this!'); } }); }; //*----------------------------------------------------------------------------- //* aspect-life-cycle-hook: validateFeatureContent(features, aspectMap): void //*----------------------------------------------------------------------------- op.alch.validateFeatureContent = function (features, aspectMap) { // NOTE: nothing to logf() here BECAUSE entire result is potential EXCEPTIONS // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.validateFeatureContent.executionOrder = executionOrder++; // log summary var aspects = []; // convert aspectMap to array ... do this RATHER than change API JUST to accommodate logging for (var propKey in aspectMap) { aspects.push(aspectMap[propKey]); } var hookCount = aspects.reduce(function (count, aspect) { return aspect.validateFeatureContent ? count + 1 : count; }, 0); var hookSummary = aspects.map(function (aspect) { return '\n Aspect.name:' + aspect.name + (aspect.validateFeatureContent ? ' <-- defines: validateFeatureContent()' : ''); }); (0, _logf2.default)('aspect-life-cycle-hook ... PROCESSING: Aspect.validateFeatureContent() ... ' + hookCount + ' hooks:' + hookSummary); // perform "Feature" property validation // NOTE 1: This is done here rather than createFeature(), because it's the aspect's // responsibility, and this is the spot where we have aspect context. // NOTE 2: The original source of this error is in createFeature(), // so we prefix any errors as such! var check = _verify2.default.prefix('createFeature() parameter violation: '); features.forEach(function (feature) { // for each feature for (var propName in feature) { // iterate over the features props // we only validate non built-in keywords // ... built-ins are validated by createFeature() // ... these extra props will be processed by an Aspect // ... an error condition if if no Aspect is registered to handle it // ... NOTE: Aspect extended properties have already been added to this isFeatureProperty() list // via extendFeatureProperty() // executed early within the extending Aspect (i.e. in genesis()) // SO it is in the list at this time!! if (!(0, _createFeature.isFeatureProperty)(propName)) { // locate the aspect that will process this item var aspect = aspectMap[propName]; // handle unrecognized aspect check(aspect, 'Feature.name: \'' + feature.name + '\' contains unrecognized property: ' + propName + ' ... no Aspect is registered to handle this!'); // delay validation when expansion is needed // ... is accomplished in subsequent step (after expansion has occurred) // ... this means that validation logic in aspect does NOT have to worry about .expandWithFassets if (!feature[propName].expandWithFassets) { // allow the aspect to validate it's content // ... ex: a reducer MUST be a function (or expandWithFassets) and it must have a shape! (0, _logf2.default)('aspect-life-cycle-hook ... Aspect.name:' + aspect.name + ' ... invoking it\'s required Aspect.validateFeatureContent() on Feature.name:' + feature.name + '\'s Feature.' + aspect.name); var errMsg = aspect.validateFeatureContent(feature); // validate self's aspect on supplied feature (which is known to contain this aspect) check(!errMsg, errMsg); // non-null is considered a validation error } } } }); }; //*---------------------------------------------------------------- //* helper: pruneActiveFeatures(features): activeFeatures //*---------------------------------------------------------------- op.helper.pruneActiveFeatures = function (features) { var check = _verify2.default.prefix('launchApp() parameter violation: '); // prune to activeFeatures, insuring all feature.names are unique var allFeatureNames = {}; var activeFeatures = features.filter(function (feature) { check(!allFeatureNames[feature.name], 'feature.name: \'' + feature.name + '\' is NOT unique'); allFeatureNames[feature.name] = true; return feature.enabled; }); (0, _logf2.default)('the following Features were supplied: ' + features.map(function (feature) { return '\n Feature.name:' + feature.name + (feature.enabled ? '' : ' <<< NOT ACTIVE'); })); (0, _logf2.default)('the following Features are in effect (i.e. active): ' + activeFeatures.map(function (feature) { return '\n Feature.name:' + feature.name; })); return activeFeatures; }; //*-------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: expandFeatureContent(fassets, activeFeatures, aspects): void //*-------------------------------------------------------------------------------------- op.alch.expandFeatureContent = function (fassets, activeFeatures, aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.expandFeatureContent.executionOrder = executionOrder++; // log summary var hookCount = aspects.reduce(function (count, aspect) { return aspect.expandFeatureContent ? count + 1 : count; }, 0); var hookSummary = aspects.map(function (aspect) { return '\n Aspect.name:' + aspect.name + (aspect.expandFeatureContent ? ' <-- defines: expandFeatureContent()' : ''); }); (0, _logf2.default)('resolving expandWithFassets() ... either by DEFAULT-PROCESS -OR- aspect-life-cycle-hook PROCESSING: Aspect.expandFeatureContent() ... ' + hookCount + ' hooks:' + hookSummary); // expand the feature content of any aspect that relies on expandWithFassets() // ... AND perform a delayed validation, once expansion has occurred // NOTE: The original source of this error is in createFeature(), // so we prefix any errors as such! var check = _verify2.default.prefix('createFeature() parameter violation: '); aspects.forEach(function (aspect) { activeFeatures.forEach(function (feature) { if (feature[aspect.name] && feature[aspect.name].expandWithFassets) { var errMsg = null; // perform the expansion if (aspect.expandFeatureContent) { // aspect wishes to do this // ... a simple process, BUT provides the hook to do more (ex: reducer transfer of slice) (0, _logf2.default)('resolving expandWithFassets() [by aspect-life-cycle-hook Aspect.name:' + aspect.name + '\'s Aspect.expandFeatureContent()] ON Feature.name:' + feature.name + '\'s Feature.' + aspect.name + ' AspectContent'); errMsg = aspect.expandFeatureContent(fassets, feature); // ... specialized validation, over-and-above the validateFeatureContent() hook check(!errMsg, errMsg); // truthy is considered a validation error } else { (0, _logf2.default)('resolving expandWithFassets() [by DEFAULT-PROCESS] ON Feature.name:' + feature.name + '\'s Feature.' + aspect.name + ' AspectContent'); // default implementation (when not done by the aspect) feature[aspect.name] = feature[aspect.name](fassets); } // perform our delayed validation (0, _logf2.default)('aspect-life-cycle-hook ... Aspect.name:' + aspect.name + ' ... invoking it\'s required Aspect.validateFeatureContent() on Feature.name:' + feature.name + '\'s Feature.' + aspect.name + ' ... DELAYED from expandWithFassets()'); errMsg = aspect.validateFeatureContent(feature); // validate self's aspect on supplied feature (which is known to contain this aspect) check(!errMsg, errMsg); // truthy is considered a validation error } }); }); }; //*---------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: assembleFeatureContent(fassets, activeFeatures, aspects): void //*---------------------------------------------------------------------------------------- op.alch.assembleFeatureContent = function (fassets, activeFeatures, aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.assembleFeatureContent.executionOrder = executionOrder++; // log summary var hookCount = aspects.reduce(function (count, aspect) { return aspect.assembleFeatureContent ? count + 1 : count; }, 0); var hookSummary = aspects.map(function (aspect) { return '\n Aspect.name:' + aspect.name + (aspect.assembleFeatureContent ? ' <-- defines: assembleFeatureContent()' : ''); }); (0, _logf2.default)('aspect-life-cycle-hook ... PROCESSING: Aspect.assembleFeatureContent() ... ' + hookCount + ' hooks:' + hookSummary); // assemble content of each aspect across all features // ... retaining needed state for subsequent ops aspects.forEach(function (aspect) { (0, _logf2.default)('aspect-life-cycle-hook ... Aspect.name:' + aspect.name + ' ... invoking it\'s required Aspect.assembleFeatureContent()'); aspect.assembleFeatureContent(fassets, activeFeatures); }); }; //*------------------------------------------------------------------------- //* aspect-life-cycle-hook: assembleAspectResources(fassets, aspects): void //*------------------------------------------------------------------------- op.alch.assembleAspectResources = function (fassets, aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.assembleAspectResources.executionOrder = executionOrder++; // log summary var hookCount = aspects.reduce(function (count, aspect) { return aspect.assembleAspectResources ? count + 1 : count; }, 0); var hookSummary = aspects.map(function (aspect) { return '\n Aspect.name:' + aspect.name + (aspect.assembleAspectResources ? ' <-- defines: assembleAspectResources()' : ''); }); (0, _logf2.default)('aspect-life-cycle-hook ... PROCESSING: Aspect.assembleAspectResources() ... ' + hookCount + ' hooks:' + hookSummary); // assemble resources for each aspect across all other aspects, ONCE ALL aspects have assembled their feature content // ... retaining needed state for subsequent ops aspects.forEach(function (aspect) { if (aspect.assembleAspectResources) { (0, _logf2.default)('aspect-life-cycle-hook ... Aspect.name:' + aspect.name + ' ... invoking it\'s defined Aspect.assembleAspectResources()'); aspect.assembleAspectResources(fassets, aspects); } }); }; //*------------------------------------------------------------------------ //* helper: defineRootAppElm(fassets, activeFeatures, aspects): rootAppElm //*------------------------------------------------------------------------ op.helper.defineRootAppElm = function (fassets, activeFeatures, aspects) { // define our curRootAppElm via DOM injections from a combination of Aspects/Features var rootAppElm = null; // we start with nothing (0, _logf2.default)('defining-rootAppElm ... starting process, rootAppElm: null'); // FIRST: DOM injection via Aspect.initialRootAppElm(fassets, curRootAppElm) rootAppElm = op.alch.initialRootAppElm(fassets, aspects, rootAppElm); // SECOND: DOM injection via Feature.appWillStart() life-cycle hook rootAppElm = op.flch.appWillStart(fassets, activeFeatures, rootAppElm); // THIRD: DOM injection via Aspect.injectRootAppElm() rootAppElm = op.alch.injectRootAppElm(fassets, aspects, rootAppElm); // FOURTH: inject our <FassetsContext.Provider> in support of withFassets() HoC // NOTE: We conditionally do this if a rootAppElm has been defined. // Otherwise, the App is responsible for this in the registerRootAppElm() hook. if (rootAppElm) { rootAppElm = _react2.default.createElement( _withFassets.FassetsContext.Provider, { value: fassets }, rootAppElm ); } // NOTE: We do NOT validate rootAppElm to insure it is non-null! // - at first glance it would appear that a null rootAppElm would render NOTHING // - HOWEVER, ULTIMATELY the app code (found in the registerRootAppElm() hook) // can display whatever it wants ... a given app may have chosen to inject it's own rootAppElm (0, _logf2.default)('defining-rootAppElm ... complete, rootAppElm: ', _logf2.default.elm2html(rootAppElm)); return rootAppElm; }; //*---------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: initialRootAppElm(fassets, aspects, curRootAppElm): rootAppElm //*---------------------------------------------------------------------------------------- op.alch.initialRootAppElm = function (fassets, aspects, curRootAppElm) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.initialRootAppElm.executionOrder = executionOrder++; // log summary var hookCount = aspects.reduce(function (count, aspect) { return aspect.initialRootAppElm ? count + 1 : count; }, 0); var hookSummary = aspects.map(function (aspect) { return '\n Aspect.name:' + aspect.name + (aspect.initialRootAppElm ? ' <-- defines: initialRootAppElm()' : ''); }); (0, _logf2.default)('aspect-life-cycle-hook ... PROCESSING: Aspect.initialRootAppElm() ... ' + hookCount + ' hooks:' + hookSummary); // DOM injection via Aspect.initialRootAppElm(fassets, curRootAppElm) return aspects.reduce(function (curRootAppElm, aspect) { if (aspect.initialRootAppElm) { (0, _logf2.default)('aspect-life-cycle-hook ... Aspect.name:' + aspect.name + ' ... invoking it\'s defined Aspect.initialRootAppElm()'); var rootAppElm = aspect.initialRootAppElm(fassets, curRootAppElm); if (rootAppElm !== curRootAppElm) { (0, _logf2.default)('defining-rootAppElm ... Aspect.name:' + aspect.name + '\'s Aspect.initialRootAppElm() CHANGED rootAppElm: ', _logf2.default.elm2html(rootAppElm)); } return rootAppElm; } else { return curRootAppElm; } }, curRootAppElm); }; //*------------------------------------------------------------------------------------------- //* feature-life-cycle-hook: appWillStart(fassets, activeFeatures, curRootAppElm): rootAppElm //*------------------------------------------------------------------------------------------- op.flch.appWillStart = function (fassets, activeFeatures, curRootAppElm) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.flch.appWillStart.executionOrder = executionOrder++; // log summary var hookCount = activeFeatures.reduce(function (count, feature) { return feature.appWillStart ? count + 1 : count; }, 0); var hookSummary = activeFeatures.map(function (feature) { return '\n Feature.name:' + feature.name + (feature.appWillStart ? ' <-- defines: appWillStart()' : ''); }); (0, _logf2.default)('feature-life-cycle-hook ... PROCESSING: Feature.appWillStart() ... ' + hookCount + ' hooks:' + hookSummary); // DOM injection via Feature.appWillStart() life-cycle hook // - can perform ANY initialization // - AND supplement our top-level content (using a non-null return) // ... wedged between the two Aspect DOM injections (in support of various Aspect needs) return activeFeatures.reduce(function (curRootAppElm, feature) { if (feature.appWillStart) { (0, _logf2.default)('feature-life-cycle-hook ... Feature.name:' + feature.name + ' ... invoking it\'s defined Feature.appWillStart()'); var rootAppElm = feature.appWillStart({ fassets: fassets, curRootAppElm: curRootAppElm }) || curRootAppElm; if (rootAppElm !== curRootAppElm) { (0, _logf2.default)('defining-rootAppElm ... Feature.name:' + feature.name + '\'s Feature.appWillStart() CHANGED rootAppElm: ', _logf2.default.elm2html(rootAppElm)); } return rootAppElm; } else { return curRootAppElm; } }, curRootAppElm); }; //*--------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: injectRootAppElm(fassets, aspects, curRootAppElm): rootAppElm //*--------------------------------------------------------------------------------------- op.alch.injectRootAppElm = function (fassets, aspects, curRootAppElm) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.injectRootAppElm.executionOrder = executionOrder++; // log summary var hookCount = aspects.reduce(function (count, aspect) { return aspect.injectRootAppElm ? count + 1 : count; }, 0); var hookSummary = aspects.map(function (aspect) { return '\n Aspect.name:' + aspect.name + (aspect.injectRootAppElm ? ' <-- defines: injectRootAppElm()' : ''); }); (0, _logf2.default)('aspect-life-cycle-hook ... PROCESSING: Aspect.injectRootAppElm() ... ' + hookCount + ' hooks:' + hookSummary); // DOM injection via Aspect.injectRootAppElm() return aspects.reduce(function (curRootAppElm, aspect) { if (aspect.injectRootAppElm) { (0, _logf2.default)('aspect-life-cycle-hook ... Aspect.name:' + aspect.name + ' ... invoking it\'s defined Aspect.injectRootAppElm()'); var rootAppElm = aspect.injectRootAppElm(fassets, curRootAppElm); if (rootAppElm !== curRootAppElm) { (0, _logf2.default)('defining-rootAppElm ... Aspect.name:' + aspect.name + '\'s Aspect.injectRootAppElm() CHANGED rootAppElm: ', _logf2.default.elm2html(rootAppElm)); } return rootAppElm; } else { return curRootAppElm; } }, curRootAppElm); }; //*--------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: injectParamsInHooks(fassets, aspects): namedParams //*--------------------------------------------------------------------------------------- op.alch.injectParamsInHooks = function (fassets, aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.injectParamsInHooks.executionOrder = executionOrder++; // gather the set of additional "Aspect Injected" named parameters // to pass into our remaining Application Life Cycle Hooks var allNamedParams = aspects.reduce(function (accumNamedParams, aspect) { // when this aspect provides the hook, gather it's injected params if (aspect.injectParamsInHooks) { // invoke the aspect's hook // + injectParamsInHooks(fassets): namedParams (0, _logf2.default)('aspect-life-cycle-hook ... Aspect.name:' + aspect.name + ' ... invoking it\'s defined Aspect.injectParamsInHooks()'); var aspectNamedParams = aspect.injectParamsInHooks(fassets); // verify the return value var check = _verify2.default.prefix('Aspect.name:' + aspect.name + ' injectParamsInHooks() return violation: '); // ... required check(aspectNamedParams, 'nothing was returned ... expecting namedParams (a plain object) ... use empty object {} for nothing'); // ... expecting namedParams (a plain object) check((0, _lodash4.default)(aspectNamedParams), 'expecting namedParams (a plain object) ... NOT: ' + aspectNamedParams); // ... insure NO name clashes with other aspect injections var aspectNamedParamKeys = Object.keys(aspectNamedParams); var accumNamedParamKeys = Object.keys(accumNamedParams); var nameClashes = accumNamedParamKeys.filter(function (entry) { return aspectNamedParamKeys.includes(entry); }); check(nameClashes.length === 0, 'the following parameter names clashed with other aspects: ' + nameClashes); // ... insure NO reserved words are used var reservedNamedParamKeys = ['showStatus', 'fassets']; // from app-life-cycle-hooks: appInit(), and appDidStart() var reservedNameClashes = reservedNamedParamKeys.filter(function (entry) { return aspectNamedParamKeys.includes(entry); }); check(reservedNameClashes.length === 0, 'the following parameter names are reserved by feature-u and cannot be used: ' + reservedNameClashes); // accumulate this aspect's namedParams accumNamedParams = _extends({}, accumNamedParams, aspectNamedParams); } // keep accumulating from other aspects return accumNamedParams; }, {}); // that's all folks return allNamedParams; }; //*----------------------------------------------------------------------------------------- //* feature-life-cycle-hook: appInit(fassets, activeFeatures, additionalHooksParams, showStatus): promise //*----------------------------------------------------------------------------------------- op.flch.appInit = function (fassets, activeFeatures, additionalHooksParams, showStatus) { // wrap entire process // ... will resolve when ALL feature.appInit() have completed! return new Promise(function (resolve, reject) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.flch.appInit.executionOrder = executionOrder++; // log summary var hookCount = activeFeatures.reduce(function (count, feature) { return feature.appInit ? count + 1 : count; }, 0); var hookSummary = activeFeatures.map(function (feature) { return '\n Feature.name:' + feature.name + (feature.appInit ? ' <-- defines: appInit()' : ''); }); (0, _logf2.default)('feature-life-cycle-hook ... PROCESSING: Feature.appInit() ... ' + hookCount + ' hooks:' + hookSummary); // invoke Feature.appInit() life-cycle hooks // ... accomplished in AsyncInit class var asyncInits = activeFeatures.reduce(function (accum, feature) { if (feature.appInit) { accum.push(new AsyncInit(feature, fassets, additionalHooksParams, showStatus, monitorNextAsyncInit)); } return accum; }, []); // monitor the FIRST incomplete AsyncInit monitorNextAsyncInit(); // actively monitor the next incomplete AsyncInit function monitorNextAsyncInit() { // monitor the next incomplete AsyncInit var nextIncompleteAsyncInit = asyncInits.find(function (asyncInit) { return !asyncInit.complete; }); if (nextIncompleteAsyncInit) { nextIncompleteAsyncInit.monitor(); } // otherwise (if there are NO MORE), we are done else { // clear status notification to user showStatus(''); // ... use '', just in case we are invoking app-supplied showStatus() // ALL feature.appInit() have completed! // ... resolve our promise return resolve('ALL feature.appInit() have completed!'); } } }); // end of ... promise }; // the default showStatus callback ... merely logs status messages function showStatusFallback() { var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var err = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var postMsg = 'STATUS CHANGE (from appInit() life cycle hook): \'' + msg + '\''; if (err) { _logf2.default.force(postMsg + ' WITH ERROR: ' + err, err); } else { _logf2.default.force(postMsg); } } // our helper class to launch/monitor async processes in feature.appInit() var AsyncInit = function () { // class constructor function AsyncInit(feature, // feature KNOWN TO HAVE appInit() hook fassets, additionalHooksParams, showStatusApp, // the app-specific showStatus() callback function monitorNextAsyncInit) { var _this = this; _classCallCheck(this, AsyncInit); // carve out instance fields here this.feature = feature; // the feature object KNOWN to have appInit() hook this.showStatusApp = showStatusApp; // the app-specific showStatus() callback function this.monitored = false; // is process being monitored by our external tracker this.complete = false; // is process complete (either with or without error) // the process's current status message (defaulted to feature name) this.statusMsg = 'initializing feature: ' + feature.name; this.err = null; // the error (if any) from this AsyncInit process // bind our showStatus() instance method, so it can be used as a direct function this.showStatus = this.showStatus.bind(this); // invoke the feature.appInit() hook (0, _logf2.default)('feature-life-cycle-hook ... Feature.name:' + feature.name + ' ... invoking it\'s defined Feature.appInit()'); var optionalPromise = null; try { optionalPromise = feature.appInit(_extends({ showStatus: this.showStatus, fassets: fassets }, additionalHooksParams)); } catch (err) { this.err = err; } // monitor promise completion (when returned) if (optionalPromise && optionalPromise.then) { // a promise was returned optionalPromise.then(function () { // feature.appInit() finished successfully (0, _logf2.default)('AsyncInit: finished async process for feature: ' + _this.feature.name + ' - \'' + _this.statusMsg + '\''); // consider this a completion (with lack of err) _this.complete = true; // now monitor the next AsyncInit process (if any) monitorNextAsyncInit(); }).catch(function (err) { // feature.appInit() finished WITH error (0, _logf2.default)('AsyncInit: finished async process for feature: ' + _this.feature.name + ' - \'' + _this.statusMsg + '\' WITH ERROR: ' + err, err); // communicate error condition to user // NOTE: this retains err in self (in a "sticky" way) _this.showStatus(_this.statusMsg, err); }); } // when NO promise was returned, we consider it complete "from the start" // ... unless it errored out else { if (!this.err) { this.complete = true; } } } // mark this AsyncInit as now being actively monitored (by our external tracking process) _createClass(AsyncInit, [{ key: 'monitor', value: function monitor() { // mark self as being actively monitored this.monitored = true; // communicate to user what we are waiting for // ... and error condition (if previously errored out) this.showStatus(this.statusMsg, this.err); } // our showStatus(), used by the feature.appInit() hook }, { key: 'showStatus', value: function showStatus() { var msg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var err = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; // retain the last known msg for this feature's AsyncInit this.statusMsg = msg; // retain the last known error for this feature's AsyncInit // ... done so in a "sticky" way so as to NOT clobber existing error if (err) { // retain latest error (when supplied) ... retaining prior err (when NOT supplied) this.err = err; } // if our process is being actively monitored, pass through to the app-specific callback function if (this.monitored) { this.showStatusApp(msg, this.err); } } }]); return AsyncInit; }(); //*------------------------------------------------------------------------------ //* feature-life-cycle-hook: appDidStart(fassets, activeFeatures, additionalHooksParams): void //*------------------------------------------------------------------------------ op.flch.appDidStart = function (fassets, activeFeatures, additionalHooksParams) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.flch.appDidStart.executionOrder = executionOrder++; // log summary var hookCount = activeFeatures.reduce(function (count, feature) { return feature.appDidStart ? count + 1 : count; }, 0); var hookSummary = activeFeatures.map(function (feature) { return '\n Feature.name:' + feature.name + (feature.appDidStart ? ' <-- defines: appDidStart()' : ''); }); (0, _logf2.default)('feature-life-cycle-hook ... PROCESSING: Feature.appDidStart() ... ' + hookCount + ' hooks:' + hookSummary); // apply Feature.appDidStart() life-cycle hooks activeFeatures.forEach(function (feature) { if (feature.appDidStart) { (0, _logf2.default)('feature-life-cycle-hook ... Feature.name:' + feature.name + ' ... invoking it\'s defined Feature.appDidStart()'); feature.appDidStart(_extends({ fassets: fassets }, additionalHooksParams)); } }); };