"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkspaceInstallManager = void 0;
const path = __importStar(require("node:path"));
const node_crypto_1 = require("node:crypto");
const semver = __importStar(require("semver"));
const js_yaml_1 = __importDefault(require("js-yaml"));
const node_core_library_1 = require("@rushstack/node-core-library");
const terminal_1 = require("@rushstack/terminal");
const BaseInstallManager_1 = require("../base/BaseInstallManager");
const DependencySpecifier_1 = require("../DependencySpecifier");
const PackageJsonEditor_1 = require("../../api/PackageJsonEditor");
const PnpmWorkspaceFile_1 = require("../pnpm/PnpmWorkspaceFile");
const RushConstants_1 = require("../RushConstants");
const Utilities_1 = require("../../utilities/Utilities");
const InstallHelpers_1 = require("./InstallHelpers");
const EnvironmentConfiguration_1 = require("../../api/EnvironmentConfiguration");
const ShrinkwrapFileFactory_1 = require("../ShrinkwrapFileFactory");
const BaseProjectShrinkwrapFile_1 = require("../base/BaseProjectShrinkwrapFile");
const CustomTipsConfiguration_1 = require("../../api/CustomTipsConfiguration");
const BaseLinkManager_1 = require("../base/BaseLinkManager");
const FlagFile_1 = require("../../api/FlagFile");
const Stopwatch_1 = require("../../utilities/Stopwatch");
/**
 * This class implements common logic between "rush install" and "rush update".
 */
