Skip to content

Commit fe386a0

Browse files
author
Nicholas Pape
authored
Add custom commands & options to Rush (microsoft#387)
1 parent 56ca101 commit fe386a0

26 files changed

+930
-695
lines changed

apps/rush-lib/config/pre-copy.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"copyTo": {
3+
"./lib/examples": [
4+
"./src/examples/**/*.*"
5+
]
6+
}
7+
}

apps/rush-lib/src/RushConstants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ export namespace RushConstants {
8282
*/
8383
export const packageDepsFilename: string = 'package-deps.json';
8484

85+
/**
86+
* Custom command line configuration file, which is used by rush for implementing
87+
* custom command and options.
88+
*/
89+
export const commandLineFilename: string = 'command-line.json';
90+
8591
/**
8692
* @beta
8793
*/

apps/rush-lib/src/cli/actions/BuildAction.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
CommandLineConfiguration,
3+
ICustomCommand,
4+
CustomOption
5+
} from '../../data/CommandLineConfiguration';
6+
7+
import RushCommandLineParser from './RushCommandLineParser';
8+
import { CustomRushAction } from './CustomRushAction';
9+
10+
/**
11+
* Using the custom command line configuration, generates a set of
12+
* rush actions that are then registered to the command line.
13+
*/
14+
export class CustomCommandFactory {
15+
public static createCommands(
16+
parser: RushCommandLineParser,
17+
commandLineConfig: CommandLineConfiguration
18+
): Map<string, CustomRushAction> {
19+
const customActions: Map<string, CustomRushAction> = new Map<string, CustomRushAction>();
20+
21+
const documentationForBuild: string = 'The Rush build command assumes that the package.json file for each'
22+
+ ' project contains scripts for "npm run clean" and "npm run test". It invokes'
23+
+ ' these commands to build each project. Projects are built in parallel where'
24+
+ ' possible, but always respecting the dependency graph for locally linked projects.'
25+
+ ' The number of simultaneous processes will be equal to the number of machine cores.'
26+
+ ' unless overridden by the --parallelism flag.';
27+
28+
// always create a build and a rebuild command
29+
customActions.set('build', new CustomRushAction(parser, {
30+
actionVerb: 'build',
31+
summary: '(EXPERIMENTAL) Build all projects that haven\'t been built, or have changed since they were last '
32+
+ 'built.',
33+
documentation: documentationForBuild
34+
}));
35+
36+
customActions.set('rebuild', new CustomRushAction(parser, {
37+
actionVerb: 'rebuild',
38+
summary: 'Clean and rebuild the entire set of projects',
39+
documentation: documentationForBuild
40+
}));
41+
42+
// Register each custom command
43+
commandLineConfig.commands.forEach((command: ICustomCommand) => {
44+
if (customActions.get(command.name)) {
45+
throw new Error(`Cannot define two custom actions with the same name: "${command.name}"`);
46+
}
47+
customActions.set(command.name, new CustomRushAction(parser, {
48+
actionVerb: command.name,
49+
summary: command.summary,
50+
documentation: command.documentation || command.summary
51+
},
52+
command.parallelized !== false));
53+
});
54+
55+
// Associate each custom option to a command
56+
commandLineConfig.options.forEach((customOption: CustomOption, longName: string) => {
57+
customOption.associatedCommands.forEach((associatedCommand: string) => {
58+
const customAction: CustomRushAction | undefined = customActions.get(associatedCommand);
59+
if (customAction) {
60+
customAction.addCustomOption(longName, customOption);
61+
} else {
62+
throw new Error(`Cannot find custom command "${associatedCommand}" associated with`
63+
+ ` custom option "${longName}".`);
64+
}
65+
});
66+
});
67+
68+
return customActions;
69+
}
70+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import * as fsx from 'fs-extra';
5+
import * as os from 'os';
6+
import * as colors from 'colors';
7+
8+
import {
9+
Event
10+
} from '../../index';
11+
12+
import {
13+
CustomOption,
14+
ICustomEnumValue
15+
} from '../../data/CommandLineConfiguration';
16+
17+
import {
18+
CommandLineFlagParameter,
19+
CommandLineIntegerParameter,
20+
CommandLineStringListParameter,
21+
CommandLineOptionParameter,
22+
ICommandLineActionOptions
23+
} from '@microsoft/ts-command-line';
24+
25+
import RushCommandLineParser from './RushCommandLineParser';
26+
import { BaseRushAction } from './BaseRushAction';
27+
import { TaskSelector } from '../utilities/TaskSelector';
28+
import { Stopwatch } from '../../utilities/Stopwatch';
29+
30+
interface ICustomOptionInstance {
31+
optionDefinition: CustomOption;
32+
parameterValue?: CommandLineFlagParameter | CommandLineOptionParameter;
33+
}
34+
35+
export class CustomRushAction extends BaseRushAction {
36+
private customOptions: Map<string, ICustomOptionInstance> = new Map<string, ICustomOptionInstance>();
37+
38+
private _fromFlag: CommandLineStringListParameter;
39+
private _toFlag: CommandLineStringListParameter;
40+
private _verboseParameter: CommandLineFlagParameter;
41+
private _parallelismParameter: CommandLineIntegerParameter;
42+
43+
constructor(private _parser: RushCommandLineParser,
44+
options: ICommandLineActionOptions,
45+
private _parallelized: boolean = true) {
46+
47+
super(options);
48+
}
49+
50+
/**
51+
* Registers a custom option to a task. This custom option is then registered during onDefineParameters()
52+
* @param longName the long name of the option, e.g. "--verbose"
53+
* @param option the Custom Option definition
54+
*/
55+
public addCustomOption(longName: string, option: CustomOption): void {
56+
if (this.customOptions.get(longName)) {
57+
throw new Error(`Cannot define two custom options with the same name: "${longName}"`);
58+
}
59+
this.customOptions.set(longName, {
60+
optionDefinition: option
61+
});
62+
}
63+
64+
public run(): void {
65+
if (!fsx.existsSync(this.rushConfiguration.rushLinkJsonFilename)) {
66+
throw new Error(`File not found: ${this.rushConfiguration.rushLinkJsonFilename}` +
67+
`${os.EOL}Did you run "rush link"?`);
68+
}
69+
this.eventHooksManager.handle(Event.preRushBuild);
70+
71+
const stopwatch: Stopwatch = Stopwatch.start();
72+
73+
const isQuietMode: boolean = !(this._verboseParameter.value);
74+
const parallelism: number = (this._parallelized ? this._parallelismParameter.value : 1);
75+
76+
// collect all custom flags here
77+
const customFlags: string[] = [];
78+
this.customOptions.forEach((customOption: ICustomOptionInstance, longName: string) => {
79+
if (customOption.parameterValue!.value) {
80+
if (customOption.optionDefinition.optionType === 'flag') {
81+
customFlags.push(longName);
82+
} else if (customOption.optionDefinition.optionType === 'enum') {
83+
customFlags.push(`${longName}=${customOption.parameterValue!.value}`);
84+
}
85+
}
86+
});
87+
88+
const tasks: TaskSelector = new TaskSelector(
89+
{
90+
rushConfiguration: this._parser.rushConfig,
91+
toFlags: this._toFlag.value,
92+
fromFlags: this._fromFlag.value,
93+
commandToRun: this.options.actionVerb,
94+
customFlags,
95+
isQuietMode,
96+
parallelism,
97+
isIncrementalBuildAllowed: this.options.actionVerb === 'build'
98+
}
99+
);
100+
101+
tasks.execute().then(
102+
() => {
103+
stopwatch.stop();
104+
console.log(colors.green(`rush ${this.options.actionVerb} (${stopwatch.toString()})`));
105+
this._collectTelemetry(stopwatch, true);
106+
this._parser.flushTelemetry();
107+
this.eventHooksManager.handle(Event.postRushBuild, this._parser.isDebug);
108+
},
109+
() => {
110+
stopwatch.stop();
111+
console.log(colors.red(`rush ${this.options.actionVerb} - Errors! (${stopwatch.toString()})`));
112+
this._collectTelemetry(stopwatch, false);
113+
this._parser.flushTelemetry();
114+
this.eventHooksManager.handle(Event.postRushBuild, this._parser.isDebug);
115+
this._parser.exitWithError();
116+
});
117+
}
118+
119+
protected onDefineParameters(): void {
120+
this._parallelismParameter = this.defineIntegerParameter({
121+
parameterLongName: '--parallelism',
122+
parameterShortName: '-p',
123+
key: 'COUNT',
124+
description: 'Change limit the number of simultaneous builds. This value defaults to the number of CPU cores'
125+
});
126+
this._toFlag = this.defineStringListParameter({
127+
parameterLongName: '--to',
128+
parameterShortName: '-t',
129+
key: 'PROJECT1',
130+
description: 'Build the specified project and all of its dependencies'
131+
});
132+
this._fromFlag = this.defineStringListParameter({
133+
parameterLongName: '--from',
134+
parameterShortName: '-f',
135+
key: 'PROJECT2',
136+
description: 'Build all projects that directly or indirectly depend on the specified project'
137+
});
138+
this._verboseParameter = this.defineFlagParameter({
139+
parameterLongName: '--verbose',
140+
parameterShortName: '-v',
141+
description: 'Display the logs during the build, rather than just displaying the build status summary'
142+
});
143+
144+
// @TODO we should throw if they are trying to overwrite built in flags
145+
146+
this.customOptions.forEach((customOption: ICustomOptionInstance, longName: string) => {
147+
if (customOption.optionDefinition.optionType === 'flag') {
148+
customOption.parameterValue = this.defineFlagParameter({
149+
parameterShortName: customOption.optionDefinition.shortName,
150+
parameterLongName: longName,
151+
description: customOption.optionDefinition.description
152+
});
153+
} else if (customOption.optionDefinition.optionType === 'enum') {
154+
customOption.parameterValue = this.defineOptionParameter({
155+
parameterShortName: customOption.optionDefinition.shortName,
156+
parameterLongName: longName,
157+
description: customOption.optionDefinition.description,
158+
defaultValue: customOption.optionDefinition.defaultValue,
159+
options: customOption.optionDefinition.enumValues.map((enumValue: ICustomEnumValue) => {
160+
return enumValue.name;
161+
})
162+
});
163+
}
164+
});
165+
}
166+
167+
private _collectTelemetry(stopwatch: Stopwatch, success: boolean): void {
168+
const extraData: { [key: string]: string } = {
169+
command_to: (!!this._toFlag.value).toString(),
170+
command_from: (!!this._fromFlag.value).toString()
171+
};
172+
173+
this.customOptions.forEach((customOption: ICustomOptionInstance, longName: string) => {
174+
if (customOption.parameterValue!.value) {
175+
extraData[`${this.options.actionVerb}_${longName}`] =
176+
customOption.parameterValue!.value.toString();
177+
}
178+
});
179+
180+
this._parser.telemetry.log({
181+
name: this.options.actionVerb,
182+
duration: stopwatch.duration,
183+
result: success ? 'Succeeded' : 'Failed',
184+
extraData
185+
});
186+
}
187+
}

0 commit comments

Comments
 (0)