@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 +1 -1
- package/src/runtime/client/client.js +207 -126
- package/src/runtime/client/types.d.ts +10 -29
- package/src/runtime/server/index.js +77 -38
- package/src/runtime/server/page/index.js +4 -3
- package/src/runtime/server/page/load_data.js +47 -10
- package/src/utils/functions.js +16 -0
- package/types/internal.d.ts +54 -3
package/package.json
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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({
|
|
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
|
-
|
|
485
|
+
params: new Set(),
|
|
486
|
+
parent: false,
|
|
487
|
+
url: false
|
|
481
488
|
};
|
|
482
489
|
|
|
483
|
-
|
|
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.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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
|
|
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('
|
|
664
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
712
|
+
server_data = /** @type {import('types').ServerData} */ (await res.json());
|
|
673
713
|
|
|
674
714
|
if (!res.ok) {
|
|
675
|
-
throw
|
|
715
|
+
throw server_data;
|
|
676
716
|
}
|
|
677
717
|
} catch (e) {
|
|
678
|
-
|
|
718
|
+
// something went catastrophically wrong — bail and defer to the server
|
|
719
|
+
native_navigation(url);
|
|
720
|
+
return;
|
|
679
721
|
}
|
|
680
722
|
|
|
681
|
-
if (
|
|
682
|
-
return
|
|
723
|
+
if (server_data.type === 'redirect') {
|
|
724
|
+
return server_data;
|
|
683
725
|
}
|
|
684
726
|
}
|
|
685
727
|
|
|
686
|
-
const server_data_nodes =
|
|
728
|
+
const server_data_nodes = server_data?.nodes;
|
|
687
729
|
|
|
688
|
-
|
|
689
|
-
return Promise.resolve().then(async () => {
|
|
690
|
-
if (!loader) return;
|
|
691
|
-
const node = await loader();
|
|
730
|
+
let parent_changed = false;
|
|
692
731
|
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
699
|
-
|
|
735
|
+
/** @type {import('./types').BranchNode | undefined} */
|
|
736
|
+
const previous = current.branch[i];
|
|
700
737
|
|
|
701
|
-
|
|
702
|
-
throw error(payload.status, payload.message);
|
|
703
|
-
}
|
|
738
|
+
const server_data_node = server_data_nodes?.[i] ?? null;
|
|
704
739
|
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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 <
|
|
736
|
-
if (
|
|
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
|
-
|
|
764
|
-
|
|
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
|
-
|
|
898
|
+
loader: default_layout_loader,
|
|
823
899
|
url,
|
|
824
900
|
params,
|
|
825
901
|
routeId,
|
|
826
902
|
parent: () => Promise.resolve({}),
|
|
827
|
-
|
|
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
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 =
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
58
|
+
const data = result ? await unwrap_promises(result) : null;
|
|
32
59
|
|
|
33
60
|
if (dev) {
|
|
34
|
-
check_serializability(
|
|
61
|
+
check_serializability(data, /** @type {string} */ (node.server_id), 'data');
|
|
35
62
|
}
|
|
36
63
|
|
|
37
|
-
return
|
|
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<
|
|
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
|
|
89
|
+
const server_data_node = await server_data_promise;
|
|
53
90
|
|
|
54
91
|
if (!node?.shared?.load) {
|
|
55
|
-
return
|
|
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:
|
|
98
|
+
data: server_data_node?.data ?? null,
|
|
62
99
|
routeId: event.routeId,
|
|
63
100
|
fetch: fetcher,
|
|
64
101
|
setHeaders: event.setHeaders,
|
package/types/internal.d.ts
CHANGED
|
@@ -62,9 +62,9 @@ export interface BuildData {
|
|
|
62
62
|
export interface CSRPageNode {
|
|
63
63
|
component: typeof SvelteComponent;
|
|
64
64
|
shared: {
|
|
65
|
-
load
|
|
66
|
-
hydrate
|
|
67
|
-
router
|
|
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>;
|