@sveltejs/kit 1.0.0-next.289 → 1.0.0-next.291

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.
@@ -1,476 +1,10 @@
1
- import Root from '__GENERATED__/root.svelte';
2
- import { fallback, routes } from '__GENERATED__/manifest.js';
3
1
  import { onMount, tick } from 'svelte';
4
- import { g as get_base_uri } from '../chunks/utils.js';
5
2
  import { writable } from 'svelte/store';
6
3
  import { base, set_paths } from '../paths.js';
4
+ import Root from '__GENERATED__/root.svelte';
5
+ import { routes, fallback } from '__GENERATED__/manifest.js';
7
6
  import { init } from './singletons.js';
8
7
 
9
- /**
10
- * @param {string} path
11
- * @param {import('types').TrailingSlash} trailing_slash
12
- */
13
- function normalize_path(path, trailing_slash) {
14
- if (path === '/' || trailing_slash === 'ignore') return path;
15
-
16
- if (trailing_slash === 'never') {
17
- return path.endsWith('/') ? path.slice(0, -1) : path;
18
- } else if (trailing_slash === 'always' && /\/[^./]+$/.test(path)) {
19
- return path + '/';
20
- }
21
-
22
- return path;
23
- }
24
-
25
- // We track the scroll position associated with each history entry in sessionStorage,
26
- // rather than on history.state itself, because when navigation is driven by
27
- // popstate it's too late to update the scroll position associated with the
28
- // state we're navigating from
29
- const SCROLL_KEY = 'sveltekit:scroll';
30
-
31
- /** @typedef {{ x: number, y: number }} ScrollPosition */
32
- /** @type {Record<number, ScrollPosition>} */
33
- let scroll_positions = {};
34
- try {
35
- scroll_positions = JSON.parse(sessionStorage[SCROLL_KEY]);
36
- } catch {
37
- // do nothing
38
- }
39
-
40
- /** @param {number} index */
41
- function update_scroll_positions(index) {
42
- scroll_positions[index] = scroll_state();
43
- }
44
-
45
- function scroll_state() {
46
- return {
47
- x: pageXOffset,
48
- y: pageYOffset
49
- };
50
- }
51
-
52
- /**
53
- * @param {Event} event
54
- * @returns {HTMLAnchorElement | SVGAElement | undefined}
55
- */
56
- function find_anchor(event) {
57
- const node = event
58
- .composedPath()
59
- .find((e) => e instanceof Node && e.nodeName.toUpperCase() === 'A'); // SVG <a> elements have a lowercase name
60
- return /** @type {HTMLAnchorElement | SVGAElement | undefined} */ (node);
61
- }
62
-
63
- /**
64
- * @param {HTMLAnchorElement | SVGAElement} node
65
- * @returns {URL}
66
- */
67
- function get_href(node) {
68
- return node instanceof SVGAElement
69
- ? new URL(node.href.baseVal, document.baseURI)
70
- : new URL(node.href);
71
- }
72
-
73
- class Router {
74
- /**
75
- * @param {{
76
- * base: string;
77
- * routes: import('types').CSRRoute[];
78
- * trailing_slash: import('types').TrailingSlash;
79
- * renderer: import('./renderer').Renderer;
80
- * }} opts
81
- */
82
- constructor({ base, routes, trailing_slash, renderer }) {
83
- this.base = base;
84
- this.routes = routes;
85
- this.trailing_slash = trailing_slash;
86
- /** Keeps tracks of multiple navigations caused by redirects during rendering */
87
- this.navigating = 0;
88
-
89
- /** @type {import('./renderer').Renderer} */
90
- this.renderer = renderer;
91
- renderer.router = this;
92
-
93
- this.enabled = true;
94
- this.initialized = false;
95
-
96
- // keeping track of the history index in order to prevent popstate navigation events if needed
97
- this.current_history_index = history.state?.['sveltekit:index'] ?? 0;
98
-
99
- if (this.current_history_index === 0) {
100
- // create initial history entry, so we can return here
101
- history.replaceState({ ...history.state, 'sveltekit:index': 0 }, '', location.href);
102
- }
103
-
104
- // if we reload the page, or Cmd-Shift-T back to it,
105
- // recover scroll position
106
- const scroll = scroll_positions[this.current_history_index];
107
- if (scroll) scrollTo(scroll.x, scroll.y);
108
-
109
- this.hash_navigating = false;
110
-
111
- this.callbacks = {
112
- /** @type {Array<({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void>} */
113
- before_navigate: [],
114
-
115
- /** @type {Array<({ from, to }: { from: URL | null, to: URL }) => void>} */
116
- after_navigate: []
117
- };
118
- }
119
-
120
- init_listeners() {
121
- history.scrollRestoration = 'manual';
122
-
123
- // Adopted from Nuxt.js
124
- // Reset scrollRestoration to auto when leaving page, allowing page reload
125
- // and back-navigation from other pages to use the browser to restore the
126
- // scrolling position.
127
- addEventListener('beforeunload', (e) => {
128
- let should_block = false;
129
-
130
- const intent = {
131
- from: this.renderer.current.url,
132
- to: null,
133
- cancel: () => (should_block = true)
134
- };
135
-
136
- this.callbacks.before_navigate.forEach((fn) => fn(intent));
137
-
138
- if (should_block) {
139
- e.preventDefault();
140
- e.returnValue = '';
141
- } else {
142
- history.scrollRestoration = 'auto';
143
- }
144
- });
145
-
146
- addEventListener('visibilitychange', () => {
147
- if (document.visibilityState === 'hidden') {
148
- update_scroll_positions(this.current_history_index);
149
-
150
- try {
151
- sessionStorage[SCROLL_KEY] = JSON.stringify(scroll_positions);
152
- } catch {
153
- // do nothing
154
- }
155
- }
156
- });
157
-
158
- /** @param {Event} event */
159
- const trigger_prefetch = (event) => {
160
- const a = find_anchor(event);
161
- if (a && a.href && a.hasAttribute('sveltekit:prefetch')) {
162
- this.prefetch(get_href(a));
163
- }
164
- };
165
-
166
- /** @type {NodeJS.Timeout} */
167
- let mousemove_timeout;
168
-
169
- /** @param {MouseEvent|TouchEvent} event */
170
- const handle_mousemove = (event) => {
171
- clearTimeout(mousemove_timeout);
172
- mousemove_timeout = setTimeout(() => {
173
- // event.composedPath(), which is used in find_anchor, will be empty if the event is read in a timeout
174
- // add a layer of indirection to address that
175
- event.target?.dispatchEvent(
176
- new CustomEvent('sveltekit:trigger_prefetch', { bubbles: true })
177
- );
178
- }, 20);
179
- };
180
-
181
- addEventListener('touchstart', trigger_prefetch);
182
- addEventListener('mousemove', handle_mousemove);
183
- addEventListener('sveltekit:trigger_prefetch', trigger_prefetch);
184
-
185
- /** @param {MouseEvent} event */
186
- addEventListener('click', (event) => {
187
- if (!this.enabled) return;
188
-
189
- // Adapted from https://github.com/visionmedia/page.js
190
- // MIT license https://github.com/visionmedia/page.js#license
191
- if (event.button || event.which !== 1) return;
192
- if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
193
- if (event.defaultPrevented) return;
194
-
195
- const a = find_anchor(event);
196
- if (!a) return;
197
-
198
- if (!a.href) return;
199
-
200
- const is_svg_a_element = a instanceof SVGAElement;
201
- const url = get_href(a);
202
- const url_string = url.toString();
203
- if (url_string === location.href) {
204
- if (!location.hash) event.preventDefault();
205
- return;
206
- }
207
-
208
- // Ignore if url does not have origin (e.g. `mailto:`, `tel:`.)
209
- // MEMO: Without this condition, firefox will open mailer twice.
210
- // See: https://github.com/sveltejs/kit/issues/4045
211
- if (!is_svg_a_element && url.origin === 'null') return;
212
-
213
- // Ignore if tag has
214
- // 1. 'download' attribute
215
- // 2. 'rel' attribute includes external
216
- const rel = (a.getAttribute('rel') || '').split(/\s+/);
217
-
218
- if (a.hasAttribute('download') || (rel && rel.includes('external'))) {
219
- return;
220
- }
221
-
222
- // Ignore if <a> has a target
223
- if (is_svg_a_element ? a.target.baseVal : a.target) return;
224
-
225
- // Check if new url only differs by hash and use the browser default behavior in that case
226
- // This will ensure the `hashchange` event is fired
227
- // Removing the hash does a full page navigation in the browser, so make sure a hash is present
228
- const [base, hash] = url.href.split('#');
229
- if (hash !== undefined && base === location.href.split('#')[0]) {
230
- // set this flag to distinguish between navigations triggered by
231
- // clicking a hash link and those triggered by popstate
232
- this.hash_navigating = true;
233
-
234
- update_scroll_positions(this.current_history_index);
235
- this.renderer.update_page_store(new URL(url.href));
236
-
237
- return;
238
- }
239
-
240
- this._navigate({
241
- url,
242
- scroll: a.hasAttribute('sveltekit:noscroll') ? scroll_state() : null,
243
- keepfocus: false,
244
- chain: [],
245
- details: {
246
- state: {},
247
- replaceState: false
248
- },
249
- accepted: () => event.preventDefault(),
250
- blocked: () => event.preventDefault()
251
- });
252
- });
253
-
254
- addEventListener('popstate', (event) => {
255
- if (event.state && this.enabled) {
256
- // if a popstate-driven navigation is cancelled, we need to counteract it
257
- // with history.go, which means we end up back here, hence this check
258
- if (event.state['sveltekit:index'] === this.current_history_index) return;
259
-
260
- this._navigate({
261
- url: new URL(location.href),
262
- scroll: scroll_positions[event.state['sveltekit:index']],
263
- keepfocus: false,
264
- chain: [],
265
- details: null,
266
- accepted: () => {
267
- this.current_history_index = event.state['sveltekit:index'];
268
- },
269
- blocked: () => {
270
- const delta = this.current_history_index - event.state['sveltekit:index'];
271
- history.go(delta);
272
- }
273
- });
274
- }
275
- });
276
-
277
- addEventListener('hashchange', () => {
278
- // if the hashchange happened as a result of clicking on a link,
279
- // we need to update history, otherwise we have to leave it alone
280
- if (this.hash_navigating) {
281
- this.hash_navigating = false;
282
- history.replaceState(
283
- { ...history.state, 'sveltekit:index': ++this.current_history_index },
284
- '',
285
- location.href
286
- );
287
- }
288
- });
289
-
290
- this.initialized = true;
291
- }
292
-
293
- /**
294
- * Returns true if `url` has the same origin and basepath as the app
295
- * @param {URL} url
296
- */
297
- owns(url) {
298
- return url.origin === location.origin && url.pathname.startsWith(this.base);
299
- }
300
-
301
- /**
302
- * @param {URL} url
303
- * @returns {import('./types').NavigationInfo | undefined}
304
- */
305
- parse(url) {
306
- if (this.owns(url)) {
307
- const path = decodeURI(url.pathname.slice(this.base.length) || '/');
308
-
309
- return {
310
- id: url.pathname + url.search,
311
- routes: this.routes.filter(([pattern]) => pattern.test(path)),
312
- url,
313
- path
314
- };
315
- }
316
- }
317
-
318
- /**
319
- * @typedef {Parameters<typeof import('$app/navigation').goto>} GotoParams
320
- *
321
- * @param {GotoParams[0]} href
322
- * @param {GotoParams[1]} opts
323
- * @param {string[]} chain
324
- */
325
- async goto(
326
- href,
327
- { noscroll = false, replaceState = false, keepfocus = false, state = {} } = {},
328
- chain
329
- ) {
330
- const url = new URL(href, get_base_uri(document));
331
-
332
- if (this.enabled) {
333
- return this._navigate({
334
- url,
335
- scroll: noscroll ? scroll_state() : null,
336
- keepfocus,
337
- chain,
338
- details: {
339
- state,
340
- replaceState
341
- },
342
- accepted: () => {},
343
- blocked: () => {}
344
- });
345
- }
346
-
347
- location.href = url.href;
348
- return new Promise(() => {
349
- /* never resolves */
350
- });
351
- }
352
-
353
- enable() {
354
- this.enabled = true;
355
- }
356
-
357
- disable() {
358
- this.enabled = false;
359
- }
360
-
361
- /**
362
- * @param {URL} url
363
- * @returns {Promise<import('./types').NavigationResult | undefined>}
364
- */
365
- async prefetch(url) {
366
- const info = this.parse(url);
367
-
368
- if (!info) {
369
- throw new Error('Attempted to prefetch a URL that does not belong to this app');
370
- }
371
-
372
- return this.renderer.load(info);
373
- }
374
-
375
- /** @param {({ from, to }: { from: URL | null, to: URL }) => void} fn */
376
- after_navigate(fn) {
377
- onMount(() => {
378
- this.callbacks.after_navigate.push(fn);
379
-
380
- return () => {
381
- const i = this.callbacks.after_navigate.indexOf(fn);
382
- this.callbacks.after_navigate.splice(i, 1);
383
- };
384
- });
385
- }
386
-
387
- /**
388
- * @param {({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void} fn
389
- */
390
- before_navigate(fn) {
391
- onMount(() => {
392
- this.callbacks.before_navigate.push(fn);
393
-
394
- return () => {
395
- const i = this.callbacks.before_navigate.indexOf(fn);
396
- this.callbacks.before_navigate.splice(i, 1);
397
- };
398
- });
399
- }
400
-
401
- /**
402
- * @param {{
403
- * url: URL;
404
- * scroll: { x: number, y: number } | null;
405
- * keepfocus: boolean;
406
- * chain: string[];
407
- * details: {
408
- * replaceState: boolean;
409
- * state: any;
410
- * } | null;
411
- * accepted: () => void;
412
- * blocked: () => void;
413
- * }} opts
414
- */
415
- async _navigate({ url, scroll, keepfocus, chain, details, accepted, blocked }) {
416
- const from = this.renderer.current.url;
417
- let should_block = false;
418
-
419
- const intent = {
420
- from,
421
- to: url,
422
- cancel: () => (should_block = true)
423
- };
424
-
425
- this.callbacks.before_navigate.forEach((fn) => fn(intent));
426
-
427
- if (should_block) {
428
- blocked();
429
- return;
430
- }
431
-
432
- const info = this.parse(url);
433
- if (!info) {
434
- location.href = url.href;
435
- return new Promise(() => {
436
- // never resolves
437
- });
438
- }
439
-
440
- update_scroll_positions(this.current_history_index);
441
-
442
- accepted();
443
-
444
- this.navigating++;
445
-
446
- const pathname = normalize_path(url.pathname, this.trailing_slash);
447
-
448
- info.url = new URL(url.origin + pathname + url.search + url.hash);
449
-
450
- const token = (this.navigating_token = {});
451
-
452
- await this.renderer.handle_navigation(info, chain, false, {
453
- scroll,
454
- keepfocus
455
- });
456
-
457
- this.navigating--;
458
-
459
- // navigation was aborted
460
- if (this.navigating_token !== token) return;
461
- if (!this.navigating) {
462
- const navigation = { from, to: url };
463
- this.callbacks.after_navigate.forEach((fn) => fn(navigation));
464
- }
465
-
466
- if (details) {
467
- const change = details.replaceState ? 0 : 1;
468
- details.state['sveltekit:index'] = this.current_history_index += change;
469
- history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', info.url);
470
- }
471
- }
472
- }
473
-
474
8
  /**
475
9
  * @param {unknown} err
476
10
  * @return {Error}
@@ -482,23 +16,6 @@ function coalesce_to_error(err) {
482
16
  : new Error(JSON.stringify(err));
483
17
  }
484
18
 
485
- /**
486
- * Hash using djb2
487
- * @param {import('types').StrictBody} value
488
- */
489
- function hash(value) {
490
- let hash = 5381;
491
- let i = value.length;
492
-
493
- if (typeof value === 'string') {
494
- while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
495
- } else {
496
- while (i) hash = (hash * 33) ^ value[--i];
497
- }
498
-
499
- return (hash >>> 0).toString(36);
500
- }
501
-
502
19
  /**
503
20
  * @param {import('types').LoadOutput} loaded
504
21
  * @returns {import('types').NormalizedLoadOutput}
@@ -565,9 +82,71 @@ function normalize(loaded) {
565
82
  }
566
83
 
567
84
  /**
568
- * @typedef {import('types').CSRComponent} CSRComponent
569
- * @typedef {{ from: URL; to: URL }} Navigating
85
+ * @param {string} path
86
+ * @param {import('types').TrailingSlash} trailing_slash
570
87
  */
