Skip to content

Commit

Permalink
replace request.origin/path/query with request.url (#3126)
Browse files Browse the repository at this point in the history
* replace request.origin/path/query with request.url

* fix some stuff

* typo

* lint

* fix test

* fix xss vuln

* fix test

* gah

* fixes

* simplify

* use pathname+search as key

* all tests passing

* lint

* tidy up

* update types in docs

* update docs

* more docs

* more docs

* more docs

* i would be lost without conduitry

* update template app

* throw useful error when trying to access path/query/origin/page

* $params -> $route.params, only use getters in dev, remove the word "deprecate"

* update docs

* finish updating load input section

* update

* make this section less verbose

* move url into $route

* update some docs

* rename route store back to page

* throw errors on accessing $page.path etc

* fix types

* fix some docs

* fix migrating docs

* decode path, strip prefix

* tidy up

* remove comment

* lint

* Update documentation/docs/05-modules.md

Co-authored-by: Ben McCann <[email protected]>

* Update documentation/migrating/04-pages.md

Co-authored-by: Ben McCann <[email protected]>

Co-authored-by: Ben McCann <[email protected]>
  • Loading branch information
Rich-Harris and benmccann authored Dec 30, 2021
1 parent d138efe commit d17f459
Show file tree
Hide file tree
Showing 74 changed files with 464 additions and 473 deletions.
21 changes: 8 additions & 13 deletions documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,19 @@ type ResponseHeaders = Record<string, string | string[]>;
type RequestHeaders = Record<string, string>;

export type RawBody = null | Uint8Array;
export interface IncomingRequest {
method: string;
path: string;
query: URLSearchParams;
headers: RequestHeaders;
rawBody: RawBody;
}

type ParameterizedBody<Body = unknown> = Body extends FormData
? ReadOnlyFormData
: (string | RawBody | ReadOnlyFormData) & Body;
// ServerRequest is exported as Request
export interface ServerRequest<Locals = Record<string, any>, Body = unknown>
extends IncomingRequest {
origin: string;

export interface Request<Locals = Record<string, any>, Body = unknown> {
url: URL;
method: string;
headers: RequestHeaders;
rawBody: RawBody;
params: Record<string, string>;
body: ParameterizedBody<Body>;
locals: Locals; // populated by hooks handle
locals: Locals;
}

type DefaultBody = JSONResponse | Uint8Array;
Expand All @@ -89,7 +84,7 @@ export interface RequestHandler<
Input = unknown,
Output extends DefaultBody = DefaultBody
> {
(request: ServerRequest<Locals, Input>):
(request: Request<Locals, Input>):
| void
| EndpointOutput<Output>
| Promise<void | EndpointOutput<Output>>;
Expand Down
40 changes: 22 additions & 18 deletions documentation/docs/03-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@ export interface LoadInput<
Stuff extends Record<string, any> = Record<string, any>,
Session = any
> {
page: {
origin: string;
path: string;
params: PageParams;
query: URLSearchParams;
};
url: URL;
params: PageParams;
fetch(info: RequestInfo, init?: RequestInit): Promise<Response>;
session: Session;
stuff: Stuff;
Expand All @@ -42,8 +38,8 @@ Our example blog page might contain a `load` function like the following:
```html
<script context="module">
/** @type {import('@sveltejs/kit').Load} */
export async function load({ page, fetch, session, stuff }) {
const url = `/blog/${page.params.slug}.json`;
export async function load({ params, fetch, session, stuff }) {
const url = `/blog/${params.slug}.json`;
const res = await fetch(url);
if (res.ok) {
Expand Down Expand Up @@ -88,20 +84,28 @@ It is recommended that you not store pre-request state in global variables, but
### Input

The `load` function receives an object containing four fields — `page`, `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 `page.query`, `page.path`, `session`, or `stuff` are used in the function, they will be re-run whenever their value changes. Note that destructuring parameters in the function declaration is enough to count as using them. In the example above, the `load({ page, fetch, session, stuff })` function will re-run every time `session` or `stuff` is changed, even though they are not used in the body of the function. If it was re-written as `load({ page, fetch })`, then it would only re-run when `page.params.slug` changes. The same reactivity applies to `page.params`, but only to the params actually used in the function. If `page.params.foo` changes, the example above would not re-run, because it did not access `page.params.foo`, only `page.params.slug`.
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`.

#### page
> Note that destructuring parameters in the function declaration is enough to count as using them.
`page` is an `{ origin, path, params, query }` object where `origin` is the URL's origin, `path` is its pathname, `params` is derived from `path` and the route filename, and `query` is an instance of [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). Mutating `page` does not update the current URL; you should instead navigate using [`goto`](#modules-$app-navigation).
#### url

So if the example above was `src/routes/blog/[slug].svelte` and the URL was `https://example.com/blog/some-post?foo=bar&baz&bizz=a&bizz=b`, the following would be true:
`url` is an instance of [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), containing properties like the `origin`, `hostname`, `pathname` and `searchParams`.

- `page.origin === 'https://example.com'`
- `page.path === '/blog/some-post'`
- `page.params.slug === 'some-post'`
- `page.query.get('foo') === 'bar'`
- `page.query.has('baz')`
- `page.query.getAll('bizz') === ['a', 'b']`
> In some environments this is derived from request headers, which you [may need to configure](#configuration-headers), during server-side rendering
#### params

`params` is derived from `url.pathname` and the route filename.

For a route filename example like `src/routes/a/[b]/[...c]` and a `url.pathname` of `/a/x/y/z`, the `params` object would look like this:

```js
{
"b": "x",
"c": "y/z"
}
```

#### fetch

Expand Down
35 changes: 14 additions & 21 deletions documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,34 @@ type ResponseHeaders = Record<string, string | string[]>;
type RequestHeaders = Record<string, string>;

export type RawBody = null | Uint8Array;
export interface IncomingRequest {
method: string;
path: string;
query: URLSearchParams;
headers: RequestHeaders;
rawBody: RawBody;
}

type ParameterizedBody<Body = unknown> = Body extends FormData
? ReadOnlyFormData
: (string | RawBody | ReadOnlyFormData) & Body;
// ServerRequest is exported as Request
export interface ServerRequest<Locals = Record<string, any>, Body = unknown>
extends IncomingRequest {
origin: string;

export interface Request<Locals = Record<string, any>, Body = unknown> {
url: URL;
method: string;
headers: RequestHeaders;
rawBody: RawBody;
params: Record<string, string>;
body: ParameterizedBody<Body>;
locals: Locals; // populated by hooks handle
locals: Locals;
}

type StrictBody = string | Uint8Array;
// ServerResponse is exported as Response
export interface ServerResponse {

export interface Response {
status: number;
headers: ResponseHeaders;
body?: StrictBody;
}

export interface Handle<Locals = Record<string, any>, Body = unknown> {
(input: {
request: ServerRequest<Locals, Body>;
resolve(request: ServerRequest<Locals, Body>): ServerResponse | Promise<ServerResponse>;
}): ServerResponse | Promise<ServerResponse>;
request: Request<Locals, Body>;
resolve(request: Request<Locals, Body>): Response | Promise<Response>;
}): Response | Promise<Response>;
}
```

Expand Down Expand Up @@ -91,9 +86,8 @@ If unimplemented, SvelteKit will log the error with default formatting.

```ts
// Declaration types for handleError hook

export interface HandleError<Locals = Record<string, any>, Body = unknown> {
(input: { error: Error & { frame?: string }; request: ServerRequest<Locals, Body> }): void;
(input: { error: Error & { frame?: string }; request: Request<Locals, Body> }): void;
}
```

Expand All @@ -115,9 +109,8 @@ If unimplemented, session is `{}`.

```ts
// Declaration types for getSession hook

export interface GetSession<Locals = Record<string, any>, Body = unknown, Session = any> {
(request: ServerRequest<Locals, Body>): Session | Promise<Session>;
(request: Request<Locals, Body>): Session | Promise<Session>;
}
```

Expand Down
6 changes: 3 additions & 3 deletions documentation/docs/05-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ Because of that, the stores are not free-floating objects: they must be accessed

- `getStores` is a convenience function around `getContext` that returns `{ navigating, page, session }`. This needs to be called at the top-level or synchronously during component or page initialisation.

The stores themselves attach to the correct context at the point of subscription, which means you can import and use them directly in components without boilerplate. However, it still needs to be called synchronously on component or page initialisation when `$`-prefix isn't used. Use `getStores` to safely `.subscribe` asynchronously instead.
The stores themselves attach to the correct context at the point of subscription, which means you can import and use them directly in components without boilerplate. However, it still needs to be called synchronously on component or page initialisation when the `$`-prefix isn't used. Use `getStores` to safely `.subscribe` asynchronously instead.

- `navigating` is a [readable store](https://svelte.dev/tutorial/readable-stores). When navigating starts, its value is `{ from, to }`, where `from` and `to` both mirror the `page` store value. When navigating finishes, its value reverts to `null`.
- `page` is a readable store whose value reflects the object passed to `load` functions — it contains `origin`, `path`, `params` and `query`. See the [`page` section](#loading-input-page) above for more details.
- `navigating` is a [readable store](https://svelte.dev/tutorial/readable-stores). When navigating starts, its value is `{ from, to }`, where `from` and `to` are both [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instances. When navigating finishes, its value reverts to `null`.
- `page` contains an object with the current [`url`](https://developer.mozilla.org/en-US/docs/Web/API/URL) and [`params`](#loading-input-params).
- `session` is a [writable store](https://svelte.dev/tutorial/writable-stores) whose initial value is whatever was returned from [`getSession`](#hooks-getsession). It can be written to, but this will _not_ cause changes to persist on the server — this is something you must implement yourself.

### $lib
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/14-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Permissions-Policy: interest-cohort=()
### headers

The [`page.origin`] property is derived from the request protocol (normally `https`) and the host, which is taken from the `Host` header by default.
The current page or endpoint's `url` is, in some environments, derived from the request protocol (normally `https`) and the host, which is taken from the `Host` header by default.

If your app is behind a reverse proxy (think load balancers and CDNs) then the `Host` header will be incorrect. In most cases, the underlying protocol and host are exposed via the [`X-Forwarded-Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) and [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) headers, which can be specified in your config:

Expand Down
2 changes: 1 addition & 1 deletion documentation/migrating/04-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { stores } from '@sapper/app';
const { preloading, page, session } = stores();
```

The `page` and `session` stores still exist; `preloading` has been replaced with a `navigating` store that contains `from` and `to` properties.
The `page` and `session` stores still exist; `preloading` has been replaced with a `navigating` store that contains `from` and `to` properties. `page` now has `url` and `params` properties, but no `path` or `query`.

You access them differently in SvelteKit. `stores` is now `getStores`, but in most cases it is unnecessary since you can import `navigating`, `page` and `session` directly from [`$app/stores`](/docs#modules-$app-stores).

Expand Down
3 changes: 2 additions & 1 deletion examples/hn.svelte.dev/src/routes/[list]/[page].svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script context="module">
const valid_lists = new Set(['news', 'newest', 'show', 'ask', 'jobs']);
export async function load({ page: { params }, fetch }) {
/** @type {import('@sveltejs/kit').Load} */
export async function load({ params, fetch }) {
const list = params.list === 'top' ? 'news' : params.list === 'new' ? 'newest' : params.list;
if (!valid_lists.has(list)) {
Expand Down
11 changes: 5 additions & 6 deletions examples/hn.svelte.dev/src/routes/__layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@
import ThemeToggler from '$lib/ThemeToggler.svelte';
import '../app.css';
$: section = $page.path.split('/')[1];
$: section = $page.url.pathname.split('/')[1];
</script>

<Nav {section}/>
<Nav {section} />

{#if $navigating}
<PreloadingIndicator/>
<PreloadingIndicator />
{/if}

<main>
<slot></slot>
<slot />
</main>

<ThemeToggler/>
<ThemeToggler />

<style>
main {
Expand Down
10 changes: 3 additions & 7 deletions examples/hn.svelte.dev/src/routes/index.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
<script context="module">
export function load({ page }) {
let host = page.host;
const i = host.indexOf(':');
if (i >= 0) {
host = host.substring(0, i);
}
/** @type {import('@sveltejs/kit').Load} */
export function load({ url }) {
return {
redirect: '/top/1',
status: host === 'localhost' || host === '127.0.0.1' ? 302 : 301
status: url.hostname === 'localhost' || url.hostname === '127.0.0.1' ? 302 : 301
};
}
</script>
18 changes: 11 additions & 7 deletions examples/hn.svelte.dev/src/routes/item/[id].svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script context="module">
export async function load({ page, fetch }) {
const res = await fetch(`https://api.hnpwa.com/v0/item/${page.params.id}.json`);
/** @type {import('@sveltejs/kit').Load} */
export async function load({ params, fetch }) {
const res = await fetch(`https://api.hnpwa.com/v0/item/${params.id}.json`);
const item = await res.json();
return { props: { item } };
Expand All @@ -24,7 +25,10 @@
{#if item.domain}<small>{item.domain}</small>{/if}
</a>

<p class="meta">{item.points} points by <a href="/user/{item.user}">{item.user}</a> {item.time_ago}</p>
<p class="meta">
{item.points} points by <a href="/user/{item.user}">{item.user}</a>
{item.time_ago}
</p>

{#if item.content}
{@html item.content}
Expand All @@ -33,7 +37,7 @@

<div class="comments">
{#each item.comments as comment}
<Comment comment='{comment}'/>
<Comment {comment} />
{/each}
</div>
</div>
Expand All @@ -44,13 +48,13 @@
}
.item {
border-bottom: 1em solid rgba(0,0,0,0.1);
border-bottom: 1em solid rgba(0, 0, 0, 0.1);
margin: 0 -2em 2em -2em;
padding: 0 2em 2em 2em;
}
:global(html).dark .item {
border-bottom: 1em solid rgba(255,255,255,0.1);;
border-bottom: 1em solid rgba(255, 255, 255, 0.1);
}
.main-link {
Expand All @@ -72,4 +76,4 @@
.comments > :global(.comment):first-child {
border-top: none;
}
</style>
</style>
11 changes: 6 additions & 5 deletions examples/hn.svelte.dev/src/routes/user/[name].svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script context="module">
export const hydrate = false;
export async function load({ page, fetch }) {
const res = await fetch(`https://api.hnpwa.com/v0/user/${page.params.name}.json`);
/** @type {import('@sveltejs/kit').Load} */
export async function load({ params, fetch }) {
const res = await fetch(`https://api.hnpwa.com/v0/user/${params.name}.json`);
const user = await res.json();
return {
props: {
name: page.params.name,
name: params.name,
user
}
};
Expand Down Expand Up @@ -39,7 +40,7 @@

{#if user.about}
<div class="about">
{@html '<p>' + user.about }
{@html '<p>' + user.about}
</div>
{/if}
</div>
</div>
2 changes: 1 addition & 1 deletion packages/create-svelte/templates/default/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const handle: Handle = async ({ request, resolve }) => {
request.locals.userid = cookies.userid || uuid();

// TODO https://github.com/sveltejs/kit/issues/1046
const method = request.query.get('_method');
const method = request.url.searchParams.get('_method');
if (method) {
request.method = method.toUpperCase();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg>
<ul>
<li class:active={$page.path === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.path === '/about'}><a sveltekit:prefetch href="/about">About</a></li>
<li class:active={$page.path === '/todos'}><a sveltekit:prefetch href="/todos">Todos</a></li>
<li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.url.pathname === '/about'}>
<a sveltekit:prefetch href="/about">About</a>
</li>
<li class:active={$page.url.pathname === '/todos'}>
<a sveltekit:prefetch href="/todos">Todos</a>
</li>
</ul>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
Expand Down
Loading

0 comments on commit d17f459

Please sign in to comment.