feature-u
Version:
Feature Based Project Organization for React
553 lines (506 loc) • 20.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: 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 = createFeature;
exports.isFeatureProperty = isFeatureProperty;
exports.extendFeatureProperty = extendFeatureProperty;
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 _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 a new {{book.api.Feature}} object, cataloging
* {{book.api.AspectContent}} to be consumed by
* {{book.api.launchApp}}. Each feature within an application
* promotes it's own {{book.api.Feature}} object.
*
* For more information, please refer to
* {{book.guide.detail_featureAndAspect}}.
*
* **Please Note** this function uses named parameters.
*
* @param {string} name the identity of the feature. Feature names
* are guaranteed to be unique. Application code can use the Feature
* name in various **single-source-of-truth** operations _(see
* {{book.guide.bestPractices}})_.
*
* @param {boolean} [enabled=true] an indicator as to whether this
* feature is enabled (true) or not (false). When used, this
* indicator is typically based on a dynamic expression, allowing
* packaged code to be dynamically enabled/disabled at run-time
* _(please refer to: {{book.guide.enablement}})_.
*
* @param {fassets} [fassets]
* an optional aspect that promotes feature assets used in
* {{book.guide.crossCom}} (i.e. the Public Face of a feature).
* `fassets` directives can both define resources, and/or declare a
* resource contract (the intention to use a set of fasset resources).
* Resources are accumulated across all features, and exposed through
* the {{book.api.FassetsObject}}, and the {{book.api.withFassets}}
* HoC.
*
* @param {appWillStartCB} [appWillStart] an optional
* {{book.guide.appLifeCycle}} invoked one time, just before the app
* starts up. This life-cycle hook can do any type of initialization,
* and/or optionally supplement the app's top-level content (using a
* non-null return) _(please refer to: {{book.guide.appWillStart}})_.
*
* @param {appInitCB} [appInit] an optional
* {{book.guide.appLifeCycle}} invoked one time, later in the app
* startup process. This life-cycle hook supports blocking async
* initialization (by simply returning a promise) _(please refer to:
* {{book.guide.appInit}})_.
*
* @param {appDidStartCB} [appDidStart] an optional
* {{book.guide.appLifeCycle}} invoked one time, immediately after the
* app has started _(please refer to: {{book.guide.appDidStart}})_.
*
* @param {AspectContent} [extendedAspect] additional aspects, as
* defined by the feature-u's Aspect plugins (please refer to:
* {{book.guide.detail_extendableAspects}} -and-
* {{book.guide.extending}}).
*
* @return {Feature} a new Feature object (to be consumed by
* launchApp()).
*
* @function createFeature
*/
function createFeature() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var name = _ref.name,
_ref$enabled = _ref.enabled,
enabled = _ref$enabled === undefined ? true : _ref$enabled,
fassets = _ref.fassets,
appWillStart = _ref.appWillStart,
appInit = _ref.appInit,
appDidStart = _ref.appDidStart,
extendedAspect = _objectWithoutProperties(_ref, ['name', 'enabled', 'fassets', 'appWillStart', 'appInit', 'appDidStart']);
// validate createFeature() parameters
var check = _verify2.default.prefix('createFeature() parameter violation: ');
// ... name
check(name, 'name is required');
check((0, _lodash2.default)(name), 'name must be a string');
// ... enabled
check(enabled === true || enabled === false, 'enabled must be a boolean');
// ... fasset: validation occurs in createFasset()
// ... appWillStart
if (appWillStart) {
check((0, _lodash4.default)(appWillStart), 'appWillStart (when supplied) must be a function');
}
// ... appInit
if (appInit) {
check((0, _lodash4.default)(appInit), 'appInit (when supplied) must be a function');
}
// ... appDidStart
if (appDidStart) {
check((0, _lodash4.default)(appDidStart), 'appDidStart (when supplied) must be a function');
}
// ... extendedAspect
// ... this validation occurs by the Aspect itself (via launchApp())
// BECAUSE we don't know the Aspects in use UNTIL run-time (in launchApp)
// create/return our new Feature object
return _extends({
name: name,
enabled: enabled,
fassets: fassets,
appWillStart: appWillStart,
appInit: appInit,
appDidStart: appDidStart
}, extendedAspect);
}
/**
* Maintain all VALID Feature properties.
*
* This is used to restrict Feature properties to ONLY valid ones:
* - preventing user typos
* - validation is employed at run-time in launchApp()
*
* Initially seeded with Feature builtins.
*
* Later, supplemented with extendFeatureProperty(name, owner) at run-time
* (via Aspect plugins).
*
* @private
*/
var validFeatureProps = {
// owner id of extension (ex: aspect's npm package name)
// =========
name: 'builtin',
enabled: 'builtin',
publicFace: 'builtin', // OBSOLETE as of feature-u@1 ... still registered for the sole purpose of generating more specific error (see: createFassets.js)
fassets: 'builtin',
appWillStart: 'builtin',
appInit: 'builtin',
appDidStart: 'builtin'
};
/**
* Is the supplied name a valid Feature property?
*
* @param {string} name the property name to check.
*
* @param {boolean} true: valid Feature property,
* false: NOT a Feature property
*
* @private
*/
function isFeatureProperty(name) {
return validFeatureProps[name] ? true : false;
}
/**
* Extend valid Feature 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 allow.
*
* @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 extendFeatureProperty(name, owner) {
// validate parameters
var check = _verify2.default.prefix('extendFeatureProperty() 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 (isFeatureProperty(name) && // already reserved
validFeatureProps[name] !== owner) {
// by a different owner
throw new Error('**ERROR** extendFeatureProperty(\'' + name + '\', \'' + owner + '\') ... \'Feature.' + name + '\' is already reserved by different owner.');
}
// reserve it
validFeatureProps[name] = owner;
(0, _logf2.default)('invoking: extendFeatureProperty(\'' + name + '\', \'' + owner + '\') ... now validFeatureProps: ', validFeatureProps);
}
//***
//*** Specification: Feature
//***
/**
* @typedef {Object} Feature
*
* The Feature object is merely a lightweight container that holds
* {{book.api.AspectContent}} of interest to **feature-u**.
*
* Each feature within an application promotes a Feature object (using
* {{book.api.createFeature}}) which catalogs the aspects of that feature.
*
* Ultimately, all Feature objects are consumed by
* {{book.api.launchApp}}.
*
* Feature content are simple key/value pairs (the key being an
* Aspect.name with values of AspectContent). These aspects can
* either be **built-in** (from core **feature-u**), or **extensions**.
*
* Here is an example:
*
* ```js
* export default createFeature({
* name: 'featureA', // builtin aspect (name must be unique across all features within app)
* enabled: true, // builtin aspect enabling/disabling feature
*
* fassets: { // builtin aspect promoting Public Face - Cross Feature Communication
* define: {
* 'api.openA': () => ...,
* 'api.closeA': () => ...,
* },
* },
*
* appWillStart: (...) => ..., // builtin aspect (Application Life Cycle Hook)
* appInit: (...) => ..., // ditto
* appDidStart: (...) => ..., // ditto
*
* reducer: ..., // feature redux reducer (extended aspect from the feature-redux plugin)
* logic: ..., // feature logic modules (extended aspect from the feature-redux-logic plugin)
* });
* ```
*
* For more information, please refer to
* {{book.guide.detail_featureAndAspect}}.
*/
//***
//*** Specification: appWillStartCB
//***
/**
* An optional {{book.guide.appLifeCycle}} invoked one time, very
* early in the app startup process.
*
* This life-cycle hook can do any type of general app-specific
* initialization _(for example initializing a **PWA service
* worker**)_.
*
* In addition, it can optionally inject static content in the app's
* DOM root. Any return is interpreted as the app's new `rootAppElm`
* _(an accumulative process)_. **IMPORTANT**: When this is used, the
* supplied `curRootAppElm` MUST be included as part of this
* definition (accommodating the accumulative process of other feature
* injections)! **More information is available at
* {{book.guide.injectingDomContent}}**
*
* For more information _(with examples)_, please refer to the
* Guide's {{book.guide.appWillStart}}.
*
* **Please Note** this function uses named parameters.
*
* @callback appWillStartCB
*
* @param {Fassets} fassets the Fassets object used in cross-feature-communication.
*
* @param {reactElm} curRootAppElm - the current react app element
* root.
*
* @return {reactElm|void} optionally, new top-level content (which in turn
* must contain the supplied `curRootAppElm`). Use a void return
* when top-level content is unchanged.
*/
//***
//*** Specification: appInitCB
//***
/**
* An optional {{book.guide.appLifeCycle}} invoked one time, later in
* the app startup process. It supports blocking async
* initialization.
*
* This hook is invoked when the app is **nearly up-and-running**.
*
* - The {{book.guide.detail_reactRegistration}} has already occurred
* _(via the {{book.api.registerRootAppElmCB}} callback)_. As a
* result, you can rely on utilities that require an app-specific
* `rootAppElm` to exist.
*
* - You have access to the `getState()` and `dispatch()` function,
* assuming you are using {{book.ext.redux}} (when detected by
* **feature-u**'s plugable aspects).
*
* These parameters are actually injected by the
* {{book.ext.featureRedux}} Aspect, and are examples of what can be
* injected by any Aspect _(please refer your specific Aspect's
* documentation to determine other parameters)_.
*
* Just like the {{book.api.appWillStartCB}} hook, you may perform any
* type of general initialization that is required by your feature.
*
* However the **hallmark of this hook** is **you can block for any
* asynchronous initialization to complete**. By simply returning a
* promise, **feature-u** will wait for the process to complete.
*
* The user is kept advised of any long-running async processes. By
* default an `'initializing feature: {feature.name}'` message is
* used, but you can customize it through the supplied
* {{book.api.showStatusCB}} function parameter.
*
* For more info with examples, please see the Guide's
* {{book.guide.appInit}}.
*
* **Please Note** this function uses named parameters.
*
* @callback appInitCB
*
* @param {showStatusCB} showStatus the function that (when invoked)
* will communicate a blocking "persistent" status message to the end
* user.
*
* @param {Fassets} fassets the Fassets object used in cross-feature-communication.
*
* @param {Any} [getState] - the redux function returning the top-level
* app state (when redux is in use).
*
* @param {function} [dispatch] - the redux dispatch() function (when
* redux is in use).
*
* @param {any} [injectedAspectParams] - additional parameters
* injected by Aspect plugins _(please refer your specific Aspect's
* documentation to determine other parameters)_. The `getState` and
* `dispatch` params (above) are examples of this.
*
* @return {Promise|void} optionally, a promise (for asynchronous
* processes) - and feature-u will wait for the process to complete.
* Use a void return (for synchronous processes) - and no blocking
* will occur.
*/
//***
//*** Specification: appDidStartCB
//***
/**
* An optional {{book.guide.appLifeCycle}} invoked one time,
* once the app startup process has completed.
*
* This life-cycle hook can be used to trigger **"the app is
* running"** events. A typical usage is to **"kick start"** some
* early application logic.
*
* Because the app is up-and-running at this time, you have access to
* the `getState()` and `dispatch()` function ... assuming you are using
* {{book.ext.redux}} (when detected by **feature-u**'s plugable aspects).
* These parameters are actually injected by the
* {{book.ext.featureRedux}} Aspect, and are examples of what can be
* injected by any Aspect _(please refer your specific Aspect's
* documentation to determine other parameters)_.
*
* For more info with examples, please see the Guide's
* {{book.guide.appDidStart}}.
*
* **Please Note** this function uses named parameters.
*
* @callback appDidStartCB
*
* @param {Fassets} fassets the Fassets object used in cross-feature-communication.
*
* @param {Any} [getState] - the redux function returning the top-level
* app state (when redux is in use).
*
* @param {function} [dispatch] - the redux dispatch() function (when
* redux is in use).
*
* @param {any} [injectedAspectParams] - additional parameters
* injected by Aspect plugins _(please refer your specific Aspect's
* documentation to determine other parameters)_. The `getState` and
* `dispatch` params (above) are examples of this.
*
* @return void
*/
//***
//*** Specification: fassets
//***
/**
* @typedef {BuiltInAspect} fassets
*
* A builtin aspect that publicly promotes feature-based resources
* called `fassets` (feature assets). These resources are the basis
* of {{book.guide.crossCom}}. You can think of this as the Public Face
* of a feature.
*
* **SideBar**: The term `fassets` is a play on words. While it is
* pronounced "facet" _and is loosely related to this term_, it is
* spelled fassets (i.e. feature assets).
*
* Feature resources are accumulated across all features, and exposed
* through the {{book.api.FassetsObject}}. They can also be referenced
* via the {{book.api.withFassets}} HoC.
*
* The `fassets` aspect can both define resources, and/or declare a
* resource contract (i.e. the intention to use a set of fasset
* resources). This is accomplished via three separate `fassets`
* directives: `define`, `use`, and `defineUse`. A good summary of
* these directives can be found at
* {{book.guide.crossCom_fassetsRecapPushOrPull}}.
*
* 1. **define**: define public resources, held in the
* {{book.api.FassetsObject}}
*
* ```js
* fassets: {
* define: {
* '{fassetsKey}': {fassetsValue}
*
* ...
*
* NOTES:
* - fassetsKey MUST be unique
* - are case-sensitive
* - may contain federated namespace (via dots ".")
* ... normalized in fassets object
* ... ex: 'MainPage.launch'
* - may be any valid JS identifier (less $ support)
* - may NOT contain wildcards
* ... i.e. must be defined completely
*
* // examples ...
* 'openView': actions.view.open, // fassets.openView(viewName): Action
*
* // federated namespace example
* 'selector.currentView': selector.currentView, // fassets.selector.currentView(appState): viewName
*
* // UI Component example
* 'MainPage.cart.link': () => <Link to="/cart">Cart</Link>,
* 'MainPage.cart.body': () => <Route path="/cart" component={ShoppingCart}/>,
* }
* }
* ```
*
* 2. **use**: specify public resource keys that will be **used** by the
* containing feature (i.e. a resource contract)
*
* ```js
* fassets: {
* use: [
* '{fassetsKey}',
* -or-
* ['$fassetsKey', {required: true/false, type: $validationFn}],
*
* ...
*
* NOTES:
* - each key will be supplied by other features
* - this is a communication to other features (i.e. a contract)
* ... saying: I plan to "use" these injections
* HOWEVER: feature-u cannot strictly enforce this usage
* ... enclosed feature should reference this
* {fassetsKey} through fassets.get(), or withFassets()
* - is case-sensitive
* - may contain federated namespace (with dots ".")
* ... ex: 'MainPage.launch'
* - may be any valid JS identifier (less $ support)
* - may contain wildcards (with "*")
* ... ex: 'MainPage.*.link'
*
* // examples ...
* 'MainPage.launch',
*
* // may contain wildcards ...
* 'MainPage.*.link',
* 'MainPage.*.body',
*
* // optionally supply options object, controlling optionality and data types
* ['MainPage.*.link', { required: true, type: any }], // same as DEFAULTS
* ['MainPage.*.link', { required: false, }], // optional of any type
* ['MainPage.*.link', { type: comp }], // required of react component type
* ['MainPage.*.link', { required: false, type: comp }], // optional of react component type
* ]
* }
* ```
*
* 3. **defineUse**: define public resources specified by other features (via
* the `use` directive)
*
* ```js
* fassets: {
* defineUse: {
* '{fassetsKey}': {fassetsValue}
*
* ...
*
* NOTES:
* - this is identical to fassets.define EXCEPT:
* - it MUST MATCH a fassets.use directive
* ... using this directive, feature-u will perform additional
* validation to unsure these entries match a use contract
*
* // examples ...
* 'MainPage.cart.link': () => <Link to="/cart">Cart</Link>,
* 'MainPage.cart.body': () => <Route path="/cart" component={ShoppingCart}/>,
* }
* }
* ```
*
* For more information, please refer to {{book.guide.crossCom}},
* {{book.api.FassetsObject}}, the {{book.api.withFassets}} HoC,
* and the {{book.guide.crossCom_fassetsRecapPushOrPull}}.
*/