UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

230 lines (196 loc) • 6.05 kB
import type { HookContext, HookManager } from "../../types/hooks.js"; import type { UserInterruptionManager } from "../../types/user-interruptions.js"; import type { Interface } from "node:readline"; import { createInterface } from "node:readline"; import { styleText } from "node:util"; import { assertHardhatInvariant, HardhatError, } from "@nomicfoundation/hardhat-errors"; import { AsyncMutex } from "@nomicfoundation/hardhat-utils/synchronization"; export class UserInterruptionManagerImplementation implements UserInterruptionManager { readonly #hooks; readonly #mutex = new AsyncMutex(); constructor(hooks: HookManager) { this.#hooks = hooks; } public async displayMessage( interruptor: string, message: string, ): Promise<void> { return await this.#mutex.exclusiveRun(async () => { return await this.#hooks.runHandlerChain( "userInterruptions", "displayMessage", [interruptor, message], defaultDisplayMessage, ); }); } public async requestInput( interruptor: string, inputDescription: string, ): Promise<string> { return await this.#mutex.exclusiveRun(async () => { return await this.#hooks.runHandlerChain( "userInterruptions", "requestInput", [interruptor, inputDescription], defaultRequestInput, ); }); } public async requestSecretInput( interruptor: string, inputDescription: string, ): Promise<string> { return await this.#mutex.exclusiveRun(async () => { return await this.#hooks.runHandlerChain( "userInterruptions", "requestSecretInput", [interruptor, inputDescription], defaultRequestSecretInput, ); }); } public async uninterrupted<ReturnT>( f: () => ReturnT, ): Promise<Awaited<ReturnT>> { return await this.#mutex.exclusiveRun(f); } } /** * Lines read from stdin that were not the answer to the prompt that read * them. When input is piped, a single chunk usually contains the answers to * several prompts, and readline still emits its remaining lines after the * interface used for the first answer is closed, so they are kept here for * the next prompts. */ const pendingLines: string[] = []; function takePendingLine( interruptor: string, inputDescription: string, ): string | undefined { const line = pendingLines.shift(); if (line !== undefined) { process.stdout.write( styleText("blue", `[${interruptor}]`) + ` ${inputDescription}: `, ); } return line; } async function defaultDisplayMessage( _context: HookContext, interruptor: string, message: string, ) { console.log(styleText("blue", `[${interruptor}]`) + ` ${message}`); } async function defaultRequestInput( _context: HookContext, interruptor: string, inputDescription: string, ): Promise<string> { const pendingLine = takePendingLine(interruptor, inputDescription); if (pendingLine !== undefined) { return pendingLine; } const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY === true, }); rl.on("line", (line) => pendingLines.push(line)); return await askQuestion(rl, interruptor, inputDescription); } async function defaultRequestSecretInput( _context: HookContext, interruptor: string, inputDescription: string, ): Promise<string> { const pendingLine = takePendingLine(interruptor, inputDescription); if (pendingLine !== undefined) { return pendingLine; } const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY === true, }); rl.on("line", (line) => pendingLines.push(line)); /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We need to access a private property of the readline interface. */ const rlAsAny = rl as any; let initialMessage: string | undefined; rlAsAny._writeToOutput = (out: string) => { if (initialMessage === undefined || out.length !== 1) { if (initialMessage === undefined) { initialMessage = out; } assertHardhatInvariant( rlAsAny.output !== undefined, "Expected readline output to be defined", ); // We show the initial message as is if (out.startsWith(initialMessage)) { rlAsAny.output.write(initialMessage); out = out.slice(initialMessage.length); } else if (out.trim() === "") { rlAsAny.output.write(out); out = ""; } } // We show the rest of the chars as "*" for (const _ of out) { rlAsAny.output.write("*"); } }; return await askQuestion(rl, interruptor, inputDescription); } /** * Asks the user for input through the given readline interface. * * If the interface closes before an answer arrives (e.g. stdin reaches EOF, or * the user cancels the prompt) it throws instead of never settling. */ async function askQuestion( rl: Interface, interruptor: string, inputDescription: string, ): Promise<string> { return await new Promise<string>((resolve, reject) => { let prompted = false; let answered = false; rl.once("close", () => { if (answered) { return; } // The prompt doesn't end with a newline, so we print one to keep the // error off the prompt's line if (prompted) { process.stdout.write("\n"); } reject( new HardhatError( HardhatError.ERRORS.CORE.GENERAL.PROMPT_CLOSED_BEFORE_ANSWER, { interruptor }, ), ); }); // If a previous prompt already read stdin's EOF, readline won't receive // another "end" event, so we close the interface ourselves. if (process.stdin.readableEnded) { rl.close(); return; } prompted = true; rl.question( styleText("blue", `[${interruptor}]`) + ` ${inputDescription}: `, (answer) => { answered = true; resolve(answer); rl.close(); }, ); }); }