Skip to content

Commit c530d33

Browse files
authored
[feat] Allow server-only load functions to return more than JSON (#6318)
closes #6008 fixes #6357 This allows load functions to return things like Date objects, regexes, Map and Set, BigInt and so on. It also allows repeated and cyclical references, for the times when they're useful.
1 parent c228724 commit c530d33

File tree

31 files changed

+423
-314
lines changed

31 files changed

+423
-314
lines changed

.changeset/tender-spiders-fail.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@sveltejs/adapter-netlify': patch
3+
'@sveltejs/adapter-vercel': patch
4+
'@sveltejs/kit': patch
5+
---
6+
7+
Use devalue to serialize server-only `load` return values

documentation/docs/03-routing.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export async function load({ params }) {
106106
}
107107
```
108108

109-
During client-side navigation, SvelteKit will load this data using `fetch`, which means that the returned value must be serializable as JSON.
109+
During client-side navigation, SvelteKit will load this data from the server, which means that the returned value must be serializable using [devalue](https://github.com/rich-harris/devalue).
110110

111111
#### Actions
112112

documentation/docs/05-load.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Loading data
44

55
A [`+page.svelte`](/docs/routing#page-page-svelte) or [`+layout.svelte`](/docs/routing#layout-layout-svelte) gets its `data` from a `load` function.
66

7-
If the `load` function is defined in `+page.js` or `+layout.js` it will run both on the server and in the browser. If it's instead defined in `+page.server.js` or `+layout.server.js` it will only run on the server, in which case it can (for example) make database calls and access private [environment variables](/docs/modules#$env-static-private), but can only return data that can be serialized as JSON. In both cases, the return value (if there is one) must be an object.
7+
If the `load` function is defined in `+page.js` or `+layout.js` it will run both on the server and in the browser. If it's instead defined in `+page.server.js` or `+layout.server.js` it will only run on the server, in which case it can (for example) make database calls and access private [environment variables](/docs/modules#$env-static-private), but can only return data that can be serialized with [devalue](https://github.com/rich-harris/devalue). In both cases, the return value (if there is one) must be an object.
88

99
```js
1010
/// file: src/routes/+page.js
@@ -256,7 +256,7 @@ export async function load({ setHeaders }) {
256256

257257
### Output
258258

259-
The returned `data`, if any, must be an object of values. For a server-only `load` function, these values must be JSON-serializable. Top-level promises will be awaited, which makes it easy to return multiple promises without creating a waterfall:
259+
The returned `data`, if any, must be an object of values. For a server-only `load` function, these values must be serializable with [devalue](https://github.com/rich-harris/devalue). Top-level promises will be awaited, which makes it easy to return multiple promises without creating a waterfall:
260260

261261
```js
262262
// @filename: $types.d.ts

packages/adapter-netlify/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ async function generate_lambda_functions({ builder, publish, split, esm }) {
211211
writeFileSync(`.netlify/functions-internal/${name}.js`, fn);
212212

213213
redirects.push(`${pattern} /.netlify/functions/${name} 200`);
214-
redirects.push(`${pattern}/__data.json /.netlify/functions/${name} 200`);
214+
redirects.push(`${pattern}/__data.js /.netlify/functions/${name} 200`);
215215
}
216216
};
217217
});

packages/adapter-vercel/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export default function ({ external = [], edge, split } = {}) {
224224
sliced_pattern = '^/?';
225225
}
226226

227-
const src = `${sliced_pattern}(?:/__data.json)?$`; // TODO adding /__data.json is a temporary workaround — those endpoints should be treated as distinct routes
227+
const src = `${sliced_pattern}(?:/__data.js)?$`; // TODO adding /__data.js is a temporary workaround — those endpoints should be treated as distinct routes
228228

229229
await generate_function(route.id || 'index', src, entry.generateManifest);
230230
}

packages/kit/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"dependencies": {
1313
"@sveltejs/vite-plugin-svelte": "^1.0.1",
1414
"cookie": "^0.5.0",
15-
"devalue": "^2.0.1",
15+
"devalue": "^3.1.2",
1616
"kleur": "^4.1.4",
1717
"magic-string": "^0.26.2",
1818
"mime": "^3.0.0",

packages/kit/src/core/constants.js renamed to packages/kit/src/constants.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
export const SVELTE_KIT_ASSETS = '/_svelte_kit_assets';
44

55
export const GENERATED_COMMENT = '// this file is generated — do not edit it\n';
6+
7+
export const DATA_SUFFIX = '/__data.js';

packages/kit/src/core/env.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GENERATED_COMMENT } from './constants.js';
1+
import { GENERATED_COMMENT } from '../constants.js';
22
import { runtime_base } from './utils.js';
33

44
/**

packages/kit/src/core/sync/write_ambient.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path';
22
import { get_env } from '../../exports/vite/utils.js';
3-
import { GENERATED_COMMENT } from '../constants.js';
3+
import { GENERATED_COMMENT } from '../../constants.js';
44
import { create_types } from '../env.js';
55
import { write_if_changed } from './utils.js';
66

packages/kit/src/exports/vite/dev/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { installPolyfills } from '../../../exports/node/polyfills.js';
88
import { coalesce_to_error } from '../../../utils/error.js';
99
import { posixify } from '../../../utils/filesystem.js';
1010
import { load_template } from '../../../core/config/index.js';
11-
import { SVELTE_KIT_ASSETS } from '../../../core/constants.js';
11+
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
1212
import * as sync from '../../../core/sync/sync.js';
1313
import { get_mime_lookup, runtime_base, runtime_prefix } from '../../../core/utils.js';
1414
import { get_env, prevent_illegal_vite_imports, resolve_entry } from '../utils.js';

packages/kit/src/exports/vite/preview/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import sirv from 'sirv';
44
import { pathToFileURL } from 'url';
55
import { getRequest, setResponse } from '../../../exports/node/index.js';
66
import { installPolyfills } from '../../../exports/node/polyfills.js';
7-
import { SVELTE_KIT_ASSETS } from '../../../core/constants.js';
7+
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
88
import { loadEnv } from 'vite';
99

1010
/** @typedef {import('http').IncomingMessage} Req */

packages/kit/src/runtime/client/client.js

+69-54
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Root from '__GENERATED__/root.svelte';
1010
import { nodes, server_loads, dictionary, matchers } from '__GENERATED__/client-manifest.js';
1111
import { HttpError, Redirect } from '../control.js';
1212
import { stores } from './singletons.js';
13+
import { DATA_SUFFIX } from '../../constants.js';
1314

1415
const SCROLL_KEY = 'sveltekit:scroll';
1516
const INDEX_KEY = 'sveltekit:index';
@@ -393,7 +394,7 @@ export function create_client({ target, base, trailing_slash }) {
393394
* status: number;
394395
* error: HttpError | Error | null;
395396
* routeId: string | null;
396-
* validation_errors?: string | undefined;
397+
* validation_errors?: Record<string, any> | null;
397398
* }} opts
398399
*/
399400
async function get_navigation_result_from_branch({
@@ -715,24 +716,14 @@ export function create_client({ target, base, trailing_slash }) {
715716

716717
if (invalid_server_nodes.some(Boolean)) {
717718
try {
718-
const res = await native_fetch(
719-
`${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
720-
{
721-
headers: {
722-
'x-sveltekit-invalidated': invalid_server_nodes.map((x) => (x ? '1' : '')).join(',')
723-
}
724-
}
725-
);
726-
727-
server_data = /** @type {import('types').ServerData} */ (await res.json());
728-
729-
if (!res.ok) {
730-
throw server_data;
731-
}
732-
} catch (e) {
733-
// something went catastrophically wrong — bail and defer to the server
734-
native_navigation(url);
735-
return;
719+
server_data = await load_data(url, invalid_server_nodes);
720+
} catch (error) {
721+
return load_root_error_page({
722+
status: 500,
723+
error: /** @type {Error} */ (error),
724+
url,
725+
routeId: route.id
726+
});
736727
}
737728

738729
if (server_data.type === 'redirect') {
@@ -882,19 +873,18 @@ export function create_client({ target, base, trailing_slash }) {
882873
if (node.server) {
883874
// TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
884875
// existing root layout data
885-
const res = await native_fetch(
886-
`${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
887-
{
888-
headers: {
889-
'x-sveltekit-invalidated': '1'
890-
}
891-
}
892-
);
876+
try {
877+
const server_data = await load_data(url, [true]);
893878

894-
const server_data_nodes = await res.json();
895-
server_data_node = server_data_nodes?.[0] ?? null;
879+
if (
880+
server_data.type !== 'data' ||
881+
(server_data.nodes[0] && server_data.nodes[0].type !== 'data')
882+
) {
883+
throw 0;
884+
}
896885

897-
if (!res.ok || server_data_nodes?.type !== 'data') {
886+
server_data_node = server_data.nodes[0] ?? null;
887+
} catch {
898888
// at this point we have no choice but to fall back to the server
899889
native_navigation(url);
900890

@@ -1298,32 +1288,24 @@ export function create_client({ target, base, trailing_slash }) {
12981288
});
12991289
},
13001290

1301-
_hydrate: async ({ status, error, node_ids, params, routeId }) => {
1291+
_hydrate: async ({
1292+
status,
1293+
error: original_error, // TODO get rid of this
1294+
node_ids,
1295+
params,
1296+
routeId,
1297+
data: server_data_nodes,
1298+
errors: validation_errors
1299+
}) => {
13021300
const url = new URL(location.href);
13031301

13041302
/** @type {import('./types').NavigationFinished | undefined} */
13051303
let result;
13061304

13071305
try {
1308-
/**
1309-
* @param {string} type
1310-
* @param {any} fallback
1311-
*/
1312-
const parse = (type, fallback) => {
1313-
const script = document.querySelector(`script[sveltekit\\:data-type="${type}"]`);
1314-
return script?.textContent ? JSON.parse(script.textContent) : fallback;
1315-
};
1316-
/**
1317-
* @type {Array<import('types').ServerDataNode | null>}
1318-
* On initial navigation, this will only consist of data nodes or `null`.
1319-
* A possible error is passed through the `error` property, in which case
1320-
* the last entry of `node_ids` is an error page and the last entry of
1321-
* `server_data_nodes` is `null`.
1322-
*/
1323-
const server_data_nodes = parse('server_data', []);
1324-
const validation_errors = parse('validation_errors', undefined);
1325-
13261306
const branch_promises = node_ids.map(async (n, i) => {
1307+
const server_data_node = server_data_nodes[i];
1308+
13271309
return load_node({
13281310
loader: nodes[n],
13291311
url,
@@ -1336,7 +1318,7 @@ export function create_client({ target, base, trailing_slash }) {
13361318
}
13371319
return data;
13381320
},
1339-
server_data_node: create_data_node(server_data_nodes[i])
1321+
server_data_node: create_data_node(server_data_node)
13401322
});
13411323
});
13421324

@@ -1345,13 +1327,15 @@ export function create_client({ target, base, trailing_slash }) {
13451327
params,
13461328
branch: await Promise.all(branch_promises),
13471329
status,
1348-
error: /** @type {import('../server/page/types').SerializedHttpError} */ (error)
1330+
error: /** @type {import('../server/page/types').SerializedHttpError} */ (original_error)
13491331
?.__is_http_error
13501332
? new HttpError(
1351-
/** @type {import('../server/page/types').SerializedHttpError} */ (error).status,
1352-
error.message
1333+
/** @type {import('../server/page/types').SerializedHttpError} */ (
1334+
original_error
1335+
).status,
1336+
original_error.message
13531337
)
1354-
: error,
1338+
: original_error,
13551339
validation_errors,
13561340
routeId
13571341
});
@@ -1377,3 +1361,34 @@ export function create_client({ target, base, trailing_slash }) {
13771361
}
13781362
};
13791363
}
1364+
1365+
let data_id = 1;
1366+
1367+
/**
1368+
* @param {URL} url
1369+
* @param {boolean[]} invalid
1370+
* @returns {Promise<import('types').ServerData>}
1371+
*/
1372+
async function load_data(url, invalid) {
1373+
const data_url = new URL(url);
1374+
data_url.pathname = url.pathname.replace(/\/$/, '') + DATA_SUFFIX;
1375+
data_url.searchParams.set('__invalid', invalid.map((x) => (x ? 'y' : 'n')).join(''));
1376+
data_url.searchParams.set('__id', String(data_id++));
1377+
1378+
// The __data.js file is generated by the server and looks like
1379+
// `window.__sveltekit_data = ${devalue(data)}`. We do this instead
1380+
// of `export const data` because modules are cached indefinitely,
1381+
// and that would cause memory leaks.
1382+
//
1383+
// The data is read and deleted in the same tick as the promise
1384+
// resolves, so it's not vulnerable to race conditions
1385+
await import(/* @vite-ignore */ data_url.href);
1386+
1387+
// @ts-expect-error
1388+
const server_data = window.__sveltekit_data;
1389+
1390+
// @ts-expect-error
1391+
delete window.__sveltekit_data;
1392+
1393+
return server_data;
1394+
}

packages/kit/src/runtime/client/start.js

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export { set_public_env } from '../env-public.js';
2020
* node_ids: number[];
2121
* params: Record<string, string>;
2222
* routeId: string | null;
23+
* data: Array<import('types').ServerDataNode | null>;
24+
* errors: Record<string, any> | null;
2325
* };
2426
* }} opts
2527
*/

packages/kit/src/runtime/client/types.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface Client {
2727
node_ids: number[];
2828
params: Record<string, string>;
2929
routeId: string | null;
30+
data: Array<import('types').ServerDataNode | null>;
31+
errors: Record<string, any> | null;
3032
}) => Promise<void>;
3133
_start_router: () => void;
3234
}

0 commit comments

Comments
 (0)