diff --git a/examples/developer-guide/device_assets/application.json b/examples/developer-guide/device_assets/application.json index 353343b81..da3e53a8b 100644 --- a/examples/developer-guide/device_assets/application.json +++ b/examples/developer-guide/device_assets/application.json @@ -13,5 +13,6 @@ } } } - } + }, + { "settings": [ "device:iphone" ], "selector": "iphone" } ] diff --git a/examples/developer-guide/device_views/application.json b/examples/developer-guide/device_views/application.json index 2191ce90d..2d8e0704e 100644 --- a/examples/developer-guide/device_views/application.json +++ b/examples/developer-guide/device_views/application.json @@ -7,6 +7,9 @@ "type" : "device" } } - } + }, + { "settings": [ "device:android" ], "selector": "android" }, + { "settings": [ "device:blackberry" ], "selector": "blackberry" }, + { "settings": [ "device:iphone" ], "selector": "iphone" } ] diff --git a/examples/developer-guide/scroll_views/application.json b/examples/developer-guide/scroll_views/application.json index 2f3874a3d..5ae51dccc 100644 --- a/examples/developer-guide/scroll_views/application.json +++ b/examples/developer-guide/scroll_views/application.json @@ -1,23 +1,24 @@ [ { - "settings": [ "master" ], - "specs": { - "frame" : { - "type" : "HTMLFrameMojit", - "config": { - "deploy": true, - "child" : { - "type" : "scroll" - }, - "assets": { - "top": { - "css":[ - "/static/scroll/assets/index.css" - ] + "settings": [ "master" ], + "specs": { + "frame" : { + "type" : "HTMLFrameMojit", + "config": { + "deploy": true, + "child" : { + "type" : "scroll" + }, + "assets": { + "top": { + "css":[ + "/static/scroll/assets/index.css" + ] + } } } } } - } -} + }, + { "settings": [ "device:iphone" ], "selector": "iphone" } ] diff --git a/examples/developer-guide/scroll_views/mojits/scroll/lang/en.js b/examples/developer-guide/scroll_views/mojits/scroll/lang/scroll_en.js similarity index 100% rename from examples/developer-guide/scroll_views/mojits/scroll/lang/en.js rename to examples/developer-guide/scroll_views/mojits/scroll/lang/scroll_en.js diff --git a/examples/getting-started-guide/part4/paged-yql/application.json b/examples/getting-started-guide/part4/paged-yql/application.json index 524272a8c..b58ffbdcc 100644 --- a/examples/getting-started-guide/part4/paged-yql/application.json +++ b/examples/getting-started-guide/part4/paged-yql/application.json @@ -23,5 +23,17 @@ "type": "PagedFlickr" } } + }, + { + "settings": [ "device:iphone" ], + "selector": "iphone" + }, + { + "settings": [ "device:android" ], + "selector": "droid" + }, + { + "settings": [ "device:android", "environment:dev" ], + "selector": "devdroid" } ] diff --git a/examples/getting-started-guide/part5/flickr-list/application.json b/examples/getting-started-guide/part5/flickr-list/application.json index a9421a3f9..a1666d1b7 100644 --- a/examples/getting-started-guide/part5/flickr-list/application.json +++ b/examples/getting-started-guide/part5/flickr-list/application.json @@ -50,5 +50,6 @@ "type": "FlickrDetail" } } - } + }, + { "settings": [ "device:iphone" ], "selector": "iphone" } ] diff --git a/examples/getting-started-guide/part5/flickr-list/routes.json b/examples/getting-started-guide/part5/flickr-list/routes.json index 2e80c1c1b..54c7733e3 100644 --- a/examples/getting-started-guide/part5/flickr-list/routes.json +++ b/examples/getting-started-guide/part5/flickr-list/routes.json @@ -11,7 +11,7 @@ "flickr_base": { "verbs": ["get"], "path": "/flickr", - "param": "page=1&image=0", + "params": "page=1&image=0", "call": "flickr.index" } diff --git a/examples/newsboxes/application.json b/examples/newsboxes/application.json index 74a4827b9..e048096ab 100644 --- a/examples/newsboxes/application.json +++ b/examples/newsboxes/application.json @@ -68,4 +68,6 @@ } } } -}] +}, +{ "settings": [ "device:opera-mini" ], "selector": "opera-mini" } +] diff --git a/examples/sandbox/ex1/application.json b/examples/sandbox/ex1/application.json index 1f28639a5..814ea9c16 100644 --- a/examples/sandbox/ex1/application.json +++ b/examples/sandbox/ex1/application.json @@ -40,5 +40,6 @@ "action": "jsonReturn" } } - } + }, + { "settings": [ "device:iphone" ], "selector": "iphone" } ] diff --git a/source/lib/app/addons/ac/deploy.server.js b/source/lib/app/addons/ac/deploy.server.js index 1e65db40e..b1478df40 100644 --- a/source/lib/app/addons/ac/deploy.server.js +++ b/source/lib/app/addons/ac/deploy.server.js @@ -80,11 +80,11 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { var store = this.rs, contextServer = this.ac.context, - appConfigServer = store.getAppConfig(contextServer, - 'application'), + appConfigServer = store.getAppConfig(contextServer), contextClient, appConfigClient, yuiConfig = {}, + fwConfig, yuiConfigEscaped, yuiConfigStr, yuiModules, @@ -97,7 +97,6 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { binder, i, id, - instances = {}, clientConfig = {}, clientConfigEscaped, clientConfigStr, @@ -109,11 +108,12 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { type, module, path, - pathToRoot; + pathToRoot, + urls; contextClient = Y.mojito.util.copy(contextServer); contextClient.runtime = 'client'; - appConfigClient = store.getAppConfig(contextClient, 'application'); + appConfigClient = store.getAppConfig(contextClient); clientConfig.context = contextClient; if (appConfigClient.yui && appConfigClient.yui.config) { @@ -137,8 +137,7 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { yuiConfig.loaderPath = appConfigClient.yui.loader; } - clientConfig.store = store.serializeClientStore(contextClient, - instances); + clientConfig.store = store.serializeClientStore(contextClient); usePrecomputed = appConfigServer.yui && appConfigServer.yui.dependencyCalculations && (-1 !== @@ -154,6 +153,8 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { useOnDemand = true; } + urls = store.store.getAllURLs(); + // Set the YUI URL to use on the client (This has to be done // before any other scripts are added) if (appConfigClient.yui && appConfigClient.yui.url) { @@ -176,6 +177,7 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { yuiModules = ['yui']; yuiJsUrlContains.yui = true; if (useOnDemand) { + fwConfig = store.store.getFrameworkConfig(); yuiModules.push('get'); yuiJsUrlContains.get = true; yuiModules.push('loader-base'); @@ -184,10 +186,8 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { yuiJsUrlContains['loader-rollup'] = true; yuiModules.push('loader-yui3'); yuiJsUrlContains['loader-yui3'] = true; - for (i = 0; i < store.store._fwConfig. - ondemandBaseYuiModules.length; i += 1) { - module = - store.store._fwConfig.ondemandBaseYuiModules[i]; + for (i = 0; i < fwConfig.ondemandBaseYuiModules.length; i += 1) { + module = fwConfig.ondemandBaseYuiModules[i]; yuiModules.push(module); yuiJsUrlContains[module] = true; } @@ -209,7 +209,7 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { path = binder.needs[module]; // Anything we don't know about we'll assume is // a YUI library module. - if (!store.fileFromStaticHandlerURL(path)) { + if (!urls[path]) { yuiModules.push(module); yuiJsUrlContains[module] = true; } @@ -240,10 +240,10 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { // fw & app scripts. if (useOnDemand) { // add all framework-level and app-level code - this.addScripts('bottom', store.getYuiConfigFw('client', - contextClient).modules); - this.addScripts('bottom', store.getYuiConfigApp('client', - contextClient).modules); + this.addScripts('bottom', store.store.yui.getConfigShared( + 'client', + contextClient + ).modules, false); } // add binders' dependencies @@ -312,7 +312,7 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { // Add all the scripts we have collected assetHandler.addAssets( - this.getScripts(appConfigServer.embedJsFilesInHtmlFrame) + this.getScripts(appConfigServer.embedJsFilesInHtmlFrame, urls) ); // Add the boot script assetHandler.addAsset('blob', 'bottom', initializer); @@ -344,10 +344,14 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { * @private * @param {bool} embed Should returned scripts be embedded in script * tags. + * @param {object} urls Mapping of URLs to filesystem paths. The keys + * are the URLs, and the values are the cooresponding filesystem + * paths. * @return {object} An object containing script descriptors. */ - getScripts: function(embed) { + getScripts: function(embed, urls) { var i, + path, x, assets = {}, blob = { @@ -359,16 +363,14 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { // Walk over the scripts and check what we can do for (i in this.scripts) { if (this.scripts.hasOwnProperty(i)) { - if (embed && this.rs._staticURLs[i]) { - //blob.content+= fs.readFileSync(this.rs._staticURLs[i], - // 'utf8'); - //delete this.scripts[i]; + path = urls[i]; + if (embed && path) { this.scripts[i] = { type: 'blob', position: 'bottom', content: '' + minify(fs.readFileSync(path, 'utf8')) + + '' }; } else { this.scripts[i] = { diff --git a/source/lib/app/addons/rs/config.server.js b/source/lib/app/addons/rs/config.server.js new file mode 100644 index 000000000..b7e260358 --- /dev/null +++ b/source/lib/app/addons/rs/config.server.js @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2012, 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*/ + + +/** + * The Resource Store is a Y.Base -- a host for Y.Plugins. + * Each Addon provides additional functions through a namespace that is + * attached directly to the resource store. + * @module ResourceStoreAddon + */ + + +/** + * @class RSAddonConfig + * @extension ResourceStore.server + */ +YUI.add('addon-rs-config', function(Y, NAME) { + + var libfs = require('fs'), + libpath = require('path'), + // TODO: move YCB into a node module + libycb = require(libpath.join(__dirname, '../../../libs/ycb')); + + function RSAddonConfig() { + RSAddonConfig.superclass.constructor.apply(this, arguments); + } + RSAddonConfig.NS = 'config'; + + Y.extend(RSAddonConfig, Y.Plugin.Base, { + + /** + * This methods is part of Y.Plugin.Base. See documentation for that for details. + * @method initializer + * @param {object} config Configuration object as per Y.Plugin.Base + * @return {nothing} + */ + initializer: function(config) { + this.appRoot = config.appRoot; + this.mojitoRoot = config.mojitoRoot; + this.afterHostMethod('findResourceVersionByConvention', this.findResourceVersionByConvention, this); + this.beforeHostMethod('parseResourceVersion', this.parseResourceVersion, this); + + this._jsonCache = {}; // fullPath: contents as JSON object + this._ycbCache = {}; // fullPath: YCB config object + this._ycbDims = this._readYcbDimensions(); + }, + + + /** + * Returns the YCB dimensions for the application. + * @method getDimensions + * @return {object} the YCB dimensions structure for the app + */ + getDimensions: function() { + return Y.clone(this._ycbDims, true); + }, + + + /** + * Reads and parses a JSON file. + * @method readConfigJSON + * @param {string} fullPath path to JSON file + * @return {user-defined} contents of JSON file + */ + // TODO: async interface + readConfigJSON: function(fullPath) { + var json, + contents; + if (!libpath.existsSync(fullPath)) { + return {}; + } + json = this._jsonCache[fullPath]; + if (!json) { + try { + contents = libfs.readFileSync(fullPath, 'utf-8'); + json = Y.JSON.parse(contents); + } catch (e) { + throw new Error('Error parsing JSON file: ' + fullPath); + } + this._jsonCache[fullPath] = json; + } + return Y.clone(json, true); + }, + + + /** + * Reads a configuration file that is in YCB format. + * @method readConfigYCB + * @param {object} ctx runtime context + * @param {string} fullPath path to the YCB file + * @return {object} the contextualized configuration + */ + // TODO: async interface + readConfigYCB: function(fullPath, ctx) { + var store = this.get('host'), + cacheKey, + json, + ycb; + + ctx = store.mergeRecursive(store.getStaticContext(), ctx); + + store.validateContext(ctx); + + ycb = this._ycbCache[fullPath]; + if (!ycb) { + json = this.readConfigJSON(fullPath); + json = this._ycbDims.concat(json); + ycb = new libycb.Ycb(json); + this._ycbCache[fullPath] = ycb; + } + return ycb.read(ctx, {}); + }, + + + /** + * Using AOP, this is called after the ResourceStore's version. + * @method findResourceVersionByConvention + * @param {object} source metadata about where the resource is located + * @param {string} mojitType name of mojit to which the resource likely belongs + * @return {object||null} for config file resources, returns metadata signifying that + */ + findResourceVersionByConvention: function(source, mojitType) { + var fs = source.fs, + use = false; + + // we only care about files + if (!fs.isFile) { + return; + } + // we don't care about files in subdirectories + if ('.' !== fs.subDir) { + return; + } + // we only care about json files + if ('.json' !== fs.ext) { + return; + } + // use package.json for the app and the mojit + if ('package' === fs.basename && 'bundle' !== fs.rootType) { + use = true; + } + // use all configs in the application + if ('app' === fs.rootType) { + use = true; + } + // use configs from non-shared mojit resources + if (mojitType && 'shared' !== mojitType) { + use = true; + } + if (!use) { + return; + } + + return new Y.Do.AlterReturn(null, { + type: 'config' + }); + }, + + + /** + * Using AOP, this is called before the ResourceStore's version. + * @method parseResourceVersion + * @param {object} source metadata about where the resource is located + * @param {string} type type of the resource + * @param {string} subtype subtype of the resource + * @param {string} mojitType name of mojit to which the resource likely belongs + * @return {object||null} for config file resources, returns the resource metadata + */ + parseResourceVersion: function(source, type, subtype, mojitType) { + var baseParts, + res; + + if ('config' !== type) { + return; + } + + baseParts = source.fs.basename.split('.'); + if (baseParts.length !== 1) { + Y.log('invalid config filename. skipping ' + source.fs.fullPath, 'warn', NAME); + return; + } + res = { + source: source, + type: 'config', + affinity: 'common', + selector: '*' + }; + if ('app' !== source.fs.rootType) { + res.mojit = mojitType; + } + res.name = libpath.join(source.fs.subDir, baseParts.join('.')); + res.id = [res.type, res.subtype, res.name].join('-'); + return new Y.Do.Halt(null, res); + }, + + + /** + * Read the application's dimensions.json file for YCB processing. If not + * available, fall back to the framework's default dimensions.json. + * @private + * @method _readYcbDimensions + * @return {array} contents of the dimensions.json file + */ + _readYcbDimensions: function() { + var path = libpath.join(this.appRoot, 'dimensions.json'); + if (!libpath.existsSync(path)) { + path = libpath.join(this.mojitoRoot, 'dimensions.json'); + } + return this.readConfigJSON(path); + } + + + }); + Y.namespace('mojito.addons.rs'); + Y.mojito.addons.rs.config = RSAddonConfig; + +}, '0.0.1', { requires: ['plugin', 'oop', 'json-parse']}); diff --git a/source/lib/app/addons/rs/selector.server.js b/source/lib/app/addons/rs/selector.server.js new file mode 100644 index 000000000..0f0d69e89 --- /dev/null +++ b/source/lib/app/addons/rs/selector.server.js @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2012, 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*/ + + +/** + * @module ResourceStoreAddon + */ + +/** + * @class RSAddonSelector + * @extension ResourceStore.server + */ +YUI.add('addon-rs-selector', function(Y, NAME) { + + var libpath = require('path'), + libycb = require(libpath.join(__dirname, '../../../libs/ycb')); + + function RSAddonSelector() { + RSAddonSelector.superclass.constructor.apply(this, arguments); + } + RSAddonSelector.NS = 'selector'; + RSAddonSelector.DEPS = ['config']; + + Y.extend(RSAddonSelector, Y.Plugin.Base, { + + /** + * This methods is part of Y.Plugin.Base. See documentation for that for details. + * @method initializer + * @param {object} config Configuration object as per Y.Plugin.Base + * @return {nothing} + */ + initializer: function(config) { + var dims, + json; + this.appRoot = config.appRoot; + this.mojitoRoot = config.mojitoRoot; + + dims = config.host.config.getDimensions(); + json = config.host.config.readConfigJSON(libpath.join(this.appRoot, 'application.json')); + json = dims.concat(json); + // TODO: use rs.config for this too + this._appConfigYCB = new libycb.Ycb(json); + }, + + + /** + * Returns a list of all priority-ordered selector lists (POSLs). + * @method getAllPOSLs + * @return {array} list of priority-ordered selector lists + */ + getAllPOSLs: function() { + var c, + ctx, + ctxs, + posl, + posls = {}; + ctxs = this._listUsedContexts(); + for (c = 0; c < ctxs.length; c += 1) { + ctx = ctxs[c]; + posl = this.getPOSLFromContext(ctx); + posls[Y.JSON.stringify(posl)] = posl; + } + ctxs = null; // free a bunch of memory + return Y.Object.values(posls); + }, + + + /** + * Returns the priority-ordered selector list (POSL) for the context. + * @method getPOSLFromContext + * @param {object} ctx runtime context + * @return {array} priority-ordered selector list + */ + getPOSLFromContext: function(ctx) { + var store = this.get('host'), + sels = ['*'], + p, + part, + parts; + + store.validateContext(ctx); + + // TODO: use rs.config for this too + parts = this._appConfigYCB.readNoMerge(ctx, {}); + for (p = 0; p < parts.length; p += 1) { + part = parts[p]; + if (part.selector && store.selectors[part.selector]) { + sels.unshift(part.selector); + } + } + return sels; + }, + + + /** + * Returns the a list of dimensions that are actually used in the + * application.json file. + * @private + * @method _listUsedDimensions + * @return {array} list of dimensions and values + * (values have no structure) + */ + _listUsedDimensions: function() { + var ctxs = [], + ctxValues = {}; // dimName: value: true + this._appConfigYCB.walkSettings(function(settings, config) { + Y.Object.each(settings, function(val, name) { + if (!ctxValues[name]) { + ctxValues[name] = {}; + } + ctxValues[name][val] = true; + }); + return true; + }); + Y.Object.each(ctxValues, function(vals, name) { + ctxs[name] = Object.keys(vals); + }); + return ctxs; + }, + + + /** + * Generates a list of contexts to which application.json is sensitive. + * @private + * @method _listUsedContexts + * @return {array of objects} all contexts in application.json + */ + _listUsedContexts: function() { + var dims = this._listUsedDimensions(), + nctxs, + c, + ctxs = [], + dn, + dname, + dnames = Object.keys(dims), + dv, + dval, + dvals, + e, + each, + mod; + + nctxs = 1; + for (dn = 0; dn < dnames.length; dn += 1) { + dname = dnames[dn]; + dvals = dims[dname]; + if (dname !== 'runtime') { + // we never have indeterminant runtime + dvals.push('*'); + } + nctxs *= dvals.length; + } + + for (c = 0; c < nctxs; c += 1) { + ctxs[c] = {}; + } + mod = 1; + for (dn = 0; dn < dnames.length; dn += 1) { + dname = dnames[dn]; + dvals = dims[dname]; + mod *= dvals.length; + each = nctxs / mod; + + e = each; + dv = 0; + for (c = 0; c < nctxs; e -= 1, c += 1) { + if (0 === e) { + e = each; + dv += 1; + dv = dv % dvals.length; + } + dval = dvals[dv]; + if ('*' !== dval) { + ctxs[c][dname] = dval; + } + } + } + return ctxs; + } + + + }); + Y.namespace('mojito.addons.rs'); + Y.mojito.addons.rs.selector = RSAddonSelector; + +}, '0.0.1', { requires: ['plugin', 'oop', 'json-stringify']}); diff --git a/source/lib/app/addons/rs/url.server.js b/source/lib/app/addons/rs/url.server.js new file mode 100644 index 000000000..968b498fd --- /dev/null +++ b/source/lib/app/addons/rs/url.server.js @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2012, 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*/ + + +/** + * @module ResourceStoreAddon + */ + +/** + * @class RSAddonUrl + * @extension ResourceStore.server + */ +YUI.add('addon-rs-url', function(Y, NAME) { + + var libfs = require('fs'), + libpath = require('path'); + + function RSAddonUrl() { + RSAddonUrl.superclass.constructor.apply(this, arguments); + } + RSAddonUrl.NS = 'url'; + + Y.extend(RSAddonUrl, Y.Plugin.Base, { + + /** + * This methods is part of Y.Plugin.Base. See documentation for that for details. + * @method initializer + * @param {object} config Configuration object as per Y.Plugin.Base + * @return {nothing} + */ + initializer: function(config) { + var appConfig; + this.appRoot = config.appRoot; + this.mojitoRoot = config.mojitoRoot; + this.afterHostMethod('preloadResourceVersions', this.preloadResourceVersions, this); + this.onHostEvent('getMojitTypeDetails', this.onGetMojitTypeDetails, this); + + appConfig = config.host.getStaticAppConfig(); + this.config = appConfig.staticHandling || {}; + this.config.appName = this.config.appName || libpath.basename(this.appRoot); + this.config.frameworkName = this.config.frameworkName || 'mojito'; + if (!this.config.hasOwnProperty('prefix')) { + this.config.prefix = 'static'; + } + // FUTURE: deprecate appConfig.assumeRollups + this.assumeRollups = this.config.assumeRollups || appConfig.assumeRollups; + }, + + + /** + * Using AOP, this is called after the ResourceStore's version. + * It computes the static handler URL for all resources in all the + * mojits (as well as the mojit itself). + * @method preloadResourceVersions + * @return {nothing} + */ + preloadResourceVersions: function() { + var store = this.get('host'), + mojits, + m, + mojit, + mojitRes, + mojitControllerRess, + packageJson, + mojitIsPublic, + ress, + r, + res, + skip; + + mojits = store.listAllMojits(); + mojits.push('shared'); + for (m = 0; m < mojits.length; m += 1) { + mojit = mojits[m]; + mojitRes = store.getResourceVersions({id: 'mojit--' + mojit})[0]; + if (mojitRes) { + this._calcResourceURL(mojitRes, mojitRes); + } + + // Server-only framework mojits like DaliProxy and HTMLFrameMojit + // should never have URLs associated with them. This never used + // to be an issue until we added the "assumeRollups" feature to + // preload JSON specs for specific mojits during the compile step + // (`mojito compile json`) for Livestand. + if ('shared' !== mojit && 'mojito' === mojitRes.source.pkg.name) { + mojitControllerRess = store.getResourceVersions({mojit: mojit, id: 'controller--controller'}); + if (mojitControllerRess.length === 1 && + mojitControllerRess[0].affinity.affinity === 'server') { + continue; + } + } + + mojitIsPublic = false; + if (mojitRes) { + packageJson = libpath.join(mojitRes.source.fs.fullPath, 'package.json'); + packageJson = store.config.readConfigJSON(packageJson); + if ('public' === (packageJson.yahoo && + packageJson.yahoo.mojito && + packageJson.yahoo.mojito['package'])) { + mojitIsPublic = true; + } + } + + ress = store.getResourceVersions({mojit: mojit}); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + + skip = false; + if ('config' === res.type) { + skip = true; + } + + // This is mainly used during `mojito build html5app`. + // In that situation, the user mainly doesn't want to + // publish each mojit's package.json. However, Livestand + // did need to, so this feature allowed them to opt-in. + if ('config--package' === res.id && mojitIsPublic) { + skip = false; + } + + if (skip) { + continue; + } + + this._calcResourceURL(res, mojitRes); + } + } + }, + + + /** + * This is called when the ResourceStore fires this event. + * It calculates the `assetsRoot` for the mojit. + * @method onGetMojitTypeDetails + * @param {object} evt The fired event + * @return {nothing} + */ + onGetMojitTypeDetails: function(evt) { + var ress = this.get('host').getResources(evt.args.env, evt.args.ctx, {type: 'mojit', name: evt.args.mojitType}); + evt.mojit.assetsRoot = ress[0].url + '/assets'; + }, + + + /** + * Calculates the static handler URL for the resource. + * @private + * @method _calcResourceURL + * @param {object} res the resource for which to calculate the URL + * @param {object} mojitRes the resource for the mojit + * @return {nothing} + */ + _calcResourceURL: function(res, mojitRes) { + var fs = res.source.fs, + relativePath = fs.fullPath.substr(fs.rootDir.length + 1), + urlParts = [], + rollupParts = [], + rollupFsPath; + + // Don't clobber a URL calculated by another RS addon. + if (res.hasOwnProperty('url')) { + return; + } + + if (this.config.prefix) { + urlParts.push(this.config.prefix); + rollupParts.push(this.config.prefix); + } + + if ('shared' === res.mojit) { + if ('mojito' === res.source.pkg.name) { + urlParts.push(this.config.frameworkName); + } else { + urlParts.push(this.config.appName); + } + // fw resources are also put into the app-level rollup + if (res.yui && res.yui.name) { + rollupParts.push(this.config.appName); + rollupParts.push('rollup.client.js'); + rollupFsPath = libpath.join(this.appRoot, 'rollup.client.js'); + } + } else { + if ('mojit' === res.type) { + urlParts.push(res.name); + } else { + urlParts.push(res.mojit); + } + if (res.yui && res.yui.name) { + rollupParts.push(res.mojit); + rollupParts.push('rollup.client.js'); + rollupFsPath = libpath.join(mojitRes.source.fs.fullPath, 'rollup.client.js'); + } + } + + if ('mojit' === res.type) { + if ('shared' !== res.name) { + res.url = '/' + urlParts.join('/'); + } + return; + } + + urlParts.push(relativePath); + + if (rollupFsPath && (this.assumeRollups || libpath.existsSync(rollupFsPath))) { + res.url = '/' + rollupParts.join('/'); + fs.rollupPath = rollupFsPath; + } else { + res.url = '/' + urlParts.join('/'); + } + } + + + }); + Y.namespace('mojito.addons.rs'); + Y.mojito.addons.rs.url = RSAddonUrl; + +}, '0.0.1', { requires: ['plugin', 'oop']}); diff --git a/source/lib/app/addons/rs/yui.server.js b/source/lib/app/addons/rs/yui.server.js new file mode 100644 index 000000000..e93481dd7 --- /dev/null +++ b/source/lib/app/addons/rs/yui.server.js @@ -0,0 +1,629 @@ +/* + * Copyright (c) 2012, 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*/ + + +/** + * @module ResourceStoreAddon + */ + + +/** + * @class RSAddonYUI + * @extension ResourceStore.server + */ +YUI.add('addon-rs-yui', function(Y, NAME) { + + var libfs = require('fs'), + libpath = require('path'), + libvm = require('vm'), + MODULE_SUBDIRS = { + autoload: true, + tests: true, + yui_modules: true + }; + + function RSAddonYUI() { + RSAddonYUI.superclass.constructor.apply(this, arguments); + } + RSAddonYUI.NS = 'yui'; + + Y.extend(RSAddonYUI, Y.Plugin.Base, { + + /** + * This methods is part of Y.Plugin.Base. See documentation for that for details. + * @method initializer + * @param {object} config Configuration object as per Y.Plugin.Base + * @return {nothing} + */ + initializer: function(config) { + this.appRoot = config.appRoot; + this.mojitoRoot = config.mojitoRoot; + this.afterHostMethod('findResourceVersionByConvention', this.findResourceVersionByConvention, this); + this.beforeHostMethod('parseResourceVersion', this.parseResourceVersion, this); + this.beforeHostMethod('addResourceVersion', this.addResourceVersion, this); + this.onHostEvent('getMojitTypeDetails', this.onGetMojitTypeDetails, this); + this.onHostEvent('mojitResourcesResolved', this.onMojitResourcesResolved, this); + this.yuiConfig = config.host.getStaticAppConfig().yui; + + this.modules = {}; // env: poslKey: module: details + this.sortedModules = {}; // env: poslKey: lang: module: details + + this.usePrecomputed = -1 !== this.yuiConfig.dependencyCalculations.indexOf('precomputed'); + this.useOnDemand = -1 !== this.yuiConfig.dependencyCalculations.indexOf('ondemand'); + if (!this.usePrecomputed) { + this.useOnDemand = true; + } + }, + + + /** + * Returns a datastructure which tells a YUI instance where to find + * the YUI modules that are shared among all mojits. + * @method getConfigShared + * @param {string} env runtime environment (either `client`, or `server`) + * @param {object} ctx runtime context + * @param {boolean} justApp Indicates whether to include the YUI + * modules just found in the application (true), or also include + * those found in mojito (false). + * @return {object} datastructure for configuring YUI + */ + getConfigShared: function(env, ctx, justApp) { + var r, + res, + ress, + modules = {}; + ress = this.get('host').getResources(env, ctx, { mojit: 'shared' }); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if (!res.yui || !res.yui.name) { + continue; + } + if (justApp && ('mojito' === res.source.pkg.name)) { + continue; + } + modules[res.yui.name] = this._makeYUIModuleConfig(env, res); + } + return { modules: modules }; + }, + + + /** + * Returns a datastructure which tells a YUI instance where to find + * the YUI modules that are in all the mojits. + * @method getConfigAllMojits + * @param {string} env runtime environment (either `client`, or `server`) + * @param {object} ctx runtime context + * @return {object} datastructure for configuring YUI + */ + getConfigAllMojits: function(env, ctx) { + var store = this.get('host'), + m, + mojit, + mojits, + r, + res, + ress, + modules = {}; + mojits = store.listAllMojits(); + for (m = 0; m < mojits.length; m += 1) { + mojit = mojits[m]; + ress = store.getResources(env, ctx, { mojit: mojit }); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if (!res.yui || !res.yui.name) { + continue; + } + if (res.mojit !== mojit) { + // generally only happens if res.mojit is 'shared' + continue; + } + modules[res.yui.name] = this._makeYUIModuleConfig(env, res); + } + } + return { modules: modules }; + }, + + + /** + * Using AOP, this is called after the ResourceStore's version. + * @method findResourceVersionByConvention + * @param {object} source metadata about where the resource is located + * @param {string} mojitType name of mojit to which the resource likely belongs + * @return {object||null} for yui modules or lang bundles, returns metadata signifying that + */ + findResourceVersionByConvention: function(source, mojitType) { + var fs = source.fs; + + if (!fs.isFile) { + return; + } + if ('.js' !== fs.ext) { + return; + } + + if (fs.subDirArray.length >= 1 && MODULE_SUBDIRS[fs.subDirArray[0]]) { + return new Y.Do.AlterReturn(null, { + type: 'yui-module', + skipSubdirParts: 1 + }); + } + + if (fs.subDirArray.length >= 1 && 'lang' === fs.subDirArray[0]) { + return new Y.Do.AlterReturn(null, { + type: 'yui-lang', + skipSubdirParts: 1 + }); + } + }, + + + /** + * Using AOP, this is called before the ResourceStore's version. + * @method parseResourceVersion + * @param {object} source metadata about where the resource is located + * @param {string} type type of the resource + * @param {string} subtype subtype of the resource + * @param {string} mojitType name of mojit to which the resource likely belongs + * @return {object||null} for yui modules or lang bundles, returns the resource metadata + */ + parseResourceVersion: function(source, type, subtype, mojitType) { + var fs = source.fs, + baseParts, + res; + + if ('yui-lang' === type) { + res = { + source: source, + mojit: mojitType, + type: 'yui-lang', + affinity: 'common', + selector: '*' + }; + if (!res.yui) { + res.yui = {}; + } + if (fs.basename === mojitType) { + res.yui.lang = ''; + } else if (mojitType === fs.basename.substr(0, mojitType.length)) { + res.yui.lang = fs.basename.substr(mojitType.length + 1); + } else { + Y.log('invalid YUI lang file format. skipping ' + fs.fullPath, 'error', NAME); + } + res.name = res.yui.lang; + res.id = [res.type, res.subtype, res.name].join('-'); + return new Y.Do.Halt(null, res); + } + + if ('yui-module' === type) { + baseParts = fs.basename.split('.'); + res = { + source: source, + mojit: mojitType, + type: 'yui-module', + affinity: 'server', + selector: '*' + }; + if (baseParts.length >= 3) { + res.selector = baseParts.pop(); + } + if (baseParts.length >= 2) { + res.affinity = baseParts.pop(); + } + if (baseParts.length !== 1) { + Y.log('invalid yui-module filename. skipping ' + fs.fullPath, 'warn', NAME); + return; + } + this._captureYUIModuleDetails(res); + res.name = res.yui.name; + res.id = [res.type, res.subtype, res.name].join('-'); + return new Y.Do.Halt(null, res); + } + }, + + + /** + * Using AOP, this is called before the ResourceStore's version. + * If the resource is a YUI module, augments the metadata with details + * about the YUI module. + * @method addResourceVersion + * @param {object} res resource version metadata + * @return {nothing} + */ + addResourceVersion: function(res) { + if ('.js' !== res.source.fs.ext) { + return; + } + if (res.yui && res.yui.name) { + // work done already + return; + } + // ASSUMPTION: no app-level resources are YUI modules + if (!res.mojit) { + return; + } + if ('asset' === res.type) { + return; + } + this._captureYUIModuleDetails(res); + }, + + + /** + * This is called when the ResourceStore fires this event. + * It augments the mojit type details with the precomputed YUI module + * dependencies. + * @method onGetMojitTypeDetails + * @param {object} evt The fired event. + * @return {nothing} + */ + onGetMojitTypeDetails: function(evt) { + var store = this.get('host'), + dest = evt.mojit, + env = evt.args.env, + ctx = evt.args.ctx, + posl = evt.args.posl, + poslKey = Y.JSON.stringify(posl), + mojitType = evt.args.mojitType, + ress, + r, + res, + sorted; + //console.log('--------------------------------- onGetMojitTypeDetails -- ' + [env, ctx.lang, poslKey, mojitType].join(',')); + + if (!dest.yui) { + dest.yui = { config: {} }; + } + if (!dest.yui.config) { + dest.yui.config = { module: {} }; + } + + if (this.modules[env] && this.modules[env][poslKey]) { + dest.yui.config.modules = this.modules[env][poslKey][mojitType]; + } + + ress = store.getResources(env, ctx, {mojit: mojitType}); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if (res.type === 'binder') { + if (!dest.views[res.name]) { + dest.views[res.name] = {}; + } + dest.views[res.name]['binder-module'] = res.yui.name; + sorted = this._getYUIDependencies('client', poslKey, ctx.lang, res.yui.name); + if (sorted && sorted.paths) { + dest.views[res.name]['binder-yui-sorted'] = Y.clone(sorted.paths, true); + } + } + if (res.type === 'controller') { + dest['controller-module'] = res.yui.name; + sorted = this._getYUIDependencies(env, poslKey, ctx.lang, res.yui.name); + if (sorted && sorted.sorted) { + dest.yui.sorted = sorted.sorted.slice(); + } + if (this.usePrecomputed && sorted && sorted.paths) { + dest.yui.sortedPaths = Y.clone(sorted.paths, true); + } + } + } + }, + + + /** + * This is called when the ResourceStore fires this event. + * It precomputes the YUI module dependencies, to be used later during + * onGetMojitTypeDetails. + * @method onMojitResourcesResolved + * @param {object} evt The fired event + * @return {nothing} + */ + onMojitResourcesResolved: function(evt) { + var env = evt.env, + posl = evt.posl, + poslKey = Y.JSON.stringify(posl), + mojit = evt.mojit, + ress = evt.ress, + r, + res, + langs = {}, + l, + ll, + lang, + langName, + langNames, + viewEngineRequired = {}, + modules = {}, + binders = {}, + controller, + controllerRequired = {}, + required, + sorted, + binderName, + binder; + //console.log('--------------------------------- onMojitResourcesResolved -- ' + [env, poslKey, mojit].join(',')); + + if ('shared' === mojit) { + return; + } + + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if ('addon' === res.type && 'view-engines' === res.subtype) { + viewEngineRequired[res.yui.name] = true; + } + if ('yui-lang' === res.type) { + langs[res.name] = res; + } + if (res.yui && res.yui.name) { + modules[res.yui.name] = { + requires: res.yui.meta.requires, + fullpath: (('client' === env) ? res.url : res.source.fs.fullPath) + }; + if (res.mojit === mojit && res.type !== 'yui-lang' && res.type !== 'binder') { + controllerRequired[res.yui.name] = true; + } + if ('binder' === res.type) { + binders[res.name] = res; + } + if ('controller' === res.type) { + controller = res; + } + } + } + if (controller && modules['inlinecss/' + mojit]) { + controller.yui.meta.requires.push('inlinecss/' + mojit); + } + + if (!this.modules[env]) { + this.modules[env] = {}; + } + if (!this.modules[env][poslKey]) { + this.modules[env][poslKey] = {}; + } + this.modules[env][poslKey][mojit] = Y.clone(modules, true); + + // we always want to do calculations for no-lang + if (!langs['']) { + langs[''] = undefined; + } + langNames = Object.keys(langs); + for (l = 0; l < langNames.length; l += 1) { + langName = langNames[l]; + lang = langs[langName]; + + if (controller) { + required = Y.clone(controllerRequired, true); + required['mojito-dispatcher'] = true; + required[controller.yui.name] = true; + if (lang && lang.yui) { + required[lang.yui.name] = true; + } else { + for (ll in langs) { + if (langs.hasOwnProperty(ll) && ll !== '') { + lang = langs[ll]; + required[lang.yui.name] = true; + } + } + } + // we don't know which views will be used, so we need all view engines + required = Y.merge(required, viewEngineRequired); + sorted = this._precomputeYUIDependencies(langName, env, mojit, modules, required); + this._setYUIDependencies(env, poslKey, langName, controller.yui.name, sorted); + } + + if ('client' === env) { + for (binderName in binders) { + if (binders.hasOwnProperty(binderName)) { + binder = binders[binderName]; + required = { 'mojito-client': true }; + required[binder.yui.name] = true; + + // view engines are needed to support mojitProxy.render() + required = Y.merge(required, viewEngineRequired); + + sorted = this._precomputeYUIDependencies(langName, env, mojit, modules, required); + this._setYUIDependencies(env, poslKey, langName, binder.yui.name, sorted); + } + } + } + } // for each lang + }, + + + /** + * Precomputes a set of dependencies. + * @private + * @method _precomputeYUIDependencies + * @param {string} lang YUI language code + * @param {string} env runtime environment (either `client`, or `server`) + * @param {string} mojit name of the mojit + * @param {object} modules YUI module metadata + * @param {object} required lookup hash of YUI module names that are required + * @return {object} precomputed (and sorted) module dependencies + */ + _precomputeYUIDependencies: function(lang, env, mojit, modules, required) { + var loader, + m, + module, + info, + sortedPaths = {}; + + // We don't actually need the full list, just the required modules. + // YUI.Loader() will do the rest at runtime. + if (!this.usePrecomputed) { + for (module in required) { + if (required.hasOwnProperty(module)) { + sortedPaths[module] = modules[module].fullpath; + } + } + return { + sorted: Object.keys(sortedPaths), + paths: sortedPaths + }; + } + + // HACK + // We need to clear YUI's cached dependencies, since there's no + // guarantee that the previously calculated dependencies have been done + // using the same context as this calculation. + delete YUI.Env._renderedMods; + + // Use ignoreRegistered here instead of the old `delete YUI.Env._renderedMods` hack + loader = new Y.Loader({ lang: lang, ignoreRegistered: true }); + // Only override the default if it's required + if (this.yuiConfig && this.yuiConfig.base) { + loader.base = this.yuiConfig.base; + } + + loader.addGroup({modules: modules}, mojit); + loader.calculate({required: required}); + + for (m = 0; m < loader.sorted.length; m += 1) { + module = loader.sorted[m]; + info = loader.moduleInfo[module]; + if (info) { + // modules with "nodejs" in their name are tweaks on other modules + if ('client' === env && module.indexOf('nodejs') !== -1) { + continue; + } + sortedPaths[module] = info.fullpath || loader._url(info.path); + } + } + return { + sorted: loader.sorted, + paths: sortedPaths + }; + }, + + + /** + * Saves the precomputed YUI module dependencies for later. + * @private + * @method _setYUIDependencies + * @param {string} env runtime environment (either `client`, or `server`) + * @param {string} poslKey key (representing the POSL) under which to save the moduldes + * @param {string} lang YUI language code + * @param {string} module YUI module name for which the precomputed dependencies are for + * @param {object} sorted the precomputed dependencies + * @return {nothing} + */ + _setYUIDependencies: function(env, poslKey, lang, module, sorted) { + if (!this.sortedModules[env]) { + this.sortedModules[env] = {}; + } + if (!this.sortedModules[env][poslKey]) { + this.sortedModules[env][poslKey] = {}; + } + if (!this.sortedModules[env][poslKey][lang]) { + this.sortedModules[env][poslKey][lang] = {}; + } + this.sortedModules[env][poslKey][lang][module] = sorted; + }, + + + /** + * Returns precomputed dependencies saved by _setYuiSorted. + * @private + * @method _getYUIDependencies + * @param {string} env runtime environment (either `client`, or `server`) + * @param {string} poslKey key (representing the POSL) under which to save the moduldes + * @param {string} lang YUI language code + * @param {string} module YUI module name for which the precomputed dependencies are for + * @return {object} the precomputed dependencies + */ + _getYUIDependencies: function(env, poslKey, lang, module) { + lang = lang || ''; + var parts = lang.split('-'), + p, + test; + if (!this.sortedModules[env]) { + return; + } + if (!this.sortedModules[env][poslKey]) { + return; + } + // example: first try "zh-Hans-CN", then "zh-Hans", then "zh" + for (p = parts.length; p > 0; p -= 1) { + test = parts.slice(0, p).join('-'); + if (this.sortedModules[env][poslKey][test] && + this.sortedModules[env][poslKey][test][module]) { + return this.sortedModules[env][poslKey][test][module]; + } + } + // fall back to "default language" + return this.sortedModules[env][poslKey][''][module]; + }, + + + /** + * Generates the YUI configuration for the resource. + * @private + * @method _makeYUIModuleConfig + * @param {string} env runtime environment (either `client`, or `server`) + * @param {object} res the resource metadata + * @return {object} the YUI configuration for the module + */ + _makeYUIModuleConfig: function(env, res) { + var config = { + fullpath: ('client' === env) ? res.url : res.source.fs.fullPath, + requires: (res.yui.meta && res.yui.meta.requires) || [] + }; + return config; + }, + + + /** + * If the resource is a YUI module, augments its metadata with metadata + * about the YUI module. + * @private + * @method _captureYUIModuleDetails + * @param {object} res resource metadata + * @return {nothing} + */ + _captureYUIModuleDetails: function(res) { + var file, + ctx, + yui = {}; + file = libfs.readFileSync(res.source.fs.fullPath, 'utf8'); + ctx = { + console: { + log: function() {} + }, + window: {}, + document: {}, + YUI: { + ENV: {}, + config: {}, + use: function() {}, + add: function(name, fn, version, meta) { + yui.name = name; + yui.version = version; + yui.meta = meta || {}; + if (!yui.meta.requires) { + yui.meta.requires = []; + } + } + } + }; + try { + libvm.runInNewContext(file, ctx, res.source.fs.fullPath); + } catch (e) { + yui = null; + Y.log(e.message + '\n' + e.stack, 'error', NAME); + } + if (yui) { + res.yui = Y.merge(res.yui || {}, yui); + } + } + + + }); + Y.namespace('mojito.addons.rs'); + Y.mojito.addons.rs.yui = RSAddonYUI; + +}, '0.0.1', { requires: ['plugin', 'oop', 'loader-base', 'json-stringify']}); diff --git a/source/lib/app/addons/view-engines/mu.client.js b/source/lib/app/addons/view-engines/mu.client.js index f3a65ef1a..551a33751 100644 --- a/source/lib/app/addons/view-engines/mu.client.js +++ b/source/lib/app/addons/view-engines/mu.client.js @@ -8,7 +8,18 @@ /*jslint anon:true, sloppy:true, nomen:true*/ /*global YUI*/ +/** + * View engines. + * + * Please see the [documentation](http://developer.yahoo.com/cocktails/mojito/docs/topics/mojito_extensions.html#view-engines). + * + * @module ViewEngines + */ + +/** + * @Module ViewEngines + */ YUI.add('mojito-mu', function(Y, NAME) { var CACHE = {}, diff --git a/source/lib/app/addons/view-engines/mu.server.js b/source/lib/app/addons/view-engines/mu.server.js index b62dd8eef..509b4cee9 100644 --- a/source/lib/app/addons/view-engines/mu.server.js +++ b/source/lib/app/addons/view-engines/mu.server.js @@ -9,6 +9,9 @@ /*global YUI*/ +/** + * @Module ViewEngines + */ YUI.add('mojito-mu', function(Y, NAME) { var mu = YUI.require(__dirname + '/../../libs/Mulib/Mu'), @@ -76,4 +79,4 @@ YUI.add('mojito-mu', function(Y, NAME) { Y.namespace('mojito.addons.viewEngines').mu = MuAdapter; -}, '0.1.0', {requires: []}); +}, '0.1.0', {requires: ['json-stringify']}); diff --git a/source/lib/archetypes/app/default/application.json b/source/lib/app/archetypes/app/default/application.json similarity index 100% rename from source/lib/archetypes/app/default/application.json rename to source/lib/app/archetypes/app/default/application.json diff --git a/source/lib/archetypes/app/default/assets/favicon.ico b/source/lib/app/archetypes/app/default/assets/favicon.ico similarity index 100% rename from source/lib/archetypes/app/default/assets/favicon.ico rename to source/lib/app/archetypes/app/default/assets/favicon.ico diff --git a/source/lib/archetypes/app/default/index.js b/source/lib/app/archetypes/app/default/index.js similarity index 100% rename from source/lib/archetypes/app/default/index.js rename to source/lib/app/archetypes/app/default/index.js diff --git a/source/lib/archetypes/app/default/mojits/.placeholder b/source/lib/app/archetypes/app/default/mojits/.placeholder similarity index 100% rename from source/lib/archetypes/app/default/mojits/.placeholder rename to source/lib/app/archetypes/app/default/mojits/.placeholder diff --git a/source/lib/archetypes/app/default/package.json.mu b/source/lib/app/archetypes/app/default/package.json.mu similarity index 100% rename from source/lib/archetypes/app/default/package.json.mu rename to source/lib/app/archetypes/app/default/package.json.mu diff --git a/source/lib/archetypes/app/default/routes.json b/source/lib/app/archetypes/app/default/routes.json similarity index 100% rename from source/lib/archetypes/app/default/routes.json rename to source/lib/app/archetypes/app/default/routes.json diff --git a/source/lib/archetypes/app/default/server.js b/source/lib/app/archetypes/app/default/server.js similarity index 100% rename from source/lib/archetypes/app/default/server.js rename to source/lib/app/archetypes/app/default/server.js diff --git a/source/lib/archetypes/app/full/application.json b/source/lib/app/archetypes/app/full/application.json similarity index 100% rename from source/lib/archetypes/app/full/application.json rename to source/lib/app/archetypes/app/full/application.json diff --git a/source/lib/archetypes/app/full/assets/favicon.ico b/source/lib/app/archetypes/app/full/assets/favicon.ico similarity index 100% rename from source/lib/archetypes/app/full/assets/favicon.ico rename to source/lib/app/archetypes/app/full/assets/favicon.ico diff --git a/source/lib/archetypes/app/full/index.js b/source/lib/app/archetypes/app/full/index.js similarity index 100% rename from source/lib/archetypes/app/full/index.js rename to source/lib/app/archetypes/app/full/index.js diff --git a/source/lib/archetypes/app/full/mojits/.placeholder b/source/lib/app/archetypes/app/full/mojits/.placeholder similarity index 100% rename from source/lib/archetypes/app/full/mojits/.placeholder rename to source/lib/app/archetypes/app/full/mojits/.placeholder diff --git a/source/lib/archetypes/app/full/package.json.mu b/source/lib/app/archetypes/app/full/package.json.mu similarity index 100% rename from source/lib/archetypes/app/full/package.json.mu rename to source/lib/app/archetypes/app/full/package.json.mu diff --git a/source/lib/archetypes/app/full/routes.json b/source/lib/app/archetypes/app/full/routes.json similarity index 100% rename from source/lib/archetypes/app/full/routes.json rename to source/lib/app/archetypes/app/full/routes.json diff --git a/source/lib/archetypes/app/full/server.js b/source/lib/app/archetypes/app/full/server.js similarity index 100% rename from source/lib/archetypes/app/full/server.js rename to source/lib/app/archetypes/app/full/server.js diff --git a/source/lib/archetypes/app/simple/application.json b/source/lib/app/archetypes/app/simple/application.json similarity index 100% rename from source/lib/archetypes/app/simple/application.json rename to source/lib/app/archetypes/app/simple/application.json diff --git a/source/lib/archetypes/app/simple/mojits/.placeholder b/source/lib/app/archetypes/app/simple/mojits/.placeholder similarity index 100% rename from source/lib/archetypes/app/simple/mojits/.placeholder rename to source/lib/app/archetypes/app/simple/mojits/.placeholder diff --git a/source/lib/archetypes/app/simple/package.json.mu b/source/lib/app/archetypes/app/simple/package.json.mu similarity index 100% rename from source/lib/archetypes/app/simple/package.json.mu rename to source/lib/app/archetypes/app/simple/package.json.mu diff --git a/source/lib/archetypes/app/simple/server.js b/source/lib/app/archetypes/app/simple/server.js similarity index 100% rename from source/lib/archetypes/app/simple/server.js rename to source/lib/app/archetypes/app/simple/server.js diff --git a/source/lib/archetypes/mojit/default/assets/index.css b/source/lib/app/archetypes/mojit/default/assets/index.css similarity index 100% rename from source/lib/archetypes/mojit/default/assets/index.css rename to source/lib/app/archetypes/mojit/default/assets/index.css diff --git a/source/lib/archetypes/mojit/default/binders/index.js.mu b/source/lib/app/archetypes/mojit/default/binders/index.js.mu similarity index 100% rename from source/lib/archetypes/mojit/default/binders/index.js.mu rename to source/lib/app/archetypes/mojit/default/binders/index.js.mu diff --git a/source/lib/archetypes/mojit/default/controller.server.js.mu b/source/lib/app/archetypes/mojit/default/controller.server.js.mu similarity index 100% rename from source/lib/archetypes/mojit/default/controller.server.js.mu rename to source/lib/app/archetypes/mojit/default/controller.server.js.mu diff --git a/source/lib/archetypes/mojit/default/definition.json b/source/lib/app/archetypes/mojit/default/definition.json similarity index 100% rename from source/lib/archetypes/mojit/default/definition.json rename to source/lib/app/archetypes/mojit/default/definition.json diff --git a/source/lib/archetypes/mojit/default/models/foo.server.js.mu b/source/lib/app/archetypes/mojit/default/models/foo.server.js.mu similarity index 100% rename from source/lib/archetypes/mojit/default/models/foo.server.js.mu rename to source/lib/app/archetypes/mojit/default/models/foo.server.js.mu diff --git a/source/lib/archetypes/mojit/default/tests/binders/index.common-tests.js.mu b/source/lib/app/archetypes/mojit/default/tests/binders/index.common-tests.js.mu similarity index 100% rename from source/lib/archetypes/mojit/default/tests/binders/index.common-tests.js.mu rename to source/lib/app/archetypes/mojit/default/tests/binders/index.common-tests.js.mu diff --git a/source/lib/archetypes/mojit/default/tests/controller.server-tests.js.mu b/source/lib/app/archetypes/mojit/default/tests/controller.server-tests.js.mu similarity index 100% rename from source/lib/archetypes/mojit/default/tests/controller.server-tests.js.mu rename to source/lib/app/archetypes/mojit/default/tests/controller.server-tests.js.mu diff --git a/source/lib/archetypes/mojit/default/tests/models/foo.server-tests.js.mu b/source/lib/app/archetypes/mojit/default/tests/models/foo.server-tests.js.mu similarity index 100% rename from source/lib/archetypes/mojit/default/tests/models/foo.server-tests.js.mu rename to source/lib/app/archetypes/mojit/default/tests/models/foo.server-tests.js.mu diff --git a/source/lib/archetypes/mojit/default/views/index.mu.html b/source/lib/app/archetypes/mojit/default/views/index.mu.html similarity index 100% rename from source/lib/archetypes/mojit/default/views/index.mu.html rename to source/lib/app/archetypes/mojit/default/views/index.mu.html diff --git a/source/lib/archetypes/mojit/full/assets/.placeholder b/source/lib/app/archetypes/mojit/full/assets/.placeholder similarity index 100% rename from source/lib/archetypes/mojit/full/assets/.placeholder rename to source/lib/app/archetypes/mojit/full/assets/.placeholder diff --git a/source/lib/archetypes/mojit/full/assets/index.css b/source/lib/app/archetypes/mojit/full/assets/index.css similarity index 100% rename from source/lib/archetypes/mojit/full/assets/index.css rename to source/lib/app/archetypes/mojit/full/assets/index.css diff --git a/source/lib/archetypes/mojit/full/binders/index.js.mu b/source/lib/app/archetypes/mojit/full/binders/index.js.mu similarity index 100% rename from source/lib/archetypes/mojit/full/binders/index.js.mu rename to source/lib/app/archetypes/mojit/full/binders/index.js.mu diff --git a/source/lib/archetypes/mojit/full/controller.server.js.mu b/source/lib/app/archetypes/mojit/full/controller.server.js.mu similarity index 100% rename from source/lib/archetypes/mojit/full/controller.server.js.mu rename to source/lib/app/archetypes/mojit/full/controller.server.js.mu diff --git a/source/lib/archetypes/mojit/full/defaults.json b/source/lib/app/archetypes/mojit/full/defaults.json similarity index 100% rename from source/lib/archetypes/mojit/full/defaults.json rename to source/lib/app/archetypes/mojit/full/defaults.json diff --git a/source/lib/archetypes/mojit/full/definition.json b/source/lib/app/archetypes/mojit/full/definition.json similarity index 100% rename from source/lib/archetypes/mojit/full/definition.json rename to source/lib/app/archetypes/mojit/full/definition.json diff --git a/source/lib/archetypes/mojit/full/models/foo.server.js.mu b/source/lib/app/archetypes/mojit/full/models/foo.server.js.mu similarity index 100% rename from source/lib/archetypes/mojit/full/models/foo.server.js.mu rename to source/lib/app/archetypes/mojit/full/models/foo.server.js.mu diff --git a/source/lib/archetypes/mojit/full/tests/binders/index.common-tests.js.mu b/source/lib/app/archetypes/mojit/full/tests/binders/index.common-tests.js.mu similarity index 100% rename from source/lib/archetypes/mojit/full/tests/binders/index.common-tests.js.mu rename to source/lib/app/archetypes/mojit/full/tests/binders/index.common-tests.js.mu diff --git a/source/lib/archetypes/mojit/full/tests/controller.server-tests.js.mu b/source/lib/app/archetypes/mojit/full/tests/controller.server-tests.js.mu similarity index 100% rename from source/lib/archetypes/mojit/full/tests/controller.server-tests.js.mu rename to source/lib/app/archetypes/mojit/full/tests/controller.server-tests.js.mu diff --git a/source/lib/archetypes/mojit/full/tests/models/model.server-tests.js.mu b/source/lib/app/archetypes/mojit/full/tests/models/model.server-tests.js.mu similarity index 100% rename from source/lib/archetypes/mojit/full/tests/models/model.server-tests.js.mu rename to source/lib/app/archetypes/mojit/full/tests/models/model.server-tests.js.mu diff --git a/source/lib/archetypes/mojit/full/views/index.mu.html b/source/lib/app/archetypes/mojit/full/views/index.mu.html similarity index 100% rename from source/lib/archetypes/mojit/full/views/index.mu.html rename to source/lib/app/archetypes/mojit/full/views/index.mu.html diff --git a/source/lib/archetypes/mojit/simple/controller.server.js.mu b/source/lib/app/archetypes/mojit/simple/controller.server.js.mu similarity index 100% rename from source/lib/archetypes/mojit/simple/controller.server.js.mu rename to source/lib/app/archetypes/mojit/simple/controller.server.js.mu diff --git a/source/lib/app/autoload/action-context.common.js b/source/lib/app/autoload/action-context.common.js index d4730dc6d..941696994 100644 --- a/source/lib/app/autoload/action-context.common.js +++ b/source/lib/app/autoload/action-context.common.js @@ -186,13 +186,15 @@ YUI.add('mojito-action-context', function(Y, NAME) { /** * Mixes all the Action Context addons into the Action Context - * @attachActionContextAddons + * @private + * @method attachActionContextAddons * @param {Array} addons The action context addons. * @param {object} command The command object. * @param {object} adapter The output adapter. * @param {Y.mojito.ActionContext} ac The action context. + * @param {ResourceStore} store the resource store */ - function attachActionContextAddons(addons, command, adapter, ac) { + function attachActionContextAddons(addons, command, adapter, ac, store) { var addonName, addon, dependencies = {}; @@ -217,6 +219,9 @@ YUI.add('mojito-action-context', function(Y, NAME) { addon = new addons[addonName](command, adapter, ac); if (addon.namespace) { ac[addon.namespace] = addon; + if (Y.Lang.isFunction(addon.setStore)) { + addon.setStore(store); + } } } } @@ -274,24 +279,14 @@ YUI.add('mojito-action-context', function(Y, NAME) { self._dispatch.apply(self, arguments); }; - // TODO: should rework to be 'getAppConfig()' and 'getAppRoutes()' and - // not property access through a hash. this.app = { - config: store.getAppConfig(this.context, 'application'), + config: store.getAppConfig(this.context), routes: store.getRoutes(this.context) }; // this is where the addons list is injected onto the action // context...yay! - attachActionContextAddons(Y.mojito.addons.ac, command, adapter, this); - - // There is only one addon that requires the store so check for it here. - // TODO: how can we generalize this so it's not hard-coded to only the - // deploy add-on. Oh, and note we don't make sure that setStore is a - // callable function ;). - if (this.deploy) { - this.deploy.setStore(store); - } + attachActionContextAddons(Y.mojito.addons.ac, command, adapter, this, store); Y.log('ActionContext created for "' + (instance.id || '@' + instance.type) + '/' + command.action + '"', 'mojito', NAME); diff --git a/source/lib/app/autoload/controller-context.common.js b/source/lib/app/autoload/controller-context.common.js index e382793fb..b15e9739f 100644 --- a/source/lib/app/autoload/controller-context.common.js +++ b/source/lib/app/autoload/controller-context.common.js @@ -34,13 +34,13 @@ YUI.add('mojito-controller-context', function(Y, NAME) { // do a shallow merge of app-level and mojit-level configs // mojit config properties take precedence - configApp = this.store.getAppConfig({}, 'application').config, + configApp = this.store.getAppConfig({}).config, configCombo = Y.merge(configApp, instance.config), // Y.mojito.controller for legacy, multi-instance. // Y.mojito.controllers for shared instance c = this.Y.mojito.controller || - this.Y.mojito.controllers[instance.controllerModuleName]; + this.Y.mojito.controllers[instance['controller-module']]; if (!Y.Lang.isObject(c)) { error = new Error('Mojit controller prototype is not an' + @@ -78,8 +78,8 @@ YUI.add('mojito-controller-context', function(Y, NAME) { Y.Object.each(this.Y.mojito.models, function(model, modelName) { - if (!shareYUIInstance || (instance.modelYUIModuleNames && - instance.modelYUIModuleNames[modelName])) { + if (!shareYUIInstance || (instance.models && + instance.models[modelName])) { // TODO: Why? There's no particular reason to inherit here. var modelInstance = Y.mojito.util.heir(model); diff --git a/source/lib/app/autoload/dispatch.common.js b/source/lib/app/autoload/dispatch.common.js index b50dcb421..290dcff67 100644 --- a/source/lib/app/autoload/dispatch.common.js +++ b/source/lib/app/autoload/dispatch.common.js @@ -32,70 +32,6 @@ YUI.add('mojito-dispatcher', function(Y, NAME) { useOnDemand, appShareYUIInstance; - - /** - * Modifies the YUI modules in the instance to point to the correct - * language. - * - * @method fixupInstanceLang - * @param {string} lang target language. - * @param {Object} instance mojit instance (results of expandInstance()). - * @private - */ - function fixupInstanceLang(type, lang, instance) { - var fixedSorted = [], - fixedSortedPaths = {}, - bestLang = Y.Intl.lookupBestLang(lang, - Y.Object.keys(instance.yui.langs)), - suffix = (bestLang) ? '_' + bestLang : '', - OK = {}, - fixedMod, - fixedPath; - - // hard fallbacks if no "root" bundle - if (!bestLang && !instance.yui.langs['']) { - if (instance.yui.langs.en) { - bestLang = 'en'; - suffix = '_en'; - } - if (!bestLang && instance.yui.langs['en-US']) { - bestLang = 'en-US'; - suffix = '_en-US'; - } - } - - OK['lang/' + type + suffix] = true; - if (suffix) { - OK['lang/datatype-date-format' + suffix] = true; - } else { - // The "root" (no lang) version doesn't contain aggregates like %x. - OK['lang/datatype-date-format_en'] = true; - } - - Y.Array.each(instance.yui.sorted, function(mod) { - if ('lang/' === mod.substring(0, 5)) { - if (OK[mod]) { - fixedSorted.push(mod); - } - } else { - fixedSorted.push(mod); - } - }); - Y.Object.each(instance.yui.sortedPaths, function(path, mod) { - if ('lang/' === mod.substring(0, 5)) { - if (OK[mod]) { - fixedSortedPaths[mod] = path; - } - } else { - fixedSortedPaths[mod] = path; - } - }); - - instance.yui.sorted = fixedSorted; - instance.yui.sortedPaths = fixedSortedPaths; - } - - /* Optimization methods: ============ 1). YUI({bootstrap:false}).use('*') @@ -214,11 +150,7 @@ YUI.add('mojito-dispatcher', function(Y, NAME) { var moduleList, mojitYuiModules; - fixupInstanceLang(command.instance.type, command.context.lang, instance); - - moduleList = (usePrecomputed ? - instance.yui.sorted : - instance.yui.requires); + moduleList = instance.yui.sorted; // gotta copy this or else it pollutes the client runtime mojitYuiModules = Y.mojito.util.copy(moduleList); @@ -395,7 +327,7 @@ YUI.add('mojito-dispatcher', function(Y, NAME) { logger.log('Dispatcher created', 'debug', NAME); - appConfigStatic = store.getAppConfig({}, 'application'); + appConfigStatic = store.getAppConfig({}); appShareYUIInstance = (false !== appConfigStatic.shareYUIInstance); usePrecomputed = appConfigStatic.yui && (-1 !== diff --git a/source/lib/app/autoload/mojito-client.client.js b/source/lib/app/autoload/mojito-client.client.js index aa425cb04..e2db65ea8 100644 --- a/source/lib/app/autoload/mojito-client.client.js +++ b/source/lib/app/autoload/mojito-client.client.js @@ -697,9 +697,8 @@ YUI.add('mojito-client', function(Y, NAME) { command.instance, this.context, function(err, details) { // if there is a controller in the client type details, that - // means the controller exists here "cast details.controller - // to Boolean" ;) - var existsOnClient = Boolean(details.controller); + // means the controller exists here + var existsOnClient = Boolean(details['controller-module']); command.context = my.context; diff --git a/source/lib/app/autoload/resource-store-adapter.common.js b/source/lib/app/autoload/resource-store-adapter.common.js index e6d4aa5c5..f73e08b25 100644 --- a/source/lib/app/autoload/resource-store-adapter.common.js +++ b/source/lib/app/autoload/resource-store-adapter.common.js @@ -18,8 +18,7 @@ */ YUI.add('mojito-resource-store-adapter', function(Y, NAME) { - var APP_ROOT_PATH = '', - logger; + var logger; Y.namespace('mojito').ResourceStoreAdapter = { @@ -36,14 +35,9 @@ YUI.add('mojito-resource-store-adapter', function(Y, NAME) { logger.log('resource store adapter init', 'mojito', NAME); - APP_ROOT_PATH = resourceStore._root; - this.ENV = env; this.store = resourceStore; - this._root = resourceStore._root; - this._staticURLs = resourceStore._staticURLs; - return this; }, @@ -126,12 +120,12 @@ YUI.add('mojito-resource-store-adapter', function(Y, NAME) { getAppPath: function() { - return APP_ROOT_PATH; + return this.store._config.root; }, - getAppConfig: function(context, name) { - return this.store.getAppConfig(context, name); + getAppConfig: function(context) { + return this.store.getAppConfig(context); }, @@ -191,27 +185,9 @@ YUI.add('mojito-resource-store-adapter', function(Y, NAME) { }, - getYuiConfigAllMojits: function(env, ctx) { - //logger.log('getYuiConfigAllMojits', 'warn', NAME); - return this.store.getYuiConfigAllMojits(env, ctx); - }, - - - getYuiConfigApp: function(env, ctx) { - //logger.log('getYuiConfigApp', 'warn', NAME); - return this.store.getYuiConfigApp(env, ctx); - }, - - - getYuiConfigFw: function(env, ctx) { - //logger.log('getYuiConfigFw', 'warn', NAME); - return this.store.getYuiConfigFw(env, ctx); - }, - - - serializeClientStore: function(ctx, instances) { + serializeClientStore: function(ctx) { //logger.log('serializeClientStore', 'warn', NAME); - return this.store.serializeClientStore(ctx, instances); + return this.store.serializeClientStore(ctx); }, @@ -221,12 +197,6 @@ YUI.add('mojito-resource-store-adapter', function(Y, NAME) { }, - fileFromStaticHandlerURL: function(url) { - //logger.log('fileFromStaticHandlerURL', 'warn', NAME); - return this.store.fileFromStaticHandlerURL(url); - }, - - getRoutes: function(ctx) { //logger.log('getRoutes', 'warn', NAME); return this.store.getRoutes(ctx); @@ -234,5 +204,6 @@ YUI.add('mojito-resource-store-adapter', function(Y, NAME) { }; }, '0.1.0', {requires: [ - 'mojito-util' + 'mojito-util', + 'json-stringify' ]}); diff --git a/source/lib/app/autoload/store.client.js b/source/lib/app/autoload/store.client.js index db1c1fd92..7405ff557 100644 --- a/source/lib/app/autoload/store.client.js +++ b/source/lib/app/autoload/store.client.js @@ -191,7 +191,7 @@ YUI.add('mojito-client-store', function(Y, NAME) { /* * TODO: REVIEW RE [Issue 78] */ - getAppConfig: function(context, name) { + getAppConfig: function(context) { return this.appConfig; }, diff --git a/source/lib/management/commands/build.js b/source/lib/app/commands/build.js similarity index 79% rename from source/lib/management/commands/build.js rename to source/lib/app/commands/build.js index f26b53265..35b9a531e 100644 --- a/source/lib/management/commands/build.js +++ b/source/lib/app/commands/build.js @@ -9,17 +9,16 @@ var libpath = require('path'), - utils = require('../utils'), + utils = require(libpath.join(__dirname, '../../management/utils')), fs = require('fs'), libqs = require('querystring'), - ResourceStore = require(libpath.join(__dirname, '../..', - 'store.server.js') - ), MODE_755 = parseInt('755', 8), + getSpecURL, mkdirP, rmdirR, writeWebPagesToFiles, - Y = require('yui').YUI({useSync: true}).use('json-parse', 'json-stringify'); + YUI = require('yui').YUI, + Y = YUI({useSync: true}).use('json-parse', 'json-stringify', 'escape'); Y.applyConfig({useSync: false}); @@ -67,13 +66,28 @@ exports.options = [ * @param {Function} callback Function to invoke on commmand completion. */ exports.run = function(params, options, callback) { - - var store = new ResourceStore(process.cwd()), + var store, type = 'html5app', + cwd = process.cwd(), destination, appConfig, config = {}; + Y.applyConfig({ + useSync: true, + modules: { + 'mojito-resource-store': { + fullpath: libpath.join(__dirname, '../../store.server.js') + } + } + }); + Y.use('mojito-resource-store'); + Y.applyConfig({useSync: false}); + store = new Y.mojito.ResourceStore({ + root: cwd, + context: {} + }); + if (!params[0]) { params[0] = ''; } @@ -91,26 +105,18 @@ exports.run = function(params, options, callback) { if (params[1] && params[1][0] === '/') { destination = libpath.join(params[1]); } else if (params[1]) { - destination = libpath.join(store._root, params[1]); + destination = libpath.join(cwd, params[1]); } else { - destination = libpath.join(store._root, 'artifacts/builds', type); + destination = libpath.join(cwd, 'artifacts/builds', type); } // Are we in a Mojito App? - utils.isMojitoApp(store._root, exports.usage, true); + utils.isMojitoApp(cwd, exports.usage, true); - // TODO: probably should try to use store to read appConfig - try { - appConfig = Y.JSON.parse(String(fs.readFileSync(libpath.join(store._root, - 'application.json')))); - appConfig = appConfig[0]; - - // Is there a "builds" section for this "type" in the appConfig - if (appConfig.builds && appConfig.builds[type]) { - config = appConfig.builds[type]; - } - } catch (err) { - // there is no application.json, but that is okay + appConfig = store.getStaticAppConfig(); + // Is there a "builds" section for this "type" in the appConfig + if (appConfig.builds && appConfig.builds[type]) { + config = appConfig.builds[type]; } if (options.replace) { @@ -148,11 +154,21 @@ exports.buildhtml5app = function(cmdOptions, store, config, destination, indexJs = "module.exports = require('express')." + "createServer(require('express')['static'](__dirname));", url, + storeURLs, urls = {}, // from: to app, context = '', + contextObj = libqs.parse(cmdOptions.context), appConfig, - tunnelPrefix; + tunnelPrefix, + dynamicURLs = {}, + mr, + mojitRes, + mojitRess, + sr, + specRes, + specRess, + id; if (typeof cmdOptions.context === 'string') { // Parse the context into an object @@ -171,12 +187,11 @@ exports.buildhtml5app = function(cmdOptions, store, config, destination, store.preload(); - appConfig = store.getAppConfig(libqs.parse(cmdOptions.context), - 'application'); + appConfig = store.getAppConfig(contextObj); tunnelPrefix = appConfig.tunnelPrefix || '/tunnel'; console.log('Building a "' + type + '" of the Mojito application at "' + - store._root + '"'); + store._config.root + '"'); console.log('...'); @@ -184,9 +199,10 @@ exports.buildhtml5app = function(cmdOptions, store, config, destination, manifest += 'CACHE:\n'; // Copy all the files into the destination directory - for (url in store._staticURLs) { - if (store._staticURLs.hasOwnProperty(url)) { - from = store._staticURLs[url]; // filesystem path + storeURLs = store.getAllURLs(); + for (url in storeURLs) { + if (storeURLs.hasOwnProperty(url)) { + from = storeURLs[url]; // filesystem path to = libpath.join(destination, url); extension = from.split('.').pop(); @@ -194,7 +210,7 @@ exports.buildhtml5app = function(cmdOptions, store, config, destination, manifest += url + '\n'; - if (serverFiles[extension] !== undefined) { + if (serverFiles[extension]) { urls[url + context] = url; mkdirP(libpath.dirname(libpath.join(destination, url)), MODE_755); @@ -204,9 +220,32 @@ exports.buildhtml5app = function(cmdOptions, store, config, destination, } } + mojitRess = store.getResources('client', contextObj, {type: 'mojit'}); + for (mr = 0; mr < mojitRess.length; mr += 1) { + mojitRes = mojitRess[mr]; + if (!mojitRes.url) { + continue; + } + url = mojitRes.url + '/definition.json'; + dynamicURLs[url] = true; + + specRess = store.getResources('client', contextObj, {type: 'spec', mojit: mojitRes.name}); + for (sr = 0; sr < specRess.length; sr += 1) { + specRes = specRess[sr]; + dynamicURLs[specRes.url] = true; + } + } + + for (id in appConfig.specs) { + if (appConfig.specs.hasOwnProperty(id)) { + url = getSpecURL(appConfig, id); + dynamicURLs[url] = true; + } + } + // Get all the dynamic URLs we have to call via the "tunnel" - for (url in store._dynamicURLs) { - if (store._dynamicURLs.hasOwnProperty(url)) { + for (url in dynamicURLs) { + if (dynamicURLs.hasOwnProperty(url)) { urls[tunnelPrefix + url + context] = url; mkdirP(libpath.dirname(libpath.join(destination, url)), MODE_755); } @@ -230,6 +269,24 @@ exports.buildhtml5app = function(cmdOptions, store, config, destination, }; +getSpecURL = function(appConfig, id) { + var prefix = '/static', + parts = id.split(':'), + typeName = parts[0], + specName = parts[1] || 'default', + ns = typeName.replace(/\./g, '_'), + url; + + if (appConfig && appConfig.staticHandling && + appConfig.staticHandling.hasOwnProperty('prefix')) { + prefix = (appConfig.staticHandling.prefix ? '/' + + appConfig.staticHandling.prefix : ''); + } + url = prefix + '/' + typeName + '/specs/' + specName + '.json'; + return url; +}; + + mkdirP = function(p, mode) { var ps = libpath.normalize(p).split('/'), i; @@ -370,11 +427,13 @@ function forceRelativePaths(root, relativePath, content, force) { content = content.replace(/(src|href)="([^"]+)"/g, function(all, name, val) { - var fixed = val; - if ('/' === val.charAt(0)) { - fixed = libpath.join(pathTo(libpath.dirname(val), dirname), - libpath.basename(val)); + // FUTURE: once the "/" aren't escaped, we can do this easier + var fixed = utils.decodeHTMLEntities(val); + if ('/' === fixed.charAt(0)) { + fixed = libpath.join(pathTo(libpath.dirname(fixed), dirname), + libpath.basename(fixed)); } + fixed = Y.Escape.html(fixed); return name + '="' + fixed + '"'; }); } diff --git a/source/lib/management/commands/compile.js b/source/lib/app/commands/compile.js similarity index 66% rename from source/lib/management/commands/compile.js rename to source/lib/app/commands/compile.js index 70b3b147a..ef81e2254 100644 --- a/source/lib/management/commands/compile.js +++ b/source/lib/app/commands/compile.js @@ -8,13 +8,9 @@ /*jslint anon:true, sloppy:true, regexp:true, nomen:true*/ -var path = require('path'), - utils = require('../utils'), - fs = require('fs'), - - // Mojito Resource Store - ResourceStore = require(path.join(__dirname, '../..', - 'store.server.js')), +var libpath = require('path'), + libfs = require('fs'), + libutils = require(libpath.join(__dirname, '../../management/utils')), // private compilation function container compile = {}, @@ -109,8 +105,109 @@ options = [ ]; +/** + * Returns details on how to make inline CSS for mojits. + * + * This example comes from (a modified) GSG5. + * [ { + * mojitName: 'FlickrDetail', + * yuiModuleName: 'inlinecss/FlickrDetail', + * dest: '/blah/mojits/FlickrDetail/autoload/compiled' + + * '/css.iphone.client.js', + * srcs: { + * '/static/FlickrDetail/assets/index.css': true, + * '/static/FlickrDetail/assets/message.css': true + * } + * ] + * + * @method getInlineCssMojits + * @param store {string} resource store + * @param env {string} "client" or "server" + * @param context {object} runtime context + * @return {array} object describing where to put the inline CSS file and what it should contain + */ +function getInlineCssMojits(store, env, context) { + var m, + mojit, + mojits, + mojitRes, + r, + res, + ress, + selector, + dest, + srcs, + inlines = []; + + mojits = store.listAllMojits(); + for (m = 0; m < mojits.length; m += 1) { + mojit = mojits[m]; + + mojitRes = store.getResources('client', context, {type: 'mojit', name: mojit}); + mojitRes = mojitRes[0]; + if ('mojito' === mojitRes.source.pkg.name) { + // don't write framework-provided inlinecss into the framework directory + continue; + } + + // TODO: This isn't quite right, since multiple contexts might map to + // posls with the same lead selector. + selector = store.selector.getPOSLFromContext(context)[0]; + + srcs = []; + ress = store.getResources(env, context, {mojit: mojit}); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if (mojit !== res.mojit) { + continue; + } + if ((res.type === 'asset') && (res.subtype === 'css')) { + srcs[res.url] = true; + } + } + dest = 'autoload/compiled/inlinecss' + ('*' === selector ? '' : '.' + + selector) + '.common.js'; + dest = libpath.join(mojitRes.source.fs.fullPath, dest); + if (Object.keys(srcs).length) { + inlines.push({ + mojitName: mojit, + yuiModuleName: 'inlinecss/' + mojit, + dest: dest, + srcs: srcs + }); + } + } // for each mojit + + return inlines; +} + + +/** + * Creates the Resource Store object. + * @private + * @method MakeStore + * @param {Object} cfg Configuration for the resource store. + * @return {ResourceStore} the new resource store object + */ +function makeStore(cfg) { + var store; + Y.applyConfig({ + useSync: true, + modules: { + 'mojito-resource-store': { + fullpath: libpath.join(__dirname, '../../store.server.js') + } + } + }); + Y.use('mojito-resource-store'); + store = new Y.mojito.ResourceStore(cfg); + Y.applyConfig({useSync: true}); + return store; +} + + run = function(params, options, callback) { - var store = new ResourceStore(process.cwd()), + var cwd = process.cwd(), displayResults, type, context = {}; @@ -118,7 +215,7 @@ run = function(params, options, callback) { // TODO: don't assign to a parameter. options = options || {}; - utils.isMojitoApp(store._root, exports.usage, true); + libutils.isMojitoApp(cwd, exports.usage, true); if (options.context) { // TODO: parseURL. @@ -126,11 +223,11 @@ run = function(params, options, callback) { } displayResults = function(err) { - utils.log(''); + libutils.log(''); msgs.forEach(function(msg) { - utils.log(msg); + libutils.log(msg); }); - utils.log(''); + libutils.log(''); callback(err); }; @@ -146,11 +243,11 @@ run = function(params, options, callback) { compileType = type = params.shift(); if (!type) { - utils.error('Please provide the type of compilation you want.', + libutils.error('Please provide the type of compilation you want.', exports.usage, true); } if (!compile[type]) { - utils.error("Unknown type '" + type + "'", exports.usage, true); + libutils.error("Unknown type '" + type + "'", exports.usage, true); } compile[type](context, options, displayResults); @@ -190,14 +287,14 @@ compile.all = function(context, options, callback) { return callback(); } if (options.verbose) { - utils.log('executing -- ' + action.toUpperCase() + ' --'); + libutils.log('executing -- ' + action.toUpperCase() + ' --'); } compile[action](context, options, function(err) { if (err) { return callback(err); } if (options.verbose) { - utils.log('done -- ' + action.toUpperCase() + ' --\n\n'); + libutils.log('done -- ' + action.toUpperCase() + ' --\n\n'); } runOne(); }); @@ -214,10 +311,7 @@ compile.all = function(context, options, callback) { * @return {object} The return value from any optional callback function. */ compile.inlinecss = function(context, options, callback) { - var app = new utils.App({ - port: options.port || 11111, - verbose: options.verbose - }), + var app, action = options.remove ? 'Removed' : 'Created', processed = 0, cwd = process.cwd(), @@ -225,8 +319,13 @@ compile.inlinecss = function(context, options, callback) { inlines, inlineNext; + app = new libutils.App({ + port: options.port || 11111, + verbose: options.verbose + }); + if (options.app) { - utils.warn('Creating app-level inline css not supported\n'); + libutils.warn('Creating app-level inline css not supported\n'); return callback(); } @@ -238,6 +337,7 @@ compile.inlinecss = function(context, options, callback) { shortDest, i, url, + storeURLs, fs2url = {}, srcKey, srcDir, @@ -252,9 +352,10 @@ compile.inlinecss = function(context, options, callback) { mojitName = inline.mojitName; // need a reverse mapping - for (url in store._staticURLs) { - if (store._staticURLs.hasOwnProperty(url)) { - fs2url[store._staticURLs[url]] = url; + storeURLs = store.getAllURLs(); + for (url in storeURLs) { + if (storeURLs.hasOwnProperty(url)) { + fs2url[storeURLs[url]] = url; } } @@ -267,7 +368,7 @@ compile.inlinecss = function(context, options, callback) { if (options.remove) { if (removeFile(inline.dest)) { if (options.verbose) { - utils.log('Removed: ' + inline.dest); + libutils.log('Removed: ' + inline.dest); } processed += 1; // on to the next one @@ -313,10 +414,10 @@ compile.inlinecss = function(context, options, callback) { if ('data:' === url.substr(0, 5)) { return whole; } - srcDir = path.dirname(inline.srcs[srcKey]); - fs = path.join(srcDir, url); + srcDir = libpath.dirname(inline.srcs[srcKey]); + fs = libpath.join(srcDir, url); if (!fs2url[fs]) { - utils.warn('couldn\'t normalize url(' + url + + libutils.warn('couldn\'t normalize url(' + url + ')(' + fs + ') in ' + inline.srcs[srcKey]); return whole; } @@ -357,19 +458,19 @@ compile.inlinecss = function(context, options, callback) { var store; if (err) { - utils.error(err); + libutils.error(err); } else { store = appInstance.store; - inlines = store.getInlineCssMojits('client', context); + inlines = getInlineCssMojits(store, 'client', context); - utils.log((options.remove ? 'Removing' : 'Creating') + + libutils.log((options.remove ? 'Removing' : 'Creating') + ' inline css...'); inlineNext(store, function(err) { try { app.close(); } catch (err2) { - utils.warn('(app server was not running) ' + err2); + libutils.warn('(app server was not running) ' + err2); } if (err) { @@ -394,88 +495,115 @@ compile.inlinecss = function(context, options, callback) { */ compile.rollups = function(context, options, callback) { var cwd = process.cwd(), - action = options.remove ? 'Removed' : 'Created', + store = makeStore({root: cwd, appConfig: { assumeRollups: true}}), + rollups = {}, processed = 0, - store = new ResourceStore(cwd), - rollups, - rollup, - mojitName; + r, + res, + ress, + m, + mojit, + mojits, + mojitRes, + dest, + s, + src, + srcs, + shortDest, + rollupBody; store.preload(); - utils.log((options.remove ? 'Removing' : 'Creating') + ' rollups...'); + libutils.log((options.remove ? 'Removing' : 'Creating') + ' rollups...'); - function rollOneUp(rollup) { - var i, - src, - rollupBody, - shortDest; + if (options.app || options.core) { + // FUTURE: rollup true-app-level resources somewhere? + mojits = [ 'shared' ]; + } else { + mojits = store.listAllMojits(); + } + if (options.mojit) { + mojits = [ options.mojit ]; + } - shortDest = rollup.dest; - if (cwd === shortDest.substr(0, cwd.length)) { - shortDest = shortDest.substr(cwd.length + 1); - } + for (m = 0; m < mojits.length; m += 1) { + mojit = mojits[m]; - if (options.remove) { - if (path.existsSync(rollup.dest)) { - try { - fs.unlinkSync(rollup.dest); - } catch (err) { - return callback(err); - } - - if (options.verbose) { - utils.log('Removed: ' + shortDest); - } - processed += 1; + if ('shared' !== mojit) { + mojitRes = store.getResources('client', context, {type: 'mojit', name: mojit}); + if (!mojitRes || !mojitRes.length) { + callback('Unknown "' + mojit + '"'); + return; + } + mojitRes = mojitRes[0]; + if ('mojito' === mojitRes.source.pkg.name) { + // don't write framework-provided rollups into the framework directory + continue; } - return; } - rollupBody = ''; - for (i = 0; i < rollup.srcs.length; i += 1) { - src = rollup.srcs[i]; - if (!options['core'] || src.match(/\/mojito\//)) { - rollupBody += fs.readFileSync(src, 'utf-8'); + ress = store.getResources('client', context, {mojit: mojit}); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if (res.mojit !== mojit) { + continue; + } + if (options.core && 'mojito' !== res.source.pkg.name) { + continue; + } + dest = res.source.fs.rollupPath; + if (dest) { + if (!rollups[dest]) { + rollups[dest] = []; + } + rollups[dest].push(res.source.fs.fullPath); + continue; } } - fs.writeFileSync(rollup.dest, rollupBody, 'utf-8'); - if (options.verbose) { - utils.log('Rolled up: ' + shortDest); - } - processed += 1; } - if (options.app || options.core) { - rollup = store.getRollupsApp('client', context); - rollOneUp(rollup); - utils.log('All rollups have been ' + - (options.remove ? 'removed' : 'created') + '\n'); - callback(); - return; - } + for (dest in rollups) { + if (rollups.hasOwnProperty(dest)) { + srcs = rollups[dest]; - rollups = store.getRollupsMojits('client', context); - - if (options.mojit && !rollups[options.mojit]) { - callback('Unknown "' + options.mojit + '"'); - return; - } + shortDest = dest; + if (cwd === shortDest.substr(0, cwd.length)) { + shortDest = shortDest.substr(cwd.length + 1); + } - for (mojitName in rollups) { - if (rollups.hasOwnProperty(mojitName)) { + if (options.remove) { + if (libpath.existsSync(dest)) { + try { + libfs.unlinkSync(dest); + } catch (err) { + return callback(err); + } + if (options.verbose) { + libutils.log('Removed: ' + shortDest); + } + processed += 1; + } + continue; + } - // TODO: verify the logic inversion here. - //if (options['mojit'] && mojitName != options['mojit']) {continue}; - if (!options.mojit || (mojitName === options.mojit)) { - rollup = rollups[mojitName]; - rollOneUp(rollup); + if (!srcs.length) { + continue; + } + rollupBody = ''; + for (s = 0; s < srcs.length; s += 1) { + src = srcs[s]; + rollupBody += libfs.readFileSync(src, 'utf-8'); } + libfs.writeFileSync(dest, rollupBody, 'utf-8'); + if (options.verbose) { + libutils.log('Rolled up: ' + shortDest); + } + processed += 1; } } - msgs.push(action + ' compiled rollup YUI modules for ' + processed + - ' mojits.'); + msgs.push((options.remove ? 'Removed' : 'Created') + + ' compiled rollup YUI modules for ' + processed + ' mojits.'); callback(); }; @@ -488,7 +616,8 @@ compile.rollups = function(context, options, callback) { * @return {object} The return value from any optional callback function. */ compile.views = function(context, options, callback) { - var store = new ResourceStore(process.cwd()), + var cwd = process.cwd(), + store = makeStore({root: cwd}), compiledFilename = '/autoload/compiled/views.common.js', mojits, yuiConfig, @@ -505,52 +634,56 @@ compile.views = function(context, options, callback) { // there are no views in the app, so no need to do this if (options.app) { - utils.warn('Compiling app-level views not supported\n'); + libutils.warn('Compiling app-level views not supported\n'); return callback(); } store.preload(); - utils.log((options.remove ? 'Removing compiled' : 'Compiling') + + libutils.log((options.remove ? 'Removing compiled' : 'Compiling') + ' views...'); - // Get all the Mojits - mojits = store.getAllMojits('server', context); - - if (options.mojit && !mojits[options.mojit]) { - callback('Unknown "' + options.mojit + '"'); - return; + if (options.mojit) { + mojits = [ options.mojit ]; + } else { + mojits = store.listAllMojits(); } - // loop through all mojits one at a time, only once per mojit - Object.keys(mojits).forEach(function(mojitName) { - var outputFilepath = store._mojitPaths[mojitName] + compiledFilename, + Y.Array.each(mojits, function(mojitName) { + var mojitRes, + outputFilepath, mojitNs = mojitName.replace(/\./g, '_'), yuiModuleCacheWriter, viewName, MojY; + mojitRes = store.getResources('server', context, {type: 'mojit', name: mojitName}); + if (!mojitRes || !mojitRes.length) { + callback('Unknown mojit "' + options.mojit + '"'); + } + mojitRes = mojitRes[0]; + + outputFilepath = libpath.join(mojitRes.source.fs.fullPath, 'autoload/compiled/views.common.js'); + + if ('mojito' === mojitRes.source.pkg.name) { + // don't write framework-provided views into the framework directory + return; + } + if (options.remove) { if (removeFile(outputFilepath)) { if (options.verbose) { - utils.log('Removed: ' + outputFilepath); + libutils.log('Removed: ' + outputFilepath); } processed += 1; } return; } - // Skip anything in the "lib/mojits" (open source) or - // "mojit/mojits" (ynodejs_mojito) directories as it's internal - if (outputFilepath.indexOf('lib/mojits') >= 0 || - outputFilepath.indexOf('mojito/mojits') >= 0) { - return; - } - yuiModuleCacheWriter = new YuiModuleCacheWriter('views/' + mojitName, outputFilepath, options); - mojit = mojits[mojitName]; + mojit = store.getMojitTypeDetails('server', context, mojitName); if (mojit.views) { // Check each view for a template and engine @@ -603,8 +736,9 @@ compile.views = function(context, options, callback) { * @return {object} The return value from any optional callback function. */ compile.json = function(context, options, callback) { - var store = new ResourceStore(process.cwd()), - app = new utils.App({ + var cwd = process.cwd(), + store = makeStore({root: cwd}), + app = new libutils.App({ port: options.port || 11111, verbose: options.verbose, appConfig: { @@ -617,7 +751,7 @@ compile.json = function(context, options, callback) { processed = 0, total = 0, action = options.remove ? 'Removed' : 'Created', - compiledFilename = '/autoload/compiled/json.common.js', + compiledFilename = 'autoload/compiled/json.common.js', processNextMojit, processSpecs, processNextSpec, @@ -632,15 +766,17 @@ compile.json = function(context, options, callback) { // there are no json configs in the app, so no need to do this if (options.app) { - utils.warn('Compiling app-level json not supported\n'); + libutils.warn('Compiling app-level json not supported\n'); return callback(); } store.preload(); - // Get all the Mojits - mojits = store.getAllMojits('server', context); - mojitNames = Object.keys(mojits); + if (options.mojit) { + mojitNames = [ options.mojit ]; + } else { + mojitNames = store.listAllMojits(); + } total = mojitNames.length; processSpecs = function(newSpecs, mojitName, store, yuiModuleCacheWriter, @@ -663,7 +799,7 @@ compile.json = function(context, options, callback) { } if (options.verbose) { - utils.log('\tprocessing spec... ' + fullSpecName); + libutils.log('\tprocessing spec... ' + fullSpecName); } parts = fullSpecName.split(':'); @@ -672,10 +808,14 @@ compile.json = function(context, options, callback) { specName = parts[1] || 'default'; if (!mojitName || mName === mojitName) { + /* NOTE_1: During the resource store redesign, it was noticed + * that this branch is never called, which STRONGLY suggests that + * this feature was never used. + */ specUrl = '/' + mName + '/specs/' + specName + '.json'; if (options.verbose) { - utils.log('found spec (' + specName + ') for ' + mName); + libutils.log('found spec (' + specName + ') for ' + mName); } getContentFromUrl(app, specUrl, jsonOpts, function(spec) { yuiModuleCacheWriter.createNamespace('compiled.' + mojitNs + @@ -687,14 +827,18 @@ compile.json = function(context, options, callback) { } }; - processDefinitionJSON = function(mojitName, store, yuiModuleCacheWriter, + processDefinitionJSON = function(mojitRes, store, yuiModuleCacheWriter, cb) { - var processFullDefinition = function(mojitName, ymcw, cb) { + var mojitName = mojitRes.name, + processFullDefinition, + processPreloadDefinitions; + + processFullDefinition = function(mojitName, ymcw, cb) { + // TODO: probably want to use mojitRes.url instead var url = staticPrefix + mojitName + '/definition.json'; getContentFromUrl(app, url, jsonOpts, function(definition) { var defObj = Y.JSON.parse(definition); - ymcw.createNamespace('compiled.' + mojitName.replace(/\./g, '_') + '.definitions').cache( 'definition', @@ -702,21 +846,21 @@ compile.json = function(context, options, callback) { ); cb(); }); - }, - processPreloadDefinitions = function(mojitNames, ymcw, cb) { - var continuation = function() { - if (mojitNames.length) { - processFullDefinition(mojitNames.shift(), ymcw, - continuation); - } else { - cb(); - } - }; - - processFullDefinition(mojitNames.shift(), ymcw, - continuation); + }; + processPreloadDefinitions = function(mojitNames, ymcw, cb) { + var continuation = function() { + if (mojitNames.length) { + processFullDefinition(mojitNames.shift(), ymcw, + continuation); + } else { + cb(); + } }; + processFullDefinition(mojitNames.shift(), ymcw, + continuation); + }; + /* * The resource store doesn't respond well if you call * store._getMojitConfig() a bunch of times in a row synchronously. @@ -724,25 +868,29 @@ compile.json = function(context, options, callback) { */ setTimeout(function() { if (options.verbose) { - utils.log('looking for definition.json for ' + mojitName); + libutils.log('looking for definition.json for ' + mojitName); } var specsToPreload = [], mojitNamesToPreload = [], - definition = store._getMojitConfig('server', {}, - mojitName, 'definition'); + path, + definition; + + path = libpath.join(mojitRes.source.fs.fullPath, 'definition.json'); + + // TODO: use commandline context instead? + definition = store.config.readConfigYCB(path, {}); if (Object.keys(definition).length > 0) { if (definition.preload) { if (options.verbose) { - utils.log('processing preload mojits for ' + mojitName); + libutils.log('processing preload mojits for ' + mojitName); } definition.preload.forEach(function(toPreload) { Object.keys(store._appConfigStatic.specs).forEach( function(fullSpecName) { var mName = fullSpecName.split(':').shift(); - if (mName === toPreload) { specsToPreload.push(fullSpecName); } @@ -751,9 +899,9 @@ compile.json = function(context, options, callback) { }); if (options.verbose) { - utils.log('processing the preload specs for ' + + libutils.log('processing the preload specs for ' + mojitName); - utils.log(specsToPreload.join(', ')); + libutils.log(specsToPreload.join(', ')); } specsToPreload.forEach(function(stp) { @@ -781,6 +929,7 @@ compile.json = function(context, options, callback) { processNextMojit = function(store, cb) { var mojitName = mojitNames.shift(), + mojitRes, outputFilepath, theCloser, yuiModuleCacheWriter; @@ -790,25 +939,27 @@ compile.json = function(context, options, callback) { } if (options.verbose) { - utils.log('Processing mojit... ' + mojitName); + libutils.log('Processing mojit... ' + mojitName); } - outputFilepath = store._mojitPaths[mojitName] + compiledFilename; count += 1; - if (options.remove) { - if (removeFile(outputFilepath)) { - processed += 1; - } + mojitRes = store.getResources('server', context, {type: 'mojit', name: mojitName}); + if (!mojitRes || !mojitRes.length) { return processNextMojit(store, cb); } + mojitRes = mojitRes[0]; - // Skip anything in the "lib/mojits" (open source) or - // "mojit/mojits" (ynodejs_mojito) directories as it's internal - if (outputFilepath.indexOf('lib/mojits') >= 0 || - outputFilepath.indexOf('mojito/mojits') >= 0) { - if (options.verbose) { - utils.log('skipping ' + outputFilepath); + if ('mojito' === mojitRes.source.pkg.name) { + // don't write framework-provided json into the framework directory + return processNextMojit(store, cb); + } + + outputFilepath = libpath.join(mojitRes.source.fs.fullPath, compiledFilename); + + if (options.remove) { + if (removeFile(outputFilepath)) { + processed += 1; } return processNextMojit(store, cb); } @@ -824,14 +975,16 @@ compile.json = function(context, options, callback) { }; // look for definitions - processDefinitionJSON(mojitName, store, yuiModuleCacheWriter, + // TODO: really only need to do this after all the mojits are processed + processDefinitionJSON(mojitRes, store, yuiModuleCacheWriter, function() { // look for specs - var specs = []; - if (store._appConfigStatic.specs) { + var appConfig = store.getStaticAppConfig(), + specs = []; + if (appConfig.specs) { specs = Object.keys(store._appConfigStatic.specs); } - processSpecs(specs, mojitName, store, yuiModuleCacheWriter, + processSpecs(specs, mojitRes.name, store, yuiModuleCacheWriter, theCloser); }); }; @@ -839,18 +992,18 @@ compile.json = function(context, options, callback) { // start up the server app.start(function(err, appInst) { if (err) { - utils.error(err); + libutils.error(err); return; } - utils.log((options.remove ? 'Removing compiled' : 'Compiling') + + libutils.log((options.remove ? 'Removing compiled' : 'Compiling') + ' json...'); processNextMojit(store, function() { try { app.close(); } catch (err2) { - utils.info('(app server was not running) ' + err2); + libutils.info('(app server was not running) ' + err2); } msgs.push(action + ' compiled JSON YUI modules for ' + processed + ' mojits.'); @@ -861,7 +1014,7 @@ compile.json = function(context, options, callback) { clean = function(context, options, cb) { - utils.warn('Cleaning all compiled files!'); + libutils.warn('Cleaning all compiled files!'); options.remove = true; options.app = true; compile.all(context, options, function() { @@ -882,12 +1035,12 @@ everything = function(context, options, cb) { mkdirP = function(p, mode) { - var ps = path.normalize(p).split('/'), + var ps = libpath.normalize(p).split('/'), i; for (i = 0; i <= ps.length; i += 1) { try { - fs.mkdirSync(ps.slice(0, i).join('/'), mode); + libfs.mkdirSync(ps.slice(0, i).join('/'), mode); } catch (err) { // Dirty way to check dir } @@ -896,23 +1049,23 @@ mkdirP = function(p, mode) { rmdirR = function(path) { - var files = fs.readdirSync(path), + var files = libfs.readdirSync(path), i, currFile; /* Loop through and delete everything in the sub-tree after checking it */ for (i = 0; i < files.length; i += 1) { - currFile = fs.statSync(path + '/' + files[i]); + currFile = libfs.statSync(path + '/' + files[i]); if (currFile.isDirectory()) { // Recursive function back to the beginning rmdirR(path + '/' + files[i]); } else if (currFile.isSymbolicLink()) { // Unlink symlinks - fs.unlinkSync(path + '/' + files[i]); + libfs.unlinkSync(path + '/' + files[i]); } else { // Assume it's a file - perhaps a try/catch belongs here? - fs.unlinkSync(path + '/' + files[i]); + libfs.unlinkSync(path + '/' + files[i]); } } @@ -920,7 +1073,7 @@ rmdirR = function(path) { * Now that we know everything in the sub-tree has been deleted, * we can delete the main directory. Huzzah for the shopkeep. */ - return fs.rmdirSync(path); + return libfs.rmdirSync(path); }; @@ -931,8 +1084,8 @@ getContentFromUrl = function(app, url, opts, callback) { } app.getWebPage(url, opts, function(err, url, content) { if (err) { - utils.error('FAILED to get ' + url); - utils.error(err); + libutils.error('FAILED to get ' + url); + libutils.error(err); } else { callback(content); } @@ -941,9 +1094,9 @@ getContentFromUrl = function(app, url, opts, callback) { removeFile = function(file) { - if (path.existsSync(file)) { + if (libpath.existsSync(file)) { try { - fs.unlinkSync(file); + libfs.unlinkSync(file); } catch (err) { return false; } @@ -1024,7 +1177,7 @@ YuiModuleCacheWriter = function(name, file, options) { /** * Create a clean prototype instance. */ -YuiModuleCacheWriter.prototype = utils.heir(YuiModuleCacher.prototype); +YuiModuleCacheWriter.prototype = libutils.heir(YuiModuleCacher.prototype); /** @@ -1045,18 +1198,18 @@ YuiModuleCacheWriter.prototype.write = function() { // only write the file if there is something to write if (Object.keys(namespaces).length > 0) { try { - mkdirP(path.dirname(file), parseInt('755', 8)); + mkdirP(libpath.dirname(file), parseInt('755', 8)); if (this.opts.verbose) { - utils.log('writing file: ' + file); + libutils.log('writing file: ' + file); } - fs.writeFileSync(file, output, 'utf8'); + libfs.writeFileSync(file, output, 'utf8'); if (this.opts.verbose) { - utils.log('Created: ' + file); + libutils.log('Created: ' + file); } return true; } catch (err) { - utils.error('Error writing file: ' + file); - utils.error(err); + libutils.error('Error writing file: ' + file); + libutils.error(err); return false; } } diff --git a/source/lib/management/commands/create.js b/source/lib/app/commands/create.js similarity index 97% rename from source/lib/management/commands/create.js rename to source/lib/app/commands/create.js index c5817d472..b2d2f8a79 100644 --- a/source/lib/management/commands/create.js +++ b/source/lib/app/commands/create.js @@ -8,10 +8,10 @@ /*jslint anon:true, sloppy:true, regexp:true, nomen:true*/ -var utils = require('../utils'), +var path = require('path'), fs = require('fs'), - path = require('path'), - archetypePath = path.join(__dirname, '/../../archetypes'), + utils = require(path.join(__dirname, '../../management/utils')), + archetypePath = path.join(__dirname, '../archetypes'), // Found at http://www.crockford.com/javascript/survey.html reservedWords = [ 'abstract', diff --git a/source/lib/management/commands/docs.js b/source/lib/app/commands/docs.js similarity index 97% rename from source/lib/management/commands/docs.js rename to source/lib/app/commands/docs.js index e9b7323b4..4c4ced0f7 100644 --- a/source/lib/management/commands/docs.js +++ b/source/lib/app/commands/docs.js @@ -8,11 +8,11 @@ /*jslint anon:true, sloppy:true, regexp:true, nomen:true*/ -var utils = require('../utils'), - fs = require('fs'), +var fs = require('fs'), path = require('path'), exec = require('child_process').exec, - copyExclude = require('../utils').copyExclude, + utils = require(path.join(__dirname, '../../management/utils')), + copyExclude = utils.copyExclude, usage, dir_mojito = path.join(__dirname, '../../'), dir_yuidoc = path.join(dir_mojito, 'libs/yuidoc'), diff --git a/source/lib/app/commands/gv.js b/source/lib/app/commands/gv.js new file mode 100644 index 000000000..b16caf2fc --- /dev/null +++ b/source/lib/app/commands/gv.js @@ -0,0 +1,608 @@ +/* + * Copyright (c) 2011-2012, Yahoo! Inc. All rights reserved. + * Copyrights licensed under the New BSD License. + * See the accompanying LICENSE file for terms. + */ + + +/*jslint anon:true, nomen:true, sloppy:true*/ + + +// TODO: +// * include YUI internal structure (but not edges/dependencies) +// * [warning][server] trace anything that leads to a YUI module that uses the DOM +// * also draw meta.optional edges +// * color-code or shape-code module types (from yui, mojito fw, app-level, +// mojits, affinity) + +var run, + libpath = require('path'), + libfs = require('fs'), + libutils = require(libpath.join(__dirname, '../../management/utils')), + YUI = require('yui').YUI, + Y = YUI(), + + MODE_ALL = parseInt('777', 8), + + artifactsDir = 'artifacts', + resultsDir = 'artifacts/gv'; + + + +// Quote a graphviz property. +function gvQuote(str) { + str = str.toString().replace(/"/g, '\\"'); + return '"' + str + '"'; +} + + +// Turn an object into a set of graphviz properties. +function gvStyle(style, nopad) { + var pairs = []; + Y.Object.each(style, function(v, k) { + pairs.push(k + '=' + gvQuote(v)); + }); + if (!pairs.length) { + return ''; + } + return (nopad ? '' : ' ') + '[' + pairs.join(',') + ']'; +} + + +function Node(name) { + this.name = name; + this.attrs = {}; // arbitrary user-defined attributes + this.style = {}; // graphviz style attributes +} + +function Edge(tail, head, directed) { + this.tail = tail; + this.head = head; + this.directed = directed || false; + this.attrs = {}; // arbitrary user-defined attributes + this.style = {}; // graphviz style attributes +} + +function Graph(name) { + this.name = name; + this.type = 'graph';// top-level: graph | digraph + // subgraph: group | subgraph | cluster + this.title = name; // title can be different than name + // (name is used as the identifier) + // (for .type===cluster you might want to set .style.label) + this.attrs = {}; // arbitrary user-defined attributes + this.style = {}; // graphviz style attributes for the Graph itself + this.styles = { // type: default graphviz style attributes + node: {}, // ... for nodes + edge: {}, // ... for nodes + graph: {}, // ... for nodes + all: {} // ... for everything + }; + this._nodes = {}; // id: Node + this._edges = {}; // id: Edge + this._subgraphs = {}; // id: Graph +} +Graph.prototype = { + + // finds the node, no matter how deeply nested + // creates the node if it doesn't already exist + // @param name {string} name of node + getNode: function(name) { + var node; + this._find('_nodes', name, function(parent, found) { + node = found; + }); + if (!node) { + node = new Node(name); + this._nodes[name] = node; + } + return node; + }, + + // finds the edge, no matter how deeply nested + // creates the edge if it doesn't already exist + // @param tail {string} name of tail node + // @param head {string} name of head node + getEdge: function(tail, head, directed) { + var id = this._makeEdgeID(tail, head, directed), + edge; + this._find('_edges', id, function(parent, found) { + edge = found; + }); + if (!edge) { + edge = new Edge(tail, head, directed); + this._edges[id] = edge; + } + return edge; + }, + + // finds the subgraph, no matter how deeply nested + // creates the subgraph if it doesn't already exist + // @param name {string} name of subgraph + // @return {Graph} found subgraph + getSubgraph: function(name) { + var subgraph; + this._find('_subgraphs', name, function(parent, found) { + subgraph = found; + }); + if (!subgraph) { + subgraph = new Graph(name); + this._subgraphs[name] = subgraph; + } + return subgraph; + }, + + // moves the node from existing subgraph to the one speciefied + // @param node {string} name of node to move + // @param parent {string} name of new parent parent + moveNodeToSubgraph: function(node, parent) { + var found; + this._find('_nodes', node, function(foundParent, foundItem) { + delete foundParent._nodes[node]; + found = foundItem; + }); + if (!found) { + found = new Node(node); + } + this.getSubgraph(parent)._nodes[node] = found; + }, + + // arranges for child subgraph to be drawn inside parent subgraph + // @param child {string} child subgraph name + // @param parent {string} parent subgraph name + moveSubgraphToSubgraph: function(child, parent) { + var found; + this._find('_subgraphs', child, function(foundParent, foundItem) { + delete foundParent._subgraphs[child]; + found = foundItem; + }); + if (!found) { + found = new Graph(child); + } + this.getSubgraph(parent)._subgraphs[child] = found; + }, + + // if a filter returns false, that node/edge/subgraph is skipped + // @param filters {object} callbacks to det + // @param filters.node {function(Node)} + // @param filters.edge {function(Edge)} + // @param filters.subgraph {function(Graph)} + // @param _ctx {object} [private] graph drawing context, for recursion + // @return {string} graphviz DOT notation + render: function(filters, _ctx) { + _ctx = _ctx || {depth: 0, count: 0}; + _ctx.count += 1; + var out = '', + section, + i, + indent = ''; + + // I would just do the following, but jslint is overly strict. + // indent = new Array(_ctx.depth + 1).join(' '); + for (i = 0; i < _ctx.depth; i += 1) { + indent += ' '; + } + + if ('group' === this.type) { + out += indent + '{\n'; + } else if ('cluster' === this.type) { + out += indent + 'subgraph ' + gvQuote('cluster_' + _ctx.count) + ' {\n'; + } else { + out += indent + this.type + ' ' + gvQuote(this.title) + ' {\n'; + } + + section = ''; + if (Object.keys(this.styles.all).length) { + Y.Object.each(this.styles.all, function(v, k) { + section += indent + ' ' + k + '=' + gvQuote(v) + ';\n'; + }); + } + if (Object.keys(this.styles.node).length) { + section += indent + ' node' + gvStyle(this.styles.node) + ';\n'; + } + if (Object.keys(this.styles.edge).length) { + section += indent + ' edge' + gvStyle(this.styles.edge) + ';\n'; + } + if (Object.keys(this.styles.graph).length) { + section += indent + ' graph' + gvStyle(this.styles.graph) + ';\n'; + } + if (section) { + out += indent + ' // defaults\n'; + out += section; + out += '\n'; + } + + section = ''; + this._filter(this._nodes, filters.node, function(key, val) { + section += indent + ' ' + gvQuote(val.name) + gvStyle(val.style) + ';\n'; + }); + if (section) { + out += indent + ' // nodes\n'; + out += section; + out += '\n'; + } + + section = ''; + this._filter(this._edges, filters.edge, function(key, val) { + section += indent + ' ' + gvQuote(val.tail) + + ' ' + (val.directed ? '->' : '--') + ' ' + + gvQuote(val.head) + gvStyle(val.style) + ';\n'; + }); + if (section) { + out += indent + ' // edges\n'; + out += section; + out += '\n'; + } + + section = ''; + this._filter(this._subgraphs, filters.subgraph, function(key, val) { + var c = { depth: _ctx.depth + 1, count: _ctx.count }; + section += val.render(filters, c); + _ctx.count = c.count; + }); + if (section) { + out += indent + ' // subgraphs\n'; + out += section; + out += '\n'; + } + + section = ''; + Y.Object.each(this.style, function(v, k) { + section += indent + ' ' + k + '=' + gvQuote(v) + ';\n'; + }); + if (section) { + out += indent + ' // this graph\n'; + out += section; + } + out += indent + '};\n'; + return out; + }, + + // generic recursive find algorithm + _find: function(source, target, cb) { + var id, subgraph; + if (source) { + for (id in this[source]) { + if (this[source].hasOwnProperty(id) && id === target) { + cb(this, this[source][id]); + return true; + } + } + } + for (id in this._subgraphs) { + if (this._subgraphs.hasOwnProperty(id)) { + subgraph = this._subgraphs[id]; + if (subgraph._find(source, target, cb)) { + return true; + } + } + } + return false; + }, + + // generic filter algorithm + _filter: function(obj, filter, cb) { + Y.Object.each(obj, function(val, key) { + if (filter && !filter(val)) { + return; + } + cb(key, val); + }); + }, + + // edges are a triple, yet we need a string key for hashes + _makeEdgeID: function(tail, head, directed) { + return [tail, directed, head].join(','); + } + +}; + + +// turn a list of resources into graph parts +function parseResources(graph, ress, options) { + var r, + res, + subgraph, + subgraphName, + tail, + tailName, + head, + headName, + edge, + rs, + reqs; + + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + + if (!res.yui || !res.yui.name) { + continue; + } + if ('yui-lang' === res.type && !options.lang) { + continue; + } + if ('binder' === res.type && !options.client) { + continue; + } + + // The idea here is that we -do- want to collect information about + // every module, so that we know which subgraph to draw it in. + // Later (during the render filters) we'll drop those that aren't + // of particular interest. + + tailName = res.yui.name; + + subgraphName = 'package ' + res.source.pkg.name + '@' + res.source.pkg.version; + if (res.mojit && 'shared' !== res.mojit) { + subgraphName = 'mojit ' + res.mojit; + } + + // create subgraph and node for -everything- (we'll filter later) + subgraph = graph.getSubgraph(subgraphName); + subgraph.type = 'cluster'; + subgraph.style.label = subgraphName; + + tail = graph.getNode(tailName); + graph.moveNodeToSubgraph(tailName, subgraphName); + // TODO: This might be handy to style different types differently. + //['type', 'subtype', 'affinity'].forEach(function(k) { + // tail.attrs[k] = res[k]; + //}); + tail.attrs.pkg = res.source.pkg; + + if ('mojito' === res.source.pkg.name && !options.framework) { + subgraph.attrs.sparse = true; + continue; + } + + reqs = res.yui.meta.requires || []; + for (rs = 0; rs < reqs.length; rs += 1) { + headName = reqs[rs]; + edge = graph.getEdge(tailName, headName, true); + tail.attrs.hasEdge = true; + head = graph.getNode(headName); + head.attrs.hasEdge = true; + } + } +} + + +// modify the graph to highlight traced node and nodes depending on it +function trace(graph, options) { + var doneNodes = {}, // name: true + todoNodes = [], + headName, + head, + e, + edge, + edges = {}; // headName: [ Edge ] + + // TODO: detect if options.trace doesn't exist in graph + todoNodes.push(options.trace); + + Y.Object.each(graph._edges, function(edge) { + if (!edges[edge.head]) { + edges[edge.head] = []; + } + edges[edge.head].push(edge); + }); + + while (todoNodes.length) { + headName = todoNodes.shift(); + if (doneNodes[headName]) { + continue; + } + head = graph.getNode(headName); + head.attrs.trace = true; + doneNodes[headName] = true; + + for (e = 0; e < edges[headName].length; e += 1) { + edge = edges[headName][e]; + edge.attrs.trace = true; + todoNodes.push(edge.tail); + } + } +} + + +run = function(params, options) { + var env, + store, + title, + graph, + ress, + m, + mojit, + mojits, + appConfigRes, + contents, + file; + + options = options || {}; + env = options.client ? 'client' : 'server'; + + if (params.length) { + libutils.error('Unknown extra parameters.'); + return; + } + + // make results dir + if (!libpath.existsSync(artifactsDir)) { + libfs.mkdirSync(artifactsDir, MODE_ALL); + } + if (!libpath.existsSync(resultsDir)) { + libfs.mkdirSync(resultsDir, MODE_ALL); + } + + Y.applyConfig({ + useSync: true, + modules: { + 'mojito-resource-store': { + fullpath: libpath.join(__dirname, '../../store.server.js') + } + } + }); + Y.use('mojito-resource-store'); + + // load details + store = new Y.mojito.ResourceStore({ + root: process.cwd(), + context: {} + }); + store.preload(); + + appConfigRes = store.getResources('server', {}, {id: 'config--application'})[0]; + title = appConfigRes.source.pkg.name + '@' + appConfigRes.source.pkg.version + ' ' + env; + graph = new Graph(title); + graph.type = 'digraph'; + + graph.styles.all.rankdir = 'LR'; + graph.styles.all.fontsize = '11'; + graph.styles.node.fontsize = '11'; + graph.styles.node.shape = 'rectangle'; + graph.styles.node.style = 'filled,rounded'; + graph.styles.node.fillcolor = 'white'; + graph.styles.edge.color = 'grey33'; + graph.styles.edge.arrowsize = '0.5'; + graph.styles.edge.fontsize = '8'; + graph.styles.graph.style = 'filled'; + graph.styles.graph.color = 'lightgrey'; + + graph.style.clusterrank = 'local'; // DOT -- special handling for clusters + graph.style.compound = 'true'; // allow edges between clusters (also requires edge[lhead,ltail]) +// graph.style.concentrate = 'true'; // SOMETIMES CRASHES -- reduce number of edges + graph.style.model = 'circuit'; // NEATO -- + graph.style.overlap = 'false'; // DOT -- +// graph.style.pack = 'true'; +// graph.style.packmode = 'clust'; +// graph.style.rankdir = 'LR'; // DOT -- direction of graph + graph.style.ranksep = '1.5'; // TWOPI,DOT -- space between ranks + graph.style.remincross = 'true'; // DOT -- if clusters, rerun cross minimization + graph.style.splines = 'polyline'; // how to draw edges + graph.style.start = 'self'; // FDP,NEATO -- + + ress = store.getResources(env, {}, {}); + parseResources(graph, ress, options); + + mojits = store.listAllMojits(); + mojits.push('shared'); + for (m = 0; m < mojits.length; m += 1) { + mojit = mojits[m]; + ress = store.getResources(env, {}, { mojit: mojit }); + parseResources(graph, ress, options); + } + + if (options.trace) { + trace(graph, options); + } + + // generate graph + contents = graph.render({ + node: function(node) { + // TODO: tweak each node style somehow + //node.style.label = node.name; + //if (node.attrs.affinity) { + // node.style.label += ' (' + node.attrs.affinity.affinity + ')'; + //} + if (node.attrs.trace) { + node.style.penwidth = 1.5; + node.style.color = '#CC0000'; + node.style.fontcolor = '#AA0000'; + node.style.fillcolor = '#FFDDDD'; + if (node.name === options.trace) { + node.style.peripheries = 2; + } + } + return true; + }, + + edge: function(edge) { + if (edge.attrs.trace) { + edge.style.penwidth = 1.1; + edge.style.color = '#AA4444'; + } + return true; + }, + + subgraph: function(subgraph) { + if (subgraph.attrs.sparse) { + var doomed = []; + Y.Object.each(subgraph._nodes, function(node) { + // always draw nodes found in the application + if ('mojito' === node.attrs.pkg.name && !node.attrs.hasEdge) { + doomed.push(node.name); + } + }); + Y.Array.each(doomed, function(name) { + delete subgraph._nodes[name]; + }); + if (!Object.keys(subgraph._nodes).length) { + return false; + } + } + return true; + } + }); + file = libpath.join(resultsDir, 'yui.' + env + '.dot'); + libfs.writeFileSync(file, contents, 'utf-8'); + + console.log('Dotfile generated.' + + ' To turn it into a graph, run the following:'); + console.log('$ dot -Tgif ' + file + ' > ' + + libpath.join(resultsDir, 'yui.' + env + '.gif')); +}; + + +/** + * Standard usage string export. + */ +exports.usage = 'mojito gv // generates a GraphViz[1] file' + + ' describing the dependencies\n' + + ' // between the YUI modules\n' + + '\n' + + 'OPTIONS:\n' + + '\t --client: use modules for the client\n' + + '\t -c: short for --client\n' + + '\t --framework: include framework (Mojito) modules\n' + + '\t -f: short for --framework\n' + + '\t --lang: also show language bundles\n' + + '\t -l: short for --lang\n' + + '\t --trace target: hightlight all modules leading to the target\n' + + '\t -t target: short for --trace\n' + + '\n' + + '[1] http://en.wikipedia.org/wiki/Graphviz\n'; + + +/** + * Standard options list export. + */ +exports.options = [ + { + longName: 'framework', + shortName: 'f', + hasValue: false + }, + { + longName: 'client', + shortName: 'c', + hasValue: false + }, + { + longName: 'lang', + shortName: 'l', + hasValue: false + }, + { + longName: 'trace', + shortName: 't', + hasValue: true + } +]; + + +/** + * Standard run method hook export. + */ +exports.run = run; + + diff --git a/source/lib/management/commands/help.js b/source/lib/app/commands/help.js similarity index 98% rename from source/lib/management/commands/help.js rename to source/lib/app/commands/help.js index e9bb56ef2..9d0289791 100644 --- a/source/lib/management/commands/help.js +++ b/source/lib/app/commands/help.js @@ -51,7 +51,7 @@ function helpCommand(command) { } catch (e) { try { help('./' + command); - } catch(e) { + } catch (ee) { console.log('No such command: ' + command); helpTop(); } diff --git a/source/lib/management/commands/info.js b/source/lib/app/commands/info.js similarity index 91% rename from source/lib/management/commands/info.js rename to source/lib/app/commands/info.js index f50920237..85ac6ac12 100644 --- a/source/lib/management/commands/info.js +++ b/source/lib/app/commands/info.js @@ -5,12 +5,12 @@ */ -/*jslint anon:true, sloppy:true*/ +/*jslint anon:true, nomen:true, sloppy:true*/ var fs = require('fs'), path = require('path'), - utils = require('../utils'), + utils = require(path.join(__dirname, '../../management/utils')), Y = require('yui').YUI({useSync: true}).use('json-parse', 'json-stringify'); Y.applyConfig({useSync: false}); diff --git a/source/lib/management/commands/jslint.js b/source/lib/app/commands/jslint.js similarity index 98% rename from source/lib/management/commands/jslint.js rename to source/lib/app/commands/jslint.js index 39dc3c8f0..feb46f460 100644 --- a/source/lib/management/commands/jslint.js +++ b/source/lib/app/commands/jslint.js @@ -10,7 +10,7 @@ var fs = require('fs'), path = require('path'), - utils = require('../utils'), + utils = require(path.join(__dirname, '../../management/utils')), usage = 'mojito jslint [app | mojit] [] {options}\n' + '\nOPTIONS: \n' + '\t --print : print results to stdout \n' + @@ -157,9 +157,10 @@ function OutputStdout(filename) { * and returning the number of errors encountered. */ function lintOneFile(infile, outfile) { - var jslint = require('../fulljslint').jslint, + var jslint = require('../../management/fulljslint').jslint, OPTS = { 'continue': true, // Tolerate continue + node: true, predef: [ // CommonJS 'exports', @@ -366,7 +367,7 @@ function processFiles(inDir, outDir, excludeMatcher) { if (!outDir) { out = new OutputStdout(relname); } else { - out = new OutputFile(path.join(outDir, outname)) + out = new OutputFile(path.join(outDir, outname)); } errors = lintOneFile(f, out); diff --git a/source/lib/management/commands/start.js b/source/lib/app/commands/start.js similarity index 95% rename from source/lib/management/commands/start.js rename to source/lib/app/commands/start.js index 61176c9fa..4040dc72b 100644 --- a/source/lib/management/commands/start.js +++ b/source/lib/app/commands/start.js @@ -5,11 +5,11 @@ */ -/*jslint anon:true, sloppy:true*/ +/*jslint anon:true, nomen:true, sloppy:true*/ var path = require('path'), - utils = require('../utils'), + utils = require(path.join(__dirname, '../../management/utils')), fs = require('fs'), Y = require('yui').YUI({useSync: true}).use('json-parse', 'json-stringify'); diff --git a/source/lib/management/commands/test.js b/source/lib/app/commands/test.js similarity index 95% rename from source/lib/management/commands/test.js rename to source/lib/app/commands/test.js index 9925bfd3d..75ce9930a 100644 --- a/source/lib/management/commands/test.js +++ b/source/lib/app/commands/test.js @@ -11,11 +11,11 @@ var pathlib = require('path'), fs = require('fs'), - utils = require('../utils'), - ymc = require('../yui-module-configurator'), + utils = require('../../management/utils'), + ymc = require('../../management/yui-module-configurator'), exec = require('child_process').exec, - copyExclude = require('../utils').copyExclude, - copyFile = require('../utils').copyFile, + copyExclude = utils.copyExclude, + copyFile = utils.copyFile, MODE_ALL = parseInt('777', 8), @@ -26,8 +26,6 @@ var pathlib = require('path'), fwTestsRoot = pathlib.join(targetMojitoPath, 'lib/tests'), - ResourceStore, - resultsDir = 'artifacts/test', resultsFile = pathlib.join(resultsDir, 'result.xml'), coverageDir = pathlib.join(resultsDir, 'coverage'), @@ -120,13 +118,12 @@ function collectRunResults(results) { } } -function configureYUI(YUI, store, load) { +function configureYUI(YUI, store) { YUI.applyConfig({ useSync: true, groups: { - 'mojito-fw': store.getYuiConfigFw('server', {}), - 'mojito-app': store.getYuiConfigApp('server', {}), - 'mojito-mojits': store.getYuiConfigAllMojits('server', {}) + 'mojito-shared': store.yui.getConfigShared('server', {}, false), + 'mojito-mojits': store.yui.getConfigAllMojits('server', {}) } }); } @@ -669,7 +666,8 @@ runTests = function(opts) { testModuleNames = ['mojito', 'mojito-test']; testRunner = function(testPath) { - var testConfigs, + var Ystore, + testConfigs, sourceConfigs; if (testType === 'mojit') { @@ -692,20 +690,32 @@ runTests = function(opts) { modules: testConfigs }); } else { - ResourceStore = require(pathlib.join(targetMojitoPath, - 'lib/store.server.js')); - store = new ResourceStore(testPath); + Ystore = YUI(); + Ystore.applyConfig({ + useSync: true, + modules: { + 'mojito-resource-store': { + fullpath: pathlib.join(targetMojitoPath, 'lib/store.server.js') + } + } + }); + Ystore.use('mojito-resource-store'); + store = new Ystore.mojito.ResourceStore({ + root: testPath, + context: {}, + appConfig: { env: 'test' } + }); - store.preload({}, { env: 'test' }); + store.preload(); - configureYUI(YUI, store, testModuleNames); + configureYUI(YUI, store); if (testType === 'fw') { - testConfigs = store.getYuiConfigApp('server', {}).modules; + testConfigs = store.yui.getConfigShared('server', {}, true).modules; } else if (testType === 'app') { testConfigs = merge( - store.getYuiConfigApp('server', {}).modules, - store.getYuiConfigAllMojits('server', {}).modules + store.yui.getConfigShared('server', {}, true).modules, + store.yui.getConfigAllMojits('server', {}).modules ); } } diff --git a/source/lib/management/commands/version.js b/source/lib/app/commands/version.js similarity index 96% rename from source/lib/management/commands/version.js rename to source/lib/app/commands/version.js index 8d2955048..4100b2978 100644 --- a/source/lib/management/commands/version.js +++ b/source/lib/app/commands/version.js @@ -10,7 +10,7 @@ var fs = require('fs'), path = require('path'), - utils = require('../utils'), + utils = require(path.join(__dirname, '../../management/utils')), usage = 'mojito version [app | mojit] []', Y = require('yui').YUI({useSync: true}).use('json-parse', 'json-stringify'); diff --git a/source/lib/app/middleware/mojito-handler-static.js b/source/lib/app/middleware/mojito-handler-static.js index b05fd144b..1d5051e6f 100644 --- a/source/lib/app/middleware/mojito-handler-static.js +++ b/source/lib/app/middleware/mojito-handler-static.js @@ -164,10 +164,11 @@ function clearCache(key) { */ function staticProvider(store, globalLogger) { logger = globalLogger; - var appConfig = store.getAppConfig(null, 'application'), + var appConfig = store.getStaticAppConfig(), options = appConfig.staticHandling || {}, cache = options.cache, - maxAge = options.maxAge; + maxAge = options.maxAge, + urls = store.getAllURLs(); if (cache && !maxAge) { maxAge = cache; @@ -197,10 +198,10 @@ function staticProvider(store, globalLogger) { // Use the resource store as a URI "rewriter" here. // /favicon.ico is sent to ./my_app_folder/assets/favicon.ico - filename = store.fileFromStaticHandlerURL(path); + filename = urls[path]; // TODO: [Issue 80] remove this for performance if ((!filename) && (path === '/favicon.ico')) { - filename = pa.join(store._root, 'assets', path); + filename = pa.join(store._config.root, 'assets', path); } if (!filename) { diff --git a/source/lib/app/middleware/mojito-handler-tunnel.js b/source/lib/app/middleware/mojito-handler-tunnel.js index c45a7d1b1..838b5c955 100644 --- a/source/lib/app/middleware/mojito-handler-tunnel.js +++ b/source/lib/app/middleware/mojito-handler-tunnel.js @@ -41,7 +41,7 @@ TunnelServer.prototype = { logger = globalLogger; //console.log('creating handle'); this._store = store; - config = store.getAppConfig({}, 'application'); + config = store.getAppConfig({}); this.tunnelPrefix = (config && config.tunnelPrefix) ? config.tunnelPrefix : '/tunnel'; diff --git a/source/lib/app/middleware/mojito-router.js b/source/lib/app/middleware/mojito-router.js index 904e80847..b949475f6 100644 --- a/source/lib/app/middleware/mojito-router.js +++ b/source/lib/app/middleware/mojito-router.js @@ -55,7 +55,7 @@ Router.prototype = { routes = store.getRoutes(context), routeMaker = new RouteMakerClass(routes), query = liburl.parse(req.url, true).query, - appConfig = store.getAppConfig(context, 'application'), + appConfig = store.getAppConfig(context), url, routeMatch; @@ -106,7 +106,7 @@ Router.prototype = { //and is never a string here. i.e. this assert always passes: //require('assert').ok(typeof routeMatch.param !== 'string'); command.params = { - route: simpleMerge(routeMatch.query, routeMatch.param), + route: simpleMerge(routeMatch.query, routeMatch.params), url: query || {}, body: req.body || {}, file: {} // FUTURE: add multi-part file data here diff --git a/source/lib/index.js b/source/lib/index.js index a2fba4ee6..95b3f2f72 100644 --- a/source/lib/index.js +++ b/source/lib/index.js @@ -34,7 +34,6 @@ var MOJITO_MIDDLEWARE = [ // TODO: [Issue 80] go back to connect? var express = require('express'), - ResourceStore = require('./store.server'), OutputHandler = require('./output-handler.server'), libpath = require('path'); @@ -47,26 +46,14 @@ global._mojito = {}; // This configures YUI with both the Mojito framework and all the // YUI modules in the application. -function configureYUI(YUI, store, load) { - var fw, - app, +function configureYUI(Y, store, load) { + var shared, module; - fw = store.getYuiConfigFw('server', {}); - app = store.getYuiConfigApp('server', {}); - YUI.applyConfig({ - groups: { - 'mojito-fw': fw, - 'mojito-app': app - } - }); - // also pre-load fw and app modules - for (module in fw.modules) { - if (fw.modules.hasOwnProperty(module)) { - load.push(module); - } - } - for (module in app.modules) { - if (app.modules.hasOwnProperty(module)) { + shared = store.yui.getConfigShared('server', {}, false); + Y.applyConfig(shared); + // also pre-load shared modules + for (module in shared.modules) { + if (shared.modules.hasOwnProperty(module)) { load.push(module); } } @@ -128,14 +115,51 @@ MojitoServer.prototype = { options.context = {}; } - store = new ResourceStore(options.dir); + // all logging that comes from YUI comes from here + // We need to do this early, since creating a Y instance appears to copy + // the function. + YUI.applyConfig({ logFn: function(msg, lvl, src) { + // translating YUI logs so they are categorized outside the rest + // of Mojito's log levels + var args = Array.prototype.slice.call(arguments); + if (!this.mojito || src === 'yui' || src === 'loader' || + src === 'get') { + if ((!logger) && (!logConfig.yui)) { + return; + } + args[1] = 'YUI-' + lvl.toUpperCase(); + } + if (logger) { + logger.log.apply(logger, args); + } else { + console.log(serverLog.options.formatter(msg, lvl, src, + new Date().getTime(), serverLog.options)); + } + }}); + + Y = YUI({ core: CORE_YUI_MODULES, useSync: true }); + + Y.applyConfig({ + modules: { + 'mojito-resource-store': { + fullpath: libpath.join(__dirname, 'store.server.js') + } + } + }); + Y.applyConfig({ useSync: true }); + Y.use('mojito-resource-store'); + store = new Y.mojito.ResourceStore({ + root: options.dir, + context: options.context, + appConfig: options.appConfig + }); // share the resource store as a property of the application instance // (useful for the Mojito CLI) app.store = store; - store.preload(options.context, options.appConfig); - appConfig = store.getAppConfig(null, 'application'); + store.preload(); + appConfig = store.getAppConfig(null); // TODO: extract function if (appConfig.log && appConfig.log.server) { @@ -152,30 +176,6 @@ MojitoServer.prototype = { } } - configureYUI(YUI, store, CORE_MOJITO_MODULES); - - // all logging that comes from YUI comes from here - // We need to do this early, since creating a Y instance appears to copy - // the function. - YUI.GlobalConfig.logFn = function(msg, lvl, src) { - // translating YUI logs so they are categorized outside the rest - // of Mojito's log levels - var args = Array.prototype.slice.call(arguments); - if (!this.mojito || src === 'yui' || src === 'loader' || - src === 'get') { - if ((!logger) && (!logConfig.yui)) { - return; - } - args[1] = 'YUI-' + lvl.toUpperCase(); - } - if (logger) { - logger.log.apply(logger, args); - } else { - console.log(serverLog.options.formatter(msg, lvl, src, - new Date().getTime(), serverLog.options)); - } - }; - // merge application log options over top defaults Object.keys(logConfig).forEach(function(k) { if (logConfig[k] !== undefined) { @@ -183,15 +183,16 @@ MojitoServer.prototype = { } }); - Y = YUI({ core: CORE_YUI_MODULES, useSync: true }); + configureYUI(Y, store, CORE_MOJITO_MODULES); // Load logger early so that we can plug it in before the other loading // happens. + Y.applyConfig({ useSync: true }); Y.use('mojito-logger'); // TODO: extract function logger = new Y.mojito.Logger(serverLog.options); - store.setLogger(logger); + Y.applyConfig({ useSync: true }); Y.use.apply(Y, CORE_MOJITO_MODULES); Y.applyConfig({ useSync: false }); diff --git a/source/lib/libs/ycb.js b/source/lib/libs/ycb.js index 525af1843..7e1f563ae 100644 --- a/source/lib/libs/ycb.js +++ b/source/lib/libs/ycb.js @@ -5,146 +5,230 @@ */ -module.exports = { +/*jslint anon:true, sloppy:true, nomen:true*/ - version: '2.0.0', - DEFAULT: '*', - SEPARATOR: '/', - SUBMATCH: /\$\$[a-zA-Z0-9.-_]*\$\$/, +var VERSION = '2.0.0', + DEFAULT = '*', + SEPARATOR = '/', + SUBMATCH = /\$\$[a-zA-Z0-9.-_]*\$\$/, + Y = require('yui').YUI({useSync: true}).use('json-parse', 'json-stringify', 'oop'); - /* - * Processes an Object representing a YCB 2.0 Bundle as defined in the spec. - * - * - * @method read - * @param {object} bundle - * @param {object} context - * @param {boolean} validate - * @param {boolean} debug - * @return {object} +Y.applyConfig({useSync: false}); + + +//--------------------------------------------------------------- +// OBJECT ORIENTED INTERFACE + +function Ycb(bundle, options) { + this.options = options || {}; + this.dimensions = {}; + this.settings = {}; + this.schema = {}; + this.dimsUsed = {}; // dim name: value: true + this._processRawBundle(bundle, this.options); +} +Ycb.prototype = { + + + /** + * Returns the dimensions in the YCB file. + * @method getDimensions + * @return {object} the dimensions */ - read: function(bundle, context, validate, debug){ + getDimensions: function() { + return Y.clone(this.dimensions, true); + }, - var rawConfig, lookupPaths, - path, config = {}; - if(!context){ - context = {}; + /** + * Iterates over all the setting sections in the YCB file, calling the + * callback for each section. + * @method walkSettings + * @param callback {function(settings, config)} + * @param callback.settings {object} the condition under which section will be used + * @param callback.config {object} the configuration in the section + * @param callback.return {boolean} if the callback returns false, then walking is stopped + * @return {nothing} results returned via callback + */ + walkSettings: function(callback) { + var path, + context; + for (path in this.settings) { + if (this.settings.hasOwnProperty(path)) { + context = this._getContextFromLookupPath(path); + // clone, so that noone mutates us + if (!callback(context, Y.clone(this.settings[path], true))) { + break; + } + } } + }, - rawConfig = this._processRawBundle(bundle); - lookupPaths = this._getLookupPaths(rawConfig.dimensions, context); - if(debug){ - console.log(Y.JSON.stringify(context,null,4)); - console.log(Y.JSON.stringify(rawConfig,null,4)); - console.log(Y.JSON.stringify(lookupPaths,null,4)); + /** + * Read the file. + * @method read + * @param context {object} + * @param options {object} + * @return {object} + */ + read: function(context, options) { + var lookupPaths, + path, + config = {}; + + context = context || {}; + options = objectMerge(this.options, options || {}); + + lookupPaths = this._getLookupPaths(context, options); + + if (options.debug) { + console.log(Y.JSON.stringify(context, null, 4)); + console.log(Y.JSON.stringify(this.dimensions, null, 4)); + console.log(Y.JSON.stringify(this.settings, null, 4)); + console.log(Y.JSON.stringify(this.schema, null, 4)); + console.log(Y.JSON.stringify(lookupPaths, null, 4)); } // Now we simply merge each macting settings section we find into the config - for(path=0; path combination[location].total){ - + if (combination[location].current > combination[location].total) { combination[location].current = 0; - return tumble(combination, location-1); } return true; } - do{ + do { path = []; - - for(pos=0; pos [] []'); diff --git a/source/lib/management/colors.js b/source/lib/management/colors.js index cf981ce49..f780d1ca4 100644 --- a/source/lib/management/colors.js +++ b/source/lib/management/colors.js @@ -54,32 +54,42 @@ function stylize(str, style) { (['bold', 'underline', 'italic', 'inverse', 'grey', 'yellow', 'red', 'green', 'blue', 'white', 'cyan', 'magenta']).forEach( function(style) { - Object.defineProperty(String.prototype, style, { - get: function() { - return stylize(this, style); - } - }); + try { + Object.defineProperty(String.prototype, style, { + get: function() { + return stylize(this, style); + } + }); + } catch (e) { + // just ignore + } } ); // prototypes string with method "rainbow" // rainbow will apply a the color spectrum to a string, changing colors every // letter -Object.defineProperty(String.prototype, 'rainbow', { - get: function() { - //RoY G BiV - var rainbowcolors = ['red', 'yellow', 'green', 'blue', 'magenta'], - exploded = this.split(''), - i = 0; +try { + Object.defineProperty(String.prototype, 'rainbow', { + get: function() { + //RoY G BiV + var rainbowcolors = ['red', 'yellow', 'green', 'blue', 'magenta'], + exploded = this.split(''), + i = 0; + + exploded = exploded.map(function(letter) { + if (letter === ' ') { + return letter; + } else { + return stylize(letter, rainbowcolors[(i += 1) % + rainbowcolors.length]); + } + }); + return exploded.join(''); + } + }); +} catch (e) { + // just ignore +} + - exploded = exploded.map(function(letter) { - if (letter === ' ') { - return letter; - } else { - return stylize(letter, rainbowcolors[(i += 1) % - rainbowcolors.length]); - } - }); - return exploded.join(''); - } -}); diff --git a/source/lib/management/commands/gv.js b/source/lib/management/commands/gv.js deleted file mode 100644 index 72dd95b28..000000000 --- a/source/lib/management/commands/gv.js +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (c) 2011-2012, Yahoo! Inc. All rights reserved. - * Copyrights licensed under the New BSD License. - * See the accompanying LICENSE file for terms. - */ - - -/*jslint anon:true, sloppy:true*/ - - -// TODO: -// color-code or shape-code module types (from yui, mojito fw, app-level, -// mojits) use store.getAppLevelYuiModules() to de-stress (or not draw) -// modules that aren't used by the app. - -var run, - usage, - options, - - libpath = require('path'), - libfs = require('fs'), - libutils = require('../utils'), - - MODE_ALL = parseInt('777', 8), - - ResourceStore = require('../../store.server.js'), - - artifactsDir = 'artifacts', - resultsDir = 'artifacts/gv'; - - -function parseReqs(dest, modules) { - var module, - info; - - for (module in modules) { - if (modules.hasOwnProperty(module)) { - info = modules[module]; - dest[module] = info.requires || []; - } - } -} - - -function makeDepGraph(reqs, destFile) { - var graph, - mod, - i, - req, - graphAttrs; - - graph = 'digraph yui {\n'; - graph += ' rankdir="LR";\n'; - graph += ' fontsize=11;\n'; - graph += ' node [shape=Mrecord,fontsize=11];\n'; - graph += ' edge [color=grey33,arrowsize=0.5,fontsize=8];\n'; - - for (mod in reqs) { - if (reqs.hasOwnProperty(mod)) { - for (i = 0; i < reqs[mod].length; i += 1) { - req = reqs[mod][i]; - graph += ' "' + mod + '" -> "' + req + '";\n'; - } - } - } - - graphAttrs = [ - 'remincross=true', - // 'rankdir=LR', - 'ranksep=1.5', - 'clusterrank=local', - 'model=circuit', - 'overlap=false', - 'splines=compound', - // 'pack=true', - // 'packmode=clust', - // 'concentrate=true', // sometimes causes crash/coredump - // 'start=self', - 'compound=true' - ]; - graph += ' graph [' + graphAttrs.join(',') + '];\n'; - graph += '}\n'; - - libfs.writeFileSync(destFile, graph, 'utf-8'); -} - - -run = function(params, options) { - var env, store, - reqs = {}, - resultsFile; - - options = options || {}; - env = options.client ? 'client' : 'server'; - - if (params.length) { - libutils.error('Unknown extra parameters.'); - return; - } - - // make results dir - if (!libpath.existsSync(artifactsDir)) { - libfs.mkdirSync(artifactsDir, MODE_ALL); - } - if (!libpath.existsSync(resultsDir)) { - libfs.mkdirSync(resultsDir, MODE_ALL); - } - - // load details - store = new ResourceStore(process.cwd()); - store.preload(); - if (options.framework) { - parseReqs(reqs, store.getYuiConfigFw(env, {}).modules); - } - parseReqs(reqs, store.getYuiConfigApp(env, {}).modules); - parseReqs(reqs, store.getYuiConfigAllMojits(env, {}).modules); - - // generate graph - resultsFile = libpath.join(resultsDir, 'yui.' + env + '.dot'); - makeDepGraph(reqs, resultsFile); - - console.log('Dotfile generated.' + - ' To turn it into a graph, run the following:'); - console.log('$ dot -Tgif ' + resultsFile + ' > ' + - libpath.join(resultsDir, 'yui.' + env + '.gif')); -}; - - -usage = 'mojito gv // generates a GraphViz[1] file' + - ' describing the dependencies\n' + - ' // between the YUI modules\n' + - '\n' + - 'OPTIONS:\n' + - '\t --client: use modules for the client\n' + - '\t -c: short for --client\n' + - '\t --framework: include framework (Mojito) modules\n' + - '\t -f: short for --framework\n' + - '\n' + - '[1] http://en.wikipedia.org/wiki/Graphviz\n'; - - -options = [ - { - longName: 'framework', - shortName: 'f', - hasValue: false - }, - { - longName: 'client', - shortName: 'c', - hasValue: false - } -]; - - -/** - * Standard usage string export. - */ -exports.usage = usage; - - -/** - * Standard options list export. - */ -exports.options = options; - - -/** - * Standard run method hook export. - */ -exports.run = run; diff --git a/source/lib/management/utils.js b/source/lib/management/utils.js index 7490df727..49544c3f9 100644 --- a/source/lib/management/utils.js +++ b/source/lib/management/utils.js @@ -15,7 +15,7 @@ var fs = require('fs'), http = require('http'), tty = require('tty'), mojito = require('../index.js'), - archetypes_dir = path.join(__dirname, '/../archetypes'), + archetypes_dir = path.join(__dirname, '../app/archetypes'), isatty = tty.isatty(1) && tty.isatty(2); @@ -23,11 +23,15 @@ if (!isatty) { // fake out the getters that the "color" library would have added (['bold', 'underline', 'italic', 'inverse', 'grey', 'yellow', 'red', 'green', 'blue', 'white', 'cyan', 'magenta']).forEach(function(style) { - Object.defineProperty(String.prototype, style, { - get: function() { - return this; + try { + Object.defineProperty(String.prototype, style, { + get: function() { + return this; + } + }); + } catch (e) { + // just ignore } - }); }); } else { require('./colors'); @@ -74,6 +78,30 @@ function heir(o) { } +/** + * Decodes XML entities in the string. + * Only decodes a subset of named entities. + * @method decodeHTMLEntities + * @param {string} txt String to decode + * @return {string} input string with with XML entities decoded + */ +// TODO: find a node module that can do this well +function decodeHTMLEntities(txt) { + txt = txt.replace(/(&[^;]+;)/g, function(all, ent) { + if ('&#x' === ent.substr(0, 3)) { + return String.fromCharCode(parseInt(ent.substring(3, ent.length - 1), 16)); + } + return ent; + }); + txt = txt.replace(/</g, '<'); + txt = txt.replace(/>/g, '>'); + txt = txt.replace(/"/g, '"'); + txt = txt.replace(/'/g, "'"); + txt = txt.replace(/&/g, '&'); + return txt; +} + + function process_template(archetype_path, file, mojit_dir, template) { var archetype_file = path.join(archetype_path, file), @@ -652,3 +680,6 @@ exports.heir = heir; /** */ exports.contextCsvToObject = contextCsvToObject; + +exports.decodeHTMLEntities = decodeHTMLEntities; + diff --git a/source/lib/store.server.js b/source/lib/store.server.js index d2c737a0b..7f1a088a5 100644 --- a/source/lib/store.server.js +++ b/source/lib/store.server.js @@ -4,1121 +4,656 @@ * See the accompanying LICENSE file for terms. */ + /*jslint anon:true, sloppy:true, regexp: true, continue: true, nomen:true, node:true */ - -var libfs = require('fs'), - libglob = require('glob'), - libpath = require('path'), - libqs = require('querystring'), - libvm = require('vm'), - libwalker = require('./package-walker.server'), - libycb = require('./libs/ycb'), - - isPositiveInt = /^[0-9]+$/, - isSpaceOpencurly = / \{/g, - isSpaceClosecurly = / \}/g, - isNotAlphaNum = /[^a-zA-Z0-9]/, - - // fallback logger - // This is the logger that is used until setLogger() is called. - logger = { - log: function(msg, lvl, who) { - var log = console.log; - - if (lvl === 'warn' || lvl === 'error') { - // console.warn provides unbuffered output and avoids message - // truncation when process.exit() is called after logging - log = console.warn; - } - log('[' + lvl + '] ' + who + ': ' + msg); - } - }, - - // nodejs-yui3 has global state about which modules are loaded. Use - // multiple require()'d instances as a wall to prevent cross-contamination - // when using loader for dependency calculations. - utilYUI = require('yui').YUI, - serverYUI = require('yui').YUI, - clientYUI = require('yui').YUI, - - Y = utilYUI({ useSync: true }).use('intl', 'json-parse', 'json-stringify'), - - mojitoRoot = __dirname, - - NAME = 'MojitoServer'; - -// TODO: move string constants here. - - -// The Affinity object is to manage the use of the affinity string in -// filenames. Some files have affinities that have multiple parts -// (e.g. "server-tests"). -function Affinity(affinity) { - this._init(affinity); -} - - -Affinity.prototype = { - _init: function(aff) { - var parts; - - if (aff.indexOf('-') === -1) { - this.affinity = aff; - } else { - parts = aff.split('-'); - this.affinity = parts[0]; - this.type = parts[1]; - } - }, - toString: function() { - return this.affinity; - } -}; - - - /** - * The Resource Store manages information about the "resources" in a Mojito + * The ResourceStore manages information about the "resources" in a Mojito * application. These resources are things that have representation on the * filesystem. * + * You generally don't need to worry about this class (and its addons) unless + * you are extending Mojito. + * * Each resource can have many different versions. This is not talking about * revisions, which is how the resource changes over time. It is instead * talking about how there can be a version of the resource just for iphones, * one just for android, a fallback, etc. * - * There are various types of resources: - *
- *   config      -- a piece of configuration, sometimes for another resource
- *   controller  -- the controller for a mojit
- *   model       -- a model for a mojit
- *   view        -- a view for a mojit
- *   binder      -- a binder for a mojit
- *   action      -- an action to augment the controller
- *   asset       -- an asset (css, js, image, etc)
- *   addon       -- an addon to the mojito system
- *   spec        -- the configuration for a mojit instance
- *   yui-lang    -- a YUI3 language bundle
- *   yui-module  -- a YUI3 module (that isn't one of the above)
- * 
- * - * The metadata kept about each resource is "normalized" to the follow keys: - * (not all resources will have all keys) - * (some types will have additional keys) - * - *
- *   - id
- *       context-insensitive ID of the resource
- *       said another way, all versions of a resource have the same ID
- *
- *   - type
- *       see above
- *
- *   - fsPath
- *       the path on the filesystem
- *
- *   - staticHandlerURL
- *       the URL that will cause the asset handler to serve the resource
- *       for resources that can be deployed by reference to the client
- *
- *   - name
- *       specific to type
- *
- *   - configType
- *       for type=config
- *       the type of the configuration
- *
- *   - viewEngine
- *       for type=view
- *       `mu`, `dust`, etc
+ * The metadata kept about each resource is normalized to the follow keys:
+ * 
+ *
source (object)
+ *
where the source came from. (not shipped to the client.) + *
+ *
fs (object)
+ *
filesystem details
+ *
pkg (object)
+ *
packaging details
+ *
+ *
+ *
mojit (string)
+ *
which mojit this applies to, if any. ("shared" means the resource is available to all mojits.)
+ *
type (string)
+ *
resource type
+ *
subtype (string)
+ *
not all types of subtypes
+ *
name (string)
+ *
common to all versions of the resource
+ *
id (string)
+ *
unique ID. common to all versions of the resource. (typically {type}-{subtype}-{name}.)
+ *
yui (object)
+ *
for resources that are YUI modules
+ *
* - * - viewOutputFormat - * for type=view - * output format that the view will generate - * `xml`, `html`, etc + * The following are only used in the metadata for each resource version + * (The metadata for resolved resources won't have these, since they're intrinsically + * part of the resolved resource.) + *
+ *
affinity (string)
+ *
runtime affinity. either server, client, or common
+ *
selector (string)
+ *
version selector
+ * * - * - assetType - * for type=asset - * `css`, `js`, `png`, `swf`, etc - * - * - addonType - * for type=addon - * the mojito subsystem to which the addon should be added - * - * - yuiModuleName - * for any resource delivered as a YUI3 module - * the YUI3 module name - * - * - yuiModuleVersion - * for any resource delivered as a YUI3 module - * the YUI3 module version - * - * - yuiModuleMeta - * for any resource delivered as a YUI3 module - * the YUI3 module metadata - * (requires, langs, etc) - * - * - yuiSortedPaths - * for any resource delivered as a YUI3 module - * a list of YUI modules required by the module, - * with transitive dependencies resolved - * format: { yui-module-name: URL-to-load-yui-module } - *
- * - * @module MojitoServer - * @class ResourceStore.server - * @constructor - * @param root {string} directory where application is found - * @param libs {object} dependent libraries -- this param is mainly used - * during unit testing + * @module ResourceStore */ -function ServerStore(root, libs) { - //logger.log('ctor(' + root + ')'); - this._root = root; - this._version = '0.1.0'; - this._shortRoot = libpath.basename(root); - this._libs = libs || {}; - - // the static version of the appConfig (application.json) - // It's "static" because it's determined at server-start time, and doesn't - // change after that. - this._appConfigStatic = null; - - // full paths to routes files, to aid detection - this._preload_routes = {}; - - // Each of these is a complex datastructure, the first key of which is the - // resource ID ("resid"). (For mojitMeta, the first key is the mojit type - // and the second key is resid.) - // Under resid the next key is the affinity ("client", "server", or - // "common".) - // Under affinity is a datastructure that tracks all versions of the resource. - // There is a special key "contexts" which lists all the contexts that the - // resource has been specialized for. "contexts" is an object. The key is a - // string that identifies the context, and the value is a partial context that - // describes the specialization. (An example might be "device=iphone" for - // the key and { device:'iphone' } for the value.) - // The rest of the keys are the context strings (as found in "contexts"), and - // the values are the metadata about the resource versions. - // [resid][affinity].contexts - // [resid][affinity][ctxKey] = { metadata } - // (These are mostly populated by the _preloadResource() method.) - this._preload = { - appMeta: {}, // application-level - sharedMeta: {}, // shared between each mojit - mojitMeta: {} // individual to each mojit - }; - - // These are similar to the _preload above, except the affinity has been resolved - // down for each environment ("client" or "server"). Also, the ctxKey has been - // moved above resid to optimize lookup during runtime. - this._appMetaNC = {}; // [resid] = { parts } - this._appMeta = {}; // [env][ctxKey][resid] = { parts } - this._sharedMeta = {}; // [env][ctxKey][resid] = { parts } - this._mojitMeta = {}; // [env][type][ctxKey][resid] = { parts } - this._mojitYuiRequired = {}; // [env][type][ctxKey] = [ YUI module names ] - this._mojitYuiSorted = {}; // [env][type][ctxKey] = [ YUI module names ] - this._mojitYuiSortedPaths = {}; // [env][type][ctxKey][module] = path - - this._jsonCache = {}; // fullpath: contents as JSON object - this._ycbCache = {}; // fullpath: context: YCB config object - - this._staticURLs = {}; // url => fullpath - this._dynamicURLs = {}; // url => dynamic content - this._mojitAssetRoots = {}; // mojitType => URL prefix - this._mojitLangs = {}; // mojitType => [en-US,en-GB,en] - this._mojitPaths = {}; // mojitType => filesystem directory of mojit - - this._expandInstanceCache = { // [env][cacheKey] = instance - client: {}, - server: {} - }; +YUI.add('mojito-resource-store', function(Y, NAME) { + + var libs = {}, + + isNotAlphaNum = /[^a-zA-Z0-9]/, + + mojitoRoot = __dirname, + mojitoVersion = '0.666.666', // special case for weird packaging situations + + CONVENTION_SUBDIR_TYPES = { + // subdir: resource type + 'actions': 'action', + 'binders': 'binder', + 'commands': 'command', + 'middleware': 'middleware', + 'models': 'model', + 'specs': 'spec', + 'views': 'view' + }, + CONVENTION_SUBDIR_TYPE_IS_JS = { + 'action': true, + 'binder': true, + 'model': true + }, + // which addon subtypes are app-level + ADDON_SUBTYPES_APPLEVEL = { + 'rs': true + }, + DEFAULT_AFFINITIES = { + 'action': 'server', + 'addon': 'server', + 'archetype': 'server', + 'asset': 'common', + 'binder': 'common', // need to be common so that binders meta-bubble + 'command': 'server', + 'controller': 'server', + 'middleware': 'server', + 'model': 'server', + 'spec': 'common', + 'view': 'common' + }; - // TODO: bake this into the refactoring work. - // this stuff is mainly so that we can send mocks during testing - if (!this._libs.fs) { - this._libs.fs = libfs; - } - if (!this._libs.path) { - this._libs.path = libpath; - } - if (libycb && (!this._libs.ycb)) { - this._libs.ycb = libycb; - } -} + libs.fs = require('fs'); + libs.glob = require('glob'); + libs.path = require('path'); + libs.semver = require('semver'); + libs.walker = require('./package-walker.server'); -ServerStore.prototype = { + // The Affinity object is to manage the use of the affinity string in + // filenames. Some files have affinities that have multiple parts + // (e.g. "server-tests"). + function Affinity(affinity) { + var parts; + if (affinity.indexOf('-') === -1) { + this.affinity = affinity; + } else { + parts = affinity.split('-'); + this.affinity = parts[0]; + this.type = parts[1]; + } + } + Affinity.prototype = { + toString: function() { + return this.affinity; + } + }; - // =========================================== - // ================== PUBLIC ================= /** - * Preloads everything in the app, and as well pertinent parts of - * the framework. - * - * @method preload - * @param {object} appContext the base context for reading configuration. - * @param {object} appConfig overrides for the app config. - * @return {nothing} + * @class ResourceStore.server + * @constructor + * @requires addon-rs-config, addon-rs-selector + * @param {object} config configuration for the store + * @param {string} config.root directory to manage (usually the application directory) + * @param {object} config.context static context + * @param {object} config.appConfig overrides for `application.json` */ - preload: function(appContext, appConfig) { - //logger.log('preload()'); - var type, - ctxKey, - resid, - res, - n; - - if (!this._preload) { - // we've already been preloaded - // This situation mainly happens in the commandline scripts. - return; - } + function ResourceStore(config) { + ResourceStore.superclass.constructor.apply(this, arguments); + } + ResourceStore.NAME = 'ResourceStore'; + ResourceStore.ATTRS = {}; - this._fwConfig = this._readConfigJSON(libpath.join(mojitoRoot, - 'config.json')); - this._ycbDims = this._readYcbDimensions(); - this._validYCBDims = this._precalcValidYCBDimensions( - this._ycbDims[0].dimensions - ); - this._defaultYCBContext = appContext || {}; - - // need to read the statically configured appConfig now so that values - // are available during generation of the static URLs - this._appConfigStatic = this._readAppConfigStatic(); - - // generates URL's about each spec in application.json - this._urlsForAppSpecs(); - - // merge app configuration overrides - if (appConfig) { - for (n in appConfig) { - if (appConfig.hasOwnProperty(n)) { - logger.log('overriding application config value: ' + n, - 'warn'); - this._appConfigStatic[n] = appConfig[n]; + + Y.extend(ResourceStore, Y.Base, { + + /** + * This methods is part of Y.Base. See documentation for that for details. + * @method initializer + * @param {object} cfg Configuration object as per Y.Base + * @return {nothing} + */ + initializer: function(cfg) { + var i; + + this._config = cfg; + this._config.context = this._config.context || {}; + this._config.appConfig = this._config.appConfig || {}; + this._config.mojitoRoot = this._config.mojitoRoot || mojitoRoot; + this._jsonCache = {}; // fullPath: contents as JSON object + this._ycbCache = {}; // fullPath: context: YCB config object + + this._libs = {}; + for (i in libs) { + if (libs.hasOwnProperty(i)) { + this._libs[i] = libs[i]; + } + } + + this._appRVs = []; // array of resource versions + this._mojitRVs = {}; // mojitType: array of resource versions + this._appResources = {}; // env: posl: array of resources + this._mojitResources = {}; // env: posl: mojitType: array of resources + this._expandInstanceCache = { // env: cacheKey: instance + client: {}, + server: {} + }; + + /** + * All selectors that are actually in the app. + * Key is selector, value is just boolean `true`. + * This won't be populated until `preloadResourceVersions()` is done. + * @property selectors + * @type Object + */ + this.selectors = {}; + + // Y.Plugin AOP doesn't allow afterHostMethod() callbacks to + // modify the results, so we fire an event instead. + this.publish('getMojitTypeDetails', {emitFacade: true, preventable: false}); + this.publish('mojitResourcesResolved', {emitFacade: true, preventable: false}); + + // We'll start with just our "config" addon. + this._yuiUseSync({ + 'addon-rs-config': { + fullpath: this._libs.path.join(__dirname, 'app/addons/rs/config.server.js') } - } - } + }); + this.plug(Y.mojito.addons.rs.config, { appRoot: this._config.root, mojitoRoot: this._config.mojitoRoot }); - // generates metadata about each resource - this._preloadMeta(); + this._validDims = this._parseValidDims(this.config.getDimensions()); + this.validateContext(this._config.context); + this._fwConfig = this.config.readConfigJSON(this._libs.path.join(this._config.mojitoRoot, 'config.json')); + this._appConfigStatic = this.getAppConfig({}); + }, + destructor: function() {}, - // takes the preloaded info about each resource and resolves - // version priority (.server. more specific that .common. etc) - this._cookdown(); - // preread configs - this._prereadConfigs(this._appMeta.server); - for (type in this._mojitMeta.server) { - if (this._mojitMeta.server.hasOwnProperty(type)) { - this._prereadConfigs(this._mojitMeta.server[type]); - } - } + //==================================================================== + // PUBLIC METHODS - // We need to run this function even for "ondemand", since it calculates - // additional implied dependencies, such as a binder on MojitoClient, - // or a controller on the view-engine needed to render its views. - this._precalcYuiDependencies(); - - // binders are client-side-only resources, yet we need to know about - // them when talking about the 'server' environment - for (type in this._mojitMeta.client) { - if (this._mojitMeta.client.hasOwnProperty(type)) { - - for (ctxKey in this._mojitMeta.client[type]) { - if (this._mojitMeta.client[type].hasOwnProperty(ctxKey)) { - - for (resid in this._mojitMeta.client[type][ctxKey]) { - if (this._mojitMeta.client[type][ctxKey]. - hasOwnProperty(resid)) { - res = this._mojitMeta.client[type - ][ctxKey][resid]; - if (res.type !== 'binder') { - continue; - } - this._mojitMeta.server[type - ][ctxKey][resid] = res; + + /** + * Validates the context, and throws an exception if it isn't. + * @method validateContext + * @param {object} ctx the context + * @return {nothing} if this method returns at all then the context is valid + */ + validateContext: function(ctx) { + var k, + parts, + p, + test, + found; + for (k in ctx) { + if (ctx.hasOwnProperty(k)) { + if (!ctx[k]) { + continue; + } + if ('langs' === k) { + // pseudo-context variable created by our middleware + continue; + } + if (!this._validDims[k]) { + throw new Error('INVALID dimension key "' + k + '"'); + } + // we need to support language fallbacks + if ('lang' === k) { + found = false; + parts = ctx[k].split('-'); + for (p = parts.length; p > 0; p -= 1) { + test = parts.slice(0, p).join('-'); + if (this._validDims[k][test]) { + found = true; + break; } } + if (!found) { + throw new Error('INVALID dimension value "' + ctx[k] + '" for key "' + k + '"'); + } + continue; + } + if (!this._validDims[k][ctx[k]]) { + throw new Error('INVALID dimension value "' + ctx[k] + '" for key "' + k + '"'); } } } - } - }, - + return true; + }, - /** - * Sets the logger object. - * - * @method setLogger - * @param l {object} object containing a log(message,level,source) function - * @return {nothing} - */ - setLogger: function(l) { - logger = l; - }, + /** + * Returns the static (non-runtime-sensitive) context + * @method getStaticContext + * @return {object} the context + */ + getStaticContext: function() { + return Y.clone(this._config.context, true); + }, - /** - * Returns, via callback, the fully expanded mojit instance specification. - * - * @method getSpec - * @param env {string} either "client" or "server" - * @param id {string} the ID of the spec to return - * @param context {object} the runtime context for the spec - * @param callback {function(err,spec)} callback used to return the results (or error) - * @return {nothing} results returned via the callback parameter - */ - getSpec: function(env, id, context, callback) { - this.expandInstanceForEnv(env, {base: id}, context, function(err, obj) { - if (err) { - callback(err); - return; - } + /** + * Returns the static (non-runtime-sensitive) version of the application.json. + * @method getStaticAppConfig + * @return {object} the configuration from applications.json + */ + getStaticAppConfig: function() { + return Y.clone(this._appConfigStatic, true); + }, - if (env === 'client' && obj) { - delete obj.assets; - } - callback(null, obj); - }); - }, + /** + * Returns Mojito's built-in configuration. + * @method getFrameworkConfig + * @return {object} the configuration for mojito + */ + getFrameworkConfig: function() { + return Y.clone(this._fwConfig, true); + }, - /** - * Returns, via callback, the details of the mojit type. - * - * @method getType - * @param env {string} either "client" or "server" - * @param type {string} the mojit type - * @param context {object} the runtime context for the spec - * @param callback {function(err,spec)} callback used to return the results (or error) - * @return {nothing} results returned via the callback parameter - */ - getType: function(env, type, context, callback) { + /** + * Returns a contextualized application configuration. + * @method getAppConfig + * @param {object} ctx the context + * @return {object} the application configuration contextualized by the "ctx" argument. + */ + getAppConfig: function(ctx) { + var appConfig, + ycb; - this.expandInstanceForEnv(env, {type: type}, context, function(err, - obj) { - if (err) { - callback(err); - return; - } + this.validateContext(ctx); - if (env === 'client' && obj) { - delete obj.assets; + if (this._appConfigStatic && (!ctx || !Object.keys(ctx).length)) { + return Y.clone(this._appConfigStatic, true); } - callback(null, obj); - }); - }, - - /** - * This just calls expandInstanceForEnv() with `env` set to `server`. - * - * @method expandInstance - * @param instance {map} Partial instance to expand. - * @param ctx {object} The request context. - * @param cb {function(err,instance)} callback used to return the results (or error) - * @return {nothing} results returned via the callback parameter - */ - expandInstance: function(instance, ctx, cb) { - this.expandInstanceForEnv('server', instance, ctx, cb); - return; - }, - - - /** - * This method takes a partial instance and expands it to all details needed - * to run the mojit. - * - * Only `base` or `type` fields are required. You should only specify one. - * - *
-     *  instance: {
-     *      base: string
-     *          // specifies a "base" instance which this instance will extend
-     *          // the value refers to a key of `specs` in `application.json`
-     *      type: string
-     *          // specifies the mojit type
-     *      action: "",
-     *          // specifies a default action if the instance isn't dispatched
-     *          // with a specific one.
-     *      config: object
-     *          // the config for the mojit
-     *          // this will be augmented (appropriately) with the mojit type
-     *          // defaults found in the type's `defaults.json`
-     *      appConfig: object
-     *          // the application config (appropriate for the context)
-     *      assetRoot: "",
-     *          // path to directory containing assets
-     *          // the path will be a URL if `env` is `client` otherwise it's a
-     *          // filesystem path
-     *      definition: object
-     *          // the body of the `defintion.json` for the mojit type
-     *      defaults: object
-     *          // the body of the `defaults.json` for the mojit type
-     *      yui: {
-     *          // details for generating a YUI sandbox for this instance
-     *          config: {
-     *              // configuration details for the YUI.GlobalConfig.groups (or
-     *              // an equivalent).
-     *              // The module paths are given as `fullpath` and contain
-     *              // either a URL if `env' is `client` or a filesystem path if
-     *              // `env` is `server`
-     *          },
-     *          requires: []
-     *              // list of YUI modules that this instance requires
-     *      }
-     *      actions: array
-     *          // list of paths to the YUI modules containing actions
-     *      controller: string
-     *          // path to controller
-     *          // the path will be a URL if `env` is `client` otherwise it's a
-     *          // filesystem path
-     *      lang:
-     *          // path to YUI module of the language bundle
-     *          // the path will be a URL if `env` is `client` otherwise it's a
-     *          // filesystem path
-     *      models: object
-     *          // list of models used by the mojit type
-     *          // the key is the model name, and the value is the path to the
-     *          // model file
-     *          // the path will be a URL if `env` is `client` otherwise it's a
-     *          // filesystem path
-     *      views: {
-     *          // list of views in the mojit type
-     *          // the key is the view name, and the value is details about the
-     *          // view
-     *          view-name: {
-     *              "content-path": "",
-     *                  // the path to use to load the body of the view
-     *                  // the path will be a URL if `env` is `client` otherwise
-     *                  // it's a filesystem path
-     *              "engine": "",
-     *                  // which engine is used to render the view
-     *              "binder-path": "",
-     *                  // the path to the binder
-     *                  // the path will be a URL if `env` is `client` otherwise
-     *                  // it's a filesystem path
-     *              "binder-module": ""
-     *                  // the YUI module name of the binder
-     *          }
-     *      }
-     *  }
-     * 
- * - * @method expandInstanceForEnv - * @param env {string} "client" or "server" - * @param instance {object} partial instance to expand - * @param ctx {object} the runtime context for the instance - * @param cb {function(err,instance)} callback used to return the results (or error) - * @return {nothing} results returned via the callback parameter - */ - expandInstanceForEnv: function(env, instance, ctx, cb) { - //logger.log('expandInstanceForEnv(' + env + ',' + - // (instance.id||'@'+instance.type) + ')'); - var self = this, - base, - appConfig = this.getAppConfig(ctx, 'application'), - cacheKey = Y.JSON.stringify(instance) + Y.JSON.stringify( - this._getValidYCBContext(ctx) - ), - cacheValue = this._expandInstanceCache[env][cacheKey]; - - if (cacheValue) { - cb(null, this._cloneObj(cacheValue)); - return; - } + // start with the base + appConfig = Y.clone(this._fwConfig.appConfigBase, true); - function gotBase(out, fromBase) { - var spec; - - // type details rebuilt every time - delete out.actions; - delete out.assetsRoot; - delete out.assets; - delete out.controller; - delete out.defaults; - delete out.definition; - delete out.models; - delete out.views; - delete out.yui; - - out.config = out.config || {}; - out.action = out.action || 'index'; - if (!out.instanceId) { - out.instanceId = Y.guid(); - //DEBUGGING: out.instanceId += '-instance-server-' + out.type; - } - // DEPRECATED, kept in case a user is using it. - out.guid = out.instanceId; + // apply the read values from the file + ycb = this.config.readConfigYCB(this._libs.path.join(this._config.root, 'application.json'), ctx); + this.mergeRecursive(appConfig, ycb); - try { - self.getMojitTypeDetails(env, ctx, out.type, out); - } catch (err) { - return cb(err); - } + // apply the passed-in overrides + this.mergeRecursive(appConfig, Y.clone(this._config.appConfig, true)); - // apply type defaults to config - if ((!fromBase) && out.defaults && out.defaults.config) { - spec = self._cloneObj(out.defaults.config); - self._mergeRecursive(spec, out.config); - out.config = spec; - } + return appConfig; + }, - if (!out.appConfig) { - out.appConfig = appConfig; - delete out.appConfig.specs; - } - self._expandInstanceCache[env][cacheKey] = self._cloneObj(out); - cb(null, out); - } - if (instance.base) { - if (appConfig.specs) { - base = appConfig.specs[instance.base]; - } - if (!base) { - return cb(new Error('Unknown "base" of "' + instance.base + - '"')); - } - // The base will need to carry it's ID with it. - base.id = instance.base; - this.expandInstanceForEnv(env, base, ctx, function(err, - baseInstance) { - if (err) { - return cb(err); + /** + * Preloads everything in the app, and as well pertinent parts of + * the framework. + * + * @method preload + * @return {nothing} + */ + preload: function() { + // We need to do an initial sweep to find the resource store addons. + this.preloadResourceVersions(); + // And then use them. + this.loadAddons(); + // Then, do another sweep so that the loaded addons can be used. + this.preloadResourceVersions(); + this.resolveResourceVersions(); + }, + + + /** + * Returns a list of resource versions that match the filter. + * (To get the list of resource versions from all mojits, you'll need + * to call `listAllMojits()` and iterate over that list, calling this + * method with `mojit:` in the filter.) + * + * @method getResourceVersions + * @param {object} filter limit returned resource versions to only those whose keys/values match the filter + * @return {array of objects} list of matching resource versions + */ + getResourceVersions: function(filter) { + var source, + out = [], + r, + res, + k, + use; + + source = filter.mojit ? this._mojitRVs[filter.mojit] : this._appRVs; + if (!source) { + return []; + } + for (r = 0; r < source.length; r += 1) { + res = source[r]; + use = true; + for (k in filter) { + if (filter.hasOwnProperty(k)) { + if (res[k] !== filter[k]) { + use = false; + break; + } + } } - var temp = baseInstance; - self._mergeRecursive(temp, instance); - gotBase(temp, true); - }); - } else { - gotBase(this._cloneObj(instance), false); - } - }, - - - /** - * gets application configuration - * - * @method getAppConfig - * @param ctx {object} the runtime context under which to load the config - * @param name {string} type of config to read: - * - definition: reads ./application.json - * - package: reads ./package.json - * - routes: reads ./routes.json (or whatever was configured in - * appConfig('definition').routesFiles) - * @return {object} config object - */ - getAppConfig: function(ctx, name) { - //logger.log('getAppConfig('+name+')'); - var resid, - res, - ycb; - - if ('application' === name && (!ctx || !Object.keys(ctx).length)) { - return this._cloneObj(this._appConfigStatic); - } - - resid = 'config-' + name; - res = this._getContextualizedResource(this._appMeta.server, ctx, resid); - if (!res) { - return {}; - } - ycb = this._readConfigYCB(ctx, res.fsPath, true); - return this._cloneObj(ycb); - }, - - - /** - * Returns the routes configured in the application. - * - * @method getRoutes - * @param ctx {object} runtime context under which to load the routes - * @return {object} routes - */ - getRoutes: function(ctx) { - //logger.log('getRoutes()'); - var ress, - resid, - res, - r, - routes = {}; - - // TODO: [Issue 100] trapped this error. It only appears when there is - // no application.json - try { - ress = this._getResourceListForContext(this._appMeta.server, ctx); - } catch (err) { - //logger.log(err); - ress = {}; - } - - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { - res = ress[resid]; - if ('routes' !== res.configType) { - continue; + if (use) { + out.push(res); } - r = this._readConfigYCB(ctx, res.fsPath); - this._mergeRecursive(routes, r); } - } - if (!Object.keys(routes).length) { - routes = this._cloneObj(this._fwConfig.defaultRoutes); - } - return routes; - }, - + return out; + }, - /** - * Returns the filesystem location of the static URL. - * Returns undefined if given URL isn't part of the app. - * - * @method fileFromStaticHandlerURL - * @param url {string} static URL - * @return {string} path on filesystem of specified URL, or undefined - */ - fileFromStaticHandlerURL: function(url) { - //logger.log('fileFromStaticHandlerURL('+url+')'); - return this._staticURLs[url]; - }, - - /** - * Returns the YUI configuration object which tells YUI about the - * YUI modules in all the mojits. - * - * @method getYuiConfigAllMojits - * @param env {string} "client" or "server" - * @param ctx {object} runtime context for YUI configuration - * @return {object} YUI configuration for all mojits - */ - getYuiConfigAllMojits: function(env, ctx) { - // TODO: use getMojitTypeDetails() to generate this. - //logger.log('getYuiConfigAllMojits('+env+')'); - var modules = {}, - type, - ress, - resid, - res, - ctxKey; - - for (type in this._mojitMeta[env]) { - if (this._mojitMeta[env].hasOwnProperty(type)) { - ress = this._getResourceListForContext( - this._mojitMeta[env][type], - ctx - ); - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { - res = ress[resid]; - if (!res.yuiModuleName) { - continue; - } - if (res.sharedMojit) { - continue; + /** + * Returns a list of resources that match the filter. + * (To get the list of resources from all mojits, you'll need to call + * `listAllMojits()` and iterate over that list, calling this method + * with `mojit:` in the filter.) + * + * @method getResources + * @param {string} env the runtime environment (either `client` or `server`) + * @param {object} ctx the context + * @param {object} filter limit returned resources to only those whose keys/values match the filter + * @return {array of objects} list of matching resources + */ + getResources: function(env, ctx, filter) { + var posl, + source, + out = [], + r, + res, + k, + use; + + this.validateContext(ctx); + + posl = Y.JSON.stringify(this.selector.getPOSLFromContext(ctx)); + if (filter.mojit) { + if (!this._mojitResources[env] || + !this._mojitResources[env][posl] || + !this._mojitResources[env][posl][filter.mojit]) { + return []; + } + source = this._mojitResources[env][posl][filter.mojit]; + } else { + if (!this._appResources[env] || + !this._appResources[env][posl]) { + return []; + } + source = this._appResources[env][posl]; + } + // this is taken care of already, and will trip up mojit-level + // resources that are actually shared + delete filter.mojit; + for (r = 0; r < source.length; r += 1) { + res = source[r]; + use = true; + for (k in filter) { + if (filter.hasOwnProperty(k)) { + if (res[k] !== filter[k]) { + use = false; + break; } - modules[res.yuiModuleName] = { - fullpath: ('client' === env) ? - res.staticHandlerURL : - res.fsPath, - requires: res.yuiModuleMeta.requires - }; } } - - // add all langs - ress = this._getLangList(this._mojitMeta[env][type]); - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { - res = ress[resid]; - modules[res.yuiModuleName] = { - fullpath: ('client' === env) ? - res.staticHandlerURL : - res.fsPath, - requires: res.yuiModuleMeta.requires - }; - } + if (use) { + out.push(res); } } - } + return out; + }, - return {modules: modules}; - }, - - /** - * Returns the YUI configuration object which tells YUI about the - * YUI modules in the Mojito framework. - * - * @method getYuiConfigFw - * @param env {string} "client" or "server" - * @param ctx {object} runtime context for YUI configuration - * @return {object} YUI configuration for Mojito framework - */ - getYuiConfigFw: function(env, ctx) { - //logger.log('getYuiConfigFw('+env+')'); - var modules = {}, - ress, - resid, - res; - - if (!this._sharedMeta[env]) { - return {modules: {}}; - } - ress = this._getResourceListForContext(this._sharedMeta[env], ctx); - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { - res = ress[resid]; - if (!res.yuiModuleName) { - continue; - } - if ('mojito' !== res.pkg.name) { - continue; + /** + * Returns a list of all mojits in the app, except for the "shared" mojit. + * @method listAllMojits + * @return {array} list of mojits + */ + listAllMojits: function() { + var mojitType, + list = []; + for (mojitType in this._mojitRVs) { + if (this._mojitRVs.hasOwnProperty(mojitType)) { + if ('shared' !== mojitType) { + list.push(mojitType); + } } - modules[res.yuiModuleName] = { - fullpath: ('client' === env) ? - res.staticHandlerURL : - res.fsPath, - requires: res.yuiModuleMeta.requires - }; } - } - return {modules: modules}; - }, + return list; + }, - /** - * Returns the YUI configuration object which tells YUI about the - * YUI modules in the application (which aren't part of a mojit). - * - * @method getYuiConfigApp - * @param env {string} "client" or "server" - * @param ctx {object} runtime context for YUI configuration - * @return {object} YUI configuration for the app-level modules - */ - getYuiConfigApp: function(env, ctx) { - //logger.log('getYuiConfigApp('+env+')'); - var modules = {}, - ress, - resid, - res; - - if (!this._sharedMeta[env]) { - return {modules: {}}; - } - ress = this._getResourceListForContext(this._sharedMeta[env], ctx); - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { - res = ress[resid]; - if (!res.yuiModuleName) { - continue; + /** + * Returns, via callback, the fully expanded mojit instance specification. + * + * @async + * @method getSpec + * @param {string} env the runtime environment (either `client` or `server`) + * @param {string} id the ID of the spec to return + * @param {object} ctx the runtime context for the spec + * @param {function(err,spec)} callback callback used to return the results (or error) + */ + getSpec: function(env, id, ctx, callback) { + this.expandInstanceForEnv(env, {base: id}, ctx, function(err, obj) { + if (err) { + callback(err); + return; } - if ('mojito' === res.pkg.name) { - continue; + if (env === 'client' && obj) { + delete obj.assets; } - modules[res.yuiModuleName] = { - fullpath: ('client' === env) ? - res.staticHandlerURL : - res.fsPath, - requires: res.yuiModuleMeta.requires - }; - } - } - return {modules: modules}; - }, - - - /** - * returns a serializeable object used to initialize Mojito on the client - * - * FUTURE: [Issue 105] Cache the output of this function - * cache key: all of ctx - * - * @method serializeClientStore - * @param context {object} runtime context - * @param instance {array} DEPRECATED: list of instances to deploy to the client - * (only instances with IDs will be deployable) - * @return {object} object that should be serialized and used to initialize the MojitoClient - */ - serializeClientStore: function(ctx, instances) { - //logger.log('serializeClientStore()'); - var i, - id, - instance, - type, - types = {}, - out = { - appConfig: {}, - specs: {}, // instance details - mojits: {}, // type details - routes: {} - }; - - out.appConfig = this.getAppConfig(ctx, 'application'); - - for (i = 0; i < instances.length; i += 1) { - instance = instances[i]; - types[instance.type] = true; - id = instance.id; - if (id) { - out.specs[id] = out.appConfig.specs[id]; - } - } - - for (type in types) { - if (types.hasOwnProperty(type)) { - out.mojits[type] = {}; - this.getMojitTypeDetails('client', ctx, type, out.mojits[type]); - } - } + callback(null, obj); + }); + }, + + + /** + * Returns, via callback, the details of the mojit type. + * + * @async + * @method getType + * @param {string} env the runtime environment (either `client` or `server`) + * @param {string} type the mojit type + * @param {object} ctx the runtime context for the type + * @param {function(err,spec)} callback callback used to return the results (or error) + */ + getType: function(env, type, ctx, callback) { + this.expandInstanceForEnv(env, {type: type}, ctx, function(err, obj) { + if (err) { + callback(err); + return; + } + if (env === 'client' && obj) { + delete obj.assets; + } + callback(null, obj); + }); + }, - out.routes = this.getRoutes(ctx); - // these aren't needed on the client - delete out.appConfig.mojitsDirs; - delete out.appConfig.routesFiles; - delete out.appConfig.specs; + /** + * This just calls `expandInstanceForEnv()` with `env` set to `server`. + * + * @async + * @method expandInstance + * @param {map} instance partial instance to expand + * @param {object} ctx the context + * @param {function(err,instance)} cb callback used to return the results (or error) + */ + expandInstance: function(instance, ctx, cb) { + this.expandInstanceForEnv('server', instance, ctx, cb); + return; + }, - return out; - }, + /** + * Expands the instance into all details necessary to dispatch the mojit. + * @method expandInstanceForEnv + * @param {string} env the runtime environment (either `client` or `server`) + * @param {object} instance + * @param {object} ctx the context + * @param {function(err,instance)} cb callback used to return the results (or error) + */ + expandInstanceForEnv: function(env, instance, ctx, cb) { + var cacheKey = Y.JSON.stringify(instance) + Y.JSON.stringify(ctx), + cacheValue = this._expandInstanceCache[env][cacheKey], + spec, + typeDetails, + config; - /** - * Returns a list of all mojit types in the application. - * - * @method listAllMojits - * @param env {string} "client" or "server" - * @return {array} list of mojit types - */ - listAllMojits: function(env) { - var mojitType, - list = []; + this.validateContext(ctx); - for (mojitType in this._mojitMeta[env]) { - if (this._mojitMeta[env].hasOwnProperty(mojitType)) { - list.push(mojitType); + if (cacheValue) { + cb(null, Y.clone(cacheValue, true)); + return; } - } - return list; - }, + // TODO: should this be done here, or somewhere else? + ctx.runtime = env; - /** - * Returns details about all mojits in the application. - * - * @method getAllMojits - * @param env {string} "client" or "server" - * @param ctx {object} runtime context - * @return {object} keys are mojit type names, values are details about each mojit - */ - getAllMojits: function(env, ctx) { + try { + spec = this._expandSpec(env, ctx, instance); + } catch (err) { + return cb(err); + } + spec.config = spec.config || {}; + spec.action = spec.action || 'index'; + if (!spec.instanceId) { + spec.instanceId = Y.guid(); + } - var mojits, - mojit, - list = {}; + spec.appConfig = this.getAppConfig(ctx); + delete spec.appConfig.specs; - if (!ctx) { - ctx = {}; - } + try { + this.getMojitTypeDetails(env, ctx, spec.type, spec); + } catch (err2) { + return cb(err2); + } + if (spec.defaults && spec.defaults.config) { + config = Y.clone(spec.defaults.config, true); + this.mergeRecursive(config, spec.config); + spec.config = config; + } + + this._expandInstanceCache[env][cacheKey] = Y.clone(spec, true); + cb(null, spec); + }, + + + /** + * Returns details about a mojit type. + * + * As the last step of execution, this fires the `getMojitTypeDetails` + * event so that Resource Store addons can augment the returned structure. + * + * @method getMojitTypeDetails + * @param {string} env the runtime environment (either `client` or `server`) + * @param {object} ctx the context + * @param {string} mojitType mojit type + * @param {object} dest object in which to place the results + * @return {object} returns the "dest" parameter, which has had details added to it + */ + /** + * Fired at the end of the `getMojitTypeDetails()` method to allow + * modification of the results. + * @event getMojitTypeDetails + * @param {object} args input arguments + * @param {string} args.env the runtime environment (either `client` or `server`) + * @param {object} args.ctx runtime context + * @param {array} args.posl priority-ordered seletor list + * @param {string} args.mojitType name of mojit + * @param {object} mojit the mojit type details + */ + getMojitTypeDetails: function(env, ctx, mojitType, dest) { + //Y.log('getMojitTypeDetails('+env+', '+JSON.stringify(ctx)+', '+mojitType+')', 'debug', NAME); + var ress, + r, + res, + engine, + engines = {}, // view engines + posl = this.selector.getPOSLFromContext(ctx), + ctxKey, + module; - mojits = this.listAllMojits(env); + this.validateContext(ctx); - for (mojit in mojits) { - if (mojits.hasOwnProperty(mojit)) { - list[mojits[mojit]] = - this.getMojitTypeDetails(env, ctx, mojits[mojit]); + if ('shared' === mojitType) { + throw new Error('Mojit name "shared" is special and isn\'t a real mojit.'); } - } - return list; - }, - - - /** - * Given a set of known contexts, finds the best match for a runtime context. - * Gives special consideration to the "lang" key in the contexts. - * - * @method _findBestContext - * @param currentContext {object} runtime context - * @param contexts {object} a mapping of context key to context - * @return {string} null or the context key of the best match - * @private - */ - _findBestContext: function(currentContext, contexts) { - var availableLangs = [], - bestCtxKey, - bestLang, - context, - ctxKey, - i, - matchingKeys = []; - - // Collect languages from matching contexts - // We're done if we find an exact match - for (ctxKey in contexts) { - if (contexts.hasOwnProperty(ctxKey)) { - context = contexts[ctxKey]; - if (this._matchContext(currentContext, context)) { - if (context.lang) { - if (currentContext.lang === context.lang) { - bestCtxKey = ctxKey; - break; - } - availableLangs.push(context.lang); - } - matchingKeys.push(ctxKey); - } + if (!dest) { + dest = {}; } - } - // If no exact match, find the next best language - if (!bestCtxKey && availableLangs && availableLangs.length && - currentContext && currentContext.lang) { - bestLang = Y.Intl.lookupBestLang(currentContext.lang, - availableLangs); - if (bestLang) { - for (i = 0; i < matchingKeys.length; i += 1) { - if (contexts[matchingKeys[i]].lang === bestLang) { - bestCtxKey = matchingKeys[i]; - break; - } - } + if (!dest.assets) { + dest.assets = {}; + } + if (!dest.models) { + dest.models = {}; + } + if (!dest.views) { + dest.views = {}; } - } - - return bestCtxKey || - (matchingKeys.length && matchingKeys[0]) || - null; - }, - - - /** - * Returns details about a mojit type. - * - * @method getMojitTypeDetails - * @param env {string} "client" or "server" - * @param ctx {object} runtime context - * @param mojitType {string} mojit type - * @param dest {object} object in which to place the results - * @return {object} returns the "dest" parameter, which has had details added to it - */ - getMojitTypeDetails: function(env, ctx, mojitType, dest) { - //logger.log('getMojitTypeDetails('+env+',ctx,'+mojitType+')'); - var ress, - resid, - res, - name, - engine, - engines = {}, - ctxKey, - assumeRollups = this._appConfigStatic.assumeRollups, - usePrecomputed = -1 !== this._appConfigStatic.yui. - dependencyCalculations.indexOf('precomputed'), - useOnDemand = -1 !== this._appConfigStatic.yui. - dependencyCalculations.indexOf('ondemand'), - module, - lddf, // lang/datatype-date-format - lddfPath; - - if (!usePrecomputed) { - useOnDemand = true; - } - - if (!dest) { - dest = {}; - } - - if (!dest.actions) { - dest.actions = []; - } - if (!dest.assets) { - dest.assets = {}; - } - if (!dest.models) { - dest.models = {}; - } - if (!dest.modelYUIModuleNames) { - dest.modelYUIModuleNames = {}; - } - if (!dest.views) { - dest.views = {}; - } - if (!dest.yui) { - dest.yui = {config: {}, requires: []}; - } - if (!dest.yui.config) { - dest.yui.config = {modules: {}}; - } - if (!dest.yui.config.modules) { - dest.yui.config.modules = {}; - } - if (!dest.yui.requires) { - dest.yui.requires = []; - } - if (!dest.yui.langs) { - dest.yui.langs = {}; - } - - if (usePrecomputed) { - ctxKey = this._findBestContext(ctx, - this._mojitYuiSorted[env][mojitType].contexts); - dest.yui.requires = - this._mojitYuiRequired[env][mojitType][ctxKey] || []; - dest.yui.sorted = - this._cloneObj( - this._mojitYuiSorted[env][mojitType][ctxKey] || [] - ); - dest.yui.sortedPaths = - this._cloneObj( - this._mojitYuiSortedPaths[env][mojitType][ctxKey] || {} - ); - } - - dest.assetsRoot = this._mojitAssetRoots[mojitType]; - dest.definition = this._getMojitConfig('server', ctx, mojitType, - 'definition'); - dest.defaults = this._getMojitConfig('server', ctx, mojitType, - 'defaults'); - ress = this._getResourceListForContext(this._mojitMeta[env][mojitType], - ctx); - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { + dest.definition = {}; + dest.defaults = {}; - res = ress[resid]; + ress = this.getResources(env, ctx, { mojit: mojitType }); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; - if (res.type === 'action') { - if (env === 'client') { - if (assumeRollups) { - dest.actions.push(res.rollupURL); - } else { - dest.actions.push(res.staticHandlerURL); - } - } else { - dest.actions.push(res.fsPath); + if (res.type === 'config') { + if ('definition' === res.source.fs.basename) { + dest.definition = this.config.readConfigYCB(res.source.fs.fullPath, ctx); + } + if ('defaults' === res.source.fs.basename) { + dest.defaults = this.config.readConfigYCB(res.source.fs.fullPath, ctx); } } if (res.type === 'asset') { - name = res.name; if (env === 'client') { - if (assumeRollups) { - dest.assets[name] = res.rollupURL; - } else { - dest.assets[name] = res.staticHandlerURL; - } + dest.assets[res.name + res.source.fs.ext] = res.url; } else { - dest.assets[name] = res.fsPath; + dest.assets[res.name + res.source.fs.ext] = res.source.fs.fullPath; } } @@ -1126,57 +661,26 @@ ServerStore.prototype = { if (!dest.views[res.name]) { dest.views[res.name] = {}; } - dest.views[res.name]['binder-url'] = res.staticHandlerURL; + dest.views[res.name]['binder-url'] = res.url; if (env === 'client') { - if (assumeRollups) { - dest.views[res.name]['binder-path'] = res.rollupURL; - } else { - dest.views[res.name]['binder-path'] = - res.staticHandlerURL; - } + dest.views[res.name]['binder-path'] = res.url; } else { - dest.views[res.name]['binder-path'] = res.fsPath; - } - dest.views[res.name]['binder-module'] = res.yuiModuleName; - dest.views[res.name]['binder-yui-sorted'] = - res.yuiSortedPaths; - if ('server' === env) { - // don't do any other type of server-side processing for - // the binder ESPECIALLY don't add it to dest.yui.* - continue; + dest.views[res.name]['binder-path'] = res.source.fs.fullPath; } } if (res.type === 'controller') { // We need the YUI Module name of the contoller so we can // select a language for it - dest.controllerModuleName = res.yuiModuleName; if (env === 'client') { - if (assumeRollups) { - dest.controller = res.rollupURL; - } else { - dest.controller = res.staticHandlerURL; - } + dest['controller-path'] = res.url; } else { - dest.controller = res.fsPath; + dest['controller-path'] = res.source.fs.fullPath; } } if (res.type === 'model') { - if (env === 'client') { - if (assumeRollups) { - dest.models[res.name] = res.rollupURL; - } else { - dest.models[res.name] = res.staticHandlerURL; - } - } else { - dest.models[res.name] = res.fsPath; - } - if (res.yuiModuleName) { - dest.modelYUIModuleNames[res.yuiModuleName] = true; - //logger.log("Processing Models:" + res.name + ":" + - // res.yuiModuleName, 'mojito', NAME); - } + dest.models[res.name] = true; } if (res.type === 'view') { @@ -1184,3223 +688,1248 @@ ServerStore.prototype = { dest.views[res.name] = {}; } if (env === 'client') { - if (assumeRollups) { - dest.views[res.name]['content-path'] = - res.rollupURL; - } else { - dest.views[res.name]['content-path'] = - res.staticHandlerURL; - } + dest.views[res.name]['content-path'] = res.url; } else { - dest.views[res.name]['content-path'] = res.fsPath; + dest.views[res.name]['content-path'] = res.source.fs.fullPath; } - dest.views[res.name].engine = res.viewEngine; - engines[res.viewEngine] = true; + dest.views[res.name].engine = res.view.engine; + engines[res.view.engine] = true; } + } - if (res.type === 'yui-lang') { - dest.yui.langs[res.langCode] = res.yuiModuleName; - } + // YUI AOP doesn't give plugins enough control, so use + // onHostMethod() and afterHostMethod(). + this.fire('getMojitTypeDetails', { + args: { + env: env, + ctx: ctx, + posl: posl, + mojitType: mojitType + }, + mojit: dest + }); + return dest; + }, - if (res.yuiModuleName) { - if (res.addonType === 'view-engines') { - // we'll only load the viewEngines that we need - continue; - } - if (useOnDemand) { - dest.yui.requires.push(res.yuiModuleName); - } - if (res.sharedMojit) { - continue; - } - dest.yui.config.modules[res.yuiModuleName] = { - fullpath: undefined, - requires: res.yuiModuleMeta.requires || [] - }; - if (env === 'client') { - if (assumeRollups) { - dest.yui.config.modules[res.yuiModuleName - ].fullpath = res.rollupURL; - } else { - dest.yui.config.modules[res.yuiModuleName - ].fullpath = res.staticHandlerURL; - } - } else { - dest.yui.config.modules[res.yuiModuleName].fullpath = - res.fsPath; - } - // If we have "lang" use it - if (this._mojitLangs[res.yuiModuleName]) { - this._mojitLangs[res.yuiModuleName].sort(); - dest.yui.config.modules[res.yuiModuleName].lang = - this._mojitLangs[res.yuiModuleName]; - } + /** + * Returns the routes configured in the application. + * @method getRoutes + * @param {object} ctx the context + * @return {object} routes + */ + getRoutes: function(ctx) { + var appConfig = this.getAppConfig(ctx), + routesFiles = appConfig.routesFiles, + p, + path, + fixedPaths = {}, + out = {}, + ress, + r, + res, + routes; + + for (p = 0; p < routesFiles.length; p += 1) { + path = routesFiles[p]; + // relative paths are relative to the application + if ('/' !== path.charAt(1)) { + path = this._libs.path.join(this._config.root, path); } + fixedPaths[path] = true; } - } - for (engine in engines) { - if (engines.hasOwnProperty(engine)) { - resid = 'addon-view-engines-' + engine; - res = this._getContextualizedResource( - this._mojitMeta[env][mojitType], - ctx, - resid - ); - dest.yui.config.modules[res.yuiModuleName] = { - fullpath: ('client' === env) ? - res.staticHandlerURL : - res.fsPath, - requires: res.yuiModuleMeta.requires || [] - }; - if (useOnDemand) { - dest.yui.requires.push(res.yuiModuleName); + + ress = this.getResources('server', ctx, {type: 'config'}); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if (fixedPaths[res.source.fs.fullPath]) { + routes = Y.clone(this.config.readConfigYCB(res.source.fs.fullPath, ctx), true); + out = Y.merge(out, routes); } } - } - // We need to include -all- the langs since we don't know which will - // actually be used. dispatch() will cull that down to the right one. - if (usePrecomputed) { - for (name in dest.yui.langs) { - if (dest.yui.langs.hasOwnProperty(name)) { - module = dest.yui.langs[name]; - lddf = 'lang/datatype-date-format_' + (name || 'en'); - if (dest.yui.sortedPaths[lddf]) { - lddfPath = dest.yui.sortedPaths[lddf].replace('_' + - name, '_MOJITOPLACEHOLDER'); - } - if (!dest.yui.sortedPaths[module]) { - dest.yui.sorted.push(module); - dest.yui.sortedPaths[module] = - dest.yui.config.modules[module].fullpath; + if (!Object.keys(out).length) { + return this._fwConfig.defaultRoutes; + } + return out; + }, + + + /** + * Sugar method that returns all "url" metadata of all resources. + * @method getAllURLs + * @return {object} for all resources with a "url" metadatum, the key is + * that URL and the value the filesystem path + */ + getAllURLs: function() { + var r, + res, + ress, + m, + mojit, + mojits, + urls = {}; + mojits = this.listAllMojits(); + mojits.push('shared'); + for (m = 0; m < mojits.length; m += 1) { + mojit = mojits[m]; + ress = this.getResourceVersions({mojit: mojit}); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if (res.url) { + urls[res.url] = res.source.fs.rollupPath || res.source.fs.fullPath; } } } - if (lddfPath) { - for (name in dest.yui.langs) { - if (dest.yui.langs.hasOwnProperty(name)) { - lddf = 'lang/datatype-date-format_' + (name || 'en'); - if (!dest.yui.sortedPaths[lddf]) { - dest.yui.sorted.push(lddf); - dest.yui.sortedPaths[lddf] = - lddfPath.replace('MOJITOPLACEHOLDER', - (name || 'en')); + return urls; + }, + + + /** + * Recursively merge one object onto another. + * [original implementation](http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically/383245#383245) + * + * @method mergeRecursive + * @param {object} dest object to merge into + * @param {object} src object to merge onto "dest" + * @param {boolean} typeMatch controls whether a non-object in the src is + * allowed to clobber a non-object in the dest (if a different type) + * @return {object} the modified "dest" object is also returned directly + */ + mergeRecursive: function(dest, src, typeMatch) { + var p; + for (p in src) { + if (src.hasOwnProperty(p)) { + // Property in destination object set; update its value. + if (src[p] && src[p].constructor === Object) { + if (!dest[p]) { + dest[p] = {}; + } + dest[p] = this.mergeRecursive(dest[p], src[p]); + } else { + if (dest[p] && typeMatch) { + if (typeof dest[p] === typeof src[p]) { + dest[p] = src[p]; + } + } else { + dest[p] = src[p]; } } } } - } + return dest; + }, - return dest; - }, + //==================================================================== + // CALLBACK METHODS + // These are called at various points in the algorithm of public + // methods. They are public so that they can be hooked into via AOP. - /** - * Returns details on how to make rollups for app-level resources. - * - * @method getRollupsApp - * @param env {string} "client" or "server" - * @param ctx {object} runtime context - * @return {object} object describing where to put the rollup and what it should contain - */ - getRollupsApp: function(env, ctx) { - var dest = libpath.join(this._root, 'rollup.' + env + '.js'), - srcs = [], - ress, - resid, - res; - - ress = this._getResourceListForContext(this._sharedMeta[env], ctx); - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { - res = ress[resid]; - if (!res.yuiModuleName) { - continue; - } - srcs.push(res.fsPath); - } - } - return { - dest: dest, - srcs: srcs - }; - }, + /** + * Augments this resource store with addons that we know about. + * To find the addons, call `preloadResourceVersions()` first. + * + * You most often don't want to call this directly, but instead to hook + * into it using the AOP mechanism of `Y.Plugin.Base`: + * + * this.afterHostMethod('loadAddons', this._myLoadAddons, this); + * + * @method loadAddons + * @return {nothing} + */ + loadAddons: function() { + var modules = {}, + ress, + r, + res; - /** - * Returns details on how to make rollups for mojit-level resources. - * - * This example comes from GSG5. - * { FlickrDetail: - * dest: '/blah/blah/mojits/FlickrDetail/rollup.client.js' - * srcs: [ - * '/blah/blah/mojits/FlickrDetail/controller.common.js', - * '/blah/blah/mojits/FlickrDetail/binders/index.js', - * '/blah/blah/mojits/FlickrDetail/lang/FlickrDetail_de.js', - * '/blah/blah/mojits/FlickrDetail/lang/FlickrDetail_en-US.js' - * ] - * } - * - * @method getRollupsMojits - * @param env {string} "client" or "server" - * @param ctx {object} runtime context - * @return {object} object describing where to put the rollup and what it should contain - */ - getRollupsMojits: function(env, ctx) { - var mojitName, - ress, - resid, - res, - dest, - srcs, - rollups = {}, - mojitoMojits = libpath.join(mojitoRoot, 'mojits'); - - for (mojitName in this._mojitMeta[env]) { - if (this._mojitMeta[env].hasOwnProperty(mojitName)) { - srcs = []; - ress = this._getResourceListForContext( - this._mojitMeta[env][mojitName], - ctx - ); - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { - res = ress[resid]; - if (res.sharedMojit) { - continue; - } - if (!res.yuiModuleName) { - continue; - } - srcs.push(res.fsPath); - } - } - dest = libpath.join(this._mojitPaths[mojitName], 'rollup.' + - env + '.js'); - if (dest.indexOf(mojitoMojits) === 0) { - // we shouldn't write to the mojits that ship with Mojito - dest = false; - } - if (dest && srcs.length) { - rollups[mojitName] = { - dest: dest, - srcs: srcs + Y.Object.each(Y.mojito.addons.rs, function(fn, name) { + this.unplug(name); + }, this); + + ress = this.getResourceVersions({type: 'addon', subtype: 'rs'}); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if ('rs' === res.subtype) { + // FUTURE: ideally we shouldn't proscribe the YUI module name of RS addons + // (We can/should introspect the file for the YUI module name.) + modules['addon-rs-' + res.name] = { + fullpath: res.source.fs.fullPath }; } } - } - return rollups; - }, + this._yuiUseSync(modules); + Y.Object.each(Y.mojito.addons.rs, function(fn, name) { + this.plug(fn, { appRoot: this._config.root, mojitoRoot: this._config.mojitoRoot }); + }, this); + }, - /** - * Returns details on how to make inline CSS for mojits. - * - * This example comes from (a modified) GSG5. - * [ { - * context: { device: 'iphone' }, - * mojitName: 'FlickrDetail', - * yuiModuleName: 'inlinecss/FlickrDetail', - * dest: '/blah/mojits/FlickrDetail/autoload/compiled' + - * '/css.iphone.client.js', - * srcs: { - * '/static/FlickrDetail/assets/index.css': - * ' /blah/mojits/FlickrDetail/assets/index.iphone.css', - * '/static/FlickrDetail/assets/message.css': - * ' /blah/mojits/FlickrDetail/assets/message.css' - * } - * ] - * - * @method getInlineCssMojits - * @param env {string} "client" or "server" - * @param ctxFilter {object} (optional) runtime context to restrict results to - * @return {array} object describing where to put the inline CSS file and what it should contain - */ - getInlineCssMojits: function(env, ctxFilter) { - var mojitName, - ctxKey, - ctx, - ress, - resid, - res, - selector, - dest, - srcs, - inlines = [], - mojitoMojits = libpath.join(mojitoRoot, 'mojits'); - - // This will make our test later a little easier. - if (typeof ctxFilter === 'object' && !Object.keys(ctxFilter).length) { - ctxFilter = null; - } - for (mojitName in this._mojitMeta[env]) { - if (this._mojitMeta[env].hasOwnProperty(mojitName)) { - for (ctxKey in this._mojitMeta[env][mojitName].contexts) { - if (this._mojitMeta[env][mojitName - ].contexts.hasOwnProperty(ctxKey)) { - ctx = this._mojitMeta[env][mojitName].contexts[ctxKey]; - if (ctxFilter && !this._matchContext(ctx, ctxFilter)) { - continue; - } + /** + * Preload metadata about all resource versions in the application + * (and Mojito framework). + * + * You most often don't want to call this directly, but instead to hook + * into it using the AOP mechanism of `Y.Plugin.Base`: + * + * this.afterHostMethod('preloadResourceVersions', this._myPreloadResourceVersions, this); + * + * @method preloadResourceVersions + * @return {nothing} + */ + preloadResourceVersions: function() { + var me = this, + walker, + walkedMojito = false, + dir, + info; + + this.selectors = {}; + this._appRVs = []; + this._mojitRVs = {}; + + walker = new this._libs.walker.BreadthFirst(this._config.root); + walker.walk(function(err, info) { + if (err) { + throw err; + } + if ('mojito' === info.pkg.name) { + walkedMojito = true; + } + me._preloadPackage(info); + }); - ress = this._cloneObj(this._mojitMeta[env - ][mojitName]['*']); - if ('*' !== ctxKey) { - ress = this._mergeRecursive(ress, - this._mojitMeta[env][mojitName][ctxKey]); - } - srcs = []; - for (resid in ress) { - if (ress.hasOwnProperty(resid)) { - res = ress[resid]; - if ((res.type !== 'asset') || - (res.assetType !== 'css')) { - continue; - } - srcs[res.staticHandlerURL] = res.fsPath; + // user might not have installed mojito as a dependency of their + // application. (they -should- have but might not have.) + // FUTURE: instead walk -all- global packages? + if (!walkedMojito) { + dir = this._libs.path.join(this._config.mojitoRoot, '..'); + info = { + depth: 999, + parents: [], + dir: dir + }; + info.pkg = this.config.readConfigJSON(this._libs.path.join(dir, 'package.json')); + + if (Object.keys(info.pkg).length) { + mojitoVersion = info.pkg.version; + } else { + // special case for weird packaging situations + info.dir = this._config.mojitoRoot; + info.pkg = { + name: 'mojito', + version: mojitoVersion, + yahoo: { + mojito: { + type: 'bundle', + location: 'app' } } - selector = this._selectorFromContext(ctx); - dest = 'autoload/compiled/inlinecss' + (selector ? '.' + - selector : '') + '.common.js'; - dest = libpath.join(this._mojitPaths[mojitName], dest); - if (dest.indexOf(mojitoMojits) === 0) { - // we shouldn't write to the mojits that ship with - // Mojito - continue; - } - if (Object.keys(srcs).length) { - inlines.push({ - context: ctx, - mojitName: mojitName, - yuiModuleName: 'inlinecss/' + mojitName, - dest: dest, - srcs: srcs - }); - } - - } // has - } // foreach ctxKey - } // has - } // foreach mojit - return inlines; - }, - - - // =========================================== - // ================= PRIVATE ================= + }; + } - /** - * the "static" version of the application.json is the version that has - * the context applied that was given at server-start time. - * - * @method _readAppConfigStatic - * @return {object} static config - * @private - */ - _readAppConfigStatic: function() { - var path = libpath.join(this._root, 'application.json'), - config = this._cloneObj(this._fwConfig.appConfigBase), - appConfig; - - if (this._libs.path.existsSync(path)) { - appConfig = this._readConfigYCB({}, path); - this._mergeRecursive(config, appConfig); - } + this._preloadPackage(info); + } + }, + + + /** + * Called by the ResourceStore to decide if a file should be considered + * a resource. You most often don't want to call this directly, but + * instead to hook into it using the AOP mechanism of `Y.Plugin.Base`: + * + * this.afterHostMethod('findResourceVersionByConvention', this._myFindResourceByConvention, this); + * + * Generally `findResourceVersionByConvention()` and `parseResourceVersion()` are meant to work together. + * This method figures out the type (and subtype) of a file, and `parseResourceVersion()` turns + * the file into an actual resource. + * + * @method findResourceVersionByConvention + * @param {object} source the same as the `source` part of a resource + * @param {string} mojitType the name of the mojit + * @return {boolean|object} If the source is a directory, a boolean can be returned. + * True indicates that the directory contents should be scanned, while false + * indicates that the directory should be skipped. + * If the source does represent a resource, then an object with the following + * fields should be returned: + * type {string} type of the resource, + * subtype {string} optional subtype of the resource, + * skipSubdirParts {integer} number of path parts of `source.fs.subDir` to skip + */ + findResourceVersionByConvention: function(source, mojitType) { + var fs = source.fs, + baseParts = fs.basename.split('.'), + type; - return config; - }, + if (!fs.isFile && '.' === fs.subDir && CONVENTION_SUBDIR_TYPES[fs.basename]) { + return true; + } + type = CONVENTION_SUBDIR_TYPES[fs.subDirArray[0]]; + if (!fs.isFile && type) { + return true; + } + if (fs.isFile && type && fs.subDirArray.length >= 1) { + if (CONVENTION_SUBDIR_TYPE_IS_JS[type] && '.js' !== fs.ext) { + return false; + } + if ('spec' === type && '.json' !== fs.ext) { + return false; + } + return { + type: type, + skipSubdirParts: 1 + }; + } + // special case: addons + if (!fs.isFile && '.' === fs.subDir && 'addons' === fs.basename) { + return true; + } + if (!fs.isFile && fs.subDirArray.length < 2 && 'addons' === fs.subDirArray[0]) { + return true; + } + if (fs.isFile && fs.subDirArray.length >= 1 && 'addons' === fs.subDirArray[0]) { + if ('.js' !== fs.ext) { + return false; + } + return { + type: 'addon', + subtype: fs.subDirArray[1], + skipSubdirParts: 2 + }; + } - /** - * Read the application's dimensions.json file for YCB processing. If not - * available, fall back to the framework's default dimensions.json. - * - * @method _readYcbDimensions - * @return {array} contents of the dimensions.json file - * @private - */ - _readYcbDimensions: function() { - var libpath = this._libs.path, - path = libpath.join(this._root, 'dimensions.json'), - dims; + // special case: archetypes + if (!fs.isFile && '.' === fs.subDir && 'archetypes' === fs.basename) { + return true; + } + if (!fs.isFile && fs.subDirArray.length < 2 && 'archetypes' === fs.subDirArray[0]) { + return true; + } + if (!fs.isFile && fs.subDirArray.length === 2 && 'archetypes' === fs.subDirArray[0]) { + return { + type: 'archetype', + subtype: fs.subDirArray[1], + skipSubdirParts: 2 + }; + } - if (!libpath.existsSync(path)) { - path = libpath.join(mojitoRoot, 'dimensions.json'); - } + // special case: assets + if (!fs.isFile && '.' === fs.subDir && 'assets' === fs.basename) { + return true; + } + if (!fs.isFile && 'assets' === fs.subDirArray[0]) { + return true; + } + if (fs.isFile && 'assets' === fs.subDirArray[0] && fs.subDirArray.length >= 1) { + return { + type: 'asset', + subtype: fs.ext.substr(1), + skipSubdirParts: 1 + }; + } - dims = this._readConfigJSON(path); - if (!this._isValidYcbDimensions(dims)) { - throw new Error('Invalid dimensions.json: ' + path); - } - return dims; - }, + // special case: controller + if (fs.isFile && '.' === fs.subDir && 'controller' === baseParts[0]) { + if ('.js' !== fs.ext) { + return false; + } + return { + type: 'controller' + }; + } + // special case: mojit + if (!fs.isFile && '.' === fs.subDir && 'mojits' === fs.basename) { + // don't bother finding mojits here, since they're loaded explicitly in + // the app and bundle in different ways + return false; + } - /** - * Perform some light validation for YCB dimensions JSON objects. It should - * look something like this: - * [{ - * "dimensions": [ - * "dim1": {}, - * "dim2": {}, - * ... - * ] - * }] - * - * @method _isValidYcbDimensions - * @param {array} dimensions - * @return {boolean} - * @private - */ - _isValidYcbDimensions: function(dims) { - var isArray = Y.Lang.isArray; + // unknown path + return true; + }, + + + /** + * Called by the ResourceStore to turn a file into a resource. + * You most often don't want to call this directly, but instead to hook + * into it using the AOP mechanism of `Y.Plugin.Base`: + * + * this.beforeHostMethod('parseResourceVersion', this._myParseResource, this); + * + * Generally `findResourceVersionByConvention()` and `parseResourceVersion()` are meant to work together. + * `findResourceVersionByConvention()` figures out the type (and subtype) of a file, and + * this method turns the file into an actual resource. + * + * @method parseResourceVersion + * @param {object} source the same as the `source` part of a resource + * @param {string} type the resource type of the file + * @param {string} subtype the optional resource subtype of the file + * @param {string} mojitType the name of the mojit + * @return {object|undefined} the resource version + */ + parseResourceVersion: function(source, type, subtype, mojitType) { + var fs = source.fs, + baseParts = fs.basename.split('.'), + res; - return isArray(dims) && - dims.length === 1 && - isArray(dims[0].dimensions) && - dims[0].dimensions.length > 0; - }, + // app-level resources + if ('archetype' === type || 'command' === type || 'middleware' === type) { + if ('mojit' === fs.rootType) { + Y.log(type + ' cannot be defined in a mojit. skipping ' + fs.fullPath, 'warn', NAME); + return; + } + res = { + source: source, + mojit: null, + type: type, + subtype: subtype, + name: fs.basename, + affinity: DEFAULT_AFFINITIES[type], + selector: '*' + }; + res.id = [res.type, res.subtype, res.name].join('-'); + return res; + } + // mojit parts with format {name}.{affinity}.{selector} + if ('action' === type || + 'addon' === type || + 'controller' === type || + 'model' === type) { + res = { + source: source, + mojit: mojitType, + type: type, + subtype: subtype, + affinity: DEFAULT_AFFINITIES[type], + selector: '*' + }; + if (baseParts.length >= 3) { + res.selector = baseParts.pop(); + } + if (baseParts.length >= 2) { + res.affinity = baseParts.pop(); + } + if (baseParts.length !== 1) { + Y.log('invalid ' + type + ' filename. skipping ' + fs.fullPath, 'warn', NAME); + return; + } + res.name = this._libs.path.join(fs.subDirArray.join('/'), baseParts.join('.')); + res.id = [res.type, res.subtype, res.name].join('-'); + // special case + if ('addon' === type && ADDON_SUBTYPES_APPLEVEL[res.subtype]) { + res.mojit = null; + } + return res; + } - /** - * preload metadata about all resources in the application (and Mojito framework) - * - * @method _preloadMeta - * @return {nothing} work down via other called methods - * @private - */ - _preloadMeta: function() { - var me = this, - walker, - walkedMojito = false, - dir, - info; - walker = new libwalker.BreadthFirst(this._root); - walker.walk(function(err, info) { - if (err) { - throw err; + // mojit parts with format {name}.{selector} + if ('asset' === type || 'binder' === type) { + res = { + source: source, + mojit: mojitType, + type: type, + subtype: subtype, + affinity: DEFAULT_AFFINITIES[type], + selector: '*' + }; + if (baseParts.length >= 2) { + res.selector = baseParts.pop(); + } + if (baseParts.length !== 1) { + Y.log('invalid ' + type + ' filename. skipping ' + fs.fullPath, 'warn', NAME); + return; + } + res.name = this._libs.path.join(fs.subDirArray.join('/'), baseParts.join('.')); + res.id = [res.type, res.subtype, res.name].join('-'); + return res; } - if ('mojito' === info.pkg.name) { - walkedMojito = true; + + // special case: spec + if ('spec' === type) { + res = { + source: source, + mojit: mojitType, + type: 'spec', + affinity: DEFAULT_AFFINITIES[type], + selector: '*' + }; + if (baseParts.length !== 1) { + Y.log('invalid spec filename. skipping ' + source.fs.fullPath, 'warn', NAME); + return; + } + res.name = this._libs.path.join(source.fs.subDir, baseParts.join('.')); + res.id = [res.type, res.subtype, res.name].join('-'); + return res; } - me._preloadPackage(info); - }); - - // user might not have installed mojito as a dependency of their - // application. (they -should- have but might not have.) - if (!walkedMojito) { - dir = libpath.join(mojitoRoot, '..'); - info = { - depth: 999, - parents: [], - dir: dir - }; - info.pkg = this._readMojitConfigFile(libpath.join(dir, 'package.json'), false); - - // special case for weird packaging situations - if (!Object.keys(info.pkg).length) { - info.dir = mojitoRoot; - info.pkg = { - name: 'mojito', - version: '0.666.666', - yahoo: { - mojito: { - type: 'bundle', - location: 'app' - } - } + + // special case: view + if ('view' === type) { + res = { + source: source, + mojit: mojitType, + type: type, + subtype: subtype, + view: { + outputFormat: fs.ext.substr(1), + engine: baseParts.pop() + }, + affinity: DEFAULT_AFFINITIES[type], + selector: '*' }; + if (baseParts.length >= 2) { + res.selector = baseParts.pop(); + } + if (baseParts.length !== 1) { + Y.log('invalid view filename. skipping ' + fs.fullPath, 'warn', NAME); + return; + } + res.name = this._libs.path.join(fs.subDirArray.join('/'), baseParts.join('.')); + res.id = [res.type, res.subtype, res.name].join('-'); + return res; } - this._preloadPackage(info); - } - }, + // just ignore unknown types + return; + }, + + + /** + * Called by the ResourceStore to register a resource version. + * You most often don't want to call this directly, but instead to hook + * into it using the AOP mechanism of `Y.Plugin.Base`: + * + * this.beforeHostMethod('parseResourceVersion', this._myParseResource, this); + * + * @method addResourceVersion + * @param {object} res the resource version + * @return {nothing} + */ + addResourceVersion: function(res) { + res.affinity = new Affinity(res.affinity); + if (this._appConfigStatic.deferAllOptionalAutoloads && + 'optional' === res.affinity.type) { + return; + } - /** - * preloads metadata about resources in the application directory - * (but not node_modules/) - * - * @method _preloadApp - * @param pkg {object} metadata (name and version) about the app's package - * @return {nothing} work down via other called methods - * @private - */ - _preloadApp: function(pkg) { - var i, - path; - - // mark routes, to aid in detecting them later - for (i = 0; i < this._appConfigStatic.routesFiles.length; i += 1) { - path = this._appConfigStatic.routesFiles[i]; - if ('/' !== path.charAt(0)) { - path = libpath.join(this._root, path); + if (res.selector) { + this.selectors[res.selector] = true; } - if (!libpath.existsSync(path)) { - logger.log('missing routes file. skipping ' + path, 'warn', NAME); - continue; + if (res.mojit) { + if (!this._mojitRVs[res.mojit]) { + this._mojitRVs[res.mojit] = []; + } + this._mojitRVs[res.mojit].push(res); + } else { + this._appRVs.push(res); } - this._preload_routes[path] = true; - } + }, - this._preloadDirBundle(this._root, pkg, true); - // load mojitsDirs - for (i = 0; i < this._appConfigStatic.mojitsDirs.length; i += 1) { - path = this._appConfigStatic.mojitsDirs[i]; - this._preloadDirMojits(path, pkg); - } + /** + * For each possible runtime configuration (based on context), pre-calculates + * which versions of the resources will be used. + * The priority (highest to lowest): + * source, + * selector, + * affinity (env or "common"). + * + * @method resolveResourceVersions + * @return {nothing} + */ + /** + * Fired after the resources for a mojit have been resolved. + * @event mojitResourcesResolved + * @param {string} env the runtime environment (either `client` or `server`) + * @param {array} posl priority-ordered seletor list + * @param {string} mojit name of the mojit + * @param {array} ress list of resources in the mojit (for the `env` and `posl`) + */ + resolveResourceVersions: function() { + var p, poslKey, posl, posls = {}, + e, env, envs = [ 'client', 'server' ], + affinities, selectors, sourceBase, + type, ress, + s; + + posls = this.selector.getAllPOSLs(); + + for (e = 0; e < envs.length; e += 1) { + env = envs[e]; + + affinities = {}; // affinity: priority modifier + affinities[env] = 1; + affinities.common = 0; + + for (p = 0; p < posls.length; p += 1) { + posl = posls[p]; + poslKey = Y.JSON.stringify(posl); + selectors = {}; // selector: priority modifier + for (s = 0; s < posl.length; s += 1) { + selectors[posl[s]] = (posl.length - s - 1) * 2; + } + sourceBase = posl.length * 2; + //console.log('-- source base ' + sourceBase); + //console.log(selectors); + //console.log(affinities); + + if (!this._appResources[env]) { + this._appResources[env] = {}; + } + this._appResources[env][poslKey] = + this._resolveVersions(affinities, selectors, sourceBase, [ this._appRVs ]); - // load mojitDirs - if (this._appConfigStatic.mojitDirs) { - for (i = 0; i < this._appConfigStatic.mojitDirs.length; i += 1) { - path = this._appConfigStatic.mojitDirs[i]; - this._preloadDirMojit(path, pkg, path); + if (!this._mojitResources[env]) { + this._mojitResources[env] = {}; + } + if (!this._mojitResources[env][poslKey]) { + this._mojitResources[env][poslKey] = {}; + } + for (type in this._mojitRVs) { + if (this._mojitRVs.hasOwnProperty(type)) { + ress = this._resolveVersions(affinities, selectors, sourceBase, [ this._mojitRVs.shared, this._mojitRVs[type] ]); + this._mojitResources[env][poslKey][type] = ress; + this.fire('mojitResourcesResolved', { + env: env, + posl: posl, + mojit: type, + ress: ress + }); + } + } + } } - } - }, + }, - /** - * preloads metadata about resources in a package - * (but not subpackages in its node_modules/) - * - * @method _preloadPackage - * @param info {object} metadata about the package - * @return {nothing} work down via other called methods - * @private - */ - _preloadPackage: function(info) { - var dir, - pkg; - // FUTURE: use info.inherit to scope mojit dependencies - /* - console.log('--PACKAGE-- ' + info.depth + ' ' + info.pkg.name + '@' + info.pkg.version - + ' \t' + (info.pkg.yahoo && info.pkg.yahoo.mojito && info.pkg.yahoo.mojito.type) - + ' \t[' + info.parents.join(',') + ']' - // + ' \t-- ' + Y.JSON.stringify(info.inherit) - ); - */ - pkg = { - name: info.pkg.name, - version: info.pkg.version - }; - if (0 === info.depth) { - // the actual application is handled specially - this._preloadApp(pkg); - return; - } - if (!info.pkg.yahoo || !info.pkg.yahoo.mojito) { - return; - } - switch (info.pkg.yahoo.mojito.type) { - case 'bundle': - dir = libpath.join(info.dir, info.pkg.yahoo.mojito.location); - this._preloadDirBundle(dir, pkg, false); - this._preloadDirMojits(libpath.join(dir, 'mojits'), pkg); - break; - case 'mojit': - dir = libpath.join(info.dir, info.pkg.yahoo.mojito.location); - this._preloadDirMojit(dir, pkg, info.dir); - break; - default: - logger.log('Unknown package type "' + info.pkg.yahoo.mojito.type + '"', 'warn', NAME); - break; - } - }, + /** + * Returns a serializable object used to initialized Mojito on the client. + * + * FUTURE: [issue 105] cache the output of this function + * cache key: all of ctx + * + * @method serializeClientStore + * @param {object} ctx the context + * @return {object} object that should be serialized and used to initialize MojitoClient + */ + serializeClientStore: function(ctx) { + var out = {}; + out.specs = {}; + out.mojits = {}; + out.appConfig = this.getAppConfig(ctx); + delete out.appConfig.mojitsDirs; + delete out.appConfig.mojitDirs; + delete out.appConfig.routesFiles; + delete out.appConfig.specs; - /** - * preloads metadata about resource in a directory - * - * @method _preloadDirBundle - * @param dir {string} directory path - * @param pkg {object} metadata (name and version) about the package - * @param loadConfig {boolean} whether to also preload metadata about the configuration files - * @return {nothing} work down via other called methods - * @private - */ - _preloadDirBundle: function(dir, pkg, loadConfig) { - var i, - res, - resources; - resources = this._findResourcesByConvention(dir, pkg, 'shared'); - for (i = 0; i < resources.length; i += 1) { - res = resources[i]; - switch (res.type) { - case 'config': - if (!loadConfig) { - if ('package' !== res.name) { - logger.log('config file "' + res.shortPath + '" not used here. skipping.', 'warn', NAME); - } - break; - } - this._preloadResource(res, null); - break; + out.routes = this.getRoutes(ctx); - // app-level - case 'archetype': - case 'command': - case 'middleware': - this._preloadResource(res, null); - break; + return out; + }, - // mojit-level - case 'action': - case 'addon': - case 'asset': - case 'binder': - case 'controller': - case 'model': - case 'spec': - case 'view': - case 'yui-lang': - case 'yui-module': - this._preloadResource(res, 'shared'); - break; - default: - logger.log('unknown resource type "' + res.type + '". skipping ' + res.fsPath, 'warn', NAME); - break; - } // switch - } // for each resource - }, + //==================================================================== + // PRIVATE METHODS - /** - * @method _parseResourceArchetype - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceArchetype: function(res, subtype) { - var dir, - ext, - file; + /** + * Used for unit testing. + * @private + * @method _mockLib + * @param {string} name name of library to mock out + * @param {situation-dependent} lib library to mock out + * @return {nothing} + */ + _mockLib: function(name, lib) { + this._libs[name] = lib; + }, - // archetypes don't support our ".affinity." or ".selector." filename syntax - dir = libpath.dirname(res.shortPath); - ext = libpath.extname(res.shortPath); - file = libpath.basename(res.shortPath, ext); - res.name = libpath.join(dir, file); - res.type = 'archetype'; - res.subtype = subtype; - res.id = 'archetype-' + res.subtype + '-' + res.name; - return res; - }, + /** + * @private + * @method @parseValidDims + * @param {object} dims contents of dimensions.json + * @return {object} lookup hash for dimension keys and values + */ + _parseValidDims: function(dims) { + var d, + dim, + dimName, + out = {}; + function grabKeys(dimName, o) { + var k; + for (k in o) { + if (o.hasOwnProperty(k)) { + out[dimName][k] = true; + if (Y.Lang.isObject(o[k])) { + grabKeys(dimName, o[k]); + } + } + } + } + for (d = 0; d < dims[0].dimensions.length; d += 1) { + dim = dims[0].dimensions[d]; + for (dimName in dim) { + if (dim.hasOwnProperty(dimName)) { + out[dimName] = {}; + grabKeys(dimName, dim[dimName]); + } + } + } + return out; + }, - /** - * @method _parseResourceCommand - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceCommand: function(res) { - var dir, - ext, - file; + /** + * Applies spec inheritance by following the `base` and merging up the + * results. + * @private + * @method _expandSpec + * @param {string} env the runtime environment (either `client` or `server`) + * @param {object} ctx runtime context + * @param {object} spec spec to expand + * @return {object} expanded sped + */ + // FUTURE: expose this to RS addons? + _expandSpec: function(env, ctx, spec) { + var appConfig, + base, + specParts, + mojitName, + specName, + ress; - // commands don't support our ".affinity." or ".selector." filename syntax - dir = libpath.dirname(res.shortPath); - ext = libpath.extname(res.shortPath); - file = libpath.basename(res.shortPath, ext); + if (!spec.base) { + return spec; + } - res.name = libpath.join(dir, file); - res.type = 'command'; - res.id = 'command-' + res.name; - return res; - }, + // The base will need to carry its ID with it. + spec.id = spec.base; + appConfig = this.getAppConfig(ctx); + base = appConfig.specs[spec.base]; + if (!base) { + // look in resources + specParts = spec.base.split(':'); + mojitName = specParts.shift(); + specName = specParts.join(':') || 'default'; + ress = this.getResources(env, ctx, {type: 'spec', mojit: mojitName, name: specName}); + if (1 === ress.length) { + base = this.config.readConfigYCB(ress[0].source.fs.fullPath, ctx); + } + } + if (!base) { + throw new Error('Unknown base of "' + spec.base + '"'); + } - /** - * @method _parseResourceMiddleware - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceMiddleware: function(res) { - var dir, - ext, - file; + delete spec.base; + return this.mergeRecursive(this._expandSpec(env, ctx, base), spec); + }, - // middleware doesn't support our ".affinity." or ".selector." filename syntax - dir = libpath.dirname(res.shortPath); - ext = libpath.extname(res.shortPath); - file = libpath.basename(res.shortPath, ext); - res.name = libpath.join(dir, file); - res.type = 'middleware'; - res.id = 'middleware-' + res.name; - return res; - }, + /** + * preloads metadata about resources in a package + * (but not subpackages in its `node_modules/`) + * + * @private + * @method _preloadPackage + * @param {object} info metadata about the package + * @return {nothing} + */ + _preloadPackage: function(info) { + var dir, + pkg; + // FUTURE: use info.inherit to scope mojit dependencies + /* + console.log('--PACKAGE-- ' + info.depth + ' ' + info.pkg.name + '@' + info.pkg.version + + ' \t' + (info.pkg.yahoo && info.pkg.yahoo.mojito && info.pkg.yahoo.mojito.type) + + ' \t[' + info.parents.join(',') + ']' + // + ' \t-- ' + Y.JSON.stringify(info.inherit) + ); + */ + pkg = { + name: info.pkg.name, + version: info.pkg.version, + depth: info.depth + }; + if (0 === info.depth) { + // the actual application is handled specially + this._preloadApp(pkg); + return; + } + if (!info.pkg.yahoo || !info.pkg.yahoo.mojito) { + return; + } + switch (info.pkg.yahoo.mojito.type) { + case 'bundle': + dir = this._libs.path.join(info.dir, info.pkg.yahoo.mojito.location); + this._preloadDirBundle(dir, pkg); + break; + case 'mojit': + dir = this._libs.path.join(info.dir, info.pkg.yahoo.mojito.location); + this._preloadDirMojit(dir, 'pkg', pkg); + break; + default: + Y.log('Unknown package type "' + info.pkg.yahoo.mojito.type + '"', 'warn', NAME); + break; + } + }, - /** - * @method _parseResourceConfig - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceConfig: function(res, mojitType) { - var dir, - ext, - file, - pathParts; - - // configs don't support our ".affinity." or ".selector." filename syntax - dir = libpath.dirname(res.shortPath); - ext = libpath.extname(res.shortPath); - file = libpath.basename(res.shortPath, ext); - pathParts = { - affinity: new Affinity('server'), - contextKey: '*', - contextParts: {}, - ext: ext - }; + /** + * preloads metadata about resources in the application directory + * (but not `node_modules/`) + * + * @private + * @method _preloadApp + * @param {object} pkg metadata (name and version) about the app's package + * @return {nothing} + */ + _preloadApp: function(pkg) { + var ress, + r, + res, + list, + i; - if ('.json' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - if (this._preload_routes[res.fsPath]) { - res.configType = 'routes'; - } - res.name = libpath.join(dir, file); - res.type = 'config'; - res.id = 'config-' + res.name; - res.pathParts = pathParts; - return res; - }, + ress = this._findResourcesByConvention(this._config.root, 'app', pkg, 'shared'); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if ('mojit' !== res.type) { + // ignore app-level mojits found by convention, since they'll be loaded below + this.addResourceVersion(ress[r]); + } + } + // load mojitsDirs + list = this._globList(this._config.root, this._appConfigStatic.mojitsDirs); + for (i = 0; i < list.length; i += 1) { + this._preloadDirMojits(list[i], 'app', pkg); + } - /** - * @method _parseResourceAction - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceAction: function(res, mojitType) { - var pathParts = this._parsePath(res, 'server'); - if ('.js' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - res.name = libpath.join(libpath.dirname(res.shortPath), pathParts.shortFile); - res.type = 'action'; - res.id = 'action-' + res.name; - this._precalcYuiModule(res); - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * @method _parseResourceAddon - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceAddon: function(res, mojitType, subtype) { - var pathParts = this._parsePath(res, 'server'); - if ('.js' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - res.name = libpath.join(libpath.dirname(res.shortPath), pathParts.shortFile); - res.type = 'addon'; - res.addonType = subtype; - res.id = 'addon-' + res.addonType + '-' + res.name; - this._precalcYuiModule(res); - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * @method _parseResourceAsset - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceAsset: function(res, mojitType) { - var dir, - ext, - file, - fileParts, - pathParts; - - // binders don't support our ".affinity." filename syntax - dir = libpath.dirname(res.shortPath); - ext = libpath.extname(res.shortPath); - file = libpath.basename(res.shortPath, ext); - fileParts = file.split('.'); - - pathParts = { - affinity: new Affinity('common'), - contextKey: '*', - contextParts: {}, - ext: ext - }; - if (fileParts.length >= 2) { - pathParts.contextParts.device = fileParts.pop(); - pathParts.contextKey = libqs.stringify(pathParts.contextParts); - } - pathParts.shortFile = fileParts.join('.'); - if (this._skipBadPath(pathParts)) { - return; - } - - res.name = libpath.join(dir, pathParts.shortFile) + pathParts.ext; - res.type = 'asset'; - res.assetType = pathParts.ext.substr(1); - res.id = 'asset-' + res.name; - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * @method _parseResourceBinder - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceBinder: function(res, mojitType) { - var dir, - ext, - file, - fileParts, - pathParts; - - // binders don't support our ".affinity." filename syntax - dir = libpath.dirname(res.shortPath); - ext = libpath.extname(res.shortPath); - file = libpath.basename(res.shortPath, ext); - fileParts = file.split('.'); - - pathParts = { - affinity: new Affinity('client'), - contextKey: '*', - contextParts: {}, - ext: ext - }; - if (fileParts.length >= 2) { - pathParts.contextParts.device = fileParts.pop(); - pathParts.contextKey = libqs.stringify(pathParts.contextParts); - } - pathParts.shortFile = fileParts.join('.'); - if ('.js' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - - res.name = libpath.join(libpath.dirname(res.shortPath), pathParts.shortFile); - res.type = 'binder'; - res.id = 'binder-' + res.name; - this._precalcYuiModule(res); - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * @method _parseResourceController - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceController: function(res, mojitType) { - var pathParts = this._parsePath(res, 'server'); - if ('.js' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - res.name = 'controller'; - res.type = 'controller'; - res.id = 'controller'; - this._precalcYuiModule(res); - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * @method _parseResourceModel - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceModel: function(res, mojitType) { - var pathParts = this._parsePath(res, 'server'); - if ('.js' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - res.name = libpath.join(libpath.dirname(res.shortPath), pathParts.shortFile); - res.type = 'model'; - res.id = 'model-' + res.name; - this._precalcYuiModule(res); - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * @method _parseResourceSpec - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceSpec: function(res, mojitType) { - var dir, - ext, - file, - pathParts, - appConfig, - prefix; - - // specs don't support our ".affinity." or ".selector." filename syntax - dir = libpath.dirname(res.shortPath); - ext = libpath.extname(res.shortPath); - file = libpath.basename(res.shortPath, ext); - pathParts = { - affinity: new Affinity('server'), - contextKey: '*', - contextParts: {}, - ext: ext - }; - pathParts.shortFile = file; - if ('.json' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - - res.name = libpath.join(libpath.dirname(res.shortPath), pathParts.shortFile); - res.type = 'spec'; - res.id = 'spec-' + res.name; - - appConfig = this._appConfigStatic.staticHandling || {}; - prefix = '/static'; - if (typeof appConfig.prefix !== 'undefined') { - prefix = appConfig.prefix ? '/' + appConfig.prefix : ''; - } - - // namespaced by mojitType - res.specName = mojitType; - if (res.name !== 'default') { - res.specName += ':' + res.name; - } - - res.dynamicHandlerURL = prefix + '/' + mojitType + '/specs/' + res.shortPath; - return res; - }, - - - /** - * @method _parseResourceView - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceView: function(res, mojitType) { - var pathParts, - fileParts; - // views don't support our ".affinity." filename syntax - pathParts = { - affinity: new Affinity('common'), - contextKey: '*', - contextParts: {}, - ext: libpath.extname(res.shortPath) - }; - fileParts = libpath.basename(res.shortPath).split('.'); - res.viewOutputFormat = fileParts.pop(); - res.viewEngine = fileParts.pop(); - if (fileParts.length >= 2) { - pathParts.contextParts.device = fileParts.pop(); - pathParts.contextKey = libqs.stringify(pathParts.contextParts); - } - pathParts.shortFile = fileParts.join('.'); - - if (fileParts.length !== 1) { - logger.log('invalid view filename. skipping ' + res.fsPath, 'warn', NAME); - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - - res.name = libpath.join(libpath.dirname(res.shortPath), pathParts.shortFile); - res.type = 'view'; - res.id = 'view-' + res.name; - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * @method _parseResourceYuiLang - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceYuiLang: function(res, mojitType) { - var pathParts, - shortName; - // language bundles don't support our ".affinity." filename syntax - pathParts = { - affinity: new Affinity('common'), - contextKey: '*', - contextParts: {}, - ext: libpath.extname(res.shortPath) - }; - if ('.js' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - pathParts.shortFile = libpath.basename(res.shortPath, pathParts.ext); - if (pathParts.shortFile === mojitType) { - res.langCode = ''; - } else if (mojitType === pathParts.shortFile.substr(0, mojitType.length)) { - res.langCode = pathParts.shortFile.substr(mojitType.length + 1); - } else { - logger.log('invalid YUI lang file format. skipping ' + res.fsPath, 'error', NAME); - return; - } - if (res.langCode) { - pathParts.contextParts.lang = res.langCode; - pathParts.contextKey = libqs.stringify(pathParts.contextParts); - } - - res.name = res.langCode; - res.type = 'yui-lang'; - res.id = 'yui-lang-' + res.langCode; - - if (!this._mojitLangs[mojitType]) { - this._mojitLangs[mojitType] = []; - } - if (res.langCode) { - this._mojitLangs[mojitType].push(res.langCode); - } - - this._precalcYuiModule(res); - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * @method _parseResourceYuiModule - * @param res {object} partial resource - * @return {object|null} parsed resource, or null if shouldn't be used - * @private - */ - _parseResourceYuiModule: function(res, mojitType) { - var pathParts = this._parsePath(res); - if ('.js' !== pathParts.ext) { - return; - } - if (this._skipBadPath(pathParts)) { - return; - } - - if (pathParts.affinity.type) { - // This allows the app config to specify that some "client" affinity - // code should not be deployed if it has an "optional" subtype - if (this._appConfigStatic.deferAllOptionalAutoloads && - pathParts.affinity.type === 'optional') { - pathParts.affinity = 'none'; - } else if (pathParts.affinity.type === 'tests' && - this._appConfigStatic.env !== 'test') { - // this filters tests from being included with deployed - // applications - pathParts.affinity = 'none'; - } - } - if ('none' === pathParts.affinity) { - return; - } - - this._precalcYuiModule(res); - res.name = res.yuiModuleName; - res.type = 'yui-module'; - res.id = 'yui-module-' + res.name; - this._precalcStaticURL(res, mojitType); - res.pathParts = pathParts; - return res; - }, - - - /** - * preloads a directory containing many mojits - * - * @method _preloadDirMojits - * @param dir {string} directory path - * @param pkg {object} metadata (name and version) about the package - * @return {nothing} work down via other called methods - * @private - */ - _preloadDirMojits: function(dir, pkg) { - var i, - realDirs, - children, - childName, - childPath; - - if ('/' !== dir.charAt(0)) { - dir = libpath.join(this._root, dir); - } - - // handle globbing - if (dir.indexOf('*') >= 0) { - realDirs = libglob.sync(dir, {}); - if (!realDirs.length) { - logger.log('Failed to find any mojitsDirs matching ' + dir, 'error', NAME); - return; - } - for (i = 0; i < realDirs.length; i += 1) { - this._preloadDirMojits(realDirs[i], pkg); - } - return; - } - - if (!this._libs.path.existsSync(dir)) { - return; - } - - children = this._sortedReaddirSync(dir); - for (i = 0; i < children.length; i += 1) { - childName = children[i]; - if ('.' === childName.substring(0, 1)) { - continue; - } - childPath = libpath.join(dir, childName); - this._preloadDirMojit(childPath, pkg, childPath); - } - }, - - - /** - * preloads a directory that represents a single mojit - * - * @method _preloadDirMojit - * @param dir {string} directory path - * @param pkg {object} metadata (name and version) about the package - * @param pkgDir {string} directory of the packaging for this mojit - * @return {nothing} work down via other called methods - * @private - */ - _preloadDirMojit: function(dir, pkg, pkgDir) { - var i, - realDirs, - resources, - res, - mojitType, - packageJson, - definitionJson, - appConfig, - prefix, - url; - - if ('/' !== dir.charAt(0)) { - dir = libpath.join(this._root, dir); - } - - // handle globbing - if (dir.indexOf('*') >= 0) { - realDirs = libglob.sync(dir, {}); - if (!realDirs.length) { - logger.log('Failed to find any mojitDirs matching ' + dir, 'error', NAME); - return; - } - for (i = 0; i < realDirs.length; i += 1) { - this._preloadDirMojit(realDirs[i], pkg, pkgDir); - } - return; - } - - if (!this._libs.path.existsSync(dir)) { - return; - } - - mojitType = libpath.basename(dir); - packageJson = this._readMojitConfigFile(libpath.join(pkgDir, 'package.json'), false); - if (packageJson) { - if (packageJson.name) { - mojitType = packageJson.name; - } - if (pkg.name !== 'mojito') { - // TODO: deprecate. NPM "engine" is better - if (!this._mojitoVersionMatch(packageJson, this._version)) { - logger.log('Mojito version mismatch: mojit skipped in "' + - dir + '"', 'warn', NAME); - return; - } - - this._mojitPackageAsAsset(dir, mojitType, packageJson); - } - } - this._mojitPaths[mojitType] = dir; - - definitionJson = this._readMojitConfigFile(libpath.join(dir, 'definition.json'), true); - if (definitionJson.appLevel) { - mojitType = 'shared'; - } - - if ('shared' !== mojitType) { - // TODO: [Issue 109] re-use logic from _precalcStaticURL() for - // prefix (so that all options are supported) - appConfig = this._appConfigStatic.staticHandling || {}; - prefix = '/static'; - if (typeof appConfig.prefix !== 'undefined') { - prefix = appConfig.prefix ? '/' + appConfig.prefix : ''; - } - url = prefix + '/' + mojitType + '/definition.json'; - this._dynamicURLs[url] = libpath.join(dir, 'definition.json'); - } - - resources = this._findResourcesByConvention(dir, pkg, mojitType); - for (i = 0; i < resources.length; i += 1) { - res = resources[i]; - switch (res.type) { - // app-level - case 'archetype': - case 'command': - case 'middleware': - logger.log('app-level resources not allowed here. skipping ' + res.fsPath, 'warn', NAME); - this._preloadResource(res, mojitType); - break; - - // mojit-level - case 'action': - case 'addon': - case 'asset': - case 'binder': - case 'controller': - case 'model': - case 'spec': - case 'view': - case 'yui-lang': - case 'yui-module': - this._preloadResource(res, mojitType); - break; - case 'config': - if ('shared' !== mojitType) { - this._preloadResource(res, mojitType); - } - break; - - default: - logger.log('unknown resource type "' + res.type + '". skipping ' + res.fsPath, 'warn', NAME); - break; - } // switch - } - }, - - - /** - * Finds resources based on our conventions - * -doesn't- load mojits or their contents. That's done elsewhere. - * - * actions/{name}.**.js - * addons/{subtype}/{name}.**.js - * archetypes/{subtype}/{name}/ - * assets/{everything} - * binders/{name}.**.js - * commands/{name}.js - * controller.**.js - * lang/{name}.**.js - * middleware/{name}.js - * models/{name}.**.js - * views/{name}.**.{ext} - * yui_modules/{name}.**.js - * - * @method _findResourcesByConvention - * @param dir {string} directory from which to find resources - * @param pkg {object} metadata (name and version) about the package - * @param mojitType {string|null} name of mojit to which the resource belongs - * @return {array} list of resources - * @private - */ - _findResourcesByConvention: function(dir, pkg, mojitType) { - var me = this, - resources = []; - //console.log('-- FIND RESOURCES BY CONVENTION -- ' + pkg.name + '@' + pkg.version + ' -- ' + mojitType); - - this._walkDirRecursive(dir, function(error, subdir, file, isFile) { - var pathParts, - fileParts, - ext, - subtype, - res; - - if ('node_modules' === file) { - return false; - } - if ('libs' === file) { - return false; - } - if ('tests' === file && 'test' !== me._appConfigStatic.env) { - return false; - } - - pathParts = libpath.join(subdir, file).split('/'); - if (!isFile) { - - // mojits are loaded another way later - if ('.' === subdir && 'mojits' === file) { - return false; - } - - if (pathParts.length === 3 && 'archetypes' === pathParts[0]) { - subtype = pathParts[1]; - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(2).join('/') - }; - res = me._parseResourceArchetype(res, subtype); - if (res) { - resources.push(res); - } - // no need to recurse into the archetype at this time - return false; - } - - // otherwise, just recurse - return true; - } - - fileParts = file.split('.'); - ext = fileParts[fileParts.length - 1]; - - if ('.' === subdir && 'json' === ext) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: libpath.join(subdir, file) - }; - res = me._parseResourceConfig(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'commands' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceCommand(res); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'middleware' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceMiddleware(res); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'actions' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceAction(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 3 && 'addons' === pathParts[0]) { - subtype = pathParts[1]; - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(2).join('/') - }; - res = me._parseResourceAddon(res, mojitType, subtype); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'assets' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceAsset(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'binders' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceBinder(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if ('.' === subdir && 'controller' === fileParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: libpath.join(subdir, file) - }; - res = me._parseResourceController(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'lang' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceYuiLang(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'models' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceModel(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'specs' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceSpec(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && 'views' === pathParts[0]) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceView(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - if (pathParts.length >= 2 && ('yui_modules' === pathParts[0] - || 'autoload' === pathParts[0] - || 'tests' === pathParts[0])) { - res = { - pkg: pkg, - fsPath: libpath.join(dir, subdir, file), - shortPath: pathParts.slice(1).join('/') - }; - res = me._parseResourceYuiModule(res, mojitType); - if (res) { - resources.push(res); - } - return; - } - - // unknown file, just skip. (this includes config files which are - // handled separately.) - return; - }); - - return resources; - }, - - - /** - * @method _parsePath - * @param res {object} partial resource - * @return {object} metadata: ext, shortFile, affinity, contextKey, and contextParts - * @private - */ - _parsePath: function(res, defaultAffinity) { - var out = {}, - fileParts, - device; - - out.contextKey = '*'; - out.contextParts = {}; - out.affinity = defaultAffinity || 'server'; - out.ext = libpath.extname(res.shortPath); - - fileParts = libpath.basename(res.shortPath, out.ext).split('.'); - if (fileParts.length >= 3) { - device = fileParts.pop(); - } - if (fileParts.length >= 2) { - out.affinity = fileParts.pop(); - } - out.shortFile = fileParts.join('.'); - - out.affinity = new Affinity(out.affinity); - if (device) { - out.contextParts.device = device; - } - if (!this._objectIsEmpty(out.contextParts)) { - out.contextKey = libqs.stringify(out.contextParts); - } - return out; - }, - - - /** - * utility that registers the resource for later parts of the algorithm - * - * @method _preloadResource - * @param res {object} metadata about the resource - * @param mojitType {string} which mojit, if applicatable - * @return {nothing} - * @private - */ - _preloadResource: function(res, mojitType) { - var dest, - config, - using, - skipping; - - if ('spec' === res.type) { - config = this._readConfigYCB({}, res.fsPath); - if (!this._appConfigStatic.specs) { - this._appConfigStatic.specs = {}; - } - this._appConfigStatic.specs[res.specName] = config; - this._dynamicURLs[res.dynamicHandlerURL] = res.fsPath; - return; - } - - // non-contextualized resources - // (... which are most app-level resources) - if (!res.pathParts) { - this._appMetaNC[res.id] = res; - return; - } - - if (!mojitType) { - dest = this._preload.appMeta; - } else if ('shared' === mojitType) { - res.sharedMojit = true; - dest = this._preload.sharedMeta; - } else { - if (!this._preload.mojitMeta[mojitType]) { - this._preload.mojitMeta[mojitType] = {}; - } - dest = this._preload.mojitMeta[mojitType]; - } - - if (!dest[res.id]) { - dest[res.id] = {}; - } - dest = dest[res.id]; - if (!dest[res.pathParts.affinity]) { - dest[res.pathParts.affinity] = {}; - } - dest = dest[res.pathParts.affinity]; - if (!dest.contexts) { - dest.contexts = {}; - } - if (dest[res.pathParts.contextKey]) { - using = 'from pkg ' + dest[res.pathParts.contextKey].pkg.name + '@' + - dest[res.pathParts.contextKey].pkg.version; - skipping = 'from pkg ' + res.pkg.name + '@' + res.pkg.version; - if (using === skipping) { - using = dest[res.pathParts.contextKey].shortPath; - skipping = res.shortPath; - } - if (using === skipping) { - using = dest[res.pathParts.contextKey].fsPath; - skipping = res.fsPath; - } - logger.log('ALREADY EXISTS: ' + res.type + ' ' + res.shortPath + - (mojitType ? ' -- in mojit ' + mojitType : '') + - ' -- using ' + using + ' -- skipping ' + skipping, - 'info', NAME); - return; - } - dest.contexts[res.pathParts.contextKey] = res.pathParts.contextParts; - dest[res.pathParts.contextKey] = res; - delete res.pathParts; - - if (res.staticHandlerURL) { - this._staticURLs[res.staticHandlerURL] = res.staticHandlerFsPath; - delete res.staticHandlerFsPath; - } - }, - - - /** - * Note: this MUST be called before _parseResourceSpec() - * - * Generates URL's about each spec in application.json - * - * @method _urlsForAppSpecs - * @return {nothing} - * @private - */ - _urlsForAppSpecs: function() { - var specs = this._appConfigStatic.specs || {}, - appConfig, - prefix, - id, - url; - - appConfig = this._appConfigStatic.staticHandling || {}; - prefix = '/static'; - if (typeof appConfig.prefix !== 'undefined') { - prefix = appConfig.prefix ? '/' + appConfig.prefix : ''; - } - - for (id in specs) { - if (specs.hasOwnProperty(id)) { - // Set the URL of the spec - // TODO: use appconfig.staticHandling.appName - url = prefix + '/' + id + '/specs/default.json'; - this._dynamicURLs[url] = 'application.json'; - } - } - }, - - - /** - * prereads the configuration file, if possible - * (configuration files in YCB format cannot be preread) - * - * @method _prereadConfigs - * @param src {object} contextualized resources - * @return {nothing} - * @private - */ - _prereadConfigs: function(src) { - var ctxKey, - res, - resid; - - if ((!src) || (!Object.keys(src))) { - return; - } - for (ctxKey in src.contexts) { - if (src.contexts.hasOwnProperty(ctxKey)) { - for (resid in src[ctxKey]) { - if (src[ctxKey].hasOwnProperty(resid)) { - res = src[ctxKey][resid]; - if ('config' === res.type && - !this._jsonCache[res.fsPath]) { - //logger.log('prereading ' + res.fsPath, 'info', - // NAME); - this._jsonCache[res.fsPath] = - this._readConfigJSON(res.fsPath); - } - } - } - } - } - }, - - - /** - * Reads and parses a JSON file - * - * @method _readConfigJSON - * @param fullpath {string} path to JSON file - * @return {mixed} contents of JSON file - * @private - */ - _readConfigJSON: function(fullpath) { - //logger.log('_readConfigJSON(' + fullpath + ')'); - var json, - contents = this._libs.fs.readFileSync(fullpath, 'utf-8'); - - try { - json = Y.JSON.parse(contents); - } catch (e) { - logger.log(this._reportJavaScriptSyntaxErrors(contents, fullpath), - 'warn', NAME); - throw new Error('Error parsing JSON file: ' + fullpath); - } - return json; - }, - - - /** - * Create a lookup table for validating YCB dimensions and values. The - * table looks like this: - * - *
-     * {
-     *   "dim1": {
-     *     "val1": null,
-     *     "val2": null,
-     *     ...
-     *   },
-     *   ...
-     * }
-     * 
- * - * @method _precalcValidYCBDimensions - * @param dimensions {object} Top-level YCB "dimensions" object - * @return object - * @private - */ - _precalcValidYCBDimensions: function(dimensions) { - var validDims = {}, - name, - i; - - for (i = 0; i < dimensions.length; i += 1) { - for (name in dimensions[i]) { - if (dimensions[i].hasOwnProperty(name)) { - validDims[name] = {}; - this._flattenYCBDimension(validDims[name], - dimensions[i][name]); - } - } - } - - return validDims; - }, - - - /** - * Flatten the keys in a nested structure into a single object. The first - * argument is modified. All values are set to null. - * - * @method _flattenYCBDimensions - * @param keys {object} The accumulator for keys. - * @param obj {object} - * @private - */ - _flattenYCBDimension: function(keys, obj) { - var name; - - for (name in obj) { - if (obj.hasOwnProperty(name)) { - keys[name] = null; - if (typeof obj[name] === 'object') { - this._flattenYCBDimension(keys, obj[name]); - } - } - } - }, - - - /** - * Return a context that contains only valid dimensions and values. - * - * @method _getValidYCBContext - * @param ctx {object} runtime context - * @return {object} filtered runtime context - * @private - */ - _getValidYCBContext: function(ctx) { - var validDims = this._validYCBDims, - validCtx = {}, - name, - value; - - for (name in ctx) { - if (ctx.hasOwnProperty(name)) { - value = ctx[name]; - if (validDims[name] && validDims[name].hasOwnProperty(value)) { - validCtx[name] = value; - } - } - } - return validCtx; - }, - - - /** - * reads a configuration file that is in YCB format - * - * @method _readConfigYCB - * @param ctx {object} runtime context - * @param fullpath {string} path to the YCB file - * @param isAppConfig {boolean} indicates whether the file being read is the application.json - * @return {object} the contextualized configuration - * @private - */ - _readConfigYCB: function(ctx, fullpath, isAppConfig) { - var cacheKey, - appConfigStatic, - jsonCache = this._jsonCache, - json, - ycbCache = this._ycbCache, - ycb; - - ctx = this._getValidYCBContext(ctx); - - //cache key only needs to account for dynamic context - cacheKey = Y.JSON.stringify(ctx); - - //logger.log('_readConfigYCB('+fullpath+')', 'mojito', NAME); - - if (!fullpath) { - //logger.log('unknown fullpath', 'mojito', NAME); - return {}; - } - - if (!ycbCache[fullpath]) { - ycbCache[fullpath] = {}; - } - - ycb = ycbCache[fullpath][cacheKey]; - if (!ycb) { - json = jsonCache[fullpath]; - if (!json) { - json = this._readConfigJSON(fullpath); - jsonCache[fullpath] = json; - } - json = this._ycbDims.concat(json); - - ctx = this._mergeRecursive(this._cloneObj(this._defaultYCBContext), - ctx); - - // libycb.read() will distructively modify its first argument - ycb = libycb.read(this._cloneObj(json), ctx); - if (isAppConfig) { - appConfigStatic = this._cloneObj(this._appConfigStatic); - ycb = this._mergeRecursive(appConfigStatic, ycb); + // load mojitDirs + list = this._globList(this._config.root, this._appConfigStatic.mojitDirs || []); + for (i = 0; i < list.length; i += 1) { + this._preloadDirMojit(list[i], 'app', pkg); } - ycbCache[fullpath][cacheKey] = ycb; - } - return ycb; - }, + }, - /** - * Reads a configuration file for a mojit - * - * @method _readMojitConfigFile - * @param path {string} path to the file - * @param ycb {boolean} indicates whether the file should be read using the YCB library - * @return {object} the configuration - * @private - */ - _readMojitConfigFile: function(path, ycb) { - //logger.log('_readMojitConfigFile(' + path + ',' + ycb + ')'); - var json, - contents; + /** + * preloads metadata about resources in a directory + * + * @private + * @method _preloadDirBundle + * @param {string} dir directory path + * @param {object} pkg metadata (name and version) about the package + * @return {nothing} + */ + _preloadDirBundle: function(dir, pkg) { + var ress, + r, + res; + // FUTURE: support configuration too + + ress = this._findResourcesByConvention(dir, 'bundle', pkg, 'shared'); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + this.addResourceVersion(res); + } + this._preloadDirMojits(this._libs.path.join(dir, 'mojits'), 'bundle', pkg); + }, + + + /** + * preloads a directory containing many mojits + * + * @private + * @method _preloadDirMojits + * @param {string} dir directory path + * @param {string} dirType type represented by the "dir" argument. values are "app", "bundle", "pkg", or "mojit" + * @param {object} pkg metadata (name and version) about the package + * @return {nothing} + */ + _preloadDirMojits: function(dir, dirType, pkg) { + var i, + realDirs, + children, + childName, + childPath; - if (!this._libs.path.existsSync(path)) { - return {}; - } - if (ycb) { - return this._readConfigYCB({}, path); - } - try { - contents = this._libs.fs.readFileSync(path, 'utf-8'); - json = Y.JSON.parse(contents); - } catch (e) { - logger.log(this._reportJavaScriptSyntaxErrors(contents, path), - 'warn', NAME); - throw new Error('Error reading or parsing JSON file: ' + path); - } - return json; - }, - - - /** - * Registers the mojit's package.json as a static resource - * - * @method _mojitPackageAsAsset - * @param dir {string} directory of mojit - * @param mojitType {string} name of mojit - * @param packageJson {object} contents of mojit's package.json - * @return {nothing} - * @private - */ - _mojitPackageAsAsset: function(dir, mojitType, packageJson) { - var pkg, - fakeResource; - - // FUTURE: deprecate config.mojito in package.json - pkg = (packageJson.yahoo && packageJson.yahoo.mojito && - packageJson.yahoo.mojito['package']) || - (packageJson.config && packageJson.config.mojito && - packageJson.config.mojito['package']); - - if (pkg === 'public') { - // We have to check if the "package.json" files wants to do this - fakeResource = { - type: 'package', - fsPath: libpath.join(dir, 'package.json'), - shortPath: 'package.json' - }; - this._precalcStaticURL(fakeResource, mojitType); - if (fakeResource.staticHandlerURL) { - this._staticURLs[fakeResource.staticHandlerURL] = - fakeResource.staticHandlerFsPath; + if ('/' !== dir.charAt(0)) { + dir = this._libs.path.join(this._config.root, dir); } - } - }, - - - /** - * Checks to see if the version of Mojito specified in a mojit's - * package.json matches the current verison of Mojito. - * - * @method _mojitoVersionMatch - * @param pack {object} contents of the mojit's package.json file - * @param version {string} current version of mojito - * @return {boolean} returns true if the mojit can be used - * @private - */ - _mojitoVersionMatch: function(pack, version) { - var packageVersion; - - // There was no package.json file so assume version is OK - if (Object.keys(pack).length === 0) { - return true; - } - - // If we have a version make sure it is less than the given one - packageVersion = (pack.yahoo && pack.yahoo.mojito && - pack.yahoo.mojito.version) || - (pack.config && pack.config.mojito && pack.config.mojito.version); - if (packageVersion) { - if (packageVersion === '*') { - return true; + if (!this._libs.path.existsSync(dir)) { + return; } - // TODO: [Issue 95] Put real version checking code here. - return packageVersion <= version; - } - // No version is set so return false as we don't know what this is - return false; - }, + children = this._sortedReaddirSync(dir); + for (i = 0; i < children.length; i += 1) { + childName = children[i]; + if ('.' === childName.substring(0, 1)) { + continue; + } + childPath = this._libs.path.join(dir, childName); + this._preloadDirMojit(childPath, dirType, pkg); + } + }, - /** - * Takes the preloaded info and resolves ("cooks down") affinity, etc. - * - * This function is a doozy. This is where all the magic happens as far - * as which version of each resource is used. The results are stored in - * this._fwMeta, _appMeta, _mojitMeta, etc. The primary key of these is - * the environment ("client" or "server"). The secondary key is the - * context key -- a string representation of the partial context. - * - * We do that to have fast lookup during runtime. - * - * The algorithm chooses first from the most specific source: mojit- - * specific has higher precedence than shared. Within each of those, - * the environment-specific version ("client" or "server") has higher - * precedence than the "common" affinity. - * - * We do this for each context key (partial context). We resolve - * context inheritance (for example no-context versus device=iphone) - * at runtime (in getMojitTypeDetails()). - * - * (Half of the above algorithm is implemented here, and half in - * _cookdownMerge() which is a utility for this method.) - * - * @method _cookdown - * @return {nothing} - * @private - */ - _cookdown: function() { - //logger.log('_cookdown()'); - var env, - envs = ['client', 'server'], - type, // mojit type - i, - merged, // results of _cookdownMerge() - resid, - resids, // array (really object keys) of all resource IDs - ctxKey; - - // example of _preload.mojitMeta: - // [type][resid][affinity] = { - // "contexts": { "device=iphone": { "device":"iphone"} }, - // "device=iphone": { - // "type": ... - // "fsPath": ... - // } - // } - // resid was generated during _preloadFile*(), for example: - // controller is 'controller' - // model is 'model-foo' - // view is 'view-index' - // - // TODO 2011-06-16: [Issue 109] break down into smaller pieces - // (at least for more fine-grained unit testing). - for (type in this._preload.mojitMeta) { - if (this._preload.mojitMeta.hasOwnProperty(type)) { - // TODO: LINT [Issue 110] detect dupe resids. - resids = {}; - - // need resids from all sources - for (resid in this._preload.sharedMeta) { - if (this._preload.sharedMeta.hasOwnProperty(resid)) { - resids[resid] = true; - } - } - for (resid in this._preload.mojitMeta[type]) { - if (this._preload.mojitMeta[type].hasOwnProperty(resid)) { - resids[resid] = true; - } - } + /** + * preloads a directory that represents a single mojit + * + * @private + * @method _preloadDirMojit + * @param {string} dir directory path + * @param {string} dirType type represented by the "dir" argument. values are "app", "bundle", "pkg", or "mojit" + * @param {object} pkg metadata (name and version) about the package + * @return {nothing} + */ + _preloadDirMojit: function(dir, dirType, pkg) { + var mojitType, + packageJson, + definitionJson, + ress, + r, + res; - for (resid in resids) { - if (resids.hasOwnProperty(resid)) { - for (i = 0; i < envs.length; i += 1) { - env = envs[i]; - merged = this._cookdownMerge(env, [ - // ORDER IS IMPORTANT - this._preload.sharedMeta[resid], - this._preload.mojitMeta[type][resid] - ]); - if (merged) { - if (!this._mojitMeta[env]) { - this._mojitMeta[env] = {}; - } - if (!this._mojitMeta[env][type]) { - this._mojitMeta[env][type] = {}; - } - if (!this._mojitMeta[env][type].contexts) { - this._mojitMeta[env][type].contexts = {}; - } - for (ctxKey in merged.contexts) { - if (merged.contexts. - hasOwnProperty(ctxKey)) { - this._mojitMeta[env][type - ].contexts[ctxKey] = - merged.contexts[ctxKey]; - if (!this._mojitMeta[env - ][type][ctxKey]) { - this._mojitMeta[env - ][type][ctxKey] = {}; - } - // TODO: give example of - // data structure. - this._mojitMeta[env - ][type][ctxKey][resid] = - merged[ctxKey]; - } - } - } // have merged - } // foreach env - } - } // foreach resid - } - } // foreach type - - for (resid in this._preload.appMeta) { - if (this._preload.appMeta.hasOwnProperty(resid)) { - for (i = 0; i < envs.length; i += 1) { - env = envs[i]; - merged = this._cookdownMerge(env, - [this._preload.appMeta[resid]]); - if (merged) { - if (!this._appMeta[env]) { - this._appMeta[env] = {}; - } - if (!this._appMeta[env].contexts) { - this._appMeta[env].contexts = {}; - } - for (ctxKey in merged.contexts) { - if (merged.contexts.hasOwnProperty(ctxKey)) { - this._appMeta[env].contexts[ctxKey] = - merged.contexts[ctxKey]; - if (!this._appMeta[env][ctxKey]) { - this._appMeta[env][ctxKey] = {}; - } - this._appMeta[env][ctxKey][resid] = - merged[ctxKey]; - } - } - } - } + if ('/' !== dir.charAt(0)) { + dir = this._libs.path.join(this._config.root, dir); } - } - for (resid in this._preload.sharedMeta) { - if (this._preload.sharedMeta.hasOwnProperty(resid)) { - for (i = 0; i < envs.length; i += 1) { - env = envs[i]; - merged = this._cookdownMerge(env, - [this._preload.sharedMeta[resid]]); - if (merged) { - if (!this._sharedMeta[env]) { - this._sharedMeta[env] = {}; - } - if (!this._sharedMeta[env].contexts) { - this._sharedMeta[env].contexts = {}; - } - for (ctxKey in merged.contexts) { - if (merged.contexts.hasOwnProperty(ctxKey)) { - this._sharedMeta[env].contexts[ctxKey] = - merged.contexts[ctxKey]; - if (!this._sharedMeta[env][ctxKey]) { - this._sharedMeta[env][ctxKey] = {}; - } - this._sharedMeta[env][ctxKey][resid] = - merged[ctxKey]; - } - } - } - } + if (!this._libs.path.existsSync(dir)) { + return; } - } - - delete this._preload_routes; - delete this._preload; - }, - - /** - * This is a utility for _cookdown(). See docs on that for details. - * - * The general idea is that we start with the lowest priority items - * and let higher priority items clobber. - * - * @method _cookdownMerge - * @param env {string} "client" or "server" - * @param srcs {array} ORDER MATTERS! list of resources to merge - * @return {TODO} TODO - * @private - */ - _cookdownMerge: function(env, srcs) { - var merged = { contexts: {} }, - affinities = ['common', env], // priority order - s, - src, - lastS = srcs.length - 1, - found = false, // TODO: when is found==false? - a, - affinity, - ctx, - res, - ctxKey; - - for (s = 0; s < srcs.length; s += 1) { - src = srcs[s]; - if (!src) { - continue; + if ('pkg' === dirType) { + mojitType = pkg.name; + } else { + mojitType = this._libs.path.basename(dir); } - for (a = 0; a < affinities.length; a += 1) { - affinity = affinities[a]; - if (!src[affinity]) { - continue; + packageJson = this.config.readConfigJSON(this._libs.path.join(dir, 'package.json')); + if (packageJson) { + if (packageJson.name) { + mojitType = packageJson.name; } - for (ctxKey in src[affinity].contexts) { - if (src[affinity].contexts.hasOwnProperty(ctxKey)) { - merged.contexts[ctxKey] = - src[affinity].contexts[ctxKey]; - res = this._cloneObj(src[affinity][ctxKey]); - if (('config' === res.type) && (s !== lastS)) { - // only pull in configs from the last source - continue; - } - merged.type = res.type; - merged[ctxKey] = res; - found = true; + + if (packageJson.engines && packageJson.engines.mojito) { + if (!this._libs.semver.satisfies(mojitoVersion, packageJson.engines.mojito)) { + Y.log('skipping mojit because of version check ' + dir, 'warn', NAME); + return; } } - } - } - if (!found) { - return null; - } - return merged; - }, - - /** - * Calculates, at server start time, the YUI module dependencies - * for mojit controllers and binders - * - * @method _precalcYuiDependencies - * @return {nothing} - * @private - */ - _precalcYuiDependencies: function() { - var e, - i, - env, - envs = ['client', 'server'], - mojitType, - ctxKey, - module, - parts, - required, - resid, - res, - sorted, - ctxs; - - for (e = 0; e < envs.length; e += 1) { - env = envs[e]; - - // mojit-specific - // -------------- - if (!this._mojitYuiSorted[env]) { - this._mojitYuiRequired[env] = {}; - this._mojitYuiSorted[env] = {}; - this._mojitYuiSortedPaths[env] = {}; + // TODO: register mojit's package.json as a static asset, in "static handler" plugin } - for (mojitType in this._mojitMeta[env]) { - if (this._mojitMeta[env].hasOwnProperty(mojitType)) { - - if (!this._mojitYuiSorted[env][mojitType]) { - this._mojitYuiRequired[env][mojitType] = {}; - this._mojitYuiSorted[env][mojitType] = {}; - this._mojitYuiSortedPaths[env][mojitType] = {}; - } - for (ctxKey in this._mojitMeta[env][mojitType].contexts) { - if (this._mojitMeta[env - ][mojitType].contexts.hasOwnProperty(ctxKey)) { - - // we handle non-context version below - if ('*' === ctxKey) { - continue; - } - if (!this._mojitYuiSorted[env - ][mojitType].contexts) { - this._mojitYuiRequired[env - ][mojitType].contexts = {}; - this._mojitYuiSorted[env - ][mojitType].contexts = {}; - this._mojitYuiSortedPaths[env - ][mojitType].contexts = {}; - } - - parts = {}; - this._precalcYuiDependencies_getDepParts(env, - this._mojitMeta[env][mojitType]['*'], parts); - this._precalcYuiDependencies_getDepParts(env, - this._mojitMeta[env][mojitType][ctxKey], parts); - if (parts.controller && - parts.modules['inlinecss/' + mojitType]) { - parts.modules[parts.controller.yuiModuleName]. - requires.push('inlinecss/' + mojitType); - } - this._mojitYuiSorted[env - ][mojitType].contexts[ctxKey] = - this._mojitMeta[env - ][mojitType].contexts[ctxKey]; - this._mojitYuiRequired[env - ][mojitType].contexts[ctxKey] = - this._mojitMeta[env - ][mojitType].contexts[ctxKey]; - if (parts.controller) { - parts.required[parts.controller.yuiModuleName] = - true; - // dependencies necessary to dispatch the mojit - parts.required['mojito-dispatcher'] = true; - sorted = this._sortYUIModules( - this._mojitMeta[env][mojitType - ].contexts[ctxKey], - env, - this._appConfigStatic.yui, - mojitType, - parts.modules, - parts.required - ); - this._mojitYuiRequired[env - ][mojitType][ctxKey] = - Object.keys(parts.required); - this._mojitYuiSorted[env - ][mojitType][ctxKey] = sorted.sorted; - this._mojitYuiSortedPaths[env - ][mojitType][ctxKey] = sorted.paths; - } - - // also calculate sortedPaths for each individual binder - if ('client' === env) { - for (resid in this._mojitMeta[env - ][mojitType][ctxKey]) { - if (this._mojitMeta[env - ][mojitType][ctxKey - ].hasOwnProperty(resid)) { - res = this._mojitMeta[env - ][mojitType][ctxKey][resid]; - if (res.type !== 'binder') { - continue; - } - required = {}; - required[res.yuiModuleName] = true; - // all binders have this dependency, - // even if not explicitly given - required['mojito-client'] = true; - // view engines are needed to support - // mojitProxy.render() - for (i in parts.viewEngines) { - if (parts.viewEngines. - hasOwnProperty(i)) { - required[parts.viewEngines[i]] = - true; - } - } - sorted = this._sortYUIModules( - this._mojitMeta[env][mojitType - ].contexts[ctxKey], - env, - this._appConfigStatic.yui, - mojitType, - parts.modules, - required - ); - res.yuiSortedPaths = sorted.paths; - } - } - } // env==client - } - } // foreach context (except '*') - - // here's where we handle the non-context version - if (this._mojitMeta[env][mojitType]['*']) { - if (!this._mojitYuiSorted[env][mojitType].contexts) { - this._mojitYuiRequired[env - ][mojitType].contexts = {}; - this._mojitYuiSorted[env][mojitType].contexts = {}; - this._mojitYuiSortedPaths[env - ][mojitType].contexts = {}; - } - parts = {}; - this._precalcYuiDependencies_getDepParts(env, - this._mojitMeta[env][mojitType]['*'], - parts); - if (parts.controller && parts.modules['inlinecss/' + - mojitType]) { - parts.modules[parts.controller.yuiModuleName]. - requires.push('inlinecss/' + mojitType); - } - this._mojitYuiSorted[env][mojitType].contexts['*'] = - this._mojitMeta[env][mojitType].contexts['*']; - this._mojitYuiRequired[env][mojitType].contexts['*'] = - this._mojitMeta[env][mojitType].contexts['*']; - if (parts.controller) { - parts.required[parts.controller.yuiModuleName] = - true; - // dependencies necessary to dispatch the mojit - parts.required['mojito-dispatcher'] = true; - sorted = this._sortYUIModules( - this._mojitMeta[env][mojitType].contexts['*'], - env, - this._appConfigStatic.yui, - mojitType, - parts.modules, - parts.required - ); - this._mojitYuiRequired[env][mojitType]['*'] = - Object.keys(parts.required); - this._mojitYuiSorted[env][mojitType]['*'] = - sorted.sorted; - this._mojitYuiSortedPaths[env][mojitType]['*'] = - sorted.paths; - } - // also calculate sortedPaths for each individual binder - if ('client' === env) { - for (resid in this._mojitMeta[env - ][mojitType]['*']) { - if (this._mojitMeta[env][mojitType - ]['*'].hasOwnProperty(resid)) { - res = this._mojitMeta[env][mojitType - ]['*'][resid]; - if (res.type !== 'binder') { - continue; - } - required = {}; - required[res.yuiModuleName] = true; - // all binders have this dependency, even if - // not explicitly given - required['mojito-client'] = true; - // view engines are needed to support - // mojitProxy.render() - for (i in parts.viewEngines) { - if (parts.viewEngines. - hasOwnProperty(i) - ) { - required[parts.viewEngines[i]] = - true; - } - } - sorted = this._sortYUIModules( - this._mojitMeta[env][mojitType - ].contexts['*'], - env, - this._appConfigStatic.yui, - mojitType, - parts.modules, - required - ); - res.yuiSortedPaths = sorted.paths; - } - } - } // env==client - } // context=='*' - } - } // foreach mojitType - } // foreach env - - // log warning if server mojit has dom dependency - Y.Object.each(this._mojitYuiRequired.server, function(val, mojit) { - var deps = (val['*'] && val['*'].join()) || '', - badre = /\b(dom-\w+|node-\w+|io-upload-iframe)/g, - isbad = deps.match(badre); - - if (isbad) { - logger.log('your mojit "' + mojit + '" has a server affinity and these client-related deps: ' + isbad.join(', '), 'WARN', NAME); - logger.log('Mojito may be unable to start, unless you have provided server-side DOM/host-object suppport', 'WARN', NAME); + definitionJson = this.config.readConfigYCB(this._libs.path.join(dir, 'definition.json'), {}); + if (definitionJson.appLevel) { + mojitType = 'shared'; } - }); - - }, - - - // Fills dest with: - // .controller resource for the controller - // .modules hash yuiModuleName: { - // fullpath: where to load the module from - // requires: list of required modules - // } - // .required hash yuiModuleName:true of modules required by the - // source controller - // .viewEngines hash name:yuiModuleName of view engines - // - // @method _precalcYuiDependencies_getDepParts - // @param env {string} "client" or "server" - // @param source {object} list of resources - // @param dest {object} where to add results - // @return {nothing} results put in "dest" argument - // @private - _precalcYuiDependencies_getDepParts: function(env, source, dest) { - var resid, - res, - viewEngine; - - if (!source) { - return; - } - dest.required = dest.required || {}; - dest.viewEngines = dest.viewEngines || {}; - dest.modules = dest.modules || {}; - - // all mojits essentially have this dependency implicitly (even it not - // given explicitly) - dest.required.mojito = true; - - for (resid in source) { - if (source.hasOwnProperty(resid)) { - res = source[resid]; - if ('view' === res.type) { - viewEngine = source['addon-view-engines-' + res.viewEngine]; - if (viewEngine) { - dest.required[viewEngine.yuiModuleName] = true; - dest.viewEngines[res.viewEngine] = - viewEngine.yuiModuleName; + + // the mojit itself is registered as an app-level resource + res = { + source: { + fs: { + fullPath: dir, + rootDir: dir, + rootType: dirType, + subDir: '.', + subDirArray: ['.'], + basename: this._libs.path.basename(dir), + isFile: false, + ext: null + }, + pkg: pkg + }, + type: 'mojit', + name: mojitType, + id: 'mojit--' + mojitType, + affinity: 'common', + selector: '*' + }; + this.addResourceVersion(res); + + ress = this._findResourcesByConvention(dir, 'mojit', pkg, mojitType); + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + // just in case, only add those resources that really do belong to us + if (res.mojit === mojitType) { + this.addResourceVersion(res); + } + // FUTURE: else warn? + } + }, + + + /** + * Resolves versions for a list of resources. + * The priority is based on passed-in configuration. + * See `resolveResourceVersions()` for details. + * + * @private + * @method _resolveVersions + * @param {object} affinities lookup hash for priority adjustment for each affinity + * @param {object} selectors lookup hash for priority adjustment for each selector + * @param {int} sourceBase multiplier for order in source list + * @param {array of arrays} srcs resource versions to resolve + * @return {array} list of resolved resources + */ + _resolveVersions: function(affinities, selectors, sourceBase, srcs) { + var s, src, + r, res, + priority, + versions = {}, // id: priority: resource + out = [], + resid, + highest, + chosen; + + for (s = 0; s < srcs.length; s += 1) { + src = srcs[s]; + for (r = 0; r < src.length; r += 1) { + res = src[r]; + if (!selectors.hasOwnProperty(res.selector)) { + continue; } - } - if (res.yuiModuleName) { - // The binder is part of the resources for the server, - // but shouldn't be added as a runtime dependency. - if ('server' === env && 'binder' === res.type) { + if (!affinities.hasOwnProperty(res.affinity)) { continue; } - if (!res.sharedMojit) { - dest.required[res.yuiModuleName] = true; + priority = (s * sourceBase) + + selectors[res.selector] + affinities[res.affinity]; + //console.log('--DEBUG-- pri=' + priority + ' --' + // + ' src' + s + '=' + (s * sourceBase) + // + ' ' + res.selector + '=' + selectors[res.selector] + // + ' ' + res.affinity + '=' + affinities[res.affinity] + // + ' -- ' + res.id); + if (!versions[res.id]) { + versions[res.id] = {}; } - dest.modules[res.yuiModuleName] = { - requires: res.yuiModuleMeta.requires, - fullpath: (('client' === env) ? - res.staticHandlerURL : - res.fsPath) - }; - if (('controller' === res.type)) { - dest.controller = res; + if (!versions[res.id][priority]) { + versions[res.id][priority] = res; } } } - } - // TODO: move other dynamic dependency adjustments - // (compiled views inlinecss) here. - }, - - - /** - * Uses YUI Loader to sort a list of YUI modules. - * - * @method _sortYUIModules - * @param ctx {object} runtime context - * @param env {string} runtime environment ("client" or "server") - * @param yuiConfig {object} configuration for YUI - * @param mojitType {string} name of mojit - * @param modules {object} YUI configuration for all modules - * @param required {object} lookup hash of modules that are required - * @return {object} list of load-order sorted module names, and object - * listing paths used to load those modules - * @private - */ - _sortYUIModules: function(ctx, env, yuiConfig, mojitType, modules, - required) { - var YUI = serverYUI, - Y, - loader, - sortedPaths = {}, - usePrecomputed = -1 !== this._appConfigStatic.yui. - dependencyCalculations.indexOf('precomputed'), - useOnDemand = -1 !== this._appConfigStatic.yui. - dependencyCalculations.indexOf('ondemand'), - j, - module, - info; - - if (!usePrecomputed) { - useOnDemand = true; - } - - if ('client' === env) { - // Use clientYUI to avoid cross-contamination with serverYUI - YUI = clientYUI; - } - - // We don't actually need the full list, just the base required modules. - // YUI.Loader() will do the rest at runtime. - if (useOnDemand) { - for (module in required) { - if (required.hasOwnProperty(module)) { - sortedPaths[module] = modules[module].fullpath; - } - } - return { sorted: Object.keys(required), paths: sortedPaths }; - } - - Y = YUI({ useSync: true }).use('loader-base'); - Y.applyConfig({ useSync: false }); - - // We need to clear YUI's cached dependencies, since there's no - // guarantee that the previously calculated dependencies have been done - // using the same context as this calculation. - delete YUI.Env._renderedMods; - - //Use ignoreRegistered here instead of the old `delete YUI.Env._renderedMods` hack - loader = new Y.Loader({ lang: ctx.lang, ignoreRegistered: true }); - - //Only override the default if it's required - if (yuiConfig && yuiConfig.base) { - loader.base = yuiConfig.base; - } - loader.addGroup({modules: modules}, mojitType); - loader.calculate({required: required}); - for (j = 0; j < loader.sorted.length; j += 1) { - module = loader.sorted[j]; - info = loader.moduleInfo[module]; - if (info) { - // modules with "nodejs" in their name are tweaks on other modules - if ('client' === env && module.indexOf('nodejs') !== -1) { - continue; + for (resid in versions) { + if (versions.hasOwnProperty(resid)) { + highest = Math.max.apply(Math, Object.keys(versions[resid])); + //console.log('--DEBUG-- highest=' + highest + ' -- ' + resid); + chosen = Y.clone(versions[resid][highest], true); + out.push(chosen); } - sortedPaths[module] = info.fullpath || loader._url(info.path); } - } - return { sorted: loader.sorted, paths: sortedPaths }; - }, + return out; + }, - /** - * Calculates the static handling URL for a resource. - * - * @method _precalcStaticURL - * @param res {object} metadata about the resource - * @param mojitType {string} mojit type, can be undefined for non-mojit-specific resources - * @return {nothing} new metadata added to the "res" argument - * @private - */ - _precalcStaticURL: function(res, mojitType) { - /* alternate approach which shows power of precalculating the URL and - * then passing it around everywhere - res.staticHandlerURL = '/static/' + Math.floor(Math.random() * - 1000000000); - return; - */ - var url, - parts = [], - path, - i, - config = this._appConfigStatic.staticHandling || {}, - prefix = config.prefix, - appName = config.appName || this._shortRoot, - frameworkName = config.frameworkName || 'mojito', - rollupParts = [], - rollupFsPath; - - // TODO: [Issue 111] magic constants should should come from fw.json. - - /* - Server only framework mojits like DaliProxy and HTMLFrameMojit should - never have static URLs associated with them, so we skip them. This never - used to be an issue until we added the "assumeRollups" functionality to - preload JSON specs for specified mojits during the compile step (mojito - compile json) for Livestand. I think we need to reevaluate this entire - process so we don't have such a fragile condition below. + /** + * Finds resources based on our conventions. + * -Doesn't- load mojits or their contents. That's done elsewhere. + * + * @private + * @method _findResourcesByConvention + * @param {string} dir directory from which to find resources + * @param {string} dirType type represented by the "dir" argument. values are "app", "bundle", "pkg", or "mojit" + * @param {object} pkg metadata (name and version) about the package + * @param {string|null} mojitType name of mojit to which the resource belongs + * @return {array} list of resources */ - // TODO: reevaluate this entire process so we don't have such a fragile - // condition below. - if (mojitType === 'DaliProxy' || mojitType === 'HTMLFrameMojit') { - return; - } - - switch (res.type) { - case 'action': - path = libpath.join('actions', res.shortPath); - break; - case 'addon': - path = libpath.join('addons', res.addonType, res.shortPath); - break; - case 'asset': - path = libpath.join('assets', res.shortPath); - break; - case 'binder': - path = libpath.join('binders', res.shortPath); - break; - case 'controller': - path = res.shortPath; - break; - case 'model': - path = libpath.join('models', res.shortPath); - break; - case 'view': - path = libpath.join('views', res.shortPath); - break; - case 'yui-lang': - path = libpath.join('lang', res.shortPath); - break; - case 'yui-module': - // FUTURE: change this to 'yui_modules' - path = libpath.join('autoload', res.shortPath); - break; - case 'package': - path = res.shortPath; - break; - default: - return; - } + _findResourcesByConvention: function(dir, dirType, pkg, mojitType) { + var me = this, + ress = []; + //console.log('-- FIND RESOURCES BY CONVENTION -- ' + pkg.name + '@' + pkg.version + ' -- ' + mojitType); - if (!config.hasOwnProperty('prefix')) { - prefix = 'static'; - } - if (prefix) { - parts.push(prefix); - rollupParts.push(prefix); - } + this._walkDirRecursive(dir, function(error, subdir, file, isFile) { + var source, ret, res; - if ('shared' === mojitType) { - if (res.pkg && 'mojito' === res.pkg.name) { - parts.push(frameworkName); - if (config.useRollups && res.yuiModuleName) { - // fw resources are put into app-level rollup - rollupParts.push(appName); - rollupFsPath = libpath.join(this._root, 'rollup.client.js'); + if ('node_modules' === file) { + return false; } - } else { - parts.push(appName); - if (config.useRollups && res.yuiModuleName) { - rollupParts.push(appName); - rollupFsPath = libpath.join(this._root, 'rollup.client.js'); + if ('libs' === file && 'test' !== me._appConfigStatic.env) { + return false; } - } - } else { - parts.push(mojitType); - if (config.useRollups && res.yuiModuleName) { - rollupParts.push(mojitType); - rollupFsPath = libpath.join(this._mojitPaths[mojitType], - 'rollup.client.js'); - } - } - if (mojitType) { - if (!this._mojitAssetRoots[mojitType]) { - this._mojitAssetRoots[mojitType] = '/' + - libpath.join(parts.join('/'), 'assets'); - } - } - - // only use rollup URL if rollup file exists or we are assuming rollups - if ((rollupFsPath && this._libs.path.existsSync(rollupFsPath) && - config.useRollups) || this._appConfigStatic.assumeRollups) { - // useful for debugging: path += '?orig=' + path; - res.rollupURL = '/' + libpath.join(rollupParts.join('/'), - 'rollup.client.js'); - url = res.rollupURL; - res.staticHandlerFsPath = rollupFsPath; - } - if (!url) { - url = '/' + libpath.join(parts.join('/'), path); - res.staticHandlerFsPath = res.fsPath; - } - res.staticHandlerURL = url; - }, - - - /** - * Attempt to gather YUI-module details. - * - * @method _precalcYuiModule - * @param res {object} metadata about the resource - * @return {nothing} new metadata added to the "res" argument - * @private - */ - _precalcYuiModule: function(res) { - var file = this._libs.fs.readFileSync(res.fsPath, 'utf8'), - ctx = { - console: { - log: function() {} - }, - window: {}, - document: {}, - YUI: { - add: function(name, fn, version, meta) { - res.yuiModuleName = name; - res.yuiModuleVersion = version; - res.yuiModuleMeta = meta || {}; - } + if ('tests' === file && 'test' !== me._appConfigStatic.env) { + return false; } - }; - - try { - libvm.runInNewContext(file, ctx, res.fsPath); - } catch (e) { - if (e.stack.indexOf('SyntaxError:') === 0) { - // the stack of a SyntaxError thrown by runInNewContext() lacks - // filename, line and column numbers for the error. Also, it - // does not write to stderr as the docs claim. - - // see "Lack of error message in vm.* stuff" from 2011-04-29 at - // http://groups.google.com/group/nodejs/browse_thread/ - // thread/2075b964a3f7dd79/bd0df1ae36829813 - - logger.log(this._reportJavaScriptSyntaxErrors(file)); - logger.log(e.message + ' in file: ' + res.fsPath, 'error', - NAME); - - } else { - logger.log(e.message + '\n' + e.stack, 'error', NAME); - } - process.exit(-1); - } - }, - - - /** - * Reads one the configuration files for a mojit - * - * @method _getMojitConfig - * @param env {string} "client" or "server" - * @param ctx {object} runtime context - * @param mojitType {string} name of mojit - * @param name {string} config resource name, either "definition" or "defaults" - * @private - */ - _getMojitConfig: function(env, ctx, mojitType, name) { - //logger.log('_getMojitConfig('+env+','+mojitType+','+name+')'); - var resid, - res; - - if (!this._mojitMeta[env][mojitType]) { - throw new Error('Cannot find meta data for mojit type \"' + mojitType + - '\"'); - } - - resid = 'config-' + name; - res = this._getContextualizedResource(this._mojitMeta[env][mojitType], ctx, - resid); - if (!res) { - return {}; - } - return this._readConfigYCB(ctx, res.fsPath); - }, + source = { + fs: { + fullPath: me._libs.path.join(dir, subdir, file), + rootDir: dir, + rootType: dirType, + subDir: subdir, + subDirArray: subdir.split('/'), + isFile: isFile, + ext: me._libs.path.extname(file) + }, + pkg: pkg + }; + source.fs.basename = me._libs.path.basename(file, source.fs.ext); - /** - * Returns whether a runtime context matches a partial context - * - * @method _matchContext - * @param ctx {object} runtime context - * @param ctxParts {object} partial context - * @private - */ - _matchContext: function(ctx, ctxParts) { - var k; - - for (k in ctxParts) { - if (ctxParts.hasOwnProperty(k)) { - // FUTURE -- handle "lang" slightly specially ("en" should match - // "en-US") - // For now we will skip the "lang" check as it could change on - // the fly in the client and we need all the "lang" files - // available in our YUI instance. - if (k !== 'lang' && ctx[k] !== ctxParts[k]) { + if (me._skipBadPath(source.fs)) { return false; } - } - } - return true; - }, - - /** - * Returns a list of resource metadata that match the context - * - * @method _getResourceListForContext - * @param src {object} list of contextualized resources, key is contextKey - * @param ctx {object} context to match - * @return {object} list of resources, key is resource ID - * @private - */ - _getResourceListForContext: function(src, ctx) { - var list = {}, // resid: resource - resid, - ctxKey; - - if (src['*']) { - for (resid in src['*']) { - if (src['*'].hasOwnProperty(resid)) { - list[resid] = src['*'][resid]; - } - } - } - for (ctxKey in src.contexts) { - if (src.contexts.hasOwnProperty(ctxKey)) { - if ('*' === ctxKey) { - continue; - } - if (!this._matchContext(ctx, src.contexts[ctxKey])) { - continue; - } - for (resid in src[ctxKey]) { - if (src[ctxKey].hasOwnProperty(resid)) { - list[resid] = src[ctxKey][resid]; + ret = me.findResourceVersionByConvention(source, mojitType); + if ('object' === typeof ret) { + if (ret.skipSubdirParts) { + source.fs.subDirArray = source.fs.subDirArray.slice(ret.skipSubdirParts); + source.fs.subDir = source.fs.subDirArray.join('/') || '.'; } + res = me.parseResourceVersion(source, ret.type, ret.subtype, mojitType); + if ('object' === typeof res) { + ress.push(res); + } + // don't recurse into resources that are directories + return false; } - } - } - return list; - }, + return ret; + }); + return ress; + }, - /** - * Returns a list of the language resources - * doesn't discriminate based on context: returns all langs for all - * contexts. - * - * @method _getLangList - * @param src {object} list of contextualized resources, key is contextKey - * @return {object} list of language resources, key is resource ID - * @private - */ - _getLangList: function(src) { - var list = {}, // resid: res - ctxKey, - resid, - res; - - for (ctxKey in src.contexts) { - if (src.contexts.hasOwnProperty(ctxKey)) { - for (resid in src[ctxKey]) { - if (src[ctxKey].hasOwnProperty(resid)) { - res = src[ctxKey][resid]; - if ('yui-lang' === res.type) { - list[resid] = res; - } - } - } - } - } - return list; - }, + /** + * Indicates whether file should be skipped based on its path + * + * @private + * @method _skipBadPath + * @param {object} pathParts the "source.fs" part of the resource + * @return {boolean} true indicates that the file should be skipped + */ + _skipBadPath: function(fs) { + if (fs.isFile && fs.ext.substr(1).match(isNotAlphaNum)) { + return true; + } + return false; + }, - /** - * Returns the metadata for a resource specific for a particular runtime context - * - * @method _getContextualizedResource - * @param src {object} list of contextualized resources, key is contextKey - * @param ctx {object} context to match - * @param resid {string} ID of resource to find - * @return {object} resource metadata - * @private - */ - _getContextualizedResource: function(src, ctx, resid) { - var ctxKey, - res; - // TODO: [Issue 100] Review, for when there is no app.json file. - if (!src || !src.contexts) { - return {}; - } + /** + * A wrapper for `fs.readdirSync()` that guarantees ordering. The order + * in which the file system is walked is significant within the resource + * store, e.g., when looking up a matching context. + * + * @private + * @method _sortedReaddirSync + * @param {string} path directory to read + * @return {array} files in the directory + */ + _sortedReaddirSync: function(path) { + var out = this._libs.fs.readdirSync(path); + return out.sort(); + }, + + + /** + * Recursively walks a directory + * @private + * @method _walkDirRecursive + * @param {string} dir directory to start at + * @param {function(error, subdir, name, isFile)} cb callback called for each file + * @param {string} _subdir INTERNAL argument for recursion, please ignore + */ + _walkDirRecursive: function(dir, cb, _subdir) { + var subdir, + fulldir, + children, + i, + childName, + childPath, + childFullPath, + childStat; + + subdir = _subdir || '.'; + fulldir = this._libs.path.join(dir, subdir); + if (!this._libs.path.existsSync(fulldir)) { + return; + } - for (ctxKey in src.contexts) { - if (src.contexts.hasOwnProperty(ctxKey)) { - // look for specific first - if ('*' === ctxKey) { + children = this._sortedReaddirSync(fulldir); + for (i = 0; i < children.length; i += 1) { + childName = children[i]; + if ('.' === childName.substring(0, 1)) { continue; } - if (!this._matchContext(ctx, src.contexts[ctxKey])) { + if ('node_modules' === childName) { continue; } - res = src[ctxKey][resid]; - if (res) { - return res; - } - } - } - // fallback - return src['*'][resid]; - }, - - - /** - * Indicates whether file should be skipped based on its path - * - * @method _skipBadPath - * @param pathParts {object} return value of _parsePath() (or the equivalent) - * @return {boolean} true indicates that the file should be skipped - * @private - */ - _skipBadPath: function(pathParts) { - var ext = pathParts.ext.substring(1); - if (ext.match(isNotAlphaNum)) { - return true; - } - return false; - }, - - - /** - * Generate a report of syntax errors for JavaScript code. This is also - * very useful to find syntax errors in JSON documents. - * - * @method _reportJavaScriptSyntaxErrors - * @param {string} js the JavaScript - * @param {string} filename OPTIONAL. the name of the file containing the - * JavaScript - * @return {string} if errors were found, a multi-line error report - * @private - */ - _reportJavaScriptSyntaxErrors: function(js, filename) { - - // use a really lenient JSLINT to find syntax errors - - var jslint = require('./management/fulljslint').jslint, - opts = { - // turn off all the usual checks - devel: true, - browser: true, - node: true, - rhino: true, - widget: true, - windows: true, - bitwise: true, - regexp: true, - confusion: true, - undef: true, - 'continue': true, - unparam: true, - debug: true, - sloppy: true, - eqeq: true, - sub: true, - es5: true, - vars: true, - evil: true, - white: true, - forin: true, - css: true, - newcap: true, - cap: true, - nomen: true, - on: true, - plusplus: true, - fragment: true, - - // prevent well-known globals from showing up as errors - predef: [ - // CommonJS - 'exports', - // YUI - 'YUI', 'YUI_config', 'YAHOO', 'YAHOO_config', 'Y', - // Node - 'global', 'process', 'require', '__filename', 'module', - // Browser - 'document', 'navigator', 'console', 'self', 'window' - ] - }, - // identify errors about undefined globals - nameIsNotDefined = / is not defined\.$/, - success, - report = [], - len, - e, - i; - - success = jslint(js, opts); - if (!success) { - len = jslint.errors.length; - for (i = 0; i < len; i += 1) { - e = jslint.errors[i]; - if (e && e.reason && !nameIsNotDefined.test(e.reason)) { - report.push(e.line + ',' + e.character + ': ' + e.reason); - report.push(' ' + - (e.evidence || '').replace(/^\s+|\s+$/, '')); + childPath = this._libs.path.join(subdir, childName); + childFullPath = this._libs.path.join(dir, childPath); + try { + childStat = this._libs.fs.statSync(childFullPath); + } catch (e) { + Y.log('invalid file. skipping ' + childFullPath, 'warn', NAME); + continue; } - } - } - - if (filename && report.length) { - report.unshift('Syntax errors detected in ' + filename); - } - - return report.join('\n'); - }, - - - /** - * Returns the selector for the runtime context - * - * @method _selectorFromContext - * @param ctx {object} runtime context - * @return {string|null} selector for context - * @private - */ - _selectorFromContext: function(ctx) { - if (ctx.device) { - return ctx.device; - } - return null; - }, - - - /** - * @method _objectIsEmpty - * @param o {object} - * @return {boolean} true if the object is empty - * @private - */ - _objectIsEmpty: function(o) { - if (!o) { - return true; - } - return (0 === Object.keys(o).length); - }, - - - // from http://stackoverflow.com/questions/171251/ - // how-can-i-merge-properties-of-two-javascript-objects-dynamically/ - // 383245#383245 - /** - * Recursively merge one object onto another - * - * @method _mergeRecursive - * @param dest {object} object to merge into - * @param src {object} object to merge onto "dest" - * @param matchType {boolean} controls whether a non-object in the src is - * allowed to clobber a non-object in the dest (if a different type) - * @return {object} the modified "dest" object is also returned directly - * @private - */ - _mergeRecursive: function(dest, src, typeMatch) { - var p; - - for (p in src) { - if (src.hasOwnProperty(p)) { - // Property in destination object set; update its value. - if (src[p] && src[p].constructor === Object) { - if (!dest[p]) { - dest[p] = {}; - } - dest[p] = this._mergeRecursive(dest[p], src[p]); - } else { - if (dest[p] && typeMatch) { - if (typeof dest[p] === typeof src[p]) { - dest[p] = src[p]; - } - } else { - dest[p] = src[p]; + if (childStat.isFile()) { + cb(null, subdir, childName, true); + } else if (childStat.isDirectory()) { + if (cb(null, subdir, childName, false)) { + this._walkDirRecursive(dir, cb, childPath); } } } - } - return dest; - }, - - - /** - * @method _cloneObj - * @param o {mixed} - * @return {mixed} deep copy of argument - * @private - */ - _cloneObj: function(o) { - var newO, - i; - - if (typeof o !== 'object') { - return o; - } - if (!o) { - return o; - } - - if ('[object Array]' === Object.prototype.toString.apply(o)) { - newO = []; - for (i = 0; i < o.length; i += 1) { - newO[i] = this._cloneObj(o[i]); - } - return newO; - } - - newO = {}; - for (i in o) { - if (o.hasOwnProperty(i)) { - newO[i] = this._cloneObj(o[i]); - } - } - return newO; - }, + }, - /** - * A wrapper for fs.readdirSync() that guarantees ordering. The order in - * which the file system is walked is significant within the resource - * store, e.g., when looking up a matching context. - * - * @method _sortedReaddirSync - * @param path {string} directory to read - * @return {array} files in the directory - * @private - */ - _sortedReaddirSync: function(path) { - var out = this._libs.fs.readdirSync(path); - return out.sort(); - }, - - - /** - * Recursively walks a directory - * - * @method _walkDirRecursive - * @param dir {string} directory to start at - * @param cb {function(error, subdir, name, isFile)} callback called for each file - * @param _subdir {string} INTERNAL argument, please ignore - * @return {nothing} value returned via callback - * @private - */ - _walkDirRecursive: function(dir, cb, _subdir) { - var subdir, - fulldir, - children, - i, - childName, - childPath, - childFullPath, - childStat; - - subdir = _subdir || '.'; - fulldir = libpath.join(dir, subdir); - if (!this._libs.path.existsSync(fulldir)) { - return; + /** + * Takes a list of globs and turns it into a list of matching paths. + * @private + * @method _globList + * @param {string} prefix prefix for every path in the list + * @param {array} list list of globs + * @return {array} list of paths matching the globs + */ + _globList: function(prefix, list) { + var found = [], + i, + glob; + for (i = 0; i < list.length; i += 1) { + glob = list[i]; + if ('/' !== glob.charAt(0)) { + glob = this._libs.path.join(prefix, glob); + } + found = found.concat(this._libs.glob.sync(glob, {})); + } + return found; + }, + + + /** + * Augments this resource store's Y object with the specified YUI modules. + * @private + * @method _yuiUseSync + * @param {object} modules YUI module configuration information + * @return {nothing} + */ + _yuiUseSync: function(modules) { + Y.applyConfig({ + useSync: true, + modules: modules + }); + Y.use.apply(Y, Object.keys(modules)); + Y.applyConfig({ useSync: false }); } - children = this._sortedReaddirSync(fulldir); - for (i = 0; i < children.length; i += 1) { - childName = children[i]; - if ('.' === childName.substring(0, 1)) { - continue; - } - if ('node_modules' === childName) { - continue; - } - childPath = libpath.join(subdir, childName); - childFullPath = libpath.join(dir, childPath); - childStat = this._libs.fs.statSync(childFullPath); - if (childStat.isFile()) { - cb(null, subdir, childName, true); - } else if (childStat.isDirectory()) { - if (cb(null, subdir, childName, false)) { - this._walkDirRecursive(dir, cb, childPath); - } - } - } - } + }); -}; + Y.namespace('mojito'); + Y.mojito.ResourceStore = ResourceStore; -module.exports = ServerStore; +}, '0.0.1', { requires: [ + 'base', + 'json-stringify', + 'oop' +]}); diff --git a/source/lib/tests/autoload/app/addons/ac/deploy-tests.server.js b/source/lib/tests/autoload/app/addons/ac/deploy-tests.server.js index 4e2cd08ac..61bcdc976 100644 --- a/source/lib/tests/autoload/app/addons/ac/deploy-tests.server.js +++ b/source/lib/tests/autoload/app/addons/ac/deploy-tests.server.js @@ -10,9 +10,9 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { A = YUITest.Assert, AA = YUITest.ArrayAssert, OA = YUITest.ObjectAssert; - + suite.add(new YUITest.TestCase({ - + name: 'basics', setUp: function() { @@ -23,6 +23,7 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { addon = null; }, + 'YUI_config uses application.json yui.config': function() { var realRouteMaker = Y.mojito.RouteMaker; @@ -50,11 +51,14 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { serializeClientStore: function() { return 'clientstore'; }, - store: { _fwConfig: { ondemandBaseYuiModules:[] } }, - getYuiConfigFw: function() { return {}; }, - getYuiConfigApp: function() { return {}; }, - fileFromStaticHandlerURL: function() { - return 'path'; + store: { + getAllURLs: function() { return {}; }, + getFrameworkConfig: function() { + return { ondemandBaseYuiModules:[] }; + }, + yui: { + getConfigShared: function() { return {}; } + } } }); @@ -92,6 +96,7 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { A.areSame('klingon', config.lang, 'wrong lang used'); }, + 'honor yui.config.fetchCSS=false in application.json': function() { var realLoader = Y.mojito.Loader; @@ -130,11 +135,14 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { serializeClientStore: function() { return 'clientstore'; }, - store: { _fwConfig: { ondemandBaseYuiModules:[] } }, - getYuiConfigFw: function() { return {}; }, - getYuiConfigApp: function() { return {}; }, - fileFromStaticHandlerURL: function() { - return 'path'; + store: { + getAllURLs: function() { return {}; }, + getFrameworkConfig: function() { + return { ondemandBaseYuiModules:[] }; + }, + yui: { + getConfigShared: function() { return {}; } + } } }); @@ -179,6 +187,7 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { A.areSame(1, counts['blob bottom'], 'wrong number of blob:bottom'); }, + 'dependencyCalculations precomputed': function() { var calledYuiModules; var realLoader = Y.mojito.Loader; @@ -218,11 +227,14 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { serializeClientStore: function() { return 'clientstore'; }, - store: { _fwConfig: { ondemandBaseYuiModules:[] } }, - getYuiConfigFw: function() { return {}; }, - getYuiConfigApp: function() { return {}; }, - fileFromStaticHandlerURL: function() { - return 'path'; + store: { + getAllURLs: function() { return {}; }, + getFrameworkConfig: function() { + return { ondemandBaseYuiModules:[] }; + }, + yui: { + getConfigShared: function() { return {}; } + } } }); @@ -258,6 +270,7 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { A.areSame("'*'", YUI_use); }, + 'dependencyCalculations ondemand': function() { var calledYuiModules; var realLoader = Y.mojito.Loader; @@ -297,11 +310,14 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { serializeClientStore: function() { return 'clientstore'; }, - store: { _fwConfig: { ondemandBaseYuiModules:[] } }, - getYuiConfigFw: function() { return {}; }, - getYuiConfigApp: function() { return {}; }, - fileFromStaticHandlerURL: function() { - return 'path'; + store: { + getAllURLs: function() { return {}; }, + getFrameworkConfig: function() { + return { ondemandBaseYuiModules:[] }; + }, + yui: { + getConfigShared: function() { return {}; } + } } }); @@ -337,6 +353,7 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { A.areSame("'mojito-client'", YUI_use); }, + 'dependencyCalculations precomputed+ondemand': function() { var calledYuiModules; var realLoader = Y.mojito.Loader; @@ -376,11 +393,14 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { serializeClientStore: function() { return 'clientstore'; }, - store: { _fwConfig: { ondemandBaseYuiModules:[] } }, - getYuiConfigFw: function() { return {}; }, - getYuiConfigApp: function() { return {}; }, - fileFromStaticHandlerURL: function() { - return 'path'; + store: { + getAllURLs: function() { return {}; }, + getFrameworkConfig: function() { + return { ondemandBaseYuiModules:[] }; + }, + yui: { + getConfigShared: function() { return {}; } + } } }); @@ -420,7 +440,7 @@ YUI.add('mojito-deploy-addon-tests', function(Y, NAME) { })); YUITest.TestRunner.add(suite); - + }, '0.0.1', {requires: [ 'mojito-deploy-addon', diff --git a/source/lib/tests/autoload/app/addons/ac/partial-tests.common.js b/source/lib/tests/autoload/app/addons/ac/partial-tests.common.js index e491da874..fc84a8c9c 100644 --- a/source/lib/tests/autoload/app/addons/ac/partial-tests.common.js +++ b/source/lib/tests/autoload/app/addons/ac/partial-tests.common.js @@ -8,8 +8,6 @@ YUI.add('mojito-partial-addon-tests', function(Y, NAME) { var suite = new YUITest.TestSuite(NAME), path = require('path'), fixtures = path.join(__dirname, '../../../../fixtures/store'), - ResourceStore = require(path.join(__dirname, - '../../../../../store.server')), Assert = YUITest.Assert, ObjectAssert = YUITest.ObjectAssert, Mock = YUITest.Mock, @@ -29,9 +27,6 @@ YUI.add('mojito-partial-addon-tests', function(Y, NAME) { } }; - var store = new ResourceStore(fixtures); - store.setLogger(logger); - var mockCallback = Mock(); Mock.expect(mockCallback, { method: 'callback', @@ -59,9 +54,6 @@ YUI.add('mojito-partial-addon-tests', function(Y, NAME) { } }; - var store = new ResourceStore(fixtures); - store.setLogger(logger); - var addon = new Y.mojito.addons.ac.partial(command, null, ac); var mockRenderer = Mock(); @@ -98,9 +90,6 @@ YUI.add('mojito-partial-addon-tests', function(Y, NAME) { } }; - var store = new ResourceStore(fixtures); - store.setLogger(logger); - var mockCallback = Mock(); Mock.expect(mockCallback, { method: 'callback', diff --git a/source/lib/tests/autoload/app/addons/rs/config-tests.server.js b/source/lib/tests/autoload/app/addons/rs/config-tests.server.js new file mode 100644 index 000000000..c08798a64 --- /dev/null +++ b/source/lib/tests/autoload/app/addons/rs/config-tests.server.js @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2011-2012, Yahoo! Inc. All rights reserved. + * Copyrights licensed under the New BSD License. + * See the accompanying LICENSE file for terms. + */ +YUI.add('mojito-addon-rs-config-tests', function(Y, NAME) { + + var suite = new YUITest.TestSuite(NAME), + libfs = require('fs'), + libpath = require('path'), + mojitoRoot = libpath.join(__dirname, '../../../../../'), + A = YUITest.Assert, + OA = YUITest.ObjectAssert, + AA = YUITest.ArrayAssert; + + + function MockRS(config) { + MockRS.superclass.constructor.apply(this, arguments); + } + MockRS.NAME = 'MockResourceStore'; + MockRS.ATTRS = {}; + Y.extend(MockRS, Y.Base, { + + initializer: function(cfg) { + this._config = cfg || {}; + }, + + validateContext: function() { + }, + + cloneObj: function(o) { + return Y.clone(o); + }, + + getStaticContext: function() { + return this._config.context || {}; + }, + + mergeRecursive: function(dest, src, typeMatch) { + var p; + for (p in src) { + if (src.hasOwnProperty(p)) { + // Property in destination object set; update its value. + if (src[p] && src[p].constructor === Object) { + if (!dest[p]) { + dest[p] = {}; + } + dest[p] = this.mergeRecursive(dest[p], src[p]); + } else { + if (dest[p] && typeMatch) { + if (typeof dest[p] === typeof src[p]) { + dest[p] = src[p]; + } + } else { + dest[p] = src[p]; + } + } + } + } + return dest; + }, + + findResourceVersionByConvention: function(source, mojitType) { + // no-op + }, + + parseResourceVersion: function(source, type, subtype, mojitType) { + // no-op + } + + }); + + + function readJSON(dir, file) { + var path = libpath.join(dir, file); + var contents = libfs.readFileSync(path, 'utf-8'); + return Y.JSON.parse(contents); + } + + + function cmp(x, y, msg, path) { + if (Y.Lang.isArray(x)) { + A.isArray(x, msg || 'first arg should be an array'); + A.isArray(y, msg || 'second arg should be an array'); + A.areSame(x.length, y.length, msg || 'arrays are different lengths'); + for (var i = 0; i < x.length; i += 1) { + cmp(x[i], y[i], msg); + } + return; + } + if (Y.Lang.isObject(x)) { + A.isObject(x, msg || 'first arg should be an object'); + A.isObject(y, msg || 'second arg should be an object'); + A.areSame(Object.keys(x).length, Object.keys(y).length, msg || 'object keys are different lengths'); + for (var i in x) { + if (x.hasOwnProperty(i)) { + cmp(x[i], y[i], msg); + } + } + return; + } + A.areSame(x, y, msg || 'args should be the same'); + } + + + function makeSource(dir, dirType, subdir, file, isFile) { + var source = { + fs: { + fullPath: libpath.join(dir, subdir, file), + rootDir: dir, + rootType: dirType, + subDir: subdir, + subDirArray: subdir.split('/'), + isFile: isFile, + ext: libpath.extname(file) + }, + pkg: { + name: 'unittest', + version: '999.666.999', + depth: 999 + } + }; + source.fs.basename = libpath.basename(file, source.fs.ext); + return source; + } + + + suite.add(new YUITest.TestCase({ + + name: 'config rs addon tests', + + 'read dimensions': function() { + // from mojito + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + var have = store.config.getDimensions(); + var want = readJSON(mojitoRoot, 'dimensions.json'); + cmp(want, have); + + // app-specified + fixtures = libpath.join(__dirname, '../../../../fixtures/ycb'); + store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + have = store.config.getDimensions(); + want = readJSON(fixtures, 'dimensions.json'); + cmp(want, have); + }, + + + 'find config resources': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + // skip non-json files + var source = makeSource(fixtures, 'app', '.', 'server.js', true); + var have = store.findResourceVersionByConvention(source, null); + var want = undefined; + cmp(have, want, 'skip non-json files'); + + // include all json files in the app + source = makeSource(fixtures, 'app', '.', 'x.json', true); + have = store.findResourceVersionByConvention(source, null); + want = { type: 'config' }; + cmp(have, want, 'include all json files in the app'); + + // ... explicitly including package.json + source = makeSource(fixtures, 'app', '.', 'package.json', true); + have = store.findResourceVersionByConvention(source, null); + want = { type: 'config' }; + cmp(have, want, 'include package.json in the app'); + + // exclude all json files in a bundle + source = makeSource(fixtures, 'bundle', '.', 'x.json', true); + have = store.findResourceVersionByConvention(source, null); + want = undefined; + cmp(have, want, 'exclude all json files in a bundle'); + + // ... explicitly excluding package.json + source = makeSource(fixtures, 'bundle', '.', 'package.json', true); + have = store.findResourceVersionByConvention(source, null); + want = undefined; + cmp(have, want, 'exclude package.json in a bundle'); + + // include all json files in a mojit + source = makeSource(fixtures, 'mojit', '.', 'x.json', true); + have = store.findResourceVersionByConvention(source, 'foo'); + want = { type: 'config' }; + cmp(have, want, 'include all json files in a mojit'); + + // ... except for the 'shared' mojit + source = makeSource(fixtures, 'mojit', '.', 'x.json', true); + have = store.findResourceVersionByConvention(source, 'shared'); + want = undefined; + cmp(have, want, 'exclude all json files in the "shared" mojit'); + + // ... explicitly including package.json + source = makeSource(fixtures, 'mojit', '.', 'package.json', true); + have = store.findResourceVersionByConvention(source, 'shared'); + want = { type: 'config' }; + cmp(have, want, 'include package.json in the "shared" mojit'); + }, + + + 'parse found resource': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var source = makeSource(fixtures, 'app', '.', 'application.json', true); + var res = store.parseResourceVersion(source, 'config'); + A.isNotUndefined(res); + cmp(res.source, source); + A.areSame('config', res.type); + A.areSame('common', res.affinity); + A.areSame('*', res.selector); + A.areSame('application', res.name); + A.areSame('config--application', res.id); + A.isUndefined(res.mojit); + + source = makeSource(fixtures, 'mojit', '.', 'defaults.json', true); + res = store.parseResourceVersion(source, 'config', undefined, 'x'); + A.isNotUndefined(res); + cmp(res.source, source); + A.areSame('config', res.type); + A.areSame('common', res.affinity); + A.areSame('*', res.selector); + A.areSame('defaults', res.name); + A.areSame('config--defaults', res.id); + A.areSame('x', res.mojit); + }, + + + 'read JSON files': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var path = libpath.join(fixtures, 'application.json'); + var have = store.config.readConfigJSON(path); + var want = readJSON(fixtures, 'application.json'); + cmp(have, want); + }, + + + 'read YCB files': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var path = libpath.join(fixtures, 'application.json'); + var have = store.config.readConfigYCB(path, { runtime: 'server' }); + var want = { + "mojitDirs": [ + "soloMojit" + ], + "staticHandling": { + "useRollups": true + }, + "testKey1": "testVal1-server", + "testKey2": "testVal2", + "testKey3": "testVal3", + "specs": { + "test1": { + "type": "test_mojit_1" + } + }, + "selector": "shelves", + "pathos": "portended" + }; + cmp(have, want); + }, + + + 'malformed JSON for config file': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/badfiles2'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var path = libpath.join(fixtures, 'routes.json'); + try { + store.config.readConfigJSON(path); + } + catch (err) { + A.areSame('Error parsing JSON file:', err.message.substr(0, 24)); + } + }, + + + 'JSON config file not YCB': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/badfiles3'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var path = libpath.join(fixtures, 'routes.json'); + var have = store.config.readConfigYCB(path, {}); + var want = {}; + cmp(have, want); + } + + + })); + + YUITest.TestRunner.add(suite); + +}, '0.0.1', {requires: ['base', 'oop', 'addon-rs-config', 'json-parse']}); diff --git a/source/lib/tests/autoload/app/addons/rs/selector-tests.server.js b/source/lib/tests/autoload/app/addons/rs/selector-tests.server.js new file mode 100644 index 000000000..6ba1bff2c --- /dev/null +++ b/source/lib/tests/autoload/app/addons/rs/selector-tests.server.js @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2011-2012, Yahoo! Inc. All rights reserved. + * Copyrights licensed under the New BSD License. + * See the accompanying LICENSE file for terms. + */ +YUI.add('mojito-addon-rs-selector-tests', function(Y, NAME) { + + var suite = new YUITest.TestSuite(NAME), + libpath = require('path'), + mojitoRoot = libpath.join(__dirname, '../../../../../'), + A = YUITest.Assert; + + + function MockRS(config) { + MockRS.superclass.constructor.apply(this, arguments); + } + MockRS.NAME = 'MockResourceStore'; + MockRS.ATTRS = {}; + Y.extend(MockRS, Y.Base, { + initializer: function(cfg) { + this._config = cfg || {}; + this.selectors = { + 'devdroid': true, + 'droid': true, + 'shelves': true, + 'right': true, + '*': true + }; + }, + validateContext: function() {}, + cloneObj: function(o) { + return Y.clone(o); + } + }); + + + function cmp(x, y, msg, path) { + if (Y.Lang.isArray(x)) { + A.isArray(x, msg || 'first arg should be an array'); + A.isArray(y, msg || 'second arg should be an array'); + A.areSame(x.length, y.length, msg || 'arrays are different lengths'); + for (var i = 0; i < x.length; i += 1) { + cmp(x[i], y[i], msg); + } + return; + } + if (Y.Lang.isObject(x)) { + A.isObject(x, msg || 'first arg should be an object'); + A.isObject(y, msg || 'second arg should be an object'); + A.areSame(Object.keys(x).length, Object.keys(y).length, msg || 'object keys are different lengths'); + for (var i in x) { + if (x.hasOwnProperty(i)) { + cmp(x[i], y[i], msg); + } + } + return; + } + A.areSame(x, y, msg || 'args should be the same'); + } + + + suite.add(new YUITest.TestCase({ + + name: 'selector rs addon tests', + + 'read dimensions': function() { + // from mojito + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + store.plug(Y.mojito.addons.rs.selector, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var have = store.selector.getPOSLFromContext({}); + var want = ['*']; + cmp(have, want); + + var have = store.selector.getPOSLFromContext({runtime:'client'}); + var want = ['right', '*']; + cmp(have, want); + + var have = store.selector.getPOSLFromContext({runtime:'server'}); + var want = ['shelves', '*']; + cmp(have, want); + + var have = store.selector.getPOSLFromContext({device:'android'}); + var want = ['droid', '*']; + cmp(have, want); + + var have = store.selector.getPOSLFromContext({runtime:'server', device:'android'}); + var want = ['shelves', 'droid', '*']; + cmp(have, want); + + var have = store.selector.getPOSLFromContext({device:'android', environment:'dev'}); + var want = ['devdroid', 'droid', '*']; + cmp(have, want); + + var have = store.selector.getPOSLFromContext({runtime:'server', device:'android', environment:'dev'}); + var want = ['shelves', 'devdroid', 'droid', '*']; + cmp(have, want); + }, + + + 'get all posls': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + store.plug(Y.mojito.addons.rs.selector, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var have = store.selector.getAllPOSLs(); + var want = [ + [ 'shelves', '*' ], + [ 'shelves', 'devdroid', 'droid', '*' ], + [ 'shelves', 'droid', '*' ], + [ 'right', '*' ], + [ 'right', 'devdroid', 'droid', '*' ], + [ 'right', 'droid', '*' ] + ]; + cmp(want, have); + }, + + + 'list used dimensions': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + store.plug(Y.mojito.addons.rs.selector, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var have = store.selector._listUsedDimensions(); + var want = { + runtime: ['server', 'client'], + device: ['iphone', 'android'], + environment: ['dev'] + } + cmp(want, have); + }, + + + 'list used contexts': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.config, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + store.plug(Y.mojito.addons.rs.selector, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var have = store.selector._listUsedContexts(); + var want = [ + { runtime: 'server', device: 'iphone', environment: 'dev' }, + { runtime: 'server', device: 'iphone' }, + { runtime: 'server', device: 'android', environment: 'dev' }, + { runtime: 'server', device: 'android' }, + { runtime: 'server', environment: 'dev' }, + { runtime: 'server' }, + { runtime: 'client', device: 'iphone', environment: 'dev' }, + { runtime: 'client', device: 'iphone' }, + { runtime: 'client', device: 'android', environment: 'dev' }, + { runtime: 'client', device: 'android' }, + { runtime: 'client', environment: 'dev' }, + { runtime: 'client' } + ]; + cmp(want, have); + } + + + })); + + YUITest.TestRunner.add(suite); + +}, '0.0.1', {requires: [ + 'base', + 'oop', + 'addon-rs-config', + 'addon-rs-selector' +]}); diff --git a/source/lib/tests/autoload/app/addons/rs/url-tests.server.js b/source/lib/tests/autoload/app/addons/rs/url-tests.server.js new file mode 100644 index 000000000..c843ac040 --- /dev/null +++ b/source/lib/tests/autoload/app/addons/rs/url-tests.server.js @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2012, Yahoo! Inc. All rights reserved. + * Copyrights licensed under the New BSD License. + * See the accompanying LICENSE file for terms. + */ +YUI.add('mojito-addon-rs-url-tests', function(Y, NAME) { + + var suite = new YUITest.TestSuite(NAME), + libfs = require('fs'), + libpath = require('path'), + mojitoRoot = libpath.join(__dirname, '../../../../../'), + A = YUITest.Assert, + OA = YUITest.ObjectAssert, + AA = YUITest.ArrayAssert; + + + function MockRS(config) { + MockRS.superclass.constructor.apply(this, arguments); + } + MockRS.NAME = 'MockResourceStore'; + MockRS.ATTRS = {}; + Y.extend(MockRS, Y.Base, { + + initializer: function(cfg) { + this._config = cfg || {}; + this._mojits = {}; + this._appRVs = []; + this._mojitRVs = {}; + this.publish('getMojitTypeDetails', {emitFacade: true, preventable: false}); + this.config = { + readConfigJSON: function() { return {} } + }; + }, + + getStaticAppConfig: function() { + return Y.clone(this._config.appConfig); + }, + + listAllMojits: function() { + return Object.keys(this._mojits); + }, + + getResourceVersions: function(filter) { + var source, + out = [], + r, + res, + k, + use; + source = filter.mojit ? this._mojitRVs[filter.mojit] : this._appRVs; + if (!source) { + return []; + } + for (r = 0; r < source.length; r += 1) { + res = source[r]; + use = true; + for (k in filter) { + if (filter.hasOwnProperty(k)) { + if (res[k] !== filter[k]) { + use = false; + break; + } + } + } + if (use) { + out.push(res); + } + } + return out; + }, + + preloadResourceVersions: function() { + // no-op + }, + + _makeResource: function(pkg, mojit, type, name, affinity, yuiName) { + if (mojit && mojit !== 'shared') { + this._mojits[mojit] = true; + } + var res = { + source: { + fs: { + fullPath: 'path/for/' + type + '--' + name + '.' + affinity + '.ext', + rootDir: 'path/for' + }, + pkg: { + name: pkg + } + }, + mojit: mojit, + type: type, + name: name, + id: type + '--' + name, + affinity: { affinity: affinity } + } + if (yuiName) { + res.yui = { name: yuiName }; + } + if (mojit) { + if (!this._mojitRVs[mojit]) { + this._mojitRVs[mojit] = []; + } + this._mojitRVs[mojit].push(res); + } else { + this._appRVs.push(res); + } + } + + }); + + + function cmp(x, y, msg, path) { + if (Y.Lang.isArray(x)) { + A.isArray(x, msg || 'first arg should be an array'); + A.isArray(y, msg || 'second arg should be an array'); + A.areSame(x.length, y.length, msg || 'arrays are different lengths'); + for (var i = 0; i < x.length; i += 1) { + cmp(x[i], y[i], msg); + } + return; + } + if (Y.Lang.isObject(x)) { + A.isObject(x, msg || 'first arg should be an object'); + A.isObject(y, msg || 'second arg should be an object'); + A.areSame(Object.keys(x).length, Object.keys(y).length, msg || 'object keys are different lengths'); + for (var i in x) { + if (x.hasOwnProperty(i)) { + cmp(x[i], y[i], msg); + } + } + return; + } + A.areSame(x, y, msg || 'args should be the same'); + } + + + suite.add(new YUITest.TestCase({ + + name: 'url rs addon tests', + + 'skip mojito-provided server-only mojits': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ + root: fixtures, + appConfig: {} + }); + store.plug(Y.mojito.addons.rs.url, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('mojito', null, 'mojit', 'X', 'common'); + store._makeResource('mojito', 'X', 'controller', 'controller', 'server'); + store.preloadResourceVersions(); + A.isUndefined(store._mojitRVs.X[0].url); + }, + + + 'include mojito-provided non-server-only mojits': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ + root: fixtures, + appConfig: {} + }); + store.plug(Y.mojito.addons.rs.url, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('mojito', null, 'mojit', 'X', 'common'); + store._makeResource('mojito', 'X', 'controller', 'controller', 'common'); + store._makeResource('mojito', null, 'mojit', 'Y', 'common'); + store._makeResource('mojito', 'Y', 'controller', 'controller', 'client'); + store._makeResource('mojito', 'Y', 'controller', 'controller', 'server'); + store.preloadResourceVersions(); + A.areSame(1, store._mojitRVs.X.length); + A.areSame('/static/X/controller--controller.common.ext', store._mojitRVs.X[0].url); + A.areSame(2, store._mojitRVs.Y.length); + A.areSame('/static/Y/controller--controller.client.ext', store._mojitRVs.Y[0].url); + A.areSame('/static/Y/controller--controller.server.ext', store._mojitRVs.Y[1].url); + }, + + + 'resources in "shared" mojit': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ + root: fixtures, + appConfig: {} + }); + store.plug(Y.mojito.addons.rs.url, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('mojito', 'shared', 'x', 'y', 'common'); + store._makeResource('orange', 'shared', 'x', 'y', 'common'); + store.preloadResourceVersions(); + A.areSame('/static/mojito/x--y.common.ext', store._mojitRVs.shared[0].url); + A.areSame('/static/store/x--y.common.ext', store._mojitRVs.shared[1].url); + }, + + + 'normal mojit resources': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ + root: fixtures, + appConfig: {} + }); + store.plug(Y.mojito.addons.rs.url, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('orange', null, 'mojit', 'X', 'common'); + store._makeResource('orange', 'X', 'x', 'y', 'common'); + store._makeResource('orange', null, 'mojit', 'Y', 'common'); + store._makeResource('orange', 'Y', 'x', 'y', 'common'); + store.preloadResourceVersions(); + A.areSame('/static/X/x--y.common.ext', store._mojitRVs.X[0].url); + A.areSame('/static/Y/x--y.common.ext', store._mojitRVs.Y[0].url); + }, + + + 'configuration via appConfig': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ + root: fixtures, + appConfig: { + staticHandling: { + prefix: '', + frameworkName: 'FFF', + appName: 'AAA' + } + } + }); + store.plug(Y.mojito.addons.rs.url, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('mojito', 'shared', 'x', 'y', 'common'); + store._makeResource('orange', 'shared', 'x', 'y', 'common'); + store.preloadResourceVersions(); + A.areSame('/FFF/x--y.common.ext', store._mojitRVs.shared[0].url); + A.areSame('/AAA/x--y.common.ext', store._mojitRVs.shared[1].url); + }, + + + 'assume rollups': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ + root: fixtures, + appConfig: { + staticHandling: { assumeRollups: true } + } + }); + store.plug(Y.mojito.addons.rs.url, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('mojito', 'shared', 'x', 'y', 'common', 'red'); + store._makeResource('orange', 'shared', 'x', 'y', 'common', 'red'); + store._makeResource('orange', null, 'mojit', 'X', 'common'); + store._makeResource('orange', 'X', 'x', 'y', 'common', 'red'); + store._makeResource('orange', null, 'mojit', 'Y', 'common'); + store._makeResource('orange', 'Y', 'x', 'y', 'common', 'red'); + store._makeResource('orange', 'Y', 'not', 'yui', 'common'); + store.preloadResourceVersions(); + A.areSame('/static/store/rollup.client.js', store._mojitRVs.shared[0].url); + A.areSame(libpath.join(fixtures, 'rollup.client.js'), store._mojitRVs.shared[0].source.fs.rollupPath); + A.areSame('/static/store/rollup.client.js', store._mojitRVs.shared[1].url); + A.areSame(libpath.join(fixtures, 'rollup.client.js'), store._mojitRVs.shared[1].source.fs.rollupPath); + A.areSame('/static/X/rollup.client.js', store._mojitRVs.X[0].url); + A.areSame('path/for/mojit--X.common.ext/rollup.client.js', store._mojitRVs.X[0].source.fs.rollupPath); + A.areSame('/static/Y/rollup.client.js', store._mojitRVs.Y[0].url); + A.areSame('path/for/mojit--Y.common.ext/rollup.client.js', store._mojitRVs.Y[0].source.fs.rollupPath); + A.areSame('/static/Y/not--yui.common.ext', store._mojitRVs.Y[1].url); + A.isUndefined(store._mojitRVs.Y[1].source.fs.rollupPath); + }, + + + 'augment getMojitTypeDetails': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + + var store = new MockRS({ + root: fixtures, + appConfig: {} + }); + store.getResources = function() { + return [ { url: 'TEST' } ]; + }; + store.plug(Y.mojito.addons.rs.url, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + var mojit = {}; + store.fire('getMojitTypeDetails', { + args: { + env: 'server', + ctx: {}, + mojitType: 'Foo' + }, + mojit: mojit + }); + A.areSame('TEST/assets', mojit.assetsRoot); + } + + + })); + + YUITest.TestRunner.add(suite); + +}, '0.0.1', {requires: ['base', 'oop', 'addon-rs-url']}); diff --git a/source/lib/tests/autoload/app/addons/rs/yui-tests.server.js b/source/lib/tests/autoload/app/addons/rs/yui-tests.server.js new file mode 100644 index 000000000..a3a6bc3bc --- /dev/null +++ b/source/lib/tests/autoload/app/addons/rs/yui-tests.server.js @@ -0,0 +1,771 @@ +/* + * Copyright (c) 2011-2012, Yahoo! Inc. All rights reserved. + * Copyrights licensed under the New BSD License. + * See the accompanying LICENSE file for terms. + */ +YUI.add('mojito-addon-rs-yui-tests', function(Y, NAME) { + + var suite = new YUITest.TestSuite(NAME), + libpath = require('path'), + mojitoRoot = libpath.join(__dirname, '../../../../../'), + A = YUITest.Assert, + AA = YUITest.ArrayAssert; + + + function MockRS(config) { + MockRS.superclass.constructor.apply(this, arguments); + } + MockRS.NAME = 'MockResourceStore'; + MockRS.ATTRS = {}; + Y.extend(MockRS, Y.Base, { + + initializer: function(cfg) { + this._config = cfg || {}; + this.RVs = {}; + this._mojitResources = {}; // env: ctx: mojitType: list of resources + this._appResources = {}; // env: ctx: list of resources + this._mojits = {}; + this.publish('getMojitTypeDetails', {emitFacade: true, preventable: false}); + this._appConfig = { + yui: { + dependencyCalculations: 'precomputed' + } + }; + }, + + listAllMojits: function() { + return Object.keys(this._mojits); + }, + + getStaticAppConfig: function() { + return Y.clone(this._appConfig, true); + }, + + getResources: function(env, ctx, filter) { + var source, + out = [], + r, + res, + k, + use; + + ctx = Y.JSON.stringify(ctx); + if (filter.mojit) { + if (!this._mojitResources[env] || + !this._mojitResources[env][ctx] || + !this._mojitResources[env][ctx][filter.mojit]) { + return []; + } + source = this._mojitResources[env][ctx][filter.mojit]; + } else { + if (!this._appResources[env] || + !this._appResources[env][ctx]) { + return []; + } + source = this._appResources[env][ctx]; + } + // this is taken care of already, and will trip up mojit-level + // resources that are actually shared + delete filter.mojit; + for (r = 0; r < source.length; r += 1) { + res = source[r]; + use = true; + for (k in filter) { + if (filter.hasOwnProperty(k)) { + if (res[k] !== filter[k]) { + use = false; + break; + } + } + } + if (use) { + out.push(res); + } + } + return out; + }, + + findResourceVersionByConvention: function(source, mojitType) { + // no-op + }, + + parseResourceVersion: function(source, type, subtype, mojitType) { + // no-op + }, + + addResourceVersion: function(res) { + this.RVs[[res.affinity, res.selector, res.id].join('/')] = res; + }, + + _makeResource: function(env, ctx, mojit, type, name, yuiName, pkgName) { + if (mojit && mojit !== 'shared') { + this._mojits[mojit] = true; + } + var res = { + source: { + fs: { + fullPath: 'path/for/' + type + '--' + name + '.common.ext', + rootDir: 'path/for' + }, + pkg: { name: (pkgName || 'testing') } + }, + mojit: mojit, + type: type, + name: name, + id: type + '--' + name + } + if (yuiName) { + res.yui = { name: yuiName }; + } + ctx = Y.JSON.stringify(ctx); + if (mojit) { + if (!this._mojitResources[env]) { + this._mojitResources[env] = {}; + } + if (!this._mojitResources[env][ctx]) { + this._mojitResources[env][ctx] = {}; + } + if (!this._mojitResources[env][ctx][mojit]) { + this._mojitResources[env][ctx][mojit] = []; + } + this._mojitResources[env][ctx][mojit].push(res); + } else { + if (!this._appResources[env]) { + this._appResources[env] = {}; + } + if (!this._appResources[env][ctx]) { + this._appResources[env][ctx] = []; + } + this._appResources[env][ctx].push(res); + } + } + + }); + + + function cmp(x, y, msg, path) { + if (Y.Lang.isArray(x)) { + A.isArray(x, msg || 'first arg should be an array'); + A.isArray(y, msg || 'second arg should be an array'); + A.areSame(x.length, y.length, msg || 'arrays are different lengths'); + for (var i = 0; i < x.length; i += 1) { + cmp(x[i], y[i], msg); + } + return; + } + if (Y.Lang.isObject(x)) { + A.isObject(x, msg || 'first arg should be an object'); + A.isObject(y, msg || 'second arg should be an object'); + A.areSame(Object.keys(x).length, Object.keys(y).length, msg || 'object keys are different lengths'); + for (var i in x) { + if (x.hasOwnProperty(i)) { + cmp(x[i], y[i], msg); + } + } + return; + } + A.areSame(x, y, msg || 'args should be the same'); + } + + + function makeSource(dir, dirType, subdir, file, isFile) { + var source = { + fs: { + fullPath: libpath.join(dir, subdir, file), + rootDir: dir, + rootType: dirType, + subDir: subdir, + subDirArray: subdir.split('/'), + isFile: isFile, + ext: libpath.extname(file) + }, + pkg: { + name: 'unittest', + version: '999.666.999', + depth: 999 + } + }; + source.fs.basename = libpath.basename(file, source.fs.ext); + return source; + } + + + suite.add(new YUITest.TestCase({ + + name: 'yui rs addon tests', + + 'find yui resources': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.yui, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var source = makeSource(fixtures, 'app', 'autoload', 'x.server.txt', true); + var have = store.findResourceVersionByConvention(source, null); + var want = undefined; + cmp(have, want); + + source = makeSource(fixtures, 'app', 'blix', 'x.server.js', true); + have = store.findResourceVersionByConvention(source, null); + want = undefined; + cmp(have, want); + + source = makeSource(fixtures, 'app', 'autoload', 'x.server.js', true); + have = store.findResourceVersionByConvention(source, null); + want = { type: 'yui-module', skipSubdirParts: 1 }; + cmp(have, want); + + source = makeSource(fixtures, 'app', 'yui_modules', 'x.server.js', true); + have = store.findResourceVersionByConvention(source, null); + want = { type: 'yui-module', skipSubdirParts: 1 }; + cmp(have, want); + + source = makeSource(fixtures, 'app', 'lang', 'x.server.js', true); + have = store.findResourceVersionByConvention(source, null); + want = { type: 'yui-lang', skipSubdirParts: 1 }; + cmp(have, want); + }, + + + 'parse found resource': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/conventions'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.yui, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var source = makeSource(fixtures, 'app', 'autoload', 'm.common.js', true); + var res = store.parseResourceVersion(source, 'yui-module'); + A.isNotUndefined(res); + cmp(res.source, source); + A.areSame('yui-module', res.type); + A.areSame('common', res.affinity); + A.areSame('*', res.selector); + A.areSame('m', res.name); + A.areSame('yui-module--m', res.id); + A.isUndefined(res.mojit); + + source = makeSource(fixtures, 'app', 'autoload', 'm.common.iphone.js', true); + res = store.parseResourceVersion(source, 'yui-module'); + A.isNotUndefined(res); + cmp(res.source, source); + A.areSame('yui-module', res.type); + A.areSame('common', res.affinity); + A.areSame('iphone', res.selector); + A.areSame('m', res.name); + A.areSame('yui-module--m', res.id); + A.isUndefined(res.mojit); + + source = makeSource(fixtures, 'app', 'yui_modules', 'x.common.js', true); + res = store.parseResourceVersion(source, 'yui-module'); + A.isNotUndefined(res); + cmp(res.source, source); + A.areSame('yui-module', res.type); + A.areSame('common', res.affinity); + A.areSame('*', res.selector); + A.areSame('x', res.name); + A.areSame('yui-module--x', res.id); + A.isUndefined(res.mojit); + + source = makeSource(fixtures, 'bundle', 'lang', 'testing.js', true); + res = store.parseResourceVersion(source, 'yui-lang', undefined, 'testing'); + A.isNotUndefined(res); + cmp(res.source, source); + A.areSame('yui-lang', res.type); + A.areSame('common', res.affinity); + A.areSame('*', res.selector); + A.areSame('', res.name); + A.areSame('yui-lang--', res.id); + A.areSame('testing', res.mojit); + + source = makeSource(fixtures, 'bundle', 'lang', 'testing_de.js', true); + res = store.parseResourceVersion(source, 'yui-lang', undefined, 'testing'); + A.isNotUndefined(res); + cmp(res.source, source); + A.areSame('yui-lang', res.type); + A.areSame('common', res.affinity); + A.areSame('*', res.selector); + A.areSame('de', res.name); + A.areSame('yui-lang--de', res.id); + A.areSame('testing', res.mojit); + + source = makeSource(fixtures, 'bundle', 'lang', 'testing_en-US.js', true); + res = store.parseResourceVersion(source, 'yui-lang', undefined, 'testing'); + A.isNotUndefined(res); + cmp(res.source, source); + A.areSame('yui-lang', res.type); + A.areSame('common', res.affinity); + A.areSame('*', res.selector); + A.areSame('en-US', res.name); + A.areSame('yui-lang--en-US', res.id); + A.areSame('testing', res.mojit); + }, + + + 'parse other resources': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/conventions'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.yui, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var source = makeSource(fixtures+'/mojits/X', 'mojit', '.', 'controller.common.js', true); + var res = { + source: source, + mojit: 'X', + type: 'controller', + name: 'controller', + id: 'controller--controller', + affinity: 'common', + selector: '*' + }; + store.addResourceVersion(res); + res = store.RVs['common/*' + '/controller--controller']; + cmp(res.source, source); + A.isNotUndefined(res.yui); + A.areSame('X', res.yui.name); + + source = makeSource(fixtures+'/mojits/X', 'mojit', 'assets', 'foo.common.js', true); + res = { + source: source, + mojit: 'X', + type: 'asset', + name: 'foo', + id: 'asset-js-foo', + affinity: 'common', + selector: '*' + }; + store.addResourceVersion(res); + res = store.RVs['common/*' + '/asset-js-foo']; + cmp(res.source, source); + A.isUndefined(res.yui); + }, + + + 'augment getMojitTypeDetails': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.yui, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('server', {}, 'Foo', 'binder', 'index', 'FooBinderIndex'); + store._makeResource('server', {}, 'Foo', 'binder', 'list', 'FooBinderList'); + store._makeResource('server', {}, 'Foo', 'controller', 'controller', 'FooController'); + var mojit = { views: {} }; + store.fire('getMojitTypeDetails', { + args: { + env: 'server', + ctx: {}, + mojitType: 'Foo' + }, + mojit: mojit + }); + A.isNotUndefined(mojit.views.index); + A.areSame('FooBinderIndex', mojit.views.index['binder-module']); + A.isNotUndefined(mojit.views.list); + A.areSame('FooBinderList', mojit.views.list['binder-module']); + A.areSame('FooController', mojit['controller-module']); + }, + + + 'find and parse resources by convention': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/conventions'), + store = new Y.mojito.ResourceStore({ root: fixtures }); + + // fake out some parts of preload(), which we're trying to avoid + store._fwConfig = store.config.readConfigJSON(libpath.join(mojitoRoot, 'config.json')); + store._appConfigStatic = store.getStaticAppConfig(); + store.plug(Y.mojito.addons.rs.yui, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + var pkg = { name: 'test', version: '6.6.6' }; + var mojitType = 'testing'; + var ress = store._findResourcesByConvention(fixtures, 'app', pkg, mojitType) + + var r, res; + for (r = 0; r < ress.length; r++) { + res = ress[r]; + A.isNotUndefined(res.id, 'no resource id'); + switch (res.id) { + case 'action--x': + case 'action--y/z': + case 'addon-a-x': + case 'archetype-x-y': + case 'asset-css-x': + case 'asset-css-y/z': + case 'binder--x': + case 'command--x': + case 'config--config': + case 'controller--controller': + case 'middleware--x': + case 'spec--default': + case 'spec--x': + case 'view--x': + break; + case 'yui-lang--': + A.areSame(pkg, res.source.pkg); + A.areSame('yui-lang', res.type); + A.areSame('', res.name); + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.', res.source.fs.subDir); + A.areSame('testing', res.source.fs.basename); + A.areSame('.js', res.source.fs.ext); + break; + case 'yui-lang--de': + A.areSame(pkg, res.source.pkg); + A.areSame('yui-lang', res.type); + A.areSame('de', res.name); + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.', res.source.fs.subDir); + A.areSame('testing_de', res.source.fs.basename); + A.areSame('.js', res.source.fs.ext); + break; + case 'yui-lang--en': + A.areSame(pkg, res.source.pkg); + A.areSame('yui-lang', res.type); + A.areSame('en', res.name); + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.', res.source.fs.subDir); + A.areSame('testing_en', res.source.fs.basename); + A.areSame('.js', res.source.fs.ext); + break; + case 'yui-lang--en-US': + A.areSame(pkg, res.source.pkg); + A.areSame('yui-lang', res.type); + A.areSame('en-US', res.name); + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.', res.source.fs.subDir); + A.areSame('testing_en-US', res.source.fs.basename); + A.areSame('.js', res.source.fs.ext); + break; + case 'yui-module--m': + A.areSame(pkg, res.source.pkg); + A.areSame('yui-module', res.type); + A.areSame('m', res.name); + A.areSame('m', res.yui.name); + switch (res.source.fs.basename) { + case 'm.common': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + break; + case 'm.common.iphone': + A.areSame('iphone', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + break; + default: + A.fail('unknown resource ' + res.source.fs.fullPath); + break; + } + break; + case 'yui-module--x': + A.areSame(pkg, res.source.pkg); + A.areSame('yui-module', res.type); + A.areSame('x', res.name); + A.areSame('x', res.yui.name); + switch (res.source.fs.basename) { + case 'x.common': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + break; + case 'x.common.iphone': + A.areSame('iphone', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + break; + default: + A.fail('unknown resource ' + res.source.fs.fullPath); + break; + } + break; + case 'yui-module--z': + A.areSame(pkg, res.source.pkg); + A.areSame('yui-module', res.type); + A.areSame('z', res.name); + A.areSame('z', res.yui.name); + A.areSame('y', res.source.fs.subDir); + switch (res.source.fs.basename) { + case 'z.common': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + break; + case 'z.common.android': + A.areSame('android', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + break; + default: + A.fail('unknown resource ' + res.source.fs.fullPath); + break; + } + break; + + default: + A.fail('unknown resource ' + res.id); + break; + } + } + A.areSame(31, ress.length, 'wrong number of resources'); + }, + + + 'server mojit instance yui': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + var store = new Y.mojito.ResourceStore({ root: fixtures, mojitoRoot: mojitoRoot }); + store.preload(); + + var instance = {type:'TestMojit2'}; + store.expandInstance(instance, {}, function(err, instance) { + A.isNotUndefined(instance.yui); + + A.isNotUndefined(instance.yui.config); + A.isNotUndefined(instance.yui.config.modules); + A.isNotUndefined(instance.yui.config.modules['test_mojit_2']); + A.areSame(libpath.join(fixtures, 'mojits/test_mojit_2/controller.server.js'), instance.yui.config.modules['test_mojit_2'].fullpath); + A.isNotUndefined(instance.yui.config.modules['mojito-mu']); + A.areSame(libpath.join(mojitoRoot, 'app/addons/view-engines/mu.server.js'), instance.yui.config.modules['mojito-mu'].fullpath); + + A.isArray(instance.yui.sorted); + AA.contains('test_mojit_2', instance.yui.sorted); + AA.doesNotContain('test_applevelModel', instance.yui.sorted); + AA.doesNotContain('ModelFlickr', instance.yui.sorted); + AA.contains('mojito-mu', instance.yui.sorted); + AA.contains('mojito', instance.yui.sorted); + + A.isObject(instance.yui.sortedPaths); + A.areSame(libpath.join(fixtures, 'mojits/test_mojit_2/controller.server.js'), instance.yui.sortedPaths['test_mojit_2']); + A.isUndefined(instance.yui.sortedPaths['test_applevelModel']); + A.isUndefined(instance.yui.sortedPaths['ModelFlickr']); + A.areSame(libpath.join(mojitoRoot, 'app/addons/view-engines/mu.server.js'), instance.yui.sortedPaths['mojito-mu']); + A.areSame(libpath.join(mojitoRoot, 'app/autoload/mojito.common.js'), instance.yui.sortedPaths['mojito']); + }); + }, + + + 'server mojit instance yui - precomputed': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/precomputed'); + var store = new Y.mojito.ResourceStore({ root: fixtures }); + store.preload(); + + var instance = { type:'PagedFlickr' }; + store.expandInstance(instance, {}, function(err, instance) { + A.isNotUndefined(instance.yui); + + A.isArray(instance.yui.sorted); + AA.contains('intl', instance.yui.sorted); + AA.contains('datatype-date-format', instance.yui.sorted); + AA.contains('mojito', instance.yui.sorted); + AA.contains('mojito-util', instance.yui.sorted); + AA.contains('mojito-intl-addon', instance.yui.sorted); + AA.contains('lang/PagedFlickr_de', instance.yui.sorted); + AA.contains('lang/PagedFlickr_en', instance.yui.sorted); + AA.contains('lang/PagedFlickr_en-US', instance.yui.sorted); + + A.isObject(instance.yui.sortedPaths); + A.isNotUndefined(instance.yui.sortedPaths['intl']); + A.isNotUndefined(instance.yui.sortedPaths['datatype-date-format']); + A.isNotUndefined(instance.yui.sortedPaths['mojito']); + A.isNotUndefined(instance.yui.sortedPaths['mojito-util']); + A.isNotUndefined(instance.yui.sortedPaths['mojito-intl-addon']); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/controller.common.js'), instance.yui.sortedPaths['PagedFlickr']); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_de.js'), instance.yui.sortedPaths['lang/PagedFlickr_de']); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_en.js'), instance.yui.sortedPaths['lang/PagedFlickr_en']); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_en-US.js'), instance.yui.sortedPaths['lang/PagedFlickr_en-US']); + + // the particular datatype-date-format for no-lang is up to YUI, + // so this test is a little fragile + AA.contains('lang/datatype-date-format_en', instance.yui.sorted); + A.isNotUndefined(instance.yui.sortedPaths['lang/datatype-date-format_en']); + }); + }, + + + 'server mojit instance yui - ondemand': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/ondemand'); + var store = new Y.mojito.ResourceStore({ root: fixtures }); + store.preload(); + + var instance = { type:'PagedFlickr' }; + store.expandInstance(instance, {}, function(err, instance) { + A.isNotUndefined(instance.yui); + + A.isArray(instance.yui.sorted); + AA.contains('mojito-dispatcher', instance.yui.sorted); + AA.contains('mojito-mu', instance.yui.sorted); + AA.contains('PagedFlickr', instance.yui.sorted); + AA.contains('lang/PagedFlickr_de', instance.yui.sorted); + AA.contains('lang/PagedFlickr_en', instance.yui.sorted); + AA.contains('lang/PagedFlickr_en-US', instance.yui.sorted); + + A.isUndefined(instance.yui.sortedPaths); + }); + }, + + + 'server mojit instance yui - precomputed+ondemand': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/precomputed-ondemand'); + var store = new Y.mojito.ResourceStore({ root: fixtures }); + store.preload(); + + var instance = { type:'PagedFlickr' }; + store.expandInstance(instance, {}, function(err, instance) { + A.isNotUndefined(instance.yui); + + A.isArray(instance.yui.sorted); + AA.contains('intl', instance.yui.sorted, 'contains intl'); + AA.contains('datatype-date-format', instance.yui.sorted, 'contains datatype-date-format'); + AA.contains('mojito', instance.yui.sorted, 'contains mojito'); + AA.contains('mojito-util', instance.yui.sorted, 'contains mojito-util'); + AA.contains('mojito-intl-addon', instance.yui.sorted, 'contains mojito-intl-addon'); + AA.contains('lang/PagedFlickr_de', instance.yui.sorted); + AA.contains('lang/PagedFlickr_en', instance.yui.sorted); + AA.contains('lang/PagedFlickr_en-US', instance.yui.sorted); + AA.doesNotContain('lang/datatype-date-format_de', instance.yui.sorted, 'does not contain datatype-date-format_de'); + AA.contains('lang/datatype-date-format_en', instance.yui.sorted, 'contains datatype-date-format_en'); + AA.doesNotContain('lang/datatype-date-format_en-US', instance.yui.sorted, 'does not contain datatype-date-format_en-US'); + + A.isObject(instance.yui.sortedPaths); + A.isNotUndefined(instance.yui.sortedPaths['intl']); + A.isNotUndefined(instance.yui.sortedPaths['datatype-date-format']); + A.isNotUndefined(instance.yui.sortedPaths['mojito']); + A.isNotUndefined(instance.yui.sortedPaths['mojito-util']); + A.isNotUndefined(instance.yui.sortedPaths['mojito-intl-addon']); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/controller.common.js'), instance.yui.sortedPaths['PagedFlickr']); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_de.js'), instance.yui.sortedPaths['lang/PagedFlickr_de']); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_en.js'), instance.yui.sortedPaths['lang/PagedFlickr_en']); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_en-US.js'), instance.yui.sortedPaths['lang/PagedFlickr_en-US']); + A.isUndefined(instance.yui.sortedPaths['lang/datatype-date-format_de']); + A.isNotUndefined(instance.yui.sortedPaths['lang/datatype-date-format_en']); + A.isUndefined(instance.yui.sortedPaths['lang/datatype-date-format_en-US']); + }); + }, + + + 'stuff with ctx{lang:}, in language fallback': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/gsg5'), + store = new Y.mojito.ResourceStore({ root: fixtures }), + ctx, spec; + store.preload(); + + // first test + ctx = { lang: 'en-US' }; + spec = { type: 'PagedFlickr' }; + store.expandInstance(spec, ctx, function(err, instance) { + A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is undefined {lang:en-US}'); + A.isUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en'], 'en is not undefined {lang:en-US}'); + + // second test + ctx = { lang: 'en' }; + spec = { type: 'PagedFlickr' }; + store.expandInstance(spec, ctx, function(err, instance) { + A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en'], 'en is undefined {lang-en}'); + A.isUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is not undefined {lang:en}'); + + // third test + ctx = { lang: 'de-AT' }; + spec = { type: 'PagedFlickr' }; + store.expandInstance(spec, ctx, function(err, instance) { + A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_de'], 'de is undefined {lang:de-AT}'); + A.isUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is not undefined {lang:de-AT}'); + + // fourth test + ctx = { lang: 'tr-TR' }; + spec = { type: 'PagedFlickr' }; + store.expandInstance(spec, ctx, function(err, instance) { + A.isTrue( + Boolean(instance.yui.sortedPaths['lang/PagedFlickr_de']), + 'de is undefined {lang:tr-TR}' + ); + A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is undefined {lang:tr-TR}'); + + // fifth test + ctx = {}; + spec = { type: 'PagedFlickr' }; + store.expandInstance(spec, ctx, function(err, instance) { + A.isTrue( + Boolean(instance.yui.sortedPaths['lang/PagedFlickr_de']), + 'de is undefined {}' + ); + A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is undefined {}'); + }); + }); + }); + }); + }); + }, + + + 'appConfig yui.base': function() { + var fixtures = libpath.join(__dirname, '../../../../fixtures/gsg5-appConfig'), + store = new Y.mojito.ResourceStore({ root: fixtures }); + store.preload(); + var spec = { type: 'PagedFlickr' }; + store.expandInstance(spec, {}, function(err, instance) { + A.areSame('/foo/', instance.yui.sortedPaths['oop'].substr(0, 5)); + A.areSame('/foo/', instance.yui.sortedPaths['intl'].substr(0, 5)); + A.areSame('/foo/', instance.yui.sortedPaths['jsonp'].substr(0, 5)); + A.areSame('/foo/', instance.yui.sortedPaths['yql'].substr(0, 5)); + A.areSame('/foo/', instance.yui.sortedPaths['querystring-parse'].substr(0, 5)); + A.areSame('/foo/', instance.yui.sortedPaths['querystring-stringify'].substr(0, 5)); + A.areSame('/foo/', instance.yui.sortedPaths['json-stringify'].substr(0, 5)); + }); + }, + + + 'get config shared': function() { + var fixtures, + store, + config; + fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.yui, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('server', {}, 'shared', 'binder', 'index', 'FooBinderIndex'); + store._makeResource('server', {}, 'shared', 'binder', 'list', 'FooBinderList', 'mojito'); + store._makeResource('server', {}, 'Foo', 'controller', 'controller', 'FooController'); + + config = store.yui.getConfigShared('server', {}, false); + A.isNotUndefined(config.modules); + A.isNotUndefined(config.modules.FooBinderIndex); + A.isNotUndefined(config.modules.FooBinderList); + A.isUndefined(config.modules.FooController); + + config = store.yui.getConfigShared('server', {}, true); + A.isNotUndefined(config.modules); + A.isNotUndefined(config.modules.FooBinderIndex); + A.isUndefined(config.modules.FooBinderList); + A.isUndefined(config.modules.FooController); + }, + + + 'get config all mojits': function() { + var fixtures, + store, + config; + fixtures = libpath.join(__dirname, '../../../../fixtures/store'); + store = new MockRS({ root: fixtures }); + store.plug(Y.mojito.addons.rs.yui, { appRoot: fixtures, mojitoRoot: mojitoRoot } ); + + store._makeResource('server', {}, 'shared', 'binder', 'index', 'FooBinderIndex'); + store._makeResource('server', {}, 'Foo', 'binder', 'list', 'FooBinderList', 'mojito'); + store._makeResource('server', {}, 'Bar', 'controller', 'controller', 'BarController'); + + config = store.yui.getConfigAllMojits('server', {}, false); + A.isNotUndefined(config.modules); + A.isUndefined(config.modules.FooBinderIndex); + A.isNotUndefined(config.modules.FooBinderList); + A.isNotUndefined(config.modules.BarController); + } + + + })); + + YUITest.TestRunner.add(suite); + +}, '0.0.1', {requires: [ + 'base', + 'oop', + 'mojito-resource-store', + 'addon-rs-yui', + 'json-stringify' +]}); diff --git a/source/lib/tests/autoload/app/autoload/resource-store-adapter-tests.common.js b/source/lib/tests/autoload/app/autoload/resource-store-adapter-tests.common.js index fe883349a..4e1efe2bb 100644 --- a/source/lib/tests/autoload/app/autoload/resource-store-adapter-tests.common.js +++ b/source/lib/tests/autoload/app/autoload/resource-store-adapter-tests.common.js @@ -8,7 +8,7 @@ YUI.add('mojito-resource-store-adapter-tests', function(Y, NAME) { var suite = new YUITest.TestSuite(NAME), path = require('path'), fixtures = path.join(__dirname, '../../../fixtures/store'), - ResourceStore = require(path.join(__dirname, '../../../../store.server')), + resourceStore, dummyLog = {log: function() {}}, A = YUITest.Assert; @@ -16,47 +16,26 @@ YUI.add('mojito-resource-store-adapter-tests', function(Y, NAME) { name: 'Resource Store Adapter Tests', - 'pre load': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - + init: function() { + resourceStore = new Y.mojito.ResourceStore({ root: fixtures }); resourceStore.preload(); + }, - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - //Y.log(Y.JSON.stringify(store,null,4)); - + 'pre load': function() { + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); A.isTrue(store.getAppPath() === fixtures); }, 'server app config value': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - var config = store.getAppConfig(null, 'application'); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); + var config = store.getAppConfig(null); A.isTrue(config.testKey1 === 'testVal1'); }, 'server mojit config value': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {base:'test1'}; - store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.id === 'test1'); A.isTrue(instance.type === 'test_mojit_1'); A.isTrue(instance.config.testKey4 === 'testVal4'); @@ -64,18 +43,9 @@ YUI.add('mojito-resource-store-adapter-tests', function(Y, NAME) { }, 'server mojit config value via type': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {type:'test_mojit_1'}; - store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.type === 'test_mojit_1'); A.isTrue(instance.config.testKey4 === 'testVal4'); A.isTrue(instance.config.testKey6.testKey7 === 'testVal7'); @@ -83,21 +53,12 @@ YUI.add('mojito-resource-store-adapter-tests', function(Y, NAME) { }, 'server mojit config value via type & overrride': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = { type:'test_mojit_1', config:{testKey4: 'other'} }; - store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.type === 'test_mojit_1'); A.isTrue(instance.config.testKey4 === 'other'); A.isTrue(instance.config.testKey5 === 'testVal5'); @@ -105,220 +66,92 @@ YUI.add('mojito-resource-store-adapter-tests', function(Y, NAME) { }, 'server mojit config assets': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {type:'test_mojit_1'}; - store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.assets['css/main.css'] !== undefined); A.isTrue(instance.assets['js/main.js'] !== undefined); }); }, 'server mojit config views': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {type:'test_mojit_1'}; - store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.views['test_1'] !== undefined); A.isTrue(instance.views['test_2'] !== undefined); }); }, 'server mojit config models': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {type:'test_mojit_1'}; - store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.models['test_1'] !== undefined); A.isTrue(instance.models['test_2'] !== undefined); }); }, 'server mojit config actions': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {type:'test_mojit_1'}; - store.expandInstance(instance, {}, function(err, instance){ - - A.isTrue(instance.actions.length === 2); + A.isNotUndefined(instance.yui.config.modules['test_mojit_1_actions_test_1']); + A.isNotUndefined(instance.yui.config.modules['test_mojit_1_actions_test_2']); }); }, 'server mojit config appConfig': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var command = { type:'test_mojit_1', appConfig:{ testKey3: 'other' } }; - - store.expandInstance(command, {}, function(err, instance){ - - A.isTrue(instance.appConfig.testKey2 === 'testVal2'); - A.isTrue(instance.appConfig.testKey3 === 'other'); + store.expandInstance(command, {}, function(err, instance) { + A.areSame('testVal2', instance.appConfig.testKey2); + A.areSame('other', instance.appConfig.testKey3); }); }, 'TODO: server mojit config definition override': function() { - A.skip(); return; - var store = new ResourceStore(fixtures); - + var store = new Y.mojito.ResourceStore({ root: fixtures }); store.preload(); var command = {type:'test_mojit_1'}; - store.expandInstance(command, {}, function(err, instance){ - A.isTrue(instance.models['other_1'] === '/path/to/other_1'); }); }, 'server mojit instance definition override': function() { - - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var command = { type:'test_mojit_1', models: { 'other_2': '/path/to/other_2' } }; - store.expandInstance(command, {}, function(err, instance){ - A.isTrue(instance.models['other_2'] === '/path/to/other_2'); }); }, 'server mojit type name can come from package.json': function() { - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {type:'TestMojit2'}; store.expandInstance(instance, {}, function(err, instance){ - A.isNotUndefined(instance.controller); + A.isNotUndefined(instance['controller-path']); A.areSame('/static/TestMojit2/assets', instance.assetsRoot); A.isNotUndefined(instance.yui.config.modules.test_mojit_2); }); }, - 'server mojit is NOT loaded becuase of pacakge mojito version miss-match': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - A.isTrue(typeof store._staticURLs['/static/test_mojit_4/package.json'] === 'undefined'); - A.isTrue(typeof store._staticURLs['/static/TestMojit4/package.json'] === 'undefined'); - }, - - 'server mojit is loaded becuase of pacakge mojito version match': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - var instance = {type:'TestMojit2'}; - store.expandInstance(instance, {}, function(err, instance){ - A.areSame('/static/TestMojit2/assets', instance.assetsRoot); - }); - }, - - 'server a mojits package.json file is NOT publicly accessible': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - A.isTrue(typeof store._staticURLs['/static/TestMojit2/package.json'] === 'undefined'); - }, - - 'server a mojits package.json file is publicly accessible': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - A.isTrue(typeof store._staticURLs['/static/TestMojit3/package.json'] === 'string'); - }, - - 'server a mojit is NOT loaded because it has a package.json file with no mojito config': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - A.isTrue(typeof store._staticURLs['/static/TestMojit5/package.json'] === 'undefined'); - }, - 'server mojit view index.mu.html is loaded correctly': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {type:'TestMojit3'}; store.expandInstance(instance, {}, function(err, instance){ A.areSame('index.mu.html', instance.views.index['content-path'].split('/').pop()); @@ -326,72 +159,24 @@ YUI.add('mojito-resource-store-adapter-tests', function(Y, NAME) { }, 'server mojit view index.iphone.mu.html is loaded correctly': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); var instance = {type:'TestMojit3'}; store.expandInstance(instance, {device:'iphone'}, function(err, instance){ A.areSame('index.iphone.mu.html', instance.views.index['content-path'].split('/').pop()); }); }, - 'server mojit view index1.mu.html is loaded correctly': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - var instance = {type:'TestMojit3', action: 'index1'}; - store.expandInstance(instance, {device:'forotheriphone'}, function(err, instance){ - A.areSame('index1.forotheriphone.mu.html', instance.views.index1['content-path'].split('/').pop()); - }); - }, - - 'server mojit view index1.iphone.mu.html is loaded correctly': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - - var instance = {type:'TestMojit3', action: 'index1'}; - store.expandInstance(instance, {device:'otheriphone'}, function(err, instance){ - A.areSame('index1.otheriphone.mu.html', instance.views.index1['content-path'].split('/').pop()); - }); - }, - 'test getSpec() from specs dir': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); store.getSpec('server', 'TestMojit2', {}, function(err, instance){ - A.isTrue(instance.type === 'TestMojit2'); A.isTrue(instance.config.testKey1 === 'testVal1'); }); }, 'test getType()': function(){ - var resourceStore = new ResourceStore(fixtures), - store; - - resourceStore.preload(); - - store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); - + var store = Y.mojito.ResourceStoreAdapter.init('server', resourceStore, dummyLog); store.getType('server', 'test_mojit_1', {}, function(err, instance){ - A.isTrue(instance.type === 'test_mojit_1'); A.isTrue(instance.config.testKey4 === 'testVal4'); A.isTrue(instance.config.testKey6.testKey7 === 'testVal7'); @@ -402,4 +187,7 @@ YUI.add('mojito-resource-store-adapter-tests', function(Y, NAME) { YUITest.TestRunner.add(suite); -}, '0.0.1', {requires: ['mojito-resource-store-adapter']}); +}, '0.0.1', {requires: [ + 'mojito-resource-store', + 'mojito-resource-store-adapter' +]}); diff --git a/source/lib/tests/autoload/libs/ycb-tests.js b/source/lib/tests/autoload/libs/ycb-tests.js index ab848ac59..3cc89c576 100644 --- a/source/lib/tests/autoload/libs/ycb-tests.js +++ b/source/lib/tests/autoload/libs/ycb-tests.js @@ -5,110 +5,132 @@ */ YUI.add('mojito-ycb-tests', function(Y, NAME) { - var libycb = require(__dirname + '/../../../libs/ycb.js'), + var libpath = require('path'), + libfs = require('fs'), + libycb = require(libpath.join(__dirname, '../../../libs/ycb.js')), suite = new YUITest.TestSuite(NAME), A = YUITest.Assert, - OA = YUITest.ObjectAssert, AA = YUITest.ArrayAssert; + + function readFixtureFile(file){ + var path = libpath.join(__dirname, '../../', 'fixtures/ycb' , file); + var data = libfs.readFileSync(path, 'utf8'); + return Y.JSON.parse(data); + } + + + function cmp(x, y, msg) { + if (Y.Lang.isArray(x)) { + A.isArray(x, msg || 'first arg should be an array'); + A.isArray(y, msg || 'second arg should be an array'); + A.areSame(x.length, y.length, msg || 'arrays are different lengths'); + for (var i = 0; i < x.length; i += 1) { + cmp(x[i], y[i], msg); + } + return; + } + if (Y.Lang.isObject(x)) { + A.isObject(x, msg || 'first arg should be an object'); + A.isObject(y, msg || 'second arg should be an object'); + A.areSame(Object.keys(x).length, Object.keys(y).length, msg || 'object keys are different lengths'); + for (var i in x) { + if (x.hasOwnProperty(i)) { + cmp(x[i], y[i], msg); + } + } + return; + } + A.areSame(x, y, msg || 'args should be the same'); + } + + suite.add(new YUITest.TestCase({ name: 'ycb', - setUp: function() { - - }, + setUp: function() {}, - tearDown: function() { + tearDown: function() {}, - }, 'test if we can use the module': function() { - A.isTrue(libycb.version === '2.0.0'); }, - 'test _flattenDimension': function() { + 'test _flattenDimension': function() { var dims = readFixtureFile('dimensions.json'), - flat; - - flat = libycb._flattenDimension('', dims[0].dimensions[6]['lang']); + ycb = new libycb.Ycb(dims), + flat = ycb._flattenDimension('', dims[0].dimensions[6]['lang']); - //Y.log(Y.JSON.stringify(flat,null,4)); - - A.isTrue(flat['en'] === 'en'); - A.isTrue(flat['en/en_CA'] === 'en_CA'); - A.isTrue(flat['fr'] === 'fr'); - A.isTrue(flat['fr/fr_FR/fr_CA'] === 'fr_CA'); + A.areSame('en', flat['en']); + A.areSame('en_CA', flat['en/en_CA']); + A.areSame('fr', flat['fr']); + A.areSame('fr_CA', flat['fr/fr_FR/fr_CA']); }, - 'test _flattenDimensions': function() { + 'test _flattenDimensions': function() { var dims = readFixtureFile('dimensions.json'), - flat; - - flat = libycb._flattenDimensions(dims[0].dimensions); + ycb = new libycb.Ycb(dims), + flat = ycb._dimensionPaths; - //Y.log(Y.JSON.stringify(flat,null,4)); - - A.isTrue(flat['lang']['en'] === 'en'); - A.isTrue(flat['lang']['en/en_CA'] === 'en_CA'); - A.isTrue(flat['lang']['fr'] === 'fr'); - A.isTrue(flat['lang']['fr/fr_FR/fr_CA'] === 'fr_CA'); + A.areSame('en', flat['lang']['en']); + A.areSame('en_CA', flat['lang']['en/en_CA']); + A.areSame('fr', flat['lang']['fr']); + A.areSame('fr_CA', flat['lang']['fr/fr_FR/fr_CA']); }, - 'test _makeOrderedLookupList': function() { + 'test _makeOrderedLookupList': function() { var dims = readFixtureFile('dimensions.json'), + ycb = new libycb.Ycb(dims), context, list; - context = { - 'region': 'ir', - 'environment': 'preproduction', - 'lang': 'fr_CA' - }; - - list = libycb._makeOrderedLookupList(dims[0].dimensions, context); - - //Y.log(Y.JSON.stringify(list,null,4)); - - A.isTrue(list['environment'][0] === 'preproduction'); - A.isTrue(list['lang'][0] === 'fr_CA'); - A.isTrue(list['region'][0] === 'ir'); + 'region': 'ir', + 'environment': 'preproduction', + 'lang': 'fr_CA' + }; + list = ycb._makeOrderedLookupList(context, {useAllDimensions: true}); + + A.areSame('preproduction', list['environment'][0]); + A.areSame('*', list['environment'][1]); + A.areSame('fr_CA', list['lang'][0]); + A.areSame('fr_FR', list['lang'][1]); + A.areSame('fr', list['lang'][2]); + A.areSame('*', list['lang'][3]); + A.areSame('ir', list['region'][0]); + A.areSame('gb', list['region'][1]); + A.areSame('*', list['region'][2]); }, - 'test _getLookupPath': function() { + 'test _getLookupPath': function() { var dims = readFixtureFile('dimensions.json'), + ycb = new libycb.Ycb(dims), context, path; - context = { - 'region': 'ir', - 'environment': 'preproduction', - 'lang': 'fr_FR' - }; - - path = libycb._getLookupPath(dims[0].dimensions, context); + 'region': 'ir', + 'environment': 'preproduction', + 'lang': 'fr_FR' + }; + path = ycb._getLookupPath(context, {useAllDimensions: true}); - //Y.log(Y.JSON.stringify(paths,null,4)); - - A.isTrue(path === 'preproduction/*/*/*/*/*/fr_FR/ir/*/*/*'); + A.areSame('preproduction/*/*/*/*/*/fr_FR/ir/*/*/*', path); }, - 'test _getLookupPaths': function() { + 'test _getLookupPaths': function() { var dims = readFixtureFile('dimensions.json'), + ycb = new libycb.Ycb(dims), context, paths, expected; - context = { - 'region': 'ir', - 'environment': 'preproduction', - 'lang': 'fr_FR' - }; - - paths = libycb._getLookupPaths(dims[0].dimensions, context); - //Y.log(Y.JSON.stringify(paths,null,4)); + 'region': 'ir', + 'environment': 'preproduction', + 'lang': 'fr_FR' + }; + paths = ycb._getLookupPaths(context, {useAllDimensions: true}); expected = [ "*/*/*/*/*/*/*/*/*/*/*", @@ -133,205 +155,162 @@ YUI.add('mojito-ycb-tests', function(Y, NAME) { AA.itemsAreEqual(expected, paths); }, - 'test _processRawBundle': function() { + 'test _processRawBundle': function() { var bundle, ycb; - bundle = readFixtureFile('dimensions.json') .concat(readFixtureFile('simple-1.json')[0]); + ycb = new libycb.Ycb(bundle), - //Y.log(Y.JSON.stringify(bundle,null,4)); - - ycb = libycb._processRawBundle(bundle); - - //Y.log(Y.JSON.stringify(ycb,null,4)); - - A.isTrue(ycb.settings['*/*/*/*/*/*/*/*/*/*/*'].title_key === 'YRB_YAHOO'); - A.isTrue(typeof ycb.dimensions[7].region.us !== 'undefined'); + A.areSame('YRB_YAHOO', ycb.settings['*/*/*/*/*/*/*/*/*/*/*'].title_key); + A.isNotUndefined(ycb.dimensions[7].region.us); }, - 'test _processRawBundle with dupe error': function() { + 'test _processRawBundle with dupe error': function() { var bundle, ycb; - bundle = readFixtureFile('dimensions.json') .concat(readFixtureFile('simple-1.json')) .concat(readFixtureFile('simple-2.json')); - //Y.log(Y.JSON.stringify(bundle,null,4)); - // This should throw an error for us to trap - try{ - ycb = libycb._processRawBundle(bundle); - }catch(err){ + try { + ycb = new libycb.Ycb(bundle); + } catch(err) { A.isTrue(true); return; } - - //Y.log(Y.JSON.stringify(ycb,null,4)); - A.isTrue(false); }, - 'test _processRawBundle with many settings': function() { + 'test _processRawBundle with many settings': function() { var bundle, ycb; - bundle = readFixtureFile('dimensions.json') .concat(readFixtureFile('simple-1.json')) .concat(readFixtureFile('simple-3.json')); + ycb = new libycb.Ycb(bundle); - //Y.log(Y.JSON.stringify(bundle,null,4)); - - ycb = libycb._processRawBundle(bundle); - - //Y.log(Y.JSON.stringify(ycb,null,4)); - - A.isTrue(ycb.settings['*/*/*/*/*/*/*/*/*/*/*'].title_key === 'YRB_YAHOO'); - A.isTrue(ycb.settings['*/*/*/*/*/*/*/fr/*/*/*'].links.home === 'http://fr.yahoo.com'); - A.isTrue(ycb.settings['*/*/*/*/*/*/*/fr/*/*/bt'].logo === 'yahoo_bt_FR.png'); - A.isTrue(typeof ycb.dimensions[7].region.us !== 'undefined'); + A.areSame('YRB_YAHOO', ycb.settings['*/*/*/*/*/*/*/*/*/*/*'].title_key); + A.areSame('http://fr.yahoo.com', ycb.settings['*/*/*/*/*/*/*/fr/*/*/*'].links.home); + A.areSame('yahoo_bt_FR.png', ycb.settings['*/*/*/*/*/*/*/fr/*/*/bt'].logo); + A.isNotUndefined(ycb.dimensions[7].region.us); }, - 'test _applySubstitutions': function() { + 'test _applySubstitutions': function() { var config, ycb; - config = readFixtureFile('substitutions.json'); - - //Y.log(Y.JSON.stringify(config,null,4)); - - libycb._applySubstitutions(config); - - //Y.log(Y.JSON.stringify(config,null,4)); + ycb = new libycb.Ycb([]); + ycb._applySubstitutions(config); A.isTrue(config.key0.key4 === 'The value of key0.key2 is value2'); A.isTrue(config.key5.key4 === 'The value of key0.key2 is value2'); A.isTrue(config.key6.key7.key8.key4 === 'The value of key0.key2 is value2'); A.isTrue(config.key6.key9[2] === 'The value of key0.key2 is value2'); - A.isTrue(config['$$key0.key1$$'] === 'error'); + A.isTrue(config['$$key0.key1$$'] === '--YCB-SUBSTITUTION-ERROR--'); A.isTrue(config.key10.key11.key4 === 'The value of key0.key2 is value2'); A.isTrue(config.key11[4] === 'The value of key0.key2 is value2'); A.isTrue(config.key8.key4 === 'The value of key0.key2 is value2'); }, - 'test if we can use a simple config': function() { - - var bundle, ycb; + 'test if we can use a simple config': function() { + var bundle, config; bundle = readFixtureFile('simple-1.json'); + config = libycb.read(bundle); - ycb = libycb.read(bundle); - - //Y.log(Y.JSON.stringify(ycb,null,4)); - - A.isTrue(ycb.title_key === 'YRB_YAHOO'); - A.isTrue(ycb.links.home === 'http://www.yahoo.com'); - A.isTrue(ycb.links.mail === 'http://mail.yahoo.com'); + A.areSame('YRB_YAHOO', config.title_key); + A.areSame('http://www.yahoo.com', config.links.home); + A.areSame('http://mail.yahoo.com', config.links.mail); }, - 'test if we can use a simple config with dimensions': function() { - - var bundle, ycb; + 'test if we can use a simple config with dimensions': function() { + var bundle, config; bundle = readFixtureFile('dimensions.json') .concat(readFixtureFile('simple-1.json')); + config = libycb.read(bundle); - ycb = libycb.read(bundle); - - A.isTrue(ycb.title_key === 'YRB_YAHOO'); - A.isTrue(ycb.links.home === 'http://www.yahoo.com'); - A.isTrue(ycb.links.mail === 'http://mail.yahoo.com'); + A.areSame('YRB_YAHOO', config.title_key); + A.areSame('http://www.yahoo.com', config.links.home); + A.areSame('http://mail.yahoo.com', config.links.mail); }, - 'test if we can use a simple config with dimensions and extra settings': function() { - - var bundle, ycb; + 'test if we can use a simple config with dimensions and extra settings': function() { + var bundle, config; bundle = readFixtureFile('dimensions.json') .concat(readFixtureFile('simple-1.json')) .concat(readFixtureFile('simple-3.json')); + config = libycb.read(bundle); - ycb = libycb.read(bundle); - - A.isTrue(ycb.title_key === 'YRB_YAHOO'); - A.isTrue(ycb.links.home === 'http://www.yahoo.com'); - A.isTrue(ycb.links.mail === 'http://mail.yahoo.com'); + A.areSame('YRB_YAHOO', config.title_key); + A.areSame('http://www.yahoo.com', config.links.home); + A.areSame('http://mail.yahoo.com', config.links.mail); }, - 'test if we can use a simple config with dimensions and conext IR': function() { - - var bundle, context, ycb; + 'test if we can use a simple config with dimensions and context IR': function() { + var bundle, context, config; bundle = readFixtureFile('dimensions.json') .concat(readFixtureFile('simple-1.json')) .concat(readFixtureFile('simple-3.json')); - context = { - 'region': 'ir', - 'environment': 'preproduction', - 'lang': 'fr_FR' - }; - - ycb = libycb.read(bundle, context); - //Y.log(Y.JSON.stringify(ycb,null,4)); - - A.isTrue(ycb.title_key === 'YRB_YAHOO'); - A.isTrue(ycb.logo === 'yahoo_FR.png'); - A.isTrue(ycb.links.home === 'http://gb.yahoo.com'); - A.isTrue(ycb.links.mail === 'http://gb.mail.yahoo.com'); + 'region': 'ir', + 'environment': 'preproduction', + 'lang': 'fr_FR' + }; + config = libycb.read(bundle, context); + + A.areSame('YRB_YAHOO', config.title_key); + A.areSame('yahoo_FR.png', config.logo); + A.areSame('http://gb.yahoo.com', config.links.home); + A.areSame('http://gb.mail.yahoo.com', config.links.mail); }, - 'test if we can use a simple config with dimensions and conext FR': function() { - - var bundle, context, ycb; + 'test if we can use a simple config with dimensions and context FR': function() { + var bundle, context, config; bundle = readFixtureFile('dimensions.json') .concat(readFixtureFile('simple-1.json')) .concat(readFixtureFile('simple-3.json')); - context = { - 'region': 'fr', - 'environment': 'preproduction', - 'lang': 'fr_FR' - }; - - ycb = libycb.read(bundle, context); - - //Y.log(Y.JSON.stringify(ycb,null,4)); - - A.isTrue(ycb.title_key === 'YRB_YAHOO'); - A.isTrue(ycb.logo === 'yahoo_FR.png'); - A.isTrue(ycb.links.home === 'http://fr.yahoo.com'); - A.isTrue(ycb.links.mail === 'http://fr.mail.yahoo.com'); + 'region': 'fr', + 'environment': 'preproduction', + 'lang': 'fr_FR' + }; + config = libycb.read(bundle, context); + + A.areSame('YRB_YAHOO', config.title_key); + A.areSame('yahoo_FR.png', config.logo); + A.areSame('http://fr.yahoo.com', config.links.home); + A.areSame('http://fr.mail.yahoo.com', config.links.mail); }, - 'test if we can use a simple config with dimensions and conext GB & BT': function() { - - var bundle, context, ycb; + 'test if we can use a simple config with dimensions and context GB & BT': function() { + var bundle, context, config; bundle = readFixtureFile('dimensions.json') .concat(readFixtureFile('simple-1.json')) .concat(readFixtureFile('simple-3.json')); - context = { - 'region': 'gb', - 'environment': 'preproduction', - 'flavor': 'bt' - }; - - ycb = libycb.read(bundle, context); - //Y.log(Y.JSON.stringify(ycb,null,4)); - - A.isTrue(ycb.title_key === 'YRB_YAHOO'); - A.isTrue(ycb.logo === 'yahoo_bt_GB.png'); - A.isTrue(ycb.links.home === 'http://gb.yahoo.com'); - A.isTrue(ycb.links.mail === 'http://gb.mail.yahoo.com'); + 'region': 'gb', + 'environment': 'preproduction', + 'flavor': 'bt' + }; + config = libycb.read(bundle, context); + + A.areSame('YRB_YAHOO', config.title_key); + A.areSame('yahoo_bt_GB.png', config.logo); + A.areSame('http://gb.yahoo.com', config.links.home); + A.areSame('http://gb.mail.yahoo.com', config.links.mail); }, + 'test ycb accepts falsey config values': function() { var bundle, - ycb, + config, foo = { settings: [ 'master' ], title_key: 'YRB_YAHOO', @@ -344,31 +323,93 @@ YUI.add('mojito-ycb-tests', function(Y, NAME) { }; bundle = readFixtureFile('dimensions.json').concat([foo]); + config = libycb.read(bundle); + + A.areEqual(config['data-url'], foo['data-url']); + A.isTrue('false_ok' in config); + A.areEqual(config.false_ok, foo.false_ok); + A.isTrue('undef' in config); + A.areEqual(config.undef, foo.undef); + A.isTrue('zero' in config); + A.areEqual(config.zero, foo.zero); + }, - ycb = libycb.read(bundle); - A.areEqual(ycb['data-url'], foo['data-url']); + 'skip unused dimensions': function() { + var bundle, ycb; + bundle = readFixtureFile('dimensions.json') + .concat(readFixtureFile('simple-1.json')) + .concat(readFixtureFile('simple-3.json')); + ycb = new libycb.Ycb(bundle); + + A.areSame(3, Object.keys(ycb.dimsUsed).length); + A.isNotUndefined(ycb.dimsUsed.region); + A.areSame(3, Object.keys(ycb.dimsUsed.region).length); + A.isTrue(ycb.dimsUsed.region.ca); + A.isTrue(ycb.dimsUsed.region.gb); + A.isTrue(ycb.dimsUsed.region.fr); + A.areSame(1, Object.keys(ycb.dimsUsed.lang).length); + A.isTrue(ycb.dimsUsed.lang.fr); + A.areSame(2, Object.keys(ycb.dimsUsed.flavor).length); + A.isTrue(ycb.dimsUsed.flavor.att); + A.isTrue(ycb.dimsUsed.flavor.bt); + + var context = { + 'region': 'ir', + 'environment': 'preproduction', + 'lang': 'fr_FR' + }; + var paths = ycb._getLookupPaths(context, {}); + var expected = [ + '*/*/*/*/*/*/*/*/*/*/*', + '*/*/*/*/*/*/*/gb/*/*/*', + '*/*/*/*/*/*/*/ir/*/*/*', + '*/*/*/*/*/*/fr/*/*/*/*', + '*/*/*/*/*/*/fr/gb/*/*/*', + '*/*/*/*/*/*/fr/ir/*/*/*', + '*/*/*/*/*/*/fr_FR/*/*/*/*', + '*/*/*/*/*/*/fr_FR/gb/*/*/*', + '*/*/*/*/*/*/fr_FR/ir/*/*/*' + ]; + AA.itemsAreEqual(expected, paths); + }, + - A.isTrue('false_ok' in ycb); - A.areEqual(ycb.false_ok, foo.false_ok); + 'get dimensions': function() { + var bundle, ycb; + bundle = readFixtureFile('dimensions.json'); + ycb = new libycb.Ycb(Y.clone(bundle, true)); + cmp(bundle[0].dimensions, ycb.getDimensions()); + }, - A.isTrue('undef' in ycb); - A.areEqual(ycb.undef, foo.undef); - A.isTrue('zero' in ycb); - A.areEqual(ycb.zero, foo.zero); + 'walk settings': function() { + var bundle, ycb; + bundle = readFixtureFile('dimensions.json') + .concat(readFixtureFile('simple-1.json')) + .concat(readFixtureFile('simple-3.json')); + ycb = new libycb.Ycb(bundle); + var ctxs = {}; + ycb.walkSettings(function(settings, config) { + ctxs[JSON.stringify(settings)] = true; + return true; + }); + A.areSame(9, Object.keys(ctxs).length); + A.isTrue(ctxs['{}']); + A.isTrue(ctxs['{"region":"ca"}']); + A.isTrue(ctxs['{"region":"gb"}']); + A.isTrue(ctxs['{"lang":"fr"}']); + A.isTrue(ctxs['{"region":"fr"}']); + A.isTrue(ctxs['{"flavor":"att"}']); + A.isTrue(ctxs['{"region":"ca","flavor":"att"}']); + A.isTrue(ctxs['{"region":"gb","flavor":"bt"}']); + A.isTrue(ctxs['{"region":"fr","flavor":"bt"}']); } - })); - - function readFixtureFile(file){ - var path = require('path').join(__dirname, '../../', 'fixtures/ycb' , file), - data = require('fs').readFileSync(path, 'utf8'); + })); - return Y.JSON.parse(data); - } YUITest.TestRunner.add(suite); -}, '0.0.1', {requires: []}); +}, '0.0.1', {requires: ['json', 'oop']}); diff --git a/source/lib/tests/autoload/management/utils.server-tests.js b/source/lib/tests/autoload/management/utils.server-tests.js new file mode 100644 index 000000000..069f7d05a --- /dev/null +++ b/source/lib/tests/autoload/management/utils.server-tests.js @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2011-2012, Yahoo! Inc. All rights reserved. + * Copyrights licensed under the New BSD License. + * See the accompanying LICENSE file for terms. + */ +YUI.add('mojito-management-utils-tests', function(Y, NAME) { + + var suite = new YUITest.TestSuite(NAME), + libpath = require('path'), + libutils = require(libpath.join(__dirname, '../../../management/utils.js')), + A = YUITest.Assert, + AA = YUITest.ArrayAssert; + + + function cmp(x, y, msg) { + if (Y.Lang.isArray(x)) { + A.isArray(x, msg || 'first arg should be an array'); + A.isArray(y, msg || 'second arg should be an array'); + A.areSame(x.length, y.length, msg || 'arrays are different lengths'); + for (var i = 0; i < x.length; i += 1) { + cmp(x[i], y[i], msg); + } + return; + } + if (Y.Lang.isObject(x)) { + A.isObject(x, msg || 'first arg should be an object'); + A.isObject(y, msg || 'second arg should be an object'); + A.areSame(Object.keys(x).length, Object.keys(y).length, msg || 'object keys are different lengths'); + for (var i in x) { + if (x.hasOwnProperty(i)) { + cmp(x[i], y[i], msg); + } + } + return; + } + A.areSame(x, y, msg || 'args should be the same'); + } + + + suite.add(new YUITest.TestCase({ + + name: 'Management Utils tests', + + + 'decode HTML entities': function() { + A.isFunction(libutils.decodeHTMLEntities); + A.areSame('', libutils.decodeHTMLEntities('')); + A.areSame('orange & red', libutils.decodeHTMLEntities('orange & red')); + A.areSame('& red', libutils.decodeHTMLEntities('& red')); + A.areSame('orange &', libutils.decodeHTMLEntities('orange &')); + A.areSame('', libutils.decodeHTMLEntities('<orange & red>')); + A.areSame('orange & red', libutils.decodeHTMLEntities('orange &amp; red')); + A.areSame('orange © red', libutils.decodeHTMLEntities('orange © red')); + A.areSame('orange y red', libutils.decodeHTMLEntities('orange y red')); + } + + + })); + + + YUITest.TestRunner.add(suite); + +}, '0.0.1', {requires: []}); diff --git a/source/lib/tests/autoload/store.server-tests.js b/source/lib/tests/autoload/store.server-tests.js index afc49aa11..c60e2884c 100644 --- a/source/lib/tests/autoload/store.server-tests.js +++ b/source/lib/tests/autoload/store.server-tests.js @@ -7,53 +7,118 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { var suite = new YUITest.TestSuite(NAME), libpath = require('path'), - fixtures = libpath.join(__dirname, '../fixtures/store'), mojitoRoot = libpath.join(__dirname, '../..'), - ResourceStore = require(libpath.join(__dirname, '../../store.server')), + store, Mock = YUITest.Mock, A = YUITest.Assert, AA = YUITest.ArrayAssert; - suite.add(new YUITest.TestCase({ - name: 'Store tests', + function cmp(x, y, msg) { + if (Y.Lang.isArray(x)) { + A.isArray(x, msg || 'first arg should be an array'); + A.isArray(y, msg || 'second arg should be an array'); + A.areSame(x.length, y.length, msg || 'arrays are different lengths'); + for (var i = 0; i < x.length; i += 1) { + cmp(x[i], y[i], msg); + } + return; + } + if (Y.Lang.isObject(x)) { + A.isObject(x, msg || 'first arg should be an object'); + A.isObject(y, msg || 'second arg should be an object'); + A.areSame(Object.keys(x).length, Object.keys(y).length, msg || 'object keys are different lengths'); + for (var i in x) { + if (x.hasOwnProperty(i)) { + cmp(x[i], y[i], msg); + } + } + return; + } + A.areSame(x, y, msg || 'args should be the same'); + } - 'pre load': function() { - var store = new ResourceStore(fixtures); + suite.add(new YUITest.TestCase({ + + name: 'Store tests -- preload fixture "store"', + + init: function() { + var fixtures = libpath.join(__dirname, '../fixtures/store'); + store = new Y.mojito.ResourceStore({ root: fixtures }); store.preload(); + }, + 'pre load': function() { + var fixtures = libpath.join(__dirname, '../fixtures/store'); //Y.log(Y.JSON.stringify(store,null,4)); - A.isTrue(store._root === fixtures); + A.isTrue(store._config.root === fixtures); }, - 'pre load no application.json file': function() { + 'valid context': function() { + var success; - var fixtures = libpath.join(__dirname, '../fixtures/store_no_app_config'), - store = new ResourceStore(fixtures); - store.preload(); + try { + store.validateContext({}); + } catch(e) { + A.fail('{} should be valid'); + } - //Y.log(Y.JSON.stringify(store,null,4)); - A.isTrue(store._root === fixtures); - }, + try { + store.validateContext({device:'iphone'}); + } catch(e) { + A.fail('{device:iphone} should be valid'); + } - 'server app config value': function() { + try { + store.validateContext({device:'iphone',lang:'en'}); + } catch(e) { + A.fail('{device:iphone,lang:en} should be valid'); + } - var store = new ResourceStore(fixtures); - store.preload(); + try { + store.validateContext({device:'iphone',runtime:'common'}); + } catch(e) { + A.fail('{device:iphone,runtime:common} should be valid'); + } + + try { + success = undefined; + store.validateContext({device:'blender'}); + success = true; + } catch(e) { + success = false; + } + A.isFalse(success, '{device:blender} should be invalid'); + + try { + success = undefined; + store.validateContext({device:'iphone',texture:'corrugated'}); + success = true; + } catch(e) { + success = false; + } + A.isFalse(success, '{device:iphone,texture:corrugated} should be invalid'); - var config = store.getAppConfig(null, 'application'); + try { + success = undefined; + store.validateContext({device:'iphone',runtime:'kite'}); + success = true; + } catch(e) { + success = false; + } + A.isFalse(success, '{device:iphone,runtime:kite} should be invalid'); + + }, + + 'server app config value': function() { + var config = store.getAppConfig(null); A.isTrue(config.testKey1 === 'testVal1'); }, 'server mojit config value': function() { - - var store = new ResourceStore(fixtures); - store.preload(); - var instance = {base:'test1'}; store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.id === 'test1', 'wrong ID'); A.isTrue(instance.type === 'test_mojit_1', 'wrong type'); A.isTrue(instance.config.testKey4 === 'testVal4', 'missing key from definition.json'); @@ -61,13 +126,8 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'server mojit config value via type': function() { - - var store = new ResourceStore(fixtures); - store.preload(); - var instance = {type:'test_mojit_1'}; store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.type === 'test_mojit_1', 'wrong ID'); A.isTrue(instance.config.testKey4 === 'testVal4', 'missing config from definition.json'); A.isTrue(instance.config.testKey6.testKey7 === 'testVal7', 'missing deep config from definition.json'); @@ -75,16 +135,11 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'server mojit config value via type & overrride': function() { - - var store = new ResourceStore(fixtures); - store.preload(); - var instance = { type:'test_mojit_1', config:{testKey4: 'other'} }; store.expandInstance(instance, {}, function(err, instance){ - A.isTrue(instance.type === 'test_mojit_1', 'wrong ID'); A.isTrue(instance.config.testKey4 === 'other', 'missing config from definition.json'); A.isTrue(instance.config.testKey5 === 'testVal5', 'missing deep config from definition.json'); @@ -92,14 +147,10 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'server mojit instance assets': function() { - - var store = new ResourceStore(fixtures); - store.preload(); - + var fixtures = libpath.join(__dirname, '../fixtures/store'); var instance = {type:'test_mojit_1'}; store.expandInstance(instance, {}, function(err, instance) { A.areSame('/static/test_mojit_1/assets', instance.assetsRoot); - // we'll skip the favicon.ico that ships with Mojito // (it's not availble when running --coverage anyway) A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/assets/css/main.css'), instance.assets['css/main.css']); @@ -108,330 +159,71 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'server mojit instance views & binders': function() { - - var store = new ResourceStore(fixtures); - store.preload(); - var instance = {type:'test_mojit_1'}; - store.expandInstance(instance, {}, function(err, instance) { + store.expandInstanceForEnv('client', instance, {}, function(err, instance) { A.areSame(3, Y.Object.keys(instance.views).length); A.isObject(instance.views['test_1']); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/views/test_1.mu.html'), instance.views['test_1']['content-path']); + A.areSame('/static/test_mojit_1/views/test_1.mu.html', instance.views['test_1']['content-path']); A.areSame('mu', instance.views['test_1']['engine']); - A.areSame('/static/test_mojit_1/binders/test_1.js', instance.views['test_1']['binder-url']); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/binders/test_1.js'), instance.views['test_1']['binder-path']); + A.areSame('/static/test_mojit_1/binders/test_1.js', instance.views['test_1']['binder-path']); A.areSame('test_mojit_1Bindertest_1', instance.views['test_1']['binder-module']); A.isNotUndefined(instance.views['test_1']['binder-yui-sorted']['mojito-client']); A.isObject(instance.views['test_2']); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/views/test_2.mu.html'), instance.views['test_2']['content-path']); + A.areSame('/static/test_mojit_1/views/test_2.mu.html', instance.views['test_2']['content-path']); A.areSame('mu', instance.views['test_2']['engine']); A.isObject(instance.views['subdir/test_1']); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/views/subdir/test_1.mu.html'), instance.views['subdir/test_1']['content-path']); + A.areSame('/static/test_mojit_1/views/subdir/test_1.mu.html', instance.views['subdir/test_1']['content-path']); A.areSame('mu', instance.views['subdir/test_1']['engine']); - A.areSame('/static/test_mojit_1/binders/subdir/test_1.js', instance.views['subdir/test_1']['binder-url']); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/binders/subdir/test_1.js'), instance.views['subdir/test_1']['binder-path']); + A.areSame('/static/test_mojit_1/binders/subdir/test_1.js', instance.views['subdir/test_1']['binder-path']); A.areSame('test_mojit_1Bindersubdir/test_1', instance.views['subdir/test_1']['binder-module']); A.isNotUndefined(instance.views['subdir/test_1']['binder-yui-sorted']['mojito-client']); }); }, 'server mojit instance models': function() { - - var store = new ResourceStore(fixtures); - store.preload(); - var instance = {type:'test_mojit_1'}; store.expandInstance(instance, {}, function(err, instance) { A.areSame(4, Y.Object.keys(instance.models).length); - A.areSame(libpath.join(fixtures, 'models/flickr.common.js'), instance.models['flickr']); - A.areSame(libpath.join(fixtures, 'mojits/test_applevel/models/test_applevel.server.js'), instance.models['test_applevel']); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/models/test_1.server.js'), instance.models['test_1']); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/models/test_2.server.js'), instance.models['test_2']); - - A.areSame(4, Y.Object.keys(instance.modelYUIModuleNames).length); - A.isTrue(instance.modelYUIModuleNames['ModelFlickr']); - A.isTrue(instance.modelYUIModuleNames['test_applevelModel']); - A.isTrue(instance.modelYUIModuleNames['test_mojit_1_model_test_1']); - A.isTrue(instance.modelYUIModuleNames['test_mojit_1_model_test_2']); - }); - }, - - 'server mojit instance actions': function() { - - var store = new ResourceStore(fixtures); - store.preload(); - - var instance = {type:'test_mojit_1'}; - store.expandInstance(instance, {}, function(err, instance) { - var actions = instance.actions.sort(); - A.areSame(2, actions.length); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/actions/test_1.server.js'), actions[0]); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/actions/test_2.server.js'), actions[1]); + A.isTrue(instance.models['flickr']); + A.isTrue(instance.models['test_applevel']); + A.isTrue(instance.models['test_1']); + A.isTrue(instance.models['test_2']); }); }, - 'server mojit instance yui': function() { - - var store = new ResourceStore(fixtures); - store.preload(); - - var instance = {type:'TestMojit2'}; - store.expandInstance(instance, {}, function(err, instance) { - A.isNotUndefined(instance.yui); - - A.isNotUndefined(instance.yui.config); - A.isNotUndefined(instance.yui.config.modules); - A.isNotUndefined(instance.yui.config.modules['test_mojit_2']); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_2/controller.server.js'), instance.yui.config.modules['test_mojit_2'].fullpath); - A.isNotUndefined(instance.yui.config.modules['mojito-mu']); - A.areSame(libpath.join(mojitoRoot, 'app/addons/view-engines/mu.server.js'), instance.yui.config.modules['mojito-mu'].fullpath); - - A.isObject(instance.yui.langs); - A.areSame(2, Y.Object.keys(instance.yui.langs).length, 'wrong number of langs'); - A.areSame('lang/test_mojit_2_en-US', instance.yui.langs['en-US']); - A.areSame('lang/test_mojit_2_de', instance.yui.langs['de']); - - A.isArray(instance.yui.requires); - AA.contains('test_mojit_2', instance.yui.requires); - AA.doesNotContain('test_applevelModel', instance.yui.requires); - AA.doesNotContain('ModelFlickr', instance.yui.requires); - AA.contains('mojito-mu', instance.yui.requires); - AA.contains('mojito', instance.yui.requires); - - A.isArray(instance.yui.sorted); - AA.contains('test_mojit_2', instance.yui.sorted); - AA.doesNotContain('test_applevelModel', instance.yui.sorted); - AA.doesNotContain('ModelFlickr', instance.yui.sorted); - AA.contains('mojito-mu', instance.yui.sorted); - AA.contains('mojito', instance.yui.sorted); - - A.isObject(instance.yui.sortedPaths); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_2/controller.server.js'), instance.yui.sortedPaths['test_mojit_2']); - A.isUndefined(instance.yui.sortedPaths['test_applevelModel']); - A.isUndefined(instance.yui.sortedPaths['ModelFlickr']); - A.areSame(libpath.join(mojitoRoot, 'app/addons/view-engines/mu.server.js'), instance.yui.sortedPaths['mojito-mu']); - A.areSame(libpath.join(mojitoRoot, 'app/autoload/mojito.common.js'), instance.yui.sortedPaths['mojito']); - }); - - }, - - 'server mojit instance yui - precomputed': function() { - - var fixtures = libpath.join(__dirname, '../fixtures/precomputed'); - var store = new ResourceStore(fixtures); - store.preload(); - - var instance = { type:'PagedFlickr' }; - store.expandInstance(instance, {}, function(err, instance) { - A.isNotUndefined(instance.yui); - - A.isArray(instance.yui.requires); - AA.contains('mojito', instance.yui.requires); - AA.contains('mojito-dispatcher', instance.yui.requires); - AA.contains('mojito-mu', instance.yui.requires); - AA.contains('PagedFlickr', instance.yui.requires); - - A.isArray(instance.yui.sorted); - AA.contains('intl', instance.yui.sorted); - AA.contains('datatype-date-format', instance.yui.sorted); - AA.contains('mojito', instance.yui.sorted); - AA.contains('mojito-util', instance.yui.sorted); - AA.contains('mojito-intl-addon', instance.yui.sorted); - AA.contains('lang/PagedFlickr_de', instance.yui.sorted); - AA.contains('lang/PagedFlickr_en', instance.yui.sorted); - AA.contains('lang/PagedFlickr_en-US', instance.yui.sorted); - AA.contains('lang/datatype-date-format_de', instance.yui.sorted); - AA.contains('lang/datatype-date-format_en', instance.yui.sorted); - AA.contains('lang/datatype-date-format_en-US', instance.yui.sorted); - - A.isObject(instance.yui.sortedPaths); - A.isNotUndefined(instance.yui.sortedPaths['intl']); - A.isNotUndefined(instance.yui.sortedPaths['datatype-date-format']); - A.isNotUndefined(instance.yui.sortedPaths['mojito']); - A.isNotUndefined(instance.yui.sortedPaths['mojito-util']); - A.isNotUndefined(instance.yui.sortedPaths['mojito-intl-addon']); - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/controller.common.js'), instance.yui.sortedPaths['PagedFlickr']); - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_de.js'), instance.yui.sortedPaths['lang/PagedFlickr_de']); - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_en.js'), instance.yui.sortedPaths['lang/PagedFlickr_en']); - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_en-US.js'), instance.yui.sortedPaths['lang/PagedFlickr_en-US']); - A.isNotUndefined(instance.yui.sortedPaths['lang/datatype-date-format_de']); - A.isNotUndefined(instance.yui.sortedPaths['lang/datatype-date-format_en']); - A.isNotUndefined(instance.yui.sortedPaths['lang/datatype-date-format_en-US']); - - A.isObject(instance.yui.langs); - A.areSame('lang/PagedFlickr_de', instance.yui.langs['de']); - A.areSame('lang/PagedFlickr_en', instance.yui.langs['en']); - A.areSame('lang/PagedFlickr_en-US', instance.yui.langs['en-US']); - }); - - }, - - 'server mojit instance yui - ondemand': function() { - - var fixtures = libpath.join(__dirname, '../fixtures/ondemand'); - var store = new ResourceStore(fixtures); - store.preload(); - - var instance = { type:'PagedFlickr' }; - store.expandInstance(instance, {}, function(err, instance) { - A.isNotUndefined(instance.yui); - - A.isArray(instance.yui.requires); - AA.contains('mojito', instance.yui.requires); - AA.contains('mojito-dispatcher', instance.yui.requires); - AA.contains('mojito-controller-context', instance.yui.requires); - AA.contains('mojito-action-context', instance.yui.requires); - AA.contains('mojito-output-adapter-addon', instance.yui.requires); - AA.contains('mojito-deploy-addon', instance.yui.requires); - AA.contains('mojito-partial-addon', instance.yui.requires); - AA.contains('mojito-url-addon', instance.yui.requires); - AA.contains('mojito-mu', instance.yui.requires); - AA.contains('mojito-util', instance.yui.requires); - AA.contains('mojito-view-renderer', instance.yui.requires); - AA.contains('ModelFlickr', instance.yui.requires); - AA.contains('PagedFlickr', instance.yui.requires); - AA.contains('lang/PagedFlickr_de', instance.yui.requires); - AA.contains('lang/PagedFlickr_en', instance.yui.requires); - AA.contains('lang/PagedFlickr_en-US', instance.yui.requires); - - A.isUndefined(instance.yui.sorted); - A.isUndefined(instance.yui.sortedPaths); - - A.isObject(instance.yui.langs); - A.areSame('lang/PagedFlickr_de', instance.yui.langs['de']); - A.areSame('lang/PagedFlickr_en', instance.yui.langs['en']); - A.areSame('lang/PagedFlickr_en-US', instance.yui.langs['en-US']); - }); - - }, - - 'server mojit instance yui - precomputed+ondemand': function() { - - var fixtures = libpath.join(__dirname, '../fixtures/precomputed-ondemand'); - var store = new ResourceStore(fixtures); - store.preload(); - - var instance = { type:'PagedFlickr' }; - store.expandInstance(instance, {}, function(err, instance) { - A.isNotUndefined(instance.yui); - - A.isArray(instance.yui.requires); - AA.contains('mojito', instance.yui.requires); - AA.contains('mojito-dispatcher', instance.yui.requires); - AA.contains('mojito-controller-context', instance.yui.requires); - AA.contains('mojito-action-context', instance.yui.requires); - AA.contains('mojito-output-adapter-addon', instance.yui.requires); - AA.contains('mojito-deploy-addon', instance.yui.requires); - AA.contains('mojito-partial-addon', instance.yui.requires); - AA.contains('mojito-url-addon', instance.yui.requires); - AA.contains('mojito-mu', instance.yui.requires); - AA.contains('mojito-util', instance.yui.requires); - AA.contains('mojito-view-renderer', instance.yui.requires); - AA.contains('ModelFlickr', instance.yui.requires); - AA.contains('PagedFlickr', instance.yui.requires); - AA.contains('lang/PagedFlickr_de', instance.yui.requires); - AA.contains('lang/PagedFlickr_en', instance.yui.requires); - AA.contains('lang/PagedFlickr_en-US', instance.yui.requires); - - A.isArray(instance.yui.sorted); - AA.doesNotContain('intl', instance.yui.sorted); - AA.doesNotContain('datatype-date-format', instance.yui.sorted); - AA.contains('mojito', instance.yui.sorted); - AA.doesNotContain('mojito-util', instance.yui.sorted); - AA.doesNotContain('mojito-intl-addon', instance.yui.sorted); - AA.contains('lang/PagedFlickr_de', instance.yui.sorted); - AA.contains('lang/PagedFlickr_en', instance.yui.sorted); - AA.contains('lang/PagedFlickr_en-US', instance.yui.sorted); - AA.doesNotContain('lang/datatype-date-format_de', instance.yui.sorted); - AA.doesNotContain('lang/datatype-date-format_en', instance.yui.sorted); - AA.doesNotContain('lang/datatype-date-format_en-US', instance.yui.sorted); - - A.isObject(instance.yui.sortedPaths); - A.isUndefined(instance.yui.sortedPaths['intl']); - A.isUndefined(instance.yui.sortedPaths['datatype-date-format']); - A.isNotUndefined(instance.yui.sortedPaths['mojito']); - A.isUndefined(instance.yui.sortedPaths['mojito-util']); - A.isUndefined(instance.yui.sortedPaths['mojito-intl-addon']); - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/controller.common.js'), instance.yui.sortedPaths['PagedFlickr']); - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_de.js'), instance.yui.sortedPaths['lang/PagedFlickr_de']); - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_en.js'), instance.yui.sortedPaths['lang/PagedFlickr_en']); - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/lang/PagedFlickr_en-US.js'), instance.yui.sortedPaths['lang/PagedFlickr_en-US']); - A.isUndefined(instance.yui.sortedPaths['lang/datatype-date-format_de']); - A.isUndefined(instance.yui.sortedPaths['lang/datatype-date-format_en']); - A.isUndefined(instance.yui.sortedPaths['lang/datatype-date-format_en-US']); - - A.isObject(instance.yui.langs); - A.areSame('lang/PagedFlickr_de', instance.yui.langs['de']); - A.areSame('lang/PagedFlickr_en', instance.yui.langs['en']); - A.areSame('lang/PagedFlickr_en-US', instance.yui.langs['en-US']); - }); - - }, - - 'dynamic handling of mojit definition.json': function() { - var store = new ResourceStore(fixtures); - store.preload(); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_1/definition.json'), store._dynamicURLs['/static/test_mojit_1/definition.json']); - }, - 'server mojit type name can come from package.json': function() { - var store = new ResourceStore(fixtures); - store.preload(); - var instance = {type:'TestMojit2'}; store.expandInstance(instance, {}, function(err, instance){ - A.isNotUndefined(instance.controller); + A.isNotUndefined(instance['controller-path']); A.areSame('/static/TestMojit2/assets', instance.assetsRoot); A.isNotUndefined(instance.yui.config.modules.test_mojit_2); }); }, - 'server mojit is NOT loaded because of package mojito version miss-match': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - - A.isTrue(typeof store._staticURLs['/static/test_mojit_4/package.json'] === 'undefined'); - A.isTrue(typeof store._staticURLs['/static/TestMojit4/package.json'] === 'undefined'); + 'server mojit is NOT loaded because of package mojito version mismatch': function(){ + var urls = store.getAllURLs(); + A.isUndefined(urls['/static/test_mojit_4/package.json']); + A.isUndefined(urls['/static/TestMojit4/package.json']); }, 'server mojit is loaded because of package mojito version match': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - var instance = {type:'TestMojit2'}; store.expandInstance(instance, {}, function(err, instance){ A.areSame('/static/TestMojit2/assets', instance.assetsRoot); }); }, - 'server a mojits package.json file is NOT publicly accessible': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - - A.isTrue(typeof store._staticURLs['/static/TestMojit2/package.json'] === 'undefined'); + 'server a mojits package.json file is available as appropriate': function() { + var urls = store.getAllURLs(); + A.isUndefined(urls['/static/TestMojit2/package.json']); + A.isNotUndefined(urls['/static/TestMojit3/package.json']); + A.isUndefined(urls['/static/TestMojit5/package.json']); }, - 'server a mojits package.json file is publicly accessible': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - - A.isTrue(typeof store._staticURLs['/static/TestMojit3/package.json'] === 'string'); - }, - - 'server a mojit is NOT loaded because it has a package.json file with no mojito config': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - - A.isTrue(typeof store._staticURLs['/static/TestMojit5/package.json'] === 'undefined'); - }, - - 'server mojit view index.mu.html is loaded correctly': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - + 'server mojit view index.mu.html is loaded correctly': function() { var instance = {type:'TestMojit3'}; store.expandInstance(instance, {}, function(err, instance){ A.areSame('index.mu.html', instance.views.index['content-path'].split('/').pop()); @@ -439,38 +231,13 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'server mojit view index.iphone.mu.html is loaded correctly': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - var instance = {type:'TestMojit3'}; store.expandInstance(instance, {device:'iphone'}, function(err, instance){ A.areSame('index.iphone.mu.html', instance.views.index['content-path'].split('/').pop()); }); }, - 'server mojit view index1.mu.html is loaded correctly': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - - var instance = {type:'TestMojit3'}; - store.expandInstance(instance, {device:'forotheriphone'}, function(err, instance){ - A.areSame('index1.forotheriphone.mu.html', instance.views.index1['content-path'].split('/').pop()); - }); - }, - - 'server mojit view index1.iphone.mu.html is loaded correctly': function(){ - var store = new ResourceStore(fixtures); - store.preload(); - - var instance = {type:'TestMojit3'}; - store.expandInstance(instance, {device:'otheriphone'}, function(err, instance){ - A.areSame('index1.otheriphone.mu.html', instance.views.index1['content-path'].split('/').pop()); - }); - }, - 'app-level mojits': function() { - var store = new ResourceStore(fixtures); - store.preload(); var instance = { type: 'test_mojit_1' }; store.expandInstance(instance, {}, function(err, instance) { A.isNotUndefined(instance.models.test_applevel); @@ -478,104 +245,40 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'mojitDirs setting': function() { - var store = new ResourceStore(fixtures); - store.preload(); var instance = { type: 'soloMojit' }; store.expandInstance(instance, {}, function(err, instance) { - A.isNotUndefined(instance.controller); + A.isNotUndefined(instance['controller-path']); }); }, 'expandInstance caching': function() { - var store = new ResourceStore(fixtures); - store.preload(); var instance = 'foo'; var context = {}; - store._expandInstanceCache.server[Y.JSON.stringify(instance)+Y.JSON.stringify(context)] = 'bar'; + var key = Y.JSON.stringify(instance) + Y.JSON.stringify(context); + store._expandInstanceCache.server[key] = 'bar'; store.expandInstance(instance, context, function(err, instance) { A.areEqual('bar', instance); }); }, - 'rollups mojits': function() { - var store = new ResourceStore(fixtures); - store.preload(); - - var rollups = store.getRollupsMojits('client', {}); - // We'll just check test_mojit_1, so that this test doesn't break as others add mojits to the fixtures - var rollup = rollups['rollups']; - A.isNotUndefined(rollup); - A.areEqual(rollup.dest, libpath.join(fixtures,'mojits/rollups/rollup.client.js'), 'wrong dest'); - A.areEqual(3, rollup.srcs.length, 'wrong number of sources'); - rollup.srcs.sort(); - A.areEqual(rollup.srcs[0], libpath.join(fixtures,'mojits/rollups/binders/index.js')); - A.areEqual(rollup.srcs[1], libpath.join(fixtures,'mojits/rollups/controller.common.js')); - A.areEqual(rollup.srcs[2], libpath.join(fixtures,'mojits/rollups/models/model.client.js')); - }, - - 'rollups app': function() { - var store = new ResourceStore(fixtures); - store.preload(); - - var rollup = store.getRollupsApp('client', {}); - A.isNotUndefined(rollup); - A.areEqual(rollup.dest, libpath.join(fixtures,'rollup.client.js')); - // Hmmm... since the rollups.src list contains a great deal of the - // Mojito framework YUI modules, and since those change often, it's - // a bit fragile to do a hard test for specific values. - // So, instead, we'll just test for a few things. - AA.contains(libpath.join(fixtures, 'models/flickr.common.js'), rollup.srcs); - AA.contains(libpath.join(mojitoRoot, 'app/autoload/mojito.common.js'), rollup.srcs); - AA.contains(libpath.join(mojitoRoot, 'app/autoload/mojito-client.client.js'), rollup.srcs); - }, - - 'inline css mojits': function() { - var store = new ResourceStore(fixtures); - store.preload(); - - var inlines = store.getInlineCssMojits('client', {}); - // We'll just check test_mojit_1, so that this test doesn't break as others add mojits to the fixtures - var found = 0; - Y.Array.each(inlines, function(inline) { - if (inline.mojitName !== 'inlinecss') { - return; - } - ++found; - if (inline.context.device && 'iphone' === inline.context.device) { - A.areEqual(inline.dest, libpath.join(fixtures,'mojits/inlinecss/autoload/compiled/inlinecss.iphone.common.js')); - A.areEqual(inline.yuiModuleName, 'inlinecss/inlinecss'); - A.areEqual(3, Object.keys(inline.srcs).length); - A.areEqual(inline.srcs['/static/inlinecss/assets/foo.css'], libpath.join(fixtures,'mojits/inlinecss/assets/foo.css')); - A.areEqual(inline.srcs['/static/inlinecss/assets/bar.iphone.css'], libpath.join(fixtures,'mojits/inlinecss/assets/bar.iphone.css')); - A.areEqual(inline.srcs['/static/inlinecss/assets/deeper/foo.css'], libpath.join(fixtures,'mojits/inlinecss/assets/deeper/foo.css')); - } - else { - A.areEqual(inline.dest, libpath.join(fixtures,'mojits/inlinecss/autoload/compiled/inlinecss.common.js')); - A.areEqual(inline.yuiModuleName, 'inlinecss/inlinecss'); - A.areEqual(3, Object.keys(inline.srcs).length); - A.areEqual(inline.srcs['/static/inlinecss/assets/foo.css'], libpath.join(fixtures,'mojits/inlinecss/assets/foo.css')); - A.areEqual(inline.srcs['/static/inlinecss/assets/bar.css'], libpath.join(fixtures,'mojits/inlinecss/assets/bar.css')); - A.areEqual(inline.srcs['/static/inlinecss/assets/deeper/foo.css'], libpath.join(fixtures,'mojits/inlinecss/assets/deeper/foo.css')); - } - }); - A.areEqual(2, found); - }, - - 'multi preload, and setLogger()': function() { - var store = new ResourceStore(fixtures); - var logsBefore, logs = 0; - store.setLogger({ log: function() { - logs++; - } }); - store.preload(); - logsBefore = logs; + 'multi preload': function() { + var pre = { + appRVs: Y.clone(store._appRVs, true), + mojitRVs: Y.clone(store._mojitRVs, true), + appResources: Y.clone(store._appResources, true), + mojitResources: Y.clone(store._mojitResources, true) + }; store.preload(); - A.areSame(logsBefore, logs); + var post = { + appRVs: Y.clone(store._appRVs, true), + mojitRVs: Y.clone(store._mojitRVs, true), + appResources: Y.clone(store._appResources, true), + mojitResources: Y.clone(store._mojitResources, true) + }; + cmp(post, pre); }, 'call getSpec()': function() { - var store = new ResourceStore(fixtures); - store.preload(); store.getSpec('server', 'test1', {}, function(err, instance) { A.areSame('test_mojit_1', instance.type); A.areSame('test1', instance.id); @@ -585,8 +288,6 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'call getType()': function() { - var store = new ResourceStore(fixtures); - store.preload(); store.getType('server', 'test_mojit_1', {}, function(err, instance) { A.areSame('test_mojit_1', instance.type); A.isUndefined(instance.id); @@ -596,21 +297,18 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'instance with base pointing to non-existant spec': function() { - var store = new ResourceStore(fixtures), - spec = { base: 'nonexistant' }; - store.preload(); + var spec = { base: 'nonexistant' }; store.expandInstance(spec, {}, function(err, instance) { A.isNotUndefined(err); + A.areSame('Unknown base of "nonexistant"', err.message); A.isUndefined(instance); }); }, 'getAppConfig() returns contextualized info': function() { - var store = new ResourceStore(fixtures), - config, - context = { runtime: 'server' }; - store.preload(context); - config = store.getAppConfig(null, 'application'); + var context = { runtime: 'server' }, + config; + config = store.getAppConfig(context); A.isObject(config); A.areSame('testVal1-server', config.testKey1, 'testKey1 wasnt contextualized to the server'); A.areSame('testVal2', config.testKey2, 'testKey2 gotten from the wrong context'); @@ -618,35 +316,15 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { A.isUndefined(config.testKey4, 'testKey4 gotten from the wrong context'); }, - 'getAppConfig() for something other than "definition"': function() { - var store = new ResourceStore(fixtures); - store.preload(); - var config = store.getAppConfig({}, 'routes'); - A.isObject(config); - A.isObject(config.flickr_by_page); - A.isObject(config.flickr_base); - }, - 'call getRoutes()': function() { - var store = new ResourceStore(fixtures); - store.preload(); var routes = store.getRoutes({}); A.isObject(routes, 'no routes at all'); A.isObject(routes.flickr_by_page, 'missing route flickr_by_page'); A.isObject(routes.flickr_base, 'missing route flickr_base'); }, - 'call fileFromStaticHandlerURL()': function() { - var store = new ResourceStore(fixtures); - store.preload(); - var fullpath = store.fileFromStaticHandlerURL('/static/TestMojit2/controller.server.js'); - A.areSame(libpath.join(fixtures, 'mojits/test_mojit_2/controller.server.js'), fullpath); - }, - 'call serializeClientStore()': function() { - var store = new ResourceStore(fixtures); - store.preload(); - var client = store.serializeClientStore({}, []); + var client = store.serializeClientStore({}); A.isObject(client, 'config is missing'); A.isObject(client.appConfig, 'missing appConfig'); A.areSame('/tunnel', client.appConfig.tunnelPrefix); @@ -663,10 +341,8 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'call listAllMojits()': function() { - var store = new ResourceStore(fixtures); - store.preload(); var list = store.listAllMojits('server'); - A.areSame(9, list.length, 'found the wrong number of mojits'); + A.areSame(10, list.length, 'found the wrong number of mojits'); AA.contains('DaliProxy', list); AA.contains('HTMLFrameMojit', list); AA.contains('LazyLoad', list); @@ -675,137 +351,75 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { AA.contains('test_mojit_1', list); AA.contains('TestMojit2', list); AA.contains('TestMojit3', list); + AA.contains('TestMojit5', list); AA.contains('soloMojit', list); }, - 'call getAllMojits()': function() { - var store = new ResourceStore(fixtures); - store.preload(); - var mojits = store.getAllMojits('server', {}); - A.areSame(9, Object.keys(mojits).length, 'Found the wrong number of mojits'); - A.isObject(mojits.DaliProxy); - // DaliProxy has no static assets - A.isUndefined(mojits.DaliProxy.assetsRoot); - A.isObject(mojits.HTMLFrameMojit); - // HTMLFrameMojit has no static assets - A.isUndefined(mojits.HTMLFrameMojit.assetsRoot); - A.isObject(mojits.LazyLoad); - A.isObject(mojits.inlinecss); - A.areSame('/static/inlinecss/assets', mojits.inlinecss.assetsRoot, "'/static/inlinecss/assets', mojits.inlinecss.assetsRoot"); - A.isObject(mojits.rollups); - A.areSame('/static/rollups/assets', mojits.rollups.assetsRoot, "'/static/rollups/assets', mojits.rollups.assetsRoot"); - A.isObject(mojits.test_mojit_1); - A.areSame('/static/test_mojit_1/assets', mojits.test_mojit_1.assetsRoot, "'/static/test_mojit_1/assets', mojits.test_mojit_1.assetsRoot"); - A.isObject(mojits.TestMojit2); - A.areSame('/static/TestMojit2/assets', mojits.TestMojit2.assetsRoot, "'/static/TestMojit2/assets', mojits.TestMojit2.assetsRoot"); - A.isObject(mojits.TestMojit3); - A.areSame('/static/TestMojit3/assets', mojits.TestMojit3.assetsRoot, "'/static/TestMojit3/assets', mojits.TestMojit3.assetsRoot"); - A.isObject(mojits.soloMojit); - A.areSame('/static/soloMojit/assets', mojits.soloMojit.assetsRoot, "'/static/soloMojit/assets', mojits.soloMojit.assetsRoot"); + 'app with rollups': function() { + var fixtures = libpath.join(__dirname, '../fixtures/store'); + var spec = { type: 'rollups' }; + store.expandInstanceForEnv('client', spec, {}, function(err, instance) { + A.areSame('/static/rollups/rollup.client.js', instance.yui.sortedPaths['rollups']); + A.areSame('/static/rollups/rollup.client.js', instance.yui.sortedPaths['rollupsModelClient']); + var urls = store.getAllURLs(); + A.areSame(libpath.join(fixtures, 'mojits/rollups/rollup.client.js'), urls['/static/rollups/rollup.client.js']); + }); }, - 'stuff with ctx{lang:}, in language fallback': function() { - var fixtures = libpath.join(__dirname, '../fixtures/gsg5'), - store = new ResourceStore(fixtures), - ctx, spec; + 'app resource overrides framework resource': function() { + var fixtures = libpath.join(__dirname, '../fixtures/store'), + ress; + ress = store.getResources('server', {}, {mojit: 'HTMLFrameMojit', type: 'controller'}); + A.areSame(libpath.join(fixtures, 'mojits/HTMLFrameMojit/controller.server.js'), ress[0].source.fs.fullPath); + } + + })); + + + suite.add(new YUITest.TestCase({ + + name: 'Store tests -- preload fixture "gsg5"', + + init: function() { + var fixtures = libpath.join(__dirname, '../fixtures/gsg5'); + store = new Y.mojito.ResourceStore({ root: fixtures }); store.preload(); + }, - // first test - ctx = { lang: 'en-US' }; - spec = { type: 'PagedFlickr' }; + 'controller with selector': function() { + var fixtures = libpath.join(__dirname, '../fixtures/gsg5'); + var spec = { type: 'PagedFlickr' }; + var ctx = { device: 'iphone' }; store.expandInstance(spec, ctx, function(err, instance) { - A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is undefined {lang:en-US}'); - A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en'], 'en is undefined {lang:en-US}'); - - // second test - ctx = { lang: 'en' }; - spec = { type: 'PagedFlickr' }; - store.expandInstance(spec, ctx, function(err, instance) { - A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en'], 'en is undefined {lang-en}'); - A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is undefined {lang:en}'); - - // third test - ctx = { lang: 'de-US' }; - spec = { type: 'PagedFlickr' }; - store.expandInstance(spec, ctx, function(err, instance) { - A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_de'], 'de is undefined {lang:de-US}'); - A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is undefined {lang:de-US}'); - - // fourth test - ctx = { lang: 'xy-ZU' }; - spec = { type: 'PagedFlickr' }; - store.expandInstance(spec, ctx, function(err, instance) { - A.isTrue( - Boolean(instance.yui.sortedPaths['lang/PagedFlickr_de']), - 'de is undefined {lang:xy-ZU}' - ); - A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is undefined {lang:xy-ZU}'); - - // fifth test - ctx = {}; - spec = { type: 'PagedFlickr' }; - store.expandInstance(spec, ctx, function(err, instance) { - A.isTrue( - Boolean(instance.yui.sortedPaths['lang/PagedFlickr_de']), - 'de is undefined {}' - ); - A.isNotUndefined(instance.yui.sortedPaths['lang/PagedFlickr_en-US'], 'en-US is undefined {}'); - }); - }); - }); - }); + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/controller.common.iphone.js'), instance['controller-path']); }); }, - 'bad files': function() { - var fixtures = libpath.join(__dirname, '../fixtures/badfiles'), - store = new ResourceStore(fixtures); - store.preload(); - var spec = { type: 'M' }; - store.expandInstance(spec, {}, function(err, instance) { - A.isUndefined(instance.yui.sortedPaths['addon-ac-not']); - A.isUndefined(instance.yui.sortedPaths['MAutoloadNot']); - A.isUndefined(instance.yui.sortedPaths['MModelNot']); - A.isUndefined(instance.views['not']['binder-url']); + 'binder with selector': function() { + var fixtures = libpath.join(__dirname, '../fixtures/gsg5'); + var spec = { type: 'PagedFlickr' }; + var ctx = { device: 'iphone' }; + store.expandInstance(spec, ctx, function(err, instance) { + A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/views/index.iphone.mu.html'), instance.views.index['content-path']); }); }, - 'malformed JSON for config file': function() { - var fixtures = libpath.join(__dirname, '../fixtures/badfiles2'), - store = new ResourceStore(fixtures), - logCalled = 0; - store.setLogger({ log: function() { - logCalled++; - }}); - try { - store.preload(); - } - catch (err) { - A.areSame('Error parsing JSON file:', err.message.substr(0, 24)); - A.areSame(1, logCalled); - return; - } - }, + })); + + + suite.add(new YUITest.TestCase({ + + name: 'Store tests -- preload fixture "gsg5-appConfig"', - 'JSON config file not YCB': function() { - var fixtures = libpath.join(__dirname, '../fixtures/badfiles3'), - store = new ResourceStore(fixtures), - r, logCalled = 0; - store.setLogger({ log: function(msg, lvl, who) { - logCalled++; - }}); + init: function() { + var fixtures = libpath.join(__dirname, '../fixtures/gsg5-appConfig'); + store = new Y.mojito.ResourceStore({ root: fixtures }); store.preload(); - r = store.getRoutes(); - A.areSame(0, logCalled); - A.isNotUndefined(r._default_path); }, 'appConfig deferAllOptionalAutoloads': function() { - var fixtures = libpath.join(__dirname, '../fixtures/gsg5-appConfig'), - store = new ResourceStore(fixtures); - store.preload(); var spec = { type: 'PagedFlickr' }; - store.expandInstance(spec, {}, function(err, instance) { + store.expandInstanceForEnv('client', spec, {}, function(err, instance) { A.isUndefined(instance.views.index['binder-yui-sorted']['breg'], 'breg'); A.isUndefined(instance.views.index['binder-yui-sorted']['dali-bean'], 'dali-bean'); A.isUndefined(instance.views.index['binder-yui-sorted']['dali-transport-base'], 'dali-transport-base'); @@ -820,69 +434,70 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { }, 'appConfig staticHandling.prefix': function() { - var fixtures = libpath.join(__dirname, '../fixtures/gsg5-appConfig'), - store = new ResourceStore(fixtures); - store.preload(); var spec = { type: 'PagedFlickr' }; store.expandInstance(spec, {}, function(err, instance) { A.areSame('/PagedFlickr/assets', instance.assetsRoot); }); }, - 'controller with selector': function() { - var fixtures = libpath.join(__dirname, '../fixtures/gsg5'), - store = new ResourceStore(fixtures); + })); + + + suite.add(new YUITest.TestCase({ + + name: 'Store tests -- misc', + + 'static context is really static': function() { + var fixtures = libpath.join(__dirname, '../fixtures/store'), + context = { runtime: 'server' }, + store = new Y.mojito.ResourceStore({ root: fixtures, context: context }), + config; store.preload(); - var spec = { type: 'PagedFlickr' }; - var ctx = { device: 'iphone' }; - store.expandInstance(spec, ctx, function(err, instance) { - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/controller.common.iphone.js'), instance.controller); - }); + config = store.getAppConfig(); + A.isObject(config); + A.areSame('testVal1-server', config.testKey1, 'testKey1 wasnt contextualized to the server'); + A.areSame('testVal2', config.testKey2, 'testKey2 gotten from the wrong context'); + A.areSame('portended', config.pathos, 'missing contextualized config'); + A.isUndefined(config.testKey4, 'testKey4 gotten from the wrong context'); }, - 'binder with selector': function() { - var fixtures = libpath.join(__dirname, '../fixtures/gsg5'), - store = new ResourceStore(fixtures); + 'pre load no application.json file': function() { + var fixtures = libpath.join(__dirname, '../fixtures/store_no_app_config'), + store = new Y.mojito.ResourceStore({ root: fixtures }); store.preload(); - var spec = { type: 'PagedFlickr' }; - var ctx = { device: 'iphone' }; - store.expandInstance(spec, ctx, function(err, instance) { - A.areSame(libpath.join(fixtures, 'mojits/PagedFlickr/views/index.iphone.mu.html'), instance.views.index['content-path']); - }); + + //Y.log(Y.JSON.stringify(store,null,4)); + A.isTrue(store._config.root === fixtures); }, - 'app with rollups': function() { - var store = new ResourceStore(fixtures); + 'default routes': function() { + var fixtures = libpath.join(__dirname, '../fixtures/store_no_app_config'), + store = new Y.mojito.ResourceStore({ root: fixtures }); store.preload(); - var spec = { type: 'rollups' }; - store.expandInstanceForEnv('client', spec, {}, function(err, instance) { - A.areSame('/static/rollups/rollup.client.js', instance.yui.sortedPaths['rollups']); - A.areSame('/static/rollups/rollup.client.js', instance.yui.sortedPaths['rollupsBinderIndex']); - A.areSame('/static/rollups/rollup.client.js', instance.yui.sortedPaths['rollupsModelClient']); - }); - }, - 'TODO app with app-level rollup': function() { - A.skip(); + var have = store.getRoutes(); + A.isObject(have._default_path); }, - 'appConfig yui.base': function() { - var fixtures = libpath.join(__dirname, '../fixtures/gsg5-appConfig'), - store = new ResourceStore(fixtures); + 'bad files': function() { + var fixtures = libpath.join(__dirname, '../fixtures/badfiles'), + store = new Y.mojito.ResourceStore({ root: fixtures }); store.preload(); - var spec = { type: 'PagedFlickr' }; + var spec = { type: 'M' }; store.expandInstance(spec, {}, function(err, instance) { - A.areSame('/foo/', instance.yui.sortedPaths['oop'].substr(0, 5)); - A.areSame('/foo/', instance.yui.sortedPaths['intl'].substr(0, 5)); - A.areSame('/foo/', instance.yui.sortedPaths['jsonp'].substr(0, 5)); - A.areSame('/foo/', instance.yui.sortedPaths['yql'].substr(0, 5)); - A.areSame('/foo/', instance.yui.sortedPaths['querystring-parse'].substr(0, 5)); - A.areSame('/foo/', instance.yui.sortedPaths['querystring-stringify'].substr(0, 5)); - A.areSame('/foo/', instance.yui.sortedPaths['json-stringify'].substr(0, 5)); + A.isUndefined(instance.yui.sortedPaths['addon-ac-not']); + A.isUndefined(instance.yui.sortedPaths['MAutoloadNot']); + A.isUndefined(instance.yui.sortedPaths['MModelNot']); + A.isUndefined(instance.views['not']['binder-url']); }); }, + 'TODO app with app-level rollup': function() { + A.skip(); + }, + 'sortedReaddirSync() sorts the result of fs.readdirSync()': function() { + var fixtures = libpath.join(__dirname, '../fixtures/store'); var mockfs = Mock(); Mock.expect(mockfs, { @@ -891,145 +506,28 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { returns: ['d', 'c', 'a', 'b'] }); - var store = new ResourceStore(fixtures, { fs: mockfs }); + var store = new Y.mojito.ResourceStore({ root: fixtures }); + store._mockLib('fs', mockfs); var files = store._sortedReaddirSync('dir'); AA.itemsAreSame(['a', 'b', 'c', 'd'], files); Mock.verify(mockfs); }, - '_readYcbDimensions() uses application dimensions.json': function() { - var store, - mockpath = Mock(), - joinCount = 0; - - Mock.expect(mockpath, { - method: 'join', - args: [Mock.Value.String, 'dimensions.json'], - returns: 'joinedpath_app', - callCount: 1, - run: function(base, ignored) { - A.areSame(store._root, base); - } - }); - - Mock.expect(mockpath, { - method: 'existsSync', - args: ['joinedpath_app'], - returns: true - }); - - var mockstore = Mock(); - - Mock.expect(mockstore, { - method: '_readConfigJSON', - args: ['joinedpath_app'], - returns: [] - }); - - Mock.expect(mockstore, { - method: '_isValidYcbDimensions', - args: [Mock.Value.Object], - returns: true - }); - - store = new ResourceStore(fixtures, { path: mockpath }); - store._readConfigJSON = Y.bind(mockstore._readConfigJSON, mockstore); - store._isValidYcbDimensions = Y.bind(mockstore._isValidYcbDimensions, mockstore); - - var dims = store._readYcbDimensions(); - AA.isEmpty(dims, 'Expected the empty array returned by the mocked method'); - - Mock.verify(mockstore); - Mock.verify(mockpath); - }, - - '_readYcbDimensions() falls back when application dimensions.json missing': function() { - var store; - - var mockstore = Mock(); - - Mock.expect(mockstore, { - method: '_readConfigJSON', - args: ['joinedpath_fw'], - args: [libpath.join(__dirname, '../../dimensions.json')], - returns: [] - }); - - Mock.expect(mockstore, { - method: '_isValidYcbDimensions', - args: [Mock.Value.Object], - returns: true - }); - - store = new ResourceStore(fixtures); - store._readConfigJSON = Y.bind(mockstore._readConfigJSON, mockstore); - store._isValidYcbDimensions = Y.bind(mockstore._isValidYcbDimensions, mockstore); - - var dims = store._readYcbDimensions(); - AA.isEmpty(dims, 'Expected the empty array returned by the mocked method'); - - Mock.verify(mockstore); - }, - - '_readYcbDimensions() throws an error when dimensions.json is invalid': function() { - var mockstore = Mock(); - - Mock.expect(mockstore, { - method: '_readConfigJSON', - args: [Mock.Value.String], - returns: [] //the invalid dimensions.json - }); - - var store = new ResourceStore(fixtures); - store._readConfigJSON = Y.bind(mockstore._readConfigJSON, mockstore); - - try { - store._readYcbDimensions(); - A.fail('Expected an exception'); - } catch (e) { - A.areSame('Invalid dimensions.json: ' + libpath.join(mojitoRoot, 'dimensions.json'), e.message); - } - - Mock.verify(mockstore); - }, - - '_isValidYcbDimensions() allows dimensions array with single key': function() { - var dims = [{ dimensions: [{ dimKey: {} }] }]; - var store = new ResourceStore(fixtures); - A.isTrue(store._isValidYcbDimensions(dims)); - }, - - '_isValidYcbDimensions() spots empty array': function() { - var dims = []; - var store = new ResourceStore(fixtures); - A.isFalse(store._isValidYcbDimensions(dims)); - }, - - '_isValidYcbDimensions() spots empty dimensions': function() { - var dims = [{ dimensions: [] }]; - var store = new ResourceStore(fixtures); - A.isFalse(store._isValidYcbDimensions(dims)); - }, - - '_isValidYcbDimensions() spots too many top-level items': function() { - var dims = [{ dimensions: [{ dimKey: {} }] }, { extraDimensions: [] }]; - var store = new ResourceStore(fixtures); - A.isFalse(store._isValidYcbDimensions(dims)); - }, - '_skipBadPath() does just that': function() { - var store = new ResourceStore(fixtures); - A.areSame(true, store._skipBadPath({ ext: '.js~' })); - A.areSame(false, store._skipBadPath({ ext: '.js' })); + var fixtures = libpath.join(__dirname, '../fixtures/store'); + var store = new Y.mojito.ResourceStore({ root: fixtures }); + A.isTrue(store._skipBadPath({ isFile: true, ext: '.js~' }), 'need to skip bad file naems'); + A.isFalse(store._skipBadPath({ isFile: false, ext: '.js~' }), 'need to not-skip bad directory names'); + A.isFalse(store._skipBadPath({ isFile: true, ext: '.js' }), 'need to not-skip good file names'); }, 'load node_modules': function() { var fixtures = libpath.join(__dirname, '../fixtures/packages'), - store = new ResourceStore(fixtures); + store = new Y.mojito.ResourceStore({ root: fixtures }); store.preload(); - if (!store._mojitMeta.server.a && !store._mojitMeta.server.aa && !store._mojitMeta.server.ba) { + if (!store._mojitRVs.a && !store._mojitRVs.aa && !store._mojitRVs.ba) { // This happens when mojito is installed via npm, since npm // won't install the node_modules/ directories in // tests/fixtures/packages. @@ -1037,395 +535,259 @@ YUI.add('mojito-store-server-tests', function(Y, NAME) { return; } - var m, mojits = ['a', 'aa', 'ba']; - var mojitType, mojitMeta; + var m, mojitType, mojits = ['a', 'aa', 'ba']; + var r, res, ress, found; for (m = 0; m < mojits.length; m += 1) { mojitType = mojits[m]; - mojitMeta = store._mojitMeta.server[mojitType]; - A.isNotUndefined(mojitMeta, 'mojitMeta should be defined'); - mojitMeta = mojitMeta['*']; - A.isNotUndefined(mojitMeta['yui-module-b'], 'yui-module-b should be defined'); - A.isNotUndefined(mojitMeta['yui-module-ab'], 'yui-module-ab should be defined'); - A.isNotUndefined(mojitMeta['yui-module-bb'], 'yui-module-bb should be defined'); - A.isNotUndefined(mojitMeta['yui-module-cb'], 'yui-module-cb should be defined'); - // tests that yahoo.mojito.location in package.json works - // (which mojito package itself uses) - A.isNotUndefined(mojitMeta['addon-ac-assets'], 'addon-ac-assets should be defined'); + + ress = store.getResources('server', {}, {mojit: mojitType}); + found = 0; + for (r = 0; r < ress.length; r += 1) { + res = ress[r]; + if (res.id === 'yui-module--b') { found += 1; } + if (res.id === 'yui-module--ab') { found += 1; } + if (res.id === 'yui-module--bb') { found += 1; } + if (res.id === 'yui-module--cb') { found += 1; } + } + A.areSame(4, found, 'some child node_modules not loaded'); } - var details = {}; - store.getMojitTypeDetails('server', {}, 'a', details); - A.isNotNull(details.controller.match(/a\/foo\/controller\.server\.js$/), 'controller should not be null'); + var details = store.getMojitTypeDetails('server', {}, 'a'); + A.isNotNull(details['controller-path'].match(/a\/foo\/controller\.server\.js$/), 'controller should not be null'); }, 'find and parse resources by convention': function() { var fixtures = libpath.join(__dirname, '../fixtures/conventions'), - store = new ResourceStore(fixtures); + store = new Y.mojito.ResourceStore({ root: fixtures }); // fake out some parts of preload(), which we're trying to avoid - store._fwConfig = store._readConfigJSON(libpath.join(mojitoRoot, 'config.json')); - store._appConfigStatic = store._readAppConfigStatic(); + store._fwConfig = store.config.readConfigJSON(libpath.join(mojitoRoot, 'config.json')); + store._appConfigStatic = store.getStaticAppConfig(); var dir = libpath.join(__dirname, '../fixtures/conventions'); var pkg = { name: 'test', version: '6.6.6' }; var mojitType = 'testing'; - var ress = store._findResourcesByConvention(dir, pkg, mojitType) + var ress = store._findResourcesByConvention(dir, 'app', pkg, mojitType) var r, res; for (r = 0; r < ress.length; r++) { res = ress[r]; A.isNotUndefined(res.id, 'no resource id'); switch (res.id) { - case 'action-x': - A.areSame(pkg, res.pkg); + case 'action--x': + A.areSame(pkg, res.source.pkg); A.areSame('action', res.type); A.areSame('x', res.name); - A.areSame('action-x', res.yuiModuleName); - switch (res.shortPath) { - case 'x.common.js': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); + switch (res.source.fs.basename) { + case 'x.common': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + A.areSame('x', res.name); break; - case 'x.common.iphone.js': - A.areSame('device=iphone', res.pathParts.contextKey); - A.areSame('iphone', res.pathParts.contextParts.device); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); + case 'x.common.iphone': + A.areSame('iphone', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + A.areSame('x', res.name); break; default: - A.fail('unknown resource ' + res.fsPath); + A.fail('unknown resource ' + res.source.fs.fullPath); break; } break; - case 'action-y/z': - A.areSame(pkg, res.pkg); + case 'action--y/z': + A.areSame(pkg, res.source.pkg); A.areSame('action', res.type); A.areSame('y/z', res.name); - A.areSame('y/z.common.js', res.shortPath); - A.areSame('action-y-z', res.yuiModuleName); - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('z', res.pathParts.shortFile); + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + A.areSame('z.common', res.source.fs.basename); break; case 'addon-a-x': - A.areSame(pkg, res.pkg); + A.areSame(pkg, res.source.pkg); A.areSame('addon', res.type); - A.areSame('a', res.addonType); + A.areSame('a', res.subtype); A.areSame('x', res.name); - A.areSame('addon-a-x', res.yuiModuleName); - switch (res.shortPath) { - case 'x.common.js': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); + switch (res.source.fs.basename) { + case 'x.common': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + A.areSame('x', res.name); break; - case 'x.common.iphone.js': - A.areSame('device=iphone', res.pathParts.contextKey); - A.areSame('iphone', res.pathParts.contextParts.device); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); + case 'x.common.iphone': + A.areSame('iphone', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); + A.areSame('x', res.name); break; default: - A.fail('unknown resource ' + res.fsPath); + A.fail('unknown resource ' + res.source.fs.fullPath); break; } break; case 'archetype-x-y': - A.areSame(pkg, res.pkg); + A.areSame(pkg, res.source.pkg); A.areSame('archetype', res.type); A.areSame('x', res.subtype); A.areSame('y', res.name); - A.areSame('y', res.shortPath); + A.areSame('y', res.source.fs.basename); break; - case 'asset-x.css': - A.areSame(pkg, res.pkg); + case 'asset-css-x': + A.areSame(pkg, res.source.pkg); A.areSame('asset', res.type); - A.areSame('css', res.assetType); - A.areSame('x.css', res.name); - switch (res.shortPath) { - case 'x.css': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.css', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); + A.areSame('css', res.subtype); + A.areSame('x', res.name); + switch (res.source.fs.basename) { + case 'x': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.css', res.source.fs.ext); break; - case 'x.iphone.css': - A.areSame('device=iphone', res.pathParts.contextKey); - A.areSame('iphone', res.pathParts.contextParts.device); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.css', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); + case 'x.iphone': + A.areSame('iphone', res.selector); + A.areSame('common', res.affinity); + A.areSame('.css', res.source.fs.ext); break; default: - A.fail('unknown resource ' + res.fsPath); + A.fail('unknown resource ' + res.source.fs.fullPath); break; } break; - case 'asset-y/z.css': - A.areSame(pkg, res.pkg); + case 'asset-css-y/z': + A.areSame(pkg, res.source.pkg); A.areSame('asset', res.type); - A.areSame('css', res.assetType); - A.areSame('y/z.css', res.name); - switch (res.shortPath) { - case 'y/z.css': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.css', res.pathParts.ext); - A.areSame('z', res.pathParts.shortFile); + A.areSame('css', res.subtype); + A.areSame('y/z', res.name); + switch (res.source.fs.basename) { + case 'z': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.css', res.source.fs.ext); break; - case 'y/z.android.css': - A.areSame('device=android', res.pathParts.contextKey); - A.areSame('android', res.pathParts.contextParts.device); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.css', res.pathParts.ext); - A.areSame('z', res.pathParts.shortFile); + case 'z.android': + A.areSame('android', res.selector); + A.areSame('common', res.affinity); + A.areSame('.css', res.source.fs.ext); break; default: - A.fail('unknown resource ' + res.fsPath); + A.fail('unknown resource ' + res.source.fs.fullPath); break; } break; - case 'binder-x': - A.areSame(pkg, res.pkg); + case 'binder--x': + A.areSame(pkg, res.source.pkg); A.areSame('binder', res.type); A.areSame('x', res.name); - A.areSame('x', res.yuiModuleName); - switch (res.shortPath) { - case 'x.js': - A.areSame('*', res.pathParts.contextKey); - A.areSame('client', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); + switch (res.source.fs.basename) { + case 'x': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); break; - case 'x.iphone.js': - A.areSame('device=iphone', res.pathParts.contextKey); - A.areSame('iphone', res.pathParts.contextParts.device); - A.areSame('client', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); + case 'x.iphone': + A.areSame('iphone', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); break; default: - A.fail('unknown resource ' + res.fsPath); + A.fail('unknown resource ' + res.source.fs.fullPath); break; } break; - case 'command-x': - A.areSame(pkg, res.pkg); + case 'command--x': + A.areSame(pkg, res.source.pkg); A.areSame('command', res.type); A.areSame('x', res.name); - A.areSame('x.js', res.shortPath); + A.areSame('x', res.source.fs.basename); break; - case 'config-config': - A.areSame(pkg, res.pkg); + case 'config--config': + A.areSame(pkg, res.source.pkg); A.areSame('config', res.type); A.areSame('config', res.name); - A.areSame('config.json', res.shortPath); + A.areSame('config', res.source.fs.basename); + A.areSame('.json', res.source.fs.ext); break; - case 'controller': - A.areSame(pkg, res.pkg); + case 'controller--controller': + A.areSame(pkg, res.source.pkg); A.areSame('controller', res.type); A.areSame('controller', res.name); - A.areSame('controller', res.yuiModuleName); - switch (res.shortPath) { - case 'controller.common.js': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('controller', res.pathParts.shortFile); + switch (res.source.fs.basename) { + case 'controller.common': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.js', res.source.fs.ext); break; - case 'controller.server.iphone.js': - A.areSame('device=iphone', res.pathParts.contextKey); - A.areSame('iphone', res.pathParts.contextParts.device); - A.areSame('server', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('controller', res.pathParts.shortFile); + case 'controller.server.iphone': + A.areSame('iphone', res.selector); + A.areSame('server', res.affinity); + A.areSame('.js', res.source.fs.ext); break; default: - A.fail('unknown resource ' + res.fsPath); + A.fail('unknown resource ' + res.source.fs.fullPath); break; } break; - case 'middleware-x': - A.areSame(pkg, res.pkg); + case 'middleware--x': + A.areSame(pkg, res.source.pkg); A.areSame('middleware', res.type); A.areSame('x', res.name); - A.areSame('x.js', res.shortPath); + A.areSame('x', res.source.fs.basename); + A.areSame('.js', res.source.fs.ext); break; - case 'spec-default': - A.areSame(pkg, res.pkg); + case 'spec--default': + A.areSame(pkg, res.source.pkg); A.areSame('spec', res.type); A.areSame('default', res.name); - A.areSame('testing', res.specName); - A.areSame('default.json', res.shortPath); + A.areSame('default', res.source.fs.basename); + A.areSame('.json', res.source.fs.ext); break; - case 'spec-x': - A.areSame(pkg, res.pkg); + case 'spec--x': + A.areSame(pkg, res.source.pkg); A.areSame('spec', res.type); + A.areSame('testing', res.mojit); A.areSame('x', res.name); - A.areSame('testing:x', res.specName); - A.areSame('x.json', res.shortPath); + A.areSame('x', res.source.fs.basename); + A.areSame('.json', res.source.fs.ext); break; - case 'view-x': - A.areSame(pkg, res.pkg); + case 'view--x': + A.areSame(pkg, res.source.pkg); A.areSame('view', res.type); A.areSame('x', res.name); - A.areSame('html', res.viewOutputFormat); - A.areSame('mu', res.viewEngine); - switch (res.shortPath) { - case 'x.mu.html': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.html', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); - break; - case 'x.iphone.mu.html': - A.areSame('device=iphone', res.pathParts.contextKey); - A.areSame('iphone', res.pathParts.contextParts.device); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.html', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); - break; - default: - A.fail('unknown resource ' + res.fsPath); - break; - } - break; - case 'yui-lang-': - A.areSame(pkg, res.pkg); - A.areSame('yui-lang', res.type); - A.areSame('', res.name); - A.areSame('', res.langCode); - A.areSame('testing.js', res.shortPath); - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('testing', res.pathParts.shortFile); - break; - case 'yui-lang-de': - A.areSame(pkg, res.pkg); - A.areSame('yui-lang', res.type); - A.areSame('de', res.name); - A.areSame('de', res.langCode); - A.areSame('testing_de.js', res.shortPath); - A.areSame('lang=de', res.pathParts.contextKey); - A.areSame('de', res.pathParts.contextParts.lang); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('testing_de', res.pathParts.shortFile); - break; - case 'yui-lang-en': - A.areSame(pkg, res.pkg); - A.areSame('yui-lang', res.type); - A.areSame('en', res.name); - A.areSame('en', res.langCode); - A.areSame('testing_en.js', res.shortPath); - A.areSame('lang=en', res.pathParts.contextKey); - A.areSame('en', res.pathParts.contextParts.lang); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('testing_en', res.pathParts.shortFile); - break; - case 'yui-lang-en-US': - A.areSame(pkg, res.pkg); - A.areSame('yui-lang', res.type); - A.areSame('en-US', res.name); - A.areSame('en-US', res.langCode); - A.areSame('testing_en-US.js', res.shortPath); - A.areSame('lang=en-US', res.pathParts.contextKey); - A.areSame('en-US', res.pathParts.contextParts.lang); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('testing_en-US', res.pathParts.shortFile); - break; - case 'yui-module-m': - A.areSame(pkg, res.pkg); - A.areSame('yui-module', res.type); - A.areSame('m', res.name); - A.areSame('m', res.yuiModuleName); - switch (res.shortPath) { - case 'm.common.js': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('m', res.pathParts.shortFile); - break; - case 'm.common.iphone.js': - A.areSame('device=iphone', res.pathParts.contextKey); - A.areSame('iphone', res.pathParts.contextParts.device); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('m', res.pathParts.shortFile); - break; - default: - A.fail('unknown resource ' + res.fsPath); - break; - } - break; - case 'yui-module-x': - A.areSame(pkg, res.pkg); - A.areSame('yui-module', res.type); - A.areSame('x', res.name); - A.areSame('x', res.yuiModuleName); - switch (res.shortPath) { - case 'x.common.js': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); - break; - case 'x.common.iphone.js': - A.areSame('device=iphone', res.pathParts.contextKey); - A.areSame('iphone', res.pathParts.contextParts.device); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('x', res.pathParts.shortFile); - break; - default: - A.fail('unknown resource ' + res.fsPath); + A.areSame('html', res.view.outputFormat); + A.areSame('mu', res.view.engine); + switch (res.source.fs.basename) { + case 'x.mu': + A.areSame('*', res.selector); + A.areSame('common', res.affinity); + A.areSame('.html', res.source.fs.ext); break; - } - break; - case 'yui-module-z': - A.areSame(pkg, res.pkg); - A.areSame('yui-module', res.type); - A.areSame('z', res.name); - A.areSame('z', res.yuiModuleName); - switch (res.shortPath) { - case 'y/z.common.js': - A.areSame('*', res.pathParts.contextKey); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('z', res.pathParts.shortFile); - break; - case 'y/z.common.android.js': - A.areSame('device=android', res.pathParts.contextKey); - A.areSame('android', res.pathParts.contextParts.device); - A.areSame('common', res.pathParts.affinity.affinity); - A.areSame('.js', res.pathParts.ext); - A.areSame('z', res.pathParts.shortFile); + case 'x.iphone.mu': + A.areSame('iphone', res.selector); + A.areSame('common', res.affinity); + A.areSame('.html', res.source.fs.ext); break; default: - A.fail('unknown resource ' + res.fsPath); + A.fail('unknown resource ' + res.source.fs.fullPath); break; } break; - default: A.fail('unknown resource ' + res.id); break; } } - A.areSame(31, ress.length, 'wrong number of resources'); + A.areSame(21, ress.length, 'wrong number of resources'); } })); YUITest.TestRunner.add(suite); -}, '0.0.1', {requires: ['oop', 'mojito-resource-store-adapter']}); +}, '0.0.1', {requires: [ + 'oop', + 'mojito-resource-store', + 'mojito-resource-store-adapter' +]}); diff --git a/source/lib/tests/fixtures/gsg5/application.json b/source/lib/tests/fixtures/gsg5/application.json index a9421a3f9..2653c7486 100644 --- a/source/lib/tests/fixtures/gsg5/application.json +++ b/source/lib/tests/fixtures/gsg5/application.json @@ -50,5 +50,9 @@ "type": "FlickrDetail" } } + }, + { + "settings": [ "device:iphone" ], + "selector": "iphone" } ] diff --git a/source/lib/tests/fixtures/store/application.json b/source/lib/tests/fixtures/store/application.json index 024372df2..75ccabd65 100644 --- a/source/lib/tests/fixtures/store/application.json +++ b/source/lib/tests/fixtures/store/application.json @@ -20,13 +20,30 @@ { "settings": [ "runtime:server" ], + "selector": "shelves", "testKey1": "testVal1-server", "pathos": "portended" }, { "settings": [ "runtime:client" ], + "selector": "right", "testKey2": "testVal2-client", "testKey4": "testVal4-client" + }, + { + "settings": [ "device:iphone" ], + + "selector": "iphone" + }, + { + "settings": [ "device:android" ], + + "selector": "droid" + }, + { + "settings": [ "device:android", "environment:dev" ], + + "selector": "devdroid" } ] diff --git a/source/lib/tests/fixtures/store/mojits/HTMLFrameMojit/controller.server.js b/source/lib/tests/fixtures/store/mojits/HTMLFrameMojit/controller.server.js new file mode 100644 index 000000000..5288b5c25 --- /dev/null +++ b/source/lib/tests/fixtures/store/mojits/HTMLFrameMojit/controller.server.js @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2012, Yahoo! Inc. All rights reserved. + * Copyrights licensed under the New BSD License. + * See the accompanying LICENSE file for terms. + */ +YUI.add('HTMLFrameMojit', function(Y, NAME) { + // just the existence is important +}, '0.1.0', {requires: []}); diff --git a/source/lib/tests/fixtures/store/mojits/test_mojit_2/package.json b/source/lib/tests/fixtures/store/mojits/test_mojit_2/package.json index 5811cd49b..65f94715a 100644 --- a/source/lib/tests/fixtures/store/mojits/test_mojit_2/package.json +++ b/source/lib/tests/fixtures/store/mojits/test_mojit_2/package.json @@ -1,8 +1,8 @@ { "name": "TestMojit2", - "config": { + "yahoo": { "mojito": { - "version": "*" + "version": "0.3.x" } } -} \ No newline at end of file +} diff --git a/source/lib/tests/fixtures/store/mojits/test_mojit_3/package.json b/source/lib/tests/fixtures/store/mojits/test_mojit_3/package.json index ed1f90df3..c98083c6f 100644 --- a/source/lib/tests/fixtures/store/mojits/test_mojit_3/package.json +++ b/source/lib/tests/fixtures/store/mojits/test_mojit_3/package.json @@ -1,9 +1,11 @@ { "name": "TestMojit3", - "config": { + "yahoo": { "mojito": { - "version": "0.1.0", "package": "public" } + }, + "engines": { + "mojito": ">0.1.0" } } diff --git a/source/lib/tests/fixtures/store/mojits/test_mojit_4/package.json b/source/lib/tests/fixtures/store/mojits/test_mojit_4/package.json index 762dbdcfe..b2c6da8cc 100644 --- a/source/lib/tests/fixtures/store/mojits/test_mojit_4/package.json +++ b/source/lib/tests/fixtures/store/mojits/test_mojit_4/package.json @@ -1,9 +1,11 @@ { "name": "TestMojit4", - "config": { + "yahoo": { "mojito": { - "version": "1.0.0", "package": "public" } + }, + "engines": { + "mojito": "<0.1.0" } } diff --git a/source/package.json b/source/package.json index a77511a5a..94463a5db 100644 --- a/source/package.json +++ b/source/package.json @@ -17,6 +17,7 @@ "express": "2.5.2", "glob": "~3.1.11", "mime": "1.2.4", + "semver": "1.0.14", "yui": "~3.5.1", "yuitest": "~0.7.4" },