[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: make jsx elements functions

James Garbutt (Jul 9, 2025, 4:37 PM +0100) bc903a2d c26349e9

+300 -90
+2 -2
packages/jsx/src/components/confirm.ts
··· 3 3 4 4 export type ConfirmProps = ConfirmOptions; 5 5 6 - export function Confirm(props: ConfirmProps): ReturnType<typeof confirm> { 7 - return confirm(props); 6 + export function Confirm(props: ConfirmProps): () => ReturnType<typeof confirm> { 7 + return () => confirm(props); 8 8 }
+41
packages/jsx/src/components/field.ts
··· 1 + import { isCancel } from '@clack/prompts'; 2 + import type { JSX } from '../types.js'; 3 + import { resolveChildren } from '../utils.js'; 4 + 5 + export interface FieldResult { 6 + name: PropertyKey; 7 + value: unknown; 8 + } 9 + 10 + export interface FieldProps { 11 + name: PropertyKey; 12 + children?: JSX.Element | JSX.Element[] | string; 13 + } 14 + 15 + export function Field(props: FieldProps): () => Promise<FieldResult> { 16 + return async () => { 17 + let value: unknown = undefined; 18 + 19 + if (props.children) { 20 + const resolvedChildren = await resolveChildren(props.children); 21 + const valueArr: unknown[] = []; 22 + 23 + for (const child of resolvedChildren) { 24 + if (!isCancel(child)) { 25 + valueArr.push(child); 26 + } 27 + } 28 + 29 + if (valueArr.length === 1) { 30 + value = valueArr[0]; 31 + } else { 32 + value = valueArr; 33 + } 34 + } 35 + 36 + return { 37 + name: props.name, 38 + value, 39 + }; 40 + }; 41 + }
+33
packages/jsx/src/components/form.ts
··· 1 + import { isCancel } from '@clack/prompts'; 2 + import type { JSX } from '../types.js'; 3 + import { resolveChildren } from '../utils.js'; 4 + 5 + export interface FormProps { 6 + children?: JSX.Element | JSX.Element[] | string; 7 + } 8 + 9 + function isChildLike(child: unknown): child is { name: PropertyKey; value: unknown } { 10 + return typeof child === 'object' && child !== null && 'name' in child && 'value' in child; 11 + } 12 + 13 + export function Form(props: FormProps): () => Promise<Record<PropertyKey, unknown>> { 14 + return async () => { 15 + const results: Record<PropertyKey, unknown> = {}; 16 + 17 + if (props.children) { 18 + const resolvedChildren = await resolveChildren(props.children); 19 + 20 + for (const child of resolvedChildren) { 21 + if (isCancel(child)) { 22 + continue; 23 + } 24 + 25 + if (isChildLike(child)) { 26 + results[child.name] = child.value; 27 + } 28 + } 29 + } 30 + 31 + return results; 32 + }; 33 + }
+17 -15
packages/jsx/src/components/note.ts
··· 9 9 title?: string; 10 10 } 11 11 12 - export async function Note(props: NoteProps): Promise<void> { 13 - let message = ''; 12 + export function Note(props: NoteProps): () => Promise<void> { 13 + return async () => { 14 + let message = ''; 14 15 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; 16 + if (props.children) { 17 + const messages: string[] = []; 18 + const children = await resolveChildren(props.children); 19 + for (const child of children) { 20 + // TODO (43081j): handle cancelling of children 21 + if (isCancel(child)) { 22 + continue; 23 + } 24 + messages.push(String(child)); 22 25 } 23 - messages.push(String(child)); 26 + message = messages.join('\n'); 27 + } else if (props.message) { 28 + message = props.message; 24 29 } 25 - message = messages.join('\n'); 26 - } else if (props.message) { 27 - message = props.message; 28 - } 29 30 30 - note(message, props.title, props); 31 + note(message, props.title, props); 32 + }; 31 33 }
+18 -16
packages/jsx/src/components/option.ts
··· 8 8 children?: JSX.Element | JSX.Element[] | string; 9 9 } 10 10 11 - export async function Option<T>(props: OptionProps<T>): Promise<PromptOption<T>> { 12 - const { children, ...opts } = props; 11 + export function Option<T>(props: OptionProps<T>): () => Promise<PromptOption<T>> { 12 + return async () => { 13 + const { children, ...opts } = props; 13 14 14 - if (children) { 15 - const resolvedChildren = await resolveChildren(children); 16 - const childStrings: string[] = []; 15 + if (children) { 16 + const resolvedChildren = await resolveChildren(children); 17 + const childStrings: string[] = []; 17 18 18 - for (const child of resolvedChildren) { 19 - if (isCancel(child)) { 20 - continue; 19 + for (const child of resolvedChildren) { 20 + if (isCancel(child)) { 21 + continue; 22 + } 23 + childStrings.push(String(child)); 21 24 } 22 - childStrings.push(String(child)); 25 + 26 + return { 27 + ...opts, 28 + label: childStrings.join('\n'), 29 + } as PromptOption<T>; 23 30 } 24 31 25 - return { 26 - ...opts, 27 - label: childStrings.join('\n'), 28 - } as PromptOption<T>; 29 - } 30 - 31 - return opts as PromptOption<T>; 32 + return opts as PromptOption<T>; 33 + }; 32 34 }
+2 -2
packages/jsx/src/components/password.ts
··· 3 3 4 4 export type PasswordProps = PasswordOptions; 5 5 6 - export function Password(props: PasswordProps): ReturnType<typeof password> { 7 - return password(props); 6 + export function Password(props: PasswordProps): () => ReturnType<typeof password> { 7 + return () => password(props); 8 8 }
+14 -12
packages/jsx/src/components/select.ts
··· 11 11 return obj !== null && typeof obj === 'object' && Object.hasOwnProperty.call(obj, 'value'); 12 12 }; 13 13 14 - export async function Select(props: SelectProps): ReturnType<typeof select> { 15 - const { children, ...opts } = props; 16 - const options: Option<unknown>[] = []; 17 - const resolvedChildren = await resolveChildren(props.children); 14 + export function Select(props: SelectProps): () => ReturnType<typeof select> { 15 + return async () => { 16 + const { children, ...opts } = props; 17 + const options: Option<unknown>[] = []; 18 + const resolvedChildren = await resolveChildren(props.children); 18 19 19 - for (const child of resolvedChildren) { 20 - if (isOptionLike(child)) { 21 - options.push(child); 20 + for (const child of resolvedChildren) { 21 + if (isOptionLike(child)) { 22 + options.push(child); 23 + } 22 24 } 23 - } 24 25 25 - return select({ 26 - ...opts, 27 - options, 28 - }); 26 + return select({ 27 + ...opts, 28 + options, 29 + }); 30 + }; 29 31 }
+2 -2
packages/jsx/src/components/text.ts
··· 3 3 4 4 export type TextProps = TextOptions; 5 5 6 - export function Text(props: TextProps): ReturnType<typeof text> { 7 - return text(props); 6 + export function Text(props: TextProps): () => ReturnType<typeof text> { 7 + return () => text(props); 8 8 }
+8 -2
packages/jsx/src/index.ts
··· 1 1 import { Confirm, type ConfirmProps } from './components/confirm.js'; 2 + import { Field, type FieldProps } from './components/field.js'; 3 + import { Form, type FormProps } from './components/form.js'; 2 4 import { Note, type NoteProps } from './components/note.js'; 3 5 import { Option, type OptionProps } from './components/option.js'; 4 6 import { Password, type PasswordProps } from './components/password.js'; ··· 20 22 type OptionProps, 21 23 Select, 22 24 type SelectProps, 25 + Field, 26 + type FieldProps, 27 + Form, 28 + type FormProps, 23 29 }; 24 30 25 31 export function Fragment(props: { children: JSX.Element | JSX.Element[] }): JSX.Element { 26 - return Promise.resolve(props.children); 32 + return () => Promise.resolve(props.children); 27 33 } 28 34 29 35 export type Component = ··· 44 50 if (typeof tagOrFn === 'function') { 45 51 return (tagOrFn as (props: unknown) => JSX.Element)(props); 46 52 } 47 - return Promise.resolve(null); 53 + return () => Promise.resolve(null); 48 54 } 49 55 50 56 export { jsx };
+1 -1
packages/jsx/src/types.ts
··· 1 1 namespace JSX { 2 2 export type IntrinsicElements = never; 3 3 4 - export type Element = Promise<unknown>; 4 + export type Element = () => Promise<unknown>; 5 5 } 6 6 7 7 export type { JSX };
+1 -1
packages/jsx/src/utils.ts
··· 7 7 const results: unknown[] = []; 8 8 9 9 for (const child of arr) { 10 - const result = await child; 10 + const result = typeof child === 'string' ? child : await child(); 11 11 12 12 results.push(result); 13 13 }
+73
packages/jsx/test/components/__snapshots__/field.test.tsx.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`Field > renders and resolves children 1`] = ` 4 + [ 5 + "<cursor.hide>", 6 + "│ 7 + ◆ enter some text 8 + │ _ 9 + └ 10 + ", 11 + "<cursor.backward count=999><cursor.up count=4>", 12 + "<cursor.down count=2>", 13 + "<erase.line><cursor.left count=1>", 14 + "│ a█", 15 + "<cursor.down count=2>", 16 + "<cursor.backward count=999><cursor.up count=4>", 17 + "<cursor.down count=2>", 18 + "<erase.line><cursor.left count=1>", 19 + "│ ab█", 20 + "<cursor.down count=2>", 21 + "<cursor.backward count=999><cursor.up count=4>", 22 + "<cursor.down count=1>", 23 + "<erase.down>", 24 + "◇ enter some text 25 + │ ab", 26 + " 27 + ", 28 + "<cursor.show>", 29 + ] 30 + `; 31 + 32 + exports[`Field > resolves multiple children into array 1`] = ` 33 + [ 34 + "<cursor.hide>", 35 + "│ 36 + ◆ enter some text 37 + │ _ 38 + └ 39 + ", 40 + "<cursor.backward count=999><cursor.up count=4>", 41 + "<cursor.down count=2>", 42 + "<erase.line><cursor.left count=1>", 43 + "│ a█", 44 + "<cursor.down count=2>", 45 + "<cursor.backward count=999><cursor.up count=4>", 46 + "<cursor.down count=1>", 47 + "<erase.down>", 48 + "◇ enter some text 49 + │ a", 50 + " 51 + ", 52 + "<cursor.show>", 53 + "<cursor.hide>", 54 + "│ 55 + ◆ enter some more text 56 + │ _ 57 + └ 58 + ", 59 + "<cursor.backward count=999><cursor.up count=4>", 60 + "<cursor.down count=2>", 61 + "<erase.line><cursor.left count=1>", 62 + "│ b█", 63 + "<cursor.down count=2>", 64 + "<cursor.backward count=999><cursor.up count=4>", 65 + "<cursor.down count=1>", 66 + "<erase.down>", 67 + "◇ enter some more text 68 + │ b", 69 + " 70 + ", 71 + "<cursor.show>", 72 + ] 73 + `;
+6 -6
packages/jsx/test/components/__snapshots__/note.test.tsx.snap
··· 46 46 │ ● Yes / ○ No 47 47 └ 48 48 ", 49 - "<cursor.hide>", 50 - "│ 51 - ◆ say yes again 52 - │ ● Yes / ○ No 53 - └ 54 - ", 55 49 "<cursor.backward count=999><cursor.up count=4>", 56 50 "<cursor.down count=1>", 57 51 "<erase.down>", ··· 60 54 " 61 55 ", 62 56 "<cursor.show>", 57 + "<cursor.hide>", 58 + "│ 59 + ◆ say yes again 60 + │ ● Yes / ○ No 61 + └ 62 + ", 63 63 "<cursor.backward count=999><cursor.up count=4>", 64 64 "<cursor.down count=1>", 65 65 "<erase.down>",
+3 -3
packages/jsx/test/components/confirm.test.tsx
··· 12 12 }); 13 13 14 14 test('can set message', async () => { 15 - const task = <Confirm message="foo?" input={input} output={output} />; 15 + const task = (<Confirm message="foo?" input={input} output={output} />)(); 16 16 input.emit('keypress', '', { name: 'return' }); 17 17 const result = await task; 18 18 expect(result).to.equal(true); ··· 20 20 }); 21 21 22 22 test('can set active text', async () => { 23 - const task = <Confirm message="foo?" active="DO IT" input={input} output={output} />; 23 + const task = (<Confirm message="foo?" active="DO IT" input={input} output={output} />)(); 24 24 input.emit('keypress', '', { name: 'return' }); 25 25 const result = await task; 26 26 expect(result).to.equal(true); ··· 28 28 }); 29 29 30 30 test('can set inactive text', async () => { 31 - const task = <Confirm message="foo?" inactive="DONT DO IT" input={input} output={output} />; 31 + const task = (<Confirm message="foo?" inactive="DONT DO IT" input={input} output={output} />)(); 32 32 input.emit('keypress', '', { name: 'return' }); 33 33 const result = await task; 34 34 expect(result).to.equal(true);
+56
packages/jsx/test/components/field.test.tsx
··· 1 + import { MockReadable, MockWritable, nextTick } from '@clack/test-utils'; 2 + import { beforeEach, describe, expect, test } from 'vitest'; 3 + import { Field, Text } from '../../src/index.js'; 4 + 5 + describe('Field', () => { 6 + let input: MockReadable; 7 + let output: MockWritable; 8 + 9 + beforeEach(() => { 10 + input = new MockReadable(); 11 + output = new MockWritable(); 12 + }); 13 + 14 + test('renders and resolves children', async () => { 15 + const task = ( 16 + <Field name="foo"> 17 + <Text message="enter some text" output={output} input={input} /> 18 + </Field> 19 + )(); 20 + 21 + input.emit('keypress', 'a', { name: 'a' }); 22 + input.emit('keypress', 'b', { name: 'b' }); 23 + input.emit('keypress', '', { name: 'return' }); 24 + 25 + const result = await task; 26 + 27 + expect(result).to.deep.equal({ 28 + name: 'foo', 29 + value: 'ab', 30 + }); 31 + expect(output.buffer).toMatchSnapshot(); 32 + }); 33 + 34 + test('resolves multiple children into array', async () => { 35 + const task = ( 36 + <Field name="foo"> 37 + <Text message="enter some text" output={output} input={input} /> 38 + <Text message="enter some more text" output={output} input={input} /> 39 + </Field> 40 + )(); 41 + 42 + input.emit('keypress', 'a', { name: 'a' }); 43 + input.emit('keypress', '', { name: 'return' }); 44 + await nextTick(); 45 + input.emit('keypress', 'b', { name: 'b' }); 46 + input.emit('keypress', '', { name: 'return' }); 47 + 48 + const result = await task; 49 + 50 + expect(result).to.deep.equal({ 51 + name: 'foo', 52 + value: ['a', 'b'], 53 + }); 54 + expect(output.buffer).toMatchSnapshot(); 55 + }); 56 + });
+6 -5
packages/jsx/test/components/note.test.tsx
··· 1 - import { MockReadable, MockWritable } from '@clack/test-utils'; 1 + import { MockReadable, MockWritable, nextTick } from '@clack/test-utils'; 2 2 import { beforeEach, describe, expect, test } from 'vitest'; 3 3 import { Confirm, Note } from '../../src/index.js'; 4 4 ··· 13 13 14 14 test('can render string message', async () => { 15 15 const task = <Note message="foo" output={output} />; 16 - await task; 16 + await task(); 17 17 18 18 expect(output.buffer).toMatchSnapshot(); 19 19 }); 20 20 21 21 test('can render children as message', async () => { 22 22 const task = <Note output={output}>a message</Note>; 23 - await task; 23 + await task(); 24 24 25 25 expect(output.buffer).toMatchSnapshot(); 26 26 }); ··· 30 30 <Note output={output}> 31 31 <Confirm message="say yes" input={input} output={output} /> 32 32 </Note> 33 - ); 33 + )(); 34 34 input.emit('keypress', '', { name: 'return' }); 35 35 await task; 36 36 ··· 43 43 <Confirm message="say yes" input={input} output={output} /> 44 44 <Confirm message="say yes again" input={input} output={output} /> 45 45 </Note> 46 - ); 46 + )(); 47 47 input.emit('keypress', '', { name: 'return' }); 48 + await nextTick(); 48 49 input.emit('keypress', '', { name: 'return' }); 49 50 await task; 50 51
+3 -3
packages/jsx/test/components/password.test.tsx
··· 12 12 }); 13 13 14 14 test('renders password input', async () => { 15 - const task = <Password message="foo" output={output} input={input} />; 15 + const task = (<Password message="foo" output={output} input={input} />)(); 16 16 17 17 input.emit('keypress', '', { name: 'return' }); 18 18 ··· 23 23 }); 24 24 25 25 test('renders user input', async () => { 26 - const task = <Password message="foo" output={output} input={input} />; 26 + const task = (<Password message="foo" output={output} input={input} />)(); 27 27 28 28 input.emit('keypress', 'a', { name: 'a' }); 29 29 input.emit('keypress', 'b', { name: 'b' }); ··· 36 36 }); 37 37 38 38 test('can set custom mask', async () => { 39 - const task = <Password message="foo" mask="!" output={output} input={input} />; 39 + const task = (<Password message="foo" mask="!" output={output} input={input} />)(); 40 40 41 41 input.emit('keypress', 'a', { name: 'a' }); 42 42 input.emit('keypress', 'b', { name: 'b' });
+7 -13
packages/jsx/test/components/select.test.tsx
··· 1 - import { MockReadable, MockWritable } from '@clack/test-utils'; 1 + import { MockReadable, MockWritable, nextTick } from '@clack/test-utils'; 2 2 import { beforeEach, describe, expect, test } from 'vitest'; 3 3 import { Option, Select } from '../../src/index.js'; 4 4 ··· 17 17 <Option value={'opt0'} /> 18 18 <Option value={'opt1'} /> 19 19 </Select> 20 - ); 21 - 22 - // wait a tick... sad times 23 - await new Promise((res) => setTimeout(res, 0)); 20 + )(); 24 21 22 + await nextTick(); 25 23 input.emit('keypress', '', { name: 'return' }); 26 24 27 25 const result = await task; ··· 36 34 <Option value={303}>Three o three</Option> 37 35 <Option value={808}>Eight o eight</Option> 38 36 </Select> 39 - ); 40 - 41 - // wait a tick... sad times 42 - await new Promise((res) => setTimeout(res, 0)); 37 + )(); 43 38 39 + await nextTick(); 44 40 input.emit('keypress', '', { name: 'return' }); 45 41 46 42 const result = await task; ··· 59 55 Eight o eight 60 56 </Option> 61 57 </Select> 62 - ); 63 - 64 - // wait a tick... sad times 65 - await new Promise((res) => setTimeout(res, 0)); 58 + )(); 66 59 60 + await nextTick(); 67 61 input.emit('keypress', '', { name: 'return' }); 68 62 69 63 const result = await task;
+4 -4
packages/jsx/test/components/text.test.tsx
··· 12 12 }); 13 13 14 14 test('renders text input', async () => { 15 - const task = <Text message="foo" output={output} input={input} />; 15 + const task = (<Text message="foo" output={output} input={input} />)(); 16 16 17 17 input.emit('keypress', '', { name: 'return' }); 18 18 ··· 23 23 }); 24 24 25 25 test('can set placeholder', async () => { 26 - const task = <Text message="foo" placeholder="bar" output={output} input={input} />; 26 + const task = (<Text message="foo" placeholder="bar" output={output} input={input} />)(); 27 27 28 28 input.emit('keypress', '', { name: 'return' }); 29 29 ··· 34 34 }); 35 35 36 36 test('can set default value', async () => { 37 - const task = <Text message="foo" defaultValue="bar" output={output} input={input} />; 37 + const task = (<Text message="foo" defaultValue="bar" output={output} input={input} />)(); 38 38 39 39 input.emit('keypress', '', { name: 'return' }); 40 40 ··· 45 45 }); 46 46 47 47 test('can set initial value', async () => { 48 - const task = <Text message="foo" initialValue="bar" output={output} input={input} />; 48 + const task = (<Text message="foo" initialValue="bar" output={output} input={input} />)(); 49 49 50 50 input.emit('keypress', '', { name: 'return' }); 51 51
+3 -3
packages/jsx/test/jsx.test.tsx
··· 16 16 message: 'foo?', 17 17 input, 18 18 output, 19 - }); 19 + })(); 20 20 input.emit('keypress', '', { name: 'return' }); 21 21 const result = await task; 22 22 expect(result).to.equal(true); 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); ··· 31 31 32 32 test('unknown elements are null', async () => { 33 33 const task = jsx('unknown-nonsense' as never, {} as never); 34 - const result = await task; 34 + const result = await task(); 35 35 expect(result).to.equal(null); 36 36 }); 37 37 });