@sveltejs/kit 3.0.0-next.2 → 3.0.0-next.21

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.
Files changed (195) hide show
  1. package/package.json +62 -36
  2. package/src/cli.js +15 -12
  3. package/src/constants.js +6 -1
  4. package/src/core/adapt/builder.js +66 -72
  5. package/src/core/adapt/index.js +17 -6
  6. package/src/core/config/index.js +134 -80
  7. package/src/core/config/options.js +295 -273
  8. package/src/core/config/types.d.ts +1 -1
  9. package/src/core/env.js +146 -16
  10. package/src/core/generate_manifest/index.js +18 -15
  11. package/src/core/postbuild/analyse.js +8 -20
  12. package/src/core/postbuild/crawl.js +22 -6
  13. package/src/core/postbuild/entities.js +8 -2
  14. package/src/core/postbuild/fallback.js +4 -2
  15. package/src/core/postbuild/prerender.js +222 -70
  16. package/src/core/postbuild/queue.js +2 -1
  17. package/src/core/sync/create_manifest_data/conflict.js +1 -1
  18. package/src/core/sync/create_manifest_data/index.js +82 -62
  19. package/src/core/sync/sync.js +40 -31
  20. package/src/core/sync/ts.js +1 -1
  21. package/src/core/sync/utils.js +22 -2
  22. package/src/core/sync/{write_non_ambient.js → write_app_types.js} +107 -58
  23. package/src/core/sync/write_client_manifest.js +14 -29
  24. package/src/core/sync/write_env.js +7 -5
  25. package/src/core/sync/write_server.js +37 -45
  26. package/src/core/sync/write_tsconfig/index.js +274 -0
  27. package/src/core/sync/write_tsconfig/test-app/package.json +7 -0
  28. package/src/core/sync/write_tsconfig/utils.js +77 -0
  29. package/src/core/sync/write_tsconfig/validate.js +128 -0
  30. package/src/core/sync/write_types/index.js +127 -123
  31. package/src/core/utils.js +30 -5
  32. package/src/exports/env/index.js +77 -0
  33. package/src/exports/env/public.d.ts +55 -0
  34. package/src/exports/hooks/index.js +3 -9
  35. package/src/exports/hooks/public.d.ts +195 -0
  36. package/src/exports/hooks/sequence.js +9 -6
  37. package/src/exports/index.js +52 -29
  38. package/src/exports/internal/client.js +5 -0
  39. package/src/exports/internal/env.js +8 -5
  40. package/src/exports/internal/index.js +1 -90
  41. package/src/exports/internal/{event.js → server/event.js} +2 -3
  42. package/src/exports/internal/server/index.js +37 -0
  43. package/src/exports/internal/server/telemetry.js +95 -0
  44. package/src/exports/internal/shared.js +90 -0
  45. package/src/exports/node/index.js +64 -22
  46. package/src/exports/params/index.js +70 -0
  47. package/src/exports/params/public.d.ts +63 -0
  48. package/src/exports/public.d.ts +50 -1733
  49. package/src/exports/remote/index.js +11 -0
  50. package/src/exports/remote/public.d.ts +519 -0
  51. package/src/exports/url.js +86 -0
  52. package/src/exports/vite/build/build_server.js +54 -65
  53. package/src/exports/vite/build/remote.js +24 -19
  54. package/src/exports/vite/build/utils.js +0 -8
  55. package/src/exports/vite/dev/index.js +218 -146
  56. package/src/exports/vite/index.js +1590 -858
  57. package/src/exports/vite/module_ids.js +2 -2
  58. package/src/exports/vite/preview/index.js +40 -32
  59. package/src/exports/vite/public.d.ts +588 -0
  60. package/src/exports/vite/utils.js +84 -46
  61. package/src/pathname.js +55 -0
  62. package/src/runner.js +15 -0
  63. package/src/runtime/app/env/internal.js +4 -4
  64. package/src/runtime/app/env/types.d.ts +1 -1
  65. package/src/runtime/app/environment/index.js +3 -3
  66. package/src/runtime/app/{forms.js → forms/index.js} +73 -42
  67. package/src/runtime/app/forms/public.d.ts +2 -0
  68. package/src/runtime/app/forms/types.d.ts +56 -0
  69. package/src/runtime/app/internal/transport.js +53 -0
  70. package/src/runtime/app/manifest/index.js +1 -0
  71. package/src/runtime/app/{navigation.js → navigation/index.js} +2 -1
  72. package/src/runtime/app/navigation/public.d.ts +237 -0
  73. package/src/runtime/app/paths/client.js +37 -37
  74. package/src/runtime/app/paths/index.js +1 -1
  75. package/src/runtime/app/paths/internal/client.js +34 -2
  76. package/src/runtime/app/paths/internal/server.js +6 -23
  77. package/src/runtime/app/paths/internal.d.ts +3 -0
  78. package/src/runtime/app/paths/public.d.ts +1 -29
  79. package/src/runtime/app/paths/server.js +36 -17
  80. package/src/runtime/app/paths/types.d.ts +11 -19
  81. package/src/runtime/app/server/index.js +2 -2
  82. package/src/runtime/app/server/public.d.ts +201 -0
  83. package/src/runtime/app/server/remote/command.js +13 -11
  84. package/src/runtime/app/server/remote/form.js +61 -39
  85. package/src/runtime/app/server/remote/prerender.js +45 -45
  86. package/src/runtime/app/server/remote/query.js +106 -108
  87. package/src/runtime/app/server/remote/requested.js +27 -19
  88. package/src/runtime/app/server/remote/shared.js +76 -76
  89. package/src/runtime/app/service-worker/index.js +24 -0
  90. package/src/runtime/app/state/client.js +4 -2
  91. package/src/runtime/app/state/index.js +7 -5
  92. package/src/runtime/app/state/public.d.ts +72 -0
  93. package/src/runtime/app/state/server.js +3 -0
  94. package/src/runtime/app/stores.js +15 -78
  95. package/src/runtime/client/bundle.js +1 -1
  96. package/src/runtime/client/client-entry.js +3 -0
  97. package/src/runtime/client/client.js +1336 -648
  98. package/src/runtime/client/constants.js +3 -6
  99. package/src/runtime/client/entry.js +24 -3
  100. package/src/runtime/client/fetcher.js +25 -25
  101. package/src/runtime/client/ndjson.js +1 -1
  102. package/src/runtime/client/parse.js +1 -1
  103. package/src/runtime/client/payload.js +17 -0
  104. package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
  105. package/src/runtime/client/remote-functions/command.svelte.js +37 -61
  106. package/src/runtime/client/remote-functions/form.svelte.js +267 -178
  107. package/src/runtime/client/remote-functions/prerender.svelte.js +32 -11
  108. package/src/runtime/client/remote-functions/query/index.js +10 -17
  109. package/src/runtime/client/remote-functions/query/instance.svelte.js +64 -18
  110. package/src/runtime/client/remote-functions/query/proxy.js +5 -5
  111. package/src/runtime/client/remote-functions/query-batch.svelte.js +61 -70
  112. package/src/runtime/client/remote-functions/query-live/index.js +1 -1
  113. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +63 -21
  114. package/src/runtime/client/remote-functions/query-live/iterator.js +15 -12
  115. package/src/runtime/client/remote-functions/query-live/proxy.js +2 -12
  116. package/src/runtime/client/remote-functions/shared.svelte.js +90 -65
  117. package/src/runtime/client/snapshots.js +147 -0
  118. package/src/runtime/client/state.svelte.js +94 -55
  119. package/src/runtime/client/stream.js +3 -2
  120. package/src/runtime/client/types.d.ts +13 -9
  121. package/src/runtime/client/utils.js +22 -110
  122. package/src/runtime/components/root.svelte +56 -0
  123. package/src/runtime/env/dynamic/private.js +7 -0
  124. package/src/runtime/env/dynamic/public.js +7 -0
  125. package/src/runtime/env/static/private.js +6 -0
  126. package/src/runtime/env/static/public.js +6 -0
  127. package/src/runtime/form-utils.js +172 -61
  128. package/src/runtime/pathname.js +20 -32
  129. package/src/runtime/props.svelte.js +72 -0
  130. package/src/runtime/server/constants.js +0 -3
  131. package/src/runtime/server/cookie.js +72 -55
  132. package/src/runtime/server/csrf.js +65 -0
  133. package/src/runtime/server/data/index.js +49 -53
  134. package/src/runtime/server/dev.js +22 -0
  135. package/src/runtime/server/endpoint.js +6 -7
  136. package/src/runtime/server/env_module.js +0 -5
  137. package/src/runtime/server/errors.js +160 -0
  138. package/src/runtime/server/fetch.js +32 -39
  139. package/src/runtime/server/index.js +90 -64
  140. package/src/runtime/server/internal.js +71 -0
  141. package/src/runtime/server/page/actions.js +85 -64
  142. package/src/runtime/server/page/crypto.js +2 -2
  143. package/src/runtime/server/page/csp.js +88 -104
  144. package/src/runtime/server/page/data_serializer.js +24 -25
  145. package/src/runtime/server/page/index.js +36 -53
  146. package/src/runtime/server/page/load_data.js +50 -57
  147. package/src/runtime/server/page/render.js +173 -239
  148. package/src/runtime/server/page/respond_with_error.js +17 -31
  149. package/src/runtime/server/page/serialize_data.js +2 -13
  150. package/src/runtime/server/page/server_routing.js +85 -26
  151. package/src/runtime/server/remote-functions.js +640 -0
  152. package/src/runtime/server/respond.js +190 -131
  153. package/src/runtime/server/sourcemaps.js +183 -0
  154. package/src/runtime/server/state.js +53 -0
  155. package/src/runtime/server/utils.js +13 -155
  156. package/src/runtime/shared.js +20 -40
  157. package/src/runtime/utils.js +3 -0
  158. package/src/types/ambient-private.d.ts +11 -1
  159. package/src/types/ambient.d.ts +87 -36
  160. package/src/types/global-private.d.ts +25 -25
  161. package/src/types/internal.d.ts +163 -157
  162. package/src/types/private.d.ts +41 -1
  163. package/src/utils/error.js +28 -4
  164. package/src/utils/escape.js +9 -25
  165. package/src/utils/features.js +1 -1
  166. package/src/utils/filesystem.js +1 -23
  167. package/src/utils/fork.js +7 -2
  168. package/src/utils/hash.js +21 -0
  169. package/src/utils/http.js +8 -7
  170. package/src/utils/import.js +9 -2
  171. package/src/utils/imports.js +83 -0
  172. package/src/utils/mime.js +9 -0
  173. package/src/utils/page_nodes.js +6 -7
  174. package/src/utils/params.js +67 -0
  175. package/src/utils/regex.js +9 -0
  176. package/src/utils/routing.js +145 -73
  177. package/src/utils/shared-iterator.js +5 -0
  178. package/src/utils/streaming.js +14 -4
  179. package/src/utils/url.js +33 -2
  180. package/src/version.js +1 -1
  181. package/types/index.d.ts +2616 -2433
  182. package/types/index.d.ts.map +120 -106
  183. package/src/core/sync/write_ambient.js +0 -18
  184. package/src/core/sync/write_root.js +0 -148
  185. package/src/core/sync/write_tsconfig.js +0 -250
  186. package/src/exports/internal/server.js +0 -22
  187. package/src/exports/node/polyfills.js +0 -30
  188. package/src/runtime/server/app.js +0 -9
  189. package/src/runtime/server/remote.js +0 -457
  190. package/src/runtime/shared-server.js +0 -7
  191. package/src/runtime/telemetry/otel.js +0 -21
  192. package/src/runtime/telemetry/record_span.js +0 -65
  193. package/src/types/synthetic/$lib.md +0 -5
  194. /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
  195. /package/src/{runtime/telemetry/noop.js → telemetry.js} +0 -0
