Skip to content

Commit e4dec12

Browse files
authored
Add changeset-like prompts, changeset demo (#81)
2 parents a79733e + 8a4a12f commit e4dec12

File tree

10 files changed

+333
-6
lines changed

10 files changed

+333
-6
lines changed

.changeset/dull-boats-drum.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clack/prompts': minor
3+
'@clack/core': patch
4+
---
5+
6+
add `groupMultiselect` prompt

.changeset/tasty-comics-warn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clack/core': minor
3+
---
4+
5+
Add `GroupMultiSelect` prompt

examples/changesets/index.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as p from '@clack/prompts';
2+
import { setTimeout } from 'node:timers/promises';
3+
import color from 'picocolors';
4+
5+
function onCancel() {
6+
p.cancel('Operation cancelled.');
7+
process.exit(0);
8+
}
9+
10+
async function main() {
11+
console.clear();
12+
13+
await setTimeout(1000);
14+
15+
p.intro(`${color.bgCyan(color.black(' changesets '))}`);
16+
17+
const changeset = await p.group(
18+
{
19+
packages: () =>
20+
p.groupMultiselect({
21+
message: 'Which packages would you like to include?',
22+
options: {
23+
'changed packages': [
24+
{ value: '@scope/a' },
25+
{ value: '@scope/b' },
26+
{ value: '@scope/c' },
27+
],
28+
'unchanged packages': [
29+
{ value: '@scope/x' },
30+
{ value: '@scope/y' },
31+
{ value: '@scope/z' },
32+
]
33+
}
34+
}),
35+
major: ({ results }) => {
36+
const packages = results.packages ?? [];
37+
return p.multiselect({
38+
message: `Which packages should have a ${color.red('major')} bump?`,
39+
options: packages.map(value => ({ value })),
40+
required: false,
41+
})
42+
},
43+
minor: ({ results }) => {
44+
const packages = results.packages ?? [];
45+
const major = Array.isArray(results.major) ? results.major : [];
46+
const possiblePackages = packages.filter(pkg => !major.includes(pkg))
47+
if (possiblePackages.length === 0) return;
48+
return p.multiselect({
49+
message: `Which packages should have a ${color.yellow('minor')} bump?`,
50+
options: possiblePackages.map(value => ({ value })),
51+
required: false
52+
})
53+
},
54+
patch: async ({ results }) => {
55+
const packages = results.packages ?? [];
56+
const major = Array.isArray(results.major) ? results.major : [];
57+
const minor = Array.isArray(results.minor) ? results.minor : [];
58+
const possiblePackages = packages.filter(pkg => !major.includes(pkg) && !minor.includes(pkg));
59+
if (possiblePackages.length === 0) return;
60+
let note = possiblePackages.join('\n');
61+
62+
p.log.step(`These packages will have a ${color.green('patch')} bump.\n${color.dim(note)}`);
63+
return possiblePackages
64+
}
65+
},
66+
{
67+
onCancel
68+
}
69+
);
70+
71+
const message = await p.text({
72+
placeholder: 'Summary',
73+
message: 'Please enter a summary for this change'
74+
})
75+
76+
if (p.isCancel(message)) {
77+
return onCancel()
78+
}
79+
80+
p.outro(`Changeset added! ${color.underline(color.cyan('.changeset/orange-crabs-sing.md'))}`);
81+
}
82+
83+
main().catch(console.error);

examples/changesets/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@example/changesets",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"dependencies": {
7+
"@clack/core": "workspace:*",
8+
"@clack/prompts": "workspace:*",
9+
"picocolors": "^1.0.0"
10+
},
11+
"scripts": {
12+
"start": "jiti ./index.ts"
13+
},
14+
"devDependencies": {
15+
"jiti": "^1.17.0"
16+
}
17+
}

