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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "1.0.0-next.430",
3
+ "version": "1.0.0-next.431",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -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;
689
+
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));
665
696
 
666
- if (route.uses_server_data) {
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,6 +825,9 @@ export function create_client({ target, base, trailing_slash }) {
782
825
  }
783
826
  }
784
827
 
828
+ // TODO post-https://github.com/sveltejs/kit/discussions/6124, this will
829
+ // no longer be necessary — if we get here, it's because the root layout
830
+ // load function failed, which means we have to fall back to the server
785
831
  return await load_root_error_page({
786
832
  status,
787
833
  error,
@@ -813,30 +859,57 @@ export function create_client({ target, base, trailing_slash }) {
813
859
  * url: URL;
814
860
  * routeId: string | null
815
861
  * }} opts
862
+ * @returns {Promise<import('./types').NavigationFinished>}
816
863
  */
817
864
  async function load_root_error_page({ status, error, url, routeId }) {
818
865
  /** @type {Record<string, string>} */
819
866
  const params = {}; // error page does not have params
820
867
 
868
+ const node = await default_layout_loader();
869
+
870
+ /** @type {import('types').ServerDataNode | null} */
871
+ let server_data_node = null;
872
+
873
+ if (node.server) {
874
+ // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
875
+ // existing root layout data
876
+ const res = await native_fetch(
877
+ `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
878
+ {
879
+ headers: {
880
+ 'x-sveltekit-invalidated': '1'
881
+ }
882
+ }
883
+ );
884
+
885
+ const server_data_nodes = await res.json();
886
+ server_data_node = server_data_nodes?.[0] ?? null;
887
+
888
+ if (!res.ok || server_data_nodes?.type !== 'data') {
889
+ // at this point we have no choice but to fall back to the server
890
+ native_navigation(url);
891
+
892
+ // @ts-expect-error
893
+ return;
894
+ }
895
+ }
896
+
821
897
  const root_layout = await load_node({
822
- node: await default_layout,
898
+ loader: default_layout_loader,
823
899
  url,
824
900
  params,
825
901
  routeId,
826
902
  parent: () => Promise.resolve({}),
827
- server_data: null // TODO!!!!!
903
+ server_data_node: create_data_node(server_data_node)
828
904
  });
829
905
 
906
+ /** @type {import('./types').BranchNode} */
830
907
  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
- }
908
+ node: await default_error_loader(),
909
+ loader: default_error_loader,
910
+ shared: null,
911
+ server: null,
912
+ data: null
840
913
  };
841
914
 
842
915
  return await get_navigation_result_from_branch({
@@ -985,7 +1058,8 @@ export function create_client({ target, base, trailing_slash }) {
985
1058
  if (resource === undefined) {
986
1059
  // Force rerun of all load functions, regardless of their dependencies
987
1060
  for (const node of current.branch) {
988
- node?.uses.dependencies.add('');
1061
+ node?.server?.uses.dependencies.add('');
1062
+ node?.shared?.uses.dependencies.add('');
989
1063
  }
990
1064
  invalidated.push(() => true);
991
1065
  } else if (typeof resource === 'function') {
@@ -1230,12 +1304,19 @@ export function create_client({ target, base, trailing_slash }) {
1230
1304
  const script = document.querySelector(`script[sveltekit\\:data-type="${type}"]`);
1231
1305
  return script?.textContent ? JSON.parse(script.textContent) : fallback;
1232
1306
  };
1233
- const server_data = parse('server_data', []);
1307
+ /**
1308
+ * @type {Array<import('types').ServerDataNode | null>}
1309
+ * On initial navigation, this will only consist of data nodes or `null`.
1310
+ * A possible error is passed through the `error` property, in which case
1311
+ * the last entry of `node_ids` is an error page and the last entry of
1312
+ * `server_data_nodes` is `null`.
1313
+ */
1314
+ const server_data_nodes = parse('server_data', []);
1234
1315
  const validation_errors = parse('validation_errors', undefined);
1235
1316
 
1236
1317
  const branch_promises = node_ids.map(async (n, i) => {
1237
1318
  return load_node({
1238
- node: await nodes[n](),
1319
+ loader: nodes[n],
1239
1320
  url,
1240
1321
  params,
1241
1322
  routeId,
@@ -1246,7 +1327,7 @@ export function create_client({ target, base, trailing_slash }) {
1246
1327
  }
1247
1328
  return data;
1248
1329
  },
1249
- server_data: server_data[i] ?? null
1330
+ server_data_node: create_data_node(server_data_nodes[i])
1250
1331
  });
1251
1332
  });
1252
1333
 
@@ -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
- }
@@ -10,6 +10,7 @@ import { negotiate } from '../../utils/http.js';
10
10
  import { HttpError, Redirect } from '../../index/private.js';
11
11
  import { load_server_data } from './page/load_data.js';
12
12
  import { json } from '../../index/index.js';
13
+ import { once } from '../../utils/functions.js';
13
14
 
14
15
  /* global __SVELTEKIT_ADAPTER_NAME__ */
15
16
 
@@ -254,19 +255,26 @@ export async function respond(request, options, state) {
254
255
  let response;
255
256
  if (is_data_request && route.type === 'page') {
256
257
  try {
257
- /** @type {Redirect | HttpError | Error} */
258
- let error;
259
-
260
- // TODO only get the data we need for the navigation
261
- const promises = [...route.layouts, route.leaf].map(async (n, i) => {
262
- try {
263
- if (error) return;
264
-
265
- // == because it could be undefined (in dev) or null (in build, because of JSON.stringify)
266
- const node = n == undefined ? n : await options.manifest._.nodes[n]();
267
- return {
268
- // TODO return `uses`, so we can reuse server data effectively
269
- data: await load_server_data({
258
+ const node_ids = [...route.layouts, route.leaf];
259
+
260
+ const invalidated =
261
+ request.headers.get('x-sveltekit-invalidated')?.split(',').map(Boolean) ??
262
+ node_ids.map(() => true);
263
+
264
+ let aborted = false;
265
+
266
+ const functions = node_ids.map((n, i) => {
267
+ return once(async () => {
268
+ try {
269
+ if (aborted) {
270
+ return /** @type {import('types').ServerDataSkippedNode} */ ({
271
+ type: 'skip'
272
+ });
273
+ }
274
+
275
+ // == because it could be undefined (in dev) or null (in build, because of JSON.stringify)
276
+ const node = n == undefined ? n : await options.manifest._.nodes[n]();
277
+ return load_server_data({
270
278
  dev: options.dev,
271
279
  event,
272
280
  node,
@@ -274,47 +282,78 @@ export async function respond(request, options, state) {
274
282
  /** @type {Record<string, any>} */
275
283
  const data = {};
276
284
  for (let j = 0; j < i; j += 1) {
277
- const parent = await promises[j];
278
- if (!parent || parent instanceof HttpError || 'error' in parent) {
279
- return data;
280
- }
285
+ const parent = /** @type {import('types').ServerDataNode} */ (
286
+ await functions[j]()
287
+ );
281
288
  Object.assign(data, parent.data);
282
289
  }
283
290
  return data;
284
291
  }
285
- })
286
- };
287
- } catch (e) {
288
- error = normalize_error(e);
289
-
290
- if (error instanceof Redirect) {
291
- throw error;
292
- }
293
-
294
- if (error instanceof HttpError) {
295
- return error; // { status, message }
292
+ });
293
+ } catch (e) {
294
+ aborted = true;
295
+ throw e;
296
296
  }
297
+ });
298
+ });
297
299
 
298
- options.handle_error(error, event);
299
-
300
- return {
301
- error: error_to_pojo(error, options.get_stack)
302
- };
300
+ const promises = functions.map(async (fn, i) => {
301
+ if (!invalidated[i]) {
302
+ return /** @type {import('types').ServerDataSkippedNode} */ ({
303
+ type: 'skip'
304
+ });
303
305
  }
306
+
307
+ return fn();
304
308
  });
305
309
 
306
- response = json({
310
+ let length = promises.length;
311
+ const nodes = await Promise.all(
312
+ promises.map((p, i) =>
313
+ p.catch((e) => {
314
+ const error = normalize_error(e);
315
+
316
+ if (error instanceof Redirect) {
317
+ throw error;
318
+ }
319
+
320
+ length = i + 1; // don't include nodes after first error
321
+
322
+ if (error instanceof HttpError) {
323
+ return /** @type {import('types').ServerErrorNode} */ ({
324
+ type: 'error',
325
+ httperror: { ...error }
326
+ });
327
+ }
328
+
329
+ options.handle_error(error, event);
330
+
331
+ return /** @type {import('types').ServerErrorNode} */ ({
332
+ type: 'error',
333
+ error: error_to_pojo(error, options.get_stack)
334
+ });
335
+ })
336
+ )
337
+ );
338
+
339
+ /** @type {import('types').ServerData} */
340
+ const server_data = {
307
341
  type: 'data',
308
- nodes: await Promise.all(promises)
309
- });
342
+ nodes: nodes.slice(0, length)
343
+ };
344
+
345
+ response = json(server_data);
310
346
  } catch (e) {
311
347
  const error = normalize_error(e);
312
348
 
313
349
  if (error instanceof Redirect) {
314
- response = json({
350
+ /** @type {import('types').ServerData} */
351
+ const server_data = {
315
352
  type: 'redirect',
316
353
  location: error.location
317
- });
354
+ };
355
+
356
+ response = json(server_data);
318
357
  } else {
319
358
  response = json(error_to_pojo(error, options.get_stack), { status: 500 });
320
359
  }
@@ -145,7 +145,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
145
145
  /** @type {Error | null} */
146
146
  let load_error = null;
147
147
 
148
- /** @type {Array<Promise<Record<string, any> | null>>} */
148
+ /** @type {Array<Promise<import('types').ServerDataNode | null>>} */
149
149
  const server_promises = nodes.map((node, i) => {
150
150
  if (load_error) {
151
151
  // if an error happens immediately, don't bother with the rest of the nodes
@@ -168,7 +168,8 @@ export async function render_page(event, route, options, state, resolve_opts) {
168
168
  /** @type {Record<string, any>} */
169
169
  const data = {};
170
170
  for (let j = 0; j < i; j += 1) {
171
- Object.assign(data, await server_promises[j]);
171
+ const parent = await server_promises[j];
172
+ if (parent) Object.assign(data, await parent.data);
172
173
  }
173
174
  return data;
174
175
  }
@@ -291,7 +292,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
291
292
  response: new Response(undefined),
292
293
  body: JSON.stringify({
293
294
  type: 'data',
294
- nodes: branch.map((branch_node) => ({ data: branch_node?.server_data }))
295
+ nodes: branch.map((branch_node) => branch_node?.server_data)
295
296
  })
296
297
  });
297
298
  }
@@ -8,19 +8,46 @@ import { LoadURL, PrerenderingURL } from '../../../utils/url.js';
8
8
  * node: import('types').SSRNode | undefined;
9
9
  * parent: () => Promise<Record<string, any>>;
10
10
  * }} opts
11
+ * @returns {Promise<import('types').ServerDataNode | null>}
11
12
  */
12
13
  export async function load_server_data({ dev, event, node, parent }) {
13
14
  if (!node?.server) return null;
14
15
 
15
- const server_data = await node.server.load?.call(null, {
16
+ const uses = {
17
+ dependencies: new Set(),
18
+ params: new Set(),
19
+ parent: false,
20
+ url: false
21
+ };
22
+
23
+ /** @param {string[]} deps */
24
+ function depends(...deps) {
25
+ for (const dep of deps) {
26
+ const { href } = new URL(dep, event.url);
27
+ uses.dependencies.add(href);
28
+ }
29
+ }
30
+
31
+ const params = new Proxy(event.params, {
32
+ get: (target, key) => {
33
+ uses.params.add(key);
34
+ return target[/** @type {string} */ (key)];
35
+ }
36
+ });
37
+
38
+ const result = await node.server.load?.call(null, {
16
39
  // can't use destructuring here because it will always
17
40
  // invoke event.clientAddress, which breaks prerendering
18
41
  get clientAddress() {
19
42
  return event.clientAddress;
20
43
  },
44
+ depends,
21
45
  locals: event.locals,
22
- params: event.params,
23
- parent,
46
+ params,
47
+ parent: async () => {
48
+ uses.parent = true;
49
+ return parent();
50
+ },
24
51
  platform: event.platform,
25
52
  request: event.request,
26
53
  routeId: event.routeId,
@@ -28,13 +55,22 @@ export async function load_server_data({ dev, event, node, parent }) {
28
55
  url: event.url
29
56
  });
30
57
 
31
- const result = server_data ? await unwrap_promises(server_data) : null;
58
+ const data = result ? await unwrap_promises(result) : null;
32
59
 
33
60
  if (dev) {
34
- check_serializability(result, /** @type {string} */ (node.server_id), 'data');
61
+ check_serializability(data, /** @type {string} */ (node.server_id), 'data');
35
62
  }
36
63
 
37
- return result;
64
+ return {
65
+ type: 'data',
66
+ data,
67
+ uses: {
68
+ dependencies: uses.dependencies.size > 0 ? Array.from(uses.dependencies) : undefined,
69
+ params: uses.params.size > 0 ? Array.from(uses.params) : undefined,
70
+ parent: uses.parent ? 1 : undefined,
71
+ url: uses.url ? 1 : undefined
72
+ }
73
+ };
38
74
  }
39
75
 
40
76
  /**
@@ -44,21 +80,22 @@ export async function load_server_data({ dev, event, node, parent }) {
44
80
  * fetcher: typeof fetch;
45
81
  * node: import('types').SSRNode | undefined;
46
82
  * parent: () => Promise<Record<string, any>>;
47
- * server_data_promise: Promise<Record<string, any> | null>;
83
+ * server_data_promise: Promise<import('types').ServerDataNode | null>;
48
84
  * state: import('types').SSRState;
49
85
  * }} opts
86
+ * @returns {Promise<Record<string, any> | null>}
50
87
  */
51
88
  export async function load_data({ event, fetcher, node, parent, server_data_promise, state }) {
52
- const server_data = await server_data_promise;
89
+ const server_data_node = await server_data_promise;
53
90
 
54
91
  if (!node?.shared?.load) {
55
- return server_data;
92
+ return server_data_node?.data ?? null;
56
93
  }
57
94
 
58
95
  const load_input = {
59
96
  url: state.prerendering ? new PrerenderingURL(event.url) : new LoadURL(event.url),
60
97
  params: event.params,
61
- data: server_data,
98
+ data: server_data_node?.data ?? null,
62
99
  routeId: event.routeId,
63
100
  fetch: fetcher,
64
101
  setHeaders: event.setHeaders,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @template T
3
+ * @param {() => T} fn
4
+ */
5
+ export function once(fn) {
6
+ let done = false;
7
+
8
+ /** @type T */
9
+ let result;
10
+
11
+ return () => {
12
+ if (done) return result;
13
+ done = true;
14
+ return (result = fn());
15
+ };
16
+ }
@@ -62,9 +62,9 @@ export interface BuildData {
62
62
  export interface CSRPageNode {
63
63
  component: typeof SvelteComponent;
64
64
  shared: {
65
- load: Load;
66
- hydrate: boolean;
67
- router: boolean;
65
+ load?: Load;
66
+ hydrate?: boolean;
67
+ router?: boolean;
68
68
  };
69
69
  server: boolean;
70
70
  }
@@ -169,6 +169,50 @@ export interface Respond {
169
169
 
170
170
  export type RouteData = PageData | EndpointData;
171
171
 
172
+ export type ServerData =
173
+ | {
174
+ type: 'redirect';
175
+ location: string;
176
+ }
177
+ | {
178
+ type: 'data';
179
+ nodes: Array<ServerDataNode | ServerDataSkippedNode | ServerErrorNode | null>;
180
+ };
181
+
182
+ /**
183
+ * Signals a successful response of the server `load` function.
184
+ * The `uses` property tells the client when it's possible to reuse this data
185
+ * in a subsequent request.
186
+ */
187
+ export interface ServerDataNode {
188
+ type: 'data';
189
+ data: Record<string, any> | null;
190
+ uses: {
191
+ dependencies?: string[];
192
+ params?: string[];
193
+ parent?: number | void; // 1 or undefined
194
+ url?: number | void; // 1 or undefined
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Signals that the server `load` function was not run, and the
200
+ * client should use what it has in memory
201
+ */
202
+ export interface ServerDataSkippedNode {
203
+ type: 'skip';
204
+ }
205
+
206
+ /**
207
+ * Signals that the server `load` function failed
208
+ */
209
+ export interface ServerErrorNode {
210
+ type: 'error';
211
+ // Either-or situation, but we don't want to have to do a type assertion
212
+ error?: Record<string, any>;
213
+ httperror?: { status: number; message: string };
214
+ }
215
+
172
216
  export interface SSRComponent {
173
217
  default: {
174
218
  render(props: Record<string, any>): {
@@ -292,6 +336,13 @@ export interface SSRState {
292
336
 
293
337
  export type StrictBody = string | Uint8Array;
294
338
 
339
+ export interface Uses {
340
+ dependencies: Set<string>;
341
+ params: Set<string>;
342
+ parent: boolean;
343
+ url: boolean;
344
+ }
345
+
295
346
  export type ValidatedConfig = RecursiveRequired<Config>;
296
347
 
297
348
  export type ValidatedKitConfig = RecursiveRequired<KitConfig>;