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
text/typescript
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();
},
);
});
}