"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

function _bluebird() {
  const data = require("bluebird");

  _bluebird = function () {
    return data;
  };

  return data;
}

function _path() {
  const data = _interopRequireDefault(require("path"));

  _path = function () {
    return data;
  };

  return data;
}

function _ramda() {
  const data = _interopRequireDefault(require("ramda"));

  _ramda = function () {
    return data;
  };

  return data;
}

function _glob() {
  const data = _interopRequireDefault(require("glob"));

  _glob = function () {
    return data;
  };

  return data;
}

function _bitId() {
  const data = require("../bit-id");

  _bitId = function () {
    return data;
  };

  return data;
}

function _constants() {
  const data = require("../constants");

  _constants = function () {
    return data;
  };

  return data;
}

function _logger() {
  const data = _interopRequireDefault(require("../logger/logger"));

  _logger = function () {
    return data;
  };

  return data;
}

function _utils() {
  const data = require("../utils");

  _utils = function () {
    return data;
  };

  return data;
}

function _linkGenerator() {
  const data = require("./link-generator");

  _linkGenerator = function () {
    return data;
  };

  return data;
}

function _linkContent() {
  const data = require("./link-content");

  _linkContent = function () {
    return data;
  };

  return data;
}

function _componentNodeModulesPath() {
  const data = _interopRequireDefault(require("../utils/bit/component-node-modules-path"));

  _componentNodeModulesPath = function () {
    return data;
  };

  return data;
}

function _bitMap() {
  const data = _interopRequireDefault(require("../consumer/bit-map/bit-map"));

  _bitMap = function () {
    return data;
  };

  return data;
}

function _symlink() {
  const data = _interopRequireDefault(require("./symlink"));

  _symlink = function () {
    return data;
  };

  return data;
}

function _dataToPersist() {
  const data = _interopRequireDefault(require("../consumer/component/sources/data-to-persist"));

  _dataToPersist = function () {
    return data;
  };

  return data;
}

function _linkFile() {
  const data = _interopRequireDefault(require("./link-file"));

  _linkFile = function () {
    return data;
  };

  return data;
}

function _componentsList() {
  const data = _interopRequireDefault(require("../consumer/component/components-list"));

  _componentsList = function () {
    return data;
  };

  return data;
}

function _packageJsonUtils() {
  const data = require("../consumer/component/package-json-utils");

  _packageJsonUtils = function () {
    return data;
  };

  return data;
}

function _packageJsonFile() {
  const data = _interopRequireDefault(require("../consumer/component/package-json-file"));

  _packageJsonFile = function () {
    return data;
  };

  return data;
}

function _path2() {
  const data = require("../utils/path");

  _path2 = function () {
    return data;
  };

  return data;
}

/**
 * link given components to node_modules, so it's possible to use absolute link instead of relative
 * for example, require('@bit/remote-scope.bar.foo)
 */
class NodeModuleLinker {
  // preparation for the capsule, which is going to have only BitMap with no Consumer
  constructor(components, consumer, bitMap) {
    this.components = _componentsList().default.getUniqueComponents(components);
    this.consumer = consumer;
    this.bitMap = bitMap;
    this.dataToPersist = new (_dataToPersist().default)();
  }

  link() {
    var _this = this;

    return (0, _bluebird().coroutine)(function* () {
      const links = yield _this.getLinks();

      const linksResults = _this.getLinksResults();

      if (_this.consumer) links.addBasePath(_this.consumer.getPath());
      yield links.persistAllToFS();
      return linksResults;
    })();
  }

  getLinks() {
    var _this2 = this;

    return (0, _bluebird().coroutine)(function* () {
      _this2.dataToPersist = new (_dataToPersist().default)();
      yield _this2._populateShouldDependenciesSavedAsComponentsData();
      yield Promise.all(_this2.components.map(component => {
        const componentId = component.id.toString();

        _logger().default.debug(`linking component to node_modules: ${componentId}`);

        const componentMap = _this2.bitMap.getComponent(component.id);

        component.componentMap = componentMap;

        switch (componentMap.origin) {
          case _constants().COMPONENT_ORIGINS.IMPORTED:
            return _this2._populateImportedComponentsLinks(component);

          case _constants().COMPONENT_ORIGINS.NESTED:
            return _this2._populateNestedComponentsLinks(component);

          case _constants().COMPONENT_ORIGINS.AUTHORED:
            return _this2._populateAuthoredComponentsLinks(component);

          default:
            throw new Error(`ComponentMap.origin ${componentMap.origin} of ${componentId} is not recognized`);
        }
      }));
      return _this2.dataToPersist;
    })();
  }

