···11+---
22+"@clack/prompts": minor
33+"@clack/core": minor
44+---
55+66+Adds support for Standard Schema validation
77+88+Prompts accept an optional `validate()` function to validate user input. While a function provides more flexibility and customization over your validation, it can be a bit verbose. To help solve this, there are libraries that provide schema-based validation to make shorthand and type-strict validation substantially easier.
99+1010+Libraries following the [Standard Schema specification](https://github.com/standard-schema/standard-schema) are now natively supported. For example, using [Arktype](https://arktype.io/):
1111+1212+```diff
1313+import { text } from '@clack/prompts';
1414+import { type } from 'arktype';
1515+1616+const name = await text({
1717+ message: 'Enter your email',
1818++ validate: type('string.email').describe('Invalid email'),
1919+});
2020+```
···2424export { block, getColumns, getRows, isCancel, wrapTextWithPrefix } from './utils/index.js';
2525export type { ClackSettings } from './utils/settings.js';
2626export { settings, updateSettings } from './utils/settings.js';
2727+export type { Validate } from './utils/validation.js';
2828+export { runValidation } from './utils/validation.js';
+10-2
packages/core/src/prompts/prompt.ts
···1313 setRawMode,
1414 settings,
1515} from '../utils/index.js';
1616+import type { Validate } from '../utils/validation.js';
1717+import { runValidation } from '../utils/validation.js';
16181719export interface PromptOptions<TValue, Self extends Prompt<TValue>> {
1820 render(this: Omit<Self, 'prompt'>): string | undefined;
1921 initialValue?: any;
2022 initialUserInput?: string;
2121- validate?: ((value: TValue | undefined) => string | Error | undefined) | undefined;
2323+2424+ /**
2525+ * A function or a [Standard Schema](https://github.com/standard-schema/standard-schema)
2626+ * that validates user input. If a custom function is given, you should return a `string` or `Error`
2727+ * to show as a validation error, or `undefined` to accept the result.
2828+ */
2929+ validate?: Validate<TValue> | undefined;
2230 input?: Readable;
2331 output?: Writable;
2432 signal?: AbortSignal;
···230238231239 if (key?.name === 'return' && this._shouldSubmit(char, key)) {
232240 if (this.opts.validate) {
233233- const problem = this.opts.validate(this.value);
241241+ const problem = runValidation(this.opts.validate, this.value);
234242 if (problem) {
235243 this.error = problem instanceof Error ? problem.message : problem;
236244 this.state = 'error';
+78
packages/core/src/utils/standard-schema.ts
···11+// https://standardschema.dev/schema
22+33+/** The Standard Schema interface. */
44+export interface StandardSchemaV1<Input = unknown, Output = Input> {
55+ /** The Standard Schema properties. */
66+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
77+}
88+99+export declare namespace StandardSchemaV1 {
1010+ /** The Standard Schema properties interface. */
1111+ export interface Props<Input = unknown, Output = Input> {
1212+ /** The version number of the standard. */
1313+ readonly version: 1;
1414+ /** The vendor name of the schema library. */
1515+ readonly vendor: string;
1616+ /** Validates unknown input values. */
1717+ readonly validate: (
1818+ value: unknown,
1919+ options?: StandardSchemaV1.Options | undefined
2020+ ) => Result<Output> | Promise<Result<Output>>;
2121+ /** Inferred types associated with the schema. */
2222+ readonly types?: Types<Input, Output> | undefined;
2323+ }
2424+2525+ /** The result interface of the validate function. */
2626+ export type Result<Output> = SuccessResult<Output> | FailureResult;
2727+2828+ /** The result interface if validation succeeds. */
2929+ export interface SuccessResult<Output> {
3030+ /** The typed output value. */
3131+ readonly value: Output;
3232+ /** A falsy value for `issues` indicates success. */
3333+ readonly issues?: undefined;
3434+ }
3535+3636+ export interface Options {
3737+ /** Explicit support for additional vendor-specific parameters, if needed. */
3838+ readonly libraryOptions?: Record<string, unknown> | undefined;
3939+ }
4040+4141+ /** The result interface if validation fails. */
4242+ export interface FailureResult {
4343+ /** The issues of failed validation. */
4444+ readonly issues: ReadonlyArray<Issue>;
4545+ }
4646+4747+ /** The issue interface of the failure output. */
4848+ export interface Issue {
4949+ /** The error message of the issue. */
5050+ readonly message: string;
5151+ /** The path of the issue, if any. */
5252+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
5353+ }
5454+5555+ /** The path segment interface of the issue. */
5656+ export interface PathSegment {
5757+ /** The key representing a path segment. */
5858+ readonly key: PropertyKey;
5959+ }
6060+6161+ /** The Standard Schema types interface. */
6262+ export interface Types<Input = unknown, Output = Input> {
6363+ /** The input type of the schema. */
6464+ readonly input: Input;
6565+ /** The output type of the schema. */
6666+ readonly output: Output;
6767+ }
6868+6969+ /** Infers the input type of a Standard Schema. */
7070+ export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
7171+ Schema['~standard']['types']
7272+ >['input'];
7373+7474+ /** Infers the output type of a Standard Schema. */
7575+ export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
7676+ Schema['~standard']['types']
7777+ >['output'];
7878+}
+61
packages/core/src/utils/validation.ts
···11+import type { StandardSchemaV1 } from './standard-schema.js';
22+33+/**
44+ * A function or [Standard Schema](https://github.com/standard-schema/standard-schema)
55+ * that validates user input. If a custom function is given, you should return a
66+ * `string` or `Error` to show as a validation error, or `undefined` to accept the result.
77+ *
88+ * @example Using arktype
99+ * ```ts
1010+ * import { text } from '@clack/prompts';
1111+ * import { type } from 'arktype';
1212+ *
1313+ * const name = await text({
1414+ * message: 'Enter your name (letters only)',
1515+ * validate: type('string.alpha').describe('Name can only contain letters'),
1616+ * });
1717+ * ```
1818+ *
1919+ * @example Custom validator
2020+ * ```ts
2121+ * import { text } from '@clack/prompts';
2222+ *
2323+ * const age = await text({
2424+ * message: 'Enter your age:',
2525+ * validate(value) {
2626+ * if (!value) return 'Please enter a value';
2727+ * const num = parseInt(value);
2828+ * if (isNaN(num)) return 'Please enter a valid number';
2929+ * if (num < 0 || num > 120) return 'Age must be between 0 and 120';
3030+ * return undefined;
3131+ * },
3232+ * });
3333+ * ```
3434+ */
3535+export type Validate<TValue> =
3636+ | ((value: TValue | undefined) => string | Error | undefined)
3737+ | StandardSchemaV1<TValue | undefined, unknown>;
3838+3939+/**
4040+ * Runs the `validate()` option and normalizes the result
4141+ * @param validate - The validate option
4242+ * @param value - The user input
4343+ * @returns the validation result
4444+ */
4545+export function runValidation<TValue>(
4646+ validate: Validate<TValue>,
4747+ value: TValue | undefined
4848+): string | Error | undefined {
4949+ if ('~standard' in validate) {
5050+ const result = validate['~standard'].validate(value);
5151+ // https://standardschema.dev/schema#how-to-only-allow-synchronous-validation
5252+ // TODO: https://github.com/bombshell-dev/clack/issues/92
5353+ if (result instanceof Promise) {
5454+ throw new TypeError(
5555+ 'Schema validation must be synchronous. Update `validate()` and remove any asynchronous logic.'
5656+ );
5757+ }
5858+ return result.issues?.at(0)?.message;
5959+ }
6060+ return validate(value);
6161+}
···11import { styleText } from 'node:util';
22+import type { Validate } from '@clack/core';
23import { AutocompletePrompt, settings } from '@clack/core';
34import {
45 type CommonOptions,
···7071 placeholder?: string;
71727273 /**
7373- * A function that validates user input. Return a `string` or `Error` to show as a
7474- * validation error, or `undefined` to accept the result.
7474+ * A function or a [Standard Schema](https://github.com/standard-schema/standard-schema)
7575+ * that validates user input. If a custom function is given, you should return a `string` or `Error`
7676+ * to show as a validation error, or `undefined` to accept the result.
7577 */
7676- validate?: (value: Value | Value[] | undefined) => string | Error | undefined;
7878+ validate?: Validate<Value | Value[]>;
77797880 /**
7981 * Custom filter function to match options against the search input.
+11-5
packages/prompts/src/date.ts
···11import { styleText } from 'node:util';
22-import type { DateFormat, State } from '@clack/core';
33-import { DatePrompt, settings } from '@clack/core';
22+import type { DateFormat, State, Validate } from '@clack/core';
33+import { DatePrompt, runValidation, settings } from '@clack/core';
44import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js';
5566export type { DateFormat };
···1313 initialValue?: Date;
1414 minDate?: Date;
1515 maxDate?: Date;
1616- validate?: (value: Date | undefined) => string | Error | undefined;
1616+1717+ /**
1818+ * A function or a [Standard Schema](https://github.com/standard-schema/standard-schema)
1919+ * that validates user input. If a custom function is given, you should return a `string` or `Error`
2020+ * to show as a validation error, or `undefined` to accept the result.
2121+ */
2222+ validate?: Validate<Date>;
1723}
18241925export const date = (opts: DateOptions) => {
···2329 validate(value: Date | undefined) {
2430 if (value === undefined) {
2531 if (opts.defaultValue !== undefined) return undefined;
2626- if (validate) return validate(value);
3232+ if (validate) return runValidation(validate, value);
2733 return settings.date.messages.required;
2834 }
2935 const iso = (d: Date) => d.toISOString().slice(0, 10);
···3339 if (opts.maxDate && iso(value) > iso(opts.maxDate)) {
3440 return settings.date.messages.beforeMax(opts.maxDate);
3541 }
3636- if (validate) return validate(value);
4242+ if (validate) return runValidation(validate, value);
3743 return undefined;
3844 },
3945 render() {
+5-3
packages/prompts/src/password.ts
···11import { styleText } from 'node:util';
22+import type { Validate } from '@clack/core';
23import { PasswordPrompt, settings } from '@clack/core';
34import { type CommonOptions, S_BAR, S_BAR_END, S_PASSWORD_MASK, symbol } from './common.js';
45···1819 mask?: string;
19202021 /**
2121- * A function that validates user input. Return a `string` or `Error` to show as a
2222- * validation error, or `undefined` to accept the result.
2222+ * A function or a [Standard Schema](https://github.com/standard-schema/standard-schema)
2323+ * that validates user input. If a custom function is given, you should return a `string` or `Error`
2424+ * to show as a validation error, or `undefined` to accept the result.
2325 */
2424- validate?: (value: string | undefined) => string | Error | undefined;
2626+ validate?: Validate<string>;
25272628 /**
2729 * When enabled it causes the input to be cleared if/when validation fails.
+7-4
packages/prompts/src/path.ts
···11import { existsSync, lstatSync, readdirSync } from 'node:fs';
22import { dirname, join } from 'node:path';
33+import type { Validate } from '@clack/core';
44+import { runValidation } from '@clack/core';
35import { autocomplete } from './autocomplete.js';
46import type { CommonOptions } from './common.js';
57···3335 initialValue?: string;
34363537 /**
3636- * A function that validates the given path. Return a `string` or `Error` to show as a
3737- * validation error, or `undefined` to accept the result.
3838+ * A function or a [Standard Schema](https://github.com/standard-schema/standard-schema)
3939+ * that validates user input. If a custom function is given, you should return a `string` or `Error`
4040+ * to show as a validation error, or `undefined` to accept the result.
3841 */
3939- validate?: (value: string | undefined) => string | Error | undefined;
4242+ validate?: Validate<string>;
4043}
41444245/**
···7174 return 'Please select a path';
7275 }
7376 if (validate) {
7474- return validate(value);
7777+ return runValidation(validate, value);
7578 }
7679 return undefined;
7780 },
+5-3
packages/prompts/src/text.ts
···11import { styleText } from 'node:util';
22+import type { Validate } from '@clack/core';
23import { settings, TextPrompt } from '@clack/core';
34import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js';
45···2829 initialValue?: string;
29303031 /**
3131- * A function that validates user input. Return a `string` or `Error` to show as a
3232- * validation error, or `undefined` to accept the result.
3232+ * A function or a [Standard Schema](https://github.com/standard-schema/standard-schema)
3333+ * that validates user input. If a custom function is given, you should return a `string` or `Error`
3434+ * to show as a validation error, or `undefined` to accept the result.
3335 */
3434- validate?: (value: string | undefined) => string | Error | undefined;
3636+ validate?: Validate<string>;
3537}
36383739/**