[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: rework it all to use render() functions

This way we can keep an ast-like thingymajig around until we want to
actually render.

James Garbutt (Jul 13, 2025, 1:51 PM +0100) 36e756b7 4d639194

+227 -145
+10 -2
packages/jsx/src/components/confirm.ts
··· 1 1 import type { ConfirmOptions } from '@clack/prompts'; 2 2 import { confirm } from '@clack/prompts'; 3 + import type { JSX } from '../types.js'; 3 4 4 5 export type ConfirmProps = ConfirmOptions; 5 6 6 - export function Confirm(props: ConfirmProps): () => ReturnType<typeof confirm> { 7 - return () => confirm(props); 7 + export function Confirm(props: ConfirmProps): JSX.Element { 8 + return { 9 + render: (options) => 10 + confirm({ 11 + input: options?.input, 12 + output: options?.output, 13 + ...props, 14 + }), 15 + }; 8 16 }
+21 -19
packages/jsx/src/components/field.ts
··· 12 12 children?: JSX.Element | JSX.Element[] | string; 13 13 } 14 14 15 - export function Field(props: FieldProps): () => Promise<FieldResult> { 16 - return async () => { 17 - let value: unknown = undefined; 15 + export function Field(props: FieldProps): JSX.Element { 16 + return { 17 + render: async (options) => { 18 + let value: unknown = undefined; 18 19 19 - if (props.children) { 20 - const resolvedChildren = await resolveChildren(props.children); 21 - const valueArr: unknown[] = []; 20 + if (props.children) { 21 + const resolvedChildren = await resolveChildren(props.children, options); 22 + const valueArr: unknown[] = []; 22 23 23 - for (const child of resolvedChildren) { 24 - if (!isCancel(child)) { 25 - valueArr.push(child); 24 + for (const child of resolvedChildren) { 25 + if (!isCancel(child)) { 26 + valueArr.push(child); 27 + } 26 28 } 27 - } 28 29 29 - if (valueArr.length === 1) { 30 - value = valueArr[0]; 31 - } else { 32 - value = valueArr; 30 + if (valueArr.length === 1) { 31 + value = valueArr[0]; 32 + } else { 33 + value = valueArr; 34 + } 33 35 } 34 - } 35 36 36 - return { 37 - name: props.name, 38 - value, 39 - }; 37 + return { 38 + name: props.name, 39 + value, 40 + }; 41 + }, 40 42 }; 41 43 }
+15 -13
packages/jsx/src/components/form.ts
··· 10 10 return typeof child === 'object' && child !== null && 'name' in child && 'value' in child; 11 11 } 12 12 13 - export function Form(props: FormProps): () => Promise<Record<PropertyKey, unknown>> { 14 - return async () => { 15 - const results: Record<PropertyKey, unknown> = {}; 13 + export function Form(props: FormProps): JSX.Element { 14 + return { 15 + render: async (options) => { 16 + const results: Record<PropertyKey, unknown> = {}; 16 17 17 - if (props.children) { 18 - const resolvedChildren = await resolveChildren(props.children); 18 + if (props.children) { 19 + const resolvedChildren = await resolveChildren(props.children, options); 19 20 20 - for (const child of resolvedChildren) { 21 - if (isCancel(child)) { 22 - continue; 23 - } 21 + for (const child of resolvedChildren) { 22 + if (isCancel(child)) { 23 + continue; 24 + } 24 25 25 - if (isChildLike(child)) { 26 - results[child.name] = child.value; 26 + if (isChildLike(child)) { 27 + results[child.name] = child.value; 28 + } 27 29 } 28 30 } 29 - } 30 31 31 - return results; 32 + return results; 33 + }, 32 34 }; 33 35 }
+22 -16
packages/jsx/src/components/note.ts
··· 9 9 title?: string; 10 10 } 11 11 12 - export function Note(props: NoteProps): () => Promise<void> { 13 - return async () => { 14 - let message = ''; 12 + export function Note(props: NoteProps): JSX.Element { 13 + return { 14 + render: async (options) => { 15 + let message = ''; 15 16 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; 17 + if (props.children) { 18 + const messages: string[] = []; 19 + const children = await resolveChildren(props.children, options); 20 + for (const child of children) { 21 + // TODO (43081j): handle cancelling of children 22 + if (isCancel(child)) { 23 + continue; 24 + } 25 + messages.push(String(child)); 23 26 } 24 - messages.push(String(child)); 27 + message = messages.join('\n'); 28 + } else if (props.message) { 29 + message = props.message; 25 30 } 26 - message = messages.join('\n'); 27 - } else if (props.message) { 28 - message = props.message; 29 - } 30 31 31 - note(message, props.title, props); 32 + note(message, props.title, { 33 + input: options?.input, 34 + output: options?.output, 35 + ...props, 36 + }); 37 + }, 32 38 }; 33 39 }
+19 -17
packages/jsx/src/components/option.ts
··· 8 8 children?: JSX.Element | JSX.Element[] | string; 9 9 } 10 10 11 - export function Option<T>(props: OptionProps<T>): () => Promise<PromptOption<T>> { 12 - return async () => { 13 - const { children, ...opts } = props; 11 + export function Option<T>(props: OptionProps<T>): JSX.Element { 12 + return { 13 + render: async (options) => { 14 + const { children, ...opts } = props; 14 15 15 - if (children) { 16 - const resolvedChildren = await resolveChildren(children); 17 - const childStrings: string[] = []; 16 + if (children) { 17 + const resolvedChildren = await resolveChildren(children, options); 18 + const childStrings: string[] = []; 18 19 19 - for (const child of resolvedChildren) { 20 - if (isCancel(child)) { 21 - continue; 20 + for (const child of resolvedChildren) { 21 + if (isCancel(child)) { 22 + continue; 23 + } 24 + childStrings.push(String(child)); 22 25 } 23 - childStrings.push(String(child)); 24 - } 25 26 26 - return { 27 - ...opts, 28 - label: childStrings.join('\n'), 29 - } as PromptOption<T>; 30 - } 27 + return { 28 + ...opts, 29 + label: childStrings.join('\n'), 30 + } as PromptOption<T>; 31 + } 31 32 32 - return opts as PromptOption<T>; 33 + return opts as PromptOption<T>; 34 + }, 33 35 }; 34 36 }
+10 -2
packages/jsx/src/components/password.ts
··· 1 1 import type { PasswordOptions } from '@clack/prompts'; 2 2 import { password } from '@clack/prompts'; 3 + import type { JSX } from '../types.js'; 3 4 4 5 export type PasswordProps = PasswordOptions; 5 6 6 - export function Password(props: PasswordProps): () => ReturnType<typeof password> { 7 - return () => password(props); 7 + export function Password(props: PasswordProps): JSX.Element { 8 + return { 9 + render: (options) => 10 + password({ 11 + input: options?.input, 12 + output: options?.output, 13 + ...props, 14 + }), 15 + }; 8 16 }
+17 -13
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 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); 14 + export function Select(props: SelectProps): JSX.Element { 15 + return { 16 + render: async (renderOptions) => { 17 + const { children, ...opts } = props; 18 + const options: Option<unknown>[] = []; 19 + const resolvedChildren = await resolveChildren(props.children, renderOptions); 19 20 20 - for (const child of resolvedChildren) { 21 - if (isOptionLike(child)) { 22 - options.push(child); 21 + for (const child of resolvedChildren) { 22 + if (isOptionLike(child)) { 23 + options.push(child); 24 + } 23 25 } 24 - } 25 26 26 - return select({ 27 - ...opts, 28 - options, 29 - }); 27 + return select({ 28 + input: renderOptions?.input, 29 + output: renderOptions?.output, 30 + ...opts, 31 + options, 32 + }); 33 + }, 30 34 }; 31 35 }
+10 -2
packages/jsx/src/components/text.ts
··· 1 1 import type { TextOptions } from '@clack/prompts'; 2 2 import { text } from '@clack/prompts'; 3 + import type { JSX } from '../types.js'; 3 4 4 5 export type TextProps = TextOptions; 5 6 6 - export function Text(props: TextProps): () => ReturnType<typeof text> { 7 - return () => text(props); 7 + export function Text(props: TextProps): JSX.Element { 8 + return { 9 + render: (options) => 10 + text({ 11 + input: options?.input, 12 + output: options?.output, 13 + ...props, 14 + }), 15 + }; 8 16 }
+16 -4
packages/jsx/src/index.ts
··· 6 6 import { Password, type PasswordProps } from './components/password.js'; 7 7 import { Select, type SelectProps } from './components/select.js'; 8 8 import { Text, type TextProps } from './components/text.js'; 9 - import type { JSX } from './types.js'; 9 + import type { JSX, RenderFunction, RenderOptions } from './types.js'; 10 10 11 11 export type { JSX }; 12 12 export { ··· 29 29 }; 30 30 31 31 export function Fragment(props: { children: JSX.Element | JSX.Element[] }): JSX.Element { 32 - return () => Promise.resolve(props.children); 32 + return { 33 + render: () => Promise.resolve(props.children), 34 + }; 33 35 } 34 36 35 37 export type Component = ··· 47 49 ): JSX.Element; 48 50 function jsx<T extends Component>(fn: T, props: Parameters<T>[0], _key?: string): JSX.Element; 49 51 function jsx(tagOrFn: string | Component, props: unknown, _key?: string): JSX.Element { 52 + let render: RenderFunction; 50 53 if (typeof tagOrFn === 'function') { 51 - return (tagOrFn as (props: unknown) => JSX.Element)(props); 54 + const renderFn = (tagOrFn as (props: unknown) => JSX.Element)(props); 55 + render = (options) => renderFn.render(options); 56 + } else { 57 + render = () => Promise.resolve(null); 52 58 } 53 - return () => Promise.resolve(null); 59 + return { 60 + render, 61 + }; 54 62 } 55 63 56 64 export { jsx }; 57 65 export const jsxDEV = jsx; 66 + 67 + export async function render(node: JSX.Element, options?: RenderOptions): Promise<void> { 68 + await node.render(options); 69 + }
+9 -1
packages/jsx/src/types.ts
··· 1 + import type { CommonOptions } from '@clack/prompts'; 2 + 3 + export interface RenderOptions extends CommonOptions {} 4 + 5 + export type RenderFunction = (options?: RenderOptions) => Promise<unknown>; 6 + 1 7 namespace JSX { 2 8 export type IntrinsicElements = never; 3 9 4 - export type Element = () => Promise<unknown>; 10 + export type Element = { 11 + render: RenderFunction; 12 + }; 5 13 } 6 14 7 15 export type { JSX };
+4 -3
packages/jsx/src/utils.ts
··· 1 - import type { JSX } from './types.js'; 1 + import type { JSX, RenderOptions } from './types.js'; 2 2 3 3 export async function resolveChildren( 4 - children: JSX.Element[] | JSX.Element | string 4 + children: JSX.Element[] | JSX.Element | string, 5 + options?: RenderOptions 5 6 ): Promise<unknown[]> { 6 7 const arr = Array.isArray(children) ? children : [children]; 7 8 const results: unknown[] = []; 8 9 9 10 for (const child of arr) { 10 - const result = typeof child === 'string' ? child : await child(); 11 + const result = typeof child === 'string' ? child : await child.render(options); 11 12 12 13 results.push(result); 13 14 }
+6 -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 element = <Confirm message="foo?" />; 16 + const task = element.render({ input, output }); 16 17 input.emit('keypress', '', { name: 'return' }); 17 18 const result = await task; 18 19 expect(result).to.equal(true); ··· 20 21 }); 21 22 22 23 test('can set active text', async () => { 23 - const task = (<Confirm message="foo?" active="DO IT" input={input} output={output} />)(); 24 + const element = <Confirm message="foo?" active="DO IT" input={input} output={output} />; 25 + const task = element.render({ input, output }); 24 26 input.emit('keypress', '', { name: 'return' }); 25 27 const result = await task; 26 28 expect(result).to.equal(true); ··· 28 30 }); 29 31 30 32 test('can set inactive text', async () => { 31 - const task = (<Confirm message="foo?" inactive="DONT DO IT" input={input} output={output} />)(); 33 + const element = <Confirm message="foo?" inactive="DONT DO IT" />; 34 + const task = element.render({ input, output }); 32 35 input.emit('keypress', '', { name: 'return' }); 33 36 const result = await task; 34 37 expect(result).to.equal(true);
+9 -7
packages/jsx/test/components/field.test.tsx
··· 12 12 }); 13 13 14 14 test('renders and resolves children', async () => { 15 - const task = ( 15 + const element = ( 16 16 <Field name="foo"> 17 - <Text message="enter some text" output={output} input={input} /> 17 + <Text message="enter some text" /> 18 18 </Field> 19 - )(); 19 + ); 20 + const task = element.render({ input, output }); 20 21 21 22 input.emit('keypress', 'a', { name: 'a' }); 22 23 input.emit('keypress', 'b', { name: 'b' }); ··· 32 33 }); 33 34 34 35 test('resolves multiple children into array', async () => { 35 - const task = ( 36 + const element = ( 36 37 <Field name="foo"> 37 - <Text message="enter some text" output={output} input={input} /> 38 - <Text message="enter some more text" output={output} input={input} /> 38 + <Text message="enter some text" /> 39 + <Text message="enter some more text" /> 39 40 </Field> 40 - )(); 41 + ); 42 + const task = element.render({ input, output }); 41 43 42 44 input.emit('keypress', 'a', { name: 'a' }); 43 45 input.emit('keypress', '', { name: 'return' });
+9 -7
packages/jsx/test/components/form.test.tsx
··· 12 12 }); 13 13 14 14 test('renders and resolves object', async () => { 15 - const task = ( 15 + const element = ( 16 16 <Form> 17 17 <Field name="foo"> 18 - <Text message="enter some text" output={output} input={input} /> 18 + <Text message="enter some text" /> 19 19 </Field> 20 20 </Form> 21 - )(); 21 + ); 22 + const task = element.render({ input, output }); 22 23 23 24 input.emit('keypress', 'a', { name: 'a' }); 24 25 input.emit('keypress', 'b', { name: 'b' }); ··· 32 33 expect(output.buffer).toMatchSnapshot(); 33 34 }); 34 35 test('renders and resolves multiple fields', async () => { 35 - const task = ( 36 + const element = ( 36 37 <Form> 37 38 <Field name="foo"> 38 - <Text message="enter some text" output={output} input={input} /> 39 + <Text message="enter some text" /> 39 40 </Field> 40 41 <Field name="bar"> 41 - <Text message="enter some other text" output={output} input={input} /> 42 + <Text message="enter some other text" /> 42 43 </Field> 43 44 </Form> 44 - )(); 45 + ); 46 + const task = element.render({ input, output }); 45 47 46 48 input.emit('keypress', 'a', { name: 'a' }); 47 49 input.emit('keypress', '', { name: 'return' });
+17 -13
packages/jsx/test/components/note.test.tsx
··· 12 12 }); 13 13 14 14 test('can render string message', async () => { 15 - const task = <Note message="foo" output={output} />; 16 - await task(); 15 + const element = <Note message="foo" />; 16 + const task = element.render({ output }); 17 + await task; 17 18 18 19 expect(output.buffer).toMatchSnapshot(); 19 20 }); 20 21 21 22 test('can render children as message', async () => { 22 - const task = <Note output={output}>a message</Note>; 23 - await task(); 23 + const element = <Note>a message</Note>; 24 + const task = element.render({ output }); 25 + await task; 24 26 25 27 expect(output.buffer).toMatchSnapshot(); 26 28 }); 27 29 28 30 test('can render complex results as message', async () => { 29 - const task = ( 30 - <Note output={output}> 31 - <Confirm message="say yes" input={input} output={output} /> 31 + const element = ( 32 + <Note> 33 + <Confirm message="say yes" /> 32 34 </Note> 33 - )(); 35 + ); 36 + const task = element.render({ input, output }); 34 37 input.emit('keypress', '', { name: 'return' }); 35 38 await task; 36 39 ··· 38 41 }); 39 42 40 43 test('can render multiple children as message', async () => { 41 - const task = ( 42 - <Note output={output}> 43 - <Confirm message="say yes" input={input} output={output} /> 44 - <Confirm message="say yes again" input={input} output={output} /> 44 + const element = ( 45 + <Note> 46 + <Confirm message="say yes" /> 47 + <Confirm message="say yes again" /> 45 48 </Note> 46 - )(); 49 + ); 50 + const task = element.render({ input, output }); 47 51 input.emit('keypress', '', { name: 'return' }); 48 52 await nextTick(); 49 53 input.emit('keypress', '', { name: 'return' });
+6 -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 element = <Password message="foo" />; 16 + const task = element.render({ input, output }); 16 17 17 18 input.emit('keypress', '', { name: 'return' }); 18 19 ··· 23 24 }); 24 25 25 26 test('renders user input', async () => { 26 - const task = (<Password message="foo" output={output} input={input} />)(); 27 + const element = <Password message="foo" />; 28 + const task = element.render({ input, output }); 27 29 28 30 input.emit('keypress', 'a', { name: 'a' }); 29 31 input.emit('keypress', 'b', { name: 'b' }); ··· 36 38 }); 37 39 38 40 test('can set custom mask', async () => { 39 - const task = (<Password message="foo" mask="!" output={output} input={input} />)(); 41 + const element = <Password message="foo" mask="!" />; 42 + const task = element.render({ input, output }); 40 43 41 44 input.emit('keypress', 'a', { name: 'a' }); 42 45 input.emit('keypress', 'b', { name: 'b' });
+12 -9
packages/jsx/test/components/select.test.tsx
··· 12 12 }); 13 13 14 14 test('renders options', async () => { 15 - const task = ( 16 - <Select message="foo" input={input} output={output}> 15 + const element = ( 16 + <Select message="foo"> 17 17 <Option value={'opt0'} /> 18 18 <Option value={'opt1'} /> 19 19 </Select> 20 - )(); 20 + ); 21 + const task = element.render({ input, output }); 21 22 22 23 await nextTick(); 23 24 input.emit('keypress', '', { name: 'return' }); ··· 29 30 }); 30 31 31 32 test('renders options with labels', async () => { 32 - const task = ( 33 - <Select message="foo" input={input} output={output}> 33 + const element = ( 34 + <Select message="foo"> 34 35 <Option value={303}>Three o three</Option> 35 36 <Option value={808}>Eight o eight</Option> 36 37 </Select> 37 - )(); 38 + ); 39 + const task = element.render({ input, output }); 38 40 39 41 await nextTick(); 40 42 input.emit('keypress', '', { name: 'return' }); ··· 46 48 }); 47 49 48 50 test('renders options with hints', async () => { 49 - const task = ( 50 - <Select message="foo" input={input} output={output}> 51 + const element = ( 52 + <Select message="foo"> 51 53 <Option value={303} hint="hint one"> 52 54 Three o three 53 55 </Option> ··· 55 57 Eight o eight 56 58 </Option> 57 59 </Select> 58 - )(); 60 + ); 61 + const task = element.render({ input, output }); 59 62 60 63 await nextTick(); 61 64 input.emit('keypress', '', { name: 'return' });
+8 -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 element = <Text message="foo" />; 16 + const task = element.render({ input, output }); 16 17 17 18 input.emit('keypress', '', { name: 'return' }); 18 19 ··· 23 24 }); 24 25 25 26 test('can set placeholder', async () => { 26 - const task = (<Text message="foo" placeholder="bar" output={output} input={input} />)(); 27 + const element = <Text message="foo" placeholder="bar" />; 28 + const task = element.render({ input, output }); 27 29 28 30 input.emit('keypress', '', { name: 'return' }); 29 31 ··· 34 36 }); 35 37 36 38 test('can set default value', async () => { 37 - const task = (<Text message="foo" defaultValue="bar" output={output} input={input} />)(); 39 + const element = <Text message="foo" defaultValue="bar" />; 40 + const task = element.render({ input, output }); 38 41 39 42 input.emit('keypress', '', { name: 'return' }); 40 43 ··· 45 48 }); 46 49 47 50 test('can set initial value', async () => { 48 - const task = (<Text message="foo" initialValue="bar" output={output} input={input} />)(); 51 + const element = <Text message="foo" initialValue="bar" />; 52 + const task = element.render({ input, output }); 49 53 50 54 input.emit('keypress', '', { name: 'return' }); 51 55
+7 -7
packages/jsx/test/jsx.test.tsx
··· 12 12 }); 13 13 14 14 test('can render', async () => { 15 - const task = jsx(Confirm, { 15 + const element = jsx(Confirm, { 16 16 message: 'foo?', 17 - input, 18 - output, 19 - })(); 17 + }); 18 + const task = element.render({ input, output }); 20 19 input.emit('keypress', '', { name: 'return' }); 21 20 const result = await task; 22 21 expect(result).to.equal(true); 23 22 }); 24 23 25 24 test('can render JSX', async () => { 26 - const task = (<Confirm message="foo?" input={input} output={output} />)(); 25 + const element = <Confirm message="foo?" />; 26 + const task = element.render({ input, output }); 27 27 input.emit('keypress', '', { name: 'return' }); 28 28 const result = await task; 29 29 expect(result).to.equal(true); 30 30 }); 31 31 32 32 test('unknown elements are null', async () => { 33 - const task = jsx('unknown-nonsense' as never, {} as never); 34 - const result = await task(); 33 + const element = jsx('unknown-nonsense' as never, {} as never); 34 + const result = await element.render(); 35 35 expect(result).to.equal(null); 36 36 }); 37 37 });