"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.RushProjectConfiguration = void 0;
const node_core_library_1 = require("@rushstack/node-core-library");
const heft_config_file_1 = require("@rushstack/heft-config-file");
const rig_package_1 = require("@rushstack/rig-package");
const RushConstants_1 = require("../logic/RushConstants");
const OverlappingPathAnalyzer_1 = require("../utilities/OverlappingPathAnalyzer");
const rush_project_schema_json_1 = __importDefault(require("../schemas/rush-project.schema.json"));
const anything_schema_json_1 = __importDefault(require("../schemas/anything.schema.json"));
const HotlinkManager_1 = require("../utilities/HotlinkManager");
const RUSH_PROJECT_CONFIGURATION_FILE = new heft_config_file_1.ProjectConfigurationFile({
    projectRelativeFilePath: `config/${RushConstants_1.RushConstants.rushProjectConfigFilename}`,
    jsonSchemaObject: rush_project_schema_json_1.default,
    propertyInheritance: {
        operationSettings: {
            inheritanceType: heft_config_file_1.InheritanceType.custom,
            inheritanceFunction: (child, parent) => {
                if (!child) {
                    return parent;
                }
                else if (!parent) {
                    return child;
                }
                else {
                    // Merge any properties that need to be merged
                    const resultOperationSettingsByOperationName = new Map();
                    for (const parentOperationSettings of parent) {
                        resultOperationSettingsByOperationName.set(parentOperationSettings.operationName, parentOperationSettings);
                    }
                    const childEncounteredOperationNames = new Set();
                    for (const childOperationSettings of child) {
                        const operationName = childOperationSettings.operationName;
                        if (childEncounteredOperationNames.has(operationName)) {
                            // If the operation settings already exist, but didn't come from the parent, then
                            // it shows up multiple times in the child.
                            const childSourceFilePath = RUSH_PROJECT_CONFIGURATION_FILE.getObjectSourceFilePath(child);
                            throw new Error(`The operation "${operationName}" occurs multiple times in the "operationSettings" array ` +
                                `in "${childSourceFilePath}".`);
                        }
                        childEncounteredOperationNames.add(operationName);
                        let mergedOperationSettings = resultOperationSettingsByOperationName.get(operationName);
                        if (mergedOperationSettings) {
                            // The parent operation settings object already exists
                            const outputFolderNames = mergedOperationSettings.outputFolderNames && childOperationSettings.outputFolderNames
                                ? [
                                    ...mergedOperationSettings.outputFolderNames,
                                    ...childOperationSettings.outputFolderNames
                                ]
                                : mergedOperationSettings.outputFolderNames || childOperationSettings.outputFolderNames;
                            const dependsOnEnvVars = mergedOperationSettings.dependsOnEnvVars && childOperationSettings.dependsOnEnvVars
                                ? [
                                    ...mergedOperationSettings.dependsOnEnvVars,
                                    ...childOperationSettings.dependsOnEnvVars
                                ]
                                : mergedOperationSettings.dependsOnEnvVars || childOperationSettings.dependsOnEnvVars;
                            mergedOperationSettings = {
                                ...mergedOperationSettings,
                                ...childOperationSettings,
                                ...(outputFolderNames ? { outputFolderNames } : {}),
                                ...(dependsOnEnvVars ? { dependsOnEnvVars } : {})
                            };
                            resultOperationSettingsByOperationName.set(operationName, mergedOperationSettings);
                        }
                        else {
                            resultOperationSettingsByOperationName.set(operationName, childOperationSettings);
                        }
                    }
                    return Array.from(resultOperationSettingsByOperationName.values());
                }
            }
        },
        incrementalBuildIgnoredGlobs: {
            inheritanceType: heft_config_file_1.InheritanceType.replace
        }
    }
});
const OLD_RUSH_PROJECT_CONFIGURATION_FILE = new heft_config_file_1.ProjectConfigurationFile({
    projectRelativeFilePath: RUSH_PROJECT_CONFIGURATION_FILE.projectRelativeFilePath,
    jsonSchemaObject: anything_schema_json_1.default
});
/**
 * Use this class to load the "config/rush-project.json" config file.
 *
 * This file provides project-specific configuration options.
 * @alpha
 */
