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

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 +1996 -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 +19 -15
  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 +1015 -354
  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 +996 -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 +1926 -0
  32. package/package.json +96 -54
  33. package/svelte-kit.js +2 -0
  34. package/types/ambient-modules.d.ts +191 -0
  35. package/types/app.d.ts +45 -0
  36. package/types/config.d.ts +168 -0
  37. package/types/endpoint.d.ts +20 -0
  38. package/types/helper.d.ts +53 -0
  39. package/types/hooks.d.ts +55 -0
  40. package/types/index.d.ts +18 -0
  41. package/types/internal.d.ts +237 -0
  42. package/types/page.d.ts +73 -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
+ }
197
+
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
+ }
129
214
 
130
- // load current page
131
- this.history.replaceState({}, '', location.href);
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
+ });
135
238
  }
136
239
 
137
- select(url) {
138
- if (url.origin !== location.origin) return null;
139
- if (!url.pathname.startsWith(this.base)) return null;
240
+ enable() {
241
+ this.enabled = true;
242
+ }
243
+
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 };
200
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
+ };
381
+ }
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
+ );
201
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;
244
471
 
472
+ this.session_id = 1;
473
+ this.invalid = new Set();
474
+ this.invalidating = null;
475
+ this.autoscroll = true;
476
+ this.updating = false;
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,750 @@ 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
+ }
523
+
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({
588
+ url,
589
+ params,
590
+ stuff,
591
+ branch,
592
+ status,
593
+ error
594
+ });
595
+ } catch (e) {
596
+ if (error) throw e;
597
+
598
+ result = await this._load_error({
599
+ status: 500,
600
+ error: coalesce_to_error(e),
601
+ url
602
+ });
603
+ }
604
+
605
+ if (result.redirect) {
606
+ // this is a real edge case — `load` would need to return
607
+ // a redirect but only in the browser
608
+ location.href = new URL(result.redirect, location.href).href;
609
+ return;
610
+ }
611
+
612
+ this._init(result);
613
+ }
614
+
615
+ /**
616
+ * @param {import('./types').NavigationInfo} info
617
+ * @param {string[]} chain
618
+ * @param {boolean} no_cache
619
+ * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
620
+ */
621
+ async handle_navigation(info, chain, no_cache, opts) {
622
+ if (this.started) {
623
+ this.stores.navigating.set({
624
+ from: this.current.url,
625
+ to: info.url
626
+ });
627
+ }
628
+
629
+ await this.update(info, chain, no_cache, opts);
630
+ }
631
+
632
+ /**
633
+ * @param {import('./types').NavigationInfo} info
634
+ * @param {string[]} chain
635
+ * @param {boolean} no_cache
636
+ * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
637
+ */
638
+ async update(info, chain, no_cache, opts) {
639
+ const token = (this.token = {});
640
+ let navigation_result = await this._get_navigation_result(info, no_cache);
641
+
642
+ // abort if user navigated during update
643
+ if (token !== this.token) return;
644
+
645
+ this.invalid.clear();
646
+
647
+ if (navigation_result.redirect) {
648
+ if (chain.length > 10 || chain.includes(info.url.pathname)) {
649
+ navigation_result = await this._load_error({
650
+ status: 500,
651
+ error: new Error('Redirect loop'),
652
+ url: info.url
653
+ });
654
+ } else {
655
+ if (this.router) {
656
+ this.router.goto(navigation_result.redirect, { replaceState: true }, [
657
+ ...chain,
658
+ info.url.pathname
659
+ ]);
660
+ } else {
661
+ location.href = new URL(navigation_result.redirect, location.href).href;
662
+ }
663
+
664
+ return;
665
+ }
666
+ }
667
+
668
+ this.updating = true;
308
669
 
309
- if (this.initial.error) {
310
- props.components = [this.layout.default];
670
+ if (this.started) {
671
+ this.current = navigation_result.state;
672
+
673
+ this.root.$set(navigation_result.props);
674
+ this.stores.navigating.set(null);
311
675
  } else {
312
- const hydrated = await this.hydrate(selected);
676
+ this._init(navigation_result);
677
+ }
313
678
 
314
- if (hydrated.redirect) {
315
- throw new Error('TODO client-side redirects');
679
+ // opts must be passed if we're navigating
680
+ if (opts) {
681
+ const { hash, scroll, keepfocus } = opts;
682
+
683
+ if (!keepfocus) {
684
+ getSelection()?.removeAllRanges();
685
+ document.body.focus();
316
686
  }
317
687
 
318
- Object.assign(props, hydrated.props);
319
- this.current = hydrated.state;
688
+ // need to render the DOM before we can scroll to the rendered elements
689
+ await tick();
690
+
691
+ if (this.autoscroll) {
692
+ const deep_linked = hash && document.getElementById(hash.slice(1));
693
+ if (scroll) {
694
+ scrollTo(scroll.x, scroll.y);
695
+ } else if (deep_linked) {
696
+ // Here we use `scrollIntoView` on the element instead of `scrollTo`
697
+ // because it natively supports the `scroll-margin` and `scroll-behavior`
698
+ // CSS properties.
699
+ deep_linked.scrollIntoView();
700
+ } else {
701
+ scrollTo(0, 0);
702
+ }
703
+ }
704
+ } else {
705
+ // in this case we're simply invalidating
706
+ await tick();
320
707
  }
321
708
 
709
+ this.loading.promise = null;
710
+ this.loading.id = null;
711
+ this.autoscroll = true;
712
+ this.updating = false;
713
+
714
+ if (!this.router) return;
715
+
716
+ const leaf_node = navigation_result.state.branch[navigation_result.state.branch.length - 1];
717
+ if (leaf_node && leaf_node.module.router === false) {
718
+ this.router.disable();
719
+ } else {
720
+ this.router.enable();
721
+ }
722
+ }
723
+
724
+ /**
725
+ * @param {import('./types').NavigationInfo} info
726
+ * @returns {Promise<import('./types').NavigationResult>}
727
+ */
728
+ load(info) {
729
+ this.loading.promise = this._get_navigation_result(info, false);
730
+ this.loading.id = info.id;
731
+
732
+ return this.loading.promise;
733
+ }
734
+
735
+ /** @param {string} href */
736
+ invalidate(href) {
737
+ this.invalid.add(href);
738
+
739
+ if (!this.invalidating) {
740
+ this.invalidating = Promise.resolve().then(async () => {
741
+ const info = this.router && this.router.parse(new URL(location.href));
742
+ if (info) await this.update(info, [], true);
743
+
744
+ this.invalidating = null;
745
+ });
746
+ }
747
+
748
+ return this.invalidating;
749
+ }
750
+
751
+ /** @param {import('./types').NavigationResult} result */
752
+ _init(result) {
753
+ this.current = result.state;
754
+
755
+ const style = document.querySelector('style[data-svelte]');
756
+ if (style) style.remove();
757
+
322
758
  this.root = new this.Root({
323
759
  target: this.target,
324
- props,
760
+ props: {
761
+ stores: this.stores,
762
+ ...result.props
763
+ },
325
764
  hydrate: true
326
765
  });
327
766
 
328
- this.initial = null;
767
+ this.started = true;
329
768
  }
330
769
 
331
- notify(selected) {
332
- this.stores.navigating.set({
333
- from: this.current.page,
334
- to: selected.page
770
+ /**
771
+ * @param {import('./types').NavigationInfo} info
772
+ * @param {boolean} no_cache
773
+ * @returns {Promise<import('./types').NavigationResult>}
774
+ */
775
+ async _get_navigation_result(info, no_cache) {
776
+ if (this.loading.id === info.id && this.loading.promise) {
777
+ return this.loading.promise;
778
+ }
779
+
780
+ for (let i = 0; i < info.routes.length; i += 1) {
781
+ const route = info.routes[i];
782
+
783
+ // load code for subsequent routes immediately, if they are as
784
+ // likely to match the current path/query as the current one
785
+ let j = i + 1;
786
+ while (j < info.routes.length) {
787
+ const next = info.routes[j];
788
+ if (next[0].toString() === route[0].toString()) {
789
+ next[1].forEach((loader) => loader());
790
+ j += 1;
791
+ } else {
792
+ break;
793
+ }
794
+ }
795
+
796
+ const result = await this._load(
797
+ {
798
+ route,
799
+ info
800
+ },
801
+ no_cache
802
+ );
803
+ if (result) return result;
804
+ }
805
+
806
+ return await this._load_error({
807
+ status: 404,
808
+ error: new Error(`Not found: ${info.url.pathname}`),
809
+ url: info.url
335
810
  });
336
811
  }
337
812
 
338
- async render(selected) {
339
- const token = (this.token = {});
813
+ /**
814
+ *
815
+ * @param {{
816
+ * url: URL;
817
+ * params: Record<string, string>;
818
+ * stuff: Record<string, any>;
819
+ * branch: Array<import('./types').BranchNode | undefined>;
820
+ * status: number;
821
+ * error?: Error;
822
+ * }} opts
823
+ */
824
+ async _get_navigation_result_from_branch({ url, params, stuff, branch, status, error }) {
825
+ const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
826
+ const redirect = filtered.find((f) => f.loaded && f.loaded.redirect);
827
+
828
+ /** @type {import('./types').NavigationResult} */
829
+ const result = {
830
+ redirect: redirect && redirect.loaded ? redirect.loaded.redirect : undefined,
831
+ state: {
832
+ url,
833
+ params,
834
+ branch,
835
+ session_id: this.session_id
836
+ },
837
+ props: {
838
+ components: filtered.map((node) => node.module.default)
839
+ }
840
+ };
841
+
842
+ for (let i = 0; i < filtered.length; i += 1) {
843
+ const loaded = filtered[i].loaded;
844
+ result.props[`props_${i}`] = loaded ? await loaded.props : null;
845
+ }
340
846
 
341
- const hydrated = await this.hydrate(selected);
847
+ if (!this.current.url || url.href !== this.current.url.href) {
848
+ result.props.page = { url, params, status, error, stuff };
849
+
850
+ // TODO remove this for 1.0
851
+ /**
852
+ * @param {string} property
853
+ * @param {string} replacement
854
+ */
855
+ const print_error = (property, replacement) => {
856
+ Object.defineProperty(result.props.page, property, {
857
+ get: () => {
858
+ throw new Error(`$page.${property} has been replaced by $page.url.${replacement}`);
859
+ }
860
+ });
861
+ };
342
862
 
343
- if (this.token === token) {
344
- // check render wasn't aborted
345
- this.current = hydrated.state;
863
+ print_error('origin', 'origin');
864
+ print_error('path', 'pathname');
865
+ print_error('query', 'searchParams');
866
+ }
346
867
 
347
- this.root.$set(hydrated.props);
348
- this.stores.navigating.set(null);
868
+ const leaf = filtered[filtered.length - 1];
869
+ const maxage = leaf.loaded && leaf.loaded.maxage;
870
+
871
+ if (maxage) {
872
+ const key = url.pathname + url.search; // omit hash
873
+ let ready = false;
874
+
875
+ const clear = () => {
876
+ if (this.cache.get(key) === result) {
877
+ this.cache.delete(key);
878
+ }
879
+
880
+ unsubscribe();
881
+ clearTimeout(timeout);
882
+ };
883
+
884
+ const timeout = setTimeout(clear, maxage * 1000);
885
+
886
+ const unsubscribe = this.stores.session.subscribe(() => {
887
+ if (ready) clear();
888
+ });
889
+
890
+ ready = true;
891
+
892
+ this.cache.set(key, result);
349
893
  }
894
+
895
+ return result;
350
896
  }
351
897
 
352
- async hydrate({ route, page }) {
353
- const props = {
354
- error: null,
355
- status: 200,
356
- components: []
898
+ /**
899
+ * @param {{
900
+ * status?: number;
901
+ * error?: Error;
902
+ * module: CSRComponent;
903
+ * url: URL;
904
+ * params: Record<string, string>;
905
+ * stuff: Record<string, any>;
906
+ * }} options
907
+ * @returns
908
+ */
909
+ async _load_node({ status, error, module, url, params, stuff }) {
910
+ /** @type {import('./types').BranchNode} */
911
+ const node = {
912
+ module,
913
+ uses: {
914
+ params: new Set(),
915
+ url: false,
916
+ session: false,
917
+ stuff: false,
918
+ dependencies: []
919
+ },
920
+ loaded: null,
921
+ stuff
357
922
  };
358
923
 
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));
924
+ /** @type {Record<string, string>} */
925
+ const uses_params = {};
926
+ for (const key in params) {
927
+ Object.defineProperty(uses_params, key, {
928
+ get() {
929
+ node.uses.params.add(key);
930
+ return params[key];
931
+ },
932
+ enumerable: true
933
+ });
934
+ }
935
+
936
+ const session = this.$session;
937
+
938
+ if (module.load) {
939
+ const { started } = this;
940
+
941
+ /** @type {import('types/page').LoadInput | import('types/page').ErrorLoadInput} */
942
+ const load_input = {
943
+ params: uses_params,
944
+ get url() {
945
+ node.uses.url = true;
946
+ return url;
947
+ },
948
+ get session() {
949
+ node.uses.session = true;
950
+ return session;
951
+ },
952
+ get stuff() {
953
+ node.uses.stuff = true;
954
+ return { ...stuff };
955
+ },
956
+ fetch(resource, info) {
957
+ const requested = typeof resource === 'string' ? resource : resource.url;
958
+ const { href } = new URL(requested, url);
959
+ node.uses.dependencies.push(href);
960
+
961
+ return started ? fetch(resource, info) : initial_fetch(resource, info);
365
962
  }
963
+ };
964
+
965
+ if (import.meta.env.DEV) {
966
+ // TODO remove this for 1.0
967
+ Object.defineProperty(load_input, 'page', {
968
+ get: () => {
969
+ throw new Error('`page` in `load` functions has been replaced by `url` and `params`');
970
+ }
971
+ });
366
972
  }
367
973
 
368
- return fetch(url, opts);
369
- };
974
+ if (error) {
975
+ /** @type {import('types/page').ErrorLoadInput} */ (load_input).status = status;
976
+ /** @type {import('types/page').ErrorLoadInput} */ (load_input).error = error;
977
+ }
370
978
 
371
- const query = page.query.toString();
979
+ const loaded = await module.load.call(null, load_input);
372
980
 
373
- const state = {
374
- page,
375
- query,
376
- session_changed: false,
377
- nodes: []
378
- };
981
+ if (!loaded) {
982
+ throw new Error('load function must return a value');
983
+ }
984
+
985
+ node.loaded = normalize(loaded);
986
+ if (node.loaded.stuff) node.stuff = node.loaded.stuff;
987
+ }
379
988
 
380
- const component_promises = [this.layout_loader(), ...route.parts.map((loader) => loader())];
381
- const props_promises = [];
989
+ return node;
990
+ }
991
+
992
+ /**
993
+ * @param {import('./types').NavigationCandidate} selected
994
+ * @param {boolean} no_cache
995
+ * @returns {Promise<import('./types').NavigationResult | undefined>} undefined if fallthrough
996
+ */
997
+ async _load({ route, info: { url, path } }, no_cache) {
998
+ const key = url.pathname + url.search;
999
+
1000
+ if (!no_cache) {
1001
+ const cached = this.cache.get(key);
1002
+ if (cached) return cached;
1003
+ }
382
1004
 
383
- let context = {};
384
- let redirect;
1005
+ const [pattern, a, b, get_params] = route;
1006
+ const params = get_params
1007
+ ? // the pattern is for the route which we've already matched to this path
1008
+ get_params(/** @type {RegExpExecArray} */ (pattern.exec(path)))
1009
+ : {};
385
1010
 
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
1011
+ const changed = this.current.url && {
1012
+ url: key !== this.current.url.pathname + this.current.url.search,
1013
+ params: Object.keys(params).filter((key) => this.current.params[key] !== params[key]),
1014
+ session: this.session_id !== this.current.session_id
393
1015
  };
394
1016
 
395
- try {
396
- for (let i = 0; i < component_promises.length; i += 1) {
397
- const previous = this.current.nodes[i];
1017
+ /** @type {Array<import('./types').BranchNode | undefined>} */
1018
+ let branch = [];
398
1019
 
399
- const { default: component, load } = await component_promises[i];
400
- props.components[i] = component;
1020
+ /** @type {Record<string, any>} */
1021
+ let stuff = {};
1022
+ let stuff_changed = false;
1023
+
1024
+ /** @type {number | undefined} */
1025
+ let status = 200;
1026
+
1027
+ /** @type {Error | undefined} */
1028
+ let error;
1029
+
1030
+ // preload modules
1031
+ a.forEach((loader) => loader());
1032
+
1033
+ load: for (let i = 0; i < a.length; i += 1) {
1034
+ /** @type {import('./types').BranchNode | undefined} */
1035
+ let node;
1036
+
1037
+ try {
1038
+ if (!a[i]) continue;
1039
+
1040
+ const module = await a[i]();
1041
+ const previous = this.current.branch[i];
401
1042
 
402
1043
  const changed_since_last_render =
403
1044
  !previous ||
404
- component !== previous.component ||
1045
+ module !== previous.module ||
1046
+ (changed.url && previous.uses.url) ||
405
1047
  changed.params.some((param) => previous.uses.params.has(param)) ||
406
- (changed.query && previous.uses.query) ||
407
1048
  (changed.session && previous.uses.session) ||
408
- (changed.context && previous.uses.context);
1049
+ previous.uses.dependencies.some((dep) => this.invalid.has(dep)) ||
1050
+ (stuff_changed && previous.uses.stuff);
409
1051
 
410
1052
  if (changed_since_last_render) {
411
- const hash = page.path + query;
412
-
413
- // see if we have some cached data
414
- const cache = this.caches.get(component);
415
- const cached = cache && cache.get(hash);
416
-
417
- let node;
418
- let loaded;
419
-
420
- if (cached && (!changed.context || !cached.node.uses.context)) {
421
- ({ node, loaded } = cached);
422
- } else {
423
- node = {
424
- component,
425
- uses: {
426
- params: new Set(),
427
- query: false,
428
- session: false,
429
- context: false
430
- }
431
- };
432
-
433
- const params = {};
434
- for (const key in page.params) {
435
- Object.defineProperty(params, key, {
436
- get() {
437
- node.uses.params.add(key);
438
- return page.params[key];
439
- },
440
- enumerable: true
441
- });
442
- }
443
-
444
- const session = this.$session;
445
-
446
- loaded =
447
- load &&
448
- (await load.call(null, {
449
- page: {
450
- ...page,
451
- params,
452
- get query() {
453
- node.uses.query = true;
454
- return page.query;
455
- }
456
- },
457
- get session() {
458
- node.uses.session = true;
459
- return session;
460
- },
461
- get context() {
462
- node.uses.context = true;
463
- return { ...context };
464
- },
465
- fetch: fetcher
466
- }));
467
- }
468
-
469
- if (loaded) {
470
- if (loaded.error) {
471
- const error = new Error(loaded.error.message);
472
- error.status = loaded.error.status;
473
- throw error;
1053
+ node = await this._load_node({
1054
+ module,
1055
+ url,
1056
+ params,
1057
+ stuff
1058
+ });
1059
+
1060
+ if (node && node.loaded) {
1061
+ if (node.loaded.fallthrough) {
1062
+ return;
474
1063
  }
475
-
476
- if (loaded.redirect) {
477
- redirect = loaded.redirect;
478
- break;
1064
+ if (node.loaded.error) {
1065
+ status = node.loaded.status;
1066
+ error = node.loaded.error;
479
1067
  }
480
1068
 
481
- if (loaded.context) {
482
- changed.context = true;
483
-
484
- context = {
485
- ...context,
486
- ...loaded.context
1069
+ if (node.loaded.redirect) {
1070
+ return {
1071
+ redirect: node.loaded.redirect,
1072
+ props: {},
1073
+ state: this.current
487
1074
  };
488
1075
  }
489
1076
 
490
- if (loaded.maxage) {
491
- if (!this.caches.has(component)) {
492
- this.caches.set(component, new Map());
493
- }
494
-
495
- const cache = this.caches.get(component);
496
- const cached = { node, loaded };
497
-
498
- cache.set(hash, cached);
1077
+ if (node.loaded.stuff) {
1078
+ stuff_changed = true;
1079
+ }
1080
+ }
1081
+ } else {
1082
+ node = previous;
1083
+ }
1084
+ } catch (e) {
1085
+ status = 500;
1086
+ error = coalesce_to_error(e);
1087
+ }
499
1088
 
500
- let ready = false;
1089
+ if (error) {
1090
+ while (i--) {
1091
+ if (b[i]) {
1092
+ let error_loaded;
501
1093
 
502
- const timeout = setTimeout(() => {
503
- clear();
504
- }, loaded.maxage * 1000);
1094
+ /** @type {import('./types').BranchNode | undefined} */
1095
+ let node_loaded;
1096
+ let j = i;
1097
+ while (!(node_loaded = branch[j])) {
1098
+ j -= 1;
1099
+ }
505
1100
 
506
- const clear = () => {
507
- if (cache.get(hash) === cached) {
508
- cache.delete(hash);
509
- }
1101
+ try {
1102
+ error_loaded = await this._load_node({
1103
+ status,
1104
+ error,
1105
+ module: await b[i](),
1106
+ url,
1107
+ params,
1108
+ stuff: node_loaded.stuff
1109
+ });
510
1110
 
511
- unsubscribe();
512
- clearTimeout(timeout);
513
- };
1111
+ if (error_loaded && error_loaded.loaded && error_loaded.loaded.error) {
1112
+ continue;
1113
+ }
514
1114
 
515
- const unsubscribe = this.stores.session.subscribe(() => {
516
- if (ready) clear();
517
- });
1115
+ if (error_loaded && error_loaded.loaded && error_loaded.loaded.stuff) {
1116
+ stuff = {
1117
+ ...stuff,
1118
+ ...error_loaded.loaded.stuff
1119
+ };
1120
+ }
518
1121
 
519
- ready = true;
1122
+ branch = branch.slice(0, j + 1).concat(error_loaded);
1123
+ break load;
1124
+ } catch (e) {
1125
+ continue;
520
1126
  }
521
-
522
- props_promises[i] = loaded.props;
523
1127
  }
524
-
525
- state.nodes[i] = node;
526
- } else {
527
- state.nodes[i] = previous;
528
1128
  }
529
- }
530
1129
 
531
- const new_props = await Promise.all(props_promises);
532
-
533
- new_props.forEach((p, i) => {
534
- if (p) {
535
- props[`props_${i}`] = p;
1130
+ return await this._load_error({
1131
+ status,
1132
+ error,
1133
+ url
1134
+ });
1135
+ } else {
1136
+ if (node && node.loaded && node.loaded.stuff) {
1137
+ stuff = {
1138
+ ...stuff,
1139
+ ...node.loaded.stuff
1140
+ };
536
1141
  }
537
- });
538
1142
 
539
- if (!this.current.page || page.path !== this.current.page.path) {
540
- props.page = page;
1143
+ branch.push(node);
541
1144
  }
542
- } catch (error) {
543
- props.error = error;
544
- props.status = 500;
545
- state.nodes = [];
546
1145
  }
547
1146
 
548
- return { redirect, props, state };
1147
+ return await this._get_navigation_result_from_branch({
1148
+ url,
1149
+ params,
1150
+ stuff,
1151
+ branch,
1152
+ status,
1153
+ error
1154
+ });
549
1155
  }
550
1156
 
551
- async prefetch(url) {
552
- const page = this.router.select(url);
1157
+ /**
1158
+ * @param {{
1159
+ * status: number;
1160
+ * error: Error;
1161
+ * url: URL;
1162
+ * }} opts
1163
+ */
1164
+ async _load_error({ status, error, url }) {
1165
+ /** @type {Record<string, string>} */
1166
+ const params = {}; // error page does not have params
1167
+
1168
+ const node = await this._load_node({
1169
+ module: await this.fallback[0],
1170
+ url,
1171
+ params,
1172
+ stuff: {}
1173
+ });
1174
+ const error_node = await this._load_node({
1175
+ status,
1176
+ error,
1177
+ module: await this.fallback[1],
1178
+ url,
1179
+ params,
1180
+ stuff: (node && node.loaded && node.loaded.stuff) || {}
1181
+ });
553
1182
 
554
- if (page) {
555
- if (url.href !== this.prefetching.href) {
556
- this.prefetching = { href: url.href, promise: this.hydrate(page) };
557
- }
1183
+ const branch = [node, error_node];
1184
+ const stuff = { ...node?.loaded?.stuff, ...error_node?.loaded?.stuff };
558
1185
 
559
- return this.prefetching.promise;
560
- } else {
561
- throw new Error(`Could not prefetch ${url.href}`);
562
- }
1186
+ return await this._get_navigation_result_from_branch({
1187
+ url,
1188
+ params,
1189
+ stuff,
1190
+ branch,
1191
+ status,
1192
+ error
1193
+ });
563
1194
  }
564
1195
  }
565
1196
 
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
- });
1197
+ // @ts-expect-error - doesn't exist yet. generated by Rollup
1198
+
1199
+ /**
1200
+ * @param {{
1201
+ * paths: {
1202
+ * assets: string;
1203
+ * base: string;
1204
+ * },
1205
+ * target: Node;
1206
+ * session: any;
1207
+ * route: boolean;
1208
+ * spa: boolean;
1209
+ * trailing_slash: import('types/internal').TrailingSlash;
1210
+ * hydrate: {
1211
+ * status: number;
1212
+ * error: Error;
1213
+ * nodes: Array<Promise<import('types/internal').CSRComponent>>;
1214
+ * url: URL;
1215
+ * params: Record<string, string>;
1216
+ * };
1217
+ * }} opts
1218
+ */
1219
+ async function start({ paths, target, session, route, spa, trailing_slash, hydrate }) {
1220
+ if (import.meta.env.DEV && !target) {
1221
+ throw new Error('Missing target element. See https://kit.svelte.dev/docs#configuration-target');
1222
+ }
573
1223
 
574
1224
  const renderer = new Renderer({
575
1225
  Root,
576
- layout,
1226
+ fallback,
577
1227
  target,
578
- preloaded,
579
- error,
580
- status,
581
1228
  session
582
1229
  });
583
1230
 
1231
+ const router = route
1232
+ ? new Router({
1233
+ base: paths.base,
1234
+ routes,
1235
+ trailing_slash,
1236
+ renderer
1237
+ })
1238
+ : null;
1239
+
584
1240
  init({ router, renderer });
585
1241
  set_paths(paths);
586
1242
 
587
- await router.init({ renderer });
1243
+ if (hydrate) await renderer.start(hydrate);
1244
+ if (router) {
1245
+ if (spa) router.goto(location.href, { replaceState: true }, []);
1246
+ router.init_listeners();
1247
+ }
1248
+
1249
+ dispatchEvent(new CustomEvent('sveltekit:start'));
588
1250
  }
589
1251
 
590
1252
  export { start };
591
- //# sourceMappingURL=start.js.map