@sveltejs/kit 2.48.8 → 2.49.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "2.48.8",
3
+ "version": "2.49.1",
4
4
  "description": "SvelteKit is the fastest way to build Svelte apps",
5
5
  "keywords": [
6
6
  "framework",
@@ -88,6 +88,7 @@ export function write_root(manifest_data, output) {
88
88
  }
89
89
 
90
90
  if (!browser) {
91
+ // svelte-ignore state_referenced_locally
91
92
  setContext('__svelte__', stores);
92
93
  }
93
94
 
@@ -97,6 +98,7 @@ export function write_root(manifest_data, output) {
97
98
  if (browser) {
98
99
  $effect.pre(() => stores.page.set(page));
99
100
  } else {
101
+ // svelte-ignore state_referenced_locally
100
102
  stores.page.set(page);
101
103
  }
102
104
  `
@@ -4,7 +4,6 @@
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,
@@ -105,19 +104,7 @@ export function form(validate_or_fn, maybe_fn) {
105
104
  type: 'form',
106
105
  name: '',
107
106
  id: '',
108
- /** @param {FormData} form_data */
109
- fn: async (form_data) => {
110
- const validate_only = form_data.get('sveltekit:validate_only') === 'true';
111
-
112
- let data = maybe_fn ? convert_formdata(form_data) : undefined;
113
-
114
- if (data && data.id === undefined) {
115
- const id = form_data.get('sveltekit:id');
116
- if (typeof id === 'string') {
117
- data.id = JSON.parse(id);
118
- }
119
- }
120
-
107
+ fn: async (data, meta, form_data) => {
121
108
  // TODO 3.0 remove this warning
122
109
  if (DEV && !data) {
123
110
  const error = () => {
@@ -153,12 +140,12 @@ export function form(validate_or_fn, maybe_fn) {
153
140
  const { event, state } = get_request_store();
154
141
  const validated = await schema?.['~standard'].validate(data);
155
142
 
156
- if (validate_only) {
143
+ if (meta.validate_only) {
157
144
  return validated?.issues?.map((issue) => normalize_issue(issue, true)) ?? [];
158
145
  }
159
146
 
160
147
  if (validated?.issues !== undefined) {
161
- handle_issues(output, validated.issues, event.isRemoteRequest, form_data);
148
+ handle_issues(output, validated.issues, form_data);
162
149
  } else {
163
150
  if (validated !== undefined) {
164
151
  data = validated.value;
@@ -179,7 +166,7 @@ export function form(validate_or_fn, maybe_fn) {
179
166
  );
180
167
  } catch (e) {
181
168
  if (e instanceof ValidationError) {
182
- handle_issues(output, e.issues, event.isRemoteRequest, form_data);
169
+ handle_issues(output, e.issues, form_data);
183
170
  } else {
184
171
  throw e;
185
172
  }
@@ -298,15 +285,14 @@ export function form(validate_or_fn, maybe_fn) {
298
285
  /**
299
286
  * @param {{ issues?: InternalRemoteFormIssue[], input?: Record<string, any>, result: any }} output
300
287
  * @param {readonly StandardSchemaV1.Issue[]} issues
301
- * @param {boolean} is_remote_request
302
- * @param {FormData} form_data
288
+ * @param {FormData | null} form_data - null if the form is progressively enhanced
303
289
  */
304
- function handle_issues(output, issues, is_remote_request, form_data) {
290
+ function handle_issues(output, issues, form_data) {
305
291
  output.issues = issues.map((issue) => normalize_issue(issue, true));
306
292
 
307
293
  // if it was a progressively-enhanced submission, we don't need
308
294
  // to return the input — it's already there
309
- if (!is_remote_request) {
295
+ if (form_data) {
310
296
  output.input = {};
311
297
 
312
298
  for (let key of form_data.keys()) {
@@ -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
- if (updates.length > 0) {
186
- data.set('sveltekit:remote_refreshes', JSON.stringify(updates.map((u) => u._key)));
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/${action_id}`, {
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) {
@@ -539,7 +543,9 @@ export function form(id) {
539
543
  /** @type {InternalRemoteFormIssue[]} */
540
544
  let array = [];
541
545
 
542
- const validated = await preflight_schema?.['~standard'].validate(convert(form_data));
546
+ const data = convert(form_data);
547
+
548
+ const validated = await preflight_schema?.['~standard'].validate(data);
543
549
 
544
550
  if (validate_id !== id) {
545
551
  return;
@@ -548,11 +554,16 @@ export function form(id) {
548
554
  if (validated?.issues) {
549
555
  array = validated.issues.map((issue) => normalize_issue(issue, false));
550
556
  } else if (!preflightOnly) {
551
- form_data.set('sveltekit:validate_only', 'true');
552
-
553
- const response = await fetch(`${base}/${app_dir}/remote/${action_id}`, {
557
+ const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
554
558
  method: 'POST',
555
- body: form_data
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
556
567
  });
557
568
 
558
569
  const result = await response.json();
@@ -644,12 +655,6 @@ function clone(element) {
644
655
  */
645
656
  function validate_form_data(form_data, enctype) {
646
657
  for (const key of form_data.keys()) {
647
- if (key.startsWith('sveltekit:')) {
648
- throw new Error(
649
- 'FormData keys starting with `sveltekit:` are reserved for internal use and should not be set manually'
650
- );
651
- }
652
-
653
658
  if (/^\$[.[]?/.test(key)) {
654
659
  throw new Error(
655
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
  /**
@@ -316,7 +316,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
316
316
  let teed_body;
317
317
 
318
318
  const proxy = new Proxy(response, {
319
- get(response, key, _receiver) {
319
+ get(response, key, receiver) {
320
320
  /**
321
321
  * @param {string | undefined} body
322
322
  * @param {boolean} is_b64
@@ -427,7 +427,28 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
427
427
  };
428
428
  }
429
429
 
430
- return Reflect.get(response, key, response);
430
+ const value = Reflect.get(response, key, response);
431
+
432
+ if (value instanceof Function) {
433
+ // On Node v24+, the Response object has a private element #state – we
434
+ // need to bind this function to the response in order to allow it to
435
+ // access this private element. Defining the name and length ensure it
436
+ // is identical to the original function when introspected.
437
+ return Object.defineProperties(
438
+ /**
439
+ * @this {any}
440
+ */
441
+ function () {
442
+ return Reflect.apply(value, this === receiver ? response : this, arguments);
443
+ },
444
+ {
445
+ name: { value: value.name },
446
+ length: { value: value.length }
447
+ }
448
+ );
449
+ }
450
+
451
+ return value;
431
452
  }
432
453
  });
433
454
 
@@ -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.formData();
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
- if (additional_args) {
127
- form_data.set('sveltekit:id', decodeURIComponent(additional_args));
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 data = await with_request_store({ event, state }, () => fn(form_data));
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(data, transport),
137
- refreshes: data.issues ? {} : await serialize_refreshes(form_client_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
- for (const key of client_refreshes) {
213
- if (refreshes[key] !== undefined) continue;
210
+ if (client_refreshes) {
211
+ for (const key of client_refreshes) {
212
+ if (refreshes[key] !== undefined) continue;
214
213
 
215
- const [hash, name, payload] = key.split('/');
214
+ const [hash, name, payload] = key.split('/');
216
215
 
217
- const loader = manifest._.remotes[hash];
218
- const fn = (await loader?.())?.default?.[name];
216
+ const loader = manifest._.remotes[hash];
217
+ const fn = (await loader?.())?.default?.[name];
219
218
 
220
- if (!fn) error(400, 'Bad Request');
219
+ if (!fn) error(400, 'Bad Request');
221
220
 
222
- refreshes[key] = with_request_store({ event, state }, () =>
223
- fn(parse_remote_arg(payload, transport))
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
- // If this is a keyed form instance (created via form.for(key)), add the key to the form data (unless already set)
298
- if (action_id && !form_data.has('id')) {
299
- // The action_id is URL-encoded JSON, decode and parse it
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.
@@ -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: (data: FormData) => Promise<any>;
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
@@ -1,4 +1,4 @@
1
1
  // generated during release, do not modify
2
2
 
3
3
  /** @type {string} */
4
- export const VERSION = '2.48.8';
4
+ export const VERSION = '2.49.1';