"use strict";
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 logger_1 = require("@pnpm/logger");
const camelcaseKeys = require("camelcase-keys");
const find_packages_1 = require("find-packages");
const graphSequencer = require("graph-sequencer");
const loadYamlFile = require("load-yaml-file");
const minimatch = require("minimatch");
const pLimit = require("p-limit");
const path = require("path");
const pkgs_graph_1 = require("pkgs-graph");
const R = require("ramda");
const readIniFile = require("read-ini-file");
const supi_1 = require("supi");
const createStoreController_1 = require("../../createStoreController");
const getCommandFullName_1 = require("../../getCommandFullName");
const requireHooks_1 = require("../../requireHooks");
const help_1 = require("../help");
const exec_1 = require("./exec");
const list_1 = require("./list");
const outdated_1 = require("./outdated");
const run_1 = require("./run");
const supportedRecursiveCommands = new Set([
    'install',
    'uninstall',
    'update',
    'link',
    'unlink',
    'list',
    'outdated',
    'rebuild',
    'run',
    'test',
    'exec',
]);
exports.default = (input, opts) => __awaiter(this, void 0, void 0, function* () {
    let concurrency = 4;
    if (!isNaN(parseInt(input[0], 10))) {
        concurrency = parseInt(input.shift(), 10);
    }
    if (concurrency < 1) {
        throw new Error('Concurrency should be at least 1');
    }
    const cmd = input.shift();
    if (!cmd) {
        help_1.default(['recursive']);
        return;
    }
    const cmdFullName = getCommandFullName_1.default(cmd);
    if (!supportedRecursiveCommands.has(cmdFullName)) {
        help_1.default(['recursive']);
        const err = new Error(`"recursive ${cmdFullName}" is not a pnpm command. See "pnpm help recursive".`);
        err['code'] = 'ERR_PNPM_INVALID_RECURSIVE_COMMAND'; // tslint:disable-line:no-string-literal
        throw err;
    }
    logger_1.default.warn('The recursive command is an experimental feature. Breaking changes may happen in non-major versions.');
    const cwd = process.cwd();
    const packagesManifest = yield requirePackagesManifest(cwd);
    let pkgs = yield find_packages_1.default(cwd, {
        ignore: [
            '**/node_modules/**',
            '**/bower_components/**',
        ],
        patterns: packagesManifest && packagesManifest.packages || undefined,
    });
    pkgs.sort((pkg1, pkg2) => pkg1.path.localeCompare(pkg2.path));
    const pkgGraphResult = pkgs_graph_1.default(pkgs);
    if (opts.scope) {
        pkgGraphResult.graph = filterGraph(pkgGraphResult.graph, opts.scope);
        pkgs = pkgs.filter((pkg) => pkgGraphResult.graph[pkg.path]);
    }
    switch (cmdFullName) {
        case 'list':
            yield list_1.default(pkgs, input, cmd, opts); // tslint:disable-line:no-any
            return;
        case 'outdated':
            yield outdated_1.default(pkgs, input, cmd, opts); // tslint:disable-line:no-any
            return;
        case 'test':
            return run_1.default(pkgs, ['test', ...input], cmd, Object.assign({}, opts, { concurrency })); // tslint:disable-line:no-any
        case 'run':
            return run_1.default(pkgs, input, cmd, Object.assign({}, opts, { concurrency })); // tslint:disable-line:no-any
        case 'update':
            opts = Object.assign({}, opts, { update: true, allowNew: false, concurrency }); // tslint:disable-line:no-any
            break;
        case 'exec':
            return exec_1.default(pkgs, input, cmd, Object.assign({}, opts, { concurrency })); // tslint:disable-line:no-any
    }
    const store = yield createStoreController_1.default(opts);
    // It is enough to save the store.json file once,
    // once all installations are done.
    // That's why saveState that is passed to the install engine
    // does nothing.
    const saveState = store.ctrl.saveState;
    const storeController = Object.assign({}, store.ctrl, { saveState: () => __awaiter(this, void 0, void 0, function* () { return undefined; }) });
    if (cmdFullName === 'link') {
        yield linkPackages(pkgGraphResult.graph, {
            registry: opts.registry,
            store: store.path,
            storeController,
        });
    }
    const graph = new Map(Object.keys(pkgGraphResult.graph).map((pkgPath) => [pkgPath, pkgGraphResult.graph[pkgPath].dependencies]));
    const graphSequencerResult = graphSequencer({
        graph,
        groups: [Object.keys(pkgGraphResult.graph)],
    });
    const chunks = graphSequencerResult.chunks;
    const installOpts = Object.assign(opts, {
        ownLifecycleHooksStdio: 'pipe',
        store: store.path,
        storeController,
    });
    const limitInstallation = pLimit(concurrency);
    let action; // tslint:disable-line:no-any
    switch (cmdFullName) {
        case 'unlink':
            action = (input.length === 0 ? supi_1.unlink : supi_1.unlinkPkgs.bind(null, input));
            break;
        case 'rebuild':
            action = (input.length === 0 ? supi_1.rebuild : supi_1.rebuildPkgs.bind(null, input));
            break;
        case 'uninstall':
            action = supi_1.uninstall.bind(null, input);
            break;
        default:
            action = (input.length === 0 ? supi_1.install : supi_1.installPkgs.bind(null, input));
            break;
    }
    for (const chunk of chunks) {
        yield Promise.all(chunk.map((prefix) => limitInstallation(() => __awaiter(this, void 0, void 0, function* () {
            const hooks = opts.ignorePnpmfile ? {} : requireHooks_1.default(prefix, opts);
            try {
                const localConfigs = yield readLocalConfigs(prefix);
                return yield action(Object.assign({}, installOpts, localConfigs, { bin: path.join(prefix, 'node_modules', '.bin'), hooks,
                    prefix, rawNpmConfig: Object.assign({}, installOpts.rawNpmConfig, localConfigs.rawNpmConfig), storeController }));
            }
            catch (err) {
                logger_1.default.info(err);
                err['prefix'] = prefix; // tslint:disable-line:no-string-literal
                throw err;
            }
        }))));
    }
    yield saveState();
});
function readLocalConfigs(prefix) {
    return __awaiter(this, void 0, void 0, function* () {
        try {
            const ini = yield readIniFile(path.join(prefix, '.npmrc'));
            return camelcaseKeys(ini);
        }
        catch (err) {
            if (err.code !== 'ENOENT')
                throw err;
            return {};
        }
    });
}
function linkPackages(graph, opts) {
    const limitLinking = pLimit(12);
    const linkOpts = Object.assign({}, opts, { skipInstall: true });
    return Promise.all(Object.keys(graph)
        .filter((pkgPath) => graph[pkgPath].dependencies && graph[pkgPath].dependencies.length)
        .map((pkgPath) => limitLinking(() => supi_1.link(graph[pkgPath].dependencies, path.join(pkgPath, 'node_modules'), Object.assign({}, linkOpts, { prefix: pkgPath })))));
}
function requirePackagesManifest(dir) {
    return __awaiter(this, void 0, void 0, function* () {
        try {
            return yield loadYamlFile(path.join(dir, 'pnpm-workspace.yaml'));
        }
        catch (err) {
            if (err['code'] === 'ENOENT') { // tslint:disable-line
                return null;
            }
            throw err;
        }
    });
}
function filterGraph(graph, scope) {
    const root = R.keys(graph).filter((id) => graph[id].manifest.name && minimatch(graph[id].manifest.name, scope));
    if (!root.length)
        return {};
    const subgraphNodeIds = new Set();
    pickSubgraph(graph, root, subgraphNodeIds);
    return R.pick(Array.from(subgraphNodeIds), graph);
}
function pickSubgraph(graph, nextNodeIds, walked) {
    for (const nextNodeId of nextNodeIds) {
        if (!walked.has(nextNodeId)) {
            walked.add(nextNodeId);
            pickSubgraph(graph, graph[nextNodeId].dependencies, walked);
        }
    }
}
//# sourceMappingURL=index.js.map