diff --git a/.changeset/bright-hornets-destroy.md b/.changeset/bright-hornets-destroy.md new file mode 100644 index 0000000..b2c01bc --- /dev/null +++ b/.changeset/bright-hornets-destroy.md @@ -0,0 +1,6 @@ +--- +"@clack/prompts": patch +"@clack/core": patch +--- + +Prevents placeholder from being used as input value in text prompts diff --git a/examples/basic/default-value.ts b/examples/basic/default-value.ts new file mode 100644 index 0000000..c05eb89 --- /dev/null +++ b/examples/basic/default-value.ts @@ -0,0 +1,32 @@ +import * as p from '@clack/prompts'; +import color from 'picocolors'; + +async function main() { + const defaultPath = 'my-project'; + + const result = await p.text( + { + message: 'Enter the directory to bootstrap the project', + placeholder: ` (hit Enter to use '${defaultPath}')`, + defaultValue: defaultPath, + validate: (value) => { + if (!value) { + return 'Directory is required'; + } + if (value.includes(' ')) { + return 'Directory cannot contain spaces'; + } + return undefined; + }, + } + ); + + if (p.isCancel(result)) { + p.cancel('Operation cancelled.'); + process.exit(0); + } + + p.outro(`Let's bootstrap the project in ${color.cyan(result)}`); +} + +main().catch(console.error); diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index be5c0dd..fcd6f0f 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -14,6 +14,7 @@ export interface PromptOptions { render(this: Omit): string | undefined; placeholder?: string; initialValue?: any; + defaultValue?: any; validate?: ((value: any) => string | Error | undefined) | undefined; input?: Readable; output?: Writable; @@ -222,9 +223,12 @@ export default class Prompt { this.emit('key', char?.toLowerCase(), key); if (key?.name === 'return') { - if (!this.value && this.opts.placeholder) { - this.rl?.write(this.opts.placeholder); - this._setValue(this.opts.placeholder); + if (!this.value) { + if (this.opts.defaultValue) { + this._setValue(this.opts.defaultValue); + } else { + this._setValue(''); + } } if (this.opts.validate) { diff --git a/packages/core/src/prompts/text.ts b/packages/core/src/prompts/text.ts index f8bdf41..052a9bd 100644 --- a/packages/core/src/prompts/text.ts +++ b/packages/core/src/prompts/text.ts @@ -29,6 +29,9 @@ export default class TextPrompt extends Prompt { if (!this.value) { this.value = opts.defaultValue; } + if (this.value === undefined) { + this.value = ''; + } }); } } diff --git a/packages/core/test/prompts/prompt.test.ts b/packages/core/test/prompts/prompt.test.ts index d9665b5..dcc4d33 100644 --- a/packages/core/test/prompts/prompt.test.ts +++ b/packages/core/test/prompts/prompt.test.ts @@ -38,7 +38,7 @@ describe('Prompt', () => { const resultPromise = instance.prompt(); input.emit('keypress', '', { name: 'return' }); const result = await resultPromise; - expect(result).to.equal(undefined); + expect(result).to.equal(''); expect(isCancel(result)).to.equal(false); expect(instance.state).to.equal('submit'); expect(output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]); diff --git a/packages/core/test/prompts/text.test.ts b/packages/core/test/prompts/text.test.ts index f51c6c8..8230814 100644 --- a/packages/core/test/prompts/text.test.ts +++ b/packages/core/test/prompts/text.test.ts @@ -110,5 +110,32 @@ describe('TextPrompt', () => { input.emit('keypress', 'right', { name: 'right' }); expect(instance.valueWithCursor).to.equal('foo█'); }); + + test('does not use placeholder as value when pressing enter', async () => { + const instance = new TextPrompt({ + input, + output, + render: () => 'foo', + placeholder: ' (hit Enter to use default)', + defaultValue: 'default-value' + }); + const resultPromise = instance.prompt(); + input.emit('keypress', '', { name: 'return' }); + const result = await resultPromise; + expect(result).to.equal('default-value'); + }); + + test('returns empty string when no value and no default', async () => { + const instance = new TextPrompt({ + input, + output, + render: () => 'foo', + placeholder: ' (hit Enter to use default)' + }); + const resultPromise = instance.prompt(); + input.emit('keypress', '', { name: 'return' }); + const result = await resultPromise; + expect(result).to.equal(''); + }); }); }); diff --git a/packages/prompts/src/text.ts b/packages/prompts/src/text.ts index 9ca0b1f..34f84de 100644 --- a/packages/prompts/src/text.ts +++ b/packages/prompts/src/text.ts @@ -30,8 +30,10 @@ export const text = (opts: TextOptions) => { return `${title.trim()}\n${color.yellow(S_BAR)} ${value}\n${color.yellow( S_BAR_END )} ${color.yellow(this.error)}\n`; - case 'submit': - return `${title}${color.gray(S_BAR)} ${color.dim(this.value || opts.placeholder)}`; + case 'submit': { + const displayValue = typeof this.value === 'undefined' ? '' : this.value; + return `${title}${color.gray(S_BAR)} ${color.dim(displayValue)}`; + } case 'cancel': return `${title}${color.gray(S_BAR)} ${color.strikethrough( color.dim(this.value ?? '') diff --git a/packages/prompts/test/__snapshots__/path.test.ts.snap b/packages/prompts/test/__snapshots__/path.test.ts.snap index ce1138d..2794569 100644 --- a/packages/prompts/test/__snapshots__/path.test.ts.snap +++ b/packages/prompts/test/__snapshots__/path.test.ts.snap @@ -143,17 +143,21 @@ exports[`text (isCI = false) > renders submitted value 1`] = ` exports[`text (isCI = false) > validation errors render and clear (using Error) 1`] = ` [ - "", + "", + "", + "", "│ -◆ foo -│ /tmp/foo -└ +▲ foo +│ /tmp/foo +└ should be /tmp/bar ", "", - "", - "", - "│ /tmp/b ", - "", + "", + "", + "◆ foo +│ /tmp/b  +└ +", "", "", "", @@ -186,17 +190,21 @@ exports[`text (isCI = false) > validation errors render and clear (using Error) exports[`text (isCI = false) > validation errors render and clear 1`] = ` [ - "", + "", + "", + "", "│ -◆ foo -│ /tmp/foo -└ +▲ foo +│ /tmp/foo +└ should be /tmp/bar ", "", - "", - "", - "│ /tmp/b ", - "", + "", + "", + "◆ foo +│ /tmp/b  +└ +", "", "", "", @@ -370,17 +378,21 @@ exports[`text (isCI = true) > renders submitted value 1`] = ` exports[`text (isCI = true) > validation errors render and clear (using Error) 1`] = ` [ - "", + "", + "", + "", "│ -◆ foo -│ /tmp/foo -└ +▲ foo +│ /tmp/foo +└ should be /tmp/bar ", "", - "", - "", - "│ /tmp/b ", - "", + "", + "", + "◆ foo +│ /tmp/b  +└ +", "", "", "", @@ -413,17 +425,21 @@ exports[`text (isCI = true) > validation errors render and clear (using Error) 1 exports[`text (isCI = true) > validation errors render and clear 1`] = ` [ - "", + "", + "", + "", "│ -◆ foo -│ /tmp/foo -└ +▲ foo +│ /tmp/foo +└ should be /tmp/bar ", "", - "", - "", - "│ /tmp/b ", - "", + "", + "", + "◆ foo +│ /tmp/b  +└ +", "", "", "", diff --git a/packages/prompts/test/__snapshots__/suggestion.test.ts.snap b/packages/prompts/test/__snapshots__/suggestion.test.ts.snap index e1f2aee..93446c2 100644 --- a/packages/prompts/test/__snapshots__/suggestion.test.ts.snap +++ b/packages/prompts/test/__snapshots__/suggestion.test.ts.snap @@ -142,17 +142,21 @@ exports[`text (isCI = false) > renders submitted value 1`] = ` exports[`text (isCI = false) > validation errors render and clear (using Error) 1`] = ` [ - "", + "", + "", + "", "│ -◆ foo -│ xyz -└ +▲ foo +│ xyz +└ should be xy ", "", - "", - "", - "│ xyz", - "", + "", + "", + "◆ foo +│ xyz +└ +", "", "", "", @@ -180,17 +184,21 @@ exports[`text (isCI = false) > validation errors render and clear (using Error) exports[`text (isCI = false) > validation errors render and clear 1`] = ` [ - "", + "", + "", + "", "│ -◆ foo -│ xyz -└ +▲ foo +│ xyz +└ should be xy ", "", - "", - "", - "│ xyz", - "", + "", + "", + "◆ foo +│ xyz +└ +", "", "", "", @@ -358,17 +366,21 @@ exports[`text (isCI = true) > renders submitted value 1`] = ` exports[`text (isCI = true) > validation errors render and clear (using Error) 1`] = ` [ - "", + "", + "", + "", "│ -◆ foo -│ xyz -└ +▲ foo +│ xyz +└ should be xy ", "", - "", - "", - "│ xyz", - "", + "", + "", + "◆ foo +│ xyz +└ +", "", "", "", @@ -396,17 +408,21 @@ exports[`text (isCI = true) > validation errors render and clear (using Error) 1 exports[`text (isCI = true) > validation errors render and clear 1`] = ` [ - "", + "", + "", + "", "│ -◆ foo -│ xyz -└ +▲ foo +│ xyz +└ should be xy ", "", - "", - "", - "│ xyz", - "", + "", + "", + "◆ foo +│ xyz +└ +", "", "", "", diff --git a/packages/prompts/test/__snapshots__/text.test.ts.snap b/packages/prompts/test/__snapshots__/text.test.ts.snap index 02d4e79..cd421cc 100644 --- a/packages/prompts/test/__snapshots__/text.test.ts.snap +++ b/packages/prompts/test/__snapshots__/text.test.ts.snap @@ -38,6 +38,44 @@ exports[`text (isCI = false) > defaultValue sets the value but does not render 1 ] `; +exports[`text (isCI = false) > empty string when no value and no default 1`] = ` +[ + "", + "│ +◆ foo +│   (hit Enter to use default) +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "", +] +`; + +exports[`text (isCI = false) > placeholder is not used as value when pressing enter 1`] = ` +[ + "", + "│ +◆ foo +│   (hit Enter to use default) +└ +", + "", + "", + "", + "◇ foo +│ default-value", + " +", + "", +] +`; + exports[`text (isCI = false) > renders cancelled value if one set 1`] = ` [ "", @@ -80,7 +118,7 @@ exports[`text (isCI = false) > renders message 1`] = ` "", "", "◇ foo -│ undefined", +│", " ", "", @@ -99,7 +137,7 @@ exports[`text (isCI = false) > renders placeholder if set 1`] = ` "", "", "◇ foo -│ bar", +│", " ", "", @@ -249,6 +287,44 @@ exports[`text (isCI = true) > defaultValue sets the value but does not render 1` ] `; +exports[`text (isCI = true) > empty string when no value and no default 1`] = ` +[ + "", + "│ +◆ foo +│   (hit Enter to use default) +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "", +] +`; + +exports[`text (isCI = true) > placeholder is not used as value when pressing enter 1`] = ` +[ + "", + "│ +◆ foo +│   (hit Enter to use default) +└ +", + "", + "", + "", + "◇ foo +│ default-value", + " +", + "", +] +`; + exports[`text (isCI = true) > renders cancelled value if one set 1`] = ` [ "", @@ -291,7 +367,7 @@ exports[`text (isCI = true) > renders message 1`] = ` "", "", "◇ foo -│ undefined", +│", " ", "", @@ -310,7 +386,7 @@ exports[`text (isCI = true) > renders placeholder if set 1`] = ` "", "", "◇ foo -│ bar", +│", " ", "", diff --git a/packages/prompts/test/text.test.ts b/packages/prompts/test/text.test.ts index 749ca03..d2ddcd2 100644 --- a/packages/prompts/test/text.test.ts +++ b/packages/prompts/test/text.test.ts @@ -52,8 +52,7 @@ describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { const value = await result; expect(output.buffer).toMatchSnapshot(); - - expect(value).toBe('bar'); + expect(value).toBe(''); }); test(' applies placeholder', async () => { @@ -174,4 +173,37 @@ describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { expect(value).toBe('xy'); expect(output.buffer).toMatchSnapshot(); }); + + test('placeholder is not used as value when pressing enter', async () => { + const result = prompts.text({ + message: 'foo', + placeholder: ' (hit Enter to use default)', + defaultValue: 'default-value', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('default-value'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('empty string when no value and no default', async () => { + const result = prompts.text({ + message: 'foo', + placeholder: ' (hit Enter to use default)', + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe(''); + expect(output.buffer).toMatchSnapshot(); + }); });