Skip to content

Commit bd6449a

Browse files
authored
Merge pull request microsoft#1485 from darsi-an/simplify-setting-build-rebuild-commands
[rush] Add support to simplify setting for build and rebuild commands
2 parents 0eba933 + 543b5c3 commit bd6449a

File tree

16 files changed

+419
-99
lines changed

16 files changed

+419
-99
lines changed

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

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,51 @@ import {
2222
*/
2323
export class CommandLineConfiguration {
2424
private static _jsonSchema: JsonSchema = JsonSchema.fromFile(
25-
path.join(__dirname, '../schemas/command-line.schema.json'));
25+
path.join(__dirname, '../schemas/command-line.schema.json')
26+
);
2627

2728
public readonly commands: CommandJson[] = [];
2829
public readonly parameters: ParameterJson[] = [];
2930

31+
public static readonly defaultBuildCommandJson: CommandJson = {
32+
commandKind: RushConstants.bulkCommandKind,
33+
name: RushConstants.buildCommandName,
34+
summary: 'Build all projects that haven\'t been built, or have changed since they were last built.',
35+
description: 'This command is similar to "rush rebuild", except that "rush build" performs'
36+
+ ' an incremental build. In other words, it only builds projects whose source files have changed'
37+
+ ' since the last successful build. The analysis requires a Git working tree, and only considers'
38+
+ ' source files that are tracked by Git and whose path is under the project folder. (For more details'
39+
+ ' about this algorithm, see the documentation for the "package-deps-hash" NPM package.) The incremental'
40+
+ ' build state is tracked in a per-project folder called ".rush/temp" which should NOT be added to Git. The'
41+
+ ' build command is tracked by the "arguments" field in the "package-deps_build.json" file contained'
42+
+ ' therein; a full rebuild is forced whenever the command has changed (e.g. "--production" or not).',
43+
enableParallelism: true,
44+
ignoreMissingScript: false,
45+
ignoreDependencyOrder: false,
46+
incremental: true,
47+
allowWarningsInSuccessfulBuild: false,
48+
safeForSimultaneousRushProcesses: false
49+
};
50+
51+
public static readonly defaultRebuildCommandJson: CommandJson = {
52+
commandKind: RushConstants.bulkCommandKind,
53+
name: RushConstants.rebuildCommandName,
54+
summary: 'Clean and rebuild the entire set of projects',
55+
description: 'This command assumes that the package.json file for each project contains'
56+
+ ' a "scripts" entry for "npm run build" that performs a full clean build.'
57+
+ ' Rush invokes this script to build each project that is registered in rush.json.'
58+
+ ' Projects are built in parallel where possible, but always respecting the dependency'
59+
+ ' graph for locally linked projects. The number of simultaneous processes will be'
60+
+ ' based on the number of machine cores unless overridden by the --parallelism flag.'
61+
+ ' (For an incremental build, see "rush build" instead of "rush rebuild".)',
62+
enableParallelism: true,
63+
ignoreMissingScript: false,
64+
ignoreDependencyOrder: false,
65+
incremental: false,
66+
allowWarningsInSuccessfulBuild: false,
67+
safeForSimultaneousRushProcesses: false
68+
};
69+
3070
/**
3171
* Use CommandLineConfiguration.loadFromFile()
3272
*/
@@ -56,19 +96,53 @@ export class CommandLineConfiguration {
5696
}
5797
}
5898
}
59-
6099
}
61100
}
62101

