"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelectionParameterSet = exports.SUBSPACE_LONG_ARG_NAME = void 0;
const node_core_library_1 = require("@rushstack/node-core-library");
const terminal_1 = require("@rushstack/terminal");
const Selection_1 = require("../../logic/Selection");
const GitChangedProjectSelectorParser_1 = require("../../logic/selectors/GitChangedProjectSelectorParser");
const NamedProjectSelectorParser_1 = require("../../logic/selectors/NamedProjectSelectorParser");
const TagProjectSelectorParser_1 = require("../../logic/selectors/TagProjectSelectorParser");
const VersionPolicyProjectSelectorParser_1 = require("../../logic/selectors/VersionPolicyProjectSelectorParser");
const SubspaceSelectorParser_1 = require("../../logic/selectors/SubspaceSelectorParser");
const PathProjectSelectorParser_1 = require("../../logic/selectors/PathProjectSelectorParser");
exports.SUBSPACE_LONG_ARG_NAME = '--subspace';
/**
 * This class is provides the set of command line parameters used to select projects
 * based on dependencies.
 *
 * It is a separate component such that unrelated actions can share the same parameters.
 */
class SelectionParameterSet {
    constructor(rushConfiguration, action, options) {
        const { gitOptions, includeSubspaceSelector, cwd } = options;
        this._rushConfiguration = rushConfiguration;
        const selectorParsers = new Map();
        const nameSelectorParser = new NamedProjectSelectorParser_1.NamedProjectSelectorParser(rushConfiguration);
        selectorParsers.set('name', nameSelectorParser);
        selectorParsers.set('git', new GitChangedProjectSelectorParser_1.GitChangedProjectSelectorParser(rushConfiguration, gitOptions));
        selectorParsers.set('tag', new TagProjectSelectorParser_1.TagProjectSelectorParser(rushConfiguration));
        selectorParsers.set('version-policy', new VersionPolicyProjectSelectorParser_1.VersionPolicyProjectSelectorParser(rushConfiguration));
        selectorParsers.set('subspace', new SubspaceSelectorParser_1.SubspaceSelectorParser(rushConfiguration));
        selectorParsers.set('path', new PathProjectSelectorParser_1.PathProjectSelectorParser(rushConfiguration, cwd));
        this._selectorParserByScope = selectorParsers;
        const getCompletionsAsync = async () => {
            const completions = ['.'];
            for (const [prefix, selector] of selectorParsers) {
                for (const completion of selector.getCompletions()) {
                    completions.push(`${prefix}:${completion}`);
                }
            }
            // Include completions from the name parser without a scope
            for (const completion of nameSelectorParser.getCompletions()) {
                completions.push(completion);
            }
            return completions;
        };
        this._toProject = action.defineStringListParameter({
            parameterLongName: '--to',
            parameterShortName: '-t',
            argumentName: 'PROJECT',
            description: 'Normally all projects in the monorepo will be processed;' +
                ' adding this parameter will instead select a subset of projects.' +
                ' Each "--to" parameter expands this selection to include PROJECT and all its dependencies.' +
                ' "." can be used as shorthand for the project in the current working directory.' +
                ' For details, refer to the website article "Selecting subsets of projects".',
            getCompletionsAsync
        });
        this._toExceptProject = action.defineStringListParameter({
            parameterLongName: '--to-except',
            parameterShortName: '-T',
            argumentName: 'PROJECT',
            description: 'Normally all projects in the monorepo will be processed;' +
                ' adding this parameter will instead select a subset of projects.' +
                ' Each "--to-except" parameter expands this selection to include all dependencies of PROJECT,' +
                ' but not PROJECT itself.' +
                ' "." can be used as shorthand for the project in the current working directory.' +
                ' For details, refer to the website article "Selecting subsets of projects".',
            getCompletionsAsync
        });
        this._fromProject = action.defineStringListParameter({
            parameterLongName: '--from',
            parameterShortName: '-f',
            argumentName: 'PROJECT',
            description: 'Normally all projects in the monorepo will be processed;' +
                ' adding this parameter will instead select a subset of projects.' +
                ' Each "--from" parameter expands this selection to include PROJECT and all projects that depend on it,' +
                ' plus all dependencies of this set.' +
                ' "." can be used as shorthand for the project in the current working directory.' +
                ' For details, refer to the website article "Selecting subsets of projects".',
            getCompletionsAsync
        });
        this._onlyProject = action.defineStringListParameter({
            parameterLongName: '--only',
            parameterShortName: '-o',
            argumentName: 'PROJECT',
            description: 'Normally all projects in the monorepo will be processed;' +
                ' adding this parameter will instead select a subset of projects.' +
                ' Each "--only" parameter expands this selection to include PROJECT; its dependencies are not added.' +
                ' "." can be used as shorthand for the project in the current working directory.' +
                ' Note that this parameter is "unsafe" as it may produce a selection that excludes some dependencies.' +
                ' For details, refer to the website article "Selecting subsets of projects".',
            getCompletionsAsync
        });
        this._impactedByProject = action.defineStringListParameter({
            parameterLongName: '--impacted-by',
            parameterShortName: '-i',
            argumentName: 'PROJECT',
            description: 'Normally all projects in the monorepo will be processed;' +
                ' adding this parameter will instead select a subset of projects.' +
                ' Each "--impacted-by" parameter expands this selection to include PROJECT and any projects that' +
                ' depend on PROJECT (and thus might be broken by changes to PROJECT).' +
                ' "." can be used as shorthand for the project in the current working directory.' +
                ' Note that this parameter is "unsafe" as it may produce a selection that excludes some dependencies.' +
                ' For details, refer to the website article "Selecting subsets of projects".',
            getCompletionsAsync
        });
        this._impactedByExceptProject = action.defineStringListParameter({
            parameterLongName: '--impacted-by-except',
            parameterShortName: '-I',
            argumentName: 'PROJECT',
            description: 'Normally all projects in the monorepo will be processed;' +
                ' adding this parameter will instead select a subset of projects.' +
                ' Each "--impacted-by-except" parameter works the same as "--impacted-by" except that PROJECT itself' +
                ' is not added to the selection.' +
                ' "." can be used as shorthand for the project in the current working directory.' +
                ' Note that this parameter is "unsafe" as it may produce a selection that excludes some dependencies.' +
                ' For details, refer to the website article "Selecting subsets of projects".',
            getCompletionsAsync
        });
        this._toVersionPolicy = action.defineStringListParameter({
            parameterLongName: '--to-version-policy',
            argumentName: 'VERSION_POLICY_NAME',
            description: 'Normally all projects in the monorepo will be processed;' +
                ' adding this parameter will instead select a subset of projects.' +
                ' The "--to-version-policy" parameter is equivalent to specifying "--to" for each of the projects' +
                ' belonging to VERSION_POLICY_NAME.' +
                ' For details, refer to the website article "Selecting subsets of projects".'
        });
        this._fromVersionPolicy = action.defineStringListParameter({
            parameterLongName: '--from-version-policy',
            argumentName: 'VERSION_POLICY_NAME',
            description: 'Normally all projects in the monorepo will be processed;' +
                ' adding this parameter will instead select a subset of projects.' +
                ' The "--from-version-policy" parameter is equivalent to specifying "--from" for each of the projects' +
                ' belonging to VERSION_POLICY_NAME.' +
                ' For details, refer to the website article "Selecting subsets of projects".'
        });
        if (includeSubspaceSelector) {
            this._subspaceParameter = action.defineStringParameter({
                parameterLongName: exports.SUBSPACE_LONG_ARG_NAME,
                argumentName: 'SUBSPACE_NAME',
                description: '(EXPERIMENTAL) Specifies a Rush subspace to be installed. Requires the "subspacesEnabled" feature to be enabled in subspaces.json.'
            });
        }
    }
    /**
     * Used to implement the `preventSelectingAllSubspaces` policy which checks for commands that accidentally
     * select everything.   Return `true` if the CLI was invoked with selection parameters.
     *
     * @remarks
     * It is still possible for a user to select everything, but they must do so using an explicit selection
     * such as `rush install --from thing-that-everything-depends-on`.
     */
    didUserSelectAnything() {
        var _a;
        if ((_a = this._subspaceParameter) === null || _a === void 0 ? void 0 : _a.value) {
            return true;
        }
        return [
            this._impactedByProject,
            this._impactedByExceptProject,
            this._onlyProject,
            this._toProject,
            this._fromProject,
            this._toExceptProject,
            this._fromVersionPolicy,
            this._toVersionPolicy
        ].some((x) => x.values.length > 0);
    }
    /**
     * Computes the set of selected projects based on all parameter values.
     *
     * If no parameters are specified, returns all projects in the Rush config file.
     */
    async getSelectedProjectsAsync(terminal) {
        var _a, _b;
        // Hack out the old version-policy parameters
        for (const value of this._fromVersionPolicy.values) {
            this._fromProject.values.push(`version-policy:${value}`);
        }
        for (const value of this._toVersionPolicy.values) {
            this._toProject.values.push(`version-policy:${value}`);
        }
        const selectors = [
            this._onlyProject,
            this._fromProject,
            this._toProject,
            this._toExceptProject,
            this._impactedByProject,
            this._impactedByExceptProject
        ];
        // Check if any of the selection parameters have a value specified on the command line
        const isSelectionSpecified = selectors.some((param) => param.values.length > 0) ||
            !!((_a = this._subspaceParameter) === null || _a === void 0 ? void 0 : _a.value);
        // If no selection parameters are specified, return everything
        if (!isSelectionSpecified) {
            return new Set(this._rushConfiguration.projects);
        }
        const [
        // Include exactly these projects (--only)
        onlyProjects, 
        // Include all projects that depend on these projects, and all dependencies thereof
        fromProjects, 
        // --to
        toRaw, 
        // --to-except
        toExceptProjects, 
        // --impacted-by
        impactedByProjects, 
        // --impacted-by-except
        impactedByExceptProjects] = await Promise.all(selectors.map((param) => {
            return this._evaluateProjectParameterAsync(param, terminal);
        }));
        let subspaceProjects = [];
        if ((_b = this._subspaceParameter) === null || _b === void 0 ? void 0 : _b.value) {
            if (!this._rushConfiguration.subspacesFeatureEnabled) {
                // eslint-disable-next-line no-console
                console.log();
                // eslint-disable-next-line no-console
                console.log(terminal_1.Colorize.red(`The "${exports.SUBSPACE_LONG_ARG_NAME}" parameter can only be passed if "subspacesEnabled" ` +
                    'is set to true in subspaces.json.'));
                throw new node_core_library_1.AlreadyReportedError();
            }
            const subspace = this._rushConfiguration.getSubspace(this._subspaceParameter.value);
            subspaceProjects = subspace.getProjects();
        }
        const selection = Selection_1.Selection.union(
        // Safe command line options
        Selection_1.Selection.expandAllDependencies(Selection_1.Selection.union(toRaw, Selection_1.Selection.directDependenciesOf(toExceptProjects), 
        // --from / --from-version-policy
        Selection_1.Selection.expandAllConsumers(fromProjects))), subspaceProjects, 
        // Unsafe command line option: --only
        onlyProjects, 
        // Unsafe command line options: --impacted-by, --impacted-by-except
        Selection_1.Selection.expandAllConsumers(Selection_1.Selection.union(impactedByProjects, Selection_1.Selection.directConsumersOf(impactedByExceptProjects))));
        return selection;
    }
    /**
     * Represents the selection as `--filter` parameters to pnpm.
     *
     * @remarks
     *
     * IMPORTANT: This function produces PNPM CLI operators that select projects from PNPM's temp workspace.
     * If Rush subspaces are enabled, PNPM cannot see the complete Rush workspace, and therefore these operators
     * would malfunction. In the current implementation, we calculate them anyway, then `BaseInstallAction.runAsync()`
     * will overwrite `pnpmFilterArgumentValues` with a flat list of project names.  In the future, these
     * two code paths will be combined into a single general solution.
     *
     * @see https://pnpm.io/filtering
     */
    async getPnpmFilterArgumentValuesAsync(terminal) {
        const args = [];
        // Include exactly these projects (--only)
        for (const project of await this._evaluateProjectParameterAsync(this._onlyProject, terminal)) {
            args.push(project.packageName);
        }
        // Include all projects that depend on these projects, and all dependencies thereof
        const fromProjects = Selection_1.Selection.union(
        // --from
        await this._evaluateProjectParameterAsync(this._fromProject, terminal));
        // All specified projects and all projects that they depend on
        for (const project of Selection_1.Selection.union(
        // --to
        await this._evaluateProjectParameterAsync(this._toProject, terminal), 
        // --from / --from-version-policy
        Selection_1.Selection.expandAllConsumers(fromProjects))) {
            args.push(`${project.packageName}...`);
        }
        // --to-except
        // All projects that the project directly or indirectly declares as a dependency
        for (const project of await this._evaluateProjectParameterAsync(this._toExceptProject, terminal)) {
            args.push(`${project.packageName}^...`);
        }
        // --impacted-by
        // The project and all projects directly or indirectly declare it as a dependency
        for (const project of await this._evaluateProjectParameterAsync(this._impactedByProject, terminal)) {
            args.push(`...${project.packageName}`);
        }
        // --impacted-by-except
        // All projects that directly or indirectly declare the specified project as a dependency
        for (const project of await this._evaluateProjectParameterAsync(this._impactedByExceptProject, terminal)) {
            args.push(`...^${project.packageName}`);
        }
        return args;
    }
    /**
     * Usage telemetry for selection parameters. Only saved locally, and if requested in the config.
     */
    getTelemetry() {
        return {
            command_from: `${this._fromProject.values.length > 0}`,
            command_impactedBy: `${this._impactedByProject.values.length > 0}`,
            command_impactedByExcept: `${this._impactedByExceptProject.values.length > 0}`,
            command_only: `${this._onlyProject.values.length > 0}`,
            command_to: `${this._toProject.values.length > 0}`,
            command_toExcept: `${this._toExceptProject.values.length > 0}`,
            command_fromVersionPolicy: `${this._fromVersionPolicy.values.length > 0}`,
            command_toVersionPolicy: `${this._toVersionPolicy.values.length > 0}`
        };
    }
    /**
     * Computes the referents of parameters that accept a project identifier.
     * Handles '.', unscoped names, and scoped names.
     */
    async _evaluateProjectParameterAsync(listParameter, terminal) {
        const parameterName = listParameter.longName;
        const selection = new Set();
        for (const rawSelector of listParameter.values) {
            const scopeIndex = rawSelector.indexOf(':');
            let scope;
            let unscopedSelector;
            if (scopeIndex < 0) {
                // No explicit scope - determine if this looks like a path
                // Check for relative paths: '.', '..', or those followed by '/' and more
                // Check for absolute POSIX paths: starting with '/'
                const isRelativePath = rawSelector === '.' ||
                    rawSelector === '..' ||
                    rawSelector.startsWith('./') ||
                    rawSelector.startsWith('../');
                const isAbsolutePosixPath = rawSelector.startsWith('/');
                if (isRelativePath || isAbsolutePosixPath) {
                    // Route to path: selector
                    scope = 'path';
                    unscopedSelector = rawSelector;
                }
                else {
                    // Default to name: selector
                    scope = 'name';
                    unscopedSelector = rawSelector;
                }
            }
            else {
                scope = rawSelector.slice(0, scopeIndex);
                unscopedSelector = rawSelector.slice(scopeIndex + 1);
            }
            const handler = this._selectorParserByScope.get(scope);
            if (!handler) {
                terminal.writeErrorLine(`Unsupported selector prefix "${scope}" passed to "${parameterName}": "${rawSelector}".` +
                    ` Supported prefixes: ${Array.from(this._selectorParserByScope.keys(), (selectorParserScope) => `"${selectorParserScope}:"`).join(', ')}`);
                throw new node_core_library_1.AlreadyReportedError();
            }
            for (const project of await handler.evaluateSelectorAsync({
                unscopedSelector,
                terminal,
                parameterName
            })) {
                selection.add(project);
            }
        }
        return selection;
    }
}
exports.SelectionParameterSet = SelectionParameterSet;
//# sourceMappingURL=SelectionParameterSet.js.map