public async prepareCommonTempAsync()

in apps/rush-lib/src/logic/installManager/RushInstallManager.ts [81:368]


  public async prepareCommonTempAsync(
    shrinkwrapFile: BaseShrinkwrapFile | undefined
  ): Promise<{ shrinkwrapIsUpToDate: boolean; shrinkwrapWarnings: string[] }> {
    const stopwatch: Stopwatch = Stopwatch.start();

    // Example: "C:\MyRepo\common\temp\projects"
    const tempProjectsFolder: string = path.join(
      this.rushConfiguration.commonTempFolder,
      RushConstants.rushTempProjectsFolderName
    );

    console.log(os.EOL + colors.bold('Updating temp projects in ' + tempProjectsFolder));

    Utilities.createFolderWithRetry(tempProjectsFolder);

    const shrinkwrapWarnings: string[] = [];

    // We will start with the assumption that it's valid, and then set it to false if
    // any of the checks fail
    let shrinkwrapIsUpToDate: boolean = true;

    if (!shrinkwrapFile) {
      shrinkwrapIsUpToDate = false;
    } else if (shrinkwrapFile.isWorkspaceCompatible && !this.options.fullUpgrade) {
      console.log();
      console.log(
        colors.red(
          'The shrinkwrap file had previously been updated to support workspaces. Run "rush update --full" ' +
            'to update the shrinkwrap file.'
        )
      );
      throw new AlreadyReportedError();
    }

    // dependency name --> version specifier
    const allExplicitPreferredVersions: Map<string, string> = this.rushConfiguration
      .getCommonVersions(this.options.variant)
      .getAllPreferredVersions();

    if (shrinkwrapFile) {
      // Check any (explicitly) preferred dependencies first
      allExplicitPreferredVersions.forEach((version: string, dependency: string) => {
        const dependencySpecifier: DependencySpecifier = new DependencySpecifier(dependency, version);

        if (!shrinkwrapFile.hasCompatibleTopLevelDependency(dependencySpecifier)) {
          shrinkwrapWarnings.push(
            `Missing dependency "${dependency}" (${version}) required by the preferred versions from ` +
              RushConstants.commonVersionsFilename
          );
          shrinkwrapIsUpToDate = false;
        }
      });

      if (this._findMissingTempProjects(shrinkwrapFile)) {
        // If any Rush project's tarball is missing from the shrinkwrap file, then we need to update
        // the shrinkwrap file.
        shrinkwrapIsUpToDate = false;
      }

      // If there are orphaned projects, we need to update
      const orphanedProjects: ReadonlyArray<string> = shrinkwrapFile.findOrphanedProjects(
        this.rushConfiguration
      );
      if (orphanedProjects.length > 0) {
        for (const orhpanedProject of orphanedProjects) {
          shrinkwrapWarnings.push(
            `Your ${this.rushConfiguration.shrinkwrapFilePhrase} references "${orhpanedProject}" ` +
              'which was not found in rush.json'
          );
        }
        shrinkwrapIsUpToDate = false;
      }
    }

    // dependency name --> version specifier
    const commonDependencies: Map<string, string> = new Map([
      ...allExplicitPreferredVersions,
      ...this.rushConfiguration.getImplicitlyPreferredVersions(this.options.variant)
    ]);

    // To make the common/package.json file more readable, sort alphabetically
    // according to rushProject.tempProjectName instead of packageName.
    const sortedRushProjects: RushConfigurationProject[] = this.rushConfiguration.projects.slice(0);
    Sort.sortBy(sortedRushProjects, (x) => x.tempProjectName);

    for (const rushProject of sortedRushProjects) {
      const packageJson: PackageJsonEditor = rushProject.packageJsonEditor;

      // Example: "C:\MyRepo\common\temp\projects\my-project-2.tgz"
      const tarballFile: string = this._tempProjectHelper.getTarballFilePath(rushProject);

      // Example: dependencies["@rush-temp/my-project-2"] = "file:./projects/my-project-2.tgz"
      commonDependencies.set(
        rushProject.tempProjectName,
        `file:./${RushConstants.rushTempProjectsFolderName}/${rushProject.unscopedTempProjectName}.tgz`
      );

      const tempPackageJson: IRushTempPackageJson = {
        name: rushProject.tempProjectName,
        version: '0.0.0',
        private: true,
        dependencies: {}
      };

      // Collect pairs of (packageName, packageVersion) to be added as dependencies of the @rush-temp package.json
      const tempDependencies: Map<string, string> = new Map<string, string>();

      // These can be regular, optional, or peer dependencies (but NOT dev dependencies).
      // (A given packageName will never appear more than once in this list.)
      for (const dependency of packageJson.dependencyList) {
        if (this.options.fullUpgrade && this._revertWorkspaceNotation(dependency)) {
          shrinkwrapIsUpToDate = false;
        }

        // If there are any optional dependencies, copy directly into the optionalDependencies field.
        if (dependency.dependencyType === DependencyType.Optional) {
          if (!tempPackageJson.optionalDependencies) {
            tempPackageJson.optionalDependencies = {};
          }
          tempPackageJson.optionalDependencies[dependency.name] = dependency.version;
        } else {
          tempDependencies.set(dependency.name, dependency.version);
        }
      }

      for (const dependency of packageJson.devDependencyList) {
        if (this.options.fullUpgrade && this._revertWorkspaceNotation(dependency)) {
          shrinkwrapIsUpToDate = false;
        }

        // If there are devDependencies, we need to merge them with the regular dependencies.  If the same
        // library appears in both places, then the dev dependency wins (because presumably it's saying what you
        // want right now for development, not the range that you support for consumers).
        tempDependencies.set(dependency.name, dependency.version);
      }
      Sort.sortMapKeys(tempDependencies);

      for (const [packageName, packageVersion] of tempDependencies.entries()) {
        const dependencySpecifier: DependencySpecifier = new DependencySpecifier(packageName, packageVersion);

        // Is there a locally built Rush project that could satisfy this dependency?
        // If so, then we will symlink to the project folder rather than to common/temp/node_modules.
        // In this case, we don't want "npm install" to process this package, but we do need
        // to record this decision for linking later, so we add it to a special 'rushDependencies' field.
        const localProject: RushConfigurationProject | undefined =
          this.rushConfiguration.getProjectByName(packageName);

        if (localProject) {
          // Don't locally link if it's listed in the cyclicDependencyProjects
          if (!rushProject.cyclicDependencyProjects.has(packageName)) {
            // Also, don't locally link if the SemVer doesn't match
            const localProjectVersion: string = localProject.packageJsonEditor.version;
            if (semver.satisfies(localProjectVersion, packageVersion)) {
              // We will locally link this package, so instead add it to our special "rushDependencies"
              // field in the package.json file.
              if (!tempPackageJson.rushDependencies) {
                tempPackageJson.rushDependencies = {};
              }
              tempPackageJson.rushDependencies[packageName] = packageVersion;
              continue;
            }
          }
        }

        // We will NOT locally link this package; add it as a regular dependency.
        tempPackageJson.dependencies![packageName] = packageVersion;

        if (
          shrinkwrapFile &&
          !shrinkwrapFile.tryEnsureCompatibleDependency(dependencySpecifier, rushProject.tempProjectName)
        ) {
          shrinkwrapWarnings.push(
            `Missing dependency "${packageName}" (${packageVersion}) required by "${rushProject.packageName}"`
          );
          shrinkwrapIsUpToDate = false;
        }
      }

      if (this.rushConfiguration.packageManager === 'yarn') {
        // This feature is only implemented by the Yarn package manager
        if (packageJson.resolutionsList.length > 0) {
          tempPackageJson.resolutions = packageJson.saveToObject().resolutions;
        }
      }

      // Example: "C:\MyRepo\common\temp\projects\my-project-2"
      const tempProjectFolder: string = this._tempProjectHelper.getTempProjectFolder(rushProject);

      // Example: "C:\MyRepo\common\temp\projects\my-project-2\package.json"
      const tempPackageJsonFilename: string = path.join(tempProjectFolder, FileConstants.PackageJson);

      // we only want to overwrite the package if the existing tarball's package.json is different from tempPackageJson
      let shouldOverwrite: boolean = true;
      try {
        // if the tarball and the temp file still exist, then compare the contents
        if (FileSystem.exists(tarballFile) && FileSystem.exists(tempPackageJsonFilename)) {
          // compare the extracted package.json with the one we are about to write
          const oldBuffer: Buffer = FileSystem.readFileToBuffer(tempPackageJsonFilename);
          const newBuffer: Buffer = Buffer.from(JsonFile.stringify(tempPackageJson));

          if (Buffer.compare(oldBuffer, newBuffer) === 0) {
            shouldOverwrite = false;
          }
        }
      } catch (error) {
        // ignore the error, we will go ahead and create a new tarball
      }

      if (shouldOverwrite) {
        try {
          // ensure the folder we are about to zip exists
          Utilities.createFolderWithRetry(tempProjectFolder);

          // remove the old tarball & old temp package json, this is for any cases where new tarball creation
          // fails, and the shouldOverwrite logic is messed up because the my-project-2\package.json
          // exists and is updated, but the tarball is not accurate
          FileSystem.deleteFile(tarballFile);
          FileSystem.deleteFile(tempPackageJsonFilename);

          // write the expected package.json file into the zip staging folder
          JsonFile.save(tempPackageJson, tempPackageJsonFilename);

          // Delete the existing tarball and create a new one
          this._tempProjectHelper.createTempProjectTarball(rushProject);

          console.log(`Updating ${tarballFile}`);
        } catch (error) {
          console.log(colors.yellow(error as string));
          // delete everything in case of any error
          FileSystem.deleteFile(tarballFile);
          FileSystem.deleteFile(tempPackageJsonFilename);
        }
      }

      // When using frozen shrinkwrap, we need to validate that the tarball integrities are up-to-date
      // with the shrinkwrap file, since these will cause install to fail.
      if (
        shrinkwrapFile &&
        this.rushConfiguration.packageManager === 'pnpm' &&
        this.rushConfiguration.experimentsConfiguration.configuration.usePnpmFrozenLockfileForRushInstall
      ) {
        const pnpmShrinkwrapFile: PnpmShrinkwrapFile = shrinkwrapFile as PnpmShrinkwrapFile;
        const tarballIntegrityValid: boolean = await this._validateRushProjectTarballIntegrityAsync(
          pnpmShrinkwrapFile,
          rushProject
        );
        if (!tarballIntegrityValid) {
          shrinkwrapIsUpToDate = false;
          shrinkwrapWarnings.push(
            `Invalid or missing tarball integrity hash in shrinkwrap for "${rushProject.packageName}"`
          );
        }
      }

      // Save the package.json if we modified the version references and warn that the package.json was modified
      if (packageJson.saveIfModified()) {
        console.log(
          colors.yellow(
            `"${rushProject.packageName}" depends on one or more local packages which used "workspace:" ` +
              'notation. The package.json has been modified and must be committed to source control.'
          )
        );
      }
    }

    // Remove the workspace file if it exists
    if (this.rushConfiguration.packageManager === 'pnpm') {
      const workspaceFilePath: string = path.join(
        this.rushConfiguration.commonTempFolder,
        'pnpm-workspace.yaml'
      );
      try {
        await FileSystem.deleteFileAsync(workspaceFilePath);
      } catch (e) {
        if (!FileSystem.isNotExistError(e as Error)) {
          throw e;
        }
      }
    }

    // Write the common package.json
    InstallHelpers.generateCommonPackageJson(this.rushConfiguration, commonDependencies);

    stopwatch.stop();
    console.log(`Finished creating temporary modules (${stopwatch.toString()})`);

    return { shrinkwrapIsUpToDate, shrinkwrapWarnings };
  }