Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit fallthrough #3217

Merged
merged 17 commits into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/purple-cycles-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Fallthrough is now explicit and layout components now also support fallthrough
13 changes: 8 additions & 5 deletions documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,18 @@ export interface EndpointOutput<Body extends DefaultBody = DefaultBody> {
body?: Body;
}

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

export interface Fallthrough {
fallthrough?: true;
}

export interface RequestHandler<
Locals = Record<string, any>,
Input = unknown,
Output extends DefaultBody = DefaultBody
> {
(request: Request<Locals, Input>):
| void
| EndpointOutput<Output>
| Promise<void | EndpointOutput<Output>>;
(request: Request<Locals, Input>): MaybePromise<Fallthrough | EndpointOutput<Output>>;
}
```

Expand Down Expand Up @@ -125,7 +128,7 @@ The job of this function is to return a `{ status, headers, body }` object repre

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).)

> Returning nothing is equivalent to an explicit 404 response.
> 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:

Expand Down
10 changes: 7 additions & 3 deletions documentation/docs/03-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ A component that defines a page or a layout can export a `load` function that ru
// Declaration types for Loading
// * declarations that are not exported are for internal use

export interface Fallthrough {
fallthrough?: true;
}

export interface LoadInput<
PageParams extends Record<string, string> = Record<string, string>,
Stuff extends Record<string, any> = Record<string, any>,
Expand All @@ -20,7 +24,7 @@ export interface LoadInput<
stuff: Stuff;
}

export interface LoadOutput<
export type LoadOutput<
Props extends Record<string, any> = Record<string, any>,
Stuff extends Record<string, any> = Record<string, any>
> {
Expand All @@ -30,7 +34,7 @@ export interface LoadOutput<
props?: Props;
stuff?: Stuff;
maxage?: number;
}
} | Fallthrough
```

Our example blog page might contain a `load` function like the following:
Expand Down Expand Up @@ -62,7 +66,7 @@ Our example blog page might contain a `load` function like the following:

`load` is similar to `getStaticProps` or `getServerSideProps` in Next.js, except that it runs on both the server and the client.

If `load` returns nothing, SvelteKit will [fall through](#routing-advanced-fallthrough-routes) to other routes until something responds, or will respond with a generic 404.
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.

SvelteKit's `load` receives an implementation of `fetch`, which has the following special properties:

Expand Down
14 changes: 6 additions & 8 deletions packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,8 +582,9 @@ export class Renderer {

const loaded = await module.load.call(null, load_input);

// if the page component returns nothing from load, fall through
if (!loaded) return;
if (!loaded) {
throw new Error('load function must return a value');
}

node.loaded = normalize(loaded);
if (node.loaded.stuff) node.stuff = node.loaded.stuff;
Expand Down Expand Up @@ -660,9 +661,10 @@ export class Renderer {
stuff
});

const is_leaf = i === a.length - 1;

if (node && node.loaded) {
if (node.loaded.fallthrough) {
return;
}
if (node.loaded.error) {
status = node.loaded.status;
error = node.loaded.error;
Expand All @@ -679,10 +681,6 @@ export class Renderer {
if (node.loaded.stuff) {
stuff_changed = true;
}
} else if (is_leaf && module.load) {
// if the leaf node has a `load` function
// that returns nothing, fall through
return;
}
} else {
node = previous;
Expand Down
7 changes: 4 additions & 3 deletions packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@ export async function render_endpoint(request, route, match) {
const response = await handler(request);
const preface = `Invalid response from route ${request.url.pathname}`;

if (!response) {
return;
}
if (typeof response !== 'object') {
return error(`${preface}: expected an object, got ${typeof response}`);
}

if (response.fallthrough) {
return;
}

let { status = 200, body, headers = {} } = response;

headers = lowercase_keys(headers);
Expand Down
14 changes: 6 additions & 8 deletions packages/kit/src/runtime/server/page/load_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { is_root_relative, resolve } from '../../../utils/url.js';
* $session: any;
* stuff: Record<string, any>;
* prerender_enabled: boolean;
* is_leaf: boolean;
* is_error: boolean;
* status?: number;
* error?: Error;
Expand All @@ -34,7 +33,6 @@ export async function load_node({
$session,
stuff,
prerender_enabled,
is_leaf,
is_error,
status,
error
Expand Down Expand Up @@ -294,16 +292,16 @@ export async function load_node({
}

loaded = await module.load.call(null, load_input);

if (!loaded) {
throw new Error(`load function must return a value${options.dev ? ` (${node.entry})` : ''}`);
}
} else {
loaded = {};
}

// if leaf node (i.e. page component) has a load function
// that returns nothing, we fall through to the next one
if (!loaded && is_leaf && !is_error) return;

if (!loaded) {
throw new Error(`${node.entry} - load must return a value except for page fall through`);
if (loaded.fallthrough && !is_error) {
return;
}

return {
Expand Down
2 changes: 0 additions & 2 deletions packages/kit/src/runtime/server/page/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ export async function respond(opts) {
node,
stuff,
prerender_enabled: is_prerender_enabled(options, node, state),
is_leaf: i === nodes.length - 1,
is_error: false
});

Expand Down Expand Up @@ -147,7 +146,6 @@ export async function respond(opts) {
node: error_node,
stuff: node_loaded.stuff,
prerender_enabled: is_prerender_enabled(options, error_node, state),
is_leaf: false,
is_error: true,
status,
error
Expand Down
2 changes: 0 additions & 2 deletions packages/kit/src/runtime/server/page/respond_with_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export async function respond_with_error({ request, options, state, $session, st
$session,
stuff: {},
prerender_enabled: is_prerender_enabled(options, default_error, state),
is_leaf: false,
is_error: false
})
);
Expand All @@ -59,7 +58,6 @@ export async function respond_with_error({ request, options, state, $session, st
$session,
stuff: loaded ? loaded.stuff : {},
prerender_enabled: is_prerender_enabled(options, default_error, state),
is_leaf: false,
is_error: true,
status,
error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export function get() {
}

/** @type {import('@sveltejs/kit').RequestHandler} */
export function del() {}
export function del() {
return { fallthrough: true };
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export function get({ params }) {
body: { type: 'animal' }
};
}
return { fallthrough: true };
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
};
}
}
return { fallthrough: true };
}
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export function get({ params }) {
body: { type: 'mineral' }
};
}
return { fallthrough: true };
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
};
}
}
return { fallthrough: true };
}
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export function get({ params }) {
body: { type: 'vegetable' }
};
}
return { fallthrough: true };
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
};
}
}
return { fallthrough: true };
}
</script>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script context="module">
/** @type {import("@sveltejs/kit").Load} */