class WorkspaceInstallManager extends BaseInstallManager_1.BaseInstallManager {
    /**
     * @override
     */
    async doInstallAsync() {
        // TODO: Remove when "rush link" and "rush unlink" are deprecated
        if (this.options.noLink) {
            // eslint-disable-next-line no-console
            console.log(terminal_1.Colorize.red('The "--no-link" option was provided but is not supported when using workspaces. Run the command again ' +
                'without specifying this argument.'));
            throw new node_core_library_1.AlreadyReportedError();
        }
        await super.doInstallAsync();
    }
    /**
     * Regenerates the common/temp/package.json and related workspace files.
     * If shrinkwrapFile is provided, this function also validates whether it contains
     * everything we need to install and returns true if so; in all other cases,
     * the return value is false.
     *
     * @override
     */
    async prepareCommonTempAsync(subspace, shrinkwrapFile) {
        var _a;
        // Block use of the RUSH_TEMP_FOLDER environment variable
        if (EnvironmentConfiguration_1.EnvironmentConfiguration.rushTempFolderOverride !== undefined) {
            throw new Error('The RUSH_TEMP_FOLDER environment variable is not compatible with workspace installs. If attempting ' +
                'to move the PNPM store path, see the `RUSH_PNPM_STORE_PATH` environment variable.');
        }
        const { fullUpgrade, allowShrinkwrapUpdates, variant } = this.options;
        // eslint-disable-next-line no-console
        console.log('\n' + terminal_1.Colorize.bold('Updating workspace files in ' + subspace.getSubspaceTempFolderPath()));
        const shrinkwrapWarnings = [];
        // We will start with the assumption that it's valid, and then set it to false if
        // any of the checks fail
        let shrinkwrapIsUpToDate = true;
        if (!shrinkwrapFile) {
            shrinkwrapIsUpToDate = false;
        }
        else {
            if (!shrinkwrapFile.isWorkspaceCompatible && !fullUpgrade) {
                // eslint-disable-next-line no-console
                console.log();
                // eslint-disable-next-line no-console
                console.log(terminal_1.Colorize.red('The shrinkwrap file has not been updated to support workspaces. Run "rush update --full" to update ' +
                    'the shrinkwrap file.'));
                throw new node_core_library_1.AlreadyReportedError();
            }
            // If there are orphaned projects, we need to update
            const orphanedProjects = shrinkwrapFile.findOrphanedProjects(this.rushConfiguration, subspace);
            if (orphanedProjects.length > 0) {
                for (const orphanedProject of orphanedProjects) {
                    shrinkwrapWarnings.push(`Your ${this.rushConfiguration.shrinkwrapFilePhrase} references "${orphanedProject}" ` +
                        `which was not found in ${RushConstants_1.RushConstants.rushJsonFilename}`);
                }
                shrinkwrapIsUpToDate = false;
            }
        }
        // If preferred versions have been updated, or if the repo-state.json is invalid,
        // we can't be certain of the state of the shrinkwrap
        const repoState = subspace.getRepoState();
        if (!repoState.isValid) {
            shrinkwrapWarnings.push(`The ${RushConstants_1.RushConstants.repoStateFilename} file is invalid. There may be a merge conflict marker in the file.`);
            shrinkwrapIsUpToDate = false;
        }
        else {
            const commonVersions = subspace.getCommonVersions(variant);
            if (repoState.preferredVersionsHash !== commonVersions.getPreferredVersionsHash()) {
                shrinkwrapWarnings.push(`Preferred versions from ${RushConstants_1.RushConstants.commonVersionsFilename} have been modified.`);
                shrinkwrapIsUpToDate = false;
            }
            const stopwatch = Stopwatch_1.Stopwatch.start();
            const packageJsonInjectedDependenciesHash = subspace.getPackageJsonInjectedDependenciesHash(variant);
            stopwatch.stop();
            this._terminal.writeDebugLine(`Total amount of time spent to hash related package.json files in the injected installation case: ${stopwatch.toString()}`);
            if (packageJsonInjectedDependenciesHash) {
                // if packageJsonInjectedDependenciesHash exists
                // make sure it matches the value in repoState
                if (packageJsonInjectedDependenciesHash !== repoState.packageJsonInjectedDependenciesHash) {
                    shrinkwrapWarnings.push(`Some injected dependencies' package.json might have been modified.`);
                    shrinkwrapIsUpToDate = false;
                }
            }
            else {
                // if packageJsonInjectedDependenciesHash not exists
                // there is a situation that the subspace previously has injected dependencies but removed
                // so we can check if the repoState up to date
                if (repoState.packageJsonInjectedDependenciesHash !== undefined) {
                    shrinkwrapWarnings.push(`It was detected that ${repoState.filePath} contains packageJsonInjectedDependenciesHash` +
                        ' but the injected dependencies feature is not enabled. You can manually remove this field in repo-state.json.' +
                        ' Or run rush update command to update the repo-state.json file.');
                }
            }
        }
        // To generate the workspace file, we will add each project to the file as we loop through and validate
        const workspaceFile = new PnpmWorkspaceFile_1.PnpmWorkspaceFile(path.join(subspace.getSubspaceTempFolderPath(), 'pnpm-workspace.yaml'));
        // For pnpm package manager, we need to handle dependenciesMeta changes in package.json. See more: https://pnpm.io/package_json#dependenciesmeta
        // If dependenciesMeta settings is different between package.json and pnpm-lock.yaml, then shrinkwrapIsUpToDate return false.
        // Build a object for dependenciesMeta settings in projects' package.json
        // key is the package path, value is the dependenciesMeta info for that package
        const expectedDependenciesMetaByProjectRelativePath = {};
        const commonTempFolder = subspace.getSubspaceTempFolderPath();
        const rushJsonFolder = this.rushConfiguration.rushJsonFolder;
        // get the relative path from common temp folder to repo root folder
        const relativeFromTempFolderToRootFolder = path.relative(commonTempFolder, rushJsonFolder);
        // Loop through the projects and add them to the workspace file. While we're at it, also validate that
        // referenced workspace projects are valid, and check if the shrinkwrap file is already up-to-date.
        for (const rushProject of this.rushConfiguration.projects) {
            if (!subspace.contains(rushProject)) {
                // skip processing any project that isn't in this subspace
                continue;
            }
            const packageJson = rushProject.packageJsonEditor;
            workspaceFile.addPackage(rushProject.projectFolder);
            for (const { name, version, dependencyType } of [
                ...packageJson.dependencyList,
                ...packageJson.devDependencyList
            ]) {
                // Allow the package manager to handle peer dependency resolution, since this is simply a constraint
                // enforced by the package manager. Additionally, peer dependencies are simply a version constraint
                // and do not need to be converted to workspaces protocol.
                if (dependencyType === PackageJsonEditor_1.DependencyType.Peer) {
                    continue;
                }
                const dependencySpecifier = DependencySpecifier_1.DependencySpecifier.parseWithCache(name, version);
                // Is there a locally built Rush project that could satisfy this dependency?
                let referencedLocalProject = this.rushConfiguration.getProjectByName(name);
                // If we enable exemptDecoupledDependenciesBetweenSubspaces, it will only check dependencies within the subspace.
                if (this.rushConfiguration.experimentsConfiguration.configuration
                    .exemptDecoupledDependenciesBetweenSubspaces) {
                    if (referencedLocalProject && !subspace.contains(referencedLocalProject)) {
                        referencedLocalProject = undefined;
                    }
                }
                // Validate that local projects are referenced with workspace notation. If not, and it is not a
                // cyclic dependency, then it needs to be updated to specify `workspace:*` explicitly. Currently only
                // supporting versions and version ranges for specifying a local project.
                if ((dependencySpecifier.specifierType === DependencySpecifier_1.DependencySpecifierType.Version ||
                    dependencySpecifier.specifierType === DependencySpecifier_1.DependencySpecifierType.Range) &&
                    referencedLocalProject &&
                    !rushProject.decoupledLocalDependencies.has(name)) {
                    // Make sure that this version is intended to target a local package. If not, then we will fail since it
                    // is not explicitly specified as a cyclic dependency.
                    if (!semver.satisfies(referencedLocalProject.packageJsonEditor.version, dependencySpecifier.versionSpecifier)) {
                        // eslint-disable-next-line no-console
                        console.log();
                        // eslint-disable-next-line no-console
                        console.log(terminal_1.Colorize.red(`"${rushProject.packageName}" depends on package "${name}" (${version}) which belongs to ` +
                            'the workspace but cannot be fulfilled with the specified version range. Either ' +
                            'specify a valid version range, or add the package to "decoupledLocalDependencies" in rush.json.'));
                        throw new node_core_library_1.AlreadyReportedError();
                    }
                    if (!allowShrinkwrapUpdates) {
                        // eslint-disable-next-line no-console
                        console.log();
                        // eslint-disable-next-line no-console
                        console.log(terminal_1.Colorize.red(`"${rushProject.packageName}" depends on package "${name}" (${version}) which exists within ` +
                            'the workspace. Run "rush update" to update workspace references for this package. ' +
                            `If package "${name}" is intentionally expected to be installed from an external package feed, ` +
                            `list package "${name}" in the "decoupledLocalDependencies" field in the ` +
                            `"${rushProject.packageName}" entry in rush.json to suppress this error.`));
                        throw new node_core_library_1.AlreadyReportedError();
                    }
                    if (fullUpgrade) {
                        // We will update to `workspace` notation. If the version specified is a range, then use the provided range.
                        // Otherwise, use `workspace:*` to ensure we're always using the workspace package.
                        const workspaceRange = !!semver.validRange(dependencySpecifier.versionSpecifier) &&
                            !semver.valid(dependencySpecifier.versionSpecifier)
                            ? dependencySpecifier.versionSpecifier
                            : '*';
                        packageJson.addOrUpdateDependency(name, `workspace:${workspaceRange}`, dependencyType);
                        shrinkwrapIsUpToDate = false;
                        continue;
                    }
                }
                else if (dependencySpecifier.specifierType === DependencySpecifier_1.DependencySpecifierType.Workspace &&
                    rushProject.decoupledLocalDependencies.has(name)) {
                    // If the dependency is a local project that is decoupled, then we need to ensure that it is not specified
                    // as a workspace project. If it is, then we need to update the package.json to remove the workspace notation.
                    this._terminal.writeWarningLine(`"${rushProject.packageName}" depends on package ${name}@${version}, but also lists it in ` +
                        `its "decoupledLocalDependencies" array. Either update the host project's package.json to use ` +
                        `a version from an external feed instead of "workspace:" notation, or remove the dependency from the ` +
                        `host project's "decoupledLocalDependencies" array in rush.json.`);
                    throw new node_core_library_1.AlreadyReportedError();
                }
                else if (!rushProject.decoupledLocalDependencies.has(name)) {
                    // Already specified as a local project. Allow the package manager to validate this
                    continue;
                }
            }
            // Save the package.json if we modified the version references and warn that the package.json was modified
            if (packageJson.saveIfModified()) {
                // eslint-disable-next-line no-console
                console.log(terminal_1.Colorize.yellow(`"${rushProject.packageName}" depends on one or more workspace packages which did not use "workspace:" ` +
                    'notation. The package.json has been modified and must be committed to source control.'));
            }
            // Now validate that the shrinkwrap file matches what is in the package.json
            if (await (shrinkwrapFile === null || shrinkwrapFile === void 0 ? void 0 : shrinkwrapFile.isWorkspaceProjectModifiedAsync(rushProject, subspace, variant))) {
                shrinkwrapWarnings.push(`Dependencies of project "${rushProject.packageName}" do not match the current shrinkwrap.`);
                shrinkwrapIsUpToDate = false;
            }
            const dependencyMetaList = packageJson.dependencyMetaList;
            if (dependencyMetaList.length !== 0) {
                const dependenciesMeta = {};
                for (const dependencyMeta of dependencyMetaList) {
                    dependenciesMeta[dependencyMeta.name] = {
                        injected: dependencyMeta.injected
                    };
                }
                // get the relative path from common temp folder to package folder, to align with the value in pnpm-lock.yaml
                const relativePathFromTempFolderToPackageFolder = node_core_library_1.Path.convertToSlashes(`${relativeFromTempFolderToRootFolder}/${rushProject.projectRelativeFolder}`);
                expectedDependenciesMetaByProjectRelativePath[relativePathFromTempFolderToPackageFolder] =
                    dependenciesMeta;
            }
        }
        // Build a object for dependenciesMeta settings in pnpm-lock.yaml
        // key is the package path, value is the dependenciesMeta info for that package
        const lockfileDependenciesMetaByProjectRelativePath = {};
        if ((shrinkwrapFile === null || shrinkwrapFile === void 0 ? void 0 : shrinkwrapFile.importers) !== undefined) {
            for (const [key, value] of shrinkwrapFile === null || shrinkwrapFile === void 0 ? void 0 : shrinkwrapFile.importers) {
                const projectRelativePath = node_core_library_1.Path.convertToSlashes(key);
                // we only need to verify packages that exist in package.json and pnpm-lock.yaml
                // PNPM won't actively remove deleted packages in importers, unless it has to
                // so it is possible that a deleted package still showing in pnpm-lock.yaml
                if (expectedDependenciesMetaByProjectRelativePath[projectRelativePath] === undefined) {
                    continue;
                }
                if (value.dependenciesMeta !== undefined) {
                    lockfileDependenciesMetaByProjectRelativePath[projectRelativePath] = value.dependenciesMeta;
                }
            }
        }
        // Now, we compare these two objects to see if they are equal or not
        const dependenciesMetaAreEqual = node_core_library_1.Objects.areDeepEqual(expectedDependenciesMetaByProjectRelativePath, lockfileDependenciesMetaByProjectRelativePath);
        if (!dependenciesMetaAreEqual) {
            shrinkwrapWarnings.push("The dependenciesMeta settings in one or more package.json don't match the current shrinkwrap.");
            shrinkwrapIsUpToDate = false;
        }
        // Check if overrides and globalOverrides are the same
        const pnpmOptions = subspace.getPnpmOptions() || this.rushConfiguration.pnpmOptions;
        const overridesAreEqual = node_core_library_1.Objects.areDeepEqual((_a = pnpmOptions.globalOverrides) !== null && _a !== void 0 ? _a : {}, (shrinkwrapFile === null || shrinkwrapFile === void 0 ? void 0 : shrinkwrapFile.overrides) ? Object.fromEntries(shrinkwrapFile === null || shrinkwrapFile === void 0 ? void 0 : shrinkwrapFile.overrides) : {});
        if (!overridesAreEqual) {
            shrinkwrapWarnings.push("The overrides settings doesn't match the current shrinkwrap.");
            shrinkwrapIsUpToDate = false;
        }
        // Check if packageExtensionsChecksum matches globalPackageExtension's hash
        let packageExtensionsChecksum;
        let existingPackageExtensionsChecksum;
        if (shrinkwrapFile) {
            existingPackageExtensionsChecksum = shrinkwrapFile.packageExtensionsChecksum;
            let packageExtensionsChecksumAlgorithm;
            if (existingPackageExtensionsChecksum) {
                const dashIndex = existingPackageExtensionsChecksum.indexOf('-');
                if (dashIndex !== -1) {
                    packageExtensionsChecksumAlgorithm = existingPackageExtensionsChecksum.substring(0, dashIndex);
                }
                if (packageExtensionsChecksumAlgorithm && packageExtensionsChecksumAlgorithm !== 'sha256') {
                    this._terminal.writeErrorLine(`The existing packageExtensionsChecksum algorithm "${packageExtensionsChecksumAlgorithm}" is not supported. ` +
                        `This may indicate that the shrinkwrap was created with a newer version of PNPM than Rush supports.`);
                    throw new node_core_library_1.AlreadyReportedError();
                }
            }
            const globalPackageExtensions = pnpmOptions.globalPackageExtensions;
            // https://github.com/pnpm/pnpm/blob/ba9409ffcef0c36dc1b167d770a023c87444822d/pkg-manager/core/src/install/index.ts#L331
            if (globalPackageExtensions && Object.keys(globalPackageExtensions).length !== 0) {
                if (packageExtensionsChecksumAlgorithm) {
                    // In PNPM v10, the algorithm changed to SHA256 and the digest changed from hex to base64
                    packageExtensionsChecksum = await createObjectChecksumAsync(globalPackageExtensions);
                }
                else {
                    packageExtensionsChecksum = createObjectChecksumLegacy(globalPackageExtensions);
                }
            }
        }
        const packageExtensionsChecksumAreEqual = packageExtensionsChecksum === existingPackageExtensionsChecksum;
        if (!packageExtensionsChecksumAreEqual) {
            shrinkwrapWarnings.push("The package extension hash doesn't match the current shrinkwrap.");
            shrinkwrapIsUpToDate = false;
        }
        // Write the common package.json
        InstallHelpers_1.InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined, this._terminal);
        // Set catalog definitions in the workspace file if specified
        if (pnpmOptions.globalCatalogs) {
            if (this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
                semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '9.5.0')) {
                this._terminal.writeWarningLine(terminal_1.Colorize.yellow(`Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
                    `doesn't support the "globalCatalogs" fields in ` +
                    `${this.rushConfiguration.commonRushConfigFolder}/${RushConstants_1.RushConstants.pnpmConfigFilename}. ` +
                    'Remove these fields or upgrade to pnpm 9.5.0 or newer.'));
            }
            const catalogs = {};
            if (pnpmOptions.globalCatalogs) {
                Object.assign(catalogs, pnpmOptions.globalCatalogs);
            }
            workspaceFile.setCatalogs(catalogs);
        }
        // Save the generated workspace file. Don't update the file timestamp unless the content has changed,
        // since "rush install" will consider this timestamp
        workspaceFile.save(workspaceFile.workspaceFilename, { onlyIfChanged: true });
        return { shrinkwrapIsUpToDate, shrinkwrapWarnings };
    }
    async canSkipInstallAsync(lastModifiedDate, subspace, variant) {
        if (!(await super.canSkipInstallAsync(lastModifiedDate, subspace, variant))) {
            return false;
        }
        const potentiallyChangedFiles = [];
        if (this.rushConfiguration.isPnpm) {
            // Add workspace file. This file is only modified when workspace packages change.
            const pnpmWorkspaceFilename = path.join(subspace.getSubspaceTempFolderPath(), 'pnpm-workspace.yaml');
            if (node_core_library_1.FileSystem.exists(pnpmWorkspaceFilename)) {
                potentiallyChangedFiles.push(pnpmWorkspaceFilename);
            }
        }
        // Also consider timestamps for all the project node_modules folders, as well as the package.json
        // files
        // Example: [ "C:\MyRepo\projects\projectA\node_modules", "C:\MyRepo\projects\projectA\package.json" ]
        potentiallyChangedFiles.push(...subspace.getProjects().map((project) => {
            return path.join(project.projectFolder, RushConstants_1.RushConstants.nodeModulesFolderName);
        }), ...subspace.getProjects().map((project) => {
            return path.join(project.projectFolder, node_core_library_1.FileConstants.PackageJson);
        }));
        // NOTE: If any of the potentiallyChangedFiles does not exist, then isFileTimestampCurrent()
        // returns false.
        return Utilities_1.Utilities.isFileTimestampCurrentAsync(lastModifiedDate, potentiallyChangedFiles);
    }
    /**
     * Runs "pnpm install" in the common folder.
     */
    async installAsync(cleanInstall, subspace) {
        // Example: "C:\MyRepo\common\temp\npm-local\node_modules\.bin\npm"
        const packageManagerFilename = this.rushConfiguration.packageManagerToolFilename;
        const packageManagerEnv = InstallHelpers_1.InstallHelpers.getPackageManagerEnvironment(this.rushConfiguration, this.options);
        if (terminal_1.ConsoleTerminalProvider.supportsColor) {
            packageManagerEnv.FORCE_COLOR = '1';
        }
        const commonNodeModulesFolder = path.join(subspace.getSubspaceTempFolderPath(), RushConstants_1.RushConstants.nodeModulesFolderName);
        // Is there an existing "node_modules" folder to consider?
        if (node_core_library_1.FileSystem.exists(commonNodeModulesFolder)) {
            // Should we delete the entire "node_modules" folder?
            if (cleanInstall) {
                // YES: Delete "node_modules"
                // Explain to the user why we are hosing their node_modules folder
                // eslint-disable-next-line no-console
                console.log('Deleting files from ' + commonNodeModulesFolder);
                this.installRecycler.moveFolder(commonNodeModulesFolder);
                Utilities_1.Utilities.createFolderWithRetry(commonNodeModulesFolder);
            }
        }
        const doInstallInternalAsync = async (options) => {
            // Run "npm install" in the common folder
            // To ensure that the output is always colored, set the option "--color=always", even when it's piped.
            // Without this argument, certain text that should be colored (such as red) will appear white.
            const installArgs = ['install'];
            this.pushConfigurationArgs(installArgs, options, subspace);
            // eslint-disable-next-line no-console
            console.log('\n' +
                terminal_1.Colorize.bold(`Running "${this.rushConfiguration.packageManager} install" in` +
                    ` ${subspace.getSubspaceTempFolderPath()}`) +
                '\n');
            // If any diagnostic options were specified, then show the full command-line
            if (this.options.debug ||
                this.options.collectLogFile ||
                this.options.networkConcurrency ||
                this.options.onlyShrinkwrap) {
                // eslint-disable-next-line no-console
                console.log('\n' +
                    terminal_1.Colorize.green('Invoking package manager: ') +
                    node_core_library_1.FileSystem.getRealPath(packageManagerFilename) +
                    ' ' +
                    installArgs.join(' ') +
                    '\n');
            }
            // Store the tip IDs that should be printed.
            // They will be printed all at once *after* the install
            const tipIDsToBePrinted = new Set();
            const pnpmTips = [];
            for (const [customTipId, customTip] of Object.entries(CustomTipsConfiguration_1.PNPM_CUSTOM_TIPS)) {
                if (this.rushConfiguration.customTipsConfiguration.providedCustomTipsByTipId.has(customTipId)) {
                    pnpmTips.push(customTip);
                }
            }
            const onPnpmStdoutChunk = (chunk) => {
                // Iterate over the supported custom tip metadata and try to match the chunk.
                if (pnpmTips.length > 0) {
                    for (const { isMatch, tipId } of pnpmTips) {
                        if (isMatch === null || isMatch === void 0 ? void 0 : isMatch(chunk)) {
                            tipIDsToBePrinted.add(tipId);
                        }
                    }
                }
                // Replace `pnpm approve-builds` with `rush-pnpm approve-builds` when running
                // `rush install` or `rush update` to instruct users to use the correct command
                const modifiedChunk = chunk.replace(/pnpm approve-builds/g, `rush-pnpm --subspace ${subspace.subspaceName} approve-builds`);
                // Return modified chunk if it was changed, otherwise return void to keep original
                return modifiedChunk !== chunk ? modifiedChunk : undefined;
            };
            try {
                await Utilities_1.Utilities.executeCommandWithRetryAsync({
                    command: packageManagerFilename,
                    args: installArgs,
                    workingDirectory: subspace.getSubspaceTempFolderPath(),
                    environment: packageManagerEnv,
                    suppressOutput: false,
                    onStdoutStreamChunk: onPnpmStdoutChunk
                }, this.options.maxInstallAttempts, () => {
                    if (this.rushConfiguration.isPnpm) {
                        this._terminal.writeWarningLine(`Deleting the "node_modules" folder`);
                        this.installRecycler.moveFolder(commonNodeModulesFolder);
                        // Leave the pnpm-store as is for the retry. This ensures that packages that have already
                        // been downloaded need not be downloaded again, thereby potentially increasing the chances
                        // of a subsequent successful install.
                        Utilities_1.Utilities.createFolderWithRetry(commonNodeModulesFolder);
                    }
                });
            }
            finally {
                // The try-finally is to avoid the tips NOT being printed if the install fails.
                // NOT catching the error because we want to keep the other behaviors (i.e., the error will be caught and handle in upper layers).
                if (tipIDsToBePrinted.size > 0) {
                    this._terminal.writeLine();
                    for (const tipID of tipIDsToBePrinted) {
                        this.rushConfiguration.customTipsConfiguration._showTip(this._terminal, tipID);
                    }
                }
            }
        };
        const { configuration: experiments } = this.rushConfiguration.experimentsConfiguration;
        if (this.options.allowShrinkwrapUpdates &&
            experiments.usePnpmLockfileOnlyThenFrozenLockfileForRushUpdate) {
            await doInstallInternalAsync({
                ...this.options,
                onlyShrinkwrap: true
            });
            await doInstallInternalAsync({
                ...this.options,
                allowShrinkwrapUpdates: false
            });
        }
        else {
            await doInstallInternalAsync(this.options);
        }
        // If all attempts fail we just terminate. No special handling needed.
        // Ensure that node_modules folders exist after install, since the timestamps on these folders are used
        // to determine if the install can be skipped
        const projectNodeModulesFolders = [
            path.join(subspace.getSubspaceTempFolderPath(), RushConstants_1.RushConstants.nodeModulesFolderName),
            ...this.rushConfiguration.projects.map((project) => {
                return path.join(project.projectFolder, RushConstants_1.RushConstants.nodeModulesFolderName);
            })
        ];
        for (const nodeModulesFolder of projectNodeModulesFolders) {
            node_core_library_1.FileSystem.ensureFolder(nodeModulesFolder);
        }
        // eslint-disable-next-line no-console
        console.log('');
    }
    async postInstallAsync(subspace) {
        var _a, _b;
        // Grab the temp shrinkwrap, as this was the most recently completed install. It may also be
        // more up-to-date than the checked-in shrinkwrap since filtered installs are not written back.
        // Note that if there are no projects, or if we're in PNPM workspace mode and there are no
        // projects with dependencies, a lockfile won't be generated.
        const tempShrinkwrapFile = ShrinkwrapFileFactory_1.ShrinkwrapFileFactory.getShrinkwrapFile({
            packageManager: this.rushConfiguration.packageManager,
            shrinkwrapFilePath: subspace.getTempShrinkwrapFilename(),
            subspaceHasNoProjects: subspace.getProjects().length === 0
        });
        if (tempShrinkwrapFile) {
            // Write or delete all project shrinkwraps related to the install
            await node_core_library_1.Async.forEachAsync(subspace.getProjects(), async (project) => {
                var _a;
                await ((_a = tempShrinkwrapFile.getProjectShrinkwrap(project)) === null || _a === void 0 ? void 0 : _a.updateProjectShrinkwrapAsync());
            }, { concurrency: 10 });
        }
        else if (this.rushConfiguration.isPnpm && ((_a = this.rushConfiguration.pnpmOptions) === null || _a === void 0 ? void 0 : _a.useWorkspaces)) {
            // If we're in PNPM workspace mode and PNPM didn't create a shrinkwrap file,
            // there are no dependencies. Generate empty shrinkwrap files for all projects.
            await node_core_library_1.Async.forEachAsync(subspace.getProjects(), async (project) => {
                await BaseProjectShrinkwrapFile_1.BaseProjectShrinkwrapFile.saveEmptyProjectShrinkwrapFileAsync(project);
            }, { concurrency: 10 });
        }
        else {
            // This is an unexpected case
            throw new Error('A shrinkwrap file does not exist after after successful installation. This probably indicates a ' +
                'bug in the package manager.');
        }
        // If the splitWorkspaceCompatibility is enabled for subspaces, create symlinks to mimic the behaviour
        // of having the node_modules folder created directly in the project folder. This requires symlinking two categories:
        // 1) Symlink any packages that are declared to be publicly hoisted, such as by using public-hoist-pattern in .npmrc.
        //    This creates a symlink from <project_folder>/node_modules/<dependency> -> temp/<subspace_name>/node_modules/<dependency>
        // 2) Symlink any workspace packages that are declared in the temp folder, as some packages may expect these packages to exist
        //    in the node_modules folder.
        //    This creates a symlink from temp/<subspace_name>/node_modules/<workspace_dependency_name> -> <workspace_dependency_folder>
        if (this.rushConfiguration.subspacesFeatureEnabled &&
            ((_b = this.rushConfiguration.subspacesConfiguration) === null || _b === void 0 ? void 0 : _b.splitWorkspaceCompatibility)) {
            const tempNodeModulesPath = `${subspace.getSubspaceTempFolderPath()}/node_modules`;
            const modulesFilePath = `${tempNodeModulesPath}/${RushConstants_1.RushConstants.pnpmModulesFilename}`;
            if (subspace.subspaceName.startsWith('split_') &&
                subspace.getProjects().length === 1 &&
                (await node_core_library_1.FileSystem.existsAsync(modulesFilePath))) {
                // Find the .modules.yaml file in the subspace temp/node_modules folder
                const modulesContent = await node_core_library_1.FileSystem.readFileAsync(modulesFilePath);
                const yamlContent = js_yaml_1.default.load(modulesContent, {
                    filename: modulesFilePath
                });
                const { hoistedDependencies } = yamlContent;
                const subspaceProject = subspace.getProjects()[0];
                const projectNodeModulesPath = `${subspaceProject.projectFolder}/node_modules`;
                for (const value of Object.values(hoistedDependencies)) {
                    for (const [filePath, type] of Object.entries(value)) {
                        if (type === 'public') {
                            if (Utilities_1.Utilities.existsOrIsSymlink(`${projectNodeModulesPath}/${filePath}`)) {
                                await node_core_library_1.FileSystem.deleteFolderAsync(`${projectNodeModulesPath}/${filePath}`);
                            }
                            // If we don't already have a symlink for this package, create one
                            const parentDir = Utilities_1.Utilities.trimAfterLastSlash(`${projectNodeModulesPath}/${filePath}`);
                            await node_core_library_1.FileSystem.ensureFolderAsync(parentDir);
                            await BaseLinkManager_1.BaseLinkManager._createSymlinkAsync({
                                linkTargetPath: `${tempNodeModulesPath}/${filePath}`,
                                newLinkPath: `${projectNodeModulesPath}/${filePath}`,
                                symlinkKind: BaseLinkManager_1.SymlinkKind.Directory
                            });
                        }
                    }
                }
            }
            // Look for any workspace linked packages anywhere in this subspace, symlink them from the temp node_modules folder.
            const subspaceDependencyProjects = new Set();
            for (const subspaceProject of subspace.getProjects()) {
                for (const dependencyProject of subspaceProject.dependencyProjects) {
                    subspaceDependencyProjects.add(dependencyProject);
                }
            }
            for (const dependencyProject of subspaceDependencyProjects) {
                const symlinkToCreate = `${tempNodeModulesPath}/${dependencyProject.packageName}`;
                if (!Utilities_1.Utilities.existsOrIsSymlink(symlinkToCreate)) {
                    const parentFolder = Utilities_1.Utilities.trimAfterLastSlash(symlinkToCreate);
                    await node_core_library_1.FileSystem.ensureFolderAsync(parentFolder);
                    await BaseLinkManager_1.BaseLinkManager._createSymlinkAsync({
                        linkTargetPath: dependencyProject.projectFolder,
                        newLinkPath: symlinkToCreate,
                        symlinkKind: BaseLinkManager_1.SymlinkKind.Directory
                    });
                }
            }
        }
        // TODO: Remove when "rush link" and "rush unlink" are deprecated
        await new FlagFile_1.FlagFile(subspace.getSubspaceTempFolderPath(), RushConstants_1.RushConstants.lastLinkFlagFilename, {}).createAsync();
    }
    /**
     * Used when invoking the NPM tool.  Appends the common configuration options
     * to the command-line.
     */
    pushConfigurationArgs(args, options, subspace) {
        super.pushConfigurationArgs(args, options, subspace);
        // Add workspace-specific args
        if (this.rushConfiguration.isPnpm) {
            args.push('--recursive');
            args.push('--link-workspace-packages', 'false');
            if (process.stdout.isTTY) {
                // If we're on a TTY console and something else didn't set a `--reporter` parameter,
                // explicitly set the default reporter. This fixes an issue where, when the pnpm
                // output is being monitored to match custom tips, pnpm will detect a non-TTY
                // stdout stream and use the `append-only` reporter.
                //
                // See docs here: https://pnpm.io/cli/install#--reportername
                let includesReporterArg = false;
                for (const arg of args) {
                    if (arg.startsWith('--reporter')) {
                        includesReporterArg = true;
                        break;
                    }
                }
                if (!includesReporterArg) {
                    args.push('--reporter', 'default');
                }
            }
            for (const arg of this.options.pnpmFilterArgumentValues) {
                args.push('--filter', arg);
            }
        }
    }
}
exports.WorkspaceInstallManager = WorkspaceInstallManager;
/**
 * Source: https://github.com/pnpm/pnpm/blob/ba9409ffcef0c36dc1b167d770a023c87444822d/pkg-manager/core/src/install/index.ts#L821-L824
 */
