text: // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
  // See LICENSE in the project root for license information.
  
  import * as colors from 'colors';
  import * as path from 'path';
  import * as resolve from 'resolve';
  import * as npmPacklist from 'npm-packlist';
  import pnpmLinkBins from '@pnpm/link-bins';
  
  // (Used only by the legacy code fragment in the resolve.sync() hook below)
  import * as fsForResolve from 'fs';
  
  import ignore, { Ignore } from 'ignore';
  import {
    Path,
    FileSystem,
    PackageJsonLookup,
    FileSystemStats,
    Sort,
    JsonFile,
    IPackageJson,
    AlreadyExistsBehavior,
    InternalError,
    NewlineKind,
    Text
  } from '@rushstack/node-core-library';
  import { DeployArchiver } from './DeployArchiver';
  import { RushConfiguration } from '../../api/RushConfiguration';
  import { SymlinkAnalyzer, ILinkInfo } from './SymlinkAnalyzer';
  import { RushConfigurationProject } from '../../api/RushConfigurationProject';
  import { DeployScenarioConfiguration, IDeployScenarioProjectJson } from './DeployScenarioConfiguration';
  import { PnpmfileConfiguration } from './PnpmfileConfiguration';
  import { matchesWithStar } from './Utils';
  
  // (@types/npm-packlist is missing this API)
  declare module 'npm-packlist' {
    export class WalkerSync {
      public readonly result: string[];
      public constructor(opts: { path: string });
      public start(): void;
    }
  }
  
  /**
   * The deploy-matadata.json file format.
   */
  export interface IDeployMetadataJson {
    scenarioName: string;
    mainProjectName: string;
    projects: IProjectInfoJson[];
    links: ILinkInfo[];
  }
  
  /**
   * Part of the deploy-matadata.json file format. Represents a Rush project to be deployed.
   */
  interface IProjectInfoJson {
    /**
     * This path is relative to the deploy folder.
     */
    path: string;
  }
  
  /**
   * Stores additional information about folders being copied.
   * Only some of the IDeployState.foldersToCopy items will an IFolderInfo object.
   */
  interface IFolderInfo {
    /**
     * This is the lookup key for IDeployState.folderInfosByPath.
     * It is an absolute real path.
     */
    folderPath: string;
    /**
     * True if this is the package folder for a local Rush project.
     */
    isRushProject: boolean;
  
    projectSettings?: IDeployScenarioProjectJson;
  }
  
  /**
   * This object tracks DeployManager state during a deployment.
   */
  export interface IDeployState {
    scenarioFilePath: string;
  
    /**
     * The parsed scenario config file, as defined by the "deploy-scenario.schema.json" JSON schema
     */
    scenarioConfiguration: DeployScenarioConfiguration;
  
    mainProjectName: string;
  
    /**
     * The source folder that copying originates from.  Generally it is the repo root folder with rush.json.
     */
    sourceRootFolder: string;
  
    /**
     * The target folder for the deployment.  By default it will be "common/deploy".
     */
    targetRootFolder: string;
  
    /**
     * During the analysis stage, _collectFoldersRecursive() uses this set to collect the absolute paths
     * of the package folders to be copied.  The copying is performed later by _deployFolder().
     */
    foldersToCopy: Set<string>;
  
    /**
     * Additional information about some of the foldersToCopy paths.
     * The key is the absolute real path from foldersToCopy.
     */
    folderInfosByPath: Map<string, IFolderInfo>;
  
    symlinkAnalyzer: SymlinkAnalyzer;
  
    pnpmfileConfiguration: PnpmfileConfiguration;
  
    /**
     * The desired path to be used when archiving the target folder. Supported file extensions: .zip.
     */
    createArchiveFilePath: string | undefined;
  }
  
  /**
   * Manages the business logic for the "rush deploy" command.
   */
  export class DeployManager {
    private readonly _rushConfiguration: RushConfiguration;
    private readonly _packageJsonLookup: PackageJsonLookup;
  
    public constructor(rushConfiguration: RushConfiguration) {
      this._rushConfiguration = rushConfiguration;
      this._packageJsonLookup = new PackageJsonLookup();
    }
  
    /**
     * Recursively crawl the node_modules dependencies and collect the result in IDeployState.foldersToCopy.
     */
    private _collectFoldersRecursive(packageJsonFolderPath: string, deployState: IDeployState): void {
      const packageJsonRealFolderPath: string = FileSystem.getRealPath(packageJsonFolderPath);
  
      if (deployState.foldersToCopy.has(packageJsonRealFolderPath)) {
        // we've already seen this folder
        return;
      }
  
      deployState.foldersToCopy.add(packageJsonRealFolderPath);
  
      const originalPackageJson: IPackageJson = JsonFile.load(
        path.join(packageJsonRealFolderPath, 'package.json')
      );
  
      const sourceFolderInfo: IFolderInfo | undefined = deployState.folderInfosByPath.get(
        FileSystem.getRealPath(packageJsonFolderPath)
      );
  
      // Transform packageJson using pnpmfile.js
      const packageJson: IPackageJson = deployState.pnpmfileConfiguration.transform(originalPackageJson);
  
      // Union of keys from regular dependencies, peerDependencies, optionalDependencies
      // (and possibly devDependencies if includeDevDependencies=true)
      const dependencyNamesToProcess: Set<string> = new Set<string>();
  
      // Just the keys from optionalDependencies and peerDependencies
      const optionalDependencyNames: Set<string> = new Set<string>();
  
      for (const name of Object.keys(packageJson.dependencies || {})) {
        dependencyNamesToProcess.add(name);
      }
      if (deployState.scenarioConfiguration.json.includeDevDependencies) {
        for (const name of Object.keys(packageJson.devDependencies || {})) {
          dependencyNamesToProcess.add(name);
        }
      }
      for (const name of Object.keys(packageJson.peerDependencies || {})) {
        dependencyNamesToProcess.add(name);
        optionalDependencyNames.add(name); // consider peers optional, since they are so frequently broken
      }
      for (const name of Object.keys(packageJson.optionalDependencies || {})) {
        dependencyNamesToProcess.add(name);
        optionalDependencyNames.add(name);
      }
  
      if (sourceFolderInfo && sourceFolderInfo.isRushProject) {
        const projectSettings: IDeployScenarioProjectJson | undefined = sourceFolderInfo.projectSettings;
        if (projectSettings) {
          this._applyDependencyFilters(
            dependencyNamesToProcess,
            projectSettings.additionalDependenciesToInclude,
            projectSettings.dependenciesToExclude
          );
        }
      }
  
      for (const dependencyPackageName of dependencyNamesToProcess) {
        try {
          this._traceResolveDependency(dependencyPackageName, packageJsonRealFolderPath, deployState);
        } catch (resolveErr) {
          if (resolveErr.code === 'MODULE_NOT_FOUND' && optionalDependencyNames.has(dependencyPackageName)) {
            // Ignore missing optional dependency
            continue;
          }
          throw resolveErr;
        }
      }
  
      if (
        this._rushConfiguration.packageManager === 'pnpm' &&
        !deployState.scenarioConfiguration.json.omitPnpmWorkaroundLinks
      ) {
        // Replicate the PNPM workaround links.
  
        // Only apply this logic for packages that were actually installed under the common/temp folder.
        if (Path.isUnder(packageJsonFolderPath, this._rushConfiguration.commonTempFolder)) {
          try {
            // The PNPM workaround links are created in this folder.  We will resolve the current package
            // from that location and collect any additional links encountered along the way.
            const pnpmDotFolderPath: string = path.join(
              this._rushConfiguration.commonTempFolder,
              'node_modules',
              '.pnpm'
            );
  
            // TODO: Investigate how package aliases are handled by PNPM in this case.  For example:
            //
            // "dependencies": {
            //   "alias-name": "npm:real-name@^1.2.3"
            // }
            this._traceResolveDependency(packageJson.name, pnpmDotFolderPath, deployState);
          } catch (resolveErr) {
            if (resolveErr.code === 'MODULE_NOT_FOUND') {
              // The workaround link isn't guaranteed to exist, so ignore if it's missing
              // NOTE: If you encounter this warning a lot, please report it to the Rush maintainers.
              console.log('Ignoring missing PNPM workaround link for ' + packageJsonFolderPath);
            }
          }
        }
      }
    }
  
    private _applyDependencyFilters(
      allDependencyNames: Set<string>,
      additionalDependenciesToInclude: string[] = [],
      dependenciesToExclude: string[] = []
    ): Set<string> {
      // Track packages that got added/removed for reporting purposes
      const extraIncludedPackageNames: string[] = [];
      const extraExcludedPackageNames: string[] = [];
  
      for (const patternWithStar of dependenciesToExclude) {
        for (const dependency of allDependencyNames) {
          if (matchesWithStar(patternWithStar, dependency)) {
            if (allDependencyNames.delete(dependency)) {
              extraExcludedPackageNames.push(dependency);
            }
          }
        }
      }
  
      for (const dependencyToInclude of additionalDependenciesToInclude) {
        if (!allDependencyNames.has(dependencyToInclude)) {
          allDependencyNames.add(dependencyToInclude);
          extraIncludedPackageNames.push(dependencyToInclude);
        }
      }
  
      if (extraIncludedPackageNames.length > 0) {
        extraIncludedPackageNames.sort();
        console.log('Extra dependencies included by settings: ' + extraIncludedPackageNames.join(', '));
      }
  
      if (extraExcludedPackageNames.length > 0) {
        extraExcludedPackageNames.sort();
        console.log('Extra dependencies excluded by settings: ' + extraExcludedPackageNames.join(', '));
      }
  
      return allDependencyNames;
    }
  
    private _traceResolveDependency(
      packageName: string,
      startingFolder: string,
      deployState: IDeployState
    ): void {
      // The "resolve" library models the Node.js require() API, which gives precedence to "core" system modules
      // over an NPM package with the same name.  But we are traversing package.json dependencies, which never
      // refer to system modules.  Appending a "/" forces require() to look for the NPM package.
      const resolveSuffix: string = packageName + resolve.isCore(packageName) ? '/' : '';
  
      const resolvedDependency: string = resolve.sync(packageName + resolveSuffix, {
        basedir: startingFolder,
        preserveSymlinks: false,
        packageFilter: (pkg, dir) => {
          // point "main" at a file that is guaranteed to exist
          // This helps resolve packages such as @types/node that have no entry point
          pkg.main = './package.json';
          return pkg;
        },
        realpathSync: (filePath) => {
          // This code fragment is a modification of the documented default implementation from the "fs-extra" docs
          try {
            const resolvedPath: string = fsForResolve.realpathSync(filePath);
  
            deployState.symlinkAnalyzer.analyzePath(filePath);
            return resolvedPath;
          } catch (realpathErr) {
            if (realpathErr.code !== 'ENOENT') {
              throw realpathErr;
            }
          }
          return filePath;
        }
      });
  
      if (!resolvedDependency) {
        // This should not happen, since the resolve.sync() docs say it will throw an exception instead
        throw new InternalError(`Error resolving ${packageName} from ${startingFolder}`);
      }
  
      const dependencyPackageFolderPath: string | undefined = this._packageJsonLookup.tryGetPackageFolderFor(
        resolvedDependency
      );
  
      if (!dependencyPackageFolderPath) {
        throw new Error(`Error finding package.json folder for ${resolvedDependency}`);
      }
  
      this._collectFoldersRecursive(dependencyPackageFolderPath, deployState);
    }
  
    /**
     * Maps a file path from IDeployState.sourceRootFolder --> IDeployState.targetRootFolder
     *
     * Example input: "C:\MyRepo\libraries\my-lib"
     * Example output: "C:\MyRepo\common\deploy\libraries\my-lib"
     */
    private _remapPathForDeployFolder(absolutePathInSourceFolder: string, deployState: IDeployState): string {
      if (!Path.isUnderOrEqual(absolutePathInSourceFolder, deployState.sourceRootFolder)) {
        throw new Error(
          `Source path is not under ${deployState.sourceRootFolder}\n${absolutePathInSourceFolder}`
        );
      }
      const relativePath: string = path.relative(deployState.sourceRootFolder, absolutePathInSourceFolder);
      const absolutePathInTargetFolder: string = path.join(deployState.targetRootFolder, relativePath);
      return absolutePathInTargetFolder;
    }
  
    /**
     * Maps a file path from IDeployState.sourceRootFolder --> relative path
     *
     * Example input: "C:\MyRepo\libraries\my-lib"
     * Example output: "libraries/my-lib"
     */
    private _remapPathForDeployMetadata(absolutePathInSourceFolder: string, deployState: IDeployState): string {
      if (!Path.isUnderOrEqual(absolutePathInSourceFolder, deployState.sourceRootFolder)) {
        throw new Error(
          `Source path is not under ${deployState.sourceRootFolder}\n${absolutePathInSourceFolder}`
        );
      }
      const relativePath: string = path.relative(deployState.sourceRootFolder, absolutePathInSourceFolder);
      return Text.replaceAll(relativePath, '\\', '/');
    }
  
    /**
     * Copy one package folder to the deployment target folder.
     */
    private _deployFolder(sourceFolderPath: string, deployState: IDeployState): void {
      let useNpmIgnoreFilter: boolean = false;
  
      if (!deployState.scenarioConfiguration.json.includeNpmIgnoreFiles) {
        const sourceFolderInfo: IFolderInfo | undefined = deployState.folderInfosByPath.get(
          FileSystem.getRealPath(sourceFolderPath)
        );
        if (sourceFolderInfo) {
          if (sourceFolderInfo.isRushProject) {
            useNpmIgnoreFilter = true;
          }
        }
      }
  
      const targetFolderPath: string = this._remapPathForDeployFolder(sourceFolderPath, deployState);
  
      if (useNpmIgnoreFilter) {
        // Use npm-packlist to filter the files.  Using the WalkerSync class (instead of the sync() API) ensures
        // that "bundledDependencies" are not included.
        const walker: npmPacklist.WalkerSync = new npmPacklist.WalkerSync({
          path: sourceFolderPath
        });
        walker.start();
        const npmPackFiles: string[] = walker.result;
  
        const alreadyCopiedSourcePaths: Set<string> = new Set();
  
        for (const npmPackFile of npmPackFiles) {
          // In issue https://github.com/microsoft/rushstack/issues/2121 we found that npm-packlist sometimes returns
          // duplicate file paths, for example:
          //
          //   'dist//index.js'
          //   'dist/index.js'
          //
          // We can detect the duplicates by comparing the path.resolve() result.
          const copySourcePath: string = path.resolve(sourceFolderPath, npmPackFile);
          if (alreadyCopiedSourcePaths.has(copySourcePath)) {
            continue;
          }
          alreadyCopiedSourcePaths.add(copySourcePath);
  
          const copyDestinationPath: string = path.join(targetFolderPath, npmPackFile);
  
          if (deployState.symlinkAnalyzer.analyzePath(copySourcePath).kind !== 'link') {
            FileSystem.ensureFolder(path.dirname(copyDestinationPath));
  
            FileSystem.copyFile({
              sourcePath: copySourcePath,
              destinationPath: copyDestinationPath,
              alreadyExistsBehavior: AlreadyExistsBehavior.Error
            });
          }
        }
      } else {
        // use a simplistic "ignore" ruleset to filter the files
        const ignoreFilter: Ignore = ignore();
        ignoreFilter.add([
          // The top-level node_modules folder is always excluded
          '/node_modules',
          // Also exclude well-known folders that can contribute a lot of unnecessary files
          '**/.git',
          '**/.svn',
          '**/.hg',
          '**/.DS_Store'
        ]);
  
        FileSystem.copyFiles({
          sourcePath: sourceFolderPath,
          destinationPath: targetFolderPath,
          alreadyExistsBehavior: AlreadyExistsBehavior.Error,
          filter: (src: string, dest: string) => {
            const relativeSrc: string = path.relative(sourceFolderPath, src);
            if (!relativeSrc) {
              return true; // don't filter sourceFolderPath itself
            }
  
            if (ignoreFilter.ignores(relativeSrc)) {
              return false;
            }
  
            const stats: FileSystemStats = FileSystem.getLinkStatistics(src);
            if (stats.isSymbolicLink()) {
              deployState.symlinkAnalyzer.analyzePath(src);
              return false;
            } else {
              return true;
            }
          }
        });
      }
    }
  
    /**
     * Create a symlink as described by the ILinkInfo object.
     */
    private _deploySymlink(originalLinkInfo: ILinkInfo, deployState: IDeployState): boolean {
      const linkInfo: ILinkInfo = {
        kind: originalLinkInfo.kind,
        linkPath: this._remapPathForDeployFolder(originalLinkInfo.linkPath, deployState),
        targetPath: this._remapPathForDeployFolder(originalLinkInfo.targetPath, deployState)
      };
  
      // Has the link target been created yet?  If not, we should try again later
      if (!FileSystem.exists(linkInfo.targetPath)) {
        return false;
      }
  
      const newLinkFolder: string = path.dirname(linkInfo.linkPath);
      FileSystem.ensureFolder(newLinkFolder);
  
      // Link to the relative path for symlinks
      const relativeTargetPath: string = path.relative(newLinkFolder, linkInfo.targetPath);
  
      // NOTE: This logic is based on NpmLinkManager._createSymlink()
      if (process.platform === 'win32') {
        if (linkInfo.kind === 'folderLink') {
          // For directories, we use a Windows "junction".  On Unix, this produces a regular symlink.
          FileSystem.createSymbolicLinkJunction({
            linkTargetPath: relativeTargetPath,
            newLinkPath: linkInfo.linkPath
          });
        } else {
          // For files, we use a Windows "hard link", because creating a symbolic link requires
          // administrator permission.
  
          // NOTE: We cannot use the relative path for hard links
          FileSystem.createHardLink({
            linkTargetPath: relativeTargetPath,
            newLinkPath: linkInfo.linkPath
          });
        }
      } else {
        // However hard links seem to cause build failures on Mac, so for all other operating systems
        // we use symbolic links for this case.
        if (linkInfo.kind === 'folderLink') {
          FileSystem.createSymbolicLinkFolder({
            linkTargetPath: relativeTargetPath,
            newLinkPath: linkInfo.linkPath
          });
        } else {
          FileSystem.createSymbolicLinkFile({
            linkTargetPath: relativeTargetPath,
            newLinkPath: linkInfo.linkPath
          });
        }
      }
  
      return true;
    }
  
    /**
     * Recursively apply the "additionalProjectToInclude" setting.
     */
    private _collectAdditionalProjectsToInclude(
      includedProjectNamesSet: Set<string>,
      projectName: string,
      deployState: IDeployState
    ): void {
      if (includedProjectNamesSet.has(projectName)) {
        return;
      }
      includedProjectNamesSet.add(projectName);
  
      const projectSettings:
        | IDeployScenarioProjectJson
        | undefined = deployState.scenarioConfiguration.projectJsonsByName.get(projectName);
      if (projectSettings && projectSettings.additionalProjectsToInclude) {
        for (const additionalProjectToInclude of projectSettings.additionalProjectsToInclude) {
          this._collectAdditionalProjectsToInclude(
            includedProjectNamesSet,
            additionalProjectToInclude,
            deployState
          );
        }
      }
    }
  
    /**
     * Write the common/deploy/deploy-metadata.json file.
     */
    private _writeDeployMetadata(deployState: IDeployState): void {
      const deployMetadataFilePath: string = path.join(deployState.targetRootFolder, 'deploy-metadata.json');
  
      const deployMetadataJson: IDeployMetadataJson = {
        scenarioName: path.basename(deployState.scenarioFilePath),
        mainProjectName: deployState.mainProjectName,
        projects: [],
        links: []
      };
  
      deployState.folderInfosByPath.forEach((folderInfo) => {
        if (!folderInfo.isRushProject) {
          // It's not a Rush project
          return;
        }
  
        if (!deployState.foldersToCopy.has(folderInfo.folderPath)) {
          // It's not something we crawled
          return;
        }
  
        deployMetadataJson.projects.push({
          path: this._remapPathForDeployMetadata(folderInfo.folderPath, deployState)
        });
      });
  
      // Remap the links to be relative to target folder
      for (const absoluteLinkInfo of deployState.symlinkAnalyzer.reportSymlinks()) {
        const relativeInfo: ILinkInfo = {
          kind: absoluteLinkInfo.kind,
          linkPath: this._remapPathForDeployMetadata(absoluteLinkInfo.linkPath, deployState),
          targetPath: this._remapPathForDeployMetadata(absoluteLinkInfo.targetPath, deployState)
        };
        deployMetadataJson.links.push(relativeInfo);
      }
  
      JsonFile.save(deployMetadataJson, deployMetadataFilePath, {
        newlineConversion: NewlineKind.OsDefault
      });
    }
  
    private async _makeBinLinksAsync(deployState: IDeployState): Promise<void> {
      for (const [, folderInfo] of deployState.folderInfosByPath) {
        if (!folderInfo.isRushProject) {
          return;
        }
  
        const deployedPath: string = this._remapPathForDeployMetadata(folderInfo.folderPath, deployState);
        const projectFolder: string = path.join(deployState.targetRootFolder, deployedPath, 'node_modules');
        const projectBinFolder: string = path.join(
          deployState.targetRootFolder,
          deployedPath,
          'node_modules',
          '.bin'
        );
  
        await pnpmLinkBins(projectFolder, projectBinFolder, {
          warn: (msg: string) => console.warn(colors.yellow(msg))
        });
      }
    }
  
    private async _prepareDeploymentAsync(deployState: IDeployState): Promise<void> {
      // Calculate the set with additionalProjectsToInclude
      const includedProjectNamesSet: Set<string> = new Set();
      this._collectAdditionalProjectsToInclude(
        includedProjectNamesSet,
        deployState.mainProjectName,
        deployState
      );
  
      for (const rushProject of this._rushConfiguration.projects) {
        const projectFolder: string = FileSystem.getRealPath(rushProject.projectFolder);
        const projectSettings:
          | IDeployScenarioProjectJson
          | undefined = deployState.scenarioConfiguration.projectJsonsByName.get(rushProject.packageName);
  
        deployState.folderInfosByPath.set(projectFolder, {
          folderPath: projectFolder,
          isRushProject: true,
          projectSettings
        });
      }
  
      for (const projectName of includedProjectNamesSet) {
        console.log(colors.cyan('Analyzing project: ') + projectName);
        const project: RushConfigurationProject | undefined = this._rushConfiguration.getProjectByName(
          projectName
        );
  
        if (!project) {
          throw new Error(`The project ${projectName} is not defined in rush.json`);
        }
  
        this._collectFoldersRecursive(project.projectFolder, deployState);
  
        console.log();
      }
  
      Sort.sortSet(deployState.foldersToCopy);
  
      console.log('Copying folders...');
      for (const folderToCopy of deployState.foldersToCopy) {
        this._deployFolder(folderToCopy, deployState);
      }
  
      console.log('Writing deploy-metadata.json');
      this._writeDeployMetadata(deployState);
  
      if (deployState.scenarioConfiguration.json.linkCreation === 'script') {
        console.log('Copying create-links.js');
        FileSystem.copyFile({
          sourcePath: path.join(__dirname, '../../scripts/create-links.js'),
          destinationPath: path.join(deployState.targetRootFolder, 'create-links.js'),
          alreadyExistsBehavior: AlreadyExistsBehavior.Error
        });
      }
  
      if (deployState.scenarioConfiguration.json.linkCreation === 'default') {
        console.log('Creating symlinks...');
        const linksToCopy: ILinkInfo[] = deployState.symlinkAnalyzer.reportSymlinks();
  
        for (const linkToCopy of linksToCopy) {
          if (!this._deploySymlink(linkToCopy, deployState)) {
            // TODO: If a symbolic link points to another symbolic link, then we should order the operations
            // so that the intermediary target is created first.  This case was procrastinated because it does
            // not seem to occur in practice.  If you encounter this, please report it.
            throw new InternalError('Target does not exist: ' + JSON.stringify(linkToCopy, undefined, 2));
          }
        }
  
        await this._makeBinLinksAsync(deployState);
      }
      if (deployState.scenarioConfiguration.json.folderToCopy !== undefined) {
        const sourceFolderPath: string = path.resolve(
          this._rushConfiguration.rushJsonFolder,
          deployState.scenarioConfiguration.json.folderToCopy
        );
        FileSystem.copyFiles({
          sourcePath: sourceFolderPath,
          destinationPath: deployState.targetRootFolder,
          alreadyExistsBehavior: AlreadyExistsBehavior.Error
        });
      }
      await DeployArchiver.createArchiveAsync(deployState);
    }
  
    /**
     * The main entry point for performing a deployment.
     */
    public async deployAsync(
      mainProjectName: string | undefined,
      scenarioName: string | undefined,
      overwriteExisting: boolean,
      targetFolderParameter: string | undefined,
      createArchiveFilePath: string | undefined
    ): Promise<void> {
      const scenarioFilePath: string = DeployScenarioConfiguration.getConfigFilePath(
        scenarioName,
        this._rushConfiguration
      );
      const scenarioConfiguration: DeployScenarioConfiguration = DeployScenarioConfiguration.loadFromFile(
        scenarioFilePath,
        this._rushConfiguration
      );
  
      if (!mainProjectName) {
        if (scenarioConfiguration.json.deploymentProjectNames.length === 1) {
          // If there is only one project, then "--project" is optional
          mainProjectName = scenarioConfiguration.json.deploymentProjectNames[0];
        } else {
          throw new Error(
            `The ${path.basename(scenarioFilePath)} configuration specifies multiple items for` +
              ` "deploymentProjectNames". Use the "--project" parameter to indicate the project to be deployed.`
          );
        }
      } else {
        if (scenarioConfiguration.json.deploymentProjectNames.indexOf(mainProjectName) < 0) {
          throw new Error(
            `The project "${mainProjectName}" does not appear in the list of "deploymentProjectNames"` +
              ` from ${path.basename(scenarioFilePath)}.`
          );
        }
      }
  
      let targetRootFolder: string;
      if (targetFolderParameter) {
        targetRootFolder = path.resolve(targetFolderParameter);
        if (!FileSystem.exists(targetRootFolder)) {
          throw new Error(
            'The specified target folder does not exist: ' + JSON.stringify(targetFolderParameter)
          );
        }
      } else {
        targetRootFolder = path.join(this._rushConfiguration.commonFolder, 'deploy');
      }
      const sourceRootFolder: string = this._rushConfiguration.rushJsonFolder;
  
      console.log(colors.cyan('Deploying to target folder:  ') + targetRootFolder);
      console.log(colors.cyan('Main project for deployment: ') + mainProjectName + '\n');
  
      FileSystem.ensureFolder(targetRootFolder);
  
      // Is the target folder empty?
      if (FileSystem.readFolder(targetRootFolder).length > 0) {
        if (overwriteExisting) {
          console.log('Deleting target folder contents because "--overwrite" was specified...');
          FileSystem.ensureEmptyFolder(targetRootFolder);
          console.log();
        } else {
          throw new Error(
            'The deploy target folder is not empty. You can specify "--overwrite"' +
              ' to recursively delete all folder contents.'
          );
        }
      }
  
      // If create archive is set, ensure it has a legal extension
      if (createArchiveFilePath && path.extname(createArchiveFilePath) !== '.zip') {
        throw new Error(
          'The "--create-archive" parameter currently only supports archives with the .zip file extension.'
        );
      }
  
      const deployState: IDeployState = {
        scenarioFilePath,
        scenarioConfiguration,
        mainProjectName,
        sourceRootFolder,
        targetRootFolder,
        foldersToCopy: new Set(),
        folderInfosByPath: new Map(),
        symlinkAnalyzer: new SymlinkAnalyzer(),
        pnpmfileConfiguration: new PnpmfileConfiguration(this._rushConfiguration),
        createArchiveFilePath
      };
  
      await this._prepareDeploymentAsync(deployState);
  
      console.log('\n' + colors.green('The operation completed successfully.'));
    }
  }
  