Show:

File: src/core/Plugin.js

                                /**
                                * 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 === '*';
                                    };
                                
                                })();