@sveltejs/kit 1.0.0-next.22 → 1.0.0-next.220

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