@@ -1,13 +1,19 @@
1
1
  /** @import { StandardSchemaV1 } from '@standard-schema/spec' */
2
- /** @import { RemoteFormInput, RemoteForm, RemoteQueryUpdate } from '@sveltejs/kit' */
3
- /** @import { InternalRemoteFormIssue, RemoteFunctionResponse } from 'types' */
4
- import { app_dir, base } from '$app/paths/internal/client';
5
- import * as devalue from 'devalue';
2
+ /** @import { RemoteFormInput, RemoteForm, RemoteQueryUpdate } from '@sveltejs/kit/remote' */
3
+ /** @import { InternalRemoteFormIssue } from 'types' */
4
+ import { app_dir, base } from '#app/paths';
6
5
  import { DEV } from 'esm-env';
7
- import { HttpError } from '@sveltejs/kit/internal';
8
- import { app, query_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
6
+
7
+ import {
8
+ query_responses,
9
+ _goto,
10
+ set_nearest_error_page,
11
+ handle_error,
12
+ refreshAll
13
+ } from '../client.js';
14
+ import { page } from '../state.svelte.js';
9
15
  import { tick } from 'svelte';
10
- import { apply_refreshes, categorize_updates, apply_reconnections } from './shared.svelte.js';
16
+ import { categorize_updates, remote_request } from './shared.svelte.js';
11
17
  import { createAttachmentKey } from 'svelte/attachments';
12
18
  import {
13
19
  convert_formdata,
@@ -18,7 +24,11 @@ import {
18
24
  build_path_string,
19
25
  normalize_issue,
20
26
  serialize_binary_form,
21
- BINARY_FORM_CONTENT_TYPE
27
+ deep_get,
28
+ DELETE_KEY,
29
+ BINARY_FORM_CONTENT_TYPE,
30
+ parse_form_key,
31
+ coerce_form_value
22
32
  } from '../../form-utils.js';
23
33
 
24
34
  /**
@@ -30,16 +40,22 @@ import {
30
40
  * @returns {InternalRemoteFormIssue[]}
31
41
  */
32
42
  function merge_with_server_issues(form_data, current_issues, client_issues) {
43
+ const client_names = new Set(client_issues.map((issue) => issue.name));
44
+
33
45
  const merged = [
34
- ...current_issues.filter(
35
- (issue) => issue.server && !client_issues.some((i) => i.name === issue.name)
36
- ),
46
+ ...current_issues.filter((issue) => issue.server && !client_names.has(issue.name)),
37
47
  ...client_issues
38
48
  ];
39
49
 
40
- const keys = Array.from(form_data.keys());
50
+ /** @type {Map<string, number>} */
51
+ const positions = new Map();
52
+ let i = 0;
53
+ for (const key of form_data.keys()) {
54
+ if (!positions.has(key)) positions.set(key, i);
55
+ i++;
56
+ }
41
57
 
42
- return merged.sort((a, b) => keys.indexOf(a.name) - keys.indexOf(b.name));
58
+ return merged.sort((a, b) => (positions.get(a.name) ?? -1) - (positions.get(b.name) ?? -1));
43
59
  }
44
60
 
45
61
  /**
@@ -53,24 +69,56 @@ export function form(id) {
53
69
  /** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
54
70
  const instances = new Map();
55
71
 
72
+ /** @type {StandardSchemaV1 | null} */
73
+ let shared_preflight_schema = null;
74
+
56
75
  /** @param {string | number | boolean} [key] */
57
76
  function create_instance(key) {
58
77
  const action_id_without_key = id;
59
78
  const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
60
- const action = '?/remote=' + encodeURIComponent(action_id);
79
+ const action = '/remote=' + encodeURIComponent(action_id);
80
+
81
+ /** @type {string} */
82
+ let cached_search = '';
83
+ /** @type {string} */
84
+ let cached_query = '';
85
+
86
+ /** @returns {string} */
87
+ function get_action() {
88
+ if (page.url.search !== cached_search) {
89
+ cached_search = page.url.search;
90
+
91
+ if (page.url.search) {
92
+ const params = new URLSearchParams(page.url.search);
93
+ params.delete('/remote');
94
+ cached_query = params.toString();
95
+ } else {
96
+ cached_query = '';
97
+ }
98
+ }
99
+
100
+ return `?${cached_query && `${cached_query}&`}${action}`;
101
+ }
102
+
103
+ // the output of a non-enhanced submission that resulted in this page —
104
+ // consume it so the form's state survives hydration (form outputs are
105
+ // always value nodes; the server never serializes them as errors)
106
+ /** @type {{ input?: Record<string, any>, issues?: InternalRemoteFormIssue[], result?: any } | undefined} */
107
+ const initial = query_responses[action_id]?.v;
108
+ delete query_responses[action_id];
61
109
 
62
110
  /**
63
111
  * @type {Record<string, string | string[] | File | File[]>}
64
112
  */
65
- let input = $state({});
113
+ let input = $state(initial?.input ?? {});
66
114
 
67
115
  /** @type {InternalRemoteFormIssue[]} */
68
- let raw_issues = $state.raw([]);
116
+ let raw_issues = $state.raw(initial?.issues ?? []);
69
117
 
70
118
  const issues = $derived(flatten_issues(raw_issues));
71
119
 
72
120
  /** @type {any} */
73
- let result = $state.raw(query_responses[action_id]);
121
+ let result = $state.raw(initial?.result);
74
122
 
75
123
  /** @type {number} */
76
124
  let pending_count = $state(0);
@@ -83,7 +131,9 @@ export function form(id) {
83
131
  */
84
132
  let enhance_callback = async (instance) => {
85
133
  if (await instance.submit()) {
86
- instance.element.reset();
134
+ await tick();
135
+ // We call reset from the prototype to avoid DOM clobbering
136
+ HTMLFormElement.prototype.reset.call(instance.element);
87
137
  }
88
138
  };
89
139
 
@@ -91,13 +141,22 @@ export function form(id) {
91
141
  let element = null;
92
142
 
93
143
  /** @type {Record<string, boolean>} */
94
- let touched = {};
144
+ let touched = $state({});
145
+
146
+ /** @type {Record<string, boolean>} */
147
+ let dirty = $state({});
95
148
 
96
- let submitted = false;
149
+ /** @type {Record<string, boolean>} */
150
+ let can_validate = {};
151
+
152
+ let submitted = $state(false);
97
153
 
98
154
  /** @type {InternalRemoteFormIssue[] | null} */
99
155
  let unread_issues = null;
100
156
 
157
+ /** @type {{ name: string; type: 'number' | 'boolean' | null; is_array: boolean } | null} */
158
+ let previous_submitter = null;
159
+
101
160
  /**
102
161
  * In dev, warn if there are validation issues going unread
103
162
  */
@@ -132,8 +191,8 @@ export function form(id) {
132
191
  * @returns {Record<string, any>}
133
192
  */
134
193
  function convert(form_data) {
135
- const data = convert_formdata(form_data);
136
- if (key !== undefined && !form_data.has('id')) {
194
+ const data = convert_formdata(action_id_without_key, form_data);
195
+ if (key !== undefined && !('id' in data)) {
137
196
  data.id = key;
138
197
  }
139
198
  return data;
@@ -163,109 +222,84 @@ export function form(id) {
163
222
  /** @type {Error | undefined} */
164
223
  let updates_error;
165
224
 
166
- /** @type {Promise<boolean> & { updates: (...args: RemoteQueryUpdate[]) => Promise<boolean> }} */
167
- const promise = (async () => {
168
- try {
169
- await Promise.resolve();
170
-
171
- if (updates_error) {
172
- throw updates_error;
173
- }
225
+ const promise =
226
+ /** @type {Promise<boolean> & { updates: (...args: RemoteQueryUpdate[]) => Promise<boolean> }} */ (
227
+ (async () => {
228
+ try {
229
+ await Promise.resolve();
174
230
 
175
- if (should_preflight) {
176
- const valid = await preflight(form_data);
177
- if (!valid) return false;
178
- }
231
+ if (updates_error) {
232
+ throw updates_error;
233
+ }
179
234
 
180
- const { blob } = serialize_binary_form(convert(form_data), {
181
- remote_refreshes: Array.from(refreshes ?? [])
182
- });
235
+ if (should_preflight) {
236
+ const valid = await preflight(form_data);
237
+ if (!valid) return false;
238
+ }
183
239
 
184
- const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
185
- method: 'POST',
186
- headers: {
187
- 'Content-Type': BINARY_FORM_CONTENT_TYPE,
188
- // Forms cannot be called during rendering, so it's save to use location here
189
- 'x-sveltekit-pathname': location.pathname,
190
- 'x-sveltekit-search': location.search
191
- },
192
- body: blob
193
- });
240
+ const { blob } = serialize_binary_form(convert(form_data), {
241
+ remote_refreshes: Array.from(refreshes ?? [])
242
+ });
243
+
244
+ const response = await remote_request(
245
+ `${base}/${app_dir}/remote/${action_id_without_key}`,
246
+ {
247
+ method: 'POST',
248
+ headers: {
249
+ 'Content-Type': BINARY_FORM_CONTENT_TYPE,
250
+ // Forms cannot be called during rendering, so it's save to use location here
251
+ 'x-sveltekit-pathname': location.pathname,
252
+ 'x-sveltekit-search': location.search
253
+ },
254
+ body: blob
255
+ }
256
+ );
194
257
 
195
- if (!response.ok) {
196
- // We only end up here in case of a network error or if the server has an internal error
197
- // (which shouldn't happen because we handle errors on the server and always send a 200 response)
198
- throw new Error('Failed to execute remote function');
199
- }
258
+ ({ issues: raw_issues = [], result } = response._ ?? {});
200
259
 
201
- const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
260
+ // if the developer took control of updates via `.updates(...)` (even with
261
+ // no arguments), or the server performed explicit refreshes, don't invalidateAll
262
+ const should_refresh = refreshes === null && !response.r;
202
263
 
203
- // reset issues in case it's a redirect or error (but issues passed in that case)
204
- raw_issues = [];
205
- result = undefined;
264
+ if (response.redirect) {
265
+ // Use internal version to allow redirects to external URLs
266
+ void _goto(response.redirect, {
267
+ refreshAll: should_refresh
268
+ });
269
+ return true;
270
+ }
206
271
 
207
- if (form_result.type === 'result') {
208
- ({ issues: raw_issues = [], result } = devalue.parse(form_result.result, app.decoders));
209
- const succeeded = raw_issues.length === 0;
272
+ const succeeded = raw_issues.length === 0;
210
273
 
211
- if (succeeded) {
212
- if (refreshes === null && !form_result.refreshes && !form_result.reconnects) {
213
- void invalidateAll();
214
- } else {
215
- if (form_result.refreshes) {
216
- apply_refreshes(form_result.refreshes);
274
+ if (succeeded) {
275
+ if (should_refresh) {
276
+ void refreshAll();
217
277
  }
218
- if (form_result.reconnects) {
219
- apply_reconnections(form_result.reconnects);
278
+ } else {
279
+ if (DEV) {
280
+ warn_on_missing_issue_reads();
220
281
  }
221
282
  }
222
- } else {
223
- if (DEV) {
224
- warn_on_missing_issue_reads();
225
- }
226
- }
227
283
 
228
- return succeeded;
229
- } else if (form_result.type === 'redirect') {
230
- const stringified_refreshes = form_result.refreshes ?? '';
231
- const stringified_reconnects = form_result.reconnects ?? '';
232
- if (stringified_refreshes) {
233
- apply_refreshes(stringified_refreshes);
234
- }
235
-
236
- if (stringified_reconnects) {
237
- apply_reconnections(stringified_reconnects);
238
- }
239
-
240
- // Use internal version to allow redirects to external URLs
241
- void _goto(
242
- form_result.location,
243
- {
244
- invalidateAll:
245
- refreshes === null && !stringified_refreshes && !stringified_reconnects
246
- },
247
- 0
248
- );
249
- return true;
250
- } else {
251
- throw new HttpError(form_result.status ?? 500, form_result.error);
252
- }
253
- } catch (e) {
254
- result = undefined;
255
- throw e;
256
- } finally {
257
- overrides?.forEach((fn) => fn());
258
-
259
- void tick().then(() => {
260
- if (entry) {
261
- entry.count--;
262
- if (entry.count === 0) {
263
- instances.delete(key);
264
- }
284
+ return succeeded;
285
+ } catch (e) {
286
+ result = undefined;
287
+ raw_issues = [];
288
+ throw e;
289
+ } finally {
290
+ overrides?.forEach((fn) => fn());
291
+
292
+ void tick().then(() => {
293
+ if (entry) {
294
+ entry.count--;
295
+ if (entry.count === 0) {
296
+ instances.delete(key);
297
+ }
298
+ }
299
+ });
265
300
  }
266
- });
267
- }
268
- })();
301
+ })()
302
+ );
269
303
 
270
304
  let updates_called = false;
271
305
  promise.updates = (...args) => {
@@ -319,7 +353,8 @@ export function form(id) {
319
353
  */
320
354
  async function preflight(form_data) {
321
355
  const data = convert(form_data);
322
- const validated = await preflight_schema?.['~standard'].validate(data);
356
+ const schema = preflight_schema ?? shared_preflight_schema;
357
+ const validated = await schema?.['~standard'].validate(data);
323
358
 
324
359
  if (validated?.issues) {
325
360
  raw_issues = merge_with_server_issues(
@@ -343,11 +378,13 @@ export function form(id) {
343
378
  return true;
344
379
  }
345
380
 
346
- /** @type {RemoteForm<T, U>} */
347
- const instance = {};
381
+ const instance = /** @type {RemoteForm<T, U>} */ ({});
348
382
 
349
383
  instance.method = 'POST';
350
- instance.action = action;
384
+ Object.defineProperty(instance, 'action', {
385
+ get: get_action,
386
+ enumerable: true
387
+ });
351
388
 
352
389
  instance[createAttachmentKey()] = (/** @type {HTMLFormElement} */ form) => {
353
390
  if (element) {
@@ -363,6 +400,8 @@ export function form(id) {
363
400
  element = form;
364
401
 
365
402
  touched = {};
403
+ dirty = {};
404
+ can_validate = {};
366
405
 
367
406
  /** @param {SubmitEvent} event */
368
407
  const handle_submit = async (event) => {
@@ -396,6 +435,33 @@ export function form(id) {
396
435
 
397
436
  const form_data = new FormData(form, event.submitter);
398
437
 
438
+ if (
439
+ previous_submitter !== null &&
440
+ !Array.from(form_data.keys())
441
+ .map((k) => parse_form_key(action_id_without_key, k).name)
442
+ .includes(previous_submitter.name)
443
+ ) {
444
+ set_nested_value(input, previous_submitter, undefined);
445
+ }
446
+
447
+ if (event.submitter) {
448
+ const name = event.submitter.getAttribute('name');
449
+
450
+ /** @type {null | ReturnType<typeof parse_form_key>} */
451
+ let submitter = null;
452
+
453
+ const value = /** @type {any} */ (event.submitter).value;
454
+
455
+ if (name !== null && value !== undefined) {
456
+ submitter = parse_form_key(action_id_without_key, name);
457
+ set_nested_value(input, submitter, coerce_form_value(submitter.type, value));
458
+ }
459
+
460
+ previous_submitter = submitter;
461
+ } else {
462
+ previous_submitter = null;
463
+ }
464
+
399
465
  if (DEV) {
400
466
  validate_form_data(form_data, clone(form).enctype);
401
467
  }
@@ -412,10 +478,12 @@ export function form(id) {
412
478
 
413
479
  await enhance_callback(create_enhance_callback_instance(form, form_data));
414
480
  } catch (e) {
415
- const error =
416
- e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
417
- const status = e instanceof HttpError ? e.status : 500;
418
- void set_nearest_error_page(error, status);
481
+ const error = await handle_error(e, {
482
+ params: {},
483
+ route: { id: null },
484
+ url: new URL(location.href)
485
+ });
486
+ void set_nearest_error_page(error);
419
487
  } finally {
420
488
  pending_count--;
421
489
  }
@@ -427,17 +495,14 @@ export function form(id) {
427
495
  // but that makes the types unnecessarily awkward
428
496
  const element = /** @type {HTMLInputElement} */ (e.target);
429
497
 
430
- let name = element.name;
498
+ const name = element.name;
431
499
  if (!name) return;
432
500
 
433
- const is_array = name.endsWith('[]');
434
- if (is_array) name = name.slice(0, -2);
501
+ const field = parse_form_key(action_id_without_key, name);
435
502
 
436
503
  const is_file = element.type === 'file';
437
504
 
438
- touched[name] = true;
439
-
440
- if (is_array) {
505
+ if (field.is_array) {
441
506
  let value;
442
507
 
443
508
  if (element.tagName === 'SELECT') {
@@ -447,7 +512,7 @@ export function form(id) {
447
512
  );
448
513
  } else {
449
514
  const elements = /** @type {HTMLInputElement[]} */ (
450
- Array.from(form.querySelectorAll(`[name="${name}[]"]`))
515
+ Array.from(form.querySelectorAll(`[name="${name}"]`))
451
516
  );
452
517
 
453
518
  if (DEV) {
@@ -468,7 +533,7 @@ export function form(id) {
468
533
  }
469
534
  }
470
535
 
471
- set_nested_value(input, name, value);
536
+ set_nested_value(input, field, value);
472
537
  } else if (is_file) {
473
538
  if (DEV && element.multiple) {
474
539
  throw new Error(
@@ -479,28 +544,19 @@ export function form(id) {
479
544
  const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
480
545
 
481
546
  if (file) {
482
- set_nested_value(input, name, file);
547
+ set_nested_value(input, field, file);
483
548
  } else {
484
- // Remove the property by setting to undefined and clean up
485
- const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
486
- let current = /** @type {any} */ (input);
487
- for (let i = 0; i < path_parts.length - 1; i++) {
488
- if (current[path_parts[i]] == null) return;
489
- current = current[path_parts[i]];
490
- }
491
- delete current[path_parts[path_parts.length - 1]];
549
+ set_nested_value(input, field, DELETE_KEY);
492
550
  }
493
551
  } else {
494
552
  set_nested_value(
495
553
  input,
496
- name,
554
+ field,
497
555
  element.type === 'checkbox' && !element.checked ? null : element.value
498
556
  );
499
557
  }
500
558
 
501
- name = name.replace(/^[nb]:/, '');
502
-
503
- touched[name] = true;
559
+ dirty[field.name] = true;
504
560
  };
505
561
 
506
562
  const handle_reset = async () => {
@@ -508,19 +564,39 @@ export function form(id) {
508
564
  // the inputs are actually updated (so that it can be cancelled)
509
565
  await tick();
510
566
 
511
- input = convert_formdata(new FormData(form));
567
+ input = convert_formdata(action_id_without_key, new FormData(form));
568
+ raw_issues = [];
569
+ touched = {};
570
+ dirty = {};
571
+ can_validate = {};
572
+ submitted = false;
573
+ };
574
+
575
+ /** @param {Event} e */
576
+ const handle_focusout = (e) => {
577
+ const name = /** @type {HTMLInputElement} */ (e.target).name;
578
+ if (!name) return;
579
+
580
+ const field = parse_form_key(action_id_without_key, name);
581
+
582
+ touched[field.name] = true;
583
+
584
+ if (Object.hasOwn(dirty, field.name)) {
585
+ can_validate[field.name] = true;
586
+ }
512
587
  };
513
588
 
514
589
  form.addEventListener('submit', handle_submit);
515
590
  form.addEventListener('input', handle_input);
591
+ form.addEventListener('focusout', handle_focusout);
516
592
  form.addEventListener('reset', handle_reset);
517
593
 
518
594
  return () => {
519
595
  form.removeEventListener('submit', handle_submit);
520
596
  form.removeEventListener('input', handle_input);
597
+ form.removeEventListener('focusout', handle_focusout);
521
598
  form.removeEventListener('reset', handle_reset);
522
599
  element = null;
523
- preflight_schema = undefined;
524
600
  };
525
601
  };
526
602
 
@@ -551,29 +627,35 @@ export function form(id) {
551
627
 
552
628
  const submission = submit(form_data, true);
553
629
 
554
- void submission.finally(() => {
630
+ const decrement = () => {
555
631
  pending_count--;
556
- });
632
+ };
633
+ void submission.then(decrement, decrement);
557
634
 
558
635
  return submission;
559
636
  }
560
637
  },
561
638
  fields: {
562
639
  get: () =>
563
- create_field_proxy(
564
- {},
565
- () => input,
566
- (path, value) => {
640
+ create_field_proxy({
641
+ form_id: action_id_without_key,
642
+ get: () => input,
643
+ set: (path, value) => {
567
644
  if (path.length === 0) {
568
645
  input = value;
569
- } else {
646
+ } else if (value !== deep_get(input, path)) {
570
647
  deep_set(input, path.map(String), value);
571
648
 
572
649
  const key = build_path_string(path);
573
- touched[key] = true;
650
+
651
+ if (element) {
652
+ touched[key] = true;
653
+ dirty[key] = true;
654
+ can_validate[key] = true;
655
+ }
574
656
  }
575
657
  },
576
- (path, all) => {
658
+ get_issues: (path, all) => {
577
659
  if (DEV && unread_issues !== null && path !== undefined) {
578
660
  unread_issues = unread_issues.filter((issue) => {
579
661
  return (
@@ -584,8 +666,10 @@ export function form(id) {
584
666
  }
585
667
 
586
668
  return issues;
587
- }
588
- )
669
+ },
670
+ get_touched: () => touched,
671
+ get_dirty: () => dirty
672
+ })
589
673
  },
590
674
  result: {
591
675
  get: () => result
@@ -593,16 +677,24 @@ export function form(id) {
593
677
  pending: {
594
678
  get: () => pending_count
595
679
  },
680
+ submitted: {
681
+ get: () => submitted
682
+ },
596
683
  preflight: {
597
684
  /** @type {RemoteForm<T, U>['preflight']} */
598
685
  value: (schema) => {
599
686
  preflight_schema = schema;
687
+
688
+ if (key === undefined) {
689
+ shared_preflight_schema = schema;
690
+ }
691
+
600
692
  return instance;
601
693
  }
602
694
  },
603
695
  validate: {
604
696
  /** @type {RemoteForm<any, any>['validate']} */
605
- value: async ({ includeUntouched = false, preflightOnly = false } = {}) => {
697
+ value: async ({ all = false, preflightOnly = false } = {}) => {
606
698
  if (!element) return;
607
699
 
608
700
  const id = ++validate_id;
@@ -620,8 +712,8 @@ export function form(id) {
620
712
  let array = [];
621
713
 
622
714
  const data = convert(form_data);
623
-
624
- const validated = await preflight_schema?.['~standard'].validate(data);
715
+ const schema = preflight_schema ?? shared_preflight_schema;
716
+ const validated = await schema?.['~standard'].validate(data);
625
717
 
626
718
  if (validate_id !== id) {
627
719
  return;
@@ -630,34 +722,31 @@ export function form(id) {
630
722
  if (validated?.issues) {
631
723
  array = validated.issues.map((issue) => normalize_issue(issue, false));
632
724
  } else if (!preflightOnly) {
633
- const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
634
- method: 'POST',
635
- headers: {
636
- 'Content-Type': BINARY_FORM_CONTENT_TYPE,
637
- // Validation should not be and will not be called during rendering, so it's save to use location here
638
- 'x-sveltekit-pathname': location.pathname,
639
- 'x-sveltekit-search': location.search
640
- },
641
- body: serialize_binary_form(data, {
642
- validate_only: true
643
- }).blob
644
- });
645
-
646
- const result = await response.json();
725
+ const result = await remote_request(
726
+ `${base}/${app_dir}/remote/${action_id_without_key}`,
727
+ {
728
+ method: 'POST',
729
+ headers: {
730
+ 'Content-Type': BINARY_FORM_CONTENT_TYPE,
731
+ // Validation should not be and will not be called during rendering, so it's save to use location here
732
+ 'x-sveltekit-pathname': location.pathname,
733
+ 'x-sveltekit-search': location.search
734
+ },
735
+ body: serialize_binary_form(data, {
736
+ validate_only: true
737
+ }).blob
738
+ }
739
+ );
647
740
 
648
741
  if (validate_id !== id) {
649
742
  return;
650
743
  }
651
744
 
652
- if (result.type === 'result') {
653
- array = /** @type {InternalRemoteFormIssue[]} */ (
654
- devalue.parse(result.result, app.decoders)
655
- );
656
- }
745
+ array = /** @type {InternalRemoteFormIssue[]} */ (result._);
657
746
  }
658
747
 
659
- if (!includeUntouched && !submitted) {
660
- array = array.filter((issue) => touched[issue.name]);
748
+ if (!all && !submitted) {
749
+ array = array.filter((issue) => can_validate[issue.name]);
661
750
  }
662
751
 
663
752
  const is_server_validation = !validated?.issues && !preflightOnly;