"use strict";

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

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getEntryPointsForComponent = getEntryPointsForComponent;
exports.getComponentsDependenciesLinks = getComponentsDependenciesLinks;
exports.getIndexFileName = getIndexFileName;
exports.getLinksByDependencies = getLinksByDependencies;
exports.getEntryPointForAngularComponent = getEntryPointForAngularComponent;

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 _lodash() {
  const data = _interopRequireDefault(require("lodash.groupby"));

  _lodash = function () {
    return data;
  };

  return data;
}

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

  _constants = function () {
    return data;
  };

  return data;
}

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

  _utils = function () {
    return data;
  };

  return data;
}

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

  _logger = function () {
    return data;
  };

  return data;
}

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

  _consumerComponent = function () {
    return data;
  };

  return data;
}

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

  _bitId = function () {
    return data;
  };

  return data;
}

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

  _componentMap = function () {
    return data;
  };

  return data;
}

function _postinstall() {
  const data = _interopRequireDefault(require("../consumer/component/templates/postinstall.default-template"));

  _postinstall = function () {
    return data;
  };

  return data;
}

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

  _linkContent = function () {
    return data;
  };

  return data;
}

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

  _dependencyFileLinkGenerator = function () {
    return data;
  };

  return data;
}

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

  _linkFile = function () {
    return data;
  };

  return data;
}

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

  _bitMap = function () {
    return data;
  };

  return data;
}

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

  _dataToPersist = function () {
    return data;
  };

  return data;
}

function _componentIdToPackageName() {
  const data = _interopRequireDefault(require("../utils/bit/component-id-to-package-name"));

  _componentIdToPackageName = function () {
    return data;
  };

  return data;
}

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

  _symlink = function () {
    return data;
  };

  return data;
}

// todo: move to bit-javascript
function getIndexFileName(mainFile) {
  return `${_constants().DEFAULT_INDEX_NAME}.${(0, _utils().getExt)(mainFile)}`;
}
/**
 * a component may have many dependencies, this function returns the links content for all of its dependencies
 */


function getComponentLinks({
  consumer,
  component,
  dependencies,
  createNpmLinkFiles,
  bitMap
}) {
  const componentMap = bitMap.getComponent(component.id);
  component.componentMap = componentMap;

  const directDependencies = _getDirectDependencies(component, componentMap, createNpmLinkFiles);

  const flattenedDependencies = _getFlattenedDependencies(component, componentMap, createNpmLinkFiles);

  const links = directDependencies.map(dep => {
    if (!dep.relativePaths || _ramda().default.isEmpty(dep.relativePaths)) return [];

    const getDependencyIdWithResolvedVersion = () => {
      // Check if the dependency is latest, if yes we need to resolve if from the flatten dependencies to get the
      // Actual version number, because on the bitmap we have only specific versions
      if (dep.id.getVersion().latest) {
        return flattenedDependencies.resolveVersion(dep.id);
      }

      return dep.id;
    };

    const dependencyId = getDependencyIdWithResolvedVersion();

    const dependencyComponent = _getDependencyComponent(dependencyId, dependencies, component.id);

    const dependencyLinks = dep.relativePaths.map(relativePath => {
      const dependencyFileLinkGenerator = new (_dependencyFileLinkGenerator().default)({
        consumer,
        bitMap,
        component,
        relativePath,
        dependencyComponent,
        createNpmLinkFiles
      });
      return dependencyFileLinkGenerator.generate();
    });
    return _ramda().default.flatten(dependencyLinks);
  });
  const dataToPersist = new (_dataToPersist().default)();
  const internalCustomResolvedLinks = component.customResolvedPaths.length ? getInternalCustomResolvedLinks(component, componentMap, createNpmLinkFiles) : [];

  const flattenLinks = _ramda().default.flatten(links).concat(internalCustomResolvedLinks);

  const {
    postInstallLinks,
    postInstallSymlinks,
    linksToWrite,
    symlinks
  } = groupLinks(flattenLinks);
  const shouldGeneratePostInstallScript = postInstallLinks.length || postInstallSymlinks.length;

  if (shouldGeneratePostInstallScript) {
    const postInstallFile = generatePostInstallScript(component, postInstallLinks, postInstallSymlinks);
    dataToPersist.addFile(postInstallFile);
  }

  const customResolveAliasesAdded = addCustomResolveAliasesToPackageJson(component, flattenLinks);

  if (customResolveAliasesAdded || shouldGeneratePostInstallScript) {
    // $FlowFixMe it has been verified above that component.packageJsonFile is not empty
    const packageJsonFile = component.packageJsonFile.toVinylFile();
    dataToPersist.addFile(packageJsonFile);
  }

  if (symlinks.length) {
    dataToPersist.addManySymlinks(symlinks.map(symlink => _symlink().default.makeInstance(symlink.source, symlink.dest)));
  } // $FlowFixMe


  dataToPersist.addManyFiles(linksToWrite.map(linkToWrite => _linkFile().default.load(linkToWrite)));
  return dataToPersist;
}

