@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "2.43.8",
3
+ "version": "2.45.0",
4
4
  "description": "SvelteKit is the fastest way to build Svelte apps",
5
5
  "keywords": [
6
6
  "framework",
@@ -41,7 +41,7 @@
41
41
  "@types/set-cookie-parser": "^2.4.7",
42
42
  "dts-buddy": "^0.6.2",
43
43
  "rollup": "^4.14.2",
44
- "svelte": "^5.39.3",
44
+ "svelte": "^5.39.8",
45
45
  "svelte-preprocess": "^6.0.0",
46
46
  "typescript": "^5.3.3",
47
47
  "vite": "^6.3.5",
@@ -320,7 +320,8 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
320
320
  // avoid triggering `filterSerializeResponseHeaders` guard
321
321
  const headers = Object.fromEntries(response.headers);
322
322
 
323
- if (config.prerender.crawl && headers['content-type'] === 'text/html') {
323
+ // if it's a 200 HTML response, crawl it. Skip error responses, as we don't save those
324
+ if (response.ok && config.prerender.crawl && headers['content-type'] === 'text/html') {
324
325
  const { ids, hrefs } = crawl(body.toString(), decoded);
325
326
 
326
327
  actual_hashlinks.set(decoded, ids);
@@ -1813,81 +1813,138 @@ export interface Snapshot<T = any> {
1813
1813
  // If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle
1814
1814
  type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
1815
1815
 
1816
- // Helper type to convert union to intersection
1817
- type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
1818
- ? I
1819
- : never;
1820
-
1821
- type FlattenInput<T, Prefix extends string> = T extends string | number | boolean | null | undefined
1822
- ? { [P in Prefix]: string }
1823
- : WillRecurseIndefinitely<T> extends true
1824
- ? { [key: string]: string }
1825
- : T extends Array<infer U>
1826
- ? U extends string | File
1827
- ? { [P in Prefix]: string[] }
1828
- : FlattenInput<U, `${Prefix}[${number}]`>
1829
- : T extends File
1830
- ? { [P in Prefix]: string }
1831
- : {
1832
- // Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1833
- [K in keyof Required<T>]: FlattenInput<
1834
- T[K],
1835
- Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1836
- >;
1837
- }[keyof T];
1838
-
1839
- type FlattenIssues<T, Prefix extends string> = T extends
1840
- | string
1841
- | number
1842
- | boolean
1843
- | null
1844
- | undefined
1845
- ? { [P in Prefix]: RemoteFormIssue[] }
1846
- : WillRecurseIndefinitely<T> extends true
1847
- ? { [key: string]: RemoteFormIssue[] }
1848
- : T extends Array<infer U>
1849
- ? { [P in Prefix | `${Prefix}[${number}]`]: RemoteFormIssue[] } & FlattenIssues<
1850
- U,
1851
- `${Prefix}[${number}]`
1852
- >
1853
- : T extends File
1854
- ? { [P in Prefix]: RemoteFormIssue[] }
1855
- : {
1856
- // Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1857
- [K in keyof Required<T>]: FlattenIssues<
1858
- T[K],
1859
- Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1860
- >;
1861
- }[keyof T];
1862
-
1863
- type FlattenKeys<T, Prefix extends string> = T extends string | number | boolean | null | undefined
1864
- ? { [P in Prefix]: string }
1865
- : WillRecurseIndefinitely<T> extends true
1866
- ? { [key: string]: string }
1867
- : T extends Array<infer U>
1868
- ? U extends string | File
1869
- ? { [P in `${Prefix}[]`]: string[] }
1870
- : FlattenKeys<U, `${Prefix}[${number}]`>
1871
- : T extends File
1872
- ? { [P in Prefix]: string }
1873
- : {
1874
- // Required<T> is crucial here to avoid an undefined type to sneak into the union, which would turn the intersection into never
1875
- [K in keyof Required<T>]: FlattenKeys<
1876
- T[K],
1877
- Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1878
- >;
1879
- }[keyof T];
1816
+ // Input type mappings for form fields
1817
+ type InputTypeMap = {
1818
+ text: string;
1819
+ email: string;
1820
+ password: string;
1821
+ url: string;
1822
+ tel: string;
1823
+ search: string;
1824
+ number: number;
1825
+ range: number;
1826
+ date: string;
1827
+ 'datetime-local': string;
1828
+ time: string;
1829
+ month: string;
1830
+ week: string;
1831
+ color: string;
1832
+ checkbox: boolean | string[];
1833
+ radio: string;
1834
+ file: File;
1835
+ hidden: string;
1836
+ submit: string;
1837
+ button: string;
1838
+ reset: string;
1839
+ image: string;
1840
+ select: string;
1841
+ 'select multiple': string[];
1842
+ 'file multiple': File[];
1843
+ };
1844
+
1845
+ // Valid input types for a given value type
1846
+ export type RemoteFormFieldType<T> = {
1847
+ [K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never;
1848
+ }[keyof InputTypeMap];
1849
+
1850
+ // Input element properties based on type
1851
+ type InputElementProps<T extends keyof InputTypeMap> = T extends 'checkbox' | 'radio'
1852
+ ? {
1853
+ type: T;
1854
+ 'aria-invalid': boolean | 'false' | 'true' | undefined;
1855
+ get checked(): boolean;
1856
+ set checked(value: boolean);
1857
+ }
1858
+ : T extends 'file'
1859
+ ? {
1860
+ type: 'file';
1861
+ 'aria-invalid': boolean | 'false' | 'true' | undefined;
1862
+ get files(): FileList | null;
1863
+ set files(v: FileList | null);
1864
+ }
1865
+ : {
1866
+ type: T;
1867
+ 'aria-invalid': boolean | 'false' | 'true' | undefined;
1868
+ get value(): string | number;
1869
+ set value(v: string | number);
1870
+ };
1871
+
1872
+ type RemoteFormFieldMethods<T> = {
1873
+ /** The values that will be submitted */
1874
+ value(): T;
1875
+ /** Set the values that will be submitted */
1876
+ set(input: T): T;
1877
+ /** Validation issues, if any */
1878
+ issues(): RemoteFormIssue[] | undefined;
1879
+ };
1880
+
1881
+ export type RemoteFormFieldValue = string | string[] | number | boolean | File | File[];
1882
+
1883
+ type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
1884
+ ? Value extends string[]
1885
+ ? [type: 'checkbox', value: Value[number] | (string & {})]
1886
+ : [type: Type]
1887
+ : Type extends 'radio'
1888
+ ? [type: 'radio', value: Value | (string & {})]
1889
+ : [type: Type];
1890
+
1891
+ /**
1892
+ * Form field accessor type that provides name(), value(), and issues() methods
1893
+ */
1894
+ export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFieldMethods<Value> & {
1895
+ /**
1896
+ * Returns an object that can be spread onto an input element with the correct type attribute,
1897
+ * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
1898
+ * @example
1899
+ * ```svelte
1900
+ * <input {...myForm.fields.myString.as('text')} />
1901
+ * <input {...myForm.fields.myNumber.as('number')} />
1902
+ * <input {...myForm.fields.myBoolean.as('checkbox')} />
1903
+ * ```
1904
+ */
1905
+ as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T>;
1906
+ };
1907
+
1908
+ type RemoteFormFieldContainer<Value> = RemoteFormFieldMethods<Value> & {
1909
+ /** Validation issues belonging to this or any of the fields that belong to it, if any */
1910
+ allIssues(): RemoteFormIssue[] | undefined;
1911
+ };
1912
+
1913
+ /**
1914
+ * Recursive type to build form fields structure with proxy access
1915
+ */
1916
+ type RemoteFormFields<T> =
1917
+ WillRecurseIndefinitely<T> extends true
1918
+ ? RecursiveFormFields
1919
+ : NonNullable<T> extends string | number | boolean | File
1920
+ ? RemoteFormField<NonNullable<T>>
1921
+ : T extends string[] | File[]
1922
+ ? RemoteFormField<T> & { [K in number]: RemoteFormField<T[number]> }
1923
+ : T extends Array<infer U>
1924
+ ? RemoteFormFieldContainer<T> & { [K in number]: RemoteFormFields<U> }
1925
+ : RemoteFormFieldContainer<T> & { [K in keyof T]-?: RemoteFormFields<T[K]> };
1926
+
1927
+ // By breaking this out into its own type, we avoid the TS recursion depth limit
1928
+ type RecursiveFormFields = RemoteFormField<any> & { [key: string]: RecursiveFormFields };
1929
+
1930
+ type MaybeArray<T> = T | T[];
1880
1931
 
1881
1932
  export interface RemoteFormInput {
1882
- [key: string]: FormDataEntryValue | FormDataEntryValue[] | RemoteFormInput | RemoteFormInput[];
1933
+ [key: string]: MaybeArray<string | number | boolean | File | RemoteFormInput>;
1883
1934
  }
1884
1935
 
1885
1936
  export interface RemoteFormIssue {
1886
- name: string;
1887
- path: Array<string | number>;
1888
1937
  message: string;
1889
1938
  }
1890
1939
 
1940
+ // If the schema specifies `id` as a string or number, ensure that `for(...)`
1941
+ // only accepts that type. Otherwise, accept `string | number`
1942
+ type ExtractId<Input> = Input extends { id: infer Id }
1943
+ ? Id extends string | number
1944
+ ? Id
1945
+ : string | number
1946
+ : string | number;
1947
+
1891
1948
  /**
1892
1949
  * The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
1893
1950
  */
@@ -1912,8 +1969,8 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
1912
1969
  [attachment: symbol]: (node: HTMLFormElement) => void;
1913
1970
  };
1914
1971
  /**
1915
- * Create an instance of the form for the given key.
1916
- * The key is stringified and used for deduplication to potentially reuse existing instances.
1972
+ * Create an instance of the form for the given `id`.
1973
+ * The `id` is stringified and used for deduplication to potentially reuse existing instances.
1917
1974
  * Useful when you have multiple forms that use the same remote form action, for example in a loop.
1918
1975
  * ```svelte
1919
1976
  * {#each todos as todo}
@@ -1925,15 +1982,7 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
1925
1982
  * {/each}
1926
1983
  * ```
1927
1984
  */
1928
- for(key: string | number | boolean): Omit<RemoteForm<Input, Output>, 'for'>;
1929
- /**
1930
- * This method exists to allow you to typecheck `name` attributes. It returns its argument
1931
- * @example
1932
- * ```svelte
1933
- * <input name={login.field('username')} />
1934
- * ```
1935
- **/
1936
- field<Name extends keyof UnionToIntersection<FlattenKeys<Input, ''>>>(string: Name): Name;
1985
+ for(id: ExtractId<Input>): Omit<RemoteForm<Input, Output>, 'for'>;
1937
1986
  /** Preflight checks */
1938
1987
  preflight(schema: StandardSchemaV1<Input, any>): RemoteForm<Input, Output>;
1939
1988
  /** Validate the form contents programmatically */
@@ -1946,10 +1995,8 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
1946
1995
  get result(): Output | undefined;
1947
1996
  /** The number of pending submissions */
1948
1997
  get pending(): number;
1949
- /** The submitted values */
1950
- input: null | UnionToIntersection<FlattenInput<Input, ''>>;
1951
- /** Validation issues */
1952
- issues: null | UnionToIntersection<FlattenIssues<Input, ''>>;
1998
+ /** Access form fields using object notation */
1999
+ fields: Input extends void ? never : RemoteFormFields<Input>;
1953
2000
  /** Spread this onto a `<button>` or `<input type="submit">` */
1954
2001
  buttonProps: {
1955
2002
  type: 'submit';
@@ -1,10 +1,17 @@
1
1
  /** @import { RemoteFormInput, RemoteForm } from '@sveltejs/kit' */
2
- /** @import { MaybePromise, RemoteInfo } from 'types' */
2
+ /** @import { InternalRemoteFormIssue, MaybePromise, RemoteInfo } from 'types' */
3
3
  /** @import { StandardSchemaV1 } from '@standard-schema/spec' */
4
4
  import { get_request_store } from '@sveltejs/kit/internal/server';
5
5
  import { DEV } from 'esm-env';
6
+ import {
7
+ convert_formdata,
8
+ flatten_issues,
9
+ create_field_proxy,
10
+ set_nested_value,
11
+ throw_on_old_property_access,
12
+ deep_set
13
+ } from '../../../form-utils.svelte.js';
6
14
  import { get_cache, run_remote_function } from './shared.js';
7
- import { convert_formdata, flatten_issues } from '../../../utils.js';
8
15
 
9
16
  /**
10
17
  * Creates a form object that can be spread onto a `<form>` element.
@@ -98,10 +105,16 @@ export function form(validate_or_fn, maybe_fn) {
98
105
  /** @param {FormData} form_data */
99
106
  fn: async (form_data) => {
100
107
  const validate_only = form_data.get('sveltekit:validate_only') === 'true';
101
- form_data.delete('sveltekit:validate_only');
102
108
 
103
109
  let data = maybe_fn ? convert_formdata(form_data) : undefined;
104
110
 
111
+ if (data && data.id === undefined) {
112
+ const id = form_data.get('sveltekit:id');
113
+ if (typeof id === 'string') {
114
+ data.id = JSON.parse(id);
115
+ }
116
+ }
117
+
105
118
  // TODO 3.0 remove this warning
106
119
  if (DEV && !data) {
107
120
  const error = () => {
@@ -128,7 +141,7 @@ export function form(validate_or_fn, maybe_fn) {
128
141
  }
129
142
  }
130
143
 
131
- /** @type {{ input?: Record<string, string | string[]>, issues?: Record<string, StandardSchemaV1.Issue[]>, result: Output }} */
144
+ /** @type {{ input?: Record<string, any>, issues?: Record<string, InternalRemoteFormIssue[]>, result: Output }} */
132
145
  const output = {};
133
146
 
134
147
  const { event, state } = get_request_store();
@@ -140,18 +153,27 @@ export function form(validate_or_fn, maybe_fn) {
140
153
 
141
154
  if (validated?.issues !== undefined) {
142
155
  output.issues = flatten_issues(validated.issues);
143
- output.input = {};
144
156
 
145
- for (let key of form_data.keys()) {
146
- // redact sensitive fields
147
- if (/^[.\]]?_/.test(key)) continue;
157
+ // if it was a progressively-enhanced submission, we don't need
158
+ // to return the input — it's already there
159
+ if (!event.isRemoteRequest) {
160
+ output.input = {};
161
+
162
+ for (let key of form_data.keys()) {
163
+ // redact sensitive fields
164
+ if (/^[.\]]?_/.test(key)) continue;
148
165
 
149
- const is_array = key.endsWith('[]');
150
- const values = form_data.getAll(key).filter((value) => typeof value === 'string');
166
+ const is_array = key.endsWith('[]');
167
+ const values = form_data.getAll(key).filter((value) => typeof value === 'string');
151
168
 
152
- if (is_array) key = key.slice(0, -2);
169
+ if (is_array) key = key.slice(0, -2);
153
170
 
154
- output.input[key] = is_array ? values : values[0];
171
+ output.input = set_nested_value(
172
+ /** @type {Record<string, any>} */ (output.input),
173
+ key,
174
+ is_array ? values : values[0]
175
+ );
176
+ }
155
177
  }
156
178
  } else {
157
179
  if (validated !== undefined) {
@@ -185,16 +207,30 @@ export function form(validate_or_fn, maybe_fn) {
185
207
  enumerable: true
186
208
  });
187
209
 
188
- for (const property of ['input', 'issues']) {
189
- Object.defineProperty(instance, property, {
190
- get() {
191
- try {
192
- return get_cache(__)?.['']?.[property] ?? {};
193
- } catch {
194
- return undefined;
195
- }
196
- }
197
- });
210
+ Object.defineProperty(instance, 'fields', {
211
+ get() {
212
+ const data = get_cache(__)?.[''];
213
+ return create_field_proxy(
214
+ {},
215
+ () => data?.input ?? {},
216
+ (path, value) => {
217
+ if (data) {
218
+ // don't override a submission
219
+ return;
220
+ }
221
+
222
+ const input = path.length === 0 ? value : deep_set({}, path.map(String), value);
223
+
224
+ get_cache(__)[''] ??= { input };
225
+ },
226
+ () => data?.issues ?? {}
227
+ );
228
+ }
229
+ });
230
+
231
+ // TODO 3.0 remove
232
+ if (DEV) {
233
+ throw_on_old_property_access(instance);
198
234
  }
199
235
 
200
236
  Object.defineProperty(instance, 'result', {
@@ -217,10 +253,6 @@ export function form(validate_or_fn, maybe_fn) {
217
253
  get: () => 0
218
254
  });
219
255
 
220
- Object.defineProperty(instance, 'field', {
221
- value: (/** @type {string} */ name) => name
222
- });
223
-
224
256
  Object.defineProperty(instance, 'preflight', {
225
257
  // preflight is a noop on the server
226
258
  value: () => instance
@@ -1,5 +1,5 @@
1
1
  /** @import { RequestEvent } from '@sveltejs/kit' */
2
- /** @import { ServerHooks, MaybePromise, RequestState, RemoteInfo } from 'types' */
2
+ /** @import { ServerHooks, MaybePromise, RequestState, RemoteInfo, RequestStore } from 'types' */
3
3
  import { parse } from 'devalue';
4
4
  import { error } from '@sveltejs/kit';
5
5
  import { with_request_store, get_request_store } from '@sveltejs/kit/internal/server';
@@ -103,44 +103,48 @@ export function parse_remote_response(data, transport) {
103
103
  * @param {(arg?: any) => T} fn
104
104
  */
105
105
  export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) {
106
- /** @type {RequestEvent} */
107
- const cleansed = {
108
- ...event,
109
- setHeaders: () => {
110
- throw new Error('setHeaders is not allowed in remote functions');
111
- },
112
- cookies: {
113
- ...event.cookies,
114
- set: (name, value, opts) => {
115
- if (!allow_cookies) {
116
- throw new Error('Cannot set cookies in `query` or `prerender` functions');
117
- }
118
-
119
- if (opts.path && !opts.path.startsWith('/')) {
120
- throw new Error('Cookies set in remote functions must have an absolute path');
121
- }
122
-
123
- return event.cookies.set(name, value, opts);
106
+ /** @type {RequestStore} */
107
+ const store = {
108
+ event: {
109
+ ...event,
110
+ setHeaders: () => {
111
+ throw new Error('setHeaders is not allowed in remote functions');
124
112
  },
125
- delete: (name, opts) => {
126
- if (!allow_cookies) {
127
- throw new Error('Cannot delete cookies in `query` or `prerender` functions');
128
- }
129
-
130
- if (opts.path && !opts.path.startsWith('/')) {
131
- throw new Error('Cookies deleted in remote functions must have an absolute path');
113
+ cookies: {
114
+ ...event.cookies,
115
+ set: (name, value, opts) => {
116
+ if (!allow_cookies) {
117
+ throw new Error('Cannot set cookies in `query` or `prerender` functions');
118
+ }
119
+
120
+ if (opts.path && !opts.path.startsWith('/')) {
121
+ throw new Error('Cookies set in remote functions must have an absolute path');
122
+ }
123
+
124
+ return event.cookies.set(name, value, opts);
125
+ },
126
+ delete: (name, opts) => {
127
+ if (!allow_cookies) {
128
+ throw new Error('Cannot delete cookies in `query` or `prerender` functions');
129
+ }
130
+
131
+ if (opts.path && !opts.path.startsWith('/')) {
132
+ throw new Error('Cookies deleted in remote functions must have an absolute path');
133
+ }
134
+
135
+ return event.cookies.delete(name, opts);
132
136
  }
133
-
134
- return event.cookies.delete(name, opts);
135
137
  }
136
138
  },
137
- route: { id: null },
138
- url: new URL(event.url.origin)
139
+ state: {
140
+ ...state,
141
+ is_in_remote_function: true
142
+ }
139
143
  };
140
144
 
141
145
  // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function
142
- const validated = await with_request_store({ event: cleansed, state }, () => validate(arg));
143
- return with_request_store({ event: cleansed, state }, () => fn(validated));
146
+ const validated = await with_request_store(store, () => validate(arg));
147
+ return with_request_store(store, () => fn(validated));
144
148
  }
145
149
 
146
150
  /**
@@ -40,7 +40,9 @@ export function command(id) {
40
40
  refreshes: updates.map((u) => u._key)
41
41
  }),
42
42
  headers: {
43
- 'Content-Type': 'application/json'
43
+ 'Content-Type': 'application/json',
44
+ 'x-sveltekit-pathname': location.pathname,
45
+ 'x-sveltekit-search': location.search
44
46
  }
45
47
  });
46
48