class RushProjectConfiguration {
    constructor(project, rushProjectJson, operationSettingsByOperationName) {
        this._validationCache = new WeakSet();
        this.project = project;
        this.incrementalBuildIgnoredGlobs = rushProjectJson.incrementalBuildIgnoredGlobs || [];
        this.disableBuildCacheForProject = rushProjectJson.disableBuildCacheForProject || false;
        this.operationSettingsByOperationName = operationSettingsByOperationName;
    }
    /**
     * Validates that the requested phases are compatible.
     * Deferral of this logic to its own method means that Rush no longer eagerly validates
     * all defined commands in command-line.json. As such, while validation will be run for a given
     * command upon invoking that command, defining overlapping phases in "rush custom-command"
     * that are not used by "rush build" will not cause "rush build" to exit with an error.
     */
    validatePhaseConfiguration(phases, terminal) {
        // Don't repeatedly validate the same set of phases for the same project.
        if (this._validationCache.has(phases)) {
            return;
        }
        const overlappingPathAnalyzer = new OverlappingPathAnalyzer_1.OverlappingPathAnalyzer();
        const { operationSettingsByOperationName, project } = this;
        let hasErrors = false;
        for (const phase of phases) {
            const operationName = phase.name;
            const operationSettings = operationSettingsByOperationName.get(operationName);
            if (operationSettings) {
                if (operationSettings.outputFolderNames) {
                    for (const outputFolderName of operationSettings.outputFolderNames) {
                        const otherOverlappingOperationNames = overlappingPathAnalyzer.addPathAndGetFirstEncounteredLabels(outputFolderName, operationName);
                        if (otherOverlappingOperationNames) {
                            const overlapsWithOwnOperation = otherOverlappingOperationNames === null || otherOverlappingOperationNames === void 0 ? void 0 : otherOverlappingOperationNames.includes(operationName);
                            if (overlapsWithOwnOperation) {
                                terminal.writeErrorLine(`The project "${project.packageName}" has a ` +
                                    `"${RUSH_PROJECT_CONFIGURATION_FILE.projectRelativeFilePath}" configuration that defines an ` +
                                    `operation with overlapping paths in the "outputFolderNames" list. The operation is ` +
                                    `"${operationName}", and the conflicting path is "${outputFolderName}".`);
                            }
                            else {
                                terminal.writeErrorLine(`The project "${project.packageName}" has a ` +
                                    `"${RUSH_PROJECT_CONFIGURATION_FILE.projectRelativeFilePath}" configuration that defines ` +
                                    'two operations in the same command whose "outputFolderNames" would overlap. ' +
                                    'Operations outputs in the same command must be disjoint so that they can be independently cached. ' +
                                    `The "${outputFolderName}" path overlaps between these operations: ` +
                                    `"${operationName}", "${otherOverlappingOperationNames.join('", "')}"`);
                            }
                            hasErrors = true;
                        }
                    }
                }
                // Validate that parameter names to ignore actually exist for this operation
                if (operationSettings.parameterNamesToIgnore) {
                    // Build a set of valid parameter names for this phase
                    const validParameterNames = new Set();
                    for (const parameter of phase.associatedParameters) {
                        validParameterNames.add(parameter.longName);
                    }
                    // Collect all invalid parameter names
                    const invalidParameterNames = [];
                    for (const parameterName of operationSettings.parameterNamesToIgnore) {
                        if (!validParameterNames.has(parameterName)) {
                            invalidParameterNames.push(parameterName);
                        }
                    }
                    // Report all invalid parameters in a single message
                    if (invalidParameterNames.length > 0) {
                        terminal.writeErrorLine(`The project "${project.packageName}" has a ` +
                            `"${RUSH_PROJECT_CONFIGURATION_FILE.projectRelativeFilePath}" configuration that specifies ` +
                            `invalid parameter(s) in "parameterNamesToIgnore" for operation "${operationName}": ` +
                            `${invalidParameterNames.join(', ')}. ` +
                            `Valid parameters for this operation are: ${Array.from(validParameterNames).sort().join(', ') || '(none)'}.`);
                        hasErrors = true;
                    }
                }
            }
        }
        this._validationCache.add(phases);
        if (hasErrors) {
            throw new node_core_library_1.AlreadyReportedError();
        }
    }
    /**
     * Examines the list of source files for the project and the target phase and returns a reason
     * why the project cannot enable the build cache for that phase, or undefined if it is safe to so do.
     */
    getCacheDisabledReason(trackedFileNames, phaseName, isNoOp) {
        const rushConfiguration = this.project.rushConfiguration;
        if (rushConfiguration) {
            const hotlinkManager = HotlinkManager_1.HotlinkManager.loadFromRushConfiguration(rushConfiguration);
            if (hotlinkManager.hasAnyHotlinksInSubspace(this.project.subspace.subspaceName)) {
                return 'Caching has been disabled for this project because it is in a subspace with hotlinked dependencies.';
            }
        }
        // Skip no-op operations as they won't have any output/cacheable things.
        if (isNoOp) {
            return undefined;
        }
        if (this.disableBuildCacheForProject) {
            return 'Caching has been disabled for this project.';
        }
        const operationSettings = this.operationSettingsByOperationName.get(phaseName);
        if (!operationSettings) {
            return `This project does not define the caching behavior of the "${phaseName}" command, so caching has been disabled.`;
        }
        if (operationSettings.disableBuildCacheForOperation) {
            return `Caching has been disabled for this project's "${phaseName}" command.`;
        }
        const { outputFolderNames } = operationSettings;
        if (!outputFolderNames) {
            return;
        }
        const normalizedProjectRelativeFolder = node_core_library_1.Path.convertToSlashes(this.project.projectRelativeFolder);
        const normalizedOutputFolders = outputFolderNames.map((outputFolderName) => `${normalizedProjectRelativeFolder}/${outputFolderName}/`);
        const inputOutputFiles = [];
        for (const file of trackedFileNames) {
            for (const outputFolder of normalizedOutputFolders) {
                if (file.startsWith(outputFolder)) {
                    inputOutputFiles.push(file);
                }
            }
        }
        if (inputOutputFiles.length > 0) {
            return ('The following files are used to calculate project state ' +
                `and are considered project output: ${inputOutputFiles.join(', ')}`);
        }
    }
    /**
     * Source of truth for whether a project is unable to use the build cache for a given phase.
     * As some operations may not have a rush-project.json file defined at all, but may be no-op operations
     *  we'll want to ignore those completely.
     */
    static getCacheDisabledReasonForProject(options) {
        const { projectConfiguration, trackedFileNames, phaseName, isNoOp } = options;
        if (isNoOp) {
            return undefined;
        }
        if (!projectConfiguration) {
            return (`Project does not have a ${RushConstants_1.RushConstants.rushProjectConfigFilename} configuration file, ` +
                'or one provided by a rig, so it does not support caching.');
        }
        return projectConfiguration.getCacheDisabledReason(trackedFileNames, phaseName, isNoOp);
    }
    /**
     * Loads the rush-project.json data for the specified project.
     */
    static async tryLoadForProjectAsync(project, terminal) {
        // false is a signal that the project config does not exist
        const cacheEntry = RushProjectConfiguration._configCache.get(project);
        if (cacheEntry !== undefined) {
            return cacheEntry || undefined;
        }
        const rushProjectJson = await this._tryLoadJsonForProjectAsync(project, terminal);
        if (rushProjectJson) {
            const result = RushProjectConfiguration._getRushProjectConfiguration(project, rushProjectJson, terminal);
            RushProjectConfiguration._configCache.set(project, result);
            return result;
        }
        else {
            RushProjectConfiguration._configCache.set(project, false);
            return undefined;
        }
    }
    /**
     * Load only the `incrementalBuildIgnoredGlobs` property from the rush-project.json file, skipping
     * validation of other parts of the config file.
     *
     * @remarks
     * This function exists to allow the ProjectChangeAnalyzer to load just the ignore globs without
     * having to validate the rest of the `rush-project.json` file against the repo's command-line configuration.
     */
    static async tryLoadIgnoreGlobsForProjectAsync(project, terminal) {
        const rushProjectJson = await this._tryLoadJsonForProjectAsync(project, terminal);
        return rushProjectJson === null || rushProjectJson === void 0 ? void 0 : rushProjectJson.incrementalBuildIgnoredGlobs;
    }
    /**
     * Load the rush-project.json data for all selected projects.
     * Validate compatibility of output folders across all selected phases.
     */
    static async tryLoadForProjectsAsync(projects, terminal) {
        const result = new Map();
        await node_core_library_1.Async.forEachAsync(projects, async (project) => {
            const projectConfig = await RushProjectConfiguration.tryLoadForProjectAsync(project, terminal);
            if (projectConfig) {
                result.set(project, projectConfig);
            }
        }, { concurrency: 50 });
        return result;
    }
    static async _tryLoadJsonForProjectAsync(project, terminal) {
        const rigConfig = await rig_package_1.RigConfig.loadForProjectFolderAsync({
            projectFolderPath: project.projectFolder
        });
        try {
            return await RUSH_PROJECT_CONFIGURATION_FILE.tryLoadConfigurationFileForProjectAsync(terminal, project.projectFolder, rigConfig);
        }
        catch (e1) {
            // Detect if the project is using the old rush-project.json schema
            let oldRushProjectJson;
            try {
                oldRushProjectJson =
                    await OLD_RUSH_PROJECT_CONFIGURATION_FILE.tryLoadConfigurationFileForProjectAsync(terminal, project.projectFolder, rigConfig);
            }
            catch (e2) {
                // Ignore
            }
            if ((oldRushProjectJson === null || oldRushProjectJson === void 0 ? void 0 : oldRushProjectJson.projectOutputFolderNames) ||
                (oldRushProjectJson === null || oldRushProjectJson === void 0 ? void 0 : oldRushProjectJson.phaseOptions) ||
                (oldRushProjectJson === null || oldRushProjectJson === void 0 ? void 0 : oldRushProjectJson.buildCacheOptions)) {
                throw new Error(`The ${RUSH_PROJECT_CONFIGURATION_FILE.projectRelativeFilePath} file appears to be ` +
                    'in an outdated format. Please see the UPGRADING.md notes for details. ' +
                    'Quick link: https://rushjs.io/link/upgrading');
            }
            else {
                throw e1;
            }
        }
    }
    static _getRushProjectConfiguration(project, rushProjectJson, terminal) {
        var _a;
        const operationSettingsByOperationName = new Map();
        let hasErrors = false;
        if (rushProjectJson.operationSettings) {
            for (const operationSettings of rushProjectJson.operationSettings) {
                const operationName = operationSettings.operationName;
                const existingOperationSettings = operationSettingsByOperationName.get(operationName);
                if (existingOperationSettings) {
                    const existingOperationSettingsJsonPath = RUSH_PROJECT_CONFIGURATION_FILE.getObjectSourceFilePath(existingOperationSettings);
                    const operationSettingsJsonPath = RUSH_PROJECT_CONFIGURATION_FILE.getObjectSourceFilePath(operationSettings);
                    hasErrors = true;
                    let errorMessage = `The operation "${operationName}" appears multiple times in the "${project.packageName}" project's ` +
                        `${RUSH_PROJECT_CONFIGURATION_FILE.projectRelativeFilePath} file's ` +
                        'operationSettings property.';
                    if (existingOperationSettingsJsonPath && operationSettingsJsonPath) {
                        if (existingOperationSettingsJsonPath !== operationSettingsJsonPath) {
                            errorMessage +=
                                ` It first appears in "${existingOperationSettingsJsonPath}" and again ` +
                                    `in "${operationSettingsJsonPath}".`;
                        }
                        else if (!node_core_library_1.Path.convertToSlashes(existingOperationSettingsJsonPath).startsWith(node_core_library_1.Path.convertToSlashes(project.projectFolder))) {
                            errorMessage += ` It appears multiple times in "${operationSettingsJsonPath}".`;
                        }
                    }
                    terminal.writeErrorLine(errorMessage);
                }
                else {
                    operationSettingsByOperationName.set(operationName, operationSettings);
                }
            }
            for (const [operationName, operationSettings] of operationSettingsByOperationName) {
                if ((_a = operationSettings.sharding) === null || _a === void 0 ? void 0 : _a.shardOperationSettings) {
                    terminal.writeWarningLine(`DEPRECATED: The "sharding.shardOperationSettings" field is deprecated. Please create a new operation, '${operationName}:shard' to track shard operation settings.`);
                }
            }
        }
        if (hasErrors) {
            throw new node_core_library_1.AlreadyReportedError();
        }
        return new RushProjectConfiguration(project, rushProjectJson, operationSettingsByOperationName);
    }
}
exports.RushProjectConfiguration = RushProjectConfiguration;
RushProjectConfiguration._configCache = new Map();
//# sourceMappingURL=RushProjectConfiguration.js.map