function createObjectChecksumLegacy(obj) {
    const s = JSON.stringify(node_core_library_1.Sort.sortKeys(obj));
    return (0, node_crypto_1.createHash)('md5').update(s).digest('hex');
}
/**
 * Source: https://github.com/pnpm/pnpm/blob/bdbd31aa4fa6546d65b6eee50a79b51879340d40/crypto/object-hasher/src/index.ts#L8-L12
 */
const defaultOptions = {
    respectType: false,
    algorithm: 'sha256',
    encoding: 'base64'
};
/**
 * https://github.com/pnpm/pnpm/blob/bdbd31aa4fa6546d65b6eee50a79b51879340d40/crypto/object-hasher/src/index.ts#L21-L26
 */
const withSortingOptions = {
    ...defaultOptions,
    unorderedArrays: true,
    unorderedObjects: true,
    unorderedSets: true
};
/**
 * Source: https://github.com/pnpm/pnpm/blob/bdbd31aa4fa6546d65b6eee50a79b51879340d40/crypto/object-hasher/src/index.ts#L45-L49
 */
async function createObjectChecksumAsync(obj) {
    const { default: hash } = await Promise.resolve().then(() => __importStar(require('object-hash')));
    const packageExtensionsChecksum = hash(obj, withSortingOptions);
    return `${defaultOptions.algorithm}-${packageExtensionsChecksum}`;
}
//# sourceMappingURL=WorkspaceInstallManager.js.map