@sveltejs/kit 2.43.8 → 2.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "2.43.8",
3
+ "version": "2.44.0",
4
4
  "description": "SvelteKit is the fastest way to build Svelte apps",
5
5
  "keywords": [
6
6
  "framework",
@@ -41,7 +41,7 @@
41
41
  "@types/set-cookie-parser": "^2.4.7",
42
42
  "dts-buddy": "^0.6.2",
43
43
  "rollup": "^4.14.2",
44
- "svelte": "^5.39.3",
44
+ "svelte": "^5.39.8",
45
45
  "svelte-preprocess": "^6.0.0",
46
46
  "typescript": "^5.3.3",
47
47
  "vite": "^6.3.5",
@@ -320,7 +320,8 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
320
320
  // avoid triggering `filterSerializeResponseHeaders` guard
321
321
  const headers = Object.fromEntries(response.headers);
322
322
 
323
- if (config.prerender.crawl && headers['content-type'] === 'text/html') {
323
+ // if it's a 200 HTML response, crawl it. Skip error responses, as we don't save those
324
+ if (response.ok && config.prerender.crawl && headers['content-type'] === 'text/html') {
324
325
  const { ids, hrefs } = crawl(body.toString(), decoded);
325
326
 
326
327
  actual_hashlinks.set(decoded, ids);
@@ -1813,78 +1813,127 @@ export interface Snapshot<T = any> {
1813
1813
  // If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle
1814
1814
  type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
1815
1815
 
1816
- // Helper type to convert union to intersection
1817
- type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
1818
- ? I
1819
- : never;
1820
-
1821
- type FlattenInput<T, Prefix extends string> = T extends string | number | boolean | null | undefined
1822
- ? { [P in Prefix]: string }
1823
- : WillRecurseIndefinitely<T> extends true
1824
- ? { [key: string]: string }
1825
- : T extends Array<infer U>
1826
- ? U extends string | File
1827
- ? { [P in Prefix]: string[] }
1828
- : FlattenInput<U, `${Prefix}[${number}]`>
1829
- : T extends File
1830
- ? { [P in Prefix]: string }
1831
- : {
1832
- // Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1833
- [K in keyof Required<T>]: FlattenInput<
1834
- T[K],
1835
- Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1836
- >;
1837
- }[keyof T];
1838
-
1839
- type FlattenIssues<T, Prefix extends string> = T extends
1840
- | string
1841
- | number
1842
- | boolean
1843
- | null
1844
- | undefined
1845
- ? { [P in Prefix]: RemoteFormIssue[] }
1846
- : WillRecurseIndefinitely<T> extends true
1847
- ? { [key: string]: RemoteFormIssue[] }
1848
- : T extends Array<infer U>
1849
- ? { [P in Prefix | `${Prefix}[${number}]`]: RemoteFormIssue[] } & FlattenIssues<
1850
- U,
1851
- `${Prefix}[${number}]`
1852
- >
1853
- : T extends File
1854
- ? { [P in Prefix]: RemoteFormIssue[] }
1855
- : {
1856
- // Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1857
- [K in keyof Required<T>]: FlattenIssues<
1858
- T[K],
1859
- Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1860
- >;
1861
- }[keyof T];
1862
-
1863
- type FlattenKeys<T, Prefix extends string> = T extends string | number | boolean | null | undefined
1864
- ? { [P in Prefix]: string }
1865
- : WillRecurseIndefinitely<T> extends true
1866
- ? { [key: string]: string }
1867
- : T extends Array<infer U>
1868
- ? U extends string | File
1869
- ? { [P in `${Prefix}[]`]: string[] }
1870
- : FlattenKeys<U, `${Prefix}[${number}]`>
1871
- : T extends File
1872
- ? { [P in Prefix]: string }
1873
- : {
1874
- // Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1875
- [K in keyof Required<T>]: FlattenKeys<
1876
- T[K],
1877
- Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1878
- >;
1879
- }[keyof T];
1816
+ // Input type mappings for form fields
1817
+ type InputTypeMap = {
1818
+ text: string;
1819
+ email: string;
1820
+ password: string;
1821
+ url: string;
1822
+ tel: string;
1823
+ search: string;
1824
+ number: number;
1825
+ range: number;
1826
+ date: string;
1827
+ 'datetime-local': string;
1828
+ time: string;
1829
+ month: string;
1830
+ week: string;
1831
+ color: string;
1832
+ checkbox: boolean | string[];
1833
+ radio: string;
1834
+ file: File;
1835
+ hidden: string;
1836
+ submit: string;
1837
+ button: string;
1838
+ reset: string;
1839
+ image: string;
1840
+ select: string;
1841
+ 'select multiple': string[];
1842
+ 'file multiple': File[];
1843
+ };
1844
+
1845
+ // Valid input types for a given value type
1846
+ export type RemoteFormFieldType<T> = {
1847
+ [K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never;
1848
+ }[keyof InputTypeMap];
1849
+
1850
+ // Input element properties based on type
1851
+ type InputElementProps<T extends keyof InputTypeMap> = T extends 'checkbox' | 'radio'
1852
+ ? {
1853
+ type: T;
1854
+ 'aria-invalid': boolean | 'false' | 'true' | undefined;
1855
+ get checked(): boolean;
1856
+ set checked(value: boolean);
1857
+ }
1858
+ : T extends 'file'
1859
+ ? {
1860
+ type: 'file';
1861
+ 'aria-invalid': boolean | 'false' | 'true' | undefined;
1862
+ get files(): FileList | null;
1863
+ set files(v: FileList | null);
1864
+ }
1865
+ : {
1866
+ type: T;
1867
+ 'aria-invalid': boolean | 'false' | 'true' | undefined;
1868
+ get value(): string | number;
1869
+ set value(v: string | number);
1870
+ };
1871
+
1872
+ type RemoteFormFieldMethods<T> = {
1873
+ /** The values that will be submitted */
1874
+ value(): T;
1875
+ /** Set the values that will be submitted */
1876
+ set(input: T): T;
1877
+ /** Validation issues, if any */
1878
+ issues(): RemoteFormIssue[] | undefined;
1879
+ };
1880
+
1881
+ export type RemoteFormFieldValue = string | string[] | number | boolean | File | File[];
1882
+
1883
+ type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
1884
+ ? Value extends string[]
1885
+ ? [type: 'checkbox', value: Value[number] | (string & {})]
1886
+ : [type: Type]
1887
+ : Type extends 'radio'
1888
+ ? [type: 'radio', value: Value | (string & {})]
1889
+ : [type: Type];
1890
+
1891
+ /**
1892
+ * Form field accessor type that provides name(), value(), and issues() methods
1893
+ */
1894
+ export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFieldMethods<Value> & {
1895
+ /**
1896
+ * Returns an object that can be spread onto an input element with the correct type attribute,
1897
+ * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
1898
+ * @example
1899
+ * ```svelte
1900
+ * <input {...myForm.fields.myString.as('text')} />
1901
+ * <input {...myForm.fields.myNumber.as('number')} />
1902
+ * <input {...myForm.fields.myBoolean.as('checkbox')} />
1903
+ * ```
1904
+ */
1905
+ as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T>;
1906
+ };
1907
+
1908
+ type RemoteFormFieldContainer<Value> = RemoteFormFieldMethods<Value> & {
1909
+ /** Validation issues belonging to this or any of the fields that belong to it, if any */
1910
+ allIssues(): RemoteFormIssue[] | undefined;
1911
+ };
1912
+
1913
+ /**
1914
+ * Recursive type to build form fields structure with proxy access
1915
+ */
1916
+ type RemoteFormFields<T> =
1917
+ WillRecurseIndefinitely<T> extends true
1918
+ ? RecursiveFormFields
1919
+ : NonNullable<T> extends string | number | boolean | File
1920
+ ? RemoteFormField<NonNullable<T>>
1921
+ : T extends string[] | File[]
1922
+ ? RemoteFormField<T> & { [K in number]: RemoteFormField<T[number]> }
1923
+ : T extends Array<infer U>
1924
+ ? RemoteFormFieldContainer<T> & { [K in number]: RemoteFormFields<U> }
1925
+ : RemoteFormFieldContainer<T> & { [K in keyof T]-?: RemoteFormFields<T[K]> };
1926
+
1927
+ // By breaking this out into its own type, we avoid the TS recursion depth limit
1928
+ type RecursiveFormFields = RemoteFormField<any> & { [key: string]: RecursiveFormFields };
1929
+
1930
+ type MaybeArray<T> = T | T[];
1880
1931
 
1881
1932
  export interface RemoteFormInput {
1882
- [key: string]: FormDataEntryValue | FormDataEntryValue[] | RemoteFormInput | RemoteFormInput[];
1933
+ [key: string]: MaybeArray<string | number | boolean | File | RemoteFormInput>;
1883
1934
  }
1884
1935
 
1885
1936
  export interface RemoteFormIssue {
1886
- name: string;
1887
- path: Array<string | number>;
1888
1937
  message: string;
1889
1938
  }
1890
1939
 
@@ -1926,14 +1975,6 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
1926
1975
  * ```
1927
1976
  */
1928
1977
  for(key: string | number | boolean): Omit<RemoteForm<Input, Output>, 'for'>;
1929
- /**
1930
- * This method exists to allow you to typecheck `name` attributes. It returns its argument
1931
- * @example
1932
- * ```svelte
1933
- * <input name={login.field('username')} />
1934
- * ```
1935
- **/
1936
- field<Name extends keyof UnionToIntersection<FlattenKeys<Input, ''>>>(string: Name): Name;
1937
1978
  /** Preflight checks */
1938
1979
  preflight(schema: StandardSchemaV1<Input, any>): RemoteForm<Input, Output>;
1939
1980
  /** Validate the form contents programmatically */
@@ -1946,10 +1987,8 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
1946
1987
  get result(): Output | undefined;
1947
1988
  /** The number of pending submissions */
1948
1989
  get pending(): number;
1949
- /** The submitted values */
1950
- input: null | UnionToIntersection<FlattenInput<Input, ''>>;
1951
- /** Validation issues */
1952
- issues: null | UnionToIntersection<FlattenIssues<Input, ''>>;
1990
+ /** Access form fields using object notation */
1991
+ fields: Input extends void ? never : RemoteFormFields<Input>;
1953
1992
  /** Spread this onto a `<button>` or `<input type="submit">` */
1954
1993
  buttonProps: {
1955
1994
  type: 'submit';
@@ -1,10 +1,17 @@
1
1
  /** @import { RemoteFormInput, RemoteForm } from '@sveltejs/kit' */
2
- /** @import { MaybePromise, RemoteInfo } from 'types' */
2
+ /** @import { InternalRemoteFormIssue, MaybePromise, RemoteInfo } from 'types' */
3
3
  /** @import { StandardSchemaV1 } from '@standard-schema/spec' */
4
4
  import { get_request_store } from '@sveltejs/kit/internal/server';
5
5
  import { DEV } from 'esm-env';
6
+ import {
7
+ convert_formdata,
8
+ flatten_issues,
9
+ create_field_proxy,
10
+ set_nested_value,
11
+ throw_on_old_property_access,
12
+ deep_set
13
+ } from '../../../form-utils.svelte.js';
6
14
  import { get_cache, run_remote_function } from './shared.js';
7
- import { convert_formdata, flatten_issues } from '../../../utils.js';
8
15
 
9
16
  /**
10
17
  * Creates a form object that can be spread onto a `<form>` element.
@@ -128,7 +135,7 @@ export function form(validate_or_fn, maybe_fn) {
128
135
  }
129
136
  }
130
137
 
131
- /** @type {{ input?: Record<string, string | string[]>, issues?: Record<string, StandardSchemaV1.Issue[]>, result: Output }} */
138
+ /** @type {{ input?: Record<string, any>, issues?: Record<string, InternalRemoteFormIssue[]>, result: Output }} */
132
139
  const output = {};
133
140
 
134
141
  const { event, state } = get_request_store();
@@ -140,18 +147,27 @@ export function form(validate_or_fn, maybe_fn) {
140
147
 
141
148
  if (validated?.issues !== undefined) {
142
149
  output.issues = flatten_issues(validated.issues);
143
- output.input = {};
144
150
 
145
- for (let key of form_data.keys()) {
146
- // redact sensitive fields
147
- if (/^[.\]]?_/.test(key)) continue;
151
+ // if it was a progressively-enhanced submission, we don't need
152
+ // to return the input — it's already there
153
+ if (!event.isRemoteRequest) {
154
+ output.input = {};
148
155
 
149
- const is_array = key.endsWith('[]');
150
- const values = form_data.getAll(key).filter((value) => typeof value === 'string');
156
+ for (let key of form_data.keys()) {
157
+ // redact sensitive fields
158
+ if (/^[.\]]?_/.test(key)) continue;
151
159
 
152
- if (is_array) key = key.slice(0, -2);
160
+ const is_array = key.endsWith('[]');
161
+ const values = form_data.getAll(key).filter((value) => typeof value === 'string');
153
162
 
154
- output.input[key] = is_array ? values : values[0];
163
+ if (is_array) key = key.slice(0, -2);
164
+
165
+ output.input = set_nested_value(
166
+ /** @type {Record<string, any>} */ (output.input),
167
+ key,
168
+ is_array ? values : values[0]
169
+ );
170
+ }
155
171
  }
156
172
  } else {
157
173
  if (validated !== undefined) {
@@ -185,16 +201,30 @@ export function form(validate_or_fn, maybe_fn) {
185
201
  enumerable: true
186
202
  });
187
203
 
188
- for (const property of ['input', 'issues']) {
189
- Object.defineProperty(instance, property, {
190
- get() {
191
- try {
192
- return get_cache(__)?.['']?.[property] ?? {};
193
- } catch {
194
- return undefined;
195
- }
196
- }
197
- });
204
+ Object.defineProperty(instance, 'fields', {
205
+ get() {
206
+ const data = get_cache(__)?.[''];
207
+ return create_field_proxy(
208
+ {},
209
+ () => data?.input ?? {},
210
+ (path, value) => {
211
+ if (data) {
212
+ // don't override a submission
213
+ return;
214
+ }
215
+
216
+ const input = path.length === 0 ? value : deep_set({}, path.map(String), value);
217
+
218
+ get_cache(__)[''] ??= { input };
219
+ },
220
+ () => data?.issues ?? {}
221
+ );
222
+ }
223
+ });
224
+
225
+ // TODO 3.0 remove
226
+ if (DEV) {
227
+ throw_on_old_property_access(instance);
198
228
  }
199
229
 
200
230
  Object.defineProperty(instance, 'result', {
@@ -217,10 +247,6 @@ export function form(validate_or_fn, maybe_fn) {
217
247
  get: () => 0
218
248
  });
219
249
 
220
- Object.defineProperty(instance, 'field', {
221
- value: (/** @type {string} */ name) => name
222
- });
223
-
224
250
  Object.defineProperty(instance, 'preflight', {
225
251
  // preflight is a noop on the server
226
252
  value: () => instance
@@ -133,9 +133,7 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali
133
133
 
134
134
  return event.cookies.delete(name, opts);
135
135
  }
136
- },
137
- route: { id: null },
138
- url: new URL(event.url.origin)
136
+ }
139
137
  };
140
138
 
141
139
  // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function
@@ -40,7 +40,9 @@ export function command(id) {
40
40
  refreshes: updates.map((u) => u._key)
41
41
  }),
42
42
  headers: {
43
- 'Content-Type': 'application/json'
43
+ 'Content-Type': 'application/json',
44
+ 'x-sveltekit-pathname': location.pathname,
45
+ 'x-sveltekit-search': location.search
44
46
  }
45
47
  });
46
48
 
@@ -1,6 +1,6 @@
1
1
  /** @import { StandardSchemaV1 } from '@standard-schema/spec' */
2
2
  /** @import { RemoteFormInput, RemoteForm, RemoteQueryOverride } from '@sveltejs/kit' */
3
- /** @import { RemoteFunctionResponse } from 'types' */
3
+ /** @import { InternalRemoteFormIssue, RemoteFunctionResponse } from 'types' */
4
4
  /** @import { Query } from './query.svelte.js' */
5
5
  import { app_dir, base } from '$app/paths/internal/client';
6
6
  import * as devalue from 'devalue';
@@ -10,7 +10,34 @@ import { app, remote_responses, _goto, set_nearest_error_page, invalidateAll } f
10
10
  import { tick } from 'svelte';
11
11
  import { refresh_queries, release_overrides } from './shared.svelte.js';
12
12
  import { createAttachmentKey } from 'svelte/attachments';
13
- import { convert_formdata, file_transport, flatten_issues } from '../../utils.js';
13
+ import {
14
+ convert_formdata,
15
+ flatten_issues,
16
+ create_field_proxy,
17
+ deep_set,
18
+ set_nested_value,
19
+ throw_on_old_property_access
20
+ } from '../../form-utils.svelte.js';
21
+
22
+ /**
23
+ * Merge client issues into server issues
24
+ * @param {Record<string, InternalRemoteFormIssue[]>} current_issues
25
+ * @param {Record<string, InternalRemoteFormIssue[]>} client_issues
26
+ * @returns {Record<string, InternalRemoteFormIssue[]>}
27
+ */
28
+ function merge_with_server_issues(current_issues, client_issues) {
29
+ const merged_issues = Object.fromEntries(
30
+ Object.entries(current_issues)
31
+ .map(([key, issue_list]) => [key, issue_list.filter((issue) => issue.server)])
32
+ .filter(([, issue_list]) => issue_list.length > 0)
33
+ );
34
+
35
+ for (const [key, new_issue_list] of Object.entries(client_issues)) {
36
+ merged_issues[key] = [...(merged_issues[key] || []), ...new_issue_list];
37
+ }
38
+
39
+ return merged_issues;
40
+ }
14
41
 
15
42
  /**
16
43
  * Client-version of the `form` function from `$app/server`.
@@ -28,10 +55,14 @@ export function form(id) {
28
55
  const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
29
56
  const action = '?/remote=' + encodeURIComponent(action_id);
30
57
 
31
- /** @type {Record<string, string | string[] | File | File[]>} */
32
- let input = $state({});
58
+ /**
59
+ * By making this $state.raw() and creating a new object each time we update it,
60
+ * all consumers along the update chain are properly invalidated.
61
+ * @type {Record<string, string | string[] | File | File[]>}
62
+ */
63
+ let input = $state.raw({});
33
64
 
34
- /** @type {Record<string, StandardSchemaV1.Issue[]>} */
65
+ /** @type {Record<string, InternalRemoteFormIssue[]>} */
35
66
  let issues = $state.raw({});
36
67
 
37
68
  /** @type {any} */
@@ -49,6 +80,8 @@ export function form(id) {
49
80
  /** @type {Record<string, boolean>} */
50
81
  let touched = {};
51
82
 
83
+ let submitted = false;
84
+
52
85
  /**
53
86
  * @param {HTMLFormElement} form
54
87
  * @param {FormData} form_data
@@ -57,10 +90,13 @@ export function form(id) {
57
90
  async function handle_submit(form, form_data, callback) {
58
91
  const data = convert_formdata(form_data);
59
92
 
93
+ submitted = true;
94
+
60
95
  const validated = await preflight_schema?.['~standard'].validate(data);
61
96
 
62
97
  if (validated?.issues) {
63
- issues = flatten_issues(validated.issues);
98
+ const client_issues = flatten_issues(validated.issues, false);
99
+ issues = merge_with_server_issues(issues, client_issues);
64
100
  return;
65
101
  }
66
102
 
@@ -134,7 +170,11 @@ export function form(id) {
134
170
 
135
171
  const response = await fetch(`${base}/${app_dir}/remote/${action_id}`, {
136
172
  method: 'POST',
137
- body: data
173
+ body: data,
174
+ headers: {
175
+ 'x-sveltekit-pathname': location.pathname,
176
+ 'x-sveltekit-search': location.search
177
+ }
138
178
  });
139
179
 
140
180
  if (!response.ok) {
@@ -146,21 +186,25 @@ export function form(id) {
146
186
  const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
147
187
 
148
188
  if (form_result.type === 'result') {
149
- ({
150
- input = {},
151
- issues = {},
152
- result
153
- } = devalue.parse(form_result.result, {
154
- ...app.decoders,
155
- File: file_transport.decode
156
- }));
189
+ ({ issues = {}, result } = devalue.parse(form_result.result, app.decoders));
190
+
191
+ // Mark server issues with server: true
192
+ for (const issue_list of Object.values(issues)) {
193
+ for (const issue of issue_list) {
194
+ issue.server = true;
195
+ }
196
+ }
157
197
 
158
198
  if (issues.$) {
159
199
  release_overrides(updates);
160
- } else if (form_result.refreshes) {
161
- refresh_queries(form_result.refreshes, updates);
162
200
  } else {
163
- void invalidateAll();
201
+ input = {};
202
+
203
+ if (form_result.refreshes) {
204
+ refresh_queries(form_result.refreshes, updates);
205
+ } else {
206
+ void invalidateAll();
207
+ }
164
208
  }
165
209
  } else if (form_result.type === 'redirect') {
166
210
  const refreshes = form_result.refreshes ?? '';
@@ -275,23 +319,37 @@ export function form(id) {
275
319
  touched[name] = true;
276
320
 
277
321
  if (is_array) {
278
- const elements = /** @type {HTMLInputElement[]} */ (
279
- Array.from(form.querySelectorAll(`[name="${name}[]"]`))
280
- );
322
+ let value;
281
323
 
282
- if (DEV) {
283
- for (const e of elements) {
284
- if ((e.type === 'file') !== is_file) {
285
- throw new Error(
286
- `Cannot mix and match file and non-file inputs under the same name ("${element.name}")`
287
- );
324
+ if (element.tagName === 'SELECT') {
325
+ value = Array.from(
326
+ element.querySelectorAll('option:checked'),
327
+ (e) => /** @type {HTMLOptionElement} */ (e).value
328
+ );
329
+ } else {
330
+ const elements = /** @type {HTMLInputElement[]} */ (
331
+ Array.from(form.querySelectorAll(`[name="${name}[]"]`))
332
+ );
333
+
334
+ if (DEV) {
335
+ for (const e of elements) {
336
+ if ((e.type === 'file') !== is_file) {
337
+ throw new Error(
338
+ `Cannot mix and match file and non-file inputs under the same name ("${element.name}")`
339
+ );
340
+ }
288
341
  }
289
342
  }
343
+
344
+ value = is_file
345
+ ? elements.map((input) => Array.from(input.files ?? [])).flat()
346
+ : elements.map((element) => element.value);
347
+ if (element.type === 'checkbox') {
348
+ value = /** @type {string[]} */ (value.filter((_, i) => elements[i].checked));
349
+ }
290
350
  }
291
351
 
292
- input[name] = is_file
293
- ? elements.map((input) => Array.from(input.files ?? [])).flat()
294
- : elements.map((element) => element.value);
352
+ input = set_nested_value(input, name, value);
295
353
  } else if (is_file) {
296
354
  if (DEV && element.multiple) {
297
355
  throw new Error(
@@ -302,12 +360,23 @@ export function form(id) {
302
360
  const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
303
361
 
304
362
  if (file) {
305
- input[name] = file;
363
+ input = set_nested_value(input, name, file);
306
364
  } else {
307
- delete input[name];
365
+ // Remove the property by setting to undefined and clean up
366
+ const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
367
+ let current = /** @type {any} */ (input);
368
+ for (let i = 0; i < path_parts.length - 1; i++) {
369
+ if (current[path_parts[i]] == null) return;
370
+ current = current[path_parts[i]];
371
+ }
372
+ delete current[path_parts[path_parts.length - 1]];
308
373
  }
309
374
  } else {
310
- input[name] = element.value;
375
+ input = set_nested_value(
376
+ input,
377
+ name,
378
+ element.type === 'checkbox' && !element.checked ? null : element.value
379
+ );
311
380
  }
312
381
  });
313
382
 
@@ -387,18 +456,29 @@ export function form(id) {
387
456
 
388
457
  let validate_id = 0;
389
458
 
459
+ // TODO 3.0 remove
460
+ if (DEV) {
461
+ throw_on_old_property_access(instance);
462
+ }
463
+
390
464
  Object.defineProperties(instance, {
391
465
  buttonProps: {
392
466
  value: button_props
393
467
  },
394
- input: {
395
- get: () => input,
396
- set: (v) => {
397
- input = v;
398
- }
399
- },
400
- issues: {
401
- get: () => issues
468
+ fields: {
469
+ get: () =>
470
+ create_field_proxy(
471
+ {},
472
+ () => input,
473
+ (path, value) => {
474
+ if (path.length === 0) {
475
+ input = value;
476
+ } else {
477
+ input = deep_set(input, path.map(String), value);
478
+ }
479
+ },
480
+ () => issues
481
+ )
402
482
  },
403
483
  result: {
404
484
  get: () => result
@@ -406,11 +486,8 @@ export function form(id) {
406
486
  pending: {
407
487
  get: () => pending_count
408
488
  },
409
- field: {
410
- value: (/** @type {string} */ name) => name
411
- },
412
489
  preflight: {
413
- /** @type {RemoteForm<any, any>['preflight']} */
490
+ /** @type {RemoteForm<T, U>['preflight']} */
414
491
  value: (schema) => {
415
492
  preflight_schema = schema;
416
493
  return instance;
@@ -455,7 +532,7 @@ export function form(id) {
455
532
  }
456
533
  }
457
534
 
458
- if (!includeUntouched) {
535
+ if (!includeUntouched && !submitted) {
459
536
  array = array.filter((issue) => {
460
537
  if (issue.path !== undefined) {
461
538
  let path = '';
@@ -475,7 +552,10 @@ export function form(id) {
475
552
  });
476
553
  }
477
554
 
478
- issues = flatten_issues(array);
555
+ const is_server_validation = !validated?.issues;
556
+ const new_issues = flatten_issues(array, is_server_validation);
557
+
558
+ issues = is_server_validation ? new_issues : merge_with_server_issues(issues, new_issues);
479
559
  }
480
560
  },
481
561
  enhance: {