"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandLineConfiguration = void 0;
const node_core_library_1 = require("@rushstack/node-core-library");
const RushConstants_1 = require("../logic/RushConstants");
const command_line_schema_json_1 = __importDefault(require("../schemas/command-line.schema.json"));
const DEFAULT_BUILD_COMMAND_JSON = {
    commandKind: RushConstants_1.RushConstants.bulkCommandKind,
    name: RushConstants_1.RushConstants.buildCommandName,
    summary: "Build all projects that haven't been built, or have changed since they were last built.",
    description: 'This command is similar to "rush rebuild", except that "rush build" performs' +
        ' an incremental build. In other words, it only builds projects whose source files have changed' +
        ' since the last successful build. The analysis requires a Git working tree, and only considers' +
        ' source files that are tracked by Git and whose path is under the project folder. (For more details' +
        ' about this algorithm, see the documentation for the "package-deps-hash" NPM package.) The incremental' +
        ' build state is tracked in a per-project folder called ".rush/temp" which should NOT be added to Git. The' +
        ' build command is tracked by the "arguments" field in the "package-deps_build.json" file contained' +
        ' therein; a full rebuild is forced whenever the command has changed (e.g. "--production" or not).',
    safeForSimultaneousRushProcesses: false,
    enableParallelism: true,
    incremental: true
};
const DEFAULT_REBUILD_COMMAND_JSON = {
    commandKind: RushConstants_1.RushConstants.bulkCommandKind,
    name: RushConstants_1.RushConstants.rebuildCommandName,
    summary: 'Clean and rebuild the entire set of projects.',
    description: 'This command assumes that the package.json file for each project contains' +
        ' a "scripts" entry for "npm run build" that performs a full clean build.' +
        ` Rush invokes this script to build each project that is registered in ${RushConstants_1.RushConstants.rushJsonFilename}.` +
        ' Projects are built in parallel where possible, but always respecting the dependency' +
        ' graph for locally linked projects.  The number of simultaneous processes will be' +
        ' based on the number of machine cores unless overridden by the --parallelism flag.' +
        ' (For an incremental build, see "rush build" instead of "rush rebuild".)',
    safeForSimultaneousRushProcesses: false,
    enableParallelism: true,
    incremental: false
};
/**
 * This function replaces colons (":") with underscores ("_").
 *
 * ts-command-line restricts command names to lowercase letters, numbers, underscores, and colons.
 * Replacing colons with underscores produces a filesystem-safe name.
 */
function _normalizeNameForLogFilenameIdentifiers(name) {
    return name.replace(/:/g, '_'); // Replace colons with underscores to be filesystem-safe
}
/**
 * Custom Commands and Options for the Rush Command Line
 */
