@sveltejs/kit 3.0.0-next.4 → 3.0.0-next.6

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 (68) hide show
  1. package/package.json +17 -17
  2. package/src/core/adapt/builder.js +18 -4
  3. package/src/core/adapt/index.js +1 -4
  4. package/src/core/config/index.js +64 -5
  5. package/src/core/config/options.js +73 -21
  6. package/src/core/env.js +35 -4
  7. package/src/core/postbuild/analyse.js +8 -12
  8. package/src/core/postbuild/crawl.js +22 -6
  9. package/src/core/postbuild/prerender.js +40 -23
  10. package/src/core/sync/create_manifest_data/index.js +23 -5
  11. package/src/core/sync/write_client_manifest.js +7 -0
  12. package/src/core/sync/write_server.js +13 -10
  13. package/src/core/sync/write_tsconfig.js +2 -4
  14. package/src/exports/index.js +32 -10
  15. package/src/exports/internal/env.js +1 -1
  16. package/src/exports/internal/index.js +6 -5
  17. package/src/exports/node/index.js +57 -6
  18. package/src/exports/public.d.ts +218 -114
  19. package/src/exports/vite/build/build_server.js +6 -1
  20. package/src/exports/vite/dev/index.js +10 -20
  21. package/src/exports/vite/index.js +306 -222
  22. package/src/exports/vite/preview/index.js +15 -7
  23. package/src/exports/vite/utils.js +8 -10
  24. package/src/runtime/app/env/types.d.ts +1 -1
  25. package/src/runtime/app/forms.js +22 -5
  26. package/src/runtime/app/paths/client.js +1 -3
  27. package/src/runtime/app/paths/public.d.ts +0 -28
  28. package/src/runtime/app/paths/server.js +7 -3
  29. package/src/runtime/app/server/remote/form.js +10 -3
  30. package/src/runtime/app/server/remote/query.js +3 -6
  31. package/src/runtime/app/server/remote/requested.js +8 -4
  32. package/src/runtime/app/server/remote/shared.js +5 -7
  33. package/src/runtime/client/client.js +184 -93
  34. package/src/runtime/client/fetcher.js +3 -2
  35. package/src/runtime/client/remote-functions/form.svelte.js +134 -24
  36. package/src/runtime/client/remote-functions/prerender.svelte.js +8 -2
  37. package/src/runtime/client/remote-functions/query/index.js +3 -2
  38. package/src/runtime/client/remote-functions/query/instance.svelte.js +9 -2
  39. package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
  40. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +26 -9
  41. package/src/runtime/client/remote-functions/shared.svelte.js +17 -7
  42. package/src/runtime/client/types.d.ts +9 -1
  43. package/src/runtime/client/utils.js +1 -1
  44. package/src/runtime/form-utils.js +84 -16
  45. package/src/runtime/server/cookie.js +22 -33
  46. package/src/runtime/server/csrf.js +65 -0
  47. package/src/runtime/server/data/index.js +7 -7
  48. package/src/runtime/server/env_module.js +0 -5
  49. package/src/runtime/server/index.js +1 -1
  50. package/src/runtime/server/page/actions.js +41 -26
  51. package/src/runtime/server/page/index.js +2 -1
  52. package/src/runtime/server/page/render.js +21 -23
  53. package/src/runtime/server/page/respond_with_error.js +7 -8
  54. package/src/runtime/server/remote.js +64 -27
  55. package/src/runtime/server/respond.js +81 -50
  56. package/src/runtime/server/utils.js +7 -7
  57. package/src/runtime/telemetry/otel.js +1 -1
  58. package/src/types/ambient.d.ts +5 -4
  59. package/src/types/global-private.d.ts +4 -4
  60. package/src/types/internal.d.ts +8 -10
  61. package/src/types/private.d.ts +33 -1
  62. package/src/types/synthetic/$lib.md +1 -1
  63. package/src/utils/error.js +11 -3
  64. package/src/utils/shared-iterator.js +5 -0
  65. package/src/utils/url.js +99 -2
  66. package/src/version.js +1 -1
  67. package/types/index.d.ts +340 -186
  68. package/types/index.d.ts.map +10 -9
