[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

wip: initial components

James Garbutt (Jul 8, 2025, 5:41 PM +0100) c933bd91 eb79a94e

+663 -26
+1 -1
packages/jsx/jsx-runtime.d.ts
··· 1 - export { jsx, jsxDEV } from './dist/index.mjs'; 1 + export * from './dist/index.mjs';
+1 -1
packages/jsx/jsx-runtime.js
··· 1 - export { jsx, jsxDEV } from './dist/index.mjs'; 1 + export * from './dist/index.mjs';
+1
packages/jsx/package.json
··· 57 57 }, 58 58 "devDependencies": { 59 59 "vitest": "^3.1.1", 60 + "vitest-ansi-serializer": "^0.1.2", 60 61 "@clack/test-utils": "workspace:*" 61 62 } 62 63 }
+8
packages/jsx/src/components/confirm.ts
··· 1 + import type { ConfirmOptions } from '@clack/prompts'; 2 + import { confirm } from '@clack/prompts'; 3 + 4 + export type ConfirmProps = ConfirmOptions; 5 + 6 + export function Confirm(props: ConfirmProps): ReturnType<typeof confirm> { 7 + return confirm(props); 8 + }
+31
packages/jsx/src/components/note.ts
··· 1 + import type { NoteOptions } from '@clack/prompts'; 2 + import { isCancel, note } from '@clack/prompts'; 3 + import type { JSX } from '../types.js'; 4 + import { resolveChildren } from '../utils.js'; 5 + 6 + export interface NoteProps extends NoteOptions { 7 + children?: JSX.Element[] | JSX.Element | string; 8 + message?: string; 9 + title?: string; 10 + } 11 + 12 + export async function Note(props: NoteProps): Promise<void> { 13 + let message = ''; 14 + 15 + if (props.children) { 16 + const messages: string[] = []; 17 + const children = await resolveChildren(props.children); 18 + for (const child of children) { 19 + // TODO (43081j): handle cancelling of children 20 + if (isCancel(child)) { 21 + continue; 22 + } 23 + messages.push(String(child)); 24 + } 25 + message = messages.join('\n'); 26 + } else if (props.message) { 27 + message = props.message; 28 + } 29 + 30 + note(message, props.title, props); 31 + }
+31
packages/jsx/src/components/option.ts
··· 1 + import { type Option, isCancel } from '@clack/prompts'; 2 + import type { JSX } from '../types.js'; 3 + import { resolveChildren } from '../utils.js'; 4 + 5 + export interface OptionProps { 6 + value: unknown; 7 + children?: JSX.Element | JSX.Element[] | string; 8 + } 9 + 10 + export async function Option(props: OptionProps): Promise<Option<unknown>> { 11 + let label = ''; 12 + if (props.children) { 13 + const children = await resolveChildren(props.children); 14 + const childStrings: string[] = []; 15 + 16 + for (const child of children) { 17 + if (isCancel(child)) { 18 + continue; 19 + } 20 + childStrings.push(String(child)); 21 + } 22 + 23 + label = childStrings.join('\n'); 24 + } else { 25 + label = String(props.value); 26 + } 27 + return { 28 + value: props.value, 29 + label, 30 + }; 31 + }
+8
packages/jsx/src/components/password.ts
··· 1 + import type { PasswordOptions } from '@clack/prompts'; 2 + import { password } from '@clack/prompts'; 3 + 4 + export type PasswordProps = PasswordOptions; 5 + 6 + export function Password(props: PasswordProps): ReturnType<typeof password> { 7 + return password(props); 8 + }
+35
packages/jsx/src/components/select.ts
··· 1 + import type { Option, SelectOptions } from '@clack/prompts'; 2 + import { select } from '@clack/prompts'; 3 + import type { JSX } from '../types.js'; 4 + import { resolveChildren } from '../utils.js'; 5 + 6 + export interface SelectProps extends Omit<SelectOptions<unknown>, 'options'> { 7 + children: JSX.Element[] | JSX.Element; 8 + } 9 + 10 + const isOptionLike = (obj: unknown): obj is Option<unknown> => { 11 + return ( 12 + obj !== null && 13 + typeof obj === 'object' && 14 + Object.hasOwnProperty.call(obj, 'label') && 15 + Object.hasOwnProperty.call(obj, 'value') && 16 + typeof (obj as { label: string }).label === 'string' 17 + ); 18 + }; 19 + 20 + export async function Select(props: SelectProps): ReturnType<typeof select> { 21 + const { children, ...opts } = props; 22 + const options: Option<unknown>[] = []; 23 + const resolvedChildren = await resolveChildren(props.children); 24 + 25 + for (const child of resolvedChildren) { 26 + if (isOptionLike(child)) { 27 + options.push(child); 28 + } 29 + } 30 + 31 + return select({ 32 + ...opts, 33 + options, 34 + }); 35 + }
+8
packages/jsx/src/components/text.ts
··· 1 + import type { TextOptions } from '@clack/prompts'; 2 + import { text } from '@clack/prompts'; 3 + 4 + export type TextProps = TextOptions; 5 + 6 + export function Text(props: TextProps): ReturnType<typeof text> { 7 + return text(props); 8 + }
+31 -22
packages/jsx/src/index.ts
··· 1 - import type { ConfirmOptions } from '@clack/prompts'; 2 - import { confirm } from '@clack/prompts'; 3 - 4 - namespace JSX { 5 - export interface IntrinsicElements { 6 - } 7 - 8 - export type Element = Promise<unknown>; 9 - } 1 + import { Confirm, type ConfirmProps } from './components/confirm.js'; 2 + import { Note, type NoteProps } from './components/note.js'; 3 + import { Option, type OptionProps } from './components/option.js'; 4 + import { Password, type PasswordProps } from './components/password.js'; 5 + import { Select, type SelectProps } from './components/select.js'; 6 + import { Text, type TextProps } from './components/text.js'; 7 + import type { JSX } from './types.js'; 10 8 11 9 export type { JSX }; 10 + export { 11 + Confirm, 12 + type ConfirmProps, 13 + Note, 14 + type NoteProps, 15 + Text, 16 + type TextProps, 17 + Password, 18 + type PasswordProps, 19 + Option, 20 + type OptionProps, 21 + Select, 22 + type SelectProps, 23 + }; 12 24 13 - export function Confirm(props: ConfirmOptions): ReturnType<typeof confirm> { 14 - return confirm(props); 25 + export function Fragment(props: { children: JSX.Element | JSX.Element[] }): JSX.Element { 26 + return Promise.resolve(props.children); 15 27 } 16 28 17 29 export type Component = 18 - | typeof Confirm; 30 + | typeof Confirm 31 + | typeof Note 32 + | typeof Text 33 + | typeof Password 34 + | typeof Option 35 + | typeof Select; 19 36 20 37 function jsx<T extends keyof JSX.IntrinsicElements>( 21 38 tag: T, 22 39 props: JSX.IntrinsicElements[T], 23 40 _key?: string 24 41 ): JSX.Element; 25 - function jsx<T extends Component>( 26 - fn: T, 27 - props: Parameters<T>[0], 28 - _key?: string 29 - ): JSX.Element; 30 - function jsx( 31 - tagOrFn: string | Component, 32 - props: unknown, 33 - _key?: string 34 - ): JSX.Element { 42 + function jsx<T extends Component>(fn: T, props: Parameters<T>[0], _key?: string): JSX.Element; 43 + function jsx(tagOrFn: string | Component, props: unknown, _key?: string): JSX.Element { 35 44 if (typeof tagOrFn === 'function') { 36 45 return (tagOrFn as (props: unknown) => JSX.Element)(props); 37 46 }
+7
packages/jsx/src/types.ts
··· 1 + namespace JSX { 2 + export type IntrinsicElements = {}; 3 + 4 + export type Element = Promise<unknown>; 5 + } 6 + 7 + export type { JSX };
+16
packages/jsx/src/utils.ts
··· 1 + import type { JSX } from './types.js'; 2 + 3 + export async function resolveChildren( 4 + children: JSX.Element[] | JSX.Element | string 5 + ): Promise<unknown[]> { 6 + const arr = Array.isArray(children) ? children : [children]; 7 + const results: unknown[] = []; 8 + 9 + for (const child of arr) { 10 + const result = await child; 11 + 12 + results.push(result); 13 + } 14 + 15 + return results; 16 + }
+302
packages/jsx/test/__snapshots__/jsx.test.tsx.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`jsx > Confirm > can set active text 1`] = ` 4 + [ 5 + "<cursor.hide>", 6 + "│ 7 + ◆ foo? 8 + │ ● DO IT / ○ No 9 + └ 10 + ", 11 + "<cursor.backward count=999><cursor.up count=4>", 12 + "<cursor.down count=1>", 13 + "<erase.down>", 14 + "◇ foo? 15 + │ DO IT", 16 + " 17 + ", 18 + "<cursor.show>", 19 + ] 20 + `; 21 + 22 + exports[`jsx > Confirm > can set inactive text 1`] = ` 23 + [ 24 + "<cursor.hide>", 25 + "│ 26 + ◆ foo? 27 + │ ● Yes / ○ DONT DO IT 28 + └ 29 + ", 30 + "<cursor.backward count=999><cursor.up count=4>", 31 + "<cursor.down count=1>", 32 + "<erase.down>", 33 + "◇ foo? 34 + │ Yes", 35 + " 36 + ", 37 + "<cursor.show>", 38 + ] 39 + `; 40 + 41 + exports[`jsx > Confirm > can set message 1`] = ` 42 + [ 43 + "<cursor.hide>", 44 + "│ 45 + ◆ foo? 46 + │ ● Yes / ○ No 47 + └ 48 + ", 49 + "<cursor.backward count=999><cursor.up count=4>", 50 + "<cursor.down count=1>", 51 + "<erase.down>", 52 + "◇ foo? 53 + │ Yes", 54 + " 55 + ", 56 + "<cursor.show>", 57 + ] 58 + `; 59 + 60 + exports[`jsx > Note > can render children as message 1`] = ` 61 + [ 62 + "│ 63 + ◇  ──────────╮ 64 + │ │ 65 + │ a message │ 66 + │ │ 67 + ├─────────────╯ 68 + ", 69 + ] 70 + `; 71 + 72 + exports[`jsx > Note > can render complex results as message 1`] = ` 73 + [ 74 + "<cursor.hide>", 75 + "│ 76 + ◆ say yes 77 + │ ● Yes / ○ No 78 + └ 79 + ", 80 + "<cursor.backward count=999><cursor.up count=4>", 81 + "<cursor.down count=1>", 82 + "<erase.down>", 83 + "◇ say yes 84 + │ Yes", 85 + " 86 + ", 87 + "<cursor.show>", 88 + "│ 89 + ◇  ─────╮ 90 + │ │ 91 + │ true │ 92 + │ │ 93 + ├────────╯ 94 + ", 95 + ] 96 + `; 97 + 98 + exports[`jsx > Note > can render multiple children as message 1`] = ` 99 + [ 100 + "<cursor.hide>", 101 + "│ 102 + ◆ say yes 103 + │ ● Yes / ○ No 104 + └ 105 + ", 106 + "<cursor.hide>", 107 + "│ 108 + ◆ say yes again 109 + │ ● Yes / ○ No 110 + └ 111 + ", 112 + "<cursor.backward count=999><cursor.up count=4>", 113 + "<cursor.down count=1>", 114 + "<erase.down>", 115 + "◇ say yes 116 + │ Yes", 117 + " 118 + ", 119 + "<cursor.show>", 120 + "<cursor.backward count=999><cursor.up count=4>", 121 + "<cursor.down count=1>", 122 + "<erase.down>", 123 + "◇ say yes again 124 + │ Yes", 125 + " 126 + ", 127 + "<cursor.show>", 128 + "│ 129 + ◇  ─────╮ 130 + │ │ 131 + │ true │ 132 + │ true │ 133 + │ │ 134 + ├────────╯ 135 + ", 136 + ] 137 + `; 138 + 139 + exports[`jsx > Note > can render string message 1`] = ` 140 + [ 141 + "│ 142 + ◇  ────╮ 143 + │ │ 144 + │ foo │ 145 + │ │ 146 + ├───────╯ 147 + ", 148 + ] 149 + `; 150 + 151 + exports[`jsx > Password > can set custom mask 1`] = ` 152 + [ 153 + "<cursor.hide>", 154 + "│ 155 + ◆ foo 156 + │ _ 157 + └ 158 + ", 159 + "<cursor.backward count=999><cursor.up count=4>", 160 + "<cursor.down count=2>", 161 + "<erase.line><cursor.left count=1>", 162 + "│ !_", 163 + "<cursor.down count=2>", 164 + "<cursor.backward count=999><cursor.up count=4>", 165 + "<cursor.down count=2>", 166 + "<erase.line><cursor.left count=1>", 167 + "│ !!_", 168 + "<cursor.down count=2>", 169 + "<cursor.backward count=999><cursor.up count=4>", 170 + "<cursor.down count=1>", 171 + "<erase.down>", 172 + "◇ foo 173 + │ !!", 174 + " 175 + ", 176 + "<cursor.show>", 177 + ] 178 + `; 179 + 180 + exports[`jsx > Password > renders password input 1`] = ` 181 + [ 182 + "<cursor.hide>", 183 + "│ 184 + ◆ foo 185 + │ _ 186 + └ 187 + ", 188 + "<cursor.backward count=999><cursor.up count=4>", 189 + "<cursor.down count=1>", 190 + "<erase.down>", 191 + "◇ foo 192 + │", 193 + " 194 + ", 195 + "<cursor.show>", 196 + ] 197 + `; 198 + 199 + exports[`jsx > Password > renders user input 1`] = ` 200 + [ 201 + "<cursor.hide>", 202 + "│ 203 + ◆ foo 204 + │ _ 205 + └ 206 + ", 207 + "<cursor.backward count=999><cursor.up count=4>", 208 + "<cursor.down count=2>", 209 + "<erase.line><cursor.left count=1>", 210 + "│ ▪_", 211 + "<cursor.down count=2>", 212 + "<cursor.backward count=999><cursor.up count=4>", 213 + "<cursor.down count=2>", 214 + "<erase.line><cursor.left count=1>", 215 + "│ ▪▪_", 216 + "<cursor.down count=2>", 217 + "<cursor.backward count=999><cursor.up count=4>", 218 + "<cursor.down count=1>", 219 + "<erase.down>", 220 + "◇ foo 221 + │ ▪▪", 222 + " 223 + ", 224 + "<cursor.show>", 225 + ] 226 + `; 227 + 228 + exports[`jsx > Text > can set default value 1`] = ` 229 + [ 230 + "<cursor.hide>", 231 + "│ 232 + ◆ foo 233 + │ _ 234 + └ 235 + ", 236 + "<cursor.backward count=999><cursor.up count=4>", 237 + "<cursor.down count=1>", 238 + "<erase.down>", 239 + "◇ foo 240 + │ bar", 241 + " 242 + ", 243 + "<cursor.show>", 244 + ] 245 + `; 246 + 247 + exports[`jsx > Text > can set initial value 1`] = ` 248 + [ 249 + "<cursor.hide>", 250 + "│ 251 + ◆ foo 252 + │ bar█ 253 + └ 254 + ", 255 + "<cursor.backward count=999><cursor.up count=4>", 256 + "<cursor.down count=1>", 257 + "<erase.down>", 258 + "◇ foo 259 + │ bar", 260 + " 261 + ", 262 + "<cursor.show>", 263 + ] 264 + `; 265 + 266 + exports[`jsx > Text > can set placeholder 1`] = ` 267 + [ 268 + "<cursor.hide>", 269 + "│ 270 + ◆ foo 271 + │ bar 272 + └ 273 + ", 274 + "<cursor.backward count=999><cursor.up count=4>", 275 + "<cursor.down count=1>", 276 + "<erase.down>", 277 + "◇ foo 278 + │", 279 + " 280 + ", 281 + "<cursor.show>", 282 + ] 283 + `; 284 + 285 + exports[`jsx > Text > renders text input 1`] = ` 286 + [ 287 + "<cursor.hide>", 288 + "│ 289 + ◆ foo 290 + │ _ 291 + └ 292 + ", 293 + "<cursor.backward count=999><cursor.up count=4>", 294 + "<cursor.down count=1>", 295 + "<erase.down>", 296 + "◇ foo 297 + │", 298 + " 299 + ", 300 + "<cursor.show>", 301 + ] 302 + `;
+173 -2
packages/jsx/test/jsx.test.tsx
··· 1 1 import { MockReadable, MockWritable } from '@clack/test-utils'; 2 2 import { beforeEach, describe, expect, test } from 'vitest'; 3 - import { Confirm, jsx } from '../src/index.js'; 3 + import { Confirm, Note, Option, Password, Select, Text, jsx } from '../src/index.js'; 4 4 5 5 describe('jsx', () => { 6 6 let input: MockReadable; ··· 23 23 }); 24 24 25 25 test('can render JSX', async () => { 26 - const task = (<Confirm message="foo?" input={input} output={output} />); 26 + const task = <Confirm message="foo?" input={input} output={output} />; 27 27 input.emit('keypress', '', { name: 'return' }); 28 28 const result = await task; 29 29 expect(result).to.equal(true); ··· 33 33 const task = jsx('unknown-nonsense' as never, {} as never); 34 34 const result = await task; 35 35 expect(result).to.equal(null); 36 + }); 37 + 38 + describe('Confirm', () => { 39 + test('can set message', async () => { 40 + const task = <Confirm message="foo?" input={input} output={output} />; 41 + input.emit('keypress', '', { name: 'return' }); 42 + const result = await task; 43 + expect(result).to.equal(true); 44 + expect(output.buffer).toMatchSnapshot(); 45 + }); 46 + 47 + test('can set active text', async () => { 48 + const task = <Confirm message="foo?" active="DO IT" input={input} output={output} />; 49 + input.emit('keypress', '', { name: 'return' }); 50 + const result = await task; 51 + expect(result).to.equal(true); 52 + expect(output.buffer).toMatchSnapshot(); 53 + }); 54 + 55 + test('can set inactive text', async () => { 56 + const task = <Confirm message="foo?" inactive="DONT DO IT" input={input} output={output} />; 57 + input.emit('keypress', '', { name: 'return' }); 58 + const result = await task; 59 + expect(result).to.equal(true); 60 + expect(output.buffer).toMatchSnapshot(); 61 + }); 62 + }); 63 + 64 + describe('Note', () => { 65 + test('can render string message', async () => { 66 + const task = <Note message="foo" output={output} />; 67 + await task; 68 + 69 + expect(output.buffer).toMatchSnapshot(); 70 + }); 71 + 72 + test('can render children as message', async () => { 73 + const task = <Note output={output}>a message</Note>; 74 + await task; 75 + 76 + expect(output.buffer).toMatchSnapshot(); 77 + }); 78 + 79 + test('can render complex results as message', async () => { 80 + const task = ( 81 + <Note output={output}> 82 + <Confirm message="say yes" input={input} output={output} /> 83 + </Note> 84 + ); 85 + input.emit('keypress', '', { name: 'return' }); 86 + await task; 87 + 88 + expect(output.buffer).toMatchSnapshot(); 89 + }); 90 + 91 + test('can render multiple children as message', async () => { 92 + const task = ( 93 + <Note output={output}> 94 + <Confirm message="say yes" input={input} output={output} /> 95 + <Confirm message="say yes again" input={input} output={output} /> 96 + </Note> 97 + ); 98 + input.emit('keypress', '', { name: 'return' }); 99 + input.emit('keypress', '', { name: 'return' }); 100 + await task; 101 + 102 + expect(output.buffer).toMatchSnapshot(); 103 + }); 104 + }); 105 + 106 + describe('Text', () => { 107 + test('renders text input', async () => { 108 + const task = <Text message="foo" output={output} input={input} />; 109 + 110 + input.emit('keypress', '', { name: 'return' }); 111 + 112 + const result = await task; 113 + 114 + expect(result).to.equal(''); 115 + expect(output.buffer).toMatchSnapshot(); 116 + }); 117 + 118 + test('can set placeholder', async () => { 119 + const task = <Text message="foo" placeholder="bar" output={output} input={input} />; 120 + 121 + input.emit('keypress', '', { name: 'return' }); 122 + 123 + const result = await task; 124 + 125 + expect(result).to.equal(''); 126 + expect(output.buffer).toMatchSnapshot(); 127 + }); 128 + 129 + test('can set default value', async () => { 130 + const task = <Text message="foo" defaultValue="bar" output={output} input={input} />; 131 + 132 + input.emit('keypress', '', { name: 'return' }); 133 + 134 + const result = await task; 135 + 136 + expect(result).to.equal('bar'); 137 + expect(output.buffer).toMatchSnapshot(); 138 + }); 139 + 140 + test('can set initial value', async () => { 141 + const task = <Text message="foo" initialValue="bar" output={output} input={input} />; 142 + 143 + input.emit('keypress', '', { name: 'return' }); 144 + 145 + const result = await task; 146 + 147 + expect(result).to.equal('bar'); 148 + expect(output.buffer).toMatchSnapshot(); 149 + }); 150 + }); 151 + 152 + describe('Password', () => { 153 + test('renders password input', async () => { 154 + const task = <Password message="foo" output={output} input={input} />; 155 + 156 + input.emit('keypress', '', { name: 'return' }); 157 + 158 + const result = await task; 159 + 160 + expect(result).to.equal(undefined); 161 + expect(output.buffer).toMatchSnapshot(); 162 + }); 163 + 164 + test('renders user input', async () => { 165 + const task = <Password message="foo" output={output} input={input} />; 166 + 167 + input.emit('keypress', 'a', { name: 'a' }); 168 + input.emit('keypress', 'b', { name: 'b' }); 169 + input.emit('keypress', '', { name: 'return' }); 170 + 171 + const result = await task; 172 + 173 + expect(result).to.equal('ab'); 174 + expect(output.buffer).toMatchSnapshot(); 175 + }); 176 + 177 + test('can set custom mask', async () => { 178 + const task = <Password message="foo" mask="!" output={output} input={input} />; 179 + 180 + input.emit('keypress', 'a', { name: 'a' }); 181 + input.emit('keypress', 'b', { name: 'b' }); 182 + input.emit('keypress', '', { name: 'return' }); 183 + 184 + const result = await task; 185 + 186 + expect(result).to.equal('ab'); 187 + expect(output.buffer).toMatchSnapshot(); 188 + }); 189 + }); 190 + 191 + describe('Select', () => { 192 + test('renders options', async () => { 193 + const task = ( 194 + <Select message="pick one" input={input} output={output}> 195 + <Option value={303}>Option One</Option> 196 + <Option value={808}>Option Two</Option> 197 + </Select> 198 + ); 199 + 200 + input.emit('keypress', '', { name: 'return' }); 201 + 202 + const result = await task; 203 + 204 + expect(result).to.equal(303); 205 + expect(output.buffer).toMatchSnapshot(); 206 + }); 36 207 }); 37 208 });
+7
packages/jsx/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + snapshotSerializers: ['vitest-ansi-serializer'], 6 + }, 7 + });
+3
pnpm-lock.yaml
··· 86 86 vitest: 87 87 specifier: ^3.1.1 88 88 version: 3.1.1(@types/node@18.16.0) 89 + vitest-ansi-serializer: 90 + specifier: ^0.1.2 91 + version: 0.1.2(vitest@3.1.1(@types/node@18.16.0)) 89 92 90 93 packages/prompts: 91 94 dependencies: