Skip to content
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

Implement lazily created sub commands. #1276

Closed
wants to merge 1 commit into from
Closed

Conversation

mshima
Copy link
Contributor

@mshima mshima commented Jun 7, 2020

Pull Request

Problem

Some programs implements plugin like sub commands.
The plugin options isn't known at the time the root commander is created.

Solution

  • Implement lazily created sub commands.
  • Promisifying some parts of current private api is required.

ChangeLog

@shadowspawn
Copy link
Collaborator

Interesting, thanks. I have quite a few details I want to work through, but think I get the idea.

Promisifying some parts of current private api is required.

In case I am missing something, I think lazy-loading and promise support are two different things? (i.e. might be desirable but not required?)

@mshima
Copy link
Contributor Author

mshima commented Jun 8, 2020

It’s required for my use case now.
Since I am using network and it doesn’t provides a sync api alternative.

@mshima
Copy link
Contributor Author

mshima commented Jun 9, 2020

@shadowspawn let me know if this feature is desired.

See the implementation:
https://github.com/mshima/environment/blob/cli/cli/index.js

It currently uses @npmcli/arborist (npm 7 beta) for testing purpose.

@shadowspawn
Copy link
Collaborator

Thanks for the example.

@shadowspawn
Copy link
Collaborator

@shadowspawn let me know if this feature is desired.

Yes and no. I am interested in exploring the details further, but won't be accepting the PR in its current form if that is what you are asking.

Due to this PR I have been doing some research and thinking on lazy loading, and promisifying, and how best to add new functionality to command creation. I will post more.

For my own reference on the command creation, took me a while to find a previous PR where a similar addition was being made to the command "options" like executableFile. #1185 suggested adding construct, but it shipped as a new factory method instead in #1191.

@mshima
Copy link
Contributor Author

mshima commented Jun 10, 2020

Probably its possible to implement this feature with multiple independent commands, like you recommended at #886, but it will use allowUnknowOptions and I always like to try the upstream approach first.

At https://github.com/mshima/commander.js/tree/multiple I have a implementation (poor) that allows to run multiple commands using -- (can be made configurable).
One side effect is that #1033 can have a workaround:

myCli --globalAndLocalOption -- command --globalAndLocalOption

@mshima
Copy link
Contributor Author

mshima commented Jun 10, 2020

The end result is:

yo --local-only -- generator1 --generator1-config -- generator2 --generator-2-config
  • Create a environment with localOnly option
  • Install and register generator1 to run with generator1Config option
  • Install and register generator2 to run with generator2Config option
  • When program.parseAsync promise resolves, run the environment.

@shadowspawn
Copy link
Collaborator

Your example is a bit different than I expected. It isn't just lazy-loading, but is loading dynamic commands based on the arguments to the declared command (i.e. declared run which loads a command based on argument namespace):

  .command('run <namespace>', 'run the generator', {isDefault: true, subCommandBuilder: subCommandHandler})
  .command('help <namespace>', 'shows the generator help', {subCommandBuilder: subCommandHandler})

And you mentioned running multiple commands too in the following comments.

These are just words so might not be clear, but are you interested in "dynamic" loading of commands, or "lazy" loading of commands? (I think your proposed implementation supports both, but some of the changes I was going to explore do not!)

@mshima
Copy link
Contributor Author

mshima commented Jun 10, 2020

IMO the created command isn't dynamic, once it's created the definitions are all there.
The run command is dynamic.
Probably the feature request definition is Add support to dynamic commands which allows to create lazily created sub commands.

Comment on lines +872 to +875
if (actionPromise instanceof Promise) {
return actionPromise;
}
return Promise.resolve(actionPromise);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be more consistent with JavaScript conventions to check for "thenable" than for a Promise as such, but in fact can just drop the test entirely as Promise.resolve handles Promises too.

* @api public
*/

parseAsync(argv, parseOptions) {
this.parse(argv, parseOptions);
return Promise.all(this._actionResults).then(() => this);
return this._parseProgram(argv, parseOptions).then(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of scattering Promise.resolve through the code in places that do not use Promises, could call Promise.resolve here instead to minimise the code churn.

@shadowspawn
Copy link
Collaborator

It is encouraging that lazy loading and dynamic loading using Promises did not require too much change. However, this PR is doing too many things that each need to be tackled more carefully for easy adoption and the longterm maintenance of the program.

The recursive run test is pretty cool as an example of this implementation.

A possible insight for future evolution of Commander API is that a key difference between an ordinary command and one implemented using a stand-alone executable is that the latter is essentially lazy loaded. The executable command name and parameters and description are all available so that the (parent) help can be generated without loading the command itself. This is also relevant for dynamic and lazy loading.

I wonder whether the dynamic loading could be implemented with current code with an async action handler.

Thank you for your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants