Skip to content

Commit 21eee71

Browse files
authored
Merge pull request microsoft#684 from Microsoft/pgonzal/rush-global-commands2
[rush] Add support for "global" custom commands
2 parents 5a04ea9 + 589ae36 commit 21eee71

17 files changed

Lines changed: 419 additions & 263 deletions

apps/rush-lib/src/api/CommandLineJson.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface IBulkCommandJson extends IBaseCommandJson {
2525
*/
2626
export interface IGlobalCommandJson extends IBaseCommandJson {
2727
commandKind: 'global';
28-
scriptPath: string;
28+
shellCommand: string;
2929
}
3030

3131
export type CommandJson = IBulkCommandJson | IGlobalCommandJson;

apps/rush-lib/src/api/Rush.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IPackageJson } from '@microsoft/node-core-library';
88

99
import { RushCommandLineParser } from '../cli/RushCommandLineParser';
1010
import { RushConstants } from './RushConstants';
11-
import { RushX } from './RushX';
11+
import { RushXCommandLine } from '../cli/RushXCommandLine';
1212
import { CommandLineMigrationAdvisor } from '../cli/CommandLineMigrationAdvisor';
1313

1414
/**
@@ -55,7 +55,7 @@ export class Rush {
5555
public static launchRushX(launcherVersion: string, isManaged: boolean): void {
5656
Rush._printStartupBanner(isManaged);
5757

58-
RushX.launchRushX(launcherVersion, isManaged);
58+
RushXCommandLine.launchRushX(launcherVersion, isManaged);
5959
}
6060

6161
/**

apps/rush-lib/src/cli/RushCommandLineParser.ts

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import * as os from 'os';
54
import * as colors from 'colors';
6-
import { CommandLineParser, CommandLineFlagParameter } from '@microsoft/ts-command-line';
5+
import * as os from 'os';
6+
import * as path from 'path';
7+
8+
import { CommandLineParser, CommandLineFlagParameter, CommandLineAction } from '@microsoft/ts-command-line';
79

810
import { RushConfiguration } from '../api/RushConfiguration';
11+
import { RushConstants } from '../api/RushConstants';
12+
import { CommandLineConfiguration } from '../api/CommandLineConfiguration';
13+
import { CommandJson } from '../api/CommandLineJson';
914
import { Utilities } from '../utilities/Utilities';
15+
import { BaseScriptAction } from '../cli/scriptActions/BaseScriptAction';
16+
1017
import { ChangeAction } from './actions/ChangeAction';
1118
import { CheckAction } from './actions/CheckAction';
1219
import { UpdateAction } from './actions/UpdateAction';
@@ -17,7 +24,9 @@ import { PurgeAction } from './actions/PurgeAction';
1724
import { UnlinkAction } from './actions/UnlinkAction';
1825
import { ScanAction } from './actions/ScanAction';
1926
import { VersionAction } from './actions/VersionAction';
20-
import { CustomCommandFactory } from './custom/CustomCommandFactory';
27+
28+
import { BulkScriptAction } from './scriptActions/BulkScriptAction';
29+
import { GlobalScriptAction } from './scriptActions/GlobalScriptAction';
2130

2231
import { Telemetry } from '../logic/Telemetry';
2332
import { AlreadyReportedError } from '../utilities/AlreadyReportedError';
@@ -100,17 +109,117 @@ export class RushCommandLineParser extends CommandLineParser {
100109
this.addAction(new UnlinkAction(this));
101110
this.addAction(new VersionAction(this));
102111

103-
CustomCommandFactory.addActions(this);
112+
this._populateScriptActions();
104113

105114
} catch (error) {
106115
this._reportErrorAndSetExitCode(error);
107116
}
108117
}
109118

119+
private _populateScriptActions(): void {
120+
if (!this.rushConfiguration) {
121+
return;
122+
}
123+
124+
const commandLineConfigFile: string = path.join(
125+
this.rushConfiguration.commonRushConfigFolder, RushConstants.commandLineFilename
126+
);
127+
const commandLineConfiguration: CommandLineConfiguration
128+
= CommandLineConfiguration.loadFromFileOrDefault(commandLineConfigFile);
129+
130+
const documentationForBuild: string = 'The Rush build command assumes that the package.json file for each'
131+
+ ' project contains a "scripts" entry for "npm run build". It invokes'
132+
+ ' this commands to build each project. Projects are built in parallel where'
133+
+ ' possible, but always respecting the dependency graph for locally linked projects.'
134+
+ ' The number of simultaneous processes will be based on the number of machine cores'
135+
+ ' unless overridden by the --parallelism flag.';
136+
137+
// always create a build and a rebuild command
138+
this.addAction(new BulkScriptAction({
139+
actionName: 'build',
140+
summary: '(EXPERIMENTAL) Build all projects that haven\'t been built, or have changed since they were last '
141+
+ 'built.',
142+
documentation: documentationForBuild,
143+
144+
parser: this,
145+
commandLineConfiguration: commandLineConfiguration,
146+
147+
enableParallelism: true,
148+
ignoreMissingScript: false
149+
}));
150+
151+
this.addAction(new BulkScriptAction({
152+
actionName: 'rebuild',
153+
summary: 'Clean and rebuild the entire set of projects',
154+
documentation: documentationForBuild,
155+
156+
parser: this,
157+
commandLineConfiguration: commandLineConfiguration,
158+
159+
enableParallelism: true,
160+
ignoreMissingScript: false
161+
}));
162+
163+
// Register each custom command
164+
for (const command of commandLineConfiguration.commands) {
165+
if (this.tryGetAction(command.name)) {
166+
throw new Error(`${RushConstants.commandLineFilename} defines a command "${command.name}"`
167+
+ ` using a name that already exists`);
168+
}
169+
170+
switch (command.commandKind) {
171+
case 'bulk':
172+
this.addAction(new BulkScriptAction({
173+
actionName: command.name,
174+
summary: command.summary,
175+
documentation: command.description || command.summary,
176+
177+
parser: this,
178+
commandLineConfiguration: commandLineConfiguration,
179+
180+
enableParallelism: command.enableParallelism,
181+
ignoreMissingScript: command.ignoreMissingScript || false
182+
}));
183+
break;
184+
case 'global':
185+
this.addAction(new GlobalScriptAction({
186+
actionName: command.name,
187+
summary: command.summary,
188+
documentation: command.description || command.summary,
189+
190+
parser: this,
191+
commandLineConfiguration: commandLineConfiguration,
192+
193+
shellCommand: command.shellCommand
194+
}));
195+
break;
196+
default:
197+
throw new Error(`${RushConstants.commandLineFilename} defines a command "${command!.name}"`
198+
+ ` using an unsupported command kind "${command!.commandKind}"`);
199+
}
200+
}
201+
202+
// Check for any invalid associations
203+
for (const parameter of commandLineConfiguration.parameters) {
204+
for (const associatedCommand of parameter.associatedCommands) {
205+
const action: CommandLineAction | undefined = this.tryGetAction(associatedCommand);
206+
if (!action) {
207+
throw new Error(`${RushConstants.commandLineFilename} defines a parameter "${parameter.longName}"`
208+
+ ` that is associated with a nonexistent command "${associatedCommand}"`);
209+
}
210+
if (!(action instanceof BaseScriptAction)) {
211+
throw new Error(`${RushConstants.commandLineFilename} defines a parameter "${parameter.longName}"`
212+
+ ` that is associated with a command "${associatedCommand}", but that command does not`
213+
+ ` support custom parameters`);
214+
}
215+
}
216+
}
217+
}
218+
110219
private _reportErrorAndSetExitCode(error: Error): void {
111220
if (!(error instanceof AlreadyReportedError)) {
112221
const prefix: string = 'ERROR: ';
113-
console.error(os.EOL + colors.red(prefix + Utilities.wrapWords(error.message).trim()));
222+
console.error(os.EOL + colors.red(Utilities.wrapWords(prefix + error.message)));
114223
}
115224

116225
if (this._debugParameter.value) {
@@ -127,6 +236,10 @@ export class RushCommandLineParser extends CommandLineParser {
127236
// performs nontrivial work that can throw an exception. Either the Rush class would need
128237
// to handle reporting for those exceptions, or else _populateActions() should be moved
129238
// to a RushCommandLineParser lifecycle stage that can handle it.
130-
process.exit(1);
239+
if (process.exitCode > 0) {
240+
process.exit(process.exitCode);
241+
} else {
242+
process.exit(1);
243+
}
131244
}
132245
}
Lines changed: 24 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,20 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import * as child_process from 'child_process';
54
import * as colors from 'colors';
65
import * as os from 'os';
76
import * as path from 'path';
87

98
import {
109
PackageJsonLookup,
1110
IPackageJson,
12-
Text,
13-
IPackageJsonScriptTable
11+
Text
1412
} from '@microsoft/node-core-library';
1513
import { Utilities } from '../utilities/Utilities';
14+
import { ProjectCommandSet } from '../logic/ProjectCommandSet';
15+
import { RushConfiguration } from '../api/RushConfiguration';
1616

17-
/**
18-
* Parses the "scripts" section from package.json
19-
*/
20-
class ProjectCommandSet {
21-
public readonly malformedScriptNames: string[] = [];
22-
public readonly commandNames: string[] = [];
23-
private readonly _scriptsByName: Map<string, string> = new Map<string, string>();
24-
25-
public constructor(packageJson: IPackageJson) {
26-
const scripts: IPackageJsonScriptTable = packageJson.scripts || { };
27-
28-
for (const scriptName of Object.keys(scripts)) {
29-
if (scriptName[0] === '-' || scriptName.length === 0) {
30-
this.malformedScriptNames.push(scriptName);
31-
} else {
32-
this.commandNames.push(scriptName);
33-
this._scriptsByName.set(scriptName, scripts[scriptName]);
34-
}
35-
}
36-
37-
this.commandNames.sort();
38-
}
39-
40-
public tryGetScriptBody(commandName: string): string | undefined {
41-
return this._scriptsByName.get(commandName);
42-
}
43-
44-
public getScriptBody(commandName: string): string {
45-
const result: string | undefined = this.tryGetScriptBody(commandName);
46-
if (result === undefined) {
47-
throw new Error(`The command "${commandName}" was not found`);
48-
}
49-
return result;
50-
}
51-
}
52-
53-
export class RushX {
17+
export class RushXCommandLine {
5418
public static launchRushX(launcherVersion: string, isManaged: boolean): void {
5519
// NodeJS can sometimes accidentally terminate with a zero exit code (e.g. for an uncaught
5620
// promise exception), so we start with the assumption that the exit code is 1
@@ -82,7 +46,7 @@ export class RushX {
8246
// rush -h
8347
// rush --unrecognized-option
8448
if (args.length === 0 || args[0][0] === '-') {
85-
RushX._showUsage(packageJson, projectCommandSet);
49+
RushXCommandLine._showUsage(packageJson, projectCommandSet);
8650
return;
8751
}
8852

@@ -103,24 +67,32 @@ export class RushX {
10367
return;
10468
}
10569

70+
// Are we in a Rush repo?
71+
let rushConfiguration: RushConfiguration | undefined = undefined;
72+
if (RushConfiguration.tryFindRushJsonLocation(false)) {
73+
rushConfiguration = RushConfiguration.loadFromDefaultLocation();
74+
}
75+
10676
console.log('Executing: ' + JSON.stringify(scriptBody) + os.EOL);
10777

10878
const packageFolder: string = path.dirname(packageJsonFilePath);
10979

110-
const result: child_process.ChildProcess = Utilities.executeLifecycleCommandAsync(
111-
scriptBody,
112-
packageFolder,
113-
packageFolder
80+
const exitCode: number = Utilities.executeLifecycleCommand(scriptBody,
81+
{
82+
workingDirectory: packageFolder,
83+
// If there is a rush.json then use its .npmrc from the temp folder.
84+
// Otherwise look for npmrc in the project folder.
85+
initCwd: rushConfiguration ? rushConfiguration.commonTempFolder : packageFolder,
86+
handleOutput: false
87+
}
11488
);
11589

116-
result.on('close', (code) => {
117-
if (code) {
118-
console.log(colors.red(`The script failed with exit code ${code}`));
119-
}
90+
if (exitCode > 0) {
91+
console.log(colors.red(`The script failed with exit code ${exitCode}`));
92+
}
93+
94+
process.exitCode = exitCode;
12095

121-
// Pass along the exit code of the child process
122-
process.exitCode = code || 0;
123-
});
12496
} catch (error) {
12597
console.log(colors.red('Error: ' + error.message));
12698
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { RushConfiguration } from '../../api/RushConfiguration';
1616
import { EventHooksManager } from '../../logic/EventHooksManager';
1717
import { RushCommandLineParser } from './../RushCommandLineParser';
1818

19-
export interface IRushCommandLineActionOptions extends ICommandLineActionOptions {
19+
export interface IBaseRushActionOptions extends ICommandLineActionOptions {
2020
/**
2121
* If true, no locking mechanism will be enforced when this action is run.
2222
* Note this defaults to false (which is a safer assumption in case this value
@@ -46,7 +46,7 @@ export abstract class BaseRushAction extends CommandLineAction {
4646
return this._parser;
4747
}
4848

49-
constructor(options: IRushCommandLineActionOptions) {
49+
constructor(options: IBaseRushActionOptions) {
5050
super(options);
5151

5252
this._parser = options.parser;

0 commit comments

Comments
 (0)