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

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