88
+ function normalize_path(path, trailing_slash) {
89
+ if (path === '/' || trailing_slash === 'ignore') return path;
90
+
91
+ if (trailing_slash === 'never') {
92
+ return path.endsWith('/') ? path.slice(0, -1) : path;
93
+ } else if (trailing_slash === 'always' && /\/[^./]+$/.test(path)) {
94
+ return path + '/';
95
+ }
96
+
97
+ return path;
98
+ }
99
+
100
+ /**
101
+ * Hash using djb2
102
+ * @param {import('types').StrictBody} value
103
+ */
104
+ function hash(value) {
105
+ let hash = 5381;
106
+ let i = value.length;
107
+
108
+ if (typeof value === 'string') {
109
+ while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
110
+ } else {
111
+ while (i) hash = (hash * 33) ^ value[--i];
112
+ }
113
+
114
+ return (hash >>> 0).toString(36);
115
+ }
116
+
117
+ /** @param {HTMLDocument} doc */
118
+ function get_base_uri(doc) {
119
+ let baseURI = doc.baseURI;
120
+
121
+ if (!baseURI) {
122
+ const baseTags = doc.getElementsByTagName('base');
123
+ baseURI = baseTags.length ? baseTags[0].href : doc.URL;
124
+ }
125
+
126
+ return baseURI;
127
+ }
128
+
129
+ function scroll_state() {
130
+ return {
131
+ x: pageXOffset,
132
+ y: pageYOffset
133
+ };
134
+ }
135
+
136
+ /** @param {Event} event */
137
+ function find_anchor(event) {
138
+ const node = event
139
+ .composedPath()
140
+ .find((e) => e instanceof Node && e.nodeName.toUpperCase() === 'A'); // SVG <a> elements have a lowercase name
141
+ return /** @type {HTMLAnchorElement | SVGAElement | undefined} */ (node);
142
+ }
143
+
144
+ /** @param {HTMLAnchorElement | SVGAElement} node */
145
+ function get_href(node) {
146
+ return node instanceof SVGAElement
147
+ ? new URL(node.href.baseVal, document.baseURI)
148
+ : new URL(node.href);
149
+ }
571
150
 
