ember-cli/ember-cli

View on GitHub
lib/models/host-info-cache.js

Summary

Maintainability
C
7 hrs
Test Coverage
A
90%
'use strict';

function allPkgInfosEqualAtIndex(paths, index) {
  const itemToCheck = paths[0][index];
  return paths.every((pathToLazyEngine) => pathToLazyEngine[index] === itemToCheck);
}

class HostInfoCache {
  constructor(project) {
    this.project = project;
    this._bundledPackageInfoCache = new Map();
    this._hostAddonInfoCache = new Map();
    this._lcaHostCache = new Map();
  }

  /**
   * Given a path (calculated as part of `getHostAddonInfo`), return the correct
   * "bundle host". A bundle host is considered the project or lazy engine.
   *
   * For example, given the following package structure:
   *
   *      --Project--
   *       /      \
   *      /        \
   * Lazy Engine A  \
   *              Addon A
   *                |
   *                |
   *           Lazy Engine B
   *            /          \
   *           /            \
   *      Lazy Engine A   Lazy Engine C
   *
   * The provided paths for lazy engine A would look like:
   *
   * - [Project]
   * - [Project, Addon A, Lazy Engine B]
   *
   * For this project structure, this function would return [Project, [Project]]
   *
   * Similarly, given the following project structure:
   *
   *            --Project--
   *             /      \
   *            /        \
   *     Lazy Engine A    \
   *          /        Lazy Engine B
   *         /               |
   *        /                |
   *  Lazy Engine C     Lazy Engine C
   *
   * The provided paths for lazy engine C would look like:
   *
   * - [Project, Lazy Engine A]
   * - [Project, Lazy Engine B]
   *
   * In this case, the host is the project and would also return [Project, [Project]]
   *
   * @method _findNearestBundleHost
   * @param {Array<PackageInfo[]>} paths The found paths to a given bundle host
   * @return {[PackageInfo, PackageInfo[]]}
   * @private
   */
  _findNearestBundleHost(paths, pkgInfoForLazyEngine) {
    // building an engine in isolation (it's considered the project, but it's
    // also added as a dependency to the project by `ember-cli`)
    if (this.project._packageInfo === pkgInfoForLazyEngine) {
      return [this.project._packageInfo, [this.project._packageInfo]];
    }

    const shortestPath = paths.reduce(
      (acc, pathToLazyEngine) => Math.min(acc, pathToLazyEngine.length),
      Number.POSITIVE_INFINITY
    );

    const pathsEqualToShortest = paths.filter((pathToLazyEngine) => pathToLazyEngine.length === shortestPath);
    const [firstPath] = pathsEqualToShortest;

    for (let i = firstPath.length - 1; i >= 0; i--) {
      const pkgInfo = firstPath[i];

      if (pkgInfo.isForBundleHost() && allPkgInfosEqualAtIndex(pathsEqualToShortest, i)) {
        return [pkgInfo, firstPath.slice(0, i + 1)];
      }
    }

    // this should _never_ be triggered
    throw new Error(
      `[ember-cli] Could not find a common host for: \`${pkgInfoForLazyEngine.name}\` (located at \`${pkgInfoForLazyEngine.realPath}\`)`
    );
  }

