@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.
@@ -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, { ...transport, File: file_transport }),
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 && !remote_id) {
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 + '/') {
@@ -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
- };
@@ -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
@@ -1,4 +1,4 @@
1
1
  // generated during release, do not modify
2
2
 
3
3
  /** @type {string} */
4
- export const VERSION = '2.43.8';
4
+ export const VERSION = '2.44.0';