63102
/**
64-
* Loads the configuration from the specified file. If the file does not exist,
65-
* then an empty default instance is returned. If the file contains errors, then
66-
* an exception is thrown.
103+
* Loads the configuration from the specified file and applies any omitted default build
104+
* settings. If the file does not exist, then an empty default instance is returned.
105+
* If the file contains errors, then an exception is thrown.
67106
*/
68107
public static loadFromFileOrDefault(jsonFilename: string): CommandLineConfiguration {
69108
let commandLineJson: ICommandLineJson | undefined = undefined;
70109
if (FileSystem.exists(jsonFilename)) {
71-
commandLineJson = JsonFile.loadAndValidate(jsonFilename, CommandLineConfiguration._jsonSchema);
110+
commandLineJson = JsonFile.load(jsonFilename);
111+
112+
// merge commands specified in command-line.json and default (re)build settings
113+
// Ensure both build commands are included and preserve any other commands specified
114+
if (commandLineJson && commandLineJson.commands) {
115+
for (let i: number = 0; i < commandLineJson.commands.length; i++) {
116+
const command: CommandJson = commandLineJson.commands[i];
117+
118+
// Determine if we have a set of default parameters
119+
let commandDefaultDefinition: CommandJson | {} = {};
120+
switch (command.commandKind) {
121+
case RushConstants.bulkCommandKind: {
122+
switch (command.name) {
123+
case RushConstants.buildCommandName: {
124+
commandDefaultDefinition = CommandLineConfiguration.defaultBuildCommandJson
125+
break;
126+
}
127+
128+
case RushConstants.rebuildCommandName: {
129+
commandDefaultDefinition = CommandLineConfiguration.defaultRebuildCommandJson;
130+
break;
131+
}
132+
}
133+
break;
134+
}
135+
}
136+
137+
// Merge the default parameters into the repo-specified parameters
138+
commandLineJson.commands[i] = {
139+
...commandDefaultDefinition,
140+
...command
141+
}
142+
}
143+
144+
CommandLineConfiguration._jsonSchema.validateObject(commandLineJson, jsonFilename);
145+
}
72146
}
73147

74148
return new CommandLineConfiguration(commandLineJson);

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

Lines changed: 62 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -195,53 +195,12 @@ export class RushCommandLineParser extends CommandLineParser {
195195
}
196196

197197
private _addDefaultBuildActions(commandLineConfiguration?: CommandLineConfiguration): void {
198-
if (!this.tryGetAction('build')) {
199-
// always create a build and a rebuild command
200-
this.addAction(new BulkScriptAction({
201-
actionName: 'build',
202-
summary: '(EXPERIMENTAL) Build all projects that haven\'t been built, or have changed since they were last '
203-
+ 'built.',
204-
documentation: 'This command is similar to "rush rebuild", except that "rush build" performs'
205-
+ ' an incremental build. In other words, it only builds projects whose source files have changed'
206-
+ ' since the last successful build. The analysis requires a Git working tree, and only considers'
207-
+ ' source files that are tracked by Git and whose path is under the project folder. (For more details'
208-
+ ' about this algorithm, see the documentation for the "package-deps-hash" NPM package.) The incremental'
209-
+ ' build state is tracked in a per-project folder called ".rush/temp" which should NOT be added to Git. The'
210-
+ ' build command is tracked by the "arguments" field in the "package-deps_build.json" file contained'
211-
+ ' therein; a full rebuild is forced whenever the command has changed (e.g. "--production" or not).',
212-
parser: this,
213-
commandLineConfiguration: commandLineConfiguration,
214-
215-
enableParallelism: true,
216-
ignoreMissingScript: false,
217-
ignoreDependencyOrder: false,
218-
incremental: true,
219-
allowWarningsInSuccessfulBuild: false
220-
}));
198+
if (!this.tryGetAction(RushConstants.buildCommandName)) {
199+
this._addCommandLineConfigAction(commandLineConfiguration, CommandLineConfiguration.defaultBuildCommandJson);
221200
}
222201

223-
if (!this.tryGetAction('rebuild')) {
224-
this.addAction(new BulkScriptAction({
225-
actionName: 'rebuild',
226-
// To remain compatible with existing repos, `rebuild` defaults to calling the `build` command in each repo.
227-
commandToRun: 'build',
228-
summary: 'Clean and rebuild the entire set of projects',
229-
documentation: 'This command assumes that the package.json file for each project contains'
230-
+ ' a "scripts" entry for "npm run build" that performs a full clean build.'
231-
+ ' Rush invokes this script to build each project that is registered in rush.json.'
232-
+ ' Projects are built in parallel where possible, but always respecting the dependency'
233-
+ ' graph for locally linked projects. The number of simultaneous processes will be'
234-
+ ' based on the number of machine cores unless overridden by the --parallelism flag.'
235-
+ ' (For an incremental build, see "rush build" instead of "rush rebuild".)',
236-
parser: this,
237-
commandLineConfiguration: commandLineConfiguration,
238-
239-
enableParallelism: true,
240-
ignoreMissingScript: false,
241-
ignoreDependencyOrder: false,
242-
incremental: false,
243-
allowWarningsInSuccessfulBuild: false
244-
}));
202+
if (!this.tryGetAction(RushConstants.rebuildCommandName)) {
203+
this._addCommandLineConfigAction(commandLineConfiguration, CommandLineConfiguration.defaultRebuildCommandJson);
245204
}
246205
}
247206

@@ -252,48 +211,61 @@ export class RushCommandLineParser extends CommandLineParser {
252211

253212
// Register each custom command
254213
for (const command of commandLineConfiguration.commands) {
255-
if (this.tryGetAction(command.name)) {
256-
throw new Error(`${RushConstants.commandLineFilename} defines a command "${command.name}"`
257-
+ ` using a name that already exists`);
258-
}
214+
this._addCommandLineConfigAction(commandLineConfiguration, command);
215+
}
216+
}
259217

260-
this._validateCommandLineConfigCommand(command);
261-
262-
switch (command.commandKind) {
263-
case 'bulk':
264-
this.addAction(new BulkScriptAction({
265-
actionName: command.name,
266-
summary: command.summary,
267-
documentation: command.description || command.summary,
268-
safeForSimultaneousRushProcesses: command.safeForSimultaneousRushProcesses,
269-
270-
parser: this,
271-
commandLineConfiguration: commandLineConfiguration,
272-
273-
enableParallelism: command.enableParallelism,
274-
ignoreMissingScript: command.ignoreMissingScript || false,
275-
ignoreDependencyOrder: command.ignoreDependencyOrder || false,
276-
incremental: command.incremental || false,
277-
allowWarningsInSuccessfulBuild: !!command.allowWarningsInSuccessfulBuild
278-
}));
279-
break;
280-
case 'global':
281-
this.addAction(new GlobalScriptAction({
282-
actionName: command.name,
283-
summary: command.summary,
284-
documentation: command.description || command.summary,
285-
safeForSimultaneousRushProcesses: command.safeForSimultaneousRushProcesses,
286-
287-
parser: this,
288-
commandLineConfiguration: commandLineConfiguration,
289-
290-
shellCommand: command.shellCommand
291-
}));
292-
break;
293-
default:
294-
throw new Error(`${RushConstants.commandLineFilename} defines a command "${command!.name}"`
295-
+ ` using an unsupported command kind "${command!.commandKind}"`);
296-
}
218+
private _addCommandLineConfigAction(
219+
commandLineConfiguration: CommandLineConfiguration | undefined,
220+
command: CommandJson
221+
): void {
222+
if (this.tryGetAction(command.name)) {
223+
throw new Error(`${RushConstants.commandLineFilename} defines a command "${command.name}"`
224+
+ ` using a name that already exists`);
225+
}
226+
227+
this._validateCommandLineConfigCommand(command);
228+
229+
switch (command.commandKind) {
230+
case RushConstants.bulkCommandKind:
231+
this.addAction(new BulkScriptAction({
232+
actionName: command.name,
233+
234+
// The rush rebuild and rush build command invoke the same NPM script because they share the same
235+
// package-deps-hash state.
236+
commandToRun: command.name === RushConstants.rebuildCommandName ? 'build' : undefined,
237+
238+
summary: command.summary,
239+
documentation: command.description || command.summary,
240+
safeForSimultaneousRushProcesses: command.safeForSimultaneousRushProcesses,
241+
242+
parser: this,
243+
commandLineConfiguration: commandLineConfiguration,
244+
245+
enableParallelism: command.enableParallelism,
246+
ignoreMissingScript: command.ignoreMissingScript || false,
247+
ignoreDependencyOrder: command.ignoreDependencyOrder || false,
248+
incremental: command.incremental || false,
249+
allowWarningsInSuccessfulBuild: !!command.allowWarningsInSuccessfulBuild
250+
}));
251+
break;
252+
253+
case RushConstants.globalCommandKind:
254+
this.addAction(new GlobalScriptAction({
255+
actionName: command.name,
256+
summary: command.summary,
257+
documentation: command.description || command.summary,
258+
safeForSimultaneousRushProcesses: command.safeForSimultaneousRushProcesses,
259+
260+
parser: this,
261+
commandLineConfiguration: commandLineConfiguration,
262+
263+
shellCommand: command.shellCommand
264+
}));
265+
break;
266+
default:
267+
throw new Error(`${RushConstants.commandLineFilename} defines a command "${command!.name}"`
268+
+ ` using an unsupported command kind "${command!.commandKind}"`);
297269
}
298270
}
299271

@@ -321,13 +293,14 @@ export class RushCommandLineParser extends CommandLineParser {
321293

322294
private _validateCommandLineConfigCommand(command: CommandJson): void {
323295
// There are some restrictions on the 'build' and 'rebuild' commands.
324-
if (command.name !== 'build' && command.name !== 'rebuild') {
296+
if (command.name !== RushConstants.buildCommandName && command.name !== RushConstants.rebuildCommandName) {
325297
return;
326298
}
327299

328-
if (command.commandKind === 'global') {
300+
if (command.commandKind === RushConstants.globalCommandKind) {
329301
throw new Error(`${RushConstants.commandLineFilename} defines a command "${command.name}" using ` +
330-
`the command kind "global". This command can only be designated as a command kind "bulk".`);
302+
`the command kind "${RushConstants.globalCommandKind}". This command can only be designated as a command ` +
303+
`kind "${RushConstants.bulkCommandKind}".`);
331304
}
332305
if (command.safeForSimultaneousRushProcesses) {
333306
throw new Error(`${RushConstants.commandLineFilename} defines a command "${command.name}" using ` +

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

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ describe('RushCommandLineParser', () => {
199199
});
200200

201201
describe(`'rebuild' action`, () => {
202-
it(`executes the package's 'rebuild' (not 'build') script`, () => {
202+
it(`executes the package's 'build' script`, () => {
203203
const repoName: string = 'overrideRebuildAndRunRebuildActionRepo';
204204
const instance: IParserTestInstance = getCommandLineParserInstance(repoName, 'rebuild');
205205

@@ -211,7 +211,75 @@ describe('RushCommandLineParser', () => {
211211
expect(packageCount).toEqual(2);
212212

213213
// Use regex for task name in case spaces were prepended or appended to spawned command
214-
const expectedBuildTaskRegexp: RegExp = /fake_REbuild_task_but_works_with_mock/;
214+
const expectedBuildTaskRegexp: RegExp = /fake_build_task_but_works_with_mock/;
215+
216+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
217+
const firstSpawn: any[] = instance.spawnMock.mock.calls[0];
218+
expect(firstSpawn[SPAWN_ARG_ARGS]).toEqual(expect.arrayContaining([
219+
expect.stringMatching(expectedBuildTaskRegexp)
220+
]));
221+
expect(firstSpawn[SPAWN_ARG_OPTIONS]).toEqual(expect.any(Object));
222+
expect(firstSpawn[SPAWN_ARG_OPTIONS].cwd).toEqual(path.resolve(__dirname, `${repoName}/a`));
223+
224+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
225+
const secondSpawn: any[] = instance.spawnMock.mock.calls[1];
226+
expect(secondSpawn[SPAWN_ARG_ARGS]).toEqual(expect.arrayContaining([
227+
expect.stringMatching(expectedBuildTaskRegexp)
228+
]));
229+
expect(secondSpawn[SPAWN_ARG_OPTIONS]).toEqual(expect.any(Object));
230+
expect(secondSpawn[SPAWN_ARG_OPTIONS].cwd).toEqual(path.resolve(__dirname, `${repoName}/b`));
231+
});
232+
});
233+
});
234+
});
235+
236+
describe(`in repo with 'rebuild' or 'build' partially set`, () => {
237+
describe(`'build' action`, () => {
238+
it(`executes the package's 'build' script`, () => {
239+
const repoName: string = 'overrideAndDefaultBuildActionRepo';
240+
const instance: IParserTestInstance = getCommandLineParserInstance(repoName, 'build');
241+
expect.assertions(8);
242+
return expect(instance.parser.execute()).resolves.toEqual(true)
243+
.then(() => {
244+
// There should be 1 build per package
245+
const packageCount: number = instance.spawnMock.mock.calls.length;
246+
expect(packageCount).toEqual(2);
247+
248+
// Use regex for task name in case spaces were prepended or appended to spawned command
249+
const expectedBuildTaskRegexp: RegExp = /fake_build_task_but_works_with_mock/;
250+
251+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
252+
const firstSpawn: any[] = instance.spawnMock.mock.calls[0];
253+
expect(firstSpawn[SPAWN_ARG_ARGS]).toEqual(expect.arrayContaining([
254+
expect.stringMatching(expectedBuildTaskRegexp)
255+
]));
256+
expect(firstSpawn[SPAWN_ARG_OPTIONS]).toEqual(expect.any(Object));
257+
expect(firstSpawn[SPAWN_ARG_OPTIONS].cwd).toEqual(path.resolve(__dirname, `${repoName}/a`));
258+
259+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
260+
const secondSpawn: any[] = instance.spawnMock.mock.calls[1];
261+
expect(secondSpawn[SPAWN_ARG_ARGS]).toEqual(expect.arrayContaining([
262+
expect.stringMatching(expectedBuildTaskRegexp)
263+
]));
264+
expect(secondSpawn[SPAWN_ARG_OPTIONS]).toEqual(expect.any(Object));
265+
expect(secondSpawn[SPAWN_ARG_OPTIONS].cwd).toEqual(path.resolve(__dirname, `${repoName}/b`));
266+
});
267+
});
268+
});
269+
270+
describe(`'rebuild' action`, () => {
271+
it(`executes the package's 'build' script`, () => {
272+
const repoName: string = 'overrideAndDefaultRebuildActionRepo';
273+
const instance: IParserTestInstance = getCommandLineParserInstance(repoName, 'rebuild');
274+
expect.assertions(8);
275+
return expect(instance.parser.execute()).resolves.toEqual(true)
276+
.then(() => {
277+
// There should be 1 build per package
278+
const packageCount: number = instance.spawnMock.mock.calls.length;
279+
expect(packageCount).toEqual(2);
280+
281+
// Use regex for task name in case spaces were prepended or appended to spawned command
282+
const expectedBuildTaskRegexp: RegExp = /fake_build_task_but_works_with_mock/;
215283

216284
// eslint-disable-next-line @typescript-eslint/no-explicit-any
217285
const firstSpawn: any[] = instance.spawnMock.mock.calls[0];

apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ Positional arguments:
4040
import-strings
4141
Imports translated strings into each project.
4242
deploy Deploys the build
43-
build (EXPERIMENTAL) Build all projects that haven't been built,
44-
or have changed since they were last built.
43+
build Build all projects that haven't been built, or have changed
44+
since they were last built.
4545
rebuild Clean and rebuild the entire set of projects
4646
4747
Optional arguments:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "a",
3+
"version": "1.0.0",
4+
"description": "Test package a",
5+
"scripts": {
6+
"build": "fake_build_task_but_works_with_mock",
7+
"rebuild": "fake_REbuild_task_but_works_with_mock"
8+
}
9+
}

0 commit comments

Comments
 (0)