@sveltejs/kit 1.0.0-next.21 → 1.0.0-next.210

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