eyeglass
Version:
Sass modules for npm.
722 lines • 29.9 kB
JavaScript
"use strict";
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
Object.defineProperty(exports, "__esModule", { value: true });
var archy = require("archy");
var path = require("path");
var semver = require("semver");
var semver_1 = require("semver");
var debug = require("../util/debug");
var packageUtils = require("../util/package");
var resolve_1 = require("../util/resolve");
var SimpleCache_1 = require("../util/SimpleCache");
var URI_1 = require("../util/URI");
var EyeglassModule_1 = require("./EyeglassModule");
var merge = require("lodash.merge");
var typescriptUtils_1 = require("../util/typescriptUtils");
var heimdall = require("heimdalljs");
var perf_1 = require("../util/perf");
// XXX For some weird reason importing ../Eyeglass to use the static VERSION constant doesn't work.
// XXX I get undefined from importing Eyeglass instead of the class I'm expecting.
// eslint-disable-next-line
var pkg = require("../../package.json");
var EYEGLASS_VERSION = new semver_1.SemVer(pkg.version);
exports.ROOT_NAME = ":root";
var BOUNDARY_VERSIONS = [
new semver_1.SemVer("1.6.0"),
new semver_1.SemVer(EYEGLASS_VERSION),
new semver_1.SemVer("2.9.9"),
new semver_1.SemVer("3.0.0"),
new semver_1.SemVer("3.9.9"),
new semver_1.SemVer("4.0.0")
];
var globalModuleCache = new SimpleCache_1.SimpleCache();
var globalModulePackageCache = new SimpleCache_1.SimpleCache();
function resetGlobalCaches() {
globalModuleCache.purge();
globalModulePackageCache.purge();
}
exports.resetGlobalCaches = resetGlobalCaches;
/**
* Discovers all of the modules for a given directory
*
* @constructor
* @param {String} dir - the directory to discover modules in
* @param {Array} modules - the explicit modules to include
* @param {Boolean} useGlobalModuleCache - whether or not to use the global module cache
*/
var EyeglassModules = /** @class */ (function () {
function EyeglassModules(dir, config, modules) {
var _this = this;
var timer = heimdall.start("eyeglass:modules");
try {
this.config = config;
var useGlobalModuleCache = config.eyeglass.useGlobalModuleCache;
this.issues = {
dependencies: {
versions: [],
missing: []
},
engine: {
missing: [],
incompatible: []
}
};
this.cache = {
access: new SimpleCache_1.SimpleCache(),
modules: useGlobalModuleCache ? globalModuleCache : new SimpleCache_1.SimpleCache(),
modulePackage: useGlobalModuleCache ? globalModulePackageCache : new SimpleCache_1.SimpleCache(),
};
// find the nearest package.json for the given directory
dir = packageUtils.findNearestPackage(path.resolve(dir));
// resolve the current location into a module tree
var moduleTree = this.resolveModule(dir, true);
// if any modules were passed in, add them to the module tree
if (modules && modules.length) {
var discoverTimer = heimdall.start("eyeglass:modules:discovery");
try {
moduleTree.dependencies = modules.reduce(function (dependencies, mod) {
var resolvedMod = new EyeglassModule_1.default(merge(mod, {
isEyeglassModule: true
}), _this.discoverModules.bind(_this));
dependencies[resolvedMod.name] = resolvedMod;
return dependencies;
}, moduleTree.dependencies);
}
finally {
discoverTimer.stop();
}
}
var resolutionTimer = heimdall.start("eyeglass:modules:resolution");
try {
// convert the tree into a flat collection of deduped modules
var collection_1 = this.dedupeModules(flattenModules(moduleTree));
// expose the collection
this.collection = collection_1;
// convert the collection object into a simple array for easy iteration
this.list = Object.keys(collection_1).map(function (name) { return collection_1[name]; });
// prune and expose the tree
this.tree = this.pruneModuleTree(moduleTree);
// set the current projects name
this.projectName = moduleTree.name;
// expose a convenience reference to the eyeglass module itself
this.eyeglass = this.find("eyeglass");
// check for any issues we may have encountered
this.checkForIssues();
}
catch (e) {
// typescript needs this catch & throw to convince it that the instance properties are initialized.
throw e;
}
finally {
resolutionTimer.stop();
}
/* istanbul ignore next - don't test debug */
debug.modules && debug.modules("discovered modules\n\t" + this.getGraph().replace(/\n/g, "\n\t"));
}
catch (e) {
// typescript needs this catch & throw to convince it that the instance properties are initialized.
throw e;
}
finally {
timer.stop();
}
}
/**
* initializes all of the modules with the given engines
*
* @param {Eyeglass} eyeglass - the eyeglass instance
* @param {Function} sass - the sass engine
*/
EyeglassModules.prototype.init = function (eyeglass, sass) {
this.list.forEach(function (mod) { return mod.init(eyeglass, sass); });
};
/**
* Checks whether or not a given location has access to a given module
*
* @param {String} name - the module name to find
* @param {String} origin - the location of the originating request
* @returns {Object} the module reference if access is granted, null if access is prohibited
*/
EyeglassModules.prototype.access = function (name, origin) {
var mod = this.find(name);
// if the module exists and we can access the module from the origin
if (mod && this.canAccessModule(name, origin)) {
return mod;
}
else {
// if not, return null
return null;
}
};
/**
* Finds a module reference by the module name
*
* @param {String} name - the module name to find
* @returns {Object} the module reference
*/
EyeglassModules.prototype.find = function (name) {
return this.getFinalModule(name);
};
Object.defineProperty(EyeglassModules.prototype, "modulePathMap", {
/**
* Creates, caches and returns a mapping of filesystem locations to eyeglass
* modules.
*/
get: function () {
var e_1, _a;
if (this._modulePathMap) {
return this._modulePathMap;
}
else {
var names = Object.keys(this.collection);
var modulePathMap = {};
try {
for (var names_1 = __values(names), names_1_1 = names_1.next(); !names_1_1.done; names_1_1 = names_1.next()) {
var name = names_1_1.value;
var mod = this.collection[name];
modulePathMap[mod.path] = mod;
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (names_1_1 && !names_1_1.done && (_a = names_1.return)) _a.call(names_1);
}
finally { if (e_1) throw e_1.error; }
}
this._modulePathMap = modulePathMap;
return this._modulePathMap;
}
},
enumerable: true,
configurable: true
});
/**
* Finds the most specific eyeglass module that contains the given filesystem
* location. It does this by walking up the directory structure and looking
* to see if it finds the main directory of an eyeglass module.
*/
EyeglassModules.prototype.findByPath = function (location) {
var pathMap = this.modulePathMap;
// This is the only filesystem operation: we have to make sure
// we're working with real path locations because the module directories
// are also only real paths. This means that sass files that are sym-linked
// into a subdirectory of an eyeglass module will not resolve against that
// module. (Sym-linking an eyeglass module itself is supported.)
var parentLocation = perf_1.realpathSync(location);
do {
location = parentLocation;
var mod = pathMap[location];
if (mod) {
return mod;
}
parentLocation = path.dirname(location);
} while (parentLocation != location);
return null;
};
/**
* Returns a formatted string of the module hierarchy
*
* @returns {String} the module hierarchy
*/
EyeglassModules.prototype.getGraph = function () {
var hierarchy = getHierarchy(this.tree);
hierarchy.label = this.getDecoratedRootName();
return archy(hierarchy);
};
/**
* resolves the module and it's dependencies
*
* @param {String} pkgPath - the path to the modules package.json location
* @param {Boolean} isRoot - whether or not it's the root of the project
* @returns {Object} the resolved module definition
*/
EyeglassModules.prototype.resolveModule = function (pkgPath, isRoot) {
var _this = this;
if (isRoot === void 0) { isRoot = false; }
var cacheKey = "resolveModule~" + pkgPath + "!" + !!isRoot;
return this.cache.modules.getOrElse(cacheKey, function () {
var pkg = packageUtils.getPackage(pkgPath);
var isEyeglassMod = EyeglassModule_1.default.isEyeglassModule(pkg.data);
// if the module is an eyeglass module OR it's the root project
if (isEyeglassMod || (pkg.data && isRoot)) {
// return a module reference
return new EyeglassModule_1.default({
isEyeglassModule: isEyeglassMod,
path: path.dirname(pkg.path)
}, _this.discoverModules.bind(_this), isRoot);
}
else {
return;
}
});
};
/**
* dedupes a collection of modules to a single version
*
* @this {EyeglassModules}
*
* @param {Object} module - the collection of modules
* @returns {Object} the deduped module collection
*/
EyeglassModules.prototype.dedupeModules = function (modules) {
var e_2, _a, e_3, _b;
var deduped = {};
var otherVersions = new Array();
try {
for (var _c = __values(Object.keys(modules)), _d = _c.next(); !_d.done; _d = _c.next()) {
var name = _d.value;
var secondNewestModule = void 0;
var newestModule = void 0;
try {
for (var _e = (e_3 = void 0, __values(modules[name])), _f = _e.next(); !_f.done; _f = _e.next()) {
var m = _f.value;
if (!newestModule) {
newestModule = m;
}
else {
if (semver.compare(m.semver, newestModule.semver) > 0) {
if (secondNewestModule) {
otherVersions.push(secondNewestModule);
}
secondNewestModule = newestModule;
newestModule = m;
}
else {
if (secondNewestModule && semver.compare(m.semver, secondNewestModule.semver) > 0) {
otherVersions.push(secondNewestModule);
secondNewestModule = m;
}
else if (secondNewestModule) {
otherVersions.push(m);
}
else {
secondNewestModule = m;
}
}
}
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_f && !_f.done && (_b = _e.return)) _b.call(_e);
}
finally { if (e_3) throw e_3.error; }
}
// In case the app and a dependency have the same name, we discard the app
// Because they're not the same thing.
if (secondNewestModule && newestModule.isRoot) {
newestModule = secondNewestModule;
}
else if (secondNewestModule) {
otherVersions.push(secondNewestModule);
}
deduped[name] = newestModule;
// check for any version issues
this.issues.dependencies.versions.push.apply(this.issues.dependencies.versions, getDependencyVersionIssues(otherVersions, deduped[name]));
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
}
finally { if (e_2) throw e_2.error; }
}
return deduped;
};
/**
* checks for any issues in the modules we've discovered
*
* @this {EyeglassModules}
*
*/
EyeglassModules.prototype.checkForIssues = function () {
var _this = this;
this.list.forEach(function (mod) {
// We don't check the app root for issues unless it declares itself to be
// an eyeglass module. (because the app doesn't have to be a well-formed
// eyeglass module.)
if (mod.isRoot && !mod.isEyeglassModule) {
return;
}
// check engine compatibility
if (!mod.eyeglass || !mod.eyeglass.needs) {
// if `eyeglass.needs` is not present...
// add the module to the missing engines list
_this.issues.engine.missing.push(mod);
}
else if (!_this.isCompatibleWithThisEyeglass(mod.eyeglass.needs)) {
// if the current version of eyeglass does not satisfy the need...
// add the module to the incompatible engines list
_this.issues.engine.incompatible.push(mod);
}
});
};
EyeglassModules.prototype.isCompatibleWithThisEyeglass = function (needs) {
var assertCompatSpec = this.config.eyeglass.assertEyeglassCompatibility;
// If we don't have a forced compat version just check against the module
if (!assertCompatSpec) {
return semver.satisfies(EYEGLASS_VERSION, needs);
}
// We only use the forced compat version if it is functionally higher than
// the module's needed version
var minModule = semver.minSatisfying(BOUNDARY_VERSIONS, needs);
var minCompat = semver.minSatisfying(BOUNDARY_VERSIONS, assertCompatSpec);
if (minModule === null || minCompat === null || semver.gt(minModule, minCompat)) {
return semver.satisfies(EYEGLASS_VERSION, needs);
}
else {
return semver.satisfies(EYEGLASS_VERSION, assertCompatSpec + " || " + needs);
}
};
/**
* rewrites the module tree to reflect the deduped modules
*
* @this {EyeglassModules}
*
* @param {Object} moduleTree - the tree to prune
* @returns {Object} the pruned tree
*/
EyeglassModules.prototype.pruneModuleTree = function (moduleTree) {
var e_4, _a;
var finalModule = moduleTree.isEyeglassModule && this.find(moduleTree.name);
// normalize the branch
var branch = {
name: finalModule && finalModule.name || moduleTree.name,
version: (finalModule && finalModule.version || moduleTree.version),
path: finalModule && finalModule.path || moduleTree.path,
dependencies: undefined
};
// if the tree has dependencies
var dependencies = moduleTree.dependencies;
if (dependencies) {
// copy them into the branch after recursively pruning
branch.dependencies = {};
try {
for (var _b = __values(Object.keys(dependencies)), _c = _b.next(); !_c.done; _c = _b.next()) {
var name = _c.value;
branch.dependencies[name] = this.pruneModuleTree(dependencies[name]);
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_4) throw e_4.error; }
}
}
return branch;
};
/**
* resolves the eyeglass module itself
*
* @returns {Object} the resolved eyeglass module definition
*/
EyeglassModules.prototype.getEyeglassSelf = function () {
var eyeglassDir = path.resolve(__dirname, "..", "..");
var eyeglassPkg = packageUtils.getPackage(eyeglassDir);
var resolvedPkg = resolve_1.default(eyeglassPkg.path, eyeglassPkg.path, eyeglassDir);
return this.resolveModule(resolvedPkg, false);
};
/**
* discovers all the modules for a given set of options
*
* @param {Object} options - the options to use
* @returns {Object} the discovered modules
*/
EyeglassModules.prototype.discoverModules = function (options) {
var _this = this;
var pkg = options.pkg || packageUtils.getPackage(options.dir);
var dependencies = {};
if (!(options.isRoot || EyeglassModule_1.default.isEyeglassModule(pkg.data))) {
return null;
}
// if there's package.json contents...
/* istanbul ignore else - defensive conditional, don't care about else-case */
if (pkg.data) {
merge(dependencies,
// always include the `dependencies` and `peerDependencies`
pkg.data.dependencies, pkg.data.peerDependencies,
// only include the `devDependencies` if isRoot
options.isRoot && pkg.data.devDependencies);
}
// for each dependency...
var dependentModules = Object.keys(dependencies).reduce(function (modules, dependency) {
// resolve the package.json
var resolvedPkg = _this.resolveModulePackage(packageUtils.getPackagePath(dependency), pkg.path, URI_1.URI.system(options.dir));
if (!resolvedPkg) {
// if it didn't resolve, they likely didn't `npm install` it correctly
_this.issues.dependencies.missing.push(dependency);
}
else {
// otherwise, set it onto our collection
var resolvedModule = _this.resolveModule(resolvedPkg);
if (resolvedModule) {
modules[resolvedModule.name] = resolvedModule;
}
}
return modules;
}, {});
// if it's the root...
if (options.isRoot) {
// ensure eyeglass itself is a direct dependency
dependentModules["eyeglass"] = dependentModules["eyeglass"] || this.getEyeglassSelf();
}
return Object.keys(dependentModules).length ? dependentModules : null;
};
/**
* resolves the package for a given module
*
* @see resolve()
*/
EyeglassModules.prototype.resolveModulePackage = function (id, parent, parentDir) {
var cacheKey = "resolveModulePackage~" + id + "!" + parent + "!" + parentDir;
return this.cache.modulePackage.getOrElse(cacheKey, function () {
try {
var startTime = new Date();
var resolution = resolve_1.default(id, parent, parentDir);
var endTime = new Date();
var delta = startTime.valueOf() - endTime.valueOf();
if (delta > 1000) {
console.log("SLOW RESOLUTION (" + delta + "): " + cacheKey);
}
return resolution;
}
catch (e) {
/* istanbul ignore next - don't test debug */
debug.modules && debug.modules('failed to resolve module package %s', e);
return;
}
});
};
/**
* gets the final module from the collection
*
* @this {EyeglassModules}
*
* @param {String} name - the module name to find
* @returns {Object} the module reference
*/
EyeglassModules.prototype.getFinalModule = function (name) {
return this.collection[name];
};
/**
* gets the root name and decorates it
*
* @this {EyeglassModules}
*
* @returns {String} the decorated name
*/
EyeglassModules.prototype.getDecoratedRootName = function () {
return exports.ROOT_NAME + ((this.projectName) ? "(" + this.projectName + ")" : "");
};
/**
* whether or not a module can be accessed by the origin request
*
* @this {EyeglassModules}
*
* @param {String} name - the module name to find
* @param {String} origin - the location of the originating request
* @returns {Boolean} whether or not access is permitted
*/
EyeglassModules.prototype.canAccessModule = function (name, origin) {
var _this = this;
// eyeglass can be imported by anyone, regardless of the dependency tree
if (name === "eyeglass") {
return true;
}
var canAccessFrom = function (origin) {
// find the nearest package for the origin
var mod = _this.findByPath(origin);
if (!mod) {
throw new Error("No module found containing '" + origin + "'.");
}
var modulePath = mod.path;
var cacheKey = modulePath + "!" + origin;
return _this.cache.access.getOrElse(cacheKey, function () {
// find all the branches that match the origin
var branches = findBranchesByPath(_this.tree, modulePath);
var canAccess = branches.some(function (branch) {
// if the reference is to itself (branch.name)
// OR it's an immediate dependency (branch.dependencies[name])
if (branch.name === name || branch.dependencies && branch.dependencies[name]) {
return true;
}
else {
return false;
}
});
// If strict dependency checks are disabled, just return the true.
if (!canAccess && _this.config.eyeglass.disableStrictDependencyCheck) {
debug.warning("Overriding strict dependency check for %s from %s", name, origin);
return true;
}
else {
/* istanbul ignore next - don't test debug */
debug.importer("%s can%s be imported from %s", name, (canAccess ? "" : "not"), origin);
return canAccess;
}
});
};
// check if we can access from the origin...
var canAccess = canAccessFrom(origin);
// if not...
if (!canAccess) {
// check for a symlink...
var realOrigin = perf_1.realpathSync(origin);
/* istanbul ignore if */
if (realOrigin !== origin) {
/* istanbul ignore next */
canAccess = canAccessFrom(realOrigin);
}
}
return canAccess;
};
return EyeglassModules;
}());
exports.default = EyeglassModules;
/**
* given a set of dependencies, gets the hierarchy nodes
*
* @param {Object} dependencies - the dependencies
* @returns {Object} the corresponding hierarchy nodes (for use in archy)
*/
function getHierarchyNodes(dependencies) {
if (dependencies) {
// for each dependency, recurse and get it's hierarchy
return Object.keys(dependencies).map(function (name) { return getHierarchy(dependencies[name]); });
}
else {
return;
}
}
/**
* gets the archy hierarchy for a given branch
*
* @param {Object} branch - the branch to traverse
* @returns {Object} the corresponding archy hierarchy
*/
function getHierarchy(branch) {
// return an object the confirms to the archy expectations
return {
// if the branch has a version on it, append it to the label
label: branch.name + (branch.version ? "@" + branch.version : ""),
nodes: getHierarchyNodes(branch.dependencies)
};
}
/**
* find a branches in a tree with a given path
*
* @param {Object} tree - the module tree to traverse
* @param {String} dir - the path to search for
* @returns {Object} the branches of the tree that contain the path
*/
function findBranchesByPath(mod, dir, branches) {
var e_5, _a;
if (branches === void 0) { branches = new Array(); }
// iterate over the tree
if (!mod) {
return branches;
}
if (mod.path === dir) {
branches.push(mod);
}
if (mod.dependencies) {
var subModNames = Object.keys(mod.dependencies);
try {
for (var subModNames_1 = __values(subModNames), subModNames_1_1 = subModNames_1.next(); !subModNames_1_1.done; subModNames_1_1 = subModNames_1.next()) {
var subModName = subModNames_1_1.value;
findBranchesByPath(mod.dependencies[subModName], dir, branches);
}
}
catch (e_5_1) { e_5 = { error: e_5_1 }; }
finally {
try {
if (subModNames_1_1 && !subModNames_1_1.done && (_a = subModNames_1.return)) _a.call(subModNames_1);
}
finally { if (e_5) throw e_5.error; }
}
}
return branches;
}
/**
* given a branch of modules, flattens them into a collection
*
* @param {Object} branch - the module branch
* @param {Object} collection - the incoming collection
* @returns {Object} the resulting collection
*/
function flattenModules(branch, collection) {
var e_6, _a;
if (collection === void 0) { collection = {}; }
// We capture the app root to a special name so we can always find it easily
// and so it remains in the collection in case de-duplication against a
// dependency would trigger its removal.
if (branch.isRoot) {
collection[":root"] = new Set([branch]);
}
// if the branch itself is a module, add it...
if (branch.isEyeglassModule || branch.isRoot) {
collection[branch.name] = collection[branch.name] || new Set();
collection[branch.name].add(branch);
}
var dependencies = branch.dependencies;
if (dependencies) {
try {
for (var _b = __values(Object.keys(dependencies)), _c = _b.next(); !_c.done; _c = _b.next()) {
var name = _c.value;
flattenModules(dependencies[name], collection);
}
}
catch (e_6_1) { e_6 = { error: e_6_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_6) throw e_6.error; }
}
}
return collection;
}
/**
* given a set of versions, checks if there are any compat issues
*
* @param {Array<Object>} modules - the various modules to check
* @param {String} finalModule - the final module to check against
* @returns {Array<Object>} the array of any issues found
*/
function getDependencyVersionIssues(modules, finalModule) {
return modules.map(function (mod) {
// if the versions are not identical, log it
if (mod.version !== finalModule.version) {
/* istanbul ignore next - don't test debug */
debug.modules && debug.modules("asked for %s@%s but using %s", mod.name, mod.version, finalModule.version);
}
// check that the current module version is satisfied by the finalModule version
// if not, push an error object onto the results
if (!semver.satisfies(mod.semver, "^" + finalModule.semver.version)) {
return {
name: mod.name,
left: mod,
right: finalModule
};
}
else {
return;
}
}).filter(typescriptUtils_1.isPresent);
}
//# sourceMappingURL=EyeglassModules.2_4_1.js.map