UNPKG

eyeglass

Version:
722 lines 29.9 kB
"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