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
Prev Previous commit
Next Next commit
generate __data.js
  • Loading branch information
Rich-Harris committed Aug 26, 2022
commit 24b1984b9d2f3ceafa1917facca9f6845fb48619
132 changes: 132 additions & 0 deletions packages/kit/src/runtime/server/data/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { HttpError, Redirect } from '../../../index/private.js';
import { normalize_error } from '../../../utils/error.js';
import { once } from '../../../utils/functions.js';
import { load_server_data } from '../page/load_data.js';
import { error_to_pojo } from '../utils.js';
import devalue from 'devalue';

/**
* @param {import('types').RequestEvent} event
* @param {import('types').SSRRoute} route
* @param {import('types').SSROptions} options
* @param {import('types').SSRState} state
* @returns {Promise<Response>}
*/
export async function render_data(event, route, options, state) {
if (!route.page) {
// requesting /__data.js should fail for a +server.js
return new Response(undefined, {
status: 404
});
}

try {
const node_ids = [...route.page.layouts, route.page.leaf];

const invalidated =
event.request.headers.get('x-sveltekit-invalidated')?.split(',').map(Boolean) ??
node_ids.map(() => true);

let aborted = false;

const functions = node_ids.map((n, i) => {
return once(async () => {
try {
if (aborted) {
return /** @type {import('types').ServerDataSkippedNode} */ ({
type: 'skip'
});
}

// == 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({
event,
state,
node,
parent: async () => {
/** @type {Record<string, any>} */
const data = {};
for (let j = 0; j < i; j += 1) {
const parent = /** @type {import('types').ServerDataNode | null} */ (
await functions[j]()
);

if (parent) {
Object.assign(data, parent.data);
}
}
return data;
}
});
} catch (e) {
aborted = true;
throw e;
}
});
});

const promises = functions.map(async (fn, i) => {
if (!invalidated[i]) {
return /** @type {import('types').ServerDataSkippedNode} */ ({
type: 'skip'
});
}

return fn();
});

let length = promises.length;
const nodes = await Promise.all(
promises.map((p, i) =>
p.catch((e) => {
const error = normalize_error(e);

if (error instanceof Redirect) {
throw error;
}

// Math.min because array isn't guaranteed to resolve in order
length = Math.min(length, i + 1);

if (error instanceof HttpError) {
return /** @type {import('types').ServerErrorNode} */ ({
type: 'error',
httperror: { ...error }
});
}

options.handle_error(error, event);

return /** @type {import('types').ServerErrorNode} */ ({
type: 'error',
error: error_to_pojo(error, options.get_stack)
});
})
)
);

/** @type {import('types').ServerData} */
const server_data = {
type: 'data',
nodes: nodes.slice(0, length)
};

return new Response(`window.__data = ${devalue(server_data)}`);
} catch (e) {
const error = normalize_error(e);

if (error instanceof Redirect) {
/** @type {import('types').ServerData} */
const server_data = {
type: 'redirect',
location: error.location
};

return new Response(`window.__data = ${devalue(server_data)}`);
} else {
// TODO make it clearer that this was an unexpected error
return new Response(`window.__data = ${devalue(error_to_pojo(error, options.get_stack))}`);
}
}
}
161 changes: 20 additions & 141 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@ import { render_endpoint } from './endpoint.js';
import { render_page } from './page/index.js';
import { render_response } from './page/render.js';
import { respond_with_error } from './page/respond_with_error.js';
import { coalesce_to_error, normalize_error } from '../../utils/error.js';
import { serialize_error, GENERIC_ERROR, error_to_pojo } from './utils.js';
import { coalesce_to_error } from '../../utils/error.js';
import { serialize_error, GENERIC_ERROR } from './utils.js';
import { decode_params, disable_search, normalize_path } from '../../utils/url.js';
import { exec } from '../../utils/routing.js';
import { negotiate } from '../../utils/http.js';
import { HttpError, Redirect } from '../../index/private.js';
import { load_server_data } from './page/load_data.js';
import { json } from '../../index/index.js';
import { once } from '../../utils/functions.js';
import { render_data } from './data/index.js';

/* global __SVELTEKIT_ADAPTER_NAME__ */

const DATA_SUFFIX = '/__data.json';
const DATA_SUFFIX = '/__data.js';

/** @param {{ html: string }} opts */
const default_transform = ({ html }) => html;
Expand Down Expand Up @@ -69,12 +66,7 @@ export async function respond(request, options, state) {
}

const is_data_request = decoded.endsWith(DATA_SUFFIX);

if (is_data_request) {
const data_suffix_length = DATA_SUFFIX.length - (options.trailing_slash === 'always' ? 1 : 0);
decoded = decoded.slice(0, -data_suffix_length) || '/';
url = new URL(url.origin + url.pathname.slice(0, -data_suffix_length) + url.search);
}
if (is_data_request) decoded = decoded.slice(0, -DATA_SUFFIX.length);