  /**
   * Returns a `Set` of package-info objects that a given bundle host is
   * _directly_ responsible for bundling (i.e., it excludes other bundle
   * hosts/lazy engines when it encounters these)
   *
   * @method _getBundledPackageInfos
   * @param {PackageInfo} pkgInfoToStartAt
   * @return {Set<PackageInfo>}
   * @private
   */
  _getBundledPackageInfos(pkgInfoToStartAt) {
    let pkgInfos = this._bundledPackageInfoCache.get(pkgInfoToStartAt);

    if (pkgInfos) {
      return pkgInfos;
    }

    if (!pkgInfoToStartAt.isForBundleHost()) {
      throw new Error(
        `[ember-cli] \`${pkgInfoToStartAt.name}\` is not a bundle host; \`getBundledPackageInfos\` should only be used to find bundled package infos for a project or lazy engine`
      );
    }

    pkgInfos = new Set();
    this._bundledPackageInfoCache.set(pkgInfoToStartAt, pkgInfos);

    let findAddons = (currentPkgInfo) => {
      if (!currentPkgInfo.valid || !currentPkgInfo.addonMainPath) {
        return;
      }

      if (pkgInfos.has(currentPkgInfo)) {
        return;
      }

      if (currentPkgInfo.isForBundleHost()) {
        return;
      }

      pkgInfos.add(currentPkgInfo);

      let addonPackageList = currentPkgInfo.discoverAddonAddons();
      addonPackageList.forEach((pkgInfo) => findAddons(pkgInfo));
    };

    let addonPackageList = pkgInfoToStartAt.project
      ? pkgInfoToStartAt.discoverProjectAddons()
      : pkgInfoToStartAt.discoverAddonAddons();

    addonPackageList.forEach((pkgInfo) => findAddons(pkgInfo));

    return pkgInfos;
  }

  /**
   * This function intends to return a common host for a bundle host (lazy engine). The root
   * package info should be the starting point (i.e., the project's package info). We do this
   * by performing a breadth-first traversal until we find the intended lazy engine (represented
   * as a package-info & the 1st argument passed to this function). As part of the traversal, we keep
   * track of all paths to said engine; then, once we find the intended engine we use this to determine
   * the nearest common host amongst all shortest paths.
   *
   * Some context:
   *
   * For a given engine/bundle host, this finds the lowest common ancestor that is considered a
   * host amongst _all_ engines by the same name in the project.
   *
   * For example, given the following package structure:
   *
   *      --Project--
   *       /      \
   *      /        \
   * Lazy Engine A  \
   *              Addon A
   *                |
   *                |
   *           Lazy Engine B
   *            /          \
   *           /            \
   *      Lazy Engine A   Lazy Engine C
   *
   * - The LCA host for Lazy Engine A is the project
   * - The LCA host for Lazy Engine B is the project
   * - The LCA host for Lazy Engine C is Lazy Engine B
   *
   * This also returns `hostAndAncestorBundledPackageInfos`, which are all bundled addons above a given host:
   *
   * - `hostAndAncestorBundledPackageInfos` for lazy engine A includes all non-lazy dependencies of its LCA host & above (in this case, just the project)
   * - `hostAndAncestorBundledPackageInfos` for lazy engine B includes all non-lazy dependencies of its LCA host & above (in this case, just the project)
   * - `hostAndAncestorBundledPackageInfos` for lazy engine C includes non-lazy deps of lazy engine B & non-lazy deps of the project (LCA host & above)
   *
   * This is intended to mimic the behavior of `ancestorHostAddons` in `ember-engines`:
   * https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/engine-addon.js#L333
   *
   * Unfortunately, we can't easily repurpose the logic in `ember-engines` since the algorithm has to be different;
   * in `ember-engines` we need access to the actual addon instance, however, this is intended to be used _during_
   * addon instantiation, so we only have access to package-info objects. In having said this, we _can_ repurpose
   * the `hostPackageInfo` to determine the LCA host; see below `findLCAHost`.
   *
   * @method getHostAddonInfo
   * @param {PackageInfo} packageInfoForLazyEngine
   * @return {{ hostPackageInfo: PackageInfo, hostAndAncestorBundledPackageInfos: Set<PackageInfo> }}
   */
  getHostAddonInfo(packageInfoForLazyEngine) {
    const cacheKey = `${this.project._packageInfo.realPath}-${packageInfoForLazyEngine.realPath}`;

    let hostInfoCacheEntry = this._hostAddonInfoCache.get(cacheKey);

    if (hostInfoCacheEntry) {
      return hostInfoCacheEntry;
    }

    if (!packageInfoForLazyEngine.isForEngine()) {
      throw new Error(
        `[ember-cli] \`${packageInfoForLazyEngine.name}\` is not an engine; \`getHostAddonInfo\` should only be used to find host information about engines`
      );
    }

    const queue = [{ pkgInfo: this.project._packageInfo, path: [] }];
    const visited = new Set();
    const foundPaths = [];

    while (queue.length) {
      const { pkgInfo: currentPackageInfo, path } = queue.shift();

      const {
        addonMainPath,
        inRepoAddons = [],
        dependencyPackages = {},
        devDependencyPackages = {},
      } = currentPackageInfo;

      const isCurrentPackageInfoProject = this.project._packageInfo === currentPackageInfo;

      // don't process non-ember addons
      if (!isCurrentPackageInfoProject && typeof addonMainPath !== 'string') {
        continue;
      }

      // store found paths
      if (currentPackageInfo === packageInfoForLazyEngine) {
        foundPaths.push([...path]);
      }

      // don't process a given `PackageInfo` object more than once
      if (!visited.has(currentPackageInfo)) {
        visited.add(currentPackageInfo);

        // add current package info to current path
        path.push(currentPackageInfo);

        queue.push(
          ...[...inRepoAddons, ...Object.values(dependencyPackages), ...Object.values(devDependencyPackages)].map(
            (pkgInfo) => ({ pkgInfo, path: [...path] })
          )
        );
      }
    }

    const [hostPackageInfo, foundPath] = this._findNearestBundleHost(foundPaths, packageInfoForLazyEngine);

    const hostAndAncestorBundledPackageInfos = foundPath
      .filter((pkgInfo) => pkgInfo.isForBundleHost())
      .reduce((acc, curr) => {
        acc.push(...this._getBundledPackageInfos(curr));
        return acc;
      }, []);

    hostInfoCacheEntry = {
      hostPackageInfo,
      hostAndAncestorBundledPackageInfos: new Set(hostAndAncestorBundledPackageInfos),
    };

    this._hostAddonInfoCache.set(cacheKey, hostInfoCacheEntry);
    return hostInfoCacheEntry;
  }

