Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rush] Customize default installation #4976

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,8 @@ export class RushConfigurationProject {
// @deprecated
get downstreamDependencyProjects(): string[];
// @beta
readonly installRemotely: boolean;
// @beta
get isMainProject(): boolean;
// @deprecated
get localDependencyProjects(): ReadonlyArray<RushConfigurationProject>;
Expand All @@ -1330,6 +1332,8 @@ export class RushConfigurationProject {
get versionPolicy(): VersionPolicy | undefined;
// @beta
readonly versionPolicyName: string | undefined;
// @beta
readonly versionRange: string | undefined;
}

// @beta
Expand Down
48 changes: 42 additions & 6 deletions libraries/rush-lib/src/api/RushConfigurationProject.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { type IPackageJson, FileConstants, FileSystem } from '@rushstack/node-core-library';
import * as path from 'path';
import * as semver from 'semver';
import { type IPackageJson, FileSystem, FileConstants } from '@rushstack/node-core-library';

import type { RushConfiguration } from './RushConfiguration';
import type { VersionPolicy, LockStepVersionPolicy } from './VersionPolicy';
import type { PackageJsonEditor } from './PackageJsonEditor';
import { DependencySpecifier, DependencySpecifierType } from '../logic/DependencySpecifier';
import { RushConstants } from '../logic/RushConstants';
import type { PackageJsonEditor } from './PackageJsonEditor';
import { PackageNameParsers } from './PackageNameParsers';
import { DependencySpecifier, DependencySpecifierType } from '../logic/DependencySpecifier';
import type { RushConfiguration } from './RushConfiguration';
import { SaveCallbackPackageJsonEditor } from './SaveCallbackPackageJsonEditor';
import type { Subspace } from './Subspace';
import type { LockStepVersionPolicy, VersionPolicy } from './VersionPolicy';

/**
* This represents the JSON data object for a project entry in the rush.json configuration file.
Expand All @@ -29,6 +29,8 @@ export interface IRushConfigurationProjectJson {
publishFolder?: string;
tags?: string[];
subspaceName?: string;
installRemotely?: boolean;
versionRange?: string;
}

/**
Expand Down Expand Up @@ -56,6 +58,16 @@ export interface IRushConfigurationProjectOptions {
* The containing subspace.
*/
subspace: Subspace;

/**
* If specified, package will be downloaded by NPM registry.
*/
installRemotely?: boolean;

/**
* If specified, it will be downloaded according to the NPM version range (ignored if installRemotely=false).
*/
versionRange?: boolean;
}

/**
Expand Down Expand Up @@ -124,6 +136,23 @@ export class RushConfigurationProject {
*/
public readonly reviewCategory: string | undefined;

/**
*
* Indicates how this project should be installed by rush add.
* Default value is "true".
*
* @beta
*/
public readonly installRemotely: boolean = true;

/**
*
* Indicates which version should be installed by rush add. Ignored if installRemotely=false.
*
* @beta
*/
public readonly versionRange: string | undefined;

