UNPKG

mojito

Version:

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

1,230 lines (1,053 loc) 47.3 kB
/* * 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, nomen:true, stupid:true, continue:true, node:true*/ /*global YUI*/ /** * @module ResourceStoreAddon */ /** * @class RSAddonYUI * @extension ResourceStore.server */ YUI.add('addon-rs-yui', function(Y, NAME) { 'use strict'; var libfs = require('fs'), libpath = require('path'), libvm = require('vm'), libmime = require('mime'), liburl = require('url'), libutil = require('../../../util.js'), Module = require('module'), serialize = require('express-state/lib/serialize'), WARN_SERVER_MODULES = /\b(dom-[\w\-]+|node-[\w\-]+|io-upload-iframe)/ig, MODULE_SUBDIRS = { autoload: true, tests: true, yui_modules: true }, resourceSortByDepthTest = function (a, b) { return a.source.pkg.depth - b.source.pkg.depth; }, yuiSandboxFactory = require(libpath.join(__dirname, '..', '..', '..', 'yui-sandbox.js')), syntheticStat = null, MODULE_META_ENTRIES = ['path', 'requires', 'use', 'optional', 'skinnable', 'after', 'condition', 'lang', 'langPack', 'test', 'templates', 'langBundles', 'optionalRequires'], REGEX_LANG_TOKEN = /\"\{langToken\}\"/g, REGEX_LANG_PATH = /\{langPath\}/g, REGEX_LOCALE = /\_([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)$/, MODULE_PER_LANG = ['loader-app-base'], MODULE_TEMPLATES = { /* * This is a replacement of the original loader to include loader-app * module, which represents the meta of the app. */ 'loader-app': 'YUI.add("loader",function(Y){' + '},"",{requires:["loader-base","loader-yui3","loader-app"]});', /* * Use this module when you want to rely on the loader to do recursive * computations to resolve combo urls for app yui modules in the client * runtime. * Note: This is the default config used by YUI. */ 'loader-app-base': 'YUI.add("loader-app",function(Y){' + 'Y.applyConfig({groups:{app:Y.merge(' + '((Y.config.groups&&Y.config.groups.app)||{}),' + '{modules:{app-base}}' + ')}});' + '},"",{requires:["loader-base"]});' }; function RSAddonYUI() { RSAddonYUI.superclass.constructor.apply(this, arguments); } RSAddonYUI.NS = 'yui'; Y.extend(RSAddonYUI, Y.Plugin.Base, { /** * This methods is part of Y.Plugin.Base. See documentation for that for details. * @method initializer * @param {object} config Configuration object as per Y.Plugin.Base * @return {nothing} */ initializer: function(config) { this.host = config.host; this.appRoot = config.appRoot; this.mojitoRoot = config.mojitoRoot; // for all synthetic files, since we don't have an actual file, we need to // create a stat object, in this case we use the mojito folder stat as // a replacement. We make it syncronous since it is meant to be executed // once during the preload process. syntheticStat = libfs.statSync(libpath.join(__dirname, '../../../..')); this.afterHostMethod('preloadResourceVersions', this.preloadResourceVersions, this); this.afterHostMethod('findResourceVersionByConvention', this.findResourceVersionByConvention, this); this.beforeHostMethod('parseResourceVersion', this.parseResourceVersion, this); this.beforeHostMethod('addResourceVersion', this.addResourceVersion, this); this.beforeHostMethod('makeResourceVersions', this.makeResourceVersions, this); this.afterHostMethod('resolveResourceVersions', this.resolveResourceVersions, this); this.beforeHostMethod('getResourceContent', this.getResourceContent, this); this.onHostEvent('loadConfigs', this.loadConfigs, this); this.loadConfigs(); this.langs = {}; // keys are list of languages in the app, values are simply "true" this.resContents = {}; // res.id: contents this.appModulesDetails = {}; // res.yui.name: static handler details this.yuiModulesDetails = {}; // res.yui.name: static handler details this._langLoaderCreated = {}; // lang mapping indicating whether a lang specific loader has been created. }, preloadResourceVersions: function () { this._langLoaderCreated = {}; }, loadConfigs: function () { this.staticAppConfig = this.host.getStaticAppConfig() || {}; this.staticHandling = this.staticAppConfig.staticHandling || {}; this.staticPrefix = libutil.webpath('/', (this.staticHandling.prefix || 'static'), "/"); this.yuiConfig = (this.staticAppConfig.yui && this.staticAppConfig.yui.config) || {}; }, /** * Returns a datastructure which tells a YUI instance where to find * the YUI modules that are shared among all mojits. * @method getConfigShared * @param {string} env runtime environment (either `client`, or `server`) * @return {object} datastructure for configuring YUI */ getConfigShared: function(env) { var r, res, ress, modules = {}; ress = this.get('host').getMojitResourceVersions('shared'); for (r = 0; r < ress.length; r += 1) { res = ress[r]; if (!res.yui || !res.yui.name) { continue; } if (res.affinity.affinity !== env && res.affinity.affinity !== 'common') { continue; } modules[res.yui.name] = this._makeYUIModuleConfig(env, res); } return { modules: modules }; }, /** * Returns a datastructure which tells a YUI instance where to find * the YUI modules in the app. * @method getModulesConfig * @param {string} env runtime environment (either `client`, or `server`) * @param {boolean} justApp Indicates whether to include the YUI * modules just found in the application (true), or also include * those found in mojito (false). * @return {object} datastructure for configuring YUI */ getModulesConfig: function(env, justApp, lang) { var store = this.get('host'), m, mojit, mojits, modules = {}; mojits = store.listAllMojits(); mojits.push('shared'); for (m = 0; m < mojits.length; m += 1) { mojit = mojits[m]; this.getMojitModulesConfig(mojit, modules, env, justApp, lang); } return { modules: modules }; }, getMojitModulesConfig: function (mojit, modules, env, justApp, lang) { var store = this.get('host'), r, res, ress; ress = store.getMojitResourceVersions(mojit); ress.sort(resourceSortByDepthTest); for (r = 0; r < ress.length; r += 1) { res = ress[r]; if (!res.yui || !res.yui.name) { continue; } if (res.affinity.affinity !== env && res.affinity.affinity !== 'common') { continue; } if (justApp && ('mojito' === res.source.pkg.name)) { continue; } if (lang && res.type === 'yui-lang' && res.yui.lang !== lang && res.yui.lang !== '') { continue; } // don't overwrite resource if it's there already modules[res.yui.name] = modules[res.yui.name] || this._makeYUIModuleConfig(env, res); } }, /** * Hook to allow other RS addons to control the yui * configuration. By default, the `yui.config` will * allow customization of the combo handler when needed * from `application.json`. * @method getYUIConfig * @param {object} ctx the context * @return {object} yui configuration */ getYUIConfig: function(ctx) { var version = Y.version, yuiPrefix = libutil.webpath(this.staticPrefix, 'yui/'), appConfig = this.get('host').getAppConfig(ctx), yuiConfig; if (this.staticHandling.serveYUIFromAppOrigin) { // by default, we want to serve YUI from CDN yuiConfig = { maxURLLength: 1024, base: yuiPrefix, comboBase: "/combo~", comboSep: "~", root: yuiPrefix }; } else { yuiConfig = { // the base path for non-combo paths base: 'http://yui.yahooapis.com/' + version + '/', // the path to the combo service comboBase: 'http://yui.yahooapis.com/combo?', comboSep: '&', // a fragment to prepend to the path attribute when // when building combo urls root: version + '/' }; } yuiConfig = Y.merge(yuiConfig, { fetchCSS: true, combine: true }, (appConfig.yui && appConfig.yui.config) || {}); // to boot the app in the client with the proper lang yuiConfig.lang = ctx.lang; yuiConfig.groups = yuiConfig.groups || {}; yuiConfig.groups.app = this.getAppGroupConfig(ctx, yuiConfig); yuiConfig.seed = this.getAppSeedFiles(ctx, yuiConfig); return yuiConfig; }, /** * Hook to allow other RS addons to control the combo * handler configuration for group "app". By default, * the `yui.config.groups.app` will allow customization * of the combo handler when needed from `application.json` * @method getAppGroupConfig * @param {object} ctx the context * @return {object} yui configuration for group "app" */ getAppGroupConfig: function(ctx) { var appConfig = this.get('host').getAppConfig(ctx), yuiConfig = (appConfig.yui && appConfig.yui.config) || {}; return Y.merge({ combine: (yuiConfig.combine === false) ? false : true, maxURLLength: 1024, base: this.staticPrefix, comboBase: "/combo~", comboSep: "~", root: this.staticPrefix }, ((yuiConfig.groups && yuiConfig.groups.app) || {})); }, /** * Produce the YUI seed files. This can be controlled through * application.json->yui->config->seed in a form of * a array with the list of full paths for all seed files. * @method getAppSeedFiles * @param {object} ctx the context * @param {object} yuiConfig the config that is sent to client * @return {array} list of seed files */ getAppSeedFiles: function(ctx, yuiConfig) { yuiConfig = yuiConfig || {}; // to support legacy var files = [], seed = Y.Array(yuiConfig.seed || []), appGroupConfig = (yuiConfig.groups && yuiConfig.groups.app) || {}, hash = {}, appModules = [], yuiModules = [], filter = yuiConfig.filter || 'min', file, lang, i; // picking up the closest language based on yui config // of from the yui bundles lang = Y.mojito.util.findClosestLang(ctx.lang, this.langs); function newEntry(f) { // flushing any pending combo for yui core modules if (yuiModules.length > 0) { files.push(yuiConfig.comboBase + yuiModules.join(yuiConfig.comboSep)); yuiModules = []; } // flushing any pending combo for app modules if (appModules.length > 0) { files.push(appGroupConfig.comboBase + appModules.join(appGroupConfig.comboSep)); appModules = []; } if (f) { files.push(f); } } // adjusting filter to be url friendly filter = filter === 'raw' ? '' : '-' + filter; // adjusting lang just to be url friendly lang = lang ? '_' + lang : ''; // The seed files collection is lang aware, hence we should adjust // is on runtime. for (i = 0; i < seed.length; i += 1) { // adjusting the seed based on {langToken} to facilitate // the customization of the seed file url per lang. seed[i] = seed[i].replace(REGEX_LANG_PATH, lang); if (hash.hasOwnProperty(seed[i])) { Y.log('Skiping duplicated entry in yui.config.seed: ' + seed[i], 'warn', NAME); } else if (liburl.parse(seed[i]).protocol) { newEntry(seed[i]); } else if (this.appModulesDetails.hasOwnProperty(seed[i])) { // app module file = this.appModulesDetails[seed[i]].url.split('/').pop(); // default app module if (appGroupConfig.combine === false) { // if the combo is disabled, then we need to insert one by one // this is useful for offline and hybrid apps where the combo // does not work. newEntry(appGroupConfig.base + file); } else { // the item is a module and should be combined appModules.push(appGroupConfig.root + file); } } else { // assume yui core module file = seed[i] + '/' + seed[i] + filter + '.js'; // the module is a yui core module, treat is accordingly if (yuiConfig.combine === false) { // if the combo is disabled, then we need to insert one by one // this is useful for offline and hybrid apps where the combo // does not work. newEntry(yuiConfig.base + file); } else { // the item is a module and should be combined yuiModules.push(yuiConfig.root + file); } } // hash table to avoid duplicated entries in the seed hash[seed[i]] = true; } newEntry(); // just to flush any remaining entry return files; }, /** * Aggregate all yui core files * using the path of as the hash. * * @private * @method getYUIURLDetails * @return {object} yui core resources by url */ getYUIURLDetails: function () { var name, urls = {}; for (name in this.yuiModulesDetails) { if (this.yuiModulesDetails.hasOwnProperty(name)) { urls[this.yuiModulesDetails[name].url] = this.yuiModulesDetails[name]; } } return urls; }, /** * Using AOP, this is called after the ResourceStore's version. * @method findResourceVersionByConvention * @param {object} source metadata about where the resource is located * @param {string} mojitType name of mojit to which the resource likely belongs * @return {object||null} for yui modules or lang bundles, returns metadata signifying that */ findResourceVersionByConvention: function(source, mojitType) { var fs = source.fs; if (!fs.isFile) { return; } if ('.js' !== fs.ext) { return; } if (fs.subDirArray.length >= 1 && MODULE_SUBDIRS[fs.subDirArray[0]]) { return new Y.Do.AlterReturn(null, { type: 'yui-module', skipSubdirParts: 1 }); } if (fs.subDirArray.length >= 1 && 'lang' === fs.subDirArray[0]) { return new Y.Do.AlterReturn(null, { type: 'yui-lang', skipSubdirParts: 1 }); } }, /** * Using AOP, this is called before the ResourceStore's version. * @method parseResourceVersion * @param {object} source metadata about where the resource is located * @param {string} type type of the resource * @param {string} subtype subtype of the resource * @param {string} mojitType name of mojit to which the resource likely belongs * @return {object||null} for yui modules or lang bundles, returns the resource metadata */ parseResourceVersion: function(source, type, subtype, mojitType) { var store = this.get('host'), fs = source.fs, baseParts, res, sandbox, m; // If lazyLangs is on then process yui lang files just // by reading the filename, instead of executing them. if ('yui-lang' === type && store.lazyLangs) { res = { source: source, mojit: mojitType, type: 'yui-lang', affinity: 'common', selector: '*' }; if (!res.yui) { res.yui = {}; } if (source.fs.basename.indexOf(mojitType) === 0) { m = source.fs.basename.substring(mojitType.length).match(REGEX_LOCALE); res.yui.lang = (m && m[1]) || ''; res.yui.name = 'lang/' + mojitType + (res.yui.lang ? '_' + res.yui.lang : ''); res.name = res.yui.name; res.id = [res.type, res.subtype, res.name].join('-'); if (!store.lazyLangs) { this.langs[res.yui.lang] = true; } } else { Y.log('Unexpected lang filename "' + source.fs.basename + '". The filename should start with "' + mojitType + '"', 'warn'); } return new Y.Do.Halt(null, res); } if ('yui-lang' === type) { res = { source: source, mojit: mojitType, type: 'yui-lang', affinity: 'common', selector: '*' }; if (!res.yui) { res.yui = {}; } sandbox = { Intl: { add: function(langFor, lang) { res.yui.langFor = langFor; res.yui.lang = lang; } } }; this._captureYUIModuleDetails(res, sandbox); if (!res.yui) { // This resource is not a valid YUI module and should not be added. return new Y.Do.Halt(); } res.name = res.yui.name; res.id = [res.type, res.subtype, res.name].join('-'); this.langs[res.yui.lang] = true; if (res.yui.name === 'lang/' + res.yui.langFor) { res.yui.isRootLang = true; } return new Y.Do.Halt(null, res); } if ('yui-module' === type) { baseParts = fs.basename.split('.'); res = { source: source, mojit: mojitType, type: 'yui-module', affinity: 'server', selector: '*' }; if (baseParts.length >= 3) { res.selector = baseParts.pop(); } if (baseParts.length >= 2) { res.affinity = baseParts.pop(); } if (baseParts.length !== 1) { Y.log('invalid yui-module filename. skipping ' + fs.fullPath, 'warn', NAME); return; } this._captureYUIModuleDetails(res); if (!res.yui) { // This resource is not a valid YUI module and should not be added. return new Y.Do.Halt(); } res.name = res.yui.name; res.id = [res.type, res.subtype, res.name].join('-'); return new Y.Do.Halt(null, res); } }, /** * Using AOP, this is called before the ResourceStore's version. * If the resource is a YUI module, augments the metadata with details * about the YUI module. * @method addResourceVersion * @param {object} res resource version metadata * @return {nothing} */ addResourceVersion: function(res) { if ('.js' !== res.source.fs.ext) { return; } if (res.yui && res.yui.name) { // work done already return; } // ASSUMPTION: no app-level resources are YUI modules if (!res.mojit) { return; } if ('asset' === res.type) { return; } var store = this.get('host'); this._captureYUIModuleDetails(res); if (!res.yui) { // Do not add YUI resources that failed while capturing details. return new Y.Do.Halt(); } }, /** * Using AOP, this is called before the ResourceStore's version. * We register some fake resource versions that represent the YUI * configurations. * @method addResourceVersion * @param {object} res resource version metadata * @return {nothing} */ makeResourceVersions: function() { var store = this.get('host'), res, l, langs = Object.keys(this.langs); // we always want to make the no-lang version if (!this.langs['']) { langs.push(''); } res = { source: {}, mojit: 'shared', type: 'yui-module', subtype: 'synthetic', name: 'loader-app', affinity: 'client', selector: '*', yui: { name: 'loader-app' } }; res.id = [res.type, res.subtype, res.name].join('-'); res.source.pkg = store.getAppPkgMeta(); res.source.fs = store.makeResourceFSMeta(this.appRoot, 'app', '.', 'loader-app.js', true); store.addResourceVersion(res); for (l = 0; l < langs.length; l += 1) { this._makeLoaderResourceVersion(langs[l]); } // we can also make some fake resources for all yui // modules that we might want to serve. this._precalcYUIResources(); }, _makeLoaderResourceVersion: function (lang) { if (this._langLoaderCreated[lang]) { return []; } var store = this.get('host'), i, name, res, ress = [], langExt = lang ? '_' + lang : ''; for (i = 0; i < MODULE_PER_LANG.length; i += 1) { name = MODULE_PER_LANG[i]; res = { source: {}, mojit: 'shared', type: 'yui-module', subtype: 'synthetic', name: [name, lang].join('-'), affinity: 'client', selector: '*', yui: { name: name + langExt } }; res.id = [res.type, res.subtype, res.name].join('-'); res.source.pkg = store.getAppPkgMeta(); res.source.fs = store.makeResourceFSMeta(this.appRoot, 'app', '.', name + langExt + '.js', true); store.addResourceVersion(res); ress.push(res); } this._langLoaderCreated[lang] = true; this.langs[lang] = true; return ress; }, /** * Using AOP, this is called after the ResourceStore's version. * We precompute the YUI configurations. * @method resolveResourceVersions * @return {nothing} */ resolveResourceVersions: function() { var me = this, store = this.get('host'), m, mojit, mojits; me._processResources(store.getAppResourceVersions()); mojits = store.listAllMojits(); mojits.push('shared'); for (m = 0; m < mojits.length; m += 1) { mojit = mojits[m]; me._processResources(store.getMojitResourceVersions(mojit)); } this._precalcLoaderMeta(); }, _processResources: function (ress) { var r, store = this.get('host'), res; for (r = 0; r < ress.length; r += 1) { res = ress[r]; if ('client' !== res.affinity.affinity) { // appModulesDetails is used by getAppSeedFiles, which only matters for the client continue; } if (!res.yui || !res.yui.name) { continue; } if (this.appModulesDetails[res.yui.name]) { if (this.appModulesDetails[res.yui.name].path !== res.source.fs.fullPath) { Y.log('YUI module collision for name=' + res.yui.name + '. Choosing:\n' + this.appModulesDetails[res.yui.name].path + ' over\n' + res.source.fs.fullPath, 'debug', NAME); } } else { this.appModulesDetails[res.yui.name] = store.makeStaticHandlerDetails(res); } } }, /** * Return the content for resources we make in makeResourceVersions(). * * @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(res, callback) { var contents = res.name && this.resContents[res.name]; if (contents) { callback(null, new Buffer(contents, 'utf8'), syntheticStat); return new Y.Do.Halt(null, null); } }, /** * Precomputes YUI modules resources, so that we don't have to at runtime. * @private * @method _precalcYUIResources * @return {nothing} */ _precalcYUIResources: function() { var store = this.get('host'), name, modules, mimetype, charset, fullpath, Ysandbox; if (!this.staticHandling.serveYUIFromAppOrigin) { // this should helps with the memory consumption // by avoiding serving YUI Core modules, and instead // getting those modules from CDN. return; } Ysandbox = yuiSandboxFactory .getYUI(this.yuiConfig.filter)(Y.merge(this.yuiConfig)); // used to find the the modules in YUI itself Ysandbox.use('loader'); modules = (new Ysandbox.Loader(Ysandbox.config)).moduleInfo || {}; for (name in modules) { if (modules.hasOwnProperty(name)) { // faking a RS object for the sake of simplicity fullpath = libpath.join(__dirname, '../../../../node_modules/yui', modules[name].path); mimetype = libmime.lookup(fullpath); charset = libmime.charsets.lookup(mimetype); modules[name] = store.makeStaticHandlerDetails({ type: 'yui-module', name: name, url: libutil.webpath(this.staticPrefix, 'yui', modules[name].path), path: modules[name].path, source: { fs: { fullPath: fullpath } }, mime: { type: mimetype, charset: charset } }); } } this.yuiModulesDetails = modules; }, /** * Precomputes YUI loader metadata, so that we don't have to at runtime. * @private * @method _precalcLoaderMeta * @param {array} langs array of languages for which to compute YUI loader metadata * @return {nothing} */ _precalcLoaderMeta: function(lang) { var store = this.get('host'), langs, Ysandbox, modules_config, Ysanbdox, loader, resolved, appMetaData = { base: {} }, modules = {}, // regular meta (a la loader-yui3) name, i, l; if (lang) { langs = ['', lang]; } else { langs = Object.keys(this.langs); // we always want to make the no-lang version if (!this.langs['']) { langs.push(''); } } Ysandbox = yuiSandboxFactory .getYUI(this.yuiConfig.filter)(Y.merge(this.yuiConfig)); modules_config = this.getModulesConfig('client', false, lang).modules; Ysandbox.applyConfig({ modules: Ysandbox.merge({}, modules_config), useSync: true }); Ysandbox.use('loader'); // using the loader at the server side to compute the loader metadata // to avoid loading the whole thing on demand. loader = new Ysandbox.Loader(Ysandbox.merge(Ysandbox.config, { require: Ysandbox.Object.keys(modules_config) })); resolved = loader.resolve(true); // we need to copy, otherwise the datastructures that Y.loader holds // onto get mixed with our changes, and Y.loader gets confused resolved = Y.mojito.util.copy(resolved); this._processMeta(resolved.jsMods, modules, modules_config); this._processMeta(resolved.cssMods, modules, modules_config); for (i = 0; i < langs.length; i += 1) { lang = langs[i] || '*'; appMetaData.base[lang] = {}; for (name in modules) { if (modules.hasOwnProperty(name)) { if (modules[name].owner && !modules[modules[name].owner]) { // if there is not a module corresponding with the lang pack // that means the controller doesn't have client affinity, // in that case, we don't need to ship it. continue; } if ((lang === '*') || (modules[name].langPack === '*') || (!modules[name].langPack) || (lang === modules[name].langPack)) { // we want to separate modules into different buckets // to be able to support groups in loader config if (modules_config[name]) { appMetaData.base[lang][name] = modules[name]; } } } } appMetaData.base[lang] = serialize(appMetaData.base[lang]); } // for each lang this.resContents['loader-app'] = MODULE_TEMPLATES['loader-app']; for (l = 0; l < langs.length; l += 1) { lang = langs[l] || ''; for (i = 0; i < MODULE_PER_LANG.length; i += 1) { name = MODULE_PER_LANG[i]; // populating the internal cache using name+lang as the key this.resContents[([name, lang].join('-'))] = this._produceMeta(name, lang || '*', appMetaData); } } }, /** * @private * @method _processMeta * @param {object} resolvedMods resolved module metadata, from Y.Loader.resolve() * @param {object} modules regular YUI module metadata (ala loader-yui3) * @param {object} appModules a hash table with the modules that are part of the app, use to correct paths when needed. * @return {nothing} */ _processMeta: function(resolvedMods, modules, appModules) { var m, l, i, module, name, mod, mod1, lang, bundle, intlmodules = []; for (m in resolvedMods) { if (resolvedMods.hasOwnProperty(m) && appModules.hasOwnProperty(resolvedMods[m].name)) { module = resolvedMods[m]; mod = name = module.name; bundle = name.indexOf('lang/') === 0; lang = bundle && REGEX_LOCALE.exec(name); if (lang) { mod = mod.slice(0, lang.index); // eg. lang/foo_en-US -> lang/foo lang = lang[1]; mod1 = mod.split("/"); if (intlmodules.indexOf(mod1[1]) === -1) { intlmodules.push(mod1[1]); } // TODO: validate lang } mod = bundle ? mod.slice(5) : mod; // eg. lang/foo -> foo // language manipulation // TODO: this routine is very restrictive, and we might want to // make it optional later on. if (module.lang) { module.lang = ['{langToken}']; } if (bundle) { module.owner = mod; // applying some extra optimizations module.langPack = lang || '*'; module.intl = true; module.expanded_map = undefined; } // getting the last portion of the url which // is the important part for loader to make // combo urls shorter module.path = module.path.split('/').pop(); modules[module.name] = {}; if (module.type === 'css') { modules[module.name].type = 'css'; } for (i = 0; i < MODULE_META_ENTRIES.length; i += 1) { if (MODULE_META_ENTRIES[i] === 'path' && module.intl) { module[MODULE_META_ENTRIES[i]] = 'lang/' + module[MODULE_META_ENTRIES[i]]; } if (module[MODULE_META_ENTRIES[i]]) { modules[module.name][MODULE_META_ENTRIES[i]] = module[MODULE_META_ENTRIES[i]]; } } } } //scan modules in resolvedMods, if a module is also listed in intlmodules, //lang holder will be added to the module's meta data. //lang info will be plugged in later when _precalcLoaderMeta is called for (m in resolvedMods) { if (resolvedMods.hasOwnProperty(m) && appModules.hasOwnProperty(resolvedMods[m].name)) { module = resolvedMods[m]; if (intlmodules.indexOf(module.name) > -1) { modules[module.name].lang = ['{langToken}']; } } } }, /** * Generates the final YUI metadata. * @private * @method _produceMeta * @param {string} name type of YUI metadata to return * @param {string} lang which language the metadata should be customized for * @param {object} appMetaData gathered YUI metadata for the application * @return {string} the requested YUI metadata */ _produceMeta: function(name, lang, appMetaData) { var token = '', path = ''; if (lang) { token = '"' + lang + '"'; path = '_' + lang; } else { lang = '*'; } // module definition definitions return MODULE_TEMPLATES[name] .replace('{app-base}', appMetaData.base[lang] || appMetaData.base['*']) .replace(REGEX_LANG_TOKEN, token) .replace(REGEX_LANG_PATH, path); }, /** * Precomputes a set of dependencies. * @private * @method _precomputeYUIDependencies * @param {string} lang YUI language code * @param {string} env runtime environment (either `client`, or `server`) * @param {string} mojit name of the mojit * @param {object} modules YUI module metadata * @param {object} required lookup hash of YUI module names that are required * @param {boolean} forceYLoader whether to force the use of Y.Loader * @return {object} precomputed (and sorted) module dependencies */ _precomputeYUIDependencies: function(lang, env, mojit, modules, required, forceYLoader) { var loader, m, module, originalYUAnodejs, info, warn, sortedPaths = {}; // We don't actually need the full list, just the required modules. // YUI.Loader() will do the rest at runtime. if (!forceYLoader) { for (module in required) { if (required.hasOwnProperty(module) && modules[module]) { sortedPaths[module] = modules[module].fullpath; } } return { sorted: Object.keys(sortedPaths), paths: sortedPaths }; } // HACK // We need to clear YUI's cached dependencies, since there's no // guarantee that the previously calculated dependencies have been done // using the same context as this calculation. YUI.Env._renderedMods = undefined; // Trick the loader into thinking it's -not- running on nodejs. // This is the official way to do it. originalYUAnodejs = Y.UA.nodejs; Y.UA.nodejs = ('server' === env); // Use ignoreRegistered here instead of the old `YUI.Env._renderedMods = undefined;` hack loader = new Y.Loader({ ignoreRegistered: true }); // Only override the default if it's required if (this.yuiConfig.base) { loader.base = this.yuiConfig.base; } loader.addGroup({modules: modules}, mojit); loader.calculate({required: required}); Y.UA.nodejs = originalYUAnodejs; for (m = 0; m < loader.sorted.length; m += 1) { module = loader.sorted[m]; info = loader.moduleInfo[module]; if (info) { // modules with "nodejs" in their name are tweaks on other modules if ('client' === env && module.indexOf('nodejs') !== -1) { continue; } sortedPaths[module] = info.fullpath || loader._url(info.path); } } // log warning if server mojit has dom dependency if ('server' === env) { warn = Y.Object.keys(sortedPaths).join(' ').match(WARN_SERVER_MODULES); if (warn) { Y.log('your mojit "' + mojit + '" has a server affinity and these client-related deps: ' + warn.join(', '), 'WARN', NAME); Y.log('Mojito may be unable to start, unless you have provided server-side DOM/host-object suppport', 'WARN', NAME); } } return { sorted: loader.sorted, paths: sortedPaths }; }, /** * Generates the YUI configuration for the resource. * @private * @method _makeYUIModuleConfig * @param {string} env runtime environment (either `client`, or `server`) * @param {object} res the resource metadata * @return {object} the YUI configuration for the module */ _makeYUIModuleConfig: function(env, res) { var config = { requires: (res.yui.meta && res.yui.meta.requires) || [] }; if ('client' === env) { // using relative path since the loader will do the rest config.path = res.url; } else { config.fullpath = res.source.fs.fullPath; } return config; }, /** * If the resource is a YUI module, augments its metadata with metadata * about the YUI module and execute the module if it has a server/common affinity. * @private * @method _captureYUIModuleDetails * @param {object} res resource metadata * @param {object} runSandbox if passed, the function in the module * will be called using this parameter as the YUI sandbox * @return {nothing} */ _captureYUIModuleDetails: function(res, runSandbox) { var file = libfs.readFileSync(res.source.fs.fullPath, 'utf8'), yui = res.yui || {}, store = this.get('host'), originalAdd = store.YUI.add, mod = new Module(res.source.fs.fullPath, module); mod.filename = res.source.fs.fullPath; mod.paths = Module._nodeModulePaths(libpath.dirname(res.source.fs.fullPath)); try { mod._compile('module.exports = function (YUI) {' + 'return (function () {' + file + '\n;}).apply(global);' + '};', res.source.fs.fullPath); } catch (e1) { Y.log('Error compiling ' + mod.filename + ': ' + e1.message, 'error', NAME); return; } // Hook into YUI.add in order to capture the module's YUI metadata. store.YUI.add = function(name, fn, version, meta) { // Check YUI.Env.mods to make sure the same module is not added multiple times. // This is important because if multiple modules have the same name, Mojito uses the first one, // so it should not be overwritten. if (res.affinity !== 'client' && !store.YUI.Env.mods[name]) { // Client side modules are never used on the server so only add // modules with server or common affinity. originalAdd.apply(store.YUI, arguments); } yui.name = name; yui.version = version; yui.meta = meta || {}; if (!yui.meta.requires) { yui.meta.requires = []; } if (runSandbox) { try { fn(runSandbox, yui.name); } catch (e) { Y.log('failed to run javascript file ' + res.source.fs.fullPath + '\n' + e.message, 'error', NAME); } } }; try { mod.exports(store.YUI); res.yui = yui; } catch (e2) { Y.log('Error running ' + mod.filename + '\n' + e2.stack, 'error', NAME); } store.YUI.add = originalAdd; } }); Y.namespace('mojito.addons.rs'); Y.mojito.addons.rs.yui = RSAddonYUI; }, '0.0.1', { requires: ['plugin', 'oop', 'loader-base', 'mojito-util']});