UNPKG

mojito

Version:

Mojito provides an architecture, components and tools for developers to build complex web applications faster.

1,065 lines (919 loc) 39.9 kB
/* * Copyright (c) 2011-2013, Yahoo! Inc. All rights reserved. * Copyrights licensed under the New BSD License. * See the accompanying LICENSE file for terms. */ /*jslint anon:true, sloppy:true, nomen:true*/ /*global YUI*/ // Set up a global client-side Mojito namespace if (!YUI._mojito) { /** * The top-level Mojito namespace. * @type {Object} */ YUI._mojito = { // A general cache object to be used by internal mojito only _cache: {}, _clientY: null, _clientYlog: null }; } YUI.add('mojito-client', function(Y, NAME) { // These methods are methods that potentially make XHR calls to retrieve // data from the server. When the Mojito client is "paused" by calling the // pause() function, all the methods below are queued as they are executed // instead of being fully executed. When the resume() function is called, // the pause queue is flushed and all the intercepted actions are taken at // that time. var PAUSEABLE = [ 'executeAction', 'doRender', 'doBroadcast', 'doListen', 'doUnlisten' ], log, lifecycleEvents, State = { PAUSED: 'paused', ACTIVE: 'active' }, // hook system handler globalHookHandler; // because there is a moment during startup when we need it, cache the // original Y instance for use as the log platform // And don't clobber the global if (!YUI._mojito._clientY) { YUI._mojito._clientY = Y; } // this is the heart of mojitProxy.render(), but it needs to be a separate // function called once we have mojit type details function privateRender(mp, data, view, viewEngine, cb) { var mojitView, renderer; if (!mp._views || !mp._views[view]) { cb(new Error('View "' + view + '" not found')); return; } data = data || {}; // this is presumed to be useful enough that we'll set it up for them data.mojit_assets = data.mojit_assets || mp._assetsRoot; mojitView = mp._views[view]; renderer = new Y.mojito.ViewRenderer(mojitView.engine, viewEngine); Y.log('Rendering "' + view + '" in Binder', 'debug', NAME); renderer.render(data, mp.type, mojitView['content-path'], { buffer: '', error: function(err) { cb(err); }, flush: function(data) { this.buffer += data; }, done: function(data) { this.buffer += data; cb(null, this.buffer); } }); } function setNewMojitView(viewData, mp) { Y.log('setting new view on mojit ' + mp._instanceId, 'debug', NAME); var newNode = Y.Node.create(viewData); mp._node.replace(newNode); mp._element = document.getElementById(mp._viewId); mp._node = new Y.Node(mp._element); if (Y.Lang.isFunction(mp._binder.onRefreshView)) { mp._binder.onRefreshView(mp._node, mp._element); } } // we have to match the children by instanceId to identify them and replace // their viewId with the actual viewId for the binder that should be // attached. function processChildren(children, binderMap) { var name, viewId, found; for (name in children) { if (children.hasOwnProperty(name)) { for (viewId in binderMap) { if (binderMap.hasOwnProperty(viewId) && !found) { if (binderMap[viewId].instanceId === children[name].instanceId) { children[name].viewId = viewId; found = true; } } } found = false; } } return children; } function bindNode(binder, node, element) { var handles = []; if (Y.Lang.isFunction(binder.bind)) { // Pass the "node" to the bind method try { binder.bind(node, element); } catch (e) { Y.log(e.stack || e.message, 'error', NAME); } } // all automatic event delegation if (Y.Lang.isFunction(binder.handleClick)) { // This code should be deferred till after the page has visibly // loaded handles.push(Y.delegate('click', binder.handleClick, node, function() { return true; }, binder)); } // TODO: add all the event delegation magic here. Y.log('Attached ' + handles.length + ' event delegates', 'debug', NAME); return handles; } // TODO: complete work to call this in the destroyMojitProxy function(). // this function is never called /iy function unbindNode(binder, handles) { var retainBinder = false; if (Y.Lang.isFunction(binder.unbind)) { // let the binder decide whether it wants to stick around in case // its node is reattached at some point in the future retainBinder = binder.unbind.call(binder); } if (handles) { Y.Array.each(handles, function(handle) { Y.log('Detaching event handle from binder', 'debug', NAME); handle.detach(); }); } return retainBinder; } function findParentProxy(mojits, childId) { var p; Y.Object.some(mojits, function(mojit) { Y.Object.some(mojit.children, function(child) { if (child.viewId === childId) { p = mojit.proxy; return true; } }); if (p) { return true; } }); return p; } function recordBoundMojit(mojits, parentid, newid, type) { if (parentid && mojits[parentid]) { if (!mojits[parentid].children) { mojits[parentid].children = {}; } mojits[parentid].children[newid] = { type: type, viewId: newid }; //console.log('recorded %s child of %s', newid, parentid); } } /** * The starting point for mojito to run in the browser. You can access one * instance of the Mojito Client running within the browser environment * through window.YMojito.client. * @module MojitoClient * @class Client * @constructor * @namespace Y.mojito * @param {Object} config The entire configuration object written by the * server to start up mojito. */ function MojitoClient(config) { this.page = Y.merge((config && config.page) || {}, { // rehydrating pageData model data: new Y.Model((config && config.page && config.page.data) || {}) }); this.timeLogStack = []; this.yuiConsole = null; this._pauseQueue = []; if (config) { // HookSystem::StartBlock // TODO: validate that config.perf could be set for client. if (config.perf) { globalHookHandler = {}; // enabling perf group // TODO: this should not be executed here. Y.mojito.hooks.enableHookGroup(globalHookHandler, 'mojito-perf'); } // HookSystem::EndBlock this.init(config); } } lifecycleEvents = {}; /** * Subscribe to a MojitoClient lifecycle event. * @method subscribe * @param {string} evt name of event to subscribe to. * @param {function(data)} cb callback called when the event fires. */ MojitoClient.subscribe = function(evt, cb) { // Subscribe works only in appLevel if (Y && YUI._mojito._clientY && (Y._yuid !== YUI._mojito._clientY._yuid)) { // Y.log('Applevel subscribe ? ' + Y._yuid + " ==== " + // YUI._mojito._clientY._yuid, 'error', NAME); return; } if (!lifecycleEvents[evt]) { lifecycleEvents[evt] = []; } lifecycleEvents[evt].push(cb); }; /** * Fires a lifecycle event. * @method fireLifecycle * @param {string} evt The name of event to fire. * @param {Object} data The data to pass to listeners. * @private */ function fireLifecycle(evt, data) { var cbs = lifecycleEvents[evt], c; if (!cbs || !cbs.length) { return; } for (c = 0; c < cbs.length; c += 1) { cbs[c](data); } } /** * Fired at the beginning of the startup of MojitoClient. * * The data contains the following: * <dl> * <dt><code>config</code></dt> * <dd>the config object used to initialize the MojitoClient</dd> * </dl> * * Any change to the config will be picked up and used by MojitoClient. * @param {Object} data The data for the event. */ /** * Fired at the end of the startup of MojitoClient. * @param {Object} data The data for the event. (Empty in this case). */ /** * Fired before the binders are attached to the page. * * The data contains the following: * <dl> * <dt><code>binderMap</code></dt> * <dd>the details of the binders to attach to the page</dd> * <dt><code>parentId</code></dt> * <dd>[optional] the parent binder view id to attach any children</dd> * <dt><code>topLevelMojitViewId</code></dt> * <dd>[optional] the topmost (root) binder view id to attach as a child to * the parent</dd> * </dl> * * Any change to the data will be picked up and used by MojitoClient. * @param {Object} data The data for the event. */ /** * Fired after the binders are attached to the page. * @param {Object} data The data for the event. (Empty in this case). */ MojitoClient.prototype = { init: function(config) { var that = this, appConfig = config.appConfig, forwardConfig = { config: config, // pass globalHookhandler to addons that may want to use hooks globalHookHandler: globalHookHandler }; // static app config and app config are equivalent in the client runtime this.page.staticAppConfig = this.page.appConfig = appConfig; this.page.routes = config.routes; fireLifecycle('pre-init', forwardConfig); // if we didn't originaly have hooks enabled, copy back from config object. // This is the case where an add-on module wants to turn on hooks and // created an instance of the globalHookHandler. if (!globalHookHandler) { globalHookHandler = forwardConfig.globalHookHandler; } // HookSystem::StartBlock Y.mojito.hooks.hook('mojitoClient', globalHookHandler, 'start', this); // HookSystem::EndBlock // YUI Console if (appConfig && appConfig.yui && appConfig.yui.showConsoleInClient && !that.yuiConsole) { YUI().use('console-filters', function(Y) { Y.one('body').addClass('yui3-skin-sam'); that.yuiConsole = new Y.Console({ plugins: [Y.Plugin.ConsoleFilters], logSource: Y.Global, height: 600 }); that.yuiConsole.render(); that.init(config); }); return; } if (Y.mojito.TunnelClient) { this.tunnel = new Y.mojito.TunnelClient(config.appConfig); } // Note this is the client-store, not the server-store. this.resourceStore = new Y.mojito.ResourceStore(config); this.dispatcher = Y.mojito.Dispatcher.init(this.resourceStore, this.tunnel); // request context from server this.context = config.context; // application configuration this.config = config; // create listener bag for mojit comms this._listeners = {}; // the mojits represented in the current DOM, keyed by DOM element // id this._mojits = {}; // HookSystem::StartBlock Y.mojito.hooks.hook('mojitoClient', globalHookHandler, 'end', this); // HookSystem::EndBlock /* FUTURE -- perhaps only do this once a user needs it var singletons; singletons = { tunnel: this.tunnel, resourceStore: this.resourceStore, dispatcher: this.dispatcher } fireLifecycle('made-singletons', singletons); // allow the event listeners to modify the singletons this.tunnel = singletons.tunnel; this.resourceStore = singletons.resourceStore; this.dispatcher = singletons.dispatcher; */ this.attachBinders(config.binderMap); // wrap pause-able methods Y.Array.each(PAUSEABLE, function(mName) { var me = this, originalMethod = me[mName]; this[mName] = function() { // during execution of these pauseable function, we'll check // to see if the client is in a paused state if (me._state === State.PAUSED) { // now just queue the method call with original function // and args for execution on resume() me._pauseQueue.push({ fn: originalMethod, args: arguments }); } else { // not paused, so go ahead and apply the function originalMethod.apply(me, arguments); } }; }, this); this._state = State.ACTIVE; fireLifecycle('post-init', {}); }, /** * Given a set of binder information, initialize binder instances and * bind them to the page. * @method attachBinders * @private * @param {Object} binderMap viewId ==> binder data, contains all we * need from the mojit dispatch's meta object about all the binders * that were executed to create the DOM addition recently added to * the document. * @param {string} parentId the parent binder view id to attach any * children. * @param {string} topLevelMojitViewId the topmost (root) binder view * id to attach as a child to the parent. */ attachBinders: function(binderMap, parentId, topLevelMojitViewId) { var context = this.context, me = this, newMojitProxies = [], parent, topLevelMojitObj, totalBinders = Y.Object.size(binderMap), bindersComplete = 0, onBinderComplete, // Note: This here so we can get access view meta data for // each binder store = this.resourceStore, eventData = { binderMap: binderMap, parentId: parentId, topLevelMojitViewId: topLevelMojitViewId }, pageData = this.page.data; fireLifecycle('pre-attach-binders', eventData); this.pause(); binderMap = eventData.binderMap; parentId = eventData.parentId; topLevelMojitViewId = eventData.topLevelMojitViewId; // HookSystem::StartBlock Y.mojito.hooks.hook('mojitoClientBind', globalHookHandler, 'start', this); // HookSystem::EndBlock if (!totalBinders) { // HookSystem::StartBlock Y.mojito.hooks.hook('mojitoClientBind', globalHookHandler, 'resume', this); // HookSystem::EndBlock me.resume(); // HookSystem::StartBlock Y.mojito.hooks.hook('mojitoClientBind', globalHookHandler, 'end', this); // HookSystem::EndBlock fireLifecycle('post-attach-binders', {}); return; } onBinderComplete = function() { // only run the function when all binders have completed bindersComplete += 1; if (bindersComplete < totalBinders) { return; } // now that all binders have been initialized and accounted // for... // first, we must create the MojitClient's state of the binders // before binding, in case the binders' bind() function tries to // do anything that includes children Y.Array.each(newMojitProxies, function(item) { var proxy = item.proxy, children = item.children; // 'me' here is the MojitoClient instance. me._mojits[proxy._viewId] = { proxy: proxy, children: children }; }); // now we'll loop through again and do the binding, saving the // handles Y.Array.each(newMojitProxies, function(item) { var viewid = item.proxy.getId(), mojit = me._mojits[viewid], proxy = item.proxy; mojit.handles = bindNode(proxy._binder, proxy._node, proxy._element); recordBoundMojit(me._mojits, parentId, viewid, proxy.type); }); // HookSystem::StartBlock Y.mojito.hooks.hook('mojitoClientBindComplete', globalHookHandler, 'start', this); // HookSystem::EndBlock me.resume(); // HookSystem::StartBlock Y.mojito.hooks.hook('mojitoClientBindComplete', globalHookHandler, 'end', this); // HookSystem::EndBlock fireLifecycle('post-attach-binders', {}); }; // loop over the binder map, load, use, and instantiate them Y.Object.each(binderMap, function(binderData, viewId) { var type = binderData.type, base = binderData.base, binderName = binderData.name, instanceId = binderData.instanceId, mojitProxy, binderClass, children, binder, mojitNode, element; // Make sure viewIds's are not bound to more than once if (me._mojits[viewId]) { Y.log('Not rebinding binder for ' + type + ' for DOM node ' + viewId, 'debug', NAME); onBinderComplete(); return; } if (!binderName) { Y.log('No binder for ' + type + '.' + binderData.action, 'warn', NAME); onBinderComplete(); return; } // "Y.mojito.binders" is blind to all new "binders" added to // the page we have to "use()" any binder name we are given // to have access to it. Y.use(binderData.name, function(Y) { // Check again to make sure viewIds's are not bound // more than once, just in case they were bound during // the async fetch for the binder if (me._mojits[viewId]) { Y.log('Not rebinding binder for ' + binderData.type + ' for DOM node ' + viewId, 'debug', NAME); onBinderComplete(); return; } element = document.getElementById(viewId); if (!element) { Y.log('Did not find DOM node "' + viewId + '" for binder "' + binderName + '"', 'warn', NAME); onBinderComplete(); return; } mojitNode = new Y.Node(element); // BY reference here is the 'use()' return value...the // Binder class we need to access. binderClass = Y.mojito.binders[binderName]; binder = Y.mojito.util.heir(binderClass); Y.log('Created binder "' + binderName + '" for DOM node "' + viewId + '"', 'debug', NAME); if (binderData.children) { children = processChildren(binderData.children, binderMap); } // One mojitProxy per binder. The mp is how client code // gets to the binder...they don't hold refs to anything // but the mp. (close enough). mojitProxy = new Y.mojito.MojitProxy({ // private action: binderData.action, binder: binder, base: base, node: mojitNode, element: element, viewId: viewId, instanceId: instanceId, client: me, store: store, // public type: type, config: { children: children }, proxied: binderData.proxied, data: binderData.data, pageData: pageData, context: context }); // If client is paused, proxy must be paused if (me._state === State.PAUSED) { mojitProxy._pause(); } newMojitProxies.push({ proxy: mojitProxy, children: children }); if (Y.Lang.isFunction(binder.init)) { try { binder.init(mojitProxy); } catch (e) { Y.log(e.stack || e.message, 'error', NAME); } } onBinderComplete(); }); }, this); }, /** * Used for binders to execute their actions through the Mojito * framework through their proxies. * @method executeAction * @param {Object} command must contain mojit id and action to execute. * @param {String} viewId the view id of the current mojit, which is * executing the action. * @param {Function} cb callback to run when complete. * @private */ executeAction: function(command, viewId, cb) { var outputHandler; // Sending a command to dispatcher that defines our action execution Y.log('Executing "' + (command.instance.base || '@' + command.instance.type) + '/' + command.action + '" on the client.', 'debug', NAME); command.context = this.context; outputHandler = new Y.mojito.OutputHandler(viewId, cb, this); // HookSystem::StartBlock if (Y.mojito.hooks) { outputHandler.hook = globalHookHandler; } // HookSystem::EndBlock this.dispatcher.dispatch(command, outputHandler); }, doRender: function(mp, data, view, cb) { var viewEngine = this.config.appConfig.viewEngine; if (!mp._views || !mp._assetsRoot) { this.resourceStore.expandInstance({type: mp.type}, mp.context, function(err, typeInfo) { if (err) { cb(new Error( 'Failed to load mojit information for ' + mp.type )); return; } mp._views = typeInfo.views; mp._assetsRoot = typeInfo.assetsRoot; privateRender(mp, data, view, viewEngine, cb); }); } else { privateRender(mp, data, view, viewEngine, cb); } }, doBroadcast: function(eventId, source, payload, opts) { opts = opts || {}; var tgtInstId, tgtViewId, child = opts.target ? this._mojits[source].children[opts.target.slot] : null; if (opts && opts.target) { if (opts.target.slot && child) { tgtInstId = child.instanceId; // find the target of the message Y.Object.each(this._mojits, function(v, k) { if (v.proxy._instanceId === tgtInstId) { tgtViewId = k; } }); // if there was no target found, give an error and return if (!tgtViewId) { Y.log('No broadcast target found for ' + opts.target.slot + ':' + tgtInstId, 'warn', NAME); return; } } else if (opts.target.viewId) { tgtViewId = opts.target.viewId; } } if (this._listeners[eventId]) { Y.Array.each(this._listeners[eventId], function(listener) { if (!tgtViewId || tgtViewId === listener.viewId) { listener.cb({ data: payload, source: source }); } }); } }, doListen: function(eventId, viewId, callback) { if (!this._listeners[eventId]) { this._listeners[eventId] = []; } this._listeners[eventId].push({ viewId: viewId, cb: callback }); }, doUnlisten: function(viewId, needleEvent) { var listeners = this._listeners, eventType; function processListenerArray(arr, id) { var i = 0; while (i < arr.length) { if (arr[i].viewId === id) { arr.splice(i, 1); // no increment. i is now the "next" index } else { i += 1; } } return arr; } // if there is only one event to unlisten, do it quickly if (needleEvent) { processListenerArray(listeners[needleEvent], viewId); } else { // but if we need to unlisten to all callbacks registered by // this binder, we must loop over the entire listener object for (eventType in listeners) { if (listeners.hasOwnProperty(eventType)) { processListenerArray(listeners[eventType], viewId); } } } }, /** * @method destroyMojitProxy * @param {String} id The mojit's viewId to destroy * @param {Boolean} retainNode */ destroyMojitProxy: function(viewId, retainNode) { var mojits = this._mojits, parent, instanceId; if (mojits[viewId] && mojits[viewId].proxy) { // lookup instanceId for this viewId instanceId = mojits[viewId].proxy._instanceId; // TODO: activate call to unbindNode below: // unbindNode(mojits[viewId].proxy._binder, // mojits[viewId].handles); mojits[viewId].proxy._destroy(retainNode); // is there a better alternative for this delete? // maybe not, but it might introduce a perf penalty // if a lot of mojits are created and destroyed, // and we can't use the undefined trick because // viewId has an infinite domain delete mojits[viewId]; // We don't manage binder children automatically, but any time a // new child is added or removed, we should at least give the // application code a chance to stay up to date if they want to. // The only gap is when a mojit destroys itself. // onChildDestroyed is called whenever a binder is destroyed so // any parents can be notified. parent = findParentProxy(mojits, viewId); if (parent && parent._binder.onChildDestroyed && Y.Lang.isFunction(parent._binder.onChildDestroyed)) { parent._binder.onChildDestroyed({ id: viewId }); } } }, /** * Pause the Mojito Client and all mojits that are running. This will * notify all binders that they have been paused by calling their * onPause() functions. It will prevent the immediate execution of * several mojit proxy operations that might cause a long process to * begin (especially things that might go to the server). * * To resume, simply call .resume(). This will immediately execute all * actions that occurred while Mojito was paused. * @method pause */ pause: function() { if (this._state === State.PAUSED) { Y.log('Cannot "pause" the mojito client because it has' + ' already been paused.', 'warn', NAME); return; } this._state = State.PAUSED; Y.Object.each(this._mojits, function(moj) { moj.proxy._pause(); }); Y.log('Mojito Client state: ' + this._state + '.', 'debug', NAME); }, /** * Resumes the Mojito client after it has been paused (see method * "pause"). If there are any queued actions that were executed and * cached during the pause, calling resume() will immediately execute * them. All binders are notified through their onResume() function that * they are been resumed. * @method resume */ resume: function() { if (this._state !== State.PAUSED) { Y.log('Cannot "resume" the mojito client because it was' + ' never paused.', 'warn', NAME); return; } this._state = State.ACTIVE; Y.Object.each(this._mojits, function(moj) { moj.proxy._resume(); }); Y.Array.each(this._pauseQueue, function(queuedItem) { var fn = queuedItem.fn, args = queuedItem.args; fn.apply(this, args); }, this); this._pauseQueue = []; Y.log('Mojito Client state: ' + this._state + '.', 'debug', NAME); }, refreshMojitView: function(mp, opts, cb) { var my = this; mp.invoke(mp._action, opts, function(err, data, meta) { if (err) { if (typeof cb === 'function') { cb(new Error(err)); return; } throw new Error(err); } /* * The new markup returned from the server has all new DOM ids * within it, but we don't want to use them. Before doing any * DOM stuff, we are going to replace all the new view ids with * our current view ids for this mojit view as well as any * children that have come along for the ride. */ var idReplacements = {}, // from: to metaBinderViewId, mBinder, freshBinders = {}, clientMojitViewId, clientMojit, processMojitChildrenForIdReplacements; /* * Recursive function used to walk down the hierarchy of * children in order to replace every view id within the meta * data */ processMojitChildrenForIdReplacements = function(clientChildren, metaChildren, idRepls) { var metaChild, childMojitProxy, metaSubChildren, slot; if (!metaChildren || !clientChildren) { return; } for (slot in metaChildren) { if (metaChildren.hasOwnProperty(slot)) { metaChild = metaChildren[slot]; if (clientChildren && clientChildren[slot]) { childMojitProxy = clientChildren[slot].proxy; } if (childMojitProxy) { metaSubChildren = meta.binders[ metaChild.viewId ].children; idRepls[metaChild.viewId] = childMojitProxy.getId(); if (metaSubChildren) { processMojitChildrenForIdReplacements( my.mojits[childMojitProxy.getId()]. children, metaSubChildren, idRepls ); } } } } }; for (clientMojitViewId in my._mojits) { if (my._mojits.hasOwnProperty(clientMojitViewId)) { clientMojit = my._mojits[clientMojitViewId]; for (metaBinderViewId in meta.binders) { if (meta.binders.hasOwnProperty(metaBinderViewId)) { mBinder = meta.binders[metaBinderViewId]; if (mBinder.instanceId === clientMojit.proxy._instanceId) { Y.log('matched instanceId ' + mBinder.instanceId, 'debug', NAME ); idReplacements[metaBinderViewId] = clientMojitViewId; processMojitChildrenForIdReplacements( my._mojits[clientMojit.proxy.getId()]. children, mBinder.children, idReplacements ); } } } } } Y.Object.each(idReplacements, function(to, from) { var regex = new RegExp(from, 'g'); data = data.replace(regex, to); }); setNewMojitView(data, mp); // Do a "light bind" for each child, keeping track of any // binders that need a "full bind". We'll bind those in the // attachBinders call below this loop. Y.Object.each(meta.children, function(child, slot) { var childViewId = idReplacements[child.viewId], childMojit = my._mojits[childViewId], childProxy, childNode, childElement, childBinder; // may not be a binder for this mojit child, so there would // be no mojit proxy yet if (!childMojit) { // this must be a new binder instance that we need to // instantiate freshBinders[child.viewId] = meta.binders[child.viewId]; return; } childProxy = my._mojits[childViewId].proxy; childNode = mp._node.one('#' + childViewId); childElement = childNode._node; childBinder = childProxy._binder; // set new node and element into the mojit proxy object childProxy._node = childNode; childProxy._element = childElement; if (Y.Lang.isFunction(childBinder.onRefreshView)) { childBinder.onRefreshView(childNode, childElement); } else if (Y.Lang.isFunction(childBinder.bind)) { try { childBinder.bind(childNode, childElement); } catch (e) { Y.log(e.stack || e.message, 'error', NAME); } } }); // Do a "full bind" on the new binders we tracked in the loop // above. These need the full treatment. my.attachBinders(freshBinders); if (cb) { cb(data, meta); } }); } }; Y.namespace('mojito').Client = MojitoClient; }, '0.1.0', {requires: [ 'io-base', 'event-delegate', 'node-base', 'querystring-stringify-simple', 'mojito', 'model', 'mojito-dispatcher', 'mojito-route-maker', 'mojito-client-store', 'mojito-mojit-proxy', 'mojito-tunnel-client', 'mojito-output-handler', 'mojito-util', 'mojito-hooks' ]});