@@ -94,8 +94,6 @@ export function initial_fetch(resource, opts) {
94
94
  script.remove(); // In case multiple script tags match the same selector
95
95
  let { body, ...init } = JSON.parse(script.textContent);
96
96
 
97
- const ttl = script.getAttribute('data-ttl');
98
- if (ttl) cache.set(selector, { body, init, ttl: 1000 * Number(ttl) });
99
97
  const b64 = script.getAttribute('data-b64');
100
98
  if (b64 !== null) {
101
99
  // Can't use native_fetch('data:...;base64,${body}')
@@ -103,6 +101,9 @@ export function initial_fetch(resource, opts) {
103
101
  body = base64_decode(body);
104
102
  }
105
103
 
104
+ const ttl = script.getAttribute('data-ttl');
105
+ if (ttl) cache.set(selector, { body, init, ttl: 1000 * Number(ttl) });
106
+
106
107
  return Promise.resolve(new Response(body, init));
107
108
  }
108
109
 
@@ -4,7 +4,13 @@
4
4
  import { app_dir, base } from '$app/paths/internal/client';
5
5
  import { DEV } from 'esm-env';
6
6
  import { HttpError } from '@sveltejs/kit/internal';
7
- import { query_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
7
+ import {
8
+ query_responses,
9
+ _goto,
10
+ set_nearest_error_page,
11
+ invalidateAll,
12
+ handle_error
13
+ } from '../client.js';
8
14
  import { tick } from 'svelte';
9
15
  import { categorize_updates, remote_request } from './shared.svelte.js';
10
16
  import { createAttachmentKey } from 'svelte/attachments';
@@ -17,7 +23,8 @@ import {
17
23
  build_path_string,
18
24
  normalize_issue,
19
25
  serialize_binary_form,
20
- BINARY_FORM_CONTENT_TYPE
26
+ BINARY_FORM_CONTENT_TYPE,
27
+ deep_get
21
28
  } from '../../form-utils.js';
22
29
 
23
30
  /**
@@ -52,6 +59,9 @@ export function form(id) {
52
59
  /** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
53
60
  const instances = new Map();
54
61
 
62
+ /** @type {StandardSchemaV1 | null} */
63
+ let shared_preflight_schema = null;
64
+
55
65
  /** @param {string | number | boolean} [key] */
56
66
  function create_instance(key) {
57
67
  const action_id_without_key = id;
@@ -89,7 +99,9 @@ export function form(id) {
89
99
  */
90
100
  let enhance_callback = async (instance) => {
91
101
  if (await instance.submit()) {
92
- instance.element.reset();
102
+ await tick();
103
+ // We call reset from the prototype to avoid DOM clobbering
104
+ HTMLFormElement.prototype.reset.call(instance.element);
93
105
  }
94
106
  };
95
107
 
@@ -97,13 +109,22 @@ export function form(id) {
97
109
  let element = null;
98
110
 
99
111
  /** @type {Record<string, boolean>} */
100
- let touched = {};
112
+ let touched = $state({});
113
+
114
+ /** @type {Record<string, boolean>} */
115
+ let dirty = $state({});
116
+
117
+ /** @type {Record<string, boolean>} */
118
+ let can_validate = {};
101
119
 
102
- let submitted = false;
120
+ let submitted = $state(false);
103
121
 
104
122
  /** @type {InternalRemoteFormIssue[] | null} */
105
123
  let unread_issues = null;
106
124
 
125
+ /** @type {string | null} */
126
+ let previous_submitter_name = null;
127
+
107
128
  /**
108
129
  * In dev, warn if there are validation issues going unread
109
130
  */
@@ -302,7 +323,8 @@ export function form(id) {
302
323
  */
303
324
  async function preflight(form_data) {
304
325
  const data = convert(form_data);
305
- const validated = await preflight_schema?.['~standard'].validate(data);
326
+ const schema = preflight_schema ?? shared_preflight_schema;
327
+ const validated = await schema?.['~standard'].validate(data);
306
328
 
307
329
  if (validated?.issues) {
308
330
  raw_issues = merge_with_server_issues(
@@ -346,6 +368,8 @@ export function form(id) {
346
368
  element = form;
347
369
 
348
370
  touched = {};
371
+ dirty = {};
372
+ can_validate = {};
349
373
 
350
374
  /** @param {SubmitEvent} event */
351
375
  const handle_submit = async (event) => {
@@ -379,6 +403,29 @@ export function form(id) {
379
403
 
380
404
  const form_data = new FormData(form, event.submitter);
381
405
 
406
+ if (
407
+ previous_submitter_name !== null &&
408
+ !Array.from(form_data.keys()).map(strip_prefix).includes(previous_submitter_name)
409
+ ) {
410
+ // Strip any `n:`/`b:` type prefix before clearing, otherwise
411
+ // `set_nested_value` would coerce `undefined` to `NaN`/`false`
412
+ // instead of clearing the previously-submitted value.
413
+ set_nested_value(input, previous_submitter_name, undefined);
414
+ }
415
+
416
+ if (event.submitter) {
417
+ const name = event.submitter.getAttribute('name');
418
+ const value = /** @type {any} */ (event.submitter).value;
419
+
420
+ if (name !== null && value !== undefined) {
421
+ set_nested_value(input, name, value);
422
+ }
423
+
424
+ previous_submitter_name = strip_prefix(name);
425
+ } else {
426
+ previous_submitter_name = null;
427
+ }
428
+
382
429
  if (DEV) {
383
430
  validate_form_data(form_data, clone(form).enctype);
384
431
  }
@@ -396,9 +443,14 @@ export function form(id) {
396
443
  await enhance_callback(create_enhance_callback_instance(form, form_data));
397
444
  } catch (e) {
398
445
  const error =
399
- e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
400
- const status = e instanceof HttpError ? e.status : 500;
401
- void set_nearest_error_page(error, status);
446
+ e instanceof HttpError
447
+ ? e.body
448
+ : await handle_error(e, {
449
+ params: {},
450
+ route: { id: null },
451
+ url: new URL(location.href)
452
+ });
453
+ void set_nearest_error_page(error);
402
454
  } finally {
403
455
  pending_count--;
404
456
  }
@@ -418,8 +470,6 @@ export function form(id) {
418
470
 
419
471
  const is_file = element.type === 'file';
420
472
 
421
- touched[name] = true;
422
-
423
473
  if (is_array) {
424
474
  let value;
425
475
 
@@ -481,9 +531,9 @@ export function form(id) {
481
531
  );
482
532
  }
483
533
 
484
- name = name.replace(/^[nb]:/, '');
534
+ name = strip_prefix(name);
485
535
 
486
- touched[name] = true;
536
+ dirty[name] = true;
487
537
  };
488
538
 
489
539
  const handle_reset = async () => {
@@ -492,18 +542,38 @@ export function form(id) {
492
542
  await tick();
493
543
 
494
544
  input = convert_formdata(new FormData(form));
545
+ raw_issues = [];
546
+ touched = {};
547
+ dirty = {};
548
+ can_validate = {};
549
+ submitted = false;
550
+ };
551
+
552
+ /** @param {Event} e */
553
+ const handle_focusout = (e) => {
554
+ let name = /** @type {HTMLInputElement} */ (e.target).name;
555
+ if (!name) return;
556
+
557
+ name = strip_prefix(name).replace(/\[\]$/, '');
558
+
559
+ touched[name] = true;
560
+
561
+ if (Object.hasOwn(dirty, name)) {
562
+ can_validate[name] = true;
563
+ }
495
564
  };
496
565
 
497
566
  form.addEventListener('submit', handle_submit);
498
567
  form.addEventListener('input', handle_input);
568
+ form.addEventListener('focusout', handle_focusout);
499
569
  form.addEventListener('reset', handle_reset);
500
570
 
501
571
  return () => {
502
572
  form.removeEventListener('submit', handle_submit);
503
573
  form.removeEventListener('input', handle_input);
574
+ form.removeEventListener('focusout', handle_focusout);
504
575
  form.removeEventListener('reset', handle_reset);
505
576
  element = null;
506
- preflight_schema = undefined;
507
577
  };
508
578
  };
509
579
 
@@ -534,9 +604,10 @@ export function form(id) {
534
604
 
535
605
  const submission = submit(form_data, true);
536
606
 
537
- void submission.finally(() => {
607
+ const decrement = () => {
538
608
  pending_count--;
539
- });
609
+ };
610
+ void submission.then(decrement, decrement);
540
611
 
541
612
  return submission;
542
613
  }
@@ -549,11 +620,16 @@ export function form(id) {
549
620
  (path, value) => {
550
621
  if (path.length === 0) {
551
622
  input = value;
552
- } else {
623
+ } else if (value !== deep_get(input, path)) {
553
624
  deep_set(input, path.map(String), value);
554
625
 
555
626
  const key = build_path_string(path);
556
- touched[key] = true;
627
+
628
+ if (element) {
629
+ touched[key] = true;
630
+ dirty[key] = true;
631
+ can_validate[key] = true;
632
+ }
557
633
  }
558
634
  },
559
635
  (path, all) => {
@@ -567,7 +643,10 @@ export function form(id) {
567
643
  }
568
644
 
569
645
  return issues;
570
- }
646
+ },
647
+ () => touched,
648
+ () => dirty,
649
+ []
571
650
  )
572
651
  },
573
652
  result: {
@@ -576,16 +655,29 @@ export function form(id) {
576
655
  pending: {
577
656
  get: () => pending_count
578
657
  },
658
+ submitted: {
659
+ get: () => submitted
660
+ },
579
661
  preflight: {
580
662
  /** @type {RemoteForm<T, U>['preflight']} */
581
663
  value: (schema) => {
582
664
  preflight_schema = schema;
665
+
666
+ if (key === undefined) {
667
+ shared_preflight_schema = schema;
668
+ }
669
+
583
670
  return instance;
584
671
  }
585
672
  },
586
673
  validate: {
587
674
  /** @type {RemoteForm<any, any>['validate']} */
588
- value: async ({ includeUntouched = false, preflightOnly = false } = {}) => {
675
+ value: async ({
676
+ all = false,
677
+ preflightOnly = false,
678
+ // @ts-expect-error TODO remove this in 3.0
679
+ includeUntouched
680
+ } = {}) => {
589
681
  if (!element) return;
590
682
 
591
683
  const id = ++validate_id;
@@ -603,8 +695,8 @@ export function form(id) {
603
695
  let array = [];
604
696
 
605
697
  const data = convert(form_data);
606
-
607
- const validated = await preflight_schema?.['~standard'].validate(data);
698
+ const schema = preflight_schema ?? shared_preflight_schema;
699
+ const validated = await schema?.['~standard'].validate(data);
608
700
 
609
701
  if (validate_id !== id) {
610
702
  return;
@@ -636,8 +728,16 @@ export function form(id) {
636
728
  array = /** @type {InternalRemoteFormIssue[]} */ (result._);
637
729
  }
638
730
 
639
- if (!includeUntouched && !submitted) {
640
- array = array.filter((issue) => touched[issue.name]);
731
+ if (includeUntouched !== undefined) {
732
+ console.warn(
733
+ `\`{ includeUntouched: ${includeUntouched} }\` has been replaced with \`{ all: ${includeUntouched} }\``
734
+ );
735
+
736
+ all = includeUntouched;
737
+ }
738
+
739
+ if (!all && !submitted) {
740
+ array = array.filter((issue) => can_validate[issue.name]);
641
741
  }
642
742
 
643
743
  const is_server_validation = !validated?.issues && !preflightOnly;
@@ -728,3 +828,13 @@ function validate_form_data(form_data, enctype) {
728
828
  }
729
829
  }
730
830
  }
831
+
832
+ /**
833
+ * Remove the `n:` or `b:` prefix from a field name
834
+ * @template {string | null} T
835
+ * @param {T} name
836
+ * @returns {T}
837
+ */
838
+ function strip_prefix(name) {
839
+ return /** @type {T} */ (name && name.replace(/^[nb]:/, ''));
840
+ }
@@ -2,9 +2,10 @@
2
2
  import { app_dir, base } from '$app/paths/internal/client';
3
3
  import { version } from '$app/env';
4
4
  import * as devalue from 'devalue';
5
- import { app, goto, prerender_responses } from '../client.js';
5
+ import { app, _goto, prerender_responses } from '../client.js';
6
6
  import { get_remote_request_headers, remote_request, unwrap_node } from './shared.svelte.js';
7
7
  import { create_remote_key, stringify_remote_arg } from '../../shared.js';
8
+ import { noop } from '../../../utils/functions.js';
8
9
 
9
10
  // Initialize Cache API for prerender functions
10
11
  const CACHE_NAME = __SVELTEKIT_DEV__ ? `sveltekit:${Date.now()}` : `sveltekit:${version}`;
@@ -97,7 +98,8 @@ export function prerender(id) {
97
98
  const result = await remote_request(url, { headers });
98
99
 
99
100
  if (result.redirect) {
100
- void goto(result.redirect);
101
+ // Use internal version to allow redirects to external URLs
102
+ void _goto(result.redirect, {}, 0);
101
103
  return;
102
104
  }
103
105
 
@@ -166,6 +168,10 @@ class Prerender {
166
168
  throw error;
167
169
  }
168
170
  );
171
+
172
+ // rejections are surfaced via `.error` for reactive consumers — make sure the
173
+ // stored promise (consumed without `await`) never becomes an unhandled rejection
174
+ this.#promise.catch(noop);
169
175
  }
170
176
 
171
177
  /**
@@ -1,6 +1,6 @@
1
1
  /** @import { RemoteQueryFunction } from '@sveltejs/kit' */
2
2
  import { app_dir, base } from '$app/paths/internal/client';
3
- import { goto, query_map } from '../../client.js';
3
+ import { _goto, query_map } from '../../client.js';
4
4
  import { get_remote_request_headers, QUERY_FUNCTION_ID, remote_request } from '../shared.svelte.js';
5
5
  import { DEV } from 'esm-env';
6
6
  import { QueryProxy } from './proxy.js';
@@ -29,7 +29,8 @@ export function query(id) {
29
29
  const result = await remote_request(url, { headers: get_remote_request_headers() });
30
30
 
31
31
  if (result.redirect) {
32
- await goto(result.redirect);
32
+ // Use internal version to allow redirects to external URLs
33
+ await _goto(result.redirect, {}, 0);
33
34
  }
34
35
  });
35
36
  };
@@ -70,7 +70,7 @@ export class Query {
70
70
  delete query_responses[key];
71
71
 
72
72
  if (node.e) {
73
- this.fail(new HttpError(node.e[0] ?? 500, node.e[1]));
73
+ this.fail(new HttpError(node.e.status, node.e));
74
74
  } else {
75
75
  this.set(/** @type {T} */ (node.v));
76
76
  }
@@ -87,7 +87,9 @@ export class Query {
87
87
  // every time you see this comment, try removing the `tick.then` here and see
88
88
  // if all the tests still pass with the latest svelte version
89
89
  // if they do, congrats, you can remove tick.then
90
- void tick().then(() => this.#get_promise());
90
+ void tick()
91
+ .then(() => this.#get_promise())
92
+ .catch(noop);
91
93
  }
92
94
 
93
95
  #clear_pending() {
@@ -100,6 +102,11 @@ export class Query {
100
102
 
101
103
  const { promise, resolve, reject } = Promise.withResolvers();
102
104
 
105
+ // the rejection is surfaced via `.error` / the `then` getter for awaiting
106
+ // consumers — a purely reactive consumer (`.current`) attaches no handler,
107
+ // so make sure the stored promise can never become an unhandled rejection
108
+ promise.catch(noop);
109
+
103
110
  this.#latest.push(resolve);
104
111
 
105
112
  Promise.resolve(this.#fn())
@@ -1,6 +1,6 @@
1
1
  /** @import { RemoteQueryFunction } from '@sveltejs/kit' */
2
2
  import { app_dir, base } from '$app/paths/internal/client';
3
- import { goto } from '../client.js';
3
+ import { _goto } from '../client.js';
4
4
  import { get_remote_request_headers, QUERY_FUNCTION_ID, remote_request } from './shared.svelte.js';
5
5
  import { QueryProxy } from './query/proxy.js';
6
6
  import { HttpError } from '@sveltejs/kit/internal';
@@ -50,7 +50,8 @@ export function query_batch(id) {
50
50
  });
51
51
 
52
52
  if (response.redirect) {
53
- await goto(response.redirect);
53
+ // Use internal version to allow redirects to external URLs
54
+ await _goto(response.redirect, {}, 0);
54
55
 
55
56
  // settle all batched promises (with `undefined`, like a redirect
56
57
  // from a non-batched query) so that callers don't hang forever
@@ -71,7 +72,7 @@ export function query_batch(id) {
71
72
 
72
73
  for (const { resolve, reject } of resolvers) {
73
74
  if (result.type === 'error') {
74
- reject(new HttpError(result.status, result.error));
75
+ reject(new HttpError(result.error.status, result.error));
75
76
  } else {
76
77
  resolve(result.data);
77
78
  }
@@ -91,7 +91,7 @@ export class LiveQuery {
91
91
  // the query failed during SSR — seed the failed state (mirroring `fail()`,
92
92
  // minus its terminal `#done`), so the main loop still connects as usual
93
93
  // and the query can recover
94
- const error = new HttpError(node.e[0] ?? 500, node.e[1]);
94
+ const error = new HttpError(node.e.status, node.e);
95
95
  this.#loading = false;
96
96
  this.#error = error;
97
97
 
@@ -122,6 +122,7 @@ export class LiveQuery {
122
122
 
123
123
  /** @type {PromiseWithResolvers<void>} */
124
124
  const { promise: stopped, resolve: on_stop } = Promise.withResolvers();
125
+ let connected = false;
125
126
 
126
127
  while (!this.#done) {
127
128
  const controller = new AbortController();
@@ -134,16 +135,22 @@ export class LiveQuery {
134
135
  const generator = create_live_iterator(this.#id, this.#payload, controller, () => {
135
136
  this.#connected = true;
136
137
  this.#attempt = 0;
138
+ connected = true;
137
139
  on_connect();
138
140
  });
139
141
 
140
142
  try {
141
143
  const { done, value } = await generator.next();
142
144
 
143
- // TODO how much special handling does this need?
144
- // should we even try to reconnect if this is the case?
145
- if (done && !this.#ready) {
146
- throw new Error('Live query completed before yielding a value');
145
+ if (done) {
146
+ if (!this.#ready) {
147
+ throw new Error('Live query completed before yielding a value');
148
+ }
149
+ // stream completed without yielding (e.g. the generator returned
150
+ // immediately on reconnect) — keep the last good value
151
+ this.#done = true;
152
+ this.#fan_out.done();
153
+ break;
147
154
  }
148
155
 
149
156
  this.set(value);
@@ -200,6 +207,12 @@ export class LiveQuery {
200
207
  }
201
208
 
202
209
  this.#interrupt = null;
210
+ // If the loop exited without ever successfully connecting, settle the
211
+ // reconnect handshake so callers (e.g. `invalidateAll()`) never await
212
+ // a forever-pending promise.
213
+ if (!connected) {
214
+ on_connect_failed(this.#error ?? new Error('Live query connection was interrupted'));
215
+ }
203
216
  on_stop();
204
217
  }
205
218
 
@@ -343,10 +356,13 @@ export class LiveQuery {
343
356
  promise.catch(noop);
344
357
  this.#done = false;
345
358
  this.#attempt = 0;
346
- // The previous fan-out may have been closed by `done()`/`fail()`. Future
347
- // `for await` consumers need a fresh, open fan-out attached to the new
348
- // `#main` lifetime.
349
- this.#fan_out = new SharedIterator();
359
+ // Keep the existing fan-out open so active `for await` consumers
360
+ // continue receiving values from the new connection without interruption.
361
+ // Only replace it if it was already closed by a prior `done()`/`fail()`
362
+ // (e.g. reconnecting after a finite or hard-failed stream)
363
+ if (this.#fan_out.closed) {
364
+ this.#fan_out = new SharedIterator();
365
+ }
350
366
  this.#main({ on_connect, on_connect_failed }).catch(noop);
351
367
  await promise;
352
368
  }
@@ -381,6 +397,7 @@ export class LiveQuery {
381
397
  void this.#interrupt?.();
382
398
 
383
399
  if (this.#reject_first) {
400
+ this.#promise.catch(noop);
384
401
  this.#reject_first(error);
385
402
  this.#resolve_first = null;
386
403
  this.#reject_first = null;
@@ -2,7 +2,7 @@
2
2
  /** @import { RemoteQueryUpdate } from '@sveltejs/kit' */
3
3
  /** @import { CacheEntry } from './cache.svelte.js' */
4
4
  import * as devalue from 'devalue';
5
- import { app, goto, live_query_map, query_map, query_responses } from '../client.js';
5
+ import { app, _goto, live_query_map, query_map, query_responses } from '../client.js';
6
6
  import { HttpError, Redirect } from '@sveltejs/kit/internal';
7
7
  import { untrack } from 'svelte';
8
8
  import { create_remote_key, split_remote_key } from '../../shared.js';
@@ -86,7 +86,7 @@ export function pin_while_resolving(cache_map, cache, id, payload, then) {
86
86
  */
87
87
  export function unwrap_node(node) {
88
88
  if (node.e) {
89
- throw new HttpError(node.e[0] ?? 500, node.e[1]);
89
+ throw new HttpError(node.e.status, node.e);
90
90
  }
91
91
 
92
92
  return node.v;
@@ -114,13 +114,22 @@ export async function remote_request(url, init) {
114
114
  const response = await fetch(url, init);
115
115
 
116
116
  if (!response.ok) {
117
- throw new HttpError(500, 'Failed to execute remote function');
117
+ const result = await response.json().catch(() => ({
118
+ type: 'error',
119
+ status: response.status,
120
+ error: response.statusText
121
+ }));
122
+
123
+ throw new HttpError(
124
+ result.error?.status ?? result.status ?? response.status ?? 500,
125
+ result.error
126
+ );
118
127
  }
119
128
 
120
129
  const result = /** @type {RemoteFunctionResponse} */ (await response.json());
121
130
 
122
131
  if (result.type === 'error') {
123
- throw new HttpError(result.status ?? 500, result.error);
132
+ throw new HttpError(result.error.status, result.error);
124
133
  }
125
134
 
126
135
  const data = /** @type {RemoteFunctionData} */ (
@@ -136,7 +145,7 @@ export async function remote_request(url, init) {
136
145
  function refresh(key, entry, result) {
137
146
  if (entry?.resource) {
138
147
  if (result.e) {
139
- entry.resource.fail(new HttpError(result.e[0] ?? 500, result.e[1]));
148
+ entry.resource.fail(new HttpError(result.e.status, result.e));
140
149
  } else {
141
150
  entry.resource.set(result.v);
142
151
  }
@@ -184,12 +193,13 @@ export async function remote_request(url, init) {
184
193
  */
185
194
  export async function handle_side_channel_response(response) {
186
195
  if (response.type === 'redirect') {
187
- await goto(response.location);
196
+ // Use internal version to allow redirects to external URLs
197
+ await _goto(response.location, {}, 0);
188
198
  throw new Redirect(307, response.location);
189
199
  }
190
200
 
191
201
  if (response.type === 'error') {
192
- throw new HttpError(response.status ?? 500, response.error);
202
+ throw new HttpError(response.error.status, response.error);
193
203
  }
194
204
 
195
205
  return response;
@@ -58,6 +58,12 @@ export interface SvelteKitApp {
58
58
  hash: boolean;
59
59
 
60
60
  root: typeof SvelteComponent;
61
+
62
+ /**
63
+ * Lazily loads the contents of src/error.html, used as a last-resort
64
+ * error page when the root layout's load function throws during client-side rendering.
65
+ */
66
+ get_error_template: () => Promise<(data: { status: number; message: string }) => string>;
61
67
  }
62
68
 
63
69
  export type NavigationIntent = {
@@ -77,6 +83,7 @@ export type NavigationResult = NavigationRedirect | NavigationFinished;
77
83
 
78
84
  export type NavigationRedirect = {
79
85
  type: 'redirect';
86
+ status: number;
80
87
  location: string;
81
88
  };
82
89
 
@@ -119,7 +126,8 @@ export interface NavigationState {
119
126
  }
120
127
 
121
128
  export interface HydrateOptions {
122
- status: number;
129
+ /** Provided in the case of a form action that returns `fail`, but otherwise derived from `error` */
130
+ status?: number;
123
131
  error: App.Error | null;
124
132
  node_ids: number[];
125
133
  params: Record<string, string>;
@@ -1,6 +1,6 @@
1
1
  import { BROWSER, DEV } from 'esm-env';
2
2
  import { writable } from 'svelte/store';
3
- import { assets } from '$app/paths';
3
+ import { assets } from '$app/paths/internal/client';
4
4
  import { version } from '$app/env';
5
5
  import { noop } from '../../utils/functions.js';
6
6
  import { PRELOAD_PRIORITIES } from './constants.js';