Skip to content

Commit 1f0d822

Browse files
authored
Remove fallthrough (#4330)
* remove fallthrough * changeset * remove fallthrough documentation * tweak docs * simplify * simplify * simplify a tiny bit * lint * oops
1 parent fac2844 commit 1f0d822

File tree

33 files changed

+74
-447
lines changed

33 files changed

+74
-447
lines changed

.changeset/nasty-lemons-provide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[breaking] remove fallthrough routes

documentation/docs/01-routing.md

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -323,11 +323,10 @@ A route can have multiple dynamic parameters, for example `src/routes/[category]
323323
It's possible for multiple routes to match a given path. For example each of these routes would match `/foo-abc`:
324324

325325
```bash
326+
src/routes/[...catchall].svelte
326327
src/routes/[a].js
327328
src/routes/[b].svelte
328-
src/routes/[c].svelte
329-
src/routes/[...catchall].svelte
330-
src/routes/foo-[bar].svelte
329+
src/routes/foo-[c].svelte
331330
```
332331

333332
SvelteKit needs to know which route is being requested. To do so, it sorts them according to the following rules...
@@ -340,48 +339,8 @@ SvelteKit needs to know which route is being requested. To do so, it sorts them
340339
...resulting in this ordering, meaning that `/foo-abc` will invoke `src/routes/foo-[bar].svelte` rather than a less specific route:
341340

342341
```bash
343-
src/routes/foo-[bar].svelte
342+
src/routes/foo-[c].svelte
344343
src/routes/[a].js
345344
src/routes/[b].svelte
346-
src/routes/[c].svelte
347345
src/routes/[...catchall].svelte
348346
```
349-
350-
#### Fallthrough routes
351-
352-
In rare cases, the ordering above might not be what you want for a given path. For example, perhaps `/foo-abc` should resolve to `src/routes/foo-[bar].svelte`, but `/foo-def` should resolve to `src/routes/[b].svelte`.
353-
354-
Higher priority routes can _fall through_ to lower priority routes by returning `{ fallthrough: true }`, either from `load` (for pages) or a request handler (for endpoints):
355-
356-
```svelte
357-
/// file: src/routes/foo-[bar].svelte
358-
<script context="module">
359-
export function load({ params }) {
360-
if (params.bar === 'def') {
361-
return { fallthrough: true };
362-
}
363-
364-
// ...
365-
}
366-
</script>
367-
```
368-
369-
```js
370-
/// file: src/routes/[a].js
371-
372-
// @filename: [a].d.ts
373-
import type { RequestHandler as GenericRequestHandler } from '@sveltejs/kit';
374-
export type RequestHandler<Body = any> = GenericRequestHandler<{ a: string }, Body>;
375-
376-
// @filename: index.js
377-
// @errors: 2366
378-
// ---cut---
379-
/** @type {import('./[a]').RequestHandler} */
380-
export function get({ params }) {
381-
if (params.a === 'foo-def') {
382-
return { fallthrough: true };
383-
}
384-
385-
// ...
386-
}
387-
```

packages/kit/src/runtime/client/client.js

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export function create_client({ target, session, base, trailing_slash }) {
177177

178178
const intent = get_navigation_intent(url);
179179

180-
load_cache.promise = get_navigation_result(intent, false);
180+
load_cache.promise = load_route(intent, false);
181181
load_cache.id = intent.id;
182182

183183
return load_cache.promise;
@@ -191,7 +191,7 @@ export function create_client({ target, session, base, trailing_slash }) {
191191
*/
192192
async function update(intent, redirect_chain, no_cache, opts) {
193193
const current_token = (token = {});
194-
let navigation_result = await get_navigation_result(intent, no_cache);
194+
let navigation_result = await load_route(intent, no_cache);
195195

196196
if (!navigation_result && intent.url.pathname === location.pathname) {
197197
// this could happen in SPA fallback mode if the user navigated to
@@ -341,36 +341,6 @@ export function create_client({ target, session, base, trailing_slash }) {
341341
}
342342
}
343343

344-
/**
345-
* @param {import('./types').NavigationIntent} intent
346-
* @param {boolean} no_cache
347-
*/
348-
async function get_navigation_result(intent, no_cache) {
349-
if (load_cache.id === intent.id && load_cache.promise) {
350-
return load_cache.promise;
351-
}
352-
353-
for (let i = 0; i < intent.routes.length; i += 1) {
354-
const route = intent.routes[i];
355-
356-
// load code for subsequent routes immediately, if they are as
357-
// likely to match the current path/query as the current one
358-
let j = i + 1;
359-
while (j < intent.routes.length) {
360-
const next = intent.routes[j];
361-
if (next[0].toString() === route[0].toString()) {
362-
next[1].forEach((loader) => loader());
363-
j += 1;
364-
} else {
365-
break;
366-
}
367-
}
368-
369-
const result = await load_route(route, intent, no_cache);
370-
if (result) return result;
371-
}
372-
}
373-
374344
/**
375345
*
376346
* @param {{
@@ -557,11 +527,16 @@ export function create_client({ target, session, base, trailing_slash }) {
557527
}
558528

559529
/**
560-
* @param {import('types').CSRRoute} route
561530
* @param {import('./types').NavigationIntent} intent
562531
* @param {boolean} no_cache
563532
*/
564-
async function load_route(route, { id, url, path, routes }, no_cache) {
533+
async function load_route({ id, url, path, route }, no_cache) {
534+
if (!route) return;
535+
536+
if (load_cache.id === id && load_cache.promise) {
537+
return load_cache.promise;
538+
}
539+
565540
if (!no_cache) {
566541
const cached = cache.get(id);
567542
if (cached) return cached;
@@ -625,7 +600,7 @@ export function create_client({ target, session, base, trailing_slash }) {
625600
`${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
626601
{
627602
headers: {
628-
'x-sveltekit-load': /** @type {string} */ (shadow_key)
603+
'x-sveltekit-load': 'true'
629604
}
630605
}
631606
);
@@ -641,15 +616,7 @@ export function create_client({ target, session, base, trailing_slash }) {
641616
};
642617
}
643618

