@sveltejs/kit 2.43.8 → 2.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/core/postbuild/prerender.js +2 -1
- package/src/exports/public.d.ts +129 -82
- package/src/runtime/app/server/remote/form.js +58 -26
- package/src/runtime/app/server/remote/shared.js +36 -32
- package/src/runtime/client/remote-functions/command.svelte.js +3 -1
- package/src/runtime/client/remote-functions/form.svelte.js +141 -51
- package/src/runtime/client/remote-functions/shared.svelte.js +9 -1
- package/src/runtime/form-utils.svelte.js +439 -0
- package/src/runtime/server/page/render.js +2 -2
- package/src/runtime/server/remote.js +14 -4
- package/src/runtime/server/respond.js +6 -5
- package/src/runtime/utils.js +0 -123
- package/src/types/internal.d.ts +9 -1
- package/src/version.js +1 -1
- package/types/index.d.ts +129 -82
- package/types/index.d.ts.map +13 -5
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/** @import { RemoteForm } from '@sveltejs/kit' */
|
|
2
|
+
/** @import { InternalRemoteFormIssue } from 'types' */
|
|
3
|
+
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
|
4
|
+
|
|
5
|
+
import { DEV } from 'esm-env';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sets a value in a nested object using a path string, not mutating the original object but returning a new object
|
|
9
|
+
* @param {Record<string, any>} object
|
|
10
|
+
* @param {string} path_string
|
|
11
|
+
* @param {any} value
|
|
12
|
+
*/
|
|
13
|
+
export function set_nested_value(object, path_string, value) {
|
|
14
|
+
if (path_string.startsWith('n:')) {
|
|
15
|
+
path_string = path_string.slice(2);
|
|
16
|
+
value = value === '' ? undefined : parseFloat(value);
|
|
17
|
+
} else if (path_string.startsWith('b:')) {
|
|
18
|
+
path_string = path_string.slice(2);
|
|
19
|
+
value = value === 'on';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return deep_set(object, split_path(path_string), value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert `FormData` into a POJO
|
|
27
|
+
* @param {FormData} data
|
|
28
|
+
*/
|
|
29
|
+
export function convert_formdata(data) {
|
|
30
|
+
/** @type {Record<string, any>} */
|
|
31
|
+
let result = Object.create(null); // guard against prototype pollution
|
|
32
|
+
|
|
33
|
+
for (let key of data.keys()) {
|
|
34
|
+
if (key.startsWith('sveltekit:')) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const is_array = key.endsWith('[]');
|
|
39
|
+
/** @type {any[]} */
|
|
40
|
+
let values = data.getAll(key);
|
|
41
|
+
|
|
42
|
+
if (is_array) key = key.slice(0, -2);
|
|
43
|
+
|
|
44
|
+
if (values.length > 1 && !is_array) {
|
|
45
|
+
throw new Error(`Form cannot contain duplicated keys — "${key}" has ${values.length} values`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// an empty `<input type="file">` will submit a non-existent file, bizarrely
|
|
49
|
+
values = values.filter(
|
|
50
|
+
(entry) => typeof entry === 'string' || entry.name !== '' || entry.size > 0
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (key.startsWith('n:')) {
|
|
54
|
+
key = key.slice(2);
|
|
55
|
+
values = values.map((v) => (v === '' ? undefined : parseFloat(/** @type {string} */ (v))));
|
|
56
|
+
} else if (key.startsWith('b:')) {
|
|
57
|
+
key = key.slice(2);
|
|
58
|
+
values = values.map((v) => v === 'on');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
result = set_nested_value(result, key, is_array ? values : values[0]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const path_regex = /^[a-zA-Z_$]\w*(\.[a-zA-Z_$]\w*|\[\d+\])*$/;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} path
|
|
71
|
+
*/
|
|
72
|
+
export function split_path(path) {
|
|
73
|
+
if (!path_regex.test(path)) {
|
|
74
|
+
throw new Error(`Invalid path ${path}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return path.split(/\.|\[|\]/).filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Sets a value in a nested object using an array of keys.
|
|
82
|
+
* Does not mutate the original object; returns a new object.
|
|
83
|
+
* @param {Record<string, any>} object
|
|
84
|
+
* @param {string[]} keys
|
|
85
|
+
* @param {any} value
|
|
86
|
+
*/
|
|
87
|
+
export function deep_set(object, keys, value) {
|
|
88
|
+
const result = Object.assign(Object.create(null), object); // guard against prototype pollution
|
|
89
|
+
let current = result;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < keys.length - 1; i += 1) {
|
|
92
|
+
const key = keys[i];
|
|
93
|
+
const is_array = /^\d+$/.test(keys[i + 1]);
|
|
94
|
+
const exists = key in current;
|
|
95
|
+
const inner = current[key];
|
|
96
|
+
|
|
97
|
+
if (exists && is_array !== Array.isArray(inner)) {
|
|
98
|
+
throw new Error(`Invalid array key ${keys[i + 1]}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
current[key] = is_array
|
|
102
|
+
? exists
|
|
103
|
+
? [...inner]
|
|
104
|
+
: []
|
|
105
|
+
: // guard against prototype pollution
|
|
106
|
+
Object.assign(Object.create(null), inner);
|
|
107
|
+
|
|
108
|
+
current = current[key];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
current[keys[keys.length - 1]] = value;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {readonly StandardSchemaV1.Issue[]} issues
|
|
117
|
+
* @param {boolean} [server=false] - Whether these issues come from server validation
|
|
118
|
+
*/
|
|
119
|
+
export function flatten_issues(issues, server = false) {
|
|
120
|
+
/** @type {Record<string, InternalRemoteFormIssue[]>} */
|
|
121
|
+
const result = {};
|
|
122
|
+
|
|
123
|
+
for (const issue of issues) {
|
|
124
|
+
/** @type {InternalRemoteFormIssue} */
|
|
125
|
+
const normalized = { name: '', path: [], message: issue.message, server };
|
|
126
|
+
|
|
127
|
+
(result.$ ??= []).push(normalized);
|
|
128
|
+
|
|
129
|
+
let name = '';
|
|
130
|
+
|
|
131
|
+
if (issue.path !== undefined) {
|
|
132
|
+
for (const segment of issue.path) {
|
|
133
|
+
const key = /** @type {string | number} */ (
|
|
134
|
+
typeof segment === 'object' ? segment.key : segment
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
normalized.path.push(key);
|
|
138
|
+
|
|
139
|
+
if (typeof key === 'number') {
|
|
140
|
+
name += `[${key}]`;
|
|
141
|
+
} else if (typeof key === 'string') {
|
|
142
|
+
name += name === '' ? key : '.' + key;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
(result[name] ??= []).push(normalized);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
normalized.name = name;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gets a nested value from an object using a path array
|
|
157
|
+
* @param {Record<string, any>} object
|
|
158
|
+
* @param {(string | number)[]} path
|
|
159
|
+
* @returns {any}
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
export function deep_get(object, path) {
|
|
163
|
+
let current = object;
|
|
164
|
+
for (const key of path) {
|
|
165
|
+
if (current == null || typeof current !== 'object') {
|
|
166
|
+
return current;
|
|
167
|
+
}
|
|
168
|
+
current = current[key];
|
|
169
|
+
}
|
|
170
|
+
return current;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Creates a proxy-based field accessor for form data
|
|
175
|
+
* @param {any} target - Function or empty POJO
|
|
176
|
+
* @param {() => Record<string, any>} get_input - Function to get current input data
|
|
177
|
+
* @param {(path: (string | number)[], value: any) => void} set_input - Function to set input data
|
|
178
|
+
* @param {() => Record<string, InternalRemoteFormIssue[]>} get_issues - Function to get current issues
|
|
179
|
+
* @param {(string | number)[]} path - Current access path
|
|
180
|
+
* @returns {any} Proxy object with name(), value(), and issues() methods
|
|
181
|
+
*/
|
|
182
|
+
export function create_field_proxy(target, get_input, set_input, get_issues, path = []) {
|
|
183
|
+
return new Proxy(target, {
|
|
184
|
+
get(target, prop) {
|
|
185
|
+
if (typeof prop === 'symbol') return target[prop];
|
|
186
|
+
|
|
187
|
+
// Handle array access like jobs[0]
|
|
188
|
+
if (/^\d+$/.test(prop)) {
|
|
189
|
+
return create_field_proxy({}, get_input, set_input, get_issues, [
|
|
190
|
+
...path,
|
|
191
|
+
parseInt(prop, 10)
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const key = build_path_string(path);
|
|
196
|
+
|
|
197
|
+
if (prop === 'set') {
|
|
198
|
+
const set_func = function (/** @type {any} */ newValue) {
|
|
199
|
+
set_input(path, newValue);
|
|
200
|
+
return newValue;
|
|
201
|
+
};
|
|
202
|
+
return create_field_proxy(set_func, get_input, set_input, get_issues, [...path, prop]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (prop === 'value') {
|
|
206
|
+
const value_func = function () {
|
|
207
|
+
// TODO Ideally we'd create a $derived just above and use it here but we can't because of push_reaction which prevents
|
|
208
|
+
// changes to deriveds created within an effect to rerun the effect - an argument for
|
|
209
|
+
// reverting that change in async mode?
|
|
210
|
+
// TODO we did that in Svelte now; bump Svelte version and use $derived here
|
|
211
|
+
return deep_get(get_input(), path);
|
|
212
|
+
};
|
|
213
|
+
return create_field_proxy(value_func, get_input, set_input, get_issues, [...path, prop]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (prop === 'issues' || prop === 'allIssues') {
|
|
217
|
+
const issues_func = () => {
|
|
218
|
+
const all_issues = get_issues()[key === '' ? '$' : key];
|
|
219
|
+
|
|
220
|
+
if (prop === 'allIssues') {
|
|
221
|
+
return all_issues?.map((issue) => ({
|
|
222
|
+
message: issue.message
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return all_issues
|
|
227
|
+
?.filter((issue) => issue.name === key)
|
|
228
|
+
?.map((issue) => ({
|
|
229
|
+
message: issue.message
|
|
230
|
+
}));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return create_field_proxy(issues_func, get_input, set_input, get_issues, [...path, prop]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (prop === 'as') {
|
|
237
|
+
/**
|
|
238
|
+
* @param {string} type
|
|
239
|
+
* @param {string} [input_value]
|
|
240
|
+
*/
|
|
241
|
+
const as_func = (type, input_value) => {
|
|
242
|
+
const is_array =
|
|
243
|
+
type === 'file multiple' ||
|
|
244
|
+
type === 'select multiple' ||
|
|
245
|
+
(type === 'checkbox' && typeof input_value === 'string');
|
|
246
|
+
|
|
247
|
+
const prefix =
|
|
248
|
+
type === 'number' || type === 'range'
|
|
249
|
+
? 'n:'
|
|
250
|
+
: type === 'checkbox' && !is_array
|
|
251
|
+
? 'b:'
|
|
252
|
+
: '';
|
|
253
|
+
|
|
254
|
+
// Base properties for all input types
|
|
255
|
+
/** @type {Record<string, any>} */
|
|
256
|
+
const base_props = {
|
|
257
|
+
name: prefix + key + (is_array ? '[]' : ''),
|
|
258
|
+
get 'aria-invalid'() {
|
|
259
|
+
const issues = get_issues();
|
|
260
|
+
return key in issues ? 'true' : undefined;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Add type attribute only for non-text inputs and non-select elements
|
|
265
|
+
if (type !== 'text' && type !== 'select' && type !== 'select multiple') {
|
|
266
|
+
base_props.type = type === 'file multiple' ? 'file' : type;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Handle select inputs
|
|
270
|
+
if (type === 'select' || type === 'select multiple') {
|
|
271
|
+
return Object.defineProperties(base_props, {
|
|
272
|
+
multiple: { value: is_array, enumerable: true },
|
|
273
|
+
value: {
|
|
274
|
+
enumerable: true,
|
|
275
|
+
get() {
|
|
276
|
+
const input = get_input();
|
|
277
|
+
return deep_get(input, path);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Handle checkbox inputs
|
|
284
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
285
|
+
if (DEV) {
|
|
286
|
+
if (type === 'radio' && !input_value) {
|
|
287
|
+
throw new Error('Radio inputs must have a value');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (type === 'checkbox' && is_array && !input_value) {
|
|
291
|
+
throw new Error('Checkbox array inputs must have a value');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return Object.defineProperties(base_props, {
|
|
296
|
+
value: { value: input_value ?? 'on', enumerable: true },
|
|
297
|
+
checked: {
|
|
298
|
+
enumerable: true,
|
|
299
|
+
get() {
|
|
300
|
+
const input = get_input();
|
|
301
|
+
const value = deep_get(input, path);
|
|
302
|
+
|
|
303
|
+
if (type === 'radio') {
|
|
304
|
+
return value === input_value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (is_array) {
|
|
308
|
+
return (value ?? []).includes(input_value);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle file inputs
|
|
318
|
+
if (type === 'file' || type === 'file multiple') {
|
|
319
|
+
return Object.defineProperties(base_props, {
|
|
320
|
+
multiple: { value: is_array, enumerable: true },
|
|
321
|
+
files: {
|
|
322
|
+
enumerable: true,
|
|
323
|
+
get() {
|
|
324
|
+
const input = get_input();
|
|
325
|
+
const value = deep_get(input, path);
|
|
326
|
+
|
|
327
|
+
// Convert File/File[] to FileList-like object
|
|
328
|
+
if (value instanceof File) {
|
|
329
|
+
// In browsers, we can create a proper FileList using DataTransfer
|
|
330
|
+
if (typeof DataTransfer !== 'undefined') {
|
|
331
|
+
const fileList = new DataTransfer();
|
|
332
|
+
fileList.items.add(value);
|
|
333
|
+
return fileList.files;
|
|
334
|
+
}
|
|
335
|
+
// Fallback for environments without DataTransfer
|
|
336
|
+
return { 0: value, length: 1 };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (Array.isArray(value) && value.every((f) => f instanceof File)) {
|
|
340
|
+
if (typeof DataTransfer !== 'undefined') {
|
|
341
|
+
const fileList = new DataTransfer();
|
|
342
|
+
value.forEach((file) => fileList.items.add(file));
|
|
343
|
+
return fileList.files;
|
|
344
|
+
}
|
|
345
|
+
// Fallback for environments without DataTransfer
|
|
346
|
+
/** @type {any} */
|
|
347
|
+
const fileListLike = { length: value.length };
|
|
348
|
+
value.forEach((file, index) => {
|
|
349
|
+
fileListLike[index] = file;
|
|
350
|
+
});
|
|
351
|
+
return fileListLike;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Handle all other input types (text, number, etc.)
|
|
361
|
+
return Object.defineProperties(base_props, {
|
|
362
|
+
value: {
|
|
363
|
+
enumerable: true,
|
|
364
|
+
get() {
|
|
365
|
+
const input = get_input();
|
|
366
|
+
const value = deep_get(input, path);
|
|
367
|
+
|
|
368
|
+
return value != null ? String(value) : '';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
return create_field_proxy(as_func, get_input, set_input, get_issues, [...path, 'as']);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Handle property access (nested fields)
|
|
378
|
+
return create_field_proxy({}, get_input, set_input, get_issues, [...path, prop]);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Builds a path string from an array of path segments
|
|
385
|
+
* @param {(string | number)[]} path
|
|
386
|
+
* @returns {string}
|
|
387
|
+
*/
|
|
388
|
+
function build_path_string(path) {
|
|
389
|
+
let result = '';
|
|
390
|
+
|
|
391
|
+
for (const segment of path) {
|
|
392
|
+
if (typeof segment === 'number') {
|
|
393
|
+
result += `[${segment}]`;
|
|
394
|
+
} else {
|
|
395
|
+
result += result === '' ? segment : '.' + segment;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @param {RemoteForm<any, any>} instance
|
|
404
|
+
* @deprecated remove in 3.0
|
|
405
|
+
*/
|
|
406
|
+
export function throw_on_old_property_access(instance) {
|
|
407
|
+
Object.defineProperty(instance, 'field', {
|
|
408
|
+
value: (/** @type {string} */ name) => {
|
|
409
|
+
const new_name = name.endsWith('[]') ? name.slice(0, -2) : name;
|
|
410
|
+
throw new Error(
|
|
411
|
+
`\`form.field\` has been removed: Instead of \`<input name={form.field('${name}')} />\` do \`<input {...form.fields.${new_name}.as(type)} />\``
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
for (const property of ['input', 'issues']) {
|
|
417
|
+
Object.defineProperty(instance, property, {
|
|
418
|
+
get() {
|
|
419
|
+
const new_name = property === 'issues' ? 'issues' : 'value';
|
|
420
|
+
return new Proxy(
|
|
421
|
+
{},
|
|
422
|
+
{
|
|
423
|
+
get(_, prop) {
|
|
424
|
+
const prop_string = typeof prop === 'string' ? prop : String(prop);
|
|
425
|
+
const old =
|
|
426
|
+
prop_string.includes('[') || prop_string.includes('.')
|
|
427
|
+
? `['${prop_string}']`
|
|
428
|
+
: `.${prop_string}`;
|
|
429
|
+
const replacement = `.${prop_string}.${new_name}()`;
|
|
430
|
+
throw new Error(
|
|
431
|
+
`\`form.${property}\` has been removed: Instead of \`form.${property}${old}\` write \`form.fields${replacement}\``
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -13,7 +13,7 @@ import { SVELTE_KIT_ASSETS } from '../../../constants.js';
|
|
|
13
13
|
import { SCHEME } from '../../../utils/url.js';
|
|
14
14
|
import { create_server_routing_response, generate_route_object } from './server_routing.js';
|
|
15
15
|
import { add_resolution_suffix } from '../../pathname.js';
|
|
16
|
-
import { with_request_store } from '@sveltejs/kit/internal/server';
|
|
16
|
+
import { try_get_request_store, with_request_store } from '@sveltejs/kit/internal/server';
|
|
17
17
|
import { text_encoder } from '../../utils.js';
|
|
18
18
|
import { get_global_name } from '../utils.js';
|
|
19
19
|
import { create_remote_cache_key } from '../../shared.js';
|
|
@@ -190,7 +190,7 @@ export async function render_response({
|
|
|
190
190
|
throw new Error(
|
|
191
191
|
`Cannot call \`fetch\` eagerly during server-side rendering with relative URL (${info}) — put your \`fetch\` calls inside \`onMount\` or a \`load\` function instead`
|
|
192
192
|
);
|
|
193
|
-
} else if (!warned) {
|
|
193
|
+
} else if (!warned && !try_get_request_store()?.state.is_in_remote_function) {
|
|
194
194
|
console.warn(
|
|
195
195
|
'Avoid calling `fetch` eagerly during server-side rendering — put your `fetch` calls inside `onMount` or a `load` function instead'
|
|
196
196
|
);
|
|
@@ -12,7 +12,6 @@ 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';
|
|
16
15
|
|
|
17
16
|
/** @type {typeof handle_remote_call_internal} */
|
|
18
17
|
export async function handle_remote_call(event, state, options, manifest, id) {
|
|
@@ -38,7 +37,7 @@ export async function handle_remote_call(event, state, options, manifest, id) {
|
|
|
38
37
|
* @param {string} id
|
|
39
38
|
*/
|
|
40
39
|
async function handle_remote_call_internal(event, state, options, manifest, id) {
|
|
41
|
-
const [hash, name,
|
|
40
|
+
const [hash, name, additional_args] = id.split('/');
|
|
42
41
|
const remotes = manifest._.remotes;
|
|
43
42
|
|
|
44
43
|
if (!remotes[hash]) error(404);
|
|
@@ -123,13 +122,18 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
123
122
|
);
|
|
124
123
|
form_data.delete('sveltekit:remote_refreshes');
|
|
125
124
|
|
|
125
|
+
// 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));
|
|
128
|
+
}
|
|
129
|
+
|
|
126
130
|
const fn = info.fn;
|
|
127
131
|
const data = await with_request_store({ event, state }, () => fn(form_data));
|
|
128
132
|
|
|
129
133
|
return json(
|
|
130
134
|
/** @type {RemoteFunctionResponse} */ ({
|
|
131
135
|
type: 'result',
|
|
132
|
-
result: stringify(data,
|
|
136
|
+
result: stringify(data, transport),
|
|
133
137
|
refreshes: data.issues ? {} : await serialize_refreshes(form_client_refreshes)
|
|
134
138
|
})
|
|
135
139
|
);
|
|
@@ -152,7 +156,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id)
|
|
|
152
156
|
|
|
153
157
|
const payload =
|
|
154
158
|
info.type === 'prerender'
|
|
155
|
-
?
|
|
159
|
+
? additional_args
|
|
156
160
|
: /** @type {string} */ (
|
|
157
161
|
// new URL(...) necessary because we're hiding the URL from the user in the event object
|
|
158
162
|
new URL(event.request.url).searchParams.get('payload')
|
|
@@ -290,6 +294,12 @@ async function handle_remote_form_post_internal(event, state, manifest, id) {
|
|
|
290
294
|
const form_data = await event.request.formData();
|
|
291
295
|
const fn = /** @type {RemoteInfo & { type: 'form' }} */ (/** @type {any} */ (form).__).fn;
|
|
292
296
|
|
|
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));
|
|
301
|
+
}
|
|
302
|
+
|
|
293
303
|
await with_request_store({ event, state }, () => fn(form_data));
|
|
294
304
|
|
|
295
305
|
// We don't want the data to appear on `let { form } = $props()`, which is why we're not returning it.
|
|
@@ -129,8 +129,8 @@ export async function internal_respond(request, options, manifest, state) {
|
|
|
129
129
|
.map((node) => node === '1');
|
|
130
130
|
url.searchParams.delete(INVALIDATED_PARAM);
|
|
131
131
|
} else if (remote_id) {
|
|
132
|
-
url.pathname = base;
|
|
133
|
-
url.search = '';
|
|
132
|
+
url.pathname = request.headers.get('x-sveltekit-pathname') ?? base;
|
|
133
|
+
url.search = request.headers.get('x-sveltekit-search') ?? '';
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/** @type {Record<string, string>} */
|
|
@@ -148,7 +148,8 @@ export async function internal_respond(request, options, manifest, state) {
|
|
|
148
148
|
handleValidationError: options.hooks.handleValidationError,
|
|
149
149
|
tracing: {
|
|
150
150
|
record_span
|
|
151
|
-
}
|
|
151
|
+
},
|
|
152
|
+
is_in_remote_function: false
|
|
152
153
|
};
|
|
153
154
|
|
|
154
155
|
/** @type {import('@sveltejs/kit').RequestEvent} */
|
|
@@ -294,7 +295,7 @@ export async function internal_respond(request, options, manifest, state) {
|
|
|
294
295
|
return text('Not found', { status: 404, headers });
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
if (!state.prerendering?.fallback
|
|
298
|
+
if (!state.prerendering?.fallback) {
|
|
298
299
|
// TODO this could theoretically break — should probably be inside a try-catch
|
|
299
300
|
const matchers = await manifest._.matchers();
|
|
300
301
|
|
|
@@ -329,7 +330,7 @@ export async function internal_respond(request, options, manifest, state) {
|
|
|
329
330
|
: undefined;
|
|
330
331
|
|
|
331
332
|
// determine whether we need to redirect to add/remove a trailing slash
|
|
332
|
-
if (route) {
|
|
333
|
+
if (route && !remote_id) {
|
|
333
334
|
// if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`,
|
|
334
335
|
// regardless of the `trailingSlash` route option
|
|
335
336
|
if (url.pathname === base || url.pathname === base + '/') {
|
package/src/runtime/utils.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** @import { RemoteFormIssue } from '@sveltejs/kit' */
|
|
2
|
-
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
|
3
1
|
import { BROWSER } from 'esm-env';
|
|
4
2
|
|
|
5
3
|
export const text_encoder = new TextEncoder();
|
|
@@ -65,124 +63,3 @@ export function base64_decode(encoded) {
|
|
|
65
63
|
|
|
66
64
|
return bytes;
|
|
67
65
|
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Convert `FormData` into a POJO
|
|
71
|
-
* @param {FormData} data
|
|
72
|
-
*/
|
|
73
|
-
export function convert_formdata(data) {
|
|
74
|
-
/** @type {Record<string, any>} */
|
|
75
|
-
const result = Object.create(null); // guard against prototype pollution
|
|
76
|
-
|
|
77
|
-
for (let key of data.keys()) {
|
|
78
|
-
const is_array = key.endsWith('[]');
|
|
79
|
-
let values = data.getAll(key);
|
|
80
|
-
|
|
81
|
-
if (is_array) key = key.slice(0, -2);
|
|
82
|
-
|
|
83
|
-
if (values.length > 1 && !is_array) {
|
|
84
|
-
throw new Error(`Form cannot contain duplicated keys — "${key}" has ${values.length} values`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// an empty `<input type="file">` will submit a non-existent file, bizarrely
|
|
88
|
-
values = values.filter(
|
|
89
|
-
(entry) => typeof entry === 'string' || entry.name !== '' || entry.size > 0
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
deep_set(result, split_path(key), is_array ? values : values[0]);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const path_regex = /^[a-zA-Z_$]\w*(\.[a-zA-Z_$]\w*|\[\d+\])*$/;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* @param {string} path
|
|
102
|
-
*/
|
|
103
|
-
export function split_path(path) {
|
|
104
|
-
if (!path_regex.test(path)) {
|
|
105
|
-
throw new Error(`Invalid path ${path}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return path.split(/\.|\[|\]/).filter(Boolean);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @param {Record<string, any>} object
|
|
113
|
-
* @param {string[]} keys
|
|
114
|
-
* @param {any} value
|
|
115
|
-
*/
|
|
116
|
-
export function deep_set(object, keys, value) {
|
|
117
|
-
for (let i = 0; i < keys.length - 1; i += 1) {
|
|
118
|
-
const key = keys[i];
|
|
119
|
-
const is_array = /^\d+$/.test(keys[i + 1]);
|
|
120
|
-
|
|
121
|
-
if (object[key]) {
|
|
122
|
-
if (is_array !== Array.isArray(object[key])) {
|
|
123
|
-
throw new Error(`Invalid array key ${keys[i + 1]}`);
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
object[key] ??= is_array ? [] : Object.create(null); // guard against prototype pollution
|
|
127
|
-
}
|
|
128
|
-
object = object[key];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
object[keys[keys.length - 1]] = value;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* @param {readonly StandardSchemaV1.Issue[]} issues
|
|
136
|
-
*/
|
|
137
|
-
export function flatten_issues(issues) {
|
|
138
|
-
/** @type {Record<string, RemoteFormIssue[]>} */
|
|
139
|
-
const result = {};
|
|
140
|
-
|
|
141
|
-
for (const issue of issues) {
|
|
142
|
-
/** @type {RemoteFormIssue} */
|
|
143
|
-
const normalized = { name: '', path: [], message: issue.message };
|
|
144
|
-
|
|
145
|
-
(result.$ ??= []).push(normalized);
|
|
146
|
-
|
|
147
|
-
let name = '';
|
|
148
|
-
|
|
149
|
-
if (issue.path !== undefined) {
|
|
150
|
-
for (const segment of issue.path) {
|
|
151
|
-
const key = /** @type {string | number} */ (
|
|
152
|
-
typeof segment === 'object' ? segment.key : segment
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
normalized.path.push(key);
|
|
156
|
-
|
|
157
|
-
if (typeof key === 'number') {
|
|
158
|
-
name += `[${key}]`;
|
|
159
|
-
} else if (typeof key === 'string') {
|
|
160
|
-
name += name === '' ? key : '.' + key;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
(result[name] ??= []).push(normalized);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
normalized.name = name;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* We need to encode `File` objects when returning `issues` from a `form` submission,
|
|
175
|
-
* because some validators include the original value in the issue. It doesn't
|
|
176
|
-
* need to deserialize to a `File` object
|
|
177
|
-
* @type {import('@sveltejs/kit').Transporter}
|
|
178
|
-
*/
|
|
179
|
-
export const file_transport = {
|
|
180
|
-
encode: (file) =>
|
|
181
|
-
file instanceof File && {
|
|
182
|
-
size: file.size,
|
|
183
|
-
type: file.type,
|
|
184
|
-
name: file.name,
|
|
185
|
-
lastModified: file.lastModified
|
|
186
|
-
},
|
|
187
|
-
decode: (data) => data
|
|
188
|
-
};
|