"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const logger_1 = require("@pnpm/logger");
const utils_1 = require("@pnpm/utils");
const camelcaseKeys = require("camelcase-keys");
const graphSequencer = require("graph-sequencer");
const isSubdir = require("is-subdir");
const mem = require("mem");
const fs = require("mz/fs");
const pFilter = require("p-filter");
const pLimit = require("p-limit");
const path = require("path");
const pkgs_graph_1 = require("pkgs-graph");
const readIniFile = require("read-ini-file");
const supi_1 = require("supi");
const createStoreController_1 = require("../../createStoreController");
const findWorkspacePackages_1 = require("../../findWorkspacePackages");
const getCommandFullName_1 = require("../../getCommandFullName");
const getPinnedVersion_1 = require("../../getPinnedVersion");
const loggers_1 = require("../../loggers");
const parsePackageSelectors_1 = require("../../parsePackageSelectors");
const requireHooks_1 = require("../../requireHooks");
const help_1 = require("../help");
const exec_1 = require("./exec");
const filter_1 = require("./filter");
const list_1 = require("./list");
const outdated_1 = require("./outdated");
const recursiveSummary_1 = require("./recursiveSummary");
const run_1 = require("./run");
const supportedRecursiveCommands = new Set([
    'install',
    'uninstall',
    'update',
    'unlink',
    'list',
    'outdated',
    'rebuild',
    'run',
    'test',
    'exec',
]);
exports.default = async (input, opts) => {
    if (opts.workspaceConcurrency < 1) {
        const err = new Error('Workspace concurrency should be at least 1');
        err['code'] = 'ERR_PNPM_INVALID_WORKSPACE_CONCURRENCY'; // tslint:disable-line:no-string-literal
        throw err;
    }
    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;
    }
    const workspacePrefix = opts.workspacePrefix || process.cwd();
    const allWorkspacePkgs = await findWorkspacePackages_1.default(workspacePrefix);
    if (!allWorkspacePkgs.length) {
        logger_1.default.info({ message: `No packages found in "${workspacePrefix}"`, prefix: workspacePrefix });
        return;
    }
    if (opts.filter) {
        // TODO: maybe @pnpm/config should return this in a parsed form already?
        // We don't use opts.prefix in this case because opts.prefix searches for a package.json in parent directories and
        // selects the directory where it finds one
        opts['packageSelectors'] = opts.filter.map((f) => parsePackageSelectors_1.default(f, process.cwd())); // tslint:disable-line
    }
    const atLeastOnePackageMatched = await recursive(allWorkspacePkgs, input, opts, cmdFullName, cmd);
    if (!atLeastOnePackageMatched) {
        logger_1.default.info({ message: `No packages matched the filters in "${workspacePrefix}"`, prefix: workspacePrefix });
        return;
    }
};
async function recursive(allPkgs, input, opts, cmdFullName, cmd) {
    if (allPkgs.length === 0) {
        // It might make sense to throw an exception in this case
        return false;
    }
    const pkgGraphResult = pkgs_graph_1.default(allPkgs);
    let pkgs;
    if (opts.packageSelectors && opts.packageSelectors.length) {
        pkgGraphResult.graph = filter_1.filterGraph(pkgGraphResult.graph, opts.packageSelectors);
        pkgs = allPkgs.filter((pkg) => pkgGraphResult.graph[pkg.path]);
    }
    else {
        pkgs = allPkgs;
    }
    if (pkgs.length === 0) {
        return false;
    }
    loggers_1.scopeLogger.debug({
        selected: pkgs.length,
        total: allPkgs.length,
        workspacePrefix: opts.workspacePrefix,
    });
    const throwOnFail = recursiveSummary_1.throwOnCommandFail.bind(null, `pnpm recursive ${cmd}`);
    switch (cmdFullName) {
        case 'list':
            await list_1.default(pkgs, input, cmd, opts); // tslint:disable-line:no-any
            return true;
        case 'outdated':
            await outdated_1.default(pkgs, input, cmd, opts); // tslint:disable-line:no-any
            return true;
    }
    const chunks = opts.sort
        ? sortPackages(pkgGraphResult.graph)
        : [Object.keys(pkgGraphResult.graph).sort()];
    switch (cmdFullName) {
        case 'test':
            throwOnFail(await run_1.default(chunks, pkgGraphResult.graph, ['test', ...input], cmd, opts)); // tslint:disable-line:no-any
            return true;
        case 'run':
            throwOnFail(await run_1.default(chunks, pkgGraphResult.graph, input, cmd, opts)); // tslint:disable-line:no-any
            return true;
        case 'update':
            opts = Object.assign({}, opts, { update: true, allowNew: false }); // tslint:disable-line:no-any
            break;
        case 'exec':
            throwOnFail(await exec_1.default(chunks, pkgGraphResult.graph, input, cmd, opts)); // tslint:disable-line:no-any
            return true;
    }
    const store = await 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: async () => undefined });
    const localPackages = opts.linkWorkspacePackages && cmdFullName !== 'unlink'
        ? findWorkspacePackages_1.arrayOfLocalPackagesToMap(allPkgs)
        : {};
    const installOpts = Object.assign(opts, {
        localPackages,
        ownLifecycleHooksStdio: 'pipe',
        pruneShrinkwrapImporters: (!opts.ignoredPackages || opts.ignoredPackages.size === 0)
            && pkgs.length === allPkgs.length,
        store: store.path,
        storeController,
        targetDependenciesField: utils_1.getSaveType(opts),
    });
    const result = {
        fails: [],
        passes: 0,
    };
    const memReadLocalConfigs = mem(readLocalConfigs);
    function getImporters() {
        const importers = [];
        chunks.forEach((prefixes, buildIndex) => {
            if (opts.ignoredPackages) {
                prefixes = prefixes.filter((prefix) => !opts.ignoredPackages.has(prefix));
            }
            prefixes.forEach((prefix) => {
                importers.push({ buildIndex, prefix });
            });
        });
        return importers;
    }
    if (cmdFullName !== 'rebuild') {
        if (opts.shrinkwrapDirectory && ['install', 'uninstall', 'update'].indexOf(cmdFullName) !== -1) {
            let importers = getImporters();
            const isFromWorkspace = isSubdir.bind(null, opts.shrinkwrapDirectory);
            importers = await pFilter(importers, async ({ prefix }) => isFromWorkspace(await fs.realpath(prefix)));
            if (importers.length === 0)
                return true;
            const hooks = opts.ignorePnpmfile ? {} : requireHooks_1.default(opts.shrinkwrapDirectory, opts);
            const mutation = cmdFullName === 'uninstall' ? 'uninstallSome' : (input.length === 0 ? 'install' : 'installSome');
            const mutatedImporters = await Promise.all(importers.map(async ({ buildIndex, prefix }) => {
                const localConfigs = await memReadLocalConfigs(prefix);
                const shamefullyFlatten = typeof localConfigs.shamefullyFlatten === 'boolean'
                    ? localConfigs.shamefullyFlatten
                    : opts.shamefullyFlatten;
                switch (mutation) {
                    case 'uninstallSome':
                        return {
                            dependencyNames: input,
                            mutation,
                            prefix,
                            shamefullyFlatten,
                            targetDependenciesField: utils_1.getSaveType(installOpts),
                        };
                    case 'installSome':
                        return {
                            allowNew: cmdFullName === 'install',
                            dependencySelectors: input,
                            mutation,
                            pinnedVersion: getPinnedVersion_1.default({
                                saveExact: typeof localConfigs.saveExact === 'boolean' ? localConfigs.saveExact : opts.saveExact,
                                savePrefix: typeof localConfigs.savePrefix === 'string' ? localConfigs.savePrefix : opts.savePrefix,
                            }),
                            prefix,
                            shamefullyFlatten,
                            targetDependenciesField: utils_1.getSaveType(installOpts),
                        };
                    case 'install':
                        return {
                            buildIndex,
                            mutation,
                            prefix,
                            shamefullyFlatten,
                        };
                }
            }));
            await supi_1.mutateModules(mutatedImporters, Object.assign({}, installOpts, { hooks, storeController: store.ctrl }));
            return true;
        }
        let pkgPaths = chunks.length === 0
            ? chunks[0]
            : Object.keys(pkgGraphResult.graph).sort();
        let action; // tslint:disable-line:no-any
        switch (cmdFullName) {
            case 'unlink':
                action = (input.length === 0 ? unlink : unlinkPkgs.bind(null, input));
                break;
            case 'uninstall':
                action = supi_1.uninstall.bind(null, input);
                break;
            default:
                action = input.length === 0 ? supi_1.install : supi_1.addDependenciesToPackage.bind(null, input);
                break;
        }
        const limitInstallation = pLimit(opts.workspaceConcurrency);
        await Promise.all(pkgPaths.map((prefix) => limitInstallation(async () => {
            const hooks = opts.ignorePnpmfile ? {} : requireHooks_1.default(prefix, opts);
            try {
                if (opts.ignoredPackages && opts.ignoredPackages.has(prefix)) {
                    return;
                }
                const localConfigs = await memReadLocalConfigs(prefix);
                await action(Object.assign({}, installOpts, localConfigs, { bin: path.join(prefix, 'node_modules', '.bin'), hooks, ignoreScripts: true, prefix, rawNpmConfig: Object.assign({}, installOpts.rawNpmConfig, localConfigs.rawNpmConfig), storeController }));
                result.passes++;
            }
            catch (err) {
                logger_1.default.info(err);
                if (!opts.bail) {
                    result.fails.push({
                        error: err,
                        message: err.message,
                        prefix,
                    });
                    return;
                }
                err['prefix'] = prefix; // tslint:disable-line:no-string-literal
                throw err;
            }
        })));
        await saveState();
    }
    if (cmdFullName === 'rebuild' ||
        !opts.shrinkwrapOnly && !opts.ignoreScripts && (cmdFullName === 'install' || cmdFullName === 'update' || cmdFullName === 'unlink')) {
        const action = (cmdFullName !== 'rebuild' || input.length === 0
            ? supi_1.rebuild
            : (importers, opts) => supi_1.rebuildPkgs(importers, input, opts) // tslint:disable-line
        );
        if (opts.shrinkwrapDirectory) {
            const importers = getImporters();
            await action(importers, Object.assign({}, installOpts, { pending: cmdFullName !== 'rebuild' || opts.pending === true }));
            return true;
        }
        const limitRebuild = pLimit(opts.workspaceConcurrency);
        for (const chunk of chunks) {
            await Promise.all(chunk.map((prefix) => limitRebuild(async () => {
                try {
                    if (opts.ignoredPackages && opts.ignoredPackages.has(prefix)) {
                        return;
                    }
                    const localConfigs = await memReadLocalConfigs(prefix);
                    await action([{ buildIndex: 0, prefix }], Object.assign({}, installOpts, localConfigs, { bin: path.join(prefix, 'node_modules', '.bin'), pending: cmdFullName !== 'rebuild' || opts.pending === true, prefix, rawNpmConfig: Object.assign({}, installOpts.rawNpmConfig, localConfigs.rawNpmConfig) }));
                    result.passes++;
                }
                catch (err) {
                    logger_1.default.info(err);
                    if (!opts.bail) {
                        result.fails.push({
                            error: err,
                            message: err.message,
                            prefix,
                        });
                        return;
                    }
                    err['prefix'] = prefix; // tslint:disable-line:no-string-literal
                    throw err;
                }
            })));
        }
    }
    throwOnFail(result);
    return true;
}
exports.recursive = recursive;
function unlink(opts) {
    return supi_1.mutateModules([
        {
            mutation: 'unlink',
            prefix: opts.prefix,
        },
    ], opts);
}
function unlinkPkgs(dependencyNames, opts) {
    return supi_1.mutateModules([
        {
            dependencyNames,
            mutation: 'unlinkSome',
            prefix: opts.prefix,
        },
    ], opts);
}
function sortPackages(pkgGraph) {
    const keys = Object.keys(pkgGraph);
    const setOfKeys = new Set(keys);
    const graph = new Map(keys.map((pkgPath) => [
        pkgPath,
        pkgGraph[pkgPath].dependencies.filter(
        /* remove cycles of length 1 (ie., package 'a' depends on 'a').  They
        confuse the graph-sequencer, but can be ignored when ordering packages
        topologically.

        See the following example where 'b' and 'c' depend on themselves:

          graphSequencer({graph: new Map([
            ['a', ['b', 'c']],
            ['b', ['b']],
            ['c', ['b', 'c']]]
          ),
          groups: [['a', 'b', 'c']]})

        returns chunks:

            [['b'],['a'],['c']]

        But both 'b' and 'c' should be executed _before_ 'a', because 'a' depends on
        them.  It works (and is considered 'safe' if we run:)

          graphSequencer({graph: new Map([
            ['a', ['b', 'c']],
            ['b', []],
            ['c', ['b']]]
          ), groups: [['a', 'b', 'c']]})

        returning:

            [['b'], ['c'], ['a']]

        */
        d => d !== pkgPath &&
            /* remove unused dependencies that we can ignore due to a filter expression.
    
            Again, the graph sequencer used to behave weirdly in the following edge case:
    
              graphSequencer({graph: new Map([
                ['a', ['b', 'c']],
                ['d', ['a']],
                ['e', ['a', 'b', 'c']]]
              ),
              groups: [['a', 'e', 'e']]})
    
            returns chunks:
    
                [['d'],['a'],['e']]
    
            But we really want 'a' to be executed first.
            */
            setOfKeys.has(d))
    ]));
    const graphSequencerResult = graphSequencer({
        graph,
        groups: [keys],
    });
    return graphSequencerResult.chunks;
}
async function readLocalConfigs(prefix) {
    try {
        const ini = await readIniFile(path.join(prefix, '.npmrc'));
        return camelcaseKeys(ini);
    }
    catch (err) {
        if (err.code !== 'ENOENT')
            throw err;
        return {};
    }
}
//# sourceMappingURL=index.js.map