@sveltejs/kit 2.43.7 → 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 +2 -2
- package/src/core/postbuild/prerender.js +2 -1
- package/src/exports/public.d.ts +118 -79
- package/src/exports/vite/dev/index.js +23 -10
- package/src/exports/vite/index.js +26 -2
- package/src/runtime/app/server/remote/form.js +51 -25
- package/src/runtime/app/server/remote/shared.js +1 -3
- package/src/runtime/client/remote-functions/command.svelte.js +3 -1
- package/src/runtime/client/remote-functions/form.svelte.js +127 -47
- package/src/runtime/client/remote-functions/query.svelte.js +12 -1
- package/src/runtime/client/remote-functions/shared.svelte.js +9 -1
- package/src/runtime/form-utils.svelte.js +435 -0
- package/src/runtime/server/remote.js +1 -2
- package/src/runtime/server/respond.js +4 -4
- package/src/runtime/utils.js +0 -123
- package/src/types/internal.d.ts +8 -1
- package/src/version.js +1 -1
- package/types/index.d.ts +118 -79
- package/types/index.d.ts.map +12 -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,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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
Array.from(form.querySelectorAll(`[name="${name}[]"]`))
|
|
280
|
-
);
|
|
322
|
+
let value;
|
|
281
323
|
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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
|
|
363
|
+
input = set_nested_value(input, name, file);
|
|
306
364
|
} else {
|
|
307
|
-
|
|
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
|
|
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
|
-
|
|
395
|
-
get: () =>
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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<
|
|
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
|
-
|
|
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: {
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
/** @import { RemoteQueryFunction } from '@sveltejs/kit' */
|
|
2
2
|
/** @import { RemoteFunctionResponse } from 'types' */
|
|
3
3
|
import { app_dir, base } from '$app/paths/internal/client';
|
|
4
|
-
import { app, goto, remote_responses } from '../client.js';
|
|
4
|
+
import { app, goto, query_map, remote_responses } from '../client.js';
|
|
5
5
|
import { tick } from 'svelte';
|
|
6
6
|
import { create_remote_function, remote_request } from './shared.svelte.js';
|
|
7
7
|
import * as devalue from 'devalue';
|
|
8
8
|
import { HttpError, Redirect } from '@sveltejs/kit/internal';
|
|
9
|
+
import { DEV } from 'esm-env';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @param {string} id
|
|
12
13
|
* @returns {RemoteQueryFunction<any, any>}
|
|
13
14
|
*/
|
|
14
15
|
export function query(id) {
|
|
16
|
+
if (DEV) {
|
|
17
|
+
// If this reruns as part of HMR, refresh the query
|
|
18
|
+
for (const [key, entry] of query_map) {
|
|
19
|
+
if (key === id || key.startsWith(id + '/')) {
|
|
20
|
+
// use optional chaining in case a prerender function was turned into a query
|
|
21
|
+
entry.resource.refresh?.();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
return create_remote_function(id, (cache_key, payload) => {
|
|
16
27
|
return new Query(cache_key, async () => {
|
|
17
28
|
if (Object.hasOwn(remote_responses, cache_key)) {
|
|
@@ -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');
|