/**
* A list of local projects that appear as devDependencies for this project, but cannot be
* locally linked because it would create a cyclic dependency; instead, the last published
Expand Down Expand Up @@ -209,10 +238,17 @@ export class RushConfigurationProject {
/** @internal */
public constructor(options: IRushConfigurationProjectOptions) {
const { projectJson, rushConfiguration, tempProjectName, allowedProjectTags } = options;
const { packageName, projectFolder: projectRelativeFolder } = projectJson;
const {
packageName,
projectFolder: projectRelativeFolder,
installRemotely = true,
versionRange
} = projectJson;
this.rushConfiguration = rushConfiguration;
this.packageName = packageName;
this.projectRelativeFolder = projectRelativeFolder;
this.installRemotely = installRemotely;
this.versionRange = versionRange;

validateRelativePathField(projectRelativeFolder, 'projectFolder', rushConfiguration.rushJsonFile);

Expand Down
195 changes: 117 additions & 78 deletions libraries/rush-lib/src/logic/PackageJsonUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as semver from 'semver';
import { Colorize, ConsoleTerminalProvider, Terminal, type ITerminalProvider } from '@rushstack/terminal';
import type * as NpmCheck from 'npm-check';
import { ConsoleTerminalProvider, Terminal, type ITerminalProvider, Colorize } from '@rushstack/terminal';
import * as semver from 'semver';

import { DependencyType, type PackageJsonDependency } from '../api/PackageJsonEditor';
import type { RushConfiguration } from '../api/RushConfiguration';
import type { RushConfigurationProject } from '../api/RushConfigurationProject';
import type { RushGlobalFolder } from '../api/RushGlobalFolder';
import type { Subspace } from '../api/Subspace';
import { Utilities } from '../utilities/Utilities';
import type { BaseInstallManager } from './base/BaseInstallManager';
import type { IInstallManagerOptions } from './base/BaseInstallManagerTypes';
import { InstallManagerFactory } from './InstallManagerFactory';
import { VersionMismatchFinder } from './versionMismatch/VersionMismatchFinder';
import { PurgeManager } from './PurgeManager';
import { Utilities } from '../utilities/Utilities';
import { DependencyType, type PackageJsonDependency } from '../api/PackageJsonEditor';
import type { RushGlobalFolder } from '../api/RushGlobalFolder';
import type { RushConfigurationProject } from '../api/RushConfigurationProject';
import type { VersionMismatchFinderEntity } from './versionMismatch/VersionMismatchFinderEntity';
import { VersionMismatchFinderProject } from './versionMismatch/VersionMismatchFinderProject';
import { RushConstants } from './RushConstants';
import { InstallHelpers } from './installManager/InstallHelpers';
import type { DependencyAnalyzer, IDependencyAnalysis } from './DependencyAnalyzer';
import { InstallHelpers } from './installManager/InstallHelpers';
import { InstallManagerFactory } from './InstallManagerFactory';
import {
SemVerStyle,
type IPackageForRushAdd,
type IPackageJsonUpdaterRushAddOptions,
type IPackageJsonUpdaterRushBaseUpdateOptions,
type IPackageJsonUpdaterRushRemoveOptions,
SemVerStyle
type IPackageJsonUpdaterRushRemoveOptions
} from './PackageJsonUpdaterTypes';
import type { Subspace } from '../api/Subspace';
import { PurgeManager } from './PurgeManager';
import { RushConstants } from './RushConstants';
import { VersionMismatchFinder } from './versionMismatch/VersionMismatchFinder';
import type { VersionMismatchFinderEntity } from './versionMismatch/VersionMismatchFinderEntity';
import { VersionMismatchFinderProject } from './versionMismatch/VersionMismatchFinderProject';

/**
* Options for adding a dependency to a particular project.
Expand Down Expand Up @@ -551,6 +551,86 @@ export class PackageJsonUpdater {
}
}

private async _getRemoteLatestVersionAsync(packageName: string): Promise<string> {
let selectedVersion: string | undefined;
this._terminal.writeLine(`Querying NPM registry for latest version of "${packageName}"...`);

let commandArgs: string[];
if (this._rushConfiguration.packageManager === 'yarn') {
commandArgs = ['info', packageName, 'dist-tags.latest', '--silent'];
} else {
commandArgs = ['view', `${packageName}@latest`, 'version'];
}

selectedVersion = (
await Utilities.executeCommandAndCaptureOutputAsync(
this._rushConfiguration.packageManagerToolFilename,
commandArgs,
this._rushConfiguration.commonTempFolder
)
).trim();

this._terminal.writeLine();
this._terminal.writeLine(`Found latest version: ${Colorize.cyan(selectedVersion)}`);

return selectedVersion;
}

private async _getRemoteSpecifiedVersionAsync(
packageName: string,
initialSpec: string = 'latest'
): Promise<string> {
let selectedVersion: string | undefined;
this._terminal.writeLine(`Querying registry for all versions of "${packageName}"...`);

let commandArgs: string[];
if (this._rushConfiguration.packageManager === 'yarn') {
commandArgs = ['info', packageName, 'versions', '--json'];
} else {
commandArgs = ['view', packageName, 'versions', '--json'];
}

const allVersions: string = await Utilities.executeCommandAndCaptureOutputAsync(
this._rushConfiguration.packageManagerToolFilename,
commandArgs,
this._rushConfiguration.commonTempFolder
);

let versionList: string[];
if (this._rushConfiguration.packageManager === 'yarn') {
versionList = JSON.parse(allVersions).data;
} else {
versionList = JSON.parse(allVersions);
}

this._terminal.writeLine(Colorize.gray(`Found ${versionList.length} available versions.`));

for (const version of versionList) {
if (semver.satisfies(version, initialSpec)) {
selectedVersion = initialSpec;
this._terminal.writeLine(`Found a version that satisfies ${initialSpec}: ${Colorize.cyan(version)}`);
break;
}
}

if (!selectedVersion) {
throw new Error(
`Unable to find a version of "${packageName}" that satisfies` +
` the version specifier "${initialSpec}"`
);
}

return selectedVersion;
}

private async _getRemoteVersionAsync(packageName: string, initialSpec: string = 'latest'): Promise<string> {
if (initialSpec === 'latest') {
return this._getRemoteLatestVersionAsync(packageName);
}

return this._getRemoteSpecifiedVersionAsync(packageName, initialSpec);
}

/**
* Selects an appropriate version number for a particular package, given an optional initial SemVer spec.
* If ensureConsistentVersions, tries to pick a version that will be consistent.
Expand Down Expand Up @@ -659,7 +739,9 @@ export class PackageJsonUpdater {
if (semver.satisfies(version, initialSpec)) {
// 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) {
if (localProject.installRemotely) {
selectedVersion = await this._getRemoteVersionAsync(packageName, initialSpec);
} else if (useWorkspaces) {
selectedVersion = initialSpec === version ? '*' : initialSpec;
selectedVersionPrefix = workspacePrefix;
} else {
Expand All @@ -675,52 +757,26 @@ export class PackageJsonUpdater {
);
}
} else {
this._terminal.writeLine(`Querying registry for all versions of "${packageName}"...`);

let commandArgs: string[];
if (this._rushConfiguration.packageManager === 'yarn') {
commandArgs = ['info', packageName, 'versions', '--json'];
} else {
commandArgs = ['view', packageName, 'versions', '--json'];
}

const allVersions: string = await Utilities.executeCommandAndCaptureOutputAsync(
this._rushConfiguration.packageManagerToolFilename,
commandArgs,
this._rushConfiguration.commonTempFolder
);

let versionList: string[];
if (this._rushConfiguration.packageManager === 'yarn') {
versionList = JSON.parse(allVersions).data;
} else {
versionList = JSON.parse(allVersions);
}

this._terminal.writeLine(Colorize.gray(`Found ${versionList.length} available versions.`));

for (const version of versionList) {
if (semver.satisfies(version, initialSpec)) {
selectedVersion = initialSpec;
this._terminal.writeLine(
`Found a version that satisfies ${initialSpec}: ${Colorize.cyan(version)}`
);
break;
}
}

if (!selectedVersion) {
throw new Error(
`Unable to find a version of "${packageName}" that satisfies` +
` the version specifier "${initialSpec}"`
);
}
// if the package is not a project in the local repository, then we need to query the registry
// to find the latest version that satisfies the spec
selectedVersion = await this._getRemoteVersionAsync(packageName, initialSpec);
}
} else {
if (localProject !== undefined) {
// 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) {
if (localProject.installRemotely) {
selectedVersion = await this._getRemoteVersionAsync(packageName, localProject.versionRange);
this._terminal.writeLine(
Colorize.green('Assigning "') +
Colorize.cyan(selectedVersion) +
Colorize.green(
`" for "${packageName}" because it is the preferred version defined by ${RushConstants.rushJsonFilename}.`
)
);

return selectedVersion;
} else if (useWorkspaces) {
selectedVersion = '*';
selectedVersionPrefix = workspacePrefix;
} else {
Expand All @@ -736,27 +792,10 @@ export class PackageJsonUpdater {
this._terminal.writeLine();
}

this._terminal.writeLine(`Querying NPM registry for latest version of "${packageName}"...`);

let commandArgs: string[];
if (this._rushConfiguration.packageManager === 'yarn') {
commandArgs = ['info', packageName, 'dist-tags.latest', '--silent'];
} else {
commandArgs = ['view', `${packageName}@latest`, 'version'];
}

selectedVersion = (
await Utilities.executeCommandAndCaptureOutputAsync(
this._rushConfiguration.packageManagerToolFilename,
commandArgs,
this._rushConfiguration.commonTempFolder
)
).trim();
// if the package is not a project in the local repository with no spec defined, then we need to
// query the registry to find the latest version
selectedVersion = await this._getRemoteVersionAsync(packageName);
}

this._terminal.writeLine();

this._terminal.writeLine(`Found latest version: ${Colorize.cyan(selectedVersion)}`);
}

this._terminal.writeLine();
Expand Down
Loading
Loading