@sveltejs/kit 2.48.8 → 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/runtime/app/server/remote/form.js +7 -21
- package/src/runtime/client/remote-functions/form.svelte.js +23 -18
- 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/package.json
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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 {
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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) {
|
|
@@ -539,7 +543,9 @@ export function form(id) {
|
|
|
539
543
|
/** @type {InternalRemoteFormIssue[]} */
|
|
540
544
|
let array = [];
|
|
541
545
|
|
|
542
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -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