@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
571 lines (529 loc) • 20.7 kB
TypeScript
import { StandardSchemaV1 } from '@standard-schema/spec';
import { DeepPartial, IsAny, MaybePromise } from 'types';
export * from './index.js';
type ImageInputValue = { x: number; y: number };
type IsImageInputValue<T> = T extends ImageInputValue
? Exclude<keyof T, keyof ImageInputValue> extends never
? true
: false
: false;
// If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle
type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
// Input type mappings for form fields
type InputTypeMap = {
text: string;
email: string;
password: string;
url: string;
tel: string;
search: string;
number: number;
range: number;
date: string;
'datetime-local': string;
time: string;
month: string;
week: string;
color: string;
checkbox: boolean | string[];
radio: string;
file: File;
hidden: string | number | boolean;
submit: string | number | boolean;
button: string;
reset: string;
image: ImageInputValue;
select: string;
'select multiple': string[];
'file multiple': File[];
};
// Valid input types for a given value type
export type RemoteFormFieldType<T> = {
[K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never;
}[keyof InputTypeMap];
// Input element properties based on type
type InputElementProps<T extends keyof InputTypeMap, Value> = T extends 'checkbox' | 'radio'
? {
name: string;
type: T;
value?: string;
'aria-invalid': boolean | 'false' | 'true' | undefined;
get checked(): boolean;
set checked(value: boolean);
readonly defaultChecked?: boolean;
}
: T extends 'image'
? {
name: string;
type: 'image';
'aria-invalid': boolean | 'false' | 'true' | undefined;
}
: T extends 'file'
? {
name: string;
type: 'file';
'aria-invalid': boolean | 'false' | 'true' | undefined;
get files(): FileList | null;
set files(v: FileList | null);
}
: T extends 'select'
? {
name: string;
'aria-invalid': boolean | 'false' | 'true' | undefined;
get value(): string;
set value(v: string);
}
: T extends 'select multiple'
? {
name: string;
multiple: true;
'aria-invalid': boolean | 'false' | 'true' | undefined;
get value(): string[];
set value(v: string[]);
}
: T extends 'text'
? {
name: string;
'aria-invalid': boolean | 'false' | 'true' | undefined;
get value(): Value extends string ? string : string | number;
set value(v: Value extends string ? string : string | number);
readonly defaultValue?: Value extends string ? string : string | number;
}
: {
name: string;
type: T;
'aria-invalid': boolean | 'false' | 'true' | undefined;
get value(): string | number;
set value(v: string | number);
readonly defaultValue?: string | number;
};
type RemoteFormFieldMethods<T> = {
/** The values that will be submitted */
value(): DeepPartial<T>;
/** Set the values that will be submitted */
set(input: DeepPartial<T>): DeepPartial<T>;
/** Whether the field or any nested field has been interacted with since the form was mounted */
touched(): boolean;
/** Whether the field or any nested field has been edited since the form was mounted */
dirty(): boolean;
/** Validation issues, if any */
issues(): RemoteFormIssue[] | undefined;
};
// These two types use "T extends unknown ? .. : .." to distribute over unions.
// Example: if "type T = A | b" then "keyof T" only contains keys that both A and B have, with "KeysOfUnion<T>" we get the keys of both A and B
type KeysOfUnion<T> = T extends unknown ? keyof T : never;
type ValueOfUnionKey<T, K extends PropertyKey> = T extends unknown
? K extends keyof T
? T[K]
: never
: never;
export type RemoteFormFieldValue =
| string
| string[]
| number
| boolean
| File
| File[]
| ImageInputValue;
type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
? Value extends string[]
? [type: Type, value: Value[number] | (string & {}), checked?: boolean]
: Value extends boolean
? [type: Type, value?: boolean]
: [type: Type, value?: Value]
: Type extends 'image'
? [type: Type]
: Type extends 'submit' | 'hidden'
? Value extends string
? [type: Type, value: Value | (string & {})]
: [type: Type, value: Value]
: Type extends 'radio'
? [type: Type, value: Value | (string & {}), checked?: boolean]
: Type extends 'file' | 'file multiple'
? [type: Type]
: [type: Type, value?: Value];
type WidenLiteralString<T> = T extends string ? (string extends T ? T : string) : T;
/**
* Form field accessor type that provides name(), value(), and issues() methods
*/
export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFieldMethods<Value> & {
/**
* Returns an object that can be spread onto an input element with the correct type attribute,
* aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
* @example
* ```svelte
* <input {...myForm.fields.myString.as('text')} />
* <input {...myForm.fields.myNumber.as('number')} />
* <input {...myForm.fields.myBoolean.as('checkbox')} />
* ```
*/
as<T extends RemoteFormFieldType<Value>>(
...args: AsArgs<T, Value>
): InputElementProps<T, WidenLiteralString<Value>>;
};
type RemoteFormFieldContainer<Value> = RemoteFormFieldMethods<Value> & {
/** Validation issues belonging to this or any of the fields that belong to it, if any */
allIssues(): RemoteFormIssue[] | undefined;
};
type UnknownField<Value> = RemoteFormFieldMethods<Value> & {
/** Validation issues belonging to this or any of the fields that belong to it, if any */
allIssues(): RemoteFormIssue[] | undefined;
/**
* Returns an object that can be spread onto an input element with the correct type attribute,
* aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
* @example
* ```svelte
* <input {...myForm.fields.myString.as('text')} />
* <input {...myForm.fields.myNumber.as('number')} />
* <input {...myForm.fields.myBoolean.as('checkbox')} />
* ```
*/
as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T, Value>;
} & {
[key: string | number]: UnknownField<any>;
};
type RemoteFormFieldsRoot<
Input extends RemoteFormInput | void,
Original extends [RemoteFormInput | void] = [Input]
> =
IsAny<Input> extends true
? RecursiveFormFields
: Input extends void
? {
/** Validation issues, if any */
issues(): RemoteFormIssue[] | undefined;
/** Validation issues belonging to this or any of the fields that belong to it, if any */
allIssues(): RemoteFormIssue[] | undefined;
}
: WillRecurseIndefinitely<Input> extends true
? RecursiveFormFields
: RemoteFormFieldContainer<Original[0]> & {
[K in KeysOfUnion<Original[0]>]-?: RemoteFormFields<ValueOfUnionKey<Original[0], K>>;
};
/**
* Recursive type to build form fields structure with proxy access
*/
export type RemoteFormFields<T> =
WillRecurseIndefinitely<T> extends true
? RecursiveFormFields
: NonNullable<T> extends string | number | boolean | File
? RemoteFormField<NonNullable<T>>
: IsImageInputValue<NonNullable<T>> extends true
? RemoteFormField<NonNullable<T> & ImageInputValue> &
Pick<RemoteFormFieldContainer<T>, 'allIssues'> & {
[K in KeysOfUnion<T>]-?: RemoteFormFields<ValueOfUnionKey<T, K>>;
}
: // [NonNullable<T>] is used to prevent distributing over union while still allowing
// nullable wrappers (e.g. `string[] | undefined` from a schema with `.default([])`)
// to be treated as arrays; only the last condition should distribute over unions
[NonNullable<T>] extends [string[] | File[]]
? RemoteFormField<NonNullable<T>> & {
[K in number]: RemoteFormField<NonNullable<T>[number]>;
}
: [NonNullable<T>] extends [Array<infer U>]
? RemoteFormFieldContainer<NonNullable<T>> & {
[K in number]: RemoteFormFields<U>;
}
: RemoteFormFieldContainer<T> & {
[K in KeysOfUnion<T>]-?: RemoteFormFields<ValueOfUnionKey<T, K>>;
};
// By breaking this out into its own type, we avoid the TS recursion depth limit
type RecursiveFormFields = RemoteFormFieldContainer<any> & {
[key: string | number]: UnknownField<any>;
};
type MaybeArray<T> = T | T[];
export interface RemoteFormInput {
[key: string]: MaybeArray<string | number | boolean | File | RemoteFormInput> | undefined;
}
export interface RemoteFormIssue {
message: string;
path: Array<string | number>;
}
// If the schema specifies `id` as a string or number, ensure that `for(...)`
// only accepts that type. Otherwise, accept `string | number`
type ExtractId<Input> = Input extends { id: infer Id }
? Id extends string | number
? Id
: string | number
: string | number;
/**
* A function and proxy object used to imperatively create validation errors in form handlers.
*
* Access properties to create field-specific issues: `issue.fieldName('message')`.
* The type structure mirrors the input data structure for type-safe field access.
* Call `invalid(issue.foo(...), issue.nested.bar(...))` to throw a validation error.
*/
export type RemoteFormInvalidField<T> =
WillRecurseIndefinitely<T> extends true
? Record<string | number, any>
: NonNullable<T> extends string | number | boolean | File
? (message: string) => StandardSchemaV1.Issue
: NonNullable<T> extends Array<infer U>
? {
[K in number]: RemoteFormInvalidField<U>;
} & ((message: string) => StandardSchemaV1.Issue)
: NonNullable<T> extends RemoteFormInput
? {
[K in keyof T]-?: RemoteFormInvalidField<T[K]>;
} & ((message: string) => StandardSchemaV1.Issue)
: Record<string, never>;
/**
* The form instance as received inside an `enhance` callback. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
*/
export type RemoteFormEnhanceInstance<
Input extends RemoteFormInput | void = RemoteFormInput | void,
Output = any
> = Omit<RemoteForm<Input, Output>, 'enhance' | 'element'> & {
readonly element: HTMLFormElement;
};
/**
* The callback passed to a remote form's `enhance` method. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
*/
export type RemoteFormEnhanceCallback<
Input extends RemoteFormInput | void = RemoteFormInput | void,
Output = any
> = (form: RemoteFormEnhanceInstance<Input, Output>) => MaybePromise<void>;
/**
* The type of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
*/
export type RemoteForm<Input extends RemoteFormInput | void, Output> = RemoteForm_<
Input,
Output,
[Input]
>;
type RemoteForm_<
Input extends RemoteFormInput | void,
Output,
Original extends [RemoteFormInput | void]
> = {
/** Attachment that sets up an event handler that intercepts the form submission on the client to prevent a full page reload */
[attachment: symbol]: (node: HTMLFormElement) => void;
method: 'POST';
/** The URL to send the form to. */
action: string;
/** The `<form>` element this instance is currently attached to, if any. */
get element(): HTMLFormElement | null;
/** Submit the currently attached form programmatically. */
submit(): Promise<boolean> & {
updates: (...updates: RemoteQueryUpdate[]) => Promise<boolean>;
};
/** Use the `enhance` method to influence what happens when the form is submitted. */
enhance(callback: RemoteFormEnhanceCallback<Input, Output>): {
method: 'POST';
action: string;
[attachment: symbol]: (node: HTMLFormElement) => void;
};
/**
* Create an instance of the form for the given `id`.
* The `id` is stringified and used for deduplication to potentially reuse existing instances.
* Useful when you have multiple forms that use the same remote form action, for example in a loop.
* ```svelte
* {#each todos as todo}
* {const todoForm = updateTodo.for(todo.id)}
* <form {...todoForm}>
* {#if todoForm.result?.invalid}<p>Invalid data</p>{/if}
* ...
* </form>
* {/each}
* ```
*/
for(id: ExtractId<Input>): Omit<RemoteForm<Input, Output>, 'for'>;
/** Preflight checks */
preflight(schema: StandardSchemaV1<Input, any>): RemoteForm<Input, Output>;
/** Validate the form contents programmatically */
validate(options?: {
/**
* Set this to `true` to also show validation issues of fields that haven't yet been
* edited and blurred. This option is ignored for forms that have previously been
* submitted, in which case all fields are always subject to validation
* (unless the form is reset, at which point it is treated as pristine)
*/
all?: boolean;
/** Set this to `true` to only run the `preflight` validation. */
preflightOnly?: boolean;
}): Promise<void>;
/** The result of the form submission */
get result(): Output | undefined;
/** The number of pending submissions */
get pending(): number;
/** True if the form has been submitted at least once, and hasn't been reset since */
get submitted(): boolean;
/** Access form fields using object notation */
fields: RemoteFormFieldsRoot<Input, Original>;
};
/**
* The type of a remote `command` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#command) for full documentation.
*/
export type RemoteCommand<Input, Output> = {
(arg: undefined extends Input ? Input | void : Input): Promise<Output> & {
updates(...updates: RemoteQueryUpdate[]): Promise<Output>;
};
/** The number of pending command executions */
get pending(): number;
};
export type RemoteQueryUpdate =
| RemoteQuery<any>
| RemoteLiveQuery<any>
| RemoteQueryFunction<any, any>
| RemoteLiveQueryFunction<any, any>
| RemoteQueryOverride;
export type RemoteResource<T> = Promise<T> & {
/** The error in case the query fails. */
get error(): App.Error | undefined;
/** `true` before the first result is available and during refreshes */
get loading(): boolean;
} & (
| {
/** The current value of the query. Undefined until `ready` is `true` */
get current(): undefined;
ready: false;
}
| {
/** The current value of the query. Undefined until `ready` is `true` */
get current(): T;
ready: true;
}
);
export type RemoteQuery<T> = RemoteResource<T> & {
/**
* On the client, this function will update the value of the query without re-fetching it.
*
* On the server, this can be called in the context of a `command` or `form` and the specified data will accompany the action response back to the client.
* This prevents SvelteKit needing to refresh all queries on the page in a second server round-trip.
*/
set(value: T): void;
/**
* On the client, this function will re-fetch the query from the server.
*
* On the server, this can be called in the context of a `command` or `form` and the refreshed data will accompany the action response back to the client.
* This prevents SvelteKit needing to refresh all queries on the page in a second server round-trip.
*/
refresh(): Promise<void>;
/**
* Temporarily override a query's value during a [single-flight mutation](https://svelte.dev/docs/kit/remote-functions#Single-flight-mutations) to provide optimistic updates.
*
* ```svelte
* <script>
* import { getTodos, addTodo } from './todos.remote.js';
* const todos = getTodos();
* </script>
*
* <form {...addTodo.enhance(async (form) => {
* await form.submit().updates(
* todos.withOverride((todos) => [...todos, { text: form.fields.text.value() }])
* );
* })}>
* <input type="text" name="text" />
* <button type="submit">Add Todo</button>
* </form>
* ```
*/
withOverride(update: (current: T) => T): RemoteQueryOverride;
};
export type RemoteLiveQuery<T> = RemoteResource<T> &
AsyncIterable<T> & {
/** `true` if the live stream is currently connected. */
readonly connected: boolean;
/** `true` once the current live stream iterator is done. */
readonly done: boolean;
/** Reconnects the live stream immediately. */
reconnect(): Promise<void>;
};
export type RemoteQueryOverride = () => void;
/**
* The type of a remote `prerender` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
*/
export type RemotePrerenderFunction<Input, Output> = (
arg: undefined extends Input ? Input | void : Input
) => RemoteResource<Output>;
/**
* The return value of a remote `query` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query) for full documentation.
*
* The optional `Validated` generic parameter represents the argument type *after* the
* query's schema has validated and (optionally) transformed it — this is the type the
* query's implementation function receives on the server, and the type yielded by
* [`requested`](https://svelte.dev/docs/kit/$app-server#requested). For queries declared
* with [Standard Schema](https://standardschema.dev/) it differs from `Input` when the
* schema contains a transform (e.g. `v.pipe(v.number(), v.transform(String))` has
* `Input = number` but `Validated = string`). For `'unchecked'` validators and queries
* without arguments it defaults to `Input`.
*/
export type RemoteQueryFunction<Input, Output, _Validated = Input> = (
arg: undefined extends Input ? Input | void : Input
) => RemoteQuery<Output>;
/**
* The type of a remote `query.live` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.live) for full documentation.
*
* The optional `Validated` generic parameter represents the argument type *after* the
* query's schema has validated and (optionally) transformed it, and matches the type
* yielded by [`requested`](https://svelte.dev/docs/kit/$app-server#requested).
*/
export type RemoteLiveQueryFunction<Input, Output, _Validated = Input> = (
arg: undefined extends Input ? Input | void : Input
) => RemoteLiveQuery<Output>;
/**
* A single entry yielded by [`requested`](https://svelte.dev/docs/kit/$app-server#requested)
* when called with a regular `query`. `arg` is the validated argument (the input *after*
* the query's schema validated and transformed it, if applicable); `query` is a
* `RemoteQuery` bound to the client's original cache key, so `refresh()` / `set()` will
* update the correct client entry.
*/
export type RequestedEntry<Validated, Output> = {
arg: Validated;
query: RemoteQuery<Output>;
/** Explicitly ignore this requested update. */
ignore: () => void;
};
/**
* A single entry yielded by [`requested`](https://svelte.dev/docs/kit/$app-server#requested)
* when called with a `query.live`. `arg` is the validated argument; `query` is a
* `RemoteLiveQuery` bound to the client's original cache key, so `reconnect()` targets
* the correct client subscription.
*/
export type RemoteLiveQueryRequestedEntry<Validated, Output> = {
arg: Validated;
query: RemoteLiveQuery<Output>;
/** Explicitly ignore this requested update. */
ignore: () => void;
};
export type RemoteQueryRequestedResult<Validated, Output> = Iterable<
RequestedEntry<Validated, Output>
> &
AsyncIterable<RequestedEntry<Validated, Output>> & {
/**
* Call `refresh` on all queries selected by this `requested` invocation.
* This is identical to:
* ```ts
* import { requested } from '$app/server';
*
* for await (const { query } of requested(getPost, ...)) {
* void query.refresh();
* }
* ```
*/
refreshAll: () => Promise<void>;
/** Explicitly ignore all updates selected by this `requested` invocation. */
ignoreAll: () => Promise<void>;
};
export type RemoteLiveQueryRequestedResult<Validated, Output> = Iterable<
RemoteLiveQueryRequestedEntry<Validated, Output>
> &
AsyncIterable<RemoteLiveQueryRequestedEntry<Validated, Output>> & {
/**
* Call `reconnect` on all live queries selected by this `requested` invocation.
* This is identical to:
* ```ts
* import { requested } from '$app/server';
*
* for await (const { query } of requested(liveQuery, ...)) {
* void query.reconnect();
* }
* ```
*/
reconnectAll: () => Promise<void>;
/** Explicitly ignore all updates selected by this `requested` invocation. */
ignoreAll: () => Promise<void>;
};
export type RequestedResult<Validated, Output> =
| RemoteQueryRequestedResult<Validated, Output>
| RemoteLiveQueryRequestedResult<Validated, Output>;