@sveltejs/kit 1.0.0-next.20 → 1.0.0-next.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +12 -9
  2. package/assets/components/error.svelte +19 -3
  3. package/assets/runtime/app/env.js +20 -0
  4. package/assets/runtime/app/navigation.js +45 -13
  5. package/assets/runtime/app/paths.js +1 -2
  6. package/assets/runtime/app/stores.js +15 -9
  7. package/assets/runtime/chunks/utils.js +13 -0
  8. package/assets/runtime/env.js +8 -0
  9. package/assets/runtime/internal/singletons.js +5 -11
  10. package/assets/runtime/internal/start.js +979 -351
  11. package/assets/runtime/paths.js +13 -0
  12. package/dist/chunks/cert.js +29254 -0
  13. package/dist/chunks/constants.js +8 -0
  14. package/dist/chunks/error.js +21 -0
  15. package/dist/chunks/index.js +4745 -0
  16. package/dist/chunks/index2.js +700 -0
  17. package/dist/chunks/index3.js +1060 -0
  18. package/dist/chunks/index4.js +398 -0
  19. package/dist/chunks/index5.js +413 -0
  20. package/dist/chunks/index6.js +15574 -0
  21. package/dist/{standard.js → chunks/standard.js} +16 -18
  22. package/dist/chunks/url.js +59 -0
  23. package/dist/cli.js +947 -67
  24. package/dist/hooks.js +28 -0
  25. package/dist/install-fetch.js +6070 -0
  26. package/dist/node.js +51 -0
  27. package/dist/ssr.js +1771 -0
  28. package/package.json +89 -54
  29. package/svelte-kit.js +2 -0
  30. package/types/ambient-modules.d.ts +173 -0
  31. package/types/app.d.ts +21 -0
  32. package/types/config.d.ts +96 -0
  33. package/types/endpoint.d.ts +18 -0
  34. package/types/helper.d.ts +41 -0
  35. package/types/hooks.d.ts +36 -0
  36. package/types/index.d.ts +17 -0
  37. package/types/internal.d.ts +219 -0
  38. package/types/page.d.ts +77 -0
  39. package/CHANGELOG.md +0 -276
  40. package/assets/runtime/app/navigation.js.map +0 -1
  41. package/assets/runtime/app/paths.js.map +0 -1
  42. package/assets/runtime/app/stores.js.map +0 -1
  43. package/assets/runtime/internal/singletons.js.map +0 -1
  44. package/assets/runtime/internal/start.js.map +0 -1
  45. package/assets/runtime/utils-85ebcc60.js +0 -18
  46. package/assets/runtime/utils-85ebcc60.js.map +0 -1
  47. package/dist/api.js +0 -44
  48. package/dist/api.js.map +0 -1
  49. package/dist/build.js +0 -246
  50. package/dist/build.js.map +0 -1
  51. package/dist/cli.js.map +0 -1
  52. package/dist/colors.js +0 -37
  53. package/dist/colors.js.map +0 -1
  54. package/dist/create_app.js +0 -578
  55. package/dist/create_app.js.map +0 -1
  56. package/dist/index.js +0 -12042
  57. package/dist/index.js.map +0 -1
  58. package/dist/index2.js +0 -544
  59. package/dist/index2.js.map +0 -1
  60. package/dist/index3.js +0 -71
  61. package/dist/index3.js.map +0 -1
  62. package/dist/index4.js +0 -466
  63. package/dist/index4.js.map +0 -1
  64. package/dist/index5.js +0 -725
  65. package/dist/index5.js.map +0 -1
  66. package/dist/index6.js +0 -713
  67. package/dist/index6.js.map +0 -1
  68. package/dist/logging.js +0 -43
  69. package/dist/logging.js.map +0 -1
  70. package/dist/package.js +0 -432
  71. package/dist/package.js.map +0 -1
  72. package/dist/renderer.js +0 -2391
  73. package/dist/renderer.js.map +0 -1
  74. package/dist/standard.js.map +0 -1
  75. package/dist/utils.js +0 -54
  76. package/dist/utils.js.map +0 -1
  77. package/svelte-kit +0 -3
@@ -1,12 +1,9 @@
1
1
  import Root from '../../generated/root.svelte';
2
- import { pages, ignore, layout } from '../../generated/manifest.js';
3
- import { f as find_anchor, g as get_base_uri } from '../utils-85ebcc60.js';
2
+ import { fallback, routes } from '../../generated/manifest.js';
3
+ import { g as get_base_uri } from '../chunks/utils.js';
4
4
  import { writable } from 'svelte/store';
5
- import { init, set_paths } from './singletons.js';
6
-
7
- function which(event) {
8
- return event.which === null ? event.button : event.which;
9
- }
5
+ import { init } from './singletons.js';
6
+ import { set_paths } from '../paths.js';
10
7
 
