Inquirer.js icon indicating copy to clipboard operation
Inquirer.js copied to clipboard

Running `prompt` inside `Prompt._run` crashes the process

Open neurolag opened this issue 4 years ago • 4 comments

I've been trying to create a prompt for asking for a list of certain objects. For that reason I created a custom Prompt which itself runs prompt to ask questions and add the answers to an array.

For better understanding, I created a tiny example (it's written in TypeScript - if you wish, I could re-write it in js.):

import { InputQuestionOptions, prompt } from "inquirer";
import Prompt = require("inquirer/lib/prompts/base");

(
    async () =>
    {
        try
        {
            prompt.registerPrompt(
                "test",
                class extends Prompt<InputQuestionOptions<any>>
                {
                    /**
                     * @inheritdoc
                     *
                     * @param resolve
                     * A callback for resolving the prompt.
                     *
                     * @returns
                     * The prompt itself.
                     */
                    protected override _run(resolve: (value: any) => void): void
                    {
                        (async () =>
                        {
                            let result: string[] = [];

                            let answers: {
                                /**
                                 * A value indicating whether another email should be added.
                                 */
                                repeat: boolean;

                                /**
                                 * The entered email.
                                 */
                                email: string;
                            };

                            do
                            {
                                answers = await prompt<typeof answers>(
                                    [
                                        {
                                            name: "email",
                                            message: "please enter an email"
                                        },
                                        {
                                            name: "repeat",
                                            type: "confirm",
                                            message: "would you like to enter an additional email?"
                                        }
                                    ]);

                                result.push(answers.email);
                            }
                            while (answers.repeat);

                            resolve(result);
                        })();
                    }
                });

            let result = await prompt(
                [
                    {
                        type: "test",
                        name: "emails"
                    },
                    {
                        type: "input",
                        name: "food",
                        message: "I have one more question for you! What's your favourite food?"
                    }
                ]);

            console.log("No worries, I'm still alive!");
            console.log(result);
        }
        catch (exception)
        {
            console.log("Error thrown");
            console.log(exception);
        }
    })();

Expected behavior

The code should either follow the normal path (echo "No worries, I'm still alive!" and log the result) or throw an exception.

Actual behavior

The process crashes (exit-code 0) after rendering the 2nd question without any error-message:

Debugger attached.
? please enter an email [email protected]
? please enter an email [email protected]
? would you like to enter an additional email? (Y/n)
? would you like to enter an additional email? Yes
? please enter an email [email protected]
? please enter an email [email protected]
? would you like to enter an additional email? (Y/n) n
? would you like to enter an additional email? No
? I have one more question for you! What's your favourite food? Waiting for the debugger to disconnect...

More details

What's quite funny is, that the custom prompt works if it appears last.

This here causes the described issue:

            let result = await prompt(
                [
                    {
                        type: "test",
                        name: "emails"
                    },
                    {
                        type: "input",
                        name: "food",
                        message: "I have one more question for you! What's your favourite food?"
                    }
                ]);

however, this works:

            let result = await prompt(
                [
                    {
                        type: "test",
                        name: "emails"
                    }
                ]);

and this here works, as well:

            let result = await prompt(
                [
                    {
                        type: "input",
                        name: "food",
                        message: "I have one more question for you! What's your favourite food?"
                    },
                    {
                        type: "test",
                        name: "emails"
                    }
                ]);

Any idea what might cause this issue?

neurolag avatar Jun 02 '21 16:06 neurolag

Yeah, the problem is Node doesn't really allow running 2 readlines at the same time. So nesting your prompts make them conflict with each other.

I don't know of any fixes we can do to Inquirer to allow this.

Similar issue to https://github.com/SBoudrias/Inquirer.js/issues/941

SBoudrias avatar Jun 02 '21 18:06 SBoudrias

I think I found a solution, though, I'm not quite sure it's a valid one...? I added a this.rl.pause()-statement at the beginning and a this.rl.resume()-statement before the resolve(...) and it looks like this fixes the issue.

Do you think this is a valid solution?

 import { createPromptModule, InputQuestionOptions, prompt } from "inquirer";
 import Prompt = require("inquirer/lib/prompts/base");
 import { createInterface } from "readline";
 
 (
     async () =>
     {
         try
         {
             prompt.registerPrompt(
                 "test",
                 class extends Prompt<InputQuestionOptions<any>>
                 {
                     /**
                      * @inheritdoc
                      *
                      * @param resolve
                      * A callback for resolving the prompt.
                      *
                      * @returns
                      * The prompt itself.
                      */
                     protected override _run(resolve: (value: any) => void): this
                     {
                         (async () =>
                         {
+                            this.rl.pause();
                             let result: string[] = [];
 
                             let answers: {
                                 /**
                                  * A value indicating whether another email should be added.
                                  */
                                 repeat: boolean;
 
                                 /**
                                  * The entered email.
                                  */
                                 email: string;
                             };
 
                             do
                             {
                                 let promptResult = prompt<typeof answers>(
                                     [
                                         {
                                             name: "email",
                                             message: "please enter an email"
                                         },
                                         {
                                             name: "repeat",
                                             type: "confirm",
                                             message: "would you like to enter an additional email?"
                                         }
                                     ]);
 
                                 answers = await promptResult;
                                 result.push(answers.email);
                             }
                             while (answers.repeat);
 
+                            this.rl.resume();
                             resolve(result);
                         })();
 
                        return this;
                    }
                });

            let result = await prompt(
                [
                    {
                        type: "test",
                        name: "emails"
                    },
                    {
                        type: "input",
                        name: "food",
                        message: "I have one more question for you! What's your favourite food?"
                    }
                ]);

            console.log("No worries, I'm still alive!");
            console.log(result);
        }
        catch (exception)
        {
            console.log("Error thrown");
            console.log(exception);
        }
    })();

neurolag avatar Jun 02 '21 19:06 neurolag

Though - when using this approach, lines are doubled:

let result = await prompt(
    [
        {
            type: "input",
            name: "food",
            message: "I have one more question for you! What's your favourite food?"
        },
        {
            type: "test",
            name: "emails"
        },
        {
            type: "input",
            name: "moreFood",
            message: "I have one more question for you! What's your favourite food?"
        },
        {
            type: "test",
            name: "moreEmails"
        },
        {
            type: "input",
            name: "evenMoreFood",
            message: "I have one more question for you! What's your favourite food?"
        }
    ]);
? I have one more question for you! What's your favourite food? Lasagne
? please enter an email [email protected]
? would you like to enter an additional email? Yes
? please enter an email [email protected]
? would you like to enter an additional email? No
? I have one more question for you! What's your favourite food? Pizza
? I have one more question for you! What's your favourite food? Pizza
? please enter an email [email protected]
? would you like to enter an additional email? No
? I have one more question for you! What's your favourite food? Hamburger
? I have one more question for you! What's your favourite food? Hamburger

As you can see, the line containing "Pizza" and "Hamburger" are doubled - also, the defaults won't vanish on typing.

However - I think this is still quite an "okay"-ish workaround ^^

neurolag avatar Jun 02 '21 20:06 neurolag

Though, I consider it a workaround, but this here works perfectly for me:

 import { createInterface } from "readline";
 import { ReadStream } from "tty";
 import { createPromptModule, InputQuestionOptions, prompt } from "inquirer";
 import Prompt = require("inquirer/lib/prompts/base");
 
 (
     async () =>
     {
         try
         {
             prompt.registerPrompt(
                 "test",
                 class extends Prompt<InputQuestionOptions<any>>
                 {
                     /**
                      * @inheritdoc
                      *
                      * @param resolve
                      * A callback for resolving the prompt.
                      *
                      * @returns
                      * The prompt itself.
                      */
                     protected override _run(resolve: (value: any) => void): this
                     {
                         (async () =>
                         {
+                            this.rl.pause();
                             let result: string[] = [];
 
                             let answers: {
                                 /**
                                  * A value indicating whether another email should be added.
                                  */
                                 repeat: boolean;
 
                                 /**
                                  * The entered email.
                                  */
                                 email: string;
                             };
 
                             do
                             {
                                 let promptResult = prompt<typeof answers>(
                                     [
                                         {
                                             name: "email",
                                             message: "please enter an email"
                                         },
                                         {
                                             name: "repeat",
                                             type: "confirm",
                                             message: "would you like to enter an additional email?"
                                         }
                                     ]);
 
                                 answers = await promptResult;
                                 result.push(answers.email);
                             }
                             while (answers.repeat);
 
+                            ((this.rl as any).input as ReadStream)?.setRawMode?.(true); // Null-conditional operators (`?.`) ensure, that members are accessed only if they exist.
+                            this.rl.resume();
                             resolve(result);
                         })();
 
                        return this;
                    }
                });

            let result = await prompt(
                [
                    {
                        type: "test",
                        name: "emails"
                    },
                    {
                        type: "input",
                        name: "food",
                        message: "I have one more question for you! What's your favourite food?"
                    }
                ]);

            console.log("No worries, I'm still alive!");
            console.log(result);
        }
        catch (exception)
        {
            console.log("Error thrown");
            console.log(exception);
        }
    })();

neurolag avatar Jun 02 '21 23:06 neurolag