function _getDependencyComponent(dependencyId, dependencies, componentId) {
  const componentWithSameVersion = dependencies.find(dependency => dependency.id.isEqual(dependencyId));
  if (componentWithSameVersion) return componentWithSameVersion;
  const dependencyComponent = dependencies.find(dependency => dependency.id.isEqualWithoutVersion(dependencyId));

  if (!dependencyComponent) {
    const errorMessage = `link-generation: failed finding ${dependencyId.toString()} in the dependencies array of ${componentId.toString()}.
The dependencies array has the following ids: ${dependencies.map(d => d.id).join(', ')}`;
    throw new Error(errorMessage);
  }

  _logger().default.warn(`link-generation: failed finding an exact version of ${dependencyId.toString()} in the dependencies array of ${componentId.toString()}.
    will use ${dependencyComponent.id.toString()} instead. this might happen when the dependency version is overridden in package.json or bit.json`);

  return dependencyComponent;
}

function _getDirectDependencies(component, componentMap, createNpmLinkFiles) {
  // devDependencies of Nested components are not written to the filesystem, so no need to link them.
  return componentMap.origin === _constants().COMPONENT_ORIGINS.NESTED || createNpmLinkFiles ? component.dependencies.get() : component.getAllNonEnvsDependencies();
}

function _getFlattenedDependencies(component, componentMap, createNpmLinkFiles) {
  return componentMap.origin === _constants().COMPONENT_ORIGINS.NESTED || createNpmLinkFiles ? component.flattenedDependencies : _bitId().BitIds.fromArray(component.getAllNonEnvsFlattenedDependencies());
}

function groupLinks(flattenLinks) {
  const groupedLinks = (0, _lodash().default)(flattenLinks, link => link.linkPath);
  const linksToWrite = [];
  const postInstallLinks = [];
  const postInstallSymlinks = [];
  const symlinks = [];
  Object.keys(groupedLinks).forEach(group => {
    let content = '';
    const firstGroupItem = groupedLinks[group][0];

    if (firstGroupItem.symlinkTo) {
      if (firstGroupItem.postInstallSymlink) {
        postInstallSymlinks.push({
          source: firstGroupItem.symlinkTo,
          dest: firstGroupItem.linkPath
        });
        return;
      }

      symlinks.push({
        source: firstGroupItem.symlinkTo,
        dest: firstGroupItem.linkPath
      });
      return;
    }

    if (firstGroupItem.isEs6) {
      // check by the first item of the array, it can be any other item as well
      content = 'Object.defineProperty(exports, "__esModule", { value: true });\n';
    }

    content += groupedLinks[group].map(linkItem => linkItem.linkContent).join('\n');
    const linkFile = {
      filePath: group,
      content
    };

    if (firstGroupItem.postInstallLink) {
      postInstallLinks.push(linkFile);
    } else {
      linksToWrite.push(linkFile);
    }
  });
  return {
    postInstallLinks,
    postInstallSymlinks,
    linksToWrite,
    symlinks
  };
}
/**
 * The following scenario will help understanding why links are needed.
 * Component A has a dependency B. (for instance, in a.js there is a require statement to 'b.js').
 * While importing component A, it knows about the B dependency and it saves it under 'dependencies' directory of A.
 * The problem is that the above require is broken, because 'b.js' is not in the same place where it was originally.
 * This function solves this issue by creating the 'b.js' file in the original location and points to the new location
 * under 'dependencies' of A.
 *
 * It does the link generation in two steps.
 * step 1: "componentsLinks", it generates links to all imported components.
 * target: imported components. source: dependencies.
 * step 2: "dependenciesLinks", it generates links to all dependencies of the imported components.
 * target: dependencies. source: other dependencies.
 * this step is not needed when the imported components don't have dependencies, or when the
 * dependencies were installed as npm/yarn packages.
 */


