UNPKG

yui-pathogen-encoder

Version:

Enables pathogen encoding in YUI Loader

430 lines (365 loc) 18.2 kB
/* * Copyright (c) 2013, Yahoo! Inc. All rights reserved. * Copyrights licensed under the New BSD License. * See the accompanying LICENSE file for terms. */ /*jslint node: true, nomen: true */ /** The `yui.loader` extension exposes a locator plugin to build and register yui meta modules from shifter module metadata. @module yui @submodule loader **/ "use strict"; var path = require('path'), utils = require('./utils'), debug = require('debug')('express:yui:loader'); /** The `yui.loader` extension exposes a locator plugin to build and register yui modules and metadata. Here is an example: var plugin = app.yui.plugin(); You can also specify whether or not the bundles should be registered as a group on loader and modules in a bundle should be attached into a Y instance created for the server side, as well as a bunch of custom settings. Here is another example: var plugin = app.yui.plugin({ registerGroup: true, registerServerModules: true, lint: true, coverage: true, silence: false }); @class loader @static @uses *path, utils, shifter, *debug @extensionfor yui */ module.exports = { /** Registers information about modules that will be used to generate the bundle meta. @method register @protected @param {string} bundleName The bundle name to be registered. @param {string} cacheKey The cache key for the file that generates mod. @param {Object} mod The module information generated by the shifter module. **/ register: function (bundleName, cacheKey, mod) { this._bundles = this._bundles || {}; this._bundles[bundleName] = this._bundles[bundleName] || {}; this._bundles[bundleName][cacheKey] = mod; }, /** Creates a locator plugin that can analyze locator bundles, build modules and build loader metadata for all yui modules within the bundle. @method plugin @public @param {Object} options Optional plugin configuration objects that, if passed, will be mix with the default configuration of the plugin. @param {Boolean} options.registerGroup Whether or not the bundle should be registered as a loader group to be used from the client and server. Default to false. @param {Boolean|Function} options.registerServerModules Whether or not server modules should be provisioned to be loaded thru `app.yui.use()` on the server side. Default to false. @param {Boolean} options.cache Whether or not the shifting process should be cached to speed up the build process. By default, it is true. @param {string} options.buildDir Optional custom filesystem path for the output folder of the shifter. Default to an internal computation based on `locator.buildDir`. @param {object} options.args Optional custom shifter cli arguments. This will overrule custom `options` that are translated into shifter arguments. @param {object} options.lint Optional enable linting in shifter. @param {object} options.coverage Optional generate `-coverage.js` version of modules in shifter. @param {object} options.silent Optional run shifter in silent mode. @param {object} options.quiet Optional run shifter in quiet mode. @param {boolean} options.cssproc Optional flag to preprocess css to readjust urls for assets to resolve with `base` for the corresponding group to make them work with combo. @param {RegExp|Function} options.filter optional regex or function to execute for each `evt.files`. If no `filter` is supplied, all modified files will be shifted. If the regex is provided, it will be tested against every `evn.files`, testing the relative path to determine if the file should be shifted or not. In a function if provided, the function will be called for every `evt.files` with the following arguments: @param {Object} filter.bundle the current bundle to where the file belongs to @param {Object} filter.relativePath the relative path to the file from the bundle @param {boolean} filter.return Return true to indicate that the file should be shifted. Otherise the file will be skipped. @return {object} locator plugin **/ plugin: function (options) { var yui = this, args = ['--no-global-config']; options = options || {}; if (options.filter && utils.isRegExp(options.filter)) { // adding support for a regex instead of a functions options.filter = function (bundle, relativePath) { return !!options.filter.test(relativePath); }; } if (!options.coverage) { args.push('--no-coverage'); } if (!options.lint) { args.push('--no-lint'); } // if not debug, then let's make shifter to run in silence mode if (!utils.debugMode || options.silent) { debug('running shifter in silent mode'); args.push('--silent'); } // if not debug, then let's make shifter to run in quiet mode if (!utils.debugMode || options.quiet) { debug('running shifter in quiet mode'); args.push('--quiet'); } return { describe: utils.extend({ summary: 'Plugin to build YUI Loader metadata for a bundle', types: ['*'], cache: true, args: args }, options), bundleUpdated: function (evt, api) { var self = this, bundle = evt.bundle, bundleName = bundle.name, moduleName = 'loader-' + bundleName, destination_path = moduleName + '.js', meta, builds, files; // producing the yui build directory for the bundle if (!bundle.yuiBuildDirectory) { // augmenting bundle obj with more metadata about yui bundle.yuiBuildDirectory = options.buildDir || path.resolve(bundle.buildDirectory); } // getting files to be shifted files = yui._filterFilesInBundle(bundle, evt.files, self.describe.filter); // getting all build.json that should be shifted builds = yui._buildsInBundle(bundle, files, api.getBundleFiles(bundleName, { extensions: 'json' })); meta = yui._bundles && yui._bundles[bundleName]; if (!meta || builds.length === 0) { // this bundle does not have any yui module registered return; } // storing name of the default bundle if the current // bundle has files and is the root bundle from locator if (api.getRootBundleName() === bundleName) { yui._defaultBundle = bundleName; } return api.promise(function (fulfilled, rejected) { var server, client, serverMeta = {}, clientMeta = {}, mod, build, affinity; // allocating metas for client and server for (mod in meta) { if (meta.hasOwnProperty(mod)) { for (build in meta[mod].builds) { if (meta[mod].builds.hasOwnProperty(build)) { affinity = meta[mod].builds[build].config && meta[mod].builds[build].config.affinity; if (affinity !== 'client') { // if not marked as client, it should be available on the server serverMeta[mod] = serverMeta[mod] || { name: meta[mod].name, buildfile: meta[mod].buildfile, builds: {} }; serverMeta[mod].builds[build] = meta[mod].builds[build]; } if (affinity !== 'server') { // if not marked as server, it should be available on the client clientMeta[mod] = clientMeta[mod] || { name: meta[mod].name, buildfile: meta[mod].buildfile, builds: {} }; clientMeta[mod].builds[build] = meta[mod].builds[build]; } } } } } // defining the synthetically created meta module for client, it is not needed on the server clientMeta[moduleName] = clientMeta[moduleName] || { name: moduleName, buildfile: destination_path, builds: {} }; clientMeta[moduleName].builds[moduleName] = { name: moduleName, config: { affinity: 'client' } }; // computing the meta module client = new (yui.BuilderClass)({ name: moduleName, group: bundleName }); client.compile(clientMeta); return api.writeFileInBundle(bundleName, destination_path, client.data.js).then(function (newfile) { var args = []; // automatically registering new groups to be served if (options.registerGroup) { yui.registerGroup(bundle.name, bundle.yuiBuildDirectory, newfile); // automatically register modules into a server instance if needed if (options.registerServerModules) { // computing the metas for the server side server = new (yui.BuilderClass)({ name: moduleName + '-server', group: bundleName }); server.compile(serverMeta); // registering server affinity modules on the server yui.registerModules(bundleName, (utils.isFunction(options.registerServerModules) ? options.registerServerModules(bundleName, server.data.json) : server.data.json)); // attach any module added to `bundle.useServerModules` by // any plugin. this is very useful for plugins that compile templates, // where those templates should be attached automatically if (bundle.useServerModules) { yui.attachModules(bundle.useServerModules); } } } // adding the new meta module into the builds collection builds.push(newfile); // if cssproc is enabled, `base` is going to be added // in front of each `url()` in the css modules thru shifter. args = args.concat(yui._cssprocInBundle(bundle, options.cssproc)); // building files for the bundle yui.shiftFiles(builds, { buildDir: bundle.yuiBuildDirectory, args: args.concat(self.describe.args), cache: self.describe.cache }, function (e) { if (e) { rejected(e); return; } fulfilled(); }); }, rejected).then(null, rejected); }); } }; }, /** Analyze modified files and build.json files to infer the list of files that should be shifted. @method _buildsInBundle @protected @param {Object} bundle the bundle to be analyzed @param {array} modifiedFiles The filesystem path for all modified files in bundle. @param {array} jsonFiles The filesystem path for all json files in bundle. @return {array} The filesystem path for all files that should be shifted using shifter **/ _buildsInBundle: function (bundle, modifiedFiles, jsonFiles) { var bundleName = bundle.name, file, dir, mod, i, m, builds = {}; // validating and ordering the list of files to make sure they are processed // in the same order every time to generate the metas. If the order is not // preserved, your CI might generate a re-ordered meta module that might // invalidate cache due to the nature of the promises used in locator that // are async by nature. modifiedFiles = (modifiedFiles && modifiedFiles.sort()) || []; jsonFiles = (jsonFiles && jsonFiles.sort()) || []; // looking for modified yui modules for (m = 0; m < modifiedFiles.length; m += 1) { file = modifiedFiles[m]; // there is not need to add loader meta module into builds collection if (path.extname(file) === '.js' && path.basename(file) !== 'loader-' + bundleName + '.js') { mod = this._checkYUIModule(file); if (mod) { this.register(bundleName, file, mod); builds[file] = true; } } } // looking for build.json for (i = 0; i < jsonFiles.length; i += 1) { if (path.basename(jsonFiles[i]) === 'build.json') { mod = this._checkBuildFile(jsonFiles[i]); if (mod) { dir = path.dirname(jsonFiles[i]); for (m = 0; m < modifiedFiles.length; m += 1) { file = modifiedFiles[m]; // if build.json itself was modified, we should not skip if (file === jsonFiles[i]) { builds[jsonFiles[i]] = true; } // if there is a modified .js file in the range, // and it is not under build directory, // we should shift it, just in case // note: this is not ideal, but we don't know how to analyze a build.json to really // know when to build it or not, so we need to build it everytime if (path.extname(file) === '.js' && file.indexOf(dir) === 0 && file.indexOf(bundle.buildDirectory) === -1) { builds[jsonFiles[i]] = true; } } this.register(bundleName, jsonFiles[i], mod); } } } return Object.keys(builds).sort(); }, /** Set the proper shifter configuration for images in the css modules to be processed based on the group configuration. @method _cssprocInBundle @protected @param {Object} bundle the bundle to be analyzed @param {boolean} cssproc Whether or not we should add cssproc to shifter arguments. @return {array} the arguments to be added to shifter or empty array **/ _cssprocInBundle: function (bundle, cssproc) { var args = [], bundleName = bundle.name, config; if (cssproc) { config = this.config(); if (config.groups && config.groups[bundleName] && config.groups[bundleName].base) { args.push('--cssproc', config.groups[bundleName].base); } else { throw new Error('Invalid use of `cssproc` option. Group `' + bundleName + '` ' + 'does not have `base` defined, this is required to be used as the ' + 'base url for all assets.'); } } return args; }, /** Get the fullPath of all modified files that should be shifted. @method _filterFilesInBundle @protected @param {Object} bundle the bundle to be analyzed @param {object} the original `evt.files` from locator plugin @param {function} filter Custom function to analyze each file @return {array} the list of files to be shifted or empty array **/ _filterFilesInBundle: function (bundle, list, filter) { var files = []; // getting the fullPath of all modified files that should be shifted in a form of an array Object.keys(list || {}).forEach(function (element) { // filtering out files based on filder if neded if (!filter || filter(bundle, list[element].relativePath)) { // producing an array of fullPath values files.push(list[element].fullPath); } }); return files; } };