@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.
- package/package.json +62 -36
- package/src/cli.js +15 -12
- package/src/constants.js +6 -1
- package/src/core/adapt/builder.js +66 -72
- package/src/core/adapt/index.js +17 -6
- package/src/core/config/index.js +134 -80
- package/src/core/config/options.js +295 -273
- package/src/core/config/types.d.ts +1 -1
- package/src/core/env.js +146 -16
- package/src/core/generate_manifest/index.js +18 -15
- package/src/core/postbuild/analyse.js +8 -20
- package/src/core/postbuild/crawl.js +22 -6
- package/src/core/postbuild/entities.js +8 -2
- package/src/core/postbuild/fallback.js +4 -2
- package/src/core/postbuild/prerender.js +222 -70
- package/src/core/postbuild/queue.js +2 -1
- package/src/core/sync/create_manifest_data/conflict.js +1 -1
- package/src/core/sync/create_manifest_data/index.js +82 -62
- package/src/core/sync/sync.js +40 -31
- package/src/core/sync/ts.js +1 -1
- package/src/core/sync/utils.js +22 -2
- package/src/core/sync/{write_non_ambient.js → write_app_types.js} +107 -58
- package/src/core/sync/write_client_manifest.js +14 -29
- package/src/core/sync/write_env.js +7 -5
- package/src/core/sync/write_server.js +37 -45
- package/src/core/sync/write_tsconfig/index.js +274 -0
- package/src/core/sync/write_tsconfig/test-app/package.json +7 -0
- package/src/core/sync/write_tsconfig/utils.js +77 -0
- package/src/core/sync/write_tsconfig/validate.js +128 -0
- package/src/core/sync/write_types/index.js +127 -123
- package/src/core/utils.js +30 -5
- package/src/exports/env/index.js +77 -0
- package/src/exports/env/public.d.ts +55 -0
- package/src/exports/hooks/index.js +3 -9
- package/src/exports/hooks/public.d.ts +195 -0
- package/src/exports/hooks/sequence.js +9 -6
- package/src/exports/index.js +52 -29
- package/src/exports/internal/client.js +5 -0
- package/src/exports/internal/env.js +8 -5
- package/src/exports/internal/index.js +1 -90
- package/src/exports/internal/{event.js → server/event.js} +2 -3
- package/src/exports/internal/server/index.js +37 -0
- package/src/exports/internal/server/telemetry.js +95 -0
- package/src/exports/internal/shared.js +90 -0
- package/src/exports/node/index.js +64 -22
- package/src/exports/params/index.js +70 -0
- package/src/exports/params/public.d.ts +63 -0
- package/src/exports/public.d.ts +50 -1733
- package/src/exports/remote/index.js +11 -0
- package/src/exports/remote/public.d.ts +519 -0
- package/src/exports/url.js +86 -0
- package/src/exports/vite/build/build_server.js +54 -65
- package/src/exports/vite/build/remote.js +24 -19
- package/src/exports/vite/build/utils.js +0 -8
- package/src/exports/vite/dev/index.js +218 -146
- package/src/exports/vite/index.js +1590 -858
- package/src/exports/vite/module_ids.js +2 -2
- package/src/exports/vite/preview/index.js +40 -32
- package/src/exports/vite/public.d.ts +588 -0
- package/src/exports/vite/utils.js +84 -46
- package/src/pathname.js +55 -0
- package/src/runner.js +15 -0
- package/src/runtime/app/env/internal.js +4 -4
- package/src/runtime/app/env/types.d.ts +1 -1
- package/src/runtime/app/environment/index.js +3 -3
- package/src/runtime/app/{forms.js → forms/index.js} +73 -42
- package/src/runtime/app/forms/public.d.ts +2 -0
- package/src/runtime/app/forms/types.d.ts +56 -0
- package/src/runtime/app/internal/transport.js +53 -0
- package/src/runtime/app/manifest/index.js +1 -0
- package/src/runtime/app/{navigation.js → navigation/index.js} +2 -1
- package/src/runtime/app/navigation/public.d.ts +237 -0
- package/src/runtime/app/paths/client.js +37 -37
- package/src/runtime/app/paths/index.js +1 -1
- package/src/runtime/app/paths/internal/client.js +34 -2
- package/src/runtime/app/paths/internal/server.js +6 -23
- package/src/runtime/app/paths/internal.d.ts +3 -0
- package/src/runtime/app/paths/public.d.ts +1 -29
- package/src/runtime/app/paths/server.js +36 -17
- package/src/runtime/app/paths/types.d.ts +11 -19
- package/src/runtime/app/server/index.js +2 -2
- package/src/runtime/app/server/public.d.ts +201 -0
- package/src/runtime/app/server/remote/command.js +13 -11
- package/src/runtime/app/server/remote/form.js +61 -39
- package/src/runtime/app/server/remote/prerender.js +45 -45
- package/src/runtime/app/server/remote/query.js +106 -108
- package/src/runtime/app/server/remote/requested.js +27 -19
- package/src/runtime/app/server/remote/shared.js +76 -76
- package/src/runtime/app/service-worker/index.js +24 -0
- package/src/runtime/app/state/client.js +4 -2
- package/src/runtime/app/state/index.js +7 -5
- package/src/runtime/app/state/public.d.ts +72 -0
- package/src/runtime/app/state/server.js +3 -0
- package/src/runtime/app/stores.js +15 -78
- package/src/runtime/client/bundle.js +1 -1
- package/src/runtime/client/client-entry.js +3 -0
- package/src/runtime/client/client.js +1336 -648
- package/src/runtime/client/constants.js +3 -6
- package/src/runtime/client/entry.js +24 -3
- package/src/runtime/client/fetcher.js +25 -25
- package/src/runtime/client/ndjson.js +1 -1
- package/src/runtime/client/parse.js +1 -1
- package/src/runtime/client/payload.js +17 -0
- package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
- package/src/runtime/client/remote-functions/command.svelte.js +37 -61
- package/src/runtime/client/remote-functions/form.svelte.js +267 -178
- package/src/runtime/client/remote-functions/prerender.svelte.js +32 -11
- package/src/runtime/client/remote-functions/query/index.js +10 -17
- package/src/runtime/client/remote-functions/query/instance.svelte.js +64 -18
- package/src/runtime/client/remote-functions/query/proxy.js +5 -5
- package/src/runtime/client/remote-functions/query-batch.svelte.js +61 -70
- package/src/runtime/client/remote-functions/query-live/index.js +1 -1
- package/src/runtime/client/remote-functions/query-live/instance.svelte.js +63 -21
- package/src/runtime/client/remote-functions/query-live/iterator.js +15 -12
- package/src/runtime/client/remote-functions/query-live/proxy.js +2 -12
- package/src/runtime/client/remote-functions/shared.svelte.js +90 -65
- package/src/runtime/client/snapshots.js +147 -0
- package/src/runtime/client/state.svelte.js +94 -55
- package/src/runtime/client/stream.js +3 -2
- package/src/runtime/client/types.d.ts +13 -9
- package/src/runtime/client/utils.js +22 -110
- package/src/runtime/components/root.svelte +56 -0
- package/src/runtime/env/dynamic/private.js +7 -0
- package/src/runtime/env/dynamic/public.js +7 -0
- package/src/runtime/env/static/private.js +6 -0
- package/src/runtime/env/static/public.js +6 -0
- package/src/runtime/form-utils.js +172 -61
- package/src/runtime/pathname.js +20 -32
- package/src/runtime/props.svelte.js +72 -0
- package/src/runtime/server/constants.js +0 -3
- package/src/runtime/server/cookie.js +72 -55
- package/src/runtime/server/csrf.js +65 -0
- package/src/runtime/server/data/index.js +49 -53
- package/src/runtime/server/dev.js +22 -0
- package/src/runtime/server/endpoint.js +6 -7
- package/src/runtime/server/env_module.js +0 -5
- package/src/runtime/server/errors.js +160 -0
- package/src/runtime/server/fetch.js +32 -39
- package/src/runtime/server/index.js +90 -64
- package/src/runtime/server/internal.js +71 -0
- package/src/runtime/server/page/actions.js +85 -64
- package/src/runtime/server/page/crypto.js +2 -2
- package/src/runtime/server/page/csp.js +88 -104
- package/src/runtime/server/page/data_serializer.js +24 -25
- package/src/runtime/server/page/index.js +36 -53
- package/src/runtime/server/page/load_data.js +50 -57
- package/src/runtime/server/page/render.js +173 -239
- package/src/runtime/server/page/respond_with_error.js +17 -31
- package/src/runtime/server/page/serialize_data.js +2 -13
- package/src/runtime/server/page/server_routing.js +85 -26
- package/src/runtime/server/remote-functions.js +640 -0
- package/src/runtime/server/respond.js +190 -131
- package/src/runtime/server/sourcemaps.js +183 -0
- package/src/runtime/server/state.js +53 -0
- package/src/runtime/server/utils.js +13 -155
- package/src/runtime/shared.js +20 -40
- package/src/runtime/utils.js +3 -0
- package/src/types/ambient-private.d.ts +11 -1
- package/src/types/ambient.d.ts +87 -36
- package/src/types/global-private.d.ts +25 -25
- package/src/types/internal.d.ts +163 -157
- package/src/types/private.d.ts +41 -1
- package/src/utils/error.js +28 -4
- package/src/utils/escape.js +9 -25
- package/src/utils/features.js +1 -1
- package/src/utils/filesystem.js +1 -23
- package/src/utils/fork.js +7 -2
- package/src/utils/hash.js +21 -0
- package/src/utils/http.js +8 -7
- package/src/utils/import.js +9 -2
- package/src/utils/imports.js +83 -0
- package/src/utils/mime.js +9 -0
- package/src/utils/page_nodes.js +6 -7
- package/src/utils/params.js +67 -0
- package/src/utils/regex.js +9 -0
- package/src/utils/routing.js +145 -73
- package/src/utils/shared-iterator.js +5 -0
- package/src/utils/streaming.js +14 -4
- package/src/utils/url.js +33 -2
- package/src/version.js +1 -1
- package/types/index.d.ts +2616 -2433
- package/types/index.d.ts.map +120 -106
- package/src/core/sync/write_ambient.js +0 -18
- package/src/core/sync/write_root.js +0 -148
- package/src/core/sync/write_tsconfig.js +0 -250
- package/src/exports/internal/server.js +0 -22
- package/src/exports/node/polyfills.js +0 -30
- package/src/runtime/server/app.js +0 -9
- package/src/runtime/server/remote.js +0 -457
- package/src/runtime/shared-server.js +0 -7
- package/src/runtime/telemetry/otel.js +0 -21
- package/src/runtime/telemetry/record_span.js +0 -65
- package/src/types/synthetic/$lib.md +0 -5
- /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
- /package/src/{runtime/telemetry/noop.js → telemetry.js} +0 -0
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
/** @import {
|
|
1
|
+
/** @import { LoadEvent, NavigationEvent } from '@sveltejs/kit' */
|
|
2
|
+
/** @import { ActionResult } from '$app/forms' */
|
|
3
|
+
/** @import { AfterNavigate, BeforeNavigate, GotoOptions, Navigation, NavigationTarget, NavigationType, OnNavigate } from '$app/navigation' */
|
|
4
|
+
/** @import { RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */
|
|
5
|
+
/** @import { NavigationFinished, NavigationIntent } from './types.js' */
|
|
2
6
|
/** @import { CacheEntry } from './remote-functions/cache.svelte.js' */
|
|
3
7
|
/** @import { Query } from './remote-functions/query/instance.svelte.js' */
|
|
4
8
|
/** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */
|
|
5
9
|
import { BROWSER, DEV } from 'esm-env';
|
|
6
|
-
import
|
|
7
|
-
import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
|
|
8
|
-
|
|
9
|
-
// Svelte 4 and under don't have `untrack`, so we have to fallback if `untrack` is not exported
|
|
10
|
-
const untrack = svelte.untrack ?? ((value) => value());
|
|
11
|
-
import {
|
|
12
|
-
decode_params,
|
|
13
|
-
decode_pathname,
|
|
14
|
-
strip_hash,
|
|
15
|
-
make_trackable,
|
|
16
|
-
normalize_path
|
|
17
|
-
} from '../../utils/url.js';
|
|
10
|
+
import { settled, tick, fork, onMount, hydrate, mount } from 'svelte';
|
|
11
|
+
import { HttpError, Redirect, SvelteKitError, HandledHttpError } from '@sveltejs/kit/internal';
|
|
12
|
+
import { decode_pathname, strip_hash, make_trackable, normalize_path } from '../../utils/url.js';
|
|
18
13
|
import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js';
|
|
19
|
-
import {
|
|
14
|
+
import { parse_routes, parse_server_route } from './parse.js';
|
|
20
15
|
import * as storage from './session-storage.js';
|
|
21
16
|
import {
|
|
22
17
|
find_anchor,
|
|
@@ -26,24 +21,27 @@ import {
|
|
|
26
21
|
is_external_url,
|
|
27
22
|
origin,
|
|
28
23
|
scroll_state,
|
|
29
|
-
notifiable_store,
|
|
30
|
-
create_updated_store,
|
|
31
24
|
load_css
|
|
32
25
|
} from './utils.js';
|
|
33
|
-
import { base } from '$app/paths';
|
|
26
|
+
import { base, set_match_implementation } from '$app/paths/internal/client';
|
|
34
27
|
import * as devalue from 'devalue';
|
|
35
28
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
HISTORY_INFO_KEY,
|
|
30
|
+
HISTORY_METADATA_KEY,
|
|
38
31
|
PRELOAD_PRIORITIES,
|
|
39
|
-
|
|
40
|
-
STATES_KEY,
|
|
41
|
-
SNAPSHOT_KEY,
|
|
42
|
-
PAGE_URL_KEY
|
|
32
|
+
SNAPSHOT_KEY
|
|
43
33
|
} from './constants.js';
|
|
34
|
+
import {
|
|
35
|
+
capture_navigation_snapshot,
|
|
36
|
+
current_registrations,
|
|
37
|
+
delete_navigation_snapshot,
|
|
38
|
+
init_snapshots,
|
|
39
|
+
persist_navigation_snapshots,
|
|
40
|
+
restore_navigation_snapshot
|
|
41
|
+
} from './snapshots.js';
|
|
44
42
|
import { validate_page_exports } from '../../utils/exports.js';
|
|
43
|
+
import { add_deprecated_handle_error_properties } from '../../utils/error.js';
|
|
45
44
|
import { noop } from '../../utils/functions.js';
|
|
46
|
-
import { compact } from '../../utils/array.js';
|
|
47
45
|
import {
|
|
48
46
|
INVALIDATED_PARAM,
|
|
49
47
|
TRAILING_SLASH_PARAM,
|
|
@@ -51,42 +49,68 @@ import {
|
|
|
51
49
|
validate_depends,
|
|
52
50
|
validate_load_response
|
|
53
51
|
} from '../shared.js';
|
|
54
|
-
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
|
|
52
|
+
|
|
53
|
+
import { page, navigating, updated, notify_version } from './state.svelte.js';
|
|
54
|
+
import { payload } from './payload.js';
|
|
55
|
+
import {
|
|
56
|
+
add_data_suffix,
|
|
57
|
+
add_resolution_suffix,
|
|
58
|
+
route_id_resolution_pathname
|
|
59
|
+
} from '../pathname.js';
|
|
60
|
+
import { noop_span } from '../../telemetry.js';
|
|
59
61
|
import { read_ndjson } from './ndjson.js';
|
|
62
|
+
import Root from '../components/root.svelte';
|
|
63
|
+
import { Props, RenderNode } from '../props.svelte.js';
|
|
64
|
+
import { init_transport, parse, stringify } from '#app/internal/transport';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {{
|
|
68
|
+
* historyIndex: number;
|
|
69
|
+
* navigationIndex: number;
|
|
70
|
+
* pageUrl?: string;
|
|
71
|
+
* state: string;
|
|
72
|
+
* persistState: boolean;
|
|
73
|
+
* resetIndex: number;
|
|
74
|
+
* }} HistoryMetadata
|
|
75
|
+
*/
|
|
60
76
|
|
|
61
77
|
export { load_css };
|
|
62
78
|
const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
|
|
63
79
|
|
|
64
80
|
let errored = false;
|
|
81
|
+
|
|
65
82
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
83
|
+
* `reset` functions for `<svelte:boundary>`s in the generated root that have
|
|
84
|
+
* failed. A failed boundary stays failed until `reset()` is called — prop
|
|
85
|
+
* updates alone don't re-render its content — so without resetting, a client
|
|
86
|
+
* navigation away from a render error would leave the stale `+error.svelte`
|
|
87
|
+
* mounted. The boundary's `onerror` populates this array; `navigate` drains it
|
|
88
|
+
* after applying the new props. See sveltejs/kit#15694.
|
|
89
|
+
* @type {Set<() => void>}
|
|
71
90
|
*/
|
|
72
|
-
|
|
91
|
+
const resetters = new Set();
|
|
73
92
|
|
|
74
|
-
// We track
|
|
93
|
+
// We track information associated with each history entry in sessionStorage,
|
|
75
94
|
// rather than on history.state itself, because when navigation is driven by
|
|
76
|
-
// popstate it's too late to update the
|
|
95
|
+
// popstate it's too late to access the options or update the focus position associated with the
|
|
77
96
|
// state we're navigating from
|
|
78
97
|
/**
|
|
79
|
-
*
|
|
80
|
-
* @type {Record<number, { x: number; y: number }>}
|
|
98
|
+
* @type {Record<number, { scroll?: { x: number; y: number }; resetIndex?: number }>}
|
|
81
99
|
*/
|
|
82
|
-
const
|
|
100
|
+
const history_info = storage.get(HISTORY_INFO_KEY) ?? {};
|
|
83
101
|
|
|
84
102
|
/**
|
|
85
103
|
* navigation index -> any
|
|
86
104
|
* @type {Record<string, any[]>}
|
|
105
|
+
* @deprecated TODO 4.0 get rid of this
|
|
87
106
|
*/
|
|
88
107
|
const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
|
|
89
108
|
|
|
109
|
+
init_snapshots(() => (started && !is_navigating && !updating ? current_history_index : null));
|
|
110
|
+
|
|
111
|
+
/** @type {Props} */
|
|
112
|
+
let props;
|
|
113
|
+
|
|
90
114
|
if (DEV && BROWSER) {
|
|
91
115
|
let warned = false;
|
|
92
116
|
|
|
@@ -100,14 +124,19 @@ if (DEV && BROWSER) {
|
|
|
100
124
|
let stack = new Error().stack?.split('\n');
|
|
101
125
|
if (!stack) return;
|
|
102
126
|
if (!stack[0].includes('https:') && !stack[0].includes('http:')) stack = stack.slice(1); // Chrome includes the error message in the stack
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
127
|
+
|
|
128
|
+
// skip over `warn` and the place where `warn` was called
|
|
129
|
+
const frame = stack[2];
|
|
130
|
+
|
|
131
|
+
// Ignore calls that happen inside dependencies, including SvelteKit.
|
|
132
|
+
// The second condition is only relevant when developing SvelteKit and running it, as there's no node_modules in the stack then (but we still do it to not get repeatedly confused)
|
|
133
|
+
// `frame` can be falsy if we came from an anonymous function
|
|
134
|
+
if (frame?.includes('node_modules') || frame?.includes(current_module_url)) return;
|
|
106
135
|
|
|
107
136
|
warned = true;
|
|
108
137
|
|
|
109
138
|
console.warn(
|
|
110
|
-
"Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use
|
|
139
|
+
"Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use `goto(...)` from `$app/navigation` instead."
|
|
111
140
|
);
|
|
112
141
|
};
|
|
113
142
|
|
|
@@ -124,18 +153,61 @@ if (DEV && BROWSER) {
|
|
|
124
153
|
};
|
|
125
154
|
}
|
|
126
155
|
|
|
127
|
-
export const stores = {
|
|
128
|
-
url: /* @__PURE__ */ notifiable_store({}),
|
|
129
|
-
page: /* @__PURE__ */ notifiable_store({}),
|
|
130
|
-
navigating: /* @__PURE__ */ writable(
|
|
131
|
-
/** @type {import('@sveltejs/kit').Navigation | null} */ (null)
|
|
132
|
-
),
|
|
133
|
-
updated: /* @__PURE__ */ create_updated_store()
|
|
134
|
-
};
|
|
135
|
-
|
|
136
156
|
/** @param {number} index */
|
|
137
|
-
function
|
|
138
|
-
|
|
157
|
+
function capture_scroll(index) {
|
|
158
|
+
history_info[index].scroll = scroll_state();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {number} index
|
|
163
|
+
* @param {Pick<HistoryMetadata, 'resetIndex'>} options
|
|
164
|
+
*/
|
|
165
|
+
function set_history_options(index, options) {
|
|
166
|
+
history_info[index] = {
|
|
167
|
+
...history_info[index],
|
|
168
|
+
resetIndex: options.resetIndex
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {boolean} reset */
|
|
173
|
+
function blur_active_element(reset) {
|
|
174
|
+
if (
|
|
175
|
+
reset &&
|
|
176
|
+
document.activeElement instanceof HTMLElement &&
|
|
177
|
+
document.activeElement !== document.body
|
|
178
|
+
) {
|
|
179
|
+
document.activeElement.blur();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {URL} url
|
|
185
|
+
* @param {{ x: number; y: number } | null | undefined} scroll
|
|
186
|
+
* @param {boolean} reset
|
|
187
|
+
* @param {Element | null} active_element
|
|
188
|
+
*/
|
|
189
|
+
function reset_scroll_and_focus(url, scroll, reset, active_element) {
|
|
190
|
+
/** @type {Element | null} */
|
|
191
|
+
let deep_linked = null;
|
|
192
|
+
|
|
193
|
+
if (autoscroll) {
|
|
194
|
+
if (scroll) {
|
|
195
|
+
scrollTo(scroll.x, scroll.y);
|
|
196
|
+
} else if ((deep_linked = get_hash_element(url))) {
|
|
197
|
+
deep_linked.scrollIntoView();
|
|
198
|
+
} else {
|
|
199
|
+
scrollTo(0, 0);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const changed_focus =
|
|
204
|
+
document.activeElement !== active_element && document.activeElement !== document.body;
|
|
205
|
+
|
|
206
|
+
if (reset && !changed_focus) {
|
|
207
|
+
reset_focus(url, !deep_linked);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
autoscroll = true;
|
|
139
211
|
}
|
|
140
212
|
|
|
141
213
|
/**
|
|
@@ -145,9 +217,12 @@ function update_scroll_positions(index) {
|
|
|
145
217
|
function clear_onward_history(current_history_index, current_navigation_index) {
|
|
146
218
|
// if we navigated back, then pushed a new state, we can
|
|
147
219
|
// release memory by pruning the scroll/snapshot lookup
|
|
220
|
+
// the new entry reuses the first abandoned index, so its slot must start clean
|
|
221
|
+
delete_navigation_snapshot(current_history_index);
|
|
148
222
|
let i = current_history_index + 1;
|
|
149
|
-
while (
|
|
150
|
-
delete
|
|
223
|
+
while (history_info[i]) {
|
|
224
|
+
delete history_info[i];
|
|
225
|
+
delete_navigation_snapshot(i);
|
|
151
226
|
i += 1;
|
|
152
227
|
}
|
|
153
228
|
|
|
@@ -163,7 +238,8 @@ function clear_onward_history(current_history_index, current_navigation_index) {
|
|
|
163
238
|
* Returns a `Promise` that never resolves (to prevent any
|
|
164
239
|
* subsequent work, e.g. history manipulation, from happening)
|
|
165
240
|
* @param {URL} url
|
|
166
|
-
* @param {boolean} [replace]
|
|
241
|
+
* @param {boolean} [replace] if `true`, will replace the current `history` entry rather than creating a new one with `pushState`
|
|
242
|
+
* @returns {Promise<any>} a promise that never resolves
|
|
167
243
|
*/
|
|
168
244
|
function native_navigation(url, replace = false) {
|
|
169
245
|
if (replace) {
|
|
@@ -187,7 +263,7 @@ async function update_service_worker() {
|
|
|
187
263
|
}
|
|
188
264
|
}
|
|
189
265
|
|
|
190
|
-
/** @type {import('types').CSRRoute[]} All routes of the app. Only available when
|
|
266
|
+
/** @type {import('types').CSRRoute[]} All routes of the app. Only available when router.resolution=client */
|
|
191
267
|
let routes;
|
|
192
268
|
/** @type {import('types').CSRPageNodeLoader} */
|
|
193
269
|
let default_layout_loader;
|
|
@@ -202,30 +278,24 @@ let target;
|
|
|
202
278
|
export let app;
|
|
203
279
|
|
|
204
280
|
/**
|
|
205
|
-
* Data that was serialized during SSR for queries/forms/commands
|
|
206
|
-
*
|
|
207
|
-
*
|
|
281
|
+
* Data that was serialized during SSR for queries/forms/commands, stored as
|
|
282
|
+
* `{ v }` (value) or `{ e }` (error) nodes so that failed states survive hydration.
|
|
283
|
+
* Entries are deleted as they are consumed (when the corresponding resource is created).
|
|
284
|
+
* @type {Record<string, RemoteFunctionDataNode>}
|
|
208
285
|
*/
|
|
209
|
-
export
|
|
286
|
+
export const query_responses = {};
|
|
210
287
|
|
|
211
288
|
/**
|
|
212
|
-
* Data that was serialized during SSR for prerender functions
|
|
289
|
+
* Data that was serialized during SSR for prerender functions, stored as
|
|
290
|
+
* `{ v }` (value) or `{ e }` (error) nodes.
|
|
213
291
|
* This persists across client-side navigations.
|
|
214
|
-
* @type {Record<string,
|
|
292
|
+
* @type {Record<string, RemoteFunctionDataNode>}
|
|
215
293
|
*/
|
|
216
|
-
export
|
|
294
|
+
export const prerender_responses = {};
|
|
217
295
|
|
|
218
296
|
/** @type {Array<((url: URL) => boolean)>} */
|
|
219
297
|
const invalidated = [];
|
|
220
298
|
|
|
221
|
-
/**
|
|
222
|
-
* An array of the `+layout.svelte` and `+page.svelte` component instances
|
|
223
|
-
* that currently live on the page — used for capturing and restoring snapshots.
|
|
224
|
-
* It's updated/manipulated through `bind:this` in `Root.svelte`.
|
|
225
|
-
* @type {import('svelte').SvelteComponent[]}
|
|
226
|
-
*/
|
|
227
|
-
const components = [];
|
|
228
|
-
|
|
229
299
|
/** @type {{id: string, token: {}, promise: Promise<import('./types.js').NavigationResult>, fork: Promise<import('svelte').Fork | null> | null} | null} */
|
|
230
300
|
let load_cache = null;
|
|
231
301
|
|
|
@@ -246,20 +316,49 @@ function discard_load_cache() {
|
|
|
246
316
|
*/
|
|
247
317
|
const reroute_cache = new Map();
|
|
248
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Sentinel for a route that exists but has no code to preload, i.e. a `+server.js` with no
|
|
321
|
+
* `+page`. Cached like a parsed route so that repeated `preloadCode(id)` calls for the same
|
|
322
|
+
* id don't re-request it.
|
|
323
|
+
*/
|
|
324
|
+
const ENDPOINT_ONLY = Symbol('endpoint only');
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Cache of route ID -> parsed route for server-side route resolution.
|
|
328
|
+
* Populated whenever a server route resolution occurs (`match`, link preloading,
|
|
329
|
+
* hydration), so that `preloadCode(id)` doesn't need an extra server round trip.
|
|
330
|
+
* Lives until full page reload.
|
|
331
|
+
* @type {Map<string, import('types').CSRRoute | typeof ENDPOINT_ONLY>}
|
|
332
|
+
*/
|
|
333
|
+
const route_id_cache = new Map();
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Parse a server-provided route and record it in `route_id_cache`, so that a subsequent
|
|
337
|
+
* `preloadCode(id)` for the same route doesn't need another server round trip. Always use
|
|
338
|
+
* this rather than calling `parse_server_route` directly.
|
|
339
|
+
* @param {import('types').CSRRouteServer} server_route
|
|
340
|
+
* @returns {import('types').CSRRoute}
|
|
341
|
+
*/
|
|
342
|
+
function parse_and_cache_server_route(server_route) {
|
|
343
|
+
const route = parse_server_route(server_route, app.nodes);
|
|
344
|
+
route_id_cache.set(route.id, route);
|
|
345
|
+
return route;
|
|
346
|
+
}
|
|
347
|
+
|
|
249
348
|
/**
|
|
250
349
|
* Note on before_navigate_callbacks, on_navigate_callbacks and after_navigate_callbacks:
|
|
251
350
|
* do not re-assign as some closures keep references to these Sets
|
|
252
351
|
*/
|
|
253
|
-
/** @type {Set<(navigation:
|
|
352
|
+
/** @type {Set<(navigation: BeforeNavigate) => void>} */
|
|
254
353
|
const before_navigate_callbacks = new Set();
|
|
255
354
|
|
|
256
|
-
/** @type {Set<(navigation:
|
|
355
|
+
/** @type {Set<(navigation: OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */
|
|
257
356
|
const on_navigate_callbacks = new Set();
|
|
258
357
|
|
|
259
|
-
/** @type {Set<(navigation:
|
|
358
|
+
/** @type {Set<(navigation: AfterNavigate) => void>} */
|
|
260
359
|
const after_navigate_callbacks = new Set();
|
|
261
360
|
|
|
262
|
-
/** @type {import('./types.js').NavigationState & { nav:
|
|
361
|
+
/** @type {import('./types.js').NavigationState & { nav: NavigationEvent }} */
|
|
263
362
|
let current = {
|
|
264
363
|
branch: [],
|
|
265
364
|
error: null,
|
|
@@ -275,23 +374,41 @@ let started = false;
|
|
|
275
374
|
let autoscroll = true;
|
|
276
375
|
let updating = false;
|
|
277
376
|
let is_navigating = false;
|
|
278
|
-
|
|
377
|
+
/** @type {HistoryMetadata | null} */
|
|
378
|
+
let hash_navigating = null;
|
|
279
379
|
/** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */
|
|
280
380
|
let has_navigated = false;
|
|
281
381
|
|
|
282
382
|
let force_invalidation = false;
|
|
283
383
|
|
|
284
|
-
/** @type {import('svelte').SvelteComponent} */
|
|
285
|
-
let root;
|
|
286
|
-
|
|
287
384
|
/** @type {number} keeping track of the history index in order to prevent popstate navigation events if needed */
|
|
288
385
|
let current_history_index;
|
|
289
386
|
|
|
290
387
|
/** @type {number} */
|
|
291
388
|
let current_navigation_index;
|
|
292
389
|
|
|
293
|
-
/** @type {
|
|
294
|
-
let
|
|
390
|
+
/** @type {number} */
|
|
391
|
+
let current_reset_index;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @param {any} [state]
|
|
395
|
+
* @returns {HistoryMetadata | undefined}
|
|
396
|
+
*/
|
|
397
|
+
function get_history_metadata(state = history.state) {
|
|
398
|
+
return state?.[HISTORY_METADATA_KEY];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** @type {{}} Token for the latest navigation. Updated on new navigations */
|
|
402
|
+
let navigation_token;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @type {{}}
|
|
406
|
+
* The latest invalidate(All) token. Superseeded by both later invalidate(All)s and navigations.
|
|
407
|
+
* This is separate to navigation_token because an invalidate(All) might be triggered while a navigation
|
|
408
|
+
* is in progress, and we want to be able to finish this navigation (unless the invalidation finishes before
|
|
409
|
+
* it and redirects, in which case we will do the redirect triggered by the invalidation).
|
|
410
|
+
*/
|
|
411
|
+
let invalidation_token;
|
|
295
412
|
|
|
296
413
|
/**
|
|
297
414
|
* A set of tokens which are associated to current preloads.
|
|
@@ -299,10 +416,11 @@ let token;
|
|
|
299
416
|
* If a preload token is in the set and the preload errors, the error
|
|
300
417
|
* handling logic (for example reloading) is skipped.
|
|
301
418
|
*/
|
|
419
|
+
/** @type {Set<{}>} */
|
|
302
420
|
const preload_tokens = new Set();
|
|
303
421
|
|
|
304
422
|
/** @type {Promise<void> | null} */
|
|
305
|
-
|
|
423
|
+
let pending_invalidate;
|
|
306
424
|
|
|
307
425
|
/**
|
|
308
426
|
* @type {Map<string, Map<string, CacheEntry<Query<any>>>>}
|
|
@@ -316,21 +434,65 @@ export const query_map = new Map();
|
|
|
316
434
|
*/
|
|
317
435
|
export const live_query_map = new Map();
|
|
318
436
|
|
|
437
|
+
set_match_implementation(async (url) => {
|
|
438
|
+
if (typeof url === 'string') {
|
|
439
|
+
url = new URL(url, location.href);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const intent = await get_navigation_intent(url, false);
|
|
443
|
+
|
|
444
|
+
if (intent) {
|
|
445
|
+
return {
|
|
446
|
+
id: /** @type {import('$app/types').RouteId} */ (intent.route.id),
|
|
447
|
+
params: intent.params
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return null;
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
let embedded_start = Promise.resolve();
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* TODO this indirection is a grotesque workaround for the fact that multiple apps
|
|
458
|
+
* can operate on the same shared mutable state. This is fundamentally unsound,
|
|
459
|
+
* and the `start`/`_start` distinction does not fix it, but it does get the
|
|
460
|
+
* tests passing. We need to rethink this whole thing but not right now
|
|
461
|
+
* @param {import('./types.js').SvelteKitApp} _app
|
|
462
|
+
* @param {HTMLElement} _target
|
|
463
|
+
* @param {Parameters<typeof _hydrate>[1]} [data]
|
|
464
|
+
*/
|
|
465
|
+
export function start(_app, _target, data) {
|
|
466
|
+
if (__SVELTEKIT_EMBEDDED__) {
|
|
467
|
+
const start_promise = embedded_start.then(() => _start(_app, _target, data));
|
|
468
|
+
embedded_start = start_promise.catch(noop);
|
|
469
|
+
return start_promise;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return _start(_app, _target, data);
|
|
473
|
+
}
|
|
474
|
+
|
|
319
475
|
/**
|
|
320
476
|
* @param {import('./types.js').SvelteKitApp} _app
|
|
321
477
|
* @param {HTMLElement} _target
|
|
322
|
-
* @param {Parameters<typeof _hydrate>[1]} [
|
|
478
|
+
* @param {Parameters<typeof _hydrate>[1]} [data]
|
|
323
479
|
*/
|
|
324
|
-
|
|
480
|
+
async function _start(_app, _target, data) {
|
|
325
481
|
if (DEV && _target === document.body) {
|
|
326
482
|
console.warn(
|
|
327
483
|
'Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>'
|
|
328
484
|
);
|
|
329
485
|
}
|
|
330
486
|
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
487
|
+
if (payload.data) {
|
|
488
|
+
const { q = {}, p = {}, l = {}, f = {} } = payload.data;
|
|
489
|
+
|
|
490
|
+
// store the whole nodes — error records seed the corresponding
|
|
491
|
+
// resources in a failed state when they are created during hydration
|
|
492
|
+
for (const k in q) query_responses[k] = q[k];
|
|
493
|
+
for (const k in l) query_responses[k] = l[k];
|
|
494
|
+
for (const k in f) query_responses[k] = f[k];
|
|
495
|
+
for (const k in p) prerender_responses[k] = p[k];
|
|
334
496
|
}
|
|
335
497
|
|
|
336
498
|
// detect basic auth credentials in the current URL
|
|
@@ -343,9 +505,11 @@ export async function start(_app, _target, hydrate) {
|
|
|
343
505
|
|
|
344
506
|
app = _app;
|
|
345
507
|
|
|
508
|
+
init_transport(app.hooks.transport ?? {});
|
|
509
|
+
|
|
346
510
|
await _app.hooks.init?.();
|
|
347
511
|
|
|
348
|
-
routes = __SVELTEKIT_CLIENT_ROUTING__ ?
|
|
512
|
+
routes = __SVELTEKIT_CLIENT_ROUTING__ ? parse_routes(_app) : [];
|
|
349
513
|
container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement;
|
|
350
514
|
target = _target;
|
|
351
515
|
|
|
@@ -353,31 +517,56 @@ export async function start(_app, _target, hydrate) {
|
|
|
353
517
|
// connectivity errors after initialisation don't nuke the app
|
|
354
518
|
default_layout_loader = _app.nodes[0];
|
|
355
519
|
default_error_loader = _app.nodes[1];
|
|
356
|
-
void default_layout_loader();
|
|
357
|
-
void default_error_loader();
|
|
358
520
|
|
|
359
|
-
|
|
360
|
-
|
|
521
|
+
const [root_layout, root_error] = await Promise.all([
|
|
522
|
+
default_layout_loader(),
|
|
523
|
+
default_error_loader()
|
|
524
|
+
]);
|
|
525
|
+
|
|
526
|
+
const tree = new RenderNode(root_layout.component, root_error.component);
|
|
527
|
+
|
|
528
|
+
props = new Props({
|
|
529
|
+
page,
|
|
530
|
+
tree,
|
|
531
|
+
form: undefined,
|
|
532
|
+
error: undefined,
|
|
533
|
+
onerror: (_, reset) => resetters.add(reset)
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const history_metadata = get_history_metadata();
|
|
537
|
+
current_history_index = history_metadata?.historyIndex ?? 0;
|
|
538
|
+
current_navigation_index = history_metadata?.navigationIndex ?? 0;
|
|
539
|
+
current_reset_index = history_metadata?.resetIndex ?? 0;
|
|
361
540
|
|
|
362
541
|
if (!current_history_index) {
|
|
363
542
|
// we use Date.now() as an offset so that cross-document navigations
|
|
364
543
|
// within the app don't result in data loss
|
|
365
|
-
current_history_index = current_navigation_index = Date.now();
|
|
544
|
+
current_history_index = current_navigation_index = current_reset_index = Date.now();
|
|
366
545
|
|
|
367
546
|
// create initial history entry, so we can return here
|
|
368
547
|
history.replaceState(
|
|
369
548
|
{
|
|
370
549
|
...history.state,
|
|
371
|
-
[
|
|
372
|
-
|
|
550
|
+
[HISTORY_METADATA_KEY]: {
|
|
551
|
+
historyIndex: current_history_index,
|
|
552
|
+
navigationIndex: current_navigation_index,
|
|
553
|
+
state: stringify({}),
|
|
554
|
+
persistState: false,
|
|
555
|
+
resetIndex: current_history_index
|
|
556
|
+
}
|
|
373
557
|
},
|
|
374
558
|
''
|
|
375
559
|
);
|
|
376
560
|
}
|
|
377
561
|
|
|
562
|
+
set_history_options(
|
|
563
|
+
current_history_index,
|
|
564
|
+
/** @type {HistoryMetadata} */ (get_history_metadata())
|
|
565
|
+
);
|
|
566
|
+
|
|
378
567
|
// if we reload the page, or Cmd-Shift-T back to it,
|
|
379
568
|
// recover scroll position
|
|
380
|
-
const scroll =
|
|
569
|
+
const scroll = history_info[current_history_index]?.scroll;
|
|
381
570
|
function restore_scroll() {
|
|
382
571
|
if (scroll) {
|
|
383
572
|
history.scrollRestoration = 'manual';
|
|
@@ -385,15 +574,17 @@ export async function start(_app, _target, hydrate) {
|
|
|
385
574
|
}
|
|
386
575
|
}
|
|
387
576
|
|
|
388
|
-
if (
|
|
577
|
+
if (data) {
|
|
389
578
|
restore_scroll();
|
|
390
579
|
|
|
391
|
-
await _hydrate(target,
|
|
580
|
+
await _hydrate(target, data);
|
|
392
581
|
} else {
|
|
393
582
|
await navigate({
|
|
394
583
|
type: 'enter',
|
|
395
584
|
url: resolve_url(app.hash ? decode_hash(new URL(location.href)) : location.href),
|
|
396
|
-
replace_state: true
|
|
585
|
+
replace_state: true,
|
|
586
|
+
state: history_metadata?.persistState ? parse(history_metadata.state) : {},
|
|
587
|
+
persist_state: history_metadata?.persistState ?? false
|
|
397
588
|
});
|
|
398
589
|
|
|
399
590
|
restore_scroll();
|
|
@@ -402,15 +593,17 @@ export async function start(_app, _target, hydrate) {
|
|
|
402
593
|
_start_router();
|
|
403
594
|
}
|
|
404
595
|
|
|
405
|
-
async function _invalidate(
|
|
596
|
+
async function _invalidate(reset_page_state = true) {
|
|
406
597
|
// Accept all invalidations as they come, don't swallow any while another invalidation
|
|
407
598
|
// is running because subsequent invalidations may make earlier ones outdated,
|
|
408
599
|
// but batch multiple synchronous invalidations.
|
|
409
600
|
await (pending_invalidate ||= Promise.resolve());
|
|
410
|
-
if (
|
|
601
|
+
if (pending_invalidate === null) return;
|
|
411
602
|
pending_invalidate = null;
|
|
412
603
|
|
|
413
|
-
const
|
|
604
|
+
const token = (invalidation_token = {});
|
|
605
|
+
const nav_token = navigation_token;
|
|
606
|
+
const navigating = is_navigating;
|
|
414
607
|
const intent = await get_navigation_intent(current.url, true);
|
|
415
608
|
|
|
416
609
|
// Clear preload, it might be affected by the invalidation.
|
|
@@ -439,31 +632,36 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
|
|
|
439
632
|
}
|
|
440
633
|
}
|
|
441
634
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
635
|
+
const prev_state = page.state;
|
|
636
|
+
const prev_shallow = page.shallow;
|
|
637
|
+
const navigation_result = intent && (await load_route(intent));
|
|
638
|
+
if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (navigation_result.type === 'redirect') {
|
|
643
|
+
return _goto(
|
|
644
|
+
new URL(navigation_result.location, current.url).href,
|
|
645
|
+
{ replace: true },
|
|
646
|
+
1,
|
|
647
|
+
token
|
|
648
|
+
);
|
|
649
|
+
}
|
|
446
650
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
nav_token
|
|
453
|
-
);
|
|
454
|
-
}
|
|
651
|
+
// A navigation started before the invalidation and ended before it finished. The invalidation did not redirect,
|
|
652
|
+
// hence it likely contains outdated data now, so we ignore it.
|
|
653
|
+
if (navigating && !is_navigating) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
455
656
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
update(navigation_result.props.page);
|
|
461
|
-
current = { ...navigation_result.state, nav: current.nav };
|
|
462
|
-
reset_invalidation();
|
|
463
|
-
root.$set(navigation_result.props);
|
|
464
|
-
} else {
|
|
465
|
-
reset_invalidation();
|
|
657
|
+
// Preserve `page.state` when invalidating without resetting it (e.g. `refresh`/`refreshAll`)
|
|
658
|
+
if (!reset_page_state) {
|
|
659
|
+
navigation_result.props.page.state = prev_state;
|
|
466
660
|
}
|
|
661
|
+
navigation_result.props.page.shallow = prev_shallow;
|
|
662
|
+
apply_navigation_result(navigation_result);
|
|
663
|
+
current = { ...navigation_result.state, nav: current.nav };
|
|
664
|
+
reset_invalidation();
|
|
467
665
|
|
|
468
666
|
// only wait for promises that are connected to queries that still exist
|
|
469
667
|
/** @type {Promise<any>[]} */
|
|
@@ -492,61 +690,86 @@ function reset_invalidation() {
|
|
|
492
690
|
force_invalidation = false;
|
|
493
691
|
}
|
|
494
692
|
|
|
495
|
-
|
|
693
|
+
let warned_snapshot_export = false;
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* @param {number} index
|
|
697
|
+
* @deprecated TODO 4.0 get rid of this
|
|
698
|
+
*/
|
|
496
699
|
function capture_snapshot(index) {
|
|
497
|
-
if (components.some((c) => c?.snapshot)) {
|
|
498
|
-
|
|
700
|
+
if (props.components.some((c) => c?.snapshot)) {
|
|
701
|
+
if (DEV && !warned_snapshot_export) {
|
|
702
|
+
warned_snapshot_export = true;
|
|
703
|
+
console.warn(
|
|
704
|
+
'`export const snapshot` is deprecated. Use the `snapshot` helper from `$app/navigation` instead.'
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
snapshots[index] = props.components.map((c) => c?.snapshot?.capture());
|
|
499
708
|
}
|
|
500
709
|
}
|
|
501
710
|
|
|
502
|
-
/**
|
|
711
|
+
/**
|
|
712
|
+
* @param {number} index
|
|
713
|
+
* @deprecated TODO 4.0 get rid of this
|
|
714
|
+
*/
|
|
503
715
|
function restore_snapshot(index) {
|
|
504
716
|
snapshots[index]?.forEach((value, i) => {
|
|
505
|
-
components[i]?.snapshot?.restore(value);
|
|
717
|
+
props.components[i]?.snapshot?.restore(value);
|
|
506
718
|
});
|
|
507
719
|
}
|
|
508
720
|
|
|
509
721
|
function persist_state() {
|
|
510
|
-
|
|
511
|
-
storage.set(
|
|
722
|
+
capture_scroll(current_history_index);
|
|
723
|
+
storage.set(HISTORY_INFO_KEY, history_info);
|
|
512
724
|
|
|
513
725
|
capture_snapshot(current_navigation_index);
|
|
514
726
|
storage.set(SNAPSHOT_KEY, snapshots);
|
|
727
|
+
|
|
728
|
+
persist_navigation_snapshots(current_history_index);
|
|
515
729
|
}
|
|
516
730
|
|
|
517
731
|
/**
|
|
518
732
|
* @param {string | URL} url
|
|
519
|
-
* @param {{
|
|
520
|
-
* @param {number} redirect_count
|
|
733
|
+
* @param {{ type?: NavigationType; replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean; event?: Event; action_result?: ActionResult }} [options]
|
|
734
|
+
* @param {number} [redirect_count]
|
|
521
735
|
* @param {{}} [nav_token]
|
|
736
|
+
* @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
|
|
737
|
+
* @returns {Promise<void>}
|
|
522
738
|
*/
|
|
523
|
-
export async function _goto(url, options, redirect_count, nav_token) {
|
|
739
|
+
export async function _goto(url, options = {}, redirect_count = 0, nav_token = {}, intent) {
|
|
524
740
|
/** @type {Set<string>} */
|
|
525
741
|
let query_keys;
|
|
526
742
|
/** @type {Set<string>} */
|
|
527
743
|
let live_query_keys;
|
|
528
744
|
|
|
529
|
-
// Clear preload cache when
|
|
745
|
+
// Clear preload cache when refreshAll is true to ensure fresh data
|
|
530
746
|
// after form submissions or explicit invalidations
|
|
531
|
-
if (options.
|
|
747
|
+
if (options.refreshAll) {
|
|
532
748
|
discard_load_cache();
|
|
533
749
|
}
|
|
534
750
|
|
|
535
751
|
await navigate({
|
|
536
|
-
type: 'goto',
|
|
752
|
+
type: options.type ?? 'goto',
|
|
537
753
|
url: resolve_url(url),
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
replace_state: options.replaceState,
|
|
754
|
+
reset: options.reset,
|
|
755
|
+
replace_state: options.replace,
|
|
541
756
|
state: options.state,
|
|
757
|
+
persist_state: options.persistState,
|
|
758
|
+
event: options.event,
|
|
759
|
+
action_result: options.action_result,
|
|
542
760
|
redirect_count,
|
|
543
761
|
nav_token,
|
|
762
|
+
intent,
|
|
544
763
|
accept: () => {
|
|
545
|
-
if (options.
|
|
764
|
+
if (options.refreshAll) {
|
|
546
765
|
force_invalidation = true;
|
|
547
766
|
query_keys = new Set();
|
|
548
767
|
for (const [id, entries] of query_map) {
|
|
549
|
-
for (const payload of entries
|
|
768
|
+
for (const [payload, entry] of entries) {
|
|
769
|
+
// don't refresh yet, as some queries will be unrendered,
|
|
770
|
+
// but clear caches so that newly rendered queries
|
|
771
|
+
// don't use stale data. TODO same for `live_query_map`
|
|
772
|
+
entry.resource?.reset();
|
|
550
773
|
query_keys.add(create_remote_key(id, payload));
|
|
551
774
|
}
|
|
552
775
|
}
|
|
@@ -564,17 +787,16 @@ export async function _goto(url, options, redirect_count, nav_token) {
|
|
|
564
787
|
}
|
|
565
788
|
});
|
|
566
789
|
|
|
567
|
-
if (options.
|
|
790
|
+
if (options.refreshAll) {
|
|
568
791
|
// TODO the ticks shouldn't be necessary, something inside Svelte itself is buggy
|
|
569
792
|
// when a query in a layout that still exists after page change is refreshed earlier than this
|
|
570
|
-
void
|
|
571
|
-
.tick
|
|
572
|
-
.then(svelte.tick)
|
|
793
|
+
void tick()
|
|
794
|
+
.then(tick)
|
|
573
795
|
.then(() => {
|
|
574
796
|
for (const [id, entries] of query_map) {
|
|
575
797
|
for (const [payload, { resource }] of entries) {
|
|
576
798
|
if (query_keys?.has(create_remote_key(id, payload))) {
|
|
577
|
-
void resource.
|
|
799
|
+
void resource.start();
|
|
578
800
|
}
|
|
579
801
|
}
|
|
580
802
|
}
|
|
@@ -603,18 +825,15 @@ async function _preload_data(intent) {
|
|
|
603
825
|
load_cache = {
|
|
604
826
|
id: intent.id,
|
|
605
827
|
token: preload,
|
|
606
|
-
promise: load_route({ ...intent, preload }).
|
|
828
|
+
promise: load_route({ ...intent, preload }).finally(() => {
|
|
607
829
|
preload_tokens.delete(preload);
|
|
608
|
-
if (result.type === 'loaded' && result.state.error) {
|
|
609
|
-
// Don't cache errors, because they might be transient
|
|
610
|
-
discard_load_cache();
|
|
611
|
-
}
|
|
612
|
-
return result;
|
|
613
830
|
}),
|
|
614
831
|
fork: null
|
|
615
832
|
};
|
|
616
833
|
|
|
617
|
-
|
|
834
|
+
load_cache.promise.catch(discard_load_cache);
|
|
835
|
+
|
|
836
|
+
if (__SVELTEKIT_FORK_PRELOADS__) {
|
|
618
837
|
const lc = load_cache;
|
|
619
838
|
|
|
620
839
|
lc.fork = lc.promise.then((result) => {
|
|
@@ -622,9 +841,8 @@ async function _preload_data(intent) {
|
|
|
622
841
|
// resolve, bail rather than creating an orphan fork
|
|
623
842
|
if (lc === load_cache && result.type === 'loaded') {
|
|
624
843
|
try {
|
|
625
|
-
return
|
|
626
|
-
|
|
627
|
-
update(result.props.page);
|
|
844
|
+
return fork(() => {
|
|
845
|
+
apply_navigation_result(result);
|
|
628
846
|
});
|
|
629
847
|
} catch {
|
|
630
848
|
// if it errors, it's because the experimental flag isn't enabled in Svelte
|
|
@@ -639,6 +857,53 @@ async function _preload_data(intent) {
|
|
|
639
857
|
return load_cache.promise;
|
|
640
858
|
}
|
|
641
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Fetch and parse the route with the given ID from the server-side route resolution endpoint.
|
|
862
|
+
* Returns `ENDPOINT_ONLY` if the route exists but has no code to preload, or `undefined` if
|
|
863
|
+
* there is no such route. Only used when `router.resolution === 'server'`.
|
|
864
|
+
* @param {string} id
|
|
865
|
+
* @returns {Promise<import('types').CSRRoute | typeof ENDPOINT_ONLY | undefined>}
|
|
866
|
+
*/
|
|
867
|
+
async function load_route_by_id(id) {
|
|
868
|
+
/** @type {{ route?: import('types').CSRRouteServer, endpoint_only?: boolean }} */
|
|
869
|
+
let module;
|
|
870
|
+
|
|
871
|
+
try {
|
|
872
|
+
module = await import(
|
|
873
|
+
/* @vite-ignore */
|
|
874
|
+
base + route_id_resolution_pathname(id)
|
|
875
|
+
);
|
|
876
|
+
} catch {
|
|
877
|
+
// if there's no module at that path the response is a 404 (or, on a static
|
|
878
|
+
// host, fallback HTML with the wrong MIME type) and the import rejects —
|
|
879
|
+
// treat it the same as an unknown route rather than surfacing a cryptic error
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (module.endpoint_only) {
|
|
884
|
+
// The route exists, it just has no code to preload.
|
|
885
|
+
route_id_cache.set(id, ENDPOINT_ONLY);
|
|
886
|
+
return ENDPOINT_ONLY;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (!module.route) return;
|
|
890
|
+
|
|
891
|
+
return parse_and_cache_server_route(module.route);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Import the modules for a route's layout and leaf nodes, without running `load` functions.
|
|
896
|
+
* @param {import('types').CSRRoute} route
|
|
897
|
+
* @returns {Promise<void>}
|
|
898
|
+
*/
|
|
899
|
+
async function load_route_nodes(route) {
|
|
900
|
+
await Promise.all(
|
|
901
|
+
/** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ (
|
|
902
|
+
[...route.layouts, route.leaf].filter(Boolean)
|
|
903
|
+
).map(([, node_loader]) => node_loader())
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
642
907
|
/**
|
|
643
908
|
* @param {URL} url
|
|
644
909
|
* @returns {Promise<void>}
|
|
@@ -647,24 +912,20 @@ async function _preload_code(url) {
|
|
|
647
912
|
const route = (await get_navigation_intent(url, false))?.route;
|
|
648
913
|
|
|
649
914
|
if (route) {
|
|
650
|
-
await
|
|
651
|
-
/** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ (
|
|
652
|
-
[...route.layouts, route.leaf].filter(Boolean)
|
|
653
|
-
).map((load) => load[1]())
|
|
654
|
-
);
|
|
915
|
+
await load_route_nodes(route);
|
|
655
916
|
}
|
|
656
917
|
}
|
|
657
918
|
|
|
658
919
|
/**
|
|
659
920
|
* @param {import('./types.js').NavigationFinished} result
|
|
660
921
|
* @param {HTMLElement} target
|
|
661
|
-
* @param {boolean}
|
|
922
|
+
* @param {boolean} should_hydrate
|
|
662
923
|
*/
|
|
663
|
-
async function initialize(result, target,
|
|
924
|
+
async function initialize(result, target, should_hydrate) {
|
|
664
925
|
if (__SVELTEKIT_DEV__ && result.state.error && document.querySelector('vite-error-overlay'))
|
|
665
926
|
return;
|
|
666
927
|
|
|
667
|
-
/** @type {
|
|
928
|
+
/** @type {NavigationEvent} */
|
|
668
929
|
const nav = {
|
|
669
930
|
params: current.params,
|
|
670
931
|
route: { id: current.route?.id ?? null },
|
|
@@ -682,48 +943,46 @@ async function initialize(result, target, hydrate) {
|
|
|
682
943
|
if (style) style.remove();
|
|
683
944
|
}
|
|
684
945
|
|
|
685
|
-
|
|
946
|
+
apply_navigation_result(result);
|
|
947
|
+
|
|
948
|
+
// TODO treeshake `hydrate` in csr mode
|
|
949
|
+
const render = should_hydrate ? hydrate : mount;
|
|
686
950
|
|
|
687
|
-
|
|
951
|
+
render(Root, {
|
|
688
952
|
target,
|
|
689
|
-
props
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const error = await handle_error(e, current.nav);
|
|
697
|
-
rendering_error = { error, status: get_status(e) };
|
|
698
|
-
page.error = error;
|
|
699
|
-
page.status = rendering_error.status;
|
|
700
|
-
return error;
|
|
701
|
-
}
|
|
702
|
-
: undefined
|
|
953
|
+
props,
|
|
954
|
+
transformError: /** @param {unknown} e */ async (e) => {
|
|
955
|
+
const error = await handle_error(e, current.nav);
|
|
956
|
+
page.error = error;
|
|
957
|
+
page.status = error.status;
|
|
958
|
+
return error;
|
|
959
|
+
}
|
|
703
960
|
});
|
|
704
961
|
|
|
705
962
|
// Wait for a microtask in case svelte experimental async is enabled,
|
|
706
963
|
// which causes component script blocks to run asynchronously
|
|
707
964
|
void (await Promise.resolve());
|
|
708
965
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (hydrate) {
|
|
712
|
-
/** @type {import('@sveltejs/kit').AfterNavigate} */
|
|
966
|
+
if (should_hydrate) {
|
|
967
|
+
/** @type {AfterNavigate} */
|
|
713
968
|
const navigation = {
|
|
714
969
|
from: null,
|
|
715
970
|
to: {
|
|
716
971
|
...nav,
|
|
717
|
-
scroll:
|
|
972
|
+
scroll: history_info[current_history_index]?.scroll ?? scroll_state()
|
|
718
973
|
},
|
|
719
974
|
willUnload: false,
|
|
720
975
|
type: 'enter',
|
|
976
|
+
shallow: false,
|
|
721
977
|
complete: Promise.resolve()
|
|
722
978
|
};
|
|
723
979
|
|
|
724
980
|
after_navigate_callbacks.forEach((fn) => fn(navigation));
|
|
725
981
|
}
|
|
726
982
|
|
|
983
|
+
restore_snapshot(current_navigation_index);
|
|
984
|
+
restore_navigation_snapshot(current_history_index);
|
|
985
|
+
|
|
727
986
|
started = true;
|
|
728
987
|
}
|
|
729
988
|
|
|
@@ -734,7 +993,7 @@ async function initialize(result, target, hydrate) {
|
|
|
734
993
|
* params: Record<string, string>;
|
|
735
994
|
* branch: Array<import('./types.js').BranchNode | undefined>;
|
|
736
995
|
* errors?: Array<import('types').CSRPageNodeLoader | undefined>;
|
|
737
|
-
* status
|
|
996
|
+
* status?: number;
|
|
738
997
|
* error: App.Error | null;
|
|
739
998
|
* route: import('types').CSRRoute | null;
|
|
740
999
|
* form?: Record<string, any> | null;
|
|
@@ -778,35 +1037,12 @@ async function get_navigation_result_from_branch({
|
|
|
778
1037
|
route
|
|
779
1038
|
},
|
|
780
1039
|
props: {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
page: clone_page(page)
|
|
1040
|
+
page,
|
|
1041
|
+
tree: /** @type {RenderNode} */ ({})
|
|
784
1042
|
}
|
|
785
1043
|
};
|
|
786
1044
|
|
|
787
|
-
if (
|
|
788
|
-
let last_idx = -1;
|
|
789
|
-
result.props.errors = await Promise.all(
|
|
790
|
-
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
791
|
-
branch
|
|
792
|
-
.map((b, i) => {
|
|
793
|
-
if (i === 0) return undefined; // root layout wraps root error component, not the other way around
|
|
794
|
-
if (!b) return null;
|
|
795
|
-
|
|
796
|
-
i--;
|
|
797
|
-
// Find the closest error component up to the previous branch
|
|
798
|
-
while (i > last_idx + 1 && !errors[i]) i -= 1;
|
|
799
|
-
last_idx = i;
|
|
800
|
-
return errors[i]?.()
|
|
801
|
-
.then((e) => e.component)
|
|
802
|
-
.catch(() => undefined);
|
|
803
|
-
})
|
|
804
|
-
// filter out indexes where there was no branch, but keep indexes where there was a branch but no error component
|
|
805
|
-
.filter((e) => e !== null)
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if (error && __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__) {
|
|
1045
|
+
if (error) {
|
|
810
1046
|
result.props.error = error;
|
|
811
1047
|
}
|
|
812
1048
|
|
|
@@ -817,23 +1053,53 @@ async function get_navigation_result_from_branch({
|
|
|
817
1053
|
let data = {};
|
|
818
1054
|
let data_changed = !page;
|
|
819
1055
|
|
|
820
|
-
let
|
|
1056
|
+
let current_node = result.props.tree;
|
|
1057
|
+
|
|
1058
|
+
/** @type {RenderNode | undefined} */
|
|
1059
|
+
let previous_node = props.tree;
|
|
821
1060
|
|
|
822
|
-
for (let i = 0; i <
|
|
1061
|
+
for (let i = 0; i < branch.length; i += 1) {
|
|
823
1062
|
const node = branch[i];
|
|
824
1063
|
const prev = current.branch[i];
|
|
825
1064
|
|
|
826
|
-
if (node?.data !== prev?.data) data_changed = true;
|
|
827
1065
|
if (!node) continue;
|
|
828
1066
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1067
|
+
if (
|
|
1068
|
+
// if an ancestor node in this path changed, `data_changed` is already true and the
|
|
1069
|
+
// accumulated `data` differs from the previous render, so we must re-merge
|
|
1070
|
+
data_changed ||
|
|
1071
|
+
!previous_node ||
|
|
1072
|
+
node?.data !== prev?.data
|
|
1073
|
+
) {
|
|
1074
|
+
current_node.data = { ...data, ...node.data };
|
|
1075
|
+
data_changed = true;
|
|
1076
|
+
} else {
|
|
1077
|
+
// use existing object — prevents effects re-running unnecessarily
|
|
1078
|
+
current_node.data = previous_node.data;
|
|
834
1079
|
}
|
|
835
1080
|
|
|
836
|
-
|
|
1081
|
+
data = current_node.data;
|
|
1082
|
+
|
|
1083
|
+
if (i < branch.length - 1) {
|
|
1084
|
+
let next_index = i + 1;
|
|
1085
|
+
while (next_index < branch.length && !branch[next_index]) {
|
|
1086
|
+
next_index += 1;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const next = branch[next_index];
|
|
1090
|
+
|
|
1091
|
+
if (next) {
|
|
1092
|
+
const error_loader =
|
|
1093
|
+
errors?.slice(0, next_index + 1).findLast((x) => x) ?? default_error_loader;
|
|
1094
|
+
|
|
1095
|
+
current_node = current_node.child = new RenderNode(
|
|
1096
|
+
next.node.component,
|
|
1097
|
+
(await error_loader()).component
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
previous_node = previous_node?.child;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
837
1103
|
}
|
|
838
1104
|
|
|
839
1105
|
const page_changed =
|
|
@@ -851,11 +1117,11 @@ async function get_navigation_result_from_branch({
|
|
|
851
1117
|
id: route?.id ?? null
|
|
852
1118
|
},
|
|
853
1119
|
state: {},
|
|
854
|
-
|
|
1120
|
+
shallow: null,
|
|
1121
|
+
status: status ?? error?.status ?? 200,
|
|
855
1122
|
url: new URL(url),
|
|
856
1123
|
form: form ?? null,
|
|
857
|
-
|
|
858
|
-
data: data_changed ? data : page.data
|
|
1124
|
+
data
|
|
859
1125
|
};
|
|
860
1126
|
}
|
|
861
1127
|
|
|
@@ -921,7 +1187,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node
|
|
|
921
1187
|
}
|
|
922
1188
|
}
|
|
923
1189
|
|
|
924
|
-
/** @type {
|
|
1190
|
+
/** @type {LoadEvent} */
|
|
925
1191
|
const load_input = {
|
|
926
1192
|
tracing: { enabled: false, root: noop_span, current: noop_span },
|
|
927
1193
|
route: new Proxy(route, {
|
|
@@ -1134,32 +1400,21 @@ function diff_search_params(old_url, new_url) {
|
|
|
1134
1400
|
}
|
|
1135
1401
|
|
|
1136
1402
|
/**
|
|
1137
|
-
* @
|
|
1138
|
-
* @
|
|
1403
|
+
* @overload
|
|
1404
|
+
* @param {import('./types.js').NavigationIntent & { action_result?: ActionResult }} intent
|
|
1405
|
+
* @returns {Promise<import('./types.js').NavigationResult | undefined>}
|
|
1139
1406
|
*/
|
|
1140
|
-
function preload_error({ error, url, route, params }) {
|
|
1141
|
-
return {
|
|
1142
|
-
type: 'loaded',
|
|
1143
|
-
state: {
|
|
1144
|
-
error,
|
|
1145
|
-
url,
|
|
1146
|
-
route,
|
|
1147
|
-
params,
|
|
1148
|
-
branch: []
|
|
1149
|
-
},
|
|
1150
|
-
props: {
|
|
1151
|
-
page: clone_page(page),
|
|
1152
|
-
constructors: []
|
|
1153
|
-
}
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
1407
|
/**
|
|
1158
|
-
* @
|
|
1408
|
+
* @overload
|
|
1409
|
+
* @param {import('./types.js').NavigationIntent & { preload: {} }} intent
|
|
1159
1410
|
* @returns {Promise<import('./types.js').NavigationResult>}
|
|
1160
1411
|
*/
|
|
1161
|
-
|
|
1162
|
-
|
|
1412
|
+
/**
|
|
1413
|
+
* @param {import('./types.js').NavigationIntent & { preload?: {}; action_result?: ActionResult }} intent
|
|
1414
|
+
* @returns {Promise<import('./types.js').NavigationResult | undefined>}
|
|
1415
|
+
*/
|
|
1416
|
+
async function load_route({ id, invalidating, url, params, route, preload, action_result }) {
|
|
1417
|
+
if (!action_result && load_cache?.id === id) {
|
|
1163
1418
|
// the preload becomes the real navigation
|
|
1164
1419
|
preload_tokens.delete(load_cache.token);
|
|
1165
1420
|
return load_cache.promise;
|
|
@@ -1168,6 +1423,8 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1168
1423
|
const { errors, layouts, leaf } = route;
|
|
1169
1424
|
|
|
1170
1425
|
const loaders = [...layouts, leaf];
|
|
1426
|
+
const leaf_index = loaders.length - 1;
|
|
1427
|
+
const action_error = action_result?.type === 'error';
|
|
1171
1428
|
|
|
1172
1429
|
// preload modules to avoid waterfall, but handle rejections
|
|
1173
1430
|
// so they don't get reported to Sentry et al (we don't need
|
|
@@ -1178,13 +1435,16 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1178
1435
|
/** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
|
|
1179
1436
|
let server_data = null;
|
|
1180
1437
|
const url_changed = current.url ? id !== get_page_key(current.url) : false;
|
|
1181
|
-
|
|
1438
|
+
// current.route is null after an error-page render, so a missing route counts as changed
|
|
1439
|
+
const route_changed = !current.route || route.id !== current.route.id;
|
|
1182
1440
|
const search_params_changed = diff_search_params(current.url, url);
|
|
1183
1441
|
|
|
1184
1442
|
let parent_invalid = false;
|
|
1185
1443
|
|
|
1186
1444
|
if (__SVELTEKIT_HAS_SERVER_LOAD__) {
|
|
1187
1445
|
const invalid_server_nodes = loaders.map((loader, i) => {
|
|
1446
|
+
if (action_error && i === leaf_index) return false;
|
|
1447
|
+
|
|
1188
1448
|
const previous = current.branch[i];
|
|
1189
1449
|
|
|
1190
1450
|
const invalid =
|
|
@@ -1213,12 +1473,11 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1213
1473
|
} catch (error) {
|
|
1214
1474
|
const handled_error = await handle_error(error, { url, params, route: { id } });
|
|
1215
1475
|
|
|
1216
|
-
if (preload_tokens.has(preload)) {
|
|
1217
|
-
|
|
1476
|
+
if (preload && preload_tokens.has(preload)) {
|
|
1477
|
+
throw handled_error;
|
|
1218
1478
|
}
|
|
1219
1479
|
|
|
1220
1480
|
return load_root_error_page({
|
|
1221
|
-
status: get_status(error),
|
|
1222
1481
|
error: handled_error,
|
|
1223
1482
|
url,
|
|
1224
1483
|
route
|
|
@@ -1236,7 +1495,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1236
1495
|
let parent_changed = false;
|
|
1237
1496
|
|
|
1238
1497
|
const branch_promises = loaders.map(async (loader, i) => {
|
|
1239
|
-
if (!loader) return;
|
|
1498
|
+
if (!loader || (action_error && i === leaf_index)) return;
|
|
1240
1499
|
|
|
1241
1500
|
/** @type {import('./types.js').BranchNode | undefined} */
|
|
1242
1501
|
const previous = current.branch[i];
|
|
@@ -1261,7 +1520,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1261
1520
|
|
|
1262
1521
|
if (server_data_node?.type === 'error') {
|
|
1263
1522
|
// rethrow and catch below
|
|
1264
|
-
throw server_data_node;
|
|
1523
|
+
throw new HandledHttpError(server_data_node.error);
|
|
1265
1524
|
}
|
|
1266
1525
|
|
|
1267
1526
|
return load_node({
|
|
@@ -1299,34 +1558,27 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1299
1558
|
if (err instanceof Redirect) {
|
|
1300
1559
|
return {
|
|
1301
1560
|
type: 'redirect',
|
|
1561
|
+
status: err.status,
|
|
1302
1562
|
location: err.location
|
|
1303
1563
|
};
|
|
1304
1564
|
}
|
|
1305
1565
|
|
|
1306
|
-
if (preload_tokens.has(preload)) {
|
|
1307
|
-
|
|
1308
|
-
error: await handle_error(err, { params, url, route: { id: route.id } }),
|
|
1309
|
-
url,
|
|
1310
|
-
params,
|
|
1311
|
-
route
|
|
1312
|
-
});
|
|
1566
|
+
if (preload && preload_tokens.has(preload)) {
|
|
1567
|
+
throw await handle_error(err, { params, url, route: { id: route.id } });
|
|
1313
1568
|
}
|
|
1314
1569
|
|
|
1315
|
-
let status = get_status(err);
|
|
1316
1570
|
/** @type {App.Error} */
|
|
1317
1571
|
let error;
|
|
1318
1572
|
|
|
1319
1573
|
if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
|
|
1320
1574
|
// this is the server error rethrown above, reconstruct but don't invoke
|
|
1321
1575
|
// the client error handler; it should've already been handled on the server
|
|
1322
|
-
status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
|
|
1323
1576
|
error = /** @type {import('types').ServerErrorNode} */ (err).error;
|
|
1324
1577
|
} else if (err instanceof HttpError) {
|
|
1325
|
-
error = err.
|
|
1578
|
+
error = await handle_error(err, { params, url, route: { id: route.id } });
|
|
1326
1579
|
} else {
|
|
1327
1580
|
// Referenced node could have been removed due to redeploy, check
|
|
1328
|
-
|
|
1329
|
-
if (updated) {
|
|
1581
|
+
if (await updated.check()) {
|
|
1330
1582
|
// Before reloading, try to update the service worker if it exists
|
|
1331
1583
|
await update_service_worker();
|
|
1332
1584
|
return await native_navigation(url);
|
|
@@ -1335,20 +1587,15 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1335
1587
|
error = await handle_error(err, { params, url, route: { id: route.id } });
|
|
1336
1588
|
}
|
|
1337
1589
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
route
|
|
1348
|
-
});
|
|
1349
|
-
} else {
|
|
1350
|
-
return await server_fallback(url, { id: route.id }, error, status);
|
|
1351
|
-
}
|
|
1590
|
+
return load_route_error({
|
|
1591
|
+
i,
|
|
1592
|
+
branch,
|
|
1593
|
+
errors,
|
|
1594
|
+
error,
|
|
1595
|
+
url,
|
|
1596
|
+
params,
|
|
1597
|
+
route
|
|
1598
|
+
});
|
|
1352
1599
|
}
|
|
1353
1600
|
} else {
|
|
1354
1601
|
// push an empty slot so we can rewind past gaps to the
|
|
@@ -1357,16 +1604,37 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
|
|
|
1357
1604
|
}
|
|
1358
1605
|
}
|
|
1359
1606
|
|
|
1607
|
+
if (action_result?.type === 'error') {
|
|
1608
|
+
// render the nearest error boundary of the route the action belongs to,
|
|
1609
|
+
// as a native form submission would
|
|
1610
|
+
return load_route_error({
|
|
1611
|
+
i: loaders.length,
|
|
1612
|
+
branch,
|
|
1613
|
+
errors,
|
|
1614
|
+
error: action_result.error,
|
|
1615
|
+
status: action_result.status,
|
|
1616
|
+
url,
|
|
1617
|
+
params,
|
|
1618
|
+
route
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1360
1622
|
return get_navigation_result_from_branch({
|
|
1361
1623
|
url,
|
|
1362
1624
|
params,
|
|
1363
1625
|
branch,
|
|
1364
1626
|
errors,
|
|
1365
|
-
status: 200,
|
|
1366
1627
|
error: null,
|
|
1367
1628
|
route,
|
|
1368
|
-
|
|
1369
|
-
|
|
1629
|
+
status: action_result?.status,
|
|
1630
|
+
// `error` results returned above, `redirect` results are never passed here
|
|
1631
|
+
form:
|
|
1632
|
+
action_result?.type === 'success' || action_result?.type === 'failure'
|
|
1633
|
+
? (action_result.data ?? null)
|
|
1634
|
+
: // Reset `form` on navigation, but not invalidation
|
|
1635
|
+
invalidating
|
|
1636
|
+
? undefined
|
|
1637
|
+
: null
|
|
1370
1638
|
});
|
|
1371
1639
|
}
|
|
1372
1640
|
|
|
@@ -1401,14 +1669,42 @@ async function load_nearest_error_page(i, branch, errors) {
|
|
|
1401
1669
|
|
|
1402
1670
|
/**
|
|
1403
1671
|
* @param {{
|
|
1404
|
-
*
|
|
1672
|
+
* i: number;
|
|
1673
|
+
* branch: Array<import('./types.js').BranchNode | undefined>;
|
|
1674
|
+
* errors: Array<import('types').CSRPageNodeLoader | undefined>;
|
|
1675
|
+
* error: App.Error;
|
|
1676
|
+
* status?: number;
|
|
1677
|
+
* url: URL;
|
|
1678
|
+
* params: Record<string, string>;
|
|
1679
|
+
* route: import('./types.js').NavigationIntent['route'];
|
|
1680
|
+
* }} opts
|
|
1681
|
+
*/
|
|
1682
|
+
async function load_route_error({ i, branch, errors, error, status, url, params, route }) {
|
|
1683
|
+
const error_load = await load_nearest_error_page(i, branch, errors);
|
|
1684
|
+
if (error_load) {
|
|
1685
|
+
return get_navigation_result_from_branch({
|
|
1686
|
+
url,
|
|
1687
|
+
params,
|
|
1688
|
+
branch: branch.slice(0, error_load.idx).concat(error_load.node),
|
|
1689
|
+
errors,
|
|
1690
|
+
error,
|
|
1691
|
+
status,
|
|
1692
|
+
route
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
return server_fallback(url, { id: route.id }, error);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* @param {{
|
|
1405
1701
|
* error: App.Error;
|
|
1406
1702
|
* url: URL;
|
|
1407
1703
|
* route: { id: string | null }
|
|
1408
1704
|
* }} opts
|
|
1409
|
-
* @returns {Promise<import('./types.js').NavigationFinished>}
|
|
1705
|
+
* @returns {Promise<import('./types.js').NavigationFinished | undefined>} returns `undefined` in case of a redirect
|
|
1410
1706
|
*/
|
|
1411
|
-
async function load_root_error_page({
|
|
1707
|
+
async function load_root_error_page({ error, url, route }) {
|
|
1412
1708
|
/** @type {Record<string, string>} */
|
|
1413
1709
|
const params = {}; // error page does not have params
|
|
1414
1710
|
|
|
@@ -1432,11 +1728,15 @@ async function load_root_error_page({ status, error, url, route }) {
|
|
|
1432
1728
|
}
|
|
1433
1729
|
|
|
1434
1730
|
server_data_node = server_data.nodes[0] ?? null;
|
|
1435
|
-
} catch {
|
|
1731
|
+
} catch (e) {
|
|
1436
1732
|
// at this point we have no choice but to fall back to the server, if it wouldn't
|
|
1437
|
-
// bring us right back here, turning this into an endless loop
|
|
1438
|
-
if
|
|
1439
|
-
|
|
1733
|
+
// bring us right back here, turning this into an endless loop.
|
|
1734
|
+
// if __data.json returned 404, the route doesn't exist — don't reload or we loop
|
|
1735
|
+
if (
|
|
1736
|
+
!(e instanceof HttpError && e.status === 404) &&
|
|
1737
|
+
(url.origin !== origin || url.pathname !== location.pathname || hydrated)
|
|
1738
|
+
) {
|
|
1739
|
+
return await native_navigation(url);
|
|
1440
1740
|
}
|
|
1441
1741
|
}
|
|
1442
1742
|
}
|
|
@@ -1465,18 +1765,30 @@ async function load_root_error_page({ status, error, url, route }) {
|
|
|
1465
1765
|
url,
|
|
1466
1766
|
params,
|
|
1467
1767
|
branch: [root_layout, root_error],
|
|
1468
|
-
status,
|
|
1469
1768
|
error,
|
|
1470
1769
|
errors: [],
|
|
1471
1770
|
route: null
|
|
1472
1771
|
});
|
|
1473
1772
|
} catch (error) {
|
|
1773
|
+
// client-side navigation if the root layout loader throws a redirect while
|
|
1774
|
+
// rendering the default error page
|
|
1474
1775
|
if (error instanceof Redirect) {
|
|
1475
|
-
|
|
1476
|
-
return
|
|
1776
|
+
await _goto(new URL(error.location, location.href));
|
|
1777
|
+
return;
|
|
1477
1778
|
}
|
|
1478
1779
|
|
|
1479
|
-
//
|
|
1780
|
+
// otherwise, render the static error page
|
|
1781
|
+
const error_template = await app.get_error_template();
|
|
1782
|
+
const handled = await handle_error(error, { url, params, route });
|
|
1783
|
+
const message = String(handled?.message ?? '')
|
|
1784
|
+
.replace(/&/g, '&')
|
|
1785
|
+
.replace(/</g, '<')
|
|
1786
|
+
.replace(/>/g, '>');
|
|
1787
|
+
const html = error_template({ status: handled.status, message });
|
|
1788
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
1789
|
+
document.documentElement.replaceChild(document.adoptNode(parsed.head), document.head);
|
|
1790
|
+
document.documentElement.replaceChild(document.adoptNode(parsed.body), document.body);
|
|
1791
|
+
|
|
1480
1792
|
throw error;
|
|
1481
1793
|
}
|
|
1482
1794
|
}
|
|
@@ -1566,7 +1878,7 @@ export async function get_navigation_intent(url, invalidating) {
|
|
|
1566
1878
|
id: get_page_key(url),
|
|
1567
1879
|
invalidating,
|
|
1568
1880
|
route,
|
|
1569
|
-
params
|
|
1881
|
+
params,
|
|
1570
1882
|
url
|
|
1571
1883
|
};
|
|
1572
1884
|
}
|
|
@@ -1583,7 +1895,7 @@ export async function get_navigation_intent(url, invalidating) {
|
|
|
1583
1895
|
return {
|
|
1584
1896
|
id: get_page_key(url),
|
|
1585
1897
|
invalidating,
|
|
1586
|
-
route:
|
|
1898
|
+
route: parse_and_cache_server_route(route),
|
|
1587
1899
|
params,
|
|
1588
1900
|
url
|
|
1589
1901
|
};
|
|
@@ -1607,17 +1919,19 @@ function get_page_key(url) {
|
|
|
1607
1919
|
/**
|
|
1608
1920
|
* @param {{
|
|
1609
1921
|
* url: URL;
|
|
1610
|
-
* type:
|
|
1922
|
+
* type: Navigation["type"];
|
|
1611
1923
|
* intent?: import('./types.js').NavigationIntent;
|
|
1612
1924
|
* delta?: number;
|
|
1613
1925
|
* event?: PopStateEvent | MouseEvent;
|
|
1614
1926
|
* scroll?: { x: number, y: number };
|
|
1927
|
+
* shallow?: boolean;
|
|
1928
|
+
* target?: { params: Record<string, string> | null; route: { id: string } | null; url: URL };
|
|
1615
1929
|
* }} opts
|
|
1616
1930
|
*/
|
|
1617
|
-
function _before_navigate({ url, type, intent, delta, event, scroll }) {
|
|
1931
|
+
function _before_navigate({ url, type, intent, delta, event, scroll, shallow = false, target }) {
|
|
1618
1932
|
let should_block = false;
|
|
1619
1933
|
|
|
1620
|
-
const nav = create_navigation(current, intent, url, type, scroll ?? null);
|
|
1934
|
+
const nav = create_navigation(current, intent, url, type, scroll ?? null, shallow, target);
|
|
1621
1935
|
|
|
1622
1936
|
if (nav.navigation.type === 'popstate' && delta !== undefined) {
|
|
1623
1937
|
nav.navigation.delta = delta;
|
|
@@ -1638,6 +1952,9 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
|
|
|
1638
1952
|
|
|
1639
1953
|
if (!is_navigating) {
|
|
1640
1954
|
// Don't run the event during redirects
|
|
1955
|
+
// TODO this isn't fully right: if you do a goto(...) while another goto(...) is in progress,
|
|
1956
|
+
// or you click a link while a navigation is in progress, the beforNavigate calls are not triggered,
|
|
1957
|
+
// and maybe they should be?
|
|
1641
1958
|
before_navigate_callbacks.forEach((fn) => fn(cancellable));
|
|
1642
1959
|
}
|
|
1643
1960
|
|
|
@@ -1646,42 +1963,49 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
|
|
|
1646
1963
|
|
|
1647
1964
|
/**
|
|
1648
1965
|
* @param {{
|
|
1649
|
-
* type:
|
|
1966
|
+
* type: NavigationType;
|
|
1650
1967
|
* url: URL;
|
|
1651
1968
|
* popped?: {
|
|
1652
1969
|
* state: Record<string, any>;
|
|
1653
|
-
* scroll
|
|
1970
|
+
* scroll?: { x: number, y: number };
|
|
1654
1971
|
* delta: number;
|
|
1972
|
+
* shallow: { params: Record<string, string> | null; route: { id: string } | null; url: URL } | null;
|
|
1655
1973
|
* };
|
|
1656
|
-
*
|
|
1657
|
-
* noscroll?: boolean;
|
|
1974
|
+
* reset?: boolean;
|
|
1658
1975
|
* replace_state?: boolean;
|
|
1659
1976
|
* state?: Record<string, any>;
|
|
1977
|
+
* persist_state?: boolean;
|
|
1660
1978
|
* redirect_count?: number;
|
|
1661
1979
|
* nav_token?: {};
|
|
1662
1980
|
* accept?: () => void;
|
|
1663
1981
|
* block?: () => void;
|
|
1664
|
-
* event?: Event
|
|
1982
|
+
* event?: Event;
|
|
1983
|
+
* intent?: NavigationIntent | undefined;
|
|
1984
|
+
* action_result?: ActionResult;
|
|
1665
1985
|
* }} opts
|
|
1986
|
+
* @returns {Promise<void>}
|
|
1666
1987
|
*/
|
|
1667
1988
|
async function navigate({
|
|
1668
1989
|
type,
|
|
1669
1990
|
url,
|
|
1670
1991
|
popped,
|
|
1671
|
-
|
|
1672
|
-
noscroll,
|
|
1992
|
+
reset = true,
|
|
1673
1993
|
replace_state,
|
|
1674
1994
|
state = {},
|
|
1995
|
+
persist_state = false,
|
|
1675
1996
|
redirect_count = 0,
|
|
1676
1997
|
nav_token = {},
|
|
1677
1998
|
accept = noop,
|
|
1678
1999
|
block = noop,
|
|
1679
|
-
event
|
|
2000
|
+
event,
|
|
2001
|
+
intent,
|
|
2002
|
+
action_result
|
|
1680
2003
|
}) {
|
|
1681
|
-
const prev_token =
|
|
1682
|
-
|
|
2004
|
+
const prev_token = navigation_token;
|
|
2005
|
+
const prev_invalidation_token = invalidation_token;
|
|
2006
|
+
navigation_token = invalidation_token = nav_token;
|
|
1683
2007
|
|
|
1684
|
-
|
|
2008
|
+
intent ??= await get_navigation_intent(url, false);
|
|
1685
2009
|
const nav =
|
|
1686
2010
|
type === 'enter'
|
|
1687
2011
|
? create_navigation(current, intent, url, type)
|
|
@@ -1691,29 +2015,33 @@ async function navigate({
|
|
|
1691
2015
|
delta: popped?.delta,
|
|
1692
2016
|
intent,
|
|
1693
2017
|
scroll: popped?.scroll,
|
|
2018
|
+
shallow: !!popped?.shallow,
|
|
2019
|
+
target: popped?.shallow ?? undefined,
|
|
1694
2020
|
// @ts-ignore
|
|
1695
2021
|
event
|
|
1696
2022
|
});
|
|
1697
2023
|
|
|
1698
2024
|
if (!nav) {
|
|
1699
2025
|
block();
|
|
1700
|
-
if (
|
|
2026
|
+
if (navigation_token === nav_token) navigation_token = prev_token;
|
|
2027
|
+
if (invalidation_token === nav_token) invalidation_token = prev_invalidation_token;
|
|
1701
2028
|
return;
|
|
1702
2029
|
}
|
|
1703
2030
|
|
|
1704
2031
|
// store this before calling `accept()`, which may change the index
|
|
1705
2032
|
const previous_history_index = current_history_index;
|
|
1706
2033
|
const previous_navigation_index = current_navigation_index;
|
|
2034
|
+
const previous_snapshot_registrations = current_registrations();
|
|
1707
2035
|
|
|
1708
2036
|
accept();
|
|
1709
2037
|
|
|
1710
2038
|
is_navigating = true;
|
|
1711
2039
|
|
|
1712
2040
|
if (started && nav.navigation.type !== 'enter') {
|
|
1713
|
-
|
|
2041
|
+
navigating.current = nav.navigation;
|
|
1714
2042
|
}
|
|
1715
2043
|
|
|
1716
|
-
let navigation_result = intent && (await load_route(intent));
|
|
2044
|
+
let navigation_result = intent && (await load_route({ ...intent, action_result }));
|
|
1717
2045
|
|
|
1718
2046
|
if (!navigation_result) {
|
|
1719
2047
|
if (is_external_url(url, base, app.hash)) {
|
|
@@ -1735,7 +2063,6 @@ async function navigate({
|
|
|
1735
2063
|
route: { id: null }
|
|
1736
2064
|
}
|
|
1737
2065
|
),
|
|
1738
|
-
404,
|
|
1739
2066
|
replace_state
|
|
1740
2067
|
);
|
|
1741
2068
|
} else {
|
|
@@ -1750,7 +2077,6 @@ async function navigate({
|
|
|
1750
2077
|
params: {},
|
|
1751
2078
|
route: { id: null }
|
|
1752
2079
|
}),
|
|
1753
|
-
404,
|
|
1754
2080
|
replace_state
|
|
1755
2081
|
);
|
|
1756
2082
|
}
|
|
@@ -1761,11 +2087,13 @@ async function navigate({
|
|
|
1761
2087
|
url = intent?.url || url;
|
|
1762
2088
|
|
|
1763
2089
|
// abort if user navigated during update
|
|
1764
|
-
if (
|
|
2090
|
+
if (navigation_token !== nav_token) {
|
|
1765
2091
|
nav.reject(new Error('navigation aborted'));
|
|
1766
|
-
return
|
|
2092
|
+
return;
|
|
1767
2093
|
}
|
|
1768
2094
|
|
|
2095
|
+
if (!navigation_result) return;
|
|
2096
|
+
|
|
1769
2097
|
if (navigation_result.type === 'redirect') {
|
|
1770
2098
|
// whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
|
|
1771
2099
|
if (redirect_count < 20) {
|
|
@@ -1773,10 +2101,10 @@ async function navigate({
|
|
|
1773
2101
|
type,
|
|
1774
2102
|
url: new URL(navigation_result.location, url),
|
|
1775
2103
|
popped,
|
|
1776
|
-
|
|
1777
|
-
noscroll,
|
|
2104
|
+
reset,
|
|
1778
2105
|
replace_state,
|
|
1779
2106
|
state,
|
|
2107
|
+
persist_state,
|
|
1780
2108
|
redirect_count: redirect_count + 1,
|
|
1781
2109
|
nav_token
|
|
1782
2110
|
});
|
|
@@ -1786,7 +2114,6 @@ async function navigate({
|
|
|
1786
2114
|
}
|
|
1787
2115
|
|
|
1788
2116
|
navigation_result = await load_root_error_page({
|
|
1789
|
-
status: 500,
|
|
1790
2117
|
error: await handle_error(new Error('Redirect loop'), {
|
|
1791
2118
|
url,
|
|
1792
2119
|
params: {},
|
|
@@ -1795,12 +2122,13 @@ async function navigate({
|
|
|
1795
2122
|
url,
|
|
1796
2123
|
route: { id: null }
|
|
1797
2124
|
});
|
|
2125
|
+
|
|
2126
|
+
if (!navigation_result) return;
|
|
1798
2127
|
} else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
|
|
1799
|
-
|
|
1800
|
-
if (updated) {
|
|
2128
|
+
if (await updated.check()) {
|
|
1801
2129
|
// Before reloading, try to update the service worker if it exists
|
|
1802
2130
|
await update_service_worker();
|
|
1803
|
-
await native_navigation(url, replace_state);
|
|
2131
|
+
return await native_navigation(url, replace_state);
|
|
1804
2132
|
}
|
|
1805
2133
|
}
|
|
1806
2134
|
|
|
@@ -1810,28 +2138,48 @@ async function navigate({
|
|
|
1810
2138
|
|
|
1811
2139
|
updating = true;
|
|
1812
2140
|
|
|
1813
|
-
|
|
2141
|
+
capture_scroll(previous_history_index);
|
|
1814
2142
|
capture_snapshot(previous_navigation_index);
|
|
2143
|
+
if (replace_state) {
|
|
2144
|
+
delete_navigation_snapshot(previous_history_index);
|
|
2145
|
+
} else {
|
|
2146
|
+
capture_navigation_snapshot(previous_history_index);
|
|
2147
|
+
}
|
|
1815
2148
|
|
|
1816
2149
|
// ensure the url pathname matches the page's trailing slash option
|
|
1817
2150
|
if (navigation_result.props.page.url.pathname !== url.pathname) {
|
|
1818
2151
|
url.pathname = navigation_result.props.page.url.pathname;
|
|
1819
2152
|
}
|
|
1820
2153
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
//
|
|
2154
|
+
if (popped) {
|
|
2155
|
+
state = popped.state;
|
|
2156
|
+
} else {
|
|
2157
|
+
// we immediately serialize-then-parse to ensure that the value is
|
|
2158
|
+
// serializable, and to prevent the developer from dangerously
|
|
2159
|
+
// relying on the identity of the serialized objects
|
|
2160
|
+
const serialized_state = stringify(state);
|
|
2161
|
+
state = parse(serialized_state);
|
|
2162
|
+
|
|
2163
|
+
// Store the serialized state so the browser history can preserve custom transport values.
|
|
2164
|
+
// This is a new navigation, rather than a popstate.
|
|
1825
2165
|
const change = replace_state ? 0 : 1;
|
|
2166
|
+
if (type !== 'enter') {
|
|
2167
|
+
if (reset) current_reset_index += 1;
|
|
2168
|
+
}
|
|
1826
2169
|
|
|
1827
2170
|
const entry = {
|
|
1828
|
-
[
|
|
1829
|
-
|
|
1830
|
-
|
|
2171
|
+
[HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
|
|
2172
|
+
historyIndex: (current_history_index += change),
|
|
2173
|
+
navigationIndex: (current_navigation_index += change),
|
|
2174
|
+
state: serialized_state,
|
|
2175
|
+
persistState: persist_state,
|
|
2176
|
+
resetIndex: current_reset_index
|
|
2177
|
+
})
|
|
1831
2178
|
};
|
|
1832
2179
|
|
|
1833
2180
|
const fn = replace_state ? history.replaceState : history.pushState;
|
|
1834
2181
|
fn.call(history, entry, '', url);
|
|
2182
|
+
set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);
|
|
1835
2183
|
|
|
1836
2184
|
if (!replace_state) {
|
|
1837
2185
|
clear_onward_history(current_history_index, current_navigation_index);
|
|
@@ -1850,6 +2198,7 @@ async function navigate({
|
|
|
1850
2198
|
}
|
|
1851
2199
|
|
|
1852
2200
|
navigation_result.props.page.state = state;
|
|
2201
|
+
navigation_result.props.page.shallow = popped?.shallow ?? null;
|
|
1853
2202
|
|
|
1854
2203
|
/**
|
|
1855
2204
|
* @type {Promise<void> | undefined}
|
|
@@ -1859,9 +2208,7 @@ async function navigate({
|
|
|
1859
2208
|
const after_navigate = (
|
|
1860
2209
|
await Promise.all(
|
|
1861
2210
|
// eslint-disable-next-line @typescript-eslint/await-thenable -- we need to await because they can be asynchronous
|
|
1862
|
-
Array.from(on_navigate_callbacks, (fn) =>
|
|
1863
|
-
fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation))
|
|
1864
|
-
)
|
|
2211
|
+
Array.from(on_navigate_callbacks, (fn) => fn(/** @type {OnNavigate} */ (nav.navigation)))
|
|
1865
2212
|
)
|
|
1866
2213
|
).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function');
|
|
1867
2214
|
|
|
@@ -1880,7 +2227,13 @@ async function navigate({
|
|
|
1880
2227
|
}
|
|
1881
2228
|
|
|
1882
2229
|
// Type-casts are save because we know this resolved a proper SvelteKit route
|
|
1883
|
-
const target =
|
|
2230
|
+
const target = popped?.shallow
|
|
2231
|
+
? {
|
|
2232
|
+
params: navigation_result.state.params,
|
|
2233
|
+
route: { id: navigation_result.state.route?.id ?? null },
|
|
2234
|
+
url: navigation_result.state.url
|
|
2235
|
+
}
|
|
2236
|
+
: /** @type {NavigationTarget} */ (nav.navigation.to);
|
|
1884
2237
|
current = {
|
|
1885
2238
|
...navigation_result.state,
|
|
1886
2239
|
nav: {
|
|
@@ -1895,20 +2248,33 @@ async function navigate({
|
|
|
1895
2248
|
navigation_result.props.page.url = url;
|
|
1896
2249
|
}
|
|
1897
2250
|
|
|
2251
|
+
// Remove focus before updating the component tree, so that blur/focusout
|
|
2252
|
+
// handlers fire while the old component's data is still valid (#14575)
|
|
2253
|
+
blur_active_element(reset);
|
|
2254
|
+
|
|
1898
2255
|
const fork = load_cache_fork && (await load_cache_fork);
|
|
1899
2256
|
|
|
1900
2257
|
if (fork) {
|
|
1901
2258
|
commit_promise = fork.commit();
|
|
2259
|
+
// `fork.commit()` applies the preloaded state synchronously before the
|
|
2260
|
+
// first `await`, so reset any previously-failed boundaries now so the
|
|
2261
|
+
// stale `+error.svelte` is torn down. See sveltejs/kit#15694.
|
|
2262
|
+
for (const reset_boundary of resetters) {
|
|
2263
|
+
reset_boundary();
|
|
2264
|
+
}
|
|
2265
|
+
resetters.clear();
|
|
1902
2266
|
} else {
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
//
|
|
1906
|
-
|
|
1907
|
-
|
|
2267
|
+
apply_navigation_result(navigation_result);
|
|
2268
|
+
|
|
2269
|
+
// Reset any boundaries that failed on a previous navigation now that the
|
|
2270
|
+
// new props are applied, otherwise the stale `+error.svelte` stays
|
|
2271
|
+
// mounted above the new route's content. See sveltejs/kit#15694.
|
|
2272
|
+
for (const reset_boundary of resetters) {
|
|
2273
|
+
reset_boundary();
|
|
1908
2274
|
}
|
|
1909
|
-
|
|
2275
|
+
resetters.clear();
|
|
1910
2276
|
|
|
1911
|
-
commit_promise =
|
|
2277
|
+
commit_promise = settled();
|
|
1912
2278
|
}
|
|
1913
2279
|
|
|
1914
2280
|
has_navigated = true;
|
|
@@ -1920,75 +2286,32 @@ async function navigate({
|
|
|
1920
2286
|
|
|
1921
2287
|
await commit_promise;
|
|
1922
2288
|
|
|
1923
|
-
|
|
1924
|
-
// of some store shenanigans. `settled()` and `f.commit()`
|
|
1925
|
-
// should resolve after DOM updates in newer versions
|
|
1926
|
-
await svelte.tick();
|
|
1927
|
-
await svelte.tick();
|
|
1928
|
-
|
|
1929
|
-
if (token !== nav_token) {
|
|
2289
|
+
if (navigation_token !== nav_token) {
|
|
1930
2290
|
// a new navigation happened while we were waiting for the DOM to update, so abort
|
|
1931
2291
|
nav.reject(new Error('navigation aborted'));
|
|
1932
|
-
return
|
|
2292
|
+
return;
|
|
1933
2293
|
}
|
|
1934
2294
|
|
|
1935
|
-
|
|
1936
|
-
if (navigation_result.props.page && rendering_error) {
|
|
1937
|
-
Object.assign(navigation_result.props.page, rendering_error);
|
|
1938
|
-
}
|
|
2295
|
+
reset_scroll_and_focus(url, reset ? popped?.scroll : scroll_state(), reset, activeElement);
|
|
1939
2296
|
|
|
1940
|
-
|
|
1941
|
-
/** @type {Element | null | ''} */
|
|
1942
|
-
let deep_linked = null;
|
|
2297
|
+
is_navigating = false;
|
|
1943
2298
|
|
|
1944
|
-
|
|
1945
|
-
const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
|
|
1946
|
-
if (scroll) {
|
|
1947
|
-
scrollTo(scroll.x, scroll.y);
|
|
1948
|
-
} else if ((deep_linked = url.hash && document.getElementById(get_id(url)))) {
|
|
1949
|
-
// Here we use `scrollIntoView` on the element instead of `scrollTo`
|
|
1950
|
-
// because it natively supports the `scroll-margin` and `scroll-behavior`
|
|
1951
|
-
// CSS properties.
|
|
1952
|
-
deep_linked.scrollIntoView();
|
|
1953
|
-
} else {
|
|
1954
|
-
scrollTo(0, 0);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
2299
|
+
nav.fulfil(undefined);
|
|
1957
2300
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
// also refocus when activeElement is body already because the
|
|
1962
|
-
// focus event might not have been fired on it yet
|
|
1963
|
-
document.activeElement !== document.body;
|
|
1964
|
-
|
|
1965
|
-
if (!keepfocus && !changed_focus) {
|
|
1966
|
-
// We don't need to manually restore the scroll position if we're navigating
|
|
1967
|
-
// to a fragment identifier. It is automatically done for us when we set the
|
|
1968
|
-
// sequential navigation starting point with `location.replace`
|
|
1969
|
-
reset_focus(url, !deep_linked);
|
|
2301
|
+
// Update to.scroll to the actual scroll position after navigation completed
|
|
2302
|
+
if (nav.navigation.to) {
|
|
2303
|
+
nav.navigation.to.scroll = scroll_state();
|
|
1970
2304
|
}
|
|
1971
2305
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
is_navigating = false;
|
|
2306
|
+
after_navigate_callbacks.forEach((fn) => fn(/** @type {AfterNavigate} */ (nav.navigation)));
|
|
1975
2307
|
|
|
1976
2308
|
if (type === 'popstate') {
|
|
1977
2309
|
restore_snapshot(current_navigation_index);
|
|
1978
2310
|
}
|
|
2311
|
+
// new and replaced entries have no stored values, so this only resets there
|
|
2312
|
+
restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);
|
|
1979
2313
|
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
// Update to.scroll to the actual scroll position after navigation completed
|
|
1983
|
-
if (nav.navigation.to) {
|
|
1984
|
-
nav.navigation.to.scroll = scroll_state();
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
after_navigate_callbacks.forEach((fn) =>
|
|
1988
|
-
fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
|
|
1989
|
-
);
|
|
1990
|
-
|
|
1991
|
-
stores.navigating.set((navigating.current = null));
|
|
2314
|
+
navigating.current = null;
|
|
1992
2315
|
|
|
1993
2316
|
updating = false;
|
|
1994
2317
|
}
|
|
@@ -1998,23 +2321,21 @@ async function navigate({
|
|
|
1998
2321
|
* @param {URL} url
|
|
1999
2322
|
* @param {{ id: string | null }} route
|
|
2000
2323
|
* @param {App.Error} error
|
|
2001
|
-
* @param {number} status
|
|
2002
2324
|
* @param {boolean} [replace_state]
|
|
2003
|
-
* @returns {Promise<import('./types.js').NavigationFinished>}
|
|
2325
|
+
* @returns {Promise<import('./types.js').NavigationFinished | undefined>}
|
|
2004
2326
|
*/
|
|
2005
|
-
async function server_fallback(url, route, error,
|
|
2327
|
+
async function server_fallback(url, route, error, replace_state) {
|
|
2006
2328
|
if (url.origin === origin && url.pathname === location.pathname && !hydrated) {
|
|
2007
2329
|
// We would reload the same page we're currently on, which isn't hydrated,
|
|
2008
2330
|
// which means no SSR, which means we would end up in an endless loop
|
|
2009
2331
|
return await load_root_error_page({
|
|
2010
|
-
status,
|
|
2011
2332
|
error,
|
|
2012
2333
|
url,
|
|
2013
2334
|
route
|
|
2014
2335
|
});
|
|
2015
2336
|
}
|
|
2016
2337
|
|
|
2017
|
-
if (DEV && status !== 404) {
|
|
2338
|
+
if (DEV && error.status !== 404) {
|
|
2018
2339
|
console.error(
|
|
2019
2340
|
'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)'
|
|
2020
2341
|
);
|
|
@@ -2043,22 +2364,63 @@ let current_a = { element: undefined, href: undefined };
|
|
|
2043
2364
|
function setup_preload() {
|
|
2044
2365
|
/** @type {NodeJS.Timeout} */
|
|
2045
2366
|
let mousemove_timeout;
|
|
2367
|
+
/** @type {HTMLAnchorElement | SVGAElement | undefined} */
|
|
2368
|
+
let hovered_a;
|
|
2046
2369
|
/** @type {PreloadDataPriority} */
|
|
2047
2370
|
let current_priority;
|
|
2048
2371
|
|
|
2049
|
-
|
|
2050
|
-
|
|
2372
|
+
function clear_hover_preload() {
|
|
2373
|
+
clearTimeout(mousemove_timeout);
|
|
2374
|
+
hovered_a?.removeEventListener('mousemove', start_hover_preload);
|
|
2375
|
+
hovered_a?.removeEventListener('mouseleave', clear_hover_preload);
|
|
2376
|
+
hovered_a = undefined;
|
|
2377
|
+
}
|
|
2051
2378
|
|
|
2379
|
+
function start_hover_preload() {
|
|
2052
2380
|
clearTimeout(mousemove_timeout);
|
|
2053
2381
|
mousemove_timeout = setTimeout(() => {
|
|
2054
|
-
|
|
2382
|
+
if (!hovered_a) return;
|
|
2383
|
+
void preload(hovered_a, PRELOAD_PRIORITIES.hover);
|
|
2055
2384
|
}, 20);
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// Use mouseover initially instead of mousemove to avoid cluttering the event queue
|
|
2388
|
+
container.addEventListener('mouseover', (event) => {
|
|
2389
|
+
if (!(event.target instanceof Element)) return;
|
|
2390
|
+
|
|
2391
|
+
const a = find_anchor(event.target, container);
|
|
2392
|
+
if (!a || a === hovered_a) return;
|
|
2393
|
+
|
|
2394
|
+
clear_hover_preload();
|
|
2395
|
+
hovered_a = a;
|
|
2396
|
+
|
|
2397
|
+
const options = get_router_options(a);
|
|
2398
|
+
|
|
2399
|
+
if (
|
|
2400
|
+
options.preload_code < PRELOAD_PRIORITIES.hover &&
|
|
2401
|
+
options.preload_data < PRELOAD_PRIORITIES.hover
|
|
2402
|
+
) {
|
|
2403
|
+
// don't add event listeners if no preloading will happen
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// Instead of just preloading right away, we start a mousemove listener to implement
|
|
2408
|
+
// "mouse comes to a rest" behavior. This avoid false positives when you just move
|
|
2409
|
+
// your mouse across the screen and happen to pass over a link.
|
|
2410
|
+
a.addEventListener('mousemove', start_hover_preload);
|
|
2411
|
+
a.addEventListener('mouseleave', clear_hover_preload, { once: true });
|
|
2412
|
+
|
|
2413
|
+
start_hover_preload();
|
|
2056
2414
|
});
|
|
2057
2415
|
|
|
2058
2416
|
/** @param {Event} event */
|
|
2059
2417
|
function tap(event) {
|
|
2060
2418
|
if (event.defaultPrevented) return;
|
|
2061
|
-
|
|
2419
|
+
|
|
2420
|
+
const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
|
|
2421
|
+
if (!a) return;
|
|
2422
|
+
|
|
2423
|
+
void preload(a, PRELOAD_PRIORITIES.tap);
|
|
2062
2424
|
}
|
|
2063
2425
|
|
|
2064
2426
|
container.addEventListener('mousedown', tap);
|
|
@@ -2077,12 +2439,10 @@ function setup_preload() {
|
|
|
2077
2439
|
);
|
|
2078
2440
|
|
|
2079
2441
|
/**
|
|
2080
|
-
* @param {
|
|
2442
|
+
* @param {HTMLAnchorElement | SVGAElement} a
|
|
2081
2443
|
* @param {PreloadDataPriority} priority
|
|
2082
2444
|
*/
|
|
2083
|
-
async function preload(
|
|
2084
|
-
const a = find_anchor(element, container);
|
|
2085
|
-
|
|
2445
|
+
async function preload(a, priority) {
|
|
2086
2446
|
// we don't want to preload data again if the user has already hovered/tapped
|
|
2087
2447
|
const interacted =
|
|
2088
2448
|
a === current_a.element && a?.href === current_a.href && priority >= current_priority;
|
|
@@ -2106,15 +2466,13 @@ function setup_preload() {
|
|
|
2106
2466
|
if (!intent) return;
|
|
2107
2467
|
|
|
2108
2468
|
if (DEV) {
|
|
2109
|
-
void _preload_data(intent).
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
);
|
|
2117
|
-
}
|
|
2469
|
+
void _preload_data(intent).catch((error) => {
|
|
2470
|
+
console.warn(
|
|
2471
|
+
`Preloading data for ${intent.url.pathname} failed with the following error: ${error.message}\n` +
|
|
2472
|
+
'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' +
|
|
2473
|
+
'This route was preloaded due to a data-sveltekit-preload-data attribute. ' +
|
|
2474
|
+
'See https://svelte.dev/docs/kit/link-options for more info'
|
|
2475
|
+
);
|
|
2118
2476
|
});
|
|
2119
2477
|
} else {
|
|
2120
2478
|
void _preload_data(intent);
|
|
@@ -2152,25 +2510,41 @@ function setup_preload() {
|
|
|
2152
2510
|
|
|
2153
2511
|
/**
|
|
2154
2512
|
* @param {unknown} error
|
|
2155
|
-
* @param {
|
|
2156
|
-
* @returns {
|
|
2513
|
+
* @param {NavigationEvent} event
|
|
2514
|
+
* @returns {Promise<App.Error>}
|
|
2157
2515
|
*/
|
|
2158
|
-
function handle_error(error, event) {
|
|
2159
|
-
if (error instanceof
|
|
2516
|
+
export async function handle_error(error, event) {
|
|
2517
|
+
if (error instanceof HandledHttpError) {
|
|
2160
2518
|
return error.body;
|
|
2161
2519
|
}
|
|
2162
2520
|
|
|
2163
|
-
|
|
2521
|
+
/** @type {import('@sveltejs/kit/hooks').ClientCaughtError} */
|
|
2522
|
+
let caught;
|
|
2523
|
+
|
|
2524
|
+
if (error instanceof HttpError) {
|
|
2525
|
+
caught = { kind: 'app', error: error.body };
|
|
2526
|
+
} else if (error instanceof SvelteKitError) {
|
|
2527
|
+
caught = { kind: 'framework', error: { status: error.status, message: error.text } };
|
|
2528
|
+
} else {
|
|
2529
|
+
caught = { kind: 'unknown', error };
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
if (DEV && caught.kind !== 'app') {
|
|
2164
2533
|
errored = true;
|
|
2165
2534
|
console.warn('The next HMR update will cause the page to reload');
|
|
2166
2535
|
}
|
|
2167
2536
|
|
|
2168
|
-
const
|
|
2169
|
-
|
|
2537
|
+
const fallback =
|
|
2538
|
+
caught.kind === 'unknown' ? { status: 500, message: 'Internal Error' } : caught.error;
|
|
2170
2539
|
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2540
|
+
const input = { ...caught, event };
|
|
2541
|
+
if (DEV) add_deprecated_handle_error_properties(input, fallback);
|
|
2542
|
+
|
|
2543
|
+
const app_error = await app.hooks.handleError(input);
|
|
2544
|
+
|
|
2545
|
+
// the hook returns only the properties it wants to override; anything it omits
|
|
2546
|
+
// (including by returning nothing at all) is inherited from the caught error
|
|
2547
|
+
return { ...fallback, ...app_error };
|
|
2174
2548
|
}
|
|
2175
2549
|
|
|
2176
2550
|
/**
|
|
@@ -2192,7 +2566,7 @@ function add_navigation_callback(callbacks, callback) {
|
|
|
2192
2566
|
* A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a URL.
|
|
2193
2567
|
*
|
|
2194
2568
|
* `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
|
|
2195
|
-
* @param {(navigation:
|
|
2569
|
+
* @param {(navigation: AfterNavigate) => void} callback
|
|
2196
2570
|
* @returns {void}
|
|
2197
2571
|
*/
|
|
2198
2572
|
export function afterNavigate(callback) {
|
|
@@ -2209,7 +2583,7 @@ export function afterNavigate(callback) {
|
|
|
2209
2583
|
* If the navigation will (if not cancelled) cause the document to unload — in other words `'leave'` navigations and `'link'` navigations where `navigation.to.route === null` — `navigation.willUnload` is `true`.
|
|
2210
2584
|
*
|
|
2211
2585
|
* `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
|
|
2212
|
-
* @param {(navigation:
|
|
2586
|
+
* @param {(navigation: BeforeNavigate) => void} callback
|
|
2213
2587
|
* @returns {void}
|
|
2214
2588
|
*/
|
|
2215
2589
|
export function beforeNavigate(callback) {
|
|
@@ -2224,7 +2598,7 @@ export function beforeNavigate(callback) {
|
|
|
2224
2598
|
* If a function (or a `Promise` that resolves to a function) is returned from the callback, it will be called once the DOM has updated.
|
|
2225
2599
|
*
|
|
2226
2600
|
* `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
|
|
2227
|
-
* @param {(navigation:
|
|
2601
|
+
* @param {(navigation: OnNavigate) => import('types').MaybePromise<(() => void) | void>} callback
|
|
2228
2602
|
* @returns {void}
|
|
2229
2603
|
*/
|
|
2230
2604
|
export function onNavigate(callback) {
|
|
@@ -2250,40 +2624,103 @@ export function disableScrollHandling() {
|
|
|
2250
2624
|
}
|
|
2251
2625
|
}
|
|
2252
2626
|
|
|
2627
|
+
let warned_on_invalidate_all = false;
|
|
2628
|
+
let warned_on_replace_state = false;
|
|
2629
|
+
let warned_on_push_state = false;
|
|
2630
|
+
let warned_on_replace_state_function = false;
|
|
2631
|
+
|
|
2253
2632
|
/**
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2633
|
+
* @param {string | URL} url
|
|
2634
|
+
* @param {'goto' | 'pushState' | 'replaceState'} caller
|
|
2635
|
+
*/
|
|
2636
|
+
async function resolve_intent(url, caller) {
|
|
2637
|
+
const resolved = new URL(resolve_url(url));
|
|
2638
|
+
|
|
2639
|
+
if (resolved.origin !== origin) {
|
|
2640
|
+
throw new Error(
|
|
2641
|
+
DEV
|
|
2642
|
+
? `Cannot use \`${caller}\` with an external URL. Use \`window.location = "${url}"\` instead`
|
|
2643
|
+
: `${caller}: invalid URL`
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
const intent = await get_navigation_intent(resolved, false);
|
|
2648
|
+
|
|
2649
|
+
if (!intent) {
|
|
2650
|
+
throw new Error(
|
|
2651
|
+
DEV
|
|
2652
|
+
? `Cannot use \`${caller}\` with a URL that does not resolve to a route within the app. Use \`window.location = "${url}"\` instead`
|
|
2653
|
+
: `${caller}: invalid URL`
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
return intent;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
/**
|
|
2661
|
+
* Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
|
|
2662
|
+
* (as they would be with a regular navigation) or preserved.
|
|
2256
2663
|
*
|
|
2257
|
-
*
|
|
2664
|
+
* Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) or the state change has been applied.
|
|
2258
2665
|
*
|
|
2259
|
-
*
|
|
2260
|
-
*
|
|
2261
|
-
*
|
|
2262
|
-
* @param {
|
|
2263
|
-
* @param {
|
|
2264
|
-
* @param {boolean} [opts.invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://svelte.dev/docs/kit/load#rerunning-load-functions for more info on invalidation.
|
|
2265
|
-
* @param {Array<string | URL | ((url: URL) => boolean)>} [opts.invalidate] Causes any load functions to re-run if they depend on one of the urls
|
|
2266
|
-
* @param {App.PageState} [opts.state] An optional object that will be available as `page.state`
|
|
2666
|
+
* `goto` is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
|
|
2667
|
+
* For external URLs, use `window.location = url` to perform a full-page navigation instead of calling `goto(url)`.
|
|
2668
|
+
*
|
|
2669
|
+
* @param {string | URL} url Where to navigate to. Note that if you've set [`config.paths.base`](https://svelte.dev/docs/kit/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app.
|
|
2670
|
+
* @param {GotoOptions} [opts] Options related to the navigation
|
|
2267
2671
|
* @returns {Promise<void>}
|
|
2268
2672
|
*/
|
|
2269
|
-
export function goto(url, opts = {}) {
|
|
2673
|
+
export async function goto(url, opts = {}) {
|
|
2270
2674
|
if (!BROWSER) {
|
|
2271
2675
|
throw new Error('Cannot call goto(...) on the server');
|
|
2272
2676
|
}
|
|
2273
2677
|
|
|
2274
|
-
|
|
2678
|
+
if (DEV) {
|
|
2679
|
+
if ('replaceState' in opts && !warned_on_replace_state) {
|
|
2680
|
+
warned_on_replace_state = true;
|
|
2681
|
+
console.warn(
|
|
2682
|
+
`The \`goto(..., { replaceState: ${opts.replaceState} })\` option has been deprecated in favour of \`replace\``
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
if ('noScroll' in opts || 'keepFocus' in opts) {
|
|
2687
|
+
throw new Error(
|
|
2688
|
+
`The \`goto(..., { noScroll: true, keepFocus: true })\` options have been replaced by \`reset: false\``
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
const replace = opts.replace ?? opts.replaceState ?? false;
|
|
2275
2694
|
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2695
|
+
const intent = await resolve_intent(url, 'goto');
|
|
2696
|
+
|
|
2697
|
+
if (opts.shallow) {
|
|
2698
|
+
return update_state(
|
|
2699
|
+
intent,
|
|
2700
|
+
opts.state ?? {},
|
|
2701
|
+
{
|
|
2702
|
+
replace,
|
|
2703
|
+
persist_state: opts.persistState ?? false,
|
|
2704
|
+
reset: opts.reset ?? false
|
|
2705
|
+
},
|
|
2706
|
+
'goto'
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
if (DEV && 'invalidateAll' in opts && !warned_on_invalidate_all) {
|
|
2711
|
+
warned_on_invalidate_all = true;
|
|
2712
|
+
console.warn(
|
|
2713
|
+
`The \`goto(..., { invalidateAll: ${opts.invalidateAll} })\` option has been deprecated in favour of \`refreshAll\``
|
|
2283
2714
|
);
|
|
2284
2715
|
}
|
|
2285
2716
|
|
|
2286
|
-
return _goto(
|
|
2717
|
+
return _goto(
|
|
2718
|
+
intent.url,
|
|
2719
|
+
{ ...opts, replace, refreshAll: opts.refreshAll ?? opts.invalidateAll },
|
|
2720
|
+
0,
|
|
2721
|
+
{},
|
|
2722
|
+
intent
|
|
2723
|
+
);
|
|
2287
2724
|
}
|
|
2288
2725
|
|
|
2289
2726
|
/**
|
|
@@ -2302,16 +2739,17 @@ export function goto(url, opts = {}) {
|
|
|
2302
2739
|
* invalidate((url) => url.pathname === '/path');
|
|
2303
2740
|
* ```
|
|
2304
2741
|
* @param {string | URL | ((url: URL) => boolean)} resource The invalidated URL
|
|
2742
|
+
* @param {boolean} [keepState] If `true`, the current `page.state` will be preserved. Otherwise, it will be reset to an empty object. `false` by default.
|
|
2305
2743
|
* @returns {Promise<void>}
|
|
2306
2744
|
*/
|
|
2307
|
-
export function invalidate(resource) {
|
|
2745
|
+
export function invalidate(resource, keepState = false) {
|
|
2308
2746
|
if (!BROWSER) {
|
|
2309
2747
|
throw new Error('Cannot call invalidate(...) on the server');
|
|
2310
2748
|
}
|
|
2311
2749
|
|
|
2312
2750
|
push_invalidated(resource);
|
|
2313
2751
|
|
|
2314
|
-
return _invalidate();
|
|
2752
|
+
return _invalidate(!keepState);
|
|
2315
2753
|
}
|
|
2316
2754
|
|
|
2317
2755
|
/**
|
|
@@ -2328,6 +2766,10 @@ function push_invalidated(resource) {
|
|
|
2328
2766
|
|
|
2329
2767
|
/**
|
|
2330
2768
|
* Causes all `load` and `query` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated.
|
|
2769
|
+
*
|
|
2770
|
+
* Note that this resets `page.state` to an empty object. If you want to preserve `page.state` (for example when using [shallow routing](https://svelte.dev/docs/kit/shallow-routing)), use `refreshAll` instead.
|
|
2771
|
+
*
|
|
2772
|
+
* @deprecated Use [`refreshAll`](https://svelte.dev/docs/kit/$app-navigation#refreshAll) instead. Unlike `invalidateAll`, `refreshAll` does not reset `page.state`.
|
|
2331
2773
|
* @returns {Promise<void>}
|
|
2332
2774
|
*/
|
|
2333
2775
|
export function invalidateAll() {
|
|
@@ -2340,18 +2782,17 @@ export function invalidateAll() {
|
|
|
2340
2782
|
}
|
|
2341
2783
|
|
|
2342
2784
|
/**
|
|
2343
|
-
* Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run
|
|
2785
|
+
* Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run.
|
|
2344
2786
|
* Returns a `Promise` that resolves when the page is subsequently updated.
|
|
2345
|
-
* @param {{ includeLoadFunctions?: boolean }} [options]
|
|
2346
2787
|
* @returns {Promise<void>}
|
|
2347
2788
|
*/
|
|
2348
|
-
export function refreshAll(
|
|
2789
|
+
export function refreshAll() {
|
|
2349
2790
|
if (!BROWSER) {
|
|
2350
2791
|
throw new Error('Cannot call refreshAll() on the server');
|
|
2351
2792
|
}
|
|
2352
2793
|
|
|
2353
2794
|
force_invalidation = true;
|
|
2354
|
-
return _invalidate(
|
|
2795
|
+
return _invalidate(false);
|
|
2355
2796
|
}
|
|
2356
2797
|
|
|
2357
2798
|
/**
|
|
@@ -2364,7 +2805,7 @@ export function refreshAll({ includeLoadFunctions = true } = {}) {
|
|
|
2364
2805
|
* Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete.
|
|
2365
2806
|
*
|
|
2366
2807
|
* @param {string} href Page to preload
|
|
2367
|
-
* @returns {Promise<{ type: 'loaded';
|
|
2808
|
+
* @returns {Promise<({ type: 'loaded'; data: Record<string, any> } | { type: 'redirect'; location: string } | { type: 'error'; error: App.Error }) & { status: number; }>}
|
|
2368
2809
|
*/
|
|
2369
2810
|
export async function preloadData(href) {
|
|
2370
2811
|
if (!BROWSER) {
|
|
@@ -2378,15 +2819,32 @@ export async function preloadData(href) {
|
|
|
2378
2819
|
throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`);
|
|
2379
2820
|
}
|
|
2380
2821
|
|
|
2381
|
-
|
|
2822
|
+
/** @type {Awaited<ReturnType<typeof _preload_data>>} */
|
|
2823
|
+
let result;
|
|
2824
|
+
|
|
2825
|
+
try {
|
|
2826
|
+
result = await _preload_data(intent);
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
// `load_route` throws the handled error (an `App.Error` with a `status`)
|
|
2829
|
+
// when a preload fails, so surface it in the documented `{ type: 'error' }` shape
|
|
2830
|
+
const handled = /** @type {App.Error & { status?: number }} */ (error);
|
|
2831
|
+
return {
|
|
2832
|
+
type: 'error',
|
|
2833
|
+
status: handled?.status ?? 500,
|
|
2834
|
+
error: handled
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2382
2838
|
if (result.type === 'redirect') {
|
|
2383
2839
|
return {
|
|
2384
2840
|
type: result.type,
|
|
2841
|
+
status: result.status,
|
|
2385
2842
|
location: result.location
|
|
2386
2843
|
};
|
|
2387
2844
|
}
|
|
2388
2845
|
|
|
2389
2846
|
const { status, data } = result.props.page ?? page;
|
|
2847
|
+
|
|
2390
2848
|
return { type: result.type, status, data };
|
|
2391
2849
|
}
|
|
2392
2850
|
|
|
@@ -2394,139 +2852,264 @@ export async function preloadData(href) {
|
|
|
2394
2852
|
* Programmatically imports the code for routes that haven't yet been fetched.
|
|
2395
2853
|
* Typically, you might call this to speed up subsequent navigation.
|
|
2396
2854
|
*
|
|
2397
|
-
*
|
|
2855
|
+
* Takes a route ID such as `/about` or `/blog/[slug]`. Unlike pathnames, route IDs
|
|
2856
|
+
* are never prefixed with the app's [base path](https://svelte.dev/docs/kit/configuration#paths).
|
|
2857
|
+
* If you have a pathname rather than a route ID, you can convert it with
|
|
2858
|
+
* [`match`](https://svelte.dev/docs/kit/$app-paths#match) from `$app/paths`:
|
|
2859
|
+
*
|
|
2860
|
+
* ```js
|
|
2861
|
+
* import { match } from '$app/paths';
|
|
2862
|
+
* import { preloadCode } from '$app/navigation';
|
|
2863
|
+
*
|
|
2864
|
+
* const matched = await match('/blog/hello-world');
|
|
2865
|
+
* if (matched) await preloadCode(matched.id);
|
|
2866
|
+
* ```
|
|
2398
2867
|
*
|
|
2399
2868
|
* Unlike `preloadData`, this won't call `load` functions.
|
|
2400
2869
|
* Returns a Promise that resolves when the modules have been imported.
|
|
2401
2870
|
*
|
|
2402
|
-
* @param {
|
|
2871
|
+
* @param {import('$app/types').RouteId} id
|
|
2403
2872
|
* @returns {Promise<void>}
|
|
2404
2873
|
*/
|
|
2405
|
-
export async function preloadCode(
|
|
2874
|
+
export async function preloadCode(id) {
|
|
2406
2875
|
if (!BROWSER) {
|
|
2407
2876
|
throw new Error('Cannot call preloadCode(...) on the server');
|
|
2408
2877
|
}
|
|
2409
2878
|
|
|
2410
|
-
|
|
2879
|
+
if (DEV && id[0] !== '/') {
|
|
2880
|
+
throw new Error(
|
|
2881
|
+
`argument passed to preloadCode must be a route ID (i.e. "/blog/[slug]" rather than "blog/[slug]")`
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2411
2884
|
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
'argument passed to preloadCode must be a pathname (i.e. "/about" rather than "http://example.com/about"'
|
|
2416
|
-
);
|
|
2417
|
-
}
|
|
2885
|
+
const route = __SVELTEKIT_CLIENT_ROUTING__
|
|
2886
|
+
? routes.find((r) => r.id === id)
|
|
2887
|
+
: (route_id_cache.get(id) ?? (await load_route_by_id(id)));
|
|
2418
2888
|
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2889
|
+
if (route === ENDPOINT_ONLY) {
|
|
2890
|
+
if (DEV) {
|
|
2891
|
+
console.warn(
|
|
2892
|
+
`'${id}' has no \`+page\`, so there is no code to preload. If you meant to warm up an ` +
|
|
2893
|
+
`endpoint, request it with \`fetch\` instead.`
|
|
2422
2894
|
);
|
|
2423
2895
|
}
|
|
2424
2896
|
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
if (!route) {
|
|
2901
|
+
if (DEV) {
|
|
2902
|
+
// warn rather than throw, since under client routing an endpoint-only route id is
|
|
2903
|
+
// indistinguishable from a typo — the client manifest only contains routes with a `+page`
|
|
2904
|
+
let message = `'${id}' did not match any route`;
|
|
2905
|
+
|
|
2906
|
+
if (__SVELTEKIT_CLIENT_ROUTING__) {
|
|
2907
|
+
message += ` (note that routes without a \`+page\` have no code to preload)`;
|
|
2908
|
+
|
|
2909
|
+
// the most common migration mistake is passing a pathname, which used to work
|
|
2910
|
+
const candidates = [id];
|
|
2911
|
+
if (base && id.startsWith(base)) candidates.push(id.slice(base.length) || '/');
|
|
2912
|
+
|
|
2913
|
+
if (candidates.some((path) => routes.some((r) => r.exec(path)))) {
|
|
2914
|
+
message += `. It does match as a pathname — use \`match(...)\` from \`$app/paths\` to convert a pathname into a route ID`;
|
|
2915
|
+
}
|
|
2429
2916
|
}
|
|
2917
|
+
|
|
2918
|
+
console.warn(message);
|
|
2430
2919
|
}
|
|
2920
|
+
|
|
2921
|
+
return;
|
|
2431
2922
|
}
|
|
2432
2923
|
|
|
2433
|
-
|
|
2924
|
+
await load_route_nodes(route);
|
|
2434
2925
|
}
|
|
2435
2926
|
|
|
2436
2927
|
/**
|
|
2437
|
-
* Programmatically create a new history entry with the given `page.state`.
|
|
2928
|
+
* Programmatically create a new history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
|
|
2438
2929
|
*
|
|
2930
|
+
* @deprecated Use `goto(url, { state, shallow: true })` instead.
|
|
2439
2931
|
* @param {string | URL} url
|
|
2440
2932
|
* @param {App.PageState} state
|
|
2441
|
-
* @returns {void}
|
|
2933
|
+
* @returns {Promise<void>}
|
|
2442
2934
|
*/
|
|
2443
|
-
export function pushState(url, state) {
|
|
2935
|
+
export async function pushState(url, state) {
|
|
2444
2936
|
if (!BROWSER) {
|
|
2445
2937
|
throw new Error('Cannot call pushState(...) on the server');
|
|
2446
2938
|
}
|
|
2447
2939
|
|
|
2448
|
-
if (DEV) {
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
try {
|
|
2454
|
-
// use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
|
|
2455
|
-
devalue.stringify(state);
|
|
2456
|
-
} catch (error) {
|
|
2457
|
-
// @ts-expect-error
|
|
2458
|
-
throw new Error(`Could not serialize state${error.path}`, { cause: error });
|
|
2459
|
-
}
|
|
2940
|
+
if (DEV && !warned_on_push_state) {
|
|
2941
|
+
warned_on_push_state = true;
|
|
2942
|
+
console.warn(
|
|
2943
|
+
'`pushState(...)` is deprecated. Use `goto(url, { state, shallow: true })` instead.'
|
|
2944
|
+
);
|
|
2460
2945
|
}
|
|
2461
2946
|
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
const opts = {
|
|
2465
|
-
[HISTORY_INDEX]: (current_history_index += 1),
|
|
2466
|
-
[NAVIGATION_INDEX]: current_navigation_index,
|
|
2467
|
-
[PAGE_URL_KEY]: page.url.href,
|
|
2468
|
-
[STATES_KEY]: state
|
|
2469
|
-
};
|
|
2470
|
-
|
|
2471
|
-
history.pushState(opts, '', resolve_url(url));
|
|
2472
|
-
has_navigated = true;
|
|
2473
|
-
|
|
2474
|
-
page.state = state;
|
|
2475
|
-
root.$set({
|
|
2476
|
-
// we need to assign a new page object so that subscribers are correctly notified
|
|
2477
|
-
page: untrack(() => clone_page(page))
|
|
2478
|
-
});
|
|
2947
|
+
const intent = await resolve_intent(url, 'pushState');
|
|
2479
2948
|
|
|
2480
|
-
|
|
2949
|
+
await update_state(
|
|
2950
|
+
intent,
|
|
2951
|
+
state,
|
|
2952
|
+
{ replace: false, persist_state: false, reset: false },
|
|
2953
|
+
'pushState'
|
|
2954
|
+
);
|
|
2481
2955
|
}
|
|
2482
2956
|
|
|
2483
2957
|
/**
|
|
2484
|
-
* Programmatically replace the current history entry with the given `page.state`.
|
|
2958
|
+
* Programmatically replace the current history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
|
|
2485
2959
|
*
|
|
2960
|
+
* @deprecated Use `goto(url, { state, shallow: true, replace: true })` instead.
|
|
2486
2961
|
* @param {string | URL} url
|
|
2487
2962
|
* @param {App.PageState} state
|
|
2488
|
-
* @returns {void}
|
|
2963
|
+
* @returns {Promise<void>}
|
|
2489
2964
|
*/
|
|
2490
|
-
export function replaceState(url, state) {
|
|
2965
|
+
export async function replaceState(url, state) {
|
|
2491
2966
|
if (!BROWSER) {
|
|
2492
2967
|
throw new Error('Cannot call replaceState(...) on the server');
|
|
2493
2968
|
}
|
|
2494
2969
|
|
|
2495
|
-
if (DEV) {
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2970
|
+
if (DEV && !warned_on_replace_state_function) {
|
|
2971
|
+
warned_on_replace_state_function = true;
|
|
2972
|
+
console.warn(
|
|
2973
|
+
'`replaceState(...)` is deprecated. Use `goto(url, { state, shallow: true, replace: true })` instead.'
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2499
2976
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2977
|
+
const intent = await resolve_intent(url, 'replaceState');
|
|
2978
|
+
|
|
2979
|
+
await update_state(
|
|
2980
|
+
intent,
|
|
2981
|
+
state,
|
|
2982
|
+
{ replace: true, persist_state: false, reset: false },
|
|
2983
|
+
'replaceState'
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
/**
|
|
2988
|
+
* @param {NavigationIntent} intent
|
|
2989
|
+
* @param {App.PageState} state
|
|
2990
|
+
* @param {{ replace: boolean; persist_state: boolean; reset: boolean; }} options
|
|
2991
|
+
* @param {'goto' | 'pushState' | 'replaceState'} caller
|
|
2992
|
+
*/
|
|
2993
|
+
async function update_state(intent, state, { replace, persist_state, reset }, caller) {
|
|
2994
|
+
const url = intent.url;
|
|
2995
|
+
const previous_snapshot_registrations = current_registrations();
|
|
2996
|
+
|
|
2997
|
+
if (DEV && !started) {
|
|
2998
|
+
throw new Error(`Cannot call ${caller}(...) before router is initialized`);
|
|
2507
2999
|
}
|
|
2508
3000
|
|
|
2509
|
-
const
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
3001
|
+
const nav =
|
|
3002
|
+
// For backwards compatibility we don't trigger navigation hooks etc for push/replaceState
|
|
3003
|
+
caller === 'goto' ? _before_navigate({ url, type: 'goto', intent, shallow: true }) : undefined;
|
|
3004
|
+
|
|
3005
|
+
if (!nav && caller === 'goto') return;
|
|
3006
|
+
|
|
3007
|
+
const nav_token = {};
|
|
3008
|
+
|
|
3009
|
+
if (nav) {
|
|
3010
|
+
navigation_token = invalidation_token = nav_token;
|
|
3011
|
+
is_navigating = true;
|
|
3012
|
+
navigating.current = nav.navigation;
|
|
3013
|
+
updating = true;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
if (replace) {
|
|
3017
|
+
delete_navigation_snapshot(current_history_index);
|
|
3018
|
+
} else {
|
|
3019
|
+
capture_scroll(current_history_index);
|
|
3020
|
+
capture_navigation_snapshot(current_history_index);
|
|
3021
|
+
}
|
|
3022
|
+
if (reset) current_reset_index += 1;
|
|
3023
|
+
|
|
3024
|
+
// as above, serialize-then-parse to prevent bugs
|
|
3025
|
+
const serialized_state = stringify(state);
|
|
3026
|
+
state = parse(serialized_state);
|
|
3027
|
+
|
|
3028
|
+
const entry = {
|
|
3029
|
+
[HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
|
|
3030
|
+
historyIndex: (current_history_index += replace ? 0 : 1),
|
|
3031
|
+
navigationIndex: current_navigation_index,
|
|
3032
|
+
pageUrl: page.url.href,
|
|
3033
|
+
state: serialized_state,
|
|
3034
|
+
persistState: persist_state,
|
|
3035
|
+
resetIndex: current_reset_index
|
|
3036
|
+
})
|
|
2514
3037
|
};
|
|
2515
3038
|
|
|
2516
|
-
history.replaceState
|
|
3039
|
+
const fn = replace ? history.replaceState : history.pushState;
|
|
3040
|
+
fn.call(history, entry, '', url);
|
|
3041
|
+
set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);
|
|
3042
|
+
|
|
3043
|
+
if (!replace) {
|
|
3044
|
+
has_navigated = true;
|
|
3045
|
+
clear_onward_history(current_history_index, current_navigation_index);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
if (nav) {
|
|
3049
|
+
const after_navigate = (
|
|
3050
|
+
await Promise.all(
|
|
3051
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable -- we need to await because they can be asynchronous
|
|
3052
|
+
Array.from(on_navigate_callbacks, (fn) => fn(/** @type {OnNavigate} */ (nav.navigation)))
|
|
3053
|
+
)
|
|
3054
|
+
).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function');
|
|
3055
|
+
|
|
3056
|
+
if (after_navigate.length > 0) {
|
|
3057
|
+
function cleanup() {
|
|
3058
|
+
after_navigate.forEach((fn) => after_navigate_callbacks.delete(fn));
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
after_navigate.push(cleanup);
|
|
3062
|
+
after_navigate.forEach((fn) => after_navigate_callbacks.add(fn));
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
blur_active_element(reset);
|
|
2517
3067
|
|
|
2518
3068
|
page.state = state;
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
3069
|
+
page.shallow = {
|
|
3070
|
+
params: intent?.params ?? null,
|
|
3071
|
+
route: intent ? { id: intent.route.id } : null,
|
|
3072
|
+
url
|
|
3073
|
+
};
|
|
3074
|
+
|
|
3075
|
+
if (nav) {
|
|
3076
|
+
const { activeElement } = document;
|
|
3077
|
+
|
|
3078
|
+
await settled();
|
|
3079
|
+
|
|
3080
|
+
if (navigation_token !== nav_token) {
|
|
3081
|
+
// a new navigation happened while we were waiting for the DOM to update, so abort
|
|
3082
|
+
nav.reject(new Error('navigation aborted'));
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
reset_scroll_and_focus(url, reset ? null : scroll_state(), reset, activeElement);
|
|
3087
|
+
|
|
3088
|
+
is_navigating = false;
|
|
3089
|
+
nav.fulfil(undefined);
|
|
3090
|
+
|
|
3091
|
+
if (nav.navigation.to) {
|
|
3092
|
+
nav.navigation.to.scroll = scroll_state();
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
after_navigate_callbacks.forEach((fn) => fn(/** @type {AfterNavigate} */ (nav.navigation)));
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);
|
|
3099
|
+
|
|
3100
|
+
if (nav) {
|
|
3101
|
+
navigating.current = null;
|
|
3102
|
+
updating = false;
|
|
3103
|
+
}
|
|
2522
3104
|
}
|
|
2523
3105
|
|
|
2524
3106
|
/**
|
|
2525
|
-
*
|
|
2526
|
-
* In case of an error, it
|
|
3107
|
+
* Updates the `form` property of the current page with the given data and updates `page.status`.
|
|
3108
|
+
* In case of an error, it renders the nearest error page. In case of a redirect, it navigates to
|
|
3109
|
+
* the redirect location.
|
|
2527
3110
|
* @template {Record<string, unknown> | undefined} Success
|
|
2528
3111
|
* @template {Record<string, unknown> | undefined} Failure
|
|
2529
|
-
* @param {
|
|
3112
|
+
* @param {ActionResult<Success, Failure>} result
|
|
2530
3113
|
* @returns {Promise<void>}
|
|
2531
3114
|
*/
|
|
2532
3115
|
export async function applyAction(result) {
|
|
@@ -2534,37 +3117,81 @@ export async function applyAction(result) {
|
|
|
2534
3117
|
throw new Error('Cannot call applyAction(...) on the server');
|
|
2535
3118
|
}
|
|
2536
3119
|
|
|
3120
|
+
if (result.type === 'redirect') {
|
|
3121
|
+
await _goto(result.location, { refreshAll: true });
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
2537
3125
|
if (result.type === 'error') {
|
|
2538
|
-
await set_nearest_error_page(result.error
|
|
2539
|
-
} else if (result.type === 'redirect') {
|
|
2540
|
-
await _goto(result.location, { invalidateAll: true }, 0);
|
|
3126
|
+
await set_nearest_error_page(result.error);
|
|
2541
3127
|
} else {
|
|
2542
3128
|
page.form = result.data;
|
|
2543
3129
|
page.status = result.status;
|
|
2544
3130
|
|
|
2545
3131
|
/** @type {Record<string, any>} */
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
form: null,
|
|
2550
|
-
page: clone_page(page)
|
|
2551
|
-
});
|
|
3132
|
+
// this brings Svelte's view of the world in line with SvelteKit's
|
|
3133
|
+
// after use:enhance reset the form....
|
|
3134
|
+
props.form = null;
|
|
2552
3135
|
|
|
2553
3136
|
// ...so that setting the `form` prop takes effect and isn't ignored
|
|
2554
3137
|
await tick();
|
|
2555
|
-
|
|
3138
|
+
props.form = result.data;
|
|
2556
3139
|
|
|
2557
3140
|
if (result.type === 'success') {
|
|
2558
|
-
reset_focus(page.url);
|
|
3141
|
+
reset_focus(/** @type {URL} */ (page.url));
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
/**
|
|
3147
|
+
* Whether a submission should update the current page in place rather than navigating — either
|
|
3148
|
+
* because it lands where we already are, or because the result carries no location at all
|
|
3149
|
+
* (hand-constructed results, which have always applied in place).
|
|
3150
|
+
* @param {string | undefined} value
|
|
3151
|
+
*/
|
|
3152
|
+
export function is_current_location(value) {
|
|
3153
|
+
if (value === undefined) return true;
|
|
3154
|
+
|
|
3155
|
+
const destination = resolve_url(value);
|
|
3156
|
+
const current = new URL(location.href);
|
|
3157
|
+
if (destination.pathname !== current.pathname) return false;
|
|
3158
|
+
|
|
3159
|
+
const keys = new Set([...destination.searchParams.keys(), ...current.searchParams.keys()]);
|
|
3160
|
+
for (const key of keys) {
|
|
3161
|
+
const destination_values = destination.searchParams.getAll(key).sort();
|
|
3162
|
+
const current_values = current.searchParams.getAll(key).sort();
|
|
3163
|
+
|
|
3164
|
+
if (
|
|
3165
|
+
destination_values.length !== current_values.length ||
|
|
3166
|
+
destination_values.some((value, i) => value !== current_values[i])
|
|
3167
|
+
) {
|
|
3168
|
+
return false;
|
|
2559
3169
|
}
|
|
2560
3170
|
}
|
|
3171
|
+
|
|
3172
|
+
return true;
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
/**
|
|
3176
|
+
* Navigate to where a form submission should land, rendering that page with the given result
|
|
3177
|
+
* — what the browser would do for a native submission.
|
|
3178
|
+
* @param {string} location
|
|
3179
|
+
* @param {ActionResult} result
|
|
3180
|
+
* @param {boolean} refresh_all
|
|
3181
|
+
* @returns {Promise<void>}
|
|
3182
|
+
*/
|
|
3183
|
+
export function apply_action_navigation(location, result, refresh_all) {
|
|
3184
|
+
return _goto(location, {
|
|
3185
|
+
type: 'form',
|
|
3186
|
+
refreshAll: refresh_all,
|
|
3187
|
+
action_result: result
|
|
3188
|
+
});
|
|
2561
3189
|
}
|
|
2562
3190
|
|
|
2563
3191
|
/**
|
|
2564
3192
|
* @param {App.Error} error
|
|
2565
|
-
* @param {number} status
|
|
2566
3193
|
*/
|
|
2567
|
-
export async function set_nearest_error_page(error
|
|
3194
|
+
export async function set_nearest_error_page(error) {
|
|
2568
3195
|
const url = new URL(location.href);
|
|
2569
3196
|
|
|
2570
3197
|
const { branch, route } = current;
|
|
@@ -2576,7 +3203,6 @@ export async function set_nearest_error_page(error, status = 500) {
|
|
|
2576
3203
|
url,
|
|
2577
3204
|
params: current.params,
|
|
2578
3205
|
branch: branch.slice(0, error_load.idx).concat(error_load.node),
|
|
2579
|
-
status,
|
|
2580
3206
|
error,
|
|
2581
3207
|
// do not set errors, we haven't changed the page so the previous ones are still current
|
|
2582
3208
|
route
|
|
@@ -2584,8 +3210,7 @@ export async function set_nearest_error_page(error, status = 500) {
|
|
|
2584
3210
|
|
|
2585
3211
|
current = { ...navigation_result.state, nav: current.nav };
|
|
2586
3212
|
|
|
2587
|
-
|
|
2588
|
-
update(navigation_result.props.page);
|
|
3213
|
+
apply_navigation_result(navigation_result);
|
|
2589
3214
|
|
|
2590
3215
|
void tick().then(() => reset_focus(current.url));
|
|
2591
3216
|
}
|
|
@@ -2608,7 +3233,7 @@ function _start_router() {
|
|
|
2608
3233
|
|
|
2609
3234
|
// If we're navigating, beforeNavigate was already called. If we end up in here during navigation,
|
|
2610
3235
|
// it's due to an external or full-page-reload link, for which we don't want to call the hook again.
|
|
2611
|
-
/** @type {
|
|
3236
|
+
/** @type {BeforeNavigate} */
|
|
2612
3237
|
const navigation = {
|
|
2613
3238
|
...nav.navigation,
|
|
2614
3239
|
cancel: () => {
|
|
@@ -2627,13 +3252,19 @@ function _start_router() {
|
|
|
2627
3252
|
history.scrollRestoration = 'auto';
|
|
2628
3253
|
}
|
|
2629
3254
|
});
|
|
2630
|
-
|
|
2631
3255
|
addEventListener('visibilitychange', () => {
|
|
2632
3256
|
if (document.visibilityState === 'hidden') {
|
|
2633
3257
|
persist_state();
|
|
3258
|
+
} else {
|
|
3259
|
+
// the tab just became visible — a good time to check for a new deployment
|
|
3260
|
+
void updated.check();
|
|
2634
3261
|
}
|
|
2635
3262
|
});
|
|
2636
3263
|
|
|
3264
|
+
addEventListener('focus', () => {
|
|
3265
|
+
void updated.check();
|
|
3266
|
+
});
|
|
3267
|
+
|
|
2637
3268
|
// @ts-expect-error this isn't supported everywhere yet
|
|
2638
3269
|
if (!navigator.connection?.saveData) {
|
|
2639
3270
|
setup_preload();
|
|
@@ -2724,17 +3355,21 @@ function _start_router() {
|
|
|
2724
3355
|
return;
|
|
2725
3356
|
}
|
|
2726
3357
|
// set this flag to distinguish between navigations triggered by
|
|
2727
|
-
// clicking a hash link and those triggered by popstate
|
|
2728
|
-
|
|
3358
|
+
// clicking a hash link and those triggered by popstate. We gotta retrieve
|
|
3359
|
+
// history metadata here because the hashchange event will occur after history.state was updated
|
|
3360
|
+
hash_navigating = {
|
|
3361
|
+
.../** @type {HistoryMetadata} */ (get_history_metadata()),
|
|
3362
|
+
resetIndex: current_reset_index + (options.reset ? 1 : 0)
|
|
3363
|
+
};
|
|
2729
3364
|
|
|
2730
|
-
|
|
3365
|
+
capture_scroll(current_history_index);
|
|
2731
3366
|
|
|
2732
3367
|
update_url(url);
|
|
2733
3368
|
|
|
2734
3369
|
if (!options.replace_state) return;
|
|
2735
3370
|
|
|
2736
3371
|
// hashchange event shouldn't occur if the router is replacing state.
|
|
2737
|
-
hash_navigating =
|
|
3372
|
+
hash_navigating = null;
|
|
2738
3373
|
}
|
|
2739
3374
|
|
|
2740
3375
|
event.preventDefault();
|
|
@@ -2749,12 +3384,13 @@ function _start_router() {
|
|
|
2749
3384
|
setTimeout(fulfil, 100); // fallback for edge case where rAF doesn't fire because e.g. tab was backgrounded
|
|
2750
3385
|
});
|
|
2751
3386
|
|
|
2752
|
-
|
|
3387
|
+
const changed = url.href !== location.href;
|
|
3388
|
+
|
|
3389
|
+
await _goto(url, {
|
|
2753
3390
|
type: 'link',
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
replace_state: options.replace_state ?? url.href === location.href,
|
|
3391
|
+
reset: options.reset,
|
|
3392
|
+
replace: options.replace_state ?? !changed,
|
|
3393
|
+
refreshAll: !changed,
|
|
2758
3394
|
event
|
|
2759
3395
|
});
|
|
2760
3396
|
});
|
|
@@ -2799,8 +3435,7 @@ function _start_router() {
|
|
|
2799
3435
|
void navigate({
|
|
2800
3436
|
type: 'form',
|
|
2801
3437
|
url,
|
|
2802
|
-
|
|
2803
|
-
noscroll: options.noscroll,
|
|
3438
|
+
reset: options.reset,
|
|
2804
3439
|
replace_state: options.replace_state ?? url.href === location.href,
|
|
2805
3440
|
event
|
|
2806
3441
|
});
|
|
@@ -2809,58 +3444,89 @@ function _start_router() {
|
|
|
2809
3444
|
addEventListener('popstate', async (event) => {
|
|
2810
3445
|
if (resetting_focus) return;
|
|
2811
3446
|
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3447
|
+
const history_metadata = get_history_metadata(event.state);
|
|
3448
|
+
|
|
3449
|
+
if (history_metadata?.historyIndex) {
|
|
3450
|
+
const history_index = history_metadata.historyIndex;
|
|
3451
|
+
const source_info = history_info[current_history_index];
|
|
3452
|
+
navigation_token = invalidation_token = {};
|
|
2815
3453
|
|
|
2816
3454
|
// if a popstate-driven navigation is cancelled, we need to counteract it
|
|
2817
3455
|
// with history.go, which means we end up back here, hence this check
|
|
2818
3456
|
if (history_index === current_history_index) return;
|
|
2819
3457
|
|
|
2820
|
-
const
|
|
2821
|
-
const
|
|
2822
|
-
const
|
|
2823
|
-
const
|
|
2824
|
-
const
|
|
3458
|
+
const delta = history_index - current_history_index;
|
|
3459
|
+
const reset_index = history_metadata.resetIndex;
|
|
3460
|
+
const reset = reset_index !== (source_info?.resetIndex ?? current_reset_index);
|
|
3461
|
+
const scroll = history_info[history_index]?.scroll;
|
|
3462
|
+
const state = parse(history_metadata.state);
|
|
3463
|
+
const url = new URL(history_metadata.pageUrl ?? location.href);
|
|
3464
|
+
const navigation_index = history_metadata.navigationIndex;
|
|
3465
|
+
const is_hash_change =
|
|
3466
|
+
current.url && (location.href + current.url.href).includes('#') // check if even has a hash
|
|
3467
|
+
? strip_hash(location) === strip_hash(current.url)
|
|
3468
|
+
: false;
|
|
2825
3469
|
const shallow =
|
|
2826
|
-
navigation_index === current_navigation_index &&
|
|
3470
|
+
navigation_index === current_navigation_index &&
|
|
3471
|
+
((has_navigated &&
|
|
3472
|
+
(history_metadata.pageUrl === undefined || history_metadata.pageUrl === location.href)) ||
|
|
3473
|
+
is_hash_change);
|
|
3474
|
+
const shallow_url = history_metadata.pageUrl ? new URL(location.href) : null;
|
|
3475
|
+
const shallow_intent = shallow_url
|
|
3476
|
+
? await get_navigation_intent(shallow_url, false)
|
|
3477
|
+
: undefined;
|
|
3478
|
+
const shallow_target = shallow_url
|
|
3479
|
+
? {
|
|
3480
|
+
params: shallow_intent?.params ?? null,
|
|
3481
|
+
route: shallow_intent ? { id: shallow_intent.route.id } : null,
|
|
3482
|
+
url: shallow_url
|
|
3483
|
+
}
|
|
3484
|
+
: null;
|
|
2827
3485
|
|
|
2828
3486
|
if (shallow) {
|
|
2829
3487
|
// We don't need to navigate, we just need to update scroll and/or state.
|
|
2830
3488
|
// This happens with hash links and `pushState`/`replaceState`. The
|
|
2831
3489
|
// exception is if we haven't navigated yet, since we could have
|
|
2832
3490
|
// got here after a modal navigation then a reload
|
|
3491
|
+
|
|
3492
|
+
blur_active_element(reset);
|
|
3493
|
+
|
|
2833
3494
|
if (state !== page.state) {
|
|
2834
3495
|
page.state = state;
|
|
2835
3496
|
}
|
|
2836
3497
|
|
|
2837
|
-
|
|
3498
|
+
page.shallow = shallow_target;
|
|
2838
3499
|
|
|
2839
|
-
|
|
2840
|
-
if (scroll) scrollTo(scroll.x, scroll.y);
|
|
3500
|
+
update_url(url);
|
|
2841
3501
|
|
|
3502
|
+
capture_scroll(current_history_index);
|
|
3503
|
+
capture_navigation_snapshot(current_history_index);
|
|
2842
3504
|
current_history_index = history_index;
|
|
3505
|
+
current_reset_index = reset_index;
|
|
3506
|
+
if (reset && scroll) scrollTo(scroll.x, scroll.y);
|
|
3507
|
+
restore_navigation_snapshot(current_history_index, current_registrations());
|
|
2843
3508
|
return;
|
|
2844
3509
|
}
|
|
2845
3510
|
|
|
2846
|
-
const delta = history_index - current_history_index;
|
|
2847
|
-
|
|
2848
3511
|
await navigate({
|
|
2849
3512
|
type: 'popstate',
|
|
2850
3513
|
url,
|
|
3514
|
+
reset,
|
|
2851
3515
|
popped: {
|
|
2852
3516
|
state,
|
|
2853
3517
|
scroll,
|
|
2854
|
-
delta
|
|
3518
|
+
delta,
|
|
3519
|
+
shallow: shallow_target
|
|
2855
3520
|
},
|
|
2856
3521
|
accept: () => {
|
|
2857
3522
|
current_history_index = history_index;
|
|
2858
3523
|
current_navigation_index = navigation_index;
|
|
3524
|
+
current_reset_index = reset_index;
|
|
2859
3525
|
},
|
|
2860
3526
|
block: () => {
|
|
2861
3527
|
history.go(-delta);
|
|
2862
3528
|
},
|
|
2863
|
-
nav_token:
|
|
3529
|
+
nav_token: navigation_token,
|
|
2864
3530
|
event
|
|
2865
3531
|
});
|
|
2866
3532
|
} else {
|
|
@@ -2884,16 +3550,22 @@ function _start_router() {
|
|
|
2884
3550
|
// if the hashchange happened as a result of clicking on a link,
|
|
2885
3551
|
// we need to update history, otherwise we have to leave it alone
|
|
2886
3552
|
if (hash_navigating) {
|
|
2887
|
-
|
|
3553
|
+
const history_metadata = hash_navigating;
|
|
3554
|
+
hash_navigating = null;
|
|
3555
|
+
current_reset_index = history_metadata.resetIndex;
|
|
2888
3556
|
history.replaceState(
|
|
2889
3557
|
{
|
|
2890
3558
|
...history.state,
|
|
2891
|
-
[
|
|
2892
|
-
|
|
3559
|
+
[HISTORY_METADATA_KEY]: {
|
|
3560
|
+
...history_metadata,
|
|
3561
|
+
historyIndex: ++current_history_index,
|
|
3562
|
+
navigationIndex: current_navigation_index
|
|
3563
|
+
}
|
|
2893
3564
|
},
|
|
2894
3565
|
'',
|
|
2895
3566
|
location.href
|
|
2896
3567
|
);
|
|
3568
|
+
set_history_options(current_history_index, history_metadata);
|
|
2897
3569
|
}
|
|
2898
3570
|
});
|
|
2899
3571
|
|
|
@@ -2912,7 +3584,7 @@ function _start_router() {
|
|
|
2912
3584
|
// the navigation away from it was successful.
|
|
2913
3585
|
// Info about bfcache here: https://web.dev/bfcache
|
|
2914
3586
|
if (event.persisted) {
|
|
2915
|
-
|
|
3587
|
+
navigating.current = null;
|
|
2916
3588
|
}
|
|
2917
3589
|
});
|
|
2918
3590
|
|
|
@@ -2921,18 +3593,17 @@ function _start_router() {
|
|
|
2921
3593
|
*/
|
|
2922
3594
|
function update_url(url) {
|
|
2923
3595
|
current.url = page.url = url;
|
|
2924
|
-
stores.page.set(clone_page(page));
|
|
2925
|
-
stores.page.notify();
|
|
2926
3596
|
}
|
|
2927
3597
|
}
|
|
2928
3598
|
|
|
2929
3599
|
/**
|
|
2930
3600
|
* @param {HTMLElement} target
|
|
2931
3601
|
* @param {import('./types.js').HydrateOptions} opts
|
|
3602
|
+
* @returns {Promise<void>}
|
|
2932
3603
|
*/
|
|
2933
3604
|
async function _hydrate(
|
|
2934
3605
|
target,
|
|
2935
|
-
{ status
|
|
3606
|
+
{ status, error, node_ids, params, route, server_route, data: server_data_nodes, form }
|
|
2936
3607
|
) {
|
|
2937
3608
|
hydrated = true;
|
|
2938
3609
|
|
|
@@ -2952,7 +3623,7 @@ async function _hydrate(
|
|
|
2952
3623
|
} else {
|
|
2953
3624
|
// undefined in case of 404
|
|
2954
3625
|
if (server_route) {
|
|
2955
|
-
parsed_route = route =
|
|
3626
|
+
parsed_route = route = parse_and_cache_server_route(server_route);
|
|
2956
3627
|
} else {
|
|
2957
3628
|
route = { id: null };
|
|
2958
3629
|
params = {};
|
|
@@ -2961,7 +3632,7 @@ async function _hydrate(
|
|
|
2961
3632
|
|
|
2962
3633
|
/** @type {import('./types.js').NavigationFinished | undefined} */
|
|
2963
3634
|
let result;
|
|
2964
|
-
let
|
|
3635
|
+
let should_hydrate = true;
|
|
2965
3636
|
|
|
2966
3637
|
try {
|
|
2967
3638
|
const branch_promises = node_ids.map(async (n, i) => {
|
|
@@ -3015,28 +3686,31 @@ async function _hydrate(
|
|
|
3015
3686
|
if (error instanceof Redirect) {
|
|
3016
3687
|
// this is a real edge case — `load` would need to return
|
|
3017
3688
|
// a redirect but only in the browser
|
|
3018
|
-
await native_navigation(new URL(error.location, location.href));
|
|
3019
|
-
return;
|
|
3689
|
+
return await native_navigation(new URL(error.location, location.href));
|
|
3020
3690
|
}
|
|
3021
3691
|
|
|
3692
|
+
const handled_error = await handle_error(error, { url, params, route });
|
|
3693
|
+
|
|
3022
3694
|
result = await load_root_error_page({
|
|
3023
|
-
|
|
3024
|
-
error: await handle_error(error, { url, params, route }),
|
|
3695
|
+
error: handled_error,
|
|
3025
3696
|
url,
|
|
3026
3697
|
route
|
|
3027
3698
|
});
|
|
3028
3699
|
|
|
3029
3700
|
target.textContent = '';
|
|
3030
|
-
|
|
3031
|
-
} finally {
|
|
3032
|
-
query_responses = {};
|
|
3701
|
+
should_hydrate = false;
|
|
3033
3702
|
}
|
|
3034
3703
|
|
|
3704
|
+
// Exit early when we encounter a redirect while loading the root error page.
|
|
3705
|
+
// In this case, `initialize` will be called later on
|
|
3706
|
+
if (!result) return;
|
|
3707
|
+
|
|
3035
3708
|
if (result.props.page) {
|
|
3036
|
-
|
|
3709
|
+
const history_metadata = get_history_metadata();
|
|
3710
|
+
result.props.page.state = history_metadata?.persistState ? parse(history_metadata.state) : {};
|
|
3037
3711
|
}
|
|
3038
3712
|
|
|
3039
|
-
await initialize(result, target,
|
|
3713
|
+
await initialize(result, target, should_hydrate);
|
|
3040
3714
|
}
|
|
3041
3715
|
|
|
3042
3716
|
/**
|
|
@@ -3059,21 +3733,22 @@ async function load_data(url, invalid) {
|
|
|
3059
3733
|
const fetcher = DEV ? dev_fetch : window.fetch;
|
|
3060
3734
|
const res = await fetcher(data_url.href, {});
|
|
3061
3735
|
|
|
3736
|
+
// detect new deployments from the response header
|
|
3737
|
+
notify_version(res.headers.get('x-sveltekit-version'));
|
|
3738
|
+
|
|
3062
3739
|
if (!res.ok) {
|
|
3063
|
-
// error message is a JSON-stringified string which devalue can't handle at the top level
|
|
3064
|
-
// turn it into a HttpError to not call handleError on the client again (was already handled on the server)
|
|
3065
3740
|
// if `__data.json` doesn't exist or the server has an internal error,
|
|
3066
3741
|
// avoid parsing the HTML error page as a JSON
|
|
3067
|
-
/** @type {
|
|
3068
|
-
let message;
|
|
3742
|
+
/** @type {App.Error} */
|
|
3743
|
+
let error = { status: res.status, message: 'Internal Error' };
|
|
3744
|
+
|
|
3069
3745
|
if (res.headers.get('content-type')?.includes('application/json')) {
|
|
3070
|
-
|
|
3746
|
+
error = { status: res.status, ...(await res.json()) };
|
|
3071
3747
|
} else if (res.status === 404) {
|
|
3072
|
-
message = 'Not Found';
|
|
3073
|
-
} else if (res.status === 500) {
|
|
3074
|
-
message = 'Internal Error';
|
|
3748
|
+
error.message = 'Not Found';
|
|
3075
3749
|
}
|
|
3076
|
-
|
|
3750
|
+
|
|
3751
|
+
throw new HandledHttpError(error);
|
|
3077
3752
|
}
|
|
3078
3753
|
|
|
3079
3754
|
return new Promise((resolve, reject) => {
|
|
@@ -3176,8 +3851,8 @@ function reset_focus(url, scroll = true) {
|
|
|
3176
3851
|
|
|
3177
3852
|
// Mimic the browsers' behaviour and set the sequential focus navigation
|
|
3178
3853
|
// starting point to the fragment identifier.
|
|
3179
|
-
const
|
|
3180
|
-
if (
|
|
3854
|
+
const element = get_hash_element(url);
|
|
3855
|
+
if (element) {
|
|
3181
3856
|
const { x, y } = scroll_state();
|
|
3182
3857
|
|
|
3183
3858
|
// `element.focus()` doesn't work on Safari and Firefox Ubuntu so we need
|
|
@@ -3186,7 +3861,7 @@ function reset_focus(url, scroll = true) {
|
|
|
3186
3861
|
const history_state = history.state;
|
|
3187
3862
|
|
|
3188
3863
|
resetting_focus = true;
|
|
3189
|
-
location.replace(new URL(`#${id}`, location.href));
|
|
3864
|
+
location.replace(new URL(`#${element.id}`, location.href));
|
|
3190
3865
|
|
|
3191
3866
|
// Firefox has a bug that sets the history state to `null` so we need to
|
|
3192
3867
|
// restore it after. See https://bugzilla.mozilla.org/show_bug.cgi?id=1199924
|
|
@@ -3208,9 +3883,6 @@ function reset_focus(url, scroll = true) {
|
|
|
3208
3883
|
const tabindex = root.getAttribute('tabindex');
|
|
3209
3884
|
|
|
3210
3885
|
root.tabIndex = -1;
|
|
3211
|
-
// TODO: remove this when we switch to TypeScript 6
|
|
3212
|
-
// @ts-ignore options.focusVisible is only typed in TypeScript 6
|
|
3213
|
-
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#browser_compatibility
|
|
3214
3886
|
root.focus({ preventScroll: true, focusVisible: false });
|
|
3215
3887
|
|
|
3216
3888
|
// restore `tabindex` as to prevent `root` from stealing input from elements
|
|
@@ -3263,14 +3935,24 @@ function reset_focus(url, scroll = true) {
|
|
|
3263
3935
|
}
|
|
3264
3936
|
|
|
3265
3937
|
/**
|
|
3266
|
-
* @template {
|
|
3938
|
+
* @template {NavigationType} T
|
|
3267
3939
|
* @param {import('./types.js').NavigationState} current
|
|
3268
3940
|
* @param {import('./types.js').NavigationIntent | undefined} intent
|
|
3269
3941
|
* @param {URL | null} url
|
|
3270
3942
|
* @param {T} type
|
|
3271
3943
|
* @param {{ x: number, y: number } | null} [target_scroll] The scroll position for the target (for popstate navigations)
|
|
3944
|
+
* @param {boolean} [shallow]
|
|
3945
|
+
* @param {{ params: Record<string, string> | null; route: { id: string } | null; url: URL }} [target]
|
|
3272
3946
|
*/
|
|
3273
|
-
function create_navigation(
|
|
3947
|
+
function create_navigation(
|
|
3948
|
+
current,
|
|
3949
|
+
intent,
|
|
3950
|
+
url,
|
|
3951
|
+
type,
|
|
3952
|
+
target_scroll = null,
|
|
3953
|
+
shallow = false,
|
|
3954
|
+
target
|
|
3955
|
+
) {
|
|
3274
3956
|
/** @type {(value: any) => void} */
|
|
3275
3957
|
let fulfil;
|
|
3276
3958
|
|
|
@@ -3285,7 +3967,7 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
|
|
|
3285
3967
|
// Handle any errors off-chain so that it doesn't show up as an unhandled rejection
|
|
3286
3968
|
complete.catch(noop);
|
|
3287
3969
|
|
|
3288
|
-
/** @type {(
|
|
3970
|
+
/** @type {(Navigation | AfterNavigate) & { type: T }} */
|
|
3289
3971
|
const navigation = /** @type {any} */ ({
|
|
3290
3972
|
from: {
|
|
3291
3973
|
params: current.params,
|
|
@@ -3294,13 +3976,14 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
|
|
|
3294
3976
|
scroll: scroll_state()
|
|
3295
3977
|
},
|
|
3296
3978
|
to: url && {
|
|
3297
|
-
params: intent?.params ?? null,
|
|
3298
|
-
route: { id: intent?.route?.id ?? null },
|
|
3299
|
-
url,
|
|
3979
|
+
params: target ? target.params : (intent?.params ?? null),
|
|
3980
|
+
route: { id: target ? (target.route?.id ?? null) : (intent?.route?.id ?? null) },
|
|
3981
|
+
url: target?.url ?? url,
|
|
3300
3982
|
scroll: target_scroll
|
|
3301
3983
|
},
|
|
3302
|
-
willUnload: !intent,
|
|
3984
|
+
willUnload: !shallow && !intent,
|
|
3303
3985
|
type,
|
|
3986
|
+
shallow,
|
|
3304
3987
|
complete
|
|
3305
3988
|
});
|
|
3306
3989
|
|
|
@@ -3313,28 +3996,6 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
|
|
|
3313
3996
|
};
|
|
3314
3997
|
}
|
|
3315
3998
|
|
|
3316
|
-
/**
|
|
3317
|
-
* TODO: remove this in 3.0 when the page store is also removed
|
|
3318
|
-
*
|
|
3319
|
-
* We need to assign a new page object so that subscribers are correctly notified.
|
|
3320
|
-
* However, spreading `{ ...page }` returns an empty object so we manually
|
|
3321
|
-
* assign to each property instead.
|
|
3322
|
-
*
|
|
3323
|
-
* @param {import('@sveltejs/kit').Page} page
|
|
3324
|
-
*/
|
|
3325
|
-
function clone_page(page) {
|
|
3326
|
-
return {
|
|
3327
|
-
data: page.data,
|
|
3328
|
-
error: page.error,
|
|
3329
|
-
form: page.form,
|
|
3330
|
-
params: page.params,
|
|
3331
|
-
route: page.route,
|
|
3332
|
-
state: page.state,
|
|
3333
|
-
status: page.status,
|
|
3334
|
-
url: page.url
|
|
3335
|
-
};
|
|
3336
|
-
}
|
|
3337
|
-
|
|
3338
3999
|
/**
|
|
3339
4000
|
* @param {URL} url
|
|
3340
4001
|
* @returns {URL}
|
|
@@ -3363,6 +4024,15 @@ function get_id(url) {
|
|
|
3363
4024
|
return decodeURIComponent(id);
|
|
3364
4025
|
}
|
|
3365
4026
|
|
|
4027
|
+
/**
|
|
4028
|
+
* @param {URL} url
|
|
4029
|
+
* @returns {Element | null}
|
|
4030
|
+
*/
|
|
4031
|
+
function get_hash_element(url) {
|
|
4032
|
+
const id = get_id(url);
|
|
4033
|
+
return id ? document.getElementById(id) : null;
|
|
4034
|
+
}
|
|
4035
|
+
|
|
3366
4036
|
if (DEV) {
|
|
3367
4037
|
// Nasty hack to silence harmless warnings the user can do nothing about
|
|
3368
4038
|
const console_warn = console.warn;
|
|
@@ -3386,3 +4056,21 @@ if (DEV) {
|
|
|
3386
4056
|
});
|
|
3387
4057
|
}
|
|
3388
4058
|
}
|
|
4059
|
+
|
|
4060
|
+
/**
|
|
4061
|
+
* @param {NavigationFinished} result
|
|
4062
|
+
*/
|
|
4063
|
+
function apply_navigation_result(result) {
|
|
4064
|
+
Object.assign(page, result.props.page);
|
|
4065
|
+
|
|
4066
|
+
props.tree.data = result.props.tree.data;
|
|
4067
|
+
props.tree.child = result.props.tree.child;
|
|
4068
|
+
|
|
4069
|
+
if ('form' in result.props) {
|
|
4070
|
+
props.form = result.props.form;
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
if ('error' in result.props) {
|
|
4074
|
+
props.error = result.props.error;
|
|
4075
|
+
}
|
|
4076
|
+
}
|