gulp-recipe-loader
Version:
Gulp environment with receipe modules autoloading with hooks
304 lines (261 loc) • 10.2 kB
JavaScript
var findup = require('findup-sync');
var loadPlugins = require('gulp-load-plugins');
var multimatch = require('multimatch');
var _ = require('lodash');
var path = require('path');
var globby = require('globby');
var gutil = require('gulp-util');
var fs = require('fs');
// workaround for linked development modules
var prequire = require('parent-require');
var requireFn = function (module) {
try {
return require(module);
}
catch(e) {
if(e.code === 'MODULE_NOT_FOUND') {
try {
return prequire(module);
}
catch(e2) {} // throw original error
}
throw e;
}
};
// error handling
function formatError(e) {
if (!e.err) {
return e.message;
}
// PluginError
if (typeof e.err.showStack === 'boolean') {
return e.err.toString();
}
// normal error
if (e.err.stack) {
return e.err.stack;
}
// unknown (string, number, etc.)
return new Error(String(e.err)).stack;
}
// Necessary to get the current `module.parent` and resolve paths correctly when required from multiple places.
delete require.cache[__filename];
var parentDir = path.dirname(module.parent.filename);
function camelize(str) {
return str.replace(/-(\w)/g, function(m, p1) {
return p1.toUpperCase();
});
}
/**
* Checks if at least one of devDependency directory exists
* (to distinguish production and dev npm install)
*
* @param dependency
* @returns {*}
*/
function devDependenciesExists(dependency) {
var depExists;
if (!dependency) {
return false;
}
try {
depExists = fs.statSync(path.join('node_modules', dependency)).isDirectory();
}
catch(e) {
depExists = false;
}
return depExists;
}
// require gulp from outside world to prevent multiple instances. This could also be a peer dependency,
// but it gets tricky with multiple layers of modules
module.exports = function (gulp, options) {
if(!options) {
options = {};
}
var allRecipesInitalized = false;
// set default options
options = _.merge({
tasks: {},
paths: {},
order: {},
sources: {
defaultBase: '.'
},
recipesPattern: 'gulp-recipes/{*/main.js,*.js}',
rename: {}
}, options);
// read package.json or get it from options
var packageFile = options.package || findup('package.json', {cwd: parentDir});
if (typeof packageFile === 'string') {
packageFile = require(packageFile);
}
// check for devDependencies
var loadDevDependencies = devDependenciesExists(_.findKey(_.result(packageFile,'devDependencies')));
// lazy load all non-recipe plugins from package.json
var $ = loadPlugins({
pattern: ['*', '!gulp-recipe-*', '!gulp'],
scope: loadDevDependencies ? ['dependencies', 'devDependencies'] : ['dependencies'],
replaceString: 'gulp-',
camelize: true,
lazy: true,
config: packageFile,
rename: options.rename,
requireFn: requireFn
});
// force single gulp instance
Object.defineProperty($, 'gulp', {value: gulp});
// publish some internal packages to modules, if not published already
_.each(['event-stream', 'lodash', 'through2', 'gulp-watch'], function (internal) {
var camelized = camelize(internal.replace('gulp-',''));
if(!$.hasOwnProperty(camelized)) {
Object.defineProperty($, camelized, {
get: function() {
return require(internal);
}
});
}
});
// load utility functions
$.gutil = gutil;
$.utils = require('./utils')($);
$.lazypipe = require('./lib/lazypipe').lazypipe;
// resolve external recipe directories
var externPattern = ['gulp-recipe-*', '!gulp-recipe-loader'];
var externScope = loadDevDependencies ? ['dependencies', 'devDependencies'] : ['dependencies'];
var replaceString = 'gulp-recipe-';
var pluginNames = _.reduce(externScope, function(result, prop) {
return result.concat(Object.keys(packageFile[prop] || {}));
}, []);
var recipeDirectory = _.transform(multimatch(pluginNames, externPattern), function (obj, name) {
var renamed = options.rename[name] || camelize(name.replace(replaceString, ''));
obj[renamed] = path.join(parentDir, 'node_modules', name);
}, {});
// lazy load all recipes from package.json
var extPluginsConfig = {
pattern: externPattern,
scope: externScope,
replaceString: replaceString,
camelize: true,
lazy: false,
config: packageFile,
rename: options.rename,
requireFn: requireFn
};
var recipes = loadPlugins(extPluginsConfig);
// load all recipes from local project directory
var localRecipes = _.object(_.map(globby.sync(options.recipesPattern), function (module) {
var recipeName = path.basename(module, '.js');
if(recipeName === 'main') {
recipeName = path.basename(path.dirname(module));
}
return [recipeName, require(path.join(parentDir, module))];
}));
// create a way to extend lib getter object with modules local libs, prefer local versions
var LibsProto = function () {};
LibsProto.prototype = $;
var localLibBuilder = function (recipeName) {
var localLibs = new LibsProto();
var dir = recipeDirectory[recipeName];
if(dir) {
// find internal package.json
var localPackageFile = require(findup('package.json', {cwd: dir}));
// load recipe dependencies
var localConfig = _.defaults({
pattern: '*',
replaceString: 'gulp-',
config: localPackageFile,
lazy: true,
requireFn: function (name) {
// resolve inner dependency path
var depPath = path.join(dir, 'node_modules', name);
try {
// direct module require may fail, if dedupe was done
return requireFn(depPath);
}
catch(e) {
if(e.code === 'MODULE_NOT_FOUND') {
// for that occasions a regular require is sufficient
try {
return requireFn(name);
}
catch(e2) {} // throw original error
}
throw e;
}
}
}, extPluginsConfig);
var localPlugins = loadPlugins(localConfig);
// pass lazy properties of loaded dependencies into local $ object
_.each(Object.getOwnPropertyNames(localPlugins), function (prop) {
Object.defineProperty(localLibs, prop, {
get: function () {
return localPlugins[prop];
}
});
});
}
return localLibs;
};
var processSourceHook = _.once(function () {
// This hook function is evaluated on first source pipe usage,
// which means inside a specific task, after all recipes are successfuly loaded.
// It's safe to grab pipe hooks from there, unles a specific plugin
// decides to evaluate pipe before initialization. Yell at it!
if(!allRecipesInitalized) {
throw new $.utils.RecipeError('Stream created before all recipes are initialized.');
}
return $.utils.sequentialLazypipe($.utils.getPipes('processSource'));
});
// prepare lazy initializers for recipes, so it may be cross referenced
$.recipes = {};
_.each(_.merge(recipes, localRecipes), function (recipeDef, key) {
Object.defineProperty($.recipes, key, {
enumerable: true,
get: _.once(function () {
if(_.isFunction(recipeDef)) {
recipeDef = { recipe: recipeDef };
}
var localLibs, localConfig, sources;
try {
// load module's local dependencies
localLibs = localLibBuilder(key);
// run config reader on given config
localConfig = recipeDef.configReader ? recipeDef.configReader(localLibs, _.cloneDeep(options)) : _.cloneDeep(options);
// prepare source pipes
if(localConfig.sources) {
if(_.isUndefined(localConfig.sources.defaultBase) && options.sources) {
localConfig.sources.defaultBase = options.sources.defaultBase;
}
sources = localLibs.utils.makeSources(localConfig.sources, function () {
return processSourceHook()();
});
}
return recipeDef.recipe(localLibs, localConfig, sources);
}
catch(e) {
// catch recipe errors
if(e instanceof $.utils.RecipeError) {
throw new $.utils.NamedRecipeError(key, e);
}
else {
throw new $.utils.NamedRecipeError(key, e, {showStack: true});
}
}
})
});
});
// force load all recipes
_.each(Object.getOwnPropertyNames($.recipes), function (key) {
try {
return $.recipes[key];
}
catch(e) {
var msg = formatError({err: e});
$.gutil.log(msg);
process.exit(1);
}
});
allRecipesInitalized = true;
return $;
};
;