@sveltejs/kit 1.0.0-next.430 → 1.0.0-next.433

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.
@@ -7,11 +7,12 @@ declare module '__GENERATED__/client-manifest.js' {
7
7
  export const nodes: CSRPageNodeLoader[];
8
8
 
9
9
  /**
10
- * A map of `[routeId: string]: [errors, layouts, page]` tuples, which
10
+ * A map of `[routeId: string]: [leaf, layouts, errors]` tuples, which
11
11
  * is parsed into an array of routes on startup. The numbers refer to the
12
- * indices in `nodes`.
12
+ * indices in `nodes`. The route layout and error nodes are not referenced,
13
+ * they are always number 0 and 1 and always apply.
13
14
  */
14
- export const dictionary: Record<string, [number[], number[], number]>;
15
+ export const dictionary: Record<string, [leaf: number, layouts?: number[], errors?: number[]]>;
15
16
 
16
17
  export const matchers: Record<string, ParamMatcher>;
17
18
  }
@@ -16,10 +16,13 @@ const INDEX_KEY = 'sveltekit:index';
16
16
 
17
17
  const routes = parse(nodes, dictionary, matchers);
18
18
 
19
+ const default_layout_loader = nodes[0];
20
+ const default_error_loader = nodes[1];
21
+
19
22
  // we import the root layout/error nodes eagerly, so that
20
23
  // connectivity errors after initialisation don't nuke the app
21
- const default_layout = nodes[0]();
22
- const default_error = nodes[1]();
24
+ default_layout_loader();
25
+ default_error_loader();
23
26
 
24
27
  // We track the scroll position associated with each history entry in sessionStorage,
25
28
  // rather than on history.state itself, because when navigation is driven by
