Skip to content

Instantly share code, notes, and snippets.

@imalberto
Created October 18, 2013 00:27
Show Gist options
  • Select an option

  • Save imalberto/7034653 to your computer and use it in GitHub Desktop.

Select an option

Save imalberto/7034653 to your computer and use it in GitHub Desktop.
locator-yui/lib/plugin.js
/*
* 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 */
"use strict";
var libpath = require('path'),
utils = require('./utils'),
shifter = require('./shifter'),
BuilderClass = require('./builder'),
debug = require('debug')('locator:yui'),
description = require('../package.json').description;
/**
`locator-yui` plugin builds and registers yui modules and metadata from the
bundle by `locator`.
Here is an example:
var Locator = require('locator'),
LocatorYUI = require('locator-yui'),
loc;
loc = new Locator({ buildDirectory: __dirname });
loc.plug(new LocatorYUI({ // options here // })).parseBundle(__dirname);
Here is another example with custom settings:
loc.plug(new LocatorYUI({
lint: true,
coverage: true,
silence: false
})
.parseBundle(__dirname);
@class plugin
@static
@uses *path, utils, shifter, builder, *debug
@extensionfor yui
*/
/**
Creates a locator plugin that can analyze locator bundles, build modules
and build loader metadata for all yui modules within the bundle.
@constructor
@static
@param {Object} config Optional plugin configuration
objects that, if passed, will be mix with the default
configuration of the plugin.
@param {Boolean} config.cache Whether or not the shifting process should be cached
to speed up the build process. By default, it is true.
@param {object} config.args Optional custom shifter cli arguments. This will overrule
custom `options` that are translated into shifter arguments.
@param {Boolean} config.lint Optional enable linting in shifter.
@param {Boolean} config.coverage Optional generate `-coverage.js` version of modules in shifter.
@param {Boolean} config.silent Optional run shifter in silent mode.
@param {Boolean} config.quiet Optional run shifter in quiet mode.
@param {String} config.cssproc Optional loader `base` value to preprocess css to readjust urls
for assets to resolve with `base` for the corresponding bundle build directory to make them
work with combo.
@param {RegExp|Function} config.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
`evt.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. Otherwise the file will be skipped.
**/
function PluginClass(config) {
var args = ['--no-global-config'];
config = config || {};
// enable cache by default
if (!config.hasOwnProperty('cache')) {
config.cache = true;
}
if (config.filter && utils.isRegExp(config.filter)) {
// adding support for a regex instead of a functions
config.filter = function (bundle, relativePath) {
return !!config.filter.test(relativePath);
};
}
if (!config.coverage) {
args.push('--no-coverage');
}
if (!config.lint) {
args.push('--no-lint');
}
// if not debug, then let's make shifter to run in silence mode
if (!utils.debugMode || config.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 || config.quiet) {
debug('running shifter in quiet mode');
args.push('--quiet');
}
this.describe = {
summary: description,
types: ['*'],
args: args.concat(config.args || []),
options: config
};
debug('computed arguments for shifter: %s', this.describe.args.join(' '));
// internal cache structure
this._bundles = {};
}
PluginClass.prototype = {
/**
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[bundleName] = this._bundles[bundleName] || {};
this._bundles[bundleName][cacheKey] = mod;
},
/**
Analyze build information and generate loader meta data from it.
@method getLoaderData
@protected
@param {string} bundleName The name of the bundle to be analyzed.
@param {function} filter Optional function to filter modules.
@param {Object} The data generated by BuilderClass, including the js version of
the meta module, as well as the json version.
**/
getLoaderData: function (bundleName, filter) {
var meta = this._bundles[bundleName] || {},
buildMeta = {},
mod,
build,
obj;
for (mod in meta) {
if (meta.hasOwnProperty(mod)) {
for (build in meta[mod].builds) {
if (meta[mod].builds.hasOwnProperty(build)) {
// if there is a filter, and the module doesn't pass it, we should discard it,
// this helps to distinguish by affinity or any other config
if (!filter || filter(meta[mod].name, meta[mod].builds[build].config || {})) {
buildMeta[mod] = buildMeta[mod] || {
name: meta[mod].name,
buildfile: meta[mod].buildfile,
builds: {}
};
buildMeta[mod].builds[build] = meta[mod].builds[build];
}
}
}
}
}
if (!Object.keys(buildMeta).length) {
return;
}
// computing the meta module
obj = new BuilderClass({
name: 'loader-' + bundleName,
group: bundleName
});
obj.compile(buildMeta);
return (obj.data && Object.keys(obj.data.json).length) && obj.data;
},
/**
@method _createServerLoaderData
@protected
@param {Object} bundle
@return {Object} server loader data
**/
_createServerLoaderData: function (bundle) {
var loaderData = this.getLoaderData(bundle.name, function (name, config) {
return (config.affinity !== 'client');
});
return loaderData;
},
/**
@method _createClientLoaderData
@protected
@param {Object} bundle
@param {String} moduleName
@return {Object}
**/
_createClientLoaderData: function (bundle, moduleName) {
var bundleName = bundle.name,
loaderData;
// dealing with client stuff
loaderData = this.getLoaderData(bundleName, function (name, config) {
return (config.affinity !== 'server');
});
if (loaderData) {
// adding meta module (which is synthetic at this point
loaderData.json[moduleName] = {
group: bundleName,
affinity: 'client'
};
}
return loaderData;
},
/**
@method _attachServerLoaderData
@protected
@param {Object} bundle
@param {Object} loaderData
**/
_attachServerLoaderData: function (bundle, loaderData) {
if (loaderData) {
// attaching server loader data into loader for other components to use it
// this helps `express-yui` to use yui modules on the serve side for example
bundle.yui = bundle.yui || {};
bundle.yui.server = loaderData.json;
}
},
/**
@method _attachClientLoaderData
@protected
@param {Object} bundle
@param {Object} api the `api` object from Locator
@param {String} destPath fs path where to write the loader metadata file
@param {Object} loaderData
**/
_attachClientLoaderData: function (bundle, api, destPath, loaderData) {
var bundleName = bundle.name;
if (loaderData) {
// attaching client loader data into loader for other components to use it
// this helps `express-yui` to create seed urls for example
bundle.yui = bundle.yui || {};
bundle.yui.client = loaderData.json;
}
// writing meta module if needed
return loaderData && api.writeFileInBundle(bundleName, destPath, loaderData.js);
},
/**
@method _attachClientMetaData
@protected
@param {Object} bundle
@param {Array} builds
@param {String} moduleName
@param {String} newfile
**/
_attachClientMetaData: function (bundle, builds, moduleName, newfile) {
if (newfile) {
// store a fullpath to the file into the bundle for
// other components to use it
bundle.yui.metaModuleFullpath = newfile;
bundle.yui.metaModuleName = moduleName;
// adding the new meta module into the builds collection
builds.push(newfile);
}
},
/**
Creates a funtion that will execute `shifter` on the given `builds`
configuration.
@method _shiftEverything
@protected
@param {Object} bundle
@param {String} cssproc
@param {Object} builds
@param {Object} shifter
@param {Function} cb
**/
_shiftEverything: function (bundle, cssproc, builds, shifter, cb) {
var self = this,
args = [].concat(self.describe.args);
// if cssproc is enabled, `base` is going to be computed and it is going to be added
// in front of each `url()` in the css modules thru shifter.
if (cssproc) {
args = args.concat('--cssproc', cssproc + '/' + libpath.basename(bundle.buildDirectory));
}
// building files for the bundle
shifter.shiftFiles(builds, {
buildDir: bundle.buildDirectory,
args: args,
cache: self.describe.options.cache
}, function (e) {
cb(e);
});
},
bundleUpdated: function (evt, api) {
var self = this,
bundle = evt.bundle,
bundleName = bundle.name,
moduleName = 'loader-' + bundleName,
destination_path = moduleName + '.js',
cssproc = this.describe.options.cssproc,
meta,
builds,
files;
// getting files to be shifted
files = utils.filterFilesInBundle(bundle, evt.files, self.describe.options.filter);
// getting all build.json that should be shifted
builds = this._buildsInBundle(bundle, files, api.getBundleFiles(bundleName, {
extensions: 'json'
}));
meta = this._bundles[bundleName];
if (!meta || builds.length === 0) {
// no yui module in queue
return;
}
function generateServerData(fulfilled, rejected) {
var loaderData = self._createServerLoaderData(bundle);
fulfilled(loaderData);
}
function generateClientData(fulfilled, rejected) {
var loaderData = self._createClientLoaderData(bundle, moduleName);
fulfilled(loaderData);
}
function attachServerData(loaderData) {
self._attachServerLoaderData(bundle, loaderData);
}
function attachClientData(loaderData) {
return self._attachClientLoaderData(bundle, api, destination_path, loaderData);
}
function attachClientMetaData(newfile) {
self._attachClientMetaData(bundle, builds, moduleName, newfile);
}
function shiftEverything(fulfilled, rejected) {
self._shiftEverything(bundle, cssproc, builds, shifter, function (err) {
if (err) {
rejected(err);
}
fulfilled();
});
}
return api.promise(generateServerData)
.then(attachServerData)
.then(function () {
return api.promise(generateClientData);
})
.then(attachClientData)
.then(attachClientMetaData)
.then(function () {
return api.promise(shiftEverything);
});
},
/**
Analyze modified files and build.json files to infer the list of `build.json`
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 self = this,
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 (libpath.extname(file) === '.js' && libpath.basename(file) !== 'loader-' + bundleName + '.js') {
mod = shifter._checkYUIModule(file);
if (mod) {
self.register(bundleName, file, mod);
builds[file] = true;
}
}
}
// looking for build.json
for (i = 0; i < jsonFiles.length; i += 1) {
if (libpath.basename(jsonFiles[i]) === 'build.json') {
mod = shifter._checkBuildFile(jsonFiles[i]);
if (mod) {
dir = libpath.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 (libpath.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();
}
// getMetaModule
// 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.
};
module.exports = PluginClass;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment