UNPKG

8.86 kBJavaScriptView Raw
1'use strict';
2
3var resolvePackagePath = require('./resolve-package-path');
4var cacheKey = require('./cache-key');
5var tryRequire = require('./try-require');
6var crypto = require('crypto');
7
8/*
9 * Entries in 'MODULE_ENTRY' cache. The idea is to minimize the information needed for
10 * each entry (don't store the entire package.json object), but keep
11 * information around for all parts of computing a hash for the dependency
12 * tree of the original module named by 'name' and 'dir' in hashForDep.
13 * The ModuleEntry is created at the time the package.json file is
14 * read in (so that's only done once), but some properties may not be initialized
15 * until later (this then supports creating cycles in the dependencies list,
16 * which is supported by Node).
17 */
18module.exports = ModuleEntry;
19
20function ModuleEntry(name, version, rootDir, sourceHash) {
21
22 // The name of the module, from its package.json file.
23 this.name = name;
24
25 // The version of the module, from its package.json file.
26 this.version = version;
27
28 // The resolved real file path for the directory containing the package.json file.
29 // The string has no trailing path separator(s), as path.resolve() removes them.
30 this.rootDir = rootDir;
31
32 // The computed hash for only the module's source files (under
33 // the root dir, but specifically NOT any node_modules dependencies).
34 // See locate() for initialization of this value.
35 this._sourceHash = sourceHash;
36
37 // The computed hash for the module's source files plus
38 // the source hashes of the dependencies, recursively to the
39 // leaves of the dependency tree.
40 this._hash = null;
41
42 // References to other ModuleEntry objects that correspond
43 // to the dependencies listed in the package.json file.
44 // See locate() for initialization of this value.
45 this._dependencies = Object.create(null);
46}
47
48ModuleEntry.prototype.addDependency = function(dependency, moduleEntry) {
49 this._dependencies[dependency] = moduleEntry;
50};
51
52/*
53 * Starting from an initial ModuleEntry, traverse the entry's dependencies
54 * ("almost" ModuleEntry objects) recursively to the leaves of the dependency
55 * tree. Return an object containing all the ModuleEntry objects,
56 * keyed by each entry's rootDir (real path to the module's root directory.)
57 * NOTE: for speed, the initial caller of _gatherDependencies must pass
58 * in a result object, so we aren't constantly checking that it is set.
59 */
60ModuleEntry.prototype._gatherDependencies = function(dependencies) {
61
62 var moduleEntry = this;
63
64 if (dependencies[moduleEntry.rootDir] !== undefined) {
65 // we already hit this path during the dependencies somewhere earlier
66 // in the dependency tree, so avoid cycling.
67 return;
68 }
69
70 dependencies[moduleEntry.rootDir] = moduleEntry;
71
72 Object.keys(moduleEntry._dependencies).forEach(function(dep) {
73 moduleEntry._dependencies[dep]._gatherDependencies(dependencies);
74 });
75
76 return dependencies;
77};
78
79/*
80 * Compute/return the 'hash' field for the given moduleEntry.
81 * The hash actually consists only of the _sourceHash values of all the
82 * dependencies (ModuleEntry objects) concatenated with '\x00' between
83 * entries. The '_sourceHash' values are computed at the time the
84 * ModuleEntry is created, so don't need to be done again.
85 */
86ModuleEntry.prototype.getHash = function(heimdallNode) {
87 if (this._hash !== null) {
88 return this._hash;
89 }
90
91 // We haven't computed the hash for this entry yet, though the
92 // ModuleEntry entries are all present. It is possible that we
93 // have the hash for some dependencies already.
94
95 // Compute the full transitive dependency list. There are no duplicates,
96 // because we check during _gatherDependencies before inserting in the result.
97 // The keys of the returned object are the package.json baseDir values (the
98 // moduleEntry.rootDir). The values are the moduleEntry objects.
99 var dependencies = this._gatherDependencies(Object.create(null));
100
101 // For repeatability between runs, sort the dependencies list keys
102 // (rootDir values) before assembling into a final hash value.
103 var dependencyRootDirs = Object.keys(dependencies).sort();
104
105 if (heimdallNode) {
106 heimdallNode.stats.paths += dependencyRootDirs.length;
107 }
108
109 var sourceHashes = dependencyRootDirs.map(function(rootDir) {
110 return dependencies[rootDir]._sourceHash;
111 }).join('\x00');
112
113 var hash = crypto.createHash('sha1').update(sourceHashes).digest('hex');
114
115 return (this._hash = hash);
116};
117
118
119/*
120 * Compute and return the ModuleEntry for a given name/dir pair. This is done
121 * recursively so a whole tree can be computed all at once, independent of the
122 * hashing functions. We also compute the '_sourceHash' value (i.e. the hash of
123 * just the package source files, excluding the node_modules dependencies) at
124 * the same time to make it easier to establish the 'hash' value (includes
125 * dependency _sourceHash values) later.
126 * Note this is a class function, not an instance function.
127 */
128ModuleEntry.locate = function(caches, name, dir, hashTreeFn) {
129 var Constructor = this;
130
131 var nameDirKey = cacheKey(name, dir);
132 var realPathKey;
133
134 // It's possible that for a given name/dir pair, there is no package.
135 // Record a null entry in CACHES.PATH anyway so we don't need to
136 // redo that search each time.
137 if (caches.PATH.has(nameDirKey)) {
138 realPathKey = caches.PATH.get(nameDirKey);
139 if (realPathKey !== null) {
140 return caches.MODULE_ENTRY.get(realPathKey);
141 } else {
142 return null;
143 }
144 }
145
146 // There is no caches.PATH entry. Try to get a real package.json path. If there
147 // isn't a real path, the name+dir reference is invalid, so note that in caches.PATH.
148 // If there is a real path, check if it is already in the caches.MODULE_ENTRY.
149 // If not, create it and insert it.
150 var realPath = resolvePackagePath(caches, name, dir);
151
152 if (realPath === null) {
153 caches.PATH.set(nameDirKey, null);
154 return null;
155 }
156
157 // We have a path to a file that supposedly is package.json. We need to be sure
158 // that either we already have a caches.MODULE_ENTRY entry (in which case we can
159 // just create a caches.PATH entry to point to it) or that we can read the
160 // package.json file, at which point we can create a caches.MODULE_ENTRY entry,
161 // then finally the new caches.PATH ENTRY.
162
163 // Generate the cache key for a given real file path. This key is then
164 // used as the key for entries in the CacheGroup.MODULE_ENTRY cache
165 // and values in the CacheGroup.PATH cache.
166 realPathKey = cacheKey('hashed:' + realPath, '');
167
168 if (caches.MODULE_ENTRY.has(realPathKey)) {
169 caches.PATH.set(nameDirKey, realPathKey);
170 return caches.MODULE_ENTRY.get(realPathKey);
171 }
172
173 // Require the package. If we get a 'module-not-found' error it will return null
174 // and we can insert an entry in the cache saying we couldn't find the package
175 // in case it's requested later. Any other more serious error we specifically
176 // don't try to catch here.
177 var thePackage = tryRequire(realPath);
178
179 // if the package was not found, do as above to create a caches.PATH entry
180 // that refers to no path, so we don't waste time doing it again later.
181 if (thePackage === null) {
182 caches.PATH.set(nameDirKey, null);
183 return thePackage;
184 }
185
186 // We have the package object and the relevant keys.
187
188 // Compute the dir containing the package.json,
189 var rootDir = realPath.slice(0, realPath.length - 13); // length('/package.json') === 13
190
191 // Create and insert the new ModuleEntry into the cache.MODULE_ENTRY
192 // now so we know to stop when dealing with cycles.
193 var moduleEntry = new Constructor(thePackage.name, thePackage.version, rootDir, hashTreeFn(rootDir));
194
195 caches.MODULE_ENTRY.set(realPathKey, moduleEntry);
196 caches.PATH.set(nameDirKey, realPathKey);
197
198 // compute the dependencies here so the ModuleEntry doesn't need to know
199 // about caches or the hashTreeFn or the package and we don't need to
200 // guard against accidental reinitialization of dependencies.
201 if (thePackage.dependencies) {
202 // Recursively locate the references to other moduleEntry objects for the module's
203 // dependencies. Initially we just do the 'local' dependencies (i.e. the ones referenced
204 // in the package.json's 'dependencies' field.) Later, moduleEntry._gatherDependencies
205 // is called to compute the complete list including transitive dependencies, and that
206 // is then used for the final moduleEntry._hash calculation.
207 Object.keys(thePackage.dependencies).sort().forEach(function(dep) {
208 var dependencyModuleEntry = Constructor.locate(caches, dep, rootDir, hashTreeFn);
209
210 // there's not really a good reason to include a failed resolution
211 // of a package in the dependencies list.
212 if (dependencyModuleEntry !== null) {
213 moduleEntry.addDependency(dep, dependencyModuleEntry);
214 }
215 });
216 }
217
218 return moduleEntry;
219};