11
8
  function scroll_state() {
12
9
  return {
@@ -15,26 +12,59 @@ function scroll_state() {
15
12
  };
16
13
  }
17
14
 
15
+ /**
16
+ * @param {Event} event
17
+ * @returns {HTMLAnchorElement | SVGAElement | undefined}
18
+ */
19
+ function find_anchor(event) {
20
+ const node = event
21
+ .composedPath()
22
+ .find((e) => e instanceof Node && e.nodeName.toUpperCase() === 'A'); // SVG <a> elements have a lowercase name
23
+ return /** @type {HTMLAnchorElement | SVGAElement | undefined} */ (node);
24
+ }
25
+
26
+ /**
27
+ * @param {HTMLAnchorElement | SVGAElement} node
28
+ * @returns {URL}
29
+ */
30
+ function get_href(node) {
31
+ return node instanceof SVGAElement
32
+ ? new URL(node.href.baseVal, document.baseURI)
33
+ : new URL(node.href);
34
+ }
35
+
18
36
  class Router {
19
- constructor({ base, host, pages, ignore }) {
37
+ /**
38
+ * @param {{
39
+ * base: string;
40
+ * routes: import('types/internal').CSRRoute[];
41
+ * trailing_slash: import('types/internal').TrailingSlash;
42
+ * renderer: import('./renderer').Renderer
43
+ * }} opts
44
+ */
45
+ constructor({ base, routes, trailing_slash, renderer }) {
20
46
  this.base = base;
21
- this.host = host;
22
- this.pages = pages;
23
- this.ignore = ignore;
47
+ this.routes = routes;
48
+ this.trailing_slash = trailing_slash;
49
+ /** Keeps tracks of multiple navigations caused by redirects during rendering */
50
+ this.navigating = 0;
24
51
 
25
- this.history = window.history || {
26
- pushState: () => {},
27
- replaceState: () => {},
28
- scrollRestoration: 'auto'
29
- };
30
- }
31
-
32
- init({ renderer }) {
52
+ /** @type {import('./renderer').Renderer} */
33
53
  this.renderer = renderer;
34
54
  renderer.router = this;
35
55
 
36
- if ('scrollRestoration' in this.history) {
37
- this.history.scrollRestoration = 'manual';
56
+ this.enabled = true;
57
+
58
+ // make it possible to reset focus
59
+ document.body.setAttribute('tabindex', '-1');
60
+
61
+ // create initial history entry, so we can return here
62
+ history.replaceState(history.state || {}, '', location.href);
63
+ }
64
+
65
+ init_listeners() {
66
+ if ('scrollRestoration' in history) {
67
+ history.scrollRestoration = 'manual';
38
68
  }
39
69
 
40
70
  // Adopted from Nuxt.js
@@ -42,16 +72,18 @@ class Router {
42
72
  // and back-navigation from other pages to use the browser to restore the
43
73
  // scrolling position.
44
74
  addEventListener('beforeunload', () => {
45
- this.history.scrollRestoration = 'auto';
75
+ history.scrollRestoration = 'auto';
46
76
  });
47
77
 
48
78
  // Setting scrollRestoration to manual again when returning to this page.
49
79
  addEventListener('load', () => {
50
- this.history.scrollRestoration = 'manual';
80
+ history.scrollRestoration = 'manual';
51
81
  });
52
82
 
53
83
  // There's no API to capture the scroll location right before the user
54
84
  // hits the back/forward button, so we listen for scroll events
85
+
86
+ /** @type {NodeJS.Timeout} */
55
87
  let scroll_timer;
56
88
  addEventListener('scroll', () => {
57
89
  clearTimeout(scroll_timer);
@@ -62,145 +94,306 @@ class Router {
62
94
  ...(history.state || {}),
63
95
  'sveltekit:scroll': scroll_state()
64
96
  };
65
- history.replaceState(new_state, document.title, window.location);
66
- }, 50);
97
+ history.replaceState(new_state, document.title, window.location.href);
98
+ // iOS scroll event intervals happen between 30-150ms, sometimes around 200ms
99
+ }, 200);
67
100
  });
68
101
 
102
+ /** @param {MouseEvent|TouchEvent} event */
103
+ const trigger_prefetch = (event) => {
104
+ const a = find_anchor(event);
105
+ if (a && a.href && a.hasAttribute('sveltekit:prefetch')) {
106
+ this.prefetch(get_href(a));
107
+ }
108
+ };
109
+
110
+ /** @type {NodeJS.Timeout} */
111
+ let mousemove_timeout;
112
+
113
+ /** @param {MouseEvent|TouchEvent} event */
114
+ const handle_mousemove = (event) => {
115
+ clearTimeout(mousemove_timeout);
116
+ mousemove_timeout = setTimeout(() => {
117
+ trigger_prefetch(event);
118
+ }, 20);
119
+ };
120
+
121
+ addEventListener('touchstart', trigger_prefetch);
122
+ addEventListener('mousemove', handle_mousemove);
123
+
124
+ /** @param {MouseEvent} event */
69
125
  addEventListener('click', (event) => {
126
+ if (!this.enabled) return;
127
+
70
128
  // Adapted from https://github.com/visionmedia/page.js
71
129
  // MIT license https://github.com/visionmedia/page.js#license
72
- if (which(event) !== 1) return;
130
+ if (event.button || event.which !== 1) return;
73
131
  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
74
132
  if (event.defaultPrevented) return;
75
133
 
76
- const a = find_anchor(event.target);
134
+ const a = find_anchor(event);
77
135
  if (!a) return;
78
136
 
79
137
  if (!a.href) return;
80
138
 
81
- // check if link is inside an svg
82
- // in this case, both href and target are always inside an object
83
- const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
84
- const href = String(svg ? a.href.baseVal : a.href);
85
-
86
- if (href === location.href) {
139
+ const url = get_href(a);
140
+ const url_string = url.toString();
141
+ if (url_string === location.href) {
87
142
  if (!location.hash) event.preventDefault();
88
143
  return;
89
144
  }
90
145
 
91
146
  // Ignore if tag has
92
147
  // 1. 'download' attribute
93
- // 2. rel='external' attribute
94
- if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
148
+ // 2. 'rel' attribute includes external
149
+ const rel = (a.getAttribute('rel') || '').split(/\s+/);
150
+
151
+ if (a.hasAttribute('download') || (rel && rel.includes('external'))) {
152
+ return;
153
+ }
95
154
 
96
155
  // Ignore if <a> has a target
97
- if (svg ? a.target.baseVal : a.target) return;
156
+ if (a instanceof SVGAElement ? a.target.baseVal : a.target) return;
98
157
 
99
- const url = new URL(href);
158
+ if (!this.owns(url)) return;
100
159
 
101
- // Don't handle hash changes
102
- if (url.pathname === location.pathname && url.search === location.search) return;
160
+ const noscroll = a.hasAttribute('sveltekit:noscroll');
103
161
 
104
- const selected = this.select(url);
105
- if (selected) {
106
- const noscroll = a.hasAttribute('sveltekit:noscroll');
107
- this.renderer.notify(selected);
108
- this.history.pushState({}, '', url.href);
109
- this.navigate(selected, noscroll ? scroll_state() : false, url.hash);
110
- event.preventDefault();
162
+ const i1 = url_string.indexOf('#');
163
+ const i2 = location.href.indexOf('#');
164
+ const u1 = i1 >= 0 ? url_string.substring(0, i1) : url_string;
165
+ const u2 = i2 >= 0 ? location.href.substring(0, i2) : location.href;
166
+ history.pushState({}, '', url.href);
167
+ if (u1 === u2) {
168
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
111
169
  }
170
+ this._navigate(url, noscroll ? scroll_state() : null, false, [], url.hash);
171
+ event.preventDefault();
112
172
  });
113
173
 
114
174
  addEventListener('popstate', (event) => {
115
- if (event.state) {
175
+ if (event.state && this.enabled) {
116
176
  const url = new URL(location.href);
117
- const selected = this.select(url);
118
- if (selected) {
119
- this.navigate(selected, event.state['sveltekit:scroll']);
120
- } else {
121
- // eslint-disable-next-line
122
- location.href = location.href; // nosonar
123
- }
177
+ this._navigate(url, event.state['sveltekit:scroll'], false, []);
124
178
  }
125
179
  });
180
+ }
126
181
 
127
- // make it possible to reset focus
128
- document.body.setAttribute('tabindex', '-1');
182
+ /** @param {URL} url */
183
+ owns(url) {
184
+ return url.origin === location.origin && url.pathname.startsWith(this.base);
185
+ }
129
186
 
130
- // load current page
131
- this.history.replaceState({}, '', location.href);
187
+ /**
188
+ * @param {URL} url
189
+ * @returns {import('./types').NavigationInfo | undefined}
190
+ */
191
+ parse(url) {
192
+ if (this.owns(url)) {
193
+ const path = url.pathname.slice(this.base.length) || '/';
132
194
 
133
- const selected = this.select(new URL(location.href));
134
- if (selected) return this.renderer.start(selected);
135
- }
195
+ const decoded_path = decodeURI(path);
196
+ const routes = this.routes.filter(([pattern]) => pattern.test(decoded_path));
136
197
 
137
- select(url) {
138
- if (url.origin !== location.origin) return null;
139
- if (!url.pathname.startsWith(this.base)) return null;
198
+ const query = new URLSearchParams(url.search);
199
+ const id = `${path}?${query}`;
140
200
 
141
- let path = url.pathname.slice(this.base.length);
201
+ return { id, routes, path, decoded_path, query };
202
+ }
203
+ }
204
+
205
+ /**
206
+ * @typedef {Parameters<typeof import('$app/navigation').goto>} GotoParams
207
+ *
208
+ * @param {GotoParams[0]} href
209
+ * @param {GotoParams[1]} opts
210
+ * @param {string[]} chain
211
+ */
212
+ async goto(
213
+ href,
214
+ { noscroll = false, replaceState = false, keepfocus = false, state = {} } = {},
215
+ chain
216
+ ) {
217
+ const url = new URL(href, get_base_uri(document));
142
218
 
143
- if (path === '') {
144
- path = '/';
219
+ if (this.enabled && this.owns(url)) {
220
+ history[replaceState ? 'replaceState' : 'pushState'](state, '', href);
221
+ return this._navigate(url, noscroll ? scroll_state() : null, keepfocus, chain, url.hash);
145
222
  }
146
223
 
147
- // avoid accidental clashes between server routes and page routes
148
- if (this.ignore.some((pattern) => pattern.test(path))) return;
224
+ location.href = url.href;
225
+ return new Promise(() => {
226
+ /* never resolves */
227
+ });
228
+ }
149
229
 
150
- for (const route of this.pages) {
151
- const match = route.pattern.exec(path);
230
+ enable() {
231
+ this.enabled = true;
232
+ }
152
233
 
153
- if (match) {
154
- const query = new URLSearchParams(url.search);
155
- const params = route.params(match);
234
+ disable() {
235
+ this.enabled = false;
236
+ }
156
237
 
157
- const page = { host: this.host, path, query, params };
238
+ /**
239
+ * @param {URL} url
240
+ * @returns {Promise<import('./types').NavigationResult>}
241
+ */
242
+ async prefetch(url) {
243
+ const info = this.parse(url);
158
244
 
159
- return { href: url.href, route, match, page };
160
- }
245
+ if (!info) {
246
+ throw new Error('Attempted to prefetch a URL that does not belong to this app');
161
247
  }
248
+
249
+ return this.renderer.load(info);
162
250
  }
163
251
 
164
- async goto(href, { noscroll = false, replaceState = false } = {}) {
165
- const url = new URL(href, get_base_uri(document));
166
- const selected = this.select(url);
252
+ /**
253
+ * @param {URL} url
254
+ * @param {{ x: number, y: number }?} scroll
255
+ * @param {boolean} keepfocus
256
+ * @param {string[]} chain
257
+ * @param {string} [hash]
258
+ */
259
+ async _navigate(url, scroll, keepfocus, chain, hash) {
260
+ const info = this.parse(url);
261
+
262
+ if (!info) {
263
+ throw new Error('Attempted to navigate to a URL that does not belong to this app');
264
+ }
167
265
 
168
- if (selected) {
169
- this.renderer.notify(selected);
266
+ if (!this.navigating) {
267
+ dispatchEvent(new CustomEvent('sveltekit:navigation-start'));
268
+ }
269
+ this.navigating++;
170
270
 
171
- // TODO shouldn't need to pass the hash here
172
- this.history[replaceState ? 'replaceState' : 'pushState']({}, '', href);
173
- return this.navigate(selected, noscroll ? scroll_state() : false, url.hash);
271
+ // remove trailing slashes
272
+ if (info.path !== '/') {
273
+ const has_trailing_slash = info.path.endsWith('/');
274
+
275
+ const incorrect =
276
+ (has_trailing_slash && this.trailing_slash === 'never') ||
277
+ (!has_trailing_slash &&
278
+ this.trailing_slash === 'always' &&
279
+ !(info.path.split('/').pop() || '').includes('.'));
280
+
281
+ if (incorrect) {
282
+ info.path = has_trailing_slash ? info.path.slice(0, -1) : info.path + '/';
283
+ history.replaceState({}, '', `${this.base}${info.path}${location.search}`);
284
+ }
174
285
  }
175
286
 
176
- location.href = href;
177
- return new Promise(() => {
178
- /* never resolves */
179
- });
287
+ await this.renderer.handle_navigation(info, chain, false, { hash, scroll, keepfocus });
288
+
289
+ this.navigating--;
290
+ if (!this.navigating) {
291
+ dispatchEvent(new CustomEvent('sveltekit:navigation-end'));
292
+ }
180
293
  }
294
+ }
181
295
 
182
- async navigate(selected, scroll, hash) {
183
- // remove trailing slashes
184
- if (location.pathname.endsWith('/') && location.pathname !== '/') {
185
- history.replaceState({}, '', `${location.pathname.slice(0, -1)}${location.search}`);
296
+ /**
297
+ * @param {unknown} err
298
+ * @return {Error}
299
+ */
300
+ function coalesce_to_error(err) {
301
+ return err instanceof Error ||
302
+ (err && /** @type {any} */ (err).name && /** @type {any} */ (err).message)
303
+ ? /** @type {Error} */ (err)
304
+ : new Error(JSON.stringify(err));
305
+ }
306
+
307
+ /**
308
+ * Hash using djb2
309
+ * @param {import('types/hooks').StrictBody} value
310
+ */
311
+ function hash(value) {
312
+ let hash = 5381;
313
+ let i = value.length;
314
+
315
+ if (typeof value === 'string') {
316
+ while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
317
+ } else {
318
+ while (i) hash = (hash * 33) ^ value[--i];
319
+ }
320
+
321
+ return (hash >>> 0).toString(36);
322
+ }
323
+
324
+ /**
325
+ * @param {import('types/page').LoadOutput} loaded
326
+ * @returns {import('types/internal').NormalizedLoadOutput}
327
+ */
328
+ function normalize(loaded) {
329
+ const has_error_status =
330
+ loaded.status && loaded.status >= 400 && loaded.status <= 599 && !loaded.redirect;
331
+ if (loaded.error || has_error_status) {
332
+ const status = loaded.status;
333
+
334
+ if (!loaded.error && has_error_status) {
335
+ return {
336
+ status: status || 500,
337
+ error: new Error()
338
+ };
186
339
  }
187
340
 
188
- await this.renderer.render(selected);
341
+ const error = typeof loaded.error === 'string' ? new Error(loaded.error) : loaded.error;
189
342
 
190
- document.body.focus();
343
+ if (!(error instanceof Error)) {
344
+ return {
345
+ status: 500,
346
+ error: new Error(
347
+ `"error" property returned from load() must be a string or instance of Error, received type "${typeof error}"`
348
+ )
349
+ };
350
+ }
191
351
 
192
- const deep_linked = hash && document.getElementById(hash.slice(1));
193
- if (scroll) {
194
- scrollTo(scroll.x, scroll.y);
195
- } else if (deep_linked) {
196
- // scroll is an element id (from a hash), we need to compute y
197
- scrollTo(0, deep_linked.getBoundingClientRect().top + scrollY);
198
- } else {
199
- scrollTo(0, 0);
352
+ if (!status || status < 400 || status > 599) {
353
+ console.warn('"error" returned from load() without a valid status code — defaulting to 500');
354
+ return { status: 500, error };
200
355
  }
356
+
357
+ return { status, error };
201
358
  }
359
+
360
+ if (loaded.redirect) {
361
+ if (!loaded.status || Math.floor(loaded.status / 100) !== 3) {
362
+ return {
363
+ status: 500,
364
+ error: new Error(
365
+ '"redirect" property returned from load() must be accompanied by a 3xx status code'
366
+ )
367
+ };
368
+ }
369
+
370
+ if (typeof loaded.redirect !== 'string') {
371
+ return {
372
+ status: 500,
373
+ error: new Error('"redirect" property returned from load() must be a string')
374
+ };
375
+ }
376
+ }
377
+
378
+ // TODO remove before 1.0
379
+ if (/** @type {any} */ (loaded).context) {
380
+ throw new Error(
381
+ 'You are returning "context" from a load function. ' +
382
+ '"context" was renamed to "stuff", please adjust your code accordingly.'
383
+ );
384
+ }
385
+
386
+ return /** @type {import('types/internal').NormalizedLoadOutput} */ (loaded);
202
387
  }
203
388
 
389
+ /**
390
+ * @typedef {import('types/internal').CSRComponent} CSRComponent
391
+ *
392
+ * @typedef {Partial<import('types/page').Page>} Page
393
+ * @typedef {{ from: Page; to: Page }} Navigating
394
+ */
395
+
396
+ /** @param {any} value */
204
397
  function page_store(value) {
205
398
  const store = writable(value);
206
399
  let ready = true;
@@ -210,12 +403,15 @@ function page_store(value) {
210
403
  store.update((val) => val);
211
404
  }
212
405
 
406
+ /** @param {any} new_value */
213
407
  function set(new_value) {
214
408
  ready = false;
215
409
  store.set(new_value);
216
410
  }
217
411
 
412
+ /** @param {(value: any) => void} run */
218
413
  function subscribe(run) {
414
+ /** @type {any} */
219
415
  let old_value;
220
416
  return store.subscribe((new_value) => {
221
417
  if (old_value === undefined || (ready && new_value !== old_value)) {
@@ -227,38 +423,74 @@ function page_store(value) {
227
423
  return { notify, set, subscribe };
228
424
  }
229
425
 
426
+ /**
427
+ * @param {RequestInfo} resource
428
+ * @param {RequestInit} [opts]
429
+ */
430
+ function initial_fetch(resource, opts) {
431
+ const url = typeof resource === 'string' ? resource : resource.url;
432
+
433
+ let selector = `script[data-type="svelte-data"][data-url=${JSON.stringify(url)}]`;
434
+
435
+ if (opts && typeof opts.body === 'string') {
436
+ selector += `[data-body="${hash(opts.body)}"]`;
437
+ }
438
+
439
+ const script = document.querySelector(selector);
440
+ if (script && script.textContent) {
441
+ const { body, ...init } = JSON.parse(script.textContent);
442
+ return Promise.resolve(new Response(body, init));
443
+ }
444
+
445
+ return fetch(resource, opts);
446
+ }
447
+
230
448
  class Renderer {
231
- constructor({ Root, layout, target, error, status, preloaded, session }) {
449
+ /**
450
+ * @param {{
451
+ * Root: CSRComponent;
452
+ * fallback: [CSRComponent, CSRComponent];
453
+ * target: Node;
454
+ * session: any;
455
+ * host: string;
456
+ * }} opts
457
+ */
458
+ constructor({ Root, fallback, target, session, host }) {
232
459
  this.Root = Root;
233
- this.layout = layout;
234
- this.layout_loader = () => layout;
460
+ this.fallback = fallback;
461
+ this.host = host;
462
+
463
+ /** @type {import('./router').Router | undefined} */
464
+ this.router;
235
465
 
236
- // TODO ideally we wouldn't need to store these...
237
466
  this.target = target;
238
467
 
239
- this.initial = {
240
- preloaded,
241
- error,
242
- status
243
- };
468
+ this.started = false;
469
+
470
+ this.session_id = 1;
471
+ this.invalid = new Set();
472
+ this.invalidating = null;
244
473
 
474
+ /** @type {import('./types').NavigationState} */
245
475
  this.current = {
476
+ // @ts-ignore - we need the initial value to be null
246
477
  page: null,
247
- query: null,
248
- session_changed: false,
249
- nodes: []
478
+ session_id: 0,
479
+ branch: []
250
480
  };
251
481
 
252
- this.caches = new Map();
482
+ /** @type {Map<string, import('./types').NavigationResult>} */
483
+ this.cache = new Map();
253
484
 
254
- this.prefetching = {
255
- href: null,
485
+ /** @type {{id: string | null, promise: Promise<import('./types').NavigationResult> | null}} */
486
+ this.loading = {
487
+ id: null,
256
488
  promise: null
257
489
  };
258
490
 
259
491
  this.stores = {
260
492
  page: page_store({}),
261
- navigating: writable(null),
493
+ navigating: writable(/** @type {Navigating | null} */ (null)),
262
494
  session: writable(session)
263
495
  };
264
496
 
@@ -266,326 +498,722 @@ class Renderer {
266
498
 
267
499
  this.root = null;
268
500
 
269
- const trigger_prefetch = (event) => {
270
- const a = find_anchor(event.target);
271
-
272
- if (a && a.hasAttribute('sveltekit:prefetch')) {
273
- this.prefetch(new URL(a.href));
274
- }
275
- };
276
-
277
- let mousemove_timeout;
278
- const handle_mousemove = (event) => {
279
- clearTimeout(mousemove_timeout);
280
- mousemove_timeout = setTimeout(() => {
281
- trigger_prefetch(event);
282
- }, 20);
283
- };
284
-
285
- addEventListener('touchstart', trigger_prefetch);
286
- addEventListener('mousemove', handle_mousemove);
287
-
288
501
  let ready = false;
289
502
  this.stores.session.subscribe(async (value) => {
290
503
  this.$session = value;
291
504
 
292
- if (!ready) return;
293
- this.current.session_changed = true;
505
+ if (!ready || !this.router) return;
506
+ this.session_id += 1;
294
507
 
295
- const selected = this.router.select(new URL(location.href));
296
- this.render(selected);
508
+ const info = this.router.parse(new URL(location.href));
509
+ if (info) this.update(info, [], true);
297
510
  });
298
511
  ready = true;
299
512
  }
300
513
 
301
- async start(selected) {
302
- const props = {
303
- stores: this.stores,
304
- error: this.initial.error,
305
- status: this.initial.status,
306
- page: selected.page
307
- };
514
+ /**
515
+ * @param {{
516
+ * status: number;
517
+ * error: Error;
518
+ * nodes: Array<Promise<CSRComponent>>;
519
+ * page: import('types/page').Page;
520
+ * }} selected
521
+ */
522
+ async start({ status, error, nodes, page }) {
523
+ /** @type {Array<import('./types').BranchNode | undefined>} */
524
+ const branch = [];
525
+
526
+ /** @type {Record<string, any>} */
527
+ let stuff = {};
528
+
529
+ /** @type {import('./types').NavigationResult | undefined} */
530
+ let result;
531
+
532
+ let error_args;
533
+
534
+ try {
535
+ for (let i = 0; i < nodes.length; i += 1) {
536
+ const is_leaf = i === nodes.length - 1;
537
+
538
+ const node = await this._load_node({
539
+ module: await nodes[i],
540
+ page,
541
+ stuff,
542
+ status: is_leaf ? status : undefined,
543
+ error: is_leaf ? error : undefined
544
+ });
545
+
546
+ branch.push(node);
547
+
548
+ if (node && node.loaded) {
549
+ if (node.loaded.error) {
550
+ if (error) throw node.loaded.error;
551
+ error_args = {
552
+ status: node.loaded.status,
553
+ error: node.loaded.error,
554
+ path: page.path,
555
+ query: page.query
556
+ };
557
+ } else if (node.loaded.stuff) {
558
+ stuff = {
559
+ ...stuff,
560
+ ...node.loaded.stuff
561
+ };
562
+ }
563
+ }
564
+ }
565
+
566
+ result = error_args
567
+ ? await this._load_error(error_args)
568
+ : await this._get_navigation_result_from_branch({ page, branch });
569
+ } catch (e) {
570
+ if (error) throw e;
571
+
572
+ result = await this._load_error({
573
+ status: 500,
574
+ error: coalesce_to_error(e),
575
+ path: page.path,
576
+ query: page.query
577
+ });
578
+ }
308
579
 
309
- if (this.initial.error) {
310
- props.components = [this.layout.default];
580
+ if (result.redirect) {
581
+ // this is a real edge case — `load` would need to return
582
+ // a redirect but only in the browser
583
+ location.href = new URL(result.redirect, location.href).href;
584
+ return;
585
+ }
586
+
587
+ this._init(result);
588
+ }
589
+
590
+ /**
591
+ * @param {import('./types').NavigationInfo} info
592
+ * @param {string[]} chain
593
+ * @param {boolean} no_cache
594
+ * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
595
+ */
596
+ async handle_navigation(info, chain, no_cache, opts) {
597
+ if (this.started) {
598
+ this.stores.navigating.set({
599
+ from: {
600
+ path: this.current.page.path,
601
+ query: this.current.page.query
602
+ },
603
+ to: {
604
+ path: info.path,
605
+ query: info.query
606
+ }
607
+ });
608
+ }
609
+
610
+ await this.update(info, chain, no_cache, opts);
611
+ }
612
+
613
+ /**
614
+ * @param {import('./types').NavigationInfo} info
615
+ * @param {string[]} chain
616
+ * @param {boolean} no_cache
617
+ * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
618
+ */
619
+ async update(info, chain, no_cache, opts) {
620
+ const token = (this.token = {});
621
+ let navigation_result = await this._get_navigation_result(info, no_cache);
622
+
623
+ // abort if user navigated during update
624
+ if (token !== this.token) return;
625
+
626
+ this.invalid.clear();
627
+
628
+ if (navigation_result.redirect) {
629
+ if (chain.length > 10 || chain.includes(info.path)) {
630
+ navigation_result = await this._load_error({
631
+ status: 500,
632
+ error: new Error('Redirect loop'),
633
+ path: info.path,
634
+ query: info.query
635
+ });
636
+ } else {
637
+ if (this.router) {
638
+ this.router.goto(navigation_result.redirect, { replaceState: true }, [
639
+ ...chain,
640
+ info.path
641
+ ]);
642
+ } else {
643
+ location.href = new URL(navigation_result.redirect, location.href).href;
644
+ }
645
+
646
+ return;
647
+ }
648
+ }
649
+
650
+ if (navigation_result.reload) {
651
+ location.reload();
652
+ } else if (this.started) {
653
+ this.current = navigation_result.state;
654
+
655
+ this.root.$set(navigation_result.props);
656
+ this.stores.navigating.set(null);
311
657
  } else {
312
- const hydrated = await this.hydrate(selected);
658
+ this._init(navigation_result);
659
+ }
313
660
 
314
- if (hydrated.redirect) {
315
- throw new Error('TODO client-side redirects');
661
+ // opts must be passed if we're navigating...
662
+ if (opts) {
663
+ const { hash, scroll, keepfocus } = opts;
664
+
665
+ if (!keepfocus) {
666
+ getSelection()?.removeAllRanges();
667
+ document.body.focus();
668
+ }
669
+
670
+ const old_page_y_offset = Math.round(pageYOffset);
671
+ const old_max_page_y_offset = document.documentElement.scrollHeight - innerHeight;
672
+
673
+ await 0;
674
+
675
+ const new_page_y_offset = Math.round(pageYOffset);
676
+ const new_max_page_y_offset = document.documentElement.scrollHeight - innerHeight;
677
+
678
+ // After `await 0`, the `onMount()` function in the component executed.
679
+ // Check if no scrolling happened on mount.
680
+ const no_scroll_happened =
681
+ // In most cases, we can compare whether `pageYOffset` changed between navigation
682
+ new_page_y_offset === Math.min(old_page_y_offset, new_max_page_y_offset) ||
683
+ // But if the page is scrolled to/near the bottom, the browser would also scroll
684
+ // to/near the bottom of the new page on navigation. Since we can't detect when this
685
+ // behaviour happens, we naively compare by the y offset from the bottom of the page.
686
+ old_max_page_y_offset - old_page_y_offset === new_max_page_y_offset - new_page_y_offset;
687
+
688
+ // If there was no scrolling, we run on our custom scroll handling
689
+ if (no_scroll_happened) {
690
+ const deep_linked = hash && document.getElementById(hash.slice(1));
691
+ if (scroll) {
692
+ scrollTo(scroll.x, scroll.y);
693
+ } else if (deep_linked) {
694
+ // Here we use `scrollIntoView` on the element instead of `scrollTo`
695
+ // because it natively supports the `scroll-margin` and `scroll-behavior`
696
+ // CSS properties.
697
+ deep_linked.scrollIntoView();
698
+ } else {
699
+ scrollTo(0, 0);
700
+ }
316
701
  }
702
+ } else {
703
+ // ...they will not be supplied if we're simply invalidating
704
+ await 0;
705
+ }
706
+
707
+ this.loading.promise = null;
708
+ this.loading.id = null;
317
709
 
318
- Object.assign(props, hydrated.props);
319
- this.current = hydrated.state;
710
+ if (!this.router) return;
711
+
712
+ const leaf_node = navigation_result.state.branch[navigation_result.state.branch.length - 1];
713
+ if (leaf_node && leaf_node.module.router === false) {
714
+ this.router.disable();
715
+ } else {
716
+ this.router.enable();
320
717
  }
718
+ }
719
+
720
+ /**
721
+ * @param {import('./types').NavigationInfo} info
722
+ * @returns {Promise<import('./types').NavigationResult>}
723
+ */
724
+ load(info) {
725
+ this.loading.promise = this._get_navigation_result(info, false);
726
+ this.loading.id = info.id;
727
+
728
+ return this.loading.promise;
729
+ }
730
+
731
+ /** @param {string} href */
732
+ invalidate(href) {
733
+ this.invalid.add(href);
734
+
735
+ if (!this.invalidating) {
736
+ this.invalidating = Promise.resolve().then(async () => {
737
+ const info = this.router && this.router.parse(new URL(location.href));
738
+ if (info) await this.update(info, [], true);
739
+
740
+ this.invalidating = null;
741
+ });
742
+ }
743
+
744
+ return this.invalidating;
745
+ }
746
+
747
+ /** @param {import('./types').NavigationResult} result */
748
+ _init(result) {
749
+ this.current = result.state;
750
+
751
+ const style = document.querySelector('style[data-svelte]');
752
+ if (style) style.remove();
321
753
 
322
754
  this.root = new this.Root({
323
755
  target: this.target,
324
- props,
756
+ props: {
757
+ stores: this.stores,
758
+ ...result.props
759
+ },
325
760
  hydrate: true
326
761
  });
327
762
 
328
- this.initial = null;
763
+ this.started = true;
329
764
  }
330
765
 
331
- notify(selected) {
332
- this.stores.navigating.set({
333
- from: this.current.page,
334
- to: selected.page
766
+ /**
767
+ * @param {import('./types').NavigationInfo} info
768
+ * @param {boolean} no_cache
769
+ * @returns {Promise<import('./types').NavigationResult>}
770
+ */
771
+ async _get_navigation_result(info, no_cache) {
772
+ if (this.loading.id === info.id && this.loading.promise) {
773
+ return this.loading.promise;
774
+ }
775
+
776
+ for (let i = 0; i < info.routes.length; i += 1) {
777
+ const route = info.routes[i];
778
+
779
+ // load code for subsequent routes immediately, if they are as
780
+ // likely to match the current path/query as the current one
781
+ let j = i + 1;
782
+ while (j < info.routes.length) {
783
+ const next = info.routes[j];
784
+ if (next[0].toString() === route[0].toString()) {
785
+ next[1].forEach((loader) => loader());
786
+ j += 1;
787
+ } else {
788
+ break;
789
+ }
790
+ }
791
+
792
+ const result = await this._load(
793
+ {
794
+ route,
795
+ info
796
+ },
797
+ no_cache
798
+ );
799
+ if (result) return result;
800
+ }
801
+
802
+ return await this._load_error({
803
+ status: 404,
804
+ error: new Error(`Not found: ${info.path}`),
805
+ path: info.path,
806
+ query: info.query
335
807
  });
336
808
  }
337
809
 
338
- async render(selected) {
339
- const token = (this.token = {});
810
+ /**
811
+ *
812
+ * @param {{
813
+ * page: import('types/page').Page;
814
+ * branch: Array<import('./types').BranchNode | undefined>
815
+ * }} opts
816
+ */
817
+ async _get_navigation_result_from_branch({ page, branch }) {
818
+ const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
819
+ const redirect = filtered.find((f) => f.loaded && f.loaded.redirect);
820
+
821
+ /** @type {import('./types').NavigationResult} */
822
+ const result = {
823
+ redirect: redirect && redirect.loaded ? redirect.loaded.redirect : undefined,
824
+ state: {
825
+ page,
826
+ branch,
827
+ session_id: this.session_id
828
+ },
829
+ props: {
830
+ components: filtered.map((node) => node.module.default)
831
+ }
832
+ };
833
+
834
+ for (let i = 0; i < filtered.length; i += 1) {
835
+ const loaded = filtered[i].loaded;
836
+ result.props[`props_${i}`] = loaded ? await loaded.props : null;
837
+ }
340
838
 
341
- const hydrated = await this.hydrate(selected);
839
+ if (
840
+ !this.current.page ||
841
+ page.path !== this.current.page.path ||
842
+ page.query.toString() !== this.current.page.query.toString()
843
+ ) {
844
+ result.props.page = page;
845
+ }
342
846
 
343
- if (this.token === token) {
344
- // check render wasn't aborted
345
- this.current = hydrated.state;
847
+ const leaf = filtered[filtered.length - 1];
848
+ const maxage = leaf.loaded && leaf.loaded.maxage;
346
849
 
347
- this.root.$set(hydrated.props);
348
- this.stores.navigating.set(null);
850
+ if (maxage) {
851
+ const key = `${page.path}?${page.query}`;
852
+ let ready = false;
853
+
854
+ const clear = () => {
855
+ if (this.cache.get(key) === result) {
856
+ this.cache.delete(key);
857
+ }
858
+
859
+ unsubscribe();
860
+ clearTimeout(timeout);
861
+ };
862
+
863
+ const timeout = setTimeout(clear, maxage * 1000);
864
+
865
+ const unsubscribe = this.stores.session.subscribe(() => {
866
+ if (ready) clear();
867
+ });
868
+
869
+ ready = true;
870
+
871
+ this.cache.set(key, result);
349
872
  }
873
+
874
+ return result;
350
875
  }
351
876
 
352
- async hydrate({ route, page }) {
353
- const props = {
354
- error: null,
355
- status: 200,
356
- components: []
877
+ /**
878
+ * @param {{
879
+ * status?: number;
880
+ * error?: Error;
881
+ * module: CSRComponent;
882
+ * page: import('types/page').Page;
883
+ * stuff: Record<string, any>;
884
+ * }} options
885
+ * @returns
886
+ */
887
+ async _load_node({ status, error, module, page, stuff }) {
888
+ /** @type {import('./types').BranchNode} */
889
+ const node = {
890
+ module,
891
+ uses: {
892
+ params: new Set(),
893
+ path: false,
894
+ query: false,
895
+ session: false,
896
+ stuff: false,
897
+ dependencies: []
898
+ },
899
+ loaded: null,
900
+ stuff
357
901
  };
358
902
 
359
- const fetcher = (url, opts) => {
360
- if (this.initial) {
361
- const script = document.querySelector(`script[type="svelte-data"][url="${url}"]`);
362
- if (script) {
363
- const { body, ...init } = JSON.parse(script.textContent);
364
- return Promise.resolve(new Response(body, init));
903
+ /** @type {Record<string, string>} */
904
+ const params = {};
905
+ for (const key in page.params) {
906
+ Object.defineProperty(params, key, {
907
+ get() {
908
+ node.uses.params.add(key);
909
+ return page.params[key];
910
+ },
911
+ enumerable: true
912
+ });
913
+ }
914
+
915
+ const session = this.$session;
916
+
917
+ if (module.load) {
918
+ const { started } = this;
919
+
920
+ /** @type {import('types/page').LoadInput | import('types/page').ErrorLoadInput} */
921
+ const load_input = {
922
+ page: {
923
+ host: page.host,
924
+ params,
925
+ get path() {
926
+ node.uses.path = true;
927
+ return page.path;
928
+ },
929
+ get query() {
930
+ node.uses.query = true;
931
+ return page.query;
932
+ }
933
+ },
934
+ get session() {
935
+ node.uses.session = true;
936
+ return session;
937
+ },
938
+ get stuff() {
939
+ node.uses.stuff = true;
940
+ return { ...stuff };
941
+ },
942
+ fetch(resource, info) {
943
+ const url = typeof resource === 'string' ? resource : resource.url;
944
+ const { href } = new URL(url, new URL(page.path, document.baseURI));
945
+ node.uses.dependencies.push(href);
946
+
947
+ return started ? fetch(resource, info) : initial_fetch(resource, info);
365
948
  }
949
+ };
950
+
951
+ if (error) {
952
+ /** @type {import('types/page').ErrorLoadInput} */ (load_input).status = status;
953
+ /** @type {import('types/page').ErrorLoadInput} */ (load_input).error = error;
366
954
  }
367
955
 
368
- return fetch(url, opts);
369
- };
956
+ const loaded = await module.load.call(null, load_input);
370
957
 
371
- const query = page.query.toString();
958
+ // if the page component returns nothing from load, fall through
959
+ if (!loaded) return;
372
960
 
373
- const state = {
374
- page,
375
- query,
376
- session_changed: false,
377
- nodes: []
378
- };
961
+ node.loaded = normalize(loaded);
962
+ if (node.loaded.stuff) node.stuff = node.loaded.stuff;
963
+ }
379
964
 
380
- const component_promises = [this.layout_loader(), ...route.parts.map((loader) => loader())];
381
- const props_promises = [];
965
+ return node;
966
+ }
382
967
 
383
- let context = {};
384
- let redirect;
968
+ /**
969
+ * @param {import('./types').NavigationCandidate} selected
970
+ * @param {boolean} no_cache
971
+ * @returns {Promise<import('./types').NavigationResult | undefined>} undefined if fallthrough
972
+ */
973
+ async _load({ route, info: { path, decoded_path, query } }, no_cache) {
974
+ const key = `${decoded_path}?${query}`;
975
+
976
+ if (!no_cache) {
977
+ const cached = this.cache.get(key);
978
+ if (cached) return cached;
979
+ }
385
980
 
386
- const changed = {
387
- params: Object.keys(page.params).filter((key) => {
388
- return !this.current.page || this.current.page.params[key] !== page.params[key];
389
- }),
390
- query: query !== this.current.query,
391
- session: this.current.session_changed,
392
- context: false
981
+ const [pattern, a, b, get_params] = route;
982
+ const params = get_params
983
+ ? // the pattern is for the route which we've already matched to this path
984
+ get_params(/** @type {RegExpExecArray} */ (pattern.exec(decoded_path)))
985
+ : {};
986
+
987
+ const changed = this.current.page && {
988
+ path: path !== this.current.page.path,
989
+ params: Object.keys(params).filter((key) => this.current.page.params[key] !== params[key]),
990
+ query: query.toString() !== this.current.page.query.toString(),
991
+ session: this.session_id !== this.current.session_id
393
992
  };
394
993
 
395
- try {
396
- for (let i = 0; i < component_promises.length; i += 1) {
397
- const previous = this.current.nodes[i];
994
+ /** @type {import('types/page').Page} */
995
+ const page = { host: this.host, path, query, params };
996
+
997
+ /** @type {Array<import('./types').BranchNode | undefined>} */
998
+ let branch = [];
398
999
 
399
- const { default: component, load } = await component_promises[i];
400
- props.components[i] = component;
1000
+ /** @type {Record<string, any>} */
1001
+ let stuff = {};
1002
+ let stuff_changed = false;
1003
+
1004
+ /** @type {number | undefined} */
1005
+ let status = 200;
1006
+
1007
+ /** @type {Error | undefined} */
1008
+ let error;
1009
+
1010
+ // preload modules
1011
+ a.forEach((loader) => loader());
1012
+
1013
+ load: for (let i = 0; i < a.length; i += 1) {
1014
+ /** @type {import('./types').BranchNode | undefined} */
1015
+ let node;
1016
+
1017
+ try {
1018
+ if (!a[i]) continue;
1019
+
1020
+ const module = await a[i]();
1021
+ const previous = this.current.branch[i];
401
1022
 
402
1023
  const changed_since_last_render =
403
1024
  !previous ||
404
- component !== previous.component ||
1025
+ module !== previous.module ||
1026
+ (changed.path && previous.uses.path) ||
405
1027
  changed.params.some((param) => previous.uses.params.has(param)) ||
406
1028
  (changed.query && previous.uses.query) ||
407
1029
  (changed.session && previous.uses.session) ||
408
- (changed.context && previous.uses.context);
1030
+ previous.uses.dependencies.some((dep) => this.invalid.has(dep)) ||
1031
+ (stuff_changed && previous.uses.stuff);
409
1032
 
410
1033
  if (changed_since_last_render) {
411
- const hash = page.path + query;
412
-
413
- // see if we have some cached data
414
- const cache = this.caches.get(component);
415
- const cached = cache && cache.get(hash);
416
-
417
- let node;
418
- let loaded;
419
-
420
- if (cached && (!changed.context || !cached.node.uses.context)) {
421
- ({ node, loaded } = cached);
422
- } else {
423
- node = {
424
- component,
425
- uses: {
426
- params: new Set(),
427
- query: false,
428
- session: false,
429
- context: false
430
- }
431
- };
432
-
433
- const params = {};
434
- for (const key in page.params) {
435
- Object.defineProperty(params, key, {
436
- get() {
437
- node.uses.params.add(key);
438
- return page.params[key];
439
- },
440
- enumerable: true
441
- });
1034
+ node = await this._load_node({
1035
+ module,
1036
+ page,
1037
+ stuff
1038
+ });
1039
+
1040
+ const is_leaf = i === a.length - 1;
1041
+
1042
+ if (node && node.loaded) {
1043
+ if (node.loaded.error) {
1044
+ status = node.loaded.status;
1045
+ error = node.loaded.error;
442
1046
  }
443
1047
 
444
- const session = this.$session;
445
-
446
- loaded =
447
- load &&
448
- (await load.call(null, {
449
- page: {
450
- ...page,
451
- params,
452
- get query() {
453
- node.uses.query = true;
454
- return page.query;
455
- }
456
- },
457
- get session() {
458
- node.uses.session = true;
459
- return session;
460
- },
461
- get context() {
462
- node.uses.context = true;
463
- return { ...context };
464
- },
465
- fetch: fetcher
466
- }));
467
- }
468
-
469
- if (loaded) {
470
- if (loaded.error) {
471
- const error = new Error(loaded.error.message);
472
- error.status = loaded.error.status;
473
- throw error;
1048
+ if (node.loaded.redirect) {
1049
+ return {
1050
+ redirect: node.loaded.redirect,
1051
+ props: {},
1052
+ state: this.current
1053
+ };
474
1054
  }
475
1055
 
476
- if (loaded.redirect) {
477
- redirect = loaded.redirect;
478
- break;
1056
+ if (node.loaded.stuff) {
1057
+ stuff_changed = true;
479
1058
  }
1059
+ } else if (is_leaf && module.load) {
1060
+ // if the leaf node has a `load` function
1061
+ // that returns nothing, fall through
1062
+ return;
1063
+ }
1064
+ } else {
1065
+ node = previous;
1066
+ }
1067
+ } catch (e) {
1068
+ status = 500;
1069
+ error = coalesce_to_error(e);
1070
+ }
480
1071
 
481
- if (loaded.context) {
482
- changed.context = true;
1072
+ if (error) {
1073
+ while (i--) {
1074
+ if (b[i]) {
1075
+ let error_loaded;
483
1076
 
484
- context = {
485
- ...context,
486
- ...loaded.context
487
- };
1077
+ /** @type {import('./types').BranchNode | undefined} */
1078
+ let node_loaded;
1079
+ let j = i;
1080
+ while (!(node_loaded = branch[j])) {
1081
+ j -= 1;
488
1082
  }
489
1083
 
490
- if (loaded.maxage) {
491
- if (!this.caches.has(component)) {
492
- this.caches.set(component, new Map());
493
- }
494
-
495
- const cache = this.caches.get(component);
496
- const cached = { node, loaded };
497
-
498
- cache.set(hash, cached);
499
-
500
- let ready = false;
501
-
502
- const timeout = setTimeout(() => {
503
- clear();
504
- }, loaded.maxage * 1000);
505
-
506
- const clear = () => {
507
- if (cache.get(hash) === cached) {
508
- cache.delete(hash);
509
- }
510
-
511
- unsubscribe();
512
- clearTimeout(timeout);
513
- };
514
-
515
- const unsubscribe = this.stores.session.subscribe(() => {
516
- if (ready) clear();
1084
+ try {
1085
+ error_loaded = await this._load_node({
1086
+ status,
1087
+ error,
1088
+ module: await b[i](),
1089
+ page,
1090
+ stuff: node_loaded.stuff
517
1091
  });
518
1092
 
519
- ready = true;
520
- }
1093
+ if (error_loaded && error_loaded.loaded && error_loaded.loaded.error) {
1094
+ continue;
1095
+ }
521
1096
 
522
- props_promises[i] = loaded.props;
1097
+ branch = branch.slice(0, j + 1).concat(error_loaded);
1098
+ break load;
1099
+ } catch (e) {
1100
+ continue;
1101
+ }
523
1102
  }
524
-
525
- state.nodes[i] = node;
526
- } else {
527
- state.nodes[i] = previous;
528
1103
  }
529
- }
530
-
531
- const new_props = await Promise.all(props_promises);
532
1104
 
533
- new_props.forEach((p, i) => {
534
- if (p) {
535
- props[`props_${i}`] = p;
1105
+ return await this._load_error({
1106
+ status,
1107
+ error,
1108
+ path,
1109
+ query
1110
+ });
1111
+ } else {
1112
+ if (node && node.loaded && node.loaded.stuff) {
1113
+ stuff = {
1114
+ ...stuff,
1115
+ ...node.loaded.stuff
1116
+ };
536
1117
  }
537
- });
538
1118
 
539
- if (!this.current.page || page.path !== this.current.page.path) {
540
- props.page = page;
1119
+ branch.push(node);
541
1120
  }
542
- } catch (error) {
543
- props.error = error;
544
- props.status = 500;
545
- state.nodes = [];
546
1121
  }
547
1122
 
548
- return { redirect, props, state };
1123
+ return await this._get_navigation_result_from_branch({ page, branch });
549
1124
  }
550
1125
 
551
- async prefetch(url) {
552
- const page = this.router.select(url);
1126
+ /**
1127
+ * @param {{
1128
+ * status?: number;
1129
+ * error: Error;
1130
+ * path: string;
1131
+ * query: URLSearchParams
1132
+ * }} opts
1133
+ */
1134
+ async _load_error({ status, error, path, query }) {
1135
+ const page = {
1136
+ host: this.host,
1137
+ path,
1138
+ query,
1139
+ params: {}
1140
+ };
553
1141
 
554
- if (page) {
555
- if (url.href !== this.prefetching.href) {
556
- this.prefetching = { href: url.href, promise: this.hydrate(page) };
557
- }
1142
+ const node = await this._load_node({
1143
+ module: await this.fallback[0],
1144
+ page,
1145
+ stuff: {}
1146
+ });
558
1147
 
559
- return this.prefetching.promise;
560
- } else {
561
- throw new Error(`Could not prefetch ${url.href}`);
562
- }
1148
+ const branch = [
1149
+ node,
1150
+ await this._load_node({
1151
+ status,
1152
+ error,
1153
+ module: await this.fallback[1],
1154
+ page,
1155
+ stuff: (node && node.loaded && node.loaded.stuff) || {}
1156
+ })
1157
+ ];
1158
+
1159
+ return await this._get_navigation_result_from_branch({ page, branch });
563
1160
  }
564
1161
  }
565
1162
 
566
- async function start({ paths, target, host, session, preloaded, error, status }) {
567
- const router = new Router({
568
- base: paths.base,
569
- host,
570
- pages,
571
- ignore
572
- });
1163
+ // @ts-expect-error - doesn't exist yet. generated by Rollup
1164
+
1165
+ /**
1166
+ * @param {{
1167
+ * paths: {
1168
+ * assets: string;
1169
+ * base: string;
1170
+ * },
1171
+ * target: Node;
1172
+ * session: any;
1173
+ * host: string;
1174
+ * route: boolean;
1175
+ * spa: boolean;
1176
+ * trailing_slash: import('types/internal').TrailingSlash;
1177
+ * hydrate: {
1178
+ * status: number;
1179
+ * error: Error;
1180
+ * nodes: Array<Promise<import('types/internal').CSRComponent>>;
1181
+ * page: import('types/page').Page;
1182
+ * };
1183
+ * }} opts
1184
+ */
1185
+ async function start({ paths, target, session, host, route, spa, trailing_slash, hydrate }) {
1186
+ if (import.meta.env.DEV && !target) {
1187
+ throw new Error('Missing target element. See https://kit.svelte.dev/docs#configuration-target');
1188
+ }
573
1189
 
574
1190
  const renderer = new Renderer({
575
1191
  Root,
576
- layout,
1192
+ fallback,
577
1193
  target,
578
- preloaded,
579
- error,
580
- status,
581
- session
1194
+ session,
1195
+ host
582
1196
  });
583
1197
 
584
- init({ router, renderer });
1198
+ const router = route
1199
+ ? new Router({
1200
+ base: paths.base,
1201
+ routes,
1202
+ trailing_slash,
1203
+ renderer
1204
+ })
1205
+ : null;
1206
+
1207
+ init(router);
585
1208
  set_paths(paths);
586
1209
 
587
- await router.init({ renderer });
1210
+ if (hydrate) await renderer.start(hydrate);
1211
+ if (router) {
1212
+ if (spa) router.goto(location.href, { replaceState: true }, []);
1213
+ router.init_listeners();
1214
+ }
1215
+
1216
+ dispatchEvent(new CustomEvent('sveltekit:start'));
588
1217
  }
589
1218
 
590
1219
  export { start };
591
- //# sourceMappingURL=start.js.map