@sveltejs/kit 2.40.0 → 2.42.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.40.0",
3
+ "version": "2.42.0",
4
4
  "description": "SvelteKit is the fastest way to build Svelte apps",
5
5
  "keywords": [
6
6
  "framework",
@@ -7,6 +7,7 @@ import { load_error_page, load_template } from '../config/index.js';
7
7
  import { runtime_directory } from '../utils.js';
8
8
  import { isSvelte5Plus, write_if_changed } from './utils.js';
9
9
  import colors from 'kleur';
10
+ import { escape_html } from '../../utils/escape.js';
10
11
 
11
12
  /**
12
13
  * @param {{
@@ -54,6 +55,7 @@ export const options = {
54
55
  .replace('%sveltekit.body%', '" + body + "')
55
56
  .replace(/%sveltekit\.assets%/g, '" + assets + "')
56
57
  .replace(/%sveltekit\.nonce%/g, '" + nonce + "')
58
+ .replace(/%sveltekit\.version%/g, escape_html(config.kit.version.name))
57
59
  .replace(
58
60
  /%sveltekit\.env\.([^%]+)%/g,
59
61
  (_match, capture) => `" + (env[${s(capture)}] ?? "") + "`
@@ -1810,28 +1810,105 @@ export interface Snapshot<T = any> {
1810
1810
  restore: (snapshot: T) => void;
1811
1811
  }
1812
1812
 
1813
+ // If T is unknown or RemoteFormInput, the types below will recurse indefinitely and create giant unions that TS can't handle
1814
+ type WillRecurseIndefinitely<T> = unknown extends T
1815
+ ? true
1816
+ : RemoteFormInput extends T
1817
+ ? true
1818
+ : false;
1819
+
1820
+ // Helper type to convert union to intersection
1821
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
1822
+ ? I
1823
+ : never;
1824
+
1825
+ type FlattenInput<T, Prefix extends string> =
1826
+ WillRecurseIndefinitely<T> extends true
1827
+ ? { [key: string]: string }
1828
+ : T extends Array<infer U>
1829
+ ? U extends string | File
1830
+ ? { [P in Prefix]: string[] }
1831
+ : FlattenInput<U, `${Prefix}[${number}]`>
1832
+ : T extends File
1833
+ ? { [P in Prefix]: string }
1834
+ : T extends object
1835
+ ? {
1836
+ [K in keyof T]: FlattenInput<
1837
+ T[K],
1838
+ Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1839
+ >;
1840
+ }[keyof T]
1841
+ : { [P in Prefix]: string };
1842
+
1843
+ type FlattenIssues<T, Prefix extends string> =
1844
+ WillRecurseIndefinitely<T> extends true
1845
+ ? { [key: string]: RemoteFormIssue[] }
1846
+ : T extends Array<infer U>
1847
+ ? { [P in Prefix | `${Prefix}[${number}]`]: RemoteFormIssue[] } & FlattenIssues<
1848
+ U,
1849
+ `${Prefix}[${number}]`
1850
+ >
1851
+ : T extends File
1852
+ ? { [P in Prefix]: RemoteFormIssue[] }
1853
+ : T extends object
1854
+ ? {
1855
+ [K in keyof T]: FlattenIssues<
1856
+ T[K],
1857
+ Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1858
+ >;
1859
+ }[keyof T]
1860
+ : { [P in Prefix]: RemoteFormIssue[] };
1861
+
1862
+ type FlattenKeys<T, Prefix extends string> =
1863
+ WillRecurseIndefinitely<T> extends true
1864
+ ? { [key: string]: string }
1865
+ : T extends Array<infer U>
1866
+ ? U extends string | File
1867
+ ? { [P in `${Prefix}[]`]: string[] }
1868
+ : FlattenKeys<U, `${Prefix}[${number}]`>
1869
+ : T extends File
1870
+ ? { [P in Prefix]: string }
1871
+ : T extends object
1872
+ ? {
1873
+ [K in keyof T]: FlattenKeys<
1874
+ T[K],
1875
+ Prefix extends '' ? K & string : `${Prefix}.${K & string}`
1876
+ >;
1877
+ }[keyof T]
1878
+ : { [P in Prefix]: string };
1879
+
1880
+ export interface RemoteFormInput {
1881
+ [key: string]: FormDataEntryValue | FormDataEntryValue[] | RemoteFormInput | RemoteFormInput[];
1882
+ }
1883
+
1884
+ export interface RemoteFormIssue {
1885
+ name: string;
1886
+ path: Array<string | number>;
1887
+ message: string;
1888
+ }
1889
+
1813
1890
  /**
1814
1891
  * The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
1815
1892
  */
1816
- export type RemoteForm<Result> = {
1893
+ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
1894
+ /** Attachment that sets up an event handler that intercepts the form submission on the client to prevent a full page reload */
1895
+ [attachment: symbol]: (node: HTMLFormElement) => void;
1817
1896
  method: 'POST';
1818
1897
  /** The URL to send the form to. */
1819
1898
  action: string;
1820
- /** Event handler that intercepts the form submission on the client to prevent a full page reload */
1821
- onsubmit: (event: SubmitEvent) => void;
1822
1899
  /** Use the `enhance` method to influence what happens when the form is submitted. */
1823
1900
  enhance(
1824
1901
  callback: (opts: {
1825
1902
  form: HTMLFormElement;
1826
- data: FormData;
1903
+ data: Input;
1827
1904
  submit: () => Promise<void> & {
1828
1905
  updates: (...queries: Array<RemoteQuery<any> | RemoteQueryOverride>) => Promise<void>;
1829
1906
  };
1830
- }) => void
1907
+ }) => void | Promise<void>
1831
1908
  ): {
1832
1909
  method: 'POST';
1833
1910
  action: string;
1834
- onsubmit: (event: SubmitEvent) => void;
1911
+ [attachment: symbol]: (node: HTMLFormElement) => void;
1835
1912
  };
