eyeglass
Version:
Sass modules for npm.
665 lines • 27 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const archy_1 = __importDefault(require("archy"));
const path = __importStar(require("path"));
const semver = __importStar(require("semver"));
const semver_1 = require("semver");
const debug = __importStar(require("../util/debug"));
const packageUtils = __importStar(require("../util/package"));
const resolve_1 = __importDefault(require("../util/resolve"));
const SimpleCache_1 = require("../util/SimpleCache");
const URI_1 = require("../util/URI");
const EyeglassModule_1 = __importStar(require("./EyeglassModule"));
const lodash_merge_1 = __importDefault(require("lodash.merge"));
const typescriptUtils_1 = require("../util/typescriptUtils");
const heimdall = require("heimdalljs");
const perf_1 = require("../util/perf");
const version_1 = __importDefault(require("../util/version"));
const EYEGLASS_VERSION = version_1.default.semver;
exports.ROOT_NAME = ":root";
const 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")
];
const globalModuleCache = new SimpleCache_1.SimpleCache();
const 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
*/
class EyeglassModules {
constructor(dir, config, modules) {
let timer = heimdall.start("eyeglass:modules");
try {
this.config = config;
let useGlobalModuleCache = config.eyeglass.useGlobalModuleCache;
this.issues = {
dependencies: {
versions: [],
missing: []
},
engine: {
missing: [],
incompatible: []
}
};
this.cache = {
access: new SimpleCache_1.SimpleCache(),
compatibility: 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
let moduleTree = this.resolveModule(dir, true);
// if any modules were passed in, add them to the module tree
if (modules && modules.length) {
let discoverTimer = heimdall.start("eyeglass:modules:discovery");
try {
for (let mod of modules) {
if (EyeglassModule_1.isModuleReference(mod)) {
if (moduleTree.hasModulePath(mod.path)) {
// If we already have this module in the module tree, skip it.
debug.modules(`Eyeglass module ${mod.path} is already in the module tree. Skipping...`);
continue;
}
}
let resolvedMod = new EyeglassModule_1.default(lodash_merge_1.default(mod, {
isEyeglassModule: true
}), this.discoverModules.bind(this));
// If we already have a direct dependency on a module with this name, skip it.
if (!moduleTree.dependencies[resolvedMod.name]) {
moduleTree.dependencies[resolvedMod.name] = resolvedMod;
}
else {
debug.modules(`Eyeglass module ${resolvedMod.name} is already a dependency. Skipping...`);
}
}
}
finally {
discoverTimer.stop();
}
}
let resolutionTimer = heimdall.start("eyeglass:modules:resolution");
try {
debug.modules && debug.modules("discovered modules\n\t" + this.getGraph(moduleTree).replace(/\n/g, "\n\t"));
// convert the tree into a flat collection of deduped modules
let collection = this.dedupeModules(flattenModules(moduleTree));
// expose the collection
this.collection = collection;
// convert the collection object into a simple array for easy iteration
this.list = Object.keys(collection).map((name) => collection[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("resolved modules\n\t" + this.getGraph(this.tree).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
*/
init(eyeglass, sass) {
this.list.forEach((mod) => 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
*/
access(name, origin) {
let 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
*/
find(name) {
return this.getFinalModule(name);
}
/**
* Creates, caches and returns a mapping of filesystem locations to eyeglass
* modules.
*/
get modulePathMap() {
if (this._modulePathMap) {
return this._modulePathMap;
}
else {
let names = Object.keys(this.collection);
let modulePathMap = {};
for (let name of names) {
let mod = this.collection[name];
modulePathMap[mod.path] = mod;
}
this._modulePathMap = modulePathMap;
return this._modulePathMap;
}
}
/**
* 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.
*/
findByPath(location) {
let 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.)
let parentLocation = perf_1.realpathSync(location);
do {
location = parentLocation;
let 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
*/
getGraph(tree) {
if (!tree) {
tree = this.tree;
}
let hierarchy = getHierarchy(tree);
hierarchy.label = this.getDecoratedRootName();
return archy_1.default(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
*/
resolveModule(pkgPath, isRoot = false) {
let cacheKey = `resolveModule~${pkgPath}!${!!isRoot}`;
return this.cache.modules.getOrElse(cacheKey, () => {
let pkg = packageUtils.getPackage(pkgPath);
let 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
*/
dedupeModules(modules) {
let deduped = {};
for (let name of Object.keys(modules)) {
let otherVersions = new Array();
let secondNewestModule;
let newestModule;
for (let m of modules[name]) {
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;
}
}
}
}
// 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]));
}
return deduped;
}
/**
* checks for any issues in the modules we've discovered
*
* @this {EyeglassModules}
*
*/
checkForIssues() {
this.list.forEach((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);
}
});
}
isCompatibleWithThisEyeglass(needs) {
let cacheKey = needs;
return this.cache.compatibility.getOrElse(cacheKey, () => {
let 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
let minModule = semver.minSatisfying(BOUNDARY_VERSIONS, needs);
let 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
*/
pruneModuleTree(moduleTree) {
let finalModule = moduleTree.isEyeglassModule && this.find(moduleTree.name);
// normalize the branch
let 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
let dependencies = moduleTree.dependencies;
if (dependencies) {
// copy them into the branch after recursively pruning
branch.dependencies = {};
for (let name of Object.keys(dependencies)) {
branch.dependencies[name] = this.pruneModuleTree(dependencies[name]);
}
}
return branch;
}
/**
* resolves the eyeglass module itself
*
* @returns {Object} the resolved eyeglass module definition
*/
getEyeglassSelf() {
let eyeglassDir = path.resolve(__dirname, "..", "..");
let eyeglassPkg = packageUtils.getPackage(eyeglassDir);
let 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
*/
discoverModules(options) {
let pkg = options.pkg || packageUtils.getPackage(options.dir);
let 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) {
lodash_merge_1.default(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...
let dependentModules = Object.keys(dependencies).reduce((modules, dependency) => {
// resolve the package.json
let 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
let 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()
*/
resolveModulePackage(id, parent, parentDir) {
let cacheKey = "resolveModulePackage~" + id + "!" + parent + "!" + parentDir;
return this.cache.modulePackage.getOrElse(cacheKey, function () {
try {
return resolve_1.default(id, parent, parentDir);
}
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
*/
getFinalModule(name) {
return this.collection[name];
}
/**
* gets the root name and decorates it
*
* @this {EyeglassModules}
*
* @returns {String} the decorated name
*/
getDecoratedRootName() {
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
*/
canAccessModule(name, origin) {
// eyeglass can be imported by anyone, regardless of the dependency tree
if (name === "eyeglass") {
return true;
}
let canAccessFrom = (origin) => {
// find the nearest package for the origin
let mod = this.findByPath(origin);
if (!mod) {
if (this.config.eyeglass.disableStrictDependencyCheck) {
return true;
}
else {
throw new Error(`No module found containing '${origin}'.`);
}
}
let modulePath = mod.path;
let cacheKey = modulePath + "!" + origin;
return this.cache.access.getOrElse(cacheKey, () => {
// find all the branches that match the origin
let branches = findBranchesByPath(this.tree, modulePath);
let 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...
let canAccess = canAccessFrom(origin);
// if not...
if (!canAccess) {
// check for a symlink...
let realOrigin = perf_1.realpathSync(origin);
/* istanbul ignore if */
if (realOrigin !== origin) {
/* istanbul ignore next */
canAccess = canAccessFrom(realOrigin);
}
}
return canAccess;
}
}
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((name) => 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 = new Array()) {
// iterate over the tree
if (!mod) {
return branches;
}
if (mod.path === dir) {
branches.push(mod);
}
if (mod.dependencies) {
let subModNames = Object.keys(mod.dependencies);
for (let subModName of subModNames) {
findBranchesByPath(mod.dependencies[subModName], dir, branches);
}
}
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 = {}) {
// 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);
}
let dependencies = branch.dependencies;
if (dependencies) {
for (let name of Object.keys(dependencies)) {
flattenModules(dependencies[name], collection);
}
}
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) {
let satisfied = semver.satisfies(finalModule.semver.version, "^" + mod.semver);
// 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@%s which %s a conflict", mod.name, mod.version, finalModule.name, finalModule.version, satisfied ? "is not" : "is");
}
// check that the current module version is satisfied by the finalModule version
// if not, push an error object onto the results
if (!satisfied) {
return {
name: mod.name,
requested: {
module: mod,
version: mod.semver.toString(),
},
resolved: {
module: finalModule,
version: finalModule.semver.toString(),
}
};
}
else {
return;
}
}).filter(typescriptUtils_1.isPresent);
}
//# sourceMappingURL=EyeglassModules.js.map