/**
* The `Matter.Plugin` module contains functions for registering and installing plugins on modules.
*
* @class Plugin
*/
var Plugin = {};
module.exports = Plugin;
var Common = require('./Common');
(function() {
Plugin._registry = {};
/**
* Registers a plugin object so it can be resolved later by name.
* @method register
* @param plugin {} The plugin to register.
* @return {object} The plugin.
*/
Plugin.register = function(plugin) {
if (!Plugin.isPlugin(plugin)) {
Common.warn('Plugin.register:', Plugin.toString(plugin), 'does not implement all required fields.');
}
if (plugin.name in Plugin._registry) {
var registered = Plugin._registry[plugin.name],
pluginVersion = Plugin.versionParse(plugin.version).number,
registeredVersion = Plugin.versionParse(registered.version).number;
if (pluginVersion > registeredVersion) {
Common.warn('Plugin.register:', Plugin.toString(registered), 'was upgraded to', Plugin.toString(plugin));
Plugin._registry[plugin.name] = plugin;
} else if (pluginVersion < registeredVersion) {
Common.warn('Plugin.register:', Plugin.toString(registered), 'can not be downgraded to', Plugin.toString(plugin));
} else if (plugin !== registered) {
Common.warn('Plugin.register:', Plugin.toString(plugin), 'is already registered to different plugin object');
}
} else {
Plugin._registry[plugin.name] = plugin;
}
return plugin;
};
/**
* Resolves a dependency to a plugin object from the registry if it exists.
* The `dependency` may contain a version, but only the name matters when resolving.
* @method resolve
* @param dependency {string} The dependency.
* @return {object} The plugin if resolved, otherwise `undefined`.
*/
Plugin.resolve = function(dependency) {
return Plugin._registry[Plugin.dependencyParse(dependency).name];
};
/**
* Returns a pretty printed plugin name and version.
* @method toString
* @param plugin {} The plugin.
* @return {string} Pretty printed plugin name and version.
*/
Plugin.toString = function(plugin) {
return typeof plugin === 'string' ? plugin : (plugin.name || 'anonymous') + '@' + (plugin.version || plugin.range || '0.0.0');
};
/**
* Returns `true` if the object meets the minimum standard to be considered a plugin.
* This means it must define the following properties:
* - `name`
* - `version`
* - `install`
* @method isPlugin
* @param obj {} The obj to test.
* @return {boolean} `true` if the object can be considered a plugin otherwise `false`.
*/
Plugin.isPlugin = function(obj) {
return obj && obj.name && obj.version && obj.install;
};
/**
* Returns `true` if a plugin with the given `name` been installed on `module`.
* @method isUsed
* @param module {} The module.
* @param name {string} The plugin name.
* @return {boolean} `true` if a plugin with the given `name` been installed on `module`, otherwise `false`.
*/
Plugin.isUsed = function(module, name) {
return module.used.indexOf(name) > -1;
};
/**
* Returns `true` if `plugin.for` is applicable to `module` by comparing against `module.name` and `module.version`.
* If `plugin.for` is not specified then it is assumed to be applicable.
* The value of `plugin.for` is a string of the format `'module-name'` or `'module-name@version'`.
* @method isFor
* @param plugin {} The plugin.
* @param module {} The module.
* @return {boolean} `true` if `plugin.for` is applicable to `module`, otherwise `false`.
*/
Plugin.isFor = function(plugin, module) {
var parsed = plugin.for && Plugin.dependencyParse(plugin.for);
return !plugin.for || (module.name === parsed.name && Plugin.versionSatisfies(module.version, parsed.range));
};
/**
* Installs the plugins by calling `plugin.install` on each plugin specified in `plugins` if passed, otherwise `module.uses`.
* For installing plugins on `Matter` see the convenience function `Matter.use`.
* Plugins may be specified either by their name or a reference to the plugin object.
* Plugins themselves may specify further dependencies, but each plugin is installed only once.
* Order is important, a topological sort is performed to find the best resulting order of installation.
* This sorting attempts to satisfy every dependency's requested ordering, but may not be exact in all cases.
* This function logs the resulting status of each dependency in the console, along with any warnings.
* - A green tick ✅ indicates a dependency was resolved and installed.
* - An orange diamond 🔶 indicates a dependency was resolved but a warning was thrown for it or one if its dependencies.
* - A red cross ❌ indicates a dependency could not be resolved.
* Avoid calling this function multiple times on the same module unless you intend to manually control installation order.
* @method use
* @param module {} The module install plugins on.
* @param [plugins=module.uses] {} The plugins to install on module (optional, defaults to `module.uses`).
*/
Plugin.use = function(module, plugins) {
module.uses = (module.uses || []).concat(plugins || []);
if (module.uses.length === 0) {
Common.warn('Plugin.use:', Plugin.toString(module), 'does not specify any dependencies to install.');
return;
}
var dependencies = Plugin.dependencies(module),
sortedDependencies = Common.topologicalSort(dependencies),
status = [];
for (var i = 0; i < sortedDependencies.length; i += 1) {
if (sortedDependencies[i] === module.name) {
continue;
}
var plugin = Plugin.resolve(sortedDependencies[i]);
if (!plugin) {
status.push('❌ ' + sortedDependencies[i]);
continue;
}
if (Plugin.isUsed(module, plugin.name)) {
continue;
}
if (!Plugin.isFor(plugin, module)) {
Common.warn('Plugin.use:', Plugin.toString(plugin), 'is for', plugin.for, 'but installed on', Plugin.toString(module) + '.');
plugin._warned = true;
}
if (plugin.install) {
plugin.install(module);
} else {
Common.warn('Plugin.use:', Plugin.toString(plugin), 'does not specify an install function.');
plugin._warned = true;
}
if (plugin._warned) {
status.push('🔶 ' + Plugin.toString(plugin));
delete plugin._warned;
} else {
status.push('✅ ' + Plugin.toString(plugin));
}
module.used.push(plugin.name);
}
if (status.length > 0) {
Common.info(status.join(' '));
}
};
/**
* Recursively finds all of a module's dependencies and returns a flat dependency graph.
* @method dependencies
* @param module {} The module.
* @return {object} A dependency graph.
*/
Plugin.dependencies = function(module, tracked) {
var parsedBase = Plugin.dependencyParse(module),
name = parsedBase.name;
tracked = tracked || {};
if (name in tracked) {
return;
}
module = Plugin.resolve(module) || module;
tracked[name] = Common.map(module.uses || [], function(dependency) {
if (Plugin.isPlugin(dependency)) {
Plugin.register(dependency);
}
var parsed = Plugin.dependencyParse(dependency),
resolved = Plugin.resolve(dependency);
if (resolved && !Plugin.versionSatisfies(resolved.version, parsed.range)) {
Common.warn(
'Plugin.dependencies:', Plugin.toString(resolved), 'does not satisfy',
Plugin.toString(parsed), 'used by', Plugin.toString(parsedBase) + '.'
);
resolved._warned = true;
module._warned = true;
} else if (!resolved) {
Common.warn(
'Plugin.dependencies:', Plugin.toString(dependency), 'used by',
Plugin.toString(parsedBase), 'could not be resolved.'
);
module._warned = true;
}
return parsed.name;
});
for (var i = 0; i < tracked[name].length; i += 1) {
Plugin.dependencies(tracked[name][i], tracked);
}
return tracked;
};
/**
* Parses a dependency string into its components.
* The `dependency` is a string of the format `'module-name'` or `'module-name@version'`.
* See documentation for `Plugin.versionParse` for a description of the format.
* This function can also handle dependencies that are already resolved (e.g. a module object).
* @method dependencyParse
* @param dependency {string} The dependency of the format `'module-name'` or `'module-name@version'`.
* @return {object} The dependency parsed into its components.
*/
Plugin.dependencyParse = function(dependency) {
if (Common.isString(dependency)) {
var pattern = /^[\w-]+(@(\*|[\^~]?\d+\.\d+\.\d+(-[0-9A-Za-z-+]+)?))?$/;
if (!pattern.test(dependency)) {
Common.warn('Plugin.dependencyParse:', dependency, 'is not a valid dependency string.');
}
return {
name: dependency.split('@')[0],
range: dependency.split('@')[1] || '*'
};
}
return {
name: dependency.name,
range: dependency.range || dependency.version
};
};
/**
* Parses a version string into its components.
* Versions are strictly of the format `x.y.z` (as in [semver](http://semver.org/)).
* Versions may optionally have a prerelease tag in the format `x.y.z-alpha`.
* Ranges are a strict subset of [npm ranges](https://docs.npmjs.com/misc/semver#advanced-range-syntax).
* Only the following range types are supported:
* - Tilde ranges e.g. `~1.2.3`
* - Caret ranges e.g. `^1.2.3`
* - Greater than ranges e.g. `>1.2.3`
* - Greater than or equal ranges e.g. `>=1.2.3`
* - Exact version e.g. `1.2.3`
* - Any version `*`
* @method versionParse
* @param range {string} The version string.
* @return {object} The version range parsed into its components.
*/
Plugin.versionParse = function(range) {
var pattern = /^(\*)|(\^|~|>=|>)?\s*((\d+)\.(\d+)\.(\d+))(-[0-9A-Za-z-+]+)?$/;
if (!pattern.test(range)) {
Common.warn('Plugin.versionParse:', range, 'is not a valid version or range.');
}
var parts = pattern.exec(range);
var major = Number(parts[4]);
var minor = Number(parts[5]);
var patch = Number(parts[6]);
return {
isRange: Boolean(parts[1] || parts[2]),
version: parts[3],
range: range,
operator: parts[1] || parts[2] || '',
major: major,
minor: minor,
patch: patch,
parts: [major, minor, patch],
prerelease: parts[7],
number: major * 1e8 + minor * 1e4 + patch
};
};
/**
* Returns `true` if `version` satisfies the given `range`.
* See documentation for `Plugin.versionParse` for a description of the format.
* If a version or range is not specified, then any version (`*`) is assumed to satisfy.
* @method versionSatisfies
* @param version {string} The version string.
* @param range {string} The range string.
* @return {boolean} `true` if `version` satisfies `range`, otherwise `false`.
*/
Plugin.versionSatisfies = function(version, range) {
range = range || '*';
var r = Plugin.versionParse(range),
v = Plugin.versionParse(version);
if (r.isRange) {
if (r.operator === '*' || version === '*') {
return true;
}
if (r.operator === '>') {
return v.number > r.number;
}
if (r.operator === '>=') {
return v.number >= r.number;
}
if (r.operator === '~') {
return v.major === r.major && v.minor === r.minor && v.patch >= r.patch;
}
if (r.operator === '^') {
if (r.major > 0) {
return v.major === r.major && v.number >= r.number;
}
if (r.minor > 0) {
return v.minor === r.minor && v.patch >= r.patch;
}
return v.patch === r.patch;
}
}
return version === range || version === '*';
};
})();