feature-u
Version:
Feature Based Project Organization for React
653 lines (590 loc) • 24.8 kB
JavaScript
'use strict';
exports.__esModule = true;
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 = createAspect;
exports.isAspectProperty = isAspectProperty;
exports.extendAspectProperty = extendAspectProperty;
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);
var _createFeature = require('../core/createFeature');
var _logf = require('../util/logf');
var _logf2 = _interopRequireDefault(_logf);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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; }
/**
* Create an {{book.api.Aspect}} object, used to extend **feature-u**.
*
* The {{book.api.Aspect}} object promotes a series of life-cycle
* methods that **feature-u** invokes in a controlled way. This
* life-cycle is controlled by {{book.api.launchApp}} _... it is
* supplied the Aspects, and it invokes their methods._
*
* The essential characteristics of a typical {{book.api.Aspect}}
* life-cycle is to:
*
* - accumulate {{book.api.AspectContent}} across all features
* - perform the desired setup and configuration
* - expose the framework in some way _(by injecting a component in the
* root DOM, or some {{book.guide.extending_aspectCrossCommunication}}
* mechanism)_
*
* The {{book.guide.extending}} section provides more insight on how
* {{book.api.Aspect}}s are created and used.
*
* Aspect Plugins have NO one specific method that is required. Rather
* the requirement is to **specify something** _(so as to not have an
* empty plugin that does nothing)_.
* Please refer to the **"No Single Aspect Method
* is Required"** discussion in the
* {{book.guide.extending_aspectLifeCycleMethods}}.
*
* **Please Note** this function uses named parameters. The order in
* which these items are presented represents the same order they are
* executed.
*
* @param {string} name the `Aspect.name` is used to "key"
* {{book.api.AspectContent}} of this type in the {{book.api.Feature}}
* object. <br/>
*
* For example: an `Aspect.name: 'xyz'` would permit a `Feature.xyz:
* xyzContent` construct.<br/>
*
* As a result, Aspect names cannot clash with built-in aspects, and
* they must be unique _(across all aspects that are in-use)_.<br/>
*
* The `Aspect.name` is required, primarily for identity
* purposes _(in logs and such)_.
*
* @param {genesisMeth} [genesis] a Life Cycle Hook invoked
* one time, at the very beginning of the app's start up process.
* This hook can perform Aspect related **initialization** and
* **validation**:
*
* @param {validateFeatureContentMeth} [validateFeatureContent] a
* validation hook allowing this aspect to verify it's content on the
* supplied feature (which is known to contain this aspect).
*
* @param {expandFeatureContentMeth} [expandFeatureContent] an
* aspect expansion hook, defaulting to the algorithm defined
* by {{book.api.expandWithFassets}}.<br/>
*
* This function rarely needs to be overridden. It provides a hook to
* aspects that need to transfer additional content from the expansion
* function to the expanded content.
*
* @param {assembleFeatureContentMeth} [assembleFeatureContent] the
* Aspect method that assembles content for this aspect across all
* features, retaining needed state for subsequent ops.<br/>
*
* This method is typically the primary task that is accomplished by
* most aspects.
*
* @param {assembleAspectResourcesMeth} [assembleAspectResources] an
* Aspect method that assemble resources for this aspect
* across all other aspects, retaining needed state for subsequent
* ops.<br/>
*
* This hook is executed after all the aspects have assembled their
* feature content (i.e. after
* {{book.api.assembleFeatureContentMeth}}).
*
* @param {initialRootAppElmMeth} [initialRootAppElm] a
* callback hook that promotes some characteristic of this aspect
* within the `rootAppElm` ... the top-level react DOM that represents
* the display of the entire application.<br/>
*
* The {{book.guide.extending_definingAppElm}} section highlights when
* to use {{book.api.initialRootAppElmMeth}} verses
* {{book.api.injectRootAppElmMeth}}.
*
* @param {injectRootAppElmMeth} [injectRootAppElm] a
* callback hook that promotes some characteristic of this aspect
* within the `rootAppElm` ... the top-level react DOM that represents
* the display of the entire application.<br/>
*
* The {{book.guide.extending_definingAppElm}} section highlights when
* to use {{book.api.initialRootAppElmMeth}} verses
* {{book.api.injectRootAppElmMeth}}.
*
* @param {injectParamsInHooksMeth} [injectParamsInHooks] an
* Aspect method that promotes `namedParams` into the
* feature's {{book.guide.appLifeCycles}}, from this aspect.<br/>
* This hook is executed after all aspects have assembled their
* feature content (i.e. after
* {{book.api.assembleFeatureContentMeth}}).
*
* @param {Any} [config] a sub-object that can be used for
* any type of configuration that a specific Aspect may need _(see:
* {{book.guide.aspectConfig}})_.
*
* @param {Any} [additionalMethods] additional methods (proprietary to
* specific Aspects), supporting
* {{book.guide.extending_aspectCrossCommunication}} ... a contract
* between one or more aspects _(see:
* {{book.guide.additionalMethods}})_.
*
* @return {Aspect} a new Aspect object (to be consumed by {{book.api.launchApp}}).
*
* @function createAspect
*/
function createAspect() {
var namedParams = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
// ***
// *** validate parameters
// ***
var check = _verify2.default.prefix('createAspect() parameter violation: ');
// ... namedParams
check((0, _lodash6.default)(namedParams), 'only named parameters may be supplied');
// descturcture our individual namedParams
// NOTE 1: 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 :-(
// NOTE 2: We no-op critical methods that are assumed to exist throughout the code-base.
// ... by NOT requiring these critical methods, we support edge cases where
// aspect content is not needed from the feature set
// (ex: a plugin "adds value" to another plugin).
var name = namedParams.name,
genesis = namedParams.genesis,
_namedParams$validate = namedParams.validateFeatureContent,
validateFeatureContent = _namedParams$validate === undefined ? noOp : _namedParams$validate,
expandFeatureContent = namedParams.expandFeatureContent,
_namedParams$assemble = namedParams.assembleFeatureContent,
assembleFeatureContent = _namedParams$assemble === undefined ? noOp : _namedParams$assemble,
assembleAspectResources = namedParams.assembleAspectResources,
initialRootAppElm = namedParams.initialRootAppElm,
injectRootAppElm = namedParams.injectRootAppElm,
injectParamsInHooks = namedParams.injectParamsInHooks,
_namedParams$config = namedParams.config,
config = _namedParams$config === undefined ? {} : _namedParams$config,
additionalMethods = _objectWithoutProperties(namedParams, ['name', 'genesis', 'validateFeatureContent', 'expandFeatureContent', 'assembleFeatureContent', 'assembleAspectResources', 'initialRootAppElm', 'injectRootAppElm', 'injectParamsInHooks', 'config']);
// ... name (NOTE: name check takes precedence to facilitate `Aspect.name` identity in subsequent errors :-)
check(name, 'name is required (at minimum for identity purposes)');
check((0, _lodash2.default)(name), 'name must be a string');
check(!(0, _createFeature.isFeatureProperty)(name), 'Aspect.name: \'' + name + '\' is a reserved Feature keyword');
// NOTE: Aspect.name uniqueness is validated in launchApp() (once we know all aspects in-use)
// ... unrecognized positional parameter
// NOTE: when defaulting entire struct, arguments.length is 0
check(arguments.length <= 1, 'Aspect.name:' + name + ' ... unrecognized positional parameters (only named parameters can be specified) ... ' + arguments.length + ' positional parameters were found');
// ... for all method params (when supplied BY client),
// - verify they are functions -AND-
// - total how many were supplied
var totalMethodsSupplied = ['genesis', 'validateFeatureContent', 'expandFeatureContent', 'assembleFeatureContent', 'assembleAspectResources', 'initialRootAppElm', 'injectRootAppElm', 'injectParamsInHooks'].reduce(function (accum, paramName) {
var param = namedParams[paramName]; // get original param supplied by client (does NOT reflect the defaults above)
if (param) {
check((0, _lodash4.default)(param), 'Aspect.name:' + name + ' ... ' + paramName + ' (when supplied) must be a function');
accum++;
}
return accum;
}, 0);
// ... config
check(config, 'Aspect.name:' + name + ' ... config is required');
check((0, _lodash6.default)(config), 'Aspect.name:' + name + ' ... config must be a plain object literal');
// ... additionalMethods
// ... this validation occurs in launchApp()
// BECAUSE we don't know the Aspects in use UNTIL run-time
// ... must specify at least ONE method param
// so as to not have an empty plugin that does nothing
check(totalMethodsSupplied > 0, 'Aspect.name:' + name + ' ... at least one method must be supplied ... an empty Aspect plugin does nothing!');
// ***
// *** return our new Aspect object
// ***
return _extends({
name: name,
genesis: genesis,
validateFeatureContent: validateFeatureContent,
expandFeatureContent: expandFeatureContent,
assembleFeatureContent: assembleFeatureContent,
assembleAspectResources: assembleAspectResources,
initialRootAppElm: initialRootAppElm,
injectRootAppElm: injectRootAppElm,
injectParamsInHooks: injectParamsInHooks,
config: config
}, additionalMethods);
}
// no-op method for critical methods that are assumed to exist throughout the code-base
function noOp() {}
/**
* Maintain all VALID Aspect properties.
*
* This is used to restrict Aspect properties to ONLY valid ones:
* - preventing user typos
* - validation is employed at run-time in launchApp()
*
* Initially seeded with Aspect builtins.
*
* Later, supplemented with extendAspectProperty(name, owner) at run-time
* (via Aspect plugins).
*
* @private
*/
var validAspectProps = {
name: 'builtin',
genesis: 'builtin',
validateFeatureContent: 'builtin',
expandFeatureContent: 'builtin',
assembleFeatureContent: 'builtin',
assembleAspectResources: 'builtin',
initialRootAppElm: 'builtin',
injectRootAppElm: 'builtin',
injectParamsInHooks: 'builtin',
config: 'builtin'
};
/**
* Is the supplied name a valid Aspect property?
*
* @param {string} name the property name to check.
*
* @param {boolean} true: valid Aspect property,
* false: NOT a Aspect property
*
* @private
*/
function isAspectProperty(name) {
return validAspectProps[name] ? true : false;
}
/**
* Extend valid Aspect properties to include the supplied name
* ... used when extending APIs for
* {{book.guide.extending_aspectCrossCommunication}}.
*
* **feature-u** keeps track of the agent that owns this extension
* (using the owner parameter). This is used to prevent exceptions
* when duplicate extension requests are made by the same owner. This
* can happen when multiple instances of an aspect type are supported,
* and also in unit testing.
*
* @param {string} name the property name to extend.
*
* @param {string} owner the requesting owner id of this extension
* request. Use any string that uniquely identifies your utility
* _(such as the aspect's npm package name)_.
*
* @throws {Error} when supplied name is already reserved by a different owner
*/
function extendAspectProperty(name, owner) {
// validate parameters
var check = _verify2.default.prefix('extendAspectProperty() parameter violation: ');
check(name, 'name is required');
check((0, _lodash2.default)(name), 'name must be a string');
check(owner, 'owner is required');
check((0, _lodash2.default)(owner), 'owner must be a string');
// verify supplied name is NOT already reserved (by a different owner)
if (isAspectProperty(name) && // already reserved
validAspectProps[name] !== owner) {
// by a different owner
throw new Error('**ERROR** extendAspectProperty(\'' + name + '\', \'' + owner + '\') ... \'Aspect.name:' + name + '\' is already reserved by different owner.');
}
// reserve it
validAspectProps[name] = owner;
(0, _logf2.default)('invoking: extendAspectProperty(\'' + name + '\', \'' + owner + '\') ... now validAspectProps: ', validAspectProps);
}
//***
//*** Specification: Aspect
//***
/**
* @typedef {Object} Aspect
*
* Aspect objects (emitted from {{book.api.createAspect}}) are used to
* extend **feature-u**.
*
* The Aspect object promotes a series of life-cycle methods that
* **feature-u** invokes in a controlled way. This life-cycle is
* controlled by {{book.api.launchApp}} _... it is supplied the
* Aspects, and it invokes their methods._
*
* Typically Aspects are packaged separately _(as an external npm
* **feature-u** extension)_, although they can be created locally
* within a project _(if needed)_.
*
* For more information, please refer to
* {{book.guide.detail_extendableAspects}} and
* {{book.guide.extending}}.
*/
//***
//*** Specification: AspectContent
//***
/**
* @typedef {Any} AspectContent
*
* The content (or payload) of an {{book.api.Aspect}}, specified
* within a {{book.api.Feature}}.
*
* The content type is specific to the Aspect. For example, a redux
* Aspect assembles reducers (via `Feature.reducer`), while a
* redux-logic Aspect gathers logic modules (via `Feature.logic`),
* etc.
*
* AspectContent can either be defined from **built-in** aspects
* _(via core **feature-u**)_, or **extensions** _(from
* {{book.api.Aspect}})_.
*
* An {{book.api.Aspect}} object extends **feature-u** by accumulating
* information of interest from {{book.api.Feature}} objects _(indexed
* by the Aspect name)_.
*
* **Note**: Whenever AspectContent definitions require the
* {{book.api.FassetsObject}} **at code expansion time**, you can wrap the
* definition in a {{book.api.expandWithFassets}} function. In other
* words, your aspect content can either be the actual content itself
* _(ex: a reducer)_, or a function that returns the content.
*
* For more information, please refer to
* {{book.guide.detail_featureAndAspect}}.
*/
//***
//*** Specification: genesisMeth
//***
/**
* A Life Cycle Hook invoked one time, at the very beginning of
* the app's start up process.
*
* **Antiquated Note**:
*
* - The `genesis()` hook is somewhat antiquated, relegated to Aspects
* that are promoted as singletons. In this scenario, client-side
* configuration could be introduced after instantiation _(by adding
* content to {{book.guide.aspectConfig}})_, while still allowing
* **initialization** and **validation** to occur early in the startup
* process _(via this `genesis()` hook)_.
*
* - A better alternative to the `genesis()` hook is to promote your
* {{book.guide.extending_customAspectPlugins}} as non-singletons,
* where **initialization** and **validation** can be directly promoted
* though the plugin constructor.
*
* The `genesis()` hook can perform Aspect related **initialization** and
* **validation**:
*
* - **initialization**: It is possible to to register proprietary
* Aspect/Feature APIs in the `genesis()` hook ... via
* {{book.api.extendAspectProperty}} and
* {{book.api.extendFeatureProperty}} _(please see:
* {{book.guide.extending_aspectCrossCommunication}} and
* {{book.guide.crossCom}})_.
*
* The preferred place to do this initialization is in the plugin
* constructor _(see **Antiquated Note** above)_.
*
* - **validation**: It is possible to perform Aspect validation in the
* `genesis()` hook ... say for required configuration properties
* injected by the client after instantiation. This is the reason for
* the optional return string.
*
* The preferred place to do validation is in the plugin constructor,
* gathering this information as constructor parameters _(see
* **Antiquated Note** above)_.
*
*
* **API:** {{book.api.genesisMeth$}}
*
* @callback genesisMeth
*
* @return {string} an error message when self is in an invalid state
* (falsy when valid). Because this validation occurs under the
* control of {{book.api.launchApp}}, any message is prefixed with:
* `'launchApp() parameter violation: '`.
*/
//***
//*** Specification: validateFeatureContentMeth
//***
/**
* A validation hook allowing this aspect to verify it's content on
* the supplied feature.
*
* **API:** {{book.api.validateFeatureContentMeth$}}
*
* @callback validateFeatureContentMeth
*
* @param {Feature} feature - the feature to validate, which is known
* to contain this aspect.
*
* @return {string} an error message string when the supplied feature
* contains invalid content for this aspect (falsy when valid).
* Because this validation conceptually occurs under the control of
* {{book.api.createFeature}}, any message is prefixed with:
* `'createFeature() parameter violation: '`.
*/
//***
//*** Specification: expandFeatureContentMeth
//***
/**
* Expand self's {{book.api.AspectContent}} in the supplied feature,
* replacing that content (within the feature). Once expansion is
* complete, **feature-u** will perform a delayed validation of the
* expanded content.
*
* **API:** {{book.api.expandFeatureContentMeth$}}
*
* The default behavior simply implements the expansion algorithm
* defined by {{book.api.expandWithFassets}}:
*
* ```js
* feature[this.name] = feature[this.name](fassets);
* ```
*
* This default behavior rarely needs to change. It however provides
* a hook for aspects that need to transfer additional content from
* the expansion function to the expanded content. As an example, the
* `reducer` aspect must transfer the slice property from the
* expansion function to the expanded reducer.
*
* @callback expandFeatureContentMeth
*
* @param {Fassets} fassets the Fassets object used in feature
* cross-communication.
*
* @param {Feature} feature - the feature which is known to contain
* this aspect **and** is in need of expansion (as defined by
* {{book.api.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
* {{book.api.validateFeatureContentMeth}} hook.
*/
//***
//*** Specification: assembleFeatureContentMeth
//***
/**
* The Aspect method that assembles content for this aspect
* across all features, retaining needed state for subsequent ops.
* This method is typically the primary task that is accomplished by
* most aspects.
*
* **API:** {{book.api.assembleFeatureContentMeth$}}
*
* @callback assembleFeatureContentMeth
*
* @param {Fassets} fassets the Fassets object used in feature
* cross-communication.
*
* @param {Feature[]} activeFeatures - The set of active (enabled)
* features that comprise this application.
*
* @return void
*/
//***
//*** Specification: assembleAspectResourcesMeth
//***
/**
* An Aspect method that assembles resources for this aspect
* across all other aspects, retaining needed state for subsequent
* ops. This hook is executed after all the aspects have assembled
* their feature content (i.e. after
* {{book.api.assembleFeatureContentMeth}}).
*
* **API:** {{book.api.assembleAspectResourcesMeth$}}
*
* This is an optional second-pass (so-to-speak) of Aspect data
* gathering, that facilitates
* {{book.guide.extending_aspectCrossCommunication}}. It allows an
* extending aspect to gather resources from other aspects, using an
* additional API (ex: `Aspect.getXyz()`).
*
* @callback assembleAspectResourcesMeth
*
* @param {Fassets} fassets the Fassets object used in feature
* cross-communication.
*
* @param {Aspect[]} aspects - The set of **feature-u** Aspect objects
* used in this this application.
*
* @return void
*/
//***
//*** Specification: initialRootAppElmMeth
//***
/**
* A callback hook that promotes some characteristic of this
* aspect within the `rootAppElm` ... the top-level react DOM that
* represents the display of the entire application.
*
* **API:** {{book.api.initialRootAppElmMeth$}}
*
* The {{book.guide.extending_definingAppElm}} section highlights when
* to use {{book.api.initialRootAppElmMeth}} verses
* {{book.api.injectRootAppElmMeth}}.
*
* **NOTE**: When this hook is used, the supplied curRootAppElm MUST be
* included as part of this definition!
*
* @callback initialRootAppElmMeth
*
* @param {Fassets} fassets the Fassets object used in feature
* cross-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).
*/
//***
//*** Specification: injectRootAppElmMeth
//***
/**
* A callback hook that promotes some characteristic of this
* aspect within the `rootAppElm` ... the top-level react DOM that
* represents the display of the entire application.
*
* **API:** {{book.api.injectRootAppElmMeth$}}
*
* The {{book.guide.extending_definingAppElm}} section highlights when
* to use {{book.api.initialRootAppElmMeth}} verses
* {{book.api.injectRootAppElmMeth}}.
*
* **NOTE**: When this hook is used, the supplied curRootAppElm MUST be
* included as part of this definition!
*
* @callback injectRootAppElmMeth
*
* @param {Fassets} fassets the Fassets object used in feature
* cross-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).
*/
//***
//*** Specification: injectParamsInHooksMeth
//***
/**
* An Aspect method that promotes `namedParams` into the
* feature's {{book.guide.appLifeCycles}}, from this aspect. This
* hook is executed after all aspects have assembled their feature
* content (i.e. after {{book.api.assembleFeatureContentMeth}}).
*
* Here is a `namedParams` example from a redux aspect, promoting it's
* state and dispatch functions:
*
* ```js
* {getState, dispatch}
* ```
*
* **API:** {{book.api.injectParamsInHooksMeth$}}
*
* Any aspect may promote their own set of `namedParams`. **feature-u**
* will insure there are no name clashes across aspects (which results
* in an exception). If your parameter names have a high potential
* for clashing, a **best practice** would be to qualify them in some
* way to better insure uniqueness.
*
* @callback injectParamsInHooksMeth
*
* @param {Fassets} fassets the Fassets object used in feature
* cross-communication.
*
* @return {namedParams} a plain object that will be injected (as
* named parameters) into the feature's {{book.guide.appLifeCycles}},
* from this aspect.
*/