@sveltejs/kit 1.0.0-next.225 → 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
@@ -1628,8 +1628,9 @@ function read_only_form_data() {
1628
1628
  * @param {string} value
1629
1629
  */
1630
1630
  append(key, value) {
1631
- if (map.has(key)) {
1632
- (map.get(key) || []).push(value);
1631
+ const existing_values = map.get(key);
1632
+ if (existing_values) {
1633
+ existing_values.push(value);
1633
1634
  } else {
1634
1635
  map.set(key, [value]);
1635
1636
  }
@@ -1651,12 +1652,15 @@ class ReadOnlyFormData {
1651
1652
  /** @param {string} key */
1652
1653
  get(key) {
1653
1654
  const value = this.#map.get(key);
1654
- return value && value[0];
1655
+ if (!value) {
1656
+ return null;
1657
+ }
1658
+ return value[0];
1655
1659
  }
1656
1660
 
1657
1661
  /** @param {string} key */
1658
1662
  getAll(key) {
1659
- return this.#map.get(key);
1663
+ return this.#map.get(key) || [];
1660
1664
  }
1661
1665
 
1662
1666
  /** @param {string} key */
@@ -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
  /**
@@ -256,9 +256,22 @@ function crawl(html) {
256
256
  } else if (name === 'src') {
257
257
  hrefs.push(value);
258
258
  } else if (name === 'srcset') {
259
- const candidates = value.split(',');
259
+ const candidates = [];
260
+ let insideURL = true;
261
+ value = value.trim();
262
+ for (let i = 0; i < value.length; i++) {
263
+ if (value[i] === ',' && (!insideURL || (insideURL && value[i + 1] === ' '))) {
264
+ candidates.push(value.slice(0, i));
265
+ value = value.substring(i + 1).trim();
266
+ i = 0;
267
+ insideURL = true;
268
+ } else if (value[i] === ' ') {
269
+ insideURL = false;
270
+ }
271
+ }
272
+ candidates.push(value);
260
273
  for (const candidate of candidates) {
261
- const src = candidate.trim().split(WHITESPACE)[0];
274
+ const src = candidate.split(WHITESPACE)[0];
262
275
  hrefs.push(src);
263
276
  }
264
277
  }
package/dist/cli.js CHANGED
@@ -870,7 +870,7 @@ async function launch(port, https) {
870
870
  exec(`${cmd} ${https ? 'https' : 'http'}://localhost:${port}`);
871
871
  }
872
872
 
873
- const prog = sade('svelte-kit').version('1.0.0-next.225');
873
+ const prog = sade('svelte-kit').version('1.0.0-next.227');
874
874
 
875
875
  prog
876
876
  .command('dev')
@@ -1022,7 +1022,7 @@ async function check_port(port) {
1022
1022
  function welcome({ port, host, https, open, loose, allow, cwd }) {
1023
1023
  if (open) launch(port, https);
1024
1024
 
1025
- console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.225'}\n`));
1025
+ console.log($.bold().cyan(`\n SvelteKit v${'1.0.0-next.227'}\n`));
1026
1026
 
1027
1027
  const protocol = https ? 'https:' : 'http:';
1028
1028
  const exposed = typeof host !== 'undefined' && host !== 'localhost' && host !== '127.0.0.1';
package/dist/ssr.js CHANGED
@@ -1558,8 +1558,9 @@ function read_only_form_data() {
1558
1558
  * @param {string} value
1559
1559
  */
1560
1560
  append(key, value) {
1561
- if (map.has(key)) {
1562
- (map.get(key) || []).push(value);
1561
+ const existing_values = map.get(key);
1562
+ if (existing_values) {
1563
+ existing_values.push(value);
1563
1564
  } else {
1564
1565
  map.set(key, [value]);
1565
1566
  }
@@ -1581,12 +1582,15 @@ class ReadOnlyFormData {
1581
1582
  /** @param {string} key */
1582
1583
  get(key) {
1583
1584
  const value = this.#map.get(key);
1584
- return value && value[0];
1585
+ if (!value) {
1586
+ return null;
1587
+ }
1588
+ return value[0];
1585
1589
  }
1586
1590
 
1587
1591
  /** @param {string} key */
1588
1592
  getAll(key) {
1589
- return this.#map.get(key);
1593
+ return this.#map.get(key) || [];
1590
1594
  }
1591
1595
 
1592
1596
  /** @param {string} key */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "1.0.0-next.225",
3
+ "version": "1.0.0-next.227",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -72,6 +72,19 @@ declare module '$app/navigation' {
72
72
  * Returns a Promise that resolves when the routes have been prefetched.
73
73
  */
74
74
  export function prefetchRoutes(routes?: string[]): Promise<any>;
75
+
76
+ /**
77
+ * A navigation interceptor that triggers before we navigate to a new route.
78
+ * This is helpful if we want to conditionally prevent a navigation from completing or lookup the upcoming url.
79
+ */
80
+ export function beforeNavigate(
81
+ fn: ({ from, to, cancel }: { from: URL; to: URL | null; cancel: () => void }) => void
82
+ ): any;
83
+
84
+ /**
85
+ * A lifecycle function that runs when the page mounts, and also whenever SvelteKit navigates to a new URL but stays on this component.
86
+ */
87
+ export function afterNavigate(fn: ({ from, to }: { from: URL | null; to: URL }) => void): any;
75
88
  }
76
89
 
77
90
  declare module '$app/paths' {
package/types/helper.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  interface ReadOnlyFormData {
2
- get(key: string): string;
2
+ get(key: string): string | null;
3
3
  getAll(key: string): string[];
4
4
  has(key: string): boolean;
5
5
  entries(): Generator<[string, string], void>;