309 lines
12 KiB
JavaScript
309 lines
12 KiB
JavaScript
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
|
|
|
|
import { HttpError, Redirect, ActionFailure, ValidationError } from './internal/index.js';
|
|
import { BROWSER, DEV } from 'esm-env';
|
|
import {
|
|
add_data_suffix,
|
|
add_resolution_suffix,
|
|
has_data_suffix,
|
|
has_resolution_suffix,
|
|
strip_data_suffix,
|
|
strip_resolution_suffix
|
|
} from '../runtime/pathname.js';
|
|
import { text_encoder } from '../runtime/utils.js';
|
|
|
|
export { VERSION } from '../version.js';
|
|
|
|
// TODO 3.0: remove these types as they are not used anymore (we can't remove them yet because that would be a breaking change)
|
|
/**
|
|
* @template {number} TNumber
|
|
* @template {any[]} [TArray=[]]
|
|
* @typedef {TNumber extends TArray['length'] ? TArray[number] : LessThan<TNumber, [...TArray, TArray['length']]>} LessThan
|
|
*/
|
|
|
|
/**
|
|
* @template {number} TStart
|
|
* @template {number} TEnd
|
|
* @typedef {Exclude<TEnd | LessThan<TEnd>, LessThan<TStart>>} NumericRange
|
|
*/
|
|
|
|
// Keep the status codes as `number` because restricting to certain numbers makes it unnecessarily hard to use compared to the benefits
|
|
// (we have runtime errors already to check for invalid codes). Also see https://github.com/sveltejs/kit/issues/11780
|
|
|
|
// we have to repeat the JSDoc because the display for function overloads is broken
|
|
// see https://github.com/microsoft/TypeScript/issues/55056
|
|
|
|
/**
|
|
* Throws an error with a HTTP status code and an optional message.
|
|
* When called during request handling, this will cause SvelteKit to
|
|
* return an error response without invoking `handleError`.
|
|
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
|
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
* @param {App.Error} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
|
* @overload
|
|
* @param {number} status
|
|
* @param {App.Error} body
|
|
* @return {never}
|
|
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
|
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
|
*/
|
|
/**
|
|
* Throws an error with a HTTP status code and an optional message.
|
|
* When called during request handling, this will cause SvelteKit to
|
|
* return an error response without invoking `handleError`.
|
|
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
|
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body] An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
|
* @overload
|
|
* @param {number} status
|
|
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body]
|
|
* @return {never}
|
|
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
|
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
|
*/
|
|
/**
|
|
* Throws an error with a HTTP status code and an optional message.
|
|
* When called during request handling, this will cause SvelteKit to
|
|
* return an error response without invoking `handleError`.
|
|
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
|
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
|
* @return {never}
|
|
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
|
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
|
*/
|
|
export function error(status, body) {
|
|
if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) {
|
|
throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
|
|
}
|
|
|
|
throw new HttpError(status, body);
|
|
}
|
|
|
|
/**
|
|
* Checks whether this is an error thrown by {@link error}.
|
|
* @template {number} T
|
|
* @param {unknown} e
|
|
* @param {T} [status] The status to filter for.
|
|
* @return {e is (HttpError & { status: T extends undefined ? never : T })}
|
|
*/
|
|
export function isHttpError(e, status) {
|
|
if (!(e instanceof HttpError)) return false;
|
|
return !status || e.status === status;
|
|
}
|
|
|
|
/**
|
|
* Redirect a request. When called during request handling, SvelteKit will return a redirect response.
|
|
* Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it.
|
|
*
|
|
* Most common status codes:
|
|
* * `303 See Other`: redirect as a GET request (often used after a form POST request)
|
|
* * `307 Temporary Redirect`: redirect will keep the request method
|
|
* * `308 Permanent Redirect`: redirect will keep the request method, SEO will be transferred to the new page
|
|
*
|
|
* [See all redirect status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages)
|
|
*
|
|
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number)} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308.
|
|
* @param {string | URL} location The location to redirect to.
|
|
* @throws {Redirect} This error instructs SvelteKit to redirect to the specified location.
|
|
* @throws {Error} If the provided status is invalid.
|
|
* @return {never}
|
|
*/
|
|
export function redirect(status, location) {
|
|
if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) {
|
|
throw new Error('Invalid status code');
|
|
}
|
|
|
|
throw new Redirect(
|
|
// @ts-ignore
|
|
status,
|
|
location.toString()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks whether this is a redirect thrown by {@link redirect}.
|
|
* @param {unknown} e The object to check.
|
|
* @return {e is Redirect}
|
|
*/
|
|
export function isRedirect(e) {
|
|
return e instanceof Redirect;
|
|
}
|
|
|
|
/**
|
|
* Create a JSON `Response` object from the supplied data.
|
|
* @param {any} data The value that will be serialized as JSON.
|
|
* @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. `Content-Type: application/json` and `Content-Length` headers will be added automatically.
|
|
*/
|
|
export function json(data, init) {
|
|
// TODO deprecate this in favour of `Response.json` when it's
|
|
// more widely supported
|
|
const body = JSON.stringify(data);
|
|
|
|
// we can't just do `text(JSON.stringify(data), init)` because
|
|
// it will set a default `content-type` header. duplicated code
|
|
// means less duplicated work
|
|
const headers = new Headers(init?.headers);
|
|
if (!headers.has('content-length')) {
|
|
headers.set('content-length', text_encoder.encode(body).byteLength.toString());
|
|
}
|
|
|
|
if (!headers.has('content-type')) {
|
|
headers.set('content-type', 'application/json');
|
|
}
|
|
|
|
return new Response(body, {
|
|
...init,
|
|
headers
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a `Response` object from the supplied body.
|
|
* @param {string} body The value that will be used as-is.
|
|
* @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. A `Content-Length` header will be added automatically.
|
|
*/
|
|
export function text(body, init) {
|
|
const headers = new Headers(init?.headers);
|
|
if (!headers.has('content-length')) {
|
|
const encoded = text_encoder.encode(body);
|
|
headers.set('content-length', encoded.byteLength.toString());
|
|
return new Response(encoded, {
|
|
...init,
|
|
headers
|
|
});
|
|
}
|
|
|
|
return new Response(body, {
|
|
...init,
|
|
headers
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create an `ActionFailure` object. Call when form submission fails.
|
|
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
* @overload
|
|
* @param {number} status
|
|
* @returns {import('./public.js').ActionFailure<undefined>}
|
|
*/
|
|
/**
|
|
* Create an `ActionFailure` object. Call when form submission fails.
|
|
* @template [T=undefined]
|
|
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
* @param {T} data Data associated with the failure (e.g. validation errors)
|
|
* @overload
|
|
* @param {number} status
|
|
* @param {T} data
|
|
* @returns {import('./public.js').ActionFailure<T>}
|
|
*/
|
|
/**
|
|
* Create an `ActionFailure` object. Call when form submission fails.
|
|
* @param {number} status
|
|
* @param {any} [data]
|
|
* @returns {import('./public.js').ActionFailure<any>}
|
|
*/
|
|
export function fail(status, data) {
|
|
// @ts-expect-error unique symbol missing
|
|
return new ActionFailure(status, data);
|
|
}
|
|
|
|
/**
|
|
* Checks whether this is an action failure thrown by {@link fail}.
|
|
* @param {unknown} e The object to check.
|
|
* @return {e is import('./public.js').ActionFailure}
|
|
*/
|
|
export function isActionFailure(e) {
|
|
return e instanceof ActionFailure;
|
|
}
|
|
|
|
/**
|
|
* Use this to throw a validation error to imperatively fail form validation.
|
|
* Can be used in combination with `issue` passed to form actions to create field-specific issues.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* import { invalid } from '@sveltejs/kit';
|
|
* import { form } from '$app/server';
|
|
* import { tryLogin } from '$lib/server/auth';
|
|
* import * as v from 'valibot';
|
|
*
|
|
* export const login = form(
|
|
* v.object({ name: v.string(), _password: v.string() }),
|
|
* async ({ name, _password }) => {
|
|
* const success = tryLogin(name, _password);
|
|
* if (!success) {
|
|
* invalid('Incorrect username or password');
|
|
* }
|
|
*
|
|
* // ...
|
|
* }
|
|
* );
|
|
* ```
|
|
* @param {...(StandardSchemaV1.Issue | string)} issues
|
|
* @returns {never}
|
|
* @since 2.47.3
|
|
*/
|
|
export function invalid(...issues) {
|
|
throw new ValidationError(
|
|
issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks whether this is an validation error thrown by {@link invalid}.
|
|
* @param {unknown} e The object to check.
|
|
* @return {e is import('./public.js').ActionFailure}
|
|
* @since 2.47.3
|
|
*/
|
|
export function isValidationError(e) {
|
|
return e instanceof ValidationError;
|
|
}
|
|
|
|
/**
|
|
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
|
|
* Returns the normalized URL as well as a method for adding the potential suffix back
|
|
* based on a new pathname (possibly including search) or URL.
|
|
* ```js
|
|
* import { normalizeUrl } from '@sveltejs/kit';
|
|
*
|
|
* const { url, denormalize } = normalizeUrl('/blog/post/__data.json');
|
|
* console.log(url.pathname); // /blog/post
|
|
* console.log(denormalize('/blog/post/a')); // /blog/post/a/__data.json
|
|
* ```
|
|
* @param {URL | string} url
|
|
* @returns {{ url: URL, wasNormalized: boolean, denormalize: (url?: string | URL) => URL }}
|
|
* @since 2.18.0
|
|
*/
|
|
export function normalizeUrl(url) {
|
|
url = new URL(url, 'http://internal');
|
|
|
|
const is_route_resolution = has_resolution_suffix(url.pathname);
|
|
const is_data_request = has_data_suffix(url.pathname);
|
|
const has_trailing_slash = url.pathname !== '/' && url.pathname.endsWith('/');
|
|
|
|
if (is_route_resolution) {
|
|
url.pathname = strip_resolution_suffix(url.pathname);
|
|
} else if (is_data_request) {
|
|
url.pathname = strip_data_suffix(url.pathname);
|
|
} else if (has_trailing_slash) {
|
|
url.pathname = url.pathname.slice(0, -1);
|
|
}
|
|
|
|
return {
|
|
url,
|
|
wasNormalized: is_data_request || is_route_resolution || has_trailing_slash,
|
|
denormalize: (new_url = url) => {
|
|
new_url = new URL(new_url, url);
|
|
if (is_route_resolution) {
|
|
new_url.pathname = add_resolution_suffix(new_url.pathname);
|
|
} else if (is_data_request) {
|
|
new_url.pathname = add_data_suffix(new_url.pathname);
|
|
} else if (has_trailing_slash && !new_url.pathname.endsWith('/')) {
|
|
new_url.pathname += '/';
|
|
}
|
|
return new_url;
|
|
}
|
|
};
|
|
}
|