@sveltejs/kit 1.7.2 → 1.8.0
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 +3 -3
- package/src/core/env.js +6 -10
- package/src/core/generate_manifest/index.js +1 -1
- package/src/core/sync/write_ambient.js +6 -5
- package/src/core/sync/write_client_manifest.js +3 -2
- package/src/core/sync/write_server.js +6 -4
- package/src/exports/vite/build/utils.js +1 -0
- package/src/exports/vite/dev/index.js +15 -9
- package/src/exports/vite/index.js +67 -35
- package/src/internal.d.ts +7 -0
- package/src/runtime/app/environment.js +1 -1
- package/src/runtime/client/client.js +118 -62
- package/src/runtime/client/parse.js +2 -5
- package/src/runtime/client/start.js +5 -16
- package/src/runtime/client/types.d.ts +37 -1
- package/src/runtime/client/utils.js +1 -1
- package/src/runtime/env/dynamic/private.js +1 -1
- package/src/runtime/env/dynamic/public.js +1 -1
- package/src/runtime/server/data/index.js +118 -18
- package/src/runtime/server/index.js +1 -1
- package/src/runtime/server/page/index.js +16 -11
- package/src/runtime/server/page/load_data.js +56 -6
- package/src/runtime/server/page/render.js +253 -153
- package/src/runtime/server/page/types.d.ts +2 -2
- package/src/runtime/server/utils.js +12 -21
- package/src/runtime/{shared.js → shared-server.js} +0 -13
- package/src/utils/streaming.js +44 -0
- package/types/index.d.ts +5 -7
- package/types/internal.d.ts +49 -17
- package/src/runtime/client/ambient.d.ts +0 -30
|
@@ -25,8 +25,6 @@ import {
|
|
|
25
25
|
} from './fetcher.js';
|
|
26
26
|
import { parse } from './parse.js';
|
|
27
27
|
|
|
28
|
-
import Root from '__GENERATED__/root.svelte';
|
|
29
|
-
import { nodes, server_loads, dictionary, matchers, hooks } from '__CLIENT__/manifest.js';
|
|
30
28
|
import { base } from '__sveltekit/paths';
|
|
31
29
|
import { HttpError, Redirect } from '../control.js';
|
|
32
30
|
import { stores } from './singletons.js';
|
|
@@ -36,16 +34,6 @@ import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './const
|
|
|
36
34
|
import { validate_common_exports } from '../../utils/exports.js';
|
|
37
35
|
import { compact } from '../../utils/array.js';
|
|
38
36
|
|
|
39
|
-
const routes = parse(nodes, server_loads, dictionary, matchers);
|
|
40
|
-
|
|
41
|
-
const default_layout_loader = nodes[0];
|
|
42
|
-
const default_error_loader = nodes[1];
|
|
43
|
-
|
|
44
|
-
// we import the root layout/error nodes eagerly, so that
|
|
45
|
-
// connectivity errors after initialisation don't nuke the app
|
|
46
|
-
default_layout_loader();
|
|
47
|
-
default_error_loader();
|
|
48
|
-
|
|
49
37
|
// We track the scroll position associated with each history entry in sessionStorage,
|
|
50
38
|
// rather than on history.state itself, because when navigation is driven by
|
|
51
39
|
// popstate it's too late to update the scroll position associated with the
|
|
@@ -64,12 +52,21 @@ function update_scroll_positions(index) {
|
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
/**
|
|
67
|
-
* @param {
|
|
68
|
-
*
|
|
69
|
-
* }} opts
|
|
55
|
+
* @param {import('./types').SvelteKitApp} app
|
|
56
|
+
* @param {HTMLElement} target
|
|
70
57
|
* @returns {import('./types').Client}
|
|
71
58
|
*/
|
|
72
|
-
export function create_client(
|
|
59
|
+
export function create_client(app, target) {
|
|
60
|
+
const routes = parse(app);
|
|
61
|
+
|
|
62
|
+
const default_layout_loader = app.nodes[0];
|
|
63
|
+
const default_error_loader = app.nodes[1];
|
|
64
|
+
|
|
65
|
+
// we import the root layout/error nodes eagerly, so that
|
|
66
|
+
// connectivity errors after initialisation don't nuke the app
|
|
67
|
+
default_layout_loader();
|
|
68
|
+
default_error_loader();
|
|
69
|
+
|
|
73
70
|
const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement;
|
|
74
71
|
/** @type {Array<((url: URL) => boolean)>} */
|
|
75
72
|
const invalidated = [];
|
|
@@ -432,7 +429,7 @@ export function create_client({ target }) {
|
|
|
432
429
|
|
|
433
430
|
page = /** @type {import('types').Page} */ (result.props.page);
|
|
434
431
|
|
|
435
|
-
root = new
|
|
432
|
+
root = new app.root({
|
|
436
433
|
target,
|
|
437
434
|
props: { ...result.props, stores, components },
|
|
438
435
|
hydrate: true
|
|
@@ -736,22 +733,8 @@ export function create_client({ target }) {
|
|
|
736
733
|
* @returns {import('./types').DataNode | null}
|
|
737
734
|
*/
|
|
738
735
|
function create_data_node(node, previous) {
|
|
739
|
-
if (node?.type === 'data')
|
|
740
|
-
|
|
741
|
-
type: 'data',
|
|
742
|
-
data: node.data,
|
|
743
|
-
uses: {
|
|
744
|
-
dependencies: new Set(node.uses.dependencies ?? []),
|
|
745
|
-
params: new Set(node.uses.params ?? []),
|
|
746
|
-
parent: !!node.uses.parent,
|
|
747
|
-
route: !!node.uses.route,
|
|
748
|
-
url: !!node.uses.url
|
|
749
|
-
},
|
|
750
|
-
slash: node.slash
|
|
751
|
-
};
|
|
752
|
-
} else if (node?.type === 'skip') {
|
|
753
|
-
return previous ?? null;
|
|
754
|
-
}
|
|
736
|
+
if (node?.type === 'data') return node;
|
|
737
|
+
if (node?.type === 'skip') return previous ?? null;
|
|
755
738
|
return null;
|
|
756
739
|
}
|
|
757
740
|
|
|
@@ -774,7 +757,7 @@ export function create_client({ target }) {
|
|
|
774
757
|
errors.forEach((loader) => loader?.().catch(() => {}));
|
|
775
758
|
loaders.forEach((loader) => loader?.[1]().catch(() => {}));
|
|
776
759
|
|
|
777
|
-
/** @type {import('types').
|
|
760
|
+
/** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
|
|
778
761
|
let server_data = null;
|
|
779
762
|
|
|
780
763
|
const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
|
|
@@ -981,7 +964,7 @@ export function create_client({ target }) {
|
|
|
981
964
|
/** @type {import('types').ServerDataNode | null} */
|
|
982
965
|
let server_data_node = null;
|
|
983
966
|
|
|
984
|
-
const default_layout_has_server_load = server_loads[0] === 0;
|
|
967
|
+
const default_layout_has_server_load = app.server_loads[0] === 0;
|
|
985
968
|
|
|
986
969
|
if (default_layout_has_server_load) {
|
|
987
970
|
// TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
|
|
@@ -1311,6 +1294,21 @@ export function create_client({ target }) {
|
|
|
1311
1294
|
after_navigate();
|
|
1312
1295
|
}
|
|
1313
1296
|
|
|
1297
|
+
/**
|
|
1298
|
+
* @param {unknown} error
|
|
1299
|
+
* @param {import('types').NavigationEvent} event
|
|
1300
|
+
* @returns {import('types').MaybePromise<App.Error>}
|
|
1301
|
+
*/
|
|
1302
|
+
function handle_error(error, event) {
|
|
1303
|
+
if (error instanceof HttpError) {
|
|
1304
|
+
return error.body;
|
|
1305
|
+
}
|
|
1306
|
+
return (
|
|
1307
|
+
app.hooks.handleError({ error, event }) ??
|
|
1308
|
+
/** @type {any} */ ({ message: event.route.id != null ? 'Internal Error' : 'Not Found' })
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1314
1312
|
return {
|
|
1315
1313
|
after_navigate: (fn) => {
|
|
1316
1314
|
onMount(() => {
|
|
@@ -1710,9 +1708,13 @@ export function create_client({ target }) {
|
|
|
1710
1708
|
try {
|
|
1711
1709
|
const branch_promises = node_ids.map(async (n, i) => {
|
|
1712
1710
|
const server_data_node = server_data_nodes[i];
|
|
1711
|
+
// Type isn't completely accurate, we still need to deserialize uses
|
|
1712
|
+
if (server_data_node?.uses) {
|
|
1713
|
+
server_data_node.uses = deserialize_uses(server_data_node.uses);
|
|
1714
|
+
}
|
|
1713
1715
|
|
|
1714
1716
|
return load_node({
|
|
1715
|
-
loader: nodes[n],
|
|
1717
|
+
loader: app.nodes[n],
|
|
1716
1718
|
url,
|
|
1717
1719
|
params,
|
|
1718
1720
|
route,
|
|
@@ -1760,7 +1762,7 @@ export function create_client({ target }) {
|
|
|
1760
1762
|
/**
|
|
1761
1763
|
* @param {URL} url
|
|
1762
1764
|
* @param {boolean[]} invalid
|
|
1763
|
-
* @returns {Promise<import('types').
|
|
1765
|
+
* @returns {Promise<import('types').ServerNodesResponse |import('types').ServerRedirectNode>}
|
|
1764
1766
|
*/
|
|
1765
1767
|
async function load_data(url, invalid) {
|
|
1766
1768
|
const data_url = new URL(url);
|
|
@@ -1774,44 +1776,98 @@ async function load_data(url, invalid) {
|
|
|
1774
1776
|
);
|
|
1775
1777
|
|
|
1776
1778
|
const res = await native_fetch(data_url.href);
|
|
1777
|
-
const data = await res.json();
|
|
1778
1779
|
|
|
1779
1780
|
if (!res.ok) {
|
|
1780
1781
|
// error message is a JSON-stringified string which devalue can't handle at the top level
|
|
1781
1782
|
// turn it into a HttpError to not call handleError on the client again (was already handled on the server)
|
|
1782
|
-
throw new HttpError(res.status,
|
|
1783
|
+
throw new HttpError(res.status, await res.json());
|
|
1783
1784
|
}
|
|
1784
1785
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1786
|
+
return new Promise(async (resolve) => {
|
|
1787
|
+
/**
|
|
1788
|
+
* Map of deferred promises that will be resolved by a subsequent chunk of data
|
|
1789
|
+
* @type {Map<string, import('types').Deferred>}
|
|
1790
|
+
*/
|
|
1791
|
+
const deferreds = new Map();
|
|
1792
|
+
const reader = /** @type {ReadableStream<Uint8Array>} */ (res.body).getReader();
|
|
1793
|
+
const decoder = new TextDecoder();
|
|
1794
|
+
|
|
1795
|
+
/**
|
|
1796
|
+
* @param {any} data
|
|
1797
|
+
*/
|
|
1798
|
+
function deserialize(data) {
|
|
1799
|
+
return devalue.unflatten(data, {
|
|
1800
|
+
Promise: (id) => {
|
|
1801
|
+
return new Promise((fulfil, reject) => {
|
|
1802
|
+
deferreds.set(id, { fulfil, reject });
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
let text = '';
|
|
1809
|
+
|
|
1810
|
+
while (true) {
|
|
1811
|
+
// Format follows ndjson (each line is a JSON object) or regular JSON spec
|
|
1812
|
+
const { done, value } = await reader.read();
|
|
1813
|
+
if (done && !text) break;
|
|
1814
|
+
|
|
1815
|
+
text += !value && text ? '\n' : decoder.decode(value); // no value -> final chunk -> add a new line to trigger the last parse
|
|
1816
|
+
|
|
1817
|
+
while (true) {
|
|
1818
|
+
const split = text.indexOf('\n');
|
|
1819
|
+
if (split === -1) {
|
|
1820
|
+
break;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const node = JSON.parse(text.slice(0, split));
|
|
1824
|
+
text = text.slice(split + 1);
|
|
1825
|
+
|
|
1826
|
+
if (node.type === 'redirect') {
|
|
1827
|
+
return resolve(node);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
if (node.type === 'data') {
|
|
1831
|
+
// This is the first (and possibly only, if no pending promises) chunk
|
|
1832
|
+
node.nodes?.forEach((/** @type {any} */ node) => {
|
|
1833
|
+
if (node?.type === 'data') {
|
|
1834
|
+
node.uses = deserialize_uses(node.uses);
|
|
1835
|
+
node.data = deserialize(node.data);
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
resolve(node);
|
|
1840
|
+
} else if (node.type === 'chunk') {
|
|
1841
|
+
// This is a subsequent chunk containing deferred data
|
|
1842
|
+
const { id, data, error } = node;
|
|
1843
|
+
const deferred = /** @type {import('types').Deferred} */ (deferreds.get(id));
|
|
1844
|
+
deferreds.delete(id);
|
|
1845
|
+
|
|
1846
|
+
if (error) {
|
|
1847
|
+
deferred.reject(deserialize(error));
|
|
1848
|
+
} else {
|
|
1849
|
+
deferred.fulfil(deserialize(data));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1796
1853
|
}
|
|
1797
1854
|
});
|
|
1798
1855
|
|
|
1799
|
-
|
|
1856
|
+
// TODO edge case handling necessary? stream() read fails?
|
|
1800
1857
|
}
|
|
1801
1858
|
|
|
1802
1859
|
/**
|
|
1803
|
-
* @param {
|
|
1804
|
-
* @
|
|
1805
|
-
* @returns {import('../../../types/private.js').MaybePromise<App.Error>}
|
|
1860
|
+
* @param {any} uses
|
|
1861
|
+
* @return {import('types').Uses}
|
|
1806
1862
|
*/
|
|
1807
|
-
function
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1863
|
+
function deserialize_uses(uses) {
|
|
1864
|
+
return {
|
|
1865
|
+
dependencies: new Set(uses?.dependencies ?? []),
|
|
1866
|
+
params: new Set(uses?.params ?? []),
|
|
1867
|
+
parent: !!uses?.parent,
|
|
1868
|
+
route: !!uses?.route,
|
|
1869
|
+
url: !!uses?.url
|
|
1870
|
+
};
|
|
1815
1871
|
}
|
|
1816
1872
|
|
|
1817
1873
|
function reset_focus() {
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { exec, parse_route_id } from '../../utils/routing.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @param {import('types').
|
|
5
|
-
* @param {number[]} server_loads
|
|
6
|
-
* @param {typeof import('__CLIENT__/manifest.js').dictionary} dictionary
|
|
7
|
-
* @param {Record<string, (param: string) => boolean>} matchers
|
|
4
|
+
* @param {import('./types').SvelteKitApp} app
|
|
8
5
|
* @returns {import('types').CSRRoute[]}
|
|
9
6
|
*/
|
|
10
|
-
export function parse(nodes, server_loads, dictionary, matchers) {
|
|
7
|
+
export function parse({ nodes, server_loads, dictionary, matchers }) {
|
|
11
8
|
const layouts_with_server_load = new Set(server_loads);
|
|
12
9
|
|
|
13
10
|
return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
|
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
import { DEV } from 'esm-env';
|
|
2
2
|
import { create_client } from './client.js';
|
|
3
3
|
import { init } from './singletons.js';
|
|
4
|
-
import { set_assets, set_version, set_public_env } from '../shared.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
* @param {
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* hydrate: Parameters<import('./types').Client['_hydrate']>[0];
|
|
11
|
-
* target: HTMLElement;
|
|
12
|
-
* version: string;
|
|
13
|
-
* }} opts
|
|
6
|
+
* @param {import('./types').SvelteKitApp} app
|
|
7
|
+
* @param {HTMLElement} target
|
|
8
|
+
* @param {Parameters<import('./types').Client['_hydrate']>[0]} [hydrate]
|
|
14
9
|
*/
|
|
15
|
-
export async function start(
|
|
16
|
-
set_public_env(env);
|
|
17
|
-
set_assets(assets);
|
|
18
|
-
set_version(version);
|
|
19
|
-
|
|
10
|
+
export async function start(app, target, hydrate) {
|
|
20
11
|
if (DEV && target === document.body) {
|
|
21
12
|
console.warn(
|
|
22
13
|
`Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>`
|
|
23
14
|
);
|
|
24
15
|
}
|
|
25
16
|
|
|
26
|
-
const client = create_client(
|
|
27
|
-
target
|
|
28
|
-
});
|
|
17
|
+
const client = create_client(app, target);
|
|
29
18
|
|
|
30
19
|
init({ client });
|
|
31
20
|
|
|
@@ -9,7 +9,43 @@ import {
|
|
|
9
9
|
preloadData
|
|
10
10
|
} from '$app/navigation';
|
|
11
11
|
import { SvelteComponent } from 'svelte';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
ClientHooks,
|
|
14
|
+
CSRPageNode,
|
|
15
|
+
CSRPageNodeLoader,
|
|
16
|
+
CSRRoute,
|
|
17
|
+
Page,
|
|
18
|
+
ParamMatcher,
|
|
19
|
+
TrailingSlash,
|
|
20
|
+
Uses
|
|
21
|
+
} from 'types';
|
|
22
|
+
|
|
23
|
+
export interface SvelteKitApp {
|
|
24
|
+
/**
|
|
25
|
+
* A list of all the error/layout/page nodes used in the app
|
|
26
|
+
*/
|
|
27
|
+
nodes: CSRPageNodeLoader[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A list of all layout node ids that have a server load function.
|
|
31
|
+
* Pages are not present because it's shorter to encode it on the leaf itself.
|
|
32
|
+
*/
|
|
33
|
+
server_loads: number[];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A map of `[routeId: string]: [leaf, layouts, errors]` tuples, which
|
|
37
|
+
* is parsed into an array of routes on startup. The numbers refer to the indices in `nodes`.
|
|
38
|
+
* If the leaf number is negative, it means it does use a server load function and the complement is the node index.
|
|
39
|
+
* The route layout and error nodes are not referenced, they are always number 0 and 1 and always apply.
|
|
40
|
+
*/
|
|
41
|
+
dictionary: Record<string, [leaf: number, layouts: number[], errors?: number[]]>;
|
|
42
|
+
|
|
43
|
+
matchers: Record<string, ParamMatcher>;
|
|
44
|
+
|
|
45
|
+
hooks: ClientHooks;
|
|
46
|
+
|
|
47
|
+
root: typeof SvelteComponent;
|
|
48
|
+
}
|
|
13
49
|
|
|
14
50
|
export interface Client {
|
|
15
51
|
// public API, exposed via $app/navigation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BROWSER, DEV } from 'esm-env';
|
|
2
2
|
import { writable } from 'svelte/store';
|
|
3
3
|
import { assets } from '__sveltekit/paths';
|
|
4
|
-
import { version } from '
|
|
4
|
+
import { version } from '__sveltekit/environment';
|
|
5
5
|
import { PRELOAD_PRIORITIES } from './constants.js';
|
|
6
6
|
|
|
7
7
|
/* global __SVELTEKIT_APP_VERSION_FILE__, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__ */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { private_env as env } from '../../shared.js';
|
|
1
|
+
export { private_env as env } from '../../shared-server.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { public_env as env } from '../../shared.js';
|
|
1
|
+
export { public_env as env } from '../../shared-server.js';
|
|
@@ -2,9 +2,11 @@ import { HttpError, Redirect } from '../../control.js';
|
|
|
2
2
|
import { normalize_error } from '../../../utils/error.js';
|
|
3
3
|
import { once } from '../../../utils/functions.js';
|
|
4
4
|
import { load_server_data } from '../page/load_data.js';
|
|
5
|
-
import { clarify_devalue_error, handle_error_and_jsonify,
|
|
5
|
+
import { clarify_devalue_error, handle_error_and_jsonify, stringify_uses } from '../utils.js';
|
|
6
6
|
import { normalize_path } from '../../../utils/url.js';
|
|
7
7
|
import { text } from '../../../exports/index.js';
|
|
8
|
+
import * as devalue from 'devalue';
|
|
9
|
+
import { create_async_iterator } from '../../../utils/streaming.js';
|
|
8
10
|
|
|
9
11
|
export const INVALIDATED_PARAM = 'x-sveltekit-invalidated';
|
|
10
12
|
|
|
@@ -58,6 +60,7 @@ export async function render_data(
|
|
|
58
60
|
|
|
59
61
|
// == because it could be undefined (in dev) or null (in build, because of JSON.stringify)
|
|
60
62
|
const node = n == undefined ? n : await manifest._.nodes[n]();
|
|
63
|
+
// load this. for the child, return as is. for the final result, stream things
|
|
61
64
|
return load_server_data({
|
|
62
65
|
event: new_event,
|
|
63
66
|
state,
|
|
@@ -114,33 +117,50 @@ export async function render_data(
|
|
|
114
117
|
)
|
|
115
118
|
);
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
const stubs = nodes.slice(0, length).map(serialize_data_node);
|
|
120
|
+
const { data, chunks } = get_data_json(event, options, nodes);
|
|
119
121
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return json_response(JSON.stringify(clarify_devalue_error(event, error)), 500);
|
|
122
|
+
if (!chunks) {
|
|
123
|
+
// use a normal JSON response where possible, so we get `content-length`
|
|
124
|
+
// and can use browser JSON devtools for easier inspecting
|
|
125
|
+
return json_response(data);
|
|
125
126
|
}
|
|
127
|
+
|
|
128
|
+
return new Response(
|
|
129
|
+
new ReadableStream({
|
|
130
|
+
async start(controller) {
|
|
131
|
+
controller.enqueue(data);
|
|
132
|
+
for await (const chunk of chunks) {
|
|
133
|
+
controller.enqueue(chunk);
|
|
134
|
+
}
|
|
135
|
+
controller.close();
|
|
136
|
+
}
|
|
137
|
+
}),
|
|
138
|
+
{
|
|
139
|
+
headers: {
|
|
140
|
+
// text/plain isn't strictly correct, but it makes it easier to inspect
|
|
141
|
+
// the data, and doesn't affect how it is consumed by the client
|
|
142
|
+
'content-type': 'text/plain',
|
|
143
|
+
'cache-control': 'private, no-store'
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
);
|
|
126
147
|
} catch (e) {
|
|
127
148
|
const error = normalize_error(e);
|
|
128
149
|
|
|
129
150
|
if (error instanceof Redirect) {
|
|
130
151
|
return redirect_json_response(error);
|
|
131
152
|
} else {
|
|
132
|
-
|
|
133
|
-
return json_response(JSON.stringify(await handle_error_and_jsonify(event, options, error)));
|
|
153
|
+
return json_response(await handle_error_and_jsonify(event, options, error), 500);
|
|
134
154
|
}
|
|
135
155
|
}
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
/**
|
|
139
|
-
* @param {string} json
|
|
159
|
+
* @param {Record<string, any> | string} json
|
|
140
160
|
* @param {number} [status]
|
|
141
161
|
*/
|
|
142
162
|
function json_response(json, status = 200) {
|
|
143
|
-
return text(json, {
|
|
163
|
+
return text(typeof json === 'string' ? json : JSON.stringify(json), {
|
|
144
164
|
status,
|
|
145
165
|
headers: {
|
|
146
166
|
'content-type': 'application/json',
|
|
@@ -153,10 +173,90 @@ function json_response(json, status = 200) {
|
|
|
153
173
|
* @param {Redirect} redirect
|
|
154
174
|
*/
|
|
155
175
|
export function redirect_json_response(redirect) {
|
|
156
|
-
return json_response(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
176
|
+
return json_response({
|
|
177
|
+
type: 'redirect',
|
|
178
|
+
location: redirect.location
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* If the serialized data contains promises, `chunks` will be an
|
|
184
|
+
* async iterable containing their resolutions
|
|
185
|
+
* @param {import('types').RequestEvent} event
|
|
186
|
+
* @param {import('types').SSROptions} options
|
|
187
|
+
* @param {Array<import('types').ServerDataSkippedNode | import('types').ServerDataNode | import('types').ServerErrorNode | null | undefined>} nodes
|
|
188
|
+
* @returns {{ data: string, chunks: AsyncIterable<string> | null }}
|
|
189
|
+
*/
|
|
190
|
+
export function get_data_json(event, options, nodes) {
|
|
191
|
+
let promise_id = 1;
|
|
192
|
+
let count = 0;
|
|
193
|
+
|
|
194
|
+
const { iterator, push, done } = create_async_iterator();
|
|
195
|
+
|
|
196
|
+
const reducers = {
|
|
197
|
+
/** @param {any} thing */
|
|
198
|
+
Promise: (thing) => {
|
|
199
|
+
if (typeof thing?.then === 'function') {
|
|
200
|
+
const id = promise_id++;
|
|
201
|
+
count += 1;
|
|
202
|
+
|
|
203
|
+
/** @type {'data' | 'error'} */
|
|
204
|
+
let key = 'data';
|
|
205
|
+
|
|
206
|
+
thing
|
|
207
|
+
.catch(
|
|
208
|
+
/** @param {any} e */ async (e) => {
|
|
209
|
+
key = 'error';
|
|
210
|
+
return handle_error_and_jsonify(event, options, /** @type {any} */ (e));
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
.then(
|
|
214
|
+
/** @param {any} value */
|
|
215
|
+
async (value) => {
|
|
216
|
+
let str;
|
|
217
|
+
try {
|
|
218
|
+
str = devalue.stringify(value, reducers);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
const error = await handle_error_and_jsonify(
|
|
221
|
+
event,
|
|
222
|
+
options,
|
|
223
|
+
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
key = 'error';
|
|
227
|
+
str = devalue.stringify(error, reducers);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
count -= 1;
|
|
231
|
+
|
|
232
|
+
push(`{"type":"chunk","id":${id},"${key}":${str}}\n`);
|
|
233
|
+
if (count === 0) done();
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return id;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const strings = nodes.map((node) => {
|
|
244
|
+
if (!node) return 'null';
|
|
245
|
+
|
|
246
|
+
if (node.type === 'error' || node.type === 'skip') {
|
|
247
|
+
return JSON.stringify(node);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return `{"type":"data","data":${devalue.stringify(node.data, reducers)},${stringify_uses(
|
|
251
|
+
node
|
|
252
|
+
)}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
data: `{"type":"data","nodes":[${strings.join(',')}]}\n`,
|
|
257
|
+
chunks: count > 0 ? iterator : null
|
|
258
|
+
};
|
|
259
|
+
} catch (e) {
|
|
260
|
+
throw new Error(clarify_devalue_error(event, /** @type {any} */ (e)));
|
|
261
|
+
}
|
|
162
262
|
}
|
|
@@ -3,12 +3,7 @@ import { compact } from '../../../utils/array.js';
|
|
|
3
3
|
import { normalize_error } from '../../../utils/error.js';
|
|
4
4
|
import { add_data_suffix } from '../../../utils/url.js';
|
|
5
5
|
import { HttpError, Redirect } from '../../control.js';
|
|
6
|
-
import {
|
|
7
|
-
redirect_response,
|
|
8
|
-
static_error_page,
|
|
9
|
-
handle_error_and_jsonify,
|
|
10
|
-
serialize_data_node
|
|
11
|
-
} from '../utils.js';
|
|
6
|
+
import { redirect_response, static_error_page, handle_error_and_jsonify } from '../utils.js';
|
|
12
7
|
import {
|
|
13
8
|
handle_action_json_request,
|
|
14
9
|
handle_action_request,
|
|
@@ -19,6 +14,7 @@ import { load_data, load_server_data } from './load_data.js';
|
|
|
19
14
|
import { render_response } from './render.js';
|
|
20
15
|
import { respond_with_error } from './respond_with_error.js';
|
|
21
16
|
import { get_option } from '../../../utils/options.js';
|
|
17
|
+
import { get_data_json } from '../data/index.js';
|
|
22
18
|
|
|
23
19
|
/**
|
|
24
20
|
* @param {import('types').RequestEvent} event
|
|
@@ -290,13 +286,22 @@ export async function render_page(event, route, page, options, manifest, state,
|
|
|
290
286
|
}
|
|
291
287
|
|
|
292
288
|
if (state.prerendering && should_prerender_data) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
// ndjson format
|
|
290
|
+
let { data, chunks } = get_data_json(
|
|
291
|
+
event,
|
|
292
|
+
options,
|
|
293
|
+
branch.map((node) => node?.server_data)
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
if (chunks) {
|
|
297
|
+
for await (const chunk of chunks) {
|
|
298
|
+
data += chunk;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
296
301
|
|
|
297
302
|
state.prerendering.dependencies.set(data_pathname, {
|
|
298
|
-
response: text(
|
|
299
|
-
body
|
|
303
|
+
response: text(data),
|
|
304
|
+
body: data
|
|
300
305
|
});
|
|
301
306
|
}
|
|
302
307
|
|