lib/models/package-info-cache/node-modules-list.js

'use strict';

const ErrorList = require('./error-list');

let NULL;

/**
 * Class that stores information about a node_modules directory (i.e., the
 * packages and subdirectories in the directory). It is one of the
 * two types of entries in a PackageInfoCache. It is only created by the
 * PackageInfoCache.
 *
 * @public
 * @class NodeModulesList
 */
module.exports = class NodeModulesList {
  constructor(realPath, cache) {
    this.realPath = realPath;
    this.hasEntries = false; // for speed
    this.entries = Object.create(null);
    this.errors = new ErrorList();
    this.cache = cache;
  }

  // when we encounter a node_modules we will never traverse, we insert a NULL variant.
  // https://en.wikipedia.org/wiki/Null_object_pattern
  // returns a Frozen and Empty NodeModulesList
  static get NULL() {
    if (NULL) {
      return NULL;
    }

    NULL = new this('/dev/null'); // could be anywhere, why not /dev/null?

    Object.freeze(NULL.entries);
    Object.freeze(NULL.errors.errors);
    Object.freeze(NULL.errors);
    Object.freeze(NULL);

    return NULL;
  }

  /**
   * Given error data, add an ErrorEntry to the ErrorList for this object.
   *
   * @protected
   * @method addError
   * @param {String} errorType one of the Errors.ERROR_* constants.
   * @param {Object} errorData any error data relevant to the type of error
   * being created. See showErrors().
   */
  addError(errorType, errorData) {
    this.errors.addError(errorType, errorData);
  }

  /**
   * Indicate if there are any errors in the NodeModulesList itself (not
   * including errors within the individual entries).
   *
   * @public
   * @method hasErrors
   */
  hasErrors() {
    return this.errors.hasErrors();
  }

  /**
   * Add an entry (PackageInfo or NodeModulesList instance) to the entries
   * for this list. This is only called by PackageInfoCache. It is not intended
   * to be called directly by anything else.
   *
   * @protected
   * @method addEntry
   * @param {String} entryName the name of the entry, i.e., the name of the
   * file or subdirectory in the directory listing.
   * @param {Object} entryVal the PackageInfo or NodeModulesList tha corresponds
   * to the given entry name in the file system.
   */
  addEntry(entryName, entryVal) {
    this.hasEntries = true;
    this.entries[entryName] = entryVal;
  }

  /**
   * Return a PackageInfo object for a given package name (which may include
   * a scope)
   *
   * @public
   * @method findPackage
   * @param {String} packageName the name (possibly including a scope) of
   *    the PackageInfo the caller wants returned.
   * @return the desired PackageInfo if one exists for the given name, else null.
   */
  findPackage(packageName) {
    if (!this.hasEntries) {
      return null;
    } // for speed

    let val;

    if (packageName.startsWith('@')) {
      let parts = packageName.split('/');
      let entry = this.entries[parts[0]]; // scope
      val =
        entry instanceof NodeModulesList
          ? entry.findPackage(parts[1]) // the real name
          : null;
    } else {
      val = this.entries[packageName] || null;
    }

    return val;
  }
};