@@ -463,62 +466,57 @@ export function create_client({ target, base, trailing_slash }) {
463
466
  * If `server_data` is passed, this is treated as the initial run and the page endpoint is not requested.
464
467
  *
465
468
  * @param {{
466
- * node: import('types').CSRPageNode;
469
+ * loader: import('types').CSRPageNodeLoader;
467
470
  * parent: () => Promise<Record<string, any>>;
468
471
  * url: URL;
469
472
  * params: Record<string, string>;
470
473
  * routeId: string | null;
471
- * server_data: Record<string, any> | null;
474
+ * server_data_node: import('./types').DataNode | null;
472
475
  * }} options
473
476
  * @returns {Promise<import('./types').BranchNode>}
474
477
  */
475
- async function load_node({ node, parent, url, params, routeId, server_data }) {
478
+ async function load_node({ loader, parent, url, params, routeId, server_data_node }) {
479
+ /** @type {Record<string, any> | null} */
480
+ let data = null;
481
+
482
+ /** @type {import('types').Uses} */
476
483
  const uses = {
477
- params: new Set(),
478
- url: false,
479
484
  dependencies: new Set(),
480
- parent: false
485
+ params: new Set(),
486
+ parent: false,
487
+ url: false
481
488
  };
482
489
 
483
- /** @param {string[]} deps */
484
- function depends(...deps) {
485
- for (const dep of deps) {
486
- const { href } = new URL(dep, url);
487
- uses.dependencies.add(href);
488
- }
489
- }
490
-
491
- /** @type {Record<string, any> | null} */
492
- let data = null;
490
+ const node = await loader();
493
491
 
494
- if (node.server) {
495
- // +page|layout.server.js data means we need to mark this URL as a dependency of itself,
496
- // unless we want to get clever with usage detection on the server, which could
497
- // be returned to the client either as payload or custom headers
498
- uses.dependencies.add(url.href);
499
- uses.url = true;
500
- }
492
+ if (node.shared?.load) {
493
+ /** @param {string[]} deps */
494
+ function depends(...deps) {
495
+ for (const dep of deps) {
496
+ const { href } = new URL(dep, url);
497
+ uses.dependencies.add(href);
498
+ }
499
+ }
501
500
 
502
- /** @type {Record<string, string>} */
503
- const uses_params = {};
504
- for (const key in params) {
505
- Object.defineProperty(uses_params, key, {
506
- get() {
507
- uses.params.add(key);
508
- return params[key];
509
- },
510
- enumerable: true
511
- });
512
- }
501
+ /** @type {Record<string, string>} */
502
+ const uses_params = {};
503
+ for (const key in params) {
504
+ Object.defineProperty(uses_params, key, {
505
+ get() {
506
+ uses.params.add(key);
507
+ return params[key];
508
+ },
509
+ enumerable: true
510
+ });
511
+ }
513
512
 
514
- const load_url = new LoadURL(url);
513
+ const load_url = new LoadURL(url);
515
514
 
516
- if (node.shared?.load) {
517
515
  /** @type {import('types').LoadEvent} */
518
516
  const load_input = {
519
517
  routeId,
520
518
  params: uses_params,
521
- data: server_data,
519
+ data: server_data_node?.data ?? null,
522
520
  get url() {
523
521
  uses.url = true;
524
522
  return load_url;
@@ -564,11 +562,9 @@ export function create_client({ target, base, trailing_slash }) {
564
562
  },
565
563
  setHeaders: () => {}, // noop
566
564
  depends,
567
- get parent() {
568
- // uses.parent assignment here, not on method inokation, else we wouldn't notice when someone
569
- // does await parent() inside an if branch which wasn't executed yet.
565
+ parent() {
570
566
  uses.parent = true;
571
- return parent;
567
+ return parent();
572
568
  }
573
569
  };
574
570
 
@@ -614,11 +610,55 @@ export function create_client({ target, base, trailing_slash }) {
614
610
 
615
611
  return {
616
612
  node,
617
- data: data || server_data,
618
- uses
613
+ loader,
614
+ server: server_data_node,
615
+ shared: node.shared?.load ? { type: 'data', data, uses } : null,
616
+ data: data ?? server_data_node?.data ?? null
619
617
  };
620
618
  }
621
619
 
620
+ /**
621
+ * @param {import('types').Uses | undefined} uses
622
+ * @param {boolean} parent_changed
623
+ * @param {{ url: boolean, params: string[] }} changed
624
+ */
625
+ function has_changed(changed, parent_changed, uses) {
626
+ if (!uses) return false;
627
+
628
+ if (uses.parent && parent_changed) return true;
629
+ if (changed.url && uses.url) return true;
630
+
631
+ for (const param of changed.params) {
632
+ if (uses.params.has(param)) return true;
633
+ }
634
+
635
+ for (const dep of uses.dependencies) {
636
+ if (invalidated.some((fn) => fn(dep))) return true;
637
+ }
638
+
639
+ return false;
640
+ }
641
+
642
+ /**
643
+ * @param {import('types').ServerDataNode | import('types').ServerDataSkippedNode | null} node
644
+ * @returns {import('./types').DataNode | null}
645
+ */
646
+ function create_data_node(node) {
647
+ if (node?.type === 'data') {
648
+ return {
649
+ type: 'data',
650
+ data: node.data,
651
+ uses: {
652
+ dependencies: new Set(node.uses.dependencies ?? []),
653
+ params: new Set(node.uses.params ?? []),
654
+ parent: !!node.uses.parent,
655
+ url: !!node.uses.url
656
+ }
657
+ };
658
+ }
659
+ return null;
660
+ }
661
+
622
662
  /**
623
663
  * @param {import('./types').NavigationIntent} intent
624
664
  * @returns {Promise<import('./types').NavigationResult | undefined>}
@@ -640,89 +680,95 @@ export function create_client({ target, base, trailing_slash }) {
640
680
  // to act on the failures at this point)
641
681
  [...errors, ...layouts, leaf].forEach((loader) => loader?.().catch(() => {}));
642
682
 
643
- const nodes = [...layouts, leaf];
683
+ const loaders = [...layouts, leaf];
644
684
 
645
685
  // To avoid waterfalls when someone awaits a parent, compute as much as possible here already
646
- /** @type {boolean[]} */
647
- const nodes_changed_since_last_render = [];
648
- for (let i = 0; i < nodes.length; i++) {
649
- if (!nodes[i]) {
650
- nodes_changed_since_last_render.push(false);
651
- } else {
652
- const previous = current.branch[i];
653
- const changed_since_last_render =
654
- !previous ||
655
- (changed.url && previous.uses.url) ||
656
- changed.params.some((param) => previous.uses.params.has(param)) ||
657
- Array.from(previous.uses.dependencies).some((dep) => invalidated.some((fn) => fn(dep))) ||
658
- (previous.uses.parent && nodes_changed_since_last_render.includes(true));
659
- nodes_changed_since_last_render.push(changed_since_last_render);
660
- }
661
- }
662
686
 
663
- /** @type {import('./types').ServerDataPayload | null} */
664
- let server_data_payload = null;
687
+ /** @type {import('types').ServerData | null} */
688
+ let server_data = null;
665
689
 
666
- if (route.uses_server_data) {
690
+ const invalid_server_nodes = loaders.reduce((acc, loader, i) => {
691
+ const previous = current.branch[i];
692
+ const invalid =
693
+ loader &&
694
+ (previous?.loader !== loader ||
695
+ has_changed(changed, acc.some(Boolean), previous.server?.uses));
696
+
697
+ acc.push(invalid);
698
+ return acc;
699
+ }, /** @type {boolean[]} */ ([]));
700
+
701
+ if (route.uses_server_data && invalid_server_nodes.some(Boolean)) {
667
702
  try {
668
703
  const res = await native_fetch(
669
- `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`
704
+ `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
705
+ {
706
+ headers: {
707
+ 'x-sveltekit-invalidated': invalid_server_nodes.map((x) => (x ? '1' : '')).join(',')
708
+ }
709
+ }
670
710
  );
671
711
 
672
- server_data_payload = /** @type {import('./types').ServerDataPayload} */ (await res.json());
712
+ server_data = /** @type {import('types').ServerData} */ (await res.json());
673
713
 
674
714
  if (!res.ok) {
675
- throw server_data_payload;
715
+ throw server_data;
676
716
  }
677
717
  } catch (e) {
678
- throw new Error('TODO render fallback error page');
718
+ // something went catastrophically wrong — bail and defer to the server
719
+ native_navigation(url);
720
+ return;
679
721
  }
680
722
 
681
- if (server_data_payload.type === 'redirect') {
682
- return server_data_payload;
723
+ if (server_data.type === 'redirect') {
724
+ return server_data;
683
725
  }
684
726
  }
685
727
 
686
- const server_data_nodes = server_data_payload?.nodes;
728
+ const server_data_nodes = server_data?.nodes;
687
729
 
688
- const branch_promises = nodes.map(async (loader, i) => {
689
- return Promise.resolve().then(async () => {
690
- if (!loader) return;
691
- const node = await loader();
730
+ let parent_changed = false;
692
731
 
693
- /** @type {import('./types').BranchNode | undefined} */
694
- const previous = current.branch[i];
695
- const changed_since_last_render =
696
- nodes_changed_since_last_render[i] || !previous || node !== previous.node;
732
+ const branch_promises = loaders.map(async (loader, i) => {
733
+ if (!loader) return;
697
734
 
698
- if (changed_since_last_render) {
699
- const payload = server_data_nodes?.[i];
735
+ /** @type {import('./types').BranchNode | undefined} */
736
+ const previous = current.branch[i];
700
737
 
701
- if (payload?.status) {
702
- throw error(payload.status, payload.message);
703
- }
738
+ const server_data_node = server_data_nodes?.[i] ?? null;
704
739
 
705
- if (payload?.error) {
706
- throw payload.error;
707
- }
740
+ const can_reuse_server_data = !server_data_node || server_data_node.type === 'skip';
741
+ // re-use data from previous load if it's still valid
742
+ const valid =
743
+ can_reuse_server_data &&
744
+ loader === previous?.loader &&
745
+ !has_changed(changed, parent_changed, previous.shared?.uses);
746
+ if (valid) return previous;
708
747
 
709
- return await load_node({
710
- node,
711
- url,
712
- params,
713
- routeId: route.id,
714
- parent: async () => {
715
- const data = {};
716
- for (let j = 0; j < i; j += 1) {
717
- Object.assign(data, (await branch_promises[j])?.data);
718
- }
719
- return data;
720
- },
721
- server_data: payload?.data ?? null
722
- });
748
+ parent_changed = true;
749
+
750
+ if (server_data_node?.type === 'error') {
751
+ if (server_data_node.httperror) {
752
+ // reconstruct as an HttpError
753
+ throw error(server_data_node.httperror.status, server_data_node.httperror.message);
723
754
  } else {
724
- return previous;
755
+ throw server_data_node.error;
725
756
  }
757
+ }
758
+
759
+ return load_node({
760
+ loader,
761
+ url,
762
+ params,
763
+ routeId: route.id,
764
+ parent: async () => {
765
+ const data = {};
766
+ for (let j = 0; j < i; j += 1) {
767
+ Object.assign(data, (await branch_promises[j])?.data);
768
+ }
769
+ return data;
770
+ },
771
+ server_data_node: create_data_node(server_data_node) ?? previous?.server ?? null
726
772
  });
727
773
  });
728
774
 
@@ -732,8 +778,8 @@ export function create_client({ target, base, trailing_slash }) {
732
778
  /** @type {Array<import('./types').BranchNode | undefined>} */
733
779
  const branch = [];
734
780
 
735
- for (let i = 0; i < nodes.length; i += 1) {
736
- if (nodes[i]) {
781
+ for (let i = 0; i < loaders.length; i += 1) {
782
+ if (loaders[i]) {
737
783
  try {
738
784
  branch.push(await branch_promises[i]);
739
785
  } catch (e) {
@@ -759,13 +805,10 @@ export function create_client({ target, base, trailing_slash }) {
759
805
  try {
760
806
  error_loaded = {
761
807
  node: await errors[i](),
808
+ loader: errors[i],
762
809
  data: {},
763
- uses: {
764
- params: new Set(),
765
- url: false,
766
- dependencies: new Set(),
767
- parent: false
768
- }
810
+ server: null,
811
+ shared: null
769
812
  };
770
813
 
771
814
  return await get_navigation_result_from_branch({
@@ -782,12 +825,10 @@ export function create_client({ target, base, trailing_slash }) {
782
825
  }
783
826
  }
784
827
 
785
- return await load_root_error_page({
786
- status,
787
- error,
788
- url,
789
- routeId: route.id
790
- });
828
+ // if we get here, it's because the root `load` function failed,
829
+ // and we need to fall back to the server
830
+ native_navigation(url);
831
+ return;
791
832
  }
792
833
  } else {
793
834
  // push an empty slot so we can rewind past gaps to the
@@ -813,30 +854,57 @@ export function create_client({ target, base, trailing_slash }) {
813
854
  * url: URL;
814
855
  * routeId: string | null
815
856
  * }} opts
857
+ * @returns {Promise<import('./types').NavigationFinished>}
816
858
  */
817
859
  async function load_root_error_page({ status, error, url, routeId }) {
818
860
  /** @type {Record<string, string>} */
819
861
  const params = {}; // error page does not have params
820
862
 
863
+ const node = await default_layout_loader();
864
+
865
+ /** @type {import('types').ServerDataNode | null} */
866
+ let server_data_node = null;
867
+
868
+ if (node.server) {
869
+ // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
870
+ // existing root layout data
871
+ const res = await native_fetch(
872
+ `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
873
+ {
874
+ headers: {
875
+ 'x-sveltekit-invalidated': '1'
876
+ }
877
+ }
878
+ );
879
+
880
+ const server_data_nodes = await res.json();
881
+ server_data_node = server_data_nodes?.[0] ?? null;
882
+
883
+ if (!res.ok || server_data_nodes?.type !== 'data') {
884
+ // at this point we have no choice but to fall back to the server
885
+ native_navigation(url);
886
+
887
+ // @ts-expect-error
888
+ return;
889
+ }
890
+ }
891
+
821
892
  const root_layout = await load_node({
822
- node: await default_layout,
893
+ loader: default_layout_loader,
823
894
  url,
824
895
  params,
825
896
  routeId,
826
897
  parent: () => Promise.resolve({}),
827
- server_data: null // TODO!!!!!
898
+ server_data_node: create_data_node(server_data_node)
828
899
  });
829
900
 
901
+ /** @type {import('./types').BranchNode} */
830
902
  const root_error = {
831
- node: await default_error,
832
- data: null,
833
- // TODO make this unnecessary
834
- uses: {
835
- params: new Set(),
836
- url: false,
837
- dependencies: new Set(),
838
- parent: false
839
- }
903
+ node: await default_error_loader(),
904
+ loader: default_error_loader,
905
+ shared: null,
906
+ server: null,
907
+ data: null
840
908
  };
841
909
 
842
910
  return await get_navigation_result_from_branch({
@@ -985,7 +1053,8 @@ export function create_client({ target, base, trailing_slash }) {
985
1053
  if (resource === undefined) {
986
1054
  // Force rerun of all load functions, regardless of their dependencies
987
1055
  for (const node of current.branch) {
988
- node?.uses.dependencies.add('');
1056
+ node?.server?.uses.dependencies.add('');
1057
+ node?.shared?.uses.dependencies.add('');
989
1058
  }
990
1059
  invalidated.push(() => true);
991
1060
  } else if (typeof resource === 'function') {
@@ -1230,12 +1299,19 @@ export function create_client({ target, base, trailing_slash }) {
1230
1299
  const script = document.querySelector(`script[sveltekit\\:data-type="${type}"]`);
1231
1300
  return script?.textContent ? JSON.parse(script.textContent) : fallback;
1232
1301
  };
1233
- const server_data = parse('server_data', []);
1302
+ /**
1303
+ * @type {Array<import('types').ServerDataNode | null>}
1304
+ * On initial navigation, this will only consist of data nodes or `null`.
1305
+ * A possible error is passed through the `error` property, in which case
1306
+ * the last entry of `node_ids` is an error page and the last entry of
1307
+ * `server_data_nodes` is `null`.
1308
+ */
1309
+ const server_data_nodes = parse('server_data', []);
1234
1310
  const validation_errors = parse('validation_errors', undefined);
1235
1311
 
1236
1312
  const branch_promises = node_ids.map(async (n, i) => {
1237
1313
  return load_node({
1238
- node: await nodes[n](),
1314
+ loader: nodes[n],
1239
1315
  url,
1240
1316
  params,
1241
1317
  routeId,
@@ -1246,7 +1322,7 @@ export function create_client({ target, base, trailing_slash }) {
1246
1322
  }
1247
1323
  return data;
1248
1324
  },
1249
- server_data: server_data[i] ?? null
1325
+ server_data_node: create_data_node(server_data_nodes[i])
1250
1326
  });
1251
1327
  });
1252
1328
 
@@ -2,14 +2,19 @@ import { exec, parse_route_id } from '../../utils/routing.js';
2
2
 
3
3
  /**
4
4
  * @param {import('types').CSRPageNodeLoader[]} nodes
5
- * @param {Record<string, [number[], number[], number, 1?]>} dictionary
5
+ * @param {typeof import('__GENERATED__/client-manifest.js').dictionary} dictionary
6
6
  * @param {Record<string, (param: string) => boolean>} matchers
7
7
  * @returns {import('types').CSRRoute[]}
8
8
  */
9
9
  export function parse(nodes, dictionary, matchers) {
10
- return Object.entries(dictionary).map(([id, [errors, layouts, leaf, uses_server_data]]) => {
10
+ return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
11
11
  const { pattern, names, types } = parse_route_id(id);
12
12
 
13
+ // whether or not the route uses the server data is
14
+ // encoded using the ones' complement, to save space
15
+ const uses_server_data = leaf < 0;
16
+ if (uses_server_data) leaf = ~leaf;
17
+
13
18
  const route = {
14
19
  id,
15
20
  /** @param {string} path */
@@ -17,10 +22,10 @@ export function parse(nodes, dictionary, matchers) {
17
22
  const match = pattern.exec(path);
18
23
  if (match) return exec(match, names, types, matchers);
19
24
  },
20
- errors: errors.map((n) => nodes[n]),
21
- layouts: layouts.map((n) => nodes[n]),
25
+ errors: [1, ...(errors || [])].map((n) => nodes[n]),
26
+ layouts: [0, ...(layouts || [])].map((n) => nodes[n]),
22
27
  leaf: nodes[leaf],
23
- uses_server_data: !!uses_server_data
28
+ uses_server_data
24
29
  };
25
30
 
26
31
  // bit of a hack, but ensures that layout/error node lists are the same
@@ -6,7 +6,7 @@ import {
6
6
  prefetch,
7
7
  prefetchRoutes
8
8
  } from '$app/navigation';
9
- import { CSRPageNode, CSRRoute } from 'types';
9
+ import { CSRPageNode, CSRPageNodeLoader, CSRRoute, ServerErrorNode, Uses } from 'types';
10
10
  import { HttpError } from '../../index/private.js';
11
11
  import { SerializedHttpError } from '../server/page/types.js';
12
12
 
@@ -65,15 +65,18 @@ export type NavigationFinished = {
65
65
 
66
66
  export type BranchNode = {
67
67
  node: CSRPageNode;
68
+ loader: CSRPageNodeLoader;
69
+ server: DataNode | null;
70
+ shared: DataNode | null;
68
71
  data: Record<string, any> | null;
69
- uses: {
70
- params: Set<string>;
71
- url: boolean; // TODO make more granular?
72
- dependencies: Set<string>;
73
- parent: boolean;
74
- };
75
72
  };
76
73
 
74
+ export interface DataNode {
75
+ type: 'data';
76
+ data: Record<string, any> | null;
77
+ uses: Uses;
78
+ }
79
+
77
80
  export type NavigationState = {
78
81
  branch: Array<BranchNode | undefined>;
79
82
  error: HttpError | Error | null;
@@ -81,25 +84,3 @@ export type NavigationState = {
81
84
  session_id: number;
82
85
  url: URL;
83
86
  };
84
-
85
- export type ServerDataPayload = ServerDataRedirected | ServerDataLoaded;
86
-
87
- export interface ServerDataRedirected {
88
- type: 'redirect';
89
- location: string;
90
- }
91
-
92
- export interface ServerDataLoaded {
93
- type: 'data';
94
- nodes: Array<{
95
- data?: Record<string, any> | null; // TODO or `-1` to indicate 'reuse cached data'?
96
- status?: number;
97
- message?: string;
98
- error?: {
99
- name: string;
100
- message: string;
101
- stack: string;
102
- [key: string]: any;
103
- };
104
- }>;
105
- }
@@ -3,14 +3,12 @@ import { check_method_names, method_not_allowed } from './utils.js';
3
3
 
4
4
  /**
5
5
  * @param {import('types').RequestEvent} event
6
- * @param {import('types').SSREndpoint} route
6
+ * @param {import('types').SSREndpoint} mod
7
7
  * @returns {Promise<Response>}
8
8
  */
9
- export async function render_endpoint(event, route) {
9
+ export async function render_endpoint(event, mod) {
10
10
  const method = /** @type {import('types').HttpMethod} */ (event.request.method);
11
11
 
12
- const mod = await route.load();
13
-
14
12
  // TODO: Remove for 1.0
15
13
  check_method_names(mod);
16
14
 
@@ -21,12 +19,6 @@ export async function render_endpoint(event, route) {
21
19
  }
22
20
 
23
21
  if (!handler) {
24
- if (event.request.headers.get('x-sveltekit-load')) {
25
- // TODO would be nice to avoid these requests altogether,
26
- // by noting whether or not page endpoints export `get`
27
- return new Response(undefined, { status: 204 });
28
- }
29
-
30
22
  return method_not_allowed(mod, method);
31
23
  }
32
24