mojito
Version:
Mojito provides an architecture, components and tools for developers to build complex web applications faster.
379 lines (336 loc) • 14.4 kB
JavaScript
/*
* 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, stupid:true, node:true */
/*global YUI*/
/**
* The <strong>Resource Store</strong> 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) {
'use strict';
var libfs = require('fs'),
libpath = require('path'),
existsSync = libfs.existsSync || libpath.existsSync,
libycb = require('ycb'),
libyaml = require('js-yaml');
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._simpleCache = {}; // fullPath: contents as JSON object
this._ycbCache = {}; // fullPath: context: YCB config object
this._ycbDims = this._readYcbDimensions();
this._ycbAppConfig = this._readYcbAppConfig();
},
/**
* Returns the YCB dimensions for the application.
* @method getDimensions
* @return {object} the YCB dimensions structure for the app
*/
getDimensions: function() {
return Y.mojito.util.copy(this._ycbDims);
},
/**
* Returns the YCB library object for the application config.
* @method getAppConfigYCB
* @return {YCB} YCB library object for the application config
*/
getAppConfigYCB: function() {
return this._ycbAppConfig;
},
/**
* Reads a JSON file. In mojito, this should generally only be used for
* package.json files, and all other mojito config files should instead
* be read using readConfigSimple() or readConfigYCB().
* @method readConfigJSON
* @param {string} fullPath path to JSON file
* @return {user-defined} contents of file as an object
*/
readConfigJSON: function(fullPath) {
var json,
contents;
if (!existsSync(fullPath)) {
return {};
}
json = this._jsonCache[fullPath];
if (!json) {
try {
contents = libfs.readFileSync(fullPath, 'utf-8');
json = JSON.parse(contents);
} catch (e) {
throw new Error('Error parsing JSON file: ' + fullPath);
}
this._jsonCache[fullPath] = json;
}
return Y.mojito.util.copy(json);
},
/**
* Reads and parses a JSON or YAML structured file.
* @method readConfigSimple
* @param {string} fullPath path to JSON or YAML file
* @return {user-defined} contents of file as an object
*/
readConfigSimple: function(fullPath) {
var extensions = ['.yml', '.yaml', '.json'],
basename, // everything except the extension
i,
json = false,
raw,
obj;
obj = this._simpleCache[fullPath];
if (!obj) {
basename = fullPath;
if (libpath.extname(fullPath)) {
basename = fullPath.slice(0, libpath.extname(fullPath).length * -1);
}
for (i = extensions.length - 1; i >= 0; i -= 1) {
try {
fullPath = basename + extensions[i];
raw = libfs.readFileSync(fullPath, 'utf8');
try {
if (i === 2) { // json
obj = JSON.parse(raw);
json = true;
} else { // yaml or yml
obj = libyaml.load(raw);
if (json) {
Y.log(basename + extensions[2] + ' exists but ' + extensions[i] + ' file will be used instead', 'warn', NAME);
}
}
// TODO: what happen when one of them exists?
// and what then more than one exists?
} catch (parseErr) {
throw new Error(parseErr);
}
} catch (err) {
if (err.errno !== 34) { // if the error was not "no such file or directory" report it
throw new Error("Error parsing file: " + fullPath + "\n" + err);
}
}
}
if (!obj) {
obj = {};
}
this._simpleCache[fullPath] = obj;
}
return Y.mojito.util.copy(obj);
},
/**
* 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.blendStaticContext(ctx);
if (!this._ycbCache[fullPath]) {
this._ycbCache[fullPath] = {};
}
cacheKey = JSON.stringify(ctx);
ycb = this._ycbCache[fullPath][cacheKey];
if (!ycb) {
json = this.readConfigSimple(fullPath);
json = this._ycbDims.concat(json);
ycb = libycb.read(json, ctx);
this._ycbCache[fullPath][cacheKey] = ycb;
}
return Y.mojito.util.copy(ycb);
},
/**
* Creates a YCB configuration bundle using contents from multiple files.
* The appropriate dimensions.json file will be mixed in, and doesn't need
* to be part of the list of files. This method is tolerant to errors, and
* will fallback to `{}` if a file does not exists or fails to load.
* @method createMultipartYCB
* @param {array} paths list of files to load
* @return {YCB} return a YCB library object
*/
createMultipartYCB: function(paths) {
var p,
path,
config,
s,
section,
settings = {},
bundle = [];
bundle.push(this.getDimensions()[0]);
for (p = 0; p < paths.length; p += 1) {
path = paths[p];
config = this.readConfigSimple(path);
if (!Y.Lang.isArray(config)) {
Y.log('not a YCB file: ' + path, 'error', NAME);
return;
}
for (s = 0; s < config.length; s += 1) {
section = config[s];
if (!Y.Lang.isArray(section.settings)) {
Y.log('missing "settings" in YCB file: ' + path, 'error', NAME);
return;
}
section.__ycb_source__ = path;
bundle.push(section);
}
}
return new libycb.Ycb(bundle);
},
/**
* 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 or yaml files
if ('.json' !== fs.ext && '.yaml' !== fs.ext && '.yml' !== 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 (!existsSync(path)) {
path = libpath.join(this.mojitoRoot, 'dimensions.json');
}
return this.readConfigSimple(path);
},
/**
* Initializes the special multi-file YCB library for all the application
* files. By default, we try to load `application.json`, then mix any other
* relative config file specified in the master section under the
* `applicationConfigFiles` array, which is optional.
* @private
* @method _readYcbAppConfig
* @return {object} libycb object
*/
_readYcbAppConfig: function() {
var ycb,
i,
rootAppJSON = libpath.join(this.appRoot, 'application.json'),
paths = [],
relativePaths;
// since application.json is optional, we should be careful
// in any case there is a low-level cache mechanism going on here.
if (Y.Lang.isArray(this.readConfigSimple(rootAppJSON))) {
// ensuring the context runtime:server to read applicationConfigFiles,
// it does not matter if runtime is not a dimension, it works just fine
// since the purpose of this block is to read applicationConfigFiles
// from /application.json, and the rest is part of this.createMultipartYCB
ycb = this.readConfigYCB(rootAppJSON, {
runtime: 'server'
});
// adding the master application.json as the top level
paths.push(rootAppJSON);
// optional applicationConfigFiles to mix in more configs
relativePaths = ycb.applicationConfigFiles || [];
for (i = 0; i < relativePaths.length; i += 1) {
paths.push(libpath.resolve(this.appRoot, relativePaths[i]));
}
}
// Note: it doesn't matter if we try to reach the same file multiple
// times (application.json) because readConfigSimple will cache it anyway
ycb = this.createMultipartYCB(paths);
if (!ycb) {
throw new Error("failed to create a YCB config from the following files:\n " + paths.join("\n "));
}
return ycb;
}
});
Y.namespace('mojito.addons.rs');
Y.mojito.addons.rs.config = RSAddonConfig;
}, '0.0.1', { requires: ['plugin', 'oop', 'mojito-util']});