diff --git a/apps/api-extractor/package.json b/apps/api-extractor/package.json index 59be8a3229f..94634e98886 100644 --- a/apps/api-extractor/package.json +++ b/apps/api-extractor/package.json @@ -39,7 +39,7 @@ "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", - "semver": "~5.3.0", + "semver": "~7.3.0", "source-map": "~0.6.1", "typescript": "~3.9.5" }, @@ -49,7 +49,7 @@ "@rushstack/eslint-config": "1.0.2", "@types/jest": "25.2.1", "@types/lodash": "4.14.116", - "@types/semver": "5.3.33", + "@types/semver": "~7.3.1", "@types/node": "10.17.13", "@types/resolve": "1.17.1", "gulp": "~4.0.2" diff --git a/apps/rush-lib/assets/rush-init/rush.json b/apps/rush-lib/assets/rush-init/rush.json index a8763548436..775fcc437a3 100644 --- a/apps/rush-lib/assets/rush-init/rush.json +++ b/apps/rush-lib/assets/rush-init/rush.json @@ -26,7 +26,7 @@ * Specify one of: "pnpmVersion", "npmVersion", or "yarnVersion". See the Rush documentation * for details about these alternatives. */ - "pnpmVersion": "4.12.5", + "pnpmVersion": "4.14.4", /*[LINE "HYPOTHETICAL"]*/ "npmVersion": "4.5.0", /*[LINE "HYPOTHETICAL"]*/ "yarnVersion": "1.9.4", @@ -94,7 +94,23 @@ * * The default value is false. */ - /*[LINE "HYPOTHETICAL"]*/ "preventManualShrinkwrapChanges": true + /*[LINE "HYPOTHETICAL"]*/ "preventManualShrinkwrapChanges": true, + + /** + * If true, then `rush install` will use the PNPM workspaces feature to perform the + * install. + * + * This feature uses PNPM to peform the entire monorepo install. When using workspaces, Rush will + * generate a "pnpm-workspace.yaml" file referencing all local projects to install. Rush will + * also generate a "pnpmfile.js" which is used to provide preferred versions support. When install + * is run, this pnpmfile will be used to replace dependency version ranges with a smaller subset + * of the original range. If the preferred version is not fully a subset of the original version + * range, it will be left as-is. After this, the pnpmfile.js provided in the repository (if one + * exists) will be called to further modify package dependencies. + * + * The default value is false. + */ + /*[LINE "HYPOTHETICAL"]*/ "useWorkspaces": true }, /** diff --git a/apps/rush-lib/package.json b/apps/rush-lib/package.json index fa316d81b15..10d66f0e668 100644 --- a/apps/rush-lib/package.json +++ b/apps/rush-lib/package.json @@ -41,7 +41,7 @@ "npm-packlist": "~2.1.2", "read-package-tree": "~5.1.5", "resolve": "~1.17.0", - "semver": "~5.3.0", + "semver": "~7.3.0", "strict-uri-encode": "~2.0.0", "tar": "~5.0.5", "true-case-path": "~2.2.1", @@ -64,7 +64,7 @@ "@types/npm-packlist": "~1.1.1", "@types/read-package-tree": "5.1.0", "@types/resolve": "1.17.1", - "@types/semver": "5.3.33", + "@types/semver": "~7.3.1", "@types/strict-uri-encode": "2.0.0", "@types/tar": "4.0.3", "@types/wordwrap": "1.0.0", diff --git a/apps/rush-lib/src/api/CommonVersionsConfiguration.ts b/apps/rush-lib/src/api/CommonVersionsConfiguration.ts index 674db10321b..90514feacec 100644 --- a/apps/rush-lib/src/api/CommonVersionsConfiguration.ts +++ b/apps/rush-lib/src/api/CommonVersionsConfiguration.ts @@ -1,13 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import * as crypto from 'crypto'; import * as path from 'path'; import { JsonFile, JsonSchema, MapExtensions, ProtectableMap, - FileSystem + FileSystem, + Sort } from '@rushstack/node-core-library'; import { PackageNameParsers } from './PackageNameParsers'; import { JsonSchemaUrls } from '../logic/JsonSchemaUrls'; @@ -148,6 +150,23 @@ export class CommonVersionsConfiguration { return this._filePath; } + /** + * Get a sha1 hash of the preferred versions. + */ + public getPreferredVersionsHash(): string { + // Sort so that the hash is stable + const orderedPreferredVersions: Map = new Map( + this._preferredVersions.protectedView + ); + Sort.sortMapKeys(orderedPreferredVersions); + + // JSON.stringify does not support maps, so we need to convert to an object first + const preferredVersionsObj: { [dependency: string]: string } = MapExtensions.toObject( + orderedPreferredVersions + ); + return crypto.createHash('sha1').update(JSON.stringify(preferredVersionsObj)).digest('hex'); + } + /** * Writes the "common-versions.json" file to disk, using the filename that was passed to loadFromFile(). */ diff --git a/apps/rush-lib/src/api/PackageJsonEditor.ts b/apps/rush-lib/src/api/PackageJsonEditor.ts index 739b544d67b..a6a5c82e5fc 100644 --- a/apps/rush-lib/src/api/PackageJsonEditor.ts +++ b/apps/rush-lib/src/api/PackageJsonEditor.ts @@ -201,7 +201,13 @@ export class PackageJsonEditor { this._onChange.bind(this) ); - if (dependencyType === DependencyType.Regular || dependencyType === DependencyType.Optional) { + // Rush collapses everything that isn't a devDependency into the dependencies + // field, so we need to set the value dependening on dependency type + if ( + dependencyType === DependencyType.Regular || + dependencyType === DependencyType.Optional || + dependencyType === DependencyType.Peer + ) { this._dependencies.set(packageName, dependency); } else { this._devDependencies.set(packageName, dependency); diff --git a/apps/rush-lib/src/api/RushConfiguration.ts b/apps/rush-lib/src/api/RushConfiguration.ts index 2b4c85061e5..c0902c4c6c9 100644 --- a/apps/rush-lib/src/api/RushConfiguration.ts +++ b/apps/rush-lib/src/api/RushConfiguration.ts @@ -24,6 +24,7 @@ import { YarnPackageManager } from './packageManager/YarnPackageManager'; import { PnpmPackageManager } from './packageManager/PnpmPackageManager'; import { ExperimentsConfiguration } from './ExperimentsConfiguration'; import { PackageNameParsers } from './PackageNameParsers'; +import { RepoStateFile } from '../logic/RepoStateFile'; const MINIMUM_SUPPORTED_RUSH_JSON_VERSION: string = '0.0.0'; const DEFAULT_BRANCH: string = 'master'; @@ -38,6 +39,7 @@ const knownRushConfigFilenames: string[] = [ '.npmrc-publish', RushConstants.pinnedVersionsFilename, RushConstants.commonVersionsFilename, + RushConstants.repoStateFilename, RushConstants.browserApprovedPackagesFilename, RushConstants.nonbrowserApprovedPackagesFilename, RushConstants.versionPoliciesFilename, @@ -168,6 +170,10 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { * {@inheritDoc PnpmOptionsConfiguration.preventManualShrinkwrapChanges} */ preventManualShrinkwrapChanges?: boolean; + /** + * {@inheritDoc PnpmOptionsConfiguration.useWorkspaces} + */ + useWorkspaces?: boolean; } /** @@ -334,7 +340,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration * @remarks * This feature protects against accidental inconsistencies that may be introduced * if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited. When this - * feature is enabled, `rush update` will append a hash to the file as a YAML comment, + * feature is enabled, `rush update` will write a hash of the shrinkwrap contents to repo-state.json, * and then `rush update` and `rush install` will validate the hash. Note that this does not prohibit * manual modifications, but merely requires `rush update` be run * afterwards, ensuring that PNPM can report or repair any potential inconsistencies. @@ -346,6 +352,14 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration */ public readonly preventManualShrinkwrapChanges: boolean; + /** + * If true, then Rush will use the workspaces feature to install and link packages when invoking PNPM. + * + * @remarks + * The default value is false. (For now.) + */ + public readonly useWorkspaces: boolean; + /** @internal */ public constructor(json: IPnpmOptionsJson, commonTempFolder: string) { super(json); @@ -360,6 +374,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration this.strictPeerDependencies = !!json.strictPeerDependencies; this.resolutionStrategy = json.resolutionStrategy || 'fewer-dependencies'; this.preventManualShrinkwrapChanges = !!json.preventManualShrinkwrapChanges; + this.useWorkspaces = !!json.useWorkspaces; } } @@ -1425,6 +1440,28 @@ export class RushConfiguration { return CommonVersionsConfiguration.loadFromFile(commonVersionsFilename); } + /** + * Gets the path to the repo-state.json file for a specific variant. + * @param variant - The name of the current variant in use by the active command. + */ + public getRepoStateFilePath(variant?: string | undefined): string { + const repoStateFilename: string = path.join( + this.commonRushConfigFolder, + ...(variant ? [RushConstants.rushVariantsFolderName, variant] : []), + RushConstants.repoStateFilename + ); + return repoStateFilename; + } + + /** + * Gets the contents from the repo-state.json file for a specific variant. + * @param variant - The name of the current variant in use by the active command. + */ + public getRepoState(variant?: string | undefined): RepoStateFile { + const repoStateFilename: string = this.getRepoStateFilePath(variant); + return RepoStateFile.loadFromFile(repoStateFilename, variant); + } + /** * Gets the committed shrinkwrap file name for a specific variant. * @param variant - The name of the current variant in use by the active command. diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 32cd2e44af5..8ff51a78b54 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -10,6 +10,7 @@ import { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { BaseRushAction } from './BaseRushAction'; import { RushCommandLineParser } from '../RushCommandLineParser'; import { PackageJsonUpdater, SemVerStyle } from '../../logic/PackageJsonUpdater'; +import { DependencySpecifier } from '../../logic/DependencySpecifier'; export class AddAction extends BaseRushAction { private _allFlag: CommandLineFlagParameter; @@ -25,9 +26,9 @@ export class AddAction extends BaseRushAction { 'Adds a specified package as a dependency of the current project (as determined by the current working directory)' + ' and then runs "rush update". If no version is specified, a version will be automatically detected (typically' + ' either the latest version or a version that won\'t break the "ensureConsistentVersions" policy). If a version' + - ' range is specified, the latest version in the range will be used. The version will be automatically prepended' + - ' with a tilde, unless the "--exact" or "--caret" flags are used. The "--make-consistent" flag can be used to' + - ' update all packages with the dependency.' + ' range (or a workspace range) is specified, the latest version in the range will be used. The version will be' + + ' automatically prepended with a tilde, unless the "--exact" or "--caret" flags are used. The "--make-consistent"' + + ' flag can be used to update all packages with the dependency.' ]; super({ actionName: 'add', @@ -128,8 +129,11 @@ export class AddAction extends BaseRushAction { throw new Error(`The package name "${packageName}" is not valid.`); } - if (version && version !== 'latest' && !semver.validRange(version) && !semver.valid(version)) { - throw new Error(`The SemVer specifier "${version}" is not valid.`); + if (version && version !== 'latest') { + const specifier: DependencySpecifier = new DependencySpecifier(packageName, version); + if (!semver.validRange(specifier.versionSpecifier) && !semver.valid(specifier.versionSpecifier)) { + throw new Error(`The SemVer specifier "${version}" is not valid.`); + } } const updater: PackageJsonUpdater = new PackageJsonUpdater(this.rushConfiguration, this.rushGlobalFolder); diff --git a/apps/rush-lib/src/cli/actions/BaseInstallAction.ts b/apps/rush-lib/src/cli/actions/BaseInstallAction.ts index ec29a2db4df..3e0973bd274 100644 --- a/apps/rush-lib/src/cli/actions/BaseInstallAction.ts +++ b/apps/rush-lib/src/cli/actions/BaseInstallAction.ts @@ -12,7 +12,8 @@ import { import { BaseRushAction } from './BaseRushAction'; import { Event } from '../../api/EventHooks'; -import { InstallManager, IInstallManagerOptions } from '../../logic/InstallManager'; +import { BaseInstallManager, IInstallManagerOptions } from '../../logic/base/BaseInstallManager'; +import { InstallManagerFactory } from '../../logic/InstallManagerFactory'; import { PurgeManager } from '../../logic/PurgeManager'; import { SetupChecks } from '../../logic/SetupChecks'; import { StandardScriptUpdater } from '../../logic/StandardScriptUpdater'; @@ -117,7 +118,7 @@ export abstract class BaseInstallAction extends BaseRushAction { const installManagerOptions: IInstallManagerOptions = this.buildInstallOptions(); - const installManager: InstallManager = new InstallManager( + const installManager: BaseInstallManager = InstallManagerFactory.getInstallManager( this.rushConfiguration, this.rushGlobalFolder, purgeManager, diff --git a/apps/rush-lib/src/cli/actions/InstallAction.ts b/apps/rush-lib/src/cli/actions/InstallAction.ts index 9133fb9e2d6..2422d61a4e1 100644 --- a/apps/rush-lib/src/cli/actions/InstallAction.ts +++ b/apps/rush-lib/src/cli/actions/InstallAction.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import { BaseInstallAction } from './BaseInstallAction'; -import { IInstallManagerOptions } from '../../logic/InstallManager'; +import { IInstallManagerOptions } from '../../logic/base/BaseInstallManager'; import { RushCommandLineParser } from '../RushCommandLineParser'; export class InstallAction extends BaseInstallAction { diff --git a/apps/rush-lib/src/cli/actions/UpdateAction.ts b/apps/rush-lib/src/cli/actions/UpdateAction.ts index 48e05ad7e48..27d800c7262 100644 --- a/apps/rush-lib/src/cli/actions/UpdateAction.ts +++ b/apps/rush-lib/src/cli/actions/UpdateAction.ts @@ -4,7 +4,7 @@ import { CommandLineFlagParameter } from '@rushstack/ts-command-line'; import { BaseInstallAction } from './BaseInstallAction'; -import { IInstallManagerOptions } from '../../logic/InstallManager'; +import { IInstallManagerOptions } from '../../logic/base/BaseInstallManager'; import { RushCommandLineParser } from '../RushCommandLineParser'; export class UpdateAction extends BaseInstallAction { diff --git a/apps/rush-lib/src/cli/actions/VersionAction.ts b/apps/rush-lib/src/cli/actions/VersionAction.ts index 778afff94fa..4761e514c5d 100644 --- a/apps/rush-lib/src/cli/actions/VersionAction.ts +++ b/apps/rush-lib/src/cli/actions/VersionAction.ts @@ -154,10 +154,16 @@ export class VersionAction extends BaseRushAction { const newPolicyVersion: semver.SemVer = new semver.SemVer(policy.version); if (newPolicyVersion.prerelease.length) { // Update 1.5.0-alpha.10 to 1.5.0-beta.10 - newPolicyVersion.prerelease[0] = this._prereleaseIdentifier.value; + // For example, if we are parsing "1.5.0-alpha.10" then the newPolicyVersion.prerelease array + // would contain [ "alpha", 10 ], so we would replace "alpha" with "beta" + newPolicyVersion.prerelease = [ + this._prereleaseIdentifier.value, + ...newPolicyVersion.prerelease.slice(1) + ]; } else { // Update 1.5.0 to 1.5.0-beta - newPolicyVersion.prerelease.push(this._prereleaseIdentifier.value); + // Since there is no length, we can just set to a new array + newPolicyVersion.prerelease = [this._prereleaseIdentifier.value]; } newVersion = newPolicyVersion.format(); } diff --git a/apps/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts b/apps/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts index 7e9524d076f..b3753e3ca77 100644 --- a/apps/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts +++ b/apps/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts @@ -9,7 +9,7 @@ import { BaseScriptAction, IBaseScriptActionOptions } from './BaseScriptAction'; import { Utilities } from '../../utilities/Utilities'; import { AlreadyReportedError } from '../../utilities/AlreadyReportedError'; import { FileSystem, LockFile, IPackageJson, JsonFile, PackageName } from '@rushstack/node-core-library'; -import { InstallHelpers } from '../../logic/InstallHelpers'; +import { InstallHelpers } from '../../logic/installManager/InstallHelpers'; import { RushConstants } from '../../logic/RushConstants'; import { LastInstallFlag } from '../../api/LastInstallFlag'; @@ -138,14 +138,12 @@ export class GlobalScriptAction extends BaseScriptAction { console.log(`Installing dependencies under ${this._autoinstallerNameFullPath}...\n`); - Utilities.executeCommand( - this.rushConfiguration.packageManagerToolFilename, - ['install', '--frozen-lockfile'], - this._autoinstallerNameFullPath, - undefined, - /* suppressOutput */ false, - /* keepEnvironment */ true - ); + Utilities.executeCommand({ + command: this.rushConfiguration.packageManagerToolFilename, + args: ['install', '--frozen-lockfile'], + workingDirectory: this._autoinstallerNameFullPath, + keepEnvironment: true + }); // Create file: ../common/autoinstallers/my-task/.rush/temp/last-install.flag lastInstallFlag.create(); diff --git a/apps/rush-lib/src/cli/test/Cli.test.ts b/apps/rush-lib/src/cli/test/Cli.test.ts index b9577fc8440..e2372c8f4b3 100644 --- a/apps/rush-lib/src/cli/test/Cli.test.ts +++ b/apps/rush-lib/src/cli/test/Cli.test.ts @@ -11,7 +11,12 @@ describe('CLI', () => { const startPath: string = path.resolve(path.join(__dirname, '../../start.js')); expect(() => { - Utilities.executeCommand('node', [startPath], workingDir, undefined, true); + Utilities.executeCommand({ + command: 'node', + args: [startPath], + workingDirectory: workingDir, + suppressOutput: true + }); }).not.toThrow(); }); }); diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index eb03329eb02..ef240cd265f 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -64,11 +64,11 @@ Adds a specified package as a dependency of the current project (as determined by the current working directory) and then runs \\"rush update\\". If no version is specified, a version will be automatically detected (typically either the latest version or a version that won't break the -\\"ensureConsistentVersions\\" policy). If a version range is specified, the -latest version in the range will be used. The version will be automatically -prepended with a tilde, unless the \\"--exact\\" or \\"--caret\\" flags are used. The -\\"--make-consistent\\" flag can be used to update all packages with the -dependency. +\\"ensureConsistentVersions\\" policy). If a version range (or a workspace range) +is specified, the latest version in the range will be used. The version will +be automatically prepended with a tilde, unless the \\"--exact\\" or \\"--caret\\" +flags are used. The \\"--make-consistent\\" flag can be used to update all +packages with the dependency. Optional arguments: -h, --help Show this help message and exit. diff --git a/apps/rush-lib/src/index.ts b/apps/rush-lib/src/index.ts index 969686353d6..7a31829cc75 100644 --- a/apps/rush-lib/src/index.ts +++ b/apps/rush-lib/src/index.ts @@ -39,6 +39,8 @@ export { CommonVersionsConfiguration } from './api/CommonVersionsConfiguration'; export { PackageJsonEditor, PackageJsonDependency, DependencyType } from './api/PackageJsonEditor'; +export { RepoStateFile } from './logic/RepoStateFile'; + export { EventHooks, Event } from './api/EventHooks'; export { ChangeManager } from './api/ChangeManager'; diff --git a/apps/rush-lib/src/logic/DependencySpecifier.ts b/apps/rush-lib/src/logic/DependencySpecifier.ts index 51f653aad58..bdcf3d8a31f 100644 --- a/apps/rush-lib/src/logic/DependencySpecifier.ts +++ b/apps/rush-lib/src/logic/DependencySpecifier.ts @@ -4,6 +4,56 @@ import npmPackageArg = require('npm-package-arg'); import { InternalError } from '@rushstack/node-core-library'; +/** + * The parsed format of a provided version specifier. + */ +export enum DependencySpecifierType { + /** + * A git repository + */ + Git = 'Git', + + /** + * A tagged version, e.g. "example@latest" + */ + Tag = 'Tag', + + /** + * A specific version number, e.g. "example@1.2.3" + */ + Version = 'Version', + + /** + * A version range, e.g. "example@2.x" + */ + Range = 'Range', + + /** + * A local .tar.gz, .tar or .tgz file + */ + File = 'File', + + /** + * A local directory + */ + Directory = 'Directory', + + /** + * An HTTP url to a .tar.gz, .tar or .tgz file + */ + Remote = 'Remote', + + /** + * A package alias, e.g. "npm:other-package@^1.2.3" + */ + Alias = 'Alias', + + /** + * A package specified using workspace protocol, e.g. "workspace:^1.2.3" + */ + Workspace = 'Workspace' +} + /** * An NPM "version specifier" is a string that can appear as a package.json "dependencies" value. * Example version specifiers: `^1.2.3`, `file:./blah.tgz`, `npm:other-package@~1.2.3`, and so forth. @@ -22,18 +72,9 @@ export class DependencySpecifier { public readonly versionSpecifier: string; /** - * The type of `versionSpecifier`: - * - * git - a git repository - * tag - a tagged version, e.g. "example@latest" - * version - A specific version number, e.g. "example@1.2.3" - * range - A version range, e.g. "example@2.x" - * file - A local .tar.gz, .tar or .tgz file - * directory - A local directory - * remote - An HTTP url to a .tar.gz, .tar or .tgz file - * alias - A package alias such as "npm:other-package@^1.2.3" + * The type of the `versionSpecifier`. */ - public readonly specifierType: string; + public readonly specifierType: DependencySpecifierType; /** * If `specifierType` is `alias`, then this is the parsed target dependency. @@ -46,20 +87,49 @@ export class DependencySpecifier { this.packageName = packageName; this.versionSpecifier = versionSpecifier; - const result: npmPackageArg.AliasResult = npmPackageArg.resolve( - packageName, - versionSpecifier - ) as npmPackageArg.AliasResult; + // Workspace ranges are a feature from PNPM and Yarn. Set the version specifier + // to the trimmed version range. + if (versionSpecifier.startsWith('workspace:')) { + this.specifierType = DependencySpecifierType.Workspace; + this.versionSpecifier = versionSpecifier.slice(this.specifierType.length + 1).trim(); + this.aliasTarget = undefined; + return; + } - this.specifierType = result.type; + const result: npmPackageArg.Result = npmPackageArg.resolve(packageName, versionSpecifier); + this.specifierType = DependencySpecifier.getDependencySpecifierType(result.type); - if (result.type === 'alias') { - if (!result.subSpec || !result.subSpec.name) { + if (this.specifierType === DependencySpecifierType.Alias) { + const aliasResult: npmPackageArg.AliasResult = result as npmPackageArg.AliasResult; + if (!aliasResult.subSpec || !aliasResult.subSpec.name) { throw new InternalError('Unexpected result from npm-package-arg'); } - this.aliasTarget = new DependencySpecifier(result.subSpec.name, result.subSpec.rawSpec); + this.aliasTarget = new DependencySpecifier(aliasResult.subSpec.name, aliasResult.subSpec.rawSpec); } else { this.aliasTarget = undefined; } } + + public static getDependencySpecifierType(specifierType: string): DependencySpecifierType { + switch (specifierType) { + case 'git': + return DependencySpecifierType.Git; + case 'tag': + return DependencySpecifierType.Tag; + case 'version': + return DependencySpecifierType.Version; + case 'range': + return DependencySpecifierType.Range; + case 'file': + return DependencySpecifierType.File; + case 'directory': + return DependencySpecifierType.Directory; + case 'remote': + return DependencySpecifierType.Remote; + case 'alias': + return DependencySpecifierType.Alias; + default: + throw new InternalError(`Unexpected npm-package-arg result type "${specifierType}"`); + } + } } diff --git a/apps/rush-lib/src/logic/InstallHelpers.ts b/apps/rush-lib/src/logic/InstallHelpers.ts deleted file mode 100644 index f4382ac512a..00000000000 --- a/apps/rush-lib/src/logic/InstallHelpers.ts +++ /dev/null @@ -1,105 +0,0 @@ -// 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 os from 'os'; -import * as path from 'path'; -import { LockFile, FileSystem } from '@rushstack/node-core-library'; - -import { LastInstallFlag } from '../api/LastInstallFlag'; -import { Utilities } from '../utilities/Utilities'; -import { PackageManagerName } from '../api/packageManager/PackageManager'; -import { RushConfiguration } from '../api/RushConfiguration'; -import { RushGlobalFolder } from '../api/RushGlobalFolder'; - -export class InstallHelpers { - /** - * If the "(p)npm-local" symlink hasn't been set up yet, this creates it, installing the - * specified (P)npm version in the user's home directory if needed. - */ - public static async ensureLocalPackageManager( - rushConfiguration: RushConfiguration, - rushGlobalFolder: RushGlobalFolder, - maxInstallAttempts: number - ): Promise { - // Example: "C:\Users\YourName\.rush" - const rushUserFolder: string = rushGlobalFolder.nodeSpecificPath; - - if (!FileSystem.exists(rushUserFolder)) { - console.log('Creating ' + rushUserFolder); - FileSystem.ensureFolder(rushUserFolder); - } - - const packageManager: PackageManagerName = rushConfiguration.packageManager; - const packageManagerVersion: string = rushConfiguration.packageManagerToolVersion; - - const packageManagerAndVersion: string = `${packageManager}-${packageManagerVersion}`; - // Example: "C:\Users\YourName\.rush\pnpm-1.2.3" - const packageManagerToolFolder: string = path.join(rushUserFolder, packageManagerAndVersion); - - const packageManagerMarker: LastInstallFlag = new LastInstallFlag(packageManagerToolFolder, { - node: process.versions.node - }); - - console.log(`Trying to acquire lock for ${packageManagerAndVersion}`); - - const lock: LockFile = await LockFile.acquire(rushUserFolder, packageManagerAndVersion); - - console.log(`Acquired lock for ${packageManagerAndVersion}`); - - if (!packageManagerMarker.isValid() || lock.dirtyWhenAcquired) { - console.log(colors.bold(`Installing ${packageManager} version ${packageManagerVersion}${os.EOL}`)); - - // note that this will remove the last-install flag from the directory - Utilities.installPackageInDirectory({ - directory: packageManagerToolFolder, - packageName: packageManager, - version: rushConfiguration.packageManagerToolVersion, - tempPackageTitle: `${packageManager}-local-install`, - maxInstallAttempts: maxInstallAttempts, - // This is using a local configuration to install a package in a shared global location. - // Generally that's a bad practice, but in this case if we can successfully install - // the package at all, we can reasonably assume it's good for all the repositories. - // In particular, we'll assume that two different NPM registries cannot have two - // different implementations of the same version of the same package. - // This was needed for: https://github.com/microsoft/rushstack/issues/691 - commonRushConfigFolder: rushConfiguration.commonRushConfigFolder - }); - - console.log(`Successfully installed ${packageManager} version ${packageManagerVersion}`); - } else { - console.log(`Found ${packageManager} version ${packageManagerVersion} in ${packageManagerToolFolder}`); - } - - packageManagerMarker.create(); - - // Example: "C:\MyRepo\common\temp" - FileSystem.ensureFolder(rushConfiguration.commonTempFolder); - - // Example: "C:\MyRepo\common\temp\pnpm-local" - const localPackageManagerToolFolder: string = path.join( - rushConfiguration.commonTempFolder, - `${packageManager}-local` - ); - - console.log(os.EOL + 'Symlinking "' + localPackageManagerToolFolder + '"'); - console.log(' --> "' + packageManagerToolFolder + '"'); - - // We cannot use FileSystem.exists() to test the existence of a symlink, because it will - // return false for broken symlinks. There is no way to test without catching an exception. - try { - FileSystem.deleteFolder(localPackageManagerToolFolder); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - - FileSystem.createSymbolicLinkJunction({ - linkTargetPath: packageManagerToolFolder, - newLinkPath: localPackageManagerToolFolder - }); - - lock.release(); - } -} diff --git a/apps/rush-lib/src/logic/InstallManager.ts b/apps/rush-lib/src/logic/InstallManager.ts deleted file mode 100644 index 96f998966bf..00000000000 --- a/apps/rush-lib/src/logic/InstallManager.ts +++ /dev/null @@ -1,1498 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as glob from 'glob'; -import * as colors from 'colors'; -import * as fetch from 'node-fetch'; -import * as http from 'http'; -import HttpsProxyAgent = require('https-proxy-agent'); -import * as os from 'os'; -import * as path from 'path'; -import * as semver from 'semver'; -import * as tar from 'tar'; -import * as globEscape from 'glob-escape'; -import { - JsonFile, - Text, - IPackageJson, - MapExtensions, - FileSystem, - FileConstants, - Sort, - PosixModeBits, - JsonObject, - NewlineKind -} from '@rushstack/node-core-library'; - -import { ApprovedPackagesChecker } from '../logic/ApprovedPackagesChecker'; -import { AsyncRecycler } from '../utilities/AsyncRecycler'; -import { BaseLinkManager } from '../logic/base/BaseLinkManager'; -import { BaseShrinkwrapFile } from '../logic/base/BaseShrinkwrapFile'; -import { PolicyValidator } from '../logic/policy/PolicyValidator'; -import { IRushTempPackageJson } from '../logic/base/BasePackage'; -import { Git } from '../logic/Git'; -import { LastInstallFlag } from '../api/LastInstallFlag'; -import { LinkManagerFactory } from '../logic/LinkManagerFactory'; -import { PurgeManager } from './PurgeManager'; -import { RushConfiguration, ICurrentVariantJson, IConfigurationEnvironment } from '../api/RushConfiguration'; -import { RushConfigurationProject } from '../api/RushConfigurationProject'; -import { RushConstants } from '../logic/RushConstants'; -import { ShrinkwrapFileFactory } from '../logic/ShrinkwrapFileFactory'; -import { Stopwatch } from '../utilities/Stopwatch'; -import { Utilities } from '../utilities/Utilities'; -import { Rush } from '../api/Rush'; -import { PackageJsonEditor, DependencyType, PackageJsonDependency } from '../api/PackageJsonEditor'; -import { AlreadyReportedError } from '../utilities/AlreadyReportedError'; -import { CommonVersionsConfiguration } from '../api/CommonVersionsConfiguration'; -import { RushGlobalFolder } from '../api/RushGlobalFolder'; -import { PnpmPackageManager } from '../api/packageManager/PnpmPackageManager'; -import { DependencySpecifier } from './DependencySpecifier'; -import { EnvironmentConfiguration } from '../api/EnvironmentConfiguration'; -import { InstallHelpers } from './InstallHelpers'; - -// The PosixModeBits are intended to be used with bitwise operations. -/* eslint-disable no-bitwise */ - -/** - * The "noMtime" flag is new in tar@4.4.1 and not available yet for \@types/tar. - * As a temporary workaround, augment the type. - */ -declare module 'tar' { - // eslint-disable-next-line @typescript-eslint/naming-convention - export interface CreateOptions { - /** - * "Set to true to omit writing mtime values for entries. Note that this prevents using other - * mtime-based features like tar.update or the keepNewer option with the resulting tar archive." - */ - noMtime?: boolean; - } -} - -export interface IInstallManagerOptions { - /** - * Whether the global "--debug" flag was specified. - */ - debug: boolean; - /** - * Whether or not Rush will automatically update the shrinkwrap file. - * True for "rush update", false for "rush install". - */ - allowShrinkwrapUpdates: boolean; - /** - * Whether to skip policy checks. - */ - bypassPolicy: boolean; - /** - * Whether to skip linking, i.e. require "rush link" to be done manually later. - */ - noLink: boolean; - /** - * Whether to delete the shrinkwrap file before installation, i.e. so that all dependencies - * will be upgraded to the latest SemVer-compatible version. - */ - fullUpgrade: boolean; - /** - * Whether to force an update to the shrinkwrap file even if it appears to be unnecessary. - * Normally Rush uses heuristics to determine when "pnpm install" can be skipped, - * but sometimes the heuristics can be inaccurate due to external influences - * (pnpmfile.js script logic, registry changes, etc). - */ - recheckShrinkwrap: boolean; - - /** - * The value of the "--network-concurrency" command-line parameter, which - * is a diagnostic option used to troubleshoot network failures. - * - * Currently only supported for PNPM. - */ - networkConcurrency: number | undefined; - - /** - * Whether or not to collect verbose logs from the package manager. - * If specified when using PNPM, the logs will be in /common/temp/pnpm.log - */ - collectLogFile: boolean; - - /** - * The variant to consider when performing installations and validating shrinkwrap updates. - */ - variant?: string | undefined; - - /** - * Retry the install the specified number of times - */ - maxInstallAttempts: number; -} - -/** - * This class implements common logic between "rush install" and "rush update". - */ -export class InstallManager { - private _rushConfiguration: RushConfiguration; - private _rushGlobalFolder: RushGlobalFolder; - private _commonNodeModulesMarker: LastInstallFlag; - private _commonTempFolderRecycler: AsyncRecycler; - - private _options: IInstallManagerOptions; - - public constructor( - rushConfiguration: RushConfiguration, - rushGlobalFolder: RushGlobalFolder, - purgeManager: PurgeManager, - options: IInstallManagerOptions - ) { - this._rushConfiguration = rushConfiguration; - this._rushGlobalFolder = rushGlobalFolder; - this._commonTempFolderRecycler = purgeManager.commonTempFolderRecycler; - this._options = options; - - const lastInstallState: JsonObject = { - node: process.versions.node, - packageManager: rushConfiguration.packageManager, - packageManagerVersion: rushConfiguration.packageManagerToolVersion - }; - - if (lastInstallState.packageManager === 'pnpm') { - lastInstallState.storePath = rushConfiguration.pnpmOptions.pnpmStorePath; - } - - this._commonNodeModulesMarker = new LastInstallFlag( - this._rushConfiguration.commonTempFolder, - lastInstallState - ); - } - - /** - * Returns a map of all direct dependencies that only have a single semantic version specifier. - * Returns a map: dependency name --> version specifier - */ - public static collectImplicitlyPreferredVersions( - rushConfiguration: RushConfiguration, - options: { - variant?: string | undefined; - } = {} - ): Map { - // First, collect all the direct dependencies of all local projects, and their versions: - // direct dependency name --> set of version specifiers - const versionsForDependencies: Map> = new Map>(); - - rushConfiguration.projects.forEach((project: RushConfigurationProject) => { - InstallManager._collectVersionsForDependencies(rushConfiguration, { - versionsForDependencies, - dependencies: project.packageJsonEditor.dependencyList, - cyclicDependencies: project.cyclicDependencyProjects, - variant: options.variant - }); - - InstallManager._collectVersionsForDependencies(rushConfiguration, { - versionsForDependencies, - dependencies: project.packageJsonEditor.devDependencyList, - cyclicDependencies: project.cyclicDependencyProjects, - variant: options.variant - }); - }); - - // If any dependency has more than one version, then filter it out (since we don't know which version - // should be preferred). What remains will be the list of preferred dependencies. - // dependency --> version specifier - const implicitlyPreferred: Map = new Map(); - versionsForDependencies.forEach((versions: Set, dep: string) => { - if (versions.size === 1) { - const version: string = versions.values().next().value; - implicitlyPreferred.set(dep, version); - } - }); - return implicitlyPreferred; - } - - // Helper for collectImplicitlyPreferredVersions() - private static _updateVersionsForDependencies( - versionsForDependencies: Map>, - dependency: string, - version: string - ): void { - if (!versionsForDependencies.has(dependency)) { - versionsForDependencies.set(dependency, new Set()); - } - versionsForDependencies.get(dependency)!.add(version); - } - - // Helper for collectImplicitlyPreferredVersions() - private static _collectVersionsForDependencies( - rushConfiguration: RushConfiguration, - options: { - versionsForDependencies: Map>; - dependencies: ReadonlyArray; - cyclicDependencies: Set; - variant: string | undefined; - } - ): void { - const { variant, dependencies, versionsForDependencies, cyclicDependencies } = options; - - const commonVersions: CommonVersionsConfiguration = rushConfiguration.getCommonVersions(variant); - - const allowedAlternativeVersions: Map> = - commonVersions.allowedAlternativeVersions; - - for (const dependency of dependencies) { - const alternativesForThisDependency: ReadonlyArray = - allowedAlternativeVersions.get(dependency.name) || []; - - // For each dependency, collectImplicitlyPreferredVersions() is collecting the set of all version specifiers - // that appear across the repo. If there is only one version specifier, then that's the "preferred" one. - // However, there are a few cases where additional version specifiers can be safely ignored. - let ignoreVersion: boolean = false; - - // 1. If the version specifier was listed in "allowedAlternativeVersions", then it's never a candidate. - // (Even if it's the only version specifier anywhere in the repo, we still ignore it, because - // otherwise the rule would be difficult to explain.) - if (alternativesForThisDependency.indexOf(dependency.version) > 0) { - ignoreVersion = true; - } else { - // Is it a local project? - const localProject: RushConfigurationProject | undefined = rushConfiguration.getProjectByName( - dependency.name - ); - if (localProject) { - // 2. If it's a symlinked local project, then it's not a candidate, because the package manager will - // never even see it. - // However there are two ways that a local project can NOT be symlinked: - // - if the local project doesn't satisfy the referenced semver specifier; OR - // - if the local project was specified in "cyclicDependencyProjects" in rush.json - if ( - semver.satisfies(localProject.packageJsonEditor.version, dependency.version) && - !cyclicDependencies.has(dependency.name) - ) { - ignoreVersion = true; - } - } - - if (!ignoreVersion) { - InstallManager._updateVersionsForDependencies( - versionsForDependencies, - dependency.name, - dependency.version - ); - } - } - } - } - - public get commonNodeModulesMarker(): LastInstallFlag { - return this._commonNodeModulesMarker; - } - - public async doInstall(): Promise { - const options: IInstallManagerOptions = this._options; - - // Check the policies - PolicyValidator.validatePolicy(this._rushConfiguration, options); - - // Git hooks are only installed if the repo opts in by including files in /common/git-hooks - const hookSource: string = path.join(this._rushConfiguration.commonFolder, 'git-hooks'); - const hookDestination: string | undefined = Git.getHooksFolder(); - - if (FileSystem.exists(hookSource) && hookDestination) { - const allHookFilenames: string[] = FileSystem.readFolder(hookSource); - // Ignore the ".sample" file(s) in this folder. - const hookFilenames: string[] = allHookFilenames.filter((x) => !/\.sample$/.test(x)); - if (hookFilenames.length > 0) { - console.log(os.EOL + colors.bold('Found files in the "common/git-hooks" folder.')); - - // Clear the currently installed git hooks and install fresh copies - FileSystem.ensureEmptyFolder(hookDestination); - - // Only copy files that look like Git hook names - const filteredHookFilenames: string[] = hookFilenames.filter((x) => /^[a-z\-]+/.test(x)); - for (const filename of filteredHookFilenames) { - // Copy the file. Important: For Bash scripts, the EOL must not be CRLF. - const hookFileContent: string = FileSystem.readFile(path.join(hookSource, filename)); - FileSystem.writeFile(path.join(hookDestination, filename), hookFileContent, { - convertLineEndings: NewlineKind.Lf - }); - - FileSystem.changePosixModeBits( - path.join(hookDestination, filename), - PosixModeBits.UserRead | PosixModeBits.UserExecute - ); - } - - console.log( - 'Successfully installed these Git hook scripts: ' + filteredHookFilenames.join(', ') + os.EOL - ); - } - } - - const approvedPackagesChecker: ApprovedPackagesChecker = new ApprovedPackagesChecker( - this._rushConfiguration - ); - if (approvedPackagesChecker.approvedPackagesFilesAreOutOfDate) { - if (this._options.allowShrinkwrapUpdates) { - approvedPackagesChecker.rewriteConfigFiles(); - console.log( - colors.yellow( - 'Approved package files have been updated. These updates should be committed to source control' - ) - ); - } else { - throw new Error(`Approved packages files are out-of date. Run "rush update" to update them.`); - } - } - - // Ensure that the package manager is installed - await InstallHelpers.ensureLocalPackageManager( - this._rushConfiguration, - this._rushGlobalFolder, - this._options.maxInstallAttempts - ); - - let shrinkwrapFile: BaseShrinkwrapFile | undefined = undefined; - - // (If it's a full update, then we ignore the shrinkwrap from Git since it will be overwritten) - if (!options.fullUpgrade) { - try { - shrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile( - this._rushConfiguration.packageManager, - this._rushConfiguration.packageManagerOptions, - this._rushConfiguration.getCommittedShrinkwrapFilename(options.variant) - ); - } catch (ex) { - console.log(); - console.log(`Unable to load the ${this._shrinkwrapFilePhrase}: ${ex.message}`); - - if (!options.allowShrinkwrapUpdates) { - console.log(); - console.log(colors.red('You need to run "rush update" to fix this problem')); - throw new AlreadyReportedError(); - } - - shrinkwrapFile = undefined; - } - } - - // Write a file indicating which variant is being installed. - // This will be used by bulk scripts to determine the correct Shrinkwrap file to track. - const currentVariantJsonFilename: string = this._rushConfiguration.currentVariantJsonFilename; - const currentVariantJson: ICurrentVariantJson = { - variant: options.variant || null // eslint-disable-line @rushstack/no-null - }; - - // Determine if the variant is already current by updating current-variant.json. - // If nothing is written, the variant has not changed. - const variantIsUpToDate: boolean = !JsonFile.save(currentVariantJson, currentVariantJsonFilename, { - onlyIfChanged: true - }); - - if (options.variant) { - console.log(); - console.log(colors.bold(`Using variant '${options.variant}' for installation.`)); - } else if (!variantIsUpToDate && !options.variant) { - console.log(); - console.log(colors.bold('Using the default variant for installation.')); - } - - const shrinkwrapIsUpToDate: boolean = - this._createTempModulesAndCheckShrinkwrap({ - shrinkwrapFile, - variant: options.variant - }) && !options.recheckShrinkwrap; - - if (!shrinkwrapIsUpToDate) { - if (!options.allowShrinkwrapUpdates) { - console.log(); - console.log( - colors.red(`The ${this._shrinkwrapFilePhrase} is out of date. You need to run "rush update".`) - ); - throw new AlreadyReportedError(); - } - } - - await this._installCommonModules({ - shrinkwrapIsUpToDate, - variantIsUpToDate, - ...options - }); - - if (!options.noLink) { - const linkManager: BaseLinkManager = LinkManagerFactory.getLinkManager(this._rushConfiguration); - await linkManager.createSymlinksForProjects(false); - } else { - console.log( - os.EOL + colors.yellow('Since "--no-link" was specified, you will need to run "rush link" manually.') - ); - } - } - - /** - * Regenerates the common/package.json and all temp_modules projects. - * 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. - */ - private _createTempModulesAndCheckShrinkwrap(options: { - shrinkwrapFile: BaseShrinkwrapFile | undefined; - variant: string | undefined; - }): boolean { - const { shrinkwrapFile, variant } = options; - - 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; - } - - // dependency name --> version specifier - const allExplicitPreferredVersions: Map = this._rushConfiguration - .getCommonVersions(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( - `"${dependency}" (${version}) required by the preferred versions from ` + - RushConstants.commonVersionsFilename - ); - shrinkwrapIsUpToDate = false; - } - }); - - if (this._findOrphanedTempProjects(shrinkwrapFile)) { - // If there are any orphaned projects, then "npm install" would fail because the shrinkwrap - // contains references such as "resolved": "file:projects\\project1" that refer to nonexistent - // file paths. - shrinkwrapIsUpToDate = false; - } - } - - // Also copy down the committed .npmrc file, if there is one - // "common\config\rush\.npmrc" --> "common\temp\.npmrc" - // Also ensure that we remove any old one that may be hanging around - Utilities.syncNpmrc( - this._rushConfiguration.commonRushConfigFolder, - this._rushConfiguration.commonTempFolder - ); - - // also, copy the pnpmfile.js if it exists - if (this._rushConfiguration.packageManager === 'pnpm') { - const committedPnpmFilePath: string = this._rushConfiguration.getPnpmfilePath(this._options.variant); - const tempPnpmFilePath: string = path.join( - this._rushConfiguration.commonTempFolder, - RushConstants.pnpmfileFilename - ); - - // ensure that we remove any old one that may be hanging around - this._syncFile(committedPnpmFilePath, tempPnpmFilePath); - } - - const commonPackageJson: IPackageJson = { - dependencies: {}, - description: 'Temporary file generated by the Rush tool', - name: 'rush-common', - private: true, - version: '0.0.0' - }; - - // dependency name --> version specifier - const allPreferredVersions: Map = new Map(); - - // Should we add implicitly preferred versions? - let useImplicitlyPinnedVersions: boolean; - if (this._rushConfiguration.commonVersions.implicitlyPreferredVersions !== undefined) { - // Use the manually configured setting - useImplicitlyPinnedVersions = this._rushConfiguration.commonVersions.implicitlyPreferredVersions; - } else { - // Default to true. - useImplicitlyPinnedVersions = true; - } - - if (useImplicitlyPinnedVersions) { - // Add in the implicitly preferred versions. - // These are any first-level dependencies for which we only consume a single version range - // (e.g. every package that depends on react uses an identical specifier) - const implicitlyPreferredVersions: Map< - string, - string - > = InstallManager.collectImplicitlyPreferredVersions(this._rushConfiguration, { variant }); - MapExtensions.mergeFromMap(allPreferredVersions, implicitlyPreferredVersions); - } - - // Add in the explicitly preferred versions. - // Note that these take precedence over implicitly preferred versions. - MapExtensions.mergeFromMap(allPreferredVersions, allExplicitPreferredVersions); - - // Add any preferred versions to the top of the commonPackageJson - // do this in alphabetical order for simpler debugging - for (const dependency of Array.from(allPreferredVersions.keys()).sort()) { - commonPackageJson.dependencies![dependency] = allPreferredVersions.get(dependency)!; - } - - // 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._getTarballFilePath(rushProject); - - // Example: dependencies["@rush-temp/my-project-2"] = "file:./projects/my-project-2.tgz" - commonPackageJson.dependencies![ - 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 = new Map(); - - // 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 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 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 "rush link" 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; - - let tryReusingPackageVersionsFromShrinkwrap: boolean = true; - - if (this._rushConfiguration.packageManager === 'pnpm') { - // Shrinkwrap churn optimization doesn't make sense when --frozen-lockfile is true - tryReusingPackageVersionsFromShrinkwrap = !this._rushConfiguration.experimentsConfiguration - .configuration.usePnpmFrozenLockfileForRushInstall; - } - - if (shrinkwrapFile) { - if ( - !shrinkwrapFile.tryEnsureCompatibleDependency( - dependencySpecifier, - rushProject.tempProjectName, - tryReusingPackageVersionsFromShrinkwrap - ) - ) { - shrinkwrapWarnings.push( - `"${packageName}" (${packageVersion}) required by "${rushProject.packageName}"` - ); - shrinkwrapIsUpToDate = false; - } - } - } - - // Example: "C:\MyRepo\common\temp\projects\my-project-2" - const tempProjectFolder: string = this._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._createTempProjectTarball(rushProject); - - console.log(`Updating ${tarballFile}`); - } catch (error) { - console.log(colors.yellow(error)); - // delete everything in case of any error - FileSystem.deleteFile(tarballFile); - FileSystem.deleteFile(tempPackageJsonFilename); - } - } - } - - // Example: "C:\MyRepo\common\temp\package.json" - const commonPackageJsonFilename: string = path.join( - this._rushConfiguration.commonTempFolder, - FileConstants.PackageJson - ); - - if (shrinkwrapFile) { - // If we have a (possibly incomplete) shrinkwrap file, check to see if any shrinkwrap-specific - // changes make the shrinkwrap out-of-date, and save it as the temporary file. - if (shrinkwrapFile.shouldForceRecheck()) { - shrinkwrapIsUpToDate = false; - } - - shrinkwrapFile.save(this._rushConfiguration.tempShrinkwrapFilename); - shrinkwrapFile.save(this._rushConfiguration.tempShrinkwrapPreinstallFilename); - } else { - // Otherwise delete the temporary file - FileSystem.deleteFile(this._rushConfiguration.tempShrinkwrapFilename); - - if (this._rushConfiguration.packageManager === 'pnpm') { - // Workaround for https://github.com/pnpm/pnpm/issues/1890 - // - // When "rush update --full" is run, rush deletes common/temp/pnpm-lock.yaml so that - // a new lockfile can be generated. But because of the above bug "pnpm install" would - // respect "common/temp/node_modules/.pnpm-lock.yaml" and thus would not generate a - // new lockfile. Deleting this file in addition to deleting common/temp/pnpm-lock.yaml - // ensures that a new lockfile will be generated with "rush update --full". - - const pnpmPackageManager: PnpmPackageManager = this._rushConfiguration - .packageManagerWrapper as PnpmPackageManager; - - FileSystem.deleteFile( - path.join( - this._rushConfiguration.commonTempFolder, - pnpmPackageManager.internalShrinkwrapRelativePath - ) - ); - } - } - - // Don't update the file timestamp unless the content has changed, since "rush install" - // will consider this timestamp - JsonFile.save(commonPackageJson, commonPackageJsonFilename, { onlyIfChanged: true }); - - stopwatch.stop(); - console.log(`Finished creating temporary modules (${stopwatch.toString()})`); - - if (shrinkwrapWarnings.length > 0) { - console.log(); - console.log( - colors.yellow( - Utilities.wrapWords(`The ${this._shrinkwrapFilePhrase} is missing the following dependencies:`) - ) - ); - - for (const shrinkwrapWarning of shrinkwrapWarnings) { - console.log(colors.yellow(' ' + shrinkwrapWarning)); - } - console.log(); - } - - return shrinkwrapIsUpToDate; - } - - private _getTempProjectFolder(rushProject: RushConfigurationProject): string { - const unscopedTempProjectName: string = rushProject.unscopedTempProjectName; - return path.join( - this._rushConfiguration.commonTempFolder, - RushConstants.rushTempProjectsFolderName, - unscopedTempProjectName - ); - } - - /** - * Deletes the existing tarball and creates a tarball for the given rush project - */ - private _createTempProjectTarball(rushProject: RushConfigurationProject): void { - const tarballFile: string = this._getTarballFilePath(rushProject); - const tempProjectFolder: string = this._getTempProjectFolder(rushProject); - - FileSystem.deleteFile(tarballFile); - - // NPM expects the root of the tarball to have a directory called 'package' - const npmPackageFolder: string = 'package'; - - const tarOptions: tar.CreateOptions = { - gzip: true, - file: tarballFile, - cwd: tempProjectFolder, - portable: true, - noMtime: true, - noPax: true, - sync: true, - prefix: npmPackageFolder, - filter: (path: string, stat: tar.FileStat): boolean => { - if ( - !this._rushConfiguration.experimentsConfiguration.configuration.noChmodFieldInTarHeaderNormalization - ) { - stat.mode = - (stat.mode & ~0x1ff) | PosixModeBits.AllRead | PosixModeBits.UserWrite | PosixModeBits.AllExecute; - } - return true; - } - } as tar.CreateOptions; - // create the new tarball - tar.create(tarOptions, [FileConstants.PackageJson]); - } - - /** - * Runs "npm/pnpm/yarn install" in the "common/temp" folder. - */ - private _installCommonModules( - options: { - shrinkwrapIsUpToDate: boolean; - variantIsUpToDate: boolean; - } & IInstallManagerOptions - ): Promise { - const { shrinkwrapIsUpToDate, variantIsUpToDate } = options; - - const usePnpmFrozenLockfile: boolean = - this._rushConfiguration.packageManager === 'pnpm' && - this._rushConfiguration.experimentsConfiguration.configuration.usePnpmFrozenLockfileForRushInstall === - true; - return Promise.resolve().then(() => { - console.log( - os.EOL + colors.bold('Checking node_modules in ' + this._rushConfiguration.commonTempFolder) + os.EOL - ); - - const commonNodeModulesFolder: string = path.join( - this._rushConfiguration.commonTempFolder, - 'node_modules' - ); - - // This marker file indicates that the last "rush install" completed successfully - const markerFileExistedAndWasValidAtStart: boolean = this._commonNodeModulesMarker.checkValidAndReportStoreIssues(); - - // If "--clean" or "--full-clean" was specified, or if the last install was interrupted, - // then we will need to delete the node_modules folder. Otherwise, we can do an incremental - // install. - const deleteNodeModules: boolean = !markerFileExistedAndWasValidAtStart; - - // Based on timestamps, can we skip this install entirely? - if (shrinkwrapIsUpToDate && !deleteNodeModules && variantIsUpToDate) { - const potentiallyChangedFiles: string[] = []; - - // Consider the timestamp on the node_modules folder; if someone tampered with it - // or deleted it entirely, then we can't skip this install - potentiallyChangedFiles.push(commonNodeModulesFolder); - - // Additionally, if they pulled an updated npm-shrinkwrap.json file from Git, - // then we can't skip this install - potentiallyChangedFiles.push(this._rushConfiguration.getCommittedShrinkwrapFilename(options.variant)); - - // Add common-versions.json file to the potentially changed files list. - potentiallyChangedFiles.push(this._rushConfiguration.getCommonVersionsFilePath(options.variant)); - - if (this._rushConfiguration.packageManager === 'pnpm') { - // If the repo is using pnpmfile.js, consider that also - const pnpmFileFilename: string = this._rushConfiguration.getPnpmfilePath(options.variant); - - if (FileSystem.exists(pnpmFileFilename)) { - potentiallyChangedFiles.push(pnpmFileFilename); - } - } - - // Also consider timestamps for all the temp tarballs. (createTempModulesAndCheckShrinkwrap() will - // carefully preserve these timestamps unless something has changed.) - // Example: "C:\MyRepo\common\temp\projects\my-project-2.tgz" - potentiallyChangedFiles.push( - ...this._rushConfiguration.projects.map((x) => { - return this._getTarballFilePath(x); - }) - ); - - // NOTE: If commonNodeModulesMarkerFilename (or any of the potentiallyChangedFiles) does not - // exist, then isFileTimestampCurrent() returns false. - if (Utilities.isFileTimestampCurrent(this._commonNodeModulesMarker.path, potentiallyChangedFiles)) { - // Nothing to do, because everything is up to date according to time stamps - return; - } - } - - // Since we are actually running npm/pnpm/yarn install, recreate all the temp project tarballs. - // This ensures that any existing tarballs with older header bits will be regenerated. - // It is safe to assume that temp project pacakge.jsons already exist. - for (const rushProject of this._rushConfiguration.projects) { - this._createTempProjectTarball(rushProject); - } - - return this._checkIfReleaseIsPublished() - .catch((error) => { - // If the user is working in an environment that can't reach the registry, - // don't bother them with errors. - return undefined; - }) - .then((publishedRelease: boolean | undefined) => { - if (publishedRelease === false) { - console.log( - colors.yellow('Warning: This release of the Rush tool was unpublished; it may be unstable.') - ); - } - - // Since we're going to be tampering with common/node_modules, delete the "rush link" flag file if it exists; - // this ensures that a full "rush link" is required next time - Utilities.deleteFile(this._rushConfiguration.rushLinkJsonFilename); - - // Delete the successful install file to indicate the install transaction has started - this._commonNodeModulesMarker.clear(); - - // NOTE: The PNPM store is supposed to be transactionally safe, so we don't delete it automatically. - // The user must request that via the command line. - if (deleteNodeModules) { - if (this._rushConfiguration.packageManager === 'npm') { - console.log(`Deleting the "npm-cache" folder`); - // This is faster and more thorough than "npm cache clean" - this._commonTempFolderRecycler.moveFolder(this._rushConfiguration.npmCacheFolder); - - console.log(`Deleting the "npm-tmp" folder`); - this._commonTempFolderRecycler.moveFolder(this._rushConfiguration.npmTmpFolder); - } - } - - // Example: "C:\MyRepo\common\temp\npm-local\node_modules\.bin\npm" - const packageManagerFilename: string = this._rushConfiguration.packageManagerToolFilename; - - let packageManagerEnv: NodeJS.ProcessEnv = process.env; - - let configurationEnvironment: IConfigurationEnvironment | undefined = undefined; - - if (this._rushConfiguration.packageManager === 'npm') { - if ( - this._rushConfiguration.npmOptions && - this._rushConfiguration.npmOptions.environmentVariables - ) { - configurationEnvironment = this._rushConfiguration.npmOptions.environmentVariables; - } - } else if (this._rushConfiguration.packageManager === 'pnpm') { - if ( - this._rushConfiguration.pnpmOptions && - this._rushConfiguration.pnpmOptions.environmentVariables - ) { - configurationEnvironment = this._rushConfiguration.pnpmOptions.environmentVariables; - } - } else if (this._rushConfiguration.packageManager === 'yarn') { - if ( - this._rushConfiguration.yarnOptions && - this._rushConfiguration.yarnOptions.environmentVariables - ) { - configurationEnvironment = this._rushConfiguration.yarnOptions.environmentVariables; - } - } - - packageManagerEnv = this._mergeEnvironmentVariables(process.env, configurationEnvironment); - - // Is there an existing "node_modules" folder to consider? - if (FileSystem.exists(commonNodeModulesFolder)) { - // Should we delete the entire "node_modules" folder? - if (deleteNodeModules) { - // YES: Delete "node_modules" - - // Explain to the user why we are hosing their node_modules folder - console.log('Deleting files from ' + commonNodeModulesFolder); - - this._commonTempFolderRecycler.moveFolder(commonNodeModulesFolder); - - Utilities.createFolderWithRetry(commonNodeModulesFolder); - } else { - // NO: Prepare to do an incremental install in the "node_modules" folder - - // note: it is not necessary to run "prune" with pnpm - if (this._rushConfiguration.packageManager === 'npm') { - console.log( - `Running "${this._rushConfiguration.packageManager} prune"` + - ` in ${this._rushConfiguration.commonTempFolder}` - ); - const args: string[] = ['prune']; - this._pushConfigurationArgs(args, options); - - Utilities.executeCommandWithRetry( - this._options.maxInstallAttempts, - packageManagerFilename, - args, - this._rushConfiguration.commonTempFolder, - packageManagerEnv - ); - - // Delete the (installed image of) the temp projects, since "npm install" does not - // detect changes for "file:./" references. - // We recognize the temp projects by their names, which always start with "rush-". - - // Example: "C:\MyRepo\common\temp\node_modules\@rush-temp" - const pathToDeleteWithoutStar: string = path.join( - commonNodeModulesFolder, - RushConstants.rushTempNpmScope - ); - console.log(`Deleting ${pathToDeleteWithoutStar}\\*`); - // Glob can't handle Windows paths - const normalizedpathToDeleteWithoutStar: string = Text.replaceAll( - pathToDeleteWithoutStar, - '\\', - '/' - ); - - // Example: "C:/MyRepo/common/temp/node_modules/@rush-temp/*" - for (const tempModulePath of glob.sync( - globEscape(normalizedpathToDeleteWithoutStar) + '/*' - )) { - // We could potentially use AsyncRecycler here, but in practice these folders tend - // to be very small - Utilities.dangerouslyDeletePath(tempModulePath); - } - } - } - } - - if (this._rushConfiguration.packageManager === 'yarn') { - // Yarn does not correctly detect changes to a tarball, so we need to forcibly clear its cache - const yarnRushTempCacheFolder: string = path.join( - this._rushConfiguration.yarnCacheFolder, - 'v2', - 'npm-@rush-temp' - ); - if (FileSystem.exists(yarnRushTempCacheFolder)) { - console.log('Deleting ' + yarnRushTempCacheFolder); - Utilities.dangerouslyDeletePath(yarnRushTempCacheFolder); - } - } - - // Run "npm install" in the common folder - const installArgs: string[] = ['install']; - this._pushConfigurationArgs(installArgs, options); - - console.log( - os.EOL + - colors.bold( - `Running "${this._rushConfiguration.packageManager} install" in` + - ` ${this._rushConfiguration.commonTempFolder}` - ) + - os.EOL - ); - - // If any diagnostic options were specified, then show the full command-line - if (options.debug || options.collectLogFile || options.networkConcurrency) { - console.log( - os.EOL + - colors.green('Invoking package manager: ') + - FileSystem.getRealPath(packageManagerFilename) + - ' ' + - installArgs.join(' ') + - os.EOL - ); - } - - try { - Utilities.executeCommandWithRetry( - this._options.maxInstallAttempts, - packageManagerFilename, - installArgs, - this._rushConfiguration.commonTempFolder, - packageManagerEnv, - false, - () => { - if (this._rushConfiguration.packageManager === 'pnpm') { - console.log(colors.yellow(`Deleting the "node_modules" folder`)); - this._commonTempFolderRecycler.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.createFolderWithRetry(commonNodeModulesFolder); - } - } - ); - } catch (error) { - // All the install attempts failed. - - if ( - this._rushConfiguration.packageManager === 'pnpm' && - this._rushConfiguration.pnpmOptions.pnpmStore === 'local' - ) { - // If the installation has failed even after the retries, then pnpm store may - // have got into a corrupted, irrecoverable state. Delete the store so that a - // future install can create the store afresh. - console.log(colors.yellow(`Deleting the "pnpm-store" folder`)); - this._commonTempFolderRecycler.moveFolder(this._rushConfiguration.pnpmOptions.pnpmStorePath); - } - - throw error; - } - - if (this._rushConfiguration.packageManager === 'npm') { - console.log(os.EOL + colors.bold('Running "npm shrinkwrap"...')); - const npmArgs: string[] = ['shrinkwrap']; - this._pushConfigurationArgs(npmArgs, options); - Utilities.executeCommand( - this._rushConfiguration.packageManagerToolFilename, - npmArgs, - this._rushConfiguration.commonTempFolder - ); - console.log('"npm shrinkwrap" completed' + os.EOL); - - this._fixupNpm5Regression(); - } - - if (options.allowShrinkwrapUpdates && (usePnpmFrozenLockfile || !shrinkwrapIsUpToDate)) { - // Shrinkwrap files may need to be post processed after install, so load and save it - const tempShrinkwrapFile: - | BaseShrinkwrapFile - | undefined = ShrinkwrapFileFactory.getShrinkwrapFile( - this._rushConfiguration.packageManager, - this._rushConfiguration.packageManagerOptions, - this._rushConfiguration.tempShrinkwrapFilename - ); - if (tempShrinkwrapFile) { - tempShrinkwrapFile.save(this._rushConfiguration.tempShrinkwrapFilename); - } - - // Copy (or delete) common\temp\pnpm-lock.yaml --> common\config\rush\pnpm-lock.yaml - this._syncFile( - this._rushConfiguration.tempShrinkwrapFilename, - this._rushConfiguration.getCommittedShrinkwrapFilename(options.variant) - ); - } else { - // TODO: Validate whether the package manager updated it in a nontrivial way - } - - // Finally, create the marker file to indicate a successful install - this._commonNodeModulesMarker.create(); - - console.log(''); - }); - }); - } - - private _mergeEnvironmentVariables( - baseEnv: NodeJS.ProcessEnv, - environmentVariables?: IConfigurationEnvironment - ): NodeJS.ProcessEnv { - const packageManagerEnv: NodeJS.ProcessEnv = baseEnv; - - if (environmentVariables) { - // eslint-disable-next-line guard-for-in - for (const envVar in environmentVariables) { - let setEnvironmentVariable: boolean = true; - console.log(`\nProcessing definition for environment variable: ${envVar}`); - - if (baseEnv.hasOwnProperty(envVar)) { - setEnvironmentVariable = false; - console.log(`Environment variable already defined:`); - console.log(` Name: ${envVar}`); - console.log(` Existing value: ${baseEnv[envVar]}`); - console.log(` Value set in rush.json: ${environmentVariables[envVar].value}`); - - if (environmentVariables[envVar].override) { - setEnvironmentVariable = true; - console.log(`Overriding the environment variable with the value set in rush.json.`); - } else { - console.log(colors.yellow(`WARNING: Not overriding the value of the environment variable.`)); - } - } - - if (setEnvironmentVariable) { - if (this._options.debug) { - console.log(`Setting environment variable for package manager.`); - console.log(` Name: ${envVar}`); - console.log(` Value: ${environmentVariables[envVar].value}`); - } - packageManagerEnv[envVar] = environmentVariables[envVar].value; - } - } - } - - return packageManagerEnv; - } - - private _checkIfReleaseIsPublished(): Promise { - return Promise.resolve().then(() => { - const lastCheckFile: string = path.join( - this._rushGlobalFolder.nodeSpecificPath, - 'rush-' + Rush.version, - 'last-check.flag' - ); - - if (FileSystem.exists(lastCheckFile)) { - let cachedResult: boolean | 'error' | undefined = undefined; - try { - // NOTE: mtimeMs is not supported yet in Node.js 6.x - const nowMs: number = new Date().getTime(); - const ageMs: number = nowMs - FileSystem.getStatistics(lastCheckFile).mtime.getTime(); - const HOUR: number = 60 * 60 * 1000; - - // Is the cache too old? - if (ageMs < 24 * HOUR) { - // No, read the cached result - cachedResult = JsonFile.load(lastCheckFile); - } - } catch (e) { - // Unable to parse file - } - if (cachedResult === 'error') { - return Promise.reject(new Error('Unable to contact server')); - } - if (cachedResult === true || cachedResult === false) { - return cachedResult; - } - } - - // Before we start the network operation, record a failed state. If the process exits for some reason, - // this will record the error. It will also update the timestamp to prevent other Rush instances - // from attempting to update the file. - JsonFile.save('error', lastCheckFile, { ensureFolderExists: true }); - - // For this check we use the official registry, not the private registry - return this._queryIfReleaseIsPublished('https://registry.npmjs.org:443') - .then((publishedRelease: boolean) => { - // Cache the result - JsonFile.save(publishedRelease, lastCheckFile, { ensureFolderExists: true }); - return publishedRelease; - }) - .catch((error: Error) => { - JsonFile.save('error', lastCheckFile, { ensureFolderExists: true }); - return Promise.reject(error); - }); - }); - } - - private _queryIfReleaseIsPublished(registryUrl: string): Promise { - let queryUrl: string = registryUrl; - if (queryUrl[-1] !== '/') { - queryUrl += '/'; - } - // Note that the "@" symbol does not normally get URL-encoded - queryUrl += RushConstants.rushPackageName.replace('/', '%2F'); - - const userAgent: string = `pnpm/? npm/? node/${process.version} ${os.platform()} ${os.arch()}`; - - const headers: fetch.Headers = new fetch.Headers(); - headers.append('user-agent', userAgent); - headers.append('accept', 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'); - - let agent: http.Agent | undefined = undefined; - if (process.env.HTTP_PROXY) { - agent = new HttpsProxyAgent(process.env.HTTP_PROXY); - } - - return fetch - .default(queryUrl, { - headers: headers, - agent: agent - }) - .then((response: fetch.Response) => { - if (!response.ok) { - return Promise.reject(new Error('Failed to query')); - } - return response.json().then((data) => { - let url: string; - try { - if (!data.versions[Rush.version]) { - // Version was not published - return false; - } - url = data.versions[Rush.version].dist.tarball; - if (!url) { - return Promise.reject(new Error(`URL not found`)); - } - } catch (e) { - return Promise.reject(new Error('Error parsing response')); - } - - // Make sure the tarball wasn't deleted from the CDN - headers.set('accept', '*/*'); - return fetch - .default(url, { - headers: headers, - agent: agent - }) - .then((response2: fetch.Response) => { - if (!response2.ok) { - if (response2.status === 404) { - return false; - } else { - return Promise.reject(new Error('Failed to fetch')); - } - } - return true; - }); - }); - }); - } - - /** - * Used when invoking the NPM tool. Appends the common configuration options - * to the command-line. - */ - private _pushConfigurationArgs(args: string[], options: IInstallManagerOptions): void { - if (this._rushConfiguration.packageManager === 'npm') { - if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '5.0.0')) { - // NOTE: - // - // When using an npm version older than v5.0.0, we do NOT install optional dependencies for - // Rush, because npm does not generate the shrinkwrap file consistently across platforms. - // - // Consider the "fsevents" package. This is a Mac specific package - // which is an optional second-order dependency. Optional dependencies work by attempting to install - // the package, but removes the package if the install failed. - // This means that someone running generate on a Mac WILL have fsevents included in their shrinkwrap. - // When someone using Windows attempts to install from the shrinkwrap, the install will fail. - // - // If someone generates the shrinkwrap using Windows, then fsevents will NOT be listed in the shrinkwrap. - // When someone using Mac attempts to install from the shrinkwrap, they will NOT have the - // optional dependency installed. - // - // This issue has been fixed as of npm v5.0.0: https://github.com/npm/npm/releases/tag/v5.0.0 - // - // For more context, see https://github.com/microsoft/rushstack/issues/761#issuecomment-428689600 - args.push('--no-optional'); - } - args.push('--cache', this._rushConfiguration.npmCacheFolder); - args.push('--tmp', this._rushConfiguration.npmTmpFolder); - - if (options.collectLogFile) { - args.push('--verbose'); - } - } else if (this._rushConfiguration.packageManager === 'pnpm') { - // Only explicitly define the store path if `pnpmStore` is using the default, or has been set to - // 'local'. If `pnpmStore` = 'global', then allow PNPM to use the system's default - // path. In all cases, this will be overridden by RUSH_PNPM_STORE_PATH - if ( - this._rushConfiguration.pnpmOptions.pnpmStore === 'local' || - EnvironmentConfiguration.pnpmStorePathOverride - ) { - args.push('--store', this._rushConfiguration.pnpmOptions.pnpmStorePath); - } - - // we are using the --no-lock flag for now, which unfortunately prints a warning, but should be OK - // since rush already has its own install lock file which will invalidate the cache for us. - // we theoretically could use the lock file, but we would need to clean the store if the - // lockfile existed, otherwise PNPM would hang indefinitely. it is simpler to rely on Rush's - // last install flag, which encapsulates the entire installation - args.push('--no-lock'); - - if ( - this._rushConfiguration.experimentsConfiguration.configuration.usePnpmFrozenLockfileForRushInstall && - !this._options.allowShrinkwrapUpdates - ) { - if (semver.gte(this._rushConfiguration.packageManagerToolVersion, '3.0.0')) { - args.push('--frozen-lockfile'); - } else { - args.push('--frozen-shrinkwrap'); - } - } else { - // Ensure that Rush's tarball dependencies get synchronized properly with the pnpm-lock.yaml file. - // See this GitHub issue: https://github.com/pnpm/pnpm/issues/1342 - if (semver.gte(this._rushConfiguration.packageManagerToolVersion, '3.0.0')) { - args.push('--no-prefer-frozen-lockfile'); - } else { - args.push('--no-prefer-frozen-shrinkwrap'); - } - } - - if (options.collectLogFile) { - args.push('--reporter', 'ndjson'); - } - - if (options.networkConcurrency) { - args.push('--network-concurrency', options.networkConcurrency.toString()); - } - - if (this._rushConfiguration.pnpmOptions.strictPeerDependencies) { - args.push('--strict-peer-dependencies'); - } - - if ((this._rushConfiguration.packageManagerWrapper as PnpmPackageManager).supportsResolutionStrategy) { - args.push(`--resolution-strategy=${this._rushConfiguration.pnpmOptions.resolutionStrategy}`); - } - } else if (this._rushConfiguration.packageManager === 'yarn') { - args.push('--link-folder', 'yarn-link'); - args.push('--cache-folder', this._rushConfiguration.yarnCacheFolder); - - // Without this option, Yarn will sometimes stop and ask for user input on STDIN - // (e.g. "Which command would you like to run?"). - args.push('--non-interactive'); - - if (options.networkConcurrency) { - args.push('--network-concurrency', options.networkConcurrency.toString()); - } - - if (this._rushConfiguration.yarnOptions.ignoreEngines) { - args.push('--ignore-engines'); - } - } - } - - /** - * Copies the file "sourcePath" to "destinationPath", overwriting the target file location. - * If the source file does not exist, then the target file is deleted. - */ - private _syncFile(sourcePath: string, destinationPath: string): void { - if (FileSystem.exists(sourcePath)) { - console.log('Updating ' + destinationPath); - FileSystem.copyFile({ sourcePath, destinationPath }); - } else { - if (FileSystem.exists(destinationPath)) { - console.log('Deleting ' + destinationPath); - FileSystem.deleteFile(destinationPath); - } - } - } - - /** - * Gets the path to the tarball - * Example: "C:\MyRepo\common\temp\projects\my-project-2.tgz" - */ - private _getTarballFilePath(project: RushConfigurationProject): string { - return path.join( - this._rushConfiguration.commonTempFolder, - RushConstants.rushTempProjectsFolderName, - `${project.unscopedTempProjectName}.tgz` - ); - } - - /** - * This is a workaround for a bug introduced in NPM 5 (and still unfixed as of NPM 5.5.1): - * https://github.com/npm/npm/issues/19006 - * - * The regression is that "npm install" sets the package.json "version" field for the - * @rush-temp projects to a value like "file:projects/example.tgz", when it should be "0.0.0". - * This causes "rush link" to fail later, when read-package-tree tries to parse the bad version. - * The error looks like this: - * - * ERROR: Failed to parse package.json for foo: Invalid version: "file:projects/example.tgz" - * - * Our workaround is to rewrite the package.json files for each of the @rush-temp projects - * in the node_modules folder, after "npm install" completes. - */ - private _fixupNpm5Regression(): void { - const pathToDeleteWithoutStar: string = path.join( - this._rushConfiguration.commonTempFolder, - 'node_modules', - RushConstants.rushTempNpmScope - ); - // Glob can't handle Windows paths - const normalizedPathToDeleteWithoutStar: string = Text.replaceAll(pathToDeleteWithoutStar, '\\', '/'); - - let anyChanges: boolean = false; - - // Example: "C:/MyRepo/common/temp/node_modules/@rush-temp/*/package.json" - for (const packageJsonPath of glob.sync( - globEscape(normalizedPathToDeleteWithoutStar) + '/*/package.json' - )) { - // Example: "C:/MyRepo/common/temp/node_modules/@rush-temp/example/package.json" - const packageJsonObject: IRushTempPackageJson = JsonFile.load(packageJsonPath); - - // The temp projects always use "0.0.0" as their version - packageJsonObject.version = '0.0.0'; - - if (JsonFile.save(packageJsonObject, packageJsonPath, { onlyIfChanged: true })) { - anyChanges = true; - } - } - - if (anyChanges) { - console.log(os.EOL + colors.yellow(Utilities.wrapWords(`Applied workaround for NPM 5 bug`)) + os.EOL); - } - } - - /** - * Checks for temp projects that exist in the shrinkwrap file, but don't exist - * in rush.json. This might occur, e.g. if a project was recently deleted or renamed. - * - * @returns true if orphans were found, or false if everything is okay - */ - private _findOrphanedTempProjects(shrinkwrapFile: BaseShrinkwrapFile): boolean { - // We can recognize temp projects because they are under the "@rush-temp" NPM scope. - for (const tempProjectName of shrinkwrapFile.getTempProjectNames()) { - if (!this._rushConfiguration.findProjectByTempName(tempProjectName)) { - console.log( - os.EOL + - colors.yellow( - Utilities.wrapWords( - `Your ${this._shrinkwrapFilePhrase} references a project "${tempProjectName}" which no longer exists.` - ) - ) + - os.EOL - ); - return true; // found one - } - } - - return false; // none found - } - - private get _shrinkwrapFilePhrase(): string { - return this._rushConfiguration.shrinkwrapFilePhrase; - } -} diff --git a/apps/rush-lib/src/logic/InstallManagerFactory.ts b/apps/rush-lib/src/logic/InstallManagerFactory.ts new file mode 100644 index 00000000000..fb27a19b0da --- /dev/null +++ b/apps/rush-lib/src/logic/InstallManagerFactory.ts @@ -0,0 +1,42 @@ +// 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 semver from 'semver'; + +import { BaseInstallManager, IInstallManagerOptions } from './base/BaseInstallManager'; +import { RushInstallManager } from './installManager/RushInstallManager'; +import { WorkspaceInstallManager } from './installManager/WorkspaceInstallManager'; +import { AlreadyReportedError } from '../utilities/AlreadyReportedError'; +import { PurgeManager } from './PurgeManager'; +import { RushConfiguration } from '../api/RushConfiguration'; +import { RushGlobalFolder } from '../api/RushGlobalFolder'; + +export class InstallManagerFactory { + public static getInstallManager( + rushConfiguration: RushConfiguration, + rushGlobalFolder: RushGlobalFolder, + purgeManager: PurgeManager, + options: IInstallManagerOptions + ): BaseInstallManager { + if ( + rushConfiguration.packageManager === 'pnpm' && + rushConfiguration.pnpmOptions && + rushConfiguration.pnpmOptions.useWorkspaces + ) { + if (!semver.satisfies(rushConfiguration.packageManagerToolVersion, '>=4.14.3')) { + console.log(); + console.log( + colors.red( + 'Workspaces are only supported in Rush for PNPM >=4.14.3. Upgrade PNPM to use the workspaces feature.' + ) + ); + throw new AlreadyReportedError(); + } + + return new WorkspaceInstallManager(rushConfiguration, rushGlobalFolder, purgeManager, options); + } + + return new RushInstallManager(rushConfiguration, rushGlobalFolder, purgeManager, options); + } +} diff --git a/apps/rush-lib/src/logic/PackageJsonUpdater.ts b/apps/rush-lib/src/logic/PackageJsonUpdater.ts index e6b3930182b..d5fe9122b5a 100644 --- a/apps/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/apps/rush-lib/src/logic/PackageJsonUpdater.ts @@ -5,7 +5,8 @@ import * as colors from 'colors'; import * as semver from 'semver'; import { RushConfiguration } from '../api/RushConfiguration'; -import { InstallManager, IInstallManagerOptions } from './InstallManager'; +import { BaseInstallManager, IInstallManagerOptions } from './base/BaseInstallManager'; +import { InstallManagerFactory } from './InstallManagerFactory'; import { VersionMismatchFinder } from './versionMismatch/VersionMismatchFinder'; import { PurgeManager } from './PurgeManager'; import { Utilities } from '../utilities/Utilities'; @@ -15,7 +16,7 @@ import { RushConfigurationProject } from '../api/RushConfigurationProject'; import { VersionMismatchFinderEntity } from './versionMismatch/VersionMismatchFinderEntity'; import { VersionMismatchFinderProject } from './versionMismatch/VersionMismatchFinderProject'; import { RushConstants } from './RushConstants'; -import { InstallHelpers } from './InstallHelpers'; +import { InstallHelpers } from './installManager/InstallHelpers'; /** * The type of SemVer range specifier that is prepended to the version @@ -123,7 +124,7 @@ export class PackageJsonUpdater { variant } = options; - const implicitlyPinned: Map = InstallManager.collectImplicitlyPreferredVersions( + const implicitlyPinned: Map = InstallHelpers.collectImplicitlyPreferredVersions( this._rushConfiguration, { variant @@ -143,7 +144,7 @@ export class PackageJsonUpdater { variant: variant, maxInstallAttempts: RushConstants.defaultMaxInstallAttempts }; - const installManager: InstallManager = new InstallManager( + const installManager: BaseInstallManager = InstallManagerFactory.getInstallManager( this._rushConfiguration, this._rushGlobalFolder, purgeManager, @@ -281,7 +282,7 @@ export class PackageJsonUpdater { */ private async _getNormalizedVersionSpec( projects: RushConfigurationProject[], - installManager: InstallManager, + installManager: BaseInstallManager, packageName: string, initialSpec: string | undefined, implicitlyPinnedVersion: string | undefined, @@ -328,7 +329,18 @@ export class PackageJsonUpdater { RushConstants.defaultMaxInstallAttempts ); + const useWorkspaces: boolean = !!( + this._rushConfiguration.pnpmOptions && this._rushConfiguration.pnpmOptions.useWorkspaces + ); + const workspacePrefix: string = 'workspace:'; + + // Trim 'workspace:' notation from the spec, since we're going to be tweaking the range + if (useWorkspaces && initialSpec && initialSpec.startsWith(workspacePrefix)) { + initialSpec = initialSpec.substring(workspacePrefix.length).trim(); + } + let selectedVersion: string | undefined; + let selectedVersionPrefix: string = ''; if (initialSpec && initialSpec !== 'latest') { console.log(colors.gray('Finding versions that satisfy the selector: ') + initialSpec); @@ -337,7 +349,14 @@ export class PackageJsonUpdater { if (localProject !== undefined) { const version: string = localProject.packageJson.version; if (semver.satisfies(version, initialSpec)) { - selectedVersion = version; + // For workspaces, assume that specifying the exact version means you always want to consume + // the local project. Otherwise, use the exact local package version + if (useWorkspaces) { + selectedVersion = initialSpec === version ? '*' : initialSpec; + selectedVersionPrefix = workspacePrefix; + } else { + selectedVersion = version; + } } else { throw new Error( `The dependency being added ("${packageName}") is a project in the local Rush repository, ` + @@ -398,7 +417,14 @@ export class PackageJsonUpdater { } if (localProject !== undefined) { - selectedVersion = localProject.packageJson.version; + // For workspaces, assume that no specified version range means you always want to consume + // the local project. Otherwise, use the exact local package version + if (useWorkspaces) { + selectedVersion = '*'; + selectedVersionPrefix = workspacePrefix; + } else { + selectedVersion = localProject.packageJson.version; + } } else { console.log(`Querying NPM registry for latest version of "${packageName}"...`); @@ -423,41 +449,40 @@ export class PackageJsonUpdater { console.log(); - switch (rangeStyle) { - case SemVerStyle.Caret: { - console.log( - colors.grey( - `Assigning version "^${selectedVersion}" for "${packageName}" because the "--caret"` + - ` flag was specified.` - ) - ); - return `^${selectedVersion}`; - } + let reasonForModification: string = ''; + if (selectedVersion !== '*') { + switch (rangeStyle) { + case SemVerStyle.Caret: { + selectedVersionPrefix += '^'; + reasonForModification = ' because the "--caret" flag was specified'; + break; + } - case SemVerStyle.Exact: { - console.log( - colors.grey( - `Assigning version "${selectedVersion}" for "${packageName}" because the "--exact"` + - ` flag was specified.` - ) - ); - return selectedVersion; - } + case SemVerStyle.Exact: { + reasonForModification = ' because the "--exact" flag was specified'; + break; + } - case SemVerStyle.Tilde: { - console.log(colors.gray(`Assigning version "~${selectedVersion}" for "${packageName}".`)); - return `~${selectedVersion}`; - } + case SemVerStyle.Tilde: { + selectedVersionPrefix += '~'; + break; + } - case SemVerStyle.Passthrough: { - console.log(colors.gray(`Assigning version "${selectedVersion}" for "${packageName}".`)); - return selectedVersion; - } + case SemVerStyle.Passthrough: { + break; + } - default: { - throw new Error(`Unexpected SemVerStyle ${rangeStyle}.`); + default: { + throw new Error(`Unexpected SemVerStyle ${rangeStyle}.`); + } } } + + const normalizedVersion: string = selectedVersionPrefix + selectedVersion; + console.log( + colors.gray(`Assigning version "${normalizedVersion}" for "${packageName}"${reasonForModification}.`) + ); + return normalizedVersion; } private _collectAllDownstreamDependencies( diff --git a/apps/rush-lib/src/logic/PublishUtilities.ts b/apps/rush-lib/src/logic/PublishUtilities.ts index c2d88473030..829a9c451f9 100644 --- a/apps/rush-lib/src/logic/PublishUtilities.ts +++ b/apps/rush-lib/src/logic/PublishUtilities.ts @@ -19,6 +19,7 @@ import { execSync } from 'child_process'; import { PrereleaseToken } from './PrereleaseToken'; import { ChangeFiles } from './ChangeFiles'; import { RushConfiguration } from '../api/RushConfiguration'; +import { DependencySpecifier, DependencySpecifierType } from './DependencySpecifier'; export interface IChangeInfoHash { [key: string]: IChangeInfo; @@ -97,7 +98,7 @@ export class PublishUtilities { // For hotfix changes, do not re-write new version change.newVersion = change.changeType! >= ChangeType.patch - ? semver.inc(pkg.version, PublishUtilities._getReleaseType(change.changeType!)) + ? semver.inc(pkg.version, PublishUtilities._getReleaseType(change.changeType!))! : change.changeType === ChangeType.hotfix ? change.newVersion : pkg.version; @@ -211,7 +212,14 @@ export class PublishUtilities { ); if (shouldExecute) { - Utilities.executeCommand(command, args, workingDirectory, environment, false, true); + Utilities.executeCommand({ + command, + args, + workingDirectory, + environment, + suppressOutput: false, + keepEnvironment: true + }); } } @@ -220,9 +228,16 @@ export class PublishUtilities { dependencyName: string, newProjectVersion: string ): string { - const currentDependencyVersion: string = dependencies[dependencyName]; + const currentDependencySpecifier: DependencySpecifier = new DependencySpecifier( + dependencyName, + dependencies[dependencyName] + ); + const currentDependencyVersion: string = currentDependencySpecifier.versionSpecifier; let newDependencyVersion: string; - if (PublishUtilities.isRangeDependency(currentDependencyVersion)) { + + if (currentDependencyVersion === '*') { + newDependencyVersion = '*'; + } else if (PublishUtilities.isRangeDependency(currentDependencyVersion)) { newDependencyVersion = PublishUtilities._getNewRangeDependency(newProjectVersion); } else if (currentDependencyVersion.lastIndexOf('~', 0) === 0) { newDependencyVersion = '~' + newProjectVersion; @@ -231,7 +246,9 @@ export class PublishUtilities { } else { newDependencyVersion = newProjectVersion; } - return newDependencyVersion; + return currentDependencySpecifier.specifierType === DependencySpecifierType.Workspace + ? `workspace:${newDependencyVersion}` + : newDependencyVersion; } private static _getReleaseType(changeType: ChangeType): semver.ReleaseType { @@ -253,9 +270,9 @@ export class PublishUtilities { let upperLimit: string = newVersion; if (semver.prerelease(newVersion)) { // Remove the prerelease first, then bump major. - upperLimit = semver.inc(newVersion, 'patch'); + upperLimit = semver.inc(newVersion, 'patch')!; } - upperLimit = semver.inc(upperLimit, 'major'); + upperLimit = semver.inc(upperLimit, 'major')!; return `>=${newVersion} <${upperLimit}`; } @@ -411,7 +428,15 @@ export class PublishUtilities { // TODO: treat prerelease version the same as non-prerelease version. // For prerelease, the newVersion needs to be appended with prerelease name. // And dependency should specify the specific prerelease version. - dependencies[depName] = PublishUtilities._getChangeInfoNewVersion(depChange, prereleaseToken); + const currentSpecifier: DependencySpecifier = new DependencySpecifier( + depName, + dependencies[depName] + ); + const newVersion: string = PublishUtilities._getChangeInfoNewVersion(depChange, prereleaseToken); + dependencies[depName] = + currentSpecifier.specifierType === DependencySpecifierType.Workspace + ? `workspace:${newVersion}` + : newVersion; } else if (depChange && depChange.changeType! >= ChangeType.hotfix) { PublishUtilities._updateDependencyVersion( packageName, @@ -445,7 +470,7 @@ export class PublishUtilities { return newVersion; } if (prereleaseToken.isPrerelease && change.changeType === ChangeType.dependency) { - newVersion = semver.inc(newVersion, 'patch'); + newVersion = semver.inc(newVersion, 'patch')!; } return `${newVersion}-${prereleaseToken.name}`; } else { @@ -526,7 +551,7 @@ export class PublishUtilities { currentChange.changeType = ChangeType.none; } else { if (change.changeType === ChangeType.hotfix) { - const prereleaseComponents: string[] = semver.prerelease(pkg.version); + const prereleaseComponents: ReadonlyArray | null = semver.prerelease(pkg.version); if (!rushConfiguration.hotfixChangeEnabled) { throw new Error(`Cannot add hotfix change; hotfixChangeEnabled is false in configuration.`); } @@ -535,7 +560,7 @@ export class PublishUtilities { if (!prereleaseComponents) { currentChange.newVersion += '-hotfix'; } - currentChange.newVersion = semver.inc(currentChange.newVersion, 'prerelease'); + currentChange.newVersion = semver.inc(currentChange.newVersion, 'prerelease')!; } else { // When there are multiple changes of this package, the final value of new version // should not depend on the order of the changes. @@ -545,7 +570,7 @@ export class PublishUtilities { } currentChange.newVersion = change.changeType! >= ChangeType.patch - ? semver.inc(pkg.version, PublishUtilities._getReleaseType(currentChange.changeType!)) + ? semver.inc(pkg.version, PublishUtilities._getReleaseType(currentChange.changeType!))! : packageVersion; } @@ -615,7 +640,10 @@ export class PublishUtilities { dependencies[change.packageName] && !PublishUtilities._isCyclicDependency(allPackages, parentPackageName, change.packageName) ) { - const requiredVersion: string = dependencies[change.packageName]; + const requiredVersion: string = new DependencySpecifier( + change.packageName, + dependencies[change.packageName] + ).versionSpecifier; const alwaysUpdate: boolean = !!prereleaseToken && prereleaseToken.hasValue && !allChanges.hasOwnProperty(parentPackageName); @@ -670,13 +698,36 @@ export class PublishUtilities { allPackages: Map, rushConfiguration: RushConfiguration ): void { - const currentDependencyVersion: string = dependencies[dependencyName]; - - dependencies[dependencyName] = PublishUtilities.getNewDependencyVersion( + let currentDependencyVersion: string | undefined = dependencies[dependencyName]; + let newDependencyVersion: string = PublishUtilities.getNewDependencyVersion( dependencies, dependencyName, dependencyChange.newVersion! ); + dependencies[dependencyName] = newDependencyVersion; + + // "*" is a special case for workspace ranges, since it will publish using the exact + // version of the local dependency, so we need to modify what we write for our change + // comment + const currentDependencySpecifier: DependencySpecifier = new DependencySpecifier( + dependencyName, + currentDependencyVersion + ); + currentDependencyVersion = + currentDependencySpecifier.specifierType === DependencySpecifierType.Workspace && + currentDependencySpecifier.versionSpecifier === '*' + ? undefined + : currentDependencySpecifier.versionSpecifier; + + const newDependencySpecifier: DependencySpecifier = new DependencySpecifier( + dependencyName, + newDependencyVersion + ); + newDependencyVersion = + newDependencySpecifier.specifierType === DependencySpecifierType.Workspace && + newDependencySpecifier.versionSpecifier === '*' + ? dependencyChange.newVersion! + : newDependencySpecifier.versionSpecifier; // Add dependency version update comment. PublishUtilities._addChange( @@ -684,8 +735,9 @@ export class PublishUtilities { packageName: packageName, changeType: ChangeType.dependency, comment: - `Updating dependency "${dependencyName}" from \`${currentDependencyVersion}\`` + - ` to \`${dependencies[dependencyName]}\`` + `Updating dependency "${dependencyName}" ` + + (currentDependencyVersion ? `from \`${currentDependencyVersion}\` ` : '') + + `to \`${newDependencyVersion}\`` }, allChanges, allPackages, diff --git a/apps/rush-lib/src/logic/RepoStateFile.ts b/apps/rush-lib/src/logic/RepoStateFile.ts new file mode 100644 index 00000000000..23bffc9f36a --- /dev/null +++ b/apps/rush-lib/src/logic/RepoStateFile.ts @@ -0,0 +1,178 @@ +// 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 'path'; +import { FileSystem, JsonFile, JsonSchema, NewlineKind } from '@rushstack/node-core-library'; + +import { RushConfiguration } from '../api/RushConfiguration'; +import { PnpmShrinkwrapFile } from './pnpm/PnpmShrinkwrapFile'; +import { CommonVersionsConfiguration } from '../api/CommonVersionsConfiguration'; + +/** + * This interface represents the raw repo-state.json file + * Example: + * { + * "pnpmShrinkwrapHash": "...", + * "preferredVersionsHash": "..." + * } + */ +interface IRepoStateJson { + /** + * A hash of the PNPM shrinkwrap file contents + */ + pnpmShrinkwrapHash?: string; + /** + * A hash of the CommonVersionsConfiguration.preferredVersions field + */ + preferredVersionsHash?: string; +} + +/** + * This file is used to track the state of various Rush-related features. It is generated + * and updated by Rush. + * + * @public + */ +export class RepoStateFile { + private static _jsonSchema: JsonSchema = JsonSchema.fromFile( + path.join(__dirname, '../schemas/repo-state.schema.json') + ); + private _repoStateFilePath: string; + private _variant: string | undefined; + private _pnpmShrinkwrapHash: string | undefined; + private _preferredVersionsHash: string | undefined; + private _modified: boolean = false; + + private constructor( + repoStateJson: IRepoStateJson | undefined, + filePath: string, + variant: string | undefined + ) { + this._repoStateFilePath = filePath; + this._variant = variant; + + if (repoStateJson) { + this._pnpmShrinkwrapHash = repoStateJson.pnpmShrinkwrapHash; + this._preferredVersionsHash = repoStateJson.preferredVersionsHash; + } + } + + /** + * Get the absolute file path of the repo-state.json file. + */ + public get filePath(): string { + return this._repoStateFilePath; + } + + /** + * The hash of the pnpm shrinkwrap file at the end of the last update. + */ + public get pnpmShrinkwrapHash(): string | undefined { + return this._pnpmShrinkwrapHash; + } + + /** + * The hash of all preferred versions at the end of the last update. + */ + public get preferredVersionsHash(): string | undefined { + return this._preferredVersionsHash; + } + + /** + * Loads the repo-state.json data from the specified file path. + * If the file has not been created yet, then an empty object is returned. + * + * @param jsonFilename - The path to the repo-state.json file. + * @param variant - The variant currently being used by Rush. + */ + public static loadFromFile(jsonFilename: string, variant: string | undefined): RepoStateFile { + let repoStateJson: IRepoStateJson | undefined = undefined; + try { + repoStateJson = JsonFile.loadAndValidate(jsonFilename, RepoStateFile._jsonSchema); + } catch (error) { + if (!FileSystem.isNotExistError(error)) { + throw error; + } + } + + return new RepoStateFile(repoStateJson, jsonFilename, variant); + } + + /** + * Refresh the data contained in repo-state.json using the current state + * of the Rush repo, and save the file if changes were made. + * + * @param rushConfiguration - The Rush configuration for the repo. + * + * @returns true if the file was modified, otherwise false. + */ + public refreshState(rushConfiguration: RushConfiguration): boolean { + // Only support saving the pnpm shrinkwrap hash if it was enabled + const preventShrinkwrapChanges: boolean = + rushConfiguration.packageManager === 'pnpm' && + rushConfiguration.pnpmOptions && + rushConfiguration.pnpmOptions.preventManualShrinkwrapChanges; + if (preventShrinkwrapChanges) { + const pnpmShrinkwrapFile: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile( + rushConfiguration.getCommittedShrinkwrapFilename(this._variant), + rushConfiguration.pnpmOptions + ); + if (pnpmShrinkwrapFile) { + const shrinkwrapFileHash: string = pnpmShrinkwrapFile.getShrinkwrapHash(); + if (this._pnpmShrinkwrapHash !== shrinkwrapFileHash) { + this._pnpmShrinkwrapHash = shrinkwrapFileHash; + this._modified = true; + } + } + } else if (this._pnpmShrinkwrapHash !== undefined) { + this._pnpmShrinkwrapHash = undefined; + this._modified = true; + } + + // Currently, only support saving the preferred versions hash if using workspaces + const useWorkspaces: boolean = + rushConfiguration.pnpmOptions && rushConfiguration.pnpmOptions.useWorkspaces; + if (useWorkspaces) { + const commonVersions: CommonVersionsConfiguration = rushConfiguration.getCommonVersions(this._variant); + const preferredVersionsHash: string = commonVersions.getPreferredVersionsHash(); + if (this._preferredVersionsHash !== preferredVersionsHash) { + this._preferredVersionsHash = preferredVersionsHash; + this._modified = true; + } + } else if (this._preferredVersionsHash !== undefined) { + this._preferredVersionsHash = undefined; + this._modified = true; + } + + return this._saveIfModified(); + } + + /** + * Writes the "repo-state.json" file to disk, using the filename that was passed to loadFromFile(). + */ + private _saveIfModified(): boolean { + if (this._modified) { + const content: string = + '// DO NOT MODIFY THIS FILE. It is generated and used by Rush.' + + `${NewlineKind.Lf}${this._serialize()}`; + FileSystem.writeFile(this._repoStateFilePath, content); + this._modified = false; + return true; + } + + return false; + } + + private _serialize(): string { + // We need to set these one-by-one, since JsonFile.stringify does not like undefined values + const repoStateJson: IRepoStateJson = {}; + if (this._pnpmShrinkwrapHash) { + repoStateJson.pnpmShrinkwrapHash = this._pnpmShrinkwrapHash; + } + if (this._preferredVersionsHash) { + repoStateJson.preferredVersionsHash = this._preferredVersionsHash; + } + + return JsonFile.stringify(repoStateJson, { newlineConversion: NewlineKind.Lf }); + } +} diff --git a/apps/rush-lib/src/logic/RushConstants.ts b/apps/rush-lib/src/logic/RushConstants.ts index 983511d0848..b3c05bf6b29 100644 --- a/apps/rush-lib/src/logic/RushConstants.ts +++ b/apps/rush-lib/src/logic/RushConstants.ts @@ -119,6 +119,12 @@ export class RushConstants { */ public static readonly commonVersionsFilename: string = 'common-versions.json'; + /** + * The filename ("repo-state.json") for a file used by Rush to + * store the state of various features as they stand in the repo. + */ + public static readonly repoStateFilename: string = 'repo-state.json'; + /** * The name of the per-project folder where project-specific Rush files are stored. For example, * the package-deps files, which are used by commands to determine if a particular project needs to be rebuilt. diff --git a/apps/rush-lib/src/logic/VersionManager.ts b/apps/rush-lib/src/logic/VersionManager.ts index 95ad420ff10..80b32cb5b30 100644 --- a/apps/rush-lib/src/logic/VersionManager.ts +++ b/apps/rush-lib/src/logic/VersionManager.ts @@ -14,6 +14,7 @@ import { RushConfigurationProject } from '../api/RushConfigurationProject'; import { VersionPolicyConfiguration } from '../api/VersionPolicyConfiguration'; import { PublishUtilities } from './PublishUtilities'; import { ChangeManager } from './ChangeManager'; +import { DependencySpecifier } from './DependencySpecifier'; export class VersionManager { private _rushConfiguration: RushConfiguration; @@ -313,7 +314,14 @@ export class VersionManager { oldDependencyVersion: string, newDependencyVersion: string ): void { - if (!semver.satisfies(updatedDependentProject.version, oldDependencyVersion) && !projectVersionChanged) { + const oldSpecifier: DependencySpecifier = new DependencySpecifier( + updatedDependentProject.name, + oldDependencyVersion + ); + if ( + !semver.satisfies(updatedDependentProject.version, oldSpecifier.versionSpecifier) && + !projectVersionChanged + ) { this._addChange(changes, { changeType: ChangeType.patch, packageName: clonedProject.name diff --git a/apps/rush-lib/src/logic/base/BaseInstallManager.ts b/apps/rush-lib/src/logic/base/BaseInstallManager.ts new file mode 100644 index 00000000000..102e32dc360 --- /dev/null +++ b/apps/rush-lib/src/logic/base/BaseInstallManager.ts @@ -0,0 +1,659 @@ +// 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 fetch from 'node-fetch'; +import * as fs from 'fs'; +import * as http from 'http'; +import HttpsProxyAgent = require('https-proxy-agent'); +import * as os from 'os'; +import * as path from 'path'; +import * as semver from 'semver'; +import { FileSystem, JsonFile, JsonObject, PosixModeBits, NewlineKind } from '@rushstack/node-core-library'; + +import { AlreadyReportedError } from '../../utilities/AlreadyReportedError'; +import { ApprovedPackagesChecker } from '../ApprovedPackagesChecker'; +import { AsyncRecycler } from '../../utilities/AsyncRecycler'; +import { BaseLinkManager } from './BaseLinkManager'; +import { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile'; +import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; +import { Git } from '../Git'; +import { LastInstallFlag } from '../../api/LastInstallFlag'; +import { PnpmPackageManager } from '../../api/packageManager/PnpmPackageManager'; +import { PurgeManager } from '../PurgeManager'; +import { RushConfiguration, ICurrentVariantJson } from '../../api/RushConfiguration'; +import { Rush } from '../../api/Rush'; +import { RushGlobalFolder } from '../../api/RushGlobalFolder'; +import { RushConstants } from '../RushConstants'; +import { ShrinkwrapFileFactory } from '../ShrinkwrapFileFactory'; +import { Utilities } from '../../utilities/Utilities'; +import { InstallHelpers } from '../installManager/InstallHelpers'; +import { PolicyValidator } from '../policy/PolicyValidator'; +import { LinkManagerFactory } from '../LinkManagerFactory'; + +export interface IInstallManagerOptions { + /** + * Whether the global "--debug" flag was specified. + */ + debug: boolean; + + /** + * Whether or not Rush will automatically update the shrinkwrap file. + * True for "rush update", false for "rush install". + */ + allowShrinkwrapUpdates: boolean; + + /** + * Whether to skip policy checks. + */ + bypassPolicy: boolean; + + /** + * Whether to skip linking, i.e. require "rush link" to be done manually later. + */ + noLink: boolean; + + /** + * Whether to delete the shrinkwrap file before installation, i.e. so that all dependencies + * will be upgraded to the latest SemVer-compatible version. + */ + fullUpgrade: boolean; + + /** + * Whether to force an update to the shrinkwrap file even if it appears to be unnecessary. + * Normally Rush uses heuristics to determine when "pnpm install" can be skipped, + * but sometimes the heuristics can be inaccurate due to external influences + * (pnpmfile.js script logic, registry changes, etc). + */ + recheckShrinkwrap: boolean; + + /** + * The value of the "--network-concurrency" command-line parameter, which + * is a diagnostic option used to troubleshoot network failures. + * + * Currently only supported for PNPM. + */ + networkConcurrency: number | undefined; + + /** + * Whether or not to collect verbose logs from the package manager. + * If specified when using PNPM, the logs will be in /common/temp/pnpm.log + */ + collectLogFile: boolean; + + /** + * The variant to consider when performing installations and validating shrinkwrap updates. + */ + variant?: string | undefined; + + /** + * Retry the install the specified number of times + */ + maxInstallAttempts: number; +} + +/** + * This class implements common logic between "rush install" and "rush update". + */ +export abstract class BaseInstallManager { + private _rushConfiguration: RushConfiguration; + private _rushGlobalFolder: RushGlobalFolder; + private _commonTempInstallFlag: LastInstallFlag; + private _installRecycler: AsyncRecycler; + + private _options: IInstallManagerOptions; + + public constructor( + rushConfiguration: RushConfiguration, + rushGlobalFolder: RushGlobalFolder, + purgeManager: PurgeManager, + options: IInstallManagerOptions + ) { + this._rushConfiguration = rushConfiguration; + this._rushGlobalFolder = rushGlobalFolder; + this._installRecycler = purgeManager.commonTempFolderRecycler; + this._options = options; + + const lastInstallState: JsonObject = { + node: process.versions.node, + packageManager: rushConfiguration.packageManager, + packageManagerVersion: rushConfiguration.packageManagerToolVersion + }; + + if (lastInstallState.packageManager === 'pnpm' && rushConfiguration.pnpmOptions) { + lastInstallState.storePath = rushConfiguration.pnpmOptions.pnpmStorePath; + if (rushConfiguration.pnpmOptions.useWorkspaces) { + lastInstallState.workspaces = rushConfiguration.pnpmOptions.useWorkspaces; + } + } + + this._commonTempInstallFlag = new LastInstallFlag( + this._rushConfiguration.commonTempFolder, + lastInstallState + ); + } + + protected get rushConfiguration(): RushConfiguration { + return this._rushConfiguration; + } + + protected get rushGlobalFolder(): RushGlobalFolder { + return this._rushGlobalFolder; + } + + protected get installRecycler(): AsyncRecycler { + return this._installRecycler; + } + + protected get options(): IInstallManagerOptions { + return this._options; + } + + public async doInstall(): Promise { + const { shrinkwrapIsUpToDate, variantIsUpToDate } = await this.prepareAsync(); + + // This marker file indicates that the last "rush install" completed successfully. + // If "--purge" was specified, or if the last install was interrupted, then we will need to + // perform a clean install. Otherwise, we can do an incremental install. + const cleanInstall: boolean = !this._commonTempInstallFlag.checkValidAndReportStoreIssues(); + + // Allow us to defer the file read until we need it + const canSkipInstall: () => boolean = () => { + // Based on timestamps, can we skip this install entirely? + const outputStats: fs.Stats = FileSystem.getStatistics(this._commonTempInstallFlag.path); + return this.canSkipInstall(outputStats.mtime); + }; + + if (cleanInstall || !shrinkwrapIsUpToDate || !variantIsUpToDate || !canSkipInstall()) { + let publishedRelease: boolean | undefined; + try { + publishedRelease = await this._checkIfReleaseIsPublished(); + } catch { + // If the user is working in an environment that can't reach the registry, + // don't bother them with errors. + } + + if (publishedRelease === false) { + console.log( + colors.yellow('Warning: This release of the Rush tool was unpublished; it may be unstable.') + ); + } + + // Since we're going to be tampering with common/node_modules, delete the "rush link" flag file if it exists; + // this ensures that a full "rush link" is required next time + Utilities.deleteFile(this.rushConfiguration.rushLinkJsonFilename); + + // Delete the successful install file to indicate the install transaction has started + this._commonTempInstallFlag.clear(); + + // Perform the actual install + await this.installAsync(cleanInstall); + + const usePnpmFrozenLockfile: boolean = + this._rushConfiguration.packageManager === 'pnpm' && + this._rushConfiguration.experimentsConfiguration.configuration.usePnpmFrozenLockfileForRushInstall === + true; + + if (this.options.allowShrinkwrapUpdates && (usePnpmFrozenLockfile || !shrinkwrapIsUpToDate)) { + // Copy (or delete) common\temp\pnpm-lock.yaml --> common\config\rush\pnpm-lock.yaml + Utilities.syncFile( + this._rushConfiguration.tempShrinkwrapFilename, + this._rushConfiguration.getCommittedShrinkwrapFilename(this.options.variant) + ); + } else { + // TODO: Validate whether the package manager updated it in a nontrivial way + } + + // Always update the state file if running "rush update" + if (this.options.allowShrinkwrapUpdates) { + if (this.rushConfiguration.getRepoState(this.options.variant).refreshState(this.rushConfiguration)) { + console.log( + colors.yellow( + `${RushConstants.repoStateFilename} has been modified and must be committed to source control.` + ) + ); + } + } + + // Create the marker file to indicate a successful install + this._commonTempInstallFlag.create(); + + console.log(''); + } + + if (!this.options.noLink) { + const linkManager: BaseLinkManager = LinkManagerFactory.getLinkManager(this._rushConfiguration); + await linkManager.createSymlinksForProjects(false); + } else { + console.log( + os.EOL + colors.yellow('Since "--no-link" was specified, you will need to run "rush link" manually.') + ); + } + } + + protected abstract prepareCommonTempAsync( + shrinkwrapFile: BaseShrinkwrapFile | undefined + ): Promise<{ shrinkwrapIsUpToDate: boolean; shrinkwrapWarnings: string[] }>; + + protected abstract canSkipInstall(lastInstallDate: Date): boolean; + + protected abstract installAsync(cleanInstall: boolean): Promise; + + protected async prepareAsync(): Promise<{ variantIsUpToDate: boolean; shrinkwrapIsUpToDate: boolean }> { + // Check the policies + PolicyValidator.validatePolicy(this._rushConfiguration, this.options); + + // Git hooks are only installed if the repo opts in by including files in /common/git-hooks + const hookSource: string = path.join(this._rushConfiguration.commonFolder, 'git-hooks'); + const hookDestination: string | undefined = Git.getHooksFolder(); + + if (FileSystem.exists(hookSource) && hookDestination) { + const allHookFilenames: string[] = FileSystem.readFolder(hookSource); + // Ignore the ".sample" file(s) in this folder. + const hookFilenames: string[] = allHookFilenames.filter((x) => !/\.sample$/.test(x)); + if (hookFilenames.length > 0) { + console.log(os.EOL + colors.bold('Found files in the "common/git-hooks" folder.')); + + // Clear the currently installed git hooks and install fresh copies + FileSystem.ensureEmptyFolder(hookDestination); + + // Only copy files that look like Git hook names + const filteredHookFilenames: string[] = hookFilenames.filter((x) => /^[a-z\-]+/.test(x)); + for (const filename of filteredHookFilenames) { + // Copy the file. Important: For Bash scripts, the EOL must not be CRLF. + const hookFileContent: string = FileSystem.readFile(path.join(hookSource, filename)); + FileSystem.writeFile(path.join(hookDestination, filename), hookFileContent, { + convertLineEndings: NewlineKind.Lf + }); + + FileSystem.changePosixModeBits( + path.join(hookDestination, filename), + // eslint-disable-next-line no-bitwise + PosixModeBits.UserRead | PosixModeBits.UserExecute + ); + } + + console.log( + 'Successfully installed these Git hook scripts: ' + filteredHookFilenames.join(', ') + os.EOL + ); + } + } + + const approvedPackagesChecker: ApprovedPackagesChecker = new ApprovedPackagesChecker( + this._rushConfiguration + ); + if (approvedPackagesChecker.approvedPackagesFilesAreOutOfDate) { + if (this._options.allowShrinkwrapUpdates) { + approvedPackagesChecker.rewriteConfigFiles(); + console.log( + colors.yellow( + 'Approved package files have been updated. These updates should be committed to source control' + ) + ); + } else { + throw new Error(`Approved packages files are out-of date. Run "rush update" to update them.`); + } + } + + // Ensure that the package manager is installed + await InstallHelpers.ensureLocalPackageManager( + this._rushConfiguration, + this._rushGlobalFolder, + this._options.maxInstallAttempts + ); + + let shrinkwrapFile: BaseShrinkwrapFile | undefined = undefined; + + // (If it's a full update, then we ignore the shrinkwrap from Git since it will be overwritten) + if (!this.options.fullUpgrade) { + try { + shrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile( + this._rushConfiguration.packageManager, + this._rushConfiguration.packageManagerOptions, + this._rushConfiguration.getCommittedShrinkwrapFilename(this.options.variant) + ); + } catch (ex) { + console.log(); + console.log(`Unable to load the ${this._rushConfiguration.shrinkwrapFilePhrase}: ${ex.message}`); + + if (!this.options.allowShrinkwrapUpdates) { + console.log(); + console.log(colors.red('You need to run "rush update" to fix this problem')); + throw new AlreadyReportedError(); + } + + shrinkwrapFile = undefined; + } + } + + // Write a file indicating which variant is being installed. + // This will be used by bulk scripts to determine the correct Shrinkwrap file to track. + const currentVariantJsonFilename: string = this._rushConfiguration.currentVariantJsonFilename; + const currentVariantJson: ICurrentVariantJson = { + variant: this.options.variant || null // eslint-disable-line @rushstack/no-null + }; + + // Determine if the variant is already current by updating current-variant.json. + // If nothing is written, the variant has not changed. + const variantIsUpToDate: boolean = !JsonFile.save(currentVariantJson, currentVariantJsonFilename, { + onlyIfChanged: true + }); + + if (this.options.variant) { + console.log(); + console.log(colors.bold(`Using variant '${this.options.variant}' for installation.`)); + } else if (!variantIsUpToDate && !this.options.variant) { + console.log(); + console.log(colors.bold('Using the default variant for installation.')); + } + + // Also copy down the committed .npmrc file, if there is one + // "common\config\rush\.npmrc" --> "common\temp\.npmrc" + // Also ensure that we remove any old one that may be hanging around + Utilities.syncNpmrc( + this._rushConfiguration.commonRushConfigFolder, + this._rushConfiguration.commonTempFolder + ); + + // also, copy the pnpmfile.js if it exists + if (this._rushConfiguration.packageManager === 'pnpm') { + const committedPnpmFilePath: string = this._rushConfiguration.getPnpmfilePath(this._options.variant); + const tempPnpmFilePath: string = path.join( + this._rushConfiguration.commonTempFolder, + RushConstants.pnpmfileFilename + ); + + // ensure that we remove any old one that may be hanging around + Utilities.syncFile(committedPnpmFilePath, tempPnpmFilePath); + } + + // Allow for package managers to do their own preparation and check that the shrinkwrap is up to date + // eslint-disable-next-line prefer-const + let { shrinkwrapIsUpToDate, shrinkwrapWarnings } = await this.prepareCommonTempAsync(shrinkwrapFile); + shrinkwrapIsUpToDate = shrinkwrapIsUpToDate && !this.options.recheckShrinkwrap; + + // Write out the reported warnings + if (shrinkwrapWarnings.length > 0) { + console.log(); + console.log( + colors.yellow( + Utilities.wrapWords( + `The ${this.rushConfiguration.shrinkwrapFilePhrase} contains the following issues:` + ) + ) + ); + + for (const shrinkwrapWarning of shrinkwrapWarnings) { + console.log(colors.yellow(' ' + shrinkwrapWarning)); + } + console.log(); + } + + this._syncTempShrinkwrap(shrinkwrapFile); + + // Force update if the shrinkwrap is out of date + if (!shrinkwrapIsUpToDate) { + if (!this.options.allowShrinkwrapUpdates) { + console.log(); + console.log( + colors.red( + `The ${this.rushConfiguration.shrinkwrapFilePhrase} is out of date. You need to run "rush update".` + ) + ); + throw new AlreadyReportedError(); + } + } + + return { shrinkwrapIsUpToDate, variantIsUpToDate }; + } + + /** + * Used when invoking the NPM tool. Appends the common configuration options + * to the command-line. + */ + protected pushConfigurationArgs(args: string[], options: IInstallManagerOptions): void { + if (this._rushConfiguration.packageManager === 'npm') { + if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '5.0.0')) { + // NOTE: + // + // When using an npm version older than v5.0.0, we do NOT install optional dependencies for + // Rush, because npm does not generate the shrinkwrap file consistently across platforms. + // + // Consider the "fsevents" package. This is a Mac specific package + // which is an optional second-order dependency. Optional dependencies work by attempting to install + // the package, but removes the package if the install failed. + // This means that someone running generate on a Mac WILL have fsevents included in their shrinkwrap. + // When someone using Windows attempts to install from the shrinkwrap, the install will fail. + // + // If someone generates the shrinkwrap using Windows, then fsevents will NOT be listed in the shrinkwrap. + // When someone using Mac attempts to install from the shrinkwrap, they will NOT have the + // optional dependency installed. + // + // This issue has been fixed as of npm v5.0.0: https://github.com/npm/npm/releases/tag/v5.0.0 + // + // For more context, see https://github.com/microsoft/rushstack/issues/761#issuecomment-428689600 + args.push('--no-optional'); + } + args.push('--cache', this._rushConfiguration.npmCacheFolder); + args.push('--tmp', this._rushConfiguration.npmTmpFolder); + + if (options.collectLogFile) { + args.push('--verbose'); + } + } else if (this._rushConfiguration.packageManager === 'pnpm') { + // Only explicitly define the store path if `pnpmStore` is using the default, or has been set to + // 'local'. If `pnpmStore` = 'global', then allow PNPM to use the system's default + // path. In all cases, this will be overridden by RUSH_PNPM_STORE_PATH + if ( + this._rushConfiguration.pnpmOptions.pnpmStore === 'local' || + EnvironmentConfiguration.pnpmStorePathOverride + ) { + args.push('--store', this._rushConfiguration.pnpmOptions.pnpmStorePath); + } + + // we are using the --no-lock flag for now, which unfortunately prints a warning, but should be OK + // since rush already has its own install lock file which will invalidate the cache for us. + // we theoretically could use the lock file, but we would need to clean the store if the + // lockfile existed, otherwise PNPM would hang indefinitely. it is simpler to rely on Rush's + // last install flag, which encapsulates the entire installation + args.push('--no-lock'); + + if ( + this._rushConfiguration.experimentsConfiguration.configuration.usePnpmFrozenLockfileForRushInstall && + !this._options.allowShrinkwrapUpdates + ) { + if (semver.gte(this._rushConfiguration.packageManagerToolVersion, '3.0.0')) { + args.push('--frozen-lockfile'); + } else { + args.push('--frozen-shrinkwrap'); + } + } else { + // Ensure that Rush's tarball dependencies get synchronized properly with the pnpm-lock.yaml file. + // See this GitHub issue: https://github.com/pnpm/pnpm/issues/1342 + if (semver.gte(this._rushConfiguration.packageManagerToolVersion, '3.0.0')) { + args.push('--no-prefer-frozen-lockfile'); + } else { + args.push('--no-prefer-frozen-shrinkwrap'); + } + } + + if (options.collectLogFile) { + args.push('--reporter', 'ndjson'); + } + + if (options.networkConcurrency) { + args.push('--network-concurrency', options.networkConcurrency.toString()); + } + + if (this._rushConfiguration.pnpmOptions.strictPeerDependencies) { + args.push('--strict-peer-dependencies'); + } + + if ((this._rushConfiguration.packageManagerWrapper as PnpmPackageManager).supportsResolutionStrategy) { + args.push(`--resolution-strategy=${this._rushConfiguration.pnpmOptions.resolutionStrategy}`); + } + } else if (this._rushConfiguration.packageManager === 'yarn') { + args.push('--link-folder', 'yarn-link'); + args.push('--cache-folder', this._rushConfiguration.yarnCacheFolder); + + // Without this option, Yarn will sometimes stop and ask for user input on STDIN + // (e.g. "Which command would you like to run?"). + args.push('--non-interactive'); + + if (options.networkConcurrency) { + args.push('--network-concurrency', options.networkConcurrency.toString()); + } + + if (this._rushConfiguration.yarnOptions.ignoreEngines) { + args.push('--ignore-engines'); + } + } + } + + private _checkIfReleaseIsPublished(): Promise { + return Promise.resolve().then(() => { + const lastCheckFile: string = path.join( + this._rushGlobalFolder.nodeSpecificPath, + 'rush-' + Rush.version, + 'last-check.flag' + ); + + if (FileSystem.exists(lastCheckFile)) { + let cachedResult: boolean | 'error' | undefined = undefined; + try { + // NOTE: mtimeMs is not supported yet in Node.js 6.x + const nowMs: number = new Date().getTime(); + const ageMs: number = nowMs - FileSystem.getStatistics(lastCheckFile).mtime.getTime(); + const HOUR: number = 60 * 60 * 1000; + + // Is the cache too old? + if (ageMs < 24 * HOUR) { + // No, read the cached result + cachedResult = JsonFile.load(lastCheckFile); + } + } catch (e) { + // Unable to parse file + } + if (cachedResult === 'error') { + return Promise.reject(new Error('Unable to contact server')); + } + if (cachedResult === true || cachedResult === false) { + return cachedResult; + } + } + + // Before we start the network operation, record a failed state. If the process exits for some reason, + // this will record the error. It will also update the timestamp to prevent other Rush instances + // from attempting to update the file. + JsonFile.save('error', lastCheckFile, { ensureFolderExists: true }); + + // For this check we use the official registry, not the private registry + return this._queryIfReleaseIsPublished('https://registry.npmjs.org:443') + .then((publishedRelease: boolean) => { + // Cache the result + JsonFile.save(publishedRelease, lastCheckFile, { ensureFolderExists: true }); + return publishedRelease; + }) + .catch((error: Error) => { + JsonFile.save('error', lastCheckFile, { ensureFolderExists: true }); + return Promise.reject(error); + }); + }); + } + + // Helper for checkIfReleaseIsPublished() + private _queryIfReleaseIsPublished(registryUrl: string): Promise { + let queryUrl: string = registryUrl; + if (queryUrl[-1] !== '/') { + queryUrl += '/'; + } + // Note that the "@" symbol does not normally get URL-encoded + queryUrl += RushConstants.rushPackageName.replace('/', '%2F'); + + const userAgent: string = `pnpm/? npm/? node/${process.version} ${os.platform()} ${os.arch()}`; + + const headers: fetch.Headers = new fetch.Headers(); + headers.append('user-agent', userAgent); + headers.append('accept', 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'); + + let agent: http.Agent | undefined = undefined; + if (process.env.HTTP_PROXY) { + agent = new HttpsProxyAgent(process.env.HTTP_PROXY); + } + + return fetch + .default(queryUrl, { + headers: headers, + agent: agent + }) + .then((response: fetch.Response) => { + if (!response.ok) { + return Promise.reject(new Error('Failed to query')); + } + return response.json().then((data) => { + let url: string; + try { + if (!data.versions[Rush.version]) { + // Version was not published + return false; + } + url = data.versions[Rush.version].dist.tarball; + if (!url) { + return Promise.reject(new Error(`URL not found`)); + } + } catch (e) { + return Promise.reject(new Error('Error parsing response')); + } + + // Make sure the tarball wasn't deleted from the CDN + headers.set('accept', '*/*'); + return fetch + .default(url, { + headers: headers, + agent: agent + }) + .then((response2: fetch.Response) => { + if (!response2.ok) { + if (response2.status === 404) { + return false; + } else { + return Promise.reject(new Error('Failed to fetch')); + } + } + return true; + }); + }); + }); + } + + private _syncTempShrinkwrap(shrinkwrapFile: BaseShrinkwrapFile | undefined): void { + if (shrinkwrapFile) { + // If we have a (possibly incomplete) shrinkwrap file, save it as the temporary file. + shrinkwrapFile.save(this.rushConfiguration.tempShrinkwrapFilename); + shrinkwrapFile.save(this.rushConfiguration.tempShrinkwrapPreinstallFilename); + } else { + // Otherwise delete the temporary file + FileSystem.deleteFile(this.rushConfiguration.tempShrinkwrapFilename); + + if (this.rushConfiguration.packageManager === 'pnpm') { + // Workaround for https://github.com/pnpm/pnpm/issues/1890 + // + // When "rush update --full" is run, rush deletes common/temp/pnpm-lock.yaml so that + // a new lockfile can be generated. But because of the above bug "pnpm install" would + // respect "common/temp/node_modules/.pnpm-lock.yaml" and thus would not generate a + // new lockfile. Deleting this file in addition to deleting common/temp/pnpm-lock.yaml + // ensures that a new lockfile will be generated with "rush update --full". + + const pnpmPackageManager: PnpmPackageManager = this.rushConfiguration + .packageManagerWrapper as PnpmPackageManager; + + FileSystem.deleteFile( + path.join( + this.rushConfiguration.commonTempFolder, + pnpmPackageManager.internalShrinkwrapRelativePath + ) + ); + } + } + } +} diff --git a/apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts b/apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts index 3be7726de09..eb46a829821 100644 --- a/apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts +++ b/apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts @@ -6,8 +6,8 @@ import * as semver from 'semver'; import { FileSystem } from '@rushstack/node-core-library'; import { RushConstants } from '../../logic/RushConstants'; -import { DependencySpecifier } from '../DependencySpecifier'; -import { IPolicyValidatorOptions } from '../policy/PolicyValidator'; +import { DependencySpecifier, DependencySpecifierType } from '../DependencySpecifier'; +import { IShrinkwrapFilePolicyValidatorOptions } from '../policy/ShrinkwrapFilePolicy'; import { PackageManagerOptionsConfigurationBase } from '../../api/RushConfiguration'; import { PackageNameParsers } from '../../api/PackageNameParsers'; @@ -24,15 +24,6 @@ export abstract class BaseShrinkwrapFile { return undefined; } - /** - * Return whether or not the committed shrinkwrap file should be forcibly rechecked for changes. - * - * @virtual - */ - public shouldForceRecheck(): boolean { - return false; - } - /** * Serializes and saves the shrinkwrap file to specified location */ @@ -47,7 +38,7 @@ export abstract class BaseShrinkwrapFile { */ public validate( packageManagerOptionsConfig: PackageManagerOptionsConfigurationBase, - policyOptions: IPolicyValidatorOptions + policyOptions: IShrinkwrapFilePolicyValidatorOptions ): void {} /** @@ -121,6 +112,57 @@ export abstract class BaseShrinkwrapFile { /** @virtual */ protected abstract getTopLevelDependencyVersion(dependencyName: string): DependencySpecifier | undefined; + /** + * Returns true if the specified workspace in the shrinkwrap file includes a package that would + * satisfy the specified SemVer version range. + * + * Consider this example: + * + * - project-a\ + * - lib-a@1.2.3 + * - lib-b@1.0.0 + * - lib-b@2.0.0 + * + * In this example, hasCompatibleWorkspaceDependency("lib-b", ">= 1.1.0", "workspace-key-for-project-a") + * would fail because it finds lib-b@1.0.0 which does not satisfy the pattern ">= 1.1.0". + * + * @virtual + */ + public hasCompatibleWorkspaceDependency( + dependencySpecifier: DependencySpecifier, + workspaceKey: string + ): boolean { + const shrinkwrapDependency: DependencySpecifier | undefined = this.getWorkspaceDependencyVersion( + dependencySpecifier, + workspaceKey + ); + return shrinkwrapDependency + ? this._checkDependencyVersion(dependencySpecifier, shrinkwrapDependency) + : false; + } + + /** + * Returns the list of keys to workspace projects specified in the shrinkwrap. + * Example: [ '../../apps/project1', '../../apps/project2' ] + * + * @virtual + */ + public abstract getWorkspaceKeys(): ReadonlyArray; + + /** + * Returns the key to the project in the workspace specified by the shrinkwrap. + * Example: '../../apps/project1' + * + * @virtual + */ + public abstract getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string; + + /** @virtual */ + protected abstract getWorkspaceDependencyVersion( + dependencySpecifier: DependencySpecifier, + workspaceKey: string + ): DependencySpecifier | undefined; + /** @virtual */ protected abstract serialize(): string; @@ -151,9 +193,9 @@ export abstract class BaseShrinkwrapFile { // // In this case, the shrinkwrap file will have a key equivalent to "npm:target-name@1.2.5", // and so we need to unwrap the target and compare "1.2.5" with "^1.2.3". - if (projectDependency.specifierType === 'alias') { + if (projectDependency.specifierType === DependencySpecifierType.Alias) { // Does the shrinkwrap install it as an alias? - if (shrinkwrapDependency.specifierType === 'alias') { + if (shrinkwrapDependency.specifierType === DependencySpecifierType.Alias) { // Does the shrinkwrap have the right package name? if (projectDependency.packageName === shrinkwrapDependency.packageName) { // Yes, the aliases match, so let's compare their targets in the logic below @@ -170,8 +212,8 @@ export abstract class BaseShrinkwrapFile { } switch (normalizedProjectDependency.specifierType) { - case 'version': - case 'range': + case DependencySpecifierType.Version: + case DependencySpecifierType.Range: return semver.satisfies( normalizedShrinkwrapDependency.versionSpecifier, normalizedProjectDependency.versionSpecifier diff --git a/apps/rush-lib/src/logic/base/BaseWorkspaceFile.ts b/apps/rush-lib/src/logic/base/BaseWorkspaceFile.ts new file mode 100644 index 00000000000..e3546d58ed5 --- /dev/null +++ b/apps/rush-lib/src/logic/base/BaseWorkspaceFile.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { FileSystem } from '@rushstack/node-core-library'; + +export interface IWorkspaceFileSaveOptions { + /** + * If there is an existing file, and the contents have not changed, then + * don't write anything; this preserves the old timestamp. + */ + onlyIfChanged?: boolean; + + /** + * Creates the folder recursively using FileSystem.ensureFolder() + * Defaults to false. + */ + ensureFolderExists?: boolean; +} + +/** + * This class is a parser for pnpm's pnpm-workspace.yaml file format. + */ +export abstract class BaseWorkspaceFile { + protected _alreadyWarnedSpecs: Set = new Set(); + + /** + * Serializes and saves the workspace file to specified location + */ + public save(filePath: string, options: IWorkspaceFileSaveOptions): void { + // Do we need to read the previous file contents? + let oldBuffer: Buffer | undefined = undefined; + if (options.onlyIfChanged && FileSystem.exists(filePath)) { + try { + oldBuffer = FileSystem.readFileToBuffer(filePath); + } catch (error) { + // Ignore this error, and try writing a new file. If that fails, then we should report that + // error instead. + } + } + + const newYaml: string = this.serialize(); + + const newBuffer: Buffer = Buffer.from(newYaml); // utf8 encoding happens here + + if (options.onlyIfChanged) { + // Has the file changed? + if (oldBuffer && Buffer.compare(newBuffer, oldBuffer) === 0) { + // Nothing has changed, so don't touch the file + return; + } + } + + FileSystem.writeFile(filePath, newBuffer.toString(), { + ensureFolderExists: options.ensureFolderExists + }); + } + + /** + * Adds a package path to the workspace file. + * + * @virtual + */ + public abstract addPackage(packagePath: string): void; + + /** @virtual */ + protected abstract serialize(): string; +} diff --git a/apps/rush-lib/src/logic/deploy/PnpmfileConfiguration.ts b/apps/rush-lib/src/logic/deploy/PnpmfileConfiguration.ts index 825bdee9b48..bb57a2d330f 100644 --- a/apps/rush-lib/src/logic/deploy/PnpmfileConfiguration.ts +++ b/apps/rush-lib/src/logic/deploy/PnpmfileConfiguration.ts @@ -39,7 +39,12 @@ export class PnpmfileConfiguration { log: (message: string) => {} }; - if (rushConfiguration.packageManager === 'pnpm') { + // Avoid setting the hook when not using pnpm or when using pnpm workspaces, since workspaces mode + // already transforms the package.json + if ( + rushConfiguration.packageManager === 'pnpm' && + (!rushConfiguration.pnpmOptions || !rushConfiguration.pnpmOptions.useWorkspaces) + ) { const pnpmFilePath: string = rushConfiguration.getPnpmfilePath(); if (FileSystem.exists(pnpmFilePath)) { console.log('Loading ' + path.relative(rushConfiguration.rushJsonFolder, pnpmFilePath)); diff --git a/apps/rush-lib/src/logic/installManager/InstallHelpers.ts b/apps/rush-lib/src/logic/installManager/InstallHelpers.ts new file mode 100644 index 00000000000..464a257fc01 --- /dev/null +++ b/apps/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -0,0 +1,377 @@ +// 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 os from 'os'; +import * as path from 'path'; +import * as semver from 'semver'; +import { + FileConstants, + FileSystem, + IPackageJson, + JsonFile, + LockFile, + MapExtensions +} from '@rushstack/node-core-library'; + +import { CommonVersionsConfiguration } from '../../api/CommonVersionsConfiguration'; +import { LastInstallFlag } from '../../api/LastInstallFlag'; +import { PackageJsonDependency } from '../../api/PackageJsonEditor'; +import { PackageManagerName } from '../../api/packageManager/PackageManager'; +import { RushConfiguration, IConfigurationEnvironment } from '../../api/RushConfiguration'; +import { RushConfigurationProject } from '../../api/RushConfigurationProject'; +import { RushGlobalFolder } from '../../api/RushGlobalFolder'; +import { Utilities } from '../../utilities/Utilities'; + +export class InstallHelpers { + /** + * Returns a map containing all preferred versions for a Rush project. + * Returns a map: dependency name --> version specifier + */ + public static collectPreferredVersions( + rushConfiguration: RushConfiguration, + options: { + explicitPreferredVersions?: Map; + variant?: string | undefined; + } = {} + ): Map { + // dependency name --> version specifier + const allExplicitPreferredVersions: Map = options.explicitPreferredVersions + ? options.explicitPreferredVersions + : rushConfiguration.getCommonVersions(options.variant).getAllPreferredVersions(); + + // dependency name --> version specifier + const allPreferredVersions: Map = new Map(); + + // Should we add implicitly preferred versions? + let useImplicitlyPinnedVersions: boolean; + if (rushConfiguration.commonVersions.implicitlyPreferredVersions !== undefined) { + // Use the manually configured setting + useImplicitlyPinnedVersions = rushConfiguration.commonVersions.implicitlyPreferredVersions; + } else { + // Default to true. + useImplicitlyPinnedVersions = true; + } + + if (useImplicitlyPinnedVersions) { + // Add in the implicitly preferred versions. + // These are any first-level dependencies for which we only consume a single version range + // (e.g. every package that depends on react uses an identical specifier) + const implicitlyPreferredVersions: Map< + string, + string + > = InstallHelpers.collectImplicitlyPreferredVersions(rushConfiguration, options); + MapExtensions.mergeFromMap(allPreferredVersions, implicitlyPreferredVersions); + } + + // Add in the explicitly preferred versions. + // Note that these take precedence over implicitly preferred versions. + MapExtensions.mergeFromMap(allPreferredVersions, allExplicitPreferredVersions); + return allPreferredVersions; + } + + /** + * Returns a map of all direct dependencies that only have a single semantic version specifier. + * Returns a map: dependency name --> version specifier + */ + public static collectImplicitlyPreferredVersions( + rushConfiguration: RushConfiguration, + options: { + variant?: string | undefined; + } = {} + ): Map { + // First, collect all the direct dependencies of all local projects, and their versions: + // direct dependency name --> set of version specifiers + const versionsForDependencies: Map> = new Map>(); + + rushConfiguration.projects.forEach((project: RushConfigurationProject) => { + InstallHelpers._collectVersionsForDependencies(rushConfiguration, { + versionsForDependencies, + dependencies: project.packageJsonEditor.dependencyList, + cyclicDependencies: project.cyclicDependencyProjects, + variant: options.variant + }); + + InstallHelpers._collectVersionsForDependencies(rushConfiguration, { + versionsForDependencies, + dependencies: project.packageJsonEditor.devDependencyList, + cyclicDependencies: project.cyclicDependencyProjects, + variant: options.variant + }); + }); + + // If any dependency has more than one version, then filter it out (since we don't know which version + // should be preferred). What remains will be the list of preferred dependencies. + // dependency --> version specifier + const implicitlyPreferred: Map = new Map(); + versionsForDependencies.forEach((versions: Set, dep: string) => { + if (versions.size === 1) { + const version: string = Array.from(versions)[0]; + implicitlyPreferred.set(dep, version); + } + }); + return implicitlyPreferred; + } + + public static generateCommonPackageJson( + rushConfiguration: RushConfiguration, + dependencies: Map = new Map() + ): void { + const commonPackageJson: IPackageJson = { + dependencies: {}, + description: 'Temporary file generated by the Rush tool', + name: 'rush-common', + private: true, + version: '0.0.0' + }; + + // Add any preferred versions to the top of the commonPackageJson + // do this in alphabetical order for simpler debugging + for (const dependency of Array.from(dependencies.keys()).sort()) { + commonPackageJson.dependencies![dependency] = dependencies.get(dependency)!; + } + + // Example: "C:\MyRepo\common\temp\package.json" + const commonPackageJsonFilename: string = path.join( + rushConfiguration.commonTempFolder, + FileConstants.PackageJson + ); + + // Don't update the file timestamp unless the content has changed, since "rush install" + // will consider this timestamp + JsonFile.save(commonPackageJson, commonPackageJsonFilename, { onlyIfChanged: true }); + } + + public static getPackageManagerEnvironment( + rushConfiguration: RushConfiguration, + options: { + debug?: boolean; + } = {} + ): NodeJS.ProcessEnv { + let configurationEnvironment: IConfigurationEnvironment | undefined = undefined; + + if (rushConfiguration.packageManager === 'npm') { + if (rushConfiguration.npmOptions && rushConfiguration.npmOptions.environmentVariables) { + configurationEnvironment = rushConfiguration.npmOptions.environmentVariables; + } + } else if (rushConfiguration.packageManager === 'pnpm') { + if (rushConfiguration.pnpmOptions && rushConfiguration.pnpmOptions.environmentVariables) { + configurationEnvironment = rushConfiguration.pnpmOptions.environmentVariables; + } + } else if (rushConfiguration.packageManager === 'yarn') { + if (rushConfiguration.yarnOptions && rushConfiguration.yarnOptions.environmentVariables) { + configurationEnvironment = rushConfiguration.yarnOptions.environmentVariables; + } + } + + return InstallHelpers._mergeEnvironmentVariables(process.env, configurationEnvironment, options); + } + + /** + * If the "(p)npm-local" symlink hasn't been set up yet, this creates it, installing the + * specified (P)npm version in the user's home directory if needed. + */ + public static async ensureLocalPackageManager( + rushConfiguration: RushConfiguration, + rushGlobalFolder: RushGlobalFolder, + maxInstallAttempts: number + ): Promise { + // Example: "C:\Users\YourName\.rush" + const rushUserFolder: string = rushGlobalFolder.nodeSpecificPath; + + if (!FileSystem.exists(rushUserFolder)) { + console.log('Creating ' + rushUserFolder); + FileSystem.ensureFolder(rushUserFolder); + } + + const packageManager: PackageManagerName = rushConfiguration.packageManager; + const packageManagerVersion: string = rushConfiguration.packageManagerToolVersion; + + const packageManagerAndVersion: string = `${packageManager}-${packageManagerVersion}`; + // Example: "C:\Users\YourName\.rush\pnpm-1.2.3" + const packageManagerToolFolder: string = path.join(rushUserFolder, packageManagerAndVersion); + + const packageManagerMarker: LastInstallFlag = new LastInstallFlag(packageManagerToolFolder, { + node: process.versions.node + }); + + console.log(`Trying to acquire lock for ${packageManagerAndVersion}`); + + const lock: LockFile = await LockFile.acquire(rushUserFolder, packageManagerAndVersion); + + console.log(`Acquired lock for ${packageManagerAndVersion}`); + + if (!packageManagerMarker.isValid() || lock.dirtyWhenAcquired) { + console.log(colors.bold(`Installing ${packageManager} version ${packageManagerVersion}${os.EOL}`)); + + // note that this will remove the last-install flag from the directory + Utilities.installPackageInDirectory({ + directory: packageManagerToolFolder, + packageName: packageManager, + version: rushConfiguration.packageManagerToolVersion, + tempPackageTitle: `${packageManager}-local-install`, + maxInstallAttempts: maxInstallAttempts, + // This is using a local configuration to install a package in a shared global location. + // Generally that's a bad practice, but in this case if we can successfully install + // the package at all, we can reasonably assume it's good for all the repositories. + // In particular, we'll assume that two different NPM registries cannot have two + // different implementations of the same version of the same package. + // This was needed for: https://github.com/microsoft/rushstack/issues/691 + commonRushConfigFolder: rushConfiguration.commonRushConfigFolder + }); + + console.log(`Successfully installed ${packageManager} version ${packageManagerVersion}`); + } else { + console.log(`Found ${packageManager} version ${packageManagerVersion} in ${packageManagerToolFolder}`); + } + + packageManagerMarker.create(); + + // Example: "C:\MyRepo\common\temp" + FileSystem.ensureFolder(rushConfiguration.commonTempFolder); + + // Example: "C:\MyRepo\common\temp\pnpm-local" + const localPackageManagerToolFolder: string = path.join( + rushConfiguration.commonTempFolder, + `${packageManager}-local` + ); + + console.log(os.EOL + 'Symlinking "' + localPackageManagerToolFolder + '"'); + console.log(' --> "' + packageManagerToolFolder + '"'); + + // We cannot use FileSystem.exists() to test the existence of a symlink, because it will + // return false for broken symlinks. There is no way to test without catching an exception. + try { + FileSystem.deleteFolder(localPackageManagerToolFolder); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + FileSystem.createSymbolicLinkJunction({ + linkTargetPath: packageManagerToolFolder, + newLinkPath: localPackageManagerToolFolder + }); + + lock.release(); + } + + // Helper for collectImplicitlyPreferredVersions() + private static _collectVersionsForDependencies( + rushConfiguration: RushConfiguration, + options: { + versionsForDependencies: Map>; + dependencies: ReadonlyArray; + cyclicDependencies: Set; + variant: string | undefined; + } + ): void { + const { variant, dependencies, versionsForDependencies, cyclicDependencies } = options; + + const commonVersions: CommonVersionsConfiguration = rushConfiguration.getCommonVersions(variant); + + const allowedAlternativeVersions: Map> = + commonVersions.allowedAlternativeVersions; + + for (const dependency of dependencies) { + const alternativesForThisDependency: ReadonlyArray = + allowedAlternativeVersions.get(dependency.name) || []; + + // For each dependency, collectImplicitlyPreferredVersions() is collecting the set of all version specifiers + // that appear across the repo. If there is only one version specifier, then that's the "preferred" one. + // However, there are a few cases where additional version specifiers can be safely ignored. + let ignoreVersion: boolean = false; + + // 1. If the version specifier was listed in "allowedAlternativeVersions", then it's never a candidate. + // (Even if it's the only version specifier anywhere in the repo, we still ignore it, because + // otherwise the rule would be difficult to explain.) + if (alternativesForThisDependency.indexOf(dependency.version) > 0) { + ignoreVersion = true; + } else { + // Is it a local project? + const localProject: RushConfigurationProject | undefined = rushConfiguration.getProjectByName( + dependency.name + ); + if (localProject) { + // 2. If it's a symlinked local project, then it's not a candidate, because the package manager will + // never even see it. + // However there are two ways that a local project can NOT be symlinked: + // - if the local project doesn't satisfy the referenced semver specifier; OR + // - if the local project was specified in "cyclicDependencyProjects" in rush.json + if ( + semver.satisfies(localProject.packageJsonEditor.version, dependency.version) && + !cyclicDependencies.has(dependency.name) + ) { + ignoreVersion = true; + } + } + + if (!ignoreVersion) { + InstallHelpers._updateVersionsForDependencies( + versionsForDependencies, + dependency.name, + dependency.version + ); + } + } + } + } + + // Helper for collectImplicitlyPreferredVersions() + private static _updateVersionsForDependencies( + versionsForDependencies: Map>, + dependency: string, + version: string + ): void { + if (!versionsForDependencies.has(dependency)) { + versionsForDependencies.set(dependency, new Set()); + } + versionsForDependencies.get(dependency)!.add(version); + } + + // Helper for getPackageManagerEnvironment + private static _mergeEnvironmentVariables( + baseEnv: NodeJS.ProcessEnv, + environmentVariables?: IConfigurationEnvironment, + options: { + debug?: boolean; + } = {} + ): NodeJS.ProcessEnv { + const packageManagerEnv: NodeJS.ProcessEnv = baseEnv; + + if (environmentVariables) { + // eslint-disable-next-line guard-for-in + for (const envVar in environmentVariables) { + let setEnvironmentVariable: boolean = true; + console.log(`\nProcessing definition for environment variable: ${envVar}`); + + if (baseEnv.hasOwnProperty(envVar)) { + setEnvironmentVariable = false; + console.log(`Environment variable already defined:`); + console.log(` Name: ${envVar}`); + console.log(` Existing value: ${baseEnv[envVar]}`); + console.log(` Value set in rush.json: ${environmentVariables[envVar].value}`); + + if (environmentVariables[envVar].override) { + setEnvironmentVariable = true; + console.log(`Overriding the environment variable with the value set in rush.json.`); + } else { + console.log(colors.yellow(`WARNING: Not overriding the value of the environment variable.`)); + } + } + + if (setEnvironmentVariable) { + if (options.debug) { + console.log(`Setting environment variable for package manager.`); + console.log(` Name: ${envVar}`); + console.log(` Value: ${environmentVariables[envVar].value}`); + } + packageManagerEnv[envVar] = environmentVariables[envVar].value; + } + } + } + + return packageManagerEnv; + } +} diff --git a/apps/rush-lib/src/logic/installManager/RushInstallManager.ts b/apps/rush-lib/src/logic/installManager/RushInstallManager.ts new file mode 100644 index 00000000000..731946d9e6b --- /dev/null +++ b/apps/rush-lib/src/logic/installManager/RushInstallManager.ts @@ -0,0 +1,722 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as glob from 'glob'; +import * as colors from 'colors'; +import * as os from 'os'; +import * as path from 'path'; +import * as semver from 'semver'; +import * as tar from 'tar'; +import * as globEscape from 'glob-escape'; +import { + JsonFile, + Text, + FileSystem, + FileConstants, + Sort, + PosixModeBits, + InternalError +} from '@rushstack/node-core-library'; + +import { BaseInstallManager } from '../base/BaseInstallManager'; +import { BaseShrinkwrapFile } from '../../logic/base/BaseShrinkwrapFile'; +import { IRushTempPackageJson } from '../../logic/base/BasePackage'; +import { RushConfigurationProject } from '../../api/RushConfigurationProject'; +import { RushConstants } from '../../logic/RushConstants'; +import { Stopwatch } from '../../utilities/Stopwatch'; +import { Utilities } from '../../utilities/Utilities'; +import { PackageJsonEditor, DependencyType, PackageJsonDependency } from '../../api/PackageJsonEditor'; +import { DependencySpecifier, DependencySpecifierType } from '../DependencySpecifier'; +import { InstallHelpers } from './InstallHelpers'; +import { AlreadyReportedError } from '../../utilities/AlreadyReportedError'; + +/** + * The "noMtime" flag is new in tar@4.4.1 and not available yet for \@types/tar. + * As a temporary workaround, augment the type. + */ +declare module 'tar' { + // eslint-disable-next-line @typescript-eslint/naming-convention + export interface CreateOptions { + /** + * "Set to true to omit writing mtime values for entries. Note that this prevents using other + * mtime-based features like tar.update or the keepNewer option with the resulting tar archive." + */ + noMtime?: boolean; + } +} + +/** + * This class implements common logic between "rush install" and "rush update". + */ +export class RushInstallManager extends BaseInstallManager { + /** + * Regenerates the common/package.json and all temp_modules projects. + * 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 + */ + protected 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 { + let workspaceKeys: ReadonlyArray = []; + try { + workspaceKeys = shrinkwrapFile.getWorkspaceKeys(); + } catch { + // Swallow errors since not all shrinkwrap types support workspaces + } + if (workspaceKeys.length !== 0 && !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 = 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._findOrphanedTempProjects(shrinkwrapFile)) { + // If there are any orphaned projects, then "npm install" would fail because the shrinkwrap + // contains references such as "resolved": "file:projects\\project1" that refer to nonexistent + // file paths. + shrinkwrapIsUpToDate = false; + } + } + + // dependency name --> version specifier + const commonDependencies: Map = InstallHelpers.collectPreferredVersions( + this.rushConfiguration, + { + explicitPreferredVersions: allExplicitPreferredVersions, + variant: 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._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 = new Map(); + + // 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 "rush link" 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; + + let tryReusingPackageVersionsFromShrinkwrap: boolean = true; + + if (this.rushConfiguration.packageManager === 'pnpm') { + // Shrinkwrap churn optimization doesn't make sense when --frozen-lockfile is true + tryReusingPackageVersionsFromShrinkwrap = !this.rushConfiguration.experimentsConfiguration + .configuration.usePnpmFrozenLockfileForRushInstall; + } + + if (shrinkwrapFile) { + if ( + !shrinkwrapFile.tryEnsureCompatibleDependency( + dependencySpecifier, + rushProject.tempProjectName, + tryReusingPackageVersionsFromShrinkwrap + ) + ) { + shrinkwrapWarnings.push( + `Missing dependency "${packageName}" (${packageVersion}) required by "${rushProject.packageName}"` + ); + shrinkwrapIsUpToDate = false; + } + } + } + + // Example: "C:\MyRepo\common\temp\projects\my-project-2" + const tempProjectFolder: string = this._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._createTempProjectTarball(rushProject); + + console.log(`Updating ${tarballFile}`); + } catch (error) { + console.log(colors.yellow(error)); + // delete everything in case of any error + FileSystem.deleteFile(tarballFile); + FileSystem.deleteFile(tempPackageJsonFilename); + } + } + + // Remove the workspace file if it exists + if (this.rushConfiguration.packageManager === 'pnpm') { + const workspaceFilePath: string = path.join( + this.rushConfiguration.commonTempFolder, + 'pnpm-workspace.yaml' + ); + if (FileSystem.exists(workspaceFilePath)) { + FileSystem.deleteFile(workspaceFilePath); + } + } + + // 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.' + ) + ); + } + } + + // Write the common package.json + InstallHelpers.generateCommonPackageJson(this.rushConfiguration, commonDependencies); + + stopwatch.stop(); + console.log(`Finished creating temporary modules (${stopwatch.toString()})`); + + return { shrinkwrapIsUpToDate, shrinkwrapWarnings }; + } + + private _getTempProjectFolder(rushProject: RushConfigurationProject): string { + const unscopedTempProjectName: string = rushProject.unscopedTempProjectName; + return path.join( + this.rushConfiguration.commonTempFolder, + RushConstants.rushTempProjectsFolderName, + unscopedTempProjectName + ); + } + + /** + * Deletes the existing tarball and creates a tarball for the given rush project + */ + private _createTempProjectTarball(rushProject: RushConfigurationProject): void { + const tarballFile: string = this._getTarballFilePath(rushProject); + const tempProjectFolder: string = this._getTempProjectFolder(rushProject); + + FileSystem.deleteFile(tarballFile); + + // NPM expects the root of the tarball to have a directory called 'package' + const npmPackageFolder: string = 'package'; + + const tarOptions: tar.CreateOptions = { + gzip: true, + file: tarballFile, + cwd: tempProjectFolder, + portable: true, + noMtime: true, + noPax: true, + sync: true, + prefix: npmPackageFolder, + filter: (path: string, stat: tar.FileStat): boolean => { + if ( + !this.rushConfiguration.experimentsConfiguration.configuration.noChmodFieldInTarHeaderNormalization + ) { + stat.mode = + // eslint-disable-next-line no-bitwise + (stat.mode & ~0x1ff) | PosixModeBits.AllRead | PosixModeBits.UserWrite | PosixModeBits.AllExecute; + } + return true; + } + } as tar.CreateOptions; + // create the new tarball + tar.create(tarOptions, [FileConstants.PackageJson]); + } + + private _revertWorkspaceNotation(dependency: PackageJsonDependency): boolean { + const specifier: DependencySpecifier = new DependencySpecifier(dependency.name, dependency.version); + if (specifier.specifierType !== DependencySpecifierType.Workspace) { + return false; + } + // Replace workspace notation with the supplied version range + if (specifier.versionSpecifier === '*') { + // When converting to workspaces, exact package versions are replaced with a '*', so undo this + const localProject: RushConfigurationProject | undefined = this.rushConfiguration.getProjectByName( + specifier.packageName + ); + if (!localProject) { + throw new InternalError(`Could not find local project with package name ${specifier.packageName}`); + } + dependency.setVersion(localProject.packageJson.version); + } else { + dependency.setVersion(specifier.versionSpecifier); + } + return true; + } + + /** + * Check whether or not the install is already valid, and therefore can be skipped. + * + * @override + */ + protected canSkipInstall(lastModifiedDate: Date): boolean { + console.log( + os.EOL + + colors.bold( + `Checking ${RushConstants.nodeModulesFolderName} in ${this.rushConfiguration.commonTempFolder}` + ) + + os.EOL + ); + + // Based on timestamps, can we skip this install entirely? + const potentiallyChangedFiles: string[] = []; + + // Consider the timestamp on the node_modules folder; if someone tampered with it + // or deleted it entirely, then we can't skip this install + potentiallyChangedFiles.push( + path.join(this.rushConfiguration.commonTempFolder, RushConstants.nodeModulesFolderName) + ); + + // Additionally, if they pulled an updated npm-shrinkwrap.json file from Git, + // then we can't skip this install + potentiallyChangedFiles.push(this.rushConfiguration.getCommittedShrinkwrapFilename(this.options.variant)); + + // Add common-versions.json file to the potentially changed files list. + potentiallyChangedFiles.push(this.rushConfiguration.getCommonVersionsFilePath(this.options.variant)); + + if (this.rushConfiguration.packageManager === 'pnpm') { + // If the repo is using pnpmfile.js, consider that also + const pnpmFileFilename: string = this.rushConfiguration.getPnpmfilePath(this.options.variant); + + if (FileSystem.exists(pnpmFileFilename)) { + potentiallyChangedFiles.push(pnpmFileFilename); + } + } + + // Also consider timestamps for all the temp tarballs. (createTempModulesAndCheckShrinkwrap() will + // carefully preserve these timestamps unless something has changed.) + // Example: "C:\MyRepo\common\temp\projects\my-project-2.tgz" + potentiallyChangedFiles.push( + ...this.rushConfiguration.projects.map((x) => { + return this._getTarballFilePath(x); + }) + ); + + return Utilities.isFileTimestampCurrent(lastModifiedDate, potentiallyChangedFiles); + } + + /** + * Runs "npm/pnpm/yarn install" in the "common/temp" folder. + * + * @override + */ + protected async installAsync(cleanInstall: boolean): Promise { + // Since we are actually running npm/pnpm/yarn install, recreate all the temp project tarballs. + // This ensures that any existing tarballs with older header bits will be regenerated. + // It is safe to assume that temp project pacakge.jsons already exist. + for (const rushProject of this.rushConfiguration.projects) { + this._createTempProjectTarball(rushProject); + } + + // NOTE: The PNPM store is supposed to be transactionally safe, so we don't delete it automatically. + // The user must request that via the command line. + if (cleanInstall) { + if (this.rushConfiguration.packageManager === 'npm') { + console.log(`Deleting the "npm-cache" folder`); + // This is faster and more thorough than "npm cache clean" + this.installRecycler.moveFolder(this.rushConfiguration.npmCacheFolder); + + console.log(`Deleting the "npm-tmp" folder`); + this.installRecycler.moveFolder(this.rushConfiguration.npmTmpFolder); + } + } + + // Example: "C:\MyRepo\common\temp\npm-local\node_modules\.bin\npm" + const packageManagerFilename: string = this.rushConfiguration.packageManagerToolFilename; + + const packageManagerEnv: NodeJS.ProcessEnv = InstallHelpers.getPackageManagerEnvironment( + this.rushConfiguration, + this.options + ); + + const commonNodeModulesFolder: string = path.join( + this.rushConfiguration.commonTempFolder, + RushConstants.nodeModulesFolderName + ); + + // Is there an existing "node_modules" folder to consider? + if (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 + console.log('Deleting files from ' + commonNodeModulesFolder); + + this.installRecycler.moveFolder(commonNodeModulesFolder); + + Utilities.createFolderWithRetry(commonNodeModulesFolder); + } else { + // NO: Prepare to do an incremental install in the "node_modules" folder + + // note: it is not necessary to run "prune" with pnpm + if (this.rushConfiguration.packageManager === 'npm') { + console.log( + `Running "${this.rushConfiguration.packageManager} prune"` + + ` in ${this.rushConfiguration.commonTempFolder}` + ); + const args: string[] = ['prune']; + this.pushConfigurationArgs(args, this.options); + + Utilities.executeCommandWithRetry( + { + command: packageManagerFilename, + args: args, + workingDirectory: this.rushConfiguration.commonTempFolder, + environment: packageManagerEnv + }, + this.options.maxInstallAttempts + ); + + // Delete the (installed image of) the temp projects, since "npm install" does not + // detect changes for "file:./" references. + // We recognize the temp projects by their names, which always start with "rush-". + + // Example: "C:\MyRepo\common\temp\node_modules\@rush-temp" + const pathToDeleteWithoutStar: string = path.join( + commonNodeModulesFolder, + RushConstants.rushTempNpmScope + ); + console.log(`Deleting ${pathToDeleteWithoutStar}\\*`); + // Glob can't handle Windows paths + const normalizedpathToDeleteWithoutStar: string = Text.replaceAll( + pathToDeleteWithoutStar, + '\\', + '/' + ); + + // Example: "C:/MyRepo/common/temp/node_modules/@rush-temp/*" + for (const tempModulePath of glob.sync(globEscape(normalizedpathToDeleteWithoutStar) + '/*')) { + // We could potentially use AsyncRecycler here, but in practice these folders tend + // to be very small + Utilities.dangerouslyDeletePath(tempModulePath); + } + } + } + } + + if (this.rushConfiguration.packageManager === 'yarn') { + // Yarn does not correctly detect changes to a tarball, so we need to forcibly clear its cache + const yarnRushTempCacheFolder: string = path.join( + this.rushConfiguration.yarnCacheFolder, + 'v2', + 'npm-@rush-temp' + ); + if (FileSystem.exists(yarnRushTempCacheFolder)) { + console.log('Deleting ' + yarnRushTempCacheFolder); + Utilities.dangerouslyDeletePath(yarnRushTempCacheFolder); + } + } + + // Run "npm install" in the common folder + const installArgs: string[] = ['install']; + this.pushConfigurationArgs(installArgs, this.options); + + console.log( + os.EOL + + colors.bold( + `Running "${this.rushConfiguration.packageManager} install" in` + + ` ${this.rushConfiguration.commonTempFolder}` + ) + + os.EOL + ); + + // If any diagnostic options were specified, then show the full command-line + if (this.options.debug || this.options.collectLogFile || this.options.networkConcurrency) { + console.log( + os.EOL + + colors.green('Invoking package manager: ') + + FileSystem.getRealPath(packageManagerFilename) + + ' ' + + installArgs.join(' ') + + os.EOL + ); + } + + try { + Utilities.executeCommandWithRetry( + { + command: packageManagerFilename, + args: installArgs, + workingDirectory: this.rushConfiguration.commonTempFolder, + environment: packageManagerEnv, + suppressOutput: false + }, + this.options.maxInstallAttempts, + () => { + if (this.rushConfiguration.packageManager === 'pnpm') { + console.log(colors.yellow(`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.createFolderWithRetry(commonNodeModulesFolder); + } + } + ); + } catch (error) { + // All the install attempts failed. + + if ( + this.rushConfiguration.packageManager === 'pnpm' && + this.rushConfiguration.pnpmOptions.pnpmStore === 'local' + ) { + // If the installation has failed even after the retries, then pnpm store may + // have got into a corrupted, irrecoverable state. Delete the store so that a + // future install can create the store afresh. + console.log(colors.yellow(`Deleting the "pnpm-store" folder`)); + this.installRecycler.moveFolder(this.rushConfiguration.pnpmOptions.pnpmStorePath); + } + + throw error; + } + + if (this.rushConfiguration.packageManager === 'npm') { + console.log(os.EOL + colors.bold('Running "npm shrinkwrap"...')); + const npmArgs: string[] = ['shrinkwrap']; + this.pushConfigurationArgs(npmArgs, this.options); + Utilities.executeCommand({ + command: this.rushConfiguration.packageManagerToolFilename, + args: npmArgs, + workingDirectory: this.rushConfiguration.commonTempFolder + }); + console.log('"npm shrinkwrap" completed' + os.EOL); + + this._fixupNpm5Regression(); + } + } + + /** + * Gets the path to the tarball + * Example: "C:\MyRepo\common\temp\projects\my-project-2.tgz" + */ + private _getTarballFilePath(project: RushConfigurationProject): string { + return path.join( + this.rushConfiguration.commonTempFolder, + RushConstants.rushTempProjectsFolderName, + `${project.unscopedTempProjectName}.tgz` + ); + } + + /** + * This is a workaround for a bug introduced in NPM 5 (and still unfixed as of NPM 5.5.1): + * https://github.com/npm/npm/issues/19006 + * + * The regression is that "npm install" sets the package.json "version" field for the + * @rush-temp projects to a value like "file:projects/example.tgz", when it should be "0.0.0". + * This causes "rush link" to fail later, when read-package-tree tries to parse the bad version. + * The error looks like this: + * + * ERROR: Failed to parse package.json for foo: Invalid version: "file:projects/example.tgz" + * + * Our workaround is to rewrite the package.json files for each of the @rush-temp projects + * in the node_modules folder, after "npm install" completes. + */ + private _fixupNpm5Regression(): void { + const pathToDeleteWithoutStar: string = path.join( + this.rushConfiguration.commonTempFolder, + 'node_modules', + RushConstants.rushTempNpmScope + ); + // Glob can't handle Windows paths + const normalizedPathToDeleteWithoutStar: string = Text.replaceAll(pathToDeleteWithoutStar, '\\', '/'); + + let anyChanges: boolean = false; + + // Example: "C:/MyRepo/common/temp/node_modules/@rush-temp/*/package.json" + for (const packageJsonPath of glob.sync( + globEscape(normalizedPathToDeleteWithoutStar) + '/*/package.json' + )) { + // Example: "C:/MyRepo/common/temp/node_modules/@rush-temp/example/package.json" + const packageJsonObject: IRushTempPackageJson = JsonFile.load(packageJsonPath); + + // The temp projects always use "0.0.0" as their version + packageJsonObject.version = '0.0.0'; + + if (JsonFile.save(packageJsonObject, packageJsonPath, { onlyIfChanged: true })) { + anyChanges = true; + } + } + + if (anyChanges) { + console.log(os.EOL + colors.yellow(Utilities.wrapWords(`Applied workaround for NPM 5 bug`)) + os.EOL); + } + } + + /** + * Checks for temp projects that exist in the shrinkwrap file, but don't exist + * in rush.json. This might occur, e.g. if a project was recently deleted or renamed. + * + * @returns true if orphans were found, or false if everything is okay + */ + private _findOrphanedTempProjects(shrinkwrapFile: BaseShrinkwrapFile): boolean { + // We can recognize temp projects because they are under the "@rush-temp" NPM scope. + for (const tempProjectName of shrinkwrapFile.getTempProjectNames()) { + if (!this.rushConfiguration.findProjectByTempName(tempProjectName)) { + console.log( + os.EOL + + colors.yellow( + Utilities.wrapWords( + `Your ${this.rushConfiguration.shrinkwrapFilePhrase} references a project "${tempProjectName}" which no longer exists.` + ) + ) + + os.EOL + ); + return true; // found one + } + } + + return false; // none found + } +} diff --git a/apps/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/apps/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts new file mode 100644 index 00000000000..584c35c6c1a --- /dev/null +++ b/apps/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -0,0 +1,509 @@ +// 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 os from 'os'; +import * as path from 'path'; +import * as semver from 'semver'; +import { FileSystem, InternalError, MapExtensions, JsonFile } from '@rushstack/node-core-library'; + +import { AlreadyReportedError } from '../../utilities/AlreadyReportedError'; +import { BaseInstallManager, IInstallManagerOptions } from '../base/BaseInstallManager'; +import { BaseShrinkwrapFile } from '../../logic/base/BaseShrinkwrapFile'; +import { DependencySpecifier, DependencySpecifierType } from '../DependencySpecifier'; +import { PackageJsonEditor, DependencyType } from '../../api/PackageJsonEditor'; +import { PnpmWorkspaceFile } from '../pnpm/PnpmWorkspaceFile'; +import { RushConfigurationProject } from '../../api/RushConfigurationProject'; +import { RushConstants } from '../../logic/RushConstants'; +import { Stopwatch } from '../../utilities/Stopwatch'; +import { Utilities } from '../../utilities/Utilities'; +import { InstallHelpers } from './InstallHelpers'; +import { CommonVersionsConfiguration } from '../../api/CommonVersionsConfiguration'; +import { RepoStateFile } from '../RepoStateFile'; +import { IPnpmfileShimSettings } from '../pnpm/IPnpmfileShimSettings'; + +/** + * This class implements common logic between "rush install" and "rush update". + */ +export class WorkspaceInstallManager extends BaseInstallManager { + /** + * @override + */ + public async doInstall(): Promise { + // Workspaces do not support the no-link option, so throw if this is passed + if (this.options.noLink) { + console.log(); + console.log( + colors.red( + 'The "--no-link" option was provided but is not supported when using workspaces. Run the command again ' + + 'without specifying this argument.' + ) + ); + throw new AlreadyReportedError(); + } + + await super.doInstall(); + } + + /** + * 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 + */ + protected async prepareCommonTempAsync( + shrinkwrapFile: BaseShrinkwrapFile | undefined + ): Promise<{ shrinkwrapIsUpToDate: boolean; shrinkwrapWarnings: string[] }> { + const stopwatch: Stopwatch = Stopwatch.start(); + + console.log( + os.EOL + colors.bold('Updating workspace files in ' + this.rushConfiguration.commonTempFolder) + ); + + // Shim support for common versions resolution into the pnpmfile. When using workspaces, there are no + // "hoisted" packages, so we need to apply the correct versions to indirect dependencies through the + // pnpmfile. + if (this.rushConfiguration.packageManager === 'pnpm') { + const tempPnpmFilePath: string = path.join( + this.rushConfiguration.commonTempFolder, + RushConstants.pnpmfileFilename + ); + await this.createShimPnpmfileAsync(tempPnpmFilePath); + } + + 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.getWorkspaceKeys().length === 0 && + this.rushConfiguration.projects.length !== 0 && + !this.options.fullUpgrade + ) { + console.log(); + console.log( + colors.red( + 'The shrinkwrap file has not been updated to support workspaces. Run "rush update --full" to update ' + + 'the shrinkwrap file.' + ) + ); + throw new AlreadyReportedError(); + } + } + + if (shrinkwrapFile) { + if (this._findOrphanedWorkspaceProjects(shrinkwrapFile)) { + // If there are any orphaned projects, then install would fail because the shrinkwrap + // contains references that refer to nonexistent file paths. + shrinkwrapIsUpToDate = false; + } + } + + // If preferred versions have been updated, then we can't be certain of the state of the shrinkwrap + const repoState: RepoStateFile = this.rushConfiguration.getRepoState(this.options.variant); + const commonVersions: CommonVersionsConfiguration = this.rushConfiguration.getCommonVersions( + this.options.variant + ); + if (repoState.preferredVersionsHash !== commonVersions.getPreferredVersionsHash()) { + shrinkwrapWarnings.push( + `Preferred versions from ${RushConstants.commonVersionsFilename} have been modified.` + ); + shrinkwrapIsUpToDate = false; + } + + // To generate the workspace file, we will add each project to the file as we loop through and validate + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile( + path.join(this.rushConfiguration.commonTempFolder, 'pnpm-workspace.yaml') + ); + + // 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) { + const packageJson: PackageJsonEditor = rushProject.packageJsonEditor; + workspaceFile.addPackage(rushProject.projectFolder); + + for (const { name, version, dependencyType } of [ + ...packageJson.dependencyList, + ...packageJson.devDependencyList + ]) { + const dependencySpecifier: DependencySpecifier = new DependencySpecifier(name, version); + + // Is there a locally built Rush project that could satisfy this dependency? + const referencedLocalProject: + | RushConfigurationProject + | undefined = this.rushConfiguration.getProjectByName(name); + + // 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 === DependencySpecifierType.Version || + dependencySpecifier.specifierType === DependencySpecifierType.Range) && + referencedLocalProject && + !rushProject.cyclicDependencyProjects.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 + ) + ) { + console.log(); + console.log( + colors.red( + `"${rushProject.packageName}" depends on package "${name}" (${version}) which exists ` + + 'within the workspace but cannot be fulfilled with the specified version range. Either ' + + 'specify a valid version range, or add the package as a cyclic dependency.' + ) + ); + throw new AlreadyReportedError(); + } + + if (!this.options.allowShrinkwrapUpdates) { + console.log(); + console.log( + colors.red( + `"${rushProject.packageName}" depends on package "${name}" (${version}) which exists within ` + + 'the workspace. Run "rush update" to update workspace references for this package.' + ) + ); + throw new AlreadyReportedError(); + } + + if (this.options.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: string = + !!semver.validRange(dependencySpecifier.versionSpecifier) && + !semver.valid(dependencySpecifier.versionSpecifier) + ? dependencySpecifier.versionSpecifier + : '*'; + packageJson.addOrUpdateDependency(name, `workspace:${workspaceRange}`, dependencyType); + shrinkwrapIsUpToDate = false; + continue; + } + } else if (dependencySpecifier.specifierType === DependencySpecifierType.Workspace) { + // Already specified as a local project. Allow the package manager to validate this + continue; + } + + // Allow the package manager to handle peer dependency resolution, since this is simply a constraint + // enforced by the package manager + if (dependencyType === DependencyType.Peer) { + continue; + } + + // It is not a local dependency, validate that it is compatible + if ( + shrinkwrapFile && + !shrinkwrapFile.hasCompatibleWorkspaceDependency( + dependencySpecifier, + shrinkwrapFile.getWorkspaceKeyByPath( + this.rushConfiguration.commonTempFolder, + rushProject.projectFolder + ) + ) + ) { + shrinkwrapWarnings.push( + `Missing dependency "${name}" (${version}) required by "${rushProject.packageName}"` + ); + shrinkwrapIsUpToDate = false; + } + } + + // 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 workspace packages which did not use "workspace:" ` + + 'notation. The package.json has been modified and must be committed to source control.' + ) + ); + } + } + + // Write the common package.json + InstallHelpers.generateCommonPackageJson(this.rushConfiguration); + + // 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 }); + + stopwatch.stop(); + console.log(`Finished creating workspace (${stopwatch.toString()})`); + + return { shrinkwrapIsUpToDate, shrinkwrapWarnings }; + } + + protected canSkipInstall(lastModifiedDate: Date): boolean { + console.log( + os.EOL + + colors.bold( + `Checking ${RushConstants.nodeModulesFolderName} in ${this.rushConfiguration.commonTempFolder}` + ) + + os.EOL + ); + + // Based on timestamps, can we skip this install entirely? + const potentiallyChangedFiles: string[] = []; + + // Consider the timestamp on the node_modules folder; if someone tampered with it + // or deleted it entirely, then we can't skip this install + potentiallyChangedFiles.push( + path.join(this.rushConfiguration.commonTempFolder, RushConstants.nodeModulesFolderName) + ); + + // Additionally, if they pulled an updated shrinkwrap file from Git, then we can't skip this install + potentiallyChangedFiles.push(this.rushConfiguration.getCommittedShrinkwrapFilename(this.options.variant)); + + // Add common-versions.json file to the potentially changed files list. + potentiallyChangedFiles.push(this.rushConfiguration.getCommonVersionsFilePath(this.options.variant)); + + if (this.rushConfiguration.packageManager === 'pnpm') { + // If the repo is using pnpmfile.js, consider that also + const pnpmFileFilename: string = this.rushConfiguration.getPnpmfilePath(this.options.variant); + + if (FileSystem.exists(pnpmFileFilename)) { + potentiallyChangedFiles.push(pnpmFileFilename); + } + + // Add workspace file. This file is only modified when workspace packages change. + const pnpmWorkspaceFilename: string = path.join( + this.rushConfiguration.commonTempFolder, + 'pnpm-workspace.yaml' + ); + + if (FileSystem.exists(pnpmWorkspaceFilename)) { + potentiallyChangedFiles.push(); + } + } + + // Also consider timestamps for all the project node_modules folders. + // Example: "C:\MyRepo\projects\projectA\node_modules" + potentiallyChangedFiles.push( + ...this.rushConfiguration.projects.map((x) => { + return path.join(x.projectFolder, RushConstants.nodeModulesFolderName); + }) + ); + + // NOTE: If any of the potentiallyChangedFiles does not exist, then isFileTimestampCurrent() + // returns false. + return Utilities.isFileTimestampCurrent(lastModifiedDate, potentiallyChangedFiles); + } + + /** + * Runs "npm install" in the common folder. + */ + protected async installAsync(cleanInstall: boolean): Promise { + // Example: "C:\MyRepo\common\temp\npm-local\node_modules\.bin\npm" + const packageManagerFilename: string = this.rushConfiguration.packageManagerToolFilename; + + const packageManagerEnv: NodeJS.ProcessEnv = InstallHelpers.getPackageManagerEnvironment( + this.rushConfiguration, + this.options + ); + + const commonNodeModulesFolder: string = path.join( + this.rushConfiguration.commonTempFolder, + RushConstants.nodeModulesFolderName + ); + + // Is there an existing "node_modules" folder to consider? + if (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 + console.log('Deleting files from ' + commonNodeModulesFolder); + + this.installRecycler.moveFolder(commonNodeModulesFolder); + + Utilities.createFolderWithRetry(commonNodeModulesFolder); + } + } + + // Run "npm install" in the common folder + const installArgs: string[] = ['install']; + this.pushConfigurationArgs(installArgs, this.options); + + console.log( + os.EOL + + colors.bold( + `Running "${this.rushConfiguration.packageManager} install" in` + + ` ${this.rushConfiguration.commonTempFolder}` + ) + + os.EOL + ); + + // If any diagnostic options were specified, then show the full command-line + if (this.options.debug || this.options.collectLogFile || this.options.networkConcurrency) { + console.log( + os.EOL + + colors.green('Invoking package manager: ') + + FileSystem.getRealPath(packageManagerFilename) + + ' ' + + installArgs.join(' ') + + os.EOL + ); + } + + try { + Utilities.executeCommandWithRetry( + { + command: packageManagerFilename, + args: installArgs, + workingDirectory: this.rushConfiguration.commonTempFolder, + environment: packageManagerEnv, + suppressOutput: false + }, + this.options.maxInstallAttempts, + () => { + if (this.rushConfiguration.packageManager === 'pnpm') { + console.log(colors.yellow(`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.createFolderWithRetry(commonNodeModulesFolder); + } + } + ); + + // 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: string[] = [ + path.join(this.rushConfiguration.commonTempFolder, RushConstants.nodeModulesFolderName), + ...this.rushConfiguration.projects.map((x) => { + return path.join(x.projectFolder, RushConstants.nodeModulesFolderName); + }) + ]; + + for (const nodeModulesFolder of projectNodeModulesFolders) { + FileSystem.ensureFolder(nodeModulesFolder); + } + } catch (error) { + // All the install attempts failed. + + if ( + this.rushConfiguration.packageManager === 'pnpm' && + this.rushConfiguration.pnpmOptions.pnpmStore === 'local' + ) { + // If the installation has failed even after the retries, then pnpm store may + // have got into a corrupted, irrecoverable state. Delete the store so that a + // future install can create the store afresh. + console.log(colors.yellow(`Deleting the "pnpm-store" folder`)); + this.installRecycler.moveFolder(this.rushConfiguration.pnpmOptions.pnpmStorePath); + } + + throw error; + } + + console.log(''); + } + + /** + * Preferred versions are supported using pnpmfile by substituting any dependency version specifier + * for the preferred version during package resolution. This is only done if the preferred version range + * is a subset of the dependency version range. Allowed alternate versions are not modified. The pnpmfile + * shim will subsequently call into the provided pnpmfile, if one exists. + */ + protected async createShimPnpmfileAsync(filename: string): Promise { + const pnpmfileDir: string = path.dirname(filename); + let pnpmfileExists: boolean = false; + try { + // Attempt to move the existing pnpmfile if there is one + await FileSystem.moveAsync({ + sourcePath: filename, + destinationPath: path.join(pnpmfileDir, 'clientPnpmfile.js') + }); + pnpmfileExists = true; + } catch (error) { + if (!FileSystem.isNotExistError(error)) { + throw error; + } + } + + const pnpmfileShimSettings: IPnpmfileShimSettings = { + allPreferredVersions: MapExtensions.toObject( + InstallHelpers.collectPreferredVersions(this.rushConfiguration, this.options) + ), + allowedAlternativeVersions: MapExtensions.toObject( + this.rushConfiguration.getCommonVersions(this.options.variant).allowedAlternativeVersions + ), + semverPath: require.resolve('semver'), + useClientPnpmfile: pnpmfileExists + }; + + // Write the settings to be consumed by the pnpmfile + await JsonFile.saveAsync(pnpmfileShimSettings, path.resolve(pnpmfileDir, 'pnpmfileSettings.json'), { + ensureFolderExists: true + }); + + // Copy the shim pnpmfile to the original path + await FileSystem.copyFileAsync({ + sourcePath: path.resolve(__dirname, '..', 'pnpm', 'PnpmfileShim.js'), + destinationPath: filename + }); + } + + /** + * Used when invoking the NPM tool. Appends the common configuration options + * to the command-line. + */ + protected pushConfigurationArgs(args: string[], options: IInstallManagerOptions): void { + super.pushConfigurationArgs(args, options); + + // Add workspace-specific args + if (this.rushConfiguration.packageManager === 'pnpm') { + args.push('--recursive'); + args.push('--link-workspace-packages', 'false'); + } + } + + /** + * Checks for projects that exist in the shrinkwrap file, but don't exist + * in rush.json. This might occur, e.g. if a project was recently deleted or renamed. + * + * @returns true if orphans were found, or false if everything is okay + */ + private _findOrphanedWorkspaceProjects(shrinkwrapFile: BaseShrinkwrapFile): boolean { + for (const workspaceKey of shrinkwrapFile.getWorkspaceKeys()) { + // Look for the RushConfigurationProject using the workspace key + let rushProjectPath: string; + if (this.rushConfiguration.packageManager === 'pnpm') { + // PNPM workspace keys are relative paths from the workspace root, which is the common temp folder + rushProjectPath = path.resolve(this.rushConfiguration.commonTempFolder, workspaceKey); + } else { + throw new InternalError('Orphaned workspaces cannot be checked for the provided package manager'); + } + + if (!this.rushConfiguration.tryGetProjectForPath(rushProjectPath)) { + console.log( + os.EOL + + colors.yellow( + Utilities.wrapWords( + `Your ${this.rushConfiguration.shrinkwrapFilePhrase} references a project at "${rushProjectPath}" ` + + 'which no longer exists.' + ) + ) + + os.EOL + ); + return true; // found one + } + } + + return false; // none found + } +} diff --git a/apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts b/apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts index a2267d4a49e..9f02b415259 100644 --- a/apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts +++ b/apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts @@ -1,6 +1,6 @@ import * as os from 'os'; -import { JsonFile, FileSystem } from '@rushstack/node-core-library'; +import { JsonFile, FileSystem, InternalError } from '@rushstack/node-core-library'; import { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile'; import { DependencySpecifier } from '../DependencySpecifier'; @@ -114,4 +114,22 @@ export class NpmShrinkwrapFile extends BaseShrinkwrapFile { return new DependencySpecifier(dependencySpecifier.packageName, dependencyJson.version); } + + /** @override */ + public getWorkspaceKeys(): ReadonlyArray { + throw new InternalError('Not implemented'); + } + + /** @override */ + public getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string { + throw new InternalError('Not implemented'); + } + + /** @override */ + protected getWorkspaceDependencyVersion( + dependencySpecifier: DependencySpecifier, + workspaceKey: string + ): DependencySpecifier | undefined { + throw new InternalError('Not implemented'); + } } diff --git a/apps/rush-lib/src/logic/pnpm/IPnpmfileShimSettings.ts b/apps/rush-lib/src/logic/pnpm/IPnpmfileShimSettings.ts new file mode 100644 index 00000000000..d1f37be422e --- /dev/null +++ b/apps/rush-lib/src/logic/pnpm/IPnpmfileShimSettings.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export interface IPnpmfileShimSettings { + allPreferredVersions: { [dependencyName: string]: string }; + allowedAlternativeVersions: { [dependencyName: string]: ReadonlyArray }; + semverPath: string; + useClientPnpmfile: boolean; +} diff --git a/apps/rush-lib/src/logic/pnpm/PnpmLinkManager.ts b/apps/rush-lib/src/logic/pnpm/PnpmLinkManager.ts index b2cd99f118e..4e3471ccd29 100644 --- a/apps/rush-lib/src/logic/pnpm/PnpmLinkManager.ts +++ b/apps/rush-lib/src/logic/pnpm/PnpmLinkManager.ts @@ -15,8 +15,14 @@ import { BasePackage } from '../base/BasePackage'; import { RushConstants } from '../../logic/RushConstants'; import { IRushLinkJson } from '../../api/RushConfiguration'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; -import { PnpmShrinkwrapFile, IPnpmShrinkwrapDependencyYaml } from './PnpmShrinkwrapFile'; +import { + PnpmShrinkwrapFile, + IPnpmShrinkwrapDependencyYaml, + IPnpmShrinkwrapImporterYaml +} from './PnpmShrinkwrapFile'; import { PnpmProjectDependencyManifest } from './PnpmProjectDependencyManifest'; +import { PackageJsonDependency, DependencyType } from '../../api/PackageJsonEditor'; +import { DependencySpecifier, DependencySpecifierType } from '../DependencySpecifier'; // special flag for debugging, will print extra diagnostic information, // but comes with performance cost @@ -46,9 +52,17 @@ export class PnpmLinkManager extends BaseLinkManager { ); } + const useWorkspaces: boolean = + this._rushConfiguration.packageManager === 'pnpm' && + this._rushConfiguration.pnpmOptions && + this._rushConfiguration.pnpmOptions.useWorkspaces; + for (const rushProject of this._rushConfiguration.projects) { - console.log(os.EOL + 'LINKING: ' + rushProject.packageName); - await this._linkProject(rushProject, rushLinkJson, pnpmShrinkwrapFile); + if (useWorkspaces) { + await this._linkWorkspaceProject(rushProject, rushLinkJson, pnpmShrinkwrapFile); + } else { + await this._linkProject(rushProject, rushLinkJson, pnpmShrinkwrapFile); + } } } else { console.log( @@ -73,8 +87,9 @@ export class PnpmLinkManager extends BaseLinkManager { rushLinkJson: IRushLinkJson, pnpmShrinkwrapFile: PnpmShrinkwrapFile ): Promise { - // first, read the temp package.json information + console.log(os.EOL + 'LINKING: ' + project.packageName); + // first, read the temp package.json information // Example: "project1" const unscopedTempProjectName: string = this._rushConfiguration.packageNameParser.getUnscopedName( project.tempProjectName @@ -290,6 +305,127 @@ export class PnpmLinkManager extends BaseLinkManager { }); } + /** + * This is called once for each local project from Rush.json. + * + * TODO: This should be moved into WorkspaceInstallManager directly, since there is no actual linking + * being done by Rush for this style of install. + * + * @param project The local project that we will create symlinks for + * @param rushLinkJson The common/temp/rush-link.json output file + */ + private async _linkWorkspaceProject( + project: RushConfigurationProject, + rushLinkJson: IRushLinkJson, + pnpmShrinkwrapFile: PnpmShrinkwrapFile + ): Promise { + // First, generate the local dependency graph. When using workspaces, Rush forces `workspace:` + // notation for all locally-referenced projects. + const localDependencies: PackageJsonDependency[] = [ + ...project.packageJsonEditor.dependencyList, + ...project.packageJsonEditor.devDependencyList + ].filter( + (x) => new DependencySpecifier(x.name, x.version).specifierType === DependencySpecifierType.Workspace + ); + + for (const { name } of localDependencies) { + const matchedRushPackage: + | RushConfigurationProject + | undefined = this._rushConfiguration.getProjectByName(name); + + if (matchedRushPackage) { + // We found a suitable match, so add the local package as a local link + let localLinks: string[] = rushLinkJson.localLinks[project.packageName]; + if (!localLinks) { + localLinks = []; + rushLinkJson.localLinks[project.packageName] = localLinks; + } + localLinks.push(name); + } else { + throw new InternalError( + `Cannot find dependency "${name}" for "${project.packageName}" in the Rush configuration` + ); + } + } + + const importerKey: string = pnpmShrinkwrapFile.getWorkspaceKeyByPath( + this._rushConfiguration.commonTempFolder, + project.projectFolder + ); + const workspaceImporter: + | IPnpmShrinkwrapImporterYaml + | undefined = pnpmShrinkwrapFile.getWorkspaceImporter(importerKey); + if (!workspaceImporter) { + throw new InternalError( + `Cannot find shrinkwrap entry using importer key for workspace project: ${importerKey}` + ); + } + const pnpmProjectDependencyManifest: PnpmProjectDependencyManifest = new PnpmProjectDependencyManifest({ + pnpmShrinkwrapFile, + project + }); + const useProjectDependencyManifest: boolean = !this._rushConfiguration.experimentsConfiguration + .configuration.legacyIncrementalBuildDependencyDetection; + + // Then, do non-local dependencies + const dependencies: PackageJsonDependency[] = [ + ...project.packageJsonEditor.dependencyList, + ...project.packageJsonEditor.devDependencyList + ].filter( + (x) => new DependencySpecifier(x.name, x.version).specifierType !== DependencySpecifierType.Workspace + ); + + for (const { name, dependencyType } of dependencies) { + // read the version number from the shrinkwrap entry + let version: string | undefined; + switch (dependencyType) { + case DependencyType.Regular: + version = (workspaceImporter.dependencies || {})[name]; + break; + case DependencyType.Dev: + // Dev dependencies are folded into dependencies if there is a duplicate + // definition, so we should also check there + version = + (workspaceImporter.devDependencies || {})[name] || (workspaceImporter.dependencies || {})[name]; + break; + case DependencyType.Optional: + version = (workspaceImporter.optionalDependencies || {})[name]; + break; + case DependencyType.Peer: + // Peer dependencies do not need to be considered since they aren't a true + // dependency, and would be satisfied in the consuming package. They are + // also not specified in the workspace importer + continue; + } + + if (!version) { + // Optional dependencies by definition may not exist, so avoid throwing on these + if (dependencyType !== DependencyType.Optional) { + throw new InternalError( + `Cannot find shrinkwrap entry dependency "${name}" for workspace project: ${project.packageName}` + ); + } + continue; + } + + if (useProjectDependencyManifest) { + // Add to the manifest and provide all the parent dependencies. Peer dependencies are not mapped at + // the importer level, so provide an empty object for that + pnpmProjectDependencyManifest.addDependency(name, version, { + dependencies: { ...workspaceImporter.dependencies, ...workspaceImporter.devDependencies }, + optionalDependencies: { ...workspaceImporter.optionalDependencies }, + peerDependencies: {} + }); + } + } + + if (useProjectDependencyManifest) { + pnpmProjectDependencyManifest.save(); + } else { + pnpmProjectDependencyManifest.deleteIfExists(); + } + } + private _getPathToLocalInstallation(folderNameInLocalInstallationRoot: string): string { // See https://github.com/pnpm/pnpm/releases/tag/v4.0.0 if (this._pnpmVersion.major >= 4) { @@ -341,7 +477,8 @@ export class PnpmLinkManager extends BaseLinkManager { ); } - // read the version number from the shrinkwrap entry + // read the version number from the shrinkwrap entry and return if no version is specified + // and the dependency is optional const version: string | undefined = isOptional ? (parentShrinkwrapEntry.optionalDependencies || {})[dependencyName] : (parentShrinkwrapEntry.dependencies || {})[dependencyName]; @@ -370,7 +507,11 @@ export class PnpmLinkManager extends BaseLinkManager { !this._rushConfiguration.experimentsConfiguration.configuration .legacyIncrementalBuildDependencyDetection ) { - pnpmProjectDependencyManifest.addDependency(newLocalPackage, parentShrinkwrapEntry); + pnpmProjectDependencyManifest.addDependency( + newLocalPackage.name, + newLocalPackage.version!, + parentShrinkwrapEntry + ); } return newLocalPackage; diff --git a/apps/rush-lib/src/logic/pnpm/PnpmProjectDependencyManifest.ts b/apps/rush-lib/src/logic/pnpm/PnpmProjectDependencyManifest.ts index e515d041e9e..450bc6f7b16 100644 --- a/apps/rush-lib/src/logic/pnpm/PnpmProjectDependencyManifest.ts +++ b/apps/rush-lib/src/logic/pnpm/PnpmProjectDependencyManifest.ts @@ -13,7 +13,6 @@ import { } from './PnpmShrinkwrapFile'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { RushConstants } from '../RushConstants'; -import { BasePackage } from '../base/BasePackage'; import { DependencySpecifier } from '../DependencySpecifier'; export interface IPnpmProjectDependencyManifestOptions { @@ -59,12 +58,15 @@ export class PnpmProjectDependencyManifest { return path.join(project.projectRushTempFolder, RushConstants.projectDependencyManifestFilename); } - public addDependency(pkg: BasePackage, parentShrinkwrapEntry: IPnpmShrinkwrapDependencyYaml): void { - if (!pkg.version) { - throw new InternalError(`Version missing from dependency ${pkg.name}`); - } - - this._addDependencyInternal(pkg.name, pkg.version, parentShrinkwrapEntry); + public addDependency( + name: string, + version: string, + parentShrinkwrapEntry: Pick< + IPnpmShrinkwrapDependencyYaml, + 'dependencies' | 'optionalDependencies' | 'peerDependencies' + > + ): void { + this._addDependencyInternal(name, version, parentShrinkwrapEntry); } /** @@ -90,7 +92,10 @@ export class PnpmProjectDependencyManifest { private _addDependencyInternal( name: string, version: string, - parentShrinkwrapEntry: IPnpmShrinkwrapDependencyYaml, + parentShrinkwrapEntry: Pick< + IPnpmShrinkwrapDependencyYaml, + 'dependencies' | 'optionalDependencies' | 'peerDependencies' + >, throwIfShrinkwrapEntryMissing: boolean = true ): void { const shrinkwrapEntry: @@ -164,6 +169,16 @@ export class PnpmProjectDependencyManifest { } } + if ( + this._project.rushConfiguration.pnpmOptions && + this._project.rushConfiguration.pnpmOptions.useWorkspaces + ) { + // When using workspaces, hoisting of dependencies is not possible. Therefore, all packages that are consumed + // should be specified as direct dependencies in the shrinkwrap. Given this, there is no need to look for peer + // dependencies, since it is simply a constraint to be validated by the package manager. + return; + } + for (const peerDependencyName in shrinkwrapEntry.peerDependencies) { if (shrinkwrapEntry.peerDependencies.hasOwnProperty(peerDependencyName)) { // Peer dependencies come in the form of a semantic version range @@ -263,11 +278,17 @@ export class PnpmProjectDependencyManifest { const specifierMatches: RegExpExecArray | null = /^[^_]+_(.+)$/.exec(specifier); if (specifierMatches) { const combinedPeerDependencies: string = specifierMatches[1]; - // Parse "eslint@6.6.0+typescript@3.6.4" --> ["eslint@6.6.0", "typescript@3.6.4"] + // "eslint@6.6.0+typescript@3.6.4+@types+webpack@4.1.9" --> ["eslint@6.6.0", "typescript@3.6.4", "@types", "webpack@4.1.9"] const peerDependencies: string[] = combinedPeerDependencies.split('+'); - for (const peerDependencySpecifier of peerDependencies) { + for (let i: number = 0; i < peerDependencies.length; i++) { + // Scopes are also separated by '+', so reduce the proceeding value into it + if (peerDependencies[i].indexOf('@') === 0) { + peerDependencies[i] = `${peerDependencies[i]}/${peerDependencies[i + 1]}`; + peerDependencies.splice(i + 1, 1); + } + // Parse "eslint@6.6.0" --> "eslint", "6.6.0" - const peerMatches: RegExpExecArray | null = /^([^+@]+)@(.+)$/.exec(peerDependencySpecifier); + const peerMatches: RegExpExecArray | null = /^(@?[^+@]+)@(.+)$/.exec(peerDependencies[i]); if (peerMatches) { const peerDependencyName: string = peerMatches[1]; const peerDependencyVersion: string = peerMatches[2]; diff --git a/apps/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/apps/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 542414354f5..72c12b0adc1 100644 --- a/apps/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/apps/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -1,5 +1,6 @@ import * as yaml from 'js-yaml'; import * as os from 'os'; +import * as path from 'path'; import * as semver from 'semver'; import * as crypto from 'crypto'; import * as colors from 'colors'; @@ -11,7 +12,7 @@ import { PackageManagerOptionsConfigurationBase, PnpmOptionsConfiguration } from '../../api/RushConfiguration'; -import { IPolicyValidatorOptions } from '../policy/PolicyValidator'; +import { IShrinkwrapFilePolicyValidatorOptions } from '../policy/ShrinkwrapFilePolicy'; import { AlreadyReportedError } from '../../utilities/AlreadyReportedError'; // This is based on PNPM's own configuration: @@ -48,6 +49,17 @@ export interface IPnpmShrinkwrapDependencyYaml { peerDependenciesMeta: { [dependency: string]: IPeerDependenciesMetaYaml }; } +export interface IPnpmShrinkwrapImporterYaml { + /** The list of resolved version numbers for direct dependencies */ + dependencies: { [dependency: string]: string }; + /** The list of resolved version numbers for dev dependencies */ + devDependencies: { [dependency: string]: string }; + /** The list of resolved version numbers for optional dependencies */ + optionalDependencies: { [dependency: string]: string }; + /** The list of specifiers used to resolve dependency versions */ + specifiers: { [dependency: string]: string }; +} + /** * This interface represents the raw pnpm-lock.YAML file * Example: @@ -82,6 +94,8 @@ export interface IPnpmShrinkwrapDependencyYaml { interface IPnpmShrinkwrapYaml { /** The list of resolved version numbers for direct dependencies */ dependencies: { [dependency: string]: string }; + /** The list of importers for local workspace projects */ + importers: { [relativePath: string]: IPnpmShrinkwrapImporterYaml }; /** The description of the solved graph */ packages: { [dependencyVersion: string]: IPnpmShrinkwrapDependencyYaml }; /** URL of the registry which was used */ @@ -186,22 +200,12 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { */ public readonly shrinkwrapFilename: string; - private static readonly _shrinkwrapHashPrefix: string = '# shrinkwrap hash:'; private _shrinkwrapJson: IPnpmShrinkwrapYaml; - private _shrinkwrapHash: string | undefined; - private _shrinkwrapHashEnabled: boolean | undefined; - - private constructor( - shrinkwrapJson: IPnpmShrinkwrapYaml, - shrinkwrapFilename: string, - shrinkwrapHash?: string, - shrinkwrapHashEnabled?: boolean - ) { + + private constructor(shrinkwrapJson: IPnpmShrinkwrapYaml, shrinkwrapFilename: string) { super(); this._shrinkwrapJson = shrinkwrapJson; this.shrinkwrapFilename = shrinkwrapFilename; - this._shrinkwrapHash = shrinkwrapHash; - this._shrinkwrapHashEnabled = shrinkwrapHashEnabled; // Normalize the data if (!this._shrinkwrapJson.registry) { @@ -210,6 +214,9 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { if (!this._shrinkwrapJson.dependencies) { this._shrinkwrapJson.dependencies = {}; } + if (!this._shrinkwrapJson.importers) { + this._shrinkwrapJson.importers = {}; + } if (!this._shrinkwrapJson.specifiers) { this._shrinkwrapJson.specifiers = {}; } @@ -229,38 +236,21 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { const shrinkwrapContent: string = FileSystem.readFile(shrinkwrapYamlFilename); const parsedData: IPnpmShrinkwrapYaml = yaml.safeLoad(shrinkwrapContent); - - let shrinkwrapHash: string | undefined; - if (pnpmOptions.preventManualShrinkwrapChanges) { - // Grab the shrinkwrap hash out of the comment where we store it. - const hashYamlCommentRegExp: RegExp = new RegExp( - `\\n\\s*${PnpmShrinkwrapFile._shrinkwrapHashPrefix}\\s*(\\S+)\\s*$` - ); - const match: RegExpMatchArray | null = shrinkwrapContent.match(hashYamlCommentRegExp); - shrinkwrapHash = match ? match[1] : undefined; - } - - return new PnpmShrinkwrapFile( - parsedData, - shrinkwrapYamlFilename, - shrinkwrapHash, - pnpmOptions.preventManualShrinkwrapChanges - ); + return new PnpmShrinkwrapFile(parsedData, shrinkwrapYamlFilename); } catch (error) { throw new Error(`Error reading "${shrinkwrapYamlFilename}":${os.EOL} ${error.message}`); } } - /** @override */ - public shouldForceRecheck(): boolean { - // Ensure the shrinkwrap is rechecked when the hash is enabled but no hash is populated. - return super.shouldForceRecheck() || (!!this._shrinkwrapHashEnabled && !this._shrinkwrapHash); + public getShrinkwrapHash(): string { + const shrinkwrapContent: string = this.serialize(); + return crypto.createHash('sha1').update(shrinkwrapContent).digest('hex'); } /** @override */ public validate( packageManagerOptionsConfig: PackageManagerOptionsConfigurationBase, - policyOptions: IPolicyValidatorOptions + policyOptions: IShrinkwrapFilePolicyValidatorOptions ): void { super.validate(packageManagerOptionsConfig, policyOptions); if (!(packageManagerOptionsConfig instanceof PnpmOptionsConfiguration)) { @@ -270,19 +260,17 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { // Only check the hash if allowShrinkwrapUpdates is false. If true, the shrinkwrap file // may have changed and the hash could be invalid. if (packageManagerOptionsConfig.preventManualShrinkwrapChanges && !policyOptions.allowShrinkwrapUpdates) { - if (!this._shrinkwrapHash) { + if (!policyOptions.repoState.pnpmShrinkwrapHash) { console.log( colors.red( - 'The shrinkwrap file does not contain the generated hash. You may need to run "rush update" to ' + + 'The existing shrinkwrap file hash could not be found. You may need to run "rush update" to ' + 'populate the hash. See the "preventManualShrinkwrapChanges" setting documentation for details.' ) + os.EOL ); throw new AlreadyReportedError(); } - const shrinkwrapContent: string = yaml.safeDump(this._shrinkwrapJson, SHRINKWRAP_YAML_FORMAT); - const calculatedHash: string = crypto.createHash('sha1').update(shrinkwrapContent).digest('hex'); - if (calculatedHash !== this._shrinkwrapHash) { + if (this.getShrinkwrapHash() !== policyOptions.repoState.pnpmShrinkwrapHash) { console.log( colors.red( 'The shrinkwrap file hash does not match the expected hash. Please run "rush update" to ensure the ' + @@ -432,15 +420,21 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { * @override */ protected serialize(): string { - let shrinkwrapContent: string = yaml.safeDump(this._shrinkwrapJson, SHRINKWRAP_YAML_FORMAT); - if (this._shrinkwrapHashEnabled) { - this._shrinkwrapHash = crypto.createHash('sha1').update(shrinkwrapContent).digest('hex'); - shrinkwrapContent = `${shrinkwrapContent.trimRight()}\n${PnpmShrinkwrapFile._shrinkwrapHashPrefix} ${ - this._shrinkwrapHash - }\n`; + // Ensure that if any of the top-level properties are provided but empty that they are removed. We populate the object + // properties when we read the shrinkwrap but PNPM does not set these top-level properties unless they are present. + const shrinkwrapToSerialize: Partial = { ...this._shrinkwrapJson }; + for (const key of Object.keys(shrinkwrapToSerialize).filter((key) => + shrinkwrapToSerialize.hasOwnProperty(key) + )) { + if ( + typeof shrinkwrapToSerialize[key] === 'object' && + Object.entries(shrinkwrapToSerialize[key] || {}).length === 0 + ) { + delete shrinkwrapToSerialize[key]; + } } - return shrinkwrapContent; + return yaml.safeDump(shrinkwrapToSerialize, SHRINKWRAP_YAML_FORMAT); } /** @@ -518,6 +512,66 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { return this._parsePnpmDependencyKey(packageName, dependencyKey); } + /** @override */ + public getWorkspaceKeys(): ReadonlyArray { + const result: string[] = []; + for (const key of Object.keys(this._shrinkwrapJson.importers)) { + // Avoid including the common workspace + if (key !== '.') { + result.push(key); + } + } + result.sort(); // make the result deterministic + return result; + } + + /** @override */ + public getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string { + return path.relative(workspaceRoot, projectFolder).replace(new RegExp(`\\${path.sep}`, 'g'), '/'); + } + + public getWorkspaceImporter(importerPath: string): IPnpmShrinkwrapImporterYaml | undefined { + return BaseShrinkwrapFile.tryGetValue(this._shrinkwrapJson.importers, importerPath); + } + + /** + * Gets the resolved version number of a dependency for a specific temp project. + * For PNPM, we can reuse the version that another project is using. + * Note that this function modifies the shrinkwrap data. + * + * @override + */ + protected getWorkspaceDependencyVersion( + dependencySpecifier: DependencySpecifier, + workspaceKey: string + ): DependencySpecifier | undefined { + // PNPM doesn't have the same advantage of NPM, where we can skip generate as long as the + // shrinkwrap file puts our dependency in either the top of the node_modules folder + // or underneath the package we are looking at. + // This is because the PNPM shrinkwrap file describes the exact links that need to be created + // to recreate the graph.. + // Because of this, we actually need to check for a version that this package is directly + // linked to. + + const packageName: string = dependencySpecifier.packageName; + const projectImporter: IPnpmShrinkwrapImporterYaml | undefined = this.getWorkspaceImporter(workspaceKey); + if (!projectImporter) { + return undefined; + } + + const allDependencies: { [dependency: string]: string } = { + ...(projectImporter.optionalDependencies || {}), + ...(projectImporter.dependencies || {}), + ...(projectImporter.devDependencies || {}) + }; + if (!allDependencies.hasOwnProperty(packageName)) { + return undefined; + } + + const dependencyKey: string = allDependencies[packageName]; + return this._parsePnpmDependencyKey(packageName, dependencyKey); + } + /** * Returns the version of a dependency being used by a given project */ diff --git a/apps/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/apps/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts new file mode 100644 index 00000000000..dd3618a549c --- /dev/null +++ b/apps/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as globEscape from 'glob-escape'; +import * as os from 'os'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; +import { FileSystem, Sort, Text } from '@rushstack/node-core-library'; + +import { BaseWorkspaceFile } from '../base/BaseWorkspaceFile'; + +// This is based on PNPM's own configuration: +// https://github.com/pnpm/pnpm-shrinkwrap/blob/master/src/write.ts +const WORKSPACE_YAML_FORMAT: yaml.DumpOptions = { + lineWidth: 1000, + noCompatMode: true, + noRefs: true, + sortKeys: true +}; + +/** + * This interface represents the raw pnpm-workspace.YAML file + * Example: + * { + * "packages": [ + * "../../apps/project1" + * ] + * } + */ +interface IPnpmWorkspaceYaml { + /** The list of local package directories */ + packages: string[]; +} + +export class PnpmWorkspaceFile extends BaseWorkspaceFile { + /** + * The filename of the workspace file. + */ + public readonly workspaceFilename: string; + + private _workspacePackages: Set; + + /** + * The PNPM workspace file is used to specify the location of workspaces relative to the root + * of your PNPM install. + */ + public constructor(workspaceYamlFilename: string) { + super(); + + this.workspaceFilename = workspaceYamlFilename; + let workspaceYaml: IPnpmWorkspaceYaml; + try { + // Populate with the existing file, or an empty list if the file doesn't exist + workspaceYaml = FileSystem.exists(workspaceYamlFilename) + ? yaml.safeLoad(FileSystem.readFile(workspaceYamlFilename).toString()) + : { packages: [] }; + } catch (error) { + throw new Error(`Error reading "${workspaceYamlFilename}":${os.EOL} ${error.message}`); + } + + this._workspacePackages = new Set(workspaceYaml.packages); + } + + /** @override */ + public addPackage(packagePath: string): void { + // Ensure the path is relative to the pnpm-workspace.yaml file + if (path.isAbsolute(packagePath)) { + packagePath = path.relative(path.dirname(this.workspaceFilename), packagePath); + } + + // Glob can't handle Windows paths + const globPath: string = Text.replaceAll(packagePath, '\\', '/'); + this._workspacePackages.add(globEscape(globPath)); + } + + /** @override */ + protected serialize(): string { + // Ensure stable sort order when serializing + Sort.sortSet(this._workspacePackages); + + const workspaceYaml: IPnpmWorkspaceYaml = { + packages: Array.from(this._workspacePackages) + }; + return yaml.safeDump(workspaceYaml, WORKSPACE_YAML_FORMAT); + } +} diff --git a/apps/rush-lib/src/logic/pnpm/PnpmfileShim.ts b/apps/rush-lib/src/logic/pnpm/PnpmfileShim.ts new file mode 100644 index 00000000000..bd8cfe052c1 --- /dev/null +++ b/apps/rush-lib/src/logic/pnpm/PnpmfileShim.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// Uncomment "/* type */" when we upgrade to TS 3.9 +import { /* type */ IPackageJson } from '@rushstack/node-core-library'; +import { /* type */ IPnpmfileShimSettings } from './IPnpmfileShimSettings'; +import /* type */ * as TSemver from 'semver'; + +interface ILockfile {} + +interface IPnpmfile { + hooks?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + afterAllResolved?: (lockfile: ILockfile, context: any) => ILockfile; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readPackage?: (pkg: IPackageJson, context: any) => IPackageJson; + }; +} + +// Load in the generated settings file +const pnpmfileSettings: IPnpmfileShimSettings = require('./pnpmfileSettings.json'); +// We will require semver from this path on disk, since this is the version of semver shipping with Rush +const semver: typeof TSemver = require(pnpmfileSettings.semverPath); +// Only require the client pnpmfile if requested +const clientPnpmfile: IPnpmfile | undefined = pnpmfileSettings.useClientPnpmfile + ? require('./clientPnpmfile') + : undefined; + +// Set the preferred versions on the dependency map. If the version on the map is an allowedAlternativeVersion +// then skip it. Otherwise, check to ensure that the common version is a subset of the specified version. If +// it is, then replace the specified version with the preferredVersion +function setPreferredVersions(dependencies?: { [dependencyName: string]: string }): void { + for (const name of Object.keys(dependencies || {})) { + if (pnpmfileSettings.allPreferredVersions.hasOwnProperty(name)) { + const preferredVersion: string = pnpmfileSettings.allPreferredVersions[name]; + const version: string = dependencies![name]; + if (pnpmfileSettings.allowedAlternativeVersions.hasOwnProperty(name)) { + const allowedAlternatives: ReadonlyArray | undefined = + pnpmfileSettings.allowedAlternativeVersions[name]; + if (allowedAlternatives && allowedAlternatives.indexOf(version) > -1) { + continue; + } + } + let isValidRange: boolean = false; + try { + isValidRange = !!semver.validRange(preferredVersion) && !!semver.validRange(version); + } catch { + // Swallow invalid range errors + } + + if (isValidRange && semver.subset(preferredVersion, version)) { + dependencies![name] = preferredVersion; + } + } + } +} + +const pnpmfileShim: IPnpmfile = { + hooks: { + // Call the original pnpmfile (if it exists) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + afterAllResolved: (lockfile: ILockfile, context: any) => { + return clientPnpmfile && clientPnpmfile.hooks && clientPnpmfile.hooks.afterAllResolved + ? clientPnpmfile.hooks.afterAllResolved(lockfile, context) + : lockfile; + }, + + // Set the preferred versions in the package, then call the original pnpmfile (if it exists) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readPackage: (pkg: IPackageJson, context: any) => { + setPreferredVersions(pkg.dependencies); + setPreferredVersions(pkg.devDependencies); + setPreferredVersions(pkg.optionalDependencies); + return clientPnpmfile && clientPnpmfile.hooks && clientPnpmfile.hooks.readPackage + ? clientPnpmfile.hooks.readPackage(pkg, context) + : pkg; + } + } +}; + +export = pnpmfileShim; diff --git a/apps/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts b/apps/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts index d82e3ca3265..9185a1e96a2 100644 --- a/apps/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts +++ b/apps/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts @@ -2,10 +2,16 @@ // See LICENSE in the project root for license information. import * as os from 'os'; + import { RushConfiguration } from '../../api/RushConfiguration'; import { IPolicyValidatorOptions } from './PolicyValidator'; import { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile'; import { ShrinkwrapFileFactory } from '../ShrinkwrapFileFactory'; +import { RepoStateFile } from '../RepoStateFile'; + +export interface IShrinkwrapFilePolicyValidatorOptions extends IPolicyValidatorOptions { + repoState: RepoStateFile; +} /** * A policy that validates shrinkwrap files used by package managers. @@ -25,6 +31,9 @@ export class ShrinkwrapFilePolicy { } // Run shrinkwrap-specific validation - shrinkwrapFile.validate(rushConfiguration.packageManagerOptions, options); + shrinkwrapFile.validate(rushConfiguration.packageManagerOptions, { + ...options, + repoState: rushConfiguration.getRepoState(options.shrinkwrapVariant) + }); } } diff --git a/apps/rush-lib/src/logic/test/ChangeManager.test.ts b/apps/rush-lib/src/logic/test/ChangeManager.test.ts index 77e9467fb86..c55398c6570 100644 --- a/apps/rush-lib/src/logic/test/ChangeManager.test.ts +++ b/apps/rush-lib/src/logic/test/ChangeManager.test.ts @@ -161,3 +161,172 @@ describe('ChangeManager', () => { }); /* eslint-enable dot-notation */ }); + +describe('WorkspaceChangeManager', () => { + const rushJsonFile: string = path.resolve(__dirname, 'workspacePackages', 'rush.json'); + let rushConfiguration: RushConfiguration; + let changeManager: ChangeManager; + + beforeEach(() => { + rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile); + changeManager = new ChangeManager(rushConfiguration); + }); + + /* eslint-disable dot-notation */ + it('can apply changes to the package.json files in the dictionary', () => { + changeManager.load(path.join(__dirname, 'multipleChanges')); + changeManager.apply(false); + + expect(changeManager.allPackages.get('a')!.packageJson.version).toEqual('2.0.0'); + expect(changeManager.allPackages.get('b')!.packageJson.version).toEqual('1.0.1'); + expect(changeManager.allPackages.get('b')!.packageJson.dependencies!['a']).toEqual( + 'workspace:>=2.0.0 <3.0.0' + ); + expect(changeManager.allPackages.get('c')!.packageJson.version).toEqual('1.0.0'); + expect(changeManager.allPackages.get('c')!.packageJson.dependencies!['b']).toEqual( + 'workspace:>=1.0.1 <2.0.0' + ); + }); + + it('can update explicit version dependency', () => { + changeManager.load(path.join(__dirname, 'explicitVersionChange')); + changeManager.apply(false); + + expect(changeManager.allPackages.get('c')!.packageJson.version).toEqual('1.0.1'); + expect(changeManager.allPackages.get('d')!.packageJson.version).toEqual('1.0.1'); + expect(changeManager.allPackages.get('d')!.packageJson.dependencies!['c']).toEqual('workspace:1.0.1'); + }); + + it('can update explicit cyclic dependency', () => { + changeManager.load(path.join(__dirname, 'cyclicDepsExplicit')); + changeManager.apply(false); + + expect(changeManager.allPackages.get('cyclic-dep-explicit-1')!.packageJson.version).toEqual('2.0.0'); + expect( + changeManager.allPackages.get('cyclic-dep-explicit-1')!.packageJson.dependencies![ + 'cyclic-dep-explicit-2' + ] + ).toEqual('workspace:>=1.0.0 <2.0.0'); + expect(changeManager.allPackages.get('cyclic-dep-explicit-2')!.packageJson.version).toEqual('1.0.0'); + expect( + changeManager.allPackages.get('cyclic-dep-explicit-2')!.packageJson.dependencies![ + 'cyclic-dep-explicit-1' + ] + ).toEqual('>=1.0.0 <2.0.0'); + }); + + it('can update root with patch change for prerelease', () => { + const prereleaseName: string = 'alpha.1'; + const prereleaseToken: PrereleaseToken = new PrereleaseToken(prereleaseName); + + changeManager.load(path.join(__dirname, 'rootPatchChange'), prereleaseToken); + changeManager.apply(false); + + expect(changeManager.allPackages.get('a')!.packageJson.version).toEqual('1.0.1-' + prereleaseName); + expect(changeManager.allPackages.get('b')!.packageJson.version).toEqual('1.0.1-' + prereleaseName); + expect(changeManager.allPackages.get('b')!.packageJson.dependencies!['a']).toEqual( + 'workspace:1.0.1-' + prereleaseName + ); + expect(changeManager.allPackages.get('c')!.packageJson.version).toEqual('1.0.1-' + prereleaseName); + expect(changeManager.allPackages.get('d')!.packageJson.version).toEqual('1.0.1-' + prereleaseName); + expect(changeManager.allPackages.get('d')!.packageJson.dependencies!['c']).toEqual( + 'workspace:1.0.1-' + prereleaseName + ); + }); + + it('can update non-root with patch change for prerelease', () => { + const prereleaseName: string = 'beta.1'; + const prereleaseToken: PrereleaseToken = new PrereleaseToken(prereleaseName); + + changeManager.load(path.join(__dirname, 'explicitVersionChange'), prereleaseToken); + changeManager.apply(false); + + expect(changeManager.allPackages.get('a')!.packageJson.version).toEqual('1.0.0'); + expect(changeManager.allPackages.get('b')!.packageJson.version).toEqual('1.0.0'); + expect(changeManager.allPackages.get('b')!.packageJson.dependencies!['a']).toEqual( + 'workspace:>=1.0.0 <2.0.0' + ); + expect(changeManager.allPackages.get('c')!.packageJson.version).toEqual('1.0.1-' + prereleaseName); + expect(changeManager.allPackages.get('d')!.packageJson.version).toEqual('1.0.1-' + prereleaseName); + expect(changeManager.allPackages.get('d')!.packageJson.dependencies!['c']).toEqual( + 'workspace:1.0.1-' + prereleaseName + ); + }); + + it('can update cyclic dependency for non-explicit prerelease', () => { + const prereleaseName: string = 'beta.1'; + const prereleaseToken: PrereleaseToken = new PrereleaseToken(prereleaseName); + + changeManager.load(path.join(__dirname, 'cyclicDeps'), prereleaseToken); + changeManager.apply(false); + + expect(changeManager.allPackages.get('cyclic-dep-1')!.packageJson.version).toEqual( + '2.0.0-' + prereleaseName + ); + expect(changeManager.allPackages.get('cyclic-dep-1')!.packageJson.dependencies!['cyclic-dep-2']).toEqual( + 'workspace:1.0.1-' + prereleaseName + ); + expect(changeManager.allPackages.get('cyclic-dep-2')!.packageJson.version).toEqual( + '1.0.1-' + prereleaseName + ); + expect(changeManager.allPackages.get('cyclic-dep-2')!.packageJson.dependencies!['cyclic-dep-1']).toEqual( + 'workspace:2.0.0-' + prereleaseName + ); + }); + + it('can update root with patch change for adding version suffix', () => { + const suffix: string = 'dk.1'; + const prereleaseToken: PrereleaseToken = new PrereleaseToken(undefined, suffix); + + changeManager.load(path.join(__dirname, 'rootPatchChange'), prereleaseToken); + changeManager.apply(false); + + expect(changeManager.allPackages.get('a')!.packageJson.version).toEqual('1.0.0-' + suffix); + expect(changeManager.allPackages.get('b')!.packageJson.version).toEqual('1.0.0-' + suffix); + expect(changeManager.allPackages.get('b')!.packageJson.dependencies!['a']).toEqual( + 'workspace:1.0.0-' + suffix + ); + expect(changeManager.allPackages.get('c')!.packageJson.version).toEqual('1.0.0-' + suffix); + expect(changeManager.allPackages.get('d')!.packageJson.version).toEqual('1.0.0-' + suffix); + expect(changeManager.allPackages.get('d')!.packageJson.dependencies!['c']).toEqual( + 'workspace:1.0.0-' + suffix + ); + }); + + it('can update non-root with patch change for version suffix', () => { + const suffix: string = 'dk.1'; + const prereleaseToken: PrereleaseToken = new PrereleaseToken(undefined, suffix); + + changeManager.load(path.join(__dirname, 'explicitVersionChange'), prereleaseToken); + changeManager.apply(false); + + expect(changeManager.allPackages.get('a')!.packageJson.version).toEqual('1.0.0'); + expect(changeManager.allPackages.get('b')!.packageJson.version).toEqual('1.0.0'); + expect(changeManager.allPackages.get('b')!.packageJson.dependencies!['a']).toEqual( + 'workspace:>=1.0.0 <2.0.0' + ); + expect(changeManager.allPackages.get('c')!.packageJson.version).toEqual('1.0.0-' + suffix); + expect(changeManager.allPackages.get('d')!.packageJson.version).toEqual('1.0.0-' + suffix); + expect(changeManager.allPackages.get('d')!.packageJson.dependencies!['c']).toEqual( + 'workspace:1.0.0-' + suffix + ); + }); + + it('can update cyclic dependency for non-explicit suffix', () => { + const suffix: string = 'dk.1'; + const prereleaseToken: PrereleaseToken = new PrereleaseToken(undefined, suffix); + + changeManager.load(path.join(__dirname, 'cyclicDeps'), prereleaseToken); + changeManager.apply(false); + + expect(changeManager.allPackages.get('cyclic-dep-1')!.packageJson.version).toEqual('1.0.0-' + suffix); + expect(changeManager.allPackages.get('cyclic-dep-1')!.packageJson.dependencies!['cyclic-dep-2']).toEqual( + 'workspace:1.0.0-' + suffix + ); + expect(changeManager.allPackages.get('cyclic-dep-2')!.packageJson.version).toEqual('1.0.0-' + suffix); + expect(changeManager.allPackages.get('cyclic-dep-2')!.packageJson.dependencies!['cyclic-dep-1']).toEqual( + 'workspace:1.0.0-' + suffix + ); + }); + /* eslint-enable dot-notation */ +}); diff --git a/apps/rush-lib/src/logic/test/VersionManager.test.ts b/apps/rush-lib/src/logic/test/VersionManager.test.ts index 3fde036e6a9..e0438bfb2a8 100644 --- a/apps/rush-lib/src/logic/test/VersionManager.test.ts +++ b/apps/rush-lib/src/logic/test/VersionManager.test.ts @@ -98,3 +98,84 @@ describe('VersionManager', () => { }); /* eslint-enable dot-notation */ }); + +describe('WorkspaceVersionManager', () => { + const rushJsonFile: string = path.resolve(__dirname, 'workspaceRepo', 'rush.json'); + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile); + let versionManager: VersionManager; + + beforeEach(() => { + versionManager = new VersionManager( + rushConfiguration, + 'test@microsoft.com', + rushConfiguration.versionPolicyConfiguration + ); + }); + + /* eslint-disable dot-notation */ + describe('ensure', () => { + it('fixes lock step versions', () => { + versionManager.ensure('testPolicy1'); + const updatedPackages: Map = versionManager.updatedProjects; + const expectedVersion: string = '10.10.0'; + expect(updatedPackages.size).toEqual(6); + expect(updatedPackages.get('a')!.version).toEqual(expectedVersion); + expect(updatedPackages.get('b')!.version).toEqual(expectedVersion); + expect(updatedPackages.get('b')!.dependencies!['a']).toEqual(`workspace:~${expectedVersion}`); + expect(updatedPackages.get('c')!.version).toEqual('3.1.1'); + expect(updatedPackages.get('c')!.dependencies!['b']).toEqual(`workspace:>=10.10.0 <11.0.0`); + expect(updatedPackages.get('d')!.version).toEqual('4.1.1'); + expect(updatedPackages.get('d')!.dependencies!['b']).toEqual(`workspace:>=10.10.0 <11.0.0`); + expect(updatedPackages.get('f')!.version).toEqual('1.0.0'); + expect(updatedPackages.get('f')!.dependencies!['a']).toEqual(`workspace:~10.10.0`); + expect(updatedPackages.get('g')!.devDependencies!['a']).toEqual(`workspace:~10.10.0`); + + const changeFiles: Map = versionManager.changeFiles; + expect(changeFiles.size).toEqual(4); + expect(_getChanges(changeFiles, 'a')!).toHaveLength(1); + expect(_getChanges(changeFiles, 'a')![0].changeType).toEqual(ChangeType.none); + expect(_getChanges(changeFiles, 'b')!).toHaveLength(1); + expect(_getChanges(changeFiles, 'b')![0].changeType).toEqual(ChangeType.none); + expect(_getChanges(changeFiles, 'c')!).toHaveLength(2); + expect(_getChanges(changeFiles, 'c')![0].changeType).toEqual(ChangeType.patch); + expect(_getChanges(changeFiles, 'c')![1].changeType).toEqual(ChangeType.dependency); + expect(_getChanges(changeFiles, 'd')!).toHaveLength(2); + expect(_getChanges(changeFiles, 'd')![0].changeType).toEqual(ChangeType.patch); + expect(_getChanges(changeFiles, 'd')![1].changeType).toEqual(ChangeType.dependency); + }); + + it('fixes major version for individual version policy', () => { + versionManager.ensure('testPolicy2'); + const updatedPackages: Map = versionManager.updatedProjects; + expect(updatedPackages.size).toEqual(2); + expect(updatedPackages.get('c')!.version).toEqual('5.0.0'); + expect(updatedPackages.get('c')!.dependencies!['b']).toEqual(`workspace:>=2.0.0 <3.0.0`); + expect(updatedPackages.get('e')!.version).toEqual('10.10.0'); + expect(updatedPackages.get('e')!.dependencies!['c']).toEqual('workspace:~5.0.0'); + }); + + it('does not change packageJson if not needed by individual version policy', () => { + versionManager.ensure('testPolicy3'); + const updatedPackages: Map = versionManager.updatedProjects; + expect(updatedPackages.size).toEqual(0); + }); + }); + + describe('bump', () => { + it('bumps to prerelease version', () => { + versionManager.bump('testPolicy1', BumpType.prerelease, 'dev', false); + const updatedPackages: Map = versionManager.updatedProjects; + const expectedVersion: string = '10.10.1-dev.0'; + + const changeFiles: Map = versionManager.changeFiles; + + expect(updatedPackages.get('a')!.version).toEqual(expectedVersion); + expect(updatedPackages.get('b')!.version).toEqual(expectedVersion); + expect(updatedPackages.get('e')!.version).toEqual(expectedVersion); + expect(updatedPackages.get('g')!.devDependencies!['a']).toEqual(`workspace:~${expectedVersion}`); + expect(_getChanges(changeFiles, 'a')).not.toBeDefined(); + expect(_getChanges(changeFiles, 'b')).not.toBeDefined(); + }); + }); + /* eslint-enable dot-notation */ +}); diff --git a/apps/rush-lib/src/logic/test/workspacePackages/a/CHANGELOG.json b/apps/rush-lib/src/logic/test/workspacePackages/a/CHANGELOG.json new file mode 100644 index 00000000000..ad83830451d --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/a/CHANGELOG.json @@ -0,0 +1,10 @@ +{ + "name": "a", + "entries": [ + { + "version": "1.0.0", + "date": "Fri, Jul 21, 2017 22:30:12 PM", + "comments": {} + } + ] +} \ No newline at end of file diff --git a/apps/rush-lib/src/logic/test/workspacePackages/a/package.json b/apps/rush-lib/src/logic/test/workspacePackages/a/package.json new file mode 100644 index 00000000000..e57f46f8473 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/a/package.json @@ -0,0 +1,5 @@ +{ + "name": "a", + "version": "1.0.0", + "description": "Test package a" +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/b/package.json b/apps/rush-lib/src/logic/test/workspacePackages/b/package.json new file mode 100644 index 00000000000..ca4d5662d97 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/b/package.json @@ -0,0 +1,8 @@ +{ + "name": "b", + "version": "1.0.0", + "description": "Test package b", + "dependencies": { + "a": "workspace:>=1.0.0 <2.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/c/package.json b/apps/rush-lib/src/logic/test/workspacePackages/c/package.json new file mode 100644 index 00000000000..91588ae3627 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/c/package.json @@ -0,0 +1,9 @@ +{ + "name": "c", + "version": "1.0.0", + "description": "Test package c", + "dependencies": { + "b": "workspace:>=1.0.0 <2.0.0", + "handlebars": "~4.0.11" + } +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/common/package.json b/apps/rush-lib/src/logic/test/workspacePackages/common/package.json new file mode 100644 index 00000000000..d49725bee24 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/common/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": {}, + "description": "Temporary file generated by the Rush tool", + "name": "rush-common", + "private": true, + "version": "0.0.0" +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-1/package.json b/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-1/package.json new file mode 100644 index 00000000000..855f3fd74cb --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-1/package.json @@ -0,0 +1,8 @@ +{ + "name": "cyclic-dep-1", + "version": "1.0.0", + "description": "cyclic-dep-1", + "dependencies": { + "cyclic-dep-2": "workspace:>=1.0.0 <2.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-2/package.json b/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-2/package.json new file mode 100644 index 00000000000..c07faa152ca --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-2/package.json @@ -0,0 +1,8 @@ +{ + "name": "cyclic-dep-2", + "version": "1.0.0", + "description": "cyclic-dep-2", + "dependencies": { + "cyclic-dep-1": "workspace:>=1.0.0 <2.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-explicit-1/package.json b/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-explicit-1/package.json new file mode 100644 index 00000000000..441e0e17faa --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-explicit-1/package.json @@ -0,0 +1,8 @@ +{ + "name": "cyclic-dep-explicit-1", + "version": "1.0.0", + "description": "cyclic-dep-explicit-1", + "dependencies": { + "cyclic-dep-explicit-2": "workspace:>=1.0.0 <2.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-explicit-2/package.json b/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-explicit-2/package.json new file mode 100644 index 00000000000..6308d48dec1 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/cyclic-dep-explicit-2/package.json @@ -0,0 +1,8 @@ +{ + "name": "cyclic-dep-explicit-2", + "version": "1.0.0", + "description": "cyclic-dep-explicit-2", + "dependencies": { + "cyclic-dep-explicit-1": ">=1.0.0 <2.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/d/package.json b/apps/rush-lib/src/logic/test/workspacePackages/d/package.json new file mode 100644 index 00000000000..f6ae89b73d6 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/d/package.json @@ -0,0 +1,8 @@ +{ + "name": "d", + "version": "1.0.0", + "description": "Test package d", + "dependencies": { + "c": "workspace:1.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspacePackages/rush.json b/apps/rush-lib/src/logic/test/workspacePackages/rush.json new file mode 100644 index 00000000000..4c4825157f4 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspacePackages/rush.json @@ -0,0 +1,61 @@ +{ + "npmVersion": "3.10.8", + "rushVersion": "1.0.5", + "projectFolderMinDepth": 1, + "hotfixChangeEnabled": true, + "approvedPackagesPolicy": { + "reviewCategories": ["first-party", "third-party", "prototype"], + "ignoredNpmScopes": ["@types", "@internal"] + }, + "projects": [ + { + "packageName": "a", + "projectFolder": "a", + "reviewCategory": "third-party", + "shouldPublish": true + }, + { + "packageName": "b", + "projectFolder": "b", + "reviewCategory": "third-party", + "shouldPublish": true + }, + { + "packageName": "c", + "projectFolder": "c", + "reviewCategory": "third-party", + "shouldPublish": true + }, + { + "packageName": "d", + "projectFolder": "d", + "reviewCategory": "third-party", + "shouldPublish": true + }, + { + "packageName": "cyclic-dep-1", + "projectFolder": "cyclic-dep-1", + "reviewCategory": "third-party", + "shouldPublish": true + }, + { + "packageName": "cyclic-dep-2", + "projectFolder": "cyclic-dep-2", + "reviewCategory": "third-party", + "shouldPublish": true + }, + { + "packageName": "cyclic-dep-explicit-1", + "projectFolder": "cyclic-dep-explicit-1", + "reviewCategory": "third-party", + "shouldPublish": true + }, + { + "packageName": "cyclic-dep-explicit-2", + "projectFolder": "cyclic-dep-explicit-2", + "reviewCategory": "third-party", + "shouldPublish": true, + "cyclicDependencyProjects": ["cyclic-dep-explicit-1"] + } + ] +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/a/package.json b/apps/rush-lib/src/logic/test/workspaceRepo/a/package.json new file mode 100644 index 00000000000..e57f46f8473 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/a/package.json @@ -0,0 +1,5 @@ +{ + "name": "a", + "version": "1.0.0", + "description": "Test package a" +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/b/package.json b/apps/rush-lib/src/logic/test/workspaceRepo/b/package.json new file mode 100644 index 00000000000..1707e03417a --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/b/package.json @@ -0,0 +1,8 @@ +{ + "name": "b", + "version": "2.0.0", + "description": "Test package b", + "dependencies": { + "a": "workspace:~1.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/c/package.json b/apps/rush-lib/src/logic/test/workspaceRepo/c/package.json new file mode 100644 index 00000000000..436566d07c7 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/c/package.json @@ -0,0 +1,8 @@ +{ + "name": "c", + "version": "3.1.1", + "description": "Test package c", + "dependencies": { + "b": "workspace:>=2.0.0 <3.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/changes/a.json b/apps/rush-lib/src/logic/test/workspaceRepo/changes/a.json new file mode 100644 index 00000000000..43f7263c5e8 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/changes/a.json @@ -0,0 +1,9 @@ +{ + "changes": [ + { + "packageName": "a", + "type": "patch", + "comment": "Patching a" + } + ] +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/changes/b.json b/apps/rush-lib/src/logic/test/workspaceRepo/changes/b.json new file mode 100644 index 00000000000..6e3320bd6e8 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/changes/b.json @@ -0,0 +1,9 @@ +{ + "changes": [ + { + "packageName": "b", + "type": "patch", + "comment": "Patching b" + } + ] +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/changes/c.json b/apps/rush-lib/src/logic/test/workspaceRepo/changes/c.json new file mode 100644 index 00000000000..81eaa9cb51c --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/changes/c.json @@ -0,0 +1,9 @@ +{ + "changes": [ + { + "packageName": "c", + "type": "patch", + "comment": "Patching c" + } + ] +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/changes/d.json b/apps/rush-lib/src/logic/test/workspaceRepo/changes/d.json new file mode 100644 index 00000000000..abccc8a5d4e --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/changes/d.json @@ -0,0 +1,9 @@ +{ + "changes": [ + { + "packageName": "d", + "type": "patch", + "comment": "Patching d" + } + ] +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/common/config/rush/version-policies.json b/apps/rush-lib/src/logic/test/workspaceRepo/common/config/rush/version-policies.json new file mode 100644 index 00000000000..ad0b40cd496 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/common/config/rush/version-policies.json @@ -0,0 +1,17 @@ +[ + { + "policyName": "testPolicy1", + "definitionName": "lockStepVersion", + "version": "10.10.0", + "nextBump": "patch" + }, + { + "policyName": "testPolicy2", + "definitionName": "individualVersion", + "lockedMajor": 5 + }, + { + "policyName": "testPolicy3", + "definitionName": "individualVersion" + } +] diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/d/package.json b/apps/rush-lib/src/logic/test/workspaceRepo/d/package.json new file mode 100644 index 00000000000..d3ff1341f07 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/d/package.json @@ -0,0 +1,8 @@ +{ + "name": "d", + "version": "4.1.1", + "description": "Test package d", + "dependencies": { + "b": "workspace:>=2.0.0 <3.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/e/package.json b/apps/rush-lib/src/logic/test/workspaceRepo/e/package.json new file mode 100644 index 00000000000..e25c11d8daf --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/e/package.json @@ -0,0 +1,8 @@ +{ + "name": "e", + "version": "10.10.0", + "description": "Test package e", + "dependencies": { + "c": "workspace:~3.1.1" + } +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/f/package.json b/apps/rush-lib/src/logic/test/workspaceRepo/f/package.json new file mode 100644 index 00000000000..be93f30c2c5 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/f/package.json @@ -0,0 +1,8 @@ +{ + "name": "f", + "version": "1.0.0", + "description": "Test package f", + "dependencies": { + "a": "workspace:~1.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/g/package.json b/apps/rush-lib/src/logic/test/workspaceRepo/g/package.json new file mode 100644 index 00000000000..58fb54644ea --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/g/package.json @@ -0,0 +1,8 @@ +{ + "name": "g", + "version": "0.0.1", + "description": "Test package g", + "devDependencies": { + "a": "workspace:~1.0.0" + } +} diff --git a/apps/rush-lib/src/logic/test/workspaceRepo/rush.json b/apps/rush-lib/src/logic/test/workspaceRepo/rush.json new file mode 100644 index 00000000000..c5bc0d96fc2 --- /dev/null +++ b/apps/rush-lib/src/logic/test/workspaceRepo/rush.json @@ -0,0 +1,47 @@ +{ + "npmVersion": "3.10.8", + "rushVersion": "1.0.5", + + "projects": [ + { + "packageName": "a", + "projectFolder": "a", + "reviewCategory": "third-party", + "versionPolicyName": "testPolicy1" + }, + { + "packageName": "b", + "projectFolder": "b", + "reviewCategory": "third-party", + "versionPolicyName": "testPolicy1" + }, + { + "packageName": "c", + "projectFolder": "c", + "reviewCategory": "third-party", + "versionPolicyName": "testPolicy2" + }, + { + "packageName": "d", + "projectFolder": "d", + "reviewCategory": "third-party", + "versionPolicyName": "testPolicy3" + }, + { + "packageName": "e", + "projectFolder": "e", + "reviewCategory": "third-party", + "versionPolicyName": "testPolicy1" + }, + { + "packageName": "f", + "projectFolder": "f", + "reviewCategory": "third-party" + }, + { + "packageName": "g", + "projectFolder": "g", + "reviewCategory": "third-party" + } + ] +} diff --git a/apps/rush-lib/src/logic/yarn/YarnShrinkwrapFile.ts b/apps/rush-lib/src/logic/yarn/YarnShrinkwrapFile.ts index 6182f5b42df..225d8e62482 100644 --- a/apps/rush-lib/src/logic/yarn/YarnShrinkwrapFile.ts +++ b/apps/rush-lib/src/logic/yarn/YarnShrinkwrapFile.ts @@ -247,4 +247,22 @@ export class YarnShrinkwrapFile extends BaseShrinkwrapFile { ): DependencySpecifier | undefined { throw new InternalError('Not implemented'); } + + /** @override */ + public getWorkspaceKeys(): ReadonlyArray { + throw new InternalError('Not implemented'); + } + + /** @override */ + public getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string { + throw new InternalError('Not implemented'); + } + + /** @override */ + protected getWorkspaceDependencyVersion( + dependencySpecifier: DependencySpecifier, + workspaceKey: string + ): DependencySpecifier | undefined { + throw new InternalError('Not implemented'); + } } diff --git a/apps/rush-lib/src/schemas/repo-state.schema.json b/apps/rush-lib/src/schemas/repo-state.schema.json new file mode 100644 index 00000000000..916f2fb69aa --- /dev/null +++ b/apps/rush-lib/src/schemas/repo-state.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Rush repo-state.json file", + "description": "For use with the Rush tool, this file tracks the state of various features in the Rush repo. See http://rushjs.io for details.", + + "type": "object", + "properties": { + "$schema": { + "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", + "type": "string" + }, + "pnpmShrinkwrapHash": { + "description": "A hash of the contents of the PNPM shrinkwrap file for the repository. This hash is used to determine whether or not the shrinkwrap has been modified prior to install.", + "type": "string" + }, + "preferredVersionsHash": { + "description": "A hash of \"preferred versions\" for the repository. This hash is used to determine whether or not preferred versions have been modified prior to install.", + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/apps/rush-lib/src/schemas/rush.schema.json b/apps/rush-lib/src/schemas/rush.schema.json index 2637f0872c1..800142df43d 100644 --- a/apps/rush-lib/src/schemas/rush.schema.json +++ b/apps/rush-lib/src/schemas/rush.schema.json @@ -106,6 +106,10 @@ "preventManualShrinkwrapChanges": { "description": "If true, then \"rush install\" will report an error if manual modifications were made to the PNPM shrinkwrap file without running `rush update` afterwards. To temporarily disable this validation when invoking \"rush install\", use the \"--bypassPolicy\" command-line parameter. The default value is false.", "type": "boolean" + }, + "useWorkspaces": { + "description": "If true, then Rush will use the workspaces feature to install and link packages when invoking PNPM. The default value is false.", + "type": "boolean" } }, "additionalProperties": false diff --git a/apps/rush-lib/src/utilities/Utilities.ts b/apps/rush-lib/src/utilities/Utilities.ts index 21d143c2673..4cec356fc3d 100644 --- a/apps/rush-lib/src/utilities/Utilities.ts +++ b/apps/rush-lib/src/utilities/Utilities.ts @@ -19,6 +19,18 @@ export interface IEnvironment { [environmentVariableName: string]: string | undefined; } +/** + * Options for Utilities.executeCommand(). + */ +export interface IExecuteCommandOptions { + command: string; + args: string[]; + workingDirectory: string; + environment?: IEnvironment; + suppressOutput?: boolean; + keepEnvironment?: boolean; +} + /** * Options for Utilities.installPackageInDirectory(). */ @@ -268,25 +280,20 @@ export class Utilities { } /* - * Returns true if outputFilename has a more recent last modified timestamp - * than all of the inputFilenames, which would imply that we don't need to rebuild it. - * Returns false if any of the files does not exist. + * Returns true if dateToCompare is more recent than all of the inputFilenames, which + * would imply that we don't need to rebuild it. Returns false if any of the files + * does not exist. * NOTE: The filenames can also be paths for directories, in which case the directory * timestamp is compared. */ - public static isFileTimestampCurrent(outputFilename: string, inputFilenames: string[]): boolean { - if (!FileSystem.exists(outputFilename)) { - return false; - } - const outputStats: fs.Stats = FileSystem.getStatistics(outputFilename); - + public static isFileTimestampCurrent(dateToCompare: Date, inputFilenames: string[]): boolean { for (const inputFilename of inputFilenames) { if (!FileSystem.exists(inputFilename)) { return false; } const inputStats: fs.Stats = FileSystem.getStatistics(inputFilename); - if (outputStats.mtime < inputStats.mtime) { + if (dateToCompare < inputStats.mtime) { return false; } } @@ -326,21 +333,14 @@ export class Utilities { * Executes the command with the specified command-line parameters, and waits for it to complete. * The current directory will be set to the specified workingDirectory. */ - public static executeCommand( - command: string, - args: string[], - workingDirectory: string, - environment?: IEnvironment, - suppressOutput: boolean = false, - keepEnvironment: boolean = false - ): void { + public static executeCommand(options: IExecuteCommandOptions): void { Utilities._executeCommandInternal( - command, - args, - workingDirectory, - suppressOutput ? undefined : [0, 1, 2], - environment, - keepEnvironment + options.command, + options.args, + options.workingDirectory, + options.suppressOutput ? undefined : [0, 1, 2], + options.environment, + options.keepEnvironment ); } @@ -371,12 +371,8 @@ export class Utilities { * Attempts to run Utilities.executeCommand() up to maxAttempts times before giving up. */ public static executeCommandWithRetry( + options: IExecuteCommandOptions, maxAttempts: number, - command: string, - args: string[], - workingDirectory: string, - environment?: IEnvironment, - suppressOutput: boolean = false, retryCallback?: () => void ): void { if (maxAttempts < 1) { @@ -387,10 +383,10 @@ export class Utilities { for (;;) { try { - Utilities.executeCommand(command, args, workingDirectory, environment, suppressOutput); + Utilities.executeCommand(options); } catch (error) { console.log(os.EOL + 'The command failed:'); - console.log(` ${command} ` + args.join(' ')); + console.log(` ${options.command} ` + options.args.join(' ')); console.log(`ERROR: ${error.toString()}`); if (attemptNumber < maxAttempts) { @@ -486,12 +482,14 @@ export class Utilities { // NOTE: Here we use whatever version of NPM we happen to find in the PATH Utilities.executeCommandWithRetry( - options.maxInstallAttempts, - 'npm', - ['install'], - directory, - Utilities._createEnvironmentForRushCommand({}), - options.suppressOutput + { + command: 'npm', + args: ['install'], + workingDirectory: directory, + environment: Utilities._createEnvironmentForRushCommand({}), + suppressOutput: options.suppressOutput + }, + options.maxInstallAttempts ); } @@ -574,6 +572,23 @@ export class Utilities { FileSystem.writeFile(targetNpmrcPath, resultLines.join(os.EOL)); } + /** + * Copies the file "sourcePath" to "destinationPath", overwriting the target file location. + * If the source file does not exist, then the target file is deleted. + */ + public static syncFile(sourcePath: string, destinationPath: string): void { + if (FileSystem.exists(sourcePath)) { + console.log(`Updating ${destinationPath}`); + FileSystem.copyFile({ sourcePath, destinationPath }); + } else { + if (FileSystem.exists(destinationPath)) { + // If the source file doesn't exist and there is one in the target, delete the one in the target + console.log(`Deleting ${destinationPath}`); + FileSystem.deleteFile(destinationPath); + } + } + } + /** * syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file. * If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder. diff --git a/apps/rush/package.json b/apps/rush/package.json index c389a9b1047..fdf48bda16e 100644 --- a/apps/rush/package.json +++ b/apps/rush/package.json @@ -34,7 +34,7 @@ "@rushstack/node-core-library": "3.24.4", "@microsoft/rush-lib": "5.27.3", "colors": "~1.2.1", - "semver": "~5.3.0" + "semver": "~7.3.0" }, "devDependencies": { "@microsoft/rush-stack-compiler-3.5": "0.8.0", @@ -42,7 +42,7 @@ "@rushstack/eslint-config": "1.0.2", "@types/jest": "25.2.1", "@types/node": "10.17.13", - "@types/semver": "5.3.33", + "@types/semver": "~7.3.1", "@types/sinon": "1.16.34", "chai": "~3.5.0", "gulp": "~4.0.2", diff --git a/build-tests/api-extractor-test-02/package.json b/build-tests/api-extractor-test-02/package.json index d1c0804ff43..4ce70aed023 100644 --- a/build-tests/api-extractor-test-02/package.json +++ b/build-tests/api-extractor-test-02/package.json @@ -9,9 +9,9 @@ "build": "node build.js" }, "dependencies": { - "@types/semver": "5.3.33", + "@types/semver": "~7.3.1", "api-extractor-test-01": "1.0.0", - "semver": "~5.3.0" + "semver": "~7.3.0" }, "devDependencies": { "@microsoft/api-extractor": "7.9.0", diff --git a/common/changes/@microsoft/api-extractor/supportWorkspaces2_2020-07-03-04-54.json b/common/changes/@microsoft/api-extractor/supportWorkspaces2_2020-07-03-04-54.json new file mode 100644 index 00000000000..f55c74c0ad2 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/supportWorkspaces2_2020-07-03-04-54.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "", + "type": "none" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "3473356+D4N14L@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build-sass/supportWorkspaces2_2020-07-03-04-54.json b/common/changes/@microsoft/gulp-core-build-sass/supportWorkspaces2_2020-07-03-04-54.json new file mode 100644 index 00000000000..b962decb437 --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build-sass/supportWorkspaces2_2020-07-03-04-54.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/gulp-core-build-sass", + "comment": "", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build-sass", + "email": "3473356+D4N14L@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/gulp-core-build/supportWorkspaces2_2020-06-17-19-59.json b/common/changes/@microsoft/gulp-core-build/supportWorkspaces2_2020-06-17-19-59.json new file mode 100644 index 00000000000..629de4dfae3 --- /dev/null +++ b/common/changes/@microsoft/gulp-core-build/supportWorkspaces2_2020-06-17-19-59.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/gulp-core-build", + "comment": "", + "type": "none" + } + ], + "packageName": "@microsoft/gulp-core-build", + "email": "3473356+D4N14L@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/supportWorkspaces2_2020-06-17-19-59.json b/common/changes/@microsoft/rush/supportWorkspaces2_2020-06-17-19-59.json new file mode 100644 index 00000000000..d918ca72cfb --- /dev/null +++ b/common/changes/@microsoft/rush/supportWorkspaces2_2020-06-17-19-59.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add preliminary workspaces support for PNPM", + "type": "minor" + } + ], + "packageName": "@microsoft/rush", + "email": "3473356+D4N14L@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@rushstack/node-core-library/supportWorkspaces2_2020-06-17-19-59.json b/common/changes/@rushstack/node-core-library/supportWorkspaces2_2020-06-17-19-59.json new file mode 100644 index 00000000000..2f920ab86a1 --- /dev/null +++ b/common/changes/@rushstack/node-core-library/supportWorkspaces2_2020-06-17-19-59.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Add a utility method to convert a map into an object", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library", + "email": "3473356+D4N14L@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index fec83d2528b..723f15e992d 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -118,7 +118,7 @@ dependencies: '@types/orchestrator': 0.0.30 '@types/read-package-tree': 5.1.0 '@types/resolve': 1.17.1 - '@types/semver': 5.3.33 + '@types/semver': 7.3.1 '@types/serve-static': 1.13.1 '@types/sinon': 1.16.34 '@types/source-map': 0.5.0 @@ -205,7 +205,7 @@ dependencies: pseudolocale: 1.1.0 read-package-tree: 5.1.6 resolve: 1.17.0 - semver: 5.3.0 + semver: 7.3.2 sinon: 1.17.7 source-map: 0.6.1 strict-uri-encode: 2.0.0 @@ -1385,6 +1385,12 @@ packages: dev: false resolution: integrity: sha512-UwrBgjsRS8BSsckIEdrAhIAmdh0MJidtKTvD3S6tpMq6qHLY3uGaNYcRDUjPxpF4hOAOEbMNSXhhfxmNHB1QNQ== + /@types/semver/7.3.1: + dependencies: + '@types/node': 10.17.13 + dev: false + resolution: + integrity: sha512-ooD/FJ8EuwlDKOI6D9HWxgIgJjMg2cuziXm/42npDC8y4NjxplBUn9loewZiBNCt44450lHAU0OSb51/UqXeag== /@types/serve-static/1.13.1: dependencies: '@types/express-serve-static-core': 4.11.0 @@ -14210,7 +14216,7 @@ packages: dev: false name: '@rush-temp/api-documenter-test' resolution: - integrity: sha512-Qk1biNNk3nWYenDTiIqYW70vLuReSnw31Rc2Fd85le2izmqMzpVYoisw+KF8ja6Xt9ypQd1h0eUvDy31hTEyDQ== + integrity: sha512-N3sX6Wn+j3iX5n/0/hgQlE9cGDGjicw2md5w8pICe3wZ+EdhPGKhb+/T9f+6/gLN7ze9UQK3r+Was0jn+15Yng== tarball: 'file:projects/api-documenter-test.tgz' version: 0.0.0 'file:projects/api-documenter.tgz': @@ -14228,7 +14234,7 @@ packages: dev: false name: '@rush-temp/api-documenter' resolution: - integrity: sha512-NP2auGxTmo90sgiJQaYfXT7rAr6LM//a9D2cwZOC8ll0AZrI1Zj7mkLiqGQzEkZ0E0qVqoM2RHbl4AeJqq4XOg== + integrity: sha512-26IFpsCHnC8xswRADM6yNgyjSpamM/4dr2HQ0i2vJP4+T1CAp+fQB/c/6DTAPSrkRJh4JoUk+2vEAnyKdyxX+A== tarball: 'file:projects/api-documenter.tgz' version: 0.0.0 'file:projects/api-extractor-lib1-test.tgz': @@ -14239,7 +14245,7 @@ packages: dev: false name: '@rush-temp/api-extractor-lib1-test' resolution: - integrity: sha512-j4cGvRknTDXwBbM7rx7X+V62UfeTK9kAGkSpT3GpaZrRTwbtZhzPWyqY9lzw6tN43xlX/Iuc1g4FmnVDe1lqWA== + integrity: sha512-GJEYULnJp6VrMhne9cAQ+6sG7eqQcLfOErf4GkzxPjTm8Rcw41y0loj+Fg39XDTkHxIP4+7RVgUCBHnpCo044w== tarball: 'file:projects/api-extractor-lib1-test.tgz' version: 0.0.0 'file:projects/api-extractor-lib2-test.tgz': @@ -14251,7 +14257,7 @@ packages: dev: false name: '@rush-temp/api-extractor-lib2-test' resolution: - integrity: sha512-0u0G7fscl+qgsZ7GNjTcRgAKGFVUvrk8UhgIe0bHUQhwruHEjrTvpjdM0HbsdOhfVt63fn+AkP4q0JCK15ZxKQ== + integrity: sha512-GSbBTZXZLdyBB5tDty+Z3KVQASBC++6ATAXQexS+lKWUL+0+sypToNUxrakMqySNdxa2lkguApHEERuR/ux49A== tarball: 'file:projects/api-extractor-lib2-test.tgz' version: 0.0.0 'file:projects/api-extractor-lib3-test.tgz': @@ -14263,7 +14269,7 @@ packages: dev: false name: '@rush-temp/api-extractor-lib3-test' resolution: - integrity: sha512-W+ZrYIx1rYjFin/GB0CmDltXHLL3OsJ9xxoPcH5wuZQWODiP6bBD6rpeREeoZ3webIwF7yz5RWKZ27PDgUCJDg== + integrity: sha512-ZsBehU1FmzR9k3oWlFo14DVTzOIAPxuT8l3KamEMnUJy/Jr2Hr8yDHq2BiWgOozf905CMC0VisJWK5MoJKZrRQ== tarball: 'file:projects/api-extractor-lib3-test.tgz' version: 0.0.0 'file:projects/api-extractor-model.tgz': @@ -14291,7 +14297,7 @@ packages: dev: false name: '@rush-temp/api-extractor-scenarios' resolution: - integrity: sha512-eUE0t2GtLcGwT3WjFlgrRtxwNKaizkYauWQew1fRglDIMQp+KBwslGEVkqq60fy9vUQF5ccnP+Vp9l42kxrXAw== + integrity: sha512-sL6YBKqdUloUWJMuaH8atKi2wjaao5DWeDcDnGBz/F91AM9WKvyxIFIVk2GfMhPdDqSIG1CofGWlDjJCZf1g4A== tarball: 'file:projects/api-extractor-scenarios.tgz' version: 0.0.0 'file:projects/api-extractor-test-01.tgz': @@ -14305,20 +14311,20 @@ packages: dev: false name: '@rush-temp/api-extractor-test-01' resolution: - integrity: sha512-gtQg1x+6BbSPaQJzE2lXPhYuASORkogG94ctPkiyst6rEYlHifzW0rPB8TyEo+SjzcsVNil6Q8UygxxtBkR/dg== + integrity: sha512-ivgavBxw3KbtXdnokXG6NJTx3PB6e5UOs9hfpdRbEnbYgXe02UEpxq2i6yeyxC4mONjsRu7Srp9nQ0bgtOg7VA== tarball: 'file:projects/api-extractor-test-01.tgz' version: 0.0.0 'file:projects/api-extractor-test-02.tgz': dependencies: '@types/node': 10.17.13 - '@types/semver': 5.3.33 + '@types/semver': 7.3.1 fs-extra: 7.0.1 - semver: 5.3.0 + semver: 7.3.2 typescript: 3.9.5 dev: false name: '@rush-temp/api-extractor-test-02' resolution: - integrity: sha512-Omu2+uEnL+lWKpDSyzZITCAMVmH8rx6Y18u84GNt5CYiVHatjpOTco0mt0vgXE6BZn4/W9SCNtSirUZt6hDzWA== + integrity: sha512-bLdmbASOurd5ETsUGmQzu5d82bNCcIbsvtopdksrjqHVur62TBlK80EiegmoXDe8J3V5MYgI92KPHYgHG9iFtg== tarball: 'file:projects/api-extractor-test-02.tgz' version: 0.0.0 'file:projects/api-extractor-test-03.tgz': @@ -14340,7 +14346,7 @@ packages: dev: false name: '@rush-temp/api-extractor-test-04' resolution: - integrity: sha512-kuJ7xZ9rFAB1Xha/iTothhJRqXIUF2s3t+c6hj6Ls8jufjpo+jwnk2ho4TrjzCI7hzGP7ydLnXywFKZhZ0GuIQ== + integrity: sha512-FpnFl6J0BRmdkq6Y7Mvv+HM3yOQUEZVB5rvmusfxmDk1eLfdIVJqSBmlbAmefVOeRLozfTca3rL/t02nU+VvbQ== tarball: 'file:projects/api-extractor-test-04.tgz' version: 0.0.0 'file:projects/api-extractor.tgz': @@ -14352,18 +14358,18 @@ packages: '@types/lodash': 4.14.116 '@types/node': 10.17.13 '@types/resolve': 1.17.1 - '@types/semver': 5.3.33 + '@types/semver': 7.3.1 colors: 1.2.5 gulp: 4.0.2 lodash: 4.17.15 resolve: 1.17.0 - semver: 5.3.0 + semver: 7.3.2 source-map: 0.6.1 typescript: 3.9.5 dev: false name: '@rush-temp/api-extractor' resolution: - integrity: sha512-SPDsuk+UHLaWEUHD7HIjB4jisVMV/JgIstW5vjnQADDENYwSEicA0vnH6j4CIfZHDMq8Gf2y73XMLLmtdS5DeQ== + integrity: sha512-TnLRREXVw5WT36j+XkIIx1SyOuoxJTJL1ruaT572GLqDAEs+8SjxjxKb59H+XltYL8LZTjlUrusiXjS/Xkj4xQ== tarball: 'file:projects/api-extractor.tgz' version: 0.0.0 'file:projects/debug-certificate-manager.tgz': @@ -14378,7 +14384,7 @@ packages: dev: false name: '@rush-temp/debug-certificate-manager' resolution: - integrity: sha512-RCmD4L/QRni3fAbW7QQIZWxgQ2BxCoaMBHhJNl+MN7Bt4mBZ7jZf1770fNzTba8TTXpq5RasjUU7ihGEL7qsOQ== + integrity: sha512-w4g2wfUNni3VvvwfSwurrF8lzrtL2iRs8Wh+rltj+d4mad+kUYlJKTaatUC7QH47z3aOhBvXgQ6z/cNVlHMfLQ== tarball: 'file:projects/debug-certificate-manager.tgz' version: 0.0.0 'file:projects/doc-plugin-rush-stack.tgz': @@ -14391,7 +14397,7 @@ packages: dev: false name: '@rush-temp/doc-plugin-rush-stack' resolution: - integrity: sha512-N6qFz5WIhWV7dvh2FWM8knYWOx7AHQHg8LYJWTTXnfWHeJx74XlKiK+EzVYmOEMNXKCFj8ihIvNmUMBNZpUlzw== + integrity: sha512-S9Su0lWdXAS+9hD874FIYZMDOS+QhcGHk1LuhMsHqAmKs57IRnIbKQY8J2CBe4u4oCXXvqHf/tM6VlLLsBbp+A== tarball: 'file:projects/doc-plugin-rush-stack.tgz' version: 0.0.0 'file:projects/eslint-config.tgz': @@ -14448,7 +14454,7 @@ packages: dev: false name: '@rush-temp/generate-api-docs' resolution: - integrity: sha512-kYuy0KhmbVlRgzysnD3WPVUVnWCIELSGDkiyY9ggRdvrTAbBacjYEPbYp7qep6BXAzdqd7eDPuWv6e3mzwvVvg== + integrity: sha512-P5sPB7nmoQSRPByV3DNoj6DvFTPLFfgfDvTMjh0q7GwQxjkcO5Fcjzi2dgn5Bw2NqY90fHQXg0mn3M1ECpy+0g== tarball: 'file:projects/generate-api-docs.tgz' version: 0.0.0 'file:projects/gulp-core-build-mocha.tgz': @@ -14492,7 +14498,7 @@ packages: dev: false name: '@rush-temp/gulp-core-build-sass' resolution: - integrity: sha512-Mlna3VhrcK35LdQBockrHzexTwQj1LnjHrL+vlhi70m9UcCgrXSucynGMPNvGDHCv9O0mUC2O/XIFoUXdf8eeQ== + integrity: sha512-H43Cg5Pi1tkvprKzPbmwfy1FR5XAO4F+HlSDLl8PQ4j0kbchC1QG4xmw3JyYlZoytXDj+bBLPemKvbH4w3nuRw== tarball: 'file:projects/gulp-core-build-sass.tgz' version: 0.0.0 'file:projects/gulp-core-build-serve.tgz': @@ -14515,7 +14521,7 @@ packages: dev: false name: '@rush-temp/gulp-core-build-serve' resolution: - integrity: sha512-EcDZupb0dv3v+0Yve3aVwNEK0amNhR8o7nwaVOx+gOxhxXdUW1D2gv0ekKomJhO+m2xnWngGiCIY4qBGQlnwPA== + integrity: sha512-WqO9Fxz/JzIDWu43Jjlz7UM8Ndsj7uT+UJwCyKZYHiDzlpd9iy3ZyYg5PZmC//qFd2v0V+DdRMdKNdtX/R2Hyg== tarball: 'file:projects/gulp-core-build-serve.tgz' version: 0.0.0 'file:projects/gulp-core-build-typescript.tgz': @@ -14534,7 +14540,7 @@ packages: dev: false name: '@rush-temp/gulp-core-build-typescript' resolution: - integrity: sha512-6wSeAy3PrwsXxBCmWnAtqK/xVuu40Nb42vQ/ym0fQ50mTTRDB6qhTgY+Yl3wseHBtf4UvVX8JaZExsaQdLhqqg== + integrity: sha512-SlNQaM+F6T7rtrTu0fHQDOxB6qxnyyyzbIapwWF40Wii+Rkl/8M4v2uIpuH0bkdTn3jjKDukj6VneWiIft16Dw== tarball: 'file:projects/gulp-core-build-typescript.tgz' version: 0.0.0 'file:projects/gulp-core-build-webpack.tgz': @@ -14552,7 +14558,7 @@ packages: dev: false name: '@rush-temp/gulp-core-build-webpack' resolution: - integrity: sha512-QC5XgdjN+DvNrs3GkM/txMlLSL+cWXdSrVyr4x4VKCaZtTUrdePomtnC0EptxRLIiDHdkP7L8kPxcJi5uGE69A== + integrity: sha512-vI88woBOs3twABx66INX2c34IeqcPY+c1pVFnCSxkE+rz1xUmDU/eScAAiYDZ4gM49IYqNfLSgRF7hP4ZLFwUg== tarball: 'file:projects/gulp-core-build-webpack.tgz' version: 0.0.0 'file:projects/gulp-core-build.tgz': @@ -14570,7 +14576,7 @@ packages: '@types/node': 10.17.13 '@types/node-notifier': 0.0.28 '@types/orchestrator': 0.0.30 - '@types/semver': 5.3.33 + '@types/semver': 7.3.1 '@types/through2': 2.0.32 '@types/vinyl': 2.0.3 '@types/yargs': 0.0.34 @@ -14596,7 +14602,7 @@ packages: object-assign: 4.1.1 orchestrator: 0.3.8 pretty-hrtime: 1.0.3 - semver: 5.3.0 + semver: 7.3.2 through2: 2.0.5 vinyl: 2.2.0 xml: 1.0.1 @@ -14605,7 +14611,7 @@ packages: dev: false name: '@rush-temp/gulp-core-build' resolution: - integrity: sha512-h8DdNgMxQdLqlazyoNc0dKWCguq14N09od1C9pRC3QTL3nBEtmoAQY/cbFiAu5gu3s5DE3O1FD+s4c+4jJ7zBA== + integrity: sha512-FwoXkTqeyxoYFYrfi3OD9nJeRo4Kei5n0nmm+i5wVW2zK8wn/hpYyWrZfNMZG+OtreFhQ6kXDpwqUOXh+JK3aA== tarball: 'file:projects/gulp-core-build.tgz' version: 0.0.0 'file:projects/heft.tgz': @@ -14620,11 +14626,10 @@ packages: resolve: 1.17.0 tapable: 1.1.3 true-case-path: 2.2.1 - typescript: 3.7.5 dev: false name: '@rush-temp/heft' resolution: - integrity: sha512-of6kz/4U9YQ+9xvHtrXWg1hQYlCEyrHJdUIptbeBpwJgBxS7DLn2ffzvWHm4Rts2Vs2NLiXm3OZS96tAoG5qSQ== + integrity: sha512-ZM/Il5u9m3Z6vORYQHEev9UE6gDysPrPFe92dh+cv/FDiDI9jk9h/x/OFs6mh53DgS1alKkOrJ4F92YJA2geCg== tarball: 'file:projects/heft.tgz' version: 0.0.0 'file:projects/load-themed-styles.tgz': @@ -14638,7 +14643,7 @@ packages: dev: false name: '@rush-temp/load-themed-styles' resolution: - integrity: sha512-01vIZKCCPY6M/uPP94vTdUGrFllQ7PFfmmdy/mY8oGDwETMqhahB08al3DizNfnf4mGCuWbvogPd3QvpLHQqlA== + integrity: sha512-N+8Bu3SSYU/ywsuMFcIEoYE/cia02gdfnE7+nkLV3BQTTmEWRWKCLLKMLuHYRNHNWfiBR54Xv63LcePz26PceA== tarball: 'file:projects/load-themed-styles.tgz' version: 0.0.0 'file:projects/loader-load-themed-styles.tgz': @@ -14654,7 +14659,7 @@ packages: dev: false name: '@rush-temp/loader-load-themed-styles' resolution: - integrity: sha512-p7Iw5IsjI2eu+A2NgaOiqndBPJ46DHdd/cpykA7DoS8nF8geTM6/74DuTnAWRzrVSbbEHT44+04zCeMZeu9Mrg== + integrity: sha512-peoYtJIZrwnKHPiSObHs/6ScZzVzrphJPQhaWQK/inqJ2mXo93mEcTjL4wzbDiubiR5JWb6IBla3OWyvM+/bBg== tarball: 'file:projects/loader-load-themed-styles.tgz' version: 0.0.0 'file:projects/loader-raw-script.tgz': @@ -14669,7 +14674,7 @@ packages: dev: false name: '@rush-temp/loader-raw-script' resolution: - integrity: sha512-g0YMLdY/Df2nQnUg3tP4osBSxrXDliwTFR8sD3qw5wrh7xtuF2JhsUXjMMeheAe31igxYUI2mYhPIsTUnDi1YQ== + integrity: sha512-pxNT3U49dpxqZEjjd7e98RP8W9wrPDVlxI/DgZRYaddlmknfWZlUg7gNYOihk0rr78V+5vsdl7T/xmk80u0eWw== tarball: 'file:projects/loader-raw-script.tgz' version: 0.0.0 'file:projects/localization-plugin-test-01.tgz': @@ -14684,7 +14689,7 @@ packages: dev: false name: '@rush-temp/localization-plugin-test-01' resolution: - integrity: sha512-T1h640rfmZr9IjXFfJU79OBnKR24s3d/T63izdP7l5QQSx0+ypQ3nZJX8cNK1LbuFPto31C7A0vcf/RjbM5j6w== + integrity: sha512-awh8RmQ7k/SeDz0aBtV9Q1rU/Oa0FcvKa1ZmiEueX95D/Q4dsx9sKIZ6EngI9wARKUBybnMd7S4XKhAfI/4sog== tarball: 'file:projects/localization-plugin-test-01.tgz' version: 0.0.0 'file:projects/localization-plugin-test-02.tgz': @@ -14701,7 +14706,7 @@ packages: dev: false name: '@rush-temp/localization-plugin-test-02' resolution: - integrity: sha512-oI6y8e286Uk1W1/153DjcpIWF+0hSST/JhOxzG83DI7SVNN49k/VWaM4YAXXmGe2WdhUsQDqh8sopm0HAhNq0w== + integrity: sha512-X2BlXkZD9TumA9AdUmKRmtJk/BDHIqgRZMXj34kwKB4vVgoYntLxdSGlcgSJNusHDlSXMNWMiT11PvorcjME2g== tarball: 'file:projects/localization-plugin-test-02.tgz' version: 0.0.0 'file:projects/localization-plugin-test-03.tgz': @@ -14716,7 +14721,7 @@ packages: dev: false name: '@rush-temp/localization-plugin-test-03' resolution: - integrity: sha512-1FZYUFCd91HyzzVV656MKZS/Pce9++sM5L6NhXZmKzFfKsDwFWhD3TbruN3L7qfDJ4fEAH9m64hVgYLcjA6Biw== + integrity: sha512-s87/fvvpIdQzrtl1c/HIjcgze/l29CYbv4r/9Hb0LPipsv9OQ8a3SAEI5R//YDzaCE3tFSlwuVq5A+Rpakxvkg== tarball: 'file:projects/localization-plugin-test-03.tgz' version: 0.0.0 'file:projects/localization-plugin.tgz': @@ -14737,7 +14742,7 @@ packages: dev: false name: '@rush-temp/localization-plugin' resolution: - integrity: sha512-NGBE5WLIbTDgzylanE3T46NtyiIzG1HhUKNIDwpmVQy8ZxD+IIqeWNHOM9utBEz/KzBVBMuKlfPg3vl+gOxMIw== + integrity: sha512-/vxTSha51J4tm9TCFijIma1f6pG5VV2mFofbypj5TaY9lnbVdqV+63WA1JFQgEf4qZSO0UyjZLCtdqeW6YLYsg== tarball: 'file:projects/localization-plugin.tgz' version: 0.0.0 'file:projects/node-core-library.tgz': @@ -14748,20 +14753,20 @@ packages: '@types/jest': 25.2.1 '@types/jju': 1.4.1 '@types/node': 10.17.13 - '@types/semver': 5.3.33 + '@types/semver': 7.3.1 '@types/timsort': 0.3.0 '@types/z-schema': 3.16.31 colors: 1.2.5 fs-extra: 7.0.1 gulp: 4.0.2 jju: 1.4.0 - semver: 5.3.0 + semver: 7.3.2 timsort: 0.3.0 z-schema: 3.18.4 dev: false name: '@rush-temp/node-core-library' resolution: - integrity: sha512-N7DPoPcUe2jkYGGaJa3MgkilIglNa2QZwGr0JSjzCGEsV0HfHl4T3k4kAdYD5l77b+iL/qdVzGviiaHlwOQQ6g== + integrity: sha512-+QxiCr+dzamzPCYnGHY5Ttuss+UuIKXMsxDpEEcYiQfPKYCdzS6tm22gJiwc1eG1Exd2sfqJ/ILdQdH0nYOOIw== tarball: 'file:projects/node-core-library.tgz' version: 0.0.0 'file:projects/node-library-build-eslint-test.tgz': @@ -14774,7 +14779,7 @@ packages: dev: false name: '@rush-temp/node-library-build-eslint-test' resolution: - integrity: sha512-hOP/rEc21giPwlkAXL9oMWM1kWDOwQfFbucvUvwFtnIvwRW5BU5hwfLUpLsjgiC5tQ1XBCLFojZqoGY8G9vRPQ== + integrity: sha512-MBzeUZk62MvhUzW3mLeVUXIgULkCgGUk8Ehi+4PXLIaEaOPCdBIFVGkZDhLnk39Eub/8Egjh7gH22T9po24Dbg== tarball: 'file:projects/node-library-build-eslint-test.tgz' version: 0.0.0 'file:projects/node-library-build-tslint-test.tgz': @@ -14787,7 +14792,7 @@ packages: dev: false name: '@rush-temp/node-library-build-tslint-test' resolution: - integrity: sha512-OSj8uA034CdrDT/08Nlptckd168hDytKHXXXUPAXQ6QiMBfKm8Z2g577vZyH1lfE9WWK9yRpVljs7+7DtFIMWg== + integrity: sha512-+D1eYp72IgyoC9cgAs/w9sWdUCo/MBe+oB4lQ8lQ69YEY42XsYVVGDyNABrRiiT/luUUU5ECciLhWT/Dpuvq4Q== tarball: 'file:projects/node-library-build-tslint-test.tgz' version: 0.0.0 'file:projects/node-library-build.tgz': @@ -14798,7 +14803,7 @@ packages: dev: false name: '@rush-temp/node-library-build' resolution: - integrity: sha512-3x3En+KDCO8RFVxwvrzP614SP8nUTc9R/5eAfUC5Gm0mzmLZaNHBFOXHANDWvbM6nCH09p7hK2QXmu93wHcw1A== + integrity: sha512-LKHrOcDmEhsJnedyGUHqoC3RwHDZptOrJsE2JpXJnBXOiwbq56y35mtSXCvfXaMtmX4rMZVBClFq7Hbrsfw7WQ== tarball: 'file:projects/node-library-build.tgz' version: 0.0.0 'file:projects/package-deps-hash.tgz': @@ -14812,7 +14817,7 @@ packages: dev: false name: '@rush-temp/package-deps-hash' resolution: - integrity: sha512-HvlJgTBK1wsoSq697epvJWYsuF0tu85yD+IXlBqkc77oub2YgpjP9An3CNxo7Kvm+FUnYCq3AoJFn62MVjgTSA== + integrity: sha512-teCk5NgTOrJdXvUaBfuWJH8BRxL05rTuh8kf6mprUkPKZ2hIpmvA7riXxuuVeW8PNtHozffrQZ4WWALTW/8AmA== tarball: 'file:projects/package-deps-hash.tgz' version: 0.0.0 'file:projects/repo-toolbox.tgz': @@ -14822,7 +14827,7 @@ packages: dev: false name: '@rush-temp/repo-toolbox' resolution: - integrity: sha512-YQulqxSmK0+VHyzZO8qHNVmkapqqZ7m2znjnH0pWmc75loYrUHMUFjxUPOIA6ZL5TK43/MtX/EHfOaxGVUqpXw== + integrity: sha512-kpl0zSrwq98YiabSjKn/C/fyNj0AaSmPQZoe5HvF7BJ/PHANhKt9oODwkO5d9BOItshbb5nUkowwt7mFhv1C6g== tarball: 'file:projects/repo-toolbox.tgz' version: 0.0.0 'file:projects/rush-buildxl.tgz': @@ -14833,7 +14838,7 @@ packages: dev: false name: '@rush-temp/rush-buildxl' resolution: - integrity: sha512-5z4k+kvvd7xBlG51bsnNCMBJTbCgoHzv8zzH5l8qNQoeS9MZ1K24xYvl4dfsq4OvGiOHwCEoX/zAzZH1LQTxbw== + integrity: sha512-daV3LDqarUTJvk3PBp9YDnyaWRToca3CGH08sGoDkbJGCKTF1qtuHio5hTO+tVB9BStBe0DqPxMQyn+cy01EGQ== tarball: 'file:projects/rush-buildxl.tgz' version: 0.0.0 'file:projects/rush-lib.tgz': @@ -14851,7 +14856,7 @@ packages: '@types/npm-packlist': 1.1.1 '@types/read-package-tree': 5.1.0 '@types/resolve': 1.17.1 - '@types/semver': 5.3.33 + '@types/semver': 7.3.1 '@types/strict-uri-encode': 2.0.0 '@types/tar': 4.0.3 '@types/wordwrap': 1.0.0 @@ -14876,7 +14881,7 @@ packages: npm-packlist: 2.1.2 read-package-tree: 5.1.6 resolve: 1.17.0 - semver: 5.3.0 + semver: 7.3.2 strict-uri-encode: 2.0.0 tar: 5.0.5 true-case-path: 2.2.1 @@ -14885,7 +14890,7 @@ packages: dev: false name: '@rush-temp/rush-lib' resolution: - integrity: sha512-wf96rkDvqiVcl0GQkxIzgkGVxWmY2A/dEZJorW6QSuU63gMXDDewvdM5o7hnraAVSPDJccTta4GhNkJ2+BXWdA== + integrity: sha512-s6KNN2WSiLi+AgoLQkeB5GTStGp4XVZNkF4E5LnIHRGxOkxW1PInIp3uMH5lq0goD5uVssQoSQu1m6e2dG5Cgg== tarball: 'file:projects/rush-lib.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.4-library-test.tgz': @@ -14895,7 +14900,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.4-library-test' resolution: - integrity: sha512-4VhQoGUInk8shJLdM6lCb9GsOll3oWH4OcJVQ6cAwgK6YKdFb1eH23Tw/ZooOeb6MxnUu7fLZV2vYLZeBGWieg== + integrity: sha512-VHngAbPFlPEIJu03Es93w1f3M4qeTzIsRF139TeWm+xZdLVxG1t0YngcVf8oYtaBgFXAulqMmMj404ExXzl3CA== tarball: 'file:projects/rush-stack-compiler-2.4-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.4.tgz': @@ -14912,7 +14917,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.4' resolution: - integrity: sha512-hG1r5yVDkBfx71HPn3Mev3yLwwOW95hi5Lr252mIkJnm+ahdIWalooVao0FD7iobjbBSDiLOHTytiJU4B32Igg== + integrity: sha512-iC5Irn0ZTtJtQQOjgHAlVk/09woLuwHgsqkeBsb2fJDqrR61Ju5J95wwiaU538JcJmyxh7ZJBDOCvbqLKmoG5w== tarball: 'file:projects/rush-stack-compiler-2.4.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.7-library-test.tgz': @@ -14922,7 +14927,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.7-library-test' resolution: - integrity: sha512-srXLgtPwX+/ohQ8xpWKCPF8QbMBuNpSyG35DhKK3KhrMOhcgjT391Hgk1x1WIeZQtV5PlPphoc3VNqO6ouZb6w== + integrity: sha512-sCfTPALT5gAgV7hWrT3N4qO/H2NXisKx5eIrjhdiAUK0SqB8ratcGH3V/TRpEqaRgTXWZq8UzSh+XcnHAaVIOw== tarball: 'file:projects/rush-stack-compiler-2.7-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.7.tgz': @@ -14939,7 +14944,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.7' resolution: - integrity: sha512-rMrR8wum1C9L+d3rTjiWk6XCec7lVJvYiaHhjIXtTd0ES4dLdosgCia3uq9ajD0+NToY4QAAwoq+Y+gsTaT+3A== + integrity: sha512-QW7Ei6IkWSDe9won4aCLSrGdWHvVCL81SKYFtefN1ccYAiZjPeEyTs5JIcuPNio9FBbj9ywxSEQFd0iQrluoLw== tarball: 'file:projects/rush-stack-compiler-2.7.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.8-library-test.tgz': @@ -14949,7 +14954,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.8-library-test' resolution: - integrity: sha512-AXRaABTMx4u38YP67laQmqB+9Q5nLVnxAzQgC3Tz2ukUtg7VOwW78aSbJS6tWi6r40koxf0kjza1xaJys/LbEg== + integrity: sha512-jSqVwOewBxVxWkldehxsLZh4RTjXeu3Y5HK8z4b/MP4MLVvCSiSJjRsWYHPOcHtC4GwbmiLrnFemkzRJYTccdg== tarball: 'file:projects/rush-stack-compiler-2.8-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.8.tgz': @@ -14966,7 +14971,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.8' resolution: - integrity: sha512-BgpuKWwcQsUPz/CnVlSa5I0RCHt14HtoVDsxSgts+OftigNAqwbYDS+bLIIaPH13CVWJj4hXkY+8SVUGtBtHpw== + integrity: sha512-ywquBd0lbKHaQWLZ76Oq83gBxlqegtp1xaOic59iLfi0HESaiJh3q8jrYYbWG/x2iJSxJ7ySimUfNq+b6iYs9w== tarball: 'file:projects/rush-stack-compiler-2.8.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.9-library-test.tgz': @@ -14976,7 +14981,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.9-library-test' resolution: - integrity: sha512-COU64dI/gxMf5Fde2Hpk+XCkcB64qzY47rIJ9qZWTp7b42rEPkqBZCCmOvdprCV1QRXezeY+E+SYSxirN/1f/w== + integrity: sha512-aX/auHxjJtfSBGnwbvEFRiiWva2WPbEFAAnSXlz0MwpJKSLx/elRezLxTizewi8tfmOWf9/zJkllI/OwEpxKaQ== tarball: 'file:projects/rush-stack-compiler-2.9-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-2.9.tgz': @@ -14993,7 +14998,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-2.9' resolution: - integrity: sha512-Tq6IFsZBQvwJfaRARpw7a51TXHUlPeHJT2MX69eXN1vXQQWle9pIoSpq4iQS5CWy7J8f+j2honpUy6DpuCEzlQ== + integrity: sha512-xOUdXc3fFgOlMROjPRfOWg0YVygMXsUpG3A5RoCiYimZLl6yOUhndR54UA+4EQYt9PpwgNOOORumQxgQaKRwOQ== tarball: 'file:projects/rush-stack-compiler-2.9.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.0-library-test.tgz': @@ -15003,7 +15008,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.0-library-test' resolution: - integrity: sha512-KbHGX9685/56jgLK+OXEphJa7PATxHNUrmNadMkKFTejTky68+PqpqZd/aV9HPC/XW+TuHwH5/vYq3Ih5tUXDQ== + integrity: sha512-dIdMFIfLwY9rhsAnYyaBvKeadBMiHf2gx3nWxJxruO9HSahdrMrQ9jDaEXej5MK/weD+1Pd76HURUC2k2W9tGg== tarball: 'file:projects/rush-stack-compiler-3.0-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.0.tgz': @@ -15020,7 +15025,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.0' resolution: - integrity: sha512-vMLB2XFdd0LGxgFcllFuQSvzzwXn6TqFdmb4xeki/2mSbYZku1AQaV3a8IWybiX59Xh9XpPanNhMsuu0iqiNog== + integrity: sha512-n3cUf0o1gKbMQvjVEc+4AQpll7brDVQ5BHqMckV5jiHIGodhVTMttQOKl6wq80OsWdqSNvHLu7r9JcxmHohb6Q== tarball: 'file:projects/rush-stack-compiler-3.0.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.1-library-test.tgz': @@ -15030,7 +15035,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.1-library-test' resolution: - integrity: sha512-FdL2pswO0WrPpMYll+E3Wg2TA97tOm7wFPOklI+Z+Y4Zw8FuGO7eG0lRMC76skBwyeaM7K/ZRYcX8MFQWvNBJA== + integrity: sha512-9Y2vi2YbRp8jBgqtIU3NM7DngyS15+sBCSUVHiXHopxgzQsNcb+H/FnVottHzIjQdk4l4WU2Hm/N2TnA8SKAxA== tarball: 'file:projects/rush-stack-compiler-3.1-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.1.tgz': @@ -15047,7 +15052,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.1' resolution: - integrity: sha512-/ti1UbntA4cFzQr0ZgG9rqUIQXqNGydcjmhMXpn/p9hEjZaQdJ/R+xTCQcwt14vLiaXZqkwziad+cDM2GAQyKQ== + integrity: sha512-w5EuBcFgTLYP+lh1OjTZKqKaVaQI/ZDQmdkyVrkBNUadJkVcb8G7TotndrgEYfYyNsYE/KX066UujXaGJgN+Dw== tarball: 'file:projects/rush-stack-compiler-3.1.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.2-library-test.tgz': @@ -15057,7 +15062,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.2-library-test' resolution: - integrity: sha512-aniHkRDpldzVnHxvrXBtXjbAHxh3swMCIo9vEnB0hHw9FFFUdFWxKVowmAj4lj7os2cLgimq3URLfom1k2kWzQ== + integrity: sha512-nc2B1hO9W3VrYZ8R2/4X1DSQVzUTDhxuhm0bIsb2zmbkMIGK4FnPcDy0wRgorLx1DgC0kkR1jHFvh8ZLChGPkA== tarball: 'file:projects/rush-stack-compiler-3.2-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.2.tgz': @@ -15074,7 +15079,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.2' resolution: - integrity: sha512-cU/RSNyebjIUmJfbnWZQW96KoMAFKEFtyehPVguTA9vSalA+kTfFAQ9gkGwlmCQ3aTpM4f9z3VJgew/4QM2ifw== + integrity: sha512-MDIMKdD6HRm5K4CI325dlkHCveiBjR+BoUORFr4sqCN9s4mJhlRtdxfaBn1i/04AJFgcCgPMa7L7KgReEPSPNg== tarball: 'file:projects/rush-stack-compiler-3.2.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.3-library-test.tgz': @@ -15084,7 +15089,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.3-library-test' resolution: - integrity: sha512-efsGPnFV/RsutIBBRgyLx2Zq0C15rNtxN9lY6VJXhS5CA8wPA0H9MrIxDrMZo4t4Slxu2y61XMYbU6J/opelXw== + integrity: sha512-1HxiLT7wSfKyPfdLnO1asb4MJ4NSViZNAgVr29HU8cFIETVD+B+rBan/qKWJWcKBMDntgEYOwPAh9tKBGTrvBw== tarball: 'file:projects/rush-stack-compiler-3.3-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.3.tgz': @@ -15101,7 +15106,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.3' resolution: - integrity: sha512-b7NF93lucZ3408ZIizqdda01HqEGoFbaDrGSf89B6gN1i88WrrXoQ2K+17v+vc4dLY32+HDM0mgw6xy8rYCMVQ== + integrity: sha512-y3VhB0CpRezYiSiZRi+LOvjmaeX49I5akoxPZsAbMZZQEJR2OuYXAMuPWg+g8vHrAldEG9FXN1y3Kaj/G6ZuYg== tarball: 'file:projects/rush-stack-compiler-3.3.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.4-library-test.tgz': @@ -15111,7 +15116,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.4-library-test' resolution: - integrity: sha512-R4TRjNCH5A+ER7bucWPT4+NlyD6MFumjSrMak2KLYDg33KXbJN051fzieqJQoltfNHGkQDIgXCh/6qZR8OFISg== + integrity: sha512-cC+RIr/IyFTcV/ftDha8Y7jMDc2G1AuzeWlTune/f4ep/uc9+PeLfwUwODDQcpxUycIL9TzUwnDd3ZKMAlKtIw== tarball: 'file:projects/rush-stack-compiler-3.4-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.4.tgz': @@ -15128,7 +15133,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.4' resolution: - integrity: sha512-LdDJIHD+jg+T2JsfdquoatKkxL8hHnH6Tt/1IcOxku7ZIqTWpFeQ+g3Et5eGQ99ki33u2jN5zCLijR3NPGQYxA== + integrity: sha512-z/hX/vrdwJHwc2cbBnMSWyy84CMJelGrSDfnaAa5j4emwVtKntyOcJkU3krj//Z9UNknLYp2c8xStVLykud3Ig== tarball: 'file:projects/rush-stack-compiler-3.4.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.5-library-test.tgz': @@ -15138,7 +15143,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.5-library-test' resolution: - integrity: sha512-fyZKZsfcBG+y2FEzqfVyMgykQf6OceXQ9nGviE6WAwKBtz0Q0+PMPIYUuUmYDLXwSYaDSr5teXUk1rcva1vSag== + integrity: sha512-cfAbSdwPvWTgq6D9F/SGvhdyLwJcpVg+SIqM0l45gSFR7Rg44IzDIc5QiZ+uzDKGnDupMX0V5Kuu1O9eW02i7Q== tarball: 'file:projects/rush-stack-compiler-3.5-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.5.tgz': @@ -15155,7 +15160,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.5' resolution: - integrity: sha512-JTxecGPqctAodYS7PndNd7oncQ5bk6WTx9wxIjLnm00BG0mvPcksngOEQ81GT7c9Gi65S3OF6/5tZpN3Yf/ADA== + integrity: sha512-cbHr87qGapmZNrpcINuVVni6vDyiQn07yHz3euGfGg/3EWWV9INOG9C6nPd5K5vQXgPtz68kuzaJjXt/ybrfDQ== tarball: 'file:projects/rush-stack-compiler-3.5.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.6-library-test.tgz': @@ -15165,7 +15170,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.6-library-test' resolution: - integrity: sha512-8JdP1KV8bF0cjH6CORh+D+dRzW6ekO0TTFpEdcysnR/eUcDcNlDYhyifOqymkp/xfsCB8zRZh2uwi51N5v4P1g== + integrity: sha512-sOm2pJJJfcYJK4SeDofcqMZ9BFZATfxIhucyS7Xai9elIlI1zTUJyeSWZgVUS531MxcKoWrQnwHwBWpJr8DmqQ== tarball: 'file:projects/rush-stack-compiler-3.6-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.6.tgz': @@ -15182,7 +15187,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.6' resolution: - integrity: sha512-+2EQQ8/YMqzqc/bi2CiUwpQnKUpvGzTepNtVdtodO+zkUy5fLElf+5nzmet3dPEWCreR1pAc/ce16MHE6qVTnA== + integrity: sha512-PctVC5qNzfm21MNdRAYF+3eXWSb6cxNnPa2pGvKrUKG5F36uAg/w/lUvTiFG5xCZj5t19s0WfATZbXGCRwThHg== tarball: 'file:projects/rush-stack-compiler-3.6.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.7-library-test.tgz': @@ -15192,7 +15197,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.7-library-test' resolution: - integrity: sha512-s4Sh6RDfa/jml/8XxxYA2XpGSHZubZGnFtiy+nYuzUyKeqlZITaZ21lmK3/SMKBrvFhLRHsopk1YdWFPARRVhw== + integrity: sha512-RgA22oa8sMN5ZPP03C27Dp2bZ2zBD69OnaNsbQ7veDmuiT5io6tNDno2KDKD2krBUleIyP5uG18eITwjK+cjsA== tarball: 'file:projects/rush-stack-compiler-3.7-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.7.tgz': @@ -15209,7 +15214,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.7' resolution: - integrity: sha512-rLsIDtuo9SRh3D64477T1JQWJ+xgK4M9PU1MDaK46iAWNyAjohUcKSaaiAcSROfgthWtWRMo/bKOIJ+nDwFAlQ== + integrity: sha512-LDG2a36dFPVNQ/R8md/LClKBtmwSlal2Oj0CVQAbtUezuB5M7dI1eb99wxKASxY7qKltE6rM8qsyIFPjOpgD3w== tarball: 'file:projects/rush-stack-compiler-3.7.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.8-library-test.tgz': @@ -15219,7 +15224,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.8-library-test' resolution: - integrity: sha512-2SuQ8Q166CtI6gUn4EDk4noGJvM1z3Lf+S9T2Qdeq9U/2phIXbdvlvDU3Uas6HmCIfOazBifOz+Kg2K/r1uN5Q== + integrity: sha512-5UysTUeQWuRX6vXnUi8K90NtbpLKi2eWSJvUFymgD3H7bkE48BvJkL9kIAX25sw7TR+TlzPTPIWasnk44MTGeA== tarball: 'file:projects/rush-stack-compiler-3.8-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.8.tgz': @@ -15236,7 +15241,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.8' resolution: - integrity: sha512-H/wZFsTyYIq7c0crHhDXDoct6T5+l3NvyrzkZBinRNv11EZgYLsbpYb/504HhFGGMSgXc8iwltLNlc/JA8TDmQ== + integrity: sha512-9rYR/uTXYOxcexqdgE7I2H8/N+8tUdMGa7RFM1FzebhbCcbzUadwTfQjbmuWdrDoDC+mlERdDe/kibbnAMdakg== tarball: 'file:projects/rush-stack-compiler-3.8.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.9-library-test.tgz': @@ -15246,7 +15251,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.9-library-test' resolution: - integrity: sha512-e4zCSyw0fH/N1UmJiHkIcSvfzH8P01fIrf9MeEuSW+xG+BGXp73GHcNvJTsMcj9xNO/MIPOqZhW5YDLz5KNQug== + integrity: sha512-5+x3XdYejC2TzmIw25PSsUxkJvEUhaLf6fOInJZXVw9MymDrNJb89GAnj38j0RCNfmeGCAQZzpvoGAYT0IK13w== tarball: 'file:projects/rush-stack-compiler-3.9-library-test.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-3.9.tgz': @@ -15263,7 +15268,7 @@ packages: dev: false name: '@rush-temp/rush-stack-compiler-3.9' resolution: - integrity: sha512-xVJGjTvwoKsEaL4jZSCOazIX148/7CLF0IVh7kplkm2mGuHfoHRWMOXV5Y5I1DQRBw6d1CF/yOtRxgafd85EZw== + integrity: sha512-qlSS3pTeBugJsj9Zv2U+4eTAzEx5LdLD22Abo6Iir/C03452R+Nlp8BFr90i509y2M+tnhdhpncLQQMl4Hv4wg== tarball: 'file:projects/rush-stack-compiler-3.9.tgz' version: 0.0.0 'file:projects/rush-stack-compiler-shared.tgz': @@ -15279,17 +15284,17 @@ packages: '@types/jest': 25.2.1 '@types/mocha': 5.2.5 '@types/node': 10.17.13 - '@types/semver': 5.3.33 + '@types/semver': 7.3.1 '@types/sinon': 1.16.34 chai: 3.5.0 colors: 1.2.5 gulp: 4.0.2 - semver: 5.3.0 + semver: 7.3.2 sinon: 1.17.7 dev: false name: '@rush-temp/rush' resolution: - integrity: sha512-8p/3ugfWriL5cPcBzZDEUWDTKca1DMoNUWBaspRJRm2Qcp1AMjuX+sy+J03UnLUrSUnzSeTnLJs3nYzWd3mgPg== + integrity: sha512-KZrz7WcO/R9aDkBcbPuBnZwE4XEdPKtBMDZ59Jzgx+63taV7oriAnE/GIsOtyFRk5irD9CyBdVkvRmbi7Aye9Q== tarball: 'file:projects/rush.tgz' version: 0.0.0 'file:projects/rushell.tgz': @@ -15302,7 +15307,7 @@ packages: dev: false name: '@rush-temp/rushell' resolution: - integrity: sha512-sj5UrwwDh615gbhQbi8VgtSHhI89Ze+12DPBixnsa7sXZwYTQ+YVJ0dOBSYqITUyed/ZpPR70ADeW5hf90MCKg== + integrity: sha512-ARflLxFkROH79kLtn5NyqfMYGWltLLR636nqI7fwU29ek6UGhR7L9YUN7XlCQc0UtvBWo5QHkP94/1qSsR6g5Q== tarball: 'file:projects/rushell.tgz' version: 0.0.0 'file:projects/set-webpack-public-path-plugin.tgz': @@ -15322,7 +15327,7 @@ packages: dev: false name: '@rush-temp/set-webpack-public-path-plugin' resolution: - integrity: sha512-yOQKXWQq3wS4mYOeM5yhKJetDcASn5jaOpPfpI6OByacPGdSvT0bUBWM8pxuPGGp0cchdq2+yAYYsDWJ1rbJ1g== + integrity: sha512-bUzd+GoM1njjrqu/s9ipifyEIjNI3iyrGMM6sYG9KsZ/Ps/oj1Sjyz9C5b9vg2/LD8+08UjLkrilzTicR6sAZQ== tarball: 'file:projects/set-webpack-public-path-plugin.tgz' version: 0.0.0 'file:projects/stream-collator.tgz': @@ -15338,7 +15343,7 @@ packages: dev: false name: '@rush-temp/stream-collator' resolution: - integrity: sha512-Nm5Bfyl1i2EnfGG7ijwlP2TBmti/JSJmgS14pl0FtRMDWOP599DOh36N5jw/z0krrYfieaF2zYOJBRqiHl+pzw== + integrity: sha512-S1St/fmGk12uLVgXmLTzUDWYqilQz/N+sepOLHyqyNSQY+vTutwOjtu1anmcVmmBEMOpxslRvB5SGDagq4I45Q== tarball: 'file:projects/stream-collator.tgz' version: 0.0.0 'file:projects/ts-command-line-test.tgz': @@ -15349,7 +15354,7 @@ packages: dev: false name: '@rush-temp/ts-command-line-test' resolution: - integrity: sha512-AQxi+9T6peEEsJWdyCZVEzd6r4jQaQ5EVbQEoWAZ9NOIIVEpjUZLaiEO/d93ggEGQqKvHB479OgtbgJbUyXhFg== + integrity: sha512-aH/q8anIDQ/909Lx6fRAm9977X5/u3pTHoCkQ7iG03HijH2PV/5ryIlUQCQmRccEDIQco1EdVTrYDWXqhD4yEw== tarball: 'file:projects/ts-command-line-test.tgz' version: 0.0.0 'file:projects/ts-command-line.tgz': @@ -15378,7 +15383,7 @@ packages: dev: false name: '@rush-temp/typings-generator' resolution: - integrity: sha512-NAT3Xv0oy0P4+NDUceFMc9vlJ4O1C/JZkH5cEBmNS0iQQ3hAo+SO1SFkJ4ihTNMPBVd0DS5b7XGm5xgRODLxjA== + integrity: sha512-XDbicbj2x8mvxcjqTE/s1LWNky6u8FwiU6w8iPCreK8qHu9xc5lUGltErrxD3Jq7IY/OzqL23QfluKZs0qF2Kw== tarball: 'file:projects/typings-generator.tgz' version: 0.0.0 'file:projects/web-library-build-test.tgz': @@ -15390,7 +15395,7 @@ packages: dev: false name: '@rush-temp/web-library-build-test' resolution: - integrity: sha512-zK8JowWRhZ4Lbwzec4wvWmLqSWg4xY0xalIitKd1WHz3VoxW50BY/kJM/iWtcvjKtxCoYIdNbGNuDkOKkgzEIw== + integrity: sha512-xHQuFo8rVslpAgvxRrUeURmporLuqxx3sQLXuX1QGgxqdkKJJ2tX5nC9PoZQmN6SmdDcpR2QxEAe1Uq5t3YQfQ== tarball: 'file:projects/web-library-build-test.tgz' version: 0.0.0 'file:projects/web-library-build.tgz': @@ -15402,7 +15407,7 @@ packages: dev: false name: '@rush-temp/web-library-build' resolution: - integrity: sha512-2I0Hk+AmU4AtPc0R1PUNdsIewWhTvMuK0XDQBLYIg9XMWm5W41ValoiQV6mhxmPKNsg4MJvfhsktN6VltyUJgA== + integrity: sha512-ySCvbIGDLrN/g2HLip529IhUvGuCjjshx5VAb9HAH4ORXHRo/xgRUdG6L/frAYDwp4Zoa2Hhx4x4CvjjRBl95g== tarball: 'file:projects/web-library-build.tgz' version: 0.0.0 registry: '' @@ -15526,7 +15531,7 @@ specifiers: '@types/orchestrator': 0.0.30 '@types/read-package-tree': 5.1.0 '@types/resolve': 1.17.1 - '@types/semver': 5.3.33 + '@types/semver': ~7.3.1 '@types/serve-static': 1.13.1 '@types/sinon': 1.16.34 '@types/source-map': 0.5.0 @@ -15613,7 +15618,7 @@ specifiers: pseudolocale: ~1.1.0 read-package-tree: ~5.1.5 resolve: ~1.17.0 - semver: ~5.3.0 + semver: ~7.3.0 sinon: ~1.17.3 source-map: ~0.6.1 strict-uri-encode: ~2.0.0 @@ -15639,4 +15644,4 @@ specifiers: xmldoc: ~1.1.2 yargs: ~4.6.0 z-schema: ~3.18.3 -# shrinkwrap hash: bab8e24cea983bf1cf2b941875a9fe13cf462d64 +# shrinkwrap hash: 1156756a2cb99c2e5fa67b2956ad9e699f52234a diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index e910acfaf14..002a514eefa 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -480,6 +480,9 @@ export class LockFile { // @public export class MapExtensions { static mergeFromMap(targetMap: Map, sourceMap: ReadonlyMap): void; + static toObject(map: Map): { + [key: string]: TValue; + }; } // @public diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 464ae5f6b6f..56e4fd7853b 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -68,6 +68,7 @@ export class CommonVersionsConfiguration { readonly allowedAlternativeVersions: Map>; readonly filePath: string; getAllPreferredVersions(): Map; + getPreferredVersionsHash(): string; readonly implicitlyPreferredVersions: boolean | undefined; static loadFromFile(jsonFilename: string): CommonVersionsConfiguration; readonly preferredVersions: Map; @@ -176,6 +177,7 @@ export interface _IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { preventManualShrinkwrapChanges?: boolean; resolutionStrategy?: ResolutionStrategy; strictPeerDependencies?: boolean; + useWorkspaces?: boolean; } // @public @@ -289,11 +291,21 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly preventManualShrinkwrapChanges: boolean; readonly resolutionStrategy: ResolutionStrategy; readonly strictPeerDependencies: boolean; + readonly useWorkspaces: boolean; } // @public export type PnpmStoreOptions = 'local' | 'global'; +// @public +export class RepoStateFile { + readonly filePath: string; + static loadFromFile(jsonFilename: string, variant: string | undefined): RepoStateFile; + readonly pnpmShrinkwrapHash: string | undefined; + readonly preferredVersionsHash: string | undefined; + refreshState(rushConfiguration: RushConfiguration): boolean; + } + // @public export type ResolutionStrategy = 'fewer-dependencies' | 'fast'; @@ -331,6 +343,8 @@ export class RushConfiguration { getCommonVersionsFilePath(variant?: string | undefined): string; getPnpmfilePath(variant?: string | undefined): string; getProjectByName(projectName: string): RushConfigurationProject | undefined; + getRepoState(variant?: string | undefined): RepoStateFile; + getRepoStateFilePath(variant?: string | undefined): string; readonly gitAllowedEmailRegExps: string[]; readonly gitSampleEmail: string; readonly gitVersionBumpCommitMessage: string | undefined; diff --git a/core-build/gulp-core-build-sass/src/CSSModules.ts b/core-build/gulp-core-build-sass/src/CSSModules.ts index c64ed2db18e..4d5f8650c72 100644 --- a/core-build/gulp-core-build-sass/src/CSSModules.ts +++ b/core-build/gulp-core-build-sass/src/CSSModules.ts @@ -24,7 +24,9 @@ export interface ICSSModules { export default class CSSModules implements ICSSModules { private _classMap: IClassMap; private _rootPath: string; - private _customizedGenerateScopedName: ((name: string, fileName: string, css: string) => string) | undefined; + private _customizedGenerateScopedName: + | ((name: string, fileName: string, css: string) => string) + | undefined; /** * CSSModules includes the source file's path relative to the project root @@ -34,7 +36,10 @@ export default class CSSModules implements ICSSModules { * That is used in {@link ./SassTask#SassTask} * But will default the process' current working dir. */ - public constructor(rootPath?: string, generateScopedName?: (name: string, fileName: string, css: string) => string) { + public constructor( + rootPath?: string, + generateScopedName?: (name: string, fileName: string, css: string) => string + ) { this._classMap = {}; if (rootPath) { this._rootPath = rootPath; diff --git a/core-build/gulp-core-build-sass/src/SassTask.ts b/core-build/gulp-core-build-sass/src/SassTask.ts index a23582864f2..71e84ba4725 100644 --- a/core-build/gulp-core-build-sass/src/SassTask.ts +++ b/core-build/gulp-core-build-sass/src/SassTask.ts @@ -131,7 +131,10 @@ export class SassTask extends GulpTask { const isFileModuleCss: boolean = !!filePath.match(/\.module\.s(a|c)ss/); const processAsModuleCss: boolean = isFileModuleCss || !!this.taskConfig.useCSSModules; - const cssModules: ICSSModules = new CSSModules(this.buildConfig.rootPath, this.taskConfig.generateScopedName); + const cssModules: ICSSModules = new CSSModules( + this.buildConfig.rootPath, + this.taskConfig.generateScopedName + ); if (!processAsModuleCss && this.taskConfig.warnOnNonCSSModules) { const relativeFilePath: string = path.relative(this.buildConfig.rootPath, filePath); diff --git a/core-build/gulp-core-build/package.json b/core-build/gulp-core-build/package.json index 892b6a5fca4..bd1f4c7ea6d 100644 --- a/core-build/gulp-core-build/package.json +++ b/core-build/gulp-core-build/package.json @@ -20,7 +20,7 @@ "@types/node": "10.17.13", "@types/node-notifier": "0.0.28", "@types/orchestrator": "0.0.30", - "@types/semver": "5.3.33", + "@types/semver": "~7.3.1", "@types/through2": "2.0.32", "@types/vinyl": "2.0.3", "@types/yargs": "0.0.34", @@ -46,7 +46,7 @@ "object-assign": "~4.1.0", "orchestrator": "~0.3.8", "pretty-hrtime": "~1.0.2", - "semver": "~5.3.0", + "semver": "~7.3.0", "through2": "~2.0.1", "vinyl": "~2.2.0", "xml": "~1.0.1", diff --git a/libraries/node-core-library/package.json b/libraries/node-core-library/package.json index 7c32199829b..02868f46474 100644 --- a/libraries/node-core-library/package.json +++ b/libraries/node-core-library/package.json @@ -16,7 +16,7 @@ "colors": "~1.2.1", "fs-extra": "~7.0.1", "jju": "~1.4.0", - "semver": "~5.3.0", + "semver": "~7.3.0", "timsort": "~0.3.0", "z-schema": "~3.18.3" }, @@ -27,7 +27,7 @@ "@types/fs-extra": "7.0.0", "@types/jest": "25.2.1", "@types/jju": "1.4.1", - "@types/semver": "5.3.33", + "@types/semver": "~7.3.1", "@types/timsort": "0.3.0", "@types/z-schema": "3.16.31", "gulp": "~4.0.2" diff --git a/libraries/node-core-library/src/MapExtensions.ts b/libraries/node-core-library/src/MapExtensions.ts index b84ba96a960..e31e3b70e7b 100644 --- a/libraries/node-core-library/src/MapExtensions.ts +++ b/libraries/node-core-library/src/MapExtensions.ts @@ -19,4 +19,19 @@ export class MapExtensions { targetMap.set(pair[0], pair[1]); } } + + /** + * Converts a string-keyed map to an object. + * @remarks + * This function has the same effect as Object.fromEntries(map.entries()) + * in supported versions of Node (\>= 12.0.0). + * @param map - The map that the object properties will be sourced from + */ + public static toObject(map: Map): { [key: string]: TValue } { + const object: { [key: string]: TValue } = {}; + for (const [key, value] of map.entries()) { + object[key] = value; + } + return object; + } }