opensphere-build-resolver
Version:
Resolves projects, their dependencies, plugins, and config to the correct arguments for compilation via the Google Closure Compiler, sass/node-sass, and other tools.
386 lines (338 loc) • 11.2 kB
JavaScript
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const glob = require('glob');
const path = require('path');
const resolve = require('resolve');
/**
* If a package is designated as an app.
* @param {Object} pack The package
* @return {boolean} If the package is an app
*/
const isAppPackage = function(pack) {
return pack.build && pack.build.type === 'app';
};
/**
* If a package is designated as a config pack.
* @param {Object} pack The package
* @return {boolean} If the package is a config pack
*/
const isConfigPackage = function(pack) {
return pack.build && pack.build.type === 'config';
};
/**
* If a package is designated as a plugin
* @param {Object} pack The package
* @return {boolean} If the package is a plugin
*/
const isPluginPackage = function(pack) {
return pack.build && pack.build.type === 'plugin';
};
/**
* @param {Object} basePackage The alleged base package
* @param {Object} pluginPackage The alleged plugin package
* @param {Array=} optDepStack Dependancy stack to check pluginPackage is a plugin to the parent
* @return {boolean} If the plugin package is a plugin to the
* base package
*/
const isPluginOfPackage = function(basePackage, pluginPackage, optDepStack) {
if (optDepStack && isPluginPackage(pluginPackage)) {
// Grab the index of the current plugin
const pluginIndex = optDepStack.findIndex((item) => {
return item == pluginPackage.name;
});
// Get the parents name
const parentName = pluginIndex > 0 ? optDepStack[pluginIndex - 1] : '';
// If this is a plugin to the parent
if (pluginPackage.name.indexOf(parentName + '-') === 0) {
// If the parent IS the base package. Then this is a plugin of the package
if (parentName == basePackage.name) {
return true;
} else {
// Since this isnt a plugin to the base package, we only will accept it
// if the parent is a library
const parentPackage = getPackage(parentName);
return parentPackage ? parentPackage.build.type === 'lib' : false;
}
} else {
return false;
}
}
return isPluginPackage(pluginPackage) &&
pluginPackage.name.indexOf(basePackage.name + '-') === 0;
};
/**
* Get the real file system path from a glob path.
* @param {string} glob The glob path
* @return {string} The real path
*/
const realPath = function(glob) {
var wildIndex = glob.indexOf('*');
if (wildIndex) {
var prefix = glob.substr(0, wildIndex);
if (prefix) {
// resolve the portion of the path up to the first wildcard
var suffix = glob.substr(wildIndex);
return path.join(fs.realpathSync(prefix), suffix);
}
// started with a wildcard, return the original glob
return glob;
}
// no wildcard, use the original path
return fs.realpathSync(glob);
};
/**
* @param {string} filePath The path
* @return {string} The path or flattened path, whichever happens to exist
*/
const flattenPath = function(filePath) {
try {
filePath = realPath(filePath);
fs.accessSync(filePath, 'r');
return filePath;
} catch (e) {
}
return filePath.replace(/node_modules.*node_modules/, 'node_modules');
};
/**
* @param {Object<string, number>} map The map of files to priorities
* @return {function(a: string, b: string):number} compare function for sorting
*/
const getPrioritySort = function(map) {
/**
* @param {string} a First item
* @param {string} b Other item
* @return {number} per compare functions
*/
return function(a, b) {
var pa = map[a] || 0;
var pb = map[b] || 0;
return pa - pb;
};
};
/**
* @param {number} depth The depth to indent
* @return {string} The indent string
*/
const getIndent = function(depth) {
var indent = '';
for (var i = 1; i < depth; i++) {
indent += ' ';
}
if (depth > 0) {
indent += ' \u221F ';
}
return indent;
};
/**
* Get the `package.json` as a JSON object for a package.
* @param {string} packageName The package name.
* @return {Object|undefined} The resolved `package.json`, or undefined if not found.
*/
const getPackage = function(packageName) {
try {
return require(path.join(packageName, 'package.json'));
} catch (e) {
}
return undefined;
};
/**
* Resolve the absolute path for a file/directory under `node_modules`.
* @param {string} modulePath The relative path. Should begin with the module name.
* @param {string=} optBasedir Optional paths to resolve module location from.
* @return {string|undefined} The resolved path, or undefined if the module could not be found.
*/
const resolveModulePath = function(modulePath, optBasedir) {
try {
var parts = modulePath.split(/[\\\/]/);
if (parts && parts.length) {
// if the package is scoped, use the first two parts of the path. ie, @scope/package.
var packageName = parts[0].startsWith('@') ? path.join(parts.shift(), parts.shift()) : parts.shift();
var basePath = path.dirname(resolve.sync(path.join(packageName, 'package.json'), {
basedir: optBasedir
}));
// join the remaining path to the resource (if any)
return path.normalize(path.join(basePath, parts.join(path.sep)));
}
} catch (e) {
}
return undefined;
};
/**
* Search a list of files for lines matching a pattern.
* @param {!RegExp} pattern The pattern.
* @param {!Array<string>} files The file paths to search.
* @return {Promise<Array<Object>>} A promise that resolves to the matched files.
*/
const getMatchingLines = function(pattern, files) {
return Promise.reduce(files, function(matches, file) {
return fs.readFileAsync(file, 'utf8').then(function(content) {
var lines = content.split(/[\r\n]+/).filter(function(line) {
return pattern.test(line);
});
if (lines.length) {
matches.push({
file: file,
lines: lines
});
}
return matches;
});
}, []);
};
/**
* Search files in a directory and return lines matching a pattern.
* @param {RegExp} pattern The pattern to match.
* @param {string} directory The directory to search.
* @param {string|undefined} globPattern Glob pattern to filter the list of files to search.
* @return {Promise<Array<Object>>} A promise that resolves to the matched files.
*/
const findLines = function(pattern, directory, globPattern) {
globPattern = globPattern || '**/*';
return new Promise(function(resolve, reject) {
// find all files in the directory matching the glob pattern
glob(path.join(directory, globPattern), function(err, files) {
if (!err) {
// glob will always use backslashes, so map the files to use the system path separator
getMatchingLines(pattern, files.map((f) => path.resolve(f))).then(resolve);
} else {
// directory not found
resolve([]);
}
});
});
};
/**
* Get the sort priority for a package.
* @param {Object} pack The package.
* @param {number} depth The package depth.
* @param {Object} basePackage The base package.
* @return {number} The sort priority.
*/
const getPackagePriority = function(pack, depth, basePackage) {
//
// use priority if specified, so the load order can be controlled by the package.
// if no priority is present, use the resolved depth.
//
var priority = 0;
if (pack) {
if (pack.build && pack.build.priority !== undefined) {
priority = pack.build.priority;
} else {
priority = -depth * 10;
if (isConfigPackage(pack) ||
(isPluginPackage(pack) && isPluginOfPackage(basePackage, pack))) {
priority++;
}
}
}
return priority;
};
const groups = {
BASE: 0,
PLUGIN: 1000,
CONFIG: 10000
};
/**
* @param {?Array<string>} depStack
* @return {number}
*/
const getGroup = function(depStack) {
let group = groups.BASE;
if (depStack) {
let rootPackageName;
let pluginRegex;
let configRegex;
for (let i = 0, n = depStack.length; i < n; i++) {
if (i === 0) {
rootPackageName = depStack[i];
pluginRegex = new RegExp(`^${rootPackageName}-plugin-`);
configRegex = new RegExp(`^${rootPackageName}-config`);
} else {
if (pluginRegex && pluginRegex.test(depStack[i])) {
group = Math.max(group, groups.PLUGIN);
} else if (configRegex && configRegex.test(depStack[i])) {
group = Math.max(group, groups.CONFIG);
}
}
}
}
return group;
};
/**
* Sort config objects in ascending order.
* @param {Object} a First object
* @param {Object} b Second object
* @return {number} The sort order
*/
const priorityGroupDepthSort = function(a, b) {
const an = a.priority || a.group - a.depth;
const bn = b.priority || b.group - b.depth;
return an - bn;
};
/**
* @param {Array<Object>} list
* @return {function(Object, number, Array<string>):Promise}
*/
const getGroupDepthUpdater = function(list) {
return (pack, depth, depStack) => {
list.forEach(function(config) {
if (config.name === pack.name) {
const newGroup = getGroup(depStack);
if (newGroup < config.group) {
config.group = newGroup;
config.depth = depth;
} else if (newGroup === config.group) {
config.depth = Math.max(depth, config.depth);
}
}
});
return Promise.resolve();
};
};
/**
* Update dependencies that have already been resolved.
*
* @param {Object} dependencies The package dependencies.
* @param {Object} resolved Map of resolved dependencies.
* @param {number} depth The current depth.
* @param {Array<string>} depStack The current dependency stack.
* @param {Function} updater The update function.
* @param {Array<string>} updated Packages that have already been updated.
*
* @return {Promise} A promise that resolves when all dependencies have been updated.
*/
const updateDependencies = function(dependencies, resolved, depth, depStack, updater) {
const promises = [];
if (dependencies) {
for (const key in dependencies) {
const resolvedPack = resolved[key];
// Only update a package once to avoid dependency cycles
if (resolvedPack && resolvedPack.build && depStack.indexOf(resolvedPack.name) === -1) {
const newDepth = depth + 1;
const newDepStack = [...depStack, resolvedPack.name];
promises.push(updater(resolvedPack, newDepth, newDepStack));
promises.push(updateDependencies(resolvedPack.dependencies, resolved, newDepth, newDepStack,
updater));
}
}
}
return Promise.all(promises);
};
module.exports = {
findLines: findLines,
isAppPackage: isAppPackage,
isConfigPackage: isConfigPackage,
isPluginPackage: isPluginPackage,
isPluginOfPackage: isPluginOfPackage,
flattenPath: flattenPath,
Groups: groups,
getPrioritySort: getPrioritySort,
getGroup: getGroup,
getGroupDepthUpdater: getGroupDepthUpdater,
getIndent: getIndent,
getPackage: getPackage,
getPackagePriority: getPackagePriority,
priorityGroupDepthSort: priorityGroupDepthSort,
resolveModulePath: resolveModulePath,
updateDependencies: updateDependencies
};