UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

352 lines (294 loc) 13.7 kB
/** * 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 && !plugin.silent) { 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 === '*'; }; })();