  getLinksResults() {
    const linksResults = [];

    const getExistingLinkResult = id => linksResults.find(linkResult => linkResult.id.isEqual(id));

    const addLinkResult = (id, from, to) => {
      if (!id) return;
      const existingLinkResult = getExistingLinkResult(id);

      if (existingLinkResult) {
        existingLinkResult.bound.push({
          from,
          to
        });
      } else {
        linksResults.push({
          id,
          bound: [{
            from,
            to
          }]
        });
      }
    };

    this.dataToPersist.symlinks.forEach(symlink => {
      addLinkResult(symlink.componentId, symlink.src, symlink.dest);
    });
    this.dataToPersist.files.forEach(file => {
      addLinkResult(file.componentId, file.srcPath, file.path);
    });
    this.components.forEach(component => {
      const existingLinkResult = getExistingLinkResult(component.id);

      if (!existingLinkResult) {
        linksResults.push({
          id: component.id,
          bound: []
        });
      }
    });
    return linksResults;
  }

  _populateImportedComponentsLinks(component) {
    var _this3 = this;

    return (0, _bluebird().coroutine)(function* () {
      const componentMap = component.componentMap;
      const componentId = component.id;
      const bindingPrefix = _this3.consumer ? _this3.consumer.config.bindingPrefix : _constants().DEFAULT_BINDINGS_PREFIX;
      const linkPath = (0, _componentNodeModulesPath().default)(bindingPrefix, componentId, true); // when a user moves the component directory, use component.writtenPath to find the correct target
      // $FlowFixMe

      const srcTarget = component.writtenPath || componentMap.rootDir;
      const shouldDistsBeInsideTheComponent = _this3.consumer ? _this3.consumer.shouldDistsBeInsideTheComponent() : true;

      if (_this3.consumer && !component.dists.isEmpty() && component.dists.writeDistsFiles && !shouldDistsBeInsideTheComponent) {
        const distTarget = component.dists.getDistDir(_this3.consumer, componentMap.getRootDir());

        const packagesSymlinks = _this3._getSymlinkPackages(srcTarget, distTarget, component);

        _this3.dataToPersist.addManySymlinks(packagesSymlinks);

        const distSymlink = _symlink().default.makeInstance(distTarget, linkPath, componentId);

        distSymlink.forDistOutsideComponentsDir = true;

        _this3.dataToPersist.addSymlink(distSymlink);
      } else if (srcTarget !== '.') {
        // avoid creating symlinks from node_modules to itself
        _this3.dataToPersist.addSymlink(_symlink().default.makeInstance(srcTarget, linkPath, componentId));
      }

      yield _this3._populateDependenciesAndMissingLinks(component);
    })();
  }

  _populateNestedComponentsLinks(component) {
    var _this4 = this;

    return (0, _bluebird().coroutine)(function* () {
      yield _this4._populateDependenciesAndMissingLinks(component);
    })();
  }
  /**
   * authored components are linked only when they were exported before
   */


  _populateAuthoredComponentsLinks(component) {
    const componentId = component.id;
    if (!componentId.scope) return; // scope is a must to generate the link

    const filesToBind = component.componentMap.getFilesRelativeToConsumer();
    component.dists.updateDistsPerWorkspaceConfig(component.id, this.consumer, component.componentMap);
    filesToBind.forEach(file => {
      const isMain = file === component.componentMap.mainFile;
      const possiblyDist = component.dists.calculateDistFileForAuthored(_path().default.normalize(file), this.consumer, isMain);

      const dest = _path().default.join((0, _componentNodeModulesPath().default)(component.bindingPrefix, componentId), file);

      const destRelative = (0, _path2().getPathRelativeRegardlessCWD)(_path().default.dirname(dest), possiblyDist);
      const fileContent = (0, _linkContent().getLinkToFileContent)(destRelative);

      if (fileContent) {
        const linkFile = _linkFile().default.load({
          filePath: dest,
          content: fileContent,
          srcPath: file,
          componentId,
          override: true
        });

        this.dataToPersist.addFile(linkFile);
      } else {
        // it's an un-supported file, create a symlink instead
        this.dataToPersist.addSymlink(_symlink().default.makeInstance(file, dest, componentId));
      }
    });

    this._createPackageJsonForAuthor(component);
  }
  /**
   * for IMPORTED and NESTED components
   */