644-
if (res.status === 204) {
645-
if (route !== routes[routes.length - 1]) {
646-
// fallthrough
647-
return;
648-
}
649-
props = {};
650-
} else {
651-
props = await res.json();
652-
}
619+
props = res.status === 204 ? {} : await res.json();
653620
} else {
654621
status = res.status;
655622
error = new Error('Failed to load data');
@@ -672,9 +639,12 @@ export function create_client({ target, session, base, trailing_slash }) {
672639
}
673640

674641
if (node.loaded) {
642+
// TODO remove for 1.0
643+
// @ts-expect-error
675644
if (node.loaded.fallthrough) {
676-
return;
645+
throw new Error('fallthrough is no longer supported');
677646
}
647+
678648
if (node.loaded.error) {
679649
status = node.loaded.status;
680650
error = node.loaded.error;
@@ -811,6 +781,7 @@ export function create_client({ target, session, base, trailing_slash }) {
811781

812782
/** @param {URL} url */
813783
function owns(url) {
784+
// TODO now that we've got rid of fallthrough, check against routes immediately
814785
return url.origin === location.origin && url.pathname.startsWith(base);
815786
}
816787

@@ -821,7 +792,7 @@ export function create_client({ target, session, base, trailing_slash }) {
821792
/** @type {import('./types').NavigationIntent} */
822793
const intent = {
823794
id: url.pathname + url.search,
824-
routes: routes.filter(([pattern]) => pattern.test(path)),
795+
route: routes.find(([pattern]) => pattern.test(path)),
825796
url,
826797
path
827798
};
@@ -860,14 +831,14 @@ export function create_client({ target, session, base, trailing_slash }) {
860831
return;
861832
}
862833

863-
if (!owns(url)) {
834+
const pathname = normalize_path(url.pathname, trailing_slash);
835+
const normalized = new URL(url.origin + pathname + url.search + url.hash);
836+
837+
if (!owns(normalized)) {
864838
await native_navigation(url);
865839
}
866840

867-
const pathname = normalize_path(url.pathname, trailing_slash);
868-
url = new URL(url.origin + pathname + url.search + url.hash);
869-
870-
const intent = get_navigation_intent(url);
841+
const intent = get_navigation_intent(normalized);
871842

872843
update_scroll_positions(current_history_index);
873844

@@ -896,7 +867,7 @@ export function create_client({ target, session, base, trailing_slash }) {
896867
if (navigating_token !== current_navigating_token) return;
897868

898869
if (!navigating) {
899-
const navigation = { from, to: url };
870+
const navigation = { from, to: normalized };
900871
callbacks.after_navigate.forEach((fn) => fn(navigation));
901872

902873
stores.navigating.set(null);

packages/kit/src/runtime/client/types.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ export type NavigationIntent = {
3838
*/
3939
path: string;
4040
/**
41-
* The routes that could satisfy this navigation intent
41+
* The route that matches `path`
4242
*/
43-
routes: CSRRoute[];
43+
route: CSRRoute | undefined; // TODO i'm pretty sure we can make this required, and simplify some stuff
4444
/**
4545
* The destination URL
4646
*/

packages/kit/src/runtime/server/endpoint.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ export async function render_endpoint(event, mod) {
5959
return error(`${preface}: expected an object, got ${typeof response}`);
6060
}
6161

62+
// TODO remove for 1.0
63+
// @ts-expect-error
6264
if (response.fallthrough) {
63-
return;
65+
throw new Error('fallthrough is no longer supported');
6466
}
6567

6668
const { status = 200, body = {} } = response;

packages/kit/src/runtime/server/index.js

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,7 @@ export async function respond(request, options, state) {
165165
event.url = new URL(event.url.origin + normalized + event.url.search);
166166
}
167167

168-
// `key` will be set if this request came from a client-side navigation
169-
// to a page with a matching endpoint
170-
const key = request.headers.get('x-sveltekit-load');
171-
172168
for (const route of options.manifest._.routes) {
173-
if (key) {
174-
// client is requesting data for a specific endpoint
175-
if (route.type !== 'page') continue;
176-
if (route.key !== key) continue;
177-
}
178-
179169
const match = route.pattern.exec(decoded);
180170
if (!match) continue;
181171

@@ -188,7 +178,7 @@ export async function respond(request, options, state) {
188178
response = await render_endpoint(event, await route.shadow());
189179

190180
// loading data for a client-side transition is a special case
191-
if (key) {
181+
if (request.headers.has('x-sveltekit-load')) {
192182
if (response) {
193183
// since redirects are opaque to the browser, we need to repackage
194184
// 3xx responses as 200s with a custom header
@@ -205,12 +195,8 @@ export async function respond(request, options, state) {
205195
}
206196
}
207197
} else {
208-
// fallthrough
209198
response = new Response(undefined, {
210-
status: 204,
211-
headers: {
212-
'content-type': 'application/json'
213-
}
199+
status: 204
214200
});
215201
}
216202
}
@@ -257,6 +243,8 @@ export async function respond(request, options, state) {
257243

258244
return response;
259245
}
246+
247+
break;
260248
}
261249

262250
// if this request came direct from the user, rather than

packages/kit/src/runtime/server/page/load_node.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { coalesce_to_error } from '../../../utils/error.js';
2121
* status?: number;
2222
* error?: Error;
2323
* }} opts
24-
* @returns {Promise<import('./types').Loaded | undefined>} undefined for fallthrough
24+
* @returns {Promise<import('./types').Loaded>}
2525
*/
2626
export async function load_node({
2727
event,
@@ -50,7 +50,7 @@ export async function load_node({
5050
*/
5151
let set_cookie_headers = [];
5252

53-
/** @type {import('types').Either<import('types').Fallthrough, import('types').LoadOutput>} */
53+
/** @type {import('types').LoadOutput} */
5454
let loaded;
5555

5656
/** @type {import('types').ShadowData} */
@@ -63,8 +63,6 @@ export async function load_node({
6363
)
6464
: {};
6565

66-
if (shadow.fallthrough) return;
67-
6866
if (shadow.cookies) {
6967
set_cookie_headers.push(...shadow.cookies);
7068
}
@@ -325,8 +323,15 @@ export async function load_node({
325323
loaded = await module.load.call(null, load_input);
326324

327325
if (!loaded) {
326+
// TODO do we still want to enforce this now that there's no fallthrough?
328327
throw new Error(`load function must return a value${options.dev ? ` (${node.entry})` : ''}`);
329328
}
329+
330+
// TODO remove for 1.0
331+
// @ts-expect-error
332+
if (loaded.fallthrough) {
333+
throw new Error('fallthrough is no longer supported');
334+
}
330335
} else if (shadow.body) {
331336
loaded = {
332337
props: shadow.body
@@ -335,10 +340,6 @@ export async function load_node({
335340
loaded = {};
336341
}
337342

338-
if (loaded.fallthrough && !is_error) {
339-
return;
340-
}
341-
342343
// generate __data.json files when prerendering
343344
if (shadow.body && state.prerender) {
344345
const pathname = `${event.url.pathname.replace(/\/$/, '')}/__data.json`;
@@ -401,7 +402,11 @@ async function load_shadow_data(route, event, options, prerender) {
401402
if (!is_get) {
402403
const result = await handler(event);
403404

404-
if (result.fallthrough) return result;
405+
// TODO remove for 1.0
406+
// @ts-expect-error
407+
if (result.fallthrough) {
408+
throw new Error('fallthrough is no longer supported');
409+
}
405410

406411
const { status, headers, body } = validate_shadow_output(result);
407412
data.status = status;
@@ -426,7 +431,11 @@ async function load_shadow_data(route, event, options, prerender) {
426431
if (get) {
427432
const result = await get(event);
428433

429-
if (result.fallthrough) return result;
434+
// TODO remove for 1.0
435+
// @ts-expect-error
436+
if (result.fallthrough) {
437+
throw new Error('fallthrough is no longer supported');
438+
}
430439

431440
const { status, headers, body } = validate_shadow_output(result);
432441
add_cookies(/** @type {string[]} */ (data.cookies), headers);

packages/kit/test/apps/basics/src/routes/endpoint-output/empty.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,3 @@
22
export function get() {
33
return {};
44
}
5-
6-
/** @type {import('@sveltejs/kit').RequestHandler} */
7-
export function del() {
8-
return { fallthrough: true };
9-
}

0 commit comments

Comments
 (0)