Skip to content

Commit bbe2c6f

Browse files
committed
Merge branch 'builder' of https://github.com/Mist3rBru/clack into builder
2 parents d7d2b3e + 232c31d commit bbe2c6f

File tree

14 files changed

+967
-516
lines changed

14 files changed

+967
-516
lines changed

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"access": "public",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
10-
"ignore": ["@example/*"]
10+
"ignore": []
1111
}

.changeset/red-walls-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clack/prompts': minor
3+
---
4+
5+
add prompt `workflow` builder
File renamed without changes.

examples/basic/tsconfig.json

Lines changed: 0 additions & 3 deletions
This file was deleted.
File renamed without changes.

examples/changesets/package.json

Lines changed: 0 additions & 17 deletions
This file was deleted.

examples/changesets/tsconfig.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

examples/basic/package.json renamed to examples/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@example/basic",
2+
"name": "@clack/examples",
33
"private": true,
44
"version": "0.0.0",
55
"type": "module",
@@ -9,8 +9,10 @@
99
"picocolors": "^1.0.0"
1010
},
1111
"scripts": {
12-
"start": "jiti ./index.ts",
13-
"spinner": "jiti ./spinner.ts"
12+
"basic": "jiti ./basic.ts",
13+
"spinner": "jiti ./spinner.ts",
14+
"changesets": "jiti ./changesets.ts",
15+
"workflow": "jiti ./workflow.ts"
1416
},
1517
"devDependencies": {
1618
"jiti": "^1.17.0"
File renamed without changes.

examples/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../tsconfig.json"
3+
}

examples/workflow.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as p from '@clack/prompts';
2+
3+
(async () => {
4+
const results = await p
5+
.workflow()
6+
.step('path', () =>
7+
p.text({
8+
message: 'Where should we create your project?',
9+
placeholder: './sparkling-solid',
10+
validate: (value) => {
11+
if (!value) return 'Please enter a path.';
12+
if (value[0] !== '.') return 'Please enter a relative path.';
13+
},
14+
})
15+
)
16+
.step('password', () =>
17+
p.password({
18+
message: 'Provide a password',
19+
validate: (value) => {
20+
if (!value) return 'Please enter a password.';
21+
if (value.length < 5) return 'Password should have at least 5 characters.';
22+
},
23+
})
24+
)
25+
.step('type', ({ results }) =>
26+
p.select({
27+
message: `Pick a project type within "${results.path}"`,
28+
initialValue: 'ts',
29+
maxItems: 5,
30+
options: [
31+
{ value: 'ts', label: 'TypeScript' },
32+
{ value: 'js', label: 'JavaScript' },
33+
{ value: 'rust', label: 'Rust' },
34+
{ value: 'go', label: 'Go' },
35+
{ value: 'python', label: 'Python' },
36+
{ value: 'coffee', label: 'CoffeeScript', hint: 'oh no' },
37+
],
38+
})
39+
)
40+
.step('tools', () =>
41+
p.multiselect({
42+
message: 'Select additional tools.',
43+
initialValues: ['prettier', 'eslint'],
44+
options: [
45+
{ value: 'prettier', label: 'Prettier', hint: 'recommended' },
46+
{ value: 'eslint', label: 'ESLint', hint: 'recommended' },
47+
{ value: 'stylelint', label: 'Stylelint' },
48+
{ value: 'gh-action', label: 'GitHub Action' },
49+
],
50+
})
51+
)
52+
.step('install', ({ results }) =>
53+
p.confirm({
54+
message: 'Install dependencies?',
55+
initialValue: false,
56+
})
57+
)
58+
.run();
59+
60+
await p
61+
.workflow()
62+
.step('cancel', () => p.text({ message: 'Try cancel prompt (Ctrl + C):' }))
63+
.step('afterCancel', () => p.text({ message: 'This will not appear!' }))
64+
.onCancel(({ results }) => {
65+
p.cancel('Workflow canceled');
66+
process.exit(0);
67+
})
68+
.run();
69+
})();

packages/prompts/README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ s.stop('Installed via npm');
123123

124124
## Utilities
125125

126-
### Grouping
126+
### Group
127127

128128
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.
129129

