"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.HotlinkManager = void 0;
const pnpm_sync_lib_1 = require("pnpm-sync-lib");
const semver = __importStar(require("semver"));
const terminal_1 = require("@rushstack/terminal");
const node_core_library_1 = require("@rushstack/node-core-library");
const package_extractor_1 = require("@rushstack/package-extractor");
const RushConstants_1 = require("../logic/RushConstants");
const PnpmSyncUtilities_1 = require("./PnpmSyncUtilities");
const BaseLinkManager_1 = require("../logic/base/BaseLinkManager");
const rush_hotlink_state_schema_json_1 = __importDefault(require("../schemas/rush-hotlink-state.schema.json"));
const actionNameConstants_1 = require("./actionNameConstants");
const PROJECT_LINKS_STATE_JSON_SCHEMA = node_core_library_1.JsonSchema.fromLoadedObject(rush_hotlink_state_schema_json_1.default);
class HotlinkManager {
    constructor(options) {
        const { rushLinkStateFilePath, linksBySubspaceName } = options;
        this._rushLinkStateFilePath = rushLinkStateFilePath;
        this._linksBySubspaceName = linksBySubspaceName;
    }
    hasAnyHotlinksInSubspace(subspaceName) {
        var _a;
        return !!((_a = this._linksBySubspaceName.get(subspaceName)) === null || _a === void 0 ? void 0 : _a.length);
    }
    async _hardLinkToLinkedPackageAsync(terminal, sourcePath, targetFolder, lockfileId) {
        const logMessageCallback = (logMessageOptions) => {
            PnpmSyncUtilities_1.PnpmSyncUtilities.processLogMessage(logMessageOptions, terminal);
        };
        await (0, pnpm_sync_lib_1.pnpmSyncUpdateFileAsync)({
            sourceProjectFolder: sourcePath,
            // TODO: Update pnpmSyncUpdateFileAsync to take an Iterable<string>
            targetFolders: Array.from(targetFolder),
            lockfileId,
            logMessageCallback
        });
        const pnpmSyncJsonPath = `${sourcePath}/${RushConstants_1.RushConstants.nodeModulesFolderName}/${RushConstants_1.RushConstants.pnpmSyncFilename}`;
        await (0, pnpm_sync_lib_1.pnpmSyncCopyAsync)({
            pnpmSyncJsonPath,
            ensureFolderAsync: node_core_library_1.FileSystem.ensureFolderAsync,
            forEachAsyncWithConcurrency: node_core_library_1.Async.forEachAsync,
            getPackageIncludedFiles: package_extractor_1.PackageExtractor.getPackageIncludedFilesAsync,
            logMessageCallback
        });
    }
    async _modifyAndSaveLinkStateAsync(cb) {
        const newLinksBySubspaceName = await cb(this._linksBySubspaceName);
        this._linksBySubspaceName = newLinksBySubspaceName;
        const linkStateJson = {
            fileVersion: 0,
            linksBySubspace: Object.fromEntries(newLinksBySubspaceName)
        };
        await node_core_library_1.JsonFile.saveAsync(linkStateJson, this._rushLinkStateFilePath);
    }
    async purgeLinksAsync(terminal, subspaceName) {
        if (!this.hasAnyHotlinksInSubspace(subspaceName)) {
            return false;
        }
        const logMessageCallback = (logMessageOptions) => {
            PnpmSyncUtilities_1.PnpmSyncUtilities.processLogMessage(logMessageOptions, terminal);
        };
        await this._modifyAndSaveLinkStateAsync(async (linksBySubspaceName) => {
            var _a;
            const rushLinkFileState = (_a = linksBySubspaceName.get(subspaceName)) !== null && _a !== void 0 ? _a : [];
            await node_core_library_1.Async.forEachAsync(rushLinkFileState, async ({ linkedPackagePath, affectedPnpmVirtualStoreFolderPaths = [] }) => {
                await (0, pnpm_sync_lib_1.pnpmSyncUpdateFileAsync)({
                    sourceProjectFolder: linkedPackagePath,
                    targetFolders: [],
                    lockfileId: subspaceName,
                    logMessageCallback
                });
                // pnpm will reuse packages in .pnpm directory, so we need to manually delete them before installation
                await node_core_library_1.Async.forEachAsync(affectedPnpmVirtualStoreFolderPaths, async (affectedPnpmVirtualStoreFolderName) => {
                    await node_core_library_1.FileSystem.deleteFolderAsync(affectedPnpmVirtualStoreFolderName);
                }, { concurrency: 10 });
            }, { concurrency: 10 });
            const newLinksBySubspaceName = new Map(linksBySubspaceName);
            newLinksBySubspaceName.delete(subspaceName);
            return newLinksBySubspaceName;
        });
        return true;
    }
    async _getLinkedPackageInfoAsync(linkedPackagePath) {
        const linkedPackageJsonPath = `${linkedPackagePath}/${node_core_library_1.FileConstants.PackageJson}`;
        const linkedPackageJsonExists = await node_core_library_1.FileSystem.existsAsync(linkedPackageJsonPath);
        if (!linkedPackageJsonExists) {
            throw new Error(`Cannot find ${node_core_library_1.FileConstants.PackageJson} in the path ${linkedPackagePath}`);
        }
        const { dependencies = {}, name: packageName, peerDependencies = {} } = await node_core_library_1.JsonFile.loadAsync(linkedPackageJsonPath);
        const linkedPackageNodeModulesPath = `${linkedPackagePath}/${RushConstants_1.RushConstants.nodeModulesFolderName}`;
        const externalDependencies = [];
        const workspaceDependencies = [];
        for (const [name, protocol] of Object.entries(dependencies)) {
            if (protocol.startsWith('workspace')) {
                workspaceDependencies.push(name);
            }
            else {
                externalDependencies.push(name);
            }
        }
        return {
            packageName,
            linkedPackageNodeModulesPath,
            externalDependencies,
            workspaceDependencies,
            peerDependencies
        };
    }
    async _getPackagePathsMatchingNameAndVersionAsync(consumerPackagePnpmDependenciesFolderPath, packageName, versionRange) {
        const subDirectories = await node_core_library_1.FileSystem.readFolderItemNamesAsync(consumerPackagePnpmDependenciesFolderPath);
        const packageSourcePathSet = new Set();
        for (const dirName of subDirectories) {
            const packageSourcePath = `${consumerPackagePnpmDependenciesFolderPath}/${dirName}/${RushConstants_1.RushConstants.nodeModulesFolderName}/${packageName}`;
            if (await node_core_library_1.FileSystem.existsAsync(packageSourcePath)) {
                const { version } = await node_core_library_1.JsonFile.loadAsync(`${packageSourcePath}/${node_core_library_1.FileConstants.PackageJson}`);
                if (semver.satisfies(version, versionRange, { includePrerelease: true })) {
                    packageSourcePathSet.add(packageSourcePath);
                }
            }
        }
        return packageSourcePathSet;
    }
    async bridgePackageAsync(terminal, subspace, linkedPackagePath, version) {
        const subspaceName = subspace.subspaceName;
        try {
            const { packageName } = await this._getLinkedPackageInfoAsync(linkedPackagePath);
            const consumerPackagePnpmDependenciesFolderPath = `${subspace.getSubspaceTempFolderPath()}/${RushConstants_1.RushConstants.nodeModulesFolderName}/${RushConstants_1.RushConstants.pnpmVirtualStoreFolderName}`;
            const sourcePathSet = await this._getPackagePathsMatchingNameAndVersionAsync(consumerPackagePnpmDependenciesFolderPath, packageName, version);
            if (sourcePathSet.size === 0) {
                throw new Error(`Cannot find package ${packageName} ${version} in ${consumerPackagePnpmDependenciesFolderPath}`);
            }
            await this._hardLinkToLinkedPackageAsync(terminal, linkedPackagePath, sourcePathSet, subspaceName);
            await this._modifyAndSaveLinkStateAsync((linksBySubspaceName) => {
                var _a;
                const newConsumerPackageLinks = [
                    ...((_a = linksBySubspaceName.get(subspaceName)) !== null && _a !== void 0 ? _a : [])
                ];
                const existingLinkIndex = newConsumerPackageLinks.findIndex((link) => link.linkedPackageName === packageName);
                if (existingLinkIndex >= 0) {
                    newConsumerPackageLinks.splice(existingLinkIndex, 1);
                }
                newConsumerPackageLinks.push({
                    linkedPackagePath,
                    linkedPackageName: packageName,
                    affectedPnpmVirtualStoreFolderPaths: Array.from(sourcePathSet),
                    linkType: 'BridgePackage'
                });
                const newLinksBySubspaceName = new Map(linksBySubspaceName);
                newLinksBySubspaceName.set(subspaceName, newConsumerPackageLinks);
                return newLinksBySubspaceName;
            });
            terminal.writeLine(terminal_1.Colorize.green(`Successfully bridged package "${packageName}" for "${subspaceName}"`));
        }
        catch (error) {
            terminal.writeErrorLine(terminal_1.Colorize.red(`Failed to bridge package "${linkedPackagePath}" to "${subspaceName}": ${error.message}`));
            const alreadyExistsError = new node_core_library_1.AlreadyReportedError();
            alreadyExistsError.message = error.message;
            alreadyExistsError.stack = error.stack;
            throw alreadyExistsError;
        }
    }
    async linkPackageAsync(terminal, consumerPackage, linkedPackagePath) {
        const consumerPackageName = consumerPackage.packageName;
        try {
            const { packageName: linkedPackageName } = await this._getLinkedPackageInfoAsync(linkedPackagePath);
            const slashIndex = linkedPackageName.indexOf('/');
            const [scope, packageBaseName] = slashIndex !== -1
                ? [linkedPackageName.substring(0, slashIndex), linkedPackageName.substring(slashIndex)]
                : [undefined, linkedPackageName];
            let sourceNodeModulesPath = `${consumerPackage.projectFolder}/${RushConstants_1.RushConstants.nodeModulesFolderName}`;
            if (scope) {
                sourceNodeModulesPath = `${sourceNodeModulesPath}/${scope}`;
            }
            await node_core_library_1.FileSystem.ensureFolderAsync(sourceNodeModulesPath);
            const symlinkPath = `${sourceNodeModulesPath}/${packageBaseName}`;
            // Create symlink to linkedPackage
            await BaseLinkManager_1.BaseLinkManager._createSymlinkAsync({
                symlinkKind: BaseLinkManager_1.SymlinkKind.Directory,
                linkTargetPath: linkedPackagePath,
                newLinkPath: symlinkPath,
                alreadyExistsBehavior: node_core_library_1.AlreadyExistsBehavior.Overwrite
            });
            // Record the link information between the consumer package and the linked package
            await this._modifyAndSaveLinkStateAsync((linksBySubspaceName) => {
                var _a;
                const subspaceName = consumerPackage.subspace.subspaceName;
                const newConsumerPackageLinks = [
                    ...((_a = linksBySubspaceName.get(subspaceName)) !== null && _a !== void 0 ? _a : [])
                ];
                const existingLinkIndex = newConsumerPackageLinks.findIndex((link) => link.linkedPackageName === linkedPackageName);
                if (existingLinkIndex >= 0) {
                    newConsumerPackageLinks.splice(existingLinkIndex, 1);
                }
                newConsumerPackageLinks.push({
                    linkedPackagePath,
                    linkedPackageName,
                    linkType: 'LinkPackage'
                });
                const newLinksBySubspaceName = new Map(linksBySubspaceName);
                newLinksBySubspaceName.set(subspaceName, newConsumerPackageLinks);
                return newLinksBySubspaceName;
            });
            terminal.writeLine(terminal_1.Colorize.green(`Successfully linked package "${linkedPackageName}" for "${consumerPackageName}"`));
        }
        catch (error) {
            terminal.writeErrorLine(terminal_1.Colorize.red(`Failed to link package "${linkedPackagePath}" to "${consumerPackageName}": ${error.message}`));
            const alreadyExistsError = new node_core_library_1.AlreadyReportedError();
            alreadyExistsError.message = error.message;
            alreadyExistsError.stack = error.stack;
            throw alreadyExistsError;
        }
    }
    static loadFromRushConfiguration(rushConfiguration) {
        // TODO: make this function async
        const rushLinkStateFilePath = `${rushConfiguration.commonTempFolder}/${RushConstants_1.RushConstants.rushHotlinkStateFilename}`;
        let rushLinkState;
        try {
            rushLinkState = node_core_library_1.JsonFile.loadAndValidate(rushLinkStateFilePath, PROJECT_LINKS_STATE_JSON_SCHEMA);
        }
        catch (error) {
            if (!node_core_library_1.FileSystem.isNotExistError(error)) {
                throw error;
            }
        }
        if (!rushLinkState) {
            return new HotlinkManager({
                rushLinkStateFilePath,
                linksBySubspaceName: new Map()
            });
        }
        else {
            const { fileVersion, linksBySubspace } = rushLinkState;
            if (fileVersion !== 0) {
                throw new Error(`The rush project link state file "${rushLinkStateFilePath}" has an unexpected format, so this repo's ` +
                    `installation state is likely in an inconsistent state. Run 'rush ${actionNameConstants_1.PURGE_ACTION_NAME}' purge to clear ` +
                    `the installation.`);
            }
            else {
                const linksBySubspaceName = new Map(Object.entries(linksBySubspace));
                return new HotlinkManager({
                    rushLinkStateFilePath,
                    linksBySubspaceName
                });
            }
        }
    }
}
exports.HotlinkManager = HotlinkManager;
//# sourceMappingURL=HotlinkManager.js.map