"use strict";
/**********************************************************************
 * Copyright (c) 2018-2020 Red Hat, Inc.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 ***********************************************************************/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPackageVersion = exports.getFullPackageName = exports.Yarn = void 0;
const path = require("path");
const cli_error_1 = require("./cli-error");
const command_1 = require("./command");
const init_1 = require("./init");
const logger_1 = require("./logger");
/**
 * Handle the parsing of node packages with Yarn.
 * It allows to grab direct/production dependencies (not the dev dependencies)
 * @author Florent Benoit
 */
class Yarn {
    constructor(rootFolder, dependenciesDirectory, forbiddenPackages, excludedPackages) {
        this.rootFolder = rootFolder;
        this.dependenciesDirectory = dependenciesDirectory;
        this.forbiddenPackages = forbiddenPackages;
        this.excludedPackages = excludedPackages;
    }
    /**
     * Get package.json dependency paths (not including dev dependencies)
     */
    async getDependencies(rootModule) {
        // grab output of the command
        const command = new command_1.Command(this.dependenciesDirectory);
        const stdout = await command.exec(Yarn.YARN_GET_DEPENDENCIES);
        // Check that we've tree array
        const match = /^{"type":"tree","data":{"type":"list","trees":(.*)}}$/gm.exec(stdout);
        if (!match || match.length !== 2) {
            throw new cli_error_1.CliError('Not able to find a dependency tree when executing ' + Yarn.YARN_GET_DEPENDENCIES + '. Found ' + stdout);
        }
        // parse array into JSON
        const inputTrees = JSON.parse(match[1]);
        // Get node_modules folder
        const configStdout = await command.exec(Yarn.YARN_GET_CONFIG);
        const matchConfig = /^{"type":"log","data":"(.*)"}$/gm.exec(configStdout);
        if (!matchConfig || matchConfig.length !== 2) {
            throw new cli_error_1.CliError('Not able to get yarn configuration when executing ' + Yarn.YARN_GET_CONFIG + '. Found ' + configStdout);
        }
        // parse array into JSON
        const unescaped = matchConfig[1].replace(/\\n/g, '').replace(/\\"/g, '"');
        const jsonConfig = JSON.parse(unescaped);
        let nodeModulesFolder = jsonConfig.modulesFolder;
        if (!nodeModulesFolder) {
            nodeModulesFolder = path.resolve(this.rootFolder, 'node_modules');
        }
        // First, populate in a tree all the dependencies found by yarn
        const nodeTreeDependencies = new Map();
        inputTrees.map(yarnNode => this.insertNode(yarnNode, nodeTreeDependencies));
        // now, capture only expected dependencies for the given root module (so we drop some other dependencies that may be in yarn.lock file)
        const subsetDependencies = [];
        const initNode = nodeTreeDependencies.get(rootModule);
        if (!initNode) {
            throw new cli_error_1.CliError(`The initial module ${rootModule} was not found in dependencies`);
        }
        this.findDependencies(initNode, nodeTreeDependencies, subsetDependencies);
        // OK, now grab folders for each of these dependencies
        const nodePackages = [];
        subsetDependencies.forEach(moduleName => this.addNodePackage(nodeModulesFolder, moduleName, nodePackages));
        // return unique entries
        return Promise.resolve(nodePackages.map(e => e.path).filter((value, index, array) => index === array.indexOf(value)));
    }
    /**
     * Exclude dependencies which defined on excludedPackages
     *
     * @protected
     * @param {string} pkg
     * @returns
     * @memberof Yarn
     */
    isExcluded(pkg) {
        const exist = this.excludedPackages.indexOf(pkg) !== -1;
        if (exist) {
            logger_1.Logger.debug(` --> Excluding the dependency ${pkg}`);
            return true;
        }
        return false;
    }
    /**
     * Find from children all the direct dependencies. Also exclude some dependencies by not analyzing them.
     * Allow as well to report error in case of a forbidden dependency found
     * @param children the list of dependencies to analyze
     * @param nodeTreeDependencies the object containing the tree of dependencies
     * @param subsetDependencies  the
     */
    findDependencies(children, nodeTreeDependencies, subsetDependencies) {
        children.forEach(child => {
            // only loop on exist
            if (subsetDependencies.indexOf(child) >= 0 || this.isExcluded(child)) {
                return;
            }
            subsetDependencies.push(child);
            // loop on children in any
            let depChildren = nodeTreeDependencies.get(child);
            if (depChildren) {
                depChildren = depChildren.filter(depChild => !this.isExcluded(depChild));
                const matching = [];
                const foundForbiddenPackage = depChildren.some(r => {
                    const res = this.forbiddenPackages.indexOf(r) >= 0;
                    if (res) {
                        matching.push(r);
                    }
                    return res;
                });
                if (foundForbiddenPackage) {
                    throw new cli_error_1.CliError(`Forbidden dependencies ${matching} has been found as dependencies of ${child}` +
                        `Current dependencies: ${depChildren}, excluded list: ${this.forbiddenPackages}`);
                }
                this.findDependencies(depChildren, nodeTreeDependencies, subsetDependencies);
            }
        });
    }
    /**
     * Insert the given node into the Map/tree of dependencies
     * @param yarnNode the node to insert
     * @param nodeTreeDependencies the tree to update
     */
    insertNode(yarnNode, nodeTreeDependencies) {
        const npmModuleName = yarnNode.name.substring(0, yarnNode.name.lastIndexOf('@'));
        // init dependencies object if not existing
        let dependencies = nodeTreeDependencies.get(npmModuleName);
        if (!dependencies) {
            dependencies = [];
            nodeTreeDependencies.set(npmModuleName, dependencies);
        }
        // insert all children as well
        if (yarnNode.children) {
            yarnNode.children.map(child => {
                const childName = child.name.substring(0, child.name.lastIndexOf('@'));
                dependencies.push(childName);
                this.insertNode(child, nodeTreeDependencies);
            });
        }
    }
    /**
     * Add a node package (entry of yarn list) to the given array.
     * Also loop on all children and call ourself back
     * @param nodeModulesFolder the node_modules location
     * @param yarnNode the node entry to add
     * @param packages the array representing all node dependencies
     */
    async addNodePackage(nodeModulesFolder, moduleName, packages) {
        // build package
        const nodePackage = { name: moduleName, path: path.resolve(nodeModulesFolder, moduleName) };
        // add to the array
        packages.push(nodePackage);
    }
}
exports.Yarn = Yarn;
/**
 * Command to grab dependencies
 */
Yarn.YARN_GET_DEPENDENCIES = 'yarn list --json --prod';
/**
 * Command to grab yarn configuration.
 */
Yarn.YARN_GET_CONFIG = 'yarn config current --json';
/**
 * Returns the full package name of an installed package or the emtpy string if not installed
 */
async function getFullPackageName(rootFolder, name) {
    const pkg = JSON.parse(await new command_1.Command(path.resolve(rootFolder)).exec(init_1.Init.GET_PACKAGE_WITH_VERSION_CMD + name))
        .data.trees[0];
    return pkg ? pkg.name : '';
}
exports.getFullPackageName = getFullPackageName;
/**
 * Returns the version of an installed package or the emtpy string if not installed
 */
async function getPackageVersion(rootFolder, name) {
    const nameWithVersion = getFullPackageName(rootFolder, name);
    return (await nameWithVersion).split('@').pop() || '';
}
exports.getPackageVersion = getPackageVersion;
//# sourceMappingURL=yarn.js.map