export async function load({ params }) {
if (params.foo !== 'okay') {
return { fallthrough: true };
}
return {};
}
</script>

<slot />
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import { page } from '$app/stores';
</script>

<h1>foo is {$page.params.foo}</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script context="module">
/** @type {import("@sveltejs/kit").Load} */

export async function load({ params }) {
if (params.xyz !== 'ok') {
return { fallthrough: true };
}
return {};
}
</script>

<slot />
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import { page } from '$app/stores';
</script>

<h1>xyz is {$page.params.xyz}</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<a href="/routing/fallthrough-layout/okay">okay</a>
<a href="/routing/fallthrough-layout/ok">ok</a>
<a href="/routing/fallthrough-layout/notok">notok</a>

<slot />
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
return {
props: {}
};
} else {
return;
}
return { fallthrough: true };
};
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ let random = 0;
/** @type {import('@sveltejs/kit').RequestHandler<any, FormData>} */
export function post({ body }) {
random = +body.get('random');
return { fallthrough: true };
}

export function get() {
Expand Down
11 changes: 11 additions & 0 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,17 @@ test.describe.parallel('Routing', () => {
expect(await page.textContent('h1')).toBe('404');
});

test('dynamic fallthrough of layout', async ({ page, clicknav }) => {
await page.goto('/routing/fallthrough-layout/okay');
expect(await page.textContent('h1')).toBe('foo is okay');

await clicknav('[href="/routing/fallthrough-layout/ok"]');
expect(await page.textContent('h1')).toBe('xyz is ok');

await clicknav('[href="/routing/fallthrough-layout/notok"]');
expect(await page.textContent('h1')).toBe('404');
});

test('last parameter in a segment wins in cases of ambiguity', async ({ page, clicknav }) => {
await page.goto('/routing/split-params');
await clicknav('[href="/routing/split-params/x-y-z"]');
Expand Down
6 changes: 4 additions & 2 deletions packages/kit/types/endpoint.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ServerRequest } from './hooks';
import { JSONString, MaybePromise, ResponseHeaders } from './helper';
import { JSONString, MaybePromise, ResponseHeaders, Either, Fallthrough } from './helper';

type DefaultBody = JSONString | Uint8Array;

Expand All @@ -14,5 +14,7 @@ export interface RequestHandler<
Input = unknown,
Output extends DefaultBody = DefaultBody
> {
(request: ServerRequest<Locals, Input>): MaybePromise<void | EndpointOutput<Output>>;
(request: ServerRequest<Locals, Input>): MaybePromise<
Either<EndpointOutput<Output>, Fallthrough>
>;
}
12 changes: 12 additions & 0 deletions packages/kit/types/helper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,15 @@ export type RecursiveRequired<T> = {
? Extract<T[K], Function> // only take the Function type.
: T[K]; // Use the exact type for everything else
};

type Only<T, U> = {
[P in keyof T]: T[P];
} & {
[P in keyof U]?: never;
};

export type Either<T, U> = Only<T, U> | Only<U, T>;

export interface Fallthrough {
fallthrough: true;
}
20 changes: 12 additions & 8 deletions packages/kit/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ServerResponse
} from './hooks';
import { Load } from './page';
import { Either, Fallthrough } from './helper';

type PageId = string;

Expand Down Expand Up @@ -219,13 +220,16 @@ export interface BuildData {
entries: string[];
}

export interface NormalizedLoadOutput {
status: number;
error?: Error;
redirect?: string;
props?: Record<string, any> | Promise<Record<string, any>>;
stuff?: Record<string, any>;
maxage?: number;
}
export type NormalizedLoadOutput = Either<
{
status: number;
error?: Error;
redirect?: string;
props?: Record<string, any> | Promise<Record<string, any>>;
stuff?: Record<string, any>;
maxage?: number;
},
Fallthrough
>;

export type TrailingSlash = 'never' | 'always' | 'ignore';
Loading