@sveltejs/kit 2.60.1 → 2.61.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.
Files changed (45) hide show
  1. package/package.json +8 -9
  2. package/src/core/postbuild/analyse.js +1 -3
  3. package/src/core/sync/create_manifest_data/conflict.js +72 -0
  4. package/src/core/sync/create_manifest_data/index.js +1 -65
  5. package/src/core/sync/write_non_ambient.js +2 -2
  6. package/src/core/sync/write_types/index.js +1 -1
  7. package/src/exports/public.d.ts +23 -30
  8. package/src/exports/vite/build/build_server.js +36 -13
  9. package/src/exports/vite/dev/index.js +4 -2
  10. package/src/exports/vite/index.js +18 -16
  11. package/src/runtime/app/server/index.js +1 -2
  12. package/src/runtime/app/server/remote/form.js +10 -0
  13. package/src/runtime/app/server/remote/query.js +100 -36
  14. package/src/runtime/client/client.js +13 -8
  15. package/src/runtime/client/remote-functions/cache.svelte.js +157 -0
  16. package/src/runtime/client/remote-functions/form.svelte.js +235 -196
  17. package/src/runtime/client/remote-functions/index.js +2 -2
  18. package/src/runtime/client/remote-functions/prerender.svelte.js +1 -2
  19. package/src/runtime/client/remote-functions/query/cache.js +4 -0
  20. package/src/runtime/client/remote-functions/query/index.js +48 -0
  21. package/src/runtime/client/remote-functions/query/instance.svelte.js +249 -0
  22. package/src/runtime/client/remote-functions/query/proxy.js +156 -0
  23. package/src/runtime/client/remote-functions/query-batch.svelte.js +1 -1
  24. package/src/runtime/client/remote-functions/query-live/cache.js +4 -0
  25. package/src/runtime/client/remote-functions/query-live/index.js +31 -0
  26. package/src/runtime/client/remote-functions/{query-live.svelte.js → query-live/instance.svelte.js} +61 -310
  27. package/src/runtime/client/remote-functions/query-live/iterator.js +91 -0
  28. package/src/runtime/client/remote-functions/query-live/proxy.js +144 -0
  29. package/src/runtime/client/remote-functions/shared.svelte.js +53 -6
  30. package/src/runtime/client/utils.js +1 -1
  31. package/src/runtime/form-utils.js +7 -16
  32. package/src/runtime/server/index.js +2 -3
  33. package/src/runtime/server/page/actions.js +2 -9
  34. package/src/runtime/server/page/csp.js +3 -4
  35. package/src/runtime/server/page/render.js +13 -14
  36. package/src/runtime/server/respond.js +60 -36
  37. package/src/runtime/server/utils.js +23 -3
  38. package/src/types/global-private.d.ts +5 -0
  39. package/src/types/internal.d.ts +29 -6
  40. package/src/utils/routing.js +3 -1
  41. package/src/utils/shared-iterator.js +213 -0
  42. package/src/version.js +1 -1
  43. package/types/index.d.ts +27 -31
  44. package/types/index.d.ts.map +1 -1
  45. package/src/runtime/client/remote-functions/query.svelte.js +0 -512