if (!state.prerendering?.fallback) {
const matchers = await options.manifest._.matchers();
Expand All @@ -92,26 +84,19 @@ export async function respond(request, options, state) {
}
}

if (route) {
if (route.page) {
const normalized = normalize_path(url.pathname, options.trailing_slash);

if (normalized !== url.pathname && !state.prerendering?.fallback) {
return new Response(undefined, {
status: 301,
headers: {
'x-sveltekit-normalize': '1',
location:
// ensure paths starting with '//' are not treated as protocol-relative
(normalized.startsWith('//') ? url.origin + normalized : normalized) +
(url.search === '?' ? '' : url.search)
}
});
}
} else if (is_data_request) {
// requesting /__data.json should fail for a standalone endpoint
if (route?.page) {
const normalized = normalize_path(url.pathname, options.trailing_slash);

if (normalized !== url.pathname && !state.prerendering?.fallback) {
return new Response(undefined, {
status: 404
status: 301,
headers: {
'x-sveltekit-normalize': '1',
location:
// ensure paths starting with '//' are not treated as protocol-relative
(normalized.startsWith('//') ? url.origin + normalized : normalized) +
(url.search === '?' ? '' : url.search)
}
});
}
}
Expand Down Expand Up @@ -250,115 +235,9 @@ export async function respond(request, options, state) {
if (route) {
/** @type {Response} */
let response;
if (is_data_request && route.page) {
try {
const node_ids = [...route.page.layouts, route.page.leaf];

const invalidated =
request.headers.get('x-sveltekit-invalidated')?.split(',').map(Boolean) ??
node_ids.map(() => true);

let aborted = false;

const functions = node_ids.map((n, i) => {
return once(async () => {
try {
if (aborted) {
return /** @type {import('types').ServerDataSkippedNode} */ ({
type: 'skip'
});
}

// == 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({
event,
state,
node,
parent: async () => {
/** @type {Record<string, any>} */
const data = {};
for (let j = 0; j < i; j += 1) {
const parent = /** @type {import('types').ServerDataNode | null} */ (
await functions[j]()
);

if (parent) {
Object.assign(data, parent.data);
}
}
return data;
}
});
} catch (e) {
aborted = true;
throw e;
}
});
});

const promises = functions.map(async (fn, i) => {
if (!invalidated[i]) {
return /** @type {import('types').ServerDataSkippedNode} */ ({
type: 'skip'
});
}

return fn();
});

let length = promises.length;
const nodes = await Promise.all(
promises.map((p, i) =>
p.catch((e) => {
const error = normalize_error(e);

if (error instanceof Redirect) {
throw error;
}

// Math.min because array isn't guaranteed to resolve in order
length = Math.min(length, i + 1);

if (error instanceof HttpError) {
return /** @type {import('types').ServerErrorNode} */ ({
type: 'error',
httperror: { ...error }
});
}

options.handle_error(error, event);

return /** @type {import('types').ServerErrorNode} */ ({
type: 'error',
error: error_to_pojo(error, options.get_stack)
});
})
)
);

/** @type {import('types').ServerData} */
const server_data = {
type: 'data',
nodes: nodes.slice(0, length)
};

response = json(server_data);
} catch (e) {
const error = normalize_error(e);

if (error instanceof Redirect) {
/** @type {import('types').ServerData} */
const server_data = {
type: 'redirect',
location: error.location
};

response = json(server_data);
} else {
response = json(error_to_pojo(error, options.get_stack), { status: 500 });
}
}
if (is_data_request) {
response = await render_data(event, route, options, state);
} else if (route.page) {
response = await render_page(event, route, route.page, options, state, resolve_opts);
} else if (route.endpoint) {
Expand All @@ -370,7 +249,7 @@ export async function respond(request, options, state) {
}

if (!is_data_request) {
// we only want to set cookies on __data.json requests, we don't
// we only want to set cookies on __data.js requests, we don't
// want to cache stuff erroneously etc
for (const key in headers) {
const value = headers[key];
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export async function render_page(event, route, page, options, state, resolve_op
}

const should_prerender_data = nodes.some((node) => node?.server);
const data_pathname = `${event.url.pathname.replace(/\/$/, '')}/__data.json`;
const data_pathname = `${event.url.pathname.replace(/\/$/, '')}/__data.js`;
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

// it's crucial that we do this before returning the non-SSR response, otherwise
// SvelteKit will erroneously believe that the path has been prerendered,
Expand Down
6 changes: 6 additions & 0 deletions packages/kit/src/runtime/server/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import devalue from 'devalue';
import { HttpError } from '../../index/private.js';

/** @param {any} body */
Expand Down Expand Up @@ -114,3 +115,8 @@ export function allowed_methods(mod) {

return allowed;
}

/** @param {any} data */
export function data_response(data) {
return new Response(`window.__data = ${devalue(data)}`);
}