yui-pathogen-encoder
Version:
Enables pathogen encoding in YUI Loader
430 lines (365 loc) • 18.2 kB
JavaScript
/*
* 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
**/
;
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;
}
};