From 27b418567e5338782f356e356e76a9a310795cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20L=C3=B6fgren?= Date: Tue, 21 Feb 2023 10:34:07 +0100 Subject: [PATCH 1/2] break(group): replace `onCancel` by returning `cancel` - Consistent API with individual prompts - Less indirection => less confusing - Don't wrap partial results in object => simplify destruction - Same amount of LOC - Rewrite docs - Simplify implementation - Remove nonsense `.catch()` handler - Remove error prone `any` cast --- examples/basic/index.ts | 89 +++++++++++++++++------------------ packages/prompts/README.md | 43 ++++++++--------- packages/prompts/src/index.ts | 51 +++++++------------- 3 files changed, 80 insertions(+), 103 deletions(-) diff --git a/examples/basic/index.ts b/examples/basic/index.ts index db795e57..1775afe4 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -1,6 +1,6 @@ import * as p from '@clack/prompts'; -import color from 'picocolors'; import { setTimeout } from 'node:timers/promises'; +import color from 'picocolors'; async function main() { console.clear(); @@ -9,51 +9,48 @@ async function main() { p.intro(`${color.bgCyan(color.black(' create-app '))}`); - const project = await p.group( - { - path: () => - p.text({ - message: 'Where should we create your project?', - placeholder: './sparkling-solid', - validate: (value) => { - if (!value) return 'Please enter a path.'; - if (value[0] !== '.') return 'Please enter a relative path.'; - }, - }), - type: ({ results }) => - p.select({ - message: `Pick a project type within "${results.path}"`, - initialValue: 'ts', - options: [ - { value: 'ts', label: 'TypeScript' }, - { value: 'js', label: 'JavaScript' }, - { value: 'coffee', label: 'CoffeeScript', hint: 'oh no' }, - ], - }), - tools: () => - p.multiselect({ - message: 'Select additional tools.', - initialValue: ['prettier', 'eslint'], - options: [ - { value: 'prettier', label: 'Prettier', hint: 'recommended' }, - { value: 'eslint', label: 'ESLint', hint: 'recommended' }, - { value: 'stylelint', label: 'Stylelint' }, - { value: 'gh-action', label: 'GitHub Action' }, - ], - }), - install: () => - p.confirm({ - message: 'Install dependencies?', - initialValue: false, - }), - }, - { - onCancel: () => { - p.cancel('Operation cancelled.'); - process.exit(0); - }, - } - ); + const project = await p.group({ + path: () => + p.text({ + message: 'Where should we create your project?', + placeholder: './sparkling-solid', + validate: (value) => { + if (!value) return 'Please enter a path.'; + if (value[0] !== '.') return 'Please enter a relative path.'; + }, + }), + type: ({ path }) => + p.select({ + message: `Pick a project type within "${path}"`, + initialValue: 'ts', + options: [ + { value: 'ts', label: 'TypeScript' }, + { value: 'js', label: 'JavaScript' }, + { value: 'coffee', label: 'CoffeeScript', hint: 'oh no' }, + ], + }), + tools: () => + p.multiselect({ + message: 'Select additional tools.', + initialValue: ['prettier', 'eslint'], + options: [ + { value: 'prettier', label: 'Prettier', hint: 'recommended' }, + { value: 'eslint', label: 'ESLint', hint: 'recommended' }, + { value: 'stylelint', label: 'Stylelint' }, + { value: 'gh-action', label: 'GitHub Action' }, + ], + }), + install: () => + p.confirm({ + message: 'Install dependencies?', + initialValue: false, + }), + }); + + if (p.isCancel(project)) { + p.cancel('Operation cancelled.'); + process.exit(0); + } if (project.install) { const s = p.spinner(); diff --git a/packages/prompts/README.md b/packages/prompts/README.md index 183c2355..11cb6984 100644 --- a/packages/prompts/README.md +++ b/packages/prompts/README.md @@ -125,34 +125,29 @@ s.stop('Installed via npm'); ### Grouping -Grouping prompts together is a great way to keep your code organized. This accepts a JSON object with a name that can be used to reference the group later. The second argument is an optional but has a `onCancel` callback that will be called if the user cancels one of the prompts in the group. +Group prompts together to keep your code organized. Returns results of each prompt accessible by name. If one prompt is cancelled, the entire group is cancelled and `cancel` is returned instead. Each prompt factory function is passed the results of previous prompts. ```js import * as p from '@clack/prompts'; -const group = await p.group( - { - name: () => p.text({ message: 'What is your name?' }), - age: () => p.text({ message: 'What is your age?' }), - color: ({ results }) => - p.multiselect({ - message: `What is your favorite color ${results.name}?`, - options: [ - { value: 'red', label: 'Red' }, - { value: 'green', label: 'Green' }, - { value: 'blue', label: 'Blue' }, - ], - }), - }, - { - // On Cancel callback that wraps the group - // So if the user cancels one of the prompts in the group this function will be called - onCancel: ({ results }) => { - p.cancel('Operation cancelled.'); - process.exit(0); - }, - } -); +const group = await p.group({ + name: () => p.text({ message: 'What is your name?' }), + age: () => p.text({ message: 'What is your age?' }), + color: ({ name }) => + p.multiselect({ + message: `What is your favorite color ${name}?`, + options: [ + { value: 'red', label: 'Red' }, + { value: 'green', label: 'Green' }, + { value: 'blue', label: 'Blue' }, + ], + }), +}); + +if (p.isCancel(group)) { + p.cancel('Operation cancelled.'); + process.exit(0); +} console.log(group.name, group.age, group.color); ``` diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index cfcb080f..9eb21138 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -367,48 +367,33 @@ function ansiRegex() { return new RegExp(pattern, 'g'); } -export type PromptGroupAwaitedReturn = { - [P in keyof T]: Exclude, symbol>; +type GroupAwaitedReturn = { + [P in keyof T]: Awaited; }; -export interface PromptGroupOptions { - /** - * Control how the group can be canceld - * if one of the prompts is canceld. - */ - onCancel?: (opts: { results: Partial> }) => void; -} +type GroupPromptFactory = ( + results: Partial> +) => Promise; -export type PromptGroup = { - [P in keyof T]: (opts: { results: Partial> }) => Promise; +export type Group = { + [P in keyof T]: GroupPromptFactory; }; /** - * Define a group of prompts to be displayed - * and return a results of objects within the group + * Define a group of prompts to be displayed in sequence, + * returns results for each prompt in the group or `cancel`. */ -export const group = async ( - prompts: PromptGroup, - opts?: PromptGroupOptions -): Promise> => { - const results = {} as any; - const promptNames = Object.keys(prompts); - - for (const name of promptNames) { - const result = await prompts[name as keyof T]({ results }).catch((e) => { - throw e; - }); - - // Pass the results to the onCancel function - // so the user can decide what to do with the results - // TODO: Switch to callback within core to avoid isCancel Fn - if (typeof opts?.onCancel === 'function' && isCancel(result)) { - results[name] = 'canceled'; - opts.onCancel({ results }); - continue; +export const group = async (prompts: Group): Promise | symbol> => { + const results = {} as GroupAwaitedReturn; + + for (const [key, prompt] of Object.entries>(prompts)) { + const result = await prompt(results); + + if (isCancel(result)) { + return result; } - results[name] = result; + results[key as keyof T] = result; } return results; From e91ba5cefd58e73da130691f76ad29d4659500a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20L=C3=B6fgren?= Date: Tue, 21 Feb 2023 10:42:21 +0100 Subject: [PATCH 2/2] docs: changeset --- .changeset/tall-carrots-unite.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tall-carrots-unite.md diff --git a/.changeset/tall-carrots-unite.md b/.changeset/tall-carrots-unite.md new file mode 100644 index 00000000..aa0868cc --- /dev/null +++ b/.changeset/tall-carrots-unite.md @@ -0,0 +1,5 @@ +--- +"@clack/prompts": patch +--- + +Replace `onCancel` handler for `group()` by returning `cancel` as results instead. Partial results are no longer wrapped in an object.