@@ -156,3 +156,32 @@ const group = await p.group(
156156

157157
console.log(group.name, group.age, group.color);
158158
```
159+
160+
### Workflow
161+
162+
Just like `group`, but on builder way, so you can choose which one fits better.
163+
164+
```js
165+
import * as p from '@clack/prompts';
166+
167+
const results = await p
168+
.workflow()
169+
.step('name', () => p.text({ message: 'What is your name?' }))
170+
.step('age', () => p.text({ message: 'What is your age?' }))
171+
.step('color', ({ results }) =>
172+
p.multiselect({
173+
message: `What is your favorite color ${results.name}?`,
174+
options: [
175+
{ value: 'red', label: 'Red' },
176+
{ value: 'green', label: 'Green' },
177+
{ value: 'blue', label: 'Blue' },
178+
],
179+
})
180+
)
181+
.onCancel(() => {
182+
p.cancel('Workflow canceled');
183+
process.exit(0);
184+
})
185+
.run();
186+
console.log(results.name, results.age, results.color);
187+
```

packages/prompts/src/index.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -878,36 +878,38 @@ function ansiRegex() {
878878
return new RegExp(pattern, 'g');
879879
}
880880

881-
export type PromptGroupAwaitedReturn<T> = {
881+
type Prettify<T> = {
882+
[P in keyof T]: T[P];
883+
} & {};
884+
885+
export type PromptGroupAwaitedReturn<T> = Prettify<{
882886
[P in keyof T]: Exclude<Awaited<T[P]>, symbol>;
887+
}>;
888+
889+
export type PromptWithOptions<TResults, TReturn> = (opts: {
890+
results: PromptGroupAwaitedReturn<TResults>;
891+
}) => TReturn;
892+
893+
export type PromptGroup<T> = {
894+
[P in keyof T]: PromptWithOptions<Partial<Omit<T, P>>, void | Promise<T[P] | void>>;
883895
};
884896

885897
export interface PromptGroupOptions<T> {
886898
/**
887899
* Control how the group can be canceled
888900
* if one of the prompts is canceled.
889901
*/
890-
onCancel?: (opts: { results: Prettify<Partial<PromptGroupAwaitedReturn<T>>> }) => void;
902+
onCancel?: PromptWithOptions<Partial<T>, void>;
891903
}
892904

893-
type Prettify<T> = {
894-
[P in keyof T]: T[P];
895-
} & {};
896-
897-
export type PromptGroup<T> = {
898-
[P in keyof T]: (opts: {
899-
results: Prettify<Partial<PromptGroupAwaitedReturn<Omit<T, P>>>>;
900-
}) => void | Promise<T[P] | void>;
901-
};
902-
903905
/**
904906
* Define a group of prompts to be displayed
905907
* and return a results of objects within the group
906908
*/
907909
export const group = async <T>(
908910
prompts: PromptGroup<T>,
909911
opts?: PromptGroupOptions<T>
910-
): Promise<Prettify<PromptGroupAwaitedReturn<T>>> => {
912+
): Promise<PromptGroupAwaitedReturn<T>> => {
911913
const results = {} as any;
912914
const promptNames = Object.keys(prompts);
913915

@@ -931,3 +933,51 @@ export const group = async <T>(
931933

932934
return results;
933935
};
936+
937+
type NextWorkflowBuilder<
938+
TResults extends Record<string, unknown>,
939+
TKey extends string,
940+
TResult
941+
> = WorkflowBuilder<
942+
{
943+
[Key in keyof TResults]: Key extends TKey ? TResult : TResults[Key];
944+
} & {
945+
[Key in TKey]: TResult;
946+
}
947+
>;
948+
949+
class WorkflowBuilder<TResults extends Record<string, unknown> = {}> {
950+
private results: TResults = {} as TResults;
951+
private prompts: Record<string, PromptWithOptions<TResults, unknown>> = {};
952+
private cancelCallback: PromptWithOptions<Partial<TResults>, void> | undefined;
953+
954+
public step<TKey extends string, TResult>(
955+
key: TKey extends keyof TResults ? never : TKey,
956+
prompt: PromptWithOptions<TResults, TResult>
957+
): NextWorkflowBuilder<TResults, TKey, TResult> {
958+
this.prompts[key] = prompt;
959+
return this as NextWorkflowBuilder<TResults, TKey, TResult>;
960+
}
961+
962+
public onCancel(cb: PromptWithOptions<Partial<TResults>, void>): WorkflowBuilder<TResults> {
963+
this.cancelCallback = cb;
964+
return this;
965+
}
966+
967+
public async run(): Promise<PromptGroupAwaitedReturn<TResults>> {
968+
for (const [key, prompt] of Object.entries(this.prompts)) {
969+
const result = await prompt({ results: this.results as any });
970+
if (isCancel(result)) {
971+
this.cancelCallback?.({ results: this.results as any });
972+
continue;
973+
}
974+
//@ts-ignore
975+
this.results[key] = result;
976+
}
977+
return this.results as PromptGroupAwaitedReturn<TResults>;
978+
}
979+
}
980+
981+
export const workflow = () => {
982+
return new WorkflowBuilder();
983+
};

0 commit comments

Comments
 (0)