/*global apf*/ define(function(require, exports, module) { main.consumes = ["app"]; main.provides = ["ext", "Plugin"]; return main; function main(options, imports, register) { var Emitter = require("events").EventEmitter; var architectApp = imports.app; var plugins = []; var lut = {}; var manuallyDisabled = {}; var dependencies = {}; var counters = {}; var $id = 1; /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); var emit = plugin.getEmitter(); var settings, api; plugin.__defineSetter__("settings", function(remote) { settings = remote; settings.on("read", function() { var s = settings.getNode("state/ext/counters"); for (var type in s) { counters[type] = s[type.substr(1)]; } }); delete plugin.settings; }); plugin.__defineSetter__("api", function(remote) { api = remote; delete plugin.api; }); var eventRegistry = Object.create(null); /***** Methods *****/ function uid(type, name) { while (!name || lut[name]) { if (!counters[type]) counters[type] = 0; name = type + counters[type]++; } if (settings && counters[type]) settings.set("state/ext/counters/@" + type, counters[type]); return name; } function registerPlugin(plugin, loaded) { if (plugins.indexOf(plugin) == -1) plugins.push(plugin); lut[plugin.name] = plugin; loaded(true); var deps = plugin.deps; if (deps) { deps.forEach(function(dep) { // if (dep !== plugin.name) throw new Error(dep); (dependencies[dep] || (dependencies[dep] = {}))[plugin.name] = 1; }); } emit("register", { plugin: plugin }); } function unregisterPlugin(plugin, loaded, ignoreDeps, keep) { if (!plugin.registered) return; if (!ignoreDeps && getDependents(plugin.name).length) return false; if (!keep) plugins.splice(plugins.indexOf(plugin), 1); delete lut[plugin.name]; loaded(false, 0); var deps = plugin.deps; if (deps && dependencies) { deps.forEach(function(dep) { delete dependencies[dep][plugin.name]; }); } emit("unregister", { plugin: plugin }); } function getDependents(pluginName) { var usedBy = []; // Check for dependencies needing this plugin if (dependencies) { var deps = dependencies[pluginName]; if (deps) { Object.keys(deps).forEach(function(name) { usedBy.push(name); }); } } return usedBy; } function unloadAllPlugins(exclude) { if (lut.settings) lut.settings.unload(null, true); function unload(plugin) { if (!plugin || exclude && exclude[plugin.name]) return; var deps = dependencies[plugin.name]; if (deps) { Object.keys(deps).forEach(function(name) { unload(lut[name]); }); } plugin.unload(null, true); } var list = plugins.slice(0); for (var i = list.length - 1; i >= 0; i--) { var plugin = list[i]; if (!plugin.loaded) continue; if (plugin.unload) unload(plugin); else console.warn("Ignoring not a plugin: " + plugin.name); } } function loadRemotePlugin(id, options, callback) { var vfs = architectApp.services.vfs; vfs.extend(id, options, function(err, meta) { callback(err, meta && meta.api); }); } function fetchRemoteApi(id, callback) { var vfs = architectApp.services.vfs; vfs.use(id, {}, function(err, meta) { callback(err, meta && meta.api); }); } function unloadRemotePlugin(id, options, callback) { var vfs = architectApp.services.vfs; if (typeof options == "function") { callback = options; options = {}; } vfs.unextend(id, options, callback); } function enablePlugin(name) { if (!lut[name] && !manuallyDisabled[name]) throw new Error("Could not find plugin: " + name); (lut[name] || manuallyDisabled[name]).load(name); } function disablePlugin(name) { if (!lut[name]) throw new Error("Could not find plugin: " + name); var plugin = lut[name]; if (plugin.unload({ keep: true }) === false) throw new Error("Failed unloading plugin: " + name); manuallyDisabled[name] = plugin; } /***** Register and define API *****/ /** * The Cloud9 Extension Manager * @singleton */ plugin.freezePublicAPI({ /** * */ get plugins() { return plugins.slice(0); }, /** * */ get named() { var named = Object.create(lut); for (var name in manuallyDisabled) { if (!lut[name]) lut[name] = manuallyDisabled[name]; } return named; }, _events: [ /** * Fires when a plugin registers * @event register * @param {Object} e * @param {Plugin} e.plugin the plugin that registers */ "register", /** * Fires when a plugin unregisters * @event unregister * @param {Object} e * @param {Plugin} e.plugin the plugin that unregisters */ "unregister" ], /** * Loads a plugin on the remote server. This plugin can be either * source that we have local, or a path to a file that already * exists on the server. The plugin provides an api that is returned * in the callback. The remote plugin format is very simple. Here's * an example of a Math module: * * #### Math Modules * * module.exports = function (vfs, options, register) { * register(null, { * add: function (a, b, callback) { * callback(null, a + b); * }, * multiply: function (a, b, callback) { * callback(null, a * b); * } * }); * }; * * @param {String} id A unique identifier for this module * @param {Object} options Options to specify * @param {String} [options.code] The implementation of a module, e.g. require("text!./my-service.js"). * @param {String} [options.file] An absolute path to a module on the remote disk * @param {Boolean} [options.redefine] specifying whether to replace an existing module with the same `id` * @param {Function} callback called when the code has been loaded. * @param {Error} callback.err The error object if an error has occured. * @param {Object} callback.api The api the code that loaded defined. * */ loadRemotePlugin: loadRemotePlugin, /** * */ fetchRemoteApi: fetchRemoteApi, /** * Unloads a plugin loaded with loadRemotePlugin * @param {String} id The unique identifier for this module * @param {Function} callback */ unloadRemotePlugin: unloadRemotePlugin, /** * */ unloadAllPlugins: unloadAllPlugins, /** * */ getDependents: getDependents, /** * */ enablePlugin: enablePlugin, /** * */ disablePlugin: disablePlugin }); function Plugin(developer, deps) { var elements = []; var names = {}; var waiting = {}; var events = []; var other = []; var name = ""; var time = 0; var registered = false; var loaded = false; var event = new Emitter(); var disabled = false; var onNewEvents = {}; var declaredEvents = []; this.deps = deps; this.developer = developer; event.on("newListener", function(type, listener) { if (!(type in onNewEvents)) return; var data = onNewEvents[type]; if (data === -1) event.emit("$event." + type, listener); else { listener(onNewEvents[type]); if (event.listeners(type).indexOf(listener) > -1) console.trace("Used 'on' instead of 'once' to " + "listen to sticky event " + name + "." + type); } }); function init(reg) { registered = reg; } /***** Methods *****/ this.getEmitter = function() { var emit = event.emit.bind(event); var _self = this; var sticky = function(name, e, plugin) { if (plugin) { _self.on("$event." + name, function(listener) { listener(e); }, plugin); onNewEvents[name] = -1; } else { onNewEvents[name] = e; } return emit(name, e); }; function unsticky(name, e) { delete onNewEvents[name]; } emit.listeners = event.listeners.bind(event); emit.setMaxListeners = event.setMaxListeners.bind(event); emit.sticky = sticky; emit.unsticky = unsticky; return emit; }; this.freezePublicAPI = function(api) { // Build a list of known events to warn users if they use a // non-existent event. if (api._events) { api._events.forEach(function(name) { declaredEvents[name] = true; }); delete api._events; } // Reverse prototyping of the API // if (!this.__proto__) { // modifying __proto__ is very slow on chrome! Object.keys(api).forEach(function(key) { var d = Object.getOwnPropertyDescriptor(api, key); Object.defineProperty(this, key, d); }, this); // } // else { // api.__proto__ = this.__proto__; // this.__proto__ = api; // Object.freeze(api); // } if (!baseclass) { delete this.baseclass; delete this.freezePublicAPI.baseclass; delete this.freezePublicAPI; delete this.setAPIKey; delete this.getEmitter; Object.freeze(this); } baseclass = false; return this; }; var baseclass; this.baseclass = this.freezePublicAPI.baseclass = function() { baseclass = true; }; function getElement(name, callback) { // remove id's after storing them. if (!callback) { // If we run without APF, just return a simple object if (typeof apf == "undefined") return {}; if (!names[name]) { throw new Error("Could not find AML element by name '" + name + "'"); } return names[name]; } else { if (names[name]) callback(names[name]); else { (waiting[name] || (waiting[name] = [])).push(callback); } } } function addElement() { for (var i = 0; i < arguments.length; i++) { var node = arguments[i]; elements.push(node); recur(node); } function recur(node) { (node.childNodes || []).forEach(recur); var id = node.id; if (!id) return; // Delete their global reference delete window[id]; // delete apf.nameserver.lookup.all[node.id]; // Keep their original name in a lookup table names[id] = node; // Set a new unique id if (node.localName != "page") { // Temp hack, should fix in tabs node.id = "element" + node.$uniqueId; apf.nameserver.lookup.all[node.id] = node; } // Call all callbacks waiting for this element if (waiting[id]) { waiting[id].forEach(function(callback) { callback(node); }); delete waiting[id]; } } return arguments[0]; } function addEvent(emitter, type, listener) { if (!listener.listenerId) listener.listenerId = $id++; events.push([emitter.name, type, listener.listenerId]); } function addOther(o) { other.push(o); } function initLoad(type, listener) { if (type == "load") listener(); } function load(nm, type) { var dt = Date.now(); if (type) nm = uid(type, nm); if (nm && !name) name = nm; event.name = name; eventRegistry[name] = event; registerPlugin(this, init); loaded = true; event.emit("load"); event.on("newListener", initLoad); time = Date.now() - dt; } function enable() { emit("enablePlugin", { plugin: this }); event.emit("enable"); disabled = false; } function disable() { emit("disablePlugin", { plugin: this }); event.emit("disable"); disabled = true; } function unload(e, ignoreDeps) { if (!loaded) return; if (event.emit("beforeUnload", e) === false) return false; if (unregisterPlugin(this, init, ignoreDeps, e && e.keep) === false) return false; loaded = false; event.emit("unload", e); this.cleanUp(); event.off("newListener", initLoad); setTimeout(function() { if (eventRegistry[name] == event && !loaded) { delete eventRegistry[name]; } }); } function cleanUp(what, otherPlugin) { if (!what || ~what.indexOf("elements")) { // Loop through elements elements.forEach(function(element) { element.destroy(true, true); }); elements = []; names = {}; waiting = []; } // Loop through events if (!what || ~what.indexOf("events")) { events.forEach(function(eventRecord) { var event = eventRegistry[eventRecord[0]]; if (!event) return; // this happens with mock plugins during testing if (otherPlugin && otherPlugin.name != event.name) return; var type = eventRecord[1]; var id = eventRecord[2]; var _events = event._events; var eventList = _events && _events[type]; if (typeof eventList == "function") { if (eventList.listenerId == id) event.off(type, eventList); } else if (Array.isArray(eventList)) { eventList.some(function(listener) { if (listener.listenerId != id) return; event.off(type, listener); return true; }); } }); events = []; onNewEvents = {}; } // Loop through other if (!what || ~what.indexOf("other")) { other.forEach(function(o) { o(); }); other = []; } } /***** Register and define API *****/ this.baseclass(); /** * Base class for all Plugins of Cloud9. A Cloud9 Plugin is * an instance of the Plugin class. This class offers ways to * describe the API it offers as well as ways to clean up the * objects created during the lifetime of the plugin. * * Note that everything in Cloud9 is a plugin. This means that * your plugins have the exact same possibilities as any other part * of Cloud9. When building plugins you can simply create additional * features or replace existing functionality, by turning off the * core plugins and preference of your own. * * Check out the [template](http://example.org/template) for the * recommended way of building a plugin. All Cloud9 Core Plugins * are written in this way. * * Our goal has been to create an extensible system that works both * in the browser as well as in Node.js. The Cloud9 CLI uses the * exact same plugin structure as the Cloud9 in the browser. The * same goes for many of the Cloud9 platform services. We focussed * on making the system easy to use by making sure you can create * plugins using simple javascript, html and css. The plugins you * create will become available as services inside the Cloud9 * plugin system. This means that other plugins can consume your * functionality and importantly you can replace existing services * by giving your plugin the same service name. * * The plugin class will allow you to specify an API that is * "frozen" upon definition (See {@link Object#freeze}. This means * that once your plugin's API is defined the object's interface * cannot be changed anymore. Property gettters and setters will * still work and events can still be set/unset. Having immutable * APIs will prevent users from common hacks that devs often use * in the javascript community, adding new properties to objects. * It is our aim that this will increase the stability of the system * as a whole while introducing foreign plugins to it. * * The event flow of a basic plugin is as follows: * * * {@link #event-load} - *The plugin is loaded (this can happen multiple times to the same plugin instance)* * * {@link #event-unload} - *The plugin is unloaded* * * #### User Actions: * * * {@link #event-disable} - *The plugin is disabled* * * {@link #event-enable} - *The plugin is enabled* * * The following example shows how to implement a basic plugin: * * define(function(require, exports, module) { * main.consumes = ["dependency"]; * main.provides = ["myplugin"]; * return main; * * function main(options, imports, register) { * var dependency = imports.dependency; * * var plugin = new Plugin("(Company) Name", main.consumes); * var emit = plugin.getEmitter(); * * plugin.on("load", function(e) { * // Create a command, menu item, etc * }); * plugin.on("unload", function(e) { * // Any custom unload code (most things are cleaned up automatically) * }); * * function doSomething(){ * } * * plugin.freezePublicAPI({ * doSomething : doSomething * }); * } * }); * * @class Plugin * @extends Object */ /** * @constructor * Creates a new Plugin instance. * @param {String} developer The name of the developer of the plugin * @param {String[]} deps A list of dependencies for this * plugin. In most cases it's a reference to main.consumes. */ this.freezePublicAPI({ _events: [ /** * Fires when the plugin is loaded * @event load */ "load", /** * Fires before the plugin is unloaded * @event beforeUnload */ "beforeUnload", /** * Fires when the plugin is unloaded * @event unload */ "unload", /** * Fires when the plugin is enabled * @event enable */ "enable", /** * Fires when the plugin is disabled * @event disable */ "disable", /** * Fires any time a new listener is added. * * plugin.on('newListener', function (event, listener) { * // new listener added * }); * * @event newListener */ "newListener", /** * Fires any time a listener is removed. * * plugin.on('removeListener', function (event, listener) { * // listener is removed * }); * * @event removeListener */ "removeListener" ], /** * @property {Boolean} registered Specifies whether the plugin is registered * @readonly */ get registered() { return registered; }, /** * @property {Date} time The time when the plugin was registered * @readonly */ get time() { return time; }, /** * @property {Boolean} enabled Specifies whether the plugin is enabled * @readonly */ get enabled() { return !disabled; }, /** * @property {Boolean} loaded whether the plugin is loaded. * This happens by calling load or setting the name. * @readonly */ get loaded() { return loaded; }, /** * @property {String} name The name of the plugin */ get name() { return name; }, set name(val) { if (name == val) return; if (!name) { name = val; this.load(); } else throw new Error("Plugin Name Exception"); }, /** * Copies all methods and properties from `api` and then freezes * the plugin to prevent further changes to its API. * * @method freezePublicAPI * @param {Object} api simple object that defines the API **/ /** * Fetches the event emitter for this plugin. After freezing the * public API of this plugin, this method will no longer be * available. * * Note that there is a limit to the amount of event listeners * that can be placed on a plugin. This is to protect developers * from leaking event listeners. When this limit is reached * an error is thrown. Use the emit.setMaxListeners * to change the amount of listeners a plugin can have: * * var emit = plugin.getEmitter(); * emit.setMaxListeners(100); * * @method getEmitter * @return {Function} * @return {String} return.eventName The name of the event to emit * @return {Object} return.eventObject The data passed as the first argument to all event listeners * @return {Boolean} return.immediateEmit Specifies whether * to emit the event to event handlers that are set after * emitting this event. This is useful for instance when you * want to have a "load" event that others add listeners to * after the load event is called. **/ /** * Fetches a UI element. You can use this method both sync and async. * @param {String} name the id of the element to fetch * @param {Function} [callback] the function to call when the * element is available (could be immediately) **/ getElement: getElement, /** * Register an element for destruction during the destroy phase of * this plugin's lifecycle. * @param {AMLElement} element the element to register **/ addElement: addElement, /** * Register an event for destruction during the destroy phase of * this plugin's lifecycle. * @param {Array} ev Array containing three elements: * object, event-name, callback **/ addEvent: addEvent, /** * Register a function that is called during the destroy phase of * this plugin's lifecycle. * @param {Function} o function called during the destroy phase * of this plugin's lifecycle **/ addOther: addOther, /** * Loads this plugin into Cloud9 **/ load: load, /** * @ignore Enable this plugin **/ enable: enable, /** * @ignore Disable this plugin **/ disable: disable, /** * Unload this plugin from Cloud9 **/ unload: unload, /** * Removes all elements, events and other items registered for * cleanup by this plugin */ cleanUp: cleanUp, /** * Adds an event handler to this plugin. Note that unlike the * event implementation you know from the browser, you are * able to add the same listener multiple times to listen to * the same event. * * @param {String} name The name of this event * @param {Function} callback The function called when the event is fired * @param {Plugin} plugin The plugin that is responsible * for the event listener. Make sure to always add a reference * to a plugin when adding events in order for the listeners * to be cleaned up when the plugin unloads. If you forget * this you will leak listeners into Cloud9. * @fires newListener **/ on: function(eventName, callback, plugin) { // if (!declaredEvents[eventName]) // console.warn("Missing event description or unknown event '" + eventName + "' for plugin '" + name + "'", new Error().stack); event.on(eventName, callback, plugin); }, /** * Adds an event handler to this plugin and removes it after executing it once * @param {String} name the name of this event * @param {Function} callback the function called when the event is fired **/ once: function(eventName, callback) { // if (!declaredEvents[eventName]) // console.warn("Missing event description or unknown event '" + eventName + "' for plugin '" + name + "'"); event.once(eventName, callback); }, /** * Removes an event handler from this plugin * @param {String} name the name of this event * @param {Function} callback the function previously registered as event handler * @fires removeListener **/ off: event.removeListener.bind(event), /** * Returns an array of listeners for an event specified by `name` * @param {String} name the name of this event */ listeners: event.listeners.bind(event) }); } register(null, { ext: plugin, Plugin: Plugin }); } });