1836
1913
  /**
1837
1914
  * Create an instance of the form for the given key.
@@ -1847,11 +1924,27 @@ export type RemoteForm<Result> = {
1847
1924
  * {/each}
1848
1925
  * ```
1849
1926
  */
1850
- for(key: string | number | boolean): Omit<RemoteForm<Result>, 'for'>;
1927
+ for(key: string | number | boolean): Omit<RemoteForm<Input, Output>, 'for'>;
1928
+ /**
1929
+ * This method exists to allow you to typecheck `name` attributes. It returns its argument
1930
+ * @example
1931
+ * ```svelte
1932
+ * <input name={login.field('username')} />
1933
+ * ```
1934
+ **/
1935
+ field<Name extends keyof UnionToIntersection<FlattenKeys<Input, ''>>>(string: Name): Name;
1936
+ /** Preflight checks */
1937
+ preflight(schema: StandardSchemaV1<Input, any>): RemoteForm<Input, Output>;
1938
+ /** Validate the form contents programmatically */
1939
+ validate(options?: { includeUntouched?: boolean }): Promise<void>;
1851
1940
  /** The result of the form submission */
1852
- get result(): Result | undefined;
1941
+ get result(): Output | undefined;
1853
1942
  /** The number of pending submissions */
1854
1943
  get pending(): number;
1944
+ /** The submitted values */
1945
+ input: null | UnionToIntersection<FlattenInput<Input, ''>>;
1946
+ /** Validation issues */
1947
+ issues: null | UnionToIntersection<FlattenIssues<Input, ''>>;
1855
1948
  /** Spread this onto a `<button>` or `<input type="submit">` */