function getComponentsDependenciesLinks(componentDependencies, consumer, createNpmLinkFiles, bitMap) {
  const componentsDependenciesLinks = new (_dataToPersist().default)();
  const linkedComponents = new (_bitId().BitIds)();
  const componentsToLink = getComponentsToLink();
  addLinksForComponents();
  addLinksForDependencies();
  return componentsDependenciesLinks;

  function getComponentsToLink() {
    return componentDependencies.reduce((acc, componentWithDeps) => {
      const component = componentWithDeps.component;
      const componentMap = bitMap.getComponent(component.id);
      component.componentMap = componentMap;

      if (componentMap.origin === _constants().COMPONENT_ORIGINS.AUTHORED) {
        _logger().default.debug(`writeComponentsDependenciesLinks, ignoring a component ${component.id.toString()} as it is an author component`);

        return acc;
      }

      return acc.concat(componentWithDeps);
    }, []);
  }

  function addLinksForComponents() {
    componentsToLink.forEach(componentWithDeps => {
      const component = componentWithDeps.component;
      if (linkedComponents.has(component.id)) return; // it must be IMPORTED. We don't pass NESTED to this function

      _logger().default.debug(`writeComponentsDependenciesLinks, generating links for ${component.id.toString()}`);

      const componentsLinks = getComponentLinks({
        consumer,
        component,
        dependencies: componentWithDeps.allDependencies,
        createNpmLinkFiles,
        bitMap
      });
      componentsDependenciesLinks.merge(componentsLinks);
      linkedComponents.push(component.id);
    });
  }

  function addLinksForDependencies() {
    componentsToLink.forEach(componentWithDeps => {
      if (!componentWithDeps.component.dependenciesSavedAsComponents) return;
      componentWithDeps.allDependencies.forEach(dep => {
        if (linkedComponents.has(dep.id)) return; // We pass here the componentWithDeps.dependencies again because it contains the full dependencies objects
        // also the indirect ones
        // The dep.dependencies contain only an id and relativePaths and not the full object

        const dependencies = componentWithDeps.allDependencies;
        dependencies.push(componentWithDeps.component);
        const dependencyLinks = getComponentLinks({
          consumer,
          component: dep,
          dependencies,
          createNpmLinkFiles,
          bitMap
        });
        componentsDependenciesLinks.merge(dependencyLinks);
        linkedComponents.push(dep.id);
      });
    });
  }
}
/**
 * important: do not attempt to move this function into DependencyFileLinkGenerator as it should be
 * running even when a component does not have any dependencies.
 *
 * when using custom module resolutions, and inside a component there is a file that requires
 * another file by custom-resolved syntax, we must generate links on the imported component inside
 * node_modules.
 *
 * E.g. original component "utils/jump" has two files:
 * bar/foo.js => require('utils/is-string); // "src" is set to be a module-directory
 * utils/is-string.js
 *
 * imported component:
 * components/utils/jump/bar/foo.js
 * components/utils/jump/utils/is-string.js
 * components/utils/jump/node_modules/utils/is-string // this is the file we generate here
 */


