From 8c880e32ecf115d11adac0794000fe82c1a4240f Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Tue, 13 Oct 2026 14:26:33 +0800 Subject: [PATCH] feat(prompts): support readonly options in multiselect --- .changeset/quiet-moons-matter.md | 6 ++++ packages/core/src/prompts/multi-select.ts | 6 ++-- packages/core/src/utils/cursor.ts | 2 +- packages/prompts/src/limit-options.ts | 2 +- packages/prompts/src/multi-select.ts | 4 +-- packages/prompts/test/multi-select.test.ts | 36 +++++++++++++++++++++- 6 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 .changeset/quiet-moons-matter.md diff --git a/.changeset/quiet-moons-matter.md b/.changeset/quiet-moons-matter.md new file mode 100644 index 00000000..234d84e2 --- /dev/null +++ b/.changeset/quiet-moons-matter.md @@ -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. diff --git a/packages/core/src/prompts/multi-select.ts b/packages/core/src/prompts/multi-select.ts index dd590518..54ac46c5 100644 --- a/packages/core/src/prompts/multi-select.ts +++ b/packages/core/src/prompts/multi-select.ts @@ -8,13 +8,13 @@ interface OptionLike { export interface MultiSelectOptions extends PromptOptions> { - options: T[]; - initialValues?: T['value'][]; + options: readonly T[]; + initialValues?: readonly T['value'][]; required?: boolean; cursorAt?: T['value']; } export default class MultiSelectPrompt extends Prompt { - options: T[]; + options: readonly T[]; cursor = 0; private get _value(): T['value'] { diff --git a/packages/core/src/utils/cursor.ts b/packages/core/src/utils/cursor.ts index e078c986..47aa7e15 100644 --- a/packages/core/src/utils/cursor.ts +++ b/packages/core/src/utils/cursor.ts @@ -1,7 +1,7 @@ export function findCursor( cursor: number, delta: number, - options: T[] + options: readonly T[] ) { const hasEnabledOptions = options.some((opt) => !opt.disabled); if (!hasEnabledOptions) { diff --git a/packages/prompts/src/limit-options.ts b/packages/prompts/src/limit-options.ts index 8805afbd..72507d34 100644 --- a/packages/prompts/src/limit-options.ts +++ b/packages/prompts/src/limit-options.ts @@ -10,7 +10,7 @@ export interface LimitOptionsParams extends CommonOptions { /** * The list of options to display. */ - options: TOption[]; + options: readonly TOption[]; /** * The index of the currently active/selected option. diff --git a/packages/prompts/src/multi-select.ts b/packages/prompts/src/multi-select.ts index b52239cc..8b28c915 100644 --- a/packages/prompts/src/multi-select.ts +++ b/packages/prompts/src/multi-select.ts @@ -23,8 +23,8 @@ export const MULTISELECT_INSTRUCTIONS = [ export interface MultiSelectOptions extends CommonOptions { message: string; - options: Option[]; - initialValues?: Value[]; + options: readonly Option[]; + initialValues?: readonly Value[]; maxItems?: number; required?: boolean; cursorAt?: Value; diff --git a/packages/prompts/test/multi-select.test.ts b/packages/prompts/test/multi-select.test.ts index e11125e1..c10b146d 100644 --- a/packages/prompts/test/multi-select.test.ts +++ b/packages/prompts/test/multi-select.test.ts @@ -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'; @@ -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>(); + }); });