UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

169 lines (146 loc) • 6.13 kB
/** @import { RemoteResource, RemotePrerenderFunction } from '$app/server' */ /** @import { RemoteFunctionResponse, RemotePrerenderInputsGenerator, RemotePrerenderInternals, MaybePromise } from 'types' */ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ import { HandledHttpError } from '@sveltejs/kit/internal'; import { get_request_store } from '@sveltejs/kit/internal/server'; import { stringify_remote_arg } from '../../../shared.js'; import { parse, stringify } from '#app/internal/transport'; import { noop } from '../../../../utils/functions.js'; import { app_dir, base } from '#app/paths'; import { create_validator, get_response, run_remote_function } from './shared.js'; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation. * * @template Output * @overload * @param {() => MaybePromise<Output>} fn * @param {{ inputs?: RemotePrerenderInputsGenerator<void>, dynamic?: boolean }} [options] * @returns {RemotePrerenderFunction<void, Output>} * @since 2.27 */ /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation. * * @template Input * @template Output * @overload * @param {'unchecked'} validate * @param {(arg: Input) => MaybePromise<Output>} fn * @param {{ inputs?: RemotePrerenderInputsGenerator<Input>, dynamic?: boolean }} [options] * @returns {RemotePrerenderFunction<Input, Output>} * @since 2.27 */ /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation. * * @template {StandardSchemaV1} Schema * @template Output * @overload * @param {Schema} schema * @param {(arg: StandardSchemaV1.InferOutput<Schema>) => MaybePromise<Output>} fn * @param {{ inputs?: RemotePrerenderInputsGenerator<StandardSchemaV1.InferInput<Schema>>, dynamic?: boolean }} [options] * @returns {RemotePrerenderFunction<StandardSchemaV1.InferInput<Schema>, Output>} * @since 2.27 */ /** * @template Input * @template Output * @param {any} validate_or_fn * @param {any} [fn_or_options] * @param {{ inputs?: RemotePrerenderInputsGenerator<Input>, dynamic?: boolean }} [maybe_options] * @returns {RemotePrerenderFunction<Input, Output>} * @since 2.27 */ /*@__NO_SIDE_EFFECTS__*/ export function prerender(validate_or_fn, fn_or_options, maybe_options) { const maybe_fn = typeof fn_or_options === 'function' ? fn_or_options : undefined; /** @type {typeof maybe_options} */ const options = maybe_options ?? (maybe_fn ? undefined : fn_or_options); /** @type {(arg?: Input) => MaybePromise<Output>} */ const fn = maybe_fn ?? validate_or_fn; /** @type {(arg?: any) => MaybePromise<Input>} */ const validate = create_validator(validate_or_fn, maybe_fn); /** @type {RemotePrerenderInternals} */ const __ = { type: 'prerender', id: '', name: '', has_arg: !!maybe_fn, inputs: options?.inputs, dynamic: options?.dynamic }; /** @type {RemotePrerenderFunction<Input, Output> & { __: RemotePrerenderInternals }} */ const wrapper = (arg) => { const { event, state } = get_request_store(); const payload = stringify_remote_arg(arg); // `get_response` (as opposed to bare `get_cache`) also registers the call in the // implicit lookup, so that the result is inlined into the page payload (`data.p`) // and the client doesn't need to fetch it again upon hydration /** @type {Promise<Output> & Partial<RemoteResource<Output>>} */ const promise = get_response(__, payload, state, async () => { const id = __.id; const url = `${base}/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`; if (!state.prerendering && !__SVELTEKIT_DEV__ && !event.isRemoteRequest) { /** @type {RemoteFunctionResponse | undefined} */ let prerendered; try { // TODO adapters can provide prerendered data more efficiently than // fetching from the public internet // `request.url` rather than `event.url`, which throws inside queries const response = await fetch(new URL(url, event.request.url).href, { // in the unlikely event that an attacker is able to spoof the origin, // this protects us against SSRF (the attacker-controlled server cannot // trick us into making a request to an internal resource) redirect: 'manual' }); if (response.ok) { prerendered = /** @type {RemoteFunctionResponse} */ (await response.json()); } } catch { // not available prerendered, fallback to normal function } if (prerendered) { if (prerendered.type === 'error') { throw new HandledHttpError(prerendered.error); } return parse(prerendered.data)._; } } // during a prerender run, the same function might be invoked while rendering // multiple pages — share the result across the entire run if (state.prerendering?.remote_responses.has(url)) { return /** @type {Promise<any>} */ (state.prerendering.remote_responses.get(url)); } const promise = run_remote_function( event, { ...state, is_in_remote_prerender: true }, false, () => validate(arg), fn ); if (state.prerendering) { state.prerendering.remote_responses.set(url, promise); } const result = await promise; if (state.prerendering) { const body = { type: 'result', data: stringify({ _: result }) }; state.prerendering.dependencies.set(url, { body: JSON.stringify(body), response: Response.json(body) }); } // TODO this is missing error/loading/current/status return result; }); promise.catch(noop); return /** @type {RemoteResource<Output>} */ (promise); }; Object.defineProperty(wrapper, '__', { value: __ }); return wrapper; }