@@ -79,6 +79,15 @@ export function form(id) {
79
79
  /** @type {StandardSchemaV1 | undefined} */
80
80
  let preflight_schema = undefined;
81
81
 
82
+ /**
83
+ * @param {Omit<RemoteForm<T, U>, 'enhance' | 'element'> & { readonly element: HTMLFormElement }} instance
84
+ */
85
+ let enhance_callback = async (instance) => {
86
+ if (await instance.submit()) {
87
+ instance.element.reset();
88
+ }
89
+ };
90
+
82
91
  /** @type {HTMLFormElement | null} */
83
92
  let element = null;
84
93
 
@@ -132,86 +141,11 @@ export function form(id) {
132
141
  }
133
142
 
134
143
  /**
135
- * @param {HTMLFormElement} form
136
144
  * @param {FormData} form_data
137
- * @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback
138
- */
139
- async function handle_submit(form, form_data, callback) {
140
- const data = convert(form_data);
141
-
142
- submitted = true;
143
-
144
- // Increment pending count immediately so that `pending` reflects
145
- // the in-progress state during async preflight validation
146
- pending_count++;
147
-
148
- const validated = await preflight_schema?.['~standard'].validate(data);
149
-
150
- if (validated?.issues) {
151
- raw_issues = merge_with_server_issues(
152
- form_data,
153
- raw_issues,
154
- validated.issues.map((issue) => normalize_issue(issue, false))
155
- );
156
-
157
- if (DEV) {
158
- warn_on_missing_issue_reads();
159
- }
160
-
161
- pending_count--;
162
- return;
163
- }
164
-
165
- // Preflight passed - clear stale client-side preflight issues
166
- if (preflight_schema) {
167
- raw_issues = raw_issues.filter((issue) => issue.server);
168
- }
169
-
170
- // TODO 3.0 remove this warning
171
- if (DEV) {
172
- const error = () => {
173
- throw new Error(
174
- 'Remote form functions no longer get passed a FormData object. The payload is now a POJO. See https://kit.svelte.dev/docs/remote-functions#form for details.'
175
- );
176
- };
177
- for (const key of [
178
- 'append',
179
- 'delete',
180
- 'entries',
181
- 'forEach',
182
- 'get',
183
- 'getAll',
184
- 'has',
185
- 'keys',
186
- 'set',
187
- 'values'
188
- ]) {
189
- if (!(key in data)) {
190
- Object.defineProperty(data, key, { get: error });
191
- }
192
- }
193
- }
194
-
195
- try {
196
- await callback({
197
- form,
198
- data,
199
- submit: () => submit(form_data)
200
- });
201
- } catch (e) {
202
- const error = e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
203
- const status = e instanceof HttpError ? e.status : 500;
204
- void set_nearest_error_page(error, status);
205
- } finally {
206
- pending_count--;
207
- }
208
- }
209
-
210
- /**
211
- * @param {FormData} data
145
+ * @param {boolean} should_preflight
212
146
  * @returns {Promise<boolean> & { updates: (...args: any[]) => Promise<boolean> }}
213
147
  */
214
- function submit(data) {
148
+ function submit(form_data, should_preflight) {
215
149
  // Store a reference to the current instance and increment the usage count for the duration
216
150
  // of the request. This ensures that the instance is not deleted in case of an optimistic update
217
151
  // (e.g. when deleting an item in a list) that fails and wants to surface an error to the user afterwards.
@@ -239,7 +173,12 @@ export function form(id) {
239
173
  throw updates_error;
240
174
  }
241
175
 
242
- const { blob } = serialize_binary_form(convert(data), {
176
+ if (should_preflight) {
177
+ const valid = await preflight(form_data);
178
+ if (!valid) return false;
179
+ }
180
+
181
+ const { blob } = serialize_binary_form(convert(form_data), {
243
182
  remote_refreshes: Array.from(refreshes ?? [])
244
183
  });
245
184
 
@@ -351,16 +290,99 @@ export function form(id) {
351
290
  return promise;
352
291
  }
353
292
 
293
+ /**
294
+ * @param {HTMLFormElement} form
295
+ * @param {FormData} form_data
296
+ * @returns {Omit<RemoteForm<T, U>, 'enhance' | 'element'> & { readonly element: HTMLFormElement }}
297
+ */
298
+ function create_enhance_callback_instance(form, form_data) {
299
+ const { enhance: _enhance, ...descriptors } = Object.getOwnPropertyDescriptors(instance);
300
+ void _enhance;
301
+
302
+ return /** @type {Omit<RemoteForm<T, U>, 'enhance' | 'element'> & { readonly element: HTMLFormElement }} */ (
303
+ Object.defineProperties(
304
+ {},
305
+ {
306
+ ...descriptors,
307
+ data: {
308
+ get() {
309
+ // TODO 3.0 remove
310
+ throw new Error(
311
+ `The \`data\` property has been removed from the \`enhance\` callback argument. Use \`instance.fields.value()\` instead.`
312
+ );
313
+ }
314
+ },
315
+ form: {
316
+ get() {
317
+ // TODO 3.0 remove
318
+ throw new Error(
319
+ `The \`form\` property has been removed from the \`enhance\` callback argument. To get the current \`<form>\` element, use \`instance.element\` instead.`
320
+ );
321
+ }
322
+ },
323
+ element: {
324
+ value: form
325
+ },
326
+ submit: {
327
+ value: () => submit(form_data, false)
328
+ }
329
+ }
330
+ )
331
+ );
332
+ }
333
+
334
+ /**
335
+ * @param {FormData} form_data
336
+ */
337
+ async function preflight(form_data) {
338
+ const data = convert(form_data);
339
+ const validated = await preflight_schema?.['~standard'].validate(data);
340
+
341
+ if (validated?.issues) {
342
+ raw_issues = merge_with_server_issues(
343
+ form_data,
344
+ raw_issues,
345
+ validated.issues.map((issue) => normalize_issue(issue, false))
346
+ );
347
+
348
+ if (DEV) {
349
+ warn_on_missing_issue_reads();
350
+ }
351
+
352
+ return false;
353
+ }
354
+
355
+ // Preflight passed - clear stale client-side preflight issues
356
+ if (preflight_schema) {
357
+ raw_issues = raw_issues.filter((issue) => issue.server);
358
+ }
359
+
360
+ return true;
361
+ }
362
+
354
363
  /** @type {RemoteForm<T, U>} */
355
364
  const instance = {};
356
365
 
357
366
  instance.method = 'POST';
358
367
  instance.action = action;
359
368
 
360
- /** @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback */
361
- const form_onsubmit = (callback) => {
369
+ instance[createAttachmentKey()] = (/** @type {HTMLFormElement} */ form) => {
370
+ if (element) {
371
+ let message = `A form object can only be attached to a single \`<form>\` element`;
372
+ if (DEV && !key) {
373
+ const name = id.split('/').pop();
374
+ message += `. To create multiple instances, use \`${name}.for(key)\``;
375
+ }
376
+
377
+ throw new Error(message);
378
+ }
379
+
380
+ element = form;
381
+
382
+ touched = {};
383
+
362
384
  /** @param {SubmitEvent} event */
363
- return async (event) => {
385
+ const handle_submit = async (event) => {
364
386
  const form = /** @type {HTMLFormElement} */ (event.target);
365
387
  const method = event.submitter?.hasAttribute('formmethod')
366
388
  ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod
@@ -395,142 +417,129 @@ export function form(id) {
395
417
  validate_form_data(form_data, clone(form).enctype);
396
418
  }
397
419
 
398
- await handle_submit(form, form_data, callback);
399
- };
400
- };
401
-
402
- /** @param {(event: SubmitEvent) => void} onsubmit */
403
- function create_attachment(onsubmit) {
404
- return (/** @type {HTMLFormElement} */ form) => {
405
- if (element) {
406
- let message = `A form object can only be attached to a single \`<form>\` element`;
407
- if (DEV && !key) {
408
- const name = id.split('/').pop();
409
- message += `. To create multiple instances, use \`${name}.for(key)\``;
410
- }
411
-
412
- throw new Error(message);
413
- }
420
+ submitted = true;
414
421
 
415
- element = form;
422
+ try {
423
+ // Increment pending count immediately so that `pending` reflects
424
+ // the in-progress state during async preflight validation
425
+ pending_count++;
416
426
 
417
- touched = {};
427
+ const valid = await preflight(form_data);
428
+ if (!valid) return;
418
429
 
419
- form.addEventListener('submit', onsubmit);
430
+ await enhance_callback(create_enhance_callback_instance(form, form_data));
431
+ } catch (e) {
432
+ const error =
433
+ e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
434
+ const status = e instanceof HttpError ? e.status : 500;
435
+ void set_nearest_error_page(error, status);
436
+ } finally {
437
+ pending_count--;
438
+ }
439
+ };
420
440
 
421
- /** @param {Event} e */
422
- const handle_input = (e) => {
423
- // strictly speaking it can be an HTMLTextAreaElement or HTMLSelectElement
424
- // but that makes the types unnecessarily awkward
425
- const element = /** @type {HTMLInputElement} */ (e.target);
441
+ /** @param {Event} e */
442
+ const handle_input = (e) => {
443
+ // strictly speaking it can be an HTMLTextAreaElement or HTMLSelectElement
444
+ // but that makes the types unnecessarily awkward
445
+ const element = /** @type {HTMLInputElement} */ (e.target);
426
446
 
427
- let name = element.name;
428
- if (!name) return;
447
+ let name = element.name;
448
+ if (!name) return;
429
449
 
430
- const is_array = name.endsWith('[]');
431
- if (is_array) name = name.slice(0, -2);
450
+ const is_array = name.endsWith('[]');
451
+ if (is_array) name = name.slice(0, -2);
432
452
 
433
- const is_file = element.type === 'file';
453
+ const is_file = element.type === 'file';
434
454
 
435
- touched[name] = true;
455
+ touched[name] = true;
436
456
 
437
- if (is_array) {
438
- let value;
457
+ if (is_array) {
458
+ let value;
439
459
 
440
- if (element.tagName === 'SELECT') {
441
- value = Array.from(
442
- element.querySelectorAll('option:checked'),
443
- (e) => /** @type {HTMLOptionElement} */ (e).value
444
- );
445
- } else {
446
- const elements = /** @type {HTMLInputElement[]} */ (
447
- Array.from(form.querySelectorAll(`[name="${name}[]"]`))
448
- );
460
+ if (element.tagName === 'SELECT') {
461
+ value = Array.from(
462
+ element.querySelectorAll('option:checked'),
463
+ (e) => /** @type {HTMLOptionElement} */ (e).value
464
+ );
465
+ } else {
466
+ const elements = /** @type {HTMLInputElement[]} */ (
467
+ Array.from(form.querySelectorAll(`[name="${name}[]"]`))
468
+ );
449
469
 
450
- if (DEV) {
451
- for (const e of elements) {
452
- if ((e.type === 'file') !== is_file) {
453
- throw new Error(
454
- `Cannot mix and match file and non-file inputs under the same name ("${element.name}")`
455
- );
456
- }
470
+ if (DEV) {
471
+ for (const e of elements) {
472
+ if ((e.type === 'file') !== is_file) {
473
+ throw new Error(
474
+ `Cannot mix and match file and non-file inputs under the same name ("${element.name}")`
475
+ );
457
476
  }
458
477
  }
459
-
460
- value = is_file
461
- ? elements.map((input) => Array.from(input.files ?? [])).flat()
462
- : elements.map((element) => element.value);
463
- if (element.type === 'checkbox') {
464
- value = /** @type {string[]} */ (value.filter((_, i) => elements[i].checked));
465
- }
466
478
  }
467
479
 
468
- set_nested_value(input, name, value);
469
- } else if (is_file) {
470
- if (DEV && element.multiple) {
471
- throw new Error(
472
- `Can only use the \`multiple\` attribute when \`name\` includes a \`[]\` suffix consider changing "${name}" to "${name}[]"`
473
- );
480
+ value = is_file
481
+ ? elements.map((input) => Array.from(input.files ?? [])).flat()
482
+ : elements.map((element) => element.value);
483
+ if (element.type === 'checkbox') {
484
+ value = /** @type {string[]} */ (value.filter((_, i) => elements[i].checked));
474
485
  }
486
+ }
475
487
 
476
- const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
477
-
478
- if (file) {
479
- set_nested_value(input, name, file);
480
- } else {
481
- // Remove the property by setting to undefined and clean up
482
- const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
483
- let current = /** @type {any} */ (input);
484
- for (let i = 0; i < path_parts.length - 1; i++) {
485
- if (current[path_parts[i]] == null) return;
486
- current = current[path_parts[i]];
487
- }
488
- delete current[path_parts[path_parts.length - 1]];
489
- }
490
- } else {
491
- set_nested_value(
492
- input,
493
- name,
494
- element.type === 'checkbox' && !element.checked ? null : element.value
488
+ set_nested_value(input, name, value);
489
+ } else if (is_file) {
490
+ if (DEV && element.multiple) {
491
+ throw new Error(
492
+ `Can only use the \`multiple\` attribute when \`name\` includes a \`[]\` suffix — consider changing "${name}" to "${name}[]"`
495
493
  );
496
494
  }
497
495
 
498
- name = name.replace(/^[nb]:/, '');
496
+ const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
499
497
 
500
- touched[name] = true;
501
- };
502
-
503
- form.addEventListener('input', handle_input);
498
+ if (file) {
499
+ set_nested_value(input, name, file);
500
+ } else {
501
+ // Remove the property by setting to undefined and clean up
502
+ const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
503
+ let current = /** @type {any} */ (input);
504
+ for (let i = 0; i < path_parts.length - 1; i++) {
505
+ if (current[path_parts[i]] == null) return;
506
+ current = current[path_parts[i]];
507
+ }
508
+ delete current[path_parts[path_parts.length - 1]];
509
+ }
510
+ } else {
511
+ set_nested_value(
512
+ input,
513
+ name,
514
+ element.type === 'checkbox' && !element.checked ? null : element.value
515
+ );
516
+ }
504
517
 
505
- const handle_reset = async () => {
506
- // need to wait a moment, because the `reset` event occurs before
507
- // the inputs are actually updated (so that it can be cancelled)
508
- await tick();
518
+ name = name.replace(/^[nb]:/, '');
509
519
 
510
- input = convert_formdata(new FormData(form));
511
- };
520
+ touched[name] = true;
521
+ };
512
522
 
513
- form.addEventListener('reset', handle_reset);
523
+ const handle_reset = async () => {
524
+ // need to wait a moment, because the `reset` event occurs before
525
+ // the inputs are actually updated (so that it can be cancelled)
526
+ await tick();
514
527
 
515
- return () => {
516
- form.removeEventListener('submit', onsubmit);
517
- form.removeEventListener('input', handle_input);
518
- form.removeEventListener('reset', handle_reset);
519
- element = null;
520
- preflight_schema = undefined;
521
- };
528
+ input = convert_formdata(new FormData(form));
522
529
  };
523
- }
524
530
 
525
- instance[createAttachmentKey()] = create_attachment(
526
- form_onsubmit(({ submit, form }) =>
527
- submit().then((succeeded) => {
528
- if (succeeded) {
529
- form.reset();
530
- }
531
- })
532
- )
533
- );
531
+ form.addEventListener('submit', handle_submit);
532
+ form.addEventListener('input', handle_input);
533
+ form.addEventListener('reset', handle_reset);
534
+
535
+ return () => {
536
+ form.removeEventListener('submit', handle_submit);
537
+ form.removeEventListener('input', handle_input);
538
+ form.removeEventListener('reset', handle_reset);
539
+ element = null;
540
+ preflight_schema = undefined;
541
+ };
542
+ };
534
543
 
535
544
  let validate_id = 0;
536
545
 
@@ -549,6 +558,37 @@ export function form(id) {
549
558
  }
550
559
 
551
560
  Object.defineProperties(instance, {
561
+ element: {
562
+ get: () => element
563
+ },
564
+ submit: {
565
+ value: () => {
566
+ if (!element) {
567
+ throw new Error('Cannot call submit() before the form is attached');
568
+ }
569
+
570
+ const default_submitter = /** @type {HTMLElement | undefined} */ (
571
+ element.querySelector('button:not([type]), [type="submit"]')
572
+ );
573
+
574
+ const form_data = new FormData(element, default_submitter);
575
+
576
+ if (DEV) {
577
+ validate_form_data(form_data, clone(element).enctype);
578
+ }
579
+
580
+ submitted = true;
581
+ pending_count++;
582
+
583
+ const submission = submit(form_data, true);
584
+
585
+ void submission.finally(() => {
586
+ pending_count--;
587
+ });
588
+
589
+ return submission;
590
+ }
591
+ },
552
592
  fields: {
553
593
  get: () =>
554
594
  create_field_proxy(
@@ -659,13 +699,12 @@ export function form(id) {
659
699
  }
660
700
  },
661
701
  enhance: {
662
- /** @type {RemoteForm<any, any>['enhance']} */
702
+ /**
703
+ * @param {(instance: Omit<RemoteForm<T, U>, 'enhance' | 'element'> & { readonly element: HTMLFormElement }) => any} callback
704
+ */
663
705
  value: (callback) => {
664
- return {
665
- method: 'POST',
666
- action,
667
- [createAttachmentKey()]: create_attachment(form_onsubmit(callback))
668
- };
706
+ enhance_callback = callback;
707
+ return instance;
669
708
  }
670
709
  }
671
710
  });
@@ -1,6 +1,6 @@
1
1
  export { command } from './command.svelte.js';
2
2
  export { form } from './form.svelte.js';
3
3
  export { prerender } from './prerender.svelte.js';
4
- export { query } from './query.svelte.js';
4
+ export { query } from './query/index.js';
5
5
  export { query_batch } from './query-batch.svelte.js';
6
- export { query_live } from './query-live.svelte.js';
6
+ export { query_live } from './query-live/index.js';
@@ -2,13 +2,12 @@
2
2
  import { app_dir, base } from '$app/paths/internal/client';
3
3
  import { version } from '__sveltekit/environment';
4
4
  import * as devalue from 'devalue';
5
- import { DEV } from 'esm-env';
6
5
  import { app, prerender_responses } from '../client.js';
7
6
  import { get_remote_request_headers, remote_request } from './shared.svelte.js';
8
7
  import { create_remote_key, stringify_remote_arg } from '../../shared.js';
9
8
 
10
9
  // Initialize Cache API for prerender functions
11
- const CACHE_NAME = DEV ? `sveltekit:${Date.now()}` : `sveltekit:${version}`;
10
+ const CACHE_NAME = __SVELTEKIT_DEV__ ? `sveltekit:${Date.now()}` : `sveltekit:${version}`;
12
11
  /** @type {Cache | undefined} */
13
12
  let prerender_cache;
14
13
 
@@ -0,0 +1,4 @@
1
+ import { query_map } from '../../client.js';
2
+ import { CacheController } from '../cache.svelte.js';
3
+
4
+ export const cache = new CacheController(query_map);
@@ -0,0 +1,48 @@
1
+ /** @import { RemoteQueryFunction } from '@sveltejs/kit' */
2
+ import { app_dir, base } from '$app/paths/internal/client';
3
+ import { app, query_map, query_responses } from '../../client.js';
4
+ import { get_remote_request_headers, QUERY_FUNCTION_ID, remote_request } from '../shared.svelte.js';
5
+ import * as devalue from 'devalue';
6
+ import { DEV } from 'esm-env';
7
+ import { unfriendly_hydratable } from '../../../shared.js';
8
+ import { QueryProxy } from './proxy.js';
9
+
10
+ /**
11
+ * @param {string} id
12
+ * @returns {RemoteQueryFunction<any, any>}
13
+ */
14
+ export function query(id) {
15
+ if (DEV) {
16
+ // If this reruns as part of HMR, refresh all live entries.
17
+ const entries = query_map.get(id);
18
+
19
+ if (entries) {
20
+ for (const { resource } of entries.values()) {
21
+ void resource.refresh();
22
+ }
23
+ }
24
+ }
25
+
26
+ /** @type {RemoteQueryFunction<any, any>} */
27
+ const wrapper = (arg) => {
28
+ return new QueryProxy(id, arg, async (key, payload) => {
29
+ if (Object.hasOwn(query_responses, key)) {
30
+ const value = query_responses[key];
31
+ delete query_responses[key];
32
+ return value;
33
+ }
34
+
35
+ const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
36
+
37
+ const serialized = await unfriendly_hydratable(key, () =>
38
+ remote_request(url, get_remote_request_headers())
39
+ );
40
+
41
+ return devalue.parse(serialized, app.decoders);
42
+ });
43
+ };
44
+
45
+ Object.defineProperty(wrapper, QUERY_FUNCTION_ID, { value: id });
46
+
47
+ return wrapper;
48
+ }