"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const decorators_1 = require("./common/decorators");
const helpers_1 = require("./common/helpers");
const constants_1 = require("./constants");
class NodePackageManager {
    constructor($fs, $hostInfo, $errors, $childProcess, $logger, $httpClient) {
        this.$fs = $fs;
        this.$hostInfo = $hostInfo;
        this.$errors = $errors;
        this.$childProcess = $childProcess;
        this.$logger = $logger;
        this.$httpClient = $httpClient;
    }
    install(packageName, pathToSave, config) {
        return __awaiter(this, void 0, void 0, function* () {
            if (config.disableNpmInstall) {
                return;
            }
            if (config.ignoreScripts) {
                config["ignore-scripts"] = true;
            }
            const packageJsonPath = path.join(pathToSave, "package.json");
            const jsonContentBefore = this.$fs.readJson(packageJsonPath);
            const flags = this.getFlagsString(config, true);
            let params = ["install"];
            const isInstallingAllDependencies = packageName === pathToSave;
            if (!isInstallingAllDependencies) {
                params.push(packageName);
            }
            params = params.concat(flags);
            const cwd = pathToSave;
            const etcDirectoryLocation = path.join(cwd, "etc");
            const etcExistsPriorToInstallation = this.$fs.exists(etcDirectoryLocation);
            if (config.path) {
                let relativePathFromCwdToSource = "";
                if (config.frameworkPath) {
                    relativePathFromCwdToSource = path.relative(config.frameworkPath, pathToSave);
                    if (this.$fs.exists(relativePathFromCwdToSource)) {
                        packageName = relativePathFromCwdToSource;
                    }
                }
            }
            try {
                const spawnResult = yield this.getNpmInstallResult(params, cwd);
                if (isInstallingAllDependencies) {
                    return null;
                }
                params = params.concat(["--json", "--dry-run", "--prefix", cwd]);
                const spawnNpmDryRunResult = yield this.$childProcess.spawnFromEvent(this.getNpmExecutableName(), params, "close");
                return this.parseNpmInstallResult(spawnNpmDryRunResult.stdout, spawnResult.stdout, packageName);
            }
            catch (err) {
                if (err.message && err.message.indexOf("EPEERINVALID") !== -1) {
                    this.$logger.warn(err.message);
                }
                else {
                    this.$fs.writeJson(packageJsonPath, jsonContentBefore);
                    throw err;
                }
            }
            finally {
                if (!etcExistsPriorToInstallation) {
                    this.$fs.deleteDirectory(etcDirectoryLocation);
                }
            }
        });
    }
    uninstall(packageName, config, path) {
        return __awaiter(this, void 0, void 0, function* () {
            const flags = this.getFlagsString(config, false);
            return this.$childProcess.exec(`npm uninstall ${packageName} ${flags}`, { cwd: path });
        });
    }
    search(filter, config) {
        return __awaiter(this, void 0, void 0, function* () {
            const flags = this.getFlagsString(config, false);
            return this.$childProcess.exec(`npm search ${filter.join(" ")} ${flags}`);
        });
    }
    view(packageName, config) {
        return __awaiter(this, void 0, void 0, function* () {
            const wrappedConfig = _.extend({}, config, { json: true });
            const flags = this.getFlagsString(wrappedConfig, false);
            let viewResult;
            try {
                viewResult = yield this.$childProcess.exec(`npm view ${packageName} ${flags}`);
            }
            catch (e) {
                this.$errors.failWithoutHelp(e.message);
            }
            return JSON.parse(viewResult);
        });
    }
    searchNpms(keyword) {
        return __awaiter(this, void 0, void 0, function* () {
            const httpRequestResult = yield this.$httpClient.httpRequest(`https://api.npms.io/v2/search?q=keywords:${keyword}`);
            const result = JSON.parse(httpRequestResult.body);
            return result;
        });
    }
    getRegistryPackageData(packageName) {
        return __awaiter(this, void 0, void 0, function* () {
            const registry = yield this.$childProcess.exec(`npm config get registry`);
            const url = registry.trim() + packageName;
            this.$logger.trace(`Trying to get data from npm registry for package ${packageName}, url is: ${url}`);
            const responseData = (yield this.$httpClient.httpRequest(url)).body;
            this.$logger.trace(`Successfully received data from npm registry for package ${packageName}. Response data is: ${responseData}`);
            const jsonData = JSON.parse(responseData);
            this.$logger.trace(`Successfully parsed data from npm registry for package ${packageName}.`);
            return jsonData;
        });
    }
    getCachePath() {
        return __awaiter(this, void 0, void 0, function* () {
            const cachePath = yield this.$childProcess.exec(`npm config get cache`);
            return path.join(cachePath.trim(), constants_1.CACACHE_DIRECTORY_NAME);
        });
    }
    getNpmExecutableName() {
        let npmExecutableName = "npm";
        if (this.$hostInfo.isWindows) {
            npmExecutableName += ".cmd";
        }
        return npmExecutableName;
    }
    getFlagsString(config, asArray) {
        const array = [];
        for (const flag in config) {
            if (flag === "global") {
                array.push(`--${flag}`);
                array.push(`${config[flag]}`);
            }
            else if (config[flag]) {
                if (flag === "dist-tags" || flag === "versions") {
                    array.push(` ${flag}`);
                    continue;
                }
                array.push(`--${flag}`);
            }
        }
        if (asArray) {
            return array;
        }
        return array.join(" ");
    }
    parseNpmInstallResult(npmDryRunInstallOutput, npmInstallOutput, userSpecifiedPackageName) {
        try {
            const originalOutput = JSON.parse(npmDryRunInstallOutput);
            const npm5Output = originalOutput;
            const npmOutput = originalOutput;
            let name;
            _.forOwn(npmOutput.dependencies, (peerDependency, key) => {
                if (!peerDependency.required && !peerDependency.peerMissing) {
                    name = key;
                    return false;
                }
            });
            if (!name && npm5Output.updated) {
                const packageNameWithoutVersion = userSpecifiedPackageName.split('@')[0];
                const updatedDependency = _.find(npm5Output.updated, ['name', packageNameWithoutVersion]) || npm5Output.updated[0];
                return {
                    name: updatedDependency.name,
                    originalOutput,
                    version: updatedDependency.version
                };
            }
            const dependency = _.pick(npmOutput.dependencies, name);
            return {
                name,
                originalOutput,
                version: dependency[name].version
            };
        }
        catch (err) {
            this.$logger.trace(`Unable to parse result of npm --dry-run operation. Output is: ${npmDryRunInstallOutput}.`);
            this.$logger.trace("Now we'll try to parse the real output of npm install command.");
            const npmOutputMatchRegExp = /^.--\s+(?!UNMET)(.*)@((?:\d+\.){2}\d+)/m;
            const match = npmInstallOutput.match(npmOutputMatchRegExp);
            if (match) {
                return {
                    name: match[1],
                    version: match[2]
                };
            }
        }
        this.$logger.trace("Unable to get information from npm installation, trying to return value specified by user.");
        return this.getDependencyInformation(userSpecifiedPackageName);
    }
    getDependencyInformation(dependency) {
        const scopeDependencyMatch = dependency.match(NodePackageManager.SCOPED_DEPENDENCY_REGEXP);
        let name = null;
        let version = null;
        if (scopeDependencyMatch) {
            name = scopeDependencyMatch[1];
            version = scopeDependencyMatch[2];
        }
        else {
            const matches = dependency.match(NodePackageManager.DEPENDENCY_REGEXP);
            if (matches) {
                name = matches[1];
                version = matches[2];
            }
        }
        return {
            name,
            version
        };
    }
    getNpmInstallResult(params, cwd) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                const npmExecutable = this.getNpmExecutableName();
                const stdioValue = helpers_1.isInteractive() ? "inherit" : "pipe";
                const childProcess = this.$childProcess.spawn(npmExecutable, params, { cwd, stdio: stdioValue });
                let isFulfilled = false;
                let capturedOut = "";
                let capturedErr = "";
                if (childProcess.stdout) {
                    childProcess.stdout.on("data", (data) => {
                        this.$logger.write(data.toString());
                        capturedOut += data;
                    });
                }
                if (childProcess.stderr) {
                    childProcess.stderr.on("data", (data) => {
                        capturedErr += data;
                    });
                }
                childProcess.on("close", (arg) => {
                    const exitCode = typeof arg === "number" ? arg : arg && arg.code;
                    if (exitCode === 0) {
                        isFulfilled = true;
                        const result = {
                            stdout: capturedOut,
                            stderr: capturedErr,
                            exitCode
                        };
                        resolve(result);
                    }
                    else {
                        let errorMessage = `Command ${npmExecutable} ${params && params.join(" ")} failed with exit code ${exitCode}`;
                        if (capturedErr) {
                            errorMessage += ` Error output: \n ${capturedErr}`;
                        }
                        if (!isFulfilled) {
                            isFulfilled = true;
                            reject(new Error(errorMessage));
                        }
                    }
                });
                childProcess.on("error", (err) => {
                    if (!isFulfilled) {
                        isFulfilled = true;
                        reject(err);
                    }
                });
            });
        });
    }
}
NodePackageManager.SCOPED_DEPENDENCY_REGEXP = /^(@.+?)(?:@(.+?))?$/;
NodePackageManager.DEPENDENCY_REGEXP = /^(.+?)(?:@(.+?))?$/;
__decorate([
    decorators_1.exported("npm")
], NodePackageManager.prototype, "install", null);
__decorate([
    decorators_1.exported("npm")
], NodePackageManager.prototype, "uninstall", null);
__decorate([
    decorators_1.exported("npm")
], NodePackageManager.prototype, "search", null);
__decorate([
    decorators_1.exported("npm")
], NodePackageManager.prototype, "view", null);
__decorate([
    decorators_1.cache()
], NodePackageManager.prototype, "getCachePath", null);
exports.NodePackageManager = NodePackageManager;
$injector.register("npm", NodePackageManager);
