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

[breaking] move hydrate and router options to handle #3397

Closed
wants to merge 16 commits into from
Closed
5 changes: 5 additions & 0 deletions .changeset/rare-eels-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] move `hydrate` and `router` options to `handle`
10 changes: 9 additions & 1 deletion documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export interface Response {
}

export interface ResolveOpts {
hydrate?: boolean;
router?: boolean;
ssr?: boolean;
}

Expand Down Expand Up @@ -80,9 +82,13 @@ export async function handle({ request, resolve }) {

You can add call multiple `handle` functions with [the `sequence` helper function](#modules-sveltejs-kit-hooks).

#### `resolve` options

`resolve` also supports a second, optional parameter that gives you more control over how the response will be rendered. That parameter is an object that can have the following fields:

- `ssr` — specifies whether the page will be loaded and rendered on the server.
- `hydrate` — specifies whether the code to [hydrate](#appendix-hydration) the page will be included on the page.
- `router` — specifies whether the code for the [client-side router](#appendix-routing) will be included on the page.
- `ssr` — specifies whether the page will be loaded and [rendered on the server](#appendix-ssr).

```js
/** @type {import('@sveltejs/kit').Handle} */
Expand All @@ -96,6 +102,8 @@ export async function handle({ request, resolve }) {
```

> Disabling [server-side rendering](#appendix-ssr) effectively turns your SvelteKit app into a [**single-page app** or SPA](#appendix-csr-and-spa). In most situations this is not recommended ([see appendix](#appendix-ssr)). Consider whether it's truly appropriate to disable it, and do so selectively rather than for all requests.
>
> If `hydrate` is `false`, SvelteKit will not add any JavaScript to the page at all. If `ssr` is disabled, `hydrate` must be `true` or no content will be rendered.

### handleError

Expand Down
30 changes: 1 addition & 29 deletions documentation/docs/11-page-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,7 @@ title: Page options

By default, SvelteKit will render any component first on the server and send it to the client as HTML. It will then render the component again in the browser to make it interactive in a process called **hydration**. For this reason, you need to ensure that components can run in both places. SvelteKit will then initialise a [**router**](#routing) that takes over subsequent navigations.

You can control each of these on a per-app or per-page basis. Note that each of the per-page settings use [`context="module"`](https://svelte.dev/docs#script_context_module), and only apply to page components, _not_ [layout](#layouts) components.

If both are specified, per-page settings override per-app settings in case of conflicts.

### router

SvelteKit includes a [client-side router](#appendix-routing) that intercepts navigations (from the user clicking on links, or interacting with the back/forward buttons) and updates the page contents, rather than letting the browser handle the navigation by reloading.

In certain circumstances you might need to disable [client-side routing](#appendix-routing) with the app-wide [`router` config option](#configuration-router) or the page-level `router` export:

```html
<script context="module">
export const router = false;
</script>
```

Note that this will disable client-side routing for any navigation from this page, regardless of whether the router is already active.

### hydrate

Ordinarily, SvelteKit [hydrates](#appendix-hydration) your server-rendered HTML into an interactive page. Some pages don't require JavaScript at all — many blog posts and 'about' pages fall into this category. In these cases you can skip hydration when the app boots up with the app-wide [`hydrate` config option](#configuration-hydrate) or the page-level `hydrate` export:

```html
<script context="module">
export const hydrate = false;
</script>
```

> If `hydrate` and `router` are both `false`, SvelteKit will not add any JavaScript to the page at all. If [server-side rendering](#hooks-handle) is disabled in `handle`, `hydrate` must be `true` or no content will be rendered.
Note that each of the per-page settings use [`context="module"`](https://svelte.dev/docs#script_context_module), and only apply to page components, _not_ [layout](#layouts) components. If both per-page and per-app settings are specified, the per-page settings override per-app settings in case of conflicts.

### prerender

Expand Down
5 changes: 0 additions & 5 deletions documentation/docs/14-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const config = {
onError: 'fail'
},
protocol: null,
router: true,
serviceWorker: {
register: true,
files: (filepath) => !/\.DS_STORE/.test(filepath)
Expand Down Expand Up @@ -224,10 +223,6 @@ See [Prerendering](#page-options-prerender). An object containing zero or more o

The protocol is assumed to be `'https'` (unless you're developing locally without the `--https` flag) unless [`config.kit.headers.protocol`](#configuration-headers) is set. If necessary, you can override it here.

### router

Enables or disables the client-side [router](#page-options-router) app-wide.

### serviceWorker

An object containing zero or more of the following values:
Expand Down
1 change: 0 additions & 1 deletion packages/kit/src/core/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export class App {
read,
root,
service_worker: ${has_service_worker ? "base + '/service-worker.js'" : 'null'},
router: ${s(config.kit.router)},
target: ${s(config.kit.target)},
template,
trailing_slash: ${s(config.kit.trailingSlash)}
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ test('fills in defaults', () => {
pages: undefined
},
protocol: null,
router: true,
router: null,
ssr: null,
target: null,
trailingSlash: 'never'
Expand Down Expand Up @@ -173,7 +173,7 @@ test('fills in partial blanks', () => {
pages: undefined
},
protocol: null,
router: true,
router: null,
ssr: null,
target: null,
trailingSlash: 'never'
Expand Down
9 changes: 8 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,14 @@ const options = object(

protocol: string(null),

router: boolean(true),
// TODO remove this for 1.0
router: validate(null, (input) => {
if (input !== undefined) {
throw new Error(
'config.kit.router has been removed — use the handle hook instead: https://kit.svelte.dev/docs#hooks-handle'
);
}
}),

serviceWorker: object({
register: boolean(true),
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/config/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ test('load default config (esm)', async () => {
pages: undefined
},
protocol: null,
router: true,
router: null,
ssr: null,
target: null,
trailingSlash: 'never'
Expand Down
9 changes: 0 additions & 9 deletions packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,15 +323,6 @@ export class Renderer {
this.loading.id = null;
this.autoscroll = true;
this.updating = false;

if (!this.router) return;

const leaf_node = navigation_result.state.branch[navigation_result.state.branch.length - 1];
if (leaf_node && leaf_node.module.router === false) {
this.router.disable();
} else {
this.router.enable();
}
}

/**
Expand Down
81 changes: 31 additions & 50 deletions packages/kit/src/runtime/client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ export class Router {
this.renderer = renderer;
renderer.router = this;

this.enabled = true;

// make it possible to reset focus
document.body.setAttribute('tabindex', '-1');

Expand Down Expand Up @@ -152,8 +150,6 @@ export class Router {

/** @param {MouseEvent} event */
addEventListener('click', (event) => {
if (!this.enabled) return;

// Adapted from https://github.com/visionmedia/page.js
// MIT license https://github.com/visionmedia/page.js#license
if (event.button || event.which !== 1) return;
Expand Down Expand Up @@ -211,26 +207,26 @@ export class Router {
});

addEventListener('popstate', (event) => {
if (event.state && this.enabled) {
// if a popstate-driven navigation is cancelled, we need to counteract it
// with history.go, which means we end up back here, hence this check
if (event.state['sveltekit:index'] === this.current_history_index) return;

this._navigate({
url: new URL(location.href),
scroll: event.state['sveltekit:scroll'],
keepfocus: false,
chain: [],
details: null,
accepted: () => {
this.current_history_index = event.state['sveltekit:index'];
},
blocked: () => {
const delta = this.current_history_index - event.state['sveltekit:index'];
history.go(delta);
}
});
}
if (!event.state) return;

// if a popstate-driven navigation is cancelled, we need to counteract it
// with history.go, which means we end up back here, hence this check
if (event.state['sveltekit:index'] === this.current_history_index) return;

this._navigate({
url: new URL(location.href),
scroll: event.state['sveltekit:scroll'],
keepfocus: false,
chain: [],
details: null,
accepted: () => {
this.current_history_index = event.state['sveltekit:index'];
},
blocked: () => {
const delta = this.current_history_index - event.state['sveltekit:index'];
history.go(delta);
}
});
});
}

Expand Down Expand Up @@ -273,35 +269,20 @@ export class Router {
) {
const url = new URL(href, get_base_uri(document));

if (this.enabled) {
return this._navigate({
url,
scroll: noscroll ? scroll_state() : null,
keepfocus,
chain,
details: {
state,
replaceState
},
accepted: () => {},
blocked: () => {}
});
}

location.href = url.href;
return new Promise(() => {
/* never resolves */
return this._navigate({
url,
scroll: noscroll ? scroll_state() : null,
keepfocus,
chain,
details: {
state,
replaceState
},
accepted: () => {},
blocked: () => {}
});
}

enable() {
this.enabled = true;
}

disable() {
this.enabled = false;
}

/**
* @param {URL} url
* @returns {Promise<import('./types').NavigationResult>}
Expand Down
25 changes: 18 additions & 7 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,21 @@ export async function respond(incoming, options, state = {}) {
print_error('path', 'pathname');
print_error('query', 'searchParams');

let ssr = true;
const resolve_opts = {
hydrate: options.hydrate,
router: true,
ssr: true
};

try {
return await options.hooks.handle({
request,
resolve: async (request, opts) => {
if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr);
if (resolve_opts.hydrate && opts && 'hydrate' in opts) {
resolve_opts.hydrate = /** @type {boolean} */ (opts.hydrate);
}
if (opts && 'router' in opts) resolve_opts.router = /** @type {boolean} */ (opts.router);
if (opts && 'ssr' in opts) resolve_opts.ssr = /** @type {boolean} */ (opts.ssr);

if (state.prerender && state.prerender.fallback) {
return await render_response({
Expand All @@ -97,11 +105,14 @@ export async function respond(incoming, options, state = {}) {
options,
state,
$session: await options.hooks.getSession(request),
page_config: { router: true, hydrate: true },
stuff: {},
status: 200,
branch: [],
ssr: false
resolve_opts: {
hydrate: true,
router: true,
ssr: false
}
});
}

Expand All @@ -119,7 +130,7 @@ export async function respond(incoming, options, state = {}) {
const response =
route.type === 'endpoint'
? await render_endpoint(request, route, match)
: await render_page(request, route, match, options, state, ssr);
: await render_page(request, route, match, options, state, resolve_opts);

if (response) {
// inject ETags for 200 responses, if the endpoint
Expand Down Expand Up @@ -177,7 +188,7 @@ export async function respond(incoming, options, state = {}) {
$session,
status: 404,
error: new Error(`Not found: ${request.url.pathname}`),
ssr
resolve_opts
});
}
}
Expand All @@ -196,7 +207,7 @@ export async function respond(incoming, options, state = {}) {
$session,
status: 500,
error,
ssr
resolve_opts
});
} catch (/** @type {unknown} */ e) {
const error = coalesce_to_error(e);
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { respond } from './respond.js';
* @param {RegExpExecArray} match
* @param {import('types/internal').SSRRenderOptions} options
* @param {import('types/internal').SSRRenderState} state
* @param {boolean} ssr
* @param {Required<import('types/hooks').ResolveOpts>} resolve_opts
* @returns {Promise<import('types/hooks').ServerResponse | undefined>}
*/
export async function render_page(request, route, match, options, state, ssr) {
export async function render_page(request, route, match, options, state, resolve_opts) {
if (state.initiator === route) {
// infinite request cycle detected
return {
Expand All @@ -31,7 +31,7 @@ export async function render_page(request, route, match, options, state, ssr) {
$session,
route,
params,
ssr
resolve_opts
});

if (response) {
Expand Down
Loading