572
151
  /** @param {any} value */
573
152
  function notifiable_store(value) {
@@ -671,264 +250,240 @@ function initial_fetch(resource, opts) {
671
250
  return fetch(resource, opts);
672
251
  }
673
252
 
674
- class Renderer {
675
- /**
676
- * @param {{
677
- * Root: CSRComponent;
678
- * fallback: [CSRComponent, CSRComponent];
679
- * target: Node;
680
- * session: any;
681
- * }} opts
682
- */
683
- constructor({ Root, fallback, target, session }) {
684
- this.Root = Root;
685
- this.fallback = fallback;
686
-
687
- /** @type {import('./router').Router | undefined} */
688
- this.router;
689
-
690
- this.target = target;
691
-
692
- this.started = false;
693
-
694
- this.session_id = 1;
695
- this.invalid = new Set();
696
- this.invalidating = null;
697
- this.autoscroll = true;
698
- this.updating = false;
699
-
700
- /** @type {import('./types').NavigationState} */
701
- this.current = {
702
- // @ts-ignore - we need the initial value to be null
703
- url: null,
704
- session_id: 0,
705
- branch: []
706
- };
707
-
708
- /** @type {Map<string, import('./types').NavigationResult>} */
709
- this.cache = new Map();
253
+ const SCROLL_KEY = 'sveltekit:scroll';
254
+ const INDEX_KEY = 'sveltekit:index';
710
255
 
711
- /** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
712
- this.loading = {
713
- id: null,
714
- promise: null
715
- };
256
+ // We track the scroll position associated with each history entry in sessionStorage,
257
+ // rather than on history.state itself, because when navigation is driven by
258
+ // popstate it's too late to update the scroll position associated with the
259
+ // state we're navigating from
716
260
 
717
- this.stores = {
718
- url: notifiable_store({}),
719
- page: notifiable_store({}),
720
- navigating: writable(/** @type {Navigating | null} */ (null)),
721
- session: writable(session),
722
- updated: create_updated_store()
723
- };
261
+ /** @typedef {{ x: number, y: number }} ScrollPosition */
262
+ /** @type {Record<number, ScrollPosition>} */
263
+ let scroll_positions = {};
264
+ try {
265
+ scroll_positions = JSON.parse(sessionStorage[SCROLL_KEY]);
266
+ } catch {
267
+ // do nothing
268
+ }
724
269
 
725
- this.$session = null;
270
+ /** @param {number} index */
271
+ function update_scroll_positions(index) {
272
+ scroll_positions[index] = scroll_state();
273
+ }
726
274
 
727
- this.root = null;
275
+ /**
276
+ * @param {{
277
+ * target: Element;
278
+ * session: App.Session;
279
+ * base: string;
280
+ * trailing_slash: import('types').TrailingSlash;
281
+ * }} opts
282
+ * @returns {import('./types').Client}
283
+ */
284
+ function create_client({ target, session, base, trailing_slash }) {
285
+ /** @type {Map<string, import('./types').NavigationResult>} */
286
+ const cache = new Map();
287
+
288
+ /** @type {Set<string>} */
289
+ const invalidated = new Set();
290
+
291
+ const stores = {
292
+ url: notifiable_store({}),
293
+ page: notifiable_store({}),
294
+ navigating: writable(/** @type {import('types').Navigation | null} */ (null)),
295
+ session: writable(session),
296
+ updated: create_updated_store()
297
+ };
728
298
 
729
- let ready = false;
730
- this.stores.session.subscribe(async (value) => {
731
- this.$session = value;
299
+ /** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
300
+ const load_cache = {
301
+ id: null,
302
+ promise: null
303
+ };
732
304
 
733
- if (!ready || !this.router) return;
734
- this.session_id += 1;
305
+ const callbacks = {
306
+ /** @type {Array<(opts: { from: URL, to: URL | null, cancel: () => void }) => void>} */
307
+ before_navigate: [],
735
308
 
736
- const info = this.router.parse(new URL(location.href));
737
- if (info) this.update(info, [], true);
738
- });
739
- ready = true;
740
- }
309
+ /** @type {Array<(opts: { from: URL | null, to: URL }) => void>} */
310
+ after_navigate: []
311
+ };
741
312
 
742
- disable_scroll_handling() {
743
- if (import.meta.env.DEV && this.started && !this.updating) {
744
- throw new Error('Can only disable scroll handling during navigation');
745
- }
313
+ /** @type {import('./types').NavigationState} */
314
+ let current = {
315
+ // @ts-ignore - we need the initial value to be null
316
+ url: null,
317
+ session_id: 0,
318
+ branch: []
319
+ };
746
320
 
747
- if (this.updating || !this.started) {
748
- this.autoscroll = false;
749
- }
750
- }
321
+ let started = false;
322
+ let autoscroll = true;
323
+ let updating = false;
324
+ let session_id = 1;
751
325
 
