From 00562799babee063367afcdffc83bbe89b4e68bf Mon Sep 17 00:00:00 2001 From: Pedro Date: Tue, 24 Sep 2024 17:18:39 +0800 Subject: [PATCH 1/2] feat: add installRemotely param --- common/reviews/api/rush-lib.api.md | 2 + .../src/api/RushConfigurationProject.ts | 28 +++++++--- .../rush-lib/src/logic/PackageJsonUpdater.ts | 36 ++++++------- .../installManager/WorkspaceInstallManager.ts | 51 ++++++++++--------- .../rush-lib/src/schemas/rush.schema.json | 4 ++ 5 files changed, 72 insertions(+), 49 deletions(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 454ac3daa07..a3101995acb 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1305,6 +1305,8 @@ export class RushConfigurationProject { // @deprecated get downstreamDependencyProjects(): string[]; // @beta + readonly installRemotely: boolean; + // @beta get isMainProject(): boolean; // @deprecated get localDependencyProjects(): ReadonlyArray; diff --git a/libraries/rush-lib/src/api/RushConfigurationProject.ts b/libraries/rush-lib/src/api/RushConfigurationProject.ts index 21e7062807a..3fbc248e506 100644 --- a/libraries/rush-lib/src/api/RushConfigurationProject.ts +++ b/libraries/rush-lib/src/api/RushConfigurationProject.ts @@ -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. @@ -29,6 +29,7 @@ export interface IRushConfigurationProjectJson { publishFolder?: string; tags?: string[]; subspaceName?: string; + installRemotely?: boolean; } /** @@ -56,6 +57,11 @@ export interface IRushConfigurationProjectOptions { * The containing subspace. */ subspace: Subspace; + + /** + * If specified, it will be downloaded from the external database or workspace. + */ + installRemotely?: boolean; } /** @@ -124,6 +130,15 @@ export class RushConfigurationProject { */ public readonly reviewCategory: string | undefined; + /** + * + * Indicates how this project should be installed by rush add. + * Default value is "false". + * + * @beta + */ + public readonly installRemotely: boolean = false; + /** * 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 @@ -209,10 +224,11 @@ export class RushConfigurationProject { /** @internal */ public constructor(options: IRushConfigurationProjectOptions) { const { projectJson, rushConfiguration, tempProjectName, allowedProjectTags } = options; - const { packageName, projectFolder: projectRelativeFolder } = projectJson; + const { packageName, projectFolder: projectRelativeFolder, installRemotely = false } = projectJson; this.rushConfiguration = rushConfiguration; this.packageName = packageName; this.projectRelativeFolder = projectRelativeFolder; + this.installRemotely = installRemotely; validateRelativePathField(projectRelativeFolder, 'projectFolder', rushConfiguration.rushJsonFile); diff --git a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts index 7f24813d06b..c74fd39929f 100644 --- a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts @@ -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. @@ -659,7 +659,7 @@ 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 (useWorkspaces && !localProject.installRemotely) { selectedVersion = initialSpec === version ? '*' : initialSpec; selectedVersionPrefix = workspacePrefix; } else { @@ -720,7 +720,7 @@ export class PackageJsonUpdater { 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 (useWorkspaces && !localProject.installRemotely) { selectedVersion = '*'; selectedVersionPrefix = workspacePrefix; } else { diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 8308aef00c8..583a4b0b719 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -1,47 +1,47 @@ // 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 * as semver from 'semver'; -import yaml from 'js-yaml'; import { - FileSystem, - FileConstants, AlreadyReportedError, Async, + FileConstants, + FileSystem, type IDependenciesMetaTable, Path, Sort } from '@rushstack/node-core-library'; import { createHash } from 'crypto'; +import yaml from 'js-yaml'; +import * as path from 'path'; +import * as semver from 'semver'; -import { BaseInstallManager } from '../base/BaseInstallManager'; -import type { IInstallManagerOptions } from '../base/BaseInstallManagerTypes'; -import type { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile'; -import { DependencySpecifier, DependencySpecifierType } from '../DependencySpecifier'; +import { Colorize, ConsoleTerminalProvider } from '@rushstack/terminal'; +import type { CommonVersionsConfiguration } from '../../api/CommonVersionsConfiguration'; +import { type CustomTipId, type ICustomTipInfo, PNPM_CUSTOM_TIPS } from '../../api/CustomTipsConfiguration'; +import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; +import { FlagFile } from '../../api/FlagFile'; import { - type PackageJsonEditor, DependencyType, - type PackageJsonDependencyMeta + type PackageJsonDependencyMeta, + type PackageJsonEditor } from '../../api/PackageJsonEditor'; -import { PnpmWorkspaceFile } from '../pnpm/PnpmWorkspaceFile'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; -import { RushConstants } from '../RushConstants'; +import type { Subspace } from '../../api/Subspace'; +import { objectsAreDeepEqual } from '../../utilities/objectUtilities'; +import { Stopwatch } from '../../utilities/Stopwatch'; import { Utilities } from '../../utilities/Utilities'; -import { InstallHelpers } from './InstallHelpers'; -import type { CommonVersionsConfiguration } from '../../api/CommonVersionsConfiguration'; -import type { RepoStateFile } from '../RepoStateFile'; -import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; -import { ShrinkwrapFileFactory } from '../ShrinkwrapFileFactory'; +import { BaseInstallManager } from '../base/BaseInstallManager'; +import type { IInstallManagerOptions } from '../base/BaseInstallManagerTypes'; +import { BaseLinkManager, SymlinkKind } from '../base/BaseLinkManager'; import { BaseProjectShrinkwrapFile } from '../base/BaseProjectShrinkwrapFile'; -import { type CustomTipId, type ICustomTipInfo, PNPM_CUSTOM_TIPS } from '../../api/CustomTipsConfiguration'; +import type { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile'; +import { DependencySpecifier, DependencySpecifierType } from '../DependencySpecifier'; import type { PnpmShrinkwrapFile } from '../pnpm/PnpmShrinkwrapFile'; -import { objectsAreDeepEqual } from '../../utilities/objectUtilities'; -import type { Subspace } from '../../api/Subspace'; -import { Colorize, ConsoleTerminalProvider } from '@rushstack/terminal'; -import { BaseLinkManager, SymlinkKind } from '../base/BaseLinkManager'; -import { FlagFile } from '../../api/FlagFile'; -import { Stopwatch } from '../../utilities/Stopwatch'; +import { PnpmWorkspaceFile } from '../pnpm/PnpmWorkspaceFile'; +import type { RepoStateFile } from '../RepoStateFile'; +import { RushConstants } from '../RushConstants'; +import { ShrinkwrapFileFactory } from '../ShrinkwrapFileFactory'; +import { InstallHelpers } from './InstallHelpers'; export interface IPnpmModules { hoistedDependencies: { [dep in string]: { [depPath in string]: string } }; @@ -232,6 +232,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { (dependencySpecifier.specifierType === DependencySpecifierType.Version || dependencySpecifier.specifierType === DependencySpecifierType.Range) && referencedLocalProject && + !referencedLocalProject.installRemotely && !rushProject.decoupledLocalDependencies.has(name) ) { // Make sure that this version is intended to target a local package. If not, then we will fail since it diff --git a/libraries/rush-lib/src/schemas/rush.schema.json b/libraries/rush-lib/src/schemas/rush.schema.json index dce5fcaae37..89e4978f742 100644 --- a/libraries/rush-lib/src/schemas/rush.schema.json +++ b/libraries/rush-lib/src/schemas/rush.schema.json @@ -304,6 +304,10 @@ "description": "Facilitates postprocessing of a project's files prior to publishing. If specified, the \"publishFolder\" is the relative path to a subfolder of the project folder. The \"rush publish\" command will publish the subfolder instead of the project folder. The subfolder must contain its own package.json file, which is typically a build output.", "type": "string" }, + "installRemotely": { + "description": "Specifies the installation type: \"remote\" or \"workspace\". The default value is false.", + "type": "boolean" + }, "tags": { "description": "An optional set of custom tags that can be used to select this project. For example, adding \"my-custom-tag\" will allow this project to be selected by the command \"rush list --only tag:my-custom-tag\". The tag name must be one or more words separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits, \".\", and \"@\" characters.", "type": "array", From 77f5c200b6bf40afd4c8e3058e068e71b3f0f6a0 Mon Sep 17 00:00:00 2001 From: Pedro Date: Wed, 16 Oct 2024 17:42:06 -0700 Subject: [PATCH 2/2] feat: add installRemotely and versionRange new parameters --- common/reviews/api/rush-lib.api.md | 2 + .../src/api/RushConfigurationProject.ts | 28 ++- .../rush-lib/src/logic/PackageJsonUpdater.ts | 163 +++++++++++------- .../rush-lib/src/schemas/rush.schema.json | 6 +- 4 files changed, 132 insertions(+), 67 deletions(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index a3101995acb..87cc64385f3 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1332,6 +1332,8 @@ export class RushConfigurationProject { get versionPolicy(): VersionPolicy | undefined; // @beta readonly versionPolicyName: string | undefined; + // @beta + readonly versionRange: string | undefined; } // @beta diff --git a/libraries/rush-lib/src/api/RushConfigurationProject.ts b/libraries/rush-lib/src/api/RushConfigurationProject.ts index 3fbc248e506..71449175531 100644 --- a/libraries/rush-lib/src/api/RushConfigurationProject.ts +++ b/libraries/rush-lib/src/api/RushConfigurationProject.ts @@ -30,6 +30,7 @@ export interface IRushConfigurationProjectJson { tags?: string[]; subspaceName?: string; installRemotely?: boolean; + versionRange?: string; } /** @@ -59,9 +60,14 @@ export interface IRushConfigurationProjectOptions { subspace: Subspace; /** - * If specified, it will be downloaded from the external database or workspace. + * 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; } /** @@ -133,11 +139,19 @@ export class RushConfigurationProject { /** * * Indicates how this project should be installed by rush add. - * Default value is "false". + * 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 installRemotely: boolean = false; + public readonly versionRange: string | undefined; /** * A list of local projects that appear as devDependencies for this project, but cannot be @@ -224,11 +238,17 @@ export class RushConfigurationProject { /** @internal */ public constructor(options: IRushConfigurationProjectOptions) { const { projectJson, rushConfiguration, tempProjectName, allowedProjectTags } = options; - const { packageName, projectFolder: projectRelativeFolder, installRemotely = false } = 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); diff --git a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts index c74fd39929f..1c9e1d312a1 100644 --- a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts @@ -551,6 +551,86 @@ export class PackageJsonUpdater { } } + private async _getRemoteLatestVersionAsync(packageName: string): Promise { + 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 { + 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 { + 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. @@ -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 && !localProject.installRemotely) { + if (localProject.installRemotely) { + selectedVersion = await this._getRemoteVersionAsync(packageName, initialSpec); + } else if (useWorkspaces) { selectedVersion = initialSpec === version ? '*' : initialSpec; selectedVersionPrefix = workspacePrefix; } else { @@ -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 && !localProject.installRemotely) { + 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 { @@ -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(); diff --git a/libraries/rush-lib/src/schemas/rush.schema.json b/libraries/rush-lib/src/schemas/rush.schema.json index 89e4978f742..8bd8cdd69f9 100644 --- a/libraries/rush-lib/src/schemas/rush.schema.json +++ b/libraries/rush-lib/src/schemas/rush.schema.json @@ -305,9 +305,13 @@ "type": "string" }, "installRemotely": { - "description": "Specifies the installation type: \"remote\" or \"workspace\". The default value is false.", + "description": "If true, then this project will be installed by NPM registry. The default value is true.", "type": "boolean" }, + "versionRange": { + "description": "Specifies the package version, using version range syntax.", + "type": "string" + }, "tags": { "description": "An optional set of custom tags that can be used to select this project. For example, adding \"my-custom-tag\" will allow this project to be selected by the command \"rush list --only tag:my-custom-tag\". The tag name must be one or more words separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits, \".\", and \"@\" characters.", "type": "array",