class CommandLineConfiguration {
    /**
     * Use CommandLineConfiguration.loadFromFile()
     *
     * @internal
     */
    constructor(commandLineJson, options = {}) {
        var _a, _b, _c;
        this.commands = new Map();
        this.phases = new Map();
        this.parameters = [];
        /**
         * These path will be prepended to the PATH environment variable
         */
        this.additionalPathFolders = [];
        /**
         * A map of bulk command names to their corresponding synthetic phase identifiers
         */
        this._syntheticPhasesByTranslatedBulkCommandName = new Map();
        const phasesJson = commandLineJson === null || commandLineJson === void 0 ? void 0 : commandLineJson.phases;
        if (phasesJson) {
            const phaseNameRegexp = new RegExp(`^${RushConstants_1.RushConstants.phaseNamePrefix}[a-z][a-z0-9]*([-][a-z0-9]+)*$`);
            for (const phase of phasesJson) {
                if (this.phases.has(phase.name)) {
                    throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, the phase "${phase.name}" is specified ` +
                        'more than once.');
                }
                if (!phase.name.match(phaseNameRegexp)) {
                    throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, the phase "${phase.name}"'s name ` +
                        'is not a valid phase name. Phase names must begin with the ' +
                        `required prefix "${RushConstants_1.RushConstants.phaseNamePrefix}" followed by a name containing ` +
                        'lowercase letters, numbers, or hyphens. The name must start with a letter and ' +
                        'must not end with a hyphen.');
                }
                if (phase.ignoreMissingScript !== undefined && phase.missingScriptBehavior !== undefined) {
                    throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, the phase "${phase.name}"'s defines ` +
                        'both "ignoreMissingScript" and "missingScriptBehavior". If using the "missingScriptBehavior", ' +
                        `remove "ignoreMissingScript", since it subsumes the functionality.`);
                }
                // This is a completely fresh object. Avoid use of the `...` operator in its construction
                // to guarantee monomorphism.
                const processedPhase = {
                    name: phase.name,
                    isSynthetic: false,
                    logFilenameIdentifier: _normalizeNameForLogFilenameIdentifiers(phase.name),
                    associatedParameters: new Set(),
                    dependencies: {
                        self: new Set(),
                        upstream: new Set()
                    },
                    missingScriptBehavior: (_a = phase.missingScriptBehavior) !== null && _a !== void 0 ? _a : (phase.ignoreMissingScript ? 'log' : 'error'),
                    allowWarningsOnSuccess: !!phase.allowWarningsOnSuccess
                };
                this.phases.set(phase.name, processedPhase);
            }
            // Resolve phase names to the underlying objects
            for (const rawPhase of phasesJson) {
                // The named phase not existing was already handled in the loop above
                const phase = this.phases.get(rawPhase.name);
                const selfDependencies = (_b = rawPhase.dependencies) === null || _b === void 0 ? void 0 : _b.self;
                const upstreamDependencies = (_c = rawPhase.dependencies) === null || _c === void 0 ? void 0 : _c.upstream;
                if (selfDependencies) {
                    for (const dependencyName of selfDependencies) {
                        const dependency = this.phases.get(dependencyName);
                        if (!dependency) {
                            throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, in the phase "${phase.name}", the self ` +
                                `dependency phase "${dependencyName}" does not exist.`);
                        }
                        phase.dependencies.self.add(dependency);
                    }
                }
                if (upstreamDependencies) {
                    for (const dependencyName of upstreamDependencies) {
                        const dependency = this.phases.get(dependencyName);
                        if (!dependency) {
                            throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, in the phase "${phase.name}", ` +
                                `the upstream dependency phase "${dependencyName}" does not exist.`);
                        }
                        phase.dependencies.upstream.add(dependency);
                    }
                }
            }
            // Do the recursive stuff after the dependencies have been converted
            const safePhases = new Set();
            const cycleDetector = new Set();
            for (const phase of this.phases.values()) {
                this._checkForPhaseSelfCycles(phase, cycleDetector, safePhases);
            }
        }
        const commandsJson = commandLineJson === null || commandLineJson === void 0 ? void 0 : commandLineJson.commands;
        let buildCommandPhases;
        let buildCommandOriginalPhases;
        if (commandsJson) {
            for (const command of commandsJson) {
                if (this.commands.has(command.name)) {
                    throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, the command "${command.name}" is specified ` +
                        'more than once.');
                }
                let normalizedCommand;
                switch (command.commandKind) {
                    case RushConstants_1.RushConstants.phasedCommandKind: {
                        const originalPhases = new Set();
                        const commandPhases = new Set();
                        const watchPhases = new Set();
                        normalizedCommand = {
                            ...command,
                            isSynthetic: false,
                            associatedParameters: new Set(),
                            originalPhases,
                            phases: commandPhases,
                            watchPhases,
                            alwaysWatch: false,
                            alwaysInstall: undefined
                        };
                        for (const phaseName of command.phases) {
                            const phase = this.phases.get(phaseName);
                            if (!phase) {
                                throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, in the "phases" property of the ` +
                                    `"${normalizedCommand.name}" command, the phase "${phaseName}" does not exist.`);
                            }
                            originalPhases.add(phase);
                            commandPhases.add(phase);
                        }
                        // Apply implicit phase dependency expansion
                        // The equivalent of the "--to" operator used for projects
                        // Appending to the set while iterating it accomplishes a full breadth-first search
                        for (const phase of commandPhases) {
                            for (const dependency of phase.dependencies.self) {
                                commandPhases.add(dependency);
                            }
                            for (const dependency of phase.dependencies.upstream) {
                                commandPhases.add(dependency);
                            }
                        }
                        const { watchOptions, installOptions } = command;
                        if (watchOptions) {
                            normalizedCommand.alwaysWatch = watchOptions.alwaysWatch;
                            normalizedCommand.watchDebounceMs = watchOptions.debounceMs;
                            // No implicit phase dependency expansion for watch mode.
                            for (const phaseName of watchOptions.watchPhases) {
                                const phase = this.phases.get(phaseName);
                                if (!phase) {
                                    throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, in the "watchPhases" property of the ` +
                                        `"${normalizedCommand.name}" command, the phase "${phaseName}" does not exist.`);
                                }
                                watchPhases.add(phase);
                            }
                        }
                        if (installOptions) {
                            normalizedCommand.alwaysInstall = installOptions.alwaysInstall;
                        }
                        break;
                    }
                    case RushConstants_1.RushConstants.globalCommandKind: {
                        normalizedCommand = {
                            ...command,
                            associatedParameters: new Set()
                        };
                        break;
                    }
                    case RushConstants_1.RushConstants.bulkCommandKind: {
                        // Translate the bulk command into a phased command
                        normalizedCommand = this._translateBulkCommandToPhasedCommand(command);
                        break;
                    }
                }
                if (normalizedCommand.name === RushConstants_1.RushConstants.buildCommandName ||
                    normalizedCommand.name === RushConstants_1.RushConstants.rebuildCommandName) {
                    if (normalizedCommand.commandKind === RushConstants_1.RushConstants.globalCommandKind) {
                        throw new Error(`${RushConstants_1.RushConstants.commandLineFilename} defines a command "${normalizedCommand.name}" using ` +
                            `the command kind "${RushConstants_1.RushConstants.globalCommandKind}". This command can only be designated as a command ` +
                            `kind "${RushConstants_1.RushConstants.bulkCommandKind}" or "${RushConstants_1.RushConstants.phasedCommandKind}".`);
                    }
                    else if (command.safeForSimultaneousRushProcesses) {
                        throw new Error(`${RushConstants_1.RushConstants.commandLineFilename} defines a command "${normalizedCommand.name}" using ` +
                            `"safeForSimultaneousRushProcesses=true". This configuration is not supported for "${normalizedCommand.name}".`);
                    }
                    else if (normalizedCommand.name === RushConstants_1.RushConstants.buildCommandName) {
                        // Record the build command phases in case we need to construct a synthetic "rebuild" command
                        buildCommandPhases = normalizedCommand.phases;
                        buildCommandOriginalPhases = normalizedCommand.originalPhases;
                    }
                }
                this.commands.set(normalizedCommand.name, normalizedCommand);
            }
        }
        if (!options.doNotIncludeDefaultBuildCommands) {
            let buildCommand = this.commands.get(RushConstants_1.RushConstants.buildCommandName);
            if (!buildCommand) {
                // If the build command was not specified in the config file, add the default build command
                buildCommand = this._translateBulkCommandToPhasedCommand(DEFAULT_BUILD_COMMAND_JSON);
                buildCommand.disableBuildCache = DEFAULT_BUILD_COMMAND_JSON.disableBuildCache;
                buildCommandPhases = buildCommand.phases;
                buildCommandOriginalPhases = buildCommand.originalPhases;
                this.commands.set(buildCommand.name, buildCommand);
            }
        }
        const buildCommand = this.commands.get(RushConstants_1.RushConstants.buildCommandName);
        if (buildCommand && !this.commands.has(RushConstants_1.RushConstants.rebuildCommandName)) {
            // If a rebuild command was not specified in the config file, add the default rebuild command
            if (!buildCommandPhases || !buildCommandOriginalPhases) {
                throw new Error(`Phases for the "${RushConstants_1.RushConstants.buildCommandName}" were not found.`);
            }
            const rebuildCommand = {
                ...DEFAULT_REBUILD_COMMAND_JSON,
                commandKind: RushConstants_1.RushConstants.phasedCommandKind,
                isSynthetic: true,
                phases: buildCommandPhases,
                disableBuildCache: DEFAULT_REBUILD_COMMAND_JSON.disableBuildCache,
                associatedParameters: buildCommand.associatedParameters, // rebuild should share build's parameters in this case,
                originalPhases: buildCommandOriginalPhases,
                watchPhases: new Set(),
                alwaysWatch: false,
                alwaysInstall: undefined
            };
            this.commands.set(rebuildCommand.name, rebuildCommand);
        }
        const parametersJson = commandLineJson === null || commandLineJson === void 0 ? void 0 : commandLineJson.parameters;
        if (parametersJson) {
            for (const parameter of parametersJson) {
                const normalizedParameter = {
                    ...parameter,
                    associatedPhases: parameter.associatedPhases ? [...parameter.associatedPhases] : [],
                    associatedCommands: parameter.associatedCommands ? [...parameter.associatedCommands] : []
                };
                this.parameters.push(normalizedParameter);
                // Do some basic validation
                switch (normalizedParameter.parameterKind) {
                    case 'choice': {
                        const alternativeNames = normalizedParameter.alternatives.map((x) => x.name);
                        if (normalizedParameter.defaultValue &&
                            alternativeNames.indexOf(normalizedParameter.defaultValue) < 0) {
                            throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, the parameter "${normalizedParameter.longName}",` +
                                ` specifies a default value "${normalizedParameter.defaultValue}"` +
                                ` which is not one of the defined alternatives: "${alternativeNames.toString()}"`);
                        }
                        break;
                    }
                }
                let parameterHasAssociatedCommands = false;
                if (normalizedParameter.associatedCommands) {
                    for (const associatedCommandName of normalizedParameter.associatedCommands) {
                        const syntheticPhase = this._syntheticPhasesByTranslatedBulkCommandName.get(associatedCommandName);
                        if (syntheticPhase) {
                            // If this parameter was associated with a bulk command, include the association
                            // with the synthetic phase
                            normalizedParameter.associatedPhases.push(syntheticPhase.name);
                        }
                        const associatedCommand = this.commands.get(associatedCommandName);
                        if (!associatedCommand) {
                            throw new Error(`${RushConstants_1.RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` +
                                `that is associated with a command "${associatedCommandName}" that is not defined in ` +
                                'this file.');
                        }
                        else {
                            associatedCommand.associatedParameters.add(normalizedParameter);
                            parameterHasAssociatedCommands = true;
                        }
                    }
                }
                if (normalizedParameter.associatedPhases) {
                    for (const associatedPhaseName of normalizedParameter.associatedPhases) {
                        const associatedPhase = this.phases.get(associatedPhaseName);
                        if (!associatedPhase) {
                            throw new Error(`${RushConstants_1.RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` +
                                `that is associated with a phase "${associatedPhaseName}" that does not exist.`);
                        }
                    }
                }
                if (!parameterHasAssociatedCommands) {
                    throw new Error(`${RushConstants_1.RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}"` +
                        ` that lists no associated commands.`);
                }
                // In the presence of plugins, there is utility to defining parameters that are associated with a phased
                // command but no phases. Don't enforce that a parameter is associated with at least one phase.
            }
        }
    }
    /**
     * Performs a depth-first search to detect cycles in the directed graph of phase "self" dependencies.
     *
     * @param phase The phase node currently being checked
     * @param phasesInPath The current path from the start node to `phase`
     * @param cycleFreePhases Phases that have already been fully walked and confirmed to not be in any cycles
     */
    _checkForPhaseSelfCycles(phase, phasesInPath, cycleFreePhases) {
        if (cycleFreePhases.has(phase)) {
            // phase is known to not be reachable from itself, i.e. not in a cycle. Skip.
            return;
        }
        for (const dependency of phase.dependencies.self) {
            if (phasesInPath.has(dependency)) {
                throw new Error(`In ${RushConstants_1.RushConstants.commandLineFilename}, there exists a cycle within the ` +
                    `set of ${dependency.name} dependencies: ${Array.from(phasesInPath, (phaseInPath) => phaseInPath.name).join(', ')}`);
            }
            else {
                phasesInPath.add(dependency);
                this._checkForPhaseSelfCycles(dependency, phasesInPath, cycleFreePhases);
                phasesInPath.delete(dependency);
            }
        }
        // phase is not reachable from itself, mark for skipping
        cycleFreePhases.add(phase);
    }
    static _applyBuildCommandDefaults(commandLineJson) {
        // merge commands specified in command-line.json and default (re)build settings
        // Ensure both build commands are included and preserve any other commands specified
        if (commandLineJson === null || commandLineJson === void 0 ? void 0 : commandLineJson.commands) {
            for (let i = 0; i < commandLineJson.commands.length; i++) {
                const command = commandLineJson.commands[i];
                // Determine if we have a set of default parameters
                let commandDefaultDefinition = {};
                switch (command.commandKind) {
                    case RushConstants_1.RushConstants.phasedCommandKind:
                    case RushConstants_1.RushConstants.bulkCommandKind: {
                        switch (command.name) {
                            case RushConstants_1.RushConstants.buildCommandName: {
                                commandDefaultDefinition = DEFAULT_BUILD_COMMAND_JSON;
                                break;
                            }
                            case RushConstants_1.RushConstants.rebuildCommandName: {
                                commandDefaultDefinition = DEFAULT_REBUILD_COMMAND_JSON;
                                break;
                            }
                        }
                        break;
                    }
                }
                // Merge the default parameters into the repo-specified parameters
                commandLineJson.commands[i] = {
                    ...commandDefaultDefinition,
                    ...command
                };
            }
        }
    }
    /**
     * Load the command-line.json configuration file from the specified path. Note that this
     * does not include the default build settings. This option is intended to be used to load
     * command-line.json files from plugins. To load a common/config/rush/command-line.json file,
     * use {@see loadFromFileOrDefault} instead.
     *
     * If the file does not exist, this function returns `undefined`
     */
    static tryLoadFromFile(jsonFilePath) {
        var _a, _b;
        let commandLineJson;
        try {
            commandLineJson = node_core_library_1.JsonFile.loadAndValidate(jsonFilePath, CommandLineConfiguration._jsonSchema);
        }
        catch (e) {
            if (!node_core_library_1.FileSystem.isNotExistError(e)) {
                throw e;
            }
        }
        if (commandLineJson) {
            this._applyBuildCommandDefaults(commandLineJson);
            const hasBuildCommand = !!((_a = commandLineJson.commands) === null || _a === void 0 ? void 0 : _a.some((command) => command.name === RushConstants_1.RushConstants.buildCommandName));
            const hasRebuildCommand = !!((_b = commandLineJson.commands) === null || _b === void 0 ? void 0 : _b.some((command) => command.name === RushConstants_1.RushConstants.rebuildCommandName));
            return new CommandLineConfiguration(commandLineJson, {
                doNotIncludeDefaultBuildCommands: !(hasBuildCommand || hasRebuildCommand)
            });
        }
        else {
            return undefined;
        }
    }
    /**
     * Loads the configuration from the specified file and applies any omitted default build
     * settings.  If the file does not exist, then a default instance is returned.
     * If the file contains errors, then an exception is thrown.
     */
    static loadFromFileOrDefault(jsonFilePath, doNotIncludeDefaultBuildCommands) {
        let commandLineJson = undefined;
        if (jsonFilePath) {
            try {
                commandLineJson = node_core_library_1.JsonFile.load(jsonFilePath);
            }
            catch (e) {
                if (!node_core_library_1.FileSystem.isNotExistError(e)) {
                    throw e;
                }
            }
            // merge commands specified in command-line.json and default (re)build settings
            // Ensure both build commands are included and preserve any other commands specified
            if (commandLineJson === null || commandLineJson === void 0 ? void 0 : commandLineJson.commands) {
                this._applyBuildCommandDefaults(commandLineJson);
                CommandLineConfiguration._jsonSchema.validateObject(commandLineJson, jsonFilePath);
            }
        }
        return new CommandLineConfiguration(commandLineJson, { doNotIncludeDefaultBuildCommands });
    }
    prependAdditionalPathFolder(pathFolder) {
        this.additionalPathFolders.unshift(pathFolder);
    }
    _translateBulkCommandToPhasedCommand(command) {
        const phaseName = command.name;
        const phase = {
            name: phaseName,
            isSynthetic: true,
            logFilenameIdentifier: _normalizeNameForLogFilenameIdentifiers(command.name),
            associatedParameters: new Set(),
            dependencies: {
                self: new Set(),
                upstream: new Set()
            },
            missingScriptBehavior: command.ignoreMissingScript ? 'log' : 'error',
            allowWarningsOnSuccess: !!command.allowWarningsInSuccessfulBuild,
            shellCommand: command.shellCommand
        };
        if (!command.ignoreDependencyOrder) {
            phase.dependencies.upstream.add(phase);
        }
        this.phases.set(phaseName, phase);
        this._syntheticPhasesByTranslatedBulkCommandName.set(command.name, phase);
        const phases = new Set([phase]);
        const translatedCommand = {
            ...command,
            commandKind: 'phased',
            isSynthetic: true,
            associatedParameters: new Set(),
            phases,
            originalPhases: phases,
            // Bulk commands used the same phases for watch as for regular execution. Preserve behavior.
            watchPhases: command.watchForChanges ? phases : new Set(),
            alwaysWatch: !!command.watchForChanges,
            alwaysInstall: undefined
        };
        return translatedCommand;
    }
}
exports.CommandLineConfiguration = CommandLineConfiguration;
CommandLineConfiguration._jsonSchema = node_core_library_1.JsonSchema.fromLoadedObject(command_line_schema_json_1.default);
//# sourceMappingURL=CommandLineConfiguration.js.map