  _populateDependenciesAndMissingLinks(component) {
    var _this5 = this;

    return (0, _bluebird().coroutine)(function* () {
      // $FlowFixMe loaded from FS, componentMap must be set
      const componentMap = component.componentMap;

      if (component.hasDependencies()) {
        const dependenciesLinks = _this5._getDependenciesLinks(component, componentMap);

        _this5.dataToPersist.addManySymlinks(dependenciesLinks);
      }

      const missingDependenciesLinks = _this5.consumer && component.issues && component.issues.missingLinks ? _this5._getMissingLinks(component) : [];

      _this5.dataToPersist.addManySymlinks(missingDependenciesLinks);

      if (_this5.consumer && component.issues && component.issues.missingCustomModuleResolutionLinks) {
        const missingCustomResolvedLinks = yield _this5._getMissingCustomResolvedLinks(component);

        _this5.dataToPersist.addManyFiles(missingCustomResolvedLinks.files);

        _this5.dataToPersist.addManySymlinks(missingCustomResolvedLinks.symlinks);

        if (component.componentFromModel && component.componentFromModel.hasDependencies()) {
          // when custom-resolve links are missing, the component has been loaded without that
          // dependency. (see "deleting the link generated for the custom-module-resolution" test)
          // as a result, dependency links were not generated. our option is to get it from the scope
          const dependenciesLinks = _this5._getDependenciesLinks(component.componentFromModel, componentMap);

          _this5.dataToPersist.addManySymlinks(dependenciesLinks);
        }
      }
    })();
  }
  /**
   * When the dists is outside the components directory, it doesn't have access to the node_modules of the component's
   * root-dir. The solution is to go through the node_modules packages one by one and symlink them.
   */


  _getSymlinkPackages(from, to, component) {
    if (!this.consumer) throw new Error('getSymlinkPackages expects the Consumer to be defined');
    const dependenciesSavedAsComponents = component.dependenciesSavedAsComponents;

    const fromNodeModules = _path().default.join(from, 'node_modules');

    const toNodeModules = _path().default.join(to, 'node_modules');

    _logger().default.debug(`symlinkPackages for dists outside the component directory from ${fromNodeModules} to ${toNodeModules}`);

    const unfilteredDirs = _glob().default.sync('*', {
      cwd: fromNodeModules
    }); // when dependenciesSavedAsComponents the node_modules/@bit has real link files, we don't want to touch them
    // otherwise, node_modules/@bit has packages as any other directory in node_modules


    const dirsToFilter = dependenciesSavedAsComponents ? [this.consumer.config.bindingPrefix] : [];
    const customResolvedData = component.dependencies.getCustomResolvedData();

    if (!_ramda().default.isEmpty(customResolvedData)) {
      // filter out packages that are actually symlinks to dependencies
      Object.keys(customResolvedData).forEach(importSource => dirsToFilter.push((0, _utils().first)(importSource.split('/'))));
    }

    const dirs = dirsToFilter.length ? unfilteredDirs.filter(dir => !dirsToFilter.includes(dir)) : unfilteredDirs;
    if (!dirs.length) return [];
    return dirs.map(dir => {
      const fromDir = _path().default.join(fromNodeModules, dir);

      const toDir = _path().default.join(toNodeModules, dir);

      return _symlink().default.makeInstance(fromDir, toDir);
    });
  }

  _getDependenciesLinks(component, componentMap) {
    const getSymlinks = dependency => {
      const dependencyComponentMap = this.bitMap.getComponentIfExist(dependency.id);
      const dependenciesLinks = [];
      if (!dependencyComponentMap || !dependencyComponentMap.rootDir) return dependenciesLinks;
      const parentRootDir = componentMap.getRootDir();
      const dependencyRootDir = dependencyComponentMap.getRootDir();
      dependenciesLinks.push(this._getDependencyLink(parentRootDir, dependency.id, dependencyRootDir, component.bindingPrefix));

      if (this.consumer && !this.consumer.shouldDistsBeInsideTheComponent()) {
        // when dists are written outside the component, it doesn't matter whether a component
        // has dists files or not, in case it doesn't have, the files are copied from the component
        // dir into the dist dir. (see consumer-component.write())
        const from = component.dists.getDistDirForConsumer(this.consumer, parentRootDir);
        const to = component.dists.getDistDirForConsumer(this.consumer, dependencyRootDir);

        const distSymlink = this._getDependencyLink(from, dependency.id, to, component.bindingPrefix);

        distSymlink.forDistOutsideComponentsDir = true;
        dependenciesLinks.push(distSymlink);
      }

      return dependenciesLinks;
    };

    const symlinks = component.getAllDependencies().map(dependency => getSymlinks(dependency));
    return _ramda().default.flatten(symlinks);
  }

