@sveltejs/kit 2.43.8 → 2.45.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.
@@ -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,18 +80,35 @@ export function form(id) {
49
80
  /** @type {Record<string, boolean>} */
50
81
  let touched = {};
51
82
 
83
+ let submitted = false;
84
+
85
+ /**
86
+ * @param {FormData} form_data
87
+ * @returns {Record<string, any>}
88
+ */
89
+ function convert(form_data) {
90
+ const data = convert_formdata(form_data);
91
+ if (key !== undefined && !form_data.has('id')) {
92
+ data.id = key;
93
+ }
94
+ return data;
95
+ }
96
+
52
97
  /**
53
98
  * @param {HTMLFormElement} form
54
99
  * @param {FormData} form_data
55
100
  * @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback
56
101
  */
57
102
  async function handle_submit(form, form_data, callback) {
58
- const data = convert_formdata(form_data);
103
+ const data = convert(form_data);
104
+
105
+ submitted = true;
59
106
 
60
107
  const validated = await preflight_schema?.['~standard'].validate(data);
61
108
 
62
109
  if (validated?.issues) {
63
- issues = flatten_issues(validated.issues);
110
+ const client_issues = flatten_issues(validated.issues, false);
111
+ issues = merge_with_server_issues(issues, client_issues);
64
112
  return;
65
113
  }
66
114
 
@@ -134,7 +182,11 @@ export function form(id) {
134
182
 
135
183
  const response = await fetch(`${base}/${app_dir}/remote/${action_id}`, {
136
184
  method: 'POST',
137
- body: data
185
+ body: data,
186
+ headers: {
187
+ 'x-sveltekit-pathname': location.pathname,
188
+ 'x-sveltekit-search': location.search
189
+ }
138
190
  });
139
191
 
140
192
  if (!response.ok) {
@@ -146,21 +198,25 @@ export function form(id) {
146
198
  const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
147
199
 
148
200
  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
- }));
201
+ ({ issues = {}, result } = devalue.parse(form_result.result, app.decoders));
202
+
203
+ // Mark server issues with server: true
204
+ for (const issue_list of Object.values(issues)) {
205
+ for (const issue of issue_list) {
206
+ issue.server = true;
207
+ }
208
+ }
157
209
 
158
210
  if (issues.$) {
159
211
  release_overrides(updates);
160
- } else if (form_result.refreshes) {
161
- refresh_queries(form_result.refreshes, updates);
162
212
  } else {
163
- void invalidateAll();
213
+ input = {};
214
+
215
+ if (form_result.refreshes) {
216
+ refresh_queries(form_result.refreshes, updates);
217
+ } else {
218
+ void invalidateAll();
219
+ }
164
220
  }
165
221
  } else if (form_result.type === 'redirect') {
166
222
  const refreshes = form_result.refreshes ?? '';
@@ -275,23 +331,37 @@ export function form(id) {
275
331
  touched[name] = true;
276
332
 
277
333
  if (is_array) {
278
- const elements = /** @type {HTMLInputElement[]} */ (
279
- Array.from(form.querySelectorAll(`[name="${name}[]"]`))
280
- );
334
+ let value;
281
335
 
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
- );
336
+ if (element.tagName === 'SELECT') {
337
+ value = Array.from(
338
+ element.querySelectorAll('option:checked'),
339
+ (e) => /** @type {HTMLOptionElement} */ (e).value
340
+ );
341
+ } else {
342
+ const elements = /** @type {HTMLInputElement[]} */ (
343
+ Array.from(form.querySelectorAll(`[name="${name}[]"]`))
344
+ );
345
+
346
+ if (DEV) {
347
+ for (const e of elements) {
348
+ if ((e.type === 'file') !== is_file) {
349
+ throw new Error(
350
+ `Cannot mix and match file and non-file inputs under the same name ("${element.name}")`
351
+ );
352
+ }
288
353
  }
289
354
  }
355
+
356
+ value = is_file
357
+ ? elements.map((input) => Array.from(input.files ?? [])).flat()
358
+ : elements.map((element) => element.value);
359
+ if (element.type === 'checkbox') {
360
+ value = /** @type {string[]} */ (value.filter((_, i) => elements[i].checked));
361
+ }
290
362
  }
291
363
 
292
- input[name] = is_file
293
- ? elements.map((input) => Array.from(input.files ?? [])).flat()
294
- : elements.map((element) => element.value);
364
+ input = set_nested_value(input, name, value);
295
365
  } else if (is_file) {
296
366
  if (DEV && element.multiple) {
297
367
  throw new Error(
@@ -302,12 +372,23 @@ export function form(id) {
302
372
  const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
303
373
 
304
374
  if (file) {
305
- input[name] = file;
375
+ input = set_nested_value(input, name, file);
306
376
  } else {
307
- delete input[name];
377
+ // Remove the property by setting to undefined and clean up
378
+ const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
379
+ let current = /** @type {any} */ (input);
380
+ for (let i = 0; i < path_parts.length - 1; i++) {
381
+ if (current[path_parts[i]] == null) return;
382
+ current = current[path_parts[i]];
383
+ }
384
+ delete current[path_parts[path_parts.length - 1]];
308
385
  }
309
386
  } else {
310
- input[name] = element.value;
387
+ input = set_nested_value(
388
+ input,
389
+ name,
390
+ element.type === 'checkbox' && !element.checked ? null : element.value
391
+ );
311
392
  }
312
393
  });
313
394
 
@@ -387,18 +468,29 @@ export function form(id) {
387
468
 
388
469
  let validate_id = 0;
389
470
 
471
+ // TODO 3.0 remove
472
+ if (DEV) {
473
+ throw_on_old_property_access(instance);
474
+ }
475
+
390
476
  Object.defineProperties(instance, {
391
477
  buttonProps: {
392
478
  value: button_props
393
479
  },
394
- input: {
395
- get: () => input,
396
- set: (v) => {
397
- input = v;
398
- }
399
- },
400
- issues: {
401
- get: () => issues
480
+ fields: {
481
+ get: () =>
482
+ create_field_proxy(
483
+ {},
484
+ () => input,
485
+ (path, value) => {
486
+ if (path.length === 0) {
487
+ input = value;
488
+ } else {
489
+ input = deep_set(input, path.map(String), value);
490
+ }
491
+ },
492
+ () => issues
493
+ )
402
494
  },
403
495
  result: {
404
496
  get: () => result
@@ -406,11 +498,8 @@ export function form(id) {
406
498
  pending: {
407
499
  get: () => pending_count
408
500
  },
409
- field: {
410
- value: (/** @type {string} */ name) => name
411
- },
412
501
  preflight: {
413
- /** @type {RemoteForm<any, any>['preflight']} */
502
+ /** @type {RemoteForm<T, U>['preflight']} */
414
503
  value: (schema) => {
415
504
  preflight_schema = schema;
416
505
  return instance;
@@ -428,9 +517,7 @@ export function form(id) {
428
517
  /** @type {readonly StandardSchemaV1.Issue[]} */
429
518
  let array = [];
430
519
 
431
- const validated = await preflight_schema?.['~standard'].validate(
432
- convert_formdata(form_data)
433
- );
520
+ const validated = await preflight_schema?.['~standard'].validate(convert(form_data));
434
521
 
435
522
  if (validated?.issues) {
436
523
  array = validated.issues;
@@ -455,7 +542,7 @@ export function form(id) {
455
542
  }
456
543
  }
457
544
 
458
- if (!includeUntouched) {
545
+ if (!includeUntouched && !submitted) {
459
546
  array = array.filter((issue) => {
460
547
  if (issue.path !== undefined) {
461
548
  let path = '';
@@ -475,7 +562,10 @@ export function form(id) {
475
562
  });
476
563
  }
477
564
 
478
- issues = flatten_issues(array);
565
+ const is_server_validation = !validated?.issues;
566
+ const new_issues = flatten_issues(array, is_server_validation);
567
+
568
+ issues = is_server_validation ? new_issues : merge_with_server_issues(issues, new_issues);
479
569
  }
480
570
  },
481
571
  enhance: {
@@ -12,7 +12,15 @@ import { create_remote_cache_key, stringify_remote_arg } from '../../shared.js';
12
12
  * @param {string} url
13
13
  */
14
14
  export async function remote_request(url) {
15
- const response = await fetch(url);
15
+ const response = await fetch(url, {
16
+ headers: {
17
+ // TODO in future, when we support forking, we will likely need
18
+ // to grab this from context as queries will run before
19
+ // `location.pathname` is updated
20
+ 'x-sveltekit-pathname': location.pathname,
21
+ 'x-sveltekit-search': location.search
22
+ }
23
+ });
16
24
 
17
25
  if (!response.ok) {
18
26
  throw new HttpError(500, 'Failed to execute remote function');