/ src / module-cache.js
module-cache.js
  1  const Module = require('module');
  2  const path = require('path');
  3  const semver = require('semver');
  4  
  5  // Extend semver.Range to memoize matched versions for speed
  6  class Range extends semver.Range {
  7    constructor() {
  8      super(...arguments);
  9      this.matchedVersions = new Set();
 10      this.unmatchedVersions = new Set();
 11    }
 12  
 13    test(version) {
 14      if (this.matchedVersions.has(version)) return true;
 15      if (this.unmatchedVersions.has(version)) return false;
 16  
 17      const matches = super.test(...arguments);
 18      if (matches) {
 19        this.matchedVersions.add(version);
 20      } else {
 21        this.unmatchedVersions.add(version);
 22      }
 23      return matches;
 24    }
 25  }
 26  
 27  let nativeModules = null;
 28  
 29  const cache = {
 30    builtins: {},
 31    debug: false,
 32    dependencies: {},
 33    extensions: {},
 34    folders: {},
 35    ranges: {},
 36    registered: false,
 37    resourcePath: null,
 38    resourcePathWithTrailingSlash: null
 39  };
 40  
 41  // isAbsolute is inlined from fs-plus so that fs-plus itself can be required
 42  // from this cache.
 43  let isAbsolute;
 44  if (process.platform === 'win32') {
 45    isAbsolute = pathToCheck =>
 46      pathToCheck &&
 47      (pathToCheck[1] === ':' ||
 48        (pathToCheck[0] === '\\' && pathToCheck[1] === '\\'));
 49  } else {
 50    isAbsolute = pathToCheck => pathToCheck && pathToCheck[0] === '/';
 51  }
 52  
 53  const isCorePath = pathToCheck =>
 54    pathToCheck.startsWith(cache.resourcePathWithTrailingSlash);
 55  
 56  function loadDependencies(modulePath, rootPath, rootMetadata, moduleCache) {
 57    const fs = require('fs-plus');
 58  
 59    for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) {
 60      if (path.basename(childPath) === '.bin') continue;
 61      if (
 62        rootPath === modulePath &&
 63        (rootMetadata.packageDependencies &&
 64          rootMetadata.packageDependencies.hasOwnProperty(
 65            path.basename(childPath)
 66          ))
 67      ) {
 68        continue;
 69      }
 70  
 71      const childMetadataPath = path.join(childPath, 'package.json');
 72      if (!fs.isFileSync(childMetadataPath)) continue;
 73  
 74      const childMetadata = JSON.parse(fs.readFileSync(childMetadataPath));
 75      if (childMetadata && childMetadata.version) {
 76        var mainPath;
 77        try {
 78          mainPath = require.resolve(childPath);
 79        } catch (error) {
 80          mainPath = null;
 81        }
 82  
 83        if (mainPath) {
 84          moduleCache.dependencies.push({
 85            name: childMetadata.name,
 86            version: childMetadata.version,
 87            path: path.relative(rootPath, mainPath)
 88          });
 89        }
 90  
 91        loadDependencies(childPath, rootPath, rootMetadata, moduleCache);
 92      }
 93    }
 94  }
 95  
 96  function loadFolderCompatibility(
 97    modulePath,
 98    rootPath,
 99    rootMetadata,
100    moduleCache
101  ) {
102    const fs = require('fs-plus');
103  
104    const metadataPath = path.join(modulePath, 'package.json');
105    if (!fs.isFileSync(metadataPath)) return;
106  
107    const metadata = JSON.parse(fs.readFileSync(metadataPath));
108    const dependencies = metadata.dependencies || {};
109  
110    for (let name in dependencies) {
111      if (!semver.validRange(dependencies[name])) {
112        delete dependencies[name];
113      }
114    }
115  
116    const onDirectory = childPath => path.basename(childPath) !== 'node_modules';
117  
118    const extensions = ['.js', '.coffee', '.json', '.node'];
119    let paths = {};
120    function onFile(childPath) {
121      const needle = path.extname(childPath);
122      if (extensions.includes(needle)) {
123        const relativePath = path.relative(rootPath, path.dirname(childPath));
124        paths[relativePath] = true;
125      }
126    }
127    fs.traverseTreeSync(modulePath, onFile, onDirectory);
128  
129    paths = Object.keys(paths);
130    if (paths.length > 0 && Object.keys(dependencies).length > 0) {
131      moduleCache.folders.push({ paths, dependencies });
132    }
133  
134    for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) {
135      if (path.basename(childPath) === '.bin') continue;
136      if (
137        rootPath === modulePath &&
138        (rootMetadata.packageDependencies &&
139          rootMetadata.packageDependencies.hasOwnProperty(
140            path.basename(childPath)
141          ))
142      ) {
143        continue;
144      }
145      loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache);
146    }
147  }
148  
149  function loadExtensions(modulePath, rootPath, rootMetadata, moduleCache) {
150    const fs = require('fs-plus');
151    const extensions = ['.js', '.coffee', '.json', '.node'];
152    const nodeModulesPath = path.join(rootPath, 'node_modules');
153  
154    function onFile(filePath) {
155      filePath = path.relative(rootPath, filePath);
156      const segments = filePath.split(path.sep);
157      if (segments.includes('test')) return;
158      if (segments.includes('tests')) return;
159      if (segments.includes('spec')) return;
160      if (segments.includes('specs')) return;
161      if (
162        segments.length > 1 &&
163        !['exports', 'lib', 'node_modules', 'src', 'static', 'vendor'].includes(
164          segments[0]
165        )
166      )
167        return;
168  
169      const extension = path.extname(filePath);
170      if (extensions.includes(extension)) {
171        if (moduleCache.extensions[extension] == null) {
172          moduleCache.extensions[extension] = [];
173        }
174        moduleCache.extensions[extension].push(filePath);
175      }
176    }
177  
178    function onDirectory(childPath) {
179      // Don't include extensions from bundled packages
180      // These are generated and stored in the package's own metadata cache
181      if (rootMetadata.name === 'atom') {
182        const parentPath = path.dirname(childPath);
183        if (parentPath === nodeModulesPath) {
184          const packageName = path.basename(childPath);
185          if (
186            rootMetadata.packageDependencies &&
187            rootMetadata.packageDependencies.hasOwnProperty(packageName)
188          )
189            return false;
190        }
191      }
192  
193      return true;
194    }
195  
196    fs.traverseTreeSync(rootPath, onFile, onDirectory);
197  }
198  
199  function satisfies(version, rawRange) {
200    let parsedRange;
201    if (!(parsedRange = cache.ranges[rawRange])) {
202      parsedRange = new Range(rawRange);
203      cache.ranges[rawRange] = parsedRange;
204    }
205    return parsedRange.test(version);
206  }
207  
208  function resolveFilePath(relativePath, parentModule) {
209    if (!relativePath) return;
210    if (!(parentModule && parentModule.filename)) return;
211    if (relativePath[0] !== '.' && !isAbsolute(relativePath)) return;
212  
213    const resolvedPath = path.resolve(
214      path.dirname(parentModule.filename),
215      relativePath
216    );
217    if (!isCorePath(resolvedPath)) return;
218  
219    let extension = path.extname(resolvedPath);
220    if (extension) {
221      if (
222        cache.extensions[extension] &&
223        cache.extensions[extension].has(resolvedPath)
224      )
225        return resolvedPath;
226    } else {
227      for (extension in cache.extensions) {
228        const paths = cache.extensions[extension];
229        const resolvedPathWithExtension = `${resolvedPath}${extension}`;
230        if (paths.has(resolvedPathWithExtension)) {
231          return resolvedPathWithExtension;
232        }
233      }
234    }
235  }
236  
237  function resolveModulePath(relativePath, parentModule) {
238    if (!relativePath) return;
239    if (!(parentModule && parentModule.filename)) return;
240  
241    if (!nativeModules) nativeModules = process.binding('natives');
242    if (nativeModules.hasOwnProperty(relativePath)) return;
243    if (relativePath[0] === '.') return;
244    if (isAbsolute(relativePath)) return;
245  
246    const folderPath = path.dirname(parentModule.filename);
247  
248    const range =
249      cache.folders[folderPath] && cache.folders[folderPath][relativePath];
250    if (!range) {
251      const builtinPath = cache.builtins[relativePath];
252      if (builtinPath) {
253        return builtinPath;
254      } else {
255        return;
256      }
257    }
258  
259    const candidates = cache.dependencies[relativePath];
260    if (candidates == null) return;
261  
262    for (let version in candidates) {
263      const resolvedPath = candidates[version];
264      if (Module._cache[resolvedPath] || isCorePath(resolvedPath)) {
265        if (satisfies(version, range)) return resolvedPath;
266      }
267    }
268  }
269  
270  function registerBuiltins(devMode) {
271    if (
272      devMode ||
273      !cache.resourcePath.startsWith(`${process.resourcesPath}${path.sep}`)
274    ) {
275      const fs = require('fs-plus');
276      const atomJsPath = path.join(cache.resourcePath, 'exports', 'atom.js');
277      if (fs.isFileSync(atomJsPath)) {
278        cache.builtins.atom = atomJsPath;
279      }
280    }
281    if (cache.builtins.atom == null) {
282      cache.builtins.atom = path.join(cache.resourcePath, 'exports', 'atom.js');
283    }
284  
285    const electronAsarRoot = path.join(process.resourcesPath, 'electron.asar');
286  
287    const commonRoot = path.join(electronAsarRoot, 'common', 'api');
288    const commonBuiltins = ['clipboard', 'shell'];
289    for (const builtin of commonBuiltins) {
290      cache.builtins[builtin] = path.join(commonRoot, `${builtin}.js`);
291    }
292  
293    const rendererRoot = path.join(electronAsarRoot, 'renderer', 'api');
294    const rendererBuiltins = [
295      'crash-reporter',
296      'ipc-renderer',
297      'remote',
298      'screen'
299    ];
300    for (const builtin of rendererBuiltins) {
301      cache.builtins[builtin] = path.join(rendererRoot, `${builtin}.js`);
302    }
303  }
304  
305  exports.create = function(modulePath) {
306    const fs = require('fs-plus');
307  
308    modulePath = fs.realpathSync(modulePath);
309    const metadataPath = path.join(modulePath, 'package.json');
310    const metadata = JSON.parse(fs.readFileSync(metadataPath));
311  
312    const moduleCache = {
313      version: 1,
314      dependencies: [],
315      extensions: {},
316      folders: []
317    };
318  
319    loadDependencies(modulePath, modulePath, metadata, moduleCache);
320    loadFolderCompatibility(modulePath, modulePath, metadata, moduleCache);
321    loadExtensions(modulePath, modulePath, metadata, moduleCache);
322  
323    metadata._atomModuleCache = moduleCache;
324    fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
325  };
326  
327  exports.register = function({ resourcePath, devMode } = {}) {
328    if (cache.registered) return;
329  
330    const originalResolveFilename = Module._resolveFilename;
331    Module._resolveFilename = function(relativePath, parentModule) {
332      let resolvedPath = resolveModulePath(relativePath, parentModule);
333      if (!resolvedPath) {
334        resolvedPath = resolveFilePath(relativePath, parentModule);
335      }
336      return resolvedPath || originalResolveFilename(relativePath, parentModule);
337    };
338  
339    cache.registered = true;
340    cache.resourcePath = resourcePath;
341    cache.resourcePathWithTrailingSlash = `${resourcePath}${path.sep}`;
342    registerBuiltins(devMode);
343  };
344  
345  exports.add = function(directoryPath, metadata) {
346    // path.join isn't used in this function for speed since path.join calls
347    // path.normalize and all the paths are already normalized here.
348  
349    if (metadata == null) {
350      try {
351        metadata = require(`${directoryPath}${path.sep}package.json`);
352      } catch (error) {
353        return;
354      }
355    }
356  
357    const cacheToAdd = metadata && metadata._atomModuleCache;
358    if (!cacheToAdd) return;
359  
360    for (const dependency of cacheToAdd.dependencies || []) {
361      if (!cache.dependencies[dependency.name]) {
362        cache.dependencies[dependency.name] = {};
363      }
364      if (!cache.dependencies[dependency.name][dependency.version]) {
365        cache.dependencies[dependency.name][
366          dependency.version
367        ] = `${directoryPath}${path.sep}${dependency.path}`;
368      }
369    }
370  
371    for (const entry of cacheToAdd.folders || []) {
372      for (const folderPath of entry.paths) {
373        if (folderPath) {
374          cache.folders[`${directoryPath}${path.sep}${folderPath}`] =
375            entry.dependencies;
376        } else {
377          cache.folders[directoryPath] = entry.dependencies;
378        }
379      }
380    }
381  
382    for (const extension in cacheToAdd.extensions) {
383      const paths = cacheToAdd.extensions[extension];
384      if (!cache.extensions[extension]) {
385        cache.extensions[extension] = new Set();
386      }
387      for (let filePath of paths) {
388        cache.extensions[extension].add(`${directoryPath}${path.sep}${filePath}`);
389      }
390    }
391  };
392  
393  exports.cache = cache;
394  
395  exports.Range = Range;