diff --git a/.changeset/purple-cycles-build.md b/.changeset/purple-cycles-build.md new file mode 100644 index 000000000000..26bd9e6ceb43 --- /dev/null +++ b/.changeset/purple-cycles-build.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Fallthrough is now explicit and layout components now also support fallthrough diff --git a/documentation/docs/01-routing.md b/documentation/docs/01-routing.md index d65e2f030d3c..c74c677f4c0a 100644 --- a/documentation/docs/01-routing.md +++ b/documentation/docs/01-routing.md @@ -79,15 +79,18 @@ export interface EndpointOutput { body?: Body; } +export type MaybePromise = T | Promise; + +export interface Fallthrough { + fallthrough?: true; +} + export interface RequestHandler< Locals = Record, Input = unknown, Output extends DefaultBody = DefaultBody > { - (request: Request): - | void - | EndpointOutput - | Promise>; + (request: Request): MaybePromise>; } ``` @@ -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: diff --git a/documentation/docs/03-loading.md b/documentation/docs/03-loading.md index e460734d1a0c..214e9e555a18 100644 --- a/documentation/docs/03-loading.md +++ b/documentation/docs/03-loading.md @@ -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 = Record, Stuff extends Record = Record, @@ -20,7 +24,7 @@ export interface LoadInput< stuff: Stuff; } -export interface LoadOutput< +export type LoadOutput< Props extends Record = Record, Stuff extends Record = Record > { @@ -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: @@ -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: diff --git a/packages/kit/src/runtime/client/renderer.js b/packages/kit/src/runtime/client/renderer.js index ae655b3e4710..11e83370cc98 100644 --- a/packages/kit/src/runtime/client/renderer.js +++ b/packages/kit/src/runtime/client/renderer.js @@ -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; @@ -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; @@ -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; diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 30bb5cb8a224..dede4fb26ff2 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -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); diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index 2bc63c3553ed..c03db86434fa 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -16,7 +16,6 @@ import { is_root_relative, resolve } from '../../../utils/url.js'; * $session: any; * stuff: Record; * prerender_enabled: boolean; - * is_leaf: boolean; * is_error: boolean; * status?: number; * error?: Error; @@ -34,7 +33,6 @@ export async function load_node({ $session, stuff, prerender_enabled, - is_leaf, is_error, status, error @@ -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 { diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index 9b99ab2ed7f5..8ad951992a42 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -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 }); @@ -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 diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index 9f3e8d5823de..c9b6ccca6f2b 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -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 }) ); @@ -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 diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/empty.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/empty.js index 78ba9f6fb252..e396a880ac27 100644 --- a/packages/kit/test/apps/basics/src/routes/endpoint-output/empty.js +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/empty.js @@ -4,4 +4,6 @@ export function get() { } /** @type {import('@sveltejs/kit').RequestHandler} */ -export function del() {} +export function del() { + return { fallthrough: true }; +} diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[animal].json.js b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[animal].json.js index d71f6d74ff66..6a38766dbf37 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[animal].json.js +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[animal].json.js @@ -7,4 +7,5 @@ export function get({ params }) { body: { type: 'animal' } }; } + return { fallthrough: true }; } diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[animal].svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[animal].svelte index 2160a3cd0768..8cbc2adf6990 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[animal].svelte +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[animal].svelte @@ -14,6 +14,7 @@ }; } } + return { fallthrough: true }; } diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[mineral].json.js b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[mineral].json.js index 25efddf3b9d9..b214adea3672 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[mineral].json.js +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[mineral].json.js @@ -7,4 +7,5 @@ export function get({ params }) { body: { type: 'mineral' } }; } + return { fallthrough: true }; } diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[mineral].svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[mineral].svelte index 15b6f51d0986..29e2affd9ee1 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[mineral].svelte +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[mineral].svelte @@ -14,6 +14,7 @@ }; } } + return { fallthrough: true }; } diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[vegetable].json.js b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[vegetable].json.js index efad9b0f7866..03e967133c15 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[vegetable].json.js +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[vegetable].json.js @@ -7,4 +7,5 @@ export function get({ params }) { body: { type: 'vegetable' } }; } + return { fallthrough: true }; } diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[vegetable].svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[vegetable].svelte index 9033eb36c36d..8182e2187a38 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[vegetable].svelte +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-advanced/[vegetable].svelte @@ -14,6 +14,7 @@ }; } } + return { fallthrough: true }; } diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[foo]/__layout.svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[foo]/__layout.svelte new file mode 100644 index 000000000000..7d5da461c75a --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[foo]/__layout.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[foo]/index.svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[foo]/index.svelte new file mode 100644 index 000000000000..9272a8854586 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[foo]/index.svelte @@ -0,0 +1,5 @@ + + +

foo is {$page.params.foo}

diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[xyz]/__layout.svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[xyz]/__layout.svelte new file mode 100644 index 000000000000..977ae9b680ba --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[xyz]/__layout.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[xyz]/index.svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[xyz]/index.svelte new file mode 100644 index 000000000000..dbdf18b80545 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/[xyz]/index.svelte @@ -0,0 +1,5 @@ + + +

xyz is {$page.params.xyz}

diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/__layout.svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/__layout.svelte new file mode 100644 index 000000000000..6efc36c2dcfe --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-layout/__layout.svelte @@ -0,0 +1,5 @@ +okay +ok +notok + + diff --git a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-simple/[legal].svelte b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-simple/[legal].svelte index 2bb5fd56451f..8cfadc579533 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/fallthrough-simple/[legal].svelte +++ b/packages/kit/test/apps/basics/src/routes/routing/fallthrough-simple/[legal].svelte @@ -5,9 +5,8 @@ return { props: {} }; - } else { - return; } + return { fallthrough: true }; }; diff --git a/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js b/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js index aa7f6b287a6b..ef2063590a2b 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js +++ b/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js @@ -3,6 +3,7 @@ let random = 0; /** @type {import('@sveltejs/kit').RequestHandler} */ export function post({ body }) { random = +body.get('random'); + return { fallthrough: true }; } export function get() { diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 2abf0275783e..9536497ec4bb 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -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"]'); diff --git a/packages/kit/types/endpoint.d.ts b/packages/kit/types/endpoint.d.ts index ad71afbeed18..05e6f50530d4 100644 --- a/packages/kit/types/endpoint.d.ts +++ b/packages/kit/types/endpoint.d.ts @@ -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; @@ -14,5 +14,7 @@ export interface RequestHandler< Input = unknown, Output extends DefaultBody = DefaultBody > { - (request: ServerRequest): MaybePromise>; + (request: ServerRequest): MaybePromise< + Either, Fallthrough> + >; } diff --git a/packages/kit/types/helper.d.ts b/packages/kit/types/helper.d.ts index 200d55aaa146..f3dbf0dedd08 100644 --- a/packages/kit/types/helper.d.ts +++ b/packages/kit/types/helper.d.ts @@ -39,3 +39,15 @@ export type RecursiveRequired = { ? Extract // only take the Function type. : T[K]; // Use the exact type for everything else }; + +type Only = { + [P in keyof T]: T[P]; +} & { + [P in keyof U]?: never; +}; + +export type Either = Only | Only; + +export interface Fallthrough { + fallthrough: true; +} diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index a8e5047d1931..a9a6999e29b0 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -10,6 +10,7 @@ import { ServerResponse } from './hooks'; import { Load } from './page'; +import { Either, Fallthrough } from './helper'; type PageId = string; @@ -219,13 +220,16 @@ export interface BuildData { entries: string[]; } -export interface NormalizedLoadOutput { - status: number; - error?: Error; - redirect?: string; - props?: Record | Promise>; - stuff?: Record; - maxage?: number; -} +export type NormalizedLoadOutput = Either< + { + status: number; + error?: Error; + redirect?: string; + props?: Record | Promise>; + stuff?: Record; + maxage?: number; + }, + Fallthrough +>; export type TrailingSlash = 'never' | 'always' | 'ignore'; diff --git a/packages/kit/types/page.d.ts b/packages/kit/types/page.d.ts index e3a227360701..16f2058fe145 100644 --- a/packages/kit/types/page.d.ts +++ b/packages/kit/types/page.d.ts @@ -1,4 +1,4 @@ -import { InferValue, MaybePromise, Rec } from './helper'; +import { InferValue, MaybePromise, Rec, Either, Fallthrough } from './helper'; export interface LoadInput< PageParams extends Rec = Rec, @@ -51,10 +51,12 @@ export interface Load< InferValue, InferValue > - ): MaybePromise, - InferValue - >>; + ): MaybePromise< + Either< + LoadOutput, InferValue>, + Fallthrough + > + >; } export interface ErrorLoad<