-
Notifications
You must be signed in to change notification settings - Fork 594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[rush] Add hooks for pre and post rushx events #4286
Changes from 2 commits
bbba972
bfff506
6d29a0e
406fb69
d936b16
8050af6
454e113
9517be9
39d1011
f8a0be2
657eb84
3ba9901
738b55f
327fbe4
3815b1a
c89d1dc
a93c500
3d406c6
ff5d1b9
914ab21
aaee1f2
3065543
b7f94fd
ee01b51
710af8a
2bd1390
5618ca8
6da0cf6
125f25c
04c723d
9236fac
00d2c19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,8 @@ import { Rush } from '../api/Rush'; | |
import { RushConfiguration } from '../api/RushConfiguration'; | ||
import { NodeJsCompatibility } from '../logic/NodeJsCompatibility'; | ||
import { RushStartupBanner } from './RushStartupBanner'; | ||
import { EventHooksManager } from '../logic/EventHooksManager'; | ||
import { Event } from '../api/EventHooks'; | ||
|
||
/** | ||
* @internal | ||
|
@@ -45,129 +47,139 @@ interface IRushXCommandLineArguments { | |
} | ||
|
||
export class RushXCommandLine { | ||
public static launchRushX(launcherVersion: string, isManaged: boolean): void { | ||
RushXCommandLine._launchRushXInternal(launcherVersion, { isManaged }); | ||
public static launchRushX(launcherVersion: string, options: ILaunchRushXInternalOptions): void { | ||
try { | ||
const args: IRushXCommandLineArguments = this._getCommandLineArguments(); | ||
nebosite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const rushConfiguration: RushConfiguration | undefined = RushConfiguration.tryLoadFromDefaultLocation({ | ||
showVerbose: false | ||
}); | ||
const eventHooksManager: EventHooksManager | undefined = rushConfiguration | ||
? new EventHooksManager(rushConfiguration) | ||
: undefined; | ||
|
||
const ignoreHooks = process.env.INSIDERUSH === '1'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This generates a little ugliness in the output, because we will see messages about hooks being ignored. The upside is that we don't have to change the event handler. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rush's environment variable names are always prefixed with The name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nebosite I have renamed this variable to
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. I've been thinking about this some more and I'm not sure we want to use an environment variable this way. IMO, environment variables are better for communicating context than they are at communicating imperative. A SUPPRESS_HOOKS variable operates like an imperative, which obscures the reason that we are ignoring the hooks. It leads to code like this:
That code looks perfectly correct, but:
For the alternative, consider a context approach with a variable such as IS_RECURSIVE_CALL:
Using context gives several improvements:
I'll leave this unresolved so that you read it. It's your call to leave the the current code in place, but I think a context variable would be better here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, the main point of my feedback was that this environment variable is an internal implementation detail and may be subject to change in the future. (For example, what if Adding the You can call the variable whatever makes the most intuitive sense to you, but my suggestion is to make the name very narrow. (For example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. Good name suggestion- I changed it to _RUSH_RECURSIVE_RUSHX_CALL |
||
eventHooksManager?.handle(Event.preRushx, false /* isDebug */, ignoreHooks); | ||
nebosite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
RushXCommandLine._launchRushXInternal(launcherVersion, options, rushConfiguration, args); | ||
eventHooksManager?.handle(Event.postRushx, false /* isDebug */, ignoreHooks); | ||
nebosite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} catch (error) { | ||
console.log(colors.red('Error: ' + (error as Error).message)); | ||
} | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public static _launchRushXInternal(launcherVersion: string, options: ILaunchRushXInternalOptions): void { | ||
public static _launchRushXInternal( | ||
launcherVersion: string, | ||
options: ILaunchRushXInternalOptions, | ||
rushConfiguration: RushConfiguration | undefined, | ||
args: IRushXCommandLineArguments | ||
nebosite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
): void { | ||
// Node.js can sometimes accidentally terminate with a zero exit code (e.g. for an uncaught | ||
// promise exception), so we start with the assumption that the exit code is 1 | ||
// and set it to 0 only on success. | ||
process.exitCode = 1; | ||
|
||
const args: IRushXCommandLineArguments = this._getCommandLineArguments(); | ||
|
||
if (!args.quiet) { | ||
RushStartupBanner.logStreamlinedBanner(Rush.version, options.isManaged); | ||
} | ||
// Are we in a Rush repo? | ||
NodeJsCompatibility.warnAboutCompatibilityIssues({ | ||
isRushLib: true, | ||
alreadyReportedNodeTooNewError: !!options.alreadyReportedNodeTooNewError, | ||
rushConfiguration | ||
}); | ||
|
||
// Find the governing package.json for this folder: | ||
const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); | ||
|
||
const packageJsonFilePath: string | undefined = packageJsonLookup.tryGetPackageJsonFilePathFor( | ||
process.cwd() | ||
); | ||
if (!packageJsonFilePath) { | ||
console.log(colors.red('This command should be used inside a project folder.')); | ||
console.log( | ||
`Unable to find a package.json file in the current working directory or any of its parents.` | ||
); | ||
return; | ||
} | ||
|
||
try { | ||
// Are we in a Rush repo? | ||
const rushConfiguration: RushConfiguration | undefined = RushConfiguration.tryLoadFromDefaultLocation({ | ||
showVerbose: false | ||
}); | ||
NodeJsCompatibility.warnAboutCompatibilityIssues({ | ||
isRushLib: true, | ||
alreadyReportedNodeTooNewError: !!options.alreadyReportedNodeTooNewError, | ||
rushConfiguration | ||
}); | ||
|
||
// Find the governing package.json for this folder: | ||
const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); | ||
|
||
const packageJsonFilePath: string | undefined = packageJsonLookup.tryGetPackageJsonFilePathFor( | ||
process.cwd() | ||
if (rushConfiguration && !rushConfiguration.tryGetProjectForPath(process.cwd())) { | ||
nebosite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// GitHub #2713: Users reported confusion resulting from a situation where "rush install" | ||
// did not install the project's dependencies, because the project was not registered. | ||
console.log( | ||
colors.yellow( | ||
'Warning: You are invoking "rushx" inside a Rush repository, but this project is not registered in rush.json.' | ||
) | ||
); | ||
if (!packageJsonFilePath) { | ||
console.log(colors.red('This command should be used inside a project folder.')); | ||
console.log( | ||
`Unable to find a package.json file in the current working directory or any of its parents.` | ||
); | ||
return; | ||
} | ||
} | ||
|
||
if (rushConfiguration && !rushConfiguration.tryGetProjectForPath(process.cwd())) { | ||
// GitHub #2713: Users reported confusion resulting from a situation where "rush install" | ||
// did not install the project's dependencies, because the project was not registered. | ||
console.log( | ||
colors.yellow( | ||
'Warning: You are invoking "rushx" inside a Rush repository, but this project is not registered in rush.json.' | ||
) | ||
); | ||
} | ||
const packageJson: IPackageJson = packageJsonLookup.loadPackageJson(packageJsonFilePath); | ||
|
||
const packageJson: IPackageJson = packageJsonLookup.loadPackageJson(packageJsonFilePath); | ||
const projectCommandSet: ProjectCommandSet = new ProjectCommandSet(packageJson); | ||
|
||
const projectCommandSet: ProjectCommandSet = new ProjectCommandSet(packageJson); | ||
if (args.help) { | ||
RushXCommandLine._showUsage(packageJson, projectCommandSet); | ||
return; | ||
} | ||
|
||
if (args.help) { | ||
RushXCommandLine._showUsage(packageJson, projectCommandSet); | ||
return; | ||
} | ||
const scriptBody: string | undefined = projectCommandSet.tryGetScriptBody(args.commandName); | ||
|
||
const scriptBody: string | undefined = projectCommandSet.tryGetScriptBody(args.commandName); | ||
if (scriptBody === undefined) { | ||
console.log( | ||
colors.red( | ||
`Error: The command "${args.commandName}" is not defined in the` + | ||
` package.json file for this project.` | ||
) | ||
); | ||
|
||
if (scriptBody === undefined) { | ||
if (projectCommandSet.commandNames.length > 0) { | ||
console.log( | ||
colors.red( | ||
`Error: The command "${args.commandName}" is not defined in the` + | ||
` package.json file for this project.` | ||
) | ||
'\nAvailable commands for this project are: ' + | ||
projectCommandSet.commandNames.map((x) => `"${x}"`).join(', ') | ||
); | ||
|
||
if (projectCommandSet.commandNames.length > 0) { | ||
console.log( | ||
'\nAvailable commands for this project are: ' + | ||
projectCommandSet.commandNames.map((x) => `"${x}"`).join(', ') | ||
); | ||
} | ||
|
||
console.log(`Use ${colors.yellow('"rushx --help"')} for more information.`); | ||
return; | ||
} | ||
|
||
let commandWithArgs: string = scriptBody; | ||
let commandWithArgsForDisplay: string = scriptBody; | ||
if (args.commandArgs.length > 0) { | ||
// This approach is based on what NPM 7 now does: | ||
// https://github.com/npm/run-script/blob/47a4d539fb07220e7215cc0e482683b76407ef9b/lib/run-script-pkg.js#L34 | ||
const escapedRemainingArgs: string[] = args.commandArgs.map((x) => Utilities.escapeShellParameter(x)); | ||
console.log(`Use ${colors.yellow('"rushx --help"')} for more information.`); | ||
return; | ||
} | ||
|
||
commandWithArgs += ' ' + escapedRemainingArgs.join(' '); | ||
let commandWithArgs: string = scriptBody; | ||
let commandWithArgsForDisplay: string = scriptBody; | ||
if (args.commandArgs.length > 0) { | ||
// This approach is based on what NPM 7 now does: | ||
// https://github.com/npm/run-script/blob/47a4d539fb07220e7215cc0e482683b76407ef9b/lib/run-script-pkg.js#L34 | ||
const escapedRemainingArgs: string[] = args.commandArgs.map((x) => Utilities.escapeShellParameter(x)); | ||
|
||
// Display it nicely without the extra quotes | ||
commandWithArgsForDisplay += ' ' + args.commandArgs.join(' '); | ||
} | ||
commandWithArgs += ' ' + escapedRemainingArgs.join(' '); | ||
|
||
if (!args.quiet) { | ||
console.log(`> ${JSON.stringify(commandWithArgsForDisplay)}\n`); | ||
} | ||
// Display it nicely without the extra quotes | ||
commandWithArgsForDisplay += ' ' + args.commandArgs.join(' '); | ||
} | ||
|
||
const packageFolder: string = path.dirname(packageJsonFilePath); | ||
|
||
const exitCode: number = Utilities.executeLifecycleCommand(commandWithArgs, { | ||
rushConfiguration, | ||
workingDirectory: packageFolder, | ||
// If there is a rush.json then use its .npmrc from the temp folder. | ||
// Otherwise look for npmrc in the project folder. | ||
initCwd: rushConfiguration ? rushConfiguration.commonTempFolder : packageFolder, | ||
handleOutput: false, | ||
environmentPathOptions: { | ||
includeProjectBin: true | ||
} | ||
}); | ||
if (!args.quiet) { | ||
console.log(`> ${JSON.stringify(commandWithArgsForDisplay)}\n`); | ||
} | ||
|
||
if (exitCode > 0) { | ||
console.log(colors.red(`The script failed with exit code ${exitCode}`)); | ||
const packageFolder: string = path.dirname(packageJsonFilePath); | ||
|
||
const exitCode: number = Utilities.executeLifecycleCommand(commandWithArgs, { | ||
rushConfiguration, | ||
workingDirectory: packageFolder, | ||
// If there is a rush.json then use its .npmrc from the temp folder. | ||
// Otherwise look for npmrc in the project folder. | ||
initCwd: rushConfiguration ? rushConfiguration.commonTempFolder : packageFolder, | ||
handleOutput: false, | ||
environmentPathOptions: { | ||
includeProjectBin: true | ||
} | ||
}); | ||
|
||
process.exitCode = exitCode; | ||
} catch (error) { | ||
console.log(colors.red('Error: ' + (error as Error).message)); | ||
if (exitCode > 0) { | ||
console.log(colors.red(`The script failed with exit code ${exitCode}`)); | ||
} | ||
octogonz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
process.exitCode = exitCode; | ||
} | ||
|
||
private static _getCommandLineArguments(): IRushXCommandLineArguments { | ||
|
@@ -206,6 +218,7 @@ export class RushXCommandLine { | |
if (unknownArgs.length > 0) { | ||
// Future TODO: Instead of just displaying usage info, we could display a | ||
// specific error about the unknown flag the user tried to pass to rushx. | ||
console.log(colors.red(`Unknown arguments: ${unknownArgs.join(', ')}`)); | ||
nebosite marked this conversation as resolved.
Show resolved
Hide resolved
|
||
help = true; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the changes in RushXCommandLine.ts for the technical reasons motivating this change. It also doesn't seem kosher to call the internal version from here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's marked internal, not private. The marker is that it is expected to be called from within the package and not externally. These are in the same package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Shall I leave the code as-is, or add another layer to contain the code added to launchRushX?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to remember why we did this like this. @octogonz, do you recall?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the code, the sort-of-public entry points are:
They are not marked as
@internal
but instead have doc comments instructing third parties NOT to use them, because they are only meant to be accessed by the@microsoft/rush
CLI front end.Why didn't we label them as
@internal
? I think the answer is thatRush.launch()
is ancient and predates API Extractor, and then the other two just continued the same pattern. We "should" fix that, except that doing so would break compatibility with all previous versions of the@microsoft/rush
front end.The situation of
RushXCommandLine
is more mysterious, since this class is not actually exported byindex.d.ts
at all, yet it has both an@internal
and regular method:I forget the details, but my guess is that there is probably one early release of
@microsoft/rush
that did importRushXCommandLine.launchRushX()
but then we quickly replaced that withRush.launchRushX()
to reduce the API surface. Rush 6 would be a good opportunity to clean up this contract.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@octogonz To be clear, for this particular line of code, it is a repointing, not a renaming. Both launchRushx and _launchRushXInternal have the same names and this repointing is necessary to preserve the same functionality as before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true here, but in the
RushXCommandLine
class, this API:...got renamed to this:
I'm not necessarily opposed to cleaning this up, but since it's old code and we didn't remember whether there are
@microsoft/rush
releases that access it, it might require some additional review.Whereas the
RushXCommandLine._launchRushXInternal()
is explicitly marked as@internal
, so it seems very safe to change that one however you like, including itsILaunchRushXInternalOptions
interface.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Hold on, I'm going to just look at the Git history.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rushx
did not haveRushXCommandLine
at all:[rush] Add "rushx" binary for project-specific commands #678
@internal
member_launchRushXInternal()
:[rush] Add support for "global" custom commands #684
@internal
member was added in this later PR:[rush] Update rush NodeJS warning messages and add option to suppress non-LTS warning messages. #1388
The review comments don't explain why we added an
@internal
member toRushXCommandLine
, but perhaps it was this same paranoia about whetherRushXCommandLine.launchRushX()
was a@microsoft/rush
version selector contract or not.So, looking at this code, I think we can conclude that
RushXCommandLine
is entirely internal torush-lib
, and therefore we should remove the misleading@internal
markers from that class, and you're free to refactor it however you like. 🙂There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I removed the internal markers and refactored the call to _launchRushXInternal to take a single options argument.