@sveltejs/kit 2.48.7 → 2.49.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 +1 -1
- package/src/exports/index.js +46 -1
- package/src/exports/internal/index.js +16 -0
- package/src/exports/public.d.ts +11 -13
- package/src/runtime/app/server/remote/form.js +67 -97
- package/src/runtime/client/remote-functions/form.svelte.js +29 -20
- package/src/runtime/form-utils.js +341 -5
- package/src/runtime/server/remote.js +25 -27
- package/src/types/internal.d.ts +10 -1
- package/src/utils/http.js +4 -1
- package/src/version.js +1 -1
- package/types/index.d.ts +47 -17
- package/types/index.d.ts.map +4 -2
package/package.json
CHANGED
package/src/exports/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
|
2
|
+
|
|
3
|
+
import { HttpError, Redirect, ActionFailure, ValidationError } from './internal/index.js';
|
|
2
4
|
import { BROWSER, DEV } from 'esm-env';
|
|
3
5
|
import {
|
|
4
6
|
add_data_suffix,
|
|
@@ -215,6 +217,49 @@ export function isActionFailure(e) {
|
|
|
215
217
|
return e instanceof ActionFailure;
|
|
216
218
|
}
|
|
217
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Use this to throw a validation error to imperatively fail form validation.
|
|
222
|
+
* Can be used in combination with `issue` passed to form actions to create field-specific issues.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* import { invalid } from '@sveltejs/kit';
|
|
227
|
+
* import { form } from '$app/server';
|
|
228
|
+
* import { tryLogin } from '$lib/server/auth';
|
|
229
|
+
* import * as v from 'valibot';
|
|
230
|
+
*
|
|
231
|
+
* export const login = form(
|
|
232
|
+
* v.object({ name: v.string(), _password: v.string() }),
|
|
233
|
+
* async ({ name, _password }) => {
|
|
234
|
+
* const success = tryLogin(name, _password);
|
|
235
|
+
* if (!success) {
|
|
236
|
+
* invalid('Incorrect username or password');
|
|
237
|
+
* }
|
|
238
|
+
*
|
|
239
|
+
* // ...
|
|
240
|
+
* }
|
|
241
|
+
* );
|
|
242
|
+
* ```
|
|
243
|
+
* @param {...(StandardSchemaV1.Issue | string)} issues
|
|
244
|
+
* @returns {never}
|
|
245
|
+
* @since 2.47.3
|
|
246
|
+
*/
|
|
247
|
+
export function invalid(...issues) {
|
|
248
|
+
throw new ValidationError(
|
|
249
|
+
issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue))
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Checks whether this is an validation error thrown by {@link invalid}.
|
|
255
|
+
* @param {unknown} e The object to check.
|
|
256
|
+
* @return {e is import('./public.js').ActionFailure}
|
|
257
|
+
* @since 2.47.3
|
|
258
|
+
*/
|
|
259
|
+
export function isValidationError(e) {
|
|
260
|
+
return e instanceof ValidationError;
|
|
261
|
+
}
|
|
262
|
+
|
|
218
263
|
/**
|
|
219
264
|
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
|
|
220
265
|
* Returns the normalized URL as well as a method for adding the potential suffix back
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
|
2
|
+
|
|
1
3
|
export class HttpError {
|
|
2
4
|
/**
|
|
3
5
|
* @param {number} status
|
|
@@ -62,4 +64,18 @@ export class ActionFailure {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Error thrown when form validation fails imperatively
|
|
69
|
+
*/
|
|
70
|
+
export class ValidationError extends Error {
|
|
71
|
+
/**
|
|
72
|
+
* @param {StandardSchemaV1.Issue[]} issues
|
|
73
|
+
*/
|
|
74
|
+
constructor(issues) {
|
|
75
|
+
super('Validation failed');
|
|
76
|
+
this.name = 'ValidationError';
|
|
77
|
+
this.issues = issues;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
export { init_remote_functions } from './remote-functions.js';
|
package/src/exports/public.d.ts
CHANGED
|
@@ -1992,10 +1992,13 @@ type ExtractId<Input> = Input extends { id: infer Id }
|
|
|
1992
1992
|
: string | number;
|
|
1993
1993
|
|
|
1994
1994
|
/**
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
1995
|
+
* A function and proxy object used to imperatively create validation errors in form handlers.
|
|
1996
|
+
*
|
|
1997
|
+
* Access properties to create field-specific issues: `issue.fieldName('message')`.
|
|
1998
|
+
* The type structure mirrors the input data structure for type-safe field access.
|
|
1999
|
+
* Call `invalid(issue.foo(...), issue.nested.bar(...))` to throw a validation error.
|
|
1997
2000
|
*/
|
|
1998
|
-
type InvalidField<T> =
|
|
2001
|
+
export type InvalidField<T> =
|
|
1999
2002
|
WillRecurseIndefinitely<T> extends true
|
|
2000
2003
|
? Record<string | number, any>
|
|
2001
2004
|
: NonNullable<T> extends string | number | boolean | File
|
|
@@ -2011,15 +2014,12 @@ type InvalidField<T> =
|
|
|
2011
2014
|
: Record<string, never>;
|
|
2012
2015
|
|
|
2013
2016
|
/**
|
|
2014
|
-
* A
|
|
2015
|
-
*
|
|
2016
|
-
* Call `invalid(issue1, issue2, ...issueN)` to throw a validation error.
|
|
2017
|
-
* If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
|
|
2018
|
-
* Access properties to create field-specific issues: `invalid.fieldName('message')`.
|
|
2019
|
-
* The type structure mirrors the input data structure for type-safe field access.
|
|
2017
|
+
* A validation error thrown by `invalid`.
|
|
2020
2018
|
*/
|
|
2021
|
-
export
|
|
2022
|
-
|
|
2019
|
+
export interface ValidationError {
|
|
2020
|
+
/** The validation issues */
|
|
2021
|
+
issues: StandardSchemaV1.Issue[];
|
|
2022
|
+
}
|
|
2023
2023
|
|
|
2024
2024
|
/**
|
|
2025
2025
|
* The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
|
|
@@ -2067,8 +2067,6 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
|
|
|
2067
2067
|
includeUntouched?: boolean;
|
|
2068
2068
|
/** Set this to `true` to only run the `preflight` validation. */
|
|
2069
2069
|
preflightOnly?: boolean;
|
|
2070
|
-
/** Perform validation as if the form was submitted by the given button. */
|
|
2071
|
-
submitter?: HTMLButtonElement | HTMLInputElement;
|
|
2072
2070
|
}): Promise<void>;
|
|
2073
2071
|
/** The result of the form submission */
|
|
2074
2072
|
get result(): Output | undefined;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
/** @import { RemoteFormInput, RemoteForm } from '@sveltejs/kit' */
|
|
1
|
+
/** @import { RemoteFormInput, RemoteForm, InvalidField } from '@sveltejs/kit' */
|
|
2
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
6
|
import {
|
|
7
|
-
convert_formdata,
|
|
8
7
|
create_field_proxy,
|
|
9
8
|
set_nested_value,
|
|
10
9
|
throw_on_old_property_access,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
flatten_issues
|
|
14
13
|
} from '../../../form-utils.js';
|
|
15
14
|
import { get_cache, run_remote_function } from './shared.js';
|
|
15
|
+
import { ValidationError } from '@sveltejs/kit/internal';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Creates a form object that can be spread onto a `<form>` element.
|
|
@@ -21,7 +21,7 @@ import { get_cache, run_remote_function } from './shared.js';
|
|
|
21
21
|
*
|
|
22
22
|
* @template Output
|
|
23
23
|
* @overload
|
|
24
|
-
* @param {(
|
|
24
|
+
* @param {() => MaybePromise<Output>} fn
|
|
25
25
|
* @returns {RemoteForm<void, Output>}
|
|
26
26
|
* @since 2.27
|
|
27
27
|
*/
|
|
@@ -34,7 +34,7 @@ import { get_cache, run_remote_function } from './shared.js';
|
|
|
34
34
|
* @template Output
|
|
35
35
|
* @overload
|
|
36
36
|
* @param {'unchecked'} validate
|
|
37
|
-
* @param {(data: Input,
|
|
37
|
+
* @param {(data: Input, issue: InvalidField<Input>) => MaybePromise<Output>} fn
|
|
38
38
|
* @returns {RemoteForm<Input, Output>}
|
|
39
39
|
* @since 2.27
|
|
40
40
|
*/
|
|
@@ -47,7 +47,7 @@ import { get_cache, run_remote_function } from './shared.js';
|
|
|
47
47
|
* @template Output
|
|
48
48
|
* @overload
|
|
49
49
|
* @param {Schema} validate
|
|
50
|
-
* @param {(data: StandardSchemaV1.InferOutput<Schema>,
|
|
50
|
+
* @param {(data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>} fn
|
|
51
51
|
* @returns {RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>}
|
|
52
52
|
* @since 2.27
|
|
53
53
|
*/
|
|
@@ -55,7 +55,7 @@ import { get_cache, run_remote_function } from './shared.js';
|
|
|
55
55
|
* @template {RemoteFormInput} Input
|
|
56
56
|
* @template Output
|
|
57
57
|
* @param {any} validate_or_fn
|
|
58
|
-
* @param {(
|
|
58
|
+
* @param {(data_or_issue: any, issue?: any) => MaybePromise<Output>} [maybe_fn]
|
|
59
59
|
* @returns {RemoteForm<Input, Output>}
|
|
60
60
|
* @since 2.27
|
|
61
61
|
*/
|
|
@@ -104,19 +104,7 @@ export function form(validate_or_fn, maybe_fn) {
|
|
|
104
104
|
type: 'form',
|
|
105
105
|
name: '',
|
|
106
106
|
id: '',
|
|
107
|
-
|
|
108
|
-
fn: async (form_data) => {
|
|
109
|
-
const validate_only = form_data.get('sveltekit:validate_only') === 'true';
|
|
110
|
-
|
|
111
|
-
let data = maybe_fn ? convert_formdata(form_data) : undefined;
|
|
112
|
-
|
|
113
|
-
if (data && data.id === undefined) {
|
|
114
|
-
const id = form_data.get('sveltekit:id');
|
|
115
|
-
if (typeof id === 'string') {
|
|
116
|
-
data.id = JSON.parse(id);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
107
|
+
fn: async (data, meta, form_data) => {
|
|
120
108
|
// TODO 3.0 remove this warning
|
|
121
109
|
if (DEV && !data) {
|
|
122
110
|
const error = () => {
|
|
@@ -152,12 +140,12 @@ export function form(validate_or_fn, maybe_fn) {
|
|
|
152
140
|
const { event, state } = get_request_store();
|
|
153
141
|
const validated = await schema?.['~standard'].validate(data);
|
|
154
142
|
|
|
155
|
-
if (validate_only) {
|
|
143
|
+
if (meta.validate_only) {
|
|
156
144
|
return validated?.issues?.map((issue) => normalize_issue(issue, true)) ?? [];
|
|
157
145
|
}
|
|
158
146
|
|
|
159
147
|
if (validated?.issues !== undefined) {
|
|
160
|
-
handle_issues(output, validated.issues,
|
|
148
|
+
handle_issues(output, validated.issues, form_data);
|
|
161
149
|
} else {
|
|
162
150
|
if (validated !== undefined) {
|
|
163
151
|
data = validated.value;
|
|
@@ -165,7 +153,7 @@ export function form(validate_or_fn, maybe_fn) {
|
|
|
165
153
|
|
|
166
154
|
state.refreshes ??= {};
|
|
167
155
|
|
|
168
|
-
const
|
|
156
|
+
const issue = create_issues();
|
|
169
157
|
|
|
170
158
|
try {
|
|
171
159
|
output.result = await run_remote_function(
|
|
@@ -174,11 +162,11 @@ export function form(validate_or_fn, maybe_fn) {
|
|
|
174
162
|
true,
|
|
175
163
|
data,
|
|
176
164
|
(d) => d,
|
|
177
|
-
(data) => (!maybe_fn ? fn(
|
|
165
|
+
(data) => (!maybe_fn ? fn() : fn(data, issue))
|
|
178
166
|
);
|
|
179
167
|
} catch (e) {
|
|
180
168
|
if (e instanceof ValidationError) {
|
|
181
|
-
handle_issues(output, e.issues,
|
|
169
|
+
handle_issues(output, e.issues, form_data);
|
|
182
170
|
} else {
|
|
183
171
|
throw e;
|
|
184
172
|
}
|
|
@@ -297,15 +285,14 @@ export function form(validate_or_fn, maybe_fn) {
|
|
|
297
285
|
/**
|
|
298
286
|
* @param {{ issues?: InternalRemoteFormIssue[], input?: Record<string, any>, result: any }} output
|
|
299
287
|
* @param {readonly StandardSchemaV1.Issue[]} issues
|
|
300
|
-
* @param {
|
|
301
|
-
* @param {FormData} form_data
|
|
288
|
+
* @param {FormData | null} form_data - null if the form is progressively enhanced
|
|
302
289
|
*/
|
|
303
|
-
function handle_issues(output, issues,
|
|
290
|
+
function handle_issues(output, issues, form_data) {
|
|
304
291
|
output.issues = issues.map((issue) => normalize_issue(issue, true));
|
|
305
292
|
|
|
306
293
|
// if it was a progressively-enhanced submission, we don't need
|
|
307
294
|
// to return the input — it's already there
|
|
308
|
-
if (
|
|
295
|
+
if (form_data) {
|
|
309
296
|
output.input = {};
|
|
310
297
|
|
|
311
298
|
for (let key of form_data.keys()) {
|
|
@@ -328,89 +315,72 @@ function handle_issues(output, issues, is_remote_request, form_data) {
|
|
|
328
315
|
|
|
329
316
|
/**
|
|
330
317
|
* Creates an invalid function that can be used to imperatively mark form fields as invalid
|
|
331
|
-
* @returns {
|
|
318
|
+
* @returns {InvalidField<any>}
|
|
332
319
|
*/
|
|
333
|
-
function
|
|
334
|
-
/**
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
};
|
|
320
|
+
function create_issues() {
|
|
321
|
+
return /** @type {InvalidField<any>} */ (
|
|
322
|
+
new Proxy(
|
|
323
|
+
/** @param {string} message */
|
|
324
|
+
(message) => {
|
|
325
|
+
// TODO 3.0 remove
|
|
326
|
+
if (typeof message !== 'string') {
|
|
327
|
+
throw new Error(
|
|
328
|
+
'`invalid` should now be imported from `@sveltejs/kit` to throw validation issues. ' +
|
|
329
|
+
"The second parameter provided to the form function (renamed to `issue`) is still used to construct issues, e.g. `invalid(issue.field('message'))`. " +
|
|
330
|
+
'For more info see https://github.com/sveltejs/kit/pulls/14768'
|
|
331
|
+
);
|
|
346
332
|
}
|
|
347
333
|
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
334
|
+
return create_issue(message);
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
get(target, prop) {
|
|
338
|
+
if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
|
|
352
339
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
get(target, prop) {
|
|
356
|
-
if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* @param {string} message
|
|
360
|
-
* @param {(string | number)[]} path
|
|
361
|
-
* @returns {StandardSchemaV1.Issue}
|
|
362
|
-
*/
|
|
363
|
-
const create_issue = (message, path = []) => ({
|
|
364
|
-
message,
|
|
365
|
-
path
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
return create_issue_proxy(prop, create_issue, []);
|
|
340
|
+
return create_issue_proxy(prop, []);
|
|
341
|
+
}
|
|
369
342
|
}
|
|
370
|
-
|
|
343
|
+
)
|
|
371
344
|
);
|
|
372
|
-
}
|
|
373
345
|
|
|
374
|
-
/**
|
|
375
|
-
* Error thrown when form validation fails imperatively
|
|
376
|
-
*/
|
|
377
|
-
class ValidationError extends Error {
|
|
378
346
|
/**
|
|
379
|
-
* @param {
|
|
347
|
+
* @param {string} message
|
|
348
|
+
* @param {(string | number)[]} path
|
|
349
|
+
* @returns {StandardSchemaV1.Issue}
|
|
380
350
|
*/
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
351
|
+
function create_issue(message, path = []) {
|
|
352
|
+
return {
|
|
353
|
+
message,
|
|
354
|
+
path
|
|
355
|
+
};
|
|
385
356
|
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Creates a proxy that builds up a path and returns a function to create an issue
|
|
390
|
-
* @param {string | number} key
|
|
391
|
-
* @param {(message: string, path: (string | number)[]) => StandardSchemaV1.Issue} create_issue
|
|
392
|
-
* @param {(string | number)[]} path
|
|
393
|
-
*/
|
|
394
|
-
function create_issue_proxy(key, create_issue, path) {
|
|
395
|
-
const new_path = [...path, key];
|
|
396
357
|
|
|
397
358
|
/**
|
|
398
|
-
*
|
|
399
|
-
* @
|
|
359
|
+
* Creates a proxy that builds up a path and returns a function to create an issue
|
|
360
|
+
* @param {string | number} key
|
|
361
|
+
* @param {(string | number)[]} path
|
|
400
362
|
*/
|
|
401
|
-
|
|
363
|
+
function create_issue_proxy(key, path) {
|
|
364
|
+
const new_path = [...path, key];
|
|
402
365
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
366
|
+
/**
|
|
367
|
+
* @param {string} message
|
|
368
|
+
* @returns {StandardSchemaV1.Issue}
|
|
369
|
+
*/
|
|
370
|
+
const issue_func = (message) => create_issue(message, new_path);
|
|
406
371
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
372
|
+
return new Proxy(issue_func, {
|
|
373
|
+
get(target, prop) {
|
|
374
|
+
if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
|
|
411
375
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
376
|
+
// Handle array access like invalid.items[0]
|
|
377
|
+
if (/^\d+$/.test(prop)) {
|
|
378
|
+
return create_issue_proxy(parseInt(prop, 10), new_path);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Handle property access like invalid.field.nested
|
|
382
|
+
return create_issue_proxy(prop, new_path);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
416
386
|
}
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
set_nested_value,
|
|
19
19
|
throw_on_old_property_access,
|
|
20
20
|
build_path_string,
|
|
21
|
-
normalize_issue
|
|
21
|
+
normalize_issue,
|
|
22
|
+
serialize_binary_form,
|
|
23
|
+
BINARY_FORM_CONTENT_TYPE
|
|
22
24
|
} from '../../form-utils.js';
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -55,6 +57,7 @@ export function form(id) {
|
|
|
55
57
|
|
|
56
58
|
/** @param {string | number | boolean} [key] */
|
|
57
59
|
function create_instance(key) {
|
|
60
|
+
const action_id_without_key = id;
|
|
58
61
|
const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
|
|
59
62
|
const action = '?/remote=' + encodeURIComponent(action_id);
|
|
60
63
|
|
|
@@ -182,17 +185,18 @@ export function form(id) {
|
|
|
182
185
|
try {
|
|
183
186
|
await Promise.resolve();
|
|
184
187
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
+
const { blob } = serialize_binary_form(convert(data), {
|
|
189
|
+
remote_refreshes: updates.map((u) => u._key)
|
|
190
|
+
});
|
|
188
191
|
|
|
189
|
-
const response = await fetch(`${base}/${app_dir}/remote/${
|
|
192
|
+
const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
|
|
190
193
|
method: 'POST',
|
|
191
|
-
body: data,
|
|
192
194
|
headers: {
|
|
195
|
+
'Content-Type': BINARY_FORM_CONTENT_TYPE,
|
|
193
196
|
'x-sveltekit-pathname': location.pathname,
|
|
194
197
|
'x-sveltekit-search': location.search
|
|
195
|
-
}
|
|
198
|
+
},
|
|
199
|
+
body: blob
|
|
196
200
|
});
|
|
197
201
|
|
|
198
202
|
if (!response.ok) {
|
|
@@ -522,7 +526,7 @@ export function form(id) {
|
|
|
522
526
|
},
|
|
523
527
|
validate: {
|
|
524
528
|
/** @type {RemoteForm<any, any>['validate']} */
|
|
525
|
-
value: async ({ includeUntouched = false, preflightOnly = false
|
|
529
|
+
value: async ({ includeUntouched = false, preflightOnly = false } = {}) => {
|
|
526
530
|
if (!element) return;
|
|
527
531
|
|
|
528
532
|
const id = ++validate_id;
|
|
@@ -530,12 +534,18 @@ export function form(id) {
|
|
|
530
534
|
// wait a tick in case the user is calling validate() right after set() which takes time to propagate
|
|
531
535
|
await tick();
|
|
532
536
|
|
|
533
|
-
const
|
|
537
|
+
const default_submitter = /** @type {HTMLElement | undefined} */ (
|
|
538
|
+
element.querySelector('button:not([type]), [type="submit"]')
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const form_data = new FormData(element, default_submitter);
|
|
534
542
|
|
|
535
543
|
/** @type {InternalRemoteFormIssue[]} */
|
|
536
544
|
let array = [];
|
|
537
545
|
|
|
538
|
-
const
|
|
546
|
+
const data = convert(form_data);
|
|
547
|
+
|
|
548
|
+
const validated = await preflight_schema?.['~standard'].validate(data);
|
|
539
549
|
|
|
540
550
|
if (validate_id !== id) {
|
|
541
551
|
return;
|
|
@@ -544,11 +554,16 @@ export function form(id) {
|
|
|
544
554
|
if (validated?.issues) {
|
|
545
555
|
array = validated.issues.map((issue) => normalize_issue(issue, false));
|
|
546
556
|
} else if (!preflightOnly) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const response = await fetch(`${base}/${app_dir}/remote/${action_id}`, {
|
|
557
|
+
const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
|
|
550
558
|
method: 'POST',
|
|
551
|
-
|
|
559
|
+
headers: {
|
|
560
|
+
'Content-Type': BINARY_FORM_CONTENT_TYPE,
|
|
561
|
+
'x-sveltekit-pathname': location.pathname,
|
|
562
|
+
'x-sveltekit-search': location.search
|
|
563
|
+
},
|
|
564
|
+
body: serialize_binary_form(data, {
|
|
565
|
+
validate_only: true
|
|
566
|
+
}).blob
|
|
552
567
|
});
|
|
553
568
|
|
|
554
569
|
const result = await response.json();
|
|
@@ -640,12 +655,6 @@ function clone(element) {
|
|
|
640
655
|
*/
|
|
641
656
|
function validate_form_data(form_data, enctype) {
|
|
642
657
|
for (const key of form_data.keys()) {
|
|
643
|
-
if (key.startsWith('sveltekit:')) {
|
|
644
|
-
throw new Error(
|
|
645
|
-
'FormData keys starting with `sveltekit:` are reserved for internal use and should not be set manually'
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
658
|
if (/^\$[.[]?/.test(key)) {
|
|
650
659
|
throw new Error(
|
|
651
660
|
'`$` is used to collect all FormData validation issues and cannot be used as the `name` of a form control'
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** @import { RemoteForm } from '@sveltejs/kit' */
|
|
2
|
-
/** @import { InternalRemoteFormIssue } from 'types' */
|
|
2
|
+
/** @import { BinaryFormMeta, InternalRemoteFormIssue } from 'types' */
|
|
3
3
|
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
|
4
4
|
|
|
5
5
|
import { DEV } from 'esm-env';
|
|
6
|
+
import * as devalue from 'devalue';
|
|
7
|
+
import { text_decoder, text_encoder } from './utils.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Sets a value in a nested object using a path string, mutating the original object
|
|
@@ -31,10 +33,6 @@ export function convert_formdata(data) {
|
|
|
31
33
|
const result = {};
|
|
32
34
|
|
|
33
35
|
for (let key of data.keys()) {
|
|
34
|
-
if (key.startsWith('sveltekit:')) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
36
|
const is_array = key.endsWith('[]');
|
|
39
37
|
/** @type {any[]} */
|
|
40
38
|
let values = data.getAll(key);
|
|
@@ -64,6 +62,344 @@ export function convert_formdata(data) {
|
|
|
64
62
|
return result;
|
|
65
63
|
}
|
|
66
64
|
|
|
65
|
+
export const BINARY_FORM_CONTENT_TYPE = 'application/x-sveltekit-formdata';
|
|
66
|
+
const BINARY_FORM_VERSION = 0;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The binary format is as follows:
|
|
70
|
+
* - 1 byte: Format version
|
|
71
|
+
* - 4 bytes: Length of the header (u32)
|
|
72
|
+
* - 2 bytes: Length of the file offset table (u16)
|
|
73
|
+
* - header: devalue.stringify([data, meta])
|
|
74
|
+
* - file offset table: JSON.stringify([offset1, offset2, ...]) (empty if no files) (offsets start from the end of the table)
|
|
75
|
+
* - file1, file2, ...
|
|
76
|
+
* @param {Record<string, any>} data
|
|
77
|
+
* @param {BinaryFormMeta} meta
|
|
78
|
+
*/
|
|
79
|
+
export function serialize_binary_form(data, meta) {
|
|
80
|
+
/** @type {Array<BlobPart>} */
|
|
81
|
+
const blob_parts = [new Uint8Array([BINARY_FORM_VERSION])];
|
|
82
|
+
|
|
83
|
+
/** @type {Array<[file: File, index: number]>} */
|
|
84
|
+
const files = [];
|
|
85
|
+
|
|
86
|
+
if (!meta.remote_refreshes?.length) {
|
|
87
|
+
delete meta.remote_refreshes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const encoded_header = devalue.stringify([data, meta], {
|
|
91
|
+
File: (file) => {
|
|
92
|
+
if (!(file instanceof File)) return;
|
|
93
|
+
|
|
94
|
+
files.push([file, files.length]);
|
|
95
|
+
return [file.name, file.type, file.size, file.lastModified, files.length - 1];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const encoded_header_buffer = text_encoder.encode(encoded_header);
|
|
100
|
+
|
|
101
|
+
let encoded_file_offsets = '';
|
|
102
|
+
if (files.length) {
|
|
103
|
+
// Sort small files to the front
|
|
104
|
+
files.sort(([a], [b]) => a.size - b.size);
|
|
105
|
+
|
|
106
|
+
/** @type {Array<number>} */
|
|
107
|
+
const file_offsets = new Array(files.length);
|
|
108
|
+
let start = 0;
|
|
109
|
+
for (const [file, index] of files) {
|
|
110
|
+
file_offsets[index] = start;
|
|
111
|
+
start += file.size;
|
|
112
|
+
}
|
|
113
|
+
encoded_file_offsets = JSON.stringify(file_offsets);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const length_buffer = new Uint8Array(4);
|
|
117
|
+
const length_view = new DataView(length_buffer.buffer);
|
|
118
|
+
|
|
119
|
+
length_view.setUint32(0, encoded_header_buffer.byteLength, true);
|
|
120
|
+
blob_parts.push(length_buffer.slice());
|
|
121
|
+
|
|
122
|
+
length_view.setUint16(0, encoded_file_offsets.length, true);
|
|
123
|
+
blob_parts.push(length_buffer.slice(0, 2));
|
|
124
|
+
|
|
125
|
+
blob_parts.push(encoded_header_buffer);
|
|
126
|
+
blob_parts.push(encoded_file_offsets);
|
|
127
|
+
|
|
128
|
+
for (const [file] of files) {
|
|
129
|
+
blob_parts.push(file);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
blob: new Blob(blob_parts)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {Request} request
|
|
139
|
+
* @returns {Promise<{ data: Record<string, any>; meta: BinaryFormMeta; form_data: FormData | null }>}
|
|
140
|
+
*/
|
|
141
|
+
export async function deserialize_binary_form(request) {
|
|
142
|
+
if (request.headers.get('content-type') !== BINARY_FORM_CONTENT_TYPE) {
|
|
143
|
+
const form_data = await request.formData();
|
|
144
|
+
return { data: convert_formdata(form_data), meta: {}, form_data };
|
|
145
|
+
}
|
|
146
|
+
if (!request.body) {
|
|
147
|
+
throw new Error('Could not deserialize binary form: no body');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const reader = request.body.getReader();
|
|
151
|
+
|
|
152
|
+
/** @type {Array<Promise<Uint8Array<ArrayBuffer> | undefined>>} */
|
|
153
|
+
const chunks = [];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {number} index
|
|
157
|
+
* @returns {Promise<Uint8Array<ArrayBuffer> | undefined>}
|
|
158
|
+
*/
|
|
159
|
+
async function get_chunk(index) {
|
|
160
|
+
if (index in chunks) return chunks[index];
|
|
161
|
+
|
|
162
|
+
let i = chunks.length;
|
|
163
|
+
while (i <= index) {
|
|
164
|
+
chunks[i] = reader.read().then((chunk) => chunk.value);
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
return chunks[index];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {number} offset
|
|
172
|
+
* @param {number} length
|
|
173
|
+
* @returns {Promise<Uint8Array | null>}
|
|
174
|
+
*/
|
|
175
|
+
async function get_buffer(offset, length) {
|
|
176
|
+
/** @type {Uint8Array} */
|
|
177
|
+
let start_chunk;
|
|
178
|
+
let chunk_start = 0;
|
|
179
|
+
/** @type {number} */
|
|
180
|
+
let chunk_index;
|
|
181
|
+
for (chunk_index = 0; ; chunk_index++) {
|
|
182
|
+
const chunk = await get_chunk(chunk_index);
|
|
183
|
+
if (!chunk) return null;
|
|
184
|
+
|
|
185
|
+
const chunk_end = chunk_start + chunk.byteLength;
|
|
186
|
+
// If this chunk contains the target offset
|
|
187
|
+
if (offset >= chunk_start && offset < chunk_end) {
|
|
188
|
+
start_chunk = chunk;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
chunk_start = chunk_end;
|
|
192
|
+
}
|
|
193
|
+
// If the buffer is completely contained in one chunk, do a subarray
|
|
194
|
+
if (offset + length <= chunk_start + start_chunk.byteLength) {
|
|
195
|
+
return start_chunk.subarray(offset - chunk_start, offset + length - chunk_start);
|
|
196
|
+
}
|
|
197
|
+
// Otherwise, copy the data into a new buffer
|
|
198
|
+
const buffer = new Uint8Array(length);
|
|
199
|
+
buffer.set(start_chunk.subarray(offset - chunk_start));
|
|
200
|
+
let cursor = start_chunk.byteLength - offset + chunk_start;
|
|
201
|
+
while (cursor < length) {
|
|
202
|
+
chunk_index++;
|
|
203
|
+
let chunk = await get_chunk(chunk_index);
|
|
204
|
+
if (!chunk) return null;
|
|
205
|
+
if (chunk.byteLength > length - cursor) {
|
|
206
|
+
chunk = chunk.subarray(0, length - cursor);
|
|
207
|
+
}
|
|
208
|
+
buffer.set(chunk, cursor);
|
|
209
|
+
cursor += chunk.byteLength;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return buffer;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const header = await get_buffer(0, 1 + 4 + 2);
|
|
216
|
+
if (!header) throw new Error('Could not deserialize binary form: too short');
|
|
217
|
+
|
|
218
|
+
if (header[0] !== BINARY_FORM_VERSION) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Could not deserialize binary form: got version ${header[0]}, expected version ${BINARY_FORM_VERSION}`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const header_view = new DataView(header.buffer);
|
|
224
|
+
const data_length = header_view.getUint32(1, true);
|
|
225
|
+
const file_offsets_length = header_view.getUint16(5, true);
|
|
226
|
+
|
|
227
|
+
// Read the form data
|
|
228
|
+
const data_buffer = await get_buffer(1 + 4 + 2, data_length);
|
|
229
|
+
if (!data_buffer) throw new Error('Could not deserialize binary form: data too short');
|
|
230
|
+
|
|
231
|
+
/** @type {Array<number>} */
|
|
232
|
+
let file_offsets;
|
|
233
|
+
/** @type {number} */
|
|
234
|
+
let files_start_offset;
|
|
235
|
+
if (file_offsets_length > 0) {
|
|
236
|
+
// Read the file offset table
|
|
237
|
+
const file_offsets_buffer = await get_buffer(1 + 4 + 2 + data_length, file_offsets_length);
|
|
238
|
+
if (!file_offsets_buffer)
|
|
239
|
+
throw new Error('Could not deserialize binary form: file offset table too short');
|
|
240
|
+
|
|
241
|
+
file_offsets = /** @type {Array<number>} */ (
|
|
242
|
+
JSON.parse(text_decoder.decode(file_offsets_buffer))
|
|
243
|
+
);
|
|
244
|
+
files_start_offset = 1 + 4 + 2 + data_length + file_offsets_length;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const [data, meta] = devalue.parse(text_decoder.decode(data_buffer), {
|
|
248
|
+
File: ([name, type, size, last_modified, index]) => {
|
|
249
|
+
return new Proxy(
|
|
250
|
+
new LazyFile(
|
|
251
|
+
name,
|
|
252
|
+
type,
|
|
253
|
+
size,
|
|
254
|
+
last_modified,
|
|
255
|
+
get_chunk,
|
|
256
|
+
files_start_offset + file_offsets[index]
|
|
257
|
+
),
|
|
258
|
+
{
|
|
259
|
+
getPrototypeOf() {
|
|
260
|
+
// Trick validators into thinking this is a normal File
|
|
261
|
+
return File.prototype;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Read the request body asyncronously so it doesn't stall
|
|
269
|
+
void (async () => {
|
|
270
|
+
let has_more = true;
|
|
271
|
+
while (has_more) {
|
|
272
|
+
const chunk = await get_chunk(chunks.length);
|
|
273
|
+
has_more = !!chunk;
|
|
274
|
+
}
|
|
275
|
+
})();
|
|
276
|
+
|
|
277
|
+
return { data, meta, form_data: null };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @implements {File} */
|
|
281
|
+
class LazyFile {
|
|
282
|
+
/** @type {(index: number) => Promise<Uint8Array<ArrayBuffer> | undefined>} */
|
|
283
|
+
#get_chunk;
|
|
284
|
+
/** @type {number} */
|
|
285
|
+
#offset;
|
|
286
|
+
/**
|
|
287
|
+
* @param {string} name
|
|
288
|
+
* @param {string} type
|
|
289
|
+
* @param {number} size
|
|
290
|
+
* @param {number} last_modified
|
|
291
|
+
* @param {(index: number) => Promise<Uint8Array<ArrayBuffer> | undefined>} get_chunk
|
|
292
|
+
* @param {number} offset
|
|
293
|
+
*/
|
|
294
|
+
constructor(name, type, size, last_modified, get_chunk, offset) {
|
|
295
|
+
this.name = name;
|
|
296
|
+
this.type = type;
|
|
297
|
+
this.size = size;
|
|
298
|
+
this.lastModified = last_modified;
|
|
299
|
+
this.webkitRelativePath = '';
|
|
300
|
+
this.#get_chunk = get_chunk;
|
|
301
|
+
this.#offset = offset;
|
|
302
|
+
|
|
303
|
+
// TODO - hacky, required for private members to be accessed on proxy
|
|
304
|
+
this.arrayBuffer = this.arrayBuffer.bind(this);
|
|
305
|
+
this.bytes = this.bytes.bind(this);
|
|
306
|
+
this.slice = this.slice.bind(this);
|
|
307
|
+
this.stream = this.stream.bind(this);
|
|
308
|
+
this.text = this.text.bind(this);
|
|
309
|
+
}
|
|
310
|
+
/** @type {ArrayBuffer | undefined} */
|
|
311
|
+
#buffer;
|
|
312
|
+
async arrayBuffer() {
|
|
313
|
+
this.#buffer ??= await new Response(this.stream()).arrayBuffer();
|
|
314
|
+
return this.#buffer;
|
|
315
|
+
}
|
|
316
|
+
async bytes() {
|
|
317
|
+
return new Uint8Array(await this.arrayBuffer());
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* @param {number=} start
|
|
321
|
+
* @param {number=} end
|
|
322
|
+
* @param {string=} contentType
|
|
323
|
+
*/
|
|
324
|
+
slice(start = 0, end = this.size, contentType = this.type) {
|
|
325
|
+
// https://github.com/nodejs/node/blob/a5f3cd8cb5ba9e7911d93c5fd3ebc6d781220dd8/lib/internal/blob.js#L240
|
|
326
|
+
if (start < 0) {
|
|
327
|
+
start = Math.max(this.size + start, 0);
|
|
328
|
+
} else {
|
|
329
|
+
start = Math.min(start, this.size);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (end < 0) {
|
|
333
|
+
end = Math.max(this.size + end, 0);
|
|
334
|
+
} else {
|
|
335
|
+
end = Math.min(end, this.size);
|
|
336
|
+
}
|
|
337
|
+
const size = Math.max(end - start, 0);
|
|
338
|
+
const file = new LazyFile(
|
|
339
|
+
this.name,
|
|
340
|
+
contentType,
|
|
341
|
+
size,
|
|
342
|
+
this.lastModified,
|
|
343
|
+
this.#get_chunk,
|
|
344
|
+
this.#offset + start
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
return file;
|
|
348
|
+
}
|
|
349
|
+
stream() {
|
|
350
|
+
let cursor = 0;
|
|
351
|
+
let chunk_index = 0;
|
|
352
|
+
return new ReadableStream({
|
|
353
|
+
start: async (controller) => {
|
|
354
|
+
let chunk_start = 0;
|
|
355
|
+
let start_chunk = null;
|
|
356
|
+
for (chunk_index = 0; ; chunk_index++) {
|
|
357
|
+
const chunk = await this.#get_chunk(chunk_index);
|
|
358
|
+
if (!chunk) return null;
|
|
359
|
+
|
|
360
|
+
const chunk_end = chunk_start + chunk.byteLength;
|
|
361
|
+
// If this chunk contains the target offset
|
|
362
|
+
if (this.#offset >= chunk_start && this.#offset < chunk_end) {
|
|
363
|
+
start_chunk = chunk;
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
chunk_start = chunk_end;
|
|
367
|
+
}
|
|
368
|
+
// If the buffer is completely contained in one chunk, do a subarray
|
|
369
|
+
if (this.#offset + this.size <= chunk_start + start_chunk.byteLength) {
|
|
370
|
+
controller.enqueue(
|
|
371
|
+
start_chunk.subarray(this.#offset - chunk_start, this.#offset + this.size - chunk_start)
|
|
372
|
+
);
|
|
373
|
+
controller.close();
|
|
374
|
+
} else {
|
|
375
|
+
controller.enqueue(start_chunk.subarray(this.#offset - chunk_start));
|
|
376
|
+
cursor = start_chunk.byteLength - this.#offset + chunk_start;
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
pull: async (controller) => {
|
|
380
|
+
chunk_index++;
|
|
381
|
+
let chunk = await this.#get_chunk(chunk_index);
|
|
382
|
+
if (!chunk) {
|
|
383
|
+
controller.error('Could not deserialize binary form: incomplete file data');
|
|
384
|
+
controller.close();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (chunk.byteLength > this.size - cursor) {
|
|
388
|
+
chunk = chunk.subarray(0, this.size - cursor);
|
|
389
|
+
}
|
|
390
|
+
controller.enqueue(chunk);
|
|
391
|
+
cursor += chunk.byteLength;
|
|
392
|
+
if (cursor >= this.size) {
|
|
393
|
+
controller.close();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async text() {
|
|
399
|
+
return text_decoder.decode(await this.arrayBuffer());
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
67
403
|
const path_regex = /^[a-zA-Z_$]\w*(\.[a-zA-Z_$]\w*|\[\d+\])*$/;
|
|
68
404
|
|
|
69
405
|
/**
|
|
@@ -12,6 +12,7 @@ import { normalize_error } from '../../utils/error.js';
|
|
|
12
12
|
import { check_incorrect_fail_use } from './page/actions.js';
|
|
13
13
|
import { DEV } from 'esm-env';
|
|
14
14
|
import { record_span } from '../telemetry/record_span.js';
|
|
15
|
+
import { deserialize_binary_form } from '../form-utils.js';
|
|
15
16
|
|
|
16
17
|
/** @type {typeof handle_remote_call_internal} */
|
|
17
18
|
export async function handle_remote_call(event, state, options, manifest, id) {
|
|
@@ -116,25 +117,22 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
116
117
|
);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
const form_data = await event.request
|
|
120
|
-
form_client_refreshes = /** @type {string[]} */ (
|
|
121
|
-
JSON.parse(/** @type {string} */ (form_data.get('sveltekit:remote_refreshes')) ?? '[]')
|
|
122
|
-
);
|
|
123
|
-
form_data.delete('sveltekit:remote_refreshes');
|
|
120
|
+
const { data, meta, form_data } = await deserialize_binary_form(event.request);
|
|
124
121
|
|
|
125
122
|
// If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set)
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
// Note that additional_args will only be set if the form is not enhanced, as enhanced forms transfer the key inside `data`.
|
|
124
|
+
if (additional_args && !('id' in data)) {
|
|
125
|
+
data.id = JSON.parse(decodeURIComponent(additional_args));
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
const fn = info.fn;
|
|
131
|
-
const
|
|
129
|
+
const result = await with_request_store({ event, state }, () => fn(data, meta, form_data));
|
|
132
130
|
|
|
133
131
|
return json(
|
|
134
132
|
/** @type {RemoteFunctionResponse} */ ({
|
|
135
133
|
type: 'result',
|
|
136
|
-
result: stringify(
|
|
137
|
-
refreshes:
|
|
134
|
+
result: stringify(result, transport),
|
|
135
|
+
refreshes: result.issues ? undefined : await serialize_refreshes(meta.remote_refreshes)
|
|
138
136
|
})
|
|
139
137
|
);
|
|
140
138
|
}
|
|
@@ -178,7 +176,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
178
176
|
/** @type {RemoteFunctionResponse} */ ({
|
|
179
177
|
type: 'redirect',
|
|
180
178
|
location: error.location,
|
|
181
|
-
refreshes: await serialize_refreshes(form_client_refreshes
|
|
179
|
+
refreshes: await serialize_refreshes(form_client_refreshes)
|
|
182
180
|
})
|
|
183
181
|
);
|
|
184
182
|
}
|
|
@@ -204,24 +202,26 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
204
202
|
}
|
|
205
203
|
|
|
206
204
|
/**
|
|
207
|
-
* @param {string[]} client_refreshes
|
|
205
|
+
* @param {string[]=} client_refreshes
|
|
208
206
|
*/
|
|
209
207
|
async function serialize_refreshes(client_refreshes) {
|
|
210
208
|
const refreshes = state.refreshes ?? {};
|
|
211
209
|
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
if (client_refreshes) {
|
|
211
|
+
for (const key of client_refreshes) {
|
|
212
|
+
if (refreshes[key] !== undefined) continue;
|
|
214
213
|
|
|
215
|
-
|
|
214
|
+
const [hash, name, payload] = key.split('/');
|
|
216
215
|
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
const loader = manifest._.remotes[hash];
|
|
217
|
+
const fn = (await loader?.())?.default?.[name];
|
|
219
218
|
|
|
220
|
-
|
|
219
|
+
if (!fn) error(400, 'Bad Request');
|
|
221
220
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
refreshes[key] = with_request_store({ event, state }, () =>
|
|
222
|
+
fn(parse_remote_arg(payload, transport))
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
if (Object.keys(refreshes).length === 0) {
|
|
@@ -291,16 +291,14 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
try {
|
|
294
|
-
const form_data = await event.request.formData();
|
|
295
294
|
const fn = /** @type {RemoteInfo & { type: 'form' }} */ (/** @type {any} */ (form).__).fn;
|
|
296
295
|
|
|
297
|
-
|
|
298
|
-
if (action_id && !
|
|
299
|
-
|
|
300
|
-
form_data.set('sveltekit:id', decodeURIComponent(action_id));
|
|
296
|
+
const { data, meta, form_data } = await deserialize_binary_form(event.request);
|
|
297
|
+
if (action_id && !('id' in data)) {
|
|
298
|
+
data.id = JSON.parse(decodeURIComponent(action_id));
|
|
301
299
|
}
|
|
302
300
|
|
|
303
|
-
await with_request_store({ event, state }, () => fn(form_data));
|
|
301
|
+
await with_request_store({ event, state }, () => fn(data, meta, form_data));
|
|
304
302
|
|
|
305
303
|
// We don't want the data to appear on `let { form } = $props()`, which is why we're not returning it.
|
|
306
304
|
// It is instead available on `myForm.result`, setting of which happens within the remote `form` function.
|
package/src/types/internal.d.ts
CHANGED
|
@@ -552,6 +552,11 @@ export type ValidatedKitConfig = Omit<RecursiveRequired<KitConfig>, 'adapter'> &
|
|
|
552
552
|
adapter?: Adapter;
|
|
553
553
|
};
|
|
554
554
|
|
|
555
|
+
export type BinaryFormMeta = {
|
|
556
|
+
remote_refreshes?: string[];
|
|
557
|
+
validate_only?: boolean;
|
|
558
|
+
};
|
|
559
|
+
|
|
555
560
|
export type RemoteInfo =
|
|
556
561
|
| {
|
|
557
562
|
type: 'query' | 'command';
|
|
@@ -572,7 +577,11 @@ export type RemoteInfo =
|
|
|
572
577
|
type: 'form';
|
|
573
578
|
id: string;
|
|
574
579
|
name: string;
|
|
575
|
-
fn: (
|
|
580
|
+
fn: (
|
|
581
|
+
body: Record<string, any>,
|
|
582
|
+
meta: BinaryFormMeta,
|
|
583
|
+
form_data: FormData | null
|
|
584
|
+
) => Promise<any>;
|
|
576
585
|
}
|
|
577
586
|
| {
|
|
578
587
|
type: 'prerender';
|
package/src/utils/http.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BINARY_FORM_CONTENT_TYPE } from '../runtime/form-utils.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Given an Accept header and a list of possible content types, pick
|
|
3
5
|
* the most suitable one to respond with
|
|
@@ -74,6 +76,7 @@ export function is_form_content_type(request) {
|
|
|
74
76
|
request,
|
|
75
77
|
'application/x-www-form-urlencoded',
|
|
76
78
|
'multipart/form-data',
|
|
77
|
-
'text/plain'
|
|
79
|
+
'text/plain',
|
|
80
|
+
BINARY_FORM_CONTENT_TYPE
|
|
78
81
|
);
|
|
79
82
|
}
|
package/src/version.js
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -1968,10 +1968,13 @@ declare module '@sveltejs/kit' {
|
|
|
1968
1968
|
: string | number;
|
|
1969
1969
|
|
|
1970
1970
|
/**
|
|
1971
|
-
*
|
|
1972
|
-
*
|
|
1971
|
+
* A function and proxy object used to imperatively create validation errors in form handlers.
|
|
1972
|
+
*
|
|
1973
|
+
* Access properties to create field-specific issues: `issue.fieldName('message')`.
|
|
1974
|
+
* The type structure mirrors the input data structure for type-safe field access.
|
|
1975
|
+
* Call `invalid(issue.foo(...), issue.nested.bar(...))` to throw a validation error.
|
|
1973
1976
|
*/
|
|
1974
|
-
type InvalidField<T> =
|
|
1977
|
+
export type InvalidField<T> =
|
|
1975
1978
|
WillRecurseIndefinitely<T> extends true
|
|
1976
1979
|
? Record<string | number, any>
|
|
1977
1980
|
: NonNullable<T> extends string | number | boolean | File
|
|
@@ -1987,15 +1990,12 @@ declare module '@sveltejs/kit' {
|
|
|
1987
1990
|
: Record<string, never>;
|
|
1988
1991
|
|
|
1989
1992
|
/**
|
|
1990
|
-
* A
|
|
1991
|
-
*
|
|
1992
|
-
* Call `invalid(issue1, issue2, ...issueN)` to throw a validation error.
|
|
1993
|
-
* If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
|
|
1994
|
-
* Access properties to create field-specific issues: `invalid.fieldName('message')`.
|
|
1995
|
-
* The type structure mirrors the input data structure for type-safe field access.
|
|
1993
|
+
* A validation error thrown by `invalid`.
|
|
1996
1994
|
*/
|
|
1997
|
-
export
|
|
1998
|
-
|
|
1995
|
+
export interface ValidationError {
|
|
1996
|
+
/** The validation issues */
|
|
1997
|
+
issues: StandardSchemaV1.Issue[];
|
|
1998
|
+
}
|
|
1999
1999
|
|
|
2000
2000
|
/**
|
|
2001
2001
|
* The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
|
|
@@ -2043,8 +2043,6 @@ declare module '@sveltejs/kit' {
|
|
|
2043
2043
|
includeUntouched?: boolean;
|
|
2044
2044
|
/** Set this to `true` to only run the `preflight` validation. */
|
|
2045
2045
|
preflightOnly?: boolean;
|
|
2046
|
-
/** Perform validation as if the form was submitted by the given button. */
|
|
2047
|
-
submitter?: HTMLButtonElement | HTMLInputElement;
|
|
2048
2046
|
}): Promise<void>;
|
|
2049
2047
|
/** The result of the form submission */
|
|
2050
2048
|
get result(): Output | undefined;
|
|
@@ -2702,6 +2700,38 @@ declare module '@sveltejs/kit' {
|
|
|
2702
2700
|
* @param e The object to check.
|
|
2703
2701
|
* */
|
|
2704
2702
|
export function isActionFailure(e: unknown): e is ActionFailure;
|
|
2703
|
+
/**
|
|
2704
|
+
* Use this to throw a validation error to imperatively fail form validation.
|
|
2705
|
+
* Can be used in combination with `issue` passed to form actions to create field-specific issues.
|
|
2706
|
+
*
|
|
2707
|
+
* @example
|
|
2708
|
+
* ```ts
|
|
2709
|
+
* import { invalid } from '@sveltejs/kit';
|
|
2710
|
+
* import { form } from '$app/server';
|
|
2711
|
+
* import { tryLogin } from '$lib/server/auth';
|
|
2712
|
+
* import * as v from 'valibot';
|
|
2713
|
+
*
|
|
2714
|
+
* export const login = form(
|
|
2715
|
+
* v.object({ name: v.string(), _password: v.string() }),
|
|
2716
|
+
* async ({ name, _password }) => {
|
|
2717
|
+
* const success = tryLogin(name, _password);
|
|
2718
|
+
* if (!success) {
|
|
2719
|
+
* invalid('Incorrect username or password');
|
|
2720
|
+
* }
|
|
2721
|
+
*
|
|
2722
|
+
* // ...
|
|
2723
|
+
* }
|
|
2724
|
+
* );
|
|
2725
|
+
* ```
|
|
2726
|
+
* @since 2.47.3
|
|
2727
|
+
*/
|
|
2728
|
+
export function invalid(...issues: (StandardSchemaV1.Issue | string)[]): never;
|
|
2729
|
+
/**
|
|
2730
|
+
* Checks whether this is an validation error thrown by {@link invalid}.
|
|
2731
|
+
* @param e The object to check.
|
|
2732
|
+
* @since 2.47.3
|
|
2733
|
+
*/
|
|
2734
|
+
export function isValidationError(e: unknown): e is ActionFailure;
|
|
2705
2735
|
/**
|
|
2706
2736
|
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
|
|
2707
2737
|
* Returns the normalized URL as well as a method for adding the potential suffix back
|
|
@@ -3134,7 +3164,7 @@ declare module '$app/paths' {
|
|
|
3134
3164
|
}
|
|
3135
3165
|
|
|
3136
3166
|
declare module '$app/server' {
|
|
3137
|
-
import type { RequestEvent, RemoteCommand, RemoteForm, RemoteFormInput, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit';
|
|
3167
|
+
import type { RequestEvent, RemoteCommand, RemoteForm, RemoteFormInput, InvalidField, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit';
|
|
3138
3168
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3139
3169
|
/**
|
|
3140
3170
|
* Read the contents of an imported asset from the filesystem
|
|
@@ -3188,7 +3218,7 @@ declare module '$app/server' {
|
|
|
3188
3218
|
*
|
|
3189
3219
|
* @since 2.27
|
|
3190
3220
|
*/
|
|
3191
|
-
export function form<Output>(fn: (
|
|
3221
|
+
export function form<Output>(fn: () => MaybePromise<Output>): RemoteForm<void, Output>;
|
|
3192
3222
|
/**
|
|
3193
3223
|
* Creates a form object that can be spread onto a `<form>` element.
|
|
3194
3224
|
*
|
|
@@ -3196,7 +3226,7 @@ declare module '$app/server' {
|
|
|
3196
3226
|
*
|
|
3197
3227
|
* @since 2.27
|
|
3198
3228
|
*/
|
|
3199
|
-
export function form<Input extends RemoteFormInput, Output>(validate: "unchecked", fn: (data: Input,
|
|
3229
|
+
export function form<Input extends RemoteFormInput, Output>(validate: "unchecked", fn: (data: Input, issue: InvalidField<Input>) => MaybePromise<Output>): RemoteForm<Input, Output>;
|
|
3200
3230
|
/**
|
|
3201
3231
|
* Creates a form object that can be spread onto a `<form>` element.
|
|
3202
3232
|
*
|
|
@@ -3204,7 +3234,7 @@ declare module '$app/server' {
|
|
|
3204
3234
|
*
|
|
3205
3235
|
* @since 2.27
|
|
3206
3236
|
*/
|
|
3207
|
-
export function form<Schema extends StandardSchemaV1<RemoteFormInput, Record<string, any>>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput<Schema>,
|
|
3237
|
+
export function form<Schema extends StandardSchemaV1<RemoteFormInput, Record<string, any>>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>): RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>;
|
|
3208
3238
|
/**
|
|
3209
3239
|
* Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call.
|
|
3210
3240
|
*
|
package/types/index.d.ts.map
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"RemoteFormIssue",
|
|
76
76
|
"ExtractId",
|
|
77
77
|
"InvalidField",
|
|
78
|
-
"
|
|
78
|
+
"ValidationError",
|
|
79
79
|
"RemoteForm",
|
|
80
80
|
"RemoteCommand",
|
|
81
81
|
"RemoteResource",
|
|
@@ -128,6 +128,8 @@
|
|
|
128
128
|
"json",
|
|
129
129
|
"text",
|
|
130
130
|
"isActionFailure",
|
|
131
|
+
"invalid",
|
|
132
|
+
"isValidationError",
|
|
131
133
|
"normalizeUrl",
|
|
132
134
|
"VERSION",
|
|
133
135
|
"sequence",
|
|
@@ -213,6 +215,6 @@
|
|
|
213
215
|
null,
|
|
214
216
|
null
|
|
215
217
|
],
|
|
216
|
-
"mappings": "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
|
|
218
|
+
"mappings": "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",
|
|
217
219
|
"ignoreList": []
|
|
218
220
|
}
|