diff --git a/packages/password/package.json b/packages/password/package.json index 36ab81738b..b26795704f 100644 --- a/packages/password/package.json +++ b/packages/password/package.json @@ -57,7 +57,7 @@ "license": "MIT", "homepage": "https://github.com/SBoudrias/Inquirer.js/blob/master/packages/password/README.md", "dependencies": { - "@inquirer/input": "^1.2.14", + "@inquirer/core": "^5.1.1", "@inquirer/type": "^1.1.5", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2" diff --git a/packages/password/password.test.mts b/packages/password/password.test.mts index f22ea2e53b..832844cb50 100644 --- a/packages/password/password.test.mts +++ b/packages/password/password.test.mts @@ -60,15 +60,29 @@ describe('password prompt', () => { expect(getScreen()).toMatchInlineSnapshot('"? Enter your password %%%%"'); }); - it('errors when receiving a transformer function', async () => { - expect(() => { - password({ - message: 'Enter your password', - mask: true, - transformer: () => '', - } as any); - }).toThrowErrorMatchingInlineSnapshot( - `[Error: Inquirer password prompt do not support custom transformer function. Use the input prompt instead.]`, - ); + it('handle synchronous validation', async () => { + const { answer, events, getScreen } = await render(password, { + message: 'Enter your password', + mask: true, + validate: (value) => value.length >= 8, + }); + + expect(getScreen()).toMatchInlineSnapshot(`"? Enter your password"`); + + events.type('1'); + expect(getScreen()).toMatchInlineSnapshot(`"? Enter your password *"`); + + events.keypress('enter'); + await Promise.resolve(); + expect(getScreen()).toMatchInlineSnapshot(` + "? Enter your password * + > You must provide a valid value" + `); + + events.type('2345678'); + expect(getScreen()).toMatchInlineSnapshot(`"? Enter your password ********"`); + + events.keypress('enter'); + await expect(answer).resolves.toEqual('12345678'); }); }); diff --git a/packages/password/src/index.mts b/packages/password/src/index.mts index ce72353923..138774e7c2 100644 --- a/packages/password/src/index.mts +++ b/packages/password/src/index.mts @@ -1,39 +1,73 @@ -import type { Prompt } from '@inquirer/type'; -import input from '@inquirer/input'; +import { + createPrompt, + useState, + useKeypress, + usePrefix, + isEnterKey, + type PromptConfig, +} from '@inquirer/core'; import chalk from 'chalk'; import ansiEscapes from 'ansi-escapes'; -type InputConfig = Parameters[0]; -type PasswordConfig = Omit & { +type PasswordConfig = PromptConfig<{ mask?: boolean | string; -}; + validate?: (value: string) => boolean | string | Promise; +}>; -const password: Prompt = (config, context) => { - if ('transformer' in config) { - throw new Error( - 'Inquirer password prompt do not support custom transformer function. Use the input prompt instead.', - ); +export default createPrompt((config, done) => { + const { validate = () => true } = config; + const [status, setStatus] = useState('pending'); + const [errorMsg, setError] = useState(undefined); + const [value, setValue] = useState(''); + + const isLoading = status === 'loading'; + const prefix = usePrefix(isLoading); + + useKeypress(async (key, rl) => { + // Ignore keypress while our prompt is doing other processing. + if (status !== 'pending') { + return; + } + + if (isEnterKey(key)) { + const answer = value; + setStatus('loading'); + const isValid = await validate(answer); + if (isValid === true) { + setValue(answer); + setStatus('done'); + done(answer); + } else { + // Reset the readline line value to the previous value. On line event, the value + // get cleared, forcing the user to re-enter the value instead of fixing it. + rl.write(value); + setError(isValid || 'You must provide a valid value'); + setStatus('pending'); + } + } else { + setValue(rl.line); + setError(undefined); + } + }); + + const message = chalk.bold(config.message); + let formattedValue = ''; + + if (config.mask) { + const maskChar = typeof config.mask === 'string' ? config.mask : '*'; + formattedValue = maskChar.repeat(value.length); + } else if (status !== 'done') { + formattedValue = `${chalk.dim('[input is masked]')}${ansiEscapes.cursorHide}`; + } + + if (status === 'done') { + formattedValue = chalk.cyan(formattedValue); + } + + let error = ''; + if (errorMsg) { + error = chalk.red(`> ${errorMsg}`); } - return input( - { - ...config, // Make sure we do not display the default password - default: undefined, - transformer(str: string, { isFinal }: { isFinal: boolean }) { - if (config.mask) { - const maskChar = typeof config.mask === 'string' ? config.mask : '*'; - return maskChar.repeat(str.length); - } - - if (!isFinal) { - return `${chalk.dim('[input is masked]')}${ansiEscapes.cursorHide}`; - } - - return ''; - }, - }, - context, - ); -}; - -export default password; + return [`${prefix} ${message} ${formattedValue}`, error]; +}); diff --git a/yarn.lock b/yarn.lock index 6934365421..4b5b8e24dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -456,7 +456,7 @@ __metadata: version: 0.0.0-use.local resolution: "@inquirer/password@workspace:packages/password" dependencies: - "@inquirer/input": ^1.2.14 + "@inquirer/core": ^5.1.1 "@inquirer/testing": ^2.1.9 "@inquirer/type": ^1.1.5 ansi-escapes: ^4.3.2