@sveltejs/kit
Version: 
SvelteKit is the fastest way to build Svelte apps
329 lines (291 loc) • 9.59 kB
JavaScript
import { text } from '../../../exports/index.js';
import { compact } from '../../../utils/array.js';
import { get_status, normalize_error } from '../../../utils/error.js';
import { add_data_suffix } from '../../pathname.js';
import { Redirect } from '../../control.js';
import { redirect_response, static_error_page, handle_error_and_jsonify } from '../utils.js';
import {
	handle_action_json_request,
	handle_action_request,
	is_action_json_request,
	is_action_request
} from './actions.js';
import { load_data, load_server_data } from './load_data.js';
import { render_response } from './render.js';
import { respond_with_error } from './respond_with_error.js';
import { get_data_json } from '../data/index.js';
import { DEV } from 'esm-env';
/**
 * The maximum request depth permitted before assuming we're stuck in an infinite loop
 */
const MAX_DEPTH = 10;
/**
 * @param {import('@sveltejs/kit').RequestEvent} event
 * @param {import('types').PageNodeIndexes} page
 * @param {import('types').SSROptions} options
 * @param {import('@sveltejs/kit').SSRManifest} manifest
 * @param {import('types').SSRState} state
 * @param {import('../../../utils/page_nodes.js').PageNodes} nodes
 * @param {import('types').RequiredResolveOptions} resolve_opts
 * @returns {Promise<Response>}
 */
