@sveltejs/kit 2.40.0 → 2.42.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/core/sync/write_server.js +2 -0
- package/src/exports/public.d.ts +103 -10
- package/src/runtime/app/server/remote/form.js +146 -15
- package/src/runtime/client/client.js +12 -7
- package/src/runtime/client/remote-functions/form.svelte.js +321 -79
- package/src/runtime/server/page/render.js +28 -26
- package/src/runtime/server/remote.js +4 -3
- package/src/runtime/utils.js +122 -0
- package/src/version.js +1 -1
- package/types/index.d.ts +121 -12
- package/types/index.d.ts.map +8 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** @import {
|
|
1
|
+
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
|
2
|
+
/** @import { RemoteFormInput, RemoteForm, RemoteQueryOverride } from '@sveltejs/kit' */
|
|
2
3
|
/** @import { RemoteFunctionResponse } from 'types' */
|
|
3
4
|
/** @import { Query } from './query.svelte.js' */
|
|
4
5
|
import { app_dir, base } from '__sveltekit/paths';
|
|
@@ -15,15 +16,18 @@ import {
|
|
|
15
16
|
} from '../client.js';
|
|
16
17
|
import { tick } from 'svelte';
|
|
17
18
|
import { refresh_queries, release_overrides } from './shared.svelte.js';
|
|
19
|
+
import { createAttachmentKey } from 'svelte/attachments';
|
|
20
|
+
import { convert_formdata, file_transport, flatten_issues } from '../../utils.js';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Client-version of the `form` function from `$app/server`.
|
|
21
|
-
* @template T
|
|
24
|
+
* @template {RemoteFormInput} T
|
|
25
|
+
* @template U
|
|
22
26
|
* @param {string} id
|
|
23
|
-
* @returns {RemoteForm<T>}
|
|
27
|
+
* @returns {RemoteForm<T, U>}
|
|
24
28
|
*/
|
|
25
29
|
export function form(id) {
|
|
26
|
-
/** @type {Map<any, { count: number, instance: RemoteForm<T> }>} */
|
|
30
|
+
/** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
|
|
27
31
|
const instances = new Map();
|
|
28
32
|
|
|
29
33
|
/** @param {string | number | boolean} [key] */
|
|
@@ -31,12 +35,80 @@ export function form(id) {
|
|
|
31
35
|
const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
|
|
32
36
|
const action = '?/remote=' + encodeURIComponent(action_id);
|
|
33
37
|
|
|
38
|
+
/** @type {Record<string, string | string[] | File | File[]>} */
|
|
39
|
+
let input = $state({});
|
|
40
|
+
|
|
41
|
+
/** @type {Record<string, StandardSchemaV1.Issue[]>} */
|
|
42
|
+
let issues = $state.raw({});
|
|
43
|
+
|
|
34
44
|
/** @type {any} */
|
|
35
45
|
let result = $state.raw(started ? undefined : remote_responses[action_id]);
|
|
36
46
|
|
|
37
47
|
/** @type {number} */
|
|
38
48
|
let pending_count = $state(0);
|
|
39
49
|
|
|
50
|
+
/** @type {StandardSchemaV1 | undefined} */
|
|
51
|
+
let preflight_schema = undefined;
|
|
52
|
+
|
|
53
|
+
/** @type {HTMLFormElement | null} */
|
|
54
|
+
let element = null;
|
|
55
|
+
|
|
56
|
+
/** @type {Record<string, boolean>} */
|
|
57
|
+
let touched = {};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {HTMLFormElement} form
|
|
61
|
+
* @param {FormData} form_data
|
|
62
|
+
* @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback
|
|
63
|
+
*/
|
|
64
|
+
async function handle_submit(form, form_data, callback) {
|
|
65
|
+
const data = convert_formdata(form_data);
|
|
66
|
+
|
|
67
|
+
const validated = await preflight_schema?.['~standard'].validate(data);
|
|
68
|
+
|
|
69
|
+
if (validated?.issues) {
|
|
70
|
+
issues = flatten_issues(validated.issues);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// TODO 3.0 remove this warning
|
|
75
|
+
if (DEV) {
|
|
76
|
+
const error = () => {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'Remote form functions no longer get passed a FormData object. The payload is now a POJO. See https://kit.svelte.dev/docs/remote-functions#form for details.'
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
for (const key of [
|
|
82
|
+
'append',
|
|
83
|
+
'delete',
|
|
84
|
+
'entries',
|
|
85
|
+
'forEach',
|
|
86
|
+
'get',
|
|
87
|
+
'getAll',
|
|
88
|
+
'has',
|
|
89
|
+
'keys',
|
|
90
|
+
'set',
|
|
91
|
+
'values'
|
|
92
|
+
]) {
|
|
93
|
+
if (!(key in data)) {
|
|
94
|
+
Object.defineProperty(data, key, { get: error });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await callback({
|
|
101
|
+
form,
|
|
102
|
+
data,
|
|
103
|
+
submit: () => submit(form_data)
|
|
104
|
+
});
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const error = e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
|
|
107
|
+
const status = e instanceof HttpError ? e.status : 500;
|
|
108
|
+
void set_nearest_error_page(error, status);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
40
112
|
/**
|
|
41
113
|
* @param {FormData} data
|
|
42
114
|
* @returns {Promise<any> & { updates: (...args: any[]) => any }}
|
|
@@ -64,13 +136,6 @@ export function form(id) {
|
|
|
64
136
|
await Promise.resolve();
|
|
65
137
|
|
|
66
138
|
if (updates.length > 0) {
|
|
67
|
-
if (DEV) {
|
|
68
|
-
if (data.get('sveltekit:remote_refreshes')) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
'The FormData key `sveltekit:remote_refreshes` is reserved for internal use and should not be set manually'
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
139
|
data.set('sveltekit:remote_refreshes', JSON.stringify(updates.map((u) => u._key)));
|
|
75
140
|
}
|
|
76
141
|
|
|
@@ -88,9 +153,18 @@ export function form(id) {
|
|
|
88
153
|
const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
|
|
89
154
|
|
|
90
155
|
if (form_result.type === 'result') {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
156
|
+
({
|
|
157
|
+
input = {},
|
|
158
|
+
issues = {},
|
|
159
|
+
result
|
|
160
|
+
} = devalue.parse(form_result.result, {
|
|
161
|
+
...app.decoders,
|
|
162
|
+
File: file_transport.decode
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
if (issues.$) {
|
|
166
|
+
release_overrides(updates);
|
|
167
|
+
} else if (form_result.refreshes) {
|
|
94
168
|
refresh_queries(form_result.refreshes, updates);
|
|
95
169
|
} else {
|
|
96
170
|
void invalidateAll();
|
|
@@ -104,7 +178,6 @@ export function form(id) {
|
|
|
104
178
|
// Use internal version to allow redirects to external URLs
|
|
105
179
|
void _goto(form_result.location, { invalidateAll }, 0);
|
|
106
180
|
} else {
|
|
107
|
-
result = undefined;
|
|
108
181
|
throw new HttpError(form_result.status ?? 500, form_result.error);
|
|
109
182
|
}
|
|
110
183
|
} catch (e) {
|
|
@@ -134,43 +207,13 @@ export function form(id) {
|
|
|
134
207
|
return promise;
|
|
135
208
|
}
|
|
136
209
|
|
|
137
|
-
/** @type {RemoteForm<T>} */
|
|
210
|
+
/** @type {RemoteForm<T, U>} */
|
|
138
211
|
const instance = {};
|
|
139
212
|
|
|
140
213
|
instance.method = 'POST';
|
|
141
214
|
instance.action = action;
|
|
142
215
|
|
|
143
|
-
/**
|
|
144
|
-
* @param {HTMLFormElement} form_element
|
|
145
|
-
* @param {HTMLElement | null} submitter
|
|
146
|
-
*/
|
|
147
|
-
function create_form_data(form_element, submitter) {
|
|
148
|
-
const form_data = new FormData(form_element);
|
|
149
|
-
|
|
150
|
-
if (DEV) {
|
|
151
|
-
const enctype = submitter?.hasAttribute('formenctype')
|
|
152
|
-
? /** @type {HTMLButtonElement | HTMLInputElement} */ (submitter).formEnctype
|
|
153
|
-
: clone(form_element).enctype;
|
|
154
|
-
if (enctype !== 'multipart/form-data') {
|
|
155
|
-
for (const value of form_data.values()) {
|
|
156
|
-
if (value instanceof File) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
'Your form contains <input type="file"> fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.'
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const submitter_name = submitter?.getAttribute('name');
|
|
166
|
-
if (submitter_name) {
|
|
167
|
-
form_data.append(submitter_name, submitter?.getAttribute('value') ?? '');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return form_data;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** @param {Parameters<RemoteForm<any>['enhance']>[0]} callback */
|
|
216
|
+
/** @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback */
|
|
174
217
|
const form_onsubmit = (callback) => {
|
|
175
218
|
/** @param {SubmitEvent} event */
|
|
176
219
|
return async (event) => {
|
|
@@ -194,26 +237,105 @@ export function form(id) {
|
|
|
194
237
|
|
|
195
238
|
event.preventDefault();
|
|
196
239
|
|
|
197
|
-
const
|
|
240
|
+
const form_data = new FormData(form);
|
|
198
241
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
form,
|
|
202
|
-
data,
|
|
203
|
-
submit: () => submit(data)
|
|
204
|
-
});
|
|
205
|
-
} catch (e) {
|
|
206
|
-
const error =
|
|
207
|
-
e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
|
|
208
|
-
const status = e instanceof HttpError ? e.status : 500;
|
|
209
|
-
void set_nearest_error_page(error, status);
|
|
242
|
+
if (DEV) {
|
|
243
|
+
validate_form_data(form_data, clone(form).enctype);
|
|
210
244
|
}
|
|
245
|
+
|
|
246
|
+
await handle_submit(form, form_data, callback);
|
|
211
247
|
};
|
|
212
248
|
};
|
|
213
249
|
|
|
214
|
-
|
|
250
|
+
/** @param {(event: SubmitEvent) => void} onsubmit */
|
|
251
|
+
function create_attachment(onsubmit) {
|
|
252
|
+
return (/** @type {HTMLFormElement} */ form) => {
|
|
253
|
+
if (element) {
|
|
254
|
+
let message = `A form object can only be attached to a single \`<form>\` element`;
|
|
255
|
+
if (DEV && !key) {
|
|
256
|
+
const name = id.split('/').pop();
|
|
257
|
+
message += `. To create multiple instances, use \`${name}.for(key)\``;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw new Error(message);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
element = form;
|
|
264
|
+
|
|
265
|
+
touched = {};
|
|
266
|
+
|
|
267
|
+
form.addEventListener('submit', onsubmit);
|
|
268
|
+
|
|
269
|
+
form.addEventListener('input', (e) => {
|
|
270
|
+
// strictly speaking it can be an HTMLTextAreaElement or HTMLSelectElement
|
|
271
|
+
// but that makes the types unnecessarily awkward
|
|
272
|
+
const element = /** @type {HTMLInputElement} */ (e.target);
|
|
273
|
+
|
|
274
|
+
let name = element.name;
|
|
275
|
+
if (!name) return;
|
|
276
|
+
|
|
277
|
+
const is_array = name.endsWith('[]');
|
|
278
|
+
if (is_array) name = name.slice(0, -2);
|
|
279
|
+
|
|
280
|
+
const is_file = element.type === 'file';
|
|
281
|
+
|
|
282
|
+
touched[name] = true;
|
|
283
|
+
|
|
284
|
+
if (is_array) {
|
|
285
|
+
const elements = /** @type {HTMLInputElement[]} */ (
|
|
286
|
+
Array.from(form.querySelectorAll(`[name="${name}[]"]`))
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (DEV) {
|
|
290
|
+
for (const e of elements) {
|
|
291
|
+
if ((e.type === 'file') !== is_file) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Cannot mix and match file and non-file inputs under the same name ("${element.name}")`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
input[name] = is_file
|
|
300
|
+
? elements.map((input) => Array.from(input.files ?? [])).flat()
|
|
301
|
+
: elements.map((element) => element.value);
|
|
302
|
+
} else if (is_file) {
|
|
303
|
+
if (DEV && element.multiple) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Can only use the \`multiple\` attribute when \`name\` includes a \`[]\` suffix — consider changing "${name}" to "${name}[]"`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
|
|
215
310
|
|
|
216
|
-
|
|
311
|
+
if (file) {
|
|
312
|
+
input[name] = file;
|
|
313
|
+
} else {
|
|
314
|
+
delete input[name];
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
input[name] = element.value;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return () => {
|
|
322
|
+
element = null;
|
|
323
|
+
preflight_schema = undefined;
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
instance[createAttachmentKey()] = create_attachment(
|
|
329
|
+
form_onsubmit(({ submit, form }) =>
|
|
330
|
+
submit().then(() => {
|
|
331
|
+
if (!issues.$) {
|
|
332
|
+
form.reset();
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
/** @param {Parameters<RemoteForm<any, any>['buttonProps']['enhance']>[0]} callback */
|
|
217
339
|
const form_action_onclick = (callback) => {
|
|
218
340
|
/** @param {Event} event */
|
|
219
341
|
return async (event) => {
|
|
@@ -225,34 +347,41 @@ export function form(id) {
|
|
|
225
347
|
event.stopPropagation();
|
|
226
348
|
event.preventDefault();
|
|
227
349
|
|
|
228
|
-
const
|
|
350
|
+
const form_data = new FormData(form);
|
|
229
351
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
} catch (e) {
|
|
237
|
-
const error =
|
|
238
|
-
e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
|
|
239
|
-
const status = e instanceof HttpError ? e.status : 500;
|
|
240
|
-
void set_nearest_error_page(error, status);
|
|
352
|
+
if (DEV) {
|
|
353
|
+
const enctype = target.hasAttribute('formenctype')
|
|
354
|
+
? target.formEnctype
|
|
355
|
+
: clone(form).enctype;
|
|
356
|
+
|
|
357
|
+
validate_form_data(form_data, enctype);
|
|
241
358
|
}
|
|
359
|
+
|
|
360
|
+
if (target.name) {
|
|
361
|
+
form_data.append(target.name, target?.getAttribute('value') ?? '');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await handle_submit(form, form_data, callback);
|
|
242
365
|
};
|
|
243
366
|
};
|
|
244
367
|
|
|
245
|
-
/** @type {RemoteForm<any>['buttonProps']} */
|
|
368
|
+
/** @type {RemoteForm<any, any>['buttonProps']} */
|
|
246
369
|
// @ts-expect-error we gotta set enhance as a non-enumerable property
|
|
247
370
|
const button_props = {
|
|
248
371
|
type: 'submit',
|
|
249
372
|
formmethod: 'POST',
|
|
250
373
|
formaction: action,
|
|
251
|
-
onclick: form_action_onclick(({ submit, form }) =>
|
|
374
|
+
onclick: form_action_onclick(({ submit, form }) =>
|
|
375
|
+
submit().then(() => {
|
|
376
|
+
if (!issues.$) {
|
|
377
|
+
form.reset();
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
)
|
|
252
381
|
};
|
|
253
382
|
|
|
254
383
|
Object.defineProperty(button_props, 'enhance', {
|
|
255
|
-
/** @type {RemoteForm<any>['buttonProps']['enhance']} */
|
|
384
|
+
/** @type {RemoteForm<any, any>['buttonProps']['enhance']} */
|
|
256
385
|
value: (callback) => {
|
|
257
386
|
return {
|
|
258
387
|
type: 'submit',
|
|
@@ -267,23 +396,106 @@ export function form(id) {
|
|
|
267
396
|
get: () => pending_count
|
|
268
397
|
});
|
|
269
398
|
|
|
399
|
+
let validate_id = 0;
|
|
400
|
+
|
|
270
401
|
Object.defineProperties(instance, {
|
|
271
402
|
buttonProps: {
|
|
272
403
|
value: button_props
|
|
273
404
|
},
|
|
405
|
+
input: {
|
|
406
|
+
get: () => input,
|
|
407
|
+
set: (v) => {
|
|
408
|
+
input = v;
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
issues: {
|
|
412
|
+
get: () => issues
|
|
413
|
+
},
|
|
274
414
|
result: {
|
|
275
415
|
get: () => result
|
|
276
416
|
},
|
|
277
417
|
pending: {
|
|
278
418
|
get: () => pending_count
|
|
279
419
|
},
|
|
420
|
+
field: {
|
|
421
|
+
value: (/** @type {string} */ name) => name
|
|
422
|
+
},
|
|
423
|
+
preflight: {
|
|
424
|
+
/** @type {RemoteForm<any, any>['preflight']} */
|
|
425
|
+
value: (schema) => {
|
|
426
|
+
preflight_schema = schema;
|
|
427
|
+
return instance;
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
validate: {
|
|
431
|
+
/** @type {RemoteForm<any, any>['validate']} */
|
|
432
|
+
value: async ({ includeUntouched = false } = {}) => {
|
|
433
|
+
if (!element) return;
|
|
434
|
+
|
|
435
|
+
const id = ++validate_id;
|
|
436
|
+
|
|
437
|
+
const form_data = new FormData(element);
|
|
438
|
+
|
|
439
|
+
/** @type {readonly StandardSchemaV1.Issue[]} */
|
|
440
|
+
let array = [];
|
|
441
|
+
|
|
442
|
+
const validated = await preflight_schema?.['~standard'].validate(
|
|
443
|
+
convert_formdata(form_data)
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
if (validated?.issues) {
|
|
447
|
+
array = validated.issues;
|
|
448
|
+
} else {
|
|
449
|
+
form_data.set('sveltekit:validate_only', 'true');
|
|
450
|
+
|
|
451
|
+
const response = await fetch(`${base}/${app_dir}/remote/${action_id}`, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
body: form_data
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const result = await response.json();
|
|
457
|
+
|
|
458
|
+
if (validate_id !== id) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (result.type === 'result') {
|
|
463
|
+
array = /** @type {StandardSchemaV1.Issue[]} */ (
|
|
464
|
+
devalue.parse(result.result, app.decoders)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!includeUntouched) {
|
|
470
|
+
array = array.filter((issue) => {
|
|
471
|
+
if (issue.path !== undefined) {
|
|
472
|
+
let path = '';
|
|
473
|
+
|
|
474
|
+
for (const segment of issue.path) {
|
|
475
|
+
const key = typeof segment === 'object' ? segment.key : segment;
|
|
476
|
+
|
|
477
|
+
if (typeof key === 'number') {
|
|
478
|
+
path += `[${key}]`;
|
|
479
|
+
} else if (typeof key === 'string') {
|
|
480
|
+
path += path === '' ? key : '.' + key;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return touched[path];
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
issues = flatten_issues(array);
|
|
490
|
+
}
|
|
491
|
+
},
|
|
280
492
|
enhance: {
|
|
281
|
-
/** @type {RemoteForm<any>['enhance']} */
|
|
493
|
+
/** @type {RemoteForm<any, any>['enhance']} */
|
|
282
494
|
value: (callback) => {
|
|
283
495
|
return {
|
|
284
496
|
method: 'POST',
|
|
285
497
|
action,
|
|
286
|
-
|
|
498
|
+
[createAttachmentKey()]: create_attachment(form_onsubmit(callback))
|
|
287
499
|
};
|
|
288
500
|
}
|
|
289
501
|
}
|
|
@@ -295,7 +507,7 @@ export function form(id) {
|
|
|
295
507
|
const instance = create_instance();
|
|
296
508
|
|
|
297
509
|
Object.defineProperty(instance, 'for', {
|
|
298
|
-
/** @type {RemoteForm<
|
|
510
|
+
/** @type {RemoteForm<T, U>['for']} */
|
|
299
511
|
value: (key) => {
|
|
300
512
|
const entry = instances.get(key) ?? { count: 0, instance: create_instance(key) };
|
|
301
513
|
|
|
@@ -335,3 +547,33 @@ export function form(id) {
|
|
|
335
547
|
function clone(element) {
|
|
336
548
|
return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element));
|
|
337
549
|
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @param {FormData} form_data
|
|
553
|
+
* @param {string} enctype
|
|
554
|
+
*/
|
|
555
|
+
function validate_form_data(form_data, enctype) {
|
|
556
|
+
for (const key of form_data.keys()) {
|
|
557
|
+
if (key.startsWith('sveltekit:')) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
'FormData keys starting with `sveltekit:` are reserved for internal use and should not be set manually'
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (/^\$[.[]?/.test(key)) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
'`$` is used to collect all FormData validation issues and cannot be used as the `name` of a form control'
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (enctype !== 'multipart/form-data') {
|
|
571
|
+
for (const value of form_data.values()) {
|
|
572
|
+
if (value instanceof File) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
'Your form contains <input type="file"> fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.'
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
@@ -408,29 +408,6 @@ export async function render_response({
|
|
|
408
408
|
}`);
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
const { remote_data } = event_state;
|
|
412
|
-
|
|
413
|
-
if (remote_data) {
|
|
414
|
-
/** @type {Record<string, any>} */
|
|
415
|
-
const remote = {};
|
|
416
|
-
|
|
417
|
-
for (const key in remote_data) {
|
|
418
|
-
remote[key] = await remote_data[key];
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// TODO this is repeated in a few places — dedupe it
|
|
422
|
-
const replacer = (/** @type {any} */ thing) => {
|
|
423
|
-
for (const key in options.hooks.transport) {
|
|
424
|
-
const encoded = options.hooks.transport[key].encode(thing);
|
|
425
|
-
if (encoded) {
|
|
426
|
-
return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
properties.push(`data: ${devalue.uneval(remote, replacer)}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
411
|
// create this before declaring `data`, which may contain references to `${global}`
|
|
435
412
|
blocks.push(`${global} = {
|
|
436
413
|
${properties.join(',\n\t\t\t\t\t\t')}
|
|
@@ -482,20 +459,45 @@ export async function render_response({
|
|
|
482
459
|
args.push(`{\n${indent}\t${hydrate.join(`,\n${indent}\t`)}\n${indent}}`);
|
|
483
460
|
}
|
|
484
461
|
|
|
462
|
+
const { remote_data } = event_state;
|
|
463
|
+
|
|
464
|
+
let serialized_remote_data = '';
|
|
465
|
+
|
|
466
|
+
if (remote_data) {
|
|
467
|
+
/** @type {Record<string, any>} */
|
|
468
|
+
const remote = {};
|
|
469
|
+
|
|
470
|
+
for (const key in remote_data) {
|
|
471
|
+
remote[key] = await remote_data[key];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// TODO this is repeated in a few places — dedupe it
|
|
475
|
+
const replacer = (/** @type {any} */ thing) => {
|
|
476
|
+
for (const key in options.hooks.transport) {
|
|
477
|
+
const encoded = options.hooks.transport[key].encode(thing);
|
|
478
|
+
if (encoded) {
|
|
479
|
+
return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
serialized_remote_data = `${global}.data = ${devalue.uneval(remote, replacer)};\n\n\t\t\t\t\t\t`;
|
|
485
|
+
}
|
|
486
|
+
|
|
485
487
|
// `client.app` is a proxy for `bundleStrategy === 'split'`
|
|
486
488
|
const boot = client.inline
|
|
487
489
|
? `${client.inline.script}
|
|
488
490
|
|
|
489
|
-
|
|
491
|
+
${serialized_remote_data}${global}.app.start(${args.join(', ')});`
|
|
490
492
|
: client.app
|
|
491
493
|
? `Promise.all([
|
|
492
494
|
import(${s(prefixed(client.start))}),
|
|
493
495
|
import(${s(prefixed(client.app))})
|
|
494
496
|
]).then(([kit, app]) => {
|
|
495
|
-
kit.start(app, ${args.join(', ')});
|
|
497
|
+
${serialized_remote_data}kit.start(app, ${args.join(', ')});
|
|
496
498
|
});`
|
|
497
499
|
: `import(${s(prefixed(client.start))}).then((app) => {
|
|
498
|
-
app.start(${args.join(', ')})
|
|
500
|
+
${serialized_remote_data}app.start(${args.join(', ')})
|
|
499
501
|
});`;
|
|
500
502
|
|
|
501
503
|
if (load_env_eagerly) {
|
|
@@ -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 { file_transport } from '../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) {
|
|
@@ -128,8 +129,8 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
128
129
|
return json(
|
|
129
130
|
/** @type {RemoteFunctionResponse} */ ({
|
|
130
131
|
type: 'result',
|
|
131
|
-
result: stringify(data, transport),
|
|
132
|
-
refreshes: await serialize_refreshes(form_client_refreshes)
|
|
132
|
+
result: stringify(data, { ...transport, File: file_transport }),
|
|
133
|
+
refreshes: data.issues ? {} : await serialize_refreshes(form_client_refreshes)
|
|
133
134
|
})
|
|
134
135
|
);
|
|
135
136
|
}
|
|
@@ -262,7 +263,7 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
|
|
|
262
263
|
const remotes = manifest._.remotes;
|
|
263
264
|
const module = await remotes[hash]?.();
|
|
264
265
|
|
|
265
|
-
let form = /** @type {RemoteForm<any>} */ (module?.default[name]);
|
|
266
|
+
let form = /** @type {RemoteForm<any, any>} */ (module?.default[name]);
|
|
266
267
|
|
|
267
268
|
if (!form) {
|
|
268
269
|
event.setHeaders({
|