function getInternalCustomResolvedLinks(component, componentMap, createNpmLinkFiles) {
  const componentDir = component.writtenPath || componentMap.rootDir;

  if (!componentDir) {
    throw new Error(`getInternalCustomResolvedLinks, unable to find the written path of ${component.id.toString()}`);
  }

  const getDestination = importSource => `node_modules/${importSource}`;

  const invalidImportSources = ['.', '..']; // before v14.1.4 components might have an invalid importSource saved. see #1734

  const isResolvePathsInvalid = customPath => !invalidImportSources.includes(customPath.importSource);

  return component.customResolvedPaths.filter(customPath => isResolvePathsInvalid(customPath)).map(customPath => {
    const sourceAbs = _path().default.join(componentDir, customPath.destinationPath);

    const dest = getDestination(customPath.importSource);

    const destAbs = _path().default.join(componentDir, dest);

    const destRelative = _path().default.relative(_path().default.dirname(destAbs), sourceAbs);

    const linkContent = (0, _linkContent().getLinkToFileContent)(destRelative);
    const postInstallSymlink = createNpmLinkFiles && !linkContent;
    const packageName = (0, _componentIdToPackageName().default)(component.id, component.bindingPrefix);
    const customResolveMapping = {
      [customPath.importSource]: `${packageName}/${customPath.destinationPath}`
    };

    const getSymlink = () => {
      if (linkContent) return undefined;
      if (createNpmLinkFiles) return `${packageName}/${customPath.destinationPath}`;
      return sourceAbs;
    };

    return {
      linkPath: createNpmLinkFiles ? dest : destAbs,
      linkContent,
      postInstallLink: createNpmLinkFiles,
      customResolveMapping,
      symlinkTo: getSymlink(),
      postInstallSymlink
    };
  });
}
/**
 * change the package.json of a component to include postInstall script.
 * @see postInstallTemplate() JSDoc to understand better why this postInstall script is needed
 */


function generatePostInstallScript(component, postInstallLinks = [], postInstallSymlinks = []) {
  // $FlowFixMe todo: is it possible that writtenPath is empty here?
  const componentDir = component.writtenPath; // convert from array to object for easier parsing in the postinstall script

  const linkPathsObject = postInstallLinks.reduce((acc, val) => {
    acc[val.filePath] = val.content;
    return acc;
  }, {});
  const symlinkPathsObject = postInstallSymlinks.reduce((acc, val) => {
    acc[val.dest] = val.source;
    return acc;
  }, {});
  const postInstallCode = (0, _postinstall().default)(JSON.stringify(linkPathsObject), JSON.stringify(symlinkPathsObject));
  const POST_INSTALL_FILENAME = '.bit.postinstall.js';

  const postInstallFilePath = _path().default.join(componentDir, POST_INSTALL_FILENAME);

  const postInstallScript = `node ${POST_INSTALL_FILENAME}`;
  if (!component.packageJsonFile) throw new Error(`packageJsonFile is missing for ${component.id.toString()}`);
  component.packageJsonFile.addOrUpdateProperty('scripts', {
    postinstall: postInstallScript
  });

  const postInstallFile = _linkFile().default.load({
    filePath: postInstallFilePath,
    content: postInstallCode,
    override: true
  });

  return postInstallFile;
}

function addCustomResolveAliasesToPackageJson(component, links) {
  const resolveAliases = links.reduce((acc, link) => {
    if (link.customResolveMapping) Object.assign(acc, link.customResolveMapping);
    return acc;
  }, {});
  if (_ramda().default.isEmpty(resolveAliases)) return false;
  if (!component.packageJsonFile) return false; // e.g. author doesn't have package.json per component

  const bitProperty = component.packageJsonFile.getProperty('bit') || {};
  bitProperty.resolveAliases = resolveAliases;
  return true;
}
/**
 * Relevant for IMPORTED and NESTED only
 */