  _getMissingLinks(component) {
    const missingLinks = component.issues.missingLinks;
    const result = Object.keys(component.issues.missingLinks).map(key => {
      return missingLinks[key].map(dependencyIdRaw => {
        const dependencyId = this.bitMap.getBitId(dependencyIdRaw, {
          ignoreVersion: true
        });
        const dependencyComponentMap = this.bitMap.getComponent(dependencyId);
        if (!dependencyComponentMap.rootDir) return null;
        return this._getDependencyLink(component.componentMap.rootDir, dependencyId, dependencyComponentMap.rootDir, component.bindingPrefix);
      }).filter(x => x);
    });
    return _ramda().default.flatten(result);
  }

  _getDependencyLink(parentRootDir, bitId, rootDir, bindingPrefix) {
    const relativeDestPath = (0, _componentNodeModulesPath().default)(bindingPrefix, bitId, true);

    const destPathInsideParent = _path().default.join(parentRootDir, relativeDestPath);

    return _symlink().default.makeInstance(rootDir, destPathInsideParent, bitId);
  }

  _getMissingCustomResolvedLinks(component) {
    var _this6 = this;

    return (0, _bluebird().coroutine)(function* () {
      if (!component.componentFromModel) return new (_dataToPersist().default)();
      if (!_this6.consumer) throw new Error('_getMissingCustomResolvedLinks expects to have consumer set');
      const componentWithDependencies = yield component.toComponentWithDependencies(_this6.consumer);
      const missingLinks = component.issues.missingCustomModuleResolutionLinks;

      const dependenciesStr = _ramda().default.flatten(Object.keys(missingLinks).map(fileName => missingLinks[fileName]));

      component.copyDependenciesFromModel(dependenciesStr);
      const componentsDependenciesLinks = (0, _linkGenerator().getComponentsDependenciesLinks)([componentWithDependencies], _this6.consumer, false, _this6.bitMap);
      return componentsDependenciesLinks;
    })();
  }
  /**
   * create package.json on node_modules/@bit/component-name/package.json with a property 'main'
   * pointing to the component's main file.
   * It is needed for Authored components only.
   * Since an authored component doesn't have rootDir, it's impossible to symlink to the component directory.
   * It makes it easier for Author to use absolute syntax between their own components.
   */


  _createPackageJsonForAuthor(component) {
    const hasPackageJsonAsComponentFile = component.files.some(file => file.relative === _constants().PACKAGE_JSON);
    if (hasPackageJsonAsComponentFile) return; // don't generate package.json on top of the user package.json

    const dest = _path().default.join((0, _componentNodeModulesPath().default)(component.bindingPrefix, component.id));

    const packageJson = _packageJsonFile().default.createFromComponent(dest, component);

    this.dataToPersist.addFile(packageJson.toVinylFile());
  }
  /**
   * links are normally generated by `bit import`, `bit link` and `bit install`.
   * for `bit import` the data about whether dependenciesSavedAsComponents is already populated
   * for the rest, it's not.
   * @todo: avoid repopulating for imported. (not easy because by default, all components get "true").
   */


  _populateShouldDependenciesSavedAsComponentsData() {
    var _this7 = this;

    return (0, _bluebird().coroutine)(function* () {
      if (!_this7.components.length || !_this7.consumer) return;

      const bitIds = _this7.components.map(c => c.id);

      const shouldDependenciesSavedAsComponents = yield _this7.consumer.shouldDependenciesSavedAsComponents(bitIds);

      _this7.components.forEach(component => {
        const shouldSavedAsComponents = shouldDependenciesSavedAsComponents.find(c => c.id.isEqual(component.id));

        if (!shouldSavedAsComponents) {
          throw new Error(`_populateShouldDependenciesSavedAsComponentsData, saveDependenciesAsComponents is missing for ${component.id.toString()}`);
        }

        component.dependenciesSavedAsComponents = shouldSavedAsComponents.saveDependenciesAsComponents;
      });
    })();
  }

}

exports.default = NodeModuleLinker;