Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
86f35e9
failing test for shadowed pages
Rich-Harris Feb 2, 2022
e00e254
find shadowed pages
Rich-Harris Feb 2, 2022
c8d4dd3
store endpoint filename
Rich-Harris Feb 2, 2022
01e56bb
update unit tests
Rich-Harris Feb 2, 2022
f8a323c
load shadow data
Rich-Harris Feb 2, 2022
1455d1b
Merge branch 'master' into shadow-endpoints
Rich-Harris Feb 2, 2022
b6f740a
add shadow request handler type
Rich-Harris Feb 2, 2022
37cb09b
Merge branch 'master' into shadow-endpoints
Rich-Harris Feb 2, 2022
87a3904
fix types
Rich-Harris Feb 2, 2022
674bfd1
basic shadow endpoint working with SSR
Rich-Harris Feb 2, 2022
87437aa
implement __data.json
Rich-Harris Feb 2, 2022
98e76a5
serialize shadow props
Rich-Harris Feb 2, 2022
7632326
load shadow props in hydration and client-side navigation
Rich-Harris Feb 2, 2022
94363e5
lint
Rich-Harris Feb 2, 2022
2eb28c2
lint
Rich-Harris Feb 2, 2022
47195a5
unfocus test
Rich-Harris Feb 2, 2022
b8b4220
handle redirects
Rich-Harris Feb 2, 2022
5b63a08
more tests
Rich-Harris Feb 2, 2022
754c75a
generate __data.json files when prerendering
Rich-Harris Feb 2, 2022
ccf071d
prevent prerendering of pages shadowed by non-GET methods
Rich-Harris Feb 2, 2022
6510f43
lint
Rich-Harris Feb 2, 2022
62b5164
changeset
Rich-Harris Feb 2, 2022
e912ad5
remove old shadowing test
Rich-Harris Feb 2, 2022
af21b10
start simplifying todos page in default template
Rich-Harris Feb 3, 2022
5c6a958
content negotiation
Rich-Harris Feb 3, 2022
1b59a8a
mark shadow endpoint as dependency of page
Rich-Harris Feb 3, 2022
fe4f704
lint
Rich-Harris Feb 3, 2022
cb5c3da
revert changes for CI
Rich-Harris Feb 3, 2022
0326a10
tweak enhance signature
Rich-Harris Feb 3, 2022
dfaa24b
update docs
Rich-Harris Feb 3, 2022
96a14d5
more docs
Rich-Harris Feb 3, 2022
108db32
merge master
Rich-Harris Feb 3, 2022
6cb75db
mention accept: application/json
Rich-Harris Feb 3, 2022
3f86c1e
undo template change that was for local testing
Rich-Harris Feb 3, 2022
dc4cc20
Update documentation/docs/03-loading.md
Rich-Harris Feb 3, 2022
05bd60a
Update packages/create-svelte/templates/default/svelte.config.js
Rich-Harris Feb 3, 2022
74584ea
safer DATA_SUFFIX
Rich-Harris Feb 3, 2022
17ed223
remove comment, we decided on a 405
Rich-Harris Feb 3, 2022
238d1b0
Merge branch 'master' into shadow-endpoints
Rich-Harris Feb 3, 2022
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
5 changes: 5 additions & 0 deletions .changeset/yellow-coins-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Implement shadow endpoints
149 changes: 114 additions & 35 deletions documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,23 @@ A file called either `src/routes/about.svelte` or `src/routes/about/index.svelte
<p>TODO...</p>
```

Dynamic parameters are encoded using `[brackets]`. For example, a blog post might be defined by `src/routes/blog/[slug].svelte`. Soon, we'll see how to access that parameter in a [load function](#loading) or the [page store](#modules-$app-stores).
Dynamic parameters are encoded using `[brackets]`. For example, a blog post might be defined by `src/routes/blog/[slug].svelte`.

A file or directory can have multiple dynamic parts, like `[id]-[category].svelte`. (Parameters are 'non-greedy'; in an ambiguous case like `x-y-z`, `id` would be `x` and `category` would be `y-z`.)

### Endpoints

Endpoints are modules written in `.js` (or `.ts`) files that export functions corresponding to HTTP methods.
Endpoints are modules written in `.js` (or `.ts`) files that export functions corresponding to HTTP methods. Their job is to allow pages to read and write data that is only available on the server (for example in a database, or on the filesystem).

