_readPackage(packageDir, pkg, isRoot) {
    let normalizedPackageDir = path.normalize(packageDir);

    // Most of the time, normalizedPackageDir is already a real path (i.e. fs.realpathSync
    // will return the same value as normalizedPackageDir if the dir actually exists).
    // Because of that, we'll assume we can test for normalizedPackageDir first and return
    // if we find it.
    let pkgInfo = this.getEntry(normalizedPackageDir);
    if (pkgInfo) {
      return pkgInfo;
    }

    // collect errors we hit while trying to create the PackageInfo object.
    // We'll load these into the object once it's created.
    let setupErrors = new ErrorList();

    // We don't already have an entry (bad or otherwise) at normalizedPackageDir. See if
    // we can actually find a real path (including resolving links if needed).
    let pathFailed = false;

    let realPath = getRealDirectoryPath(normalizedPackageDir);

    if (realPath) {
      if (realPath !== normalizedPackageDir) {
        // getRealDirectoryPath actually changed something in the path (e.g.
        // by resolving a symlink), so see if we have this entry.
        pkgInfo = this.getEntry(realPath);
        if (pkgInfo) {
          return pkgInfo;
        }
      } else {
        // getRealDirectoryPath is same as normalizedPackageDir, and we know already we
        // don't have an entry there, so we need to create one.
      }
    } else {
      // no realPath, so either nothing is at the path or it's not a directory.
      // We need to use normalizedPackageDir as the real path.
      pathFailed = true;
      setupErrors.addError(Errors.ERROR_PACKAGE_DIR_MISSING, normalizedPackageDir);
      realPath = normalizedPackageDir;
    }

    // at this point we have realPath set, we don't already have a PackageInfo
    // for the path, and the path may or may not actually correspond to a
    // valid directory (pathFailed tells us which). If we don't have a pkg
    // object already, we need to be able to read one, unless we also don't
    // have a path.
    if (!pkg) {
      if (!pathFailed) {
        // we have a valid realPath
        let packageJsonPath = path.join(realPath, PACKAGE_JSON);
        let pkgfile = getRealFilePath(packageJsonPath);
        if (pkgfile) {
          try {
            pkg = fs.readJsonSync(pkgfile);
          } catch (e) {
            setupErrors.addError(Errors.ERROR_PACKAGE_JSON_PARSE, pkgfile);
          }
        } else {
          setupErrors.addError(Errors.ERROR_PACKAGE_JSON_MISSING, packageJsonPath);
        }
      }

      // Some error has occurred resulting in no pkg object, so just
      // create an empty one so we have something to use below.
      if (!pkg) {
        pkg = Object.create(null);
      }
    }

    // For storage, force the pkg.root to the calculated path. This will
    // save us from issues where we have a package for a non-existing
    // path and other stuff.
    pkg.root = realPath;

    // Create a new PackageInfo and load any errors as needed.
    // Note that pkg may be an empty object here.
    logger.info('Creating new PackageInfo instance for %o at %o', pkg.name, realPath);
    pkgInfo = new PackageInfo(pkg, realPath, this, isRoot);

    if (setupErrors.hasErrors()) {
      pkgInfo.errors = setupErrors;
      pkgInfo.valid = false;
    }

    // If we have an ember-addon, check that the main exists and points
    // to a valid file.
    if (pkgInfo.isForAddon()) {
      logger.info('%s is an addon', pkg.name);

      // Note: when we have both 'main' and ember-addon:main, the latter takes precedence
      let main = (pkg['ember-addon'] && pkg['ember-addon'].main) || pkg['main'];

      if (!main || main === '.' || main === './') {
        main = 'index.js';
      } else if (!path.extname(main)) {
        main = `${main}.js`;
      }

      logger.info('Addon entry point is %o', main);
      pkg.main = main;

      let mainPath = path.join(realPath, main);
      let mainRealPath = getRealFilePath(mainPath);

      if (mainRealPath) {
        pkgInfo.addonMainPath = mainRealPath;
      } else {
        pkgInfo.addError(Errors.ERROR_EMBER_ADDON_MAIN_MISSING, mainPath);
        this.valid = false;
      }
    }

    // The packageInfo itself is now "complete", though we have not
    // yet dealt with any of its "child" packages. Add it to the
    // cache
    this._addEntry(realPath, pkgInfo);

    let emberAddonInfo = pkg['ember-addon'];

    // Set up packageInfos for any in-repo addons
    if (emberAddonInfo) {
      let paths = emberAddonInfo.paths;

      if (paths) {
        paths.forEach((p) => {
          let addonPath = path.join(realPath, p); // real path, though may not exist.
          logger.info('Adding in-repo-addon at %o', addonPath);
          let addonPkgInfo = this._readPackage(addonPath); // may have errors in the addon package.
          pkgInfo.addInRepoAddon(addonPkgInfo);
        });
      }
    }

    if (pkgInfo.mayHaveAddons) {
      logger.info('Reading "node_modules" for %o', realPath);

      // read addon modules from node_modules. We read the whole directory
      // because it's assumed that npm/yarn may have placed addons in the
      // directory from lower down in the project tree, and we want to get
      // the data into the cache ASAP. It may not necessarily be a 'real' error
      // if we find an issue, if nobody below is actually invoking the addon.
      let nodeModules = this._readNodeModulesList(path.join(realPath, 'node_modules'));

      if (nodeModules) {
        pkgInfo.nodeModules = nodeModules;
      }
    } else {
      // will not have addons, so even if there are node_modules here, we can
      // simply pretend there are none.
      pkgInfo.nodeModules = NodeModulesList.NULL;
    }

    return pkgInfo;
  }
Given a directory that supposedly contains a package, create a PackageInfo
object and try to fill it out, EVEN IF the package.json is not readable.
Errors will then be stored in the PackageInfo for anything with the package
that might be wrong.
Because it's possible that the path given to the packageDir is not actually valid,
we'll just use the path.resolve() version of that path to search for the
path in the cache, before trying to get the 'real' path (which also then
resolves links). The cache itself is keyed on either the realPath, if the
packageDir is actually a real valid directory path, or the normalized path (before
path.resolve()), if it is not.

NOTE: the cache is also used to store the NULL_PROJECT project object,
which actually has no package.json or other files, but does have an empty
package object. Because of that, and to speed up processing, loadProject()
will pass in both the package root directory path and the project's package
object, if there is one. If the package object is present, we will use that
in preference to trying to find a package.json file.

If there is no package object, and there is no package.json or the package.json
is bad or the package is an addon with
no main, the only thing we can do is return an ErrorEntry to the caller.
Once past all those problems, if any error occurs with any of the contents
of the package, they'll be cached in the PackageInfo itself.

In summary, only PackageInfo or ErrorEntry will be returned.

@private
@method _readPackage
@param {String} pkgDir the path of the directory to read the package.json from and
 process the contents and create a new cache entry or entries.
@param {Boolean} isRoot, for when this is to be considered the root
package, whose dependencies we must all consider for discovery.