1856
1949
  buttonProps: {
1857
1950
  type: 'submit';
@@ -1862,11 +1955,11 @@ export type RemoteForm<Result> = {
1862
1955
  enhance(
1863
1956
  callback: (opts: {
1864
1957
  form: HTMLFormElement;
1865
- data: FormData;
1958
+ data: Input;
1866
1959
  submit: () => Promise<void> & {
1867
1960
  updates: (...queries: Array<RemoteQuery<any> | RemoteQueryOverride>) => Promise<void>;
1868
1961
  };
1869
- }) => void
1962
+ }) => void | Promise<void>
1870
1963
  ): {
1871
1964
  type: 'submit';
1872
1965
  formmethod: 'POST';
@@ -1,34 +1,77 @@
1
- /** @import { RemoteForm } from '@sveltejs/kit' */
2
- /** @import { RemoteInfo, MaybePromise } from 'types' */
1
+ /** @import { RemoteFormInput, RemoteForm } from '@sveltejs/kit' */
2
+ /** @import { MaybePromise, RemoteInfo } from 'types' */
3
+ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */
3
4
  import { get_request_store } from '@sveltejs/kit/internal/server';
5
+ import { DEV } from 'esm-env';
4
6
  import { run_remote_function } from './shared.js';
7
+ import { convert_formdata, flatten_issues } from '../../../utils.js';
5
8
 
6
9
  /**
7
10
  * Creates a form object that can be spread onto a `<form>` element.
8
11
  *
9
12
  * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
10
13
  *
11
- * @template T
12
- * @param {(data: FormData) => MaybePromise<T>} fn
13
- * @returns {RemoteForm<T>}
14
+ * @template Output
15
+ * @overload
16
+ * @param {() => Output} fn
17
+ * @returns {RemoteForm<void, Output>}
18
+ * @since 2.27
19
+ */
20
+ /**
21
+ * Creates a form object that can be spread onto a `<form>` element.
22
+ *
23
+ * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
24
+ *
25
+ * @template {RemoteFormInput} Input
26
+ * @template Output
27
+ * @overload
28
+ * @param {'unchecked'} validate
29
+ * @param {(data: Input) => MaybePromise<Output>} fn
30
+ * @returns {RemoteForm<Input, Output>}
31
+ * @since 2.27
32
+ */
33
+ /**
34
+ * Creates a form object that can be spread onto a `<form>` element.
35
+ *
36
+ * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
37
+ *
38
+ * @template {StandardSchemaV1<RemoteFormInput, Record<string, any>>} Schema
39
+ * @template Output
40
+ * @overload
41
+ * @param {Schema} validate
42
+ * @param {(data: StandardSchemaV1.InferOutput<Schema>) => MaybePromise<Output>} fn
43
+ * @returns {RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>}
44
+ * @since 2.27
45
+ */
46
+ /**
47
+ * @template {RemoteFormInput} Input
48
+ * @template Output
49
+ * @param {any} validate_or_fn
50
+ * @param {(data?: Input) => MaybePromise<Output>} [maybe_fn]
51
+ * @returns {RemoteForm<Input, Output>}
14
52
  * @since 2.27
15
53
  */
16
54
  /*@__NO_SIDE_EFFECTS__*/
17
55
  // @ts-ignore we don't want to prefix `fn` with an underscore, as that will be user-visible
18
- export function form(fn) {
56
+ export function form(validate_or_fn, maybe_fn) {
57
+ /** @type {(data?: Input) => Output} */
58
+ const fn = maybe_fn ?? validate_or_fn;
59
+
60
+ /** @type {StandardSchemaV1 | null} */
61
+ const schema = !maybe_fn || validate_or_fn === 'unchecked' ? null : validate_or_fn;
62
+
19
63
  /**
20
64
  * @param {string | number | boolean} [key]
21
65
  */
22
66
  function create_instance(key) {
23
- /** @type {RemoteForm<T>} */
67
+ /** @type {RemoteForm<Input, Output>} */
24
68
  const instance = {};
25
69
 
26
70
  instance.method = 'POST';
27
- instance.onsubmit = () => {};
28
71
 
29
72
  Object.defineProperty(instance, 'enhance', {
30
73
  value: () => {
31
- return { action: instance.action, method: instance.method, onsubmit: instance.onsubmit };
74
+ return { action: instance.action, method: instance.method };
32
75
  }
33
76
  });
34
77
 
@@ -54,19 +97,79 @@ export function form(fn) {
54
97
  id: '',
55
98
  /** @param {FormData} form_data */
56
99
  fn: async (form_data) => {
100
+ const validate_only = form_data.get('sveltekit:validate_only') === 'true';
101
+ form_data.delete('sveltekit:validate_only');
102
+
103
+ let data = maybe_fn ? convert_formdata(form_data) : undefined;
104
+
105
+ // TODO 3.0 remove this warning
106
+ if (DEV && !data) {
107
+ const error = () => {
108
+ throw new Error(
109
+ 'Remote form functions no longer get passed a FormData object. ' +
110
+ "`form` now has the same signature as `query` or `command`, i.e. it expects to be invoked like `form(schema, callback)` or `form('unchecked', callback)`. " +
111
+ 'The payload of the callback function is now a POJO instead of a FormData object. See https://kit.svelte.dev/docs/remote-functions#form for details.'
112
+ );
113
+ };
114
+ data = {};
115
+ for (const key of [
116
+ 'append',
117
+ 'delete',
118
+ 'entries',
119
+ 'forEach',
120
+ 'get',
121
+ 'getAll',
122
+ 'has',
123
+ 'keys',
124
+ 'set',
125
+ 'values'
126
+ ]) {
127
+ Object.defineProperty(data, key, { get: error });
128
+ }
129
+ }
130
+
131
+ /** @type {{ input?: Record<string, string | string[]>, issues?: Record<string, StandardSchemaV1.Issue[]>, result: Output }} */
132
+ const output = {};
133
+
57
134
  const { event, state } = get_request_store();
135
+ const validated = await schema?.['~standard'].validate(data);
136
+
137
+ if (validate_only) {
138
+ return validated?.issues ?? [];
139
+ }
140
+
141
+ if (validated?.issues !== undefined) {
142
+ output.issues = flatten_issues(validated.issues);
143
+ output.input = {};
144
+
145
+ for (let key of form_data.keys()) {
146
+ // redact sensitive fields
147
+ if (/^[.\]]?_/.test(key)) continue;
148
+
149
+ const is_array = key.endsWith('[]');
150
+ const values = form_data.getAll(key).filter((value) => typeof value === 'string');
58
151
 
59
- state.refreshes ??= {};
152
+ if (is_array) key = key.slice(0, -2);
60
153
 
61
- const result = await run_remote_function(event, state, true, form_data, (d) => d, fn);
154
+ output.input[key] = is_array ? values : values[0];
155
+ }
156
+ } else {
157
+ if (validated !== undefined) {
158
+ data = validated.value;
159
+ }
160
+
161
+ state.refreshes ??= {};
162
+
163
+ output.result = await run_remote_function(event, state, true, data, (d) => d, fn);
164
+ }
62
165
 
63
166
  // We don't need to care about args or deduplicating calls, because uneval results are only relevant in full page reloads
64
167
  // where only one form submission is active at the same time
65
168
  if (!event.isRemoteRequest) {
66
- (state.remote_data ??= {})[__.id] = result;
169
+ (state.remote_data ??= {})[__.id] = output;
67
170
  }
68
171
 
69
- return result;
172
+ return output;
70
173
  }
71
174
  };
72
175
 
@@ -82,11 +185,24 @@ export function form(fn) {
82
185
  enumerable: true
83
186
  });
84
187
 
188
+ for (const property of ['input', 'issues']) {
189
+ Object.defineProperty(instance, property, {
190
+ get() {
191
+ try {
192
+ const { remote_data } = get_request_store().state;
193
+ return remote_data?.[__.id]?.[property] ?? {};
194
+ } catch {
195
+ return undefined;
196
+ }
197
+ }
198
+ });
199
+ }
200
+
85
201
  Object.defineProperty(instance, 'result', {
86
202
  get() {
87
203
  try {
88
204
  const { remote_data } = get_request_store().state;
89
- return remote_data?.[__.id];
205
+ return remote_data?.[__.id]?.result;
90
206
  } catch {
91
207
  return undefined;
92
208
  }
@@ -103,9 +219,24 @@ export function form(fn) {
103
219
  get: () => 0
104
220
  });
105
221
 
222
+ Object.defineProperty(instance, 'field', {
223
+ value: (/** @type {string} */ name) => name
224
+ });
225
+
226
+ Object.defineProperty(instance, 'preflight', {
227
+ // preflight is a noop on the server
228
+ value: () => instance
229
+ });
230
+
231
+ Object.defineProperty(instance, 'validate', {
232
+ value: () => {
233
+ throw new Error('Cannot call validate() on the server');
234
+ }
235
+ });
236
+
106
237
  if (key == undefined) {
107
238
  Object.defineProperty(instance, 'for', {
108
- /** @type {RemoteForm<any>['for']} */
239
+ /** @type {RemoteForm<any, any>['for']} */
109
240
  value: (key) => {
110
241
  const { state } = get_request_store();
111
242
  const cache_key = __.id + '|' + JSON.stringify(key);
@@ -190,10 +190,7 @@ let target;
190
190
  export let app;
191
191
 
192
192
  /** @type {Record<string, any>} */
193
- // we have to conditionally access the properties of `__SVELTEKIT_PAYLOAD__`
194
- // because it will be `undefined` when users import the exports from this module.
195
- // It's only defined when the server renders a page.
196
- export const remote_responses = __SVELTEKIT_PAYLOAD__?.data ?? {};
193
+ export let remote_responses = {};
197
194
 
198
195
  /** @type {Array<((url: URL) => boolean)>} */
199
196
  const invalidated = [];
@@ -294,6 +291,10 @@ export async function start(_app, _target, hydrate) {
294
291
  );
295
292
  }
296
293
 
294
+ if (__SVELTEKIT_PAYLOAD__.data) {
295
+ remote_responses = __SVELTEKIT_PAYLOAD__?.data;
296
+ }
297
+
297
298
  // detect basic auth credentials in the current URL
298
299
  // https://github.com/sveltejs/kit/pull/11179
299
300
  // if so, refresh the page without credentials
@@ -460,7 +461,7 @@ export async function _goto(url, options, redirect_count, nav_token) {
460
461
  load_cache = null;
461
462
  }
462
463
 
463
- const result = await navigate({
464
+ await navigate({
464
465
  type: 'goto',
465
466
  url: resolve_url(url),
466
467
  keepfocus: options.keepFocus,
@@ -480,6 +481,7 @@ export async function _goto(url, options, redirect_count, nav_token) {
480
481
  }
481
482
  }
482
483
  });
484
+
483
485
  if (options.invalidateAll) {
484
486
  // TODO the ticks shouldn't be necessary, something inside Svelte itself is buggy
485
487
  // when a query in a layout that still exists after page change is refreshed earlier than this
@@ -495,7 +497,6 @@ export async function _goto(url, options, redirect_count, nav_token) {
495
497
  });
496
498
  });
497
499
  }
498
- return result;
499
500
  }
500
501
 
501
502
  /** @param {import('./types.js').NavigationIntent} intent */
@@ -1282,6 +1283,7 @@ async function load_root_error_page({ status, error, url, route }) {
1282
1283
  });
1283
1284
  } catch (error) {
1284
1285
  if (error instanceof Redirect) {
1286
+ // @ts-expect-error TODO investigate this
1285
1287
  return _goto(new URL(error.location, location.href), {}, 0);
1286
1288
  }
1287
1289
 
@@ -1576,7 +1578,7 @@ async function navigate({
1576
1578
  if (navigation_result.type === 'redirect') {
1577
1579
  // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
1578
1580
  if (redirect_count < 20) {
1579
- return navigate({
1581
+ await navigate({
1580
1582
  type,
1581
1583
  url: new URL(navigation_result.location, url),
1582
1584
  popped,
@@ -1587,6 +1589,9 @@ async function navigate({
1587
1589
  redirect_count: redirect_count + 1,
1588
1590
  nav_token
1589
1591
  });
1592
+
1593
+ nav.fulfil(undefined);
1594
+ return;
1590
1595
  }
1591
1596
 
1592
1597
  navigation_result = await load_root_error_page({