// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { FileSystem, JsonFile, LockFile, NewlineKind, PackageName } from '@rushstack/node-core-library';
import { Colorize } from '@rushstack/terminal';
import { Utilities } from '../utilities/Utilities';
import { PackageJsonEditor } from '../api/PackageJsonEditor';
import { InstallHelpers } from './installManager/InstallHelpers';
import { RushConstants } from './RushConstants';
import { LastInstallFlag } from '../api/LastInstallFlag';
import { RushCommandLineParser } from '../cli/RushCommandLineParser';
export class Autoinstaller {
    constructor(options) {
        var _a;
        this.name = options.autoinstallerName;
        this._rushConfiguration = options.rushConfiguration;
        this._rushGlobalFolder = options.rushGlobalFolder;
        this._restrictConsoleOutput =
            (_a = options.restrictConsoleOutput) !== null && _a !== void 0 ? _a : RushCommandLineParser.shouldRestrictConsoleOutput();
        Autoinstaller.validateName(this.name);
    }
    // Example: .../common/autoinstallers/my-task
    get folderFullPath() {
        return path.join(this._rushConfiguration.commonAutoinstallersFolder, this.name);
    }
    // Example: .../common/autoinstallers/my-task/package-lock.yaml
    get shrinkwrapFilePath() {
        return path.join(this._rushConfiguration.commonAutoinstallersFolder, this.name, this._rushConfiguration.shrinkwrapFilename);
    }
    // Example: .../common/autoinstallers/my-task/package.json
    get packageJsonPath() {
        return path.join(this._rushConfiguration.commonAutoinstallersFolder, this.name, 'package.json');
    }
    static validateName(autoinstallerName) {
        const nameOrError = PackageName.tryParse(autoinstallerName);
        if (nameOrError.error) {
            throw new Error(`The specified name "${autoinstallerName}" is invalid: ` + nameOrError.error);
        }
        if (nameOrError.scope) {
            throw new Error(`The specified name "${autoinstallerName}" must not contain an NPM scope`);
        }
    }
    async prepareAsync() {
        const autoinstallerFullPath = this.folderFullPath;
        if (!FileSystem.exists(autoinstallerFullPath)) {
            throw new Error(`The autoinstaller ${this.name} does not exist, Please run\nrush init-autoinstaller --name ${this.name}\n`);
        }
        await InstallHelpers.ensureLocalPackageManagerAsync(this._rushConfiguration, this._rushGlobalFolder, RushConstants.defaultMaxInstallAttempts, this._restrictConsoleOutput);
        // Example: common/autoinstallers/my-task/package.json
        const relativePathForLogs = path.relative(this._rushConfiguration.rushJsonFolder, autoinstallerFullPath);
        this._logIfConsoleOutputIsNotRestricted(`Acquiring lock for "${relativePathForLogs}" folder...`);
        const lock = await LockFile.acquireAsync(autoinstallerFullPath, 'autoinstaller');
        try {
            // Example: .../common/autoinstallers/my-task/.rush/temp
            const lastInstallFlagPath = path.join(autoinstallerFullPath, RushConstants.projectRushFolderName, 'temp');
            const packageJsonPath = path.join(autoinstallerFullPath, 'package.json');
            const packageJson = JsonFile.load(packageJsonPath);
            const lastInstallFlag = new LastInstallFlag(lastInstallFlagPath, {
                node: process.versions.node,
                packageManager: this._rushConfiguration.packageManager,
                packageManagerVersion: this._rushConfiguration.packageManagerToolVersion,
                packageJson: packageJson,
                rushJsonFolder: this._rushConfiguration.rushJsonFolder
            });
            // Example: ../common/autoinstallers/my-task/node_modules
            const nodeModulesFolder = `${autoinstallerFullPath}/${RushConstants.nodeModulesFolderName}`;
            const flagPath = `${nodeModulesFolder}/rush-autoinstaller.flag`;
            const isLastInstallFlagDirty = !(await lastInstallFlag.isValidAsync()) || !FileSystem.exists(flagPath);
            if (isLastInstallFlagDirty || lock.dirtyWhenAcquired) {
                if (FileSystem.exists(nodeModulesFolder)) {
                    this._logIfConsoleOutputIsNotRestricted('Deleting old files from ' + nodeModulesFolder);
                    FileSystem.ensureEmptyFolder(nodeModulesFolder);
                }
                // Copy: .../common/autoinstallers/my-task/.npmrc
                Utilities.syncNpmrc({
                    sourceNpmrcFolder: this._rushConfiguration.commonRushConfigFolder,
                    targetNpmrcFolder: autoinstallerFullPath,
                    supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm
                });
                this._logIfConsoleOutputIsNotRestricted(`Installing dependencies under ${autoinstallerFullPath}...\n`);
                await Utilities.executeCommandAsync({
                    command: this._rushConfiguration.packageManagerToolFilename,
                    args: ['install', '--frozen-lockfile'],
                    workingDirectory: autoinstallerFullPath,
                    keepEnvironment: true
                });
                // Create file: ../common/autoinstallers/my-task/.rush/temp/last-install.flag
                await lastInstallFlag.createAsync();
                FileSystem.writeFile(flagPath, 'If this file is deleted, Rush will assume that the node_modules folder has been cleaned and will reinstall it.');
                this._logIfConsoleOutputIsNotRestricted('Auto install completed successfully\n');
            }
            else {
                this._logIfConsoleOutputIsNotRestricted('Autoinstaller folder is already up to date\n');
            }
        }
        finally {
            // Ensure the lockfile is released when we are finished.
            lock.release();
        }
    }
    async updateAsync() {
        await InstallHelpers.ensureLocalPackageManagerAsync(this._rushConfiguration, this._rushGlobalFolder, RushConstants.defaultMaxInstallAttempts, this._restrictConsoleOutput);
        const autoinstallerPackageJsonPath = path.join(this.folderFullPath, 'package.json');
        if (!(await FileSystem.existsAsync(autoinstallerPackageJsonPath))) {
            throw new Error(`The specified autoinstaller path does not exist: ` + autoinstallerPackageJsonPath);
        }
        this._logIfConsoleOutputIsNotRestricted(`Updating autoinstaller package: ${autoinstallerPackageJsonPath}`);
        let oldFileContents = '';
        if (await FileSystem.existsAsync(this.shrinkwrapFilePath)) {
            oldFileContents = FileSystem.readFile(this.shrinkwrapFilePath, { convertLineEndings: NewlineKind.Lf });
            this._logIfConsoleOutputIsNotRestricted('Deleting ' + this.shrinkwrapFilePath);
            await FileSystem.deleteFileAsync(this.shrinkwrapFilePath);
            if (this._rushConfiguration.isPnpm) {
                // Workaround for https://github.com/pnpm/pnpm/issues/1890
                //
                // When "rush update-autoinstaller" is run, Rush deletes "common/autoinstallers/my-task/pnpm-lock.yaml"
                // so that a new lockfile will be generated. However "pnpm install" by design will try to recover
                // "pnpm-lock.yaml" from "my-task/node_modules/.pnpm/lock.yaml", which may prevent a full upgrade.
                // Deleting both files ensures that a new lockfile will always be generated.
                const pnpmPackageManager = this._rushConfiguration
                    .packageManagerWrapper;
                await FileSystem.deleteFileAsync(path.join(this.folderFullPath, pnpmPackageManager.internalShrinkwrapRelativePath));
            }
        }
        // Detect a common mistake where PNPM prints "Already up-to-date" without creating a shrinkwrap file
        const packageJsonEditor = PackageJsonEditor.load(this.packageJsonPath);
        if (packageJsonEditor.dependencyList.length === 0) {
            throw new Error('You must add at least one dependency to the autoinstaller package' +
                ' before invoking this command:\n' +
                this.packageJsonPath);
        }
        this._logIfConsoleOutputIsNotRestricted();
        Utilities.syncNpmrc({
            sourceNpmrcFolder: this._rushConfiguration.commonRushConfigFolder,
            targetNpmrcFolder: this.folderFullPath,
            supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm
        });
        await Utilities.executeCommandAsync({
            command: this._rushConfiguration.packageManagerToolFilename,
            args: ['install'],
            workingDirectory: this.folderFullPath,
            keepEnvironment: true
        });
        this._logIfConsoleOutputIsNotRestricted();
        if (this._rushConfiguration.packageManager === 'npm') {
            this._logIfConsoleOutputIsNotRestricted(Colorize.bold('Running "npm shrinkwrap"...'));
            await Utilities.executeCommandAsync({
                command: this._rushConfiguration.packageManagerToolFilename,
                args: ['shrinkwrap'],
                workingDirectory: this.folderFullPath,
                keepEnvironment: true
            });
            this._logIfConsoleOutputIsNotRestricted('"npm shrinkwrap" completed');
            this._logIfConsoleOutputIsNotRestricted();
        }
        if (!(await FileSystem.existsAsync(this.shrinkwrapFilePath))) {
            throw new Error('The package manager did not create the expected shrinkwrap file: ' + this.shrinkwrapFilePath);
        }
        const newFileContents = await FileSystem.readFileAsync(this.shrinkwrapFilePath, {
            convertLineEndings: NewlineKind.Lf
        });
        if (oldFileContents !== newFileContents) {
            this._logIfConsoleOutputIsNotRestricted(Colorize.green('The shrinkwrap file has been updated.') + '  Please commit the updated file:');
            this._logIfConsoleOutputIsNotRestricted(`\n  ${this.shrinkwrapFilePath}`);
        }
        else {
            this._logIfConsoleOutputIsNotRestricted(Colorize.green('Already up to date.'));
        }
    }
    _logIfConsoleOutputIsNotRestricted(message) {
        if (!this._restrictConsoleOutput) {
            // eslint-disable-next-line no-console
            console.log(message !== null && message !== void 0 ? message : '');
        }
    }
}
//# sourceMappingURL=Autoinstaller.js.map