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