Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow server-only load functions to return more than JSON #6318

Merged
merged 25 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
serialize initial data with devalue
  • Loading branch information
Rich-Harris committed Aug 26, 2022
commit 1985b5608107f9da3e05c7375d13c1631a5cfb59
28 changes: 9 additions & 19 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1281,31 +1281,21 @@ export function create_client({ target, base, trailing_slash }) {
});
},

_hydrate: async ({ status, error, node_ids, params, routeId }) => {
_hydrate: async ({
status,
error,
node_ids,
params,
routeId,
data: server_data_nodes,
errors: validation_errors
}) => {
const url = new URL(location.href);

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

try {
/**
* @param {string} type
* @param {any} fallback
*/
const parse = (type, fallback) => {
const script = document.querySelector(`script[sveltekit\\:data-type="${type}"]`);
return script?.textContent ? JSON.parse(script.textContent) : fallback;
};
/**
* @type {Array<import('types').ServerDataNode | null>}
* On initial navigation, this will only consist of data nodes or `null`.
* A possible error is passed through the `error` property, in which case
* the last entry of `node_ids` is an error page and the last entry of
* `server_data_nodes` is `null`.
*/
const server_data_nodes = parse('server_data', []);
const validation_errors = parse('validation_errors', undefined);

const branch_promises = node_ids.map(async (n, i) => {
return load_node({
loader: nodes[n],
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/runtime/client/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export { set_public_env } from '../env-public.js';
* node_ids: number[];
* params: Record<string, string>;
* routeId: string | null;
* data: Array<import('types').ServerDataNode | import('types').ServerErrorNode | null>;
* errors: Record<string, any> | null;
* };
* }} opts
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/runtime/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface Client {
node_ids: number[];
params: Record<string, string>;
routeId: string | null;
data: Array<import('types').ServerDataNode | import('types').ServerErrorNode | null>;
errors: Record<string, any>;
}) => Promise<void>;
_start_router: () => void;
}
Expand Down
1 change: 0 additions & 1 deletion packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@ export async function respond(request, options, state) {
// == because it could be undefined (in dev) or null (in build, because of JSON.stringify)
const node = n == undefined ? n : await options.manifest._.nodes[n]();
return load_server_data({
dev: options.dev,
event,
state,
node,
Expand Down
1 change: 0 additions & 1 deletion packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ export async function render_page(event, route, page, options, state, resolve_op
}

return await load_server_data({
dev: options.dev,
event,
state,
node,
Expand Down
46 changes: 1 addition & 45 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import { disable_search, make_trackable } from '../../../utils/url.js';
/**
* Calls the user's `load` function.
* @param {{
* dev: boolean;
* event: import('types').RequestEvent;
* state: import('types').SSRState;
* node: import('types').SSRNode | undefined;
* parent: () => Promise<Record<string, any>>;
* }} opts
* @returns {Promise<import('types').ServerDataNode | null>}
*/
export async function load_server_data({ dev, event, state, node, parent }) {
export async function load_server_data({ event, state, node, parent }) {
if (!node?.server) return null;

const uses = {
Expand Down Expand Up @@ -53,10 +52,6 @@ export async function load_server_data({ dev, event, state, node, parent }) {

const data = result ? await unwrap_promises(result) : null;

if (dev) {
check_serializability(data, /** @type {string} */ (node.server_id), 'data');
}

return {
type: 'data',
data,
Expand Down Expand Up @@ -127,42 +122,3 @@ async function unwrap_promises(object) {

return unwrapped;
}

/**
* Check that the data can safely be serialized to JSON
* @param {any} value
* @param {string} id
* @param {string} path
*/
function check_serializability(value, id, path) {
const type = typeof value;

if (type === 'string' || type === 'boolean' || type === 'number' || type === 'undefined') {
// primitives are fine
return;
}

if (type === 'object') {
// nulls are fine...
if (!value) return;

// ...so are plain arrays...
if (Array.isArray(value)) {
value.forEach((child, i) => {
check_serializability(child, id, `${path}[${i}]`);
});
return;
}

// ...and objects
const tag = Object.prototype.toString.call(value);
if (tag === '[object Object]') {
for (const key in value) {
check_serializability(value[key], id, `${path}.${key}`);
}
return;
}
}

throw new Error(`${path} returned from 'load' in ${id} cannot be serialized as JSON`);
}
4 changes: 3 additions & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ export async function render_response({
error: ${error && serialize_error(error, e => e.stack)},
node_ids: [${branch.map(({ node }) => node.index).join(', ')}],
params: ${devalue(event.params)},
routeId: ${s(event.routeId)}
routeId: ${s(event.routeId)},
data: ${devalue(branch.map(({ server_data }) => server_data))},
errors: ${validation_errors ? devalue(validation_errors) : 'null'}
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
}` : 'null'}
});
`;
Expand Down
1 change: 0 additions & 1 deletion packages/kit/src/runtime/server/page/respond_with_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export async function respond_with_error({ event, options, state, status, error,
const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout

const server_data_promise = load_server_data({
dev: options.dev,
event,
state,
node: default_layout,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="/load/devalue/regex">/load/devalue/regex</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('./$types').PageServerLoad} */
export function load() {
return {
regex: /hello/
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
/** @type {import('./$types').PageData} */
export let data;
</script>

<h1>{data.regex.test('hello')}</h1>
7 changes: 7 additions & 0 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,13 @@ test.describe('Load', () => {
expect(await page.textContent('h1')).toBe('foo.bar: Custom layout');
expect(await page.textContent('h2')).toBe('pagedata: pagedata');
});

test.only('Serializes non-JSON data', async ({ page, clicknav }) => {
await page.goto('/load/devalue');
await clicknav('[href="/load/devalue/regex"]');

expect(await page.textContent('h1')).toBe(true);
});
});

test.describe('Method overrides', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// vite.config.js
import * as path from "path";
import { sveltekit } from "@sveltejs/kit/vite";
var config = {
build: {
minify: false
},
clearScreen: false,
optimizeDeps: {
include: ["cookie", "marked"]
},
plugins: [sveltekit()],
server: {
fs: {
allow: [path.resolve("../../../src")]
}
}
};
var vite_config_default = config;
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvcmljaC9EZXZlbG9wbWVudC9TVkVMVEUvS0lUL2tpdC9wYWNrYWdlcy9raXQvdGVzdC9hcHBzL2Jhc2ljc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL3JpY2gvRGV2ZWxvcG1lbnQvU1ZFTFRFL0tJVC9raXQvcGFja2FnZXMva2l0L3Rlc3QvYXBwcy9iYXNpY3Mvdml0ZS5jb25maWcuanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL3JpY2gvRGV2ZWxvcG1lbnQvU1ZFTFRFL0tJVC9raXQvcGFja2FnZXMva2l0L3Rlc3QvYXBwcy9iYXNpY3Mvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgc3ZlbHRla2l0IH0gZnJvbSAnQHN2ZWx0ZWpzL2tpdC92aXRlJztcblxuLyoqIEB0eXBlIHtpbXBvcnQoJ3ZpdGUnKS5Vc2VyQ29uZmlnfSAqL1xuY29uc3QgY29uZmlnID0ge1xuXHRidWlsZDoge1xuXHRcdG1pbmlmeTogZmFsc2Vcblx0fSxcblx0Y2xlYXJTY3JlZW46IGZhbHNlLFxuXHRvcHRpbWl6ZURlcHM6IHtcblx0XHQvLyBmb3IgQ0ksIHdlIG5lZWQgdG8gZXhwbGljaXRseSBwcmVidW5kbGUgZGVwcywgc2luY2Vcblx0XHQvLyB0aGUgcmVsb2FkIGNvbmZ1c2VzIFBsYXl3cmlnaHRcblx0XHRpbmNsdWRlOiBbJ2Nvb2tpZScsICdtYXJrZWQnXVxuXHR9LFxuXHRwbHVnaW5zOiBbc3ZlbHRla2l0KCldLFxuXHRzZXJ2ZXI6IHtcblx0XHRmczoge1xuXHRcdFx0YWxsb3c6IFtwYXRoLnJlc29sdmUoJy4uLy4uLy4uL3NyYycpXVxuXHRcdH1cblx0fVxufTtcblxuZXhwb3J0IGRlZmF1bHQgY29uZmlnO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE4WCxZQUFZLFVBQVU7QUFDcFosU0FBUyxpQkFBaUI7QUFHMUIsSUFBTSxTQUFTO0FBQUEsRUFDZCxPQUFPO0FBQUEsSUFDTixRQUFRO0FBQUEsRUFDVDtBQUFBLEVBQ0EsYUFBYTtBQUFBLEVBQ2IsY0FBYztBQUFBLElBR2IsU0FBUyxDQUFDLFVBQVUsUUFBUTtBQUFBLEVBQzdCO0FBQUEsRUFDQSxTQUFTLENBQUMsVUFBVSxDQUFDO0FBQUEsRUFDckIsUUFBUTtBQUFBLElBQ1AsSUFBSTtBQUFBLE1BQ0gsT0FBTyxDQUFNLGFBQVEsY0FBYyxDQUFDO0FBQUEsSUFDckM7QUFBQSxFQUNEO0FBQ0Q7QUFFQSxJQUFPLHNCQUFROyIsCiAgIm5hbWVzIjogW10KfQo=