Skip to content

Commit cc3fb1f

Browse files
committed
Add functions to better support commands provided by global commands.
1 parent ac69e07 commit cc3fb1f

4 files changed

Lines changed: 76 additions & 3 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"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.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

common/reviews/api/rush-lib.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,8 @@ export interface IGetChangedProjectsOptions {
512512

513513
// @beta
514514
export interface IGlobalCommand extends IRushCommand {
515+
getCustomParametersByLongName<TParameter extends CommandLineParameter>(longName: string): TParameter;
516+
setHandled(): void;
515517
}
516518

517519
// @public
@@ -1500,7 +1502,7 @@ export class RushLifecycleHooks {
15001502
variant: string | undefined
15011503
]>;
15021504
readonly beforeInstall: AsyncSeriesHook<[
1503-
command: IGlobalCommand,
1505+
command: IRushCommand,
15041506
subspace: Subspace,
15051507
variant: string | undefined
15061508
]>;

libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as path from 'node:path';
55

66
import type { AsyncSeriesHook } from 'tapable';
77

8+
import type { CommandLineParameter } from '@rushstack/ts-command-line';
89
import {
910
FileSystem,
1011
type IPackageJson,
@@ -45,6 +46,9 @@ export class GlobalScriptAction extends BaseScriptAction<IGlobalCommandConfig> {
4546
private readonly _autoinstallerName: string;
4647
private readonly _autoinstallerFullPath: string;
4748

49+
private _customParametersByLongName: ReadonlyMap<string, CommandLineParameter> | undefined;
50+
private _isHandled: boolean = false;
51+
4852
public constructor(options: IGlobalScriptActionOptions) {
4953
super(options);
5054
this._shellCommand = options.shellCommand;
@@ -93,6 +97,37 @@ export class GlobalScriptAction extends BaseScriptAction<IGlobalCommandConfig> {
9397
this.defineScriptParameters();
9498
}
9599

100+
/**
101+
* {@inheritDoc IGlobalCommand.setHandled}
102+
*/
103+
public setHandled(): void {
104+
this._isHandled = true;
105+
}
106+
107+
/**
108+
* {@inheritDoc IGlobalCommand.getCustomParametersByLongName}
109+
*/
110+
public getCustomParametersByLongName<TParameter extends CommandLineParameter>(
111+
longName: string
112+
): TParameter {
113+
if (!this._customParametersByLongName) {
114+
const map: Map<string, CommandLineParameter> = new Map();
115+
for (const [parameterJson, parameter] of this.customParameters) {
116+
map.set(parameterJson.longName, parameter);
117+
}
118+
this._customParametersByLongName = map;
119+
}
120+
121+
const parameter: CommandLineParameter | undefined = this._customParametersByLongName.get(longName);
122+
if (!parameter) {
123+
throw new Error(
124+
`The command "${this.actionName}" does not have a custom parameter with long name "${longName}".`
125+
);
126+
}
127+
128+
return parameter as TParameter;
129+
}
130+
96131
private async _prepareAutoinstallerNameAsync(): Promise<void> {
97132
const autoInstaller: Autoinstaller = new Autoinstaller({
98133
autoinstallerName: this._autoinstallerName,
@@ -117,6 +152,20 @@ export class GlobalScriptAction extends BaseScriptAction<IGlobalCommandConfig> {
117152
await hookForAction.promise(this);
118153
}
119154

155+
// If a plugin hook called setHandled(), the command has been fully handled.
156+
// Skip the default shell command execution.
157+
if (this._isHandled) {
158+
return;
159+
}
160+
161+
if (this._shellCommand === '') {
162+
throw new Error(
163+
`The custom command "${this.actionName}" has an empty "shellCommand" value, but no plugin ` +
164+
'called setHandled() for this command. An empty "shellCommand" is intended for global ' +
165+
'commands whose implementation is provided entirely by a Rush plugin.'
166+
);
167+
}
168+
120169
const additionalPathFolders: string[] =
121170
this.commandLineConfiguration?.additionalPathFolders.slice() || [];
122171

libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import { AsyncParallelHook, AsyncSeriesHook, HookMap } from 'tapable';
55

6+
import type { CommandLineParameter } from '@rushstack/ts-command-line';
7+
68
import type { ITelemetryData } from '../logic/Telemetry';
79
import type { PhasedCommandHooks } from './PhasedCommandHooks';
810
import type { Subspace } from '../api/Subspace';
@@ -23,7 +25,17 @@ export interface IRushCommand {
2325
* @beta
2426
*/
2527
export interface IGlobalCommand extends IRushCommand {
26-
// Nothing added.
28+
/**
29+
* Get a parameter by its long name (e.g. "--output-path") that was defined in command-line.json for this command.
30+
* If the parameter was not defined or not provided on the command line, this will throw.
31+
*/
32+
getCustomParametersByLongName<TParameter extends CommandLineParameter>(longName: string): TParameter;
33+
34+
/**
35+
* Call this from a plugin hook to indicate that the command has been fully handled
36+
* by the plugin. When set, the default shell command execution will be skipped.
37+
*/
38+
setHandled(): void;
2739
}
2840

2941
/**
@@ -94,7 +106,7 @@ export class RushLifecycleHooks {
94106
* The hook to run between preparing the common/temp folder and invoking the package manager during "rush install" or "rush update".
95107
*/
96108
public readonly beforeInstall: AsyncSeriesHook<
97-
[command: IGlobalCommand, subspace: Subspace, variant: string | undefined]
109+
[command: IRushCommand, subspace: Subspace, variant: string | undefined]
98110
> = new AsyncSeriesHook(['command', 'subspace', 'variant'], 'beforeInstall');
99111

100112
/**

0 commit comments

Comments
 (0)