diff --git a/lib/app/addons/ac/deploy.server.js b/lib/app/addons/ac/deploy.server.js index 6710576f2..cdab14b12 100644 --- a/lib/app/addons/ac/deploy.server.js +++ b/lib/app/addons/ac/deploy.server.js @@ -16,8 +16,6 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { 'use strict'; - var fs = require('fs'); - /** * Access point: ac.deploy.* * Provides ability to create client runtime deployment HTML @@ -59,13 +57,16 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { var store = this.rs, contextServer = this.ac.context, + appConfigServer = store.getAppConfig(contextServer), + appGroupConfig = store.yui.getAppGroupConfig(), + seedFiles = store.yui.getAppSeedFiles(contextServer), + contextClient, appConfigClient, yuiConfig = {}, yuiConfigEscaped, yuiConfigStr, - closestLang, viewId, i, clientConfig = {}, @@ -91,8 +92,9 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { // a fragment to prepend to the path attribute when // when building combo urls root: Y.version + '/build/', - seed: "/static/combo?yui-base.js&loader-base.js&" + - "loader-yui3.js&loader-app-base{langPath}.js&loader.js" + groups: { + app: appGroupConfig + } }, ((appConfigClient.yui && appConfigClient.yui.config) || {}), { lang: contextServer.lang // same as contextClient.lang @@ -103,15 +105,30 @@ YUI.add('mojito-deploy-addon', function(Y, NAME) { delete appConfigClient.yui.config; } - // adjusting the seed based on {langPath} to facilitate - // the customization of the combo url. - closestLang = store.yui.getClosestLang(contextServer.lang); - yuiConfig.seed = yuiConfig.seed.replace('{langPath}', - (closestLang ? '_' + closestLang : '')); - // Set the YUI URL to use on the client (This has to be done - // before any other scripts are added) - assetHandler.addAsset('js', 'top', yuiConfig.seed); + // Set the YUI seed file to use on the client. This has to be done + // before any other scripts are added and can be controlled through + // application.json->yui->config->seed in a form of + // a array with the list of modules or fullpath urls. + 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. + for (i = 0; i < seedFiles.length; i += 1) { + assetHandler.addAsset('js', 'top', appGroupConfig.base + + appGroupConfig.root + seedFiles[i]); + } + + } else { + + // using combo for the seed files + assetHandler.addAsset('js', 'top', appGroupConfig.comboBase + + appGroupConfig.root + seedFiles.join(appGroupConfig.comboSep + + appGroupConfig.root)); + + } + // adding the default module for the Y.use statement in the client initialModuleList['mojito-client'] = true; diff --git a/lib/app/addons/rs/url.js b/lib/app/addons/rs/url.js index dde417fc0..2da108688 100644 --- a/lib/app/addons/rs/url.js +++ b/lib/app/addons/rs/url.js @@ -4,7 +4,7 @@ * See the accompanying LICENSE file for terms. */ -/*jslint anon:true, sloppy:true, nomen:true, stupid:true*/ +/*jslint anon:true, sloppy:true, nomen:true, stupid:true, node:true */ /*global YUI*/ @@ -18,7 +18,10 @@ */ YUI.add('addon-rs-url', function(Y, NAME) { - var libfs = require('fs'), + 'use strict'; + + var libfs = require('fs'), + liburl = require('url'), libpath = require('path'), existsSync = libfs.existsSync || libpath.existsSync, URL_PARTS = ['frameworkName', 'appName', 'prefix']; @@ -61,9 +64,6 @@ YUI.add('addon-rs-url', function(Y, NAME) { this.config[part] = defaults[part] || ''; } } - - // FUTURE: deprecate appConfig.assumeRollups - this.assumeRollups = this.config.assumeRollups || appConfig.assumeRollups; }, @@ -98,10 +98,7 @@ YUI.add('addon-rs-url', function(Y, NAME) { } // Server-only framework mojits like DaliProxy and HTMLFrameMojit - // should never have URLs associated with them. This never used - // to be an issue until we added the "assumeRollups" feature to - // preload JSON specs for specific mojits during the compile step - // (`mojito compile json`) for Livestand. + // should never have URLs associated with them. if ('shared' !== mojit && 'mojito' === mojitRes.source.pkg.name) { mojitControllerRess = store.getResourceVersions({mojit: mojit, id: 'controller--controller'}); if (mojitControllerRess.length === 1 && @@ -124,7 +121,6 @@ YUI.add('addon-rs-url', function(Y, NAME) { ress = store.getResourceVersions({mojit: mojit}); for (r = 0; r < ress.length; r += 1) { res = ress[r]; - skip = false; if ('config' === res.type) { skip = true; @@ -177,18 +173,23 @@ YUI.add('addon-rs-url', function(Y, NAME) { _calcResourceURL: function(res, mojitRes) { var fs = res.source.fs, relativePath = fs.fullPath.substr(fs.rootDir.length + 1), - urlParts = [], - rollupParts = [], - rollupFsPath; + urlParts = [liburl.resolve('/', (this.config.prefix || 'static'))]; - // Don't clobber a URL calculated by another RS addon. - if (res.hasOwnProperty('url')) { + // Don't clobber a URL calculated by another RS addon, or bother to + // proceed for server affinity resources that don't need uris + if (res.hasOwnProperty('url') || ('server' === res.affinity.affinity)) { return; } - if (this.config.prefix) { - urlParts.push(this.config.prefix); - rollupParts.push(this.config.prefix); + if (res.yui && res.yui.name) { + // any yui module in our app will have a fixed path + // that has to be really short to optimize combo + // urls as much as possible. This url will also work + // in conjuntion with "base", "comboBase" and "root" + // from application.json->yui->config + urlParts.push(res.yui.name + '.js'); + res.url = urlParts.join('/'); + return; } if ('shared' === res.mojit) { @@ -201,42 +202,23 @@ YUI.add('addon-rs-url', function(Y, NAME) { urlParts.push(this.config.appName); } } - // fw resources are also put into the app-level rollup - if (res.yui && res.yui.name) { - if (this.config.appName) { - rollupParts.push(this.config.appName); - } - rollupParts.push('rollup.client.js'); - rollupFsPath = libpath.join(this.appRoot, 'rollup.client.js'); - } } else { if ('mojit' === res.type) { urlParts.push(res.name); } else { urlParts.push(res.mojit); } - if (res.yui && res.yui.name) { - rollupParts.push(res.mojit); - rollupParts.push('rollup.client.js'); - rollupFsPath = libpath.join(mojitRes.source.fs.fullPath, 'rollup.client.js'); - } } if ('mojit' === res.type) { if ('shared' !== res.name) { - res.url = '/' + urlParts.join('/'); + res.url = urlParts.join('/'); } return; } urlParts.push(relativePath); - - if (rollupFsPath && (this.assumeRollups || existsSync(rollupFsPath))) { - res.url = '/' + rollupParts.join('/'); - fs.rollupPath = rollupFsPath; - } else { - res.url = '/' + urlParts.join('/'); - } + res.url = urlParts.join('/'); } diff --git a/lib/app/addons/rs/yui.js b/lib/app/addons/rs/yui.js index 06f5cfae6..444d39fb2 100644 --- a/lib/app/addons/rs/yui.js +++ b/lib/app/addons/rs/yui.js @@ -4,7 +4,7 @@ * See the accompanying LICENSE file for terms. */ -/*jslint anon:true, sloppy:true, nomen:true, stupid:true*/ +/*jslint anon:true, sloppy:true, nomen:true, stupid:true, node:true*/ /*global YUI*/ @@ -19,9 +19,13 @@ */ YUI.add('addon-rs-yui', function(Y, NAME) { + 'use strict'; + var libfs = require('fs'), libpath = require('path'), libvm = require('vm'), + libmime = require('mime'), + WARN_SERVER_MODULES = /\b(dom-[\w\-]+|node-[\w\-]+|io-upload-iframe)/ig, MODULE_SUBDIRS = { autoload: true, @@ -29,8 +33,10 @@ YUI.add('addon-rs-yui', function(Y, NAME) { yui_modules: true }, - yuiSandbox = require(libpath.join(__dirname, '..', '..', '..', 'yui-sandbox.js')).getYUI(), - MODULE_META_ENTRIES = ['requires', 'use', 'optional', 'skinnable', 'after', 'condition'], + yuiSandboxFactory = require(libpath.join(__dirname, '..', '..', '..', 'yui-sandbox.js')), + syntheticStat = null, + + MODULE_META_ENTRIES = ['path', 'requires', 'use', 'optional', 'skinnable', 'after', 'condition'], // TODO: revisit this list with @davglass MODULE_META_PRIVATE_ENTRIES = ['after', 'expanded', 'supersedes', 'ext', '_parsed', '_inspected', 'skinCache', 'langCache'], @@ -38,45 +44,75 @@ YUI.add('addon-rs-yui', function(Y, NAME) { REGEX_LANG_PATH = /\{langPath\}/g, REGEX_LOCALE = /\_([a-z]{2}(-[A-Z]{2})?)$/, + MODULE_PER_LANG = ['loader-app-base', 'loader-app-resolved', 'loader-yui3-base', 'loader-yui3-resolved'], 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-base",function(Y){' + + 'YUI.add("loader-app",function(Y){' + 'Y.applyConfig({groups:{app:Y.merge(' + - '{static-app-group-config},' + '((Y.config.groups&&Y.config.groups.app)||{}),' + '{modules:{app-base}}' + ')}});' + '},"",{requires:["loader-base"]});', - 'loader-app-full': - 'YUI.add("loader-app-full",function(Y){' + + + /* + * Use this module when you want to precompute the loader metadata to + * avoid doing recursive computations to resolve combo urls for app yui modules + * in the client runtime. + * Note: Keep in mind that this meta is considerable bigger than "loader-app-base". + */ + 'loader-app-resolved': + 'YUI.add("loader-app",function(Y){' + 'Y.applyConfig({groups:{app:Y.merge(' + - '{static-app-group-config},' + '((Y.config.groups&&Y.config.groups.app)||{}),' + - '{modules:{app-full}}' + + '{modules:{app-resolved}}' + ')}});' + '},"",{requires:["loader-base"]});', - 'loader': - 'YUI.add("loader",function(Y){' + - '},"",{requires:["loader-base","loader-yui3","loader-app-base"]});', - - 'loader-lock': - 'YUI.add("loader",function(Y){' + + /* + * Use this module when you want to rely on the loader to do recursive + * computations to resolve combo urls for yui core modules in the client + * runtime. + * Note: This is a more restrictive configuration than the default + * meta bundle with yui, but it is considerable smaller, which helps + * with performance. + */ + 'loader-yui3-base': + 'YUI.add("loader-yui3",function(Y){' + // TODO: we should use YUI.applyConfig() instead of the internal // YUI.Env API, but that's pending due a bug in YUI: // http://yuilibrary.com/projects/yui3/ticket/2532854 'YUI.Env[Y.version].modules=YUI.Env[Y.version].modules||' + '{yui-base};' + - '},"",{requires:["loader-base","loader-app-base"]});', + '},"",{requires:["loader-base"]});', - 'loader-full': - 'YUI.add("loader",function(Y){' + + /* + * Use this module when you want to precompute the loader metadata to + * avoid doing recursive computations to resolve combo urls for yui core + * modules in the client runtime. + * Note: Keep in mind that this meta is considerable bigger than "loader-yui3-base". + */ + 'loader-yui3-resolved': + 'YUI.add("loader-yui3",function(Y){' + // TODO: we should use YUI.applyConfig() instead of the internal // YUI.Env API, but that's pending due a bug in YUI: // http://yuilibrary.com/projects/yui3/ticket/2532854 'YUI.Env[Y.version].modules=YUI.Env[Y.version].modules||' + - '{yui-full};' + - '},"",{requires:["loader-base","loader-app-full"]});' + '{yui-resolved};' + + '},"",{requires:["loader-base"]});' }; @@ -96,16 +132,25 @@ YUI.add('addon-rs-yui', function(Y, NAME) { initializer: function(config) { 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('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.yuiConfig = config.host.getStaticAppConfig().yui || {}; - + this.staticAppConfig = config.host.getStaticAppConfig() || {}; + this.yuiConfig = (this.staticAppConfig.yui && this.staticAppConfig.yui.config) || {}; this.langs = {}; // keys are list of languages in the app, values are simply "true" - this.resContents = {}; // res.id: contents + this.resContents = {}; // res.id: contents + this.appModulesRess = {}; // res.yui.name: module ress + this.yuiModulesRess = {}; // res.yui.name: fake ress }, @@ -211,13 +256,76 @@ YUI.add('addon-rs-yui', function(Y, NAME) { return Y.merge({ combine: (this.yuiConfig.combine === false) ? false : true, maxURLLength: this.yuiConfig.maxURLLength || 1024, - base: "/static/", - comboBase: "/static/combo?", + base: "/.", + comboBase: "/combo~", + comboSep: "~", root: "" }, ((this.yuiConfig.groups && this.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 + * @return {array} list of seed files + */ + getAppSeedFiles: function(ctx) { + var closestLang = this.getClosestLang(ctx.lang), + files = [], + seed = this.yuiConfig.seed ? Y.Array(this.yuiConfig.seed) : [ + 'yui-base', + 'loader-base', + 'loader-yui3', + 'loader-app', + 'loader-app-base{langPath}' + ], + i; + + // adjusting lang just to be url friendly + closestLang = closestLang ? '_' + closestLang : ''; + + // 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. + files[i] = seed[i].replace(REGEX_LANG_PATH, closestLang); + // verifying if the file is actually a synthetic or yui module + if (this.yuiModulesRess.hasOwnProperty(files[i])) { + files[i] = this.yuiModulesRess[files[i]].url; + } else if (this.appModulesRess.hasOwnProperty(files[i])) { + files[i] = this.appModulesRess[files[i]].url; + } + } + + return files; + }, + + + /* + * Aggregate all yui core files + * using the path of as the hash. + * + * @method getYUIURLResources + * @return {object} yui core resources by url + * @api private + */ + getYUIURLResources: function () { + var name, + urls = {}; + + for (name in this.yuiModulesRess) { + if (this.yuiModulesRess.hasOwnProperty(name)) { + urls[this.yuiModulesRess[name].url] = this.yuiModulesRess[name]; + } + } + return urls; + }, + + /** * Using AOP, this is called after the ResourceStore's version. * @method findResourceVersionByConvention @@ -289,6 +397,8 @@ YUI.add('addon-rs-yui', function(Y, NAME) { res.name = res.yui.name; res.id = [res.type, res.subtype, res.name].join('-'); this.langs[res.yui.lang] = true; + // caching the res + this.appModulesRess[res.yui.name] = res; return new Y.Do.Halt(null, res); } @@ -314,6 +424,8 @@ YUI.add('addon-rs-yui', function(Y, NAME) { this._captureYUIModuleDetails(res); res.name = res.yui.name; res.id = [res.type, res.subtype, res.name].join('-'); + // caching the res + this.appModulesRess[res.yui.name] = res; return new Y.Do.Halt(null, res); } }, @@ -358,7 +470,9 @@ YUI.add('addon-rs-yui', function(Y, NAME) { //console.log('------------------------------------------- YUI makeResourceVersions'); var store = this.get('host'), l, + i, lang, + name, langExt, langs = Object.keys(this.langs), res; @@ -370,88 +484,58 @@ YUI.add('addon-rs-yui', function(Y, NAME) { res = { source: {}, - type: 'yui-loader', - subtype: 'rollup', - name: '', - affinity: 'common', + mojit: 'shared', + type: 'yui-module', + subtype: 'synthetic', + name: 'loader-app', + affinity: 'client', selector: '*', yui: { - name: 'loader' + 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.js', true); + res.source.fs = store.makeResourceFSMeta(this.appRoot, 'app', '.', 'loader-app.js', true); + // adding res to cache + this.appModulesRess['loader-app'] = res; store.addResourceVersion(res); for (l = 0; l < langs.length; l += 1) { lang = langs[l]; langExt = lang ? '_' + lang : ''; - res = { - source: {}, - type: 'yui-loader', - subtype: 'app-base', - name: lang, - affinity: 'common', - selector: '*', - yui: { - name: 'loader-app-base' + langExt - } - }; - res.id = [res.type, res.subtype, res.name].join('-'); - res.source.pkg = store.getAppPkgMeta(); - res.source.fs = store.makeResourceFSMeta(this.appRoot, 'app', '.', 'loader-app-base' + langExt + '.js', true); - store.addResourceVersion(res); + for (i = 0; i < MODULE_PER_LANG.length; i += 1) { - res = { - source: {}, - type: 'yui-loader', - subtype: 'app-full', - name: lang, - affinity: 'common', - selector: '*', - yui: { - name: 'loader-app-full' + langExt - } - }; - res.id = [res.type, res.subtype, res.name].join('-'); - res.source.pkg = store.getAppPkgMeta(); - res.source.fs = store.makeResourceFSMeta(this.appRoot, 'app', '.', 'loader-app-full' + langExt + '.js', true); - store.addResourceVersion(res); + name = MODULE_PER_LANG[i]; - res = { - source: {}, - type: 'yui-loader', - subtype: 'yui-base', - name: lang, - affinity: 'common', - selector: '*', - yui: { - name: 'loader-lock' + langExt - } - }; - res.id = [res.type, res.subtype, res.name].join('-'); - res.source.pkg = store.getAppPkgMeta(); - res.source.fs = store.makeResourceFSMeta(this.appRoot, 'app', '.', 'loader-lock' + langExt + '.js', true); - store.addResourceVersion(res); + 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); + // adding res to cache + this.appModulesRess[name + langExt] = res; + store.addResourceVersion(res); + + } - res = { - source: {}, - type: 'yui-loader', - subtype: 'yui-full', - name: lang, - affinity: 'common', - selector: '*', - yui: { - name: 'loader-full' + langExt - } - }; - res.id = [res.type, res.subtype, res.name].join('-'); - res.source.pkg = store.getAppPkgMeta(); - res.source.fs = store.makeResourceFSMeta(this.appRoot, 'app', '.', 'loader-full' + langExt + '.js', true); - store.addResourceVersion(res); } + + // we can also make some fake resources for all yui + // modules that we might want to serve. + this._precalcYUIResources(); }, @@ -486,14 +570,61 @@ YUI.add('addon-rs-yui', function(Y, NAME) { * @return {undefined} nothing is returned, the results are returned via the callback */ getResourceContent: function(res, callback) { - var contents = this.resContents[res.id]; + var contents = res.name && this.resContents[res.name]; if (contents) { - callback(null, new Buffer(contents, 'utf8'), null); + 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 name, + modules, + mimetype, + charset, + fullpath, + staticHandling = this.staticAppConfig.staticHandling || {}, + 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] = { + url: libpath.join('/', (staticHandling.prefix || 'static'), + 'yui', modules[name].path), + source: { + fs: { + isFile: true, + fullPath: fullpath + } + }, + mime: { + type: mimetype, + charset: charset + } + }; + } + } + this.yuiModulesRess = modules; + }, + /** * Precomputes YUI loader metadata, so that we don't have to at runtime. * @private @@ -524,13 +655,15 @@ YUI.add('addon-rs-yui', function(Y, NAME) { modules = {}, // regular meta (a la loader-yui3) conditions = {}, // hash to store conditional functions name, - i; + i, + l; // TODO: inline these calls, and optimize mojits = this.getConfigAllMojits('client', {}); shared = this.getConfigShared('client', {}); - Ysandbox = yuiSandbox(Y.merge(this.yuiConfig, this.getAppGroupConfig())); + Ysandbox = yuiSandboxFactory + .getYUI(this.yuiConfig.filter)(Y.merge(this.yuiConfig)); modules_config = Ysandbox.merge((mojits.modules || {}), (shared.modules || {})); Ysandbox.applyConfig({ @@ -607,22 +740,20 @@ YUI.add('addon-rs-yui', function(Y, NAME) { } } // for each lang - this.resContents['yui-loader-rollup-'] = MODULE_TEMPLATES.loader; + this.resContents['loader-app'] = MODULE_TEMPLATES['loader-app']; - for (i = 0; i < langs.length; i += 1) { - lang = langs[i] || ''; + for (l = 0; l < langs.length; l += 1) { + lang = langs[l] || ''; - this.resContents['yui-loader-app-base-' + lang] = - this._produceMeta('loader-app-base', lang || '*', appMetaData, yuiMetaData); + for (i = 0; i < MODULE_PER_LANG.length; i += 1) { - this.resContents['yui-loader-app-full-' + lang] = - this._produceMeta('loader-app-full', lang || '*', appMetaData, yuiMetaData); + 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, yuiMetaData); - this.resContents['yui-loader-yui-base-' + lang] = - this._produceMeta('loader-lock', lang || '*', appMetaData, yuiMetaData); + } - this.resContents['yui-loader-yui-full-' + lang] = - this._produceMeta('loader-full', lang || '*', appMetaData, yuiMetaData); } }, @@ -723,11 +854,10 @@ YUI.add('addon-rs-yui', function(Y, NAME) { // module definition definitions return MODULE_TEMPLATES[name] - .replace('{static-app-group-config}', JSON.stringify(this.getAppGroupConfig())) .replace('{app-base}', appMetaData.base[lang] || appMetaData.base['*']) - .replace('{app-full}', appMetaData.full[lang] || appMetaData.full['*']) + .replace('{app-resolved}', appMetaData.full[lang] || appMetaData.full['*']) .replace('{yui-base}', yuiMetaData.base[lang] || yuiMetaData.base['*']) - .replace('{yui-full}', yuiMetaData.full[lang] || yuiMetaData.full['*']) + .replace('{yui-resolved}', yuiMetaData.full[lang] || yuiMetaData.full['*']) .replace(REGEX_LANG_TOKEN, token) .replace(REGEX_LANG_PATH, path); }, @@ -829,9 +959,14 @@ YUI.add('addon-rs-yui', function(Y, NAME) { */ _makeYUIModuleConfig: function(env, res) { var config = { - fullpath: ('client' === env) ? res.url : res.source.fs.fullPath, 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; }, diff --git a/lib/app/autoload/store.server.js b/lib/app/autoload/store.server.js index 4bc161fa0..8126c96df 100644 --- a/lib/app/autoload/store.server.js +++ b/lib/app/autoload/store.server.js @@ -806,7 +806,7 @@ YUI.add('mojito-resource-store', function(Y, NAME) { 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; + urls[res.url] = res.source.fs.fullPath; } } } @@ -845,11 +845,6 @@ YUI.add('mojito-resource-store', function(Y, NAME) { res = ress[r]; if (res.url && res.source.fs.isFile) { urls[res.url] = res; - // rewrite for the favicon.ico - // /favicon.ico is sent to ./my_app_folder/assets/favicon.ico - if (/\/assets\/favicon\.ico$/.test(res.url) && (res.source.fs.rootType === 'app')) { - urls['/favicon.ico'] = res; - } } } } @@ -874,7 +869,7 @@ YUI.add('mojito-resource-store', function(Y, NAME) { filename; if (res && res.source && res.source.fs && res.source.fs.isFile) { - filename = res.source.fs.rollupPath || res.source.fs.fullPath; + filename = res.source.fs.fullPath; // FUTURE [Issue 89] stat cache? store._libs.fs.stat(filename, function(err, stat) { diff --git a/lib/app/middleware/mojito-handler-static.js b/lib/app/middleware/mojito-handler-static.js index 770bef290..f886840f8 100644 --- a/lib/app/middleware/mojito-handler-static.js +++ b/lib/app/middleware/mojito-handler-static.js @@ -19,20 +19,25 @@ */ -/*jslint node:true, nomen:true, continue: true */ +/*jslint node:true, nomen:true */ 'use strict'; /* * Module dependencies. */ -var libfs = require('fs'), - libpath = require('path'), - libmime = require('mime'), - YUI = require(libpath.join(__dirname, '..', '..', 'yui-sandbox.js')).getYUI(), - parseUrl = require('url').parse, - logger, - NAME = 'StaticHandler'; +var liburl = require('url'), + libpath = require('path'), + + NAME = 'StaticHandler', + + // TODO: this adjustments should really be done by addons/rs/url + // or a designated route. + STATIC_ROUTER = { + '/favicon.ico': {mojit: 'shared', id: 'asset-ico-favicon'}, + '/robots.txt': {mojit: 'shared', id: 'asset-txt-robots'}, + '/crossdomain.xml': {mojit: 'shared', id: 'asset-xml-crossdomain'} + }; /* * File buffer cache. @@ -169,118 +174,6 @@ function makeContentTypeHeader(resource) { resource.mime.charset : ''); } -/* - * Aggregate all yui modules from the app that are compatible with - * client runtime using the name of the yui module as the hash. - * - * @method getAppModuleResources - * @param {object} store Resource Store instance - * @return {object} yui modules resources - * @api private - */ -function getAppModuleResources(store) { - var ress, - m, - mojit, - mojits, - moduleRess = {}; - - function processRess(ress) { - var r, - res; - for (r = 0; r < ress.length; r += 1) { - res = ress[r]; - if ('common' !== res.affinity.affinity) { - continue; - } - if (res.yui && res.yui.name) { - moduleRess[res.yui.name] = res; - } - } - for (r = 0; r < ress.length; r += 1) { - res = ress[r]; - if ('client' !== res.affinity.affinity) { - continue; - } - if (res.yui && res.yui.name) { - if (moduleRess[res.yui.name]) { - logger.log('YUI Modules should have unique name per affinity. ' + - 'Module [' + res.yui.name + '] has both common and ' + - 'client affinity.', 'warn', NAME); - } - moduleRess[res.yui.name] = res; - } - } - } - - ress = store.getResourceVersions({}); - processRess(ress); - - mojits = store.listAllMojits(); - mojits.push('shared'); - for (m = 0; m < mojits.length; m += 1) { - mojit = mojits[m]; - ress = store.getResourceVersions({mojit: mojit}); - processRess(ress); - } - - return moduleRess; -} - -/* - * Aggregate all yui core modules - * using the name of the yui module as the hash. - * - * @method getYUIModuleResources - * @param {object} appConfig Static application config - * @return {object} yui core modules resources - * @api private - */ -function getYUIModuleResources(appConfig) { - var Y, - name, - modules, - mimetype, - charset, - fullpath; - - Y = YUI({ - fetchCSS: true, - combine: true, - base: "/static/combo?", - comboBase: "/static/combo?", - root: "" - }, ((appConfig.yui && appConfig.yui.config) || {})); - - // used to find the the modules in YUI itself - Y.use('loader'); - modules = (new Y.Loader(Y.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] = { - source: { - fs: { - isFile: true, - fullPath: fullpath - } - }, - mime: { - type: mimetype, - charset: charset - } - }; - } - } - return modules; -} - - /* * Static file server. * @@ -296,21 +189,17 @@ function getYUIModuleResources(appConfig) { * @return {Function} * @api public */ -function staticProvider(store, globalLogger, Y) { +function staticProvider(store, logger, Y) { - var appConfig = store.getStaticAppConfig(), - options = appConfig.staticHandling || {}, - cache = options.cache, - maxAge = options.maxAge, - staticRess, - moduleRess, - yuiRess; - - logger = globalLogger; + var appConfig = store.getStaticAppConfig(), + yuiRess = store.yui.getYUIURLResources(), + staticRess = store.getAllURLResources(), - moduleRess = getAppModuleResources(store); - yuiRess = getYUIModuleResources(appConfig); - staticRess = store.getAllURLResources(); + options = appConfig.staticHandling || {}, + cache = options.cache, + maxAge = options.maxAge, + staticPath = liburl.resolve('/', (options.prefix || 'static') + '/'), + comboPath = '/combo~'; if (cache && !maxAge) { maxAge = cache; @@ -333,18 +222,16 @@ function staticProvider(store, globalLogger, Y) { return; } - var url = parseUrl(req.url), + var url = liburl.parse(req.url), path = url.pathname, files = [], file, result = [], failures = 0, counter = 0, - module, resource, i; - function tryToFlush() { var headers, content = '', @@ -355,7 +242,7 @@ function staticProvider(store, globalLogger, Y) { } if (counter === files.length) { if (failures) { - notFound(); + notFound(res); return; } } @@ -394,7 +281,7 @@ function staticProvider(store, globalLogger, Y) { counter += 1; if (err) { logger.log('failed to read ' + path + ' because: ' + err.message, 'error', NAME); - notFound(); + notFound(res); } else { logger.log(path + ' was read from disk', 'debug', NAME); result[index].content = data; @@ -419,36 +306,31 @@ function staticProvider(store, globalLogger, Y) { return; } - - // combo urls are allow as well - if (libpath.basename(url.pathname) === 'combo' && url.query) { - // YIV might be messing around with the querystring params - // trying to formalize them by adding = and transforming / - // so we need to revert back to / and remove the = - // TODO: this might not work in Windows - files = url.query.replace(/[=]/g, '').replace(/%2F/g, '/').split('&'); - } else { + // combo urls are allow as well in a form of: + // - /combo~foo.js~bar.js + if (path.indexOf(comboPath) === 0) { + // no spaces allowed + files = url.pathname.split('~'); + files.shift(); // removing te first element from the list + } else if (path.indexOf(staticPath) === 0 || STATIC_ROUTER.hasOwnProperty(path)) { files = [path]; + } else { + // this is not a static file + next(); + return; } for (i = 0; i < files.length; i += 1) { file = files[i]; - // Cache hit if (cache && _cache[file]) { + + // Cache hit logger.log(file + ' was read from cache', 'debug', NAME); result[i] = _cache[file]; - continue; - } - // something like: - // - foo/bar-min.js becomes "bar" - // - foo/lang/bar_en-US.js becomes "lang/bar_en-US" - module = (file.indexOf('/lang/') >= 0 ? 'lang/' : '') + - libpath.basename(file, libpath.extname(file)). - replace(/\-(min|debug)$/, ''); - if (staticRess[file]) { + } else if (staticRess[file]) { // geting an static file result[i] = { @@ -456,47 +338,31 @@ function staticProvider(store, globalLogger, Y) { res: staticRess[file] }; - // TODO: these adjustments should really be done by addons/rs/url - } else if (file === '/favicon.ico') { + } else if (STATIC_ROUTER.hasOwnProperty(file)) { + // TODO: this adjustments should really be done by addons/rs/url // geting an static favicon result[i] = { path: file, - res: store.getResources('client', {}, {mojit: 'shared', id: 'asset-ico-favicon'})[0] + res: store.getResources('client', {}, STATIC_ROUTER[file])[0] }; - } else if (file === '/robots.txt') { + } else if (yuiRess[file]) { - // geting an static robot + // getting a yui library file result[i] = { path: file, - res: store.getResources('client', {}, {mojit: 'shared', id: 'asset-txt-robots'})[0] + res: yuiRess[file] }; - } else if (file === '/crossdomain.xml') { + } else if (i < (files.length - 1)) { - // geting an static crossdomain - result[i] = { - path: file, - res: store.getResources('client', {}, {mojit: 'shared', id: 'asset-xml-crossdomain'})[0] - }; - - // other kind of static files - } else if (moduleRess[module]) { - - // geting an app module - result[i] = { - path: file, - res: moduleRess[module] - }; - - } else if (yuiRess[module]) { - - // getting a yui module - result[i] = { - path: file, - res: yuiRess[module] - }; + // Note: we are tolerants with the last file in the combo + // because some proxies/routers might cut the url, and + // the loader should be able to recover from that if we send + // a partial response. + notFound(res); + return; } diff --git a/lib/config.json b/lib/config.json index 3e94ad2f0..e9bd9186e 100644 --- a/lib/config.json +++ b/lib/config.json @@ -11,6 +11,9 @@ "tunnelProxy": { "type": "TunnelProxy" } + }, + "staticHandling": { + "prefix": "static" } }, "defaultRoutes" : {