UNPKG

hash-for-dep

Version:

generates a hash that represents a module and its depenencies uniqueness

232 lines (188 loc) 9.49 kB
'use strict'; var resolvePackagePath = require('resolve-package-path'); var cacheKey = require('./cache-key'); var tryRequire = require('./try-require'); var crypto = require('crypto'); /* * Entries in 'MODULE_ENTRY' cache. The idea is to minimize the information needed for * each entry (don't store the entire package.json object), but keep * information around for all parts of computing a hash for the dependency * tree of the original module named by 'name' and 'dir' in hashForDep. * The ModuleEntry is created at the time the package.json file is * read in (so that's only done once), but some properties may not be initialized * until later (this then supports creating cycles in the dependencies list, * which is supported by Node). */ module.exports = ModuleEntry; function ModuleEntry(name, version, rootDir, sourceHash, isValid) { // The name of the module, from its package.json file. this.name = name; // The version of the module, from its package.json file. this.version = version; // The resolved real file path for the directory containing the package.json file. // The string has no trailing path separator(s), as path.resolve() removes them. this.rootDir = rootDir; // The computed hash for only the module's source files (under // the root dir, but specifically NOT any node_modules dependencies). // See locate() for initialization of this value. this._sourceHash = sourceHash; // The computed hash for the module's source files plus // the source hashes of the dependencies, recursively to the // leaves of the dependency tree. this._hash = null; // References to other ModuleEntry objects that correspond // to the dependencies listed in the package.json file. // See locate() for initialization of this value. this._dependencies = Object.create(null); this.isValid = isValid === false ? false : true; } ModuleEntry.prototype.addDependency = function(dependency, moduleEntry) { this._dependencies[dependency] = moduleEntry; }; /* * Starting from an initial ModuleEntry, traverse the entry's dependencies * ("almost" ModuleEntry objects) recursively to the leaves of the dependency * tree. Return an object containing all the ModuleEntry objects, * keyed by each entry's rootDir (real path to the module's root directory.) * NOTE: for speed, the initial caller of _gatherDependencies must pass * in a result object, so we aren't constantly checking that it is set. */ ModuleEntry.prototype._gatherDependencies = function(dependencies) { var moduleEntry = this; if (dependencies[moduleEntry.rootDir] !== undefined) { // we already hit this path during the dependencies somewhere earlier // in the dependency tree, so avoid cycling. return; } dependencies[moduleEntry.rootDir] = moduleEntry; Object.keys(moduleEntry._dependencies).forEach(function(dep) { moduleEntry._dependencies[dep]._gatherDependencies(dependencies); }); return dependencies; }; /* * Compute/return the 'hash' field for the given moduleEntry. * The hash actually consists only of the _sourceHash values of all the * dependencies (ModuleEntry objects) concatenated with '\x00' between * entries. The '_sourceHash' values are computed at the time the * ModuleEntry is created, so don't need to be done again. */ ModuleEntry.prototype.getHash = function(heimdallNode) { if (this._hash !== null) { return this._hash; } // We haven't computed the hash for this entry yet, though the // ModuleEntry entries are all present. It is possible that we // have the hash for some dependencies already. // Compute the full transitive dependency list. There are no duplicates, // because we check during _gatherDependencies before inserting in the result. // The keys of the returned object are the package.json baseDir values (the // moduleEntry.rootDir). The values are the moduleEntry objects. var dependencies = this._gatherDependencies(Object.create(null)); // For repeatability between runs, sort the dependencies list keys // (rootDir values) before assembling into a final hash value. var dependencyRootDirs = Object.keys(dependencies).sort(); if (heimdallNode) { heimdallNode.stats.paths += dependencyRootDirs.length; } var sourceHashes = dependencyRootDirs.map(function(rootDir) { return dependencies[rootDir]._sourceHash; }).join('\x00'); var hash = crypto.createHash('sha1').update(sourceHashes).digest('hex'); return (this._hash = hash); }; /* * This exists to maintain compatibbility with some unexpected old behavior. * Specifically invalid input, would still produce a hash. * * This is clearly not helpful, but to fix a regression we will restore this * behavior, but the next major version (2.x.x) we should remove support for * this, and instead error with a helpful error message */ ModuleEntry.buildInvalidModule = function(name, dir) { return new this(name, '0.0.0' , dir, crypto.createHash('sha1').update([name, dir].filter(Boolean).join('\x00')).digest('hex'), false); }; /* * Compute and return the ModuleEntry for a given name/dir pair. This is done * recursively so a whole tree can be computed all at once, independent of the * hashing functions. We also compute the '_sourceHash' value (i.e. the hash of * just the package source files, excluding the node_modules dependencies) at * the same time to make it easier to establish the 'hash' value (includes * dependency _sourceHash values) later. * Note this is a class function, not an instance function. */ ModuleEntry.locate = function(caches, name, dir, hashTreeFn) { var Constructor = this; var nameDirKey = cacheKey(name, dir); var realPathKey; // It's possible that for a given name/dir pair, there is no package. // Record a null entry in CACHES.PATH anyway so we don't need to // redo that search each time. if (caches.PATH.has(nameDirKey)) { realPathKey = caches.PATH.get(nameDirKey); if (realPathKey !== null) { return caches.MODULE_ENTRY.get(realPathKey); } else { return this.buildInvalidModule(name, dir); } } // There is no caches.PATH entry. Try to get a real package.json path. If there // isn't a real path, the name+dir reference is invalid, so note that in caches.PATH. // If there is a real path, check if it is already in the caches.MODULE_ENTRY. // If not, create it and insert it. var realPath = resolvePackagePath(name, dir, caches);; if (realPath === null) { return this.buildInvalidModule(name, dir); } // We have a path to a file that supposedly is package.json. We need to be sure // that either we already have a caches.MODULE_ENTRY entry (in which case we can // just create a caches.PATH entry to point to it) or that we can read the // package.json file, at which point we can create a caches.MODULE_ENTRY entry, // then finally the new caches.PATH ENTRY. // Generate the cache key for a given real file path. This key is then // used as the key for entries in the CacheGroup.MODULE_ENTRY cache // and values in the CacheGroup.PATH cache. realPathKey = cacheKey('hashed:' + realPath, ''); if (caches.MODULE_ENTRY.has(realPathKey)) { caches.PATH.set(nameDirKey, realPathKey); return caches.MODULE_ENTRY.get(realPathKey); } // Require the package. If we get a 'module-not-found' error it will return null // and we can insert an entry in the cache saying we couldn't find the package // in case it's requested later. Any other more serious error we specifically // don't try to catch here. var thePackage = tryRequire(realPath); // if the package was not found, do as above to create a caches.PATH entry // that refers to no path, so we don't waste time doing it again later. if (thePackage === null) { caches.PATH.set(nameDirKey, null); return thePackage; } // We have the package object and the relevant keys. // Compute the dir containing the package.json, var rootDir = realPath.slice(0, realPath.length - 13); // length('/package.json') === 13 // Create and insert the new ModuleEntry into the cache.MODULE_ENTRY // now so we know to stop when dealing with cycles. var moduleEntry = new Constructor(thePackage.name, thePackage.version, rootDir, hashTreeFn(rootDir)); caches.MODULE_ENTRY.set(realPathKey, moduleEntry); caches.PATH.set(nameDirKey, realPathKey); // compute the dependencies here so the ModuleEntry doesn't need to know // about caches or the hashTreeFn or the package and we don't need to // guard against accidental reinitialization of dependencies. if (thePackage.dependencies) { // Recursively locate the references to other moduleEntry objects for the module's // dependencies. Initially we just do the 'local' dependencies (i.e. the ones referenced // in the package.json's 'dependencies' field.) Later, moduleEntry._gatherDependencies // is called to compute the complete list including transitive dependencies, and that // is then used for the final moduleEntry._hash calculation. Object.keys(thePackage.dependencies).sort().forEach(function(dep) { var dependencyModuleEntry = Constructor.locate(caches, dep, rootDir, hashTreeFn); // there's not really a good reason to include a failed resolution // of a package in the dependencies list. if (dependencyModuleEntry !== null) { moduleEntry.addDependency(dep, dependencyModuleEntry); } }); } return moduleEntry; };