UNPKG

apostrophe

Version:

The Apostrophe Content Management System.

411 lines (364 loc) • 13.3 kB
(function() { var async, _; if (typeof module !== 'undefined') { // For npm async = require('async'); _ = require('@sailshq/lodash'); } else { // Works in browser if async and _ are already global async = window.async; _ = window._; } function afterYield(fn) { if (typeof setImmediate !== 'undefined') { return setImmediate(fn); } else { return setTimeout(fn, 0); } }; function moog(options) { options = options || {}; var self = {}; self.options = options; self.definitions = {}; self.ordinal = 0; // The "extending" argument is of interest to subclasses like // moog-require that need to know about relative paths. Must // return the new definition for the convenience of moog-require too self.define = function(type, definition, extending) { // Define many in a single call if (typeof(type) === 'object') { // Apply any definitions passed directly to the factory function _.each(type || {}, function(definition, name) { self.define(name, definition); }); return; } if (!definition) { // This can happen because we use self.define as an autoloader // when resolving "extend". The moog-require module overloads // self.define to handle this case throw new Error(new Error('The type ' + type + ' is not defined.')); } definition.__meta = definition.__meta || {}; definition.__meta.name = type; definition.__meta.ordinal = self.ordinal++; if (!extending) { definition.__meta.explicit = true; } var exists = _.has(self.definitions, type); if (definition.extendIfFirst && (!exists)) { definition.extend = definition.extendIfFirst; } if ((!definition.extend) && (definition.extend !== false)) { if (exists) { // Double definitions result in implicit subclassing of // the original definition by the new one; anything else // trying to access this type name will see // the resulting subclass via self.definitions. However // we reset the __name property for the benefit of // implementations that need to distinguish assets that // come from each subclass in the inheritance chain. definition.extend = self.definitions[type]; definition.__meta.name = 'my-' + definition.__meta.name; } else { // Extend the default base class by default, if any, unless // we're it if (self.options.defaultBaseClass && type !== self.options.defaultBaseClass) { definition.extend = self.options.defaultBaseClass; } } } self.definitions[type] = definition; return definition; }; self.redefine = function(type, definition) { delete self.definitions[type]; return self.define(type, definition); }; self.isDefined = function(type) { if (_.has(self.definitions, type)) { return true; } try { // Can we autoload it? self.define(type); // Yes, but we don't really want it yet delete self.definitions[type]; return true; } catch (e) { return false; } }; // Create an instance self.create = function(type, options, callback) { // Careful, allow skipping the options argument and also // invoking synchronously with no callback in any combination if (arguments.length === 1) { options = {}; } else if (arguments.length === 2) { if (typeof(arguments[1]) === 'function') { callback = arguments[1]; options = {}; } } options = options || {}; var definition; var that = {}; var steps = []; var seen = {}; var next = self.definitions[type]; if (!next) { if (!callback) { throw 'The type ' + type + ' is not defined.'; } else { return callback(new Error('The type ' + type + ' is not defined.')); } } while (next) { var current = next; if (_.has(seen, current.__meta.ordinal)) { var error = new Error('The type ' + type + ' encounters an infinite loop, "extend" probably points back to itself or its subclass.'); if (callback) { return callback(error); } throw error; } seen[current.__meta.ordinal] = true; steps.push(current); next = current.extend; // In most cases it'll be a string we need to look up // in self.definitions. In a few cases it is already // a pointer to another definition (see double defines, above) if (typeof(next) === 'string') { var nextName = next; next = self.definitions[nextName]; if (!next) { try { // Try to use define as an autoloader. This will fail in // the default implementation next = self.define(nextName, undefined, current); } catch (e) { console.log(e); return callback(e); } } } } // Attach metadata about the modules in the // inheritance chain, base class first that.__meta = { chain: [], name: type }; var i = steps.length - 1; while (i >= 0) { that.__meta.chain.push(steps[i].__meta); i--; } if (!callback) { return createSync(); } else { return createAsync(); } function createSync() { _.each(steps, function(step) { applyOptions(step); if (step.beforeConstruct) { if (step.beforeConstruct.length === 3) { throw new Error('moog.create was called synchronously for the type ' + type + ', but the ' + step.__meta.name + ' class has an asynchronous beforeConstruct method. You must provide a callback to create.'); } step.beforeConstruct(that, options); } }); // Now we want to start from the base class and go down steps.reverse(); _.each(steps, function(step) { if (step.construct) { if (step.construct.length === 3) { throw new Error('moog.create was called synchronously for the type ' + type + ', but the ' + step.__meta.name + ' class has an asynchronous construct method. You must provide a callback to create.'); } step.construct(that, options); } }); _.each(steps, function(step) { if (step.afterConstruct) { if (step.afterConstruct.length === 2) { console.log(step); throw new Error('moog.create was called synchronously for the type ' + type + ', but the ' + step.__meta.name + ' class has an asynchronous afterConstruct method. You must provide a callback to create.'); } step.afterConstruct(that); } }); return that; } function createAsync() { return async.series({ beforeConstruct: function(callback) { return async.eachSeries(steps, function(step, callback) { applyOptions(step); // Invoke beforeConstruct, defaulting to an empty one var beforeConstruct = step.beforeConstruct || function(self, options, callback) { return afterYield(callback); }; // Turn sync into async if (beforeConstruct.length === 2) { var syncBeforeConstruct = beforeConstruct; beforeConstruct = function(self, options, callback) { try { syncBeforeConstruct(self, options); } catch (e) { return afterYield(_.partial(callback, e)); } return afterYield(callback); }; } if (beforeConstruct.length < 3) { return callback(new Error('beforeConstruct must take the following arguments: "self", "options", and (if it is async) "callback"')); } return beforeConstruct(that, options, callback); }, callback); }, construct: function(callback) { // Now we want to start from the base class and go down steps.reverse(); return async.eachSeries(steps, function(step, callback) { // Invoke construct, defaulting to an empty one var construct = step.construct || function(self, options, callback) { return afterYield(callback); }; // Turn sync into async if (construct.length === 2) { var syncConstruct = construct; construct = function(self, options, callback) { try { syncConstruct(self, options); } catch (e) { return afterYield(_.partial(callback, e)); } return afterYield(callback); }; } if (construct.length < 3) { return callback(new Error('construct must take the following arguments: "self", "options", and (if it is async) "callback"')); } return construct(that, options, callback); }, callback); }, afterConstruct: function(callback) { return async.eachSeries(steps, function(step, callback) { // Invoke afterConstruct, defaulting to an empty one var afterConstruct = step.afterConstruct || function(self, callback) { return afterYield(callback); }; // Turn sync into async if (afterConstruct.length === 1) { var syncAfterConstruct = afterConstruct; afterConstruct = function(self, callback) { try { syncAfterConstruct(self); } catch (e) { return afterYield(_.partial(callback, e)); } return afterYield(callback); }; } if (afterConstruct.length < 2) { return callback(new Error('afterConstruct must take the following arguments: "self", and (if it is async) "callback"')); } return afterConstruct(that, callback); }, callback); } }, function(err) { if (err) { return callback(err); } return callback(null, that); }); } function applyOptions(step) { // Apply the simple option defaults _.each(step, function(val, key) { if ((key === 'construct') || (key === 'extend') || (key === 'beforeConstruct')) { return; } if (key.substr(0, 2) === '__') { return; } if (_.has(options, key)) { return; } options[key] = val; }); } }; self.createAll = function(globalOptions, specificOptions, callback) { var result = {}; var defined = _.keys(self.definitions); var explicit = _.filter(defined, function(type) { return self.definitions[type].__meta.explicit = true; }); if (callback) { return createAllAsync(); } else { return createAllSync(); } function createAllAsync() { return async.eachSeries( explicit, function(name, callback) { var options = applyOptions(name); return self.create(name, options, function(err, obj) { if (err) { return callback(err); } result[name] = obj; return callback(null); }); }, function(err) { if (err) { return callback(err); } return callback(null, result); } ); } function createAllSync() { var result = {}; _.each(explicit, function(name) { var options = applyOptions(name); result[name] = self.create(name, options); }); return result; } function applyOptions(name) { var options = {}; _.extend(options, globalOptions); if (_.has(specificOptions, name)) { _.extend(options, specificOptions[name]); } return options; } }; self.bridge = function(modules) { return _.each(modules, function(module) { if (module.setBridge) { module.setBridge(modules); } }); } self.mirror = function(meta, suffix) { var lastName; _.each(meta.chain, function(type) { var name = type.name; if (suffix) { name += suffix; } if (!self.isDefined(name)) { self.define(name, { extend: lastName }); } lastName = name; }); }; return self; }; if (typeof module !== 'undefined') { module.exports = moog; } else { window.moog = moog; } })();