UNPKG

feature-u

Version:

Feature Based Project Organization for React

553 lines (506 loc) 20.3 kB
'use strict'; 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}}. */