"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const cli_utils_1 = require("@pnpm/cli-utils");
const find_workspace_packages_1 = require("@pnpm/find-workspace-packages");
const logger_1 = require("@pnpm/logger");
const plugin_commands_rebuild_1 = require("@pnpm/plugin-commands-rebuild");
const pnpmfile_1 = require("@pnpm/pnpmfile");
const sort_packages_1 = require("@pnpm/sort-packages");
const store_connection_manager_1 = require("@pnpm/store-connection-manager");
const camelcaseKeys = require("camelcase-keys");
const isSubdir = require("is-subdir");
const mem = require("mem");
const fs = require("mz/fs");
const pFilter = require("p-filter");
const p_limit_1 = require("p-limit");
const path = require("path");
const readIniFile = require("read-ini-file");
const supi_1 = require("supi");
const updateWorkspaceDependencies_1 = require("./updateWorkspaceDependencies");
async function recursive(allProjects, input, opts, cmdFullName) {
    var _a, _b;
    if (allProjects.length === 0) {
        // It might make sense to throw an exception in this case
        return false;
    }
    const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package);
    if (pkgs.length === 0) {
        return false;
    }
    const manifestsByPath = {};
    for (const { dir, manifest, writeProjectManifest } of pkgs) {
        manifestsByPath[dir] = { manifest, writeProjectManifest };
    }
    const throwOnFail = cli_utils_1.throwOnCommandFail.bind(null, `pnpm recursive ${cmdFullName}`);
    const chunks = opts.sort !== false
        ? sort_packages_1.default(opts.selectedProjectsGraph)
        : [Object.keys(opts.selectedProjectsGraph).sort()];
    const store = await store_connection_manager_1.createOrConnectStoreController(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 = {
        ...store.ctrl,
        saveState: async () => undefined,
    };
    const workspacePackages = cmdFullName !== 'unlink'
        ? find_workspace_packages_1.arrayOfWorkspacePackagesToMap(allProjects)
        : {};
    const installOpts = Object.assign(opts, {
        ownLifecycleHooksStdio: 'pipe',
        peer: opts.savePeer,
        pruneLockfileImporters: (!opts.ignoredPackages || opts.ignoredPackages.size === 0)
            && pkgs.length === allProjects.length,
        storeController,
        storeDir: store.dir,
        workspacePackages,
        forceHoistPattern: typeof opts.rawLocalConfig['hoist-pattern'] !== 'undefined' || typeof opts.rawLocalConfig['hoist'] !== 'undefined',
        forceIndependentLeaves: typeof opts.rawLocalConfig['independent-leaves'] !== 'undefined',
        forceShamefullyHoist: typeof opts.rawLocalConfig['shamefully-hoist'] !== 'undefined',
    });
    const result = {
        fails: [],
        passes: 0,
    };
    const memReadLocalConfig = mem(readLocalConfig);
    async function getImporters() {
        const importers = [];
        await Promise.all(chunks.map((prefixes, buildIndex) => {
            if (opts.ignoredPackages) {
                prefixes = prefixes.filter((prefix) => !opts.ignoredPackages.has(prefix));
            }
            return Promise.all(prefixes.map(async (prefix) => {
                importers.push({
                    buildIndex,
                    manifest: manifestsByPath[prefix].manifest,
                    rootDir: prefix,
                });
            }));
        }));
        return importers;
    }
    const updateToLatest = opts.update && opts.latest;
    const include = (_a = opts.include, (_a !== null && _a !== void 0 ? _a : {
        dependencies: true,
        devDependencies: true,
        optionalDependencies: true,
    }));
    if (updateToLatest) {
        delete opts.include;
    }
    // For a workspace with shared lockfile
    if (opts.lockfileDir && ['add', 'install', 'remove', 'update'].includes(cmdFullName)) {
        if (opts.hoistPattern) {
            logger_1.default.info({ message: 'Only the root workspace package is going to have hoisted dependencies in node_modules', prefix: opts.lockfileDir });
        }
        let importers = await getImporters();
        const isFromWorkspace = isSubdir.bind(null, opts.lockfileDir);
        importers = await pFilter(importers, async ({ rootDir }) => isFromWorkspace(await fs.realpath(rootDir)));
        if (importers.length === 0)
            return true;
        const hooks = opts.ignorePnpmfile ? {} : pnpmfile_1.requireHooks(opts.lockfileDir, opts);
        const mutation = cmdFullName === 'remove' ? 'uninstallSome' : (input.length === 0 && !updateToLatest ? 'install' : 'installSome');
        const writeProjectManifests = [];
        const mutatedImporters = [];
        await Promise.all(importers.map(async ({ buildIndex, rootDir }) => {
            const localConfig = await memReadLocalConfig(rootDir);
            const { manifest, writeProjectManifest } = manifestsByPath[rootDir];
            let currentInput = [...input];
            if (updateToLatest) {
                if (!currentInput || !currentInput.length) {
                    currentInput = cli_utils_1.updateToLatestSpecsFromManifest(manifest, include);
                }
                else {
                    currentInput = cli_utils_1.createLatestSpecs(currentInput, manifest);
                    if (!currentInput.length) {
                        installOpts.pruneLockfileImporters = false;
                        return;
                    }
                }
            }
            if (opts.workspace) {
                if (!currentInput || !currentInput.length) {
                    currentInput = updateWorkspaceDependencies_1.updateToWorkspacePackagesFromManifest(manifest, include, workspacePackages);
                }
                else {
                    currentInput = updateWorkspaceDependencies_1.createWorkspaceSpecs(currentInput, workspacePackages);
                }
            }
            writeProjectManifests.push(writeProjectManifest);
            switch (mutation) {
                case 'uninstallSome':
                    mutatedImporters.push({
                        dependencyNames: currentInput,
                        manifest,
                        mutation,
                        rootDir,
                        targetDependenciesField: cli_utils_1.getSaveType(opts),
                    });
                    return;
                case 'installSome':
                    mutatedImporters.push({
                        allowNew: cmdFullName === 'install' || cmdFullName === 'add',
                        dependencySelectors: currentInput,
                        manifest,
                        mutation,
                        peer: opts.savePeer,
                        pinnedVersion: cli_utils_1.getPinnedVersion({
                            saveExact: typeof localConfig.saveExact === 'boolean' ? localConfig.saveExact : opts.saveExact,
                            savePrefix: typeof localConfig.savePrefix === 'string' ? localConfig.savePrefix : opts.savePrefix,
                        }),
                        rootDir,
                        targetDependenciesField: cli_utils_1.getSaveType(opts),
                    });
                    return;
                case 'install':
                    mutatedImporters.push({
                        buildIndex,
                        manifest,
                        mutation,
                        rootDir,
                    });
                    return;
            }
        }));
        const mutatedPkgs = await supi_1.mutateModules(mutatedImporters, {
            ...installOpts,
            hooks,
            storeController: store.ctrl,
        });
        if (opts.save !== false) {
            await Promise.all(mutatedPkgs
                .map(({ manifest }, index) => writeProjectManifests[index](manifest)));
        }
        return true;
    }
    let pkgPaths = chunks.length === 0
        ? chunks[0]
        : Object.keys(opts.selectedProjectsGraph).sort();
    const limitInstallation = p_limit_1.default((_b = opts.workspaceConcurrency, (_b !== null && _b !== void 0 ? _b : 4)));
    await Promise.all(pkgPaths.map((rootDir) => limitInstallation(async () => {
        const hooks = opts.ignorePnpmfile ? {} : pnpmfile_1.requireHooks(rootDir, opts);
        try {
            if (opts.ignoredPackages && opts.ignoredPackages.has(rootDir)) {
                return;
            }
            const { manifest, writeProjectManifest } = manifestsByPath[rootDir];
            let currentInput = [...input];
            if (updateToLatest) {
                if (!currentInput || !currentInput.length) {
                    currentInput = cli_utils_1.updateToLatestSpecsFromManifest(manifest, include);
                }
                else {
                    currentInput = cli_utils_1.createLatestSpecs(currentInput, manifest);
                    if (!currentInput.length)
                        return;
                }
            }
            let action; // tslint:disable-line:no-any
            switch (cmdFullName) {
                case 'unlink':
                    action = (currentInput.length === 0 ? unlink : unlinkPkgs.bind(null, currentInput));
                    break;
                case 'remove':
                    action = (manifest, opts) => supi_1.mutateModules([
                        {
                            dependencyNames: currentInput,
                            manifest,
                            mutation: 'uninstallSome',
                            rootDir,
                        },
                    ], opts);
                    break;
                default:
                    action = currentInput.length === 0
                        ? supi_1.install
                        : (manifest, opts) => supi_1.addDependenciesToPackage(manifest, currentInput, opts); // tslint:disable-line:no-any
                    break;
            }
            const localConfig = await memReadLocalConfig(rootDir);
            const newManifest = await action(manifest, {
                ...installOpts,
                ...localConfig,
                bin: path.join(rootDir, 'node_modules', '.bin'),
                dir: rootDir,
                hooks,
                ignoreScripts: true,
                pinnedVersion: cli_utils_1.getPinnedVersion({
                    saveExact: typeof localConfig.saveExact === 'boolean' ? localConfig.saveExact : opts.saveExact,
                    savePrefix: typeof localConfig.savePrefix === 'string' ? localConfig.savePrefix : opts.savePrefix,
                }),
                rawConfig: {
                    ...installOpts.rawConfig,
                    ...localConfig,
                },
                storeController,
            });
            if (opts.save !== false) {
                await writeProjectManifest(newManifest);
            }
            result.passes++;
        }
        catch (err) {
            logger_1.default.info(err);
            if (!opts.bail) {
                result.fails.push({
                    error: err,
                    message: err.message,
                    prefix: rootDir,
                });
                return;
            }
            err['prefix'] = rootDir; // tslint:disable-line:no-string-literal
            throw err;
        }
    })));
    await saveState();
    // The store should be unlocked because otherwise rebuild will not be able
    // to access it
    await storeController.close();
    if (!opts.lockfileOnly && !opts.ignoreScripts && (cmdFullName === 'add' ||
        cmdFullName === 'install' ||
        cmdFullName === 'update' ||
        cmdFullName === 'unlink')) {
        await plugin_commands_rebuild_1.rebuild.handler([], {
            ...opts,
            pending: opts.pending === true,
        });
    }
    throwOnFail(result);
    return true;
}
exports.default = recursive;
async function unlink(manifest, opts) {
    return supi_1.mutateModules([
        {
            manifest,
            mutation: 'unlink',
            rootDir: opts.dir,
        },
    ], opts);
}
async function unlinkPkgs(dependencyNames, manifest, opts) {
    return supi_1.mutateModules([
        {
            dependencyNames,
            manifest,
            mutation: 'unlinkSome',
            rootDir: opts.dir,
        },
    ], opts);
}
async function readLocalConfig(prefix) {
    try {
        const ini = await readIniFile(path.join(prefix, '.npmrc'));
        const config = camelcaseKeys(ini);
        if (config.shamefullyFlatten) {
            config.hoistPattern = '*';
            // TODO: print a warning
        }
        if (config.hoist === false) {
            config.hoistPattern = '';
        }
        return config;
    }
    catch (err) {
        if (err.code !== 'ENOENT')
            throw err;
        return {};
    }
}