```ts
// Declaration types for Endpoints
// * declarations that are not exported are for internal use
// Type declarations for endpoints (declarations marked with
// an `export` keyword can be imported from `@sveltejs/kit`)

export interface RequestHandler<Output = Record<string, any>> {
(event: RequestEvent): MaybePromise<
Either<Output extends Response ? Response : EndpointOutput<Output>, Fallthrough>
>;
}

export interface RequestEvent {
request: Request;
Expand All @@ -59,43 +65,34 @@ export interface RequestEvent {
platform: App.Platform;
}

type Body = JSONValue | Uint8Array | ReadableStream | stream.Readable;
export interface EndpointOutput<Output extends Body = Body> {
export interface EndpointOutput<Output = Record<string, any>> {
status?: number;
headers?: Headers | Partial<ResponseHeaders>;
body?: Output;
body?: Record<string, any>;
}

type MaybePromise<T> = T | Promise<T>;

interface Fallthrough {
fallthrough: true;
}

export interface RequestHandler<Output extends Body = Body> {
(event: RequestEvent): MaybePromise<
Either<Output extends Response ? Response : EndpointOutput<Output>, Fallthrough>
>;
}
```

> See the [TypeScript](#typescript) section for information on `App.Locals` and `App.Platform`.

For example, our hypothetical blog page, `/blog/cool-article`, might request data from `/blog/cool-article.json`, which could be represented by a `src/routes/blog/[slug].json.js` endpoint:
A page like `src/routes/items/[id].svelte` could get its data from `src/routes/items/[id].js`:

```js
import db from '$lib/database';

/** @type {import('@sveltejs/kit').RequestHandler} */
export async function get({ params }) {
// the `slug` parameter is available because this file
// is called [slug].json.js
const article = await db.get(params.slug);
// `params.id` comes from [id].js
const item = await db.get(params.id);

if (article) {
if (item) {
return {
body: {
article
}
body: { item }
};
}

Expand All @@ -105,7 +102,7 @@ export async function get({ params }) {
}
```

> All server-side code, including endpoints, has access to `fetch` in case you need to request data from external APIs.
> All server-side code, including endpoints, has access to `fetch` in case you need to request data from external APIs. Don't worry about the `$lib` import, we'll get to that [later](#modules-$lib).

The job of this function is to return a `{ status, headers, body }` object representing the response, where `status` is an [HTTP status code](https://httpstatusdogs.com):

Expand All @@ -114,30 +111,94 @@ The job of this function is to return a `{ status, headers, body }` object repre
- `4xx` — client error
- `5xx` — server error

If the returned `body` is an object, and no `content-type` header is returned, it will automatically be turned into a JSON response. (Don't worry about `$lib`, we'll get to that [later](#modules-$lib).)

> If `{fallthrough: true}` is returned SvelteKit will [fall through](#routing-advanced-fallthrough-routes) to other routes until something responds, or will respond with a generic 404.

For endpoints that handle other HTTP methods, like POST, export the corresponding function:
The returned `body` corresponds to the page's props:

```svelte
<script>
// populated with data from the endpoint
export let item;
</script>

<h1>{item.title}</h1>
```

#### POST, PUT, PATCH, DELETE

Endpoints can handle any HTTP method — not just `GET` — by exporting the corresponding function:

```js
export function post(event) {...}
export function put(event) {...}
export function patch(event) {...}
export function del(event) {...} // `delete` is a reserved word
```

Since `delete` is a reserved word in JavaScript, DELETE requests are handled with a `del` function.
These functions can, like `get`, return a `body` that will be passed to the page as props. Whereas 4xx/5xx responses from `get` will result in an error page rendering, similar responses to non-GET requests do not, allowing you to do things like render form validation errors:

> We don't interact with the `req`/`res` objects you might be familiar with from Node's `http` module or frameworks like Express, because they're only available on certain platforms. Instead, SvelteKit translates the returned object into whatever's required by the platform you're deploying your app to.
```js
// src/routes/items.js
import * as db from '$lib/database';

To set multiple cookies in a single set of response headers, you can return an array:
export async function get() {
const items = await db.list();

```js
return {
headers: {
'set-cookie': [cookie1, cookie2]
return {
body: { items }
};
}

export async function post({ request }) {
const [errors, item] = await db.create(request);

if (errors) {
// return validation errors
return {
status: 400,
body: { errors }
};
}
};

// redirect to the newly created item
return {
status: 303,
headers: {
location: `/items/${item.id}`
}
};
}
```

```svelte
<!-- src/routes/items.svelte -->
<script>
// The page always has access to props from `get`...
export let items;

// ...plus props from `post` when the page is rendered
// in response to a POST request, for example after
// submitting the form below
export let errors;
</script>

{#each items as item}
<Preview item={item}/>
{/each}

<form method="post">
<input name="title">

{#if errors?.title}
<p class="error">{errors.title}</p>
{/if}

<button type="submit">Create item</button>
</form>
```

If you request the route with an `accept: application/json` header, SvelteKit will render the endpoint data as JSON, rather than the page as HTML.

#### Body parsing

The `request` object is an instance of the standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) class. As such, accessing the request body is easy:
Expand All @@ -148,6 +209,18 @@ export async function post({ request }) {
}
```

#### Setting cookies

Endpoints can set cookies by returning a `headers` object with `set-cookie`. To set multiple cookies simultaneously, return an array:

```js
return {
headers: {
'set-cookie': [cookie1, cookie2]
}
};
```

#### HTTP method overrides

HTML `<form>` elements only support `GET` and `POST` methods natively. You can allow other methods, like `PUT` and `DELETE`, by specifying them in your [configuration](#configuration-methodoverride) and adding a `_method=VERB` parameter (you can configure the name) to the form's `action`:
Expand All @@ -171,15 +244,21 @@ export default {

> Using native `<form>` behaviour ensures your app continues to work when JavaScript fails or is disabled.

### Standalone endpoints

Most commonly, endpoints exist to provide data to the page with which they're paired. They can, however, exist separately from pages. Standalone endpoints have slightly more flexibility over the returned `body` type — in addition to objects, they can return a string or a `Uint8Array`.

> Support for streaming request and response bodies is [coming soon](https://github.com/sveltejs/kit/issues/3419).

### Private modules

Files and directories with a leading `_` or `.` (other than [`.well-known`](https://en.wikipedia.org/wiki/Well-known_URI)) are private by default, meaning that they do not create routes (but can be imported by files that do). You can configure which modules are considered public or private with the [`routes`](#configuration-routes) configuration.

### Advanced
### Advanced routing

#### Rest parameters

A route can have multiple dynamic parameters, for example `src/routes/[category]/[item].svelte` or even `src/routes/[category]-[item].svelte`. If the number of route segments is unknown, you can use rest syntax — for example you might implement GitHub's file viewer like so...
A route can have multiple dynamic parameters, for example `src/routes/[category]/[item].svelte` or even `src/routes/[category]-[item].svelte`. (Parameters are 'non-greedy'; in an ambiguous case like `/x-y-z`, `category` would be `x` and `item` would be `y-z`.) If the number of route segments is unknown, you can use rest syntax — for example you might implement GitHub's file viewer like so...

```bash
/[org]/[repo]/tree/[branch]/[...file]
Expand Down
46 changes: 24 additions & 22 deletions documentation/docs/03-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@
title: Loading
---

A component that defines a page or a layout can export a `load` function that runs before the component is created. This function runs both during server-side rendering and in the client, and allows you to get data for a page without (for example) showing a loading spinner and fetching data in `onMount`.
A component that defines a page or a layout can export a `load` function that runs before the component is created. This function runs both during server-side rendering and in the client, and allows you to fetch and manipulate data before the page is rendered, thus preventing loading spinners.

If the data for a page comes from its endpoint, you may not need a `load` function. It's useful when you need more flexibility, for example loading data from an external API.

```ts
// Declaration types for Loading
// * declarations that are not exported are for internal use
// Type declarations for `load` (declarations marked with
// an `export` keyword can be imported from `@sveltejs/kit`)

export interface Load<Params = Record<string, string>, Props = Record<string, any>> {
(input: LoadInput<Params>): MaybePromise<Either<Fallthrough, LoadOutput<Props>>>;
}

export interface LoadInput<Params extends Record<string, string> = Record<string, string>> {
url: URL;
params: Params;
props: Record<string, any>;
fetch(info: RequestInfo, init?: RequestInit): Promise<Response>;
session: App.Session;
stuff: Partial<App.Stuff>;
Expand All @@ -26,45 +33,36 @@ export interface LoadOutput<Props extends Record<string, any> = Record<string, a
}

type MaybePromise<T> = T | Promise<T>;

interface Fallthrough {
fallthrough: true;
}

export interface Load<Params = Record<string, string>, Props = Record<string, any>> {
(input: LoadInput<Params>): MaybePromise<Either<Fallthrough, LoadOutput<Props>>>;
}
```

> See the [TypeScript](#typescript) section for information on `App.Session` and `App.Stuff`.

Our example blog page might contain a `load` function like the following:
A page that loads data from an external API might look like this:

```html
<!-- src/routes/blog/[slug].svelte -->
<script context="module">
/** @type {import('@sveltejs/kit').Load} */
export async function load({ params, fetch, session, stuff }) {
const url = `/blog/${params.slug}.json`;
const res = await fetch(url);

if (res.ok) {
return {
props: {
article: await res.json()
}
};
}
const response = await fetch(`https://cms.example.com/article/${params.slug}.json`);

return {
status: res.status,
error: new Error(`Could not load ${url}`)
status: response.status,
props: {
article: response.ok && (await response.json())
}
};
}
</script>
```

> Note the `<script context="module">` — this is necessary because `load` runs before the component is rendered. Code that is per-component instance should go into a second `<script>` tag.

`load` is similar to `getStaticProps` or `getServerSideProps` in Next.js, except that it runs on both the server and the client.
`load` is similar to `getStaticProps` or `getServerSideProps` in Next.js, except that it runs on both the server and the client. In the example above, if a user clicks on a link to this page the data will be fetched from `cms.example.com` without going via our server.

If `load` returns `{fallthrough: true}`, SvelteKit will [fall through](#routing-advanced-fallthrough-routes) to other routes until something responds, or will respond with a generic 404.

Expand All @@ -88,7 +86,7 @@ It is recommended that you not store pre-request state in global variables, but

### Input

The `load` function receives an object containing five fields — `url`, `params`, `fetch`, `session` and `stuff`. The `load` function is reactive, and will re-run when its parameters change, but only if they are used in the function. Specifically, if `url`, `session` or `stuff` are used in the function, they will be re-run whenever their value changes, and likewise for the individual properties of `params`.
The `load` function receives an object containing five fields — `url`, `params`, `props`, `fetch`, `session` and `stuff`. The `load` function is reactive, and will re-run when its parameters change, but only if they are used in the function. Specifically, if `url`, `session` or `stuff` are used in the function, they will be re-run whenever their value changes, and likewise for the individual properties of `params`.

> Note that destructuring parameters in the function declaration is enough to count as using them.

Expand All @@ -111,6 +109,10 @@ For a route filename example like `src/routes/a/[b]/[...c]` and a `url.pathname`
}
```

#### props

If the page you're loading has an endpoint, the data returned from it is accessible inside the leaf component's `load` function as `props`. For layout components and pages without endpoints, `props` will be an empty object.

#### fetch

`fetch` is equivalent to the native `fetch` web API, and can make credentialed requests. It can be used across both client and server contexts.
Expand Down
12 changes: 2 additions & 10 deletions documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ This function runs every time SvelteKit receives a request — whether that happ
If unimplemented, defaults to `({ event, resolve }) => resolve(event)`.

```ts
// Declaration types for Hooks
// * declarations that are not exported are for internal use

// type of string[] is only for set-cookie
// everything else must be a type of string
type ResponseHeaders = Record<string, string | string[]>;
// Type declarations for `handle` (declarations marked with
// an `export` keyword can be imported from `@sveltejs/kit`)

export interface RequestEvent {
request: Request;
Expand Down Expand Up @@ -86,7 +82,6 @@ During development, if an error occurs because of a syntax error in your Svelte
If unimplemented, SvelteKit will log the error with default formatting.

```ts
// Declaration types for handleError hook
export interface HandleError {
(input: { error: Error & { frame?: string }; event: RequestEvent }): void;
}
Expand All @@ -109,7 +104,6 @@ This function takes the `event` object and returns a `session` object that is [a
If unimplemented, session is `{}`.

```ts
// Declaration types for getSession hook
export interface GetSession {
(event: RequestEvent): MaybePromise<App.Session>;
}
Expand Down Expand Up @@ -142,8 +136,6 @@ This function allows you to modify (or replace) a `fetch` request for an externa
For example, your `load` function might make a request to a public URL like `https://api.yourapp.com` when the user performs a client-side navigation to the respective page, but during SSR it might make sense to hit the API directly (bypassing whatever proxies and load balancers sit between it and the public internet).

```ts
// Declaration types for externalFetch hook

export interface ExternalFetch {
(req: Request): Promise<Response>;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/create-svelte/templates/default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build --verbose",
"preview": "svelte-kit preview",
"start": "svelte-kit start"
"package": "svelte-kit package",
"preview": "svelte-kit preview"
},
"devDependencies": {
"@sveltejs/adapter-auto": "workspace:*",
Expand Down
Loading