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

Implement new layouts system #6174

Merged
merged 47 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2a237eb
start updating tests
Rich-Harris Aug 23, 2022
f47c3fb
fix patterns
Rich-Harris Aug 23, 2022
d145557
get unit tests passing
Rich-Harris Aug 23, 2022
de383f9
fixes
Rich-Harris Aug 23, 2022
faee0df
changeset
Rich-Harris Aug 23, 2022
d6fba37
prevent conflicts
Rich-Harris Aug 23, 2022
bb5ed6e
helpful error on named layouts
Rich-Harris Aug 23, 2022
02efc1e
fiddle about with data structures
Rich-Harris Aug 23, 2022
d2d3000
vanquish red squigglies
Rich-Harris Aug 23, 2022
bdaa71c
update some unit tests
Rich-Harris Aug 23, 2022
6419645
update tests
Rich-Harris Aug 23, 2022
6e04821
fixes
Rich-Harris Aug 23, 2022
ffb8a8a
fix
Rich-Harris Aug 23, 2022
907f080
merge master
Rich-Harris Aug 23, 2022
8229811
fix test
Rich-Harris Aug 23, 2022
0a72369
update write_types
Rich-Harris Aug 23, 2022
84e266f
precompute depth
Rich-Harris Aug 23, 2022
2bd5203
remove some now-obsolete tests
Rich-Harris Aug 23, 2022
0b52b1d
tidy up
Rich-Harris Aug 23, 2022
ce66e0d
import typescript at the top of the module, once
Rich-Harris Aug 23, 2022
74433e5
remove unused argument
Rich-Harris Aug 23, 2022
e9bf361
rename functions to make stuff slightly clearer
Rich-Harris Aug 24, 2022
3287b04
fix tests
Rich-Harris Aug 24, 2022
e4aa3a2
fix
Rich-Harris Aug 24, 2022
afe4cd4
make write_types truly incremental
Rich-Harris Aug 24, 2022
7322c10
reuse compact helper
Rich-Harris Aug 24, 2022
cb262ff
docs
Rich-Harris Aug 24, 2022
0ea2976
posixify
Rich-Harris Aug 24, 2022
fa5c424
more windows stuff
Rich-Harris Aug 24, 2022
0acae38
oops
Rich-Harris Aug 24, 2022
8ea3c40
optimise client manifest size a bit
Rich-Harris Aug 24, 2022
891b319
fix
Rich-Harris Aug 24, 2022
872a291
oops
Rich-Harris Aug 24, 2022
eef4be5
fall back to the server if loading fails
Rich-Harris Aug 24, 2022
314ebe3
small tweaks and some comments
dummdidumm Aug 24, 2022
5687d7a
fix filesystem router API check
dummdidumm Aug 24, 2022
4bf6184
comment, split into methods for better separation
dummdidumm Aug 24, 2022
cb47d7f
remove console.log
dummdidumm Aug 24, 2022
fbb40a7
docs tweak
dummdidumm Aug 24, 2022
0945723
Update packages/kit/src/utils/array.js
dummdidumm Aug 24, 2022
0018d97
add note about when to use groups
dummdidumm Aug 24, 2022
2d421b5
Update .changeset/many-forks-sniff.md
dummdidumm Aug 24, 2022
d39e33c
Update packages/kit/src/runtime/client/ambient.d.ts
dummdidumm Aug 24, 2022
1b106fa
Update packages/kit/src/runtime/client/parse.js
dummdidumm Aug 24, 2022
8bba42d
Update packages/kit/src/utils/array.js
dummdidumm Aug 24, 2022
dcf2ace
am I doing this right?
dummdidumm Aug 24, 2022
7c74aa9
Thank you Conduitry
dummdidumm Aug 24, 2022
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/many-forks-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] implement new layout system
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
95 changes: 43 additions & 52 deletions documentation/docs/04-advanced-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,75 +126,66 @@ assert.equal(

To express a `%` character, use `%25`, otherwise the result will be malformed.

### Named layouts
### Advanced layouts

Some parts of your app might need something other than the default layout. For these cases you can create _named layouts_...
By default, the _layout hierarchy_ mirrors the _route hierarchy_. In some cases, that might not be what you want.

```svelte
/// file: src/routes/+layout-foo.svelte
<div class="foo">
<slot></slot>
</div>
```
#### (group)

...and then use them by referencing the layout name (`foo`, in the example above) in the filename:
Perhaps you have some routes that are 'app' routes that should have one layout (e.g. `/dashboard` or `/item`), and others that are 'marketing' routes that should have a different layout (`/blog` or `/testimonials`). We can group these routes with a directory whose name is wrapped in parentheses — unlike normal directories, `(app)` and `(marketing)` do not affect the URL pathname of the routes inside them:

```svelte
/// file: src/routes/my-special-page/+page@foo.svelte
<h1>I am inside +layout-foo</h1>
```diff
src/routes/
+│ (app)/
│ ├ dashboard/
│ ├ item/
│ └ +layout.svelte
+│ (marketing)/
│ ├ about/
│ ├ testimonials/
│ └ +layout.svelte
├ admin/
└ +layout.svelte
```

> Named layout should only be referenced from Svelte files

Named layouts are very powerful, but it can take a minute to get your head round them. Don't worry if this doesn't make sense all at once.
You can also put a `+page` directly inside a `(group)`, for example if `/` should be an `(app)` or a `(marketing)` page.

#### Scoping
#### +page@

Named layouts can be created at any depth, and will apply to any components in the same subtree. For example, `+layout-foo` will apply to `/x/one` and `/x/two`, but not `/x/three` or `/four`:
Conversely, some routes of your app might need to break out of the layout hierarchy. Let's add an `/item/[id]/embed` route inside the `(app)` group from the previous example:

```bash
```diff
src/routes/
├ x/
│ ├ +layout-foo.svelte
│ ├ one/+page@foo.svelte # ✅ page has `@foo`
│ ├ two/+page@foo.svelte # ✅ page has `@foo`
│ └ three/+page.svelte # ❌ page does not have `@foo`
└ four/+page@foo.svelte # ❌ page has `@foo`, but +layout-foo is not 'in scope'
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
+│ │ │ │ └ +page.svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte
```

#### Inheritance chains

Layouts can themselves choose to inherit from named layouts, from the same directory or a parent directory. For example, `x/y/+layout@root.svelte` is the default layout for `/x/y` (meaning `/x/y/one`, `/x/y/two` and `/x/y/three` all inherit from it) because it has no name. Because it specifies `@root`, it will inherit directly from the nearest `+layout-root.svelte`, skipping `+layout.svelte` and `x/+layout.svelte`.
Ordinarily, this would inherit the root layout, the `(app)` layout, the `item` layout and the `[id]` layout. We can reset to one of those layouts by appending `@` followed by the segment name — or, for the root layout, the empty string. In this example, we can choose from `+page@.svelte`, `+page@(app).svelte`, `+page@item.svelte` or `+page@[id].svelte`:

```
```diff
src/routes/
├ x/
│ ├ y/
│ │ ├ +layout@root.svelte
│ │ ├ one/+page.svelte
│ │ ├ two/+page.svelte
│ │ └ three/+page.svelte
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
+│ │ │ │ └ +page@(app).svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
├ +layout.svelte
└ +layout-root.svelte
└ +layout.svelte
```

> In the case where `+layout-root.svelte` contains a lone `<slot />`, this effectively means we're able to 'reset' to a blank layout for any page or nested layout in the app by adding `@root`.
#### +layout@

If no parent is specified, a layout will inherit from the nearest default (i.e. unnamed) layout _above_ it in the tree. In some cases, it's helpful for a named layout to inherit from a default layout _alongside_ it in the tree, such as `+layout-root.svelte` inheriting from `+layout.svelte`. We can do this by explicitly specifying `@default`, allowing `/x/y/one` and siblings to use the app's default layout without using `x/+layout.svelte`:
Like pages, layouts can _themselves_ break out of their parent layout hierarchy, using the same technique. For example, a `+layout@.svelte` component would reset the hierarchy for all its child routes.

```diff
src/routes/
├ x/
│ ├ y/
│ │ ├ +layout@root.svelte
│ │ ├ one/+page.svelte
│ │ ├ two/+page.svelte
│ │ └ three/+page.svelte
│ └ +layout.svelte
├ +layout.svelte
-└ +layout-root.svelte
+└ +layout-root@default.svelte
```
#### When to use layout groups

> `default` is a reserved name — in other words, you can't have a `+layout-default.svelte` file.
Not all use cases are suited for layout grouping, nor should you feel compelled to use them. It might be that your use case would result in complex `(group)` nesting, or that you don't want to introduce a `(group)` for a single outlier. It's perfectly fine to use other means such as composition (reusable `load` functions or Svelte components) or if-statements to achieve what you want.
Copy link
Member

@dominikg dominikg Aug 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! this is so important to mention and myself overlooked it too, groups are powerful and can have footguns if not handled correctly, but as always, use the right tool for the job.

Should this be in a highlighted box?

And it would be great to elaborate on the reusable layouts or conditionals inside a single layout to establish best practices. Maybe linking to an examples repo?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I enhanced the section with a little code example. For more a repository or an entry in the recipes section on learn.svelte.dev would be good indeed - but we can do that later 😄

1 change: 0 additions & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
],
"scripts": {
"build": "npm run types",
"dev": "rollup -cw",
"lint": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",
"check": "tsc",
"check:all": "tsc && pnpm -r --filter=\"./**\" check",
Expand Down
41 changes: 28 additions & 13 deletions packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function create_builder({ config, build_data, prerendered, log }) {
/** @param {import('types').RouteData} route */
// TODO routes should come pre-filtered
function not_prerendered(route) {
const path = route.type === 'page' && !route.id.includes('[') && `/${route.id}`;
const path = route.page && !route.id.includes('[') && `/${route.id}`;
if (path) {
return !prerendered_paths.has(path) && !prerendered_paths.has(path + '/');
}
Expand Down Expand Up @@ -68,17 +68,31 @@ export function create_builder({ config, build_data, prerendered, log }) {
const { routes } = build_data.manifest_data;

/** @type {import('types').RouteDefinition[]} */
const facades = routes.map((route) => ({
id: route.id,
type: route.type,
segments: route.id.split('/').map((segment) => ({
dynamic: segment.includes('['),
rest: segment.includes('[...'),
content: segment
})),
pattern: route.pattern,
methods: route.type === 'page' ? ['GET'] : build_data.server.methods[route.file]
}));
const facades = routes.map((route) => {
const methods = new Set();

if (route.page) {
methods.add('SET');
}

if (route.endpoint) {
for (const method of build_data.server.methods[route.endpoint.file]) {
methods.add(method);
}
}

return {
id: route.id,
type: route.page ? 'page' : 'endpoint', // TODO change this if support pages+endpoints
segments: route.id.split('/').map((segment) => ({
dynamic: segment.includes('['),
rest: segment.includes('[...'),
content: segment
})),
pattern: route.pattern,
methods: Array.from(methods)
};
});

const seen = new Set();

Expand All @@ -102,8 +116,9 @@ export function create_builder({ config, build_data, prerendered, log }) {

// heuristic: if /foo/[bar] is included, /foo/[bar].json should
// also be included, since the page likely needs the endpoint
// TODO is this still necessary, given the new way of doing things?
filtered.forEach((route) => {
if (route.type === 'page') {
if (route.page) {
const endpoint = routes.find((candidate) => candidate.id === route.id + '.json');

if (endpoint) {
Expand Down
45 changes: 12 additions & 33 deletions packages/kit/src/core/generate_manifest/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { s } from '../../utils/misc.js';
import { parse_route_id } from '../../utils/routing.js';
import { get_mime_lookup } from '../utils.js';

/**
Expand Down Expand Up @@ -38,12 +37,11 @@ export function generate_manifest({ build_data, relative_path, routes, format =
assets.push(build_data.service_worker);
}

/** @param {import('types').PageNode | undefined} id */
const get_index = (id) => id && /** @type {LookupEntry} */ (bundled_nodes.get(id)).index;

const matchers = new Set();

// prettier-ignore
// String representation of
/** @type {import('types').SSRManifest} */
return `{
appDir: ${s(build_data.app_dir)},
assets: new Set(${s(assets)}),
Expand All @@ -55,39 +53,20 @@ export function generate_manifest({ build_data, relative_path, routes, format =
],
routes: [
${routes.map(route => {
const { pattern, names, types } = parse_route_id(route.id);

types.forEach(type => {
route.types.forEach(type => {
if (type) matchers.add(type);
});

if (route.type === 'page') {
return `{
type: 'page',
id: ${s(route.id)},
pattern: ${pattern},
names: ${s(names)},
types: ${s(types)},
errors: ${s(route.errors.map(get_index))},
layouts: ${s(route.layouts.map(get_index))},
leaf: ${s(get_index(route.leaf))}
}`.replace(/^\t\t/gm, '');
} else {
if (!build_data.server.vite_manifest[route.file]) {
// this is necessary in cases where a .css file snuck in —
// perhaps it would be better to disallow these (and others?)
return null;
}
if (!route.page && !route.endpoint) return;

return `{
type: 'endpoint',
id: ${s(route.id)},
pattern: ${pattern},
names: ${s(names)},
types: ${s(types)},
load: ${loader(`${relative_path}/${build_data.server.vite_manifest[route.file].file}`)}
}`.replace(/^\t\t/gm, '');
}
return `{
id: ${s(route.id)},
pattern: ${route.pattern},
names: ${s(route.names)},
types: ${s(route.types)},
page: ${s(route.page)},
endpoint: ${route.endpoint ? loader(`${relative_path}/${build_data.server.vite_manifest[route.endpoint.file].file}`) : 'null'}
}`;
}).filter(Boolean).join(',\n\t\t\t\t')}
],
matchers: async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/prerender/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export async function prerender() {
/** @type {import('types').ManifestData} */
const { routes } = (await import(pathToFileURL(manifest_path).href)).manifest._;
const entries = routes
.map((route) => (route.type === 'page' && !route.id.includes('[') ? `/${route.id}` : ''))
.map((route) => (route.page && !route.id.includes('[') ? `/${route.id}` : ''))
.filter(Boolean);

for (const entry of entries) {
Expand Down
Loading