@sveltejs/kit 1.0.0-next.222 → 1.0.0-next.227

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.
package/assets/kit.js CHANGED
@@ -567,7 +567,8 @@ async function render_response({
567
567
  }) {
568
568
  const css = new Set(options.manifest._.entry.css);
569
569
  const js = new Set(options.manifest._.entry.js);
570
- const styles = new Set();
570
+ /** @type {Map<string, string>} */
571
+ const styles = new Map();
571
572
 
572
573
  /** @type {Array<{ url: string, body: string, json: string }>} */
573
574
  const serialized_data = [];
@@ -585,7 +586,7 @@ async function render_response({
585
586
  branch.forEach(({ node, loaded, fetched, uses_credentials }) => {
586
587
  if (node.css) node.css.forEach((url) => css.add(url));
587
588
  if (node.js) node.js.forEach((url) => js.add(url));
588
- if (node.styles) node.styles.forEach((content) => styles.add(content));
589
+ if (node.styles) Object.entries(node.styles).forEach(([k, v]) => styles.set(k, v));
589
590
 
590
591
  // TODO probably better if `fetched` wasn't populated unless `hydrate`
591
592
  if (fetched && page_config.hydrate) serialized_data.push(...fetched);
@@ -646,94 +647,80 @@ async function render_response({
646
647
  rendered = { head: '', html: '', css: { code: '', map: null } };
647
648
  }
648
649
 
649
- const include_js = page_config.router || page_config.hydrate;
650
- if (!include_js) js.clear();
651
-
652
- // TODO strip the AMP stuff out of the build if not relevant
653
- const links = options.amp
654
- ? styles.size > 0 || rendered.css.code.length > 0
655
- ? `<style amp-custom>${Array.from(styles).concat(rendered.css.code).join('\n')}</style>`
656
- : ''
657
- : [
658
- // From https://web.dev/priority-hints/:
659
- // Generally, preloads will load in the order the parser gets to them for anything above "Medium" priority
660
- // Thus, we should list CSS first
661
- ...Array.from(css).map((dep) => `<link rel="stylesheet" href="${options.prefix}${dep}">`),
662
- ...Array.from(js).map((dep) => `<link rel="modulepreload" href="${options.prefix}${dep}">`)
663
- ].join('\n\t\t');
664
-
665
- /** @type {string} */
666
- let init = '';
650
+ let { head, html: body } = rendered;
651
+
652
+ const inlined_style = Array.from(styles.values()).join('\n');
667
653
 
668
654
  if (options.amp) {
669
- init = `
655
+ head += `
670
656
  <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style>
671
657
  <noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
672
- <script async src="https://cdn.ampproject.org/v0.js"></script>`;
673
- init += options.service_worker
674
- ? '<script async custom-element="amp-install-serviceworker" src="https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js"></script>'
675
- : '';
676
- } else if (include_js) {
677
- // prettier-ignore
678
- init = `<script type="module">
679
- import { start } from ${s(options.prefix + options.manifest._.entry.file)};
680
- start({
681
- target: ${options.target ? `document.querySelector(${s(options.target)})` : 'document.body'},
682
- paths: ${s(options.paths)},
683
- session: ${try_serialize($session, (error) => {
684
- throw new Error(`Failed to serialize session data: ${error.message}`);
685
- })},
686
- route: ${!!page_config.router},
687
- spa: ${!ssr},
688
- trailing_slash: ${s(options.trailing_slash)},
689
- hydrate: ${ssr && page_config.hydrate ? `{
690
- status: ${status},
691
- error: ${serialize_error(error)},
692
- nodes: [
693
- ${(branch || [])
694
- .map(({ node }) => `import(${s(options.prefix + node.entry)})`)
695
- .join(',\n\t\t\t\t\t\t')}
696
- ],
697
- url: new URL(${s(url.href)}),
698
- params: ${devalue(params)}
699
- }` : 'null'}
700
- });
701
- </script>`;
702
- }
703
-
704
- if (options.service_worker && !options.amp) {
705
- init += `<script>
706
- if ('serviceWorker' in navigator) {
707
- navigator.serviceWorker.register('${options.service_worker}');
708
- }
709
- </script>`;
710
- }
658
+ <script async src="https://cdn.ampproject.org/v0.js"></script>
711
659
 
712
- const head = [
713
- rendered.head,
714
- styles.size && !options.amp
715
- ? `<style data-svelte>${Array.from(styles).join('\n')}</style>`
716
- : '',
717
- links,
718
- init
719
- ].join('\n\n\t\t');
660
+ <style amp-custom>${inlined_style}\n${rendered.css.code}</style>`;
720
661
 
721
- let body = rendered.html;
722
- if (options.amp) {
723
662
  if (options.service_worker) {
663
+ head +=
664
+ '<script async custom-element="amp-install-serviceworker" src="https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js"></script>';
665
+
724
666
  body += `<amp-install-serviceworker src="${options.service_worker}" layout="nodisplay"></amp-install-serviceworker>`;
725
667
  }
726
668
  } else {
727
- body += serialized_data
728
- .map(({ url, body, json }) => {
729
- let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr(
730
- url
731
- )}`;
732
- if (body) attributes += ` data-body="${hash(body)}"`;
733
-
734
- return `<script ${attributes}>${json}</script>`;
735
- })
736
- .join('\n\n\t');
669
+ if (inlined_style) {
670
+ head += `\n\t<style${options.dev ? ' data-svelte' : ''}>${inlined_style}</style>`;
671
+ }
672
+ // prettier-ignore
673
+ head += Array.from(css)
674
+ .map((dep) => `\n\t<link${styles.has(dep) ? ' disabled' : ''} rel="stylesheet" href="${options.prefix + dep}">`)
675
+ .join('');
676
+
677
+ if (page_config.router || page_config.hydrate) {
678
+ head += Array.from(js)
679
+ .map((dep) => `\n\t<link rel="modulepreload" href="${options.prefix + dep}">`)
680
+ .join('');
681
+ // prettier-ignore
682
+ head += `
683
+ <script type="module">
684
+ import { start } from ${s(options.prefix + options.manifest._.entry.file)};
685
+ start({
686
+ target: ${options.target ? `document.querySelector(${s(options.target)})` : 'document.body'},
687
+ paths: ${s(options.paths)},
688
+ session: ${try_serialize($session, (error) => {
689
+ throw new Error(`Failed to serialize session data: ${error.message}`);
690
+ })},
691
+ route: ${!!page_config.router},
692
+ spa: ${!ssr},
693
+ trailing_slash: ${s(options.trailing_slash)},
694
+ hydrate: ${ssr && page_config.hydrate ? `{
695
+ status: ${status},
696
+ error: ${serialize_error(error)},
697
+ nodes: [
698
+ ${(branch || [])
699
+ .map(({ node }) => `import(${s(options.prefix + node.entry)})`)
700
+ .join(',\n\t\t\t\t\t\t')}
701
+ ],
702
+ url: new URL(${s(url.href)}),
703
+ params: ${devalue(params)}
704
+ }` : 'null'}
705
+ });
706
+ </script>${options.service_worker ? `
707
+ <script>
708
+ if ('serviceWorker' in navigator) {
709
+ navigator.serviceWorker.register('${options.service_worker}');
710
+ }
711
+ </script>` : ''}`;
712
+
713
+ body += serialized_data
714
+ .map(({ url, body, json }) => {
715
+ let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr(
716
+ url
717
+ )}`;
718
+ if (body) attributes += ` data-body="${hash(body)}"`;
719
+
720
+ return `<script ${attributes}>${json}</script>`;
721
+ })
722
+ .join('\n\n\t');
723
+ }
737
724
  }
738
725
 
739
726
  /** @type {import('types/helper').ResponseHeaders} */
@@ -1641,8 +1628,9 @@ function read_only_form_data() {
1641
1628
  * @param {string} value
1642
1629
  */
1643
1630
  append(key, value) {
1644
- if (map.has(key)) {
1645
- (map.get(key) || []).push(value);
1631
+ const existing_values = map.get(key);
1632
+ if (existing_values) {
1633
+ existing_values.push(value);
1646
1634
  } else {
1647
1635
  map.set(key, [value]);
1648
1636
  }
@@ -1664,12 +1652,15 @@ class ReadOnlyFormData {
1664
1652
  /** @param {string} key */
1665
1653
  get(key) {
1666
1654
  const value = this.#map.get(key);
1667
- return value && value[0];
1655
+ if (!value) {
1656
+ return null;
1657
+ }
1658
+ return value[0];
1668
1659
  }
1669
1660
 
1670
1661
  /** @param {string} key */
1671
1662
  getAll(key) {
1672
- return this.#map.get(key);
1663
+ return this.#map.get(key) || [];
1673
1664
  }
1674
1665
 
1675
1666
  /** @param {string} key */
@@ -1849,6 +1840,28 @@ async function respond(incoming, options, state = {}) {
1849
1840
  locals: {}
1850
1841
  };
1851
1842
 
1843
+ const { parameter, allowed } = options.method_override;
1844
+ const method_override = incoming.url.searchParams.get(parameter)?.toUpperCase();
1845
+
1846
+ if (method_override) {
1847
+ if (request.method.toUpperCase() === 'POST') {
1848
+ if (allowed.includes(method_override)) {
1849
+ request.method = method_override;
1850
+ } else {
1851
+ const verb = allowed.length === 0 ? 'enabled' : 'allowed';
1852
+ const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs#configuration-methodoverride`;
1853
+
1854
+ return {
1855
+ status: 400,
1856
+ headers: {},
1857
+ body
1858
+ };
1859
+ }
1860
+ } else {
1861
+ throw new Error(`${parameter}=${method_override} is only allowed with POST requests`);
1862
+ }
1863
+ }
1864
+
1852
1865
  // TODO remove this for 1.0
1853
1866
  /**
1854
1867
  * @param {string} property
@@ -19,6 +19,8 @@ const goto = import.meta.env.SSR ? guard('goto') : goto_;
19
19
  const invalidate = import.meta.env.SSR ? guard('invalidate') : invalidate_;
20
20
  const prefetch = import.meta.env.SSR ? guard('prefetch') : prefetch_;
21
21
  const prefetchRoutes = import.meta.env.SSR ? guard('prefetchRoutes') : prefetchRoutes_;
22
+ const beforeNavigate = import.meta.env.SSR ? () => {} : beforeNavigate_;
23
+ const afterNavigate = import.meta.env.SSR ? () => {} : afterNavigate_;
22
24
 
23
25
  /**
24
26
  * @type {import('$app/navigation').goto}
@@ -62,4 +64,18 @@ async function prefetchRoutes_(pathnames) {
62
64
  await Promise.all(promises);
63
65
  }
64
66
 
65
- export { disableScrollHandling, goto, invalidate, prefetch, prefetchRoutes };
67
+ /**
68
+ * @type {import('$app/navigation').beforeNavigate}
69
+ */
70
+ function beforeNavigate_(fn) {
71
+ if (router) router.before_navigate(fn);
72
+ }
73
+
74
+ /**
75
+ * @type {import('$app/navigation').afterNavigate}
76
+ */
77
+ function afterNavigate_(fn) {
78
+ if (router) router.after_navigate(fn);
79
+ }
80
+
81
+ export { afterNavigate, beforeNavigate, disableScrollHandling, goto, invalidate, prefetch, prefetchRoutes };
@@ -1,7 +1,7 @@
1
1
  import Root from '../../generated/root.svelte';
2
2
  import { fallback, routes } from '../../generated/manifest.js';
3
+ import { onMount, tick } from 'svelte';
3
4
  import { g as get_base_uri } from '../chunks/utils.js';
4
- import { tick } from 'svelte';
5
5
  import { writable } from 'svelte/store';
6
6
  import { init } from './singletons.js';
7
7
  import { set_paths } from '../paths.js';
@@ -59,8 +59,21 @@ class Router {
59
59
  // make it possible to reset focus
60
60
  document.body.setAttribute('tabindex', '-1');
61
61
 
62
- // create initial history entry, so we can return here
63
- history.replaceState(history.state || {}, '', location.href);
62
+ // keeping track of the history index in order to prevent popstate navigation events if needed
63
+ this.current_history_index = history.state?.['sveltekit:index'] ?? 0;
64
+
65
+ if (this.current_history_index === 0) {
66
+ // create initial history entry, so we can return here
67
+ history.replaceState({ ...history.state, 'sveltekit:index': 0 }, '', location.href);
68
+ }
69
+
70
+ this.callbacks = {
71
+ /** @type {Array<({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void>} */
72
+ before_navigate: [],
73
+
74
+ /** @type {Array<({ from, to }: { from: URL | null, to: URL }) => void>} */
75
+ after_navigate: []
76
+ };
64
77
  }
65
78
 
66
79
  init_listeners() {
@@ -72,8 +85,23 @@ class Router {
72
85
  // Reset scrollRestoration to auto when leaving page, allowing page reload
73
86
  // and back-navigation from other pages to use the browser to restore the
74
87
  // scrolling position.
75
- addEventListener('beforeunload', () => {
76
- history.scrollRestoration = 'auto';
88
+ addEventListener('beforeunload', (e) => {
89
+ let should_block = false;
90
+
91
+ const intent = {
92
+ from: this.renderer.current.url,
93
+ to: null,
94
+ cancel: () => (should_block = true)
95
+ };
96
+
97
+ this.callbacks.before_navigate.forEach((fn) => fn(intent));
98
+
99
+ if (should_block) {
100
+ e.preventDefault();
101
+ e.returnValue = '';
102
+ } else {
103
+ history.scrollRestoration = 'auto';
104
+ }
77
105
  });
78
106
 
79
107
  // Setting scrollRestoration to manual again when returning to this page.
@@ -128,7 +156,7 @@ class Router {
128
156
  addEventListener('sveltekit:trigger_prefetch', trigger_prefetch);
129
157
 
130
158
  /** @param {MouseEvent} event */
131
- addEventListener('click', (event) => {
159
+ addEventListener('click', async (event) => {
132
160
  if (!this.enabled) return;
133
161
 
134
162
  // Adapted from https://github.com/visionmedia/page.js
@@ -161,8 +189,6 @@ class Router {
161
189
  // Ignore if <a> has a target
162
190
  if (a instanceof SVGAElement ? a.target.baseVal : a.target) return;
163
191
 
164
- if (!this.owns(url)) return;
165
-
166
192
  // Check if new url only differs by hash
167
193
  if (url.href.split('#')[0] === location.href.split('#')[0]) {
168
194
  // Call `pushState` to add url to history so going back works.
@@ -175,22 +201,48 @@ class Router {
175
201
  return;
176
202
  }
177
203
 
178
- history.pushState({}, '', url.href);
179
-
180
- const noscroll = a.hasAttribute('sveltekit:noscroll');
181
- this._navigate(url, noscroll ? scroll_state() : null, false, [], url.hash);
182
- event.preventDefault();
204
+ this._navigate({
205
+ url,
206
+ scroll: a.hasAttribute('sveltekit:noscroll') ? scroll_state() : null,
207
+ keepfocus: false,
208
+ chain: [],
209
+ details: {
210
+ state: {},
211
+ replaceState: false
212
+ },
213
+ accepted: () => event.preventDefault(),
214
+ blocked: () => event.preventDefault()
215
+ });
183
216
  });
184
217
 
185
218
  addEventListener('popstate', (event) => {
186
219
  if (event.state && this.enabled) {
187
- const url = new URL(location.href);
188
- this._navigate(url, event.state['sveltekit:scroll'], false, []);
220
+ // if a popstate-driven navigation is cancelled, we need to counteract it
221
+ // with history.go, which means we end up back here, hence this check
222
+ if (event.state['sveltekit:index'] === this.current_history_index) return;
223
+
224
+ this._navigate({
225
+ url: new URL(location.href),
226
+ scroll: event.state['sveltekit:scroll'],
227
+ keepfocus: false,
228
+ chain: [],
229
+ details: null,
230
+ accepted: () => {
231
+ this.current_history_index = event.state['sveltekit:index'];
232
+ },
233
+ blocked: () => {
234
+ const delta = this.current_history_index - event.state['sveltekit:index'];
235
+ history.go(delta);
236
+ }
237
+ });
189
238
  }
190
239
  });
191
240
  }
192
241
 
193
- /** @param {URL} url */
242
+ /**
243
+ * Returns true if `url` has the same origin and basepath as the app
244
+ * @param {URL} url
245
+ */
194
246
  owns(url) {
195
247
  return url.origin === location.origin && url.pathname.startsWith(this.base);
196
248
  }
@@ -226,9 +278,19 @@ class Router {
226
278
  ) {
227
279
  const url = new URL(href, get_base_uri(document));
228
280
 
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);
281
+ if (this.enabled) {
282
+ return this._navigate({
283
+ url,
284
+ scroll: noscroll ? scroll_state() : null,
285
+ keepfocus,
286
+ chain,
287
+ details: {
288
+ state,
289
+ replaceState
290
+ },
291
+ accepted: () => {},
292
+ blocked: () => {}
293
+ });
232
294
  }
233
295
 
234
296
  location.href = url.href;
@@ -259,20 +321,73 @@ class Router {
259
321
  return this.renderer.load(info);
260
322
  }
261
323
 
324
+ /** @param {({ from, to }: { from: URL | null, to: URL }) => void} fn */
325
+ after_navigate(fn) {
326
+ onMount(() => {
327
+ this.callbacks.after_navigate.push(fn);
328
+
329
+ return () => {
330
+ const i = this.callbacks.after_navigate.indexOf(fn);
331
+ this.callbacks.after_navigate.splice(i, 1);
332
+ };
333
+ });
334
+ }
335
+
262
336
  /**
263
- * @param {URL} url
264
- * @param {{ x: number, y: number }?} scroll
265
- * @param {boolean} keepfocus
266
- * @param {string[]} chain
267
- * @param {string} [hash]
337
+ * @param {({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void} fn
268
338
  */
269
- async _navigate(url, scroll, keepfocus, chain, hash) {
270
- const info = this.parse(url);
339
+ before_navigate(fn) {
340
+ onMount(() => {
341
+ this.callbacks.before_navigate.push(fn);
342
+
343
+ return () => {
344
+ const i = this.callbacks.before_navigate.indexOf(fn);
345
+ this.callbacks.before_navigate.splice(i, 1);
346
+ };
347
+ });
348
+ }
349
+
350
+ /**
351
+ * @param {{
352
+ * url: URL;
353
+ * scroll: { x: number, y: number } | null;
354
+ * keepfocus: boolean;
355
+ * chain: string[];
356
+ * details: {
357
+ * replaceState: boolean;
358
+ * state: any;
359
+ * } | null;
360
+ * accepted: () => void;
361
+ * blocked: () => void;
362
+ * }} opts
363
+ */
364
+ async _navigate({ url, scroll, keepfocus, chain, details, accepted, blocked }) {
365
+ const from = this.renderer.current.url;
366
+ let should_block = false;
367
+
368
+ const intent = {
369
+ from,
370
+ to: url,
371
+ cancel: () => (should_block = true)
372
+ };
373
+
374
+ this.callbacks.before_navigate.forEach((fn) => fn(intent));
271
375
 
376
+ if (should_block) {
377
+ blocked();
378
+ return;
379
+ }
380
+
381
+ const info = this.parse(url);
272
382
  if (!info) {
273
- throw new Error('Attempted to navigate to a URL that does not belong to this app');
383
+ location.href = url.href;
384
+ return new Promise(() => {
385
+ // never resolves
386
+ });
274
387
  }
275
388
 
389
+ accepted();
390
+
276
391
  if (!this.navigating) {
277
392
  dispatchEvent(new CustomEvent('sveltekit:navigation-start'));
278
393
  }
@@ -288,13 +403,24 @@ class Router {
288
403
  }
289
404
 
290
405
  info.url = new URL(url.origin + pathname + url.search + url.hash);
291
- history.replaceState({}, '', info.url);
292
406
 
293
- await this.renderer.handle_navigation(info, chain, false, { hash, scroll, keepfocus });
407
+ if (details) {
408
+ const change = details.replaceState ? 0 : 1;
409
+ details.state['sveltekit:index'] = this.current_history_index += change;
410
+ history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', info.url);
411
+ }
412
+
413
+ await this.renderer.handle_navigation(info, chain, false, {
414
+ scroll,
415
+ keepfocus
416
+ });
294
417
 
295
418
  this.navigating--;
296
419
  if (!this.navigating) {
297
420
  dispatchEvent(new CustomEvent('sveltekit:navigation-end'));
421
+
422
+ const navigation = { from, to: url };
423
+ this.callbacks.after_navigate.forEach((fn) => fn(navigation));
298
424
  }
299
425
  }
300
426
  }
@@ -653,10 +779,11 @@ class Renderer {
653
779
  });
654
780
  } else {
655
781
  if (this.router) {
656
- this.router.goto(navigation_result.redirect, { replaceState: true }, [
657
- ...chain,
658
- info.url.pathname
659
- ]);
782
+ this.router.goto(
783
+ new URL(navigation_result.redirect, info.url).href,
784
+ { replaceState: true },
785
+ [...chain, info.url.pathname]
786
+ );
660
787
  } else {
661
788
  location.href = new URL(navigation_result.redirect, location.href).href;
662
789
  }
@@ -678,7 +805,7 @@ class Renderer {
678
805
 
679
806
  // opts must be passed if we're navigating
680
807
  if (opts) {
681
- const { hash, scroll, keepfocus } = opts;
808
+ const { scroll, keepfocus } = opts;
682
809
 
683
810
  if (!keepfocus) {
684
811
  getSelection()?.removeAllRanges();
@@ -689,7 +816,7 @@ class Renderer {
689
816
  await tick();
690
817
 
691
818
  if (this.autoscroll) {
692
- const deep_linked = hash && document.getElementById(hash.slice(1));
819
+ const deep_linked = info.url.hash && document.getElementById(info.url.hash.slice(1));
693
820
  if (scroll) {
694
821
  scrollTo(scroll.x, scroll.y);
695
822
  } else if (deep_linked) {
@@ -765,6 +892,11 @@ class Renderer {
765
892
  });
766
893
 
767
894
  this.started = true;
895
+
896
+ if (this.router) {
897
+ const navigation = { from: null, to: new URL(location.href) };
898
+ this.router.callbacks.after_navigate.forEach((fn) => fn(navigation));
899
+ }
768
900
  }
769
901
 
770
902
  /**