752
- /**
753
- * @param {{
754
- * status: number;
755
- * error: Error;
756
- * nodes: Array<Promise<CSRComponent>>;
757
- * params: Record<string, string>;
758
- * }} selected
759
- */
760
- async start({ status, error, nodes, params }) {
761
- const url = new URL(location.href);
326
+ /** @type {Promise<void> | null} */
327
+ let invalidating = null;
762
328
 
763
- /** @type {Array<import('./types').BranchNode | undefined>} */
764
- const branch = [];
329
+ /** @type {import('svelte').SvelteComponent} */
330
+ let root;
765
331
 
766
- /** @type {Record<string, any>} */
767
- let stuff = {};
332
+ /** @type {App.Session} */
333
+ let $session;
768
334
 
769
- /** @type {import('./types').NavigationResult | undefined} */
770
- let result;
335
+ let ready = false;
336
+ stores.session.subscribe(async (value) => {
337
+ $session = value;
771
338
 
772
- let error_args;
339
+ if (!ready) return;
340
+ session_id += 1;
773
341
 
774
- try {
775
- for (let i = 0; i < nodes.length; i += 1) {
776
- const is_leaf = i === nodes.length - 1;
342
+ const intent = get_navigation_intent(new URL(location.href));
343
+ update(intent, [], true);
344
+ });
345
+ ready = true;
777
346
 
778
- let props;
347
+ /** Keeps tracks of multiple navigations caused by redirects during rendering */
348
+ let navigating = 0;
779
349
 
780
- if (is_leaf) {
781
- const serialized = document.querySelector('script[sveltekit\\:data-type="props"]');
782
- if (serialized) {
783
- props = JSON.parse(/** @type {string} */ (serialized.textContent));
784
- }
785
- }
350
+ let router_enabled = true;
786
351
 
787
- const node = await this._load_node({
788
- module: await nodes[i],
789
- url,
790
- params,
791
- stuff,
792
- status: is_leaf ? status : undefined,
793
- error: is_leaf ? error : undefined,
794
- props
795
- });
352
+ // keeping track of the history index in order to prevent popstate navigation events if needed
353
+ let current_history_index = history.state?.[INDEX_KEY] ?? 0;
796
354
 
797
- if (props) {
798
- node.uses.dependencies.add(url.href);
799
- node.uses.url = true;
800
- }
355
+ if (current_history_index === 0) {
356
+ // create initial history entry, so we can return here
357
+ history.replaceState({ ...history.state, [INDEX_KEY]: 0 }, '', location.href);
358
+ }
801
359
 
802
- branch.push(node);
360
+ // if we reload the page, or Cmd-Shift-T back to it,
361
+ // recover scroll position
362
+ const scroll = scroll_positions[current_history_index];
363
+ if (scroll) scrollTo(scroll.x, scroll.y);
803
364
 
804
- if (node && node.loaded) {
805
- if (node.loaded.error) {
806
- if (error) throw node.loaded.error;
807
- error_args = {
808
- status: node.loaded.status,
809
- error: node.loaded.error,
810
- url
811
- };
812
- } else if (node.loaded.stuff) {
813
- stuff = {
814
- ...stuff,
815
- ...node.loaded.stuff
816
- };
817
- }
818
- }
819
- }
365
+ let hash_navigating = false;
820
366
 
821
- result = error_args
822
- ? await this._load_error(error_args)
823
- : await this._get_navigation_result_from_branch({
824
- url,
825
- params,
826
- stuff,
827
- branch,
828
- status,
829
- error
830
- });
831
- } catch (e) {
832
- if (error) throw e;
833
-
834
- result = await this._load_error({
835
- status: 500,
836
- error: coalesce_to_error(e),
837
- url
838
- });
839
- }
367
+ /** @type {import('types').Page} */
368
+ let page;
840
369
 
841
- if (result.redirect) {
842
- // this is a real edge case — `load` would need to return
843
- // a redirect but only in the browser
844
- location.href = new URL(result.redirect, location.href).href;
845
- return;
846
- }
370
+ /** @type {{}} */
371
+ let token;
847
372
 
848
- this._init(result);
849
- }
373
+ /** @type {{}} */
374
+ let navigating_token;
850
375
 
851
376
  /**
852
- * @param {import('./types').NavigationInfo} info
853
- * @param {string[]} chain
854
- * @param {boolean} no_cache
855
- * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
377
+ * @param {string} href
378
+ * @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts
379
+ * @param {string[]} redirect_chain
856
380
  */