function getEntryPointsForComponent(component, consumer, bitMap) {
  const componentMap = bitMap.getComponent(component.id);
  if (componentMap.origin === _constants().COMPONENT_ORIGINS.AUTHORED) return [];
  const mainFile = component.dists.calculateMainDistFile(component.mainFile);
  const mainFileExt = (0, _utils().getExt)(mainFile);

  if (_linkContent().JAVASCRIPT_FLAVORS_EXTENSIONS.includes(mainFileExt) && component.packageJsonFile) {
    // throw new Error('hi')
    // if the main file is a javascript kind of file and the component has package.json file, no
    // need for an entry-point file because the "main" attribute of package.json takes care of that
    return [];
  }

  const files = [];
  const indexName = getIndexFileName(mainFile);
  const entryPointFileContent = (0, _linkContent().getLinkToFileContent)(`./${mainFile}`); // $FlowFixMe

  const componentRoot = component.writtenPath || componentMap.rootDir;

  const entryPointPath = _path().default.join(componentRoot, indexName);

  if (!component.dists.isEmpty() && component.dists.writeDistsFiles && consumer && !consumer.shouldDistsBeInsideTheComponent()) {
    const distDir = component.dists.getDistDirForConsumer(consumer, componentRoot);

    const entryPointDist = _path().default.join(distDir, indexName);

    _logger().default.debug(`writeEntryPointFile, on ${entryPointDist}`);

    files.push(_linkFile().default.load({
      filePath: entryPointDist,
      content: entryPointFileContent
    }));
  }

  _logger().default.debug(`writeEntryPointFile, on ${entryPointPath}`);

  files.push(_linkFile().default.load({
    filePath: entryPointPath,
    content: entryPointFileContent
  }));
  return files;
}

function getEntryPointForAngularComponent(component, consumer, bitMap) {
  if (!_isAngularComponent(component)) return null;
  const componentMap = bitMap.getComponent(component.id); // $FlowFixMe

  const componentRoot = component.writtenPath || componentMap.rootDir;
  if (componentMap.origin === _constants().COMPONENT_ORIGINS.AUTHORED) return null;
  const content = (0, _linkContent().getLinkToFileContent)(component.mainFile, []);

  const filePath = _path().default.join(componentRoot, _constants().ANGULAR_BIT_ENTRY_POINT_FILE);

  return _linkFile().default.load({
    filePath,
    content,
    override: false
  });
}

function _isAngularComponent(component) {
  return component.packageDependencies[_constants().ANGULAR_PACKAGE_IDENTIFIER] || component.peerPackageDependencies[_constants().ANGULAR_PACKAGE_IDENTIFIER];
}
/**
 * used for writing compiler and tester dependencies to the directory of their configuration file
 * the configuration directory is not always the same as the component, it can be moved by 'eject-conf' command
 * this methods write the environment dependency links no matter where the directory located on the workspace
 */


function getLinksByDependencies(_x, _x2, _x3, _x4, _x5) {
  return _getLinksByDependencies.apply(this, arguments);
}

function _getLinksByDependencies() {
  _getLinksByDependencies = (0, _bluebird().coroutine)(function* (targetDir, component, dependencies, consumer, bitMap) {
    // @todo: isolate consumer from this function for the Capsule.
    if (!consumer) throw new Error('getLinksByDependencies expects to get Consumer');
    const linksP = dependencies.get().map(
    /*#__PURE__*/
    function () {
      var _ref = (0, _bluebird().coroutine)(function* (dependency) {
        const dependencyComponent = yield consumer.loadComponentFromModel(dependency.id);
        const dependencyLinks = dependency.relativePaths.map(relativePath => {
          const dependencyFileLinkGenerator = new (_dependencyFileLinkGenerator().default)({
            consumer,
            bitMap,
            component,
            relativePath,
            dependencyComponent,
            createNpmLinkFiles: false,
            targetDir
          });
          return dependencyFileLinkGenerator.generate();
        });
        return _ramda().default.flatten(dependencyLinks);
      });

      return function (_x6) {
        return _ref.apply(this, arguments);
      };
    }());
    const links = yield Promise.all(linksP);

    const flattenLinks = _ramda().default.flatten(links);

    const {
      linksToWrite
    } = groupLinks(flattenLinks); // $FlowFixMe base is optional

    return linksToWrite.map(link => _linkFile().default.load(link));
  });
  return _getLinksByDependencies.apply(this, arguments);
}