/*
* 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, regexp: true, continue: true, nomen:true, node:true, stupid:true
*/
/**
* 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.
*
* The metadata kept about each resource is normalized to the follow keys:
* <dl>
* <dt><code>source</code> (object)</dt>
* <dd>where the source came from. (not shipped to the client.)
* <dl>
* <dt><code>fs</code> (object)</dt>
* <dd>filesystem details</dd>
* <dt><code>pkg</code> (object)</dt>
* <dd>packaging details</dd>
* </dl>
* </dd>
* <dt><code>mojit</code> (string)</dt>
* <dd>which mojit this applies to, if any. ("shared" means the resource is available to all mojits.)</dd>
* <dt><code>type</code> (string)</dt>
* <dd>resource type</dd>
* <dt><code>subtype</code> (string)</dt>
* <dd>not all types of subtypes</dd>
* <dt><code>name</code> (string)</dt>
* <dd>common to all versions of the resource</dd>
* <dt><code>id</code> (string)</dt>
* <dd>unique ID. common to all versions of the resource. (typically <code>{type}-{subtype}-{name}</code>.)</dd>
* <dt><code>yui</code> (object)</dt>
* <dd>for resources that are YUI modules</dd>
* </dl>
*
* The following are only used in the metadata for each resource <em>version</em>
* (The metadata for resolved resources won't have these, since they're intrinsically
* part of the resolved resource.)
* <dd>
* <dt><code>affinity</code> (string)</dt>
* <dd>runtime affinity. either <code>server</code>, <code>client</code>, or <code>common</code></dd>
* <dt><code>selector</code> (string)</dt>
* <dd>version selector</dd>
* </dl>
*
* @module ResourceStore
*/
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'
};
libs.fs = require('fs');
libs.glob = require('glob');
libs.path = require('path');
libs.semver = require('semver');
libs.walker = require('./package-walker.server');
// 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;
}
};
/**
* @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`
*/
function ResourceStore(config) {
ResourceStore.superclass.constructor.apply(this, arguments);
}
ResourceStore.NAME = 'ResourceStore';
ResourceStore.ATTRS = {};
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. Note that since we're
// forcing the load we have to also include mojito-util.
this._yuiUseSync({
'mojito-util': {
fullpath: this._libs.path.join(__dirname, 'app/autoload/util.common.js')
},
'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 });
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() {},
//====================================================================
// PUBLIC METHODS
/**
* 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;
},
/**
* Returns the static (non-runtime-sensitive) context
* @method getStaticContext
* @return {object} the context
*/
getStaticContext: function() {
return Y.mojito.util.copy(this._config.context);
},
/**
* Returns the static (non-runtime-sensitive) version of the application.json.
* @method getStaticAppConfig
* @return {object} the configuration from applications.json
*/
getStaticAppConfig: function() {
return Y.mojito.util.copy(this._appConfigStatic);
},
/**
* Returns Mojito's built-in configuration.
* @method getFrameworkConfig
* @return {object} the configuration for mojito
*/
getFrameworkConfig: function() {
return Y.mojito.util.copy(this._fwConfig);
},
/**
* 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.validateContext(ctx);
if (this._appConfigStatic && (!ctx || !Object.keys(ctx).length)) {
return Y.mojito.util.copy(this._appConfigStatic);
}
// start with the base
appConfig = Y.mojito.util.copy(this._fwConfig.appConfigBase);
// 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);
// apply the passed-in overrides
this.mergeRecursive(appConfig, Y.mojito.util.copy(this._config.appConfig));
return appConfig;
},
/**
* 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;
}
}
}
if (use) {
out.push(res);
}
}
return out;
},
/**
* 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;
}
}
}
if (use) {
out.push(res);
}
}
return out;
},
/**
* 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);
}
}
}
return list;
},
/**
* 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 (env === 'client' && obj) {
delete obj.assets;
}
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);
});
},
/**
* 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;
},
/**
* 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;
this.validateContext(ctx);
if (cacheValue) {
cb(null, Y.mojito.util.copy(cacheValue));
return;
}
// TODO: should this be done here, or somewhere else?
ctx.runtime = env;
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();
}
spec.appConfig = this.getAppConfig(ctx);
delete spec.appConfig.specs;
try {
this.getMojitTypeDetails(env, ctx, spec.type, spec);
} catch (err2) {
return cb(err2);
}
if (spec.defaults && spec.defaults.config) {
config = Y.mojito.util.copy(spec.defaults.config);
this.mergeRecursive(config, spec.config);
spec.config = config;
}
this._expandInstanceCache[env][cacheKey] = Y.mojito.util.copy(spec);
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;
this.validateContext(ctx);
if ('shared' === mojitType) {
throw new Error('Mojit name "shared" is special and isn\'t a real mojit.');
}
if (!dest) {
dest = {};
}
if (!dest.assets) {
dest.assets = {};
}
if (!dest.models) {
dest.models = {};
}
if (!dest.views) {
dest.views = {};
}
dest.definition = {};
dest.defaults = {};
ress = this.getResources(env, ctx, { mojit: mojitType });
for (r = 0; r < ress.length; r += 1) {
res = ress[r];
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') {
if (env === 'client') {
dest.assets[res.name + res.source.fs.ext] = res.url;
} else {
dest.assets[res.name + res.source.fs.ext] = res.source.fs.fullPath;
}
}
if (res.type === 'binder') {
if (!dest.views[res.name]) {
dest.views[res.name] = {};
}
dest.views[res.name]['binder-url'] = res.url;
if (env === 'client') {
dest.views[res.name]['binder-path'] = res.url;
} else {
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
if (env === 'client') {
dest['controller-path'] = res.url;
} else {
dest['controller-path'] = res.source.fs.fullPath;
}
}
if (res.type === 'model') {
dest.models[res.name] = true;
}
if (res.type === 'view') {
if (!dest.views[res.name]) {
dest.views[res.name] = {};
}
if (env === 'client') {
dest.views[res.name]['content-path'] = res.url;
} else {
dest.views[res.name]['content-path'] = res.source.fs.fullPath;
}
dest.views[res.name].engine = res.view.engine;
engines[res.view.engine] = true;
}
}
// 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;
},
/**
* 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;
}
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.mojito.util.copy(this.config.readConfigYCB(res.source.fs.fullPath, ctx));
out = Y.merge(out, routes);
}
}
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;
}
}
}
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;
},
//====================================================================
// 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.
/**
* 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;
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
};
}
}
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);
},
/**
* 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);
});
// 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'
}
}
};
}
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;
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
};
}
// 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
};
}
// 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
};
}
// 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;
}
// 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;
// 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;
}
// 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;
}
// 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;
}
// 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;
}
// 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;
}
if (res.selector) {
this.selectors[res.selector] = true;
}
if (res.mojit) {
if (!this._mojitRVs[res.mojit]) {
this._mojitRVs[res.mojit] = [];
}
this._mojitRVs[res.mojit].push(res);
} else {
this._appRVs.push(res);
}
},
/**
* 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 ]);
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
});
}
}
}
}
},
/**
* 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;
out.routes = this.getRoutes(ctx);
return out;
},
//====================================================================
// PRIVATE METHODS
/**
* 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;
},
/**
* @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;
},
/**
* 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;
if (!spec.base) {
return spec;
}
// The base will need to carry its ID with it.
spec.id = spec.base;
appConfig = this.getAppConfig(ctx);
// appConfig.specs might be undefined, for example in newly created apps
base = appConfig.specs && 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 "' + spec.base + '". You should have configured "' + spec.base + '" in application.json under specs or used "@' + spec.base + '" if you wanted to specify a mojit name.');
}
delete spec.base;
return this.mergeRecursive(this._expandSpec(env, ctx, base), spec);
},
/**
* 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;
}
},
/**
* 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;
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);
}
// 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);
}
},
/**
* 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 ('/' !== dir.charAt(0)) {
dir = this._libs.path.join(this._config.root, dir);
}
if (!(this._libs.fs.existsSync || 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 = this._libs.path.join(dir, childName);
this._preloadDirMojit(childPath, dirType, pkg);
}
},
/**
* 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;
if ('/' !== dir.charAt(0)) {
dir = this._libs.path.join(this._config.root, dir);
}
if (!(this._libs.fs.existsSync || this._libs.path.existsSync)(dir)) {
return;
}
if ('pkg' === dirType) {
mojitType = pkg.name;
} else {
mojitType = this._libs.path.basename(dir);
}
packageJson = this.config.readConfigJSON(this._libs.path.join(dir, 'package.json'));
if (packageJson) {
if (packageJson.name) {
mojitType = packageJson.name;
}
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;
}
}
// TODO: register mojit's package.json as a static asset, in "static handler" plugin
}
definitionJson = this.config.readConfigYCB(this._libs.path.join(dir, 'definition.json'), {});
if (definitionJson.appLevel) {
mojitType = 'shared';
}
// 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 (!affinities.hasOwnProperty(res.affinity)) {
continue;
}
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] = {};
}
if (!versions[res.id][priority]) {
versions[res.id][priority] = res;
}
}
}
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.mojito.util.copy(versions[resid][highest]);
out.push(chosen);
}
}
return out;
},
/**
* 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
*/
_findResourcesByConvention: function(dir, dirType, pkg, mojitType) {
var me = this,
ress = [];
//console.log('-- FIND RESOURCES BY CONVENTION -- ' + pkg.name + '@' + pkg.version + ' -- ' + mojitType);
this._walkDirRecursive(dir, function(error, subdir, file, isFile) {
var source, ret, res;
if ('node_modules' === file) {
return false;
}
if ('libs' === file && 'test' !== me._appConfigStatic.env) {
return false;
}
if ('tests' === file && 'test' !== me._appConfigStatic.env) {
return false;
}
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);
if (me._skipBadPath(source.fs)) {
return false;
}
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 ret;
});
return ress;
},
/**
* 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;
},
/**
* 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.fs.existsSync || this._libs.path.existsSync)(fulldir)) {
return;
}
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 = 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 (childStat.isFile()) {
cb(null, subdir, childName, true);
} else if (childStat.isDirectory()) {
if (cb(null, subdir, childName, false)) {
this._walkDirRecursive(dir, cb, childPath);
}
}
}
},
/**
* 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 });
}
});
Y.namespace('mojito');
Y.mojito.ResourceStore = ResourceStore;
}, '0.0.1', { requires: [
'base',
'json-stringify',
'oop',
'mojito-util'
]});