forked from microsoft/rushstack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCommandLineConfiguration.ts
More file actions
779 lines (688 loc) · 30.2 KB
/
CommandLineConfiguration.ts
File metadata and controls
779 lines (688 loc) · 30.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { JsonFile, JsonSchema, FileSystem } from '@rushstack/node-core-library';
import type { CommandLineParameter } from '@rushstack/ts-command-line';
import { RushConstants } from '../logic/RushConstants';
import type {
CommandJson,
ICommandLineJson,
IBulkCommandJson,
IGlobalCommandJson,
IFlagParameterJson,
IChoiceParameterJson,
IStringParameterJson,
IIntegerParameterJson,
IStringListParameterJson,
IIntegerListParameterJson,
IChoiceListParameterJson,
IPhasedCommandWithoutPhasesJson
} from './CommandLineJson';
import schemaJson from '../schemas/command-line.schema.json';
export interface IShellCommandTokenContext {
packageFolder: string;
}
/**
* The set of valid behaviors for a missing script in a project's package.json scripts for a given phase.
* @alpha
*/
export type PhaseBehaviorForMissingScript = 'silent' | 'log' | 'error';
/**
* Metadata about a phase.
* @alpha
*/
export interface IPhase {
/**
* The name of this phase.
*/
name: string;
/**
* If set to `true,` this this phase was generated from a bulk command, and
* was not explicitly defined in the command-line.json file.
*/
isSynthetic: boolean;
/**
* This property is used in the name of the filename for the logs generated by this
* phase. This is a filesystem-safe version of the phase name. For example,
* a phase with name `_phase:compile` has a `logFilenameIdentifier` of `_phase_compile`.
*/
logFilenameIdentifier: string;
/**
* The set of custom command line parameters that are relevant to this phase.
*/
associatedParameters: Set<CommandLineParameter>;
/**
* The resolved dependencies of the phase
*/
dependencies: {
self: Set<IPhase>;
upstream: Set<IPhase>;
};
/**
* By default, Rush returns a nonzero exit code if errors or warnings occur during a command. If this option is
* set to `true`, Rush will return a zero exit code if warnings occur during the execution of this phase.
*/
allowWarningsOnSuccess: boolean;
/**
* What should happen if the script is not defined in a project's package.json scripts field. Default is "error".
*/
missingScriptBehavior: PhaseBehaviorForMissingScript;
/**
* (Optional) If the `shellCommand` field is set for a bulk command, Rush will invoke it for each
* selected project; otherwise, Rush will invoke the package.json `"scripts"` entry matching Rush command/phase name.
*
* This string is the path to a script that will be invoked using the OS shell. The working directory will be
* the folder that contains rush.json. If custom parameters are associated with this command, their
* values will be appended to the end of this string.
*/
shellCommand?: string;
}
export interface ICommandWithParameters {
associatedParameters: Set<IParameterJson>;
}
export interface IPhasedCommandConfig extends IPhasedCommandWithoutPhasesJson, ICommandWithParameters {
/**
* If set to `true`, then this phased command was generated from a bulk command, and
* was not explicitly defined in the command-line.json file.
*/
isSynthetic: boolean;
disableBuildCache?: boolean;
originalPhases: Set<IPhase>;
/**
* Include upstream and self phases.
*/
phases: Set<IPhase>;
/**
* If set to `true`, this phased command will always run in watch mode, regardless of CLI flags.
*/
alwaysWatch: boolean;
/**
* The set of phases to execute when running this phased command in watch mode.
*/
watchPhases: Set<IPhase>;
/**
* How many milliseconds to wait after receiving a file system notification before executing in watch mode.
*/
watchDebounceMs?: number;
/**
* If set to `true`, then this phased command will always perform an install before executing, regardless of CLI flags.
* If set to `false`, then Rush will define a built-in "--install" CLI flag for this command.
* If undefined, then Rush does not define a built-in "--install" CLI flag for this command and no installation is performed.
*/
alwaysInstall: boolean | undefined;
}
export interface IGlobalCommandConfig extends IGlobalCommandJson, ICommandWithParameters {
/**
* 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.
*/
providedByPlugin: boolean;
}
export type Command = IGlobalCommandConfig | IPhasedCommandConfig;
/**
* Metadata about a custom parameter defined in command-line.json
* @alpha
*/
export type IParameterJson =
| IFlagParameterJson
| IChoiceParameterJson
| IStringParameterJson
| IIntegerParameterJson
| IStringListParameterJson
| IIntegerListParameterJson
| IChoiceListParameterJson;
const DEFAULT_BUILD_COMMAND_JSON: IBulkCommandJson = {
commandKind: RushConstants.bulkCommandKind,
name: RushConstants.buildCommandName,
summary: "Build all projects that haven't been built, or have changed since they were last built.",
description:
'This command is similar to "rush rebuild", except that "rush build" performs' +
' an incremental build. In other words, it only builds projects whose source files have changed' +
' since the last successful build. The analysis requires a Git working tree, and only considers' +
' source files that are tracked by Git and whose path is under the project folder. (For more details' +
' about this algorithm, see the documentation for the "package-deps-hash" NPM package.) The incremental' +
' build state is tracked in a per-project folder called ".rush/temp" which should NOT be added to Git. The' +
' build command is tracked by the "arguments" field in the "package-deps_build.json" file contained' +
' therein; a full rebuild is forced whenever the command has changed (e.g. "--production" or not).',
safeForSimultaneousRushProcesses: false,
enableParallelism: true,
incremental: true
};
const DEFAULT_REBUILD_COMMAND_JSON: IBulkCommandJson = {
commandKind: RushConstants.bulkCommandKind,
name: RushConstants.rebuildCommandName,
summary: 'Clean and rebuild the entire set of projects.',
description:
'This command assumes that the package.json file for each project contains' +
' a "scripts" entry for "npm run build" that performs a full clean build.' +
` Rush invokes this script to build each project that is registered in ${RushConstants.rushJsonFilename}.` +
' Projects are built in parallel where possible, but always respecting the dependency' +
' graph for locally linked projects. The number of simultaneous processes will be' +
' based on the number of machine cores unless overridden by the --parallelism flag.' +
' (For an incremental build, see "rush build" instead of "rush rebuild".)',
safeForSimultaneousRushProcesses: false,
enableParallelism: true,
incremental: false
};
interface ICommandLineConfigurationOptions {
/**
* If true, do not include default build and rebuild commands.
*/
doNotIncludeDefaultBuildCommands?: boolean;
}
/**
* This function replaces colons (":") with underscores ("_").
*
* ts-command-line restricts command names to lowercase letters, numbers, underscores, and colons.
* Replacing colons with underscores produces a filesystem-safe name.
*/
function _normalizeNameForLogFilenameIdentifiers(name: string): string {
return name.replace(/:/g, '_'); // Replace colons with underscores to be filesystem-safe
}
/**
* Custom Commands and Options for the Rush Command Line
*/
export class CommandLineConfiguration {
private static _jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson);
public readonly commands: Map<string, Command> = new Map();
public readonly phases: Map<string, IPhase> = new Map();
public readonly parameters: IParameterJson[] = [];
/**
* shellCommand from plugin custom command line configuration needs to be expanded with tokens
*/
public shellCommandTokenContext: IShellCommandTokenContext | undefined;
/**
* These path will be prepended to the PATH environment variable
*/
public readonly additionalPathFolders: Readonly<string[]> = [];
/**
* A map of bulk command names to their corresponding synthetic phase identifiers
*/
private readonly _syntheticPhasesByTranslatedBulkCommandName: Map<string, IPhase> = new Map();
/**
* Use CommandLineConfiguration.loadFromFile()
*
* @internal
*/
public constructor(
commandLineJson: ICommandLineJson | undefined,
options: ICommandLineConfigurationOptions = {}
) {
const phasesJson: ICommandLineJson['phases'] = commandLineJson?.phases;
if (phasesJson) {
const phaseNameRegexp: RegExp = new RegExp(
`^${RushConstants.phaseNamePrefix}[a-z][a-z0-9]*([-][a-z0-9]+)*$`
);
for (const phase of phasesJson) {
if (this.phases.has(phase.name)) {
throw new Error(
`In ${RushConstants.commandLineFilename}, the phase "${phase.name}" is specified ` +
'more than once.'
);
}
if (!phase.name.match(phaseNameRegexp)) {
throw new Error(
`In ${RushConstants.commandLineFilename}, the phase "${phase.name}"'s name ` +
'is not a valid phase name. Phase names must begin with the ' +
`required prefix "${RushConstants.phaseNamePrefix}" followed by a name containing ` +
'lowercase letters, numbers, or hyphens. The name must start with a letter and ' +
'must not end with a hyphen.'
);
}
if (phase.ignoreMissingScript !== undefined && phase.missingScriptBehavior !== undefined) {
throw new Error(
`In ${RushConstants.commandLineFilename}, the phase "${phase.name}"'s defines ` +
'both "ignoreMissingScript" and "missingScriptBehavior". If using the "missingScriptBehavior", ' +
`remove "ignoreMissingScript", since it subsumes the functionality.`
);
}
// This is a completely fresh object. Avoid use of the `...` operator in its construction
// to guarantee monomorphism.
const processedPhase: IPhase = {
name: phase.name,
isSynthetic: false,
logFilenameIdentifier: _normalizeNameForLogFilenameIdentifiers(phase.name),
associatedParameters: new Set(),
dependencies: {
self: new Set(),
upstream: new Set()
},
missingScriptBehavior: phase.missingScriptBehavior ?? (phase.ignoreMissingScript ? 'log' : 'error'),
allowWarningsOnSuccess: !!phase.allowWarningsOnSuccess
};
this.phases.set(phase.name, processedPhase);
}
// Resolve phase names to the underlying objects
for (const rawPhase of phasesJson) {
// The named phase not existing was already handled in the loop above
const phase: IPhase = this.phases.get(rawPhase.name)!;
const selfDependencies: string[] | undefined = rawPhase.dependencies?.self;
const upstreamDependencies: string[] | undefined = rawPhase.dependencies?.upstream;
if (selfDependencies) {
for (const dependencyName of selfDependencies) {
const dependency: IPhase | undefined = this.phases.get(dependencyName);
if (!dependency) {
throw new Error(
`In ${RushConstants.commandLineFilename}, in the phase "${phase.name}", the self ` +
`dependency phase "${dependencyName}" does not exist.`
);
}
phase.dependencies.self.add(dependency);
}
}
if (upstreamDependencies) {
for (const dependencyName of upstreamDependencies) {
const dependency: IPhase | undefined = this.phases.get(dependencyName);
if (!dependency) {
throw new Error(
`In ${RushConstants.commandLineFilename}, in the phase "${phase.name}", ` +
`the upstream dependency phase "${dependencyName}" does not exist.`
);
}
phase.dependencies.upstream.add(dependency);
}
}
}
// Do the recursive stuff after the dependencies have been converted
const safePhases: Set<IPhase> = new Set();
const cycleDetector: Set<IPhase> = new Set();
for (const phase of this.phases.values()) {
this._checkForPhaseSelfCycles(phase, cycleDetector, safePhases);
}
}
const commandsJson: ICommandLineJson['commands'] = commandLineJson?.commands;
let buildCommandPhases: IPhasedCommandConfig['phases'] | undefined;
let buildCommandOriginalPhases: IPhasedCommandConfig['phases'] | undefined;
if (commandsJson) {
for (const command of commandsJson) {
if (this.commands.has(command.name)) {
throw new Error(
`In ${RushConstants.commandLineFilename}, the command "${command.name}" is specified ` +
'more than once.'
);
}
let normalizedCommand: Command;
switch (command.commandKind) {
case RushConstants.phasedCommandKind: {
const originalPhases: Set<IPhase> = new Set();
const commandPhases: Set<IPhase> = new Set();
const watchPhases: Set<IPhase> = new Set();
normalizedCommand = {
...command,
isSynthetic: false,
associatedParameters: new Set<IParameterJson>(),
originalPhases,
phases: commandPhases,
watchPhases,
alwaysWatch: false,
alwaysInstall: undefined
};
for (const phaseName of command.phases) {
const phase: IPhase | undefined = this.phases.get(phaseName);
if (!phase) {
throw new Error(
`In ${RushConstants.commandLineFilename}, in the "phases" property of the ` +
`"${normalizedCommand.name}" command, the phase "${phaseName}" does not exist.`
);
}
originalPhases.add(phase);
commandPhases.add(phase);
}
// Apply implicit phase dependency expansion
// The equivalent of the "--to" operator used for projects
// Appending to the set while iterating it accomplishes a full breadth-first search
for (const phase of commandPhases) {
for (const dependency of phase.dependencies.self) {
commandPhases.add(dependency);
}
for (const dependency of phase.dependencies.upstream) {
commandPhases.add(dependency);
}
}
const { watchOptions, installOptions } = command;
if (watchOptions) {
normalizedCommand.alwaysWatch = watchOptions.alwaysWatch;
normalizedCommand.watchDebounceMs = watchOptions.debounceMs;
// No implicit phase dependency expansion for watch mode.
for (const phaseName of watchOptions.watchPhases) {
const phase: IPhase | undefined = this.phases.get(phaseName);
if (!phase) {
throw new Error(
`In ${RushConstants.commandLineFilename}, in the "watchPhases" property of the ` +
`"${normalizedCommand.name}" command, the phase "${phaseName}" does not exist.`
);
}
watchPhases.add(phase);
}
}
if (installOptions) {
normalizedCommand.alwaysInstall = installOptions.alwaysInstall;
}
break;
}
case RushConstants.globalCommandKind: {
normalizedCommand = {
...command,
providedByPlugin: false,
associatedParameters: new Set<IParameterJson>()
};
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: '',
providedByPlugin: true,
associatedParameters: new Set<IParameterJson>()
};
break;
}
case RushConstants.bulkCommandKind: {
// Translate the bulk command into a phased command
normalizedCommand = this._translateBulkCommandToPhasedCommand(command);
break;
}
}
if (
normalizedCommand.name === RushConstants.buildCommandName ||
normalizedCommand.name === RushConstants.rebuildCommandName
) {
if (normalizedCommand.commandKind === RushConstants.globalCommandKind) {
throw new Error(
`${RushConstants.commandLineFilename} defines a command "${normalizedCommand.name}" using ` +
`the command kind "${RushConstants.globalCommandKind}". This command can only be designated as a command ` +
`kind "${RushConstants.bulkCommandKind}" or "${RushConstants.phasedCommandKind}".`
);
} else if (command.safeForSimultaneousRushProcesses) {
throw new Error(
`${RushConstants.commandLineFilename} defines a command "${normalizedCommand.name}" using ` +
`"safeForSimultaneousRushProcesses=true". This configuration is not supported for "${normalizedCommand.name}".`
);
} else if (normalizedCommand.name === RushConstants.buildCommandName) {
// Record the build command phases in case we need to construct a synthetic "rebuild" command
buildCommandPhases = normalizedCommand.phases;
buildCommandOriginalPhases = normalizedCommand.originalPhases;
}
}
this.commands.set(normalizedCommand.name, normalizedCommand);
}
}
if (!options.doNotIncludeDefaultBuildCommands) {
let buildCommand: Command | undefined = this.commands.get(RushConstants.buildCommandName);
if (!buildCommand) {
// If the build command was not specified in the config file, add the default build command
buildCommand = this._translateBulkCommandToPhasedCommand(DEFAULT_BUILD_COMMAND_JSON);
buildCommand.disableBuildCache = DEFAULT_BUILD_COMMAND_JSON.disableBuildCache;
buildCommandPhases = buildCommand.phases;
buildCommandOriginalPhases = buildCommand.originalPhases;
this.commands.set(buildCommand.name, buildCommand);
}
}
const buildCommand: Command | undefined = this.commands.get(RushConstants.buildCommandName);
if (buildCommand && !this.commands.has(RushConstants.rebuildCommandName)) {
// If a rebuild command was not specified in the config file, add the default rebuild command
if (!buildCommandPhases || !buildCommandOriginalPhases) {
throw new Error(`Phases for the "${RushConstants.buildCommandName}" were not found.`);
}
const rebuildCommand: IPhasedCommandConfig = {
...DEFAULT_REBUILD_COMMAND_JSON,
commandKind: RushConstants.phasedCommandKind,
isSynthetic: true,
phases: buildCommandPhases,
disableBuildCache: DEFAULT_REBUILD_COMMAND_JSON.disableBuildCache,
associatedParameters: buildCommand.associatedParameters, // rebuild should share build's parameters in this case,
originalPhases: buildCommandOriginalPhases,
watchPhases: new Set(),
alwaysWatch: false,
alwaysInstall: undefined
};
this.commands.set(rebuildCommand.name, rebuildCommand);
}
const parametersJson: ICommandLineJson['parameters'] = commandLineJson?.parameters;
if (parametersJson) {
for (const parameter of parametersJson) {
const normalizedParameter: IParameterJson = {
...parameter,
associatedPhases: parameter.associatedPhases ? [...parameter.associatedPhases] : [],
associatedCommands: parameter.associatedCommands ? [...parameter.associatedCommands] : []
};
this.parameters.push(normalizedParameter);
// Do some basic validation
switch (normalizedParameter.parameterKind) {
case 'choice': {
const alternativeNames: string[] = normalizedParameter.alternatives.map((x) => x.name);
if (
normalizedParameter.defaultValue &&
alternativeNames.indexOf(normalizedParameter.defaultValue) < 0
) {
throw new Error(
`In ${RushConstants.commandLineFilename}, the parameter "${normalizedParameter.longName}",` +
` specifies a default value "${normalizedParameter.defaultValue}"` +
` which is not one of the defined alternatives: "${alternativeNames.toString()}"`
);
}
break;
}
}
let parameterHasAssociatedCommands: boolean = false;
if (normalizedParameter.associatedCommands) {
for (const associatedCommandName of normalizedParameter.associatedCommands) {
const syntheticPhase: IPhase | undefined =
this._syntheticPhasesByTranslatedBulkCommandName.get(associatedCommandName);
if (syntheticPhase) {
// If this parameter was associated with a bulk command, include the association
// with the synthetic phase
normalizedParameter.associatedPhases!.push(syntheticPhase.name);
}
const associatedCommand: Command | undefined = this.commands.get(associatedCommandName);
if (!associatedCommand) {
throw new Error(
`${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` +
`that is associated with a command "${associatedCommandName}" that is not defined in ` +
'this file.'
);
} else {
associatedCommand.associatedParameters.add(normalizedParameter);
parameterHasAssociatedCommands = true;
}
}
}
if (normalizedParameter.associatedPhases) {
for (const associatedPhaseName of normalizedParameter.associatedPhases) {
const associatedPhase: IPhase | undefined = this.phases.get(associatedPhaseName);
if (!associatedPhase) {
throw new Error(
`${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` +
`that is associated with a phase "${associatedPhaseName}" that does not exist.`
);
}
}
}
if (!parameterHasAssociatedCommands) {
throw new Error(
`${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}"` +
` that lists no associated commands.`
);
}
// In the presence of plugins, there is utility to defining parameters that are associated with a phased
// command but no phases. Don't enforce that a parameter is associated with at least one phase.
}
}
}
/**
* Performs a depth-first search to detect cycles in the directed graph of phase "self" dependencies.
*
* @param phase The phase node currently being checked
* @param phasesInPath The current path from the start node to `phase`
* @param cycleFreePhases Phases that have already been fully walked and confirmed to not be in any cycles
*/
private _checkForPhaseSelfCycles(
phase: IPhase,
phasesInPath: Set<IPhase>,
cycleFreePhases: Set<IPhase>
): void {
if (cycleFreePhases.has(phase)) {
// phase is known to not be reachable from itself, i.e. not in a cycle. Skip.
return;
}
for (const dependency of phase.dependencies.self) {
if (phasesInPath.has(dependency)) {
throw new Error(
`In ${RushConstants.commandLineFilename}, there exists a cycle within the ` +
`set of ${dependency.name} dependencies: ${Array.from(
phasesInPath,
(phaseInPath: IPhase) => phaseInPath.name
).join(', ')}`
);
} else {
phasesInPath.add(dependency);
this._checkForPhaseSelfCycles(dependency, phasesInPath, cycleFreePhases);
phasesInPath.delete(dependency);
}
}
// phase is not reachable from itself, mark for skipping
cycleFreePhases.add(phase);
}
private static _applyBuildCommandDefaults(commandLineJson: ICommandLineJson): void {
// merge commands specified in command-line.json and default (re)build settings
// Ensure both build commands are included and preserve any other commands specified
if (commandLineJson?.commands) {
for (let i: number = 0; i < commandLineJson.commands.length; i++) {
const command: CommandJson = commandLineJson.commands[i];
// Determine if we have a set of default parameters
let commandDefaultDefinition: CommandJson | {} = {};
switch (command.commandKind) {
case RushConstants.phasedCommandKind:
case RushConstants.bulkCommandKind: {
switch (command.name) {
case RushConstants.buildCommandName: {
commandDefaultDefinition = DEFAULT_BUILD_COMMAND_JSON;
break;
}
case RushConstants.rebuildCommandName: {
commandDefaultDefinition = DEFAULT_REBUILD_COMMAND_JSON;
break;
}
}
break;
}
}
// Merge the default parameters into the repo-specified parameters
commandLineJson.commands[i] = {
...commandDefaultDefinition,
...command
};
}
}
}
/**
* Load the command-line.json configuration file from the specified path. Note that this
* does not include the default build settings. This option is intended to be used to load
* command-line.json files from plugins. To load a common/config/rush/command-line.json file,
* use {@see loadFromFileOrDefault} instead.
*
* If the file does not exist, this function returns `undefined`
*/
public static tryLoadFromFile(jsonFilePath: string): CommandLineConfiguration | undefined {
let commandLineJson: ICommandLineJson | undefined;
try {
commandLineJson = JsonFile.loadAndValidate(jsonFilePath, CommandLineConfiguration._jsonSchema);
} catch (e) {
if (!FileSystem.isNotExistError(e as Error)) {
throw e;
}
}
if (commandLineJson) {
this._applyBuildCommandDefaults(commandLineJson);
const hasBuildCommand: boolean = !!commandLineJson.commands?.some(
(command) => command.name === RushConstants.buildCommandName
);
const hasRebuildCommand: boolean = !!commandLineJson.commands?.some(
(command) => command.name === RushConstants.rebuildCommandName
);
return new CommandLineConfiguration(commandLineJson, {
doNotIncludeDefaultBuildCommands: !(hasBuildCommand || hasRebuildCommand)
});
} else {
return undefined;
}
}
/**
* Loads the configuration from the specified file and applies any omitted default build
* settings. If the file does not exist, then a default instance is returned.
* If the file contains errors, then an exception is thrown.
*/
public static loadFromFileOrDefault(
jsonFilePath?: string,
doNotIncludeDefaultBuildCommands?: boolean
): CommandLineConfiguration {
let commandLineJson: ICommandLineJson | undefined = undefined;
if (jsonFilePath) {
try {
commandLineJson = JsonFile.load(jsonFilePath);
} catch (e) {
if (!FileSystem.isNotExistError(e as Error)) {
throw e;
}
}
// merge commands specified in command-line.json and default (re)build settings
// Ensure both build commands are included and preserve any other commands specified
if (commandLineJson?.commands) {
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.`
);
}
}
}
}
return new CommandLineConfiguration(commandLineJson, { doNotIncludeDefaultBuildCommands });
}
public prependAdditionalPathFolder(pathFolder: string): void {
(this.additionalPathFolders as string[]).unshift(pathFolder);
}
private _translateBulkCommandToPhasedCommand(command: IBulkCommandJson): IPhasedCommandConfig {
const phaseName: string = command.name;
const phase: IPhase = {
name: phaseName,
isSynthetic: true,
logFilenameIdentifier: _normalizeNameForLogFilenameIdentifiers(command.name),
associatedParameters: new Set(),
dependencies: {
self: new Set(),
upstream: new Set()
},
missingScriptBehavior: command.ignoreMissingScript ? 'log' : 'error',
allowWarningsOnSuccess: !!command.allowWarningsInSuccessfulBuild,
shellCommand: command.shellCommand
};
if (!command.ignoreDependencyOrder) {
phase.dependencies.upstream.add(phase);
}
this.phases.set(phaseName, phase);
this._syntheticPhasesByTranslatedBulkCommandName.set(command.name, phase);
const phases: Set<IPhase> = new Set([phase]);
const translatedCommand: IPhasedCommandConfig = {
...command,
commandKind: 'phased',
isSynthetic: true,
associatedParameters: new Set<IParameterJson>(),
phases,
originalPhases: phases,
// Bulk commands used the same phases for watch as for regular execution. Preserve behavior.
watchPhases: command.watchForChanges ? phases : new Set(),
alwaysWatch: !!command.watchForChanges,
alwaysInstall: undefined
};
return translatedCommand;
}
}