857
- async handle_navigation(info, chain, no_cache, opts) {
858
- if (this.started) {
859
- this.stores.navigating.set({
860
- from: this.current.url,
861
- to: info.url
381
+ async function goto(
382
+ href,
383
+ { noscroll = false, replaceState = false, keepfocus = false, state = {} },
384
+ redirect_chain
385
+ ) {
386
+ const url = new URL(href, get_base_uri(document));
387
+
388
+ if (router_enabled) {
389
+ return navigate({
390
+ url,
391
+ scroll: noscroll ? scroll_state() : null,
392
+ keepfocus,
393
+ redirect_chain,
394
+ details: {
395
+ state,
396
+ replaceState
397
+ },
398
+ accepted: () => {},
399
+ blocked: () => {}
862
400
  });
863
401
  }
864
402
 
865
- await this.update(info, chain, no_cache, opts);
403
+ await native_navigation(url);
404
+ }
405
+
406
+ /** @param {URL} url */
407
+ async function prefetch(url) {
408
+ if (!owns(url)) {
409
+ throw new Error('Attempted to prefetch a URL that does not belong to this app');
410
+ }
411
+
412
+ const intent = get_navigation_intent(url);
413
+
414
+ load_cache.promise = get_navigation_result(intent, false);
415
+ load_cache.id = intent.id;
416
+
417
+ return load_cache.promise;
866
418
  }
867
419
 
868
420
  /**
869
- * @param {import('./types').NavigationInfo} info
870
- * @param {string[]} chain
421
+ * @param {import('./types').NavigationIntent} intent
422
+ * @param {string[]} redirect_chain
871
423
  * @param {boolean} no_cache
872
424
  * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
873
425
  */
874
- async update(info, chain, no_cache, opts) {
875
- const token = (this.token = {});
876
- let navigation_result = await this._get_navigation_result(info, no_cache);
877
-
878
- if (!navigation_result && info.url.pathname === location.pathname) {
879
- navigation_result = await this._load_error({
426
+ async function update(intent, redirect_chain, no_cache, opts) {
427
+ const current_token = (token = {});
428
+ let navigation_result = await get_navigation_result(intent, no_cache);
429
+
430
+ if (!navigation_result && intent.url.pathname === location.pathname) {
431
+ // this could happen in SPA fallback mode if the user navigated to
432
+ // `/non-existent-page`. if we fall back to reloading the page, it
433
+ // will create an infinite loop. so whereas we normally handle
434
+ // unknown routes by going to the server, in this special case
435
+ // we render a client-side error page instead
436
+ navigation_result = await load_root_error_page({
880
437
  status: 404,
881
- error: new Error(`Not found: ${info.url.pathname}`),
882
- url: info.url
438
+ error: new Error(`Not found: ${intent.url.pathname}`),
439
+ url: intent.url
883
440
  });
884
441
  }
885
442
 
886
443
  if (!navigation_result) {
887
- location.href = info.url.href;
888
- return;
444
+ await native_navigation(intent.url);
445
+ return; // unnecessary, but TypeScript prefers it this way
889
446
  }
890
447
 
891
448
  // abort if user navigated during update
892
- if (token !== this.token) return;
449
+ if (token !== current_token) return;
893
450
 
894
- this.invalid.clear();
451
+ invalidated.clear();
895
452
 
896
453
  if (navigation_result.redirect) {
897
- if (chain.length > 10 || chain.includes(info.url.pathname)) {
898
- navigation_result = await this._load_error({
454
+ if (redirect_chain.length > 10 || redirect_chain.includes(intent.url.pathname)) {
455
+ navigation_result = await load_root_error_page({
899
456
  status: 500,
900
457
  error: new Error('Redirect loop'),
901
- url: info.url
458
+ url: intent.url
902
459
  });
903
460
  } else {
904
- if (this.router) {
905
- this.router.goto(new URL(navigation_result.redirect, info.url).href, {}, [
906
- ...chain,
907
- info.url.pathname
461
+ if (router_enabled) {
462
+ goto(new URL(navigation_result.redirect, intent.url).href, {}, [
463
+ ...redirect_chain,
464
+ intent.url.pathname
908
465
  ]);
909
466
  } else {
910
- location.href = new URL(navigation_result.redirect, location.href).href;
467
+ await native_navigation(new URL(navigation_result.redirect, location.href));
911
468
  }
912
469
 
913
470
  return;
914
471
  }
915
472
  } else if (navigation_result.props?.page?.status >= 400) {
916
- const updated = await this.stores.updated.check();
473
+ const updated = await stores.updated.check();
917
474
  if (updated) {
918
- location.href = info.url.href;
919
- return;
475
+ await native_navigation(intent.url);
920
476
  }
921
477
  }
922
478
 
923
- this.updating = true;
479
+ updating = true;
924
480
 
925
- if (this.started) {
926
- this.current = navigation_result.state;
481
+ if (started) {
482
+ current = navigation_result.state;
927
483
 
928
- this.root.$set(navigation_result.props);
929
- this.stores.navigating.set(null);
484
+ root.$set(navigation_result.props);
930
485
  } else {
931
- this._init(navigation_result);
486
+ initialize(navigation_result);
932
487
  }
933
488
 
934
489
  // opts must be passed if we're navigating
@@ -937,18 +492,18 @@ class Renderer {
937
492
 
938
493
  if (!keepfocus) {
939
494
  // Reset page selection and focus
940
- // We try to mimick browsers' behaviour as closely as possible by targeting the
941
- // viewport, but unfortunately it's not a perfect match — e.g. shift-tabbing won't
942
- // immediately cycle from the end of the page
495
+ // We try to mimic browsers' behaviour as closely as possible by targeting the
496
+ // first scrollable region, but unfortunately it's not a perfect match — e.g.
497
+ // shift-tabbing won't immediately cycle up from the end of the page on Chromium
943
498
  // See https://html.spec.whatwg.org/multipage/interaction.html#get-the-focusable-area
944
- const root = document.documentElement;
499
+ const root = document.body;
945
500
  const tabindex = root.getAttribute('tabindex');
946
501
 
947
502
  getSelection()?.removeAllRanges();
948
503
  root.tabIndex = -1;
949
504
  root.focus();
950
505
 
951
- // restore `tabindex` as to prevent the document from stealing input from elements
506
+ // restore `tabindex` as to prevent `root` from stealing input from elements
952
507
  if (tabindex !== null) {
953
508
  root.setAttribute('tabindex', tabindex);
954
509
  } else {
@@ -959,8 +514,8 @@ class Renderer {
959
514
  // need to render the DOM before we can scroll to the rendered elements
960
515
  await tick();
961
516
 
962
- if (this.autoscroll) {
963
- const deep_linked = info.url.hash && document.getElementById(info.url.hash.slice(1));
517
+ if (autoscroll) {
518
+ const deep_linked = intent.url.hash && document.getElementById(intent.url.hash.slice(1));
964
519
  if (scroll) {
965
520
  scrollTo(scroll.x, scroll.y);
966
521
  } else if (deep_linked) {
@@ -977,102 +532,59 @@ class Renderer {
977
532
  await tick();
978
533
  }
979
534
 
980
- this.loading.promise = null;
981
- this.loading.id = null;
982
- this.autoscroll = true;
983
- this.updating = false;
535
+ load_cache.promise = null;
536
+ load_cache.id = null;
537
+ autoscroll = true;
538
+ updating = false;
984
539
 
985
540
  if (navigation_result.props.page) {
986
- this.page = navigation_result.props.page;
541
+ page = navigation_result.props.page;
987
542
  }
988
543
 
989
- if (!this.router) return;
990
-
991
544
  const leaf_node = navigation_result.state.branch[navigation_result.state.branch.length - 1];
992
- if (leaf_node && leaf_node.module.router === false) {
993
- this.router.disable();
994
- } else {
995
- this.router.enable();
996
- }
997
- }
998
-
999
- /**
1000
- * @param {import('./types').NavigationInfo} info
1001
- * @returns {Promise<import('./types').NavigationResult | undefined>}
1002
- */
1003
- load(info) {
1004
- this.loading.promise = this._get_navigation_result(info, false);
1005
- this.loading.id = info.id;
1006
-
1007
- return this.loading.promise;
1008
- }
1009
-
1010
- /** @param {string} href */
1011
- invalidate(href) {
1012
- this.invalid.add(href);
1013
-
1014
- if (!this.invalidating) {
1015
- this.invalidating = Promise.resolve().then(async () => {
1016
- const info = this.router && this.router.parse(new URL(location.href));
1017
- if (info) await this.update(info, [], true);
1018
-
1019
- this.invalidating = null;
1020
- });
1021
- }
1022
-
1023
- return this.invalidating;
1024
- }
1025
-
1026
- /** @param {URL} url */
1027
- update_page_store(url) {
1028
- this.stores.page.set({ ...this.page, url });
1029
- this.stores.page.notify();
545
+ router_enabled = leaf_node?.module.router !== false;
1030
546
  }
1031
547
 
1032
548
  /** @param {import('./types').NavigationResult} result */
1033
- _init(result) {
1034
- this.current = result.state;
549
+ function initialize(result) {
550
+ current = result.state;
1035
551
 
1036
552
  const style = document.querySelector('style[data-svelte]');
1037
553
  if (style) style.remove();
1038
554
 
1039
- this.page = result.props.page;
555
+ page = result.props.page;
1040
556
 
1041
- this.root = new this.Root({
1042
- target: this.target,
1043
- props: {
1044
- stores: this.stores,
1045
- ...result.props
1046
- },
557
+ root = new Root({
558
+ target,
559
+ props: { ...result.props, stores },
1047
560
  hydrate: true
1048
561
  });
1049
562
 
1050
- this.started = true;
563
+ started = true;
1051
564
 
1052
- if (this.router) {
565
+ if (router_enabled) {
1053
566
  const navigation = { from: null, to: new URL(location.href) };
1054
- this.router.callbacks.after_navigate.forEach((fn) => fn(navigation));
567
+ callbacks.after_navigate.forEach((fn) => fn(navigation));
1055
568
  }
1056
569
  }
1057
570
 
1058
571
  /**
1059
- * @param {import('./types').NavigationInfo} info
572
+ * @param {import('./types').NavigationIntent} intent
1060
573
  * @param {boolean} no_cache
1061
- * @returns {Promise<import('./types').NavigationResult | undefined>}
1062
574
  */
1063
- async _get_navigation_result(info, no_cache) {
1064
- if (this.loading.id === info.id && this.loading.promise) {
1065
- return this.loading.promise;
575
+ async function get_navigation_result(intent, no_cache) {
576
+ if (load_cache.id === intent.id && load_cache.promise) {
577
+ return load_cache.promise;
1066
578
  }
1067
579
 
1068
- for (let i = 0; i < info.routes.length; i += 1) {
1069
- const route = info.routes[i];
580
+ for (let i = 0; i < intent.routes.length; i += 1) {
581
+ const route = intent.routes[i];
1070
582
 
1071
583
  // load code for subsequent routes immediately, if they are as
1072
584
  // likely to match the current path/query as the current one
1073
585
  let j = i + 1;
1074
- while (j < info.routes.length) {
1075
- const next = info.routes[j];
586
+ while (j < intent.routes.length) {
587
+ const next = intent.routes[j];
1076
588
  if (next[0].toString() === route[0].toString()) {
1077
589
  next[1].forEach((loader) => loader());
1078
590
  j += 1;
@@ -1081,13 +593,7 @@ class Renderer {
1081
593
  }
1082
594
  }
1083
595
 
1084
- const result = await this._load(
1085
- {
1086
- route,
1087
- info
1088
- },
1089
- no_cache
1090
- );
596
+ const result = await load_route(route, intent, no_cache);
1091
597
  if (result) return result;
1092
598
  }
1093
599
  }
@@ -1103,18 +609,18 @@ class Renderer {
1103
609
  * error?: Error;
1104
610
  * }} opts
1105
611
  */
1106
- async _get_navigation_result_from_branch({ url, params, stuff, branch, status, error }) {
612
+ async function get_navigation_result_from_branch({ url, params, stuff, branch, status, error }) {
1107
613
  const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
1108
- const redirect = filtered.find((f) => f.loaded && f.loaded.redirect);
614
+ const redirect = filtered.find((f) => f.loaded?.redirect);
1109
615
 
1110
616
  /** @type {import('./types').NavigationResult} */
1111
617
  const result = {
1112
- redirect: redirect && redirect.loaded ? redirect.loaded.redirect : undefined,
618
+ redirect: redirect?.loaded?.redirect,
1113
619
  state: {
1114
620
  url,
1115
621
  params,
1116
622
  branch,
1117
- session_id: this.session_id
623
+ session_id
1118
624
  },
1119
625
  props: {
1120
626
  components: filtered.map((node) => node.module.default)
@@ -1126,7 +632,7 @@ class Renderer {
1126
632
  result.props[`props_${i}`] = loaded ? await loaded.props : null;
1127
633
  }
1128
634
 
1129
- if (!this.current.url || url.href !== this.current.url.href) {
635
+ if (!current.url || url.href !== current.url.href) {
1130
636
  result.props.page = { url, params, status, error, stuff };
1131
637
 
1132
638
  // TODO remove this for 1.0
@@ -1155,8 +661,8 @@ class Renderer {
1155
661
  let ready = false;
1156
662
 
1157
663
  const clear = () => {
1158
- if (this.cache.get(key) === result) {
1159
- this.cache.delete(key);
664
+ if (cache.get(key) === result) {
665
+ cache.delete(key);
1160
666
  }
1161
667
 
1162
668
  unsubscribe();
@@ -1165,13 +671,13 @@ class Renderer {
1165
671
 
1166
672
  const timeout = setTimeout(clear, maxage * 1000);
1167
673
 
1168
- const unsubscribe = this.stores.session.subscribe(() => {
674
+ const unsubscribe = stores.session.subscribe(() => {
1169
675
  if (ready) clear();
1170
676
  });
1171
677
 
1172
678
  ready = true;
1173
679
 
1174
- this.cache.set(key, result);
680
+ cache.set(key, result);
1175
681
  }
1176
682
 
1177
683
  return result;
@@ -1181,15 +687,14 @@ class Renderer {
1181
687
  * @param {{
1182
688
  * status?: number;
1183
689
  * error?: Error;
1184
- * module: CSRComponent;
690
+ * module: import('types').CSRComponent;
1185
691
  * url: URL;
1186
692
  * params: Record<string, string>;
1187
693
  * stuff: Record<string, any>;
1188
694
  * props?: Record<string, any>;
1189
695
  * }} options
1190
- * @returns
1191
696
  */
1192
- async _load_node({ status, error, module, url, params, stuff, props }) {
697
+ async function load_node({ status, error, module, url, params, stuff, props }) {
1193
698
  /** @type {import('./types').BranchNode} */
1194
699
  const node = {
1195
700
  module,
@@ -1221,11 +726,9 @@ class Renderer {
1221
726
  });
1222
727
  }
1223
728
 
1224
- const session = this.$session;
729
+ const session = $session;
1225
730
 
1226
731
  if (module.load) {
1227
- const { started } = this;
1228
-
1229
732
  /** @type {import('types').LoadInput | import('types').ErrorLoadInput} */
1230
733
  const load_input = {
1231
734
  params: uses_params,
@@ -1281,15 +784,13 @@ class Renderer {
1281
784
  }
1282
785
 
1283
786
  /**
1284
- * @param {import('./types').NavigationCandidate} selected
787
+ * @param {import('types').CSRRoute} route
788
+ * @param {import('./types').NavigationIntent} intent
1285
789
  * @param {boolean} no_cache
1286
- * @returns {Promise<import('./types').NavigationResult | undefined>} undefined if fallthrough
1287
790
  */
1288
- async _load({ route, info: { url, path } }, no_cache) {
1289
- const key = url.pathname + url.search;
1290
-
791
+ async function load_route(route, { id, url, path }, no_cache) {
1291
792
  if (!no_cache) {
1292
- const cached = this.cache.get(key);
793
+ const cached = cache.get(id);
1293
794
  if (cached) return cached;
1294
795
  }
1295
796
 
@@ -1299,10 +800,10 @@ class Renderer {
1299
800
  get_params(/** @type {RegExpExecArray} */ (pattern.exec(path)))
1300
801
  : {};
1301
802
 
1302
- const changed = this.current.url && {
1303
- url: key !== this.current.url.pathname + this.current.url.search,
1304
- params: Object.keys(params).filter((key) => this.current.params[key] !== params[key]),
1305
- session: this.session_id !== this.current.session_id
803
+ const changed = current.url && {
804
+ url: id !== current.url.pathname + current.url.search,
805
+ params: Object.keys(params).filter((key) => current.params[key] !== params[key]),
806
+ session: session_id !== current.session_id
1306
807
  };
1307
808
 
1308
809
  /** @type {Array<import('./types').BranchNode | undefined>} */
@@ -1329,7 +830,7 @@ class Renderer {
1329
830
  if (!a[i]) continue;
1330
831
 
1331
832
  const module = await a[i]();
1332
- const previous = this.current.branch[i];
833
+ const previous = current.branch[i];
1333
834
 
1334
835
  const changed_since_last_render =
1335
836
  !previous ||
@@ -1337,7 +838,7 @@ class Renderer {
1337
838
  (changed.url && previous.uses.url) ||
1338
839
  changed.params.some((param) => previous.uses.params.has(param)) ||
1339
840
  (changed.session && previous.uses.session) ||
1340
- Array.from(previous.uses.dependencies).some((dep) => this.invalid.has(dep)) ||
841
+ Array.from(previous.uses.dependencies).some((dep) => invalidated.has(dep)) ||
1341
842
  (stuff_changed && previous.uses.stuff);
1342
843
 
1343
844
  if (changed_since_last_render) {
@@ -1363,7 +864,7 @@ class Renderer {
1363
864
  return {
1364
865
  redirect,
1365
866
  props: {},
1366
- state: this.current
867
+ state: current
1367
868
  };
1368
869
  }
1369
870
 
@@ -1375,7 +876,7 @@ class Renderer {
1375
876
  }
1376
877
 
1377
878
  if (!error) {
1378
- node = await this._load_node({
879
+ node = await load_node({
1379
880
  module,
1380
881
  url,
1381
882
  params,
@@ -1402,7 +903,7 @@ class Renderer {
1402
903
  return {
1403
904
  redirect: node.loaded.redirect,
1404
905
  props: {},
1405
- state: this.current
906
+ state: current
1406
907
  };
1407
908
  }
1408
909
 
@@ -1432,7 +933,7 @@ class Renderer {
1432
933
  }
1433
934
 
1434
935
  try {
1435
- error_loaded = await this._load_node({
936
+ error_loaded = await load_node({
1436
937
  status,
1437
938
  error,
1438
939
  module: await b[i](),
@@ -1441,11 +942,11 @@ class Renderer {
1441
942
  stuff: node_loaded.stuff
1442
943
  });
1443
944
 
1444
- if (error_loaded && error_loaded.loaded && error_loaded.loaded.error) {
945
+ if (error_loaded?.loaded?.error) {
1445
946
  continue;
1446
947
  }
1447
948
 
1448
- if (error_loaded && error_loaded.loaded && error_loaded.loaded.stuff) {
949
+ if (error_loaded?.loaded?.stuff) {
1449
950
  stuff = {
1450
951
  ...stuff,
1451
952
  ...error_loaded.loaded.stuff
@@ -1460,13 +961,13 @@ class Renderer {
1460
961
  }
1461
962
  }
1462
963
 
1463
- return await this._load_error({
964
+ return await load_root_error_page({
1464
965
  status,
1465
966
  error,
1466
967
  url
1467
968
  });
1468
969
  } else {
1469
- if (node && node.loaded && node.loaded.stuff) {
970
+ if (node?.loaded?.stuff) {
1470
971
  stuff = {
1471
972
  ...stuff,
1472
973
  ...node.loaded.stuff
@@ -1477,7 +978,7 @@ class Renderer {
1477
978
  }
1478
979
  }
1479
980
 
1480
- return await this._get_navigation_result_from_branch({
981
+ return await get_navigation_result_from_branch({
1481
982
  url,
1482
983
  params,
1483
984
  stuff,
@@ -1494,37 +995,480 @@ class Renderer {
1494
995
  * url: URL;
1495
996
  * }} opts
1496
997
  */
1497
- async _load_error({ status, error, url }) {
998
+ async function load_root_error_page({ status, error, url }) {
1498
999
  /** @type {Record<string, string>} */
1499
1000
  const params = {}; // error page does not have params
1500
1001
 
1501
- const node = await this._load_node({
1502
- module: await this.fallback[0],
1002
+ const root_layout = await load_node({
1003
+ module: await fallback[0],
1503
1004
  url,
1504
1005
  params,
1505
1006
  stuff: {}
1506
1007
  });
1507
- const error_node = await this._load_node({
1008
+
1009
+ const root_error = await load_node({
1508
1010
  status,
1509
1011
  error,
1510
- module: await this.fallback[1],
1012
+ module: await fallback[1],
1511
1013
  url,
1512
1014
  params,
1513
- stuff: (node && node.loaded && node.loaded.stuff) || {}
1015
+ stuff: (root_layout && root_layout.loaded && root_layout.loaded.stuff) || {}
1514
1016
  });
1515
1017
 
1516
- const branch = [node, error_node];
1517
- const stuff = { ...node?.loaded?.stuff, ...error_node?.loaded?.stuff };
1518
-
1519
- return await this._get_navigation_result_from_branch({
1018
+ return await get_navigation_result_from_branch({
1520
1019
  url,
1521
1020
  params,
1522
- stuff,
1523
- branch,
1021
+ stuff: {
1022
+ ...root_layout?.loaded?.stuff,
1023
+ ...root_error?.loaded?.stuff
1024
+ },
1025
+ branch: [root_layout, root_error],
1524
1026
  status,
1525
1027
  error
1526
1028
  });
1527
1029
  }
1030
+
1031
+ /** @param {URL} url */
1032
+ function owns(url) {
1033
+ return url.origin === location.origin && url.pathname.startsWith(base);
1034
+ }
1035
+
1036
+ /** @param {URL} url */
1037
+ function get_navigation_intent(url) {
1038
+ const path = decodeURI(url.pathname.slice(base.length) || '/');
1039
+
1040
+ /** @type {import('./types').NavigationIntent} */
1041
+ const intent = {
1042
+ id: url.pathname + url.search,
1043
+ routes: routes.filter(([pattern]) => pattern.test(path)),
1044
+ url,
1045
+ path
1046
+ };
1047
+
1048
+ return intent;
1049
+ }
1050
+
1051
+ /**
1052
+ * @param {{
1053
+ * url: URL;
1054
+ * scroll: { x: number, y: number } | null;
1055
+ * keepfocus: boolean;
1056
+ * redirect_chain: string[];
1057
+ * details: {
1058
+ * replaceState: boolean;
1059
+ * state: any;
1060
+ * } | null;
1061
+ * accepted: () => void;
1062
+ * blocked: () => void;
1063
+ * }} opts
1064
+ */
1065
+ async function navigate({ url, scroll, keepfocus, redirect_chain, details, accepted, blocked }) {
1066
+ const from = current.url;
1067
+ let should_block = false;
1068
+
1069
+ const navigation = {
1070
+ from,
1071
+ to: url,
1072
+ cancel: () => (should_block = true)
1073
+ };
1074
+
1075
+ callbacks.before_navigate.forEach((fn) => fn(navigation));
1076
+
1077
+ if (should_block) {
1078
+ blocked();
1079
+ return;
1080
+ }
1081
+
1082
+ if (!owns(url)) {
1083
+ await native_navigation(url);
1084
+ }
1085
+
1086
+ const pathname = normalize_path(url.pathname, trailing_slash);
1087
+ url = new URL(url.origin + pathname + url.search + url.hash);
1088
+
1089
+ const intent = get_navigation_intent(url);
1090
+
1091
+ update_scroll_positions(current_history_index);
1092
+
1093
+ accepted();
1094
+
1095
+ navigating++;
1096
+
1097
+ const current_navigating_token = (navigating_token = {});
1098
+
1099
+ if (started) {
1100
+ stores.navigating.set({
1101
+ from: current.url,
1102
+ to: intent.url
1103
+ });
1104
+ }
1105
+
1106
+ await update(intent, redirect_chain, false, {
1107
+ scroll,
1108
+ keepfocus
1109
+ });
1110
+
1111
+ navigating--;
1112
+
1113
+ // navigation was aborted
1114
+ if (navigating_token !== current_navigating_token) return;
1115
+
1116
+ if (!navigating) {
1117
+ const navigation = { from, to: url };
1118
+ callbacks.after_navigate.forEach((fn) => fn(navigation));
1119
+
1120
+ stores.navigating.set(null);
1121
+ }
1122
+
1123
+ if (details) {
1124
+ const change = details.replaceState ? 0 : 1;
1125
+ details.state[INDEX_KEY] = current_history_index += change;
1126
+ history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', intent.url);
1127
+ }
1128
+ }
1129
+
1130
+ /**
1131
+ * Loads `href` the old-fashioned way, with a full page reload.
1132
+ * Returns a `Promise` that never resolves (to prevent any
1133
+ * subsequent work, e.g. history manipulation, from happening)
1134
+ * @param {URL} url
1135
+ */
1136
+ function native_navigation(url) {
1137
+ location.href = url.href;
1138
+ return new Promise(() => {});
1139
+ }
1140
+
1141
+ return {
1142
+ after_navigate: (fn) => {
1143
+ onMount(() => {
1144
+ callbacks.after_navigate.push(fn);
1145
+
1146
+ return () => {
1147
+ const i = callbacks.after_navigate.indexOf(fn);
1148
+ callbacks.after_navigate.splice(i, 1);
1149
+ };
1150
+ });
1151
+ },
1152
+
1153
+ before_navigate: (fn) => {
1154
+ onMount(() => {
1155
+ callbacks.before_navigate.push(fn);
1156
+
1157
+ return () => {
1158
+ const i = callbacks.before_navigate.indexOf(fn);
1159
+ callbacks.before_navigate.splice(i, 1);
1160
+ };
1161
+ });
1162
+ },
1163
+
1164
+ disable_scroll_handling: () => {
1165
+ if (import.meta.env.DEV && started && !updating) {
1166
+ throw new Error('Can only disable scroll handling during navigation');
1167
+ }
1168
+
1169
+ if (updating || !started) {
1170
+ autoscroll = false;
1171
+ }
1172
+ },
1173
+
1174
+ goto: (href, opts = {}) => goto(href, opts, []),
1175
+
1176
+ invalidate: (resource) => {
1177
+ const { href } = new URL(resource, location.href);
1178
+
1179
+ invalidated.add(href);
1180
+
1181
+ if (!invalidating) {
1182
+ invalidating = Promise.resolve().then(async () => {
1183
+ const intent = get_navigation_intent(new URL(location.href));
1184
+ await update(intent, [], true);
1185
+
1186
+ invalidating = null;
1187
+ });
1188
+ }
1189
+
1190
+ return invalidating;
1191
+ },
1192
+
1193
+ prefetch: async (href) => {
1194
+ const url = new URL(href, get_base_uri(document));
1195
+ await prefetch(url);
1196
+ },
1197
+
1198
+ // TODO rethink this API
1199
+ prefetch_routes: async (pathnames) => {
1200
+ const matching = pathnames
1201
+ ? routes.filter((route) => pathnames.some((pathname) => route[0].test(pathname)))
1202
+ : routes;
1203
+
1204
+ const promises = matching.map((r) => Promise.all(r[1].map((load) => load())));
1205
+
1206
+ await Promise.all(promises);
1207
+ },
1208
+
1209
+ _start_router: () => {
1210
+ history.scrollRestoration = 'manual';
1211
+
1212
+ // Adopted from Nuxt.js
1213
+ // Reset scrollRestoration to auto when leaving page, allowing page reload
1214
+ // and back-navigation from other pages to use the browser to restore the
1215
+ // scrolling position.
1216
+ addEventListener('beforeunload', (e) => {
1217
+ let should_block = false;
1218
+
1219
+ const navigation = {
1220
+ from: current.url,
1221
+ to: null,
1222
+ cancel: () => (should_block = true)
1223
+ };
1224
+
1225
+ callbacks.before_navigate.forEach((fn) => fn(navigation));
1226
+
1227
+ if (should_block) {
1228
+ e.preventDefault();
1229
+ e.returnValue = '';
1230
+ } else {
1231
+ history.scrollRestoration = 'auto';
1232
+ }
1233
+ });
1234
+
1235
+ addEventListener('visibilitychange', () => {
1236
+ if (document.visibilityState === 'hidden') {
1237
+ update_scroll_positions(current_history_index);
1238
+
1239
+ try {
1240
+ sessionStorage[SCROLL_KEY] = JSON.stringify(scroll_positions);
1241
+ } catch {
1242
+ // do nothing
1243
+ }
1244
+ }
1245
+ });
1246
+
1247
+ /** @param {Event} event */
1248
+ const trigger_prefetch = (event) => {
1249
+ const a = find_anchor(event);
1250
+ if (a && a.href && a.hasAttribute('sveltekit:prefetch')) {
1251
+ prefetch(get_href(a));
1252
+ }
1253
+ };
1254
+
1255
+ /** @type {NodeJS.Timeout} */
1256
+ let mousemove_timeout;
1257
+
1258
+ /** @param {MouseEvent|TouchEvent} event */
1259
+ const handle_mousemove = (event) => {
1260
+ clearTimeout(mousemove_timeout);
1261
+ mousemove_timeout = setTimeout(() => {
1262
+ // event.composedPath(), which is used in find_anchor, will be empty if the event is read in a timeout
1263
+ // add a layer of indirection to address that
1264
+ event.target?.dispatchEvent(
1265
+ new CustomEvent('sveltekit:trigger_prefetch', { bubbles: true })
1266
+ );
1267
+ }, 20);
1268
+ };
1269
+
1270
+ addEventListener('touchstart', trigger_prefetch);
1271
+ addEventListener('mousemove', handle_mousemove);
1272
+ addEventListener('sveltekit:trigger_prefetch', trigger_prefetch);
1273
+
1274
+ /** @param {MouseEvent} event */
1275
+ addEventListener('click', (event) => {
1276
+ if (!router_enabled) return;
1277
+
1278
+ // Adapted from https://github.com/visionmedia/page.js
1279
+ // MIT license https://github.com/visionmedia/page.js#license
1280
+ if (event.button || event.which !== 1) return;
1281
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
1282
+ if (event.defaultPrevented) return;
1283
+
1284
+ const a = find_anchor(event);
1285
+ if (!a) return;
1286
+
1287
+ if (!a.href) return;
1288
+
1289
+ const is_svg_a_element = a instanceof SVGAElement;
1290
+ const url = get_href(a);
1291
+
1292
+ // Ignore if url does not have origin (e.g. `mailto:`, `tel:`.)
1293
+ // MEMO: Without this condition, firefox will open mailer twice.
1294
+ // See: https://github.com/sveltejs/kit/issues/4045
1295
+ if (!is_svg_a_element && url.origin === 'null') return;
1296
+
1297
+ // Ignore if tag has
1298
+ // 1. 'download' attribute
1299
+ // 2. 'rel' attribute includes external
1300
+ const rel = (a.getAttribute('rel') || '').split(/\s+/);
1301
+
1302
+ if (a.hasAttribute('download') || rel.includes('external')) {
1303
+ return;
1304
+ }
1305
+
1306
+ // Ignore if <a> has a target
1307
+ if (is_svg_a_element ? a.target.baseVal : a.target) return;
1308
+
1309
+ if (url.href === location.href) {
1310
+ if (!location.hash) event.preventDefault();
1311
+ return;
1312
+ }
1313
+
1314
+ // Check if new url only differs by hash and use the browser default behavior in that case
1315
+ // This will ensure the `hashchange` event is fired
1316
+ // Removing the hash does a full page navigation in the browser, so make sure a hash is present
1317
+ const [base, hash] = url.href.split('#');
1318
+ if (hash !== undefined && base === location.href.split('#')[0]) {
1319
+ // set this flag to distinguish between navigations triggered by
1320
+ // clicking a hash link and those triggered by popstate
1321
+ hash_navigating = true;
1322
+
1323
+ update_scroll_positions(current_history_index);
1324
+
1325
+ stores.page.set({ ...page, url });
1326
+ stores.page.notify();
1327
+
1328
+ return;
1329
+ }
1330
+
1331
+ navigate({
1332
+ url,
1333
+ scroll: a.hasAttribute('sveltekit:noscroll') ? scroll_state() : null,
1334
+ keepfocus: false,
1335
+ redirect_chain: [],
1336
+ details: {
1337
+ state: {},
1338
+ replaceState: false
1339
+ },
1340
+ accepted: () => event.preventDefault(),
1341
+ blocked: () => event.preventDefault()
1342
+ });
1343
+ });
1344
+
1345
+ addEventListener('popstate', (event) => {
1346
+ if (event.state && router_enabled) {
1347
+ // if a popstate-driven navigation is cancelled, we need to counteract it
1348
+ // with history.go, which means we end up back here, hence this check
1349
+ if (event.state[INDEX_KEY] === current_history_index) return;
1350
+
1351
+ navigate({
1352
+ url: new URL(location.href),
1353
+ scroll: scroll_positions[event.state[INDEX_KEY]],
1354
+ keepfocus: false,
1355
+ redirect_chain: [],
1356
+ details: null,
1357
+ accepted: () => {
1358
+ current_history_index = event.state[INDEX_KEY];
1359
+ },
1360
+ blocked: () => {
1361
+ const delta = current_history_index - event.state[INDEX_KEY];
1362
+ history.go(delta);
1363
+ }
1364
+ });
1365
+ }
1366
+ });
1367
+
1368
+ addEventListener('hashchange', () => {
1369
+ // if the hashchange happened as a result of clicking on a link,
1370
+ // we need to update history, otherwise we have to leave it alone
1371
+ if (hash_navigating) {
1372
+ hash_navigating = false;
1373
+ history.replaceState(
1374
+ { ...history.state, [INDEX_KEY]: ++current_history_index },
1375
+ '',
1376
+ location.href
1377
+ );
1378
+ }
1379
+ });
1380
+ },
1381
+
1382
+ _hydrate: async ({ status, error, nodes, params }) => {
1383
+ const url = new URL(location.href);
1384
+
1385
+ /** @type {Array<import('./types').BranchNode | undefined>} */
1386
+ const branch = [];
1387
+
1388
+ /** @type {Record<string, any>} */
1389
+ let stuff = {};
1390
+
1391
+ /** @type {import('./types').NavigationResult | undefined} */
1392
+ let result;
1393
+
1394
+ let error_args;
1395
+
1396
+ try {
1397
+ for (let i = 0; i < nodes.length; i += 1) {
1398
+ const is_leaf = i === nodes.length - 1;
1399
+
1400
+ let props;
1401
+
1402
+ if (is_leaf) {
1403
+ const serialized = document.querySelector('script[sveltekit\\:data-type="props"]');
1404
+ if (serialized) {
1405
+ props = JSON.parse(/** @type {string} */ (serialized.textContent));
1406
+ }
1407
+ }
1408
+
1409
+ const node = await load_node({
1410
+ module: await nodes[i],
1411
+ url,
1412
+ params,
1413
+ stuff,
1414
+ status: is_leaf ? status : undefined,
1415
+ error: is_leaf ? error : undefined,
1416
+ props
1417
+ });
1418
+
1419
+ if (props) {
1420
+ node.uses.dependencies.add(url.href);
1421
+ node.uses.url = true;
1422
+ }
1423
+
1424
+ branch.push(node);
1425
+
1426
+ if (node && node.loaded) {
1427
+ if (node.loaded.error) {
1428
+ if (error) throw node.loaded.error;
1429
+ error_args = {
1430
+ status: node.loaded.status,
1431
+ error: node.loaded.error,
1432
+ url
1433
+ };
1434
+ } else if (node.loaded.stuff) {
1435
+ stuff = {
1436
+ ...stuff,
1437
+ ...node.loaded.stuff
1438
+ };
1439
+ }
1440
+ }
1441
+ }
1442
+
1443
+ result = error_args
1444
+ ? await load_root_error_page(error_args)
1445
+ : await get_navigation_result_from_branch({
1446
+ url,
1447
+ params,
1448
+ stuff,
1449
+ branch,
1450
+ status,
1451
+ error
1452
+ });
1453
+ } catch (e) {
1454
+ if (error) throw e;
1455
+
1456
+ result = await load_root_error_page({
1457
+ status: 500,
1458
+ error: coalesce_to_error(e),
1459
+ url
1460
+ });
1461
+ }
1462
+
1463
+ if (result.redirect) {
1464
+ // this is a real edge case — `load` would need to return
1465
+ // a redirect but only in the browser
1466
+ await native_navigation(new URL(result.redirect, location.href));
1467
+ }
1468
+
1469
+ initialize(result);
1470
+ }
1471
+ };
1528
1472
  }
1529
1473
 
1530
1474
  /**
@@ -1533,7 +1477,7 @@ class Renderer {
1533
1477
  * assets: string;
1534
1478
  * base: string;
1535
1479
  * },
1536
- * target: Node;
1480
+ * target: Element;
1537
1481
  * session: any;
1538
1482
  * route: boolean;
1539
1483
  * spa: boolean;
@@ -1547,29 +1491,23 @@ class Renderer {
1547
1491
  * }} opts
1548
1492
  */
1549
1493
  async function start({ paths, target, session, route, spa, trailing_slash, hydrate }) {
1550
- const renderer = new Renderer({
1551
- Root,
1552
- fallback,
1494
+ const client = create_client({
1553
1495
  target,
1554
- session
1496
+ session,
1497
+ base: paths.base,
1498
+ trailing_slash
1555
1499
  });
1556
1500
 
1557
- const router = route
1558
- ? new Router({
1559
- base: paths.base,
1560
- routes,
1561
- trailing_slash,
1562
- renderer
1563
- })
1564
- : null;
1565
-
1566
- init({ router, renderer });
1501
+ init({ client });
1567
1502
  set_paths(paths);
1568
1503
 
1569
- if (hydrate) await renderer.start(hydrate);
1570
- if (router) {
1571
- if (spa) router.goto(location.href, { replaceState: true }, []);
1572
- router.init_listeners();
1504
+ if (hydrate) {
1505
+ await client._hydrate(hydrate);
1506
+ }
1507
+
1508
+ if (route) {
1509
+ if (spa) client.goto(location.href, { replaceState: true });
1510
+ client._start_router();
1573
1511
  }
1574
1512
 
1575
1513
  dispatchEvent(new CustomEvent('sveltekit:start'));