feature-u
Version:
Feature Based Project Organization for React
1,019 lines (814 loc) • 44.5 kB
JavaScript
;
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));
}
});
};