examples/changesets/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+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"build:core": "pnpm --filter @clack/core run build",
99
"build:prompts": "pnpm --filter @clack/prompts run build",
1010
"start": "pnpm --filter @example/basic run start",
11+
"dev": "pnpm --filter @example/changesets run start",
1112
"format": "pnpm run format:code",
1213
"format:code": "prettier -w . --cache",
1314
"format:imports": "organize-imports-cli ./packages/*/tsconfig.json",

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { default as ConfirmPrompt } from './prompts/confirm';
22
export { default as MultiSelectPrompt } from './prompts/multi-select';
3+
export { default as GroupMultiSelectPrompt } from './prompts/group-multiselect';
34
export { default as PasswordPrompt } from './prompts/password';
45
export { default as Prompt, isCancel } from './prompts/prompt';
56
export type { State } from './prompts/prompt';
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Prompt, { PromptOptions } from './prompt';
2+
3+
interface GroupMultiSelectOptions<T extends { value: any }> extends PromptOptions<GroupMultiSelectPrompt<T>> {
4+
options: Record<string, T[]>;
5+
initialValues?: T['value'][];
6+
required?: boolean;
7+
cursorAt?: T['value'];
8+
}
9+
export default class GroupMultiSelectPrompt<T extends { value: any }> extends Prompt {
10+
options: (T & { group: string | boolean })[];
11+
cursor: number = 0;
12+
13+
getGroupItems(group: string): T[] {
14+
return this.options.filter(o => o.group === group);
15+
}
16+
17+
isGroupSelected(group: string) {
18+
const items = this.getGroupItems(group);
19+
return items.every(i => this.value.includes(i.value));
20+
}
21+
22+
private toggleValue() {
23+
const item = this.options[this.cursor];
24+
if (item.group === true) {
25+
const group = item.value;
26+
const groupedItems = this.getGroupItems(group);
27+
if (this.isGroupSelected(group)) {
28+
this.value = this.value.filter((v: string) => groupedItems.findIndex(i => i.value === v) === -1);
29+
} else {
30+
this.value = [...this.value, ...groupedItems.map(i => i.value)];
31+
}
32+
this.value = Array.from(new Set(this.value));
33+
} else {
34+
const selected = this.value.includes(item.value);
35+
this.value = selected
36+
? this.value.filter((v: T['value']) => v !== item.value)
37+
: [...this.value, item.value];
38+
}
39+
}
40+
41+
constructor(opts: GroupMultiSelectOptions<T>) {
42+
super(opts, false);
43+
const { options } = opts;
44+
this.options = Object.entries(options).flatMap(([key, option]) => [
45+
{ value: key, group: true, label: key },
46+
...option.map((opt) => ({ ...opt, group: key })),
47+
])
48+
this.value = [...(opts.initialValues ?? [])];
49+
this.cursor = Math.max(
50+
this.options.findIndex(({ value }) => value === opts.cursorAt),
51+
0
52+
);
53+
54+
this.on('cursor', (key) => {
55+
switch (key) {
56+
case 'left':
57+
case 'up':
58+
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
59+
break;
60+
case 'down':
61+
case 'right':
62+
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
63+
break;
64+
case 'space':
65+
this.toggleValue();
66+
break;
67+
}
68+
});
69+
}
70+
}

packages/core/src/prompts/multi-select.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt
1414
return this.options[this.cursor].value;
1515
}
1616

17+
private toggleAll() {
18+
const allSelected = this.value.length === this.options.length;
19+
this.value = allSelected ? [] : this.options.map(v => v.value);
20+
}
21+
1722
private toggleValue() {
1823
const selected = this.value.includes(this._value);
1924
this.value = selected
@@ -30,6 +35,11 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt
3035
this.options.findIndex(({ value }) => value === opts.cursorAt),
3136
0
3237
);
38+
this.on('key', (char) => {
39+
if (char === 'a') {
40+
this.toggleAll()
41+
}
42+
})
3343

3444
this.on('cursor', (key) => {
3545
switch (key) {

0 commit comments

Comments
 (0)