Skip to content

Commit

Permalink
replace regex routes with fall-through (#583)
Browse files Browse the repository at this point in the history
* remove support for regex routes

* dont error on route clashes

* failing test for fallthrough routes

* fix SSR fallthrough tests

* fix types

* WIP implement fallthrough routes

* break if route matches

* enable all tests

* rename some methods for clarity

* rejig types, get existing tests passing

* prevent infinite loop with fallthroughs

* implement preload heuristic

* changeset

* document fallthrough routes

* add endpoint shadowing test

* remove out of date comment

* tweak docs
  • Loading branch information
Rich Harris committed Mar 22, 2021
1 parent 17e82eb commit 8a88fad
Show file tree
Hide file tree
Showing 56 changed files with 838 additions and 566 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-schools-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Replace regex routes with fallthrough routes
32 changes: 16 additions & 16 deletions documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Routing
---

At the heart of SvelteKit is a *filesystem-based router*. This means that the structure of your application is defined by the structure of your codebase — specifically, the contents of `src/routes`.
At the heart of SvelteKit is a _filesystem-based router_. This means that the structure of your application is defined by the structure of your codebase — specifically, the contents of `src/routes`.

> You can change this to a different directory by editing the [project config](#configuration).
Expand Down Expand Up @@ -74,23 +74,18 @@ export async function get(request, context) {

const article = await db.get(slug);

if (article !== null) {
if (article) {
return {
body: {
article
}
};
} else {
return {
status: 404,
body: {
error: 'Not found'
}
};
}
}
```

> Returning nothing is equivalent to an explicit 404 response.
Because this module only runs on the server (or when you build your site, if [prerendering](#prerendering)), you can freely access things like databases. (Don't worry about `$lib`, we'll get to that [later](#$lib).)

The second argument, `context`, is something you define during [setup](#setup), if necessary.
Expand All @@ -109,14 +104,14 @@ Since `delete` is a reserved word in JavaScript, DELETE requests are handled wit
>
> The `body` property of the request object exists in the case of POST requests. If you're posting form data, it will be a read-only version of the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object.

### Private modules

A filename that has a segment with a leading underscore, such as `src/routes/foo/_Private.svelte` or `src/routes/bar/_utils/cool-util.js`, is hidden from the router, but can be imported by files that are not.


### Advanced

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

```bash
Expand All @@ -134,12 +129,17 @@ A route can have multiple dynamic parameters, for example `src/routes/[category]
}
```

Finally, you can use a subset of regular expression syntax to control whether routes match or not:
#### Fallthrough routes

```bash
# matches /2021/04/25 but not /a/b/c or /1/2/3
src/routes/[year(\d{4})]/[month(\d{2})]/[day(\d{2})].svelte
Finally, if you have multiple routes that match a given path, SvelteKit will try each of them until one responds. For example if you have these routes...

```
src/routes/[baz].js
src/routes/[baz].svelte
src/routes/[qux].svelte
src/routes/foo-[bar].svelte
```

Because of technical limitations, the following characters cannot be used: `/`, `\`, `?`, `:`, `(` and `)`.
...and you navigate to `/foo-xyz`, then SvelteKit will first try `foo-[bar].svelte` because it is the best match, then will try `[baz].js` (which is also a valid match for `/foo-xyz`, but less specific), then `[baz].svelte` and `[qux].svelte` in alphabetical order (endpoints have higher precedence than pages). The first route that responds — a page that returns something from [`load`](#loading) or has no `load` function, or an endpoint that returns something — will handle the request.

If no page or endpoint responds to a request, SvelteKit will respond with a generic 404.
6 changes: 4 additions & 2 deletions documentation/docs/03-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ type Loaded = {

`load` is the SvelteKit equivalent of `getStaticProps` or `getServerSideProps` in Next.js or `asyncData` in Nuxt.js.

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.

> `load` only applies to components that define pages, not the components that they import.
### Input

The `load` function receives an object containing four fields — `page`, `fetch`, `session` and `context`.
The `load` function receives an object containing four fields — `page`, `fetch`, `session` and `context`.

#### page

Expand Down Expand Up @@ -124,4 +126,4 @@ If the `load` function returns a `props` object, the props will be passed to the

This will be merged with any existing `context` and passed to the `load` functions of subsequent layout and page components.

This only applies to layout components, _not_ page components.
This only applies to layout components, _not_ page components.
83 changes: 42 additions & 41 deletions packages/kit/src/core/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,10 @@ async function build_client({
start: path.resolve(cwd, client_entry_file)
};

manifest.pages.forEach((page) => {
page.parts.forEach((file) => {
const resolved = path.resolve(cwd, file);
const relative = path.relative(config.kit.files.routes, resolved);
input[path.join('pages', relative)] = resolved;
});
manifest.components.forEach((file) => {
const resolved = path.resolve(cwd, file);
const relative = path.relative(config.kit.files.routes, resolved);
input[path.join('pages', relative)] = resolved;
});

/** @type {any} */
Expand Down Expand Up @@ -306,42 +304,45 @@ async function build_server(
assets: ${s(manifest.assets)},
layout: ${stringify_component(manifest.layout)},
error: ${stringify_component(manifest.error)},
pages: [
${manifest.pages
.map((data) => {
const params = get_params(data.params);
const parts = data.parts.map(id => `{ id: ${s(id)}, load: components[${component_indexes.get(id)}] }`);
const js_deps = new Set(common_js_deps);
const css_deps = new Set(common_css_deps);
for (const file of data.parts) {
js_deps_by_file.get(file).forEach(asset => {
js_deps.add(asset);
});
css_deps_by_file.get(file).forEach(asset => {
css_deps.add(asset);
});
routes: [
${manifest.routes
.map((route) => {
if (route.type === 'page') {
const params = get_params(route.params);
const parts = route.parts.map(id => `{ id: ${s(id)}, load: components[${component_indexes.get(id)}] }`);
const js_deps = new Set(common_js_deps);
const css_deps = new Set(common_css_deps);
for (const file of route.parts) {
js_deps_by_file.get(file).forEach(asset => {
js_deps.add(asset);
});
css_deps_by_file.get(file).forEach(asset => {
css_deps.add(asset);
});
}
return `{
type: 'page',
pattern: ${route.pattern},
params: ${params},
parts: [${parts.join(', ')}],
css: [${Array.from(css_deps).map(s).join(', ')}],
js: [${Array.from(js_deps).map(s).join(', ')}]
}`;
} else {
const params = get_params(route.params);
const load = `() => import(${s(app_relative(route.file))})`;
return `{
type: 'endpoint',
pattern: ${route.pattern},
params: ${params},
load: ${load}
}`;
}
return `{
pattern: ${data.pattern},
params: ${params},
parts: [${parts.join(', ')}],
css: [${Array.from(css_deps).map(s).join(', ')}],
js: [${Array.from(js_deps).map(s).join(', ')}]
}`;
})
.join(',\n\t\t\t\t\t')}
],
endpoints: [
${manifest.endpoints
.map((data) => {
const params = get_params(data.params);
const load = `() => import(${s(app_relative(data.file))})`;
return `{ pattern: ${data.pattern}, params: ${params}, load: ${load} }`;
})
.join(',\n\t\t\t\t\t')}
]
Expand Down
69 changes: 35 additions & 34 deletions packages/kit/src/core/create_app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ function trim(str) {
* @param {string} base
*/
function generate_client_manifest(manifest_data, base) {
const page_ids = new Set(manifest_data.pages.map((page) => page.pattern.toString()));

const endpoints_to_ignore = manifest_data.endpoints.filter(
(route) => !page_ids.has(route.pattern.toString())
);

/** @type {Record<string, number>} */
const component_indexes = {};

Expand All @@ -71,29 +65,38 @@ function generate_client_manifest(manifest_data, base) {
.join(',\n\t\t\t\t')}
]`.replace(/^\t/gm, '');

const pages = `[
${manifest_data.pages
.map((page) => {
const params = page.params.length
? '(m) => ({ ' +
page.params
.map((param, i) => {
return param.startsWith('...')
? `${param.slice(3)}: d(m[${i + 1}])`
: `${param}: d(m[${i + 1}])`;
})
.join(', ') +
'})'
: 'empty';
return `{
// ${page.parts[page.parts.length - 1]}
pattern: ${page.pattern},
params: ${params},
parts: [${page.parts.map((part) => `components[${component_indexes[part]}]`).join(', ')}]
}`;
const routes = `[
${manifest_data.routes
.map((route) => {
if (route.type === 'page') {
const params = route.params.length
? '(m) => ({ ' +
route.params
.map((param, i) => {
return param.startsWith('...')
? `${param.slice(3)}: d(m[${i + 1}])`
: `${param}: d(m[${i + 1}])`;
})
.join(', ') +
'})'
: 'empty';
return `{
// ${route.parts[route.parts.length - 1]}
type: 'page',
pattern: ${route.pattern},
params: ${params},
parts: [${route.parts.map((part) => `components[${component_indexes[part]}]`).join(', ')}]
}`;
} else {
return `{
type: 'endpoint',
pattern: ${route.pattern},
reload: true
}`;
}
})
.join(',\n\n\t\t')}
.join(',\n\n\t\t\t')}
]`.replace(/^\t/gm, '');

return trim(`
Expand All @@ -104,11 +107,7 @@ function generate_client_manifest(manifest_data, base) {
const d = decodeURIComponent;
const empty = () => ({});
export const pages = ${pages};
export const ignore = [
${endpoints_to_ignore.map((route) => route.pattern).join(',\n\t\t\t')}
];
export const routes = ${routes};
export { layout };
`);
Expand All @@ -122,7 +121,9 @@ function generate_app(manifest_data, base) {
// TODO remove default layout altogether

const max_depth = Math.max(
...manifest_data.pages.map((page) => page.parts.filter(Boolean).length)
...manifest_data.routes.map((route) =>
route.type === 'page' ? route.parts.filter(Boolean).length : 0
)
);

const levels = [];
Expand Down
Loading

0 comments on commit 8a88fad

Please sign in to comment.