Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/quiet-moons-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clack/prompts": minor
"@clack/core": patch
---

Allow `multiselect` to accept readonly option arrays, e.g. options declared with `as const`. The `Value` type parameter is now inferred as a literal union (`'a' | 'b'`) from such arrays, and `initialValues` also accepts readonly arrays.
6 changes: 3 additions & 3 deletions packages/core/src/prompts/multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ interface OptionLike {

export interface MultiSelectOptions<T extends OptionLike>
extends PromptOptions<T['value'][], MultiSelectPrompt<T>> {
options: T[];
initialValues?: T['value'][];
options: readonly T[];
initialValues?: readonly T['value'][];
required?: boolean;
cursorAt?: T['value'];
}
export default class MultiSelectPrompt<T extends OptionLike> extends Prompt<T['value'][]> {
options: T[];
options: readonly T[];
cursor = 0;

private get _value(): T['value'] {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/cursor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export function findCursor<T extends { disabled?: boolean }>(
cursor: number,
delta: number,
options: T[]
options: readonly T[]
) {
const hasEnabledOptions = options.some((opt) => !opt.disabled);
if (!hasEnabledOptions) {
Expand Down
2 changes: 1 addition & 1 deletion packages/prompts/src/limit-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface LimitOptionsParams<TOption> extends CommonOptions {
/**
* The list of options to display.
*/
options: TOption[];
options: readonly TOption[];

/**
* The index of the currently active/selected option.
Expand Down
4 changes: 2 additions & 2 deletions packages/prompts/src/multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const MULTISELECT_INSTRUCTIONS = [

export interface MultiSelectOptions<Value> extends CommonOptions {
message: string;
options: Option<Value>[];
initialValues?: Value[];
options: readonly Option<Value>[];
initialValues?: readonly Value[];
maxItems?: number;
required?: boolean;
cursorAt?: Value;
Expand Down
36 changes: 35 additions & 1 deletion packages/prompts/test/multi-select.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
expectTypeOf,
test,
vi,
} from 'vitest';
import * as prompts from '../src/index.js';
import { MockReadable, MockWritable } from './test-utils.js';

Expand Down Expand Up @@ -492,4 +502,28 @@ describe.each(['true', 'false'])('multiselect (isCI = %s)', (isCI) => {
await result;
expect(output.buffer).toMatchSnapshot();
});

test('accepts readonly options defined with `as const`', async () => {
const options = [
{ value: 'opt0', label: 'Option 0' },
{ value: 'opt1', label: 'Option 1' },
] as const;

const result = prompts.multiselect({
message: 'foo',
options,
initialValues: ['opt1'] as const,
input,
output,
});

input.emit('keypress', '', { name: 'space' });
input.emit('keypress', '', { name: 'return' });

const value = await result;

expect(prompts.isCancel(value)).toBe(false);
expect(value).toEqual(['opt1', 'opt0']);
expectTypeOf(value).toEqualTypeOf<('opt0' | 'opt1')[] | typeof prompts.CANCEL_SYMBOL>();
});
});
Loading