@sveltejs/kit 1.0.0-next.99 → 1.0.1
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/README.md +5 -1
- package/package.json +91 -77
- package/postinstall.js +47 -0
- package/src/cli.js +44 -0
- package/src/constants.js +5 -0
- package/src/core/adapt/builder.js +221 -0
- package/src/core/adapt/index.js +31 -0
- package/src/core/config/default-error.html +56 -0
- package/src/core/config/index.js +100 -0
- package/src/core/config/options.js +387 -0
- package/src/core/config/types.d.ts +1 -0
- package/src/core/env.js +138 -0
- package/src/core/generate_manifest/index.js +116 -0
- package/src/core/prerender/crawl.js +207 -0
- package/src/core/prerender/entities.js +2252 -0
- package/src/core/prerender/fallback.js +43 -0
- package/src/core/prerender/prerender.js +459 -0
- package/src/core/prerender/queue.js +80 -0
- package/src/core/sync/create_manifest_data/conflict.js +0 -0
- package/src/core/sync/create_manifest_data/index.js +523 -0
- package/src/core/sync/create_manifest_data/sort.js +161 -0
- package/src/core/sync/create_manifest_data/types.d.ts +37 -0
- package/src/core/sync/sync.js +59 -0
- package/src/core/sync/utils.js +33 -0
- package/src/core/sync/write_ambient.js +58 -0
- package/src/core/sync/write_client_manifest.js +107 -0
- package/src/core/sync/write_matchers.js +25 -0
- package/src/core/sync/write_root.js +91 -0
- package/src/core/sync/write_tsconfig.js +195 -0
- package/src/core/sync/write_types/index.js +809 -0
- package/src/core/utils.js +67 -0
- package/src/exports/hooks/index.js +1 -0
- package/src/exports/hooks/sequence.js +44 -0
- package/src/exports/index.js +55 -0
- package/src/exports/node/index.js +172 -0
- package/src/exports/node/polyfills.js +28 -0
- package/src/exports/vite/build/build_server.js +359 -0
- package/src/exports/vite/build/build_service_worker.js +85 -0
- package/src/exports/vite/build/utils.js +230 -0
- package/src/exports/vite/dev/index.js +597 -0
- package/src/exports/vite/graph_analysis/index.js +99 -0
- package/src/exports/vite/graph_analysis/types.d.ts +5 -0
- package/src/exports/vite/graph_analysis/utils.js +6 -0
- package/src/exports/vite/index.js +708 -0
- package/src/exports/vite/preview/index.js +194 -0
- package/src/exports/vite/types.d.ts +3 -0
- package/src/exports/vite/utils.js +184 -0
- package/src/runtime/app/env.js +1 -0
- package/src/runtime/app/environment.js +13 -0
- package/src/runtime/app/forms.js +135 -0
- package/src/runtime/app/navigation.js +22 -0
- package/src/runtime/app/paths.js +1 -0
- package/src/runtime/app/stores.js +57 -0
- package/src/runtime/client/ambient.d.ts +30 -0
- package/src/runtime/client/client.js +1725 -0
- package/src/runtime/client/constants.js +10 -0
- package/src/runtime/client/fetcher.js +127 -0
- package/src/runtime/client/parse.js +60 -0
- package/src/runtime/client/singletons.js +21 -0
- package/src/runtime/client/start.js +45 -0
- package/src/runtime/client/types.d.ts +86 -0
- package/src/runtime/client/utils.js +257 -0
- package/src/runtime/components/error.svelte +6 -0
- package/{assets → src/runtime}/components/layout.svelte +0 -0
- package/src/runtime/control.js +45 -0
- package/src/runtime/env/dynamic/private.js +1 -0
- package/src/runtime/env/dynamic/public.js +1 -0
- package/src/runtime/env-private.js +6 -0
- package/src/runtime/env-public.js +6 -0
- package/src/runtime/env.js +12 -0
- package/src/runtime/hash.js +20 -0
- package/src/runtime/paths.js +11 -0
- package/src/runtime/server/cookie.js +228 -0
- package/src/runtime/server/data/index.js +158 -0
- package/src/runtime/server/endpoint.js +86 -0
- package/src/runtime/server/fetch.js +175 -0
- package/src/runtime/server/index.js +405 -0
- package/src/runtime/server/page/actions.js +267 -0
- package/src/runtime/server/page/crypto.js +239 -0
- package/src/runtime/server/page/csp.js +250 -0
- package/src/runtime/server/page/index.js +326 -0
- package/src/runtime/server/page/load_data.js +270 -0
- package/src/runtime/server/page/render.js +393 -0
- package/src/runtime/server/page/respond_with_error.js +103 -0
- package/src/runtime/server/page/serialize_data.js +87 -0
- package/src/runtime/server/page/types.d.ts +35 -0
- package/src/runtime/server/utils.js +179 -0
- package/src/utils/array.js +9 -0
- package/src/utils/error.js +22 -0
- package/src/utils/escape.js +46 -0
- package/src/utils/exports.js +54 -0
- package/src/utils/filesystem.js +178 -0
- package/src/utils/functions.js +16 -0
- package/src/utils/http.js +72 -0
- package/src/utils/misc.js +1 -0
- package/src/utils/promises.js +17 -0
- package/src/utils/routing.js +201 -0
- package/src/utils/unit_test.js +11 -0
- package/src/utils/url.js +161 -0
- package/svelte-kit.js +1 -1
- package/types/ambient.d.ts +451 -0
- package/types/index.d.ts +1168 -5
- package/types/internal.d.ts +348 -159
- package/types/private.d.ts +237 -0
- package/types/synthetic/$env+dynamic+private.md +10 -0
- package/types/synthetic/$env+dynamic+public.md +8 -0
- package/types/synthetic/$env+static+private.md +19 -0
- package/types/synthetic/$env+static+public.md +7 -0
- package/types/synthetic/$lib.md +5 -0
- package/CHANGELOG.md +0 -825
- package/assets/components/error.svelte +0 -21
- package/assets/runtime/app/env.js +0 -16
- package/assets/runtime/app/navigation.js +0 -53
- package/assets/runtime/app/paths.js +0 -1
- package/assets/runtime/app/stores.js +0 -87
- package/assets/runtime/chunks/utils.js +0 -13
- package/assets/runtime/env.js +0 -8
- package/assets/runtime/internal/singletons.js +0 -20
- package/assets/runtime/internal/start.js +0 -1061
- package/assets/runtime/paths.js +0 -12
- package/dist/chunks/_commonjsHelpers.js +0 -8
- package/dist/chunks/cert.js +0 -29079
- package/dist/chunks/constants.js +0 -3
- package/dist/chunks/index.js +0 -3532
- package/dist/chunks/index2.js +0 -583
- package/dist/chunks/index3.js +0 -31
- package/dist/chunks/index4.js +0 -1004
- package/dist/chunks/index5.js +0 -327
- package/dist/chunks/index6.js +0 -325
- package/dist/chunks/standard.js +0 -99
- package/dist/chunks/utils.js +0 -149
- package/dist/cli.js +0 -711
- package/dist/http.js +0 -66
- package/dist/install-fetch.js +0 -1699
- package/dist/ssr.js +0 -1523
- package/types/ambient-modules.d.ts +0 -115
- package/types/config.d.ts +0 -101
- package/types/endpoint.d.ts +0 -23
- package/types/helper.d.ts +0 -19
- package/types/hooks.d.ts +0 -21
- package/types/page.d.ts +0 -30
|
@@ -0,0 +1,1725 @@
|
|
|
1
|
+
import { DEV } from 'esm-env';
|
|
2
|
+
import { onMount, tick } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
make_trackable,
|
|
5
|
+
decode_pathname,
|
|
6
|
+
decode_params,
|
|
7
|
+
normalize_path,
|
|
8
|
+
add_data_suffix
|
|
9
|
+
} from '../../utils/url.js';
|
|
10
|
+
import {
|
|
11
|
+
find_anchor,
|
|
12
|
+
get_base_uri,
|
|
13
|
+
get_link_info,
|
|
14
|
+
get_router_options,
|
|
15
|
+
is_external_url,
|
|
16
|
+
scroll_state
|
|
17
|
+
} from './utils.js';
|
|
18
|
+
import {
|
|
19
|
+
lock_fetch,
|
|
20
|
+
unlock_fetch,
|
|
21
|
+
initial_fetch,
|
|
22
|
+
subsequent_fetch,
|
|
23
|
+
native_fetch
|
|
24
|
+
} from './fetcher.js';
|
|
25
|
+
import { parse } from './parse.js';
|
|
26
|
+
|
|
27
|
+
import Root from '__GENERATED__/root.svelte';
|
|
28
|
+
import { nodes, server_loads, dictionary, matchers, hooks } from '__GENERATED__/client-manifest.js';
|
|
29
|
+
import { HttpError, Redirect } from '../control.js';
|
|
30
|
+
import { stores } from './singletons.js';
|
|
31
|
+
import { unwrap_promises } from '../../utils/promises.js';
|
|
32
|
+
import * as devalue from 'devalue';
|
|
33
|
+
import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY } from './constants.js';
|
|
34
|
+
import { validate_common_exports } from '../../utils/exports.js';
|
|
35
|
+
|
|
36
|
+
const routes = parse(nodes, server_loads, dictionary, matchers);
|
|
37
|
+
|
|
38
|
+
const default_layout_loader = nodes[0];
|
|
39
|
+
const default_error_loader = nodes[1];
|
|
40
|
+
|
|
41
|
+
// we import the root layout/error nodes eagerly, so that
|
|
42
|
+
// connectivity errors after initialisation don't nuke the app
|
|
43
|
+
default_layout_loader();
|
|
44
|
+
default_error_loader();
|
|
45
|
+
|
|
46
|
+
// We track the scroll position associated with each history entry in sessionStorage,
|
|
47
|
+
// rather than on history.state itself, because when navigation is driven by
|
|
48
|
+
// popstate it's too late to update the scroll position associated with the
|
|
49
|
+
// state we're navigating from
|
|
50
|
+
|
|
51
|
+
/** @typedef {{ x: number, y: number }} ScrollPosition */
|
|
52
|
+
/** @type {Record<number, ScrollPosition>} */
|
|
53
|
+
let scroll_positions = {};
|
|
54
|
+
try {
|
|
55
|
+
scroll_positions = JSON.parse(sessionStorage[SCROLL_KEY]);
|
|
56
|
+
} catch {
|
|
57
|
+
// do nothing
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @param {number} index */
|
|
61
|
+
function update_scroll_positions(index) {
|
|
62
|
+
scroll_positions[index] = scroll_state();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {{
|
|
67
|
+
* target: HTMLElement;
|
|
68
|
+
* base: string;
|
|
69
|
+
* }} opts
|
|
70
|
+
* @returns {import('./types').Client}
|
|
71
|
+
*/
|
|
72
|
+
export function create_client({ target, base }) {
|
|
73
|
+
const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement;
|
|
74
|
+
/** @type {Array<((url: URL) => boolean)>} */
|
|
75
|
+
const invalidated = [];
|
|
76
|
+
|
|
77
|
+
/** @type {{id: string, promise: Promise<import('./types').NavigationResult>} | null} */
|
|
78
|
+
let load_cache = null;
|
|
79
|
+
|
|
80
|
+
const callbacks = {
|
|
81
|
+
/** @type {Array<(navigation: import('types').BeforeNavigate) => void>} */
|
|
82
|
+
before_navigate: [],
|
|
83
|
+
|
|
84
|
+
/** @type {Array<(navigation: import('types').AfterNavigate) => void>} */
|
|
85
|
+
after_navigate: []
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** @type {import('./types').NavigationState} */
|
|
89
|
+
let current = {
|
|
90
|
+
branch: [],
|
|
91
|
+
error: null,
|
|
92
|
+
// @ts-ignore - we need the initial value to be null
|
|
93
|
+
url: null
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** this being true means we SSR'd */
|
|
97
|
+
let hydrated = false;
|
|
98
|
+
let started = false;
|
|
99
|
+
let autoscroll = true;
|
|
100
|
+
let updating = false;
|
|
101
|
+
let navigating = false;
|
|
102
|
+
let hash_navigating = false;
|
|
103
|
+
|
|
104
|
+
let force_invalidation = false;
|
|
105
|
+
|
|
106
|
+
/** @type {import('svelte').SvelteComponent} */
|
|
107
|
+
let root;
|
|
108
|
+
|
|
109
|
+
// keeping track of the history index in order to prevent popstate navigation events if needed
|
|
110
|
+
let current_history_index = history.state?.[INDEX_KEY];
|
|
111
|
+
|
|
112
|
+
if (!current_history_index) {
|
|
113
|
+
// we use Date.now() as an offset so that cross-document navigations
|
|
114
|
+
// within the app don't result in data loss
|
|
115
|
+
current_history_index = Date.now();
|
|
116
|
+
|
|
117
|
+
// create initial history entry, so we can return here
|
|
118
|
+
history.replaceState(
|
|
119
|
+
{ ...history.state, [INDEX_KEY]: current_history_index },
|
|
120
|
+
'',
|
|
121
|
+
location.href
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// if we reload the page, or Cmd-Shift-T back to it,
|
|
126
|
+
// recover scroll position
|
|
127
|
+
const scroll = scroll_positions[current_history_index];
|
|
128
|
+
if (scroll) {
|
|
129
|
+
history.scrollRestoration = 'manual';
|
|
130
|
+
scrollTo(scroll.x, scroll.y);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @type {import('types').Page} */
|
|
134
|
+
let page;
|
|
135
|
+
|
|
136
|
+
/** @type {{}} */
|
|
137
|
+
let token;
|
|
138
|
+
|
|
139
|
+
/** @type {Promise<void> | null} */
|
|
140
|
+
let pending_invalidate;
|
|
141
|
+
|
|
142
|
+
async function invalidate() {
|
|
143
|
+
// Accept all invalidations as they come, don't swallow any while another invalidation
|
|
144
|
+
// is running because subsequent invalidations may make earlier ones outdated,
|
|
145
|
+
// but batch multiple synchronous invalidations.
|
|
146
|
+
pending_invalidate = pending_invalidate || Promise.resolve();
|
|
147
|
+
await pending_invalidate;
|
|
148
|
+
pending_invalidate = null;
|
|
149
|
+
|
|
150
|
+
const url = new URL(location.href);
|
|
151
|
+
const intent = get_navigation_intent(url, true);
|
|
152
|
+
// Clear preload, it might be affected by the invalidation.
|
|
153
|
+
// Also solves an edge case where a preload is triggered, the navigation for it
|
|
154
|
+
// was then triggered and is still running while the invalidation kicks in,
|
|
155
|
+
// at which point the invalidation should take over and "win".
|
|
156
|
+
load_cache = null;
|
|
157
|
+
await update(intent, url, []);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string | URL} url
|
|
162
|
+
* @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; state?: any; invalidateAll?: boolean }} opts
|
|
163
|
+
* @param {string[]} redirect_chain
|
|
164
|
+
* @param {{}} [nav_token]
|
|
165
|
+
*/
|
|
166
|
+
async function goto(
|
|
167
|
+
url,
|
|
168
|
+
{
|
|
169
|
+
noScroll = false,
|
|
170
|
+
replaceState = false,
|
|
171
|
+
keepFocus = false,
|
|
172
|
+
state = {},
|
|
173
|
+
invalidateAll = false
|
|
174
|
+
},
|
|
175
|
+
redirect_chain,
|
|
176
|
+
nav_token
|
|
177
|
+
) {
|
|
178
|
+
if (typeof url === 'string') {
|
|
179
|
+
url = new URL(url, get_base_uri(document));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return navigate({
|
|
183
|
+
url,
|
|
184
|
+
scroll: noScroll ? scroll_state() : null,
|
|
185
|
+
keepfocus: keepFocus,
|
|
186
|
+
redirect_chain,
|
|
187
|
+
details: {
|
|
188
|
+
state,
|
|
189
|
+
replaceState
|
|
190
|
+
},
|
|
191
|
+
nav_token,
|
|
192
|
+
accepted: () => {
|
|
193
|
+
if (invalidateAll) {
|
|
194
|
+
force_invalidation = true;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
blocked: () => {},
|
|
198
|
+
type: 'goto'
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @param {URL} url */
|
|
203
|
+
async function preload_data(url) {
|
|
204
|
+
const intent = get_navigation_intent(url, false);
|
|
205
|
+
|
|
206
|
+
if (!intent) {
|
|
207
|
+
throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
load_cache = {
|
|
211
|
+
id: intent.id,
|
|
212
|
+
promise: load_route(intent).then((result) => {
|
|
213
|
+
if (result.type === 'loaded' && result.state.error) {
|
|
214
|
+
// Don't cache errors, because they might be transient
|
|
215
|
+
load_cache = null;
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
})
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return load_cache.promise;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @param {...string} pathnames */
|
|
225
|
+
async function preload_code(...pathnames) {
|
|
226
|
+
const matching = routes.filter((route) => pathnames.some((pathname) => route.exec(pathname)));
|
|
227
|
+
|
|
228
|
+
const promises = matching.map((r) => {
|
|
229
|
+
return Promise.all([...r.layouts, r.leaf].map((load) => load?.[1]()));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await Promise.all(promises);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns `true` if update completes, `false` if it is aborted
|
|
237
|
+
* @param {import('./types').NavigationIntent | undefined} intent
|
|
238
|
+
* @param {URL} url
|
|
239
|
+
* @param {string[]} redirect_chain
|
|
240
|
+
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
|
|
241
|
+
* @param {{}} [nav_token] To distinguish between different navigation events and determine the latest. Needed for example for redirects to keep the original token
|
|
242
|
+
* @param {() => void} [callback]
|
|
243
|
+
*/
|
|
244
|
+
async function update(intent, url, redirect_chain, opts, nav_token = {}, callback) {
|
|
245
|
+
token = nav_token;
|
|
246
|
+
let navigation_result = intent && (await load_route(intent));
|
|
247
|
+
|
|
248
|
+
if (!navigation_result) {
|
|
249
|
+
navigation_result = await server_fallback(
|
|
250
|
+
url,
|
|
251
|
+
{ id: null },
|
|
252
|
+
await handle_error(new Error(`Not found: ${url.pathname}`), {
|
|
253
|
+
url,
|
|
254
|
+
params: {},
|
|
255
|
+
route: { id: null }
|
|
256
|
+
}),
|
|
257
|
+
404
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// if this is an internal navigation intent, use the normalized
|
|
262
|
+
// URL for the rest of the function
|
|
263
|
+
url = intent?.url || url;
|
|
264
|
+
|
|
265
|
+
// abort if user navigated during update
|
|
266
|
+
if (token !== nav_token) return false;
|
|
267
|
+
|
|
268
|
+
if (navigation_result.type === 'redirect') {
|
|
269
|
+
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
|
|
270
|
+
navigation_result = await load_root_error_page({
|
|
271
|
+
status: 500,
|
|
272
|
+
error: await handle_error(new Error('Redirect loop'), {
|
|
273
|
+
url,
|
|
274
|
+
params: {},
|
|
275
|
+
route: { id: null }
|
|
276
|
+
}),
|
|
277
|
+
url,
|
|
278
|
+
route: { id: null }
|
|
279
|
+
});
|
|
280
|
+
} else {
|
|
281
|
+
goto(
|
|
282
|
+
new URL(navigation_result.location, url).href,
|
|
283
|
+
{},
|
|
284
|
+
[...redirect_chain, url.pathname],
|
|
285
|
+
nav_token
|
|
286
|
+
);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
} else if (navigation_result.props?.page?.status >= 400) {
|
|
290
|
+
const updated = await stores.updated.check();
|
|
291
|
+
if (updated) {
|
|
292
|
+
await native_navigation(url);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// reset invalidation only after a finished navigation. If there are redirects or
|
|
297
|
+
// additional invalidations, they should get the same invalidation treatment
|
|
298
|
+
invalidated.length = 0;
|
|
299
|
+
force_invalidation = false;
|
|
300
|
+
|
|
301
|
+
updating = true;
|
|
302
|
+
|
|
303
|
+
if (opts && opts.details) {
|
|
304
|
+
const { details } = opts;
|
|
305
|
+
const change = details.replaceState ? 0 : 1;
|
|
306
|
+
details.state[INDEX_KEY] = current_history_index += change;
|
|
307
|
+
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// reset preload synchronously after the history state has been set to avoid race conditions
|
|
311
|
+
load_cache = null;
|
|
312
|
+
|
|
313
|
+
if (started) {
|
|
314
|
+
current = navigation_result.state;
|
|
315
|
+
|
|
316
|
+
if (navigation_result.props.page) {
|
|
317
|
+
navigation_result.props.page.url = url;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
root.$set(navigation_result.props);
|
|
321
|
+
} else {
|
|
322
|
+
initialize(navigation_result);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// opts must be passed if we're navigating
|
|
326
|
+
if (opts) {
|
|
327
|
+
const { scroll, keepfocus } = opts;
|
|
328
|
+
|
|
329
|
+
// reset focus first, so that manual focus management can override it
|
|
330
|
+
if (!keepfocus) reset_focus();
|
|
331
|
+
|
|
332
|
+
// need to render the DOM before we can scroll to the rendered elements
|
|
333
|
+
await tick();
|
|
334
|
+
|
|
335
|
+
if (autoscroll) {
|
|
336
|
+
const deep_linked = url.hash && document.getElementById(url.hash.slice(1));
|
|
337
|
+
if (scroll) {
|
|
338
|
+
scrollTo(scroll.x, scroll.y);
|
|
339
|
+
} else if (deep_linked) {
|
|
340
|
+
// Here we use `scrollIntoView` on the element instead of `scrollTo`
|
|
341
|
+
// because it natively supports the `scroll-margin` and `scroll-behavior`
|
|
342
|
+
// CSS properties.
|
|
343
|
+
deep_linked.scrollIntoView();
|
|
344
|
+
} else {
|
|
345
|
+
scrollTo(0, 0);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
await tick();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
autoscroll = true;
|
|
353
|
+
|
|
354
|
+
if (navigation_result.props.page) {
|
|
355
|
+
page = navigation_result.props.page;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (callback) callback();
|
|
359
|
+
|
|
360
|
+
updating = false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** @param {import('./types').NavigationFinished} result */
|
|
364
|
+
function initialize(result) {
|
|
365
|
+
if (DEV && document.querySelector('vite-error-overlay')) return;
|
|
366
|
+
|
|
367
|
+
current = result.state;
|
|
368
|
+
|
|
369
|
+
const style = document.querySelector('style[data-sveltekit]');
|
|
370
|
+
if (style) style.remove();
|
|
371
|
+
|
|
372
|
+
page = result.props.page;
|
|
373
|
+
|
|
374
|
+
root = new Root({
|
|
375
|
+
target,
|
|
376
|
+
props: { ...result.props, stores },
|
|
377
|
+
hydrate: true
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
/** @type {import('types').AfterNavigate} */
|
|
381
|
+
const navigation = {
|
|
382
|
+
from: null,
|
|
383
|
+
to: {
|
|
384
|
+
params: current.params,
|
|
385
|
+
route: { id: current.route?.id ?? null },
|
|
386
|
+
url: new URL(location.href)
|
|
387
|
+
},
|
|
388
|
+
willUnload: false,
|
|
389
|
+
type: 'enter'
|
|
390
|
+
};
|
|
391
|
+
callbacks.after_navigate.forEach((fn) => fn(navigation));
|
|
392
|
+
|
|
393
|
+
started = true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
*
|
|
398
|
+
* @param {{
|
|
399
|
+
* url: URL;
|
|
400
|
+
* params: Record<string, string>;
|
|
401
|
+
* branch: Array<import('./types').BranchNode | undefined>;
|
|
402
|
+
* status: number;
|
|
403
|
+
* error: App.Error | null;
|
|
404
|
+
* route: import('types').CSRRoute | null;
|
|
405
|
+
* form?: Record<string, any> | null;
|
|
406
|
+
* }} opts
|
|
407
|
+
*/
|
|
408
|
+
async function get_navigation_result_from_branch({
|
|
409
|
+
url,
|
|
410
|
+
params,
|
|
411
|
+
branch,
|
|
412
|
+
status,
|
|
413
|
+
error,
|
|
414
|
+
route,
|
|
415
|
+
form
|
|
416
|
+
}) {
|
|
417
|
+
const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
|
|
418
|
+
|
|
419
|
+
/** @type {import('types').TrailingSlash} */
|
|
420
|
+
let slash = 'never';
|
|
421
|
+
for (const node of branch) {
|
|
422
|
+
if (node?.slash !== undefined) slash = node.slash;
|
|
423
|
+
}
|
|
424
|
+
url.pathname = normalize_path(url.pathname, slash);
|
|
425
|
+
url.search = url.search; // turn `/?` into `/`
|
|
426
|
+
|
|
427
|
+
/** @type {import('./types').NavigationFinished} */
|
|
428
|
+
const result = {
|
|
429
|
+
type: 'loaded',
|
|
430
|
+
state: {
|
|
431
|
+
url,
|
|
432
|
+
params,
|
|
433
|
+
branch,
|
|
434
|
+
error,
|
|
435
|
+
route
|
|
436
|
+
},
|
|
437
|
+
props: {
|
|
438
|
+
components: filtered.map((branch_node) => branch_node.node.component)
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
if (form !== undefined) {
|
|
443
|
+
result.props.form = form;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let data = {};
|
|
447
|
+
let data_changed = !page;
|
|
448
|
+
for (let i = 0; i < filtered.length; i += 1) {
|
|
449
|
+
const node = filtered[i];
|
|
450
|
+
data = { ...data, ...node.data };
|
|
451
|
+
|
|
452
|
+
// Only set props if the node actually updated. This prevents needless rerenders.
|
|
453
|
+
if (data_changed || !current.branch.some((previous) => previous === node)) {
|
|
454
|
+
result.props[`data_${i}`] = data;
|
|
455
|
+
data_changed = data_changed || Object.keys(node.data ?? {}).length > 0;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (!data_changed) {
|
|
459
|
+
// If nothing was added, and the object entries are the same length, this means
|
|
460
|
+
// that nothing was removed either and therefore the data is the same as the previous one.
|
|
461
|
+
// This would be more readable with a separate boolean but that would cost us some bytes.
|
|
462
|
+
data_changed = Object.keys(page.data).length !== Object.keys(data).length;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const page_changed =
|
|
466
|
+
!current.url ||
|
|
467
|
+
url.href !== current.url.href ||
|
|
468
|
+
current.error !== error ||
|
|
469
|
+
form !== undefined ||
|
|
470
|
+
data_changed;
|
|
471
|
+
|
|
472
|
+
if (page_changed) {
|
|
473
|
+
result.props.page = {
|
|
474
|
+
error,
|
|
475
|
+
params,
|
|
476
|
+
route,
|
|
477
|
+
status,
|
|
478
|
+
url: new URL(url),
|
|
479
|
+
form: form ?? null,
|
|
480
|
+
// The whole page store is updated, but this way the object reference stays the same
|
|
481
|
+
data: data_changed ? data : page.data
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Call the load function of the given node, if it exists.
|
|
490
|
+
* If `server_data` is passed, this is treated as the initial run and the page endpoint is not requested.
|
|
491
|
+
*
|
|
492
|
+
* @param {{
|
|
493
|
+
* loader: import('types').CSRPageNodeLoader;
|
|
494
|
+
* parent: () => Promise<Record<string, any>>;
|
|
495
|
+
* url: URL;
|
|
496
|
+
* params: Record<string, string>;
|
|
497
|
+
* route: { id: string | null };
|
|
498
|
+
* server_data_node: import('./types').DataNode | null;
|
|
499
|
+
* }} options
|
|
500
|
+
* @returns {Promise<import('./types').BranchNode>}
|
|
501
|
+
*/
|
|
502
|
+
async function load_node({ loader, parent, url, params, route, server_data_node }) {
|
|
503
|
+
/** @type {Record<string, any> | null} */
|
|
504
|
+
let data = null;
|
|
505
|
+
|
|
506
|
+
/** @type {import('types').Uses} */
|
|
507
|
+
const uses = {
|
|
508
|
+
dependencies: new Set(),
|
|
509
|
+
params: new Set(),
|
|
510
|
+
parent: false,
|
|
511
|
+
route: false,
|
|
512
|
+
url: false
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const node = await loader();
|
|
516
|
+
|
|
517
|
+
if (DEV) {
|
|
518
|
+
validate_common_exports(node.universal);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (node.universal?.load) {
|
|
522
|
+
/** @param {string[]} deps */
|
|
523
|
+
function depends(...deps) {
|
|
524
|
+
for (const dep of deps) {
|
|
525
|
+
const { href } = new URL(dep, url);
|
|
526
|
+
uses.dependencies.add(href);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** @type {import('types').LoadEvent} */
|
|
531
|
+
const load_input = {
|
|
532
|
+
route: {
|
|
533
|
+
get id() {
|
|
534
|
+
uses.route = true;
|
|
535
|
+
return route.id;
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
params: new Proxy(params, {
|
|
539
|
+
get: (target, key) => {
|
|
540
|
+
uses.params.add(/** @type {string} */ (key));
|
|
541
|
+
return target[/** @type {string} */ (key)];
|
|
542
|
+
}
|
|
543
|
+
}),
|
|
544
|
+
data: server_data_node?.data ?? null,
|
|
545
|
+
url: make_trackable(url, () => {
|
|
546
|
+
uses.url = true;
|
|
547
|
+
}),
|
|
548
|
+
async fetch(resource, init) {
|
|
549
|
+
/** @type {URL | string} */
|
|
550
|
+
let requested;
|
|
551
|
+
|
|
552
|
+
if (resource instanceof Request) {
|
|
553
|
+
requested = resource.url;
|
|
554
|
+
|
|
555
|
+
// we're not allowed to modify the received `Request` object, so in order
|
|
556
|
+
// to fixup relative urls we create a new equivalent `init` object instead
|
|
557
|
+
init = {
|
|
558
|
+
// the request body must be consumed in memory until browsers
|
|
559
|
+
// implement streaming request bodies and/or the body getter
|
|
560
|
+
body:
|
|
561
|
+
resource.method === 'GET' || resource.method === 'HEAD'
|
|
562
|
+
? undefined
|
|
563
|
+
: await resource.blob(),
|
|
564
|
+
cache: resource.cache,
|
|
565
|
+
credentials: resource.credentials,
|
|
566
|
+
headers: resource.headers,
|
|
567
|
+
integrity: resource.integrity,
|
|
568
|
+
keepalive: resource.keepalive,
|
|
569
|
+
method: resource.method,
|
|
570
|
+
mode: resource.mode,
|
|
571
|
+
redirect: resource.redirect,
|
|
572
|
+
referrer: resource.referrer,
|
|
573
|
+
referrerPolicy: resource.referrerPolicy,
|
|
574
|
+
signal: resource.signal,
|
|
575
|
+
...init
|
|
576
|
+
};
|
|
577
|
+
} else {
|
|
578
|
+
requested = resource;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// we must fixup relative urls so they are resolved from the target page
|
|
582
|
+
const resolved = new URL(requested, url).href;
|
|
583
|
+
depends(resolved);
|
|
584
|
+
|
|
585
|
+
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved
|
|
586
|
+
return started
|
|
587
|
+
? subsequent_fetch(requested, resolved, init)
|
|
588
|
+
: initial_fetch(requested, init);
|
|
589
|
+
},
|
|
590
|
+
setHeaders: () => {}, // noop
|
|
591
|
+
depends,
|
|
592
|
+
parent() {
|
|
593
|
+
uses.parent = true;
|
|
594
|
+
return parent();
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
if (DEV) {
|
|
599
|
+
try {
|
|
600
|
+
lock_fetch();
|
|
601
|
+
data = (await node.universal.load.call(null, load_input)) ?? null;
|
|
602
|
+
if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`a load function related to route '${route.id}' returned ${
|
|
605
|
+
typeof data !== 'object'
|
|
606
|
+
? `a ${typeof data}`
|
|
607
|
+
: data instanceof Response
|
|
608
|
+
? 'a Response object'
|
|
609
|
+
: Array.isArray(data)
|
|
610
|
+
? 'an array'
|
|
611
|
+
: 'a non-plain object'
|
|
612
|
+
}, but must return a plain object at the top level (i.e. \`return {...}\`)`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
} finally {
|
|
616
|
+
unlock_fetch();
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
data = (await node.universal.load.call(null, load_input)) ?? null;
|
|
620
|
+
}
|
|
621
|
+
data = data ? await unwrap_promises(data) : null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
node,
|
|
626
|
+
loader,
|
|
627
|
+
server: server_data_node,
|
|
628
|
+
universal: node.universal?.load ? { type: 'data', data, uses } : null,
|
|
629
|
+
data: data ?? server_data_node?.data ?? null,
|
|
630
|
+
slash: node.universal?.trailingSlash ?? server_data_node?.slash
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* @param {boolean} parent_changed
|
|
636
|
+
* @param {boolean} route_changed
|
|
637
|
+
* @param {boolean} url_changed
|
|
638
|
+
* @param {import('types').Uses | undefined} uses
|
|
639
|
+
* @param {Record<string, string>} params
|
|
640
|
+
*/
|
|
641
|
+
function has_changed(parent_changed, route_changed, url_changed, uses, params) {
|
|
642
|
+
if (force_invalidation) return true;
|
|
643
|
+
|
|
644
|
+
if (!uses) return false;
|
|
645
|
+
|
|
646
|
+
if (uses.parent && parent_changed) return true;
|
|
647
|
+
if (uses.route && route_changed) return true;
|
|
648
|
+
if (uses.url && url_changed) return true;
|
|
649
|
+
|
|
650
|
+
for (const param of uses.params) {
|
|
651
|
+
if (params[param] !== current.params[param]) return true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
for (const href of uses.dependencies) {
|
|
655
|
+
if (invalidated.some((fn) => fn(new URL(href)))) return true;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* @param {import('types').ServerDataNode | import('types').ServerDataSkippedNode | null} node
|
|
663
|
+
* @param {import('./types').DataNode | null} [previous]
|
|
664
|
+
* @returns {import('./types').DataNode | null}
|
|
665
|
+
*/
|
|
666
|
+
function create_data_node(node, previous) {
|
|
667
|
+
if (node?.type === 'data') {
|
|
668
|
+
return {
|
|
669
|
+
type: 'data',
|
|
670
|
+
data: node.data,
|
|
671
|
+
uses: {
|
|
672
|
+
dependencies: new Set(node.uses.dependencies ?? []),
|
|
673
|
+
params: new Set(node.uses.params ?? []),
|
|
674
|
+
parent: !!node.uses.parent,
|
|
675
|
+
route: !!node.uses.route,
|
|
676
|
+
url: !!node.uses.url
|
|
677
|
+
},
|
|
678
|
+
slash: node.slash
|
|
679
|
+
};
|
|
680
|
+
} else if (node?.type === 'skip') {
|
|
681
|
+
return previous ?? null;
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* @param {import('./types').NavigationIntent} intent
|
|
688
|
+
* @returns {Promise<import('./types').NavigationResult>}
|
|
689
|
+
*/
|
|
690
|
+
async function load_route({ id, invalidating, url, params, route }) {
|
|
691
|
+
if (load_cache?.id === id) {
|
|
692
|
+
return load_cache.promise;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const { errors, layouts, leaf } = route;
|
|
696
|
+
|
|
697
|
+
const loaders = [...layouts, leaf];
|
|
698
|
+
|
|
699
|
+
// preload modules to avoid waterfall, but handle rejections
|
|
700
|
+
// so they don't get reported to Sentry et al (we don't need
|
|
701
|
+
// to act on the failures at this point)
|
|
702
|
+
errors.forEach((loader) => loader?.().catch(() => {}));
|
|
703
|
+
loaders.forEach((loader) => loader?.[1]().catch(() => {}));
|
|
704
|
+
|
|
705
|
+
/** @type {import('types').ServerData | null} */
|
|
706
|
+
let server_data = null;
|
|
707
|
+
|
|
708
|
+
const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
|
|
709
|
+
const route_changed = current.route ? id !== current.route.id : false;
|
|
710
|
+
|
|
711
|
+
const invalid_server_nodes = loaders.reduce((acc, loader, i) => {
|
|
712
|
+
const previous = current.branch[i];
|
|
713
|
+
|
|
714
|
+
const invalid =
|
|
715
|
+
!!loader?.[0] &&
|
|
716
|
+
(previous?.loader !== loader[1] ||
|
|
717
|
+
has_changed(
|
|
718
|
+
acc.some(Boolean),
|
|
719
|
+
route_changed,
|
|
720
|
+
url_changed,
|
|
721
|
+
previous.server?.uses,
|
|
722
|
+
params
|
|
723
|
+
));
|
|
724
|
+
|
|
725
|
+
acc.push(invalid);
|
|
726
|
+
return acc;
|
|
727
|
+
}, /** @type {boolean[]} */ ([]));
|
|
728
|
+
|
|
729
|
+
if (invalid_server_nodes.some(Boolean)) {
|
|
730
|
+
try {
|
|
731
|
+
server_data = await load_data(url, invalid_server_nodes);
|
|
732
|
+
} catch (error) {
|
|
733
|
+
return load_root_error_page({
|
|
734
|
+
status: 500,
|
|
735
|
+
error: await handle_error(error, { url, params, route: { id: route.id } }),
|
|
736
|
+
url,
|
|
737
|
+
route
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (server_data.type === 'redirect') {
|
|
742
|
+
return server_data;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const server_data_nodes = server_data?.nodes;
|
|
747
|
+
|
|
748
|
+
let parent_changed = false;
|
|
749
|
+
|
|
750
|
+
const branch_promises = loaders.map(async (loader, i) => {
|
|
751
|
+
if (!loader) return;
|
|
752
|
+
|
|
753
|
+
/** @type {import('./types').BranchNode | undefined} */
|
|
754
|
+
const previous = current.branch[i];
|
|
755
|
+
|
|
756
|
+
const server_data_node = server_data_nodes?.[i];
|
|
757
|
+
|
|
758
|
+
// re-use data from previous load if it's still valid
|
|
759
|
+
const valid =
|
|
760
|
+
(!server_data_node || server_data_node.type === 'skip') &&
|
|
761
|
+
loader[1] === previous?.loader &&
|
|
762
|
+
!has_changed(parent_changed, route_changed, url_changed, previous.universal?.uses, params);
|
|
763
|
+
if (valid) return previous;
|
|
764
|
+
|
|
765
|
+
parent_changed = true;
|
|
766
|
+
|
|
767
|
+
if (server_data_node?.type === 'error') {
|
|
768
|
+
// rethrow and catch below
|
|
769
|
+
throw server_data_node;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return load_node({
|
|
773
|
+
loader: loader[1],
|
|
774
|
+
url,
|
|
775
|
+
params,
|
|
776
|
+
route,
|
|
777
|
+
parent: async () => {
|
|
778
|
+
const data = {};
|
|
779
|
+
for (let j = 0; j < i; j += 1) {
|
|
780
|
+
Object.assign(data, (await branch_promises[j])?.data);
|
|
781
|
+
}
|
|
782
|
+
return data;
|
|
783
|
+
},
|
|
784
|
+
server_data_node: create_data_node(
|
|
785
|
+
// server_data_node is undefined if it wasn't reloaded from the server;
|
|
786
|
+
// and if current loader uses server data, we want to reuse previous data.
|
|
787
|
+
server_data_node === undefined && loader[0] ? { type: 'skip' } : server_data_node ?? null,
|
|
788
|
+
previous?.server
|
|
789
|
+
)
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// if we don't do this, rejections will be unhandled
|
|
794
|
+
for (const p of branch_promises) p.catch(() => {});
|
|
795
|
+
|
|
796
|
+
/** @type {Array<import('./types').BranchNode | undefined>} */
|
|
797
|
+
const branch = [];
|
|
798
|
+
|
|
799
|
+
for (let i = 0; i < loaders.length; i += 1) {
|
|
800
|
+
if (loaders[i]) {
|
|
801
|
+
try {
|
|
802
|
+
branch.push(await branch_promises[i]);
|
|
803
|
+
} catch (err) {
|
|
804
|
+
if (err instanceof Redirect) {
|
|
805
|
+
return {
|
|
806
|
+
type: 'redirect',
|
|
807
|
+
location: err.location
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
let status = 500;
|
|
812
|
+
/** @type {App.Error} */
|
|
813
|
+
let error;
|
|
814
|
+
|
|
815
|
+
if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
|
|
816
|
+
// this is the server error rethrown above, reconstruct but don't invoke
|
|
817
|
+
// the client error handler; it should've already been handled on the server
|
|
818
|
+
status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
|
|
819
|
+
error = /** @type {import('types').ServerErrorNode} */ (err).error;
|
|
820
|
+
} else if (err instanceof HttpError) {
|
|
821
|
+
status = err.status;
|
|
822
|
+
error = err.body;
|
|
823
|
+
} else {
|
|
824
|
+
error = await handle_error(err, { params, url, route: { id: route.id } });
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const error_load = await load_nearest_error_page(i, branch, errors);
|
|
828
|
+
if (error_load) {
|
|
829
|
+
return await get_navigation_result_from_branch({
|
|
830
|
+
url,
|
|
831
|
+
params,
|
|
832
|
+
branch: branch.slice(0, error_load.idx).concat(error_load.node),
|
|
833
|
+
status,
|
|
834
|
+
error,
|
|
835
|
+
route
|
|
836
|
+
});
|
|
837
|
+
} else {
|
|
838
|
+
// if we get here, it's because the root `load` function failed,
|
|
839
|
+
// and we need to fall back to the server
|
|
840
|
+
return await server_fallback(url, { id: route.id }, error, status);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
// push an empty slot so we can rewind past gaps to the
|
|
845
|
+
// layout that corresponds with an +error.svelte page
|
|
846
|
+
branch.push(undefined);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return await get_navigation_result_from_branch({
|
|
851
|
+
url,
|
|
852
|
+
params,
|
|
853
|
+
branch,
|
|
854
|
+
status: 200,
|
|
855
|
+
error: null,
|
|
856
|
+
route,
|
|
857
|
+
// Reset `form` on navigation, but not invalidation
|
|
858
|
+
form: invalidating ? undefined : null
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* @param {number} i Start index to backtrack from
|
|
864
|
+
* @param {Array<import('./types').BranchNode | undefined>} branch Branch to backtrack
|
|
865
|
+
* @param {Array<import('types').CSRPageNodeLoader | undefined>} errors All error pages for this branch
|
|
866
|
+
* @returns {Promise<{idx: number; node: import('./types').BranchNode} | undefined>}
|
|
867
|
+
*/
|
|
868
|
+
async function load_nearest_error_page(i, branch, errors) {
|
|
869
|
+
while (i--) {
|
|
870
|
+
if (errors[i]) {
|
|
871
|
+
let j = i;
|
|
872
|
+
while (!branch[j]) j -= 1;
|
|
873
|
+
try {
|
|
874
|
+
return {
|
|
875
|
+
idx: j + 1,
|
|
876
|
+
node: {
|
|
877
|
+
node: await /** @type {import('types').CSRPageNodeLoader } */ (errors[i])(),
|
|
878
|
+
loader: /** @type {import('types').CSRPageNodeLoader } */ (errors[i]),
|
|
879
|
+
data: {},
|
|
880
|
+
server: null,
|
|
881
|
+
universal: null
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
} catch (e) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* @param {{
|
|
893
|
+
* status: number;
|
|
894
|
+
* error: App.Error;
|
|
895
|
+
* url: URL;
|
|
896
|
+
* route: { id: string | null }
|
|
897
|
+
* }} opts
|
|
898
|
+
* @returns {Promise<import('./types').NavigationFinished>}
|
|
899
|
+
*/
|
|
900
|
+
async function load_root_error_page({ status, error, url, route }) {
|
|
901
|
+
/** @type {Record<string, string>} */
|
|
902
|
+
const params = {}; // error page does not have params
|
|
903
|
+
|
|
904
|
+
const node = await default_layout_loader();
|
|
905
|
+
|
|
906
|
+
/** @type {import('types').ServerDataNode | null} */
|
|
907
|
+
let server_data_node = null;
|
|
908
|
+
|
|
909
|
+
if (node.server) {
|
|
910
|
+
// TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
|
|
911
|
+
// existing root layout data
|
|
912
|
+
try {
|
|
913
|
+
const server_data = await load_data(url, [true]);
|
|
914
|
+
|
|
915
|
+
if (
|
|
916
|
+
server_data.type !== 'data' ||
|
|
917
|
+
(server_data.nodes[0] && server_data.nodes[0].type !== 'data')
|
|
918
|
+
) {
|
|
919
|
+
throw 0;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
server_data_node = server_data.nodes[0] ?? null;
|
|
923
|
+
} catch {
|
|
924
|
+
// at this point we have no choice but to fall back to the server, if it wouldn't
|
|
925
|
+
// bring us right back here, turning this into an endless loop
|
|
926
|
+
if (url.origin !== location.origin || url.pathname !== location.pathname || hydrated) {
|
|
927
|
+
await native_navigation(url);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const root_layout = await load_node({
|
|
933
|
+
loader: default_layout_loader,
|
|
934
|
+
url,
|
|
935
|
+
params,
|
|
936
|
+
route,
|
|
937
|
+
parent: () => Promise.resolve({}),
|
|
938
|
+
server_data_node: create_data_node(server_data_node)
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
/** @type {import('./types').BranchNode} */
|
|
942
|
+
const root_error = {
|
|
943
|
+
node: await default_error_loader(),
|
|
944
|
+
loader: default_error_loader,
|
|
945
|
+
universal: null,
|
|
946
|
+
server: null,
|
|
947
|
+
data: null
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
return await get_navigation_result_from_branch({
|
|
951
|
+
url,
|
|
952
|
+
params,
|
|
953
|
+
branch: [root_layout, root_error],
|
|
954
|
+
status,
|
|
955
|
+
error,
|
|
956
|
+
route: null
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* @param {URL} url
|
|
962
|
+
* @param {boolean} invalidating
|
|
963
|
+
*/
|
|
964
|
+
function get_navigation_intent(url, invalidating) {
|
|
965
|
+
if (is_external_url(url, base)) return;
|
|
966
|
+
|
|
967
|
+
const path = decode_pathname(url.pathname.slice(base.length) || '/');
|
|
968
|
+
|
|
969
|
+
for (const route of routes) {
|
|
970
|
+
const params = route.exec(path);
|
|
971
|
+
|
|
972
|
+
if (params) {
|
|
973
|
+
const id = url.pathname + url.search;
|
|
974
|
+
/** @type {import('./types').NavigationIntent} */
|
|
975
|
+
const intent = { id, invalidating, route, params: decode_params(params), url };
|
|
976
|
+
return intent;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* @param {{
|
|
983
|
+
* url: URL;
|
|
984
|
+
* type: import('types').NavigationType;
|
|
985
|
+
* intent?: import('./types').NavigationIntent;
|
|
986
|
+
* delta?: number;
|
|
987
|
+
* }} opts
|
|
988
|
+
*/
|
|
989
|
+
function before_navigate({ url, type, intent, delta }) {
|
|
990
|
+
let should_block = false;
|
|
991
|
+
|
|
992
|
+
/** @type {import('types').Navigation} */
|
|
993
|
+
const navigation = {
|
|
994
|
+
from: {
|
|
995
|
+
params: current.params,
|
|
996
|
+
route: { id: current.route?.id ?? null },
|
|
997
|
+
url: current.url
|
|
998
|
+
},
|
|
999
|
+
to: {
|
|
1000
|
+
params: intent?.params ?? null,
|
|
1001
|
+
route: { id: intent?.route?.id ?? null },
|
|
1002
|
+
url
|
|
1003
|
+
},
|
|
1004
|
+
willUnload: !intent,
|
|
1005
|
+
type
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
if (delta !== undefined) {
|
|
1009
|
+
navigation.delta = delta;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const cancellable = {
|
|
1013
|
+
...navigation,
|
|
1014
|
+
cancel: () => {
|
|
1015
|
+
should_block = true;
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
if (!navigating) {
|
|
1020
|
+
// Don't run the event during redirects
|
|
1021
|
+
callbacks.before_navigate.forEach((fn) => fn(cancellable));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return should_block ? null : navigation;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* @param {{
|
|
1029
|
+
* url: URL;
|
|
1030
|
+
* scroll: { x: number, y: number } | null;
|
|
1031
|
+
* keepfocus: boolean;
|
|
1032
|
+
* redirect_chain: string[];
|
|
1033
|
+
* details: {
|
|
1034
|
+
* replaceState: boolean;
|
|
1035
|
+
* state: any;
|
|
1036
|
+
* } | null;
|
|
1037
|
+
* type: import('types').NavigationType;
|
|
1038
|
+
* delta?: number;
|
|
1039
|
+
* nav_token?: {};
|
|
1040
|
+
* accepted: () => void;
|
|
1041
|
+
* blocked: () => void;
|
|
1042
|
+
* }} opts
|
|
1043
|
+
*/
|
|
1044
|
+
async function navigate({
|
|
1045
|
+
url,
|
|
1046
|
+
scroll,
|
|
1047
|
+
keepfocus,
|
|
1048
|
+
redirect_chain,
|
|
1049
|
+
details,
|
|
1050
|
+
type,
|
|
1051
|
+
delta,
|
|
1052
|
+
nav_token,
|
|
1053
|
+
accepted,
|
|
1054
|
+
blocked
|
|
1055
|
+
}) {
|
|
1056
|
+
const intent = get_navigation_intent(url, false);
|
|
1057
|
+
const navigation = before_navigate({ url, type, delta, intent });
|
|
1058
|
+
|
|
1059
|
+
if (!navigation) {
|
|
1060
|
+
blocked();
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
update_scroll_positions(current_history_index);
|
|
1065
|
+
|
|
1066
|
+
accepted();
|
|
1067
|
+
|
|
1068
|
+
navigating = true;
|
|
1069
|
+
|
|
1070
|
+
if (started) {
|
|
1071
|
+
stores.navigating.set(navigation);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
await update(
|
|
1075
|
+
intent,
|
|
1076
|
+
url,
|
|
1077
|
+
redirect_chain,
|
|
1078
|
+
{
|
|
1079
|
+
scroll,
|
|
1080
|
+
keepfocus,
|
|
1081
|
+
details
|
|
1082
|
+
},
|
|
1083
|
+
nav_token,
|
|
1084
|
+
() => {
|
|
1085
|
+
navigating = false;
|
|
1086
|
+
callbacks.after_navigate.forEach((fn) =>
|
|
1087
|
+
fn(/** @type {import('types').AfterNavigate} */ (navigation))
|
|
1088
|
+
);
|
|
1089
|
+
stores.navigating.set(null);
|
|
1090
|
+
}
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Does a full page reload if it wouldn't result in an endless loop in the SPA case
|
|
1096
|
+
* @param {URL} url
|
|
1097
|
+
* @param {{ id: string | null }} route
|
|
1098
|
+
* @param {App.Error} error
|
|
1099
|
+
* @param {number} status
|
|
1100
|
+
* @returns {Promise<import('./types').NavigationFinished>}
|
|
1101
|
+
*/
|
|
1102
|
+
async function server_fallback(url, route, error, status) {
|
|
1103
|
+
if (url.origin === location.origin && url.pathname === location.pathname && !hydrated) {
|
|
1104
|
+
// We would reload the same page we're currently on, which isn't hydrated,
|
|
1105
|
+
// which means no SSR, which means we would end up in an endless loop
|
|
1106
|
+
return await load_root_error_page({
|
|
1107
|
+
status,
|
|
1108
|
+
error,
|
|
1109
|
+
url,
|
|
1110
|
+
route
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
return await native_navigation(url);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Loads `href` the old-fashioned way, with a full page reload.
|
|
1118
|
+
* Returns a `Promise` that never resolves (to prevent any
|
|
1119
|
+
* subsequent work, e.g. history manipulation, from happening)
|
|
1120
|
+
* @param {URL} url
|
|
1121
|
+
*/
|
|
1122
|
+
function native_navigation(url) {
|
|
1123
|
+
location.href = url.href;
|
|
1124
|
+
return new Promise(() => {});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (import.meta.hot) {
|
|
1128
|
+
import.meta.hot.on('vite:beforeUpdate', () => {
|
|
1129
|
+
if (current.error) location.reload();
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function setup_preload() {
|
|
1134
|
+
/** @type {NodeJS.Timeout} */
|
|
1135
|
+
let mousemove_timeout;
|
|
1136
|
+
|
|
1137
|
+
container.addEventListener('mousemove', (event) => {
|
|
1138
|
+
const target = /** @type {Element} */ (event.target);
|
|
1139
|
+
|
|
1140
|
+
clearTimeout(mousemove_timeout);
|
|
1141
|
+
mousemove_timeout = setTimeout(() => {
|
|
1142
|
+
preload(target, 2);
|
|
1143
|
+
}, 20);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
/** @param {Event} event */
|
|
1147
|
+
function tap(event) {
|
|
1148
|
+
preload(/** @type {Element} */ (event.composedPath()[0]), 1);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
container.addEventListener('mousedown', tap);
|
|
1152
|
+
container.addEventListener('touchstart', tap, { passive: true });
|
|
1153
|
+
|
|
1154
|
+
const observer = new IntersectionObserver(
|
|
1155
|
+
(entries) => {
|
|
1156
|
+
for (const entry of entries) {
|
|
1157
|
+
if (entry.isIntersecting) {
|
|
1158
|
+
preload_code(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href).pathname);
|
|
1159
|
+
observer.unobserve(entry.target);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
{ threshold: 0 }
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* @param {Element} element
|
|
1168
|
+
* @param {number} priority
|
|
1169
|
+
*/
|
|
1170
|
+
function preload(element, priority) {
|
|
1171
|
+
const a = find_anchor(element, container);
|
|
1172
|
+
if (!a) return;
|
|
1173
|
+
|
|
1174
|
+
const { url, external } = get_link_info(a, base);
|
|
1175
|
+
if (external) return;
|
|
1176
|
+
|
|
1177
|
+
const options = get_router_options(a);
|
|
1178
|
+
|
|
1179
|
+
if (!options.reload) {
|
|
1180
|
+
if (priority <= options.preload_data) {
|
|
1181
|
+
preload_data(/** @type {URL} */ (url));
|
|
1182
|
+
} else if (priority <= options.preload_code) {
|
|
1183
|
+
preload_code(/** @type {URL} */ (url).pathname);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function after_navigate() {
|
|
1189
|
+
observer.disconnect();
|
|
1190
|
+
|
|
1191
|
+
for (const a of container.querySelectorAll('a')) {
|
|
1192
|
+
const { url, external } = get_link_info(a, base);
|
|
1193
|
+
if (external) continue;
|
|
1194
|
+
|
|
1195
|
+
const options = get_router_options(a);
|
|
1196
|
+
if (options.reload) continue;
|
|
1197
|
+
|
|
1198
|
+
if (options.preload_code === PRELOAD_PRIORITIES.viewport) {
|
|
1199
|
+
observer.observe(a);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (options.preload_code === PRELOAD_PRIORITIES.eager) {
|
|
1203
|
+
preload_code(/** @type {URL} */ (url).pathname);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
callbacks.after_navigate.push(after_navigate);
|
|
1209
|
+
after_navigate();
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
return {
|
|
1213
|
+
after_navigate: (fn) => {
|
|
1214
|
+
onMount(() => {
|
|
1215
|
+
callbacks.after_navigate.push(fn);
|
|
1216
|
+
|
|
1217
|
+
return () => {
|
|
1218
|
+
const i = callbacks.after_navigate.indexOf(fn);
|
|
1219
|
+
callbacks.after_navigate.splice(i, 1);
|
|
1220
|
+
};
|
|
1221
|
+
});
|
|
1222
|
+
},
|
|
1223
|
+
|
|
1224
|
+
before_navigate: (fn) => {
|
|
1225
|
+
onMount(() => {
|
|
1226
|
+
callbacks.before_navigate.push(fn);
|
|
1227
|
+
|
|
1228
|
+
return () => {
|
|
1229
|
+
const i = callbacks.before_navigate.indexOf(fn);
|
|
1230
|
+
callbacks.before_navigate.splice(i, 1);
|
|
1231
|
+
};
|
|
1232
|
+
});
|
|
1233
|
+
},
|
|
1234
|
+
|
|
1235
|
+
disable_scroll_handling: () => {
|
|
1236
|
+
if (DEV && started && !updating) {
|
|
1237
|
+
throw new Error('Can only disable scroll handling during navigation');
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (updating || !started) {
|
|
1241
|
+
autoscroll = false;
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
1244
|
+
|
|
1245
|
+
goto: (href, opts = {}) => {
|
|
1246
|
+
return goto(href, opts, []);
|
|
1247
|
+
},
|
|
1248
|
+
|
|
1249
|
+
invalidate: (resource) => {
|
|
1250
|
+
if (typeof resource === 'function') {
|
|
1251
|
+
invalidated.push(resource);
|
|
1252
|
+
} else {
|
|
1253
|
+
const { href } = new URL(resource, location.href);
|
|
1254
|
+
invalidated.push((url) => url.href === href);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return invalidate();
|
|
1258
|
+
},
|
|
1259
|
+
|
|
1260
|
+
invalidateAll: () => {
|
|
1261
|
+
force_invalidation = true;
|
|
1262
|
+
return invalidate();
|
|
1263
|
+
},
|
|
1264
|
+
|
|
1265
|
+
preload_data: async (href) => {
|
|
1266
|
+
const url = new URL(href, get_base_uri(document));
|
|
1267
|
+
await preload_data(url);
|
|
1268
|
+
},
|
|
1269
|
+
|
|
1270
|
+
preload_code,
|
|
1271
|
+
|
|
1272
|
+
apply_action: async (result) => {
|
|
1273
|
+
if (result.type === 'error') {
|
|
1274
|
+
const url = new URL(location.href);
|
|
1275
|
+
|
|
1276
|
+
const { branch, route } = current;
|
|
1277
|
+
if (!route) return;
|
|
1278
|
+
|
|
1279
|
+
const error_load = await load_nearest_error_page(
|
|
1280
|
+
current.branch.length,
|
|
1281
|
+
branch,
|
|
1282
|
+
route.errors
|
|
1283
|
+
);
|
|
1284
|
+
if (error_load) {
|
|
1285
|
+
const navigation_result = await get_navigation_result_from_branch({
|
|
1286
|
+
url,
|
|
1287
|
+
params: current.params,
|
|
1288
|
+
branch: branch.slice(0, error_load.idx).concat(error_load.node),
|
|
1289
|
+
status: result.status ?? 500,
|
|
1290
|
+
error: result.error,
|
|
1291
|
+
route
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
current = navigation_result.state;
|
|
1295
|
+
|
|
1296
|
+
root.$set(navigation_result.props);
|
|
1297
|
+
|
|
1298
|
+
tick().then(reset_focus);
|
|
1299
|
+
}
|
|
1300
|
+
} else if (result.type === 'redirect') {
|
|
1301
|
+
goto(result.location, { invalidateAll: true }, []);
|
|
1302
|
+
} else {
|
|
1303
|
+
/** @type {Record<string, any>} */
|
|
1304
|
+
const props = {
|
|
1305
|
+
form: result.data,
|
|
1306
|
+
page: { ...page, form: result.data, status: result.status }
|
|
1307
|
+
};
|
|
1308
|
+
root.$set(props);
|
|
1309
|
+
|
|
1310
|
+
if (result.type === 'success') {
|
|
1311
|
+
tick().then(reset_focus);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
|
|
1316
|
+
_start_router: () => {
|
|
1317
|
+
history.scrollRestoration = 'manual';
|
|
1318
|
+
|
|
1319
|
+
// Adopted from Nuxt.js
|
|
1320
|
+
// Reset scrollRestoration to auto when leaving page, allowing page reload
|
|
1321
|
+
// and back-navigation from other pages to use the browser to restore the
|
|
1322
|
+
// scrolling position.
|
|
1323
|
+
addEventListener('beforeunload', (e) => {
|
|
1324
|
+
let should_block = false;
|
|
1325
|
+
|
|
1326
|
+
if (!navigating) {
|
|
1327
|
+
// If we're navigating, beforeNavigate was already called. If we end up in here during navigation,
|
|
1328
|
+
// it's due to an external or full-page-reload link, for which we don't want to call the hook again.
|
|
1329
|
+
/** @type {import('types').BeforeNavigate} */
|
|
1330
|
+
const navigation = {
|
|
1331
|
+
from: {
|
|
1332
|
+
params: current.params,
|
|
1333
|
+
route: { id: current.route?.id ?? null },
|
|
1334
|
+
url: current.url
|
|
1335
|
+
},
|
|
1336
|
+
to: null,
|
|
1337
|
+
willUnload: true,
|
|
1338
|
+
type: 'leave',
|
|
1339
|
+
cancel: () => (should_block = true)
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
callbacks.before_navigate.forEach((fn) => fn(navigation));
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (should_block) {
|
|
1346
|
+
e.preventDefault();
|
|
1347
|
+
e.returnValue = '';
|
|
1348
|
+
} else {
|
|
1349
|
+
history.scrollRestoration = 'auto';
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
addEventListener('visibilitychange', () => {
|
|
1354
|
+
if (document.visibilityState === 'hidden') {
|
|
1355
|
+
update_scroll_positions(current_history_index);
|
|
1356
|
+
|
|
1357
|
+
try {
|
|
1358
|
+
sessionStorage[SCROLL_KEY] = JSON.stringify(scroll_positions);
|
|
1359
|
+
} catch {
|
|
1360
|
+
// do nothing
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
// @ts-expect-error this isn't supported everywhere yet
|
|
1366
|
+
if (!navigator.connection?.saveData) {
|
|
1367
|
+
setup_preload();
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/** @param {MouseEvent} event */
|
|
1371
|
+
container.addEventListener('click', (event) => {
|
|
1372
|
+
// Adapted from https://github.com/visionmedia/page.js
|
|
1373
|
+
// MIT license https://github.com/visionmedia/page.js#license
|
|
1374
|
+
if (event.button || event.which !== 1) return;
|
|
1375
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
1376
|
+
if (event.defaultPrevented) return;
|
|
1377
|
+
|
|
1378
|
+
const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
|
|
1379
|
+
if (!a) return;
|
|
1380
|
+
|
|
1381
|
+
const { url, external, has } = get_link_info(a, base);
|
|
1382
|
+
const options = get_router_options(a);
|
|
1383
|
+
if (!url) return;
|
|
1384
|
+
|
|
1385
|
+
const is_svg_a_element = a instanceof SVGAElement;
|
|
1386
|
+
|
|
1387
|
+
// Ignore URL protocols that differ to the current one and are not http(s) (e.g. `mailto:`, `tel:`, `myapp:`, etc.)
|
|
1388
|
+
// This may be wrong when the protocol is x: and the link goes to y:.. which should be treated as an external
|
|
1389
|
+
// navigation, but it's not clear how to handle that case and it's not likely to come up in practice.
|
|
1390
|
+
// MEMO: Without this condition, firefox will open mailer twice.
|
|
1391
|
+
// See:
|
|
1392
|
+
// - https://github.com/sveltejs/kit/issues/4045
|
|
1393
|
+
// - https://github.com/sveltejs/kit/issues/5725
|
|
1394
|
+
// - https://github.com/sveltejs/kit/issues/6496
|
|
1395
|
+
if (
|
|
1396
|
+
!is_svg_a_element &&
|
|
1397
|
+
url.protocol !== location.protocol &&
|
|
1398
|
+
!(url.protocol === 'https:' || url.protocol === 'http:')
|
|
1399
|
+
)
|
|
1400
|
+
return;
|
|
1401
|
+
|
|
1402
|
+
if (has.download) return;
|
|
1403
|
+
|
|
1404
|
+
// Ignore the following but fire beforeNavigate
|
|
1405
|
+
if (external || options.reload) {
|
|
1406
|
+
const navigation = before_navigate({ url, type: 'link' });
|
|
1407
|
+
if (!navigation) {
|
|
1408
|
+
event.preventDefault();
|
|
1409
|
+
}
|
|
1410
|
+
navigating = true;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Check if new url only differs by hash and use the browser default behavior in that case
|
|
1415
|
+
// This will ensure the `hashchange` event is fired
|
|
1416
|
+
// Removing the hash does a full page navigation in the browser, so make sure a hash is present
|
|
1417
|
+
const [nonhash, hash] = url.href.split('#');
|
|
1418
|
+
if (hash !== undefined && nonhash === location.href.split('#')[0]) {
|
|
1419
|
+
// set this flag to distinguish between navigations triggered by
|
|
1420
|
+
// clicking a hash link and those triggered by popstate
|
|
1421
|
+
// TODO why not update history here directly?
|
|
1422
|
+
hash_navigating = true;
|
|
1423
|
+
|
|
1424
|
+
update_scroll_positions(current_history_index);
|
|
1425
|
+
|
|
1426
|
+
current.url = url;
|
|
1427
|
+
stores.page.set({ ...page, url });
|
|
1428
|
+
stores.page.notify();
|
|
1429
|
+
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
navigate({
|
|
1434
|
+
url,
|
|
1435
|
+
scroll: options.noscroll ? scroll_state() : null,
|
|
1436
|
+
keepfocus: false,
|
|
1437
|
+
redirect_chain: [],
|
|
1438
|
+
details: {
|
|
1439
|
+
state: {},
|
|
1440
|
+
replaceState: url.href === location.href
|
|
1441
|
+
},
|
|
1442
|
+
accepted: () => event.preventDefault(),
|
|
1443
|
+
blocked: () => event.preventDefault(),
|
|
1444
|
+
type: 'link'
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
container.addEventListener('submit', (event) => {
|
|
1449
|
+
if (event.defaultPrevented) return;
|
|
1450
|
+
|
|
1451
|
+
const form = /** @type {HTMLFormElement} */ (
|
|
1452
|
+
HTMLFormElement.prototype.cloneNode.call(event.target)
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
const submitter = /** @type {HTMLButtonElement | HTMLInputElement | null} */ (
|
|
1456
|
+
event.submitter
|
|
1457
|
+
);
|
|
1458
|
+
|
|
1459
|
+
const method = submitter?.formMethod || form.method;
|
|
1460
|
+
|
|
1461
|
+
if (method !== 'get') return;
|
|
1462
|
+
|
|
1463
|
+
const url = new URL(
|
|
1464
|
+
(event.submitter?.hasAttribute('formaction') && submitter?.formAction) || form.action
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
if (is_external_url(url, base)) return;
|
|
1468
|
+
|
|
1469
|
+
const { noscroll, reload } = get_router_options(
|
|
1470
|
+
/** @type {HTMLFormElement} */ (event.target)
|
|
1471
|
+
);
|
|
1472
|
+
if (reload) return;
|
|
1473
|
+
|
|
1474
|
+
event.preventDefault();
|
|
1475
|
+
event.stopPropagation();
|
|
1476
|
+
|
|
1477
|
+
// @ts-expect-error `URLSearchParams(fd)` is kosher, but typescript doesn't know that
|
|
1478
|
+
url.search = new URLSearchParams(new FormData(event.target)).toString();
|
|
1479
|
+
|
|
1480
|
+
navigate({
|
|
1481
|
+
url,
|
|
1482
|
+
scroll: noscroll ? scroll_state() : null,
|
|
1483
|
+
keepfocus: false,
|
|
1484
|
+
redirect_chain: [],
|
|
1485
|
+
details: {
|
|
1486
|
+
state: {},
|
|
1487
|
+
replaceState: false
|
|
1488
|
+
},
|
|
1489
|
+
nav_token: {},
|
|
1490
|
+
accepted: () => {},
|
|
1491
|
+
blocked: () => {},
|
|
1492
|
+
type: 'form'
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
addEventListener('popstate', (event) => {
|
|
1497
|
+
if (event.state?.[INDEX_KEY]) {
|
|
1498
|
+
// if a popstate-driven navigation is cancelled, we need to counteract it
|
|
1499
|
+
// with history.go, which means we end up back here, hence this check
|
|
1500
|
+
if (event.state[INDEX_KEY] === current_history_index) return;
|
|
1501
|
+
|
|
1502
|
+
const delta = event.state[INDEX_KEY] - current_history_index;
|
|
1503
|
+
|
|
1504
|
+
navigate({
|
|
1505
|
+
url: new URL(location.href),
|
|
1506
|
+
scroll: scroll_positions[event.state[INDEX_KEY]],
|
|
1507
|
+
keepfocus: false,
|
|
1508
|
+
redirect_chain: [],
|
|
1509
|
+
details: null,
|
|
1510
|
+
accepted: () => {
|
|
1511
|
+
current_history_index = event.state[INDEX_KEY];
|
|
1512
|
+
},
|
|
1513
|
+
blocked: () => {
|
|
1514
|
+
history.go(-delta);
|
|
1515
|
+
},
|
|
1516
|
+
type: 'popstate',
|
|
1517
|
+
delta
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
addEventListener('hashchange', () => {
|
|
1523
|
+
// if the hashchange happened as a result of clicking on a link,
|
|
1524
|
+
// we need to update history, otherwise we have to leave it alone
|
|
1525
|
+
if (hash_navigating) {
|
|
1526
|
+
hash_navigating = false;
|
|
1527
|
+
history.replaceState(
|
|
1528
|
+
{ ...history.state, [INDEX_KEY]: ++current_history_index },
|
|
1529
|
+
'',
|
|
1530
|
+
location.href
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
// fix link[rel=icon], because browsers will occasionally try to load relative
|
|
1536
|
+
// URLs after a pushState/replaceState, resulting in a 404 — see
|
|
1537
|
+
// https://github.com/sveltejs/kit/issues/3748#issuecomment-1125980897
|
|
1538
|
+
for (const link of document.querySelectorAll('link')) {
|
|
1539
|
+
if (link.rel === 'icon') link.href = link.href;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
addEventListener('pageshow', (event) => {
|
|
1543
|
+
// If the user navigates to another site and then uses the back button and
|
|
1544
|
+
// bfcache hits, we need to set navigating to null, the site doesn't know
|
|
1545
|
+
// the navigation away from it was successful.
|
|
1546
|
+
// Info about bfcache here: https://web.dev/bfcache
|
|
1547
|
+
if (event.persisted) {
|
|
1548
|
+
stores.navigating.set(null);
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
},
|
|
1552
|
+
|
|
1553
|
+
_hydrate: async ({
|
|
1554
|
+
status = 200,
|
|
1555
|
+
error,
|
|
1556
|
+
node_ids,
|
|
1557
|
+
params,
|
|
1558
|
+
route,
|
|
1559
|
+
data: server_data_nodes,
|
|
1560
|
+
form
|
|
1561
|
+
}) => {
|
|
1562
|
+
hydrated = true;
|
|
1563
|
+
|
|
1564
|
+
const url = new URL(location.href);
|
|
1565
|
+
|
|
1566
|
+
if (!__SVELTEKIT_EMBEDDED__) {
|
|
1567
|
+
// See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation
|
|
1568
|
+
// of determining the params on the client side.
|
|
1569
|
+
({ params = {}, route = { id: null } } = get_navigation_intent(url, false) || {});
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/** @type {import('./types').NavigationFinished | undefined} */
|
|
1573
|
+
let result;
|
|
1574
|
+
|
|
1575
|
+
try {
|
|
1576
|
+
const branch_promises = node_ids.map(async (n, i) => {
|
|
1577
|
+
const server_data_node = server_data_nodes[i];
|
|
1578
|
+
|
|
1579
|
+
return load_node({
|
|
1580
|
+
loader: nodes[n],
|
|
1581
|
+
url,
|
|
1582
|
+
params,
|
|
1583
|
+
route,
|
|
1584
|
+
parent: async () => {
|
|
1585
|
+
const data = {};
|
|
1586
|
+
for (let j = 0; j < i; j += 1) {
|
|
1587
|
+
Object.assign(data, (await branch_promises[j]).data);
|
|
1588
|
+
}
|
|
1589
|
+
return data;
|
|
1590
|
+
},
|
|
1591
|
+
server_data_node: create_data_node(server_data_node)
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
result = await get_navigation_result_from_branch({
|
|
1596
|
+
url,
|
|
1597
|
+
params,
|
|
1598
|
+
branch: await Promise.all(branch_promises),
|
|
1599
|
+
status,
|
|
1600
|
+
error,
|
|
1601
|
+
form,
|
|
1602
|
+
route: routes.find(({ id }) => id === route.id) ?? null
|
|
1603
|
+
});
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
if (error instanceof Redirect) {
|
|
1606
|
+
// this is a real edge case — `load` would need to return
|
|
1607
|
+
// a redirect but only in the browser
|
|
1608
|
+
await native_navigation(new URL(error.location, location.href));
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
result = await load_root_error_page({
|
|
1613
|
+
status: error instanceof HttpError ? error.status : 500,
|
|
1614
|
+
error: await handle_error(error, { url, params, route }),
|
|
1615
|
+
url,
|
|
1616
|
+
route
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
initialize(result);
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* @param {URL} url
|
|
1627
|
+
* @param {boolean[]} invalid
|
|
1628
|
+
* @returns {Promise<import('types').ServerData>}
|
|
1629
|
+
*/
|
|
1630
|
+
async function load_data(url, invalid) {
|
|
1631
|
+
const data_url = new URL(url);
|
|
1632
|
+
data_url.pathname = add_data_suffix(url.pathname);
|
|
1633
|
+
if (DEV && url.searchParams.has('x-sveltekit-invalidated')) {
|
|
1634
|
+
throw new Error('Cannot used reserved query parameter "x-sveltekit-invalidated"');
|
|
1635
|
+
}
|
|
1636
|
+
data_url.searchParams.append(
|
|
1637
|
+
'x-sveltekit-invalidated',
|
|
1638
|
+
invalid.map((x) => (x ? '1' : '')).join('_')
|
|
1639
|
+
);
|
|
1640
|
+
|
|
1641
|
+
const res = await native_fetch(data_url.href);
|
|
1642
|
+
const data = await res.json();
|
|
1643
|
+
|
|
1644
|
+
if (!res.ok) {
|
|
1645
|
+
// error message is a JSON-stringified string which devalue can't handle at the top level
|
|
1646
|
+
throw new Error(data);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// revive devalue-flattened data
|
|
1650
|
+
data.nodes?.forEach((/** @type {any} */ node) => {
|
|
1651
|
+
if (node?.type === 'data') {
|
|
1652
|
+
node.data = devalue.unflatten(node.data);
|
|
1653
|
+
node.uses = {
|
|
1654
|
+
dependencies: new Set(node.uses.dependencies ?? []),
|
|
1655
|
+
params: new Set(node.uses.params ?? []),
|
|
1656
|
+
parent: !!node.uses.parent,
|
|
1657
|
+
route: !!node.uses.route,
|
|
1658
|
+
url: !!node.uses.url
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
return data;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* @param {unknown} error
|
|
1668
|
+
* @param {import('types').NavigationEvent} event
|
|
1669
|
+
* @returns {import('../../../types/private.js').MaybePromise<App.Error>}
|
|
1670
|
+
*/
|
|
1671
|
+
function handle_error(error, event) {
|
|
1672
|
+
if (error instanceof HttpError) {
|
|
1673
|
+
return error.body;
|
|
1674
|
+
}
|
|
1675
|
+
return (
|
|
1676
|
+
hooks.handleError({ error, event }) ??
|
|
1677
|
+
/** @type {any} */ ({ message: event.route.id != null ? 'Internal Error' : 'Not Found' })
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function reset_focus() {
|
|
1682
|
+
const autofocus = document.querySelector('[autofocus]');
|
|
1683
|
+
if (autofocus) {
|
|
1684
|
+
// @ts-ignore
|
|
1685
|
+
autofocus.focus();
|
|
1686
|
+
} else {
|
|
1687
|
+
// Reset page selection and focus
|
|
1688
|
+
// We try to mimic browsers' behaviour as closely as possible by targeting the
|
|
1689
|
+
// first scrollable region, but unfortunately it's not a perfect match — e.g.
|
|
1690
|
+
// shift-tabbing won't immediately cycle up from the end of the page on Chromium
|
|
1691
|
+
// See https://html.spec.whatwg.org/multipage/interaction.html#get-the-focusable-area
|
|
1692
|
+
const root = document.body;
|
|
1693
|
+
const tabindex = root.getAttribute('tabindex');
|
|
1694
|
+
|
|
1695
|
+
root.tabIndex = -1;
|
|
1696
|
+
root.focus({ preventScroll: true });
|
|
1697
|
+
|
|
1698
|
+
setTimeout(() => {
|
|
1699
|
+
getSelection()?.removeAllRanges();
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
// restore `tabindex` as to prevent `root` from stealing input from elements
|
|
1703
|
+
if (tabindex !== null) {
|
|
1704
|
+
root.setAttribute('tabindex', tabindex);
|
|
1705
|
+
} else {
|
|
1706
|
+
root.removeAttribute('tabindex');
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
if (DEV) {
|
|
1712
|
+
// Nasty hack to silence harmless warnings the user can do nothing about
|
|
1713
|
+
const console_warn = console.warn;
|
|
1714
|
+
console.warn = function warn(...args) {
|
|
1715
|
+
if (
|
|
1716
|
+
args.length === 1 &&
|
|
1717
|
+
/<(Layout|Page)(_[\w$]+)?> was created (with unknown|without expected) prop '(data|form)'/.test(
|
|
1718
|
+
args[0]
|
|
1719
|
+
)
|
|
1720
|
+
) {
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
console_warn(...args);
|
|
1724
|
+
};
|
|
1725
|
+
}
|