export async function render_page(event, page, options, manifest, state, nodes, resolve_opts) {
	if (state.depth > MAX_DEPTH) {
		// infinite request cycle detected
		return text(`Not found: ${event.url.pathname}`, {
			status: 404 // TODO in some cases this should be 500. not sure how to differentiate
		});
	}
	if (is_action_json_request(event)) {
		const node = await manifest._.nodes[page.leaf]();
		return handle_action_json_request(event, options, node?.server);
	}
	try {
		const leaf_node = /** @type {import('types').SSRNode} */ (nodes.page());
		let status = 200;
		/** @type {import('@sveltejs/kit').ActionResult | undefined} */
		let action_result = undefined;
		if (is_action_request(event)) {
			// for action requests, first call handler in +page.server.js
			// (this also determines status code)
			action_result = await handle_action_request(event, leaf_node.server);
			if (action_result?.type === 'redirect') {
				return redirect_response(action_result.status, action_result.location);
			}
			if (action_result?.type === 'error') {
				status = get_status(action_result.error);
			}
			if (action_result?.type === 'failure') {
				status = action_result.status;
			}
		}
		// it's crucial that we do this before returning the non-SSR response, otherwise
		// SvelteKit will erroneously believe that the path has been prerendered,
		// causing functions to be omitted from the manifest generated later
		const should_prerender = nodes.prerender();
		if (should_prerender) {
			const mod = leaf_node.server;
			if (mod?.actions) {
				throw new Error('Cannot prerender pages with actions');
			}
		} else if (state.prerendering) {
			// if the page isn't marked as prerenderable, then bail out at this point
			return new Response(undefined, {
				status: 204
			});
		}
		// if we fetch any endpoints while loading data for this page, they should
		// inherit the prerender option of the page
		state.prerender_default = should_prerender;
		const should_prerender_data = nodes.should_prerender_data();
		const data_pathname = add_data_suffix(event.url.pathname);
		/** @type {import('./types.js').Fetched[]} */
		const fetched = [];
		// renders an empty 'shell' page if SSR is turned off and if there is
		// no server data to prerender. As a result, the load functions and rendering
		// only occur client-side.
		if (nodes.ssr() === false && !(state.prerendering && should_prerender_data)) {
			// if the user makes a request through a non-enhanced form, the returned value is lost
			// because there is no SSR or client-side handling of the response
			if (DEV && action_result && !event.request.headers.has('x-sveltekit-action')) {
				if (action_result.type === 'error') {
					console.warn(
						"The form action returned an error, but +error.svelte wasn't rendered because SSR is off. To get the error page with CSR, enhance your form with `use:enhance`. See https://svelte.dev/docs/kit/form-actions#progressive-enhancement-use-enhance"
					);
				} else if (action_result.data) {
					/// case: lost data
					console.warn(
						"The form action returned a value, but it isn't available in `page.form`, because SSR is off. To handle the returned value in CSR, enhance your form with `use:enhance`. See https://svelte.dev/docs/kit/form-actions#progressive-enhancement-use-enhance"
					);
				}
			}
			return await render_response({
				branch: [],
				fetched,
				page_config: {
					ssr: false,
					csr: nodes.csr()
				},
				status,
				error: null,
				event,
				options,
				manifest,
				state,
				resolve_opts
			});
		}
		/** @type {Array<import('./types.js').Loaded | null>} */
		const branch = [];
		/** @type {Error | null} */
		let load_error = null;
		/** @type {Array<Promise<import('types').ServerDataNode | null>>} */
		const server_promises = nodes.data.map((node, i) => {
			if (load_error) {
				// if an error happens immediately, don't bother with the rest of the nodes
				throw load_error;
			}
			return Promise.resolve().then(async () => {
				try {
					if (node === leaf_node && action_result?.type === 'error') {
						// we wait until here to throw the error so that we can use
						// any nested +error.svelte components that were defined
						throw action_result.error;
					}
					return await load_server_data({
						event,
						state,
						node,
						parent: async () => {
							/** @type {Record<string, any>} */
							const data = {};
							for (let j = 0; j < i; j += 1) {
								const parent = await server_promises[j];
								if (parent) Object.assign(data, parent.data);
							}
							return data;
						}
					});
				} catch (e) {
					load_error = /** @type {Error} */ (e);
					throw load_error;
				}
			});
		});
		const csr = nodes.csr();
		/** @type {Array<Promise<Record<string, any> | null>>} */
		const load_promises = nodes.data.map((node, i) => {
			if (load_error) throw load_error;
			return Promise.resolve().then(async () => {
				try {
					return await load_data({
						event,
						fetched,
						node,
						parent: async () => {
							const data = {};
							for (let j = 0; j < i; j += 1) {
								Object.assign(data, await load_promises[j]);
							}
							return data;
						},
						resolve_opts,
						server_data_promise: server_promises[i],
						state,
						csr
					});
				} catch (e) {
					load_error = /** @type {Error} */ (e);
					throw load_error;
				}
			});
		});
		// if we don't do this, rejections will be unhandled
		for (const p of server_promises) p.catch(() => {});
		for (const p of load_promises) p.catch(() => {});
		for (let i = 0; i < nodes.data.length; i += 1) {
			const node = nodes.data[i];
			if (node) {
				try {
					const server_data = await server_promises[i];
					const data = await load_promises[i];
					branch.push({ node, server_data, data });
				} catch (e) {
					const err = normalize_error(e);
					if (err instanceof Redirect) {
						if (state.prerendering && should_prerender_data) {
							const body = JSON.stringify({
								type: 'redirect',
								location: err.location
							});
							state.prerendering.dependencies.set(data_pathname, {
								response: text(body),
								body
							});
						}
						return redirect_response(err.status, err.location);
					}
					const status = get_status(err);
					const error = await handle_error_and_jsonify(event, options, err);
					while (i--) {
						if (page.errors[i]) {
							const index = /** @type {number} */ (page.errors[i]);
							const node = await manifest._.nodes[index]();
							let j = i;
							while (!branch[j]) j -= 1;
							return await render_response({
								event,
								options,
								manifest,
								state,
								resolve_opts,
								page_config: { ssr: true, csr: true },
								status,
								error,
								branch: compact(branch.slice(0, j + 1)).concat({
									node,
									data: null,
									server_data: null
								}),
								fetched
							});
						}
					}
					// if we're still here, it means the error happened in the root layout,
					// which means we have to fall back to error.html
					return static_error_page(options, status, error.message);
				}
			} else {
				// push an empty slot so we can rewind past gaps to the
				// layout that corresponds with an +error.svelte page
				branch.push(null);
			}
		}
		if (state.prerendering && should_prerender_data) {
			// ndjson format
			let { data, chunks } = get_data_json(
				event,
				options,
				branch.map((node) => node?.server_data)
			);
			if (chunks) {
				for await (const chunk of chunks) {
					data += chunk;
				}
			}
			state.prerendering.dependencies.set(data_pathname, {
				response: text(data),
				body: data
			});
		}
		const ssr = nodes.ssr();
		return await render_response({
			event,
			options,
			manifest,
			state,
			resolve_opts,
			page_config: {
				csr: nodes.csr(),
				ssr
			},
			status,
			error: null,
			branch: ssr === false ? [] : compact(branch),
			action_result,
			fetched
		});
	} catch (e) {
		// if we end up here, it means the data loaded successfully
		// but the page failed to render, or that a prerendering error occurred
		return await respond_with_error({
			event,
			options,
			manifest,
			state,
			status: 500,
			error: e,
			resolve_opts
		});
	}
}