@sveltejs/kit 3.0.0-next.16 → 3.0.0-next.18

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.
@@ -29,6 +29,14 @@ import {
29
29
  PRELOAD_PRIORITIES,
30
30
  SNAPSHOT_KEY
31
31
  } from './constants.js';
32
+ import {
33
+ capture_navigation_snapshot,
34
+ current_registrations,
35
+ delete_navigation_snapshot,
36
+ init_snapshots,
37
+ persist_navigation_snapshots,
38
+ restore_navigation_snapshot
39
+ } from './snapshots.js';
32
40
  import { validate_page_exports } from '../../utils/exports.js';
33
41
  import { noop } from '../../utils/functions.js';
34
42
  import {
@@ -91,9 +99,12 @@ const history_info = storage.get(HISTORY_INFO_KEY) ?? {};
91
99
  /**
92
100
  * navigation index -> any
93
101
  * @type {Record<string, any[]>}
102
+ * @deprecated TODO 4.0 get rid of this
94
103
  */
95
104
  const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
96
105
 
106
+ init_snapshots(() => (started && !is_navigating && !updating ? current_history_index : null));
107
+
97
108
  /** @type {Props} */
98
109
  let props;
99
110
 
@@ -203,9 +214,12 @@ function reset_scroll_and_focus(url, scroll, reset, active_element) {
203
214
  function clear_onward_history(current_history_index, current_navigation_index) {
204
215
  // if we navigated back, then pushed a new state, we can
205
216
  // release memory by pruning the scroll/snapshot lookup
217
+ // the new entry reuses the first abandoned index, so its slot must start clean
218
+ delete_navigation_snapshot(current_history_index);
206
219
  let i = current_history_index + 1;
207
220
  while (history_info[i]) {
208
221
  delete history_info[i];
222
+ delete_navigation_snapshot(i);
209
223
  i += 1;
210
224
  }
211
225
 
@@ -673,14 +687,28 @@ function reset_invalidation() {
673
687
  force_invalidation = false;
674
688
  }
675
689
 
676
- /** @param {number} index */
690
+ let warned_snapshot_export = false;
691
+
692
+ /**
693
+ * @param {number} index
694
+ * @deprecated TODO 4.0 get rid of this
695
+ */
677
696
  function capture_snapshot(index) {
678
697
  if (props.components.some((c) => c?.snapshot)) {
698
+ if (DEV && !warned_snapshot_export) {
699
+ warned_snapshot_export = true;
700
+ console.warn(
701
+ '`export const snapshot` is deprecated. Use the `snapshot` helper from `$app/navigation` instead.'
702
+ );
703
+ }
679
704
  snapshots[index] = props.components.map((c) => c?.snapshot?.capture());
680
705
  }
681
706
  }
682
707
 
683
- /** @param {number} index */
708
+ /**
709
+ * @param {number} index
710
+ * @deprecated TODO 4.0 get rid of this
711
+ */
684
712
  function restore_snapshot(index) {
685
713
  snapshots[index]?.forEach((value, i) => {
686
714
  props.components[i]?.snapshot?.restore(value);
@@ -693,11 +721,13 @@ function persist_state() {
693
721
 
694
722
  capture_snapshot(current_navigation_index);
695
723
  storage.set(SNAPSHOT_KEY, snapshots);
724
+
725
+ persist_navigation_snapshots(current_history_index);
696
726
  }
697
727
 
698
728
  /**
699
729
  * @param {string | URL} url
700
- * @param {{ type?: import('@sveltejs/kit').NavigationType; replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean; event?: Event }} [options]
730
+ * @param {{ type?: import('@sveltejs/kit').NavigationType; replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean; event?: Event; action_result?: import('@sveltejs/kit').ActionResult }} [options]
701
731
  * @param {number} [redirect_count]
702
732
  * @param {{}} [nav_token]
703
733
  * @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
@@ -723,6 +753,7 @@ export async function _goto(url, options = {}, redirect_count = 0, nav_token = {
723
753
  state: options.state,
724
754
  persist_state: options.persistState,
725
755
  event: options.event,
756
+ action_result: options.action_result,
726
757
  redirect_count,
727
758
  nav_token,
728
759
  intent,
@@ -947,6 +978,7 @@ async function initialize(result, target, should_hydrate) {
947
978
  }
948
979
 
949
980
  restore_snapshot(current_navigation_index);
981
+ restore_navigation_snapshot(current_history_index);
950
982
 
951
983
  started = true;
952
984
  }
@@ -1366,7 +1398,7 @@ function diff_search_params(old_url, new_url) {
1366
1398
 
1367
1399
  /**
1368
1400
  * @overload
1369
- * @param {import('./types.js').NavigationIntent} intent
1401
+ * @param {import('./types.js').NavigationIntent & { action_result?: import('@sveltejs/kit').ActionResult }} intent
1370
1402
  * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1371
1403
  */
1372
1404
  /**
@@ -1375,11 +1407,11 @@ function diff_search_params(old_url, new_url) {
1375
1407
  * @returns {Promise<import('./types.js').NavigationResult>}
1376
1408
  */
1377
1409
  /**
1378
- * @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
1410
+ * @param {import('./types.js').NavigationIntent & { preload?: {}; action_result?: import('@sveltejs/kit').ActionResult }} intent
1379
1411
  * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1380
1412
  */
1381
- async function load_route({ id, invalidating, url, params, route, preload }) {
1382
- if (load_cache?.id === id) {
1413
+ async function load_route({ id, invalidating, url, params, route, preload, action_result }) {
1414
+ if (!action_result && load_cache?.id === id) {
1383
1415
  // the preload becomes the real navigation
1384
1416
  preload_tokens.delete(load_cache.token);
1385
1417
  return load_cache.promise;
@@ -1388,6 +1420,8 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1388
1420
  const { errors, layouts, leaf } = route;
1389
1421
 
1390
1422
  const loaders = [...layouts, leaf];
1423
+ const leaf_index = loaders.length - 1;
1424
+ const action_error = action_result?.type === 'error';
1391
1425
 
1392
1426
  // preload modules to avoid waterfall, but handle rejections
1393
1427
  // so they don't get reported to Sentry et al (we don't need
@@ -1406,6 +1440,8 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1406
1440
 
1407
1441
  if (__SVELTEKIT_HAS_SERVER_LOAD__) {
1408
1442
  const invalid_server_nodes = loaders.map((loader, i) => {
1443
+ if (action_error && i === leaf_index) return false;
1444
+
1409
1445
  const previous = current.branch[i];
1410
1446
 
1411
1447
  const invalid =
@@ -1456,7 +1492,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1456
1492
  let parent_changed = false;
1457
1493
 
1458
1494
  const branch_promises = loaders.map(async (loader, i) => {
1459
- if (!loader) return;
1495
+ if (!loader || (action_error && i === leaf_index)) return;
1460
1496
 
1461
1497
  /** @type {import('./types.js').BranchNode | undefined} */
1462
1498
  const previous = current.branch[i];
@@ -1548,19 +1584,15 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1548
1584
  error = await handle_error(err, { params, url, route: { id: route.id } });
1549
1585
  }
1550
1586
 
1551
- const error_load = await load_nearest_error_page(i, branch, errors);
1552
- if (error_load) {
1553
- return get_navigation_result_from_branch({
1554
- url,
1555
- params,
1556
- branch: branch.slice(0, error_load.idx).concat(error_load.node),
1557
- errors,
1558
- error,
1559
- route
1560
- });
1561
- } else {
1562
- return await server_fallback(url, { id: route.id }, error);
1563
- }
1587
+ return load_route_error({
1588
+ i,
1589
+ branch,
1590
+ errors,
1591
+ error,
1592
+ url,
1593
+ params,
1594
+ route
1595
+ });
1564
1596
  }
1565
1597
  } else {
1566
1598
  // push an empty slot so we can rewind past gaps to the
@@ -1569,6 +1601,21 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1569
1601
  }
1570
1602
  }
1571
1603
 
1604
+ if (action_result?.type === 'error') {
1605
+ // render the nearest error boundary of the route the action belongs to,
1606
+ // as a native form submission would
1607
+ return load_route_error({
1608
+ i: loaders.length,
1609
+ branch,
1610
+ errors,
1611
+ error: action_result.error,
1612
+ status: action_result.status,
1613
+ url,
1614
+ params,
1615
+ route
1616
+ });
1617
+ }
1618
+
1572
1619
  return get_navigation_result_from_branch({
1573
1620
  url,
1574
1621
  params,
@@ -1576,8 +1623,15 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1576
1623
  errors,
1577
1624
  error: null,
1578
1625
  route,
1579
- // Reset `form` on navigation, but not invalidation
1580
- form: invalidating ? undefined : null
1626
+ status: action_result?.status,
1627
+ // `error` results returned above, `redirect` results are never passed here
1628
+ form:
1629
+ action_result?.type === 'success' || action_result?.type === 'failure'
1630
+ ? (action_result.data ?? null)
1631
+ : // Reset `form` on navigation, but not invalidation
1632
+ invalidating
1633
+ ? undefined
1634
+ : null
1581
1635
  });
1582
1636
  }
1583
1637
 
@@ -1610,6 +1664,35 @@ async function load_nearest_error_page(i, branch, errors) {
1610
1664
  }
1611
1665
  }
1612
1666
 
1667
+ /**
1668
+ * @param {{
1669
+ * i: number;
1670
+ * branch: Array<import('./types.js').BranchNode | undefined>;
1671
+ * errors: Array<import('types').CSRPageNodeLoader | undefined>;
1672
+ * error: App.Error;
1673
+ * status?: number;
1674
+ * url: URL;
1675
+ * params: Record<string, string>;
1676
+ * route: import('./types.js').NavigationIntent['route'];
1677
+ * }} opts
1678
+ */
1679
+ async function load_route_error({ i, branch, errors, error, status, url, params, route }) {
1680
+ const error_load = await load_nearest_error_page(i, branch, errors);
1681
+ if (error_load) {
1682
+ return get_navigation_result_from_branch({
1683
+ url,
1684
+ params,
1685
+ branch: branch.slice(0, error_load.idx).concat(error_load.node),
1686
+ errors,
1687
+ error,
1688
+ status,
1689
+ route
1690
+ });
1691
+ }
1692
+
1693
+ return server_fallback(url, { id: route.id }, error);
1694
+ }
1695
+
1613
1696
  /**
1614
1697
  * @param {{
1615
1698
  * error: App.Error;
@@ -1894,7 +1977,8 @@ function _before_navigate({ url, type, intent, delta, event, scroll, shallow = f
1894
1977
  * accept?: () => void;
1895
1978
  * block?: () => void;
1896
1979
  * event?: Event;
1897
- * intent?: NavigationIntent | undefined
1980
+ * intent?: NavigationIntent | undefined;
1981
+ * action_result?: import('@sveltejs/kit').ActionResult;
1898
1982
  * }} opts
1899
1983
  * @returns {Promise<void>}
1900
1984
  */
@@ -1911,7 +1995,8 @@ async function navigate({
1911
1995
  accept = noop,
1912
1996
  block = noop,
1913
1997
  event,
1914
- intent
1998
+ intent,
1999
+ action_result
1915
2000
  }) {
1916
2001
  const prev_token = navigation_token;
1917
2002
  const prev_invalidation_token = invalidation_token;
@@ -1943,6 +2028,7 @@ async function navigate({
1943
2028
  // store this before calling `accept()`, which may change the index
1944
2029
  const previous_history_index = current_history_index;
1945
2030
  const previous_navigation_index = current_navigation_index;
2031
+ const previous_snapshot_registrations = current_registrations();
1946
2032
 
1947
2033
  accept();
1948
2034
 
@@ -1952,7 +2038,7 @@ async function navigate({
1952
2038
  navigating.current = nav.navigation;
1953
2039
  }
1954
2040
 
1955
- let navigation_result = intent && (await load_route(intent));
2041
+ let navigation_result = intent && (await load_route({ ...intent, action_result }));
1956
2042
 
1957
2043
  if (!navigation_result) {
1958
2044
  if (is_external_url(url, base, app.hash)) {
@@ -2051,6 +2137,11 @@ async function navigate({
2051
2137
 
2052
2138
  capture_scroll(previous_history_index);
2053
2139
  capture_snapshot(previous_navigation_index);
2140
+ if (replace_state) {
2141
+ delete_navigation_snapshot(previous_history_index);
2142
+ } else {
2143
+ capture_navigation_snapshot(previous_history_index);
2144
+ }
2054
2145
 
2055
2146
  // ensure the url pathname matches the page's trailing slash option
2056
2147
  if (navigation_result.props.page.url.pathname !== url.pathname) {
@@ -2218,6 +2309,8 @@ async function navigate({
2218
2309
  if (type === 'popstate') {
2219
2310
  restore_snapshot(current_navigation_index);
2220
2311
  }
2312
+ // new and replaced entries have no stored values, so this only resets there
2313
+ restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);
2221
2314
 
2222
2315
  navigating.current = null;
2223
2316
 
@@ -2883,6 +2976,7 @@ export async function replaceState(url, state) {
2883
2976
  */
2884
2977
  async function update_state(intent, state, { replace, persist_state, reset }, caller) {
2885
2978
  const url = intent.url;
2979
+ const previous_snapshot_registrations = current_registrations();
2886
2980
 
2887
2981
  if (DEV && !started) {
2888
2982
  throw new Error(`Cannot call ${caller}(...) before router is initialized`);
@@ -2903,7 +2997,12 @@ async function update_state(intent, state, { replace, persist_state, reset }, ca
2903
2997
  updating = true;
2904
2998
  }
2905
2999
 
2906
- if (!replace) capture_scroll(current_history_index);
3000
+ if (replace) {
3001
+ delete_navigation_snapshot(current_history_index);
3002
+ } else {
3003
+ capture_scroll(current_history_index);
3004
+ capture_navigation_snapshot(current_history_index);
3005
+ }
2907
3006
  if (reset) current_reset_index += 1;
2908
3007
 
2909
3008
  // as above, serialize-then-parse to prevent bugs
@@ -2959,38 +3058,43 @@ async function update_state(intent, state, { replace, persist_state, reset }, ca
2959
3058
  url
2960
3059
  };
2961
3060
 
2962
- if (!nav) return;
3061
+ if (nav) {
3062
+ const { activeElement } = document;
2963
3063
 
2964
- const { activeElement } = document;
3064
+ await settled();
2965
3065
 
2966
- await settled();
3066
+ if (navigation_token !== nav_token) {
3067
+ // a new navigation happened while we were waiting for the DOM to update, so abort
3068
+ nav.reject(new Error('navigation aborted'));
3069
+ return;
3070
+ }
2967
3071
 
2968
- if (navigation_token !== nav_token) {
2969
- // a new navigation happened while we were waiting for the DOM to update, so abort
2970
- nav.reject(new Error('navigation aborted'));
2971
- return;
2972
- }
3072
+ reset_scroll_and_focus(url, reset ? null : scroll_state(), reset, activeElement);
2973
3073
 
2974
- reset_scroll_and_focus(url, reset ? null : scroll_state(), reset, activeElement);
3074
+ is_navigating = false;
3075
+ nav.fulfil(undefined);
2975
3076
 
2976
- is_navigating = false;
2977
- nav.fulfil(undefined);
3077
+ if (nav.navigation.to) {
3078
+ nav.navigation.to.scroll = scroll_state();
3079
+ }
2978
3080
 
2979
- if (nav.navigation.to) {
2980
- nav.navigation.to.scroll = scroll_state();
3081
+ after_navigate_callbacks.forEach((fn) =>
3082
+ fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
3083
+ );
2981
3084
  }
2982
3085
 
2983
- after_navigate_callbacks.forEach((fn) =>
2984
- fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
2985
- );
3086
+ restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);
2986
3087
 
2987
- navigating.current = null;
2988
- updating = false;
3088
+ if (nav) {
3089
+ navigating.current = null;
3090
+ updating = false;
3091
+ }
2989
3092
  }
2990
3093
 
2991
3094
  /**
2992
- * This action updates the `form` property of the current page with the given data and updates `page.status`.
2993
- * In case of an error, it redirects to the nearest error page.
3095
+ * Updates the `form` property of the current page with the given data and updates `page.status`.
3096
+ * In case of an error, it renders the nearest error page. In case of a redirect, it navigates to
3097
+ * the redirect location.
2994
3098
  * @template {Record<string, unknown> | undefined} Success
2995
3099
  * @template {Record<string, unknown> | undefined} Failure
2996
3100
  * @param {import('@sveltejs/kit').ActionResult<Success, Failure>} result
@@ -3001,10 +3105,13 @@ export async function applyAction(result) {
3001
3105
  throw new Error('Cannot call applyAction(...) on the server');
3002
3106
  }
3003
3107
 
3108
+ if (result.type === 'redirect') {
3109
+ await _goto(result.location, { refreshAll: true });
3110
+ return;
3111
+ }
3112
+
3004
3113
  if (result.type === 'error') {
3005
3114
  await set_nearest_error_page(result.error);
3006
- } else if (result.type === 'redirect') {
3007
- await _goto(result.location, { refreshAll: true });
3008
3115
  } else {
3009
3116
  page.form = result.data;
3010
3117
  page.status = result.status;
@@ -3024,6 +3131,51 @@ export async function applyAction(result) {
3024
3131
  }
3025
3132
  }
3026
3133
 
3134
+ /**
3135
+ * Whether a submission should update the current page in place rather than navigating — either
3136
+ * because it lands where we already are, or because the result carries no location at all
3137
+ * (hand-constructed results, which have always applied in place).
3138
+ * @param {string | undefined} value
3139
+ */
3140
+ export function is_current_location(value) {
3141
+ if (value === undefined) return true;
3142
+
3143
+ const destination = resolve_url(value);
3144
+ const current = new URL(location.href);
3145
+ if (destination.pathname !== current.pathname) return false;
3146
+
3147
+ const keys = new Set([...destination.searchParams.keys(), ...current.searchParams.keys()]);
3148
+ for (const key of keys) {
3149
+ const destination_values = destination.searchParams.getAll(key).sort();
3150
+ const current_values = current.searchParams.getAll(key).sort();
3151
+
3152
+ if (
3153
+ destination_values.length !== current_values.length ||
3154
+ destination_values.some((value, i) => value !== current_values[i])
3155
+ ) {
3156
+ return false;
3157
+ }
3158
+ }
3159
+
3160
+ return true;
3161
+ }
3162
+
3163
+ /**
3164
+ * Navigate to where a form submission should land, rendering that page with the given result
3165
+ * — what the browser would do for a native submission.
3166
+ * @param {string} location
3167
+ * @param {import('@sveltejs/kit').ActionResult} result
3168
+ * @param {boolean} refresh_all
3169
+ * @returns {Promise<void>}
3170
+ */
3171
+ export function apply_action_navigation(location, result, refresh_all) {
3172
+ return _goto(location, {
3173
+ type: 'form',
3174
+ refreshAll: refresh_all,
3175
+ action_result: result
3176
+ });
3177
+ }
3178
+
3027
3179
  /**
3028
3180
  * @param {App.Error} error
3029
3181
  */
@@ -3336,9 +3488,11 @@ function _start_router() {
3336
3488
  update_url(url);
3337
3489
 
3338
3490
  capture_scroll(current_history_index);
3491
+ capture_navigation_snapshot(current_history_index);
3339
3492
  current_history_index = history_index;
3340
3493
  current_reset_index = reset_index;
3341
3494
  if (reset && scroll) scrollTo(scroll.x, scroll.y);
3495
+ restore_navigation_snapshot(current_history_index, current_registrations());
3342
3496
  return;
3343
3497
  }
3344
3498
 
@@ -1,4 +1,5 @@
1
1
  export const SNAPSHOT_KEY = 'sveltekit:snapshot';
2
+ export const NAVIGATION_SNAPSHOT_KEY = 'sveltekit:navigation-snapshot';
2
3
  export const HISTORY_INFO_KEY = 'sveltekit:history-info';
3
4
  export const HISTORY_METADATA_KEY = 'sveltekit:metadata';
4
5
 
@@ -0,0 +1,147 @@
1
+ import { BROWSER, DEV } from 'esm-env';
2
+ import { onMount } from 'svelte';
3
+ import * as storage from './session-storage.js';
4
+ import { NAVIGATION_SNAPSHOT_KEY } from './constants.js';
5
+ import { hash } from '../../utils/hash.js';
6
+ import { parse, stringify } from '#app/internal/transport';
7
+
8
+ /**
9
+ * @typedef {{ id: string; capture: () => any; restore: (value: any) => void; reset?: () => void }} SnapshotRegistration
10
+ */
11
+
12
+ // Function snapshots use history indexes so shallow entries have distinct state.
13
+ /** @type {Record<string, Record<string, string>>} */
14
+ const navigation_snapshots = storage.get(NAVIGATION_SNAPSHOT_KEY) ?? {};
15
+
16
+ /** @type {Set<SnapshotRegistration>} */
17
+ const snapshot_registrations = new Set();
18
+
19
+ /**
20
+ * The registrations mounted right now. Take before navigating and pass to
21
+ * `restore_navigation_snapshot` — only these are eligible for `reset`.
22
+ * @returns {Set<SnapshotRegistration>}
23
+ */
24
+ export function current_registrations() {
25
+ return new Set(snapshot_registrations);
26
+ }
27
+
28
+ // injected by client.js: the live history index while the router is idle, null mid-navigation
29
+ /** @type {() => number | null} */
30
+ let get_idle_history_index = () => null;
31
+
32
+ /** @param {() => number | null} fn */
33
+ export function init_snapshots(fn) {
34
+ get_idle_history_index = fn;
35
+ }
36
+
37
+ /** @param {number} index */
38
+ export function capture_navigation_snapshot(index) {
39
+ if (snapshot_registrations.size === 0) {
40
+ delete navigation_snapshots[index];
41
+ return;
42
+ }
43
+
44
+ navigation_snapshots[index] = Object.fromEntries(
45
+ Array.from(snapshot_registrations, ({ id, capture }) => [id, stringify(capture())])
46
+ );
47
+ }
48
+
49
+ /**
50
+ * @param {number} index
51
+ * @param {Set<SnapshotRegistration>} [reset_registrations]
52
+ */
53
+ export function restore_navigation_snapshot(index, reset_registrations) {
54
+ const values = navigation_snapshots[index];
55
+
56
+ for (const registration of snapshot_registrations) {
57
+ if (values && Object.hasOwn(values, registration.id)) {
58
+ registration.restore(parse(values[registration.id]));
59
+ } else if (reset_registrations?.has(registration)) {
60
+ registration.reset?.();
61
+ }
62
+ }
63
+ }
64
+
65
+ /** @param {number} index */
66
+ export function delete_navigation_snapshot(index) {
67
+ delete navigation_snapshots[index];
68
+ }
69
+
70
+ /** @param {number} index */
71
+ export function persist_navigation_snapshots(index) {
72
+ capture_navigation_snapshot(index);
73
+ storage.set(NAVIGATION_SNAPSHOT_KEY, navigation_snapshots);
74
+ }
75
+
76
+ /**
77
+ * @param {string | undefined} stack
78
+ * @returns {string}
79
+ */
80
+ function callsite_id(stack) {
81
+ let frames = stack?.split('\n') ?? [];
82
+ if (frames[0]?.trim() === 'Error') frames = frames.slice(1);
83
+
84
+ // only the callsite frame is stable: frames above it differ between hydration and
85
+ // re-mounting, and Vite query strings (stripped here) change on module invalidation
86
+ const frame = frames[1]?.replace(/\?[^)\s]*(?=:\d+:\d+\)?$)/, '');
87
+
88
+ if (!frame) {
89
+ throw new Error('Could not generate a snapshot id from the stack trace. Pass an `id` instead.');
90
+ }
91
+
92
+ return hash(frame);
93
+ }
94
+
95
+ /**
96
+ * A lifecycle function that captures state before navigating and restores it when traversing history.
97
+ *
98
+ * By default, the snapshot `id` is generated from the call site. Pass an explicit `id` to keep snapshots stable across deployments or distinguish multiple uses of a shared helper.
99
+ *
100
+ * The optional `reset` callback runs on navigations where there is no captured value to restore, such as when a new history entry is created. Captured values are serialized with the app's transport hook.
101
+ *
102
+ * `snapshot` must be called during a component initialization. It remains active as long as the component is mounted.
103
+ * @template T
104
+ * @param {{ id?: string; capture: () => T; restore: (value: T) => void; reset?: () => void }} options
105
+ * @returns {void}
106
+ */
107
+ export function snapshot(options) {
108
+ if (!BROWSER) return;
109
+
110
+ let id = options.id;
111
+ if (id === undefined) {
112
+ // restore any lowered third-party limit, else every callsite collapses to one id
113
+ const limit = Error.stackTraceLimit;
114
+ const lowered = typeof limit === 'number' && limit < 3;
115
+ if (lowered) Error.stackTraceLimit = 3;
116
+ const stack = new Error().stack;
117
+ if (lowered) Error.stackTraceLimit = limit;
118
+
119
+ id = callsite_id(stack);
120
+ }
121
+
122
+ const registration = { ...options, id };
123
+
124
+ onMount(() => {
125
+ if (DEV && Array.from(snapshot_registrations).some((existing) => existing.id === id)) {
126
+ throw new Error(
127
+ options.id === undefined
128
+ ? 'snapshot() was called multiple times from the same call site. Pass a unique `id` to distinguish the instances.'
129
+ : `A snapshot with id "${options.id}" is already registered. Pass a unique \`id\`.`
130
+ );
131
+ }
132
+
133
+ const index = get_idle_history_index();
134
+ if (index !== null) {
135
+ const values = navigation_snapshots[index];
136
+ if (values && Object.hasOwn(values, id)) {
137
+ registration.restore(parse(values[id]));
138
+ }
139
+ }
140
+
141
+ snapshot_registrations.add(registration);
142
+
143
+ return () => {
144
+ snapshot_registrations.delete(registration);
145
+ };
146
+ });
147
+ }
@@ -12,6 +12,7 @@ export class Props {
12
12
  * that currently live on the page — used for capturing and restoring snapshots.
13
13
  * It's updated/manipulated through `bind:this` in `Root.svelte`.
14
14
  * @type {Array<Record<string, any>>}
15
+ * @deprecated only used for `export const snapshot` — TODO 4.0 get rid
15
16
  */
16
17
  components = [];
17
18
 
@@ -22,7 +22,7 @@ export function get_self_origin(paths_origin, url_origin) {
22
22
 
23
23
  /**
24
24
  * Determines whether a non-remote request should be rejected as a cross-site
25
- * form submission (CSRF). Used by `respond.js` to gate form `POST`/`PUT`/
25
+ * request forgery (CSRF). Used by `respond.js` to gate form `POST`/`PUT`/
26
26
  * `PATCH`/`DELETE` requests whose `Origin` header doesn't match the app's
27
27
  * self-origin (and isn't in `trusted_origins`).
28
28
  *
@@ -6,6 +6,7 @@ import { options, get_hooks } from '__SERVER__/internal.js';
6
6
  import { set_read_implementation, set_manifest, fix_stack_trace } from './internal.js';
7
7
  import { set_env } from '__sveltekit/env';
8
8
  import { SvelteKitError } from '@sveltejs/kit/internal';
9
+ import { init_tracing } from '@sveltejs/kit/internal/server';
9
10
  import { DEV } from 'esm-env';
10
11
  import { init_transport } from '#app/internal/transport';
11
12
 
@@ -90,6 +91,8 @@ export class Server {
90
91
  // so anything that shouldn't be rerun should be wrapped in an `if` block to make sure it hasn't
91
92
  // been done already.
92
93
 
94
+ if (__SVELTEKIT_SERVER_TRACING_ENABLED__) init_tracing(import('@opentelemetry/api'));
95
+
93
96
  // set env, in case it's used in initialisation
94
97
  set_env(env);
95
98