mojito
Version:
Mojito provides an architecture, components and tools for developers to build complex web applications faster.
1,292 lines (1,130 loc) • 96.2 kB
JavaScript
/*
* Copyright (c) 2011-2013, Yahoo! Inc. All rights reserved.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/
/*jslint
forin: true, anon:true, regexp: true, continue: true, nomen:true, node:true, stupid:true, plusplus: true
*/
/*global YUI*/
/**
* 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) {
'use strict';
var libs = {},
resourceSortByDepthTest = function (a, b) {
return a.source.pkg.depth - b.source.pkg.depth;
},
isNotAlphaNum = /[^a-zA-Z0-9]/,
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': 'client',
'command': 'server',
'controller': 'server',
'middleware': 'server',
'model': 'server',
'spec': 'common',
'view': 'common'
},
PATH_SEP = require('path').sep;
libs.fs = require('fs');
libs.glob = require('glob');
libs.path = require('path');
libs.semver = require('semver');
libs.walker = require('./package-walker.server');
libs.util = require('../../util.js');
libs.logger = require('../../logger.js');
libs.yuiFactory = require('../../yui-sandbox.js');
// 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._libs = {};
for (i in libs) {
if (libs.hasOwnProperty(i)) {
this._libs[i] = libs[i];
}
}
this._config = cfg || {};
this._config.context = this._config.context || {};
this._config.appConfig = this._config.appConfig || {};
this._config.dir = this._config.dir || process.cwd();
this._config.root = this._config.root ||
this._config.dir;
this._config.mojitoRoot = this._config.mojitoRoot ||
this._libs.path.join(__dirname, '../..');
this._jsonCache = {}; // fullPath: contents as JSON object
this._ycbCache = {}; // fullPath: context: YCB config object
this._routesCache = null; // serialized routes
this._appConfigCache = {}; //cache for the app config
this._validateContextCache = {}; // ctx: error string or "VALID"
this._getMojitTypeDetailsCache = {}; // env+posl+lang+mojitType: value
this._expandSpecCache = {}; // env+ctx+spec: value
this._packagesVisited = {}; // package@version: path
this._appRVs = []; // array of resource versions
this._mojitRVs = {}; // mojitType: array of resource versions
this._appPkg = null; // metadata about the applicaions's NPM package
this._specPaths = {}; // spec name: full path
this._mojitDetails = {}; // mojitType: selector: affinity: non-stringified details
this._mojitDetailsCache = {}; // mojitType+poslString+env: resolved resources
this._unloadedLangs = {}; // mojit, lang mapping to an array of lang resources, when lazyLangs is on.
this._unloadedMojits = {}; // mojit mapping to an array of unprocessed mojit directories, when lazyMojits is on.
this._updateLoaderCount = 0; // count keeping track of newly loaded mojits requiring the loader to be updated.
this.YUI = null; // The runtime YUI object.
this.Y = null; // The runtime YUI instance.
/**
* 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('resolveMojitDetails', {emitFacade: true, preventable: false});
this.publish('loadConfigs', {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({
'addon-rs-config': {
fullpath: this._libs.path.join(
this._config.mojitoRoot,
'app/addons/rs/config.js'
)
}
});
this.plug(Y.mojito.addons.rs.config, { appRoot: this._config.root, mojitoRoot: this._config.mojitoRoot });
this.loadConfigs();
Y.log('Store initialized', 'info', NAME);
},
destructor: function() {},
//====================================================================
// PUBLIC METHODS
/**
* Loads the dimensions and configurations found in the app
* @method loadConfigs
*/
loadConfigs: function () {
this._validDims = this._parseValidDims(this.config.getDimensions());
this.validateContext(this._config.context);
this._fwConfig = this.config.readConfigSimple(this._libs.path.join(this._config.mojitoRoot, 'config.json'));
this._appConfigYCB = this.config.getAppConfigYCB();
this._appConfigStatic = this.getAppConfig({});
this.lazyResolve = this._appConfigStatic.resourceStore && this._appConfigStatic.resourceStore.lazyResolve;
this.lazyLangs = this._appConfigStatic.resourceStore && this._appConfigStatic.resourceStore.lazyLangs;
this.lazyMojits = this._appConfigStatic.resourceStore && this._appConfigStatic.resourceStore.lazyMojits;
this.fire('loadConfigs');
},
/**
* 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 cacheKey = JSON.stringify(ctx),
cacheValue,
k,
parts,
p,
test,
found;
cacheValue = this._validateContextCache[cacheKey];
if (cacheValue) {
if (cacheValue === 'VALID') {
return;
}
throw new Error(cacheValue);
}
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]) {
this._validateContextCache[cacheKey] = 'INVALID dimension key "' + k + '"';
throw new Error(this._validateContextCache[cacheKey]);
}
// 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) {
this._validateContextCache[cacheKey] = 'INVALID dimension value "' + ctx[k] + '" for key "' + k + '"';
throw new Error(this._validateContextCache[cacheKey]);
}
continue;
}
if (!this._validDims[k][ctx[k]]) {
this._validateContextCache[cacheKey] = 'INVALID dimension value "' + ctx[k] + '" for key "' + k + '"';
throw new Error(this._validateContextCache[cacheKey]);
}
}
}
this._validateContextCache[cacheKey] = 'VALID';
return true;
},
/**
* Returns the context provided blended with the static
* (non-runtime-sensitive) context.
* @method blendStaticContext
* @param {object} ctx The context to blend.
* @return {object} the context
*/
blendStaticContext: function(ctx) {
return Y.mojito.util.blend(this._config.context, ctx);
},
/**
* 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,
key,
ycb;
ctx = this.blendStaticContext(ctx);
key = JSON.stringify(ctx || {});
if (this._appConfigCache[key]) {
return JSON.parse(this._appConfigCache[key]);
}
ycb = this._appConfigYCB.read(ctx);
appConfig = Y.mojito.util.blend(this._fwConfig.appConfigBase, ycb);
appConfig = Y.mojito.util.blend(appConfig, this._config.appConfig);
this._appConfigCache[key] = JSON.stringify(appConfig);
return appConfig;
},
/**
* Does initial preload of many parts of the application and framework.
* The full preload is done by preload().
*
* @method preloadInitial
* @return {nothing}
*/
preloadInitial: function() {
if (!this._initialPreloaded) {
this.preloadResourceVersions(true);
// binding the preload to avoid calling it twice.
this._initialPreloaded = true;
Y.log('Store initial preloaded', 'info', NAME);
}
},
/**
* 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.preloadInitial();
// And then use them.
if (this.loadAddons()) {
// If we loaded some addons, do another sweep so that the loaded addons can be used.
this.preloadResourceVersions();
// reload the configs so that the addons get a chance to mess with them
this.loadConfigs();
}
this.makeResourceVersions();
this.resolveResourceVersions();
this.preloaded = true;
Y.log('Store fully preloaded', 'info', NAME);
},
/**
* Optimizes this store for the specific runtime, or for "production" if none given in static context.
* @method optimizeForEnvironment
* @return {nothing}
*/
optimizeForEnvironment: function() {
this._packagesVisited = {};
if (!this.lazyResolve) {
this._appRVs = [];
this._mojitRVs = {};
}
},
/**
* 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 app resource versions.
* @method getAppResourceVersions
*/
getAppResourceVersions: function () {
return this._appRVs;
},
/**
* Returns a list of mojit resource versions.
* @param {string} mojit
* @method getMojitResourceVersions
*/
getMojitResourceVersions: function (mojit) {
return this._mojitRVs[mojit] || [];
},
/**
* 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;
},
/**
* 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.
*
* @async
* @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 spec,
typeDetails,
newInst,
key;
// TODO: should this be done here, or somewhere else?
ctx.runtime = env;
if (!instance.instanceId) {
instance.instanceId = Y.guid();
}
// spec
try {
spec = this._expandSpec(env, ctx, instance);
} catch (err) {
return cb(err);
}
if (!spec.config) {
spec.config = {};
}
if (!spec.type) {
return cb(new Error('Instance is missing a mojit type.'));
}
// type details
try {
typeDetails = this.getMojitTypeDetails(env, ctx, spec.type);
} catch (err2) {
return cb(err2);
}
// This approach gives a noticeable performance improvement when
// typeDetails.config is empty.
newInst = Y.mojito.util.copy(typeDetails);
for (key in spec) {
if (spec.hasOwnProperty(key)) {
if (('object' === typeof typeDetails[key]) && Object.keys(typeDetails[key]).length > 0) {
if (('object' === typeof spec[key]) && Object.keys(spec[key]).length > 0) {
newInst[key] = Y.mojito.util.blend(typeDetails[key], spec[key] || {});
}
} else {
newInst[key] = spec[key];
}
}
}
// spec doesn't (appreciably) contain base
newInst.base = instance.base;
cb(null, newInst);
},
/**
* 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.
*
* NOTE! This returns an object which is shared with similar calls to
* this method. If you intend to modify the object please make a deep
* copy first and use that instead.
*
* @method getMojitTypeDetails
* @param {string} env the runtime environment (either `client` or `server`)
* @param {object} ctx the context
* @param {string} mojitType mojit type
* @return {object} details about the mojit type
*/
getMojitTypeDetails: function(env, ctx, mojitType) {
var posl = this.selector.getPOSLFromContext(ctx),
// We need to include the lang, since it's a part of the context
// that greatly affects each mojit, yet is not necessarily
// captured in the POSL.
cacheKey = JSON.stringify([env, posl, ctx.lang, mojitType]),
cacheValue = this._getMojitTypeDetailsCache[cacheKey],
newModules = false,
closestLang,
dependency,
defaults,
definition,
i,
r,
ress,
mojitRes,
details;
if ('shared' === mojitType) {
throw new Error('Mojit name "shared" is special and isn\'t a real mojit.');
}
if (!cacheValue) {
// load the mojit if it hasn't been loaded already.
if (this._unloadedMojits[mojitType]) {
this._loadMojit(mojitType);
newModules = true;
// Update the posl in case the newly loaded mojit contains new dimensions.
posl = this.selector.getPOSLFromContext(ctx, true);
}
// if lazyLangs is on then determine the closestLang for this mojit and add the corresponding lang resources
// if they havent been added already.
if (this._unloadedLangs[mojitType]) {
closestLang = Y.mojito.util.findClosestLang(ctx.lang, this._unloadedLangs[mojitType]);
newModules = this._loadMojitLangs(mojitType, closestLang) || newModules;
}
mojitRes = this.getResourceVersions({type: 'mojit', name: mojitType, selector: '*'})[0];
if (!mojitRes) {
throw new Error('Cannot find the "' + mojitType + '" mojit. Make sure "' + mojitType + '" exists in the application.');
}
defaults = this.config.readConfigYCB(this._libs.path.join(mojitRes.source.fs.fullPath, 'defaults.json'), ctx);
definition = this.config.readConfigYCB(this._libs.path.join(mojitRes.source.fs.fullPath, 'definition.json'), ctx);
// If lazyMojits is on then we must load any mojit dependency for this mojit
// before getting this mojit's details. This ensures that all of the resources
// of the dependencies have been added so that this mojit's YUI modules can
// refer to any YUI module belonging to its dependencies.
// Note that circular dependencies are allowed and will not result in an infinite loop.
if (this.lazyMojits && defaults && Y.Lang.isArray(defaults.dependencies)) {
for (i = 0; i < defaults.dependencies.length; i++) {
dependency = defaults.dependencies[i];
if (this._unloadedMojits[dependency]) {
// Only get mojit details if the mojit hasn't been loaded;
// this prevents an infinite loop if there are circular dependencies.
this.getMojitTypeDetails(env, ctx, dependency);
}
}
}
// The newModules flag indicates that resolveVersion should not return any cached details.
details = this.resolveVersion(mojitType, env, posl, mojitRes, newModules);
details.defaults = defaults;
details.definition = definition;
if (details.defaults && details.defaults.config) {
details.config = Y.mojito.util.blend(details.defaults.config, details.config);
}
details.closestLang = closestLang || Y.mojito.util.findClosestLang(ctx.lang, details.langs);
// we shouldn't expose this
if ('client' === env) {
details.fullPath = undefined;
}
if (newModules) {
this._loadMojitModules(mojitType, details, env, ctx, posl);
} else if (this.lazyMojits && !this.yui._langLoaderCreated[ctx.lang]) {
// if no new modules were added but we still dont have a loader for the current lang,
// then we should update the loader.
this._updateLoaderCount++;
}
cacheValue = details;
this._getMojitTypeDetailsCache[cacheKey] = cacheValue;
}
return cacheValue;
},
/**
* Cooks down the list of resolved mojit resources into a single structure.
* @method resolveMojitDetails
* @param {string} env the runtime environment (either `client` or `server`)
* @param {object} posl priority-ordered seletor list
* @param {string} type name of mojit
* @param {array} ress array of resources for the mojit
* @param {object} mojitRes resource for the mojit itself
* @return {object} details for the mojit
*/
resolveMojitDetails: function(env, posl, type, ress, mojitRes) {
var r,
res,
clientDetails,
details = {
fullPath: mojitRes.source.fs.fullPath,
//defaults can only be evaluated at runtime
//definition can only be evaluated at runtime
assets: {},
binders: {},
config: {},
langs: {},
models: {},
partials: {},
views: {}
},
template,
specPath,
controller,
required = {},
addonName,
acAddonNames = {};
for (r in ress) {
res = ress[r];
if (res.type === 'config') {
// these can only be determined using the runtime context
continue;
}
if (res.type === 'asset') {
if (env === 'client') {
details.assets[res.name + res.source.fs.ext] = res.url;
} else {
details.assets[res.name + res.source.fs.ext] = res.source.fs.fullPath;
}
continue;
}
if (res.type === 'controller') {
details.controller = res.yui.name;
controller = this.yui._makeYUIModuleConfig(env, res);
continue;
}
if (res.type === 'yui-lang') {
details.langs[res.yui.lang] = true;
continue;
}
if (res.type === 'model') {
details.models[res.name] = res.yui.name;
continue;
}
if (res.type === 'binder') {
details.binders[res.name] = res.yui.name;
continue;
}
if (res.type === 'view') {
template = {
'content-path': (env === 'client' ?
this._libs.util.webpath(this._appConfigStatic.pathToRoot || '', res.url) :
res.source.fs.fullPath),
'content': res.content,
'engine': res.view.engine
};
// we want to separate partials from actual templates
// in case the engine supports partials
if (res.name.indexOf('partials/') === 0) {
// removing the "partials/" prefix
details.partials[this._libs.path.basename(res.name)] = template;
}
details.views[res.name] = template;
details.views[res.name].assets = res.view.assets;
details.views[res.name].engine = res.view.engine;
continue;
}
if (res.type === 'spec') {
// During runtime we only need to know a little bit about the specs.
specPath = res.mojit;
if (res.name !== 'default') {
specPath += ':' + res.name;
}
this._specPaths[specPath] = res.source.fs.fullPath;
continue;
}
if ('addon' === res.type && 'ac' === res.subtype) {
// HACK/TODO: we are assuming the name of the filename will be
// the same as the addon namespace. This is a bold assumption
// and we will do the right thing eventually.
acAddonNames[res.yui.name] = this._libs.path.basename(res.name);
}
}
// Since the binders are not part of the server runtime, but are needed
// to define the binders map, we need to synthetically build this.
if (env !== 'client') {
clientDetails = this.resolveVersion(type, 'client', posl, mojitRes);
details.binders = clientDetails.binders;
}
this.fire('resolveMojitDetails', {
args: {
env: env,
posl: posl,
type: type,
ress: ress,
mojitRes: mojitRes
},
acAddonNames: acAddonNames,
mojitDetails: details
});
return details;
},
/**
* NOTE:
* 1. Routes are no longer contextualized on the server runtime.
* 2. Routes are still contextualized on the client runtime, will need
* to revisit.
*
* TODO: remove contextualized routes on client runtime
*
* Returns the routes configured in the application.
* @method getRoutes
* @return {object} routes
*/
getRoutes: function() {
var ctx = {}, // static app config will be merged in automatically
appConfig,
routesFiles = [],
p,
path,
pathRoutes,
routes = {};
if (this._routesCache) {
return JSON.parse(this._routesCache);
}
appConfig = this.getAppConfig(ctx);
if (appConfig) {
routesFiles = appConfig.routesFiles;
}
for (p = 0; p < routesFiles.length; p += 1) {
path = routesFiles[p];
// relative paths are relative to the application
path = this._libs.path.resolve(this._config.root, path);
pathRoutes = this.config.readConfigYCB(path, ctx);
Y.mix(routes, pathRoutes, true);
}
if (!Object.keys(routes).length) {
Y.mix(routes, this._fwConfig.defaultRoutes, true);
}
this._routesCache = JSON.stringify(routes);
return routes;
},
/**
* 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.getMojitResourceVersions(mojit);
for (r = 0; r < ress.length; r += 1) {
res = ress[r];
if (res.url) {
urls[res.url] = res.source.fs.fullPath;
}
}
}
return urls;
},
/**
* turns a resource into a datastructure for the static handler
* @method makeStaticHandlerDetails
* @param {object} res The resource.
* @return {object} A concise version of the resource containing just the details necessary
* to serve the resource via the static handler.
*/
makeStaticHandlerDetails: function(res) {
var details = {
type: res.type,
subtype: res.subtype,
name: res.name,
path: res.source.fs.fullPath,
url: res.url
};
if (res.mojit) {
details.mojit = res.mojit;
}
if (res.mime) {
details.mimetype = res.mime.type;
details.charset = res.mime.charset;
}
return details;
},
/**
* Sugar method that returns a hash table with the urls and the
* resource objects.
* @method getAllURLDetails
* @return {object} for all resources with a "url" metadatum, the key is
* that URL and the value is the results of the makeStaticHandlerDetails() call
*/
getAllURLDetails: function() {
var m,
mojit,
mojits,
urls = {};
this.getURLDetails(this.getAppResourceVersions(), urls);
mojits = this.listAllMojits();
mojits.push('shared');
for (m = 0; m < mojits.length; m += 1) {
mojit = mojits[m];
this.getURLDetails(this.getMojitResourceVersions(mojit), urls);
}
return urls;
},
getURLDetails: function (ress, urls) {
var r,
res;
for (r = 0; r < ress.length; r += 1) {
res = ress[r];
if (res.url && res.source.fs.isFile) {
if (urls[res.url]) {
if (urls[res.url].path !== res.source.fs.fullPath) {
Y.log('Url collision for ' + res.url +
'. Choosing:\n' + urls[res.url].path +
' over\n' + res.source.fs.fullPath, 'debug', NAME);
}
} else {
urls[res.url] = this.makeStaticHandlerDetails(res);
}
}
}
},
/**
* Finds the file represented by the resource, and returns its contents and filesystem info.
* @method getResourceContent
* @param {object} details static handling details
* @param {function} callback callback used to return the resource content (or error)
* @param {Error|undefined} callback.err Error that occurred, if any.
* If an error is given that the other two arguments will be undefined.
* @param {Buffer} callback.content the contents of the resource
* @param {Stat||null} callback.stat Stat object with details about the file on the filesystem
* Can be null if the resource doesn't have a direct representation on the filesystem.
* @return {undefined} nothing is returned, the results are returned via the callback
*/
getResourceContent: function(details, callback) {
var store = this,
filename;
if (details && details.path) {
filename = details.path;
// FUTURE [Issue 89] stat cache?
store._libs.fs.stat(filename, function(err, stat) {
if (err) {
Y.log('failed to find: ' + filename, 'warn', NAME);
// TODO: [Issue 90] send next an error?
callback(err);
return;
}
// reading the file directly using buffers
store._libs.fs.readFile(filename, function (err, content) {
if (err) {
Y.log('NOT FOUND: ' + filename, 'warn', NAME);
callback(err);
return;
}
store.processResourceContent(details, content, stat, callback);
});
});
} else {
// invalid res object
callback(new Error('Invalid resource store reference'));
}
},
/**
* A method that transforms the content of a resource as it's being read
* from the filesystem. This method does nothing, but provides a hook
* point for resource store addons to transform resource contents.
* @method processResourceContent
* @param {object} details static handling details
* @param {Buffer} content the contents of the resource
* @param {Stat||null} callback.stat Stat object with details about the file on the filesystem
* Can be null if the resource doesn't have a direct representation on the filesystem.
* @param {function} callback callback passed to `getResourceContent()`.
* @return {nothing} results returned via the callback
*/
processResourceContent: function(details, content, stat, callback) {
callback(undefined, content, stat);
},
/**
* 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;
},
/**
* Returns information about the application's NPM package. This is
* primarily useful when creating resources in `makeResourceVersions()`.
* @method getAppPkgMeta
* @return {object} metadata about the application's NPM package
*/
getAppPkgMeta: function() {
return this._appPkg;
},
/**
* Creates the filesystem metadata for a resource. This is primarily
* useful when creating resources in `makeResourceVersions()`.
* @method makeResourceFSMeta
* @param {string} dir directory path
* @param {string} dirType type represented by the "dir" argument. values are "app", "bundle", "pkg", or "mojit"
* @param {string} subdir directory path within "dir".
* @param {string} file name of the file or directory
* @param {boolean} isFile indicates whether the path is a file (true) or diretory (false)
* @return {object} filesystem metadata
*/
makeResourceFSMeta: function(dir, dirType, subdir, file, isFile) {
var fs = {
fullPath: this._libs.path.join(dir, subdir, file),
rootDir: dir,
rootType: dirType,
subDir: subdir,
subDirArray: subdir.split(PATH_SEP),
isFile: isFile,
ext: this._libs.path.extname(file)
};
fs.basename = this._libs.path.basename(file, fs.ext);
return fs;
},
//====================================================================
// 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 {Number} the number of loaded addons
*/
loadAddons: function() {
var modules = {},
ress,
r,
res;
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) {
// skipping any rs addon that was already plugged during the init phase (e.g.: config)
if (fn.NS && !this[fn.NAME]) {
this.plug(fn, { appRoot: this._config.root, mojitoRoot: this._config.mojitoRoot });
}
}, this);
return Object.keys(modules).length;
},
/**
* 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(initialPreload) {
var me = this,
walker,
walkedMojito = false,
dir,
info;
this.selectors = {};
this._appRVs = [];
this._mojitRVs = {};
this._packagesVisited = {};
this.YUI = null;
this.Y = null;
this._createRuntimeYUIInstance();
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;
}
if (info.depth !== 0 && (!info.pkg.yahoo || !info.pkg.yahoo.mojito)) {
return false;
}
me._preloadPackage(info, initialPreload);
});
// 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, initialPreload);
}
},
/**
* 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