UNPKG

feature-u

Version:

Feature Based Project Organization for React

1,089 lines (929 loc) 47.2 kB
'use strict'; exports.__esModule = true; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); exports.default = createFassets; exports.containsWildCard = containsWildCard; exports.matchAll = matchAll; exports.isMatch = isMatch; exports.createRegExp = createRegExp; var _verify = require('../util/verify'); var _verify2 = _interopRequireDefault(_verify); var _lodash = require('lodash.isstring'); var _lodash2 = _interopRequireDefault(_lodash); var _lodash3 = require('lodash.isplainobject'); var _lodash4 = _interopRequireDefault(_lodash3); var _lodash5 = require('lodash.isfunction'); var _lodash6 = _interopRequireDefault(_lodash5); var _fassetValidations = require('./fassetValidations'); var _fassetValidations2 = _interopRequireDefault(_fassetValidations); var _mySpace = require('../util/mySpace'); 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; } function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } /** * An internal creator of the {{book.api.FassetsObject}}. Accumulates * the {{book.api.fassetsAspect}} promoted by the set of supplied * `activeFeatures`. * * @param {Feature[]} activeFeatures the active features from which we * accumulate feature assets through the {{book.api.fassetsAspect$}}. * * @return {Fassets} a new Fassets object (promoted by launchApp()). * * @private * * @function createFassets */ function createFassets(activeFeatures) { // PRIVATE: active features (used in hasFeature()) var _hasFeature = {/* dynamically maintained ... SAMPLE: feature1: true, feature2: true, ... */ }; // PRIVATE: fassets resources (with meta data) // - defined via Feature.fassets.define/defineUse var _resources = {/* dynamically maintained ... SAMPLE: '{fassetsKey}': { // ex: 'action.openView' ... NO wildcards allowed val: {whatever}, // resource value definingFeature: {featureName}, // the feature defining this resource defineUse: boolean, // directive used in defining this resource (false: define. true: defineUse) }, ... */ }; // PRIVATE: a string blob of all resolved fassetKeys delimited by new-line // - used by fassets.get() method to fetch all keys matching a regexp // - maintained in feature expansion order // ... the same order fassets.get() exposes multiple entries // - highly optimal technique, where a single regexp search // is used per fassets.get(wildcard) // ... for even better performance, get() also caches it's results!! var _fassetsKeysBlob = ''; // PRIVATE: fassets usage contract (with meta data) // - defined via Feature.fassets.use var _usage = {/* dynamically maintained ... SAMPLE: '{useKey}': { // ex: 'MainPage.*.link' ... wildcards allowed usingWildCard: true/false // does this usage contract employ wildcards? required: true/false, // optionality (required takes precedence for multiple entries) validateFn: func, // validation function (based on registered keywords) definingFeatures: [{featureName}], // what feature(s) defined this "use" contract }, ... */ }; // PRIVATE: cache all fassets.get() searches, providing a significant optimization // - especially in the context of React Component usage, // which repeats frequently within each UI render // - this is feasible because fasset resources are pre-loaded up-front, // and these set of resources will not change var _searchCache = {/* dynamically maintained ... SAMPLE: 'MainPage.*.link': [...results], 'MainPage.*.body': [], // empty array for no results 'selector.currentView': result, 'selector.currentView': UNDEFINED, // special UNDEFINED for no results (to distguish from entry NOT in cache) ... */ }; // PRIVATE: special value used in cache to allow not-found (undefined) entry // to be cached in a recognizable way var UNDEFINED = 'UNDEFINED'; /** * @typedef {Object} Fassets * * The `fassets` object _(emitted from {{book.api.launchApp}})_ is * an accumulation of **public feature assets gathered from all * features**. It facilitates {{book.guide.crossCom}} by promoting * the public resources of any given 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). * * There are 3 different ways to reference the resources contained * in the `fassets` object: * * 1. You may directly dereference them. As an example, an * '`action.openView`' resource can be dereferenced as follows: * * ```js * fassets.action.openView('mainView'); * ``` * * 2. You may use the {{book.api.Fassets_get}} method, which * can collect multiple resources (using {{book.guide.crossCom_wildcards}}). * * 3. Your UI components may indirectly access `fassets` resources * through the {{book.api.withFassets}} Higher-order Component * (HoC). * * **SideBar**: There are several ways to get a handle to the * `fassets` object _(see * {{book.guide.crossCom_obtainingFassetsObject}})_. * * For more information, please refer to {{book.guide.crossCom}} and * {{book.guide.crossCom_fassetsBasics}}. */ var _fassets = { // *** // *** normalized fassets accumulation over all features // *** /* dynamically maintained ... SAMPLE: MainPage: { cart: { link: () => <Link to="/cart">Cart</Link>, body: () => <Route path="/cart" component={ShoppingCart}/>, }, search: { link: () => <Link to="/search">Search</Link>, body: () => <Route path="/search" component={Search}/>, }, }, ... */ // *** // *** pre-defined API (methods) // *** /** * Get (i.e. fetch) the resource(s) corresponding to the supplied * `fassetsKey`. * * The `fassets.get()` method is an alternative to directly * dereferencing the `fassets` object ... the advantage being: * * 1. it can accumulate a series of resources (when * {{book.guide.crossCom_wildcards}} are used) * * 2. and it can more gracefully return undefined at any level * within the federated namespace path * * Regarding the `fassetsKey`: * * - It is case-sensitive _(as are the defined resources)_. * * - It may contain {{book.guide.crossCom_wildcards}} (`*`), resulting in a * multiple resources being returned (a resource array), matching the * supplied pattern * * - Matches are restricted to the actual fassetKeys registered through * the {{book.api.fassetsAspect}} `define`/`defineUse` directives. In * other words, the matching algorithm will **not** drill into the * resource itself (assuming it is an object with depth). * * - The special **dot** keyword (`'.'`) will return the fassets * object itself _(in the same tradition as "current directory")_. * * - `'@withKeys'`: * * In some cases, you may wish to know the corresponding * `fassetsKey` of the returned resource. This is especially true * when multiple resources are returned _(using wildcards)_. As * an example, JSX requires unique keys for array injections _(the * `fassetsKey` is a prime candidate for this, since it is * guaranteed to be unique)_. * * To accomplish this, simply suffix the `fassetsKey` with the * keyword: `'@withKeys'`. When this is encountered, the * resource returned is a two-element array: `[fassetsKey, * resource]`. _**SideBar**: We use this suffix technique (as * opposed to an additional parameter) to be consistent with how * {{book.api.withFassets}} operates._ * * **SideBar**: The `fassets.get()` method is the basis of both the * {{book.api.useFassets}} {{book.ext.reactHook}}, and the * {{book.api.withFassets}} Higher-order Component (HoC). * * @param {string} fassetsKey the key of the resource(s) to fetch. * This may include wildcards (`*`), as well as the `'@withKeys'` * suffix _(see discussion above)_. * * @return {resource|resource[]} the requested fassets resource(s). * * - **without wildcards**, a single resource is returned * _(`undefined` for none)_. * * ```js * 'a.b.c': abcResource * 'a.b.c@withKeys': ['a.b.c', abcResource] * ``` * * - **with wildcards**, the return is a resource array, in order * of feature expansion _(empty array for none)_. * * ```js * 'a.*': [ab1Resource, ab2Resource, ...] * 'a.*@withKeys': [ ['a.b1', ab1Resource], ['a.b2', ab2Resource], ... ] * ``` * * @method Fassets.get */ get: function get(fassetsKey) { // validate parameters var check = _verify2.default.prefix('fassets.get() parameter violation: '); check(fassetsKey, 'fassetsKey is required'); check((0, _lodash2.default)(fassetsKey), 'fassetsKey must be a string ... ' + fassetsKey); // interpret optional @directive keywords suffixed in fassetsKey var _fassetsKey$split = fassetsKey.split('@'), _fassetsKey$split2 = _toArray(_fassetsKey$split), raw_fassetsKey = _fassetsKey$split2[0], directives = _fassetsKey$split2.slice(1); check(raw_fassetsKey, 'fassetsKey: \'' + fassetsKey + '\' cannot contain only directives'); var _directives$reduce = directives.reduce(function (accum, directive) { if (directive === 'withKeys') { // @withKeys directive accum.withKeys = true; } else { check(false, 'fassetsKey: \'' + fassetsKey + '\' contains an unrecognized keyword directive: \'@' + directive + '\''); } return accum; }, {}), _directives$reduce$wi = _directives$reduce.withKeys, withKeys = _directives$reduce$wi === undefined ? false : _directives$reduce$wi; // use cached value (when available) // ... NOTE: for cache purposes, fassetsKey correctly includes any @directives // (which can alter the result) var result = _searchCache[fassetsKey]; if (result) { return result === UNDEFINED ? undefined : result; } // resolve resource of supplied key var resolveResource = function resolveResource(key) { var resource = key === '.' ? _fassets // special DOT keyword ... return self (i.e. fassets) : _resources[key] ? _resources[key].val // resource found : undefined; // resource not found return withKeys // interpret @withKeys directive ? [key, resource] // ... [fassetsKey, resource] : resource; // ... resource }; // resolve get() when not seen before if (containsWildCard(raw_fassetsKey)) { // supplied fassetsKey has wildcards ... do regexp search // locate all keys matching the fassetsKey (with wildcards) // ... order is same as feature expansion order // ... empty array for NO match var keys = matchAll(_fassetsKeysBlob, createRegExp(raw_fassetsKey)); // convert keys to actual resource values result = keys.map(function (key) { return resolveResource(key); }); } else { // supplied fassetsKey has NO wildcards ... dereference directly // convert dereferenced resource (when found) to actual resource value // ... undefined for NOT found result = resolveResource(raw_fassetsKey); } // maintain cache // ... NOTE: for cache purposes, fassetsKey correctly includes any @directives // (which can alter the result) _searchCache[fassetsKey] = result === undefined ? UNDEFINED : result; // that's all folks return result; }, /** * Return an indicator as to whether the supplied feature is * active or not. * * **Note**: As an alternative to using this method, you can * conditionally reason over the existence of "well-known fasset * resources" specific to a given feature. * * @param {string} featureName the name of the feature to check. * * @return {boolean} **true**: is active, **false**: is not active * (or doesn't exist). * * @method Fassets.hasFeature */ hasFeature: function hasFeature(featureName) { // validate parameters var check = _verify2.default.prefix('fassets.hasFeature() parameter violation: '); check(featureName, 'featureName is required'); check((0, _lodash2.default)(featureName), 'featureName must be a string'); // return indicator return _hasFeature[featureName] ? true : false; } }; //*--------------------------------------------------------------------------- // OK: We are now ready to interpret/accumulate all feature fassets!! // - This requires multiple passes through our features. // ... As a simple example: // We cannot validate the usage contracts (found in fassets.use), // until all resources are accumulated (via fassets.define/defineUse) // - The comment blocks (below) breaks the process down in "stages". // ... Think of the mighty Saturn V rocket, which powered the Apollo // mission to the moon! // ... OK: I know this is corny, but geeks have to have some fun :-) // - Hopefully, this provides insight as to why there are multiple passes, // and allows you to follow the code more intuitively. //*--------------------------------------------------------------------------- //*--------------------------------------------------------------------------- // T Minus 3: Insure client code is no longer using the obsolete publicFace // built-in aspect // - OBSOLETE as of feature-u@1 // - NOTE: publicFace is still registered as a builtin for the // sole purpose of generating more specific error //*--------------------------------------------------------------------------- var featureNamesWithObsolete_publicFace = // locate features still using publicFace activeFeatures.filter(function (feature) { return feature.publicFace !== undefined; }).map(function (feature) { return feature.name; }); if (featureNamesWithObsolete_publicFace.length > 0) { (0, _verify2.default)(false, 'The OBSOLETE Feature.publicFace is still in-use in the following features: ' + featureNamesWithObsolete_publicFace + '\n' + '... as of feature-u@1 the publicFace builtin aspect has been replaced with fassets\n' + '... see: https://feature-u.js.org/cur/history.html#v1_0_0'); } //*--------------------------------------------------------------------------- // T Minus 2: Maintain our "active features" indicator // - in support of: fassets.hasFeature() //*--------------------------------------------------------------------------- activeFeatures.forEach(function (feature) { _hasFeature[feature.name] = true; }); //*--------------------------------------------------------------------------- // T Minus 1: Validate the basic structure of all Feature.fassets aspects // - including the fassets directives (define/defineUse/use) //*--------------------------------------------------------------------------- // filter features with the fassets aspect activeFeatures.filter(function (feature) { return feature.fassets !== undefined; }).forEach(function (feature) { { // HELP_EMACS: extra bracket - sorry to say, emacs web-mode can't handle indentation with nested filter/forEach (above) :-( var check = _verify2.default.prefix('Feature.name: \'' + feature.name + '\' ... ERROR in "fassets" aspect: '); var fassets = feature.fassets; // fassets must be an object literal check((0, _lodash4.default)(fassets), 'the fassets aspect MUST BE an object literal'); // insure all fassets directives are recognized var define = fassets.define, use = fassets.use, defineUse = fassets.defineUse, unknownDirectives = _objectWithoutProperties(fassets, ['define', 'use', 'defineUse']); // eslint-disable-line no-unused-vars var unknownDirectiveKeys = Object.keys(unknownDirectives); check(unknownDirectiveKeys.length === 0, 'unrecognized fassets directive(s): ' + unknownDirectiveKeys + ' ... expecting only: define/use/defineUse'); // verify at least ONE fassets directive is supplied // ... potential to relax this "empty" check // RELAXED: check(define!==undefined || use!==undefined || defineUse!==undefined, // `the fassets aspect is empty (at least one directive needed - define/use/defineUse)`); } // HELP_EMACS }); //*--------------------------------------------------------------------------- // Blast Off: Yeee Haaaa!! // - We are now off-the-ground //*--------------------------------------------------------------------------- //*--------------------------------------------------------------------------- // Stage 1: Interpret fasset "define"/"defineUse" directive, accumulating resources // - maintain _resources entries (with meta data) // - normalize resource directly in _fassets object // - validation: // * may contain federated namespace (with dots ".") // ... ex: 'MainPage.launch' // * MAY NOT contain wildcards // ... must be defined completely // * must be unique // ... cannot be defined more than once // ... these are individual "single-use" keys // In other words, we do NOT support the "pull" (bucket) philosophy // * NOTE: resource validation is postponed to subsequent stage // ... because we need BOTH _resources and _usage //*--------------------------------------------------------------------------- // filter features with the fassets aspect activeFeatures.filter(function (feature) { return feature.fassets !== undefined; }).forEach(function (feature) { { // HELP_EMACS var check = _verify2.default.prefix('Feature.name: \'' + feature.name + '\' ... ERROR in "fassets" aspect, "define/defineUse" directive: '); var fassets = feature.fassets; // interpret BOTH define/defineUse directives // ... we attempt to process in same order defined within the fassets object literal // - by using Object.keys() // - HOWEVER: I do NOT believe this is universally possible (for all JS engines) // - In general, traversal of object literal property keys is NOT guaranteed // ... or at least the order may NOT be what the user expects (when key is interpreted as an integer) // ... see: http://2ality.com/2015/10/property-traversal-order-es6.html // - While this may work in a majority of cases, I DO NOT ADVERTISE THIS!!! var directiveKeys = Object.keys(fassets); directiveKeys.filter(function (directiveKey) { return directiveKey === 'define' || directiveKey === 'defineUse'; }) // filter all define directives .forEach(function (directiveKey) { { // HELP_EMACS var defineDirective = fassets[directiveKey]; // validate that defineDirective is an object literal check((0, _lodash4.default)(defineDirective), 'the ' + directiveKey + ' directive MUST BE an object literal'); // verify at least ONE definition is supplied // ... potential to relax this "empty" check var resourceKeys = Object.keys(defineDirective); // RELAXED: check(resourceKeys.length > 0, `the ${directiveKey} directive is empty (at least one definition is needed)`); // iterpret each resource being defined // ... we attempt to process in same order defined within the fassets.define object literal // - ditto discussion (above) on processing order resourceKeys.forEach(function (resourceKey) { var resource = defineDirective[resourceKey]; // validate resource key // NOTE: resource validation is postponed to subsequent stage // ... because we need BOTH _resources and _usage // ... insure it will not overwrite our reserved fasset methods (get/hasFeature) check(resourceKey !== 'get' && resourceKey !== 'hasFeature', 'fassets.' + directiveKey + '.\'' + resourceKey + '\' is a reserved word'); // ... restrict to a programmatic structure, because we normalize it (with depth) checkProgrammaticStruct(resourceKey, check); // ... must be unique (i.e. cannot be defined more than once) // ... these are individual "single-use" keys // In other words, we do NOT support the "pull" (bucket) philosophy var resourcePreviouslyDefined = _resources[resourceKey]; if (resourcePreviouslyDefined) // conditional required to prevent template literal (next line) from referencing undefined.definingFeature check(!resourcePreviouslyDefined, 'fassets.' + directiveKey + '.\'' + resourceKey + '\' is NOT unique ... previously defined in Feature.name: \'' + resourcePreviouslyDefined.definingFeature + '\''); // retain in _resources // ... NOTE: currently this structure is tested indirectly // - can't really export it without attaching it to the fassets object _resources[resourceKey] = { // ex: 'action.openView' ... NO wildcards allowed val: resource, // resource value definingFeature: feature.name, // the feature defining this resource defineUse: directiveKey === 'defineUse' // directive used in defining this resource (false: define. true: defineUse) }; // retain our key in our string blob (delimited by new-line) _fassetsKeysBlob += resourceKey + '\n'; // inject resource directly in our _fassets object (normalized) injectFassetsResource(resourceKey, resource, _fassets, check); }); } // HELP_EMACS }); } // HELP_EMACS }); // purge last cr/lf from _fassetsKeysBlob to prevent an open wildcard '*' // from returning a rogue empty string key ('') // ... causing an internal run-time error if (_fassetsKeysBlob !== '') { _fassetsKeysBlob = _fassetsKeysBlob.slice(0, -1); } //*--------------------------------------------------------------------------- // Stage 2: Interpret fasset "use" directive, accumulating usage contract // - maintain _usage entries // - interpret options directives (used in validation of optionality and type) // - validation: // * may contain federated namespace (with dots ".") // ... ex: 'MainPage.launch' // * may contain wildcards (with "*") // ... ex: 'MainPage.*.link' // * the uniqueness of "use" keys is NOT a requirement // ... IN OTHER WORDS: multiple features can specify the same (or overlapping) "use" keys // ... HOWEVER, for duplicate keys: // - the optionality can vary (required simply takes precedence) // - the expected data types MUST be the same // NOTE: For overlapping wildcard items, there is an opportunity to have // multiple expected types. This will be caught (indirectly) through // the resource validation (subsequent stage). //*--------------------------------------------------------------------------- // filter features with the "fassets" aspect -AND- "use" directive activeFeatures.filter(function (feature) { return feature.fassets !== undefined && feature.fassets.use !== undefined; }).forEach(function (feature) { { // HELP_EMACS var check = _verify2.default.prefix('Feature.name: \'' + feature.name + '\' ... ERROR in "fassets" aspect, "use" directive: '); var useDirective = feature.fassets.use; // verify the use directive is an array check(Array.isArray(useDirective), 'the use directive MUST BE an array'); // verify at least ONE usage contract is supplied // ... potential to relax this "empty" check // RELAXED: check(useDirective.length > 0, `the use directive is empty (at least one usage contract is needed`); // process each "use" contract useDirective.forEach(function (useEntry) { // decipher the useEntry, validating, and applying default semantics var _decipherDefaultedUse = decipherDefaultedUseEntry(useEntry, check), useKey = _decipherDefaultedUse.useKey, required = _decipherDefaultedUse.required, validateFn = _decipherDefaultedUse.validateFn; // logf `processing fassets.use directive: '${useEntry}'\n:`, {useKey, required, validateFn} // maintain each use contract in our _usage object // NOTE: The uniqueness of "use" keys is NOT a requirement // IN OTHER WORDS: multiple features can specify the same (or overlapping) "use" keys // HOWEVER, for duplicate keys: // - the optionality can vary (required simply takes precedence) // - the expected data types MUST be the same // NOTE: For overlapping wildcard items, there is an opportunity to have // multiple expected types. This will be caught (indirectly) through // the resource validation (subsequent stage). if (_usage[useKey]) { // duplicate entry (from other features) // accumulate necessary items _usage[useKey].required = _usage[useKey].required || required; // required: true takes precedence _usage[useKey].definingFeatures.push(feature.name); // insure accumulation is possible check(_usage[useKey].validateFn === validateFn, 'cannot accumulate duplicate \'use\' contract from multiple features: [' + _usage[useKey].definingFeatures + '] ... the type validateFns are NOT the same'); } else { // initial entry (first time introduced) _usage[useKey] = { // ex: 'MainPage.*.link' ... wildcards allowed usingWildCard: containsWildCard(useKey), // does this usage contract employ wildcards? required: required, // optionality (required takes precedence for multiple entries) validateFn: validateFn, // validation function (based on registered keywords) definingFeatures: [feature.name] // what feature(s) defined this "use" contract }; } // logf console.log(`_usage['${useKey}']:`, _usage[useKey]); }); } // HELP_EMACS }); //*--------------------------------------------------------------------------- // Stage 3: VALIDATION: Apply client-supplied validation constraints // >> this is done in our third stage, now that both resources and // usage contracts are in place (with it's validation constraints) // A: Apply client-supplied validation constraints // ... defined in the "use" directive // B: Insure "defineUse" resources match at least ONE usage contract // ... this is a fail-fast technique, quickly giving problem insight // to the client //*--------------------------------------------------------------------------- // accumulator of ALL validation errors, to show all at once var validationErrs = []; //*** // A: Apply client-supplied validation constraints // ... defined in the "use" directive //*** // Feature.fassets.use: "type" validation _mySpace.MyObj.entries(_resources) // resource iteration .forEach(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), fassetsKey = _ref2[0], resource = _ref2[1]; _mySpace.MyObj.entries(_usage) // "matching" usage contract iteration .filter(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), useKey = _ref4[0], usage = _ref4[1]; return isMatch(fassetsKey, createRegExp(useKey)); }).forEach(function (_ref5) { var _ref6 = _slicedToArray(_ref5, 2), useKey = _ref6[0], usage = _ref6[1]; { // HELP_EMACS // apply client-supplied validation constraints // NOTE: This also indirectly tests conflicts between overlapping usage contracts // ... i.e. two usage contracts (with varying wildcards) that both match an // entry can in fact specify different type checks var errStr = usage.validateFn(resource.val); if (errStr) { validationErrs.push('VALIDATION ERROR in resource: \'' + fassetsKey + '\', expecting: ' + errStr + ' ... resource defined in Feature: \'' + resource.definingFeature + '\', usage contract \'' + useKey + '\' found in Feature: \'' + usage.definingFeatures + '\''); // EX: VALIDATION ERROR in resource: 'foo1', expecting: boolean ... resource defined in Feature: 'feature1', usage contract 'foo*' found in Feature: 'feature2' } } // HELP_EMACS }); }); // Feature.fassets.use: "required" validation _mySpace.MyObj.entries(_usage) // usage contract iteration .forEach(function (_ref7) { var _ref8 = _slicedToArray(_ref7, 2), useKey = _ref8[0], usage = _ref8[1]; { // HELP_EMACS // when usage is contractually required, insure at least one matching resource is available if (usage.required) { var resource = _fassets.get(useKey); if (resource === undefined || // resource NOT found usage.usingWildCard && resource.length === 0) { // empty wildcard usage validationErrs.push('REQUIRED RESOURCE NOT FOUND, usage contract \'' + useKey + '\' (found in Feature: \'' + usage.definingFeatures + '\') specifies a REQUIRED resource, but NO matches were found'); // EX: REQUIRED RESOURCE NOT FOUND, usage contract '*a.*.c*' (found in Feature: 'featureTest') specifies a REQUIRED resource, but NO matches were found } } } // HELP_EMACS }); //*** // B: Insure "defineUse" resources match at least ONE usage contract // ... this is a fail-fast technique, quickly giving problem insight to the client //*** _mySpace.MyObj.entries(_resources) // defineUse resource iteration .filter(function (_ref9) { var _ref10 = _slicedToArray(_ref9, 2), fassetsKey = _ref10[0], resource = _ref10[1]; return resource.defineUse; }).forEach(function (_ref11) { var _ref12 = _slicedToArray(_ref11, 2), fassetsKey = _ref12[0], definedUseResource = _ref12[1]; { // HELP_EMACS // fassetsKey must match at least one usage entry // ... because "defineUse" directives are intended to fulfill a use contract var matchFound = false; for (var useKey in _usage) { // usage contract iteration if (isMatch(fassetsKey, createRegExp(useKey))) { matchFound = true; break; } } // ERROR, when NO usage contracts match defineUse if (!matchFound) { validationErrs.push('ERROR defineUse \'' + fassetsKey + '\' directive MUST match at least one usage contract, but does NOT ... is this misspelled? (found in Feature: \'' + definedUseResource.definingFeature + '\')'); // EX: ERROR defineUse 'wow.bbb' directive MUST match at least one usage contract, but does NOT ... is this misspelled? (found in Feature: 'feature1') } } // HELP_EMACS }); //*** //*** expose ALL validation issues in ONE Error //*** if (validationErrs.length === 1) { // for a single error, just spit it out (0, _verify2.default)(false, validationErrs[0]); } else if (validationErrs.length > 1) { // for multiple errors, combine all of them with a preamble (0, _verify2.default)(false, validationErrs.length + ' validation errors were found during Feature.fasset resource accumulation:\n' + validationErrs.join('\n')); } //*--------------------------------------------------------------------------- // OK: Our Saturn V is "now in orbit"!!! // - Ready for a trajectory to the moon // - INTERPRETATION: the fassets object is ready for: // - client consumption // - and seeding the withFassets() HoC //*--------------------------------------------------------------------------- // SideBar: To free up space, our regexp cache is deleted now that createFassets() is complete // ... because the fassets.get() maintains it's own results cache, // the regexps built up in this cache will rarely be needed (if at all) _regExpCache = {}; // log summary if (_logf2.default.isEnabled()) { var hookCount = activeFeatures.reduce(function (count, feature) { return feature.fassets ? count + 1 : count; }, 0); var hookSummary = activeFeatures.map(function (feature) { return '\n Feature.name:' + feature.name + (feature.fassets ? ' <-- defines: fassets' : ''); }); (0, _logf2.default)('cross-feature-communication ... INTERPRETING: Feature.fassets ... ' + hookCount + ' hooks:' + hookSummary); (0, _logf2.default)('cross-feature-communication ... fassets define/defineUse directives: ', _resources); // ... see WHO defined WHAT (0, _logf2.default)('cross-feature-communication ... fassets use directives: ', _usage); // ... see WHO enabled usage contracts (0, _logf2.default)('cross-feature-communication ... resolved fassets object: ', _fassets); // ... see consolated list of all fassets } // return our public fassets object (used in cross-feature-communication) return _fassets; } //****************************************************************************** //****************************************************************************** //* Internal Utility Functions //****************************************************************************** //****************************************************************************** /** * An internal function that injects the supplied key/val into obj, * normalized into a structure with depth (by interpreting the key's * federated namespace). * * @param {string} key the injection key. Can contain DOTs (.) which * will be normalized into a structure with depth. * * @param {Any} val the value to inject. * * @param {Object} obj the injection object. * * @param {assertionFn} check an assertion function (with context), * used to perform validation. * * @private */ function injectFassetsResource(key, val, obj, check) { var nodeKeys = key.split('.'); // interpret federated namespace (delimited with DOTs) var lastNode = nodeKeys.pop(); // extract last node (reducing the size of nodeKeys array) var runningObj = nodeKeys.reduce(function (accum, nodeKey) { if (accum[nodeKey]) { // appending to existing structure // ... must be a plain object ... otherwise it represents a conflict check((0, _lodash4.default)(accum[nodeKey]), 'while normalizing the fassets \'' + key + '\' key, a conflict was detected with another feature at the \'' + nodeKey + '\' node (it is NOT an object)'); } else { // introduce new intermediate node accum[nodeKey] = {}; } return accum[nodeKey]; }, obj); // inject the val indexed by the lastNode // ... cannot clober (i.e. cover up or overwrite) existing data check(!runningObj[lastNode], 'while normalizing the fassets \'' + key + '\' key, a conflict was detected with another feature at the \'' + lastNode + '\' node (overwriting existing data)'); runningObj[lastNode] = val; } /** * An internal function that validates supplied key, to be a * programmatic structure. * * feature-u's fassetKeys can be any JS identifier (less $ support - currently) * TODO: Omition of $ support was due to concern about conflict in wildcard processing (never actually tried). * * JavaScript Identifier Rules: * - MAY CONTAIN: alphas, digits, _, $ * - BEGINNING WITH: alphas, _, $ * - ARE: case-sensitive * - JavaScript keywords cannot be used * * Examples: * ``` * - allow embedded DOTS "." * - disallow wildcards "*" * - valid: "a" * "a1" * "a1.b" * "a1.b2.c" * "_a._b_.c_" * - invalid: "" // empty string * "123" // must start with alpha * ".a" // beginning empty string * "a." // ending empty string * "a..b" // embedded empty string * "a.b." // ending empty string (again) * "a.b.1" // each node must start with alpha * "a.b\n.c" // cr/lf NOT supported * "a.b .c" // spaces NOT supported * - wildcards: * "a.*.c" // depends on allowWildcards parameter * "*a.*.c*" // depends on allowWildcards parameter * ``` * * @param {string} key the key to validate. * * @param {assertionFn} check an assertion function (with context), * used to perform validation. * * @param {boolean} allowWildcards an indicator as to whether to * allow wildcards (true) or not (false DEFAULT). * * @private */ function checkProgrammaticStruct(key, check) { var allowWildcards = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var errMsg = 'fassetsKey: \'' + key + '\' is invalid (NOT a JS identifier) ...'; var regExpAllowingWildcards = /^[_a-zA-Z\*][_a-zA-Z0-9\*]*$/; var regExpDisallowingWildcards = /^[_a-zA-Z][_a-zA-Z0-9]*$/; var regExpCheck = regExpAllowingWildcards; // insure NO cr/lf check(!isMatch(key, /[\n\r]/), errMsg + ' contains unsupported cr/lf'); // validate wildcards, per parameter // ... must also accomodate in our regexp check (below) but this check provides a more explicit message if (!allowWildcards) { check(!isMatch(key, /\*/), errMsg + ' wildcards are not supported'); regExpCheck = regExpDisallowingWildcards; } // analyze each node of the federated namespace var nodeKeys = key.split('.'); nodeKeys.forEach(function (nodeKey) { check(nodeKey !== '', errMsg + ' contains invalid empty string'); check(isMatch(nodeKey, regExpCheck), errMsg + ' each node must conform to a JS indentifier (less $ support)'); }); } /** * An internal function that deciphers the supplied useEntry, * validating, applying default semantics, and interpreting the two * formats: * * - a string * - a string/options in a two-element array * * @param {string-or-arrayOfStringOptionPairs} useEntry the use * entry to decipher. * * @param {assertionFn} check an assertion function (with context), * used to perform validation. * * @return {Object} defaulted object with following entries: {useKey, * required, validationFn} * * @private */ function decipherDefaultedUseEntry(useEntry, check) { // decipher various formats of useEntry var use = {}; if ((0, _lodash2.default)(useEntry)) { use.useKey = useEntry; use.required = true; use.validateFn = _fassetValidations2.default.any; } else if (Array.isArray(useEntry)) { check(useEntry.length === 2, '"use" entry must either be a string or a string/options in a two-element array ... incorrect array size: ' + useEntry.length); var _useEntry = _slicedToArray(useEntry, 2), useKey = _useEntry[0], useOptions = _useEntry[1]; check((0, _lodash2.default)(useKey), '"use" entry with options (two-element array), first element is NOT a string'); check((0, _lodash4.default)(useOptions), '"use" entry with options (two-element array), second element is NOT an object'); var _useOptions$required = useOptions.required, required = _useOptions$required === undefined ? true : _useOptions$required, _useOptions$type = useOptions.type, validateFn = _useOptions$type === undefined ? _fassetValidations2.default.any : _useOptions$type, unknownOptions = _objectWithoutProperties(useOptions, ['required', 'type']); var unknownOptionsKeys = Object.keys(unknownOptions); check(unknownOptionsKeys.length === 0, '"use" entry with options (two-element array), options have unrecognized entries: ' + unknownOptionsKeys + ' ... expecting only: required/type'); use.useKey = useKey; use.required = required; use.validateFn = validateFn; } else { // unconditional error check(false, '"use" entry must either be a string or a string/options in a two-element array'); } // validate individual items // ... use.useKey checkProgrammaticStruct(use.useKey, check, true); // allowWildcards // ... use.required check(use.required === true || use.required === false, '"use" entry with options (\'' + use.useKey + '\'), \'required\' entry must be true/false'); // ... use.validateFn check((0, _lodash6.default)(use.validateFn), '"use" entry with options (\'' + use.useKey + '\'), \'type\' entry must be a fassetValidationFn'); // that's all folks return use; } /** * Return an indicator as to whether fassets-based wildcards are * present in supplied str. * * @param {string} str the string to check for wildcards. * * @return {boolean} true: wildcards present, false: no wildcards * detected * * @private */ function containsWildCard(str) { // ... exported for unit tests only // check for fassets-based wildcards (only *) return str.includes('*'); } /** * Perform a regular expression search of the supplied string using * the regexp, returning all matches. * * @param {string} str the string to search. * * @param {RegExp} regexp the regular expression to match. * * @return {string[]} a string array of all matches, empty array for * no match. * * @private */ function matchAll(str, regexp) { // ... exported for unit tests only // NOTE: Because we re-use our regexps (for optimization) // -AND- we use the "global" regexp modifier, // WE MUST RESET IT (so as to NOT pick up where it last left off) regexp.lastIndex = 0; return str.match(regexp) || []; // simple pass-through -BUT- convert null to empty array } /** * Return an indicator as to whether the supplied regexp has a match * in str. * * @param {string} str the string to search. * * @param {RegExp} regexp the regular expression to match. * * @return {boolean} true: match, false: no match * * @private */ function isMatch(str, regexp) { // ... exported for unit tests only // NOTE: Because we re-use our regexps (for optimization) // -AND- we use the "global" regexp modifier, // WE MUST RESET IT (so as to NOT pick up where it last left off) regexp.lastIndex = 0; return regexp.test(str); // simple pass-through } // regexp cache used by createRegExp() // - optimizes repeated iteration in createFassets() "Stage 3: VALIDATION" // - SideBar: To free up space, this cache is deleted at end of createFassets() // ... the fassets.get() maintains results cache, // so regexp will be rarely needed (if at all) var _regExpCache = {/* dynamically maintained ... SAMPLE: 'MainPage.*.link': /^MainPage\..*\.link$/gm, 'selector.currentView': /^selector\.currentView$/gm, ... */ }; /** * Creates a fassets-specific regular expression from the supplied * pattern string, employing all the heuristics required by fassets * usage. * * @param {string} pattern the string to seed the regexp from. * * @return {RegExp} the newly created regexp. * * @private */ function createRegExp(pattern) { // ... exported for unit tests only var wrkStr = pattern; // use cached value (when available) var regexp = _regExpCache[pattern]; if (regexp) { return regexp; } // convert all federated namespace delimiters (DOT ".") to their literal representation wrkStr = wrkStr.replace(/\./g, '\\.'); // convert all fasset wildcards ("*") to their RegExp equilivant (".*") wrkStr = wrkStr.replace(/\*/g, '.*'); // start/end anchors are required to match entire entry // ... NOTE: the user is in control (can disable by placing * at beginning or end) wrkStr = '^' + wrkStr + '$'; // construct the RegExp // ... in support of multi-line blob, we require modifiers (g: global, m: multiline) // NOTE: This has been manually tested to work in both our single and // multi-line cases (i.e. our blob). regexp = new RegExp(wrkStr, 'gm'); // maintain cache _regExpCache[pattern] = regexp; // that's all folks return regexp; }