"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createMatcher = exports.matchDependencies = void 0;
const cli_utils_1 = require("@pnpm/cli-utils");
const error_1 = require("@pnpm/error");
const find_workspace_packages_1 = require("@pnpm/find-workspace-packages");
const logger_1 = require("@pnpm/logger");
const manifest_utils_1 = require("@pnpm/manifest-utils");
const matcher_1 = require("@pnpm/matcher");
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 pLimit = require("p-limit");
const path = require("path");
const readIniFile = require("read-ini-file");
const supi_1 = require("supi");
const getPinnedVersion_1 = require("./getPinnedVersion");
const getSaveType_1 = require("./getSaveType");
const updateToLatestSpecsFromManifest_1 = require("./updateToLatestSpecsFromManifest");
const updateWorkspaceDependencies_1 = require("./updateWorkspaceDependencies");
async function recursive(allProjects, params, 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);
    const workspacePackages = cmdFullName !== 'unlink'
        ? find_workspace_packages_1.arrayOfWorkspacePackagesToMap(allProjects)
        : {};
    const targetDependenciesField = getSaveType_1.default(opts);
    const installOpts = Object.assign(opts, {
        linkWorkspacePackagesDepth: opts.linkWorkspacePackages === 'deep' ? Infinity : opts.linkWorkspacePackages ? 0 : -1,
        ownLifecycleHooksStdio: 'pipe',
        peer: opts.savePeer,
        pruneLockfileImporters: (!opts.ignoredPackages || opts.ignoredPackages.size === 0)
            && pkgs.length === allProjects.length,
        storeController: store.ctrl,
        storeDir: store.dir,
        targetDependenciesField,
        workspacePackages,
        forceHoistPattern: typeof opts.rawLocalConfig['hoist-pattern'] !== 'undefined' || typeof opts.rawLocalConfig['hoist'] !== '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 includeDirect = (_a = opts.includeDirect) !== null && _a !== void 0 ? _a : {
        dependencies: true,
        devDependencies: true,
        optionalDependencies: true,
    };
    const updateMatch = cmdFullName === 'update' && params.length ? createMatcher(params) : null;
    // For a workspace with shared lockfile
    if (opts.lockfileDir && ['add', 'install', 'remove', 'update'].includes(cmdFullName)) {
        let importers = await getImporters();
        const calculatedRepositoryRoot = calculateRepositoryRoot(opts.workspaceDir, importers.map(x => x.rootDir));
        const isFromWorkspace = isSubdir.bind(null, calculatedRepositoryRoot);
        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' : (params.length === 0 && !updateToLatest ? 'install' : 'installSome');
        const writeProjectManifests = [];
        const mutatedImporters = [];
        await Promise.all(importers.map(async ({ buildIndex, rootDir }) => {
            var _a;
            const localConfig = await memReadLocalConfig(rootDir);
            const modulesDir = (_a = localConfig.modulesDir) !== null && _a !== void 0 ? _a : opts.modulesDir;
            const { manifest, writeProjectManifest } = manifestsByPath[rootDir];
            let currentInput = [...params];
            if (updateMatch) {
                currentInput = matchDependencies(updateMatch, manifest, includeDirect);
                if (!currentInput.length) {
                    installOpts.pruneLockfileImporters = false;
                    return;
                }
            }
            if (updateToLatest) {
                if (!params || !params.length) {
                    currentInput = updateToLatestSpecsFromManifest_1.default(manifest, includeDirect);
                }
                else {
                    currentInput = updateToLatestSpecsFromManifest_1.createLatestSpecs(currentInput, manifest);
                    if (!currentInput.length) {
                        installOpts.pruneLockfileImporters = false;
                        return;
                    }
                }
            }
            if (opts.workspace) {
                if (!currentInput || !currentInput.length) {
                    currentInput = updateWorkspaceDependencies_1.updateToWorkspacePackagesFromManifest(manifest, includeDirect, workspacePackages);
                }
                else {
                    currentInput = updateWorkspaceDependencies_1.createWorkspaceSpecs(currentInput, workspacePackages);
                }
            }
            writeProjectManifests.push(writeProjectManifest);
            switch (mutation) {
                case 'uninstallSome':
                    mutatedImporters.push({
                        dependencyNames: currentInput,
                        manifest,
                        modulesDir,
                        mutation,
                        rootDir,
                        targetDependenciesField,
                    });
                    return;
                case 'installSome':
                    mutatedImporters.push({
                        allowNew: cmdFullName === 'install' || cmdFullName === 'add',
                        dependencySelectors: currentInput,
                        manifest,
                        modulesDir,
                        mutation,
                        peer: opts.savePeer,
                        pinnedVersion: getPinnedVersion_1.default({
                            saveExact: typeof localConfig.saveExact === 'boolean' ? localConfig.saveExact : opts.saveExact,
                            savePrefix: typeof localConfig.savePrefix === 'string' ? localConfig.savePrefix : opts.savePrefix,
                        }),
                        rootDir,
                        targetDependenciesField,
                    });
                    return;
                case 'install':
                    mutatedImporters.push({
                        buildIndex,
                        manifest,
                        modulesDir,
                        mutation,
                        rootDir,
                    });
                    return;
            }
        }));
        if (!mutatedImporters.length && cmdFullName === 'update') {
            throw new error_1.default('NO_PACKAGE_IN_DEPENDENCIES', 'None of the specified packages were found in the dependencies of any of the projects.');
        }
        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 = pLimit((_b = opts.workspaceConcurrency) !== 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 = [...params];
            if (updateMatch) {
                currentInput = matchDependencies(updateMatch, manifest, includeDirect);
                if (!currentInput.length)
                    return;
            }
            if (updateToLatest) {
                if (!params || !params.length) {
                    currentInput = updateToLatestSpecsFromManifest_1.default(manifest, includeDirect);
                }
                else {
                    currentInput = updateToLatestSpecsFromManifest_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 = async (manifest, opts) => {
                        const [{ manifest: newManifest }] = await supi_1.mutateModules([
                            {
                                dependencyNames: currentInput,
                                manifest,
                                mutation: 'uninstallSome',
                                rootDir,
                            },
                        ], opts);
                        return newManifest;
                    };
                    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: getPinnedVersion_1.default({
                    saveExact: typeof localConfig.saveExact === 'boolean' ? localConfig.saveExact : opts.saveExact,
                    savePrefix: typeof localConfig.savePrefix === 'string' ? localConfig.savePrefix : opts.savePrefix,
                }),
                rawConfig: {
                    ...installOpts.rawConfig,
                    ...localConfig,
                },
                storeController: store.ctrl,
            });
            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;
        }
    })));
    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);
    if (!result.passes && cmdFullName === 'update') {
        throw new error_1.default('NO_PACKAGE_IN_DEPENDENCIES', 'None of the specified packages were found in the dependencies of any of the projects.');
    }
    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 {};
    }
}
function calculateRepositoryRoot(workspaceDir, projectDirs) {
    // assume repo root is workspace dir
    let relativeRepoRoot = '.';
    for (const rootDir of projectDirs) {
        const relativePartRegExp = new RegExp(`^(\\.\\.\\${path.sep})+`);
        const relativePartMatch = relativePartRegExp.exec(path.relative(workspaceDir, rootDir));
        if (relativePartMatch) {
            const relativePart = relativePartMatch[0];
            if (relativePart.length > relativeRepoRoot.length) {
                relativeRepoRoot = relativePart;
            }
        }
    }
    return path.resolve(workspaceDir, relativeRepoRoot);
}
function matchDependencies(match, manifest, include) {
    const deps = Object.keys(manifest_utils_1.filterDependenciesByType(manifest, include));
    const matchedDeps = [];
    for (const dep of deps) {
        const spec = match(dep);
        if (spec === null)
            continue;
        matchedDeps.push(spec ? `${dep}@${spec}` : dep);
    }
    return matchedDeps;
}
exports.matchDependencies = matchDependencies;
function createMatcher(params) {
    const matchers = params.map((param) => {
        const atIndex = param.indexOf('@', 1);
        let pattern;
        let spec;
        if (atIndex === -1) {
            pattern = param;
            spec = '';
        }
        else {
            pattern = param.substr(0, atIndex);
            spec = param.substr(atIndex + 1);
        }
        return {
            match: matcher_1.default(pattern),
            spec,
        };
    });
    return (depName) => {
        for (const { spec, match } of matchers) {
            if (match(depName))
                return spec;
        }
        return null;
    };
}
exports.createMatcher = createMatcher;
//# sourceMappingURL=recursive.js.map