  /**
   * This returns the LCA host for a given engine; we use the associated package info
   * to compute this (see `getHostAddonInfo` above); this finds the lowest common ancestor
   * that is considered a host amongst _all_ engines by the same name in the project. This
   * function is intended to replace the original behavior in `ember-engines`.
   *
   * For more info, see the original implementation here:
   *
   * https://github.com/ember-engines/ember-engines/blob/master/packages/ember-engines/lib/utils/find-lca-host.js
   *
   * @method findLCAHost
   * @param {EngineAddon} engineInstance
   * @return {EngineAddon|EmberApp}
   */
  findLCAHost(engineInstance) {
    // only compute once for a given engine
    // we're using the engine name as the cache key here because regardless of its
    // version, lazy engines will always get output to: `engines-dist/${engineName}`
    let lcaHost = this._lcaHostCache.get(engineInstance.name);

    if (lcaHost) {
      return lcaHost;
    }

    if (!engineInstance._packageInfo.isForEngine()) {
      throw new Error(
        `[ember-cli] \`findLCAHost\` should only be used for engines; \`${engineInstance.name}\` is not an engine`
      );
    }

    const { hostPackageInfo } = this.getHostAddonInfo(engineInstance._packageInfo);

    let curr = engineInstance;

    while (curr && curr.parent) {
      if (curr.app) {
        lcaHost = curr.app;
        break;
      }

      if (curr._packageInfo === hostPackageInfo) {
        lcaHost = curr;
        break;
      }

      curr = curr.parent;
    }

    if (lcaHost) {
      this._lcaHostCache.set(engineInstance.name, lcaHost);
      return lcaHost;
    }

    // this should _never_ be triggered
    throw new Error(
      `[ember-cli] Could not find an LCA host for: \`${engineInstance.name}\` (located at \`${
        engineInstance.packageRoot || engineInstance.root
      }\`)`
    );
  }
}

module.exports = HostInfoCache;