From f5e78d352748a8c6449bf51103d4b65b3bcebf6f Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sun, 15 Mar 2026 00:26:42 -0400 Subject: [PATCH 01/11] Add functions to better support commands provided by global commands. --- ...shed-versions-plugin_2026-03-15-04-22.json | 10 ++++ common/reviews/api/rush-lib.api.md | 4 +- .../cli/scriptActions/GlobalScriptAction.ts | 49 +++++++++++++++++++ .../src/pluginFramework/RushLifeCycle.ts | 16 +++++- 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json diff --git a/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json b/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json new file mode 100644 index 00000000000..ba6f7227095 --- /dev/null +++ b/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add `customParametersByLongName` and `setHandled()` to `IGlobalCommand`, enabling Rush plugins to implement native global commands. Plugins can now define global commands with an empty `shellCommand`, handle execution via the `runGlobalCustomCommand` hook, and access parsed command-line parameters.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 14063d1543e..ca0f0341078 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -514,6 +514,8 @@ export interface IGetChangedProjectsOptions { // @beta export interface IGlobalCommand extends IRushCommand { + getCustomParametersByLongName(longName: string): TParameter; + setHandled(): void; } // @public @@ -1502,7 +1504,7 @@ export class RushLifecycleHooks { variant: string | undefined ]>; readonly beforeInstall: AsyncSeriesHook<[ - command: IGlobalCommand, + command: IRushCommand, subspace: Subspace, variant: string | undefined ]>; diff --git a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts index 9b35156f188..920ff0d4ba9 100644 --- a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts @@ -5,6 +5,7 @@ import * as path from 'node:path'; import type { AsyncSeriesHook } from 'tapable'; +import type { CommandLineParameter } from '@rushstack/ts-command-line'; import { FileSystem, type IPackageJson, @@ -45,6 +46,9 @@ export class GlobalScriptAction extends BaseScriptAction { private readonly _autoinstallerName: string; private readonly _autoinstallerFullPath: string; + private _customParametersByLongName: ReadonlyMap | undefined; + private _isHandled: boolean = false; + public constructor(options: IGlobalScriptActionOptions) { super(options); this._shellCommand = options.shellCommand; @@ -93,6 +97,37 @@ export class GlobalScriptAction extends BaseScriptAction { this.defineScriptParameters(); } + /** + * {@inheritDoc IGlobalCommand.setHandled} + */ + public setHandled(): void { + this._isHandled = true; + } + + /** + * {@inheritDoc IGlobalCommand.getCustomParametersByLongName} + */ + public getCustomParametersByLongName( + longName: string + ): TParameter { + if (!this._customParametersByLongName) { + const map: Map = new Map(); + for (const [parameterJson, parameter] of this.customParameters) { + map.set(parameterJson.longName, parameter); + } + this._customParametersByLongName = map; + } + + const parameter: CommandLineParameter | undefined = this._customParametersByLongName.get(longName); + if (!parameter) { + throw new Error( + `The command "${this.actionName}" does not have a custom parameter with long name "${longName}".` + ); + } + + return parameter as TParameter; + } + private async _prepareAutoinstallerNameAsync(): Promise { const autoInstaller: Autoinstaller = new Autoinstaller({ autoinstallerName: this._autoinstallerName, @@ -117,6 +152,20 @@ export class GlobalScriptAction extends BaseScriptAction { await hookForAction.promise(this); } + // If a plugin hook called setHandled(), the command has been fully handled. + // Skip the default shell command execution. + if (this._isHandled) { + return; + } + + if (this._shellCommand === '') { + throw new Error( + `The custom command "${this.actionName}" has an empty "shellCommand" value, but no plugin ` + + 'called setHandled() for this command. An empty "shellCommand" is intended for global ' + + 'commands whose implementation is provided entirely by a Rush plugin.' + ); + } + const additionalPathFolders: string[] = this.commandLineConfiguration?.additionalPathFolders.slice() || []; diff --git a/libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts b/libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts index 76e51d8e17d..d5f68d63ffa 100644 --- a/libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts +++ b/libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts @@ -3,6 +3,8 @@ import { AsyncParallelHook, AsyncSeriesHook, HookMap } from 'tapable'; +import type { CommandLineParameter } from '@rushstack/ts-command-line'; + import type { ITelemetryData } from '../logic/Telemetry'; import type { PhasedCommandHooks } from './PhasedCommandHooks'; import type { Subspace } from '../api/Subspace'; @@ -23,7 +25,17 @@ export interface IRushCommand { * @beta */ export interface IGlobalCommand extends IRushCommand { - // Nothing added. + /** + * Get a parameter by its long name (e.g. "--output-path") that was defined in command-line.json for this command. + * If the parameter was not defined or not provided on the command line, this will throw. + */ + getCustomParametersByLongName(longName: string): TParameter; + + /** + * Call this from a plugin hook to indicate that the command has been fully handled + * by the plugin. When set, the default shell command execution will be skipped. + */ + setHandled(): void; } /** @@ -94,7 +106,7 @@ export class RushLifecycleHooks { * The hook to run between preparing the common/temp folder and invoking the package manager during "rush install" or "rush update". */ public readonly beforeInstall: AsyncSeriesHook< - [command: IGlobalCommand, subspace: Subspace, variant: string | undefined] + [command: IRushCommand, subspace: Subspace, variant: string | undefined] > = new AsyncSeriesHook(['command', 'subspace', 'variant'], 'beforeInstall'); /** From 490bda5f8e11d698412fa5349101d83cd25a8e90 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sun, 15 Mar 2026 00:27:49 -0400 Subject: [PATCH 02/11] Introduce rush-published-versions-json-plugin. --- ...shed-versions-plugin_2026-03-15-04-27.json | 10 +++ .../config/subspaces/default/pnpm-lock.yaml | 25 +++++++ .../.npmignore | 36 ++++++++++ .../LICENSE | 24 +++++++ .../README.md | 71 +++++++++++++++++++ .../command-line.json | 24 +++++++ .../config/rig.json | 7 ++ .../eslint.config.js | 20 ++++++ .../package.json | 49 +++++++++++++ .../rush-plugin-manifest.json | 12 ++++ .../src/PublishedVersionsJsonPlugin.ts | 55 ++++++++++++++ .../src/index.ts | 5 ++ .../tsconfig.json | 3 + rush.json | 6 ++ 14 files changed, 347 insertions(+) create mode 100644 common/changes/@rushstack/rush-published-versions-json-plugin/rush-published-versions-plugin_2026-03-15-04-27.json create mode 100644 rush-plugins/rush-published-versions-json-plugin/.npmignore create mode 100644 rush-plugins/rush-published-versions-json-plugin/LICENSE create mode 100644 rush-plugins/rush-published-versions-json-plugin/README.md create mode 100644 rush-plugins/rush-published-versions-json-plugin/command-line.json create mode 100644 rush-plugins/rush-published-versions-json-plugin/config/rig.json create mode 100644 rush-plugins/rush-published-versions-json-plugin/eslint.config.js create mode 100644 rush-plugins/rush-published-versions-json-plugin/package.json create mode 100644 rush-plugins/rush-published-versions-json-plugin/rush-plugin-manifest.json create mode 100644 rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts create mode 100644 rush-plugins/rush-published-versions-json-plugin/src/index.ts create mode 100644 rush-plugins/rush-published-versions-json-plugin/tsconfig.json diff --git a/common/changes/@rushstack/rush-published-versions-json-plugin/rush-published-versions-plugin_2026-03-15-04-27.json b/common/changes/@rushstack/rush-published-versions-json-plugin/rush-published-versions-plugin_2026-03-15-04-27.json new file mode 100644 index 00000000000..9a39d326bf4 --- /dev/null +++ b/common/changes/@rushstack/rush-published-versions-json-plugin/rush-published-versions-plugin_2026-03-15-04-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/rush-published-versions-json-plugin", + "comment": "Initial release.", + "type": "minor" + } + ], + "packageName": "@rushstack/rush-published-versions-json-plugin" +} \ No newline at end of file diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 9c00b0b7596..a58e687a549 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -5090,6 +5090,31 @@ importers: specifier: workspace:* version: link:../../rigs/local-node-rig + ../../../rush-plugins/rush-published-versions-json-plugin: + dependencies: + '@rushstack/node-core-library': + specifier: workspace:* + version: link:../../libraries/node-core-library + '@rushstack/rush-sdk': + specifier: workspace:* + version: link:../../libraries/rush-sdk + '@rushstack/terminal': + specifier: workspace:* + version: link:../../libraries/terminal + '@rushstack/ts-command-line': + specifier: workspace:* + version: link:../../libraries/ts-command-line + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + eslint: + specifier: ~9.37.0 + version: 9.37.0 + local-node-rig: + specifier: workspace:* + version: link:../../rigs/local-node-rig + ../../../rush-plugins/rush-redis-cobuild-plugin: dependencies: '@redis/client': diff --git a/rush-plugins/rush-published-versions-json-plugin/.npmignore b/rush-plugins/rush-published-versions-json-plugin/.npmignore new file mode 100644 index 00000000000..f7a40e10213 --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/.npmignore @@ -0,0 +1,36 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** +!/includes/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js +*.test.[cm]js +*.test.d.ts +*.test.d.[cm]ts + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README.md +# LICENSE + +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- diff --git a/rush-plugins/rush-published-versions-json-plugin/LICENSE b/rush-plugins/rush-published-versions-json-plugin/LICENSE new file mode 100644 index 00000000000..da9e82e6c70 --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/LICENSE @@ -0,0 +1,24 @@ +@rushstack/rush-published-versions-json-plugin + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/rush-plugins/rush-published-versions-json-plugin/README.md b/rush-plugins/rush-published-versions-json-plugin/README.md new file mode 100644 index 00000000000..bf975210cf7 --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/README.md @@ -0,0 +1,71 @@ +# @rushstack/rush-published-versions-json-plugin + +A Rush plugin that generates a JSON file recording the version numbers of all published packages in a Rush monorepo. + +## Installation + +1. Add the plugin package to an autoinstaller (e.g. `common/autoinstallers/rush-plugins/package.json`): + + ``` + rush init-autoinstaller --name rush-plugins + ``` + + ```bash + cd common/autoinstallers/rush-plugins + pnpm add @rushstack/rush-published-versions-json-plugin + rush update-autoinstaller --name rush-plugins + ``` + +2. Register the plugin in `common/config/rush/rush-plugins.json`: + + ```json + { + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json", + "plugins": [ + { + "packageName": "@rushstack/rush-published-versions-json-plugin", + "pluginName": "rush-published-versions-json-plugin", + "autoinstallerName": "rush-plugins" + } + ] + } + ``` + +3. Run `rush update` to install the plugin. + +## Usage + +```bash +rush record-published-versions --output-path +``` + +### Parameters + +| Parameter | Short | Required | Description | +| --- | --- | --- | --- | +| `--output-path` | `-o` | Yes | The path to the output JSON file. Relative paths are resolved from the repo root. | + +### Example + +```bash +rush record-published-versions --output-path common/config/published-versions.json +``` + +### Output format + +The output is a JSON object mapping published package names to their current versions: + +```json +{ + "@my-scope/my-library": "1.2.3", + "@my-scope/my-app": "0.5.0" +} +``` + +A package is included if it has `shouldPublish` set to `true` or has a `versionPolicy` assigned in +**rush.json**. + +## Links + +- [CHANGELOG.md](./CHANGELOG.md) +- [Rush: Using rush plugins](https://rushjs.io/pages/maintainer/using_rush_plugins/) diff --git a/rush-plugins/rush-published-versions-json-plugin/command-line.json b/rush-plugins/rush-published-versions-json-plugin/command-line.json new file mode 100644 index 00000000000..212a0ad8b6b --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/command-line.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", + "commands": [ + { + "commandKind": "global", + "name": "record-published-versions", + "summary": "Generates a JSON file recording the version numbers of all published packages.", + "safeForSimultaneousRushProcesses": true, + // The command is handled by the plugin, so we don't want Rush to attempt to execute anything. + "shellCommand": "" + } + ], + "parameters": [ + { + "parameterKind": "string", + "longName": "--output-path", + "shortName": "-o", + "argumentName": "FILE_PATH", + "description": "The path to the output JSON file. Relative paths are resolved from the repo root.", + "associatedCommands": ["record-published-versions"], + "required": true + } + ] +} diff --git a/rush-plugins/rush-published-versions-json-plugin/config/rig.json b/rush-plugins/rush-published-versions-json-plugin/config/rig.json new file mode 100644 index 00000000000..165ffb001f5 --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/config/rig.json @@ -0,0 +1,7 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + "rigPackageName": "local-node-rig" +} diff --git a/rush-plugins/rush-published-versions-json-plugin/eslint.config.js b/rush-plugins/rush-published-versions-json-plugin/eslint.config.js new file mode 100644 index 00000000000..87132f43292 --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/eslint.config.js @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const nodeProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node'); +const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); +const tsdocMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/tsdoc'); + +module.exports = [ + ...nodeProfile, + ...friendlyLocalsMixin, + ...tsdocMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + } + } +]; diff --git a/rush-plugins/rush-published-versions-json-plugin/package.json b/rush-plugins/rush-published-versions-json-plugin/package.json new file mode 100644 index 00000000000..6ef2132d72c --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/package.json @@ -0,0 +1,49 @@ +{ + "name": "@rushstack/rush-published-versions-json-plugin", + "version": "0.0.0", + "description": "A Rush plugin for generating a JSON file containing the versions of all published packages in the monorepo.", + "license": "MIT", + "repository": { + "url": "https://github.com/microsoft/rushstack.git", + "type": "git", + "directory": "rush-plugins/rush-published-versions-json-plugin" + }, + "scripts": { + "build": "heft test --clean", + "_phase:build": "heft run --only build -- --clean" + }, + "dependencies": { + "@rushstack/node-core-library": "workspace:*", + "@rushstack/rush-sdk": "workspace:*", + "@rushstack/terminal": "workspace:*" + }, + "devDependencies": { + "@rushstack/heft": "workspace:*", + "@rushstack/ts-command-line": "workspace:*", + "eslint": "~9.37.0", + "local-node-rig": "workspace:*" + }, + "main": "./lib-commonjs/index.js", + "module": "./lib-esm/index.js", + "types": "./lib-dts/index.d.ts", + "exports": { + ".": { + "types": "./lib-dts/index.d.ts", + "import": "./lib-esm/index.js", + "require": "./lib-commonjs/index.js" + }, + "./lib/*": { + "types": "./lib-dts/*.d.ts", + "import": "./lib-esm/*.js", + "require": "./lib-commonjs/*.js" + }, + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "lib/*": [ + "lib-dts/*" + ] + } + } +} diff --git a/rush-plugins/rush-published-versions-json-plugin/rush-plugin-manifest.json b/rush-plugins/rush-published-versions-json-plugin/rush-plugin-manifest.json new file mode 100644 index 00000000000..03631898b98 --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/rush-plugin-manifest.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugin-manifest.schema.json", + "plugins": [ + { + "pluginName": "rush-published-versions-json-plugin", + "description": "A Rush plugin for generating a JSON file containing the versions of all published packages in the monorepo.", + "entryPoint": "./lib-commonjs/index.js", + "associatedCommands": ["record-published-versions"], + "commandLineJsonFilePath": "./command-line.json" + } + ] +} diff --git a/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts b/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts new file mode 100644 index 00000000000..69b402d6d50 --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts @@ -0,0 +1,55 @@ +// 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 'node:path'; + +import type { IRequiredCommandLineStringParameter } from '@rushstack/ts-command-line'; +import { JsonFile } from '@rushstack/node-core-library'; +import type { + IGlobalCommand, + ILogger, + IRushPlugin, + RushConfiguration, + RushSession +} from '@rushstack/rush-sdk'; + +const PLUGIN_NAME: 'PublishedVersionsJsonPlugin' = 'PublishedVersionsJsonPlugin'; + +/** + * A Rush plugin for generating a JSON file containing the versions of all published packages in the monorepo. + * @public + */ +export class PublishedVersionsJsonPlugin implements IRushPlugin { + public readonly pluginName: string = PLUGIN_NAME; + + public apply(session: RushSession, rushConfiguration: RushConfiguration): void { + session.hooks.runGlobalCustomCommand + .for('record-published-versions') + .tapPromise(PLUGIN_NAME, async (command: IGlobalCommand) => { + command.setHandled(); + + const { terminal }: ILogger = session.getLogger(PLUGIN_NAME); + + const outputPathParameter: IRequiredCommandLineStringParameter = + command.getCustomParametersByLongName('--output-path'); + + const publishedPackageVersions: Record = {}; + for (const { + shouldPublish, + packageName, + packageJson: { version } + } of rushConfiguration.projects) { + if (shouldPublish) { + publishedPackageVersions[packageName] = version; + } + } + + const resolvedOutputPath: string = path.resolve(process.cwd(), outputPathParameter.value); + await JsonFile.saveAsync(publishedPackageVersions, resolvedOutputPath, { + ensureFolderExists: true + }); + + terminal.writeLine(`Wrote file to ${resolvedOutputPath}`); + }); + } +} diff --git a/rush-plugins/rush-published-versions-json-plugin/src/index.ts b/rush-plugins/rush-published-versions-json-plugin/src/index.ts new file mode 100644 index 00000000000..7b2de678a0e --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/src/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { PublishedVersionsJsonPlugin } from './PublishedVersionsJsonPlugin'; +export default PublishedVersionsJsonPlugin; diff --git a/rush-plugins/rush-published-versions-json-plugin/tsconfig.json b/rush-plugins/rush-published-versions-json-plugin/tsconfig.json new file mode 100644 index 00000000000..dac21d04081 --- /dev/null +++ b/rush-plugins/rush-published-versions-json-plugin/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/rush.json b/rush.json index be44a877901..d425937174e 100644 --- a/rush.json +++ b/rush.json @@ -1588,6 +1588,12 @@ "reviewCategory": "libraries", "shouldPublish": true }, + { + "packageName": "@rushstack/rush-published-versions-json-plugin", + "projectFolder": "rush-plugins/rush-published-versions-json-plugin", + "reviewCategory": "libraries", + "shouldPublish": true + }, { "packageName": "@rushstack/rush-pnpm-kit-v8", "projectFolder": "libraries/rush-pnpm-kit-v8", From 237a1aa524ce453295321316d54f7d83ed1004f7 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sun, 15 Mar 2026 00:56:54 -0400 Subject: [PATCH 03/11] fixup! Introduce rush-published-versions-json-plugin. --- .../src/PublishedVersionsJsonPlugin.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts b/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts index 69b402d6d50..5a86ae9d7e0 100644 --- a/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts +++ b/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts @@ -26,6 +26,12 @@ export class PublishedVersionsJsonPlugin implements IRushPlugin { session.hooks.runGlobalCustomCommand .for('record-published-versions') .tapPromise(PLUGIN_NAME, async (command: IGlobalCommand) => { + if (typeof command.setHandled !== 'function') { + throw new Error( + `${PLUGIN_NAME} requires Rush version 5.171.0 or newer. ` + + 'Please upgrade your Rush installation.' + ); + } command.setHandled(); const { terminal }: ILogger = session.getLogger(PLUGIN_NAME); From 9fa805769743d9d11506560b360661018d3d4b9b Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sun, 15 Mar 2026 01:00:35 -0400 Subject: [PATCH 04/11] fixup! Introduce rush-published-versions-json-plugin. --- .../src/PublishedVersionsJsonPlugin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts b/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts index 5a86ae9d7e0..6cba126c385 100644 --- a/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts +++ b/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts @@ -46,6 +46,7 @@ export class PublishedVersionsJsonPlugin implements IRushPlugin { packageJson: { version } } of rushConfiguration.projects) { if (shouldPublish) { + // Note that `shouldPublish` is also `true` when publishing is driven by a version policy. publishedPackageVersions[packageName] = version; } } From 5c5b7ad8a7a67332d6a20959da5f96663692b302 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sun, 15 Mar 2026 01:16:56 -0400 Subject: [PATCH 05/11] fixup! Introduce rush-published-versions-json-plugin. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e49d4c1c729..001b4c7d696 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/rush-plugins/rush-buildxl-graph-plugin](./rush-plugins/rush-buildxl-graph-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-buildxl-graph-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-buildxl-graph-plugin) | | [@rushstack/rush-buildxl-graph-plugin](https://www.npmjs.com/package/@rushstack/rush-buildxl-graph-plugin) | | [/rush-plugins/rush-http-build-cache-plugin](./rush-plugins/rush-http-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-http-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-http-build-cache-plugin) | | [@rushstack/rush-http-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-http-build-cache-plugin) | | [/rush-plugins/rush-mcp-docs-plugin](./rush-plugins/rush-mcp-docs-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-mcp-docs-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-mcp-docs-plugin) | [changelog](./rush-plugins/rush-mcp-docs-plugin/CHANGELOG.md) | [@rushstack/rush-mcp-docs-plugin](https://www.npmjs.com/package/@rushstack/rush-mcp-docs-plugin) | +| [/rush-plugins/rush-published-versions-json-plugin](./rush-plugins/rush-published-versions-json-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-published-versions-json-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-published-versions-json-plugin) | [changelog](./rush-plugins/rush-published-versions-json-plugin/CHANGELOG.md) | [@rushstack/rush-published-versions-json-plugin](https://www.npmjs.com/package/@rushstack/rush-published-versions-json-plugin) | | [/rush-plugins/rush-redis-cobuild-plugin](./rush-plugins/rush-redis-cobuild-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-redis-cobuild-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-redis-cobuild-plugin) | | [@rushstack/rush-redis-cobuild-plugin](https://www.npmjs.com/package/@rushstack/rush-redis-cobuild-plugin) | | [/rush-plugins/rush-resolver-cache-plugin](./rush-plugins/rush-resolver-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-resolver-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-resolver-cache-plugin) | | [@rushstack/rush-resolver-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-resolver-cache-plugin) | | [/rush-plugins/rush-serve-plugin](./rush-plugins/rush-serve-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-serve-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-serve-plugin) | | [@rushstack/rush-serve-plugin](https://www.npmjs.com/package/@rushstack/rush-serve-plugin) | From e4a777d1adb3b8459dfddf33c477f3b808fd6706 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 24 Mar 2026 00:56:27 -0400 Subject: [PATCH 06/11] fixup! Introduce rush-published-versions-json-plugin. --- rush-plugins/rush-published-versions-json-plugin/.npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/rush-plugins/rush-published-versions-json-plugin/.npmignore b/rush-plugins/rush-published-versions-json-plugin/.npmignore index f7a40e10213..e632c63d931 100644 --- a/rush-plugins/rush-published-versions-json-plugin/.npmignore +++ b/rush-plugins/rush-published-versions-json-plugin/.npmignore @@ -12,6 +12,7 @@ !CHANGELOG.md !CHANGELOG.json +!command-line.json !heft-plugin.json !rush-plugin-manifest.json !ThirdPartyNotice.txt From 0ff816b4728697add34bceb6c069fc89a07b529a Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 24 Mar 2026 00:57:15 -0400 Subject: [PATCH 07/11] Add "globalPlugin" command kind for plugin-native global commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new "globalPlugin" commandKind for command-line.json that can only be used in Rush plugin command-line.json files. Unlike "global" commands, it has no shellCommand — the implementation must be provided entirely by the plugin via the runGlobalCustomCommand hook with setHandled(). - Added globalPluginCommandKind constant to RushConstants - Added globalPluginCommand to JSON schema (no shellCommand/autoinstallerName) - Added IGlobalPluginCommandJson type; at runtime, normalized to IGlobalCommandConfig with shellCommand="" and isPluginOnly=true (similar to bulk→phased conversion) - Rejected globalPlugin in repo command-line.json (loadFromFileOrDefault) - Updated plugin command-line.json to use the new kind - Fixed plugin .npmignore to include command-line.json Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- common/reviews/api/rush-lib.api.md | 1 + .../src/api/CommandLineConfiguration.ts | 33 ++++++++++++- libraries/rush-lib/src/api/CommandLineJson.ts | 17 ++++++- .../rush-lib/src/cli/RushCommandLineParser.ts | 46 ++++++++++++------- .../cli/scriptActions/GlobalScriptAction.ts | 18 +++++++- libraries/rush-lib/src/logic/RushConstants.ts | 6 +++ .../src/schemas/command-line.schema.json | 31 ++++++++++++- .../command-line.json | 6 +-- .../src/PublishedVersionsJsonPlugin.ts | 6 --- 9 files changed, 131 insertions(+), 33 deletions(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index ca0f0341078..79f6dab782e 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1436,6 +1436,7 @@ export class RushConstants { static readonly defaultWatchDebounceMs: 1000; static readonly experimentsFilename: 'experiments.json'; static readonly globalCommandKind: 'global'; + static readonly globalPluginCommandKind: 'globalPlugin'; static readonly hashDelimiter: '|'; static readonly lastLinkFlagFilename: 'last-link'; static readonly mergeQueueIgnoreFileName: '.mergequeueignore'; diff --git a/libraries/rush-lib/src/api/CommandLineConfiguration.ts b/libraries/rush-lib/src/api/CommandLineConfiguration.ts index 5fb463764dc..56616b56a00 100644 --- a/libraries/rush-lib/src/api/CommandLineConfiguration.ts +++ b/libraries/rush-lib/src/api/CommandLineConfiguration.ts @@ -127,7 +127,13 @@ export interface IPhasedCommandConfig extends IPhasedCommandWithoutPhasesJson, I alwaysInstall: boolean | undefined; } -export interface IGlobalCommandConfig extends IGlobalCommandJson, ICommandWithParameters {} +export interface IGlobalCommandConfig extends IGlobalCommandJson, ICommandWithParameters { + /** + * If true, this command was declared with commandKind "globalPlugin" and must be handled + * entirely by a Rush plugin. There is no shell command to execute. + */ + isPluginOnly: boolean; +} export type Command = IGlobalCommandConfig | IPhasedCommandConfig; @@ -408,6 +414,20 @@ export class CommandLineConfiguration { case RushConstants.globalCommandKind: { normalizedCommand = { ...command, + isPluginOnly: false, + associatedParameters: new Set() + }; + break; + } + + case RushConstants.globalPluginCommandKind: { + // Normalize globalPlugin commands to global commands with an empty shellCommand, + // similar to how bulk commands are converted to phased commands. + normalizedCommand = { + ...command, + commandKind: RushConstants.globalCommandKind, + shellCommand: '', + isPluginOnly: true, associatedParameters: new Set() }; break; @@ -694,6 +714,17 @@ export class CommandLineConfiguration { this._applyBuildCommandDefaults(commandLineJson); CommandLineConfiguration._jsonSchema.validateObject(commandLineJson, jsonFilePath); + + // Validate that globalPlugin commands are not used in the repo's command-line.json + for (const { commandKind, name } of commandLineJson.commands) { + if (commandKind === RushConstants.globalPluginCommandKind) { + throw new Error( + `${RushConstants.commandLineFilename} defines a command "${name}" using ` + + `the command kind "${RushConstants.globalPluginCommandKind}". This command kind can only ` + + `be used in command-line.json files provided by Rush plugins.` + ); + } + } } } diff --git a/libraries/rush-lib/src/api/CommandLineJson.ts b/libraries/rush-lib/src/api/CommandLineJson.ts index e6507e49633..1ae70514798 100644 --- a/libraries/rush-lib/src/api/CommandLineJson.ts +++ b/libraries/rush-lib/src/api/CommandLineJson.ts @@ -5,7 +5,7 @@ * "baseCommand" from command-line.schema.json */ export interface IBaseCommandJson { - commandKind: 'bulk' | 'global' | 'phased'; + commandKind: 'bulk' | 'global' | 'globalPlugin' | 'phased'; name: string; summary: string; /** @@ -66,7 +66,20 @@ export interface IGlobalCommandJson extends IBaseCommandJson { shellCommand: string; } -export type CommandJson = IBulkCommandJson | IGlobalCommandJson | IPhasedCommandJson; +/** + * "globalPluginCommand" from command-line.schema.json. + * A global command whose implementation is provided entirely by a Rush plugin. + * This command kind can only be used in command-line.json files provided by Rush plugins. + */ +export interface IGlobalPluginCommandJson extends IBaseCommandJson { + commandKind: 'globalPlugin'; +} + +export type CommandJson = + | IBulkCommandJson + | IGlobalCommandJson + | IGlobalPluginCommandJson + | IPhasedCommandJson; /** * The dependencies of a phase. diff --git a/libraries/rush-lib/src/cli/RushCommandLineParser.ts b/libraries/rush-lib/src/cli/RushCommandLineParser.ts index cce1a59d91c..e47853cdd4a 100644 --- a/libraries/rush-lib/src/cli/RushCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushCommandLineParser.ts @@ -442,12 +442,11 @@ export class RushCommandLineParser extends CommandLineParser { commandLineConfiguration: CommandLineConfiguration, command: IGlobalCommandConfig ): void { - if ( - command.name === RushConstants.buildCommandName || - command.name === RushConstants.rebuildCommandName - ) { + const { name, shellCommand, autoinstallerName, isPluginOnly } = command; + + if (name === RushConstants.buildCommandName || name === RushConstants.rebuildCommandName) { throw new Error( - `${RushConstants.commandLineFilename} defines a command "${command.name}" using ` + + `${RushConstants.commandLineFilename} defines a command "${name}" using ` + `the command kind "${RushConstants.globalCommandKind}". This command can only be designated as a command ` + `kind "${RushConstants.bulkCommandKind}" or "${RushConstants.phasedCommandKind}".` ); @@ -460,8 +459,9 @@ export class RushCommandLineParser extends CommandLineParser { new GlobalScriptAction({ ...sharedCommandOptions, - shellCommand: command.shellCommand, - autoinstallerName: command.autoinstallerName + shellCommand, + autoinstallerName, + isPluginOnly }) ); } @@ -473,27 +473,39 @@ export class RushCommandLineParser extends CommandLineParser { const baseCommandOptions: IBaseScriptActionOptions = this._getSharedCommandActionOptions(commandLineConfiguration, command); + const { + enableParallelism, + incremental = false, + disableBuildCache = false, + allowOversubscription = true, + phases: initialPhases, + originalPhases, + watchPhases, + watchDebounceMs = RushConstants.defaultWatchDebounceMs, + alwaysWatch, + alwaysInstall + } = command; this.addAction( new PhasedScriptAction({ ...baseCommandOptions, - enableParallelism: command.enableParallelism, - incremental: command.incremental || false, - disableBuildCache: command.disableBuildCache || false, + enableParallelism, + incremental, + disableBuildCache, // The Async.forEachAsync() API defaults allowOversubscription=false, whereas Rush historically // defaults allowOversubscription=true to favor faster builds rather than strictly staying below // the CPU limit. - allowOversubscription: command.allowOversubscription ?? true, + allowOversubscription, - initialPhases: command.phases, - originalPhases: command.originalPhases, - watchPhases: command.watchPhases, - watchDebounceMs: command.watchDebounceMs ?? RushConstants.defaultWatchDebounceMs, + initialPhases, + originalPhases, + watchPhases, + watchDebounceMs, phases: commandLineConfiguration.phases, - alwaysWatch: command.alwaysWatch, - alwaysInstall: command.alwaysInstall + alwaysWatch, + alwaysInstall }) ); } diff --git a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts index 920ff0d4ba9..a44b864e96f 100644 --- a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts @@ -20,6 +20,7 @@ import { BaseScriptAction, type IBaseScriptActionOptions } from './BaseScriptAct import { Utilities } from '../../utilities/Utilities'; import { Stopwatch } from '../../utilities/Stopwatch'; import { Autoinstaller } from '../../logic/Autoinstaller'; +import { RushConstants } from '../../logic/RushConstants'; import type { IGlobalCommandConfig, IShellCommandTokenContext } from '../../api/CommandLineConfiguration'; import { measureAsyncFn } from '../../utilities/performance'; @@ -29,6 +30,7 @@ import { measureAsyncFn } from '../../utilities/performance'; export interface IGlobalScriptActionOptions extends IBaseScriptActionOptions { shellCommand: string; autoinstallerName: string | undefined; + isPluginOnly: boolean; } /** @@ -45,14 +47,17 @@ export class GlobalScriptAction extends BaseScriptAction { private readonly _shellCommand: string; private readonly _autoinstallerName: string; private readonly _autoinstallerFullPath: string; + private readonly _isPluginOnly: boolean; private _customParametersByLongName: ReadonlyMap | undefined; private _isHandled: boolean = false; public constructor(options: IGlobalScriptActionOptions) { super(options); - this._shellCommand = options.shellCommand; - this._autoinstallerName = options.autoinstallerName || ''; + const { shellCommand, isPluginOnly, autoinstallerName = '' } = options; + this._shellCommand = shellCommand; + this._isPluginOnly = isPluginOnly; + this._autoinstallerName = autoinstallerName; if (this._autoinstallerName) { Autoinstaller.validateName(this._autoinstallerName); @@ -158,6 +163,15 @@ export class GlobalScriptAction extends BaseScriptAction { return; } + if (this._isPluginOnly) { + throw new Error( + `The custom command "${this.actionName}" is a "${RushConstants.globalPluginCommandKind}" command, ` + + 'meaning its implementation must be provided entirely by a Rush plugin. However, no plugin ' + + 'called setHandled() for this command. Ensure that the plugin defining this command is ' + + 'properly installed and that it handles this command.' + ); + } + if (this._shellCommand === '') { throw new Error( `The custom command "${this.actionName}" has an empty "shellCommand" value, but no plugin ` + diff --git a/libraries/rush-lib/src/logic/RushConstants.ts b/libraries/rush-lib/src/logic/RushConstants.ts index 0f5ed03c8e8..0311f845722 100644 --- a/libraries/rush-lib/src/logic/RushConstants.ts +++ b/libraries/rush-lib/src/logic/RushConstants.ts @@ -275,6 +275,12 @@ export class RushConstants { */ public static readonly globalCommandKind: 'global' = 'global'; + /** + * The value of the "commandKind" property for a plugin-only global command in command-line.json. + * This command kind can only be used in command-line.json files provided by Rush plugins. + */ + public static readonly globalPluginCommandKind: 'globalPlugin' = 'globalPlugin'; + /** * The value of the "commandKind" property for a phased command in command-line.json */ diff --git a/libraries/rush-lib/src/schemas/command-line.schema.json b/libraries/rush-lib/src/schemas/command-line.schema.json index 0091e7bb7ae..625a2dc31dd 100644 --- a/libraries/rush-lib/src/schemas/command-line.schema.json +++ b/libraries/rush-lib/src/schemas/command-line.schema.json @@ -18,7 +18,7 @@ "title": "Command Kind", "description": "Indicates the kind of command: \"bulk\" commands are run separately for each project; \"global\" commands are run once for the entire repository.", "type": "string", - "enum": ["bulk", "global", "phased"] + "enum": ["bulk", "global", "globalPlugin", "phased"] }, "name": { "title": "Custom Command Name", @@ -168,6 +168,34 @@ } ] }, + "globalPluginCommand": { + "title": "Global Plugin Command", + "description": "A custom command that is run once for the entire repository, whose implementation is provided entirely by a Rush plugin. This command kind can only be used in command-line.json files provided by Rush plugins.", + "type": "object", + "allOf": [ + { "$ref": "#/definitions/baseCommand" }, + { + "type": "object", + "additionalProperties": true, + "properties": { + "commandKind": { + "enum": ["globalPlugin"] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "commandKind": { "$ref": "#/definitions/anything" }, + "name": { "$ref": "#/definitions/anything" }, + "summary": { "$ref": "#/definitions/anything" }, + "description": { "$ref": "#/definitions/anything" }, + "safeForSimultaneousRushProcesses": { "$ref": "#/definitions/anything" } + } + } + ] + }, "phasedCommand": { "title": "Phased Command", "description": "A command that contains multiple phases, that are run separately for each project", @@ -712,6 +740,7 @@ "oneOf": [ { "$ref": "#/definitions/bulkCommand" }, { "$ref": "#/definitions/globalCommand" }, + { "$ref": "#/definitions/globalPluginCommand" }, { "$ref": "#/definitions/phasedCommand" } ] } diff --git a/rush-plugins/rush-published-versions-json-plugin/command-line.json b/rush-plugins/rush-published-versions-json-plugin/command-line.json index 212a0ad8b6b..248cc35030b 100644 --- a/rush-plugins/rush-published-versions-json-plugin/command-line.json +++ b/rush-plugins/rush-published-versions-json-plugin/command-line.json @@ -2,12 +2,10 @@ "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", "commands": [ { - "commandKind": "global", + "commandKind": "globalPlugin", "name": "record-published-versions", "summary": "Generates a JSON file recording the version numbers of all published packages.", - "safeForSimultaneousRushProcesses": true, - // The command is handled by the plugin, so we don't want Rush to attempt to execute anything. - "shellCommand": "" + "safeForSimultaneousRushProcesses": true } ], "parameters": [ diff --git a/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts b/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts index 6cba126c385..4af7d7154cf 100644 --- a/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts +++ b/rush-plugins/rush-published-versions-json-plugin/src/PublishedVersionsJsonPlugin.ts @@ -26,12 +26,6 @@ export class PublishedVersionsJsonPlugin implements IRushPlugin { session.hooks.runGlobalCustomCommand .for('record-published-versions') .tapPromise(PLUGIN_NAME, async (command: IGlobalCommand) => { - if (typeof command.setHandled !== 'function') { - throw new Error( - `${PLUGIN_NAME} requires Rush version 5.171.0 or newer. ` + - 'Please upgrade your Rush installation.' - ); - } command.setHandled(); const { terminal }: ILogger = session.getLogger(PLUGIN_NAME); From c90ff737bea03cd2d9073250e71ce74d13c15a75 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 24 Mar 2026 12:37:06 -0400 Subject: [PATCH 08/11] Add changefile for globalPlugin command kind Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ush-published-versions-plugin_2026-03-24-16-34.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-24-16-34.json diff --git a/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-24-16-34.json b/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-24-16-34.json new file mode 100644 index 00000000000..1f13552a799 --- /dev/null +++ b/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-24-16-34.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add a new \"globalPlugin\" command kind for command-line.json that allows Rush plugins to define global commands without a shellCommand. This command kind can only be used in plugin-provided command-line.json files.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} From 1d6d15bab7bc3c0beff1c360ecbe7bed176eb011 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 24 Mar 2026 14:46:51 -0400 Subject: [PATCH 09/11] Address PR feedback: rename isPluginOnly to providedByPlugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename isPluginOnly → providedByPlugin for clarity per review feedback. Also update the first changefile comment to reflect the globalPlugin design. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...sh-published-versions-plugin_2026-03-15-04-22.json | 2 +- .../rush-lib/src/api/CommandLineConfiguration.ts | 11 ++++++----- libraries/rush-lib/src/cli/RushCommandLineParser.ts | 4 ++-- .../src/cli/scriptActions/GlobalScriptAction.ts | 10 +++++----- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json b/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json index ba6f7227095..fdcec4649fe 100644 --- a/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json +++ b/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "Add `customParametersByLongName` and `setHandled()` to `IGlobalCommand`, enabling Rush plugins to implement native global commands. Plugins can now define global commands with an empty `shellCommand`, handle execution via the `runGlobalCustomCommand` hook, and access parsed command-line parameters.", + "comment": "Add `getCustomParametersByLongName()` and `setHandled()` to `IGlobalCommand`, enabling Rush plugins to implement native global commands. Add a new `\"globalPlugin\"` command kind for plugin-provided command-line.json files that requires no `shellCommand`.", "type": "none" } ], diff --git a/libraries/rush-lib/src/api/CommandLineConfiguration.ts b/libraries/rush-lib/src/api/CommandLineConfiguration.ts index 56616b56a00..1f1db4984a9 100644 --- a/libraries/rush-lib/src/api/CommandLineConfiguration.ts +++ b/libraries/rush-lib/src/api/CommandLineConfiguration.ts @@ -129,10 +129,11 @@ export interface IPhasedCommandConfig extends IPhasedCommandWithoutPhasesJson, I export interface IGlobalCommandConfig extends IGlobalCommandJson, ICommandWithParameters { /** - * If true, this command was declared with commandKind "globalPlugin" and must be handled - * entirely by a Rush plugin. There is no shell command to execute. + * If true, this command was declared with commandKind "globalPlugin" and its implementation + * is provided by a Rush plugin via the `runGlobalCustomCommand` hook. There is no shell + * command to execute. */ - isPluginOnly: boolean; + providedByPlugin: boolean; } export type Command = IGlobalCommandConfig | IPhasedCommandConfig; @@ -414,7 +415,7 @@ export class CommandLineConfiguration { case RushConstants.globalCommandKind: { normalizedCommand = { ...command, - isPluginOnly: false, + providedByPlugin: false, associatedParameters: new Set() }; break; @@ -427,7 +428,7 @@ export class CommandLineConfiguration { ...command, commandKind: RushConstants.globalCommandKind, shellCommand: '', - isPluginOnly: true, + providedByPlugin: true, associatedParameters: new Set() }; break; diff --git a/libraries/rush-lib/src/cli/RushCommandLineParser.ts b/libraries/rush-lib/src/cli/RushCommandLineParser.ts index e47853cdd4a..76d97ab90bd 100644 --- a/libraries/rush-lib/src/cli/RushCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushCommandLineParser.ts @@ -442,7 +442,7 @@ export class RushCommandLineParser extends CommandLineParser { commandLineConfiguration: CommandLineConfiguration, command: IGlobalCommandConfig ): void { - const { name, shellCommand, autoinstallerName, isPluginOnly } = command; + const { name, shellCommand, autoinstallerName, providedByPlugin } = command; if (name === RushConstants.buildCommandName || name === RushConstants.rebuildCommandName) { throw new Error( @@ -461,7 +461,7 @@ export class RushCommandLineParser extends CommandLineParser { shellCommand, autoinstallerName, - isPluginOnly + providedByPlugin }) ); } diff --git a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts index a44b864e96f..0f0c38923e7 100644 --- a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts @@ -30,7 +30,7 @@ import { measureAsyncFn } from '../../utilities/performance'; export interface IGlobalScriptActionOptions extends IBaseScriptActionOptions { shellCommand: string; autoinstallerName: string | undefined; - isPluginOnly: boolean; + providedByPlugin: boolean; } /** @@ -47,16 +47,16 @@ export class GlobalScriptAction extends BaseScriptAction { private readonly _shellCommand: string; private readonly _autoinstallerName: string; private readonly _autoinstallerFullPath: string; - private readonly _isPluginOnly: boolean; + private readonly _providedByPlugin: boolean; private _customParametersByLongName: ReadonlyMap | undefined; private _isHandled: boolean = false; public constructor(options: IGlobalScriptActionOptions) { super(options); - const { shellCommand, isPluginOnly, autoinstallerName = '' } = options; + const { shellCommand, providedByPlugin, autoinstallerName = '' } = options; this._shellCommand = shellCommand; - this._isPluginOnly = isPluginOnly; + this._providedByPlugin = providedByPlugin; this._autoinstallerName = autoinstallerName; if (this._autoinstallerName) { @@ -163,7 +163,7 @@ export class GlobalScriptAction extends BaseScriptAction { return; } - if (this._isPluginOnly) { + if (this._providedByPlugin) { throw new Error( `The custom command "${this.actionName}" is a "${RushConstants.globalPluginCommandKind}" command, ` + 'meaning its implementation must be provided entirely by a Rush plugin. However, no plugin ' + From c4d9d790a62724dd5fd5c23737fe670eee33351c Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 24 Mar 2026 15:15:10 -0400 Subject: [PATCH 10/11] Split changefiles: API additions vs globalPlugin command kind Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../rush/rush-published-versions-plugin_2026-03-15-04-22.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json b/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json index fdcec4649fe..7bd951cc0d9 100644 --- a/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json +++ b/common/changes/@microsoft/rush/rush-published-versions-plugin_2026-03-15-04-22.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "Add `getCustomParametersByLongName()` and `setHandled()` to `IGlobalCommand`, enabling Rush plugins to implement native global commands. Add a new `\"globalPlugin\"` command kind for plugin-provided command-line.json files that requires no `shellCommand`.", + "comment": "Add `getCustomParametersByLongName()` and `setHandled()` to `IGlobalCommand`, enabling Rush plugins to handle global command execution and access parsed command-line parameter values.", "type": "none" } ], From f6e6cefec598f6d9ec051a84f0e19c30d7cc2a98 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 24 Mar 2026 17:15:36 -0400 Subject: [PATCH 11/11] Rush update. --- .../config/subspaces/default/pnpm-lock.yaml | 40 +++++++++---------- .../config/subspaces/default/repo-state.json | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index a58e687a549..365bd4234e1 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -782,7 +782,7 @@ importers: version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/cli': specifier: ~6.4.18 - version: 6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2) + version: 6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2) '@storybook/components': specifier: ~6.4.18 version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -818,7 +818,7 @@ importers: version: 5.2.7(webpack@4.47.0) jest: specifier: ~29.3.1 - version: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + version: 29.3.1(@types/node@20.17.19) react: specifier: ~17.0.2 version: 17.0.2 @@ -1004,7 +1004,7 @@ importers: version: 5.2.7(webpack@5.105.2) jest: specifier: ~29.3.1 - version: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + version: 29.3.1(@types/node@20.17.19) react: specifier: ~19.2.3 version: 19.2.4 @@ -5101,13 +5101,13 @@ importers: '@rushstack/terminal': specifier: workspace:* version: link:../../libraries/terminal - '@rushstack/ts-command-line': - specifier: workspace:* - version: link:../../libraries/ts-command-line devDependencies: '@rushstack/heft': specifier: workspace:* version: link:../../apps/heft + '@rushstack/ts-command-line': + specifier: workspace:* + version: link:../../libraries/ts-command-line eslint: specifier: ~9.37.0 version: 9.37.0 @@ -22452,7 +22452,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': + '@jest/core@29.7.0': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -22466,7 +22466,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.9.3)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@22.9.3) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -25179,7 +25179,7 @@ snapshots: ts-dedent: 2.2.0 util-deprecate: 1.0.2 - '@storybook/cli@6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)': + '@storybook/cli@6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)': dependencies: '@babel/core': 7.20.12 '@babel/preset-env': 7.28.6(@babel/core@7.20.12) @@ -25199,7 +25199,7 @@ snapshots: fs-extra: 9.1.0 get-port: 5.1.1 globby: 11.1.0 - jest: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + jest: 29.3.1(@types/node@20.17.19) jscodeshift: 0.13.1(@babel/preset-env@7.28.6(@babel/core@7.20.12)) json5: 2.2.3 leven: 3.1.0 @@ -28792,13 +28792,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 - create-jest@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): + create-jest@29.7.0(@types/node@20.17.19): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@20.17.19) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -32170,16 +32170,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): + jest-cli@29.7.0(@types/node@20.17.19): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0(@types/node@20.17.19) '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + create-jest: 29.7.0(@types/node@20.17.19) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@20.17.19) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -32249,7 +32249,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): + jest-config@29.7.0(@types/node@20.17.19): dependencies: '@babel/core': 7.20.12 '@jest/test-sequencer': 29.7.0(@types/node@20.17.19) @@ -32279,7 +32279,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.9.3)(babel-plugin-macros@3.1.0): + jest-config@29.7.0(@types/node@22.9.3): dependencies: '@babel/core': 7.20.12 '@jest/test-sequencer': 29.7.0(@types/node@22.9.3) @@ -32667,12 +32667,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0): + jest@29.3.1(@types/node@20.17.19): dependencies: '@jest/core': 29.5.0(babel-plugin-macros@3.1.0) '@jest/types': 29.5.0 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + jest-cli: 29.7.0(@types/node@20.17.19) transitivePeerDependencies: - '@types/node' - babel-plugin-macros diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 0c7522683f8..bd4f7c897f2 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "54470e7caa94b190d86dbd4f9cc43e0d58456972", + "pnpmShrinkwrapHash": "c482c23c40b202ed750549c796c24b3550d0ba6e", "preferredVersionsHash": "029c99bd6e65c5e1f25e2848340509811ff9753c" }