@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.
- package/package.json +2 -2
- package/src/core/postbuild/prerender.js +2 -1
- package/src/exports/public.d.ts +129 -82
- package/src/runtime/app/server/remote/form.js +58 -26
- package/src/runtime/app/server/remote/shared.js +36 -32
- package/src/runtime/client/remote-functions/command.svelte.js +3 -1
- package/src/runtime/client/remote-functions/form.svelte.js +141 -51
- package/src/runtime/client/remote-functions/shared.svelte.js +9 -1
- package/src/runtime/form-utils.svelte.js +439 -0
- package/src/runtime/server/page/render.js +2 -2
- package/src/runtime/server/remote.js +14 -4
- package/src/runtime/server/respond.js +6 -5
- package/src/runtime/utils.js +0 -123
- package/src/types/internal.d.ts +9 -1
- package/src/version.js +1 -1
- package/types/index.d.ts +129 -82
- package/types/index.d.ts.map +13 -5
|
@@ -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 {
|
|
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
|
-
/**
|
|
32
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
Array.from(form.querySelectorAll(`[name="${name}[]"]`))
|
|
280
|
-
);
|
|
334
|
+
let value;
|
|
281
335
|
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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
|
|
375
|
+
input = set_nested_value(input, name, file);
|
|
306
376
|
} else {
|
|
307
|
-
|
|
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
|
|
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
|
-
|
|
395
|
-
get: () =>
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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<
|
|
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
|
-
|
|
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');
|